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
35 changes: 22 additions & 13 deletions apps/main/src/llamalend/mutations/create-loan.mutation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCallback } from 'react'
import { useConfig } from 'wagmi'
import { Config as WagmiConfig, useConfig } from 'wagmi'
import { formatTokenAmounts } from '@/llamalend/llama.utils'
import type { LlamaMarketTemplate } from '@/llamalend/llamalend.types'
import { type LlammaMutationOptions, useLlammaMutation } from '@/llamalend/mutations/useLlammaMutation'
Expand Down Expand Up @@ -32,7 +32,7 @@ export type CreateLoanOptions = {
userAddress: Address | undefined
}

const approve = async (
export const approveLoan = async (
market: LlamaMarketTemplate,
{ userCollateral, userBorrowed, leverageEnabled }: BorrowMutation,
) =>
Expand All @@ -42,7 +42,7 @@ const approve = async (
: await market.leverage.createLoanApprove(userCollateral, userBorrowed)
: await market.createLoanApprove(userCollateral)) as Address[]

const create = async (
export const createLoan = async (
market: LlamaMarketTemplate,
{ debt, userCollateral, userBorrowed, leverageEnabled, range, slippage }: BorrowMutation,
) => {
Expand All @@ -55,6 +55,24 @@ const create = async (
return (await parent.createLoan(userCollateral, debt, range, +slippage)) as Address
}

const approveAndCreateLoan = async ({
config,
market,
...mutation
}: BorrowMutation & {
chainId: IChainId
market: MintMarketTemplate | LendMarketTemplate
config: WagmiConfig
}) => {
await waitForApproval({
isApproved: () => fetchBorrowCreateLoanIsApproved({ ...mutation, marketId: market.id }),
onApprove: () => approveLoan(market, mutation),
message: t`Approved loan creation`,
config,
})
return { hash: await createLoan(market, mutation) }
}

export const useCreateLoanMutation = ({
network,
network: { chainId },
Expand All @@ -69,16 +87,7 @@ export const useCreateLoanMutation = ({
network,
marketId,
mutationKey: [...rootKeys.userMarket({ chainId, marketId, userAddress }), 'create-loan'] as const,
mutationFn: async (mutation, { market }) => {
const params = { ...mutation, chainId, marketId }
await waitForApproval({
isApproved: () => fetchBorrowCreateLoanIsApproved(params),
onApprove: () => approve(market, mutation),
message: t`Approved loan creation`,
config,
})
return { hash: await create(market, mutation) }
},
mutationFn: async (mutation, { market }) => await approveAndCreateLoan({ ...mutation, chainId, market, config }),
validationSuite: borrowFormValidationSuite,
pendingMessage: (mutation, { market }) => t`Creating loan... ${formatTokenAmounts(market, mutation)}`,
successMessage: (mutation, { market }) => t`Loan created! ${formatTokenAmounts(market, mutation)}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,47 +37,43 @@ const convertNumbers = ({
collateralFromDebt: decimal(collateralFromDebt),
})

export const { useQuery: useCreateLoanExpectedCollateral, queryKey: createLoanExpectedCollateralQueryKey } =
queryFactory({
queryKey: ({
chainId,
marketId,
userBorrowed = '0',
userCollateral = '0',
debt,
slippage,
}: BorrowFormQueryParams) =>
[
...rootKeys.market({ chainId, marketId }),
'createLoanExpectedCollateral',
{ userCollateral },
{ userBorrowed },
{ debt },
{ slippage },
] as const,
queryFn: async ({
marketId,
userBorrowed = '0',
userCollateral = '0',
debt,
slippage,
}: BorrowFormQuery): Promise<BorrowExpectedCollateralResult> => {
const market = getLlamaMarket(marketId)
if (market instanceof LendMarketTemplate) {
return convertNumbers(
await market.leverage.createLoanExpectedCollateral(userCollateral, userBorrowed, debt, +slippage),
)
}
if (market.leverageV2.hasLeverage()) {
return convertNumbers(
await market.leverageV2.createLoanExpectedCollateral(userCollateral, userBorrowed, debt, +slippage),
)
}
export const {
useQuery: useCreateLoanExpectedCollateral,
queryKey: createLoanExpectedCollateralQueryKey,
fetchQuery: fetchLoanExpectedCollateral,
} = queryFactory({
queryKey: ({ chainId, marketId, userBorrowed = '0', userCollateral = '0', debt, slippage }: BorrowFormQueryParams) =>
[
...rootKeys.market({ chainId, marketId }),
'createLoanExpectedCollateral',
{ userCollateral },
{ userBorrowed },
{ debt },
{ slippage },
] as const,
queryFn: async ({
marketId,
userBorrowed = '0',
userCollateral = '0',
debt,
slippage,
}: BorrowFormQuery): Promise<BorrowExpectedCollateralResult> => {
const market = getLlamaMarket(marketId)
if (market instanceof LendMarketTemplate) {
return convertNumbers(
await market.leverage.createLoanExpectedCollateral(userCollateral, userBorrowed, debt, +slippage),
)
}
if (market.leverageV2.hasLeverage()) {
return convertNumbers(
await market.leverageV2.createLoanExpectedCollateral(userCollateral, userBorrowed, debt, +slippage),
)
}

assert(!+userBorrowed, `userBorrowed must be 0 for non-leverage mint markets`)
const { collateral, leverage, routeIdx } = await market.leverage.createLoanCollateral(userCollateral, debt)
return convertNumbers({ userCollateral, leverage, totalCollateral: collateral })
},
staleTime: '1m',
validationSuite: borrowQueryValidationSuite,
})
assert(!+userBorrowed, `userBorrowed must be 0 for non-leverage mint markets`)
const { collateral, leverage, routeIdx } = await market.leverage.createLoanCollateral(userCollateral, debt)
return convertNumbers({ userCollateral, leverage, totalCollateral: collateral })
},
staleTime: '1m',
validationSuite: borrowQueryValidationSuite,
})
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ export const maxReceiveValidation = createValidationSuite(
},
)

export const { useQuery: useCreateLoanMaxReceive, queryKey: createLoanMaxReceiveKey } = queryFactory({
export const {
useQuery: useCreateLoanMaxReceive,
queryKey: createLoanMaxReceiveKey,
fetchQuery: fetchLoanMaxReceive,
} = queryFactory({
queryKey: ({
chainId,
marketId,
Expand Down
3 changes: 2 additions & 1 deletion packages/curve-ui-kit/src/lib/model/query/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ export function queryFactory<
getQueryData: (params) => queryClient.getQueryData(queryKey(params)),
setQueryData: (params, data) => queryClient.setQueryData<TData>(queryKey(params), data),
prefetchQuery: (params, staleTime = 0) => queryClient.prefetchQuery({ ...getQueryOptions(params), staleTime }),
fetchQuery: (params, options) => queryClient.fetchQuery({ ...getQueryOptions(params), ...options }),
fetchQuery: (params, options = { staleTime: 0 }) =>
queryClient.fetchQuery({ ...getQueryOptions(params), ...options }),
/**
* Function that is like fetchQuery, but sets staleTime to 0 to ensure fresh data is fetched.
* Primary use case is for Zustand stores where want to both use queries and ensure freshness.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,63 +1,35 @@
import React, { useMemo } from 'react'
import React from 'react'
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
import { prefetchMarkets } from '@/lend/entities/chain/chain-query'
import { CreateLoanForm } from '@/llamalend/features/borrow/components/CreateLoanForm'
import type { OnBorrowFormUpdate } from '@/llamalend/features/borrow/types'
import type { CreateLoanOptions } from '@/llamalend/mutations/create-loan.mutation'
import networks from '@/loan/networks'
import { oneBool, oneValueOf } from '@cy/support/generators'
import { ComponentTestWrapper } from '@cy/support/helpers/ComponentTestWrapper'
import { createTestWagmiConfigFromVNet, createVirtualTestnet } from '@cy/support/helpers/tenderly'
import { LlamalendComponentWrapper } from '@cy/support/helpers/ComponentTestWrapper'
import { checkAccordion, getActionValue, TEST_LLAMA_MARKETS, useTestLlamaMarket } from '@cy/support/helpers/loan'
import { createVirtualTestnet } from '@cy/support/helpers/tenderly'
import { getRpcUrls } from '@cy/support/helpers/tenderly/vnet'
import { fundErc20, fundEth } from '@cy/support/helpers/tenderly/vnet-fund'
import { LOAD_TIMEOUT } from '@cy/support/ui'
import Box from '@mui/material/Box'
import Skeleton from '@mui/material/Skeleton'
import { useConnection } from '@ui-kit/features/connect-wallet/lib/ConnectionContext'
import { ConnectionProvider } from '@ui-kit/features/connect-wallet/lib/ConnectionProvider'
import { LlamaMarketType } from '@ui-kit/types/market'
import { Chain } from '@ui-kit/utils'

const chainId = Chain.Ethereum
const MARKETS = {
[LlamaMarketType.Mint]: {
id: 'lbtc',
collateralAddress: '0x8236a87084f8b84306f72007f36f2618a5634494' as const, // lbtc
collateral: '1',
borrow: '100',
},
[LlamaMarketType.Lend]: {
id: 'one-way-market-14',
collateralAddress: '0x4c9EDD5852cd905f086C759E8383e09bff1E68B3' as const, // USDe
collateral: '1',
borrow: '0.9',
},
}
const oneEthInWei = '0xde0b6b3a7640000' // 1 ETH=1e18 wei

const onUpdate: OnBorrowFormUpdate = async (form) => console.info('form updated', form)

type BorrowTabTestProps = { type: LlamaMarketType } & Pick<CreateLoanOptions, 'onCreated'>

const prefetch = () => prefetchMarkets({})

function BorrowTabTest({ type, onCreated }: BorrowTabTestProps) {
const { isHydrated, llamaApi } = useConnection()
const { id } = MARKETS[type]
const market = useMemo(
() =>
isHydrated &&
{ [LlamaMarketType.Mint]: llamaApi?.getMintMarket, [LlamaMarketType.Lend]: llamaApi?.getLendMarket }[type]?.(id),
[isHydrated, id, llamaApi?.getLendMarket, llamaApi?.getMintMarket, type],
)
const market = useTestLlamaMarket(type)
return market ? (
<CreateLoanForm market={market} networks={networks} chainId={chainId} onUpdate={onUpdate} onCreated={onCreated} />
) : (
<Skeleton />
)
}

describe('BorrowTabContents Component Tests', () => {
describe('Create loan form component tests', () => {
const privateKey = generatePrivateKey()
const { address } = privateKeyToAccount(privateKey)
const getVirtualNetwork = createVirtualTestnet((uuid) => ({
Expand All @@ -68,34 +40,23 @@ describe('BorrowTabContents Component Tests', () => {
}))

const marketType = oneValueOf(LlamaMarketType)
const { collateralAddress: tokenAddress, collateral, borrow } = MARKETS[marketType]
const { collateralAddress, collateral, borrow } = TEST_LLAMA_MARKETS[marketType]
const leverageEnabled = oneBool() // test with and without leverage

beforeEach(() => {
const vnet = getVirtualNetwork()
const { adminRpcUrl } = getRpcUrls(vnet)
fundEth({ adminRpcUrl, amountWei: oneEthInWei, recipientAddresses: [address] })
fundErc20({ adminRpcUrl, amountWei: oneEthInWei, tokenAddress, recipientAddresses: [address] })
fundEth({ adminRpcUrl, recipientAddresses: [address] })
fundErc20({ adminRpcUrl, tokenAddress: collateralAddress, recipientAddresses: [address] })
cy.log(`Funded some eth and collateral to ${address} in vnet ${vnet.slug}`)
})

const BorrowTabTestWrapper = (props: BorrowTabTestProps) => (
<ComponentTestWrapper config={createTestWagmiConfigFromVNet({ vnet: getVirtualNetwork(), privateKey })} autoConnect>
<ConnectionProvider
app="llamalend"
network={networks[chainId]}
onChainUnavailable={console.error}
hydrate={{ llamalend: prefetch }}
>
<Box sx={{ maxWidth: 500 }}>
<BorrowTabTest {...props} />
</Box>
</ConnectionProvider>
</ComponentTestWrapper>
<LlamalendComponentWrapper wagmi={{ vnet: getVirtualNetwork(), privateKey }} network={networks[chainId]}>
<BorrowTabTest {...props} />
</LlamalendComponentWrapper>
)

const getActionValue = (name: string) => cy.get(`[data-testid="${name}-value"]`, LOAD_TIMEOUT)

it(`calculates max debt and health for ${marketType} market ${leverageEnabled ? 'with' : 'without'} leverage`, () => {
const onCreated = cy.stub()
cy.mount(<BorrowTabTestWrapper type={marketType} onCreated={onCreated} />)
Expand All @@ -109,28 +70,7 @@ describe('BorrowTabContents Component Tests', () => {
if (leverageEnabled) {
cy.get('[data-testid="leverage-checkbox"]').click()
}

// open borrow advanced settings and check all fields
cy.contains('button', 'Health').click()

getActionValue('borrow-band-range')
.invoke(LOAD_TIMEOUT, 'text')
.should('match', /(\d(\.\d+)?) to (\d(\.\d+)?)/)
getActionValue('borrow-price-range')
.invoke(LOAD_TIMEOUT, 'text')
.should('match', /(\d(\.\d+)?) - (\d(\.\d+)?)/)
getActionValue('borrow-apr').contains('%')
getActionValue('borrow-apr-previous').contains('%')
getActionValue('borrow-ltv').contains('%')
getActionValue('borrow-n').contains('50')

if (leverageEnabled) {
getActionValue('borrow-price-impact').contains('%')
getActionValue('borrow-slippage').contains('%')
} else {
getActionValue('borrow-price-impact').should('not.exist')
getActionValue('borrow-slippage').should('not.exist')
}
checkAccordion(leverageEnabled)

cy.get('[data-testid="loan-form-errors"]').should('not.exist')
cy.get('[data-testid="create-loan-submit-button"]').click()
Expand Down
Loading
Loading