From 0fa49de40764aae13864fb45b352ba31d9e7e958 Mon Sep 17 00:00:00 2001 From: Lawrence Wang Date: Fri, 10 Jan 2020 15:03:44 -0800 Subject: [PATCH] initial copy of all files in plugin --- packages/dai-plugin-governance/LICENSE | 21 ++ packages/dai-plugin-governance/README.md | 89 +++++ .../contracts/abis/DSChief.json | 327 +++++++++++++++++ .../contracts/abis/ESM.json | 135 +++++++ .../contracts/abis/End.json | 334 ++++++++++++++++++ .../contracts/abis/Polling.json | 80 +++++ .../contracts/abis/VoteProxy.json | 102 ++++++ .../contracts/abis/VoteProxyFactory.json | 159 +++++++++ .../contracts/addresses/kovan-mcd.json | 9 + .../contracts/addresses/kovan.json | 7 + .../contracts/addresses/mainnet.json | 7 + .../contracts/addresses/testnet.json | 9 + .../contracts/contract-info.json | 20 ++ packages/dai-plugin-governance/package.json | 61 ++++ .../dai-plugin-governance/scripts/build.sh | 21 ++ .../scripts/install-testchain-outputs.sh | 28 ++ .../src/ApproveLinkTransaction.js | 46 +++ .../dai-plugin-governance/src/ChiefService.js | 191 ++++++++++ .../dai-plugin-governance/src/EsmService.js | 91 +++++ .../src/GovPollingService.js | 163 +++++++++ .../src/GovQueryApiService.js | 144 ++++++++ .../dai-plugin-governance/src/VoteProxy.js | 38 ++ .../src/VoteProxyFactoryService.js | 33 ++ .../src/VoteProxyService.js | 103 ++++++ packages/dai-plugin-governance/src/index.js | 102 ++++++ .../src/utils/constants.js | 21 ++ .../src/utils/helpers.js | 50 +++ .../test/ChiefService.test.js | 115 ++++++ .../test/EsmService.test.js | 106 ++++++ .../test/GovPollingService.test.js | 159 +++++++++ .../test/VoteProxy.test.js | 54 +++ .../test/VoteProxyFactoryService.test.js | 81 +++++ .../test/VoteProxyService.test.js | 130 +++++++ .../test/config/jestIntegrationConfig.json | 6 + .../test/config/jestTestchainConfig.json | 9 + .../config/jestTestchainConfigOriginal.json | 9 + .../test/config/original-setup.js | 7 + .../test/config/setup-test.js | 59 ++++ .../dai-plugin-governance/test/fixtures.js | 46 +++ .../test/helpers/index.js | 203 +++++++++++ .../integration/govQueryApiService.test.js | 38 ++ .../test/migrations/SingleToMultiCdp.spec.js | 2 +- .../dai/test/eth/tokens/Erc20Token.spec.js | 4 + 43 files changed, 3418 insertions(+), 1 deletion(-) create mode 100644 packages/dai-plugin-governance/LICENSE create mode 100644 packages/dai-plugin-governance/README.md create mode 100644 packages/dai-plugin-governance/contracts/abis/DSChief.json create mode 100644 packages/dai-plugin-governance/contracts/abis/ESM.json create mode 100644 packages/dai-plugin-governance/contracts/abis/End.json create mode 100644 packages/dai-plugin-governance/contracts/abis/Polling.json create mode 100644 packages/dai-plugin-governance/contracts/abis/VoteProxy.json create mode 100644 packages/dai-plugin-governance/contracts/abis/VoteProxyFactory.json create mode 100644 packages/dai-plugin-governance/contracts/addresses/kovan-mcd.json create mode 100644 packages/dai-plugin-governance/contracts/addresses/kovan.json create mode 100644 packages/dai-plugin-governance/contracts/addresses/mainnet.json create mode 100644 packages/dai-plugin-governance/contracts/addresses/testnet.json create mode 100644 packages/dai-plugin-governance/contracts/contract-info.json create mode 100644 packages/dai-plugin-governance/package.json create mode 100755 packages/dai-plugin-governance/scripts/build.sh create mode 100755 packages/dai-plugin-governance/scripts/install-testchain-outputs.sh create mode 100644 packages/dai-plugin-governance/src/ApproveLinkTransaction.js create mode 100644 packages/dai-plugin-governance/src/ChiefService.js create mode 100644 packages/dai-plugin-governance/src/EsmService.js create mode 100644 packages/dai-plugin-governance/src/GovPollingService.js create mode 100644 packages/dai-plugin-governance/src/GovQueryApiService.js create mode 100644 packages/dai-plugin-governance/src/VoteProxy.js create mode 100644 packages/dai-plugin-governance/src/VoteProxyFactoryService.js create mode 100644 packages/dai-plugin-governance/src/VoteProxyService.js create mode 100644 packages/dai-plugin-governance/src/index.js create mode 100644 packages/dai-plugin-governance/src/utils/constants.js create mode 100644 packages/dai-plugin-governance/src/utils/helpers.js create mode 100644 packages/dai-plugin-governance/test/ChiefService.test.js create mode 100644 packages/dai-plugin-governance/test/EsmService.test.js create mode 100644 packages/dai-plugin-governance/test/GovPollingService.test.js create mode 100644 packages/dai-plugin-governance/test/VoteProxy.test.js create mode 100644 packages/dai-plugin-governance/test/VoteProxyFactoryService.test.js create mode 100644 packages/dai-plugin-governance/test/VoteProxyService.test.js create mode 100644 packages/dai-plugin-governance/test/config/jestIntegrationConfig.json create mode 100644 packages/dai-plugin-governance/test/config/jestTestchainConfig.json create mode 100644 packages/dai-plugin-governance/test/config/jestTestchainConfigOriginal.json create mode 100644 packages/dai-plugin-governance/test/config/original-setup.js create mode 100644 packages/dai-plugin-governance/test/config/setup-test.js create mode 100644 packages/dai-plugin-governance/test/fixtures.js create mode 100644 packages/dai-plugin-governance/test/helpers/index.js create mode 100644 packages/dai-plugin-governance/test/integration/govQueryApiService.test.js diff --git a/packages/dai-plugin-governance/LICENSE b/packages/dai-plugin-governance/LICENSE new file mode 100644 index 000000000..6b44a5444 --- /dev/null +++ b/packages/dai-plugin-governance/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Maker Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/dai-plugin-governance/README.md b/packages/dai-plugin-governance/README.md new file mode 100644 index 000000000..5ccde2e20 --- /dev/null +++ b/packages/dai-plugin-governance/README.md @@ -0,0 +1,89 @@ +

+Dai Governance Plugin +

