Skip to content

Commit

Permalink
DAO-547: Fix invalid proposals (#91)
Browse files Browse the repository at this point in the history
* validate decimal places while user inputs the amount

* minimum amount of 1 rif

* fix NaN validation

* calculate amount usd conversion

* format treaury fiat amount

* fix build

* fix InputNumber props error
  • Loading branch information
rodrigoncalves authored Aug 5, 2024
1 parent 5b749f5 commit 4cebea4
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 35 deletions.
17 changes: 14 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"react-dom": "^18",
"react-hook-form": "^7.52.1",
"react-icons": "^5.2.1",
"react-number-format": "^5.4.0",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"viem": "^2.17.3",
Expand Down
7 changes: 4 additions & 3 deletions src/app/treasury/TreasuryContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { useGetTreasuryBucketBalance } from '@/app/treasury/hooks/useGetTreasury
import { currentEnvTreasuryContracts } from '@/lib/contracts'
import { Address } from 'viem'
import { GetPricesResult } from '@/app/user/types'
import { formatCurrency } from '@/lib/utils'

type BucketItem = {
amount: string
fiatAmount: number
fiatAmount: string
}

type Bucket = {
Expand Down Expand Up @@ -50,11 +51,11 @@ const getBucketBalance = (
) => ({
RIF: {
amount: bucketBalance.RIF.balance,
fiatAmount: Number(bucketBalance.RIF.balance) * prices.RIF.price,
fiatAmount: formatCurrency(Number(bucketBalance.RIF.balance) * (prices.RIF?.price ?? 0)),
},
rBTC: {
amount: bucketBalance.rBTC.balance,
fiatAmount: Number(bucketBalance.rBTC.balance) * prices.rBTC.price,
fiatAmount: formatCurrency(Number(bucketBalance.rBTC.balance) * (prices.rBTC?.price ?? 0)),
},
})

Expand Down
8 changes: 4 additions & 4 deletions src/app/user/Stake/UnStakingSteps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@ const UnStakingSteps = ({ onCloseModal }: StakingStepsProps) => {
balance: balances.stRIF.balance,
symbol: balances.stRIF.symbol,
contract: currentEnvContracts.stRIF,
price: prices.stRIF.price.toString(),
price: prices.stRIF?.price.toString(),
}),
[balances.stRIF.balance, balances.stRIF.symbol, prices.stRIF.price],
[balances.stRIF.balance, balances.stRIF.symbol, prices.stRIF?.price],
)

const tokenToReceive: StakingToken = useMemo(
() => ({
balance: balances.RIF.balance,
symbol: balances.RIF.symbol,
contract: currentEnvContracts.RIF,
price: prices.RIF.price.toString(),
price: prices.RIF?.price.toString(),
}),
[balances.RIF.balance, balances.RIF.symbol, prices.RIF.price],
[balances.RIF.balance, balances.RIF.symbol, prices.RIF?.price],
)

return (
Expand Down
6 changes: 3 additions & 3 deletions src/components/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import * as React from 'react'
import { cn } from '@/lib/utils'
import { useFormField } from '../Form'

export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}

