TypeScript SDK for interacting with the Kai Finance protocol (https://kai.finance).
Install:
pnpm add @kunalabs-io/kai
import { Amount, VAULTS } from '@kunalabs-io/kai'
import { Transaction } from '@mysten/sui/transactions'
import * as coin from './gen/sui/coin/functions' // bindings generated with `sui-client-gen`
async function deposit() {
const vault = VAULTS.suiUSDT // get a vault instance
const walletAddress = '...'
// deposit by manually acquiring a `Balance<T>` object
const tx = new Transaction()
const balance = '...' // get a `Balance<T>` object from somewhere
const ytBalance = vault.deposit(tx, balance) // returns balance of yield-bearing tokens (YT)
const ytCoin = coin.fromBalance(tx, vault.YT.typeName, ytBalance)
tx.transferObjects([ytCoin], walletAddress)
// alternatively, you can use the `vault.depositFromWallet()` method
// which will get the `Balance<T>` object from the wallet and deposit it
const amount = Amount.fromNum(100, vault.T.decimals) // 100 suiUSDT
await vault.depositFromWallet(tx, walletAddress, amount)
}
import { Amount, VAULTS } from '@kunalabs-io/kai'
import { Transaction } from '@mysten/sui/transactions'
import { SuiClient } from '@mysten/sui/client'
import * as coin from './gen/sui/coin/functions' // bindings generated with `sui-client-gen`
async function withdraw(client: SuiClient) {
const vault = VAULTS.suiUSDT
const walletAddress = '...'
// withdraw by manually acquiring a `Balance<YT>` object (vault's yield-bearing tokens)
const tx = new Transaction()
const ytBalance = '...' // get a `Balance<YT>` object from somewhere
const tBalance = vault.withdraw(tx, ytBalance, vault.getStrategies())
const tCoin = coin.fromBalance(tx, vault.T.typeName, tBalance)
tx.transferObjects([tCoin], walletAddress)
// alternatively, you can use the `vault.withdrawToWalletYT()` method
// which will get the `Balance<YT>` object from the wallet and withdraw it
const amountYT = Amount.fromNum(100, vault.YT.decimals) // 100 YT tokens
await vault.withdrawToWalletYT(tx, walletAddress, amountYT, vault.getStrategies())
// there's also a way to withdraw based on the amount of underlying asset (T)
const amountT = Amount.fromNum(100, vault.T.decimals) // 100 suiUSDT
await vault.withdrawToWalletT(client, tx, walletAddress, amountT, vault.getStrategies())
// you can use the `vault.withdrawToWalletAll()` method to withdraw everything from the vault
await vault.withdrawToWalletAll(client, tx, walletAddress, vault.getStrategies())
}
import { VAULTS, getVaultStats, getWalletVaultInfo } from '@kunalabs-io/kai'
import { SuiClient } from '@mysten/sui/client'
async function stats(client: SuiClient) {
const walletAddress = '...'
// first we fetch the Vault data
const vaultData = await VAULTS.suiUSDT.fetch(client)
// you can get the wallet Vault info by calling `getWalletVaultInfo()`
const walletInfo = await getWalletVaultInfo(client, walletAddress, vaultData)
console.log({
vault: VAULTS.suiUSDT.T.symbol,
ytBalance: walletInfo.ytBalance.toString(),
equity: walletInfo.equity.toString(),
})
// you can get the global Vault stats by calling `getVaultStats()`
const stats = getVaultStats(vaultData)
console.log({
vault: VAULTS.suiUSDT.T.symbol,
tvl: stats.tvl.toString(),
apr: stats.apr,
apy: stats.apy,
})
// or this helper function that gets all vault stats
const allStats = await getAllVaultStats(client)
}
import { Amount, POSITION_CONFIG_INFOS, Price, muldiv, pythPrice } from '@kunalabs-io/kai'
import { Transaction } from '@mysten/sui/transactions'
import { SuiClient } from '@mysten/sui/client'
async function create(client: SuiClient) {
const walletAddress = '...'
const configInfo = POSITION_CONFIG_INFOS.find(info => info.name === 'Bluefin suiUSDT/USDC')!
// or e.g. `ALL_POSITION_CONFIG_INFOS['0x888fcd428659608b1adb45790f65dfbac4352150f67d6312f0c0a5f1f9b04692']`
const config = await configInfo.fetchConfig(client)
const pool = await configInfo.fetchPool(client)
const [pioX, pioY] = await Promise.all([
configInfo.pioInfoX.fetchPioData(client),
configInfo.pioInfoY.fetchPioData(client),
])
const [, tickA] = pool.priceToClosestInitializablePrice(
Price.fromHuman(configInfo.X, configInfo.Y, '1')
)
const [, tickB] = pool.priceToClosestInitializablePrice(
Price.fromHuman(configInfo.X, configInfo.Y, '1.0002')
)
// principal deposit amounts
const UX = Amount.fromNum(100, config.info.X.decimals) // 100 suiUSDT
const UY = Amount.fromNum(100, config.info.Y.decimals) // 100 USDC
// we find the max liquidity (max leverage) and then adjust it for slippage
const maxL = config.findMaxPositionLiquidity({
tickA,
tickB,
UX,
UY,
poolPrice: pool.currentPrice(),
pythPrice: pythPrice(pioX, pioY),
})
const slippageBps = 50n // 0.5%
const l = muldiv(maxL, 10000n - slippageBps, 10000n)
const tx = new Transaction()
config.createPositionFromWallet(
tx,
{
tickA,
tickB,
liquidity: l,
UX,
UY,
},
walletAddress
) // see also `config.createPosition()`
}
import { Position, getAllWalletPositions } from '@kunalabs-io/kai'
import { SuiClient } from '@mysten/sui/client'
export async function positionInfo(client: SuiClient) {
// get all positions for a wallet
const walletAddress = '...'
const res = await getAllWalletPositions(client, walletAddress)
// or get an individual position
const positionId = '...'
const position = await Position.fetch(client, positionId)
// print some position info
const pool = await position.configInfo.fetchPool(client)
const [supplyPoolX, supplyPoolY] = await Promise.all([
position.configInfo.supplyPoolXInfo.fetch(client),
position.configInfo.supplyPoolYInfo.fetch(client),
])
const configData = await position.configInfo.fetchConfig(client)
const inRange = position.inRange(pool.currentTick())
const equity = position.calcEquityAmountsHuman({
poolPrice: pool.currentPrice(),
supplyPoolX,
supplyPoolY,
timestampMs: Date.now(),
})
const debt = position.calcDebtAmounts({
supplyPoolX,
supplyPoolY,
timestampMs: Date.now(),
})
const lpAmounts = position.calcLpAmounts(pool.currentPrice())
const marginLevel = position.calcMarginLevel({
currentPrice: pool.currentPrice(),
supplyPoolX,
supplyPoolY,
timestampMs: Date.now(),
})
const liquidationPrices = position.calcLiquidationPrices({
config: configData,
supplyPoolX,
supplyPoolY,
timestampMs: Date.now(),
})
const deleveragePrices = position.calcDeleveragePrices({
config: configData,
supplyPoolX,
supplyPoolY,
timestampMs: Date.now(),
})
const interestRates = position.getInterestRates({
supplyPoolX,
supplyPoolY,
timestampMs: Date.now(),
})
console.log({
positionId: position.id,
inRange,
equity,
debt: {
x: debt.x.toDecimal(),
y: debt.y.toDecimal(),
},
lpAmounts: {
x: lpAmounts.x.toDecimal(),
y: lpAmounts.y.toDecimal(),
},
marginLevel: marginLevel.toDP(4).toString(),
liquidationPrices: {
low: liquidationPrices[0].toString(),
high: liquidationPrices[1].toString(),
},
deleveragePrices: {
low: deleveragePrices[0].toString(),
high: deleveragePrices[1].toString(),
},
interestRates: {
x: interestRates.x.toString(),
y: interestRates.y.toString(),
},
})
}
Withdraws liquidity from the position.
import { Position, findPositionCapForWalletPosition } from '@kunalabs-io/kai'
import { SuiClient } from '@mysten/sui/client'
export async function withdraw(client: SuiClient) {
const walletAddress = '...'
const positionId = '...'
const position = await Position.fetch(client, positionId)
// find the position cap manually (alternatively it can be cached and passed in)
const positionCap = await findPositionCapForWalletPosition(client, position.id, walletAddress)
if (!positionCap) {
throw new Error(`PositionCap not found for position ${position.id}`)
}
const router = new AfRouterAdapter()
// or... `new CetusAggregatorAdapter(new CetusAggregatorClient())`
// The reduction (withdrawal) process consists of withdrawing the LP ammounts, any extra collateral, and
// repaying the debt based on the reduction factor. All this needs to happen in the same transaction.
//
// When the position is fully reduced, it becomes inactive and it's recommended to delete (close) it
// to claim the storage rebate. In order to delete it, the position must be fully reduced (factor = 1),
// and all pending collected fees and rewards must be collected. This can be done manually but it's
// somewhat intricate so `reduceAndMaybeDelete()` does this for you. See the implementation for more details.
const tx = await position.reduceAndMaybeDelete(
client,
router,
{
factor: new Decimal(0.5), // reduce the position by 50%
positionCapId: positionCap.id,
convertRewardsTo: USDC,
slippage: 0.01,
},
walletAddress
)
}
Withdraws pending rewards from the position.
import {
AfRouterAdapter,
CetusAggregatorAdapter,
Position,
USDC,
findPositionCapForWalletPosition,
} from '@kunalabs-io/kai'
import { SuiClient } from '@mysten/sui/client'
import { AggregatorClient as CetusAggregatorClient } from '@cetusprotocol/aggregator-sdk'
async function withdrawRewards(client: SuiClient) {
const walletAddress = '...'
const positionId = '...'
const position = await Position.fetch(client, positionId)
// find the position cap manually (alternatively it can be cached and passed in)
const positionCap = await findPositionCapForWalletPosition(client, position.id, walletAddress)
if (!positionCap) {
throw new Error(`PositionCap not found for position ${position.id}`)
}
const router = new AfRouterAdapter()
// or... `new CetusAggregatorAdapter(new CetusAggregatorClient())`
// see `position.withdrawAllRewards()` if you need something more custom
const tx = await position.withdrawAllRewardsConvertAndTransfer(
client,
router,
{
positionCapId: positionCap.id,
convertRewardsTo: USDC,
slippage: 0.01,
},
walletAddress
)
}
Compounds the position by collecting all rewards, swapping them to the correct ratio, and depositing them back into the position.
async function compound(client: SuiClient) {
const walletAddress = '...'
const positionId = '...'
const position = await Position.fetch(client, positionId)
// find the position cap manually (alternatively it can be cached and passed in)
const positionCap = await findPositionCapForWalletPosition(client, position.id, walletAddress)
if (!positionCap) {
throw new Error(`PositionCap not found for position ${position.id}`)
}
const pool = await position.configInfo.fetchPool(client)
const tx = await position.compound(
client,
{
pool,
positionCapId: positionCap.id,
slippage: 0.01,
},
walletAddress
)
}
The SDK provides functionality for monitoring, controlling, and executing liquidations of LP positions.
- Real-time position monitoring
- Automated liquidation execution
- Flash swap execution for liquidations
- OpenTelemetry metrics
The SDK liqudation consists of three main components:
-
Position Monitor (
RpcPositionMonitor
)- Continuously watches for positions that need liquidation
- Polls the chain at regular intervals
- Triggers events when positions need liquidation
-
Liquidation Controller (
LiquidationController
)- Acts as the decision-making component
- Receives positions from the monitor
- Determines how to handle liquidations
- Coordinates with the executor to perform liquidations
-
Flash Swap Executor (
FlashSwapExecutor
)- Handles the actual execution of liquidations
- Signs and sends transactions to the network
- Performs flash swaps for liquidation
The system follows a clear flow:
- Monitor detects positions needing liquidation
- Controller receives and processes these positions
- Executor carries out the actual liquidation transactions
This example repository demonstrates how to implement a liquidation bot for the Kai Finance protocol, including position monitoring, liquidation controller, and flash swap execution. The example should help you understand how to use the SDK's liquidation features in practice.