From 656f5303b5512353dbdb2ba2c521c3c2796d4b84 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Wed, 10 Dec 2025 10:34:15 +0100 Subject: [PATCH 01/10] fix: increase lite network timeouts --- tests/cypress/e2e/main/basic.cy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cypress/e2e/main/basic.cy.ts b/tests/cypress/e2e/main/basic.cy.ts index 1ddc7c613b..01e233c90c 100644 --- a/tests/cypress/e2e/main/basic.cy.ts +++ b/tests/cypress/e2e/main/basic.cy.ts @@ -1,5 +1,5 @@ import { oneOf } from '@cy/support/generators' -import { LOAD_TIMEOUT } from '@cy/support/ui' +import { API_LOAD_TIMEOUT, LOAD_TIMEOUT } from '@cy/support/ui' describe('Basic Access Test', () => { const path = oneOf('/', '/dex') @@ -40,7 +40,7 @@ describe('Basic Access Test', () => { cy.visitWithoutTestConnector('dex/corn/pools') cy.title(LOAD_TIMEOUT).should('equal', 'Pools - Curve') cy.url().should('include', '/dex/corn/pools') - cy.contains(/LBTC\/wBTCN/i, LOAD_TIMEOUT).should('be.visible') + cy.contains(/LBTC\/wBTCN/i, API_LOAD_TIMEOUT).should('be.visible') }) it('shows 404 on /dex/:network/pools/404', () => { From af96f34f9b5cb68edfc74507bfb5ef22bd8f8608 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Thu, 11 Dec 2025 22:21:04 +0100 Subject: [PATCH 02/10] fix: query validation --- .../borrow/components/CreateLoanForm.tsx | 2 -- .../components/CreateLoanInfoAccordion.tsx | 13 +++----- .../borrow/hooks/useCreateLoanForm.tsx | 1 - .../borrow-create-loan-approved.query.ts | 2 +- .../create-loan-approve-estimate-gas.query.ts | 2 +- .../create-loan/create-loan-bands.query.ts | 2 +- .../create-loan-expected-collateral.query.ts | 2 +- .../create-loan/create-loan-health.query.ts | 2 +- .../create-loan-price-impact.query.ts | 2 +- .../create-loan/create-loan-prices.query.ts | 8 +++-- .../create-loan-route-image.query.ts | 2 +- .../validation/borrow-fields.validation.ts | 6 ++++ .../queries/validation/borrow.validation.ts | 32 +++++++++---------- 13 files changed, 39 insertions(+), 37 deletions(-) diff --git a/apps/main/src/llamalend/features/borrow/components/CreateLoanForm.tsx b/apps/main/src/llamalend/features/borrow/components/CreateLoanForm.tsx index 4be08fc370..5c492ea218 100644 --- a/apps/main/src/llamalend/features/borrow/components/CreateLoanForm.tsx +++ b/apps/main/src/llamalend/features/borrow/components/CreateLoanForm.tsx @@ -68,7 +68,6 @@ export const CreateLoanForm = ({ creationError, txHash, formErrors, - tooMuchDebt, isApproved, } = useCreateLoanForm({ market, network, preset, onCreated }) const setRange = useCallback((range: number) => form.setValue('range', range, setValueOptions), [form]) @@ -84,7 +83,6 @@ export const CreateLoanForm = ({ values={values} collateralToken={collateralToken} borrowToken={borrowToken} - tooMuchDebt={tooMuchDebt} networks={networks} onSlippageChange={(value) => form.setValue('slippage', value, setValueOptions)} /> diff --git a/apps/main/src/llamalend/features/borrow/components/CreateLoanInfoAccordion.tsx b/apps/main/src/llamalend/features/borrow/components/CreateLoanInfoAccordion.tsx index 923d4c3934..c87167052e 100644 --- a/apps/main/src/llamalend/features/borrow/components/CreateLoanInfoAccordion.tsx +++ b/apps/main/src/llamalend/features/borrow/components/CreateLoanInfoAccordion.tsx @@ -25,7 +25,6 @@ export const CreateLoanInfoAccordion = ({ values: { range, slippage, leverageEnabled }, collateralToken, borrowToken, - tooMuchDebt, networks, onSlippageChange, }: { @@ -33,20 +32,18 @@ export const CreateLoanInfoAccordion = ({ values: BorrowForm collateralToken: Token | undefined borrowToken: Token | undefined - tooMuchDebt: boolean networks: NetworkDict onSlippageChange: (newSlippage: Decimal) => void }) => { const [isOpen, , , toggle] = useSwitch(false) - return ( ({ }, isOpen, )} - gas={useCreateLoanEstimateGas(networks, params, isOpen && !tooMuchDebt)} + gas={useCreateLoanEstimateGas(networks, params, isOpen)} leverage={{ enabled: leverageEnabled, expectedCollateral: useCreateLoanExpectedCollateral(params, isOpen && leverageEnabled), - maxReceive: useCreateLoanMaxReceive(params, isOpen && leverageEnabled && !tooMuchDebt), + maxReceive: useCreateLoanMaxReceive(params, isOpen && leverageEnabled), priceImpact: useCreateLoanPriceImpact(params, isOpen && leverageEnabled), slippage, onSlippageChange, diff --git a/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx b/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx index 9a7792c955..b3c19988fd 100644 --- a/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx +++ b/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx @@ -83,7 +83,6 @@ export function useCreateLoanForm({ creationError, txHash: data?.hash, isApproved: useBorrowCreateLoanIsApproved(params), - tooMuchDebt: !!form.formState.errors['maxDebt'], formErrors: useFormErrors(form.formState), } } diff --git a/apps/main/src/llamalend/queries/create-loan/borrow-create-loan-approved.query.ts b/apps/main/src/llamalend/queries/create-loan/borrow-create-loan-approved.query.ts index ec6b33a524..46b523c9aa 100644 --- a/apps/main/src/llamalend/queries/create-loan/borrow-create-loan-approved.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/borrow-create-loan-approved.query.ts @@ -27,5 +27,5 @@ export const { useQuery: useBorrowCreateLoanIsApproved, fetchQuery: fetchBorrowC : await market.createLoanIsApproved(userCollateral) }, staleTime: '1m', - validationSuite: borrowQueryValidationSuite, + validationSuite: borrowQueryValidationSuite({ debtRequired: false }), // doesn't use debt or maxDebt }) diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-approve-estimate-gas.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-approve-estimate-gas.query.ts index 273bfea7f3..a9535ea25c 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-approve-estimate-gas.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-approve-estimate-gas.query.ts @@ -36,7 +36,7 @@ const { useQuery: useCreateLoanApproveEstimateGas } = queryFactory({ ? await market.leverageV2.estimateGas.createLoanApprove(userCollateral, userBorrowed) : await market.leverage.estimateGas.createLoanApprove(userCollateral) }, - validationSuite: borrowQueryValidationSuite, + validationSuite: borrowQueryValidationSuite({ debtRequired: false }), // doesn't use debt or maxDebt dependencies: (params) => [createLoanMaxReceiveKey(params)], }) diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-bands.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-bands.query.ts index 13f8679a91..be27d4fc99 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-bands.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-bands.query.ts @@ -45,7 +45,7 @@ export const { useQuery: useCreateLoanBands } = queryFactory({ : market.createLoanBands(userCollateral, debt, range) }, staleTime: '1m', - validationSuite: borrowQueryValidationSuite, + validationSuite: borrowQueryValidationSuite(), // requires debt and maxDebt dependencies: (params) => [ createLoanMaxReceiveKey(params), ...(params.leverageEnabled ? [createLoanExpectedCollateralQueryKey(params)] : []), diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-expected-collateral.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-expected-collateral.query.ts index be8698d837..2e80815e64 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-expected-collateral.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-expected-collateral.query.ts @@ -79,5 +79,5 @@ export const { useQuery: useCreateLoanExpectedCollateral, queryKey: createLoanEx return convertNumbers({ userCollateral, leverage, totalCollateral: collateral }) }, staleTime: '1m', - validationSuite: borrowQueryValidationSuite, + validationSuite: borrowQueryValidationSuite(), // requires debt and maxDebt }) diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-health.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-health.query.ts index 24ec015617..76f730f849 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-health.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-health.query.ts @@ -47,7 +47,7 @@ export const { useQuery: useCreateLoanHealth } = queryFactory({ )! }, staleTime: '1m', - validationSuite: borrowQueryValidationSuite, + validationSuite: borrowQueryValidationSuite(), // requires debt and maxDebt dependencies: (params) => [ createLoanMaxReceiveKey(params), ...(params.leverageEnabled ? [createLoanExpectedCollateralQueryKey(params)] : []), diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-price-impact.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-price-impact.query.ts index e74cb4b24e..861d8c9e56 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-price-impact.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-price-impact.query.ts @@ -30,6 +30,6 @@ export const { useQuery: useCreateLoanPriceImpact } = queryFactory({ : +(await market.leverage.priceImpact(userCollateral, debt)) }, staleTime: '1m', - validationSuite: borrowQueryValidationSuite, + validationSuite: borrowQueryValidationSuite(), // requires debt and maxDebt dependencies: (params) => [createLoanExpectedCollateralQueryKey(params)], }) diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-prices.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-prices.query.ts index 9b5a62a70c..dfa08ff9ca 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-prices.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-prices.query.ts @@ -3,12 +3,12 @@ import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { type FieldsOf } from '@ui-kit/lib' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import { Decimal } from '@ui-kit/utils' -import type { BorrowFormQuery } from '../../features/borrow/types' +import type { BorrowForm, BorrowFormQuery } from '../../features/borrow/types' import { borrowQueryValidationSuite } from '../validation/borrow.validation' import { createLoanExpectedCollateralQueryKey } from './create-loan-expected-collateral.query' import { createLoanMaxReceiveKey } from './create-loan-max-receive.query' -type BorrowPricesReceiveQuery = BorrowFormQuery +type BorrowPricesReceiveQuery = BorrowFormQuery & Pick type BorrowPricesReceiveParams = FieldsOf type BorrowPricesResult = [Decimal, Decimal] @@ -23,6 +23,7 @@ export const { useQuery: useCreateLoanPrices } = queryFactory({ debt = '0', leverageEnabled, range, + maxDebt, }: BorrowPricesReceiveParams) => [ ...rootKeys.market({ chainId, marketId }), @@ -32,6 +33,7 @@ export const { useQuery: useCreateLoanPrices } = queryFactory({ { debt }, { leverageEnabled }, { range }, + { maxDebt }, ] as const, queryFn: async ({ marketId, @@ -51,7 +53,7 @@ export const { useQuery: useCreateLoanPrices } = queryFactory({ : convertNumbers(await market.leverage.createLoanPrices(userCollateral, debt, range)) }, staleTime: '1m', - validationSuite: borrowQueryValidationSuite, + validationSuite: borrowQueryValidationSuite(), // requires debt and maxDebt dependencies: (params) => [ createLoanMaxReceiveKey(params), ...(params.leverageEnabled ? [createLoanExpectedCollateralQueryKey(params)] : []), diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-route-image.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-route-image.query.ts index eccb7e2155..6e5bc2f521 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-route-image.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-route-image.query.ts @@ -16,5 +16,5 @@ export const { useQuery: useCreateLoanRouteImage } = queryFactory({ : null }, staleTime: '1m', - validationSuite: borrowQueryValidationSuite, + validationSuite: borrowQueryValidationSuite(), // requires debt and maxDebt }) diff --git a/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts b/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts index e81f90261e..1e961440dc 100644 --- a/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts +++ b/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts @@ -42,6 +42,12 @@ export const validateMaxDebt = (debt: Decimal | undefined | null, maxDebt: Decim }) } +export const validateMaxDebtIsSet = (maxDebt: Decimal | undefined | null) => { + test('debt', 'Maximum debt must be calculated before debt can be validated', () => { + enforce(maxDebt).isNotNullish() + }) +} + export const validateMaxCollateral = ( userCollateral: Decimal | undefined | null, maxCollateral: Decimal | undefined | null, diff --git a/apps/main/src/llamalend/queries/validation/borrow.validation.ts b/apps/main/src/llamalend/queries/validation/borrow.validation.ts index d05df09d03..96d955eb84 100644 --- a/apps/main/src/llamalend/queries/validation/borrow.validation.ts +++ b/apps/main/src/llamalend/queries/validation/borrow.validation.ts @@ -1,20 +1,23 @@ import { group } from 'vest' +import type { Suite } from 'vest' import { validateDebt, validateMaxCollateral, validateMaxDebt, + validateMaxDebtIsSet, validateRange, validateSlippage, validateUserBorrowed, validateUserCollateral, } from '@/llamalend/queries/validation/borrow-fields.validation' +import type { IChainId } from '@curvefi/llamalend-api/lib/interfaces' import { createValidationSuite, type FieldsOf } from '@ui-kit/lib' import { marketIdValidationSuite } from '@ui-kit/lib/model/query/market-id-validation' import { type BorrowForm, type BorrowFormQueryParams } from '../../features/borrow/types' export const borrowFormValidationGroup = ( { userBorrowed, userCollateral, debt, range, slippage, maxDebt, maxCollateral }: FieldsOf, - { debtRequired = true }: { debtRequired?: boolean } = {}, + { debtRequired = true, isMaxDebtRequired = false }: { debtRequired?: boolean; isMaxDebtRequired?: boolean } = {}, ) => group('borrowFormValidationGroup', () => { validateUserBorrowed(userBorrowed) @@ -24,25 +27,22 @@ export const borrowFormValidationGroup = ( validateRange(range) validateMaxDebt(debt, maxDebt) validateMaxCollateral(userCollateral, maxCollateral) + if (isMaxDebtRequired) { + validateMaxDebtIsSet(maxDebt) + } }) export const borrowFormValidationSuite = createValidationSuite(borrowFormValidationGroup) -export const borrowQueryValidationSuite = createValidationSuite( - ({ - chainId, - leverageEnabled, - marketId, - userBorrowed, - userCollateral, - debt, - range, - slippage, - }: BorrowFormQueryParams) => { +export const borrowQueryValidationSuite = ({ + debtRequired = true, + isMaxDebtRequired = debtRequired, +}: { debtRequired?: boolean; isMaxDebtRequired?: boolean } = {}): Suite => + createValidationSuite((params: BorrowFormQueryParams & { maxDebt?: FieldsOf['maxDebt'] }) => { + const { chainId, leverageEnabled, marketId, userBorrowed, userCollateral, debt, range, slippage, maxDebt } = params marketIdValidationSuite({ chainId, marketId }) borrowFormValidationGroup( - { userBorrowed, userCollateral, debt, range, slippage, leverageEnabled }, - { debtRequired: true }, + { userBorrowed, userCollateral, debt, range, slippage, leverageEnabled, maxDebt }, + { debtRequired, isMaxDebtRequired }, ) - }, -) + }) as Suite, string> From f943d3db748ea76be267af4db3dff0b81443289d Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Fri, 12 Dec 2025 15:24:35 +0100 Subject: [PATCH 03/10] fix: leverage enabled validation --- .../components/CreateLoanInfoAccordion.tsx | 6 +-- .../create-loan-expected-collateral.query.ts | 4 +- .../create-loan-max-receive.query.ts | 12 +++++- .../create-loan-price-impact.query.ts | 12 +++++- .../validation/borrow-fields.validation.ts | 38 ++++++++++++------- .../queries/validation/borrow.validation.ts | 37 ++++++++++++------ 6 files changed, 76 insertions(+), 33 deletions(-) diff --git a/apps/main/src/llamalend/features/borrow/components/CreateLoanInfoAccordion.tsx b/apps/main/src/llamalend/features/borrow/components/CreateLoanInfoAccordion.tsx index c87167052e..0165c5c20e 100644 --- a/apps/main/src/llamalend/features/borrow/components/CreateLoanInfoAccordion.tsx +++ b/apps/main/src/llamalend/features/borrow/components/CreateLoanInfoAccordion.tsx @@ -57,9 +57,9 @@ export const CreateLoanInfoAccordion = ({ gas={useCreateLoanEstimateGas(networks, params, isOpen)} leverage={{ enabled: leverageEnabled, - expectedCollateral: useCreateLoanExpectedCollateral(params, isOpen && leverageEnabled), - maxReceive: useCreateLoanMaxReceive(params, isOpen && leverageEnabled), - priceImpact: useCreateLoanPriceImpact(params, isOpen && leverageEnabled), + expectedCollateral: useCreateLoanExpectedCollateral(params, isOpen), + maxReceive: useCreateLoanMaxReceive(params, isOpen), + priceImpact: useCreateLoanPriceImpact(params, isOpen), slippage, onSlippageChange, collateralSymbol: collateralToken?.symbol, diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-expected-collateral.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-expected-collateral.query.ts index 2e80815e64..4318c81870 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-expected-collateral.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-expected-collateral.query.ts @@ -46,6 +46,7 @@ export const { useQuery: useCreateLoanExpectedCollateral, queryKey: createLoanEx userCollateral = '0', debt, slippage, + leverageEnabled, }: BorrowFormQueryParams) => [ ...rootKeys.market({ chainId, marketId }), @@ -54,6 +55,7 @@ export const { useQuery: useCreateLoanExpectedCollateral, queryKey: createLoanEx { userBorrowed }, { debt }, { slippage }, + { leverageEnabled }, ] as const, queryFn: async ({ marketId, @@ -79,5 +81,5 @@ export const { useQuery: useCreateLoanExpectedCollateral, queryKey: createLoanEx return convertNumbers({ userCollateral, leverage, totalCollateral: collateral }) }, staleTime: '1m', - validationSuite: borrowQueryValidationSuite(), // requires debt and maxDebt + validationSuite: borrowQueryValidationSuite({ isLeverageRequired: true }), // requires debt, maxDebt, and leverageEnabled }) diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-max-receive.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-max-receive.query.ts index 97ab0f4242..0a75940dc7 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-max-receive.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-max-receive.query.ts @@ -39,10 +39,18 @@ const convertNumbers = ({ }) export const maxReceiveValidation = createValidationSuite( - ({ chainId, marketId, userBorrowed, userCollateral, range, slippage }: CreateLoanMaxReceiveParams) => { + ({ + chainId, + marketId, + userBorrowed, + userCollateral, + range, + slippage, + leverageEnabled, + }: CreateLoanMaxReceiveParams) => { marketIdValidationSuite({ chainId, marketId }) borrowFormValidationGroup( - { userBorrowed, userCollateral, debt: undefined, range, slippage }, + { userBorrowed, userCollateral, debt: undefined, range, slippage, leverageEnabled }, { debtRequired: false }, ) }, diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-price-impact.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-price-impact.query.ts index 861d8c9e56..db06062cd0 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-price-impact.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-price-impact.query.ts @@ -8,13 +8,21 @@ import { createLoanExpectedCollateralQueryKey } from './create-loan-expected-col type BorrowPriceImpactResult = number // percentage export const { useQuery: useCreateLoanPriceImpact } = queryFactory({ - queryKey: ({ chainId, marketId, userBorrowed = '0', userCollateral = '0', debt = '0' }: BorrowFormQueryParams) => + queryKey: ({ + chainId, + marketId, + userBorrowed = '0', + userCollateral = '0', + debt = '0', + leverageEnabled, + }: BorrowFormQueryParams) => [ ...rootKeys.market({ chainId, marketId }), 'createLoanPriceImpact', { userCollateral }, { userBorrowed }, { debt }, + { leverageEnabled }, ] as const, queryFn: async ({ marketId, @@ -30,6 +38,6 @@ export const { useQuery: useCreateLoanPriceImpact } = queryFactory({ : +(await market.leverage.priceImpact(userCollateral, debt)) }, staleTime: '1m', - validationSuite: borrowQueryValidationSuite(), // requires debt and maxDebt + validationSuite: borrowQueryValidationSuite({ isLeverageRequired: true }), // requires debt, maxDebt, and leverageEnabled dependencies: (params) => [createLoanExpectedCollateralQueryKey(params)], }) diff --git a/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts b/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts index 1e961440dc..435449f51a 100644 --- a/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts +++ b/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts @@ -1,4 +1,4 @@ -import { enforce, test } from 'vest' +import { enforce, test, skipWhen } from 'vest' import { BORROW_PRESET_RANGES } from '@/llamalend/constants' import { Decimal } from '@ui-kit/utils' @@ -15,10 +15,10 @@ export const validateUserCollateral = (userCollateral: Decimal | undefined | nul } export const validateDebt = (debt: Decimal | undefined | null, required: boolean = true) => { - test('debt', `Debt must be a positive number${required ? '' : ' or null'}`, () => { - if (required || debt != null) { + skipWhen(!required && debt == null, () => { + test('debt', `Debt must be a positive number${required ? '' : ' or null'}`, () => { enforce(debt).isNumeric().gt(0) - } + }) }) } @@ -34,17 +34,27 @@ export const validateRange = (range: number | null | undefined, { MaxLtv, Safe } }) } -export const validateMaxDebt = (debt: Decimal | undefined | null, maxDebt: Decimal | undefined | null) => { - test('debt', 'Debt must be less than or equal to maximum borrowable amount', () => { - if (debt != null && maxDebt != null) { +export const validateMaxDebt = ( + debt: Decimal | undefined | null, + maxDebt: Decimal | undefined | null, + isMaxDebtRequired: boolean, +) => { + skipWhen(!isMaxDebtRequired, () => { + test('maxDebt', 'Maximum debt must be calculated before debt can be validated', () => { + enforce(maxDebt).isNotNullish() + }) + }) + skipWhen(maxDebt == null || debt == null, () => { + test('debt', 'Debt must be less than or equal to maximum borrowable amount', () => { + enforce(maxDebt).isNotNullish() enforce(debt).lte(maxDebt) - } + }) }) } -export const validateMaxDebtIsSet = (maxDebt: Decimal | undefined | null) => { - test('debt', 'Maximum debt must be calculated before debt can be validated', () => { - enforce(maxDebt).isNotNullish() +export const validateLeverageEnabled = (leverageEnabled: boolean | undefined | null, isLeverageRequired: boolean) => { + test('leverageEnabled', 'Leverage must be enabled', () => { + enforce(leverageEnabled).equals(true) }) } @@ -52,10 +62,10 @@ export const validateMaxCollateral = ( userCollateral: Decimal | undefined | null, maxCollateral: Decimal | undefined | null, ) => { - test('userCollateral', 'Collateral must be less than or equal to your wallet balance', () => { - if (userCollateral != null && maxCollateral != null) { + skipWhen(userCollateral == null || maxCollateral == null, () => { + test('userCollateral', 'Collateral must be less than or equal to your wallet balance', () => { enforce(userCollateral).lte(maxCollateral) - } + }) }) } diff --git a/apps/main/src/llamalend/queries/validation/borrow.validation.ts b/apps/main/src/llamalend/queries/validation/borrow.validation.ts index 96d955eb84..a2a7d3a60f 100644 --- a/apps/main/src/llamalend/queries/validation/borrow.validation.ts +++ b/apps/main/src/llamalend/queries/validation/borrow.validation.ts @@ -2,22 +2,34 @@ import { group } from 'vest' import type { Suite } from 'vest' import { validateDebt, + validateLeverageEnabled, validateMaxCollateral, validateMaxDebt, - validateMaxDebtIsSet, validateRange, validateSlippage, validateUserBorrowed, validateUserCollateral, } from '@/llamalend/queries/validation/borrow-fields.validation' -import type { IChainId } from '@curvefi/llamalend-api/lib/interfaces' import { createValidationSuite, type FieldsOf } from '@ui-kit/lib' import { marketIdValidationSuite } from '@ui-kit/lib/model/query/market-id-validation' import { type BorrowForm, type BorrowFormQueryParams } from '../../features/borrow/types' export const borrowFormValidationGroup = ( - { userBorrowed, userCollateral, debt, range, slippage, maxDebt, maxCollateral }: FieldsOf, - { debtRequired = true, isMaxDebtRequired = false }: { debtRequired?: boolean; isMaxDebtRequired?: boolean } = {}, + { + userBorrowed, + userCollateral, + debt, + range, + slippage, + maxDebt, + maxCollateral, + leverageEnabled, + }: FieldsOf, + { + debtRequired = true, + isMaxDebtRequired = false, + isLeverageRequired = false, + }: { debtRequired?: boolean; isMaxDebtRequired?: boolean; isLeverageRequired?: boolean } = {}, ) => group('borrowFormValidationGroup', () => { validateUserBorrowed(userBorrowed) @@ -25,11 +37,9 @@ export const borrowFormValidationGroup = ( validateDebt(debt, debtRequired) validateSlippage(slippage) validateRange(range) - validateMaxDebt(debt, maxDebt) + validateMaxDebt(debt, maxDebt, isMaxDebtRequired) validateMaxCollateral(userCollateral, maxCollateral) - if (isMaxDebtRequired) { - validateMaxDebtIsSet(maxDebt) - } + validateLeverageEnabled(leverageEnabled, isLeverageRequired) }) export const borrowFormValidationSuite = createValidationSuite(borrowFormValidationGroup) @@ -37,12 +47,17 @@ export const borrowFormValidationSuite = createValidationSuite(borrowFormValidat export const borrowQueryValidationSuite = ({ debtRequired = true, isMaxDebtRequired = debtRequired, -}: { debtRequired?: boolean; isMaxDebtRequired?: boolean } = {}): Suite => + isLeverageRequired = false, +}: { + debtRequired?: boolean + isMaxDebtRequired?: boolean + isLeverageRequired?: boolean +} = {}): Suite => createValidationSuite((params: BorrowFormQueryParams & { maxDebt?: FieldsOf['maxDebt'] }) => { const { chainId, leverageEnabled, marketId, userBorrowed, userCollateral, debt, range, slippage, maxDebt } = params marketIdValidationSuite({ chainId, marketId }) borrowFormValidationGroup( { userBorrowed, userCollateral, debt, range, slippage, leverageEnabled, maxDebt }, - { debtRequired, isMaxDebtRequired }, + { debtRequired, isMaxDebtRequired, isLeverageRequired }, ) - }) as Suite, string> + }) as Suite From 64f4df117075c6b6b3802c0897eb76cb472d4044 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Fri, 12 Dec 2025 15:39:42 +0100 Subject: [PATCH 04/10] perf: throttle setFormValues --- .../main/src/lend/components/PageLoanCreate/index.tsx | 11 +++++++---- .../src/features/connect-wallet/lib/CurveProvider.tsx | 2 +- packages/curve-ui-kit/src/hooks/useDebounce.ts | 2 +- .../curve-ui-kit/src/shared/ui/LargeTokenInput.tsx | 2 +- packages/curve-ui-kit/src/shared/ui/SliderInput.tsx | 2 +- .../curve-ui-kit/src/themes/design/0_primitives.ts | 8 ++++---- packages/curve-ui-kit/src/utils/timers.ts | 11 +++++++++++ 7 files changed, 26 insertions(+), 12 deletions(-) diff --git a/apps/main/src/lend/components/PageLoanCreate/index.tsx b/apps/main/src/lend/components/PageLoanCreate/index.tsx index 520c539ba4..c2cf47bfdd 100644 --- a/apps/main/src/lend/components/PageLoanCreate/index.tsx +++ b/apps/main/src/lend/components/PageLoanCreate/index.tsx @@ -15,16 +15,18 @@ import { useCreateLoanMuiForm } from '@ui-kit/hooks/useFeatureFlags' import { t } from '@ui-kit/lib/i18n' import { TabsSwitcher, type TabOption } from '@ui-kit/shared/ui/TabsSwitcher' import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' +import { useThrottle } from '@ui-kit/utils/timers' const { MaxWidth } = SizesAndSpaces /** * Callback that synchronizes the `ChartOhlc` component with the `RangeSlider` component in the new `BorrowTabContents`. */ -const useOnFormUpdate = ({ api, market }: PageContentProps): OnBorrowFormUpdate => - useCallback( +const useOnFormUpdate = ({ api, market }: PageContentProps): OnBorrowFormUpdate => { + const setFormValues = useThrottle(useStore((store) => store.loanCreate.setFormValues)) + const setStateByKeys = useThrottle(useStore((store) => store.loanCreate.setStateByKeys)) + return useCallback( async ({ debt, userCollateral, range, slippage, leverageEnabled }) => { - const { setFormValues, setStateByKeys } = useStore.getState().loanCreate const formValues: FormValues = { ...DEFAULT_FORM_VALUES, n: range, @@ -34,8 +36,9 @@ const useOnFormUpdate = ({ api, market }: PageContentProps): OnBorrowFormUpdate await setFormValues(api, market, formValues, `${slippage}`, leverageEnabled) setStateByKeys({ isEditLiqRange: true }) }, - [api, market], + [api, market, setFormValues, setStateByKeys], ) +} const LoanCreate = (pageProps: PageContentProps & { params: MarketUrlParams }) => { const { rChainId, rOwmId, rFormType, market, params, api } = pageProps diff --git a/packages/curve-ui-kit/src/features/connect-wallet/lib/CurveProvider.tsx b/packages/curve-ui-kit/src/features/connect-wallet/lib/CurveProvider.tsx index afdc508c5e..d35cf103aa 100644 --- a/packages/curve-ui-kit/src/features/connect-wallet/lib/CurveProvider.tsx +++ b/packages/curve-ui-kit/src/features/connect-wallet/lib/CurveProvider.tsx @@ -38,7 +38,7 @@ export const CurveProvider = ({ }) => { const [connectState, setConnectState] = useState(LOADING) const walletChainId = useChainId() - const { switchChainAsync } = useSwitchChain() + const { mutateAsync: switchChainAsync } = useSwitchChain() const { wallet, provider, isReconnecting } = useWagmiWallet() const isFocused = useIsDocumentFocused() const libKey = AppLibs[app] diff --git a/packages/curve-ui-kit/src/hooks/useDebounce.ts b/packages/curve-ui-kit/src/hooks/useDebounce.ts index 1e7fa509d6..262c8e76e2 100644 --- a/packages/curve-ui-kit/src/hooks/useDebounce.ts +++ b/packages/curve-ui-kit/src/hooks/useDebounce.ts @@ -79,7 +79,7 @@ export function useDebounce(initialValue: T, debounceMs: number, callback: (v */ export function useDebouncedValue( givenValue: T, - { defaultValue = givenValue, debounceMs = Duration.FormDebounce }: { defaultValue?: T; debounceMs?: number } = {}, + { defaultValue = givenValue, debounceMs = Duration.FormThrottle }: { defaultValue?: T; debounceMs?: number } = {}, ) { const [value, setValue] = useState(defaultValue) useEffect(() => { diff --git a/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx b/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx index c516047e26..0a525047d3 100644 --- a/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx +++ b/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx @@ -257,7 +257,7 @@ export const LargeTokenInput = ({ const [balance, setBalance, cancelSetBalance] = useUniqueDebounce({ defaultValue: externalBalance, callback: onBalance, - debounceMs: Duration.FormDebounce, + debounceMs: Duration.FormThrottle, // We don't want to trigger onBalance if the value is effectively the same, e.g. "0.0" and "0.00" equals: bigNumEquals, }) diff --git a/packages/curve-ui-kit/src/shared/ui/SliderInput.tsx b/packages/curve-ui-kit/src/shared/ui/SliderInput.tsx index 6034085751..aa43ad13aa 100644 --- a/packages/curve-ui-kit/src/shared/ui/SliderInput.tsx +++ b/packages/curve-ui-kit/src/shared/ui/SliderInput.tsx @@ -100,7 +100,7 @@ export const SliderInput = ({ inputProps, sliderValueTransform, testId, - debounceMs = Duration.FormDebounce, + debounceMs = Duration.FormThrottle, }: SliderInputProps) => { const isRange = isRangeValue(value) type SliderValue = T extends Decimal ? number : RangeValue diff --git a/packages/curve-ui-kit/src/themes/design/0_primitives.ts b/packages/curve-ui-kit/src/themes/design/0_primitives.ts index 1db05295b8..3c89d28230 100644 --- a/packages/curve-ui-kit/src/themes/design/0_primitives.ts +++ b/packages/curve-ui-kit/src/themes/design/0_primitives.ts @@ -134,13 +134,13 @@ export const Sizing = { } as const export const Duration = { + Delay: 100, + Flicker: 1000, + Focus: 50, + FormThrottle: 250, Snackbar: 6000, Tooltip: { Enter: 500, Exit: 500 }, - Flicker: 1000, - FormDebounce: 500, Transition: 256, - Focus: 50, - Delay: 100, } export const TransitionFunction = `ease-out ${Duration.Transition}ms` diff --git a/packages/curve-ui-kit/src/utils/timers.ts b/packages/curve-ui-kit/src/utils/timers.ts index f9ebc44990..e0fc3c35fc 100644 --- a/packages/curve-ui-kit/src/utils/timers.ts +++ b/packages/curve-ui-kit/src/utils/timers.ts @@ -1,3 +1,7 @@ +import { throttle } from 'lodash' +import { useMemo } from 'react' +import { Duration } from '@ui-kit/themes/design/0_primitives' + /** * Replaces setInterval using recursive setTimeout. * Returns a cancel function to stop future executions. @@ -27,3 +31,10 @@ export function setTimeoutInterval(callback: () => unknown, delay: number): () = clearTimeout(timeoutId) } } + +/** + * Throttles a function using lodash throttle and memoizes it with useMemo. + * Important: the passed function should be stable between renders (e.g., static or wrapped in useCallback). + */ +export const useThrottle = any>(f: T, duration = Duration.FormThrottle) => + useMemo(() => throttle(f, duration), [f, duration]) From 4a5d0ae8dff93b2f0f7f38c1057d0e47f077eb64 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Fri, 12 Dec 2025 17:23:16 +0100 Subject: [PATCH 05/10] fix: review comments --- .../lend/components/PageLoanCreate/index.tsx | 17 +++-- .../borrow-create-loan-approved.query.ts | 2 +- .../create-loan-approve-estimate-gas.query.ts | 2 +- .../create-loan/create-loan-bands.query.ts | 2 +- .../create-loan-expected-collateral.query.ts | 2 +- .../create-loan/create-loan-health.query.ts | 2 +- .../create-loan-price-impact.query.ts | 2 +- .../create-loan/create-loan-prices.query.ts | 2 +- .../create-loan-route-image.query.ts | 2 +- .../validation/borrow-fields.validation.ts | 6 +- .../queries/validation/borrow.validation.ts | 6 +- .../curve-ui-kit/src/hooks/useDebounce.ts | 73 +++++++++---------- .../src/shared/ui/LargeTokenInput.tsx | 2 +- .../src/shared/ui/SliderInput.tsx | 2 +- .../src/themes/design/0_primitives.ts | 2 +- packages/curve-ui-kit/src/utils/timers.ts | 11 --- 16 files changed, 66 insertions(+), 69 deletions(-) diff --git a/apps/main/src/lend/components/PageLoanCreate/index.tsx b/apps/main/src/lend/components/PageLoanCreate/index.tsx index c2cf47bfdd..19b0c9a6b1 100644 --- a/apps/main/src/lend/components/PageLoanCreate/index.tsx +++ b/apps/main/src/lend/components/PageLoanCreate/index.tsx @@ -11,11 +11,12 @@ import type { OnBorrowFormUpdate } from '@/llamalend/features/borrow/types' import Stack from '@mui/material/Stack' import { AppFormContentWrapper } from '@ui/AppForm' import { useNavigate } from '@ui-kit/hooks/router' +import { useDebounced } from '@ui-kit/hooks/useDebounce' import { useCreateLoanMuiForm } from '@ui-kit/hooks/useFeatureFlags' import { t } from '@ui-kit/lib/i18n' -import { TabsSwitcher, type TabOption } from '@ui-kit/shared/ui/TabsSwitcher' +import { type TabOption, TabsSwitcher } from '@ui-kit/shared/ui/TabsSwitcher' +import { Duration } from '@ui-kit/themes/design/0_primitives' import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' -import { useThrottle } from '@ui-kit/utils/timers' const { MaxWidth } = SizesAndSpaces @@ -23,8 +24,14 @@ const { MaxWidth } = SizesAndSpaces * Callback that synchronizes the `ChartOhlc` component with the `RangeSlider` component in the new `BorrowTabContents`. */ const useOnFormUpdate = ({ api, market }: PageContentProps): OnBorrowFormUpdate => { - const setFormValues = useThrottle(useStore((store) => store.loanCreate.setFormValues)) - const setStateByKeys = useThrottle(useStore((store) => store.loanCreate.setStateByKeys)) + const [setFormValues] = useDebounced( + useStore((store) => store.loanCreate.setFormValues), + Duration.FormDebounce, + ) + const [setStateByKeys] = useDebounced( + useStore((store) => store.loanCreate.setStateByKeys), + Duration.FormDebounce, + ) return useCallback( async ({ debt, userCollateral, range, slippage, leverageEnabled }) => { const formValues: FormValues = { @@ -33,7 +40,7 @@ const useOnFormUpdate = ({ api, market }: PageContentProps): OnBorrowFormUpdate debt: `${debt ?? ''}`, userCollateral: `${userCollateral ?? ''}`, } - await setFormValues(api, market, formValues, `${slippage}`, leverageEnabled) + setFormValues(api, market, formValues, `${slippage}`, leverageEnabled) setStateByKeys({ isEditLiqRange: true }) }, [api, market, setFormValues, setStateByKeys], diff --git a/apps/main/src/llamalend/queries/create-loan/borrow-create-loan-approved.query.ts b/apps/main/src/llamalend/queries/create-loan/borrow-create-loan-approved.query.ts index 46b523c9aa..6abb287864 100644 --- a/apps/main/src/llamalend/queries/create-loan/borrow-create-loan-approved.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/borrow-create-loan-approved.query.ts @@ -27,5 +27,5 @@ export const { useQuery: useBorrowCreateLoanIsApproved, fetchQuery: fetchBorrowC : await market.createLoanIsApproved(userCollateral) }, staleTime: '1m', - validationSuite: borrowQueryValidationSuite({ debtRequired: false }), // doesn't use debt or maxDebt + validationSuite: borrowQueryValidationSuite({ debtRequired: false }), }) diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-approve-estimate-gas.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-approve-estimate-gas.query.ts index a9535ea25c..1581a2ff0a 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-approve-estimate-gas.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-approve-estimate-gas.query.ts @@ -36,7 +36,7 @@ const { useQuery: useCreateLoanApproveEstimateGas } = queryFactory({ ? await market.leverageV2.estimateGas.createLoanApprove(userCollateral, userBorrowed) : await market.leverage.estimateGas.createLoanApprove(userCollateral) }, - validationSuite: borrowQueryValidationSuite({ debtRequired: false }), // doesn't use debt or maxDebt + validationSuite: borrowQueryValidationSuite({ debtRequired: false }), dependencies: (params) => [createLoanMaxReceiveKey(params)], }) diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-bands.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-bands.query.ts index be27d4fc99..6e2c5ea1c5 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-bands.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-bands.query.ts @@ -45,7 +45,7 @@ export const { useQuery: useCreateLoanBands } = queryFactory({ : market.createLoanBands(userCollateral, debt, range) }, staleTime: '1m', - validationSuite: borrowQueryValidationSuite(), // requires debt and maxDebt + validationSuite: borrowQueryValidationSuite({ debtRequired: true }), dependencies: (params) => [ createLoanMaxReceiveKey(params), ...(params.leverageEnabled ? [createLoanExpectedCollateralQueryKey(params)] : []), diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-expected-collateral.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-expected-collateral.query.ts index 4318c81870..9e90d1a50b 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-expected-collateral.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-expected-collateral.query.ts @@ -81,5 +81,5 @@ export const { useQuery: useCreateLoanExpectedCollateral, queryKey: createLoanEx return convertNumbers({ userCollateral, leverage, totalCollateral: collateral }) }, staleTime: '1m', - validationSuite: borrowQueryValidationSuite({ isLeverageRequired: true }), // requires debt, maxDebt, and leverageEnabled + validationSuite: borrowQueryValidationSuite({ debtRequired: true, isLeverageRequired: true }), }) diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-health.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-health.query.ts index 76f730f849..1cb7ffafae 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-health.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-health.query.ts @@ -47,7 +47,7 @@ export const { useQuery: useCreateLoanHealth } = queryFactory({ )! }, staleTime: '1m', - validationSuite: borrowQueryValidationSuite(), // requires debt and maxDebt + validationSuite: borrowQueryValidationSuite({ debtRequired: true }), dependencies: (params) => [ createLoanMaxReceiveKey(params), ...(params.leverageEnabled ? [createLoanExpectedCollateralQueryKey(params)] : []), diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-price-impact.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-price-impact.query.ts index db06062cd0..0959285b27 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-price-impact.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-price-impact.query.ts @@ -38,6 +38,6 @@ export const { useQuery: useCreateLoanPriceImpact } = queryFactory({ : +(await market.leverage.priceImpact(userCollateral, debt)) }, staleTime: '1m', - validationSuite: borrowQueryValidationSuite({ isLeverageRequired: true }), // requires debt, maxDebt, and leverageEnabled + validationSuite: borrowQueryValidationSuite({ debtRequired: true, isLeverageRequired: true }), dependencies: (params) => [createLoanExpectedCollateralQueryKey(params)], }) diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-prices.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-prices.query.ts index dfa08ff9ca..02ac9f04a1 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-prices.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-prices.query.ts @@ -53,7 +53,7 @@ export const { useQuery: useCreateLoanPrices } = queryFactory({ : convertNumbers(await market.leverage.createLoanPrices(userCollateral, debt, range)) }, staleTime: '1m', - validationSuite: borrowQueryValidationSuite(), // requires debt and maxDebt + validationSuite: borrowQueryValidationSuite({ debtRequired: true }), dependencies: (params) => [ createLoanMaxReceiveKey(params), ...(params.leverageEnabled ? [createLoanExpectedCollateralQueryKey(params)] : []), diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-route-image.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-route-image.query.ts index 6e5bc2f521..c10452ba95 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-route-image.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-route-image.query.ts @@ -16,5 +16,5 @@ export const { useQuery: useCreateLoanRouteImage } = queryFactory({ : null }, staleTime: '1m', - validationSuite: borrowQueryValidationSuite(), // requires debt and maxDebt + validationSuite: borrowQueryValidationSuite({ debtRequired: true }), }) diff --git a/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts b/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts index 435449f51a..2448b6b6c0 100644 --- a/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts +++ b/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts @@ -53,8 +53,10 @@ export const validateMaxDebt = ( } export const validateLeverageEnabled = (leverageEnabled: boolean | undefined | null, isLeverageRequired: boolean) => { - test('leverageEnabled', 'Leverage must be enabled', () => { - enforce(leverageEnabled).equals(true) + skipWhen(!isLeverageRequired, () => { + test('leverageEnabled', 'Leverage must be enabled', () => { + enforce(leverageEnabled).equals(true) + }) }) } diff --git a/apps/main/src/llamalend/queries/validation/borrow.validation.ts b/apps/main/src/llamalend/queries/validation/borrow.validation.ts index a2a7d3a60f..3f48dad9ff 100644 --- a/apps/main/src/llamalend/queries/validation/borrow.validation.ts +++ b/apps/main/src/llamalend/queries/validation/borrow.validation.ts @@ -45,14 +45,14 @@ export const borrowFormValidationGroup = ( export const borrowFormValidationSuite = createValidationSuite(borrowFormValidationGroup) export const borrowQueryValidationSuite = ({ - debtRequired = true, + debtRequired, isMaxDebtRequired = debtRequired, isLeverageRequired = false, }: { - debtRequired?: boolean + debtRequired: boolean isMaxDebtRequired?: boolean isLeverageRequired?: boolean -} = {}): Suite => +}): Suite => createValidationSuite((params: BorrowFormQueryParams & { maxDebt?: FieldsOf['maxDebt'] }) => { const { chainId, leverageEnabled, marketId, userBorrowed, userCollateral, debt, range, slippage, maxDebt } = params marketIdValidationSuite({ chainId, marketId }) diff --git a/packages/curve-ui-kit/src/hooks/useDebounce.ts b/packages/curve-ui-kit/src/hooks/useDebounce.ts index 262c8e76e2..33961af6b4 100644 --- a/packages/curve-ui-kit/src/hooks/useDebounce.ts +++ b/packages/curve-ui-kit/src/hooks/useDebounce.ts @@ -1,6 +1,40 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { Duration } from '@ui-kit/themes/design/0_primitives' +/** + * A hook that debounces a function call and calls a callback when the debouncing period has elapsed. + * + * @param debounceMs - The debouncing period in milliseconds + * @param callback - Callback function that is called after the debounce period + * @param onChange - Optional callback function that is called immediately when the value changes + * @returns A tuple containing the debounced function and a cancel function + */ +export function useDebounced( + callback: (...value: T) => void, + debounceMs: number, + onChange?: (...value: T) => void, +) { + const timerRef = useRef(null) + const cancel = useCallback(() => void (timerRef.current && clearTimeout(timerRef.current)), []) + useEffect(() => cancel, [cancel]) + return [ + useCallback( + (...newValue: T) => { + cancel() + onChange?.(...newValue) + + // Initiate a new timer + timerRef.current = window.setTimeout(() => { + callback(...newValue) + timerRef.current = null + }, debounceMs) + }, + [callback, cancel, debounceMs, onChange], + ), + cancel, + ] as const +} + /** * A hook that debounces a value and calls a callback when the debounce period has elapsed. * @@ -32,42 +66,7 @@ import { Duration } from '@ui-kit/themes/design/0_primitives' */ export function useDebounce(initialValue: T, debounceMs: number, callback: (value: T) => void) { const [value, setValue] = useState(initialValue) - const timerRef = useRef(null) - - // Update value when initialValue changes for controlled components - useEffect(() => { - setValue(initialValue) - }, [initialValue]) - - // Clear timer on unmount - useEffect( - () => () => { - if (timerRef.current !== null) { - clearTimeout(timerRef.current) - } - }, - [], - ) - - // Clear any existing timer - const cancel = useCallback(() => timerRef.current && clearTimeout(timerRef.current), []) - - // Sets the internal value, but calls the callback after a delay unless retriggered again. - const setDebouncedValue = useCallback( - (newValue: T) => { - setValue(newValue) - cancel() - - // Initiate a new timer - timerRef.current = window.setTimeout(() => { - callback(newValue) - timerRef.current = null - }, debounceMs) - }, - [callback, cancel, debounceMs], - ) - - return [value, setDebouncedValue, cancel] as const + return [value, ...useDebounced(callback, debounceMs, setValue)] } /** @@ -79,7 +78,7 @@ export function useDebounce(initialValue: T, debounceMs: number, callback: (v */ export function useDebouncedValue( givenValue: T, - { defaultValue = givenValue, debounceMs = Duration.FormThrottle }: { defaultValue?: T; debounceMs?: number } = {}, + { defaultValue = givenValue, debounceMs = Duration.FormDebounce }: { defaultValue?: T; debounceMs?: number } = {}, ) { const [value, setValue] = useState(defaultValue) useEffect(() => { diff --git a/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx b/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx index 0a525047d3..c516047e26 100644 --- a/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx +++ b/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx @@ -257,7 +257,7 @@ export const LargeTokenInput = ({ const [balance, setBalance, cancelSetBalance] = useUniqueDebounce({ defaultValue: externalBalance, callback: onBalance, - debounceMs: Duration.FormThrottle, + debounceMs: Duration.FormDebounce, // We don't want to trigger onBalance if the value is effectively the same, e.g. "0.0" and "0.00" equals: bigNumEquals, }) diff --git a/packages/curve-ui-kit/src/shared/ui/SliderInput.tsx b/packages/curve-ui-kit/src/shared/ui/SliderInput.tsx index aa43ad13aa..6034085751 100644 --- a/packages/curve-ui-kit/src/shared/ui/SliderInput.tsx +++ b/packages/curve-ui-kit/src/shared/ui/SliderInput.tsx @@ -100,7 +100,7 @@ export const SliderInput = ({ inputProps, sliderValueTransform, testId, - debounceMs = Duration.FormThrottle, + debounceMs = Duration.FormDebounce, }: SliderInputProps) => { const isRange = isRangeValue(value) type SliderValue = T extends Decimal ? number : RangeValue diff --git a/packages/curve-ui-kit/src/themes/design/0_primitives.ts b/packages/curve-ui-kit/src/themes/design/0_primitives.ts index 3c89d28230..f227de53b5 100644 --- a/packages/curve-ui-kit/src/themes/design/0_primitives.ts +++ b/packages/curve-ui-kit/src/themes/design/0_primitives.ts @@ -137,7 +137,7 @@ export const Duration = { Delay: 100, Flicker: 1000, Focus: 50, - FormThrottle: 250, + FormDebounce: 250, Snackbar: 6000, Tooltip: { Enter: 500, Exit: 500 }, Transition: 256, diff --git a/packages/curve-ui-kit/src/utils/timers.ts b/packages/curve-ui-kit/src/utils/timers.ts index e0fc3c35fc..f9ebc44990 100644 --- a/packages/curve-ui-kit/src/utils/timers.ts +++ b/packages/curve-ui-kit/src/utils/timers.ts @@ -1,7 +1,3 @@ -import { throttle } from 'lodash' -import { useMemo } from 'react' -import { Duration } from '@ui-kit/themes/design/0_primitives' - /** * Replaces setInterval using recursive setTimeout. * Returns a cancel function to stop future executions. @@ -31,10 +27,3 @@ export function setTimeoutInterval(callback: () => unknown, delay: number): () = clearTimeout(timeoutId) } } - -/** - * Throttles a function using lodash throttle and memoizes it with useMemo. - * Important: the passed function should be stable between renders (e.g., static or wrapped in useCallback). - */ -export const useThrottle = any>(f: T, duration = Duration.FormThrottle) => - useMemo(() => throttle(f, duration), [f, duration]) From f268a263f3f0299e5c9e10bf029471a80c36034f Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Fri, 12 Dec 2025 19:24:23 +0100 Subject: [PATCH 06/10] chore: test debounce functions --- tests/cypress/component/useDebounce.cy.tsx | 523 +++++++++++++++++++++ 1 file changed, 523 insertions(+) create mode 100644 tests/cypress/component/useDebounce.cy.tsx diff --git a/tests/cypress/component/useDebounce.cy.tsx b/tests/cypress/component/useDebounce.cy.tsx new file mode 100644 index 0000000000..de6cdbcb1e --- /dev/null +++ b/tests/cypress/component/useDebounce.cy.tsx @@ -0,0 +1,523 @@ +import { useState } from 'react' +import { useDebounced, useDebounce, useDebouncedValue, useUniqueDebounce } from '@ui-kit/hooks/useDebounce' + +// Test component for useDebounced +function UseDebouncedTest({ + debounceMs, + callback, + onChange, +}: { + debounceMs: number + callback: (...args: any[]) => void + onChange?: (...args: any[]) => void +}) { + const [debouncedFn, cancel] = useDebounced(callback, debounceMs, onChange) + + return ( +
+ + + +
+ ) +} + +// Test component for useDebounce +function UseDebounceTest({ + initialValue, + debounceMs, + callback, +}: { + initialValue: string + debounceMs: number + callback: (value: string) => void +}) { + const result = useDebounce(initialValue, debounceMs, callback) + const value = result[0] + const setValue = result[1] + const cancel = result[2] + + return ( +
+ setValue(e.target.value)} /> +
{value}
+ +
+ ) +} + +// Test component for useDebouncedValue +function UseDebouncedValueTest({ + givenValue, + debounceMs, + defaultValue, +}: { + givenValue: string + debounceMs?: number + defaultValue?: string +}) { + const debouncedValue = useDebouncedValue(givenValue, { debounceMs, defaultValue }) + + return ( +
+
{debouncedValue}
+
{givenValue}
+
+ ) +} + +// Test component for useUniqueDebounce +function UseUniqueDebounceTest({ + defaultValue, + callback, + debounceMs, + equals, +}: { + defaultValue: string + callback: (value: string) => void + debounceMs?: number + equals?: (a: string, b: string) => boolean +}) { + const result = useUniqueDebounce({ + defaultValue, + callback, + debounceMs, + equals, + }) + const value = result[0] + const setValue = result[1] + const cancel = result[2] + + return ( +
+ setValue(e.target.value)} /> +
{value}
+ +
+ ) +} + +// Test component for useUniqueDebounce with objects +function UseUniqueDebounceObjectTest({ + defaultValue, + callback, + equals, +}: { + defaultValue: { id: number; name: string } + callback: (value: { id: number; name: string }) => void + equals?: (a: { id: number; name: string }, b: { id: number; name: string }) => boolean +}) { + const result = useUniqueDebounce({ + defaultValue, + callback, + debounceMs: 200, + equals, + }) + const value = result[0] + const setValue = result[1] + + return ( +
+ + +
{JSON.stringify(value)}
+
+ ) +} + +describe('useDebounced', () => { + beforeEach(() => { + cy.clock() + }) + + it('calls callback after debounce period', () => { + const callback = cy.stub().as('callback') + cy.mount() + + cy.get('[data-testid="trigger"]').click() + cy.get('@callback').should('not.have.been.called') + + cy.tick(300) + cy.get('@callback').should('have.been.calledOnce') + cy.get('@callback').should('have.been.calledWith', 'test-value') + }) + + it('cancels previous timeout when called multiple times', () => { + const callback = cy.stub().as('callback') + cy.mount() + + cy.get('[data-testid="trigger"]').click() + cy.tick(100) + cy.get('[data-testid="trigger"]').click() + cy.tick(100) + cy.get('[data-testid="trigger"]').click() + cy.tick(300) + + cy.get('@callback').should('have.been.calledOnce') + cy.get('@callback').should('have.been.calledWith', 'test-value') + }) + + it('calls onChange immediately when provided', () => { + const callback = cy.stub().as('callback') + const onChange = cy.stub().as('onChange') + cy.mount() + + cy.get('[data-testid="trigger"]').click() + + cy.get('@onChange').should('have.been.calledOnce') + cy.get('@onChange').should('have.been.calledWith', 'test-value') + cy.get('@callback').should('not.have.been.called') + + cy.tick(300) + cy.get('@callback').should('have.been.calledOnce') + }) + + it('supports multiple arguments', () => { + const callback = cy.stub().as('callback') + cy.mount() + + cy.get('[data-testid="trigger-with-args"]').click() + cy.tick(200) + + cy.get('@callback').should('have.been.calledWith', 'arg1', 42, true) + }) + + it('cancel function prevents callback execution', () => { + const callback = cy.stub().as('callback') + cy.mount() + + cy.get('[data-testid="trigger"]').click() + cy.tick(100) + cy.get('[data-testid="cancel"]').click() + cy.tick(300) + + cy.get('@callback').should('not.have.been.called') + }) +}) + +describe('useDebounce', () => { + beforeEach(() => { + cy.clock() + }) + + it('returns initial value', () => { + const callback = cy.stub() + cy.mount() + + cy.get('[data-testid="current-value"]').should('have.text', 'initial') + }) + + it('updates value immediately but calls callback after debounce', () => { + const callback = cy.stub().as('callback') + cy.mount() + + cy.get('[data-testid="input"]').type('new value') + + cy.get('[data-testid="current-value"]').should('have.text', 'new value') + cy.get('@callback').should('not.have.been.called') + + cy.tick(300) + cy.get('@callback').should('have.been.calledOnce') + cy.get('@callback').should('have.been.calledWith', 'new value') + }) + + it('debounces multiple rapid changes', () => { + const callback = cy.stub().as('callback') + cy.mount() + + cy.get('[data-testid="input"]').type('a') + cy.tick(50) + cy.get('[data-testid="input"]').type('b') + cy.tick(50) + cy.get('[data-testid="input"]').type('c') + cy.tick(200) + + cy.get('@callback').should('have.been.calledOnce') + cy.get('@callback').should('have.been.calledWith', 'abc') + }) + + it('cancel function stops pending callback', () => { + const callback = cy.stub().as('callback') + cy.mount() + + cy.get('[data-testid="input"]').type('test') + cy.tick(100) + cy.get('[data-testid="cancel"]').click() + cy.tick(300) + + cy.get('@callback').should('not.have.been.called') + }) +}) + +describe('useDebouncedValue', () => { + beforeEach(() => { + cy.clock() + }) + + it('returns default value initially when provided', () => { + cy.mount() + + cy.get('[data-testid="debounced-value"]').should('have.text', 'default') + cy.get('[data-testid="given-value"]').should('have.text', 'actual') + }) + + it('returns given value as default when defaultValue not provided', () => { + cy.mount() + + cy.get('[data-testid="debounced-value"]').should('have.text', 'test') + }) + + it('updates debounced value after delay', () => { + function DebouncedValueWrapper() { + const [value, setValue] = useState('initial') + return ( +
+ + +
+ ) + } + + cy.mount() + + cy.get('[data-testid="debounced-value"]').should('have.text', 'initial') + + cy.get('[data-testid="update"]').click() + cy.get('[data-testid="given-value"]').should('have.text', 'updated') + cy.get('[data-testid="debounced-value"]').should('have.text', 'initial') + + cy.tick(300) + cy.get('[data-testid="debounced-value"]').should('have.text', 'updated') + }) + + it('cancels previous timeout on value change', () => { + function DebouncedValueWrapper() { + const [value, setValue] = useState('first') + return ( +
+ + + +
+ ) + } + + cy.mount() + + cy.get('[data-testid="set-second"]').click() + cy.tick(100) + cy.get('[data-testid="set-third"]').click() + cy.tick(100) + cy.get('[data-testid="debounced-value"]').should('have.text', 'first') + + cy.tick(300) + cy.get('[data-testid="debounced-value"]').should('have.text', 'third') + }) +}) + +describe('useUniqueDebounce', () => { + beforeEach(() => { + cy.clock() + }) + + it('only calls callback when value changes', () => { + const callback = cy.stub().as('callback') + cy.mount() + + cy.get('[data-testid="input"]').clear().type('new value') + cy.tick(200) + + cy.get('@callback').should('have.been.calledOnce') + cy.get('@callback').should('have.been.calledWith', 'new value') + + // Set to same value + cy.get('[data-testid="input"]').clear().type('new value') + cy.tick(200) + + // Should not call callback again + cy.get('@callback').should('have.been.calledOnce') + }) + + it('uses custom equality function when provided', () => { + const callback = cy.stub().as('callback') + const equals = (a: string, b: string) => a.toLowerCase() === b.toLowerCase() + + cy.mount() + + cy.get('[data-testid="input"]').clear().type('TEST') + cy.tick(200) + + // Should not call callback because values are equal (case-insensitive) + cy.get('@callback').should('not.have.been.called') + + cy.get('[data-testid="input"]').clear().type('different') + cy.tick(200) + + cy.get('@callback').should('have.been.calledOnce') + cy.get('@callback').should('have.been.calledWith', 'different') + }) + + it('uses default debounce time of 166ms when not provided', () => { + const callback = cy.stub().as('callback') + cy.mount() + + cy.get('[data-testid="input"]').type('test') + cy.tick(166) + + cy.get('@callback').should('have.been.calledOnce') + }) + + it('updates lastValue when defaultValue changes (async initialization)', () => { + const callback = cy.stub().as('callback') + + // This test simulates the scenario described in the hook's useEffect comment: + // 1. Component mounts with empty defaultValue (before async load) + // 2. Async data loads and defaultValue updates to a saved value + // 3. User clears the input back to empty + // 4. Callback should fire because we compare against the updated defaultValue, not the original + function AsyncInitWrapper() { + const [defaultValue, setDefaultValue] = useState('') + const result = useUniqueDebounce({ + defaultValue, + callback, + debounceMs: 200, + }) + const value = result[0] + const setValue = result[1] + + return ( +
+ + +
{value}
+
{defaultValue}
+
+ ) + } + + cy.mount() + + // Initial state: both values are empty, lastValue.current = '' + cy.get('[data-testid="current-value"]').should('have.text', '') + cy.get('[data-testid="default-value"]').should('have.text', '') + + // Simulate async load from localStorage + // This updates defaultValue to 'saved search' and lastValue.current to 'saved search' via useEffect + cy.get('[data-testid="load-async"]').click() + cy.get('[data-testid="default-value"]').should('have.text', 'saved search') + + // User clears the search + // Without the useEffect update, this would compare '' to '' (initial lastValue) + // With the useEffect update, this compares '' to 'saved search' (updated lastValue) + cy.get('[data-testid="clear"]').click() + cy.tick(200) + + // Callback should fire because '' !== 'saved search' + cy.get('@callback').should('have.been.calledOnce') + cy.get('@callback').should('have.been.calledWith', '') + }) + + it('handles object values with custom equality', () => { + const callback = cy.stub().as('callback') + const equals = (a: { id: number; name: string }, b: { id: number; name: string }) => a.id === b.id + + cy.mount( + , + ) + + cy.get('[data-testid="set-same-id"]').click() + cy.tick(200) + + // Should not call callback because id is the same + cy.get('@callback').should('not.have.been.called') + + cy.get('[data-testid="set-different-id"]').click() + cy.tick(200) + + // Should call callback because id changed + cy.get('@callback').should('have.been.calledOnce') + }) + + it('debounces rapid changes and only fires once for unique values', () => { + const callback = cy.stub().as('callback') + cy.mount() + + cy.get('[data-testid="input"]').type('a') + cy.tick(50) + cy.get('[data-testid="input"]').type('b') + cy.tick(50) + cy.get('[data-testid="input"]').type('c') + cy.tick(200) + + cy.get('@callback').should('have.been.calledOnce') + cy.get('@callback').should('have.been.calledWith', 'abc') + }) + + it('handles primitive number values correctly', () => { + const callback = cy.stub().as('callback') + + function NumberTest() { + const result = useUniqueDebounce({ + defaultValue: 0, + callback, + debounceMs: 200, + }) + const value = result[0] + const setValue = result[1] + + return ( +
+ + + +
{value}
+
+ ) + } + + cy.mount() + + cy.get('[data-testid="set-5"]').click() + cy.tick(200) + cy.get('@callback').should('have.been.calledWith', 5) + + cy.get('[data-testid="set-5-again"]').click() + cy.tick(200) + cy.get('@callback').should('have.been.calledOnce') // No additional call + + cy.get('[data-testid="set-10"]').click() + cy.tick(200) + cy.get('@callback').should('have.been.calledTwice') + cy.get('@callback').should('have.been.calledWith', 10) + }) +}) From f0bf346715cbb5f5486bd8d2e6d47f84bc623fc6 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Fri, 12 Dec 2025 20:35:58 +0100 Subject: [PATCH 07/10] fix: typecheck --- .../curve-ui-kit/src/hooks/useDebounce.ts | 2 +- tests/cypress/component/useDebounce.cy.tsx | 22 +++++-------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/packages/curve-ui-kit/src/hooks/useDebounce.ts b/packages/curve-ui-kit/src/hooks/useDebounce.ts index 33961af6b4..56689e5436 100644 --- a/packages/curve-ui-kit/src/hooks/useDebounce.ts +++ b/packages/curve-ui-kit/src/hooks/useDebounce.ts @@ -66,7 +66,7 @@ export function useDebounced( */ export function useDebounce(initialValue: T, debounceMs: number, callback: (value: T) => void) { const [value, setValue] = useState(initialValue) - return [value, ...useDebounced(callback, debounceMs, setValue)] + return [value, ...useDebounced(callback, debounceMs, setValue)] as const } /** diff --git a/tests/cypress/component/useDebounce.cy.tsx b/tests/cypress/component/useDebounce.cy.tsx index de6cdbcb1e..a1c5bc15a6 100644 --- a/tests/cypress/component/useDebounce.cy.tsx +++ b/tests/cypress/component/useDebounce.cy.tsx @@ -38,10 +38,7 @@ function UseDebounceTest({ debounceMs: number callback: (value: string) => void }) { - const result = useDebounce(initialValue, debounceMs, callback) - const value = result[0] - const setValue = result[1] - const cancel = result[2] + const [value, setValue, cancel] = useDebounce(initialValue, debounceMs, callback) return (
@@ -86,15 +83,12 @@ function UseUniqueDebounceTest({ debounceMs?: number equals?: (a: string, b: string) => boolean }) { - const result = useUniqueDebounce({ + const [value, setValue, cancel] = useUniqueDebounce({ defaultValue, callback, debounceMs, equals, }) - const value = result[0] - const setValue = result[1] - const cancel = result[2] return (
@@ -117,14 +111,12 @@ function UseUniqueDebounceObjectTest({ callback: (value: { id: number; name: string }) => void equals?: (a: { id: number; name: string }, b: { id: number; name: string }) => boolean }) { - const result = useUniqueDebounce({ + const [value, setValue] = useUniqueDebounce({ defaultValue, callback, debounceMs: 200, equals, }) - const value = result[0] - const setValue = result[1] return (
@@ -397,13 +389,11 @@ describe('useUniqueDebounce', () => { // 4. Callback should fire because we compare against the updated defaultValue, not the original function AsyncInitWrapper() { const [defaultValue, setDefaultValue] = useState('') - const result = useUniqueDebounce({ + const [value, setValue] = useUniqueDebounce({ defaultValue, callback, debounceMs: 200, }) - const value = result[0] - const setValue = result[1] return (
@@ -481,13 +471,11 @@ describe('useUniqueDebounce', () => { const callback = cy.stub().as('callback') function NumberTest() { - const result = useUniqueDebounce({ + const [value, setValue] = useUniqueDebounce({ defaultValue: 0, callback, debounceMs: 200, }) - const value = result[0] - const setValue = result[1] return (
From 987d1e0020612273ac20eb20412c7818c901afaf Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Mon, 15 Dec 2025 16:25:37 +0100 Subject: [PATCH 08/10] fix: query keys & validation --- .../borrow/components/CreateLoanForm.tsx | 9 ++++- .../components/CreateLoanInfoAccordion.tsx | 23 +++++++++--- .../borrow/components/LoanPresetSelector.tsx | 2 +- .../borrow/hooks/useCreateLoanForm.tsx | 7 ++-- .../src/llamalend/features/borrow/types.ts | 5 +++ .../features/market-list/LlamaMarketSort.tsx | 23 ------------ .../mutations/create-loan.mutation.ts | 4 +-- .../borrow-create-loan-approved.query.ts | 4 +-- .../create-loan-approve-estimate-gas.query.ts | 6 +++- .../create-loan/create-loan-bands.query.ts | 8 +++-- .../create-loan-expected-collateral.query.ts | 8 +++-- .../create-loan/create-loan-health.query.ts | 8 +++-- .../create-loan-max-receive.query.ts | 2 +- .../create-loan-price-impact.query.ts | 8 +++-- .../create-loan/create-loan-prices.query.ts | 4 +-- .../create-loan-route-image.query.ts | 14 +++++--- .../validation/borrow-fields.validation.ts | 4 +-- .../queries/validation/borrow.validation.ts | 19 +++++----- .../manage-loan/LoanFormTokenInput.tsx | 18 ++++++---- .../curve-ui-kit/src/hooks/useDebounce.ts | 1 + .../src/shared/ui/LargeTokenInput.tsx | 36 ++++++------------- packages/curve-ui-kit/src/types/util.ts | 2 -- 22 files changed, 112 insertions(+), 103 deletions(-) delete mode 100644 apps/main/src/llamalend/features/market-list/LlamaMarketSort.tsx diff --git a/apps/main/src/llamalend/features/borrow/components/CreateLoanForm.tsx b/apps/main/src/llamalend/features/borrow/components/CreateLoanForm.tsx index 5c492ea218..12192d650a 100644 --- a/apps/main/src/llamalend/features/borrow/components/CreateLoanForm.tsx +++ b/apps/main/src/llamalend/features/borrow/components/CreateLoanForm.tsx @@ -70,7 +70,14 @@ export const CreateLoanForm = ({ formErrors, isApproved, } = useCreateLoanForm({ market, network, preset, onCreated }) - const setRange = useCallback((range: number) => form.setValue('range', range, setValueOptions), [form]) + const setRange = useCallback( + (range: number) => { + // maxDebt is reset when query restarts, clear now to disable queries until recalculated + form.setValue('maxDebt', undefined, setValueOptions) + form.setValue('range', range, setValueOptions) + }, + [form], + ) useFormSync(values, onUpdate) return ( diff --git a/apps/main/src/llamalend/features/borrow/components/CreateLoanInfoAccordion.tsx b/apps/main/src/llamalend/features/borrow/components/CreateLoanInfoAccordion.tsx index 0165c5c20e..67786f40f8 100644 --- a/apps/main/src/llamalend/features/borrow/components/CreateLoanInfoAccordion.tsx +++ b/apps/main/src/llamalend/features/borrow/components/CreateLoanInfoAccordion.tsx @@ -1,7 +1,9 @@ import type { NetworkDict } from '@/llamalend/llamalend.types' import { useMarketRates } from '@/llamalend/queries/market-rates' import type { IChainId } from '@curvefi/llamalend-api/lib/interfaces' +import type { UseQueryResult } from '@tanstack/react-query' import { useSwitch } from '@ui-kit/hooks/useSwitch' +import type { Query } from '@ui-kit/types/util' import { Decimal } from '@ui-kit/utils' import { useCreateLoanEstimateGas } from '../../../queries/create-loan/create-loan-approve-estimate-gas.query' import { useCreateLoanBands } from '../../../queries/create-loan/create-loan-bands.query' @@ -15,6 +17,17 @@ import { LoanInfoAccordion } from '../../../widgets/manage-loan/LoanInfoAccordio import { useLoanToValue } from '../hooks/useLoanToValue' import { type BorrowForm, type BorrowFormQueryParams, type Token } from '../types' +/** + * Helper to extract only the relevant fields from a UseQueryResult into the Query type. + * This is necessary because passing UseQueryResult to any react component will crash the rendering due to + * react trying to serialize the react-query proxy object. + */ +const q = ({ data, isLoading, error }: UseQueryResult): Query => ({ + data, + isLoading, + error, +}) + /** * Accordion with action infos about the loan (like health, band range, price range, N, borrow APR, LTV, estimated gas, slippage) * By default, only the health info is visible. The rest is visible when the accordion is expanded. @@ -41,11 +54,11 @@ export const CreateLoanInfoAccordion = ({ isOpen={isOpen} toggle={toggle} range={range} - health={useCreateLoanHealth(params)} - bands={useCreateLoanBands(params, isOpen)} - prices={useCreateLoanPrices(params, isOpen)} - prevRates={useMarketRates(params, isOpen)} - rates={useMarketFutureRates(params, isOpen)} + health={q(useCreateLoanHealth(params))} + bands={q(useCreateLoanBands(params, isOpen))} + prices={q(useCreateLoanPrices(params, isOpen))} + prevRates={q(useMarketRates(params, isOpen))} + rates={q(useMarketFutureRates(params, isOpen))} loanToValue={useLoanToValue( { params, diff --git a/apps/main/src/llamalend/features/borrow/components/LoanPresetSelector.tsx b/apps/main/src/llamalend/features/borrow/components/LoanPresetSelector.tsx index b5d231d278..38f66cdb53 100644 --- a/apps/main/src/llamalend/features/borrow/components/LoanPresetSelector.tsx +++ b/apps/main/src/llamalend/features/borrow/components/LoanPresetSelector.tsx @@ -56,7 +56,7 @@ export const LoanPresetSelector = ({ sx={{ width: '100%', paddingBottom: Spacing.sm }} > {Object.values(BorrowPreset).map((p) => ( - + {PRESETS[p].title} ))} diff --git a/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx b/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx index b3c19988fd..f0a98632ed 100644 --- a/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx +++ b/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx @@ -13,7 +13,7 @@ import { SLIPPAGE_PRESETS } from '@ui-kit/widgets/SlippageSettings/slippage.util import { BORROW_PRESET_RANGES, BorrowPreset } from '../../../constants' import { type CreateLoanOptions, useCreateLoanMutation } from '../../../mutations/create-loan.mutation' import { useBorrowCreateLoanIsApproved } from '../../../queries/create-loan/borrow-create-loan-approved.query' -import { borrowFormValidationSuite } from '../../../queries/validation/borrow.validation' +import { borrowQueryValidationSuite } from '../../../queries/validation/borrow.validation' import { useFormErrors } from '../react-form.utils' import { type BorrowForm } from '../types' import { useMaxTokenValues } from './useMaxTokenValues' @@ -21,6 +21,8 @@ import { useMaxTokenValues } from './useMaxTokenValues' const useCallbackAfterFormUpdate = (form: UseFormReturn, callback: () => void) => useEffect(() => form.subscribe({ formState: { values: true }, callback }), [form, callback]) +const resolver = vestResolver(borrowQueryValidationSuite({ debtRequired: false })) + export function useCreateLoanForm({ market, network, @@ -36,8 +38,7 @@ export function useCreateLoanForm({ const { address: userAddress } = useConnection() const form = useForm({ ...formDefaultOptions, - // todo: also validate maxLeverage and maxCollateral - resolver: vestResolver(borrowFormValidationSuite), + resolver, defaultValues: { userCollateral: undefined, userBorrowed: `0` satisfies Decimal, diff --git a/apps/main/src/llamalend/features/borrow/types.ts b/apps/main/src/llamalend/features/borrow/types.ts index 6fe6eb12b7..03030d74d4 100644 --- a/apps/main/src/llamalend/features/borrow/types.ts +++ b/apps/main/src/llamalend/features/borrow/types.ts @@ -31,5 +31,10 @@ export type BorrowFormQuery = MarketQuery & CompleteBorrowForm /** Fields of the borrow form query before validation */ export type BorrowFormQueryParams = FieldsOf> +/** Borrow form query including max debt field */ +export type BorrowDebtQuery = BorrowFormQuery & Pick +/** Fields of the borrow debt query before validation */ +export type BorrowDebtParams = FieldsOf> + /** A simple token representation */ export type Token = { symbol: string; address: Address } diff --git a/apps/main/src/llamalend/features/market-list/LlamaMarketSort.tsx b/apps/main/src/llamalend/features/market-list/LlamaMarketSort.tsx deleted file mode 100644 index 6cb1be7231..0000000000 --- a/apps/main/src/llamalend/features/market-list/LlamaMarketSort.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useCallback } from 'react' -import { OnChangeFn, SortingState } from '@tanstack/react-table' -import { type Option, SelectFilter } from '@ui-kit/shared/ui/DataTable/SelectFilter' -import { LlamaMarketColumnId } from './columns.enum' -import { useLlamaMarketSortOptions } from './hooks/useLlamaMarketSortOptions' - -export const LlamaMarketSort = ({ - sortField, - onSortingChange, -}: { - onSortingChange: OnChangeFn - sortField: LlamaMarketColumnId -}) => ( - ) => onSortingChange([{ id, desc: true }]), - [onSortingChange], - )} - value={sortField} - /> -) diff --git a/apps/main/src/llamalend/mutations/create-loan.mutation.ts b/apps/main/src/llamalend/mutations/create-loan.mutation.ts index 58b7e89d3e..59d9e9488a 100644 --- a/apps/main/src/llamalend/mutations/create-loan.mutation.ts +++ b/apps/main/src/llamalend/mutations/create-loan.mutation.ts @@ -4,7 +4,7 @@ import { formatTokenAmounts } from '@/llamalend/llama.utils' import type { LlamaMarketTemplate } from '@/llamalend/llamalend.types' import { type LlammaMutationOptions, useLlammaMutation } from '@/llamalend/mutations/useLlammaMutation' import { fetchBorrowCreateLoanIsApproved } from '@/llamalend/queries/create-loan/borrow-create-loan-approved.query' -import { borrowFormValidationSuite } from '@/llamalend/queries/validation/borrow.validation' +import { borrowQueryValidationSuite } from '@/llamalend/queries/validation/borrow.validation' import type { IChainId as LlamaChainId, IChainId, @@ -79,7 +79,7 @@ export const useCreateLoanMutation = ({ }) return { hash: await create(market, mutation) } }, - validationSuite: borrowFormValidationSuite, + validationSuite: borrowQueryValidationSuite({ debtRequired: true }), pendingMessage: (mutation, { market }) => t`Creating loan... ${formatTokenAmounts(market, mutation)}`, successMessage: (mutation, { market }) => t`Loan created! ${formatTokenAmounts(market, mutation)}`, onSuccess: onCreated, diff --git a/apps/main/src/llamalend/queries/create-loan/borrow-create-loan-approved.query.ts b/apps/main/src/llamalend/queries/create-loan/borrow-create-loan-approved.query.ts index 6abb287864..b25f862e34 100644 --- a/apps/main/src/llamalend/queries/create-loan/borrow-create-loan-approved.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/borrow-create-loan-approved.query.ts @@ -1,7 +1,7 @@ import { getLlamaMarket } from '@/llamalend/llama.utils' import { MintMarketTemplate } from '@curvefi/llamalend-api/lib/mintMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' -import type { BorrowFormQuery, BorrowFormQueryParams } from '../../features/borrow/types' +import type { BorrowDebtQuery, BorrowFormQueryParams } from '../../features/borrow/types' import { borrowQueryValidationSuite } from '../validation/borrow.validation' export const { useQuery: useBorrowCreateLoanIsApproved, fetchQuery: fetchBorrowCreateLoanIsApproved } = queryFactory({ @@ -18,7 +18,7 @@ export const { useQuery: useBorrowCreateLoanIsApproved, fetchQuery: fetchBorrowC userBorrowed = '0', userCollateral = '0', leverageEnabled, - }: BorrowFormQuery): Promise => { + }: BorrowDebtQuery): Promise => { const market = getLlamaMarket(marketId) return leverageEnabled ? market instanceof MintMarketTemplate && market.leverageV2.hasLeverage() diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-approve-estimate-gas.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-approve-estimate-gas.query.ts index 1581a2ff0a..546a35c5e5 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-approve-estimate-gas.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-approve-estimate-gas.query.ts @@ -1,3 +1,4 @@ +import type { Suite } from 'vest' import { useEstimateGas } from '@/llamalend/hooks/useEstimateGas' import { getLlamaMarket } from '@/llamalend/llama.utils' import { type NetworkDict } from '@/llamalend/llamalend.types' @@ -36,7 +37,10 @@ const { useQuery: useCreateLoanApproveEstimateGas } = queryFactory({ ? await market.leverageV2.estimateGas.createLoanApprove(userCollateral, userBorrowed) : await market.leverage.estimateGas.createLoanApprove(userCollateral) }, - validationSuite: borrowQueryValidationSuite({ debtRequired: false }), + validationSuite: borrowQueryValidationSuite({ debtRequired: false }) as Suite< + keyof CreateLoanApproveEstimateGasQuery, + string + >, dependencies: (params) => [createLoanMaxReceiveKey(params)], }) diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-bands.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-bands.query.ts index 6e2c5ea1c5..23f7437671 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-bands.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-bands.query.ts @@ -1,7 +1,7 @@ import { getLlamaMarket } from '@/llamalend/llama.utils' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' -import type { BorrowFormQuery, BorrowFormQueryParams } from '../../features/borrow/types' +import type { BorrowDebtParams, BorrowDebtQuery } from '../../features/borrow/types' import { borrowQueryValidationSuite } from '../validation/borrow.validation' import { createLoanExpectedCollateralQueryKey } from './create-loan-expected-collateral.query' import { createLoanMaxReceiveKey } from './create-loan-max-receive.query' @@ -17,7 +17,8 @@ export const { useQuery: useCreateLoanBands } = queryFactory({ debt = '0', leverageEnabled, range, - }: BorrowFormQueryParams) => + maxDebt, + }: BorrowDebtParams) => [ ...rootKeys.market({ chainId, marketId }), 'createLoanBands', @@ -26,6 +27,7 @@ export const { useQuery: useCreateLoanBands } = queryFactory({ { debt }, { leverageEnabled }, { range }, + { maxDebt }, ] as const, queryFn: ({ marketId, @@ -34,7 +36,7 @@ export const { useQuery: useCreateLoanBands } = queryFactory({ debt = '0', leverageEnabled, range, - }: BorrowFormQuery): Promise => { + }: BorrowDebtQuery): Promise => { const market = getLlamaMarket(marketId) return leverageEnabled ? market instanceof LendMarketTemplate diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-expected-collateral.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-expected-collateral.query.ts index 9e90d1a50b..4ed3df0055 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-expected-collateral.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-expected-collateral.query.ts @@ -2,7 +2,7 @@ import { getLlamaMarket } from '@/llamalend/llama.utils' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import { assert, decimal, Decimal } from '@ui-kit/utils' -import type { BorrowFormQuery, BorrowFormQueryParams } from '../../features/borrow/types' +import type { BorrowDebtParams, BorrowDebtQuery } from '../../features/borrow/types' import { borrowQueryValidationSuite } from '../validation/borrow.validation' type BorrowExpectedCollateralResult = { @@ -47,7 +47,8 @@ export const { useQuery: useCreateLoanExpectedCollateral, queryKey: createLoanEx debt, slippage, leverageEnabled, - }: BorrowFormQueryParams) => + maxDebt, + }: BorrowDebtParams) => [ ...rootKeys.market({ chainId, marketId }), 'createLoanExpectedCollateral', @@ -56,6 +57,7 @@ export const { useQuery: useCreateLoanExpectedCollateral, queryKey: createLoanEx { debt }, { slippage }, { leverageEnabled }, + { maxDebt }, ] as const, queryFn: async ({ marketId, @@ -63,7 +65,7 @@ export const { useQuery: useCreateLoanExpectedCollateral, queryKey: createLoanEx userCollateral = '0', debt, slippage, - }: BorrowFormQuery): Promise => { + }: BorrowDebtQuery): Promise => { const market = getLlamaMarket(marketId) if (market instanceof LendMarketTemplate) { return convertNumbers( diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-health.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-health.query.ts index 1cb7ffafae..a2549a66db 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-health.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-health.query.ts @@ -3,7 +3,7 @@ import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import type { Decimal } from '@ui-kit/utils' import { decimal } from '@ui-kit/utils' -import type { BorrowFormQuery, BorrowFormQueryParams } from '../../features/borrow/types' +import type { BorrowDebtParams, BorrowDebtQuery } from '../../features/borrow/types' import { borrowQueryValidationSuite } from '../validation/borrow.validation' import { createLoanExpectedCollateralQueryKey } from './create-loan-expected-collateral.query' import { createLoanMaxReceiveKey } from './create-loan-max-receive.query' @@ -17,7 +17,8 @@ export const { useQuery: useCreateLoanHealth } = queryFactory({ debt, leverageEnabled, range, - }: BorrowFormQueryParams) => + maxDebt, + }: BorrowDebtParams) => [ ...rootKeys.market({ chainId, marketId }), 'createLoanHealth', @@ -26,6 +27,7 @@ export const { useQuery: useCreateLoanHealth } = queryFactory({ { debt }, { leverageEnabled }, { range }, + { maxDebt }, ] as const, queryFn: async ({ marketId, @@ -34,7 +36,7 @@ export const { useQuery: useCreateLoanHealth } = queryFactory({ debt = '0', leverageEnabled, range, - }: BorrowFormQuery): Promise => { + }: BorrowDebtQuery): Promise => { const market = getLlamaMarket(marketId) return decimal( leverageEnabled diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-max-receive.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-max-receive.query.ts index 0a75940dc7..8aab0d7b7c 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-max-receive.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-max-receive.query.ts @@ -51,7 +51,7 @@ export const maxReceiveValidation = createValidationSuite( marketIdValidationSuite({ chainId, marketId }) borrowFormValidationGroup( { userBorrowed, userCollateral, debt: undefined, range, slippage, leverageEnabled }, - { debtRequired: false }, + { debtRequired: false, isMaxDebtRequired: false, isLeverageRequired: false }, ) }, ) diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-price-impact.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-price-impact.query.ts index 0959285b27..43ec77d744 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-price-impact.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-price-impact.query.ts @@ -1,7 +1,7 @@ import { getLlamaMarket } from '@/llamalend/llama.utils' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' -import type { BorrowFormQuery, BorrowFormQueryParams } from '../../features/borrow/types' +import type { BorrowDebtParams, BorrowDebtQuery } from '../../features/borrow/types' import { borrowQueryValidationSuite } from '../validation/borrow.validation' import { createLoanExpectedCollateralQueryKey } from './create-loan-expected-collateral.query' @@ -15,7 +15,8 @@ export const { useQuery: useCreateLoanPriceImpact } = queryFactory({ userCollateral = '0', debt = '0', leverageEnabled, - }: BorrowFormQueryParams) => + maxDebt, + }: BorrowDebtParams) => [ ...rootKeys.market({ chainId, marketId }), 'createLoanPriceImpact', @@ -23,13 +24,14 @@ export const { useQuery: useCreateLoanPriceImpact } = queryFactory({ { userBorrowed }, { debt }, { leverageEnabled }, + { maxDebt }, ] as const, queryFn: async ({ marketId, userBorrowed = '0', userCollateral = '0', debt = '0', - }: BorrowFormQuery): Promise => { + }: BorrowDebtQuery): Promise => { const market = getLlamaMarket(marketId) return market instanceof LendMarketTemplate ? +(await market.leverage.createLoanPriceImpact(userBorrowed, debt)) diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-prices.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-prices.query.ts index 02ac9f04a1..a69e7a762c 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-prices.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-prices.query.ts @@ -3,7 +3,7 @@ import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { type FieldsOf } from '@ui-kit/lib' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import { Decimal } from '@ui-kit/utils' -import type { BorrowForm, BorrowFormQuery } from '../../features/borrow/types' +import type { BorrowDebtQuery, BorrowForm, BorrowFormQuery } from '../../features/borrow/types' import { borrowQueryValidationSuite } from '../validation/borrow.validation' import { createLoanExpectedCollateralQueryKey } from './create-loan-expected-collateral.query' import { createLoanMaxReceiveKey } from './create-loan-max-receive.query' @@ -42,7 +42,7 @@ export const { useQuery: useCreateLoanPrices } = queryFactory({ debt = '0', leverageEnabled, range, - }: BorrowPricesReceiveQuery): Promise => { + }: BorrowDebtQuery): Promise => { const market = getLlamaMarket(marketId) return !leverageEnabled ? convertNumbers(await market.createLoanPrices(userCollateral, debt, range)) diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-route-image.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-route-image.query.ts index c10452ba95..00e3697ee4 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-route-image.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-route-image.query.ts @@ -1,13 +1,19 @@ import { getLlamaMarket } from '@/llamalend/llama.utils' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' -import type { BorrowFormQuery, BorrowFormQueryParams } from '../../features/borrow/types' +import type { BorrowDebtParams, BorrowDebtQuery } from '../../features/borrow/types' import { borrowQueryValidationSuite } from '../validation/borrow.validation' export const { useQuery: useCreateLoanRouteImage } = queryFactory({ - queryKey: ({ chainId, marketId, userBorrowed = '0', debt = '0' }: BorrowFormQueryParams) => - [...rootKeys.market({ chainId, marketId }), 'createLoanRouteImage', { userBorrowed }, { debt }] as const, - queryFn: async ({ marketId, userBorrowed = '0', debt = '0' }: BorrowFormQuery) => { + queryKey: ({ chainId, marketId, userBorrowed = '0', debt = '0', maxDebt }: BorrowDebtParams) => + [ + ...rootKeys.market({ chainId, marketId }), + 'createLoanRouteImage', + { userBorrowed }, + { debt }, + { maxDebt }, + ] as const, + queryFn: async ({ marketId, userBorrowed = '0', debt = '0' }: BorrowDebtQuery) => { const market = getLlamaMarket(marketId) return market instanceof LendMarketTemplate ? await market.leverage.createLoanRouteImage(userBorrowed, debt) diff --git a/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts b/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts index 2448b6b6c0..d5be6ccb48 100644 --- a/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts +++ b/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts @@ -15,7 +15,7 @@ export const validateUserCollateral = (userCollateral: Decimal | undefined | nul } export const validateDebt = (debt: Decimal | undefined | null, required: boolean = true) => { - skipWhen(!required && debt == null, () => { + skipWhen(!required && !debt, () => { test('debt', `Debt must be a positive number${required ? '' : ' or null'}`, () => { enforce(debt).isNumeric().gt(0) }) @@ -45,7 +45,7 @@ export const validateMaxDebt = ( }) }) skipWhen(maxDebt == null || debt == null, () => { - test('debt', 'Debt must be less than or equal to maximum borrowable amount', () => { + test('maxDebt', 'Debt must be less than or equal to maximum borrowable amount', () => { enforce(maxDebt).isNotNullish() enforce(debt).lte(maxDebt) }) diff --git a/apps/main/src/llamalend/queries/validation/borrow.validation.ts b/apps/main/src/llamalend/queries/validation/borrow.validation.ts index 3f48dad9ff..7b2f34a8a4 100644 --- a/apps/main/src/llamalend/queries/validation/borrow.validation.ts +++ b/apps/main/src/llamalend/queries/validation/borrow.validation.ts @@ -1,5 +1,4 @@ import { group } from 'vest' -import type { Suite } from 'vest' import { validateDebt, validateLeverageEnabled, @@ -12,7 +11,7 @@ import { } from '@/llamalend/queries/validation/borrow-fields.validation' import { createValidationSuite, type FieldsOf } from '@ui-kit/lib' import { marketIdValidationSuite } from '@ui-kit/lib/model/query/market-id-validation' -import { type BorrowForm, type BorrowFormQueryParams } from '../../features/borrow/types' +import { type BorrowDebtParams, type BorrowForm } from '../../features/borrow/types' export const borrowFormValidationGroup = ( { @@ -26,10 +25,10 @@ export const borrowFormValidationGroup = ( leverageEnabled, }: FieldsOf, { - debtRequired = true, - isMaxDebtRequired = false, - isLeverageRequired = false, - }: { debtRequired?: boolean; isMaxDebtRequired?: boolean; isLeverageRequired?: boolean } = {}, + debtRequired, + isMaxDebtRequired, + isLeverageRequired, + }: { debtRequired: boolean; isMaxDebtRequired: boolean; isLeverageRequired: boolean }, ) => group('borrowFormValidationGroup', () => { validateUserBorrowed(userBorrowed) @@ -42,8 +41,6 @@ export const borrowFormValidationGroup = ( validateLeverageEnabled(leverageEnabled, isLeverageRequired) }) -export const borrowFormValidationSuite = createValidationSuite(borrowFormValidationGroup) - export const borrowQueryValidationSuite = ({ debtRequired, isMaxDebtRequired = debtRequired, @@ -52,12 +49,12 @@ export const borrowQueryValidationSuite = ({ debtRequired: boolean isMaxDebtRequired?: boolean isLeverageRequired?: boolean -}): Suite => - createValidationSuite((params: BorrowFormQueryParams & { maxDebt?: FieldsOf['maxDebt'] }) => { +}) => + createValidationSuite((params: BorrowDebtParams) => { const { chainId, leverageEnabled, marketId, userBorrowed, userCollateral, debt, range, slippage, maxDebt } = params marketIdValidationSuite({ chainId, marketId }) borrowFormValidationGroup( { userBorrowed, userCollateral, debt, range, slippage, leverageEnabled, maxDebt }, { debtRequired, isMaxDebtRequired, isLeverageRequired }, ) - }) as Suite + }) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx index c553f7a154..1fb4bb46bc 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx @@ -7,7 +7,7 @@ import type { LlamaNetwork } from '@/llamalend/llamalend.types' import type { INetworkName } from '@curvefi/llamalend-api/lib/interfaces' import type { PartialRecord } from '@curvefi/prices-api/objects.util' import { useTokenBalance } from '@ui-kit/hooks/useTokenBalance' -import { LargeTokenInput } from '@ui-kit/shared/ui/LargeTokenInput' +import { HelperMessage, LargeTokenInput } from '@ui-kit/shared/ui/LargeTokenInput' import { TokenLabel } from '@ui-kit/shared/ui/TokenLabel' import type { Query } from '@ui-kit/types/util' import { Decimal } from '@ui-kit/utils' @@ -52,8 +52,9 @@ export const LoanFormTokenInput = < } = useTokenBalance({ chainId: network?.chainId, userAddress }, token) const errors = form.formState.errors as PartialRecord, Error> - const relatedMaxFieldError = max?.fieldName && errors[max.fieldName] + const relatedMaxFieldError = max?.data && max?.fieldName && errors[max.fieldName] const error = errors[name] || max?.error || balanceError || relatedMaxFieldError + return ( form.setValue(name, v as FieldPathValue, setValueOptions), - [form, name], + (v?: Decimal) => { + form.setValue(name, v as FieldPathValue, setValueOptions) + if (max?.fieldName) void form.trigger(max.fieldName) // validate max field when balance changes + }, + [form, max?.fieldName, name], )} isError={!!error} - message={error?.message ?? message} + message={error?.message} walletBalance={useMemo( // todo: support separate isLoading for balance and for maxBalance in LargeTokenInput () => ({ balance, symbol: token?.symbol, loading: isBalanceLoading }), [balance, isBalanceLoading, token?.symbol], )} maxBalance={useMemo(() => max && { balance: max.data, chips: 'max' }, [max])} - /> + > + {message && } + ) } diff --git a/packages/curve-ui-kit/src/hooks/useDebounce.ts b/packages/curve-ui-kit/src/hooks/useDebounce.ts index 56689e5436..ba43ddfdd5 100644 --- a/packages/curve-ui-kit/src/hooks/useDebounce.ts +++ b/packages/curve-ui-kit/src/hooks/useDebounce.ts @@ -66,6 +66,7 @@ export function useDebounced( */ export function useDebounce(initialValue: T, debounceMs: number, callback: (value: T) => void) { const [value, setValue] = useState(initialValue) + useEffect(() => setValue(initialValue), [initialValue]) return [value, ...useDebounced(callback, debounceMs, setValue)] as const } diff --git a/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx b/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx index c516047e26..d6a713a233 100644 --- a/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx +++ b/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx @@ -27,10 +27,10 @@ const { Spacing, FontSize, FontWeight, Sizing } = SizesAndSpaces type HelperMessageProps = { message: string | ReactNode - isError: boolean + isError?: boolean } -const HelperMessage = ({ message, isError }: HelperMessageProps) => ( +export const HelperMessage = ({ message, isError }: HelperMessageProps) => ( = { type BalanceTextFieldProps = { balance: Decimal | undefined - maxBalance?: Decimal isError: boolean disabled?: boolean /** Callback fired when the numeric value changes, can be a temporary non decimal value like "5." or "-" */ @@ -195,6 +194,9 @@ export type LargeTokenInputProps = { /** Optional props forwarded to the slider */ sliderProps?: SliderInputProps['sliderProps'] + + /** Optional children to be rendered below the input */ + children?: ReactNode } /** @@ -252,6 +254,7 @@ export const LargeTokenInput = ({ inputBalanceUsd, testId, sliderProps, + children, }: LargeTokenInputProps) => { const [percentage, setPercentage] = useState(undefined) const [balance, setBalance, cancelSetBalance] = useUniqueDebounce({ @@ -303,13 +306,6 @@ export const LargeTokenInput = ({ [maxBalance?.balance, setBalance, cancelSetBalance, onBalance], ) - const handleChip = useCallback( - (chip: InputChip) => { - handleBalanceChange(chip.newBalance(maxBalance?.balance)) - }, - [handleBalanceChange, maxBalance?.balance], - ) - const updatePercentageOnNewMaxBalance = useEffectEvent((newMaxBalance?: Decimal) => { setPercentage(newMaxBalance && balance ? calculateNewPercentage(balance, newMaxBalance) : undefined) }) @@ -384,7 +380,7 @@ export const LargeTokenInput = ({ color="default" clickable disabled={disabled} - onClick={() => handleChip(chip)} + onClick={() => handleBalanceChange(chip.newBalance(maxBalance?.balance))} > ))} @@ -398,7 +394,6 @@ export const LargeTokenInput = ({ disabled={disabled} balance={balance} name={name} - maxBalance={maxBalance?.balance} isError={isError} onChange={handleBalanceChange} /> @@ -420,25 +415,15 @@ export const LargeTokenInput = ({ {/** Fourth row showing optional slider for max balance. */} {showSlider && ( - + handlePercentageChange(value as Decimal)} - sliderProps={{ - 'data-rail-background': 'danger', - ...sliderProps, - }} + sliderProps={{ 'data-rail-background': 'danger', ...sliderProps }} min={MIN_PERCENTAGE} max={MAX_PERCENTAGE} - inputProps={{ - variant: 'standard', - adornment: 'percentage', - }} + inputProps={{ variant: 'standard', adornment: 'percentage' }} /> )} @@ -446,6 +431,7 @@ export const LargeTokenInput = ({ {/** Fourth row containing optional helper (or error) message */} {message && } + {children} ) } diff --git a/packages/curve-ui-kit/src/types/util.ts b/packages/curve-ui-kit/src/types/util.ts index f2743315ba..17eb674967 100644 --- a/packages/curve-ui-kit/src/types/util.ts +++ b/packages/curve-ui-kit/src/types/util.ts @@ -1,5 +1,3 @@ -// Various Typescript utility types, useful everywhere! - /** * Creates a deep partial type that makes all properties optional recursively, * while preserving function types as-is From cc9179e08cdf66414f5a020f7b5d60bb26392734 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Mon, 15 Dec 2025 17:42:27 +0100 Subject: [PATCH 09/10] e2e: debt/maxDebt validation --- .../llamalend/mutations/useLlammaMutation.ts | 13 +++++ .../create-loan-route-image.query.ts | 2 + .../validation/borrow-fields.validation.ts | 2 +- .../src/shared/ui/LargeTokenInput.tsx | 1 + ...rpc.cy.tsx => create-loan-form.rpc.cy.tsx} | 54 ++++++++++++------- 5 files changed, 52 insertions(+), 20 deletions(-) rename tests/cypress/component/llamalend/{borrow-tab-contents.rpc.cy.tsx => create-loan-form.rpc.cy.tsx} (85%) diff --git a/apps/main/src/llamalend/mutations/useLlammaMutation.ts b/apps/main/src/llamalend/mutations/useLlammaMutation.ts index ba62bda04b..0b81aab6bf 100644 --- a/apps/main/src/llamalend/mutations/useLlammaMutation.ts +++ b/apps/main/src/llamalend/mutations/useLlammaMutation.ts @@ -104,6 +104,7 @@ export function useLlammaMutation { + console.log({ mutationKey, variables }) // Early validation - throwing here prevents mutationFn from running if (!wallet) throw new Error('Missing provider') if (!llamaApi) throw new Error('Missing llamalend api') @@ -120,11 +121,22 @@ export function useLlammaMutation { const market = getLlamaMarket(marketId!) + console.log('mutationFn called with variables:', variables, 'and market:', market) const data = await mutationFn(variables, { market }) throwIfError(data) return { data, receipt: await waitForTransactionReceipt(config, data) } }, onSuccess: async ({ data, receipt }, variables, context) => { + console.log( + `OnSuccess called with data:`, + data, + 'receipt:', + receipt, + 'variables:', + variables, + 'context:', + context, + ) logSuccess(mutationKey, { data, variables, marketId: context.market.id }) notify(successMessage(variables, context), 'success') updateUserEventsApi(wallet!, { id: networkId }, context.market, receipt.transactionHash) @@ -133,6 +145,7 @@ export function useLlammaMutation { + console.error(`Error in mutation ${mutationKey}:`, error) logError(mutationKey, { error, variables, marketId: context?.market.id }) notify(error.message, 'error') }, diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-route-image.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-route-image.query.ts index 00e3697ee4..41922bade4 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-route-image.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-route-image.query.ts @@ -1,4 +1,5 @@ import { getLlamaMarket } from '@/llamalend/llama.utils' +import { createLoanExpectedCollateralQueryKey } from '@/llamalend/queries/create-loan/create-loan-expected-collateral.query' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import type { BorrowDebtParams, BorrowDebtQuery } from '../../features/borrow/types' @@ -23,4 +24,5 @@ export const { useQuery: useCreateLoanRouteImage } = queryFactory({ }, staleTime: '1m', validationSuite: borrowQueryValidationSuite({ debtRequired: true }), + dependencies: (params) => [createLoanExpectedCollateralQueryKey(params)], }) diff --git a/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts b/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts index d5be6ccb48..55348eba37 100644 --- a/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts +++ b/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts @@ -45,7 +45,7 @@ export const validateMaxDebt = ( }) }) skipWhen(maxDebt == null || debt == null, () => { - test('maxDebt', 'Debt must be less than or equal to maximum borrowable amount', () => { + test('maxDebt', 'Debt is too high', () => { enforce(maxDebt).isNotNullish() enforce(debt).lte(maxDebt) }) diff --git a/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx b/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx index d6a713a233..8a80835103 100644 --- a/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx +++ b/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx @@ -49,6 +49,7 @@ export const HelperMessage = ({ message, isError }: HelperMessageProps) => ( color: (t) => isError ? t.design.Text.TextColors.FilledFeedback.Warning.Primary : t.design.Text.TextColors.Tertiary, }} + data-testid={`helper-message-${isError ? 'error' : 'info'}`} > {message} diff --git a/tests/cypress/component/llamalend/borrow-tab-contents.rpc.cy.tsx b/tests/cypress/component/llamalend/create-loan-form.rpc.cy.tsx similarity index 85% rename from tests/cypress/component/llamalend/borrow-tab-contents.rpc.cy.tsx rename to tests/cypress/component/llamalend/create-loan-form.rpc.cy.tsx index 0fb35e04e5..018fe09a4b 100644 --- a/tests/cypress/component/llamalend/borrow-tab-contents.rpc.cy.tsx +++ b/tests/cypress/component/llamalend/create-loan-form.rpc.cy.tsx @@ -1,6 +1,7 @@ import { useMemo } from 'react' import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' import { prefetchMarkets } from '@/lend/entities/chain/chain-query' +import { BorrowPreset } from '@/llamalend/constants' 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' @@ -57,7 +58,7 @@ function BorrowTabTest({ type, onCreated }: BorrowTabTestProps) { ) } -describe('BorrowTabContents Component Tests', () => { +describe('CreateLoanForm Component Tests', () => { const privateKey = generatePrivateKey() const { address } = privateKeyToAccount(privateKey) const getVirtualNetwork = createVirtualTestnet((uuid) => ({ @@ -99,26 +100,10 @@ describe('BorrowTabContents Component Tests', () => { 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() - cy.get('[data-testid="borrow-debt-input"] [data-testid="balance-value"]', LOAD_TIMEOUT).should('exist') - cy.get('[data-testid="borrow-collateral-input"] input[type="text"]').first().type(collateral) - cy.get('[data-testid="borrow-debt-input"] [data-testid="balance-value"]').should('not.contain.text', '?') - getActionValue('borrow-health').should('have.text', '∞') - cy.get('[data-testid="borrow-debt-input"] input[type="text"]').first().type(borrow) - getActionValue('borrow-health').should('not.contain.text', '∞') - - if (leverageEnabled) { - cy.get('[data-testid="leverage-checkbox"]').click() - } - - // open borrow advanced settings and check all fields - cy.contains('button', 'Health').click() - + function assertLoanDetailsLoaded() { getActionValue('borrow-band-range') .invoke(LOAD_TIMEOUT, 'text') - .should('match', /(\d(\.\d+)?) to (\d(\.\d+)?)/) + .should('match', /(\d(\.\d+)?) to (-?\d(\.\d+)?)/) getActionValue('borrow-price-range') .invoke(LOAD_TIMEOUT, 'text') .should('match', /(\d(\.\d+)?) - (\d(\.\d+)?)/) @@ -136,6 +121,37 @@ describe('BorrowTabContents Component Tests', () => { } cy.get('[data-testid="loan-form-errors"]').should('not.exist') + } + + it(`calculates max debt and health for ${marketType} market ${leverageEnabled ? 'with' : 'without'} leverage`, () => { + const onCreated = cy.stub() + cy.mount() + cy.get('[data-testid="borrow-debt-input"] [data-testid="balance-value"]', LOAD_TIMEOUT).should('exist') + cy.get('[data-testid="borrow-collateral-input"] input[type="text"]').first().type(collateral) + cy.get('[data-testid="borrow-debt-input"] [data-testid="balance-value"]').should('not.contain.text', '?') + getActionValue('borrow-health').should('have.text', '∞') + cy.get('[data-testid="borrow-debt-input"] input[type="text"]').first().type(borrow) + getActionValue('borrow-health').should('not.contain.text', '∞') + + if (leverageEnabled) { + cy.get('[data-testid="leverage-checkbox"]').click() + } + + // open borrow advanced settings and check all fields + cy.contains('button', 'Health').click() + assertLoanDetailsLoaded() + + // click max ltv and max borrow, then back to safe, expect error. Clear it by setting max again + cy.get(`[data-testid="loan-preset-${BorrowPreset.MaxLtv}"]`).click() + cy.get('[data-testid="borrow-set-debt-to-max"]').should('not.exist') // should only render after loaded + cy.get('[data-testid="borrow-set-debt-to-max"]', LOAD_TIMEOUT).click() + cy.get(`[data-testid="loan-preset-${BorrowPreset.Safe}"]`).click() + cy.get('[data-testid="helper-message-error"]', LOAD_TIMEOUT).should('contain.text', 'Debt is too high') + cy.get('[data-testid="borrow-set-debt-to-max"]').click() // set max again to fix error + cy.get('[data-testid="helper-message-error"]').should('not.exist') + assertLoanDetailsLoaded() + + // create the loan, expect the onCreated to be called cy.get('[data-testid="create-loan-submit-button"]').click() cy.get('[data-testid="create-loan-submit-button"]').should('be.disabled') cy.get('[data-testid="create-loan-submit-button"]', LOAD_TIMEOUT).should('be.enabled') From 79e166cc367faeb3313d76928de7db645ac75b36 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Mon, 15 Dec 2025 20:49:04 +0100 Subject: [PATCH 10/10] fix: omit market validation for form data --- .../borrow/components/CreateLoanForm.tsx | 6 +++++- .../borrow/hooks/useCreateLoanForm.tsx | 5 +++-- .../mutations/add-collateral.mutation.ts | 3 ++- .../mutations/create-loan.mutation.ts | 2 +- .../src/llamalend/mutations/repay.mutation.ts | 21 +++++++++++-------- .../llamalend/mutations/useLlammaMutation.ts | 12 ----------- .../queries/validation/borrow.validation.ts | 8 +++++-- 7 files changed, 29 insertions(+), 28 deletions(-) diff --git a/apps/main/src/llamalend/features/borrow/components/CreateLoanForm.tsx b/apps/main/src/llamalend/features/borrow/components/CreateLoanForm.tsx index 12192d650a..c5719ecfbb 100644 --- a/apps/main/src/llamalend/features/borrow/components/CreateLoanForm.tsx +++ b/apps/main/src/llamalend/features/borrow/components/CreateLoanForm.tsx @@ -122,7 +122,11 @@ export const CreateLoanForm = ({ symbol={borrowToken?.symbol} balance={values.maxDebt} loading={maxTokenValues.debt.isLoading} - onClick={() => form.setValue('debt', values.maxDebt, setValueOptions)} + onClick={() => { + form.setValue('debt', values.maxDebt, setValueOptions) + void form.trigger('maxDebt') // re-validate maxDebt when debt changes + }} + buttonTestId="borrow-set-debt-to-max" /> } /> diff --git a/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx b/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx index f0a98632ed..e6d808cedf 100644 --- a/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx +++ b/apps/main/src/llamalend/features/borrow/hooks/useCreateLoanForm.tsx @@ -21,7 +21,8 @@ import { useMaxTokenValues } from './useMaxTokenValues' const useCallbackAfterFormUpdate = (form: UseFormReturn, callback: () => void) => useEffect(() => form.subscribe({ formState: { values: true }, callback }), [form, callback]) -const resolver = vestResolver(borrowQueryValidationSuite({ debtRequired: false })) +// to crete a loan we need the debt/maxDebt, but we skip the market validation as that's given separately to the mutation +const resolver = vestResolver(borrowQueryValidationSuite({ debtRequired: false, skipMarketValidation: true })) export function useCreateLoanForm({ market, @@ -76,7 +77,7 @@ export function useCreateLoanForm({ values, params, isPending: form.formState.isSubmitting || isCreating, - onSubmit: form.handleSubmit(onSubmit), // todo: handle form errors + onSubmit: form.handleSubmit(onSubmit), maxTokenValues: useMaxTokenValues(collateralToken, params, form), borrowToken, collateralToken, diff --git a/apps/main/src/llamalend/mutations/add-collateral.mutation.ts b/apps/main/src/llamalend/mutations/add-collateral.mutation.ts index 64c4ad5534..33b823c55b 100644 --- a/apps/main/src/llamalend/mutations/add-collateral.mutation.ts +++ b/apps/main/src/llamalend/mutations/add-collateral.mutation.ts @@ -43,7 +43,8 @@ export const useAddCollateralMutation = ({ mutationKey: [...rootKeys.userMarket({ chainId, marketId, userAddress }), 'add-collateral'] as const, mutationFn: async (mutation, { market }) => { await waitForApproval({ - isApproved: () => fetchAddCollateralIsApproved({ chainId, marketId, userAddress, ...mutation }), + isApproved: () => + fetchAddCollateralIsApproved({ chainId, marketId, userAddress, ...mutation }, { staleTime: 0 }), onApprove: () => approve(market, mutation), message: t`Approved collateral addition`, config, diff --git a/apps/main/src/llamalend/mutations/create-loan.mutation.ts b/apps/main/src/llamalend/mutations/create-loan.mutation.ts index 59d9e9488a..5dd1d08b0e 100644 --- a/apps/main/src/llamalend/mutations/create-loan.mutation.ts +++ b/apps/main/src/llamalend/mutations/create-loan.mutation.ts @@ -72,7 +72,7 @@ export const useCreateLoanMutation = ({ mutationFn: async (mutation, { market }) => { const params = { ...mutation, chainId, marketId } await waitForApproval({ - isApproved: () => fetchBorrowCreateLoanIsApproved(params), + isApproved: async () => await fetchBorrowCreateLoanIsApproved(params, { staleTime: 0 }), onApprove: () => approve(market, mutation), message: t`Approved loan creation`, config, diff --git a/apps/main/src/llamalend/mutations/repay.mutation.ts b/apps/main/src/llamalend/mutations/repay.mutation.ts index 44fc7c3156..0e35a06576 100644 --- a/apps/main/src/llamalend/mutations/repay.mutation.ts +++ b/apps/main/src/llamalend/mutations/repay.mutation.ts @@ -78,15 +78,18 @@ export const useRepayMutation = ({ await waitForApproval({ isApproved: () => - fetchRepayIsApproved({ - chainId, - marketId, - userAddress, - stateCollateral, - userCollateral, - userBorrowed, - isFull, - }), + fetchRepayIsApproved( + { + chainId, + marketId, + userAddress, + stateCollateral, + userCollateral, + userBorrowed, + isFull, + }, + { staleTime: 0 }, + ), onApprove: () => approveRepay(market, mutation), message: t`Approved repayment`, config, diff --git a/apps/main/src/llamalend/mutations/useLlammaMutation.ts b/apps/main/src/llamalend/mutations/useLlammaMutation.ts index 0b81aab6bf..3c6bf19c54 100644 --- a/apps/main/src/llamalend/mutations/useLlammaMutation.ts +++ b/apps/main/src/llamalend/mutations/useLlammaMutation.ts @@ -104,7 +104,6 @@ export function useLlammaMutation { - console.log({ mutationKey, variables }) // Early validation - throwing here prevents mutationFn from running if (!wallet) throw new Error('Missing provider') if (!llamaApi) throw new Error('Missing llamalend api') @@ -121,22 +120,11 @@ export function useLlammaMutation { const market = getLlamaMarket(marketId!) - console.log('mutationFn called with variables:', variables, 'and market:', market) const data = await mutationFn(variables, { market }) throwIfError(data) return { data, receipt: await waitForTransactionReceipt(config, data) } }, onSuccess: async ({ data, receipt }, variables, context) => { - console.log( - `OnSuccess called with data:`, - data, - 'receipt:', - receipt, - 'variables:', - variables, - 'context:', - context, - ) logSuccess(mutationKey, { data, variables, marketId: context.market.id }) notify(successMessage(variables, context), 'success') updateUserEventsApi(wallet!, { id: networkId }, context.market, receipt.transactionHash) diff --git a/apps/main/src/llamalend/queries/validation/borrow.validation.ts b/apps/main/src/llamalend/queries/validation/borrow.validation.ts index 7b2f34a8a4..2fd75710ae 100644 --- a/apps/main/src/llamalend/queries/validation/borrow.validation.ts +++ b/apps/main/src/llamalend/queries/validation/borrow.validation.ts @@ -1,4 +1,4 @@ -import { group } from 'vest' +import { group, skipWhen } from 'vest' import { validateDebt, validateLeverageEnabled, @@ -45,14 +45,18 @@ export const borrowQueryValidationSuite = ({ debtRequired, isMaxDebtRequired = debtRequired, isLeverageRequired = false, + skipMarketValidation = false, }: { debtRequired: boolean isMaxDebtRequired?: boolean isLeverageRequired?: boolean + skipMarketValidation?: boolean }) => createValidationSuite((params: BorrowDebtParams) => { const { chainId, leverageEnabled, marketId, userBorrowed, userCollateral, debt, range, slippage, maxDebt } = params - marketIdValidationSuite({ chainId, marketId }) + skipWhen(skipMarketValidation, () => { + marketIdValidationSuite({ chainId, marketId }) + }) borrowFormValidationGroup( { userBorrowed, userCollateral, debt, range, slippage, leverageEnabled, maxDebt }, { debtRequired, isMaxDebtRequired, isLeverageRequired },