Skip to content
This repository was archived by the owner on May 22, 2025. It is now read-only.
Open
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[] => {
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