Skip to content

Commit

Permalink
Merge pull request #2692 from decentdao/eng-25-gassless-voting-ui
Browse files Browse the repository at this point in the history
Gasless voting UI
  • Loading branch information
DarksightKellar authored Feb 11, 2025
2 parents 3551cf8 + efd973f commit 9a74f37
Show file tree
Hide file tree
Showing 18 changed files with 297 additions and 17 deletions.
1 change: 1 addition & 0 deletions src/components/DaoCreator/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const initialState: CreatorFormState = {
daoName: '',
governance: GovernanceType.AZORIUS_ERC20,
snapshotENS: '',
gaslessVoting: false,
},
erc20Token: {
tokenCreationType: TokenCreationType.IMPORTED,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next';
import { isFeatureEnabled } from '../../../helpers/featureFlags';
import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore';
import { FractalModuleType, ICreationStepProps, VotingStrategyType } from '../../../types';
import { GaslessVotingToggleDAOCreate } from '../../ui/GaslessVotingToggle';
import { BigIntInput } from '../../ui/forms/BigIntInput';
import { CustomNonceInput } from '../../ui/forms/CustomNonceInput';
import { LabelComponent } from '../../ui/forms/InputComponent';
Expand Down Expand Up @@ -252,6 +253,10 @@ export function AzoriusGovernance(props: ICreationStepProps) {
/>
</Box>
)}
<GaslessVotingToggleDAOCreate
isEnabled={values.essentials.gaslessVoting}
onToggle={() => setFieldValue('essentials.gaslessVoting', !values.essentials.gaslessVoting)}
/>
<StepButtons
{...props}
isEdit={mode === DAOCreateMode.EDIT}
Expand Down
6 changes: 6 additions & 0 deletions src/components/DaoCreator/formComponents/Multisig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { MinusCircle, Plus } from '@phosphor-icons/react';
import { Field, FieldAttributes } from 'formik';
import { useTranslation } from 'react-i18next';
import { ICreationStepProps } from '../../../types';
import { GaslessVotingToggleDAOCreate } from '../../ui/GaslessVotingToggle';
import { AddressInput } from '../../ui/forms/EthAddressInput';
import { LabelComponent } from '../../ui/forms/InputComponent';
import LabelWrapper from '../../ui/forms/LabelWrapper';
Expand Down Expand Up @@ -151,6 +152,11 @@ export function Multisig(props: ICreationStepProps) {
</LabelComponent>
</Flex>
</StepWrapper>

<GaslessVotingToggleDAOCreate
isEnabled={values.essentials.gaslessVoting}
onToggle={() => setFieldValue('essentials.gaslessVoting', !values.essentials.gaslessVoting)}
/>
<StepButtons
{...props}
isEdit={mode === DAOCreateMode.EDIT}
Expand Down
203 changes: 203 additions & 0 deletions src/components/ui/GaslessVotingToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { Box, Text, HStack, Switch, Flex, Icon, Button } from '@chakra-ui/react';
import { GasPump, WarningCircle } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next';
import { useBalance } from 'wagmi';
import { DETAILS_BOX_SHADOW } from '../../constants/common';
import { isFeatureEnabled } from '../../helpers/featureFlags';
import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore';
import { useDaoInfoStore } from '../../store/daoInfo/useDaoInfoStore';
import { BigIntValuePair } from '../../types';
import { formatCoin } from '../../utils';
import EtherscanLink from './links/EtherscanLink';
import Divider from './utils/Divider';

interface GaslessVotingToggleProps {
isEnabled: boolean;
onToggle: () => void;
}

function GaslessVotingToggleContent({
isEnabled,
onToggle,
isSettings,
}: GaslessVotingToggleProps & { isSettings?: boolean }) {
const { t } = useTranslation('daoCreate');

return (
<Box
display="flex"
flexDirection="column"
gap="1.5rem"
w="100%"
>
<HStack
justify="space-between"
width="100%"
alignItems="flex-start"
>
<Flex
flexDirection="column"
gap="0.25rem"
>
<Text textStyle={isSettings ? 'heading-small' : 'helper-text'}>
{isSettings
? t('gaslessVotingLabelSettings', { ns: 'daoEdit' })
: t('gaslessVotingLabel')}
</Text>
<Text
textStyle={isSettings ? 'label-large' : 'helper-text'}
color="neutral-7"
w="17.25rem"
>
{isSettings
? t('gaslessVotingDescriptionSettings', { ns: 'daoEdit' })
: t('gaslessVotingDescription')}
</Text>
</Flex>
<Switch
size="md"
isChecked={isEnabled}
onChange={() => onToggle()}
variant="secondary"
/>
</HStack>
</Box>
);
}

export function GaslessVotingToggleDAOCreate(props: GaslessVotingToggleProps) {
const { t } = useTranslation('daoCreate');
const { chain, gaslessVotingSupported } = useNetworkConfigStore();

if (!isFeatureEnabled('flag_gasless_voting')) return null;
if (!gaslessVotingSupported) return null;

return (
<Box
borderRadius="0.75rem"
bg="neutral-2"
p="1.5rem"
display="flex"
flexDirection="column"
alignItems="flex-start"
gap="1.5rem"
boxShadow={DETAILS_BOX_SHADOW}
mt={2}
>
<GaslessVotingToggleContent {...props} />

<Box
p="1rem"
bg="neutral-3"
borderRadius="0.75rem"
>
<Flex alignItems="center">
<Icon
as={WarningCircle}
color="lilac-0"
width="1.5rem"
height="1.5rem"
/>
<Text
color="lilac-0"
marginLeft="1rem"
>
{t('gaslessVotingGettingStarted', {
symbol: chain.nativeCurrency.symbol,
})}
</Text>
</Flex>
</Box>
</Box>
);
}

export function GaslessVotingToggleDAOSettings(
props: GaslessVotingToggleProps & {
onGasTankTopupAmountChange: (amount: BigIntValuePair) => void;
},
) {
const { t } = useTranslation('daoEdit');
const { chain, gaslessVotingSupported } = useNetworkConfigStore();

// @todo: Retrieve and use the paymaster address here for `gasTankAddress`. Replace safe.address with the paymaster address. Remove use of `useDaoInfoStore`.
const { safe } = useDaoInfoStore();
const gasTankAddress = safe?.address;

const { data: balance } = useBalance({ address: gasTankAddress, chainId: chain.id });

if (!isFeatureEnabled('flag_gasless_voting')) return null;
if (!gaslessVotingSupported) return null;

const formattedNativeTokenBalance =
balance && formatCoin(balance.value, true, balance.decimals, balance.symbol);

return (
<Box
gap="1.5rem"
display="flex"
flexDirection="column"
>
<Divider
mt="1rem"
w={{ base: 'calc(100% + 1.5rem)', md: 'calc(100% + 3rem)' }}
mx={{ base: '-0.75rem', md: '-1.5rem' }}
/>

<GaslessVotingToggleContent
{...props}
isSettings
/>

{gasTankAddress && (
<Box
borderRadius="0.75rem"
border="1px solid"
borderColor="neutral-3"
p="1rem 0.5rem"
w="100%"
>
<EtherscanLink
type="address"
value={gasTankAddress}
isTextLink
>
<Text as="span">{gasTankAddress}</Text>
</EtherscanLink>
</Box>
)}

<Flex
mt="-0.55rem"
justifyContent="space-between"
>
<Text textStyle="body-small">
{t('titleBalance', { ns: 'modals' })}:{' '}
<Text
as="span"
color="neutral-7"
>
{formattedNativeTokenBalance}
</Text>
</Text>
<Button
variant="secondary"
leftIcon={<Icon as={GasPump} />}
onClick={() => {
console.log(
'addGas. Add this action to the proposal, to be submitted via propose changes button.',
);

// @todo: Add UI to set the amount, then call onGasTankTopupAmountChange.
props.onGasTankTopupAmountChange({
value: '1',
bigintValue: 1n,
});
}}
>
{t('addGas')}
</Button>
</Flex>
</Box>
);
}
2 changes: 1 addition & 1 deletion src/helpers/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const FEATURE_FLAGS = ['flag_dev', 'flag_yelling'] as const;
export const FEATURE_FLAGS = ['flag_dev', 'flag_gasless_voting', 'flag_yelling'] as const;

export type FeatureFlagKeys = typeof FEATURE_FLAGS;
export type FeatureFlagKey = (typeof FEATURE_FLAGS)[number];
Expand Down
5 changes: 4 additions & 1 deletion src/i18n/locales/en/daoCreate.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,8 @@
"networks": "Networks",
"networkDescription": "What network would you like to deploy this DAO on?",
"attachFractalModuleDescription": "This setting controls whether Parent DAO will be able to execute arbitrary transactions on Child DAO bypassing voting process on Child DAO.",
"fractalModuleAttachedDescription": "This setting can not be modified as Fractal Module already attached to the DAO."
"fractalModuleAttachedDescription": "This setting can not be modified as Fractal Module already attached to the DAO.",
"gaslessVotingLabel": "Gasless Voting",
"gaslessVotingDescription": "Sponsor gas for votes and proposals.",
"gaslessVotingGettingStarted": "To get you started, we're covering your first 0.1 {{symbol}} of gas fees. You can top up your balance in your DAO settings, or by sending {{symbol}} to this address directly in your wallet."
}
5 changes: 4 additions & 1 deletion src/i18n/locales/en/daoEdit.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"cannotModifyGovernance": "You do not have permissions to modify this Safe's governance."
"cannotModifyGovernance": "You do not have permissions to modify this Safe's governance.",
"gaslessVotingLabelSettings": "Sponsor Gas",
"gaslessVotingDescriptionSettings": "Fund transaction fees for DAO voters from a shared DAO gas tank.",
"addGas": "Add Gas"
}
2 changes: 2 additions & 0 deletions src/i18n/locales/en/proposalMetadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"removeProposalTemplateTitle": "Remove Proposal Template",
"removeProposalTemplateDescription": "Execution of this proposal will remove the proposal template attached to this Safe.",
"updatesSafeName": "Update Safe Name",
"enableGaslessVoting": "Enable Gasless Voting",
"topupGasTank": "Top up Gas Tank",
"updateSnapshotSpace": "Update Snapshot Space",
"lidoWithdrawalTitle": "Lido Withdrawal",
"lidoWithdrawalDescription": "This proposal will burn your Lido Withdrawal NFT and return the ETH to your Safe.",
Expand Down
51 changes: 42 additions & 9 deletions src/pages/dao/settings/general/SafeGeneralSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { encodeFunctionData, zeroAddress } from 'viem';
import { SettingsContentBox } from '../../../../components/SafeSettings/SettingsContentBox';
import { GaslessVotingToggleDAOSettings } from '../../../../components/ui/GaslessVotingToggle';
import { InputComponent } from '../../../../components/ui/forms/InputComponent';
import { BarLoader } from '../../../../components/ui/loaders/BarLoader';
import NestedPageHeader from '../../../../components/ui/page/Header/NestedPageHeader';
Expand All @@ -15,14 +16,18 @@ import { useCanUserCreateProposal } from '../../../../hooks/utils/useCanUserSubm
import { createAccountSubstring } from '../../../../hooks/utils/useGetAccountName';
import { useNetworkConfigStore } from '../../../../providers/NetworkConfig/useNetworkConfigStore';
import { useDaoInfoStore } from '../../../../store/daoInfo/useDaoInfoStore';
import { ProposalExecuteData } from '../../../../types';
import { BigIntValuePair, ProposalExecuteData } from '../../../../types';
import { validateENSName } from '../../../../utils/url';

