Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { ClosePositionProps } from '..'
import type { UserState, UserBalances } from '../types'

type Props = {
userState: Pick<UserState, 'debt' | 'stablecoin'> | undefined
userBalances: UserBalances | undefined
}

/**
* Determines if a user can close their position and how much additional
* stablecoin is required. Applies a 0.01% safety buffer to account for
* potential contract execution edge cases where exact balance matching
* might fail due to rounding or state changes between transaction
* submission and execution.
*
* @returns Object containing required amount to close and missing amount
* @example
* ```typescript
* const result = canClose({
* userState: { debt: '100', stablecoin: '50' },
* userBalances: { stablecoin: '60' }
* })
* // result: { requiredToClose: 50.005, missing: 9.995 }
* ```
*/
export function checkCanClose({ userState, userBalances }: Props): ClosePositionProps['canClose'] {
const { debt = '0', stablecoin = '0' } = userState ?? {}
const { borrowed = 0 } = userBalances ?? {}

const requiredToClose = (parseFloat(debt) - parseFloat(stablecoin)) * 1.0001
const missing = Math.max(0, requiredToClose - borrowed)

return { requiredToClose, missing }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { getTokens } from '@/llamalend/llama.utils'
import type { ActionInfosProps } from '..'
import type { UserState, Market } from '../types'
import { parseFloatOptional } from './float'

type Props = {
market: Market | undefined
userState: Pick<UserState, 'collateral' | 'stablecoin' | 'debt'> | undefined
}

export function getCollateralInfo({ market, userState }: Props): ActionInfosProps['collateral'] {
const { collateral } = userState ?? {}
const amount = parseFloatOptional(collateral)
const { symbol } = (market && getTokens(market))?.collateralToken || {}
const borrowed = (amount && symbol && { symbol, amount }) || undefined

return {
borrowed,
leverage: undefined, // I don't know yet how to determine it so it's not available for now
assetsToWithdraw: undefined, // Not sure what the point is atm, same as 'collateral' action info in loan group?
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { Address } from '@ui-kit/utils'
import type { ClosePositionProps } from '..'
import type { UserState } from '../types'

type Token = { symbol: string; address: Address; usdRate: number | undefined }

type Props = {
stablecoinToken: Token | undefined
collateralToken: Token | undefined
userState: Pick<UserState, 'collateral' | 'stablecoin' | 'debt'> | undefined
}

/**
* Calculates the recoverable collateral when closing a position
*
* This function determines what assets a user can recover when closing their position:
* 1. Any remaining collateral tokens after position closure
* 2. Excess stablecoin (if stablecoin balance exceeds outstanding debt)
*
* @returns Array of recoverable token objects
*/
export function getCollateralToRecover({
stablecoinToken,
collateralToken,
userState,
}: Props): ClosePositionProps['collateralToRecover'] {
const { collateral, debt, stablecoin } = userState ?? {}

const result: ReturnType<typeof getCollateralToRecover> = []

// Add collateral tokens if user has any remaining after position closure
const collateralBalance = (collateral && parseFloat(collateral)) || 0
if (collateral && collateralToken && collateralBalance > 0) {
result.push({
symbol: collateralToken.symbol,
address: collateralToken.address,
amount: collateralBalance,
usd: collateralBalance * (collateralToken.usdRate ?? 0),
})
}

// Add excess stablecoin (stablecoin balance minus outstanding debt) if positive
const stablecoinBalance = (stablecoin && debt && parseFloat(stablecoin) - parseFloat(debt)) || 0
if (stablecoin && stablecoinToken && stablecoinBalance > 0) {
result.push({
symbol: stablecoinToken.symbol,
address: stablecoinToken.address,
amount: stablecoinBalance,
usd: stablecoinBalance * (stablecoinToken.usdRate ?? 0),
})
}

return result
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { getTokens } from '@/llamalend/llama.utils'
import type { ClosePositionProps } from '..'
import type { UserState, Market } from '../types'

type Props = {
market: Market | undefined
userState: Pick<UserState, 'debt'> | undefined
}

/**
* Extracts debt token information for a user's position in a Llamma market.
*
* @returns Debt token object.
*/
export function getDebtToken({ market, userState }: Props): ClosePositionProps['debtToken'] {
const { debt } = userState ?? {}
const { borrowToken } = (market && getTokens(market)) || {}

if (!borrowToken || !borrowToken.address || debt == null) {
return undefined
}

return {
symbol: borrowToken.symbol,
address: borrowToken.address,
amount: parseFloat(debt),
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Parses a string to a float, returning undefined if the input is null/undefined,
* or 0 if the parsing fails.
*
* @param x - The string to parse as a float
* @returns The parsed float value, undefined if input is null/undefined, or 0 if parsing fails
*/
export const parseFloatOptional = (x?: string) => (x == null ? undefined : parseFloat(x) || 0)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ActionInfosProps } from '..'
import type { UserLoanDetails } from '../types'

type Props = {
userLoanDetails: Pick<UserLoanDetails, 'healthFull'> | undefined
}

/** Calculates the current health of a user's loan position */
export function getHealthInfo({ userLoanDetails }: Props): ActionInfosProps['health'] {
const { healthFull: healthFullRaw } = userLoanDetails ?? {}
const healthFull = isNaN(parseFloat(healthFullRaw ?? '')) ? 0 : parseFloat(healthFullRaw ?? '')

return { current: healthFull }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { getDebtToken } from './debt-token'
export { getCollateralToRecover } from './collateral-to-recover'
export { checkCanClose } from './can-close'
export { getHealthInfo } from './health-info'
export { getLoanInfo } from './loan-info'
export { getCollateralInfo } from './collateral-info'
export * from './float'
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { getTokens } from '@/llamalend/llama.utils'
import type { ActionInfosProps } from '..'
import type { LoanParameters, Market, UserState } from '../types'
import type { getCollateralToRecover } from './collateral-to-recover'
import { parseFloatOptional } from './float'

type Props = {
market: Market | undefined
loanParameters: LoanParameters | undefined
userState: Pick<UserState, 'collateral' | 'stablecoin' | 'debt'> | undefined
collateralToRecover: ReturnType<typeof getCollateralToRecover> | undefined
}

export function getLoanInfo({
market,
loanParameters,
userState,
collateralToRecover,
}: Props): ActionInfosProps['loan'] {
const { stablecoin } = userState ?? {}
const amount = parseFloatOptional(stablecoin)
const { symbol } = (market && getTokens(market))?.borrowToken || {}
const debt = (amount && symbol && { symbol, amount }) || undefined

const borrowRate = loanParameters
? {
current: parseFloat(loanParameters.rate),
next: parseFloatOptional(loanParameters.future_rate),
}
: undefined

return {
borrowRate,
debt,
ltv: undefined, // I don't know yet how to determine it so it's not available for now
collateral: (collateralToRecover ?? []).filter(
(item): item is typeof item & { amount: number } => item.amount != null && item.amount > 0,
),
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { ManageSoftLiquidation, type Props } from './ui/ManageSoftLiquidation'
export type { Props as ImproveHealthProps } from './ui/tabs/ImproveHealth'
export type { Props as ClosePositionProps } from './ui/tabs/ClosePosition'
export type { Props as ActionInfosProps } from './ui/ActionInfos'
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { Address } from 'viem'
import { useConfig } from 'wagmi'
import type { LlamaMarketTemplate } from '@/llamalend/llamalend.types'
import { useLlammaMutation } from '@/llamalend/mutations/useLlammaMutation'
import { notify } from '@ui-kit/features/connect-wallet'
import { useUserProfileStore } from '@ui-kit/features/user-profile'
import { waitForTransactionReceipt } from '@wagmi/core'

type ClosePositionOptions = {
market: LlamaMarketTemplate | undefined
}

/**
* Hook for closing a market position by self liquidating the user's position
* @param market - The llama market template to close the position for
* @returns Mutation object for closing the position
*/
export function useClosePosition({ market }: ClosePositionOptions) {
const maxSlippage = useUserProfileStore((state) => state.maxSlippage.crypto)
const config = useConfig()

return useLlammaMutation({
mutationKey: ['close-position', { marketId: market?.id }] as const,
market,
mutationFn: async (_: void, { market }) => {
const hash = (await market.selfLiquidate(+maxSlippage)) as Address
await waitForTransactionReceipt(config, { hash })
return { hash }
},
pendingMessage: 'Closing position',
onSuccess: ({ hash }) => {
notify('Position closed', 'success')
// TODO: invalidate specific queries to update the values in close position tab
},
onError: (error: Error) => notify(error.message, 'error'),
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Address } from 'viem'
import { useConfig } from 'wagmi'
import { getTokens } from '@/llamalend/llama.utils'
import type { LlamaMarketTemplate } from '@/llamalend/llamalend.types'
import { useLlammaMutation } from '@/llamalend/mutations/useLlammaMutation'
import { notify } from '@ui-kit/features/connect-wallet'
import { waitForTransactionReceipt } from '@wagmi/core'

type RepayOptions = {
market: LlamaMarketTemplate | undefined
}

export function useRepay({ market }: RepayOptions) {
const symbol = (market && getTokens(market))?.borrowToken?.symbol ?? '?'
const config = useConfig()

return useLlammaMutation({
mutationKey: ['repay', { marketId: market?.id }] as const,
market,
mutationFn: async ({ debt }: { debt: string }, { market }) => {
// TODO: doesn't support leveraged lend markets yet. not sure about mint markets either, that feature is under construction iirc.
// see const { loanRepay } = apiLending when you get to it
const hash = (await market.repay(+debt)) as Address
await waitForTransactionReceipt(config, { hash })
return { hash }
},
pendingMessage: ({ debt }) => `Repaying debt: ${debt} ${symbol}`,
onSuccess: ({ hash }, { debt }: { debt: string }) => {
notify(`Repaid debt: ${debt} ${symbol}`, 'success')
// TODO: invalidate specific queries to update the values in repay tab
},
onError: (error: Error) => notify(error.message, 'error'),
})
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,47 @@
import type { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets'
import type { MintMarketTemplate } from '@curvefi/llamalend-api/lib/mintMarkets'
import { Address } from '@ui-kit/utils'

export type Token = {
address: Address
symbol: string
chain?: string
balance?: number
}

export type Market = MintMarketTemplate | LendMarketTemplate

/**
* The types below are based on the types defined in lend.types.ts and loan.types.ts
* but cannot be imported directly as they reside in the app packages.
*
* To maintain clean architecture, we don't want the app package types
* to depend on these feature-specific types either.
*
* While a mapper from app types to feature types could be implemented, for now
* it's sufficient to manually mirror the necessary types. This allows app objects
* to be used directly within the feature without additional transformation.
*/

export type LoanParameters = {
future_rate?: string
rate: string
}

export type UserLoanDetails = {
/** User health in percent. There's a few different health values but this is the most important one. (Don't ask me why) */
healthFull: string
}

export type UserState = {
/** User's collateral token balance */
collateral: string
/** User's stablecoin balance */
stablecoin: string
/** User's outstanding debt in stablecoins */
debt: string
}

export type UserBalances = {
/** Available stablecoin balance in wallet */
borrowed: number
}
Loading
Loading