import { Transaction } from "@mysten/sui/transactions"; import { CoinInfo, MigrateOptions, Pool } from "../../types"; import { getPoolInfo } from "../PoolInfo"; import { nUSDC, haSui, pool, vSui, wUSDC, USDT, Sui } from "../../address"; import { borrowCoin, buildSwapPTBFromQuote, depositCoin, flashloan, getQuote, repayDebt, repayFlashLoan, withdrawCoin } from "../../libs/PTB"; /** * Retrieves the flashloan fee for a specified coin. * * @param coin - The target coin information. * @returns The flashloan fee rate (e.g., 0.003 represents 0.3%). */ export async function getFlashloanFee(coin: CoinInfo): Promise { const flashloanFeeUrl = "https://open-api.naviprotocol.io/api/navi/flashloan"; try { const response = await fetch(flashloanFeeUrl); const feeData = await response.json(); // Define the key for SUI based on its address format const suiKey = "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI"; if (coin.address === Sui.address) { if (!feeData.data[suiKey]) { throw new TypeError("Unable to retrieve flashloan fee for SUI."); } return Number(feeData.data[suiKey].flashloanFee); } else { if (!feeData.data[coin.address]) { throw new TypeError(`Unsupported coin: ${coin.symbol}`); } return feeData.data[coin.address].flashloanFee || 0; } } catch (error) { console.error(`Error fetching flashloan fee: ${(error as Error).message}`); throw error; } } /** * Converts an amount from display units to its smallest unit based on decimals. * * @param amount - The amount in display units. * @param decimal - The number of decimal places for the coin. * @returns The amount in the smallest unit. */ function toMinUnit(amount: number, decimal: number): number { return Math.floor(amount * Math.pow(10, decimal)); } /** * Converts an amount from the smallest unit to display units based on decimals. * * @param amount - The amount in the smallest unit. * @param decimal - The number of decimal places for the coin. * @returns The amount in display units. */ function fromMinUnit(amount: number, decimal: number): number { return amount / Math.pow(10, decimal); } /** * Calculates an appropriate borrow amount for a flash loan. * * Formula: * R = (formCoinPrice * 10^toDecimal) / (toCoinPrice * 10^formDecimal) * Borrow_amount = (formCoinAmountInMin * R * (1 - slippage)) / (1 + feeRate) * * @param formCoinAmountInMin - The user's input amount in the smallest unit. * @param formCoinPrice - The price of the formCoin (in USD or any base currency). * @param toCoinPrice - The price of the toCoin (in USD or any base currency). * @param feeRate - The flash loan fee rate (e.g., 0.003 for 0.3%). * @param slippage - The slippage during the swap process (e.g., 0.01 for 1%). * @param formDecimal - The number of decimals of formCoin. * @param toDecimal - The number of decimals of toCoin. * @returns The calculated borrow amount in the smallest unit of toCoin. * @throws Will throw an error if input validations fail or if the calculated borrow amount is non-positive. */ function calculateBorrowAmount( formCoinAmountInMin: number, formCoinPrice: number, toCoinPrice: number, feeRate: number, slippage: number, formDecimal: number, // Number of decimals for formCoin toDecimal: number // Number of decimals for toCoin ): [number, number, number] { // Input validation if ( [formCoinAmountInMin, formCoinPrice, toCoinPrice, feeRate, slippage, formDecimal, toDecimal].some( (param) => typeof param !== 'number' || isNaN(param) ) ) { throw new Error("All input parameters must be valid numbers."); } if (formCoinAmountInMin <= 0) { throw new Error("The amount of formCoin must be greater than 0."); } if (formCoinPrice <= 0 || toCoinPrice <= 0) { throw new Error("Coin prices must be greater than 0."); } if (feeRate < 0 || slippage < 0) { throw new Error("Fee rate and slippage cannot be negative."); } if (!Number.isInteger(formDecimal) || !Number.isInteger(toDecimal) || formDecimal < 0 || toDecimal < 0) { throw new Error("Decimals must be non-negative integers."); } // Calculate the exchange rate R, considering decimal differences const R = (formCoinPrice * Math.pow(10, toDecimal)) / (toCoinPrice * Math.pow(10, formDecimal)); // Calculate the borrow amount const borrowAmount = (formCoinAmountInMin * R * (1 - slippage)) / (1 + feeRate); // Ensure the borrow amount is positive if (borrowAmount <= 0) { throw new Error("The calculated borrow amount must be positive."); } // Floor the borrow amount to the nearest smallest unit const finalBorrowAmount = Math.floor(borrowAmount); const shouldSwapAmount = Math.floor(formCoinAmountInMin * R) const expectAmount = Math.floor(shouldSwapAmount * (1 + feeRate)) return [finalBorrowAmount,shouldSwapAmount, expectAmount]; } async function calcRealPriceFromSui(originalPrice: number, targetCoin: CoinInfo, migrateOptions?:MigrateOptions) { let quoteResult; try { quoteResult = await getQuote( Sui.address, targetCoin.address, 1e9, migrateOptions?.apiKey, { baseUrl: migrateOptions?.baseUrl }); console.log("Quote obtained:", quoteResult); } catch (error) { console.error(`Failed to get quote: ${(error as Error).message}`); throw error; } const amountOutNum = Number(quoteResult?.amount_out ); return (1e9 / amountOutNum) * originalPrice; } /** * Migrates supply from one coin to another using a flashloan. * * @param txb - The transaction builder. * @param fromCoin - The supply coin to migrate from. * @param toCoin - The supply coin to migrate to. * @param amount - The from coin amount min unit to migrate. * @param address - The user's address. * @param migrateOptions - Optional migration parameters. * @returns The updated transaction builder. */ export async function migrateSupplyPTB( txb: Transaction, fromCoin: CoinInfo, toCoin: CoinInfo, amount: number, address: string, migrateOptions?: MigrateOptions ) { if (fromCoin.address === toCoin.address) { throw new Error("fromCoin and toCoin cannot be the same."); } if (amount <= 0) { throw new Error("Amount must be greater than 0."); } const allPools = await getPoolInfo(); const fromPoolConfig = pool[fromCoin.symbol as keyof Pool]; const toPoolConfig = pool[toCoin.symbol as keyof Pool]; const fromPoolInfo = (allPools as { [key: string]: any })[String(fromPoolConfig.assetId)]; const toPoolInfo = (allPools as { [key: string]: any })[String(toPoolConfig.assetId)]; let fromCoinPrice = fromPoolInfo.tokenPrice; let toCoinPrice = toPoolInfo.tokenPrice; if (fromCoin.symbol === 'vSui' || fromCoin.symbol === 'haSui') { fromCoinPrice = await calcRealPriceFromSui(fromCoinPrice, fromCoin, migrateOptions); } if (toCoin.symbol === 'vSui' || toCoin.symbol === 'haSui') { toCoinPrice = await calcRealPriceFromSui(toCoinPrice, toCoin, migrateOptions); } const toCoinFlashloanFee = await getFlashloanFee(toCoin); const formCoinAmountInMin = amount; // const formCoinAmountInMin = toMinUnit(amount, fromCoin.decimal); const slippage = migrateOptions?.slippage ?? 0.005; const [borrowAmountInMin, shouldSwapAmount, _noUse] = calculateBorrowAmount( formCoinAmountInMin, Number(fromCoinPrice), Number(toCoinPrice), toCoinFlashloanFee, slippage, fromCoin.decimal, toCoin.decimal ); console.log(`Borrow Amount Calculation: User Input Amount: ${amount} ${fromCoin.symbol} From Coin: ${fromCoin.symbol} (Price: ${fromCoinPrice}) To Coin: ${toCoin.symbol} (Price: ${toCoinPrice}) Fee Rate: ${toCoinFlashloanFee * 100}% Slippage: ${slippage * 100}% Borrow Amount in Min Units: ${borrowAmountInMin} should Amount in Min Units: ${shouldSwapAmount} Borrow Amount in normal: ${fromMinUnit(borrowAmountInMin, toCoin.decimal)} should Amount in normal: ${fromMinUnit(shouldSwapAmount, toCoin.decimal)} `); const [flashloanBalance, receipt] = await flashloan(txb, toPoolConfig, borrowAmountInMin); const [flashCoin]: any = txb.moveCall({ target: '0x2::coin::from_balance', arguments: [flashloanBalance], typeArguments: [toCoin.address], }); await depositCoin(txb, toPoolConfig, flashCoin, borrowAmountInMin); const [withdrawnFromCoin] = await withdrawCoin(txb, fromPoolConfig, formCoinAmountInMin); let quote; try { quote = await getQuote( fromCoin.address, toCoin.address, formCoinAmountInMin, migrateOptions?.apiKey, { baseUrl: migrateOptions?.baseUrl } ); console.log("Quote obtained:", quote); } catch (error) { console.error(`Error in getQuote: ${(error as Error).message}`); throw error; } const minAmountOut = Math.floor(shouldSwapAmount * (1 - slippage)); const swappedToCoin = await buildSwapPTBFromQuote(address, txb, minAmountOut, withdrawnFromCoin as any, quote); const repayBalance = txb.moveCall({ target: '0x2::coin::into_balance', arguments: [swappedToCoin], typeArguments: [toCoin.address], }); const [leftBalance] = await repayFlashLoan(txb, toPoolConfig, receipt, repayBalance); const [extraCoin] = txb.moveCall({ target: '0x2::coin::from_balance', arguments: [leftBalance], typeArguments: [toCoin.address], }); txb.transferObjects([extraCoin], address); return txb; } /** * Migrates borrowing from one coin to another using a flashloan. * * @param txb - The transaction builder. * @param fromCoin - The borrow coin to migrate from. * @param toCoin - The borrow coin to migrate to. * @param amount - The from coin amount in min unit to migrate. * @param address - The user's address. * @param migrateOptions - Optional migration parameters. * @returns The updated transaction builder. */ export async function migrateBorrowPTB( txb: Transaction, fromCoin: CoinInfo, toCoin: CoinInfo, amount: number, address: string, migrateOptions?: MigrateOptions ) { if (fromCoin.address === toCoin.address) { throw new Error("fromCoin and toCoin cannot be the same."); } if (amount <= 0) { throw new Error("Amount must be greater than 0."); } const allPools = await getPoolInfo(); const fromPoolConfig = pool[fromCoin.symbol as keyof Pool]; const toPoolConfig = pool[toCoin.symbol as keyof Pool]; const fromPoolInfo = (allPools as { [key: string]: any })[String(fromPoolConfig.assetId)]; const toPoolInfo = (allPools as { [key: string]: any })[String(toPoolConfig.assetId)]; let fromCoinPrice = fromPoolInfo.tokenPrice; let toCoinPrice = toPoolInfo.tokenPrice; if (fromCoin.symbol === 'vSui' || fromCoin.symbol === 'haSui') { fromCoinPrice = await calcRealPriceFromSui(fromCoinPrice, fromCoin, migrateOptions); } if (toCoin.symbol === 'vSui' || toCoin.symbol === 'haSui') { toCoinPrice = await calcRealPriceFromSui(toCoinPrice, toCoin, migrateOptions); } const toCoinFlashloanFee = await getFlashloanFee(toCoin); const fromCoinAmountInMin = amount; // const formCoinAmountInMin = toMinUnit(amount, fromCoin.decimal); const slippage = migrateOptions?.slippage ?? 0.005; const [borrowAmountInMin, shouldSwapAmount, loanAmount] = calculateBorrowAmount( fromCoinAmountInMin, Number(fromCoinPrice), Number(toCoinPrice), toCoinFlashloanFee, slippage, fromCoin.decimal, toCoin.decimal ); console.log(`Borrow Amount Calculation: User Input Amount: ${amount} ${fromCoin.symbol} From Coin: ${fromCoin.symbol} (Price: ${fromCoinPrice}) To Coin: ${toCoin.symbol} (Price: ${toCoinPrice}) Fee Rate: ${toCoinFlashloanFee * 100}% Slippage: ${slippage * 100}% Borrow Amount in Min Units: ${borrowAmountInMin} should Amount in Min Units: ${shouldSwapAmount} Borrow Amount in normal: ${fromMinUnit(borrowAmountInMin, toCoin.decimal)} should Amount in normal: ${fromMinUnit(shouldSwapAmount, toCoin.decimal)} loanAmount Amount in Units: ${loanAmount} loanAmount Amount in normal: ${fromMinUnit(loanAmount, toCoin.decimal)} `); const [flashloanBalance, receipt] = await flashloan(txb, toPoolConfig, shouldSwapAmount); const [flashCoin]: any = txb.moveCall({ target: '0x2::coin::from_balance', arguments: [flashloanBalance], typeArguments: [toCoin.address], }); let quote; try { quote = await getQuote( toCoin.address, fromCoin.address, shouldSwapAmount, migrateOptions?.apiKey, { baseUrl: migrateOptions?.baseUrl } ); console.log("Quote obtained:", quote); } catch (error) { console.error(`Failed to get quote: ${(error as Error).message}`); throw error; } const minAmountOut = Math.floor(Number(quote.amount_out) * (1 - slippage)); const swappedFromCoin = await buildSwapPTBFromQuote(address, txb, minAmountOut, flashCoin, quote); const swapCoinValue = txb.moveCall({ target: '0x2::coin::value', arguments: [swappedFromCoin], typeArguments: [fromCoin.address], }); await repayDebt(txb, fromPoolConfig, swappedFromCoin, swapCoinValue); const [borrowedToCoin] = await borrowCoin(txb, toPoolConfig, loanAmount); const repayBalance = txb.moveCall({ target: '0x2::coin::into_balance', arguments: [borrowedToCoin], typeArguments: [toCoin.address], }); const [leftBalance] = await repayFlashLoan(txb, toPoolConfig, receipt, repayBalance); const [extraCoin] = txb.moveCall({ target: '0x2::coin::from_balance', arguments: [leftBalance], typeArguments: [toCoin.address], }); txb.transferObjects([extraCoin], address); return txb; } /** * Migrates both supply and borrow positions using flashloans. * * @param txb - The transaction builder. * @param supplyFromCoin - The coin to supply from. * @param supplyToCoin - The coin to supply to. * @param borrowFromCoin - The coin to borrow from. * @param borrowToCoin - The coin to borrow to. * @param supplyAmount - The amount to supply. * @param borrowAmount - The amount to borrow. * @param address - The user's address. * @param migrateOptions - Optional migration parameters. * @returns The updated transaction builder. */ export async function migratePTB( txb: Transaction, supplyFromCoin: CoinInfo, supplyToCoin: CoinInfo, borrowFromCoin: CoinInfo, borrowToCoin: CoinInfo, supplyAmount: number, borrowAmount: number, address: string, migrateOptions?: MigrateOptions ) { try { await migrateSupplyPTB(txb, supplyFromCoin, supplyToCoin, supplyAmount, address, migrateOptions); console.log("Supply migration completed successfully."); } catch (error) { console.error(`Error in migrateSupplyPTB: ${(error as Error).message}`); } try { await migrateBorrowPTB(txb, borrowFromCoin, borrowToCoin, borrowAmount, address, migrateOptions); console.log("Borrow migration completed successfully."); } catch (error) { console.error(`Error in migrateBorrowPTB: ${(error as Error).message}`); } return txb; } /** * Retrieves the list of coins that can be migrated. * * @returns An array of migratable coins. */ export function getMigratableCoins(): CoinInfo[] { return [Sui, wUSDC, nUSDC, vSui, USDT]; }