export function SafeGeneralSettingsPage() {
const { t } = useTranslation(['settings', 'settingsMetadata']);
const [name, setName] = useState('');
const [snapshotENS, setSnapshotENS] = useState('');
const [snapshotENSValid, setSnapshotENSValid] = useState<boolean>();

const [isGaslessVotingEnabled, setIsGaslessVotingEnabled] = useState<boolean>(false);
const [gasTankTopupAmount, setGasTankTopupAmount] = useState<BigIntValuePair>();

const navigate = useNavigate();

const { submitProposal } = useSubmitProposal();
Expand All @@ -35,6 +40,8 @@ export function SafeGeneralSettingsPage() {

const safeAddress = safe?.address;

const currentIsGaslessVotingEnabled = subgraphInfo?.gaslessVotingEnabled ?? false;

useEffect(() => {
if (
subgraphInfo?.daoName &&
Expand Down Expand Up @@ -70,28 +77,43 @@ export function SafeGeneralSettingsPage() {

const nameChanged = name !== subgraphInfo?.daoName;
const snapshotChanged = snapshotENSValid && snapshotENS !== subgraphInfo?.daoSnapshotENS;
const gaslessVotingChanged = isGaslessVotingEnabled !== currentIsGaslessVotingEnabled;
const gasTankTopupAmountSet =
gasTankTopupAmount?.bigintValue !== undefined && gasTankTopupAmount.bigintValue > 0n;

const handleEditGeneralGovernance = () => {
const changeTitles = [];
if (nameChanged) {
changeTitles.push(t('updatesSafeName', { ns: 'proposalMetadata' }));
}
if (snapshotChanged) {
changeTitles.push(t('updateSnapshotSpace', { ns: 'proposalMetadata' }));
}
const title = changeTitles.join(` ${t('and', { ns: 'common' })} `);

const keyArgs = [];
const valueArgs = [];

if (nameChanged) {
changeTitles.push(t('updatesSafeName', { ns: 'proposalMetadata' }));
keyArgs.push('daoName');
valueArgs.push(name);
}

if (snapshotChanged) {
changeTitles.push(t('updateSnapshotSpace', { ns: 'proposalMetadata' }));
keyArgs.push('snapshotENS');
valueArgs.push(snapshotENS);
}

if (gaslessVotingChanged) {
changeTitles.push(t('enableGaslessVoting', { ns: 'proposalMetadata' }));

// @todo Is KV pairs the place we're storing this flag?
keyArgs.push('gaslessVotingEnabled');
valueArgs.push(`${isGaslessVotingEnabled}`);
}

if (gasTankTopupAmountSet) {
changeTitles.push(t('topupGasTank', { ns: 'proposalMetadata' }));

// @todo add tx to send `gasTankTopupAmount` to gas tank address
}

const title = changeTitles.join(`; `);

const proposalData: ProposalExecuteData = {
metaData: {
title,
Expand Down Expand Up @@ -187,6 +209,17 @@ export function SafeGeneralSettingsPage() {
}}
/>
</Flex>

<GaslessVotingToggleDAOSettings
isEnabled={isGaslessVotingEnabled}
onToggle={() => {
console.log(
'onToggle. Add this action to the proposal, to be submitted via propose changes button.',
);
setIsGaslessVotingEnabled(!isGaslessVotingEnabled);
}}
onGasTankTopupAmountChange={setGasTankTopupAmount}
/>
{canUserCreateProposal && (
<>
<Divider
Expand Down
1 change: 1 addition & 0 deletions src/providers/NetworkConfig/networks/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export const baseConfig: NetworkConfig = {
GovernanceType.AZORIUS_ERC20,
GovernanceType.AZORIUS_ERC721,
],
gaslessVotingSupported: false,
};

export default baseConfig;
Loading

0 comments on commit 9a74f37

Please sign in to comment.