diff --git a/packages/connect-voting/src/models/Call.ts b/packages/connect-voting/src/models/Call.ts new file mode 100644 index 00000000..997b0e72 --- /dev/null +++ b/packages/connect-voting/src/models/Call.ts @@ -0,0 +1,15 @@ +import { CallData } from '../types' + +export default class Call { + readonly id: string + readonly vote: string + readonly contract: string + readonly calldata: string + + constructor(data: CallData) { + this.id = data.id + this.vote = data.vote.id + this.contract = data.contract + this.calldata = data.calldata + } +} diff --git a/packages/connect-voting/src/models/Reward.ts b/packages/connect-voting/src/models/Reward.ts new file mode 100644 index 00000000..92d3a357 --- /dev/null +++ b/packages/connect-voting/src/models/Reward.ts @@ -0,0 +1,17 @@ +import { RewardData } from '../types' + +export default class Reward { + readonly id: string + readonly vote: string + readonly token: string + readonly to: string + readonly amount: string + + constructor(data: RewardData) { + this.id = data.id + this.vote = data.vote.id + this.token = data.token + this.to = data.to + this.amount = data.amount + } +} diff --git a/packages/connect-voting/src/models/Vote.ts b/packages/connect-voting/src/models/Vote.ts index 8b20b96c..d6438683 100644 --- a/packages/connect-voting/src/models/Vote.ts +++ b/packages/connect-voting/src/models/Vote.ts @@ -1,7 +1,9 @@ import { SubscriptionCallback, SubscriptionResult } from '@aragon/connect-types' import { subscription } from '@aragon/connect-core' -import { IVotingConnector, VoteData } from '../types' +import { IVotingConnector, VoteData, RewardData } from '../types' import Cast from './Cast' +import Reward from './Reward' +import Call from './Call' export default class Vote { #connector: IVotingConnector @@ -20,6 +22,7 @@ export default class Vote { readonly nay: string readonly votingPower: string readonly script: string + readonly spec: string constructor(data: VoteData, connector: IVotingConnector) { this.#connector = connector @@ -38,6 +41,7 @@ export default class Vote { this.nay = data.nay this.votingPower = data.votingPower this.script = data.script + this.spec = data.spec } async casts({ first = 1000, skip = 0 } = {}): Promise { @@ -52,4 +56,29 @@ export default class Vote { this.#connector.onCastsForVote(this.id, first, skip, callback) ) } + + async rewards({ first = 1000, skip = 0 } = {}): Promise { + return this.#connector.rewardsForVote(this.id, first, skip) + } + + onRewards( + { first = 1000, skip = 0 } = {}, + callback?: SubscriptionCallback + ): SubscriptionResult { + return subscription(callback, (callback) => + this.#connector.onRewardsForVote(this.id, first, skip, callback) + ) + } + async calls({ first = 1000, skip = 0 } = {}): Promise { + return this.#connector.callsForVote(this.id, first, skip) + } + + onCalls( + { first = 1000, skip = 0 } = {}, + callback?: SubscriptionCallback + ): SubscriptionResult { + return subscription(callback, (callback) => + this.#connector.onCallsForVote(this.id, first, skip, callback) + ) + } } diff --git a/packages/connect-voting/src/thegraph/connector.ts b/packages/connect-voting/src/thegraph/connector.ts index bdf2c654..c28b9ade 100644 --- a/packages/connect-voting/src/thegraph/connector.ts +++ b/packages/connect-voting/src/thegraph/connector.ts @@ -6,8 +6,10 @@ import { GraphQLWrapper, QueryResult } from '@aragon/connect-thegraph' import { IVotingConnector } from '../types' import Vote from '../models/Vote' import Cast from '../models/Cast' +import Reward from '../models/Reward' import * as queries from './queries' -import { parseVotes, parseCasts } from './parsers' +import { parseVotes, parseCasts, parseRewards, parseCalls } from './parsers' +import Call from '../models/Call' export function subgraphUrlFromChainId(chainId: number) { if (chainId === 1) { @@ -98,4 +100,56 @@ export default class VotingConnectorTheGraph implements IVotingConnector { (result: QueryResult) => parseCasts(result) ) } + + async rewardsForVote( + vote: string, + first: number, + skip: number + ): Promise { + return this.#gql.performQueryWithParser( + queries.REWARDS_FOR_VOTE('query'), + { vote, first, skip }, + (result: QueryResult) => parseRewards(result) + ) + } + + onRewardsForVote( + vote: string, + first: number, + skip: number, + callback: SubscriptionCallback + ): SubscriptionHandler { + return this.#gql.subscribeToQueryWithParser( + queries.REWARDS_FOR_VOTE('subscription'), + { vote, first, skip }, + callback, + (result: QueryResult) => parseRewards(result) + ) + } + + async callsForVote( + vote: string, + first: number, + skip: number + ): Promise { + return this.#gql.performQueryWithParser( + queries.CALLS_FOR_VOTE('query'), + { vote, first, skip }, + (result: QueryResult) => parseCalls(result) + ) + } + + onCallsForVote( + vote: string, + first: number, + skip: number, + callback: SubscriptionCallback + ): SubscriptionHandler { + return this.#gql.subscribeToQueryWithParser( + queries.CALLS_FOR_VOTE('subscription'), + { vote, first, skip }, + callback, + (result: QueryResult) => parseCalls(result) + ) + } } diff --git a/packages/connect-voting/src/thegraph/parsers/calls.ts b/packages/connect-voting/src/thegraph/parsers/calls.ts new file mode 100644 index 00000000..9c51fb68 --- /dev/null +++ b/packages/connect-voting/src/thegraph/parsers/calls.ts @@ -0,0 +1,27 @@ +import { ErrorUnexpectedResult } from 'packages/connect-core/dist/cjs' +import { QueryResult } from 'packages/connect-thegraph/dist/cjs' +import Call from '../../models/Call' +import { CallData } from '../../types' + +export function parseCalls(result: QueryResult): Call[] { + const calls = result.data.calls + + if (!calls) { + throw new ErrorUnexpectedResult('Unable to parse calls.') + } + + const datas = calls.map( + (call: any): CallData => { + return { + id: call.id, + vote: call.vote.id, + contract: call.contract, + calldata: call.calldata, + } + } + ) + + return datas.map((data: CallData) => { + return new Call(data) + }) +} diff --git a/packages/connect-voting/src/thegraph/parsers/index.ts b/packages/connect-voting/src/thegraph/parsers/index.ts index 905c5968..b613de73 100644 --- a/packages/connect-voting/src/thegraph/parsers/index.ts +++ b/packages/connect-voting/src/thegraph/parsers/index.ts @@ -1,2 +1,4 @@ export { parseVotes } from './votes' export { parseCasts } from './casts' +export { parseRewards } from './rewards' +export { parseCalls } from './calls' diff --git a/packages/connect-voting/src/thegraph/parsers/rewards.ts b/packages/connect-voting/src/thegraph/parsers/rewards.ts new file mode 100644 index 00000000..f05da2c0 --- /dev/null +++ b/packages/connect-voting/src/thegraph/parsers/rewards.ts @@ -0,0 +1,28 @@ +import { ErrorUnexpectedResult } from 'packages/connect-core/dist/cjs' +import { QueryResult } from 'packages/connect-thegraph/dist/cjs' +import Reward from '../../models/Reward' +import { RewardData } from '../../types' + +export function parseRewards(result: QueryResult): Reward[] { + const rewards = result.data.rewards + + if (!rewards) { + throw new ErrorUnexpectedResult('Unable to parse rewards.') + } + + const datas = rewards.map( + (reward: any): RewardData => { + return { + id: reward.id, + vote: reward.vote.id, + token: reward.token, + to: reward.to, + amount: reward.amount, + } + } + ) + + return datas.map((data: RewardData) => { + return new Reward(data) + }) +} diff --git a/packages/connect-voting/src/thegraph/queries/index.ts b/packages/connect-voting/src/thegraph/queries/index.ts index a38aea9b..483227fa 100644 --- a/packages/connect-voting/src/thegraph/queries/index.ts +++ b/packages/connect-voting/src/thegraph/queries/index.ts @@ -21,6 +21,9 @@ export const ALL_VOTES = (type: string) => gql` nay votingPower script + spec + contract + calldata } } ` @@ -46,6 +49,7 @@ export const CASTS_FOR_VOTE = (type: string) => gql` nay votingPower script + spec } voter { id @@ -57,3 +61,30 @@ export const CASTS_FOR_VOTE = (type: string) => gql` } } ` + +export const REWARDS_FOR_VOTE = (type: string) => gql` + ${type} Rewards($vote: ID!, $first: Int!, $skip: Int!) { + rewards(where: { vote: $vote }, first: $first, skip: $skip) { + id + vote { + id + } + token + amount + to + } + } +` + +export const CALLS_FOR_VOTE = (type: string) => gql` + ${type} Calls($vote: ID!, $first: Int!, $skip: Int!) { + calls(where: { vote: $vote }, first: $first, skip: $skip) { + id + vote { + id + } + contract + calldata + } + } +` diff --git a/packages/connect-voting/src/types.ts b/packages/connect-voting/src/types.ts index e818c9a6..573333e7 100644 --- a/packages/connect-voting/src/types.ts +++ b/packages/connect-voting/src/types.ts @@ -4,6 +4,8 @@ import { } from '@aragon/connect-types' import Vote from './models/Vote' import Cast from './models/Cast' +import Reward from './models/Reward' +import Call from './models/Call' export interface VoteData { id: string @@ -20,6 +22,22 @@ export interface VoteData { nay: string votingPower: string script: string + spec: string +} + +export interface RewardData { + id: string + vote: VoteData + token: string + to: string + amount: string +} + +export interface CallData { + id: string + vote: VoteData + contract: string + calldata: string } export interface CastData { @@ -52,4 +70,18 @@ export interface IVotingConnector { skip: number, callback: SubscriptionCallback ): SubscriptionHandler + rewardsForVote(vote: string, first: number, skip: number): Promise + onRewardsForVote( + vote: string, + first: number, + skip: number, + callback: SubscriptionCallback + ): SubscriptionHandler + callsForVote(vote: string, first: number, skip: number): Promise + onCallsForVote( + vote: string, + first: number, + skip: number, + callback: SubscriptionCallback + ): SubscriptionHandler } diff --git a/packages/connect-voting/subgraph/schema.graphql b/packages/connect-voting/subgraph/schema.graphql index a58832f1..60b9f2e9 100644 --- a/packages/connect-voting/subgraph/schema.graphql +++ b/packages/connect-voting/subgraph/schema.graphql @@ -16,7 +16,25 @@ type Vote @entity { votingPower: BigInt! script: String! voteNum: BigInt! + spec: BigInt! castVotes: [Cast!] @derivedFrom(field: "vote") + rewards: [Reward!] @derivedFrom(field: "vote") + calls: [Call!] @derivedFrom(field: "vote") +} + +type Reward @entity { + id: ID! + vote: Vote! + token: Bytes! + amount: BigInt! + to: Bytes! +} + +type Call @entity { + id: ID! + vote: Vote! + contract: Bytes! + calldata: Bytes! } type Cast @entity { diff --git a/packages/connect-voting/subgraph/src/Voting.ts b/packages/connect-voting/subgraph/src/Voting.ts index 72b4b7ac..1af56d1f 100644 --- a/packages/connect-voting/subgraph/src/Voting.ts +++ b/packages/connect-voting/subgraph/src/Voting.ts @@ -1,4 +1,4 @@ -import { Address, BigInt } from '@graphprotocol/graph-ts' +import { Address, BigInt, ByteArray, Bytes } from '@graphprotocol/graph-ts' import { StartVote as StartVoteEvent, CastVote as CastVoteEvent, @@ -9,15 +9,15 @@ import { Vote as VoteEntity, Cast as CastEntity, Voter as VoterEntity, + Reward as RewardEntity, + Call as CallEntity, } from '../generated/schema' -/* eslint-disable @typescript-eslint/no-use-before-define */ - export function handleStartVote(event: StartVoteEvent): void { - const voteEntityId = buildVoteEntityId(event.address, event.params.voteId) - const vote = new VoteEntity(voteEntityId) - const voting = VotingContract.bind(event.address) - const voteData = voting.getVote(event.params.voteId) + let voteEntityId = buildVoteEntityId(event.address, event.params.voteId) + let vote = new VoteEntity(voteEntityId) + let voting = VotingContract.bind(event.address) + let voteData = voting.getVote(event.params.voteId) vote.appAddress = event.address vote.creator = event.params.creator @@ -31,21 +31,86 @@ export function handleStartVote(event: StartVoteEvent): void { vote.yea = voteData.value6 vote.nay = voteData.value7 vote.votingPower = voteData.value8 - vote.script = voteData.value9.toHex() + vote.script = voteData.value9.toHexString() vote.orgAddress = voting.kernel() vote.executedAt = BigInt.fromI32(0) vote.executed = false + vote.spec = BigInt.fromI32( + Bytes.fromHexString(vote.script.substr(0, 10)).toI32() + ) vote.save() + + let REWARD_SPEC_ID = 0x00000000 + let CALL_SPEC_ID = 0x00000001 + + switch (vote.spec.toI32()) { + case REWARD_SPEC_ID: + saveRewards(vote.id, vote.script) + break + case CALL_SPEC_ID: + saveCalls(vote.id, vote.script) + break + } +} + +export function saveCalls(voteId: string, script: string): void { + let location = 10 + + while (location < script.length) { + let contract = Address.fromHexString(script.substr(location, 40)) as Address + let calldataLength = BigInt.fromUnsignedBytes( + Bytes.fromHexString(script.substr(location + 40, 8)) as Bytes + ) + let calldataLengthNumber = calldataLength.toI32() + let calldata = Bytes.fromHexString( + script.substr(location + 48, calldataLengthNumber * 2) + ) as Bytes + + let call = new CallEntity( + buildCallEntityId( + voteId, + Bytes.fromHexString( + script.substr(location, calldataLengthNumber * 2) + `-${location}` + ).toHexString() + ) + ) + + call.contract = contract + call.calldata = calldata + call.vote = voteId + call.save() + location = location + 48 + calldataLengthNumber * 2 + } +} + +export function saveRewards(voteId: string, script: string): void { + let location = 10 + + while (location < script.length) { + let token = Address.fromHexString(script.substr(location, 40)) as Address + let to = Address.fromHexString(script.substr(location + 40, 40)) as Address + let amount = BigInt.fromUnsignedBytes( + Bytes.fromHexString(script.substr(location + 80, 64)) as Bytes + ) + + let reward = new RewardEntity(buildRewardId(voteId, token, to)) + reward.token = token + reward.amount = amount + reward.to = to + reward.vote = voteId + reward.save() + location = location + 144 + } } export function handleCastVote(event: CastVoteEvent): void { updateVoteState(event.address, event.params.voteId) - const voter = loadOrCreateVoter(event.address, event.params.voter) + let voter = loadOrCreateVoter(event.address, event.params.voter) voter.save() - const castVote = loadOrCreateCastVote( + let castVote = loadOrCreateCastVote( event.address, event.params.voteId, event.params.voter @@ -62,7 +127,7 @@ export function handleCastVote(event: CastVoteEvent): void { export function handleExecuteVote(event: ExecuteVoteEvent): void { updateVoteState(event.address, event.params.voteId) - const vote = VoteEntity.load( + let vote = VoteEntity.load( buildVoteEntityId(event.address, event.params.voteId) )! vote.executed = true @@ -70,6 +135,10 @@ export function handleExecuteVote(event: ExecuteVoteEvent): void { vote.save() } +function buildCallEntityId(voteId: string, data: string): string { + return voteId + '-call:' + data +} + function buildVoteEntityId(appAddress: Address, voteNum: BigInt): string { return ( 'appAddress:' + appAddress.toHexString() + '-vote:' + voteNum.toHexString() @@ -84,11 +153,15 @@ function buildCastEntityId(voteId: BigInt, voter: Address): string { return voteId.toHexString() + '-voter:' + voter.toHexString() } +function buildRewardId(voteId: string, token: Address, to: Address): string { + return voteId + '-reward-' + token.toHexString() + '-' + to.toHexString() +} + function loadOrCreateVoter( votingAddress: Address, voterAddress: Address ): VoterEntity { - const voterId = buildVoterId(votingAddress, voterAddress) + let voterId = buildVoterId(votingAddress, voterAddress) let voter = VoterEntity.load(voterId) if (voter === null) { voter = new VoterEntity(voterId) @@ -102,7 +175,7 @@ function loadOrCreateCastVote( voteId: BigInt, voterAddress: Address ): CastEntity { - const castVoteId = buildCastEntityId(voteId, voterAddress) + let castVoteId = buildCastEntityId(voteId, voterAddress) let castVote = CastEntity.load(castVoteId) if (castVote === null) { castVote = new CastEntity(castVoteId) @@ -112,10 +185,10 @@ function loadOrCreateCastVote( } export function updateVoteState(votingAddress: Address, voteId: BigInt): void { - const votingApp = VotingContract.bind(votingAddress) - const voteData = votingApp.getVote(voteId) + let votingApp = VotingContract.bind(votingAddress) + let voteData = votingApp.getVote(voteId) - const vote = VoteEntity.load(buildVoteEntityId(votingAddress, voteId))! + let vote = VoteEntity.load(buildVoteEntityId(votingAddress, voteId))! vote.yea = voteData.value6 vote.nay = voteData.value7