Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENG-210] ERC-721 Whitelisting; Voting section not showing #2722

Merged
merged 5 commits into from
Feb 10, 2025
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
283 changes: 160 additions & 123 deletions src/hooks/DAO/proposal/useUserERC721VotingTokens.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { abis } from '@fractal-framework/fractal-contracts';
import { useCallback, useEffect, useState } from 'react';
import { Address, GetContractReturnType, PublicClient, erc721Abi, getContract } from 'viem';
import { Address, erc721Abi, getContract } from 'viem';
import { useAccount } from 'wagmi';
import { useFractal } from '../../../providers/App/AppProvider';
import { useSafeAPI } from '../../../providers/App/hooks/useSafeAPI';
import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore';
import { AzoriusGovernance } from '../../../types';
import { AzoriusGovernance, ERC721TokenData } from '../../../types';
import useNetworkPublicClient from '../../useNetworkPublicClient';
import useVotingStrategiesAddresses from '../../utils/useVotingStrategiesAddresses';

const DEFAULT_RETURN = {
totalVotingTokenAddresses: [],
totalVotingTokenIds: [],
remainingTokenAddresses: [],
remainingTokenIds: [],
};

type ERC721VotingType = 'erc721' | 'erc721WithHats';

/**
* Retrieves list of ERC-721 voting tokens for the supplied `address`(aka `user.address`) param
* @param {string|null} [proposalId] - Proposal ID. When it's provided - calculates `remainingTokenIds` and `remainingTokenAddresses` that user can use for voting on specific proposal.
Expand All @@ -30,7 +39,10 @@ export default function useUserERC721VotingTokens(
const [remainingTokenAddresses, setRemainingTokenAddresses] = useState<Address[]>([]);

const {
governanceContracts: { linearVotingErc721Address },
governanceContracts: {
linearVotingErc721Address,
linearVotingErc721WithHatsWhitelistingAddress,
},
Comment on lines +43 to +46
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main thing was we weren't account for the second type of contract

governance,
} = useFractal();
const user = useAccount();
Expand All @@ -40,92 +52,77 @@ export default function useUserERC721VotingTokens(

const { getVotingStrategies } = useVotingStrategiesAddresses();

const azoriusGovernance = governance as AzoriusGovernance;
const { erc721Tokens } = azoriusGovernance;
const getLinearVotingContract = useCallback(
(_address: Address, _voting: ERC721VotingType) => {
return getContract({
abi:
_voting === 'erc721'
? abis.LinearERC721Voting
: abis.LinearERC721VotingWithHatsProposalCreation,
address: _address,
client: publicClient,
});
},
[publicClient],
);

const globalContextSafeAddress = safe?.address;
// Means getting these for any safe, primary use case - calculating user voting weight for freeze voting
const getUserVotingTokenData = useCallback(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extracted out some code for readability into own callback

async (_safeAddress: Address) => {
const votingStrategies = await getVotingStrategies(_safeAddress);
if (votingStrategies) {
const votingStrategy = votingStrategies.find(
strategy =>
strategy.isLinearVotingErc721 || strategy.isLinearVotingErc721WithHatsProposalCreation,
);
if (votingStrategy) {
const linear721VotingAddress = votingStrategy.strategyAddress;
const votingContract = votingStrategy.isLinearVotingErc721
? getLinearVotingContract(linear721VotingAddress, 'erc721')
: getLinearVotingContract(linear721VotingAddress, 'erc721WithHats');

const getUserERC721VotingTokens = useCallback(
async (_safeAddress: Address | null, _proposalId: number | null) => {
const totalTokenAddresses: Address[] = [];
const totalTokenIds: string[] = [];
const tokenAddresses: Address[] = [];
const tokenIds: string[] = [];
const userERC721Tokens = new Map<Address, Set<string>>();
const addresses = await votingContract.read.getAllTokenAddresses();
const governanceTokens = await Promise.all(
addresses.map(async tokenAddress => {
if (!votingContract) {
throw new Error('Voting contract is undefined');
}

let governanceTokens = erc721Tokens;
let votingContract:
| GetContractReturnType<typeof abis.LinearERC721Voting, PublicClient>
| undefined;

if (!globalContextSafeAddress || !safeAPI) {
return {
totalVotingTokenAddresses: totalTokenAddresses,
totalVotingTokenIds: totalTokenIds,
remainingTokenAddresses: tokenAddresses,
remainingTokenIds: tokenIds,
};
}
const tokenContract = getContract({
abi: erc721Abi,
address: tokenAddress,
client: publicClient,
});

if (_safeAddress && globalContextSafeAddress !== _safeAddress) {
// Means getting these for any safe, primary use case - calculating user voting weight for freeze voting
const votingStrategies = await getVotingStrategies(_safeAddress);
if (votingStrategies) {
const votingStrategyAddress = votingStrategies.find(
strategy =>
strategy.isLinearVotingErc721 ||
strategy.isLinearVotingErc721WithHatsProposalCreation,
)?.strategyAddress;
if (votingStrategyAddress) {
votingContract = getContract({
abi: abis.LinearERC721Voting,
address: votingStrategyAddress,
client: publicClient,
});
const addresses = await votingContract.read.getAllTokenAddresses();
governanceTokens = await Promise.all(
addresses.map(async tokenAddress => {
if (!votingContract) {
throw new Error('Voting contract is undefined');
}

const tokenContract = getContract({
abi: erc721Abi,
address: tokenAddress,
client: publicClient,
});

const [votingWeight, name, symbol] = await Promise.all([
votingContract.read.getTokenWeight([tokenAddress]),
tokenContract.read.name(),
tokenContract.read.symbol(),
]);

return { name, symbol, address: tokenAddress, votingWeight };
}),
);
}
}
}
const [votingWeight, name, symbol] = await Promise.all([
votingContract.read.getTokenWeight([tokenAddress]),
tokenContract.read.name(),
tokenContract.read.symbol(),
]);

if (linearVotingErc721Address && !votingContract) {
votingContract = getContract({
abi: abis.LinearERC721Voting,
address: linearVotingErc721Address,
client: publicClient,
});
return { name, symbol, address: tokenAddress, votingWeight };
}),
);
return {
votingType: votingStrategy.isLinearVotingErc721
? 'erc721'
: ('erc721WithHats' as ERC721VotingType),
governanceTokens,
linear721VotingAddress,
};
}
}
return undefined;
},
[getLinearVotingContract, getVotingStrategies, publicClient],
);

if (!governanceTokens || !votingContract || !user.address) {
return {
totalVotingTokenAddresses: totalTokenAddresses,
totalVotingTokenIds: totalTokenIds,
remainingTokenAddresses: tokenAddresses,
remainingTokenIds: tokenIds,
};
const getUserERC721Tokens = useCallback(
async (userAddress: Address, governanceTokens: ERC721TokenData[] | undefined) => {
const userERC721Tokens = new Map<Address, Set<string>>();
if (!governanceTokens || !userAddress) {
return userERC721Tokens;
}

const userAddress = user.address;
await Promise.all(
// Using `map` instead of `forEach` to simplify usage of `Promise.all`
// and guarantee syncronous contractFn assignment
Expand Down Expand Up @@ -169,56 +166,96 @@ export default function useUserERC721VotingTokens(
}
}),
);
return userERC721Tokens;
},
[publicClient],
);

const tokenIdsSets = [...userERC721Tokens.values()];
const tokenAddressesKeys = [...userERC721Tokens.keys()];
await Promise.all(
// Same here
tokenIdsSets.map(async (tokenIdsSet, setIndex) => {
const tokenAddress = tokenAddressesKeys[setIndex];
// Damn, this is so ugly
// Probably using Moralis API might improve this
// But I also don't want to intruduce another API for this single thing
// Maybe, if we will encounter need to wider support of ERC-1155 - we will bring it and improve this piece of crap as well :D
await Promise.all(
[...tokenIdsSet.values()].map(async tokenId => {
if (!votingContract) {
throw new Error('Voting contract is undefined');
}
const azoriusGovernance = governance as AzoriusGovernance;
const { erc721Tokens } = azoriusGovernance;

totalTokenAddresses.push(tokenAddress);
totalTokenIds.push(tokenId);

if (_proposalId !== null) {
const tokenVoted = await votingContract.read.hasVoted([
_proposalId,
tokenAddress,
BigInt(tokenId),
]);
if (!tokenVoted) {
tokenAddresses.push(tokenAddress);
tokenIds.push(tokenId);
}
}
}),
);
}),
const globalContextSafeAddress = safe?.address;

const getUserERC721VotingTokens = useCallback(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main function is a little easier to follow

async (_safeAddress: Address | null, _proposalId: number | null) => {
let governanceTokens = erc721Tokens;

// @dev global is set as defaults
let votingType: ERC721VotingType | undefined = linearVotingErc721Address
? 'erc721'
: linearVotingErc721WithHatsWhitelistingAddress
? 'erc721WithHats'
: undefined;

let linearVotingAddress =
linearVotingErc721Address ?? linearVotingErc721WithHatsWhitelistingAddress;

if (!globalContextSafeAddress || !safeAPI || !user.address) {
return DEFAULT_RETURN;
}

if (_safeAddress && globalContextSafeAddress !== _safeAddress) {
const userVotingTokenData = await getUserVotingTokenData(_safeAddress);
if (userVotingTokenData) {
governanceTokens = userVotingTokenData.governanceTokens;
votingType = userVotingTokenData.votingType;
linearVotingAddress = userVotingTokenData.linear721VotingAddress;
}
}
Comment on lines +198 to +205
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IF being used outside the context of viewing safe these variables are updated.


if (!governanceTokens || !votingType || !linearVotingAddress) {
return DEFAULT_RETURN;
}

const userAddress = '0xAf3ee09F37ead9F28a05AeF0d09841BC9A6Fe8e9';
const userERC721Tokens = await getUserERC721Tokens(userAddress, governanceTokens);

const votingContract =
votingType === 'erc721'
? getLinearVotingContract(linearVotingAddress, 'erc721')
: getLinearVotingContract(linearVotingAddress, 'erc721WithHats');

const tokenDataPromises = Array.from(userERC721Tokens.entries()).flatMap(
([tokenAddress, tokenIdsSet]) => {
return Array.from(tokenIdsSet).map(async tokenId => {
let hasVoted = false;
if (_proposalId !== null) {
hasVoted = await votingContract.read.hasVoted([
_proposalId,
tokenAddress,
BigInt(tokenId),
]);
}
return { tokenAddress, tokenId, hasVoted };
});
},
);
Comment on lines +219 to 233
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This replaces the nested promises to a flatmap of promises to optimise this section


const tokenData = await Promise.all(tokenDataPromises);

return {
totalVotingTokenAddresses: totalTokenAddresses,
totalVotingTokenIds: totalTokenIds,
remainingTokenAddresses: tokenAddresses,
remainingTokenIds: tokenIds,
totalVotingTokenAddresses: tokenData.map(data => data.tokenAddress),
totalVotingTokenIds: tokenData.map(data => data.tokenId),
remainingTokenAddresses:
_proposalId !== null
? tokenData.filter(data => !data.hasVoted).map(data => data.tokenAddress)
: [],
remainingTokenIds:
_proposalId !== null
? tokenData.filter(data => !data.hasVoted).map(data => data.tokenId)
: [],
};
},
[
erc721Tokens,
getVotingStrategies,
publicClient,
safeAPI,
globalContextSafeAddress,
linearVotingErc721Address,
linearVotingErc721WithHatsWhitelistingAddress,
globalContextSafeAddress,
safeAPI,
user.address,
getUserERC721Tokens,
getLinearVotingContract,
getUserVotingTokenData,
],
);

Expand All @@ -234,10 +271,10 @@ export default function useUserERC721VotingTokens(
}, [getUserERC721VotingTokens, proposalId, safeAddress]);

useEffect(() => {
if (loadOnMount && linearVotingErc721Address) {
if (loadOnMount) {
loadUserERC721VotingTokens();
}
}, [loadUserERC721VotingTokens, loadOnMount, linearVotingErc721Address]);
}, [loadUserERC721VotingTokens, loadOnMount]);
Comment on lines -237 to +278
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't believe we need to hold this one back. if there is no contract it will just set defaults


return {
totalVotingTokenIds,
Expand Down