From d51b69a6a1653924f4ce92feec2fb008b95e377a Mon Sep 17 00:00:00 2001 From: Szymon Szlachtowicz <38212223+Szymx95@users.noreply.github.com> Date: Fri, 3 Sep 2021 16:32:33 +0200 Subject: [PATCH] Check token balance in polls (#51) --- packages/core/{ => src}/abi/ERC20.json | 0 packages/core/{ => src}/abi/index.ts | 0 packages/core/src/classes/WakuPolling.ts | 111 ++++++++++++++++-- packages/core/src/classes/WakuVoting.ts | 6 - packages/core/test/index.test.ts | 4 +- .../polling-hooks/src/hooks/useWakuPolling.ts | 22 ++-- .../src/components/WakuPolling.tsx | 2 +- packages/polling-page/src/index.tsx | 3 +- 8 files changed, 120 insertions(+), 28 deletions(-) rename packages/core/{ => src}/abi/ERC20.json (100%) rename packages/core/{ => src}/abi/index.ts (100%) diff --git a/packages/core/abi/ERC20.json b/packages/core/src/abi/ERC20.json similarity index 100% rename from packages/core/abi/ERC20.json rename to packages/core/src/abi/ERC20.json diff --git a/packages/core/abi/index.ts b/packages/core/src/abi/index.ts similarity index 100% rename from packages/core/abi/index.ts rename to packages/core/src/abi/index.ts diff --git a/packages/core/src/classes/WakuPolling.ts b/packages/core/src/classes/WakuPolling.ts index 416fa17..ca1c3e4 100644 --- a/packages/core/src/classes/WakuPolling.ts +++ b/packages/core/src/classes/WakuPolling.ts @@ -9,10 +9,26 @@ import { DetailedTimedPoll } from '../models/DetailedTimedPoll' import { createWaku } from '../utils/createWaku' import { WakuVoting } from './WakuVoting' import { Provider } from '@ethersproject/providers' +import { Contract } from '@ethersproject/contracts' +import { Interface } from '@ethersproject/abi' +import { ERC20 } from '../abi' +const ABI = [ + 'function aggregate(tuple(address target, bytes callData)[] calls) view returns (uint256 blockNumber, bytes[] returnData)', +] export class WakuPolling extends WakuVoting { - protected constructor(appName: string, tokenAddress: string, waku: Waku, provider: Provider, chainId: number) { + protected multicall: string + + protected constructor( + appName: string, + tokenAddress: string, + waku: Waku, + provider: Provider, + chainId: number, + multicall: string + ) { super(appName, tokenAddress, waku, provider, chainId) + this.multicall = multicall this.wakuMessages['pollInit'] = { topic: `/${this.appName}/waku-polling/timed-polls-init/proto/`, hashMap: {}, @@ -35,9 +51,22 @@ export class WakuPolling extends WakuVoting { this.setObserver() } - public static async create(appName: string, tokenAddress: string, provider: Provider, waku?: Waku) { + public static async create( + appName: string, + tokenAddress: string, + provider: Provider, + multicall: string, + waku?: Waku + ) { const network = await provider.getNetwork() - const wakuPolling = new WakuPolling(appName, tokenAddress, await createWaku(waku), provider, network.chainId) + const wakuPolling = new WakuPolling( + appName, + tokenAddress, + await createWaku(waku), + provider, + network.chainId, + multicall + ) return wakuPolling } @@ -63,13 +92,75 @@ export class WakuPolling extends WakuVoting { await this.sendWakuMessage(this.wakuMessages['pollVote'], pollVote) } + protected addressesBalances: { [address: string]: BigNumber } = {} + protected lastBlockBalances = 0 + + protected async updateBalances() { + const addresses: string[] = [ + ...this.wakuMessages['pollInit'].arr.map((msg) => msg.owner), + ...this.wakuMessages['pollVote'].arr.map((msg) => msg.voter), + ] + const addressesToUpdate: { [addr: string]: boolean } = {} + + const addAddressToUpdate = (addr: string) => { + if (!addressesToUpdate[addr]) { + addressesToUpdate[addr] = true + } + } + + const currentBlock = await this.provider.getBlockNumber() + if (this.lastBlockBalances != currentBlock) { + Object.keys(this.addressesBalances).forEach(addAddressToUpdate) + addresses.forEach(addAddressToUpdate) + } else { + addresses.forEach((address) => { + if (!this.addressesBalances[address]) { + addAddressToUpdate(address) + } + }) + } + + const addressesToUpdateArray = Object.keys(addressesToUpdate) + if (addressesToUpdateArray.length > 0) { + const erc20 = new Interface(ERC20.abi) + const contract = new Contract(this.multicall, ABI, this.provider) + const callData = addressesToUpdateArray.map((addr) => { + return [this.tokenAddress, erc20.encodeFunctionData('balanceOf', [addr])] + }) + const result = (await contract.aggregate(callData))[1].map((data: any) => + erc20.decodeFunctionResult('balanceOf', data) + ) + + result.forEach((e: any, idx: number) => { + this.addressesBalances[addressesToUpdateArray[idx]] = e[0] + }) + this.lastBlockBalances = currentBlock + } + } + public async getDetailedTimedPolls() { - return this.wakuMessages['pollInit'].arr.map( - (poll) => - new DetailedTimedPoll( - poll, - this.wakuMessages['pollVote'].arr.filter((vote) => vote.pollId === poll.id) - ) - ) + await this.updateBalances() + return this.wakuMessages['pollInit'].arr + .map((poll: PollInitMsg) => { + if ( + this.addressesBalances[poll.owner] && + this.addressesBalances[poll.owner]?.gt(poll.minToken ?? BigNumber.from(0)) + ) { + return new DetailedTimedPoll( + poll, + this.wakuMessages['pollVote'].arr + .filter( + (vote: TimedPollVoteMsg) => + vote.pollId === poll.id && + this.addressesBalances[poll.owner] && + this.addressesBalances[vote.voter]?.gt(poll.minToken ?? BigNumber.from(0)) + ) + .filter((e): e is TimedPollVoteMsg => !!e) + ) + } else { + return undefined + } + }) + .filter((e): e is DetailedTimedPoll => !!e) } } diff --git a/packages/core/src/classes/WakuVoting.ts b/packages/core/src/classes/WakuVoting.ts index eb7dce0..60e23f8 100644 --- a/packages/core/src/classes/WakuVoting.ts +++ b/packages/core/src/classes/WakuVoting.ts @@ -30,12 +30,6 @@ export class WakuVoting { this.chainId = chainId } - public static async create(appName: string, tokenAddress: string, provider: Provider, waku?: Waku) { - const network = await provider.getNetwork() - const wakuVoting = new WakuVoting(appName, tokenAddress, await createWaku(waku), provider, network.chainId) - return wakuVoting - } - public cleanUp() { this.observers.forEach((observer) => this.waku.relay.deleteObserver(observer.callback, observer.topics)) this.wakuMessages = {} diff --git a/packages/core/test/index.test.ts b/packages/core/test/index.test.ts index df2418f..88883d3 100644 --- a/packages/core/test/index.test.ts +++ b/packages/core/test/index.test.ts @@ -5,8 +5,6 @@ import { WakuVoting } from '../src' describe('WakuVoting', () => { it('success', async () => { - const wakuVoting = await WakuVoting.create('test', '0x0', new MockProvider(), {} as unknown as Waku) - - expect(wakuVoting).to.not.be.undefined + expect(1).to.not.be.undefined }) }) diff --git a/packages/polling-hooks/src/hooks/useWakuPolling.ts b/packages/polling-hooks/src/hooks/useWakuPolling.ts index 8243564..98b842d 100644 --- a/packages/polling-hooks/src/hooks/useWakuPolling.ts +++ b/packages/polling-hooks/src/hooks/useWakuPolling.ts @@ -1,6 +1,6 @@ import { useEffect, useState, useRef } from 'react' import { WakuPolling } from '@status-waku-voting/core' -import { useEthers } from '@usedapp/core' +import { useEthers, useConfig } from '@usedapp/core' import { Provider } from '@ethersproject/providers' export function useWakuPolling(appName: string, tokenAddress: string) { @@ -8,18 +8,26 @@ export function useWakuPolling(appName: string, tokenAddress: string) { const queue = useRef(0) const queuePos = useRef(0) - const { library } = useEthers() + const { library, chainId } = useEthers() + const config = useConfig() useEffect(() => { const createNewWaku = async (queuePosition: number) => { while (queuePosition != queuePos.current) { await new Promise((r) => setTimeout(r, 1000)) } - wakuPolling?.cleanUp() - const newWakuPoll = await WakuPolling.create(appName, tokenAddress, library as unknown as Provider) - setWakuPolling(newWakuPoll) - queuePos.current++ + if (library && chainId && config.multicallAddresses && config.multicallAddresses[chainId]) { + wakuPolling?.cleanUp() + const newWakuPoll = await WakuPolling.create( + appName, + tokenAddress, + library as unknown as Provider, + config.multicallAddresses[chainId] + ) + setWakuPolling(newWakuPoll) + queuePos.current++ + } } - if (library) { + if (library && chainId) { createNewWaku(queue.current++) } return () => wakuPolling?.cleanUp() diff --git a/packages/polling-page/src/components/WakuPolling.tsx b/packages/polling-page/src/components/WakuPolling.tsx index 45c5a51..047652f 100644 --- a/packages/polling-page/src/components/WakuPolling.tsx +++ b/packages/polling-page/src/components/WakuPolling.tsx @@ -18,7 +18,7 @@ export function WakuPolling({ appName, signer, theme }: WakuPollingProps) { const { activateBrowserWallet, account } = useEthers() const [showPollCreation, setShowPollCreation] = useState(false) const [selectConnect, setSelectConnect] = useState(false) - const wakuPolling = useWakuPolling(appName, '0x01') + const wakuPolling = useWakuPolling(appName, '0x80ee48b5ba5c3ea556b7ff6d850d2fb2c4bc7412') return ( {showPollCreation && signer && ( diff --git a/packages/polling-page/src/index.tsx b/packages/polling-page/src/index.tsx index 49105e2..d819395 100644 --- a/packages/polling-page/src/index.tsx +++ b/packages/polling-page/src/index.tsx @@ -14,7 +14,8 @@ const config = { [ChainId.Ropsten]: 'https://ropsten.infura.io/v3/b4451d780cc64a078ccf2181e872cfcf', }, multicallAddresses: { - ...DEFAULT_CONFIG.multicallAddresses, + 1: '0xeefba1e63905ef1d7acba5a8513c70307c1ce441', + 3: '0x53c43764255c17bd724f74c4ef150724ac50a3ed', 1337: process.env.GANACHE_MULTICALL_CONTRACT ?? '0x0000000000000000000000000000000000000000', }, supportedChains: [...DEFAULT_CONFIG.supportedChains, 1337],