navi-sdk
Version:
The NAVI SDK Client provides tools for interacting with the Sui blockchain networks, designed for handling transactions, accounts, and smart contracts efficiently. This documentation covers the setup, account management, and transaction handling within th
453 lines (395 loc) • 16.2 kB
text/typescript
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<number> {
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];
}