diff --git a/packages/js-client/src/internal/graphql-queries/index.ts b/packages/js-client/src/internal/graphql-queries/index.ts index 2424681f..69d7b4ad 100644 --- a/packages/js-client/src/internal/graphql-queries/index.ts +++ b/packages/js-client/src/internal/graphql-queries/index.ts @@ -1,3 +1,4 @@ // add your grapql queries here and export them with this file export * from './settings'; export * from './proposal'; +export * from './members'; diff --git a/packages/js-client/src/internal/graphql-queries/members.ts b/packages/js-client/src/internal/graphql-queries/members.ts new file mode 100644 index 00000000..665d8a4f --- /dev/null +++ b/packages/js-client/src/internal/graphql-queries/members.ts @@ -0,0 +1,26 @@ +import { gql } from 'graphql-request'; + +export const QueryPluginMembers = gql` + query PluginMembers($address: String!, $block: Block_height) { + pluginMembers(block: $block, where: { pluginAddress: $address }) { + id + address + balance + votingPower + plugin { + id + } + proposals { + id + } + delegatee { + id + address + } + delegators { + id + address + } + } + } +`; diff --git a/packages/subgraph/manifest/subgraph.placeholder.yaml b/packages/subgraph/manifest/subgraph.placeholder.yaml index b940af40..bcf87624 100644 --- a/packages/subgraph/manifest/subgraph.placeholder.yaml +++ b/packages/subgraph/manifest/subgraph.placeholder.yaml @@ -75,4 +75,25 @@ templates: - event: TallyApproval(indexed uint256,indexed address) handler: handleTallyApproval file: ./src/plugin/plugin.ts - + - name: GovernanceERC20 + kind: ethereum/contract + network: {{network}} + source: + abi: GovernanceERC20 + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + entities: + - PluginMember + abis: + - name: GovernanceERC20 + file: $PLUGIN_MODULE/artifacts/@aragon/osx/token/ERC20/governance/GovernanceERC20.sol/GovernanceERC20.json + eventHandlers: + - event: Transfer(indexed address,indexed address,uint256) + handler: handleTransfer + - event: DelegateChanged(indexed address,indexed address,indexed address) + handler: handleDelegateChanged + - event: DelegateVotesChanged(indexed address,uint256,uint256) + handler: handleDelegateVotesChanged + file: ./src/plugin/governance-erc20.ts \ No newline at end of file diff --git a/packages/subgraph/schema.graphql b/packages/subgraph/schema.graphql index 3302df8b..17183a1b 100644 --- a/packages/subgraph/schema.graphql +++ b/packages/subgraph/schema.graphql @@ -66,8 +66,14 @@ type Plugin implements PluginInstallation @entity { type PluginMember @entity { id: ID! # plugin_address + member_address address: String # address as string to facilitate filtering by address on the UI - proposals: [PluginProposalMember!]! @derivedFrom(field: "approver") + balance: BigInt! plugin: Plugin! + pluginAddress: String! # address as string to facilitate filtering by address on the UI + proposals: [PluginProposalMember!]! @derivedFrom(field: "approver") + delegatee: PluginMember + votingPower: BigInt + # we assume token owners and/or delegatees are members + delegators: [PluginMember!]! @derivedFrom(field: "delegatee") } type PluginProposalMember @entity(immutable: true) { diff --git a/packages/subgraph/src/plugin/governance-erc20.ts b/packages/subgraph/src/plugin/governance-erc20.ts new file mode 100644 index 00000000..d242904e --- /dev/null +++ b/packages/subgraph/src/plugin/governance-erc20.ts @@ -0,0 +1,112 @@ +import {PluginMember} from '../../generated/schema'; +import { + DelegateChanged, + DelegateVotesChanged, + Transfer, +} from '../../generated/templates/GovernanceERC20/GovernanceERC20'; +import {GovernanceERC20 as GovernanceERC20Contract} from '../../generated/templates/GovernanceERC20/GovernanceERC20'; +import {Address, BigInt, dataSource, store} from '@graphprotocol/graph-ts'; + +function getOrCreateMember(user: Address, pluginId: string): PluginMember { + let id = [user.toHexString(), pluginId].join('_'); + let member = PluginMember.load(id); + if (!member) { + member = new PluginMember(id); + member.address = user.toHexString(); + member.balance = BigInt.zero(); + member.plugin = pluginId; + member.pluginAddress = dataSource.address().toHexString(); + member.delegatee = null; + member.votingPower = BigInt.zero(); + } + + return member; +} + +export function handleTransfer(event: Transfer): void { + let context = dataSource.context(); + let pluginId = context.getString('pluginId'); + + if (event.params.from != Address.zero()) { + let fromMember = getOrCreateMember(event.params.from, pluginId); + fromMember.balance = fromMember.balance.minus(event.params.value); + fromMember.save(); + } + + if (event.params.to != Address.zero()) { + let toMember = getOrCreateMember(event.params.to, pluginId); + toMember.balance = toMember.balance.plus(event.params.value); + toMember.save(); + } +} + +export function handleDelegateChanged(event: DelegateChanged): void { + let context = dataSource.context(); + let pluginId = context.getString('pluginId'); + const toDelegate = event.params.toDelegate; + + // make sure `fromDelegate` & `toDelegate`are members + if (event.params.fromDelegate != Address.zero()) { + let fromMember = getOrCreateMember(event.params.fromDelegate, pluginId); + fromMember.save(); + } + if (toDelegate != Address.zero()) { + let toMember = getOrCreateMember(toDelegate, pluginId); + toMember.save(); + } + + // make sure `delegator` is member and set delegatee + if (event.params.delegator != Address.zero()) { + let delegator = getOrCreateMember(event.params.delegator, pluginId); + + // set delegatee + let delegatee: string | null = null; + if (toDelegate != Address.zero()) { + delegatee = [toDelegate.toHexString(), pluginId].join('_'); + + delegator.delegatee = delegatee; + } + + delegator.save(); + } +} + +export function handleDelegateVotesChanged(event: DelegateVotesChanged): void { + const delegate = event.params.delegate; + if (delegate == Address.zero()) return; + const newVotingPower = event.params.newBalance; + + const context = dataSource.context(); + const pluginId = context.getString('pluginId'); + let member = getOrCreateMember(delegate, pluginId); + + if (isZeroBalanceAndVotingPower(member.balance, newVotingPower)) { + if (shouldRemoveMember(event.address, delegate)) { + store.remove('PluginMember', member.id); + return; + } + } + member.votingPower = newVotingPower; + member.save(); +} + +function isZeroBalanceAndVotingPower( + memberBalance: BigInt, + votingPower: BigInt +): boolean { + return ( + memberBalance.equals(BigInt.zero()) && votingPower.equals(BigInt.zero()) + ); +} + +function shouldRemoveMember( + contractAddress: Address, + delegate: Address +): boolean { + const governanceERC20Contract = GovernanceERC20Contract.bind(contractAddress); + const delegates = governanceERC20Contract.try_delegates(delegate); + if (!delegates.reverted) { + return delegates.value == delegate || delegates.value == Address.zero(); + } + return false; +} diff --git a/packages/subgraph/src/plugin/plugin.ts b/packages/subgraph/src/plugin/plugin.ts index 39fec182..d99cbcfb 100644 --- a/packages/subgraph/src/plugin/plugin.ts +++ b/packages/subgraph/src/plugin/plugin.ts @@ -5,6 +5,7 @@ import { PluginProposal, TallyElement, } from '../../generated/schema'; +import {GovernanceERC20} from '../../generated/templates'; import { ExecutionMultisigMembersAdded, ExecutionMultisigMembersRemoved, @@ -14,7 +15,7 @@ import { TallyApproval, TallySet, } from '../../generated/templates/Plugin/VocdoniVoting'; -import {Address, dataSource} from '@graphprotocol/graph-ts'; +import {Address, DataSourceContext, dataSource} from '@graphprotocol/graph-ts'; export function handlePluginSettingsUpdated( event: PluginSettingsUpdated @@ -45,6 +46,14 @@ export function handlePluginSettingsUpdated( pluginEntity.censusStrategyURI = event.params.censusStrategyURI; pluginEntity.save(); } + + // Create template + const pluginContext = new DataSourceContext(); + pluginContext.setString('pluginId', installationId.toHexString()); + GovernanceERC20.createWithContext( + event.params.daoTokenAddress, + pluginContext + ); } }