Skip to content

Commit

Permalink
Merge pull request #57 from commons-stack/initial-reserve
Browse files Browse the repository at this point in the history
Do not allow users set an initial reserve higher than their balances
  • Loading branch information
jorvixsky authored Oct 20, 2023
2 parents a892a59 + 14a99c0 commit 0fc1386
Show file tree
Hide file tree
Showing 15 changed files with 151 additions and 58 deletions.
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;

0 comments on commit 0fc1386

Please sign in to comment.