diff --git a/packages/subgraph/.gitignore b/packages/subgraph/.gitignore index 8fd224081..dc70443e2 100644 --- a/packages/subgraph/.gitignore +++ b/packages/subgraph/.gitignore @@ -9,3 +9,4 @@ artifacts deploy-output.txt dev-data tests/.latest.json +tests/helpers/extended-schema.ts diff --git a/packages/subgraph/CHANGELOG.md b/packages/subgraph/CHANGELOG.md index 829451947..9bab6dd47 100644 --- a/packages/subgraph/CHANGELOG.md +++ b/packages/subgraph/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added `method-classes`. +- Added `schema-extender.ts`. - Added `installations` to `IPlugin`. - Added `PluginRelease`. - Added `metadata` to `PluginVersion`. diff --git a/packages/subgraph/package.json b/packages/subgraph/package.json index 628e55790..46ee6673e 100644 --- a/packages/subgraph/package.json +++ b/packages/subgraph/package.json @@ -8,12 +8,14 @@ "lint": "eslint . --ext .ts", "build:contracts": "cd ../contracts && yarn build", "manifest": "scripts/build-manifest.sh", - "build": "scripts/build-subgraph.sh", + "extend:schema": "yarn ts-node tests/schema-extender.ts", + "build": "scripts/build-subgraph.sh && yarn extend:schema", "deploy": "scripts/deploy-subgraph.sh", "create:local": "graph create aragon/aragon-core-rinkeby --node http://localhost:8020", "deploy:local": "LOCAL=true scripts/deploy-subgraph.sh", "start:dev": "docker-compose -f docker/docker-compose.yml up -d && sleep 15 && yarn create:local && yarn deploy:local", "stop:dev": "docker-compose -f docker/docker-compose.yml down", + "test:fast": "graph test", "test": "graph test -r", "coverage": "graph test -c", "formatting:check": "prettier '**/*.{json,ts,js}' -c", @@ -26,6 +28,8 @@ "@typescript-eslint/parser": "^5.18.0", "eslint": "^8.12.0", "matchstick-as": "^0.5.1", - "mustache": "^4.2.0" + "mustache": "^4.2.0", + "ts-morph": "^17.0.1", + "typescript": "^4.9.5" } } diff --git a/packages/subgraph/schema.graphql b/packages/subgraph/schema.graphql index 60d9055e5..4280f0d06 100644 --- a/packages/subgraph/schema.graphql +++ b/packages/subgraph/schema.graphql @@ -348,7 +348,7 @@ type TokenVotingPlugin implements IPlugin @entity { minParticipation: BigInt minDuration: BigInt minProposerVotingPower: BigInt - proposalCount: BigInt + proposalCount: BigInt! token: Token members: [TokenVotingMember!]! @derivedFrom(field: "plugin") } @@ -362,7 +362,7 @@ type TokenVotingMember @entity { type TokenVotingVoter @entity { id: ID! # address - address: String # address as string to facilitate filtering by address on the UI + address: String! # address as string to facilitate filtering by address on the UI proposals: [TokenVotingVote!]! @derivedFrom(field: "voter") plugin: TokenVotingPlugin! lastUpdated: BigInt diff --git a/packages/subgraph/src/plugin/utils.ts b/packages/subgraph/src/plugin/utils.ts index 03b539cc7..f264f4254 100644 --- a/packages/subgraph/src/plugin/utils.ts +++ b/packages/subgraph/src/plugin/utils.ts @@ -5,6 +5,7 @@ import { ethereum, crypto, ByteArray, + BigInt } from '@graphprotocol/graph-ts'; import {TokenVoting as TokenVotingContract} from '../../generated/templates/TokenVoting/TokenVoting'; @@ -44,6 +45,7 @@ function createTokenVotingPlugin(plugin: Address, daoId: string): void { packageEntity = new TokenVotingPlugin(pluginId); packageEntity.pluginAddress = plugin; packageEntity.dao = daoId; + packageEntity.proposalCount = BigInt.zero(); let contract = TokenVotingContract.bind(plugin); let supportThreshold = contract.try_supportThreshold(); let minParticipation = contract.try_minParticipation(); @@ -80,6 +82,7 @@ function createAddresslistVotingPlugin(plugin: Address, daoId: string): void { packageEntity = new AddresslistVotingPlugin(plugin.toHexString()); packageEntity.pluginAddress = plugin; packageEntity.dao = daoId; + packageEntity.proposalCount = BigInt.zero(); let contract = AddresslistVotingContract.bind(plugin); @@ -136,6 +139,7 @@ function createMultisigPlugin(plugin: Address, daoId: string): void { packageEntity.onlyListed = false; packageEntity.pluginAddress = plugin; packageEntity.dao = daoId; + packageEntity.proposalCount = BigInt.zero(); // Create template let context = new DataSourceContext(); diff --git a/packages/subgraph/src/utils/constants.ts b/packages/subgraph/src/utils/constants.ts index 3a67ad60c..d122f0c63 100644 --- a/packages/subgraph/src/utils/constants.ts +++ b/packages/subgraph/src/utils/constants.ts @@ -7,11 +7,22 @@ export const VOTER_OPTIONS = new Map() .set(2, 'Yes') .set(3, 'No'); +export const VOTE_OPTIONS = new Map() + .set('None', '0') + .set('Abstain', '1') + .set('Yes', '2') + .set('No', '3'); + export const VOTING_MODES = new Map() .set(0, 'Standard') .set(1, 'EarlyExecution') .set(2, 'VoteReplacement'); +export const VOTING_MODE_INDEXES = new Map() + .set('Standard', '0') + .set('EarlyExecution', '1') + .set('VoteReplacement', '2'); + export const TOKEN_VOTING_INTERFACE = '0x50eb001e'; export const ADDRESSLIST_VOTING_INTERFACE = '0x5f21eb8b'; export const ADMIN_INTERFACE = '0xa5793356'; diff --git a/packages/subgraph/tests/admin/adminMembers.test.ts b/packages/subgraph/tests/admin/adminMembers.test.ts index 893c5414f..aeed83fb9 100644 --- a/packages/subgraph/tests/admin/adminMembers.test.ts +++ b/packages/subgraph/tests/admin/adminMembers.test.ts @@ -5,7 +5,7 @@ import { test, describe, beforeEach, - afterEach, + afterEach } from 'matchstick-as/assembly/index'; import {ADDRESS_ONE, ADDRESS_TWO, DAO_ADDRESS} from '../constants'; diff --git a/packages/subgraph/tests/constants.ts b/packages/subgraph/tests/constants.ts index eb96b6307..5a8df809a 100644 --- a/packages/subgraph/tests/constants.ts +++ b/packages/subgraph/tests/constants.ts @@ -19,14 +19,6 @@ export const TWO = '2'; export const THREE = '3'; export const PROPOSAL_ID = ZERO; -export const PROPOSAL_ENTITY_ID = getProposalId( - Address.fromString(CONTRACT_ADDRESS), - BigInt.fromString(PROPOSAL_ID) -); - -export const PLUGIN_ENTITY_ID = Address.fromString( - CONTRACT_ADDRESS -).toHexString(); export const STRING_DATA = 'Some String Data ...'; @@ -54,7 +46,7 @@ export const ALLOW_FAILURE_MAP = '1'; export const MIN_VOTING_POWER = TWO; export const TOTAL_VOTING_POWER = THREE; -export const CREATED_AT = '1644850000'; +export const CREATED_AT = ONE; export const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000'; @@ -72,3 +64,12 @@ export const PLUGIN_SETUP_ID = '0xfb3fd2c4cd4e19944dd3f8437e67476240cd9e3efb2294ebd10c59c8f1d6817c'; export const APPLIED_PLUGIN_SETUP_ID = '0x00000000cd4e19944dd3f8437e67476240cd9e3efb2294ebd10c59c8f1d6817c'; + +export const PROPOSAL_ENTITY_ID = getProposalId( + Address.fromString(CONTRACT_ADDRESS), + BigInt.fromString(PROPOSAL_ID) +); + +export const PLUGIN_ENTITY_ID = Address.fromString( + CONTRACT_ADDRESS +).toHexString(); diff --git a/packages/subgraph/tests/helpers/method-classes.ts b/packages/subgraph/tests/helpers/method-classes.ts new file mode 100644 index 000000000..b5b610261 --- /dev/null +++ b/packages/subgraph/tests/helpers/method-classes.ts @@ -0,0 +1,291 @@ +/** + * IMPORTANT: Do not export classes from this file. + * The classes of this file are meant to be incorporated into the classes of ./extended-schema.ts + */ + +import {Address, bigInt, BigInt, ethereum} from '@graphprotocol/graph-ts'; +import {log} from 'matchstick-as'; +import { + ERC20Contract, + TokenVotingPlugin, + TokenVotingProposal, + TokenVotingVote, + TokenVotingVoter +} from '../../generated/schema'; +import { + ProposalCreated, + ProposalExecuted, + VoteCast, + VotingSettingsUpdated +} from '../../generated/templates/TokenVoting/TokenVoting'; +import { + VOTER_OPTIONS, + VOTE_OPTIONS, + VOTING_MODES, + VOTING_MODE_INDEXES +} from '../../src/utils/constants'; +import { + ADDRESS_ONE, + ALLOW_FAILURE_MAP, + CONTRACT_ADDRESS, + CREATED_AT, + DAO_ADDRESS, + END_DATE, + MIN_VOTING_POWER, + PROPOSAL_ENTITY_ID, + PROPOSAL_ID, + SNAPSHOT_BLOCK, + START_DATE, + SUPPORT_THRESHOLD, + TOTAL_VOTING_POWER, + TWO, + VOTING_MODE, + ZERO, + MIN_PARTICIPATION, + MIN_DURATION, + ONE, + DAO_TOKEN_ADDRESS, + STRING_DATA +} from '../constants'; +import { + createNewProposalCreatedEvent, + createNewProposalExecutedEvent, + createNewVoteCastEvent, + createNewVotingSettingsUpdatedEvent, + getProposalCountCall +} from '../token/utils'; +import {createGetProposalCall, createTotalVotingPowerCall} from '../utils'; + +class ERC20ContractMethods extends ERC20Contract { + withDefaultValues(): ERC20ContractMethods { + this.id = Address.fromHexString(CONTRACT_ADDRESS).toHexString(); + this.name = 'Test Token'; + this.symbol = 'TT'; + this.decimals = 18; + + return this; + } +} +class TokenVotingVoterMethods extends TokenVotingVoter { + withDefaultValues(): TokenVotingVoterMethods { + this.id = Address.fromHexString(CONTRACT_ADDRESS) + .toHexString() + .concat('_') + .concat(ADDRESS_ONE); + this.address = ADDRESS_ONE; + this.plugin = Address.fromHexString(CONTRACT_ADDRESS).toHexString(); + this.lastUpdated = BigInt.fromString(ZERO); + + return this; + } +} + +class TokenVotingProposalMethods extends TokenVotingProposal { + withDefaultValues(): TokenVotingProposalMethods { + this.id = PROPOSAL_ENTITY_ID; + + this.dao = DAO_ADDRESS; + this.plugin = Address.fromHexString(CONTRACT_ADDRESS).toHexString(); + this.proposalId = BigInt.fromString(PROPOSAL_ID); + this.creator = Address.fromHexString(ADDRESS_ONE); + + this.open = true; + this.executed = false; + + this.votingMode = VOTING_MODE; + this.supportThreshold = BigInt.fromString(SUPPORT_THRESHOLD); + this.minVotingPower = BigInt.fromString(MIN_VOTING_POWER); + this.startDate = BigInt.fromString(START_DATE); + this.endDate = BigInt.fromString(END_DATE); + this.snapshotBlock = BigInt.fromString(SNAPSHOT_BLOCK); + + this.yes = BigInt.fromString(ZERO); + this.no = BigInt.fromString(ZERO); + this.abstain = BigInt.fromString(ZERO); + this.castedVotingPower = BigInt.fromString(ZERO); + + this.totalVotingPower = BigInt.fromString(TOTAL_VOTING_POWER); + this.allowFailureMap = BigInt.fromString(ALLOW_FAILURE_MAP); + this.createdAt = BigInt.fromString(CREATED_AT); + this.creationBlockNumber = BigInt.fromString(ZERO); + this.potentiallyExecutable = false; + + return this; + } + + // calls + // (only read call from contracts related to this) + mockCall_getProposal(actions: ethereum.Tuple[]): void { + if (!this.yes || !this.no || !this.abstain) { + throw new Error('Yes, No, or Abstain can not be null'); + } else { + createGetProposalCall( + this.plugin, + this.proposalId.toString(), + this.open, + this.executed, + this.votingMode, + this.supportThreshold.toString(), + this.minVotingPower.toString(), + this.startDate.toString(), + this.endDate.toString(), + this.snapshotBlock.toString(), + (this.abstain as BigInt).toString(), + (this.yes as BigInt).toString(), + (this.no as BigInt).toString(), + actions, + this.allowFailureMap.toString() + ); + } + } + + mockCall_totalVotingPower(): void { + createTotalVotingPowerCall( + this.plugin, + this.snapshotBlock.toString(), + this.totalVotingPower.toString() + ); + } + + // event + createEvent_ProposalCreated( + actions: ethereum.Tuple[], + description: string = STRING_DATA + ): ProposalCreated { + let event = createNewProposalCreatedEvent( + this.proposalId.toString(), + this.creator.toHexString(), + this.startDate.toString(), + this.endDate.toString(), + description, + actions, + this.allowFailureMap.toString(), + this.plugin + ); + + return event; + } + + createEvent_VoteCast( + voter: string, + voterVoteOption: string, + voterVotingPower: string + ): VoteCast { + if (!VOTE_OPTIONS.has(voterVoteOption)) { + throw new Error('Voter vote option is not valid.'); + } + + // we use casting here to remove autocompletion complaint + // since we know it will be captured by the previous check + let voteOption = VOTE_OPTIONS.get(voterVoteOption) as string; + + let event = createNewVoteCastEvent( + this.proposalId.toString(), + voter, + voteOption, + voterVotingPower, + this.plugin + ); + return event; + } + + createEvent_ProposalExecuted(): ProposalExecuted { + let event = createNewProposalExecutedEvent( + this.proposalId.toString(), + this.plugin + ); + return event; + } +} + +class TokenVotingVoteMethods extends TokenVotingVote { + // build entity + // if id not changed it will update + withDefaultValues(): TokenVotingVoteMethods { + let voterOptionIndex = 0; + if (!VOTER_OPTIONS.has(voterOptionIndex)) { + throw new Error('Voter option is not valid.'); + } + + // we use casting here to remove autocompletion complaint + // since we know it will be captured by the previous check + let voterOption = VOTER_OPTIONS.get(voterOptionIndex) as string; + + this.id = ADDRESS_ONE.concat('_').concat(PROPOSAL_ENTITY_ID); + this.voter = Address.fromHexString(CONTRACT_ADDRESS) + .toHexString() + .concat('_') + .concat(ADDRESS_ONE); + this.proposal = PROPOSAL_ENTITY_ID; + this.voteOption = voterOption; + this.votingPower = BigInt.fromString(TWO); + this.createdAt = BigInt.fromString(CREATED_AT); + this.voteReplaced = false; + this.updatedAt = BigInt.fromString(ZERO); + + return this; + } +} + +class TokenVotingPluginMethods extends TokenVotingPlugin { + // build entity + // if id not changed it will update + withDefaultValues(): TokenVotingPluginMethods { + let votingModeIndex = parseInt(VOTING_MODE); + if (!VOTING_MODES.has(votingModeIndex)) { + throw new Error('voting mode is not valid.'); + } + + // we use casting here to remove autocompletion complaint + // since we know it will be captured by the previous check + let votingMode = VOTING_MODES.get(votingModeIndex) as string; + + const pluginAddress = Address.fromHexString(CONTRACT_ADDRESS); + this.id = pluginAddress.toHexString(); + this.dao = DAO_ADDRESS; + this.pluginAddress = pluginAddress; + this.votingMode = votingMode; + this.supportThreshold = BigInt.fromString(SUPPORT_THRESHOLD); + this.minParticipation = BigInt.fromString(MIN_PARTICIPATION); + this.minDuration = BigInt.fromString(MIN_DURATION); + this.minProposerVotingPower = BigInt.zero(); + this.proposalCount = BigInt.zero(); + this.token = DAO_TOKEN_ADDRESS; + + return this; + } + + mockCall_getProposalCountCall(): void { + getProposalCountCall( + this.pluginAddress.toHexString(), + this.proposalCount.toString() + ); + } + + createEvent_VotingSettingsUpdated(): VotingSettingsUpdated { + if (this.votingMode === null) { + throw new Error('Voting mode is null.'); + } + + // we cast to string only for stoping rust compiler complaints. + let votingMode: string = this.votingMode as string; + if (!VOTING_MODE_INDEXES.has(votingMode)) { + throw new Error('Voting mode index is not valid.'); + } + + // we use casting here to remove autocompletion complaint + // since we know it will be captured by the previous check + let votingModeIndex = VOTING_MODE_INDEXES.get(votingMode) as string; + + let event = createNewVotingSettingsUpdatedEvent( + votingModeIndex, // for event we need the index of the mapping to simulate the contract event + (this.supportThreshold as BigInt).toString(), + (this.minParticipation as BigInt).toString(), + (this.minDuration as BigInt).toString(), + (this.minProposerVotingPower as BigInt).toString(), + this.pluginAddress.toHexString() + ); + + return event; + } +} diff --git a/packages/subgraph/tests/schema-extender.ts b/packages/subgraph/tests/schema-extender.ts new file mode 100644 index 000000000..76e351796 --- /dev/null +++ b/packages/subgraph/tests/schema-extender.ts @@ -0,0 +1,168 @@ +import {CodeBlockWriter, Project} from 'ts-morph'; + +function main() { + const project = new Project(); + const sourceFile = project.addSourceFileAtPath('./generated/schema.ts'); + const sourceMethodFile = project.addSourceFileAtPath( + './tests/helpers/method-classes.ts' + ); + + const outputFile = project.createSourceFile( + './tests/helpers/extended-schema.ts', + '', + { + overwrite: true + } + ); + + // Get original Entity classes + const sourceClasses = sourceFile.getClasses(); + + // Get additional method classes + const sourceMethodClasses = sourceMethodFile.getClasses(); + + // Add all imports from sourcemethod-classes + const methodsImportDeclarations = sourceMethodFile.getImportDeclarations(); + methodsImportDeclarations.forEach(importDeclaration => { + if ( + importDeclaration.getModuleSpecifierValue() !== '../../generated/schema' + ) { + outputFile.addImportDeclaration(importDeclaration.getStructure()); + } + }); + + // Import assert into generated file + outputFile.addImportDeclaration({ + namedImports: ['assert', 'log'], + moduleSpecifier: `matchstick-as` + }); + + // Add import statements for the original classes + const sourceFileNameWithoutExtension = sourceFile.getBaseNameWithoutExtension(); + outputFile.addImportDeclaration({ + namedImports: sourceClasses.map( + classDeclaration => classDeclaration.getName() as string + ), + moduleSpecifier: `../../generated/${sourceFileNameWithoutExtension}` + }); + + // Iterate through the classes in the source file + sourceClasses.forEach(classDeclaration => { + // Create a new class based on the original one + const originalClassName = classDeclaration.getName() as string; + const newClassName = `Extended${originalClassName}`; + const newClass = outputFile.addClass({ + name: newClassName, + isExported: true, + extends: originalClassName + }); + + // Create a new constructor that calls super() with a default id. + newClass.addConstructor({ + parameters: [], + statements: (writer: CodeBlockWriter) => { + const defaultEntityId = '0x1'; + writer.writeLine(`super('${defaultEntityId}');`); + } + }); + + // add methods to generated classes + sourceMethodClasses.forEach(classDeclaration => { + if (classDeclaration.getName() === `${originalClassName}Methods`) { + const methods = classDeclaration.getMethods(); + + methods.forEach(method => { + let returnType = method.getReturnTypeNode()?.getText() || ''; + if (returnType === `${originalClassName}Methods`) { + returnType = `Extended${originalClassName}`; + } + + const newMethod = newClass.addMethod({ + name: method.getName(), + returnType: returnType + }); + + const parameters = method.getParameters().map(parameter => { + return { + name: parameter.getName(), + type: parameter.getTypeNode()?.getText() || '', + hasQuestionToken: parameter.hasQuestionToken(), + initializer: parameter.getInitializer()?.getText(), + decorators: parameter + .getDecorators() + .map(decorator => decorator.getStructure()) + }; + }); + + newMethod.addParameters(parameters); + + const statements = method + .getStatements() + .map(statement => statement.getText()); + newMethod.addStatements(statements); + }); + } + }); + + newClass.addMethod({ + name: 'buildOrUpdate', + returnType: 'void', + statements: (writer: CodeBlockWriter) => { + writer.writeLine('this.save();'); + } + }); + + newClass.addMethod({ + name: 'assertEntity', + returnType: 'void', + parameters: [ + { + name: 'debug', + type: 'boolean', + initializer: 'false' + } + ], + statements: (writer: CodeBlockWriter) => { + writer.writeLine(`let entity = ${originalClassName}.load(this.id);`); + writer.writeLine(`if (!entity) throw new Error("Entity not found");`); + writer.writeLine(`let entries = entity.entries;`); + writer.write('for (let i = 0; i < entries.length; i++)').block(() => { + writer.writeLine(`let key = entries[i].key;`); + + writer.write('if (debug)').block(() => { + writer.writeLine(`log.debug('asserting for key: {}', [key]);`); + }); + + writer.writeLine(`let value = this.get(key);`); + + writer + .write('if (!value)') + .block(() => { + writer.write('if (debug)').block(() => { + writer.writeLine( + `log.debug('value is null for key: {}', [key]);` + ); + }); + }) + .write('else') + .block(() => { + writer.write('if (debug)').block(() => { + writer.writeLine( + `log.debug('asserting with value: {}', [value.displayData()]);` + ); + }); + + writer.writeLine( + `assert.fieldEquals("${originalClassName}", this.id, key, value.displayData());` + ); + }); + }); + } + }); + }); + + // Save the changes + outputFile.saveSync(); +} + +main(); diff --git a/packages/subgraph/tests/token/token-voting.test.ts b/packages/subgraph/tests/token/token-voting.test.ts index 08e458152..3311ed1ad 100644 --- a/packages/subgraph/tests/token/token-voting.test.ts +++ b/packages/subgraph/tests/token/token-voting.test.ts @@ -1,5 +1,11 @@ -import {assert, clearStore, test} from 'matchstick-as/assembly/index'; -import {Address, BigInt, Bytes} from '@graphprotocol/graph-ts'; +import { + assert, + clearStore, + log, + logStore, + test +} from 'matchstick-as/assembly/index'; +import {Address, bigInt, BigInt, Bytes} from '@graphprotocol/graph-ts'; import { handleVoteCast, @@ -8,7 +14,7 @@ import { _handleProposalCreated } from '../../src/packages/token/token-voting'; import {TokenVotingPlugin} from '../../generated/schema'; -import {VOTING_MODES} from '../../src/utils/constants'; +import {VOTER_OPTIONS, VOTING_MODES} from '../../src/utils/constants'; import { ADDRESS_ONE, DAO_TOKEN_ADDRESS, @@ -28,7 +34,10 @@ import { TOTAL_VOTING_POWER, ALLOW_FAILURE_MAP, ADDRESS_TWO, - PROPOSAL_ENTITY_ID + PROPOSAL_ENTITY_ID, + ONE, + ZERO, + TWO } from '../constants'; import { @@ -41,562 +50,204 @@ import { createNewProposalExecutedEvent, createNewProposalCreatedEvent, createNewVotingSettingsUpdatedEvent, - getProposalCountCall, - createTokenVotingProposalEntityState + getProposalCountCall } from './utils'; +import { + ExtendedTokenVotingPlugin, + ExtendedTokenVotingProposal, + ExtendedTokenVotingVote, + ExtendedTokenVotingVoter +} from '../helpers/extended-schema'; let actions = createDummyActions(DAO_TOKEN_ADDRESS, '0', '0x00000000'); test('Run TokenVoting (handleProposalCreated) mappings with mock event', () => { // create state - let tokenVotingPlugin = new TokenVotingPlugin( - Address.fromString(CONTRACT_ADDRESS).toHexString() - ); - tokenVotingPlugin.dao = DAO_ADDRESS; - tokenVotingPlugin.pluginAddress = Bytes.fromHexString(CONTRACT_ADDRESS); - tokenVotingPlugin.save(); + let tokenVotingPlugin = new ExtendedTokenVotingPlugin().withDefaultValues(); + tokenVotingPlugin.buildOrUpdate(); + // assert with default value + // eg. proposalCount is `0`. + tokenVotingPlugin.assertEntity(); - // create calls - getProposalCountCall(CONTRACT_ADDRESS, '1'); - createGetProposalCall( - CONTRACT_ADDRESS, - PROPOSAL_ID, - true, - false, - - VOTING_MODE, - SUPPORT_THRESHOLD, - MIN_VOTING_POWER, - START_DATE, - END_DATE, - SNAPSHOT_BLOCK, - - '0', // abstain - '0', // yes - '0', // no - - actions, - ALLOW_FAILURE_MAP - ); + let proposal = new ExtendedTokenVotingProposal().withDefaultValues(); - createTotalVotingPowerCall( - CONTRACT_ADDRESS, - SNAPSHOT_BLOCK, - TOTAL_VOTING_POWER - ); + // create calls + tokenVotingPlugin.proposalCount = BigInt.fromString(ONE); + tokenVotingPlugin.mockCall_getProposalCountCall(); + proposal.mockCall_getProposal(actions); + proposal.mockCall_totalVotingPower(); // create event - let event = createNewProposalCreatedEvent( - PROPOSAL_ID, - ADDRESS_ONE, - START_DATE, - END_DATE, - STRING_DATA, - [], - ALLOW_FAILURE_MAP, - CONTRACT_ADDRESS - ); + let event = proposal.createEvent_ProposalCreated(actions, STRING_DATA); // handle event - _handleProposalCreated(event, DAO_ADDRESS, STRING_DATA); - - let packageId = Address.fromString(CONTRACT_ADDRESS).toHexString(); + _handleProposalCreated(event, proposal.dao, STRING_DATA); // checks - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'id', - PROPOSAL_ENTITY_ID - ); - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'dao', - DAO_ADDRESS - ); - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'plugin', - packageId - ); - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'proposalId', - PROPOSAL_ID - ); - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'creator', - ADDRESS_ONE - ); - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'metadata', - STRING_DATA - ); - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'allowFailureMap', - ALLOW_FAILURE_MAP - ); - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'createdAt', - event.block.timestamp.toString() - ); - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'creationBlockNumber', - event.block.number.toString() - ); - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'startDate', - START_DATE - ); - - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'votingMode', - VOTING_MODES.get(parseInt(VOTING_MODE)) - ); - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'supportThreshold', - SUPPORT_THRESHOLD - ); - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'minVotingPower', - MIN_VOTING_POWER - ); - - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'startDate', - START_DATE - ); - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'endDate', - END_DATE - ); - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'snapshotBlock', - SNAPSHOT_BLOCK - ); - - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'totalVotingPower', - TOTAL_VOTING_POWER - ); - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'executed', - 'false' - ); + // expected changes + proposal.creationBlockNumber = BigInt.fromString(ONE); + proposal.votingMode = VOTING_MODES.get(parseInt(VOTING_MODE)) as string; + // check TokenVotingProposal + proposal.assertEntity(); // check TokenVotingPlugin - assert.fieldEquals( - 'TokenVotingPlugin', - Address.fromString(CONTRACT_ADDRESS).toHexString(), - 'proposalCount', - '1' - ); + tokenVotingPlugin.assertEntity(); clearStore(); }); test('Run TokenVoting (handleVoteCast) mappings with mock event', () => { - let proposal = createTokenVotingProposalEntityState(); - - // create calls 1 - createGetProposalCall( - CONTRACT_ADDRESS, - PROPOSAL_ID, - true, - false, - - VOTING_MODE, - SUPPORT_THRESHOLD, - MIN_VOTING_POWER, - START_DATE, - END_DATE, - SNAPSHOT_BLOCK, - - '0', // abstain - '1', // yes - '0', // no - - actions, - ALLOW_FAILURE_MAP - ); + // create state + let proposal = new ExtendedTokenVotingProposal().withDefaultValues(); - createTotalVotingPowerCall( - CONTRACT_ADDRESS, - SNAPSHOT_BLOCK, - TOTAL_VOTING_POWER - ); + proposal.buildOrUpdate(); + + // check proposal entity + proposal.assertEntity(); + + // // create calls + proposal.yes = bigInt.fromString(ONE); + proposal.mockCall_getProposal(actions); + proposal.mockCall_totalVotingPower(); // create event - let event = createNewVoteCastEvent( - PROPOSAL_ID, - ADDRESS_ONE, - '2', // Yes - '1', // votingPower - CONTRACT_ADDRESS - ); + let voter = new ExtendedTokenVotingVoter().withDefaultValues(); - handleVoteCast(event); + let vote = new ExtendedTokenVotingVote().withDefaultValues(); + vote.voteOption = 'Yes'; + vote.votingPower = bigInt.fromString(ONE); - // checks - let voteEntityID = ADDRESS_ONE + '_' + proposal.id; - assert.fieldEquals('TokenVotingVote', voteEntityID, 'id', voteEntityID); - assert.fieldEquals('TokenVotingVote', voteEntityID, 'voteReplaced', 'false'); - assert.fieldEquals( - 'TokenVotingVote', - voteEntityID, - 'updatedAt', - BigInt.zero().toString() + // fire an event of `VoteCast` with voter info. + let event = proposal.createEvent_VoteCast( + voter.address, + vote.voteOption, + vote.votingPower.toString() ); - // check voter - let memberId = - Address.fromString(CONTRACT_ADDRESS).toHexString() + - '_' + - Address.fromString(ADDRESS_ONE).toHexString(); - - assert.fieldEquals('TokenVotingVoter', memberId, 'id', memberId); - assert.fieldEquals('TokenVotingVoter', memberId, 'address', ADDRESS_ONE); - assert.fieldEquals( - 'TokenVotingVoter', - memberId, - 'plugin', - Address.fromString(CONTRACT_ADDRESS).toHexString() - ); - assert.fieldEquals( - 'TokenVotingVoter', - memberId, - 'lastUpdated', - event.block.timestamp.toString() - ); + // test handler + handleVoteCast(event); + + // checks vote entity created via handler (not builder) + vote.assertEntity(); // check proposal - assert.fieldEquals('TokenVotingProposal', proposal.id, 'yes', '1'); - - // Check potentiallyExecutable - // abstain: 0, yes: 1, no: 0 - // support : 100% - // worstCaseSupport : 33% - // participation : 33% - assert.fieldEquals( - 'TokenVotingProposal', - proposal.id, - 'potentiallyExecutable', - 'false' - ); - // check vote count - assert.fieldEquals( - 'TokenVotingProposal', - proposal.id, - 'castedVotingPower', - '1' - ); + // expected changes to the proposal entity + proposal.castedVotingPower = BigInt.fromString(ONE); + proposal.potentiallyExecutable = false; + // assert proposal entity + proposal.assertEntity(); // Check when voter replace vote // create calls 2 - createGetProposalCall( - CONTRACT_ADDRESS, - PROPOSAL_ID, - true, - false, - - VOTING_MODE, - SUPPORT_THRESHOLD, - MIN_VOTING_POWER, - START_DATE, - END_DATE, - SNAPSHOT_BLOCK, - - '0', // abstain - '0', // yes - '1', // no - - actions, - ALLOW_FAILURE_MAP - ); + proposal.yes = BigInt.fromString(ZERO); + proposal.no = BigInt.fromString(ONE); + proposal.mockCall_getProposal(actions); + proposal.mockCall_totalVotingPower(); - createTotalVotingPowerCall( - CONTRACT_ADDRESS, - SNAPSHOT_BLOCK, - TOTAL_VOTING_POWER - ); + vote.voteOption = 'No'; - // create event - let event2 = createNewVoteCastEvent( - PROPOSAL_ID, - ADDRESS_ONE, - '3', // No - '1', // votingPower - CONTRACT_ADDRESS + let event2 = proposal.createEvent_VoteCast( + voter.address, + vote.voteOption, + vote.votingPower.toString() ); handleVoteCast(event2); - // checks 2 - assert.fieldEquals('TokenVotingVote', voteEntityID, 'voteReplaced', 'true'); - assert.fieldEquals( - 'TokenVotingVote', - voteEntityID, - 'updatedAt', - event2.block.timestamp.toString() - ); + // expected changes in TokenVotingVote + vote.voteReplaced = true; + vote.updatedAt = bigInt.fromString(ONE); + + // checks vote entity created via handler (not builder) + vote.assertEntity(); // create calls 3 - createGetProposalCall( - CONTRACT_ADDRESS, - PROPOSAL_ID, - true, - false, - - VOTING_MODE, - SUPPORT_THRESHOLD, - MIN_VOTING_POWER, - START_DATE, - END_DATE, - SNAPSHOT_BLOCK, - - '0', // abstain - '2', // yes - '0', // no - - actions, - ALLOW_FAILURE_MAP - ); - // create event 3 - let event3 = createNewVoteCastEvent( - PROPOSAL_ID, - ADDRESS_TWO, - '2', // yes - '1', // votingPower - CONTRACT_ADDRESS + proposal.yes = BigInt.fromString(TWO); + proposal.no = BigInt.fromString(ZERO); + proposal.mockCall_getProposal(actions); + + vote.voteOption = 'Yes'; + + let event3 = proposal.createEvent_VoteCast( + voter.address, + vote.voteOption, + vote.votingPower.toString() ); handleVoteCast(event3); - // Check potentiallyExecutable - // abstain: 0, yes: 2, no: 0 - // support : 100% - // worstCaseSupport : 67% - // participation : 67% - assert.fieldEquals( - 'TokenVotingProposal', - proposal.id, - 'potentiallyExecutable', - 'true' - ); + // expected changes to the proposal entity + proposal.potentiallyExecutable = true; + proposal.castedVotingPower = BigInt.fromString(TWO); - assert.fieldEquals( - 'TokenVotingProposal', - proposal.id, - 'castedVotingPower', - '2' - ); + proposal.assertEntity(); clearStore(); }); test('Run TokenVoting (handleVoteCast) mappings with mock event and vote option "None"', () => { - // create state - let proposal = createTokenVotingProposalEntityState(); + let proposal = new ExtendedTokenVotingProposal().withDefaultValues(); // create calls - createGetProposalCall( - CONTRACT_ADDRESS, - PROPOSAL_ID, - true, - false, - - // ProposalParameters - VOTING_MODE, - SUPPORT_THRESHOLD, - MIN_VOTING_POWER, - START_DATE, - END_DATE, - SNAPSHOT_BLOCK, - - // Tally - '0', // abstain - '0', // yes - '0', // no - - actions, - ALLOW_FAILURE_MAP - ); + proposal.mockCall_getProposal(actions); // create event - let event = createNewVoteCastEvent( - PROPOSAL_ID, - ADDRESS_ONE, - '0', // none - '1', // votingPower - CONTRACT_ADDRESS + let voter = new ExtendedTokenVotingVoter().withDefaultValues(); + let vote = new ExtendedTokenVotingVote().withDefaultValues(); + vote.voteOption = 'None'; + vote.votingPower = BigInt.fromString(ONE); + + let event = proposal.createEvent_VoteCast( + voter.address, + vote.voteOption, + vote.votingPower.toString() ); handleVoteCast(event); - // checks - let entityID = ADDRESS_ONE + '_' + proposal.id; - assert.notInStore('TokenVotingVoter', entityID); + // checks TokenVotingVoter + assert.notInStore('TokenVotingVoter', voter.id); clearStore(); }); test('Run TokenVoting (handleProposalExecuted) mappings with mock event', () => { // create state - createTokenVotingProposalEntityState( - PROPOSAL_ENTITY_ID, - DAO_ADDRESS, - CONTRACT_ADDRESS, - ADDRESS_ONE - ); + let proposal = new ExtendedTokenVotingProposal().withDefaultValues(); + proposal.yes = BigInt.fromString(ONE); + proposal.buildOrUpdate(); // create calls - createGetProposalCall( - CONTRACT_ADDRESS, - PROPOSAL_ID, - true, - true, - - VOTING_MODE, - SUPPORT_THRESHOLD, - MIN_VOTING_POWER, - START_DATE, - END_DATE, - SNAPSHOT_BLOCK, - - '0', // abstain - '1', // yes - '0', // no - - actions, - ALLOW_FAILURE_MAP - ); + proposal.mockCall_getProposal(actions); // create event - let event = createNewProposalExecutedEvent('0', CONTRACT_ADDRESS); + let event = proposal.createEvent_ProposalExecuted(); // handle event handleProposalExecuted(event); // checks - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'id', - PROPOSAL_ENTITY_ID - ); - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'executed', - 'true' - ); - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'executionDate', - event.block.timestamp.toString() - ); - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'executionBlockNumber', - event.block.number.toString() - ); - assert.fieldEquals( - 'TokenVotingProposal', - PROPOSAL_ENTITY_ID, - 'executionTxHash', - event.transaction.hash.toHexString() - ); + // expected changes + proposal.executed = true; + // assert TokenVotingProposal + proposal.assertEntity(); clearStore(); }); test('Run TokenVoting (handleVotingSettingsUpdated) mappings with mock event', () => { // create state - let entityID = Address.fromString(CONTRACT_ADDRESS).toHexString(); - let tokenVotingPlugin = new TokenVotingPlugin(entityID); - tokenVotingPlugin.dao = DAO_ADDRESS; - tokenVotingPlugin.pluginAddress = Bytes.fromHexString(CONTRACT_ADDRESS); - tokenVotingPlugin.save(); + let tokenVotingPlugin = new ExtendedTokenVotingPlugin().withDefaultValues(); + tokenVotingPlugin.buildOrUpdate(); // create event - let event = createNewVotingSettingsUpdatedEvent( - VOTING_MODE, - SUPPORT_THRESHOLD, - MIN_PARTICIPATION, - MIN_DURATION, - MIN_PROPOSER_VOTING_POWER, - - CONTRACT_ADDRESS - ); + let event = tokenVotingPlugin.createEvent_VotingSettingsUpdated(); // handle event handleVotingSettingsUpdated(event); // checks - assert.fieldEquals('TokenVotingPlugin', entityID, 'id', entityID); - assert.fieldEquals( - 'TokenVotingPlugin', - entityID, - 'votingMode', - VOTING_MODES.get(parseInt(VOTING_MODE)) - ); - assert.fieldEquals( - 'TokenVotingPlugin', - entityID, - 'supportThreshold', - SUPPORT_THRESHOLD - ); - assert.fieldEquals( - 'TokenVotingPlugin', - entityID, - 'minParticipation', - MIN_PARTICIPATION - ); - assert.fieldEquals( - 'TokenVotingPlugin', - entityID, - 'minDuration', - MIN_DURATION - ); - assert.fieldEquals( - 'TokenVotingPlugin', - entityID, - 'minProposerVotingPower', - MIN_PROPOSER_VOTING_POWER - ); + tokenVotingPlugin.assertEntity(); clearStore(); }); diff --git a/yarn.lock b/yarn.lock index bddcf72b9..7ae65e7fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1975,6 +1975,16 @@ strip-ansi "^4.0.0" strip-indent "^2.0.0" +"@ts-morph/common@~0.18.0": + version "0.18.1" + resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.18.1.tgz#ca40c3a62c3f9e17142e0af42633ad63efbae0ec" + integrity sha512-RVE+zSRICWRsfrkAw5qCAK+4ZH9kwEFv5h0+/YeHTLieWP7F4wWq4JsKFuNWG+fYh/KF+8rAtgdj5zb2mm+DVA== + dependencies: + fast-glob "^3.2.12" + minimatch "^5.1.0" + mkdirp "^1.0.4" + path-browserify "^1.0.1" + "@typechain/ethers-v5@^10.0.0": version "10.0.0" resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-10.0.0.tgz#1b6e292d2ed9afb0d2f7a4674cc199bb95bad714" @@ -3571,6 +3581,11 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= +code-block-writer@^11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-11.0.3.tgz#9eec2993edfb79bfae845fbc093758c0a0b73b76" + integrity sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw== + code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -5253,6 +5268,17 @@ fast-glob@^3.0.3, fast-glob@^3.1.1: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.2.12: + version "3.2.12" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-glob@^3.2.9: version "3.2.11" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" @@ -7809,6 +7835,13 @@ minimatch@5.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@^5.1.0: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" @@ -8721,6 +8754,11 @@ pascal-case@^2.0.0: camel-case "^3.0.0" upper-case-first "^1.1.0" +path-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + path-case@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/path-case/-/path-case-2.1.1.tgz#94b8037c372d3fe2906e465bb45e25d226e8eea5" @@ -10574,6 +10612,14 @@ ts-essentials@^7.0.1: resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38" integrity sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ== +ts-morph@^17.0.1: + version "17.0.1" + resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-17.0.1.tgz#d85df4fcf9a1fcda1b331d52c00655f381c932d1" + integrity sha512-10PkHyXmrtsTvZSL+cqtJLTgFXkU43Gd0JCc0Rw6GchWbqKe0Rwgt1v3ouobTZwQzF1mGhDeAlWYBMGRV7y+3g== + dependencies: + "@ts-morph/common" "~0.18.0" + code-block-writer "^11.0.3" + ts-node@^8.1.0: version "8.10.2" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" @@ -10745,6 +10791,11 @@ typescript@^4.4.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.2.tgz#8ac1fba9f52256fdb06fb89e4122fa6a346c2998" integrity sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw== +typescript@^4.9.5: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== + typical@^2.6.0, typical@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d"