-
Notifications
You must be signed in to change notification settings - Fork 7
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
Changes from 4 commits
5dca763
6768111
a9425a8
ddd3440
2c858c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. | ||
|
@@ -30,7 +39,10 @@ export default function useUserERC721VotingTokens( | |
const [remainingTokenAddresses, setRemainingTokenAddresses] = useState<Address[]>([]); | ||
|
||
const { | ||
governanceContracts: { linearVotingErc721Address }, | ||
governanceContracts: { | ||
linearVotingErc721Address, | ||
linearVotingErc721WithHatsWhitelistingAddress, | ||
}, | ||
governance, | ||
} = useFractal(); | ||
const user = useAccount(); | ||
|
@@ -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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
], | ||
); | ||
|
||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
There was a problem hiding this comment.
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