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'