diff --git a/TESTING.md b/TESTING.md index 24e42ed5a..e9ac1b0e9 100644 --- a/TESTING.md +++ b/TESTING.md @@ -48,7 +48,8 @@ The command will perform the following actions: - Run `migrate` hardhat task to deploy the protocol - Run `migrate:ownership` hardhat task to transfer ownership of governed contracts to the governor - Run `migrate:unpause` to unpause the protocol -- Run `e2e` hardhat task to run all e2e tests, including scenarios +- Run `e2e` hardhat task to run all deployment tests (config and init) +- Run `e2e:scenario` hardhat task to run a scenario ### Other networks diff --git a/cli/commands/migrate.ts b/cli/commands/migrate.ts index 1f33b0f89..b893c6e63 100644 --- a/cli/commands/migrate.ts +++ b/cli/commands/migrate.ts @@ -35,7 +35,11 @@ const allContracts = [ 'AllocationExchange', ] -export const migrate = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise => { +export const migrate = async ( + cli: CLIEnvironment, + cliArgs: CLIArgs, + autoMine = false, +): Promise => { const graphConfigPath = cliArgs.graphConfig const force = cliArgs.force const contractName = cliArgs.contract @@ -140,7 +144,7 @@ export const migrate = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise { + const { contracts, getTestAccounts } = hre.graph() + const { Staking } = contracts + + before(async () => { + indexerFixtures = getIndexerFixtures(await getTestAccounts()) + }) + + describe('Allocations', () => { + let allocations: AllocationFixture[] = [] + let openAllocations: AllocationFixture[] = [] + let closedAllocations: AllocationFixture[] = [] + + before(async () => { + allocations = indexerFixtures.map((i) => i.allocations).flat() + openAllocations = allocations.filter((a) => !a.close) + closedAllocations = allocations.filter((a) => a.close) + }) + + it(`some allocatons should be open`, async function () { + for (const allocation of openAllocations) { + const state = await Staking.getAllocationState(allocation.signer.address) + expect(state).eq(AllocationState.Active) + } + }) + + it(`some allocatons should be closed`, async function () { + for (const allocation of closedAllocations) { + const state = await Staking.getAllocationState(allocation.signer.address) + expect(state).eq(AllocationState.Closed) + } + }) + }) +}) diff --git a/e2e/scenarios/close-allocations.ts b/e2e/scenarios/close-allocations.ts new file mode 100644 index 000000000..4d732b16e --- /dev/null +++ b/e2e/scenarios/close-allocations.ts @@ -0,0 +1,52 @@ +// ### Scenario description ### +// Common protocol actions > Close some allocations +// This scenario will close several open allocations. See fixtures for details. +// Need to wait at least 1 epoch after the allocations have been created before running it. +// On localhost, the epoch is automatically advanced to guarantee this. +// Run with: +// npx hardhat e2e:scenario close-allocations --network --graph-config config/graph..yml + +import hre from 'hardhat' +import { closeAllocation } from './lib/staking' +import { advanceToNextEpoch } from '../../test/lib/testHelpers' +import { fundAccountsETH } from './lib/accounts' +import { getIndexerFixtures } from './fixtures/indexers' +import { getGraphOptsFromArgv } from './lib/helpers' + +async function main() { + const graphOpts = getGraphOptsFromArgv() + const graph = hre.graph(graphOpts) + const indexerFixtures = getIndexerFixtures(await graph.getTestAccounts()) + + const deployer = await graph.getDeployer() + const indexers = indexerFixtures.map((i) => i.signer.address) + const indexerETHBalances = indexerFixtures.map((i) => i.ethBalance) + + // == Fund participants + console.log('\n== Fund indexers') + await fundAccountsETH(deployer, indexers, indexerETHBalances) + + // == Time travel on local networks, ensure allocations can be closed + if (['hardhat', 'localhost'].includes(hre.network.name)) { + console.log('\n== Advancing to next epoch') + await advanceToNextEpoch(graph.contracts.EpochManager) + } + + // == Close allocations + console.log('\n== Close allocations') + + for (const indexer of indexerFixtures) { + for (const allocation of indexer.allocations.filter((a) => a.close)) { + await closeAllocation(graph.contracts, indexer.signer, allocation.signer.address) + } + } +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exitCode = 1 + }) diff --git a/e2e/scenarios/create-subgraphs.test.ts b/e2e/scenarios/create-subgraphs.test.ts new file mode 100644 index 000000000..b77bd60de --- /dev/null +++ b/e2e/scenarios/create-subgraphs.test.ts @@ -0,0 +1,72 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import { recreatePreviousSubgraphId } from './lib/subgraph' +import { BigNumber } from 'ethers' +import { CuratorFixture, getCuratorFixtures } from './fixtures/curators' +import { SubgraphFixture, getSubgraphFixtures, getSubgraphOwner } from './fixtures/subgraphs' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' + +let curatorFixtures: CuratorFixture[] +let subgraphFixtures: SubgraphFixture[] +let subgraphOwnerFixture: SignerWithAddress + +describe('Publish subgraphs', () => { + const { contracts, getTestAccounts } = hre.graph() + const { GNS, GraphToken, Curation } = contracts + + before(async () => { + const testAccounts = await getTestAccounts() + curatorFixtures = getCuratorFixtures(testAccounts) + subgraphFixtures = getSubgraphFixtures() + subgraphOwnerFixture = getSubgraphOwner(testAccounts).signer + }) + + describe('GRT balances', () => { + it(`curator balances should match airdropped amount minus signalled`, async function () { + for (const curator of curatorFixtures) { + const address = curator.signer.address + const balance = await GraphToken.balanceOf(address) + expect(balance).eq(curator.grtBalance.sub(curator.signalled)) + } + }) + }) + + describe('Subgraphs', () => { + it(`should be published`, async function () { + for (let i = 0; i < subgraphFixtures.length; i++) { + const subgraphId = await recreatePreviousSubgraphId( + contracts, + subgraphOwnerFixture.address, + subgraphFixtures.length - i, + ) + const isPublished = await GNS.isPublished(subgraphId) + expect(isPublished).eq(true) + } + }) + + it(`should have signal`, async function () { + for (let i = 0; i < subgraphFixtures.length; i++) { + const subgraph = subgraphFixtures[i] + const subgraphId = await recreatePreviousSubgraphId( + contracts, + subgraphOwnerFixture.address, + subgraphFixtures.length - i, + ) + + let totalSignal: BigNumber = BigNumber.from(0) + for (const curator of curatorFixtures) { + const _subgraph = curator.subgraphs.find((s) => s.deploymentId === subgraph.deploymentId) + if (_subgraph) { + totalSignal = totalSignal.add(_subgraph.signal) + } + } + + const tokens = await GNS.subgraphTokens(subgraphId) + const MAX_PPM = 1000000 + const curationTax = await Curation.curationTaxPercentage() + const tax = totalSignal.mul(curationTax).div(MAX_PPM) + expect(tokens).eq(totalSignal.sub(tax)) + } + }) + }) +}) diff --git a/e2e/scenarios/create-subgraphs.ts b/e2e/scenarios/create-subgraphs.ts new file mode 100644 index 000000000..04b6fb535 --- /dev/null +++ b/e2e/scenarios/create-subgraphs.ts @@ -0,0 +1,71 @@ +// ### Scenario description ### +// Common protocol actions > Set up subgraphs: publish and signal +// This scenario will create a set of subgraphs and add signal to them. See fixtures for details. +// Run with: +// npx hardhat e2e:scenario create-subgraphs --network --graph-config config/graph..yml + +import hre from 'hardhat' +import { publishNewSubgraph } from './lib/subgraph' +import { fundAccountsETH, fundAccountsGRT } from './lib/accounts' +import { signal } from './lib/curation' +import { getSubgraphFixtures, getSubgraphOwner } from './fixtures/subgraphs' +import { getCuratorFixtures } from './fixtures/curators' +import { getGraphOptsFromArgv } from './lib/helpers' + +async function main() { + const graphOpts = getGraphOptsFromArgv() + const graph = hre.graph(graphOpts) + const testAccounts = await graph.getTestAccounts() + + const subgraphFixtures = getSubgraphFixtures() + const subgraphOwnerFixture = getSubgraphOwner(testAccounts) + const curatorFixtures = getCuratorFixtures(testAccounts) + + const deployer = await graph.getDeployer() + const subgraphOwners = [subgraphOwnerFixture.signer.address] + const subgraphOwnerETHBalance = [subgraphOwnerFixture.ethBalance] + const curators = curatorFixtures.map((c) => c.signer.address) + const curatorETHBalances = curatorFixtures.map((i) => i.ethBalance) + const curatorGRTBalances = curatorFixtures.map((i) => i.grtBalance) + + // == Fund participants + console.log('\n== Fund subgraph owners and curators') + await fundAccountsETH( + deployer, + [...subgraphOwners, ...curators], + [...subgraphOwnerETHBalance, ...curatorETHBalances], + ) + await fundAccountsGRT(deployer, curators, curatorGRTBalances, graph.contracts.GraphToken) + + // == Publish subgraphs + console.log('\n== Publishing subgraphs') + + for (const subgraph of subgraphFixtures) { + const id = await publishNewSubgraph( + graph.contracts, + subgraphOwnerFixture.signer, + subgraph.deploymentId, + ) + const subgraphData = subgraphFixtures.find((s) => s.deploymentId === subgraph.deploymentId) + if (subgraphData) subgraphData.subgraphId = id + } + + // == Signal subgraphs + console.log('\n== Signaling subgraphs') + for (const curator of curatorFixtures) { + for (const subgraph of curator.subgraphs) { + const subgraphData = subgraphFixtures.find((s) => s.deploymentId === subgraph.deploymentId) + if (subgraphData) + await signal(graph.contracts, curator.signer, subgraphData.subgraphId, subgraph.signal) + } + } +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exitCode = 1 + }) diff --git a/e2e/scenarios/fixtures/curators.ts b/e2e/scenarios/fixtures/curators.ts new file mode 100644 index 000000000..da7bf5b67 --- /dev/null +++ b/e2e/scenarios/fixtures/curators.ts @@ -0,0 +1,81 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { BigNumber } from 'ethers' +import { toGRT } from '../../../cli/network' + +export interface CuratorFixture { + signer: SignerWithAddress + ethBalance: BigNumber + grtBalance: BigNumber + signalled: BigNumber + subgraphs: SubgraphFixture[] +} + +export interface SubgraphFixture { + deploymentId: string + signal: BigNumber +} + +// Test account indexes +// 3: curator1 +// 4: curator2 +// 5: curator3 + +export const getCuratorFixtures = (signers: SignerWithAddress[]): CuratorFixture[] => { + return [ + // curator1 + { + signer: signers[3], + ethBalance: toGRT(0.1), + grtBalance: toGRT(100_000), + signalled: toGRT(10_400), + subgraphs: [ + { + deploymentId: '0x0653445635cc1d06bd2370d2a9a072406a420d86e7fa13ea5cde100e2108b527', + signal: toGRT(400), + }, + { + deploymentId: '0x3093dadafd593b5c2d10c16bf830e96fc41ea7b91d7dabd032b44331fb2a7e51', + signal: toGRT(4_000), + }, + { + deploymentId: '0xb3fc2abc303c70a16ab9d5fc38d7e8aeae66593a87a3d971b024dd34b97e94b1', + signal: toGRT(6_000), + }, + ], + }, + // curator2 + { + signer: signers[4], + ethBalance: toGRT(0.1), + grtBalance: toGRT(100_000), + signalled: toGRT(4_500), + subgraphs: [ + { + deploymentId: '0x3093dadafd593b5c2d10c16bf830e96fc41ea7b91d7dabd032b44331fb2a7e51', + signal: toGRT(2_000), + }, + { + deploymentId: '0xb3fc2abc303c70a16ab9d5fc38d7e8aeae66593a87a3d971b024dd34b97e94b1', + signal: toGRT(2_500), + }, + ], + }, + // curator3 + { + signer: signers[5], + ethBalance: toGRT(0.1), + grtBalance: toGRT(100_000), + signalled: toGRT(8_000), + subgraphs: [ + { + deploymentId: '0x3093dadafd593b5c2d10c16bf830e96fc41ea7b91d7dabd032b44331fb2a7e51', + signal: toGRT(4_000), + }, + { + deploymentId: '0xb3fc2abc303c70a16ab9d5fc38d7e8aeae66593a87a3d971b024dd34b97e94b1', + signal: toGRT(4_000), + }, + ], + }, + ] +} diff --git a/e2e/scenarios/fixtures/indexers.ts b/e2e/scenarios/fixtures/indexers.ts new file mode 100644 index 000000000..3a6ad7c14 --- /dev/null +++ b/e2e/scenarios/fixtures/indexers.ts @@ -0,0 +1,101 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { BigNumber } from 'ethers' +import { toGRT } from '../../../cli/network' + +export interface IndexerFixture { + signer: SignerWithAddress + ethBalance: BigNumber + grtBalance: BigNumber + stake: BigNumber + allocations: AllocationFixture[] +} + +export interface AllocationFixture { + signer: SignerWithAddress + subgraphDeploymentId: string + amount: BigNumber + close: boolean +} + +// Test account indexes +// 0: indexer1 +// 1: indexer2 +// 6: allocation1 +// 7: allocation2 +// 8: allocation3 +// 9: allocation4 +// 10: allocation5 +// 11: allocation6 +// 12: allocation7 + +export const getIndexerFixtures = (signers: SignerWithAddress[]): IndexerFixture[] => { + return [ + // indexer1 + { + signer: signers[0], + ethBalance: toGRT(0.1), + grtBalance: toGRT(100_000), + stake: toGRT(100_000), + allocations: [ + { + signer: signers[6], + subgraphDeploymentId: + '0xbbde25a2c85f55b53b7698b9476610c3d1202d88870e66502ab0076b7218f98a', + amount: toGRT(25_000), + close: false, + }, + { + signer: signers[7], + subgraphDeploymentId: + '0x0653445635cc1d06bd2370d2a9a072406a420d86e7fa13ea5cde100e2108b527', + amount: toGRT(50_000), + close: true, + }, + { + signer: signers[8], + subgraphDeploymentId: + '0xbbde25a2c85f55b53b7698b9476610c3d1202d88870e66502ab0076b7218f98a', + amount: toGRT(10_000), + close: true, + }, + ], + }, + // indexer2 + { + signer: signers[1], + ethBalance: toGRT(0.1), + grtBalance: toGRT(100_000), + stake: toGRT(100_000), + allocations: [ + { + signer: signers[9], + subgraphDeploymentId: + '0x3093dadafd593b5c2d10c16bf830e96fc41ea7b91d7dabd032b44331fb2a7e51', + amount: toGRT(25_000), + close: true, + }, + { + signer: signers[10], + subgraphDeploymentId: + '0x0653445635cc1d06bd2370d2a9a072406a420d86e7fa13ea5cde100e2108b527', + amount: toGRT(10_000), + close: false, + }, + { + signer: signers[11], + subgraphDeploymentId: + '0x3093dadafd593b5c2d10c16bf830e96fc41ea7b91d7dabd032b44331fb2a7e51', + amount: toGRT(10_000), + close: true, + }, + { + signer: signers[12], + subgraphDeploymentId: + '0xb3fc2abc303c70a16ab9d5fc38d7e8aeae66593a87a3d971b024dd34b97e94b1', + amount: toGRT(45_000), + close: true, + }, + ], + }, + ] +} diff --git a/e2e/scenarios/fixtures/subgraphs.ts b/e2e/scenarios/fixtures/subgraphs.ts new file mode 100644 index 000000000..dc2e04aab --- /dev/null +++ b/e2e/scenarios/fixtures/subgraphs.ts @@ -0,0 +1,43 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { BigNumber } from 'ethers' +import { toGRT } from '../../../cli/network' + +export interface SubgraphOwnerFixture { + signer: SignerWithAddress + ethBalance: BigNumber + grtBalance: BigNumber +} + +export interface SubgraphFixture { + deploymentId: string + subgraphId: string | null +} + +// Test account indexes +// 2: subgraphOwner +export const getSubgraphOwner = (signers: SignerWithAddress[]): SubgraphOwnerFixture => { + return { + signer: signers[2], + ethBalance: toGRT(0.1), + grtBalance: toGRT(100_000), + } +} + +export const getSubgraphFixtures = (): SubgraphFixture[] => [ + { + deploymentId: '0xbbde25a2c85f55b53b7698b9476610c3d1202d88870e66502ab0076b7218f98a', + subgraphId: null, + }, + { + deploymentId: '0x0653445635cc1d06bd2370d2a9a072406a420d86e7fa13ea5cde100e2108b527', + subgraphId: null, + }, + { + deploymentId: '0x3093dadafd593b5c2d10c16bf830e96fc41ea7b91d7dabd032b44331fb2a7e51', + subgraphId: null, + }, + { + deploymentId: '0xb3fc2abc303c70a16ab9d5fc38d7e8aeae66593a87a3d971b024dd34b97e94b1', + subgraphId: null, + }, +] diff --git a/e2e/scenarios/lib/accounts.ts b/e2e/scenarios/lib/accounts.ts new file mode 100644 index 000000000..6796a7aea --- /dev/null +++ b/e2e/scenarios/lib/accounts.ts @@ -0,0 +1,119 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { BigNumber, BigNumberish, ContractTransaction } from 'ethers' +import { ethers } from 'hardhat' +import { GraphToken } from '../../../build/types/GraphToken' +import { TransactionResponse } from '@ethersproject/providers' + +const checkBalance = async ( + address: string, + amount: BigNumber, + getBalanceFn: (address: string) => Promise, +) => { + const balance = await getBalanceFn(address) + if (balance.lt(amount)) { + throw new Error( + `Sender does not have enough funds to distribute! Required ${amount} - Balance ${balance}`, + ) + } +} + +const ensureBalance = async ( + beneficiary: string, + amount: BigNumberish, + getBalanceFn: (address: string) => Promise, + transferFn: ( + address: string, + transferAmount: BigNumber, + ) => Promise, +) => { + const balance = await getBalanceFn(beneficiary) + const balanceDif = BigNumber.from(amount).sub(balance) + + if (balanceDif.gt(0)) { + console.log(`Funding ${beneficiary} with ${balanceDif}...`) + const tx = await transferFn(beneficiary, balanceDif) + await tx.wait() + } +} + +export const ensureETHBalance = async ( + sender: SignerWithAddress, + beneficiaries: string[], + amounts: BigNumberish[], +): Promise => { + if (beneficiaries.length !== amounts.length) { + throw new Error('beneficiaries and amounts must be the same length!') + } + for (let index = 0; index < beneficiaries.length; index++) { + await ensureBalance( + beneficiaries[index], + amounts[index], + ethers.provider.getBalance, + (address: string, amount: BigNumber) => { + return sender.sendTransaction({ to: address, value: amount }) + }, + ) + } +} + +export const ensureGRTAllowance = async ( + owner: SignerWithAddress, + spender: string, + amount: BigNumberish, + grt: GraphToken, +): Promise => { + const allowance = await grt.allowance(owner.address, spender) + const allowTokens = BigNumber.from(amount).sub(allowance) + if (allowTokens.gt(0)) { + console.log( + `\nApproving ${spender} to spend ${allowTokens} tokens on ${owner.address} behalf...`, + ) + await grt.connect(owner).approve(spender, amount) + } +} + +export const fundAccountsETH = async ( + sender: SignerWithAddress, + beneficiaries: string[], + amounts: BigNumberish[], +): Promise => { + if (beneficiaries.length !== amounts.length) { + throw new Error('beneficiaries and amounts must be the same length!') + } + // Ensure sender has enough funds to distribute + const totalETH = amounts.reduce( + (sum: BigNumber, amount: BigNumberish) => sum.add(BigNumber.from(amount)), + BigNumber.from(0), + ) + await checkBalance(sender.address, BigNumber.from(totalETH), ethers.provider.getBalance) + + // Fund the accounts + await ensureETHBalance(sender, beneficiaries, amounts) +} + +export const fundAccountsGRT = async ( + sender: SignerWithAddress, + beneficiaries: string[], + amounts: BigNumberish[], + grt: GraphToken, +): Promise => { + if (beneficiaries.length !== amounts.length) { + throw new Error('beneficiaries and amounts must be the same length!') + } + // Ensure sender has enough funds to distribute + const totalGRT = amounts.reduce( + (sum: BigNumber, amount: BigNumberish) => sum.add(BigNumber.from(amount)), + BigNumber.from(0), + ) + await checkBalance(sender.address, BigNumber.from(totalGRT), grt.balanceOf) + + // Fund the accounts + for (let index = 0; index < beneficiaries.length; index++) { + await ensureBalance( + beneficiaries[index], + amounts[index], + grt.balanceOf, + grt.connect(sender).transfer, + ) + } +} diff --git a/e2e/scenarios/lib/curation.ts b/e2e/scenarios/lib/curation.ts index 53f2761cf..1dc35cbb2 100644 --- a/e2e/scenarios/lib/curation.ts +++ b/e2e/scenarios/lib/curation.ts @@ -1,15 +1,21 @@ -import { BigNumberish, Signer } from 'ethers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { BigNumberish } from 'ethers' import { NetworkContracts } from '../../../cli/contracts' +import { sendTransaction } from '../../../cli/network' +import { ensureGRTAllowance } from './accounts' export const signal = async ( contracts: NetworkContracts, - curator: Signer, + curator: SignerWithAddress, subgraphId: string, amount: BigNumberish, ): Promise => { - const { GNS } = contracts + // Approve + await ensureGRTAllowance(curator, contracts.GNS.address, amount, contracts.GraphToken) // Add signal - const tx = await GNS.connect(curator).mintSignal(subgraphId, amount, 0) - await tx.wait() + console.log(`\nAdd ${amount} in signal to subgraphId ${subgraphId}..`) + await sendTransaction(curator, contracts.GNS, 'mintSignal', [subgraphId, amount, 0], { + gasLimit: 2000000, + }) } diff --git a/e2e/scenarios/lib/helpers.ts b/e2e/scenarios/lib/helpers.ts new file mode 100644 index 000000000..db9da32cb --- /dev/null +++ b/e2e/scenarios/lib/helpers.ts @@ -0,0 +1,14 @@ +export function getGraphOptsFromArgv(): { + graphConfig: string | undefined + addressBook: string | undefined +} { + const argv = process.argv.slice(2) + + const getArgv = (index: number) => + argv[index] && argv[index] !== 'undefined' ? argv[index] : undefined + + return { + graphConfig: getArgv(0), + addressBook: getArgv(1), + } +} diff --git a/e2e/scenarios/lib/staking.ts b/e2e/scenarios/lib/staking.ts index e0726cf4f..234d6b1ce 100644 --- a/e2e/scenarios/lib/staking.ts +++ b/e2e/scenarios/lib/staking.ts @@ -1,18 +1,59 @@ -import { BigNumberish, Signer } from 'ethers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { BigNumberish, ethers } from 'ethers' import { NetworkContracts } from '../../../cli/contracts' +import { randomHexBytes, sendTransaction } from '../../../cli/network' +import { ensureGRTAllowance } from './accounts' export const stake = async ( contracts: NetworkContracts, - indexer: Signer, + indexer: SignerWithAddress, amount: BigNumberish, ): Promise => { - const { GraphToken, Staking } = contracts - // Approve - const txApprove = await GraphToken.connect(indexer).approve(Staking.address, amount) - await txApprove.wait() + await ensureGRTAllowance(indexer, contracts.Staking.address, amount, contracts.GraphToken) // Stake - const txStake = await Staking.connect(indexer).stake(amount) - await txStake.wait() + console.log(`\nStaking ${amount} tokens...`) + await sendTransaction(indexer, contracts.Staking, 'stake', [amount]) +} + +export const allocateFrom = async ( + contracts: NetworkContracts, + indexer: SignerWithAddress, + allocationSigner: SignerWithAddress, + subgraphDeploymentID: string, + amount: BigNumberish, +): Promise => { + const allocationId = allocationSigner.address + const messageHash = ethers.utils.solidityKeccak256( + ['address', 'address'], + [indexer.address, allocationId], + ) + const messageHashBytes = ethers.utils.arrayify(messageHash) + const proof = await allocationSigner.signMessage(messageHashBytes) + const metadata = ethers.constants.HashZero + + console.log(`\nAllocating ${amount} tokens on ${allocationId}...`) + await sendTransaction( + indexer, + contracts.Staking, + 'allocateFrom', + [indexer.address, subgraphDeploymentID, amount, allocationId, metadata, proof], + { + gasLimit: 2000000, + }, + ) +} + +export const closeAllocation = async ( + contracts: NetworkContracts, + indexer: SignerWithAddress, + allocationId: string, +): Promise => { + const poi = randomHexBytes() + + console.log(`\nClosing ${allocationId}...`) + await sendTransaction(indexer, contracts.Staking, 'closeAllocation', [allocationId, poi], { + gasLimit: 2000000, + }) } diff --git a/e2e/scenarios/lib/subgraph.ts b/e2e/scenarios/lib/subgraph.ts index de5cf0477..c17c4e543 100644 --- a/e2e/scenarios/lib/subgraph.ts +++ b/e2e/scenarios/lib/subgraph.ts @@ -1,17 +1,35 @@ -import { Signer, utils } from 'ethers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { BigNumber } from 'ethers' +import { solidityKeccak256 } from 'ethers/lib/utils' import { NetworkContracts } from '../../../cli/contracts' +import { randomHexBytes, sendTransaction } from '../../../cli/network' -const { hexlify, randomBytes } = utils +export const recreatePreviousSubgraphId = async ( + contracts: NetworkContracts, + owner: string, + previousIndex: number, +): Promise => { + const seqID = (await contracts.GNS.nextAccountSeqID(owner)).sub(previousIndex) + return buildSubgraphID(owner, seqID) +} + +export const buildSubgraphID = (account: string, seqID: BigNumber): string => + solidityKeccak256(['address', 'uint256'], [account, seqID]) export const publishNewSubgraph = async ( contracts: NetworkContracts, - publisher: Signer, + publisher: SignerWithAddress, deploymentId: string, -): Promise => { - const tx = await contracts.GNS.connect(publisher).publishNewSubgraph( - deploymentId, - hexlify(randomBytes(32)), - hexlify(randomBytes(32)), +): Promise => { + console.log(`\nPublishing new subgraph with deploymentId ${deploymentId}...`) + const subgraphId = buildSubgraphID( + publisher.address, + await contracts.GNS.nextAccountSeqID(publisher.address), ) - await tx.wait() + await sendTransaction(publisher, contracts.GNS, 'publishNewSubgraph', [ + deploymentId, + randomHexBytes(), + randomHexBytes(), + ]) + return subgraphId } diff --git a/e2e/scenarios/lib/token.ts b/e2e/scenarios/lib/token.ts deleted file mode 100644 index 01fd5f4f5..000000000 --- a/e2e/scenarios/lib/token.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { BigNumberish, ContractReceipt, Signer } from 'ethers' -import { NetworkContracts } from '../../../cli/contracts' - -export const airdrop = async ( - contracts: NetworkContracts, - sender: Signer, - beneficiaries: string[], - amount: BigNumberish, -): Promise => { - const { GraphToken } = contracts - - const txs: Promise[] = [] - - for (const beneficiary of beneficiaries) { - const tx = await GraphToken.connect(sender).transfer(beneficiary, amount) - txs.push(tx.wait()) - } - await Promise.all(txs) -} diff --git a/e2e/scenarios/open-allocations.test.ts b/e2e/scenarios/open-allocations.test.ts new file mode 100644 index 000000000..ae7c3de5a --- /dev/null +++ b/e2e/scenarios/open-allocations.test.ts @@ -0,0 +1,52 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import { getIndexerFixtures, IndexerFixture } from './fixtures/indexers' + +enum AllocationState { + Null, + Active, + Closed, + Finalized, + Claimed, +} + +let indexerFixtures: IndexerFixture[] + +describe('Open allocations', () => { + const { contracts, getTestAccounts } = hre.graph() + const { GraphToken, Staking } = contracts + + before(async () => { + indexerFixtures = getIndexerFixtures(await getTestAccounts()) + }) + + describe('GRT balances', () => { + it(`indexer balances should match airdropped amount minus staked`, async function () { + for (const indexer of indexerFixtures) { + const address = indexer.signer.address + const balance = await GraphToken.balanceOf(address) + expect(balance).eq(indexer.grtBalance.sub(indexer.stake)) + } + }) + }) + + describe('Staking', () => { + it(`indexers should have staked tokens`, async function () { + for (const indexer of indexerFixtures) { + const address = indexer.signer.address + const tokensStaked = (await Staking.stakes(address)).tokensStaked + expect(tokensStaked).eq(indexer.stake) + } + }) + }) + + describe('Allocations', () => { + it(`allocatons should be open`, async function () { + const allocations = indexerFixtures.map((i) => i.allocations).flat() + for (const allocation of allocations) { + const state = await Staking.getAllocationState(allocation.signer.address) + expect(state).eq(AllocationState.Active) + } + }) + }) +}) diff --git a/e2e/scenarios/open-allocations.ts b/e2e/scenarios/open-allocations.ts new file mode 100644 index 000000000..46ca4360e --- /dev/null +++ b/e2e/scenarios/open-allocations.ts @@ -0,0 +1,58 @@ +// ### Scenario description ### +// Common protocol actions > Set up indexers: stake and open allocations +// This scenario will open several allocations. See fixtures for details. +// Run with: +// npx hardhat e2e:scenario open-allocations --network --graph-config config/graph..yml + +import hre from 'hardhat' +import { allocateFrom, stake } from './lib/staking' +import { fundAccountsETH, fundAccountsGRT } from './lib/accounts' +import { getIndexerFixtures } from './fixtures/indexers' +import { getGraphOptsFromArgv } from './lib/helpers' + +async function main() { + const graphOpts = getGraphOptsFromArgv() + const graph = hre.graph(graphOpts) + const indexerFixtures = getIndexerFixtures(await graph.getTestAccounts()) + + const deployer = await graph.getDeployer() + const indexers = indexerFixtures.map((i) => i.signer.address) + const indexerETHBalances = indexerFixtures.map((i) => i.ethBalance) + const indexerGRTBalances = indexerFixtures.map((i) => i.grtBalance) + + // == Fund participants + console.log('\n== Fund indexers') + await fundAccountsETH(deployer, indexers, indexerETHBalances) + await fundAccountsGRT(deployer, indexers, indexerGRTBalances, graph.contracts.GraphToken) + + // == Stake + console.log('\n== Staking tokens') + + for (const indexer of indexerFixtures) { + await stake(graph.contracts, indexer.signer, indexer.stake) + } + + // == Open allocations + console.log('\n== Open allocations') + + for (const indexer of indexerFixtures) { + for (const allocation of indexer.allocations) { + await allocateFrom( + graph.contracts, + indexer.signer, + allocation.signer, + allocation.subgraphDeploymentId, + allocation.amount, + ) + } + } +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exitCode = 1 + }) diff --git a/e2e/scenarios/scenario1.test.ts b/e2e/scenarios/scenario1.test.ts deleted file mode 100644 index ecbdc1ee9..000000000 --- a/e2e/scenarios/scenario1.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { expect } from 'chai' -import { solidityKeccak256 } from 'ethers/lib/utils' -import hre from 'hardhat' -import { fixture } from './scenario1' - -describe('Scenario 1', () => { - const { - contracts: { GraphToken, Staking, GNS }, - getTestAccounts, - } = hre.graph() - - let indexer1: SignerWithAddress - let indexer2: SignerWithAddress - let curator1: SignerWithAddress - let curator2: SignerWithAddress - let curator3: SignerWithAddress - let subgraphOwner: SignerWithAddress - - let indexers: SignerWithAddress[] = [] - let curators: SignerWithAddress[] = [] - - before(async () => { - ;[indexer1, indexer2, subgraphOwner, curator1, curator2, curator3] = await getTestAccounts() - indexers = [indexer1, indexer2] - curators = [curator1, curator2, curator3] - }) - - describe('GRT balances', () => { - it('indexer1 should match airdropped amount minus staked', async function () { - const balance = await GraphToken.balanceOf(indexer1.address) - expect(balance).eq(fixture.grtAmount.sub(fixture.indexer1.stake)) - }) - - it('indexer2 should match airdropped amount minus staked', async function () { - const balance = await GraphToken.balanceOf(indexer2.address) - expect(balance).eq(fixture.grtAmount.sub(fixture.indexer2.stake)) - }) - - it('curator should match airdropped amount', async function () { - for (const account of curators) { - const balance = await GraphToken.balanceOf(account.address) - expect(balance).eq(fixture.grtAmount) - } - }) - }) - - describe('Staking', () => { - it('indexer1 should have tokens staked', async function () { - const tokensStaked = (await Staking.stakes(indexer1.address)).tokensStaked - expect(tokensStaked).eq(fixture.indexer1.stake) - }) - it('indexer2 should have tokens staked', async function () { - const tokensStaked = (await Staking.stakes(indexer2.address)).tokensStaked - expect(tokensStaked).eq(fixture.indexer2.stake) - }) - }) - - // describe('Subgraphs', () => { - // for (const subgraphDeploymentId of fixture.subgraphs) { - // it(`${subgraphDeploymentId} is published`, async function () { - // const seqID = await GNS.nextAccountSeqID(subgraphOwner.address) - // const subgraphId = solidityKeccak256(['address', 'uint256'], [subgraphOwner.address, seqID]) - - // await GNS.subgraphs(subgraphDeploymentId) - - // const isPublished = await GNS.isPublished(subgraphId) - // expect(isPublished).eq(true) - // }) - // } - // }) -}) diff --git a/e2e/scenarios/scenario1.ts b/e2e/scenarios/scenario1.ts deleted file mode 100644 index 61482fd25..000000000 --- a/e2e/scenarios/scenario1.ts +++ /dev/null @@ -1,75 +0,0 @@ -// We require the Hardhat Runtime Environment explicitly here. This is optional -// but useful for running the script in a standalone fashion through `node