Skip to content

Commit 9eb7f17

Browse files
committed
wip
1 parent 4f06ed8 commit 9eb7f17

File tree

17 files changed

+671
-2
lines changed

17 files changed

+671
-2
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { ClosePositionProps } from '..'
2+
import type { UserState, UserBalances } from '../types'
3+
4+
type Props = {
5+
userState: Pick<UserState, 'debt' | 'stablecoin'> | undefined
6+
userBalances: UserBalances | undefined
7+
}
8+
9+
/**
10+
* Determines if a user can close their position and how much additional
11+
* stablecoin is required. Applies a 0.01% safety buffer to account for
12+
* potential contract execution edge cases where exact balance matching
13+
* might fail due to rounding or state changes between transaction
14+
* submission and execution.
15+
*
16+
* @returns Object containing required amount to close and missing amount
17+
* @example
18+
* ```typescript
19+
* const result = canClose({
20+
* userState: { debt: '100', stablecoin: '50' },
21+
* userBalances: { stablecoin: '60' }
22+
* })
23+
* // result: { requiredToClose: 50.005, missing: 9.995 }
24+
* ```
25+
*/
26+
export function checkCanClose({ userState, userBalances }: Props): ClosePositionProps['canClose'] {
27+
const { debt = '0', stablecoin = '0' } = userState ?? {}
28+
const { stablecoin: stablecoinBalance = '0' } = userBalances ?? {}
29+
30+
const requiredToClose = (parseFloat(debt) - parseFloat(stablecoin)) * 1.0001
31+
const missing = Math.max(0, requiredToClose - parseFloat(stablecoinBalance))
32+
33+
return { requiredToClose, missing }
34+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { ActionInfosProps } from '..'
2+
import type { UserState, Market } from '../types'
3+
import { parseFloatOptional } from './float'
4+
import { getTokens } from './market'
5+
6+
type Props = {
7+
market: Market | undefined
8+
userState: Pick<UserState, 'collateral' | 'stablecoin' | 'debt'> | undefined
9+
}
10+
11+
export function getCollateralInfo({ market, userState }: Props): ActionInfosProps['collateral'] {
12+
const { collateral } = userState ?? {}
13+
const amount = parseFloatOptional(collateral)
14+
const { symbol } = (market && getTokens(market))?.collateral || {}
15+
const borrowed = (amount && symbol && { symbol, amount }) || undefined
16+
17+
return {
18+
borrowed,
19+
leverage: undefined, // I don't know yet how to determine it so it's not available for now
20+
assetsToWithdraw: undefined, // Not sure what the point is atm, same as 'collateral' action info in loan group?
21+
}
22+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { Address } from '@ui-kit/utils'
2+
import type { ClosePositionProps } from '..'
3+
import type { UserState } from '../types'
4+
5+
type Token = { symbol: string; address: Address; usdRate: number | undefined }
6+
7+
type Props = {
8+
stablecoinToken: Token | undefined
9+
collateralToken: Token | undefined
10+
userState: Pick<UserState, 'collateral' | 'stablecoin' | 'debt'> | undefined
11+
}
12+
13+
/**
14+
* Calculates the recoverable collateral when closing a position
15+
*
16+
* This function determines what assets a user can recover when closing their position:
17+
* 1. Any remaining collateral tokens after position closure
18+
* 2. Excess stablecoin (if stablecoin balance exceeds outstanding debt)
19+
*
20+
* @returns Array of recoverable token objects
21+
*/
22+
export function getCollateralToRecover({
23+
stablecoinToken,
24+
collateralToken,
25+
userState,
26+
}: Props): ClosePositionProps['collateralToRecover'] {
27+
const { collateral, debt, stablecoin } = userState ?? {}
28+
29+
const result: ReturnType<typeof getCollateralToRecover> = []
30+
31+
// Add collateral tokens if user has any remaining after position closure
32+
const collateralBalance = (collateral && parseFloat(collateral)) || 0
33+
if (collateral && collateralToken && collateralBalance > 0) {
34+
result.push({
35+
symbol: collateralToken.symbol,
36+
address: collateralToken.address,
37+
amount: collateralBalance,
38+
usd: collateralBalance * (collateralToken.usdRate ?? 0),
39+
})
40+
}
41+
42+
// Add excess stablecoin (stablecoin balance minus outstanding debt) if positive
43+
const stablecoinBalance = (stablecoin && debt && parseFloat(stablecoin) - parseFloat(debt)) || 0
44+
if (stablecoin && stablecoinToken && stablecoinBalance > 0) {
45+
result.push({
46+
symbol: stablecoinToken.symbol,
47+
address: stablecoinToken.address,
48+
amount: stablecoinBalance,
49+
usd: stablecoinBalance * (stablecoinToken.usdRate ?? 0),
50+
})
51+
}
52+
53+
return result
54+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { ClosePositionProps } from '..'
2+
import { getTokens } from '../helpers/market'
3+
import type { UserState, Market } from '../types'
4+
5+
type Props = {
6+
market: Market | undefined
7+
userState: Pick<UserState, 'debt'> | undefined
8+
}
9+
10+
/**
11+
* Extracts debt token information for a user's position in a Llamma market.
12+
*
13+
* @returns Debt token object.
14+
*/
15+
export function getDebtToken({ market, userState }: Props): ClosePositionProps['debtToken'] {
16+
const { debt } = userState ?? {}
17+
const { stablecoin } = (market && getTokens(market)) || {}
18+
19+
if (!stablecoin || !stablecoin.address || debt == null) {
20+
return undefined
21+
}
22+
23+
return {
24+
symbol: stablecoin.symbol,
25+
address: stablecoin.address,
26+
amount: parseFloat(debt),
27+
}
28+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Parses a string to a float, returning undefined if the input is null/undefined,
3+
* or 0 if the parsing fails.
4+
*
5+
* @param x - The string to parse as a float
6+
* @returns The parsed float value, undefined if input is null/undefined, or 0 if parsing fails
7+
*/
8+
export const parseFloatOptional = (x?: string) => (x == null ? undefined : parseFloat(x) || 0)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { ActionInfosProps } from '..'
2+
import type { UserLoanDetails } from '../types'
3+
4+
type Props = {
5+
userLoanDetails: Pick<UserLoanDetails, 'healthFull'> | undefined
6+
}
7+
8+
/** Calculates the current health of a user's loan position */
9+
export function getHealthInfo({ userLoanDetails }: Props): ActionInfosProps['health'] {
10+
const { healthFull: healthFullRaw } = userLoanDetails ?? {}
11+
const healthFull = isNaN(parseFloat(healthFullRaw ?? '')) ? 0 : parseFloat(healthFullRaw ?? '')
12+
13+
return { current: healthFull }
14+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export { getDebtToken } from './debt-token'
2+
export { getCollateralToRecover } from './collateral-to-recover'
3+
export { checkCanClose } from './can-close'
4+
export { getHealthInfo } from './health-info'
5+
export { getLoanInfo } from './loan-info'
6+
export { getCollateralInfo } from './collateral-info'
7+
export * from './float'
8+
export * from './market'
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { ActionInfosProps } from '..'
2+
import type { LoanParameters, Market, UserState } from '../types'
3+
import type { getCollateralToRecover } from './collateral-to-recover'
4+
import { parseFloatOptional } from './float'
5+
import { getTokens } from './market'
6+
7+
type Props = {
8+
market: Market | undefined
9+
loanParameters: LoanParameters | undefined
10+
userState: Pick<UserState, 'collateral' | 'stablecoin' | 'debt'> | undefined
11+
collateralToRecover: ReturnType<typeof getCollateralToRecover> | undefined
12+
}
13+
14+
export function getLoanInfo({
15+
market,
16+
loanParameters,
17+
userState,
18+
collateralToRecover,
19+
}: Props): ActionInfosProps['loan'] {
20+
const { stablecoin } = userState ?? {}
21+
const amount = parseFloatOptional(stablecoin)
22+
const { symbol } = (market && getTokens(market))?.stablecoin || {}
23+
const debt = (amount && symbol && { symbol, amount }) || undefined
24+
25+
const borrowRate = loanParameters
26+
? {
27+
current: parseFloat(loanParameters.rate),
28+
next: parseFloatOptional(loanParameters.future_rate),
29+
}
30+
: undefined
31+
32+
return {
33+
borrowRate,
34+
debt,
35+
ltv: undefined, // I don't know yet how to determine it so it's not available for now
36+
collateral: (collateralToRecover ?? []).filter(
37+
(item): item is typeof item & { amount: number } => item.amount != null && item.amount > 0,
38+
),
39+
}
40+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets'
2+
import type { MintMarketTemplate } from '@curvefi/llamalend-api/lib/mintMarkets'
3+
import type { Address } from '@ui-kit/utils'
4+
import type { Market } from '../types'
5+
6+
/** Type guard to check if a market is a MintMarketTemplate */
7+
export function isMintMarket(market: Market): market is MintMarketTemplate {
8+
return 'coins' in market
9+
}
10+
11+
/** Type guard to check if a market is a LendMarketTemplate */
12+
export function isLendMarket(market: Market): market is LendMarketTemplate {
13+
return 'borrowed_token' in market
14+
}
15+
16+
/**
17+
* Extracts token information from a market template
18+
* @param market - The market template
19+
* @returns Object containing stablecoin and collateral token details with symbol, address, and decimals
20+
*/
21+
export function getTokens(market: Market) {
22+
const [stablecoinSymbol, collateralSymbol] = isMintMarket(market) ? market.coins : [market.borrowed_token.symbol]
23+
const [stablecoinAddress, collateralAddress] = market?.coinAddresses ?? [undefined, undefined]
24+
const [stablecoinDecimals, collateralDecimals] = market?.coinDecimals ?? [undefined, undefined]
25+
26+
return {
27+
stablecoin: {
28+
symbol: stablecoinSymbol,
29+
address: stablecoinAddress as Address,
30+
decimals: stablecoinDecimals,
31+
},
32+
collateral: {
33+
symbol: collateralSymbol,
34+
address: collateralAddress as Address,
35+
decimals: collateralDecimals,
36+
},
37+
}
38+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import apiLending from '@/lend/lib/apiLending'
2+
import { useLlammaMutation } from '@/llamalend/hooks/mutations/useLlammaMutation'
3+
import networks from '@/loan/networks'
4+
import type { ChainId } from '@/loan/types/loan.types'
5+
import type { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets'
6+
import type { MintMarketTemplate } from '@curvefi/llamalend-api/lib/mintMarkets'
7+
import { notify } from '@ui-kit/features/connect-wallet'
8+
import { useUserProfileStore } from '@ui-kit/features/user-profile'
9+
10+
const handlers = {
11+
pendingMessage: 'Closing position',
12+
onSuccess: ({ hash }: { hash?: string }) => hash && notify('Position closed', 'success'),
13+
onError: (error: Error) => notify(error.message, 'error'),
14+
}
15+
16+
/**
17+
* Hook for closing a mint position by liquidating the user's position
18+
* @param market - The mint market template to close the position for
19+
* @returns Mutation object for closing the position
20+
*/
21+
export function useClosePositionMint({ market }: { market: MintMarketTemplate | undefined }) {
22+
const maxSlippage = useUserProfileStore((state) => state.maxSlippage.crypto)
23+
24+
return useLlammaMutation({
25+
market,
26+
mutationFn: async (vars: void, { provider, curve, market }) => {
27+
const chainId = curve.chainId as ChainId
28+
const liquidateFn = networks[chainId].api.loanLiquidate.liquidate
29+
30+
return liquidateFn(provider, market, maxSlippage)
31+
},
32+
...handlers,
33+
})
34+
}
35+
36+
const { loanSelfLiquidation } = apiLending
37+
38+
/**
39+
* Hook for closing a lending position by self-liquidating the user's position
40+
* @param market - The lend market template to close the position for
41+
* @returns Mutation object for closing the position
42+
*/
43+
export function useClosePositionLend({ market }: { market: LendMarketTemplate | undefined }) {
44+
const maxSlippage = useUserProfileStore((state) => state.maxSlippage.crypto)
45+
46+
return useLlammaMutation({
47+
market,
48+
mutationFn: async (vars: void, { provider, market }) =>
49+
loanSelfLiquidation.selfLiquidate(provider, market, maxSlippage),
50+
...handlers,
51+
})
52+
}

0 commit comments

Comments
 (0)