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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/lib/modules/pool/actions/create/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PoolType } from '@balancer/sdk'
import { bn } from '@repo/lib/shared/utils/numbers'
import { bn, isBnParseable } from '@repo/lib/shared/utils/numbers'
import type { GqlPoolType } from '@repo/lib/shared/services/api/generated/graphql'
import { GqlPoolTypeValues } from '@repo/lib/shared/services/api/graphql-enums'
import { fNumCustom } from '@repo/lib/shared/utils/numbers'
Expand Down Expand Up @@ -61,7 +61,7 @@ export function getMinSwapFeePercentage(poolType: PoolType): number {
}

export function getPercentFromPrice(value: string, price: string) {
if (!value) return '0.00'
if (!isBnParseable(value) || !isBnParseable(price) || bn(price).isZero()) return '0.00'
return bn(value).minus(price).div(price).times(100).toFixed(2)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { InfoIconPopover } from '../../InfoIconPopover'
import { useAutoRangeConfigurationOptions } from './useAutoRangeConfigurationOptions'
import { usePoolCreationForm } from '../../PoolCreationFormProvider'
import { NumberInput } from '@repo/lib/shared/components/inputs/NumberInput'
import { bn } from '@repo/lib/shared/utils/numbers'
import { bn, isBnParseable } from '@repo/lib/shared/utils/numbers'
import { getPercentFromPrice } from '../../helpers'
import { formatNumber } from '../../helpers'
import { RadioCard } from '@repo/lib/shared/components/inputs/RadioCardGroup'
Expand Down Expand Up @@ -84,6 +84,9 @@ export function ConfigOptionsGroup<T extends FieldValues = FieldValues>({
const isCustom = forceCustom || (!matchedOption && normalizedFormValue !== '')
const selectedValue = isCustom ? '' : (matchedOption?.rawValue ?? '')
const isCustomTargetPrice = isCustom && name === 'initialTargetPrice'
const hasParseableMinPrice = isBnParseable(initialMinPrice)
const hasParseableTargetPrice = isBnParseable(initialTargetPrice)
const hasParseableMaxPrice = isBnParseable(initialMaxPrice)
const ispriceRangePercentage = name === 'priceRangePercentage'
const isCustomPriceRange = isCustom && ispriceRangePercentage
const isPercentage = name === 'centerednessMargin' || name === 'priceShiftDailyRate'
Expand Down Expand Up @@ -174,16 +177,21 @@ export function ConfigOptionsGroup<T extends FieldValues = FieldValues>({
label={'Range low price'}
name={'initialMinPrice'}
percentageLabel={
initialMinPrice ? getPercentFromPrice(initialMinPrice, initialTargetPrice) : '-10.00'
hasParseableMinPrice && hasParseableTargetPrice
? getPercentFromPrice(initialMinPrice, initialTargetPrice)
: '-10.00'
}
placeholder={
initialTargetPrice ? bn(initialTargetPrice).times(0.9).toString().slice(0, 7) : ''
hasParseableTargetPrice
? bn(initialTargetPrice).times(0.9).toString().slice(0, 7)
: ''
}
validate={value => {
if (Number(value) >= Number(initialTargetPrice)) {
if (!isBnParseable(value)) return true
if (hasParseableTargetPrice && bn(value).gte(bn(initialTargetPrice))) {
return 'Range low price must be less than target price'
}
if (Number(value) >= Number(initialMaxPrice)) {
if (hasParseableMaxPrice && bn(value).gte(bn(initialMaxPrice))) {
return 'Range low price must be less than range high price'
}
return true
Expand All @@ -195,17 +203,22 @@ export function ConfigOptionsGroup<T extends FieldValues = FieldValues>({
label={'Range high price'}
name={'initialMaxPrice'}
percentageLabel={
initialMaxPrice ? getPercentFromPrice(initialMaxPrice, initialTargetPrice) : '10.00'
hasParseableMaxPrice && hasParseableTargetPrice
? getPercentFromPrice(initialMaxPrice, initialTargetPrice)
: '10.00'
}
placeholder={
initialTargetPrice ? formatNumber(bn(initialTargetPrice).times(1.1).toString()) : ''
hasParseableTargetPrice
? formatNumber(bn(initialTargetPrice).times(1.1).toString())
: ''
}
validate={value => {
if (Number(value) <= Number(initialTargetPrice)) {
if (!isBnParseable(value)) return true
if (hasParseableTargetPrice && bn(value).lte(bn(initialTargetPrice))) {
return 'Range high price must be greater than target price'
}
if (Number(value) <= Number(initialMinPrice)) {
return 'Range low price must be greater than range low price'
if (hasParseableMinPrice && bn(value).lte(bn(initialMinPrice))) {
return 'Range high price must be greater than range low price'
}
return true
}}
Expand All @@ -219,7 +232,9 @@ export function ConfigOptionsGroup<T extends FieldValues = FieldValues>({
label={customInputLabel}
name={name}
percentageLabel={
isCustomTargetPrice ? getPercentFromPrice(formValue, options[1].rawValue) : ''
isCustomTargetPrice && isBnParseable(formValue)
? getPercentFromPrice(formValue, options[1].rawValue)
: ''
}
placeholder={options[1].displayValue.replace('%', '')}
validate={value => validateFn(Number.isNaN(value) ? '' : value.toString())}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { describe, expect, test } from 'vitest'
import { calculatePeakPrice, calculateRotationComponents } from './gyro.helpers'

describe('E-CLP rotation calculations', () => {
test('returns empty rotation components for an incomplete peak price', () => {
expect(calculateRotationComponents('.')).toEqual({ c: '', s: '' })
})

test('returns an empty peak price when either rotation component is malformed', () => {
expect(calculatePeakPrice({ c: '1', s: '.' })).toBe('')
})
})
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { bn } from '@repo/lib/shared/utils/numbers'
import { bn, isBnParseable } from '@repo/lib/shared/utils/numbers'

/**
* Calculates rotation components "c" and "s" for E-CLP parameters
* @param peakPrice - tan(θ) where θ is the rotation angle (s/c)
* @returns c and s with 18 decimal precision
*/
export function calculateRotationComponents(peakPrice: string): { c: string; s: string } {
if (!peakPrice || Number(peakPrice) === 0) return { c: '', s: '' }
if (!isBnParseable(peakPrice)) return { c: '', s: '' }

// Convert input to precise decimal representation of tan(θ)
const tanθ = bn(peakPrice)
if (tanθ.isZero()) return { c: '', s: '' }

// Calculate hypotenuse of right triangle with legs (1, tanθ)
// This represents √(1 + tan²θ) from Pythagorean theorem
Expand All @@ -32,7 +33,10 @@ export function calculateRotationComponents(peakPrice: string): { c: string; s:
* @returns peakPrice as tan(θ) = s/c
*/
export function calculatePeakPrice({ c, s }: { c: string; s: string }): string {
if (!c || !s || Number(c) === 0) return ''
if (!isBnParseable(c) || !isBnParseable(s)) return ''

return bn(s).div(bn(c)).toString()
const cValue = bn(c)
if (cValue.isZero()) return ''

return bn(s).div(cValue).toString()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { describe, expect, test } from 'vitest'
import { calculatePriceBounds } from './useAutoRangeConfigurationOptions'

describe('calculatePriceBounds', () => {
test('clears derived bounds when a numeric draft is incomplete', () => {
expect(calculatePriceBounds('.', '50')).toEqual({ initialMinPrice: '', initialMaxPrice: '' })
expect(calculatePriceBounds('1', '.')).toEqual({ initialMinPrice: '', initialMaxPrice: '' })
})
})
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { usePoolCreationForm } from '../../PoolCreationFormProvider'
import { bn } from '@repo/lib/shared/utils/numbers'
import { bn, isBnParseable } from '@repo/lib/shared/utils/numbers'
import { useEffect, useRef } from 'react'
import { formatNumber } from '../../helpers'
import { usePoolSpotPriceWithoutRate } from './usePoolSpotPriceWithoutRate'
Expand All @@ -21,6 +21,22 @@ import { useWatch } from 'react-hook-form'
import { ConfigOptionsGroupProps } from './AutoRangeConfiguration'
import { AutoRangeConfig } from '../../types'

export function calculatePriceBounds(targetPrice: string, spread: string) {
if (!isBnParseable(targetPrice) || !isBnParseable(spread)) {
return { initialMinPrice: '', initialMaxPrice: '' }
}

const spreadValue = bn(spread)
const lowerBoundPercentage = (100 - spreadValue.toNumber()) / 100
const upperBoundPercentage = (100 + spreadValue.toNumber()) / 100
const targetPriceValue = bn(targetPrice)

return {
initialMinPrice: targetPriceValue.times(lowerBoundPercentage).toString(),
initialMaxPrice: targetPriceValue.times(upperBoundPercentage).toString(),
}
}

export function useAutoRangeConfigurationOptions(): ConfigOptionsGroupProps<AutoRangeConfig>[] {
const { poolCreationForm, autoRangeConfigForm } = usePoolCreationForm()
const lastCalculatedPriceBoundsRef = useRef({ minPrice: '', maxPrice: '' })
Expand All @@ -35,16 +51,6 @@ export function useAutoRangeConfigurationOptions(): ConfigOptionsGroupProps<Auto
const currentPriceMinus5 = spotPriceWithoutRate.times(0.95).toString()
const currentPricePlus5 = spotPriceWithoutRate.times(1.05).toString()

const calculatePriceBounds = (targetPrice: string, spread: string) => {
const lowerBoundPercentage = (100 - bn(spread).toNumber()) / 100
const upperBoundPercentage = (100 + bn(spread).toNumber()) / 100

const initialMinPrice = bn(targetPrice).times(lowerBoundPercentage).toString()
const initialMaxPrice = bn(targetPrice).times(upperBoundPercentage).toString()

return { initialMinPrice, initialMaxPrice }
}

const updatePriceBounds = (targetPrice: string, spread: string) => {
const { initialMinPrice, initialMaxPrice } = calculatePriceBounds(targetPrice, spread)
autoRangeConfigForm.setValue('initialMinPrice', initialMinPrice, { shouldValidate: true })
Expand Down