+ +[![GitHub License][license]][license-url] +[![NPM][npm]][npm-url] +[![Build Status][build]][build-url] + +A [dai.js](https://github.com/makerdao/dai.js) plugin for interacting with the MKR governance system. This plugin makes it easy to integrate dai governance into frontend applications such as the [maker governace dashboard](https://vote.makerdao.com/). You can use it to vote, cast proposals, query the voting contract, create a vote proxy, and much more. + +## Installation + +The Dai Governance Plugin requires **dai.js 0.9.2 or later.** + +``` +$ npm install --save @makerdao/dai-plugin-governance +``` + +or + +``` +$ yarn add @makerdao/dai-plugin-governance +``` + +## Examples + +We will have several examples once the api is more stable. Here is one to give you some sense of how this plugin can be used: + +```js +import governancePlugin from '@makerdao/dai-plugin-governance'; +import Maker from '@makerdao/dai'; + +(async () => { + const maker = Maker.create('browser', { + plugins: [governancePlugin] + }); + await maker.authenticate(); + await maker.service('chief').lock(10); +})(); +``` + +This example will initiate a MetaMask transaction to lock 10 MKR into the maker voting system. + +### Development + +## Getting started + +_Note: this project utilizes the [yarn](https://yarnpkg.com/en/) package manager_ + +Clone this repo & fetch submodules + +``` +$ git clone --recurse-submodules -j8 https://github.com/makerdao/dai-plugin-governance.git +``` + +Install project dependencies + +``` +$ yarn +$ yarn install --cwd "gov-testchain" +``` + +## Running Tests + +1. Install [dapptools](https://dapp.tools/) +1. `yarn testnet --ci yarn test` + +## Publishing + +Publish to NPM + +``` +$ yarn deploy +``` + +## Code Style + +We run Prettier on-commit, which means you can write code in whatever style you want and it will be automatically formatted according to the common style when you run `git commit`. + +### License + +The dai governance plugin is [MIT licensed](./LICENSE). + +[npm]: https://img.shields.io/npm/v/@makerdao/dai-plugin-governance.svg?style=flat +[npm-url]: https://www.npmjs.com/package/@makerdao/dai-plugin-governance +[license]: https://img.shields.io/badge/license-MIT-blue.svg +[license-url]: https://github.com/makerdao/dai-plugin-governance/blob/master/LICENSE +[build]: https://travis-ci.com/makerdao/dai-plugin-governance.svg?token=7qKLu97qQDDMKfaxt318&branch=master +[build-url]: https://travis-ci.com/makerdao/dai-plugin-governance diff --git a/packages/dai-plugin-governance/contracts/abis/DSChief.json b/packages/dai-plugin-governance/contracts/abis/DSChief.json new file mode 100644 index 000000000..2fff4bcbd --- /dev/null +++ b/packages/dai-plugin-governance/contracts/abis/DSChief.json @@ -0,0 +1,327 @@ +[ + { + "constant": true, + "inputs": [], + "name": "IOU", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "who", "type": "address" }], + "name": "getUserRoles", + "outputs": [{ "name": "", "type": "bytes32" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "owner_", "type": "address" }], + "name": "setOwner", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "GOV", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "code", "type": "address" }, + { "name": "sig", "type": "bytes4" } + ], + "name": "getCapabilityRoles", + "outputs": [{ "name": "", "type": "bytes32" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "code", "type": "address" }, + { "name": "sig", "type": "bytes4" } + ], + "name": "isCapabilityPublic", + "outputs": [{ "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "MAX_YAYS", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "whom", "type": "address" }], + "name": "lift", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "yays", "type": "address[]" }], + "name": "etch", + "outputs": [{ "name": "slate", "type": "bytes32" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "", "type": "address" }], + "name": "approvals", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "who", "type": "address" }, + { "name": "role", "type": "uint8" }, + { "name": "enabled", "type": "bool" } + ], + "name": "setUserRole", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "authority_", "type": "address" }], + "name": "setAuthority", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "role", "type": "uint8" }, + { "name": "code", "type": "address" }, + { "name": "sig", "type": "bytes4" }, + { "name": "enabled", "type": "bool" } + ], + "name": "setRoleCapability", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "owner", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "who", "type": "address" }, + { "name": "role", "type": "uint8" } + ], + "name": "hasUserRole", + "outputs": [{ "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "slate", "type": "bytes32" }], + "name": "vote", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "caller", "type": "address" }, + { "name": "code", "type": "address" }, + { "name": "sig", "type": "bytes4" } + ], + "name": "canCall", + "outputs": [{ "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "authority", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "", "type": "bytes32" }, + { "name": "", "type": "uint256" } + ], + "name": "slates", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "code", "type": "address" }, + { "name": "sig", "type": "bytes4" }, + { "name": "enabled", "type": "bool" } + ], + "name": "setPublicCapability", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "who", "type": "address" }, + { "name": "enabled", "type": "bool" } + ], + "name": "setRootUser", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "", "type": "address" }], + "name": "votes", + "outputs": [{ "name": "", "type": "bytes32" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "wad", "type": "uint256" }], + "name": "free", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "wad", "type": "uint256" }], + "name": "lock", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "yays", "type": "address[]" }], + "name": "vote", + "outputs": [{ "name": "", "type": "bytes32" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "who", "type": "address" }], + "name": "isUserRoot", + "outputs": [{ "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "", "type": "address" }], + "name": "deposits", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "hat", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "name": "GOV", "type": "address" }, + { "name": "IOU", "type": "address" }, + { "name": "MAX_YAYS", "type": "uint256" } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [{ "indexed": true, "name": "slate", "type": "bytes32" }], + "name": "Etch", + "type": "event" + }, + { + "anonymous": true, + "inputs": [ + { "indexed": true, "name": "sig", "type": "bytes4" }, + { "indexed": true, "name": "guy", "type": "address" }, + { "indexed": true, "name": "foo", "type": "bytes32" }, + { "indexed": true, "name": "bar", "type": "bytes32" }, + { "indexed": false, "name": "wad", "type": "uint256" }, + { "indexed": false, "name": "fax", "type": "bytes" } + ], + "name": "LogNote", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": true, "name": "authority", "type": "address" }], + "name": "LogSetAuthority", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": true, "name": "owner", "type": "address" }], + "name": "LogSetOwner", + "type": "event" + } +] diff --git a/packages/dai-plugin-governance/contracts/abis/ESM.json b/packages/dai-plugin-governance/contracts/abis/ESM.json new file mode 100644 index 000000000..72c4681ff --- /dev/null +++ b/packages/dai-plugin-governance/contracts/abis/ESM.json @@ -0,0 +1,135 @@ +[ + { + "inputs": [ + { "internalType": "address", "name": "gem_", "type": "address" }, + { "internalType": "address", "name": "end_", "type": "address" }, + { "internalType": "address", "name": "pit_", "type": "address" }, + { "internalType": "uint256", "name": "min_", "type": "uint256" } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": true, + "inputs": [ + { + "indexed": true, + "internalType": "bytes4", + "name": "sig", + "type": "bytes4" + }, + { + "indexed": true, + "internalType": "address", + "name": "usr", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "arg1", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "arg2", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "LogNote", + "type": "event" + }, + { + "constant": true, + "inputs": [], + "name": "Sum", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "end", + "outputs": [ + { "internalType": "contract EndLike", "name": "", "type": "address" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "fire", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "fired", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "gem", + "outputs": [ + { "internalType": "contract GemLike", "name": "", "type": "address" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "uint256", "name": "wad", "type": "uint256" }], + "name": "join", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "min", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "pit", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "sum", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + } +] diff --git a/packages/dai-plugin-governance/contracts/abis/End.json b/packages/dai-plugin-governance/contracts/abis/End.json new file mode 100644 index 000000000..27d4e0835 --- /dev/null +++ b/packages/dai-plugin-governance/contracts/abis/End.json @@ -0,0 +1,334 @@ +[ + { + "inputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": true, + "inputs": [ + { + "indexed": true, + "internalType": "bytes4", + "name": "sig", + "type": "bytes4" + }, + { + "indexed": true, + "internalType": "address", + "name": "usr", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "arg1", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "arg2", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "LogNote", + "type": "event" + }, + { + "constant": true, + "inputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "name": "Art", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "bag", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "cage", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "bytes32", "name": "ilk", "type": "bytes32" }], + "name": "cage", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "bytes32", "name": "ilk", "type": "bytes32" }, + { "internalType": "uint256", "name": "wad", "type": "uint256" } + ], + "name": "cash", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "cat", + "outputs": [ + { "internalType": "contract CatLike", "name": "", "type": "address" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "debt", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "address", "name": "guy", "type": "address" }], + "name": "deny", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "bytes32", "name": "what", "type": "bytes32" }, + { "internalType": "uint256", "name": "data", "type": "uint256" } + ], + "name": "file", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "bytes32", "name": "what", "type": "bytes32" }, + { "internalType": "address", "name": "data", "type": "address" } + ], + "name": "file", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "name": "fix", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "bytes32", "name": "ilk", "type": "bytes32" }], + "name": "flow", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "bytes32", "name": "ilk", "type": "bytes32" }], + "name": "free", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "name": "gap", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "live", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "internalType": "bytes32", "name": "", "type": "bytes32" }, + { "internalType": "address", "name": "", "type": "address" } + ], + "name": "out", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "uint256", "name": "wad", "type": "uint256" }], + "name": "pack", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "pot", + "outputs": [ + { "internalType": "contract PotLike", "name": "", "type": "address" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "address", "name": "guy", "type": "address" }], + "name": "rely", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "bytes32", "name": "ilk", "type": "bytes32" }, + { "internalType": "address", "name": "urn", "type": "address" } + ], + "name": "skim", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "bytes32", "name": "ilk", "type": "bytes32" }, + { "internalType": "uint256", "name": "id", "type": "uint256" } + ], + "name": "skip", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "spot", + "outputs": [ + { "internalType": "contract Spotty", "name": "", "type": "address" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "name": "tag", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "thaw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "vat", + "outputs": [ + { "internalType": "contract VatLike", "name": "", "type": "address" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "vow", + "outputs": [ + { "internalType": "contract VowLike", "name": "", "type": "address" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "wait", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "wards", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "when", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + } +] diff --git a/packages/dai-plugin-governance/contracts/abis/Polling.json b/packages/dai-plugin-governance/contracts/abis/Polling.json new file mode 100644 index 000000000..0b6e0c7f9 --- /dev/null +++ b/packages/dai-plugin-governance/contracts/abis/Polling.json @@ -0,0 +1,80 @@ +[ + { + "constant": false, + "inputs": [{ "name": "pollId", "type": "uint256" }], + "name": "withdrawPoll", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "pollId", "type": "uint256" }, + { "name": "optionId", "type": "uint256" } + ], + "name": "vote", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "npoll", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "name": "startDate", "type": "uint256" }, + { "name": "endDate", "type": "uint256" }, + { "name": "multiHash", "type": "string" }, + { "name": "url", "type": "string" } + ], + "name": "createPoll", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "name": "creator", "type": "address" }, + { "indexed": false, "name": "blockCreated", "type": "uint256" }, + { "indexed": true, "name": "pollId", "type": "uint256" }, + { "indexed": false, "name": "startDate", "type": "uint256" }, + { "indexed": false, "name": "endDate", "type": "uint256" }, + { "indexed": false, "name": "multiHash", "type": "string" }, + { "indexed": false, "name": "url", "type": "string" } + ], + "name": "PollCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "name": "creator", "type": "address" }, + { "indexed": false, "name": "blockWithdrawn", "type": "uint256" }, + { "indexed": false, "name": "pollId", "type": "uint256" } + ], + "name": "PollWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "name": "voter", "type": "address" }, + { "indexed": true, "name": "pollId", "type": "uint256" }, + { "indexed": true, "name": "optionId", "type": "uint256" } + ], + "name": "Voted", + "type": "event" + } +] diff --git a/packages/dai-plugin-governance/contracts/abis/VoteProxy.json b/packages/dai-plugin-governance/contracts/abis/VoteProxy.json new file mode 100644 index 000000000..d4038d4be --- /dev/null +++ b/packages/dai-plugin-governance/contracts/abis/VoteProxy.json @@ -0,0 +1,102 @@ +[ + { + "constant": true, + "inputs": [], + "name": "gov", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "cold", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "freeAll", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "iou", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "slate", "type": "bytes32" }], + "name": "vote", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "wad", "type": "uint256" }], + "name": "free", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "wad", "type": "uint256" }], + "name": "lock", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "hot", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "name": "yays", "type": "address[]" }], + "name": "vote", + "outputs": [{ "name": "", "type": "bytes32" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "chief", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "name": "_chief", "type": "address" }, + { "name": "_cold", "type": "address" }, + { "name": "_hot", "type": "address" } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + } +] diff --git a/packages/dai-plugin-governance/contracts/abis/VoteProxyFactory.json b/packages/dai-plugin-governance/contracts/abis/VoteProxyFactory.json new file mode 100644 index 000000000..7e825acfe --- /dev/null +++ b/packages/dai-plugin-governance/contracts/abis/VoteProxyFactory.json @@ -0,0 +1,159 @@ +[ + { + "inputs": [ + { + "internalType": "contract DSChief", + "name": "chief_", + "type": "address" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "cold", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "hot", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "voteProxy", + "type": "address" + } + ], + "name": "LinkConfirmed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "cold", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "hot", + "type": "address" + } + ], + "name": "LinkRequested", + "type": "event" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "cold", "type": "address" } + ], + "name": "approveLink", + "outputs": [ + { + "internalType": "contract VoteProxy", + "name": "voteProxy", + "type": "address" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "breakLink", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "chief", + "outputs": [ + { "internalType": "contract DSChief", "name": "", "type": "address" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "coldMap", + "outputs": [ + { "internalType": "contract VoteProxy", "name": "", "type": "address" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "guy", "type": "address" }], + "name": "hasProxy", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "hotMap", + "outputs": [ + { "internalType": "contract VoteProxy", "name": "", "type": "address" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ "internalType": "address", "name": "hot", "type": "address" }], + "name": "initiateLink", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "linkRequests", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "linkSelf", + "outputs": [ + { + "internalType": "contract VoteProxy", + "name": "voteProxy", + "type": "address" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/packages/dai-plugin-governance/contracts/addresses/kovan-mcd.json b/packages/dai-plugin-governance/contracts/addresses/kovan-mcd.json new file mode 100644 index 000000000..c03a3a8f7 --- /dev/null +++ b/packages/dai-plugin-governance/contracts/addresses/kovan-mcd.json @@ -0,0 +1,9 @@ +{ + "CHIEF": "0x88a23a779cb0550bb6347560ac3daedf0389b098", + "VOTE_PROXY_FACTORY": "0xeac98db8e1177d3020f3b3d0e9409a4a396c3981", + "POLLING": "0x518a0702701BF98b5242E73b2368ae07562BEEA3", + "MCD_ESM": "0xd757d65441205335621554a3c32a3d3c1fe77aad", + "MCD_END": "0x88015f50fb5b2bb1abf8a9c3e1db231e267b5c4f", + "GOV": "0x1cc05530723a4fd398b4354ea511a1b0543ba714", + "IOU": "0x99eff48e1df706d8e5ae88bec40bb48aea22f3f9" +} diff --git a/packages/dai-plugin-governance/contracts/addresses/kovan.json b/packages/dai-plugin-governance/contracts/addresses/kovan.json new file mode 100644 index 000000000..f763a68f1 --- /dev/null +++ b/packages/dai-plugin-governance/contracts/addresses/kovan.json @@ -0,0 +1,7 @@ +{ + "CHIEF": "0xbbffc76e94b34f72d96d054b31f6424249c1337d", + "VOTE_PROXY_FACTORY": "0x3E08741A68c2d964d172793cD0Ad14292F658cd8", + "GOV": "0xAaF64BFCC32d0F15873a02163e7E500671a4ffcD", + "IOU": "0x4D5d2F7E1284bc5c871ce3e1A997Bd8646c75ba5", + "POLLING": "0x518a0702701BF98b5242E73b2368ae07562BEEA3" +} diff --git a/packages/dai-plugin-governance/contracts/addresses/mainnet.json b/packages/dai-plugin-governance/contracts/addresses/mainnet.json new file mode 100644 index 000000000..7bafb6313 --- /dev/null +++ b/packages/dai-plugin-governance/contracts/addresses/mainnet.json @@ -0,0 +1,7 @@ +{ + "CHIEF": "0x9eF05f7F6deB616fd37aC3c959a2dDD25A54E4F5", + "VOTE_PROXY_FACTORY": "0x868ba9aeacA5B73c7C27F3B01588bf4F1339F2bC", + "GOV": "0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2", + "IOU": "0x496C67A4CEd9C453A60F3166AB4B329870c8E355", + "POLLING": "0xF9be8F0945acDdeeDaA64DFCA5Fe9629D0CF8E5D" +} diff --git a/packages/dai-plugin-governance/contracts/addresses/testnet.json b/packages/dai-plugin-governance/contracts/addresses/testnet.json new file mode 100644 index 000000000..5a3dcb481 --- /dev/null +++ b/packages/dai-plugin-governance/contracts/addresses/testnet.json @@ -0,0 +1,9 @@ +{ + "GOV": "0xe8608ec3d229f578bed5b405d1e04bc2a0bb65e8", + "IOU": "0xd2b90e772eea9725e19873c0cc855dd1be00cc1c", + "CHIEF": "0x538f5ebf3d7352e250c021caf2f9b7752443329e", + "POLLING": "0x33bbcca25638d046c754987a45f7cba6a73b1fd4", + "VOTE_PROXY_FACTORY": "0x454b73d687a4f6d8ecca4420f6094be93b4cbedb", + "MCD_ESM": "0x64dd2ad30b740df0e30283ae8229060f0947e7af", + "MCD_END": "0x2b0008ef7f8bb4edf6829d2369f0a7f67d7a1c58" +} diff --git a/packages/dai-plugin-governance/contracts/contract-info.json b/packages/dai-plugin-governance/contracts/contract-info.json new file mode 100644 index 000000000..a53bf3e78 --- /dev/null +++ b/packages/dai-plugin-governance/contracts/contract-info.json @@ -0,0 +1,20 @@ +{ + "chief": { + "inception_block": { + "mainnet": "0x487813", + "kovan": "0x649575" + }, + "events": { + "etch": "0x4f0892983790f53eea39a7a496f6cb40e8811b313871337b6a761efc6c67bb1f", + "vote_slate": "0xa69beaba00000000000000000000000000000000000000000000000000000000", + "vote_addresses": "0xed08132900000000000000000000000000000000000000000000000000000000", + "lock": "0xdd46706400000000000000000000000000000000000000000000000000000000" + } + }, + "proxy_factory": { + "initiate_link_gas": 45840, + "approve_link_gas": 886503, + "send_mkr_gas": 46471, + "total_link_gas": 1123568 + } +} diff --git a/packages/dai-plugin-governance/package.json b/packages/dai-plugin-governance/package.json new file mode 100644 index 000000000..a299bf88e --- /dev/null +++ b/packages/dai-plugin-governance/package.json @@ -0,0 +1,61 @@ +{ + "name": "@makerdao/dai-plugin-governance", + "description": "A dai.js plugin for adding MKR governance support to dapps.", + "version": "0.7.4-rc.2", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/makerdao/dai-plugin-governance.git" + }, + "dependencies": { + "@babel/runtime": "^7.4.3", + "@makerdao/currency": "^0.9.5", + "@makerdao/services-core": "^0.9.5", + "assert": "^2.0.0", + "ramda": "^0.25.0", + "web3-utils": "^1.0.0-beta.36" + }, + "devDependencies": { + "@babel/cli": "^7.4.3", + "@babel/core": "^7.4.3", + "@babel/node": "^7.2.2", + "@babel/plugin-proposal-class-properties": "^7.4.0", + "@babel/plugin-transform-runtime": "^7.3.4", + "@babel/preset-env": "^7.4.3", + "@makerdao/dai": "^0.16.1", + "@makerdao/dai-plugin-config": "^0.2.7-rc.3", + "@makerdao/dai-plugin-governance": "^0.5.4", + "@makerdao/testchain": "^1.0.3", + "@makerdao/testchain-client": "^0.2.6-rc.1", + "babel-eslint": "^10.0.1", + "babel-preset-env": "^1.7.0", + "babel-preset-stage-2": "^6.24.1", + "copyfiles": "^2.1.0", + "eslint": "^5.9.0", + "ganache-cli": "^6.7.0", + "husky": "^1.1.4", + "jest": "^24.5.0", + "lint-staged": "^8.0.4", + "prettier": "^1.14.2", + "sane": "^4.0.1" + }, + "scripts": { + "deploy": "yarn build && yarn publish dist", + "test": "yarn testchain --ci jest --runInBand --config ./test/config/jestTestchainConfigOriginal.json", + "test:integration": "jest --runInBand --config ./test/config/jestIntegrationConfig.json", + "testchain": "./scripts/run-testchain.sh -s default -u", + "ci": "./gov-testchain/deploy-gov --ci jest --coverage", + "build": "./scripts/build.sh", + "precommit": "lint-staged", + "build:watch": "sane ./scripts/build.sh src --wait=10", + "prepublishOnly": "if [ \"`basename $(pwd)`\" != 'dist' ]; then echo You must be in the dist folder to publish. && exit 1; fi" + }, + "lint-staged": { + "*.{js,json}": [ + "prettier --write --single-quote", + "git add" + ] + }, + "module": "src/index.js", + "main": "src/index.js" +} diff --git a/packages/dai-plugin-governance/scripts/build.sh b/packages/dai-plugin-governance/scripts/build.sh new file mode 100755 index 000000000..4a18428aa --- /dev/null +++ b/packages/dai-plugin-governance/scripts/build.sh @@ -0,0 +1,21 @@ +# #!/usr/bin/env bash + +set -e + +if [ "$1" = "dirty" ]; then + echo "Dirty mode: not removing previous build files." +else + rm -rf dist +fi + +babel contracts --out-dir ./dist/contracts +babel src --out-dir ./dist/src + +copyfiles \ + README.md \ + LICENSE \ + package.json \ + contracts/* \ + contracts/abis/* \ + contracts/addresses/* \ + dist diff --git a/packages/dai-plugin-governance/scripts/install-testchain-outputs.sh b/packages/dai-plugin-governance/scripts/install-testchain-outputs.sh new file mode 100755 index 000000000..0370def39 --- /dev/null +++ b/packages/dai-plugin-governance/scripts/install-testchain-outputs.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -e + +CWD=`dirname $0` +CONTRACTS=$CWD/../contracts +SOURCE=${1:-$CWD/../node_modules/@makerdao/testchain} + +CHIEF=`jq ".MCD_ADM" "$SOURCE/out/addresses-mcd.json"` +jq ".CHIEF=$CHIEF" $CONTRACTS/addresses/testnet.json > testnet.tmp && mv testnet.tmp $CONTRACTS/addresses/testnet.json +cp $SOURCE/out/DSChief.abi $CONTRACTS/abis/DSChief.json + +IOU=`jq ".MCD_IOU" "$SOURCE/out/addresses-mcd.json"` +jq ".IOU=$IOU" $CONTRACTS/addresses/testnet.json > testnet.tmp && mv testnet.tmp $CONTRACTS/addresses/testnet.json + +POLLING=`jq ".POLLING" "$SOURCE/out/addresses.json"` +jq ".POLLING=$POLLING" $CONTRACTS/addresses/testnet.json > testnet.tmp && mv testnet.tmp $CONTRACTS/addresses/testnet.json +cp $SOURCE/out/PollingEmitter.abi $CONTRACTS/abis/Polling.json + +GOV=`jq ".GOV" "$SOURCE/out/addresses.json"` +jq ".GOV=$GOV" $CONTRACTS/addresses/testnet.json > testnet.tmp && mv testnet.tmp $CONTRACTS/addresses/testnet.json + +for CONTRACT in "VOTE_PROXY_FACTORY","VoteProxyFactory" "MCD_ESM","ESM" "MCD_END","End" +do + IFS=',' read NAME ABI <<< "${CONTRACT}" + ADDRESS=`jq ".$NAME" "$SOURCE/out/addresses-mcd.json"` + jq ".$NAME=$ADDRESS" $CONTRACTS/addresses/testnet.json > testnet.tmp && mv testnet.tmp $CONTRACTS/addresses/testnet.json + cp $SOURCE/out/mcd/$ABI.abi $CONTRACTS/abis/$ABI.json +done \ No newline at end of file diff --git a/packages/dai-plugin-governance/src/ApproveLinkTransaction.js b/packages/dai-plugin-governance/src/ApproveLinkTransaction.js new file mode 100644 index 000000000..cf93a72e7 --- /dev/null +++ b/packages/dai-plugin-governance/src/ApproveLinkTransaction.js @@ -0,0 +1,46 @@ +import { utils } from 'ethers'; + +export default class ApproveLinkTransaction { + constructor(contract, transactionManager) { + this._contract = contract; + this._txMgr = transactionManager; + } + + get fees() { + return this._txMgr.getTransaction(this.promise).fees(); + } + + get timeStamp() { + return this._txMgr.getTransaction(this.promise).timeStamp(); + } + + get timeStampSubmitted() { + return this._txMgr.getTransaction(this.promise).timeStampSubmitted(); + } + + build(method, args) { + const promise = (async () => { + await 0; + const txo = await this._contract[method](...[...args, { promise }]); + this._parseLogs(txo.receipt.logs); + return this; + })(); + this.promise = promise; + return promise; + } + + _parseLogs(logs) { + //use lower level ethersJS functions to parse logs + const { LinkConfirmed } = this._contract.interface.events; + const web3 = this._txMgr.get('web3')._web3; + const topic = utils.keccak256(web3.utils.toHex(LinkConfirmed.signature)); + const receiptEvent = logs.filter( + e => e.topics[0].toLowerCase() === topic.toLowerCase() //filter for LinkConfirmed events + ); + const parsedLog = LinkConfirmed.parse( + receiptEvent[0].topics, + receiptEvent[0].data + ); + this.proxyAddress = parsedLog['voteProxy']; + } +} diff --git a/packages/dai-plugin-governance/src/ChiefService.js b/packages/dai-plugin-governance/src/ChiefService.js new file mode 100644 index 000000000..c8ae1a7fb --- /dev/null +++ b/packages/dai-plugin-governance/src/ChiefService.js @@ -0,0 +1,191 @@ +import { LocalService } from '@makerdao/services-core'; +// maybe a "dai.js developer utils" package is useful? +import { MKR, CHIEF } from './utils/constants'; +import { getCurrency, netIdToName } from './utils/helpers'; + +// imports from 'reads' +import { memoizeWith, uniq, nth, takeLast, identity } from 'ramda'; +import contractInfo from '../contracts/contract-info.json'; +const chiefInfo = contractInfo.chief; + +export default class ChiefService extends LocalService { + constructor(name = 'chief') { + super(name, ['smartContract', 'web3']); + } + + // Writes ----------------------------------------------- + + etch(addresses) { + return this._chiefContract().etch(addresses); + } + + lift(address) { + return this._chiefContract().lift(address); + } + + vote(picks) { + if (Array.isArray(picks)) + return this._chiefContract()['vote(address[])'](picks); + return this._chiefContract()['vote(bytes32)'](picks); + } + + lock(amt, unit = MKR) { + const mkrAmt = getCurrency(amt, unit).toFixed('wei'); + return this._chiefContract().lock(mkrAmt); + } + + free(amt, unit = MKR) { + const mkrAmt = getCurrency(amt, unit).toFixed('wei'); + return this._chiefContract().free(mkrAmt); + } + + // Reads ------------------------------------------------ + + paddedBytes32ToAddress = hex => + hex.length > 42 ? '0x' + takeLast(40, hex) : hex; + + // helper for when we might call getSlateAddresses with the same slate several times + memoizedGetSlateAddresses = memoizeWith(identity, this.getSlateAddresses); + + getLockLogs = async () => { + const chiefAddress = this._chiefContract().address; + const web3Service = this.get('web3'); + const netId = web3Service.network; + const networkName = netIdToName(netId); + const locks = await web3Service.getPastLogs({ + fromBlock: chiefInfo.inception_block[networkName], + toBlock: 'latest', + address: chiefAddress, + topics: [chiefInfo.events.lock] + }); + + return uniq( + locks + .map(logObj => nth(1, logObj.topics)) + .map(this.paddedBytes32ToAddress) + ); + }; + + async getVoteTally() { + const voters = await this.getLockLogs(); + + const withDeposits = await Promise.all( + voters.map(voter => + this.getNumDeposits(voter).then(deposits => ({ + address: voter, + deposits: parseFloat(deposits) + })) + ) + ); + + const withSlates = await Promise.all( + withDeposits.map(addressDeposit => + this.getVotedSlate(addressDeposit.address).then(slate => ({ + ...addressDeposit, + slate + })) + ) + ); + + const withVotes = await Promise.all( + withSlates.map(withSlate => + this.memoizedGetSlateAddresses(withSlate.slate).then(addresses => ({ + ...withSlate, + votes: addresses + })) + ) + ); + + const voteTally = {}; + for (const voteObj of withVotes) { + for (let vote of voteObj.votes) { + vote = vote.toLowerCase(); + if (voteTally[vote] === undefined) { + voteTally[vote] = { + approvals: voteObj.deposits, + addresses: [ + { address: voteObj.address, deposits: voteObj.deposits } + ] + }; + } else { + voteTally[vote].approvals += voteObj.deposits; + voteTally[vote].addresses.push({ + address: voteObj.address, + deposits: voteObj.deposits + }); + } + } + } + for (const [key, value] of Object.entries(voteTally)) { + const sortedAddresses = value.addresses.sort( + (a, b) => b.deposits - a.deposits + ); + const approvals = voteTally[key].approvals; + const withPercentages = sortedAddresses.map(shapedVoteObj => ({ + ...shapedVoteObj, + percent: ((shapedVoteObj.deposits * 100) / approvals).toFixed(2) + })); + voteTally[key] = withPercentages; + } + return voteTally; + } + + getVotedSlate(address) { + return this._chiefContract().votes(address); + } + + getNumDeposits(address) { + return this._chiefContract() + .deposits(address) + .then(MKR.wei); + } + + getApprovalCount(address) { + return this._chiefContract() + .approvals(address) + .then(MKR.wei); + } + + getHat() { + return this._chiefContract().hat(); + } + + async getSlateAddresses(slateHash, i = 0) { + try { + return [await this._chiefContract().slates(slateHash, i)].concat( + await this.getSlateAddresses(slateHash, i + 1) + ); + } catch (_) { + return []; + } + } + + getLockAddressLogs() { + return new Promise((resolve, reject) => { + this._chiefContract({ web3js: true }) + .LogNote({ sig: '0xdd467064' }, { fromBlock: 0, toBlock: 'latest' }) + .get((error, result) => { + if (error) reject(error); + resolve(result.map(log => log.args.guy)); + }); + }); + } + + getEtchSlateLogs() { + return new Promise((resolve, reject) => { + this._chiefContract({ web3js: true }) + .Etch({}, { fromBlock: 0, toBlock: 'latest' }) + .get((error, result) => { + if (error) reject(error); + resolve(result.map(log => log.args.slate)); + }); + }); + } + + // Internal -------------------------------------------- + + _chiefContract({ web3js = false } = {}) { + if (web3js) return this.get('smartContract').getWeb3ContractByName(CHIEF); + return this.get('smartContract').getContractByName(CHIEF); + } +} diff --git a/packages/dai-plugin-governance/src/EsmService.js b/packages/dai-plugin-governance/src/EsmService.js new file mode 100644 index 000000000..a73398c5a --- /dev/null +++ b/packages/dai-plugin-governance/src/EsmService.js @@ -0,0 +1,91 @@ +import { PrivateService } from '@makerdao/services-core'; +import { MKR, ESM, END } from './utils/constants'; +import { getCurrency } from './utils/helpers'; + +export default class EsmService extends PrivateService { + constructor(name = 'esm') { + super(name, ['smartContract', 'web3', 'token', 'allowance']); + } + + async thresholdAmount() { + const min = await this._esmContract().min(); + return getCurrency(min, MKR).shiftedBy(-18); + } + + async fired() { + const _fired = await this._esmContract().fired(); + return _fired.eq(1); + } + + async emergencyShutdownActive() { + const active = await this._endContract().live(); + return active.eq(0); + } + + async canFire() { + const [fired, live] = await Promise.all([ + this.fired(), + this.emergencyShutdownActive() + ]); + return !fired && !live; + } + + async getTotalStaked() { + const total = await this._esmContract().Sum(); + return getCurrency(total, MKR).shiftedBy(-18); + } + + async getTotalStakedByAddress(address = false) { + if (!address) { + address = this.get('web3').currentAddress(); + } + const total = await this._esmContract().sum(address); + return getCurrency(total, MKR).shiftedBy(-18); + } + + async stake(amount, skipChecks = false) { + const mkrAmount = getCurrency(amount, MKR); + if (!skipChecks) { + const [fired, mkrBalance] = await Promise.all([ + this.fired(), + this.get('token') + .getToken(MKR) + .balance() + ]); + if (fired) { + throw new Error('cannot join when emergency shutdown has been fired'); + } + if (mkrBalance.lt(mkrAmount)) { + throw new Error('amount to join is greater than the user balance'); + } + } + return this._esmContract().join(mkrAmount.toFixed('wei')); + } + + async triggerEmergencyShutdown(skipChecks = false) { + if (!skipChecks) { + const [thresholdAmount, totalStaked, canFire] = await Promise.all([ + this.thresholdAmount(), + this.getTotalStaked(), + this.canFire() + ]); + if (totalStaked.lt(thresholdAmount)) { + throw new Error( + 'total amount of staked MKR has not reached the required threshold' + ); + } + if (!canFire) { + throw new Error('emergency shutdown has already been initiated'); + } + } + return this._esmContract().fire(); + } + + _esmContract() { + return this.get('smartContract').getContractByName(ESM); + } + + _endContract() { + return this.get('smartContract').getContractByName(END); + } +} diff --git a/packages/dai-plugin-governance/src/GovPollingService.js b/packages/dai-plugin-governance/src/GovPollingService.js new file mode 100644 index 000000000..f74f873e5 --- /dev/null +++ b/packages/dai-plugin-governance/src/GovPollingService.js @@ -0,0 +1,163 @@ +import { PrivateService } from '@makerdao/services-core'; +import { POLLING } from './utils/constants'; +import { MKR } from './utils/constants'; + +const POSTGRES_MAX_INT = 2147483647; + +export default class GovPollingService extends PrivateService { + constructor(name = 'govPolling') { + super(name, ['smartContract', 'govQueryApi', 'token']); + } + + async createPoll(startDate, endDate, multiHash, url) { + const txo = await this._pollingContract().createPoll( + startDate, + endDate, + multiHash, + url + ); + const pollId = parseInt(txo.receipt.logs[0].topics[2]); + return pollId; + } + + withdrawPoll(pollId) { + return this._pollingContract().withdrawPoll(pollId); + } + + vote(pollId, optionId) { + return this._pollingContract().vote(pollId, optionId); + } + + _pollingContract() { + return this.get('smartContract').getContractByName(POLLING); + } + + //--- cache queries + + async getPoll(multiHash) { + const polls = await this.getAllWhitelistedPolls(); + const filtered = polls.filter(p => p.multiHash === multiHash); + let lowest = Infinity; + let lowestPoll; + for (let i = 0; i < filtered.length; i++) { + if (filtered[i].pollId < lowest) { + lowest = filtered[i].pollId; + lowestPoll = filtered[i]; + } + } + return lowestPoll; + } + + async _getPoll(pollId) { + const polls = await this.getAllWhitelistedPolls(); + return polls.find(p => parseInt(p.pollId) === parseInt(pollId)); + } + + async getAllWhitelistedPolls() { + if (this.polls) return this.polls; + this.polls = await this.get('govQueryApi').getAllWhitelistedPolls(); + return this.polls; + } + + refresh() { + this.polls = null; + } + + async getOptionVotingFor(address, pollId) { + return this.get('govQueryApi').getOptionVotingFor( + address.toLowerCase(), + pollId + ); + } + + async getNumUniqueVoters(pollId) { + return this.get('govQueryApi').getNumUniqueVoters(pollId); + } + + async getMkrWeight(address) { + const weight = await this.get('govQueryApi').getMkrWeight( + address.toLowerCase(), + POSTGRES_MAX_INT + ); + return MKR(weight); + } + + async getMkrAmtVoted(pollId) { + const { endDate } = await this._getPoll(pollId); + const endUnix = Math.floor(endDate / 1000); + const endBlock = await this.get('govQueryApi').getBlockNumber(endUnix); + const weights = await this.get('govQueryApi').getMkrSupport( + pollId, + endBlock + ); + return MKR(weights.reduce((acc, cur) => acc + cur.mkrSupport, 0)); + } + + async getPercentageMkrVoted(pollId) { + const [voted, total] = await Promise.all([ + this.getMkrAmtVoted(pollId), + this.get('token') + .getToken(MKR) + .totalSupply() + ]); + return voted + .div(total) + .times(100) + .toNumber(); + } + + async getWinningProposal(pollId) { + const { endDate } = await this._getPoll(pollId); + const endUnix = Math.floor(endDate / 1000); + const endBlock = await this.get('govQueryApi').getBlockNumber(endUnix); + const currentVotes = await this.get('govQueryApi').getMkrSupport( + pollId, + endBlock + ); + let max = currentVotes[0]; + for (let i = 1; i < currentVotes.length; i++) { + if (currentVotes[i].mkrSupport > max.mkrSupport) { + max = currentVotes[i]; + } + } + return max ? max.optionId : 0; + } + + async getVoteHistory(pollId, numPlots) { + const { startDate, endDate } = await this._getPoll(pollId); + const startUnix = Math.floor(startDate / 1000); + const endUnix = Math.floor(endDate / 1000); + const [startBlock, endBlock] = await Promise.all([ + this.get('govQueryApi').getBlockNumber(startUnix), + this.get('govQueryApi').getBlockNumber(endUnix) //should return current block number if endDate hasn't happened yet + ]); + + const voteHistory = []; + const interval = Math.round((endBlock - startBlock) / numPlots); + if (interval === 0) { + const mkrSupport = await this.get('govQueryApi').getMkrSupport( + pollId, + endBlock + ); + voteHistory.push([ + { + time: mkrSupport[0].blockTimestamp, + options: mkrSupport + } + ]); + } else { + for (let i = endBlock; i >= startBlock; i -= interval) { + const mkrSupport = await this.get('govQueryApi').getMkrSupport( + pollId, + i + ); + const time = mkrSupport.length > 0 ? mkrSupport[0].blockTimestamp : 0; + voteHistory.push({ + time, + options: mkrSupport + }); + } + } + return voteHistory; + } +} diff --git a/packages/dai-plugin-governance/src/GovQueryApiService.js b/packages/dai-plugin-governance/src/GovQueryApiService.js new file mode 100644 index 000000000..65f9ec457 --- /dev/null +++ b/packages/dai-plugin-governance/src/GovQueryApiService.js @@ -0,0 +1,144 @@ +import { PublicService } from '@makerdao/services-core'; +import assert from 'assert'; +import { netIdtoSpockUrl, netIdtoSpockUrlStaging } from './utils/helpers'; + +export default class QueryApi extends PublicService { + constructor(name = 'govQueryApi') { + super(name, ['web3']); + this.queryPromises = {}; + this.staging = false; + } + + async getQueryResponse(serverUrl, query) { + const resp = await fetch(serverUrl, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + query + }) + }); + const { data } = await resp.json(); + assert(data, `error fetching data from ${serverUrl}`); + return data; + } + + async getQueryResponseMemoized(serverUrl, query) { + let cacheKey = `${serverUrl};${query}`; + if (this.queryPromises[cacheKey]) return this.queryPromises[cacheKey]; + this.queryPromises[cacheKey] = this.getQueryResponse(serverUrl, query); + return this.queryPromises[cacheKey]; + } + + initialize(settings) { + if (settings.staging) { + this.staging = true; + } + } + + connect() { + const network = this.get('web3').network; + this.serverUrl = this.staging + ? netIdtoSpockUrlStaging(network) + : netIdtoSpockUrl(network); + } + + async getAllWhitelistedPolls() { + const query = `{activePolls { + nodes { + creator + pollId + blockCreated + startDate + endDate + multiHash + url + } + } + }`; + + const response = await this.getQueryResponse(this.serverUrl, query); + return response.activePolls.nodes.map(p => { + p.startDate = new Date(p.startDate * 1000); + p.endDate = new Date(p.endDate * 1000); + return p; + }); + } + + async getNumUniqueVoters(pollId) { + const query = `{uniqueVoters(argPollId:${pollId}){ + nodes + } + }`; + + const response = await this.getQueryResponse(this.serverUrl, query); + return parseInt(response.uniqueVoters.nodes[0]); + } + + async getMkrWeight(address, blockNumber) { + const query = `{totalMkrWeightProxyAndNoProxyByAddress(argAddress: "${address}", argBlockNumber: ${blockNumber}){ + nodes { + address + weight + } + } + }`; + const response = await this.getQueryResponse(this.serverUrl, query); + if (!response.totalMkrWeightProxyAndNoProxyByAddress.nodes[0]) return 0; + return response.totalMkrWeightProxyAndNoProxyByAddress.nodes[0].weight; + } + + async getOptionVotingFor(address, pollId) { + const query = `{ + currentVote(argAddress: "${address}", argPollId: ${pollId}){ + nodes{ + optionId + } + } + }`; + const response = await this.getQueryResponse(this.serverUrl, query); + if (!response.currentVote.nodes[0]) return null; + return response.currentVote.nodes[0].optionId; + } + + async getBlockNumber(unixTime) { + const query = `{ + timeToBlockNumber(argUnix: ${unixTime}){ + nodes + } + }`; + const response = await this.getQueryResponse(this.serverUrl, query); + return response.timeToBlockNumber.nodes[0]; + } + + async getMkrSupport(pollId, blockNumber) { + const query = `{voteOptionMkrWeights(argPollId: ${pollId}, argBlockNumber: ${blockNumber}){ + nodes{ + optionId + mkrSupport + } + } + }`; + const response = await this.getQueryResponseMemoized(this.serverUrl, query); + let weights = response.voteOptionMkrWeights.nodes; + // We don't want to calculate votes for 0:abstain + weights = weights.filter(o => o.optionId !== 0); + const totalWeight = weights.reduce((acc, cur) => { + const mkrSupport = isNaN(parseFloat(cur.mkrSupport)) + ? 0 + : parseFloat(cur.mkrSupport); + return acc + mkrSupport; + }, 0); + return weights.map(o => { + const mkrSupport = isNaN(parseFloat(o.mkrSupport)) + ? 0 + : parseFloat(o.mkrSupport); + o.mkrSupport = mkrSupport; + o.percentage = (100 * mkrSupport) / totalWeight; + o.blockTimestamp = new Date(o.blockTimestamp); + return o; + }); + } +} diff --git a/packages/dai-plugin-governance/src/VoteProxy.js b/packages/dai-plugin-governance/src/VoteProxy.js new file mode 100644 index 000000000..91539fb38 --- /dev/null +++ b/packages/dai-plugin-governance/src/VoteProxy.js @@ -0,0 +1,38 @@ +export default class VoteProxy { + constructor({ voteProxyService, proxyAddress, coldAddress, hotAddress }) { + this._voteProxyService = voteProxyService; + this._proxyAddress = proxyAddress; + this._coldAddress = coldAddress; + this._hotAddress = hotAddress; + } + + getProxyAddress() { + return this._proxyAddress; + } + + getColdAddress() { + return this._coldAddress; + } + + getHotAddress() { + return this._hotAddress; + } +} + +const passthroughMethods = [ + 'lock', + 'free', + 'voteExec', + 'getNumDeposits', + 'getVotedProposalAddresses' +]; + +Object.assign( + VoteProxy.prototype, + passthroughMethods.reduce((acc, name) => { + acc[name] = function(...args) { + return this._voteProxyService[name](this._proxyAddress, ...args); + }; + return acc; + }, {}) +); diff --git a/packages/dai-plugin-governance/src/VoteProxyFactoryService.js b/packages/dai-plugin-governance/src/VoteProxyFactoryService.js new file mode 100644 index 000000000..1b4f30873 --- /dev/null +++ b/packages/dai-plugin-governance/src/VoteProxyFactoryService.js @@ -0,0 +1,33 @@ +import { LocalService } from '@makerdao/services-core'; +import { VOTE_PROXY_FACTORY } from './utils/constants'; +import ApproveLinkTransaction from './ApproveLinkTransaction'; + +export default class VoteProxyFactoryService extends LocalService { + constructor(name = 'voteProxyFactory') { + super(name, ['smartContract', 'voteProxy', 'transactionManager']); + } + + initiateLink(hotAddress) { + return this._proxyFactoryContract().initiateLink(hotAddress); + } + + approveLink(coldAddress) { + const tx = new ApproveLinkTransaction( + this._proxyFactoryContract(), + this.get('transactionManager') + ); + return tx.build('approveLink', [coldAddress]); + } + + breakLink() { + return this._proxyFactoryContract().breakLink(); + } + + getVoteProxy(address) { + return this.get('voteProxy').getVoteProxy(address); + } + + _proxyFactoryContract() { + return this.get('smartContract').getContractByName(VOTE_PROXY_FACTORY); + } +} diff --git a/packages/dai-plugin-governance/src/VoteProxyService.js b/packages/dai-plugin-governance/src/VoteProxyService.js new file mode 100644 index 000000000..cb80da2aa --- /dev/null +++ b/packages/dai-plugin-governance/src/VoteProxyService.js @@ -0,0 +1,103 @@ +import { LocalService } from '@makerdao/services-core'; +import VoteProxy from './VoteProxy'; +import { MKR, VOTE_PROXY_FACTORY, ZERO_ADDRESS } from './utils/constants'; +// maybe a "dai.js developer utils" package is useful? +import { getCurrency } from './utils/helpers'; +import voteProxyAbi from '../contracts/abis/VoteProxy.json'; + +export default class VoteProxyService extends LocalService { + constructor(name = 'voteProxy') { + super(name, ['smartContract', 'chief']); + } + + // Writes ----------------------------------------------- + + lock(proxyAddress, amt, unit = MKR) { + const mkrAmt = getCurrency(amt, unit).toFixed('wei'); + return this._proxyContract(proxyAddress).lock(mkrAmt); + } + + free(proxyAddress, amt, unit = MKR) { + const mkrAmt = getCurrency(amt, unit).toFixed('wei'); + return this._proxyContract(proxyAddress).free(mkrAmt); + } + + freeAll(proxyAddress) { + return this._proxyContract(proxyAddress).freeAll(); + } + + voteExec(proxyAddress, picks) { + if (Array.isArray(picks)) + return this._proxyContract(proxyAddress)['vote(address[])'](picks); + return this._proxyContract(proxyAddress)['vote(bytes32)'](picks); + } + + // Reads ------------------------------------------------ + + async getVotedProposalAddresses(proxyAddress) { + const _slate = await this.get('chief').getVotedSlate(proxyAddress); + return this.get('chief').getSlateAddresses(_slate); + } + + async getVoteProxy(addressToCheck) { + const { + hasProxy, + role, + address: proxyAddress + } = await this._getProxyStatus(addressToCheck); + if (!hasProxy) return { hasProxy, voteProxy: null }; + const otherRole = role === 'hot' ? 'cold' : 'hot'; + const otherAddress = await this._getAddressOfRole(proxyAddress, otherRole); + return { + hasProxy, + voteProxy: new VoteProxy({ + voteProxyService: this, + proxyAddress, + [`${role}Address`]: addressToCheck, + [`${otherRole}Address`]: otherAddress + }) + }; + } + + // Internal -------------------------------------------- + + _proxyContract(address) { + return this.get('smartContract').getContractByAddressAndAbi( + address, + voteProxyAbi + ); + } + + _proxyFactoryContract() { + return this.get('smartContract').getContractByName(VOTE_PROXY_FACTORY); + } + + async _getProxyStatus(address) { + const [proxyAddressCold, proxyAddressHot] = await Promise.all([ + this._proxyFactoryContract().coldMap(address), + this._proxyFactoryContract().hotMap(address) + ]); + if (proxyAddressCold !== ZERO_ADDRESS) + return { role: 'cold', address: proxyAddressCold, hasProxy: true }; + if (proxyAddressHot !== ZERO_ADDRESS) + return { role: 'hot', address: proxyAddressHot, hasProxy: true }; + return { role: null, address: '', hasProxy: false }; + } + + _getAddressOfRole(proxyAddress, role) { + if (role === 'hot') return this._proxyContract(proxyAddress).hot(); + else if (role === 'cold') return this._proxyContract(proxyAddress).cold(); + return null; + } +} + +// add a few Chief Service methods to the Vote Proxy Service +Object.assign( + VoteProxyService.prototype, + ['getVotedSlate', 'getNumDeposits'].reduce((acc, name) => { + acc[name] = function(...args) { + return this.get('chief')[name](...args); + }; + return acc; + }, {}) +); diff --git a/packages/dai-plugin-governance/src/index.js b/packages/dai-plugin-governance/src/index.js new file mode 100644 index 000000000..cb0c7811a --- /dev/null +++ b/packages/dai-plugin-governance/src/index.js @@ -0,0 +1,102 @@ +import { map, prop } from 'ramda'; +import { + VOTE_PROXY_FACTORY, + CHIEF, + POLLING, + ESM, + END, + MKR, + IOU +} from './utils/constants'; + +import ChiefService from './ChiefService'; +import VoteProxyService from './VoteProxyService'; +import VoteProxyFactoryService from './VoteProxyFactoryService'; +import GovPollingService from './GovPollingService'; +import GovQueryApiService from './GovQueryApiService'; +import EsmService from './EsmService'; + +export default { + addConfig: function(config, { network = 'mainnet', mcd, staging = false }) { + const contractAddresses = { + kovan: mcd + ? require('../contracts/addresses/kovan-mcd.json') + : require('../contracts/addresses/kovan.json'), + mainnet: require('../contracts/addresses/mainnet.json') + }; + + try { + contractAddresses.testnet = require('../contracts/addresses/testnet.json'); + } catch (err) { + // do nothing here; throw an error only if we later attempt to use ganache + } + + const addressKey = network == 'ganache' ? 'testnet' : network; + + const esmContracts = + network === 'ganache' + ? { + [ESM]: { + address: map(prop('MCD_ESM'), contractAddresses), + abi: require('../contracts/abis/ESM.json') + }, + [END]: { + address: map(prop('MCD_END'), contractAddresses), + abi: require('../contracts/abis/End.json') + } + } + : {}; + + const addContracts = { + [CHIEF]: { + address: map(prop('CHIEF'), contractAddresses), + // TODO check for MCD-specific version of DSChief + abi: require('../contracts/abis/DSChief.json') + }, + [VOTE_PROXY_FACTORY]: { + address: map(prop('VOTE_PROXY_FACTORY'), contractAddresses), + abi: require('../contracts/abis/VoteProxyFactory.json') + }, + [POLLING]: { + address: map(prop('POLLING'), contractAddresses), + abi: require('../contracts/abis/Polling.json') + }, + ...esmContracts + }; + + const makerConfig = { + ...config, + additionalServices: [ + 'chief', + 'voteProxy', + 'voteProxyFactory', + 'govPolling', + 'govQueryApi', + 'esm' + ], + chief: [ChiefService], + voteProxy: [VoteProxyService], + voteProxyFactory: [VoteProxyFactoryService], + govPolling: [GovPollingService], + govQueryApi: [GovQueryApiService, { staging }], + esm: [EsmService], + smartContract: { addContracts }, + token: { + erc20: [ + { + currency: MKR, + symbol: MKR.symbol, + address: contractAddresses[addressKey].GOV + }, + { + currency: IOU, + symbol: IOU.symbol, + address: contractAddresses[addressKey].IOU + } + ] + } + }; + + return makerConfig; + } +}; diff --git a/packages/dai-plugin-governance/src/utils/constants.js b/packages/dai-plugin-governance/src/utils/constants.js new file mode 100644 index 000000000..b3741c5e5 --- /dev/null +++ b/packages/dai-plugin-governance/src/utils/constants.js @@ -0,0 +1,21 @@ +import { createCurrency } from '@makerdao/currency'; + +export const MKR = createCurrency('MKR'); +export const IOU = createCurrency('IOU'); + +/* Contracts */ +export const VOTE_PROXY_FACTORY = 'VOTE_PROXY_FACTORY'; +export const POLLING = 'POLLING'; +export const CHIEF = 'CHIEF'; +export const ESM = 'ESM'; +export const END = 'END'; + +/* Addresses */ +export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + +/* Spock URLs */ +export const LOCAL_URL = 'http://localhost:3001/v1'; +export const KOVAN_URL = 'https://staging-gov-db.makerfoundation.com/api/v1'; +export const STAGING_MAINNET_URL = + 'https://qa-gov-db.makerfoundation.com/api/v1'; +export const MAINNET_URL = 'https://gov-db.makerfoundation.com/api/v1'; diff --git a/packages/dai-plugin-governance/src/utils/helpers.js b/packages/dai-plugin-governance/src/utils/helpers.js new file mode 100644 index 000000000..56222ba87 --- /dev/null +++ b/packages/dai-plugin-governance/src/utils/helpers.js @@ -0,0 +1,50 @@ +import { createGetCurrency } from '@makerdao/currency'; +import { + MKR, + LOCAL_URL, + STAGING_MAINNET_URL, + KOVAN_URL, + MAINNET_URL +} from './constants'; + +/** + * @desc get network name + * @param {Number|String} id + * @return {String} + */ +export const netIdToName = id => { + switch (parseInt(id, 10)) { + case 1: + return 'mainnet'; + case 42: + return 'kovan'; + case 999: + return 'ganache'; + default: + return ''; + } +}; + +export const netIdtoSpockUrl = id => { + switch (parseInt(id, 10)) { + case 1: + return MAINNET_URL; + case 42: + return KOVAN_URL; + default: + return LOCAL_URL; + } +}; + +export const netIdtoSpockUrlStaging = id => { + switch (parseInt(id, 10)) { + case 1: + return STAGING_MAINNET_URL; + case 42: + return KOVAN_URL; + default: + return LOCAL_URL; + } +}; + +export const getCurrency = createGetCurrency({ MKR }); diff --git a/packages/dai-plugin-governance/test/ChiefService.test.js b/packages/dai-plugin-governance/test/ChiefService.test.js new file mode 100644 index 000000000..c3a2d4769 --- /dev/null +++ b/packages/dai-plugin-governance/test/ChiefService.test.js @@ -0,0 +1,115 @@ +import { + setupTestMakerInstance, + setUpAllowance, + restoreSnapshotOriginal, + sleep +} from './helpers'; +import { ZERO_ADDRESS } from '../src/utils/constants'; +import ChiefService from '../src/ChiefService'; +import * as web3utils from 'web3-utils'; + +let maker, chiefService; + +const picks = [ + '0x26EC003c72ebA27749083d588cdF7EBA665c0A1D', + '0x54F4E468FB0297F55D8DfE57336D186009A1455a' +]; +const mkrToLock = 3; + +jest.setTimeout(60000); + +beforeAll(async () => { + maker = await setupTestMakerInstance(); + chiefService = maker.service('chief'); + + maker.useAccount('owner'); +}); + +afterAll(async done => { + if (global.useOldChain) { + await restoreSnapshotOriginal(global.snapshotId); + done(); + } else { + global.client.restoreSnapshot(global.testchainId, global.defaultSnapshotId); + await sleep(15000); + + await global.client.delete(global.testchainId); + await sleep(15000); + + done(); + } +}); + +test('can create Chief Service', async () => { + expect(chiefService).toBeInstanceOf(ChiefService); +}); + +test('can cast vote with an array of addresses', async () => { + // owner casts vote with picks array + await chiefService.vote(picks); + const slate = await chiefService.getVotedSlate( + maker.currentAccount().address + ); + + const addrs = await chiefService.getSlateAddresses(slate); + + expect(addrs).toEqual(picks); +}); + +test('can cast vote with a slate hash', async () => { + // etch the picks + await chiefService.etch(picks); + + // hash the picks to get slate hash + const hash = web3utils.soliditySha3({ type: 'address[]', value: picks }); + + // cast a vote for the slate hash + await chiefService.vote(hash); + + const slate = await chiefService.getVotedSlate( + maker.currentAccount().address + ); + expect(slate).toBe(hash); + expect(slate).not.toBe(ZERO_ADDRESS); + + const addresses = await chiefService.getSlateAddresses(slate); + + expect(addresses).toEqual(picks); +}); + +test('number of deposits for a proxy contract address should equal locked MKR amount', async () => { + await setUpAllowance(maker, chiefService._chiefContract().address); + await chiefService.lock(mkrToLock); + + const numDeposits = await chiefService.getNumDeposits( + maker.currentAccount().address + ); + + expect(numDeposits.toNumber()).toBe(mkrToLock); +}); + +test('approval count for a voted-on address should equal locked MKR amount', async () => { + const approvalCount = await chiefService.getApprovalCount(picks[0]); + expect(approvalCount.toNumber()).toBe(mkrToLock); +}); + +test('getVoteTally returns the vote tally', async () => { + const voteTally = await chiefService.getVoteTally(); + + expect.assertions(picks.length); + picks.map(pick => + expect(Object.keys(voteTally).includes(pick.toLowerCase())).toBe(true) + ); +}); + +test('get hat should return lifted address', async () => { + const addressToLift = picks[0]; + + const oldHat = await chiefService.getHat(); + expect(oldHat).not.toBe(addressToLift); + + await chiefService.lift(addressToLift); + + const newHat = await chiefService.getHat(); + expect(newHat).toBe(addressToLift); +}); diff --git a/packages/dai-plugin-governance/test/EsmService.test.js b/packages/dai-plugin-governance/test/EsmService.test.js new file mode 100644 index 000000000..7a7d0d6fb --- /dev/null +++ b/packages/dai-plugin-governance/test/EsmService.test.js @@ -0,0 +1,106 @@ +import { + setupTestMakerInstance, + setUpAllowance, + restoreSnapshotOriginal, + sleep, + addressRegex +} from './helpers'; +import EsmService from '../src/EsmService'; + +let maker, esmService; +beforeAll(async () => { + maker = await setupTestMakerInstance(); + esmService = maker.service('esm'); + + await setUpAllowance(maker, esmService._esmContract().address); +}); + +afterAll(async done => { + if (global.useOldChain) { + await restoreSnapshotOriginal(global.snapshotId); + done(); + } else { + global.client.restoreSnapshot(global.testchainId, global.defaultSnapshotId); + await sleep(15000); + + await global.client.delete(global.testchainId); + await sleep(15000); + + done(); + } +}); + +test('can create ESM Service', async () => { + expect(esmService).toBeInstanceOf(EsmService); +}); + +test('can access deployed esm contract interface', async () => { + const contract = await esmService._esmContract(); + expect(addressRegex.test(contract.address)).toBe(true); +}); + +test('can access deployed end contract interface', async () => { + const contract = await esmService._endContract(); + expect(addressRegex.test(contract.address)).toBe(true); +}); + +test('can return the minimum threshold', async () => { + const threshold = await esmService.thresholdAmount(); + expect(threshold.toNumber()).toBe(50000); +}); + +test('can check whether emergency shutdown is active', async () => { + const active = await esmService.emergencyShutdownActive(); + expect(active).toBe(false); +}); + +test('can check if emergency shutdown is fireable', async () => { + const fireable = await esmService.canFire(); + expect(fireable).toBe(true); +}); + +test('can return the total amount of staked MKR', async () => { + const totalStaked = await esmService.getTotalStaked(); + expect(totalStaked.toNumber()).toEqual(0); +}); + +test('can return the total amount of staked MKR per user', async () => { + const totalStaked = await esmService.getTotalStakedByAddress(); + expect(totalStaked.toNumber()).toEqual(0); +}); + +test('can stake mkr', async () => { + await esmService.stake(1); + const totalStaked = await esmService.getTotalStaked(); + expect(totalStaked.toNumber()).toEqual(1); +}); + +test('attempt to stake more MKR than balance will fail', async () => { + expect.assertions(1); + try { + await esmService.stake(100000); + } catch (e) { + expect(e).toEqual(Error('amount to join is greater than the user balance')); + } +}); + +test('triggering the esm with staked amount < threshold will fail', async () => { + expect.assertions(1); + try { + await esmService.triggerEmergencyShutdown(); + } catch (e) { + expect(e).toEqual( + Error('total amount of staked MKR has not reached the required threshold') + ); + } +}); + +xtest('can trigger emergency shutdown', async () => { + await esmService.stake(50001); + + await esmService.triggerEmergencyShutdown(); + const fireable = await esmService.canFire(); + console.log(fireable); + const active = await esmService.emergencyShutdownActive(); + expect(active).toBe(true); +}); diff --git a/packages/dai-plugin-governance/test/GovPollingService.test.js b/packages/dai-plugin-governance/test/GovPollingService.test.js new file mode 100644 index 000000000..837faca86 --- /dev/null +++ b/packages/dai-plugin-governance/test/GovPollingService.test.js @@ -0,0 +1,159 @@ +import { + setupTestMakerInstance, + restoreSnapshotOriginal, + sleep +} from './helpers'; +import GovPollingService from '../src/GovPollingService'; +import { + dummyMkrSupportData, + dummyAllPollsData, + dummyBlockNumber, + dummyOption, + dummyWeight, + dummyNumUnique +} from './fixtures'; +import { MKR } from '../src/utils/constants'; + +let maker, govPollingService, govQueryApiService; + +jest.setTimeout(60000); + +beforeAll(async () => { + maker = await setupTestMakerInstance(); + govPollingService = maker.service('govPolling'); + govQueryApiService = maker.service('govQueryApi'); + + maker.useAccount('owner'); +}); + +afterAll(async done => { + if (global.useOldChain) { + await restoreSnapshotOriginal(global.snapshotId); + done(); + } else { + global.client.restoreSnapshot(global.testchainId, global.defaultSnapshotId); + await sleep(15000); + await global.client.delete(global.testchainId); + await sleep(15000); + done(); + } +}); + +test('can create Gov Polling Service', () => { + expect(govPollingService).toBeInstanceOf(GovPollingService); +}); + +test('can create poll', async () => { + const START_DATE = Math.floor(new Date().getTime() / 1000) + 100; + const END_DATE = START_DATE + 5000; + const MULTI_HASH = 'dummy hash'; + const URL = 'dummy url'; + const firstPollId = await govPollingService.createPoll( + START_DATE, + END_DATE, + MULTI_HASH, + URL + ); + expect(firstPollId).not.toBeNaN(); + + const secondPollId = await govPollingService.createPoll( + START_DATE, + END_DATE, + MULTI_HASH, + URL + ); + expect(secondPollId).toBe(firstPollId + 1); +}); + +test('can vote', async () => { + const OPTION_ID = 3; + const txo = await govPollingService.vote(0, OPTION_ID); + const loggedOptionId = parseInt(txo.receipt.logs[0].topics[3]); + // this will fail if the event was not emitted + expect(loggedOptionId).toBe(OPTION_ID); +}); + +test('can withdraw poll', async () => { + const POLL_ID = 0; + const txo = await govPollingService.withdrawPoll(POLL_ID); + // slice off the zeros used to pad the address to 32 bytes + const loggedCaller = txo.receipt.logs[0].topics[1].slice(26); + const { address: activeAddress } = maker.currentAccount(); + // this will fail if the event was not emitted + expect(loggedCaller).toBe(activeAddress.slice(2)); +}); + +//--- caching tests + +test('getAllWhitelistedPolls', async () => { + const mockFn = jest.fn(async () => dummyAllPollsData); + govQueryApiService.getAllWhitelistedPolls = mockFn; + const polls = await govPollingService.getAllWhitelistedPolls(); + expect(mockFn).toBeCalled(); + expect(polls).toEqual(dummyAllPollsData); +}); + +test('getMkrAmtVoted', async () => { + const mockFn = jest.fn(async () => dummyMkrSupportData); + govQueryApiService.getMkrSupport = mockFn; + govQueryApiService.getBlockNumber = jest.fn(); + const total = await govPollingService.getMkrAmtVoted(1); + expect(mockFn).toBeCalled(); + expect(total).toEqual(MKR(160)); +}); + +test('getOptionVotingFor', async () => { + const mockFn = jest.fn(async () => dummyOption); + govQueryApiService.getOptionVotingFor = mockFn; + const option = await govPollingService.getOptionVotingFor('0xaddress', 1); + expect(mockFn).toBeCalled(); + expect(option).toEqual(dummyOption); +}); + +test('getNumUniqueVoters', async () => { + const mockFn = jest.fn(async () => dummyNumUnique); + govQueryApiService.getNumUniqueVoters = mockFn; + const option = await govPollingService.getNumUniqueVoters(1); + expect(mockFn).toBeCalled(); + expect(option).toEqual(dummyNumUnique); +}); + +test('getMkrWeight', async () => { + const mockFn = jest.fn(async () => dummyWeight); + govQueryApiService.getMkrWeight = mockFn; + const option = await govPollingService.getMkrWeight('0xaddress'); + expect(mockFn).toBeCalled(); + expect(option).toEqual(MKR(dummyWeight)); +}); + +test('getWinningProposal', async () => { + const mockFn = jest.fn(async () => dummyMkrSupportData); + govQueryApiService.getMkrSupport = mockFn; + const option = await govPollingService.getWinningProposal(1); + expect(mockFn).toBeCalled(); + expect(option).toBe(2); +}); + +test('getVoteHistory', async () => { + // clear polls cache + govPollingService.refresh(); + const mockFn1 = jest.fn(async () => dummyAllPollsData); + govQueryApiService.getAllWhitelistedPolls = mockFn1; + const mockFn2 = jest.fn(async () => dummyMkrSupportData); + govQueryApiService.getMkrSupport = mockFn2; + const mockFn3 = jest.fn(async t => dummyBlockNumber(t)); + govQueryApiService.getBlockNumber = mockFn3; + const history = await govPollingService.getVoteHistory(1, 3); + expect(mockFn1).toBeCalled(); + expect(mockFn2).toBeCalled(); + expect(mockFn3).toBeCalled(); + expect(history[0].options).toBe(dummyMkrSupportData); +}); + +test('getPercentageMkrVoted', async () => { + const mockFn = jest.fn(async () => dummyMkrSupportData); + govQueryApiService.getMkrSupport = mockFn; + const percentage = await govPollingService.getPercentageMkrVoted(1); + expect(mockFn).toBeCalled(); + expect(percentage).toBe(40); +}); diff --git a/packages/dai-plugin-governance/test/VoteProxy.test.js b/packages/dai-plugin-governance/test/VoteProxy.test.js new file mode 100644 index 000000000..9df76faed --- /dev/null +++ b/packages/dai-plugin-governance/test/VoteProxy.test.js @@ -0,0 +1,54 @@ +import { + setupTestMakerInstance, + linkAccounts, + restoreSnapshotOriginal, + sleep +} from './helpers'; +import VoteProxy from '../src/VoteProxy'; + +let maker, addresses, voteProxyService; + +jest.setTimeout(60000); + +beforeAll(async () => { + maker = await setupTestMakerInstance(); + + voteProxyService = maker.service('voteProxy'); + + addresses = maker + .listAccounts() + .reduce((acc, cur) => ({ ...acc, [cur.name]: cur.address }), {}); + + await linkAccounts(maker, addresses.ali, addresses.ava); +}); + +afterAll(async done => { + if (global.useOldChain) { + await restoreSnapshotOriginal(global.snapshotId); + done(); + } else { + global.client.restoreSnapshot(global.testchainId, global.defaultSnapshotId); + await sleep(15000); + + await global.client.delete(global.testchainId); + await sleep(15000); + + done(); + } +}); + +test('Vote proxy instance returns correct information about itself', async () => { + const { voteProxy } = await voteProxyService.getVoteProxy(addresses.ali); + expect(voteProxy).toBeInstanceOf(VoteProxy); + + const vpAddress = voteProxy.getProxyAddress(); + expect(vpAddress).toBeTruthy(); + + // Hot address should be the same as the approver + const hotAddress = voteProxy.getHotAddress(); + expect(hotAddress.toLowerCase()).toBe(addresses.ava.toLowerCase()); + + // Cold address should be the same as the initiator + const coldAddress = voteProxy.getColdAddress(); + expect(coldAddress.toLowerCase()).toBe(addresses.ali.toLowerCase()); +}); diff --git a/packages/dai-plugin-governance/test/VoteProxyFactoryService.test.js b/packages/dai-plugin-governance/test/VoteProxyFactoryService.test.js new file mode 100644 index 000000000..e77b71cfa --- /dev/null +++ b/packages/dai-plugin-governance/test/VoteProxyFactoryService.test.js @@ -0,0 +1,81 @@ +import { + setupTestMakerInstance, + linkAccounts, + restoreSnapshotOriginal, + sleep +} from './helpers'; +import VoteProxyFactoryService from '../src/VoteProxyFactoryService'; + +let maker, addresses, voteProxyFactory, voteProxyService; +jest.setTimeout(60000); + +beforeAll(async () => { + maker = await setupTestMakerInstance(); + + addresses = maker + .listAccounts() + .reduce((acc, cur) => ({ ...acc, [cur.name]: cur.address }), {}); + + voteProxyFactory = maker.service('voteProxyFactory'); + voteProxyService = maker.service('voteProxy'); +}); + +afterAll(async done => { + if (global.useOldChain) { + await restoreSnapshotOriginal(global.snapshotId); + done(); + } else { + global.client.restoreSnapshot(global.testchainId, global.defaultSnapshotId); + await sleep(15000); + + await global.client.delete(global.testchainId); + await sleep(15000); + + done(); + } +}); + +test('can create VPFS Service', async () => { + const vpfs = maker.service('voteProxyFactory'); + expect(vpfs).toBeInstanceOf(VoteProxyFactoryService); +}); + +test('can create a vote proxy linking two addressses', async () => { + await linkAccounts(maker, addresses.ali, addresses.ava); + + const { hasProxy } = await voteProxyService.getVoteProxy(addresses.ali); + expect(hasProxy).toBeTruthy(); +}); + +test('can break a link between linked accounts', async () => { + maker.useAccount('ali'); + await voteProxyFactory.breakLink(); + + const { hasProxy } = await voteProxyService.getVoteProxy(addresses.ali); + expect(hasProxy).toBe(false); +}); + +test('approveLink txObject gets correct proxyAddress', async () => { + const initiator = addresses.ali; + const approver = addresses.ava; + const lad = maker.currentAccount().name; + + // initiator wants to create a link with approver + maker.useAccountWithAddress(initiator); + await maker.service('voteProxyFactory').initiateLink(approver); + + // approver confirms it + maker.useAccountWithAddress(approver); + const approveTx = await maker + .service('voteProxyFactory') + .approveLink(initiator); + + // no other side effects + maker.useAccount(lad); + + const { voteProxy } = await voteProxyService.getVoteProxy(addresses.ali); + expect(voteProxy.getProxyAddress()).toEqual(approveTx.proxyAddress); + expect(approveTx.fees.toNumber()).toBeGreaterThan(0); + expect(approveTx.timeStampSubmitted).toBeTruthy(); + expect(approveTx.timeStamp).toBeTruthy(); +}); diff --git a/packages/dai-plugin-governance/test/VoteProxyService.test.js b/packages/dai-plugin-governance/test/VoteProxyService.test.js new file mode 100644 index 000000000..b3fbc7884 --- /dev/null +++ b/packages/dai-plugin-governance/test/VoteProxyService.test.js @@ -0,0 +1,130 @@ +import { + setupTestMakerInstance, + linkAccounts, + sendMkrToAddress, + setUpAllowance, + restoreSnapshotOriginal, + sleep +} from './helpers'; +import VoteProxyService from '../src/VoteProxyService'; +import VoteProxy from '../src/VoteProxy'; + +let maker, addresses, voteProxyService, chiefService; + +jest.setTimeout(60000); + +beforeAll(async () => { + maker = await setupTestMakerInstance(); + + voteProxyService = maker.service('voteProxy'); + chiefService = maker.service('chief'); + + addresses = maker + .listAccounts() + .reduce((acc, cur) => ({ ...acc, [cur.name]: cur.address }), {}); + + await linkAccounts(maker, addresses.ali, addresses.ava); +}); + +afterAll(async done => { + if (global.useOldChain) { + await restoreSnapshotOriginal(global.snapshotId); + done(); + } else { + global.client.restoreSnapshot(global.testchainId, global.defaultSnapshotId); + await sleep(15000); + + await global.client.delete(global.testchainId); + await sleep(15000); + + done(); + } +}); + +test('can create VP Service', async () => { + const vps = maker.service('voteProxy'); + expect(vps).toBeInstanceOf(VoteProxyService); +}); + +test('can lock an amount of MKR', async () => { + const sendAmount = 5; + const amountToLock = 3; + await sendMkrToAddress(maker, addresses.owner, addresses.ali, sendAmount); + + maker.useAccount('ali'); + + const { voteProxy } = await voteProxyService.getVoteProxy(addresses.ali); + + const vpAddress = voteProxy.getProxyAddress(); + + await setUpAllowance(maker, vpAddress, voteProxy.getColdAddress()); + + // No deposits prior to locking maker + const preLockDeposits = await chiefService.getNumDeposits(vpAddress); + expect(preLockDeposits.toNumber()).toBe(0); + + await voteProxyService.lock(vpAddress, amountToLock); + + const postLockDeposits = await chiefService.getNumDeposits(vpAddress); + expect(postLockDeposits.toNumber()).toBe(amountToLock); +}); + +test('can cast an executive vote and retrieve voted on addresses from slate', async () => { + const { voteProxy } = await voteProxyService.getVoteProxy(addresses.ali); + const vpAddress = voteProxy.getProxyAddress(); + const picks = [ + '0x26EC003c72ebA27749083d588cdF7EBA665c0A1D', + '0x54F4E468FB0297F55D8DfE57336D186009A1455a' + ]; + + await voteProxyService.voteExec(vpAddress, picks); + + const addressesVotedOn = await voteProxyService.getVotedProposalAddresses( + vpAddress + ); + expect(addressesVotedOn).toEqual(picks); +}); + +test('can free an amount of MKR', async () => { + const amountToFree = 1; + + const { voteProxy } = await voteProxyService.getVoteProxy(addresses.ali); + const vpAddress = voteProxy.getProxyAddress(); + + const preFreeDeposits = await chiefService.getNumDeposits(vpAddress); + await voteProxyService.free(vpAddress, amountToFree); + + const postFreeDeposits = await chiefService.getNumDeposits(vpAddress); + expect(postFreeDeposits.toNumber()).toBe( + preFreeDeposits.toNumber() - amountToFree + ); +}); + +test('can free all MKR', async () => { + const { voteProxy } = await voteProxyService.getVoteProxy(addresses.ali); + const vpAddress = voteProxy.getProxyAddress(); + + const preFreeDeposits = await chiefService.getNumDeposits(vpAddress); + expect(preFreeDeposits.toNumber()).toBeGreaterThan(0); + + await voteProxyService.freeAll(vpAddress); + + const postFreeDeposits = await chiefService.getNumDeposits(vpAddress); + expect(postFreeDeposits.toNumber()).toBe(0); +}); + +test('getVoteProxy returns a VoteProxy if one exists for a given address', async () => { + const address = addresses.ali; + const { hasProxy, voteProxy } = await voteProxyService.getVoteProxy(address); + + expect(hasProxy).toBe(true); + expect(voteProxy).toBeInstanceOf(VoteProxy); +}); + +test('getVoteProxy returns a null if none exists for a given address', async () => { + const address = addresses.sam; + const { hasProxy, voteProxy } = await voteProxyService.getVoteProxy(address); + + expect(hasProxy).toBe(false); + expect(voteProxy).toBeNull(); +}); diff --git a/packages/dai-plugin-governance/test/config/jestIntegrationConfig.json b/packages/dai-plugin-governance/test/config/jestIntegrationConfig.json new file mode 100644 index 000000000..d01df6f37 --- /dev/null +++ b/packages/dai-plugin-governance/test/config/jestIntegrationConfig.json @@ -0,0 +1,6 @@ +{ + "rootDir": "../../", + "testMatch": ["/test/integration/*.js"], + "setupFilesAfterEnv": ["/test/config/original-setup.js"], + "testPathIgnorePatterns": ["/node_modules/", "/dist/"] +} diff --git a/packages/dai-plugin-governance/test/config/jestTestchainConfig.json b/packages/dai-plugin-governance/test/config/jestTestchainConfig.json new file mode 100644 index 000000000..b14008b91 --- /dev/null +++ b/packages/dai-plugin-governance/test/config/jestTestchainConfig.json @@ -0,0 +1,9 @@ +{ + "rootDir": "../../", + "roots": ["lib", "src", "test"], + "setupFilesAfterEnv": ["/test/config/setup-test.js"], + "testPathIgnorePatterns": [ + "/node_modules/", + "/test/integration/" + ] +} diff --git a/packages/dai-plugin-governance/test/config/jestTestchainConfigOriginal.json b/packages/dai-plugin-governance/test/config/jestTestchainConfigOriginal.json new file mode 100644 index 000000000..c5a85c29f --- /dev/null +++ b/packages/dai-plugin-governance/test/config/jestTestchainConfigOriginal.json @@ -0,0 +1,9 @@ +{ + "rootDir": "../../", + "roots": ["src", "test"], + "setupFilesAfterEnv": ["/test/config/original-setup.js"], + "testPathIgnorePatterns": [ + "/node_modules/", + "/test/integration/" + ] +} diff --git a/packages/dai-plugin-governance/test/config/original-setup.js b/packages/dai-plugin-governance/test/config/original-setup.js new file mode 100644 index 000000000..184c14024 --- /dev/null +++ b/packages/dai-plugin-governance/test/config/original-setup.js @@ -0,0 +1,7 @@ +import { takeSnapshotOriginal } from '../helpers'; + +beforeAll(async done => { + global.useOldChain = true; + global.snapshotId = await takeSnapshotOriginal(); + done(); +}); diff --git a/packages/dai-plugin-governance/test/config/setup-test.js b/packages/dai-plugin-governance/test/config/setup-test.js new file mode 100644 index 000000000..19f51a698 --- /dev/null +++ b/packages/dai-plugin-governance/test/config/setup-test.js @@ -0,0 +1,59 @@ +import { sleep } from '../helpers'; + +const backendEnv = 'prod'; +const defaultSnapshotId = '13219642453536798952'; // default for remote +const testchainUrl = 'http://18.185.172.121:4000'; +const websocketUrl = 'ws://18.185.172.121:4000/socket'; + +// const backendEnv = 'dev'; +// const defaultSnapshotId = '6925561923190355037'; // local +// const testchainUrl = process.env.TESTCHAIN_URL || 'http://localhost:4000'; +// const websocketUrl = process.env.WEBSOCKET_URL || 'ws://127.1:4000/socket'; + +const testchainConfig = { + accounts: 3, + block_mine_time: 0, + clean_on_stop: true, + description: 'DaiPluginDefaultremote1', + type: 'geth', // the restart testchain process doesn't work well with ganache + snapshot_id: defaultSnapshotId +}; +const startTestchain = async () => { + const { Client, Event } = require('@makerdao/testchain-client'); + const client = new Client(testchainUrl, websocketUrl); + + global.client = client; + await global.client.init(); + + global.client.create(testchainConfig); + const { + payload: { + response: { id } + } + } = await global.client.once('api', Event.OK); + + return id; +}; + +const setTestchainDetails = async id => { + const { + details: { + chain_details: { rpc_url } + } + } = await global.client.api.getChain(id); + + global.backendEnv = backendEnv; + global.defaultSnapshotId = defaultSnapshotId; + global.testchainPort = rpc_url.substr(rpc_url.length - 4); + global.testchainId = id; + global.rpcUrl = rpc_url.includes('.local') + ? `http://localhost:${global.testchainPort}` + : rpc_url; +}; + +beforeAll(async () => { + const id = await startTestchain(); + // sleep for 10 seconds while we wait for the chain to start up + await sleep(10000); + await setTestchainDetails(id); +}); diff --git a/packages/dai-plugin-governance/test/fixtures.js b/packages/dai-plugin-governance/test/fixtures.js new file mode 100644 index 000000000..10f7f8c51 --- /dev/null +++ b/packages/dai-plugin-governance/test/fixtures.js @@ -0,0 +1,46 @@ +export const dummyMkrSupportData = [ + { + optionId: 1, + mkrSupport: 40, + percentage: 25, + blockTimestamp: Date.now() + }, + { + optionId: 2, + mkrSupport: 120, + percentage: 75, + blockTimestamp: Date.now() + } +]; + +export const dummyAllPollsData = [ + { + creator: '0xeda95d1bdb60f901986f43459151b6d1c734b8a2', + pollId: 1, + blockCreated: 123456788, + startDate: Date.now() - 10000000, + endDate: Date.now() - 1, + multiHash: 'QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4', + url: 'https://dummyURL/1' + }, + { + creator: '0xTYa95d1bdb60f901986f43459151b6d1c734b8a2', + pollId: 2, + blockCreated: 123456789, + startDate: Date.now() - 20000000, + endDate: Date.now(), + multiHash: 'ZmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4', + url: 'https://dummyURL/2' + } +]; + +let i = 0; +export function dummyBlockNumber() { + return [8017399, 8200000][i++ % 2]; +} + +export const dummyNumUnique = 225; + +export const dummyWeight = 5.5; + +export const dummyOption = 1; diff --git a/packages/dai-plugin-governance/test/helpers/index.js b/packages/dai-plugin-governance/test/helpers/index.js new file mode 100644 index 000000000..394131341 --- /dev/null +++ b/packages/dai-plugin-governance/test/helpers/index.js @@ -0,0 +1,203 @@ +import fetch from 'node-fetch'; +import Maker from '@makerdao/dai'; +import govPlugin from '../../src/index'; +import configPlugin from '@makerdao/dai-plugin-config'; +import { createCurrency } from '@makerdao/currency'; + +const MKR = createCurrency('MKR'); + +// Until we have better event listeners from the server, we'll have to fake it with sleep +export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + +/** Feature Flag: remove this block when transition to ex_testchain is complete: */ +function ganacheAddress() { + const port = process.env.GOV_TESTNET_PORT || 2000; + return `http://localhost:${port}`; +} + +export async function takeSnapshotOriginal() { + try { + const res = await fetch(ganacheAddress(), { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'evm_snapshot', + params: [] + }) + }); + + const json = await res.json(); + return parseInt(json['result'], 16); + } catch (err) { + console.error('Request failed with:', err); + } +} + +export async function restoreSnapshotOriginal(snapId) { + try { + const res = await fetch(ganacheAddress(), { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'evm_revert', + params: [snapId] + }) + }); + + const json = await res.json(); + return json['result']; + } catch (err) { + console.error('Request failed with:', err); + } +} + +export const setupMakerOld = async () => { + const accounts = { + owner: { + type: 'privateKey', + key: '0x474beb999fed1b3af2ea048f963833c686a0fba05f5724cb6417cf3b8ee9697e' + }, + ali: { + type: 'privateKey', + key: '0xbc838ab7af00cda00cb02efbbe4dbb1ce51f5d2613acfe11bd970ce659ad8704' + }, + sam: { + type: 'privateKey', + key: '0xb3ae65f191aac33f3e3f662b8411cabf14f91f2b48cf338151d6021ea1c08541' + }, + ava: { + type: 'privateKey', + key: '0xa052332a502d9a91636931be4ffd6e1468684544a1a7bc4a64c21c6f5daa759a' + } + }; + + const maker = await Maker.create('http', { + plugins: [[govPlugin, { network: 'ganache' }]], + url: 'http://localhost:2000', + accounts, + log: false + }); + await maker.authenticate(); + return maker; +}; +/** End remove block */ + +const fetchAccounts = async () => { + const client = global.client; + const { details: chainData } = await client.api.getChain(global.testchainId); + const deployedAccounts = chainData.chain_details.accounts; + + // Find the coinbase account and put it aside + const coinbaseAccount = deployedAccounts.find( + account => account.address === chainData.chain_details.coinbase + ); + const otherAccounts = deployedAccounts.filter( + account => account !== coinbaseAccount + ); + + // Set some account names for easy reference + const accounts = ['ali', 'sam', 'ava'].reduce((result, name, i) => { + result[name] = { + type: 'privateKey', + key: otherAccounts[i].priv_key + }; + return result; + }, {}); + + // Add the coinbase account back to the accounts + accounts.owner = { type: 'privateKey', key: coinbaseAccount.priv_key }; + + return accounts; +}; + +export const takeSnapshot = async (testchainId, client, name) => { + await client.takeSnapshot(testchainId, name); + await sleep(7000); + const snapshots = await client.api.listAllSnapshots('ganache'); + const mySnap = snapshots.data.filter(x => x.description === name); + + return mySnap[0].id; +}; + +export const restoreSnapshot = async (testchainId, client, snapshotId) => { + client.restoreSnapshot(testchainId, snapshotId); + await sleep(15000); + return true; +}; + +export const deleteSnapshot = async (client, snapshotId) => { + await client.api.deleteSnapshot(snapshotId); + await sleep(10000); + return true; +}; + +export const setupTestMakerInstance = async () => { + // Remove this line when the old testchain system is fully replace + if (global.useOldChain) return setupMakerOld(); + + const accounts = await fetchAccounts(); + const maker = await Maker.create('http', { + plugins: [ + [govPlugin, { network: 'ganache' }], + [ + configPlugin, + { testchainId: global.testchainId, backendEnv: global.backendEnv } + ] + ], + url: global.rpcUrl, + accounts + }); + + await maker.authenticate(); + + return maker; +}; + +export const linkAccounts = async (maker, initiator, approver) => { + const lad = maker.currentAccount().name; + // initiator wants to create a link with approver + maker.useAccountWithAddress(initiator); + const vpsFactory = maker.service('voteProxyFactory'); + await vpsFactory.initiateLink(approver); + + // approver confirms it + maker.useAccountWithAddress(approver); + await maker.service('voteProxyFactory').approveLink(initiator); + + // no other side effects + maker.useAccount(lad); +}; + +export const sendMkrToAddress = async ( + maker, + accountToUse, + receiver, + amount +) => { + const lad = maker.currentAccount().name; + const mkr = await maker.getToken(MKR); + + await maker.useAccountWithAddress(accountToUse); + await mkr.transfer(receiver, amount); + + maker.useAccount(lad); +}; + +export const setUpAllowance = async (maker, address) => { + const lad = maker.currentAccount().name; + const mkr = await maker.getToken(MKR); + + await mkr.approveUnlimited(address); + + maker.useAccount(lad); +}; + +export const addressRegex = /^0x[a-fA-F0-9]{40}$/; diff --git a/packages/dai-plugin-governance/test/integration/govQueryApiService.test.js b/packages/dai-plugin-governance/test/integration/govQueryApiService.test.js new file mode 100644 index 000000000..06c9ba53d --- /dev/null +++ b/packages/dai-plugin-governance/test/integration/govQueryApiService.test.js @@ -0,0 +1,38 @@ +import { setupTestMakerInstance } from '../helpers'; + +let service; + +beforeAll(async () => { + const maker = await setupTestMakerInstance(); + service = maker.service('govQueryApi'); +}); + +test('get all active polls', async () => { + const polls = await service.getAllWhitelistedPolls(); + console.log('polls', polls); +}); + +test('get unique voters', async () => { + const num = await service.getNumUniqueVoters(1); + console.log('numUnique', num); +}); + +test('get mkr weight', async () => { + const weight = await service.getMkrWeight('address', 999999999); + console.log('weight', weight); +}); + +test('get current vote', async () => { + const option = await service.getOptionVotingFor('0xv', 1); + console.log('option', option); +}); + +test('get mkr weight by option', async () => { + const weights = await service.getMkrSupport(1, 999999999); + console.log('weights', weights); +}); + +test('get block number', async () => { + const num = await service.getBlockNumber(1511634513); + console.log('num', num); +}); diff --git a/packages/dai-plugin-migrations/test/migrations/SingleToMultiCdp.spec.js b/packages/dai-plugin-migrations/test/migrations/SingleToMultiCdp.spec.js index 7ce8d06a8..f1d5e5b2f 100644 --- a/packages/dai-plugin-migrations/test/migrations/SingleToMultiCdp.spec.js +++ b/packages/dai-plugin-migrations/test/migrations/SingleToMultiCdp.spec.js @@ -100,7 +100,7 @@ describe('SCD to MCD CDP Migration', () => { expect(available.toFixed('wei')).toBe('1999999999999999999'); }); - test('saiAmountNeededToBuyMkr', async () => { + test.only('saiAmountNeededToBuyMkr', async () => { await placeLimitOrder(migration._manager); const saiAmount = await migration.saiAmountNeededToBuyMkr(MKR(0.5)); expect(saiAmount).toEqual(SAI(10)); diff --git a/packages/dai/test/eth/tokens/Erc20Token.spec.js b/packages/dai/test/eth/tokens/Erc20Token.spec.js index d22e8125c..09648327f 100644 --- a/packages/dai/test/eth/tokens/Erc20Token.spec.js +++ b/packages/dai/test/eth/tokens/Erc20Token.spec.js @@ -17,6 +17,10 @@ beforeEach(() => { testAddress = TestAccountProvider.nextAddress(); }); +test('MKR total supply', async () => { + console.log(await mkr.totalSupply()); +}); + test('get ERC20 (MKR) balance of address', async () => { const balance = await mkr.balanceOf(TestAccountProvider.nextAddress()); expect(balance).toEqual(MKR(0));