const DEFAULT_CLASSES = `
export const INPUT_DEFAULT_CLASSES = `
flex w-full
p-[12px]
justify-between
Expand All @@ -26,7 +26,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type,
return (
<input
id={formItemId}
className={cn(DEFAULT_CLASSES, error && 'border-st-error focus-visible:ring-0', className)}
className={cn(INPUT_DEFAULT_CLASSES, error && 'border-st-error focus-visible:ring-0', className)}
ref={ref}
type={type}
{...props}
Expand Down
33 changes: 33 additions & 0 deletions src/components/Input/InputNumber.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { cn } from '@/lib/utils'
import React from 'react'
import { InputAttributes, NumericFormat, NumericFormatProps } from 'react-number-format'
import { useFormField } from '../Form'
import { INPUT_DEFAULT_CLASSES } from './Input'

interface InputNumberProps extends NumericFormatProps<InputAttributes> {
prefix?: string
decimalScale?: number
}

const InputNumber = React.forwardRef<NumericFormatProps<InputAttributes>, InputNumberProps>(
({ prefix, decimalScale = 8, className, type, max = Number.MAX_SAFE_INTEGER, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<NumericFormat
id={formItemId}
className={cn(INPUT_DEFAULT_CLASSES, error && 'border-st-error focus-visible:ring-0', className)}
getInputRef={ref}
decimalSeparator="."
prefix={prefix}
decimalScale={decimalScale}
allowNegative={false}
isAllowed={({ floatValue }) => !floatValue || floatValue <= Number(max)}
{...props}
/>
)
},
)

InputNumber.displayName = 'InputNumber'

export { InputNumber }
1 change: 1 addition & 0 deletions src/components/Input/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './Input'
export * from './InputNumber'
47 changes: 25 additions & 22 deletions src/pages/proposals/create.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
'use client'
import { useCreateProposal } from '@/app/proposals/hooks/useCreateProposal'
import { useVotingPower } from '@/app/proposals/hooks/useVotingPower'
import { TRANSACTION_SENT_MESSAGES } from '@/app/proposals/shared/utils'
import { useBalancesContext } from '@/app/user/Balances/context/BalancesContext'
import { useGetSpecificPrices } from '@/app/user/Balances/hooks/useGetSpecificPrices'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/Accordion'
import { Alert } from '@/components/Alert/Alert'
import { Button } from '@/components/Button'
import Image from 'next/image'
import {
Form,
FormControl,
Expand All @@ -13,45 +16,37 @@ import {
FormLabel,
FormMessage,
} from '@/components/Form'
import { Input } from '@/components/Input'
import { Input, InputNumber } from '@/components/Input'
import { MainContainer } from '@/components/MainContainer/MainContainer'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/Select'
import { Textarea } from '@/components/Textarea'
import { Header, Paragraph } from '@/components/Typography'
import { currentEnvContracts } from '@/lib/contracts'
import { sanitizeInputNumber } from '@/lib/utils'
import { formatCurrency } from '@/lib/utils'
import { usePricesContext } from '@/shared/context/PricesContext'
import { zodResolver } from '@hookform/resolvers/zod'
import Image from 'next/image'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useForm } from 'react-hook-form'
import { GoRocket } from 'react-icons/go'
import { Address } from 'viem'
import { z } from 'zod'
import { Alert } from '@/components/Alert/Alert'
import { TRANSACTION_SENT_MESSAGES } from '@/app/proposals/shared/utils'

const ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/
const MAX_AMOUNT = 999999999

const FormSchema = z.object({
proposalName: z.string().min(3).max(100),
description: z.string().min(3).max(3000),
toAddress: z.string().refine(value => ADDRESS_REGEX.test(value), 'Please enter a valid address'),
tokenAddress: z.string().length(42),
amount: z.union([z.string().transform(v => v.replace(/[^0-9.-]+/g, '')), z.number()]).pipe(
z.coerce
.number()
.positive()
.max(999999999)
.refine(value => {
const valueStr = sanitizeInputNumber(value)
const decimals = valueStr.split('.')[1]
return !decimals || decimals.length <= 8
}, 'Amount must have up to 8 decimals'),
),
amount: z.coerce.number().positive('Required').min(1).max(MAX_AMOUNT),
})

export default function CreateProposal() {
const router = useRouter()
const prices = useGetSpecificPrices()
const { isLoading: isVotingPowerLoading, canCreateProposal } = useVotingPower()
const { onCreateProposal } = useCreateProposal()
const [message, setMessage] = useState<
Expand All @@ -76,13 +71,20 @@ export default function CreateProposal() {
control,
handleSubmit,
formState: { touchedFields, errors, isValid, isDirty },
watch,
} = form

const pricesMap = useMemo(() => ({ [currentEnvContracts.RIF]: prices.RIF }), [prices])

const isProposalNameValid = !errors.proposalName && touchedFields.proposalName
const isDescriptionValid = !errors.description && touchedFields.description
const isToAddressValid = !errors.toAddress && touchedFields.toAddress
const isAmountValid = !errors.amount && touchedFields.amount
const isProposalCompleted = isProposalNameValid && isDescriptionValid
const isActionsCompleted = isToAddressValid && isAmountValid
const amountValue = watch('amount')
const tokenAddress = watch('tokenAddress')
const amountUsd = pricesMap[tokenAddress] ? amountValue * pricesMap[tokenAddress]?.price : 0

const onSubmit = async (data: z.infer<typeof FormSchema>) => {
const { proposalName, description, toAddress, tokenAddress, amount } = data
Expand Down Expand Up @@ -249,16 +251,17 @@ export default function CreateProposal() {
<FormItem className="mb-6 mx-1">
<FormLabel>Amount</FormLabel>
<FormControl>
<Input
<InputNumber
placeholder="0.00"
type="number"
className="w-64"
min={0}
max={999999999}
max={MAX_AMOUNT}
autoComplete="off"
{...field}
/>
</FormControl>
<FormDescription>= $ USD 0.00</FormDescription>
{amountValue?.toString() && (
<FormDescription>= USD {formatCurrency(amountUsd)}</FormDescription>
)}
<FormMessage />
</FormItem>
)}
Expand Down

0 comments on commit 4cebea4

Please sign in to comment.