From 8b12ec83293b92861cc7a93c1f4018854ed02de6 Mon Sep 17 00:00:00 2001 From: Santiago Gonzalez Date: Tue, 15 Oct 2024 13:10:34 -0500 Subject: [PATCH] add transfer dao tokens tx modal --- .../src/config/fieldConfig.ts | 2 + .../src/fields/TransferTokens.tsx | 159 ++++++++++++++++++ libs/moloch-v3-fields/src/fields/index.ts | 1 + libs/moloch-v3-legos/src/fields.ts | 5 + libs/moloch-v3-legos/src/form.ts | 11 ++ libs/moloch-v3-legos/src/tx.ts | 6 + .../MemberProfileCard/ManageTokens.tsx | 45 +++++ .../MemberProfileCard/MemberProfileMenu.tsx | 50 +++++- 8 files changed, 270 insertions(+), 9 deletions(-) create mode 100644 libs/moloch-v3-fields/src/fields/TransferTokens.tsx create mode 100644 libs/moloch-v3-macro-ui/src/components/MemberProfileCard/ManageTokens.tsx 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/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' && ( + + )} );