Skip to content

Commit

Permalink
voting: fetch active account votes in script (aragon#806)
Browse files Browse the repository at this point in the history
* voting: fetch active account votes in script

* Move connectedAccount out of the store and cache

* Add comment about using strings over symbols

* Move functions to helpers

* Update comments

Co-Authored-By: 2color <[email protected]>

* Add newline
  • Loading branch information
2color authored Apr 17, 2019
1 parent 8c5649c commit 9272c33
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 49 deletions.
81 changes: 76 additions & 5 deletions apps/voting/app/src/script.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import Aragon from '@aragon/api'
import { of } from 'rxjs'
import { map, publishReplay } from 'rxjs/operators'
import { addressesEqual } from './web3-utils'
import voteSettings, { hasLoadedVoteSettings } from './vote-settings'
import { voteTypeFromContractEnum } from './vote-utils'
import { EMPTY_CALLSCRIPT } from './evmscript-utils'
import tokenDecimalsAbi from './abi/token-decimals.json'
import tokenSymbolAbi from './abi/token-symbol.json'

const INITIALIZATION_TRIGGER = Symbol('INITIALIZATION_TRIGGER')
const ACCOUNTS_TRIGGER = Symbol('ACCOUNTS_TRIGGER')

const tokenAbi = [].concat(tokenDecimalsAbi, tokenSymbolAbi)

const app = new Aragon()

let connectedAccount

/*
* Calls `callback` exponentially, everytime `retry()` is called.
*
Expand Down Expand Up @@ -92,6 +98,22 @@ async function initialize(tokenAddr) {
// Hook up the script as an aragon.js store
async function createStore(token, tokenSettings) {
const { decimals: tokenDecimals, symbol: tokenSymbol } = tokenSettings

// Hot observable which emits an web3.js event-like object with an account string of the current active account.
const accounts$ = app.accounts().pipe(
map(accounts => {
return {
event: ACCOUNTS_TRIGGER,
returnValues: {
account: accounts[0],
},
}
}),
publishReplay(1)
)

accounts$.connect()

return app.store(
async (state, { event, returnValues }) => {
let nextState = {
Expand All @@ -108,6 +130,9 @@ async function createStore(token, tokenSettings) {
}
} else {
switch (event) {
case ACCOUNTS_TRIGGER:
nextState = await updateConnectedAccount(nextState, returnValues)
break
case 'CastVote':
nextState = await castVote(nextState, returnValues)
break
Expand All @@ -121,12 +146,12 @@ async function createStore(token, tokenSettings) {
break
}
}

return nextState
},
[
// Always initialize the store with our own home-made event
of({ event: INITIALIZATION_TRIGGER }),
accounts$,
]
)
}
Expand All @@ -137,17 +162,39 @@ async function createStore(token, tokenSettings) {
* *
***********************/

async function castVote(state, { voteId }) {
// Let's just reload the entire vote again,
// cause do we really want more than one source of truth with a blockchain?
async function updateConnectedAccount(state, { account }) {
connectedAccount = account
return {
...state,
// fetch all the votes casted by the connected account
connectedAccountVotes: await getAccountVotes({
connectedAccount: account,
votes: state.votes,
}),
}
}

async function castVote(state, { voteId, voter }) {
const { connectedAccountVotes } = state
// If the connected account was the one who made the vote, update their voter status
if (addressesEqual(connectedAccount, voter)) {
// fetch vote state for the connected account for this voteId
const { voteType } = await getVoterState({
connectedAccount,
voteId,
})
connectedAccountVotes[voteId] = voteType
}

const transform = async vote => ({
...vote,
data: {
...vote.data,
...(await loadVoteData(voteId)),
},
})
return updateState(state, voteId, transform)

return updateState({ ...state, connectedAccountVotes }, voteId, transform)
}

async function executeVote(state, { voteId }) {
Expand Down Expand Up @@ -175,6 +222,30 @@ async function startVote(state, { creator, metadata, voteId }) {
* *
***********************/

async function getAccountVotes({ connectedAccount, votes }) {
const connectedAccountVotes = await Promise.all(
votes.map(({ voteId }) => getVoterState({ connectedAccount, voteId }))
)
.then(voteStates =>
voteStates.reduce((states, { voteId, voteType }) => {
states[voteId] = voteType
return states
}, {})
)
.catch(console.error)

return connectedAccountVotes
}

async function getVoterState({ connectedAccount, voteId }) {
return app
.call('getVoterState', voteId, connectedAccount)
.toPromise()
.then(voteTypeFromContractEnum)
.then(voteType => ({ voteId, voteType }))
.catch(console.error)
}

async function loadVoteDescription(vote) {
if (!vote.script || vote.script === EMPTY_CALLSCRIPT) {
return vote
Expand Down
43 changes: 2 additions & 41 deletions apps/voting/app/src/vote-hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,15 @@ import {
getCanVote,
getUserBalance,
isVoteOpen,
voteTypeFromContractEnum,
} from './vote-utils'
import { useNow, usePromise } from './utils-hooks'
import { VOTE_ABSENT } from './vote-types'
import TOKEN_ABI from './abi/token-balanceOfAt.json'

// Get the votes array ready to be used in the app.
export function useVotes() {
const { votes } = useAppState()
const { votes, connectedAccountVotes } = useAppState()
const now = useNow()
const connectedAccountVotes = useConnectedAccountVotes()

return useMemo(() => {
if (!votes) {
Expand All @@ -30,48 +28,11 @@ export function useVotes() {
description: vote.data.description || '',
open: isVoteOpen(vote, now),
},
connectedAccountVote:
connectedAccountVotes.get(vote.voteId) || VOTE_ABSENT,
connectedAccountVote: connectedAccountVotes[vote.voteId] || VOTE_ABSENT,
}))
}, [votes, connectedAccountVotes, now])
}

// Get the voting state of the connected account for every vote.
export function useConnectedAccountVotes() {
const { api, appState, connectedAccount } = useAragonApi()
const [connectedAccountVotes, setConnectedAccountVotes] = useState(new Map())

const { votes } = appState

useEffect(() => {
if (!connectedAccount || !votes) {
setConnectedAccountVotes(new Map())
return
}

let cancelled = false
Promise.all(
votes.map(({ voteId }) =>
api
.call('getVoterState', voteId, connectedAccount)
.toPromise()
.then(voteTypeFromContractEnum)
.then(voteType => [voteId, voteType])
)
).then(voteStates => {
if (!cancelled) {
setConnectedAccountVotes(new Map(voteStates))
}
})

return () => {
cancelled = true
}
}, [api, votes, connectedAccount])

return connectedAccountVotes
}

// Load and returns the token contract, or null if not loaded yet.
export function useTokenContract() {
const { api, appState } = useAragonApi()
Expand Down
8 changes: 5 additions & 3 deletions apps/voting/app/src/vote-types.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export const VOTE_ABSENT = Symbol('VOTE_ABSENT')
export const VOTE_YEA = Symbol('VOTE_YEA')
export const VOTE_NAY = Symbol('VOTE_NAY')
// Because these are passed between the background script and the app, we don't use symbols
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#Supported_types
export const VOTE_ABSENT = 'VOTE_ABSENT'
export const VOTE_YEA = 'VOTE_YEA'
export const VOTE_NAY = 'VOTE_NAY'

export const VOTE_STATUS_ONGOING = Symbol('VOTE_STATUS_ONGOING')
export const VOTE_STATUS_REJECTED = Symbol('VOTE_STATUS_REJECTED')
Expand Down
12 changes: 12 additions & 0 deletions apps/voting/app/src/web3-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,15 @@ export function transformAddresses(str, callback) {
callback(part, ETH_ADDRESS_TEST_REGEX.test(part), index)
)
}

/**
* Check address equality without checksums
* @param {string} first First address
* @param {string} second Second address
* @returns {boolean} Address equality
*/
export function addressesEqual(first, second) {
first = first && first.toLowerCase()
second = second && second.toLowerCase()
return first === second
}

0 comments on commit 9272c33

Please sign in to comment.