From 96181d9f2d414dc473b782aa55d0174617b549ad Mon Sep 17 00:00:00 2001 From: Martynas Prokopas Date: Sat, 14 Nov 2020 00:40:21 +0100 Subject: [PATCH] Chore: Converted shared web3 libs to Typescript --- packages/shared/package.json | 1 + packages/shared/src/helpers/logger.ts | 2 +- packages/shared/src/helpers/numbers.ts | 6 +- .../src/models/{Protocol.js => Protocol.ts} | 111 ++++++++++-------- .../{BaseArtifacts.js => BaseArtifacts.ts} | 29 +++-- ...ynamicArtifacts.js => DynamicArtifacts.ts} | 20 ++-- ...{StaticArtifacts.js => StaticArtifacts.ts} | 11 +- .../models/environments/BrowserEnvironment.js | 37 ------ .../models/environments/BrowserEnvironment.ts | 35 ++++++ .../src/models/environments/Environment.js | 76 ------------ .../src/models/environments/Environment.ts | 85 ++++++++++++++ ...ocalEnvironment.js => LocalEnvironment.ts} | 31 ++--- ...leEnvironment.js => TruffleEnvironment.ts} | 33 +++--- .../src/models/providers/JsonRpcProvider.js | 15 --- .../src/models/providers/JsonRpcProvider.ts | 9 ++ .../src/models/providers/JsonRpcSigner.js | 39 ------ .../src/models/providers/JsonRpcSigner.ts | 42 +++++++ .../shared/src/models/providers/Wallet.js | 27 ----- .../shared/src/models/providers/Wallet.ts | 41 +++++++ .../shared/src/types/contract-helpers-test.ts | 2 + packages/shared/src/web3/Network.js | 17 --- packages/shared/src/web3/Network.ts | 17 +++ yarn.lock | 5 + 23 files changed, 371 insertions(+), 320 deletions(-) rename packages/shared/src/models/{Protocol.js => Protocol.ts} (83%) rename packages/shared/src/models/artifacts/{BaseArtifacts.js => BaseArtifacts.ts} (51%) rename packages/shared/src/models/artifacts/{DynamicArtifacts.js => DynamicArtifacts.ts} (57%) rename packages/shared/src/models/artifacts/{StaticArtifacts.js => StaticArtifacts.ts} (63%) delete mode 100644 packages/shared/src/models/environments/BrowserEnvironment.js create mode 100644 packages/shared/src/models/environments/BrowserEnvironment.ts delete mode 100644 packages/shared/src/models/environments/Environment.js create mode 100644 packages/shared/src/models/environments/Environment.ts rename packages/shared/src/models/environments/{LocalEnvironment.js => LocalEnvironment.ts} (52%) rename packages/shared/src/models/environments/{TruffleEnvironment.js => TruffleEnvironment.ts} (58%) delete mode 100644 packages/shared/src/models/providers/JsonRpcProvider.js create mode 100644 packages/shared/src/models/providers/JsonRpcProvider.ts delete mode 100644 packages/shared/src/models/providers/JsonRpcSigner.js create mode 100644 packages/shared/src/models/providers/JsonRpcSigner.ts delete mode 100644 packages/shared/src/models/providers/Wallet.js create mode 100644 packages/shared/src/models/providers/Wallet.ts create mode 100644 packages/shared/src/types/contract-helpers-test.ts delete mode 100644 packages/shared/src/web3/Network.js create mode 100644 packages/shared/src/web3/Network.ts diff --git a/packages/shared/package.json b/packages/shared/package.json index 74c4f89..f7733c3 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -44,6 +44,7 @@ "@babel/preset-typescript": "^7.12.1", "@promster/types": "^1.0.8", "@types/bcryptjs": "^2.4.2", + "@types/configstore": "^4.0.0", "@types/express": "^4.17.8", "@types/jsonwebtoken": "^8.5.0", "@typescript-eslint/eslint-plugin": "^4.6.1", diff --git a/packages/shared/src/helpers/logger.ts b/packages/shared/src/helpers/logger.ts index d48e0a9..81da905 100644 --- a/packages/shared/src/helpers/logger.ts +++ b/packages/shared/src/helpers/logger.ts @@ -47,7 +47,7 @@ class Logger { } } -const LoggerConstructor = (actor: string, color: string) => new Logger(actor, color) +const LoggerConstructor = (actor: string, color?: string) => new Logger(actor, color) export default LoggerConstructor diff --git a/packages/shared/src/helpers/numbers.ts b/packages/shared/src/helpers/numbers.ts index b2ccfeb..aecb633 100644 --- a/packages/shared/src/helpers/numbers.ts +++ b/packages/shared/src/helpers/numbers.ts @@ -1,6 +1,6 @@ import { utils } from 'ethers' import { fromWei } from 'web3-utils' -import { BigNumberish } from 'ethers/utils' +import { BigNumber, BigNumberish } from 'ethers/utils' const bn = (x: BigNumberish) => utils.bigNumberify(x) const bigExp = (x: BigNumberish, y = 18) => bn(x).mul(bn(10).pow(bn(y))) @@ -14,5 +14,7 @@ export { bigExp, maxUint, tokenToString, - MAX_UINT64 + MAX_UINT64, + BigNumber, + BigNumberish } diff --git a/packages/shared/src/models/Protocol.js b/packages/shared/src/models/Protocol.ts similarity index 83% rename from packages/shared/src/models/Protocol.js rename to packages/shared/src/models/Protocol.ts index 2c14636..b55142f 100644 --- a/packages/shared/src/models/Protocol.js +++ b/packages/shared/src/models/Protocol.ts @@ -1,38 +1,51 @@ -const { sha3, fromWei, utf8ToHex, soliditySha3 } = require('web3-utils') -const { ZERO_ADDRESS, getEventArgument, getEvents } = require('@aragon/contract-helpers-test') +import { sha3, fromWei, utf8ToHex, soliditySha3 } from 'web3-utils' +import { Contract } from 'ethers' +import { ZERO_ADDRESS, getEventArgument, getEvents } from '@aragon/contract-helpers-test' -const logger = require('../helpers/logger').default('Protocol') -const { bn, bigExp } = require('../helpers/numbers') -const { encodeVoteId, hashVote } = require('../helpers/voting') +import Logger from '../helpers/logger' +const logger = Logger('Protocol') +import { bn, bigExp, BigNumberish } from '../helpers/numbers' +import { encodeVoteId, hashVote } from '../helpers/voting' +import Environment from './environments/Environment' const ROUND_STATE_ENDED = 5 -module.exports = class { - constructor(instance, environment) { +export default class Protocol { + + instance: any // TODO: provide proper types for Protocol instance + environment: Environment + _token?: Contract + _feeToken?: Contract + _registry?: Contract + _disputeManager?: Contract + _voting?: Contract + _paymentsBook?: Contract + + constructor(instance: any, environment: Environment) { this.instance = instance this.environment = environment } - async token() { + async token(): Promise { if (!this._token) { const registry = await this.registry() const address = await registry.token() const ERC20 = await this.environment.getArtifact('ERC20Mock', '@aragon/protocol-evm') this._token = await ERC20.at(address) } - return this._token + return this._token! } - async feeToken() { + async feeToken(): Promise { if (!this._feeToken) { const { feeToken } = await this.getConfigAt() const ERC20 = await this.environment.getArtifact('ERC20Mock', '@aragon/protocol-evm') this._feeToken = await ERC20.at(feeToken) } - return this._feeToken + return this._feeToken! } - async registry() { + async registry(): Promise { if (!this._registry) { const { addr: address } = await this.instance.getGuardiansRegistry() const GuardiansRegistry = await this.environment.getArtifact('GuardiansRegistry', '@aragon/protocol-evm') @@ -41,7 +54,7 @@ module.exports = class { return this._registry } - async disputeManager() { + async disputeManager(): Promise { if (!this._disputeManager) { const { addr: address } = await this.instance.getDisputeManager() const DisputeManager = await this.environment.getArtifact('DisputeManager', '@aragon/protocol-evm') @@ -50,7 +63,7 @@ module.exports = class { return this._disputeManager } - async voting() { + async voting(): Promise { if (!this._voting) { const { addr: address } = await this.instance.getVoting() const Voting = await this.environment.getArtifact('CRVoting', '@aragon/protocol-evm') @@ -59,7 +72,7 @@ module.exports = class { return this._voting } - async paymentsBook() { + async paymentsBook(): Promise { if (!this._paymentsBook) { const { addr: address } = await this.instance.getPaymentsBook() const PaymentsBook = await this.environment.getArtifact('PaymentsBook', '@aragon/protocol-evm') @@ -81,7 +94,7 @@ module.exports = class { return this.instance.getCurrentTermId() } - async getTerm(id) { + async getTerm(id: BigNumberish) { return this.instance.getTerm(id) } @@ -105,7 +118,7 @@ module.exports = class { } } - async getRevealStatus(disputeId, roundNumber) { + async getRevealStatus(disputeId: BigNumberish, roundNumber: number) { const disputeManager = await this.disputeManager() const { createTermId } = await disputeManager.getDispute(disputeId) const { draftTerm, delayedTerms } = await disputeManager.getRound(disputeId, roundNumber) @@ -122,7 +135,7 @@ module.exports = class { return { canReveal, expired } } - async canSettle(disputeId) { + async canSettle(disputeId: BigNumberish) { const disputeManager = await this.disputeManager() const { finalRuling, lastRoundId } = await disputeManager.getDispute(disputeId) @@ -132,50 +145,50 @@ module.exports = class { return state === ROUND_STATE_ENDED } - async getGuardians(disputeId, roundNumber) { + async getGuardians(disputeId: BigNumberish, roundNumber: number) { const result = await this.environment.query(`{ dispute (id: "${disputeId}") { id rounds (where: { number: "${roundNumber}" }) { guardians { guardian { id } }} }}`) - return result.dispute.rounds[0].guardians.map(guardian => guardian.guardian.id) + return result.dispute.rounds[0].guardians.map((guardian: any) => guardian.guardian.id) } - async existsVote(voteId) { + async existsVote(voteId: BigNumberish) { const voting = await this.voting() const maxAllowedOutcomes = await voting.getMaxAllowedOutcome(voteId) return maxAllowedOutcomes !== 0 } - async isValidOutcome(voteId, outcome) { + async isValidOutcome(voteId: BigNumberish, outcome: string) { const voting = await this.voting() const exists = await this.existsVote(voteId) return exists && (await voting.isValidOutcome(voteId, outcome)) } - async getLastRoundVoteId(disputeId) { + async getLastRoundVoteId(disputeId: BigNumberish) { const disputeManager = await this.disputeManager() const { lastRoundId } = await disputeManager.getDispute(disputeId) return encodeVoteId(disputeId, lastRoundId) } - async getCommitment(voteId, voter) { + async getCommitment(voteId: BigNumberish, voter: string) { const result = await this.environment.query(`{ guardianDrafts (where: { round:"${voteId}", guardian: "${voter}" }) { commitment }}`) return (!result || !result.guardianDrafts || result.guardianDrafts.length === 0) ? undefined : result.guardianDrafts[0].commitment } - async getOutcome(voteId, voter) { + async getOutcome(voteId: BigNumberish, voter: string) { const voting = await this.voting() return voting.getVoterOutcome(voteId, voter) } - async getPeriodBalanceDetails(periodId) { + async getPeriodBalanceDetails(periodId: BigNumberish) { const paymentsBook = await this.paymentsBook() const { balanceCheckpoint, totalActiveBalance } = await paymentsBook.getPeriodBalanceDetails(periodId) return { balanceCheckpoint, totalActiveBalance } } - async heartbeat(transitions = undefined) { + async heartbeat(transitions?: any) { const needed = await this.neededTransitions() logger.info(`Required ${needed} transitions`) if (needed.eq(bn(0))) return needed @@ -185,7 +198,7 @@ module.exports = class { return Math.min(heartbeats, needed) } - async stake(guardian, amount, data = '0x') { + async stake(guardian: string, amount: BigNumberish, data = '0x') { const token = await this.token() const decimals = await token.decimals() const registry = await this.registry() @@ -195,7 +208,7 @@ module.exports = class { return registry.stakeFor(guardian, bigExp(amount, decimals), data) } - async unstake(amount, data = '0x') { + async unstake(amount: BigNumberish, data = '0x') { const token = await this.token() const decimals = await token.decimals() const registry = await this.registry() @@ -204,7 +217,7 @@ module.exports = class { return registry.unstake(bigExp(amount, decimals), data) } - async activate(amount) { + async activate(amount: BigNumberish) { const token = await this.token() const decimals = await token.decimals() const registry = await this.registry() @@ -213,18 +226,18 @@ module.exports = class { return registry.activate(bigExp(amount, decimals)) } - async activateFor(address, amount) { + async activateFor(address: string, amount: BigNumberish) { const token = await this.token() const decimals = await token.decimals() const registry = await this.registry() const symbol = await token.symbol() await this._approve(token, bigExp(amount, decimals), registry.address) - const ACTIVATE_DATA = sha3('activate(uint256)').slice(0, 10) + const ACTIVATE_DATA = sha3('activate(uint256)')!.slice(0, 10) logger.info(`Activating ${amount} ${symbol} for ${address}...`) return registry.stakeFor(address, bigExp(amount, decimals), ACTIVATE_DATA) } - async deactivate(amount) { + async deactivate(amount: BigNumberish) { const token = await this.token() const decimals = await token.decimals() const registry = await this.registry() @@ -232,7 +245,7 @@ module.exports = class { return registry.deactivate(bigExp(amount, decimals)) } - async pay(tokenAddress, amount, payer, data) { + async pay(tokenAddress: string, amount: BigNumberish, payer: string, data: any) { const paymentsBook = await this.paymentsBook() const ERC20 = await this.environment.getArtifact('ERC20Mock', '@aragon/protocol-evm') const token = await ERC20.at(tokenAddress) @@ -250,7 +263,7 @@ module.exports = class { return Arbitrable.new(this.instance.address) } - async createDispute(subject, rulings = 2, metadata = '', evidence = [], submitters = [], closeEvidencePeriod = false) { + async createDispute(subject: string, rulings = 2, metadata = '', evidence = [], submitters = [], closeEvidencePeriod = false) { logger.info(`Transferring tokens to Arbitrable instance ${subject}...`) const feeToken = await this.feeToken() const disputeManager = await this.disputeManager() @@ -280,33 +293,33 @@ module.exports = class { return disputeId } - async draft(disputeId) { + async draft(disputeId: BigNumberish) { const disputeManager = await this.disputeManager() logger.info(`Drafting dispute #${disputeId} ...`) const { hash } = await disputeManager.draft(disputeId) const receipt = await this.environment.getTransaction(hash) - return getEvents(receipt, 'GuardianDrafted', { decodeForAbi: disputeManager.interface.abi }).map(event => event.args.guardian) + return getEvents(receipt, 'GuardianDrafted', { decodeForAbi: disputeManager.interface.abi }).map((event: any) => event.args.guardian) } - async commit(disputeId, outcome, password) { + async commit(disputeId: BigNumberish, outcome: string, password: string) { const voteId = await this.getLastRoundVoteId(disputeId) logger.info(`Committing a vote for dispute #${disputeId} on vote ID ${voteId}...`) const voting = await this.voting() - return voting.commit(voteId, hashVote(outcome, soliditySha3(password))) + return voting.commit(voteId, hashVote(outcome, soliditySha3(password)!)) } - async reveal(disputeId, guardian, outcome, password) { + async reveal(disputeId: BigNumberish, guardian: string, outcome: string, password: string) { const voteId = await this.getLastRoundVoteId(disputeId) - return this.revealFor(voteId, guardian, outcome, soliditySha3(password)) + return this.revealFor(voteId, guardian, outcome, soliditySha3(password)!) } - async revealFor(voteId, guardian, outcome, salt) { + async revealFor(voteId: BigNumberish, guardian: string, outcome: string, salt: string) { logger.info(`Revealing vote for guardian ${guardian} on vote ID ${voteId}...`) const voting = await this.voting() return voting.reveal(voteId, guardian, outcome, salt) } - async appeal(disputeId, outcome) { + async appeal(disputeId: BigNumberish, outcome: string) { const disputeManager = await this.disputeManager() const { lastRoundId } = await disputeManager.getDispute(disputeId) @@ -318,7 +331,7 @@ module.exports = class { return disputeManager.createAppeal(disputeId, lastRoundId, outcome) } - async confirmAppeal(disputeId, outcome) { + async confirmAppeal(disputeId: BigNumberish, outcome: string) { const disputeManager = await this.disputeManager() const { lastRoundId } = await disputeManager.getDispute(disputeId) @@ -330,7 +343,7 @@ module.exports = class { return disputeManager.confirmAppeal(disputeId, lastRoundId, outcome) } - async settleRound(disputeId) { + async settleRound(disputeId: BigNumberish) { const disputeManager = await this.disputeManager() const { lastRoundId } = await disputeManager.getDispute(disputeId) @@ -345,7 +358,7 @@ module.exports = class { } } - async settleGuardian(disputeId, guardian) { + async settleGuardian(disputeId: BigNumberish, guardian: string) { const disputeManager = await this.disputeManager() const { lastRoundId } = await disputeManager.getDispute(disputeId) @@ -358,12 +371,12 @@ module.exports = class { } } - async execute(disputeId) { + async execute(disputeId: BigNumberish) { logger.info(`Executing ruling of dispute #${disputeId}...`) return this.instance.executeRuling(disputeId) } - async settle(disputeId) { + async settle(disputeId: BigNumberish) { const voting = await this.voting() const disputeManager = await this.disputeManager() const { finalRuling: ruling, lastRoundId } = await disputeManager.getDispute(disputeId) @@ -409,7 +422,7 @@ module.exports = class { } } - async _approve(token, amount, recipient) { + async _approve(token: Contract, amount: BigNumberish, recipient: string) { const allowance = await token.allowance(await this.environment.getSender(), recipient) if (allowance.gt(bn(0))) { logger.info(`Resetting allowance to zero for ${recipient}...`) diff --git a/packages/shared/src/models/artifacts/BaseArtifacts.js b/packages/shared/src/models/artifacts/BaseArtifacts.ts similarity index 51% rename from packages/shared/src/models/artifacts/BaseArtifacts.js rename to packages/shared/src/models/artifacts/BaseArtifacts.ts index 7339e9f..31a4db1 100644 --- a/packages/shared/src/models/artifacts/BaseArtifacts.js +++ b/packages/shared/src/models/artifacts/BaseArtifacts.ts @@ -1,40 +1,47 @@ -const { ethers } = require('ethers') +import { ethers, Signer, Contract } from 'ethers' // avoid warning log level ethers.errors.setLogLevel('error') -class BaseArtifacts { - constructor(signer) { +interface ArtifactBuilder { + abi: () => any + new: (...args: any[]) => Promise + at: (address: string) => Promise +} + +export default abstract class BaseArtifacts { + + signer: Signer + + constructor(signer: Signer) { this.signer = signer } - getContractSchema(contractName, dependency = undefined) { - throw Error('subclass responsibility') - } + abstract getContractSchema(contractName: string, dependency?: string): any - require(contractName, dependency = undefined) { + require(contractName: string, dependency?: string): ArtifactBuilder { const schema = this.getContractSchema(contractName, dependency) if (!schema) throw Error(`Please make sure you provide a contract schema for ${dependency}/${contractName}`) return this.buildArtifact(schema) } - buildArtifact(schema) { + buildArtifact(schema: any): ArtifactBuilder { const { signer } = this return { get abi() { return schema.abi }, - async new(...args) { + async new(...args: any[]) { const factory = new ethers.ContractFactory(schema.abi, schema.bytecode, signer) return factory.deploy(...args) }, - async at(address) { + async at(address: string) { return new ethers.Contract(address, schema.abi, signer) } } } } -module.exports = BaseArtifacts +export { ArtifactBuilder, BaseArtifacts } diff --git a/packages/shared/src/models/artifacts/DynamicArtifacts.js b/packages/shared/src/models/artifacts/DynamicArtifacts.ts similarity index 57% rename from packages/shared/src/models/artifacts/DynamicArtifacts.js rename to packages/shared/src/models/artifacts/DynamicArtifacts.ts index dc72321..2af94d2 100644 --- a/packages/shared/src/models/artifacts/DynamicArtifacts.js +++ b/packages/shared/src/models/artifacts/DynamicArtifacts.ts @@ -1,11 +1,11 @@ -const fs = require('fs') -const path = require('path') -const BaseArtifacts = require('./BaseArtifacts') +import fs from 'fs' +import path from 'path' +import BaseArtifacts from './BaseArtifacts' -const BUILD_DIRS = ['build/contracts', 'artifacts'] +const BUILD_DIRS: string[] = ['build/contracts', 'artifacts'] -class DynamicArtifacts extends BaseArtifacts { - getContractSchema(contractName, dependency = undefined) { +export default class DynamicArtifacts extends BaseArtifacts { + getContractSchema(contractName: string, dependency?: string): any { const contractPaths = dependency ? this._getNodeModulesPaths(dependency, contractName) : this._getLocalBuildPaths(contractName) @@ -15,18 +15,16 @@ class DynamicArtifacts extends BaseArtifacts { return artifact } - _findArtifact(paths) { + private _findArtifact(paths: string[]): any { const path = paths.find(fs.existsSync) return path ? require(path) : undefined } - _getLocalBuildPaths(contractName) { + private _getLocalBuildPaths(contractName: string): string[] { return BUILD_DIRS.map(dir => path.resolve(process.cwd(), `./${dir}/${contractName}.json`)) } - _getNodeModulesPaths(dependency, contractName) { + private _getNodeModulesPaths(dependency: string, contractName: string): string[] { return BUILD_DIRS.map(dir => path.resolve(__dirname, `../../../node_modules/${dependency}/${dir}/${contractName}.json`)) } } - -module.exports = DynamicArtifacts diff --git a/packages/shared/src/models/artifacts/StaticArtifacts.js b/packages/shared/src/models/artifacts/StaticArtifacts.ts similarity index 63% rename from packages/shared/src/models/artifacts/StaticArtifacts.js rename to packages/shared/src/models/artifacts/StaticArtifacts.ts index ee44792..fb268f1 100644 --- a/packages/shared/src/models/artifacts/StaticArtifacts.js +++ b/packages/shared/src/models/artifacts/StaticArtifacts.ts @@ -1,6 +1,6 @@ -const BaseArtifacts = require('./BaseArtifacts') +import BaseArtifacts from './BaseArtifacts' -const BUILDS = { +const BUILDS: { [repo: string]: { [contract: string]: any } } = { '@aragon/erc20-faucet': { 'ERC20Faucet': require('@aragonone/erc20-faucet/build/contracts/ERC20Faucet'), }, @@ -13,10 +13,9 @@ const BUILDS = { } } -class StaticArtifacts extends BaseArtifacts { - getContractSchema(contractName, dependency = undefined) { +export default class StaticArtifacts extends BaseArtifacts { + getContractSchema(contractName: string, dependency: string): any { + if (!BUILDS[dependency]) throw Error(`Could not find artifact for ${dependency}/${contractName}`) return BUILDS[dependency][contractName] } } - -module.exports = StaticArtifacts diff --git a/packages/shared/src/models/environments/BrowserEnvironment.js b/packages/shared/src/models/environments/BrowserEnvironment.js deleted file mode 100644 index 06db324..0000000 --- a/packages/shared/src/models/environments/BrowserEnvironment.js +++ /dev/null @@ -1,37 +0,0 @@ -const { ethers } = require('ethers') -const sleep = require('../../helpers/sleep') -const Environment = require('./Environment') -const JsonRpcSigner = require('../providers/JsonRpcSigner') -const StaticArtifacts = require('../artifacts/StaticArtifacts') - -class BrowserEnvironment extends Environment { - constructor(network) { - super(network) - } - - async isEnabled() { - await sleep(2000) - const { web3 } = window - return !!(web3 && web3.currentProvider && web3.currentProvider.selectedAddress) - } - - async _getProvider() { - const isEnabled = await this.isEnabled() - if (!isEnabled) throw Error('Could not access to a browser web3 provider, please make sure to allow one.') - const provider = window.web3.currentProvider - provider.setMaxListeners(300) - return new ethers.providers.Web3Provider(provider) - } - - async _getSigner() { - const provider = await this.getProvider() - return new JsonRpcSigner(provider) - } - - async _getArtifacts() { - const signer = await this.getSigner() - return new StaticArtifacts(signer) - } -} - -module.exports = BrowserEnvironment diff --git a/packages/shared/src/models/environments/BrowserEnvironment.ts b/packages/shared/src/models/environments/BrowserEnvironment.ts new file mode 100644 index 0000000..09b8bfa --- /dev/null +++ b/packages/shared/src/models/environments/BrowserEnvironment.ts @@ -0,0 +1,35 @@ +import { Web3Provider } from 'ethers/providers' +import sleep from '../../helpers/sleep' +import { Network, Environment } from './Environment' +import JsonRpcSigner from '../providers/JsonRpcSigner' +import StaticArtifacts from '../artifacts/StaticArtifacts' + +export default class BrowserEnvironment extends Environment { + constructor(network: Network) { + super(network) + } + + async isEnabled(): Promise { + await sleep(2000) + const { web3 } = (window as { web3?: any }) + return !!(web3 && web3.currentProvider && web3.currentProvider.selectedAddress) + } + + async _getProvider(): Promise { + const isEnabled = await this.isEnabled() + if (!isEnabled) throw Error('Could not access to a browser web3 provider, please make sure to allow one.') + const provider = (window as { web3?: any }).web3.currentProvider + provider.setMaxListeners(300) + return new Web3Provider(provider) + } + + async _getSigner(): Promise { + const provider = await this.getProvider() + return new JsonRpcSigner(provider) + } + + async _getArtifacts(): Promise { + const signer = await this.getSigner() + return new StaticArtifacts(signer) + } +} diff --git a/packages/shared/src/models/environments/Environment.js b/packages/shared/src/models/environments/Environment.js deleted file mode 100644 index 5fbfdd5..0000000 --- a/packages/shared/src/models/environments/Environment.js +++ /dev/null @@ -1,76 +0,0 @@ -const Protocol = require('../Protocol') -const { request } = require('graphql-request') - -const SUBGRAPH_LOCAL = 'http://127.0.0.1:8000' -const SUBGRAPH_REMOTE = 'https://api.thegraph.com' - -class Environment { - constructor(network) { - this.network = network - } - - getSubgraph() { - const base = this.network === 'ganache' ? SUBGRAPH_LOCAL : SUBGRAPH_REMOTE - return `${base}/subgraphs/name/aragon/aragon-protocol-${this.network}` - } - - async query(query) { - const subgraph = this.getSubgraph() - return request(subgraph, query) - } - - async getProtocol(address) { - const AragonProtocol = await this.getArtifact('AragonProtocol', '@aragon/protocol-evm') - const protocol = await AragonProtocol.at(address) - return new Protocol(protocol, this) - } - - async getTransaction(hash) { - const provider = await this.getProvider() - return provider.waitForTransaction(hash) - } - - async getProvider() { - if (!this.provider) this.provider = await this._getProvider() - return this.provider - } - - async getSigner() { - if (!this.signer) this.signer = await this._getSigner() - return this.signer - } - - async getArtifacts() { - if (!this.artifacts) this.artifacts = await this._getArtifacts() - return this.artifacts - } - - async getArtifact(contractName, dependency = undefined) { - const artifacts = await this.getArtifacts() - return artifacts.require(contractName, dependency) - } - - async getAccounts() { - const provider = await this.getProvider() - return provider.listAccounts() - } - - async getSender() { - const signer = await this.getSigner() - return signer.getAddress() - } - - async _getProvider() { - throw Error('subclass responsibility') - } - - async _getSigner() { - throw Error('subclass responsibility') - } - - async _getArtifacts() { - throw Error('subclass responsibility') - } -} - -module.exports = Environment diff --git a/packages/shared/src/models/environments/Environment.ts b/packages/shared/src/models/environments/Environment.ts new file mode 100644 index 0000000..e4b683a --- /dev/null +++ b/packages/shared/src/models/environments/Environment.ts @@ -0,0 +1,85 @@ +import Protocol from '../Protocol' +import { request } from 'graphql-request' +import { JsonRpcProvider, TransactionReceipt } from 'ethers/providers' +import { Signer } from 'ethers' + +import { BaseArtifacts, ArtifactBuilder } from '../artifacts/BaseArtifacts' + +const SUBGRAPH_LOCAL = 'http://127.0.0.1:8000' +const SUBGRAPH_REMOTE = 'https://api.thegraph.com' + +type Network = 'ganache' | 'rpc' | 'rinkeby' | 'ropsten' | 'staging' | 'mainnet' + +export default abstract class Environment { + + network: Network + provider?: JsonRpcProvider + signer?: Signer + artifacts?: BaseArtifacts + + constructor(network: Network) { + this.network = network + } + + getSubgraph(): string { + const base = this.network === 'ganache' ? SUBGRAPH_LOCAL : SUBGRAPH_REMOTE + return `${base}/subgraphs/name/aragon/aragon-protocol-${this.network}` + } + + async query(query: string): Promise { + const subgraph = this.getSubgraph() + return request(subgraph, query) + } + + async getProtocol(address: string): Promise { + const AragonProtocol = await this.getArtifact('AragonProtocol', '@aragon/protocol-evm') + const protocol = await AragonProtocol.at(address) + return new Protocol(protocol, this) + } + + async getTransaction(hash: string): Promise { + const provider = await this.getProvider() + return provider.waitForTransaction(hash) + } + + async getProvider(): Promise { + if (!this.provider) this.provider = await this._getProvider() + return this.provider + } + + async getSigner(): Promise { + if (!this.signer) this.signer = await this._getSigner() + return this.signer + } + + async getArtifacts(): Promise { + if (!this.artifacts) this.artifacts = await this._getArtifacts() + return this.artifacts + } + + async getArtifact(contractName: string, dependency?: string): Promise { + const artifacts = await this.getArtifacts() + return artifacts.require(contractName, dependency) + } + + async getAccounts(): Promise { + const provider = await this.getProvider() + return provider.listAccounts() + } + + async getSender(): Promise { + const signer = await this.getSigner() + return signer.getAddress() + } + + abstract async _getProvider(): Promise + abstract async _getSigner(): Promise + abstract async _getArtifacts(): Promise +} + +export { + Network, + Environment, + Protocol, + JsonRpcProvider, +} diff --git a/packages/shared/src/models/environments/LocalEnvironment.js b/packages/shared/src/models/environments/LocalEnvironment.ts similarity index 52% rename from packages/shared/src/models/environments/LocalEnvironment.js rename to packages/shared/src/models/environments/LocalEnvironment.ts index 880d247..23e9568 100644 --- a/packages/shared/src/models/environments/LocalEnvironment.js +++ b/packages/shared/src/models/environments/LocalEnvironment.ts @@ -1,7 +1,7 @@ -const Wallet = require('../providers/Wallet') -const Environment = require('./Environment') -const JsonRpcProvider = require('../providers/JsonRpcProvider') -const DynamicArtifacts = require('../artifacts/DynamicArtifacts') +import { Wallet, GasParams } from '../providers/Wallet' +import { Network, Environment, Protocol } from './Environment' +import JsonRpcProvider from '../providers/JsonRpcProvider' +import DynamicArtifacts from '../artifacts/DynamicArtifacts' require('dotenv').config() // Load env vars from .env file @@ -18,30 +18,33 @@ require('dotenv').config() // Load env vars from .env file const { NETWORK, PROTOCOL_ADDRESS, RPC, PRIVATE_KEY, GAS, GAS_PRICE, WEB3_POLLING_INTERVAL } = process.env -class LocalEnvironment extends Environment { +export default class LocalEnvironment extends Environment { constructor() { - super(NETWORK) + super(NETWORK as Network) } - async getProtocol(address = undefined) { - return super.getProtocol(PROTOCOL_ADDRESS) + async getProtocol(): Promise { + return super.getProtocol(PROTOCOL_ADDRESS as string) } - async _getProvider() { + async _getProvider(): Promise { const provider = new JsonRpcProvider(RPC) - provider.pollingInterval = parseInt(WEB3_POLLING_INTERVAL) + provider.pollingInterval = parseInt(WEB3_POLLING_INTERVAL as string) return provider } - async _getSigner() { + async _getSigner(): Promise { const provider = await this.getProvider() - return new Wallet(PRIVATE_KEY, provider, { gasPrice: GAS_PRICE, gasLimit: GAS }) + return new Wallet(PRIVATE_KEY as string, provider, { gasPrice: GAS_PRICE, gasLimit: GAS } as GasParams) } - async _getArtifacts() { + async _getArtifacts(): Promise { const signer = await this._getSigner() return new DynamicArtifacts(signer) } } -module.exports = LocalEnvironment +export { + LocalEnvironment, + Protocol +} diff --git a/packages/shared/src/models/environments/TruffleEnvironment.js b/packages/shared/src/models/environments/TruffleEnvironment.ts similarity index 58% rename from packages/shared/src/models/environments/TruffleEnvironment.js rename to packages/shared/src/models/environments/TruffleEnvironment.ts index 2cf1fdb..002a3d5 100644 --- a/packages/shared/src/models/environments/TruffleEnvironment.js +++ b/packages/shared/src/models/environments/TruffleEnvironment.ts @@ -1,16 +1,21 @@ -const { ethers } = require('ethers') -const Environment = require('./Environment') -const TruffleConfig = require('@truffle/config') -const JsonRpcSigner = require('../providers/JsonRpcSigner') -const DynamicArtifacts = require('../artifacts/DynamicArtifacts') - -class TruffleEnvironment extends Environment { - constructor(network, sender = undefined) { +import { ethers } from 'ethers' +import { Web3Provider } from 'ethers/providers' +import { Environment, Network, Protocol } from './Environment' +import TruffleConfig from '@truffle/config' +import JsonRpcSigner from '../providers/JsonRpcSigner' +import DynamicArtifacts from '../artifacts/DynamicArtifacts' + +export default class TruffleEnvironment extends Environment { + + sender?: string + config?: TruffleConfig + + constructor(network: Network, sender?: string) { super(network) this.sender = sender } - async getProtocol(address = undefined) { + async getProtocol(address?: string): Promise { if (address) return super.getProtocol(address) if (process.env.PROTOCOL_ADDRESS) return super.getProtocol(process.env.PROTOCOL_ADDRESS) const config = require('../../truffle-config') @@ -19,12 +24,12 @@ class TruffleEnvironment extends Environment { return super.getProtocol(protocol) } - async _getProvider() { + async _getProvider(): Promise { const { provider } = this._getNetworkConfig() - return new ethers.providers.Web3Provider(provider) + return new Web3Provider(provider) } - async _getSigner() { + async _getSigner(): Promise { const { from, gas, gasPrice } = this._getNetworkConfig() const provider = await this.getProvider() return new JsonRpcSigner(provider, this.sender || from, { gasLimit: gas, gasPrice }) @@ -35,7 +40,7 @@ class TruffleEnvironment extends Environment { return new DynamicArtifacts(signer) } - _getNetworkConfig() { + _getNetworkConfig(): TruffleConfig { if (!this.config) { this.config = TruffleConfig.detect({ logger: console }) this.config.network = this.network @@ -43,5 +48,3 @@ class TruffleEnvironment extends Environment { return this.config } } - -export default TruffleEnvironment diff --git a/packages/shared/src/models/providers/JsonRpcProvider.js b/packages/shared/src/models/providers/JsonRpcProvider.js deleted file mode 100644 index e171aae..0000000 --- a/packages/shared/src/models/providers/JsonRpcProvider.js +++ /dev/null @@ -1,15 +0,0 @@ -const ethers = require('ethers') -const logger = require('../../helpers/logger').default('Provider') - -class JsonRpcProvider extends ethers.providers.JsonRpcProvider { - constructor(url, network) { - super(url, network) - } - - send(method, params) { - logger.info(`Sending RPC request '${method}'`) - return super.send(method, params) - } -} - -module.exports = JsonRpcProvider diff --git a/packages/shared/src/models/providers/JsonRpcProvider.ts b/packages/shared/src/models/providers/JsonRpcProvider.ts new file mode 100644 index 0000000..6c84ae2 --- /dev/null +++ b/packages/shared/src/models/providers/JsonRpcProvider.ts @@ -0,0 +1,9 @@ +import { ethers } from 'ethers' +const logger = require('../../helpers/logger').default('Provider') + +export default class JsonRpcProvider extends ethers.providers.JsonRpcProvider { + send(method: string, params: any): Promise { + logger.info(`Sending RPC request '${method}'`) + return super.send(method, params) + } +} diff --git a/packages/shared/src/models/providers/JsonRpcSigner.js b/packages/shared/src/models/providers/JsonRpcSigner.js deleted file mode 100644 index 841c8c4..0000000 --- a/packages/shared/src/models/providers/JsonRpcSigner.js +++ /dev/null @@ -1,39 +0,0 @@ -const ethers = require('ethers') -const { bn } = require('../../helpers/numbers') - -class JsonRpcSigner extends ethers.Signer { - constructor(provider, address = undefined, { gasPrice = undefined, gasLimit = undefined } = {}) { - super() - - this.address = address - this.provider = provider - this.gasPrice = gasPrice - this.gasLimit = gasLimit - - Object.keys(ethers.providers.JsonRpcSigner.prototype) - .filter(fn => !['constructor', 'sendTransaction'].includes(fn)) - .forEach(fn => { this[fn] = async (...args) => this._callSigner(fn, args) }) - } - - async sendTransaction(transaction) { - if (!transaction.gasLimit && this.gasLimit) transaction.gasLimit = bn(this.gasLimit).toHexString() - if (!transaction.gasPrice && this.gasPrice) transaction.gasPrice = bn(this.gasPrice).toHexString() - - const signer = await this._getSigner() - const tx = await signer.sendTransaction(transaction) - await tx.wait(1) - return tx - } - - async _callSigner(fn, args) { - const signer = await this._getSigner() - return signer[fn](...args) - } - - async _getSigner() { - if (!this.signer) this.signer = await this.provider.getSigner(this.address) - return this.signer - } -} - -module.exports = JsonRpcSigner diff --git a/packages/shared/src/models/providers/JsonRpcSigner.ts b/packages/shared/src/models/providers/JsonRpcSigner.ts new file mode 100644 index 0000000..1605aae --- /dev/null +++ b/packages/shared/src/models/providers/JsonRpcSigner.ts @@ -0,0 +1,42 @@ +import { ethers } from 'ethers' +import { TransactionRequest, TransactionResponse, JsonRpcSigner as SignerType } from 'ethers/providers' +import { bn, BigNumberish } from '../../helpers/numbers' + +export default class JsonRpcSigner extends ethers.Signer { + + address?: string + provider: ethers.providers.JsonRpcProvider + signer: ethers.providers.JsonRpcSigner + gasPrice?: BigNumberish + gasLimit?: BigNumberish + + constructor( + provider: ethers.providers.JsonRpcProvider, + address?: string, + { gasPrice, gasLimit }: { gasPrice?: BigNumberish, gasLimit?: BigNumberish } = {} + ) { + super() + this.address = address + this.provider = provider + this.gasPrice = gasPrice + this.gasLimit = gasLimit + this.provider = provider + this.signer = provider.getSigner(this.address) + } + + async sendTransaction(transaction: TransactionRequest): Promise { + if (!transaction.gasLimit && this.gasLimit) transaction.gasLimit = bn(this.gasLimit).toHexString() + if (!transaction.gasPrice && this.gasPrice) transaction.gasPrice = bn(this.gasPrice).toHexString() + const tx = await this.signer.sendTransaction(transaction) + await tx.wait(1) + return tx + } + + // need to assign methods through this.signer because ethers.providers.JsonRpcSigner cannot be extended directly + getAddress: SignerType['getAddress'] = async () => this.signer.getAddress() + getBalance: SignerType['getBalance'] = async (blockTag?) => this.signer.getBalance(blockTag) + getTransactionCount: SignerType['getTransactionCount'] = async (blockTag?) => this.signer.getTransactionCount(blockTag) + sendUncheckedTransaction: SignerType['sendUncheckedTransaction'] = async (transaction) => this.signer.sendUncheckedTransaction(transaction) + signMessage: SignerType['signMessage'] = async (message) => this.signer.signMessage(message) + unlock: SignerType['unlock'] = async (password) => this.signer.unlock(password) +} diff --git a/packages/shared/src/models/providers/Wallet.js b/packages/shared/src/models/providers/Wallet.js deleted file mode 100644 index b343697..0000000 --- a/packages/shared/src/models/providers/Wallet.js +++ /dev/null @@ -1,27 +0,0 @@ -const ethers = require('ethers') -const { bn } = require('../../helpers/numbers') -const GasPriceOracle = require('../../helpers/gas-price-oracle') - -class Wallet extends ethers.Wallet { - constructor(privateKey, provider, { gasPrice = undefined, gasLimit = undefined } = {}) { - super(privateKey, provider) - this.gasPrice = gasPrice - this.gasLimit = gasLimit - } - - async sendTransaction(transaction) { - if (!transaction.gasLimit && this.gasLimit) transaction.gasLimit = bn(this.gasLimit).toHexString() - if (!transaction.gasPrice && this.gasPrice) transaction.gasPrice = bn(await this.getGasPrice()).toHexString() - const tx = await super.sendTransaction(transaction) - await this.provider.waitForTransaction(tx.hash) - return tx - } - - async getGasPrice() { - const network = await this.provider.getNetwork() - const averageGasPrice = await GasPriceOracle.fetch(network.chainId) - return averageGasPrice || this.gasPrice - } -} - -module.exports = Wallet diff --git a/packages/shared/src/models/providers/Wallet.ts b/packages/shared/src/models/providers/Wallet.ts new file mode 100644 index 0000000..619a7d4 --- /dev/null +++ b/packages/shared/src/models/providers/Wallet.ts @@ -0,0 +1,41 @@ +import { ethers } from 'ethers' +import { TransactionRequest, TransactionResponse } from 'ethers/providers' +import { bn, BigNumberish } from '../../helpers/numbers' +import GasPriceOracle from '../../helpers/gas-price-oracle' + +type GasParams = { gasPrice?: BigNumberish, gasLimit?: BigNumberish } + +export default class Wallet extends ethers.Wallet { + + gasPrice?: BigNumberish + gasLimit?: BigNumberish + + constructor( + privateKey: string, + provider: ethers.providers.Provider, + { gasPrice, gasLimit }: GasParams = {} + ) { + super(privateKey, provider) + this.gasPrice = gasPrice + this.gasLimit = gasLimit + } + + async sendTransaction(transaction: TransactionRequest): Promise { + if (!transaction.gasLimit && this.gasLimit) transaction.gasLimit = bn(this.gasLimit).toHexString() + if (!transaction.gasPrice && this.gasPrice) transaction.gasPrice = bn(await this.getGasPrice()).toHexString() + const tx = await super.sendTransaction(transaction) + await this.provider.waitForTransaction(tx.hash!) + return tx + } + + async getGasPrice(): Promise { + const network = await this.provider.getNetwork() + const averageGasPrice = await GasPriceOracle.fetch(network.chainId) + return averageGasPrice || this.gasPrice as BigNumberish + } +} + +export { + Wallet, + GasParams +} diff --git a/packages/shared/src/types/contract-helpers-test.ts b/packages/shared/src/types/contract-helpers-test.ts new file mode 100644 index 0000000..000aee2 --- /dev/null +++ b/packages/shared/src/types/contract-helpers-test.ts @@ -0,0 +1,2 @@ +// empty module declaration causes all imports to be implicit any for missing modules +declare module '@aragon/contract-helpers-test' diff --git a/packages/shared/src/web3/Network.js b/packages/shared/src/web3/Network.js deleted file mode 100644 index ce5e343..0000000 --- a/packages/shared/src/web3/Network.js +++ /dev/null @@ -1,17 +0,0 @@ -import Environment from '../models/environments/LocalEnvironment' - -const Network = { - get environment() { - return new Environment() - }, - - async getProtocol() { - return this.environment.getProtocol() - }, - - async query(query) { - return this.environment.query(query) - }, -} - -export default Network diff --git a/packages/shared/src/web3/Network.ts b/packages/shared/src/web3/Network.ts new file mode 100644 index 0000000..82fb022 --- /dev/null +++ b/packages/shared/src/web3/Network.ts @@ -0,0 +1,17 @@ +import { LocalEnvironment, Protocol } from '../models/environments/LocalEnvironment' + +const Network = { + get environment() { + return new LocalEnvironment() + }, + + async getProtocol(): Promise { + return this.environment.getProtocol() + }, + + async query(query: string): Promise { + return this.environment.query(query) + }, +} + +export default Network diff --git a/yarn.lock b/yarn.lock index d3a090b..b6aa610 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2948,6 +2948,11 @@ resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.14.tgz#44d2dd0b5de6185089375d976b4ec5caf6861193" integrity sha512-G+ITQPXkwTrslfG5L/BksmbLUA0M1iybEsmCWPqzSxsRRhJZimBKJkoMi8fr/CPygPTj4zO5pJH7I2/cm9M7SQ== +"@types/configstore@^4.0.0": + version "4.0.0" + resolved "https://testnet.thegraph.com/npm-registry/@types%2fconfigstore/-/configstore-4.0.0.tgz#cb718f9507e9ee73782f40d07aaca1cd747e36fa" + integrity sha512-SvCBBPzOIe/3Tu7jTl2Q8NjITjLmq9m7obzjSyb8PXWWZ31xVK6w4T6v8fOx+lrgQnqk3Yxc00LDolFsSakKCA== + "@types/connect@*": version "3.4.33" resolved "https://testnet.thegraph.com/npm-registry/@types%2fconnect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546"