Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not allow users set an initial reserve higher than their balances #57

Merged
merged 2 commits into from
Oct 20, 2023
Merged
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
25 changes: 17 additions & 8 deletions apps/abclaunch/src/components/dao-steps/ConfigureAbc.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Divider, Button, FormControl, FormLabel, HStack, InputGroup, Input, InputRightElement, Text, VStack, Image, Tooltip, Menu, MenuButton, MenuList, Flex, MenuItem } from "@chakra-ui/react";
import { InfoOutlineIcon, ChevronDownIcon } from '@chakra-ui/icons';
import { InfoOutlineIcon, ChevronDownIcon, WarningTwoIcon } from '@chakra-ui/icons';
import React from 'react';
import { useRecoilState } from "recoil";
import { newDaoAbcState } from "../../recoil";
import { collateralTokenList, getCollateralTokenInfo } from "../../utils/token-info";
import { BalanceInput } from "dao-utils";

export default function ConfigureToken() {

const [abcSettings, setAbcSettings] = useRecoilState(newDaoAbcState);

const enoughBalance = true;
const enoughBalance = abcSettings.reserveInitialBalanceIsEnough !== false;

function handleReserveRatioChange(reserveRatio: string) {
/^\d*\.?\d*$/.test(reserveRatio) && setAbcSettings(settings => ({ ...settings, reserveRatio }));
Expand All @@ -19,8 +20,9 @@ export default function ConfigureToken() {
setAbcSettings(settings => ({ ...settings, collateralToken }));
}

function handleInitialReserveChange(reserveInitialBalance: string) {
/^\d*\.?\d*$/.test(reserveInitialBalance) && setAbcSettings(settings => ({ ...settings, reserveInitialBalance }));
function handleInitialReserveChange({value, isEnough} : {value: string, isEnough: boolean | undefined}) {
(value !== abcSettings.reserveInitialBalance || isEnough !== abcSettings.reserveInitialBalanceIsEnough) &&
setAbcSettings(settings => ({ ...settings, reserveInitialBalance: value, reserveInitialBalanceIsEnough: isEnough }));
}

function handleEntryTributeChange(entryTribute: string) {
Expand Down Expand Up @@ -122,9 +124,16 @@ export default function ConfigureToken() {
</MenuList>
</Menu>
<Flex>
<InputGroup>
<Input placeholder="Enter value" borderLeft="0" borderTopLeftRadius="0" borderBottomLeftRadius="0" value={abcSettings.reserveInitialBalance} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInitialReserveChange((e.target.value))} />
</InputGroup>
<BalanceInput
placeholder="Enter value"
borderLeft="0"
borderTopLeftRadius="0"
borderBottomLeftRadius="0"
value={abcSettings.reserveInitialBalance}
token={abcSettings.collateralToken as `0x${string}`}
decimals={getCollateralTokenInfo(abcSettings.collateralToken)?.decimals || 18}
setValue={handleInitialReserveChange}
/>
</Flex>
</HStack>
</FormControl>
Expand Down Expand Up @@ -176,7 +185,7 @@ export default function ConfigureToken() {
</VStack>}
{!enoughBalance &&
<HStack mt="32px">
<Image src="../../..//public/Error.svg" w="32px" h="32px" mr="8px" />
<WarningTwoIcon color="red.500" w="32px" h="32px" mr="8px" />
<VStack spacing={0} alignItems="start">
<Text fontSize="16px" color="brand.1200">You do not have the amount specified in Initial Reserve Balance in your wallet.</Text>
<Text fontSize="16px" color="brand.1200">You must have at least that much in your wallet in order to proceed.</Text>
Expand Down
2 changes: 1 addition & 1 deletion apps/abclaunch/src/components/dao-steps/ConfigureToken.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default function ConfigureToken() {
}

return (
<VStack spacing={4} pt="75px" className="abcs-newdao-step-content" mx="100px">
<VStack spacing={4} pt="75px" mx="100px">
<Text fontFamily="VictorSerifTrial" fontSize="72px" color="brand.900">Token</Text>
<Text fontSize="24px" color="brand.900" pt="16px">Configure the DAO's token parameters.</Text>
<Divider paddingTop="24px"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function TimeInput ({ timeUnit }: {timeUnit: 'Days' | 'Hours' | 'Minutes'}) {

export default function ConfigureVoting() {
return (
<VStack spacing={4} mt="75" className="abcs-newdao-step-content">
<VStack spacing={4} mt="75">
<Text fontFamily="VictorSerifTrial" fontSize="72px" color="brand.900">Voting</Text>
<Text fontSize="24px" color="brand.900" mt="16px">Configure the DAO's voting parameters</Text>
<Divider paddingTop="24px"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function OrganizationName() {

return (
<Box pt="100px" pb="75px">
<VStack spacing={0} className="abcs-newdao-step-content">
<VStack spacing={0}>
<Text mb="48px" fontFamily="VictorSerifTrial" fontSize="72px" color="brand.900">Name your DAO</Text>
<DaoNameInput
daoName={organizationSettings.name}
Expand Down
5 changes: 3 additions & 2 deletions apps/abclaunch/src/recoil/newDaoAbc/abcIsValidSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import newDaoAbcAtom from "./atom";
export default selector({
key: 'newDaoAbcIsValid',
get: ({get}) => {
const {entryTribute, exitTribute, reserveRatio, collateralToken} = get(newDaoAbcAtom);
const {entryTribute, exitTribute, reserveRatio, collateralToken, reserveInitialBalanceIsEnough} = get(newDaoAbcAtom);
return Number(entryTribute) <= 100 && Number(exitTribute) <= 100 && Number(reserveRatio) <= 100
&& Number(entryTribute) >= 0 && Number(exitTribute) >= 0 && Number(reserveRatio) >= 0
&& collateralToken !== `0x${'0'.repeat(40)}`;
&& !!collateralToken
&& !!reserveInitialBalanceIsEnough;
},
});
6 changes: 4 additions & 2 deletions apps/abclaunch/src/recoil/newDaoAbc/atom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@ import { atom } from "recoil";
type NewDaoAbcData = {
reserveRatio: string;
reserveInitialBalance: string;
reserveInitialBalanceIsEnough: boolean | undefined;
entryTribute: string;
exitTribute: string;
collateralToken: string;
collateralToken: string | undefined;
}

export default atom<NewDaoAbcData>({
key: 'newDaoAbc',
default: {
reserveRatio: '20',
reserveInitialBalance: '0',
reserveInitialBalanceIsEnough: true,
entryTribute: '0',
exitTribute: '0',
collateralToken: `0x${'0'.repeat(40)}`,
collateralToken: undefined,
}
});
2 changes: 1 addition & 1 deletion apps/abclaunch/src/utils/token-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ export const collateralTokenList = [
new TokenInfo("GIV", "GIV", "0x528cdc92eab044e1e39fe43b9514bfdab4412b98", "/token-icons/giv-icon.png", 18),
];

export function getCollateralTokenInfo(tokenAddress: string): TokenInfo | undefined {
export function getCollateralTokenInfo(tokenAddress?: string): TokenInfo | undefined {
return collateralTokenList.find(token => token.tokenAddress === tokenAddress);
}
4 changes: 3 additions & 1 deletion pkg/dao-utils/index.tsx
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { default as useIsRegisteredDao } from "./src/hooks/useIsRegisteredDao";
export { default as DaoNameInput } from "./src/components/DaoNameInput";
export { default as useIsBalanceEnough } from "./src/hooks/useIsBalanceEnough";
export { default as DaoNameInput } from "./src/components/DaoNameInput";
export { default as BalanceInput } from "./src/components/BalanceInput";
7 changes: 1 addition & 6 deletions pkg/dao-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@
"typescript": "^5.0.2",
"vite": "^4.4.5",
"@fontsource/roboto": "^5.0.8",
"@chakra-ui/icons": "^2.1.1",
"usehooks-ts": "2.9.1"
},
"dependencies": {
"ethjs-ens": "^2.0.1",
"js-sha3": "^0.9.2"
"@chakra-ui/icons": "^2.1.1"
}
}
42 changes: 42 additions & 0 deletions pkg/dao-utils/src/components/BalanceInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Input, InputGroup, InputRightElement } from '@chakra-ui/react';
import { useIsBalanceEnough } from '../..';
import { useEffect } from 'react';
import FetchingInputIcon from './FetchingInputIcon';

export default function BalanceInput({
value,
token,
decimals,
setValue,
...props
}: {
value: string;
token: `0x${string}`;
decimals: number;
setValue: (value: {value: string, isEnough: boolean | undefined}) => void;
} & Omit<React.ComponentProps<typeof Input>, 'onChange'>) {
const { isEnough, error, isLoading } = useIsBalanceEnough({value, token, decimals});

function handleValueChange(value: string) {
setValue({value: value, isEnough: undefined});
}

useEffect(() => {
setValue({ value, isEnough });
}, [isEnough, value, setValue]);

return (
<InputGroup>
<Input
value={value}
onChange={e => handleValueChange(e.target.value)}
errorBorderColor='red.500'
isInvalid={isEnough === false}
{...props}
/>
<InputRightElement>
<FetchingInputIcon isLoading={isLoading} inputValue={value} fetched={isEnough} error={!!error} positiveIcon={<></>} spinnerIcon={<></>} />
</InputRightElement>
</InputGroup>
)
}
15 changes: 3 additions & 12 deletions pkg/dao-utils/src/components/DaoNameInput.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
import { Input, InputGroup, InputRightElement, Spinner } from '@chakra-ui/react';
import { CheckCircleIcon, WarningTwoIcon } from '@chakra-ui/icons';
import { Input, InputGroup, InputRightElement } from '@chakra-ui/react';
import { useIsRegisteredDao } from '../..';
import { useEffect } from 'react';

function DaoNameInputIcon ({ daoName, inverted, isLoading, isDaoRegistered, error }: { daoName: string, inverted: boolean, isLoading: boolean, isDaoRegistered: boolean, error: boolean}) {
return (
daoName.length == 0 ? <></> :
isLoading || isDaoRegistered === undefined ? <Spinner size='xs' /> :
!error && (!inverted && isDaoRegistered || inverted && !isDaoRegistered) ? <CheckCircleIcon color="brand.500" /> :
<WarningTwoIcon color="red.500" />
)
}
import FetchingInputIcon from './FetchingInputIcon';

export default function DaoNameInput({
daoName,
Expand Down Expand Up @@ -52,7 +43,7 @@ export default function DaoNameInput({
_hover={{ 'color': 'black' }}
/>
<InputRightElement>
<DaoNameInputIcon daoName={daoName} inverted={inverted} isLoading={isLoading} isDaoRegistered={!!isDaoRegistered} error={!!error} />
<FetchingInputIcon inputValue={daoName} inverted={inverted} isLoading={isLoading} fetched={!!isDaoRegistered} error={!!error} />
</InputRightElement>
</InputGroup>
)
Expand Down
20 changes: 20 additions & 0 deletions pkg/dao-utils/src/components/FetchingInputIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Spinner } from '@chakra-ui/react'
import { CheckCircleIcon, WarningTwoIcon } from '@chakra-ui/icons'

export default function FetchingInputIcon({ inputValue, inverted, isLoading, fetched, error, positiveIcon, negativeIcon, spinnerIcon }: {
inputValue: string,
inverted?: boolean,
isLoading: boolean,
fetched?: boolean,
error: boolean,
positiveIcon?: React.ReactElement,
negativeIcon?: React.ReactElement,
spinnerIcon?: React.ReactElement,
}) {
return (
!inputValue ? <></> :
isLoading || fetched === undefined ? spinnerIcon ?? <Spinner size='xs' /> :
!error && (!inverted && fetched || inverted && !fetched) ? positiveIcon ?? <CheckCircleIcon color="brand.500" /> :
negativeIcon ?? <WarningTwoIcon color="red.500" />
)
}
20 changes: 20 additions & 0 deletions pkg/dao-utils/src/hooks/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useEffect, useState } from "react";

// Modified from https://usehooks-ts.com/react-hook/use-debounce/
export default function useDebounce<T>(value: T, delay?: number): [T, boolean] {
const [isDebouncing, setIsDebouncing] = useState(true);
const [debouncedValue, setDebouncedValue] = useState<T>(value)

useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
setIsDebouncing(false);
}, delay || 500)

return () => {
clearTimeout(timer)
}
}, [value, delay])

return [debouncedValue, isDebouncing]
}
27 changes: 27 additions & 0 deletions pkg/dao-utils/src/hooks/useIsBalanceEnough.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useAccount, useBalance } from 'wagmi';
import { parseUnits } from 'viem';
import useDebounce from './useDebounce';

/**
* A custom React hook that checks if the user's token balance is enough for a given value.
* @param value - The value to check if the balance is enough for.
* @param token - The token address to check the balance of.
* @param decimals - The number of decimals for the token.
* @returns An object containing the `isEnough`, `error`, and `isLoading` properties.
* - `isEnough` is a boolean indicating if the balance is enough for the given value.
* - `error` is an error object if there was an error fetching the balance.
* - `isLoading` is a boolean indicating if the balance is currently being fetched.
*/
function useIsBalanceEnough({value, token, decimals}: {value: string, token: `0x${string}`, decimals: number}, delay?: number) {

const [debouncedValue, isDebouncing] = useDebounce(value, delay);
const { address } = useAccount()
const { data: balance, error, isLoading: isFetching } = useBalance({address, token});

const isLoading = !address || isFetching || isDebouncing;
const isEnough = !isLoading && token && balance ? balance.value >= parseUnits(debouncedValue, decimals) : undefined;

return { isEnough, error, isLoading };
}

export default useIsBalanceEnough;
30 changes: 7 additions & 23 deletions pkg/dao-utils/src/hooks/useIsRegisteredDao.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import { useState, useEffect } from 'react';
import { useDebounce } from 'usehooks-ts';
import { parseAbi } from 'viem';
import { namehash, normalize } from 'viem/ens';
import { useContractRead } from 'wagmi';
import useDebounce from './useDebounce';

const aragonEnsContract = '0x6f2CA655f58d5fb94A08460aC19A552EB19909FD';
const zeroAddress = `0x${'0'.repeat(40)}`;

function useIsRegisteredDaoWithoutDebounce(name: string) {
function useIsRegisteredDao(name: string, delay?: number) {

const [debouncedName, isDebouncing] = useDebounce(name, delay);

let normalizedName: string = '';
let error: Error | null = null;

try {
normalizedName = name.length > 0 ? normalize(`${name}.aragonid.eth`) : '';
normalizedName = debouncedName.length > 0 ? normalize(`${debouncedName}.aragonid.eth`) : '';
} catch (e: unknown) {
error = e as Error;
}

const { data, error: contractError, isLoading } = useContractRead({
const { data, error: contractError, isLoading: isFetching } = useContractRead({
address: aragonEnsContract,
abi: parseAbi([
'function resolver(bytes32 _node) view returns (address)'
Expand All @@ -28,27 +29,10 @@ function useIsRegisteredDaoWithoutDebounce(name: string) {
});

error = error || contractError;
const isLoading = isFetching || isDebouncing;
const isRegistered = data && data !== zeroAddress;

return { isRegistered, error, isLoading };
}

function useIsRegisteredDao(name: string, delay?: number) {
const [isDebouncing, setIsDebouncing] = useState(true);
const debounceDaoName = useDebounce(name, delay);

useEffect(() => {
setIsDebouncing(true);
const handler = setTimeout(() => setIsDebouncing(false), delay);
return () => clearTimeout(handler);
}, [name, delay]);

const { isRegistered, error, isLoading: isFetching } = useIsRegisteredDaoWithoutDebounce(debounceDaoName);

// isLoading will be true either if we're debouncing or if the request is being made
const isLoading = isDebouncing || isFetching;

return { isRegistered, error, isLoading };
}

export default useIsRegisteredDao;