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

Voting Connector: Support vote rewards #338

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions packages/connect-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export {
ConnectionContext,
IpfsResolver,
AppData,
AppMethod,
ForwardingPathData,
PermissionData,
RepoData,
Expand Down
1 change: 1 addition & 0 deletions packages/connect-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export type Metadata = (AragonArtifact | AragonManifest)[]
export interface AppMethod {
roles: string[]
sig: string
params?: any[]
/**
* This field might not be able if the contract does not use
* conventional solidity syntax and Aragon naming standards
Expand Down
17 changes: 15 additions & 2 deletions packages/connect-core/src/utils/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export const apmAppId = (appName: string): string =>
ethersUtils.namehash(`${appName}.aragonpm.eth`)

function signatureFromAbi(signature: string, abi: Abi): string {
if (signature === 'fallback') {
return 'fallback()'
}

const matches = signature.match(/(.*)\((.*)\)/m)

if (!matches) {
Expand Down Expand Up @@ -44,7 +48,7 @@ function findAppMethod(
if (Array.isArray(functions)) {
method = functions
.map((f) => {
return { ...f, sig: signatureFromAbi(f.sig, app.abi) }
return { ...f, sig: signatureFromAbi(f.sig, app.abi), params: [] }
})
.find(methodTestFn)
}
Expand Down Expand Up @@ -79,12 +83,21 @@ export function findAppMethodFromData(
{ allowDeprecated = true } = {}
): AppMethod | undefined {
const methodId = data.substring(0, 10)
return findAppMethod(
const appMethod = findAppMethod(
app,
(method: AppMethod) =>
ethersUtils.id(method.sig).substring(0, 10) === methodId,
{ allowDeprecated }
)

// Decode method's parameters
if (appMethod?.abi) {
const inputTypes = appMethod.abi.inputs.map(({ type }) => type)

appMethod.params = [...ethersUtils.defaultAbiCoder.decode(inputTypes, `0x${data.slice(10)}`)]
}

return appMethod
}

/**
Expand Down
96 changes: 93 additions & 3 deletions packages/connect-voting/src/__test__/votes.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { BigNumber } from 'ethers'
import { App, connect } from '@aragon/connect'
import { VotingConnectorTheGraph, Vote, Cast } from '../../src'
import { Action, VoteStatus } from '../types'

const VOTING_SUBGRAPH_URL =
'https://api.thegraph.com/subgraphs/name/aragon/aragon-voting-rinkeby-staging'
'https://api.thegraph.com/subgraphs/name/aragon/aragon-voting-rinkeby'
Copy link

Choose a reason for hiding this comment

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

the tests did not ran successfully, even after swapping the VOTING_SUBGRAPH_URL as stated in the PR description.

  • can you provide more context or instruction how you run test locally?
  • Is there a way to run the test successfully without swapping the URL?


const VOTING_APP_ADDRESS = '0x37187b0f2089b028482809308e776f92eeb7334e'
// For testing vote action functionality
const ACTIONS_ORG_ADDRESS = "0x63210F64Ef6F4EBB9727F6c5665CB8bbeDf20480"
const ACTIONS_VOTING_APP_ADDRESS = '0x9943c2f55d91308b8ddbc58b6e70d1774ace125e'

describe('when connecting to a voting app', () => {
let connector: VotingConnectorTheGraph
let votes: Vote[]

beforeAll(() => {
connector = new VotingConnectorTheGraph({
Expand All @@ -18,8 +26,6 @@ describe('when connecting to a voting app', () => {
})

describe('when querying for all the votes of a voting app', () => {
let votes: Vote[]

beforeAll(async () => {
votes = await connector.votesForApp(VOTING_APP_ADDRESS, 1000, 0)
})
Expand Down Expand Up @@ -87,6 +93,18 @@ describe('when connecting to a voting app', () => {
expect(vote.startDate).toEqual('1599675534')
})

test('should have a valid endDate', () => {
expect(vote.endDate).toEqual('1600280334')
})

test('should have not be accepted', () => {
expect(vote.isAccepted).toBe(false)
})

test('should have a valid status', () => {
expect(vote.status).toEqual(VoteStatus.Rejected)
})

describe('when querying for the casts of a vote', () => {
let casts: Cast[]

Expand All @@ -100,4 +118,76 @@ describe('when connecting to a voting app', () => {
})
})
})

describe("when looking at the votes actions of a voting app", () => {
let installedApps: App[]
let signallingVoteActions: Action[]
let codeExecutionVoteActions: Action[]
let voteActions: Action[]

beforeAll(async () => {
const org = await connect(ACTIONS_ORG_ADDRESS, "thegraph", { network: 4 })
installedApps = await org.apps()
connector = new VotingConnectorTheGraph({
subgraphUrl: VOTING_SUBGRAPH_URL,
})
votes = await connector.votesForApp(ACTIONS_VOTING_APP_ADDRESS, 1000, 0)

codeExecutionVoteActions = votes[0].getActions(installedApps)
signallingVoteActions = votes[1].getActions(installedApps)
voteActions = votes[4].getActions(installedApps)
})

test("should return a list of actions", () => {
expect(voteActions.length).toBeGreaterThan(0)
})

test("shouldn't return anything when getting actions from a signaling vote", () => {
expect(signallingVoteActions).toEqual([])
})

test("shouldn't return rewards when getting actions from a vote that only executes code", () => {
const action = codeExecutionVoteActions[0]
expect(action.rewards).toEqual([])
})

describe("when looking at a specific vote's action and reward", () => {
let rewardedAction: Action

beforeAll(() => {
rewardedAction = voteActions[0]
})

test('should have a valid to (target contract address)', () => {
expect(rewardedAction.to).toEqual("0xcaa6526abb106ff5c5f937e3ea9499243df86b7a")
})

test("should have a valid fnData", () => {
const { abi, notice, params, roles, sig } = rewardedAction.fnData!

expect(Object.keys(abi!).length).toBeGreaterThan(0)
expect(notice).toEqual("Create a new payment of `@tokenAmount(_token, _amount)` to `_receiver` for '`_reference`'")
expect(params!).toEqual(['0x0000000000000000000000000000000000000000',
'0x9943c2f55D91308B8DDbc58B6e70d1774AcE125e', BigNumber.from('3000000000000000000'), "\"reference\""])
expect(roles).toEqual([ 'CREATE_PAYMENTS_ROLE' ])
expect(sig).toEqual("newImmediatePayment(address,address,uint256,string)")
})

test("should have a list of rewards", () => {
expect(rewardedAction.rewards.length).toBeGreaterThan(0)
})

test("should have a valid reward", () => {
const reward = rewardedAction.rewards[0]
const { amount, token, receiver } = reward
const ETH = '0x0000000000000000000000000000000000000000'

expect(amount).toEqual('3000000000000000000')
expect(token).toEqual(ETH)
expect(receiver).toEqual('0x9943c2f55D91308B8DDbc58B6e70d1774AcE125e')
})
})

})

})
46 changes: 46 additions & 0 deletions packages/connect-voting/src/helpers/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { utils } from 'ethers'
import { AppMethod } from "@aragon/connect"
import { Reward } from '../types'

export const getRewards = (appId: string, fnData: AppMethod): Reward[] => {
PJColombo marked this conversation as resolved.
Show resolved Hide resolved
const {params, sig } = fnData

if (!params || !params.length) {
return []
}

const sigHash = utils.id(sig).substring(0, 10)

switch (appId) {
// finance.aragonpm.eth
case '0xbf8491150dafc5dcaee5b861414dca922de09ccffa344964ae167212e8c673ae': {
switch (sigHash) {
// newImmediatePayment(address,address,uint256,string)
case '0xf6364846':
return [{
receiver: params[1],
token: params[0],
amount: params[2].toString()
}]
}
break
}
// agent.aragonpm.eth
case '0x9ac98dc5f995bf0211ed589ef022719d1487e5cb2bab505676f0d084c07cf89a':
// vault.aragonpm.eth
// eslint-disable-next-line no-fallthrough
case '0x7e852e0fcfce6551c13800f1e7476f982525c2b5277ba14b24339c68416336d1':
switch (sigHash) {
// transfer(address,address,uint256)
case '0xbeabacc8':
return [{
receiver: params[1],
token: params[0],
amount: params[2].toString(),
}]
}
break
}

return []
}
3 changes: 3 additions & 0 deletions packages/connect-voting/src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './numbers'
export * from './actions'
export * from './time'
3 changes: 3 additions & 0 deletions packages/connect-voting/src/helpers/numbers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { BigNumber } from 'ethers'

export const bn = (x: string | number): BigNumber => BigNumber.from(x.toString())
4 changes: 4 additions & 0 deletions packages/connect-voting/src/helpers/time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { BigNumber } from 'ethers'
import { bn } from './numbers'

export const currentTimestampEvm = (): BigNumber => bn(Math.floor(Date.now() / 1000))
48 changes: 46 additions & 2 deletions packages/connect-voting/src/models/Vote.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { decodeCallScript, findAppMethodFromData, App } from '@aragon/connect'
import { addressesEqual, subscription } from '@aragon/connect-core'
import { SubscriptionCallback, SubscriptionResult } from '@aragon/connect-types'
import { subscription } from '@aragon/connect-core'
import { IVotingConnector, VoteData } from '../types'
import Cast from './Cast'
import { Action, IVotingConnector, VoteData, VoteStatus } from '../types'
import { bn, currentTimestampEvm, getRewards } from '../helpers'

export default class Vote {
#connector: IVotingConnector
Expand All @@ -13,13 +15,16 @@ export default class Vote {
readonly executed: boolean
readonly executedAt: string
readonly startDate: string
readonly endDate: string
readonly snapshotBlock: string
readonly supportRequiredPct: string
readonly minAcceptQuorum: string
readonly yea: string
readonly nay: string
readonly votingPower: string
readonly script: string
readonly isAccepted: boolean


constructor(data: VoteData, connector: IVotingConnector) {
this.#connector = connector
Expand All @@ -31,13 +36,52 @@ export default class Vote {
this.executed = data.executed
this.executedAt = data.executedAt
this.startDate = data.startDate
this.endDate = data.endDate
this.snapshotBlock = data.snapshotBlock
this.supportRequiredPct = data.supportRequiredPct
this.minAcceptQuorum = data.minAcceptQuorum
this.yea = data.yea
this.nay = data.nay
this.votingPower = data.votingPower
this.script = data.script
this.isAccepted = data.isAccepted
}

get status(): VoteStatus {
const currentTimestamp = currentTimestampEvm()

if (!this.executed) {
if (currentTimestamp.gte(bn(this.endDate))) {
return this.isAccepted ? VoteStatus.Accepted : VoteStatus.Rejected
}

return VoteStatus.Ongoing
}

return VoteStatus.Executed
}

getActions(installedApps: App[]): Action[] {
const rawActions = decodeCallScript(this.script)

return rawActions.map(({ to, data}): Action => {
const targetApp = installedApps.find(app => addressesEqual(app.address, to))
const fnData = targetApp ? findAppMethodFromData(targetApp, data) : undefined

// Check targetApp again to avoid typescript undefined warnings below
if (!targetApp || !fnData) {
return {
to,
rewards: []
}
}

return {
to,
fnData: findAppMethodFromData(targetApp, data),
rewards: getRewards(targetApp.appId, fnData)
}
})
}

async casts({ first = 1000, skip = 0 } = {}): Promise<Cast[]> {
Expand Down
4 changes: 4 additions & 0 deletions packages/connect-voting/src/thegraph/queries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ export const ALL_VOTES = (type: string) => gql`
executed
executedAt
startDate
endDate
snapshotBlock
supportRequiredPct
minAcceptQuorum
yea
nay
votingPower
isAccepted
script
}
}
Expand All @@ -39,12 +41,14 @@ export const CASTS_FOR_VOTE = (type: string) => gql`
executed
executedAt
startDate
endDate
snapshotBlock
supportRequiredPct
minAcceptQuorum
yea
nay
votingPower
isAccepted
script
}
voter {
Expand Down
Loading