diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 000000000..79ba35ddf --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,5 @@ +{ + "require": "ts-node/register/files", + "ignore": ["test/fixture-projects/**/*"], + "timeout": 6000 +} diff --git a/e2e/deployment/config/allocationExchange.test.ts b/e2e/deployment/config/allocationExchange.test.ts index ee023ce58..66e43074b 100644 --- a/e2e/deployment/config/allocationExchange.test.ts +++ b/e2e/deployment/config/allocationExchange.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai' import hre from 'hardhat' -import { NamedAccounts } from '../../../tasks/type-extensions' +import { NamedAccounts } from '../../../gre/type-extensions' describe('AllocationExchange configuration', () => { const { diff --git a/e2e/deployment/config/controller.test.ts b/e2e/deployment/config/controller.test.ts index 00caf2130..9bde5d85e 100644 --- a/e2e/deployment/config/controller.test.ts +++ b/e2e/deployment/config/controller.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai' import hre, { ethers } from 'hardhat' -import { NamedAccounts } from '../../../tasks/type-extensions' +import { NamedAccounts } from '../../../gre/type-extensions' describe('Controller configuration', () => { const { contracts, getNamedAccounts } = hre.graph() diff --git a/e2e/deployment/config/graphProxyAdmin.test.ts b/e2e/deployment/config/graphProxyAdmin.test.ts index 0d43984de..5c57b731f 100644 --- a/e2e/deployment/config/graphProxyAdmin.test.ts +++ b/e2e/deployment/config/graphProxyAdmin.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai' import hre from 'hardhat' -import { NamedAccounts } from '../../../tasks/type-extensions' +import { NamedAccounts } from '../../../gre/type-extensions' describe('GraphProxyAdmin configuration', () => { const { diff --git a/e2e/deployment/config/graphToken.test.ts b/e2e/deployment/config/graphToken.test.ts index b0e9a53c8..cc5e60618 100644 --- a/e2e/deployment/config/graphToken.test.ts +++ b/e2e/deployment/config/graphToken.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai' import hre from 'hardhat' -import { NamedAccounts } from '../../../tasks/type-extensions' +import { NamedAccounts } from '../../../gre/type-extensions' describe('GraphToken configuration', () => { const { diff --git a/e2e/deployment/config/rewardsManager.test.ts b/e2e/deployment/config/rewardsManager.test.ts index a5dff8a2e..475abd9f4 100644 --- a/e2e/deployment/config/rewardsManager.test.ts +++ b/e2e/deployment/config/rewardsManager.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai' import hre from 'hardhat' -import { NamedAccounts } from '../../../tasks/type-extensions' +import { NamedAccounts } from '../../../gre/type-extensions' describe('RewardsManager configuration', () => { const { diff --git a/e2e/deployment/config/subgraphNFT.test.ts b/e2e/deployment/config/subgraphNFT.test.ts index 2220a1e1b..d0bd2e10d 100644 --- a/e2e/deployment/config/subgraphNFT.test.ts +++ b/e2e/deployment/config/subgraphNFT.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai' import hre from 'hardhat' -import { NamedAccounts } from '../../../tasks/type-extensions' +import { NamedAccounts } from '../../../gre/type-extensions' describe('SubgraphNFT configuration', () => { const { diff --git a/gre/README.md b/gre/README.md new file mode 100644 index 000000000..c5e6369ce --- /dev/null +++ b/gre/README.md @@ -0,0 +1,223 @@ +# Graph Runtime Environment (GRE) + +GRE is a hardhat plugin that extends hardhat's runtime environment to inject additional functionality related to the usage of the Graph Protocol. + +### Features + +- Provides a simple interface to interact with protocol contracts +- Exposes protocol configuration via graph config file and address book +- Provides account management methods for convenience +- Multichain! Supports both L1 and L2 layers of the protocol simultaneously + +### Usage + +#### Example +Import GRE using `import './gre/gre'` on your hardhat config file and then: + +```js +// Use L2 governor account to set the L1 token address on the L2 gateway +const { l1, l2 } = hre.graph() + +const { GraphToken } = l1.contracts + +const { L2GraphTokenGateway } = l2.contracts +const { governor } = await l2.getNamedAccounts() + +const tx = L2GraphTokenGateway.connect(governor).setL1TokenAddress(GraphToken.address) +``` + +#### Network selection + +GRE supports both the L1 and L2 networks of the Graph Protocol by default. It will use hardhat's network defined via `--network` as the "main" network and then automatically detect which is the appropriate counterpart network in L1 or L2. + +Example: + +```bash +# L1: goerli and L2: arbitrum-goerli +hh console --network goerli + +# L1: mainnet and L2: arbitrum-one +hh console --network arbitrum-one + +# L1: mainnet and L2: arbitrum-one > same as previous +hh console --network mainnet +``` + +#### Configuration + +To use GRE you'll need to configure the target networks. That is done via either hardhat's config file using the `networks` [config field](https://hardhat.org/hardhat-runner/docs/config#json-rpc-based-networks) or by passing the appropriate arguments to `hre.graph()` initializer. + +__Note__: The "main" network, defined by hardhat's `--network` flag _MUST_ be properly configured for GRE to initialize successfully. It's not necessary to configure the counterpart network if you don't plan on using it. + +**Hardhat: Network config** +```js +networks: { + goerli: { + chainId: 5, + url: `https://goerli.infura.io/v3/123456` + accounts: { + mnemonic: 'test test test test test test test test test test test test', + }, + graphConfig: 'config/graph.goerli.yml' + }, +} +``` + +Fields: +- **(_REQUIRED_) chainId**: the chainId of the network. This field is not required by hardhat but it's used by GRE to simplify the API. +- **(_REQUIRED_) url**: the RPC endpoint of the network. +- **(_OPTIONAL_) accounts**: the accounts to use on the network. These will be used by the account management functions on GRE. +- **(_OPTIONAL_) graphConfig**: the path to the graph config file for the network. + +**Hardhat: Graph config** + +Additionally, the plugin adds a new config field to hardhat's config file: `graphConfig`. This can be used used to define defaults for the graph config file. + + +```js +... +networks: { +... +}, +graph: { + addressBook: 'addresses.json' + l1GraphConfig: 'config/graph.mainnet.yml' + l2GraphConfig: 'config/graph.arbitrum-one.yml' +} +... +``` + +Fields: +- **(_OPTIONAL_) addressBook**: the path to the address book. +- **(_REQUIRED_) l1GraphConfig**: default path to the graph config file for L1 networks. This will be used if the `graphConfig` field is not defined on the network config. +- **(_REQUIRED_) l2GraphConfig**: default path to the graph config file for L2 networks. This will be used if the `graphConfig` field is not defined on the network config. + +**Options: Graph initializer** + +The GRE initializer also allows you to set the address book and the graph config files like so: +```js +const graph = hre.graph({ + addressBook: 'addresses.json', + l1GraphConfig: 'config/graph.mainnet.yml' + l2GraphConfig: 'config/graph.arbitrum-one.yml' +}) + +// Here graphConfig will apply only to the "main" network given by --network +const graph = hre.graph({ + addressBook: 'addresses.json', + graphConfig: 'config/graph.mainnet.yml' +}) +``` + +**Config priority** + +The path to the graph config and the address book can be set in multiple ways. The plugin will use the following order to determine the path to the graph config file: + +1) `hre.graph({ ... })` init parameters `l1GraphConfigPath` and `l2GraphConfigPath` +2) `hre.graph({ ...})` init parameter graphConfigPath (but only for the "main" network) +3) `networks..graphConfig` network config parameter `graphConfig` in hardhat config file +4) `graph.lGraphConfig` graph config parameters `l1GraphConfig` and `l2GraphConfig` in hardhat config file + +The priority for the address book is: +1) `hre.graph({ ... })` init parameter `addressBook` +2) `graph.addressBook` graph config parameter `addressBook` in hardhat config file + +### API + +GRE exposes functionality via a simple API: + +```js +const graph = hre.graph() + +// To access the L1 object +graph.l1 + +// To access the L2 object +graph.l2 +``` + +The interface for both `l1` and `l2` objects looks like this: + +```ts +export interface GraphNetworkEnvironment { + chainId: number + contracts: NetworkContracts + graphConfig: any + addressBook: AddressBook + getNamedAccounts: () => Promise + getTestAccounts: () => Promise + getDeployer: () => Promise +} +``` + +**ChainId** + +The chainId of the network. + +**Contracts** + +Returns an object with all the contracts available in the network. Connects using a provider created with the URL specified in hardhat's network configuration (it doesn't use the usual hardhat `hre.ethers.provider`). + +```js +> const graph = hre.graph() + +// Print curation default reserve ratio on L1 +> await g.l1.contracts.Curation.defaultReserveRatio() +500000 +``` + +**Graph Config** + +Returns an object that grants raw access to the graph config file for the protocol. The graph config file is a YAML file that contains all the parameters with which the protocol was deployed. + +> TODO: add better APIs to interact with the graph config file. + +**Address Book** + +Returns an object that allows interacting with the address book. + +```js +> const graph = hre.graph() +> graph.l1.addressBook.getEntry('Curation') +{ + address: '0xE59B4820dDE28D2c235Bd9A73aA4e8716Cb93E9B', + initArgs: [ + '0x48eD7AfbaB432d1Fc6Ea84EEC70E745d9DAcaF3B', + '0x2DFDC3e11E035dD96A4aB30Ef67fab4Fb6EC01f2', + '0x8bEd0a89F18a801Da9dEA994D475DEa74f75A059', + '500000', + '10000', + '1000000000000000000' + ], + creationCodeHash: '0x25a7b6cafcebb062169bc25fca9bcce8f23bd7411235859229ae3cc99b9a7d58', + runtimeCodeHash: '0xaf2d63813a0e5059f63ec46e1b280eb9d129d5ad548f0cdd1649d9798fde10b6', + txHash: '0xf1b1f0f28b80068bcc9fd6ef475be6324a8b23cbdb792f7344f05ce00aa997d7', + proxy: true, + implementation: { + address: '0xAeaA2B058539750b740E858f97159E6856948670', + creationCodeHash: '0x022576ab4b739ee17dab126ea7e5a6814bda724aa0e4c6735a051b38a76bd597', + runtimeCodeHash: '0xc7b1f9bef01ef92779aab0ae9be86376c47584118c508f5b4e612a694a4aab93', + txHash: '0x400bfb7b6c384363b859a66930590507ddca08ebedf64b20c4b5f6bc8e76e125' + } +} +``` + +**Account management: getNamedAccounts** +Returns an object with all the named accounts available in the network. Named accounts are accounts that have special roles in the protocol, they are defined in the graph config file. + +```js +> const graph = hre.graph() +> const namedAccounts = await g.l1.getNamedAccounts() +> namedAccounts.governor.address +'0xf1135bFF22512FF2A585b8d4489426CE660f204c' +``` + +The accounts are initialized from the graph config file but if the correct mnemonic or private key is provided via hardhat network configuration then they will be fully capable of signing transactions. + +**Account management: getTestAccounts** +Returns an object with accounts which can be used for testing/interacting with the protocol. These are obtained from hardhat's network configuration using the provided mnemonic or private key. + +**Account management: getDeployer** +Returns an object with the would-be deployer account. The deployer is by convention the first (index 0) account derived from the mnemonic or private key provided via hardhat network configuration. + +It's important to note that the deployer is not a named account as it's derived from the provided mnemonic so it won't necessarily match the actual deployer for a given deployment. It's the account that would be used to deploy the protocol with the current configuration. It's not possible at the moment to recover the actual deployer account from a deployed protocol. \ No newline at end of file diff --git a/gre/accounts.ts b/gre/accounts.ts new file mode 100644 index 000000000..339606c46 --- /dev/null +++ b/gre/accounts.ts @@ -0,0 +1,55 @@ +import { EthersProviderWrapper } from '@nomiclabs/hardhat-ethers/internal/ethers-provider-wrapper' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { getItemValue, readConfig } from '../cli/config' +import { AccountNames, NamedAccounts } from './type-extensions' + +const namedAccountList: AccountNames[] = [ + 'arbitrator', + 'governor', + 'authority', + 'availabilityOracle', + 'pauseGuardian', + 'allocationExchangeOwner', +] + +export async function getNamedAccounts( + provider: EthersProviderWrapper, + graphConfigPath: string, +): Promise { + const namedAccounts = namedAccountList.reduce(async (accountsPromise, name) => { + const accounts = await accountsPromise + const address = getItemValue(readConfig(graphConfigPath, true), `general/${name}`) + accounts[name] = await SignerWithAddress.create(provider.getSigner(address)) + return accounts + }, Promise.resolve({} as NamedAccounts)) + + return namedAccounts +} + +export async function getDeployer(provider: EthersProviderWrapper): Promise { + const signer = provider.getSigner(0) + return SignerWithAddress.create(signer) +} + +export async function getTestAccounts( + provider: EthersProviderWrapper, + graphConfigPath: string, +): Promise { + // Get list of privileged accounts we don't want as test accounts + const namedAccounts = await getNamedAccounts(provider, graphConfigPath) + const blacklist = namedAccountList.map((a) => { + const account = namedAccounts[a] + return account.address + }) + blacklist.push((await getDeployer(provider)).address) + + // Get signers and filter out blacklisted accounts + const accounts = await provider.listAccounts() + const signers = await Promise.all( + accounts.map(async (account) => await SignerWithAddress.create(provider.getSigner(account))), + ) + + return signers.filter((s) => { + return !blacklist.includes(s.address) + }) +} diff --git a/gre/config.ts b/gre/config.ts new file mode 100644 index 000000000..68b624b82 --- /dev/null +++ b/gre/config.ts @@ -0,0 +1,223 @@ +import fs from 'fs' +import path from 'path' + +import { NetworkConfig, NetworksConfig } from 'hardhat/types/config' +import { HardhatRuntimeEnvironment } from 'hardhat/types/runtime' +import { HttpNetworkConfig } from 'hardhat/types/config' + +import { GraphRuntimeEnvironmentOptions } from './type-extensions' +import { GREPluginError } from './helpers/error' +import GraphNetwork from './helpers/network' + +import { createProvider } from 'hardhat/internal/core/providers/construction' +import { EthersProviderWrapper } from '@nomiclabs/hardhat-ethers/internal/ethers-provider-wrapper' + +import { logDebug } from './logger' + +interface GREChains { + l1ChainId: number + l2ChainId: number + isHHL1: boolean + isHHL2: boolean +} + +interface GREProviders { + l1Provider: EthersProviderWrapper | undefined + l2Provider: EthersProviderWrapper | undefined +} + +interface GREGraphConfigs { + l1GraphConfigPath: string | undefined + l2GraphConfigPath: string | undefined +} + +export function getAddressBookPath( + hre: HardhatRuntimeEnvironment, + opts: GraphRuntimeEnvironmentOptions, +): string { + logDebug('== Getting address book path') + logDebug(`Graph base dir: ${hre.config.paths.graph}`) + logDebug(`1) opts.addressBookPath: ${opts.addressBook}`) + logDebug(`2) hre.config.graph.addressBook: ${hre.config.graph?.addressBook}`) + + let addressBookPath = opts.addressBook ?? hre.config.graph?.addressBook + + if (addressBookPath === undefined) { + throw new GREPluginError('Must set a an addressBook path!') + } + + addressBookPath = normalizePath(addressBookPath, hre.config.paths.graph) + + if (!fs.existsSync(addressBookPath)) { + throw new GREPluginError(`Address book not found: ${addressBookPath}`) + } + + logDebug(`Address book path found: ${addressBookPath}`) + return addressBookPath +} + +export function getChains(mainChainId: number | undefined): GREChains { + logDebug('== Getting chain ids') + logDebug(`Hardhat chain id: ${mainChainId}`) + + if (!GraphNetwork.isSupported(mainChainId)) { + throw new GREPluginError(`Chain ${mainChainId} is not supported!`) + } + + // If mainChainId is supported there is a supported counterpart chainId so both chains are not undefined + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + mainChainId = mainChainId! + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const secondaryChainId = GraphNetwork.counterpart(mainChainId)! + logDebug(`Secondary chain id: ${secondaryChainId}`) + + const isHHL1 = GraphNetwork.isL1(mainChainId) + const isHHL2 = GraphNetwork.isL2(mainChainId) + const l1ChainId = isHHL1 ? mainChainId : secondaryChainId + const l2ChainId = isHHL2 ? mainChainId : secondaryChainId + + logDebug(`L1 chain id: ${l1ChainId} - Is HHL1: ${isHHL1}`) + logDebug(`L2 chain id: ${l2ChainId} - Is HHL2: ${isHHL2}`) + + return { + l1ChainId, + l2ChainId, + isHHL1, + isHHL2, + } +} + +export function getProviders( + hre: HardhatRuntimeEnvironment, + l1ChainId: number, + l2ChainId: number, + isHHL1: boolean, +): GREProviders { + logDebug('== Getting providers') + + const getProvider = ( + networks: NetworksConfig, + chainId: number, + isMainProvider: boolean, + chainLabel: string, + ): EthersProviderWrapper | undefined => { + const network = getNetworkConfig(networks, chainId) as HttpNetworkConfig + const networkName = getNetworkName(networks, chainId) + + // Ensure at least main provider is configured + if (isMainProvider && network === undefined) { + throw new GREPluginError(`Must set a provider url for chain: ${chainId}!`) + } + + logDebug(`Provider url for ${chainLabel}: ${network?.url}`) + + if (network === undefined || networkName === undefined) { + return undefined + } + + // Build provider as EthersProviderWrapper instead of JsonRpcProvider + // This allows us to use hardhat's account management methods for free + const ethereumProvider = createProvider(networkName, network) + const ethersProviderWrapper = new EthersProviderWrapper(ethereumProvider) + return ethersProviderWrapper + } + + const l1Provider = getProvider(hre.config.networks, l1ChainId, isHHL1, 'L1') + const l2Provider = getProvider(hre.config.networks, l2ChainId, !isHHL1, 'L2') + + return { + l1Provider, + l2Provider, + } +} + +export function getGraphConfigPaths( + hre: HardhatRuntimeEnvironment, + opts: GraphRuntimeEnvironmentOptions, + l1ChainId: number, + l2ChainId: number, + isHHL1: boolean, +): GREGraphConfigs { + logDebug('== Getting graph config paths') + logDebug(`Graph base dir: ${hre.config.paths.graph}`) + + const l1Network = getNetworkConfig(hre.config.networks, l1ChainId) + const l2Network = getNetworkConfig(hre.config.networks, l2ChainId) + + // Priority is as follows: + // - hre.graph() init parameter l1GraphConfigPath/l2GraphConfigPath + // - hre.graph() init parameter graphConfigPath (only for layer corresponding to hh network) + // - hh network config + // - hh graph config (layer specific: l1GraphConfig, l2GraphConfig) + let l1GraphConfigPath = + opts.l1GraphConfig ?? + (isHHL1 ? opts.graphConfig : undefined) ?? + l1Network?.graphConfig ?? + hre.config.graph.l1GraphConfig + + logDebug(`> L1 graph config`) + logDebug(`1) opts.l1GraphConfig: ${opts.l1GraphConfig}`) + logDebug(`2) opts.graphConfig: ${isHHL1 ? opts.graphConfig : undefined}`) + logDebug(`3) l1Network.graphConfig: ${l1Network?.graphConfig}`) + logDebug(`4) hre.config.graph.l1GraphConfig: ${hre.config.graph.l1GraphConfig}`) + + if (isHHL1 && l1GraphConfigPath === undefined) { + throw new GREPluginError('Must specify a graph config file for L1!') + } + + if (l1GraphConfigPath !== undefined) { + l1GraphConfigPath = normalizePath(l1GraphConfigPath, hre.config.paths.graph) + } + + let l2GraphConfigPath = + opts.l2GraphConfig ?? + (!isHHL1 ? opts.graphConfig : undefined) ?? + l2Network?.graphConfig ?? + hre.config.graph.l2GraphConfig + + logDebug(`> L2 graph config`) + logDebug(`1) opts.l2GraphConfig: ${opts.l2GraphConfig}`) + logDebug(`2) opts.graphConfig: ${!isHHL1 ? opts.graphConfig : undefined}`) + logDebug(`3) l2Network.graphConfig: ${l2Network?.graphConfig}`) + logDebug(`4) hre.config.graph.l2GraphConfig: ${hre.config.graph.l2GraphConfig}`) + + if (!isHHL1 && l2GraphConfigPath === undefined) { + throw new GREPluginError('Must specify a graph config file for L2!') + } + + if (l2GraphConfigPath !== undefined) { + l2GraphConfigPath = normalizePath(l2GraphConfigPath, hre.config.paths.graph) + } + + for (const configPath of [l1GraphConfigPath, l2GraphConfigPath]) { + if (configPath !== undefined && !fs.existsSync(configPath)) { + throw new GREPluginError(`Graph config file not found: ${configPath}`) + } + } + + logDebug(`L1 graph config path: ${l1GraphConfigPath}`) + logDebug(`L2 graph config path: ${l2GraphConfigPath}`) + + return { + l1GraphConfigPath: l1GraphConfigPath, + l2GraphConfigPath: l2GraphConfigPath, + } +} + +function getNetworkConfig(networks: NetworksConfig, chainId: number): NetworkConfig | undefined { + return Object.keys(networks) + .map((n) => networks[n]) + .find((n) => n.chainId === chainId) +} + +function getNetworkName(networks: NetworksConfig, chainId: number): string | undefined { + return Object.keys(networks).find((n) => networks[n].chainId === chainId) +} + +function normalizePath(_path: string, graphPath: string) { + if (!path.isAbsolute(_path)) { + _path = path.join(graphPath, _path) + } + return _path +} diff --git a/gre/gre.ts b/gre/gre.ts new file mode 100644 index 000000000..221557ba4 --- /dev/null +++ b/gre/gre.ts @@ -0,0 +1,117 @@ +import { HardhatConfig, HardhatRuntimeEnvironment, HardhatUserConfig } from 'hardhat/types' +import { extendConfig, extendEnvironment } from 'hardhat/config' +import { lazyFunction, lazyObject } from 'hardhat/plugins' + +import { getAddressBook } from '../cli/address-book' +import { loadContracts } from '../cli/contracts' +import { readConfig } from '../cli/config' +import { + GraphNetworkEnvironment, + GraphRuntimeEnvironment, + GraphRuntimeEnvironmentOptions, +} from './type-extensions' +import { getChains, getProviders, getAddressBookPath, getGraphConfigPaths } from './config' +import { getDeployer, getNamedAccounts, getTestAccounts } from './accounts' +import { logDebug, logWarn } from './logger' +import path from 'path' +import { EthersProviderWrapper } from '@nomiclabs/hardhat-ethers/internal/ethers-provider-wrapper' + +// Graph Runtime Environment (GRE) extensions for the HRE + +extendConfig((config: HardhatConfig, userConfig: Readonly) => { + // Source for the path convention: + // https://github.com/NomicFoundation/hardhat-ts-plugin-boilerplate/blob/d450d89f4b6ed5d26a8ae32b136b9c55d2aadab5/src/index.ts + const userPath = userConfig.paths?.graph + + let newPath: string + if (userPath === undefined) { + newPath = config.paths.root + } else { + if (path.isAbsolute(userPath)) { + newPath = userPath + } else { + newPath = path.normalize(path.join(config.paths.root, userPath)) + } + } + + config.paths.graph = newPath +}) + +extendEnvironment((hre: HardhatRuntimeEnvironment) => { + hre.graph = (opts: GraphRuntimeEnvironmentOptions = {}) => { + const { l1ChainId, l2ChainId, isHHL1 } = getChains(hre.network.config.chainId) + const { l1Provider, l2Provider } = getProviders(hre, l1ChainId, l2ChainId, isHHL1) + const addressBookPath = getAddressBookPath(hre, opts) + const { l1GraphConfigPath, l2GraphConfigPath } = getGraphConfigPaths( + hre, + opts, + l1ChainId, + l2ChainId, + isHHL1, + ) + + const l1Graph: GraphNetworkEnvironment | null = buildGraphNetworkEnvironment( + l1ChainId, + l1Provider, + l1GraphConfigPath, + addressBookPath, + isHHL1, + ) + + const l2Graph: GraphNetworkEnvironment | null = buildGraphNetworkEnvironment( + l2ChainId, + l2Provider, + l2GraphConfigPath, + addressBookPath, + isHHL1, + ) + + const gre: GraphRuntimeEnvironment = { + ...(isHHL1 ? (l1Graph as GraphNetworkEnvironment) : (l2Graph as GraphNetworkEnvironment)), + l1: l1Graph, + l2: l2Graph, + } + + logDebug('GRE initialized successfully!') + logDebug(`Main network: L${isHHL1 ? '1' : '2'}`) + logDebug(`Secondary network: ${gre.l2 !== null ? (isHHL1 ? 'L2' : 'L1') : 'not initialized'}`) + return gre + } +}) + +function buildGraphNetworkEnvironment( + chainId: number, + provider: EthersProviderWrapper | undefined, + graphConfigPath: string | undefined, + addressBookPath: string, + isHHL1: boolean, +): GraphNetworkEnvironment | null { + if (graphConfigPath === undefined) { + logWarn( + `No graph config file provided for chain: ${chainId}. ${ + isHHL1 ? 'L2' : 'L1' + } will not be initialized.`, + ) + return null + } + + if (provider === undefined) { + logWarn( + `No provider URL found for: ${chainId}. ${isHHL1 ? 'L2' : 'L1'} will not be initialized.`, + ) + return null + } + + return { + chainId: chainId, + provider: provider, + addressBook: lazyObject(() => getAddressBook(addressBookPath, chainId.toString())), + graphConfig: lazyObject(() => readConfig(graphConfigPath, true)), + contracts: lazyObject(() => + loadContracts(getAddressBook(addressBookPath, chainId.toString()), provider), + ), + getDeployer: lazyFunction(() => () => getDeployer(provider)), + getNamedAccounts: lazyFunction(() => () => getNamedAccounts(provider, graphConfigPath)), + getTestAccounts: lazyFunction(() => () => getTestAccounts(provider, graphConfigPath)), + } +} diff --git a/gre/helpers/error.ts b/gre/helpers/error.ts new file mode 100644 index 000000000..f9aafa748 --- /dev/null +++ b/gre/helpers/error.ts @@ -0,0 +1,9 @@ +import { HardhatPluginError } from 'hardhat/plugins' +import { logError } from '../logger' + +export class GREPluginError extends HardhatPluginError { + constructor(message: string) { + super('GraphRuntimeEnvironment', message) + logError(message) + } +} diff --git a/gre/helpers/network.ts b/gre/helpers/network.ts new file mode 100644 index 000000000..5fc2829ff --- /dev/null +++ b/gre/helpers/network.ts @@ -0,0 +1,45 @@ +class MapWithGetKey extends Map { + getKey(value: K): K | undefined { + for (const [k, v] of this.entries()) { + if (v === value) { + return k + } + } + return + } +} + +const chainMap = new MapWithGetKey([ + [1, 42161], // Ethereum Mainnet - Arbitrum One + [4, 421611], // Ethereum Rinkeby - Arbitrum Rinkeby + [5, 421613], // Ethereum Goerli - Arbitrum Goerli + [1337, 412346], // Localhost - Arbitrum Localhost +]) + +export const l1Chains = Array.from(chainMap.keys()) +export const l2Chains = Array.from(chainMap.values()) +export const chains = [...l1Chains, ...l2Chains] + +export const isL1 = (chainId: number): boolean => l1Chains.includes(chainId) +export const isL2 = (chainId: number): boolean => l2Chains.includes(chainId) +export const isSupported = (chainId: number | undefined): boolean => + chainId !== undefined && chains.includes(chainId) + +export const l1ToL2 = (chainId: number): number | undefined => chainMap.get(chainId) +export const l2ToL1 = (chainId: number): number | undefined => chainMap.getKey(chainId) +export const counterpart = (chainId: number): number | undefined => { + if (!isSupported(chainId)) return + return isL1(chainId) ? l1ToL2(chainId) : l2ToL1(chainId) +} + +export default { + l1Chains, + l2Chains, + chains, + isL1, + isL2, + isSupported, + l1ToL2, + l2ToL1, + counterpart, +} diff --git a/gre/logger.ts b/gre/logger.ts new file mode 100644 index 000000000..a768d6b04 --- /dev/null +++ b/gre/logger.ts @@ -0,0 +1,7 @@ +import debug from 'debug' + +const LOG_BASE = 'hardhat:gre' + +export const logDebug = debug(`${LOG_BASE}:debug`) +export const logWarn = debug(`${LOG_BASE}:warn`) +export const logError = debug(`${LOG_BASE}:error`) diff --git a/gre/test/config.test.ts b/gre/test/config.test.ts new file mode 100644 index 000000000..d206220e6 --- /dev/null +++ b/gre/test/config.test.ts @@ -0,0 +1,224 @@ +import { expect } from 'chai' +import { useEnvironment } from './helpers' + +import { getAddressBookPath, getChains, getGraphConfigPaths, getProviders } from '../config' +import path from 'path' + +describe('GRE init functions', function () { + describe('getAddressBookPath with graph-config project', function () { + useEnvironment('graph-config') + + it('should use opts parameter if available', function () { + const addressBook = getAddressBookPath(this.hre, { + addressBook: 'addresses-opts.json', + }) + expect(path.basename(addressBook)).to.equal('addresses-opts.json') + }) + + it('should use HH graph config if opts parameter not available ', function () { + const addressBook = getAddressBookPath(this.hre, {}) + expect(path.basename(addressBook)).to.equal('addresses-hre.json') + }) + }) + + describe('getAddressBookPath with default-config project', function () { + useEnvironment('default-config') + + it('should throw if no address book is specified', function () { + expect(() => getAddressBookPath(this.hre, {})).to.throw('Must set a an addressBook path!') + }) + }) + + describe('getAddressBookPath with graph-config-bad project', function () { + useEnvironment('graph-config-bad') + + it("should throw if address book doesn't exist", function () { + expect(() => getAddressBookPath(this.hre, {})).to.throw(/Address book not found: /) + }) + }) + + describe('getChains', function () { + it('should return L1 and L2 chain ids for a supported L1 chain', function () { + const { l1ChainId, l2ChainId, isHHL1, isHHL2 } = getChains(5) // Goerli + + expect(l1ChainId).to.equal(5) + expect(l2ChainId).to.equal(421613) + expect(isHHL1).to.equal(true) + expect(isHHL2).to.equal(false) + }) + it('should return L1 and L2 chain ids for a supported L2 chain', function () { + const { l1ChainId, l2ChainId, isHHL1, isHHL2 } = getChains(42161) // Arbitrum One + + expect(l1ChainId).to.equal(1) + expect(l2ChainId).to.equal(42161) + expect(isHHL1).to.equal(false) + expect(isHHL2).to.equal(true) + }) + it('should throw if provided chain is not supported', function () { + const badChainId = 999 + expect(() => getChains(badChainId)).to.throw(`Chain ${badChainId} is not supported!`) + }) + }) + + describe('getProviders with graph-config project', function () { + useEnvironment('graph-config') + + it('should return L1 and L2 providers for supported networks (HH L1)', function () { + const { l1Provider, l2Provider } = getProviders(this.hre, 5, 421613, true) + expect(l1Provider).to.be.an('object') + expect(l2Provider).to.be.an('object') + }) + + it('should return L1 and L2 providers for supported networks (HH L2)', function () { + const { l1Provider, l2Provider } = getProviders(this.hre, 5, 421613, false) + expect(l1Provider).to.be.an('object') + expect(l2Provider).to.be.an('object') + }) + + it('should return only L1 provider if L2 is not supported (HH L1)', function () { + const { l1Provider, l2Provider } = getProviders(this.hre, 5, 123456, true) + expect(l1Provider).to.be.an('object') + expect(l2Provider).to.be.undefined + }) + + it('should return only L2 provider if L1 is not supported (HH L2)', function () { + const { l1Provider, l2Provider } = getProviders(this.hre, 123456, 421613, false) + expect(l1Provider).to.be.undefined + expect(l2Provider).to.be.an('object') + }) + }) + + describe('getProviders with graph-config-bad project', function () { + useEnvironment('graph-config-bad') + + it('should throw if main network is not defined in hardhat config (HH L1)', function () { + expect(() => getProviders(this.hre, 4, 421611, true)).to.throw( + /Must set a provider url for chain: /, + ) + }) + + it('should throw if main network is not defined in hardhat config (HH L2)', function () { + expect(() => getProviders(this.hre, 5, 421613, false)).to.throw( + /Must set a provider url for chain: /, + ) + }) + }) + + describe('getGraphConfigPaths with graph-config-full project', function () { + useEnvironment('graph-config-full') + + it('should use opts parameters if available', function () { + const { l1GraphConfigPath, l2GraphConfigPath } = getGraphConfigPaths( + this.hre, + { l1GraphConfig: 'config/graph.opts.yml', l2GraphConfig: 'config/graph.arbitrum-opts.yml' }, + 5, + 421613, + true, + ) + expect(path.basename(l1GraphConfigPath)).to.equal('graph.opts.yml') + expect(path.basename(l2GraphConfigPath)).to.equal('graph.arbitrum-opts.yml') + }) + + it('should use opts graphConfig parameter only for main network if available (HH L1)', function () { + const { l1GraphConfigPath, l2GraphConfigPath } = getGraphConfigPaths( + this.hre, + { graphConfig: 'config/graph.opts.yml' }, + 4, + 421611, + true, + ) + expect(path.basename(l1GraphConfigPath)).to.equal('graph.opts.yml') + expect(path.basename(l2GraphConfigPath)).to.equal('graph.arbitrum-hre.yml') + }) + + it('should use opts graphConfig parameter only for main network if available (HH L2)', function () { + const { l1GraphConfigPath, l2GraphConfigPath } = getGraphConfigPaths( + this.hre, + { graphConfig: 'config/graph.arbitrum-opts.yml' }, + 4, + 421611, + false, + ) + expect(path.basename(l1GraphConfigPath)).to.equal('graph.hre.yml') + expect(path.basename(l2GraphConfigPath)).to.equal('graph.arbitrum-opts.yml') + }) + + it('should ignore graphConfig parameter if both config paths are provided (HH L1)', function () { + const { l1GraphConfigPath, l2GraphConfigPath } = getGraphConfigPaths( + this.hre, + { + graphConfig: 'config/graph.opts2.yml', + l1GraphConfig: 'config/graph.opts.yml', + l2GraphConfig: 'config/graph.arbitrum-opts.yml', + }, + 5, + 421613, + true, + ) + expect(path.basename(l1GraphConfigPath)).to.equal('graph.opts.yml') + expect(path.basename(l2GraphConfigPath)).to.equal('graph.arbitrum-opts.yml') + }) + + it('should ignore graphConfig parameter if both config paths are provided (HH L2)', function () { + const { l1GraphConfigPath, l2GraphConfigPath } = getGraphConfigPaths( + this.hre, + { + graphConfig: 'config/graph.opts2.yml', + l1GraphConfig: 'config/graph.opts.yml', + l2GraphConfig: 'config/graph.arbitrum-opts.yml', + }, + 5, + 421613, + false, + ) + expect(path.basename(l1GraphConfigPath)).to.equal('graph.opts.yml') + expect(path.basename(l2GraphConfigPath)).to.equal('graph.arbitrum-opts.yml') + }) + + it('should use network specific config if no opts given', function () { + const { l1GraphConfigPath, l2GraphConfigPath } = getGraphConfigPaths( + this.hre, + {}, + 1, + 42161, + false, + ) + expect(path.basename(l1GraphConfigPath)).to.equal('graph.mainnet.yml') + expect(path.basename(l2GraphConfigPath)).to.equal('graph.arbitrum-goerli.yml') + }) + + it('should use graph generic config if nothing else given', function () { + const { l1GraphConfigPath, l2GraphConfigPath } = getGraphConfigPaths( + this.hre, + {}, + 4, + 421611, + false, + ) + expect(path.basename(l1GraphConfigPath)).to.equal('graph.hre.yml') + expect(path.basename(l2GraphConfigPath)).to.equal('graph.arbitrum-hre.yml') + }) + }) + + describe('getGraphConfigPaths with graph-config-bad project', function () { + useEnvironment('graph-config-bad') + + it('should throw if no config file for main network (HH L1)', function () { + expect(() => getGraphConfigPaths(this.hre, {}, 5, 421611, true)).to.throw( + 'Must specify a graph config file for L1!', + ) + }) + + it('should throw if no config file for main network (HH L2)', function () { + expect(() => getGraphConfigPaths(this.hre, {}, 5, 421611, false)).to.throw( + 'Must specify a graph config file for L2!', + ) + }) + + it('should throw if config file does not exist', function () { + expect(() => getGraphConfigPaths(this.hre, {}, 1, 421611, true)).to.throw( + /Graph config file not found: /, + ) + }) + }) +}) diff --git a/gre/test/files/addresses-hre.json b/gre/test/files/addresses-hre.json new file mode 100644 index 000000000..e69de29bb diff --git a/gre/test/files/addresses-opts.json b/gre/test/files/addresses-opts.json new file mode 100644 index 000000000..e69de29bb diff --git a/gre/test/files/config/graph.arbitrum-goerli.yml b/gre/test/files/config/graph.arbitrum-goerli.yml new file mode 100644 index 000000000..e69de29bb diff --git a/gre/test/files/config/graph.arbitrum-hre.yml b/gre/test/files/config/graph.arbitrum-hre.yml new file mode 100644 index 000000000..e69de29bb diff --git a/gre/test/files/config/graph.arbitrum-opts.yml b/gre/test/files/config/graph.arbitrum-opts.yml new file mode 100644 index 000000000..e69de29bb diff --git a/gre/test/files/config/graph.goerli.yml b/gre/test/files/config/graph.goerli.yml new file mode 100644 index 000000000..e69de29bb diff --git a/gre/test/files/config/graph.hre.yml b/gre/test/files/config/graph.hre.yml new file mode 100644 index 000000000..e69de29bb diff --git a/gre/test/files/config/graph.mainnet.yml b/gre/test/files/config/graph.mainnet.yml new file mode 100644 index 000000000..e69de29bb diff --git a/gre/test/files/config/graph.opts.yml b/gre/test/files/config/graph.opts.yml new file mode 100644 index 000000000..e69de29bb diff --git a/gre/test/files/config/graph.opts2.yml b/gre/test/files/config/graph.opts2.yml new file mode 100644 index 000000000..e69de29bb diff --git a/gre/test/fixture-projects/default-config/hardhat.config.js b/gre/test/fixture-projects/default-config/hardhat.config.js new file mode 100644 index 000000000..4ba52ba2c --- /dev/null +++ b/gre/test/fixture-projects/default-config/hardhat.config.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/gre/test/fixture-projects/graph-config-bad/hardhat.config.ts b/gre/test/fixture-projects/graph-config-bad/hardhat.config.ts new file mode 100644 index 000000000..405d5d86c --- /dev/null +++ b/gre/test/fixture-projects/graph-config-bad/hardhat.config.ts @@ -0,0 +1,43 @@ +import '../../../gre' + +module.exports = { + paths: { + graph: '../../files', + }, + solidity: '0.8.9', + defaultNetwork: 'hardhat', + networks: { + hardhat: { + chainId: 1337, + }, + mainnet: { + chainId: 1, + graphConfig: 'config/graph.mainnet-does-not-exist.yml', + url: `https://mainnet.infura.io/v3/123456`, + }, + 'arbitrum-one': { + chainId: 42161, + url: 'https://arb1.arbitrum.io/rpc', + graphConfig: 'config/graph.arbitrum-does-not-exist.yml', + }, + goerli: { + chainId: 5, + url: `https://goerli.infura.io/v3/123456`, + }, + // 'arbitrum-goerli': { + // chainId: 421613, + // url: 'https://goerli-rollup.arbitrum.io/rpc', + // }, + // rinkeby: { + // chainId: 4, + // url: `https://goerli.infura.io/v3/123456`, + // }, + 'arbitrum-rinkeby': { + chainId: 421611, + url: `https://goerli.infura.io/v3/123456`, + }, + }, + graph: { + addressBook: 'addresses-does-not-exist.json', + }, +} diff --git a/gre/test/fixture-projects/graph-config-full/hardhat.config.ts b/gre/test/fixture-projects/graph-config-full/hardhat.config.ts new file mode 100644 index 000000000..c0d52dda1 --- /dev/null +++ b/gre/test/fixture-projects/graph-config-full/hardhat.config.ts @@ -0,0 +1,47 @@ +import '../../../gre' + +module.exports = { + paths: { + graph: '../../files', + }, + solidity: '0.8.9', + defaultNetwork: 'hardhat', + networks: { + hardhat: { + chainId: 1337, + }, + mainnet: { + chainId: 1, + graphConfig: 'config/graph.mainnet.yml', + url: `https://mainnet.infura.io/v3/123456`, + }, + 'arbitrum-one': { + chainId: 42161, + url: 'https://arb1.arbitrum.io/rpc', + graphConfig: 'config/graph.arbitrum-goerli.yml', + }, + goerli: { + chainId: 5, + url: `https://goerli.infura.io/v3/123456`, + graphConfig: 'config/graph.goerli.yml', + }, + 'arbitrum-goerli': { + chainId: 421613, + url: 'https://goerli-rollup.arbitrum.io/rpc', + graphConfig: 'config/graph.arbitrum-goerli.yml', + }, + rinkeby: { + chainId: 4, + url: `https://goerli.infura.io/v3/123456`, + }, + 'arbitrum-rinkeby': { + chainId: 421611, + url: `https://goerli.infura.io/v3/123456`, + }, + }, + graph: { + addressBook: 'addresses-hre.json', + l1GraphConfig: 'config/graph.hre.yml', + l2GraphConfig: 'config/graph.arbitrum-hre.yml', + }, +} diff --git a/gre/test/fixture-projects/graph-config/hardhat.config.ts b/gre/test/fixture-projects/graph-config/hardhat.config.ts new file mode 100644 index 000000000..5a4e5cdfc --- /dev/null +++ b/gre/test/fixture-projects/graph-config/hardhat.config.ts @@ -0,0 +1,44 @@ +import '../../../gre' + +module.exports = { + paths: { + graph: '../../files', + }, + solidity: '0.8.9', + defaultNetwork: 'hardhat', + networks: { + hardhat: { + chainId: 1337, + }, + mainnet: { + chainId: 1, + graphConfig: 'config/graph.mainnet.yml', + url: `https://mainnet.infura.io/v3/123456`, + }, + 'arbitrum-one': { + chainId: 42161, + url: 'https://arb1.arbitrum.io/rpc', + }, + goerli: { + chainId: 5, + url: `https://goerli.infura.io/v3/123456`, + }, + 'arbitrum-goerli': { + chainId: 421613, + url: 'https://goerli-rollup.arbitrum.io/rpc', + }, + localhost: { + chainId: 1337, + url: 'http://localhost:8545', + }, + 'arbitrum-rinkeby': { + chainId: 421611, + url: 'http://localhost:8545', + }, + }, + graph: { + addressBook: 'addresses-hre.json', + l1GraphConfig: 'config/graph.goerli.yml', + l2GraphConfig: 'config/graph.arbitrum-goerli.yml', + }, +} diff --git a/gre/test/gre.test.ts b/gre/test/gre.test.ts new file mode 100644 index 000000000..486e01d79 --- /dev/null +++ b/gre/test/gre.test.ts @@ -0,0 +1,70 @@ +import { expect } from 'chai' +import { useEnvironment } from './helpers' + +describe('GRE usage', function () { + describe('graph-config project setting --network to an L1', function () { + useEnvironment('graph-config', 'mainnet') + + it('should return L1 and L2 configured objects', function () { + const g = this.hre.graph() + + expect(g).to.be.an('object') + expect(g.l1).to.be.an('object') + expect(g.l2).to.be.an('object') + expect(g.l1.chainId).to.equal(1) + expect(g.l2.chainId).to.equal(42161) + expect(g.chainId).to.equal(1) + }) + }) + + describe('graph-config project setting --network to an L2', function () { + useEnvironment('graph-config', 'arbitrum-goerli') + + it('should return L1 and L2 configured objects ', function () { + const g = this.hre.graph() + + expect(g).to.be.an('object') + expect(g.l1).to.be.an('object') + expect(g.l2).to.be.an('object') + expect(g.l1.chainId).to.equal(5) + expect(g.l2.chainId).to.equal(421613) + expect(g.chainId).to.equal(421613) + }) + }) + + describe('graph-config project setting --network to an L1 with no configured counterpart', function () { + useEnvironment('graph-config', 'localhost') + + it('should return L1 configured object and L2 unconfigured', function () { + const g = this.hre.graph() + + expect(g).to.be.an('object') + expect(g.l1).to.be.an('object') + expect(g.l2).to.be.null + expect(g.l1.chainId).to.equal(1337) + expect(g.chainId).to.equal(1337) + }) + }) + + describe('graph-config project setting --network to an L2 with no configured counterpart', function () { + useEnvironment('graph-config', 'arbitrum-rinkeby') + + it('should return L2 configured object and L1 unconfigured', function () { + const g = this.hre.graph() + + expect(g).to.be.an('object') + expect(g.l1).to.be.null + expect(g.l2).to.be.an('object') + expect(g.l2.chainId).to.equal(421611) + expect(g.chainId).to.equal(421611) + }) + }) + + describe('default-config project', function () { + useEnvironment('default-config', 'mainnet') + + it('should throw', function () { + expect(() => this.hre.graph()).to.throw() + }) + }) +}) diff --git a/gre/test/helpers.ts b/gre/test/helpers.ts new file mode 100644 index 000000000..f0875a002 --- /dev/null +++ b/gre/test/helpers.ts @@ -0,0 +1,24 @@ +import { resetHardhatContext } from 'hardhat/plugins-testing' +import { HardhatRuntimeEnvironment } from 'hardhat/types' +import path from 'path' + +declare module 'mocha' { + interface Context { + hre: HardhatRuntimeEnvironment + } +} + +export function useEnvironment(fixtureProjectName: string, network?: string): void { + beforeEach('Loading hardhat environment', function () { + process.chdir(path.join(__dirname, 'fixture-projects', fixtureProjectName)) + + if (network !== undefined) { + process.env.HARDHAT_NETWORK = network + } + this.hre = require('hardhat') + }) + + afterEach('Resetting hardhat', function () { + resetHardhatContext() + }) +} diff --git a/gre/type-extensions.d.ts b/gre/type-extensions.d.ts new file mode 100644 index 000000000..a2f253124 --- /dev/null +++ b/gre/type-extensions.d.ts @@ -0,0 +1,80 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { AddressBook } from '../cli/address-book' +import { NetworkContracts } from '../cli/contracts' + +import { EthersProviderWrapper } from '@nomiclabs/hardhat-ethers/internal/ethers-provider-wrapper' + +export interface GraphRuntimeEnvironmentOptions { + addressBook?: string + l1GraphConfig?: string + l2GraphConfig?: string + graphConfig?: string +} + +export type AccountNames = + | 'arbitrator' + | 'governor' + | 'authority' + | 'availabilityOracle' + | 'pauseGuardian' + | 'allocationExchangeOwner' + +export type NamedAccounts = { + [name in AccountNames]: SignerWithAddress +} + +export interface GraphNetworkEnvironment { + chainId: number + provider: EthersProviderWrapper + contracts: NetworkContracts + graphConfig: any + addressBook: AddressBook + getNamedAccounts: () => Promise + getTestAccounts: () => Promise + getDeployer: () => Promise +} + +export interface GraphRuntimeEnvironment extends GraphNetworkEnvironment { + l1: GraphNetworkEnvironment | null + l2: GraphNetworkEnvironment | null +} + +declare module 'hardhat/types/runtime' { + export interface HardhatRuntimeEnvironment { + graph: (opts?: GraphRuntimeEnvironmentOptions) => GraphRuntimeEnvironment + } +} + +declare module 'hardhat/types/config' { + export interface HardhatConfig { + graph: Omit + } + + export interface HardhatUserConfig { + graph: Omit + } + + export interface HardhatNetworkConfig { + graphConfig?: string + } + + export interface HardhatNetworkUserConfig { + graphConfig?: string + } + + export interface HttpNetworkConfig { + graphConfig?: string + } + + export interface HttpNetworkUserConfig { + graphConfig?: string + } + + export interface ProjectPathsConfig { + graph?: string + } + + export interface ProjectPathsUserConfig { + graph?: string + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 3dc5a3936..003e7d3ee 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -28,7 +28,7 @@ import 'hardhat-storage-layout' const SKIP_LOAD = process.env.SKIP_LOAD === 'true' function loadTasks() { - require('./tasks/gre.ts') + require('./gre/gre') ;['contracts', 'misc', 'deployment', 'actions', 'verify', 'e2e'].forEach((folder) => { const tasksPath = path.join(__dirname, 'tasks', folder) fs.readdirSync(tasksPath) @@ -54,10 +54,11 @@ interface NetworkConfig { url?: string gas?: number | 'auto' gasPrice?: number | 'auto' + graphConfig?: string } const networkConfigs: NetworkConfig[] = [ - { network: 'mainnet', chainId: 1 }, + { network: 'mainnet', chainId: 1, graphConfig: 'config/graph.mainnet.yml' }, { network: 'rinkeby', chainId: 4 }, { network: 'goerli', chainId: 5 }, { network: 'kovan', chainId: 42 }, @@ -82,6 +83,9 @@ function setupNetworkProviders(hardhatConfig) { gasPrice: netConfig.gasPrice || 'auto', accounts: getAccountsKeys(), } + if (netConfig.graphConfig) { + hardhatConfig.networks[netConfig.network].graphConfig = netConfig.graphConfig + } } } @@ -136,10 +140,16 @@ const config: HardhatUserConfig = { hardfork: 'london', }, localhost: { + chainId: 1337, accounts: process.env.FORK === 'true' ? getAccountsKeys() : { mnemonic: DEFAULT_TEST_MNEMONIC }, }, }, + graph: { + addressBook: process.env.ADDRESS_BOOK, + l1GraphConfig: process.env.GRAPH_CONFIG, + l2GraphConfig: process.env.L2_GRAPH_CONFIG, + }, etherscan: { apiKey: process.env.ETHERSCAN_API_KEY, }, diff --git a/package.json b/package.json index 852fd78f7..2ca6cd1c3 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "test:e2e": "scripts/e2e", "test:gas": "RUN_EVM=true REPORT_GAS=true scripts/test", "test:coverage": "scripts/coverage", + "test:gre": "cd gre && mocha --exit --recursive 'test/**/*.test.ts' && cd ..", "lint": "yarn lint:ts && yarn lint:sol", "lint:fix": "yarn lint:ts:fix && yarn lint:sol:fix", "lint:ts": "eslint '**/*.{js,ts}'", diff --git a/tasks/gre.ts b/tasks/gre.ts deleted file mode 100644 index 41028e958..000000000 --- a/tasks/gre.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { HardhatRuntimeEnvironment } from 'hardhat/types' -import { extendEnvironment } from 'hardhat/config' -import { lazyFunction, lazyObject } from 'hardhat/plugins' - -import { getAddressBook } from '../cli/address-book' -import { loadContracts } from '../cli/contracts' -import { getItemValue, readConfig } from '../cli/config' -import { GREOptions, NamedAccounts } from './type-extensions' -import fs from 'fs' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' - -// Graph Runtime Environment (GRE) extensions for the HRE -extendEnvironment((hre: HardhatRuntimeEnvironment) => { - hre.graph = (opts: GREOptions = {}) => { - const chainId = hre.network.config.chainId?.toString() ?? '1337' - const addressBookPath = opts.addressBook ?? process.env.ADDRESS_BOOK - const graphConfigPath = opts.graphConfig ?? process.env.GRAPH_CONFIG - - if (!fs.existsSync(addressBookPath)) { - throw new Error(`Address book not found: ${addressBookPath}`) - } - - if (!fs.existsSync(graphConfigPath)) { - throw new Error(`Graph config not found: ${graphConfigPath}`) - } - - const namedAccountList = [ - 'arbitrator', - 'governor', - 'authority', - 'availabilityOracle', - 'pauseGuardian', - 'allocationExchangeOwner', - ] - - const getTestAccounts = async (): Promise => { - // Get list of privileged accounts we don't want as test accounts - const namedAccounts = await getNamedAccounts() - const blacklist = namedAccountList.map((a) => { - const account = namedAccounts[a] as SignerWithAddress - return account.address - }) - blacklist.push((await getDeployer()).address) - - // Get signers and filter out blacklisted accounts - let signers: SignerWithAddress[] = await hre.ethers.getSigners() - signers = signers.filter((s) => { - return !blacklist.includes(s.address) - }) - - return signers - } - - const getNamedAccounts = async (): Promise => { - const namedAccounts = namedAccountList.reduce(async (accountsPromise, name) => { - const accounts = await accountsPromise - const address = getItemValue(readConfig(graphConfigPath, true), `general/${name}`) - accounts[name] = await hre.ethers.getSigner(address) - return accounts - }, Promise.resolve({} as NamedAccounts)) - - return namedAccounts - } - - const getDeployer = async () => { - const signer = hre.ethers.provider.getSigner(0) - return hre.ethers.getSigner(await signer.getAddress()) - } - - return { - addressBook: lazyObject(() => getAddressBook(addressBookPath, chainId)), - graphConfig: lazyObject(() => readConfig(graphConfigPath, true)), - contracts: lazyObject(() => - loadContracts(getAddressBook(addressBookPath, chainId), hre.ethers.provider), - ), - getNamedAccounts: lazyFunction(() => getNamedAccounts), - getTestAccounts: lazyFunction(() => getTestAccounts), - getDeployer: lazyFunction(() => getDeployer), - } - } -}) diff --git a/tasks/type-extensions.d.ts b/tasks/type-extensions.d.ts deleted file mode 100644 index 5dbaa09ab..000000000 --- a/tasks/type-extensions.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { AddressBook } from '../cli/address-book' -import { NetworkContracts } from '../cli/contracts' - -export interface GREOptions { - addressBook?: string - graphConfig?: string -} - -export interface NamedAccounts { - arbitrator: SignerWithAddress - governor: SignerWithAddress - authority: SignerWithAddress - availabilityOracle: SignerWithAddress - pauseGuardian: SignerWithAddress - allocationExchangeOwner: SignerWithAddress -} - -declare module 'hardhat/types/runtime' { - export interface HardhatRuntimeEnvironment { - graph: (opts?: GREOptions) => { - contracts: NetworkContracts - graphConfig: any - addressBook: AddressBook - getNamedAccounts: () => Promise - getTestAccounts: () => Promise - getDeployer: () => Promise - } - } -} diff --git a/tsconfig.json b/tsconfig.json index 58be04eb9..e38e8e832 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ "scripts/**/*.ts", "test/**/*.ts", "tasks/**/*.ts", - "e2e/**/*.ts" + "e2e/**/*.ts", + "gre/**/*.ts" ] }