diff --git a/.gitignore b/.gitignore index 38dd6d37..e9f6e562 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ coverage coverage.json out.log .GckmsOverride.js +.GckmsOverride.ts docs modules ui diff --git a/packages/common/browser.js b/packages/common/browser.js deleted file mode 100644 index b9c6b3c9..00000000 --- a/packages/common/browser.js +++ /dev/null @@ -1,24 +0,0 @@ -// Only the browser-safe modules. -module.exports = { - ...require("./src/AbiUtils"), - ...require("./src/AdminUtils"), - ...require("./src/Constants"), - ...require("./src/ContractUtils"), - ...require("./src/TransactionUtils"), - ...require("./src/Crypto"), - ...require("./src/EmpUtils"), - ...require("./src/EncryptionHelper"), - ...require("./src/Enums"), - ...require("./src/FormattingUtils"), - ...require("./src/ObjectUtils"), - ...require("./src/PublicNetworks"), - ...require("./src/Random"), - ...require("./src/SolcoverConfig"), - ...require("./src/SolidityTestUtils"), - ...require("./src/TimeUtils"), - ...require("./src/VotingUtils"), - ...require("./src/PriceIdentifierUtils"), - ...require("./src/MultiDecimalTestHelper"), - ...require("./src/MultiVersionTestHelpers"), - ...require("./src/hardhat/fixtures"), -}; diff --git a/packages/common/index.js b/packages/common/index.js deleted file mode 100644 index 9b74ae17..00000000 --- a/packages/common/index.js +++ /dev/null @@ -1,15 +0,0 @@ -const browserSafe = { ...require("./browser.js") }; - -const browserUnsafe = { - ...require("./src/gckms/ManagedSecretProvider"), - ...require("./src/MetaMaskTruffleProvider"), - ...require("./src/MigrationUtils"), - ...require("./src/TruffleConfig"), - ...require("./src/ProviderUtils"), - ...require("./src/HardhatConfig"), - ...require("./src/RetryProvider"), - ...require("./src/UniswapV3Helpers"), - ...require("./src/FileHelpers"), -}; - -module.exports = { ...browserSafe, ...browserUnsafe }; diff --git a/packages/common/package.json b/packages/common/package.json index a460fad2..f20631b2 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -4,9 +4,9 @@ "description": "Common js utilities used by other UMA packages", "homepage": "http://umaproject.org", "license": "AGPL-3.0-or-later", - "main": "index.js", + "main": "./dist/index.js", "browser": { - "./index.js": "./browser.js" + "./index.js": "./dist/browser.js" }, "types": "./dist/index.d.ts", "publishConfig": { @@ -25,14 +25,12 @@ "url": "https://github.com/UMAprotocol/protocol/issues" }, "files": [ - "/src/**/*.js", - "/dist/**/*", - "browser.js" + "/dist/**/*" ], "dependencies": { "@ethersproject/bignumber": "^5.0.5", - "@google-cloud/kms": "^0.3.0", - "@google-cloud/storage": "^2.4.2", + "@google-cloud/kms": "^2.3.1", + "@google-cloud/storage": "^5.8.5", "@maticnetwork/maticjs": "^2.0.40", "@truffle/hdwallet-provider": "^1.2.3", "@umaprotocol/truffle-ledger-provider": "^1.0.5", @@ -40,7 +38,7 @@ "abi-decoder": "github:UMAprotocol/abi-decoder", "bignumber.js": "^8.0.1", "chalk-pipe": "^3.0.0", - "dotenv": "^6.2.0", + "dotenv": "^9.0.0", "eth-crypto": "^1.7.0", "hardhat": "^2.5.0", "hardhat-deploy": "^0.8.11", @@ -64,6 +62,9 @@ "@tsconfig/node14": "^1.0.0", "@types/ethereum-protocol": "^1.0.0", "@types/mocha": "^5.2.7", - "ethers": "^5.4.2" + "web3-core": "^1.5.0", + "web3-eth-contract": "^1.5.0", + "ethers": "^5.4.2", + "@types/lodash.uniqby": "^4.7.6" } } diff --git a/packages/common/src/AbiUtils.js b/packages/common/src/AbiUtils.ts similarity index 60% rename from packages/common/src/AbiUtils.js rename to packages/common/src/AbiUtils.ts index a0d27644..338cb6fb 100644 --- a/packages/common/src/AbiUtils.js +++ b/packages/common/src/AbiUtils.ts @@ -6,25 +6,39 @@ // pulled from the core/build/contracts directory. Example usage: // getAbiDecoder().decodeMethod(data); // This decodes the txn data into the function name and arguments. -const abiDecoder = require("abi-decoder"); +import abiDecoder from "abi-decoder"; -function importAll(r) { +export type AbiDecoder = typeof abiDecoder; + +interface Context { + keys: () => string[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (input: string): any; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function importAll(r: Context): any[] { return r.keys().map(r); } -function getAllContracts() { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getAllContracts(): any[] { let importedObjects; // Note: we use a try here because we don't want to install the require-context package in node.js contexts where // it won't work. - if (process.browser) { + const castedProcess = (process as unknown) as { browser: true }; + if (castedProcess.browser) { // This only works in webpack. // eslint-disable-next-line no-unused-vars - const requireContext = require("require-context"); + require("require-context"); // Note: all arguments must be hardcoded here for webpack to bundle the files correctly. // This line also generates a few build warnings that should be ignored. - const contractContext = require.context("@uma/core/build/contracts/", true, /\.json$/); + const castedRequire = (require as unknown) as { + context: (path: string, useSubdirectories: boolean, regex: RegExp) => Context; + }; + const contractContext = castedRequire.context("@uma/core/build/contracts/", true, /\.json$/); importedObjects = importAll(contractContext); } else { @@ -34,8 +48,8 @@ function getAllContracts() { const packageDir = path.dirname(require.resolve("@uma/core/package.json")); const contractsPath = path.join(packageDir, "build/contracts/"); - const fileList = fs.readdirSync(contractsPath).filter((name) => name.match(/\.json$/)); - importedObjects = fileList.map((filename) => { + const fileList = fs.readdirSync(contractsPath).filter((name: string) => name.match(/\.json$/)); + importedObjects = fileList.map((filename: string) => { const fileContents = fs.readFileSync(path.join(contractsPath, filename)); return JSON.parse(fileContents); }); @@ -44,7 +58,7 @@ function getAllContracts() { return importedObjects; } -function getAbiDecoder() { +export function getAbiDecoder(): typeof abiDecoder { const contracts = getAllContracts(); for (const contract of contracts) { abiDecoder.addABI(contract.abi); @@ -52,5 +66,3 @@ function getAbiDecoder() { return abiDecoder; } - -module.exports = { getAllContracts, getAbiDecoder }; diff --git a/packages/common/src/AdminUtils.js b/packages/common/src/AdminUtils.ts similarity index 78% rename from packages/common/src/AdminUtils.js rename to packages/common/src/AdminUtils.ts index 65dccb3c..18b75a77 100644 --- a/packages/common/src/AdminUtils.js +++ b/packages/common/src/AdminUtils.ts @@ -1,8 +1,15 @@ -const { getAbiDecoder } = require("./AbiUtils.js"); +import { getAbiDecoder, AbiDecoder } from "./AbiUtils"; +import type { BN } from "./types"; -let abiDecoder; +let abiDecoder: AbiDecoder; -function decodeTransaction(transaction) { +interface Transaction { + data?: string; + to: string; + value: string | BN; +} + +export function decodeTransaction(transaction: Transaction): string { let returnValue = ""; // Give to and value. @@ -33,17 +40,17 @@ function decodeTransaction(transaction) { const adminPrefix = "Admin "; -function isAdminRequest(identifierUtf8) { +export function isAdminRequest(identifierUtf8: string): boolean { return identifierUtf8.startsWith(adminPrefix); } // Assumes that `identifierUtf8` is an admin request, i.e., `isAdminRequest()` returns true for it. -function getAdminRequestId(identifierUtf8) { +export function getAdminRequestId(identifierUtf8: string): number { return parseInt(identifierUtf8.slice(adminPrefix.length), 10); } // Vote 1 for Yes, 0 for No. Any vote > 0 is technically a Yes, but the 1 is treated as the canonical yes. -const translateAdminVote = (voteValue) => { +export const translateAdminVote = (voteValue: string): string => { if (!voteValue) { return "No Vote"; } else { @@ -61,5 +68,3 @@ const translateAdminVote = (voteValue) => { } } }; - -module.exports = { decodeTransaction, isAdminRequest, getAdminRequestId, translateAdminVote }; diff --git a/packages/common/src/Constants.js b/packages/common/src/Constants.js deleted file mode 100644 index b7f6a4fc..00000000 --- a/packages/common/src/Constants.js +++ /dev/null @@ -1,47 +0,0 @@ -// The interface names that Finder.sol uses to refer to interfaces in the UMA system. -const interfaceName = { - FinancialContractsAdmin: "FinancialContractsAdmin", - Oracle: "Oracle", - Registry: "Registry", - Store: "Store", - IdentifierWhitelist: "IdentifierWhitelist", - CollateralWhitelist: "CollateralWhitelist", - AddressWhitelist: "CollateralWhitelist", - OptimisticOracle: "OptimisticOracle", - Bridge: "Bridge", - GenericHandler: "GenericHandler", - MockOracleAncillary: "Oracle", - SinkOracle: "Oracle", -}; - -module.exports = { - // These enforce the maximum number of transactions that can fit within one batch-commit and batch-reveal. - // Based off the current gas limit from Etherscan over the last 6 months of 9950000, - // the following maximum batchCommit, batchReveal and retrieveRewards are possible: - // - batchCommit: 28 commits, 6654676 gas used - // - batchReveal: 58 commits, 5828051 gas used - // - retrieveRewards: 129 commits, 3344083 gas used - // Practically, we set a safe upper bound of 25 batch commits & reveals and 100 retrievals. - BATCH_MAX_COMMITS: 25, - BATCH_MAX_REVEALS: 25, - BATCH_MAX_RETRIEVALS: 100, - - // maximum uint256 value: 2^256 - 1 - MAX_UINT_VAL: "115792089237316195423570985008687907853269984665640564039457584007913129639935", - - // maximum allowance allowed by certain ERC20's like UNI: 2^96 - 1 - MAX_SAFE_ALLOWANCE: "79228162514264337593543950335", - - // Max integer that can be safely stored in a vanilla js int. - MAX_SAFE_JS_INT: 2147483647, - - // 0x0 contract address - ZERO_ADDRESS: "0x0000000000000000000000000000000000000000", - - // Names of different interfaces in the Finder. - interfaceName, - - // Block number of first EMP created. - // https://etherscan.io/tx/0x741ccbf0f9655b0b71e3842d788d58770bd3eb80c8f5bdf4fdec7cd74a776ea3 - UMA_FIRST_EMP_BLOCK: 10103723, -}; diff --git a/packages/common/src/Constants.ts b/packages/common/src/Constants.ts new file mode 100644 index 00000000..37aa03f3 --- /dev/null +++ b/packages/common/src/Constants.ts @@ -0,0 +1,42 @@ +// The interface names that Finder.sol uses to refer to interfaces in the UMA system. +export const interfaceName = { + FinancialContractsAdmin: "FinancialContractsAdmin", + Oracle: "Oracle", + Registry: "Registry", + Store: "Store", + IdentifierWhitelist: "IdentifierWhitelist", + CollateralWhitelist: "CollateralWhitelist", + AddressWhitelist: "CollateralWhitelist", + OptimisticOracle: "OptimisticOracle", + Bridge: "Bridge", + GenericHandler: "GenericHandler", + MockOracleAncillary: "Oracle", + SinkOracle: "Oracle", +}; + +// These enforce the maximum number of transactions that can fit within one batch-commit and batch-reveal. +// Based off the current gas limit from Etherscan over the last 6 months of 9950000, +// the following maximum batchCommit, batchReveal and retrieveRewards are possible: +// - batchCommit: 28 commits, 6654676 gas used +// - batchReveal: 58 commits, 5828051 gas used +// - retrieveRewards: 129 commits, 3344083 gas used +// Practically, we set a safe upper bound of 25 batch commits & reveals and 100 retrievals. +export const BATCH_MAX_COMMITS = 25; +export const BATCH_MAX_REVEALS = 25; +export const BATCH_MAX_RETRIEVALS = 100; + +// maximum uint256 value: 2^256 - 1 +export const MAX_UINT_VAL = "115792089237316195423570985008687907853269984665640564039457584007913129639935"; + +// maximum allowance allowed by certain ERC20's like UNI: 2^96 - 1 +export const MAX_SAFE_ALLOWANCE = "79228162514264337593543950335"; + +// Max integer that can be safely stored in a vanilla js int. +export const MAX_SAFE_JS_INT = 2147483647; + +// 0x0 contract address +export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + +// Block number of first EMP created. +// https://etherscan.io/tx/0x741ccbf0f9655b0b71e3842d788d58770bd3eb80c8f5bdf4fdec7cd74a776ea3 +export const UMA_FIRST_EMP_BLOCK = 10103723; diff --git a/packages/common/src/ContractUtils.js b/packages/common/src/ContractUtils.js deleted file mode 100644 index da39ac2a..00000000 --- a/packages/common/src/ContractUtils.js +++ /dev/null @@ -1,55 +0,0 @@ -const truffleContract = require("@truffle/contract"); - -/** - * This is a hack to handle reverts for view/pure functions that don't actually revert on public networks. - * See https://forum.openzeppelin.com/t/require-in-view-pure-functions-dont-revert-on-public-networks/1211 for more - * info. - * @param {Object} result Return value from calling a contract's view-only method. - * @return null if the call reverted or the view method's result. - */ -const revertWrapper = (result) => { - if (!result) { - return null; - } - let revertValue = "3963877391197344453575983046348115674221700746820753546331534351508065746944"; - if (result.toString() === revertValue) { - return null; - } - const isObject = (obj) => { - return obj === Object(obj); - }; - if (isObject(result)) { - // Iterate over the properties of the object and see if any match the revert value. - for (let prop in result) { - if (result[prop] && result[prop].toString() === revertValue) { - return null; - } - } - } - return result; -}; - -/** - * create a truffle contract from a json object, usually read in from an artifact. - * @param {*} contractJsonObject json object representing a contract. - * @param {Object} web3 instance. In unit tests this is globally accessable but when used in production needs injection. - * @returns truffle contract instance - */ -const createContractObjectFromJson = (contractJsonObject, _web3 = web3) => { - let truffleContractCreator = truffleContract(contractJsonObject); - truffleContractCreator.setProvider(_web3.currentProvider); - return truffleContractCreator; -}; -/** - * Helper to enable enables library linking on artifacts that were not compiled within this repo, such as artifacts - * produced by an external project. Can also be useful if the artifact was compiled using ethers. - * @param {object} artifact representing the compiled contract instance. - * @param {string} libraryName to be found and replaced within the artifact. - * @returns - */ -const replaceLibraryBindingReferenceInArtitifact = (artifact, libraryName) => { - const artifactString = JSON.stringify(artifact); - return JSON.parse(artifactString.replace(/\$.*\$/g, libraryName)); -}; - -module.exports = { revertWrapper, createContractObjectFromJson, replaceLibraryBindingReferenceInArtitifact }; diff --git a/packages/common/src/ContractUtils.ts b/packages/common/src/ContractUtils.ts new file mode 100644 index 00000000..ea525831 --- /dev/null +++ b/packages/common/src/ContractUtils.ts @@ -0,0 +1,81 @@ +// The types in this package are broken, so we have to require it. +const contractConstructor_ = require("@truffle/contract"); +import type truffleContract_ from "@truffle/contract"; +import type { BN } from "./types"; +import Web3 from "web3"; +import type { provider as Provider } from "web3-core"; + +// Truffle library types aren't specified correctly. Cast and modify to correct for this. +export interface TruffleInstance { + [prop: string]: any; +} +export interface TruffleContract extends truffleContract_.Contract { + setProvider: (provider: Provider) => void; + at: (address: string) => Promise; + deployed: () => Promise; + new: (...args: any[]) => Promise; + link: (arg: any) => TruffleContract; + detectNetwork: () => Promise; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const truffleContract = contractConstructor_ as (artifact: any) => TruffleContract; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type CallResult = string | BN | { [key: string]: any }; + +function isBN(input: CallResult): input is BN { + return input?.constructor?.name === "BN"; +} + +/** + * This is a hack to handle reverts for view/pure functions that don't actually revert on public networks. + * See https://forum.openzeppelin.com/t/require-in-view-pure-functions-dont-revert-on-public-networks/1211 for more + * info. + * @param {Object} result Return value from calling a contract's view-only method. + * @return null if the call reverted or the view method's result. + */ +export const revertWrapper = (result: CallResult): null | CallResult => { + if (!result) { + return null; + } + const revertValue = "3963877391197344453575983046348115674221700746820753546331534351508065746944"; + if (result.toString() === revertValue) { + return null; + } + + if (typeof result !== "string" && !isBN(result)) { + // Iterate over the properties of the object and see if any match the revert value. + for (const prop in result) { + if (!(prop in result) && result[prop].toString() === revertValue) { + return null; + } + } + } + return result; +}; + +/** + * create a truffle contract from a json object, usually read in from an artifact. + * @param {*} contractJsonObject json object representing a contract. + * @param {Object} web3 instance. In unit tests this is globally accessable but when used in production needs injection. + * @returns truffle contract instance + */ +export const createContractObjectFromJson = ( + contractJsonObject: { [key: string]: any }, + _web3 = ((global as unknown) as { web3: Web3 }).web3 +): TruffleContract => { + const truffleContractCreator = truffleContract(contractJsonObject); + truffleContractCreator.setProvider(_web3.currentProvider); + return truffleContractCreator; +}; +/** + * Helper to enable enables library linking on artifacts that were not compiled within this repo, such as artifacts + * produced by an external project. Can also be useful if the artifact was compiled using ethers. + * @param {object} artifact representing the compiled contract instance. + * @param {string} libraryName to be found and replaced within the artifact. + */ +export const replaceLibraryBindingReferenceInArtitifact = (artifact: T, libraryName: string): T => { + const artifactString = JSON.stringify(artifact); + return JSON.parse(artifactString.replace(/\$.*\$/g, libraryName)); +}; diff --git a/packages/common/src/Crypto.js b/packages/common/src/Crypto.ts similarity index 68% rename from packages/common/src/Crypto.js rename to packages/common/src/Crypto.ts index 8328a8ea..7a60aa9a 100644 --- a/packages/common/src/Crypto.js +++ b/packages/common/src/Crypto.ts @@ -1,45 +1,63 @@ -const EthCrypto = require("eth-crypto"); +import EthCrypto from "eth-crypto"; +import type Web3 from "web3"; +import assert from "assert"; + +interface KeyPair { + privateKey: string; + publicKey: string; +} // Encrypts a message using an ethereum public key. To decrypt messages that are encrypted with this method, use // decryptMessage(). -async function encryptMessage(publicKey, message) { +export async function encryptMessage(publicKey: string, message: string): Promise { // substr(2) removes the web3 friendly "0x" from the public key. const encryptedMessageObject = await EthCrypto.encryptWithPublicKey(publicKey.substr(2), message); return "0x" + EthCrypto.cipher.stringify(encryptedMessageObject); } // Converts an ethereum public key to the corresponding address. -function addressFromPublicKey(publicKey) { +export function addressFromPublicKey(publicKey: string): string { // substr(2) just removes the web3 friendly "0x" from the public key. return EthCrypto.publicKey.toAddress(publicKey.substr(2)); } // Recovers a public key from a private key. -function recoverPublicKey(privateKey) { +export function recoverPublicKey(privateKey: string): string { // The "0x" is added to make the public key web3 friendly. return "0x" + EthCrypto.publicKeyByPrivateKey(privateKey); } // Decrypts a message that was encrypted using encryptMessage(). -async function decryptMessage(privKey, encryptedMessage) { +export async function decryptMessage(privKey: string, encryptedMessage: string): Promise { // substr(2) just removes the 0x at the beginning. parse() reverses the stringify() in encryptMessage(). const encryptedMessageObject = EthCrypto.cipher.parse(encryptedMessage.substr(2)); return await EthCrypto.decryptWithPrivateKey(privKey, encryptedMessageObject); } // Derives a private key from a signature. -async function deriveKeyPair(web3, signature) { - const privateKey = web3.utils.soliditySha3(signature).substr(2); +export async function deriveKeyPair(web3: Web3, signature: string): Promise { + const hashOutput = web3.utils.soliditySha3(signature); + assert(hashOutput, "hash returned null"); + const privateKey = hashOutput.substr(2); const publicKey = recoverPublicKey(privateKey); return { publicKey, privateKey }; } // The methods to get signatures in MetaMask and Truffle are different and return slightly different results. -async function getMessageSignatureMetamask(web3, messageToSign, signingAccount) { - return await web3.eth.personal.sign(messageToSign, signingAccount); +export async function getMessageSignatureMetamask( + web3: Web3, + messageToSign: string, + signingAccount: string +): Promise { + // Note: web3 types seem to erroneously assume that a third argument is required, but it is not, so pass undefined and cast to string. + return await web3.eth.personal.sign(messageToSign, signingAccount, (undefined as unknown) as string); } -async function getMessageSignatureTruffle(web3, messageToSign, signingAccount) { +export async function getMessageSignatureTruffle( + web3: Web3, + messageToSign: string, + signingAccount: string +): Promise { const signature = await web3.eth.sign(messageToSign, signingAccount); // The 65 byte signature consists of r (first 32 bytes), s (next 32 bytes), and v (final byte). 65 bytes is // represented as 130 hex digits and the initial "0x". In order to produce a consistent signature with Metamask, we @@ -59,19 +77,28 @@ async function getMessageSignatureTruffle(web3, messageToSign, signingAccount) { // messages. // Derive a private key, that works *only* on Metamask. -async function deriveKeyPairFromSignatureMetamask(web3, messageToSign, signingAccount) { +export async function deriveKeyPairFromSignatureMetamask( + web3: Web3, + messageToSign: string, + signingAccount: string +): Promise { return deriveKeyPair(web3, await getMessageSignatureMetamask(web3, messageToSign, signingAccount)); } // Derive a private key, that works *only* with Truffle. -async function deriveKeyPairFromSignatureTruffle(web3, messageToSign, signingAccount) { +export async function deriveKeyPairFromSignatureTruffle( + web3: Web3, + messageToSign: string, + signingAccount: string +): Promise { return deriveKeyPair(web3, await getMessageSignatureTruffle(web3, messageToSign, signingAccount)); } // Signs a message in a way where it can be verified onchain by the openzeppelin ECDSA library. -async function signMessage(web3, message, account) { +export async function signMessage(web3: Web3, message: string, account: string): Promise { // Must hash the inner message because Solidity requires a fixed length message to verify a signature. const innerMessageHash = await web3.utils.soliditySha3(message); + assert(innerMessageHash, "innerMessageHash is null"); // Construct a signature that will be accepted by openzeppelin. // See https://github.com/OpenZeppelin/openzeppelin-solidity/blob/1e584e495782ebdb5096fe65037d99dae1cbe940/contracts/cryptography/ECDSA.sol#L53 @@ -86,15 +113,3 @@ async function signMessage(web3, message, account) { } return rs + v; } - -module.exports = { - encryptMessage, - addressFromPublicKey, - decryptMessage, - recoverPublicKey, - deriveKeyPairFromSignatureTruffle, - deriveKeyPairFromSignatureMetamask, - getMessageSignatureTruffle, - getMessageSignatureMetamask, - signMessage, -}; diff --git a/packages/common/src/EmpUtils.js b/packages/common/src/EmpUtils.ts similarity index 78% rename from packages/common/src/EmpUtils.js rename to packages/common/src/EmpUtils.ts index 642272d4..b2417925 100644 --- a/packages/common/src/EmpUtils.js +++ b/packages/common/src/EmpUtils.ts @@ -1,3 +1,6 @@ +import type { BN } from "./types"; +import type Web3 from "web3"; + /** * Get the collateralization ratio for an EMP position. * Note: all inputs and outputs are expressed as fixed-point (scaled by 1e18) BNs. @@ -7,7 +10,7 @@ * @param {BN} number of tokens that have been borrowed/minted against the position * @return {BN} collateralization ratio. */ -function computeCollateralizationRatio(web3, tokenPrice, collateral, tokensOutstanding) { +export function computeCollateralizationRatio(web3: Web3, tokenPrice: BN, collateral: BN, tokensOutstanding: BN): BN { const { toWei, toBN } = web3.utils; const fixedPointScalingFactor = toBN(toWei("1")); if (tokensOutstanding.isZero() || tokenPrice.isZero()) { @@ -15,5 +18,3 @@ function computeCollateralizationRatio(web3, tokenPrice, collateral, tokensOutst } return collateral.mul(fixedPointScalingFactor).mul(fixedPointScalingFactor).div(tokensOutstanding).div(tokenPrice); } - -module.exports = { computeCollateralizationRatio }; diff --git a/packages/common/src/EncryptionHelper.js b/packages/common/src/EncryptionHelper.ts similarity index 50% rename from packages/common/src/EncryptionHelper.js rename to packages/common/src/EncryptionHelper.ts index 940431c8..4785f972 100644 --- a/packages/common/src/EncryptionHelper.js +++ b/packages/common/src/EncryptionHelper.ts @@ -1,19 +1,35 @@ -const web3 = require("web3"); +import Web3 from "web3"; +import type { BN } from "./types"; + +interface TopicHashRequest { + identifier: string; + time: string | BN; +} // Web3's soliditySha3 will attempt to auto-detect the type of given input parameters, // but this won't produce expected behavior for certain types such as `bytes32` or `address`. // Therefore, these helper methods will explicitly set types. - -function computeTopicHash(request, roundId) { - return web3.utils.soliditySha3( +export function computeTopicHash(request: TopicHashRequest, roundId: number | string): string { + const hash = Web3.utils.soliditySha3( { t: "bytes32", v: request.identifier }, { t: "uint", v: request.time }, { t: "uint", v: roundId } ); + if (hash === null) throw new Error("Returned null hash."); + return hash; } -function computeVoteHash(request) { - return web3.utils.soliditySha3( +interface VoteHashRequest { + price: string | BN; + salt: string | BN; + account: string; + time: string | BN | number; + roundId: string | BN | number; + identifier: string; +} + +export function computeVoteHash(request: VoteHashRequest): string { + const hash = Web3.utils.soliditySha3( { t: "int", v: request.price }, { t: "int", v: request.salt }, { t: "address", v: request.account }, @@ -22,10 +38,16 @@ function computeVoteHash(request) { { t: "uint", v: request.roundId }, { t: "bytes32", v: request.identifier } ); + if (hash === null) throw new Error("Returned null hash."); + return hash; } -function computeVoteHashAncillary(request) { - return web3.utils.soliditySha3( +interface VoteHashAncillaryRequest extends VoteHashRequest { + ancillaryData: string; +} + +export function computeVoteHashAncillary(request: VoteHashAncillaryRequest): string { + const hash = Web3.utils.soliditySha3( { t: "int", v: request.price }, { t: "int", v: request.salt }, { t: "address", v: request.account }, @@ -34,11 +56,11 @@ function computeVoteHashAncillary(request) { { t: "uint", v: request.roundId }, { t: "bytes32", v: request.identifier } ); + if (hash === null) throw new Error("Returned null hash."); + return hash; } -function getKeyGenMessage(roundId) { +export function getKeyGenMessage(roundId: number | string): string { // TODO: discuss dApp tradeoffs for changing this to a per-topic hash keypair. return `UMA Protocol one time key for round: ${roundId.toString()}`; } - -module.exports = { computeTopicHash, computeVoteHash, computeVoteHashAncillary, getKeyGenMessage }; diff --git a/packages/common/src/Enums.js b/packages/common/src/Enums.js deleted file mode 100644 index ba4f37d4..00000000 --- a/packages/common/src/Enums.js +++ /dev/null @@ -1,64 +0,0 @@ -// Corresponds to Registry.Roles. -const RegistryRolesEnum = { OWNER: "0", CONTRACT_CREATOR: "1" }; - -const TokenRolesEnum = { OWNER: "0", MINTER: "1", BURNER: "3" }; - -// Corresponds to VoteTiming.Phase. -const VotePhasesEnum = { COMMIT: "0", REVEAL: "1" }; - -// States for an EMP's Liquidation to be in. -const LiquidationStatesEnum = { - UNINITIALIZED: "0", - PRE_DISPUTE: "1", - PENDING_DISPUTE: "2", - DISPUTE_SUCCEEDED: "3", - DISPUTE_FAILED: "4", -}; - -// Maps the `liquidationStatus` property in the `LiquidationWithdrawn` event to human readable statuses. -// Note that these are status translations AFTER a withdrawLiquidation method is called -const PostWithdrawLiquidationRewardsStatusTranslations = { - 0: "Uninitialized", - 1: "NotDisputed", - 2: "Disputed", - 3: "DisputeSucceeded", - 4: "DisputeFailed", -}; - -// States for an EMP's Position to be in. -const PositionStatesEnum = { OPEN: "0", EXPIRED_PRICE_REQUESTED: "1", EXPIRED_PRICE_RECEIVED: "2" }; - -const PriceRequestStatusEnum = { NOT_REQUESTED: "0", ACTIVE: "1", RESOLVED: "2", FUTURE: "3" }; - -const OptimisticOracleRequestStatesEnum = { - INVALID: "0", - REQUESTED: "1", - PROPOSED: "2", - EXPIRED: "3", - DISPUTED: "4", - RESOLVED: "5", - SETTLED: "6", -}; - -const InsuredBridgeDepositStateEnum = { - UNINITIALIZED: "0", - PENDING_SLOW: "1", - PENDING_INSTANT: "2", - FINALIZED_SLOW: "3", - FINALIZED_INSTANT: "4", -}; - -const InsuredBridgeDepositTypeEnum = { SLOW: "0", INSTANT: "1" }; - -module.exports = { - RegistryRolesEnum, - TokenRolesEnum, - VotePhasesEnum, - LiquidationStatesEnum, - PostWithdrawLiquidationRewardsStatusTranslations, - PositionStatesEnum, - PriceRequestStatusEnum, - OptimisticOracleRequestStatesEnum, - InsuredBridgeDepositStateEnum, - InsuredBridgeDepositTypeEnum, -}; diff --git a/packages/common/src/Enums.ts b/packages/common/src/Enums.ts new file mode 100644 index 00000000..2eac1134 --- /dev/null +++ b/packages/common/src/Enums.ts @@ -0,0 +1,51 @@ +// Corresponds to Registry.Roles. +export const RegistryRolesEnum = { OWNER: "0", CONTRACT_CREATOR: "1" }; + +export const TokenRolesEnum = { OWNER: "0", MINTER: "1", BURNER: "3" }; + +// Corresponds to VoteTiming.Phase. +export const VotePhasesEnum = { COMMIT: "0", REVEAL: "1" }; + +// States for an EMP's Liquidation to be in. +export const LiquidationStatesEnum = { + UNINITIALIZED: "0", + PRE_DISPUTE: "1", + PENDING_DISPUTE: "2", + DISPUTE_SUCCEEDED: "3", + DISPUTE_FAILED: "4", +}; + +// Maps the `liquidationStatus` property in the `LiquidationWithdrawn` event to human readable statuses. +// Note that these are status translations AFTER a withdrawLiquidation method is called +export const PostWithdrawLiquidationRewardsStatusTranslations = { + 0: "Uninitialized", + 1: "NotDisputed", + 2: "Disputed", + 3: "DisputeSucceeded", + 4: "DisputeFailed", +}; + +// States for an EMP's Position to be in. +export const PositionStatesEnum = { OPEN: "0", EXPIRED_PRICE_REQUESTED: "1", EXPIRED_PRICE_RECEIVED: "2" }; + +export const PriceRequestStatusEnum = { NOT_REQUESTED: "0", ACTIVE: "1", RESOLVED: "2", FUTURE: "3" }; + +export const OptimisticOracleRequestStatesEnum = { + INVALID: "0", + REQUESTED: "1", + PROPOSED: "2", + EXPIRED: "3", + DISPUTED: "4", + RESOLVED: "5", + SETTLED: "6", +}; + +export const InsuredBridgeDepositStateEnum = { + UNINITIALIZED: "0", + PENDING_SLOW: "1", + PENDING_INSTANT: "2", + FINALIZED_SLOW: "3", + FINALIZED_INSTANT: "4", +}; + +export const InsuredBridgeDepositTypeEnum = { SLOW: "0", INSTANT: "1" }; diff --git a/packages/common/src/FileHelpers.js b/packages/common/src/FileHelpers.ts similarity index 70% rename from packages/common/src/FileHelpers.js rename to packages/common/src/FileHelpers.ts index b794c442..3f489e5f 100644 --- a/packages/common/src/FileHelpers.js +++ b/packages/common/src/FileHelpers.ts @@ -1,11 +1,10 @@ -const fs = require("fs"); -const path = require("path"); +import fs from "fs"; +import path from "path"; +import type { Artifact } from "hardhat/types"; -function getAllFilesInPath(dirPath, arrayOfFiles = []) { +export function getAllFilesInPath(dirPath: string, arrayOfFiles: string[] = []): string[] { const files = fs.readdirSync(dirPath); - arrayOfFiles = arrayOfFiles || []; - files.forEach((file) => { if (fs.statSync(dirPath + "/" + file).isDirectory()) arrayOfFiles = getAllFilesInPath(dirPath + "/" + file, arrayOfFiles); @@ -15,7 +14,7 @@ function getAllFilesInPath(dirPath, arrayOfFiles = []) { return arrayOfFiles; } -function findArtifactFromPath(contractName, artifactsPath) { +export function findArtifactFromPath(contractName: string, artifactsPath: string): Artifact { const allArtifactsPaths = getAllFilesInPath(artifactsPath); const desiredArtifactPaths = allArtifactsPaths.filter((a) => a.endsWith(`/${contractName}.json`)); @@ -23,5 +22,3 @@ function findArtifactFromPath(contractName, artifactsPath) { throw new Error(`Couldn't find desired artifact or found too many for ${contractName}`); return JSON.parse(fs.readFileSync(desiredArtifactPaths[0], "utf-8")); } - -module.exports = { getAllFilesInPath, findArtifactFromPath }; diff --git a/packages/common/src/FormattingUtils.js b/packages/common/src/FormattingUtils.ts similarity index 66% rename from packages/common/src/FormattingUtils.js rename to packages/common/src/FormattingUtils.ts index fb092329..5b9788d1 100644 --- a/packages/common/src/FormattingUtils.js +++ b/packages/common/src/FormattingUtils.ts @@ -1,9 +1,11 @@ -const { PublicNetworks: networkUtils } = require("./PublicNetworks"); +import { PublicNetworks as networkUtils } from "./PublicNetworks"; +import Web3 from "web3"; +import type { BN } from "./types"; -const BigNumber = require("bignumber.js"); -const moment = require("moment"); -const assert = require("assert"); -const { formatFixed, parseFixed } = require("@ethersproject/bignumber"); +import BigNumber from "bignumber.js"; +import moment from "moment"; +import assert from "assert"; +export { formatFixed, parseFixed } from "@ethersproject/bignumber"; // Apply settings to BigNumber.js library. // Note: ROUNDING_MODE is set to round ceiling so we send at least enough collateral to create the requested tokens. @@ -13,47 +15,53 @@ const { formatFixed, parseFixed } = require("@ethersproject/bignumber"); BigNumber.set({ ROUNDING_MODE: 2, RANGE: 500, EXPONENTIAL_AT: 500 }); // Given a timestamp in seconds, returns the date in the format: "MM/DD/YYYY" -const formatDateShort = (timestampInSeconds) => { - const date = moment.unix(parseInt(Number(timestampInSeconds))); +export const formatDateShort = (timestampInSeconds: number): string => { + const date = moment.unix(Math.floor(timestampInSeconds)); return date.format("MM/DD/YYYY"); }; -const formatDate = (timestampInSeconds, web3) => { - return new Date(parseInt(web3.utils.toBN(timestampInSeconds).muln(1000).toString(), 10)).toString(); +export const formatDate = (timestampInSeconds: number): string => { + return new Date(Math.floor(timestampInSeconds * 1000)).toString(); }; -const formatHours = (seconds, decimals = 2) => { +export const formatHours = (seconds: number, decimals = 2): string => { // 3600 seconds in an hour. return (seconds / 3600).toFixed(decimals); }; // formatWei converts a string or BN instance from Wei to Ether, e.g., 1e19 -> 10. -const formatWei = (num, web3) => { +export const formatWei = (num: string | BN): string => { // Web3's `fromWei` function doesn't work on BN objects in minified mode (e.g., // `web3.utils.isBN(web3.utils.fromBN("5"))` is false), so we use a workaround where we always pass in strings. // See https://github.com/ethereum/web3.js/issues/1777. - return web3.utils.fromWei(num.toString()); + return Web3.utils.fromWei(num.toString()); }; // Formats the input to round to decimalPlaces number of decimals if the number has a magnitude larger than 1 and fixes // precision to minPrecision if the number has a magnitude less than 1. -const formatWithMaxDecimals = (num, decimalPlaces, minPrecision, roundUp, showSign) => { +export const formatWithMaxDecimals = ( + num: number | string, + decimalPlaces: number, + minPrecision: number, + roundUp: boolean, + showSign: boolean +): string => { if (roundUp) { BigNumber.set({ ROUNDING_MODE: BigNumber.ROUND_UP }); } else { BigNumber.set({ ROUNDING_MODE: BigNumber.ROUND_DOWN }); } - const fullPrecisionFloat = BigNumber(num); + const fullPrecisionFloat = new BigNumber(num); const positiveSign = showSign && fullPrecisionFloat.gt(0) ? "+" : ""; let fixedPrecisionFloat; // Convert back to BN to truncate any trailing 0s that the toFixed() output would print. If the number is equal to or larger than // 1 then truncate to `decimalPlaces` number of decimal places. EG 999.999 -> 999.99 with decimalPlaces=2 If the number // is less than 1 then truncate to minPrecision precision. EG: 0.0022183471 -> 0.002218 with minPrecision=4 - if (fullPrecisionFloat.abs().gte(BigNumber(1))) { - fixedPrecisionFloat = BigNumber(fullPrecisionFloat).toFixed(decimalPlaces).toString(); + if (fullPrecisionFloat.abs().gte(new BigNumber(1))) { + fixedPrecisionFloat = new BigNumber(fullPrecisionFloat).toFixed(decimalPlaces).toString(); } else { - fixedPrecisionFloat = BigNumber(fullPrecisionFloat).toPrecision(minPrecision).toString(); + fixedPrecisionFloat = new BigNumber(fullPrecisionFloat).toPrecision(minPrecision).toString(); } // This puts commas in the thousands places, but only before the decimal point. const fixedPrecisionFloatParts = fixedPrecisionFloat.split("."); @@ -61,10 +69,16 @@ const formatWithMaxDecimals = (num, decimalPlaces, minPrecision, roundUp, showSi return positiveSign + fixedPrecisionFloatParts.join("."); }; -const createFormatFunction = (web3, numDisplayedDecimals, minDisplayedPrecision, showSign = false, decimals = 18) => { - return (valInWei) => +export const createFormatFunction = ( + _web3: Web3 | undefined, // unused + numDisplayedDecimals: number, + minDisplayedPrecision: number, + showSign = false, + decimals = 18 +) => { + return (valInWei: string | BN): string => formatWithMaxDecimals( - formatWei(ConvertDecimals(decimals, 18, web3)(valInWei), web3), + formatWei(ConvertDecimals(decimals, 18)(valInWei)), numDisplayedDecimals, minDisplayedPrecision, false, @@ -72,8 +86,9 @@ const createFormatFunction = (web3, numDisplayedDecimals, minDisplayedPrecision, ); }; +type NetworkId = keyof typeof networkUtils; // Generate an etherscan link prefix. If a networkId is provided then the URL will point to this network. Else, assume mainnet. -function createEtherscanLinkFromtx(networkId) { +export function createEtherscanLinkFromtx(networkId: NetworkId): string { // Construct etherscan link based on network let url; if (networkUtils[networkId]) { @@ -88,22 +103,23 @@ function createEtherscanLinkFromtx(networkId) { // Convert either an address or transaction to a shorter version. // 0x772871a444c6e4e9903d8533a5a13101b74037158123e6709470f0afbf6e7d94 -> 0x7787...7d94 -function createShortHexString(hex) { +export function createShortHexString(hex: string): string { return hex.substring(0, 5) + "..." + hex.substring(hex.length - 6, hex.length - 1); } // Take in either a transaction or an account and generate an etherscan link for the corresponding // network formatted in markdown. -function createEtherscanLinkMarkdown(hex, networkId = 1) { +export function createEtherscanLinkMarkdown(hex: string, networkId: NetworkId = 1): string | null { if (hex.substring(0, 2) != "0x") return null; - let shortURLString = createShortHexString(hex); + const shortURLString = createShortHexString(hex); // Transaction hash if (hex.length == 66) return `<${createEtherscanLinkFromtx(networkId)}tx/${hex}|${shortURLString}>`; // Account else if (hex.length == 42) return `<${createEtherscanLinkFromtx(networkId)}address/${hex}|${shortURLString}>`; + return null; } -function addSign(number) { +export function addSign(number: string): string { if (Number(number) > 0) { return `+${number}`; } else { @@ -120,34 +136,17 @@ function addSign(number) { // toDecimals: number - decimal value to convert to // web3: web3 object to get a big number function. // return => (amount:string)=>BN -const ConvertDecimals = (fromDecimals, toDecimals, web3) => { +export const ConvertDecimals = (fromDecimals: number, toDecimals: number): ((amountIn: string | number | BN) => BN) => { assert(fromDecimals >= 0, "requires fromDecimals as an integer >= 0"); assert(toDecimals >= 0, "requires toDecimals as an integer >= 0"); - assert(web3, "requires web3 instance"); // amount: string, BN, number - integer amount in fromDecimals smallest unit that want to convert toDecimals // returns: BN with toDecimals in smallest unit - return (amount) => { - amount = web3.utils.toBN(amount); + return (amountIn: string | number | BN) => { + const amount = Web3.utils.toBN(amountIn.toString()); if (amount.isZero()) return amount; const diff = fromDecimals - toDecimals; if (diff == 0) return amount; - if (diff > 0) return amount.div(web3.utils.toBN("10").pow(web3.utils.toBN(diff.toString()))); - return amount.mul(web3.utils.toBN("10").pow(web3.utils.toBN((-1 * diff).toString()))); + if (diff > 0) return amount.div(Web3.utils.toBN("10").pow(Web3.utils.toBN(diff.toString()))); + return amount.mul(Web3.utils.toBN("10").pow(Web3.utils.toBN((-1 * diff).toString()))); }; }; - -module.exports = { - formatDateShort, - formatDate, - formatHours, - formatWei, - formatWithMaxDecimals, - createFormatFunction, - createEtherscanLinkFromtx, - createShortHexString, - createEtherscanLinkMarkdown, - addSign, - formatFixed, - parseFixed, - ConvertDecimals, -}; diff --git a/packages/common/src/HardhatConfig.js b/packages/common/src/HardhatConfig.ts similarity index 87% rename from packages/common/src/HardhatConfig.js rename to packages/common/src/HardhatConfig.ts index 85f016f1..b0ae9243 100644 --- a/packages/common/src/HardhatConfig.js +++ b/packages/common/src/HardhatConfig.ts @@ -1,7 +1,12 @@ +import { HardhatConfig } from "hardhat/types"; + const { getNodeUrl, mnemonic } = require("./TruffleConfig"); -// eslint-disable-next-line no-unused-vars -function getHardhatConfig(configOverrides, workingDir = "./", includeTruffle = true) { +export function getHardhatConfig( + configOverrides: any, + _workingDir = "./", + includeTruffle = true +): Partial { // Hardhat plugins. These are imported inside `getHardhatConfig` so that other packages importing this function // get access to the plugins as well. if (includeTruffle) require("@nomiclabs/hardhat-truffle5"); @@ -32,7 +37,7 @@ function getHardhatConfig(configOverrides, workingDir = "./", includeTruffle = t // Some tests should not be tested using hardhat. Define all tests that end with *e2e.js to be ignored. const testBlacklist = [".e2e.js"]; - const defaultConfig = { + const defaultConfig = ({ solidity: { compilers: [ { version: solcVersion, settings: { optimizer: { enabled: true, runs: 1000000 } } }, @@ -88,7 +93,7 @@ function getHardhatConfig(configOverrides, workingDir = "./", includeTruffle = t apiKey: process.env.ETHERSCAN_API_KEY, }, namedAccounts: { deployer: 0 }, - }; + } as unknown) as HardhatConfig; // Cast to allow extra properties. return { ...defaultConfig, ...configOverrides }; } @@ -96,10 +101,12 @@ function getHardhatConfig(configOverrides, workingDir = "./", includeTruffle = t // Helper method to let the user of HardhatConfig assign a global address which is then accessible from the @uma/core // getAddressTest method. This enables hardhat to be used in tests like the main index.js entry tests in the liquidator // disputer and monitor bots. In future, this should be refactored to use https://github.com/wighawag/hardhat-deploy -function addGlobalHardhatTestingAddress(contractName, address) { - if (!global.hardhatTestingAddresses) { - global.hardhatTestingAddresses = {}; +export function addGlobalHardhatTestingAddress(contractName: string, address: string): void { + const castedGlobal = (global as unknown) as { + hardhatTestingAddresses: undefined | { [contractName: string]: string }; + }; + if (!castedGlobal.hardhatTestingAddresses) { + castedGlobal.hardhatTestingAddresses = {}; } - global.hardhatTestingAddresses[contractName] = address; + castedGlobal.hardhatTestingAddresses[contractName] = address; } -module.exports = { getHardhatConfig, addGlobalHardhatTestingAddress }; diff --git a/packages/common/src/MetaMaskTruffleProvider.js b/packages/common/src/MetaMaskTruffleProvider.ts similarity index 66% rename from packages/common/src/MetaMaskTruffleProvider.js rename to packages/common/src/MetaMaskTruffleProvider.ts index f7a3f23f..32598262 100644 --- a/packages/common/src/MetaMaskTruffleProvider.js +++ b/packages/common/src/MetaMaskTruffleProvider.ts @@ -1,35 +1,37 @@ -const MetaMaskConnector = require("node-metamask"); -const argv = require("minimist")(process.argv.slice()); +/* eslint-disable @typescript-eslint/no-explicit-any */ +import MetaMaskConnector from "node-metamask"; +import minimist from "minimist"; +import type { AbstractProvider } from "web3-core"; +const argv = minimist(process.argv.slice()); + +type MetamaskProvider = ReturnType; // Wraps the MetaMask Connector enabling truffle to init a web3 provider and continue truffle execution until the // MetaMask connection has been established. This calls metaMask asynchronously while returning a provider synchronously. -class MetaMaskTruffleProvider { +export class MetaMaskTruffleProvider implements AbstractProvider { + public wrappedProvider: MetamaskProvider | null; + public wrappedProviderPromise: Promise; constructor() { this.wrappedProvider = null; this.wrappedProviderPromise = this.getOrConstructWrappedProvider(); } // Passes the call through, by attaching a callback to the wrapper provider promise. - sendAsync(...all) { + sendAsync(...all: Parameters): void { this.wrappedProviderPromise.then((wrappedProvider) => { wrappedProvider.sendAsync(...all); }); } // Passes the call through. Requires that the wrapped provider has been created via, e.g., `constructWrappedProvider`. - send(...all) { - this.wrappedProviderPromise.then((wrappedProvider) => { + send(...all: Parameters>): void { + this.wrappedProviderPromise.then((wrappedProvider: MetamaskProvider) => { wrappedProvider.send(...all); }); } - // Passes the call through. Requires that the wrapped provider has been created via, e.g., `constructWrappedProvider`. - getAddress(...all) { - return this.getWrappedProviderOrThrow().getAddress(...all); - } - // Returns the underlying wrapped provider. - getWrappedProviderOrThrow() { + getWrappedProviderOrThrow(): MetamaskProvider { if (this.wrappedProvider) { return this.wrappedProvider; } else { @@ -38,10 +40,12 @@ class MetaMaskTruffleProvider { } // Returns a Promise that resolves to the wrapped provider. - getOrConstructWrappedProvider() { + getOrConstructWrappedProvider(): Promise { // Only if the network is MetaMask should we init the wrapped provider. if (argv.network != "metamask") { - return; + new Promise(() => { + /* do nothing */ + }); } console.log( "Using MetaMask as your Truffle provider. To connect navigate your browser to http://localhost:3333 and sign into your account.\nAll transactions will be proceeded by your MetaMask wallet. Ensure that you do not switch your network during usage of the CLI utility." @@ -62,5 +66,3 @@ class MetaMaskTruffleProvider { }); } } - -module.exports = { MetaMaskTruffleProvider }; diff --git a/packages/common/src/MultiDecimalTestHelper.js b/packages/common/src/MultiDecimalTestHelper.ts similarity index 88% rename from packages/common/src/MultiDecimalTestHelper.js rename to packages/common/src/MultiDecimalTestHelper.ts index d40cc6eb..deab6276 100644 --- a/packages/common/src/MultiDecimalTestHelper.js +++ b/packages/common/src/MultiDecimalTestHelper.ts @@ -1,7 +1,7 @@ // Run the tests against 2 different kinds of token/synth decimal combinations: // 1) matching 18 collateral & 18 synthetic decimals with 18 decimals for price feed. // 3) matching 8 collateral & 8 synthetic decimals with 18 decimals for price feed. -const TEST_DECIMAL_COMBOS = [ +export const TEST_DECIMAL_COMBOS = [ { tokenSymbol: "WETH", tokenName: "Wrapped Ether", @@ -17,5 +17,3 @@ const TEST_DECIMAL_COMBOS = [ priceFeedDecimals: 18, }, ]; - -module.exports = { TEST_DECIMAL_COMBOS }; diff --git a/packages/common/src/MultiVersionTestHelpers.js b/packages/common/src/MultiVersionTestHelpers.ts similarity index 89% rename from packages/common/src/MultiVersionTestHelpers.js rename to packages/common/src/MultiVersionTestHelpers.ts index 0acec0ee..1aacf487 100644 --- a/packages/common/src/MultiVersionTestHelpers.js +++ b/packages/common/src/MultiVersionTestHelpers.ts @@ -1,23 +1,28 @@ -const web3 = require("web3"); -const lodash = require("lodash"); -const assert = require("assert"); +import Web3 from "web3"; +import lodash from "lodash"; +import assert from "assert"; +import { ZERO_ADDRESS } from "./Constants"; -const { toWei, utf8ToHex, padRight } = web3.utils; -const { ZERO_ADDRESS } = require("./Constants"); +const { toWei, utf8ToHex, padRight } = Web3.utils; // Versions that production bots support. -const SUPPORTED_CONTRACT_VERSIONS = [ +export const SUPPORTED_CONTRACT_VERSIONS = [ { contractType: "ExpiringMultiParty", contractVersion: "2.0.1" }, { contractType: "Perpetual", contractVersion: "2.0.1" }, ]; // Versions that unit tests will test against. Note we dont test anything less than 2.0.1 as all older contracts have // expired on mainnet. -const TESTED_CONTRACT_VERSIONS = [ +export const TESTED_CONTRACT_VERSIONS = [ { contractType: "ExpiringMultiParty", contractVersion: "2.0.1" }, { contractType: "Perpetual", contractVersion: "2.0.1" }, ]; +interface Version { + contractType: string; + contractVersion: string; +} + /** * Used in conjunction with versionedIt within tests, this method will return true if the currentTestIterationVersion * is part of the SUPPORTED_CONTRACT_VERSIONS and supportedVersions (or is any), else returns false. @@ -27,7 +32,11 @@ const TESTED_CONTRACT_VERSIONS = [ * @returns {bool} true of the current test iteration version is part of & supportedVersions & SUPPORTED_CONTRACT_VERSIONS * or any, false otherwise. */ -function runTestForVersion(supportedVersions, SUPPORTED_CONTRACT_VERSIONS, currentTestIterationVersion) { +export function runTestForVersion( + supportedVersions: Version[], + SUPPORTED_CONTRACT_VERSIONS: Version[], + currentTestIterationVersion: Version +): boolean { // Validate that the array of supportedVersions provided is in the SUPPORTED_CONTRACT_VERSIONS OR is `any`. const supportedVersionOverlap = lodash.intersectionBy( supportedVersions, @@ -59,11 +68,11 @@ function runTestForVersion(supportedVersions, SUPPORTED_CONTRACT_VERSIONS, curre * @param {Object} overrideConstructorParams optional override for the constructor params generated. * @returns {Object} version compatible constructor parameters. */ -async function createConstructorParamsForContractVersion( - contractVersion, - contextObjects, +export async function createConstructorParamsForContractVersion( + contractVersion: Version, + contextObjects: any, overrideConstructorParams = {} -) { +): Promise { assert( contractVersion && contractVersion.contractVersion && contractVersion.contractType, "contractVersion must be provided, containing both a contract version and type" @@ -93,7 +102,7 @@ async function createConstructorParamsForContractVersion( (await contextObjects.timer.getCurrentTime?.())?.toNumber() || parseInt(await contextObjects.timer.methods.getCurrentTime().call()); - let constructorParams = { + const constructorParams: any = { expirationTimestamp: currentTime + 100000, withdrawalLiveness: "1000", collateralAddress: contextObjects.collateralToken.address || contextObjects.collateralToken.options.address, diff --git a/packages/common/src/ObjectUtils.js b/packages/common/src/ObjectUtils.ts similarity index 88% rename from packages/common/src/ObjectUtils.js rename to packages/common/src/ObjectUtils.ts index e25a90d4..ba025a6e 100644 --- a/packages/common/src/ObjectUtils.js +++ b/packages/common/src/ObjectUtils.ts @@ -1,5 +1,8 @@ // Contains helpful methods for interacting with the Object data type. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ObjectType = { [key: string]: any }; + /** * @notice Given `overrideProps` and `defaultProps` Objects, returns a new Object, `newObject`, * with the same properties as `defaultProps`, but replaces any property values that overlap with @@ -13,7 +16,7 @@ * the new Object and `isValid` will be called to validate each of `newObject`'s properties. * @return `newObject` a new Object with the same properties as `defaultProps`, or `defaultProps` if undefined `overrideProps`. */ -const createObjectFromDefaultProps = (overrideProps, defaultProps) => { +export const createObjectFromDefaultProps = (overrideProps: ObjectType, defaultProps: ObjectType): ObjectType => { if (!defaultProps) { throw new Error("Undefined `defaultProps`"); } @@ -22,7 +25,7 @@ const createObjectFromDefaultProps = (overrideProps, defaultProps) => { overrideProps = {}; } - const newObject = {}; + const newObject: ObjectType = {}; Object.keys(defaultProps).forEach((prop) => { // Set property value to that contained in `overrideProps` if it exists, else set to `defaultProps`. diff --git a/packages/common/src/PriceIdentifierUtils.js b/packages/common/src/PriceIdentifierUtils.ts similarity index 70% rename from packages/common/src/PriceIdentifierUtils.js rename to packages/common/src/PriceIdentifierUtils.ts index be259b6b..67176dbe 100644 --- a/packages/common/src/PriceIdentifierUtils.js +++ b/packages/common/src/PriceIdentifierUtils.ts @@ -1,9 +1,9 @@ // Blacklisted price identifiers that will not automatically display on voter clients. -const IDENTIFIER_BLACKLIST = { SOME_IDENTIFIER: ["1596666977"] }; +export const IDENTIFIER_BLACKLIST = { SOME_IDENTIFIER: ["1596666977"] }; // Price identifiers that should resolve prices to non 18 decimal precision. Any identifiers // not on this list are assumed to resolve to 18 decimals. -const IDENTIFIER_NON_18_PRECISION = { +export const IDENTIFIER_NON_18_PRECISION = { USDBTC: 8, "STABLESPREAD/USDC": 6, "STABLESPREAD/BTC": 8, @@ -19,8 +19,14 @@ const IDENTIFIER_NON_18_PRECISION = { TEST6DECIMALSANCIL: 6, }; -const getPrecisionForIdentifier = (identifier) => { - return IDENTIFIER_NON_18_PRECISION[identifier] ? IDENTIFIER_NON_18_PRECISION[identifier] : 18; +type IdentifierNon18Precision = keyof typeof IDENTIFIER_NON_18_PRECISION; + +function isNon18Precision(identifier: string): identifier is IdentifierNon18Precision { + return identifier in IDENTIFIER_NON_18_PRECISION; +} + +export const getPrecisionForIdentifier = (identifier: string): number => { + return isNon18Precision(identifier) ? IDENTIFIER_NON_18_PRECISION[identifier] : 18; }; // The optimistic oracle proposer should skip proposing prices for these identifiers, for expired EMP contracts, @@ -29,7 +35,7 @@ const getPrecisionForIdentifier = (identifier) => { // - "The type of price that the DVM will return is dependent on the timestamp the price request is made at. This // timestamp is the expiry timestamp of the contract that is intended to use this price identifier, so the TWAP // calculation is used pre-expiry and the closing index value of uSTONKS calculation is used at expiry."" -const OPTIMISTIC_ORACLE_IGNORE_POST_EXPIRY = [ +export const OPTIMISTIC_ORACLE_IGNORE_POST_EXPIRY = [ "TESTBLACKLIST", // Used for testing this list, assumed by tests to be at index 0. "uSTONKS_APR21", "GASETH-TWAP-1Mx1M", @@ -38,12 +44,4 @@ const OPTIMISTIC_ORACLE_IGNORE_POST_EXPIRY = [ ]; // Any identifier on this list -const OPTIMISTIC_ORACLE_IGNORE = ["SPACEXLAUNCH", "uTVL_KPI_UMA"]; - -module.exports = { - IDENTIFIER_BLACKLIST, - IDENTIFIER_NON_18_PRECISION, - getPrecisionForIdentifier, - OPTIMISTIC_ORACLE_IGNORE_POST_EXPIRY, - OPTIMISTIC_ORACLE_IGNORE, -}; +export const OPTIMISTIC_ORACLE_IGNORE = ["SPACEXLAUNCH", "uTVL_KPI_UMA"]; diff --git a/packages/common/src/ProviderUtils.js b/packages/common/src/ProviderUtils.ts similarity index 56% rename from packages/common/src/ProviderUtils.js rename to packages/common/src/ProviderUtils.ts index 5fc7d9d3..96fc624a 100644 --- a/packages/common/src/ProviderUtils.js +++ b/packages/common/src/ProviderUtils.ts @@ -2,11 +2,13 @@ // web3 providers to take advantage of the flexibility of providing custom configs to tailor the desired options. The network // syntax mimics that of the main UMA Truffle implementation to make this backwards compatible. -const Web3 = require("web3"); -const { getTruffleConfig, getNodeUrl } = require("./TruffleConfig"); -const argv = require("minimist")(process.argv.slice(), { string: ["network"] }); -const Url = require("url"); -const { RetryProvider } = require("./RetryProvider"); +import Web3 from "web3"; +import { getTruffleConfig, getNodeUrl, Network } from "./TruffleConfig"; +import minimist from "minimist"; +import Url from "url"; +import { RetryProvider, RetryConfig } from "./RetryProvider"; +import { AbstractProvider } from "web3-core"; +const argv = minimist(process.argv.slice(), { string: ["network"] }); // NODE_RETRY_CONFIG should be a JSON of the form (retries and delay are optional, they default to 1 and 0 respectively): // [ @@ -24,34 +26,37 @@ const { RetryProvider } = require("./RetryProvider"); const { NODE_RETRY_CONFIG } = process.env; // Set web3 to null -let web3 = null; +let web3: Web3 | null = null; -function createBasicProvider(nodeRetryConfig) { +export function createBasicProvider(nodeRetryConfig: RetryConfig[]): RetryProvider { return new RetryProvider( - nodeRetryConfig.map((configElement) => { - const protocol = Url.parse(configElement.url).protocol; - let options = { - timeout: 10000, // 10 second timeout - }; - - if (protocol.startsWith("ws")) { - // Websocket - options = { - ...options, - clientConfig: { - maxReceivedFrameSize: 100000000, // Useful if requests result are large bytes - default: 1MiB - maxReceivedMessageSize: 100000000, // bytes - default: 8MiB - }, - reconnect: { - auto: true, // Enable auto reconnection - delay: 5000, // ms - maxAttempts: 10, - onTimeout: false, - }, + nodeRetryConfig.map( + (configElement: RetryConfig): RetryConfig => { + const protocol = Url.parse(configElement.url).protocol; + if (protocol === null) throw new Error(`No protocol detected for url: ${configElement.url}`); + let options: RetryConfig["options"] = { + timeout: 10000, // 10 second timeout }; + + if (protocol.startsWith("ws")) { + // Websocket + options = { + ...options, + clientConfig: { + maxReceivedFrameSize: 100000000, // Useful if requests result are large bytes - default: 1MiB + maxReceivedMessageSize: 100000000, // bytes - default: 8MiB + }, + reconnect: { + auto: true, // Enable auto reconnection + delay: 5000, // ms + maxAttempts: 10, + onTimeout: false, + }, + }; + } + return { ...configElement, options }; } - return { options, ...configElement }; - }) + ) ); } @@ -69,11 +74,12 @@ function createBasicProvider(nodeRetryConfig) { * a `--network` argument. Useful in serverless or when running node scripts. * */ -function getWeb3(parameterizedNetwork = "test") { +export function getWeb3(parameterizedNetwork = "test"): Web3 { + const castedGlobal = (global as unknown) as { web3: Web3 | undefined }; + if (castedGlobal.web3) return castedGlobal.web3; + // If a web3 instance has already been initialized, return it. - if (web3) { - return web3; - } + if (web3) return web3; // Create basic web3 provider with no wallet connection based on the url alone. const network = argv.network || parameterizedNetwork; // Default to the test network (local network). @@ -84,7 +90,16 @@ function getWeb3(parameterizedNetwork = "test") { // Use the basic provider to create a provider with an unlocked wallet. This piggybacks off the UMA common TruffleConfig // implementing all networks & wallet types. EG: mainnet_mnemonic, kovan_gckms. Errors if no argv.network. - const providerWithWallet = getTruffleConfig().networks[network].provider(basicProvider); + const provider = getTruffleConfig().networks[network].provider; + + function isCallable( + input: typeof provider + ): input is (inputProviderOrUrl?: AbstractProvider | string) => AbstractProvider { + return input instanceof Function; + } + + if (!isCallable(provider)) throw new Error(`Null or string provider for network ${network}`); + const providerWithWallet = provider(basicProvider); // Lastly, create a web3 instance with the wallet-based provider. This can be used to query the chain via the // a basic web3 provider & has access to the users wallet based on the kind of connection they created. @@ -92,17 +107,3 @@ function getWeb3(parameterizedNetwork = "test") { return web3; } - -/** - * @notice Gets a web3 instance based on the network argument using the truffle config in this package. - * This method will be subbed in when called in a truffle test context. - */ -function getWeb3Test() { - return global.web3; -} - -if (global.web3) { - module.exports = { getWeb3: getWeb3Test }; -} else { - module.exports = { getWeb3 }; -} diff --git a/packages/common/src/PublicNetworks.js b/packages/common/src/PublicNetworks.ts similarity index 70% rename from packages/common/src/PublicNetworks.js rename to packages/common/src/PublicNetworks.ts index 6eb12f06..b4fc29db 100644 --- a/packages/common/src/PublicNetworks.js +++ b/packages/common/src/PublicNetworks.ts @@ -3,11 +3,32 @@ // ID as the network (i.e. Rinkeby will return 4 as the chainId), but some networks with chainID's > 255 need to // override the default behavior because their network ID is too high. const BRIDGE_CHAIN_ID = { 1337: 253, 80001: 254, 31337: 255 }; -const getBridgeChainId = (netId) => { - return BRIDGE_CHAIN_ID[netId] || netId; + +type ModifiedBridgeId = keyof typeof BRIDGE_CHAIN_ID; + +function isModifedChainId(netId: number): netId is ModifiedBridgeId { + return netId in BRIDGE_CHAIN_ID; +} + +export const getBridgeChainId = (netId: number): number => { + return isModifedChainId(netId) ? BRIDGE_CHAIN_ID[netId] : netId; }; -const PublicNetworks = { +interface PublicNetworksType { + [networkId: number]: { + name: string; + ethFaucet?: null | string; + etherscan: string; + daiAddress?: string; + wethAddress?: string; + customTruffleConfig?: { + confirmations: number; + timeoutBlocks: number; + }; + }; +} + +export const PublicNetworks: PublicNetworksType = { 1: { name: "mainnet", ethFaucet: null, @@ -49,4 +70,6 @@ const PublicNetworks = { }, }; -module.exports = { PublicNetworks, getBridgeChainId }; +export function isPublicNetwork(name: string): boolean { + return Object.values(PublicNetworks).some((network) => name.startsWith(network.name)); +} diff --git a/packages/common/src/Random.js b/packages/common/src/Random.js deleted file mode 100644 index 6101d89f..00000000 --- a/packages/common/src/Random.js +++ /dev/null @@ -1,16 +0,0 @@ -const web3 = require("web3"); - -function getRandomSignedInt() { - const unsignedValue = getRandomUnsignedInt(); - - // The signed range is just the unsigned range decreased by 2^255. - const signedOffset = web3.utils.toBN(2).pow(web3.utils.toBN(255)); - return unsignedValue.sub(signedOffset); -} - -// Generate a random unsigned 256 bit int. -function getRandomUnsignedInt() { - return web3.utils.toBN(web3.utils.randomHex(32)); -} - -module.exports = { getRandomSignedInt, getRandomUnsignedInt }; diff --git a/packages/common/src/Random.ts b/packages/common/src/Random.ts new file mode 100644 index 00000000..7405d670 --- /dev/null +++ b/packages/common/src/Random.ts @@ -0,0 +1,15 @@ +import Web3 from "web3"; +import { BN } from "./types"; + +export function getRandomSignedInt(): BN { + const unsignedValue = getRandomUnsignedInt(); + + // The signed range is just the unsigned range decreased by 2^255. + const signedOffset = Web3.utils.toBN(2).pow(Web3.utils.toBN(255)); + return unsignedValue.sub(signedOffset); +} + +// Generate a random unsigned 256 bit int. +export function getRandomUnsignedInt(): BN { + return Web3.utils.toBN(Web3.utils.randomHex(32)); +} diff --git a/packages/common/src/RetryProvider.js b/packages/common/src/RetryProvider.ts similarity index 61% rename from packages/common/src/RetryProvider.js rename to packages/common/src/RetryProvider.ts index 41ad5215..87009a5c 100644 --- a/packages/common/src/RetryProvider.js +++ b/packages/common/src/RetryProvider.ts @@ -1,8 +1,32 @@ -const assert = require("assert"); -const Web3 = require("web3"); +import Web3 from "web3"; +import assert from "assert"; + +type Web3ProviderOptions = + | ConstructorParameters[1] + | ConstructorParameters[1]; + +type Web3Provider = + | InstanceType + | InstanceType; + +interface Config { + retries: number; + delay: number; + url: string; + options?: Web3ProviderOptions; +} + +type PartialExcept = Partial> & Pick; +type Payload = Parameters[0]; +type Callback = Parameters[1]; +type CallbackResult = Parameters[1]; + +export type RetryConfig = PartialExcept; // Wraps one or more web3 http/websocket providers and allows per-request retries and fallbacks. -class RetryProvider { +export class RetryProvider { + private providerCaches: (Config & { provider?: Web3Provider })[]; + /** * @notice Constructs new retry provider. * @param {Array} config config object: @@ -22,21 +46,26 @@ class RetryProvider { * } * ] */ - constructor(configs) { + constructor(configs: RetryConfig[]) { assert(configs.length > 0, "Must have at least one provider"); this.providerCaches = configs.map((config) => ({ retries: 1, delay: 0, ...config })); } // Passes the send through, catches errors, and retries on error. - send(payload, callback) { + sendAsync(payload: Payload, callback: Callback): void { + this.send(payload, callback); + } + + // Passes the send through, catches errors, and retries on error. + send(payload: Payload, callback: Callback): void { // Turn callback into async-await internally. - const sendWithProvider = (provider) => { + const sendWithProvider = (provider: Web3Provider): Promise => { return new Promise((resolve, reject) => { provider.send(payload, (error, result) => { if (error) { // Error thrown in the provider. reject(error); - } else if (result.error) { + } else if (result?.error) { // Error object returned from node. // TODO: we may need to add additional logic to discern EVM execution errors from node connection errors. reject(result); @@ -50,22 +79,22 @@ class RetryProvider { // Turn retry promise result back into a callback. this._runRetry(sendWithProvider).then( (result) => callback(null, result), - (reason) => callback(reason, null) + (reason) => callback(reason, undefined) ); } // Pass through disconnect to any initialized providers. - disconnect(...all) { + disconnect(code: number, reason: string): void { for (const cache of this.providerCaches) { - cache?.provider.disconnect(...all); + cache?.provider?.disconnect(code, reason); } } - supportsSubscriptions() { + supportsSubscriptions(): boolean { return false; // return false for simplicity since some providers may be http, which doesn't support subscriptions. } - _constructOrGetProvider(index) { + _constructOrGetProvider(index: number): Web3Provider { const cache = this.providerCaches[index]; assert(cache, "No provider for this index"); if (!cache.provider) { @@ -78,7 +107,7 @@ class RetryProvider { } // Returns a Promise that resolves to the wrapped provider. - async _runRetry(fn, providerIndex = 0, retryIndex = 0) { + async _runRetry(fn: (provider: Web3Provider) => Promise, providerIndex = 0, retryIndex = 0): Promise { const provider = this._constructOrGetProvider(providerIndex); try { return await fn(provider); @@ -86,8 +115,8 @@ class RetryProvider { const { delay, retries } = this.providerCaches[providerIndex]; // If out of retries, move to next provider. const shouldMoveToNextProvider = retries <= retryIndex + 1; - let nextRetryIndex = shouldMoveToNextProvider ? 0 : retryIndex + 1; - let nextProviderIndex = shouldMoveToNextProvider ? providerIndex + 1 : providerIndex; + const nextRetryIndex = shouldMoveToNextProvider ? 0 : retryIndex + 1; + const nextProviderIndex = shouldMoveToNextProvider ? providerIndex + 1 : providerIndex; if (nextProviderIndex >= this.providerCaches.length) throw error; // No more providers to try. if (!shouldMoveToNextProvider) await new Promise((resolve) => setTimeout(resolve, delay * 1000)); // Delay only if not moving to a new provider. @@ -96,5 +125,3 @@ class RetryProvider { } } } - -module.exports = { RetryProvider }; diff --git a/packages/common/src/SolcoverConfig.js b/packages/common/src/SolcoverConfig.ts similarity index 76% rename from packages/common/src/SolcoverConfig.js rename to packages/common/src/SolcoverConfig.ts index 1e1a9e12..78a20923 100644 --- a/packages/common/src/SolcoverConfig.js +++ b/packages/common/src/SolcoverConfig.ts @@ -1,4 +1,4 @@ -const SolcoverConfig = { +export const SolcoverConfig = { providerOptions: { network_id: 1234 }, skipFiles: [ "Migrations.sol", @@ -8,5 +8,3 @@ const SolcoverConfig = { "oracle/implementation/test", ], }; - -module.exports = { SolcoverConfig }; diff --git a/packages/common/src/SolidityTestUtils.js b/packages/common/src/SolidityTestUtils.ts similarity index 55% rename from packages/common/src/SolidityTestUtils.js rename to packages/common/src/SolidityTestUtils.ts index bd932981..3ac32ad7 100644 --- a/packages/common/src/SolidityTestUtils.js +++ b/packages/common/src/SolidityTestUtils.ts @@ -1,6 +1,8 @@ +import type Web3 from "web3"; + // Attempts to execute a promise and returns false if no error is thrown, // or an Array of the error messages -async function didContractThrow(promise) { +export async function didContractThrow(promise: Promise): Promise { try { await promise; } catch (error) { @@ -9,11 +11,22 @@ async function didContractThrow(promise) { return false; } -async function advanceBlockAndSetTime(web3, time) { +type Web3Provider = + | InstanceType + | InstanceType; +type Callback = Parameters[1]; +type CallbackResult = Parameters[1]; +type CallbackError = Parameters[0]; + +export async function advanceBlockAndSetTime(web3: Web3, time: number): Promise { return new Promise((resolve, reject) => { + if (!web3.currentProvider || typeof web3?.currentProvider === "string" || !web3.currentProvider.send) { + reject(new Error("No web3 provider that allows send()")); + return; + } web3.currentProvider.send( { jsonrpc: "2.0", method: "evm_mine", params: [time], id: new Date().getTime() }, - (err, result) => { + (err: CallbackError, result: CallbackResult) => { if (err) { reject(err); } else { @@ -24,11 +37,16 @@ async function advanceBlockAndSetTime(web3, time) { }); } -async function stopMining(web3) { +export async function stopMining(web3: Web3): Promise { return new Promise((resolve, reject) => { + if (!web3.currentProvider || typeof web3?.currentProvider === "string" || !web3.currentProvider.send) { + reject(new Error("No web3 provider that allows send()")); + return; + } + web3.currentProvider.send( { jsonrpc: "2.0", method: "miner_stop", params: [], id: new Date().getTime() }, - (err, result) => { + (err: CallbackError, result: CallbackResult) => { if (err) { reject(err); } else { @@ -39,11 +57,15 @@ async function stopMining(web3) { }); } -async function startMining(web3) { +async function startMining(web3: Web3): Promise { return new Promise((resolve, reject) => { + if (!web3.currentProvider || typeof web3?.currentProvider === "string" || !web3.currentProvider.send) { + reject(new Error("No web3 provider that allows send()")); + return; + } web3.currentProvider.send( { jsonrpc: "2.0", method: "miner_start", params: [], id: new Date().getTime() }, - (err, result) => { + (err: CallbackError, result: CallbackResult) => { if (err) { reject(err); } else { @@ -54,11 +76,15 @@ async function startMining(web3) { }); } -async function takeSnapshot(web3) { +async function takeSnapshot(web3: Web3): Promise { return new Promise((resolve, reject) => { + if (!web3.currentProvider || typeof web3?.currentProvider === "string" || !web3.currentProvider.send) { + reject(new Error("No web3 provider that allows send()")); + return; + } web3.currentProvider.send( - { jsonrpc: "2.0", method: "evm_snapshot", id: new Date().getTime() }, - (err, snapshotId) => { + { jsonrpc: "2.0", method: "evm_snapshot", id: new Date().getTime(), params: [] }, + (err: CallbackError, snapshotId: CallbackResult) => { if (err) { return reject(err); } @@ -68,11 +94,15 @@ async function takeSnapshot(web3) { }); } -async function revertToSnapshot(web3, id) { +async function revertToSnapshot(web3: Web3, id: number): Promise { return new Promise((resolve, reject) => { + if (!web3.currentProvider || typeof web3?.currentProvider === "string" || !web3.currentProvider.send) { + reject(new Error("No web3 provider that allows send()")); + return; + } web3.currentProvider.send( { jsonrpc: "2.0", method: "evm_revert", params: [id], id: new Date().getTime() }, - (err, result) => { + (err: CallbackError, result) => { if (err) { return reject(err); } @@ -82,13 +112,21 @@ async function revertToSnapshot(web3, id) { }); } +type Contract = InstanceType["eth"]["Contract"]>; +type Transaction = ReturnType["eth"]["Contract"]>["deploy"]>; + // This function will mine all transactions in `transactions` in the same block with block timestamp `time`. // The return value is an array of receipts corresponding to the transactions. // Each transaction in transactions should be a web3 transaction generated like: // let transaction = truffleContract.contract.methods.myMethodName(arg1, arg2); // or if already using a web3 contract object: // let transaction = web3Contract.methods.myMethodName(arg1, arg2); -async function mineTransactionsAtTime(web3, transactions, time, sender) { +export async function mineTransactionsAtTime( + web3: Web3, + transactions: Transaction[], + time: number, + sender: string +): Promise { await stopMining(web3); try { @@ -97,7 +135,7 @@ async function mineTransactionsAtTime(web3, transactions, time, sender) { const result = transaction.send({ from: sender }); // Awaits the transactionHash, which signifies the transaction was sent, but not necessarily mined. - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { result.on("transactionHash", function () { resolve(); }); @@ -120,13 +158,3 @@ async function mineTransactionsAtTime(web3, transactions, time, sender) { await startMining(web3); } } - -module.exports = { - didContractThrow, - mineTransactionsAtTime, - advanceBlockAndSetTime, - takeSnapshot, - revertToSnapshot, - stopMining, - startMining, -}; diff --git a/packages/common/src/TimeUtils.js b/packages/common/src/TimeUtils.ts similarity index 74% rename from packages/common/src/TimeUtils.js rename to packages/common/src/TimeUtils.ts index 2b3c1194..11240f5d 100644 --- a/packages/common/src/TimeUtils.js +++ b/packages/common/src/TimeUtils.ts @@ -1,10 +1,13 @@ -const { UMA_FIRST_EMP_BLOCK } = require("./Constants.js"); -require("dotenv").config(); +import { UMA_FIRST_EMP_BLOCK } from "./Constants"; +import dotenv from "dotenv"; +import type Web3 from "web3"; +dotenv.config(); /** * @notice Return average block-time for a period. */ -async function averageBlockTimeSeconds(/* lookbackSeconds */) { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export async function averageBlockTimeSeconds(lookbackSeconds?: number): Promise { // TODO: Call an external API to get this data. Currently this value is a hard-coded estimate // based on the data from https://etherscan.io/chart/blocktime. ~13.5 seconds has been the average // since April 2016, although this value seems to spike periodically for a relatively short period of time. @@ -18,7 +21,7 @@ async function averageBlockTimeSeconds(/* lookbackSeconds */) { } // Sets fromBlock to the value of an environment variable if one is set. This can be set to 0 to make tests work with Ganache, or any other value needed for a production script or bot. -async function getFromBlock(web3) { +export async function getFromBlock(web3: Web3): Promise { const networkType = await web3.eth.net.getNetworkType(); if (process.env.FROM_BLOCK) { return Number(process.env.FROM_BLOCK); @@ -34,10 +37,8 @@ async function getFromBlock(web3) { * @param seconds the number of seconds. * @param cushionPercentage the percentage to add to the number as a cushion. */ -async function estimateBlocksElapsed(seconds, cushionPercentage = 0.0) { +export async function estimateBlocksElapsed(seconds: number, cushionPercentage = 0.0): Promise { const cushionMultiplier = cushionPercentage + 1.0; const averageBlockTime = await averageBlockTimeSeconds(); return Math.floor((seconds * cushionMultiplier) / averageBlockTime); } - -module.exports = { averageBlockTimeSeconds, getFromBlock, estimateBlocksElapsed }; diff --git a/packages/common/src/TransactionUtils.js b/packages/common/src/TransactionUtils.ts similarity index 74% rename from packages/common/src/TransactionUtils.js rename to packages/common/src/TransactionUtils.ts index ef448ac7..cc3d9d1c 100644 --- a/packages/common/src/TransactionUtils.js +++ b/packages/common/src/TransactionUtils.ts @@ -1,6 +1,17 @@ -const util = require("util"); -const argv = require("minimist")(process.argv.slice(), {}); -const ynatm = require("@umaprotocol/ynatm"); +import util from "util"; +import minimist from "minimist"; +import ynatm from "@umaprotocol/ynatm"; +import type Web3 from "web3"; +import type { TransactionReceipt } from "web3-core"; +import type { ContractSendMethod, SendOptions } from "web3-eth-contract"; + +type CallReturnValue = ReturnType; +interface AugmentedSendOptions extends SendOptions { + chainId?: string; + usingOffSetDSProxyAccount?: boolean; +} + +const argv = minimist(process.argv.slice(), {}); /** * Simulate transaction via .call() and then .send() and return receipt. If an error is thrown, return the error and add @@ -9,10 +20,24 @@ const ynatm = require("@umaprotocol/ynatm"); * @notice Uses the ynatm package to retry the transaction with increasing gas price. * @param {*Object} web3.js object for making queries and accessing Ethereum related methods. * @param {*Object} transaction Transaction to call `.call()` and subsequently `.send()` on from `senderAccount`. - * @param {*Object} config transaction config, e.g. { gasPrice, from }, passed to web3 transaction. + * @param {*Object} transactionconfig transaction config, e.g. { gasPrice, from }, passed to web3 transaction. * @return Error and type of error (originating from `.call()` or `.send()`) or transaction receipt and return value. */ -const runTransaction = async ({ web3, transaction, transactionConfig, availableAccounts = 1 }) => { +export const runTransaction = async ({ + web3, + transaction, + transactionConfig, + availableAccounts = 1, +}: { + web3: Web3; + transaction: ContractSendMethod; + transactionConfig: AugmentedSendOptions; + availableAccounts: number; +}): Promise<{ + receipt: TransactionReceipt; + returnValue: CallReturnValue; + transactionConfig: AugmentedSendOptions; +}> => { // Add chainId in case RPC enforces transactions to be replay-protected, (i.e. enforced in geth v1.10, // https://blog.ethereum.org/2021/03/03/geth-v1-10-0/). transactionConfig.chainId = web3.utils.toHex(await web3.eth.getChainId()); @@ -63,18 +88,21 @@ const runTransaction = async ({ web3, transaction, transactionConfig, availableA // hasn't mined. Min Gas price starts at caller's transactionConfig.gasPrice, with a max gasPrice of x6. const gasPriceScalingFunction = ynatm.DOUBLES; const retryDelay = 60000; + if (!transactionConfig.gasPrice) throw new Error("No gas price provided"); const minGasPrice = transactionConfig.gasPrice; - const maxGasPrice = 2 * 3 * minGasPrice; + const maxGasPrice = 2 * 3 * parseInt(minGasPrice); const receipt = await ynatm.send({ - sendTransactionFunction: (gasPrice) => transaction.send({ ...transactionConfig, gasPrice }), + sendTransactionFunction: (gasPrice: number) => + transaction.send({ ...transactionConfig, gasPrice: gasPrice.toString() }), minGasPrice, maxGasPrice, gasPriceScalingFunction, delay: retryDelay, }); - return { receipt, returnValue, transactionConfig }; + // Note: cast is due to an incorrect type in the web3 declarations that assumes send returns a contract. + return { receipt: (receipt as unknown) as TransactionReceipt, returnValue, transactionConfig }; } catch (error) { error.type = "send"; throw error; @@ -87,7 +115,7 @@ const runTransaction = async ({ web3, transaction, transactionConfig, availableA * @param {*string} account account to check. * @return Bool true if the account has pending transaction and false if no pending transaction. */ -const accountHasPendingTransactions = async (web3, account) => { +export const accountHasPendingTransactions = async (web3: Web3, account: string): Promise => { const [currentMindedTransactions, currentTransactionsIncludingPending] = await Promise.all([ web3.eth.getTransactionCount(account, "latest"), getPendingTransactionCount(web3, account), @@ -103,7 +131,9 @@ const accountHasPendingTransactions = async (web3, account) => { * @param {*string} account account to check. * @returns number representing the number of transactions, including pending. */ -const getPendingTransactionCount = async (web3, account) => { +export const getPendingTransactionCount = async (web3: Web3, account: string): Promise => { + if (!web3.currentProvider || typeof web3.currentProvider === "string" || !web3.currentProvider.send) + throw new Error("A valid provider with send method not initialized"); const sendRpc = util.promisify(web3.currentProvider.send).bind(web3.currentProvider); const rpcResponse = await sendRpc({ @@ -112,6 +142,7 @@ const getPendingTransactionCount = async (web3, account) => { params: [account, "pending"], id: Math.round(Math.random() * 100000), }); + if (!rpcResponse || !rpcResponse.result) throw new Error("Bad RPC response"); return web3.utils.toDecimal(rpcResponse.result); }; @@ -121,7 +152,7 @@ const getPendingTransactionCount = async (web3, account) => { * @param {Object} web3 Provider from Truffle/node to connect to Ethereum network. * @param {number} blockerBlockNumber block execution until this block number is mined. */ -const blockUntilBlockMined = async (web3, blockerBlockNumber, delay = 500) => { +export const blockUntilBlockMined = async (web3: Web3, blockerBlockNumber: number, delay = 500): Promise => { if (argv._.indexOf("test") !== -1) return; for (;;) { const currentBlockNumber = await web3.eth.getBlockNumber(); @@ -129,5 +160,3 @@ const blockUntilBlockMined = async (web3, blockerBlockNumber, delay = 500) => { await new Promise((r) => setTimeout(r, delay)); } }; - -module.exports = { runTransaction, blockUntilBlockMined, accountHasPendingTransactions }; diff --git a/packages/common/src/TruffleConfig.js b/packages/common/src/TruffleConfig.ts similarity index 66% rename from packages/common/src/TruffleConfig.js rename to packages/common/src/TruffleConfig.ts index 8d996641..16030717 100644 --- a/packages/common/src/TruffleConfig.js +++ b/packages/common/src/TruffleConfig.ts @@ -4,21 +4,33 @@ * should never be shared publicly and ideally should not be stored in plain text. */ -const path = require("path"); +import path from "path"; +import Web3 from "web3"; +import dotenv from "dotenv"; +import minimist from "minimist"; +import HDWalletProvider from "@truffle/hdwallet-provider"; -const HDWalletProvider = require("@truffle/hdwallet-provider"); -const LedgerWalletProvider = require("@umaprotocol/truffle-ledger-provider"); -const { getGckmsConfig } = require("./gckms/GckmsConfig.js"); -const { ManagedSecretProvider } = require("./gckms/ManagedSecretProvider.js"); -const { PublicNetworks } = require("./PublicNetworks.js"); -const { MetaMaskTruffleProvider } = require("./MetaMaskTruffleProvider.js"); -const { isPublicNetwork } = require("./MigrationUtils"); -const Web3 = require("web3"); -require("dotenv").config(); -const argv = require("minimist")(process.argv.slice(), { string: ["gasPrice"] }); +import LedgerWalletProvider from "@umaprotocol/truffle-ledger-provider"; +import { getGckmsConfig } from "./gckms/GckmsConfig"; +import { ManagedSecretProvider } from "./gckms/ManagedSecretProvider"; +import { PublicNetworks, isPublicNetwork } from "./PublicNetworks"; +import { MetaMaskTruffleProvider } from "./MetaMaskTruffleProvider"; + +import type { AbstractProvider } from "web3-core"; + +dotenv.config(); +const argv = minimist(process.argv.slice(), { string: ["gasPrice"] }); + +export interface Network { + networkCheckTimeout?: number; + network_id?: number | string; + gas?: number | string; + gasPrice?: number | string; + provider?: ((inputProviderOrUrl?: AbstractProvider | string) => AbstractProvider) | AbstractProvider | string; +} // Fallback to a public mnemonic to prevent exceptions. -const mnemonic = process.env.MNEMONIC +export const mnemonic = process.env.MNEMONIC ? process.env.MNEMONIC : "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat"; @@ -30,15 +42,15 @@ const privateKey = process.env.PRIVATE_KEY // Fallback to a backup non-prod API key. const keyOffset = process.env.KEY_OFFSET ? parseInt(process.env.KEY_OFFSET) : 0; // Start at account 0 by default. const numKeys = process.env.NUM_KEYS ? parseInt(process.env.NUM_KEYS) : 2; // Generate two wallets by default. -let singletonProvider; +let singletonProvider: AbstractProvider; // Default options -const gasPx = argv.gasPrice ? Web3.utils.toWei(argv.gasPrice, "gwei") : 1000000000; // 1 gwei +const gasPx = argv.gasPrice ? Web3.utils.toWei(argv.gasPrice, "gwei").toString() : "1000000000"; // 1 gwei const gas = undefined; // Defining this as undefined (rather than leaving undefined) forces truffle estimate gas usage. const GckmsConfig = getGckmsConfig(); // If a custom node URL is provided, use that. Otherwise use an infura websocket connection. -function getNodeUrl(networkName, useHttps = false) { +export function getNodeUrl(networkName: string, useHttps = false): string { if (isPublicNetwork(networkName) && !networkName.includes("fork")) { const infuraApiKey = process.env.INFURA_API_KEY || "e34138b2db5b496ab5cc52319d2f0299"; const name = networkName.split("_")[0]; @@ -55,7 +67,12 @@ function getNodeUrl(networkName, useHttps = false) { // Adds a public network. // Note: All public networks can be accessed using keys from GCS using the ManagedSecretProvider or using a mnemonic in the // shell environment. -function addPublicNetwork(networks, name, networkId, customTruffleConfig) { +function addPublicNetwork( + networks: { [name: string]: Network }, + name: string, + networkId: number, + customTruffleConfig: Network +) { const options = { networkCheckTimeout: 15000, network_id: networkId, @@ -69,7 +86,7 @@ function addPublicNetwork(networks, name, networkId, customTruffleConfig) { // GCS ManagedSecretProvider network. networks[name + "_gckms"] = { ...options, - provider: function (provider = nodeUrl) { + provider: function (provider: AbstractProvider | string = nodeUrl) { if (!singletonProvider) { singletonProvider = new ManagedSecretProvider(GckmsConfig, provider, 0, GckmsConfig.length); } @@ -80,7 +97,7 @@ function addPublicNetwork(networks, name, networkId, customTruffleConfig) { // Private key network. networks[name + "_privatekey"] = { ...options, - provider: function (provider = nodeUrl) { + provider: function (provider: AbstractProvider | string = nodeUrl) { if (!singletonProvider) { singletonProvider = new HDWalletProvider([privateKey], provider); } @@ -91,7 +108,7 @@ function addPublicNetwork(networks, name, networkId, customTruffleConfig) { // Mnemonic network. networks[name + "_mnemonic"] = { ...options, - provider: function (provider = nodeUrl) { + provider: function (provider: AbstractProvider | string = nodeUrl) { if (!singletonProvider) { singletonProvider = new HDWalletProvider(mnemonic, provider, keyOffset, numKeys); } @@ -107,7 +124,7 @@ function addPublicNetwork(networks, name, networkId, customTruffleConfig) { // Normal ledger wallet network. networks[name + "_ledger"] = { ...options, - provider: function (provider = nodeUrl) { + provider: function (provider: AbstractProvider | string = nodeUrl) { if (!singletonProvider) { singletonProvider = new LedgerWalletProvider(ledgerOptions, provider); } @@ -119,7 +136,7 @@ function addPublicNetwork(networks, name, networkId, customTruffleConfig) { // Note: the default derivation path matches the "legacy" ledger account in Ledger Live. networks[name + "_ledger_legacy"] = { ...options, - provider: function (provider = nodeUrl) { + provider: function (provider: AbstractProvider | string = nodeUrl) { if (!singletonProvider) { singletonProvider = new LedgerWalletProvider(legacyLedgerOptions, provider); } @@ -131,33 +148,41 @@ function addPublicNetwork(networks, name, networkId, customTruffleConfig) { // Adds a local network. // Note: local networks generally have more varied parameters, so the user can override any network option by passing // a customOptions object. -function addLocalNetwork(networks, name, customOptions) { +function addLocalNetwork(networks: { [name: string]: Network }, name: string, customOptions?: Network) { const nodeUrl = getNodeUrl(name); - const defaultOptions = { + const defaultOptions: Network = { network_id: "*", gas: gas, gasPrice: gasPx, - provider: function (provider = nodeUrl) { + provider: function (provider: string | AbstractProvider = nodeUrl): AbstractProvider { // Don't use the singleton here because there's no reason to for local networks. // Note: this is the way that truffle initializes their host + port http provider. // It is required to fix connection issues when testing. if (typeof provider === "string" && !provider.startsWith("ws")) { - return new Web3.providers.HttpProvider(provider, { keepAlive: false }); + // Deprecated method abstract provider is required but unused. Force the type to be AbstractProvider. + return (new Web3.providers.HttpProvider(provider, { keepAlive: false }) as unknown) as AbstractProvider; } const tempWeb3 = new Web3(provider); - return tempWeb3.eth.currentProvider; + if ( + !tempWeb3.eth.currentProvider || + typeof tempWeb3.eth.currentProvider === "string" || + !tempWeb3.eth.currentProvider.send + ) + throw new Error("Web3 couldn't initialize provider"); + // Similar to the above, cast to abstract provider. + return tempWeb3.eth.currentProvider as AbstractProvider; }, }; networks[name] = { ...defaultOptions, ...customOptions }; } -let networks = {}; +const networks = {}; // Public networks that need both a mnemonic and GCS ManagedSecretProvider network. for (const [id, { name, customTruffleConfig }] of Object.entries(PublicNetworks)) { - addPublicNetwork(networks, name, id, customTruffleConfig); + addPublicNetwork(networks, name, parseInt(id), customTruffleConfig); } // Add test network. @@ -179,7 +204,30 @@ addLocalNetwork(networks, "metamask", { }, }); -function getTruffleConfig(truffleContextDir = "./") { +interface TruffleConfig { + networks: { [name: string]: Network }; + plugins: string[]; + mocha?: { + enableTimeouts?: boolean; + before_timeout?: number; + }; + compilers: { + solc: { + version: string; + settings?: { + optimizer?: { + enabled: boolean; + runs: number; + }; + }; + }; + }; + migrations_directory?: string; + contracts_directory?: string; + contracts_build_directory?: string; +} + +export function getTruffleConfig(truffleContextDir = "./"): TruffleConfig { return { // See // for more about customizing your Truffle configuration! @@ -192,5 +240,3 @@ function getTruffleConfig(truffleContextDir = "./") { contracts_build_directory: path.join(truffleContextDir, "build/contracts"), }; } - -module.exports = { getTruffleConfig, getNodeUrl, mnemonic }; diff --git a/packages/common/src/UniswapV3Helpers.js b/packages/common/src/UniswapV3Helpers.ts similarity index 68% rename from packages/common/src/UniswapV3Helpers.js rename to packages/common/src/UniswapV3Helpers.ts index a01efda8..3c3fde9c 100644 --- a/packages/common/src/UniswapV3Helpers.js +++ b/packages/common/src/UniswapV3Helpers.ts @@ -1,28 +1,32 @@ // This file contains a number of useful uniswap v3 helpers helper functions. -const Web3 = require("web3"); -const { toBN, toWei } = Web3.utils; -const { ethers } = require("ethers"); +import Web3 from "web3"; +import { BN } from "./types"; +import { ethers } from "ethers"; +import UniswapV3Pool from "@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json"; +import Decimal from "decimal.js"; +import bn from "bignumber.js"; // Big number that comes with web3 does not support square root. +import { createContractObjectFromJson } from "./ContractUtils"; -const UniswapV3Pool = require("@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json"); -const Decimal = require("decimal.js"); -const bn = require("bignumber.js"); // Big number that comes with web3 does not support square root. bn.config({ EXPONENTIAL_AT: 999999, DECIMAL_PLACES: 40 }); -const { createContractObjectFromJson } = require("./ContractUtils"); +const { toBN, toWei } = Web3.utils; // Given a price defined in a ratio of reserve1 and reserve0 in x96, compute a price as sqrt(r1/r0) * 2^96 -function encodePriceSqrt(reserve1, reserve0) { +export function encodePriceSqrt string }, U extends { toString: () => string }>( + reserve1: T, + reserve0: U +): bn { return new bn(reserve1.toString()).div(reserve0.toString()).sqrt().multipliedBy(new bn(2).pow(96)).integerValue(3); } // reverse x96 operation by compting (price/(2^96))^2 -function decodePriceSqrt(priseSqrt) { +export function decodePriceSqrt(priseSqrt: string): bn { return new bn(priseSqrt).dividedBy(new bn(2).pow(96)).pow(2); } // Fetch the decoded price from a uniswap pool from slot0. -async function getCurrentPrice(poolAddress, web3) { +export async function getCurrentPrice(poolAddress: string, web3: Web3): Promise { const pool = await createContractObjectFromJson(UniswapV3Pool, web3).at(poolAddress); const slot0 = await pool.slot0(); @@ -31,7 +35,7 @@ async function getCurrentPrice(poolAddress, web3) { } // Encode a path. Note that pools (and therefore paths) change when you use different fees. -function encodePath(path, fees) { +export function encodePath(path: string[], fees: BN[] | string[] | number[]): string { const FEE_SIZE = 3; if (path.length != fees.length + 1) { throw new Error("path/fee lengths do not match"); @@ -50,15 +54,17 @@ function encodePath(path, fees) { return encoded.toLowerCase(); } -function toBNWei(input) { +export function toBNWei(input: BN | string | number): BN { return toBN(toWei(input.toString())); } +export type FeeAmount = keyof typeof TICK_SPACINGS; + // The price in a pool, in terms of tick number, can be expressed as: p=1.0001^tick (taken from uniswapV3 white paper). // Solving for the tick in terms of the price yields tick≈1.00005*ln(p) (calculated on wolframalpha). When creating a // position, the tick needs to be a multiple of the TICK_SPACING for that particular fee. This method therefore computes // a valid tick for a given price and poolFee. -function getTickFromPrice(price, poolFee) { +export function getTickFromPrice(price: BN | string | number, poolFee: FeeAmount): string { return toBNWei(10000.5) .mul(toBNWei(new Decimal(price.toString()).ln().toFixed(10))) .div(toBNWei(1)) @@ -68,14 +74,14 @@ function getTickFromPrice(price, poolFee) { .toString(); } -function getMinTick(tickSpacing) { +export function getMinTick(tickSpacing: number): number { return Math.ceil(-887272 / tickSpacing) * tickSpacing; } -function getMaxTick(tickSpacing) { +export function getMaxTick(tickSpacing: number): number { return Math.floor(887272 / tickSpacing) * tickSpacing; } -function getTickBitmapIndex(tick, tickSpacing) { +export function getTickBitmapIndex(tick: number | BN | string, tickSpacing: number): BN { const intermediate = toBN(tick.toString()).div(toBN(tickSpacing.toString())); // see https://docs.soliditylang.org/en/v0.7.6/types.html#shifts @@ -88,7 +94,7 @@ function getTickBitmapIndex(tick, tickSpacing) { else return intermediate.shrn(8); } -function computePoolAddress(factoryAddress, tokenA, tokenB, fee) { +export function computePoolAddress(factoryAddress: string, tokenA: string, tokenB: string, fee: FeeAmount): string { const POOL_BYTECODE_HASH = ethers.utils.keccak256(UniswapV3Pool.bytecode); const [token0, token1] = tokenA.toLowerCase() < tokenB.toLowerCase() ? [tokenA, tokenB] : [tokenB, tokenA]; const constructorArgumentsEncoded = ethers.utils.defaultAbiCoder.encode( @@ -107,20 +113,6 @@ function computePoolAddress(factoryAddress, tokenA, tokenB, fee) { return ethers.utils.getAddress(`0x${ethers.utils.keccak256(sanitizedInputs).slice(-40)}`); } -const FeeAmount = { LOW: 500, MEDIUM: 3000, HIGH: 10000 }; +export const FeeAmount = { LOW: 500, MEDIUM: 3000, HIGH: 10000 }; -const TICK_SPACINGS = { [FeeAmount.LOW]: 10, [FeeAmount.MEDIUM]: 60, [FeeAmount.HIGH]: 200 }; - -module.exports = { - getTickFromPrice, - encodePriceSqrt, - decodePriceSqrt, - getCurrentPrice, - encodePath, - getMinTick, - getMaxTick, - getTickBitmapIndex, - computePoolAddress, - FeeAmount, - TICK_SPACINGS, -}; +export const TICK_SPACINGS = { [FeeAmount.LOW]: 10, [FeeAmount.MEDIUM]: 60, [FeeAmount.HIGH]: 200 }; diff --git a/packages/common/src/VotingUtils.js b/packages/common/src/VotingUtils.js deleted file mode 100644 index 9f945691..00000000 --- a/packages/common/src/VotingUtils.js +++ /dev/null @@ -1,217 +0,0 @@ -const { - decryptMessage, - encryptMessage, - deriveKeyPairFromSignatureTruffle, - deriveKeyPairFromSignatureMetamask, -} = require("./Crypto"); -const { getKeyGenMessage, computeVoteHash } = require("./EncryptionHelper"); -const { BATCH_MAX_COMMITS, BATCH_MAX_RETRIEVALS, BATCH_MAX_REVEALS } = require("./Constants"); -const { getRandomUnsignedInt } = require("./Random.js"); -const { parseFixed } = require("./FormattingUtils"); - -const argv = require("minimist")(process.argv.slice()); - -/** - * Return voting contract, voting account, and signing account based on whether user is using a - * designated voting proxy. - * @param {String} account current default signing account - * @param {Object} voting DVM contract - * @param {Object} [designatedVoting] designated voting proxy contract - * @return votingContract Contract to send votes to. - * @return votingAccount address that votes are attributed to. - * @return signingAddress address used to sign encrypted messages. - */ -const getVotingRoles = (account, voting, designatedVoting) => { - if (designatedVoting) { - return { votingContract: designatedVoting, votingAccount: designatedVoting.address, signingAddress: account }; - } else { - return { votingContract: voting, votingAccount: account, signingAddress: account }; - } -}; - -/** - * Generate a salt and use it to encrypt a committed vote in response to a price request - * Return committed vote details to the voter. - * @param {Object} request {identifier, time} - * @param {String} roundId - * @param {Object} web3 - * @param {String | Number | BN} price - * @param {String} signingAccount - * @param {String} votingAccount - * @param {String?} decimals Default 18 decimal precision for price - */ -const constructCommitment = async (request, roundId, web3, price, signingAccount, votingAccount, decimals = 18) => { - const priceWei = parseFixed(price.toString(), decimals).toString(); - const salt = getRandomUnsignedInt().toString(); - const hash = computeVoteHash({ - price: priceWei, - salt, - account: votingAccount, - time: request.time, - roundId, - identifier: request.identifier, - }); - - const vote = { price: priceWei, salt }; - let publicKey; - if (argv.network === "metamask") { - publicKey = (await deriveKeyPairFromSignatureMetamask(web3, getKeyGenMessage(roundId), signingAccount)).publicKey; - } else { - publicKey = (await deriveKeyPairFromSignatureTruffle(web3, getKeyGenMessage(roundId), signingAccount)).publicKey; - } - const encryptedVote = await encryptMessage(publicKey, JSON.stringify(vote)); - - return { identifier: request.identifier, time: request.time, hash, encryptedVote, price: priceWei, salt }; -}; - -/** - * Decrypt an encrypted vote commit for the voter and return vote details - * @param {Object} request {identifier, time} - * @param {String} roundId - * @param {Object} web3 - * @param {String} signingAccount - * @param {Object} votingContract deployed Voting.sol instance - * @param {String} votingAccount - */ -const constructReveal = async (request, roundId, web3, signingAccount, votingContract, votingAccount) => { - const encryptedCommit = (await getLatestEvent("EncryptedVote", request, roundId, votingAccount, votingContract)) - .encryptedVote; - - let privateKey; - if (argv.network === "metamask") { - privateKey = (await deriveKeyPairFromSignatureMetamask(web3, getKeyGenMessage(roundId), signingAccount)).privateKey; - } else { - privateKey = (await deriveKeyPairFromSignatureTruffle(web3, getKeyGenMessage(roundId), signingAccount)).privateKey; - } - const vote = JSON.parse(await decryptMessage(privateKey, encryptedCommit)); - - return { identifier: request.identifier, time: request.time, price: vote.price.toString(), salt: vote.salt }; -}; - -// Optimally batch together vote commits in the fewest batches possible, -// each batchCommit is one Ethereum transaction. Return the number of successes -// and batches to the user -const batchCommitVotes = async (newCommitments, votingContract, account) => { - let successes = []; - let batches = 0; - for (let k = 0; k < newCommitments.length; k += BATCH_MAX_COMMITS) { - const maxBatchSize = newCommitments.slice(k, Math.min(k + BATCH_MAX_COMMITS, newCommitments.length)); - - // Always call `batchCommit`, even if there's only one commitment. Difference in gas cost is negligible. - const { receipt } = await votingContract.batchCommit( - maxBatchSize.map((commitment) => { - // This filters out the parts of the commitment that we don't need to send to solidity. - // Note: this isn't strictly necessary since web3 will only encode variables that share names with properties in - // the solidity struct. - // eslint-disable-next-line no-unused-vars - const { price, salt, ...rest } = commitment; - return rest; - }), - { from: account } - ); - - // Add the batch transaction hash to each commitment. - maxBatchSize.forEach((commitment) => { - commitment.txnHash = receipt.transactionHash; - }); - - // Append any of new batch's commitments to running commitment list - successes = successes.concat(maxBatchSize); - batches += 1; - } - - return { successes, batches }; -}; - -// Optimally batch together vote reveals in the fewest batches possible, -// each batchReveal is one Ethereum transaction. Return the number of successes -// and batches to the user -const batchRevealVotes = async (newReveals, votingContract, account) => { - let successes = []; - let batches = 0; - for (let k = 0; k < newReveals.length; k += BATCH_MAX_REVEALS) { - const maxBatchSize = newReveals.slice(k, Math.min(k + BATCH_MAX_REVEALS, newReveals.length)); - - // Always call `batchReveal`, even if there's only one reveal. Difference in gas cost is negligible. - const { receipt } = await votingContract.batchReveal(maxBatchSize, { from: account }); - - // Add the batch transaction hash to each reveal. - maxBatchSize.forEach((reveal) => { - reveal.txnHash = receipt.transactionHash; - }); - - // Append any of new batch's commitments to running commitment list - successes = successes.concat(maxBatchSize); - batches += 1; - } - - return { successes, batches }; -}; - -// Optimally batch together reward retrievals in the fewest batches possible, -// each retrieveRewards is one Ethereum transaction. Return the number of successes -// and batches to the user -const batchRetrieveRewards = async (requests, roundId, votingContract, votingAccount, signingAccount) => { - let successes = []; - let batches = 0; - for (let i = 0; i < requests.length; i += BATCH_MAX_RETRIEVALS) { - const maxBatchSize = requests.slice(i, Math.min(i + BATCH_MAX_RETRIEVALS, requests.length)); - const pendingRequests = []; - for (let j = 0; j < maxBatchSize.length; j++) { - pendingRequests.push({ identifier: maxBatchSize[j].identifier, time: maxBatchSize[j].time }); - } - - // Always call `retrieveRewards`, even if there's only one reward. Difference in gas cost is negligible. - const { receipt } = await votingContract.retrieveRewards(votingAccount, roundId, pendingRequests, { - from: signingAccount, - }); - - // Add the batch transaction hash to each reveal. - maxBatchSize.forEach((retrieve) => { - retrieve.txnHash = receipt.transactionHash; - }); - - // Append any of new batch's commitments to running commitment list - successes = successes.concat(maxBatchSize); - batches += 1; - } - - return { successes, batches }; -}; - -// Get the latest event matching the provided parameters. Assumes that all events from Voting.sol have indexed -// parameters for identifier, roundId, and voter. -const getLatestEvent = async (eventName, request, roundId, account, votingContract) => { - const events = await votingContract.getPastEvents(eventName, { - fromBlock: 0, - filter: { identifier: request.identifier, roundId: roundId.toString(), voter: account.toString() }, - }); - // Sort descending. Primary sort on block number. Secondary sort on transactionIndex. Tertiary sort on logIndex. - events.sort((a, b) => { - if (a.blockNumber !== b.blockNumber) { - return b.blockNumber - a.blockNumber; - } - - if (a.transactionIndex !== b.transactionIndex) { - return b.transactionIndex - a.transactionIndex; - } - - return b.logIndex - a.logIndex; - }); - for (const ev of events) { - if (ev.returnValues.time.toString() === request.time.toString()) { - return ev.returnValues; - } - } - return null; -}; - -module.exports = { - getVotingRoles, - getLatestEvent, - constructCommitment, - constructReveal, - batchCommitVotes, - batchRevealVotes, - batchRetrieveRewards, -}; diff --git a/packages/common/src/browser.ts b/packages/common/src/browser.ts new file mode 100644 index 00000000..5854c55f --- /dev/null +++ b/packages/common/src/browser.ts @@ -0,0 +1,24 @@ +// Only the browser-safe modules. +export * from "./AbiUtils"; +export * from "./AdminUtils"; +export * from "./Constants"; +export * from "./Constants"; +export * from "./AbiUtils"; +export * from "./AdminUtils"; +export * from "./ContractUtils"; +export * from "./TransactionUtils"; +export * from "./Crypto"; +export * from "./EmpUtils"; +export * from "./EncryptionHelper"; +export * from "./Enums"; +export * from "./FormattingUtils"; +export * from "./ObjectUtils"; +export * from "./PublicNetworks"; +export * from "./Random"; +export * from "./SolcoverConfig"; +export * from "./SolidityTestUtils"; +export * from "./TimeUtils"; +export * from "./PriceIdentifierUtils"; +export * from "./MultiDecimalTestHelper"; +export * from "./MultiVersionTestHelpers"; +export * from "./hardhat/fixtures"; diff --git a/packages/common/src/gckms/GckmsConfig.js b/packages/common/src/gckms/GckmsConfig.js deleted file mode 100644 index 70cb4981..00000000 --- a/packages/common/src/gckms/GckmsConfig.js +++ /dev/null @@ -1,112 +0,0 @@ -// Example usage: -// $(npm bin)/truffle exec --network test --keys priceFeed --keys registry - -const argv = require("minimist")(process.argv.slice()); -const fs = require("fs"); -require("dotenv").config(); - -// Grab the name property from each to get a list of the names of the public networks. -const publicNetworkNames = Object.values(require("../PublicNetworks.js").PublicNetworks).map((elt) => elt.name); -const { isPublicNetwork } = require("../MigrationUtils.js"); - -function arrayify(input) { - if (!input) return []; - if (!Array.isArray(input)) return [input]; - return input; -} - -// Note: this default config should not be used - it is intended to communicate the structure of the config. -// .gcloudKmsOverride.js should export your real config. -function getDefaultStaticConfig() { - // The anatomy of an individual config is: - // projectId: ID of a Google Cloud project - // keyRingId: ID of keyring - // cryptoKeyId: ID of the crypto key to use for decrypting the key material - // locationId: Google Cloud location, e.g., 'global'. - // ciphertextBucket: ID of a Google Cloud storage bucket. - // ciphertextFilename: Name of a file within `ciphertextBucket`. - - const defaultConfig = { - private: { - deployer: {}, - registry: {}, - store: {}, - priceFeed: {}, - sponsorWhitelist: {}, - returnCalculatorWhitelist: {}, - marginCurrencyWhitelist: {}, - // This is an example to show you what a typical config for a gcloud-stored config might look like. - example: { - projectId: "project-name", - locationId: "asia-east2", - keyRingId: "Keyring_Test", - cryptoKeyId: "keyname", - ciphertextBucket: "cipher_bucket", - ciphertextFilename: "ciphertext_fname.enc", - }, - }, - }; - - // Add a blank network config for all public networks so they don't fail to process but will fail if selected. - const blankNetworkConfig = { - deployer: {}, - registry: {}, - store: {}, - priceFeed: {}, - sponsorWhitelist: {}, - returnCalculatorWhitelist: {}, - marginCurrencyWhitelist: {}, - }; - - for (let name of publicNetworkNames) { - defaultConfig[name] = blankNetworkConfig; - } - - return defaultConfig; -} - -function getGckmsConfig(keys = arrayify(argv.keys), network = argv.network) { - let configOverride = {}; - - // If there is no env variable providing the config, attempt to pull it from a file. - // TODO: this is kinda hacky. We should refactor this to only take in the config using one method. - if (process.env.GCKMS_CONFIG) { - // If the env variable is present, just take that json. - configOverride = JSON.parse(process.env.GCKMS_CONFIG); - } else { - // Import the .GckmsOverride.js file if it exists. - // Note: this file is expected to be present in the same directory as this script. - let overrideFname = ".GckmsOverride.js"; - try { - if (fs.existsSync(`${__dirname}/${overrideFname}`)) { - configOverride = require(`./${overrideFname}`); - } - } catch (err) { - console.error(err); - } - } - - const getNetworkName = () => { - if (isPublicNetwork(network || "")) { - // Take everything before the underscore: - // mainnet_gckms -> mainnet. - return network.split("_")[0]; - } - - return "mainnet"; - }; - - // Compose the exact config for this network. - const staticConfig = { ...getDefaultStaticConfig(), ...configOverride }; - const networkConfig = staticConfig[getNetworkName()]; - - // Provide the configs for the keys requested. - const keyConfigs = keys.map((keyName) => { - return networkConfig[keyName]; - }); - - return keyConfigs; -} - -// Export the requested config. -module.exports = { getGckmsConfig }; diff --git a/packages/common/src/gckms/GckmsConfig.ts b/packages/common/src/gckms/GckmsConfig.ts new file mode 100644 index 00000000..c914f44f --- /dev/null +++ b/packages/common/src/gckms/GckmsConfig.ts @@ -0,0 +1,78 @@ +// Example usage: +// $(npm bin)/truffle exec --network test --keys priceFeed --keys registry +import minimist from "minimist"; +import fs from "fs"; +import dotenv from "dotenv"; +import { isPublicNetwork } from "../PublicNetworks"; + +const argv = minimist(process.argv.slice()); +dotenv.config(); + +// The anatomy of an individual config is: +// projectId: ID of a Google Cloud project +// keyRingId: ID of keyring +// cryptoKeyId: ID of the crypto key to use for decrypting the key material +// locationId: Google Cloud location, e.g., 'global'. +// ciphertextBucket: ID of a Google Cloud storage bucket. +// ciphertextFilename: Name of a file within `ciphertextBucket`. +export interface KeyConfig { + projectId: string; + locationId: string; + keyRingId: string; + cryptoKeyId: string; + ciphertextBucket: string; + ciphertextFilename: string; +} +export interface GckmsConfig { + [network: string]: { + [keyName: string]: KeyConfig; + }; +} + +function arrayify(input: string[] | string | undefined): string[] { + if (!input) return []; + if (!Array.isArray(input)) return [input]; + return input; +} + +export function getGckmsConfig(keys = arrayify(argv.keys), network = argv.network): KeyConfig[] { + let configOverride: GckmsConfig = {}; + + // If there is no env variable providing the config, attempt to pull it from a file. + // TODO: this is kinda hacky. We should refactor this to only take in the config using one method. + if (process.env.GCKMS_CONFIG) { + // If the env variable is present, just take that json. + configOverride = JSON.parse(process.env.GCKMS_CONFIG); + } else { + // Import the .GckmsOverride.js file if it exists. + // Note: this file is expected to be present in the same directory as this script. + const overrideFname = ".GckmsOverride.js"; + try { + if (fs.existsSync(`${__dirname}/${overrideFname}`)) { + configOverride = JSON.parse(fs.readFileSync(`./${overrideFname}`).toString("utf8")); + } + } catch (err) { + console.error(err); + } + } + + const getNetworkName = () => { + if (isPublicNetwork(network || "unknown")) { + // Take everything before the underscore: + // mainnet_gckms -> mainnet. + return network.split("_")[0]; + } + + return "mainnet"; + }; + + // Compose the exact config for this network. + const networkConfig = configOverride[getNetworkName()]; + + // Provide the configs for the keys requested. + const keyConfigs = keys.map((keyName) => { + return networkConfig[keyName] || {}; + }); + + return keyConfigs; +} diff --git a/packages/common/src/gckms/KeyInjectorPlugin.js b/packages/common/src/gckms/KeyInjectorPlugin.ts similarity index 80% rename from packages/common/src/gckms/KeyInjectorPlugin.js rename to packages/common/src/gckms/KeyInjectorPlugin.ts index 64cc4abd..39a99684 100644 --- a/packages/common/src/gckms/KeyInjectorPlugin.js +++ b/packages/common/src/gckms/KeyInjectorPlugin.ts @@ -1,7 +1,7 @@ -const { retrieveGckmsKeys } = require("./utils"); -const { extendConfig } = require("hardhat/config"); -const { HardhatPluginError } = require("hardhat/plugins"); -const { getGckmsConfig } = require("./GckmsConfig"); +import { retrieveGckmsKeys } from "./utils"; +import { extendConfig } from "hardhat/config"; +import { HardhatPluginError } from "hardhat/plugins"; +import { getGckmsConfig } from "./GckmsConfig"; // This plugin just injects GCKMS keys into the config. // Because it does so asynchonously, it creates a race condition. This means it may not work in all circumstances. diff --git a/packages/common/src/gckms/ManagedSecretProvider.js b/packages/common/src/gckms/ManagedSecretProvider.ts similarity index 57% rename from packages/common/src/gckms/ManagedSecretProvider.js rename to packages/common/src/gckms/ManagedSecretProvider.ts index 0df773c3..396b4e33 100644 --- a/packages/common/src/gckms/ManagedSecretProvider.js +++ b/packages/common/src/gckms/ManagedSecretProvider.ts @@ -1,9 +1,14 @@ -const HDWalletProvider = require("@truffle/hdwallet-provider"); -const { retrieveGckmsKeys } = require("./utils"); +import HDWalletProvider from "@truffle/hdwallet-provider"; +import { retrieveGckmsKeys } from "./utils"; +import type { KeyConfig } from "./GckmsConfig"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Tail2 = T extends [unknown, unknown, ...infer R] ? R : never; +type RemainingHDWalletArgs = Tail2>; // Wraps HDWalletProvider, deferring construction and allowing a Cloud KMS managed secret to be fetched asynchronously // and used to initialize an HDWalletProvider. -class ManagedSecretProvider { +export class ManagedSecretProvider { // cloudKmsSecretConfigs must either be: // a single config object which representing a mnemonic or a private key on GCKMS // or @@ -15,37 +20,44 @@ class ManagedSecretProvider { // locationId: Google Cloud location, e.g., 'global'. // ciphertextBucket: ID of a Google Cloud storage bucket. // ciphertextFilename: Name of a file within `ciphertextBucket`. - constructor(cloudKmsSecretConfigs, ...remainingArgs) { + private readonly remainingArgs: RemainingHDWalletArgs; + private wrappedProvider: null | HDWalletProvider; + private wrappedProviderPromise: Promise; + constructor( + private readonly cloudKmsSecretConfigs: KeyConfig[], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private readonly providerOrUrl: string | any, // Mirrors the type that HDWalletProvider expects. + ...remainingArgs: RemainingHDWalletArgs + ) { if (!Array.isArray(cloudKmsSecretConfigs)) { cloudKmsSecretConfigs = [cloudKmsSecretConfigs]; } - this.cloudKmsSecretConfigs = cloudKmsSecretConfigs; this.remainingArgs = remainingArgs; this.wrappedProvider = null; this.wrappedProviderPromise = this.getOrConstructWrappedProvider(); } // Passes the call through, by attaching a callback to the wrapper provider promise. - sendAsync(...all) { + sendAsync(...all: Parameters): ReturnType { this.wrappedProviderPromise.then((wrappedProvider) => { wrappedProvider.sendAsync(...all); }); } - // Passes the call through. Requires that the wrapped provider has been created via, e.g., `getOrConstructWrappedProvider`. - send(...all) { + // Passes the call through. Requires that the wrapped provider has been created via, e.g., `constructWrappedProvider`. + send(...all: Parameters): ReturnType { this.wrappedProviderPromise.then((wrappedProvider) => { wrappedProvider.send(...all); }); } - // Passes the call through. Requires that the wrapped provider has been created via, e.g., `getOrConstructWrappedProvider`. - getAddress(...all) { + // Passes the call through. Requires that the wrapped provider has been created via, e.g., `constructWrappedProvider`. + getAddress(...all: Parameters): ReturnType { return this.getWrappedProviderOrThrow().getAddress(...all); } // Returns the underlying wrapped provider. - getWrappedProviderOrThrow() { + getWrappedProviderOrThrow(): HDWalletProvider { if (this.wrappedProvider) { return this.wrappedProvider; } else { @@ -54,16 +66,14 @@ class ManagedSecretProvider { } // Returns a Promise that resolves to the wrapped provider. - async getOrConstructWrappedProvider() { + async getOrConstructWrappedProvider(): Promise { if (this.wrappedProvider) { return this.wrappedProvider; } const keys = await retrieveGckmsKeys(this.cloudKmsSecretConfigs); - this.wrappedProvider = new HDWalletProvider(keys, ...this.remainingArgs); + this.wrappedProvider = new HDWalletProvider(keys, this.providerOrUrl, ...this.remainingArgs); return this.wrappedProvider; } } - -module.exports = { ManagedSecretProvider }; diff --git a/packages/common/src/gckms/utils.js b/packages/common/src/gckms/utils.ts similarity index 72% rename from packages/common/src/gckms/utils.js rename to packages/common/src/gckms/utils.ts index d0e28798..6461b158 100644 --- a/packages/common/src/gckms/utils.js +++ b/packages/common/src/gckms/utils.ts @@ -1,5 +1,6 @@ -const kms = require("@google-cloud/kms"); -const { Storage } = require("@google-cloud/storage"); +import kms from "@google-cloud/kms"; +import { Storage } from "@google-cloud/storage"; +import type { KeyConfig } from "./GckmsConfig"; // This function takes an array of GCKMS configs that are shaped as follows: // { @@ -12,7 +13,7 @@ const { Storage } = require("@google-cloud/storage"); // } // // It returns an array of private keys that can be used to send transactions. -async function retrieveGckmsKeys(gckmsConfigs) { +export async function retrieveGckmsKeys(gckmsConfigs: KeyConfig[]): Promise { return await Promise.all( gckmsConfigs.map(async (config) => { const storage = new Storage(); @@ -26,7 +27,8 @@ async function retrieveGckmsKeys(gckmsConfigs) { const client = new kms.KeyManagementServiceClient(); const name = client.cryptoKeyPath(config.projectId, config.locationId, config.keyRingId, config.cryptoKeyId); const [result] = await client.decrypt({ name, ciphertext }); - return "0x" + Buffer.from(result.plaintext, "base64").toString().trim(); + if (!result.plaintext || result.plaintext instanceof Uint8Array) throw new Error("result.plaintext wrong type"); + return "0x" + Buffer.from(result.plaintext.toString(), "base64").toString().trim(); }) ); } diff --git a/packages/common/src/hardhat/fixtures/default.js b/packages/common/src/hardhat/fixtures/default.ts similarity index 78% rename from packages/common/src/hardhat/fixtures/default.js rename to packages/common/src/hardhat/fixtures/default.ts index b19e5d55..ff7401db 100644 --- a/packages/common/src/hardhat/fixtures/default.js +++ b/packages/common/src/hardhat/fixtures/default.ts @@ -1,13 +1,17 @@ -const { interfaceName } = require("../../Constants"); -const { RegistryRolesEnum } = require("../../Enums"); +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { interfaceName } from "../../Constants"; +import { RegistryRolesEnum } from "../../Enums"; +import { CombinedHRE } from "../tasks/types"; +import { DeploymentsExtension } from "hardhat-deploy/types"; -async function runDefaultFixture({ deployments }) { - const setup = deployments.createFixture(async ({ deployments, getNamedAccounts, web3 }) => { +export async function runDefaultFixture({ deployments }: { deployments: DeploymentsExtension }): Promise { + const setup = deployments.createFixture(async (hre: HardhatRuntimeEnvironment) => { + const { deployments, getNamedAccounts, web3 } = hre as CombinedHRE; // Cast because hardhat extension isn't well-typed. const { padRight, toWei, utf8ToHex } = web3.utils; await deployments.fixture(); const { deployer } = await getNamedAccounts(); - const getDeployment = async (name) => { + const getDeployment = async (name: string) => { const contract = await deployments.get(name); return new web3.eth.Contract(contract.abi, contract.address); }; @@ -15,9 +19,9 @@ async function runDefaultFixture({ deployments }) { // Setup finder. const finder = await getDeployment("Finder"); - const addToFinder = async (deploymentName, finderName) => { + const addToFinder = async (deploymentName: string, finderName: string) => { const { address } = await deployments.get(deploymentName); - const hexName = padRight(utf8ToHex(finderName)); + const hexName = padRight(utf8ToHex(finderName), 64); await finder.methods.changeImplementationAddress(hexName, address).send({ from: deployer }); }; @@ -61,5 +65,3 @@ async function runDefaultFixture({ deployments }) { }); await setup(); } - -module.exports = { runDefaultFixture }; diff --git a/packages/common/src/hardhat/fixtures/index.js b/packages/common/src/hardhat/fixtures/index.js deleted file mode 100644 index dc3e6d99..00000000 --- a/packages/common/src/hardhat/fixtures/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = { ...require("./default") }; diff --git a/packages/common/src/hardhat/fixtures/index.ts b/packages/common/src/hardhat/fixtures/index.ts new file mode 100644 index 00000000..f348fd43 --- /dev/null +++ b/packages/common/src/hardhat/fixtures/index.ts @@ -0,0 +1 @@ +export * from "./default"; diff --git a/packages/common/src/hardhat/index.js b/packages/common/src/hardhat/index.js deleted file mode 100644 index 026dec6c..00000000 --- a/packages/common/src/hardhat/index.js +++ /dev/null @@ -1,6 +0,0 @@ -const fs = require("fs"); -const path = require("path"); - -["tasks"].forEach((folder) => - fs.readdirSync(path.join(__dirname, folder)).forEach((mod) => require(path.join(__dirname, folder, mod))) -); diff --git a/packages/common/src/hardhat/index.ts b/packages/common/src/hardhat/index.ts new file mode 100644 index 00000000..1dcf9026 --- /dev/null +++ b/packages/common/src/hardhat/index.ts @@ -0,0 +1,9 @@ +import fs from "fs"; +import path from "path"; + +["tasks"].forEach((folder) => + fs + .readdirSync(path.join(__dirname, folder)) + .filter((path) => path.endsWith(".js")) + .forEach((mod) => require(path.join(__dirname, folder, mod))) +); diff --git a/packages/common/src/hardhat/plugins/ExtendedWeb3.js b/packages/common/src/hardhat/plugins/ExtendedWeb3.ts similarity index 61% rename from packages/common/src/hardhat/plugins/ExtendedWeb3.js rename to packages/common/src/hardhat/plugins/ExtendedWeb3.ts index 52ffe0d9..baa4e23e 100644 --- a/packages/common/src/hardhat/plugins/ExtendedWeb3.js +++ b/packages/common/src/hardhat/plugins/ExtendedWeb3.ts @@ -27,9 +27,46 @@ // eventReturnValues => eventReturnValues.someValueICareAbout === 5); // -const { extendEnvironment } = require("hardhat/config"); +import { extendEnvironment } from "hardhat/config"; +import type { HardhatRuntimeEnvironment, Artifact } from "hardhat/types"; +import type { ContractSendMethod, Contract, EventData } from "web3-eth-contract"; +import type Web3 from "web3"; +import type { DeploymentsExtension } from "hardhat-deploy/types"; -extendEnvironment((hre) => { +export interface ContractFactory extends Artifact { + deployed: () => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + new: (...args: any[]) => ContractSendMethod; + at: (address: string) => Contract; +} + +type FindEventFunction = ( + txnResult: { blockNumber: number }, + contract: Contract, + eventName: string, + fn: (eventValues: EventData["returnValues"]) => boolean +) => Promise<{ + match: EventData | undefined; + allEvents: EventData["returnValues"][]; +}>; + +export interface Extension { + _artifactCache: { [name: string]: Artifact }; + getContract: (name: string) => ContractFactory; + findEvent: FindEventFunction; + assertEventEmitted: (...args: Parameters) => void; + assertEventNotEmitted: (...args: Parameters) => void; +} + +interface OtherExtensions { + web3: Web3; + deployments: DeploymentsExtension; +} + +type HRE = Extension & OtherExtensions & HardhatRuntimeEnvironment; + +extendEnvironment((_hre) => { + const hre = _hre as HRE; hre._artifactCache = {}; hre.getContract = (name) => { if (!hre._artifactCache[name]) hre._artifactCache[name] = hre.artifacts.readArtifactSync(name); @@ -40,15 +77,20 @@ extendEnvironment((hre) => { return new hre.web3.eth.Contract(artifact.abi, deployment.address); }; - const newProp = (...args) => + const newProp = (...args: any[]) => new hre.web3.eth.Contract(artifact.abi, undefined).deploy({ data: artifact.bytecode, arguments: args }); - const at = (address) => new hre.web3.eth.Contract(artifact.abi, address); + const at = (address: string) => new hre.web3.eth.Contract(artifact.abi, address); return { ...artifact, deployed, new: newProp, at }; }; - hre.findEvent = async (txnResult, contract, eventName, fn = () => true) => { + hre.findEvent = async ( + txnResult, + contract, + eventName, + fn: (eventValues: EventData["returnValues"]) => boolean = () => true + ) => { // TODO: this can be improved by making sure the event falls in the correct transaction. const events = await contract.getPastEvents(eventName, { fromBlock: txnResult.blockNumber, @@ -65,7 +107,9 @@ extendEnvironment((hre) => { const { match, allEvents } = await hre.findEvent(txnResult, contract, eventName, fn); if (match === undefined) { - throw new Error(`No matching events found. Events found:\n\n${allEvents.map(JSON.stringify).join("\n\n")}`); + throw new Error( + `No matching events found. Events found:\n\n${allEvents.map((event) => JSON.stringify(event)).join("\n\n")}` + ); } }; diff --git a/packages/common/src/hardhat/tasks/artifacts.js b/packages/common/src/hardhat/tasks/artifacts.ts similarity index 92% rename from packages/common/src/hardhat/tasks/artifacts.js rename to packages/common/src/hardhat/tasks/artifacts.ts index ab6507df..7e130b38 100644 --- a/packages/common/src/hardhat/tasks/artifacts.js +++ b/packages/common/src/hardhat/tasks/artifacts.ts @@ -1,10 +1,11 @@ -const path = require("path"); -const fs = require("fs"); -const uniqBy = require("lodash.uniqby"); +import path from "path"; +import fs from "fs"; +import uniqBy from "lodash.uniqby"; +import { task, types } from "hardhat/config"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { CombinedHRE } from "./types"; -const { task, types } = require("hardhat/config"); - -function removeFileIfExists(filename) { +function removeFileIfExists(filename: string): void { try { fs.unlinkSync(filename); } catch (e) { @@ -12,18 +13,18 @@ function removeFileIfExists(filename) { } } -function normalizeClassName(name) { +function normalizeClassName(name: string): string { const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1); // Capitalize first letter. return capitalizedName.replace(/_/g, ""); // Remove underscores. } -async function getArtifactPathList(hre, relativeTo) { +async function getArtifactPathList(hre: HardhatRuntimeEnvironment, relativeTo: string) { const artifactPaths = await hre.artifacts.getArtifactPaths(); // Generate a unique list of artifacts and paths to them. Unique is necessary because there are some redundantly // named contracts. return uniqBy( - artifactPaths.map((artifactPath) => ({ + artifactPaths.map((artifactPath: string) => ({ contractName: path.basename(artifactPath).split(".")[0], relativePath: `./${path.relative(path.dirname(relativeTo), artifactPath)}`, })), @@ -31,16 +32,16 @@ async function getArtifactPathList(hre, relativeTo) { ); } -function getCorePath(hre, relativeTo) { +function getCorePath(hre: HardhatRuntimeEnvironment, relativeTo: string): string { const artifactPath = hre.config.paths.artifacts ? path.join(hre.config.paths.artifacts, "../") : "./"; return `./${path.relative(relativeTo, artifactPath)}`; } -function getAddressesMap(hre) { +function getAddressesMap(hre: HardhatRuntimeEnvironment) { // Generate a map of name => chain id => address. const networksPath = path.join(getCorePath(hre, "./"), "networks"); const dirs = fs.readdirSync(networksPath); - const addresses = {}; + const addresses: { [name: string]: { [chainId: number]: string } } = {}; for (const dir of dirs) { const chainId = parseInt(dir.split(".")[0]); const deployments = JSON.parse(fs.readFileSync(path.join(networksPath, dir), "utf8")); @@ -110,11 +111,11 @@ task("generate-contracts-frontend", "Generate typescipt for the contracts-fronte // to remove any unused json files. In modern versions of webpack, this should allow absolutely _no_ artifact // information that isn't needed to be pulled in. artifacts.forEach(({ contractName, relativePath }) => { - const abi = JSON.stringify(JSON.parse(fs.readFileSync(relativePath)).abi); + const abi = JSON.stringify(JSON.parse(fs.readFileSync(relativePath).toString("utf8")).abi); fs.appendFileSync(out, `export function get${contractName}Abi(): any[] { return JSON.parse(\`${abi}\`); }\n`); }); artifacts.forEach(({ contractName, relativePath }) => { - const bytecode = JSON.stringify(JSON.parse(fs.readFileSync(relativePath)).bytecode); + const bytecode = JSON.stringify(JSON.parse(fs.readFileSync(relativePath).toString("utf8")).bytecode); fs.appendFileSync(out, `export function get${contractName}Bytecode(): string { return \`${bytecode}\`; }\n`); }); @@ -238,11 +239,12 @@ export async function getAddress(name: DeploymentName | ContractName, chainId: n }); task("load-addresses", "Load addresses from the networks folder into the hardhat deployments folder").setAction( - async function (taskArguments, hre) { + async function (taskArguments, hre_) { + const hre = hre_ as CombinedHRE; // Generate chain id mapping. - const chainIdToNetworkName = {}; + const chainIdToNetworkName: { [chainId: number]: string } = {}; for (const [name, { chainId }] of Object.entries(hre.config.networks)) { - chainIdToNetworkName[chainId] = name; + if (chainId !== undefined) chainIdToNetworkName[chainId] = name; } const dirs = fs.readdirSync("./networks"); diff --git a/packages/common/src/hardhat/tasks/bridge.js b/packages/common/src/hardhat/tasks/bridge.ts similarity index 88% rename from packages/common/src/hardhat/tasks/bridge.js rename to packages/common/src/hardhat/tasks/bridge.ts index d58fcb10..d6244c09 100644 --- a/packages/common/src/hardhat/tasks/bridge.js +++ b/packages/common/src/hardhat/tasks/bridge.ts @@ -1,4 +1,5 @@ -const { task, types } = require("hardhat/config"); +import { task, types } from "hardhat/config"; +import type { CombinedHRE } from "./types"; // Use BLANK_FUNCTION_SIG if you don't want `deposit` or `executeProposal` to delegate a contract call. const BLANK_FUNCTION_SIG = "0x00000000"; @@ -17,7 +18,8 @@ task("register-generic-resource", "Admin can set generic resource ID on Bridge") ) .addOptionalParam("deposit", "Deposit function prototype string (e.g. func(uint256,bool))", "", types.string) .addOptionalParam("execute", "Contract to delegate call to for this resource ID", "", types.string) - .setAction(async function (taskArguments, hre) { + .setAction(async function (taskArguments, hre_) { + const hre = hre_ as CombinedHRE; // Cast to the extended HRE. const { deployments, getNamedAccounts, web3 } = hre; const { deployer } = await getNamedAccounts(); const { target, deposit, execute, rname, cid } = taskArguments; @@ -26,11 +28,11 @@ task("register-generic-resource", "Admin can set generic resource ID on Bridge") const { utf8ToHex, sha3 } = web3.utils; // Returns first 4 bytes of the sha3 hash of the function name including types/ - const _getFunctionSignature = (functionPrototypeString) => { - return sha3(utf8ToHex(functionPrototypeString)).substr(0, 10); + const _getFunctionSignature = (functionPrototypeString: string) => { + return sha3(utf8ToHex(functionPrototypeString))?.substr(0, 10); }; - const _getResourceId = (name, chainId) => { + const _getResourceId = (name: string, chainId: string) => { const encodedParams = web3.eth.abi.encodeParameters(["string", "uint8"], [name, chainId]); return web3.utils.soliditySha3(encodedParams); }; diff --git a/packages/common/src/hardhat/tasks/compile.js b/packages/common/src/hardhat/tasks/compile.ts similarity index 71% rename from packages/common/src/hardhat/tasks/compile.js rename to packages/common/src/hardhat/tasks/compile.ts index 336139d0..0624506d 100644 --- a/packages/common/src/hardhat/tasks/compile.js +++ b/packages/common/src/hardhat/tasks/compile.ts @@ -1,9 +1,14 @@ -const { getAllFilesInPath } = require("@uma/common"); +import { getAllFilesInPath } from "../../FileHelpers"; // This file is mostly taken from the modified `compile` task file written by Synthetix: https://github.com/Synthetixio/synthetix -const { internalTask } = require("hardhat/config"); -const { TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS } = require("hardhat/builtin-tasks/task-names"); +import { internalTask } from "hardhat/config"; +import type { HttpNetworkConfig } from "hardhat/types"; +import { TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS } from "hardhat/builtin-tasks/task-names"; + +interface ExtendedConfig extends HttpNetworkConfig { + compileWhitelist?: string[]; +} // This overrides a hardhat internal task, which is part of its compile task's lifecycle. // This allows us to filter on whitelisted, OVM-compatible contracts from the compilation list, @@ -23,10 +28,10 @@ internalTask(TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS, async (_, { config, network filePaths = [...filePaths, ...getAllFilesInPath(`${corePath}/contracts-ovm`)]; // Build absolute path for all directories on user-specified whitelist. - const whitelist = config.networks[network.name].compileWhitelist; + const whitelist = (config.networks[network.name] as ExtendedConfig).compileWhitelist; if (whitelist && Array.isArray(whitelist)) { - filePaths = filePaths.filter((filePath) => { - for (let whitelistedDir of whitelist) { + filePaths = filePaths.filter((filePath: string) => { + for (const whitelistedDir of whitelist) { if (filePath.includes(whitelistedDir)) return true; else continue; } diff --git a/packages/common/src/hardhat/tasks/finder.js b/packages/common/src/hardhat/tasks/finder.ts similarity index 86% rename from packages/common/src/hardhat/tasks/finder.js rename to packages/common/src/hardhat/tasks/finder.ts index c0f63af7..ce4cf679 100644 --- a/packages/common/src/hardhat/tasks/finder.js +++ b/packages/common/src/hardhat/tasks/finder.ts @@ -1,5 +1,12 @@ -const { task } = require("hardhat/config"); -const { interfaceName } = require("@uma/common"); +import { task } from "hardhat/config"; +import { interfaceName } from "../../Constants"; +import assert from "assert"; +import type { CombinedHRE } from "./types"; + +type InterfaceName = keyof typeof interfaceName; +function isInterfaceName(name: string | InterfaceName): name is InterfaceName { + return name in interfaceName; +} task("setup-finder", "Points Finder to DVM system contracts") .addFlag("registry", "Use if you want to set Registry") @@ -14,7 +21,8 @@ task("setup-finder", "Points Finder to DVM system contracts") .addFlag("sinkoracle", "Use if you want to set SinkOracle as the Oracle") .addFlag("prod", "Configure production setup in Finder") .addFlag("test", "Configure test setup in Finder") - .setAction(async function (taskArguments, hre) { + .setAction(async function (taskArguments, hre_) { + const hre = hre_ as CombinedHRE; const { deployments, getNamedAccounts, web3 } = hre; const { padRight, utf8ToHex } = web3.utils; const { deployer } = await getNamedAccounts(); @@ -62,9 +70,10 @@ task("setup-finder", "Points Finder to DVM system contracts") const Finder = await deployments.get("Finder"); const finder = new web3.eth.Contract(Finder.abi, Finder.address); console.log(`Using Finder @ ${finder.options.address}`); - for (let contractName of contractsToSet) { + for (const contractName of contractsToSet) { const deployed = await deployments.get(contractName); const contract = new web3.eth.Contract(deployed.abi, deployed.address); + if (!isInterfaceName(contractName)) throw new Error(`No mapped interface name for contract name ${contractName}`); const identifierHex = padRight(utf8ToHex(interfaceName[contractName]), 64); const currentlySetAddress = await finder.methods.interfacesImplemented(identifierHex).call(); if (currentlySetAddress !== contract.options.address) { diff --git a/packages/common/src/hardhat/tasks/fxTunnel.js b/packages/common/src/hardhat/tasks/fxTunnel.ts similarity index 77% rename from packages/common/src/hardhat/tasks/fxTunnel.js rename to packages/common/src/hardhat/tasks/fxTunnel.ts index 7c62d828..3644edfe 100644 --- a/packages/common/src/hardhat/tasks/fxTunnel.js +++ b/packages/common/src/hardhat/tasks/fxTunnel.ts @@ -1,6 +1,7 @@ -const { task, types } = require("hardhat/config"); -const MaticJs = require("@maticnetwork/maticjs"); -require("dotenv").config(); +import { task, types } from "hardhat/config"; +import MaticJs from "@maticnetwork/maticjs"; +import dotenv from "dotenv"; +dotenv.config(); // In order to receive a message on Ethereum from Polygon, `receiveMessage` must be called on the Root Tunnel contract // with a proof derived from the Polygon transaction hash that was checkpointed to Mainnet. @@ -22,9 +23,14 @@ task("root-chain-manager-proof", "Generate proof needed to receive data from roo ? `https://goerli.infura.io/v3/${process.env.INFURA_API_KEY}` : `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`, }); + + // Note: we use a private member of maticPosClient, so we cast to get rid of the error. + const castedMaticPOSClient = (maticPOSClient as unknown) as { + posRootChainManager: typeof maticPOSClient["posRootChainManager"]; + }; // This method will fail if the Polygon transaction hash has not been checkpointed to Mainnet yet. Checkpoints // happen roughly every hour. - const proof = await maticPOSClient.posRootChainManager.customPayload( + const proof = await castedMaticPOSClient.posRootChainManager.customPayload( hash, "0x8c5261668696ce22758910d05bab8f186d6eb247ceac2af2e82c7dc17669b036" // SEND_MESSAGE_EVENT_SIG, do not change ); diff --git a/packages/common/src/hardhat/tasks/identifiers.js b/packages/common/src/hardhat/tasks/identifiers.ts similarity index 77% rename from packages/common/src/hardhat/tasks/identifiers.js rename to packages/common/src/hardhat/tasks/identifiers.ts index dab252bc..5869bd63 100644 --- a/packages/common/src/hardhat/tasks/identifiers.js +++ b/packages/common/src/hardhat/tasks/identifiers.ts @@ -1,9 +1,18 @@ -const { task, types } = require("hardhat/config"); -require("dotenv").config(); -const assert = require("assert"); -const Web3 = require("web3"); +import { task, types } from "hardhat/config"; +import assert from "assert"; +import Web3 from "web3"; +import dotenv from "dotenv"; +import type { Contract } from "web3-eth-contract"; +import type { TransactionReceipt } from "web3-core"; +import type { CombinedHRE } from "./types"; +dotenv.config(); -const _whitelistIdentifier = async (web3, identifierUtf8, identifierWhitelist, deployer) => { +const _whitelistIdentifier = async ( + web3: Web3, + identifierUtf8: string, + identifierWhitelist: Contract, + deployer: string +) => { const { padRight, utf8ToHex } = web3.utils; const identifierBytes = padRight(utf8ToHex(identifierUtf8), 64); if (!(await identifierWhitelist.methods.isIdentifierSupported(identifierBytes).call())) { @@ -14,9 +23,14 @@ const _whitelistIdentifier = async (web3, identifierUtf8, identifierWhitelist, d } }; +function isString(input: string | null): input is string { + return typeof input === "string"; +} + task("whitelist-identifiers", "Whitelist identifiers from JSON file") .addParam("id", "Custom identifier to whitelist", "Test Identifier", types.string) - .setAction(async function (taskArguments, hre) { + .setAction(async function (taskArguments, hre_) { + const hre = hre_ as CombinedHRE; const { deployments, getNamedAccounts, web3 } = hre; const { deployer } = await getNamedAccounts(); const { id } = taskArguments; @@ -37,7 +51,8 @@ task("migrate-identifiers", "Adds all whitelisted identifiers on one IdentifierW false, types.boolean ) - .setAction(async function (taskArguments, hre) { + .setAction(async function (taskArguments, hre_) { + const hre = hre_ as CombinedHRE; const { deployments, getNamedAccounts, web3 } = hre; const { deployer } = await getNamedAccounts(); const { from, to, crosschain } = taskArguments; @@ -61,35 +76,42 @@ task("migrate-identifiers", "Adds all whitelisted identifiers on one IdentifierW // Filter out identifiers that are not currently whitelisted. const isIdentifierSupported = await Promise.all( - addedIdentifierEvents.map((_event) => + addedIdentifierEvents.map((_event): boolean => oldWhitelist.methods.isIdentifierSupported(_event.returnValues.identifier).call() ) ); const identifiersToWhitelist = isIdentifierSupported .map((isOnWhitelist, i) => { - if (isOnWhitelist) return addedIdentifierEvents[i].returnValues.identifier; + // Cast to help typescript discern the type. + if (isOnWhitelist) return addedIdentifierEvents[i].returnValues.identifier as string; return null; }) - .filter((id) => id); + .filter(isString); + + interface TableElement { + identifierToWhitelist: string; + utf8: string; + txn?: string; + } // Create table with results to display to user: - let resultsTable = identifiersToWhitelist.map((id) => { + const resultsTable: TableElement[] = identifiersToWhitelist.map((id) => { return { identifierToWhitelist: id, utf8: web3.utils.hexToUtf8(id) }; }); if (to) { const newWhitelist = new web3.eth.Contract(IdentifierWhitelist.abi, to); const isIdentifierSupportedOnNewWhitelist = await Promise.all( - identifiersToWhitelist.map((id) => newWhitelist.methods.isIdentifierSupported(id).call()) + identifiersToWhitelist.map((id) => newWhitelist.methods.isIdentifierSupported(id).call() as boolean) ); // Send transactions sequentially to avoid nonce collisions. Note that this might fail due to timeout if there // are a lot of transactions to send or the gas price to send with is too low. for (let i = 0; i < isIdentifierSupportedOnNewWhitelist.length; i++) { if (!isIdentifierSupportedOnNewWhitelist[i]) { - const receipt = await newWhitelist.methods + const receipt = (await newWhitelist.methods .addSupportedIdentifier(identifiersToWhitelist[i]) - .send({ from: deployer }); + .send({ from: deployer })) as TransactionReceipt; console.log( `${i}: Added new identifier ${web3.utils.hexToUtf8(identifiersToWhitelist[i])} (${receipt.transactionHash})` ); diff --git a/packages/common/src/hardhat/tasks/oracle.js b/packages/common/src/hardhat/tasks/oracle.ts similarity index 75% rename from packages/common/src/hardhat/tasks/oracle.js rename to packages/common/src/hardhat/tasks/oracle.ts index 3a08f3c6..ffdca372 100644 --- a/packages/common/src/hardhat/tasks/oracle.js +++ b/packages/common/src/hardhat/tasks/oracle.ts @@ -1,11 +1,13 @@ -const { task, types } = require("hardhat/config"); +import { task, types } from "hardhat/config"; +import { CombinedHRE } from "./types"; task("check-price", "Check whether price has resolved for Oracle and return price") .addParam("oracle", "OracleInterface implementation address to check", undefined, types.string) .addParam("identifier", "Request identifier", undefined, types.string) .addParam("timestamp", "Request timestamp", undefined, types.string) .addParam("ancillary", "Request ancillary data", undefined, types.string) - .setAction(async function (taskArguments, hre) { + .setAction(async function (taskArguments, hre_) { + const hre = hre_ as CombinedHRE; const { deployments, getNamedAccounts, web3 } = hre; const { deployer } = await getNamedAccounts(); const { oracle, identifier, timestamp, ancillary } = taskArguments; @@ -14,13 +16,15 @@ task("check-price", "Check whether price has resolved for Oracle and return pric const registry = new web3.eth.Contract(Registry.abi, Registry.address); console.log(`Checking Registry @ ${registry.options.address}`); - const isRegistered = await registry.methods.isContractRegistered(deployer).call(); + const isRegistered = (await registry.methods.isContractRegistered(deployer).call()) as boolean; if (!isRegistered) { console.log("Caller not registered"); } else { const Oracle = await deployments.get("OracleAncillaryInterface"); const oracleContract = new web3.eth.Contract(Oracle.abi, oracle); - const hasPrice = await oracleContract.methods.hasPrice(identifier, timestamp, ancillary).call({ from: deployer }); + const hasPrice = (await oracleContract.methods + .hasPrice(identifier, timestamp, ancillary) + .call({ from: deployer })) as boolean; console.log(`Oracle @ ${oracle} ${hasPrice ? "has" : "does not have"} a price`); if (hasPrice) { const price = await oracleContract.methods.getPrice(identifier, timestamp, ancillary).call({ from: deployer }); diff --git a/packages/common/src/hardhat/tasks/registry.js b/packages/common/src/hardhat/tasks/registry.ts similarity index 82% rename from packages/common/src/hardhat/tasks/registry.js rename to packages/common/src/hardhat/tasks/registry.ts index b350747a..8ff2711a 100644 --- a/packages/common/src/hardhat/tasks/registry.js +++ b/packages/common/src/hardhat/tasks/registry.ts @@ -1,7 +1,9 @@ -const { task, types } = require("hardhat/config"); -const { RegistryRolesEnum } = require("../../Enums"); +import { task, types } from "hardhat/config"; +import { RegistryRolesEnum } from "../../Enums"; +import { Contract } from "web3-eth-contract"; +import { CombinedHRE } from "./types"; -const _registerAccount = async (account, registry, deployer) => { +const _registerAccount = async (account: string, registry: Contract, deployer: string) => { const isRegistered = await registry.methods.isContractRegistered(account).call(); if (!isRegistered) { console.log(`Registering ${account}...`); @@ -14,7 +16,8 @@ const _registerAccount = async (account, registry, deployer) => { task("register-accounts", "Register deployer plus custom account with Registry capable of making price requests") .addOptionalParam("account", "Custom account to register", "", types.string) - .setAction(async function (taskArguments, hre) { + .setAction(async function (taskArguments, hre_) { + const hre = hre_ as CombinedHRE; const { deployments, getNamedAccounts, web3 } = hre; const { deployer } = await getNamedAccounts(); const { account } = taskArguments; diff --git a/packages/common/src/hardhat/tasks/test.js b/packages/common/src/hardhat/tasks/test.ts similarity index 60% rename from packages/common/src/hardhat/tasks/test.js rename to packages/common/src/hardhat/tasks/test.ts index c0d4136e..61b0034f 100644 --- a/packages/common/src/hardhat/tasks/test.js +++ b/packages/common/src/hardhat/tasks/test.ts @@ -1,5 +1,11 @@ -const { internalTask } = require("hardhat/config"); -const { TASK_TEST_GET_TEST_FILES } = require("hardhat/builtin-tasks/task-names"); +import { internalTask } from "hardhat/config"; +import type { HttpNetworkConfig } from "hardhat/types"; +import { TASK_TEST_GET_TEST_FILES } from "hardhat/builtin-tasks/task-names"; + +interface ExtendedNetworkConfig extends HttpNetworkConfig { + testBlacklist?: string[]; + testWhitelist?: string[]; +} // This overrides a hardhat internal task, which is part of its test task's lifecycle. This allows us to only run tests // that are compatible with a given network config, which are described by entries in a hardhat network's @@ -12,11 +18,13 @@ const { TASK_TEST_GET_TEST_FILES } = require("hardhat/builtin-tasks/task-names") internalTask(TASK_TEST_GET_TEST_FILES, async (_, { config, network }, runSuper) => { let filePaths = await runSuper(); + const networkConfig = config.networks[network.name] as ExtendedNetworkConfig; // Cast to allow extra props. + // Build absolute path for all directories on user-specified whitelist. - const whitelist = config.networks[network.name].testWhitelist; + const whitelist = networkConfig.testWhitelist; if (whitelist && Array.isArray(whitelist)) { - filePaths = filePaths.filter((filePath) => { - for (let whitelistString of whitelist) { + filePaths = filePaths.filter((filePath: string) => { + for (const whitelistString of whitelist) { if (filePath.includes(whitelistString)) return true; else continue; } @@ -25,10 +33,10 @@ internalTask(TASK_TEST_GET_TEST_FILES, async (_, { config, network }, runSuper) } // Some tests should not be run using hardhat. Define a `testBlacklist`. Ignore any tests that contain the blacklist. - const blacklist = config.networks[network.name].testBlacklist; + const blacklist = networkConfig.testBlacklist; if (blacklist && Array.isArray(blacklist)) { - filePaths = filePaths.filter((filePath) => { - for (let blacklistedString of blacklist) { + filePaths = filePaths.filter((filePath: string) => { + for (const blacklistedString of blacklist) { if (filePath.includes(blacklistedString)) return false; else continue; } diff --git a/packages/common/src/hardhat/tasks/types.ts b/packages/common/src/hardhat/tasks/types.ts new file mode 100644 index 00000000..f64f8f84 --- /dev/null +++ b/packages/common/src/hardhat/tasks/types.ts @@ -0,0 +1,35 @@ +import type { DeploymentsExtension } from "hardhat-deploy/types"; +import type { HardhatRuntimeEnvironment } from "hardhat/types/runtime"; +import type { EthereumProvider } from "hardhat/types"; +import type { Extension as ExtendedWeb3 } from "../plugins/ExtendedWeb3"; +import type Web3 from "web3"; + +type Address = string; + +interface DeployExtension { + deployments: DeploymentsExtension; + getNamedAccounts: () => Promise<{ + [name: string]: Address; + }>; + getUnnamedAccounts: () => Promise; + getChainId(): Promise; + companionNetworks: { + [name: string]: { + deployments: DeploymentsExtension; + getNamedAccounts: () => Promise<{ + [name: string]: Address; + }>; + getUnnamedAccounts: () => Promise; + getChainId(): Promise; + provider: EthereumProvider; + }; + }; +} + +// Assumes this includes the normal HRE, hardhat-deploy extension, the ExtendedWeb3 extension and the web3 extension. +// TODO: figure out how to get the extended HRE from hardhat-deploy. +export type CombinedHRE = HardhatRuntimeEnvironment & + DeployExtension & + ExtendedWeb3 & { + web3: Web3; + }; diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts new file mode 100644 index 00000000..42400ec9 --- /dev/null +++ b/packages/common/src/index.ts @@ -0,0 +1,9 @@ +export * from "./browser"; +export * from "./gckms/ManagedSecretProvider"; +export * from "./FileHelpers"; +export * from "./MetaMaskTruffleProvider"; +export * from "./TruffleConfig"; +export * from "./ProviderUtils"; +export * from "./HardhatConfig"; +export * from "./RetryProvider"; +export * from "./UniswapV3Helpers"; diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts new file mode 100644 index 00000000..40c671d4 --- /dev/null +++ b/packages/common/src/types.ts @@ -0,0 +1,3 @@ +import type _Web3 from "web3"; + +export type BN = ReturnType<_Web3["utils"]["toBN"]>; diff --git a/packages/common/test/TransactionUtils.js b/packages/common/test/TransactionUtils.js index 5897daf8..9dbd1c90 100644 --- a/packages/common/test/TransactionUtils.js +++ b/packages/common/test/TransactionUtils.js @@ -1,5 +1,5 @@ // Script to test -const TransactionUtils = require("../src/TransactionUtils"); +const TransactionUtils = require("../dist/TransactionUtils"); const { getTruffleContract, getAbi } = require("@uma/core"); const ERC20 = getTruffleContract("BasicERC20", web3); const ERC20ABI = getAbi("BasicERC20"); diff --git a/packages/common/tsconfig.json b/packages/common/tsconfig.json index 1f9cbdfe..46c48294 100644 --- a/packages/common/tsconfig.json +++ b/packages/common/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "@tsconfig/node14/tsconfig.json", - "include": ["src/**/*", "index.js"], + "include": ["src/**/*", "index.ts"], + "exclude": ["dist/**/*"], "compilerOptions": { + "typeRoots": ["./types", "./node_modules/@types"], "outDir": "dist", "declaration": true, - "emitDeclarationOnly": true, - "allowJs": true + "resolveJsonModule": true } } diff --git a/packages/common/types/abi-decoder/index.d.ts b/packages/common/types/abi-decoder/index.d.ts new file mode 100644 index 00000000..72f2177c --- /dev/null +++ b/packages/common/types/abi-decoder/index.d.ts @@ -0,0 +1,8 @@ +declare module "abi-decoder" { + export function addABI(abi: any | any[]): void; + export function decodeMethod(data: string): { name: string; params: any }; + export function getMethodids(): { [signature: string]: any }; + export function getABIs(): any[]; + export function decodeLogs(logs: any[]): { name: string; events: any[]; address: string }[]; + export function removeABI(abi: any | any[]): void; +} \ No newline at end of file diff --git a/packages/common/types/node-metamask/index.d.ts b/packages/common/types/node-metamask/index.d.ts new file mode 100644 index 00000000..7b4ddd08 --- /dev/null +++ b/packages/common/types/node-metamask/index.d.ts @@ -0,0 +1,35 @@ + +declare module "node-metamask" { + + interface JsonRpcPayload { + jsonrpc: string; + method: string; + params: any[]; + id?: string | number; + } + + interface JsonRpcResponse { + jsonrpc: string; + id: number; + result?: any; + error?: string; + } + + class RemoteMetaMaskProvider { + constructor(metamaskConnector: MetamaskConnector); + sendAsync(payload: JsonRpcPayload, callback: (error: Error | null, result?: JsonRpcResponse) => void): void; + send(payload: JsonRpcPayload, callback: (error: Error | null, result?: JsonRpcResponse) => void): void; + } + + export default class MetamaskConnector { + constructor(options: any); + start(): Promise; + stop(): Promise; + ready(): boolean; + send(action: any, requestId: any, payload: any, requiredAction: any): Promise<{ requestId: any, result: any }>; + getProvider(): RemoteMetaMaskProvider; + static handleAction(action: any, requestId: any, payload: any): { responseAction: any; responseRequestId: any, responsePayload: any }; + static handleMessage(msg: string): ReturnType; + }; + +} diff --git a/packages/common/types/umaprotocol__truffle-ledger-provider/index.d.ts b/packages/common/types/umaprotocol__truffle-ledger-provider/index.d.ts new file mode 100644 index 00000000..7ecf56e6 --- /dev/null +++ b/packages/common/types/umaprotocol__truffle-ledger-provider/index.d.ts @@ -0,0 +1,21 @@ +declare module "@umaprotocol/truffle-ledger-provider" { + interface JsonRpcPayload { + jsonrpc: string; + method: string; + params: any[]; + id?: string | number; + } + + interface JsonRpcResponse { + jsonrpc: string; + id: number; + result?: any; + error?: string; + } + + export default class TruffleLedgerProvider { + constructor(options: any, urlOrProvider: any); + sendAsync(payload: JsonRpcPayload, callback: (error: Error | null, result?: JsonRpcResponse) => void): void; + send(payload: JsonRpcPayload, callback: (error: Error | null, result?: JsonRpcResponse) => void): void; + } +} \ No newline at end of file diff --git a/packages/common/types/umaprotocol__ynatm/index.d.ts b/packages/common/types/umaprotocol__ynatm/index.d.ts new file mode 100644 index 00000000..af1a1276 --- /dev/null +++ b/packages/common/types/umaprotocol__ynatm/index.d.ts @@ -0,0 +1,17 @@ +declare module "@umaprotocol/ynatm" { + export const EXPONENTIAL: (base?: number, inGwei?: boolean) => (arg: { x?: number }) => number; + export const LINEAR: (slope?: number, inGwei?: boolean) => (arg: { x?: number, c?: number }) => number; + export const DOUBLES: (arg: { y?: number }) => number; + export const toGwei: (x: number) => number; + + export interface SendArgs { + sendTransactionFunction: (gasPrice: number) => T; + minGasPrice: string | number; + maxGasPrice: string | number; + gasPriceScalingFunction?: (args: { x?: number, y?: number, c?: number }) => number; + delay?: number; + rejectImmediatelyOnCondition?: (e: Error) => boolean; + } + + export const send: (args: SendArgs) => T; +} \ No newline at end of file diff --git a/packages/core/migrations/10_deploy_designated_voting_factory.js b/packages/core/migrations/10_deploy_designated_voting_factory.js index ebec36a2..e0fa82cf 100644 --- a/packages/core/migrations/10_deploy_designated_voting_factory.js +++ b/packages/core/migrations/10_deploy_designated_voting_factory.js @@ -1,6 +1,6 @@ const DesignatedVotingFactory = artifacts.require("DesignatedVotingFactory"); const Finder = artifacts.require("Finder"); -const { getKeysForNetwork, deploy } = require("@uma/common"); +const { getKeysForNetwork, deploy } = require("./MigrationUtils"); module.exports = async function (deployer, network, accounts) { const keys = getKeysForNetwork(network, accounts); diff --git a/packages/core/migrations/11_deploy_optimistic_oracle.js b/packages/core/migrations/11_deploy_optimistic_oracle.js index 9c8fd37b..c264f8e1 100644 --- a/packages/core/migrations/11_deploy_optimistic_oracle.js +++ b/packages/core/migrations/11_deploy_optimistic_oracle.js @@ -2,13 +2,8 @@ const OptimisticOracle = artifacts.require("OptimisticOracle"); const Finder = artifacts.require("Finder"); const Timer = artifacts.require("Timer"); const Registry = artifacts.require("Registry"); -const { - getKeysForNetwork, - deploy, - enableControllableTiming, - interfaceName, - RegistryRolesEnum, -} = require("@uma/common"); +const { interfaceName, RegistryRolesEnum } = require("@uma/common"); +const { getKeysForNetwork, deploy, enableControllableTiming } = require("./MigrationUtils"); module.exports = async function (deployer, network, accounts) { const keys = getKeysForNetwork(network, accounts); diff --git a/packages/core/migrations/12_support_identifiers.js b/packages/core/migrations/12_support_identifiers.js index c1f86584..558c7f36 100644 --- a/packages/core/migrations/12_support_identifiers.js +++ b/packages/core/migrations/12_support_identifiers.js @@ -1,5 +1,5 @@ const IdentiferWhitelist = artifacts.require("IdentifierWhitelist"); -const { getKeysForNetwork } = require("@uma/common"); +const { getKeysForNetwork } = require("./MigrationUtils"); const identifiers = require("../config/identifiers"); module.exports = async function (deployer, network, accounts) { diff --git a/packages/core/migrations/13_deploy_testnet_token.js b/packages/core/migrations/13_deploy_testnet_token.js index 91b09e7e..b0a95920 100644 --- a/packages/core/migrations/13_deploy_testnet_token.js +++ b/packages/core/migrations/13_deploy_testnet_token.js @@ -1,5 +1,6 @@ const TestnetERC20 = artifacts.require("TestnetERC20"); -const { deploy, setToExistingAddress, getKeysForNetwork, PublicNetworks } = require("@uma/common"); +const { PublicNetworks } = require("@uma/common"); +const { deploy, setToExistingAddress, getKeysForNetwork } = require("./MigrationUtils"); module.exports = async function (deployer, network, accounts) { const keys = getKeysForNetwork(network, accounts); diff --git a/packages/core/migrations/14_deploy_tokenfactory.js b/packages/core/migrations/14_deploy_tokenfactory.js index 4f681ff1..856ed632 100644 --- a/packages/core/migrations/14_deploy_tokenfactory.js +++ b/packages/core/migrations/14_deploy_tokenfactory.js @@ -1,5 +1,5 @@ const TokenFactory = artifacts.require("TokenFactory"); -const { getKeysForNetwork, deploy } = require("@uma/common"); +const { getKeysForNetwork, deploy } = require("./MigrationUtils"); module.exports = async function (deployer, network, accounts) { const keys = getKeysForNetwork(network, accounts); diff --git a/packages/core/migrations/15_deploy_expiring_multi_party_creator.js b/packages/core/migrations/15_deploy_expiring_multi_party_creator.js index a0574425..53771e4c 100644 --- a/packages/core/migrations/15_deploy_expiring_multi_party_creator.js +++ b/packages/core/migrations/15_deploy_expiring_multi_party_creator.js @@ -6,13 +6,8 @@ const TokenFactory = artifacts.require("TokenFactory"); const Timer = artifacts.require("Timer"); const Registry = artifacts.require("Registry"); const TestnetERC20 = artifacts.require("TestnetERC20"); -const { - RegistryRolesEnum, - interfaceName, - getKeysForNetwork, - deploy, - enableControllableTiming, -} = require("@uma/common"); +const { interfaceName, RegistryRolesEnum } = require("@uma/common"); +const { getKeysForNetwork, deploy, enableControllableTiming } = require("./MigrationUtils"); module.exports = async function (deployer, network, accounts) { const keys = getKeysForNetwork(network, accounts); diff --git a/packages/core/migrations/16_deploy_local_weth.js b/packages/core/migrations/16_deploy_local_weth.js index 01f69bfe..55d9449e 100644 --- a/packages/core/migrations/16_deploy_local_weth.js +++ b/packages/core/migrations/16_deploy_local_weth.js @@ -1,6 +1,7 @@ const WETH9 = artifacts.require("WETH9"); const AddressWhitelist = artifacts.require("AddressWhitelist"); -const { deploy, setToExistingAddress, getKeysForNetwork, PublicNetworks } = require("@uma/common"); +const { PublicNetworks } = require("@uma/common"); +const { deploy, setToExistingAddress, getKeysForNetwork } = require("./MigrationUtils"); module.exports = async function (deployer, network, accounts) { const keys = getKeysForNetwork(network, accounts); diff --git a/packages/core/migrations/17_deploy_perpetual_creator.js b/packages/core/migrations/17_deploy_perpetual_creator.js index 12310b1a..07ada113 100644 --- a/packages/core/migrations/17_deploy_perpetual_creator.js +++ b/packages/core/migrations/17_deploy_perpetual_creator.js @@ -4,7 +4,8 @@ const PerpetualLib = artifacts.require("PerpetualLib"); const TokenFactory = artifacts.require("TokenFactory"); const Timer = artifacts.require("Timer"); const Registry = artifacts.require("Registry"); -const { RegistryRolesEnum, getKeysForNetwork, deploy, enableControllableTiming } = require("@uma/common"); +const { RegistryRolesEnum } = require("@uma/common"); +const { getKeysForNetwork, deploy, enableControllableTiming } = require("./MigrationUtils"); module.exports = async function (deployer, network, accounts) { const keys = getKeysForNetwork(network, accounts); diff --git a/packages/core/migrations/1_initial_migration.js b/packages/core/migrations/1_initial_migration.js index f8254683..a852c25a 100644 --- a/packages/core/migrations/1_initial_migration.js +++ b/packages/core/migrations/1_initial_migration.js @@ -1,5 +1,5 @@ const Migrations = artifacts.require("./Migrations.sol"); -const { getKeysForNetwork, deploy } = require("@uma/common"); +const { getKeysForNetwork, deploy } = require("./MigrationUtils"); module.exports = async function (deployer, network, accounts) { const keys = getKeysForNetwork(network, accounts); diff --git a/packages/core/migrations/2_deploy_finder.js b/packages/core/migrations/2_deploy_finder.js index 4ce98208..7ab1c951 100644 --- a/packages/core/migrations/2_deploy_finder.js +++ b/packages/core/migrations/2_deploy_finder.js @@ -1,5 +1,5 @@ const Finder = artifacts.require("Finder"); -const { getKeysForNetwork, deploy } = require("@uma/common"); +const { getKeysForNetwork, deploy } = require("./MigrationUtils"); module.exports = async function (deployer, network, accounts) { const keys = getKeysForNetwork(network, accounts); diff --git a/packages/core/migrations/3_deploy_timer.js b/packages/core/migrations/3_deploy_timer.js index 62bec51c..887c8a87 100644 --- a/packages/core/migrations/3_deploy_timer.js +++ b/packages/core/migrations/3_deploy_timer.js @@ -1,5 +1,5 @@ const Timer = artifacts.require("Timer"); -const { getKeysForNetwork, deploy, enableControllableTiming } = require("@uma/common"); +const { getKeysForNetwork, deploy, enableControllableTiming } = require("./MigrationUtils"); module.exports = async function (deployer, network, accounts) { const keys = getKeysForNetwork(network, accounts); diff --git a/packages/core/migrations/4_deploy_voting_token.js b/packages/core/migrations/4_deploy_voting_token.js index f3a2f0cc..531ca1d2 100644 --- a/packages/core/migrations/4_deploy_voting_token.js +++ b/packages/core/migrations/4_deploy_voting_token.js @@ -5,7 +5,7 @@ const argv = require("minimist")(process.argv.slice(), { string: ["token_convers const VotingToken = artifacts.require("VotingToken"); const TokenMigrator = artifacts.require("TokenMigrator"); -const { getKeysForNetwork, deploy } = require("@uma/common"); +const { getKeysForNetwork, deploy } = require("./MigrationUtils"); const minterRoleEnumValue = 1; diff --git a/packages/core/migrations/5_deploy_voting.js b/packages/core/migrations/5_deploy_voting.js index 340f41d2..9be4838c 100644 --- a/packages/core/migrations/5_deploy_voting.js +++ b/packages/core/migrations/5_deploy_voting.js @@ -3,7 +3,8 @@ const Voting = artifacts.require("Voting"); const VotingToken = artifacts.require("VotingToken"); const IdentifierWhitelist = artifacts.require("IdentifierWhitelist"); const Timer = artifacts.require("Timer"); -const { getKeysForNetwork, deploy, enableControllableTiming, interfaceName } = require("@uma/common"); +const { interfaceName } = require("@uma/common"); +const { getKeysForNetwork, deploy, enableControllableTiming } = require("./MigrationUtils"); module.exports = async function (deployer, network, accounts) { const keys = getKeysForNetwork(network, accounts); diff --git a/packages/core/migrations/6_deploy_registry.js b/packages/core/migrations/6_deploy_registry.js index 5b83420f..a29f4bea 100644 --- a/packages/core/migrations/6_deploy_registry.js +++ b/packages/core/migrations/6_deploy_registry.js @@ -1,6 +1,7 @@ const Finder = artifacts.require("Finder"); const Registry = artifacts.require("Registry"); -const { getKeysForNetwork, deploy, interfaceName } = require("@uma/common"); +const { interfaceName } = require("@uma/common"); +const { getKeysForNetwork, deploy } = require("./MigrationUtils"); module.exports = async function (deployer, network, accounts) { const keys = getKeysForNetwork(network, accounts); diff --git a/packages/core/migrations/7_deploy_financial_contracts_admin.js b/packages/core/migrations/7_deploy_financial_contracts_admin.js index d25c9f12..896a6b36 100644 --- a/packages/core/migrations/7_deploy_financial_contracts_admin.js +++ b/packages/core/migrations/7_deploy_financial_contracts_admin.js @@ -1,6 +1,7 @@ const FinancialContractsAdmin = artifacts.require("FinancialContractsAdmin"); const Finder = artifacts.require("Finder"); -const { getKeysForNetwork, deploy, interfaceName } = require("@uma/common"); +const { interfaceName } = require("@uma/common"); +const { getKeysForNetwork, deploy } = require("./MigrationUtils"); module.exports = async function (deployer, network, accounts) { const keys = getKeysForNetwork(network, accounts); diff --git a/packages/core/migrations/8_deploy_store.js b/packages/core/migrations/8_deploy_store.js index 558639e6..06447268 100644 --- a/packages/core/migrations/8_deploy_store.js +++ b/packages/core/migrations/8_deploy_store.js @@ -2,7 +2,8 @@ const Finder = artifacts.require("Finder"); const Store = artifacts.require("Store"); const Timer = artifacts.require("Timer"); -const { getKeysForNetwork, deploy, enableControllableTiming, interfaceName } = require("@uma/common"); +const { interfaceName } = require("@uma/common"); +const { getKeysForNetwork, deploy, enableControllableTiming } = require("./MigrationUtils"); module.exports = async function (deployer, network, accounts) { const keys = getKeysForNetwork(network, accounts); diff --git a/packages/core/migrations/9_deploy_governor.js b/packages/core/migrations/9_deploy_governor.js index 3e1b71f5..02a0d3b3 100644 --- a/packages/core/migrations/9_deploy_governor.js +++ b/packages/core/migrations/9_deploy_governor.js @@ -2,7 +2,8 @@ const Governor = artifacts.require("Governor"); const Finder = artifacts.require("Finder"); const Registry = artifacts.require("Registry"); const Timer = artifacts.require("Timer"); -const { getKeysForNetwork, deploy, enableControllableTiming, RegistryRolesEnum } = require("@uma/common"); +const { RegistryRolesEnum } = require("@uma/common"); +const { getKeysForNetwork, deploy, enableControllableTiming } = require("./MigrationUtils"); module.exports = async function (deployer, network, accounts) { const keys = getKeysForNetwork(network, accounts); diff --git a/packages/common/src/MigrationUtils.js b/packages/core/migrations/MigrationUtils.js similarity index 98% rename from packages/common/src/MigrationUtils.js rename to packages/core/migrations/MigrationUtils.js index 517b3383..112f50b2 100644 --- a/packages/common/src/MigrationUtils.js +++ b/packages/core/migrations/MigrationUtils.js @@ -11,7 +11,7 @@ const tdr = require("truffle-deploy-registry"); const argv = require("minimist")(process.argv.slice(), { boolean: ["keep_finder", "keep_token", "keep_system"] }); // Grab the name property from each to get a list of the names of the public networks. -const publicNetworkNames = Object.values(require("./PublicNetworks.js").PublicNetworks).map((elt) => elt.name); +const publicNetworkNames = Object.values(require("@uma/common").PublicNetworks).map((elt) => elt.name); function isPublicNetwork(network) { return publicNetworkNames.some((name) => network.startsWith(name)); diff --git a/packages/merkle-distributor/scripts/0_MerklePayoutIngestor.ts b/packages/merkle-distributor/scripts/0_MerklePayoutIngestor.ts index b05b5a87..78bb7253 100644 --- a/packages/merkle-distributor/scripts/0_MerklePayoutIngestor.ts +++ b/packages/merkle-distributor/scripts/0_MerklePayoutIngestor.ts @@ -67,7 +67,7 @@ async function main() { const checkSumRecipientAddress = toChecksumAddress(recipientAddress); // Ensure consistent address case // Scale the amount by the number of decimals for that particular input. - const recipientAmountScaled = ConvertDecimals(0, o.decimals[i], Web3)(recipients[recipientAddress]); + const recipientAmountScaled = ConvertDecimals(0, o.decimals[i])(recipients[recipientAddress]); // If the output file already contains information for this particular recipient, then append and add their rewards. // Else, simply init the object with their values from the file. Note that accountIndex in both cases is set to -1. diff --git a/packages/trader/src/RangeTrader.ts b/packages/trader/src/RangeTrader.ts index 5849f57a..0fa1024d 100644 --- a/packages/trader/src/RangeTrader.ts +++ b/packages/trader/src/RangeTrader.ts @@ -41,7 +41,7 @@ export class RangeTrader { assert(tokenPriceFeed.getPriceFeedDecimals() === referencePriceFeed.getPriceFeedDecimals(), "decimals must match"); assert(exchangeAdapter, "Exchange adapter must be initialized"); - this.normalizePriceFeedDecimals = ConvertDecimals(tokenPriceFeed.getPriceFeedDecimals(), 18, this.web3); + this.normalizePriceFeedDecimals = ConvertDecimals(tokenPriceFeed.getPriceFeedDecimals(), 18); // Formats an 18 decimal point string with a define number of decimals and precision for use in message generation. this.formatDecimalString = createFormatFunction(this.web3, 2, 6, false); diff --git a/packages/trader/test/UniswapV3Trader.ts b/packages/trader/test/UniswapV3Trader.ts index c14ea91d..4e079089 100644 --- a/packages/trader/test/UniswapV3Trader.ts +++ b/packages/trader/test/UniswapV3Trader.ts @@ -321,7 +321,7 @@ describe("uniswapV3Trader.js", function () { mockTime = Number((await web3.eth.getBlock("latest")).timestamp) + 1; await tokenPriceFeed.update(); assert.equal( - Number(fromWei(tokenPriceFeed.getLastBlockPrice())).toFixed(4), + Number(Number(fromWei(tokenPriceFeed.getLastBlockPrice())).toFixed(4)), (await getCurrentPrice(poolAddress, web3)).toNumber() ); diff --git a/yarn.lock b/yarn.lock index c3b3f2ca..1eaed5a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2565,7 +2565,7 @@ retry-request "^4.0.0" teeny-request "^6.0.0" -"@google-cloud/common@^3.1.0", "@google-cloud/common@^3.4.1": +"@google-cloud/common@^3.1.0", "@google-cloud/common@^3.4.1", "@google-cloud/common@^3.7.0": version "3.7.0" resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-3.7.0.tgz#ee3fba75aeaa614978aebf8740380670026592aa" integrity sha512-oFgpKLjH9JTOAyQd3kB36iSuH8wNSpDKb1TywlB6zcsG0xmJFxLutmfPhz03KUxRMNQOZ1K1Gc9BYvJifVnGVA== @@ -2602,6 +2602,13 @@ google-gax "^0.23.0" lodash.merge "^4.6.0" +"@google-cloud/kms@^2.3.1": + version "2.4.4" + resolved "https://registry.yarnpkg.com/@google-cloud/kms/-/kms-2.4.4.tgz#e4fb95610668b1475f6623a0b69d32bbad2f73eb" + integrity sha512-YqWBZu4+57ac14p1PXvFBPU+tppwRBbI2e5vzg5cn/yi0GSIaU/nmkEYPPHSM7Fi7PS7W13AlcMT6RbVafRBoQ== + dependencies: + google-gax "^2.17.1" + "@google-cloud/logging-winston@^4.0.4": version "4.1.0" resolved "https://registry.yarnpkg.com/@google-cloud/logging-winston/-/logging-winston-4.1.0.tgz#659dd5a1024c0f42dcaa652478d206f82cdb6715" @@ -2708,6 +2715,32 @@ through2 "^3.0.0" xdg-basedir "^3.0.0" +"@google-cloud/storage@^5.8.5": + version "5.11.1" + resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-5.11.1.tgz#1b32833790e77fc447d5254910a190834a709c72" + integrity sha512-DGKHBMqE+OsLPei61yTYOOJKFMvkzVViIafJ+vtCgdXqlSfyEaDK3zXMzIqTb+D9DTJvkEFYi2Wc44eBVo1Gqw== + dependencies: + "@google-cloud/common" "^3.7.0" + "@google-cloud/paginator" "^3.0.0" + "@google-cloud/promisify" "^2.0.0" + arrify "^2.0.0" + async-retry "^1.3.1" + compressible "^2.0.12" + date-and-time "^1.0.0" + duplexify "^4.0.0" + extend "^3.0.2" + gcs-resumable-upload "^3.3.0" + get-stream "^6.0.0" + hash-stream-validation "^0.2.2" + mime "^2.2.0" + mime-types "^2.0.8" + onetime "^5.1.0" + p-limit "^3.0.1" + pumpify "^2.0.0" + snakeize "^0.1.0" + stream-events "^1.0.1" + xdg-basedir "^4.0.0" + "@google-cloud/trace-agent@^4.2.5": version "4.2.5" resolved "https://registry.yarnpkg.com/@google-cloud/trace-agent/-/trace-agent-4.2.5.tgz#443bf19f08aaceb8cf20bd7591f10efbdebf995a" @@ -6361,7 +6394,14 @@ "@types/level-errors" "*" "@types/node" "*" -"@types/lodash@^4.14.104": +"@types/lodash.uniqby@^4.7.6": + version "4.7.6" + resolved "https://registry.yarnpkg.com/@types/lodash.uniqby/-/lodash.uniqby-4.7.6.tgz#672827a701403f07904fe37f0721ae92abfa80e8" + integrity sha512-9wBhrm1y6asW50Joj6tsySCNUgzK2tCqL7vtKIej0E9RyeBFdcte7fxUosmFuMoOU0eHqOMK76kCCrK99jxHgg== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*", "@types/lodash@^4.14.104": version "4.14.171" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.171.tgz#f01b3a5fe3499e34b622c362a46a609fdb23573b" integrity sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg== @@ -11098,6 +11138,18 @@ configstore@^4.0.0: write-file-atomic "^2.0.0" xdg-basedir "^3.0.0" +configstore@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" + integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== + dependencies: + dot-prop "^5.2.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + unique-string "^2.0.0" + write-file-atomic "^3.0.0" + xdg-basedir "^4.0.0" + confusing-browser-globals@^1.0.7, confusing-browser-globals@^1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.10.tgz#30d1e7f3d1b882b25ec4933d1d1adac353d20a59" @@ -11524,6 +11576,11 @@ crypto-random-string@^1.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + css-blank-pseudo@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5" @@ -11943,6 +12000,11 @@ date-and-time@^0.6.3: resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-0.6.3.tgz#2daee52df67c28bd93bce862756ac86b68cf4237" integrity sha512-lcWy3AXDRJOD7MplwZMmNSRM//kZtJaLz4n6D1P5z9wEmZGBKhJRBIr1Xs9KNQJmdXPblvgffynYji4iylUTcA== +date-and-time@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-1.0.1.tgz#4959b7faf1ec5873e59d926d4528b9223a808a57" + integrity sha512-7u+uNfnjWkX+YFQfivvW24TjaJG6ahvTrfw1auq7KlC7osuGcZBIWGBvB9UcENjH6JnLVhMqlRripk1dSHjAUA== + dateformat@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" @@ -12638,6 +12700,11 @@ dotenv@^8.2.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== +dotenv@^9.0.0: + version "9.0.2" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-9.0.2.tgz#dacc20160935a37dea6364aa1bef819fb9b6ab05" + integrity sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg== + dotignore@^0.1.2, dotignore@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/dotignore/-/dotignore-0.1.2.tgz#f942f2200d28c3a76fbdd6f0ee9f3257c8a2e905" @@ -15609,6 +15676,19 @@ gcs-resumable-upload@^1.0.0: pumpify "^1.5.1" stream-events "^1.0.4" +gcs-resumable-upload@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/gcs-resumable-upload/-/gcs-resumable-upload-3.3.0.tgz#d1a866173f9b47e045d4406cafaa658dbb01e624" + integrity sha512-MQKWi+9hOSTyg5/SI1NBW4gAjL1wlkoevHefvr1PCBBXH4uKYLsug5qRrcotWKolDPLfWS51cWaHRN0CTtQNZw== + dependencies: + abort-controller "^3.0.0" + configstore "^5.0.0" + extend "^3.0.2" + gaxios "^4.0.0" + google-auth-library "^7.0.0" + pumpify "^2.0.0" + stream-events "^1.0.4" + genfun@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" @@ -15718,6 +15798,11 @@ get-stream@^5.0.0, get-stream@^5.1.0: dependencies: pump "^3.0.0" +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -16694,7 +16779,7 @@ hash-base@^3.0.0: readable-stream "^3.6.0" safe-buffer "^5.2.0" -hash-stream-validation@^0.2.1: +hash-stream-validation@^0.2.1, hash-stream-validation@^0.2.2: version "0.2.4" resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512" integrity sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ== @@ -23579,7 +23664,7 @@ p-is-promise@^2.0.0: resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== -p-limit@3.1.0, p-limit@^3.0.2: +p-limit@3.1.0, p-limit@^3.0.1, p-limit@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== @@ -25857,7 +25942,7 @@ pumpify@^1.3.3, pumpify@^1.3.5, pumpify@^1.5.1: inherits "^2.0.3" pump "^2.0.0" -pumpify@^2.0.1: +pumpify@^2.0.0, pumpify@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-2.0.1.tgz#abfc7b5a621307c728b551decbbefb51f0e4aa1e" integrity sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw== @@ -30349,6 +30434,13 @@ unique-string@^1.0.0: dependencies: crypto-random-string "^1.0.0" +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + universal-cookie@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-4.0.4.tgz#06e8b3625bf9af049569ef97109b4bb226ad798d" @@ -31251,7 +31343,7 @@ web3-core@1.4.0: web3-core-requestmanager "1.4.0" web3-utils "1.4.0" -web3-core@1.5.0: +web3-core@1.5.0, web3-core@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.5.0.tgz#46c09283bcfe197df0c543dbe751650cea157a7f" integrity sha512-1o/etaPSK8tFOWTA6df3t9J6ez4epeyzlNmyh/gx8uHasfa16XLKD8//A9T+O/TmvyQAaA4hWAsQcvlRcuaZ8Q== @@ -31454,7 +31546,7 @@ web3-eth-contract@1.4.0: web3-eth-abi "1.4.0" web3-utils "1.4.0" -web3-eth-contract@1.5.0: +web3-eth-contract@1.5.0, web3-eth-contract@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-1.5.0.tgz#f584a083316424110af95c3ad00c1c3a8a1796d2" integrity sha512-v4laiJRzdcoDwvqaMCzJH1BUosbTVsd01Qp+9v05Q94KycjkdeahPRXX6PEcUNW/ZF8N006iExUweGjajTZnTA== @@ -32895,6 +32987,11 @@ xdg-basedir@^3.0.0: resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + xhr-request-promise@^0.1.2: version "0.1.3" resolved "https://registry.yarnpkg.com/xhr-request-promise/-/xhr-request-promise-0.1.3.tgz#2d5f4b16d8c6c893be97f1a62b0ed4cf3ca5f96c"