Skip to content

Commit 2d37b6f

Browse files
0xAlunara0xAlunara
authored andcommitted
wip
1 parent c0b8358 commit 2d37b6f

File tree

17 files changed

+674
-2
lines changed

17 files changed

+674
-2
lines changed
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+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { useMutation } from '@tanstack/react-query'
2+
import { useConnection, useWallet } from '@ui-kit/features/connect-wallet'
3+
import { notify } from '@ui-kit/features/connect-wallet'
4+
import type { Market } from '../../llamalend.types'
5+
6+
/**
7+
* Throws an error if the data contains an error string object.
8+
*
9+
* This function checks if the contract execution result contains an error object, which typically
10+
* indicates that the transaction failed even though the preceding operations succeeded
11+
* (a "failed successfully" scenario).
12+
*
13+
* @param data - The mutation result data to check for errors
14+
* @throws {Error} Throws an error if data contains an error string that is not a user rejection
15+
*
16+
* @remarks
17+
* - The error string content is not standardized and does not have a guaranteed form
18+
* - Making errors prettier is considered out of scope
19+
* - Successfully determining if an error was simply a user cancelling a transaction is out of scope
20+
* - User rejection errors (containing "User rejected the request") are ignored and do not throw
21+
*/
22+
function throwIfError(data: unknown) {
23+
// If the data contains an error object, it probably means the transaction failed even though nothing
24+
// before that was going wrong. In other words, 'failed successfully'.
25+
if (data != null && typeof data === 'object' && 'error' in data) {
26+
// Not fail proof, as the content of the error string is not standardized
27+
// and does not have a guaranteed form. Making errors prettier is out of scope and succesfully
28+
// determined if it was simple a user cancelling a transaction is out of scope as well.
29+
if (typeof data.error === 'string' && !data.error.includes('User rejected the request')) {
30+
throw new Error(data.error)
31+
}
32+
}
33+
}
34+
35+
/**
36+
* Custom context specially for the mutation function.
37+
* Usually mutations don't have a context, but they do
38+
* when you use our specialized llamma mutation.
39+
*/
40+
type MutationFnContext<TMarket extends Market> = {
41+
provider: NonNullable<ReturnType<typeof useWallet>['provider']>
42+
curve: NonNullable<ReturnType<typeof useConnection>['llamaApi']>
43+
market: TMarket
44+
}
45+
46+
/** Normal mutation context outside of the special context for mutation functions (MutationFnContext) */
47+
type Context = {
48+
pendingNotification: ReturnType<typeof notify>
49+
}
50+
51+
type LlammaMutationOptions<TMarket extends Market, TVariables, TData, TContext extends Context> = {
52+
/** The llamma market instance */
53+
market: TMarket | undefined
54+
/** Function that performs the mutation operation */
55+
mutationFn: (variables: TVariables, context: MutationFnContext<TMarket>) => Promise<TData>
56+
/** Message to display during pending state */
57+
pendingMessage: string | ((variables: TVariables) => string)
58+
/** Callback executed on successful mutation */
59+
onSuccess?: (data: TData, variables: TVariables, context: TContext) => void
60+
/** Callback executed on mutation error */
61+
onError?: (error: Error, variables: TVariables, context: TContext) => void
62+
/** Callback executed before mutation starts */
63+
onMutate?: (variables: TVariables) => TContext
64+
/** Callback executed after mutation settles (success or error) */
65+
onSettled?: (data: TData | undefined, error: Error | null, variables: TVariables, context: TContext) => void
66+
}
67+
68+
/**
69+
* Custom hook for handling llamma-related mutations with automatic wallet and API validation
70+
* Could argue for a refactor to validate with vest like we do for queries, but I'd rather keep
71+
* it simple for now. Maybe another time, for now we're just doing a quick llamma specialization
72+
* with simple throwing errors.
73+
*/
74+
export function useLlammaMutation<TMarket extends Market, TVariables, TData, TContext extends Context>({
75+
market,
76+
mutationFn,
77+
pendingMessage,
78+
onSuccess,
79+
onError,
80+
onMutate,
81+
onSettled,
82+
}: LlammaMutationOptions<TMarket, TVariables, TData, TContext>) {
83+
const { provider } = useWallet()
84+
const { llamaApi: curve } = useConnection()
85+
86+
return useMutation({
87+
mutationFn: (variables: TVariables) => {
88+
if (!provider) throw new Error('Missing provider')
89+
if (!curve) throw new Error('Missing lending api')
90+
if (!market) throw new Error('Missing llamma market')
91+
92+
return mutationFn(variables, { provider, curve, market })
93+
},
94+
onMutate: (variables) => {
95+
const pendingNotification = notify(
96+
typeof pendingMessage === 'function' ? pendingMessage(variables) : pendingMessage,
97+
'pending',
98+
)
99+
100+
const userContext = onMutate?.(variables)
101+
return { pendingNotification, ...userContext } as TContext
102+
},
103+
onSuccess: (data, variables, context) => {
104+
throwIfError(data)
105+
onSuccess?.(data, variables, context)
106+
},
107+
onError: (error, variables, context) => {
108+
// Log the full error, otherwise we lose the original stack trace
109+
console.error('Llamma mutation error:', error)
110+
onError?.(error, variables, context!)
111+
},
112+
onSettled: (data, error, variables, context) => {
113+
context?.pendingNotification?.dismiss()
114+
onSettled?.(data, error, variables, context!)
115+
},
116+
})
117+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import apiLending from '@/lend/lib/apiLending'
2+
import { useLlammaMutation } from '@/llamalend/hooks/mutations/useLlammaMutation'
3+
import networks from '@/loan/networks'
4+
import { getLoanDecreaseActiveKey } from '@/loan/store/createLoanDecreaseSlice'
5+
import type { ChainId } from '@/loan/types/loan.types'
6+
import type { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets'
7+
import type { MintMarketTemplate } from '@curvefi/llamalend-api/lib/mintMarkets'
8+
import { notify } from '@ui-kit/features/connect-wallet'
9+
import { getTokens } from '@ui-kit/features/manage-soft-liquidation/helpers'
10+
import type { Market } from '@ui-kit/features/manage-soft-liquidation/types'
11+
12+
const handlers = (symbol: string = '?') => ({
13+
pendingMessage: ({ debt }: { debt: string }) => `Repaying debt: ${debt} ${symbol}`,
14+
onSuccess: ({ hash }: { hash?: string }, { debt }: { debt: string }) =>
15+
hash && notify(`Repaid debt: ${debt} ${symbol}`, 'success'),
16+
onError: (error: Error) => notify(error.message, 'error'),
17+
})
18+
19+
/**
20+
* Extracts the stablecoin symbol from a market
21+
* @param market - The market to extract the symbol from
22+
* @returns The stablecoin symbol or undefined if market is not available
23+
*/
24+
const getSymbol = (market: Market | undefined) => (market && getTokens(market))?.stablecoin?.symbol
25+
26+
export function useRepayMint({ market }: { market: MintMarketTemplate | undefined }) {
27+
const symbol = getSymbol(market)
28+
29+
return useLlammaMutation({
30+
market,
31+
mutationFn: ({ debt }: { debt: string }, { provider, curve, market }) => {
32+
const chainId = curve.chainId as ChainId
33+
const repayFn = networks[chainId].api.loanDecrease.repay
34+
const activeKey = getLoanDecreaseActiveKey(market, debt, false)
35+
36+
return repayFn(activeKey, provider, market, debt, false)
37+
},
38+
...handlers(symbol),
39+
})
40+
}
41+
42+
const { loanRepay } = apiLending
43+
44+
export function useRepayLend({ market }: { market: LendMarketTemplate | undefined }) {
45+
const symbol = getSymbol(market)
46+
47+
return useLlammaMutation({
48+
market,
49+
mutationFn: ({ debt }: { debt: string }, { provider, curve, market }) => {
50+
throw new Error('Not yet implemented')
51+
52+
// loanRepay.repay(
53+
// activeKey,
54+
// provider,
55+
// market,
56+
// stateCollateral,
57+
// userCollateral,
58+
// userBorrowed,
59+
// isFullRepay,
60+
// maxSlippage,
61+
// swapRequired,
62+
// )
63+
64+
return Promise.resolve({ hash: '' })
65+
},
66+
...handlers(symbol),
67+
})
68+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import type { INetworkName } from '@curvefi/llamalend-api/lib/interfaces'
2+
import type { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets'
3+
import type { MintMarketTemplate } from '@curvefi/llamalend-api/lib/mintMarkets'
24

35
export type NetworkEnum = INetworkName
46

57
export type NetworkUrlParams = { network: NetworkEnum }
8+
9+
export type Market = MintMarketTemplate | LendMarketTemplate
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { useCallback, useMemo, useState } from 'react'
2+
import { useChainId } from 'wagmi'
3+
import { useClosePositionMint } from '@/llamalend/hooks/mutations/useClosePosition'
4+
import { useRepayMint } from '@/llamalend/hooks/mutations/useRepay'
5+
import { useUserLoanDetails } from '@/loan/hooks/useUserLoanDetails'
6+
import useStore from '@/loan/store/useStore'
7+
import type { MintMarketTemplate } from '@curvefi/llamalend-api/lib/mintMarkets'
8+
import {
9+
ManageSoftLiquidation,
10+
type ActionInfosProps,
11+
type ClosePositionProps,
12+
type ImproveHealthProps,
13+
} from '@ui-kit/features/manage-soft-liquidation'
14+
import {
15+
checkCanClose,
16+
getCollateralInfo,
17+
getCollateralToRecover,
18+
getDebtToken,
19+
getHealthInfo,
20+
getLoanInfo,
21+
getTokens,
22+
parseFloatOptional,
23+
} from '@ui-kit/features/manage-soft-liquidation/helpers'
24+
import { useTokenUsdRate } from '@ui-kit/lib/model/entities/token-usd-rate'
25+
26+
type Props = {
27+
market: MintMarketTemplate | undefined
28+
}
29+
30+
/** Main component hooking up the manage soft liquidation card. Preferably only this parent component uses loan app specific imports. */
31+
export const LoanManageSoftLiq = ({ market }: Props) => {
32+
// Internal state
33+
const [repayBalance, setRepayBalance] = useState(0)
34+
35+
// User data
36+
const llammaId = market?.id || ''
37+
const userLoanDetails = useUserLoanDetails(llammaId)
38+
const userBalancesRaw = useStore((state) => state.loans.userWalletBalancesMapper[llammaId])
39+
const userBalances = useMemo(() => userBalancesRaw ?? {}, [userBalancesRaw])
40+
const { userState } = userLoanDetails ?? {}
41+
42+
// Loan data
43+
const loanDetails = useStore((state) => state.loans.detailsMapper[llammaId])
44+
const { parameters: loanParameters } = loanDetails ?? {}
45+
46+
// Tokens with usd rates
47+
const chainId = useChainId()
48+
const tokens = market && getTokens(market)
49+
const { data: stablecoinUsdRate } = useTokenUsdRate(
50+
{ chainId, tokenAddress: tokens?.stablecoin?.address },
51+
!!tokens?.stablecoin?.address,
52+
)
53+
54+
const { data: collateralTokenUsdRate } = useTokenUsdRate(
55+
{ chainId, tokenAddress: tokens?.collateral?.address },
56+
!!tokens?.collateral?.address,
57+
)
58+
59+
const stablecoinToken = useMemo(
60+
() => tokens?.stablecoin && { ...tokens.stablecoin, usdRate: stablecoinUsdRate },
61+
[tokens?.stablecoin, stablecoinUsdRate],
62+
)
63+
64+
const collateralToken = useMemo(
65+
() => tokens?.collateral && { ...tokens.collateral, usdRate: collateralTokenUsdRate },
66+
[tokens?.collateral, collateralTokenUsdRate],
67+
)
68+
69+
// Properties
70+
const debtToken = useMemo(() => getDebtToken({ market, userState }), [market, userState])
71+
const collateralToRecover = useMemo(
72+
() =>
73+
getCollateralToRecover({
74+
userState,
75+
stablecoinToken,
76+
collateralToken,
77+
}),
78+
[collateralToken, stablecoinToken, userState],
79+
)
80+
const canClose = useMemo(() => checkCanClose({ userState, userBalances }), [userBalances, userState])
81+
82+
// Improve health tab
83+
const repay = useRepayMint({ market })
84+
const onRepay = useCallback(() => {
85+
repay.mutate({ debt: repayBalance.toString() })
86+
}, [repay, repayBalance])
87+
88+
const improveHealthTab: ImproveHealthProps = {
89+
debtToken,
90+
userBalance: parseFloatOptional(userBalances?.stablecoin),
91+
status: repay.isPending ? 'repay' : 'idle',
92+
onDebtBalance: setRepayBalance,
93+
onRepay,
94+
onApproveLimited: () => {},
95+
onApproveInfinite: () => {},
96+
}
97+
98+
// Close position tab
99+
const closePosition = useClosePositionMint({ market })
100+
const onClose = useCallback(() => {
101+
closePosition.mutate()
102+
}, [closePosition])
103+
104+
const closePositionTab: ClosePositionProps = {
105+
debtToken,
106+
collateralToRecover,
107+
canClose,
108+
status: closePosition.isPending ? 'close' : 'idle',
109+
onClose,
110+
}
111+
112+
// Action infos
113+
const health = getHealthInfo({ userLoanDetails })
114+
const loan = getLoanInfo({
115+
market,
116+
loanParameters,
117+
userState,
118+
collateralToRecover,
119+
})
120+
const collateral = getCollateralInfo({ market, userState })
121+
122+
const actionInfos: ActionInfosProps = {
123+
health,
124+
loan,
125+
collateral,
126+
transaction: {
127+
estimatedTxCost: { eth: 0.0024, gwei: 0.72, dollars: 0.48 },
128+
},
129+
}
130+
131+
return (
132+
<ManageSoftLiquidation
133+
actionInfos={actionInfos}
134+
improveHealth={improveHealthTab}
135+
closePosition={closePositionTab}
136+
/>
137+
)
138+
}

0 commit comments

Comments
 (0)