diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4676e30e..1db4f777 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -5,7 +5,7 @@ contact_links: url: https://daohaus.club/ about: Learn more about DAOhaus - name: DAOhaus Discord - url: https://discord.gg/daohaus + url: https://discord.gg/kJaVkXtsXA about: 'Join our discord and report issues in #support' - name: DAOhaus Developer Docs and Contribution Guidelines url: https://docs.daohaus.club/ diff --git a/libs/form-builder/src/FormBuilder.tsx b/libs/form-builder/src/FormBuilder.tsx index 2b0d8fdd..9c2dd772 100644 --- a/libs/form-builder/src/FormBuilder.tsx +++ b/libs/form-builder/src/FormBuilder.tsx @@ -148,7 +148,7 @@ export const FormBuilder = ({ onSubmit={handleSubmit} footer={ diff --git a/libs/keychain-utils/src/contractKeychains.ts b/libs/keychain-utils/src/contractKeychains.ts index 2919422b..209dd30a 100644 --- a/libs/keychain-utils/src/contractKeychains.ts +++ b/libs/keychain-utils/src/contractKeychains.ts @@ -96,7 +96,7 @@ export const CONTRACT_KEYCHAINS: Record = { '0xaa36a7': '0x000000000000aDdB49795b0f9bA5BC298cDda236', '0x64': '0x00000000000DC7F163742Eb4aBEf650037b1f588', '0x89': '0x00000000000DC7F163742Eb4aBEf650037b1f588', - '0xa': '0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC', + '0xa': '0x00000000000DC7F163742Eb4aBEf650037b1f588', '0xa4b1': '0x00000000000DC7F163742Eb4aBEf650037b1f588', '0x2105': '0x000000000000aDdB49795b0f9bA5BC298cDda236', }, diff --git a/libs/moloch-v3-fields/src/config/fieldConfig.ts b/libs/moloch-v3-fields/src/config/fieldConfig.ts index 658fab4c..bb66aedd 100644 --- a/libs/moloch-v3-fields/src/config/fieldConfig.ts +++ b/libs/moloch-v3-fields/src/config/fieldConfig.ts @@ -20,6 +20,7 @@ import { SafeSelect } from '../fields'; import { MultisendActions } from '../fields'; import { AddressesAndAmounts } from '../fields'; import { EpochDatePicker } from '../fields'; +import { TransferTokens } from '../fields'; export const MolochFields = { ...CoreFieldLookup, @@ -41,6 +42,7 @@ export const MolochFields = { addressesAndAmounts: AddressesAndAmounts, epochDatePicker: EpochDatePicker, markdownField: MarkdownField, + transferTokens: TransferTokens, }; export type MolochFieldLego = FieldLegoBase; diff --git a/libs/moloch-v3-fields/src/fields/RequestERC20.tsx b/libs/moloch-v3-fields/src/fields/RequestERC20.tsx index fbd21968..27437f09 100644 --- a/libs/moloch-v3-fields/src/fields/RequestERC20.tsx +++ b/libs/moloch-v3-fields/src/fields/RequestERC20.tsx @@ -2,10 +2,10 @@ import { useEffect, useMemo } from 'react'; import { RegisterOptions, useFormContext } from 'react-hook-form'; import { - formatValueTo, handleBaseUnits, ignoreEmptyVal, toWholeUnits, + truncValue, ValidateField, } from '@daohaus/utils'; import { isValidNetwork } from '@daohaus/keychain-utils'; @@ -74,17 +74,21 @@ export const RequestERC20 = ( return erc20s.find(({ address }) => address === paymentTokenAddr); }, [paymentTokenAddr, erc20s]); - const tokenBalance = selectedToken?.daoBalance - ? formatValueTo({ - value: toWholeUnits(selectedToken?.daoBalance, selectedToken?.decimals), - decimals: 6, - format: 'number', - }) - : '0'; + const displayBalance = useMemo(() => { + if (!selectedToken || BigInt(selectedToken.daoBalance) === BigInt(0)) + return '0'; + return truncValue( + toWholeUnits(selectedToken.daoBalance, selectedToken.decimals), + 6 + ); + }, [selectedToken]); const setMax = () => { if (!selectedToken) return; - setValue(amtId, tokenBalance.trim()); + setValue( + amtId, + toWholeUnits(selectedToken?.daoBalance || '0', selectedToken?.decimals) + ); }; const newRules: RegisterOptions = { @@ -116,7 +120,7 @@ export const RequestERC20 = ( options={selectOptions || []} rightAddon={ } rules={newRules} diff --git a/libs/moloch-v3-fields/src/fields/RequestNativeToken.tsx b/libs/moloch-v3-fields/src/fields/RequestNativeToken.tsx index 85b19292..87d40816 100644 --- a/libs/moloch-v3-fields/src/fields/RequestNativeToken.tsx +++ b/libs/moloch-v3-fields/src/fields/RequestNativeToken.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { RegisterOptions, useFormContext } from 'react-hook-form'; -import { toWholeUnits, handleBaseUnits } from '@daohaus/utils'; +import { handleBaseUnits, toWholeUnits, truncValue } from '@daohaus/utils'; import { Buildable, Button, WrappedInput } from '@daohaus/ui'; import { isValidNetwork } from '@daohaus/keychain-utils'; import { useDaoData, useCurrentDao } from '@daohaus/moloch-v3-hooks'; @@ -29,6 +29,15 @@ export const RequestNativeToken = ( return getNetworkToken(dao, daoChain, safeAddress); }, [dao, daoChain, safeAddress]); + const displayBalance = useMemo(() => { + if (!networkTokenData || BigInt(networkTokenData.daoBalance) === BigInt(0)) + return '0'; + return truncValue( + toWholeUnits(networkTokenData.daoBalance, networkTokenData.decimals), + 6 + ); + }, [networkTokenData]); + const label = networkTokenData?.name ? `Request ${networkTokenData.name}` : `Request Network Token`; @@ -56,11 +65,7 @@ export const RequestNativeToken = ( defaultValue="0" rightAddon={ } rules={newRules} diff --git a/libs/moloch-v3-fields/src/fields/TransferTokens.tsx b/libs/moloch-v3-fields/src/fields/TransferTokens.tsx new file mode 100644 index 00000000..80cadab6 --- /dev/null +++ b/libs/moloch-v3-fields/src/fields/TransferTokens.tsx @@ -0,0 +1,159 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { RegisterOptions, useFormContext } from 'react-hook-form'; + +import { LOCAL_ABI } from '@daohaus/abis'; +import { ValidNetwork } from '@daohaus/keychain-utils'; +import { MolochV3Dao } from '@daohaus/moloch-v3-data'; +import { + useConnectedMember, + useCurrentDao, + useDaoData, +} from '@daohaus/moloch-v3-hooks'; +import { + Buildable, + Button, + Field, + WrappedInput, + WrappedInputSelect, +} from '@daohaus/ui'; +import { + createViemClient, + handleBaseUnits, + ignoreEmptyVal, + toWholeUnits, + ValidateField, + ZERO_ADDRESS, +} from '@daohaus/utils'; + +type Token = { + name: string; + value: string; + paused: boolean; + decimals: number; +}; + +export const TransferTokens = (props: Buildable) => { + const { connectedMember } = useConnectedMember(); + const { daoChain } = useCurrentDao(); + const { dao } = useDaoData(); + const { register, setValue, watch } = useFormContext(); + const [selectOptions, setSelectOptions] = useState>([]); + + const selectedTokenId = 'paymentTokenAddress'; + const selectedTokenAddr = watch(selectedTokenId); + const tokenErrorMsg = 'Token is non-transferrable'; + + const recipientId = 'recipientAddress'; + + register(recipientId); + register(selectedTokenId); + + useEffect(() => { + const getDAOTokens = async (_dao: MolochV3Dao, chainId: ValidNetwork) => { + const client = createViemClient({ + chainId, + }); + const sharesDecimals = await client.readContract({ + abi: LOCAL_ABI.ERC20, + address: _dao.sharesAddress as `0x${string}`, + functionName: 'decimals', + }); + const lootDecimals = await client.readContract({ + abi: LOCAL_ABI.ERC20, + address: _dao.lootAddress as `0x${string}`, + functionName: 'decimals', + }); + setSelectOptions([ + { + name: `${_dao.shareTokenName} (Voting Tokens)`, + value: _dao.sharesAddress, + paused: _dao.sharesPaused, + decimals: Number(sharesDecimals), + }, + { + name: `${_dao.lootTokenName} (Non-Voting Tokens)`, + value: _dao.lootAddress, + paused: _dao.lootPaused, + decimals: Number(lootDecimals), + }, + ]); + }; + if (!dao || !daoChain) return; + getDAOTokens(dao, daoChain); + }, [dao, daoChain]); + + const selectedToken = useMemo(() => { + if (!selectOptions || !selectedTokenAddr) return; + return selectOptions.find((opt) => opt.value === selectedTokenAddr); + }, [selectOptions, selectedTokenAddr]); + + const tokenBalance = useMemo(() => { + if (!connectedMember || !dao || !selectedToken) return '0'; + if (selectedToken.value === dao.sharesAddress) + return toWholeUnits(connectedMember.shares, selectedToken.decimals); + return toWholeUnits(connectedMember.loot, selectedToken.decimals); + }, [connectedMember, dao, selectedToken]); + + const setMax = useCallback(() => { + console.log('selectedToken', selectedToken); + if (!selectedToken) return; + setValue(props.id, tokenBalance.trim()); + }, [selectedToken]); + + const newRules: RegisterOptions = useMemo(() => { + return { + setValueAs: (value) => handleBaseUnits(value, selectedToken?.decimals), + validate: { + number: (value) => ignoreEmptyVal(value, ValidateField.number), + isTransferrable: () => (selectedToken?.paused ? tokenErrorMsg : true), + tokenSelected: () => !!selectedToken, + maxValue: (value) => + Number(value) <= Number(handleBaseUnits(tokenBalance, 18)) || + 'Cannot exceed current token balance', + nonZero: (value) => + Number(value) > 0 || 'Value must be greater than zero', + }, + ...props.rules, + }; + }, [selectedToken, tokenBalance]); + + return ( + <> + + Max: {tokenBalance} + + } + rules={newRules} + /> + + ignoreEmptyVal(value, ValidateField.ethAddress), + nonZeroAddress: (value) => + value !== ZERO_ADDRESS || 'Cannot send to the Zero Address', + }, + }} + /> + + ); +}; diff --git a/libs/moloch-v3-fields/src/fields/index.ts b/libs/moloch-v3-fields/src/fields/index.ts index 7c167af8..114ecedf 100644 --- a/libs/moloch-v3-fields/src/fields/index.ts +++ b/libs/moloch-v3-fields/src/fields/index.ts @@ -16,3 +16,4 @@ export * from './TagsInput'; export * from './TributeInput'; export * from './WalletConnectLink'; export * from './EpochDatePicker'; +export * from './TransferTokens'; diff --git a/libs/moloch-v3-legos/src/fields.ts b/libs/moloch-v3-legos/src/fields.ts index 16b8c127..3a84b136 100644 --- a/libs/moloch-v3-legos/src/fields.ts +++ b/libs/moloch-v3-legos/src/fields.ts @@ -126,4 +126,9 @@ export const FIELD: Record = { type: 'addressesAndAmounts', label: 'Addresses & Amounts', }, + TRANSFER_TOKENS: { + id: 'transferTokens', + type: 'transferTokens', + label: 'Transfer DAO Tokens', + }, }; diff --git a/libs/moloch-v3-legos/src/form.ts b/libs/moloch-v3-legos/src/form.ts index f52ed00c..9da84a9e 100644 --- a/libs/moloch-v3-legos/src/form.ts +++ b/libs/moloch-v3-legos/src/form.ts @@ -732,6 +732,17 @@ export const COMMON_FORMS: Record = { submitButtonText: 'Update Delegate', tx: TX.MANAGE_DELEGATE, }, + MANAGE_TOKENS: { + id: 'MANAGE_TOKENS', + fields: [FIELD.TRANSFER_TOKENS], + requiredFields: { + transferTokens: true, + paymentTokenAddress: true, + recipientAddress: true, + }, + submitButtonText: 'Transfer Tokens', + tx: TX.TRANSFER_TOKENS, + }, RAGEQUIT: { id: 'RAGEQUIT', title: 'Ragequit', diff --git a/libs/moloch-v3-legos/src/tx.ts b/libs/moloch-v3-legos/src/tx.ts index eff1a954..5445a20b 100644 --- a/libs/moloch-v3-legos/src/tx.ts +++ b/libs/moloch-v3-legos/src/tx.ts @@ -560,6 +560,12 @@ export const TX: Record = { method: 'delegate', args: ['.formValues.delegatingTo'], }, + TRANSFER_TOKENS: { + id: 'TRANSFER_TOKENS', + contract: CONTRACT.ERC_20_FUNDING, + method: 'transfer', + args: ['.formValues.recipientAddress', '.formValues.transferTokens'], + }, RAGEQUIT: { id: 'RAGEQUIT', contract: CONTRACT.CURRENT_DAO, diff --git a/libs/moloch-v3-macro-ui/src/components/MemberProfileCard/ManageTokens.tsx b/libs/moloch-v3-macro-ui/src/components/MemberProfileCard/ManageTokens.tsx new file mode 100644 index 00000000..0cecafce --- /dev/null +++ b/libs/moloch-v3-macro-ui/src/components/MemberProfileCard/ManageTokens.tsx @@ -0,0 +1,45 @@ +import { useDHConnect } from '@daohaus/connect'; +import { FormBuilder } from '@daohaus/form-builder'; +import { ValidNetwork } from '@daohaus/keychain-utils'; +import { MolochFields } from '@daohaus/moloch-v3-fields'; +import { + useConnectedMember, + useDaoData, + useDaoMembers, +} from '@daohaus/moloch-v3-hooks'; +import { COMMON_FORMS } from '@daohaus/moloch-v3-legos'; + +type ManageTokensProps = { + daoChain: ValidNetwork; + daoId: string; +}; + +export const ManageTokens = ({ daoChain, daoId }: ManageTokensProps) => { + const { refetch } = useDaoData(); + const { refetch: refetchMembers } = useDaoMembers(); + const { address } = useDHConnect(); + const { refetch: refetchMember } = useConnectedMember({ + daoChain, + daoId, + memberAddress: address as string, + }); + + const onFormComplete = () => { + refetch?.(); + refetchMembers?.(); + refetchMember?.(); + }; + + return ( + { + onFormComplete(); + }, + }} + targetNetwork={daoChain} + /> + ); +}; diff --git a/libs/moloch-v3-macro-ui/src/components/MemberProfileCard/MemberProfileMenu.tsx b/libs/moloch-v3-macro-ui/src/components/MemberProfileCard/MemberProfileMenu.tsx index dd2bf78c..468affbc 100644 --- a/libs/moloch-v3-macro-ui/src/components/MemberProfileCard/MemberProfileMenu.tsx +++ b/libs/moloch-v3-macro-ui/src/components/MemberProfileCard/MemberProfileMenu.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { RiMore2Fill } from 'react-icons/ri/index.js'; import { useDHConnect } from '@daohaus/connect'; @@ -15,6 +15,7 @@ import { } from '@daohaus/ui'; import { ManageDelegate } from './ManageDelegate'; +import { ManageTokens } from './ManageTokens'; import { ProfileMenuLink, ProfileMenuText } from './MemberProfileCard.styles'; type MemberProfileMenuProps = { @@ -51,6 +52,8 @@ export const MemberProfileMenu = ({ return connectedMember?.memberAddress === memberAddress; }, [connectedMember, memberAddress]); + const [activeDialog, setActiveDialog] = useState<'delegate' | 'transfer'>(); + if (!connectedMember || !allowMemberMenu) return null; return ( @@ -66,10 +69,23 @@ export const MemberProfileMenu = ({ {isMenuForConnectedMember && ( <> - + setActiveDialog('delegate')} + > Delegate + {isMenuForConnectedMember && ( + + setActiveDialog('transfer')} + > + Transfer + + + )} {allowLinks && ( - + setActiveDialog('delegate')} + > Delegate To @@ -109,12 +128,25 @@ export const MemberProfileMenu = ({ )} - - + + {activeDialog === 'delegate' && ( + + )} + {activeDialog === 'transfer' && ( + + )} ); diff --git a/libs/ui/src/components/molecules/Banner/Banner.tsx b/libs/ui/src/components/molecules/Banner/Banner.tsx index b2178a1e..270760a9 100644 --- a/libs/ui/src/components/molecules/Banner/Banner.tsx +++ b/libs/ui/src/components/molecules/Banner/Banner.tsx @@ -24,7 +24,7 @@ export const Banner = ({ > Give Feedback - + Support diff --git a/libs/ui/src/components/molecules/Dialog/Dialog.styles.ts b/libs/ui/src/components/molecules/Dialog/Dialog.styles.ts index 42c5ac56..fff8c6f2 100644 --- a/libs/ui/src/components/molecules/Dialog/Dialog.styles.ts +++ b/libs/ui/src/components/molecules/Dialog/Dialog.styles.ts @@ -5,7 +5,7 @@ import styled, { keyframes } from 'styled-components'; import { widthQuery } from '../../../theme'; export const DialogRoot = DialogPrimitive.Root; -export const DialogPrimitaveTrigger = DialogPrimitive.Trigger; +export const DialogPrimitiveTrigger = DialogPrimitive.Trigger; export const DialogClose = DialogPrimitive.Close; export const DialogPortal = DialogPrimitive.Portal; export const DialogTitle = DialogPrimitive.Title; diff --git a/libs/ui/src/components/molecules/Dialog/Dialog.tsx b/libs/ui/src/components/molecules/Dialog/Dialog.tsx index 3acf610c..945bf354 100644 --- a/libs/ui/src/components/molecules/Dialog/Dialog.tsx +++ b/libs/ui/src/components/molecules/Dialog/Dialog.tsx @@ -5,7 +5,7 @@ import { DialogProps } from './Dialog.types'; import { Button, H5 } from '../../atoms'; import { DialogRoot, - DialogPrimitaveTrigger, + DialogPrimitiveTrigger, DialogTitle, DialogDescription, DialogClose, @@ -26,7 +26,7 @@ type Ref = | undefined; export const Dialog = DialogRoot; -export const DialogTrigger = DialogPrimitaveTrigger; +export const DialogTrigger = DialogPrimitiveTrigger; export const DialogContent = React.forwardRef( ( diff --git a/libs/utils/src/utils/units.ts b/libs/utils/src/utils/units.ts index 1ae8de76..81601e63 100644 --- a/libs/utils/src/utils/units.ts +++ b/libs/utils/src/utils/units.ts @@ -9,6 +9,10 @@ export const toBaseUnits = (amount: string, decimals = 18) => export const toWholeUnits = (amount: string, decimals = 18) => formatUnits(BigInt(amount), decimals).toString(); +export const truncValue = (amount: string, decimals = 6) => + // wrapped again into Number to strip any trailing zeroes + Number(Number(amount).toFixed(decimals)); + type NumericalFormat = | 'currency' | 'currencyShort'