diff --git a/.circleci/config.yml b/.circleci/config.yml index 6b4acc76594..26822ffa1b1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,8 +3,7 @@ version: 2 reference: - workspace: &workspace - ~/src + workspace: &workspace ~/src ## Configurations android_config: &android_config working_directory: *workspace @@ -15,7 +14,6 @@ reference: TERM: dumb JVM_OPTS: -Xmx3200m - defaults: &defaults working_directory: ~/app docker: @@ -32,6 +30,11 @@ android-defaults: &android-defaults _JAVA_OPTIONS: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap" GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-Xmx1024m -XX:+HeapDumpOnOutOfMemoryError"' +e2e-defaults: &e2e-defaults + <<: *defaults + docker: + - image: celohq/circleci + general: artifacts: - "mobile/coverage" @@ -161,7 +164,7 @@ jobs: HOMEBREW_NO_AUTO_UPDATE=1 brew cask install android-platform-tools HOMEBREW_NO_AUTO_UPDATE=1 brew tap homebrew/cask-versions HOMEBREW_NO_AUTO_UPDATE=1 brew cask install homebrew/cask-versions/adoptopenjdk8 - - run: + - run: name: Creace Android Virtual Device (AVD) command: | avdmanager create avd -n Nexus_5X_API_28_x86 -k "system-images;android-26;google_apis;x86" -g google_apis -d "Nexus 5" @@ -182,7 +185,7 @@ jobs: name: install miscellaneous command: HOMEBREW_NO_AUTO_UPDATE=1 brew install tree # Currently not used - # - run: npm install --global react-native-kill-packager + # - run: npm install --global react-native-kill-packager - run: # need to run this because it's another OS than install_dependecies job name: yarn @@ -196,7 +199,7 @@ jobs: # yarn postinstall # fi yarn - yarn build + yarn build - save_cache: key: yarn-v4-macos-{{ .Branch }}-{{ checksum "yarn.lock" }} paths: @@ -212,7 +215,7 @@ jobs: name: Start emulator command: cd ~/src/packages/mobile && bash ./scripts/start_emulator.sh background: true - - run: + - run: name: Start metro command: cd ~/src/packages/mobile && yarn start background: true @@ -237,13 +240,13 @@ jobs: paths: - ~/src/packages/mobile/android/app/build/outputs/apk/ - ~/.gradle/ - + lint-checks: <<: *defaults steps: - attach_workspace: at: ~/app - # If this fails, fix it with + # If this fails, fix it with # `./node_modules/.bin/prettier --config .prettierrc.js --write '**/*.+(ts|tsx|js|jsx)'` - run: yarn run prettify:diff - run: yarn run lint @@ -460,7 +463,7 @@ jobs: steps: - attach_workspace: at: ~/app - + - run: name: Install and test the npm package command: | @@ -475,7 +478,7 @@ jobs: steps: - attach_workspace: at: ~/app - + - run: name: Install and test the npm package command: | @@ -486,7 +489,7 @@ jobs: npm install ~/app/packages/utils/*.tgz end-to-end-geth-transfer-test: - <<: *defaults + <<: *e2e-defaults steps: - attach_workspace: at: ~/app @@ -495,37 +498,16 @@ jobs: command: | FILES_TO_CHECK="${PWD}/packages/celotool,${PWD}/packages/protocol,${PWD}/.circleci/config.yml" ./scripts/ci_check_if_test_should_run_v2.sh ${FILES_TO_CHECK} - - run: - name: Setup Go language - command: | - set -e - set -v - wget https://dl.google.com/go/go1.11.5.linux-amd64.tar.gz - tar xf go1.11.5.linux-amd64.tar.gz -C /tmp - ls /tmp/go/bin/go - /tmp/go/bin/go version - - run: - name: Setup Rust language - command: | - set -e - set -v - curl https://sh.rustup.rs -sSf | sh -s -- -y - export PATH=${PATH}:~/.cargo/bin:/tmp/go/bin - rustup install 1.36.0 - rustup default 1.36.0 - run: name: Run test no_output_timeout: 20m command: | set -e - export PATH=${PATH}:~/.cargo/bin:/tmp/go/bin - go version cd packages/celotool - mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config ./ci_test_transfers.sh checkout master end-to-end-geth-exit-test: - <<: *defaults + <<: *e2e-defaults steps: - attach_workspace: at: ~/app @@ -534,37 +516,16 @@ jobs: command: | FILES_TO_CHECK="${PWD}/packages/celotool,${PWD}/packages/protocol,${PWD}/.circleci/config.yml" ./scripts/ci_check_if_test_should_run_v2.sh ${FILES_TO_CHECK} - - run: - name: Setup Go language - command: | - set -e - set -v - wget https://dl.google.com/go/go1.11.5.linux-amd64.tar.gz - tar xf go1.11.5.linux-amd64.tar.gz -C /tmp - ls /tmp/go/bin/go - /tmp/go/bin/go version - - run: - name: Setup Rust language - command: | - set -e - set -v - curl https://sh.rustup.rs -sSf | sh -s -- -y - export PATH=${PATH}:~/.cargo/bin:/tmp/go/bin - rustup install 1.36.0 - rustup default 1.36.0 - run: name: Run test no_output_timeout: 20m command: | set -e - export PATH=${PATH}:~/.cargo/bin:/tmp/go/bin - go version cd packages/celotool - mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config ./ci_test_exit.sh checkout master end-to-end-geth-governance-test: - <<: *defaults + <<: *e2e-defaults # Source: https://circleci.com/docs/2.0/configuration-reference/#resource_class resource_class: medium+ steps: @@ -575,37 +536,16 @@ jobs: command: | FILES_TO_CHECK="${PWD}/packages/celotool,${PWD}/packages/protocol,${PWD}/.circleci/config.yml" ./scripts/ci_check_if_test_should_run_v2.sh ${FILES_TO_CHECK} - - run: - name: Setup Go language - command: | - set -e - set -v - wget https://dl.google.com/go/go1.11.5.linux-amd64.tar.gz - tar xf go1.11.5.linux-amd64.tar.gz -C /tmp - ls /tmp/go/bin/go - /tmp/go/bin/go version - - run: - name: Setup Rust language - command: | - set -e - set -v - curl https://sh.rustup.rs -sSf | sh -s -- -y - export PATH=${PATH}:~/.cargo/bin:/tmp/go/bin - rustup install 1.36.0 - rustup default 1.36.0 - run: name: Run test no_output_timeout: 20m command: | set -e - export PATH=${PATH}:~/.cargo/bin:/tmp/go/bin - go version cd packages/celotool - mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config ./ci_test_governance.sh checkout master end-to-end-geth-sync-test: - <<: *defaults + <<: *e2e-defaults # Source: https://circleci.com/docs/2.0/configuration-reference/#resource_class resource_class: medium+ steps: @@ -616,36 +556,15 @@ jobs: command: | FILES_TO_CHECK="${PWD}/packages/celotool,${PWD}/packages/protocol,${PWD}/.circleci/config.yml" ./scripts/ci_check_if_test_should_run_v2.sh ${FILES_TO_CHECK} - - run: - name: Setup Go language - command: | - set -e - set -v - wget https://dl.google.com/go/go1.11.5.linux-amd64.tar.gz - tar xf go1.11.5.linux-amd64.tar.gz -C /tmp - ls /tmp/go/bin/go - /tmp/go/bin/go version - - run: - name: Setup Rust language - command: | - set -e - set -v - curl https://sh.rustup.rs -sSf | sh -s -- -y - export PATH=${PATH}:~/.cargo/bin:/tmp/go/bin - rustup install 1.36.0 - rustup default 1.36.0 - run: name: Run test command: | set -e - export PATH=${PATH}:~/.cargo/bin:/tmp/go/bin - go version cd packages/celotool - mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config ./ci_test_sync.sh checkout master end-to-end-geth-integration-sync-test: - <<: *defaults + <<: *e2e-defaults steps: - attach_workspace: at: ~/app @@ -655,27 +574,29 @@ jobs: FILES_TO_CHECK="${PWD}/packages/celotool,${PWD}/packages/protocol,${PWD}/.circleci/config.yml" ./scripts/ci_check_if_test_should_run_v2.sh ${FILES_TO_CHECK} - run: - name: Setup Go language + name: Run test command: | set -e - set -v - wget https://dl.google.com/go/go1.11.5.linux-amd64.tar.gz - tar xf go1.11.5.linux-amd64.tar.gz -C /tmp - ls /tmp/go/bin/go - /tmp/go/bin/go version - curl https://sh.rustup.rs -sSf | sh -s -- -y - export PATH=${PATH}:~/.cargo/bin:/tmp/go/bin - rustup install 1.36.0 - rustup default 1.36.0 + cd packages/celotool + ./ci_test_sync_with_network.sh checkout master + + end-to-end-geth-attestations-test: + <<: *e2e-defaults + resource_class: medium+ + steps: + - attach_workspace: + at: ~/app + - run: + name: Check if the test should run + command: | + FILES_TO_CHECK="${PWD}/packages/celotool,${PWD}/packages/protocol,${PWD}/.circleci/config.yml" + ./scripts/ci_check_if_test_should_run_v2.sh ${FILES_TO_CHECK} - run: name: Run test command: | set -e - export PATH=${PATH}:~/.cargo/bin:/tmp/go/bin - go version cd packages/celotool - mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config - ./ci_test_sync_with_network.sh checkout master + ./ci_test_attestations.sh checkout master web: working_directory: ~/app @@ -788,23 +709,27 @@ workflows: - end-to-end-geth-transfer-test: requires: - lint-checks - - walletkit-test + - contractkit-test - end-to-end-geth-exit-test: requires: - lint-checks - - walletkit-test + - contractkit-test - end-to-end-geth-governance-test: requires: - lint-checks - - walletkit-test + - contractkit-test - end-to-end-geth-sync-test: requires: - lint-checks - - walletkit-test + - contractkit-test - end-to-end-geth-integration-sync-test: requires: - lint-checks - - walletkit-test + - contractkit-test + - end-to-end-geth-attestations-test: + requires: + - lint-checks + - contractkit-test npm-install-testing-cron-workflow: triggers: - schedule: diff --git a/.env b/.env index d7e98f29754..df79f276420 100644 --- a/.env +++ b/.env @@ -12,11 +12,13 @@ CLUSTER_DOMAIN_NAME="celo-networks-dev" TESTNET_PROJECT_NAME="celo-testnet" BLOCKSCOUT_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/blockscout" -BLOCKSCOUT_WEB_DOCKER_IMAGE_TAG="web-f6c3e0888d1d0ef72dc8bf870808702b7fd13730" -BLOCKSCOUT_INDEXER_DOCKER_IMAGE_TAG="indexer-f6c3e0888d1d0ef72dc8bf870808702b7fd13730" +BLOCKSCOUT_DOCKER_IMAGE_TAG="5fba4843b3e78b5ab75d01766214cb24c6a40649" BLOCKSCOUT_WEB_REPLICAS=3 BLOCKSCOUT_DB_SUFFIX= +ETHSTATS_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/ethstats" +ETHSTATS_DOCKER_IMAGE_TAG="i0ffe524c625ea59e4492dc92c2e638689c36e4b0" + GETH_NODE_DOCKER_IMAGE_REPOSITORY="us.gcr.io/celo-testnet/geth" # When upgrading change this to latest commit hash from the master of the geth repo # `geth $ git show | head -n 1` diff --git a/.env.integration b/.env.integration index 12da522873d..d34a1c869f4 100644 --- a/.env.integration +++ b/.env.integration @@ -11,8 +11,7 @@ CLUSTER_DOMAIN_NAME="celo-testnet" TESTNET_PROJECT_NAME="celo-testnet" BLOCKSCOUT_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/blockscout" -BLOCKSCOUT_WEB_DOCKER_IMAGE_TAG="web-f6c3e0888d1d0ef72dc8bf870808702b7fd13730" -BLOCKSCOUT_INDEXER_DOCKER_IMAGE_TAG="indexer-f6c3e0888d1d0ef72dc8bf870808702b7fd13730" +BLOCKSCOUT_DOCKER_IMAGE_TAG="ad86714d629c01272e0651dec1fb6a968c3cec71" BLOCKSCOUT_WEB_REPLICAS=3 BLOCKSCOUT_DB_SUFFIX="25" BLOCKSCOUT_SUBNETWORK_NAME="Integration" diff --git a/README-dev.md b/README-dev.md index e854714392c..97d29247eae 100644 --- a/README-dev.md +++ b/README-dev.md @@ -1,5 +1,26 @@ # README GUIDE FOR CELO DEVELOPERS +## Monorepo inter-package dependencies + +Many packages depend on other packages within the monorepo. When this happens, follow these rules: + +1. All packages must use **master version** of sibling packages. +2. Exception to (1) are packages that represent a GAE/firebase app which must use the last published version. +3. To differentiate published vs unpublished version. Master version (in package.json) must end with suffix `-dev` and should not be published. +4. If a developer want to publish a version; then after publishing it needs to set master version to next `-dev` version and change all package.json that require on it. + +To check which pakages need amending, you can run (in the root pkg): + + yarn check:packages + +A practical example: + +- In any given moment, `contractkit/package.json#version` field **must** of the form `x.y.z-dev` +- If current version of contractkit is: `0.1.6-dev` and we want to publish a new version, we should: + - publish version `0.1.6` + - change `package.json#version` to `0.1.7-dev` + - change in other packages within monorepo that were using `0.1.6-dev` to `0.1.7-dev` + ## How to publish a new npm package First checkout the alfajores branch. diff --git a/dockerfiles/circleci/Dockerfile b/dockerfiles/circleci/Dockerfile index a42dd0b3ab3..cbe05cb1ebc 100644 --- a/dockerfiles/circleci/Dockerfile +++ b/dockerfiles/circleci/Dockerfile @@ -16,4 +16,18 @@ RUN export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)" && \ curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add - && \ sudo apt-get update -y && sudo apt-get install google-cloud-sdk -y +RUN sudo wget https://dl.google.com/go/go1.11.5.linux-amd64.tar.gz && \ + sudo tar xf go1.11.5.linux-amd64.tar.gz -C /usr/local + +RUN curl https://sh.rustup.rs -sSf | sh -s -- -y + +ENV PATH="/usr/local/go/bin:/home/circleci/.cargo/bin:${PATH}" + +RUN go version + +RUN rustup install 1.36.0 && \ + rustup default 1.36.0 + +RUN mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config + CMD ["/bin/sh"] diff --git a/dockerfiles/cli/Dockerfile b/dockerfiles/cli/Dockerfile index 2bfc8f36fc3..cc65e38f3a3 100644 --- a/dockerfiles/cli/Dockerfile +++ b/dockerfiles/cli/Dockerfile @@ -49,7 +49,7 @@ RUN npm install @celo/celocli FROM node:10-alpine as final_image ARG network_name="alfajores" -ARG network_id="44782" +ARG network_id="44784" # Without musl-dev, geth will fail with a confusing "No such file or directory" error. # bash is required for start_geth.sh @@ -62,8 +62,8 @@ COPY packages/cli/start_geth.sh /celo/start_geth.sh COPY --from=geth /usr/local/bin/geth /usr/local/bin/geth COPY --from=geth /celo/genesis.json /celo COPY --from=geth /celo/static-nodes.json /celo -COPY --from=node /celo-monorepo/node_modules /celo-monorepo/node_modules +COPY --from=node /celo-monorepo/node_modules /celo-monorepo/node_modules RUN chmod ugo+x /celo/start_geth.sh && ln -s /celo-monorepo/node_modules/.bin/celocli /usr/local/bin/celocli EXPOSE 8545 8546 30303 30303/udp -ENTRYPOINT ["/celo/start_geth.sh", "/usr/local/bin/geth", "alfajores", "full", "44782", "/root/.celo", "/celo/genesis.json", "/celo/static-nodes.json"] +ENTRYPOINT ["/celo/start_geth.sh", "/usr/local/bin/geth", "alfajores", "full", "44784", "/root/.celo", "/celo/genesis.json", "/celo/static-nodes.json"] diff --git a/package.json b/package.json index c56245f2164..eec07c2a6cf 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "postinstall": "yarn run lerna run postinstall && patch-package && yarn keys:decrypt", "preinstall": "bash scripts/create_key_templates.sh", "keys:decrypt": "bash scripts/key_placer.sh decrypt", - "keys:encrypt": "bash scripts/key_placer.sh encrypt" + "keys:encrypt": "bash scripts/key_placer.sh encrypt", + "check:packages": "node ./scripts/check-packages.js" }, "husky": { "hooks": { diff --git a/packages/attestation-service/package.json b/packages/attestation-service/package.json index 39cfc583511..2fe9a73d1cb 100644 --- a/packages/attestation-service/package.json +++ b/packages/attestation-service/package.json @@ -43,7 +43,6 @@ "yargs": "13.3.0" }, "devDependencies": { - "@celo/ganache-cli": "git+https://github.com/celo-org/ganache-cli.git#98ad2ba", "@celo/protocol": "1.0.0", "@types/dotenv": "4.0.3", "@types/debug": "^4.1.5", diff --git a/packages/celotool/ci_test_attestations.sh b/packages/celotool/ci_test_attestations.sh new file mode 100755 index 00000000000..493ce855996 --- /dev/null +++ b/packages/celotool/ci_test_attestations.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +# This test starts a standalone Geth node and runs transactions on it. + +# For testing a particular branch of Geth repo (usually, on Circle CI) +# Usage: ci_test_attestations.sh checkout +# For testing the local Geth dir (usually, for manual testing) +# Usage: ci_test_attestations.sh local + +if [ "${1}" == "checkout" ]; then + # Test master by default. + BRANCH_TO_TEST=${2:-"master"} + echo "Checking out geth at branch ${BRANCH_TO_TEST}..." + ../../node_modules/.bin/mocha -r ts-node/register src/e2e-tests/attestations_tests.ts --branch ${BRANCH_TO_TEST} +elif [ "${1}" == "local" ]; then + export GETH_DIR="${2}" + echo "Testing using local geth dir ${GETH_DIR}..." + ../../node_modules/.bin/mocha -r ts-node/register src/e2e-tests/attestations_tests.ts --localgeth ${GETH_DIR} +fi diff --git a/packages/celotool/src/cmds/account/verify.ts b/packages/celotool/src/cmds/account/verify.ts index 89b044d89c4..63b16bc48d4 100644 --- a/packages/celotool/src/cmds/account/verify.ts +++ b/packages/celotool/src/cmds/account/verify.ts @@ -1,6 +1,6 @@ import { AccountArgv } from '@celo/celotool/src/cmds/account' import { portForwardAnd } from '@celo/celotool/src/lib/port_forward' -import { CeloContract, newKit } from '@celo/contractkit' +import { newKit } from '@celo/contractkit' import { AttestationsWrapper } from '@celo/contractkit/lib/wrappers/Attestations' import { ActionableAttestation, decodeAttestationCode } from '@celo/walletkit' import prompts from 'prompts' @@ -100,10 +100,10 @@ async function requestMoreAttestations( attestationsRequested: number ) { await attestations - .approveAttestationFee(CeloContract.StableToken, attestationsRequested) + .approveAttestationFee(attestationsRequested) .then((txo) => txo.sendAndWaitForReceipt()) await attestations - .request(phoneNumber, attestationsRequested, CeloContract.StableToken) + .request(phoneNumber, attestationsRequested) .then((txo) => txo.sendAndWaitForReceipt()) } diff --git a/packages/celotool/src/cmds/deploy/initial/blockchain-api.ts b/packages/celotool/src/cmds/deploy/initial/blockchain-api.ts index e94e3db1313..85e79488e1d 100644 --- a/packages/celotool/src/cmds/deploy/initial/blockchain-api.ts +++ b/packages/celotool/src/cmds/deploy/initial/blockchain-api.ts @@ -1,7 +1,9 @@ import { switchToClusterFromEnv } from 'src/lib/cluster' import { envVar, fetchEnv } from 'src/lib/env-utils' +import { AccountType, getAddressFromEnv } from 'src/lib/generate_utils' import { execCmd } from 'src/lib/utils' import { UpgradeArgv } from '../../deploy/upgrade' + export const command = 'blockchain-api' export const describe = 'command for upgrading blockchain-api' @@ -12,6 +14,14 @@ type BlockchainApiArgv = UpgradeArgv export const handler = async (argv: BlockchainApiArgv) => { await switchToClusterFromEnv() const testnetProjectName = fetchEnv(envVar.TESTNET_PROJECT_NAME) + const newFaucetAddress = getAddressFromEnv(AccountType.VALIDATOR, 0) // We use the 0th validator as the faucet + console.info(`updating blockchain-api yaml file for env ${argv.celoEnv}`) + await execCmd( + `sed -i.bak 's/FAUCET_ADDRESS: .*$/FAUCET_ADDRESS: \"${newFaucetAddress}\"/g' ../blockchain-api/app.${ + argv.celoEnv + }.yaml` + ) + await execCmd(`rm ../blockchain-api/app.${argv.celoEnv}.yaml.bak`) // Removing temporary bak file console.info(`deploying blockchain-api for env ${argv.config} to ${testnetProjectName}`) await execCmd( `yarn --cwd ../blockchain-api run deploy -p ${testnetProjectName} -n ${argv.celoEnv}` diff --git a/packages/celotool/src/cmds/deploy/initial/contracts.ts b/packages/celotool/src/cmds/deploy/initial/contracts.ts index 59b1d41f77c..e104a972fce 100644 --- a/packages/celotool/src/cmds/deploy/initial/contracts.ts +++ b/packages/celotool/src/cmds/deploy/initial/contracts.ts @@ -73,10 +73,9 @@ async function makeMetadata(testnet: string, address: string, index: number) { const fileName = `validator-${testnet}-${address}-metadata.json` const filePath = `/tmp/${fileName}` - const metadata = new IdentityMetadataWrapper(IdentityMetadataWrapper.emptyData) + const metadata = IdentityMetadataWrapper.fromEmpty() metadata.addClaim(nameClaim) metadata.addClaim(attestationServiceClaim) - writeFileSync(filePath, metadata.toString()) await uploadFileToGoogleStorage( @@ -114,7 +113,12 @@ export const handler = async (argv: InitialArgv) => { validatorKeys, }, stableToken: { - initialAccounts: getAddressesFor(AccountType.FAUCET, mnemonic, 2), + initialBalances: { + addresses: getAddressesFor(AccountType.FAUCET, mnemonic, 2), + values: getAddressesFor(AccountType.FAUCET, mnemonic, 2).map( + () => '60000000000000000000000' + ), // 60k Celo Dollars + }, }, }) diff --git a/packages/celotool/src/cmds/deploy/upgrade/blockchain-api.ts b/packages/celotool/src/cmds/deploy/upgrade/blockchain-api.ts index e94e3db1313..adc2f9b9d04 100644 --- a/packages/celotool/src/cmds/deploy/upgrade/blockchain-api.ts +++ b/packages/celotool/src/cmds/deploy/upgrade/blockchain-api.ts @@ -1,7 +1,6 @@ -import { switchToClusterFromEnv } from 'src/lib/cluster' -import { envVar, fetchEnv } from 'src/lib/env-utils' -import { execCmd } from 'src/lib/utils' +import { handler as deployInitialBlockchainApiHandler } from '../../deploy/initial/blockchain-api' import { UpgradeArgv } from '../../deploy/upgrade' + export const command = 'blockchain-api' export const describe = 'command for upgrading blockchain-api' @@ -10,11 +9,5 @@ export const describe = 'command for upgrading blockchain-api' type BlockchainApiArgv = UpgradeArgv export const handler = async (argv: BlockchainApiArgv) => { - await switchToClusterFromEnv() - const testnetProjectName = fetchEnv(envVar.TESTNET_PROJECT_NAME) - console.info(`deploying blockchain-api for env ${argv.config} to ${testnetProjectName}`) - await execCmd( - `yarn --cwd ../blockchain-api run deploy -p ${testnetProjectName} -n ${argv.celoEnv}` - ) - console.info(`blockchain-api deploy complete`) + await deployInitialBlockchainApiHandler(argv) } diff --git a/packages/celotool/src/e2e-tests/attestations_tests.ts b/packages/celotool/src/e2e-tests/attestations_tests.ts new file mode 100644 index 00000000000..f9bcf5f07d9 --- /dev/null +++ b/packages/celotool/src/e2e-tests/attestations_tests.ts @@ -0,0 +1,63 @@ +import { ContractKit, newKit } from '@celo/contractkit' +import { AttestationsWrapper } from '@celo/contractkit/lib/wrappers/Attestations' +import { assert } from 'chai' +import { getContext, GethTestConfig, sleep } from './utils' + +const validatorAddress = '0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95' +const phoneNumber = '+15555555555' + +describe('governance tests', () => { + const gethConfig: GethTestConfig = { + migrate: true, + instances: [ + { name: 'validator0', validating: true, syncmode: 'full', port: 30303, rpcport: 8545 }, + { name: 'validator1', validating: true, syncmode: 'full', port: 30305, rpcport: 8547 }, + { name: 'validator2', validating: true, syncmode: 'full', port: 30307, rpcport: 8549 }, + { name: 'validator3', validating: true, syncmode: 'full', port: 30309, rpcport: 8551 }, + { name: 'validator4', validating: true, syncmode: 'full', port: 30311, rpcport: 8553 }, + ], + } + + const context: any = getContext(gethConfig) + let contractKit: ContractKit + let Attestations: AttestationsWrapper + + before(async function(this: any) { + this.timeout(0) + await context.hooks.before() + }) + + after(context.hooks.after) + + const restart = async () => { + await context.hooks.restart() + contractKit = newKit('http://localhost:8545') + contractKit.defaultAccount = validatorAddress + + // TODO(mcortesi): magic sleep. without it unlockAccount sometimes fails + await sleep(2) + // Assuming empty password + await contractKit.web3.eth.personal.unlockAccount(validatorAddress, '', 1000000) + Attestations = await contractKit.contracts.getAttestations() + } + + describe('Attestations', () => { + before(async function() { + this.timeout(0) + await restart() + }) + + it('requests an attestation', async function(this: any) { + this.timeout(10000) + const approve = await Attestations.approveAttestationFee(2) + await approve.sendAndWaitForReceipt() + const request = await Attestations.request(phoneNumber, 2) + await request.sendAndWaitForReceipt() + + const stats = await Attestations.getAttestationStat(phoneNumber, validatorAddress) + assert.equal(stats.total, 2) + const actionable = await Attestations.getActionableAttestations(phoneNumber, validatorAddress) + assert.lengthOf(actionable, 2) + }) + }) +}) diff --git a/packages/celotool/src/e2e-tests/exit_test.ts b/packages/celotool/src/e2e-tests/exit_test.ts index 36be7785665..b0f06387942 100644 --- a/packages/celotool/src/e2e-tests/exit_test.ts +++ b/packages/celotool/src/e2e-tests/exit_test.ts @@ -1,5 +1,5 @@ import Web3 from 'web3' -import { getContractAddress, getHooks, sleep } from './utils' +import { getContractAddress, getHooks, GethTestConfig, sleep } from './utils' const blockchainParametersAbi = [ { @@ -29,9 +29,8 @@ const blockchainParametersAbi = [ describe('exit tests', function(this: any) { this.timeout(0) - const gethConfig = { + const gethConfig: GethTestConfig = { migrateTo: 15, - migrateGovernance: false, instances: [ { name: 'validator', validating: true, syncmode: 'full', port: 30303, rpcport: 8545 }, ], diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index cccd2048795..b026ba5ff24 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -1,224 +1,9 @@ +import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' +import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { assert } from 'chai' import Web3 from 'web3' -import { strip0x } from '../lib/utils' -import { - assertRevert, - erc20Abi, - getContext, - getContractAddress, - getEnode, - importGenesis, - initAndStartGeth, - sleep, -} from './utils' - -// TODO(asa): Use the contract kit here instead -const lockedGoldAbi = [ - { - constant: true, - inputs: [ - { - name: '', - type: 'uint256', - }, - ], - name: 'cumulativeRewardWeights', - outputs: [ - { - name: 'numerator', - type: 'uint256', - }, - { - name: 'denominator', - type: 'uint256', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: false, - inputs: [], - name: 'redeemRewards', - outputs: [ - { - name: '', - type: 'uint256', - }, - ], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: false, - inputs: [ - { - name: 'role', - type: 'uint8', - }, - { - name: 'delegate', - type: 'address', - }, - { - name: 'v', - type: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - }, - ], - name: 'delegateRole', - outputs: [], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, -] - -const validatorsAbi = [ - { - constant: true, - inputs: [], - name: 'getRegisteredValidatorGroups', - outputs: [ - { - name: '', - type: 'address[]', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [ - { - name: 'account', - type: 'address', - }, - ], - name: 'getValidatorGroup', - outputs: [ - { - name: '', - type: 'string', - }, - { - name: '', - type: 'string', - }, - { - name: '', - type: 'string', - }, - { - name: '', - type: 'address[]', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: false, - inputs: [ - { - name: 'validator', - type: 'address', - }, - ], - name: 'addMember', - outputs: [ - { - name: '', - type: 'bool', - }, - ], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: false, - inputs: [ - { - name: 'validator', - type: 'address', - }, - ], - name: 'removeMember', - outputs: [ - { - name: '', - type: 'bool', - }, - ], - payable: false, - stateMutability: 'nonpayable', - type: 'function', - }, - { - constant: true, - inputs: [ - { - name: 'index', - type: 'uint256', - }, - ], - name: 'validatorAddressFromCurrentSet', - outputs: [ - { - name: '', - type: 'address', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - constant: true, - inputs: [], - name: 'numberValidatorsInCurrentSet', - outputs: [ - { - name: '', - type: 'uint256', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - name: 'previousOwner', - type: 'address', - }, - { - indexed: true, - name: 'newOwner', - type: 'address', - }, - ], - name: 'OwnershipTransferred', - type: 'event', - }, -] +import { getContext, getEnode, importGenesis, initAndStartGeth, sleep } from './utils' describe('governance tests', () => { const gethConfig = { @@ -234,9 +19,11 @@ describe('governance tests', () => { const context: any = getContext(gethConfig) let web3: any - let lockedGold: any + let election: any let validators: any let goldToken: any + let registry: any + let kit: ContractKit before(async function(this: any) { this.timeout(0) @@ -248,9 +35,11 @@ describe('governance tests', () => { const restart = async () => { await context.hooks.restart() web3 = new Web3('http://localhost:8545') - lockedGold = new web3.eth.Contract(lockedGoldAbi, await getContractAddress('LockedGoldProxy')) - goldToken = new web3.eth.Contract(erc20Abi, await getContractAddress('GoldTokenProxy')) - validators = new web3.eth.Contract(validatorsAbi, await getContractAddress('ValidatorsProxy')) + kit = newKitFromWeb3(web3) + goldToken = await kit._web3Contracts.getGoldToken() + validators = await kit._web3Contracts.getValidators() + registry = await kit._web3Contracts.getRegistry() + election = await kit._web3Contracts.getElection() } const unlockAccount = async (address: string, theWeb3: any) => { @@ -258,27 +47,27 @@ describe('governance tests', () => { await theWeb3.eth.personal.unlockAccount(address, '', 1000) } - const getParsedSignatureOfAddress = async (address: string, signer: string, signerWeb3: any) => { - // @ts-ignore - const hash = signerWeb3.utils.soliditySha3({ type: 'address', value: address }) - const signature = strip0x(await signerWeb3.eth.sign(hash, signer)) - return { - r: `0x${signature.slice(0, 64)}`, - s: `0x${signature.slice(64, 128)}`, - v: signerWeb3.utils.hexToNumber(signature.slice(128, 130)), + const getValidatorGroupMembers = async (blockNumber?: number) => { + if (blockNumber) { + const [groupAddress] = await validators.methods + .getRegisteredValidatorGroups() + .call({}, blockNumber) + const groupInfo = await validators.methods + .getValidatorGroup(groupAddress) + .call({}, blockNumber) + return groupInfo[1] + } else { + const [groupAddress] = await validators.methods.getRegisteredValidatorGroups().call() + const groupInfo = await validators.methods.getValidatorGroup(groupAddress).call() + return groupInfo[1] } } - const getValidatorGroupMembers = async () => { - const [groupAddress] = await validators.methods.getRegisteredValidatorGroups().call() - const groupInfo = await validators.methods.getValidatorGroup(groupAddress).call() - return groupInfo[3] - } - const getValidatorGroupKeys = async () => { const [groupAddress] = await validators.methods.getRegisteredValidatorGroups().call() const groupInfo = await validators.methods.getValidatorGroup(groupAddress).call() - const encryptedKeystore = JSON.parse(Buffer.from(groupInfo[0], 'base64').toString()) + const encryptedKeystore64 = groupInfo[0].split(' ')[1] + const encryptedKeystore = JSON.parse(Buffer.from(encryptedKeystore64, 'base64').toString()) // The validator group ID is the validator group keystore encrypted with validator 0's // private key. // @ts-ignore @@ -287,6 +76,17 @@ describe('governance tests', () => { return [groupAddress, decryptedKeystore.privateKey] } + const activate = async (account: string, txOptions: any = {}) => { + await unlockAccount(account, web3) + const [group] = await validators.methods.getRegisteredValidatorGroups().call() + const tx = election.methods.activate(group) + let gas = txOptions.gas + if (!gas) { + gas = await tx.estimateGas({ ...txOptions }) + } + return tx.send({ from: account, ...txOptions, gas }) + } + const removeMember = async ( groupWeb3: any, group: string, @@ -312,323 +112,341 @@ describe('governance tests', () => { return tx.send({ from: group, ...txOptions, gas }) } - const delegateRewards = async (account: string, delegate: string, txOptions: any = {}) => { - const delegateWeb3 = new Web3('http://localhost:8567') - await unlockAccount(delegate, delegateWeb3) - const { r, s, v } = await getParsedSignatureOfAddress(account, delegate, delegateWeb3) - await unlockAccount(account, web3) - const rewardRole = 2 - const tx = lockedGold.methods.delegateRole(rewardRole, delegate, v, r, s) - let gas = txOptions.gas - // We overestimate to account for variations in the fraction reduction necessary to redeem - // rewards. - if (!gas) { - gas = 2 * (await tx.estimateGas({ ...txOptions })) - } - return tx.send({ from: account, ...txOptions, gas }) + const isLastBlockOfEpoch = (blockNumber: number, epochSize: number) => { + return blockNumber % epochSize === 0 } - const redeemRewards = async (account: string, txOptions: any = {}) => { - await unlockAccount(account, web3) - const tx = lockedGold.methods.redeemRewards() - let gas = txOptions.gas - // We overestimate to account for variations in the fraction reduction necessary to redeem - // rewards. - if (!gas) { - gas = 2 * (await tx.estimateGas({ ...txOptions })) - } - return tx.send({ from: account, ...txOptions, gas }) - } - - describe('Validators.numberValidatorsInCurrentSet()', () => { - before(async function() { - this.timeout(0) + describe('when the validator set is changing', () => { + let epoch: number + const blockNumbers: number[] = [] + let allValidators: string[] + before(async function(this: any) { + this.timeout(0) // Disable test timeout await restart() - validators = new web3.eth.Contract(validatorsAbi, await getContractAddress('ValidatorsProxy')) - }) + const [groupAddress, groupPrivateKey] = await getValidatorGroupKeys() + + const groupInstance = { + name: 'validatorGroup', + validating: false, + syncmode: 'full', + port: 30325, + wsport: 8567, + privateKey: groupPrivateKey.slice(2), + peers: [await getEnode(8545)], + } + await initAndStartGeth(context.hooks.gethBinaryPath, groupInstance) + allValidators = await getValidatorGroupMembers() + assert.equal(allValidators.length, 5) + epoch = new BigNumber(await validators.methods.getEpochSize().call()).toNumber() + assert.equal(epoch, 10) + + // Give the node time to sync, and time for an epoch transition so we can activate our vote. + await sleep(20) + await activate(allValidators[0]) + const groupWeb3 = new Web3('ws://localhost:8567') + const groupKit = newKitFromWeb3(groupWeb3) + validators = await groupKit._web3Contracts.getValidators() + const membersToSwap = [allValidators[0], allValidators[1]] + let includedMemberIndex = 1 + await removeMember(groupWeb3, groupAddress, membersToSwap[0]) - it('should return the validator set size', async () => { - const numberValidators = await validators.methods.numberValidatorsInCurrentSet().call() + const changeValidatorSet = async (header: any) => { + blockNumbers.push(header.number) + // At the start of epoch N, swap members so the validator set is different for epoch N + 1. + if (header.number % epoch === 1) { + const memberToRemove = membersToSwap[includedMemberIndex] + const memberToAdd = membersToSwap[(includedMemberIndex + 1) % 2] + await removeMember(groupWeb3, groupAddress, memberToRemove) + await addMember(groupWeb3, groupAddress, memberToAdd) + includedMemberIndex = (includedMemberIndex + 1) % 2 + const newMembers = await getValidatorGroupMembers() + assert.include(newMembers, memberToAdd) + assert.notInclude(newMembers, memberToRemove) + } + } - assert.equal(numberValidators, 5) + const subscription = await groupWeb3.eth.subscribe('newBlockHeaders') + subscription.on('data', changeValidatorSet) + // Wait for a few epochs while changing the validator set. + await sleep(epoch * 4) + ;(subscription as any).unsubscribe() + // Wait for the current epoch to complete. + await sleep(epoch) }) - describe('after the validator set changes', () => { - before(async function() { - this.timeout(0) - await restart() - const [groupAddress, groupPrivateKey] = await getValidatorGroupKeys() - const epoch = 10 - - const groupInstance = { - name: 'validatorGroup', - validating: false, - syncmode: 'full', - port: 30325, - wsport: 8567, - privateKey: groupPrivateKey.slice(2), - peers: [await getEnode(8545)], - } - await initAndStartGeth(context.hooks.gethBinaryPath, groupInstance) - const groupWeb3 = new Web3('ws://localhost:8567') - validators = new groupWeb3.eth.Contract( - validatorsAbi, - await getContractAddress('ValidatorsProxy') + const getValidatorSetAtBlock = async (blockNumber: number) => { + const validatorSetSize = await election.methods + .numberValidatorsInCurrentSet() + .call({}, blockNumber) + const validatorSet = [] + for (let i = 0; i < validatorSetSize; i++) { + validatorSet.push( + await election.methods.validatorAddressFromCurrentSet(i).call({}, blockNumber) ) - // Give the node time to sync. - await sleep(15) - const members = await getValidatorGroupMembers() - await removeMember(groupWeb3, groupAddress, members[0]) - await sleep(epoch * 2) - }) + } + return validatorSet + } - it('should return the reduced validator set size', async () => { - const numberValidators = await validators.methods.numberValidatorsInCurrentSet().call() + const getLastEpochBlock = (blockNumber: number) => { + const epochNumber = Math.floor((blockNumber - 1) / epoch) + return epochNumber * epoch + } - assert.equal(numberValidators, 4) - }) + it('should always return a validator set size equal to the number of group members at the end of the last epoch', async () => { + for (const blockNumber of blockNumbers) { + const lastEpochBlock = getLastEpochBlock(blockNumber) + const validatorSetSize = await election.methods + .numberValidatorsInCurrentSet() + .call({}, blockNumber) + const groupMembership = await getValidatorGroupMembers(lastEpochBlock) + assert.equal(validatorSetSize, groupMembership.length) + } }) - }) - describe('Validators.validatorAddressFromCurrentSet()', () => { - before(async function() { - this.timeout(0) - await restart() - validators = new web3.eth.Contract(validatorsAbi, await getContractAddress('ValidatorsProxy')) + it('should always return a validator set equal to the group members at the end of the last epoch', async () => { + for (const blockNumber of blockNumbers) { + const lastEpochBlock = getLastEpochBlock(blockNumber) + const groupMembership = await getValidatorGroupMembers(lastEpochBlock) + const validatorSet = await getValidatorSetAtBlock(blockNumber) + assert.sameMembers(groupMembership, validatorSet) + } }) - it('should return the first validator', async () => { - const resultAddress = await validators.methods.validatorAddressFromCurrentSet(0).call() - - assert.equal(strip0x(resultAddress), context.validators[0].address) + it('should only have created blocks whose miner was in the current validator set', async () => { + for (const blockNumber of blockNumbers) { + const validatorSet = await getValidatorSetAtBlock(blockNumber) + const block = await web3.eth.getBlock(blockNumber) + assert.include(validatorSet.map((x) => x.toLowerCase()), block.miner.toLowerCase()) + } }) - it('should return the third validator', async () => { - const resultAddress = await validators.methods.validatorAddressFromCurrentSet(2).call() + it('should update the validator scores at the end of each epoch', async () => { + const adjustmentSpeed = fromFixed( + new BigNumber((await validators.methods.getValidatorScoreParameters().call())[1]) + ) + const uptime = 1 - assert.equal(strip0x(resultAddress), context.validators[2].address) - }) + const assertScoreUnchanged = async (validator: string, blockNumber: number) => { + const score = new BigNumber( + (await validators.methods.getValidator(validator).call({}, blockNumber))[3] + ) + const previousScore = new BigNumber( + (await validators.methods.getValidator(validator).call({}, blockNumber - 1))[3] + ) + assert.isNotNaN(score) + assert.isNotNaN(previousScore) + assert.equal(score.toFixed(), previousScore.toFixed()) + } - it('should return the fifth validator', async () => { - const resultAddress = await validators.methods.validatorAddressFromCurrentSet(4).call() + const assertScoreChanged = async (validator: string, blockNumber: number) => { + const score = new BigNumber( + (await validators.methods.getValidator(validator).call({}, blockNumber))[3] + ) + const previousScore = new BigNumber( + (await validators.methods.getValidator(validator).call({}, blockNumber - 1))[3] + ) + const expectedScore = adjustmentSpeed + .times(uptime) + .plus(new BigNumber(1).minus(adjustmentSpeed).times(fromFixed(previousScore))) + assert.isNotNaN(score) + assert.isNotNaN(previousScore) + assert.equal(score.toFixed(), toFixed(expectedScore).toFixed()) + } - assert.equal(strip0x(resultAddress), context.validators[4].address) - }) + for (const blockNumber of blockNumbers) { + let expectUnchangedScores: string[] + let expectChangedScores: string[] + if (isLastBlockOfEpoch(blockNumber, epoch)) { + expectChangedScores = await getValidatorSetAtBlock(blockNumber) + expectUnchangedScores = allValidators.filter((x) => !expectChangedScores.includes(x)) + } else { + expectUnchangedScores = allValidators + expectChangedScores = [] + } - it('should revert when asked for an out of bounds validator', async function(this: any) { - this.timeout(0) // Disable test timeout - await assertRevert( - validators.methods.validatorAddressFromCurrentSet(5).send({ - from: `0x${context.validators[0].address}`, - }) - ) - }) + for (const validator of expectUnchangedScores) { + await assertScoreUnchanged(validator, blockNumber) + } - describe('after the validator set changes', () => { - before(async function() { - this.timeout(0) - await restart() - const [groupAddress, groupPrivateKey] = await getValidatorGroupKeys() - const epoch = 10 - - const groupInstance = { - name: 'validatorGroup', - validating: false, - syncmode: 'full', - port: 30325, - wsport: 8567, - privateKey: groupPrivateKey.slice(2), - peers: [await getEnode(8545)], + for (const validator of expectChangedScores) { + await assertScoreChanged(validator, blockNumber) } - await initAndStartGeth(context.hooks.gethBinaryPath, groupInstance) - const groupWeb3 = new Web3('ws://localhost:8567') - validators = new groupWeb3.eth.Contract( - validatorsAbi, - await getContractAddress('ValidatorsProxy') + } + }) + + it('should distribute epoch payments at the end of each epoch', async () => { + const stableToken = await kit._web3Contracts.getStableToken() + const commission = 0.1 + const validatorEpochPayment = new BigNumber( + await validators.methods.validatorEpochPayment().call() + ) + const [group] = await validators.methods.getRegisteredValidatorGroups().call() + + const assertBalanceChanged = async ( + validator: string, + blockNumber: number, + expected: BigNumber + ) => { + const currentBalance = new BigNumber( + await stableToken.methods.balanceOf(validator).call({}, blockNumber) ) - // Give the node time to sync. - await sleep(15) - const members = await getValidatorGroupMembers() - await removeMember(groupWeb3, groupAddress, members[0]) - await sleep(epoch * 2) - - validators = new web3.eth.Contract( - validatorsAbi, - await getContractAddress('ValidatorsProxy') + const previousBalance = new BigNumber( + await stableToken.methods.balanceOf(validator).call({}, blockNumber - 1) ) - }) + assert.isNotNaN(currentBalance) + assert.isNotNaN(previousBalance) + assert.equal(expected.toFixed(), currentBalance.minus(previousBalance).toFixed()) + } - it('should return the second validator in the first place', async () => { - const resultAddress = await validators.methods.validatorAddressFromCurrentSet(0).call() + const assertBalanceUnchanged = async (validator: string, blockNumber: number) => { + await assertBalanceChanged(validator, blockNumber, new BigNumber(0)) + } - assert.equal(strip0x(resultAddress), context.validators[1].address) - }) + const getExpectedTotalPayment = async (validator: string, blockNumber: number) => { + const score = new BigNumber( + (await validators.methods.getValidator(validator).call({}, blockNumber))[3] + ) + assert.isNotNaN(score) + return validatorEpochPayment.times(fromFixed(score)) + } - it('should return the last validator in the fourth place', async () => { - const resultAddress = await validators.methods.validatorAddressFromCurrentSet(3).call() + for (const blockNumber of blockNumbers) { + let expectUnchangedBalances: string[] + let expectChangedBalances: string[] + if (isLastBlockOfEpoch(blockNumber, epoch)) { + expectChangedBalances = await getValidatorSetAtBlock(blockNumber) + expectUnchangedBalances = allValidators.filter((x) => !expectChangedBalances.includes(x)) + } else { + expectUnchangedBalances = allValidators + expectChangedBalances = [] + } - assert.equal(strip0x(resultAddress), context.validators[4].address) - }) + for (const validator of expectUnchangedBalances) { + await assertBalanceUnchanged(validator, blockNumber) + } - it('should revert when asked for an out of bounds validator', async function(this: any) { - this.timeout(0) - await assertRevert( - validators.methods.validatorAddressFromCurrentSet(4).send({ - from: `0x${context.validators[0].address}`, - }) - ) - }) + let expectedGroupPayment = new BigNumber(0) + for (const validator of expectChangedBalances) { + const expectedTotalPayment = await getExpectedTotalPayment(validator, blockNumber) + const groupPayment = expectedTotalPayment.times(commission) + await assertBalanceChanged( + validator, + blockNumber, + expectedTotalPayment.minus(groupPayment) + ) + expectedGroupPayment = expectedGroupPayment.plus(groupPayment) + } + await assertBalanceChanged(group, blockNumber, expectedGroupPayment) + } }) - }) - describe('when the validator set is changing', () => { - const epoch = 10 - const expectedEpochMembership = new Map() - before(async function() { - this.timeout(0) - await restart() - const [groupAddress, groupPrivateKey] = await getValidatorGroupKeys() + it('should distribute epoch rewards at the end of each epoch', async () => { + const lockedGold = await kit._web3Contracts.getLockedGold() + const governance = await kit._web3Contracts.getGovernance() + const epochReward = new BigNumber(10).pow(18) + const infraReward = new BigNumber(10).pow(18) + const [group] = await validators.methods.getRegisteredValidatorGroups().call() - const groupInstance = { - name: 'validatorGroup', - validating: false, - syncmode: 'full', - port: 30325, - wsport: 8567, - privateKey: groupPrivateKey.slice(2), - peers: [await getEnode(8545)], + const assertVotesChanged = async (blockNumber: number, expected: BigNumber) => { + const currentVotes = new BigNumber( + await election.methods.getTotalVotesForGroup(group).call({}, blockNumber) + ) + const previousVotes = new BigNumber( + await election.methods.getTotalVotesForGroup(group).call({}, blockNumber - 1) + ) + assert.equal(expected.toFixed(), currentVotes.minus(previousVotes).toFixed()) } - await initAndStartGeth(context.hooks.gethBinaryPath, groupInstance) - const groupWeb3 = new Web3('ws://localhost:8567') - validators = new groupWeb3.eth.Contract( - validatorsAbi, - await getContractAddress('ValidatorsProxy') - ) - // Give the node time to sync. - await sleep(15) - const members = await getValidatorGroupMembers() - const membersToSwap = [members[0], members[1]] - // Start with 10 nodes - await removeMember(groupWeb3, groupAddress, membersToSwap[0]) - const changeValidatorSet = async (header: any) => { - // At the start of epoch N, swap members so the validator set is different for epoch N + 1. - if (header.number % epoch === 0) { - const groupMembers = await getValidatorGroupMembers() - const direction = groupMembers.includes(membersToSwap[0]) - const removedMember = direction ? membersToSwap[0] : membersToSwap[1] - const addedMember = direction ? membersToSwap[1] : membersToSwap[0] - expectedEpochMembership.set(header.number / epoch, [removedMember, addedMember]) - await removeMember(groupWeb3, groupAddress, removedMember) - await addMember(groupWeb3, groupAddress, addedMember) - const newMembers = await getValidatorGroupMembers() - assert.include(newMembers, addedMember) - assert.notInclude(newMembers, removedMember) - } + const assertGoldTokenTotalSupplyChanged = async ( + blockNumber: number, + expected: BigNumber + ) => { + const currentSupply = new BigNumber( + await goldToken.methods.totalSupply().call({}, blockNumber) + ) + const previousSupply = new BigNumber( + await goldToken.methods.totalSupply().call({}, blockNumber - 1) + ) + assert.equal(expected.toFixed(), currentSupply.minus(previousSupply).toFixed()) } - const subscription = await groupWeb3.eth.subscribe('newBlockHeaders') - subscription.on('data', changeValidatorSet) - // Wait for a few epochs while changing the validator set. - await sleep(epoch * 3) - ;(subscription as any).unsubscribe() - // Wait for the current epoch to complete. - await sleep(epoch) - }) + const assertBalanceChanged = async ( + address: string, + blockNumber: number, + expected: BigNumber + ) => { + const currentBalance = new BigNumber( + await goldToken.methods.balanceOf(address).call({}, blockNumber) + ) + const previousBalance = new BigNumber( + await goldToken.methods.balanceOf(address).call({}, blockNumber - 1) + ) + assert.equal(expected.toFixed(), currentBalance.minus(previousBalance).toFixed()) + } - it('should have produced blocks with the correct validator set', async function(this: any) { - this.timeout(0) // Disable test timeout - assert.equal(expectedEpochMembership.size, 3) - // tslint:disable-next-line: no-console - console.log(expectedEpochMembership) - for (const [epochNumber, membership] of expectedEpochMembership) { - let containsExpectedMember = false - for (let i = epochNumber * epoch + 1; i < (epochNumber + 1) * epoch + 1; i++) { - const block = await web3.eth.getBlock(i) - assert.notEqual(block.miner.toLowerCase(), membership[1].toLowerCase()) - containsExpectedMember = - containsExpectedMember || block.miner.toLowerCase() === membership[0].toLowerCase() - } - assert.isTrue(containsExpectedMember) + const assertLockedGoldBalanceChanged = async (blockNumber: number, expected: BigNumber) => { + await assertBalanceChanged(lockedGold.options.address, blockNumber, expected) + } + + const assertGovernanceBalanceChanged = async (blockNumber: number, expected: BigNumber) => { + await assertBalanceChanged(governance.options.address, blockNumber, expected) } - }) - }) - describe('when a Locked Gold account with weight exists', () => { - const account = '0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95' - const delegate = '0x5409ed021d9299bf6814279a6a1411a7e866a631' + const assertVotesUnchanged = async (blockNumber: number) => { + await assertVotesChanged(blockNumber, new BigNumber(0)) + } - before(async function() { - this.timeout(0) - await restart() - const delegateInstance = { - name: 'delegate', - validating: false, - syncmode: 'full', - port: 30325, - rpcport: 8567, - privateKey: 'f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d', + const assertGoldTokenTotalSupplyUnchanged = async (blockNumber: number) => { + await assertGoldTokenTotalSupplyChanged(blockNumber, new BigNumber(0)) } - await initAndStartGeth(context.hooks.gethBinaryPath, delegateInstance) - // Note that we don't need to create an account or make a commitment as this has already been - // done in the migration. - await delegateRewards(account, delegate) - }) - it.skip('should be able to redeem block rewards', async function(this: any) { - this.timeout(0) // Disable test timeout - await sleep(1) - await redeemRewards(account) - assert.isAtLeast(await web3.eth.getBalance(delegate), 1) + const assertLockedGoldBalanceUnchanged = async (blockNumber: number) => { + await assertLockedGoldBalanceChanged(blockNumber, new BigNumber(0)) + } + + const assertGovernanceBalanceUnchanged = async (blockNumber: number) => { + await assertGovernanceBalanceChanged(blockNumber, new BigNumber(0)) + } + + for (const blockNumber of blockNumbers) { + if (isLastBlockOfEpoch(blockNumber, epoch)) { + await assertVotesChanged(blockNumber, epochReward) + await assertGoldTokenTotalSupplyChanged(blockNumber, epochReward.plus(infraReward)) + await assertLockedGoldBalanceChanged(blockNumber, epochReward) + await assertGovernanceBalanceChanged(blockNumber, infraReward) + } else { + await assertVotesUnchanged(blockNumber) + await assertGoldTokenTotalSupplyUnchanged(blockNumber) + await assertLockedGoldBalanceUnchanged(blockNumber) + await assertGovernanceBalanceUnchanged(blockNumber) + } + } }) }) - describe('when adding any block', () => { - let goldGenesisSupply: any - const addressesWithBalance: string[] = [] + describe('after the gold token smart contract is registered', () => { + let goldGenesisSupply = new BigNumber(0) beforeEach(async function(this: any) { this.timeout(0) // Disable test timeout await restart() const genesis = await importGenesis() - goldGenesisSupply = new BigNumber(0) - Object.keys(genesis.alloc).forEach((validator) => { - addressesWithBalance.push(validator) - goldGenesisSupply = goldGenesisSupply.plus(genesis.alloc[validator].balance) + Object.keys(genesis.alloc).forEach((address) => { + goldGenesisSupply = goldGenesisSupply.plus(genesis.alloc[address].balance) }) - // Block rewards are paid to governance and Locked Gold. - // Governance also receives a portion of transaction fees. - addressesWithBalance.push(await getContractAddress('GovernanceProxy')) - addressesWithBalance.push(await getContractAddress('LockedGoldProxy')) - // Some gold is sent to the reserve and exchange during migrations. - addressesWithBalance.push(await getContractAddress('ReserveProxy')) - addressesWithBalance.push(await getContractAddress('ExchangeProxy')) }) - it('should update the Celo Gold total supply correctly', async function(this: any) { - // To register a validator group, we send gold to a new address not included in - // `addressesWithBalance`. Therefore, we check the gold total supply at a block before - // that gold is sent. - // We don't set the total supply until block rewards are paid out, which can happen once - // either LockedGold or Governance are registered. - const _validators = new web3.eth.Contract( - validatorsAbi, - await getContractAddress('ValidatorsProxy') - ) - const events = await _validators.getPastEvents('OwnershipTransferred', { fromBlock: 0 }) - - const blockNumber = events[events.length - 1].blockNumber + 1 + it('should initialize the Celo Gold total supply correctly', async function(this: any) { + const events = await registry.getPastEvents('RegistryUpdated', { fromBlock: 0 }) + let blockNumber = 0 + for (const e of events) { + if (e.returnValues.identifier === 'GoldToken') { + blockNumber = e.blockNumber + break + } + } + assert.isAtLeast(blockNumber, 1) const goldTotalSupply = await goldToken.methods.totalSupply().call({}, blockNumber) - const balances = await Promise.all( - addressesWithBalance.map( - async (a: string) => new BigNumber(await web3.eth.getBalance(a, blockNumber)) - ) - ) - const expectedGoldTotalSupply = balances.reduce((total: BigNumber, b: BigNumber) => - b.plus(total) - ) - assert.isAtLeast(expectedGoldTotalSupply.toNumber(), goldGenesisSupply.toNumber()) - // - assert.equal(goldTotalSupply.toString(), expectedGoldTotalSupply.toString()) + assert.equal(goldTotalSupply, goldGenesisSupply.toFixed()) }) }) }) diff --git a/packages/celotool/src/e2e-tests/transfer_tests.ts b/packages/celotool/src/e2e-tests/transfer_tests.ts index 3d22ddef327..0c24c64a303 100644 --- a/packages/celotool/src/e2e-tests/transfer_tests.ts +++ b/packages/celotool/src/e2e-tests/transfer_tests.ts @@ -12,6 +12,7 @@ import { getEnode, GethInstanceConfig, getHooks, + GethTestConfig, initAndStartGeth, killInstance, sleep, @@ -170,9 +171,8 @@ describe('Transfer tests', function(this: any) { const FeeRecipientAddress = '0x4f5f8a3f45d179553e7b95119ce296010f50f6f1' const syncModes = ['full', 'fast', 'light', 'ultralight'] - const gethConfig = { + const gethConfig: GethTestConfig = { migrateTo: 8, - migrateGovernance: false, instances: [ { name: 'validator', validating: true, syncmode: 'full', port: 30303, rpcport: 8545 }, ], diff --git a/packages/celotool/src/e2e-tests/utils.ts b/packages/celotool/src/e2e-tests/utils.ts index 71a563d783b..cbdd3af3809 100644 --- a/packages/celotool/src/e2e-tests/utils.ts +++ b/packages/celotool/src/e2e-tests/utils.ts @@ -318,12 +318,24 @@ export async function startGeth(gethBinaryPath: string, instance: GethInstanceCo return instance } -export async function migrateContracts(validatorPrivateKeys: string[], to: number = 1000) { +export async function migrateContracts( + validatorPrivateKeys: string[], + validators: string[], + to: number = 1000 +) { const migrationOverrides = { validators: { - minElectableValidators: '1', validatorKeys: validatorPrivateKeys.map(ensure0x), }, + election: { + minElectableValidators: '1', + }, + stableToken: { + initialBalances: { + addresses: validators.map(ensure0x), + values: validators.map(() => '10000000000000000000000'), + }, + }, } const args = [ '--cwd', @@ -423,7 +435,11 @@ export function getContext(gethConfig: GethTestConfig) { await initAndStartGeth(gethBinaryPath, instance) } if (gethConfig.migrate || gethConfig.migrateTo) { - await migrateContracts(validatorPrivateKeys, gethConfig.migrateTo) + await migrateContracts( + validatorPrivateKeys, + validators.map((x) => x.address), + gethConfig.migrateTo + ) } await killGeth() await sleep(2) diff --git a/packages/celotool/src/lib/blockscout.ts b/packages/celotool/src/lib/blockscout.ts index 97d9fe9a314..ab5b4772b07 100644 --- a/packages/celotool/src/lib/blockscout.ts +++ b/packages/celotool/src/lib/blockscout.ts @@ -59,8 +59,7 @@ async function helmParameters( const params = [ `--set domain.name=${fetchEnv('CLUSTER_DOMAIN_NAME')}`, `--set blockscout.image.repository=${fetchEnv('BLOCKSCOUT_DOCKER_IMAGE_REPOSITORY')}`, - `--set blockscout.image.webTag=${fetchEnv('BLOCKSCOUT_WEB_DOCKER_IMAGE_TAG')}`, - `--set blockscout.image.indexerTag=${fetchEnv('BLOCKSCOUT_INDEXER_DOCKER_IMAGE_TAG')}`, + `--set blockscout.image.tag=${fetchEnv('BLOCKSCOUT_DOCKER_IMAGE_TAG')}`, `--set blockscout.db.username=${blockscoutDBUsername}`, `--set blockscout.db.password=${blockscoutDBPassword}`, `--set blockscout.db.connection_name=${blockscoutDBConnectionName.trim()}`, diff --git a/packages/celotool/src/lib/env-utils.ts b/packages/celotool/src/lib/env-utils.ts index 4820605f6c7..ed4bbd0a2ba 100644 --- a/packages/celotool/src/lib/env-utils.ts +++ b/packages/celotool/src/lib/env-utils.ts @@ -29,6 +29,8 @@ export enum envVar { CLUSTER_DOMAIN_NAME = 'CLUSTER_DOMAIN_NAME', ENV_TYPE = 'ENV_TYPE', EPOCH = 'EPOCH', + ETHSTATS_DOCKER_IMAGE_REPOSITORY = 'ETHSTATS_DOCKER_IMAGE_REPOSITORY', + ETHSTATS_DOCKER_IMAGE_TAG = 'ETHSTATS_DOCKER_IMAGE_TAG', ETHSTATS_WEBSOCKETSECRET = 'ETHSTATS_WEBSOCKETSECRET', GETH_ACCOUNT_SECRET = 'GETH_ACCOUNT_SECRET', GETH_BOOTNODE_DOCKER_IMAGE_REPOSITORY = 'GETH_BOOTNODE_DOCKER_IMAGE_REPOSITORY', diff --git a/packages/celotool/src/lib/ethstats.ts b/packages/celotool/src/lib/ethstats.ts index d186f8760e8..370576dd6dd 100644 --- a/packages/celotool/src/lib/ethstats.ts +++ b/packages/celotool/src/lib/ethstats.ts @@ -31,6 +31,8 @@ function helmParameters() { `--set domain.name=${fetchEnv(envVar.CLUSTER_DOMAIN_NAME)}`, `--set ethstats.createSecret=${isVmBased()}`, `--set ethstats.webSocketSecret="${fetchEnv(envVar.ETHSTATS_WEBSOCKETSECRET)}"`, + `--set ethstats.image.repository=${fetchEnv(envVar.ETHSTATS_DOCKER_IMAGE_REPOSITORY)}`, + `--set ethstats.image.tag=${fetchEnv(envVar.ETHSTATS_DOCKER_IMAGE_TAG)}`, ] } diff --git a/packages/celotool/src/lib/helm_deploy.ts b/packages/celotool/src/lib/helm_deploy.ts index dca0aa731c8..ff180d03774 100644 --- a/packages/celotool/src/lib/helm_deploy.ts +++ b/packages/celotool/src/lib/helm_deploy.ts @@ -264,10 +264,10 @@ export async function grantRoles( export async function retrieveCloudSQLConnectionInfo(celoEnv: string, instanceName: string) { await validateExistingCloudSQLInstance(instanceName) const [blockscoutDBUsername] = await execCmdWithExitOnFailure( - `kubectl get secret ${celoEnv}-blockscout --export -o jsonpath='{.data.DB_USERNAME}' -n ${celoEnv} | base64 --decode` + `kubectl get secret ${celoEnv}-blockscout --export -o jsonpath='{.data.DATABASE_USER}' -n ${celoEnv} | base64 --decode` ) const [blockscoutDBPassword] = await execCmdWithExitOnFailure( - `kubectl get secret ${celoEnv}-blockscout --export -o jsonpath='{.data.DB_PASSWORD}' -n ${celoEnv} | base64 --decode` + `kubectl get secret ${celoEnv}-blockscout --export -o jsonpath='{.data.DATABASE_PASSWORD}' -n ${celoEnv} | base64 --decode` ) const [blockscoutDBConnectionName] = await execCmdWithExitOnFailure( `gcloud sql instances describe ${instanceName} --format="value(connectionName)"` diff --git a/packages/cli/package.json b/packages/cli/package.json index 21b253a6243..11a52986e43 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@celo/celocli", "description": "CLI Tool for transacting with the Celo protocol", - "version": "0.0.18", + "version": "0.0.27", "author": "Celo", "license": "Apache-2.0", "repository": "celo-org/celo-monorepo", @@ -29,12 +29,11 @@ "test": "TZ=UTC jest" }, "dependencies": { + "@celo/contractkit": "^0.1.6", "@celo/utils": "^0.1.0", - "@celo/contractkit": "^0.1.1", "@oclif/command": "^1", "@oclif/config": "^1", "@oclif/plugin-help": "^2", - "ethereumjs-util": "^5.2.0", "bip32": "^1.0.2", "bip39": "^2.5.0", "chalk": "^2.4.2", @@ -42,6 +41,7 @@ "cli-ux": "^5.3.1", "debug": "^4.1.1", "elliptic": "^6.4.1", + "ethereumjs-util": "^5.2.0", "events": "^3.0.0", "firebase": "^6.2.4", "fs-extra": "^8.1.0", @@ -54,11 +54,11 @@ "@celo/dev-cli": "^2.0.3", "@types/bip32": "^1.0.1", "@types/bip39": "^2.4.2", - "@types/elliptic": "^6.4.9", - "@types/mocha": "^5.2.7", "@types/cli-table": "^0.3.0", "@types/debug": "^4.1.4", + "@types/elliptic": "^6.4.9", "@types/fs-extra": "^8.0.0", + "@types/mocha": "^5.2.7", "@types/node": "^10", "@types/web3": "^1.0.18", "globby": "^8", @@ -80,20 +80,23 @@ "config": { "description": "Configure CLI options which persist across commands" }, + "election": { + "description": "View and manage validator elections" + }, "exchange": { "description": "Commands for interacting with the Exchange" }, "lockedgold": { - "description": "Manage Locked Gold to participate in governance and earn rewards" + "description": "View and manage locked Celo Gold" }, "node": { "description": "Manage your full node" }, "validator": { - "description": "View validator information and register your own" + "description": "View and manage validators" }, "validatorgroup": { - "description": "View validator group information and cast votes" + "description": "View and manage validator groups" } }, "bin": "celocli", diff --git a/packages/cli/src/commands/account/isvalidator.ts b/packages/cli/src/commands/account/isvalidator.ts index 9a6738df870..316514dfa8e 100644 --- a/packages/cli/src/commands/account/isvalidator.ts +++ b/packages/cli/src/commands/account/isvalidator.ts @@ -17,11 +17,11 @@ export default class IsValidator extends BaseCommand { async run() { const { args } = this.parse(IsValidator) - const validators = await this.kit.contracts.getValidators() - const numberValidators = await validators.numberValidatorsInCurrentSet() + const election = await this.kit.contracts.getElection() + const numberValidators = await election.numberValidatorsInCurrentSet() for (let i = 0; i < numberValidators; i++) { - const validatorAddress = await validators.validatorAddressFromCurrentSet(i) + const validatorAddress = await election.validatorAddressFromCurrentSet(i) if (eqAddress(validatorAddress, args.address)) { console.log(`${args.address} is in the current validator set`) return diff --git a/packages/cli/src/commands/validatorset.ts b/packages/cli/src/commands/election/validatorset.ts similarity index 63% rename from packages/cli/src/commands/validatorset.ts rename to packages/cli/src/commands/election/validatorset.ts index 37ee81e5d76..74f56f97874 100644 --- a/packages/cli/src/commands/validatorset.ts +++ b/packages/cli/src/commands/election/validatorset.ts @@ -1,4 +1,4 @@ -import { BaseCommand } from '../base' +import { BaseCommand } from '../../base' export default class ValidatorSet extends BaseCommand { static description = 'Outputs the current validator set' @@ -10,8 +10,8 @@ export default class ValidatorSet extends BaseCommand { static examples = ['validatorset'] async run() { - const validators = await this.kit.contracts.getValidators() - const validatorSet = await validators.getValidatorSetAddresses() + const election = await this.kit.contracts.getElection() + const validatorSet = await election.getValidatorSetAddresses() validatorSet.forEach((validator: string) => console.log(validator)) } diff --git a/packages/cli/src/commands/election/vote.ts b/packages/cli/src/commands/election/vote.ts new file mode 100644 index 00000000000..cf957d74755 --- /dev/null +++ b/packages/cli/src/commands/election/vote.ts @@ -0,0 +1,31 @@ +import { flags } from '@oclif/command' +import BigNumber from 'bignumber.js' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class ElectionVote extends BaseCommand { + static description = 'Vote for a Validator Group in validator elections.' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: "Voter's address" }), + for: Flags.address({ + description: "Set vote for ValidatorGroup's address", + required: true, + }), + value: flags.string({ description: 'Amount of Gold used to vote for group', required: true }), + } + + static examples = [ + 'vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --for 0x932fee04521f5fcb21949041bf161917da3f588b, --value 1000000', + ] + async run() { + const res = this.parse(ElectionVote) + + this.kit.defaultAccount = res.flags.from + const election = await this.kit.contracts.getElection() + const tx = await election.vote(res.flags.for, new BigNumber(res.flags.value)) + await displaySendTx('vote', tx) + } +} diff --git a/packages/cli/src/commands/identity/create-metadata.ts b/packages/cli/src/commands/identity/create-metadata.ts index 0e3543e248d..fad6f339d14 100644 --- a/packages/cli/src/commands/identity/create-metadata.ts +++ b/packages/cli/src/commands/identity/create-metadata.ts @@ -19,7 +19,7 @@ export default class CreateMetadata extends BaseCommand { async run() { const { args } = this.parse(CreateMetadata) - const metadata = new IdentityMetadataWrapper(IdentityMetadataWrapper.emptyData) + const metadata = new IdentityMetadataWrapper(IdentityMetadataWrapper.fromEmpty()) writeFileSync(args.file, metadata.toString()) } } diff --git a/packages/cli/src/commands/lockedgold/authorize.ts b/packages/cli/src/commands/lockedgold/authorize.ts new file mode 100644 index 00000000000..aae72011553 --- /dev/null +++ b/packages/cli/src/commands/lockedgold/authorize.ts @@ -0,0 +1,52 @@ +import { flags } from '@oclif/command' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class Authorize extends BaseCommand { + static description = 'Authorize validating or voting address for a Locked Gold account' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true }), + role: flags.string({ + char: 'r', + options: ['voter', 'validator'], + description: 'Role to delegate', + }), + to: Flags.address({ required: true }), + } + + static args = [] + + static examples = [ + 'authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role voter --to 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', + ] + + async run() { + const res = this.parse(Authorize) + + if (!res.flags.role) { + this.error(`Specify role with --role`) + return + } + + if (!res.flags.to) { + this.error(`Specify authorized address with --to`) + return + } + + this.kit.defaultAccount = res.flags.from + const lockedGold = await this.kit.contracts.getLockedGold() + let tx: any + if (res.flags.role === 'voter') { + tx = await lockedGold.authorizeVoter(res.flags.from, res.flags.to) + } else if (res.flags.role === 'validator') { + tx = await lockedGold.authorizeValidator(res.flags.from, res.flags.to) + } else { + this.error(`Invalid role provided`) + return + } + await displaySendTx('authorizeTx', tx) + } +} diff --git a/packages/cli/src/commands/lockedgold/delegate.ts b/packages/cli/src/commands/lockedgold/delegate.ts deleted file mode 100644 index 9b1d39174f7..00000000000 --- a/packages/cli/src/commands/lockedgold/delegate.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Roles } from '@celo/contractkit' -import { flags } from '@oclif/command' -import { BaseCommand } from '../../base' -import { displaySendTx } from '../../utils/cli' -import { Flags } from '../../utils/command' - -export default class Delegate extends BaseCommand { - static description = 'Delegate validating, voting and reward roles for Locked Gold account' - - static flags = { - ...BaseCommand.flags, - from: Flags.address({ required: true }), - role: flags.string({ - char: 'r', - options: Object.keys(Roles), - description: 'Role to delegate', - }), - to: Flags.address({ required: true }), - } - - static args = [] - - static examples = [ - 'delegate --from=0x5409ED021D9299bf6814279A6A1411A7e866A631 --role Voting --to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', - ] - - async run() { - const res = this.parse(Delegate) - - if (!res.flags.role) { - this.error(`Specify role with --role`) - return - } - - if (!res.flags.to) { - this.error(`Specify delegate address with --to`) - return - } - - this.kit.defaultAccount = res.flags.from - const lockedGold = await this.kit.contracts.getLockedGold() - const tx = await lockedGold.delegateRoleTx( - res.flags.from, - res.flags.to, - Roles[res.flags.role as keyof typeof Roles] - ) - await displaySendTx('delegateRoleTx', tx) - } -} diff --git a/packages/cli/src/commands/lockedgold/list.ts b/packages/cli/src/commands/lockedgold/list.ts deleted file mode 100644 index 12da130666d..00000000000 --- a/packages/cli/src/commands/lockedgold/list.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Roles } from '@celo/contractkit' -import chalk from 'chalk' -import { cli } from 'cli-ux' -import { BaseCommand } from '../../base' -import { Args } from '../../utils/command' - -export default class List extends BaseCommand { - static description = "View information about all of the account's commitments" - - static flags = { - ...BaseCommand.flags, - } - - static args = [Args.address('account')] - - static examples = ['list 0x5409ed021d9299bf6814279a6a1411a7e866a631'] - - async run() { - const { args } = this.parse(List) - cli.action.start('Fetching commitments and delegates...') - const lockedGold = await this.kit.contracts.getLockedGold() - const commitments = await lockedGold.getCommitments(args.account) - const delegates = await Promise.all( - Object.keys(Roles).map(async (role: string) => ({ - role: role, - address: await lockedGold.getDelegateFromAccountAndRole( - args.account, - Roles[role as keyof typeof Roles] - ), - })) - ) - cli.action.stop() - - cli.table(delegates, { - role: { header: 'Role', get: (a) => a.role }, - delegate: { get: (a) => a.address }, - }) - - cli.log(chalk.bold.yellow('Total Gold Locked \t') + commitments.total.gold) - cli.log(chalk.bold.red('Total Account Weight \t') + commitments.total.weight) - if (commitments.locked.length > 0) { - cli.table(commitments.locked, { - noticePeriod: { header: 'NoticePeriod', get: (a) => a.time.toString() }, - value: { get: (a) => a.value.toString() }, - }) - } - if (commitments.notified.length > 0) { - cli.table(commitments.notified, { - availabilityTime: { header: 'AvailabilityTime', get: (a) => a.time.toString() }, - value: { get: (a) => a.value.toString() }, - }) - } - } -} diff --git a/packages/cli/src/commands/lockedgold/lock.ts b/packages/cli/src/commands/lockedgold/lock.ts new file mode 100644 index 00000000000..d9f59f2bcaf --- /dev/null +++ b/packages/cli/src/commands/lockedgold/lock.ts @@ -0,0 +1,40 @@ +import { Address } from '@celo/utils/lib/address' +import { flags } from '@oclif/command' +import BigNumber from 'bignumber.js' +import { BaseCommand } from '../../base' +import { displaySendTx, failWith } from '../../utils/cli' +import { Flags } from '../../utils/command' +import { LockedGoldArgs } from '../../utils/lockedgold' + +export default class Lock extends BaseCommand { + static description = 'Locks Celo Gold to be used in governance and validator elections.' + + static flags = { + ...BaseCommand.flags, + from: flags.string({ ...Flags.address, required: true }), + value: flags.string({ ...LockedGoldArgs.valueArg, required: true }), + } + + static args = [] + + static examples = [ + 'lock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --value 1000000000000000000', + ] + + async run() { + const res = this.parse(Lock) + const address: Address = res.flags.from + + this.kit.defaultAccount = address + const lockedGold = await this.kit.contracts.getLockedGold() + + const value = new BigNumber(res.flags.value) + + if (!value.gt(new BigNumber(0))) { + failWith(`Provided value must be greater than zero => [${value.toString()}]`) + } + + const tx = lockedGold.lock() + await displaySendTx('lock', tx, { value: value.toString() }) + } +} diff --git a/packages/cli/src/commands/lockedgold/lockup.ts b/packages/cli/src/commands/lockedgold/lockup.ts deleted file mode 100644 index 18c689a2fc2..00000000000 --- a/packages/cli/src/commands/lockedgold/lockup.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Address } from '@celo/utils/lib/address' -import { flags } from '@oclif/command' -import BigNumber from 'bignumber.js' -import { BaseCommand } from '../../base' -import { displaySendTx, failWith } from '../../utils/cli' -import { Flags } from '../../utils/command' -import { LockedGoldArgs } from '../../utils/lockedgold' - -export default class Commitment extends BaseCommand { - static description = 'Create a Locked Gold commitment given notice period and gold amount' - - static flags = { - ...BaseCommand.flags, - from: flags.string({ ...Flags.address, required: true }), - noticePeriod: flags.string({ ...LockedGoldArgs.noticePeriodArg, required: true }), - goldAmount: flags.string({ ...LockedGoldArgs.goldAmountArg, required: true }), - } - - static args = [] - - static examples = [ - 'lockup --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --noticePeriod 8640 --goldAmount 1000000000000000000', - ] - - async run() { - const res = this.parse(Commitment) - const address: Address = res.flags.from - - this.kit.defaultAccount = address - const lockedGold = await this.kit.contracts.getLockedGold() - - const noticePeriod = new BigNumber(res.flags.noticePeriod) - const goldAmount = new BigNumber(res.flags.goldAmount) - - if (!(await lockedGold.isVoting(address))) { - failWith(`require(!isVoting(address)) => false`) - } - - const maxNoticePeriod = await lockedGold.maxNoticePeriod() - if (!maxNoticePeriod.gte(noticePeriod)) { - failWith(`require(noticePeriod <= maxNoticePeriod) => [${noticePeriod}, ${maxNoticePeriod}]`) - } - if (!goldAmount.gt(new BigNumber(0))) { - failWith(`require(goldAmount > 0) => [${goldAmount}]`) - } - - // await displaySendTx('redeemRewards', lockedGold.methods.redeemRewards()) - const tx = lockedGold.newCommitment(noticePeriod.toString()) - await displaySendTx('lockup', tx, { value: goldAmount.toString() }) - } -} diff --git a/packages/cli/src/commands/lockedgold/notify.ts b/packages/cli/src/commands/lockedgold/notify.ts deleted file mode 100644 index 192a40e5818..00000000000 --- a/packages/cli/src/commands/lockedgold/notify.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { flags } from '@oclif/command' -import { BaseCommand } from '../../base' -import { displaySendTx } from '../../utils/cli' -import { Flags } from '../../utils/command' -import { LockedGoldArgs } from '../../utils/lockedgold' - -export default class Notify extends BaseCommand { - static description = 'Notify a Locked Gold commitment given notice period and gold amount' - - static flags = { - ...BaseCommand.flags, - from: Flags.address({ required: true }), - noticePeriod: flags.string({ ...LockedGoldArgs.noticePeriodArg, required: true }), - goldAmount: flags.string({ ...LockedGoldArgs.goldAmountArg, required: true }), - } - - static args = [] - - static examples = ['notify --noticePeriod=3600 --goldAmount=500'] - - async run() { - const res = this.parse(Notify) - this.kit.defaultAccount = res.flags.from - const lockedgold = await this.kit.contracts.getLockedGold() - await displaySendTx( - 'notifyCommitment', - lockedgold.notifyCommitment(res.flags.goldAmount, res.flags.noticePeriod) - ) - } -} diff --git a/packages/cli/src/commands/lockedgold/rewards.ts b/packages/cli/src/commands/lockedgold/rewards.ts deleted file mode 100644 index 7485f27a87e..00000000000 --- a/packages/cli/src/commands/lockedgold/rewards.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { flags } from '@oclif/command' -import { BaseCommand } from '../../base' -import { displaySendTx } from '../../utils/cli' -import { Flags } from '../../utils/command' - -export default class Rewards extends BaseCommand { - static description = 'Manage rewards for Locked Gold account' - - static flags = { - ...BaseCommand.flags, - from: Flags.address({ required: true }), - redeem: flags.boolean({ - char: 'r', - description: 'Redeem accrued rewards from Locked Gold', - exclusive: ['delegate'], - }), - delegate: Flags.address({ - char: 'd', - description: 'Delegate rewards to provided account', - exclusive: ['redeem'], - }), - } - - static args = [] - - static examples = [ - 'rewards --redeem', - 'rewards --delegate=0x56e172F6CfB6c7D01C1574fa3E2Be7CC73269D95', - ] - - async run() { - const res = this.parse(Rewards) - - if (!res.flags.redeem && !res.flags.delegate) { - this.error(`Specify action with --redeem or --delegate`) - return - } - - this.kit.defaultAccount = res.flags.from - const lockedGold = await this.kit.contracts.getLockedGold() - if (res.flags.redeem) { - const tx = lockedGold.redeemRewards() - await displaySendTx('redeemRewards', tx) - } - - if (res.flags.delegate) { - const tx = await lockedGold.delegateRewards(res.flags.from, res.flags.delegate) - await displaySendTx('delegateRewards', tx) - } - } -} diff --git a/packages/cli/src/commands/lockedgold/show.ts b/packages/cli/src/commands/lockedgold/show.ts index 2848cdee657..9c9c90c089c 100644 --- a/packages/cli/src/commands/lockedgold/show.ts +++ b/packages/cli/src/commands/lockedgold/show.ts @@ -1,60 +1,23 @@ -import { flags } from '@oclif/command' -import BigNumber from 'bignumber.js' -import chalk from 'chalk' -import { cli } from 'cli-ux' import { BaseCommand } from '../../base' +import { printValueMapRecursive } from '../../utils/cli' import { Args } from '../../utils/command' -import { LockedGoldArgs } from '../../utils/lockedgold' export default class Show extends BaseCommand { - static description = 'Show Locked Gold and corresponding account weight of a commitment given ID' + static description = 'Show Locked Gold information for a given account' static flags = { ...BaseCommand.flags, - noticePeriod: flags.string({ - ...LockedGoldArgs.noticePeriodArg, - exclusive: ['availabilityTime'], - }), - availabilityTime: flags.string({ - ...LockedGoldArgs.availabilityTimeArg, - exclusive: ['noticePeriod'], - }), } static args = [Args.address('account')] - static examples = [ - 'show 0x5409ed021d9299bf6814279a6a1411a7e866a631 --noticePeriod=3600', - 'show 0x5409ed021d9299bf6814279a6a1411a7e866a631 --availabilityTime=1562206887', - ] + static examples = ['show 0x5409ed021d9299bf6814279a6a1411a7e866a631'] async run() { // tslint:disable-next-line - const { flags, args } = this.parse(Show) - - if (!(flags.noticePeriod || flags.availabilityTime)) { - this.error(`Specify commitment ID with --noticePeriod or --availabilityTime`) - return - } + const { args } = this.parse(Show) const lockedGold = await this.kit.contracts.getLockedGold() - let value = new BigNumber(0) - let contributingWeight = new BigNumber(0) - if (flags.noticePeriod) { - cli.action.start('Fetching Locked Gold commitment...') - value = await lockedGold.getLockedCommitmentValue(args.account, flags.noticePeriod) - contributingWeight = value.times(new BigNumber(flags.noticePeriod)) - } - - if (flags.availabilityTime) { - cli.action.start('Fetching notified commitment...') - value = await lockedGold.getNotifiedCommitmentValue(args.account, flags.availabilityTime) - contributingWeight = value - } - - cli.action.stop() - - cli.log(chalk.bold.yellow('Gold Locked \t') + value.toString()) - cli.log(chalk.bold.red('Account Weight Contributed \t') + contributingWeight.toString()) + printValueMapRecursive(await lockedGold.getAccountSummary(args.account)) } } diff --git a/packages/cli/src/commands/lockedgold/unlock.ts b/packages/cli/src/commands/lockedgold/unlock.ts new file mode 100644 index 00000000000..1b04e9980c1 --- /dev/null +++ b/packages/cli/src/commands/lockedgold/unlock.ts @@ -0,0 +1,26 @@ +import { flags } from '@oclif/command' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' +import { LockedGoldArgs } from '../../utils/lockedgold' + +export default class Unlock extends BaseCommand { + static description = 'Unlocks Celo Gold, which can be withdrawn after the unlocking period.' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true }), + value: flags.string({ ...LockedGoldArgs.valueArg, required: true }), + } + + static args = [] + + static examples = ['unlock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --value 500000000'] + + async run() { + const res = this.parse(Unlock) + this.kit.defaultAccount = res.flags.from + const lockedgold = await this.kit.contracts.getLockedGold() + await displaySendTx('unlock', lockedgold.unlock(res.flags.value)) + } +} diff --git a/packages/cli/src/commands/lockedgold/withdraw.ts b/packages/cli/src/commands/lockedgold/withdraw.ts index e34fa24b954..06383dea399 100644 --- a/packages/cli/src/commands/lockedgold/withdraw.ts +++ b/packages/cli/src/commands/lockedgold/withdraw.ts @@ -1,25 +1,49 @@ import { BaseCommand } from '../../base' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' -import { LockedGoldArgs } from '../../utils/lockedgold' export default class Withdraw extends BaseCommand { - static description = 'Withdraw notified commitment given availability time' + static description = 'Withdraw unlocked gold whose unlocking period has passed.' static flags = { ...BaseCommand.flags, from: Flags.address({ required: true }), } - static args = [{ ...LockedGoldArgs.availabilityTimeArg, required: true }] - - static examples = ['withdraw 3600'] + static examples = ['withdraw --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95'] async run() { // tslint:disable-next-line - const { flags, args } = this.parse(Withdraw) + const { flags } = this.parse(Withdraw) this.kit.defaultAccount = flags.from const lockedgold = await this.kit.contracts.getLockedGold() - await displaySendTx('withdrawCommitment', lockedgold.withdrawCommitment(args.availabilityTime)) + const currentTime = Math.round(new Date().getTime() / 1000) + + while (true) { + let madeWithdrawal = false + const pendingWithdrawals = await lockedgold.getPendingWithdrawals(flags.from) + for (let i = 0; i < pendingWithdrawals.length; i++) { + const pendingWithdrawal = pendingWithdrawals[i] + if (pendingWithdrawal.time.isLessThan(currentTime)) { + console.log( + `Found available pending withdrawal of value ${pendingWithdrawal.value.toString()}, withdrawing` + ) + await displaySendTx('withdraw', lockedgold.withdraw(i)) + madeWithdrawal = true + break + } + } + if (!madeWithdrawal) { + break + } + } + const remainingPendingWithdrawals = await lockedgold.getPendingWithdrawals(flags.from) + for (const pendingWithdrawal of remainingPendingWithdrawals) { + console.log( + `Pending withdrawal of value ${pendingWithdrawal.value.toString()} available for withdrawal in ${pendingWithdrawal.time + .minus(currentTime) + .toString()} seconds.` + ) + } } } diff --git a/packages/cli/src/commands/validator/list.ts b/packages/cli/src/commands/validator/list.ts index 9136c7bbdec..af742764809 100644 --- a/packages/cli/src/commands/validator/list.ts +++ b/packages/cli/src/commands/validator/list.ts @@ -20,9 +20,7 @@ export default class ValidatorList extends BaseCommand { cli.action.stop() cli.table(validatorList, { address: {}, - id: {}, name: {}, - url: {}, publicKey: {}, affiliation: {}, }) diff --git a/packages/cli/src/commands/validator/register.ts b/packages/cli/src/commands/validator/register.ts index f237c862ff3..7c826f8fc5c 100644 --- a/packages/cli/src/commands/validator/register.ts +++ b/packages/cli/src/commands/validator/register.ts @@ -10,20 +10,12 @@ export default class ValidatorRegister extends BaseCommand { static flags = { ...BaseCommand.flags, from: Flags.address({ required: true, description: 'Address for the Validator' }), - id: flags.string({ required: true }), name: flags.string({ required: true }), - url: flags.string({ required: true }), publicKey: Flags.publicKey({ required: true }), - noticePeriod: flags.string({ - required: true, - description: - 'Notice period of the Locked Gold commitment. Specify multiple notice periods to use the sum of the commitments.', - multiple: true, - }), } static examples = [ - 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --id myID --name myName --noticePeriod 5184000 --noticePeriod 5184001 --url "http://validator.com" --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', + 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --name myName --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', ] async run() { const res = this.parse(ValidatorRegister) @@ -32,13 +24,7 @@ export default class ValidatorRegister extends BaseCommand { const attestations = await this.kit.contracts.getAttestations() await displaySendTx( 'registerValidator', - validators.registerValidator( - res.flags.id, - res.flags.name, - res.flags.url, - res.flags.publicKey as any, - res.flags.noticePeriod - ) + validators.registerValidator(res.flags.name, res.flags.publicKey as any) ) // register encryption key on attestations contract diff --git a/packages/cli/src/commands/validatorgroup/list.ts b/packages/cli/src/commands/validatorgroup/list.ts index 2fde1d28ca5..096520b8ff9 100644 --- a/packages/cli/src/commands/validatorgroup/list.ts +++ b/packages/cli/src/commands/validatorgroup/list.ts @@ -16,15 +16,12 @@ export default class ValidatorGroupList extends BaseCommand { cli.action.start('Fetching Validator Groups') const validators = await this.kit.contracts.getValidators() const vgroups = await validators.getRegisteredValidatorGroups() - const votes = await validators.getValidatorGroupsVotes() cli.action.stop() cli.table(vgroups, { address: {}, - id: {}, name: {}, - url: {}, - votes: { get: (r) => votes.find((v) => v.address === r.address)!.votes.toString() }, + commission: { get: (r) => r.commission.toFixed() }, members: { get: (r) => r.members.length }, }) } diff --git a/packages/cli/src/commands/validatorgroup/member.ts b/packages/cli/src/commands/validatorgroup/member.ts index be60216c69a..5470ece4b67 100644 --- a/packages/cli/src/commands/validatorgroup/member.ts +++ b/packages/cli/src/commands/validatorgroup/member.ts @@ -4,8 +4,8 @@ import { BaseCommand } from '../../base' import { displaySendTx } from '../../utils/cli' import { Args, Flags } from '../../utils/command' -export default class ValidatorGroupRegister extends BaseCommand { - static description = 'Manage members of a Validator Group' +export default class ValidatorGroupMembers extends BaseCommand { + static description = 'Add or remove members from a Validator Group' static flags = { ...BaseCommand.flags, @@ -33,7 +33,7 @@ export default class ValidatorGroupRegister extends BaseCommand { ] async run() { - const res = this.parse(ValidatorGroupRegister) + const res = this.parse(ValidatorGroupMembers) if (!(res.flags.accept || res.flags.remove || res.flags.reorder)) { this.error(`Specify action: --accept, --remove or --reorder`) @@ -42,9 +42,9 @@ export default class ValidatorGroupRegister extends BaseCommand { this.kit.defaultAccount = res.flags.from const validators = await this.kit.contracts.getValidators() - if (res.flags.accept) { - await displaySendTx('addMember', validators.addMember((res.args as any).validatorAddress)) + const tx = await validators.addMember(res.flags.from, (res.args as any).validatorAddress) + await displaySendTx('addMember', tx) } else if (res.flags.remove) { await displaySendTx( 'removeMember', diff --git a/packages/cli/src/commands/validatorgroup/register.ts b/packages/cli/src/commands/validatorgroup/register.ts index ca513c3ca78..090e0d8ba85 100644 --- a/packages/cli/src/commands/validatorgroup/register.ts +++ b/packages/cli/src/commands/validatorgroup/register.ts @@ -1,4 +1,5 @@ import { flags } from '@oclif/command' +import BigNumber from 'bignumber.js' import { BaseCommand } from '../../base' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' @@ -9,34 +10,23 @@ export default class ValidatorGroupRegister extends BaseCommand { static flags = { ...BaseCommand.flags, from: Flags.address({ required: true, description: 'Address for the Validator Group' }), - id: flags.string({ required: true }), name: flags.string({ required: true }), - url: flags.string({ required: true }), - noticePeriod: flags.string({ - required: true, - description: - 'Notice period of the Locked Gold commitment. Specify multiple notice periods to use the sum of the commitments.', - multiple: true, - }), + commission: flags.string({ required: true }), } static examples = [ - 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --id myID --name myName --noticePeriod 5184000 --noticePeriod 5184001 --url "http://vgroup.com"', + 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --name myName --commission 0.1', ] + async run() { const res = this.parse(ValidatorGroupRegister) this.kit.defaultAccount = res.flags.from const validators = await this.kit.contracts.getValidators() - - await displaySendTx( - 'registerValidatorGroup', - validators.registerValidatorGroup( - res.flags.id, - res.flags.name, - res.flags.url, - res.flags.noticePeriod - ) + const tx = await validators.registerValidatorGroup( + res.flags.name, + new BigNumber(res.flags.commission) ) + await displaySendTx('registerValidatorGroup', tx) } } diff --git a/packages/cli/src/commands/validatorgroup/vote.ts b/packages/cli/src/commands/validatorgroup/vote.ts deleted file mode 100644 index 52205c26dc8..00000000000 --- a/packages/cli/src/commands/validatorgroup/vote.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { flags } from '@oclif/command' -import { BaseCommand } from '../../base' -import { displaySendTx, printValueMap } from '../../utils/cli' -import { Flags } from '../../utils/command' - -export default class ValidatorGroupVote extends BaseCommand { - static description = 'Vote for a Validator Group' - - static flags = { - ...BaseCommand.flags, - from: Flags.address({ required: true, description: "Voter's address" }), - current: flags.boolean({ - exclusive: ['revoke', 'for'], - description: "Show voter's current vote", - }), - revoke: flags.boolean({ - exclusive: ['current', 'for'], - description: "Revoke voter's current vote", - }), - for: Flags.address({ - exclusive: ['current', 'revoke'], - description: "Set vote for ValidatorGroup's address", - }), - } - - static examples = [ - 'vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --for 0x932fee04521f5fcb21949041bf161917da3f588b', - 'vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --revoke', - 'vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --current', - ] - async run() { - const res = this.parse(ValidatorGroupVote) - - this.kit.defaultAccount = res.flags.from - const validators = await this.kit.contracts.getValidators() - - if (res.flags.current) { - const lockedGold = await this.kit.contracts.getLockedGold() - const details = await lockedGold.getVotingDetails(res.flags.from) - const myVote = await validators.getVoteFrom(details.accountAddress) - - printValueMap({ - ...details, - currentVote: myVote, - }) - } else if (res.flags.revoke) { - const tx = await validators.revokeVote() - await displaySendTx('revokeVote', tx) - } else if (res.flags.for) { - const tx = await validators.vote(res.flags.for) - await displaySendTx('vote', tx) - } else { - this.error('Use one of --for, --current, --revoke') - } - } -} diff --git a/packages/cli/src/utils/key_generator.test.ts b/packages/cli/src/utils/key_generator.test.ts index 60e4732c792..62038e25dec 100644 --- a/packages/cli/src/utils/key_generator.test.ts +++ b/packages/cli/src/utils/key_generator.test.ts @@ -2,7 +2,7 @@ import { validateMnemonic } from 'bip39' import { ReactNativeBip39MnemonicGenerator } from './key_generator' describe('Mnemonic validation', () => { - it('should generatet 24 word mnemonic', () => { + it('should generate 24 word mnemonic', () => { const mnemonic: string = ReactNativeBip39MnemonicGenerator.generateMnemonic() expect(mnemonic.split(' ').length).toEqual(24) }) diff --git a/packages/cli/src/utils/lockedgold.ts b/packages/cli/src/utils/lockedgold.ts index c715315334e..9e9aee7645e 100644 --- a/packages/cli/src/utils/lockedgold.ts +++ b/packages/cli/src/utils/lockedgold.ts @@ -1,15 +1,10 @@ export const LockedGoldArgs = { - noticePeriodArg: { - name: 'noticePeriod', - description: - 'duration (seconds) from notice to withdrawable; doubles as ID of a Locked Gold commitment; ', + pendingWithdrawalIndexArg: { + name: 'pendingWithdrawalIndex', + description: 'index of pending withdrawal whose unlocking period has passed', }, - availabilityTimeArg: { - name: 'availabilityTime', - description: 'unix timestamp at which withdrawable; doubles as ID of a notified commitment', - }, - goldAmountArg: { - name: 'goldAmount', - description: 'unit amount of gold token (cGLD)', + valueArg: { + name: 'value', + description: 'unit amount of Celo Gold (cGLD)', }, } diff --git a/packages/cli/start_geth.sh b/packages/cli/start_geth.sh index 3dc99208d4a..3290494b0e5 100644 --- a/packages/cli/start_geth.sh +++ b/packages/cli/start_geth.sh @@ -11,8 +11,8 @@ GETH_BINARY=${1:-"/usr/local/bin/geth"} NETWORK_NAME=${2:-"alfajores"} # Default to testing the ultralight sync mode SYNCMODE=${3:-"ultralight"} -# Default to 44782 -NETWORK_ID=${4:-"44782"} +# Default to 44784 +NETWORK_ID=${4:-"44784"} DATA_DIR=${5:-"/tmp/tmp1"} GENESIS_FILE_PATH=${6:-"/celo/genesis.json"} STATIC_NODES_FILE_PATH=${7:-"/celo/static-nodes.json"} diff --git a/packages/contractkit/package.json b/packages/contractkit/package.json index d461819180e..ea2be897e57 100644 --- a/packages/contractkit/package.json +++ b/packages/contractkit/package.json @@ -20,7 +20,7 @@ "clean:all": "yarn clean && rm -rf src/generated", "build:gen": "yarn --cwd ../protocol build", "prepublishOnly": "yarn build:gen && yarn build", - "test:prepare": "yarn --cwd ../protocol devchain generate .devchain", + "test:prepare": "yarn --cwd ../protocol devchain generate .devchain --migration_override src/test-utils/migration-override.json", "test": "jest --runInBand", "lint": "tslint -c tslint.json --project ." }, @@ -40,7 +40,7 @@ "web3-eth-abi": "1.0.0-beta.37" }, "devDependencies": { - "@celo/ganache-cli": "git+https://github.com/celo-org/ganache-cli.git#98ad2ba", + "@celo/ganache-cli": "git+https://github.com/celo-org/ganache-cli.git#9d77e02", "@celo/protocol": "1.0.0", "@types/debug": "^4.1.5", "@types/web3": "^1.0.18", diff --git a/packages/contractkit/src/base.ts b/packages/contractkit/src/base.ts index 37f7d9533a3..a410cbb7fac 100644 --- a/packages/contractkit/src/base.ts +++ b/packages/contractkit/src/base.ts @@ -2,13 +2,14 @@ export type Address = string export enum CeloContract { Attestations = 'Attestations', - LockedGold = 'LockedGold', + Election = 'Election', Escrow = 'Escrow', Exchange = 'Exchange', GasCurrencyWhitelist = 'GasCurrencyWhitelist', GasPriceMinimum = 'GasPriceMinimum', GoldToken = 'GoldToken', Governance = 'Governance', + LockedGold = 'LockedGold', Random = 'Random', Registry = 'Registry', Reserve = 'Reserve', diff --git a/packages/contractkit/src/contract-cache.ts b/packages/contractkit/src/contract-cache.ts index 18f163bd4f0..c93a5cf7713 100644 --- a/packages/contractkit/src/contract-cache.ts +++ b/packages/contractkit/src/contract-cache.ts @@ -1,6 +1,7 @@ import { CeloContract } from './base' import { ContractKit } from './kit' import { AttestationsWrapper } from './wrappers/Attestations' +import { ElectionWrapper } from './wrappers/Election' import { ExchangeWrapper } from './wrappers/Exchange' import { GasPriceMinimumWrapper } from './wrappers/GasPriceMinimum' import { GoldTokenWrapper } from './wrappers/GoldTokenWrapper' @@ -13,13 +14,14 @@ import { ValidatorsWrapper } from './wrappers/Validators' const WrapperFactories = { [CeloContract.Attestations]: AttestationsWrapper, - [CeloContract.LockedGold]: LockedGoldWrapper, + [CeloContract.Election]: ElectionWrapper, // [CeloContract.Escrow]: EscrowWrapper, [CeloContract.Exchange]: ExchangeWrapper, // [CeloContract.GasCurrencyWhitelist]: GasCurrencyWhitelistWrapper, [CeloContract.GasPriceMinimum]: GasPriceMinimumWrapper, [CeloContract.GoldToken]: GoldTokenWrapper, [CeloContract.Governance]: GovernanceWrapper, + [CeloContract.LockedGold]: LockedGoldWrapper, // [CeloContract.MultiSig]: MultiSigWrapper, // [CeloContract.Random]: RandomWrapper, // [CeloContract.Registry]: RegistryWrapper, @@ -34,13 +36,14 @@ export type ValidWrappers = keyof CFType interface WrapperCacheMap { [CeloContract.Attestations]?: AttestationsWrapper - [CeloContract.LockedGold]?: LockedGoldWrapper + [CeloContract.Election]?: ElectionWrapper // [CeloContract.Escrow]?: EscrowWrapper, [CeloContract.Exchange]?: ExchangeWrapper // [CeloContract.GasCurrencyWhitelist]?: GasCurrencyWhitelistWrapper, [CeloContract.GasPriceMinimum]?: GasPriceMinimumWrapper [CeloContract.GoldToken]?: GoldTokenWrapper [CeloContract.Governance]?: GovernanceWrapper + [CeloContract.LockedGold]?: LockedGoldWrapper // [CeloContract.MultiSig]?: MultiSigWrapper, // [CeloContract.Random]?: RandomWrapper, // [CeloContract.Registry]?: RegistryWrapper, @@ -64,8 +67,8 @@ export class WrapperCache { getAttestations() { return this.getContract(CeloContract.Attestations) } - getLockedGold() { - return this.getContract(CeloContract.LockedGold) + getElection() { + return this.getContract(CeloContract.Election) } // getEscrow() { // return this.getWrapper(CeloContract.Escrow, newEscrow) @@ -85,6 +88,9 @@ export class WrapperCache { getGovernance() { return this.getContract(CeloContract.Governance) } + getLockedGold() { + return this.getContract(CeloContract.LockedGold) + } // getMultiSig() { // return this.getWrapper(CeloContract.MultiSig, newMultiSig) // } diff --git a/packages/contractkit/src/identity/metadata.ts b/packages/contractkit/src/identity/metadata.ts index f7a6fcd4534..c59ab44645a 100644 --- a/packages/contractkit/src/identity/metadata.ts +++ b/packages/contractkit/src/identity/metadata.ts @@ -62,8 +62,10 @@ const isOfType = (type: K) => ( export class IdentityMetadataWrapper { data: IdentityMetadata - static emptyData: IdentityMetadata = { - claims: [], + static fromEmpty() { + return new IdentityMetadataWrapper({ + claims: [], + }) } static async fetchFromURL(url: string) { diff --git a/packages/contractkit/src/index.ts b/packages/contractkit/src/index.ts index 1c8b6bdecf4..7ca7df24dff 100644 --- a/packages/contractkit/src/index.ts +++ b/packages/contractkit/src/index.ts @@ -4,7 +4,6 @@ export { Address, AllContracts, CeloContract, CeloToken, NULL_ADDRESS } from './ export { IdentityMetadataWrapper } from './identity' export * from './kit' export { CeloTransactionObject } from './wrappers/BaseWrapper' -export { Roles } from './wrappers/LockedGold' /** * Creates a new web3 instance diff --git a/packages/contractkit/src/kit.ts b/packages/contractkit/src/kit.ts index ffdc769da74..a997ff88e06 100644 --- a/packages/contractkit/src/kit.ts +++ b/packages/contractkit/src/kit.ts @@ -8,6 +8,7 @@ import { toTxResult, TransactionResult } from './utils/tx-result' import { addLocalAccount } from './utils/web3-utils' import { Web3ContractCache } from './web3-contract-cache' import { AttestationsConfig } from './wrappers/Attestations' +import { ElectionConfig } from './wrappers/Election' import { ExchangeConfig } from './wrappers/Exchange' import { GasPriceMinimumConfig } from './wrappers/GasPriceMinimum' import { GovernanceConfig } from './wrappers/Governance' @@ -15,7 +16,7 @@ import { LockedGoldConfig } from './wrappers/LockedGold' import { ReserveConfig } from './wrappers/Reserve' import { SortedOraclesConfig } from './wrappers/SortedOracles' import { StableTokenConfig } from './wrappers/StableTokenWrapper' -import { ValidatorConfig } from './wrappers/Validators' +import { ValidatorsConfig } from './wrappers/Validators' const debug = debugFactory('kit:kit') @@ -36,6 +37,7 @@ export function newKitFromWeb3(web3: Web3) { } export interface NetworkConfig { + election: ElectionConfig exchange: ExchangeConfig attestations: AttestationsConfig governance: GovernanceConfig @@ -44,7 +46,7 @@ export interface NetworkConfig { gasPriceMinimum: GasPriceMinimumConfig reserve: ReserveConfig stableToken: StableTokenConfig - validators: ValidatorConfig + validators: ValidatorsConfig } export interface KitOptions { @@ -78,6 +80,7 @@ export class ContractKit { const token2 = await this.registry.addressFor(CeloContract.StableToken) const contracts = await Promise.all([ this.contracts.getExchange(), + this.contracts.getElection(), this.contracts.getAttestations(), this.contracts.getGovernance(), this.contracts.getLockedGold(), @@ -89,25 +92,27 @@ export class ContractKit { ]) const res = await Promise.all([ contracts[0].getConfig(), - contracts[1].getConfig([token1, token2]), - contracts[2].getConfig(), + contracts[1].getConfig(), + contracts[2].getConfig([token1, token2]), contracts[3].getConfig(), contracts[4].getConfig(), contracts[5].getConfig(), contracts[6].getConfig(), contracts[7].getConfig(), contracts[8].getConfig(), + contracts[9].getConfig(), ]) return { exchange: res[0], - attestations: res[1], - governance: res[2], - lockedGold: res[3], - sortedOracles: res[4], - gasPriceMinimum: res[5], - reserve: res[6], - stableToken: res[7], - validators: res[8], + election: res[1], + attestations: res[2], + governance: res[3], + lockedGold: res[4], + sortedOracles: res[5], + gasPriceMinimum: res[6], + reserve: res[7], + stableToken: res[8], + validators: res[9], } } diff --git a/packages/contractkit/src/test-utils/ganache-test.ts b/packages/contractkit/src/test-utils/ganache-test.ts index 977f980db3f..d1805081f8c 100644 --- a/packages/contractkit/src/test-utils/ganache-test.ts +++ b/packages/contractkit/src/test-utils/ganache-test.ts @@ -1,7 +1,14 @@ +import * as fs from 'fs' import Web3 from 'web3' import { JsonRPCResponse } from 'web3/providers' import { injectDebugProvider } from '../providers/debug-provider' +// This file specifies accounts available when ganache is running. These are derived +// from the MNEMONIC +export const NetworkConfig = JSON.parse( + fs.readFileSync('src/test-utils/migration-override.json').toString() +) + export function jsonRpcCall(web3: Web3, method: string, params: any[]): Promise { return new Promise((resolve, reject) => { web3.currentProvider.send( diff --git a/packages/contractkit/src/test-utils/migration-override.json b/packages/contractkit/src/test-utils/migration-override.json new file mode 100644 index 00000000000..7e2bec1145d --- /dev/null +++ b/packages/contractkit/src/test-utils/migration-override.json @@ -0,0 +1,14 @@ +{ + "stableToken": { + "initialBalances": { + "addresses": ["0x5409ed021d9299bf6814279a6a1411a7e866a631"], + "values": ["10000000000000000000000"] + }, + "oracles": [ + "0x5409ED021D9299bf6814279A6A1411A7e866A631", + "0xE36Ea790bc9d7AB70C55260C66D52b1eca985f84", + "0x06cEf8E666768cC40Cc78CF93d9611019dDcB628", + "0x7457d5E02197480Db681D3fdF256c7acA21bDc12" + ] + } +} diff --git a/packages/contractkit/src/web3-contract-cache.ts b/packages/contractkit/src/web3-contract-cache.ts index d6475497aa2..35d705170eb 100644 --- a/packages/contractkit/src/web3-contract-cache.ts +++ b/packages/contractkit/src/web3-contract-cache.ts @@ -1,6 +1,7 @@ import debugFactory from 'debug' import { CeloContract } from './base' import { newAttestations } from './generated/Attestations' +import { newElection } from './generated/Election' import { newEscrow } from './generated/Escrow' import { newExchange } from './generated/Exchange' import { newGasCurrencyWhitelist } from './generated/GasCurrencyWhitelist' @@ -20,13 +21,14 @@ const debug = debugFactory('kit:web3-contract-cache') const ContractFactories = { [CeloContract.Attestations]: newAttestations, - [CeloContract.LockedGold]: newLockedGold, + [CeloContract.Election]: newElection, [CeloContract.Escrow]: newEscrow, [CeloContract.Exchange]: newExchange, [CeloContract.GasCurrencyWhitelist]: newGasCurrencyWhitelist, [CeloContract.GasPriceMinimum]: newGasPriceMinimum, [CeloContract.GoldToken]: newGoldToken, [CeloContract.Governance]: newGovernance, + [CeloContract.LockedGold]: newLockedGold, [CeloContract.Random]: newRandom, [CeloContract.Registry]: newRegistry, [CeloContract.Reserve]: newReserve, @@ -57,6 +59,9 @@ export class Web3ContractCache { getLockedGold() { return this.getContract(CeloContract.LockedGold) } + getElection() { + return this.getContract(CeloContract.Election) + } getEscrow() { return this.getContract(CeloContract.Escrow) } diff --git a/packages/contractkit/src/wrappers/Attestations.ts b/packages/contractkit/src/wrappers/Attestations.ts index 270b53bf5ce..dc78173ebb5 100644 --- a/packages/contractkit/src/wrappers/Attestations.ts +++ b/packages/contractkit/src/wrappers/Attestations.ts @@ -2,7 +2,7 @@ import { ECIES, PhoneNumberUtils, SignatureUtils } from '@celo/utils' import { zip3 } from '@celo/utils/lib/collections' import BigNumber from 'bignumber.js' import * as Web3Utils from 'web3-utils' -import { Address, CeloToken } from '../base' +import { Address, CeloContract } from '../base' import { Attestations } from '../generated/types/Attestations' import { BaseWrapper, @@ -128,25 +128,23 @@ export class AttestationsWrapper extends BaseWrapper { setWalletAddress = proxySend(this.kit, this.contract.methods.setWalletAddress) /** - * Calculates the amount of CeloToken to request Attestations - * @param token The token to pay for attestations for - * @param attestationsRequested The number of attestations to request + * Calculates the amount of StableToken required to request Attestations + * @param attestationsRequested The number of attestations to request */ - async approveAttestationFee(token: CeloToken, attestationsRequested: number) { - const tokenContract = await this.kit.contracts.getContract(token) - const fee = await this.attestationFeeRequired(token, attestationsRequested) - return tokenContract.approve(this.address, fee.toString()) + async attestationFeeRequired(attestationsRequested: number) { + const tokenAddress = await this.kit.registry.addressFor(CeloContract.StableToken) + const attestationFee = await this.contract.methods.getAttestationRequestFee(tokenAddress).call() + return new BigNumber(attestationFee).times(attestationsRequested) } /** - * Approves the transfer of CeloToken to request Attestations - * @param token The token to pay for attestations for - * @param attestationsRequested The number of attestations to request + * Approves the necessary amount of StableToken to request Attestations + * @param attestationsRequested The number of attestations to request */ - async attestationFeeRequired(token: CeloToken, attestationsRequested: number) { - const tokenAddress = await this.kit.registry.addressFor(token) - const attestationFee = await this.contract.methods.getAttestationRequestFee(tokenAddress).call() - return new BigNumber(attestationFee).times(attestationsRequested) + async approveAttestationFee(attestationsRequested: number) { + const tokenContract = await this.kit.contracts.getContract(CeloContract.StableToken) + const fee = await this.attestationFeeRequired(attestationsRequested) + return tokenContract.approve(this.address, fee.toString()) } /** @@ -300,11 +298,10 @@ export class AttestationsWrapper extends BaseWrapper { * Requests attestations for a phone number * @param phoneNumber The phone number for which to request attestations for * @param attestationsRequested The number of attestations to request - * @param token The token with which to pay for the attestation fee */ - async request(phoneNumber: string, attestationsRequested: number, token: CeloToken) { + async request(phoneNumber: string, attestationsRequested: number) { const phoneHash = PhoneNumberUtils.getPhoneHash(phoneNumber) - const tokenAddress = await this.kit.registry.addressFor(token) + const tokenAddress = await this.kit.registry.addressFor(CeloContract.StableToken) return toTransactionObject( this.kit, this.contract.methods.request(phoneHash, attestationsRequested, tokenAddress) diff --git a/packages/contractkit/src/wrappers/Election.ts b/packages/contractkit/src/wrappers/Election.ts new file mode 100644 index 00000000000..c18385fcbfb --- /dev/null +++ b/packages/contractkit/src/wrappers/Election.ts @@ -0,0 +1,212 @@ +import { eqAddress } from '@celo/utils/lib/address' +import { zip } from '@celo/utils/lib/collections' +import BigNumber from 'bignumber.js' +import { Address, NULL_ADDRESS } from '../base' +import { Election } from '../generated/types/Election' +import { + BaseWrapper, + CeloTransactionObject, + identity, + proxyCall, + proxySend, + toBigNumber, + toNumber, + toTransactionObject, + tupleParser, +} from './BaseWrapper' + +export interface Validator { + address: Address + name: string + url: string + publicKey: string + affiliation: Address | null +} + +export interface ValidatorGroup { + address: Address + name: string + url: string + members: Address[] +} + +export interface ValidatorGroupVote { + address: Address + votes: BigNumber + eligible: boolean +} + +export interface ElectableValidators { + min: BigNumber + max: BigNumber +} + +export interface ElectionConfig { + electableValidators: ElectableValidators + electabilityThreshold: BigNumber + maxNumGroupsVotedFor: BigNumber +} + +/** + * Contract for voting for validators and managing validator groups. + */ +export class ElectionWrapper extends BaseWrapper { + activate = proxySend(this.kit, this.contract.methods.activate) + /** + * Returns the minimum and maximum number of validators that can be elected. + * @returns The minimum and maximum number of validators that can be elected. + */ + async electableValidators(): Promise { + const { min, max } = await this.contract.methods.electableValidators().call() + return { min: toBigNumber(min), max: toBigNumber(max) } + } + /** + * Returns the current election threshold. + * @returns Election threshold. + */ + electabilityThreshold = proxyCall( + this.contract.methods.getElectabilityThreshold, + undefined, + toBigNumber + ) + validatorAddressFromCurrentSet: (index: number) => Promise
= proxyCall( + this.contract.methods.validatorAddressFromCurrentSet, + tupleParser(identity) + ) + + numberValidatorsInCurrentSet = proxyCall( + this.contract.methods.numberValidatorsInCurrentSet, + undefined, + toNumber + ) + + /** + * Returns the total votes for `group` made by `account`. + * @param group The address of the validator group. + * @param account The address of the voting account. + * @return The total votes for `group` made by `account`. + */ + getTotalVotesForGroup = proxyCall( + this.contract.methods.getTotalVotesForGroup, + undefined, + toBigNumber + ) + + /** + * Returns the groups that `account` has voted for. + * @param account The address of the account casting votes. + * @return The groups that `account` has voted for. + */ + getGroupsVotedForByAccount: (account: Address) => Promise = proxyCall( + this.contract.methods.getGroupsVotedForByAccount + ) + + /** + * Returns current configuration parameters. + */ + async getConfig(): Promise { + const res = await Promise.all([ + this.electableValidators(), + this.electabilityThreshold(), + this.contract.methods.maxNumGroupsVotedFor().call(), + ]) + return { + electableValidators: res[0], + electabilityThreshold: res[1], + maxNumGroupsVotedFor: toBigNumber(res[2]), + } + } + + /** + * Returns the addresses in the current validator set. + */ + async getValidatorSetAddresses(): Promise { + const numberValidators = await this.numberValidatorsInCurrentSet() + + const validatorAddressPromises = [] + + for (let i = 0; i < numberValidators; i++) { + validatorAddressPromises.push(this.validatorAddressFromCurrentSet(i)) + } + + return Promise.all(validatorAddressPromises) + } + + /** + * Returns the current registered validator groups and their total votes and eligibility. + */ + async getValidatorGroupsVotes(): Promise { + const validators = await this.kit.contracts.getValidators() + const validatorGroupAddresses = (await validators.getRegisteredValidatorGroups()).map( + (g) => g.address + ) + const validatorGroupVotes = await Promise.all( + validatorGroupAddresses.map((g) => this.contract.methods.getTotalVotesForGroup(g).call()) + ) + const validatorGroupEligible = await Promise.all( + validatorGroupAddresses.map((g) => this.contract.methods.getGroupEligibility(g).call()) + ) + return validatorGroupAddresses.map((a, i) => ({ + address: a, + votes: toBigNumber(validatorGroupVotes[i]), + eligible: validatorGroupEligible[i], + })) + } + + /** + * Returns the current eligible validator groups and their total votes. + */ + async getEligibleValidatorGroupsVotes(): Promise { + const res = await this.contract.methods.getTotalVotesForEligibleValidatorGroups().call() + return zip((a, b) => ({ address: a, votes: new BigNumber(b), eligible: true }), res[0], res[1]) + } + + /** + * Increments the number of total and pending votes for `group`. + * @param validatorGroup The validator group to vote for. + * @param value The amount of gold to use to vote. + */ + async vote(validatorGroup: Address, value: BigNumber): Promise> { + if (this.kit.defaultAccount == null) { + throw new Error(`missing kit.defaultAccount`) + } + + const { lesser, greater } = await this.findLesserAndGreaterAfterVote(validatorGroup, value) + + return toTransactionObject( + this.kit, + this.contract.methods.vote(validatorGroup, value.toString(), lesser, greater) + ) + } + + async findLesserAndGreaterAfterVote( + votedGroup: Address, + voteWeight: BigNumber + ): Promise<{ lesser: Address; greater: Address }> { + const currentVotes = await this.getEligibleValidatorGroupsVotes() + + const selectedGroup = currentVotes.find((votes) => eqAddress(votes.address, votedGroup)) + + // modify the list + if (selectedGroup) { + selectedGroup.votes = selectedGroup.votes.plus(voteWeight) + } else { + currentVotes.push({ + address: votedGroup, + votes: voteWeight, + eligible: true, + }) + } + + // re-sort + currentVotes.sort((a, b) => a.votes.comparedTo(b.votes)) + + // find new index + const newIdx = currentVotes.findIndex((votes) => eqAddress(votes.address, votedGroup)) + + return { + lesser: newIdx === 0 ? NULL_ADDRESS : currentVotes[newIdx - 1].address, + greater: newIdx === currentVotes.length - 1 ? NULL_ADDRESS : currentVotes[newIdx + 1].address, + } + } +} diff --git a/packages/contractkit/src/wrappers/LockedGold.ts b/packages/contractkit/src/wrappers/LockedGold.ts index c87519a965c..bc3833f090c 100644 --- a/packages/contractkit/src/wrappers/LockedGold.ts +++ b/packages/contractkit/src/wrappers/LockedGold.ts @@ -1,16 +1,19 @@ +import { eqAddress } from '@celo/utils/lib/address' import { zip } from '@celo/utils/lib/collections' import BigNumber from 'bignumber.js' import Web3 from 'web3' -import { TransactionObject } from 'web3/eth/types' import { Address } from '../base' import { LockedGold } from '../generated/types/LockedGold' import { BaseWrapper, CeloTransactionObject, + NumberLike, + parseNumber, proxyCall, proxySend, toBigNumber, toTransactionObject, + tupleParser, } from '../wrappers/BaseWrapper' export interface VotingDetails { @@ -20,28 +23,25 @@ export interface VotingDetails { weight: BigNumber } -interface Commitment { - time: BigNumber - value: BigNumber -} - -export interface Commitments { - locked: Commitment[] - notified: Commitment[] - total: { - gold: BigNumber - weight: BigNumber +interface AccountSummary { + lockedGold: { + total: BigNumber + nonvoting: BigNumber + } + authorizations: { + voter: null | string + validator: null | string } + pendingWithdrawals: PendingWithdrawal[] } -export enum Roles { - Validating = '0', - Voting = '1', - Rewards = '2', +interface PendingWithdrawal { + time: BigNumber + value: BigNumber } export interface LockedGoldConfig { - maxNoticePeriod: BigNumber + unlockingPeriod: BigNumber } /** @@ -49,239 +49,155 @@ export interface LockedGoldConfig { */ export class LockedGoldWrapper extends BaseWrapper { /** - * Notifies a Locked Gold commitment, allowing funds to be withdrawn after the notice - * period. - * @param value The amount of the commitment to eventually withdraw. - * @param noticePeriod The notice period of the Locked Gold commitment. - * @return CeloTransactionObject + * Unlocks gold that becomes withdrawable after the unlocking period. + * @param value The amount of gold to unlock. */ - notifyCommitment: ( - value: string | number, - noticePeriod: string | number - ) => CeloTransactionObject = proxySend(this.kit, this.contract.methods.notifyCommitment) - + unlock: (value: NumberLike) => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.unlock, + tupleParser(parseNumber) + ) /** * Creates an account. - * @return CeloTransactionObject */ - createAccount: () => CeloTransactionObject = proxySend( + createAccount = proxySend(this.kit, this.contract.methods.createAccount) + /** + * Withdraws a gold that has been unlocked after the unlocking period has passed. + * @param index The index of the pending withdrawal to withdraw. + */ + withdraw: (index: number) => CeloTransactionObject = proxySend( this.kit, - this.contract.methods.createAccount + this.contract.methods.withdraw ) - /** - * Withdraws a notified commitment after the duration of the notice period. - * @param availabilityTime The availability time of the notified commitment. - * @return CeloTransactionObject + * @notice Locks gold to be used for voting. */ - withdrawCommitment: ( - availabilityTime: string | number - ) => CeloTransactionObject = proxySend(this.kit, this.contract.methods.withdrawCommitment) - + lock = proxySend(this.kit, this.contract.methods.lock) /** - * Redeems rewards accrued since the last redemption for the specified account. - * @return CeloTransactionObject + * Relocks gold that has been unlocked but not withdrawn. + * @param index The index of the pending withdrawal to relock. */ - redeemRewards: () => CeloTransactionObject = proxySend( + relock: (index: number) => CeloTransactionObject = proxySend( this.kit, - this.contract.methods.redeemRewards + this.contract.methods.relock ) /** - * Adds a Locked Gold commitment to `msg.sender`'s account. - * @param noticePeriod The notice period for the commitment. - * @return CeloTransactionObject + * Returns the total amount of locked gold for an account. + * @param account The account. + * @return The total amount of locked gold for an account. */ - newCommitment: (noticePeriod: string | number) => CeloTransactionObject = proxySend( - this.kit, - this.contract.methods.newCommitment + getAccountTotalLockedGold = proxyCall( + this.contract.methods.getAccountTotalLockedGold, + undefined, + toBigNumber ) - /** - * Rebonds a notified commitment, with notice period >= the remaining time to - * availability. - * - * @param value The amount of the commitment to rebond. - * @param availabilityTime The availability time of the notified commitment. - * @return CeloTransactionObject + * Returns the total amount of non-voting locked gold for an account. + * @param account The account. + * @return The total amount of non-voting locked gold for an account. */ - extendCommitment: ( - value: string | number, - availabilityTime: string | number - ) => CeloTransactionObject = proxySend(this.kit, this.contract.methods.extendCommitment) - + getAccountNonvotingLockedGold = proxyCall( + this.contract.methods.getAccountNonvotingLockedGold, + undefined, + toBigNumber + ) /** - * Returns whether or not a specified account is voting. + * Returns the voter for the specified account. * @param account The address of the account. - * @return Whether or not the account is voting. + * @return The address with which the account can vote. */ - isVoting = proxyCall(this.contract.methods.isVoting) - + getVoterFromAccount: (account: string) => Promise
= proxyCall( + this.contract.methods.getVoterFromAccount + ) + /** + * Returns the validator for the specified account. + * @param account The address of the account. + * @return The address with which the account can register a validator or group. + */ + getValidatorFromAccount: (account: string) => Promise
= proxyCall( + this.contract.methods.getValidatorFromAccount + ) /** * Check if an account already exists. * @param account The address of the account * @return Returns `true` if account exists. Returns `false` otherwise. - * In particular it will return `false` if a delegate with given address exists. - */ - isAccount = proxyCall(this.contract.methods.isAccount) - - /** - * Check if a delegate already exists. - * @param account The address of the delegate - * @return Returns `true` if delegate exists. Returns `false` otherwise. - */ - isDelegate = proxyCall(this.contract.methods.isDelegate) - - /** - * Query maximum notice period. - * @returns Current maximum notice period. - */ - maxNoticePeriod = proxyCall(this.contract.methods.maxNoticePeriod, undefined, toBigNumber) - - /** - * Returns the weight of a specified account. - * @param _account The address of the account. - * @return The weight of the specified account. */ - getAccountWeight = proxyCall(this.contract.methods.getAccountWeight, undefined, toBigNumber) - /** - * Get the delegate for a role. - * @param account Address of the active account. - * @param role one of Roles Enum ("validating", "voting", "rewards") - * @return Address of the delegate - */ - getDelegateFromAccountAndRole: (account: string, role: Roles) => Promise
= proxyCall( - this.contract.methods.getDelegateFromAccountAndRole - ) - + isAccount: (account: string) => Promise = proxyCall(this.contract.methods.isAccount) /** * Returns current configuration parameters. */ - async getConfig(): Promise { return { - maxNoticePeriod: await this.maxNoticePeriod(), + unlockingPeriod: toBigNumber(await this.contract.methods.unlockingPeriod().call()), } } - /** - * Get voting details for an address - * @param accountOrVoterAddress Accout or Voter address - */ - async getVotingDetails(accountOrVoterAddress: Address): Promise { - const accountAddress = await this.contract.methods - .getAccountFromDelegateAndRole(accountOrVoterAddress, Roles.Voting) - .call() - + async getAccountSummary(account: string): Promise { + const nonvoting = await this.getAccountNonvotingLockedGold(account) + const total = await this.getAccountTotalLockedGold(account) + const voter = await this.getVoterFromAccount(account) + const validator = await this.getValidatorFromAccount(account) + const pendingWithdrawals = await this.getPendingWithdrawals(account) return { - accountAddress, - voterAddress: accountOrVoterAddress, - weight: await this.getAccountWeight(accountAddress), + lockedGold: { + total, + nonvoting, + }, + authorizations: { + voter: eqAddress(voter, account) ? null : voter, + validator: eqAddress(validator, account) ? null : validator, + }, + pendingWithdrawals, } } - async getLockedCommitmentValue(account: Address, noticePeriod: string): Promise { - const commitment = await this.contract.methods.getLockedCommitment(account, noticePeriod).call() - return this.getValueFromCommitment(commitment) - } - - async getLockedCommitments(account: Address): Promise { - return this.zipAccountTimesAndValuesToCommitments( - account, - this.contract.methods.getNoticePeriods, - this.getLockedCommitmentValue.bind(this) - ) - } - - async getNotifiedCommitmentValue(account: Address, availTime: string): Promise { - const commitment = await this.contract.methods.getNotifiedCommitment(account, availTime).call() - return this.getValueFromCommitment(commitment) - } - - async getNotifiedCommitments(account: Address): Promise { - return this.zipAccountTimesAndValuesToCommitments( - account, - this.contract.methods.getAvailabilityTimes, - this.getNotifiedCommitmentValue.bind(this) - ) - } - /** - * Get commitments for an Account - * @param account Account address + * Authorize voting on behalf of this account to another address. + * @param account Address of the active account. + * @param voter Address to be used for voting. + * @return A CeloTransactionObject */ - async getCommitments(account: Address): Promise { - const locked = await this.getLockedCommitments(account) - const notified = await this.getNotifiedCommitments(account) - const weight = await this.getAccountWeight(account) - - const totalLocked = locked.reduce( - (acc, commitment) => acc.plus(commitment.value), - new BigNumber(0) + async authorizeVoter(account: Address, voter: Address): Promise> { + const sig = await this.getParsedSignatureOfAddress(account, voter) + // TODO(asa): Pass default tx "from" argument. + return toTransactionObject( + this.kit, + this.contract.methods.authorizeVoter(voter, sig.v, sig.r, sig.s) ) - const gold = notified.reduce((acc, commitment) => acc.plus(commitment.value), totalLocked) - - return { - locked, - notified, - total: { weight, gold }, - } } /** - * Delegate a Role to another account. + * Authorize validating on behalf of this account to another address. * @param account Address of the active account. - * @param delegate Address of the delegate - * @param role one of Roles Enum ("Validating", "Voting", "Rewards") + * @param voter Address to be used for validating. * @return A CeloTransactionObject */ - async delegateRoleTx( + async authorizeValidator( account: Address, - delegate: Address, - role: Roles + validator: Address ): Promise> { - const sig = await this.getParsedSignatureOfAddress(account, delegate) + const sig = await this.getParsedSignatureOfAddress(account, validator) return toTransactionObject( this.kit, - this.contract.methods.delegateRole(role, delegate, sig.v, sig.r, sig.s) + this.contract.methods.authorizeValidator(validator, sig.v, sig.r, sig.s) ) } /** - * Delegate a Rewards to another account. - * @param account Address of the active account. - * @param delegate Address of the delegate - * @return A CeloTransactionObject - */ - async delegateRewards(account: Address, delegate: Address): Promise> { - return this.delegateRoleTx(account, delegate, Roles.Rewards) - } - - /** - * Delegate a voting to another account. - * @param account Address of the active account. - * @param delegate Address of the delegate - * @return A CeloTransactionObject - */ - async delegateVoting(account: Address, delegate: Address): Promise> { - return this.delegateRoleTx(account, delegate, Roles.Voting) - } - - /** - * Delegate a validating to another account. - * @param account Address of the active account. - * @param delegate Address of the delegate - * @return A CeloTransactionObject + * Returns the pending withdrawals from unlocked gold for an account. + * @param account The address of the account. + * @return The value and timestamp for each pending withdrawal. */ - async delegateValidating( - account: Address, - delegate: Address - ): Promise> { - return this.delegateRoleTx(account, delegate, Roles.Validating) - } - - private getValueFromCommitment(commitment: { 0: string; 1: string }) { - return new BigNumber(commitment[0]) + async getPendingWithdrawals(account: string) { + const withdrawals = await this.contract.methods.getPendingWithdrawals(account).call() + return zip( + (time, value) => + // tslint:disable-next-line: no-object-literal-type-assertion + ({ time: toBigNumber(time), value: toBigNumber(value) } as PendingWithdrawal), + withdrawals[1], + withdrawals[0] + ) } private async getParsedSignatureOfAddress(address: Address, signer: string) { @@ -293,19 +209,4 @@ export class LockedGoldWrapper extends BaseWrapper { v: Web3.utils.hexToNumber(signature.slice(128, 130)) + 27, } } - - private async zipAccountTimesAndValuesToCommitments( - account: Address, - timesFunc: (account: string) => TransactionObject, - valueFunc: (account: string, time: string) => Promise - ) { - const accountTimes = await timesFunc(account).call() - const accountValues = await Promise.all(accountTimes.map((time) => valueFunc(account, time))) - return zip( - // tslint:disable-next-line: no-object-literal-type-assertion - (time, value) => ({ time, value } as Commitment), - accountTimes.map((time) => new BigNumber(time)), - accountValues - ) - } } diff --git a/packages/contractkit/src/wrappers/SortedOracles.test.ts b/packages/contractkit/src/wrappers/SortedOracles.test.ts new file mode 100644 index 00000000000..cc9cbb26e9f --- /dev/null +++ b/packages/contractkit/src/wrappers/SortedOracles.test.ts @@ -0,0 +1,240 @@ +import { Address, CeloContract } from '../base' +import { newKitFromWeb3 } from '../kit' +import { NetworkConfig, testWithGanache } from '../test-utils/ganache-test' +import { OracleRate, SortedOraclesWrapper } from './SortedOracles' + +/* +TEST NOTES: +- In migrations: The only account that has cUSD is accounts[0] +*/ + +testWithGanache('SortedOracles Wrapper', (web3) => { + // NOTE: These values are set in test-utils/network-config.json, and are derived + // from the MNEMONIC. If the MNEMONIC has changed, these will need to be reset. + // To do that, look at the output of web3.eth.getAccounts(), and pick a few + // addresses from that set to be oracles + const stableTokenOracles: Address[] = NetworkConfig.stableToken.oracles + const oracleAddress = stableTokenOracles[stableTokenOracles.length - 1] + + const kit = newKitFromWeb3(web3) + let allAccounts: Address[] + let sortedOracles: SortedOraclesWrapper + let stableTokenAddress: Address + let nonOracleAddress: Address + + beforeAll(async () => { + sortedOracles = await kit.contracts.getSortedOracles() + stableTokenAddress = await kit.registry.addressFor(CeloContract.StableToken) + allAccounts = await web3.eth.getAccounts() + nonOracleAddress = allAccounts.find((addr) => { + return !stableTokenOracles.includes(addr) + })! + }) + + describe('#report', () => { + const numerator = 16 + const denominator = 1 + + describe('when reporting from a whitelisted Oracle', () => { + it('should be able to report a rate', async () => { + const initialRates: OracleRate[] = await sortedOracles.getRates(CeloContract.StableToken) + + const tx = await sortedOracles.report( + CeloContract.StableToken, + numerator, + denominator, + oracleAddress + ) + await tx.sendAndWaitForReceipt() + + const resultingRates: OracleRate[] = await sortedOracles.getRates(CeloContract.StableToken) + expect(resultingRates).not.toMatchObject(initialRates) + }) + + describe('when inserting into the middle of the existing rates', () => { + beforeEach(async () => { + const rates = [15, 20, 17] + for (let i = 0; i < stableTokenOracles.length - 1; i++) { + const tx = await sortedOracles.report( + CeloContract.StableToken, + rates[i], + denominator, + stableTokenOracles[i] + ) + await tx.sendAndWaitForReceipt() + } + }) + + const expectedLesserKey = stableTokenOracles[0] + const expectedGreaterKey = stableTokenOracles[2] + + const expectedOracleOrder = [ + stableTokenOracles[1], + stableTokenOracles[2], + oracleAddress, + stableTokenOracles[0], + ] + + it('passes the correct lesserKey and greaterKey as args', async () => { + const tx = await sortedOracles.report( + CeloContract.StableToken, + numerator, + denominator, + oracleAddress + ) + const actualArgs = tx.txo.arguments + expect(actualArgs[3]).toEqual(expectedLesserKey) + expect(actualArgs[4]).toEqual(expectedGreaterKey) + + await tx.sendAndWaitForReceipt() + }) + + it('inserts the new record in the right place', async () => { + const tx = await sortedOracles.report( + CeloContract.StableToken, + numerator, + denominator, + oracleAddress + ) + await tx.sendAndWaitForReceipt() + + const resultingRates: OracleRate[] = await sortedOracles.getRates( + CeloContract.StableToken + ) + + expect(resultingRates.map((r) => r.address)).toEqual(expectedOracleOrder) + }) + }) + }) + + describe('when reporting from a non-oracle address', () => { + it('should raise an error', async () => { + const tx = await sortedOracles.report( + CeloContract.StableToken, + numerator, + denominator, + nonOracleAddress + ) + await expect(tx.sendAndWaitForReceipt()).rejects.toThrow('sender was not an oracle') + }) + + it('should not change the list of rates', async () => { + const initialRates = await sortedOracles.getRates(CeloContract.StableToken) + try { + const tx = await sortedOracles.report( + CeloContract.StableToken, + numerator, + denominator, + nonOracleAddress + ) + await tx.sendAndWaitForReceipt() + } catch (err) { + // We don't need to do anything with this error other than catch it so + // it doesn't fail this test. + } finally { + const resultingRates = await sortedOracles.getRates(CeloContract.StableToken) + expect(resultingRates).toMatchObject(initialRates) + } + }) + }) + }) + + /** + * Proxy Calls to view methods + * + * The purpose of these tests is to verify that these wrapper functions exist, + * are calling the contract methods correctly, and get some value back. The + * values checked here are often dependent on setup occuring in the protocol + * migrations run in `yarn test:prepare`. If these tests are failing, the first + * thing to check is if there have been changes to the migrations + */ + describe('#getRates', () => { + beforeEach(async () => { + for (let i = 0; i < stableTokenOracles.length; i++) { + // reports these values: + // 1/2, 2/2, 3/2, 4/2 + // resulting in: 0.5, 1, 1.5, 2 + const tx = await sortedOracles.report( + CeloContract.StableToken, + i + 1, + 2, + stableTokenOracles[i] + ) + await tx.sendAndWaitForReceipt() + } + }) + it('SBAT getRates', async () => { + const rates = await sortedOracles.getRates(CeloContract.StableToken) + expect(rates.length).toBeGreaterThan(0) + for (const rate of rates) { + expect(rate).toHaveProperty('address') + expect(rate).toHaveProperty('rate') + expect(rate).toHaveProperty('medianRelation') + } + }) + + it('returns the rate as the result of the calculation numerator/denominator', async () => { + const expectedRates = ['2', '1.5', '1', '0.5'] + const response = await sortedOracles.getRates(CeloContract.StableToken) + const actualRates = response.map((r) => r.rate.toString()) + expect(actualRates).toEqual(expectedRates) + }) + }) + + describe('#isOracle', () => { + it('returns true when this address is a whitelisted oracle for this token', async () => { + expect(await sortedOracles.isOracle(CeloContract.StableToken, oracleAddress)).toEqual(true) + }) + it('returns false when this address is not an oracle', async () => { + expect(await sortedOracles.isOracle(CeloContract.StableToken, nonOracleAddress)).toEqual( + false + ) + }) + }) + + describe('#numRates', () => { + it('returns a count of rates reported for the specified token', async () => { + // Why 1? In packages/protocol/08_stabletoken, a single rate is reported + expect(await sortedOracles.numRates(CeloContract.StableToken)).toEqBigNumber(1) + }) + }) + + describe('#medianRate', () => { + it('returns the key for the median', async () => { + const returnedMedian = await sortedOracles.medianRate(CeloContract.StableToken) + // The value `10` comes from: packages/protocol/migrationsConfig.js: + // stableToken.goldPrice + expect(returnedMedian.rate).toEqBigNumber(10) + }) + }) + + describe('#reportExpirySeconds', () => { + it('returns the number of seconds after which a report expires', async () => { + const result = await sortedOracles.reportExpirySeconds() + expect(result).toEqBigNumber(3600) + }) + }) + + /** + * Helper Functions + * + * These are functions in the wrapper that call other functions, passing in + * some regularly used arguments. The purpose of these tests is to verify that + * those arguments are being set correctly. + */ + describe('getStableTokenRates', () => { + it('gets rates for Stable Token', async () => { + const usdRatesResult = await sortedOracles.getStableTokenRates() + const getRatesResult = await sortedOracles.getRates(CeloContract.StableToken) + expect(usdRatesResult).toEqual(getRatesResult) + }) + }) + + describe('reportStableToken', () => { + it('calls report with the address for StableToken', async () => { + const tx = await sortedOracles.reportStableToken(14, 1, oracleAddress) + await tx.sendAndWaitForReceipt() + expect(tx.txo.arguments[0]).toEqual(stableTokenAddress) + }) + }) +}) diff --git a/packages/contractkit/src/wrappers/SortedOracles.ts b/packages/contractkit/src/wrappers/SortedOracles.ts index 7da98f72697..56657d08248 100644 --- a/packages/contractkit/src/wrappers/SortedOracles.ts +++ b/packages/contractkit/src/wrappers/SortedOracles.ts @@ -1,21 +1,124 @@ +import { eqAddress } from '@celo/utils/lib/address' import BigNumber from 'bignumber.js' +import { Address, CeloContract, CeloToken, NULL_ADDRESS } from '../base' import { SortedOracles } from '../generated/types/SortedOracles' -import { BaseWrapper, proxyCall, toBigNumber } from './BaseWrapper' +import { + BaseWrapper, + CeloTransactionObject, + proxyCall, + toBigNumber, + toTransactionObject, +} from './BaseWrapper' + +export enum MedianRelation { + Undefined, + Lesser, + Greater, + Equal, +} export interface SortedOraclesConfig { reportExpirySeconds: BigNumber } +export interface OracleRate { + address: Address + rate: BigNumber + medianRelation: MedianRelation +} + +export interface MedianRate { + rate: BigNumber +} + /** * Currency price oracle contract. */ export class SortedOraclesWrapper extends BaseWrapper { + /** + * Gets the number of rates that have been reported for the given token + * @param token The CeloToken token for which the Celo Gold exchange rate is being reported. + * @return The number of reported oracle rates for `token`. + */ + async numRates(token: CeloToken): Promise { + const tokenAddress = await this.kit.registry.addressFor(token) + const response = await this.contract.methods.numRates(tokenAddress).call() + return toBigNumber(response).toNumber() + } + + /** + * Returns the median rate for the given token + * @param token The CeloToken token for which the Celo Gold exchange rate is being reported. + * @return The median exchange rate for `token`, expressed as: + * amount of that token / equivalent amount in Celo Gold + */ + async medianRate(token: CeloToken): Promise { + const tokenAddress = await this.kit.registry.addressFor(token) + const response = await this.contract.methods.medianRate(tokenAddress).call() + return { + rate: toBigNumber(response[0]).div(toBigNumber(response[1])), + } + } + + /** + * Checks if the given address is whitelisted as an oracle for the token + * @param token The CeloToken token + * @param oracle The address that we're checking the oracle status of + * @returns boolean describing whether this account is an oracle + */ + async isOracle(token: CeloToken, oracle: Address): Promise { + const tokenAddress = await this.kit.registry.addressFor(token) + return this.contract.methods.isOracle(tokenAddress, oracle).call() + } + /** * Returns the report expiry parameter. * @returns Current report expiry. */ reportExpirySeconds = proxyCall(this.contract.methods.reportExpirySeconds, undefined, toBigNumber) + /** + * Updates an oracle value and the median. + * @param token The address of the token for which the Celo Gold exchange rate is being reported. + * @param numerator The amount of tokens equal to `denominator` Celo Gold. + * @param denominator The amount of Celo Gold that the `numerator` tokens are equal to. + */ + async report( + token: CeloToken, + numerator: number, + denominator: number, + oracleAddress: Address + ): Promise> { + const tokenAddress = await this.kit.registry.addressFor(token) + + const { lesserKey, greaterKey } = await this.findLesserAndGreaterKeys( + token, + numerator, + denominator, + oracleAddress + ) + + return toTransactionObject( + this.kit, + this.contract.methods.report(tokenAddress, numerator, denominator, lesserKey, greaterKey), + { from: oracleAddress } + ) + } + + /** + * Updates an oracle value and the median. + * @param token The address of the token for which the Celo Gold exchange rate is being reported. + * @param numerator The amount of tokens equal to `denominator` Celo Gold. + * @param denominator The amount of Celo Gold that the `numerator` tokens are equal to. + */ + async reportStableToken( + numerator: number, + denominator: number, + oracleAddress: Address + ): Promise> { + return this.report(CeloContract.StableToken, numerator, denominator, oracleAddress) + } + /** * Returns current configuration parameters. */ @@ -24,4 +127,68 @@ export class SortedOraclesWrapper extends BaseWrapper { reportExpirySeconds: await this.reportExpirySeconds(), } } + + /** + * Helper function to get the rates for StableToken, by passing the address + * of StableToken to `getRates`. + */ + getStableTokenRates = async (): Promise => this.getRates(CeloContract.StableToken) + + /** + * Gets all elements from the doubly linked list. + * @param token The CeloToken representing the token for which the Celo + * Gold exchange rate is being reported. Example: CeloContract.StableToken + * @return An unpacked list of elements from largest to smallest. + */ + async getRates(token: CeloToken): Promise { + const tokenAddress = await this.kit.registry.addressFor(token) + const response = await this.contract.methods.getRates(tokenAddress).call() + const rates: OracleRate[] = [] + const denominator = await this.getInternalDenominator() + + for (let i = 0; i < response[0].length; i++) { + const medRelIndex = parseInt(response[2][i], 10) + rates.push({ + address: response[0][i], + rate: toBigNumber(response[1][i]).div(denominator), + medianRelation: medRelIndex, + }) + } + return rates + } + + private async getInternalDenominator(): Promise { + return toBigNumber(await this.contract.methods.DENOMINATOR().call()) + } + + private async findLesserAndGreaterKeys( + token: CeloToken, + numerator: number, + denominator: number, + oracleAddress: Address + ): Promise<{ lesserKey: Address; greaterKey: Address }> { + const currentRates: OracleRate[] = await this.getRates(token) + + // This is how the contract calculates the rate from the numerator and denominator. + // To figure out where this new report goes in the list, we need to compare this + // value with the other rates + const value = toBigNumber(numerator.toString()).div(toBigNumber(denominator.toString())) + + let greaterKey = NULL_ADDRESS + let lesserKey = NULL_ADDRESS + + // This leverages the fact that the currentRates are already sorted from + // greatest to lowest value + for (const rate of currentRates) { + if (!eqAddress(rate.address, oracleAddress)) { + if (rate.rate.isLessThanOrEqualTo(value)) { + lesserKey = rate.address + break + } + greaterKey = rate.address + } + } + + return { lesserKey, greaterKey } + } } diff --git a/packages/contractkit/src/wrappers/StableTokenWrapper.ts b/packages/contractkit/src/wrappers/StableTokenWrapper.ts index e8593fe24df..558d55b539d 100644 --- a/packages/contractkit/src/wrappers/StableTokenWrapper.ts +++ b/packages/contractkit/src/wrappers/StableTokenWrapper.ts @@ -71,7 +71,6 @@ export class StableTokenWrapper extends BaseWrapper { toBigNumber ) - minter = proxyCall(this.contract.methods.minter) owner = proxyCall(this.contract.methods.owner) /** diff --git a/packages/contractkit/src/wrappers/Validators.test.ts b/packages/contractkit/src/wrappers/Validators.test.ts index 3d5c831e6e3..2f3844eb9e2 100644 --- a/packages/contractkit/src/wrappers/Validators.test.ts +++ b/packages/contractkit/src/wrappers/Validators.test.ts @@ -1,3 +1,4 @@ +import BigNumber from 'bignumber.js' import Web3 from 'web3' import { newKitFromWeb3 } from '../kit' import { testWithGanache } from '../test-utils/ganache-test' @@ -9,8 +10,7 @@ TEST NOTES: - In migrations: The only account that has cUSD is accounts[0] */ -const minLockedGoldValue = Web3.utils.toWei('100', 'ether') // 1 gold -const minLockedGoldNoticePeriod = 120 * 24 * 60 * 60 // 120 days +const minLockedGoldValue = Web3.utils.toWei('10', 'ether') // 10 gold // A random 64 byte hex string. const publicKey = @@ -28,16 +28,11 @@ testWithGanache('Validators Wrapper', (web3) => { let validators: ValidatorsWrapper let lockedGold: LockedGoldWrapper - const registerAccountWithCommitment = async (account: string) => { - // console.log('isAccount', ) - // console.log('isDelegate', await lockedGold.isDelegate(account)) - + const registerAccountWithLockedGold = async (account: string) => { if (!(await lockedGold.isAccount(account))) { await lockedGold.createAccount().sendAndWaitForReceipt({ from: account }) } - await lockedGold - .newCommitment(minLockedGoldNoticePeriod) - .sendAndWaitForReceipt({ from: account, value: minLockedGoldValue }) + await lockedGold.lock().sendAndWaitForReceipt({ from: account, value: minLockedGoldValue }) } beforeAll(async () => { @@ -47,23 +42,21 @@ testWithGanache('Validators Wrapper', (web3) => { }) const setupGroup = async (groupAccount: string) => { - await registerAccountWithCommitment(groupAccount) - await validators - .registerValidatorGroup('thegroup', 'The Group', 'thegroup.com', [minLockedGoldNoticePeriod]) - .sendAndWaitForReceipt({ from: groupAccount }) + await registerAccountWithLockedGold(groupAccount) + await (await validators.registerValidatorGroup( + 'The Group', + new BigNumber(0.1) + )).sendAndWaitForReceipt({ from: groupAccount }) } const setupValidator = async (validatorAccount: string) => { - await registerAccountWithCommitment(validatorAccount) + await registerAccountWithLockedGold(validatorAccount) // set account1 as the validator await validators .registerValidator( - 'goodoldvalidator', 'Good old validator', - 'goodold.com', // @ts-ignore - publicKeysData, - [minLockedGoldNoticePeriod] + publicKeysData ) .sendAndWaitForReceipt({ from: validatorAccount }) } @@ -86,7 +79,7 @@ testWithGanache('Validators Wrapper', (web3) => { await setupGroup(groupAccount) await setupValidator(validatorAccount) await validators.affiliate(groupAccount).sendAndWaitForReceipt({ from: validatorAccount }) - await validators.addMember(validatorAccount).sendAndWaitForReceipt({ from: groupAccount }) + await (await validators.addMember(groupAccount, validatorAccount)).sendAndWaitForReceipt() const members = await validators.getValidatorGroup(groupAccount).then((group) => group.members) expect(members).toContain(validatorAccount) @@ -105,7 +98,7 @@ testWithGanache('Validators Wrapper', (web3) => { for (const validator of [validator1, validator2]) { await setupValidator(validator) await validators.affiliate(groupAccount).sendAndWaitForReceipt({ from: validator }) - await validators.addMember(validator).sendAndWaitForReceipt({ from: groupAccount }) + await (await validators.addMember(groupAccount, validator)).sendAndWaitForReceipt() } const members = await validators diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 2ebe2859f61..74b9ffcccff 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -1,5 +1,4 @@ -import { eqAddress } from '@celo/utils/lib/address' -import { zip } from '@celo/utils/lib/collections' +import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { Address, NULL_ADDRESS } from '../base' import { Validators } from '../generated/types/Validators' @@ -9,113 +8,111 @@ import { proxyCall, proxySend, toBigNumber, - toNumber, toTransactionObject, } from './BaseWrapper' export interface Validator { address: Address - id: string name: string - url: string publicKey: string affiliation: string | null + score: BigNumber } export interface ValidatorGroup { address: Address - id: string name: string - url: string members: Address[] + commission: BigNumber } -export interface ValidatorGroupVote { - address: Address - votes: BigNumber +export interface BalanceRequirements { + group: BigNumber + validator: BigNumber } -export interface RegistrationRequirement { - minLockedGoldValue: BigNumber - minLockedGoldNoticePeriod: BigNumber +export interface DeregistrationLockups { + group: BigNumber + validator: BigNumber } -export interface ValidatorConfig { - minElectableValidators: BigNumber - maxElectableValidators: BigNumber - electionThreshold: BigNumber - registrationRequirement: RegistrationRequirement +export interface ValidatorsConfig { + balanceRequirements: BalanceRequirements + deregistrationLockups: DeregistrationLockups + maxGroupSize: BigNumber } /** * Contract for voting for validators and managing validator groups. */ +// TODO(asa): Support authorized validators export class ValidatorsWrapper extends BaseWrapper { affiliate = proxySend(this.kit, this.contract.methods.affiliate) deaffiliate = proxySend(this.kit, this.contract.methods.deaffiliate) + removeMember = proxySend(this.kit, this.contract.methods.removeMember) registerValidator = proxySend(this.kit, this.contract.methods.registerValidator) - registerValidatorGroup = proxySend(this.kit, this.contract.methods.registerValidatorGroup) - /** - * Returns the minimum number of validators that can be elected. - * @returns The minimum number of validators that can be elected. - */ - minElectableValidators = proxyCall( - this.contract.methods.minElectableValidators, - undefined, - toBigNumber - ) - /** - * Returns the maximum number of validators that can be elected. - * @returns The maximum number of validators that can be elected. - */ - maxElectableValidators = proxyCall( - this.contract.methods.maxElectableValidators, - undefined, - toBigNumber - ) + async registerValidatorGroup( + name: string, + commission: BigNumber + ): Promise> { + return toTransactionObject( + this.kit, + this.contract.methods.registerValidatorGroup(name, toFixed(commission).toFixed()) + ) + } + async addMember(group: string, member: string): Promise> { + const numMembers = await this.getGroupNumMembers(group) + if (numMembers.isZero()) { + const election = await this.kit.contracts.getElection() + const voteWeight = await election.getTotalVotesForGroup(group) + const { lesser, greater } = await election.findLesserAndGreaterAfterVote(group, voteWeight) + + return toTransactionObject( + this.kit, + this.contract.methods.addFirstMember(member, lesser, greater), + { from: group } + ) + } else { + return toTransactionObject(this.kit, this.contract.methods.addMember(member), { from: group }) + } + } /** - * Returns the current election threshold. - * @returns Election threshold. + * Returns the current registration requirements. + * @returns Group and validator registration requirements. */ - electionThreshold = proxyCall(this.contract.methods.getElectionThreshold, undefined, toBigNumber) - validatorAddressFromCurrentSet = proxyCall(this.contract.methods.validatorAddressFromCurrentSet) - numberValidatorsInCurrentSet = proxyCall( - this.contract.methods.numberValidatorsInCurrentSet, - undefined, - toNumber - ) - - getVoteFrom: (validatorAddress: Address) => Promise
= proxyCall( - this.contract.methods.voters - ) + async getBalanceRequirements(): Promise { + const res = await this.contract.methods.getBalanceRequirements().call() + return { + group: toBigNumber(res[0]), + validator: toBigNumber(res[1]), + } + } /** - * Returns the current registrations requirements. - * @returns Minimum deposit and notice period. + * Returns the lockup periods after deregistering groups and validators. + * @return The lockup periods after deregistering groups and validators. */ - async getRegistrationRequirement(): Promise { - const res = await this.contract.methods.getRegistrationRequirement().call() + async getDeregistrationLockups(): Promise { + const res = await this.contract.methods.getDeregistrationLockups().call() return { - minLockedGoldValue: toBigNumber(res[0]), - minLockedGoldNoticePeriod: toBigNumber(res[0]), + group: toBigNumber(res[0]), + validator: toBigNumber(res[1]), } } /** * Returns current configuration parameters. */ - async getConfig(): Promise { + async getConfig(): Promise { const res = await Promise.all([ - this.minElectableValidators(), - this.maxElectableValidators(), - this.electionThreshold(), - this.getRegistrationRequirement(), + this.getBalanceRequirements(), + this.getDeregistrationLockups(), + this.contract.methods.maxGroupSize().call(), ]) return { - minElectableValidators: res[0], - maxElectableValidators: res[1], - electionThreshold: res[2], - registrationRequirement: res[3], + balanceRequirements: res[0], + deregistrationLockups: res[1], + maxGroupSize: toBigNumber(res[2]), } } @@ -125,44 +122,23 @@ export class ValidatorsWrapper extends BaseWrapper { return Promise.all(vgAddresses.map((addr) => this.getValidator(addr))) } - async getValidatorSetAddresses(): Promise { - const numberValidators = await this.numberValidatorsInCurrentSet() - - const validatorAddressPromises = [] - - for (let i = 0; i < numberValidators; i++) { - validatorAddressPromises.push(this.validatorAddressFromCurrentSet(i)) - } - - return Promise.all(validatorAddressPromises) - } + getGroupNumMembers: (group: Address) => Promise = proxyCall( + this.contract.methods.getGroupNumMembers, + undefined, + toBigNumber + ) async getValidator(address: Address): Promise { const res = await this.contract.methods.getValidator(address).call() return { address, - id: res[0], - name: res[1], - url: res[2], - publicKey: res[3] as any, - affiliation: res[4], + name: res[0], + publicKey: res[1] as any, + affiliation: res[2], + score: fromFixed(new BigNumber(res[3])), } } - /** - * Returns whether a particular account is voting for a validator group. - * @param account The account. - * @return Whether a particular account is voting for a validator group. - */ - isVoting = proxyCall(this.contract.methods.isVoting) - - /** - * Returns whether a particular account is a registered validator or validator group. - * @param account The account. - * @return Whether a particular account is a registered validator or validator group. - */ - isValidating = proxyCall(this.contract.methods.isValidating) - /** * Returns whether a particular account has a registered validator. * @param account The account. @@ -177,18 +153,6 @@ export class ValidatorsWrapper extends BaseWrapper { */ isValidatorGroup = proxyCall(this.contract.methods.isValidatorGroup) - /** - * Returns whether an account meets the requirements to register a validator or group. - * @param account The account. - * @param noticePeriods An array of notice periods of the Locked Gold commitments - * that cumulatively meet the requirements for validator registration. - * @return Whether an account meets the requirements to register a validator or group. - */ - meetsRegistrationRequirements = proxyCall(this.contract.methods.meetsRegistrationRequirements) - - addMember = proxySend(this.kit, this.contract.methods.addMember) - removeMember = proxySend(this.kit, this.contract.methods.removeMember) - async reorderMember(groupAddr: Address, validator: Address, newIndex: number) { const group = await this.getValidatorGroup(groupAddr) @@ -226,88 +190,11 @@ export class ValidatorsWrapper extends BaseWrapper { async getValidatorGroup(address: Address): Promise { const res = await this.contract.methods.getValidatorGroup(address).call() - return { address, id: res[0], name: res[1], url: res[2], members: res[3] } - } - - async getValidatorGroupsVotes(): Promise { - const vgAddresses = await this.contract.methods.getRegisteredValidatorGroups().call() - const res = await this.contract.methods.getValidatorGroupVotes().call() - const r = zip((a, b) => ({ address: a, votes: new BigNumber(b) }), res[0], res[1]) - for (const vgAddress of vgAddresses) { - if (!res[0].includes(vgAddress)) { - r.push({ address: vgAddress, votes: new BigNumber(0) }) - } - } - return r - } - - async revokeVote(): Promise> { - if (this.kit.defaultAccount == null) { - throw new Error(`missing from at new ValdidatorUtils()`) - } - - const lockedGold = await this.kit.contracts.getLockedGold() - const votingDetails = await lockedGold.getVotingDetails(this.kit.defaultAccount) - const votedGroup = await this.getVoteFrom(votingDetails.accountAddress) - - if (votedGroup == null) { - throw new Error(`Not current vote for ${this.kit.defaultAccount}`) - } - - const { lesser, greater } = await this.findLesserAndGreaterAfterVote( - votedGroup, - votingDetails.weight.negated() - ) - - return toTransactionObject(this.kit, this.contract.methods.revokeVote(lesser, greater)) - } - - async vote(validatorGroup: Address): Promise> { - if (this.kit.defaultAccount == null) { - throw new Error(`missing from at new ValdidatorUtils()`) - } - - const lockedGold = await this.kit.contracts.getLockedGold() - const votingDetails = await lockedGold.getVotingDetails(this.kit.defaultAccount) - - const { lesser, greater } = await this.findLesserAndGreaterAfterVote( - validatorGroup, - votingDetails.weight - ) - - return toTransactionObject( - this.kit, - this.contract.methods.vote(validatorGroup, lesser, greater) - ) - } - - private async findLesserAndGreaterAfterVote( - votedGroup: Address, - voteWeight: BigNumber - ): Promise<{ lesser: Address; greater: Address }> { - const currentVotes = (await this.getValidatorGroupsVotes()).filter((g) => !g.votes.isZero()) - - const selectedGroup = currentVotes.find((cv) => eqAddress(cv.address, votedGroup)) - - // modify the list - if (selectedGroup) { - selectedGroup.votes = selectedGroup.votes.plus(voteWeight) - } else { - currentVotes.push({ - address: votedGroup, - votes: voteWeight, - }) - } - - // re-sort - currentVotes.sort((a, b) => a.votes.comparedTo(b.votes)) - - // find new index - const newIdx = currentVotes.findIndex((cv) => eqAddress(cv.address, votedGroup)) - return { - lesser: newIdx === 0 ? NULL_ADDRESS : currentVotes[newIdx - 1].address, - greater: newIdx === currentVotes.length - 1 ? NULL_ADDRESS : currentVotes[newIdx + 1].address, + address, + name: res[0], + members: res[1], + commission: fromFixed(new BigNumber(res[2])), } } } diff --git a/packages/docs/command-line-interface/election.md b/packages/docs/command-line-interface/election.md new file mode 100644 index 00000000000..6ed0013466e --- /dev/null +++ b/packages/docs/command-line-interface/election.md @@ -0,0 +1,39 @@ +--- +description: View and manage validator elections +--- + +## Commands + +### Validatorset + +Outputs the current validator set + +``` +USAGE + $ celocli election:validatorset + +EXAMPLE + validatorset +``` + +_See code: [packages/cli/src/commands/election/validatorset.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/election/validatorset.ts)_ + +### Vote + +Vote for a Validator Group in validator elections. + +``` +USAGE + $ celocli election:vote + +OPTIONS + --for=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Set vote for ValidatorGroup's address + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Voter's address + --value=value (required) Amount of Gold used to vote for group + +EXAMPLE + vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --for 0x932fee04521f5fcb21949041bf161917da3f588b, --value + 1000000 +``` + +_See code: [packages/cli/src/commands/election/vote.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/election/vote.ts)_ diff --git a/packages/docs/command-line-interface/lockedgold.md b/packages/docs/command-line-interface/lockedgold.md index 52b2f762242..ef067a6e750 100644 --- a/packages/docs/command-line-interface/lockedgold.md +++ b/packages/docs/command-line-interface/lockedgold.md @@ -1,84 +1,46 @@ --- -description: Manage Locked Gold to participate in governance and earn rewards +description: View and manage locked Celo Gold --- ## Commands -### Delegate +### Authorize -Delegate validating, voting and reward roles for Locked Gold account +Authorize validating or voting address for a Locked Gold account ``` USAGE - $ celocli lockedgold:delegate + $ celocli lockedgold:authorize OPTIONS - -r, --role=Validating|Voting|Rewards Role to delegate + -r, --role=voter|validator Role to delegate --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address --to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address EXAMPLE - delegate --from=0x5409ED021D9299bf6814279A6A1411A7e866A631 --role Voting - --to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d + authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role voter --to + 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d ``` -_See code: [packages/cli/src/commands/lockedgold/delegate.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/delegate.ts)_ +_See code: [packages/cli/src/commands/lockedgold/authorize.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/authorize.ts)_ -### List +### Lock -View information about all of the account's commitments +Locks Celo Gold to be used in governance and validator elections. ``` USAGE - $ celocli lockedgold:list ACCOUNT - -EXAMPLE - list 0x5409ed021d9299bf6814279a6a1411a7e866a631 -``` - -_See code: [packages/cli/src/commands/lockedgold/list.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/list.ts)_ - -### Lockup - -Create a Locked Gold commitment given notice period and gold amount - -``` -USAGE - $ celocli lockedgold:lockup - -OPTIONS - --from=from (required) - --goldAmount=goldAmount (required) unit amount of gold token (cGLD) - - --noticePeriod=noticePeriod (required) duration (seconds) from notice to withdrawable; doubles as ID of a Locked Gold - commitment; - -EXAMPLE - lockup --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --noticePeriod 8640 --goldAmount 1000000000000000000 -``` - -_See code: [packages/cli/src/commands/lockedgold/lockup.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/lockup.ts)_ - -### Notify - -Notify a Locked Gold commitment given notice period and gold amount - -``` -USAGE - $ celocli lockedgold:notify + $ celocli lockedgold:lock OPTIONS - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address - --goldAmount=goldAmount (required) unit amount of gold token (cGLD) - - --noticePeriod=noticePeriod (required) duration (seconds) from notice to withdrawable; doubles - as ID of a Locked Gold commitment; + --from=from (required) + --value=value (required) unit amount of Celo Gold (cGLD) EXAMPLE - notify --noticePeriod=3600 --goldAmount=500 + lock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --value 1000000000000000000 ``` -_See code: [packages/cli/src/commands/lockedgold/notify.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/notify.ts)_ +_See code: [packages/cli/src/commands/lockedgold/lock.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/lock.ts)_ ### Register @@ -97,63 +59,51 @@ EXAMPLE _See code: [packages/cli/src/commands/lockedgold/register.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/register.ts)_ -### Rewards +### Show -Manage rewards for Locked Gold account +Show Locked Gold information for a given account ``` USAGE - $ celocli lockedgold:rewards - -OPTIONS - -d, --delegate=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d Delegate rewards to provided account - -r, --redeem Redeem accrued rewards from Locked Gold - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + $ celocli lockedgold:show ACCOUNT -EXAMPLES - rewards --redeem - rewards --delegate=0x56e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 +EXAMPLE + show 0x5409ed021d9299bf6814279a6a1411a7e866a631 ``` -_See code: [packages/cli/src/commands/lockedgold/rewards.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/rewards.ts)_ +_See code: [packages/cli/src/commands/lockedgold/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/show.ts)_ -### Show +### Unlock -Show Locked Gold and corresponding account weight of a commitment given ID +Unlocks Celo Gold, which can be withdrawn after the unlocking period. ``` USAGE - $ celocli lockedgold:show ACCOUNT + $ celocli lockedgold:unlock OPTIONS - --availabilityTime=availabilityTime unix timestamp at which withdrawable; doubles as ID of a notified commitment - - --noticePeriod=noticePeriod duration (seconds) from notice to withdrawable; doubles as ID of a Locked Gold - commitment; + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + --value=value (required) unit amount of Celo Gold (cGLD) -EXAMPLES - show 0x5409ed021d9299bf6814279a6a1411a7e866a631 --noticePeriod=3600 - show 0x5409ed021d9299bf6814279a6a1411a7e866a631 --availabilityTime=1562206887 +EXAMPLE + unlock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --value 500000000 ``` -_See code: [packages/cli/src/commands/lockedgold/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/show.ts)_ +_See code: [packages/cli/src/commands/lockedgold/unlock.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/unlock.ts)_ ### Withdraw -Withdraw notified commitment given availability time +Withdraw unlocked gold whose unlocking period has passed. ``` USAGE - $ celocli lockedgold:withdraw AVAILABILITYTIME - -ARGUMENTS - AVAILABILITYTIME unix timestamp at which withdrawable; doubles as ID of a notified commitment + $ celocli lockedgold:withdraw OPTIONS --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address EXAMPLE - withdraw 3600 + withdraw --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 ``` _See code: [packages/cli/src/commands/lockedgold/withdraw.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/withdraw.ts)_ diff --git a/packages/docs/command-line-interface/validator.md b/packages/docs/command-line-interface/validator.md index 826206472f2..0d4d8cfd1f1 100644 --- a/packages/docs/command-line-interface/validator.md +++ b/packages/docs/command-line-interface/validator.md @@ -1,5 +1,5 @@ --- -description: View validator information and register your own +description: View and manage validators --- ## Commands @@ -48,19 +48,11 @@ USAGE OPTIONS --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address for the Validator - --id=id (required) --name=name (required) - - --noticePeriod=noticePeriod (required) Notice period of the Locked Gold commitment. Specify - multiple notice periods to use the sum of the commitments. - --publicKey=0x (required) Public Key - --url=url (required) - EXAMPLE - register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --id myID --name myName --noticePeriod 5184000 - --noticePeriod 5184001 --url "http://validator.com" --publicKey + register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --name myName --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf 997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d 785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d diff --git a/packages/docs/command-line-interface/validatorgroup.md b/packages/docs/command-line-interface/validatorgroup.md index 95186d3bfca..8883c2f772e 100644 --- a/packages/docs/command-line-interface/validatorgroup.md +++ b/packages/docs/command-line-interface/validatorgroup.md @@ -1,5 +1,5 @@ --- -description: View validator group information and cast votes +description: View and manage validator groups --- ## Commands @@ -20,7 +20,7 @@ _See code: [packages/cli/src/commands/validatorgroup/list.ts](https://github.com ### Member -Manage members of a Validator Group +Add or remove members from a Validator Group ``` USAGE @@ -52,18 +52,12 @@ USAGE $ celocli validatorgroup:register OPTIONS + --commission=commission (required) --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address for the Validator Group - --id=id (required) --name=name (required) - --noticePeriod=noticePeriod (required) Notice period of the Locked Gold commitment. Specify - multiple notice periods to use the sum of the commitments. - - --url=url (required) - EXAMPLE - register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --id myID --name myName --noticePeriod 5184000 - --noticePeriod 5184001 --url "http://vgroup.com" + register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --name myName --commission 0.1 ``` _See code: [packages/cli/src/commands/validatorgroup/register.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validatorgroup/register.ts)_ @@ -84,25 +78,3 @@ EXAMPLE ``` _See code: [packages/cli/src/commands/validatorgroup/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validatorgroup/show.ts)_ - -### Vote - -Vote for a Validator Group - -``` -USAGE - $ celocli validatorgroup:vote - -OPTIONS - --current Show voter's current vote - --for=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d Set vote for ValidatorGroup's address - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Voter's address - --revoke Revoke voter's current vote - -EXAMPLES - vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --for 0x932fee04521f5fcb21949041bf161917da3f588b - vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --revoke - vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --current -``` - -_See code: [packages/cli/src/commands/validatorgroup/vote.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validatorgroup/vote.ts)_ diff --git a/packages/docs/getting-started/running-a-full-node.md b/packages/docs/getting-started/running-a-full-node.md index 436f3a1e550..7024020256d 100644 --- a/packages/docs/getting-started/running-a-full-node.md +++ b/packages/docs/getting-started/running-a-full-node.md @@ -68,7 +68,7 @@ In order to allow the node to sync with the network, give it the address of exis This command specifies the settings needed to run the node, and gets it started. -`` $ docker run -p 127.0.0.1:8545:8545 -p 127.0.0.1:8546:8546 -p 30303:30303 -p 30303:30303/udp -v `pwd`:/root/.celo us.gcr.io/celo-testnet/celo-node:alfajores --verbosity 3 --networkid 44782 --syncmode full --rpc --rpcaddr 0.0.0.0 --rpcapi eth,net,web3,debug,admin,personal --lightserv 90 --lightpeers 1000 --maxpeers 1100 --etherbase $CELO_ACCOUNT_ADDRESS `` +`` $ docker run -p 127.0.0.1:8545:8545 -p 127.0.0.1:8546:8546 -p 30303:30303 -p 30303:30303/udp -v `pwd`:/root/.celo us.gcr.io/celo-testnet/celo-node:alfajores --verbosity 3 --networkid 44784 --syncmode full --rpc --rpcaddr 0.0.0.0 --rpcapi eth,net,web3,debug,admin,personal --lightserv 90 --lightpeers 1000 --maxpeers 1100 --etherbase $CELO_ACCOUNT_ADDRESS `` You'll start seeing some output. There may be some errors or warnings that are ignorable. After a few minutes, you should see lines that look like this. This means your node has synced with the network and is receiving blocks. diff --git a/packages/docs/getting-started/running-a-validator.md b/packages/docs/getting-started/running-a-validator.md index 69d1e7ca197..207442ff4f8 100644 --- a/packages/docs/getting-started/running-a-validator.md +++ b/packages/docs/getting-started/running-a-validator.md @@ -95,7 +95,7 @@ In order to allow the node to sync with the network, give it the address of exis Start up the node: -`` $ docker run -p 127.0.0.1:8545:8545 -p 127.0.0.1:8546:8546 -p 30303:30303 -p 30303:30303/udp -v `pwd`:/root/.celo us.gcr.io/celo-testnet/celo-node:alfajores --verbosity 3 --networkid 44782 --syncmode full --rpc --rpcaddr 0.0.0.0 --rpcapi eth,net,web3,debug,admin,personal --maxpeers 1100 --mine --miner.verificationpool=https://us-central1-celo-testnet-production.cloudfunctions.net/handleVerificationRequestalfajores/v0.1/sms/ --etherbase $CELO_VALIDATOR_ADDRESS `` +`` $ docker run -p 127.0.0.1:8545:8545 -p 127.0.0.1:8546:8546 -p 30303:30303 -p 30303:30303/udp -v `pwd`:/root/.celo us.gcr.io/celo-testnet/celo-node:alfajores --verbosity 3 --networkid 44784 --syncmode full --rpc --rpcaddr 0.0.0.0 --rpcapi eth,net,web3,debug,admin,personal --maxpeers 1100 --mine --miner.verificationpool=https://us-central1-celo-testnet-production.cloudfunctions.net/handleVerificationRequestalfajores/v0.1/sms/ --etherbase $CELO_VALIDATOR_ADDRESS `` {% hint style="danger" %} **Security**: The command line above includes the parameter `--rpcaddr 0.0.0.0` which makes the Celo Blockchain software listen for incoming RPC requests on all network adaptors. Exercise extreme caution in doing this when running outside Docker, as it means that any unlocked accounts and their funds may be accessed from other machines on the Internet. In the context of running a Docker container on your local machine, this together with the `docker -p` flags allows you to make RPC calls from outside the container, i.e from your local host, but not from outside your machine. Read more about [Docker Networking](https://docs.docker.com/network/network-tutorial-standalone/#use-user-defined-bridge-networks) here. @@ -103,13 +103,13 @@ Start up the node: The `mine` flag does not mean the node starts mining blocks, but rather starts trying to participate in the BFT consensus protocol. It cannot do this until it gets elected -- so next we need to stand for election. -The `networkid` parameter value of `44782` indicates we are connecting the Alfajores Testnet. +The `networkid` parameter value of `44784` indicates we are connecting the Alfajores Testnet. ## Obtain and lock up some Celo Gold for staking Visit the [Alfajores Faucet](https://celo.org/build/faucet) to send **both** of your accounts some funds. -In a new tab, unlock your accounts so that you can send transactions: +In a new tab, unlock your accounts so that you can send transactions. This only unlocks the accounts for the lifetime of the validator that's running, so be sure to unlock `$CELO_VALIDATOR_ADDRESS` again if your node gets restarted: ``` $ celocli account:unlock --account $CELO_VALIDATOR_GROUP_ADDRESS --password @@ -126,8 +126,8 @@ $ celocli lockedgold:register --from $CELO_VALIDATOR_ADDRESS Make a locked Gold commitment for both accounts in order to secure the right to register a validator and validator group. The current requirement is 1 Celo Gold with a notice period of 60 days. If you choose to stake more gold, or a longer notice period, be sure to use those values below: ``` -$ celocli lockedgold:deposit --from $CELO_VALIDATOR_GROUP_ADDRESS --goldAmount 1000000000000000000 --noticePeriod 5184000 -$ celocli lockedgold:deposit --from $CELO_VALIDATOR_ADDRESS --goldAmount 1000000000000000000 --noticePeriod 5184000 +$ celocli lockedgold:lockup --from $CELO_VALIDATOR_GROUP_ADDRESS --goldAmount 1000000000000000000 --noticePeriod 5184000 +$ celocli lockedgold:lockup --from $CELO_VALIDATOR_ADDRESS --goldAmount 1000000000000000000 --noticePeriod 5184000 ``` ## Run for election diff --git a/packages/faucet/package.json b/packages/faucet/package.json index 342545d1826..50a73d19c43 100644 --- a/packages/faucet/package.json +++ b/packages/faucet/package.json @@ -21,7 +21,7 @@ "debug": "^4.1.1", "eth-lib": "^0.2.8", "firebase": "^6.2.2", - "firebase-admin": "^7.0.0", + "firebase-admin": "^8.3.0", "firebase-functions": "^3.2.0", "rlp": "^2.2.3", "twilio": "^3.23.2", diff --git a/packages/helm-charts/blockscout/Chart.yaml b/packages/helm-charts/blockscout/Chart.yaml index 0e50daced27..4d7e5860909 100644 --- a/packages/helm-charts/blockscout/Chart.yaml +++ b/packages/helm-charts/blockscout/Chart.yaml @@ -1,8 +1,8 @@ name: blockscout -version: 0.0.1 +version: 0.0.2 description: Chart which is used to deploy a blockscout setup for a celo testnet keywords: - ethereum - blockchain - blockscout -appVersion: v1.7.3 +appVersion: v2.0.4-beta diff --git a/packages/helm-charts/blockscout/templates/_helpers.tpl b/packages/helm-charts/blockscout/templates/_helpers.tpl index dbfaffba265..766228f4716 100644 --- a/packages/helm-charts/blockscout/templates/_helpers.tpl +++ b/packages/helm-charts/blockscout/templates/_helpers.tpl @@ -18,16 +18,16 @@ volumes: {{- end -}} {{- define "celo.blockscout-env-vars" -}} -- name: DB_USERNAME +- name: DATABASE_USER valueFrom: secretKeyRef: name: {{ .Release.Namespace }}-blockscout - key: DB_USERNAME -- name: DB_PASSWORD + key: DATABASE_USER +- name: DATABASE_PASSWORD valueFrom: secretKeyRef: name: {{ .Release.Namespace }}-blockscout - key: DB_PASSWORD + key: DATABASE_PASSWORD - name: NETWORK value: Celo - name: SUBNETWORK @@ -43,7 +43,17 @@ volumes: - name: ETHEREUM_JSONRPC_WS_URL value: {{ .Values.blockscout.jsonrpc_ws_url }} - name: DATABASE_URL - value: postgres://$(DB_USERNAME):$(DB_PASSWORD)@127.0.0.1:5432/{{ .Values.blockscout.db.name }} + value: postgres://$(DATABASE_USER):$(DATABASE_PASSWORD)@127.0.0.1:5432/{{ .Values.blockscout.db.name }} +- name: DATABASE_DB + value: {{ .Values.blockscout.db.name }} +- name: DATABASE_HOSTNAME + value: "127.0.0.1" +- name: DATABASE_PORT + value: "5432" +- name: MIX_ENV + value: prod +- name: LOGO + value: /images/celo_logo.svg {{- end -}} {{- define "celo.prom-to-sd-container" -}} diff --git a/packages/helm-charts/blockscout/templates/blockscout-indexer.deployment.yaml b/packages/helm-charts/blockscout/templates/blockscout-indexer.deployment.yaml index 6d84aa59ec7..296ca77ac08 100644 --- a/packages/helm-charts/blockscout/templates/blockscout-indexer.deployment.yaml +++ b/packages/helm-charts/blockscout/templates/blockscout-indexer.deployment.yaml @@ -24,8 +24,14 @@ spec: spec: containers: - name: blockscout-indexer - image: {{ .Values.blockscout.image.repository }}:{{ .Values.blockscout.image.indexerTag }} + image: {{ .Values.blockscout.image.repository }}:{{ .Values.blockscout.image.tag }} imagePullPolicy: {{ .Values.imagePullPolicy }} + command: + - /bin/sh + - -c + args: + - | + mix cmd --app indexer iex -S mix ports: - name: http containerPort: 4000 diff --git a/packages/helm-charts/blockscout/templates/blockscout-migration.job.yaml b/packages/helm-charts/blockscout/templates/blockscout-migration.job.yaml index 2169e49afc5..7dd939c20af 100644 --- a/packages/helm-charts/blockscout/templates/blockscout-migration.job.yaml +++ b/packages/helm-charts/blockscout/templates/blockscout-migration.job.yaml @@ -13,7 +13,7 @@ spec: spec: containers: - name: blockscout-web - image: {{ .Values.blockscout.image.repository }}:{{ .Values.blockscout.image.webTag }} + image: {{ .Values.blockscout.image.repository }}:{{ .Values.blockscout.image.tag }} imagePullPolicy: {{ .Values.imagePullPolicy }} command: ["/bin/sh"] args: ["-c", "echo Sleeping for 15; sleep 15; mix ecto.migrate"] diff --git a/packages/helm-charts/blockscout/templates/blockscout-web.deployment.yaml b/packages/helm-charts/blockscout/templates/blockscout-web.deployment.yaml index 985db64e0cd..e23b7c8e883 100644 --- a/packages/helm-charts/blockscout/templates/blockscout-web.deployment.yaml +++ b/packages/helm-charts/blockscout/templates/blockscout-web.deployment.yaml @@ -24,8 +24,14 @@ spec: spec: containers: - name: blockscout-web - image: {{ .Values.blockscout.image.repository }}:{{ .Values.blockscout.image.webTag }} + image: {{ .Values.blockscout.image.repository }}:{{ .Values.blockscout.image.tag }} imagePullPolicy: {{ .Values.imagePullPolicy }} + command: + - /bin/sh + - -c + args: + - | + mix cmd --app block_scout_web mix phx.server ports: - name: http containerPort: 4000 diff --git a/packages/helm-charts/blockscout/templates/blockscout.secret.yaml b/packages/helm-charts/blockscout/templates/blockscout.secret.yaml index 9b86c90e8b2..a08eb2a4e71 100644 --- a/packages/helm-charts/blockscout/templates/blockscout.secret.yaml +++ b/packages/helm-charts/blockscout/templates/blockscout.secret.yaml @@ -9,5 +9,5 @@ metadata: heritage: {{ .Release.Service }} type: Opaque data: - DB_USERNAME: {{ .Values.blockscout.db.username | b64enc | quote }} - DB_PASSWORD: {{ .Values.blockscout.db.password | b64enc | quote }} + DATABASE_USER: {{ .Values.blockscout.db.username | b64enc | quote }} + DATABASE_PASSWORD: {{ .Values.blockscout.db.password | b64enc | quote }} diff --git a/packages/helm-charts/blockscout/values.yaml b/packages/helm-charts/blockscout/values.yaml index 4b098d76570..98b11d8240d 100644 --- a/packages/helm-charts/blockscout/values.yaml +++ b/packages/helm-charts/blockscout/values.yaml @@ -7,8 +7,7 @@ promtosd: blockscout: image: repository: gcr.io/celo-testnet/blockscout - webTag: web - indexerTag: indexer + tag: v2.0.4-beta-celo db: # ip: must be provided at runtime # IP address of the postgres DB # connection_name: must be provided at runtime # name of the cloud sql connection diff --git a/packages/helm-charts/ethstats/values.yaml b/packages/helm-charts/ethstats/values.yaml index cc719bbcd7c..0228995fc9f 100644 --- a/packages/helm-charts/ethstats/values.yaml +++ b/packages/helm-charts/ethstats/values.yaml @@ -6,7 +6,7 @@ nodeSelector: {} ethstats: image: - repository: ethereumex/eth-stats-dashboard - tag: v0.0.1 + repository: gcr.io/celo-testnet/ethstats + tag: latest service: type: NodePort diff --git a/packages/mobile/.env b/packages/mobile/.env index 3b0e51934b2..c1bce8d0cd8 100644 --- a/packages/mobile/.env +++ b/packages/mobile/.env @@ -1,8 +1,10 @@ ENVIRONMENT=local -DEFAULT_TESTNET=integration -# -1 == ZeroSync, 5 == Ultralight, see src/geth/consts.ts for more info +DEFAULT_TESTNET=alfajores +# If ZERO_SYNC_ENABLED_INITIALLY, local geth will not run initially. +# If toggled on, it will use DEFAULT_SYNC_MODE. See src/geth/consts.ts for more info +ZERO_SYNC_ENABLED_INITIALLY=false DEFAULT_SYNC_MODE=5 DEV_SETTINGS_ACTIVE_INITIALLY=true FIREBASE_ENABLED=true SECRETS_KEY=debug -SHOW_TESTNET_BANNER=true +SHOW_TESTNET_BANNER=true \ No newline at end of file diff --git a/packages/mobile/.env.alfajores b/packages/mobile/.env.alfajores index b8c02dc9997..2f6e79d4622 100644 --- a/packages/mobile/.env.alfajores +++ b/packages/mobile/.env.alfajores @@ -1,6 +1,8 @@ ENVIRONMENT=alfajores DEFAULT_TESTNET=alfajores -# -1 == ZeroSync, 5 == Ultralight, see src/geth/consts.ts for more info +# If ZERO_SYNC_ENABLED_INITIALLY, local geth will not run initially. +# If toggled on, it will use DEFAULT_SYNC_MODE. See src/geth/consts.ts for more info +ZERO_SYNC_ENABLED_INITIALLY=false DEFAULT_SYNC_MODE=5 DEV_SETTINGS_ACTIVE_INITIALLY=false FIREBASE_ENABLED=true diff --git a/packages/mobile/.env.integration b/packages/mobile/.env.integration index 95cba3a10d9..381c53f3952 100644 --- a/packages/mobile/.env.integration +++ b/packages/mobile/.env.integration @@ -1,6 +1,8 @@ ENVIRONMENT=integration DEFAULT_TESTNET=integration -# -1 == ZeroSync, 5 == Ultralight, see src/geth/consts.ts for more info +# If ZERO_SYNC_ENABLED_INITIALLY, local geth will not run initially. +# If toggled on, it will use DEFAULT_SYNC_MODE. See src/geth/consts.ts for more info +ZERO_SYNC_ENABLED_INITIALLY=false DEFAULT_SYNC_MODE=5 DEV_SETTINGS_ACTIVE_INITIALLY=true FIREBASE_ENABLED=true diff --git a/packages/mobile/.env.pilot b/packages/mobile/.env.pilot index 8f8783f5dc8..ae5455d865f 100644 --- a/packages/mobile/.env.pilot +++ b/packages/mobile/.env.pilot @@ -1,6 +1,8 @@ ENVIRONMENT=pilot DEFAULT_TESTNET=pilot -# -1 == ZeroSync, 5 == Ultralight, see src/geth/consts.ts for more info +# If ZERO_SYNC_ENABLED_INITIALLY, local geth will not run initially. +# If toggled on, it will use DEFAULT_SYNC_MODE. See src/geth/consts.ts for more info +ZERO_SYNC_ENABLED_INITIALLY=false DEFAULT_SYNC_MODE=5 DEV_SETTINGS_ACTIVE_INITIALLY=false FIREBASE_ENABLED=true diff --git a/packages/mobile/.env.pilotstaging b/packages/mobile/.env.pilotstaging index f43a2095ea5..c60447aa725 100644 --- a/packages/mobile/.env.pilotstaging +++ b/packages/mobile/.env.pilotstaging @@ -1,6 +1,8 @@ ENVIRONMENT=pilotstaging DEFAULT_TESTNET=pilotstaging -# -1 == ZeroSync, 5 == Ultralight, see src/geth/consts.ts for more info +# If ZERO_SYNC_ENABLED_INITIALLY, local geth will not run initially. +# If toggled on, it will use DEFAULT_SYNC_MODE. See src/geth/consts.ts for more info +ZERO_SYNC_ENABLED_INITIALLY=false DEFAULT_SYNC_MODE=5 DEV_SETTINGS_ACTIVE_INITIALLY=true FIREBASE_ENABLED=true diff --git a/packages/mobile/.env.production b/packages/mobile/.env.production index 6f1313858b3..812aec9945e 100644 --- a/packages/mobile/.env.production +++ b/packages/mobile/.env.production @@ -1,6 +1,8 @@ ENVIRONMENT=production DEFAULT_TESTNET=argentinaproduction -# -1 == ZeroSync, 5 == Ultralight, see src/geth/consts.ts for more info +# If ZERO_SYNC_ENABLED_INITIALLY, local geth will not run initially. +# If toggled on, it will use DEFAULT_SYNC_MODE. See src/geth/consts.ts for more info +ZERO_SYNC_ENABLED_INITIALLY=false DEFAULT_SYNC_MODE=5 DEV_SETTINGS_ACTIVE_INITIALLY=false FIREBASE_ENABLED=true diff --git a/packages/mobile/.env.staging b/packages/mobile/.env.staging index e180a477050..6fff7365b86 100644 --- a/packages/mobile/.env.staging +++ b/packages/mobile/.env.staging @@ -1,6 +1,8 @@ ENVIRONMENT=staging DEFAULT_TESTNET=alfajoresstaging -# -1 == ZeroSync, 5 == Ultralight, see src/geth/consts.ts for more info +# If ZERO_SYNC_ENABLED_INITIALLY, local geth will not run initially. +# If toggled on, it will use DEFAULT_SYNC_MODE. See src/geth/consts.ts for more info +ZERO_SYNC_ENABLED_INITIALLY=false DEFAULT_SYNC_MODE=5 DEV_SETTINGS_ACTIVE_INITIALLY=true FIREBASE_ENABLED=true diff --git a/packages/mobile/.env.test b/packages/mobile/.env.test index ed12cba5665..83a8aac2028 100644 --- a/packages/mobile/.env.test +++ b/packages/mobile/.env.test @@ -1,6 +1,8 @@ ENVIRONMENT=local DEFAULT_TESTNET=integration -# -1 == ZeroSync, 5 == Ultralight, see src/geth/consts.ts for more info +# If ZERO_SYNC_ENABLED_INITIALLY, local geth will not run initially. +# If toggled on, it will use DEFAULT_SYNC_MODE. See src/geth/consts.ts for more info +ZERO_SYNC_ENABLED_INITIALLY=false DEFAULT_SYNC_MODE=5 DEV_SETTINGS_ACTIVE_INITIALLY=true FIREBASE_ENABLED=false diff --git a/packages/mobile/.gitignore b/packages/mobile/.gitignore index e90d10842e4..78474411445 100644 --- a/packages/mobile/.gitignore +++ b/packages/mobile/.gitignore @@ -83,5 +83,6 @@ android/app/src/debug/google-services.json android/app/src/pilot/google-services.json secrets.json android/sentry.properties +ios/**/GoogleService-Info*.plist ios/sentry.properties ios/Pods diff --git a/packages/mobile/README.md b/packages/mobile/README.md index c0660c6e384..f0e4ed53308 100644 --- a/packages/mobile/README.md +++ b/packages/mobile/README.md @@ -25,7 +25,7 @@ This makes Gradle faster: export GRADLE_OPTS='-Dorg.gradle.daemon=true -Dorg.gradle.parallel=true -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError"' ``` -## Running +## Running the App 1. If you haven't already, run `yarn` from the monorepo root to install dependencies. @@ -97,6 +97,12 @@ yarn run build-sdk TESTNET before rebuilding the app. Note that this will assume the testnets have a corresponding `/blockchain-api` and `/notification-service` set up. +### Running Wallet app in ZeroSync mode + +By default, the mobile wallet app runs geth in ultralight sync mode where all the epoch headers are fetched. The default sync mode is defined in [packages/mobile/.env](https://github.com/celo-org/celo-monorepo/blob/master/packages/mobile/.env#L4) file. + +To run wallet in zero sync mode, where it would connect to the remote nodes and sign transactions in web3, change the default sync mode in the aforementioned file to -1. The mode has only been tested on Android and is hard-coded to be [crash](https://github.com/celo-org/celo-monorepo/blob/aeddeefbfb230db51d2ef76d50c5f882644a1cd3/packages/mobile/src/web3/contracts.ts#L73) on iOS. + ## Testing To execute the suite of tests, run `yarn test` @@ -160,6 +166,11 @@ export CELO_RELEASE_KEY_PASSWORD=celoFakeReleaseKeyPass ### Building an APK or Bundle ```sh +# With fastlane: +bundle install +bundle exec fastlane android build_apk env:YOUR_BUILDING_VARIANT sdkEnv:YOUR_SDK_ENV + +# Or, manually cd android/ ./gradlew clean ./gradlew bundle{YOUR_BUILDING_VARIANT}JsAndAssets @@ -171,11 +182,47 @@ cd android/ Where `YOUR_BUILD_VARIANT` can be any of the app's build variants, such as debug or release. +## Configuring the SMS Retriever + +On android, the wallet app uses the SMS Retriever API to automatically input codes during phone number verification. + +The service that route SMS messages to the app needs to be configured to [append this app signature to the message][sms retriever]. Note, the signature will need to be computed using the signing key from the google play dashboard. + ## Generating GraphQL Types We're using [GraphQL Code Generator][graphql code generator] to properly type GraphQL queries. If you make a change to a query, run `yarn build:gen-graphql-types` to update the typings in the `typings` directory. +## How we handle Geth crashes in wallet app on Android + +Our Celo app has three types of codes. + +1. Javascript code - generated from Typescript, this runs in Javascript interpreter. +2. Java bytecode - This runs on Dalvik/Art Virtual Machine. +3. Native code - Geth code is written in Golang which compiles to native code - this runs directly on the + CPU, no virtual machines involved. + +One should note that, on iOS, there is no byte code and therefore, there are only two layers, one is the Javascript code, and the other is the Native code. Till now, we have been blind towards native crashes except Google Playstore logs. + +Sentry, the crash logging mechanism we use, can catch both Javascript Errors as well as unhandled Java exceptions. It, however, does not catch Native crashes. There are quite a few tools to catch native crashes like [Bugsnag](https://www.bugsnag.com) and [Crashlytics](https://firebase.google.com/products/crashlytics). They would have worked for us under normal circumstances. However, the Geth code produced by the Gomobile library and Go compiler logs a major chunk of information about the crash at Error level and not at the Fatal level. We hypothesize that this leads to incomplete stack traces showing up in Google Play store health checks. This issue is [publicly known](https://github.com/golang/go/issues/25035) but has not been fixed. + +We cannot use libraries like [Bugsnag](https://www.bugsnag.com) since they do not allow us to extract logcat logs immediately after the crash. Therefore, We use [jndcrash](https://github.com/ivanarh/jndcrash), which uses [ndcrash](https://github.com/ivanarh/ndcrash) and enable us to log the logcat logs immediately after a native crash. We capture the results into a file and on next restart Sentry reads it. We need to do this two-step setup because once a native crash happens, running code to upload the data would be fragile. An error in sentry looks like [this](https://sentry.io/organizations/celo/issues/918120991/events/48285729031/) + +Relevant code references: + +1. [NDKCrashService](https://github.com/celo-org/celo-monorepo/blob/master/packages/mobile/android/app/src/main/java/org/celo/mobile/NdkCrashService.java) +2. [Initialization](https://github.com/celo-org/celo-monorepo/blob/8689634a1d10d74ba6d4f3b36b2484db60a95bdb/packages/mobile/android/app/src/main/java/org/celo/mobile/MainApplication.java#L156) of the NDKCrashService +3. [Sentry code](https://github.com/celo-org/celo-monorepo/blob/799d74675dc09327543c210e88cbf5cc796721a0/packages/mobile/src/sentry/Sentry.ts#L53) to read NDK crash logs on restart + +There are two major differencs in ZeroSync mode: + +1. Geth won't run at all. The web3 would instead connect to -infura.celo-testnet.org using an https provider, for example, [https://integration-infura.celo-testnet.org](https://integration-infura.celo-testnet.org). +2. Changes to [sendTransactionAsyncWithWeb3Signing](https://github.com/celo-org/celo-monorepo/blob/8689634a1d10d74ba6d4f3b36b2484db60a95bdb/packages/walletkit/src/contract-utils.ts#L362) in walletkit to poll after sending a transaction for the transaction to succeed. This is needed because http provider, unliked web sockets and IPC provider, does not support subscriptions. + +#### Why http(s) provider? + +Websockets (`ws`) would have been a better choicee but we cannot use unencrypted `ws` provider since it would be bad to send plain-text data from a privacy perspective. Geth does not support `wss` by [default](https://github.com/ethereum/go-ethereum/issues/16423). And Kubernetes does not support it either. This forced us to use https provider. + ## Troubleshooting ### `Activity class {org.celo.mobile.staging/org.celo.mobile.MainActivity} does not exist.` @@ -218,3 +265,4 @@ $ adb kill-server && adb start-server [rntl-docs]: https://callstack.github.io/react-native-testing-library/ [jest]: https://jestjs.io/docs/en/snapshot-testing [redux-saga-test-plan]: https://github.com/jfairbank/redux-saga-test-plan +[sms retriever]: https://developers.google.com/identity/sms-retriever/verify#1_construct_a_verification_message diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle index 4b98974470a..b3e49f715e5 100644 --- a/packages/mobile/android/app/build.gradle +++ b/packages/mobile/android/app/build.gradle @@ -125,7 +125,7 @@ android { minSdkVersion isDetoxTestBuild ? rootProject.ext.minSdkVersion : 18 targetSdkVersion rootProject.ext.targetSdkVersion versionCode appVersionCode - versionName "1.5.0" + versionName "1.5.1" testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "build_config_package", "org.celo.mobile" diff --git a/packages/mobile/android/app/src/main/assets/custom/LicenseDisclaimer.txt b/packages/mobile/android/app/src/main/assets/custom/LicenseDisclaimer.txt index 31a2ff50d9b..23cd9ba806f 100644 --- a/packages/mobile/android/app/src/main/assets/custom/LicenseDisclaimer.txt +++ b/packages/mobile/android/app/src/main/assets/custom/LicenseDisclaimer.txt @@ -46,7 +46,7 @@ SOFTWARE. ----- -The following software may be included in this product: @ava/babel-plugin-throws-helper, append-transform, caching-transform, call-signature, currently-unhandled, find-cache-dir, last-line-stream, node-modules-regexp, option-chain, require-precompiled, unique-temp-dir. A copy of the source code may be downloaded from https://github.com/avajs/babel-plugin-throws-helper.git (@ava/babel-plugin-throws-helper), https://github.com/jamestalmage/append-transform.git (append-transform), https://github.com/jamestalmage/caching-transform.git (caching-transform), https://github.com/jamestalmage/call-signature.git (call-signature), https://github.com/jamestalmage/currently-unhandled.git (currently-unhandled), https://github.com/avajs/find-cache-dir.git (find-cache-dir), https://github.com/jamestalmage/last-line-stream.git (last-line-stream), https://github.com/jamestalmage/node-modules-regexp.git (node-modules-regexp), https://github.com/avajs/option-chain.git (option-chain), https://github.com/jamestalmage/require-precompiled.git (require-precompiled), https://github.com/jamestalmage/unique-temp-dir.git (unique-temp-dir). This software contains the following license and notice below: +The following software may be included in this product: @ava/babel-plugin-throws-helper, append-transform, caching-transform, call-signature, currently-unhandled, find-cache-dir, last-line-stream, node-modules-regexp, option-chain, require-precompiled, unique-temp-dir. A copy of the source code may be downloaded from https://github.com/avajs/babel-plugin-throws-helper.git (@ava/babel-plugin-throws-helper), https://github.com/jamestalmage/append-transform.git (append-transform), https://github.com/jamestalmage/caching-transform.git (caching-transform), https://github.com/jamestalmage/call-signature.git (call-signature), https://github.com/jamestalmage/currently-unhandled.git (currently-unhandled), https://github.com/jamestalmage/find-cache-dir.git (find-cache-dir), https://github.com/jamestalmage/last-line-stream.git (last-line-stream), https://github.com/jamestalmage/node-modules-regexp.git (node-modules-regexp), https://github.com/avajs/option-chain.git (option-chain), https://github.com/jamestalmage/require-precompiled.git (require-precompiled), https://github.com/jamestalmage/unique-temp-dir.git (unique-temp-dir). This software contains the following license and notice below: The MIT License (MIT) @@ -189,7 +189,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----- -The following software may be included in this product: @babel/parser, babylon. A copy of the source code may be downloaded from https://github.com/babel/babel/tree/master/packages/babel-parser (@babel/parser), https://github.com/babel/babel/tree/master/packages/babylon (babylon). This software contains the following license and notice below: +The following software may be included in this product: @babel/parser, babylon. A copy of the source code may be downloaded from https://github.com/babel/babel/tree/master/packages/babel-parser (@babel/parser), https://github.com/babel/babylon (babylon). This software contains the following license and notice below: Copyright (C) 2012-2014 by various contributors (see AUTHORS) @@ -584,7 +584,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ----- -The following software may be included in this product: @evocateur/npm-registry-fetch, cacache, figgy-pudding, make-fetch-happen, npm-pick-manifest, ssri. A copy of the source code may be downloaded from https://github.com/evocateur/npm-registry-fetch (@evocateur/npm-registry-fetch), https://github.com/zkat/cacache (cacache), https://github.com/zkat/figgy-pudding (figgy-pudding), https://github.com/zkat/make-fetch-happen (make-fetch-happen), https://github.com/npm/npm-pick-manifest (npm-pick-manifest), https://github.com/zkat/ssri (ssri). This software contains the following license and notice below: +The following software may be included in this product: @evocateur/npm-registry-fetch, cacache, figgy-pudding, make-fetch-happen, npm-pick-manifest, ssri. A copy of the source code may be downloaded from https://github.com/evocateur/npm-registry-fetch (@evocateur/npm-registry-fetch), https://github.com/npm/cacache (cacache), https://github.com/zkat/figgy-pudding (figgy-pudding), https://github.com/zkat/make-fetch-happen (make-fetch-happen), https://github.com/npm/npm-pick-manifest (npm-pick-manifest), https://github.com/zkat/ssri (ssri). This software contains the following license and notice below: ISC License @@ -836,7 +836,7 @@ Apache License ----- -The following software may be included in this product: @firebase/auth, @google-cloud/common, @google-cloud/common-grpc, @google-cloud/firestore, @google-cloud/logging, @google-cloud/monitoring, @google-cloud/paginator, @google-cloud/precise-date, @google-cloud/projectify, @google-cloud/promisify, @google-cloud/pubsub, @google-cloud/storage, @opencensus/core, @opencensus/propagation-stackdriver, @xtuc/long, ascli, bytebuffer, firebase-bolt, futoin-hkdf, gcp-metadata, google-auth-library, google-proto-files, long, protobufjs, spdx-correct, sumchecker, teeny-request, validate-npm-package-license, xcode. A copy of the source code may be downloaded from https://github.com/firebase/firebase-js-sdk.git (@firebase/auth), https://github.com/googleapis/nodejs-common.git (@google-cloud/common), https://github.com/googleapis/nodejs-common-grpc.git (@google-cloud/common-grpc), https://github.com/googleapis/nodejs-firestore.git (@google-cloud/firestore), https://github.com/googleapis/nodejs-logging.git (@google-cloud/logging), https://github.com/googleapis/nodejs-monitoring.git (@google-cloud/monitoring), https://github.com/googleapis/nodejs-paginator.git (@google-cloud/paginator), https://github.com/googleapis/nodejs-precise-date.git (@google-cloud/precise-date), https://github.com/googleapis/nodejs-projectify.git (@google-cloud/projectify), https://github.com/googleapis/nodejs-promisify.git (@google-cloud/promisify), https://github.com/googleapis/nodejs-pubsub.git (@google-cloud/pubsub), https://github.com/googleapis/nodejs-storage.git (@google-cloud/storage), https://github.com/census-instrumentation/opencensus-node.git (@opencensus/core), https://github.com/census-instrumentation/opencensus-node.git (@opencensus/propagation-stackdriver), https://github.com/dcodeIO/long.js.git (@xtuc/long), https://github.com/dcodeIO/ascli.git (ascli), https://github.com/dcodeIO/bytebuffer.js.git (bytebuffer), https://github.com/firebase/bolt.git (firebase-bolt), https://github.com/futoin/util-js-hkdf.git (futoin-hkdf), https://github.com/googleapis/gcp-metadata.git (gcp-metadata), https://github.com/googleapis/google-auth-library-nodejs.git (google-auth-library), https://github.com/googleapis/nodejs-proto-files.git (google-proto-files), https://github.com/dcodeIO/long.js.git (long), https://github.com/dcodeIO/protobuf.js.git (protobufjs), https://github.com/jslicense/spdx-correct.js.git (spdx-correct), git+https://github.com/malept/sumchecker.git (sumchecker), https://github.com/googleapis/teeny-request.git (teeny-request), https://github.com/kemitchell/validate-npm-package-license.js.git (validate-npm-package-license), https://github.com/apache/cordova-node-xcode.git (xcode). This software contains the following license and notice below: +The following software may be included in this product: @firebase/auth, @google-cloud/common, @google-cloud/firestore, @google-cloud/logging, @google-cloud/monitoring, @google-cloud/paginator, @google-cloud/precise-date, @google-cloud/projectify, @google-cloud/promisify, @google-cloud/pubsub, @google-cloud/storage, @opencensus/core, @opencensus/propagation-stackdriver, @xtuc/long, ascli, bytebuffer, firebase-bolt, futoin-hkdf, gaxios, gcp-metadata, google-auth-library, google-proto-files, long, protobufjs, spdx-correct, sumchecker, teeny-request, validate-npm-package-license, xcode. A copy of the source code may be downloaded from https://github.com/firebase/firebase-js-sdk.git (@firebase/auth), https://github.com/googleapis/nodejs-common.git (@google-cloud/common), https://github.com/googleapis/nodejs-firestore.git (@google-cloud/firestore), https://github.com/googleapis/nodejs-logging.git (@google-cloud/logging), https://github.com/googleapis/nodejs-monitoring.git (@google-cloud/monitoring), https://github.com/googleapis/nodejs-paginator.git (@google-cloud/paginator), https://github.com/googleapis/nodejs-precise-date.git (@google-cloud/precise-date), https://github.com/googleapis/nodejs-projectify.git (@google-cloud/projectify), https://github.com/googleapis/nodejs-promisify.git (@google-cloud/promisify), https://github.com/googleapis/nodejs-pubsub.git (@google-cloud/pubsub), https://github.com/googleapis/nodejs-storage.git (@google-cloud/storage), https://github.com/census-instrumentation/opencensus-node.git (@opencensus/core), https://github.com/census-instrumentation/opencensus-node.git (@opencensus/propagation-stackdriver), https://github.com/dcodeIO/long.js.git (@xtuc/long), https://github.com/dcodeIO/ascli.git (ascli), https://github.com/dcodeIO/bytebuffer.js.git (bytebuffer), https://github.com/firebase/bolt.git (firebase-bolt), https://github.com/futoin/util-js-hkdf.git (futoin-hkdf), https://github.com/JustinBeckwith/gaxios.git (gaxios), https://github.com/googleapis/gcp-metadata.git (gcp-metadata), https://github.com/googleapis/google-auth-library-nodejs.git (google-auth-library), https://github.com/googleapis/nodejs-proto-files.git (google-proto-files), https://github.com/dcodeIO/long.js.git (long), https://github.com/dcodeIO/protobuf.js.git (protobufjs), https://github.com/jslicense/spdx-correct.js.git (spdx-correct), git+https://github.com/malept/sumchecker.git (sumchecker), https://github.com/googleapis/teeny-request.git (teeny-request), https://github.com/kemitchell/validate-npm-package-license.js.git (validate-npm-package-license), https://github.com/apache/cordova-node-xcode.git (xcode). This software contains the following license and notice below: Apache License Version 2.0, January 2004 @@ -1772,7 +1772,7 @@ Apache License ----- -The following software may be included in this product: @lerna/add, @lerna/batch-packages, @lerna/bootstrap, @lerna/changed, @lerna/check-working-tree, @lerna/child-process, @lerna/clean, @lerna/cli, @lerna/collect-uncommitted, @lerna/collect-updates, @lerna/command, @lerna/conventional-commits, @lerna/create, @lerna/create-symlink, @lerna/describe-ref, @lerna/diff, @lerna/exec, @lerna/filter-options, @lerna/filter-packages, @lerna/get-npm-exec-opts, @lerna/get-packed, @lerna/github-client, @lerna/gitlab-client, @lerna/global-options, @lerna/has-npm-version, @lerna/import, @lerna/init, @lerna/link, @lerna/list, @lerna/listable, @lerna/log-packed, @lerna/npm-conf, @lerna/npm-dist-tag, @lerna/npm-install, @lerna/npm-publish, @lerna/npm-run-script, @lerna/otplease, @lerna/output, @lerna/pack-directory, @lerna/package, @lerna/package-graph, @lerna/prerelease-id-from-version, @lerna/project, @lerna/prompt, @lerna/publish, @lerna/pulse-till-done, @lerna/query-graph, @lerna/resolve-symlink, @lerna/rimraf-dir, @lerna/run, @lerna/run-lifecycle, @lerna/run-parallel-batches, @lerna/run-topologically, @lerna/symlink-binary, @lerna/symlink-dependencies, @lerna/timer, @lerna/validation-error, @lerna/version, @lerna/write-log-file, lerna. A copy of the source code may be downloaded from git+https://github.com/lerna/lerna.git (@lerna/add), git+https://github.com/lerna/lerna.git (@lerna/batch-packages), git+https://github.com/lerna/lerna.git (@lerna/bootstrap), git+https://github.com/lerna/lerna.git (@lerna/changed), git+https://github.com/lerna/lerna.git (@lerna/check-working-tree), git+https://github.com/lerna/lerna.git (@lerna/child-process), git+https://github.com/lerna/lerna.git (@lerna/clean), git+https://github.com/lerna/lerna.git (@lerna/cli), git+https://github.com/lerna/lerna.git (@lerna/collect-uncommitted), git+https://github.com/lerna/lerna.git (@lerna/collect-updates), git+https://github.com/lerna/lerna.git (@lerna/command), git+https://github.com/lerna/lerna.git (@lerna/conventional-commits), git+https://github.com/lerna/lerna.git (@lerna/create), git+https://github.com/lerna/lerna.git (@lerna/create-symlink), git+https://github.com/lerna/lerna.git (@lerna/describe-ref), git+https://github.com/lerna/lerna.git (@lerna/diff), git+https://github.com/lerna/lerna.git (@lerna/exec), git+https://github.com/lerna/lerna.git (@lerna/filter-options), git+https://github.com/lerna/lerna.git (@lerna/filter-packages), git+https://github.com/lerna/lerna.git (@lerna/get-npm-exec-opts), git+https://github.com/lerna/lerna.git (@lerna/get-packed), git+https://github.com/lerna/lerna.git (@lerna/github-client), git+https://gitlab.com/lerna/lerna.git (@lerna/gitlab-client), git+https://github.com/lerna/lerna.git (@lerna/global-options), git+https://github.com/lerna/lerna.git (@lerna/has-npm-version), git+https://github.com/lerna/lerna.git (@lerna/import), git+https://github.com/lerna/lerna.git (@lerna/init), git+https://github.com/lerna/lerna.git (@lerna/link), git+https://github.com/lerna/lerna.git (@lerna/list), git+https://github.com/lerna/lerna.git (@lerna/listable), git+https://github.com/lerna/lerna.git (@lerna/log-packed), git+https://github.com/lerna/lerna.git (@lerna/npm-conf), git+https://github.com/lerna/lerna.git (@lerna/npm-dist-tag), git+https://github.com/lerna/lerna.git (@lerna/npm-install), git+https://github.com/lerna/lerna.git (@lerna/npm-publish), git+https://github.com/lerna/lerna.git (@lerna/npm-run-script), git+https://github.com/lerna/lerna.git (@lerna/otplease), git+https://github.com/lerna/lerna.git (@lerna/output), git+https://github.com/lerna/lerna.git (@lerna/pack-directory), git+https://github.com/lerna/lerna.git (@lerna/package), git+https://github.com/lerna/lerna.git (@lerna/package-graph), git+https://github.com/lerna/lerna.git (@lerna/prerelease-id-from-version), git+https://github.com/lerna/lerna.git (@lerna/project), git+https://github.com/lerna/lerna.git (@lerna/prompt), git+https://github.com/lerna/lerna.git (@lerna/publish), git+https://github.com/lerna/lerna.git (@lerna/pulse-till-done), git+https://github.com/lerna/lerna.git (@lerna/query-graph), git+https://github.com/lerna/lerna.git (@lerna/resolve-symlink), git+https://github.com/lerna/lerna.git (@lerna/rimraf-dir), git+https://github.com/lerna/lerna.git (@lerna/run), git+https://github.com/lerna/lerna.git (@lerna/run-lifecycle), git+https://github.com/lerna/lerna.git (@lerna/run-parallel-batches), git+https://github.com/lerna/lerna.git (@lerna/run-topologically), git+https://github.com/lerna/lerna.git (@lerna/symlink-binary), git+https://github.com/lerna/lerna.git (@lerna/symlink-dependencies), git+https://github.com/lerna/lerna.git (@lerna/timer), git+https://github.com/lerna/lerna.git (@lerna/validation-error), git+https://github.com/lerna/lerna.git (@lerna/version), git+https://github.com/lerna/lerna.git (@lerna/write-log-file), git+https://github.com/lerna/lerna.git (lerna). This software contains the following license and notice below: +The following software may be included in this product: @lerna/add, @lerna/bootstrap, @lerna/changed, @lerna/check-working-tree, @lerna/child-process, @lerna/clean, @lerna/cli, @lerna/collect-uncommitted, @lerna/collect-updates, @lerna/command, @lerna/conventional-commits, @lerna/create, @lerna/create-symlink, @lerna/describe-ref, @lerna/diff, @lerna/exec, @lerna/filter-options, @lerna/filter-packages, @lerna/get-npm-exec-opts, @lerna/get-packed, @lerna/github-client, @lerna/gitlab-client, @lerna/global-options, @lerna/has-npm-version, @lerna/import, @lerna/init, @lerna/link, @lerna/list, @lerna/listable, @lerna/log-packed, @lerna/npm-conf, @lerna/npm-dist-tag, @lerna/npm-install, @lerna/npm-publish, @lerna/npm-run-script, @lerna/otplease, @lerna/output, @lerna/pack-directory, @lerna/package, @lerna/package-graph, @lerna/prerelease-id-from-version, @lerna/project, @lerna/prompt, @lerna/publish, @lerna/pulse-till-done, @lerna/query-graph, @lerna/resolve-symlink, @lerna/rimraf-dir, @lerna/run, @lerna/run-lifecycle, @lerna/run-topologically, @lerna/symlink-binary, @lerna/symlink-dependencies, @lerna/timer, @lerna/validation-error, @lerna/version, @lerna/write-log-file, lerna. A copy of the source code may be downloaded from git+https://github.com/lerna/lerna.git (@lerna/add), git+https://github.com/lerna/lerna.git (@lerna/bootstrap), git+https://github.com/lerna/lerna.git (@lerna/changed), git+https://github.com/lerna/lerna.git (@lerna/check-working-tree), git+https://github.com/lerna/lerna.git (@lerna/child-process), git+https://github.com/lerna/lerna.git (@lerna/clean), git+https://github.com/lerna/lerna.git (@lerna/cli), git+https://github.com/lerna/lerna.git (@lerna/collect-uncommitted), git+https://github.com/lerna/lerna.git (@lerna/collect-updates), git+https://github.com/lerna/lerna.git (@lerna/command), git+https://github.com/lerna/lerna.git (@lerna/conventional-commits), git+https://github.com/lerna/lerna.git (@lerna/create), git+https://github.com/lerna/lerna.git (@lerna/create-symlink), git+https://github.com/lerna/lerna.git (@lerna/describe-ref), git+https://github.com/lerna/lerna.git (@lerna/diff), git+https://github.com/lerna/lerna.git (@lerna/exec), git+https://github.com/lerna/lerna.git (@lerna/filter-options), git+https://github.com/lerna/lerna.git (@lerna/filter-packages), git+https://github.com/lerna/lerna.git (@lerna/get-npm-exec-opts), git+https://github.com/lerna/lerna.git (@lerna/get-packed), git+https://github.com/lerna/lerna.git (@lerna/github-client), git+https://gitlab.com/lerna/lerna.git (@lerna/gitlab-client), git+https://github.com/lerna/lerna.git (@lerna/global-options), git+https://github.com/lerna/lerna.git (@lerna/has-npm-version), git+https://github.com/lerna/lerna.git (@lerna/import), git+https://github.com/lerna/lerna.git (@lerna/init), git+https://github.com/lerna/lerna.git (@lerna/link), git+https://github.com/lerna/lerna.git (@lerna/list), git+https://github.com/lerna/lerna.git (@lerna/listable), git+https://github.com/lerna/lerna.git (@lerna/log-packed), git+https://github.com/lerna/lerna.git (@lerna/npm-conf), git+https://github.com/lerna/lerna.git (@lerna/npm-dist-tag), git+https://github.com/lerna/lerna.git (@lerna/npm-install), git+https://github.com/lerna/lerna.git (@lerna/npm-publish), git+https://github.com/lerna/lerna.git (@lerna/npm-run-script), git+https://github.com/lerna/lerna.git (@lerna/otplease), git+https://github.com/lerna/lerna.git (@lerna/output), git+https://github.com/lerna/lerna.git (@lerna/pack-directory), git+https://github.com/lerna/lerna.git (@lerna/package), git+https://github.com/lerna/lerna.git (@lerna/package-graph), git+https://github.com/lerna/lerna.git (@lerna/prerelease-id-from-version), git+https://github.com/lerna/lerna.git (@lerna/project), git+https://github.com/lerna/lerna.git (@lerna/prompt), git+https://github.com/lerna/lerna.git (@lerna/publish), git+https://github.com/lerna/lerna.git (@lerna/pulse-till-done), git+https://github.com/lerna/lerna.git (@lerna/query-graph), git+https://github.com/lerna/lerna.git (@lerna/resolve-symlink), git+https://github.com/lerna/lerna.git (@lerna/rimraf-dir), git+https://github.com/lerna/lerna.git (@lerna/run), git+https://github.com/lerna/lerna.git (@lerna/run-lifecycle), git+https://github.com/lerna/lerna.git (@lerna/run-topologically), git+https://github.com/lerna/lerna.git (@lerna/symlink-binary), git+https://github.com/lerna/lerna.git (@lerna/symlink-dependencies), git+https://github.com/lerna/lerna.git (@lerna/timer), git+https://github.com/lerna/lerna.git (@lerna/validation-error), git+https://github.com/lerna/lerna.git (@lerna/version), git+https://github.com/lerna/lerna.git (@lerna/write-log-file), git+https://github.com/lerna/lerna.git (lerna). This software contains the following license and notice below: Copyright (c) 2015-present Lerna Contributors @@ -1932,6 +1932,18 @@ THE SOFTWARE. ----- +The following software may be included in this product: @octokit/types. A copy of the source code may be downloaded from https://github.com/octokit/types.ts. This software contains the following license and notice below: + +MIT License Copyright (c) 2019 Octokit contributors + +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 (including the next paragraph) 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. + +----- + The following software may be included in this product: @protobufjs/aspromise, @protobufjs/base64, @protobufjs/codegen, @protobufjs/eventemitter, @protobufjs/fetch, @protobufjs/float, @protobufjs/inquire, @protobufjs/path, @protobufjs/pool, @protobufjs/utf8. A copy of the source code may be downloaded from https://github.com/dcodeIO/protobuf.js.git (@protobufjs/aspromise), https://github.com/dcodeIO/protobuf.js.git (@protobufjs/base64), https://github.com/dcodeIO/protobuf.js.git (@protobufjs/codegen), https://github.com/dcodeIO/protobuf.js.git (@protobufjs/eventemitter), https://github.com/dcodeIO/protobuf.js.git (@protobufjs/fetch), https://github.com/dcodeIO/protobuf.js.git (@protobufjs/float), https://github.com/dcodeIO/protobuf.js.git (@protobufjs/inquire), https://github.com/dcodeIO/protobuf.js.git (@protobufjs/path), https://github.com/dcodeIO/protobuf.js.git (@protobufjs/pool), https://github.com/dcodeIO/protobuf.js.git (@protobufjs/utf8). This software contains the following license and notice below: Copyright (c) 2016, Daniel Wirtz All rights reserved. @@ -2589,7 +2601,7 @@ SOFTWARE. ----- -The following software may be included in this product: @types/accepts, @types/airtable, @types/anymatch, @types/async-polling, @types/babel__core, @types/babel__generator, @types/babel__template, @types/babel__traverse, @types/babel-types, @types/babylon, @types/bignumber.js, @types/bip32, @types/bip39, @types/bn.js, @types/body-parser, @types/bytebuffer, @types/caseless, @types/chai, @types/cheerio, @types/cli-table, @types/connect, @types/cookiejar, @types/cookies, @types/cors, @types/country-data, @types/debug, @types/dotenv, @types/duplexify, @types/elliptic, @types/enzyme, @types/enzyme-adapter-react-16, @types/eth-lightwallet, @types/ethereum-protocol, @types/ethereumjs-util, @types/events, @types/express, @types/express-serve-static-core, @types/fs-capacitor, @types/fs-extra, @types/glob, @types/google-libphonenumber, @types/graphql, @types/graphql-upload, @types/hdkey, @types/hoist-non-react-statics, @types/http-assert, @types/i18next, @types/invariant, @types/is-glob, @types/isomorphic-fetch, @types/istanbul-lib-coverage, @types/istanbul-lib-report, @types/istanbul-reports, @types/jest, @types/jest-diff, @types/koa, @types/koa-compose, @types/lodash, @types/lodash.zipobject, @types/long, @types/mailgun-js, @types/mathjs, @types/mime, @types/minimatch, @types/mkdirp, @types/mocha, @types/next, @types/next-server, @types/node, @types/node-fetch, @types/nodemailer, @types/normalize-package-data, @types/p-defer, @types/prettier, @types/prompts, @types/prop-types, @types/q, @types/qs, @types/range-parser, @types/react, @types/react-autosuggest, @types/react-css-modules, @types/react-google-recaptcha, @types/react-loadable, @types/react-native, @types/react-native-autocomplete-input, @types/react-native-fs, @types/react-native-keep-awake, @types/react-redux, @types/react-test-renderer, @types/redux-mock-store, @types/request, @types/resolve, @types/serve-static, @types/solidity-parser-antlr, @types/source-list-map, @types/stack-utils, @types/superagent, @types/supertest, @types/tapable, @types/tough-cookie, @types/twilio, @types/uglify-js, @types/underscore, @types/utf8, @types/uuid-js, @types/web3, @types/web3-provider-engine, @types/webpack, @types/webpack-sources, @types/ws, @types/yargs, @types/yargs-parser, @types/zen-observable. A copy of the source code may be downloaded from https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/accepts), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/airtable), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/anymatch), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/async-polling), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babel__core), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babel__generator), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babel__template), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babel__traverse), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babel-types), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babylon), https://github.com/MikeMcl/bignumber.js/ (@types/bignumber.js), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/bip32), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/bip39), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/bn.js), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/body-parser), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/bytebuffer), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/caseless), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/chai), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/cheerio), https://github.com/DefinitelyTyped/DefinitelyTyped.git.git (@types/cli-table), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/connect), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/cookiejar), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/cookies), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/cors), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/country-data), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/debug), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/dotenv), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/duplexify), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/elliptic), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/enzyme), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/enzyme-adapter-react-16), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/eth-lightwallet), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/ethereum-protocol), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/ethereumjs-util), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/events), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/express), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/express-serve-static-core), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/fs-capacitor), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/fs-extra), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/glob), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/google-libphonenumber), https://github.com/graphql/graphql-js (@types/graphql), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/graphql-upload), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/hdkey), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/hoist-non-react-statics), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/http-assert), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/i18next), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/invariant), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/is-glob), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/isomorphic-fetch), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/istanbul-lib-coverage), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/istanbul-lib-report), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/istanbul-reports), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/jest), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/jest-diff), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/koa), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/koa-compose), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/lodash), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/lodash.zipobject), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/long), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/mailgun-js), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/mathjs), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/mime), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/minimatch), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/mkdirp), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/mocha), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/next), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/next-server), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/node), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/node-fetch), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/nodemailer), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/normalize-package-data), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/p-defer), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/prettier), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/prompts), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/prop-types), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/q), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/qs), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/range-parser), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-autosuggest), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-css-modules), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-google-recaptcha), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-loadable), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-native), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-native-autocomplete-input), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-native-fs), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-native-keep-awake), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-redux), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-test-renderer), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/redux-mock-store), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/request), https://github.com/DefinitelyTyped/DefinitelyTyped.git.git (@types/resolve), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/serve-static), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/solidity-parser-antlr), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/source-list-map), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/stack-utils), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/superagent), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/supertest), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/tapable), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/tough-cookie), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/twilio), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/uglify-js), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/underscore), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/utf8), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/uuid-js), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/web3), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/web3-provider-engine), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/webpack), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/webpack-sources), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/ws), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/yargs), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/yargs-parser), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/zen-observable). This software contains the following license and notice below: +The following software may be included in this product: @types/accepts, @types/airtable, @types/anymatch, @types/async-polling, @types/babel__core, @types/babel__generator, @types/babel__template, @types/babel__traverse, @types/babel-types, @types/babylon, @types/bignumber.js, @types/bip32, @types/bip39, @types/bn.js, @types/body-parser, @types/bytebuffer, @types/caseless, @types/chai, @types/cheerio, @types/cli-table, @types/connect, @types/cookiejar, @types/cookies, @types/cors, @types/country-data, @types/debug, @types/dotenv, @types/duplexify, @types/elliptic, @types/enzyme, @types/enzyme-adapter-react-16, @types/eth-lightwallet, @types/ethereum-protocol, @types/ethereumjs-util, @types/events, @types/express, @types/express-serve-static-core, @types/fs-capacitor, @types/fs-extra, @types/glob, @types/google-libphonenumber, @types/graphql, @types/graphql-upload, @types/hdkey, @types/hoist-non-react-statics, @types/http-assert, @types/i18next, @types/invariant, @types/is-glob, @types/isomorphic-fetch, @types/istanbul-lib-coverage, @types/istanbul-lib-report, @types/istanbul-reports, @types/jest, @types/jest-diff, @types/koa, @types/koa-compose, @types/lodash, @types/lodash.zipobject, @types/long, @types/mailgun-js, @types/mathjs, @types/mime, @types/minimatch, @types/mkdirp, @types/mocha, @types/next, @types/next-server, @types/node, @types/node-fetch, @types/nodemailer, @types/normalize-package-data, @types/p-defer, @types/prettier, @types/prompts, @types/prop-types, @types/q, @types/qs, @types/range-parser, @types/react, @types/react-autosuggest, @types/react-css-modules, @types/react-google-recaptcha, @types/react-loadable, @types/react-native, @types/react-native-autocomplete-input, @types/react-native-fs, @types/react-native-keep-awake, @types/react-redux, @types/react-test-renderer, @types/redux-mock-store, @types/request, @types/resolve, @types/serve-static, @types/solidity-parser-antlr, @types/source-list-map, @types/stack-utils, @types/superagent, @types/supertest, @types/tapable, @types/tough-cookie, @types/twilio, @types/uglify-js, @types/underscore, @types/utf8, @types/uuid-js, @types/web3, @types/web3-provider-engine, @types/webpack, @types/webpack-sources, @types/ws, @types/yargs, @types/yargs-parser, @types/zen-observable. A copy of the source code may be downloaded from https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/accepts), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/airtable), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/anymatch), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/async-polling), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babel__core), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babel__generator), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babel__template), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babel__traverse), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babel-types), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/babylon), https://github.com/MikeMcl/bignumber.js/ (@types/bignumber.js), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/bip32), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/bip39), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/bn.js), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/body-parser), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/bytebuffer), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/caseless), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/chai), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/cheerio), https://github.com/DefinitelyTyped/DefinitelyTyped.git.git (@types/cli-table), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/connect), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/cookiejar), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/cookies), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/cors), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/country-data), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/debug), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/dotenv), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/duplexify), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/elliptic), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/enzyme), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/enzyme-adapter-react-16), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/eth-lightwallet), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/ethereum-protocol), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/ethereumjs-util), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/events), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/express), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/express-serve-static-core), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/fs-capacitor), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/fs-extra), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/glob), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/google-libphonenumber), https://github.com/graphql/graphql-js (@types/graphql), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/graphql-upload), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/hdkey), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/hoist-non-react-statics), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/http-assert), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/i18next), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/invariant), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/is-glob), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/isomorphic-fetch), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/istanbul-lib-coverage), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/istanbul-lib-report), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/istanbul-reports), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/jest), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/jest-diff), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/koa), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/koa-compose), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/lodash), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/lodash.zipobject), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/long), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/mailgun-js), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/mathjs), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/mime), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/minimatch), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/mkdirp), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/mocha), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/next), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/next-server), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/node), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/node-fetch), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/nodemailer), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/normalize-package-data), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/p-defer), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/prettier), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/prompts), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/prop-types), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/q), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/qs), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/range-parser), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-autosuggest), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-css-modules), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-google-recaptcha), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-loadable), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-native), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-native-autocomplete-input), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-native-fs), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-native-keep-awake), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-redux), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react-test-renderer), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/redux-mock-store), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/request), https://github.com/DefinitelyTyped/DefinitelyTyped.git.git (@types/resolve), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/serve-static), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/solidity-parser-antlr), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/source-list-map), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/stack-utils), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/superagent), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/supertest), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/tapable), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/tough-cookie), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/twilio), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/uglify-js), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/underscore), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/utf8), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/uuid-js), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/web3), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/web3-provider-engine), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/webpack), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/webpack-sources), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/ws), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/yargs), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/yargs-parser), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/zen-observable). This software contains the following license and notice below: MIT License @@ -4017,7 +4029,7 @@ THE SOFTWARE. ----- -The following software may be included in this product: acorn-jsx. A copy of the source code may be downloaded from https://github.com/RReverser/acorn-jsx. This software contains the following license and notice below: +The following software may be included in this product: acorn-jsx. A copy of the source code may be downloaded from https://github.com/acornjs/acorn-jsx. This software contains the following license and notice below: Copyright (C) 2012-2017 by Ingvar Stepanyan @@ -4224,7 +4236,7 @@ SOFTWARE. ----- -The following software may be included in this product: align-text, ansi-wrap, assign-symbols, center-align, contains-path, define-property, dotdir-regex, es6-template-regex, extglob, glob-base, glob-fs, glob-fs-dotfiles, is-accessor-descriptor, is-data-descriptor, is-dotdir, is-equal-shallow, is-extendable, is-invalid-path, is-windows, lazy-cache, noncharacters, object-visit, parse-gitignore, parse-glob, pascalcase, plugin-error, right-align, shallow-clone, unc-path-regex, window-size. A copy of the source code may be downloaded from git://github.com/jonschlinkert/align-text.git (align-text), https://github.com/jonschlinkert/ansi-wrap.git (ansi-wrap), https://github.com/jonschlinkert/assign-symbols.git (assign-symbols), https://github.com/jonschlinkert/center-align.git (center-align), https://github.com/jonschlinkert/contains-path.git (contains-path), https://github.com/jonschlinkert/define-property.git (define-property), https://github.com/regexps/dotdir-regex.git (dotdir-regex), https://github.com/jonschlinkert/es6-template-regex.git (es6-template-regex), git://github.com/jonschlinkert/extglob.git (extglob), git://github.com/jonschlinkert/glob-base.git (glob-base), https://github.com/jonschlinkert/glob-fs.git (glob-fs), https://github.com/jonschlinkert/glob-fs-dotfiles.git (glob-fs-dotfiles), https://github.com/jonschlinkert/is-accessor-descriptor.git (is-accessor-descriptor), https://github.com/jonschlinkert/is-data-descriptor.git (is-data-descriptor), https://github.com/jonschlinkert/is-dotdir.git (is-dotdir), git://github.com/jonschlinkert/is-equal-shallow.git (is-equal-shallow), https://github.com/jonschlinkert/is-extendable.git (is-extendable), git://github.com/jonschlinkert/is-invalid-path.git (is-invalid-path), https://github.com/jonschlinkert/is-windows.git (is-windows), git://github.com/jonschlinkert/lazy-cache.git (lazy-cache), git://github.com/jonschlinkert/noncharacters.git (noncharacters), https://github.com/jonschlinkert/object-visit.git (object-visit), https://github.com/jonschlinkert/parse-gitignore.git (parse-gitignore), https://github.com/jonschlinkert/parse-glob.git (parse-glob), https://github.com/jonschlinkert/pascalcase.git (pascalcase), git://github.com/jonschlinkert/plugin-error.git (plugin-error), git://github.com/jonschlinkert/right-align.git (right-align), https://github.com/jonschlinkert/shallow-clone.git (shallow-clone), https://github.com/regexhq/unc-path-regex.git (unc-path-regex), https://github.com/jonschlinkert/window-size.git (window-size). This software contains the following license and notice below: +The following software may be included in this product: align-text, ansi-wrap, assign-symbols, center-align, contains-path, define-property, dotdir-regex, es6-template-regex, extglob, glob-base, glob-fs, glob-fs-dotfiles, is-accessor-descriptor, is-data-descriptor, is-dotdir, is-equal-shallow, is-extendable, is-invalid-path, is-windows, lazy-cache, noncharacters, object-visit, parse-gitignore, parse-glob, pascalcase, plugin-error, right-align, shallow-clone, unc-path-regex, window-size. A copy of the source code may be downloaded from git://github.com/jonschlinkert/align-text.git (align-text), https://github.com/jonschlinkert/ansi-wrap.git (ansi-wrap), https://github.com/jonschlinkert/assign-symbols.git (assign-symbols), https://github.com/jonschlinkert/center-align.git (center-align), https://github.com/jonschlinkert/contains-path.git (contains-path), https://github.com/jonschlinkert/define-property.git (define-property), https://github.com/regexps/dotdir-regex.git (dotdir-regex), https://github.com/jonschlinkert/es6-template-regex.git (es6-template-regex), git://github.com/jonschlinkert/extglob.git (extglob), git://github.com/jonschlinkert/glob-base.git (glob-base), https://github.com/jonschlinkert/glob-fs.git (glob-fs), https://github.com/jonschlinkert/glob-fs-dotfiles.git (glob-fs-dotfiles), https://github.com/jonschlinkert/is-accessor-descriptor.git (is-accessor-descriptor), https://github.com/jonschlinkert/is-data-descriptor.git (is-data-descriptor), https://github.com/jonschlinkert/is-dotdir.git (is-dotdir), git://github.com/jonschlinkert/is-equal-shallow.git (is-equal-shallow), https://github.com/jonschlinkert/is-extendable.git (is-extendable), git://github.com/jonschlinkert/is-invalid-path.git (is-invalid-path), https://github.com/jonschlinkert/is-windows.git (is-windows), https://github.com/jonschlinkert/lazy-cache.git (lazy-cache), git://github.com/jonschlinkert/noncharacters.git (noncharacters), https://github.com/jonschlinkert/object-visit.git (object-visit), https://github.com/jonschlinkert/parse-gitignore.git (parse-gitignore), https://github.com/jonschlinkert/parse-glob.git (parse-glob), https://github.com/jonschlinkert/pascalcase.git (pascalcase), git://github.com/jonschlinkert/plugin-error.git (plugin-error), git://github.com/jonschlinkert/right-align.git (right-align), https://github.com/jonschlinkert/shallow-clone.git (shallow-clone), https://github.com/regexhq/unc-path-regex.git (unc-path-regex), https://github.com/jonschlinkert/window-size.git (window-size). This software contains the following license and notice below: The MIT License (MIT) @@ -4570,7 +4582,7 @@ The following software may be included in this product: ansi-colors. A copy of t The MIT License (MIT) -Copyright (c) 2015-present, Brian Woodward. +Copyright (c) 2015-2017, Brian Woodward. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -4596,7 +4608,7 @@ The following software may be included in this product: ansi-colors. A copy of t The MIT License (MIT) -Copyright (c) 2015-2017, Brian Woodward. +Copyright (c) 2015-present, Brian Woodward. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -5118,7 +5130,7 @@ OTHER DEALINGS IN THE SOFTWARE. ----- -The following software may be included in this product: archy, array-map, array-reduce, atob-lite, btoa-lite, buffer-equal, camelize, concat-map, dasherize, deep-equal, defined, ent, fast-json-stable-stringify, github-from-package, is-typedarray, json-stable-stringify, json-stable-stringify-without-jsonify, minimist, object-inspect, path-browserify, resolve, resumer, safe-regex, semver-compare, snakeize, text-table, tty-browserify, vm-browserify, wordwrap. A copy of the source code may be downloaded from http://github.com/substack/node-archy.git (archy), git://github.com/substack/array-map.git (array-map), git://github.com/substack/array-reduce.git (array-reduce), git://github.com/hughsk/atob-lite.git (atob-lite), git://github.com/hughsk/btoa-lite.git (btoa-lite), git://github.com/substack/node-buffer-equal.git (buffer-equal), git://github.com/substack/camelize.git (camelize), git://github.com/substack/node-concat-map.git (concat-map), git://github.com/shahata/dasherize.git (dasherize), http://github.com/substack/node-deep-equal.git (deep-equal), git://github.com/substack/defined.git (defined), https://github.com/substack/node-ent.git (ent), git://github.com/epoberezkin/fast-json-stable-stringify.git (fast-json-stable-stringify), git://github.com/substack/github-from-package.git (github-from-package), git://github.com/hughsk/is-typedarray.git (is-typedarray), git://github.com/substack/json-stable-stringify.git (json-stable-stringify), git://github.com/samn/json-stable-stringify.git (json-stable-stringify-without-jsonify), git://github.com/substack/minimist.git (minimist), git://github.com/substack/object-inspect.git (object-inspect), git://github.com/browserify/path-browserify.git (path-browserify), git://github.com/substack/node-resolve.git (resolve), git://github.com/substack/resumer.git (resumer), git://github.com/substack/safe-regex.git (safe-regex), git://github.com/substack/semver-compare.git (semver-compare), git://github.com/nathan7/snakeize.git (snakeize), git://github.com/substack/text-table.git (text-table), git://github.com/substack/tty-browserify.git (tty-browserify), http://github.com/substack/vm-browserify.git (vm-browserify), git://github.com/substack/node-wordwrap.git (wordwrap). This software contains the following license and notice below: +The following software may be included in this product: archy, array-map, array-reduce, atob-lite, btoa-lite, buffer-equal, camelize, concat-map, dasherize, deep-equal, defined, ent, fast-json-stable-stringify, github-from-package, is-typedarray, json-stable-stringify, json-stable-stringify-without-jsonify, minimist, object-inspect, path-browserify, resolve, resumer, safe-regex, semver-compare, snakeize, text-table, tty-browserify, vm-browserify, wordwrap. A copy of the source code may be downloaded from http://github.com/substack/node-archy.git (archy), git://github.com/substack/array-map.git (array-map), git://github.com/substack/array-reduce.git (array-reduce), git://github.com/hughsk/atob-lite.git (atob-lite), git://github.com/hughsk/btoa-lite.git (btoa-lite), git://github.com/substack/node-buffer-equal.git (buffer-equal), git://github.com/substack/camelize.git (camelize), git://github.com/substack/node-concat-map.git (concat-map), git://github.com/shahata/dasherize.git (dasherize), http://github.com/substack/node-deep-equal.git (deep-equal), git://github.com/substack/defined.git (defined), https://github.com/substack/node-ent.git (ent), git://github.com/epoberezkin/fast-json-stable-stringify.git (fast-json-stable-stringify), git://github.com/substack/github-from-package.git (github-from-package), git://github.com/hughsk/is-typedarray.git (is-typedarray), git://github.com/substack/json-stable-stringify.git (json-stable-stringify), git://github.com/samn/json-stable-stringify.git (json-stable-stringify-without-jsonify), git://github.com/substack/minimist.git (minimist), git://github.com/substack/object-inspect.git (object-inspect), git://github.com/substack/path-browserify.git (path-browserify), git://github.com/browserify/resolve.git (resolve), git://github.com/substack/resumer.git (resumer), git://github.com/substack/safe-regex.git (safe-regex), git://github.com/substack/semver-compare.git (semver-compare), git://github.com/nathan7/snakeize.git (snakeize), git://github.com/substack/text-table.git (text-table), git://github.com/substack/tty-browserify.git (tty-browserify), http://github.com/substack/vm-browserify.git (vm-browserify), git://github.com/substack/node-wordwrap.git (wordwrap). This software contains the following license and notice below: This software is released under the MIT license: @@ -5264,40 +5276,11 @@ SOFTWARE. ----- -The following software may be included in this product: arr-diff. A copy of the source code may be downloaded from https://github.com/jonschlinkert/arr-diff.git. This software contains the following license and notice below: +The following software may be included in this product: arr-diff, arr-union, array-unique, extend-shallow, get-value, is-extglob, is-glob, is-number, is-primitive, isobject, longest, micromatch, mixin-object, object.omit, parse-filepath, relative, set-value, write. A copy of the source code may be downloaded from https://github.com/jonschlinkert/arr-diff.git (arr-diff), git://github.com/jonschlinkert/arr-union.git (arr-union), git://github.com/jonschlinkert/array-unique.git (array-unique), git://github.com/jonschlinkert/extend-shallow.git (extend-shallow), https://github.com/jonschlinkert/get-value.git (get-value), https://github.com/jonschlinkert/is-extglob.git (is-extglob), https://github.com/jonschlinkert/is-glob.git (is-glob), https://github.com/jonschlinkert/is-number.git (is-number), git://github.com/jonschlinkert/is-primitive.git (is-primitive), git://github.com/jonschlinkert/isobject.git (isobject), https://github.com/jonschlinkert/longest.git (longest), https://github.com/jonschlinkert/micromatch.git (micromatch), https://github.com/jonschlinkert/mixin-object.git (mixin-object), git://github.com/jonschlinkert/object.omit.git (object.omit), https://github.com/jonschlinkert/parse-filepath.git (parse-filepath), https://github.com/jonschlinkert/relative.git (relative), git://github.com/jonschlinkert/set-value.git (set-value), https://github.com/jonschlinkert/write.git (write). This software contains the following license and notice below: The MIT License (MIT) -Copyright (c) 2014-2015 Jon Schlinkert. - -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. - ------ - -The following software may be included in this product: arr-diff, clone-deep, fill-range, for-in, has-value, has-values, kind-of, normalize-path, set-value. A copy of the source code may be downloaded from https://github.com/jonschlinkert/arr-diff.git (arr-diff), https://github.com/jonschlinkert/clone-deep.git (clone-deep), https://github.com/jonschlinkert/fill-range.git (fill-range), https://github.com/jonschlinkert/for-in.git (for-in), https://github.com/jonschlinkert/has-value.git (has-value), https://github.com/jonschlinkert/has-values.git (has-values), https://github.com/jonschlinkert/kind-of.git (kind-of), https://github.com/jonschlinkert/normalize-path.git (normalize-path), https://github.com/jonschlinkert/set-value.git (set-value). This software contains the following license and notice below: - -The MIT License (MIT) - -Copyright (c) 2014-2017, Jon Schlinkert +Copyright (c) 2014-2015, Jon Schlinkert. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -5319,11 +5302,11 @@ THE SOFTWARE. ----- -The following software may be included in this product: arr-diff, arr-union, array-unique, extend-shallow, get-value, is-extglob, is-glob, is-number, is-primitive, isobject, longest, micromatch, mixin-object, object.omit, parse-filepath, relative, set-value, write. A copy of the source code may be downloaded from https://github.com/jonschlinkert/arr-diff.git (arr-diff), git://github.com/jonschlinkert/arr-union.git (arr-union), git://github.com/jonschlinkert/array-unique.git (array-unique), https://github.com/jonschlinkert/extend-shallow.git (extend-shallow), https://github.com/jonschlinkert/get-value.git (get-value), https://github.com/jonschlinkert/is-extglob.git (is-extglob), https://github.com/jonschlinkert/is-glob.git (is-glob), https://github.com/jonschlinkert/is-number.git (is-number), git://github.com/jonschlinkert/is-primitive.git (is-primitive), git://github.com/jonschlinkert/isobject.git (isobject), https://github.com/jonschlinkert/longest.git (longest), https://github.com/jonschlinkert/micromatch.git (micromatch), https://github.com/jonschlinkert/mixin-object.git (mixin-object), git://github.com/jonschlinkert/object.omit.git (object.omit), https://github.com/jonschlinkert/parse-filepath.git (parse-filepath), https://github.com/jonschlinkert/relative.git (relative), git://github.com/jonschlinkert/set-value.git (set-value), https://github.com/jonschlinkert/write.git (write). This software contains the following license and notice below: +The following software may be included in this product: arr-diff, clone-deep, fill-range, for-in, has-value, has-values, kind-of, normalize-path, set-value. A copy of the source code may be downloaded from https://github.com/jonschlinkert/arr-diff.git (arr-diff), https://github.com/jonschlinkert/clone-deep.git (clone-deep), https://github.com/jonschlinkert/fill-range.git (fill-range), https://github.com/jonschlinkert/for-in.git (for-in), https://github.com/jonschlinkert/has-value.git (has-value), https://github.com/jonschlinkert/has-values.git (has-values), https://github.com/jonschlinkert/kind-of.git (kind-of), https://github.com/jonschlinkert/normalize-path.git (normalize-path), https://github.com/jonschlinkert/set-value.git (set-value). This software contains the following license and notice below: The MIT License (MIT) -Copyright (c) 2014-2015, Jon Schlinkert. +Copyright (c) 2014-2017, Jon Schlinkert Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -5345,6 +5328,35 @@ THE SOFTWARE. ----- +The following software may be included in this product: arr-diff. A copy of the source code may be downloaded from https://github.com/jonschlinkert/arr-diff.git. This software contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2014-2015 Jon Schlinkert. + +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. + +----- + The following software may be included in this product: arr-filter, filename-regex, for-own, object.defaults, object.reduce. A copy of the source code may be downloaded from https://github.com/jonschlinkert/arr-filter.git (arr-filter), https://github.com/regexhq/filename-regex.git (filename-regex), https://github.com/jonschlinkert/for-own.git (for-own), https://github.com/jonschlinkert/object.defaults.git (object.defaults), https://github.com/jonschlinkert/object.reduce.git (object.reduce). This software contains the following license and notice below: The MIT License (MIT) @@ -7776,7 +7788,7 @@ The following software may be included in this product: bl. A copy of the source The MIT License (MIT) ===================== -Copyright (c) 2013-2016 bl contributors +Copyright (c) 2013-2018 bl contributors ---------------------------------- *bl contributors listed at * @@ -7812,7 +7824,7 @@ The following software may be included in this product: bl. A copy of the source The MIT License (MIT) ===================== -Copyright (c) 2013-2018 bl contributors +Copyright (c) 2013-2016 bl contributors ---------------------------------- *bl contributors listed at * @@ -7829,7 +7841,7 @@ The following software may be included in this product: bluebird. A copy of the The MIT License (MIT) -Copyright (c) 2013-2018 Petka Antonov +Copyright (c) 2013-2015 Petka Antonov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -7855,7 +7867,7 @@ The following software may be included in this product: bluebird. A copy of the The MIT License (MIT) -Copyright (c) 2013-2015 Petka Antonov +Copyright (c) 2013-2018 Petka Antonov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -11877,7 +11889,7 @@ SOFTWARE. ----- -The following software may be included in this product: css-loader, enhanced-resolve, file-loader, loader-utils, mini-css-extract-plugin, schema-utils, terser-webpack-plugin, uglifyjs-webpack-plugin, url-loader, watchpack, webpack, webpack-dev-middleware, webpack-hot-middleware. A copy of the source code may be downloaded from https://github.com/webpack-contrib/css-loader.git (css-loader), git://github.com/webpack/enhanced-resolve.git (enhanced-resolve), https://github.com/webpack-contrib/file-loader.git (file-loader), https://github.com/webpack/loader-utils.git (loader-utils), https://github.com/webpack-contrib/mini-css-extract-plugin.git (mini-css-extract-plugin), https://github.com/webpack-contrib/schema-utils (schema-utils), https://github.com/webpack-contrib/terser-webpack-plugin.git (terser-webpack-plugin), https://github.com/webpack-contrib/uglifyjs-webpack-plugin.git (uglifyjs-webpack-plugin), https://github.com/webpack-contrib/url-loader.git (url-loader), https://github.com/webpack/watchpack.git (watchpack), https://github.com/webpack/webpack.git (webpack), https://github.com/webpack/webpack-dev-middleware.git (webpack-dev-middleware), https://github.com/webpack-contrib/webpack-hot-middleware.git (webpack-hot-middleware). This software contains the following license and notice below: +The following software may be included in this product: css-loader, enhanced-resolve, file-loader, loader-utils, memory-fs, mini-css-extract-plugin, schema-utils, terser-webpack-plugin, uglifyjs-webpack-plugin, url-loader, watchpack, webpack, webpack-dev-middleware, webpack-hot-middleware. A copy of the source code may be downloaded from https://github.com/webpack-contrib/css-loader.git (css-loader), git://github.com/webpack/enhanced-resolve.git (enhanced-resolve), https://github.com/webpack-contrib/file-loader.git (file-loader), https://github.com/webpack/loader-utils.git (loader-utils), https://github.com/webpack/memory-fs.git (memory-fs), https://github.com/webpack-contrib/mini-css-extract-plugin.git (mini-css-extract-plugin), https://github.com/webpack/schema-utils.git (schema-utils), https://github.com/webpack-contrib/terser-webpack-plugin.git (terser-webpack-plugin), https://github.com/webpack-contrib/uglifyjs-webpack-plugin.git (uglifyjs-webpack-plugin), https://github.com/webpack-contrib/url-loader.git (url-loader), https://github.com/webpack/watchpack.git (watchpack), https://github.com/webpack/webpack.git (webpack), https://github.com/webpack/webpack-dev-middleware.git (webpack-dev-middleware), https://github.com/webpack-contrib/webpack-hot-middleware.git (webpack-hot-middleware). This software contains the following license and notice below: Copyright JS Foundation and other contributors @@ -11930,6 +11942,30 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ----- +The following software may be included in this product: css-tree. A copy of the source code may be downloaded from https://github.com/csstree/csstree.git. This software contains the following license and notice below: + +Copyright (C) 2016-2019 by Roman Dvornov + +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. + +----- + The following software may be included in this product: cssom. A copy of the source code may be downloaded from https://github.com/NV/CSSOM.git. This software contains the following license and notice below: Copyright (c) Nikita Vasilyev @@ -12788,7 +12824,7 @@ The following software may be included in this product: detect-file. A copy of t The MIT License (MIT) -Copyright (c) 2016-2017, Brian Woodward. +Copyright (c) 2016, . Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -12814,7 +12850,7 @@ The following software may be included in this product: detect-file. A copy of t The MIT License (MIT) -Copyright (c) 2016, . +Copyright (c) 2016-2017, Brian Woodward. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -13070,6 +13106,33 @@ SOFTWARE. The following software may be included in this product: doctrine. A copy of the source code may be downloaded from https://github.com/eslint/doctrine.git. This software contains the following license and notice below: +Doctrine +Copyright jQuery Foundation and other contributors, https://jquery.org/ + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----- + +The following software may be included in this product: doctrine. A copy of the source code may be downloaded from https://github.com/eslint/doctrine.git. This software contains the following license and notice below: + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -13249,34 +13312,7 @@ END OF TERMS AND CONDITIONS ----- -The following software may be included in this product: doctrine, escodegen, estraverse, esutils, regjsparser. A copy of the source code may be downloaded from http://github.com/eslint/doctrine.git (doctrine), http://github.com/estools/escodegen.git (escodegen), http://github.com/estools/estraverse.git (estraverse), http://github.com/estools/esutils.git (esutils), git@github.com:jviereck/regjsparser.git (regjsparser). This software contains the following license and notice below: - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ------ - -The following software may be included in this product: doctrine. A copy of the source code may be downloaded from https://github.com/eslint/doctrine.git. This software contains the following license and notice below: - -Doctrine -Copyright jQuery Foundation and other contributors, https://jquery.org/ +The following software may be included in this product: doctrine, escodegen, estraverse, esutils, regjsparser. A copy of the source code may be downloaded from http://github.com/eslint/doctrine.git (doctrine), http://github.com/estools/escodegen.git (escodegen), http://github.com/estools/estraverse.git (estraverse), http://github.com/Constellation/esutils.git (esutils), git@github.com:jviereck/regjsparser.git (regjsparser). This software contains the following license and notice below: Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -13992,7 +14028,7 @@ SOFTWARE. ----- -The following software may be included in this product: enzyme, enzyme-adapter-react-16, enzyme-adapter-utils. A copy of the source code may be downloaded from https://github.com/airbnb/enzyme.git (enzyme), https://github.com/airbnb/enzyme.git (enzyme-adapter-react-16), https://github.com/airbnb/enzyme.git (enzyme-adapter-utils). This software contains the following license and notice below: +The following software may be included in this product: enzyme, enzyme-adapter-react-16, enzyme-adapter-utils, enzyme-shallow-equal. A copy of the source code may be downloaded from https://github.com/airbnb/enzyme.git (enzyme), https://github.com/airbnb/enzyme.git (enzyme-adapter-react-16), https://github.com/airbnb/enzyme.git (enzyme-adapter-utils), https://github.com/airbnb/enzyme.git (enzyme-shallow-equal). This software contains the following license and notice below: The MIT License (MIT) @@ -14498,7 +14534,6 @@ SOFTWARE. The following software may be included in this product: eslint-scope. A copy of the source code may be downloaded from https://github.com/eslint/eslint-scope.git. This software contains the following license and notice below: -eslint-scope Copyright JS Foundation and other contributors, https://js.foundation Copyright (C) 2012-2013 Yusuke Suzuki (twitter: @Constellation) and other contributors. @@ -14526,6 +14561,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The following software may be included in this product: eslint-scope. A copy of the source code may be downloaded from https://github.com/eslint/eslint-scope.git. This software contains the following license and notice below: +eslint-scope Copyright JS Foundation and other contributors, https://js.foundation Copyright (C) 2012-2013 Yusuke Suzuki (twitter: @Constellation) and other contributors. @@ -14811,7 +14847,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The following software may be included in this product: esprima. A copy of the source code may be downloaded from https://github.com/jquery/esprima.git. This software contains the following license and notice below: -Copyright (c) jQuery Foundation, Inc. and Contributors, All Rights Reserved. +Copyright JS Foundation and other contributors, https://js.foundation/ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -14837,7 +14873,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The following software may be included in this product: esprima. A copy of the source code may be downloaded from https://github.com/jquery/esprima.git. This software contains the following license and notice below: -Copyright JS Foundation and other contributors, https://js.foundation/ +Copyright (c) jQuery Foundation, Inc. and Contributors, All Rights Reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -15398,6 +15434,32 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice ----- +The following software may be included in this product: ethereum-bloom-filters. A copy of the source code may be downloaded from git+https://github.com/joshstevens19/ethereum-bloom-filters.git. This software contains the following license and notice below: + +MIT License + +Copyright (c) 2019 Josh Stevens + +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. + +----- + The following software may be included in this product: ethereum-common, ethereumjs-common. A copy of the source code may be downloaded from git+https://github.com/ethereumjs/common.git (ethereum-common), git+https://github.com/ethereumjs/ethereumjs-common.git (ethereumjs-common). This software contains the following license and notice below: The MIT License (MIT) @@ -19392,7 +19454,7 @@ The bundled Google Closure Library is licensed under Apache 2.0: ----- -The following software may be included in this product: google-p12-pem. A copy of the source code may be downloaded from https://github.com/google/google-p12-pem. This software contains the following license and notice below: +The following software may be included in this product: google-p12-pem. This software contains the following license and notice below: The MIT License (MIT) @@ -21738,6 +21800,31 @@ THE SOFTWARE. ----- +The following software may be included in this product: is2. A copy of the source code may be downloaded from git@github.com:stdarg/is2.git. This software contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2013 Edmond Meinfelder + +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. + +----- + The following software may be included in this product: isomorphic-fetch. A copy of the source code may be downloaded from https://github.com/matthew-andrews/isomorphic-fetch.git. This software contains the following license and notice below: The MIT License (MIT) @@ -22120,7 +22207,7 @@ SOFTWARE. The following software may be included in this product: js-sha3. A copy of the source code may be downloaded from https://github.com/emn178/js-sha3.git. This software contains the following license and notice below: -Copyright 2015-2016 Chen, Yi-Cyuan +Copyright 2015-2017 Chen, Yi-Cyuan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -22145,7 +22232,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. The following software may be included in this product: js-sha3. A copy of the source code may be downloaded from https://github.com/emn178/js-sha3.git. This software contains the following license and notice below: -Copyright 2015-2017 Chen, Yi-Cyuan +Copyright 2015-2018 Chen, Yi-Cyuan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -22170,7 +22257,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. The following software may be included in this product: js-sha3. A copy of the source code may be downloaded from https://github.com/emn178/js-sha3.git. This software contains the following license and notice below: -Copyright 2015-2018 Chen, Yi-Cyuan +Copyright 2015-2016 Chen, Yi-Cyuan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -22219,11 +22306,11 @@ THE SOFTWARE. ----- -The following software may be included in this product: js-tokens. A copy of the source code may be downloaded from https://github.com/lydell/js-tokens.git. This software contains the following license and notice below: +The following software may be included in this product: js-tokens, source-map-resolve. A copy of the source code may be downloaded from https://github.com/lydell/js-tokens.git (js-tokens), https://github.com/lydell/source-map-resolve.git (source-map-resolve). This software contains the following license and notice below: The MIT License (MIT) -Copyright (c) 2014, 2015, 2016, 2017, 2018 Simon Lydell +Copyright (c) 2014, 2015, 2016, 2017 Simon Lydell Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -22245,11 +22332,11 @@ THE SOFTWARE. ----- -The following software may be included in this product: js-tokens, source-map-resolve. A copy of the source code may be downloaded from https://github.com/lydell/js-tokens.git (js-tokens), https://github.com/lydell/source-map-resolve.git (source-map-resolve). This software contains the following license and notice below: +The following software may be included in this product: js-tokens. A copy of the source code may be downloaded from https://github.com/lydell/js-tokens.git. This software contains the following license and notice below: The MIT License (MIT) -Copyright (c) 2014, 2015, 2016, 2017 Simon Lydell +Copyright (c) 2014, 2015, 2016, 2017, 2018 Simon Lydell Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -22901,7 +22988,7 @@ SOFTWARE. ----- -The following software may be included in this product: keccakjs, scrypt.js. A copy of the source code may be downloaded from https://github.com/axic/keccakjs (keccakjs), https://github.com/axic/scrypt.js (scrypt.js). This software contains the following license and notice below: +The following software may be included in this product: keccakjs, scrypt.js. A copy of the source code may be downloaded from https://github.com/axic/keccakjs (keccakjs), https://github.com/axic/scryptjs (scrypt.js). This software contains the following license and notice below: The MIT License (MIT) @@ -23026,24 +23113,6 @@ THE SOFTWARE. ----- -The following software may be included in this product: level-codec, level-iterator-stream. A copy of the source code may be downloaded from https://github.com/Level/codec.git (level-codec), https://github.com/Level/iterator-stream.git (level-iterator-stream). This software contains the following license and notice below: - -The MIT License (MIT) -===================== - -Copyright (c) 2012-2015 LevelUP contributors ---------------------------------------- - -*LevelUP contributors listed at * - -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. - ------ - The following software may be included in this product: level-codec, level-errors. A copy of the source code may be downloaded from https://github.com/Level/codec.git (level-codec), https://github.com/Level/errors.git (level-errors). This software contains the following license and notice below: # The MIT License (MIT) @@ -23070,6 +23139,24 @@ SOFTWARE. ----- +The following software may be included in this product: level-codec, level-iterator-stream. A copy of the source code may be downloaded from https://github.com/Level/codec.git (level-codec), https://github.com/Level/iterator-stream.git (level-iterator-stream). This software contains the following license and notice below: + +The MIT License (MIT) +===================== + +Copyright (c) 2012-2015 LevelUP contributors +--------------------------------------- + +*LevelUP contributors listed at * + +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. + +----- + The following software may be included in this product: level-errors. A copy of the source code may be downloaded from https://github.com/level/errors.git. This software contains the following license and notice below: The MIT License (MIT) @@ -23204,9 +23291,13 @@ Original Author, when distributed with the Software. The following software may be included in this product: levelup. A copy of the source code may be downloaded from https://github.com/level/levelup.git. This software contains the following license and notice below: -# The MIT License (MIT) +The MIT License (MIT) +===================== -**Copyright © 2012-present `levelup` [Contributors](CONTRIBUTORS.md).** +Copyright (c) 2012-2016 LevelUP contributors +--------------------------------------- + +*LevelUP contributors listed at * 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: @@ -23218,13 +23309,9 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI The following software may be included in this product: levelup. A copy of the source code may be downloaded from https://github.com/level/levelup.git. This software contains the following license and notice below: -The MIT License (MIT) -===================== - -Copyright (c) 2012-2016 LevelUP contributors ---------------------------------------- +# The MIT License (MIT) -*LevelUP contributors listed at * +**Copyright © 2012-present `levelup` [Contributors](CONTRIBUTORS.md).** 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: @@ -24268,29 +24355,124 @@ SOFTWARE. ----- -The following software may be included in this product: memdown. A copy of the source code may be downloaded from https://github.com/Level/memdown.git. This software contains the following license and notice below: +The following software may be included in this product: mdn-data. A copy of the source code may be downloaded from https://github.com/mdn/data.git. This software contains the following license and notice below: -The MIT License (MIT) +CC0 1.0 Universal -Copyright (c) 2013-2018 Rod Vagg (the "Original Author") +Statement of Purpose -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 laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later +claims of infringement build upon, modify, incorporate in other works, reuse +and redistribute as freely as possible in any form whatsoever and for any +purposes, including without limitation commercial purposes. These owners may +contribute to the Commons to promote the ideal of a free culture and the +further production of creative, cultural and scientific works, or to gain +reputation or greater distribution for their Work in part through the use and +efforts of others. -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. +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with a +Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not limited +to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer's heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer's express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, +non transferable, non sublicensable, non exclusive, irrevocable and +unconditional license to exercise Affirmer's Copyright and Related Rights in +the Work (i) in all territories worldwide, (ii) for the maximum duration +provided by applicable law or treaty (including future time extensions), (iii) +in any current or future medium and for any number of copies, and (iv) for any +purpose whatsoever, including without limitation commercial, advertising or +promotional purposes (the "License"). The License shall be deemed effective as +of the date CC0 was applied by Affirmer to the Work. Should any part of the +License for any reason be judged legally invalid or ineffective under +applicable law, such partial invalidity or ineffectiveness shall not +invalidate the remainder of the License, and in such case Affirmer hereby +affirms that he or she will not (i) exercise any of his or her remaining +Copyright and Related Rights in the Work or (ii) assert any associated claims +and causes of action with respect to the Work, in either case contrary to +Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see + ----- @@ -24338,6 +24520,32 @@ Original Author, when distributed with the Software. ----- +The following software may be included in this product: memdown. A copy of the source code may be downloaded from https://github.com/Level/memdown.git. This software contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2013-2018 Rod Vagg (the "Original Author") + +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. + +----- + The following software may be included in this product: memoize-one. A copy of the source code may be downloaded from https://github.com/alexreardon/memoize-one.git. This software contains the following license and notice below: MIT License @@ -28182,9 +28390,9 @@ SOFTWARE. ----- -The following software may be included in this product: performance-now. A copy of the source code may be downloaded from git://github.com/meryn/performance-now.git. This software contains the following license and notice below: +The following software may be included in this product: performance-now. A copy of the source code may be downloaded from git://github.com/braveg1rl/performance-now.git. This software contains the following license and notice below: -Copyright (c) 2013 Meryn Stol +Copyright (c) 2013 Braveg1rl 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: @@ -28194,9 +28402,9 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ----- -The following software may be included in this product: performance-now. A copy of the source code may be downloaded from git://github.com/braveg1rl/performance-now.git. This software contains the following license and notice below: +The following software may be included in this product: performance-now. A copy of the source code may be downloaded from git://github.com/meryn/performance-now.git. This software contains the following license and notice below: -Copyright (c) 2013 Braveg1rl +Copyright (c) 2013 Meryn Stol 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: @@ -28856,11 +29064,11 @@ SOFTWARE.** ----- -The following software may be included in this product: progress, supertest. A copy of the source code may be downloaded from git://github.com/visionmedia/node-progress (progress), https://github.com/visionmedia/supertest.git (supertest). This software contains the following license and notice below: +The following software may be included in this product: progress. A copy of the source code may be downloaded from git://github.com/visionmedia/node-progress. This software contains the following license and notice below: (The MIT License) -Copyright (c) 2014 TJ Holowaychuk +Copyright (c) 2017 TJ Holowaychuk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -28883,11 +29091,11 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----- -The following software may be included in this product: progress. A copy of the source code may be downloaded from git://github.com/visionmedia/node-progress. This software contains the following license and notice below: +The following software may be included in this product: progress, supertest. A copy of the source code may be downloaded from git://github.com/visionmedia/node-progress (progress), https://github.com/visionmedia/supertest.git (supertest). This software contains the following license and notice below: (The MIT License) -Copyright (c) 2017 TJ Holowaychuk +Copyright (c) 2014 TJ Holowaychuk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -29452,7 +29660,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. The following software may be included in this product: q. A copy of the source code may be downloaded from git://github.com/kriskowal/q.git. This software contains the following license and notice below: -Copyright 2009–2017 Kristopher Michael Kowal. All rights reserved. +Copyright 2009–2014 Kristopher Michael Kowal. All rights reserved. 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 @@ -29475,7 +29683,7 @@ IN THE SOFTWARE. The following software may be included in this product: q. A copy of the source code may be downloaded from git://github.com/kriskowal/q.git. This software contains the following license and notice below: -Copyright 2009–2014 Kristopher Michael Kowal. All rights reserved. +Copyright 2009–2017 Kristopher Michael Kowal. All rights reserved. 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 @@ -30163,7 +30371,7 @@ SOFTWARE. ----- -The following software may be included in this product: react-native-autocomplete-input. A copy of the source code may be downloaded from git+ssh://git@github.com/l-urence/react-native-autocomplete-input.git. This software contains the following license and notice below: +The following software may be included in this product: react-native-autocomplete-input. A copy of the source code may be downloaded from git+ssh://git@github.com/mrlaessig/react-native-autocomplete-input.git. This software contains the following license and notice below: The MIT License (MIT) @@ -30357,6 +30565,32 @@ SOFTWARE. ----- +The following software may be included in this product: react-native-exit-app. A copy of the source code may be downloaded from https://github.com/wumke/react-native-exit-app. This software contains the following license and notice below: + +MIT License + +Copyright (c) 2018 Wumke + +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. + +----- + The following software may be included in this product: react-native-firebase. A copy of the source code may be downloaded from https://github.com/invertase/react-native-firebase.git. This software contains the following license and notice below: Copyright (c) 2018 Invertase Limited @@ -30711,6 +30945,32 @@ SOFTWARE. ----- +The following software may be included in this product: react-native-safe-area-context. A copy of the source code may be downloaded from https://github.com/th3rdwave/react-native-safe-area-context.git. This software contains the following license and notice below: + +MIT License + +Copyright (c) 2019 Th3rd Wave + +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. + +----- + The following software may be included in this product: react-native-safe-area-view. A copy of the source code may be downloaded from git@github.com:react-community/react-native-safe-area-view.git. This software contains the following license and notice below: MIT License @@ -31117,7 +31377,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ----- -The following software may be included in this product: react-redux, redux, redux-thunk. A copy of the source code may be downloaded from https://github.com/reduxjs/react-redux.git (react-redux), https://github.com/reduxjs/redux.git (redux), https://github.com/reduxjs/redux-thunk.git (redux-thunk). This software contains the following license and notice below: +The following software may be included in this product: react-redux, redux. A copy of the source code may be downloaded from https://github.com/reduxjs/react-redux.git (react-redux), https://github.com/reduxjs/redux.git (redux). This software contains the following license and notice below: The MIT License (MIT) @@ -31356,6 +31616,31 @@ IN THE SOFTWARE. The following software may be included in this product: readdirp. A copy of the source code may be downloaded from git://github.com/paulmillr/readdirp.git. This software contains the following license and notice below: +This software is released under the MIT license: + +Copyright (c) 2012-2015 Thorsten Lorenz + +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. + +----- + +The following software may be included in this product: readdirp. A copy of the source code may be downloaded from git://github.com/paulmillr/readdirp.git. This software contains the following license and notice below: + MIT License Copyright (c) 2012-2019 Thorsten Lorenz, Paul Miller (https://paulmillr.com) @@ -31380,31 +31665,6 @@ SOFTWARE. ----- -The following software may be included in this product: readdirp. A copy of the source code may be downloaded from git://github.com/paulmillr/readdirp.git. This software contains the following license and notice below: - -This software is released under the MIT license: - -Copyright (c) 2012-2015 Thorsten Lorenz - -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. - ------ - The following software may be included in this product: realpath-native. A copy of the source code may be downloaded from https://github.com/SimenB/realpath-native.git. This software contains the following license and notice below: MIT License @@ -31719,9 +31979,11 @@ SOFTWARE. ----- -The following software may be included in this product: regjsgen. A copy of the source code may be downloaded from https://github.com/d10/regjsgen.git. This software contains the following license and notice below: +The following software may be included in this product: regjsgen. A copy of the source code may be downloaded from https://github.com/bnjmnt4n/regjsgen.git. This software contains the following license and notice below: -Copyright 2014 Benjamin Tan (https://d10.github.io/) +The MIT License (MIT) + +Copyright 2014-2019 Benjamin Tan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -31744,11 +32006,9 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----- -The following software may be included in this product: regjsgen. A copy of the source code may be downloaded from https://github.com/bnjmnt4n/regjsgen.git. This software contains the following license and notice below: - -The MIT License (MIT) +The following software may be included in this product: regjsgen. A copy of the source code may be downloaded from https://github.com/d10/regjsgen.git. This software contains the following license and notice below: -Copyright 2014-2018 Benjamin Tan +Copyright 2014 Benjamin Tan (https://d10.github.io/) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -32400,7 +32660,7 @@ SOFTWARE. ----- -The following software may be included in this product: rxjs. A copy of the source code may be downloaded from https://github.com/reactivex/rxjs.git. This software contains the following license and notice below: +The following software may be included in this product: rxjs. A copy of the source code may be downloaded from git@github.com:ReactiveX/RxJS.git. This software contains the following license and notice below: Apache License Version 2.0, January 2004 @@ -32590,7 +32850,7 @@ Apache License same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors + Copyright (c) 2015-2017 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -32606,7 +32866,7 @@ Apache License ----- -The following software may be included in this product: rxjs. A copy of the source code may be downloaded from git@github.com:ReactiveX/RxJS.git. This software contains the following license and notice below: +The following software may be included in this product: rxjs. A copy of the source code may be downloaded from https://github.com/reactivex/rxjs.git. This software contains the following license and notice below: Apache License Version 2.0, January 2004 @@ -32796,7 +33056,7 @@ Apache License same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright (c) 2015-2017 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors + Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -34061,6 +34321,20 @@ THE SOFTWARE. The following software may be included in this product: slice-ansi. A copy of the source code may be downloaded from https://github.com/chalk/slice-ansi.git. This software contains the following license and notice below: +MIT License + +Copyright (c) DC + +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. + +----- + +The following software may be included in this product: slice-ansi. A copy of the source code may be downloaded from https://github.com/chalk/slice-ansi.git. This software contains the following license and notice below: + (The MIT License) Copyright (c) 2015 DC @@ -34086,20 +34360,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----- -The following software may be included in this product: slice-ansi. A copy of the source code may be downloaded from https://github.com/chalk/slice-ansi.git. This software contains the following license and notice below: - -MIT License - -Copyright (c) DC - -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. - ------ - The following software may be included in this product: sliced. A copy of the source code may be downloaded from git://github.com/aheckmann/sliced. This software contains the following license and notice below: (The MIT License) @@ -35400,6 +35660,31 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ----- +The following software may be included in this product: tcp-port-used. A copy of the source code may be downloaded from git://github.com/stdarg/tcp-port-used.git. This software contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2013 jut-io + +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. + +----- + The following software may be included in this product: tdigest. A copy of the source code may be downloaded from https://github.com/welch/tdigest.git. This software contains the following license and notice below: The MIT License (MIT) @@ -38590,7 +38875,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----- -The following software may be included in this product: uuid. A copy of the source code may be downloaded from https://github.com/shtylman/node-uuid.git. This software contains the following license and notice below: +The following software may be included in this product: uuid. A copy of the source code may be downloaded from https://github.com/defunctzombie/node-uuid.git. This software contains the following license and notice below: Copyright (c) 2010-2012 Robert Kieffer MIT License - http://opensource.org/licenses/mit-license.php @@ -39698,7 +39983,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ----- -The following software may be included in this product: workspace-aggregator-f7085830-6625-41b2-ba95-4a0cd827c7a2. This software contains the following license and notice below: +The following software may be included in this product: workspace-aggregator-b8423bb7-31c8-4bc0-83fe-ac5ca53b154a. This software contains the following license and notice below: Apache License Version 2.0, January 2004 diff --git a/packages/mobile/android/gradle.properties b/packages/mobile/android/gradle.properties index ed4ea35a9d8..cf09d2bafb6 100644 --- a/packages/mobile/android/gradle.properties +++ b/packages/mobile/android/gradle.properties @@ -20,4 +20,5 @@ CELO_RELEASE_STORE_FILE=celo-release-key.keystore CELO_RELEASE_KEY_ALIAS=celo-key-alias # Setting this manually based on version number until we have this deploying via Cloud Build -VERSION_CODE=100500001 \ No newline at end of file +# Example: v1.5.1 deployment number 1 = 100500101 +VERSION_CODE=100500101 \ No newline at end of file diff --git a/packages/mobile/fastlane/Fastfile b/packages/mobile/fastlane/Fastfile index 935c56e1a09..5c97c255b1c 100644 --- a/packages/mobile/fastlane/Fastfile +++ b/packages/mobile/fastlane/Fastfile @@ -36,19 +36,23 @@ platform :android do desc 'Build the Android application - requires environment param' lane :build do |options| + clean sh('yarn', 'run', 'build:sdk', options[:sdkEnv]) environment = options[:environment].capitalize ENV["GRADLE_OPTS"] = '-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-Xmx256m -XX:+HeapDumpOnOutOfMemoryError"' gradle(task: 'bundle' + environment + 'JsAndAssets', project_dir: 'android/') ENV["GRADLE_OPTS"] = '-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-Xmx3500m -XX:+HeapDumpOnOutOfMemoryError"' - gradle(task: 'bundle', build_type: environment, project_dir: 'android/', flags: '-x bundle' + environment + 'JsAndAssets') + if options[:buildApk] + gradle(task: 'assemble', build_type: environment, project_dir: 'android/', flags: '-x bundle' + environment + 'JsAndAssets') + else + gradle(task: 'bundle', build_type: environment, project_dir: 'android/', flags: '-x bundle' + environment + 'JsAndAssets') + end end desc 'Ship Integration to Playstore Internal' lane :integration do env = 'integration' sdkEnv = 'integration' - clean build(environment: env, sdkEnv: sdkEnv) fastlane_supply(env, 'internal', env) end @@ -57,7 +61,6 @@ platform :android do lane :staging do env = 'staging' sdkEnv = 'alfajoresstaging' - clean build(environment: env, sdkEnv: sdkEnv) fastlane_supply(env, 'internal', env) end @@ -66,7 +69,6 @@ platform :android do lane :production do env = 'release' sdkEnv = 'argentinaproduction' - clean build(environment: env, sdkEnv: sdkEnv) fastlane_supply(env, 'alpha', 'production') end @@ -75,7 +77,6 @@ platform :android do lane :alfajores do env = 'alfajores' sdkEnv = 'alfajores' - clean build(environment: env, sdkEnv: sdkEnv) fastlane_supply(env, 'internal', env) end @@ -84,10 +85,23 @@ platform :android do lane :pilotapp do env = 'pilot' sdkEnv = 'pilot' - clean build(environment: env, sdkEnv: sdkEnv) fastlane_supply(env, 'internal', env) end + + desc 'Build an Android apk' + lane :build_apk do |options| + env = options[:env] + sdkEnv = options[:sdkEnv] + build(environment: env, sdkEnv: sdkEnv, buildApk: true) + end + + desc 'Build an Android bundle' + lane :build_bundle do |options| + env = options[:env] + sdkEnv = options[:sdkEnv] + build(environment: env, sdkEnv: sdkEnv) + end end diff --git a/packages/mobile/fastlane/README.md b/packages/mobile/fastlane/README.md index 58877c7e308..93fc7aef754 100644 --- a/packages/mobile/fastlane/README.md +++ b/packages/mobile/fastlane/README.md @@ -76,6 +76,22 @@ fastlane android pilotapp Ship Pilot to Playstore Internal +### android build_apk + +``` +fastlane android build_apk +``` + +Build an Android apk + +### android build_bundle + +``` +fastlane android build_bundle +``` + +Build an Android bundle + --- This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. diff --git a/packages/mobile/ios/GoogleService-Info.plist.enc b/packages/mobile/ios/GoogleService-Info.plist.enc new file mode 100644 index 00000000000..3745b796f0e Binary files /dev/null and b/packages/mobile/ios/GoogleService-Info.plist.enc differ diff --git a/packages/mobile/ios/Podfile b/packages/mobile/ios/Podfile index 061ec1d5113..baed20b1df9 100644 --- a/packages/mobile/ios/Podfile +++ b/packages/mobile/ios/Podfile @@ -1,11 +1,6 @@ # File contents of "ios/Podfile" platform :ios, '9.0' -pre_install do |installer| - # workaround for CocoaPods/CocoaPods#3289 - Pod::Installer::Xcode::TargetValidator.send(:define_method, :verify_no_static_framework_transitive_dependencies) {} -end - target 'celo' do use_frameworks! @@ -54,6 +49,7 @@ target 'celo' do pod 'react-native-splash-screen', :path => '../../../node_modules/react-native-splash-screen' pod 'react-native-version-check', :path => '../../../node_modules/react-native-version-check' pod 'RNRandomBytes', :path => '../../../node_modules/react-native-secure-randombytes' + pod 'react-native-tcp', :path => '../../../node_modules/react-native-tcp' pod 'react-native-udp', :path => '../../../node_modules/react-native-udp' pod 'react-native-netinfo', :path => '../../../node_modules/@react-native-community/netinfo' pod 'RNShare', :path => '../../../node_modules/react-native-share' @@ -62,6 +58,7 @@ target 'celo' do pod 'CeloBlockchain', :path => '../../../node_modules/@celo/client/CeloBlockchain.podspec' pod 'RNSecureKeyStore', :path => '../../../node_modules/react-native-secure-key-store/ios' pod 'react-native-safe-area-context', :path => '../../../node_modules/react-native-safe-area-context' + pod 'RNExitApp', :path => '../../../node_modules/react-native-exit-app' pod 'Firebase/Core', '~> 5.20.2' pod 'Firebase/Auth' diff --git a/packages/mobile/ios/Podfile.lock b/packages/mobile/ios/Podfile.lock index 6adcbc5696c..3f05695a8a8 100644 --- a/packages/mobile/ios/Podfile.lock +++ b/packages/mobile/ios/Podfile.lock @@ -1,7 +1,7 @@ PODS: - Analytics (3.7.0) - boost-for-react-native (1.63.0) - - CeloBlockchain (0.0.1) + - CeloBlockchain (0.0.156) - Crashlytics (3.13.4): - Fabric (~> 1.10.2) - DoubleConversion (1.1.6) @@ -137,7 +137,9 @@ PODS: - React - react-native-safe-area-context (0.3.6): - React - - react-native-splash-screen (3.1.1): + - react-native-splash-screen (3.2.0): + - React + - react-native-tcp (3.3.0): - React - react-native-udp (2.6.1): - React @@ -206,6 +208,8 @@ PODS: - Segment-Firebase - RNDeviceInfo (2.1.0): - React + - RNExitApp (1.1.0): + - React - RNFirebase (5.5.4): - Firebase/Core - React @@ -266,6 +270,7 @@ DEPENDENCIES: - "react-native-netinfo (from `../../../node_modules/@react-native-community/netinfo`)" - react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`) - react-native-splash-screen (from `../../../node_modules/react-native-splash-screen`) + - react-native-tcp (from `../../../node_modules/react-native-tcp`) - react-native-udp (from `../../../node_modules/react-native-udp`) - react-native-version-check (from `../../../node_modules/react-native-version-check`) - React/Core (from `../node_modules/react-native`) @@ -285,6 +290,7 @@ DEPENDENCIES: - "RNAnalytics (from `../../../node_modules/@segment/analytics-react-native`)" - "RNAnalyticsIntegration-Firebase (from `../../../node_modules/@segment/analytics-react-native-firebase`)" - RNDeviceInfo (from `../../../node_modules/react-native-device-info`) + - RNExitApp (from `../../../node_modules/react-native-exit-app`) - RNFirebase (from `../../../node_modules/react-native-firebase/ios`) - RNFS (from `../../../node_modules/react-native-fs`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) @@ -351,6 +357,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native-safe-area-context" react-native-splash-screen: :path: "../../../node_modules/react-native-splash-screen" + react-native-tcp: + :path: "../../../node_modules/react-native-tcp" react-native-udp: :path: "../../../node_modules/react-native-udp" react-native-version-check: @@ -361,6 +369,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/@segment/analytics-react-native-firebase" RNDeviceInfo: :path: "../../../node_modules/react-native-device-info" + RNExitApp: + :path: "../../../node_modules/react-native-exit-app" RNFirebase: :path: "../../../node_modules/react-native-firebase/ios" RNFS: @@ -387,7 +397,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Analytics: 77fd5fb102a4a5eedafa2c2b0245ceb7b7c15e45 boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c - CeloBlockchain: 4779210b5840c586b131de638694044ecb362bda + CeloBlockchain: f258122010fb610057979254d4a3f88eb7fb0d21 Crashlytics: 2dfd686bcb918dc10ee0e76f7f853fe42c7bd552 DoubleConversion: bb338842f62ab1d708ceb63ec3d999f0f3d98ecd Fabric: 706c8b8098fff96c33c0db69cbf81f9c551d0d74 @@ -413,17 +423,19 @@ SPEC CHECKSUMS: react-native-camera: 571e4cfe70e7021e9077c1699a53e659db971f96 react-native-config: 8f6b2b9e017f6a5e92a97353c768e19e67294bb1 react-native-contacts: 348958bb7da4399040b1eb45bfb7f6af08e61412 - react-native-geth: 8ec5ada8b9c69eea6a515fdfe784878a625c9b6e + react-native-geth: b643560e11512a3c0b1d78df929cb9f2409cf4a2 react-native-keep-awake: abcf6d09d0cc5fe45df4a56a9382e25d10cff8b6 react-native-mail: 021d8ee60e374609f5689ef354dc8e36839a9ba6 react-native-netinfo: 1ea4efa22c02519ac8043ac3f000062a4e320795 react-native-safe-area-context: e380a6f783ccaec848e2f3cc8eb205a62362950d - react-native-splash-screen: 353334c5ae82d8c74501ea7cbb916cb7cb20c8bf + react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865 + react-native-tcp: e1a8c3ac010774cd71811989805ff3eaebb62f17 react-native-udp: 54a1aa9bf5c0824f930b1ba6dbfb3fd3e733bba9 react-native-version-check: 901616b6d258b385628120441bd0b285b24c7be1 RNAnalytics: 05243c1fa17186f07be3eae30514b43aeb2e0578 RNAnalyticsIntegration-Firebase: 82b879019b012d1aa1cbcb6dfc466acd7233bb97 RNDeviceInfo: 2b4f76adc479657bf37bfc927bbfb027b17515d4 + RNExitApp: c4e052df2568b43bec8a37c7cd61194d4cfee2c3 RNFirebase: 7d4733713a0f436d55388b55ca3744385c70fd2d RNFS: 0f4d630b538e93e0e9e3519ff0b7b7b48760d8ed RNGestureHandler: b65d391f4f570178d657b99a16ec99d09b8656b0 @@ -438,6 +450,6 @@ SPEC CHECKSUMS: SentryReactNative: 07237139c00366ea2e75ae3e5c566e7a71c27a90 yoga: 684513b14b03201579ba3cee20218c9d1298b0cc -PODFILE CHECKSUM: 74a2f21d73b361a2ed26a15896af8f4d70b123e0 +PODFILE CHECKSUM: 4fd5dd293e11126639ca80f4cd528455f4af5ffa COCOAPODS: 1.7.5 diff --git a/packages/mobile/ios/celo.xcodeproj/project.pbxproj b/packages/mobile/ios/celo.xcodeproj/project.pbxproj index 090693a354a..bf5fc4aed80 100644 --- a/packages/mobile/ios/celo.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/celo.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 0581BC2D3AEB42D086D403E3 /* Hind-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = AAE92820119F4CC5B23C6636 /* Hind-Medium.ttf */; }; 0F1E1EB52346439C00274556 /* rings@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0F1E1EB32346439C00274556 /* rings@2x.png */; }; 0F1E1EB62346439C00274556 /* rings@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0F1E1EB42346439C00274556 /* rings@3x.png */; }; + 0FF4C5D62355F18C009E07DD /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0FF4C5D52355F18B009E07DD /* GoogleService-Info.plist */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; @@ -53,6 +54,7 @@ 0F1E1EB32346439C00274556 /* rings@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "rings@2x.png"; sourceTree = ""; }; 0F1E1EB42346439C00274556 /* rings@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "rings@3x.png"; sourceTree = ""; }; 0FE3DE8E2347740700EA87A0 /* celo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = celo.entitlements; path = celo/celo.entitlements; sourceTree = ""; }; + 0FF4C5D52355F18B009E07DD /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 114969FB1F3AA04FFDC6831F /* Pods-celoTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-celoTests.debug.xcconfig"; path = "Target Support Files/Pods-celoTests/Pods-celoTests.debug.xcconfig"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* celo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = celo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = celo/AppDelegate.h; sourceTree = ""; }; @@ -142,6 +144,7 @@ 13B07FAE1A68108700A75B9A /* celo */ = { isa = PBXGroup; children = ( + 0FF4C5D52355F18B009E07DD /* GoogleService-Info.plist */, 0FE3DE8E2347740700EA87A0 /* celo.entitlements */, 008F07F21AC5B25A0029DE68 /* main.jsbundle */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */, @@ -310,6 +313,9 @@ DevelopmentTeam = HDPUB8C3KG; LastSwiftMigration = 1020; SystemCapabilities = { + com.apple.BackgroundModes = { + enabled = 1; + }; com.apple.Push = { enabled = 1; }; @@ -367,6 +373,7 @@ 0F1E1EB62346439C00274556 /* rings@3x.png in Resources */, 0F1E1EB52346439C00274556 /* rings@2x.png in Resources */, 0581BC2D3AEB42D086D403E3 /* Hind-Medium.ttf in Resources */, + 0FF4C5D62355F18C009E07DD /* GoogleService-Info.plist in Resources */, 2CE54C7D16D44C5380C53609 /* Hind-Regular.ttf in Resources */, 5575263ED1EA4F568D328DE9 /* Hind-SemiBold.ttf in Resources */, ); @@ -448,12 +455,14 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-celo/Pods-celo-frameworks.sh", "${BUILT_PRODUCTS_DIR}/Analytics/Analytics.framework", + "${BUILT_PRODUCTS_DIR}/CeloBlockchain/CeloBlockchain.framework", "${BUILT_PRODUCTS_DIR}/DoubleConversion/DoubleConversion.framework", "${BUILT_PRODUCTS_DIR}/Folly/folly.framework", "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", "${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework", "${BUILT_PRODUCTS_DIR}/RNDeviceInfo/RNDeviceInfo.framework", + "${BUILT_PRODUCTS_DIR}/RNExitApp/RNExitApp.framework", "${BUILT_PRODUCTS_DIR}/RNFS/RNFS.framework", "${BUILT_PRODUCTS_DIR}/RNGestureHandler/RNGestureHandler.framework", "${BUILT_PRODUCTS_DIR}/RNLocalize/RNLocalize.framework", @@ -477,6 +486,7 @@ "${BUILT_PRODUCTS_DIR}/react-native-netinfo/react_native_netinfo.framework", "${BUILT_PRODUCTS_DIR}/react-native-safe-area-context/react_native_safe_area_context.framework", "${BUILT_PRODUCTS_DIR}/react-native-splash-screen/react_native_splash_screen.framework", + "${BUILT_PRODUCTS_DIR}/react-native-tcp/react_native_tcp.framework", "${BUILT_PRODUCTS_DIR}/react-native-udp/react_native_udp.framework", "${BUILT_PRODUCTS_DIR}/react-native-version-check/react_native_version_check.framework", "${BUILT_PRODUCTS_DIR}/yoga/yoga.framework", @@ -484,12 +494,14 @@ name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Analytics.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CeloBlockchain.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DoubleConversion.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/folly.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNDeviceInfo.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNExitApp.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNFS.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNGestureHandler.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNLocalize.framework", @@ -513,6 +525,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_netinfo.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_safe_area_context.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_splash_screen.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_tcp.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_udp.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_version_check.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/yoga.framework", diff --git a/packages/mobile/ios/celo/AppDelegate.m b/packages/mobile/ios/celo/AppDelegate.m index 58a80e5d650..e92cb2d40e8 100644 --- a/packages/mobile/ios/celo/AppDelegate.m +++ b/packages/mobile/ios/celo/AppDelegate.m @@ -19,19 +19,31 @@ #import "RNSentry.h" // This is used for versions of react < 0.40 #endif -#import +@import Firebase; +#import "RNFirebaseNotifications.h" +#import "RNFirebaseMessaging.h" +#import "RNSplashScreen.h" + +// Use same key as react-native-secure-key-store +// so we don't reset already working installs +static NSString * const kHasRunBeforeKey = @"RnSksIsAppInstalled"; @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - // TODO: add GoogleService-Info.plist and enable this - // [FIRApp configure]; + // Reset keychain on first run to clear existing Firebase credentials + // Note: react-native-secure-key-store also does that but is run too late + // and hence can't clear Firebase credentials + [self resetKeychainIfNecessary]; + [FIRApp configure]; + [RNFirebaseNotifications configure]; RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"celo" initialProperties:nil]; + [RNSplashScreen showSplash:@"LaunchScreen" inRootView:rootView]; [RNSentry installWithRootView:rootView]; rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; @@ -53,4 +65,40 @@ - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge #endif } +- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { + [[RNFirebaseNotifications instance] didReceiveLocalNotification:notification]; +} + +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo +fetchCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler{ + [[RNFirebaseNotifications instance] didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; +} + +- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { + [[RNFirebaseMessaging instance] didRegisterUserNotificationSettings:notificationSettings]; +} + +// Reset keychain on first app run, this is so we don't run with leftover items +// after reinstalling the app +- (void)resetKeychainIfNecessary +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + if ([defaults boolForKey:kHasRunBeforeKey]) { + return; + } + + NSArray *secItemClasses = @[(__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrGeneric, + (__bridge id)kSecAttrAccount, + (__bridge id)kSecClassKey, + (__bridge id)kSecAttrService]; + for (id secItemClass in secItemClasses) { + NSDictionary *spec = @{(__bridge id)kSecClass:secItemClass}; + SecItemDelete((__bridge CFDictionaryRef)spec); + } + + [defaults setBool:YES forKey:kHasRunBeforeKey]; + [defaults synchronize]; +} + @end diff --git a/packages/mobile/ios/celo/Info.plist b/packages/mobile/ios/celo/Info.plist index 1aca0e103f2..b44a9fa7dca 100644 --- a/packages/mobile/ios/celo/Info.plist +++ b/packages/mobile/ios/celo/Info.plist @@ -51,6 +51,10 @@ Hind-Regular.ttf Hind-SemiBold.ttf + UIBackgroundModes + + remote-notification + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities diff --git a/packages/mobile/locales/en-US/accountScreen10.json b/packages/mobile/locales/en-US/accountScreen10.json index 925756cb466..37a2a933e0b 100644 --- a/packages/mobile/locales/en-US/accountScreen10.json +++ b/packages/mobile/locales/en-US/accountScreen10.json @@ -1,5 +1,4 @@ { - "backupKey": "Backup Key", "invite": "Invite", "celoRewards": "Celo Rewards", "languageSettings": "Language Settings", @@ -10,6 +9,10 @@ "shareAnalytics": "Share Analytics", "shareAnalytics_detail": "We collect anonymized data about how you use Celo to help improve the application for everyone.", + "celoLite": "Celo Lite", + "enableCeloLite": "Enable Celo Lite", + "celoLiteDetail": + "Celo Lite mode allows you to communicate with the Celo Network through a trusted node. You can always change this mode in app settings.", "testFaqHere": "<0>Test FAQ is <1>here", "termsOfServiceHere": "<0>Terms of service are <1>here", "editProfile": "Edit Profile", diff --git a/packages/mobile/locales/en-US/backupKeyFlow6.json b/packages/mobile/locales/en-US/backupKeyFlow6.json index b4d8337d0bc..26763e646e1 100644 --- a/packages/mobile/locales/en-US/backupKeyFlow6.json +++ b/packages/mobile/locales/en-US/backupKeyFlow6.json @@ -1,83 +1,71 @@ { - "readOnlyMode": "Read Only Mode Set Backup Key to continue sending and receiving {{CeloDollars}}", "getBackupKey": "Get Backup Key", - "learnBackupKey": "Learn Your Backup Key", - "editProfile": "Edit Profile", + "getYourKey": "Get Your Key", + "viewBackupKey": "View Backup Key", + "setUpSocialBackup": "Set Up Safeguards", + "viewSafeguards": "View Safeguards", + "failedFetchMnemonic": "Failed to fetch your Backup Key", + "backupAndRecovery": "Backup and Recovery", "backupKey": "Backup Key", + "yourBackupKey": "Your Backup Key", "delayBackup": "Dismiss for an hour", - "inviteFriends": "Invite Friends", - "verifierApp": "Verifier App", - "languageSettings": "Language Settings", - "cancel": "Cancel", - "backupKeyImportance": { + "backupKeyNotification": "Without a Backup Key, you may lose access to your wallet.", + "backupKeyIntro": { "0": - "If you lose your phone or delete the Celo app, you will lose all of the gold and {{dollars}} in your wallet.", - "1": - "You can back up your wallet by writing down a Backup Key on a piece of paper and storing it securely. This allows you to restore your wallet in the future if you need to.", - "2": - "Don’t take a screenshot or save it in your phone notes. Make sure to write the Backup Key down and keep it safe." + "Your Backup Key is the one and only key to your Celo Wallet. With this key, you can access your funds anytime, anywhere.", + "1": "KEEP THIS KEY SAFE AND PRIVATE.", + "2": "Congrats, you’ve sucessfully retrieved your Backup Key! A reminder, ", + "3": "KEEP THIS KEY SAFE.", + "4": "For more security set up Safeguards for your wallet.", + "5": + "You’ve also set up Safeguards! You’ll be able to restore your account with the help of the two friends. ", + "6": "KEEP YOUR SAFEGUARDS SECRET" }, - "setBackupKey": "Set Backup Key", - "areYouSure": "Are you sure?", "backupSkipText": { "0": "Without a Backup Key, you can lose access to your wallet ", "1": "forever." }, - "shareBackupKey": "Share Backup Key", "backupRecovery": "Share your Backup Key with only one other person who you completely trust.", - "sendWhatsApp": "Send with WhatsApp", - "continue": "Continue", - "securityTips": "Security Tips", - "backupKeySummary": { - "0": "Write this phrase down on a piece of paper and keep it somewhere safe.", - "1": "Don’t show your phrase to anyone else. They can access your wallet if they see it." - }, - "learnYourKey": "Write down or memorize your Backup Key.", - "keyWillBeVerified": "Once you have finished, we will verify that you know the key correctly.", - "question": "Question", - "questionPhrase": { - "0": "What is the ", - "1": " word of your Backup Key?" + "securityTip": "If you lose your backup key, you will lose access to your Celo Dollars and Gold.", + "backupKeySummary": + "Please write down your Backup Key. If your phone is stolen or lost, you will need the Backup Key to access your Celo Wallet.", + "bothBackupsDone": { + "0": "Congratulations!", + "1": + "You're all done! If you would like to review your recovery secrets, you can always return here later." }, - "question1": "What is the 1st word of your Backup Key?", - "question2": "What is the 2nd word of your Backup Key?", - "question3": "What is the 3rd word of your Backup Key?", - "question4": "What is the 4th word of your Backup Key?", - "question5": "What is the 5th word of your Backup Key?", - "question6": "What is the 6th word of your Backup Key?", - "question7": "What is the 7th word of your Backup Key?", - "question8": "What is the 8th word of your Backup Key?", - "question9": "What is the 9th word of your Backup Key?", - "question10": "What is the 10th word of your Backup Key?", - "question11": "What is the 11th word of your Backup Key?", - "question12": "What is the 12th word of your Backup Key?", - "question13": "What is the 13th word of your Backup Key?", - "question14": "What is the 14th word of your Backup Key?", - "question15": "What is the 15th word of your Backup Key?", - "question16": "What is the 16th word of your Backup Key?", - "question17": "What is the 17th word of your Backup Key?", - "question18": "What is the 18th word of your Backup Key?", - "question19": "What is the 19th word of your Backup Key?", - "question20": "What is the 20th word of your Backup Key?", - "question21": "What is the 21st word of your Backup Key?", - "question22": "What is the 22nd word of your Backup Key?", - "question23": "What is the 23rd word of your Backup Key?", - "question24": "What is the 24th word of your Backup Key?", - "submit": "Submit", - "dontKnow": "Don't Know? ", - "return": "Return to backup key", - "tryAgain": "Try Again", - "backToKey": - "We will take you back to the screen with your backup key so that you ensure you wrote it down correctly.", - "seeBackupKey": "See Backup Key", - "backupKeySet": "Backup Key Set", "dontLoseIt": "Please do not lose this key. It is critical that you maintain this in a safe place, as this is the only way to unlock your wallet should you lose your phone.", - "done": "Done", - "whatsappMessage": - "Important: please keep this private. \n\nI'm sending you the Backup Phrase to my Celo Wallet: ", "backupPrompt": "For the security of your funds, your account is frozen until you get your Backup Key", - "copyToClipboard": "Copy To Clipboard", - "copiedToClipboard": "Copied To Clipboard" + "copied": "Key copied to clipboard", + "savedConfirmation": "I have saved my Backup Key.", + "confirmBackupKey": "Confirm Your Backup Key", + "backupQuizInfo": + "Please verify your Backup Key by selecting the words below in the correct order.", + "backupQuizWordCount": "Word {{index}} of {{total}}", + "invalidBackupPhrase": "Invalid Backup Key", + "importBackupFailed": "Importing Wallet Failed", + "backupQuizFailed": "Incorrect Backup Key, please try again", + "backupComplete": { + "0": "Success!", + "1": "Next, you can set up Safeguards.", + "2": "You’re all set!" + }, + "socialBackupIntro": { + "header": "Introducing Safeguards", + "body": + "Safeguards is an additional layer of protection for your account in case you lose your Backup Key. Once set up, you’ll be able to restore your account with the help of two friends or family members. ", + "warning": "NEVER TELL ANYONE YOUR SAFEGUARDS’ IDENTITIES.", + "skip": "Skip For Now" + }, + "socialBackupPhraseHeader": "Safeguard Phrase {{index}}", + "socialBackup": { + "body": + "Share each phrase below with a friend. Be sure to send only one phrase to each person.", + "confirmation": "I have sent each Safeguard phrase to a trusted friend.", + "yourSafeguards": "Your Safeguards" + }, + "backupPhrasePlaceholder": + "horse leopard dog monkey shark tiger lemur whale squid wolf squirrel mouse lion elephant cat shrimp bear penguin deer turtle fox zebra goat giraffe" } diff --git a/packages/mobile/locales/en-US/global.json b/packages/mobile/locales/en-US/global.json index db9a3014954..204ccb7b440 100644 --- a/packages/mobile/locales/en-US/global.json +++ b/packages/mobile/locales/en-US/global.json @@ -8,6 +8,12 @@ "save": "Save", "next": "Next", "skip": "Skip", + "copy": "Copy", + "goBack": "Go Back", + "reset": "Reset", + "done": "Done", + "tip": "Tip: ", + "warning": "Warning ", "downloadRewards": "Download Celo Rewards", "chooseLanguage": "Choose Language", "wallet": "Wallet", @@ -24,7 +30,6 @@ "refreshFailedUnexpectedly": "Failed to refresh, please check your connectivity", "edit": "Edit", "receivedPayment": "Received Payment", - "getBackupKey": "Get Backup Key", "readOnlyState": "Read Only Mode Set Backup Key to continue sending and receiving {{CeloDollars}}", "getStarted": "Get Started", @@ -33,7 +38,6 @@ "startEarning": "Start Earning", "backToWallet": "Back to Wallet", "exchangeForGold": "Exchange for Gold", - "invalidKey": "Invalid Backup Key ", "invalidPhone": "Invalid Phone Number", "cantSelectInvalidPhone": "Cannot select contact: invalid phone number", "needMoreFundsToSend": "Need more funds to send payment", @@ -61,9 +65,8 @@ "verifyFailed": "Failed to verify", "canNotRequestFromUnverified": "Can not request from unverified users", "restartApp": "Restart App", + "quitApp": "Quit", "loading": "Loading…", - "invalidBackupPhrase": "Invalid Backup Key", - "importBackupFailed": "Importing Wallet Failed", "inviteFailed": "Failure sending invite", "importContactsFailed": "Failed to import contacts", "sendPaymentFailed": "Failure sending payment", @@ -89,6 +92,7 @@ "web3FailedToSync": "Failing to sync, check your network connection", "errorDuringSync": "Error occurred during sync, please try again later", "calculateFeeFailed": "Could not calculate fee", + "failedToSwitchSyncModes": "Failed to switch sync modes", "gold": "Gold", "localCurrencyTitle": "Select Currency" } diff --git a/packages/mobile/locales/en-US/nuxNamePin1.json b/packages/mobile/locales/en-US/nuxNamePin1.json index 310f1ca87b0..55478d39a4d 100644 --- a/packages/mobile/locales/en-US/nuxNamePin1.json +++ b/packages/mobile/locales/en-US/nuxNamePin1.json @@ -47,7 +47,7 @@ "InvitationCode": "Invitation Code", "optIn": "Opt In", "submitting": "Submitting ...", - "importIt": "Import Existing Wallet", + "importIt": "Restore Existing Wallet", "cancel": "Cancel", "important": "Important", "createPin": { diff --git a/packages/mobile/locales/en-US/nuxRestoreWallet3.json b/packages/mobile/locales/en-US/nuxRestoreWallet3.json index b51d5294eec..38aba1313e6 100644 --- a/packages/mobile/locales/en-US/nuxRestoreWallet3.json +++ b/packages/mobile/locales/en-US/nuxRestoreWallet3.json @@ -10,5 +10,9 @@ "emptyWalletWarning": "This wallet is empty.", "useEmptyAnyway": "Would you like to use it anyway?", "useEmptyWallet": "Use Empty Wallet", - "tryAnotherKey": "Try Another Backup Key" + "tryAnotherKey": "Try Another Backup Key", + "restoreSocial": "Restore with Safeguards", + "socialImportInfo": + "If you enabled Safeguards, you sent Safeguard Phrases to two friends. Please retrieve these phrases and enter them here (in any order) to regain access to your funds.", + "socialTip": "Phrases are lists of 13 words separated by spaces." } diff --git a/packages/mobile/locales/en-US/sendFlow7.json b/packages/mobile/locales/en-US/sendFlow7.json index f28d4b66aea..58e9f780b83 100644 --- a/packages/mobile/locales/en-US/sendFlow7.json +++ b/packages/mobile/locales/en-US/sendFlow7.json @@ -45,6 +45,7 @@ "sentPayment": "Sent Payment", "mobileNumber": "Mobile #", "walletAddress": "Wallet Address", + "unknown": "Unknown", "inviteSMS": "Hi{{name}}! I would like to invite you to join the Celo payments network. Your invite code is: {{code}} You can install the Celo application from the following link: {{link}}", "inviteSent": "Invite code sent!", @@ -55,8 +56,11 @@ "scanCode": "Scan Code", "writeStorageNeededForQrDownload": "Storage write permission is needed for downloading the QR code", - "ScanCodeByPlacingItInTheBox": "Scan code by placing it in the box", - "needCameraPermissionToScan": "App needs camera permission to scan QR codes", + "cameraScanInfo": "Scan code by placing it in the box", + "cameraNotAuthorizedTitle": "Enable Camera", + "cameraNotAuthorizedDescription": + "Please enable the Camera in your phone’s Settings. You’ll need it to scan QR codes.", + "cameraSettings": "Settings", "showYourQRCode": "Show your QR code", "toSentOrRequestPayment": "to send or request payment", "requestSent": "Request Sent", diff --git a/packages/mobile/locales/es-419/accountScreen10.json b/packages/mobile/locales/es-419/accountScreen10.json index 6bc3d025916..b0c62240532 100755 --- a/packages/mobile/locales/es-419/accountScreen10.json +++ b/packages/mobile/locales/es-419/accountScreen10.json @@ -1,5 +1,4 @@ { - "backupKey": "Clave de respaldo", "invite": "Invitar", "celoRewards": "Recompensas Celo", "languageSettings": "Configuración de idioma", @@ -10,6 +9,10 @@ "shareAnalytics": "Compartir estadisticas de uso", "shareAnalytics_detail": "Recopilamos datos anónimos sobre cómo utiliza Celo para ayudar a mejorar la aplicación para todos.", + "celoLite": "Celo Lite", + "enableCeloLite": "Habilitar Celo Lite", + "celoLiteDetail": + "El modo Celo Lite te permite comunicarte con la Red Celo a través de un nodo confiable. Puedes cambiar este modo en la configuración de la aplicación.", "testFaqHere": "<1>Aquí<0> están las preguntas frecuentes de la prueba. ", "termsOfServiceHere": "<1>Aquí<0> están las Condiciones de servicio.", "editProfile": "Editar perfil", diff --git a/packages/mobile/locales/es-419/backupKeyFlow6.json b/packages/mobile/locales/es-419/backupKeyFlow6.json index a64be76e736..7dcb82b7ced 100755 --- a/packages/mobile/locales/es-419/backupKeyFlow6.json +++ b/packages/mobile/locales/es-419/backupKeyFlow6.json @@ -1,86 +1,72 @@ { - "readOnlyMode": - "Modo de solo lectura: cree la clave de respaldo para seguir enviando y recibiendo {{CeloDollars}}", - "getBackupKey": "Obtener clave de respaldo", - "learnBackupKey": "Aprenda tu clave de respaldo", - "editProfile": "Editar perfil", - "backupKey": "Clave de respaldo", - "delayBackup": "Despedir por una hora", - "inviteFriends": "Invitar a amigos", - "verifierApp": "Apli del verificador", - "languageSettings": "Configuración de idioma", - "cancel": "Cancelar", - "backupKeyImportance": { - "0": - "Si pierde tu teléfono o elimina la aplicación de Celo, perderá todo el oro y los {{dollars}} de tu monedero.", - "1": - "Puede hacer un respaldo del monedero si escribe una clave de respaldo en un papel y la guarda en algún sitio seguro. Así, podrá restaurar el monedero en el futuro de ser necesario.", - "2": - "No haga una captura de pantalla ni la guarde en las notas de tu teléfono. Asegúrese de escribir la clave de respaldo a mano y mantenerla segura." - }, - "setBackupKey": "Crear clave de respaldo", - "areYouSure": "¿Está seguro?", - "shareBackupKey": "Compartir clave de respaldo", + "getYourKey": "Consigue tu llave", + "yourBackupKey": "Su llave de respaldo", + "getBackupKey": "Obtener llave de respaldo", + "backupAndRecovery": "Seguridad y recuperación", + "viewSafeguards": "Ver salvaguardas", + "backupKey": "Llave de respaldo", + "delayBackup": "Descartar por una hora", "backupRecovery": - "Comparta la clave de respaldo con una sola persona en la que confíe plenamente.", + "Comparta su llave de respaldo con solo otra persona en quien confíe por completo.", + "viewBackupKey": "Ver llave de respaldo", + "backupKeyNotification": "Sin una llave de respaldo, puede perder el acceso a su billetera.", + "setUpSocialBackup": "Establecer salvaguardas", + "securityTip": "Si pierde su llave de respaldo, perderá el acceso a sus Celo Dollars y Gold.", + "backupPrompt": + "Para la seguridad de sus fondos, su cuenta se congela hasta que obtenga su Llave de respaldo", + "backupQuizWordCount": "Palabra {{index}} de {{total}}", + "socialBackupPhraseHeader": "Frase de protección {{index}}", "backupSkipText": { - "0": "Comparta la clave de respaldo con una sola persona en la que confíe ", - "1": "plenamente." - }, - "sendWhatsApp": "Enviar por WhatsApp", - "continue": "Continuar", - "securityTips": "Consejos de seguridad", - "backupKeySummary": { - "0": "Escriba esta frase en un papel y guárdela en algún sitio seguro.", - "1": "No le muestre la frase a nadie. Si alguien la sabe, podrá acceder a tu monedero." + "0": "Sin una llave de respaldo, puede perder el acceso a su billetera", + "1": "Siempre." }, - "learnYourKey": "Escriba o memorice tu clave de respaldo.", - "keyWillBeVerified": - "Una vez que haya terminado, verificaremos que conoce la clave correctamente.", - "question": "Pregunta", - "questionPhrase": { - "0": "¿Cuál es la ", - "1": " palabra de tu clave de respaldo?" + "confirmBackupKey": "Confirme su llave de respaldo", + "importBackupFailed": "Se produjo un error al importar la billetera", + "backupPhrasePlaceholder": + "caballo leopardo perro mono tiburón tigre lémur ballena calamar lobo ardilla ratón león elefante gato camarones oso pingüino ciervo tortuga zorro cebra cabra jirafa", + "invalidBackupPhrase": "Llave de respaldo inválida", + "copied": "Llave copiada al portapapeles", + "backupComplete": { + "0": "¡Éxito!", + "1": "A continuación, puede configurar Salvaguardas.", + "2": "¡Ya está todo listo!" }, - "question1": "¿Cuál es la 1.ª palabra de tu clave de respaldo?", - "question2": "¿Cuál es la 2.ª palabra de tu clave de respaldo?", - "question3": "¿Cuál es la 3.ª palabra de tu clave de respaldo?", - "question4": "¿Cuál es la 4.ª palabra de tu clave de respaldo?", - "question5": "¿Cuál es la 5.ª palabra de tu clave de respaldo?", - "question6": "¿Cuál es la 6.ª palabra de tu clave de respaldo?", - "question7": "¿Cuál es la 7.ª palabra de tu clave de respaldo?", - "question8": "¿Cuál es la 8.ª palabra de tu clave de respaldo?", - "question9": "¿Cuál es la 9.ª palabra de tu clave de respaldo?", - "question10": "¿Cuál es la 10.ª palabra de tu clave de respaldo?", - "question11": "¿Cuál es la 11.ª palabra de tu clave de respaldo?", - "question12": "¿Cuál es la 12.ª palabra de tu clave de respaldo?", - "question13": "¿Cuál es la 13.ª palabra de tu clave de respaldo?", - "question14": "¿Cuál es la 14.ª palabra de tu clave de respaldo?", - "question15": "¿Cuál es la 15.ª palabra de tu clave de respaldo?", - "question16": "¿Cuál es la 16.ª palabra de tu clave de respaldo?", - "question17": "¿Cuál es la 17.ª palabra de tu clave de respaldo?", - "question18": "¿Cuál es la 18.ª palabra de tu clave de respaldo?", - "question19": "¿Cuál es la 19.ª palabra de tu clave de respaldo?", - "question20": "¿Cuál es la 20.ª palabra de tu clave de respaldo?", - "question21": "¿Cuál es la 21.ª palabra de tu clave de respaldo?", - "question22": "¿Cuál es la 22.ª palabra de tu clave de respaldo?", - "question23": "¿Cuál es la 23.ª palabra de tu clave de respaldo?", - "question24": "¿Cuál es la 24.ª palabra de tu clave de respaldo?", - "submit": "Enviar", - "dontKnow": "¿No la sabe? ", - "return": "Vuelva a la clave de respaldo", - "tryAgain": "Reintentar", - "backToKey": - "Le llevaremos a la pantalla con tu clave de respaldo para que verifique que la escribió correctamente.", - "seeBackupKey": "Ver clave de respaldo", - "backupKeySet": "Clave de respaldo creada", + "failedFetchMnemonic": "No se pudo recuperar su llave de copia de seguridad", + "backupKeySummary": + "Por favor escriba su llave de respaldo. Si le roban o pierde su teléfono, necesitará la llave de respaldo para acceder a su Celo Wallet.", + "backupQuizInfo": + "Verifique su llave de respaldo seleccionando las siguientes palabras en el orden correcto.", "dontLoseIt": - "No pierda esta clave. Es de suma importancia que la mantenga en un lugar seguro, ya que es la única forma de desbloquear tu monedero si pierde tu celular.", - "done": "Listo", - "whatsappMessage": - "Importante: por favor mantenga esto privado. \n\nTe estoy enviando la frase de respaldo a mi Monedero Celo: ", - "backupPrompt": - "Para la seguridad de sus fondos, tu cuenta está congelada hasta que obtenga tu clave de respaldo", - "copyToClipboard": "Copiar al portapapeles", - "copiedToClipboard": "Copiada al portapapeles" + "Por favor no pierda esta llave. Es fundamental que mantenga esto en un lugar seguro, ya que esta es la única forma de desbloquear su billetera en caso de que pierda su teléfono.", + "socialBackup": { + "body": + "Comparte cada frase a continuación con un amigo. Asegúrese de enviar solo una frase a cada persona.", + "confirmation": "He enviado cada frase de Safeguard a un amigo de confianza.", + "yourSafeguards": "Sus salvaguardas" + }, + "backupKeyIntro": { + "0": + "Su llave de respaldo es la única llave para su billetera Celo. Con esta llave, puede acceder a sus fondos en cualquier momento y en cualquier lugar.", + "1": "MANTENGA ESTA CLAVE SEGURA Y PRIVADA.", + "2": "¡Felicidades, ha recuperado con éxito su llave de copia de seguridad! Un recordatorio, ", + "3": "MANTENGA ESTA LLAVE SEGURA.", + "4": "Para mayor seguridad, configure Salvaguardas para su billetera.", + "5": + "¡También ha configurado Salvaguardas! Podrá restaurar su cuenta con la ayuda de los dos amigos. ", + "6": "MANTENGA SU SECRETO DE SALVAGUARDIAS" + }, + "bothBackupsDone": { + "0": "¡Felicidades!", + "1": + "¡Ya terminaste! Si desea revisar sus secretos de recuperación, siempre puede regresar aquí más tarde." + }, + "savedConfirmation": "He guardado mi llave de respaldo.", + "backupQuizFailed": "Llave de respaldo incorrecta, por favor intente nuevamente", + "socialBackupIntro": { + "body": + "Safeguard es una capa adicional de protección para su cuenta en caso de que pierda su llave de respaldo. Una vez configurado, podrá restaurar su cuenta con la ayuda de dos amigos o familiares.", + "header": "Introduciendo salvaguardas", + "warning": "NUNCA LE DIGA A NADIE LAS IDENTIDADES DE SUS SALVAGUARDIAS.", + "skip": "Saltar por ahora" + } } diff --git a/packages/mobile/locales/es-419/global.json b/packages/mobile/locales/es-419/global.json index ac19d046e6b..438e970a108 100755 --- a/packages/mobile/locales/es-419/global.json +++ b/packages/mobile/locales/es-419/global.json @@ -8,24 +8,28 @@ "save": "Guarda", "next": "Siguiente", "skip": "Saltar", + "copy": "Copiar", + "goBack": "Regresa", + "reset": "Reiniciar", + "done": "Hecho", + "tip": "Punta: ", + "warning": "Advertencia ", "downloadRewards": "Descargar Recompensas Celo", "chooseLanguage": "Elegir idioma", "wallet": "Monedero", - "payments": "Pagos", "send": "Envío", + "payments": "Pagos", "exchange": "Cambio", "learnMore": "Saber más", "activity": "Actividad", "history": "Historial", - "exchangeFailed": "Intercambio fallido, por favor vuelva a intentarlo", - "transactionFailed": "Transacción fallido, por favor vuelva a intentarlo", "notEnoughDollarsError": "No hay suficientes {{CeloDollars}} para cambiarlos", "notEnoughGoldError": "No hay suficiente Celo Oro para cambiarlo", + "exchangeFailed": "Intercambio fallido, por favor vuelva a intentarlo", + "transactionFailed": "Transacción fallido, por favor vuelva a intentarlo", "refreshFailedUnexpectedly": "Error al actualizar, por favor revise tu conectividad", "edit": "Editar", - "needMoreFundsToSend": "Necesita más fondos para enviar el pago", "receivedPayment": "Pago recibido", - "getBackupKey": "Obtener clave de respaldo", "readOnlyState": "Modo de solo lectura: cree la clave de respaldo para seguir enviando y recibiendo {{CeloDollars}}", "getStarted": "Primeros pasos", @@ -34,9 +38,9 @@ "startEarning": "Comenzar a ganar", "backToWallet": "Volver a el Monedero", "exchangeForGold": "Cambiar a Oro", - "cantSelectInvalidPhone": "No se puede seleccionar el contacto: número de teléfono no válido", - "invalidKey": "Clave de respaldo inválida ", "invalidPhone": "Número de teléfono inválido", + "cantSelectInvalidPhone": "No se puede seleccionar el contacto: número de teléfono no válido", + "needMoreFundsToSend": "Necesita más fondos para enviar el pago", "invalidAmount": "Monto invalido", "invalidCode": "Código de verificación inválido", "confirm": "Confirmar", @@ -61,9 +65,8 @@ "verifyFailed": "Falla durante la verificación", "canNotRequestFromUnverified": "No se puede solicitar a usuarios no verificados.", "restartApp": "Reiniciar la aplicación", + "quitApp": "Dejar", "loading": "Cargando…", - "invalidBackupPhrase": "Clave de respaldo inválida", - "importBackupFailed": "No se pudo importar el monedero", "inviteFailed": "Falló el envío de la invitación", "importContactsFailed": "Error al importar contactos", "sendPaymentFailed": "Falla en el envío de pago", @@ -90,6 +93,7 @@ "web3FailedToSync": "Fallo la sincronización, por favor verifica tu conexión", "errorDuringSync": "Ocurrió un error duranet la sincronización, por favor intente más tarde", "calculateFeeFailed": "No se pudo calcular la comisión", + "failedToSwitchSyncModes": "Error al cambiar de red modos", "gold": "Oro", "localCurrencyTitle": "Seleccione el tipo de moneda" } diff --git a/packages/mobile/locales/es-419/nuxNamePin1.json b/packages/mobile/locales/es-419/nuxNamePin1.json index 99ee20db882..31ce3c2196a 100755 --- a/packages/mobile/locales/es-419/nuxNamePin1.json +++ b/packages/mobile/locales/es-419/nuxNamePin1.json @@ -48,7 +48,7 @@ "InvitationCode": "Código de invitación", "optIn": "Inscribirse", "submitting": "Enviando ...", - "importIt": "Importa tu monedero existente", + "importIt": "Restaurar tu monedero existente", "cancel": "Cancelar", "important": "Importante", "createPin": { diff --git a/packages/mobile/locales/es-419/nuxRestoreWallet3.json b/packages/mobile/locales/es-419/nuxRestoreWallet3.json index 66a7c38b9ba..81a9d7002c6 100755 --- a/packages/mobile/locales/es-419/nuxRestoreWallet3.json +++ b/packages/mobile/locales/es-419/nuxRestoreWallet3.json @@ -10,5 +10,9 @@ "emptyWalletWarning": "Este Monedero esta vacio", "useEmptyAnyway": "¿Te gustaría usarlo de todos modos?", "useEmptyWallet": "Usar Monedero Vacío", - "tryAnotherKey": "Probar con otra Clave de Respaldo" + "tryAnotherKey": "Probar con otra Clave de Respaldo", + "restoreSocial": "Restaurar con Salvaguardas", + "socialImportInfo": + "Si habilitó las Salvaguardas, envió Frases de Salvaguarda a dos amigos. Recupere estas frases e ingréselas aquí (en cualquier orden) para recuperar el acceso a sus fondos.", + "socialTip": "Las frases son listas de 13 palabras separadas por espacios." } diff --git a/packages/mobile/locales/es-419/sendFlow7.json b/packages/mobile/locales/es-419/sendFlow7.json index d804e2d7573..0e2c1ea21d5 100755 --- a/packages/mobile/locales/es-419/sendFlow7.json +++ b/packages/mobile/locales/es-419/sendFlow7.json @@ -45,6 +45,7 @@ "sentPayment": "Enviar pago", "mobileNumber": "Número de teléfono", "walletAddress": "Dirección del Monedero", + "unknown": "Desconocido", "inviteSMS": "Hola{{name}}! Me gustaría invitarte a que te unas a la red de pagos de Celo. Tu código de invitación es: {{code}} Podés instalarte la aplicación Celo desde el siguiente vínculo: {{link}}", "inviteSent": "Código de invitación enviado!", @@ -55,9 +56,11 @@ "scanCode": "Escanear código", "writeStorageNeededForQrDownload": "Se necesita permiso de escritura de almacenamiento para descargar el código QR", - "ScanCodeByPlacingItInTheBox": "Escanee el código colocándolo en la caja", - "needCameraPermissionToScan": - "La aplicación necesita permiso de la cámara para escanear códigos QR", + "cameraScanInfo": "Escanee el código colocándolo en la caja", + "cameraNotAuthorizedTitle": "Habilitar Cámara", + "cameraNotAuthorizedDescription": + "Habilite la cámara en la configuración de su teléfono. Lo necesitará para escanear códigos QR.", + "cameraSettings": "Configuraciones", "showYourQRCode": "Muestra tu código QR", "toSentOrRequestPayment": "enviar o solicitar pago", "requestSent": "Solicitud Enviada", diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 5e624f82630..5c02efa94fc 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@celo/mobile", - "version": "1.5.0", + "version": "1.5.1", "author": "Celo", "license": "Apache-2.0", "private": true, @@ -47,7 +47,8 @@ ] }, "dependencies": { - "@celo/client": "c1ae452", + "@celo/client": "9575a01", + "@celo/contractkit": "^0.1.6", "@celo/react-components": "1.0.0", "@celo/react-native-sms-retriever": "git+https://github.com/celo-org/react-native-sms-retriever#d3a2fdb", "@celo/utils": "^0.1.1", @@ -89,18 +90,18 @@ "react-native-confirm-device-credentials": "^2.0.0", "react-native-contacts": "git://github.com/celo-org/react-native-contacts#4989b0b", "react-native-device-info": "^2.1.0", + "react-native-exit-app": "^1.1.0", "react-native-firebase": "5.5.4", "react-native-flag-secure-android": "git://github.com/kristiansorens/react-native-flag-secure-android#e234251", "react-native-fs": "^2.12.1", "react-native-gesture-handler": "^1.1.0", - "react-native-geth": "git+https://github.com/celo-org/react-native-geth#4d9ee15", + "react-native-geth": "https://github.com/celo-org/react-native-geth#8ba5091", "react-native-install-referrer": "git://github.com/celo-org/react-native-install-referrer#343bf3d", "react-native-keep-awake": "^3.0.1", "react-native-keyboard-aware-scroll-view": "^0.6.0", "react-native-localize": "^1.2.1", "react-native-mail": "^3.0.7", "react-native-modal": "^6.1.0", - "react-native-modal-dropdown": "^0.6.2", "react-native-permissions": "^1.1.1", "react-native-progress": "^3.4.0", "react-native-qrcode-svg": "^5.1.2", @@ -114,10 +115,10 @@ "react-native-sentry": "^0.43.2", "react-native-shadow": "^1.2.2", "react-native-share": "^1.1.3", - "react-native-splash-screen": "^3.1.1", + "react-native-splash-screen": "^3.2.0", "react-native-svg": "^9.8.4", "react-native-swiper": "^1.5.13", - "react-native-tcp": "git://github.com/cmcewen/react-native-tcp#08f03c2", + "react-native-tcp": "https://github.com/cmcewen/react-native-tcp#408b674", "react-native-udp": "^2.6.1", "react-native-version-check": "^3.0.2", "react-native-webview": "^5.12.1", @@ -126,7 +127,6 @@ "redux": "^4.0.4", "redux-persist": "^5.9.1", "redux-saga": "^1.0.1", - "redux-thunk": "^2.2.0", "reselect": "^3.0.1", "sleep-promise": "^8.0.1", "svgs": "^4.1.0", diff --git a/packages/mobile/rn-cli.config.js b/packages/mobile/rn-cli.config.js index f17315d3837..b9dd6fcf600 100644 --- a/packages/mobile/rn-cli.config.js +++ b/packages/mobile/rn-cli.config.js @@ -9,7 +9,7 @@ const root = path.resolve(cwd, '../..') const escapedRoot = escapeStringRegexp(root) const rnRegex = new RegExp(`${escapedRoot}\/node_modules\/(react-native)\/.*`) const celoRegex = new RegExp( - `${escapedRoot}\/packages\/(?!mobile|utils|walletkit|react-components).*` + `${escapedRoot}\/packages\/(?!mobile|utils|walletkit|contractkit|react-components).*` ) const nestedRnRegex = new RegExp(`.*\/node_modules\/.*\/node_modules\/(react-native)\/.*`) const componentsRnRegex = new RegExp(`.*react-components\/node_modules\/(react-native)\/.*`) @@ -24,6 +24,7 @@ module.exports = { extraNodeModules: { ...nodeLibs, 'crypto-js': path.resolve(cwd, 'node_modules/crypto-js'), + fs: require.resolve('react-native-fs'), 'isomorphic-fetch': require.resolve('cross-fetch'), net: require.resolve('react-native-tcp'), 'react-native': path.resolve(cwd, 'node_modules/react-native'), diff --git a/packages/mobile/scripts/translateFile.js b/packages/mobile/scripts/translateFile.js new file mode 100644 index 00000000000..e6b01e940d4 --- /dev/null +++ b/packages/mobile/scripts/translateFile.js @@ -0,0 +1,62 @@ +// Translate a file using google translate +// tslint:disable: no-console + +const fs = require('fs') +const request = require('request') + +const fileName = process.argv[2] +const googleApiToken = process.argv[3] +console.info(`Translating file: ${fileName}`) + +const json = fs.readFileSync(`../locales/en-US/${fileName}`) +const strings = JSON.parse(json) +console.info(`Found ${Object.keys(strings).length} strings`) + +function translateString(s) { + return new Promise((resolve, reject) => { + console.info(`Looking up ${s}`) + request.post( + 'https://translation.googleapis.com/language/translate/v2', + { + headers: { + Authorization: `Bearer ${googleApiToken}`, + }, + json: { + format: 'text', + q: s, + source: 'en', + target: 'es', + }, + }, + (error, res, body) => { + if (error) { + reject(error) + return + } + resolve(body.data.translations[0].translatedText) + } + ) + }) +} + +async function translateStrings(stringsToTranslate) { + const translations = {} + + const promises = Promise.all( + Object.keys(stringsToTranslate).map(async (key) => { + const val = stringsToTranslate[key] + + if (typeof val === 'string') { + const t = await translateString(val) + translations[key] = t + } else if (typeof stringsToTranslate === 'object') { + translations[key] = await translateStrings(val) + } + }) + ) + + await promises + return translations +} + +translateStrings(strings).then((translations) => console.log(JSON.stringify(translations))) diff --git a/packages/mobile/secrets.json.enc b/packages/mobile/secrets.json.enc index d3c597ccdea..a95acfbc8fe 100644 Binary files a/packages/mobile/secrets.json.enc and b/packages/mobile/secrets.json.enc differ diff --git a/packages/mobile/src/account/Account.test.tsx b/packages/mobile/src/account/Account.test.tsx index 50b3485b967..290a19509c6 100644 --- a/packages/mobile/src/account/Account.test.tsx +++ b/packages/mobile/src/account/Account.test.tsx @@ -1,18 +1,19 @@ -const { mockNavigationServiceFor } = require('test/utils') -const { navigate } = mockNavigationServiceFor('Account') - import * as React from 'react' import 'react-native' -import { fireEvent, render } from 'react-native-testing-library' import { Provider } from 'react-redux' import * as renderer from 'react-test-renderer' import Account from 'src/account/Account' -import { Screens } from 'src/navigator/Screens' import { createMockStore } from 'test/utils' -jest.useFakeTimers() - describe('Account', () => { + beforeAll(() => { + jest.useFakeTimers() + }) + + afterAll(() => { + jest.useRealTimers() + }) + it('renders correctly', () => { const tree = renderer.create( @@ -22,33 +23,18 @@ describe('Account', () => { expect(tree).toMatchSnapshot() }) - describe('when Edit Profile Pressed', () => { - it('navigates to Profile', () => { - const account = render( - - - - ) - - fireEvent.press(account.getByTestId('editProfileButton')) - expect(navigate).toBeCalledWith(Screens.Profile) - }) - }) - - describe('when dev mode active', () => { - it('renders correctly', () => { - const tree = renderer.create( - - - - ) - expect(tree).toMatchSnapshot() - }) + it('renders correctly when dev mode active', () => { + const tree = renderer.create( + + + + ) + expect(tree).toMatchSnapshot() }) }) diff --git a/packages/mobile/src/account/Account.tsx b/packages/mobile/src/account/Account.tsx index bd017c8beea..8b876914240 100644 --- a/packages/mobile/src/account/Account.tsx +++ b/packages/mobile/src/account/Account.tsx @@ -1,5 +1,4 @@ import Link from '@celo/react-components/components/Link' -import SmallButton from '@celo/react-components/components/SmallButton' import colors from '@celo/react-components/styles/colors' import { fontStyles } from '@celo/react-components/styles/fonts' import { anonymizedPhone, isE164Number } from '@celo/utils/src/phoneNumbers' @@ -10,7 +9,7 @@ import DeviceInfo from 'react-native-device-info' import SafeAreaView from 'react-native-safe-area-view' import { Sentry } from 'react-native-sentry' import { connect } from 'react-redux' -import { devModeTriggerClicked } from 'src/account/actions' +import { devModeTriggerClicked, resetBackupState } from 'src/account/actions' import SettingsItem from 'src/account/SettingsItem' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' @@ -25,7 +24,6 @@ import { headerWithBackButton } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { RootState } from 'src/redux/reducers' -import DisconnectBanner from 'src/shared/DisconnectBanner' import { navigateToURI, navigateToVerifierApp } from 'src/utils/linking' import Logger from 'src/utils/Logger' @@ -34,6 +32,7 @@ interface DispatchProps { setNumberVerified: typeof setNumberVerified resetAppOpenedState: typeof resetAppOpenedState setAnalyticsEnabled: typeof setAnalyticsEnabled + resetBackupState: typeof resetBackupState devModeTriggerClicked: typeof devModeTriggerClicked } @@ -64,6 +63,7 @@ const mapDispatchToProps = { setNumberVerified, resetAppOpenedState, setAnalyticsEnabled, + resetBackupState, devModeTriggerClicked, } @@ -86,7 +86,7 @@ export class Account extends React.Component { } backupScreen() { - navigate(Screens.Backup) + navigate(Screens.BackupIntroduction) } goToInvite() { @@ -109,6 +109,10 @@ export class Account extends React.Component { navigate(Screens.Analytics, { nextScreen: Screens.Account }) } + goToCeloLite() { + navigate(Screens.CeloLite, { nextScreen: Screens.Account }) + } + goToFAQ() { navigateToURI(FAQ_LINK) } @@ -131,6 +135,10 @@ export class Account extends React.Component { this.props.revokeVerification() } + resetBackupState = () => { + this.props.resetBackupState() + } + showDebugScreen = async () => { navigate(Screens.Debug) } @@ -181,6 +189,11 @@ export class Account extends React.Component { Reset app opened state + + + Reset backup state + + Show Debug Screen @@ -202,7 +215,6 @@ export class Account extends React.Component { return ( - @@ -214,21 +226,19 @@ export class Account extends React.Component { - - + + {features.SHOW_SHOW_REWARDS_APP_LINK && ( )} + { + it('renders correctly', () => { + const tree = renderer.create( + + + + ) + expect(tree).toMatchSnapshot() + }) +}) diff --git a/packages/mobile/src/account/Analytics.tsx b/packages/mobile/src/account/Analytics.tsx index 6eb0fc2ad04..da0b9801e85 100644 --- a/packages/mobile/src/account/Analytics.tsx +++ b/packages/mobile/src/account/Analytics.tsx @@ -1,14 +1,13 @@ import SettingsSwitchItem from '@celo/react-components/components/SettingsSwitchItem' import colors from '@celo/react-components/styles/colors' import fontStyles from '@celo/react-components/styles/fonts' -import variables from '@celo/react-components/styles/variables' import * as React from 'react' import { WithNamespaces, withNamespaces } from 'react-i18next' import { ScrollView, StyleSheet, Text } from 'react-native' import { connect } from 'react-redux' import { setAnalyticsEnabled } from 'src/app/actions' import i18n, { Namespaces } from 'src/i18n' -import { headerWithCancelButton } from 'src/navigator/Headers' +import { headerWithBackButton } from 'src/navigator/Headers' import { RootState } from 'src/redux/reducers' interface StateProps { @@ -29,7 +28,7 @@ const mapStateToProps = (state: RootState): StateProps => { export class Analytics extends React.Component { static navigationOptions = () => ({ - ...headerWithCancelButton, + ...headerWithBackButton, headerTitle: i18n.t('accountScreen10:analytics'), }) @@ -50,27 +49,10 @@ export class Analytics extends React.Component { } const style = StyleSheet.create({ - accountHeader: { - paddingTop: 20, - }, - input: { - borderWidth: 1, - borderRadius: 3, - borderColor: '#EEEEEE', - padding: 5, - height: 54, - margin: 20, - width: variables.width - 40, - fontSize: 16, - }, scrollView: { flex: 1, backgroundColor: colors.background, }, - container: { - flex: 1, - paddingLeft: 20, - }, }) export default connect( diff --git a/packages/mobile/src/account/CeloLite.test.tsx b/packages/mobile/src/account/CeloLite.test.tsx new file mode 100644 index 00000000000..84804279238 --- /dev/null +++ b/packages/mobile/src/account/CeloLite.test.tsx @@ -0,0 +1,17 @@ +import * as React from 'react' +import 'react-native' +import { Provider } from 'react-redux' +import * as renderer from 'react-test-renderer' +import CeloLite from 'src/account/CeloLite' +import { createMockStore } from 'test/utils' + +describe('CeloLite', () => { + it('renders correctly', () => { + const tree = renderer.create( + + + + ) + expect(tree).toMatchSnapshot() + }) +}) diff --git a/packages/mobile/src/account/CeloLite.tsx b/packages/mobile/src/account/CeloLite.tsx new file mode 100644 index 00000000000..cfeeb6763e2 --- /dev/null +++ b/packages/mobile/src/account/CeloLite.tsx @@ -0,0 +1,65 @@ +import SettingsSwitchItem from '@celo/react-components/components/SettingsSwitchItem' +import colors from '@celo/react-components/styles/colors' +import fontStyles from '@celo/react-components/styles/fonts' +import * as React from 'react' +import { WithNamespaces, withNamespaces } from 'react-i18next' +import { ScrollView, StyleSheet, Text } from 'react-native' +import { connect } from 'react-redux' +import i18n, { Namespaces } from 'src/i18n' +import { headerWithBackButton } from 'src/navigator/Headers' +import { RootState } from 'src/redux/reducers' +import { setZeroSyncMode } from 'src/web3/actions' + +interface StateProps { + zeroSyncEnabled: boolean +} + +interface DispatchProps { + setZeroSyncMode: typeof setZeroSyncMode +} + +type Props = StateProps & DispatchProps & WithNamespaces + +const mapDispatchToProps = { + setZeroSyncMode, +} + +const mapStateToProps = (state: RootState): StateProps => { + return { + zeroSyncEnabled: state.web3.zeroSyncMode, + } +} + +export class CeloLite extends React.Component { + static navigationOptions = () => ({ + ...headerWithBackButton, + headerTitle: i18n.t('accountScreen10:celoLite'), + }) + + render() { + const { zeroSyncEnabled, t } = this.props + return ( + + + {t('enableCeloLite')} + + + ) + } +} + +const style = StyleSheet.create({ + scrollView: { + flex: 1, + backgroundColor: colors.background, + }, +}) + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withNamespaces(Namespaces.accountScreen10)(CeloLite)) diff --git a/packages/mobile/src/account/EditProfile.tsx b/packages/mobile/src/account/EditProfile.tsx index 0b0fe4c3cb9..66a82a40506 100644 --- a/packages/mobile/src/account/EditProfile.tsx +++ b/packages/mobile/src/account/EditProfile.tsx @@ -10,7 +10,7 @@ import { setName } from 'src/account/actions' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' import { Namespaces } from 'src/i18n' -import { headerWithCancelButton } from 'src/navigator/Headers' +import { headerWithBackButton } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { RootState } from 'src/redux/reducers' @@ -32,7 +32,7 @@ const mapStateToProps = (state: RootState): StateProps => { } export class EditProfile extends React.Component { - static navigationOptions = headerWithCancelButton + static navigationOptions = headerWithBackButton state = { name: this.props.name, @@ -73,9 +73,6 @@ export class EditProfile extends React.Component { } const style = StyleSheet.create({ - accountHeader: { - paddingTop: 20, - }, input: { borderWidth: 1, borderRadius: 3, @@ -90,10 +87,6 @@ const style = StyleSheet.create({ flex: 1, backgroundColor: colors.background, }, - container: { - flex: 1, - paddingLeft: 20, - }, }) export default connect( diff --git a/packages/mobile/src/account/Education.tsx b/packages/mobile/src/account/Education.tsx index 66a86b0385f..471d673e468 100644 --- a/packages/mobile/src/account/Education.tsx +++ b/packages/mobile/src/account/Education.tsx @@ -189,20 +189,6 @@ const style = StyleSheet.create({ flex: 1, paddingHorizontal: 20, }, - footerLink: { - color: colors.celoGreen, - textAlign: 'center', - marginTop: 10, - marginBottom: 25, - }, - circleContainer: { - flex: 0, - width: PROGRESS_CIRCLE_PASSIVE_SIZE, - height: PROGRESS_CIRCLE_PASSIVE_SIZE, - alignItems: 'center', - justifyContent: 'center', - margin: 5, - }, circle: { flex: 0, backgroundColor: colors.inactive, diff --git a/packages/mobile/src/account/Invite.tsx b/packages/mobile/src/account/Invite.tsx index 57739ab2989..0b8dd33ebd0 100644 --- a/packages/mobile/src/account/Invite.tsx +++ b/packages/mobile/src/account/Invite.tsx @@ -135,15 +135,6 @@ const style = StyleSheet.create({ flex: 1, backgroundColor: colors.background, }, - inviteHeadline: { - fontSize: 24, - lineHeight: 39, - color: colors.dark, - }, - label: { - alignSelf: 'center', - textAlign: 'center', - }, }) export default componentWithAnalytics( diff --git a/packages/mobile/src/account/InviteReview.test.tsx b/packages/mobile/src/account/InviteReview.test.tsx index 9db23d9bac7..e76f174efc9 100644 --- a/packages/mobile/src/account/InviteReview.test.tsx +++ b/packages/mobile/src/account/InviteReview.test.tsx @@ -1,5 +1,3 @@ -jest.useFakeTimers() - import Button from '@celo/react-components/components/Button' import * as React from 'react' import 'react-native' @@ -19,6 +17,14 @@ jest.mock('src/identity/verification', () => { }) describe('InviteReview', () => { + beforeAll(() => { + jest.useFakeTimers() + }) + + afterAll(() => { + jest.useRealTimers() + }) + it('renders correctly', () => { const tree = renderer.create( diff --git a/packages/mobile/src/account/Licenses.test.tsx b/packages/mobile/src/account/Licenses.test.tsx new file mode 100644 index 00000000000..fc603cf69e7 --- /dev/null +++ b/packages/mobile/src/account/Licenses.test.tsx @@ -0,0 +1,17 @@ +import * as React from 'react' +import 'react-native' +import { Provider } from 'react-redux' +import * as renderer from 'react-test-renderer' +import Licenses from 'src/account/Licenses' +import { createMockStore } from 'test/utils' + +describe('Licenses', () => { + it('renders correctly', () => { + const tree = renderer.create( + + + + ) + expect(tree).toMatchSnapshot() + }) +}) diff --git a/packages/mobile/src/account/Profile.tsx b/packages/mobile/src/account/Profile.tsx index fe0e4936533..ee712ead8b5 100644 --- a/packages/mobile/src/account/Profile.tsx +++ b/packages/mobile/src/account/Profile.tsx @@ -65,9 +65,6 @@ export class Profile extends React.Component { } const style = StyleSheet.create({ - accountHeader: { - paddingTop: 20, - }, accountProfile: { paddingLeft: 10, paddingTop: 30, @@ -76,26 +73,6 @@ const style = StyleSheet.create({ flexDirection: 'column', alignItems: 'center', }, - accountFooter: { - flex: 1, - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - height: 50, - margin: 10, - }, - accountFooterText: { - paddingBottom: 10, - }, - editProfileButton: { - height: 28, - width: 110, - }, - image: { - height: 55, - width: 55, - borderRadius: 50, - }, underlinedBox: { borderTopWidth: 1, borderColor: '#EEEEEE', diff --git a/packages/mobile/src/account/__snapshots__/Account.test.tsx.snap b/packages/mobile/src/account/__snapshots__/Account.test.tsx.snap index db6bf4e7f5f..1abe1ccafe4 100644 --- a/packages/mobile/src/account/__snapshots__/Account.test.tsx.snap +++ b/packages/mobile/src/account/__snapshots__/Account.test.tsx.snap @@ -152,16 +152,94 @@ exports[`Account renders correctly 1`] = ` + + + + + + backupKeyFlow6:backupAndRecovery + + + + + + + + + + - - editProfile - + + + invite + + + + + + + + - - - backupKey + editProfile - invite + analytics - analytics + celoLite `; -exports[`Account when dev mode active renders correctly 1`] = ` +exports[`Account renders correctly when dev mode active 1`] = ` + + + + + + backupKeyFlow6:backupAndRecovery + + + + + + + + + + - - editProfile - + + + invite + + + + + + + + - - - backupKey + editProfile - invite + analytics - analytics + celoLite + + + + Reset backup state + + + + + + + + + shareAnalytics + + + + + + + + + shareAnalytics_detail + + + + + +`; diff --git a/packages/mobile/src/account/__snapshots__/CeloLite.test.tsx.snap b/packages/mobile/src/account/__snapshots__/CeloLite.test.tsx.snap new file mode 100644 index 00000000000..0a3080730d9 --- /dev/null +++ b/packages/mobile/src/account/__snapshots__/CeloLite.test.tsx.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CeloLite renders correctly 1`] = ` + + + + + + + enableCeloLite + + + + + + + + + celoLiteDetail + + + + + +`; diff --git a/packages/mobile/src/account/__snapshots__/EditProfile.test.tsx.snap b/packages/mobile/src/account/__snapshots__/EditProfile.test.tsx.snap index 40c15eac623..790a848491b 100644 --- a/packages/mobile/src/account/__snapshots__/EditProfile.test.tsx.snap +++ b/packages/mobile/src/account/__snapshots__/EditProfile.test.tsx.snap @@ -50,20 +50,14 @@ exports[`renders the EditProfile Component 1`] = ` placeholder="yourName" rejectResponderTermination={true} style={ - Array [ - Object { - "fontFamily": "Hind-Regular", - }, - Object { - "borderColor": "#D1D5D8", - "borderRadius": 3, - "padding": 8, - }, - Object { - "backgroundColor": "#FFFFFF", - "flex": 1, - }, - ] + Object { + "backgroundColor": "#FFFFFF", + "borderColor": "#D1D5D8", + "borderRadius": 3, + "flex": 1, + "fontFamily": "Hind-Regular", + "padding": 8, + } } underlineColorAndroid="transparent" value="Test" diff --git a/packages/mobile/src/account/__snapshots__/Invite.test.tsx.snap b/packages/mobile/src/account/__snapshots__/Invite.test.tsx.snap index 7a4a6ac3c87..933b5345dd8 100644 --- a/packages/mobile/src/account/__snapshots__/Invite.test.tsx.snap +++ b/packages/mobile/src/account/__snapshots__/Invite.test.tsx.snap @@ -117,20 +117,14 @@ exports[`Invite renders correctly with no recipients 1`] = ` rejectResponderTermination={true} shouldShowClipboard={[Function]} style={ - Array [ - Object { - "fontFamily": "Hind-Regular", - }, - Object { - "borderColor": "#D1D5D8", - "borderRadius": 3, - "padding": 8, - }, - Object { - "backgroundColor": "#FFFFFF", - "flex": 1, - }, - ] + Object { + "backgroundColor": "#FFFFFF", + "borderColor": "#D1D5D8", + "borderRadius": 3, + "flex": 1, + "fontFamily": "Hind-Regular", + "padding": 8, + } } underlineColorAndroid="transparent" value="" @@ -355,20 +349,14 @@ exports[`Invite renders correctly with recipients 1`] = ` rejectResponderTermination={true} shouldShowClipboard={[Function]} style={ - Array [ - Object { - "fontFamily": "Hind-Regular", - }, - Object { - "borderColor": "#D1D5D8", - "borderRadius": 3, - "padding": 8, - }, - Object { - "backgroundColor": "#FFFFFF", - "flex": 1, - }, - ] + Object { + "backgroundColor": "#FFFFFF", + "borderColor": "#D1D5D8", + "borderRadius": 3, + "flex": 1, + "fontFamily": "Hind-Regular", + "padding": 8, + } } underlineColorAndroid="transparent" value="" diff --git a/packages/mobile/src/account/__snapshots__/Licenses.test.tsx.snap b/packages/mobile/src/account/__snapshots__/Licenses.test.tsx.snap new file mode 100644 index 00000000000..3c6640ba55c --- /dev/null +++ b/packages/mobile/src/account/__snapshots__/Licenses.test.tsx.snap @@ -0,0 +1,50 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Licenses renders correctly 1`] = ` + + + +`; diff --git a/packages/mobile/src/account/actions.ts b/packages/mobile/src/account/actions.ts index 75749f03db1..4ec731b7ab9 100644 --- a/packages/mobile/src/account/actions.ts +++ b/packages/mobile/src/account/actions.ts @@ -3,6 +3,7 @@ import { PaymentRequest } from 'src/account/types' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { DefaultEventNames } from 'src/analytics/constants' +// TODO(Rossy): Remove the _ACTION suffix from these actions for consistency with other other names export enum Actions { SET_NAME = 'ACCOUNT/SET_NAME', SET_PHONE_NUMBER = 'ACCOUNT/SET_PHONE_NUMBER', @@ -14,6 +15,8 @@ export enum Actions { SET_ACCOUNT_CREATION_TIME_ACTION = 'ACCOUNT/SET_ACCOUNT_CREATION_TIME_ACTION', SET_BACKUP_COMPLETED_ACTION = 'ACCOUNT/SET_BACKUP_COMPLETED_ACTION', SET_BACKUP_DELAYED_ACTION = 'ACCOUNT/SET_BACKUP_DELAYED_ACTION', + SET_SOCIAL_BACKUP_COMPLETED_ACTION = 'ACCOUNT/SET_SOCIAL_BACKUP_COMPLETED_ACTION', + RESET_BACKUP_STATE = 'ACCOUNT/RESET_BACKUP_STATE', UPDATE_PAYMENT_REQUESTS = 'ACCOUNT/UPDATE_PAYMENT_REQUESTS', DISMISS_EARN_REWARDS = 'ACCOUNT/DISMISS_EARN_REWARDS', DISMISS_INVITE_FRIENDS = 'ACCOUNT/DISMISS_INVITE_FRIENDS', @@ -66,6 +69,14 @@ export interface SetBackupDelayedAction { type: Actions.SET_BACKUP_DELAYED_ACTION } +export interface SetSocialBackupCompletedAction { + type: Actions.SET_SOCIAL_BACKUP_COMPLETED_ACTION +} + +export interface ResetBackupState { + type: Actions.RESET_BACKUP_STATE +} + export interface UpdatePaymentRequestsAction { type: Actions.UPDATE_PAYMENT_REQUESTS paymentRequests: PaymentRequest[] @@ -96,6 +107,8 @@ export type ActionTypes = | SetAccountCreationAction | SetBackupCompletedAction | SetBackupDelayedAction + | SetSocialBackupCompletedAction + | ResetBackupState | UpdatePaymentRequestsAction | DismissEarnRewards | DismissInviteFriends @@ -152,6 +165,14 @@ export const setBackupDelayed = (): SetBackupDelayedAction => ({ type: Actions.SET_BACKUP_DELAYED_ACTION, }) +export const setSocialBackupCompleted = (): SetSocialBackupCompletedAction => ({ + type: Actions.SET_SOCIAL_BACKUP_COMPLETED_ACTION, +}) + +export const resetBackupState = (): ResetBackupState => ({ + type: Actions.RESET_BACKUP_STATE, +}) + export const updatePaymentRequests = ( paymentRequests: PaymentRequest[] ): UpdatePaymentRequestsAction => ({ diff --git a/packages/mobile/src/account/reducer.ts b/packages/mobile/src/account/reducer.ts index 491dec6b593..b3d2453fa99 100644 --- a/packages/mobile/src/account/reducer.ts +++ b/packages/mobile/src/account/reducer.ts @@ -18,6 +18,7 @@ export interface State { accountCreationTime: number backupCompleted: boolean backupDelayedTime: number + socialBackupCompleted: boolean paymentRequests: PaymentRequest[] dismissedEarnRewards: boolean dismissedInviteFriends: boolean @@ -51,6 +52,7 @@ export const initialState = { paymentRequests: [], backupCompleted: false, backupDelayedTime: 0, + socialBackupCompleted: false, dismissedEarnRewards: false, dismissedInviteFriends: false, } @@ -115,6 +117,18 @@ export const reducer = (state: State | undefined = initialState, action: ActionT ...state, backupDelayedTime: getRemoteTime(), } + case Actions.SET_SOCIAL_BACKUP_COMPLETED_ACTION: + return { + ...state, + socialBackupCompleted: true, + } + case Actions.RESET_BACKUP_STATE: + return { + ...state, + backupCompleted: false, + socialBackupCompleted: false, + backupDelayedTime: 0, + } case Actions.UPDATE_PAYMENT_REQUESTS: return { ...state, diff --git a/packages/mobile/src/analytics/constants.ts b/packages/mobile/src/analytics/constants.ts index 31c7370b351..51e93472c26 100644 --- a/packages/mobile/src/analytics/constants.ts +++ b/packages/mobile/src/analytics/constants.ts @@ -86,30 +86,32 @@ export enum CustomEventNames { // Screen name: Backup_Phrase, Backup_Insist, Backup_Share, Backup_Set set_backup_phrase = 'set_backup_phrase', // (count # of taps on “Set Backup Phrase” in Backup_Phrase) [we should not track the actual value of this field, just whether the user filled it out] + set_social_backup = 'set_social_backup', // (count # of taps on "Set up Social Backup") delay_backup = 'delay_backup', // (Count # of taps on "Delay" button in Backup_Phrase) skip_backup = 'skip_backup', // (count # of taps on “Skip” button in Backup_Phrase) + view_backup_phrase = 'view_backup_phrase', // (count # of taps on "View Backup Phrase" after already backed up) + view_social_backup = 'view_social_backup', // (count # of taps on "View Social Backup" after already set up) + skip_social_backup = 'skip_social_backup', // (count # of taps on "Skip Social Backup" ) backup_cancel = 'backup_cancel', // (count # of taps on "Cancel" button in Backup_Phrase) insist_backup_phrase = 'insist_backup_phrase', // (count # of taps on “Set Backup Phrase” in Backup_Insist) insist_skip_backup = 'insist_skip_backup', // (count # of taps on “Do Later” in Backup_Insist) whatsapp_backup = 'whatsapp_backup', // (count # of taps on “Send with Whatsapp” in Backup_Share) - share_backup_continue = 'share_backup_continue', // (count # of taps on “Continue” button in Backup_Share) - confirm_backup_phrase = 'confirm_backup_phrase', // (count # of taps on “Set Backup Phrase” button in Backup_Set) - - // Screen name: Question_1, Question_2, Question_3, Question_4, Question_Incorrect, Backup_Confirmed - question_select1 = 'question_select1', // (track # of input selections on Question_1 screen) - question_select2 = 'question_select2', // (track # of input selections on Question_2 screen) - question_select3 = 'question_select3', // (track # of input selections on Question_3 screen) - question_select4 = 'question_select4', // (track # of input selections on Question_4 screen) - question_submit1 = 'question_submit1', // (track # of taps on “Submit” button for Question_1 screen) - question_submit2 = 'question_submit2', // (track # of taps on “Submit” button for Question_2 screen) - question_submit3 = 'question_submit3', // (track # of taps on “Submit” button for Question_3 screen) - question_submit4 = 'question_submit4', // (track # of taps on “Submit” button for Question_4 screen) - question_cancel1 = 'questions_cancel1', // (track # of taps on "Cancel" button on the Question_1 Screens) - question_cancel2 = 'questions_cancel2', // (track # of taps on "Cancel" button on the Question_2 Screens) - question_cancel3 = 'questions_cancel3', // (track # of taps on "Cancel" button on the Question_3 Screens) - question_cancel4 = 'questions_cancel4', // (track # of taps on "Cancel" button on the Question_4 Screens) - question_incorrect = 'question_incorrect', // (track # of taps on “See Backup Phrase” in Question_Incorrect) - questions_done = 'questions_done', // (track # of taps on “Done” button on the Backup_Confirmed screen) + backup_continue = 'backup_continue', // (count # of taps on “Continue” button in Backup_Phrase) + social_backup_continue = 'social_backup_continue', // (Count # of taps on "Backup with Friends" in Backup_Phrase) + + // Screen name: Backup_Quiz, Question_Incorrect, Backup_Confirmed + question_select = 'question_select', // (track # of input selections on Backup_Verify screen) + question_submit = 'question_submit', // (track # of taps on “Submit” button for Backup_Quiz screen) + question_cancel = 'questions_cancel', // (track # of taps on "Cancel" button on the Backup_Quiz Screens) + question_incorrect = 'question_incorrect', // (track # of taps on “See Backup Phrase” in Backup_Quiz) + question_done = 'question_done', // (track # of taps on “Done” button on the Backup_Confirmed screen) + + // Screen name: Backup_Verify + backup_paste = 'backup_paste', // (track # of pastes in input field for Backup_Verify screen) + backup_paste_submit = 'backup_paste_submit', // (track # of taps on "Submit" button for Backup_Verify screen) + backup_paste_cancel = 'backup_paste_cancel', // (track # of taps on "Cancel" button on the Backup_Verify screen) + backup_paste_incorrect = 'backup_paste_incorrect', // (track # of taps on "See Backup Phrase" in Backup_Verify screen) + backup_paste_done = 'backup_paste_done', // (track # of taps on "Done" button on the Backup_Verify screen) // Screens: Exchange_Tutorial, Exchange_Home, Exchange_Currency exchange_button = 'exchange_button', // count # of taps on the exchange button in Exchange_Home @@ -227,6 +229,7 @@ export const PROPERTY_PATH_WHITELIST = [ 'testnet', 'timeElapsed', 'title', + 'tti', 'txId', 'verificationIndex', 'verificationsRemaining', diff --git a/packages/mobile/src/app/AppLoading.tsx b/packages/mobile/src/app/AppLoading.tsx index 421ae38fdc1..accef6de110 100644 --- a/packages/mobile/src/app/AppLoading.tsx +++ b/packages/mobile/src/app/AppLoading.tsx @@ -3,8 +3,10 @@ import colors from '@celo/react-components/styles/colors' import * as React from 'react' import { withNamespaces, WithNamespaces } from 'react-i18next' import { StyleSheet, View } from 'react-native' +import SafeAreaView from 'react-native-safe-area-view' import { Namespaces } from 'src/i18n' -import { restartApp } from 'src/utils/AppRestart' +import { RESTART_APP_I18N_KEY, restartApp } from 'src/utils/AppRestart' + const SHOW_RESTART_BUTTON_TIMEOUT = 10000 interface State { @@ -40,19 +42,19 @@ export class AppLoading extends React.Component { const { t } = this.props return ( - + {this.state.showRestartButton && ( + + ) } } @@ -116,6 +102,9 @@ const styles = StyleSheet.create({ container: { flex: 1, }, + innerContainer: { + flex: 1, + }, preview: { flex: 1, justifyContent: 'flex-end', @@ -125,51 +114,43 @@ const styles = StyleSheet.create({ height: 200, width: 200, borderRadius: 4, - zIndex: 99, }, view: { flex: 1, - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, alignItems: 'center', justifyContent: 'center', }, - viewFillVertical: { + fillVertical: { backgroundColor: 'rgba(46, 51, 56, 0.3)', width: variables.width, flex: 1, }, - viewFillHorizontal: { + fillHorizontal: { backgroundColor: 'rgba(46, 51, 56, 0.3)', flex: 1, }, - viewCameraRow: { + cameraRow: { display: 'flex', flexDirection: 'row', }, - viewCameraContainer: { + cameraContainer: { height: 200, }, - viewInfoBox: { + infoBox: { paddingVertical: 9, paddingHorizontal: 5, backgroundColor: colors.dark, opacity: 1, marginTop: 15, + borderRadius: 3, + }, + infoText: { + ...fontStyles.bodySmall, + lineHeight: undefined, color: colors.white, - zIndex: 99, }, footerContainer: { - height: 50, - width: variables.width, - backgroundColor: 'white', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - textAlign: 'center', + backgroundColor: colors.background, }, footerIcon: { borderWidth: 1, @@ -177,9 +158,6 @@ const styles = StyleSheet.create({ borderColor: colors.celoGreen, padding: 4, }, - footerText: { - color: colors.celoGreen, - }, }) export default componentWithAnalytics( diff --git a/packages/mobile/src/qrcode/__snapshots__/NotAuthorizedView.test.tsx.snap b/packages/mobile/src/qrcode/__snapshots__/NotAuthorizedView.test.tsx.snap new file mode 100644 index 00000000000..e5339bf5932 --- /dev/null +++ b/packages/mobile/src/qrcode/__snapshots__/NotAuthorizedView.test.tsx.snap @@ -0,0 +1,124 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NotAuthorizedView renders correctly 1`] = ` + + + cameraNotAuthorizedTitle + + + cameraNotAuthorizedDescription + + + + + + cameraSettings + + + + + +`; diff --git a/packages/mobile/src/redux/reducers.ts b/packages/mobile/src/redux/reducers.ts index 7350f9fd555..00abdc2b82f 100644 --- a/packages/mobile/src/redux/reducers.ts +++ b/packages/mobile/src/redux/reducers.ts @@ -1,6 +1,5 @@ -import { combineReducers, Dispatch } from 'redux' +import { combineReducers } from 'redux' import { PersistState } from 'redux-persist' -import 'redux-thunk' import { reducer as account, State as AccountState } from 'src/account/reducer' import { reducer as alert, State as AlertState } from 'src/alert/reducer' import { appReducer as app, State as AppState } from 'src/app/reducers' @@ -80,7 +79,3 @@ export interface PersistedRootState { escrow: EscrowState localCurrency: LocalCurrencyState } - -export type GetStateType = () => RootState -// @ts-ignore -export type DispatchType = Dispatch diff --git a/packages/mobile/src/redux/sagas.ts b/packages/mobile/src/redux/sagas.ts index 454e2228d94..e68b040cd0a 100644 --- a/packages/mobile/src/redux/sagas.ts +++ b/packages/mobile/src/redux/sagas.ts @@ -17,8 +17,10 @@ import { inviteSaga } from 'src/invite/saga' import { localCurrencySaga } from 'src/localCurrency/saga' import { networkInfoSaga } from 'src/networkInfo/saga' import { sendSaga } from 'src/send/saga' +import { sentrySaga } from 'src/sentry/saga' import { stableTokenSaga } from 'src/stableToken/saga' import Logger from 'src/utils/Logger' +import { web3Saga } from 'src/web3/saga' const loggerBlacklist = [ 'persist/REHYDRATE', @@ -43,6 +45,9 @@ function* loggerSaga() { yield takeEvery('*', (action: AnyAction) => { if (action && action.type && loggerBlacklist.includes(action.type)) { + // Log only action type, but not the payload as it can have + // sensitive information. + Logger.debug('redux/saga@logger', `${action.type} (payload not logged)`) return } try { @@ -72,4 +77,6 @@ export function* rootSaga() { yield spawn(dappKitSaga) yield spawn(feesSaga) yield spawn(localCurrencySaga) + yield spawn(web3Saga) + yield spawn(sentrySaga) } diff --git a/packages/mobile/src/redux/selectors.test.ts b/packages/mobile/src/redux/selectors.test.ts index caf2e62df48..2e9a11fdbb5 100644 --- a/packages/mobile/src/redux/selectors.test.ts +++ b/packages/mobile/src/redux/selectors.test.ts @@ -1,4 +1,4 @@ -import { DAYS_TO_BACKUP, DAYS_TO_DELAY } from 'src/backup/Backup' +import { DAYS_TO_BACKUP, DAYS_TO_DELAY } from 'src/backup/utils' import { disabledDueToNoBackup } from 'src/redux/selectors' const DAYS_TO_MS = 24 * 60 * 60 * 1000 diff --git a/packages/mobile/src/redux/selectors.ts b/packages/mobile/src/redux/selectors.ts index 11708318d4a..dbdd8e77473 100644 --- a/packages/mobile/src/redux/selectors.ts +++ b/packages/mobile/src/redux/selectors.ts @@ -1,6 +1,6 @@ import { createSelector } from 'reselect' import { getPaymentRequests } from 'src/account/selectors' -import { DAYS_TO_BACKUP, DAYS_TO_DELAY } from 'src/backup/Backup' +import { DAYS_TO_BACKUP, DAYS_TO_DELAY } from 'src/backup/utils' import { BALANCE_OUT_OF_SYNC_THRESHOLD } from 'src/config' import { isGethConnectedSelector } from 'src/geth/reducer' import { RootState } from 'src/redux/reducers' diff --git a/packages/mobile/src/redux/store.ts b/packages/mobile/src/redux/store.ts index d4b9c7c8e76..ff58896d509 100644 --- a/packages/mobile/src/redux/store.ts +++ b/packages/mobile/src/redux/store.ts @@ -3,7 +3,6 @@ import { createMigrate, persistReducer, persistStore } from 'redux-persist' import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2' import storage from 'redux-persist/lib/storage' import createSagaMiddleware from 'redux-saga' -import thunk from 'redux-thunk' import { migrations } from 'src/redux/migrations' import rootReducer from 'src/redux/reducers' import { rootSaga } from 'src/redux/sagas' @@ -27,7 +26,7 @@ declare var window: any export const configureStore = (initialState = {}) => { const sagaMiddleware = createSagaMiddleware() - const middlewares = [thunk, sagaMiddleware] + const middlewares = [sagaMiddleware] const enhancers = [applyMiddleware(...middlewares)] @@ -41,12 +40,4 @@ export const configureStore = (initialState = {}) => { } const { store, persistor } = configureStore() - -// TODO(cmcewen): remove once we we remove thunk -const reduxStore = store - -export const getReduxStore = () => { - return reduxStore -} - export { store, persistor } diff --git a/packages/mobile/src/send/TransferConfirmationCard.tsx b/packages/mobile/src/send/TransferConfirmationCard.tsx index 3a10bce1769..91d871c753f 100644 --- a/packages/mobile/src/send/TransferConfirmationCard.tsx +++ b/packages/mobile/src/send/TransferConfirmationCard.tsx @@ -13,6 +13,11 @@ import { FAQ_LINK } from 'src/config' import { CURRENCY_ENUM } from 'src/geth/consts' import { Namespaces } from 'src/i18n' import { faucetIcon } from 'src/images/Images' +import { + useDollarsToLocalAmount, + useLocalCurrencyCode, + useLocalCurrencySymbol, +} from 'src/localCurrency/hooks' import { Recipient } from 'src/recipients/recipient' import { TransactionTypes } from 'src/transactions/reducer' import { getMoneyDisplayValue, getNetworkFeeDisplayValue } from 'src/utils/formatting' @@ -20,7 +25,7 @@ import { navigateToURI } from 'src/utils/linking' const iconSize = 40 -export interface OwnProps { +export interface TransferConfirmationCardProps { address?: string comment?: string value: BigNumber @@ -31,112 +36,127 @@ export interface OwnProps { recipient?: Recipient } +type Props = TransferConfirmationCardProps & WithNamespaces + // Bordered content placed in a ReviewFrame // Differs from TransferReviewCard which is used during Send flow, this is for completed txs -class TransferConfirmationCard extends React.Component { - onPressGoToFaq = () => { - navigateToURI(FAQ_LINK) - } +const onPressGoToFaq = () => { + navigateToURI(FAQ_LINK) +} - renderTopSection = () => { - const { address, recipient, type, e164PhoneNumber } = this.props - if ( - type === TransactionTypes.VERIFICATION_FEE || - type === TransactionTypes.NETWORK_FEE || - type === TransactionTypes.FAUCET - ) { - return - } else { - return ( - - ) - } +const renderTopSection = (props: Props) => { + const { address, recipient, type, e164PhoneNumber } = props + if ( + type === TransactionTypes.VERIFICATION_FEE || + type === TransactionTypes.NETWORK_FEE || + type === TransactionTypes.FAUCET + ) { + return + } else { + return ( + + ) } +} - renderAmountSection = () => { - const { currency, type, value } = this.props - - switch (type) { - case TransactionTypes.INVITE_SENT: // fallthrough - case TransactionTypes.INVITE_RECEIVED: - return null - case TransactionTypes.NETWORK_FEE: - return ( - - ) - default: - return ( - - ) - } - } +const renderAmountSection = (props: Props) => { + const { currency, type, value } = props - renderBottomSection = () => { - const { t, currency, comment, type, value } = this.props + // tslint:disable react-hooks-nesting + const localCurrencyCode = useLocalCurrencyCode() + const localCurrencySymbol = useLocalCurrencySymbol() + const localValue = useDollarsToLocalAmount(value) || 0 + // tslint:enable react-hooks-nesting + const transactionValue = getMoneyDisplayValue( + currency === CURRENCY_ENUM.DOLLAR && localCurrencyCode ? localValue : value + ) - if (type === TransactionTypes.VERIFICATION_FEE) { - return {t('receiveFlow8:verificationMessage')} - } else if (type === TransactionTypes.FAUCET) { + switch (type) { + case TransactionTypes.INVITE_SENT: // fallthrough + case TransactionTypes.INVITE_RECEIVED: + return null + case TransactionTypes.NETWORK_FEE: return ( - - {t('receiveFlow8:receivedAmountFromCelo.0')} - {CURRENCIES[currency].symbol} - {getMoneyDisplayValue(this.props.value)} - {t('receiveFlow8:receivedAmountFromCelo.1')} - - ) - } else if (type === TransactionTypes.NETWORK_FEE) { - return ( - - - {t('walletFlow5:networkFeeExplanation.0')} - - {t('walletFlow5:networkFeeExplanation.1')} - - - + ) - } else if (type === TransactionTypes.INVITE_SENT || type === TransactionTypes.INVITE_RECEIVED) { + default: return ( - - - - - {t('inviteFlow11:inviteFee')} - {type === TransactionTypes.INVITE_SENT ? ( - {t('inviteFlow11:whySendFees')} - ) : ( - {t('inviteFlow11:whyReceiveFees')} - )} - - - + ) - } else if (comment) { - // When we want to add more info to the send tx drilldown, that will go here - return {comment} - } } +} + +const renderBottomSection = (props: Props) => { + const { t, currency, comment, type, value } = props - render() { + if (type === TransactionTypes.VERIFICATION_FEE) { + return {t('receiveFlow8:verificationMessage')} + } else if (type === TransactionTypes.FAUCET) { return ( - - {this.renderTopSection()} - {this.renderAmountSection()} - {this.renderBottomSection()} + + {t('receiveFlow8:receivedAmountFromCelo.0')} + {CURRENCIES[currency].symbol} + {getMoneyDisplayValue(props.value)} + {t('receiveFlow8:receivedAmountFromCelo.1')} + + ) + } else if (type === TransactionTypes.NETWORK_FEE) { + return ( + + + {t('walletFlow5:networkFeeExplanation.0')} + + {t('walletFlow5:networkFeeExplanation.1')} + + + + ) + } else if (type === TransactionTypes.INVITE_SENT || type === TransactionTypes.INVITE_RECEIVED) { + return ( + + + + + {t('inviteFlow11:inviteFee')} + {type === TransactionTypes.INVITE_SENT ? ( + {t('inviteFlow11:whySendFees')} + ) : ( + {t('inviteFlow11:whyReceiveFees')} + )} + + ) + } else if (comment) { + // When we want to add more info to the send tx drilldown, that will go here + return {comment} } } +export function TransferConfirmationCard(props: Props) { + return ( + + {renderTopSection(props)} + {renderAmountSection(props)} + {renderBottomSection(props)} + + ) +} + const style = StyleSheet.create({ container: { flex: 1, diff --git a/packages/mobile/src/send/__snapshots__/SendAmount.test.tsx.snap b/packages/mobile/src/send/__snapshots__/SendAmount.test.tsx.snap index c9ef2e05540..052ad3d60d7 100644 --- a/packages/mobile/src/send/__snapshots__/SendAmount.test.tsx.snap +++ b/packages/mobile/src/send/__snapshots__/SendAmount.test.tsx.snap @@ -264,20 +264,14 @@ exports[`SendAmount renders correctly for request payment confirmation 1`] = ` placeholderTextColor="#A3EBC6" rejectResponderTermination={true} style={ - Array [ - Object { - "fontFamily": "Hind-Regular", - }, - Object { - "borderColor": "#D1D5D8", - "borderRadius": 3, - "padding": 8, - }, - Object { - "backgroundColor": "#FFFFFF", - "flex": 1, - }, - ] + Object { + "backgroundColor": "#FFFFFF", + "borderColor": "#D1D5D8", + "borderRadius": 3, + "flex": 1, + "fontFamily": "Hind-Regular", + "padding": 8, + } } title="MXN" underlineColorAndroid="transparent" @@ -372,20 +366,14 @@ exports[`SendAmount renders correctly for request payment confirmation 1`] = ` placeholderTextColor="rgba(0, 0, 0, .4)" rejectResponderTermination={true} style={ - Array [ - Object { - "fontFamily": "Hind-Regular", - }, - Object { - "borderColor": "#D1D5D8", - "borderRadius": 3, - "padding": 8, - }, - Object { - "backgroundColor": "#FFFFFF", - "flex": 1, - }, - ] + Object { + "backgroundColor": "#FFFFFF", + "borderColor": "#D1D5D8", + "borderRadius": 3, + "flex": 1, + "fontFamily": "Hind-Regular", + "padding": 8, + } } title="for" underlineColorAndroid="transparent" diff --git a/packages/mobile/src/send/__snapshots__/TransferConfirmationCard.test.tsx.snap b/packages/mobile/src/send/__snapshots__/TransferConfirmationCard.test.tsx.snap index e4e7c430630..36d96a6e22f 100644 --- a/packages/mobile/src/send/__snapshots__/TransferConfirmationCard.test.tsx.snap +++ b/packages/mobile/src/send/__snapshots__/TransferConfirmationCard.test.tsx.snap @@ -79,7 +79,7 @@ exports[`TransferConfirmationCard renders correctly for faucet drilldown 1`] = ` ] } > - 100.00 + 133.00 - 100.00 + 133.00 @@ -491,7 +491,7 @@ exports[`TransferConfirmationCard renders correctly for sent transaction drilldo ] } > - 100.00 + 133.00 - 0.30 + 0.39 async ( - dispatch: DispatchType, - getState: GetStateType -) => { - const state = getState() - const account = currentAccountSelector(state) +export function* initializeSentryUserContext() { + const account = yield select(currentAccountSelector) + if (!account) { return } - const phoneNumber = - e164NumberSelector(state) || (await DeviceInfo.getPhoneNumber()) || 'unknownPhoneNumber' + const phoneNumber = yield select(e164NumberSelector) || + (yield call([DeviceInfo, 'getPhoneNumber'])) || + 'unknownPhoneNumber' Logger.debug( TAG, 'initializeSentryUserContext', diff --git a/packages/mobile/src/sentry/actions.ts b/packages/mobile/src/sentry/actions.ts new file mode 100644 index 00000000000..7dfef98bdc3 --- /dev/null +++ b/packages/mobile/src/sentry/actions.ts @@ -0,0 +1,11 @@ +export enum Actions { + INITIALIZE_SENTRY_USER_CONTEXT = 'SENTRY/INITIALIZE_SENTRY_USER_CONTEXT', +} + +export interface InitializeSentryUserContext { + type: Actions.INITIALIZE_SENTRY_USER_CONTEXT +} + +export const initializeSentryUserContext = (): InitializeSentryUserContext => ({ + type: Actions.INITIALIZE_SENTRY_USER_CONTEXT, +}) diff --git a/packages/mobile/src/sentry/saga.ts b/packages/mobile/src/sentry/saga.ts new file mode 100644 index 00000000000..a7ee1ff0d7a --- /dev/null +++ b/packages/mobile/src/sentry/saga.ts @@ -0,0 +1,8 @@ +import { call, take } from 'redux-saga/effects' +import { Actions } from 'src/sentry/actions' +import { initializeSentryUserContext } from 'src/sentry/Sentry' + +export function* sentrySaga() { + yield take(Actions.INITIALIZE_SENTRY_USER_CONTEXT) + yield call(initializeSentryUserContext) +} diff --git a/packages/mobile/src/shared/BackupPrompt.tsx b/packages/mobile/src/shared/BackupPrompt.tsx index 854967e8e97..c268eec07bb 100644 --- a/packages/mobile/src/shared/BackupPrompt.tsx +++ b/packages/mobile/src/shared/BackupPrompt.tsx @@ -28,7 +28,7 @@ const mapStateToProps = (state: RootState): StateProps => { export class BackupPrompt extends React.Component { goToBackup = () => { - navigate(Screens.Backup) + navigate(Screens.BackupIntroduction) } isVisible = () => { diff --git a/packages/mobile/src/shared/__snapshots__/BackupPrompt.test.tsx.snap b/packages/mobile/src/shared/__snapshots__/BackupPrompt.test.tsx.snap index 60b5ed30463..3e6cb91056c 100644 --- a/packages/mobile/src/shared/__snapshots__/BackupPrompt.test.tsx.snap +++ b/packages/mobile/src/shared/__snapshots__/BackupPrompt.test.tsx.snap @@ -83,10 +83,9 @@ exports[`BackupPrompt renders correctly 1`] = ` "alignItems": "center", "alignSelf": "flex-start", "borderRadius": 2, - "minWidth": 160, + "flexDirection": "row", "paddingHorizontal": 16, "paddingVertical": 6, - "textAlign": "center", }, Object { "backgroundColor": "transparent", @@ -113,10 +112,12 @@ exports[`BackupPrompt renders correctly 1`] = ` }, Object { "lineHeight": 20, + "textAlign": "center", }, Object { "color": "#42D689", }, + null, Object { "color": "#FFFFFF", }, diff --git a/packages/mobile/src/stableToken/saga.test.ts b/packages/mobile/src/stableToken/saga.test.ts index d81baf75eb6..17629676130 100644 --- a/packages/mobile/src/stableToken/saga.test.ts +++ b/packages/mobile/src/stableToken/saga.test.ts @@ -1,5 +1,5 @@ import { CURRENCY_ENUM } from '@celo/utils/src/currencies' -import { getErc20Balance, getStableTokenContract } from '@celo/walletkit' +import BigNumber from 'bignumber.js' import { expectSaga } from 'redux-saga-test-plan' import { call } from 'redux-saga/effects' import { waitWeb3LastBlock } from 'src/networkInfo/saga' @@ -7,13 +7,19 @@ import { fetchDollarBalance, setBalance, transferStableToken } from 'src/stableT import { stableTokenFetch, stableTokenTransfer } from 'src/stableToken/saga' import { addStandbyTransaction, removeStandbyTransaction } from 'src/transactions/actions' import { TransactionStatus, TransactionTypes } from 'src/transactions/reducer' -import { createMockContract, createMockStore } from 'test/utils' +import { + createMockContract, + createMockStore, + mockContractKitBalance, + mockContractKitContract, +} from 'test/utils' import { mockAccount } from 'test/values' const now = Date.now() Date.now = jest.fn(() => now) const BALANCE = '45' +const BALANCE_IN_WEI = '450000000000' const TX_ID = '1234' const COMMENT = 'a comment' @@ -24,6 +30,14 @@ jest.mock('@celo/walletkit', () => ({ getErc20Balance: jest.fn(() => BALANCE), })) +jest.mock('src/web3/contracts', () => ({ + contractKit: { + contracts: { + getStableToken: () => mockContractKitContract, + }, + }, +})) + jest.mock('src/web3/actions', () => ({ ...jest.requireActual('src/web3/actions'), unlockAccount: jest.fn(async () => true), @@ -44,13 +58,14 @@ describe('stableToken saga', () => { jest.useRealTimers() it('should fetch the balance and put the new balance', async () => { + mockContractKitBalance.mockReturnValueOnce(new BigNumber(BALANCE_IN_WEI)) + await expectSaga(stableTokenFetch) .provide([[call(waitWeb3LastBlock), true]]) .withState(state) .dispatch(fetchDollarBalance()) .put(setBalance(BALANCE)) .run() - expect(getErc20Balance).toHaveBeenCalled() }) it('should add a standby transaction and dispatch a sendAndMonitorTransaction', async () => { @@ -93,16 +108,6 @@ describe('stableToken saga', () => { .run() }) - // TODO(cmcewen): Figure out how to mock this so we can get actual contract calls - it('should call the contract getter', async () => { - await expectSaga(stableTokenTransfer) - .provide([[call(waitWeb3LastBlock), true]]) - .withState(state) - .dispatch(TRANSFER_ACTION) - .run() - expect(getStableTokenContract).toHaveBeenCalled() - }) - it('should remove standby transaction when pin unlock fails', async () => { unlockAccount.mockImplementationOnce(async () => false) diff --git a/packages/mobile/src/stableToken/saga.ts b/packages/mobile/src/stableToken/saga.ts index 992811545af..e723b2c2100 100644 --- a/packages/mobile/src/stableToken/saga.ts +++ b/packages/mobile/src/stableToken/saga.ts @@ -8,7 +8,7 @@ const tag = 'stableToken/saga' export const stableTokenFetch = tokenFetchFactory({ actionName: Actions.FETCH_BALANCE, - contractGetter: getStableTokenContract, + token: CURRENCY_ENUM.DOLLAR, actionCreator: setBalance, tag, }) diff --git a/packages/mobile/src/tokens/saga.ts b/packages/mobile/src/tokens/saga.ts index 5fb7e9d9c45..e0613912947 100644 --- a/packages/mobile/src/tokens/saga.ts +++ b/packages/mobile/src/tokens/saga.ts @@ -1,5 +1,5 @@ import { retryAsync } from '@celo/utils/src/async' -import { getErc20Balance, getGoldTokenContract, getStableTokenContract } from '@celo/walletkit' +import { getGoldTokenContract, getStableTokenContract } from '@celo/walletkit' import BigNumber from 'bignumber.js' import { call, put, take, takeEvery } from 'redux-saga/effects' import { showError } from 'src/alert/actions' @@ -11,31 +11,71 @@ import { addStandbyTransaction, removeStandbyTransaction } from 'src/transaction import { TransactionStatus, TransactionTypes } from 'src/transactions/reducer' import { sendAndMonitorTransaction } from 'src/transactions/saga' import Logger from 'src/utils/Logger' -import { web3 } from 'src/web3/contracts' -import { getConnectedAccount, getConnectedUnlockedAccount } from 'src/web3/saga' +import { contractKit, web3 } from 'src/web3/contracts' +import { getConnectedAccount, getConnectedUnlockedAccount, waitForWeb3Sync } from 'src/web3/saga' import * as utf8 from 'utf8' const TAG = 'tokens/saga' +// The number of wei that represent one unit in a contract +const contractWeiPerUnit: { [key in CURRENCY_ENUM]: BigNumber | null } = { + [CURRENCY_ENUM.GOLD]: null, + [CURRENCY_ENUM.DOLLAR]: null, +} + +async function getWeiPerUnit(token: CURRENCY_ENUM) { + let weiPerUnit = contractWeiPerUnit[token] + if (!weiPerUnit) { + const contract = await getTokenContract(token) + const decimals = await contract.decimals() + weiPerUnit = new BigNumber(10).pow(decimals) + contractWeiPerUnit[token] = weiPerUnit + } + return weiPerUnit +} + +export async function convertFromContractDecimals(value: BigNumber, token: CURRENCY_ENUM) { + const weiPerUnit = await getWeiPerUnit(token) + return value.dividedBy(weiPerUnit) +} + +export async function convertToContractDecimals(value: BigNumber, token: CURRENCY_ENUM) { + const weiPerUnit = await getWeiPerUnit(token) + return value.times(weiPerUnit) +} + +export async function getTokenContract(token: CURRENCY_ENUM) { + Logger.debug(TAG + '@getTokenContract', `Fetching contract for ${token}`) + await waitForWeb3Sync() + let tokenContract: any + switch (token) { + case CURRENCY_ENUM.GOLD: + tokenContract = await contractKit.contracts.getGoldToken() + break + case CURRENCY_ENUM.DOLLAR: + tokenContract = await contractKit.contracts.getStableToken() + break + default: + throw new Error(`Could not fetch contract for unknown token ${token}`) + } + return tokenContract +} + interface TokenFetchFactory { actionName: string - contractGetter: (web3: any) => any + token: CURRENCY_ENUM actionCreator: (balance: string) => any tag: string } -export function tokenFetchFactory({ - actionName, - contractGetter, - actionCreator, - tag, -}: TokenFetchFactory) { +export function tokenFetchFactory({ actionName, token, actionCreator, tag }: TokenFetchFactory) { function* tokenFetch() { try { Logger.debug(tag, 'Fetching balance') const account = yield call(getConnectedAccount) - const tokenContract = yield call(contractGetter, web3) - const balance = yield call(getErc20Balance, tokenContract, account, web3) + const tokenContract = yield call(getTokenContract, token) + const balanceInWei: BigNumber = yield call([tokenContract, tokenContract.balanceOf], account) + const balance: BigNumber = yield call(convertFromContractDecimals, balanceInWei, token) CeloAnalytics.track(CustomEventNames.fetch_balance) yield put(actionCreator(balance.toString())) } catch (error) { @@ -94,16 +134,13 @@ export async function createTransaction( return tx } -export async function fetchTokenBalanceWithRetry( - contractGetter: typeof getStableTokenContract | typeof getGoldTokenContract, - account: string -) { - Logger.debug(TAG + '@fetchTokenBalanceWithRetry', 'Checking account balance', account) - const tokenContract = await contractGetter(web3) +export async function fetchTokenBalanceInWeiWithRetry(token: CURRENCY_ENUM, account: string) { + Logger.debug(TAG + '@fetchTokenBalanceInWeiWithRetry', 'Checking account balance', account) + const tokenContract = await getTokenContract(token) // Retry needed here because it's typically the app's first tx and seems to fail on occasion - const tokenBalance = await retryAsync(tokenContract.methods.balanceOf(account).call, 3, []) - Logger.debug(TAG + '@fetchTokenBalanceWithRetry', 'Account balance', tokenBalance) - return new BigNumber(tokenBalance) + const balanceInWei = await retryAsync(tokenContract.balanceOf, 3, [account]) + Logger.debug(TAG + '@fetchTokenBalanceInWeiWithRetry', 'Account balance', balanceInWei.toString()) + return balanceInWei } export function tokenTransferFactory({ diff --git a/packages/mobile/src/transactions/ExchangeFeedItem.tsx b/packages/mobile/src/transactions/ExchangeFeedItem.tsx index fc784b0a54a..e6ec90a2a10 100644 --- a/packages/mobile/src/transactions/ExchangeFeedItem.tsx +++ b/packages/mobile/src/transactions/ExchangeFeedItem.tsx @@ -10,6 +10,13 @@ import { Image, StyleSheet, Text, View } from 'react-native' import { HomeExchangeFragment } from 'src/apollo/types' import { CURRENCY_ENUM, resolveCurrency } from 'src/geth/consts' import { Namespaces } from 'src/i18n' +import { LocalCurrencyCode, LocalCurrencySymbol } from 'src/localCurrency/consts' +import { convertDollarsToLocalAmount } from 'src/localCurrency/convert' +import { + useExchangeRate, + useLocalCurrencyCode, + useLocalCurrencySymbol, +} from 'src/localCurrency/hooks' import { navigateToExchangeReview } from 'src/transactions/actions' import { ExchangeStandby, TransactionStatus } from 'src/transactions/reducer' import { getMoneyDisplayValue } from 'src/utils/formatting' @@ -21,68 +28,132 @@ type Props = (HomeExchangeFragment | ExchangeStandby) & showGoldAmount: boolean } -type ExchangeProps = ReturnType +type ExchangeInputProps = Props & { + localCurrencyCode: LocalCurrencyCode | null + localCurrencySymbol: LocalCurrencySymbol | null + localExchangeRate: number | null | undefined +} +type ExchangeProps = + | ReturnType + | ReturnType + +function getLocalAmount( + dollarAmount: BigNumber.Value, + localExchangeRate: number | null | undefined +) { + const localAmount = convertDollarsToLocalAmount(dollarAmount, localExchangeRate) + if (!localAmount) { + return null + } -function getDollarExchangeProps({ inValue, outValue }: Props) { + return localAmount.toString() +} + +function getDollarExchangeProps({ + inValue: dollarAmount, + outValue: goldAmount, + localCurrencyCode, + localCurrencySymbol, + localExchangeRate, +}: ExchangeInputProps) { + const localAmount = getLocalAmount(dollarAmount, localExchangeRate) return { icon: require('src/transactions/ExchangeGreenGold.png'), - dollarAmount: inValue, + dollarAmount, dollarDirection: '-', - goldAmount: outValue, + localCurrencyCode, + localCurrencySymbol, + localAmount, + goldAmount, goldDirection: '', + inValue: localCurrencyCode ? localAmount : dollarAmount, inColor: colors.celoGreen, + outValue: goldAmount, outColor: colors.celoGold, } } -function getGoldExchangeProps({ inValue, outValue }: Props) { +function getGoldExchangeProps({ + inValue: goldAmount, + outValue: dollarAmount, + localCurrencyCode, + localCurrencySymbol, + localExchangeRate, +}: ExchangeInputProps) { + const localAmount = getLocalAmount(dollarAmount, localExchangeRate) return { icon: require('src/transactions/ExchangeGoldGreen.png'), - dollarAmount: outValue, + dollarAmount, dollarDirection: '', - goldAmount: inValue, + localCurrencyCode, + localCurrencySymbol, + localAmount, + goldAmount, goldDirection: '-', + inValue: goldAmount, inColor: colors.celoGold, + outValue: localCurrencyCode ? localAmount : dollarAmount, outColor: colors.celoGreen, } } -function getGoldAmountProps({ goldAmount, goldDirection }: ExchangeProps) { +function getGoldAmountProps({ goldAmount: amount, goldDirection: amountDirection }: ExchangeProps) { return { - amount: goldAmount, - amountDirection: goldDirection, + amount, + amountDirection, amountColor: colors.celoGold, + displayAmount: `${amountDirection}${getMoneyDisplayValue(amount)}`, } } -function getDollarAmountProps({ dollarAmount, dollarDirection }: ExchangeProps) { +function getDollarAmountProps({ + dollarAmount, + dollarDirection: amountDirection, + localCurrencyCode, + localCurrencySymbol, + localAmount, +}: ExchangeProps) { + const amount = localCurrencyCode ? localAmount : dollarAmount return { - amount: dollarAmount, - amountDirection: dollarDirection, + amount, + amountDirection, amountColor: colors.celoGreen, + displayAmount: amount + ? `${amountDirection}${localCurrencySymbol + + getMoneyDisplayValue(amount) + + (localCurrencyCode || '')}` + : '-', } } export function ExchangeFeedItem(props: Props) { - const { showGoldAmount, inSymbol, inValue, outValue, status, timestamp, t, i18n } = props + const { showGoldAmount, inSymbol, status, timestamp, t, i18n } = props + + const localCurrencyCode = useLocalCurrencyCode() + const localCurrencySymbol = useLocalCurrencySymbol() + const localExchangeRate = useExchangeRate() const onPress = () => { navigateToExchangeReview(timestamp, { makerToken: resolveCurrency(inSymbol), - makerAmount: new BigNumber(inValue), - takerAmount: new BigNumber(outValue), + makerAmount: new BigNumber(props.inValue), + takerAmount: new BigNumber(props.outValue), }) } const inCurrency = resolveCurrency(inSymbol) + const exchangeInputProps = { ...props, localCurrencyCode, localCurrencySymbol, localExchangeRate } const exchangeProps = inCurrency === CURRENCY_ENUM.DOLLAR - ? getDollarExchangeProps(props) - : getGoldExchangeProps(props) - const { amount, amountDirection, amountColor } = showGoldAmount + ? getDollarExchangeProps(exchangeInputProps) + : getGoldExchangeProps(exchangeInputProps) + const amountProps = showGoldAmount ? getGoldAmountProps(exchangeProps) : getDollarAmountProps(exchangeProps) + const { inValue, outValue } = exchangeProps + const { displayAmount, amountDirection, amountColor } = amountProps + const timeFormatted = formatFeedTime(timestamp, i18n) const dateTimeFormatted = getDatetimeDisplayString(timestamp, t, i18n) const isPending = status === TransactionStatus.Pending @@ -111,19 +182,18 @@ export function ExchangeFeedItem(props: Props) { styles.amount, ]} > - {amountDirection} - {getMoneyDisplayValue(amount)} + {displayAmount} - {getMoneyDisplayValue(inValue)} + {inValue ? getMoneyDisplayValue(inValue) : '-'} - {getMoneyDisplayValue(outValue)} + {outValue ? getMoneyDisplayValue(outValue) : '-'} diff --git a/packages/mobile/src/transactions/TransferFeedItem.tsx b/packages/mobile/src/transactions/TransferFeedItem.tsx index e348645d403..a0feb763e87 100644 --- a/packages/mobile/src/transactions/TransferFeedItem.tsx +++ b/packages/mobile/src/transactions/TransferFeedItem.tsx @@ -11,7 +11,11 @@ import { CURRENCIES, CURRENCY_ENUM, resolveCurrency } from 'src/geth/consts' import { Namespaces } from 'src/i18n' import { AddressToE164NumberType } from 'src/identity/reducer' import { Invitees } from 'src/invite/actions' -import { useDollarsToLocalAmount, useLocalCurrencyCode } from 'src/localCurrency/hooks' +import { + useDollarsToLocalAmount, + useLocalCurrencyCode, + useLocalCurrencySymbol, +} from 'src/localCurrency/hooks' import { getRecipientFromAddress, NumberToRecipient } from 'src/recipients/recipient' import { navigateToPaymentTransferReview } from 'src/transactions/actions' import { TransactionStatus, TransactionTypes, TransferStandby } from 'src/transactions/reducer' @@ -140,12 +144,19 @@ export function TransferFeedItem(props: Props) { } = props const localCurrencyCode = useLocalCurrencyCode() + const localCurrencySymbol = useLocalCurrencySymbol() const localValue = useDollarsToLocalAmount(value) const timeFormatted = formatFeedTime(timestamp, i18n) const dateTimeFormatted = getDatetimeDisplayString(timestamp, t, i18n) const currency = resolveCurrency(symbol) const currencyStyle = getCurrencyStyles(currency, type) const isPending = status === TransactionStatus.Pending + const transactionValue = + type === TransactionTypes.NETWORK_FEE + ? getNetworkFeeDisplayValue(props.value) + : showLocalCurrency && localCurrencyCode + ? getMoneyDisplayValue(localValue || 0) + : getMoneyDisplayValue(props.value) const { title, info, recipient } = getTransferFeedParams( type, @@ -179,9 +190,9 @@ export function TransferFeedItem(props: Props) { ]} > {currencyStyle.direction} - {type === TransactionTypes.NETWORK_FEE - ? getNetworkFeeDisplayValue(props.value) - : getMoneyDisplayValue(props.value)} + {showLocalCurrency && localCurrencySymbol} + {transactionValue} + {showLocalCurrency && localCurrencyCode} {!!info && {info}} @@ -205,16 +216,6 @@ export function TransferFeedItem(props: Props) { {' ' + timeFormatted} )} - {showLocalCurrency && - !!localCurrencyCode && - localValue && ( - - {t('localCurrencyValue', { - localValue: `${currencyStyle.direction}${getMoneyDisplayValue(localValue)}`, - localCurrencyCode, - })} - - )} diff --git a/packages/mobile/src/transactions/__snapshots__/ExchangeFeedItem.test.tsx.snap b/packages/mobile/src/transactions/__snapshots__/ExchangeFeedItem.test.tsx.snap index 9af397a1dff..b482a3ba934 100644 --- a/packages/mobile/src/transactions/__snapshots__/ExchangeFeedItem.test.tsx.snap +++ b/packages/mobile/src/transactions/__snapshots__/ExchangeFeedItem.test.tsx.snap @@ -96,7 +96,6 @@ exports[`ExchangeFeedItem renders correctly 1`] = ` ] } > - 10.00 @@ -123,7 +122,7 @@ exports[`ExchangeFeedItem renders correctly 1`] = ` ] } > - 1.00 + 1.33 - - 100.00 + $ + 133.00 + MXN undefined - - localCurrencyValue - @@ -505,8 +486,7 @@ exports[`renders for gold to dollar exchange properly 1`] = ` ] } > - - - 20.00 + -$26.60MXN - 20.00 + 26.60 + $ <0.001 + MXN undefined - - localCurrencyValue - @@ -934,8 +895,7 @@ exports[`renders for gold to dollar exchange properly 1`] = ` ] } > - - - 30.00 + -$39.90MXN - 30.00 + 39.90 - - 100.00 + $ + 133.00 + MXN undefined - - localCurrencyValue - @@ -1428,8 +1369,7 @@ exports[`renders for loading 1`] = ` ] } > - - - 20.00 + -$26.60MXN - 20.00 + 26.60 + $ <0.001 + MXN undefined - - localCurrencyValue - @@ -1942,7 +1863,9 @@ exports[`renders for no transactions 1`] = ` } > - - 100.00 + $ + 133.00 + MXN undefined - - localCurrencyValue - @@ -2141,8 +2043,7 @@ exports[`renders for no transactions 1`] = ` ] } > - - - 20.00 + -$26.60MXN - 20.00 + 26.60 + $ <0.001 + MXN undefined - - localCurrencyValue - @@ -2655,7 +2537,9 @@ exports[`renders for standbyTransactions 1`] = ` } > - - 100.00 + $ + 133.00 + MXN undefined - - localCurrencyValue - @@ -2854,8 +2717,7 @@ exports[`renders for standbyTransactions 1`] = ` ] } > - - - 20.00 + -$26.60MXN - 20.00 + 26.60 + $ <0.001 + MXN undefined - - localCurrencyValue - diff --git a/packages/mobile/src/transactions/__snapshots__/TransferFeedItem.test.tsx.snap b/packages/mobile/src/transactions/__snapshots__/TransferFeedItem.test.tsx.snap index c7e8d92a463..8601b725387 100644 --- a/packages/mobile/src/transactions/__snapshots__/TransferFeedItem.test.tsx.snap +++ b/packages/mobile/src/transactions/__snapshots__/TransferFeedItem.test.tsx.snap @@ -95,7 +95,9 @@ exports[`transfer feed item renders correctly for <0.000001 network fee 1`] = ` } > + $ <0.001 + MXN - - localCurrencyValue - - + /> @@ -246,7 +226,9 @@ exports[`transfer feed item renders correctly for faucet 1`] = ` } > - 100.00 + $ + 133.00 + MXN - - localCurrencyValue - - + /> @@ -427,7 +387,9 @@ exports[`transfer feed item renders correctly for known received 1`] = ` } > - 100.00 + $ + 133.00 + MXN - - localCurrencyValue - - + /> @@ -594,7 +534,9 @@ exports[`transfer feed item renders correctly for known sent 1`] = ` } > - - 100.00 + $ + 133.00 + MXN - - localCurrencyValue - - + /> @@ -731,7 +651,9 @@ exports[`transfer feed item renders correctly for network fee 1`] = ` } > + $ 0.002 + MXN - - localCurrencyValue - - + /> @@ -912,7 +812,9 @@ exports[`transfer feed item renders correctly for received 1`] = ` } > - 100.00 + $ + 133.00 + MXN - - localCurrencyValue - - + /> @@ -1049,7 +929,9 @@ exports[`transfer feed item renders correctly for received invite 1`] = ` } > - 1.00 + $ + 1.33 + MXN - - localCurrencyValue - - + /> @@ -1230,7 +1090,9 @@ exports[`transfer feed item renders correctly for received with encrypted commen } > - 100.00 + $ + 133.00 + MXN - - localCurrencyValue - - + /> @@ -1411,7 +1251,9 @@ exports[`transfer feed item renders correctly for sent 1`] = ` } > - - 100.00 + $ + 133.00 + MXN - - localCurrencyValue - - + /> @@ -1548,7 +1368,9 @@ exports[`transfer feed item renders correctly for sent invite 1`] = ` } > - - 1.00 + $ + 1.33 + MXN - - localCurrencyValue - - + /> @@ -1729,7 +1529,9 @@ exports[`transfer feed item renders correctly for sent transaction 1`] = ` } > - - 1.00 + $ + 1.33 + MXN - - localCurrencyValue - - + /> @@ -1910,7 +1690,9 @@ exports[`transfer feed item renders correctly for sent with encrypted comment 1` } > - - 1.00 + $ + 1.33 + MXN - - localCurrencyValue - - + /> @@ -2061,7 +1821,9 @@ exports[`transfer feed item renders correctly for verification fee 1`] = ` } > - - 0.33 + $ + 0.43 + MXN - - localCurrencyValue - - + /> @@ -2231,7 +1971,9 @@ exports[`transfer feed item renders correctly for verification reward 1`] = ` } > - 1.00 + $ + 1.33 + MXN - - localCurrencyValue - - + /> diff --git a/packages/mobile/src/transactions/actions.ts b/packages/mobile/src/transactions/actions.ts index 534fb5bb1ba..6f21490eed3 100644 --- a/packages/mobile/src/transactions/actions.ts +++ b/packages/mobile/src/transactions/actions.ts @@ -3,7 +3,7 @@ import i18n from 'src/i18n' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { ConfirmationInput as SendConfirmationCardProps } from 'src/send/SendConfirmation' -import { OwnProps as TransferConfirmationCardProps } from 'src/send/TransferConfirmationCard' +import { TransferConfirmationCardProps } from 'src/send/TransferConfirmationCard' import { StandbyTransaction, TransactionTypes } from 'src/transactions/reducer' import { web3 } from 'src/web3/contracts' diff --git a/packages/mobile/src/transactions/send.ts b/packages/mobile/src/transactions/send.ts index f9626a8945e..17059b3d052 100644 --- a/packages/mobile/src/transactions/send.ts +++ b/packages/mobile/src/transactions/send.ts @@ -6,13 +6,17 @@ import { SendTransactionLogEvent, SendTransactionLogEventType, } from '@celo/walletkit' +import { call, select } from 'redux-saga/effects' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' import { DEFAULT_INFURA_URL } from 'src/config' import Logger from 'src/utils/Logger' -import { isZeroSyncMode, web3 } from 'src/web3/contracts' +import { web3 } from 'src/web3/contracts' +import { zeroSyncSelector } from 'src/web3/selectors' import { TransactionObject } from 'web3/eth/types' +const TAG = 'transactions/send' + // As per https://www.typescriptlang.org/docs/handbook/advanced-types.html#exhaustiveness-checking function assertNever(x: never): never { throw new Error('Unexpected object: ' + x) @@ -56,31 +60,30 @@ const getLogger = (tag: string, txId: string) => { // Sends a transaction and async returns promises for the txhash, confirmation, and receipt // Only use this method if you need more granular control of the different events -export const sendTransactionPromises = async ( +export function* sendTransactionPromises( tx: TransactionObject, account: string, tag: string, txId: string, staticGas?: number | undefined -) => { - Logger.debug( - 'transactions/send@sendTransactionPromises', - `Going to send a transaction with id ${txId}` - ) - const stableToken = await getStableTokenContract(web3) +) { + Logger.debug(`${TAG}@sendTransactionPromises`, `Going to send a transaction with id ${txId}`) + const stableToken = yield call(getStableTokenContract, web3) // This if-else case is temprary and will disappear once we move from `walletkit` to `contractkit`. - if (isZeroSyncMode()) { + const zeroSyncMode = yield select(zeroSyncSelector) + if (zeroSyncMode) { // In dev mode, verify that we are actually able to connect to the network. This // ensures that we get a more meaningful error if the infura server is down, which // can happen with networks without SLA guarantees like `integration`. if (__DEV__) { - await verifyUrlWorksOrThrow(DEFAULT_INFURA_URL) + yield call(verifyUrlWorksOrThrow, DEFAULT_INFURA_URL) } Logger.debug( - 'transactions/send@sendTransactionPromises', + `${TAG}@sendTransactionPromises`, `Sending transaction with id ${txId} using web3 signing` ) - return sendTransactionAsyncWithWeb3Signing( + const transactionPromises = yield call( + sendTransactionAsyncWithWeb3Signing, web3, tx, account, @@ -88,25 +91,36 @@ export const sendTransactionPromises = async ( getLogger(tag, txId), staticGas ) + return transactionPromises } else { Logger.debug( - 'transactions/send@sendTransactionPromises', + `${TAG}@sendTransactionPromises`, `Sending transaction with id ${txId} using geth signing` ) - return sendTransactionAsync(tx, account, stableToken, getLogger(tag, txId), staticGas) + const transactionPromises = yield call( + sendTransactionAsync, + tx, + account, + stableToken, + getLogger(tag, txId), + staticGas + ) + return transactionPromises } } // Send a transaction and await for its confirmation // Use this method for sending transactions and awaiting them to be confirmed -export const sendTransaction = async ( +export function* sendTransaction( tx: TransactionObject, account: string, tag: string, txId: string, staticGas?: number | undefined -) => { - return sendTransactionPromises(tx, account, tag, txId, staticGas).then(awaitConfirmation) +) { + const txPromises = yield call(sendTransactionPromises, tx, account, tag, txId, staticGas) + const confirmation = yield call(awaitConfirmation, txPromises) + return confirmation } async function verifyUrlWorksOrThrow(url: string) { diff --git a/packages/mobile/src/utils/AppRestart.ts b/packages/mobile/src/utils/AppRestart.ts index 4e7ee1af9f5..e0086fdb30e 100644 --- a/packages/mobile/src/utils/AppRestart.ts +++ b/packages/mobile/src/utils/AppRestart.ts @@ -1,9 +1,13 @@ +import { Platform } from 'react-native' +import RNExitApp from 'react-native-exit-app' import { RestartAndroid } from 'react-native-restart-android' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' import { deleteChainData } from 'src/geth/geth' import Logger from 'src/utils/Logger' +export const RESTART_APP_I18N_KEY = Platform.OS === 'android' ? 'restartApp' : 'quitApp' + // Call this method to restart the app. export function restartApp() { // Delete chain data since that's what is most likely corrupt. @@ -12,7 +16,12 @@ export function restartApp() { .finally(() => { CeloAnalytics.track(CustomEventNames.user_restart) Logger.info('utils/AppRestart/restartApp', 'Restarting app') - RestartAndroid.restart() + if (Platform.OS === 'android') { + RestartAndroid.restart() + } else { + // We can't restart on iOS, so just exit ;) + RNExitApp.exitApp() + } }) .catch((reason) => Logger.error('utils/AppRestart/restartApp', `Deleting chain data failed: ${reason}`) diff --git a/packages/mobile/src/utils/formatting.test.ts b/packages/mobile/src/utils/formatting.test.ts index 9c23df01c9e..37186a16e8e 100644 --- a/packages/mobile/src/utils/formatting.test.ts +++ b/packages/mobile/src/utils/formatting.test.ts @@ -9,8 +9,8 @@ import { describe('utils->formatting', () => { describe('getMoneyDisplayValue', () => { - const UNROUNDED_NUMBER = 5.239895 - const ROUNDED_NUMBER_2_DECIMALS = '5.23' + const UNROUNDED_NUMBER = 5.239835 + const ROUNDED_NUMBER_2_DECIMALS = '5.24' const ROUNDED_NUMBER_3_DECIMALS = '5.239' it('formats correctly for default case', () => { diff --git a/packages/mobile/src/utils/formatting.ts b/packages/mobile/src/utils/formatting.ts index 5db448e7002..1fd85c0955c 100644 --- a/packages/mobile/src/utils/formatting.ts +++ b/packages/mobile/src/utils/formatting.ts @@ -9,11 +9,12 @@ const numeral = require('numeral') export const getMoneyDisplayValue = ( value: BigNumber.Value, currency: CURRENCY_ENUM = CURRENCY_ENUM.DOLLAR, - includeSymbol: boolean = false + includeSymbol: boolean = false, + roundingTolerance: number = 1 ): string => { const decimals = CURRENCIES[currency].displayDecimals const symbol = CURRENCIES[currency].symbol - const formattedValue = numeral(roundDown(value, decimals).toNumber()).format( + const formattedValue = numeral(roundDown(value, decimals, roundingTolerance).toNumber()).format( '0,0.' + '0'.repeat(decimals) ) return includeSymbol ? symbol + formattedValue : formattedValue @@ -57,11 +58,31 @@ export const divideByWei = (value: BigNumber.Value, decimals?: number) => { return decimals ? bn.decimalPlaces(decimals) : bn } -export function roundDown(value: BigNumber.Value, decimals: number = 2): BigNumber { +export function roundDown( + value: BigNumber.Value, + decimals: number = 2, + roundingTolerance: number = 0 +): BigNumber { + if (roundingTolerance) { + value = new BigNumber(value).decimalPlaces( + decimals + roundingTolerance, + BigNumber.ROUND_HALF_DOWN + ) + } return new BigNumber(value).decimalPlaces(decimals, BigNumber.ROUND_DOWN) } -export function roundUp(value: BigNumber.Value, decimals: number = 2): BigNumber { +export function roundUp( + value: BigNumber.Value, + decimals: number = 2, + roundingTolerance: number = 0 +): BigNumber { + if (roundingTolerance) { + value = new BigNumber(value).decimalPlaces( + decimals + roundingTolerance, + BigNumber.ROUND_HALF_DOWN + ) + } return new BigNumber(value).decimalPlaces(decimals, BigNumber.ROUND_UP) } diff --git a/packages/mobile/src/utils/permissions.android.ts b/packages/mobile/src/utils/permissions.android.ts index c7dce05db5d..1f343546880 100644 --- a/packages/mobile/src/utils/permissions.android.ts +++ b/packages/mobile/src/utils/permissions.android.ts @@ -21,18 +21,10 @@ export async function requestContactsPermission() { ) } -export async function requestCameraPermission() { - return requestPermission(PermissionsAndroid.PERMISSIONS.CAMERA) -} - export async function checkContactsPermission() { return PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.READ_CONTACTS) } -export async function checkCameraPermission() { - return PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.CAMERA) -} - async function requestPermission(permission: Permission, title?: string, message?: string) { try { const granted = await PermissionsAndroid.request( diff --git a/packages/mobile/src/utils/permissions.ios.ts b/packages/mobile/src/utils/permissions.ios.ts index 4ee9cb4562d..fbf540ef8ba 100644 --- a/packages/mobile/src/utils/permissions.ios.ts +++ b/packages/mobile/src/utils/permissions.ios.ts @@ -17,11 +17,6 @@ export async function requestContactsPermission(): Promise { }) } -export async function requestCameraPermission() { - throw new Error('Unimplemented method') - return false -} - export async function checkContactsPermission(): Promise { return new Promise((resolve, reject) => { Contacts.checkPermission((err, permission) => { @@ -33,8 +28,3 @@ export async function checkContactsPermission(): Promise { }) }) } - -export async function checkCameraPermission() { - throw new Error('Unimplemented method') - return false -} diff --git a/packages/mobile/src/verify/__snapshots__/Verify.test.tsx.snap b/packages/mobile/src/verify/__snapshots__/Verify.test.tsx.snap index 9023c581f9b..a76d1cfdc14 100644 --- a/packages/mobile/src/verify/__snapshots__/Verify.test.tsx.snap +++ b/packages/mobile/src/verify/__snapshots__/Verify.test.tsx.snap @@ -2545,20 +2545,14 @@ exports[`renders the Input step correctly 1`] = ` renderSeparator={null} renderTextInput={[Function]} style={ - Array [ - Object { - "fontFamily": "Hind-Regular", - }, - Object { - "borderColor": "#D1D5D8", - "borderRadius": 3, - "padding": 8, - }, - Object { - "backgroundColor": "#FFFFFF", - "flex": 1, - }, - ] + Object { + "backgroundColor": "#FFFFFF", + "borderColor": "#D1D5D8", + "borderRadius": 3, + "flex": 1, + "fontFamily": "Hind-Regular", + "padding": 8, + } } testID="CountryNameField" underlineColorAndroid="transparent" @@ -2647,20 +2641,14 @@ exports[`renders the Input step correctly 1`] = ` placeholderTextColor="#D1D5D8" rejectResponderTermination={true} style={ - Array [ - Object { - "fontFamily": "Hind-Regular", - }, - Object { - "borderColor": "#D1D5D8", - "borderRadius": 3, - "padding": 8, - }, - Object { - "backgroundColor": "#FFFFFF", - "flex": 1, - }, - ] + Object { + "backgroundColor": "#FFFFFF", + "borderColor": "#D1D5D8", + "borderRadius": 3, + "flex": 1, + "fontFamily": "Hind-Regular", + "padding": 8, + } } testID="PhoneNumberField" underlineColorAndroid="transparent" diff --git a/packages/mobile/src/web3/actions.ts b/packages/mobile/src/web3/actions.ts index fad10e98b10..e523d101f8b 100644 --- a/packages/mobile/src/web3/actions.ts +++ b/packages/mobile/src/web3/actions.ts @@ -7,9 +7,11 @@ const TAG = 'web3/actions' export enum Actions { SET_ACCOUNT = 'WEB3/SET_ACCOUNT', + SET_ACCOUNT_IN_WEB3_KEYSTORE = 'WEB3/SET_ACCOUNT_IN_WEB3_KEYSTORE', SET_COMMENT_KEY = 'WEB3/SET_COMMENT_KEY', SET_PROGRESS = 'WEB3/SET_PROGRESS', SET_IS_READY = 'WEB3/SET_IS_READY', + SET_IS_ZERO_SYNC = 'WEB3/SET_IS_ZERO_SYNC', SET_BLOCK_NUMBER = 'WEB3/SET_BLOCK_NUMBER', REQUEST_SYNC_PROGRESS = 'WEB3/REQUEST_SYNC_PROGRESS', UPDATE_WEB3_SYNC_PROGRESS = 'WEB3/UPDATE_WEB3_SYNC_PROGRESS', @@ -20,6 +22,16 @@ export interface SetAccountAction { address: string } +export interface SetAccountInWeb3KeystoreAction { + type: Actions.SET_ACCOUNT_IN_WEB3_KEYSTORE + address: string +} + +export interface SetIsZeroSyncAction { + type: Actions.SET_IS_ZERO_SYNC + zeroSyncMode: boolean +} + export interface SetCommentKeyAction { type: Actions.SET_COMMENT_KEY commentKey: string @@ -41,6 +53,8 @@ export interface UpdateWeb3SyncProgressAction { export type ActionTypes = | SetAccountAction + | SetAccountInWeb3KeystoreAction + | SetIsZeroSyncAction | SetCommentKeyAction | SetLatestBlockNumberAction | UpdateWeb3SyncProgressAction @@ -53,6 +67,20 @@ export const setAccount = (address: string): SetAccountAction => { } } +export const setAccountInWeb3Keystore = (address: string): SetAccountInWeb3KeystoreAction => { + return { + type: Actions.SET_ACCOUNT_IN_WEB3_KEYSTORE, + address, + } +} + +export const setZeroSyncMode = (zeroSyncMode: boolean): SetIsZeroSyncAction => { + return { + type: Actions.SET_IS_ZERO_SYNC, + zeroSyncMode, + } +} + export const setPrivateCommentKey = (commentKey: string): SetCommentKeyAction => { return { type: Actions.SET_COMMENT_KEY, diff --git a/packages/mobile/src/web3/contracts.ts b/packages/mobile/src/web3/contracts.ts index 8f3583ba34f..275bf2e891a 100644 --- a/packages/mobile/src/web3/contracts.ts +++ b/packages/mobile/src/web3/contracts.ts @@ -1,9 +1,9 @@ +import { newKitFromWeb3 } from '@celo/contractkit' import { addLocalAccount as web3utilsAddLocalAccount } from '@celo/walletkit' import { Platform } from 'react-native' -import { DocumentDirectoryPath } from 'react-native-fs' import * as net from 'react-native-tcp' import { DEFAULT_INFURA_URL, DEFAULT_TESTNET } from 'src/config' -import { GethSyncMode } from 'src/geth/consts' +import { IPC_PATH } from 'src/geth/geth' import networkConfig, { Testnets } from 'src/geth/networkConfig' import Logger from 'src/utils/Logger' import Web3 from 'web3' @@ -13,18 +13,16 @@ import { Provider } from 'web3/providers' const tag = 'web3/contracts' export const web3: Web3 = getWeb3() +export const contractKit = newKitFromWeb3(web3) -export function isZeroSyncMode(): boolean { - return networkConfig.syncMode === GethSyncMode.ZeroSync +export function isInitiallyZeroSyncMode() { + return networkConfig.initiallyZeroSync } function getIpcProvider(testnet: Testnets) { Logger.debug(tag, 'creating IPCProvider...') - const ipcProvider = new Web3.providers.IpcProvider( - `${DocumentDirectoryPath}/.${testnet}/geth.ipc`, - net - ) + const ipcProvider = new Web3.providers.IpcProvider(IPC_PATH, net) Logger.debug(tag, 'created IPCProvider') // More details on the IPC objects can be seen via this @@ -58,16 +56,6 @@ function getIpcProvider(testnet: Testnets) { return ipcProvider } -// Use Http provider on iOS until we add support for local socket on iOS in react-native-tcp -function getWeb3HttpProviderForIos(): Provider { - Logger.debug(tag, 'creating HttpProvider for iOS...') - - const httpProvider = new Web3.providers.HttpProvider('http://localhost:8545') - Logger.debug(tag, 'created HttpProvider for iOS') - - return httpProvider -} - function getWebSocketProvider(url: string): Provider { Logger.debug(tag, 'creating HttpProvider...') const provider = new Web3.providers.HttpProvider(url) @@ -80,27 +68,34 @@ function getWebSocketProvider(url: string): Provider { } function getWeb3(): Web3 { - Logger.info(`Initializing web3, platform: ${Platform.OS}, geth free mode: ${isZeroSyncMode()}`) + Logger.info( + `Initializing web3, platform: ${Platform.OS}, geth free mode: ${isInitiallyZeroSyncMode()}` + ) - if (isZeroSyncMode() && Platform.OS === 'ios') { + if (isInitiallyZeroSyncMode() && Platform.OS === 'ios') { throw new Error('Zero sync mode is currently not supported on iOS') - } else if (isZeroSyncMode()) { + } else if (isInitiallyZeroSyncMode()) { // Geth free mode const url = DEFAULT_INFURA_URL Logger.debug('contracts@getWeb3', `Connecting to url ${url}`) return new Web3(getWebSocketProvider(url)) - } else if (Platform.OS === 'ios') { - // iOS + local geth - return new Web3(getWeb3HttpProviderForIos()) } else { return new Web3(getIpcProvider(DEFAULT_TESTNET)) } } -export function addLocalAccount(web3Instance: Web3, privateKey: string) { - if (!isZeroSyncMode()) { - throw new Error('addLocalAccount can only be called in Zero sync mode') +// Mutates web3 with new provider +export function switchWeb3ProviderForSyncMode(zeroSync: boolean) { + if (zeroSync) { + web3.setProvider(getWebSocketProvider(DEFAULT_INFURA_URL)) + Logger.info(`${tag}@switchWeb3ProviderForSyncMode`, `Set provider to ${DEFAULT_INFURA_URL}`) + } else { + web3.setProvider(getIpcProvider(DEFAULT_TESTNET)) + Logger.info(`${tag}@switchWeb3ProviderForSyncMode`, `Set provider to IPC provider`) } +} + +export function addLocalAccount(web3Instance: Web3, privateKey: string) { if (!web3Instance) { throw new Error(`web3 instance is ${web3Instance}`) } diff --git a/packages/mobile/src/web3/reducer.ts b/packages/mobile/src/web3/reducer.ts index 7c10b037aba..1ec445cfbfe 100644 --- a/packages/mobile/src/web3/reducer.ts +++ b/packages/mobile/src/web3/reducer.ts @@ -1,3 +1,4 @@ +import networkConfig from 'src/geth/networkConfig' import { getRehydratePayload, REHYDRATE, RehydrateAction } from 'src/redux/persist-helper' import { Actions, ActionTypes } from 'src/web3/actions' @@ -9,7 +10,9 @@ export interface State { } latestBlockNumber: number account: string | null + accountInWeb3Keystore: string | null commentKey: string | null + zeroSyncMode: boolean } const initialState: State = { @@ -20,7 +23,9 @@ const initialState: State = { }, latestBlockNumber: 0, account: null, + accountInWeb3Keystore: null, commentKey: null, + zeroSyncMode: networkConfig.initiallyZeroSync, } export const reducer = ( @@ -46,6 +51,16 @@ export const reducer = ( ...state, account: action.address, } + case Actions.SET_ACCOUNT_IN_WEB3_KEYSTORE: + return { + ...state, + accountInWeb3Keystore: action.address, + } + case Actions.SET_IS_ZERO_SYNC: + return { + ...state, + zeroSyncMode: action.zeroSyncMode, + } case Actions.SET_COMMENT_KEY: return { ...state, diff --git a/packages/mobile/src/web3/saga.ts b/packages/mobile/src/web3/saga.ts index cde696e55de..74d3b5d2b38 100644 --- a/packages/mobile/src/web3/saga.ts +++ b/packages/mobile/src/web3/saga.ts @@ -4,30 +4,39 @@ import * as Crypto from 'crypto' import { generateMnemonic, mnemonicToSeedHex } from 'react-native-bip39' import * as RNFS from 'react-native-fs' import { REHYDRATE } from 'redux-persist/es/constants' -import { call, delay, put, race, select, take } from 'redux-saga/effects' +import { call, delay, put, race, select, spawn, take, takeLatest } from 'redux-saga/effects' import { setAccountCreationTime } from 'src/account/actions' import { getPincode } from 'src/account/saga' +import { showError } from 'src/alert/actions' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' import { ErrorMessages } from 'src/app/ErrorMessages' import { currentLanguageSelector } from 'src/app/reducers' import { getWordlist } from 'src/backup/utils' import { UNLOCK_DURATION } from 'src/geth/consts' -import { deleteChainData } from 'src/geth/geth' +import { deleteChainData, stopGethIfInitialized } from 'src/geth/geth' +import { initGethSaga } from 'src/geth/saga' import { navigateToError } from 'src/navigator/NavigationService' import { waitWeb3LastBlock } from 'src/networkInfo/saga' +import { setCachedPincode } from 'src/pincode/PincodeCache' import { setKey } from 'src/utils/keyStore' import Logger from 'src/utils/Logger' import { Actions, getLatestBlock, setAccount, + setAccountInWeb3Keystore, + SetIsZeroSyncAction, setLatestBlockNumber, setPrivateCommentKey, updateWeb3SyncProgress, } from 'src/web3/actions' -import { addLocalAccount, isZeroSyncMode, web3 } from 'src/web3/contracts' -import { currentAccountSelector } from 'src/web3/selectors' +import { addLocalAccount, switchWeb3ProviderForSyncMode, web3 } from 'src/web3/contracts' +import { + currentAccountInWeb3KeystoreSelector, + currentAccountSelector, + zeroSyncSelector, +} from 'src/web3/selectors' import { Block } from 'web3/eth/types' const ETH_PRIVATE_KEY_LENGTH = 64 @@ -40,7 +49,8 @@ const BLOCK_CHAIN_CORRUPTION_ERROR = "Error: CONNECTION ERROR: Couldn't connect // checks if web3 claims it is currently syncing and attempts to wait for it to complete export function* checkWeb3SyncProgress() { - if (isZeroSyncMode()) { + const zeroSyncMode = yield select(zeroSyncSelector) + if (zeroSyncMode) { // In this mode, the check seems to fail with // web3/saga/checking web3 sync progress: Error: Invalid JSON RPC response: "": return true @@ -142,15 +152,18 @@ export function* assignAccountFromPrivateKey(key: string) { } let account: string - if (isZeroSyncMode()) { - const privateKey = String(key) + + // Save the account to a local file on the disk. + // This is done for all sync modes, to allow users to switch into zeroSync mode. + // Note that if geth is running it saves the key using web3.personal. + const privateKey = String(key) + account = getAccountAddressFromPrivateKey(privateKey) + yield savePrivateKeyToLocalDisk(account, privateKey, pincode) + + const zeroSyncMode = yield select(zeroSyncSelector) + if (zeroSyncMode) { Logger.debug(TAG + '@assignAccountFromPrivateKey', 'Init web3 with private key') addLocalAccount(web3, privateKey) - // Save the account to a local file on the disk. - // This is only required in Geth free mode because if geth is running - // it has its own mechanism to save the encrypted key in its keystore. - account = getAccountAddressFromPrivateKey(privateKey) - yield savePrivateKeyToLocalDisk(account, privateKey, pincode) } else { try { // @ts-ignore @@ -179,7 +192,7 @@ export function* assignAccountFromPrivateKey(key: string) { yield put(setAccountCreationTime()) yield call(assignDataKeyFromPrivateKey, key) - if (!isZeroSyncMode()) { + if (!zeroSyncMode) { web3.eth.defaultAccount = account } return account @@ -199,7 +212,7 @@ function* assignDataKeyFromPrivateKey(key: string) { } function getPrivateKeyFilePath(account: string): string { - return `${RNFS.DocumentDirectoryPath}/private_key_for_${account}.txt` + return `${RNFS.DocumentDirectoryPath}/private_key_for_${account.toLowerCase()}.txt` } function ensureAddressAndKeyMatch(address: string, privateKey: string) { @@ -306,7 +319,8 @@ export function* unlockAccount(account: string) { } const pincode = yield call(getPincode) - if (isZeroSyncMode()) { + const zeroSyncMode = yield select(zeroSyncSelector) + if (zeroSyncMode) { if (accountAlreadyAddedInZeroSyncMode) { Logger.info(TAG + 'unlockAccount', `Account ${account} already added to web3 for signing`) } else { @@ -322,6 +336,7 @@ export function* unlockAccount(account: string) { return true } } catch (error) { + setCachedPincode(null) Logger.error(TAG + '@unlockAccount', 'Web3 account unlock failed', error) return false } @@ -344,3 +359,116 @@ export function* getConnectedUnlockedAccount() { throw new Error(ErrorMessages.INCORRECT_PIN) } } + +// Stores account and private key in web3 keystore using web3.eth.personal +export function* addAccountToWeb3Keystore(key: string, currentAccount: string, pincode: string) { + let account: string + Logger.debug(TAG + '@addAccountToWeb3Keystore', `using key ${key} for account ${currentAccount}`) + const zeroSyncMode = yield select(zeroSyncSelector) + if (zeroSyncMode) { + // web3.eth.personal is not accessible in zeroSync mode + throw new Error('Cannot add account to Web3 keystore while in zeroSync mode') + } + try { + // @ts-ignore + account = yield call(web3.eth.personal.importRawKey, key, pincode) + Logger.debug( + TAG + '@addAccountToWeb3Keystore', + `Successfully imported raw key for account ${account}` + ) + yield put(setAccountInWeb3Keystore(account)) + } catch (e) { + Logger.debug(TAG + '@addAccountToWeb3Keystore', 'Failed to import raw key') + if (e.toString().includes('account already exists')) { + account = currentAccount + Logger.debug(TAG + '@addAccountToWeb3Keystore', 'Importing same account as current one') + } else { + Logger.error(TAG + '@addAccountToWeb3Keystore', 'Error importing raw key') + throw e + } + } + yield call(web3.eth.personal.unlockAccount, account, pincode, UNLOCK_DURATION) + web3.eth.defaultAccount = account + return account +} + +export function* ensureAccountInWeb3Keystore() { + const currentAccount: string = yield select(currentAccountSelector) + if (currentAccount) { + const accountInWeb3Keystore: string = yield select(currentAccountInWeb3KeystoreSelector) + if (!accountInWeb3Keystore) { + Logger.debug( + TAG + '@ensureAccountInWeb3Keystore', + 'Importing account from private key to web3 keystore' + ) + const pincode = yield call(getPincode) + const privateKey: string = yield call(readPrivateKeyFromLocalDisk, currentAccount, pincode) + const account: string = yield call( + addAccountToWeb3Keystore, + privateKey, + currentAccount, + pincode + ) + return account + } else if (accountInWeb3Keystore.toLowerCase() === currentAccount.toLowerCase()) { + return accountInWeb3Keystore + } else { + throw new Error( + `Account in web3 keystore (${accountInWeb3Keystore}) does not match current account (${currentAccount})` + ) + } + } else { + throw new Error('Account not yet initialized') + } +} + +export function* switchToGethFromZeroSync() { + try { + yield call(initGethSaga) + switchWeb3ProviderForSyncMode(false) + + // After switching off zeroSync mode, ensure key is stored in web3.personal + // Note that this must happen after the sync mode is switched + // as the web3.personal where the key is stored is not available in zeroSync mode + yield call(ensureAccountInWeb3Keystore) + + // Ensure web3 is fully synced using new provider + yield call(waitForWeb3Sync) + } catch (e) { + Logger.error(TAG + '@switchToGethFromZeroSync', 'Error switching to geth from zeroSync') + yield put(showError(ErrorMessages.FAILED_TO_SWITCH_SYNC_MODES)) + } +} + +export function* switchToZeroSyncFromGeth() { + Logger.debug(TAG + 'Switching to zeroSync from geth..') + try { + yield call(stopGethIfInitialized) + switchWeb3ProviderForSyncMode(true) + } catch (e) { + Logger.error(TAG + '@switchToGethFromZeroSync', 'Error switching to zeroSync from geth') + yield put(showError(ErrorMessages.FAILED_TO_SWITCH_SYNC_MODES)) + } +} + +export function* toggleZeroSyncMode(action: SetIsZeroSyncAction) { + Logger.debug(TAG + '@toggleZeroSyncMode', ` to: ${action.zeroSyncMode}`) + if (action.zeroSyncMode) { + yield call(switchToZeroSyncFromGeth) + } else { + yield call(switchToGethFromZeroSync) + } + // Unlock account to ensure private keys are accessible in new mode + const account = yield call(getConnectedUnlockedAccount) + Logger.debug( + TAG + '@toggleZeroSyncMode', + `Switched to ${action.zeroSyncMode} and able to unlock account ${account}` + ) +} +export function* watchZeroSyncMode() { + yield takeLatest(Actions.SET_IS_ZERO_SYNC, toggleZeroSyncMode) +} + +export function* web3Saga() { + yield spawn(watchZeroSyncMode) +} diff --git a/packages/mobile/src/web3/selectors.ts b/packages/mobile/src/web3/selectors.ts index bb30016381d..4e2b7b4524d 100644 --- a/packages/mobile/src/web3/selectors.ts +++ b/packages/mobile/src/web3/selectors.ts @@ -1,5 +1,8 @@ import { RootState } from 'src/redux/reducers' export const currentAccountSelector = (state: RootState) => state.web3.account +export const currentAccountInWeb3KeystoreSelector = (state: RootState) => + state.web3.accountInWeb3Keystore +export const zeroSyncSelector = (state: RootState) => state.web3.zeroSyncMode export const privateCommentKeySelector = (state: RootState) => state.web3.commentKey diff --git a/packages/mobile/test/schemas.ts b/packages/mobile/test/schemas.ts index f4f259f0fa7..e7736fe3e44 100644 --- a/packages/mobile/test/schemas.ts +++ b/packages/mobile/test/schemas.ts @@ -54,8 +54,10 @@ export const vNeg1Schema = { }, latestBlockNumber: 0, account: '0x0000000000000000000000000000000000007E57', + accountInWeb3Keystore: '0x0000000000000000000000000000000000007E57', commentKey: '0x0000000000000000000000000000000000008F68', gasPriceLastUpdated: 0, + zeroSyncMode: false, }, identity: { attestationCodes: [], @@ -80,6 +82,7 @@ export const vNeg1Schema = { paymentRequests: [], showFakeData: false, backupCompleted: false, + socialBackupCompleted: false, backupDelayedTime: 0, dismissedEarnRewards: false, dismissedInviteFriends: false, @@ -163,7 +166,6 @@ export const v3Schema = { }, imports: { isImportingWallet: false, - isWalletEmpty: false, }, } diff --git a/packages/mobile/test/utils.ts b/packages/mobile/test/utils.ts index f795ce0676a..3c63b1d19a8 100644 --- a/packages/mobile/test/utils.ts +++ b/packages/mobile/test/utils.ts @@ -1,6 +1,6 @@ /* Utilities to facilitate testing */ +import BigNumber from 'bignumber.js' import configureMockStore from 'redux-mock-store' -import thunk from 'redux-thunk' import { InitializationState } from 'src/geth/reducer' import i18n from 'src/i18n' import { RootState } from 'src/redux/reducers' @@ -16,6 +16,13 @@ import { export const sleep = (time: number) => new Promise((resolve) => setTimeout(() => resolve(true), time)) +// ContractKit test utils +export const mockContractKitBalance = jest.fn(() => new BigNumber(10)) +export const mockContractKitContract = { + balanceOf: mockContractKitBalance, + decimals: jest.fn(async () => '10'), +} + interface MockContract { methods: { [methodName: string]: MockMethod @@ -86,8 +93,7 @@ export function mockNavigationServiceFor(test: string, navigateMock = jest.fn()) return { navigate, navigateBack, navigateReset } } -const middlewares = [thunk] -const mockStore = configureMockStore(middlewares) +const mockStore = configureMockStore() /* Create a mock store with some reasonable default values */ type RecursivePartial = { [P in keyof T]?: RecursivePartial } diff --git a/packages/mobile/test/values.ts b/packages/mobile/test/values.ts index 3d2dabacfde..f68da5dcdbd 100644 --- a/packages/mobile/test/values.ts +++ b/packages/mobile/test/values.ts @@ -16,6 +16,14 @@ export const mockName = 'John Doe' export const mockAccount = '0x0000000000000000000000000000000000007E57' export const mockAccount2 = '0x1Ff482D42D8727258A1686102Fa4ba925C46Bc42' +export const mockMnemonic = + 'prosper winner find donate tape history measure umbrella agent patrol want rhythm old unable wash wrong need fluid hammer coach reveal plastic trust lake' + +export const mockMnemonicShard1 = + 'prosper winner find donate tape history measure umbrella agent patrol want rhythm celo' +export const mockMnemonicShard2 = + 'celo old unable wash wrong need fluid hammer coach reveal plastic trust lake' + export const mockPrivateDEK = Buffer.from( '41e8e8593108eeedcbded883b8af34d2f028710355c57f4c10a056b72486aa04', 'hex' diff --git a/packages/mobile/tslint.json b/packages/mobile/tslint.json index 5a7b61b5fc3..b5c27a7820d 100644 --- a/packages/mobile/tslint.json +++ b/packages/mobile/tslint.json @@ -6,6 +6,7 @@ "rules": { "no-relative-imports": true, "max-classes-per-file": [true, 2], - "no-global-arrow-functions": false + "no-global-arrow-functions": false, + "react-hooks-nesting": "error" } } diff --git a/packages/protocol/contracts/common/UsingPrecompiles.sol b/packages/protocol/contracts/common/UsingPrecompiles.sol new file mode 100644 index 00000000000..b8108432c88 --- /dev/null +++ b/packages/protocol/contracts/common/UsingPrecompiles.sol @@ -0,0 +1,125 @@ +pragma solidity ^0.5.3; + +contract UsingPrecompiles { + + /** + * @notice calculate a * b^x for fractions a, b to `decimals` precision + * @param aNumerator Numerator of first fraction + * @param aDenominator Denominator of first fraction + * @param bNumerator Numerator of exponentiated fraction + * @param bDenominator Denominator of exponentiated fraction + * @param exponent exponent to raise b to + * @param _decimals precision + * @return numerator/denominator of the computed quantity (not reduced). + */ + function fractionMulExp( + uint256 aNumerator, + uint256 aDenominator, + uint256 bNumerator, + uint256 bDenominator, + uint256 exponent, + uint256 _decimals + ) + public + view + returns (uint256, uint256) + { + require(aDenominator != 0 && bDenominator != 0); + uint256 returnNumerator; + uint256 returnDenominator; + // solhint-disable-next-line no-inline-assembly + assembly { + let newCallDataPosition := mload(0x40) + mstore(0x40, add(newCallDataPosition, calldatasize)) + mstore(newCallDataPosition, aNumerator) + mstore(add(newCallDataPosition, 32), aDenominator) + mstore(add(newCallDataPosition, 64), bNumerator) + mstore(add(newCallDataPosition, 96), bDenominator) + mstore(add(newCallDataPosition, 128), exponent) + mstore(add(newCallDataPosition, 160), _decimals) + let success := staticcall( + 1050, // estimated gas cost for this function + 0xfc, + newCallDataPosition, + 0xc4, // input size, 6 * 32 = 192 bytes + 0, + 0 + ) + + let returnDataSize := returndatasize + let returnDataPosition := mload(0x40) + mstore(0x40, add(returnDataPosition, returnDataSize)) + returndatacopy(returnDataPosition, 0, returnDataSize) + + switch success + case 0 { + revert(returnDataPosition, returnDataSize) + } + default { + returnNumerator := mload(returnDataPosition) + returnDenominator := mload(add(returnDataPosition, 32)) + } + } + return (returnNumerator, returnDenominator); + } + + /** + * @notice Returns the current epoch size in blocks. + * @return The current epoch size in blocks. + */ + function getEpochSize() public view returns (uint256) { + uint256 ret; + // solhint-disable-next-line no-inline-assembly + assembly { + let newCallDataPosition := mload(0x40) + let success := staticcall(1000, 0xf8, newCallDataPosition, 0, 0, 0) + + returndatacopy(add(newCallDataPosition, 32), 0, 32) + ret := mload(add(newCallDataPosition, 32)) + } + return ret; + } + + function getEpochNumber() public view returns (uint256) { + uint256 epochSize = getEpochSize(); + uint256 epochNumber = block.number / epochSize; + if (block.number % epochSize == 0) { + epochNumber = epochNumber - 1; + } + return epochNumber; + } + + /** + * @notice Gets a validator address from the current validator set. + * @param index Index of requested validator in the validator set as sorted by the election. + * @return Address of validator at the requested index. + */ + function validatorAddressFromCurrentSet(uint256 index) public view returns (address) { + address validatorAddress; + assembly { + let newCallDataPosition := mload(0x40) + mstore(newCallDataPosition, index) + let success := staticcall(5000, 0xfa, newCallDataPosition, 32, 0, 0) + returndatacopy(add(newCallDataPosition, 64), 0, 32) + validatorAddress := mload(add(newCallDataPosition, 64)) + } + + return validatorAddress; + } + + /** + * @notice Gets the size of the current elected validator set. + * @return Size of the current elected validator set. + */ + function numberValidatorsInCurrentSet() public view returns (uint256) { + uint256 numberValidators; + assembly { + let success := staticcall(5000, 0xf9, 0, 0, 0, 0) + let returnData := mload(0x40) + returndatacopy(returnData, 0, 32) + numberValidators := mload(returnData) + } + + return numberValidators; + } +} diff --git a/packages/protocol/contracts/common/UsingRegistry.sol b/packages/protocol/contracts/common/UsingRegistry.sol index 378e43ea5ca..f4b7350b6a8 100644 --- a/packages/protocol/contracts/common/UsingRegistry.sol +++ b/packages/protocol/contracts/common/UsingRegistry.sol @@ -2,8 +2,17 @@ pragma solidity ^0.5.3; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; +import "./interfaces/IERC20Token.sol"; import "./interfaces/IRegistry.sol"; +import "../governance/interfaces/IElection.sol"; +import "../governance/interfaces/ILockedGold.sol"; +import "../governance/interfaces/IValidators.sol"; + +import "../identity/interfaces/IRandom.sol"; + +import "../stability/interfaces/IStableToken.sol"; + // Ideally, UsingRegistry should inherit from Initializable and implement initialize() which calls // setRegistry(). TypeChain currently has problems resolving overloaded functions, so this is not // possible right now. @@ -15,20 +24,28 @@ contract UsingRegistry is Ownable { // solhint-disable state-visibility bytes32 constant ATTESTATIONS_REGISTRY_ID = keccak256(abi.encodePacked("Attestations")); - bytes32 constant LOCKED_GOLD_REGISTRY_ID = keccak256(abi.encodePacked("LockedGold")); + bytes32 constant ELECTION_REGISTRY_ID = keccak256(abi.encodePacked("Election")); + bytes32 constant EXCHANGE_REGISTRY_ID = keccak256(abi.encodePacked("Exchange")); bytes32 constant GAS_CURRENCY_WHITELIST_REGISTRY_ID = keccak256( abi.encodePacked("GasCurrencyWhitelist") ); bytes32 constant GOLD_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("GoldToken")); bytes32 constant GOVERNANCE_REGISTRY_ID = keccak256(abi.encodePacked("Governance")); + bytes32 constant LOCKED_GOLD_REGISTRY_ID = keccak256(abi.encodePacked("LockedGold")); bytes32 constant RESERVE_REGISTRY_ID = keccak256(abi.encodePacked("Reserve")); bytes32 constant RANDOM_REGISTRY_ID = keccak256(abi.encodePacked("Random")); bytes32 constant SORTED_ORACLES_REGISTRY_ID = keccak256(abi.encodePacked("SortedOracles")); + bytes32 constant STABLE_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("StableToken")); bytes32 constant VALIDATORS_REGISTRY_ID = keccak256(abi.encodePacked("Validators")); // solhint-enable state-visibility IRegistry public registry; + modifier onlyRegisteredContract(bytes32 identifierHash) { + require(registry.getAddressForOrDie(identifierHash) == msg.sender, "only registered contract"); + _; + } + /** * @notice Updates the address pointing to a Registry contract. * @param registryAddress The address of a registry contract for routing to other contracts. @@ -37,4 +54,28 @@ contract UsingRegistry is Ownable { registry = IRegistry(registryAddress); emit RegistrySet(registryAddress); } + + function getElection() internal view returns (IElection) { + return IElection(registry.getAddressForOrDie(ELECTION_REGISTRY_ID)); + } + + function getGoldToken() internal view returns (IERC20Token) { + return IERC20Token(registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID)); + } + + function getLockedGold() internal view returns (ILockedGold) { + return ILockedGold(registry.getAddressForOrDie(LOCKED_GOLD_REGISTRY_ID)); + } + + function getRandom() internal view returns (IRandom) { + return IRandom(registry.getAddressForOrDie(RANDOM_REGISTRY_ID)); + } + + function getStableToken() internal view returns (IStableToken) { + return IStableToken(registry.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID)); + } + + function getValidators() internal view returns (IValidators) { + return IValidators(registry.getAddressForOrDie(VALIDATORS_REGISTRY_ID)); + } } diff --git a/packages/protocol/contracts/common/linkedlists/AddressLinkedList.sol b/packages/protocol/contracts/common/linkedlists/AddressLinkedList.sol index aea287c5114..c0ee2fa07f7 100644 --- a/packages/protocol/contracts/common/linkedlists/AddressLinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/AddressLinkedList.sol @@ -82,6 +82,7 @@ library AddressLinkedList { * @notice Returns the N greatest elements of the list. * @param n The number of elements to return. * @return The keys of the greatest elements. + * @dev Reverts if n is greater than the number of elements in the list. */ function headN(LinkedList.List storage list, uint256 n) public view returns (address[] memory) { bytes32[] memory byteKeys = list.headN(n); diff --git a/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol b/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol index 3949b873124..0938c1b486e 100644 --- a/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/AddressSortedLinkedList.sol @@ -1,5 +1,6 @@ pragma solidity ^0.5.3; +import "openzeppelin-solidity/contracts/math/Math.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "./AddressLinkedList.sol"; import "./SortedLinkedList.sol"; @@ -105,4 +106,59 @@ library AddressSortedLinkedList { } return (keys, values); } + + /** + * @notice Returns the minimum of `max` and the number of elements in the list > threshold. + * @param threshold The number that the element must exceed to be included. + * @param max The maximum number returned by this function. + * @return The minimum of `max` and the number of elements in the list > threshold. + */ + function numElementsGreaterThan( + SortedLinkedList.List storage list, + uint256 threshold, + uint256 max + ) + public + view + returns (uint256) + { + uint256 revisedMax = Math.min(max, list.list.numElements); + bytes32 key = list.list.head; + for (uint256 i = 0; i < revisedMax; i++) { + if (list.getValue(key) < threshold) { + return i; + } + key = list.list.elements[key].previousKey; + } + return revisedMax; + } + + /** + * @notice Returns the N greatest elements of the list. + * @param n The number of elements to return. + * @return The keys of the greatest elements. + */ + function headN( + SortedLinkedList.List storage list, + uint256 n + ) + public + view + returns (address[] memory) + { + bytes32[] memory byteKeys = list.headN(n); + address[] memory keys = new address[](n); + for (uint256 i = 0; i < n; i++) { + keys[i] = toAddress(byteKeys[i]); + } + return keys; + } + + /** + * @notice Gets all element keys from the doubly linked list. + * @return All element keys from head to tail. + */ + function getKeys(SortedLinkedList.List storage list) public view returns (address[] memory) { + return headN(list, list.list.numElements); + } } diff --git a/packages/protocol/contracts/common/linkedlists/LinkedList.sol b/packages/protocol/contracts/common/linkedlists/LinkedList.sol index e1e514668dc..e3e80bdfc1f 100644 --- a/packages/protocol/contracts/common/linkedlists/LinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/LinkedList.sol @@ -153,6 +153,7 @@ library LinkedList { * @notice Returns the keys of the N elements at the head of the list. * @param n The number of elements to return. * @return The keys of the N elements at the head of the list. + * @dev Reverts if n is greater than the number of elements in the list. */ function headN(List storage list, uint256 n) public view returns (bytes32[] memory) { require(n <= list.numElements); diff --git a/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol b/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol index 80a057324dc..9489f00802c 100644 --- a/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol @@ -144,6 +144,17 @@ library SortedLinkedList { return list.list.getKeys(); } + /** + * @notice Returns first N greatest elements of the list. + * @param n The number of elements to return. + * @return The keys of the first n elements. + * @dev Reverts if n is greater than the number of elements in the list. + */ + function headN(List storage list, uint256 n) public view returns (bytes32[] memory) { + return list.list.headN(n); + } + + // TODO(asa): Gas optimizations by passing in elements to isValueBetween /** * @notice Returns the keys of the elements greaterKey than and less than the provided value. @@ -173,9 +184,13 @@ library SortedLinkedList { greaterKey == bytes32(0) && isValueBetween(list, value, list.list.head, greaterKey) ) { return (list.list.head, greaterKey); - } else if (isValueBetween(list, value, lesserKey, list.list.elements[lesserKey].nextKey)) { + } else if ( + lesserKey != bytes32(0) && + isValueBetween(list, value, lesserKey, list.list.elements[lesserKey].nextKey)) + { return (lesserKey, list.list.elements[lesserKey].nextKey); } else if ( + greaterKey != bytes32(0) && isValueBetween(list, value, list.list.elements[greaterKey].previousKey, greaterKey) ) { return (list.list.elements[greaterKey].previousKey, greaterKey); diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol new file mode 100644 index 00000000000..e810dd07329 --- /dev/null +++ b/packages/protocol/contracts/governance/Election.sol @@ -0,0 +1,920 @@ +pragma solidity ^0.5.3; + +import "openzeppelin-solidity/contracts/math/Math.sol"; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; +import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; + +import "./interfaces/IElection.sol"; +import "../common/Initializable.sol"; +import "../common/FixidityLib.sol"; +import "../common/linkedlists/AddressSortedLinkedList.sol"; +import "../common/UsingPrecompiles.sol"; +import "../common/UsingRegistry.sol"; + + +contract Election is + IElection, Ownable, ReentrancyGuard, Initializable, UsingRegistry, UsingPrecompiles { + + using AddressSortedLinkedList for SortedLinkedList.List; + using FixidityLib for FixidityLib.Fraction; + using SafeMath for uint256; + + struct PendingVote { + // The value of the vote, in gold. + uint256 value; + // The epoch at which the vote was cast. + uint256 epoch; + } + + struct GroupPendingVotes { + // The total number of pending votes that have been cast for this group. + uint256 total; + // Pending votes cast per voter. + mapping(address => PendingVote) byAccount; + } + + // Pending votes are those for which no following elections have been held. + // These votes have yet to contribute to the election of validators and thus do not accrue + // rewards. + struct PendingVotes { + // The total number of pending votes cast across all groups. + uint256 total; + mapping(address => GroupPendingVotes) forGroup; + } + + struct GroupActiveVotes { + // The total number of active votes that have been cast for this group. + uint256 total; + // The total number of active votes by a voter is equal to the number of active vote units for + // that voter times the total number of active votes divided by the total number of active + // vote units. + uint256 totalUnits; + mapping(address => uint256) unitsByAccount; + } + + // Active votes are those for which at least one following election has been held. + // These votes have contributed to the election of validators and thus accrue rewards. + struct ActiveVotes { + // The total number of active votes cast across all groups. + uint256 total; + mapping(address => GroupActiveVotes) forGroup; + } + + struct TotalVotes { + // A list of eligible ValidatorGroups sorted by total (pending+active) votes. + // Note that this list will omit ineligible ValidatorGroups, including those that may have > 0 + // total votes. + SortedLinkedList.List eligible; + } + + struct Votes { + PendingVotes pending; + ActiveVotes active; + TotalVotes total; + // Maps an account to the list of groups it's voting for. + mapping(address => address[]) groupsVotedFor; + } + + struct ElectableValidators { + uint256 min; + uint256 max; + } + + Votes private votes; + // Governs the minimum and maximum number of validators that can be elected. + ElectableValidators public electableValidators; + // Governs how many validator groups a single account can vote for. + uint256 public maxNumGroupsVotedFor; + // Groups must receive at least this fraction of the total votes in order to be considered in + // elections. + // TODO(asa): Implement this constraint. + FixidityLib.Fraction public electabilityThreshold; + + event ElectableValidatorsSet( + uint256 min, + uint256 max + ); + + event MaxNumGroupsVotedForSet( + uint256 maxNumGroupsVotedFor + ); + + event ElectabilityThresholdSet( + uint256 electabilityThreshold + ); + + event ValidatorGroupMarkedEligible( + address group + ); + + event ValidatorGroupMarkedIneligible( + address group + ); + + event ValidatorGroupVoteCast( + address indexed account, + address indexed group, + uint256 value + ); + + event ValidatorGroupVoteActivated( + address indexed account, + address indexed group, + uint256 value + ); + + event ValidatorGroupVoteRevoked( + address indexed account, + address indexed group, + uint256 value + ); + + /** + * @notice Initializes critical variables. + * @param registryAddress The address of the registry contract. + * @param minElectableValidators The minimum number of validators that can be elected. + * @param _maxNumGroupsVotedFor The maximum number of groups that an acconut can vote for at once. + * @param _electabilityThreshold The minimum ratio of votes a group needs before its members can + * be elected. + * @dev Should be called only once. + */ + function initialize( + address registryAddress, + uint256 minElectableValidators, + uint256 maxElectableValidators, + uint256 _maxNumGroupsVotedFor, + uint256 _electabilityThreshold + ) + external + initializer + { + _transferOwnership(msg.sender); + setRegistry(registryAddress); + _setElectableValidators(minElectableValidators, maxElectableValidators); + _setMaxNumGroupsVotedFor(_maxNumGroupsVotedFor); + _setElectabilityThreshold(_electabilityThreshold); + } + + /** + * @notice Updates the minimum and maximum number of validators that can be elected. + * @param min The minimum number of validators that can be elected. + * @param max The maximum number of validators that can be elected. + * @return True upon success. + */ + function setElectableValidators(uint256 min, uint256 max) external onlyOwner returns (bool) { + return _setElectableValidators(min, max); + } + + /** + * @notice Returns the minimum and maximum number of validators that can be elected. + * @return The minimum and maximum number of validators that can be elected. + */ + function getElectableValidators() external view returns (uint256, uint256) { + return (electableValidators.min, electableValidators.max); + } + + /** + * @notice Updates the minimum and maximum number of validators that can be elected. + * @param min The minimum number of validators that can be elected. + * @param max The maximum number of validators that can be elected. + * @return True upon success. + */ + function _setElectableValidators(uint256 min, uint256 max) private returns (bool) { + require(0 < min && min <= max); + require(min != electableValidators.min || max != electableValidators.max); + electableValidators = ElectableValidators(min, max); + emit ElectableValidatorsSet(min, max); + return true; + } + + /** + * @notice Updates the maximum number of groups an account can be voting for at once. + * @param _maxNumGroupsVotedFor The maximum number of groups an account can vote for. + * @return True upon success. + */ + function setMaxNumGroupsVotedFor( + uint256 _maxNumGroupsVotedFor + ) + external + onlyOwner + returns (bool) + { + return _setMaxNumGroupsVotedFor(_maxNumGroupsVotedFor); + } + + /** + * @notice Updates the maximum number of groups an account can be voting for at once. + * @param _maxNumGroupsVotedFor The maximum number of groups an account can vote for. + * @return True upon success. + */ + function _setMaxNumGroupsVotedFor(uint256 _maxNumGroupsVotedFor) private returns (bool) { + require(_maxNumGroupsVotedFor != maxNumGroupsVotedFor); + maxNumGroupsVotedFor = _maxNumGroupsVotedFor; + emit MaxNumGroupsVotedForSet(_maxNumGroupsVotedFor); + return true; + } + + /** + * @notice Sets the electability threshold. + * @param threshold Electability threshold as unwrapped Fraction. + * @return True upon success. + */ + function setElectabilityThreshold(uint256 threshold) public onlyOwner returns (bool) { + return _setElectabilityThreshold(threshold); + } + + /** + * @notice Sets the electability threshold. + * @param threshold Electability threshold as unwrapped Fraction. + * @return True upon success. + */ + function _setElectabilityThreshold(uint256 threshold) private returns (bool) { + electabilityThreshold = FixidityLib.wrap(threshold); + require( + electabilityThreshold.lt(FixidityLib.fixed1()), + "Electability threshold must be lower than 100%" + ); + emit ElectabilityThresholdSet(threshold); + return true; + } + + /** + * @notice Gets the election threshold. + * @return Threshold value as unwrapped fraction. + */ + function getElectabilityThreshold() external view returns (uint256) { + return electabilityThreshold.unwrap(); + } + + /** + * @notice Increments the number of total and pending votes for `group`. + * @param group The validator group to vote for. + * @param value The amount of gold to use to vote. + * @param lesser The group receiving fewer votes than `group`, or 0 if `group` has the + * fewest votes of any validator group. + * @param greater The group receiving more votes than `group`, or 0 if `group` has the + * most votes of any validator group. + * @return True upon success. + * @dev Fails if `group` is empty or not a validator group. + */ + function vote( + address group, + uint256 value, + address lesser, + address greater + ) + external + nonReentrant + returns (bool) + { + require(votes.total.eligible.contains(group)); + require(0 < value); + require(canReceiveVotes(group, value)); + address account = getLockedGold().getAccountFromActiveVoter(msg.sender); + + // Add group to the groups voted for by the account. + address[] storage groups = votes.groupsVotedFor[account]; + require(groups.length < maxNumGroupsVotedFor); + for (uint256 i = 0; i < groups.length; i = i.add(1)) { + require(groups[i] != group); + } + + groups.push(group); + incrementPendingVotes(group, account, value); + incrementTotalVotes(group, value, lesser, greater); + getLockedGold().decrementNonvotingAccountBalance(account, value); + emit ValidatorGroupVoteCast(account, group, value); + return true; + } + + /** + * @notice Converts `account`'s pending votes for `group` to active votes. + * @param group The validator group to vote for. + * @return True upon success. + * @dev Pending votes cannot be activated until an election has been held. + */ + function activate(address group) external nonReentrant returns (bool) { + address account = getLockedGold().getAccountFromActiveVoter(msg.sender); + PendingVote storage pendingVote = votes.pending.forGroup[group].byAccount[account]; + require(pendingVote.epoch < getEpochNumber()); + uint256 value = pendingVote.value; + require(value > 0); + decrementPendingVotes(group, account, value); + incrementActiveVotes(group, account, value); + emit ValidatorGroupVoteActivated(account, group, value); + } + + /** + * @notice Revokes `value` pending votes for `group` + * @param group The validator group to revoke votes from. + * @param value The number of votes to revoke. + * @param lesser The group receiving fewer votes than the group for which the vote was revoked, + * or 0 if that group has the fewest votes of any validator group. + * @param greater The group receiving more votes than the group for which the vote was revoked, + * or 0 if that group has the most votes of any validator group. + * @param index The index of the group in the account's voting list. + * @return True upon success. + * @dev Fails if the account has not voted on a validator group. + */ + function revokePending( + address group, + uint256 value, + address lesser, + address greater, + uint256 index + ) + external + nonReentrant + returns (bool) + { + require(group != address(0)); + address account = getLockedGold().getAccountFromActiveVoter(msg.sender); + require(0 < value && value <= getPendingVotesForGroupByAccount(group, account)); + decrementPendingVotes(group, account, value); + decrementTotalVotes(group, value, lesser, greater); + getLockedGold().incrementNonvotingAccountBalance(account, value); + if (getTotalVotesForGroupByAccount(group, account) == 0) { + deleteElement(votes.groupsVotedFor[account], group, index); + } + emit ValidatorGroupVoteRevoked(account, group, value); + return true; + } + + /** + * @notice Revokes `value` active votes for `group` + * @param group The validator group to revoke votes from. + * @param value The number of votes to revoke. + * @param lesser The group receiving fewer votes than the group for which the vote was revoked, + * or 0 if that group has the fewest votes of any validator group. + * @param greater The group receiving more votes than the group for which the vote was revoked, + * or 0 if that group has the most votes of any validator group. + * @param index The index of the group in the account's voting list. + * @return True upon success. + * @dev Fails if the account has not voted on a validator group. + */ + function revokeActive( + address group, + uint256 value, + address lesser, + address greater, + uint256 index + ) + external + nonReentrant + returns (bool) + { + // TODO(asa): Dedup with revokePending. + require(group != address(0)); + address account = getLockedGold().getAccountFromActiveVoter(msg.sender); + require(0 < value && value <= getActiveVotesForGroupByAccount(group, account)); + decrementActiveVotes(group, account, value); + decrementTotalVotes(group, value, lesser, greater); + getLockedGold().incrementNonvotingAccountBalance(account, value); + if (getTotalVotesForGroupByAccount(group, account) == 0) { + deleteElement(votes.groupsVotedFor[account], group, index); + } + emit ValidatorGroupVoteRevoked(account, group, value); + return true; + } + + /** + * @notice Returns the total number of votes cast by an account. + * @param account The address of the account. + * @return The total number of votes cast by an account. + */ + function getTotalVotesByAccount(address account) external view returns (uint256) { + uint256 total = 0; + address[] memory groups = votes.groupsVotedFor[account]; + for (uint256 i = 0; i < groups.length; i = i.add(1)) { + total = total.add(getTotalVotesForGroupByAccount(groups[i], account)); + } + return total; + } + + /** + * @notice Returns the pending votes for `group` made by `account`. + * @param group The address of the validator group. + * @param account The address of the voting account. + * @return The pending votes for `group` made by `account`. + */ + function getPendingVotesForGroupByAccount( + address group, + address account + ) + public + view + returns (uint256) + { + return votes.pending.forGroup[group].byAccount[account].value; + } + + /** + * @notice Returns the active votes for `group` made by `account`. + * @param group The address of the validator group. + * @param account The address of the voting account. + * @return The active votes for `group` made by `account`. + */ + function getActiveVotesForGroupByAccount( + address group, + address account + ) + public + view + returns (uint256) + { + GroupActiveVotes storage groupActiveVotes = votes.active.forGroup[group]; + uint256 numerator = groupActiveVotes.unitsByAccount[account].mul(groupActiveVotes.total); + if (numerator == 0) { + return 0; + } + uint256 denominator = groupActiveVotes.totalUnits; + return numerator.div(denominator); + } + + /** + * @notice Returns the total votes for `group` made by `account`. + * @param group The address of the validator group. + * @param account The address of the voting account. + * @return The total votes for `group` made by `account`. + */ + function getTotalVotesForGroupByAccount( + address group, + address account + ) + public + view + returns (uint256) + { + uint256 pending = getPendingVotesForGroupByAccount(group, account); + uint256 active = getActiveVotesForGroupByAccount(group, account); + return pending.add(active); + } + + /** + * @notice Returns the total votes made for `group`. + * @param group The address of the validator group. + * @return The total votes made for `group`. + */ + function getTotalVotesForGroup(address group) public view returns (uint256) { + return votes.pending.forGroup[group].total.add(votes.active.forGroup[group].total); + } + + /** + * @notice Returns whether or not a group is eligible to receive votes. + * @return Whether or not a group is eligible to receive votes. + * @dev Eligible groups that have received their maximum number of votes cannot receive more. + */ + function getGroupEligibility(address group) external view returns (bool) { + return votes.total.eligible.contains(group); + } + + /** + * @notice Returns the amount of rewards that voters for `group` are due at the end of an epoch. + * @param group The group to calculate epoch rewards for. + * @param totalEpochRewards The total amount of rewards going to all voters. + * @return The amount of rewards that voters for `group` are due at the end of an epoch. + * @dev Eligible groups that have received their maximum number of votes cannot receive more. + */ + function getGroupEpochRewards( + address group, + uint256 totalEpochRewards + ) + external + view + returns (uint256) + { + // The group must meet the balance requirements in order for their voters to receive epoch + // rewards. + bool meetsBalanceRequirements = ( + getLockedGold().getAccountTotalLockedGold(group) >= + getValidators().getAccountBalanceRequirement(group) + ); + + if (meetsBalanceRequirements && votes.active.total > 0) { + return totalEpochRewards.mul(votes.active.forGroup[group].total).div(votes.active.total); + } else { + return 0; + } + } + + /** + * @notice Distributes epoch rewards to voters for `group` in the form of active votes. + * @param group The group whose voters will receive rewards. + * @param value The amount of rewards to distribute to voters for the group. + * @param lesser The group receiving fewer votes than `group` after the rewards are added. + * @param greater The group receiving more votes than `group` after the rewards are added. + * @dev Can only be called directly by the protocol. + */ + function distributeEpochRewards( + address group, + uint256 value, + address lesser, + address greater + ) + external + { + require(msg.sender == address(0)); + _distributeEpochRewards(group, value, lesser, greater); + } + + /** + * @notice Distributes epoch rewards to voters for `group` in the form of active votes. + * @param group The group whose voters will receive rewards. + * @param value The amount of rewards to distribute to voters for the group. + * @param lesser The group receiving fewer votes than `group` after the rewards are added. + * @param greater The group receiving more votes than `group` after the rewards are added. + */ + function _distributeEpochRewards( + address group, + uint256 value, + address lesser, + address greater + ) + internal + { + if (votes.total.eligible.contains(group)) { + uint256 newVoteTotal = votes.total.eligible.getValue(group).add(value); + votes.total.eligible.update(group, newVoteTotal, lesser, greater); + } + + votes.active.forGroup[group].total = votes.active.forGroup[group].total.add(value); + votes.active.total = votes.active.total.add(value); + } + + /** + * @notice Increments the number of total votes for `group` by `value`. + * @param group The validator group whose vote total should be incremented. + * @param value The number of votes to increment. + * @param lesser The group receiving fewer votes than the group for which the vote was cast, + * or 0 if that group has the fewest votes of any validator group. + * @param greater The group receiving more votes than the group for which the vote was cast, + * or 0 if that group has the most votes of any validator group. + */ + function incrementTotalVotes( + address group, + uint256 value, + address lesser, + address greater + ) + private + { + uint256 newVoteTotal = votes.total.eligible.getValue(group).add(value); + votes.total.eligible.update(group, newVoteTotal, lesser, greater); + } + + /** + * @notice Decrements the number of total votes for `group` by `value`. + * @param group The validator group whose vote total should be decremented. + * @param value The number of votes to decrement. + * @param lesser The group receiving fewer votes than the group for which the vote was revoked, + * or 0 if that group has the fewest votes of any validator group. + * @param greater The group receiving more votes than the group for which the vote was revoked, + * or 0 if that group has the most votes of any validator group. + */ + function decrementTotalVotes( + address group, + uint256 value, + address lesser, + address greater + ) + private + { + if (votes.total.eligible.contains(group)) { + uint256 newVoteTotal = votes.total.eligible.getValue(group).sub(value); + votes.total.eligible.update(group, newVoteTotal, lesser, greater); + } + } + + /** + * @notice Marks a group ineligible for electing validators. + * @param group The address of the validator group. + * @dev Can only be called by the registered "Validators" contract. + */ + function markGroupIneligible( + address group + ) + external + onlyRegisteredContract(VALIDATORS_REGISTRY_ID) + { + votes.total.eligible.remove(group); + emit ValidatorGroupMarkedIneligible(group); + } + + /** + * @notice Marks a group eligible for electing validators. + * @param group The address of the validator group. + * @param lesser The address of the group that has received fewer votes than this group. + * @param greater The address of the group that has received more votes than this group. + */ + function markGroupEligible( + address group, + address lesser, + address greater + ) + external + onlyRegisteredContract(VALIDATORS_REGISTRY_ID) + { + uint256 value = getTotalVotesForGroup(group); + votes.total.eligible.insert(group, value, lesser, greater); + emit ValidatorGroupMarkedEligible(group); + } + + /** + * @notice Increments the number of pending votes for `group` made by `account`. + * @param group The address of the validator group. + * @param account The address of the voting account. + * @param value The number of votes. + */ + function incrementPendingVotes(address group, address account, uint256 value) private { + PendingVotes storage pending = votes.pending; + pending.total = pending.total.add(value); + + GroupPendingVotes storage groupPending = pending.forGroup[group]; + groupPending.total = groupPending.total.add(value); + + PendingVote storage pendingVote = groupPending.byAccount[account]; + pendingVote.value = pendingVote.value.add(value); + pendingVote.epoch = getEpochNumber(); + } + + /** + * @notice Decrements the number of pending votes for `group` made by `account`. + * @param group The address of the validator group. + * @param account The address of the voting account. + * @param value The number of votes. + */ + function decrementPendingVotes(address group, address account, uint256 value) private { + PendingVotes storage pending = votes.pending; + pending.total = pending.total.sub(value); + + GroupPendingVotes storage groupPending = pending.forGroup[group]; + groupPending.total = groupPending.total.sub(value); + + PendingVote storage pendingVote = groupPending.byAccount[account]; + pendingVote.value = pendingVote.value.sub(value); + if (pendingVote.value == 0) { + pendingVote.epoch = 0; + } + } + + /** + * @notice Increments the number of active votes for `group` made by `account`. + * @param group The address of the validator group. + * @param account The address of the voting account. + * @param value The number of votes. + */ + function incrementActiveVotes(address group, address account, uint256 value) private { + ActiveVotes storage active = votes.active; + active.total = active.total.add(value); + + uint256 unitsDelta = getActiveVotesUnitsDelta(group, value); + + GroupActiveVotes storage groupActive = active.forGroup[group]; + groupActive.total = groupActive.total.add(value); + + groupActive.totalUnits = groupActive.totalUnits.add(unitsDelta); + groupActive.unitsByAccount[account] = groupActive.unitsByAccount[account].add(unitsDelta); + } + + /** + * @notice Decrements the number of active votes for `group` made by `account`. + * @param group The address of the validator group. + * @param account The address of the voting account. + * @param value The number of votes. + */ + function decrementActiveVotes(address group, address account, uint256 value) private { + ActiveVotes storage active = votes.active; + active.total = active.total.sub(value); + + uint256 unitsDelta = getActiveVotesUnitsDelta(group, value); + + GroupActiveVotes storage groupActive = active.forGroup[group]; + groupActive.total = groupActive.total.sub(value); + + groupActive.totalUnits = groupActive.totalUnits.sub(unitsDelta); + groupActive.unitsByAccount[account] = groupActive.unitsByAccount[account].sub(unitsDelta); + } + + /** + * @notice Returns the delta in active vote denominator for `group`. + * @param group The address of the validator group. + * @param value The number of active votes being added. + * @return The delta in active vote denominator for `group`. + * @dev Preserves unitsDelta / totalUnits = value / total + */ + function getActiveVotesUnitsDelta(address group, uint256 value) private view returns (uint256) { + if (votes.active.forGroup[group].total == 0) { + return value; + } else { + return value.mul(votes.active.forGroup[group].totalUnits).div( + votes.active.forGroup[group].total + ); + } + } + + /** + * @notice Returns the groups that `account` has voted for. + * @param account The address of the account casting votes. + * @return The groups that `account` has voted for. + */ + function getGroupsVotedForByAccount(address account) external view returns (address[] memory) { + return votes.groupsVotedFor[account]; + } + + /** + * @notice Deletes an element from a list of addresses. + * @param list The list of addresses. + * @param element The address to delete. + * @param index The index of `element` in the list. + */ + function deleteElement(address[] storage list, address element, uint256 index) private { + // TODO(asa): Move this to a library to be shared. + require(index < list.length && list[index] == element); + uint256 lastIndex = list.length.sub(1); + list[index] = list[lastIndex]; + list.length = lastIndex; + } + + /** + * @notice Returns whether or not a group can receive the specified number of votes. + * @param group The address of the group. + * @param value The number of votes. + * @return Whether or not a group can receive the specified number of votes. + * @dev Votes are not allowed to be cast that would increase a group's proportion of locked gold + * voting for it to greater than + * (numGroupMembers + 1) / min(maxElectableValidators, numRegisteredValidators) + * @dev Note that groups may still receive additional votes via rewards even if this function + * returns false. + */ + function canReceiveVotes(address group, uint256 value) public view returns (bool) { + uint256 totalVotesForGroup = getTotalVotesForGroup(group).add(value); + uint256 left = totalVotesForGroup.mul( + Math.min( + electableValidators.max, + getValidators().getNumRegisteredValidators() + ) + ); + uint256 right = getValidators().getGroupNumMembers(group).add(1).mul( + getLockedGold().getTotalLockedGold() + ); + return left <= right; + } + + /** + * @notice Returns the number of votes that a group can receive. + * @param group The address of the group. + * @return The number of votes that a group can receive. + * @dev Votes are not allowed to be cast that would increase a group's proportion of locked gold + * voting for it to greater than + * (numGroupMembers + 1) / min(maxElectableValidators, numRegisteredValidators) + * @dev Note that a group's vote total may exceed this number through rewards or config changes. + */ + function getNumVotesReceivable(address group) external view returns (uint256) { + uint256 numerator = getValidators().getGroupNumMembers(group).add(1).mul( + getLockedGold().getTotalLockedGold() + ); + uint256 denominator = Math.min( + electableValidators.max, + getValidators().getNumRegisteredValidators() + ); + return numerator.div(denominator); + } + + /** + * @notice Returns the total votes received across all groups. + * @return The total votes received across all groups. + */ + function getTotalVotes() public view returns (uint256) { + return votes.active.total.add(votes.pending.total); + } + + /** + * @notice Returns the list of validator groups eligible to elect validators. + * @return The list of validator groups eligible to elect validators. + */ + function getEligibleValidatorGroups() external view returns (address[] memory) { + return votes.total.eligible.getKeys(); + } + + /** + * @notice Returns lists of all validator groups and the number of votes they've received. + * @return Lists of all validator groups and the number of votes they've received. + */ + function getTotalVotesForEligibleValidatorGroups() + external + view + returns (address[] memory groups, uint256[] memory values) + { + return votes.total.eligible.getElements(); + } + + /** + * @notice Returns a list of elected validators with seats allocated to groups via the D'Hondt + * method. + * @return The list of elected validators. + * @dev See https://en.wikipedia.org/wiki/D%27Hondt_method#Allocation for more information. + */ + function electValidators() external view returns (address[] memory) { + // Groups must have at least `electabilityThreshold` proportion of the total votes to be + // considered for the election. + uint256 requiredVotes = electabilityThreshold.multiply( + FixidityLib.newFixed(getTotalVotes()) + ).fromFixed(); + // Only consider groups with at least `requiredVotes` but do not consider more groups than the + // max number of electable validators. + uint256 numElectionGroups = votes.total.eligible.numElementsGreaterThan( + requiredVotes, + electableValidators.max + ); + address[] memory electionGroups = votes.total.eligible.headN(numElectionGroups); + uint256[] memory numMembers = getValidators().getGroupsNumMembers(electionGroups); + // Holds the number of members elected for each of the eligible validator groups. + uint256[] memory numMembersElected = new uint256[](electionGroups.length); + uint256 totalNumMembersElected = 0; + // Assign a number of seats to each validator group. + while (totalNumMembersElected < electableValidators.max) { + uint256 groupIndex = 0; + bool memberElected = false; + (groupIndex, memberElected) = dHondt(electionGroups, numMembers, numMembersElected); + + if (memberElected) { + numMembersElected[groupIndex] = numMembersElected[groupIndex].add(1); + totalNumMembersElected = totalNumMembersElected.add(1); + } else { + break; + } + } + require(totalNumMembersElected >= electableValidators.min); + // Grab the top validators from each group that won seats. + address[] memory electedValidators = new address[](totalNumMembersElected); + totalNumMembersElected = 0; + for (uint256 i = 0; i < electionGroups.length; i = i.add(1)) { + // We use the validating delegate if one is set. + address[] memory electedGroupValidators = getValidators().getTopGroupValidators( + electionGroups[i], + numMembersElected[i] + ); + for (uint256 j = 0; j < electedGroupValidators.length; j = j.add(1)) { + electedValidators[totalNumMembersElected] = electedGroupValidators[j]; + totalNumMembersElected = totalNumMembersElected.add(1); + } + } + // Shuffle the validator set using validator-supplied entropy + return shuffleArray(electedValidators); + } + + /** + * @notice Runs a round of the D'Hondt algorithm. + * @param electionGroups The addresses of the validator groups in the election. + * @param numMembers The number of members in each group. + * @param numMembersElected The number of members elected in each group up to this point. + * @dev See https://en.wikipedia.org/wiki/D%27Hondt_method#Allocation for more information. + * @return Whether or not a group elected a member, and the index of the group if so. + */ + function dHondt( + address[] memory electionGroups, + uint256[] memory numMembers, + uint256[] memory numMembersElected + ) + private + view + returns (uint256, bool) + { + bool memberElected = false; + uint256 groupIndex = 0; + FixidityLib.Fraction memory maxN = FixidityLib.wrap(0); + for (uint256 i = 0; i < electionGroups.length; i = i.add(1)) { + address group = electionGroups[i]; + // Only consider groups with members left to be elected. + if (numMembers[i] > numMembersElected[i]) { + FixidityLib.Fraction memory n = FixidityLib.newFixed( + votes.total.eligible.getValue(group) + ).divide( + FixidityLib.newFixed(numMembersElected[i].add(1)) + ); + if (n.gt(maxN)) { + maxN = n; + groupIndex = i; + memberElected = true; + } + } + } + return (groupIndex, memberElected); + } + + /** + * @notice Randomly permutes an array of addresses. + * @param array The array to permute. + * @return The permuted array. + */ + function shuffleArray(address[] memory array) private view returns (address[] memory) { + bytes32 r = getRandom().random(); + for (uint256 i = array.length - 1; i > 0; i = i.sub(1)) { + uint256 j = uint256(r) % (i + 1); + (array[i], array[j]) = (array[j], array[i]); + r = keccak256(abi.encodePacked(r)); + } + return array; + } +} diff --git a/packages/protocol/contracts/governance/Governance.sol b/packages/protocol/contracts/governance/Governance.sol index f8cf8a605f6..35b2895e139 100644 --- a/packages/protocol/contracts/governance/Governance.sol +++ b/packages/protocol/contracts/governance/Governance.sol @@ -7,18 +7,18 @@ import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "./interfaces/IGovernance.sol"; import "./Proposals.sol"; -import "./UsingLockedGold.sol"; +import "../common/ExtractFunctionSignature.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/FractionUtil.sol"; import "../common/linkedlists/IntegerSortedLinkedList.sol"; -import "../common/ExtractFunctionSignature.sol"; +import "../common/UsingRegistry.sol"; // TODO(asa): Hardcode minimum times for queueExpiry, etc. /** * @title A contract for making, passing, and executing on-chain governance proposals. */ -contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, ReentrancyGuard { +contract Governance is IGovernance, Ownable, Initializable, ReentrancyGuard, UsingRegistry { using Proposals for Proposals.Proposal; using FixidityLib for FixidityLib.Fraction; using FractionUtil for FractionUtil.Fraction; @@ -45,14 +45,20 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree Yes } + struct UpvoteRecord { + uint256 proposalId; + uint256 weight; + } + struct VoteRecord { Proposals.VoteValue value; uint256 proposalId; + uint256 weight; } struct Voter { // Key of the proposal voted for in the proposal queue - uint256 upvotedProposal; + UpvoteRecord upvote; uint256 mostRecentReferendumProposal; // Maps a `dequeued` index to a voter's vote record. mapping(uint256 => VoteRecord) referendumVotes; @@ -89,7 +95,7 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree mapping(address => uint256) public refundedDeposits; mapping(address => ContractConstitution) private constitution; mapping(uint256 => Proposals.Proposal) private proposals; - mapping(address => Voter) public voters; + mapping(address => Voter) private voters; SortedLinkedList.List private queue; uint256[] public dequeued; uint256[] public emptyIndices; @@ -479,8 +485,7 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree nonReentrant returns (bool) { - address account = getAccountFromVoter(msg.sender); - require(!isVotingFrozen(account)); + address account = getLockedGold().getAccountFromActiveVoter(msg.sender); // TODO(asa): When upvoting a proposal that will get dequeued, should we let the tx succeed // and return false? dequeueProposalsIfReady(); @@ -492,16 +497,26 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree return false; } Voter storage voter = voters[account]; + // If the previously upvoted proposal is still in the queue but has expired, expire the + // proposal from the queue. + if ( + queue.contains(voter.upvote.proposalId) && + now >= proposals[voter.upvote.proposalId].timestamp.add(queueExpiry) + ) { + queue.remove(voter.upvote.proposalId); + emit ProposalExpired(voter.upvote.proposalId); + } // We can upvote a proposal in the queue if we're not already upvoting a proposal in the queue. - uint256 weight = getAccountWeight(account); + uint256 weight = getLockedGold().getAccountTotalLockedGold(account); + require(weight > 0, "cannot upvote without locking gold"); + require(isQueued(proposalId), "cannot upvote a proposal not in the queue"); require( - isQueued(proposalId) && - (voter.upvotedProposal == 0 || !queue.contains(voter.upvotedProposal)) && - weight > 0 + voter.upvote.proposalId == 0 || !queue.contains(voter.upvote.proposalId), + "cannot upvote more than one queued proposal" ); - uint256 upvotes = queue.getValue(proposalId).add(uint256(weight)); + uint256 upvotes = queue.getValue(proposalId).add(weight); queue.update(proposalId, upvotes, lesser, greater); - voter.upvotedProposal = proposalId; + voter.upvote = UpvoteRecord(proposalId, weight); emit ProposalUpvoted(proposalId, account, weight); return true; } @@ -524,9 +539,9 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree returns (bool) { dequeueProposalsIfReady(); - address account = getAccountFromVoter(msg.sender); + address account = getLockedGold().getAccountFromActiveVoter(msg.sender); Voter storage voter = voters[account]; - uint256 proposalId = voter.upvotedProposal; + uint256 proposalId = voter.upvote.proposalId; Proposals.Proposal storage proposal = proposals[proposalId]; require(proposal.exists()); // If acting on an expired proposal, expire the proposal. @@ -537,13 +552,16 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree queue.remove(proposalId); emit ProposalExpired(proposalId); } else { - uint256 weight = getAccountWeight(account); - require(weight > 0); - queue.update(proposalId, queue.getValue(proposalId).sub(weight), lesser, greater); - emit ProposalUpvoteRevoked(proposalId, account, weight); + queue.update( + proposalId, + queue.getValue(proposalId).sub(voter.upvote.weight), + lesser, + greater + ); + emit ProposalUpvoteRevoked(proposalId, account, voter.upvote.weight); } } - voter.upvotedProposal = 0; + voter.upvote = UpvoteRecord(0, 0); return true; } @@ -567,7 +585,7 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree require(msg.sender == approver && !proposal.isApproved() && stage == Proposals.Stage.Approval); proposal.approved = true; // Ensures networkWeight is set by the end of the Referendum stage, even if 0 votes are cast. - proposal.networkWeight = getTotalWeight(); + proposal.networkWeight = getLockedGold().getTotalLockedGold(); emit ProposalApproved(proposalId); return true; } @@ -589,8 +607,7 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree nonReentrant returns (bool) { - address account = getAccountFromVoter(msg.sender); - require(!isVotingFrozen(account)); + address account = getLockedGold().getAccountFromActiveVoter(msg.sender); dequeueProposalsIfReady(); Proposals.Proposal storage proposal = proposals[proposalId]; require(isDequeuedProposal(proposal, proposalId, index)); @@ -600,7 +617,7 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree return false; } Voter storage voter = voters[account]; - uint256 weight = getAccountWeight(account); + uint256 weight = getLockedGold().getAccountTotalLockedGold(account); require( proposal.isApproved() && stage == Proposals.Stage.Referendum && @@ -609,13 +626,13 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree ); VoteRecord storage voteRecord = voter.referendumVotes[index]; proposal.updateVote( + voteRecord.weight, weight, (voteRecord.proposalId == proposalId) ? voteRecord.value : Proposals.VoteValue.None, value ); - proposal.networkWeight = getTotalWeight(); - voteRecord.proposalId = proposalId; - voteRecord.value = value; + proposal.networkWeight = getLockedGold().getTotalLockedGold(); + voter.referendumVotes[index] = VoteRecord(value, proposalId, weight); if (proposal.timestamp > voter.mostRecentReferendumProposal) { voter.mostRecentReferendumProposal = proposalId; } @@ -803,12 +820,13 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree } /** - * @notice Returns the ID of the proposal upvoted by `account`. + * @notice Returns the ID of the proposal upvoted by `account` and the weight of that upvote. * @param account The address of the account. - * @return The ID of the proposal upvoted by `account`. + * @return The ID of the proposal upvoted by `account` and the weight of that upvote. */ - function getUpvotedProposal(address account) external view returns (uint256) { - return voters[account].upvotedProposal; + function getUpvoteRecord(address account) external view returns (uint256, uint256) { + UpvoteRecord memory upvoteRecord = voters[account].upvote; + return (upvoteRecord.proposalId, upvoteRecord.weight); } /** @@ -820,21 +838,6 @@ contract Governance is IGovernance, Ownable, Initializable, UsingLockedGold, Ree return voters[account].mostRecentReferendumProposal; } - /** - * @notice Returns whether or not a particular account is voting on proposals. - * @param account The address of the account. - * @return Whether or not the account is voting on proposals. - */ - function isVoting(address account) external view returns (bool) { - Voter storage voter = voters[account]; - bool isVotingQueue = voter.upvotedProposal != 0 && isQueued(voter.upvotedProposal); - Proposals.Proposal storage proposal = proposals[voter.mostRecentReferendumProposal]; - bool isVotingReferendum = ( - proposal.getDequeuedStage(stageDurations) == Proposals.Stage.Referendum - ); - return isVotingQueue || isVotingReferendum; - } - /** * @notice Removes the proposals with the most upvotes from the queue, moving them to the * approval stage. diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index ade20cf7e36..ff713e45841 100644 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -5,222 +5,93 @@ import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "./interfaces/ILockedGold.sol"; -import "./interfaces/IGovernance.sol"; -import "./interfaces/IValidators.sol"; import "../common/Initializable.sol"; -import "../common/UsingRegistry.sol"; -import "../common/FixidityLib.sol"; -import "../common/interfaces/IERC20Token.sol"; import "../common/Signatures.sol"; -import "../common/FractionUtil.sol"; +import "../common/UsingRegistry.sol"; contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistry { - using FixidityLib for FixidityLib.Fraction; - using FractionUtil for FractionUtil.Fraction; using SafeMath for uint256; - // TODO(asa): Remove index for gas efficiency if two updates to the same slot costs extra gas. - struct Commitment { - uint128 value; - uint128 index; + struct Authorizations { + // The address that is authorized to vote on behalf of the account. + // The account can vote as well, whether or not an authorized voter has been specified. + address voting; + // The address that is authorized to validate on behalf of the account. + // The account can manage the validator, whether or not an authorized validator has been + // specified. However if an authorized validator has been specified, only that key may actually + // participate in consensus. + address validating; } - struct Commitments { - // Maps a notice period in seconds to a Locked Gold commitment. - mapping(uint256 => Commitment) locked; - // Maps an availability time in seconds since epoch to a notified commitment. - mapping(uint256 => Commitment) notified; - uint256[] noticePeriods; - uint256[] availabilityTimes; + struct PendingWithdrawal { + // The value of the pending withdrawal. + uint256 value; + // The timestamp at which the pending withdrawal becomes available. + uint256 timestamp; + } + + // NOTE: This contract does not store an account's locked gold that is being used in electing + // validators. + struct Balances { + // The amount of locked gold that this account has that is not currently participating in + // validator elections. + uint256 nonvoting; + // Gold that has been unlocked and will become available for withdrawal. + PendingWithdrawal[] pendingWithdrawals; } struct Account { bool exists; - // The weight of the account in validator elections, governance, and block rewards. - uint256 weight; - // Each account may delegate their right to receive rewards, vote, and register a Validator or - // Validator group to exactly one address each, respectively. This address must not hold an - // account and must not be delegated to by any other account or by the same account for any - // other purpose. - address[3] delegates; - // Frozen accounts may not vote, but may redact votes. - bool votingFrozen; - // The timestamp of the last time that rewards were redeemed. - uint96 rewardsLastRedeemed; - Commitments commitments; - } - - // TODO(asa): Add minNoticePeriod - uint256 public maxNoticePeriod; - uint256 public totalWeight; - mapping(address => Account) private accounts; - // Maps voting, rewards, and validating delegates to the account that delegated these rights. - mapping(address => address) public delegations; - // Maps a block number to the cumulative reward for an account with weight 1 since genesis. - mapping(uint256 => FixidityLib.Fraction) public cumulativeRewardWeights; - - event MaxNoticePeriodSet( - uint256 maxNoticePeriod - ); - - event RoleDelegated( - DelegateRole role, - address indexed account, - address delegate - ); - - event VotingFrozen( - address indexed account - ); - - event VotingUnfrozen( - address indexed account - ); - - event NewCommitment( - address indexed account, - uint256 value, - uint256 noticePeriod - ); - - event CommitmentNotified( - address indexed account, - uint256 value, - uint256 noticePeriod, - uint256 availabilityTime - ); - - event CommitmentExtended( - address indexed account, - uint256 value, - uint256 noticePeriod, - uint256 availabilityTime - ); - - event Withdrawal( - address indexed account, - uint256 value - ); - - event NoticePeriodIncreased( - address indexed account, - uint256 value, - uint256 noticePeriod, - uint256 increase - ); + // Each account may authorize additional keys to use for voting or valdiating. + // These keys may not be keys of other accounts, and may not be authorized by any other + // account for any purpose. + Authorizations authorizations; + Balances balances; + } - function initialize(address registryAddress, uint256 _maxNoticePeriod) external initializer { + mapping(address => Account) private accounts; + // Maps voting and validating keys to the account that provided the authorization. + // Authorized addresses may not be reused. + mapping(address => address) private authorizedBy; + uint256 public totalNonvoting; + uint256 public unlockingPeriod; + + event UnlockingPeriodSet(uint256 period); + event VoterAuthorized(address indexed account, address voter); + event ValidatorAuthorized(address indexed account, address validator); + event GoldLocked(address indexed account, uint256 value); + event GoldUnlocked(address indexed account, uint256 value, uint256 available); + event GoldWithdrawn(address indexed account, uint256 value); + + function initialize(address registryAddress, uint256 _unlockingPeriod) external initializer { _transferOwnership(msg.sender); setRegistry(registryAddress); - maxNoticePeriod = _maxNoticePeriod; - } - - /** - * @notice Sets the cumulative block reward for 1 unit of account weight. - * @param blockReward The total reward allocated to bonders for this block. - * @dev Called by the EVM at the end of the block. - */ - function setCumulativeRewardWeight(uint256 blockReward) external { - require(blockReward > 0, "placeholder to suppress warning"); - return; - // TODO(asa): Modify ganache to set cumulativeRewardWeights. - // TODO(asa): Make inheritable `onlyVm` modifier. - // Only callable by the EVM. - // require(msg.sender == address(0), "sender was not vm (reserved addr 0x0)"); - // FractionUtil.Fraction storage previousCumulativeRewardWeight = cumulativeRewardWeights[ - // block.number.sub(1) - // ]; - - // // This will be true the first time this is called by the EVM. - // if (!previousCumulativeRewardWeight.exists()) { - // previousCumulativeRewardWeight.denominator = 1; - // } - - // if (totalWeight > 0) { - // FractionUtil.Fraction memory currentRewardWeight = FractionUtil.Fraction( - // blockReward, - // totalWeight - // ).reduce(); - // cumulativeRewardWeights[block.number] = previousCumulativeRewardWeight.add( - // currentRewardWeight - // ); - // } else { - // cumulativeRewardWeights[block.number] = previousCumulativeRewardWeight; - // } - } - - /** - * @notice Sets the maximum notice period for an account. - * @param _maxNoticePeriod The new maximum notice period. - */ - function setMaxNoticePeriod(uint256 _maxNoticePeriod) external onlyOwner { - maxNoticePeriod = _maxNoticePeriod; - emit MaxNoticePeriodSet(maxNoticePeriod); + unlockingPeriod = _unlockingPeriod; } /** * @notice Creates an account. * @return True if account creation succeeded. */ - function createAccount() - external - returns (bool) - { - require(isNotAccount(msg.sender) && isNotDelegate(msg.sender)); + function createAccount() external returns (bool) { + require(isNotAccount(msg.sender) && isNotAuthorized(msg.sender)); Account storage account = accounts[msg.sender]; account.exists = true; - account.rewardsLastRedeemed = uint96(block.number); return true; } /** - * @notice Redeems rewards accrued since the last redemption for the specified account. - * @return The amount of accrued rewards. - * @dev Fails if `msg.sender` is not the owner or rewards recipient of the account. - */ - function redeemRewards() external nonReentrant returns (uint256) { - require(false, "Disabled"); - address account = getAccountFromDelegateAndRole(msg.sender, DelegateRole.Rewards); - return _redeemRewards(account); - } - - /** - * @notice Freezes the voting power of `msg.sender`'s account. - */ - function freezeVoting() external { - require(isAccount(msg.sender)); - Account storage account = accounts[msg.sender]; - require(account.votingFrozen == false); - account.votingFrozen = true; - emit VotingFrozen(msg.sender); - } - - /** - * @notice Unfreezes the voting power of `msg.sender`'s account. - */ - function unfreezeVoting() external { - require(isAccount(msg.sender)); - Account storage account = accounts[msg.sender]; - require(account.votingFrozen == true); - account.votingFrozen = false; - emit VotingUnfrozen(msg.sender); - } - - /** - * @notice Delegates the validating power of `msg.sender`'s account to another address. - * @param delegate The address to delegate to. + * @notice Authorizes an address to vote on behalf of the account. + * @param voter The address to authorize. * @param v The recovery id of the incoming ECDSA signature. * @param r Output value r of the ECDSA signature. * @param s Output value s of the ECDSA signature. - * @dev Fails if the address is already a delegate or has an account . - * @dev Fails if the current account is already participating in validation. - * @dev v, r, s constitute `delegate`'s signature on `msg.sender`. + * @dev v, r, s constitute `voter`'s signature on `msg.sender`. */ - function delegateRole( - DelegateRole role, - address delegate, + function authorizeVoter( + address voter, uint8 v, bytes32 r, bytes32 s @@ -228,554 +99,376 @@ contract LockedGold is ILockedGold, ReentrancyGuard, Initializable, UsingRegistr external nonReentrant { - // TODO: split and add error messages for better dev feedback - require(isAccount(msg.sender) && isNotAccount(delegate) && isNotDelegate(delegate)); - - address signer = Signatures.getSignerOfAddress(msg.sender, v, r, s); - require(signer == delegate); - - if (role == DelegateRole.Validating) { - require(isNotValidating(msg.sender)); - } else if (role == DelegateRole.Voting) { - require(!isVoting(msg.sender)); - } else if (role == DelegateRole.Rewards) { - _redeemRewards(msg.sender); - } - Account storage account = accounts[msg.sender]; - delegations[account.delegates[uint256(role)]] = address(0); - account.delegates[uint256(role)] = delegate; - delegations[delegate] = msg.sender; - emit RoleDelegated(role, msg.sender, delegate); + authorize(voter, v, r, s); + account.authorizations.voting = voter; + emit VoterAuthorized(msg.sender, voter); } /** - * @notice Adds a Locked Gold commitment to `msg.sender`'s account. - * @param noticePeriod The notice period for the commitment. - * @return The account's new weight. + * @notice Authorizes an address to validate on behalf of the account. + * @param validator The address to authorize. + * @param v The recovery id of the incoming ECDSA signature. + * @param r Output value r of the ECDSA signature. + * @param s Output value s of the ECDSA signature. + * @dev v, r, s constitute `validator`'s signature on `msg.sender`. */ - function newCommitment( - uint256 noticePeriod + function authorizeValidator( + address validator, + uint8 v, + bytes32 r, + bytes32 s ) external nonReentrant - payable - returns (uint256) { - require(isAccount(msg.sender) && !isVoting(msg.sender)); - - // _redeemRewards(msg.sender); - require(msg.value > 0 && noticePeriod <= maxNoticePeriod); Account storage account = accounts[msg.sender]; - Commitment storage locked = account.commitments.locked[noticePeriod]; - updateLockedCommitment(account, uint256(locked.value).add(msg.value), noticePeriod); - emit NewCommitment(msg.sender, msg.value, noticePeriod); - return account.weight; + authorize(validator, v, r, s); + account.authorizations.validating = validator; + emit ValidatorAuthorized(msg.sender, validator); } /** - * @notice Notifies a Locked Gold commitment, allowing funds to be withdrawn after the notice - * period. - * @param value The amount of the commitment to eventually withdraw. - * @param noticePeriod The notice period of the Locked Gold commitment. - * @return The account's new weight. + * @notice Sets the duration in seconds users must wait before withdrawing gold after unlocking. + * @param value The unlocking period in seconds. */ - function notifyCommitment( - uint256 value, - uint256 noticePeriod - ) - external - nonReentrant - returns (uint256) - { - require(isAccount(msg.sender) && isNotValidating(msg.sender) && !isVoting(msg.sender)); - // _redeemRewards(msg.sender); - Account storage account = accounts[msg.sender]; - Commitment storage locked = account.commitments.locked[noticePeriod]; - require(locked.value >= value && value > 0); - updateLockedCommitment(account, uint256(locked.value).sub(value), noticePeriod); - - // solhint-disable-next-line not-rely-on-time - uint256 availabilityTime = now.add(noticePeriod); - Commitment storage notified = account.commitments.notified[availabilityTime]; - updateNotifiedDeposit(account, uint256(notified.value).add(value), availabilityTime); - - emit CommitmentNotified(msg.sender, value, noticePeriod, availabilityTime); - return account.weight; + function setUnlockingPeriod(uint256 value) external onlyOwner { + require(value != unlockingPeriod); + unlockingPeriod = value; + emit UnlockingPeriodSet(value); } /** - * @notice Rebonds a notified commitment, with notice period >= the remaining time to - * availability. - * @param value The amount of the commitment to rebond. - * @param availabilityTime The availability time of the notified commitment. - * @return The account's new weight. + * @notice Locks gold to be used for voting. */ - function extendCommitment( - uint256 value, - uint256 availabilityTime - ) - external - nonReentrant - returns (uint256) - { - require(isAccount(msg.sender) && !isVoting(msg.sender)); - // solhint-disable-next-line not-rely-on-time - require(availabilityTime > now); - // _redeemRewards(msg.sender); - Account storage account = accounts[msg.sender]; - Commitment storage notified = account.commitments.notified[availabilityTime]; - require(notified.value >= value && value > 0); - updateNotifiedDeposit(account, uint256(notified.value).sub(value), availabilityTime); - // solhint-disable-next-line not-rely-on-time - uint256 noticePeriod = availabilityTime.sub(now); - Commitment storage locked = account.commitments.locked[noticePeriod]; - updateLockedCommitment(account, uint256(locked.value).add(value), noticePeriod); - emit CommitmentExtended(msg.sender, value, noticePeriod, availabilityTime); - return account.weight; + function lock() external payable nonReentrant { + require(isAccount(msg.sender), "not account"); + require(msg.value > 0, "no value"); + _incrementNonvotingAccountBalance(msg.sender, msg.value); + emit GoldLocked(msg.sender, msg.value); } /** - * @notice Withdraws a notified commitment after the duration of the notice period. - * @param availabilityTime The availability time of the notified commitment. - * @return The account's new weight. + * @notice Increments the non-voting balance for an account. + * @param account The account whose non-voting balance should be incremented. + * @param value The amount by which to increment. + * @dev Can only be called by the registered Election smart contract. */ - function withdrawCommitment( - uint256 availabilityTime + function incrementNonvotingAccountBalance( + address account, + uint256 value ) external - nonReentrant - returns (uint256) + onlyRegisteredContract(ELECTION_REGISTRY_ID) { - require(isAccount(msg.sender) && !isVoting(msg.sender)); - // _redeemRewards(msg.sender); - // solhint-disable-next-line not-rely-on-time - require(now >= availabilityTime); - _redeemRewards(msg.sender); - Account storage account = accounts[msg.sender]; - Commitment storage notified = account.commitments.notified[availabilityTime]; - uint256 value = notified.value; - require(value > 0); - updateNotifiedDeposit(account, 0, availabilityTime); - - IERC20Token goldToken = IERC20Token(registry.getAddressFor(GOLD_TOKEN_REGISTRY_ID)); - require(goldToken.transfer(msg.sender, value)); - emit Withdrawal(msg.sender, value); - return account.weight; + _incrementNonvotingAccountBalance(account, value); } /** - * @notice Increases the notice period for all or part of a Locked Gold commitment. - * @param value The amount of the Locked Gold commitment to increase the notice period for. - * @param noticePeriod The notice period of the Locked Gold commitment. - * @param increase The amount to increase the notice period by. - * @return The account's new weight. + * @notice Decrements the non-voting balance for an account. + * @param account The account whose non-voting balance should be decremented. + * @param value The amount by which to decrement. + * @dev Can only be called by the registered "Election" smart contract. */ - function increaseNoticePeriod( - uint256 value, - uint256 noticePeriod, - uint256 increase + function decrementNonvotingAccountBalance( + address account, + uint256 value ) external - nonReentrant - returns (uint256) + onlyRegisteredContract(ELECTION_REGISTRY_ID) { - require(isAccount(msg.sender) && !isVoting(msg.sender)); - // _redeemRewards(msg.sender); - require(value > 0 && increase > 0); - Account storage account = accounts[msg.sender]; - Commitment storage locked = account.commitments.locked[noticePeriod]; - require(locked.value >= value); - updateLockedCommitment(account, uint256(locked.value).sub(value), noticePeriod); - uint256 increasedNoticePeriod = noticePeriod.add(increase); - uint256 increasedValue = account.commitments.locked[increasedNoticePeriod].value; - updateLockedCommitment(account, increasedValue.add(value), increasedNoticePeriod); - emit NoticePeriodIncreased(msg.sender, value, noticePeriod, increase); - return account.weight; + _decrementNonvotingAccountBalance(account, value); } /** - * @notice Returns whether or not an account's voting power is frozen. - * @param account The address of the account. - * @return Whether or not the account's voting power is frozen. - * @dev Frozen accounts can retract existing votes but not make future votes. + * @notice Increments the non-voting balance for an account. + * @param account The account whose non-voting balance should be incremented. + * @param value The amount by which to increment. */ - function isVotingFrozen(address account) external view returns (bool) { - return accounts[account].votingFrozen; + function _incrementNonvotingAccountBalance(address account, uint256 value) private { + accounts[account].balances.nonvoting = accounts[account].balances.nonvoting.add(value); + totalNonvoting = totalNonvoting.add(value); } /** - * @notice Returns the timestamp of the last time the account redeemed block rewards. - * @param _account The address of the account. - * @return The timestamp of the last time `_account` redeemed block rewards. + * @notice Decrements the non-voting balance for an account. + * @param account The account whose non-voting balance should be decremented. + * @param value The amount by which to decrement. */ - function getRewardsLastRedeemed(address _account) external view returns (uint96) { - Account storage account = accounts[_account]; - return account.rewardsLastRedeemed; + function _decrementNonvotingAccountBalance(address account, uint256 value) private { + accounts[account].balances.nonvoting = accounts[account].balances.nonvoting.sub(value); + totalNonvoting = totalNonvoting.sub(value); } - function isValidating(address validator) external view returns (bool) { - IValidators validators = IValidators(registry.getAddressFor(VALIDATORS_REGISTRY_ID)); - return validators.isValidating(validator); + /** + * @notice Unlocks gold that becomes withdrawable after the unlocking period. + * @param value The amount of gold to unlock. + */ + function unlock(uint256 value) external nonReentrant { + require(isAccount(msg.sender)); + Account storage account = accounts[msg.sender]; + uint256 balanceRequirement = getValidators().getAccountBalanceRequirement(msg.sender); + require( + balanceRequirement == 0 || + balanceRequirement <= getAccountTotalLockedGold(msg.sender).sub(value) + ); + _decrementNonvotingAccountBalance(msg.sender, value); + uint256 available = now.add(unlockingPeriod); + account.balances.pendingWithdrawals.push(PendingWithdrawal(value, available)); + emit GoldUnlocked(msg.sender, value, available); } + // TODO(asa): Allow partial relock /** - * @notice Returns the notice periods of all Locked Gold for an account. - * @param _account The address of the account. - * @return The notice periods of all Locked Gold for `_account`. + * @notice Relocks gold that has been unlocked but not withdrawn. + * @param index The index of the pending withdrawal to relock. */ - function getNoticePeriods(address _account) external view returns (uint256[] memory) { - Account storage account = accounts[_account]; - return account.commitments.noticePeriods; + function relock(uint256 index) external nonReentrant { + require(isAccount(msg.sender)); + Account storage account = accounts[msg.sender]; + require(index < account.balances.pendingWithdrawals.length); + uint256 value = account.balances.pendingWithdrawals[index].value; + _incrementNonvotingAccountBalance(msg.sender, value); + deletePendingWithdrawal(account.balances.pendingWithdrawals, index); + emit GoldLocked(msg.sender, value); } /** - * @notice Returns the availability times of all notified commitments for an account. - * @param _account The address of the account. - * @return The availability times of all notified commitments for `_account`. + * @notice Withdraws gold that has been unlocked after the unlocking period has passed. + * @param index The index of the pending withdrawal to withdraw. */ - function getAvailabilityTimes(address _account) external view returns (uint256[] memory) { - Account storage account = accounts[_account]; - return account.commitments.availabilityTimes; + function withdraw(uint256 index) external nonReentrant { + require(isAccount(msg.sender)); + Account storage account = accounts[msg.sender]; + require(index < account.balances.pendingWithdrawals.length); + PendingWithdrawal storage pendingWithdrawal = account.balances.pendingWithdrawals[index]; + require(now >= pendingWithdrawal.timestamp); + uint256 value = pendingWithdrawal.value; + deletePendingWithdrawal(account.balances.pendingWithdrawals, index); + require(getGoldToken().transfer(msg.sender, value)); + emit GoldWithdrawn(msg.sender, value); } + // TODO(asa): Dedup /** - * @notice Returns the value and index of a specified Locked Gold commitment. - * @param _account The address of the account. - * @param noticePeriod The notice period of the Locked Gold commitment. - * @return The value and index of the specified Locked Gold commitment. + * @notice Returns the account associated with `accountOrVoter`. + * @param accountOrVoter The address of the account or active authorized voter. + * @dev Fails if the `accountOrVoter` is not an account or active authorized voter. + * @return The associated account. */ - function getLockedCommitment( - address _account, - uint256 noticePeriod - ) - external - view - returns (uint256, uint256) - { - Account storage account = accounts[_account]; - Commitment storage locked = account.commitments.locked[noticePeriod]; - return (locked.value, locked.index); + function getAccountFromActiveVoter(address accountOrVoter) external view returns (address) { + address account = authorizedBy[accountOrVoter]; + if (account != address(0)) { + require(accounts[account].authorizations.voting == accountOrVoter); + return account; + } else { + require(isAccount(accountOrVoter)); + return accountOrVoter; + } } /** - * @notice Returns the value and index of a specified notified commitment. - * @param _account The address of the account. - * @param availabilityTime The availability time of the notified commitment. - * @return The value and index of the specified notified commitment. + * @notice Returns the total amount of locked gold in the system. Note that this does not include + * gold that has been unlocked but not yet withdrawn. + * @return The total amount of locked gold in the system. */ - function getNotifiedCommitment( - address _account, - uint256 availabilityTime - ) - external - view - returns (uint256, uint256) - { - Account storage account = accounts[_account]; - Commitment storage notified = account.commitments.notified[availabilityTime]; - return (notified.value, notified.index); + function getTotalLockedGold() external view returns (uint256) { + return totalNonvoting.add(getElection().getTotalVotes()); } /** - * @notice Returns the account associated with the provided delegate and role. - * @param accountOrDelegate The address of the account or voting delegate. - * @param role The delegate role to query for. - * @dev Fails if the `accountOrDelegate` is a non-voting delegate. - * @return The associated account. + * @notice Returns the total amount of locked gold not being used to vote in elections. + * @return The total amount of locked gold not being used to vote in elections. */ - function getAccountFromDelegateAndRole( - address accountOrDelegate, - DelegateRole role - ) - public - view - returns (address) - { - address delegatingAccount = delegations[accountOrDelegate]; - if (delegatingAccount != address(0)) { - require(accounts[delegatingAccount].delegates[uint256(role)] == accountOrDelegate); - return delegatingAccount; - } else { - return accountOrDelegate; - } + function getNonvotingLockedGold() external view returns (uint256) { + return totalNonvoting; } /** - * @notice Returns the weight of a specified account. - * @param _account The address of the account. - * @return The weight of the specified account. + * @notice Returns the total amount of locked gold for an account. + * @param account The account. + * @return The total amount of locked gold for an account. */ - function getAccountWeight(address _account) external view returns (uint256) { - Account storage account = accounts[_account]; - return account.weight; + function getAccountTotalLockedGold(address account) public view returns (uint256) { + uint256 total = accounts[account].balances.nonvoting; + return total.add(getElection().getTotalVotesByAccount(account)); } /** - * @notice Returns whether or not a specified account is voting. - * @param account The address of the account. - * @return Whether or not the account is voting. + * @notice Returns the total amount of non-voting locked gold for an account. + * @param account The account. + * @return The total amount of non-voting locked gold for an account. */ - function isVoting(address account) public view returns (bool) { - address voter = getDelegateFromAccountAndRole(account, DelegateRole.Voting); - IGovernance governance = IGovernance(registry.getAddressFor(GOVERNANCE_REGISTRY_ID)); - IValidators validators = IValidators(registry.getAddressFor(VALIDATORS_REGISTRY_ID)); - return (governance.isVoting(voter) || validators.isVoting(voter)); + function getAccountNonvotingLockedGold(address account) external view returns (uint256) { + return accounts[account].balances.nonvoting; } /** - * @notice Returns the weight of a commitment for a given notice period. - * @param value The value of the commitment. - * @param noticePeriod The notice period of the commitment. - * @return The weight of the commitment. - * @dev A commitment's weight is (1 + sqrt(noticePeriodDays) / 30) * value. + * @notice Returns the account associated with `accountOrValidator`. + * @param accountOrValidator The address of the account or active authorized validator. + * @dev Fails if the `accountOrValidator` is not an account or active authorized validator. + * @return The associated account. */ - function getCommitmentWeight(uint256 value, uint256 noticePeriod) public pure returns (uint256) { - uint256 precision = 10000; - uint256 noticeDays = noticePeriod.div(1 days); - uint256 preciseMultiplier = sqrt(noticeDays).mul(precision).div(30).add(precision); - return preciseMultiplier.mul(value).div(precision); + function getAccountFromActiveValidator(address accountOrValidator) public view returns (address) { + address account = authorizedBy[accountOrValidator]; + if (account != address(0)) { + require(accounts[account].authorizations.validating == accountOrValidator); + return account; + } else { + require(isAccount(accountOrValidator)); + return accountOrValidator; + } } /** - * @notice Returns the delegate for a specified account and role. - * @param account The address of the account. - * @param role The role to query for. - * @return The rewards recipient for the account. + * @notice Returns the account associated with `accountOrVoter`. + * @param accountOrVoter The address of the account or previously authorized voter. + * @dev Fails if the `accountOrVoter` is not an account or previously authorized voter. + * @return The associated account. */ - function getDelegateFromAccountAndRole( - address account, - DelegateRole role - ) - public - view - returns (address) - { - address delegate = accounts[account].delegates[uint256(role)]; - if (delegate == address(0)) { + function getAccountFromVoter(address accountOrVoter) public view returns (address) { + address account = authorizedBy[accountOrVoter]; + if (account != address(0)) { return account; } else { - return delegate; + require(isAccount(accountOrVoter)); + return accountOrVoter; } } - // TODO(asa): Factor in governance, validator election participation. /** - * @notice Redeems rewards accrued since the last redemption for a specified account. - * @param _account The address of the account to redeem rewards for. - * @return The amount of accrued rewards. + * @notice Returns the account associated with `accountOrValidator`. + * @param accountOrValidator The address of the account or previously authorized validator. + * @dev Fails if the `accountOrValidator` is not an account or previously authorized validator. + * @return The associated account. */ - function _redeemRewards(address _account) private returns (uint256) { - Account storage account = accounts[_account]; - uint256 rewardBlockNumber = block.number.sub(1); - FixidityLib.Fraction memory previousCumulativeRewardWeight = cumulativeRewardWeights[ - account.rewardsLastRedeemed - ]; - FixidityLib.Fraction memory cumulativeRewardWeight = cumulativeRewardWeights[ - rewardBlockNumber - ]; - // We should never get here except in testing, where cumulativeRewardWeight will not be set. - if (previousCumulativeRewardWeight.unwrap() == 0 || cumulativeRewardWeight.unwrap() == 0) { - return 0; + function getAccountFromValidator(address accountOrValidator) public view returns (address) { + address account = authorizedBy[accountOrValidator]; + if (account != address(0)) { + return account; + } else { + require(isAccount(accountOrValidator)); + return accountOrValidator; } + } - FixidityLib.Fraction memory rewardWeight = cumulativeRewardWeight.subtract( - previousCumulativeRewardWeight - ); - require(rewardWeight.unwrap() != 0, "Rewards weight does not exist"); - uint256 value = rewardWeight.multiply(FixidityLib.wrap(account.weight)).fromFixed(); - account.rewardsLastRedeemed = uint96(rewardBlockNumber); - if (value > 0) { - address recipient = getDelegateFromAccountAndRole(_account, DelegateRole.Rewards); - IERC20Token goldToken = IERC20Token(registry.getAddressFor(GOLD_TOKEN_REGISTRY_ID)); - require(goldToken.transfer(recipient, value)); - emit Withdrawal(recipient, value); - } - return value; + /** + * @notice Returns the voter for the specified account. + * @param account The address of the account. + * @return The address with which the account can vote. + */ + function getVoterFromAccount(address account) public view returns (address) { + require(isAccount(account)); + address voter = accounts[account].authorizations.voting; + return voter == address(0) ? account : voter; } /** - * @notice Updates the Locked Gold commitment for a given notice period to a new value. - * @param account The account to update the Locked Gold commitment for. - * @param value The new value of the Locked Gold commitment. - * @param noticePeriod The notice period of the Locked Gold commitment. + * @notice Returns the validator for the specified account. + * @param account The address of the account. + * @return The address with which the account can register a validator or group. */ - function updateLockedCommitment( - Account storage account, - uint256 value, - uint256 noticePeriod - ) - private - { - Commitment storage locked = account.commitments.locked[noticePeriod]; - require(value != locked.value); - uint256 weight; - if (locked.value == 0) { - locked.index = uint128(account.commitments.noticePeriods.length); - locked.value = uint128(value); - account.commitments.noticePeriods.push(noticePeriod); - weight = getCommitmentWeight(value, noticePeriod); - account.weight = account.weight.add(weight); - totalWeight = totalWeight.add(weight); - } else if (value == 0) { - weight = getCommitmentWeight(locked.value, noticePeriod); - account.weight = account.weight.sub(weight); - totalWeight = totalWeight.sub(weight); - deleteCommitment(locked, account.commitments, CommitmentType.Locked); - } else { - uint256 originalWeight = getCommitmentWeight(locked.value, noticePeriod); - weight = getCommitmentWeight(value, noticePeriod); - - uint256 difference; - if (weight >= originalWeight) { - difference = weight.sub(originalWeight); - account.weight = account.weight.add(difference); - totalWeight = totalWeight.add(difference); - } else { - difference = originalWeight.sub(weight); - account.weight = account.weight.sub(difference); - totalWeight = totalWeight.sub(difference); - } - - locked.value = uint128(value); - } + function getValidatorFromAccount(address account) public view returns (address) { + require(isAccount(account)); + address validator = accounts[account].authorizations.validating; + return validator == address(0) ? account : validator; } /** - * @notice Updates the notified commitment for a given availability time to a new value. - * @param account The account to update the notified commitment for. - * @param value The new value of the notified commitment. - * @param availabilityTime The availability time of the notified commitment. + * @notice Returns the pending withdrawals from unlocked gold for an account. + * @param account The address of the account. + * @return The value and timestamp for each pending withdrawal. */ - function updateNotifiedDeposit( - Account storage account, - uint256 value, - uint256 availabilityTime + function getPendingWithdrawals( + address account ) - private + external + view + returns (uint256[] memory, uint256[] memory) { - Commitment storage notified = account.commitments.notified[availabilityTime]; - require(value != notified.value); - if (notified.value == 0) { - notified.index = uint128(account.commitments.availabilityTimes.length); - notified.value = uint128(value); - account.commitments.availabilityTimes.push(availabilityTime); - account.weight = account.weight.add(notified.value); - totalWeight = totalWeight.add(notified.value); - } else if (value == 0) { - account.weight = account.weight.sub(notified.value); - totalWeight = totalWeight.sub(notified.value); - deleteCommitment(notified, account.commitments, CommitmentType.Notified); - } else { - uint256 difference; - if (value >= notified.value) { - difference = value.sub(notified.value); - account.weight = account.weight.add(difference); - totalWeight = totalWeight.add(difference); - } else { - difference = uint256(notified.value).sub(value); - account.weight = account.weight.sub(difference); - totalWeight = totalWeight.sub(difference); - } - - notified.value = uint128(value); + require(isAccount(account)); + uint256 length = accounts[account].balances.pendingWithdrawals.length; + uint256[] memory values = new uint256[](length); + uint256[] memory timestamps = new uint256[](length); + for (uint256 i = 0; i < length; i++) { + PendingWithdrawal memory pendingWithdrawal = ( + accounts[account].balances.pendingWithdrawals[i] + ); + values[i] = pendingWithdrawal.value; + timestamps[i] = pendingWithdrawal.timestamp; } + return (values, timestamps); } /** - * @notice Deletes a commitment from an account. - * @param _commitment The commitment to delete. - * @param commitments The struct containing the account's commitments. - * @param commitmentType Whether the deleted commitment is locked or notified. + * @notice Authorizes voting or validating power of `msg.sender`'s account to another address. + * @param authorized The address to authorize. + * @param v The recovery id of the incoming ECDSA signature. + * @param r Output value r of the ECDSA signature. + * @param s Output value s of the ECDSA signature. + * @dev Fails if the address is already authorized or is an account. + * @dev v, r, s constitute `current`'s signature on `msg.sender`. */ - function deleteCommitment( - Commitment storage _commitment, - Commitments storage commitments, - CommitmentType commitmentType + function authorize( + address authorized, + uint8 v, + bytes32 r, + bytes32 s ) private { - uint256 lastIndex; - if (commitmentType == CommitmentType.Locked) { - lastIndex = commitments.noticePeriods.length.sub(1); - commitments.locked[commitments.noticePeriods[lastIndex]].index = _commitment.index; - deleteElement(commitments.noticePeriods, _commitment.index, lastIndex); - } else { - lastIndex = commitments.availabilityTimes.length.sub(1); - commitments.notified[commitments.availabilityTimes[lastIndex]].index = _commitment.index; - deleteElement(commitments.availabilityTimes, _commitment.index, lastIndex); - } + require(isAccount(msg.sender) && isNotAccount(authorized) && isNotAuthorized(authorized)); - // Delete commitment info. - _commitment.index = 0; - _commitment.value = 0; - } + address signer = Signatures.getSignerOfAddress(msg.sender, v, r, s); + require(signer == authorized); - /** - * @notice Deletes an element from a list of uint256s. - * @param list The list of uint256s. - * @param index The index of the element to delete. - * @param lastIndex The index of the last element in the list. - */ - function deleteElement(uint256[] storage list, uint256 index, uint256 lastIndex) private { - list[index] = list[lastIndex]; - list[lastIndex] = 0; - list.length = lastIndex; + authorizedBy[authorized] = msg.sender; } /** * @notice Check if an account already exists. * @param account The address of the account * @return Returns `true` if account exists. Returns `false` otherwise. - * In particular it will return `false` if a delegate with given address exists. */ function isAccount(address account) public view returns (bool) { return (accounts[account].exists); } /** - * @notice Check if a delegate already exists. - * @param account The address of the delegate - * @return Returns `true` if delegate exists. Returns `false` otherwise. + * @notice Check if an account already exists. + * @param account The address of the account + * @return Returns `false` if account exists. Returns `true` otherwise. */ - function isDelegate(address account) external view returns (bool) { - return (delegations[account] != address(0)); - } - - function isNotAccount(address account) internal view returns (bool) { return (!accounts[account].exists); } - // Reverts if rewards, voting, or validating rights have been delegated to `account`. - function isNotDelegate(address account) internal view returns (bool) { - return (delegations[account] == address(0)); + /** + * @notice Check if an address has been authorized by an account for voting or validating. + * @param account The possibly authorized address. + * @return Returns `true` if authorized. Returns `false` otherwise. + */ + function isAuthorized(address account) external view returns (bool) { + return (authorizedBy[account] != address(0)); } - // TODO(asa): Allow users to notify if they would continue to meet the registration - // requirements. - function isNotValidating(address account) internal view returns (bool) { - address validator = getDelegateFromAccountAndRole(account, DelegateRole.Validating); - IValidators validators = IValidators(registry.getAddressFor(VALIDATORS_REGISTRY_ID)); - return (!validators.isValidating(validator)); + /** + * @notice Check if an address has been authorized by an account for voting or validating. + * @param account The possibly authorized address. + * @return Returns `false` if authorized. Returns `true` otherwise. + */ + function isNotAuthorized(address account) internal view returns (bool) { + return (authorizedBy[account] == address(0)); } - // TODO: consider using Fixidity's roots /** - * @notice Approxmiates the square root of x using the Bablyonian method. - * @param x The number to take the square root of. - * @return An approximation of the square root of x. - * @dev The error can be large for smaller numbers, so we multiply by the square of `precision`. + * @notice Deletes a pending withdrawal. + * @param list The list of pending withdrawals from which to delete. + * @param index The index of the pending withdrawal to delete. */ - function sqrt(uint256 x) private pure returns (FractionUtil.Fraction memory) { - uint256 precision = 100; - uint256 px = x.mul(precision.mul(precision)); - uint256 z = px.add(1).div(2); - uint256 y = px; - while (z < y) { - y = z; - z = px.div(z).add(z).div(2); - } - return FractionUtil.Fraction(y, precision); + function deletePendingWithdrawal(PendingWithdrawal[] storage list, uint256 index) private { + uint256 lastIndex = list.length.sub(1); + list[index] = list[lastIndex]; + list.length = lastIndex; } } diff --git a/packages/protocol/contracts/governance/Proposals.sol b/packages/protocol/contracts/governance/Proposals.sol index 71e1a44e738..c7ec5fcce6b 100644 --- a/packages/protocol/contracts/governance/Proposals.sol +++ b/packages/protocol/contracts/governance/Proposals.sol @@ -101,13 +101,15 @@ library Proposals { /** * @notice Adds or changes a vote on a proposal. * @param proposal The proposal struct. - * @param weight The weight of the vote. + * @param previousWeight The previous weight of the vote. + * @param currentWeight The current weight of the vote. * @param previousVote The vote to be removed, or None for a new vote. * @param currentVote The vote to be set. */ function updateVote( Proposal storage proposal, - uint256 weight, + uint256 previousWeight, + uint256 currentWeight, VoteValue previousVote, VoteValue currentVote ) @@ -115,20 +117,20 @@ library Proposals { { // Subtract previous vote. if (previousVote == VoteValue.Abstain) { - proposal.votes.abstain = proposal.votes.abstain.sub(weight); + proposal.votes.abstain = proposal.votes.abstain.sub(previousWeight); } else if (previousVote == VoteValue.Yes) { - proposal.votes.yes = proposal.votes.yes.sub(weight); + proposal.votes.yes = proposal.votes.yes.sub(previousWeight); } else if (previousVote == VoteValue.No) { - proposal.votes.no = proposal.votes.no.sub(weight); + proposal.votes.no = proposal.votes.no.sub(previousWeight); } // Add new vote. if (currentVote == VoteValue.Abstain) { - proposal.votes.abstain = proposal.votes.abstain.add(weight); + proposal.votes.abstain = proposal.votes.abstain.add(currentWeight); } else if (currentVote == VoteValue.Yes) { - proposal.votes.yes = proposal.votes.yes.add(weight); + proposal.votes.yes = proposal.votes.yes.add(currentWeight); } else if (currentVote == VoteValue.No) { - proposal.votes.no = proposal.votes.no.add(weight); + proposal.votes.no = proposal.votes.no.add(currentWeight); } } diff --git a/packages/protocol/contracts/governance/UsingLockedGold.sol b/packages/protocol/contracts/governance/UsingLockedGold.sol deleted file mode 100644 index 462895f8f79..00000000000 --- a/packages/protocol/contracts/governance/UsingLockedGold.sol +++ /dev/null @@ -1,99 +0,0 @@ -pragma solidity ^0.5.3; - -import "./interfaces/ILockedGold.sol"; -import "../common/UsingRegistry.sol"; - - -/** - * @title A contract for calling functions on the LockedGold contract. - * @dev Any contract calling these functions should guard against reentrancy. - */ -contract UsingLockedGold is UsingRegistry { - /** - * @notice Returns whether or not an account's voting power is frozen. - * @param account The address of the account. - * @return Whether or not the account's voting power is frozen. - * @dev Frozen accounts can retract existing votes but not make future votes. - */ - function isVotingFrozen(address account) internal view returns (bool) { - return getLockedGold().isVotingFrozen(account); - } - - /** - * @notice Returns the account associated with the provided account or voting delegate. - * @param accountOrDelegate The address of the account or voting delegate. - * @dev Fails if the `accountOrDelegate` is a non-voting delegate. - * @return The associated account. - */ - function getAccountFromVoter(address accountOrDelegate) internal view returns (address) { - return getLockedGold().getAccountFromDelegateAndRole( - accountOrDelegate, - ILockedGold.DelegateRole.Voting - ); - } - - /** - * @notice Returns the validator address for a particular account. - * @param account The account. - * @return The associated validator address. - */ - function getValidatorFromAccount(address account) internal view returns (address) { - return getLockedGold().getDelegateFromAccountAndRole( - account, - ILockedGold.DelegateRole.Validating - ); - } - - /** - * @notice Returns the account associated with the provided account or validating delegate. - * @param accountOrDelegate The address of the account or validating delegate. - * @dev Fails if the `accountOrDelegate` is a non-validating delegate. - * @return The associated account. - */ - function getAccountFromValidator(address accountOrDelegate) internal view returns (address) { - return getLockedGold().getAccountFromDelegateAndRole( - accountOrDelegate, - ILockedGold.DelegateRole.Validating - ); - } - - /** - * @notice Returns voting weight for a particular account. - * @param account The address of the account. - * @return The voting weight of `account`. - */ - function getAccountWeight(address account) internal view returns (uint256) { - return getLockedGold().getAccountWeight(account); - } - - /** - * @notice Returns the total weight. - * @return Total account weight. - */ - function getTotalWeight() internal view returns (uint256) { - return getLockedGold().totalWeight(); - } - - /** - * @notice Returns the Locked Gold commitment value for particular account and notice period. - * @param account The address of the account. - * @param noticePeriod The notice period of the Locked Gold commitment. - * @return The value of the Locked Gold commitment. - */ - function getLockedCommitmentValue( - address account, - uint256 noticePeriod - ) - internal - view - returns (uint256) - { - uint256 value; - (value,) = getLockedGold().getLockedCommitment(account, noticePeriod); - return value; - } - - function getLockedGold() private view returns(ILockedGold) { - return ILockedGold(registry.getAddressForOrDie(LOCKED_GOLD_REGISTRY_ID)); - } -} diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts/governance/Validators.sol index 695aaa61ea8..d73593323f7 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts/governance/Validators.sol @@ -1,11 +1,11 @@ pragma solidity ^0.5.3; +import "openzeppelin-solidity/contracts/math/Math.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol"; import "solidity-bytes-utils/contracts/BytesLib.sol"; -import "./UsingLockedGold.sol"; import "./interfaces/IValidators.sol"; import "../identity/interfaces/IRandom.sol"; @@ -13,89 +13,127 @@ import "../identity/interfaces/IRandom.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/linkedlists/AddressLinkedList.sol"; -import "../common/linkedlists/AddressSortedLinkedList.sol"; +import "../common/UsingRegistry.sol"; +import "../common/UsingPrecompiles.sol"; /** * @title A contract for registering and electing Validator Groups and Validators. */ -contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, UsingLockedGold { +contract Validators is + IValidators, Ownable, ReentrancyGuard, Initializable, UsingRegistry, UsingPrecompiles { using FixidityLib for FixidityLib.Fraction; using AddressLinkedList for LinkedList.List; - using AddressSortedLinkedList for SortedLinkedList.List; using SafeMath for uint256; using BytesLib for bytes; - // Address of the getValidator precompiled contract - address constant public GET_VALIDATOR_ADDRESS = address(0xfa); + address constant PROOF_OF_POSSESSION = address(0xff - 4); + uint256 constant MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + + // If an account has not registered a validator or group, these values represent the minimum + // amount of Locked Gold required to do so. + // If an account has a registered a validator or validator group, these values represent the + // minimum amount of Locked Gold required in order to earn epoch rewards. Furthermore, the + // account will not be able to unlock Gold if it would cause the account to fall below + // these values. + // If an account has deregistered a validator or validator group and is still subject to the + // `DeregistrationLockup`, the account will not be able to unlock Gold if it would cause the + // account to fall below these values. + struct BalanceRequirements { + uint256 group; + uint256 validator; + } + + // After deregistering a validator or validator group, the account will remain subject to the + // current balance requirements for this long (in seconds). + struct DeregistrationLockups { + uint256 group; + uint256 validator; + } + + // Stores the timestamps at which deregistration of a validator or validator group occurred. + struct DeregistrationTimestamps { + uint256 group; + uint256 validator; + } - // TODO(asa): These strings should be modifiable struct ValidatorGroup { - string identifier; string name; - string url; LinkedList.List members; + // TODO(asa): Add a function that allows groups to update their commission. + FixidityLib.Fraction commission; + } + + // Stores the epoch number at which a validator joined a particular group. + struct MembershipHistoryEntry { + uint256 epochNumber; + address group; + } + + // Stores the membership history of a validator. + struct MembershipHistory { + // The key to the most recent entry in the entries mapping. + uint256 tail; + // The number of entries in this validators membership history. + uint256 numEntries; + mapping(uint256 => MembershipHistoryEntry) entries; } - // TODO(asa): These strings should be modifiable struct Validator { - string identifier; string name; - string url; bytes publicKeysData; address affiliation; + FixidityLib.Fraction score; + MembershipHistory membershipHistory; } - struct LockedGoldCommitment { - uint256 noticePeriod; - uint256 value; + // Parameters that govern the calculation of validator's score. + struct ValidatorScoreParameters { + uint256 exponent; + FixidityLib.Fraction adjustmentSpeed; } mapping(address => ValidatorGroup) private groups; mapping(address => Validator) private validators; - // TODO(asa): Implement abstaining - mapping(address => address) public voters; + mapping(address => DeregistrationTimestamps) private deregistrationTimestamps; address[] private _groups; address[] private _validators; - SortedLinkedList.List private votes; - // TODO(asa): Support different requirements for groups vs. validators. - LockedGoldCommitment private registrationRequirement; - uint256 public minElectableValidators; - uint256 public maxElectableValidators; - FixidityLib.Fraction electionThreshold; - uint256 totalVotes; // keeps track of total weight of accounts that have active votes - - address constant PROOF_OF_POSSESSION = address(0xff - 4); - + BalanceRequirements public balanceRequirements; + DeregistrationLockups public deregistrationLockups; + ValidatorScoreParameters private validatorScoreParameters; + uint256 public validatorEpochPayment; + uint256 public membershipHistoryLength; uint256 public maxGroupSize; - event MinElectableValidatorsSet( - uint256 minElectableValidators + event MaxGroupSizeSet( + uint256 size ); - event ElectionThresholdSet( - uint256 electionThreshold + event ValidatorEpochPaymentSet( + uint256 value ); - event MaxElectableValidatorsSet( - uint256 maxElectableValidators + event ValidatorScoreParametersSet( + uint256 exponent, + uint256 adjustmentSpeed ); - event MaxGroupSizeSet( - uint256 maxGroupSize + event BalanceRequirementsSet( + uint256 group, + uint256 validator ); - event RegistrationRequirementSet( - uint256 value, - uint256 noticePeriod + event MembershipHistoryLengthSet(uint256 length); + + event DeregistrationLockupsSet( + uint256 group, + uint256 validator ); event ValidatorRegistered( address indexed validator, - string identifier, string name, - string url, bytes publicKeysData ); @@ -115,9 +153,8 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi event ValidatorGroupRegistered( address indexed group, - string identifier, string name, - string url + uint256 commission ); event ValidatorGroupDeregistered( @@ -139,179 +176,164 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi address indexed validator ); - event ValidatorGroupEmptied( - address indexed group - ); - - event ValidatorGroupVoteCast( - address indexed account, - address indexed group, - uint256 weight - ); - - event ValidatorGroupVoteRevoked( - address indexed account, - address indexed group, - uint256 weight - ); + modifier onlyVm() { + require(msg.sender == address(0)); + _; + } /** * @notice Initializes critical variables. * @param registryAddress The address of the registry contract. - * @param _minElectableValidators The minimum number of validators that can be elected. - * @param _maxElectableValidators The maximum number of validators that can be elected. - * @param requirementValue The minimum Locked Gold commitment value to register a group or - validator. - * @param requirementNoticePeriod The minimum Locked Gold commitment notice period to register - * a group or validator. - * @param threshold The minimum ratio of votes a group needs before its members can be elected. + * @param groupRequirement The minimum locked gold needed to register a group. + * @param validatorRequirement The minimum locked gold needed to register a validator. + * @param groupLockup The duration the above gold remains locked after deregistration. + * @param validatorLockup The duration the above gold remains locked after deregistration. + * @param validatorScoreExponent The exponent used in calculating validator scores. + * @param validatorScoreAdjustmentSpeed The speed at which validator scores are adjusted. + * @param _validatorEpochPayment The duration the above gold remains locked after deregistration. + * @param _membershipHistoryLength The max number of entries for validator membership history. + * @param _maxGroupSize The maximum group size. * @dev Should be called only once. */ function initialize( address registryAddress, - uint256 _minElectableValidators, - uint256 _maxElectableValidators, - uint256 requirementValue, - uint256 requirementNoticePeriod, - uint256 _maxGroupSize, - uint256 threshold + uint256 groupRequirement, + uint256 validatorRequirement, + uint256 groupLockup, + uint256 validatorLockup, + uint256 validatorScoreExponent, + uint256 validatorScoreAdjustmentSpeed, + uint256 _validatorEpochPayment, + uint256 _membershipHistoryLength, + uint256 _maxGroupSize ) external initializer { - require(_minElectableValidators > 0 && _maxElectableValidators >= _minElectableValidators); _transferOwnership(msg.sender); setRegistry(registryAddress); - setElectionThreshold(threshold); - minElectableValidators = _minElectableValidators; - maxElectableValidators = _maxElectableValidators; - registrationRequirement.value = requirementValue; - registrationRequirement.noticePeriod = requirementNoticePeriod; + setValidatorEpochPayment(_validatorEpochPayment); + setValidatorScoreParameters(validatorScoreExponent, validatorScoreAdjustmentSpeed); + setBalanceRequirements(groupRequirement, validatorRequirement); + setDeregistrationLockups(groupLockup, validatorLockup); setMaxGroupSize(_maxGroupSize); + setMembershipHistoryLength(_membershipHistoryLength); } /** - * @notice Updates the minimum number of validators that can be elected. - * @param _minElectableValidators The minimum number of validators that can be elected. + * @notice Updates the maximum number of members a group can have. + * @param size The maximum group size. * @return True upon success. */ - function setMinElectableValidators( - uint256 _minElectableValidators - ) - external - onlyOwner - returns (bool) - { - require( - _minElectableValidators > 0 && - _minElectableValidators != minElectableValidators && - _minElectableValidators <= maxElectableValidators - ); - minElectableValidators = _minElectableValidators; - emit MinElectableValidatorsSet(_minElectableValidators); + function setMaxGroupSize(uint256 size) public onlyOwner returns (bool) { + require(0 < size && size != maxGroupSize); + maxGroupSize = size; + emit MaxGroupSizeSet(size); return true; } /** - * @notice Updates the maximum number of validators that can be elected. - * @param _maxElectableValidators The maximum number of validators that can be elected. + * @notice Updates the number of validator group membership entries to store. + * @param length The number of validator group membership entries to store. * @return True upon success. */ - function setMaxElectableValidators( - uint256 _maxElectableValidators - ) - external - onlyOwner - returns (bool) - { - require( - _maxElectableValidators != maxElectableValidators && - _maxElectableValidators >= minElectableValidators - ); - maxElectableValidators = _maxElectableValidators; - emit MaxElectableValidatorsSet(_maxElectableValidators); + function setMembershipHistoryLength(uint256 length) public onlyOwner returns (bool) { + require(0 < length && length != membershipHistoryLength); + membershipHistoryLength = length; + emit MembershipHistoryLengthSet(length); return true; } /** - * @notice Changes the maximum group size. - * @param _maxGroupSize The maximum number of validators for each group. + * @notice Sets the per-epoch payment in Celo Dollars for validators, less group commission. + * @param value The value in Celo Dollars. * @return True upon success. */ - function setMaxGroupSize( - uint256 _maxGroupSize - ) - public - onlyOwner - returns (bool) - { - require(_maxGroupSize > 0); - maxGroupSize = _maxGroupSize; - emit MaxGroupSizeSet(_maxGroupSize); + function setValidatorEpochPayment(uint256 value) public onlyOwner returns (bool) { + require(value != validatorEpochPayment); + validatorEpochPayment = value; + emit ValidatorEpochPaymentSet(value); return true; } /** - * @notice Updates the minimum bonding requirements to register a validator group or validator. - * @param value The minimum Locked Gold commitment value to register a group or validator. - * @param noticePeriod The minimum Locked Gold commitment notice period to register a group or - * validator. + * @notice Updates the validator score parameters. + * @param exponent The exponent used in calculating the score. + * @param adjustmentSpeed The speed at which the score is adjusted. * @return True upon success. - * @dev The new requirement is only enforced for future validator or group registrations. */ - function setRegistrationRequirement( - uint256 value, - uint256 noticePeriod + function setValidatorScoreParameters( + uint256 exponent, + uint256 adjustmentSpeed ) - external + public onlyOwner returns (bool) { + require(adjustmentSpeed <= FixidityLib.fixed1().unwrap()); require( - value != registrationRequirement.value || - noticePeriod != registrationRequirement.noticePeriod + exponent != validatorScoreParameters.exponent || + !FixidityLib.wrap(adjustmentSpeed).equals(validatorScoreParameters.adjustmentSpeed) ); - registrationRequirement.value = value; - registrationRequirement.noticePeriod = noticePeriod; - emit RegistrationRequirementSet(value, noticePeriod); + validatorScoreParameters = ValidatorScoreParameters( + exponent, + FixidityLib.wrap(adjustmentSpeed) + ); + emit ValidatorScoreParametersSet(exponent, adjustmentSpeed); return true; } /** - * @notice Sets the election threshold. - * @param threshold Election threshold as unwrapped Fraction. + * @notice Returns the maximum number of members a group can add. + * @return The maximum number of members a group can add. + */ + function getMaxGroupSize() external view returns (uint256) { + return maxGroupSize; + } + + /** + * @notice Updates the minimum gold requirements to register a group/validator and earn rewards. + * @param group The minimum locked gold needed to register a group and earn rewards. + * @param validator The minimum locked gold needed to register a validator and earn rewards. * @return True upon success. */ - function setElectionThreshold(uint256 threshold) + function setBalanceRequirements( + uint256 group, + uint256 validator + ) public onlyOwner returns (bool) { - electionThreshold = FixidityLib.wrap(threshold); - require( - electionThreshold.lt(FixidityLib.fixed1()), - "Election threshold must be lower than 100%" - ); - emit ElectionThresholdSet(threshold); + require(group != balanceRequirements.group || validator != balanceRequirements.validator); + balanceRequirements = BalanceRequirements(group, validator); + emit BalanceRequirementsSet(group, validator); return true; } /** - * @notice Gets the election threshold. - * @return Threshold value as unwrapped fraction. + * @notice Updates the duration for which gold remains locked after deregistration. + * @param group The lockup duration for groups in seconds. + * @param validator The lockup duration for validators in seconds. + * @return True upon success. */ - function getElectionThreshold() external view returns (uint256) { - return electionThreshold.unwrap(); + function setDeregistrationLockups( + uint256 group, + uint256 validator + ) + public + onlyOwner + returns (bool) + { + require(group != deregistrationLockups.group || validator != deregistrationLockups.validator); + deregistrationLockups = DeregistrationLockups(group, validator); + emit DeregistrationLockupsSet(group, validator); + return true; } - /** * @notice Registers a validator unaffiliated with any validator group. - * @param identifier An identifier for this validator. * @param name A name for the validator. - * @param url A URL for the validator. - * @param noticePeriods The notice periods of the Locked Gold commitments that - * cumulatively meet the requirements for validator registration. * @param publicKeysData Comprised of three tightly-packed elements: * - publicKey - The public key that the validator is using for consensus, should match * msg.sender. 64 bytes. @@ -323,34 +345,30 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if the account does not have sufficient weight. */ function registerValidator( - string calldata identifier, string calldata name, - string calldata url, - bytes calldata publicKeysData, - uint256[] calldata noticePeriods + bytes calldata publicKeysData ) external nonReentrant returns (bool) { require( - bytes(identifier).length > 0 && bytes(name).length > 0 && - bytes(url).length > 0 && // secp256k1 public key + BLS public key + BLS proof of possession publicKeysData.length == (64 + 48 + 96) ); // Use the proof of possession bytes require(checkProofOfPossession(publicKeysData.slice(64, 48 + 96))); - address account = getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); - require(meetsRegistrationRequirements(account, noticePeriods)); + require(meetsValidatorBalanceRequirements(account)); - Validator memory validator = Validator(identifier, name, url, publicKeysData, address(0)); - validators[account] = validator; + validators[account].name = name; + validators[account].publicKeysData = publicKeysData; _validators.push(account); - emit ValidatorRegistered(account, identifier, name, url, publicKeysData); + updateMembershipHistory(account, address(0)); + emit ValidatorRegistered(account, name, publicKeysData); return true; } @@ -365,6 +383,140 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi return success; } + /** + * @notice Returns whether an account meets the requirements to register a validator. + * @param account The account. + * @return Whether an account meets the requirements to register a validator. + */ + function meetsValidatorBalanceRequirements(address account) public view returns (bool) { + return getLockedGold().getAccountTotalLockedGold(account) >= balanceRequirements.validator; + } + + /** + * @notice Returns whether an account meets the requirements to register a group. + * @param account The account. + * @return Whether an account meets the requirements to register a group. + */ + function meetsValidatorGroupBalanceRequirements(address account) public view returns (bool) { + return getLockedGold().getAccountTotalLockedGold(account) >= balanceRequirements.group; + } + + /** + * @notice Returns the parameters that goven how a validator's score is calculated. + * @return The parameters that goven how a validator's score is calculated. + */ + function getValidatorScoreParameters() external view returns (uint256, uint256) { + return (validatorScoreParameters.exponent, validatorScoreParameters.adjustmentSpeed.unwrap()); + } + + /** + * @notice Returns the group membership history of a validator. + * @param account The validator whose membership history to return. + * @return The group membership history of a validator. + */ + function getMembershipHistory( + address account + ) + external + view + returns (uint256[] memory, address[] memory) + { + MembershipHistory storage history = validators[account].membershipHistory; + uint256[] memory epochs = new uint256[](history.numEntries); + address[] memory membershipGroups = new address[](history.numEntries); + for (uint256 i = 0; i < history.numEntries; i = i.add(1)) { + uint256 index = history.tail.add(i); + epochs[i] = history.entries[index].epochNumber; + membershipGroups[i] = history.entries[index].group; + } + return (epochs, membershipGroups); + } + + /** + * @notice Updates a validator's score based on its uptime for the epoch. + * @param validator The address of the validator. + * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. + * @return True upon success. + */ + function updateValidatorScore(address validator, uint256 uptime) external onlyVm() { + _updateValidatorScore(validator, uptime); + } + + /** + * @notice Updates a validator's score based on its uptime for the epoch. + * @param validator The address of the validator. + * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. + * @dev new_score = uptime ** exponent * adjustmentSpeed + old_score * (1 - adjustmentSpeed) + * @return True upon success. + */ + function _updateValidatorScore(address validator, uint256 uptime) internal { + address account = getLockedGold().getAccountFromValidator(validator); + require(isValidator(account), "isvalidator"); + require(uptime <= FixidityLib.fixed1().unwrap(), "uptime"); + + uint256 numerator; + uint256 denominator; + (numerator, denominator) = fractionMulExp( + FixidityLib.fixed1().unwrap(), + FixidityLib.fixed1().unwrap(), + uptime, + FixidityLib.fixed1().unwrap(), + validatorScoreParameters.exponent, + 18 + ); + + FixidityLib.Fraction memory epochScore = FixidityLib.wrap(numerator).divide( + FixidityLib.wrap(denominator) + ); + FixidityLib.Fraction memory newComponent = validatorScoreParameters.adjustmentSpeed.multiply( + epochScore + ); + + FixidityLib.Fraction memory currentComponent = FixidityLib.fixed1().subtract( + validatorScoreParameters.adjustmentSpeed + ); + currentComponent = currentComponent.multiply(validators[account].score); + validators[account].score = FixidityLib.wrap( + Math.min( + epochScore.unwrap(), + newComponent.add(currentComponent).unwrap() + ) + ); + } + + /** + * @notice Distributes epoch payments to `validator` and its group. + */ + function distributeEpochPayment(address validator) external onlyVm() { + _distributeEpochPayment(validator); + } + + /** + * @notice Distributes epoch payments to `validator` and its group. + */ + function _distributeEpochPayment(address validator) internal { + address account = getLockedGold().getAccountFromValidator(validator); + require(isValidator(account)); + // The group that should be paid is the group that the validator was a member of at the + // time it was elected. + address group = getMembershipInLastEpoch(account); + // Both the validator and the group must maintain the minimum locked gold balance in order to + // receive epoch payments. + bool meetsBalanceRequirements = ( + getLockedGold().getAccountTotalLockedGold(group) >= getAccountBalanceRequirement(group) && + getLockedGold().getAccountTotalLockedGold(account) >= getAccountBalanceRequirement(account) + ); + if (meetsBalanceRequirements) { + FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed( + validatorEpochPayment + ).multiply(validators[account].score); + uint256 groupPayment = totalPayment.multiply(groups[group].commission).fromFixed(); + uint256 validatorPayment = totalPayment.fromFixed().sub(groupPayment); + getStableToken().mint(group, groupPayment); + getStableToken().mint(account, validatorPayment); + } + } + /** * @notice De-registers a validator, removing it from the group for which it is a member. * @param index The index of this validator in the list of all validators. @@ -372,7 +524,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if the account is not a validator. */ function deregisterValidator(uint256 index) external nonReentrant returns (bool) { - address account = getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; if (validator.affiliation != address(0)) { @@ -380,6 +532,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi } delete validators[account]; deleteElement(_validators, account, index); + deregistrationTimestamps[account].validator = now; emit ValidatorDeregistered(account); return true; } @@ -391,7 +544,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev De-affiliates with the previously affiliated group if present. */ function affiliate(address group) external nonReentrant returns (bool) { - address account = getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(isValidator(account) && isValidatorGroup(group)); Validator storage validator = validators[account]; if (validator.affiliation != address(0)) { @@ -408,7 +561,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if the account is not a validator with non-zero affiliation. */ function deaffiliate() external nonReentrant returns (bool) { - address account = getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(isValidator(account)); Validator storage validator = validators[account]; require(validator.affiliation != address(0)); @@ -418,35 +571,32 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi /** * @notice Registers a validator group with no member validators. - * @param identifier A identifier for this validator group. * @param name A name for the validator group. - * @param url A URL for the validator group. - * @param noticePeriods The notice periods of the Locked Gold commitments that - * cumulatively meet the requirements for validator registration. + * @param commission Fixidity representation of the commission this group receives on epoch + * payments made to its members. * @return True upon success. * @dev Fails if the account is already a validator or validator group. * @dev Fails if the account does not have sufficient weight. */ function registerValidatorGroup( - string calldata identifier, string calldata name, - string calldata url, - uint256[] calldata noticePeriods + uint256 commission ) external nonReentrant returns (bool) { - require(bytes(identifier).length > 0 && bytes(name).length > 0 && bytes(url).length > 0); - address account = getAccountFromValidator(msg.sender); + require(bytes(name).length > 0); + require(commission <= FixidityLib.fixed1().unwrap(), "Commission can't be greater than 100%"); + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(!isValidator(account) && !isValidatorGroup(account)); - require(meetsRegistrationRequirements(account, noticePeriods)); + require(meetsValidatorGroupBalanceRequirements(account)); + ValidatorGroup storage group = groups[account]; - group.identifier = identifier; group.name = name; - group.url = url; + group.commission = FixidityLib.wrap(commission); _groups.push(account); - emit ValidatorGroupRegistered(account, identifier, name, url); + emit ValidatorGroupRegistered(account, name, commission); return true; } @@ -457,11 +607,12 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if the account is not a validator group with no members. */ function deregisterValidatorGroup(uint256 index) external nonReentrant returns (bool) { - address account = getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); // Only empty Validator Groups can be deregistered. require(isValidatorGroup(account) && groups[account].members.numElements == 0); delete groups[account]; deleteElement(_groups, account, index); + deregistrationTimestamps[account].group = now; emit ValidatorGroupDeregistered(account); return true; } @@ -471,15 +622,65 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @param validator The validator to add to the group * @return True upon success. * @dev Fails if `validator` has not set their affiliation to this account. + * @dev Fails if the group has zero members. */ function addMember(address validator) external nonReentrant returns (bool) { - address account = getAccountFromValidator(msg.sender); - require(isValidatorGroup(account) && isValidator(validator)); - ValidatorGroup storage group = groups[account]; - require(validators[validator].affiliation == account && !group.members.contains(validator)); - require(group.members.numElements < maxGroupSize, "Maximum group size exceeded"); - group.members.push(validator); - emit ValidatorGroupMemberAdded(account, validator); + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); + require(groups[account].members.numElements > 0); + return _addMember(account, validator, address(0), address(0)); + } + + /** + * @notice Adds the first member to a group's list of members and marks it eligible for election. + * @param validator The validator to add to the group + * @param lesser The address of the group that has received fewer votes than this group. + * @param greater The address of the group that has received more votes than this group. + * @return True upon success. + * @dev Fails if `validator` has not set their affiliation to this account. + * @dev Fails if the group has > 0 members. + */ + function addFirstMember( + address validator, + address lesser, + address greater + ) + external + nonReentrant + returns (bool) + { + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); + require(groups[account].members.numElements == 0); + return _addMember(account, validator, lesser, greater); + } + + /** + * @notice Adds a member to the end of a validator group's list of members. + * @param group The address of the validator group. + * @param validator The validator to add to the group. + * @param lesser The address of the group that has received fewer votes than this group. + * @param greater The address of the group that has received more votes than this group. + * @return True upon success. + * @dev Fails if `validator` has not set their affiliation to this account. + */ + function _addMember( + address group, + address validator, + address lesser, + address greater + ) + private + returns (bool) + { + require(isValidatorGroup(group) && isValidator(validator)); + ValidatorGroup storage _group = groups[group]; + require(_group.members.numElements < maxGroupSize, "group would exceed maximum size"); + require(validators[validator].affiliation == group && !_group.members.contains(validator)); + _group.members.push(validator); + if (_group.members.numElements == 1) { + getElection().markGroupEligible(group, lesser, greater); + } + updateMembershipHistory(validator, group); + emit ValidatorGroupMemberAdded(group, validator); return true; } @@ -490,8 +691,8 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @dev Fails if `validator` is not a member of the account's group. */ function removeMember(address validator) external nonReentrant returns (bool) { - address account = getAccountFromValidator(msg.sender); - require(isValidatorGroup(account) && isValidator(validator)); + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); + require(isValidatorGroup(account) && isValidator(validator), "is not group and validator"); return _removeMember(account, validator); } @@ -514,7 +715,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi nonReentrant returns (bool) { - address account = getAccountFromValidator(msg.sender); + address account = getLockedGold().getAccountFromActiveValidator(msg.sender); require(isValidatorGroup(account) && isValidator(validator)); ValidatorGroup storage group = groups[account]; require(group.members.contains(validator)); @@ -524,134 +725,34 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi } /** - * @notice Casts a vote for a validator group. - * @param group The validator group to vote for. - * @param lesser The group receiving fewer votes than `group`, or 0 if `group` has the - * fewest votes of any validator group. - * @param greater The group receiving more votes than `group`, or 0 if `group` has the - * most votes of any validator group. - * @return True upon success. - * @dev Fails if `group` is empty or not a validator group. - * @dev Fails if the account is frozen. + * @notice Returns the locked gold balance requirement for the supplied account. + * @param account The account that may have to meet locked gold balance requirements. + * @return The locked gold balance requirement for the supplied account. */ - function vote( - address group, - address lesser, - address greater - ) - external - nonReentrant - returns (bool) - { - // Empty validator groups are not electable. - require(isValidatorGroup(group) && groups[group].members.numElements > 0); - address account = getAccountFromVoter(msg.sender); - require(!isVotingFrozen(account)); - require(voters[account] == address(0)); - uint256 weight = getAccountWeight(account); - require(weight > 0); - totalVotes = totalVotes.add(weight); - if (votes.contains(group)) { - votes.update( - group, - votes.getValue(group).add(uint256(weight)), - lesser, - greater - ); - } else { - votes.insert( - group, - weight, - lesser, - greater - ); + function getAccountBalanceRequirement(address account) public view returns (uint256) { + DeregistrationTimestamps storage timestamps = deregistrationTimestamps[account]; + if ( + isValidator(account) || + (timestamps.validator > 0 && now < timestamps.validator.add(deregistrationLockups.validator)) + ) { + return balanceRequirements.validator; } - voters[account] = group; - emit ValidatorGroupVoteCast(account, group, weight); - return true; + if ( + isValidatorGroup(account) || + (timestamps.group > 0 && now < timestamps.group.add(deregistrationLockups.group)) + ) { + return balanceRequirements.group; + } + return 0; } /** - * @notice Revokes an outstanding vote for a validator group. - * @param lesser The group receiving fewer votes than the group for which the vote was revoked, - * or 0 if that group has the fewest votes of any validator group. - * @param greater The group receiving more votes than the group for which the vote was revoked, - * or 0 if that group has the most votes of any validator group. - * @return True upon success. - * @dev Fails if the account has not voted on a validator group. + * @notice Returns the timestamp of the last time this account deregistered a validator or group. + * @param account The account to query. + * @return The timestamp of the last time this account deregistered a validator or group. */ - function revokeVote( - address lesser, - address greater - ) - external - nonReentrant - returns (bool) - { - address account = getAccountFromVoter(msg.sender); - address group = voters[account]; - require(group != address(0)); - uint256 weight = getAccountWeight(account); - totalVotes = totalVotes.sub(weight); - // If the group we had previously voted on removed all its members it is no longer eligible - // to receive votes and we don't have to worry about removing our vote. - if (votes.contains(group)) { - require(weight > 0); - uint256 newVoteTotal = votes.getValue(group).sub(uint256(weight)); - if (newVoteTotal > 0) { - votes.update( - group, - newVoteTotal, - lesser, - greater - ); - } else { - // Groups receiving no votes are not electable. - votes.remove(group); - } - } - voters[account] = address(0); - emit ValidatorGroupVoteRevoked(account, group, weight); - return true; - } - - function validatorAddressFromCurrentSet(uint256 index) external view returns (address) { - address validatorAddress; - assembly { - let newCallDataPosition := mload(0x40) - mstore(newCallDataPosition, index) - let success := staticcall( - 5000, - 0xfa, - newCallDataPosition, - 32, - 0, - 0 - ) - returndatacopy(add(newCallDataPosition, 64), 0, 32) - validatorAddress := mload(add(newCallDataPosition, 64)) - } - - return validatorAddress; - } - - function numberValidatorsInCurrentSet() external view returns (uint256) { - uint256 numberValidators; - assembly { - let success := staticcall( - 5000, - 0xf9, - 0, - 0, - 0, - 0 - ) - let returnData := mload(0x40) - returndatacopy(returnData, 0, 32) - numberValidators := mload(returnData) - } - - return numberValidators; + function getDeregistrationTimestamps(address account) external view returns (uint256, uint256) { + return (deregistrationTimestamps[account].group, deregistrationTimestamps[account].validator); } /** @@ -665,21 +766,19 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi external view returns ( - string memory identifier, string memory name, - string memory url, bytes memory publicKeysData, - address affiliation + address affiliation, + uint256 score ) { require(isValidator(account)); Validator storage validator = validators[account]; return ( - validator.identifier, validator.name, - validator.url, validator.publicKeysData, - validator.affiliation + validator.affiliation, + validator.score.unwrap() ); } @@ -693,142 +792,103 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi ) external view - returns (string memory, string memory, string memory, address[] memory) + returns (string memory, address[] memory, uint256) { require(isValidatorGroup(account)); ValidatorGroup storage group = groups[account]; - return (group.identifier, group.name, group.url, group.members.getKeys()); + return (group.name, group.members.getKeys(), group.commission.unwrap()); } /** - * @notice Returns electable validator group addresses and their vote totals. - * @return Electable validator group addresses and their vote totals. + * @notice Returns the number of members in a validator group. + * @param account The address of the validator group. + * @return The number of members in a validator group. */ - function getValidatorGroupVotes() external view returns (address[] memory, uint256[] memory) { - return votes.getElements(); + function getGroupNumMembers(address account) public view returns (uint256) { + require(isValidatorGroup(account)); + return groups[account].members.numElements; } /** - * @notice Returns the number of votes a particular validator group has received. - * @param group The account that registered the validator group. - * @return The number of votes a particular validator group has received. + * @notice Returns the top n group members for a particular group. + * @param account The address of the validator group. + * @param n The number of members to return. + * @return The top n group members for a particular group. */ - function getVotesReceived(address group) external view returns (uint256) { - return votes.getValue(group); + function getTopGroupValidators( + address account, + uint256 n + ) + external + view + returns (address[] memory) + { + address[] memory topAccounts = groups[account].members.headN(n); + address[] memory topValidators = new address[](n); + for (uint256 i = 0; i < n; i = i.add(1)) { + topValidators[i] = getLockedGold().getValidatorFromAccount(topAccounts[i]); + } + return topValidators; } /** - * @notice Returns the Locked Gold commitment requirements to register a validator or group. - * @return The minimum value and notice period for the Locked Gold commitment. + * @notice Returns the number of members in the provided validator groups. + * @param accounts The addresses of the validator groups. + * @return The number of members in the provided validator groups. */ - function getRegistrationRequirement() external view returns (uint256, uint256) { - return (registrationRequirement.value, registrationRequirement.noticePeriod); + function getGroupsNumMembers( + address[] calldata accounts + ) + external + view + returns (uint256[] memory) + { + uint256[] memory numMembers = new uint256[](accounts.length); + for (uint256 i = 0; i < accounts.length; i = i.add(1)) { + numMembers[i] = getGroupNumMembers(accounts[i]); + } + return numMembers; } /** - * @notice Returns the list of registered validator accounts. - * @return The list of registered validator accounts. + * @notice Returns the number of registered validators. + * @return The number of registered validators. */ - function getRegisteredValidators() external view returns (address[] memory) { - return _validators; + function getNumRegisteredValidators() external view returns (uint256) { + return _validators.length; } /** - * @notice Returns the list of registered validator group accounts. - * @return The list of registered validator group addresses. + * @notice Returns the Locked Gold requirements to register a validator or group. + * @return The locked gold requirements to register a validator or group. */ - function getRegisteredValidatorGroups() external view returns (address[] memory) { - return _groups; + function getBalanceRequirements() external view returns (uint256, uint256) { + return (balanceRequirements.group, balanceRequirements.validator); } /** - * @notice Returns whether a particular account is a registered validator or validator group. - * @param account The account. - * @return Whether a particular account is a registered validator or validator group. + * @notice Returns the lockup periods after deregistering groups and validators. + * @return The lockup periods after deregistering groups and validators. */ - function isValidating(address account) external view returns (bool) { - return isValidator(account) || isValidatorGroup(account); + function getDeregistrationLockups() external view returns (uint256, uint256) { + return (deregistrationLockups.group, deregistrationLockups.validator); } /** - * @notice Returns whether a particular account is voting for a validator group. - * @param account The account. - * @return Whether a particular account is voting for a validator group. + * @notice Returns the list of registered validator accounts. + * @return The list of registered validator accounts. */ - function isVoting(address account) external view returns (bool) { - return (voters[account] != address(0)); + function getRegisteredValidators() external view returns (address[] memory) { + return _validators; } /** - * @notice Returns a list of elected validators with seats allocated to groups via the D'Hondt - * method. - * @return The list of elected validators. - * @dev See https://en.wikipedia.org/wiki/D%27Hondt_method#Allocation for more information. + * @notice Returns the list of registered validator group accounts. + * @return The list of registered validator group addresses. */ - /* solhint-disable code-complexity */ - function getValidators() external view returns (address[] memory) { - // Only members of these validator groups are eligible for election. - uint256 numElectionGroups = maxElectableValidators; - if (numElectionGroups > votes.list.numElements) { - numElectionGroups = votes.list.numElements; - } - require(numElectionGroups > 0, "No votes have been cast"); - address[] memory electionGroups = votes.list.headN(numElectionGroups); - // Holds the number of members elected for each of the eligible validator groups. - uint256[] memory numMembersElected = new uint256[](electionGroups.length); - uint256 totalNumMembersElected = 0; - bool memberElectedInRound = true; - - // Assign a number of seats to each validator group. - while (totalNumMembersElected < maxElectableValidators && memberElectedInRound) { - memberElectedInRound = false; - uint256 groupIndex = 0; - FixidityLib.Fraction memory maxN = FixidityLib.wrap(0); - for (uint256 i = 0; i < electionGroups.length; i = i.add(1)) { - bool isWinningestGroupInRound = false; - uint256 numVotes = votes.getValue(electionGroups[i]); - FixidityLib.Fraction memory percentVotes = FixidityLib.newFixedFraction( - numVotes, - totalVotes - ); - if (percentVotes.lt(electionThreshold)) break; - (maxN, isWinningestGroupInRound) = dHondt(maxN, electionGroups[i], numMembersElected[i]); - if (isWinningestGroupInRound) { - memberElectedInRound = true; - groupIndex = i; - } - } - - if (memberElectedInRound) { - numMembersElected[groupIndex] = numMembersElected[groupIndex].add(1); - totalNumMembersElected = totalNumMembersElected.add(1); - } - } - require(totalNumMembersElected >= minElectableValidators); - // Grab the top validators from each group that won seats. - address[] memory electedValidators = new address[](totalNumMembersElected); - totalNumMembersElected = 0; - for (uint256 i = 0; i < electionGroups.length; i = i.add(1)) { - address[] memory electedGroupMembers = groups[electionGroups[i]].members.headN( - numMembersElected[i] - ); - for (uint256 j = 0; j < electedGroupMembers.length; j = j.add(1)) { - // We use the validating delegate if one is set. - electedValidators[totalNumMembersElected] = getValidatorFromAccount(electedGroupMembers[j]); - totalNumMembersElected = totalNumMembersElected.add(1); - } - } - // Shuffle the validator set using validator-supplied entropy - IRandom random = IRandom(registry.getAddressForOrDie(RANDOM_REGISTRY_ID)); - bytes32 r = random.random(); - for (uint256 i = electedValidators.length - 1; i > 0; i = i.sub(1)) { - uint256 j = uint256(r) % (i + 1); - (electedValidators[i], electedValidators[j]) = (electedValidators[j], electedValidators[i]); - r = keccak256(abi.encodePacked(r)); - } - return electedValidators; + function getRegisteredValidatorGroups() external view returns (address[] memory) { + return _groups; } - /* solhint-enable code-complexity */ /** * @notice Returns whether a particular account has a registered validator group. @@ -836,7 +896,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @return Whether a particular address is a registered validator group. */ function isValidatorGroup(address account) public view returns (bool) { - return bytes(groups[account].identifier).length > 0; + return bytes(groups[account].name).length > 0; } /** @@ -845,34 +905,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi * @return Whether a particular address is a registered validator. */ function isValidator(address account) public view returns (bool) { - return bytes(validators[account].identifier).length > 0; - } - - /** - * @notice Returns whether an account meets the requirements to register a validator or group. - * @param account The account. - * @param noticePeriods An array of notice periods of the Locked Gold commitments - * that cumulatively meet the requirements for validator registration. - * @return Whether an account meets the requirements to register a validator or group. - */ - function meetsRegistrationRequirements( - address account, - uint256[] memory noticePeriods - ) - public - view - returns (bool) - { - uint256 lockedValueSum = 0; - for (uint256 i = 0; i < noticePeriods.length; i = i.add(1)) { - if (noticePeriods[i] >= registrationRequirement.noticePeriod) { - lockedValueSum = lockedValueSum.add(getLockedCommitmentValue(account, noticePeriods[i])); - if (lockedValueSum >= registrationRequirement.value) { - return true; - } - } - } - return false; + return bytes(validators[account].name).length > 0; } /** @@ -885,7 +918,7 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi require(index < list.length && list[index] == element); uint256 lastIndex = list.length.sub(1); list[index] = list[lastIndex]; - list[lastIndex] = address(0); + delete list[lastIndex]; list.length = lastIndex; } @@ -901,18 +934,74 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi ValidatorGroup storage _group = groups[group]; require(validators[validator].affiliation == group && _group.members.contains(validator)); _group.members.remove(validator); + updateMembershipHistory(validator, address(0)); emit ValidatorGroupMemberRemoved(group, validator); // Empty validator groups are not electable. if (groups[group].members.numElements == 0) { - if (votes.contains(group)) { - votes.remove(group); - } - emit ValidatorGroupEmptied(group); + getElection().markGroupIneligible(group); } return true; } + /** + * @notice Updates the group membership history of a particular account. + * @param account The account whose group membership has changed. + * @param group The group that the account is now a member of. + * @return True upon success. + * @dev Note that this is used to determine a validator's membership at the time of an election, + * and so group changes within an epoch will overwrite eachother. + */ + function updateMembershipHistory(address account, address group) private returns (bool) { + MembershipHistory storage history = validators[account].membershipHistory; + uint256 epochNumber = getEpochNumber(); + uint256 head = history.numEntries == 0 ? 0 : history.tail.add(history.numEntries.sub(1)); + + if (history.entries[head].epochNumber == epochNumber) { + // There have been no elections since the validator last changed membership, overwrite the + // previous entry. + history.entries[head] = MembershipHistoryEntry(epochNumber, group); + return true; + } + + // There have been elections since the validator last changed membership, create a new entry. + uint256 index = history.numEntries == 0 ? 0 : head.add(1); + history.entries[index] = MembershipHistoryEntry(epochNumber, group); + if (history.numEntries < membershipHistoryLength) { + // Not enough entries, don't remove any. + history.numEntries = history.numEntries.add(1); + } else if (history.numEntries == membershipHistoryLength) { + // Exactly enough entries, delete the oldest one to account for the one we added. + delete history.entries[history.tail]; + history.tail = history.tail.add(1); + } else { + // Too many entries, delete the oldest two to account for the one we added. + delete history.entries[history.tail]; + delete history.entries[history.tail.add(1)]; + history.numEntries = history.numEntries.sub(1); + history.tail = history.tail.add(2); + } + } + + /** + * @notice Returns the group that `account` was a member of at the end of the last epoch. + * @param account The account whose group membership should be returned. + * @return The group that `account` was a member of at the end of the last epoch. + */ + function getMembershipInLastEpoch(address account) public view returns (address) { + uint256 epochNumber = getEpochNumber(); + MembershipHistory storage history = validators[account].membershipHistory; + uint256 head = history.numEntries == 0 ? 0 : history.tail.add(history.numEntries.sub(1)); + // If the most recent entry in the membership history is for the current epoch number, we need + // to look at the previous entry. + if (history.entries[head].epochNumber == epochNumber) { + if (head > history.tail) { + head = head.sub(1); + } + } + return history.entries[head].group; + } + /** * @notice De-affiliates a validator, removing it from the group for which it is a member. * @param validator The validator to deaffiliate from their affiliated validator group. @@ -935,34 +1024,4 @@ contract Validators is IValidators, Ownable, ReentrancyGuard, Initializable, Usi validator.affiliation = address(0); return true; } - - /** - * @notice Runs D'Hondt for a validator group. - * @param maxN The maximum number of votes per elected seat for a group in this round. - * @param groupAddress The address of the validator group. - * @param numMembersElected The number of members elected so far for this group. - * @dev See https://en.wikipedia.org/wiki/D%27Hondt_method#Allocation for more information. - * @return The new `maxN` and whether or not the group should win a seat in this round thus far. - */ - function dHondt( - FixidityLib.Fraction memory maxN, - address groupAddress, - uint256 numMembersElected - ) - private - view - returns (FixidityLib.Fraction memory, bool) - { - ValidatorGroup storage group = groups[groupAddress]; - // Only consider groups with members left to be elected. - if (group.members.numElements > numMembersElected) { - FixidityLib.Fraction memory n = FixidityLib.newFixed(votes.getValue(groupAddress)).divide( - FixidityLib.newFixed(numMembersElected.add(1)) - ); - if (n.gt(maxN)) { - return (n, true); - } - } - return (maxN, false); - } } diff --git a/packages/protocol/contracts/governance/interfaces/IElection.sol b/packages/protocol/contracts/governance/interfaces/IElection.sol index dbb105d708b..caa2df9a882 100644 --- a/packages/protocol/contracts/governance/interfaces/IElection.sol +++ b/packages/protocol/contracts/governance/interfaces/IElection.sol @@ -2,5 +2,9 @@ pragma solidity ^0.5.3; interface IElection { - function isVoting(address) external view returns(bool); + function getTotalVotes() external view returns (uint256); + function getTotalVotesByAccount(address) external view returns (uint256); + function markGroupIneligible(address) external; + function markGroupEligible(address,address,address) external; + function electValidators() external view returns (address[] memory); } diff --git a/packages/protocol/contracts/governance/interfaces/IGovernance.sol b/packages/protocol/contracts/governance/interfaces/IGovernance.sol index 8b669933899..2db70134e22 100644 --- a/packages/protocol/contracts/governance/interfaces/IGovernance.sol +++ b/packages/protocol/contracts/governance/interfaces/IGovernance.sol @@ -49,9 +49,8 @@ interface IGovernance { function getUpvotes(uint256) external view returns (uint256); function getQueue() external view returns (uint256[] memory, uint256[] memory); function getDequeue() external view returns (uint256[] memory); - function getUpvotedProposal(address) external view returns (uint256); + function getUpvoteRecord(address) external view returns (uint256, uint256); function getMostRecentReferendumProposal(address) external view returns (uint256); - function isVoting(address) external view returns (bool); function isQueued(uint256) external view returns (bool); function isProposalPassing(uint256) external view returns (bool); } diff --git a/packages/protocol/contracts/governance/interfaces/ILockedGold.sol b/packages/protocol/contracts/governance/interfaces/ILockedGold.sol index 7a007feb0cc..179e262874b 100644 --- a/packages/protocol/contracts/governance/interfaces/ILockedGold.sol +++ b/packages/protocol/contracts/governance/interfaces/ILockedGold.sol @@ -2,27 +2,12 @@ pragma solidity ^0.5.3; interface ILockedGold { - enum DelegateRole {Validating, Voting, Rewards} - enum CommitmentType {Locked, Notified} - function initialize(address, uint256) external; - function isVotingFrozen(address) external view returns (bool); - function setCumulativeRewardWeight(uint256) external; - function setMaxNoticePeriod(uint256) external; - function redeemRewards() external returns (uint256); - function freezeVoting() external; - function unfreezeVoting() external; - function newCommitment(uint256) external payable returns (uint256); - function notifyCommitment(uint256, uint256) external returns (uint256); - function extendCommitment(uint256, uint256) external returns (uint256); - function withdrawCommitment(uint256) external returns (uint256); - function increaseNoticePeriod(uint256, uint256, uint256) external returns (uint256); - function getRewardsLastRedeemed(address) external view returns (uint96); - function getNoticePeriods(address) external view returns (uint256[] memory); - function getAvailabilityTimes(address) external view returns (uint256[] memory); - function getLockedCommitment(address, uint256) external view returns (uint256, uint256); - function getAccountWeight(address) external view returns (uint256); - function delegateRole(DelegateRole, address, uint8, bytes32, bytes32) external; - function getAccountFromDelegateAndRole(address, DelegateRole) external view returns (address); - function getDelegateFromAccountAndRole(address, DelegateRole) external view returns (address); - function totalWeight() external view returns (uint256); + function getAccountFromActiveVoter(address) external view returns (address); + function getAccountFromActiveValidator(address) external view returns (address); + function getAccountFromValidator(address) external view returns (address); + function getValidatorFromAccount(address) external view returns (address); + function incrementNonvotingAccountBalance(address, uint256) external; + function decrementNonvotingAccountBalance(address, uint256) external; + function getAccountTotalLockedGold(address) external view returns (uint256); + function getTotalLockedGold() external view returns (uint256); } diff --git a/packages/protocol/contracts/governance/interfaces/IValidators.sol b/packages/protocol/contracts/governance/interfaces/IValidators.sol index adde44d0178..41a68b70859 100644 --- a/packages/protocol/contracts/governance/interfaces/IValidators.sol +++ b/packages/protocol/contracts/governance/interfaces/IValidators.sol @@ -2,7 +2,9 @@ pragma solidity ^0.5.3; interface IValidators { - function isVoting(address) external view returns (bool); - function isValidating(address) external view returns (bool); - function getValidators() external view returns (address[] memory); + function getAccountBalanceRequirement(address) external view returns (uint256); + function getGroupNumMembers(address) external view returns (uint256); + function getGroupsNumMembers(address[] calldata) external view returns (uint256[] memory); + function getNumRegisteredValidators() external view returns (uint256); + function getTopGroupValidators(address, uint256) external view returns (address[] memory); } diff --git a/packages/protocol/contracts/governance/proxies/ElectionProxy.sol b/packages/protocol/contracts/governance/proxies/ElectionProxy.sol new file mode 100644 index 00000000000..f8f99107a2e --- /dev/null +++ b/packages/protocol/contracts/governance/proxies/ElectionProxy.sol @@ -0,0 +1,8 @@ +pragma solidity ^0.5.3; + +import "../../common/Proxy.sol"; + + +/* solhint-disable no-empty-blocks */ +contract ElectionProxy is Proxy { +} diff --git a/packages/protocol/contracts/governance/test/ElectionTest.sol b/packages/protocol/contracts/governance/test/ElectionTest.sol new file mode 100644 index 00000000000..37e1bb4dcb6 --- /dev/null +++ b/packages/protocol/contracts/governance/test/ElectionTest.sol @@ -0,0 +1,21 @@ +pragma solidity ^0.5.8; + +import "../Election.sol"; +import "../../common/FixidityLib.sol"; + +/** + * @title A wrapper around Election that exposes onlyVm functions for testing. + */ +contract ElectionTest is Election { + + function distributeEpochRewards( + address group, + uint256 value, + address lesser, + address greater + ) + external + { + return _distributeEpochRewards(group, value, lesser, greater); + } +} diff --git a/packages/protocol/contracts/governance/test/MockElection.sol b/packages/protocol/contracts/governance/test/MockElection.sol new file mode 100644 index 00000000000..47f46d42ca3 --- /dev/null +++ b/packages/protocol/contracts/governance/test/MockElection.sol @@ -0,0 +1,37 @@ +pragma solidity ^0.5.3; + +import "../interfaces/IElection.sol"; + +/** + * @title Holds a list of addresses of validators + */ +contract MockElection is IElection { + + mapping(address => bool) public isIneligible; + mapping(address => bool) public isEligible; + address[] public electedValidators; + + function markGroupIneligible(address account) external { + isIneligible[account] = true; + } + + function markGroupEligible(address account, address, address) external { + isEligible[account] = true; + } + + function getTotalVotes() external view returns (uint256) { + return 0; + } + + function getTotalVotesByAccount(address) external view returns (uint256) { + return 0; + } + + function setElectedValidators(address[] calldata _electedValidators) external { + electedValidators = _electedValidators; + } + + function electValidators() external view returns (address[] memory) { + return electedValidators; + } +} diff --git a/packages/protocol/contracts/governance/test/MockLockedGold.sol b/packages/protocol/contracts/governance/test/MockLockedGold.sol index 8409bceefd4..70e1e898e24 100644 --- a/packages/protocol/contracts/governance/test/MockLockedGold.sol +++ b/packages/protocol/contracts/governance/test/MockLockedGold.sol @@ -1,5 +1,7 @@ pragma solidity ^0.5.3; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; + import "../interfaces/ILockedGold.sol"; @@ -7,107 +9,72 @@ import "../interfaces/ILockedGold.sol"; * @title A mock LockedGold for testing. */ contract MockLockedGold is ILockedGold { - mapping(address => mapping(uint256 => uint256)) public locked; - mapping(address => uint256) public weights; - mapping(address => bool) public frozen; - // Maps a delegating address to an account. - mapping(address => address) public delegations; - // Maps an account address to their voting delegate. - mapping(address => address) public voters; - // Maps an account address to their validating delegate. - mapping(address => address) public validators; - // Maps an account address to their rewards delegate. - mapping(address => address) public rewarders; - uint256 public totalWeight; - - function initialize(address, uint256) external {} - function setCumulativeRewardWeight(uint256) external {} - function setMaxNoticePeriod(uint256) external {} - function redeemRewards() external returns (uint256) {} - function freezeVoting() external {} - function unfreezeVoting() external {} - function newCommitment(uint256) external payable returns (uint256) {} - function notifyCommitment(uint256, uint256) external returns (uint256) {} - function extendCommitment(uint256, uint256) external returns (uint256) {} - function withdrawCommitment(uint256) external returns (uint256) {} - function increaseNoticePeriod(uint256, uint256, uint256) external returns (uint256) {} - function getRewardsLastRedeemed(address) external view returns (uint96) {} - function getNoticePeriods(address) external view returns (uint256[] memory) {} - function getAvailabilityTimes(address) external view returns (uint256[] memory) {} - function delegateRole(DelegateRole, address, uint8, bytes32, bytes32) external {} - - function isVotingFrozen(address account) external view returns (bool) { - return frozen[account]; + + using SafeMath for uint256; + + struct Authorizations { + address validator; + address voter; } - function setWeight(address account, uint256 weight) external { - weights[account] = weight; + mapping(address => uint256) public accountTotalLockedGold; + mapping(address => uint256) public nonvotingAccountBalance; + mapping(address => address) public authorizedValidators; + mapping(address => address) public authorizedBy; + uint256 private totalLockedGold; + + function authorizeValidator(address account, address validator) external { + authorizedValidators[account] = validator; + authorizedBy[validator] = account; } - function setTotalWeight(uint256 weight) external { - totalWeight = weight; + function getAccountFromValidator(address accountOrValidator) external view returns (address) { + if (authorizedBy[accountOrValidator] == address(0)) { + return accountOrValidator; + } else { + return authorizedBy[accountOrValidator]; + } } - function setLockedCommitment(address account, uint256 noticePeriod, uint256 value) external { - locked[account][noticePeriod] = value; + function getAccountFromActiveValidator( + address accountOrValidator + ) + external + view + returns (address) + { + return accountOrValidator; } - function setVotingFrozen(address account) external { - frozen[account] = true; + function getAccountFromActiveVoter(address accountOrVoter) external view returns (address) { + return accountOrVoter; } - function delegateVoting(address account, address delegate) external { - delegations[delegate] = account; - voters[account] = delegate; + function getValidatorFromAccount(address account) external view returns (address) { + address authorizedValidator = authorizedValidators[account]; + return authorizedValidator == address(0) ? account : authorizedValidator; } - function delegateValidating(address account, address delegate) external { - delegations[delegate] = account; - validators[account] = delegate; + function incrementNonvotingAccountBalance(address account, uint256 value) external { + nonvotingAccountBalance[account] = nonvotingAccountBalance[account].add(value); } - function getAccountWeight(address account) external view returns (uint256) { - return weights[account]; + function decrementNonvotingAccountBalance(address account, uint256 value) external { + nonvotingAccountBalance[account] = nonvotingAccountBalance[account].sub(value); } - function getAccountFromDelegateAndRole(address delegate, DelegateRole) - external view returns (address) - { - address a = delegations[delegate]; - if (a != address(0)) { - return a; - } else { - return delegate; - } + function setAccountTotalLockedGold(address account, uint256 value) external { + accountTotalLockedGold[account] = value; } - function getDelegateFromAccountAndRole(address account, DelegateRole role) - external view returns (address) - { - address a; - if (role == DelegateRole.Validating) { - a = validators[account]; - } else if (role == DelegateRole.Voting) { - a = voters[account]; - } else if (role == DelegateRole.Rewards) { - a = rewarders[account]; - } - if (a != address(0)) { - return a; - } else { - return account; - } + function getAccountTotalLockedGold(address account) external view returns (uint256) { + return accountTotalLockedGold[account]; } - function getLockedCommitment( - address account, - uint256 noticePeriod - ) - external - view - returns (uint256, uint256) - { - // Always return 0 for the index. - return (locked[account][noticePeriod], 0); + function setTotalLockedGold(uint256 value) external { + totalLockedGold = value; + } + function getTotalLockedGold() external view returns (uint256) { + return totalLockedGold; } } diff --git a/packages/protocol/contracts/governance/test/MockValidators.sol b/packages/protocol/contracts/governance/test/MockValidators.sol index 4ed212ae6f1..a593e45aecc 100644 --- a/packages/protocol/contracts/governance/test/MockValidators.sol +++ b/packages/protocol/contracts/governance/test/MockValidators.sol @@ -9,7 +9,10 @@ contract MockValidators is IValidators { mapping(address => bool) private _isValidating; mapping(address => bool) private _isVoting; - address[] private validators; + mapping(address => uint256) private numGroupMembers; + mapping(address => uint256) private balanceRequirements; + mapping(address => address[]) private members; + uint256 private numRegisteredValidators; function isValidating(address account) external view returns (bool) { return _isValidating[account]; @@ -19,8 +22,8 @@ contract MockValidators is IValidators { return _isVoting[account]; } - function getValidators() external view returns (address[] memory) { - return validators; + function getGroupNumMembers(address group) public view returns (uint256) { + return members[group].length; } function setValidating(address account) external { @@ -31,7 +34,47 @@ contract MockValidators is IValidators { _isVoting[account] = true; } - function addValidator(address account) external { - validators.push(account); + function setNumRegisteredValidators(uint256 value) external { + numRegisteredValidators = value; + } + + function getNumRegisteredValidators() external view returns (uint256) { + return numRegisteredValidators; + } + + function setMembers(address group, address[] calldata _members) external { + members[group] = _members; + } + + function setAccountBalanceRequirement(address account, uint256 value) external { + balanceRequirements[account] = value; + } + + function getAccountBalanceRequirement(address account) external view returns (uint256) { + return balanceRequirements[account]; + } + + function getTopGroupValidators( + address group, + uint256 n + ) + external + view + returns (address[] memory) + { + require(n <= members[group].length); + address[] memory validators = new address[](n); + for (uint256 i = 0; i < n; i++) { + validators[i] = members[group][i]; + } + return validators; + } + + function getGroupsNumMembers(address[] calldata groups) external view returns (uint256[] memory) { + uint256[] memory numMembers = new uint256[](groups.length); + for (uint256 i = 0; i < groups.length; i++) { + numMembers[i] = getGroupNumMembers(groups[i]); + } + return numMembers; } } diff --git a/packages/protocol/contracts/governance/test/ValidatorsTest.sol b/packages/protocol/contracts/governance/test/ValidatorsTest.sol new file mode 100644 index 00000000000..beefe62389e --- /dev/null +++ b/packages/protocol/contracts/governance/test/ValidatorsTest.sol @@ -0,0 +1,18 @@ +pragma solidity ^0.5.8; + +import "../Validators.sol"; +import "../../common/FixidityLib.sol"; + +/** + * @title A wrapper around Validators that exposes onlyVm functions for testing. + */ +contract ValidatorsTest is Validators { + + function updateValidatorScore(address validator, uint256 uptime) external { + return _updateValidatorScore(validator, uptime); + } + + function distributeEpochPayment(address validator) external { + return _distributeEpochPayment(validator); + } +} diff --git a/packages/protocol/contracts/identity/Attestations.sol b/packages/protocol/contracts/identity/Attestations.sol index c240c3da9ce..5efb5579ca2 100644 --- a/packages/protocol/contracts/identity/Attestations.sol +++ b/packages/protocol/contracts/identity/Attestations.sol @@ -10,9 +10,9 @@ import "../common/interfaces/IERC20Token.sol"; import "../governance/interfaces/IValidators.sol"; import "../common/Initializable.sol"; -import "../governance/UsingLockedGold.sol"; import "../common/UsingRegistry.sol"; import "../common/Signatures.sol"; +import "../common/UsingPrecompiles.sol"; /** @@ -24,10 +24,9 @@ contract Attestations is Initializable, UsingRegistry, ReentrancyGuard, - UsingLockedGold + UsingPrecompiles { - using SafeMath for uint256; using SafeMath for uint128; using SafeMath for uint96; @@ -706,17 +705,6 @@ contract Attestations is return identifiers[identifier].accounts; } - /** - * @notice Returns the current validator set - * TODO: Should be replaced with a precompile - */ - function getValidators() public view returns (address[] memory) { - IValidators validatorContract = IValidators( - registry.getAddressForOrDie(VALIDATORS_REGISTRY_ID) - ); - return validatorContract.getValidators(); - } - /** * @notice Helper function for batchGetAttestationStats to calculate the total number of addresses that have >0 complete attestations for the identifiers @@ -759,7 +747,7 @@ contract Attestations is IRandom random = IRandom(registry.getAddressForOrDie(RANDOM_REGISTRY_ID)); bytes32 seed = random.random(); - address[] memory validators = getValidators(); + uint256 numberValidators = numberValidatorsInCurrentSet(); uint256 currentIndex = 0; address validator; @@ -767,8 +755,9 @@ contract Attestations is while (currentIndex < n) { seed = keccak256(abi.encodePacked(seed)); - validator = validators[uint256(seed) % validators.length]; - issuer = getAccountFromValidator(validator); + validator = validatorAddressFromCurrentSet(uint256(seed) % numberValidators); + + issuer = getLockedGold().getAccountFromValidator(validator); Attestation storage attestations = state.issuedAttestations[issuer]; diff --git a/packages/protocol/contracts/identity/Random.sol b/packages/protocol/contracts/identity/Random.sol index 56badc66807..cdc9e4afca6 100644 --- a/packages/protocol/contracts/identity/Random.sol +++ b/packages/protocol/contracts/identity/Random.sol @@ -1,19 +1,47 @@ pragma solidity ^0.5.3; import "./interfaces/IRandom.sol"; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; +import "../common/Initializable.sol"; /** * @title Provides randomness for verifier selection */ -contract Random is IRandom { +contract Random is IRandom, Ownable, Initializable { + + using SafeMath for uint256; /* Stores most recent commitment per address */ mapping(address => bytes32) public commitments; - bytes32 public _random; + uint256 public randomnessBlockRetentionWindow = 256; + + mapping (uint256 => bytes32) private history; + uint256 private historyFirst; + uint256 private historySize; + + event RandomnessBlockRetentionWindowSet(uint256 value); - function initialize() external { + /** + * @notice Initializes the contract with initial parameters. + * @param _randomnessBlockRetentionWindow Number of old random blocks whose randomness + * values can be queried. + */ + function initialize(uint256 _randomnessBlockRetentionWindow) external initializer { + _transferOwnership(msg.sender); + setRandomnessBlockRetentionWindow(_randomnessBlockRetentionWindow); + } + + /** + * @notice Sets the number of old random blocks whose randomness values can be queried. + * @param value Number of old random blocks whose randomness values can be queried. + */ + function setRandomnessBlockRetentionWindow(uint256 value) public onlyOwner { + require(value > 0, "randomnessBlockRetetionWindow cannot be zero"); + randomnessBlockRetentionWindow = value; + emit RandomnessBlockRetentionWindowSet(value); } /** @@ -31,28 +59,104 @@ contract Random is IRandom { bytes32 newCommitment, address proposer ) external { - require(msg.sender == address(0)); + require(msg.sender == address(0), "only VM can call"); + _revealAndCommit(randomness, newCommitment, proposer); + } + /** + * @notice Implements step of the randomness protocol. + * @param randomness Bytes that will be added to the entropy pool. + * @param newCommitment The hash of randomness that will be revealed in the future. + * @param proposer Address of the block proposer. + */ + function _revealAndCommit( + bytes32 randomness, + bytes32 newCommitment, + address proposer + ) internal { // ensure revealed randomness matches previous commitment if (commitments[proposer] != 0) { - require(randomness != 0); + require(randomness != 0, "randomness cannot be zero if there is a previous commitment"); bytes32 expectedCommitment = computeCommitment(randomness); - require(expectedCommitment == commitments[proposer]); + require( + expectedCommitment == commitments[proposer], + "commitment didn't match the posted randomness" + ); } else { - require(randomness == 0); + require(randomness == 0, "randomness should be zero if there is no previous commitment"); } // add entropy - _random = keccak256(abi.encodePacked(_random, randomness)); + uint256 blockNumber = block.number == 0 ? 0 : block.number.sub(1); + addRandomness(block.number, keccak256(abi.encodePacked(history[blockNumber], randomness))); commitments[proposer] = newCommitment; } + /** + * @notice Add a value to the randomness history. + * @param blockNumber Current block number. + * @param randomness The new randomness added to history. + * @dev The calls to this function should be made so that on the next call, blockNumber will + * be the previous one, incremented by one. + */ + function addRandomness(uint256 blockNumber, bytes32 randomness) internal { + history[blockNumber] = randomness; + if (historySize == 0) { + historyFirst = block.number; + historySize = 1; + } else if (historySize > randomnessBlockRetentionWindow) { + delete history[historyFirst]; + delete history[historyFirst+1]; + historyFirst += 2; + historySize--; + } else if (historySize == randomnessBlockRetentionWindow) { + delete history[historyFirst]; + historyFirst++; + } else /* historySize < randomnessBlockRetentionWindow */ { + historySize++; + } + } + + /** + * @notice Compute the commitment hash for a given randomness value. + * @param randomness The value for which the commitment hash is computed. + * @return Commitment parameter. + */ function computeCommitment(bytes32 randomness) public pure returns (bytes32) { return keccak256(abi.encodePacked(randomness)); } + /** + * @notice Querying the current randomness value. + * @return Returns the current randomness value. + */ function random() external view returns (bytes32) { - return _random; + return _getBlockRandomness(block.number, block.number); + } + + /** + * @notice Get randomness values of previous blocks. + * @param blockNumber The number of block whose randomness value we want to know. + * @return The associated randomness value. + */ + function getBlockRandomness(uint256 blockNumber) external view returns (bytes32) { + return _getBlockRandomness(blockNumber, block.number); + } + + /** + * @notice Get randomness values of previous blocks. + * @param blockNumber The number of block whose randomness value we want to know. + * @param cur Number of the current block. + * @return The associated randomness value. + */ + function _getBlockRandomness(uint256 blockNumber, uint256 cur) internal view returns (bytes32) { + require(blockNumber <= cur, "Cannot query randomness of future blocks"); + require( + blockNumber > cur.sub(historySize) && + (randomnessBlockRetentionWindow >= cur || + blockNumber > cur.sub(randomnessBlockRetentionWindow)), + "Cannot query randomness older than the stored history"); + return history[blockNumber]; } } diff --git a/packages/protocol/contracts/identity/test/MockRandom.sol b/packages/protocol/contracts/identity/test/MockRandom.sol index feb688dec73..6953fdb56bf 100644 --- a/packages/protocol/contracts/identity/test/MockRandom.sol +++ b/packages/protocol/contracts/identity/test/MockRandom.sol @@ -1,23 +1,13 @@ -pragma solidity ^0.5.8; +pragma solidity ^0.5.3; -import "../interfaces/IRandom.sol"; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -/** - * @title Returns a fixed value to test 'random' things - */ -contract MockRandom is IRandom { - bytes32 public _r; +contract MockRandom { - function revealAndCommit( - bytes32 randomness, - bytes32 newCommitment, - address proposer - ) external { - _r = randomness; - } - - function random() external view returns (bytes32) { - return _r; + bytes32 public random; + + function setRandom(bytes32 value) external { + random = value; } } diff --git a/packages/protocol/contracts/identity/test/TestAttestations.sol b/packages/protocol/contracts/identity/test/TestAttestations.sol new file mode 100644 index 00000000000..5d6eb45fd67 --- /dev/null +++ b/packages/protocol/contracts/identity/test/TestAttestations.sol @@ -0,0 +1,25 @@ +pragma solidity ^0.5.3; + +import "../Attestations.sol"; + + +/* + * We need a test contract that behaves like the actual Attestations contract, + * but mocks the implementations of the validator set getters. Otherwise we + * couldn't test `request` with the current ganache local testnet. + */ +contract TestAttestations is Attestations { + address[] private __testValidators; + + function __setValidators(address[] memory validators) public { + __testValidators = validators; + } + + function numberValidatorsInCurrentSet() public view returns (uint256) { + return __testValidators.length; + } + + function validatorAddressFromCurrentSet(uint256 index) public view returns (address) { + return __testValidators[index]; + } +} diff --git a/packages/protocol/contracts/identity/test/TestRandom.sol b/packages/protocol/contracts/identity/test/TestRandom.sol new file mode 100644 index 00000000000..8930cc9e145 --- /dev/null +++ b/packages/protocol/contracts/identity/test/TestRandom.sol @@ -0,0 +1,13 @@ +pragma solidity ^0.5.3; + +import "../Random.sol"; + +contract TestRandom is Random { + function addTestRandomness(uint256 blockNumber, bytes32 randomness) external { + addRandomness(blockNumber, randomness); + } + function getTestRandomness(uint256 blockNumber, uint256 cur) external view returns (bytes32) { + return _getBlockRandomness(blockNumber, cur); + } +} + diff --git a/packages/protocol/contracts/stability/Exchange.sol b/packages/protocol/contracts/stability/Exchange.sol index 12ce4d5020d..5e0e457422e 100644 --- a/packages/protocol/contracts/stability/Exchange.sol +++ b/packages/protocol/contracts/stability/Exchange.sol @@ -11,7 +11,6 @@ import "../common/FractionUtil.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/UsingRegistry.sol"; -import "../common/interfaces/IERC20Token.sol"; /** @@ -138,7 +137,7 @@ contract Exchange is IExchange, Initializable, Ownable, UsingRegistry, Reentranc goldBucket = goldBucket.add(sellAmount); stableBucket = stableBucket.sub(buyAmount); require( - gold().transferFrom(msg.sender, address(reserve), sellAmount), + getGoldToken().transferFrom(msg.sender, address(reserve), sellAmount), "Transfer of sell token failed" ); require(IStableToken(stable).mint(msg.sender, buyAmount), "Mint of stable token failed"); @@ -332,7 +331,9 @@ contract Exchange is IExchange, Initializable, Ownable, UsingRegistry, Reentranc } function getUpdatedGoldBucket() private view returns (uint256) { - uint256 reserveGoldBalance = gold().balanceOf(registry.getAddressForOrDie(RESERVE_REGISTRY_ID)); + uint256 reserveGoldBalance = getGoldToken().balanceOf( + registry.getAddressForOrDie(RESERVE_REGISTRY_ID) + ); return reserveFraction.multiply(FixidityLib.newFixed(reserveGoldBalance)).fromFixed(); } @@ -388,8 +389,4 @@ contract Exchange is IExchange, Initializable, Ownable, UsingRegistry, Reentranc ISortedOracles(registry.getAddressForOrDie(SORTED_ORACLES_REGISTRY_ID)).medianRate(stable); return FractionUtil.Fraction(rateNumerator, rateDenominator); } - - function gold() private view returns (IERC20Token) { - return IERC20Token(registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID)); - } } diff --git a/packages/protocol/contracts/stability/Reserve.sol b/packages/protocol/contracts/stability/Reserve.sol index 315030f4af5..707e0dfbb02 100644 --- a/packages/protocol/contracts/stability/Reserve.sol +++ b/packages/protocol/contracts/stability/Reserve.sol @@ -10,7 +10,6 @@ import "./interfaces/IStableToken.sol"; import "../common/Initializable.sol"; import "../common/UsingRegistry.sol"; -import "../common/interfaces/IERC20Token.sol"; /** @@ -144,8 +143,7 @@ contract Reserve is IReserve, Ownable, Initializable, UsingRegistry, ReentrancyG returns (bool) { require(isSpender[msg.sender], "sender not allowed to transfer Reserve funds"); - IERC20Token goldToken = IERC20Token(registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID)); - require(goldToken.transfer(to, value), "transfer of gold token failed"); + require(getGoldToken().transfer(to, value), "transfer of gold token failed"); return true; } diff --git a/packages/protocol/contracts/stability/SortedOracles.sol b/packages/protocol/contracts/stability/SortedOracles.sol index d62ece7160d..adc6c729eee 100644 --- a/packages/protocol/contracts/stability/SortedOracles.sol +++ b/packages/protocol/contracts/stability/SortedOracles.sol @@ -156,23 +156,28 @@ contract SortedOracles is ISortedOracles, Ownable, Initializable { uint256 value = numerator.mul(DENOMINATOR).div(denominator); if (rates[token].contains(msg.sender)) { rates[token].update(msg.sender, value, lesserKey, greaterKey); - timestamps[token].update( - msg.sender, - // solhint-disable-next-line not-rely-on-time - now, - timestamps[token].getHead(), - address(0) - ); + + // Rather than update the timestamp, we remove it and re-add it at the + // head of the list later. The reason for this is that we need to handle + // a few different cases: + // 1. This oracle is the only one to report so far. lesserKey = address(0) + // 2. Other oracles have reported since this one's last report. lesserKey = getHead() + // 3. Other oracles have reported, but the most recent is this one. + // lesserKey = key immediately after getHead() + // + // However, if we just remove this timestamp, timestamps[token].getHead() + // does the right thing in all cases. + timestamps[token].remove(msg.sender); } else { rates[token].insert(msg.sender, value, lesserKey, greaterKey); - timestamps[token].insert( - msg.sender, - // solhint-disable-next-line not-rely-on-time - now, - timestamps[token].getHead(), - address(0) - ); } + timestamps[token].insert( + msg.sender, + // solhint-disable-next-line not-rely-on-time + now, + timestamps[token].getHead(), + address(0) + ); emit OracleReported(token, msg.sender, now, value, DENOMINATOR); uint256 newMedian = rates[token].getMedianValue(); if (newMedian != originalMedian) { diff --git a/packages/protocol/contracts/stability/StableToken.sol b/packages/protocol/contracts/stability/StableToken.sol index 2a2ce057639..1146d534d5a 100644 --- a/packages/protocol/contracts/stability/StableToken.sol +++ b/packages/protocol/contracts/stability/StableToken.sol @@ -10,6 +10,7 @@ import "../common/interfaces/ICeloToken.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/UsingRegistry.sol"; +import "../common/UsingPrecompiles.sol"; /** @@ -17,12 +18,10 @@ import "../common/UsingRegistry.sol"; */ // solhint-disable-next-line max-line-length contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, - Initializable, UsingRegistry { + Initializable, UsingRegistry, UsingPrecompiles { using FixidityLib for FixidityLib.Fraction; using SafeMath for uint256; - event MinterSet(address indexed _minter); - event InflationFactorUpdated( uint256 factor, uint256 lastUpdated @@ -44,7 +43,6 @@ contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, string comment ); - address public minter; string internal name_; string internal symbol_; uint8 internal decimals_; @@ -71,14 +69,6 @@ contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, InflationState inflationState; - /** - * @notice Throws if called by any account other than the minter. - */ - modifier onlyMinter() { - require(msg.sender == minter, "sender was not minter"); - _; - } - /** * Only VM would be able to set the caller address to 0x0 unless someone * really has the private key for 0x0 @@ -123,12 +113,15 @@ contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, uint8 _decimals, address registryAddress, uint256 inflationRate, - uint256 inflationFactorUpdatePeriod + uint256 inflationFactorUpdatePeriod, + address[] calldata initialBalanceAddresses, + uint256[] calldata initialBalanceValues ) external initializer { require(inflationRate != 0, "Must provide a non-zero inflation rate."); + _transferOwnership(msg.sender); totalSupply_ = 0; name_ = _name; @@ -141,19 +134,13 @@ contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, // solhint-disable-next-line not-rely-on-time inflationState.factorLastUpdated = now; + require(initialBalanceAddresses.length == initialBalanceValues.length); + for (uint256 i = 0; i < initialBalanceAddresses.length; i = i.add(1)) { + require(_mint(initialBalanceAddresses[i], initialBalanceValues[i])); + } setRegistry(registryAddress); } - // Should this be tied to the registry? - /** - * @notice Updates 'minter'. - * @param _minter An address with special permissions to modify its balance - */ - function setMinter(address _minter) external onlyOwner { - minter = _minter; - emit MinterSet(minter); - } - /** * @notice Updates Inflation Parameters. * @param rate new rate. @@ -250,7 +237,27 @@ contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, uint256 value ) external - onlyMinter + updateInflationFactor + returns (bool) + { + // Only the Exchange and Validators contracts are authorized to mint. + require( + msg.sender == registry.getAddressFor(EXCHANGE_REGISTRY_ID) || + msg.sender == registry.getAddressFor(VALIDATORS_REGISTRY_ID) + ); + return _mint(to, value); + } + + /** + * @notice Mints new StableToken and gives it to 'to'. + * @param to The account for which to mint tokens. + * @param value The amount of StableToken to mint. + */ + function _mint( + address to, + uint256 value + ) + private updateInflationFactor returns (bool) { @@ -283,10 +290,17 @@ contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, } /** - * @notice Burns StableToken from the balance of 'minter'. + * @notice Burns StableToken from the balance of msg.sender. * @param value The amount of StableToken to burn. */ - function burn(uint256 value) external onlyMinter updateInflationFactor returns (bool) { + function burn( + uint256 value + ) + external + onlyRegisteredContract(EXCHANGE_REGISTRY_ID) + updateInflationFactor + returns (bool) + { uint256 units = _valueToUnits(inflationState.factor, value); require(units <= balances[msg.sender], "value exceeded balance of sender"); totalSupply_ = totalSupply_.sub(units); @@ -492,67 +506,6 @@ contract StableToken is IStableToken, IERC20Token, ICeloToken, Ownable, /* solhint-enable not-rely-on-time */ } - /** - * @notice calculate a * b^x for fractions a, b to `decimals` precision - * @param aNumerator Numerator of first fraction - * @param aDenominator Denominator of first fraction - * @param bNumerator Numerator of exponentiated fraction - * @param bDenominator Denominator of exponentiated fraction - * @param exponent exponent to raise b to - * @param _decimals precision - * @return numerator/denominator of the computed quantity (not reduced). - */ - function fractionMulExp( - uint256 aNumerator, - uint256 aDenominator, - uint256 bNumerator, - uint256 bDenominator, - uint256 exponent, - uint256 _decimals - ) - public - view - returns(uint256, uint256) - { - require(aDenominator != 0 && bDenominator != 0); - uint256 returnNumerator; - uint256 returnDenominator; - // solhint-disable-next-line no-inline-assembly - assembly { - let newCallDataPosition := mload(0x40) - mstore(0x40, add(newCallDataPosition, calldatasize)) - mstore(newCallDataPosition, aNumerator) - mstore(add(newCallDataPosition, 32), aDenominator) - mstore(add(newCallDataPosition, 64), bNumerator) - mstore(add(newCallDataPosition, 96), bDenominator) - mstore(add(newCallDataPosition, 128), exponent) - mstore(add(newCallDataPosition, 160), _decimals) - let delegatecallSuccess := staticcall( - 1050, // estimated gas cost for this function - 0xfc, - newCallDataPosition, - 0xc4, // input size, 6 * 32 = 192 bytes - 0, - 0 - ) - - let returnDataSize := returndatasize - let returnDataPosition := mload(0x40) - mstore(0x40, add(returnDataPosition, returnDataSize)) - returndatacopy(returnDataPosition, 0, returnDataSize) - - switch delegatecallSuccess - case 0 { - revert(returnDataPosition, returnDataSize) - } - default { - returnNumerator := mload(returnDataPosition) - returnDenominator := mload(add(returnDataPosition, 32)) - } - } - return (returnNumerator, returnDenominator); - } - /** * @notice Transfers `value` from `msg.sender` to `to` * @param to The address to transfer to. diff --git a/packages/protocol/contracts/stability/interfaces/IStableToken.sol b/packages/protocol/contracts/stability/interfaces/IStableToken.sol index 380be53ab2d..87edfc419aa 100644 --- a/packages/protocol/contracts/stability/interfaces/IStableToken.sol +++ b/packages/protocol/contracts/stability/interfaces/IStableToken.sol @@ -6,18 +6,6 @@ pragma solidity ^0.5.3; * absence of interface inheritance is intended as a companion to IERC20.sol and ICeloToken.sol. */ interface IStableToken { - - function initialize( - string calldata, - string calldata, - uint8, - address, - uint256, - uint256 - ) external; - - function setMinter(address) external; - function mint(address, uint256) external returns (bool); function burn(uint256) external returns (bool); function debitFrom(address, uint256) external; diff --git a/packages/protocol/contracts/stability/test/MockStableToken.sol b/packages/protocol/contracts/stability/test/MockStableToken.sol index b54b538f1c7..e0e1f1f0612 100644 --- a/packages/protocol/contracts/stability/test/MockStableToken.sol +++ b/packages/protocol/contracts/stability/test/MockStableToken.sol @@ -11,6 +11,7 @@ contract MockStableToken { bool public _needsRebase; uint256 public _totalSupply; uint256 public _targetTotalSupply; + mapping (address => uint256) public balanceOf; function setNeedsRebase() external { _needsRebase = true; @@ -24,7 +25,8 @@ contract MockStableToken { _targetTotalSupply = value; } - function mint(address, uint256) external pure returns (bool) { + function mint(address to, uint256 value) external returns (bool) { + balanceOf[to] = balanceOf[to] + value; return true; } diff --git a/packages/protocol/lib/registry-utils.ts b/packages/protocol/lib/registry-utils.ts index 5a3a41cba48..82d3f155d0c 100644 --- a/packages/protocol/lib/registry-utils.ts +++ b/packages/protocol/lib/registry-utils.ts @@ -1,6 +1,7 @@ export enum CeloContractName { Attestations = 'Attestations', BlockchainParameters = 'BlockchainParameters', + Election = 'Election', Escrow = 'Escrow', Exchange = 'Exchange', GasCurrencyWhitelist = 'GasCurrencyWhitelist', @@ -26,6 +27,7 @@ export const usesRegistry = [ export const hasEntryInRegistry: string[] = [ CeloContractName.Attestations, CeloContractName.BlockchainParameters, + CeloContractName.Election, CeloContractName.Escrow, CeloContractName.Exchange, CeloContractName.GoldToken, diff --git a/packages/protocol/lib/test-utils.ts b/packages/protocol/lib/test-utils.ts index ebc328f4f0f..2e9ff68d5d5 100644 --- a/packages/protocol/lib/test-utils.ts +++ b/packages/protocol/lib/test-utils.ts @@ -5,11 +5,9 @@ import * as chaiSubset from 'chai-subset' import { spawn } from 'child_process' import { keccak256 } from 'ethereumjs-util' import { - ExchangeInstance, ProxyInstance, RegistryInstance, ReserveInstance, - StableTokenInstance, UsingRegistryInstance, } from 'types' const soliditySha3 = new (require('web3'))().utils.soliditySha3 @@ -85,6 +83,12 @@ export async function timeTravel(seconds: number, web3: Web3) { await jsonRpc(web3, 'evm_mine', []) } +export async function mineBlocks(blocks: number, web3: Web3) { + for (let i = 0; i < blocks; i++) { + await jsonRpc(web3, 'evm_mine', []) + } +} + export async function assertBalance(address: string, balance: BigNumber) { const block = await web3.eth.getBlock('latest') const web3balance = new BigNumber(await web3.eth.getBalance(address)) @@ -178,16 +182,6 @@ export const assertContractsOwnedByMultiSig = async (getContract: any) => { } } -export const assertStableTokenMinter = async (getContract: any) => { - const stableToken: StableTokenInstance = await getContract('StableToken', 'proxiedContract') - const exchange: ExchangeInstance = await getContract('Exchange', 'proxiedContract') - assert.equal( - await stableToken.minter(), - exchange.address, - 'StableToken minter not set to Exchange' - ) -} - export const assertFloatEquality = ( a: BigNumber, b: BigNumber, @@ -245,6 +239,11 @@ export function assertEqualBN( ) } +export function assertEqualBNArray(value: number[] | BN[] | BigNumber[], expected: number[] | BN[] | BigNumber[], msg?: string) { + assert.equal(value.length, expected.length, msg) + value.forEach((x, i) => assertEqualBN(x, expected[i])) +} + export function assertGteBN( value: number | BN | BigNumber, expected: number | BN | BigNumber, diff --git a/packages/protocol/migrations/02_registry.ts b/packages/protocol/migrations/02_registry.ts index 5d66d89e237..a03fa089ae9 100644 --- a/packages/protocol/migrations/02_registry.ts +++ b/packages/protocol/migrations/02_registry.ts @@ -10,7 +10,7 @@ const ContractProxy = artifacts.require(name + 'Proxy') module.exports = (deployer: any, _networkName: string, _accounts: string[]) => { // tslint:disable-next-line: no-console - console.log('Deploying Registry') + console.info('Deploying Registry') deployer.deploy(ContractProxy) deployer.deploy(Contract) deployer.then(async () => { diff --git a/packages/protocol/migrations/04_goldtoken.ts b/packages/protocol/migrations/04_goldtoken.ts index 13b7ebb9a5a..951fd5a4953 100644 --- a/packages/protocol/migrations/04_goldtoken.ts +++ b/packages/protocol/migrations/04_goldtoken.ts @@ -16,7 +16,7 @@ module.exports = deploymentForCoreContract( CeloContractName.GoldToken, initializeArgs, async (goldToken: GoldTokenInstance) => { - console.log('Whitelisting GoldToken as a gas currency') + console.info('Whitelisting GoldToken as a gas currency') const gasCurrencyWhitelist: GasCurrencyWhitelistInstance = await getDeployedProxiedContract< GasCurrencyWhitelistInstance >('GasCurrencyWhitelist', artifacts) diff --git a/packages/protocol/migrations/07_reserve.ts b/packages/protocol/migrations/07_reserve.ts index 45ab18b2c59..b13ebb75c66 100644 --- a/packages/protocol/migrations/07_reserve.ts +++ b/packages/protocol/migrations/07_reserve.ts @@ -25,7 +25,7 @@ module.exports = deploymentForCoreContract( initializeArgs, async (reserve: ReserveInstance, web3: Web3, networkName: string) => { const network: any = truffle.networks[networkName] - console.log('Sending the reserve an initial gold balance') + console.info('Sending the reserve an initial gold balance') await web3.eth.sendTransaction({ from: network.from, to: reserve.address, diff --git a/packages/protocol/migrations/08_stabletoken.ts b/packages/protocol/migrations/08_stabletoken.ts index 5bc02ece37d..57a17fbc792 100644 --- a/packages/protocol/migrations/08_stabletoken.ts +++ b/packages/protocol/migrations/08_stabletoken.ts @@ -3,7 +3,6 @@ import Web3 = require('web3') import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { - convertToContractDecimalsBN, deploymentForCoreContract, getDeployedProxiedContract, } from '@celo/protocol/lib/web3-utils' @@ -21,7 +20,6 @@ const NULL_ADDRESS = '0x0000000000000000000000000000000000000000' const initializeArgs = async (): Promise => { const rate = toFixed(config.stableToken.inflationRate) - return [ config.stableToken.tokenName, config.stableToken.tokenSymbol, @@ -29,6 +27,8 @@ const initializeArgs = async (): Promise => { config.registry.predeployedProxyAddress, rate.toString(), config.stableToken.inflationPeriod, + config.stableToken.initialBalances.addresses, + config.stableToken.initialBalances.values, ] } @@ -39,27 +39,23 @@ module.exports = deploymentForCoreContract( initializeArgs, async (stableToken: StableTokenInstance, _web3: Web3, networkName: string) => { const minerAddress: string = truffle.networks[networkName].from - const minerStartBalance = await convertToContractDecimalsBN( - config.stableToken.minerDollarBalance.toString(), - stableToken - ) - console.log( - `Minting ${minerAddress} ${config.stableToken.minerDollarBalance.toString()} StableToken` - ) - await stableToken.setMinter(minerAddress) - - const initialBalance = web3.utils.toBN(minerStartBalance) - await stableToken.mint(minerAddress, initialBalance) - for (const address of config.stableToken.initialAccounts) { - await stableToken.mint(address, initialBalance) - } - console.log('Setting GoldToken/USD exchange rate') const sortedOracles: SortedOraclesInstance = await getDeployedProxiedContract< SortedOraclesInstance >('SortedOracles', artifacts) - await sortedOracles.addOracle(stableToken.address, minerAddress) + for (const oracle of config.stableToken.oracles) { + console.info(`Adding ${oracle} as an Oracle for StableToken`) + await sortedOracles.addOracle(stableToken.address, oracle) + } + + console.info('Setting GoldToken/USD exchange rate') + // We need to seed the exchange rate, and that must be done with an account + // that's accessible to the migrations. It's in an if statement in case this + // account happened to be included in config.stableToken.oracles + if (!(await sortedOracles.isOracle(stableToken.address, minerAddress))) { + await sortedOracles.addOracle(stableToken.address, minerAddress) + } await sortedOracles.report( stableToken.address, config.stableToken.goldPrice, @@ -72,10 +68,10 @@ module.exports = deploymentForCoreContract( 'Reserve', artifacts ) - console.log('Adding StableToken to Reserve') + console.info('Adding StableToken to Reserve') await reserve.addToken(stableToken.address) - console.log('Whitelisting StableToken as a gas currency') + console.info('Whitelisting StableToken as a gas currency') const gasCurrencyWhitelist: GasCurrencyWhitelistInstance = await getDeployedProxiedContract< GasCurrencyWhitelistInstance >('GasCurrencyWhitelist', artifacts) diff --git a/packages/protocol/migrations/09_exchange.ts b/packages/protocol/migrations/09_exchange.ts index b7d33e74fbe..07d78d5a295 100644 --- a/packages/protocol/migrations/09_exchange.ts +++ b/packages/protocol/migrations/09_exchange.ts @@ -30,13 +30,6 @@ module.exports = deploymentForCoreContract( CeloContractName.Exchange, initializeArgs, async (exchange: ExchangeInstance) => { - console.log('Setting Exchange as StableToken minter') - const stableToken: StableTokenInstance = await getDeployedProxiedContract( - 'StableToken', - artifacts - ) - await stableToken.setMinter(exchange.address) - console.log('Setting Exchange as a Reserve spender') const reserve: ReserveInstance = await getDeployedProxiedContract( 'Reserve', diff --git a/packages/protocol/migrations/10_lockedgold.ts b/packages/protocol/migrations/10_lockedgold.ts index 48c601b92bd..5eb603e7fb0 100644 --- a/packages/protocol/migrations/10_lockedgold.ts +++ b/packages/protocol/migrations/10_lockedgold.ts @@ -7,5 +7,5 @@ module.exports = deploymentForCoreContract( web3, artifacts, CeloContractName.LockedGold, - async () => [config.registry.predeployedProxyAddress, config.lockedGold.maxNoticePeriod] + async () => [config.registry.predeployedProxyAddress, config.lockedGold.unlockingPeriod] ) diff --git a/packages/protocol/migrations/11_validators.ts b/packages/protocol/migrations/11_validators.ts index 868941128f2..5f24860c230 100644 --- a/packages/protocol/migrations/11_validators.ts +++ b/packages/protocol/migrations/11_validators.ts @@ -1,17 +1,21 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' import { config } from '@celo/protocol/migrationsConfig' +import { toFixed } from '@celo/utils/lib/fixidity' import { ValidatorsInstance } from 'types' const initializeArgs = async (): Promise => { return [ config.registry.predeployedProxyAddress, - config.validators.minElectableValidators, - config.validators.maxElectableValidators, - config.validators.minLockedGoldValue, - config.validators.minLockedGoldNoticePeriod, + config.validators.registrationRequirements.group, + config.validators.registrationRequirements.validator, + config.validators.deregistrationLockups.group, + config.validators.deregistrationLockups.validator, + config.validators.validatorScoreParameters.exponent, + toFixed(config.validators.validatorScoreParameters.adjustmentSpeed).toFixed(), + config.validators.validatorEpochPayment, + config.validators.membershipHistoryLength, config.validators.maxGroupSize, - config.validators.electionThreshold, ] } diff --git a/packages/protocol/migrations/12_election.ts b/packages/protocol/migrations/12_election.ts new file mode 100644 index 00000000000..94bc55add3a --- /dev/null +++ b/packages/protocol/migrations/12_election.ts @@ -0,0 +1,22 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' +import { config } from '@celo/protocol/migrationsConfig' +import { toFixed } from '@celo/utils/lib/fixidity' +import { ElectionInstance } from 'types' + +const initializeArgs = async (): Promise => { + return [ + config.registry.predeployedProxyAddress, + config.election.minElectableValidators, + config.election.maxElectableValidators, + config.election.maxVotesPerAccount, + toFixed(config.election.electabilityThreshold).toFixed(), + ] +} + +module.exports = deploymentForCoreContract( + web3, + artifacts, + CeloContractName.Election, + initializeArgs +) diff --git a/packages/protocol/migrations/12_random.ts b/packages/protocol/migrations/12_random.ts deleted file mode 100644 index a1ef4117110..00000000000 --- a/packages/protocol/migrations/12_random.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { CeloContractName } from '@celo/protocol/lib/registry-utils' -import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' -import { RandomInstance } from 'types' - -module.exports = deploymentForCoreContract(web3, artifacts, CeloContractName.Random) diff --git a/packages/protocol/migrations/13_random.ts b/packages/protocol/migrations/13_random.ts new file mode 100644 index 00000000000..0069a349c9a --- /dev/null +++ b/packages/protocol/migrations/13_random.ts @@ -0,0 +1,15 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' +import { config } from '@celo/protocol/migrationsConfig' +import { RandomInstance } from 'types' + +const initializeArgs = async (_: string): Promise => { + return [config.random.randomnessBlockRetentionWindow] +} + +module.exports = deploymentForCoreContract( + web3, + artifacts, + CeloContractName.Random, + initializeArgs +) diff --git a/packages/protocol/migrations/13_attestations.ts b/packages/protocol/migrations/14_attestations.ts similarity index 100% rename from packages/protocol/migrations/13_attestations.ts rename to packages/protocol/migrations/14_attestations.ts diff --git a/packages/protocol/migrations/14_escrow.ts b/packages/protocol/migrations/15_escrow.ts similarity index 100% rename from packages/protocol/migrations/14_escrow.ts rename to packages/protocol/migrations/15_escrow.ts diff --git a/packages/protocol/migrations/16_governance.ts b/packages/protocol/migrations/16_governance.ts index 450d40a7438..c9ce18f18e5 100644 --- a/packages/protocol/migrations/16_governance.ts +++ b/packages/protocol/migrations/16_governance.ts @@ -1,7 +1,5 @@ /* tslint:disable:no-console */ -import { GovernanceInstance, ReserveInstance } from 'types' - import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { deploymentForCoreContract, @@ -11,6 +9,7 @@ import { } from '@celo/protocol/lib/web3-utils' import { config } from '@celo/protocol/migrationsConfig' import { toFixed } from '@celo/utils/lib/fixidity' +import { GovernanceInstance, ReserveInstance } from 'types' const initializeArgs = async (networkName: string): Promise => { const approver = require('@celo/protocol/truffle-config.js').networks[networkName].from @@ -38,14 +37,14 @@ module.exports = deploymentForCoreContract( CeloContractName.Governance, initializeArgs, async (governance: GovernanceInstance) => { - console.log('Setting Governance as a Reserve spender') + console.info('Setting Governance as a Reserve spender') const reserve: ReserveInstance = await getDeployedProxiedContract( 'Reserve', artifacts ) await reserve.addSpender(governance.address) - const proxyOwnedByGovernance = ['GoldToken', 'Random'] + const proxyOwnedByGovernance = ['GoldToken'] await Promise.all( proxyOwnedByGovernance.map((contractName) => transferOwnershipOfProxy(contractName, governance.address, artifacts) @@ -55,12 +54,14 @@ module.exports = deploymentForCoreContract( const proxyAndImplementationOwnedByGovernance = [ 'Attestations', 'BlockchainParameters', + 'Election', 'Escrow', 'Exchange', 'GasCurrencyWhitelist', 'GasPriceMinimum', 'Governance', 'LockedGold', + 'Random', 'Registry', 'Reserve', 'SortedOracles', diff --git a/packages/protocol/migrations/17_elect_validators.ts b/packages/protocol/migrations/17_elect_validators.ts index 46da8d13d2e..1aeec5aeb23 100644 --- a/packages/protocol/migrations/17_elect_validators.ts +++ b/packages/protocol/migrations/17_elect_validators.ts @@ -9,9 +9,10 @@ import { } from '@celo/protocol/lib/web3-utils' import { config } from '@celo/protocol/migrationsConfig' import { blsPrivateKeyToProcessedPrivateKey } from '@celo/utils/lib/bls' +import { toFixed } from '@celo/utils/lib/fixidity' import { BigNumber } from 'bignumber.js' import * as bls12377js from 'bls12377js' -import { LockedGoldInstance, ValidatorsInstance } from 'types' +import { ElectionInstance, LockedGoldInstance, ValidatorsInstance } from 'types' const Web3 = require('web3') @@ -19,7 +20,7 @@ function serializeKeystore(keystore: any) { return Buffer.from(JSON.stringify(keystore)).toString('base64') } -async function makeMinimumDeposit(lockedGold: LockedGoldInstance, privateKey: string) { +async function lockGold(lockedGold: LockedGoldInstance, value: BigNumber, privateKey: string) { // @ts-ignore const createAccountTx = lockedGold.contract.methods.createAccount() await sendTransactionWithPrivateKey(web3, createAccountTx, privateKey, { @@ -27,13 +28,11 @@ async function makeMinimumDeposit(lockedGold: LockedGoldInstance, privateKey: st }) // @ts-ignore - const bondTx = lockedGold.contract.methods.newCommitment( - config.validators.minLockedGoldNoticePeriod - ) + const lockTx = lockedGold.contract.methods.lock() - await sendTransactionWithPrivateKey(web3, bondTx, privateKey, { + await sendTransactionWithPrivateKey(web3, lockTx, privateKey, { to: lockedGold.address, - value: config.validators.minLockedGoldValue, + value, }) } @@ -57,17 +56,15 @@ async function registerValidatorGroup( await web3.eth.sendTransaction({ from: generateAccountAddressFromPrivateKey(privateKey.slice(0)), to: account.address, - value: config.validators.minLockedGoldValue * 2, // Add a premium to cover tx fees + value: config.validators.registrationRequirements.group * 2, // Add a premium to cover tx fees }) - await makeMinimumDeposit(lockedGold, account.privateKey) + await lockGold(lockedGold, config.validators.registrationRequirements.group, account.privateKey) // @ts-ignore const tx = validators.contract.methods.registerValidatorGroup( - encodedKey, - config.validators.groupName, - config.validators.groupUrl, - [config.validators.minLockedGoldNoticePeriod] + `${config.validators.groupName} ${encodedKey}`, + toFixed(config.validators.commission).toString() ) await sendTransactionWithPrivateKey(web3, tx, account.privateKey, { @@ -95,16 +92,14 @@ async function registerValidator( const blsPoP = bls12377js.BLS.signPoP(blsValidatorPrivateKeyBytes).toString('hex') const publicKeysData = publicKey + blsPublicKey + blsPoP - await makeMinimumDeposit(lockedGold, validatorPrivateKey) + await lockGold( + lockedGold, + config.validators.registrationRequirements.validator, + validatorPrivateKey + ) // @ts-ignore - const registerTx = validators.contract.methods.registerValidator( - address, - address, - config.validators.groupUrl, - add0x(publicKeysData), - [config.validators.minLockedGoldNoticePeriod] - ) + const registerTx = validators.contract.methods.registerValidator(address, add0x(publicKeysData)) await sendTransactionWithPrivateKey(web3, registerTx, validatorPrivateKey, { to: validators.address, @@ -131,15 +126,20 @@ module.exports = async (_deployer: any) => { artifacts ) + const election: ElectionInstance = await getDeployedProxiedContract( + 'Election', + artifacts + ) + const valKeys: string[] = config.validators.validatorKeys if (valKeys.length === 0) { - console.log(' No validators to register') + console.info(' No validators to register') return } if (valKeys.length < config.validators.minElectableValidators) { - console.log( + console.info( ` Warning: Have ${valKeys.length} Validator keys but require a minimum of ${ config.validators.minElectableValidators } Validators in order for a new validator set to be elected.` @@ -156,23 +156,31 @@ module.exports = async (_deployer: any) => { ) console.info(' Adding Validators to Validator Group ...') - for (const key of valKeys) { + for (let i = 0; i < valKeys.length; i++) { + const key = valKeys[i] const address = generateAccountAddressFromPrivateKey(key.slice(2)) - // @ts-ignore - const addTx = validators.contract.methods.addMember(address) - await sendTransactionWithPrivateKey(web3, addTx, account.privateKey, { - to: validators.address, - }) + if (i === 0) { + // @ts-ignore + const addTx = validators.contract.methods.addFirstMember(address, NULL_ADDRESS, NULL_ADDRESS) + await sendTransactionWithPrivateKey(web3, addTx, account.privateKey, { + to: validators.address, + }) + } else { + // @ts-ignore + const addTx = validators.contract.methods.addMember(address) + await sendTransactionWithPrivateKey(web3, addTx, account.privateKey, { + to: validators.address, + }) + } } console.info(' Voting for Validator Group ...') // Make another deposit so our vote has more weight. const minLockedGoldVotePerValidator = 10000 - await lockedGold.newCommitment(0, { - // @ts-ignore - value: new BigNumber(valKeys.length) - .times(minLockedGoldVotePerValidator) - .times(config.validators.minLockedGoldValue), - }) - await validators.vote(account.address, NULL_ADDRESS, NULL_ADDRESS) + const value = new BigNumber(valKeys.length) + .times(minLockedGoldVotePerValidator) + .times(web3.utils.toWei('1')) + // @ts-ignore + await lockedGold.lock({ value }) + await election.vote(account.address, value, NULL_ADDRESS, NULL_ADDRESS) } diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index 0d418ddc240..c5747fa40b3 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -11,11 +11,18 @@ const DefaultConfig = { attestationExpirySeconds: 60 * 60, // 1 hour, attestationRequestFeeInDollars: 0.05, }, - lockedGold: { - maxNoticePeriod: 60 * 60 * 24 * 365 * 3, // 3 years + blockchainParameters: { + minimumClientVersion: { + major: 1, + minor: 8, + patch: 23, + }, }, - oracles: { - reportExpiry: 60 * 60, // 1 hour + election: { + minElectableValidators: '22', + maxElectableValidators: '100', + maxVotesPerAccount: 3, + electabilityThreshold: 1 / 100, }, exchange: { spread: 5 / 1000, @@ -23,6 +30,12 @@ const DefaultConfig = { updateFrequency: 3600, minimumReports: 1, }, + gasPriceMinimum: { + initialMinimum: 10000, + targetDensity: 1 / 2, + adjustmentSpeed: 1 / 2, + proposerFraction: 1 / 2, + }, governance: { approvalStageDuration: 15 * 60, // 15 minutes concurrentProposals: 10, @@ -36,11 +49,14 @@ const DefaultConfig = { participationBaselineUpdateFactor: 1 / 5, participationBaselineQuorumFactor: 1, }, - gasPriceMinimum: { - initialMinimum: 10000, - targetDensity: 1 / 2, - adjustmentSpeed: 1 / 2, - proposerFraction: 1 / 2, + lockedGold: { + unlockingPeriod: 60 * 60 * 24 * 3, // 3 days + }, + oracles: { + reportExpiry: 60 * 60, // 1 hour + }, + random: { + randomnessBlockRetentionWindow: 256, }, registry: { predeployedProxyAddress: '0x000000000000000000000000000000000000ce10', @@ -52,33 +68,38 @@ const DefaultConfig = { stableToken: { decimals: 18, goldPrice: 10, - minerDollarBalance: 60000, tokenName: 'Celo Dollar', tokenSymbol: 'cUSD', // 52nd root of 1.005, equivalent to 0.5% annual inflation inflationRate: 1.00009591886, inflationPeriod: 7 * 24 * 60 * 60, // 1 week - initialAccounts: [], + initialBalances: { + addresses: [], + values: [], + }, + oracles: [], }, validators: { - minElectableValidators: '10', - maxElectableValidators: '100', - minLockedGoldValue: '1000000000000000000', // 1 gold - minLockedGoldNoticePeriod: 60 * 24 * 60 * 60, // 60 days - electionThreshold: '0', // no threshold + registrationRequirements: { + group: '1000000000000000000', // 1 gold + validator: '1000000000000000000', // 1 gold + }, + deregistrationLockups: { + group: 60 * 24 * 60 * 60, // 60 days + validator: 60 * 24 * 60 * 60, // 60 days + }, + validatorScoreParameters: { + exponent: 1, + adjustmentSpeed: 0.1, + }, + validatorEpochPayment: '1000000000000000000', + membershipHistoryLength: 60, maxGroupSize: '70', validatorKeys: [], // We register a single validator group during the migration. groupName: 'C-Labs', - groupUrl: 'https://www.celo.org', - }, - blockchainParameters: { - minimumClientVersion: { - major: 1, - minor: 8, - patch: 23, - }, + commission: 0.1, }, } @@ -101,11 +122,11 @@ const linkedLibraries = { 'SortedLinkedListWithMedian', ], SortedLinkedListWithMedian: ['AddressSortedLinkedListWithMedian'], - AddressLinkedList: ['Validators'], - AddressSortedLinkedList: ['Validators'], + AddressLinkedList: ['Validators', 'ValidatorsTest'], + AddressSortedLinkedList: ['Election', 'ElectionTest'], IntegerSortedLinkedList: ['Governance', 'IntegerSortedLinkedListTest'], AddressSortedLinkedListWithMedian: ['SortedOracles', 'AddressSortedLinkedListWithMedianTest'], - Signatures: ['Attestations', 'LockedGold', 'Escrow'], + Signatures: ['TestAttestations', 'Attestations', 'LockedGold', 'Escrow'], } const argv = minimist(process.argv.slice(2), { diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 2a53f18b185..19b587b3f72 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -9,7 +9,7 @@ "lint:ts": "tslint -c tslint.json --project tsconfig.json", "lint:sol": "solhint './contracts/**/*.sol'", "lint": "yarn run lint:ts && yarn run lint:sol", - "clean": "rm -rf ./types/typechain && rm -rf build/* && rm -rf migrations/*.js* && rm -rf test/**/*.js* && rm -f lib/*.js*", + "clean": "rm -rf ./types/typechain && rm -rf build/* && rm -rf .0x-artifacts/* && rm -rf migrations/*.js* && rm -rf test/**/*.js* && rm -f lib/*.js*", "pretest": "yarn run build", "test": "node runTests.js", "test:coverage": "yarn run test --coverage", @@ -74,7 +74,7 @@ "web3-provider-engine": "^15.0.0" }, "devDependencies": { - "@celo/ganache-cli": "git+https://github.com/celo-org/ganache-cli.git#816a475", + "@celo/ganache-cli": "git+https://github.com/celo-org/ganache-cli.git#9d77e02", "@celo/typescript": "0.0.1", "@types/bignumber.js": "^5.0.0", "@types/bn.js": "^4.11.0", diff --git a/packages/protocol/scripts/build.ts b/packages/protocol/scripts/build.ts index 4330d07a2e2..6106cd275c4 100644 --- a/packages/protocol/scripts/build.ts +++ b/packages/protocol/scripts/build.ts @@ -8,14 +8,15 @@ const BUILD_DIR = path.join(ROOT_DIR, 'build') const CONTRACTKIT_GEN_DIR = path.normalize(path.join(ROOT_DIR, '../contractkit/src/generated')) export const ProxyContracts = [ - 'GasCurrencyWhitelistProxy', - 'GasPriceMinimumProxy', - 'MultiSigProxy', - 'LockedGoldProxy', 'AttestationsProxy', + 'ElectionProxy', 'EscrowProxy', 'ExchangeProxy', + 'GasCurrencyWhitelistProxy', + 'GasPriceMinimumProxy', 'GoldTokenProxy', + 'LockedGoldProxy', + 'MultiSigProxy', 'ReserveProxy', 'StableTokenProxy', 'SortedOraclesProxy', @@ -32,8 +33,10 @@ export const CoreContracts = [ 'Validators', // governance - 'LockedGold', + 'Election', 'Governance', + 'LockedGold', + 'Validators', // identity 'Attestations', diff --git a/packages/protocol/scripts/devchain.ts b/packages/protocol/scripts/devchain.ts index 309dd4ad6ab..220c75430b7 100644 --- a/packages/protocol/scripts/devchain.ts +++ b/packages/protocol/scripts/devchain.ts @@ -42,11 +42,23 @@ yargs 'generate ', 'Create a new devchain directory from scratch', (args) => - args.positional('datadir', { type: 'string', description: 'Data Dir' }).option('upto', { - type: 'number', - description: 'When reset, run upto given migration', - }), - (args) => exitOnError(generateDevChain(args.datadir, { upto: args.upto })) + args + .positional('datadir', { type: 'string', description: 'Data Dir' }) + .option('upto', { + type: 'number', + description: 'When reset, run upto given migration', + }) + .option('migration_override', { + type: 'string', + description: 'Path to JSON containing config values to use in migrations', + }), + (args) => + exitOnError( + generateDevChain(args.datadir, { + upto: args.upto, + migrationOverride: args.migration_override, + }) + ) ).argv async function startGanache(datadir: string, opts: { verbose?: boolean }) { @@ -137,29 +149,44 @@ function createDirIfMissing(dir: string) { } } -function runMigrations(opts: { upto?: number } = {}) { +function runMigrations(opts: { upto?: number; migrationOverride?: string } = {}) { const cmdArgs = ['truffle', 'migrate'] if (opts.upto) { cmdArgs.push('--to') cmdArgs.push(opts.upto.toString()) } + + if (opts.migrationOverride) { + cmdArgs.push('--migration_override') + cmdArgs.push(fs.readFileSync(opts.migrationOverride).toString()) + } return execCmd(`yarn`, cmdArgs, { cwd: ProtocolRoot }) } -async function runDevChain(datadir: string, opts: { reset?: boolean; upto?: number } = {}) { +async function runDevChain( + datadir: string, + opts: { reset?: boolean; upto?: number; migrationOverride?: string } = {} +) { if (opts.reset) { await resetDir(datadir) } createDirIfMissing(datadir) const stopGanache = await startGanache(datadir, { verbose: true }) if (opts.reset) { - await runMigrations({ upto: opts.upto }) + await runMigrations({ upto: opts.upto, migrationOverride: opts.migrationOverride }) } return stopGanache } -async function generateDevChain(datadir: string, opts: { upto?: number } = {}) { - const stopGanache = await runDevChain(datadir, { reset: true, upto: opts.upto }) +async function generateDevChain( + datadir: string, + opts: { upto?: number; migrationOverride?: string } = {} +) { + const stopGanache = await runDevChain(datadir, { + reset: true, + upto: opts.upto, + migrationOverride: opts.migrationOverride, + }) await stopGanache() } diff --git a/packages/protocol/scripts/truffle/network_check.ts b/packages/protocol/scripts/truffle/network_check.ts index 4f81207c363..1f75a3aabdd 100644 --- a/packages/protocol/scripts/truffle/network_check.ts +++ b/packages/protocol/scripts/truffle/network_check.ts @@ -5,7 +5,6 @@ import { assertContractsRegistered, assertProxiesSet, assertRegistryAddressesSet, - assertStableTokenMinter, getReserveBalance, proxiedContracts, } from '@celo/protocol/lib/test-utils' @@ -41,7 +40,6 @@ module.exports = async (callback: (error?: any) => number) => { await assertContractsRegistered(getContract) await assertRegistryAddressesSet(getContract) await assertContractsOwnedByMultiSig(getContract) - await assertStableTokenMinter(getContract) await assertReserveBalance() console.log('Network check succeeded!') callback() diff --git a/packages/protocol/test/common/addresssortedlinkedlistwithmedian.ts b/packages/protocol/test/common/addresssortedlinkedlistwithmedian.ts index d6b962cd5c7..64f62b32326 100644 --- a/packages/protocol/test/common/addresssortedlinkedlistwithmedian.ts +++ b/packages/protocol/test/common/addresssortedlinkedlistwithmedian.ts @@ -206,6 +206,14 @@ contract('AddressSortedLinkedListWithMedianTest', (accounts: string[]) => { ] } + const randomElementOrNullAddress = (list: string[]): string => { + if (BigNumber.random().isLessThan(0.5)) { + return NULL_ADDRESS + } else { + return randomElement(list) + } + } + const makeActionSequence = (length: number, numKeys: number): SortedListAction[] => { const sequence: SortedListAction[] = [] const listKeys: Set = new Set([]) @@ -394,8 +402,8 @@ contract('AddressSortedLinkedListWithMedianTest', (accounts: string[]) => { let greater = NULL_ADDRESS const [keys, , ,] = await addressSortedLinkedListWithMedianTest.getElements() if (keys.length > 0) { - lesser = randomElement(keys) - greater = randomElement(keys) + lesser = randomElementOrNullAddress(keys) + greater = randomElementOrNullAddress(keys) } return { lesser, greater } } diff --git a/packages/protocol/test/common/integration.ts b/packages/protocol/test/common/integration.ts index 65c3a12688a..cdcbca11e4f 100644 --- a/packages/protocol/test/common/integration.ts +++ b/packages/protocol/test/common/integration.ts @@ -31,7 +31,7 @@ contract('Integration: Governance', (accounts: string[]) => { let governance: GovernanceInstance let registry: RegistryInstance let proposalTransactions: any - let weight: BigNumber + const value = new BigNumber('1000000000000000000') before(async () => { lockedGold = await getDeployedProxiedContract('LockedGold', artifacts) @@ -39,11 +39,8 @@ contract('Integration: Governance', (accounts: string[]) => { registry = await getDeployedProxiedContract('Registry', artifacts) // Set up a LockedGold account with which we can vote. await lockedGold.createAccount() - const noticePeriod = 60 * 60 * 24 // 1 day - const value = new BigNumber('1000000000000000000') // @ts-ignore - await lockedGold.newCommitment(noticePeriod, { value }) - weight = await lockedGold.getAccountWeight(accounts[0]) + await lockedGold.lock({ value }) proposalTransactions = [ { value: 0, @@ -94,7 +91,7 @@ contract('Integration: Governance', (accounts: string[]) => { }) it('should increase the number of upvotes for the proposal', async () => { - assertEqualBN(await governance.getUpvotes(proposalId), weight) + assertEqualBN(await governance.getUpvotes(proposalId), value) }) }) @@ -117,7 +114,7 @@ contract('Integration: Governance', (accounts: string[]) => { it('should increment the vote totals', async () => { const [yes, ,] = await governance.getVoteTotals(proposalId) - assertEqualBN(yes, weight) + assertEqualBN(yes, value) }) }) diff --git a/packages/protocol/test/common/migration.ts b/packages/protocol/test/common/migration.ts index db53a566d3a..3da2d07bfd3 100644 --- a/packages/protocol/test/common/migration.ts +++ b/packages/protocol/test/common/migration.ts @@ -2,7 +2,6 @@ import { assertContractsRegistered, assertProxiesSet, assertRegistryAddressesSet, - assertStableTokenMinter, getReserveBalance, } from '@celo/protocol/lib/test-utils' import { getDeployedProxiedContract } from '@celo/protocol/lib/web3-utils' @@ -52,10 +51,4 @@ contract('Migration', () => { assert.equal(balance, expectedBalance) }) }) - - describe('Checking StableToken minter', async () => { - it('should be set to the Reserve', async () => { - await assertStableTokenMinter(getContract) - }) - }) }) diff --git a/packages/protocol/test/governance/bondeddeposits.ts b/packages/protocol/test/governance/bondeddeposits.ts deleted file mode 100644 index f2fc5d2bb20..00000000000 --- a/packages/protocol/test/governance/bondeddeposits.ts +++ /dev/null @@ -1,1088 +0,0 @@ -import { CeloContractName } from '@celo/protocol/lib/registry-utils' -import { getParsedSignatureOfAddress } from '@celo/protocol/lib/signing-utils' -import { - assertEqualBN, - assertLogMatches, - assertRevert, - NULL_ADDRESS, - timeTravel, -} from '@celo/protocol/lib/test-utils' -import BigNumber from 'bignumber.js' -import { - LockedGoldContract, - LockedGoldInstance, - MockGoldTokenContract, - MockGoldTokenInstance, - MockGovernanceContract, - MockGovernanceInstance, - MockValidatorsContract, - MockValidatorsInstance, - RegistryContract, - RegistryInstance, -} from 'types' - -const LockedGold: LockedGoldContract = artifacts.require('LockedGold') -const Registry: RegistryContract = artifacts.require('Registry') -const MockGoldToken: MockGoldTokenContract = artifacts.require('MockGoldToken') -const MockGovernance: MockGovernanceContract = artifacts.require('MockGovernance') -const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') - -// @ts-ignore -// TODO(mcortesi): Use BN -LockedGold.numberFormat = 'BigNumber' - -const HOUR = 60 * 60 -const DAY = 24 * HOUR -const YEAR = 365 * DAY - -// TODO(asa): Test reward redemption -contract('LockedGold', (accounts: string[]) => { - let account = accounts[0] - const nonOwner = accounts[1] - const maxNoticePeriod = 2 * YEAR - let mockGoldToken: MockGoldTokenInstance - let mockGovernance: MockGovernanceInstance - let mockValidators: MockValidatorsInstance - let lockedGold: LockedGoldInstance - let registry: RegistryInstance - - enum roles { - validating, - voting, - rewards, - } - const forEachRole = (tests: (arg0: roles) => void) => - Object.keys(roles) - .slice(3) - .map((role) => describe(`when dealing with ${role} role`, () => tests(roles[role]))) - - beforeEach(async () => { - lockedGold = await LockedGold.new() - mockGoldToken = await MockGoldToken.new() - mockGovernance = await MockGovernance.new() - mockValidators = await MockValidators.new() - registry = await Registry.new() - await registry.setAddressFor(CeloContractName.GoldToken, mockGoldToken.address) - await registry.setAddressFor(CeloContractName.Governance, mockGovernance.address) - await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) - await lockedGold.initialize(registry.address, maxNoticePeriod) - await lockedGold.createAccount() - }) - - describe('#isAccount()', () => { - it('created account should exist', async () => { - const b = await lockedGold.isAccount(account) - assert.equal(b, true) - }) - it('account that was not created should not exist', async () => { - const b = await lockedGold.isAccount(accounts[2]) - assert.equal(b, false) - }) - }) - - describe('#isDelegate()', () => { - const delegate = accounts[1] - - beforeEach(async () => { - const sig = await getParsedSignatureOfAddress(web3, account, delegate) - await lockedGold.delegateRole(roles.voting, delegate, sig.v, sig.r, sig.s) - }) - - it('should return true for delegate', async () => { - assert.equal(await lockedGold.isDelegate(delegate), true) - }) - it('should return false for account', async () => { - assert.equal(await lockedGold.isDelegate(account), false) - }) - it('should return false for others', async () => { - assert.equal(await lockedGold.isDelegate(accounts[4]), false) - }) - }) - - describe('#initialize()', () => { - it('should set the owner', async () => { - const owner: string = await lockedGold.owner() - assert.equal(owner, account) - }) - - it('should set the maxNoticePeriod', async () => { - const actual = await lockedGold.maxNoticePeriod() - assert.equal(actual.toNumber(), maxNoticePeriod) - }) - - it('should set the registry address', async () => { - const registryAddress: string = await lockedGold.registry() - assert.equal(registryAddress, registry.address) - }) - - it('should revert if already initialized', async () => { - await assertRevert(lockedGold.initialize(registry.address, maxNoticePeriod)) - }) - }) - - describe('#setRegistry()', () => { - const anAddress: string = accounts[2] - - it('should set the registry when called by the owner', async () => { - await lockedGold.setRegistry(anAddress) - assert.equal(await lockedGold.registry(), anAddress) - }) - - it('should revert when not called by the owner', async () => { - await assertRevert(lockedGold.setRegistry(anAddress, { from: nonOwner })) - }) - }) - - describe('#setMaxNoticePeriod()', () => { - it('should set maxNoticePeriod when called by the owner', async () => { - await lockedGold.setMaxNoticePeriod(1) - assert.equal((await lockedGold.maxNoticePeriod()).toNumber(), 1) - }) - - it('should emit a MaxNoticePeriodSet event', async () => { - const resp = await lockedGold.setMaxNoticePeriod(1) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'MaxNoticePeriodSet', { - maxNoticePeriod: new BigNumber(1), - }) - }) - - it('should revert when not called by the owner', async () => { - await assertRevert(lockedGold.setMaxNoticePeriod(1, { from: nonOwner })) - }) - }) - - describe('#delegateRole()', () => { - const delegate = accounts[1] - let sig - - beforeEach(async () => { - sig = await getParsedSignatureOfAddress(web3, account, delegate) - }) - - forEachRole((role) => { - it('should set the role delegate', async () => { - await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - assert.equal(await lockedGold.delegations(delegate), account) - assert.equal(await lockedGold.isDelegate(delegate), true) - assert.equal(await lockedGold.getDelegateFromAccountAndRole(account, role), delegate) - assert.equal(await lockedGold.getAccountFromDelegateAndRole(delegate, role), account) - }) - - it('should emit a RoleDelegated event', async () => { - const resp = await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'RoleDelegated', { - role, - account, - delegate, - }) - }) - - it('should revert if the delegate is an account', async () => { - await lockedGold.createAccount({ from: delegate }) - await assertRevert(lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s)) - }) - - it('should revert if the address is already being delegated to', async () => { - const otherAccount = accounts[2] - const otherSig = await getParsedSignatureOfAddress(web3, otherAccount, delegate) - await lockedGold.createAccount({ from: otherAccount }) - await lockedGold.delegateRole(role, delegate, otherSig.v, otherSig.r, otherSig.s, { - from: otherAccount, - }) - await assertRevert(lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s)) - }) - - it('should revert if the signature is incorrect', async () => { - const nonDelegate = accounts[3] - const incorrectSig = await getParsedSignatureOfAddress(web3, account, nonDelegate) - await assertRevert( - lockedGold.delegateRole(role, delegate, incorrectSig.v, incorrectSig.r, incorrectSig.s) - ) - }) - - describe('when a previous delegation has been made', async () => { - const newDelegate = accounts[2] - let newSig - beforeEach(async () => { - await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - newSig = await getParsedSignatureOfAddress(web3, account, newDelegate) - }) - - it('should set the new delegate', async () => { - await lockedGold.delegateRole(role, newDelegate, newSig.v, newSig.r, newSig.s) - assert.equal(await lockedGold.delegations(newDelegate), account) - assert.equal(await lockedGold.getDelegateFromAccountAndRole(account, role), newDelegate) - assert.equal(await lockedGold.getAccountFromDelegateAndRole(newDelegate, role), account) - }) - - it('should reset the previous delegate', async () => { - await lockedGold.delegateRole(role, newDelegate, newSig.v, newSig.r, newSig.s) - assert.equal(await lockedGold.delegations(delegate), NULL_ADDRESS) - }) - }) - }) - }) - - describe('#freezeVoting()', () => { - it('should set the account voting to frozen', async () => { - await lockedGold.freezeVoting() - assert.isTrue(await lockedGold.isVotingFrozen(account)) - }) - - it('should emit a VotingFrozen event', async () => { - const resp = await lockedGold.freezeVoting() - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'VotingFrozen', { - account, - }) - }) - - it('should revert if the account voting is already frozen', async () => { - await lockedGold.freezeVoting() - await assertRevert(lockedGold.freezeVoting()) - }) - }) - - describe('#unfreezeVoting()', () => { - beforeEach(async () => { - await lockedGold.freezeVoting() - }) - - it('should set the account voting to unfrozen', async () => { - await lockedGold.unfreezeVoting() - assert.isFalse(await lockedGold.isVotingFrozen(account)) - }) - - it('should emit a VotingUnfrozen event', async () => { - const resp = await lockedGold.unfreezeVoting() - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'VotingUnfrozen', { - account, - }) - }) - - it('should revert if the account voting is already unfrozen', async () => { - await lockedGold.unfreezeVoting() - await assertRevert(lockedGold.unfreezeVoting()) - }) - }) - - describe('#newCommitment()', () => { - const noticePeriod = 1 * DAY + 1 * HOUR - const value = 1000 - const expectedWeight = 1033 - - it('should add a Locked Gold commitment', async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - const noticePeriods = await lockedGold.getNoticePeriods(account) - assert.equal(noticePeriods.length, 1) - assert.equal(noticePeriods[0].toNumber(), noticePeriod) - const [lockedValue, index] = await lockedGold.getLockedCommitment(account, noticePeriod) - assert.equal(lockedValue.toNumber(), value) - assert.equal(index.toNumber(), 0) - }) - - it('should update the account weight', async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - const weight = await lockedGold.getAccountWeight(account) - assert.equal(weight.toNumber(), expectedWeight) - }) - - it('should update the total weight', async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - const totalWeight = await lockedGold.totalWeight() - assert.equal(totalWeight.toNumber(), expectedWeight) - }) - - it('should emit a NewCommitment event', async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - const resp = await lockedGold.newCommitment(noticePeriod, { value }) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'NewCommitment', { - account, - value: new BigNumber(value), - noticePeriod: new BigNumber(noticePeriod), - }) - }) - - it('should revert when the specified notice period is too large', async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await assertRevert(lockedGold.newCommitment(maxNoticePeriod + 1, { value })) - }) - - it('should revert when the specified value is 0', async () => { - await assertRevert(lockedGold.newCommitment(noticePeriod)) - }) - - it('should revert when the account does not exist', async () => { - await assertRevert(lockedGold.newCommitment(noticePeriod, { value, from: accounts[1] })) - }) - - it('should revert if the caller is voting', async () => { - await mockGovernance.setVoting(account) - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await assertRevert(lockedGold.newCommitment(noticePeriod, { value })) - }) - }) - - describe('#notifyCommitment()', () => { - const noticePeriod = 60 * 60 * 24 // 1 day - const value = 1000 - beforeEach(async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - }) - - it('should add a notified deposit', async () => { - await lockedGold.notifyCommitment(value, noticePeriod) - const availabilityTime = new BigNumber(noticePeriod).plus( - (await web3.eth.getBlock('latest')).timestamp - ) - const availabilityTimes = await lockedGold.getAvailabilityTimes(account) - assert.equal(availabilityTimes.length, 1) - assert.equal(availabilityTimes[0].toNumber(), availabilityTime.toNumber()) - - const [notifiedValue, index] = await lockedGold.getNotifiedCommitment( - account, - availabilityTime - ) - assert.equal(notifiedValue.toNumber(), value) - assert.equal(index.toNumber(), 0) - }) - - it('should remove the Locked Gold commitment', async () => { - await lockedGold.notifyCommitment(value, noticePeriod) - const noticePeriods = await lockedGold.getNoticePeriods(account) - assert.equal(noticePeriods.length, 0) - const [lockedValue, index] = await lockedGold.getLockedCommitment(account, noticePeriod) - assert.equal(lockedValue.toNumber(), 0) - assert.equal(index.toNumber(), 0) - }) - - it('should update the account weight', async () => { - await lockedGold.notifyCommitment(value, noticePeriod) - const weight = await lockedGold.getAccountWeight(account) - assert.equal(weight.toNumber(), value) - }) - - it('should update the total weight', async () => { - await lockedGold.notifyCommitment(value, noticePeriod) - const totalWeight = await lockedGold.totalWeight() - assert.equal(totalWeight.toNumber(), value) - }) - - it('should emit a CommitmentNotified event', async () => { - const resp = await lockedGold.notifyCommitment(value, noticePeriod) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'CommitmentNotified', { - account, - value: new BigNumber(value), - noticePeriod: new BigNumber(noticePeriod), - availabilityTime: new BigNumber(noticePeriod).plus( - (await web3.eth.getBlock('latest')).timestamp - ), - }) - }) - - it('should revert when the value of the Locked Gold commitment is 0', async () => { - await assertRevert(lockedGold.notifyCommitment(1, noticePeriod + 1)) - }) - - it('should revert when value is greater than the value of the Locked Gold commitment', async () => { - await assertRevert(lockedGold.notifyCommitment(value + 1, noticePeriod)) - }) - - it('should revert when the value is 0', async () => { - await assertRevert(lockedGold.notifyCommitment(0, noticePeriod)) - }) - - it('should revert if the account is validating', async () => { - await mockValidators.setValidating(account) - await assertRevert(lockedGold.notifyCommitment(value, noticePeriod)) - }) - - it('should revert if the caller is voting', async () => { - await mockGovernance.setVoting(account) - await assertRevert(lockedGold.notifyCommitment(value, noticePeriod)) - }) - }) - - describe('#extendCommitment()', () => { - const value = 1000 - const expectedWeight = 1033 - let availabilityTime: BigNumber - - beforeEach(async () => { - // Set an initial notice period of just over one day, so that when we rebond, we're - // guaranteed that the new notice period is at least one day. - const noticePeriod = 1 * DAY + 1 * HOUR - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - await lockedGold.notifyCommitment(value, noticePeriod) - availabilityTime = new BigNumber(noticePeriod).plus( - (await web3.eth.getBlock('latest')).timestamp - ) - }) - - it('should add a Locked Gold commitment', async () => { - await lockedGold.extendCommitment(value, availabilityTime) - const noticePeriods = await lockedGold.getNoticePeriods(account) - assert.equal(noticePeriods.length, 1) - const noticePeriod = availabilityTime - .minus((await web3.eth.getBlock('latest')).timestamp) - .toNumber() - assert.equal(noticePeriods[0].toNumber(), noticePeriod) - const [lockedValue, index] = await lockedGold.getLockedCommitment(account, noticePeriod) - assert.equal(lockedValue.toNumber(), value) - assert.equal(index.toNumber(), 0) - }) - - it('should remove a notified deposit', async () => { - await lockedGold.extendCommitment(value, availabilityTime) - const availabilityTimes = await lockedGold.getAvailabilityTimes(account) - assert.equal(availabilityTimes.length, 0) - const [notifiedValue, index] = await lockedGold.getNotifiedCommitment( - account, - availabilityTime - ) - assert.equal(notifiedValue.toNumber(), 0) - assert.equal(index.toNumber(), 0) - }) - - it('should update the account weight', async () => { - await lockedGold.extendCommitment(value, availabilityTime) - const weight = await lockedGold.getAccountWeight(account) - assert.equal(weight.toNumber(), expectedWeight) - }) - - it('should update the total weight', async () => { - await lockedGold.extendCommitment(value, availabilityTime) - const totalWeight = await lockedGold.totalWeight() - assert.equal(totalWeight.toNumber(), expectedWeight) - }) - - it('should emit a CommitmentExtended event', async () => { - const resp = await lockedGold.extendCommitment(value, availabilityTime) - const noticePeriod = availabilityTime.minus((await web3.eth.getBlock('latest')).timestamp) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'CommitmentExtended', { - account, - value: new BigNumber(value), - noticePeriod, - availabilityTime, - }) - }) - - it('should revert when the notified deposit is withdrawable', async () => { - await timeTravel( - availabilityTime - .minus((await web3.eth.getBlock('latest')).timestamp) - .plus(1) - .toNumber(), - web3 - ) - await assertRevert(lockedGold.extendCommitment(value, availabilityTime)) - }) - - it('should revert when the value of the notified deposit is 0', async () => { - await assertRevert(lockedGold.extendCommitment(value, availabilityTime.plus(1))) - }) - - it('should revert when the value is 0', async () => { - await assertRevert(lockedGold.extendCommitment(0, availabilityTime)) - }) - - it('should revert if the caller is voting', async () => { - await mockGovernance.setVoting(account) - await assertRevert(lockedGold.extendCommitment(value, availabilityTime)) - }) - }) - - describe('#withdrawCommitment()', () => { - const noticePeriod = 1 * DAY - const value = 1000 - let availabilityTime: BigNumber - - beforeEach(async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - await lockedGold.notifyCommitment(value, noticePeriod) - availabilityTime = new BigNumber(noticePeriod).plus( - (await web3.eth.getBlock('latest')).timestamp - ) - }) - - it('should remove the notified deposit', async () => { - await timeTravel(noticePeriod, web3) - await lockedGold.withdrawCommitment(availabilityTime) - - const availabilityTimes = await lockedGold.getAvailabilityTimes(account) - assert.equal(availabilityTimes.length, 0) - }) - - it('should update the account weight', async () => { - await timeTravel(noticePeriod, web3) - await lockedGold.withdrawCommitment(availabilityTime) - - const weight = await lockedGold.getAccountWeight(account) - assert.equal(weight.toNumber(), 0) - }) - - it('should update the total weight', async () => { - await timeTravel(noticePeriod, web3) - await lockedGold.withdrawCommitment(availabilityTime) - - const totalWeight = await lockedGold.totalWeight() - assert.equal(totalWeight.toNumber(), 0) - }) - - it('should emit a Withdrawal event', async () => { - await timeTravel(noticePeriod, web3) - const resp = await lockedGold.withdrawCommitment(availabilityTime) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'Withdrawal', { - account, - value: new BigNumber(value), - }) - }) - - it('should revert if the account is validating', async () => { - await mockValidators.setValidating(account) - await assertRevert(lockedGold.withdrawCommitment(availabilityTime)) - }) - - it('should revert when the notice period has not passed', async () => { - await assertRevert(lockedGold.withdrawCommitment(availabilityTime)) - }) - - it('should revert when the value of the notified deposit is 0', async () => { - await timeTravel(noticePeriod, web3) - await assertRevert(lockedGold.withdrawCommitment(availabilityTime.plus(1))) - }) - - it('should revert if the caller is voting', async () => { - await timeTravel(noticePeriod, web3) - await mockGovernance.setVoting(account) - await assertRevert(lockedGold.withdrawCommitment(availabilityTime)) - }) - }) - - describe('#increaseNoticePeriod()', () => { - const noticePeriod = 1 * DAY - const value = 1000 - const increase = noticePeriod - const expectedWeight = 1047 - - beforeEach(async () => { - // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails - await lockedGold.newCommitment(noticePeriod, { value }) - }) - - it('should update the Locked Gold commitment', async () => { - await lockedGold.increaseNoticePeriod(value, noticePeriod, increase) - const noticePeriods = await lockedGold.getNoticePeriods(account) - assert.equal(noticePeriods.length, 1) - assert.equal(noticePeriods[0].toNumber(), noticePeriod + increase) - const [lockedValue, index] = await lockedGold.getLockedCommitment( - account, - noticePeriod + increase - ) - assert.equal(lockedValue.toNumber(), value) - assert.equal(index.toNumber(), 0) - }) - - it('should update the account weight', async () => { - await lockedGold.increaseNoticePeriod(value, noticePeriod, increase) - const weight = await lockedGold.getAccountWeight(account) - assert.equal(weight.toNumber(), expectedWeight) - }) - - it('should update the total weight', async () => { - await lockedGold.increaseNoticePeriod(value, noticePeriod, increase) - const totalWeight = await lockedGold.totalWeight() - assert.equal(totalWeight.toNumber(), expectedWeight) - }) - - it('should emit a NoticePeriodIncreased event', async () => { - const resp = await lockedGold.increaseNoticePeriod(value, noticePeriod, increase) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertLogMatches(log, 'NoticePeriodIncreased', { - account, - value: new BigNumber(value), - noticePeriod: new BigNumber(noticePeriod), - increase: new BigNumber(increase), - }) - }) - - it('should revert if the value is 0', async () => { - await assertRevert(lockedGold.increaseNoticePeriod(0, noticePeriod, increase)) - }) - - it('should revert if the increase is 0', async () => { - await assertRevert(lockedGold.increaseNoticePeriod(value, noticePeriod, 0)) - }) - - it('should revert if the Locked Gold commitment is smaller than the value', async () => { - await assertRevert(lockedGold.increaseNoticePeriod(value, noticePeriod + 1, increase)) - }) - - it('should revert if the caller is voting', async () => { - await mockGovernance.setVoting(account) - await assertRevert(lockedGold.increaseNoticePeriod(value, noticePeriod, increase)) - }) - }) - - describe('#getAccountFromDelegateAndRole()', () => { - forEachRole((role) => { - describe('when the account is not delegating', () => { - it('should return the account when passed the account', async () => { - assert.equal(await lockedGold.getAccountFromDelegateAndRole(account, role), account) - }) - - it('should revert when passed a delegate that is not the role delegate', async () => { - const delegate = accounts[2] - const diffRole = (role + 1) % 3 - const sig = await getParsedSignatureOfAddress(web3, account, delegate) - await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - await assertRevert(lockedGold.getAccountFromDelegateAndRole(delegate, diffRole)) - }) - }) - - describe('when the account is delegating', () => { - const delegate = accounts[1] - - beforeEach(async () => { - const sig = await getParsedSignatureOfAddress(web3, account, delegate) - await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - }) - - it('should return the account when passed the delegate', async () => { - assert.equal(await lockedGold.getAccountFromDelegateAndRole(delegate, role), account) - }) - - it('should return the account when passed the account', async () => { - assert.equal(await lockedGold.getAccountFromDelegateAndRole(account, role), account) - }) - - it('should revert when passed a delegate that is not the role delegate', async () => { - const delegate = accounts[2] - const diffRole = (role + 1) % 3 - const sig = await getParsedSignatureOfAddress(web3, account, delegate) - await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - await assertRevert(lockedGold.getAccountFromDelegateAndRole(delegate, diffRole)) - }) - }) - }) - }) - - describe('#getDelegateFromAccountAndRole()', () => { - forEachRole((role) => { - describe('when the account is not delegating', () => { - it('should return the account when passed the account', async () => { - assert.equal(await lockedGold.getDelegateFromAccountAndRole(account, role), account) - }) - }) - - describe('when the account is delegating', () => { - const delegate = accounts[1] - - beforeEach(async () => { - const sig = await getParsedSignatureOfAddress(web3, account, delegate) - await lockedGold.delegateRole(role, delegate, sig.v, sig.r, sig.s) - }) - - it('should return the account when passed undelegated role', async () => { - const role2 = (role + 1) % 3 - assert.equal(await lockedGold.getDelegateFromAccountAndRole(account, role2), account) - }) - - it('should return the delegate when passed the delegated role', async () => { - assert.equal(await lockedGold.getDelegateFromAccountAndRole(account, role), delegate) - }) - }) - }) - }) - - describe('#isVoting()', () => { - describe('when the account is not delegating', () => { - it('should return false if the account is not voting in governance or validator elections', async () => { - assert.isFalse(await lockedGold.isVoting(account)) - }) - - it('should return true if the account is voting in governance', async () => { - await mockGovernance.setVoting(account) - assert.isTrue(await lockedGold.isVoting(account)) - }) - - it('should return true if the account is voting in validator elections', async () => { - await mockValidators.setVoting(account) - assert.isTrue(await lockedGold.isVoting(account)) - }) - - it('should return true if the account is voting in governance and validator elections', async () => { - await mockGovernance.setVoting(account) - await mockValidators.setVoting(account) - assert.isTrue(await lockedGold.isVoting(account)) - }) - }) - - describe('when the account is delegating', () => { - const delegate = accounts[1] - - beforeEach(async () => { - const sig = await getParsedSignatureOfAddress(web3, account, delegate) - await lockedGold.delegateRole(roles.voting, delegate, sig.v, sig.r, sig.s) - }) - - it('should return false if the delegate is not voting in governance or validator elections', async () => { - assert.isFalse(await lockedGold.isVoting(account)) - }) - - it('should return true if the delegate is voting in governance', async () => { - await mockGovernance.setVoting(delegate) - assert.isTrue(await lockedGold.isVoting(account)) - }) - - it('should return true if the delegate is voting in validator elections', async () => { - await mockValidators.setVoting(delegate) - assert.isTrue(await lockedGold.isVoting(account)) - }) - - it('should return true if the delegate is voting in governance and validator elections', async () => { - await mockGovernance.setVoting(delegate) - await mockValidators.setVoting(delegate) - assert.isTrue(await lockedGold.isVoting(account)) - }) - }) - }) - - describe('#getCommitmentWeight()', () => { - const value = new BigNumber(521000) - const oneDay = new BigNumber(DAY) - it('should return the commitment value when notice period is zero', async () => { - const noticePeriod = new BigNumber(0) - assertEqualBN(await lockedGold.getCommitmentWeight(value, noticePeriod), value) - }) - - it('should return the commitment value when notice period is less than one day', async () => { - const noticePeriod = oneDay.minus(1) - assertEqualBN(await lockedGold.getCommitmentWeight(value, noticePeriod), value) - }) - - it('should return the commitment value times 1.0333 when notice period is one day', async () => { - const noticePeriod = oneDay - assertEqualBN( - await lockedGold.getCommitmentWeight(value, noticePeriod), - value.times(1.0333).integerValue(BigNumber.ROUND_DOWN) - ) - }) - - it('should return the commitment value times 1.047 when notice period is two days', async () => { - const noticePeriod = oneDay.times(2) - assertEqualBN( - await lockedGold.getCommitmentWeight(value, noticePeriod), - value.times(1.047).integerValue(BigNumber.ROUND_DOWN) - ) - }) - - it('should return the commitment value times 1.1823 when notice period is 30 days', async () => { - const noticePeriod = oneDay.times(30) - assertEqualBN( - await lockedGold.getCommitmentWeight(value, noticePeriod), - value.times(1.1823).integerValue(BigNumber.ROUND_DOWN) - ) - }) - - it('should return the commitment value times 2.103 when notice period is 3 years', async () => { - const noticePeriod = oneDay.times(365).times(3) - assertEqualBN( - await lockedGold.getCommitmentWeight(value, noticePeriod), - value.times(2.103).integerValue(BigNumber.ROUND_DOWN) - ) - }) - }) - - describe('when there are multiple commitments, notifies, rebondings, notice period increases, and withdrawals', () => { - beforeEach(async () => { - for (const accountToCreate of accounts) { - // Account for `account` has already been created. - if (accountToCreate !== account) { - await lockedGold.createAccount({ from: accountToCreate }) - } - } - }) - - enum ActionType { - Deposit = 'Deposit', - Notify = 'Notify', - Increase = 'Increase', - Rebond = 'Rebond', - Withdraw = 'Withdraw', - } - - const initializeState = (numAccounts: number) => { - const locked: Map> = new Map() - const notified: Map> = new Map() - const noticePeriods: Map> = new Map() - const availabilityTimes: Map> = new Map() - const selectedAccounts = accounts.slice(0, numAccounts) - for (const acc of selectedAccounts) { - // Map keys, set elements appear not to be able to be BigNumbers, so we use strings instead. - locked.set(acc, new Map()) - notified.set(acc, new Map()) - noticePeriods.set(acc, new Set([])) - availabilityTimes.set(acc, new Set([])) - } - - return { locked, notified, noticePeriods, availabilityTimes, selectedAccounts } - } - - const rndElement = (elems: A[]) => { - return elems[ - Math.floor( - BigNumber.random() - .times(elems.length) - .toNumber() - ) - ] - } - const rndSetElement = (s: Set) => rndElement(Array.from(s)) - - const getOrElse = (map: Map, key: B, defaultValue: A) => - map.has(key) ? map.get(key) : defaultValue - - const executeActionsAndAssertState = async (numActions: number, numAccounts: number) => { - const { - selectedAccounts, - locked, - notified, - noticePeriods, - availabilityTimes, - } = initializeState(numAccounts) - - for (let i = 0; i < numActions; i++) { - const blockTime = 5 - await timeTravel(blockTime, web3) - account = rndElement(selectedAccounts) - - const accountLockedGold = locked.get(account) - const accountNotifiedCommitments = notified.get(account) - const accountNoticePeriods = noticePeriods.get(account) - const accountAvailabilityTimes = availabilityTimes.get(account) - - const getWithdrawableAvailabilityTimes = async (): Promise> => { - const nextTimestamp = new BigNumber((await web3.eth.getBlock('latest')).timestamp) - const items: string[] = Array.from(accountAvailabilityTimes) - return new Set(items.filter((x: string) => nextTimestamp.gt(x))) - } - - const getRebondableAvailabilityTimes = async (): Promise> => { - const nextTimestamp = new BigNumber((await web3.eth.getBlock('latest')).timestamp).plus( - blockTime - ) - const items: string[] = Array.from(accountAvailabilityTimes) - // Subtract one to cover edge case where block time is 6 seconds. - return new Set(items.filter((x: string) => nextTimestamp.plus(1).lt(x))) - } - - // Select random action type. - const actionTypeOptions = [ActionType.Deposit] - if (accountNoticePeriods.size > 0) { - actionTypeOptions.push(ActionType.Notify) - actionTypeOptions.push(ActionType.Increase) - } - const rebondableAvailabilityTimes = await getRebondableAvailabilityTimes() - if (rebondableAvailabilityTimes.size > 0) { - // Push twice to increase likelihood - actionTypeOptions.push(ActionType.Rebond) - actionTypeOptions.push(ActionType.Rebond) - } - const withdrawableAvailabilityTimes = await getWithdrawableAvailabilityTimes() - if (withdrawableAvailabilityTimes.size > 0) { - // Push twice to increase likelihood - actionTypeOptions.push(ActionType.Withdraw) - actionTypeOptions.push(ActionType.Withdraw) - } - const actionType = rndElement(actionTypeOptions) - - const getLockedCommitmentValue = (noticePeriod: string) => - getOrElse(accountLockedGold, noticePeriod, new BigNumber(0)) - const getNotifiedCommitmentValue = (availabilityTime: string) => - getOrElse(accountNotifiedCommitments, availabilityTime, new BigNumber(0)) - - const randomSometimesMaximumValue = (maximum: BigNumber) => { - assert.isFalse(maximum.eq(0)) - const random = BigNumber.random().toNumber() - if (random < 0.5) { - return maximum - } else { - return BigNumber.max( - BigNumber.random() - .times(maximum) - .integerValue(), - 1 - ) - } - } - - // Perform random action and update test implementation state. - if (actionType === ActionType.Deposit) { - const value = new BigNumber(web3.utils.randomHex(2)).toNumber() - // Notice period of at most 10 blocks. - const noticePeriod = BigNumber.random() - .times(10) - .times(blockTime) - .integerValue() - .valueOf() - await lockedGold.newCommitment(noticePeriod, { value, from: account }) - accountNoticePeriods.add(noticePeriod) - accountLockedGold.set(noticePeriod, getLockedCommitmentValue(noticePeriod).plus(value)) - } else if (actionType === ActionType.Notify || actionType === ActionType.Increase) { - const noticePeriod = rndSetElement(accountNoticePeriods) - const lockedDepositValue = getLockedCommitmentValue(noticePeriod) - const value = randomSometimesMaximumValue(lockedDepositValue) - - if (value.eq(lockedDepositValue)) { - accountLockedGold.delete(noticePeriod) - accountNoticePeriods.delete(noticePeriod) - } else { - accountLockedGold.set(noticePeriod, lockedDepositValue.minus(value)) - } - - if (actionType === ActionType.Notify) { - await lockedGold.notifyCommitment(value, noticePeriod, { from: account }) - const availabilityTime = new BigNumber(noticePeriod) - .plus((await web3.eth.getBlock('latest')).timestamp) - .valueOf() - accountAvailabilityTimes.add(availabilityTime) - accountNotifiedCommitments.set( - availabilityTime, - getNotifiedCommitmentValue(availabilityTime).plus(value) - ) - } else { - // Notice period increase of at most 10 blocks. - const increase = BigNumber.random() - .times(10) - .times(blockTime) - .integerValue() - .plus(1) - await lockedGold.increaseNoticePeriod(value, noticePeriod, increase, { - from: account, - }) - const increasedNoticePeriod = increase.plus(noticePeriod).valueOf() - accountNoticePeriods.add(increasedNoticePeriod) - accountLockedGold.set( - increasedNoticePeriod, - getLockedCommitmentValue(increasedNoticePeriod).plus(value) - ) - } - } else if (actionType === ActionType.Rebond) { - const availabilityTime = rndSetElement(rebondableAvailabilityTimes) - const notifiedDepositValue = getNotifiedCommitmentValue(availabilityTime) - const value = randomSometimesMaximumValue(notifiedDepositValue) - await lockedGold.extendCommitment(value, availabilityTime, { from: account }) - - if (value.eq(notifiedDepositValue)) { - accountNotifiedCommitments.delete(availabilityTime) - accountAvailabilityTimes.delete(availabilityTime) - } else { - accountNotifiedCommitments.set(availabilityTime, notifiedDepositValue.minus(value)) - } - const noticePeriod = new BigNumber(availabilityTime) - .minus((await web3.eth.getBlock('latest')).timestamp) - .valueOf() - accountLockedGold.set(noticePeriod, getLockedCommitmentValue(noticePeriod).plus(value)) - accountNoticePeriods.add(noticePeriod) - } else if (actionType === ActionType.Withdraw) { - const availabilityTime = rndSetElement(withdrawableAvailabilityTimes) - await lockedGold.withdrawCommitment(availabilityTime, { from: account }) - accountAvailabilityTimes.delete(availabilityTime) - accountNotifiedCommitments.delete(availabilityTime) - } else { - assert.isTrue(false) - } - - // Sanity check our test implementation. - selectedAccounts.forEach((acc) => { - if (locked.get(acc).size > 0) { - assert.hasAllKeys( - noticePeriods.get(acc), - Array.from(locked.get(acc).keys()), - `notice periods don\'t match for account: ${acc}` - ) - } - if (notified.get(acc).size > 0) { - assert.hasAllKeys( - availabilityTimes.get(acc), - Array.from(notified.get(acc).keys()), - `availability times don\'t match for account: ${acc}` - ) - } - }) - - // Test the contract state matches our test implementation. - let expectedTotalWeight = new BigNumber(0) - for (const acc of selectedAccounts) { - let expectedAccountWeight = new BigNumber(0) - const actualNoticePeriods = await lockedGold.getNoticePeriods(acc) - - assert.lengthOf(actualNoticePeriods, noticePeriods.get(acc).size) - for (let k = 0; k < actualNoticePeriods.length; k++) { - const noticePeriod = actualNoticePeriods[k] - assert.isTrue(noticePeriods.get(acc).has(noticePeriod.valueOf())) - const [actualValue, actualIndex] = await lockedGold.getLockedCommitment( - acc, - noticePeriod - ) - assertEqualBN(actualIndex, k) - const expectedValue = locked.get(acc).get(noticePeriod.valueOf()) - assertEqualBN(actualValue, expectedValue) - assertEqualBN(actualNoticePeriods[actualIndex.toNumber()], noticePeriod) - expectedAccountWeight = expectedAccountWeight.plus( - await lockedGold.getCommitmentWeight(expectedValue, noticePeriod) - ) - } - - const actualAvailabilityTimes = await lockedGold.getAvailabilityTimes(acc) - - assert.equal(actualAvailabilityTimes.length, availabilityTimes.get(acc).size) - for (let k = 0; k < actualAvailabilityTimes.length; k++) { - const availabilityTime = actualAvailabilityTimes[k] - assert.isTrue(availabilityTimes.get(acc).has(availabilityTime.valueOf())) - const [actualValue, actualIndex] = await lockedGold.getNotifiedCommitment( - acc, - availabilityTime - ) - assertEqualBN(actualIndex, k) - const expectedValue = notified.get(acc).get(availabilityTime.valueOf()) - assertEqualBN(actualValue, expectedValue) - assertEqualBN(actualAvailabilityTimes[actualIndex.toNumber()], availabilityTime) - expectedAccountWeight = expectedAccountWeight.plus(expectedValue) - } - assertEqualBN(await lockedGold.getAccountWeight(acc), expectedAccountWeight) - expectedTotalWeight = expectedTotalWeight.plus(expectedAccountWeight) - } - } - } - - it.skip('should match a simple typescript implementation', async () => { - const numActions = 100 - const numAccounts = 2 - await executeActionsAndAssertState(numActions, numAccounts) - }) - }) -}) diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts new file mode 100644 index 00000000000..4fc979117aa --- /dev/null +++ b/packages/protocol/test/governance/election.ts @@ -0,0 +1,1148 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { + assertContainSubset, + assertEqualBN, + assertRevert, + NULL_ADDRESS, + mineBlocks, +} from '@celo/protocol/lib/test-utils' +import { toFixed } from '@celo/utils/lib/fixidity' +import BigNumber from 'bignumber.js' +import { + MockLockedGoldContract, + MockLockedGoldInstance, + MockValidatorsContract, + MockValidatorsInstance, + MockRandomContract, + MockRandomInstance, + RegistryContract, + RegistryInstance, + ElectionTestContract, + ElectionTestInstance, +} from 'types' + +const ElectionTest: ElectionTestContract = artifacts.require('ElectionTest') +const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') +const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') +const MockRandom: MockRandomContract = artifacts.require('MockRandom') +const Registry: RegistryContract = artifacts.require('Registry') + +// @ts-ignore +// TODO(mcortesi): Use BN +ElectionTest.numberFormat = 'BigNumber' + +// Hard coded in ganache. +const EPOCH = 100 + +contract('Election', (accounts: string[]) => { + let election: ElectionTestInstance + let registry: RegistryInstance + let mockLockedGold: MockLockedGoldInstance + let mockValidators: MockValidatorsInstance + + const nonOwner = accounts[1] + const electableValidators = { + min: new BigNumber(4), + max: new BigNumber(6), + } + const maxNumGroupsVotedFor = new BigNumber(3) + const electabilityThreshold = toFixed(1 / 100) + + beforeEach(async () => { + election = await ElectionTest.new() + mockLockedGold = await MockLockedGold.new() + mockValidators = await MockValidators.new() + registry = await Registry.new() + await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + await election.initialize( + registry.address, + electableValidators.min, + electableValidators.max, + maxNumGroupsVotedFor, + electabilityThreshold + ) + }) + + describe('#initialize()', () => { + it('should have set the owner', async () => { + const owner: string = await election.owner() + assert.equal(owner, accounts[0]) + }) + + it('should have set electableValidators', async () => { + const [min, max] = await election.getElectableValidators() + assertEqualBN(min, electableValidators.min) + assertEqualBN(max, electableValidators.max) + }) + + it('should have set maxNumGroupsVotedFor', async () => { + const actualMaxNumGroupsVotedFor = await election.maxNumGroupsVotedFor() + assertEqualBN(actualMaxNumGroupsVotedFor, maxNumGroupsVotedFor) + }) + + it('should have set electabilityThreshold', async () => { + const actualElectabilityThreshold = await election.getElectabilityThreshold() + assertEqualBN(actualElectabilityThreshold, electabilityThreshold) + }) + + it('should not be callable again', async () => { + await assertRevert( + election.initialize( + registry.address, + electableValidators.min, + electableValidators.max, + maxNumGroupsVotedFor, + electabilityThreshold + ) + ) + }) + }) + + describe('#setElectabilityThreshold', () => { + it('should set the electability threshold', async () => { + const threshold = toFixed(1 / 10) + await election.setElectabilityThreshold(threshold) + const result = await election.getElectabilityThreshold() + assertEqualBN(result, threshold) + }) + + it('should revert when the threshold is larger than 100%', async () => { + const threshold = toFixed(new BigNumber('2')) + await assertRevert(election.setElectabilityThreshold(threshold)) + }) + }) + + describe('#setElectableValidators', () => { + const newElectableValidators = { + min: electableValidators.min.plus(1), + max: electableValidators.max.plus(1), + } + + it('should set the minimum electable valdiators', async () => { + await election.setElectableValidators(newElectableValidators.min, newElectableValidators.max) + const [min, max] = await election.getElectableValidators() + assertEqualBN(min, newElectableValidators.min) + assertEqualBN(max, newElectableValidators.max) + }) + + it('should emit the ElectableValidatorsSet event', async () => { + const resp = await election.setElectableValidators( + newElectableValidators.min, + newElectableValidators.max + ) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ElectableValidatorsSet', + args: { + min: newElectableValidators.min, + max: newElectableValidators.max, + }, + }) + }) + + it('should revert when the minElectableValidators is zero', async () => { + await assertRevert(election.setElectableValidators(0, newElectableValidators.max)) + }) + + it('should revert when the min is greater than max', async () => { + await assertRevert( + election.setElectableValidators( + newElectableValidators.max.plus(1), + newElectableValidators.max + ) + ) + }) + + it('should revert when the values are unchanged', async () => { + await assertRevert( + election.setElectableValidators(electableValidators.min, electableValidators.max) + ) + }) + + it('should revert when called by anyone other than the owner', async () => { + await assertRevert( + election.setElectableValidators(newElectableValidators.min, newElectableValidators.max, { + from: nonOwner, + }) + ) + }) + }) + + describe('#setMaxNumGroupsVotedFor', () => { + const newMaxNumGroupsVotedFor = maxNumGroupsVotedFor.plus(1) + it('should set the max electable validators', async () => { + await election.setMaxNumGroupsVotedFor(newMaxNumGroupsVotedFor) + assertEqualBN(await election.maxNumGroupsVotedFor(), newMaxNumGroupsVotedFor) + }) + + it('should emit the MaxNumGroupsVotedForSet event', async () => { + const resp = await election.setMaxNumGroupsVotedFor(newMaxNumGroupsVotedFor) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'MaxNumGroupsVotedForSet', + args: { + maxNumGroupsVotedFor: new BigNumber(newMaxNumGroupsVotedFor), + }, + }) + }) + + it('should revert when the maxNumGroupsVotedFor is unchanged', async () => { + await assertRevert(election.setMaxNumGroupsVotedFor(maxNumGroupsVotedFor)) + }) + + it('should revert when called by anyone other than the owner', async () => { + await assertRevert( + election.setMaxNumGroupsVotedFor(newMaxNumGroupsVotedFor, { from: nonOwner }) + ) + }) + }) + + describe('#markGroupEligible', () => { + const group = accounts[1] + describe('when called by the registered validators contract', () => { + beforeEach(async () => { + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + }) + + describe('when the group has no votes', () => { + let resp: any + beforeEach(async () => { + resp = await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + }) + + it('should add the group to the list of eligible groups', async () => { + assert.deepEqual(await election.getEligibleValidatorGroups(), [group]) + }) + + it('should emit the ValidatorGroupMarkedEligible event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupMarkedEligible', + args: { + group, + }, + }) + }) + + describe('when the group has already been marked eligible', () => { + it('should revert', async () => { + await assertRevert(election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + }) + }) + + describe('not called by the registered validators contract', () => { + it('should revert', async () => { + await assertRevert(election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + }) + + describe('#markGroupIneligible', () => { + const group = accounts[1] + describe('when the group is eligible', () => { + beforeEach(async () => { + await mockValidators.setMembers(group, [accounts[9]]) + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + }) + + describe('when called by the registered Validators contract', () => { + let resp: any + beforeEach(async () => { + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + resp = await election.markGroupIneligible(group) + }) + + it('should remove the group from the list of eligible groups', async () => { + assert.deepEqual(await election.getEligibleValidatorGroups(), []) + }) + + it('should emit the ValidatorGroupMarkedIneligible event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupMarkedIneligible', + args: { + group, + }, + }) + }) + }) + + describe('when not called by the registered Validators contract', () => { + it('should revert', async () => { + await assertRevert(election.markGroupIneligible(group)) + }) + }) + }) + + describe('when the group is ineligible', () => { + describe('when called by the registered Validators contract', () => { + beforeEach(async () => { + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + }) + + it('should revert', async () => { + await assertRevert(election.markGroupIneligible(group)) + }) + }) + }) + }) + + describe('#vote', () => { + const voter = accounts[0] + const group = accounts[1] + const value = new BigNumber(1000) + describe('when the group is eligible', () => { + beforeEach(async () => { + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + }) + + describe('when the group can receive votes', () => { + beforeEach(async () => { + await mockLockedGold.setTotalLockedGold(value) + await mockValidators.setNumRegisteredValidators(1) + }) + + describe('when the voter can vote for an additional group', () => { + describe('when the voter has sufficient non-voting balance', () => { + let resp: any + beforeEach(async () => { + await mockLockedGold.incrementNonvotingAccountBalance(voter, value) + resp = await election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS) + }) + + it('should add the group to the list of groups the account has voted for', async () => { + assert.deepEqual(await election.getGroupsVotedForByAccount(voter), [group]) + }) + + it("should increment the account's pending votes for the group", async () => { + assertEqualBN(await election.getPendingVotesForGroupByAccount(group, voter), value) + }) + + it("should increment the account's total votes for the group", async () => { + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter), value) + }) + + it("should increment the account's total votes", async () => { + assertEqualBN(await election.getTotalVotesByAccount(voter), value) + }) + + it('should increment the total votes for the group', async () => { + assertEqualBN(await election.getTotalVotesForGroup(group), value) + }) + + it('should increment the total votes', async () => { + assertEqualBN(await election.getTotalVotes(), value) + }) + + it("should decrement the account's nonvoting locked gold balance", async () => { + assertEqualBN(await mockLockedGold.nonvotingAccountBalance(voter), 0) + }) + + it('should emit the ValidatorGroupVoteCast event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupVoteCast', + args: { + account: voter, + group, + value: new BigNumber(value), + }, + }) + }) + }) + + describe('when the voter does not have sufficient non-voting balance', () => { + beforeEach(async () => { + await mockLockedGold.incrementNonvotingAccountBalance(voter, value.minus(1)) + }) + + it('should revert', async () => { + await assertRevert(election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + }) + + describe('when the voter cannot vote for an additional group', () => { + let newGroup: string + beforeEach(async () => { + await mockLockedGold.incrementNonvotingAccountBalance(voter, value) + for (let i = 0; i < maxNumGroupsVotedFor.toNumber(); i++) { + newGroup = accounts[i + 2] + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(newGroup, group, NULL_ADDRESS) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + await election.vote(newGroup, 1, group, NULL_ADDRESS) + } + }) + + it('should revert', async () => { + await assertRevert( + election.vote(group, value.minus(maxNumGroupsVotedFor), newGroup, NULL_ADDRESS) + ) + }) + }) + }) + + describe('when the group cannot receive votes', () => { + beforeEach(async () => { + await mockLockedGold.setTotalLockedGold(value.div(2).minus(1)) + await mockValidators.setMembers(group, [accounts[9]]) + await mockValidators.setNumRegisteredValidators(1) + assertEqualBN(await election.getNumVotesReceivable(group), value.minus(2)) + }) + + it('should revert', async () => { + await assertRevert(election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + }) + + describe('when the group is not eligible', () => { + it('should revert', async () => { + await assertRevert(election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS)) + }) + }) + }) + + describe('#activate', () => { + const voter = accounts[0] + const group = accounts[1] + const value = 1000 + beforeEach(async () => { + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + await mockLockedGold.setTotalLockedGold(value) + await mockValidators.setMembers(group, [accounts[9]]) + await mockValidators.setNumRegisteredValidators(1) + await mockLockedGold.incrementNonvotingAccountBalance(voter, value) + }) + + describe('when the voter has pending votes', () => { + beforeEach(async () => { + await election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS) + }) + + describe('when an epoch boundary has passed since the pending votes were made', () => { + let resp: any + beforeEach(async () => { + await mineBlocks(EPOCH, web3) + resp = await election.activate(group) + }) + + it("should decrement the account's pending votes for the group", async () => { + assertEqualBN(await election.getPendingVotesForGroupByAccount(group, voter), 0) + }) + + it("should increment the account's active votes for the group", async () => { + assertEqualBN(await election.getActiveVotesForGroupByAccount(group, voter), value) + }) + + it("should not modify the account's total votes for the group", async () => { + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter), value) + }) + + it("should not modify the account's total votes", async () => { + assertEqualBN(await election.getTotalVotesByAccount(voter), value) + }) + + it('should not modify the total votes for the group', async () => { + assertEqualBN(await election.getTotalVotesForGroup(group), value) + }) + + it('should not modify the total votes', async () => { + assertEqualBN(await election.getTotalVotes(), value) + }) + + it('should emit the ValidatorGroupVoteActivated event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupVoteActivated', + args: { + account: voter, + group, + value: new BigNumber(value), + }, + }) + }) + + describe('when another voter activates votes', () => { + const voter2 = accounts[2] + const value2 = 573 + beforeEach(async () => { + await mockLockedGold.incrementNonvotingAccountBalance(voter2, value2) + await election.vote(group, value2, NULL_ADDRESS, NULL_ADDRESS, { from: voter2 }) + await mineBlocks(EPOCH, web3) + await election.activate(group, { from: voter2 }) + }) + + it("should not modify the first account's active votes for the group", async () => { + assertEqualBN(await election.getActiveVotesForGroupByAccount(group, voter), value) + }) + + it("should not modify the first account's total votes for the group", async () => { + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter), value) + }) + + it("should not modify the first account's total votes", async () => { + assertEqualBN(await election.getTotalVotesByAccount(voter), value) + }) + + it("should decrement the second account's pending votes for the group", async () => { + assertEqualBN(await election.getPendingVotesForGroupByAccount(group, voter2), 0) + }) + + it("should increment the second account's active votes for the group", async () => { + assertEqualBN(await election.getActiveVotesForGroupByAccount(group, voter2), value2) + }) + + it("should not modify the second account's total votes for the group", async () => { + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter2), value2) + }) + + it("should not modify the second account's total votes", async () => { + assertEqualBN(await election.getTotalVotesByAccount(voter2), value2) + }) + + it('should not modify the total votes for the group', async () => { + assertEqualBN(await election.getTotalVotesForGroup(group), value + value2) + }) + + it('should not modify the total votes', async () => { + assertEqualBN(await election.getTotalVotes(), value + value2) + }) + }) + }) + + describe('when an epoch boundary has not passed since the pending votes were made', () => { + it('should revert', async () => { + await assertRevert(election.activate(group)) + }) + }) + }) + + describe('when the voter does not have pending votes', () => { + it('should revert', async () => { + await assertRevert(election.activate(group)) + }) + }) + }) + + describe('#revokePending', () => { + const voter = accounts[0] + const group = accounts[1] + const value = 1000 + describe('when the voter has pending votes', () => { + beforeEach(async () => { + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + await mockLockedGold.setTotalLockedGold(value) + await mockValidators.setNumRegisteredValidators(1) + await mockLockedGold.incrementNonvotingAccountBalance(voter, value) + await election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS) + }) + + describe('when the revoked value is less than the pending votes', () => { + const index = 0 + const revokedValue = value - 1 + const remaining = value - revokedValue + let resp: any + beforeEach(async () => { + resp = await election.revokePending( + group, + revokedValue, + NULL_ADDRESS, + NULL_ADDRESS, + index + ) + }) + + it("should decrement the account's pending votes for the group", async () => { + assertEqualBN(await election.getPendingVotesForGroupByAccount(group, voter), remaining) + }) + + it("should decrement the account's total votes for the group", async () => { + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter), remaining) + }) + + it("should decrement the account's total votes", async () => { + assertEqualBN(await election.getTotalVotesByAccount(voter), remaining) + }) + + it('should decrement the total votes for the group', async () => { + assertEqualBN(await election.getTotalVotesForGroup(group), remaining) + }) + + it('should decrement the total votes', async () => { + assertEqualBN(await election.getTotalVotes(), remaining) + }) + + it("should increment the account's nonvoting locked gold balance", async () => { + assertEqualBN(await mockLockedGold.nonvotingAccountBalance(voter), revokedValue) + }) + + it('should emit the ValidatorGroupVoteRevoked event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupVoteRevoked', + args: { + account: voter, + group, + value: new BigNumber(revokedValue), + }, + }) + }) + }) + + describe('when the revoked value is equal to the pending votes', () => { + describe('when the correct index is provided', () => { + const index = 0 + beforeEach(async () => { + await election.revokePending(group, value, NULL_ADDRESS, NULL_ADDRESS, index) + }) + + it('should remove the group to the list of groups the account has voted for', async () => { + assert.deepEqual(await election.getGroupsVotedForByAccount(voter), []) + }) + }) + + describe('when the wrong index is provided', () => { + const index = 1 + it('should revert', async () => { + await assertRevert( + election.revokePending(group, value, NULL_ADDRESS, NULL_ADDRESS, index) + ) + }) + }) + }) + + describe('when the revoked value is greater than the pending votes', () => { + const index = 0 + it('should revert', async () => { + await assertRevert( + election.revokePending(group, value + 1, NULL_ADDRESS, NULL_ADDRESS, index) + ) + }) + }) + }) + }) + + describe('#revokeActive', () => { + const voter = accounts[0] + const group = accounts[1] + const value = 1000 + describe('when the voter has active votes', () => { + beforeEach(async () => { + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + await mockLockedGold.setTotalLockedGold(value) + await mockValidators.setNumRegisteredValidators(1) + await mockLockedGold.incrementNonvotingAccountBalance(voter, value) + await election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS) + await mineBlocks(EPOCH, web3) + await election.activate(group) + }) + + describe('when the revoked value is less than the active votes', () => { + const index = 0 + const revokedValue = value - 1 + const remaining = value - revokedValue + let resp: any + beforeEach(async () => { + resp = await election.revokeActive(group, revokedValue, NULL_ADDRESS, NULL_ADDRESS, index) + }) + + it("should decrement the account's active votes for the group", async () => { + assertEqualBN(await election.getActiveVotesForGroupByAccount(group, voter), remaining) + }) + + it("should decrement the account's total votes for the group", async () => { + assertEqualBN(await election.getTotalVotesForGroupByAccount(group, voter), remaining) + }) + + it("should decrement the account's total votes", async () => { + assertEqualBN(await election.getTotalVotesByAccount(voter), remaining) + }) + + it('should decrement the total votes for the group', async () => { + assertEqualBN(await election.getTotalVotesForGroup(group), remaining) + }) + + it('should decrement the total votes', async () => { + assertEqualBN(await election.getTotalVotes(), remaining) + }) + + it("should increment the account's nonvoting locked gold balance", async () => { + assertEqualBN(await mockLockedGold.nonvotingAccountBalance(voter), revokedValue) + }) + + it('should emit the ValidatorGroupVoteRevoked event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupVoteRevoked', + args: { + account: voter, + group, + value: new BigNumber(revokedValue), + }, + }) + }) + }) + + describe('when the revoked value is equal to the active votes', () => { + describe('when the correct index is provided', () => { + const index = 0 + beforeEach(async () => { + await election.revokeActive(group, value, NULL_ADDRESS, NULL_ADDRESS, index) + }) + + it('should remove the group to the list of groups the account has voted for', async () => { + assert.deepEqual(await election.getGroupsVotedForByAccount(voter), []) + }) + }) + + describe('when the wrong index is provided', () => { + const index = 1 + it('should revert', async () => { + await assertRevert( + election.revokeActive(group, value, NULL_ADDRESS, NULL_ADDRESS, index) + ) + }) + }) + }) + + describe('when the revoked value is greater than the active votes', () => { + const index = 0 + it('should revert', async () => { + await assertRevert( + election.revokeActive(group, value + 1, NULL_ADDRESS, NULL_ADDRESS, index) + ) + }) + }) + }) + }) + + describe('#electValidators', () => { + let random: MockRandomInstance + let totalLockedGold: number + const group1 = accounts[0] + const group2 = accounts[1] + const group3 = accounts[2] + const validator1 = accounts[3] + const validator2 = accounts[4] + const validator3 = accounts[5] + const validator4 = accounts[6] + const validator5 = accounts[7] + const validator6 = accounts[8] + const validator7 = accounts[9] + + const hash1 = '0xa5b9d60f32436310afebcfda832817a68921beb782fabf7915cc0460b443116a' + const hash2 = '0xa832817a68921b10afebcfd0460b443116aeb782fabf7915cca5b9d60f324363' + + // If voterN votes for groupN: + // group1 gets 20 votes per member + // group2 gets 25 votes per member + // group3 gets 30 votes per member + // We cannot make any guarantee with respect to their ordering. + const voter1 = { address: accounts[0], weight: 80 } + const voter2 = { address: accounts[1], weight: 50 } + const voter3 = { address: accounts[2], weight: 30 } + totalLockedGold = voter1.weight + voter2.weight + voter3.weight + const assertSameAddresses = (actual: string[], expected: string[]) => { + assert.sameMembers(actual.map((x) => x.toLowerCase()), expected.map((x) => x.toLowerCase())) + } + + beforeEach(async () => { + await mockValidators.setMembers(group1, [validator1, validator2, validator3, validator4]) + await mockValidators.setMembers(group2, [validator5, validator6]) + await mockValidators.setMembers(group3, [validator7]) + + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(group1, NULL_ADDRESS, NULL_ADDRESS) + await election.markGroupEligible(group2, NULL_ADDRESS, group1) + await election.markGroupEligible(group3, NULL_ADDRESS, group2) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + + for (const voter of [voter1, voter2, voter3]) { + await mockLockedGold.incrementNonvotingAccountBalance(voter.address, voter.weight) + } + await mockLockedGold.setTotalLockedGold(totalLockedGold) + await mockValidators.setNumRegisteredValidators(7) + + random = await MockRandom.new() + await registry.setAddressFor(CeloContractName.Random, random.address) + await random.setRandom(hash1) + }) + + describe('when a single group has >= minElectableValidators as members and received votes', () => { + beforeEach(async () => { + await election.vote(group1, voter1.weight, group2, NULL_ADDRESS, { from: voter1.address }) + }) + + it("should return that group's member list", async () => { + assertSameAddresses(await election.electValidators(), [ + validator1, + validator2, + validator3, + validator4, + ]) + }) + }) + + describe("when > maxElectableValidators members' groups receive votes", () => { + beforeEach(async () => { + await election.vote(group1, voter1.weight, group2, NULL_ADDRESS, { from: voter1.address }) + await election.vote(group2, voter2.weight, NULL_ADDRESS, group1, { from: voter2.address }) + await election.vote(group3, voter3.weight, NULL_ADDRESS, group2, { from: voter3.address }) + }) + + it('should return maxElectableValidators elected validators', async () => { + assertSameAddresses(await election.electValidators(), [ + validator1, + validator2, + validator3, + validator5, + validator6, + validator7, + ]) + }) + }) + + describe('when different random values are provided', () => { + beforeEach(async () => { + await election.vote(group1, voter1.weight, group2, NULL_ADDRESS, { from: voter1.address }) + await election.vote(group2, voter2.weight, NULL_ADDRESS, group1, { from: voter2.address }) + await election.vote(group3, voter3.weight, NULL_ADDRESS, group2, { from: voter3.address }) + }) + + it('should return different results', async () => { + await random.setRandom(hash1) + const valsWithHash1 = (await election.electValidators()).map((x) => x.toLowerCase()) + await random.setRandom(hash2) + const valsWithHash2 = (await election.electValidators()).map((x) => x.toLowerCase()) + assert.sameMembers(valsWithHash1, valsWithHash2) + assert.notDeepEqual(valsWithHash1, valsWithHash2) + }) + }) + + describe('when a group receives enough votes for > n seats but only has n members', () => { + beforeEach(async () => { + // By incrementing the total votes by 80, we allow group3 to receive 80 votes from voter3. + const increment = 80 + const votes = 80 + await mockLockedGold.incrementNonvotingAccountBalance(voter3.address, increment) + await mockLockedGold.setTotalLockedGold(totalLockedGold + increment) + await election.vote(group3, votes, group2, NULL_ADDRESS, { from: voter3.address }) + await election.vote(group1, voter1.weight, NULL_ADDRESS, group3, { from: voter1.address }) + await election.vote(group2, voter2.weight, NULL_ADDRESS, group1, { from: voter2.address }) + }) + + it('should elect only n members from that group', async () => { + assertSameAddresses(await election.electValidators(), [ + validator7, + validator1, + validator2, + validator3, + validator5, + validator6, + ]) + }) + }) + + describe('when a group does not receive `electabilityThresholdVotes', () => { + beforeEach(async () => { + const thresholdExcludingGroup3 = (voter3.weight + 1) / totalLockedGold + await election.setElectabilityThreshold(toFixed(thresholdExcludingGroup3)) + await election.vote(group1, voter1.weight, group2, NULL_ADDRESS, { from: voter1.address }) + await election.vote(group2, voter2.weight, NULL_ADDRESS, group1, { from: voter2.address }) + await election.vote(group3, voter3.weight, NULL_ADDRESS, group2, { from: voter3.address }) + }) + + it('should not elect any members from that group', async () => { + assertSameAddresses(await election.electValidators(), [ + validator1, + validator2, + validator3, + validator4, + validator5, + validator6, + ]) + }) + }) + + describe('when there are not enough electable validators', () => { + beforeEach(async () => { + await election.vote(group2, voter2.weight, group1, NULL_ADDRESS, { from: voter2.address }) + await election.vote(group3, voter3.weight, NULL_ADDRESS, group2, { from: voter3.address }) + }) + + it('should revert', async () => { + await assertRevert(election.electValidators()) + }) + }) + }) + + describe('#getGroupEpochRewards', () => { + const voter = accounts[0] + const group1 = accounts[1] + const group2 = accounts[2] + const voteValue1 = new BigNumber(2000000) + const voteValue2 = new BigNumber(1000000) + const totalRewardValue = new BigNumber(3000000) + const balanceRequirement = new BigNumber(1000000) + beforeEach(async () => { + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(group1, NULL_ADDRESS, NULL_ADDRESS) + await election.markGroupEligible(group2, NULL_ADDRESS, group1) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + await mockLockedGold.setTotalLockedGold(voteValue1.plus(voteValue2)) + await mockValidators.setMembers(group1, [accounts[8]]) + await mockValidators.setMembers(group2, [accounts[9]]) + await mockValidators.setNumRegisteredValidators(2) + await mockLockedGold.incrementNonvotingAccountBalance(voter, voteValue1.plus(voteValue2)) + await election.vote(group1, voteValue1, group2, NULL_ADDRESS) + await election.vote(group2, voteValue2, NULL_ADDRESS, group1) + await mockValidators.setAccountBalanceRequirement(group1, balanceRequirement) + await mockValidators.setAccountBalanceRequirement(group2, balanceRequirement) + }) + + describe('when one group has active votes', () => { + beforeEach(async () => { + await mineBlocks(EPOCH, web3) + await election.activate(group1) + }) + + describe('when the group meets the balance requirements ', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(group1, balanceRequirement) + }) + + it('should return the total reward value', async () => { + assertEqualBN( + await election.getGroupEpochRewards(group1, totalRewardValue), + totalRewardValue + ) + }) + }) + + describe('when the group does not meet the balance requirements ', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(group1, balanceRequirement.minus(1)) + }) + + it('should return zero', async () => { + assertEqualBN(await election.getGroupEpochRewards(group1, totalRewardValue), 0) + }) + }) + }) + + describe('when two groups have active votes', () => { + const balanceRequirement = new BigNumber(1000000) + const expectedGroup1EpochRewards = voteValue1 + .div(voteValue1.plus(voteValue2)) + .times(totalRewardValue) + .dp(0) + beforeEach(async () => { + await mineBlocks(EPOCH, web3) + await election.activate(group1) + await election.activate(group2) + }) + + describe('when one group meets the balance requirements ', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(group1, balanceRequirement) + }) + + it('should return the proportional reward value for that group', async () => { + assertEqualBN( + await election.getGroupEpochRewards(group1, totalRewardValue), + expectedGroup1EpochRewards + ) + }) + + it('should return zero for the other group', async () => { + assertEqualBN(await election.getGroupEpochRewards(group2, totalRewardValue), 0) + }) + }) + }) + + describe('when the group does not have active votes', () => { + describe('when the group meets the balance requirements ', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(group1, balanceRequirement) + }) + + it('should return zero', async () => { + assertEqualBN(await election.getGroupEpochRewards(group1, totalRewardValue), 0) + }) + }) + }) + }) + + describe('#distributeEpochRewards', () => { + const voter = accounts[0] + const group = accounts[1] + const voteValue = new BigNumber(1000000) + const rewardValue = new BigNumber(1000000) + beforeEach(async () => { + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(group, NULL_ADDRESS, NULL_ADDRESS) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + await mockLockedGold.setTotalLockedGold(voteValue) + await mockValidators.setMembers(group, [accounts[9]]) + await mockValidators.setNumRegisteredValidators(1) + await mockLockedGold.incrementNonvotingAccountBalance(voter, voteValue) + await election.vote(group, voteValue, NULL_ADDRESS, NULL_ADDRESS) + await mineBlocks(EPOCH, web3) + await election.activate(group) + }) + + describe('when there is a single group with active votes', () => { + describe('when the group is eligible', () => { + beforeEach(async () => { + await election.distributeEpochRewards(group, rewardValue, NULL_ADDRESS, NULL_ADDRESS) + }) + + it("should increment the account's active votes for the group", async () => { + assertEqualBN( + await election.getActiveVotesForGroupByAccount(group, voter), + voteValue.plus(rewardValue) + ) + }) + + it("should increment the account's total votes for the group", async () => { + assertEqualBN( + await election.getTotalVotesForGroupByAccount(group, voter), + voteValue.plus(rewardValue) + ) + }) + + it("should increment account's total votes", async () => { + assertEqualBN(await election.getTotalVotesByAccount(voter), voteValue.plus(rewardValue)) + }) + + it('should increment the total votes for the group', async () => { + assertEqualBN(await election.getTotalVotesForGroup(group), voteValue.plus(rewardValue)) + }) + + it('should increment the total votes', async () => { + assertEqualBN(await election.getTotalVotes(), voteValue.plus(rewardValue)) + }) + }) + }) + + describe('when there are two groups with active votes', () => { + const voter2 = accounts[2] + const group2 = accounts[3] + const voteValue2 = new BigNumber(1000000) + const rewardValue2 = new BigNumber(10000000) + beforeEach(async () => { + await registry.setAddressFor(CeloContractName.Validators, accounts[0]) + await election.markGroupEligible(group2, NULL_ADDRESS, group) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + await mockLockedGold.setTotalLockedGold(voteValue.plus(voteValue2)) + await mockValidators.setNumRegisteredValidators(2) + await mockLockedGold.incrementNonvotingAccountBalance(voter2, voteValue2) + // Split voter2's vote between the two groups. + await election.vote(group, voteValue2.div(2), group2, NULL_ADDRESS, { from: voter2 }) + await election.vote(group2, voteValue2.div(2), NULL_ADDRESS, group, { from: voter2 }) + await mineBlocks(EPOCH, web3) + await election.activate(group, { from: voter2 }) + await election.activate(group2, { from: voter2 }) + }) + + describe('when boths groups are eligible', () => { + const expectedGroupTotalActiveVotes = voteValue.plus(voteValue2.div(2)).plus(rewardValue) + const expectedVoterActiveVotesForGroup = expectedGroupTotalActiveVotes + .times(2) + .div(3) + .dp(0, BigNumber.ROUND_FLOOR) + const expectedVoter2ActiveVotesForGroup = expectedGroupTotalActiveVotes + .div(3) + .dp(0, BigNumber.ROUND_FLOOR) + const expectedVoter2ActiveVotesForGroup2 = voteValue2.div(2).plus(rewardValue2) + beforeEach(async () => { + await election.distributeEpochRewards(group, rewardValue, group2, NULL_ADDRESS) + await election.distributeEpochRewards(group2, rewardValue2, group, NULL_ADDRESS) + }) + + it("should increment the accounts' active votes for both groups", async () => { + assertEqualBN( + await election.getActiveVotesForGroupByAccount(group, voter), + expectedVoterActiveVotesForGroup + ) + assertEqualBN( + await election.getActiveVotesForGroupByAccount(group, voter2), + expectedVoter2ActiveVotesForGroup + ) + assertEqualBN( + await election.getActiveVotesForGroupByAccount(group2, voter2), + expectedVoter2ActiveVotesForGroup2 + ) + }) + + it("should increment the accounts' total votes for both groups", async () => { + assertEqualBN( + await election.getTotalVotesForGroupByAccount(group, voter), + expectedVoterActiveVotesForGroup + ) + assertEqualBN( + await election.getTotalVotesForGroupByAccount(group, voter2), + expectedVoter2ActiveVotesForGroup + ) + assertEqualBN( + await election.getTotalVotesForGroupByAccount(group2, voter2), + expectedVoter2ActiveVotesForGroup2 + ) + }) + + it("should increment the accounts' total votes", async () => { + assertEqualBN( + await election.getTotalVotesByAccount(voter), + expectedVoterActiveVotesForGroup + ) + assertEqualBN( + await election.getTotalVotesByAccount(voter2), + expectedVoter2ActiveVotesForGroup.plus(expectedVoter2ActiveVotesForGroup2) + ) + }) + + it('should increment the total votes for the groups', async () => { + assertEqualBN(await election.getTotalVotesForGroup(group), expectedGroupTotalActiveVotes) + assertEqualBN( + await election.getTotalVotesForGroup(group2), + expectedVoter2ActiveVotesForGroup2 + ) + }) + + it('should increment the total votes', async () => { + assertEqualBN( + await election.getTotalVotes(), + expectedGroupTotalActiveVotes.plus(expectedVoter2ActiveVotesForGroup2) + ) + }) + + it('should update the ordering of the eligible groups', async () => { + assert.deepEqual(await election.getEligibleValidatorGroups(), [group2, group]) + }) + }) + }) + }) +}) diff --git a/packages/protocol/test/governance/governance.ts b/packages/protocol/test/governance/governance.ts index a72f1132eed..d4e11d534f7 100644 --- a/packages/protocol/test/governance/governance.ts +++ b/packages/protocol/test/governance/governance.ts @@ -109,8 +109,8 @@ contract('Governance', (accounts: string[]) => { baselineQuorumFactor ) await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) - await mockLockedGold.setWeight(account, weight) - await mockLockedGold.setTotalWeight(weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) + await mockLockedGold.setTotalLockedGold(weight) transactionSuccess1 = { value: 0, destination: testTransactions.address, @@ -920,7 +920,7 @@ contract('Governance', (accounts: string[]) => { describe('#upvote()', () => { const proposalId = new BigNumber(1) beforeEach(async () => { - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.propose( [transactionSuccess1.value], [transactionSuccess1.destination], @@ -938,7 +938,9 @@ contract('Governance', (accounts: string[]) => { it('should mark the account as having upvoted the proposal', async () => { await governance.upvote(proposalId, 0, 0) - assertEqualBN(await governance.getUpvotedProposal(account), proposalId) + const [recordId, recordWeight] = await governance.getUpvoteRecord(account) + assertEqualBN(recordId, proposalId) + assertEqualBN(recordWeight, weight) }) it('should return true', async () => { @@ -960,16 +962,6 @@ contract('Governance', (accounts: string[]) => { }) }) - it('should revert when the account is frozen', async () => { - await mockLockedGold.setVotingFrozen(account) - await assertRevert(governance.upvote(proposalId, 0, 0)) - }) - - it('should revert when the account weight is 0', async () => { - await mockLockedGold.setWeight(account, 0) - await assertRevert(governance.upvote(proposalId, 0, 0)) - }) - it('should revert when upvoting a proposal that is not queued', async () => { await assertRevert(governance.upvote(proposalId.plus(1), 0, 0)) }) @@ -1009,8 +1001,7 @@ contract('Governance', (accounts: string[]) => { { value: minDeposit } ) const otherAccount = accounts[1] - await mockLockedGold.setWeight(otherAccount, weight) - await mockLockedGold.setTotalWeight(weight * 2) + await mockLockedGold.setAccountTotalLockedGold(otherAccount, weight) await governance.upvote(otherProposalId, proposalId, 0, { from: otherAccount }) await timeTravel(queueExpiry, web3) }) @@ -1069,12 +1060,73 @@ contract('Governance', (accounts: string[]) => { await assertRevert(governance.upvote(proposalId, 0, 0)) }) }) + + describe('when the previously upvoted proposal is in the queue and expired', () => { + const upvotedProposalId = 2 + // Expire the upvoted proposal without dequeueing it. + const queueExpiry = 60 + beforeEach(async () => { + await governance.setQueueExpiry(60) + await governance.upvote(proposalId, 0, 0) + await timeTravel(queueExpiry, web3) + await governance.propose( + [transactionSuccess1.value], + [transactionSuccess1.destination], + transactionSuccess1.data, + [transactionSuccess1.data.length], + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + { value: minDeposit } + ) + }) + + it('should increase the number of upvotes for the proposal', async () => { + await governance.upvote(upvotedProposalId, 0, 0) + assertEqualBN(await governance.getUpvotes(upvotedProposalId), weight) + }) + + it('should mark the account as having upvoted the proposal', async () => { + await governance.upvote(upvotedProposalId, 0, 0) + const [recordId, recordWeight] = await governance.getUpvoteRecord(account) + assertEqualBN(recordId, upvotedProposalId) + assertEqualBN(recordWeight, weight) + }) + + it('should return true', async () => { + const success = await governance.upvote.call(upvotedProposalId, 0, 0) + assert.isTrue(success) + }) + + it('should emit the ProposalExpired event', async () => { + const resp = await governance.upvote(upvotedProposalId, 0, 0) + assert.equal(resp.logs.length, 2) + const log = resp.logs[0] + assertLogMatches2(log, { + event: 'ProposalExpired', + args: { + proposalId: new BigNumber(proposalId), + }, + }) + }) + it('should emit the ProposalUpvoted event', async () => { + const resp = await governance.upvote(upvotedProposalId, 0, 0) + assert.equal(resp.logs.length, 2) + const log = resp.logs[1] + assertLogMatches2(log, { + event: 'ProposalUpvoted', + args: { + proposalId: new BigNumber(upvotedProposalId), + account, + upvotes: new BigNumber(weight), + }, + }) + }) + }) }) describe('#revokeUpvote()', () => { const proposalId = new BigNumber(1) beforeEach(async () => { - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.propose( [transactionSuccess1.value], [transactionSuccess1.destination], @@ -1098,12 +1150,9 @@ contract('Governance', (accounts: string[]) => { it('should mark the account as not having upvoted a proposal', async () => { await governance.revokeUpvote(0, 0) - assertEqualBN(await governance.getUpvotedProposal(account), 0) - }) - - it('should succeed when the account is frozen', async () => { - await mockLockedGold.setVotingFrozen(account) - await governance.revokeUpvote(0, 0) + const [recordId, recordWeight] = await governance.getUpvoteRecord(account) + assertEqualBN(recordId, 0) + assertEqualBN(recordWeight, 0) }) it('should emit the ProposalUpvoteRevoked event', async () => { @@ -1125,11 +1174,6 @@ contract('Governance', (accounts: string[]) => { await assertRevert(governance.revokeUpvote(0, 0)) }) - it('should revert when the account weight is 0', async () => { - await mockLockedGold.setWeight(account, 0) - await assertRevert(governance.revokeUpvote(0, 0)) - }) - describe('when the upvoted proposal has expired', () => { beforeEach(async () => { await timeTravel(queueExpiry, web3) @@ -1145,7 +1189,9 @@ contract('Governance', (accounts: string[]) => { it('should mark the account as not having upvoted a proposal', async () => { await governance.revokeUpvote(0, 0) - assertEqualBN(await governance.getUpvotedProposal(account), 0) + const [recordId, recordWeight] = await governance.getUpvoteRecord(account) + assertEqualBN(recordId, 0) + assertEqualBN(recordWeight, 0) }) it('should emit the ProposalExpired event', async () => { @@ -1175,7 +1221,9 @@ contract('Governance', (accounts: string[]) => { it('should mark the account as not having upvoted a proposal', async () => { await governance.revokeUpvote(0, 0) - assertEqualBN(await governance.getUpvotedProposal(account), 0) + const [recordId, recordWeight] = await governance.getUpvoteRecord(account) + assertEqualBN(recordId, 0) + assertEqualBN(recordWeight, 0) }) }) }) @@ -1359,7 +1407,7 @@ contract('Governance', (accounts: string[]) => { await timeTravel(dequeueFrequency, web3) await governance.approve(proposalId, index) await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) }) it('should return true', async () => { @@ -1403,13 +1451,8 @@ contract('Governance', (accounts: string[]) => { }) }) - it('should revert when the account is frozen', async () => { - await mockLockedGold.setVotingFrozen(account) - await assertRevert(governance.vote(proposalId, index, value)) - }) - it('should revert when the account weight is 0', async () => { - await mockLockedGold.setWeight(account, 0) + await mockLockedGold.setAccountTotalLockedGold(account, 0) await assertRevert(governance.vote(proposalId, index, value)) }) @@ -1552,7 +1595,7 @@ contract('Governance', (accounts: string[]) => { await timeTravel(dequeueFrequency, web3) await governance.approve(proposalId, index) await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.vote(proposalId, index, value) await timeTravel(referendumStageDuration, web3) }) @@ -1616,7 +1659,7 @@ contract('Governance', (accounts: string[]) => { await timeTravel(dequeueFrequency, web3) await governance.approve(proposalId, index) await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.vote(proposalId, index, value) await timeTravel(referendumStageDuration, web3) }) @@ -1639,7 +1682,7 @@ contract('Governance', (accounts: string[]) => { await timeTravel(dequeueFrequency, web3) await governance.approve(proposalId, index) await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.vote(proposalId, index, value) await timeTravel(referendumStageDuration, web3) }) @@ -1665,7 +1708,7 @@ contract('Governance', (accounts: string[]) => { await timeTravel(dequeueFrequency, web3) await governance.approve(proposalId, index) await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.vote(proposalId, index, value) await timeTravel(referendumStageDuration, web3) }) @@ -1732,7 +1775,7 @@ contract('Governance', (accounts: string[]) => { await timeTravel(dequeueFrequency, web3) await governance.approve(proposalId, index) await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.vote(proposalId, index, value) await timeTravel(referendumStageDuration, web3) }) @@ -1756,7 +1799,7 @@ contract('Governance', (accounts: string[]) => { await timeTravel(dequeueFrequency, web3) await governance.approve(proposalId, index) await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.vote(proposalId, index, value) await timeTravel(referendumStageDuration, web3) }) @@ -1781,7 +1824,7 @@ contract('Governance', (accounts: string[]) => { await timeTravel(dequeueFrequency, web3) await governance.approve(proposalId, index) await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.vote(proposalId, index, value) await timeTravel(referendumStageDuration, web3) await timeTravel(executionStageDuration, web3) @@ -1829,6 +1872,7 @@ contract('Governance', (accounts: string[]) => { }) }) + /* describe('#isVoting()', () => { describe('when the account has never acted on a proposal', () => { it('should return false', async () => { @@ -1839,7 +1883,7 @@ contract('Governance', (accounts: string[]) => { describe('when the account has upvoted a proposal', () => { const proposalId = 1 beforeEach(async () => { - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.propose( [transactionSuccess1.value], [transactionSuccess1.destination], @@ -1892,7 +1936,7 @@ contract('Governance', (accounts: string[]) => { await timeTravel(dequeueFrequency, web3) await governance.approve(proposalId, index) await timeTravel(approvalStageDuration, web3) - await mockLockedGold.setWeight(account, weight) + await mockLockedGold.setAccountTotalLockedGold(account, weight) await governance.vote(proposalId, index, value) }) @@ -1911,6 +1955,7 @@ contract('Governance', (accounts: string[]) => { }) }) }) + */ describe('#isProposalPassing()', () => { const proposalId = 1 @@ -1931,8 +1976,8 @@ contract('Governance', (accounts: string[]) => { describe('when the adjusted support is greater than threshold', () => { beforeEach(async () => { - await mockLockedGold.setWeight(account, (weight * 51) / 100) - await mockLockedGold.setWeight(otherAccount, (weight * 49) / 100) + await mockLockedGold.setAccountTotalLockedGold(account, (weight * 51) / 100) + await mockLockedGold.setAccountTotalLockedGold(otherAccount, (weight * 49) / 100) await governance.vote(proposalId, index, VoteValue.Yes) await governance.vote(proposalId, index, VoteValue.No, { from: otherAccount }) }) @@ -1945,8 +1990,8 @@ contract('Governance', (accounts: string[]) => { describe('when the adjusted support is less than or equal to threshold', () => { beforeEach(async () => { - await mockLockedGold.setWeight(account, (weight * 50) / 100) - await mockLockedGold.setWeight(otherAccount, (weight * 50) / 100) + await mockLockedGold.setAccountTotalLockedGold(account, (weight * 50) / 100) + await mockLockedGold.setAccountTotalLockedGold(otherAccount, (weight * 50) / 100) await governance.vote(proposalId, index, VoteValue.Yes) await governance.vote(proposalId, index, VoteValue.No, { from: otherAccount }) }) diff --git a/packages/protocol/test/governance/lockedgold.ts b/packages/protocol/test/governance/lockedgold.ts new file mode 100644 index 00000000000..aa0cc353172 --- /dev/null +++ b/packages/protocol/test/governance/lockedgold.ts @@ -0,0 +1,491 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { getParsedSignatureOfAddress } from '@celo/protocol/lib/signing-utils' +import { + assertEqualBN, + assertLogMatches, + assertLogMatches2, + assertRevert, + timeTravel, +} from '@celo/protocol/lib/test-utils' +import BigNumber from 'bignumber.js' +import { + LockedGoldContract, + LockedGoldInstance, + MockElectionContract, + MockElectionInstance, + MockGoldTokenContract, + MockGoldTokenInstance, + MockValidatorsContract, + MockValidatorsInstance, + RegistryContract, + RegistryInstance, +} from 'types' + +const LockedGold: LockedGoldContract = artifacts.require('LockedGold') +const MockElection: MockElectionContract = artifacts.require('MockElection') +const MockGoldToken: MockGoldTokenContract = artifacts.require('MockGoldToken') +const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') +const Registry: RegistryContract = artifacts.require('Registry') + +// @ts-ignore +// TODO(mcortesi): Use BN +LockedGold.numberFormat = 'BigNumber' + +const HOUR = 60 * 60 +const DAY = 24 * HOUR +let authorizationTests = { voter: {}, validator: {} } + +contract('LockedGold', (accounts: string[]) => { + let account = accounts[0] + const nonOwner = accounts[1] + const unlockingPeriod = 3 * DAY + let lockedGold: LockedGoldInstance + let mockElection: MockElectionInstance + let mockValidators: MockValidatorsInstance + let registry: RegistryInstance + + const capitalize = (s: string) => { + return s.charAt(0).toUpperCase() + s.slice(1) + } + + beforeEach(async () => { + const mockGoldToken: MockGoldTokenInstance = await MockGoldToken.new() + lockedGold = await LockedGold.new() + mockElection = await MockElection.new() + mockValidators = await MockValidators.new() + registry = await Registry.new() + await registry.setAddressFor(CeloContractName.GoldToken, mockGoldToken.address) + await registry.setAddressFor(CeloContractName.Election, mockElection.address) + await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + await lockedGold.initialize(registry.address, unlockingPeriod) + await lockedGold.createAccount() + + authorizationTests.voter = { + fn: lockedGold.authorizeVoter, + getAuthorizedFromAccount: lockedGold.getVoterFromAccount, + getAccountFromActiveAuthorized: lockedGold.getAccountFromActiveVoter, + getAccountFromAuthorized: lockedGold.getAccountFromVoter, + } + authorizationTests.validator = { + fn: lockedGold.authorizeValidator, + getAuthorizedFromAccount: lockedGold.getValidatorFromAccount, + getAccountFromActiveAuthorized: lockedGold.getAccountFromActiveValidator, + getAccountFromAuthorized: lockedGold.getAccountFromValidator, + } + }) + + describe('#initialize()', () => { + it('should set the owner', async () => { + const owner: string = await lockedGold.owner() + assert.equal(owner, account) + }) + + it('should set the registry address', async () => { + const registryAddress: string = await lockedGold.registry() + assert.equal(registryAddress, registry.address) + }) + + it('should set the unlocking period', async () => { + const period = await lockedGold.unlockingPeriod() + assertEqualBN(unlockingPeriod, period) + }) + + it('should revert if already initialized', async () => { + await assertRevert(lockedGold.initialize(registry.address, unlockingPeriod)) + }) + }) + + describe('#setRegistry()', () => { + const anAddress: string = accounts[2] + + it('should set the registry when called by the owner', async () => { + await lockedGold.setRegistry(anAddress) + assert.equal(await lockedGold.registry(), anAddress) + }) + + it('should revert when not called by the owner', async () => { + await assertRevert(lockedGold.setRegistry(anAddress, { from: nonOwner })) + }) + }) + + describe('#setUnlockingPeriod', () => { + const newUnlockingPeriod = unlockingPeriod + 1 + it('should set the unlockingPeriod', async () => { + await lockedGold.setUnlockingPeriod(newUnlockingPeriod) + assertEqualBN(await lockedGold.unlockingPeriod(), newUnlockingPeriod) + }) + + it('should emit the UnlockingPeriodSet event', async () => { + const resp = await lockedGold.setUnlockingPeriod(newUnlockingPeriod) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertLogMatches2(log, { + event: 'UnlockingPeriodSet', + args: { + period: newUnlockingPeriod, + }, + }) + }) + + it('should revert when the unlockingPeriod is unchanged', async () => { + await assertRevert(lockedGold.setUnlockingPeriod(unlockingPeriod)) + }) + + it('should revert when called by anyone other than the owner', async () => { + await assertRevert(lockedGold.setUnlockingPeriod(newUnlockingPeriod, { from: nonOwner })) + }) + }) + + Object.keys(authorizationTests).forEach((key) => { + describe('authorization tests:', () => { + let authorizationTest: any + beforeEach(async () => { + authorizationTest = authorizationTests[key] + }) + + describe(`#authorize${capitalize(key)}()`, () => { + const authorized = accounts[1] + let sig + + beforeEach(async () => { + sig = await getParsedSignatureOfAddress(web3, account, authorized) + }) + + it(`should set the authorized ${key}`, async () => { + await authorizationTest.fn(authorized, sig.v, sig.r, sig.s) + assert.equal(await authorizationTest.getAuthorizedFromAccount(account), authorized) + assert.equal(await authorizationTest.getAccountFromActiveAuthorized(authorized), account) + }) + + it(`should emit a ${capitalize(key)}Authorized event`, async () => { + const resp = await authorizationTest.fn(authorized, sig.v, sig.r, sig.s) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + const expected = { account } + expected[key] = authorized + assertLogMatches(log, `${capitalize(key)}Authorized`, expected) + }) + + it(`should revert if the ${key} is an account`, async () => { + await lockedGold.createAccount({ from: authorized }) + await assertRevert(authorizationTest.fn(authorized, sig.v, sig.r, sig.s)) + }) + + it(`should revert if the ${key} is already authorized`, async () => { + const otherAccount = accounts[2] + const otherSig = await getParsedSignatureOfAddress(web3, otherAccount, authorized) + await lockedGold.createAccount({ from: otherAccount }) + await authorizationTest.fn(authorized, otherSig.v, otherSig.r, otherSig.s, { + from: otherAccount, + }) + await assertRevert(authorizationTest.fn(authorized, sig.v, sig.r, sig.s)) + }) + + it('should revert if the signature is incorrect', async () => { + const nonVoter = accounts[3] + const incorrectSig = await getParsedSignatureOfAddress(web3, account, nonVoter) + await assertRevert( + authorizationTest.fn(authorized, incorrectSig.v, incorrectSig.r, incorrectSig.s) + ) + }) + + describe('when a previous authorization has been made', async () => { + const newAuthorized = accounts[2] + let newSig + beforeEach(async () => { + await authorizationTest.fn(authorized, sig.v, sig.r, sig.s) + newSig = await getParsedSignatureOfAddress(web3, account, newAuthorized) + await authorizationTest.fn(newAuthorized, newSig.v, newSig.r, newSig.s) + }) + + it(`should set the new authorized ${key}`, async () => { + assert.equal(await authorizationTest.getAuthorizedFromAccount(account), newAuthorized) + assert.equal( + await authorizationTest.getAccountFromActiveAuthorized(newAuthorized), + account + ) + }) + + it('should preserve the previous authorization', async () => { + assert.equal(await authorizationTest.getAccountFromAuthorized(authorized), account) + }) + }) + }) + + describe(`#getAccountFrom${capitalize(key)}()`, () => { + describe(`when the account has not authorized a ${key}`, () => { + it('should return the account when passed the account', async () => { + assert.equal(await authorizationTest.getAccountFromActiveAuthorized(account), account) + }) + + it('should revert when passed an address that is not an account', async () => { + await assertRevert(authorizationTest.getAccountFromActiveAuthorized(accounts[1])) + }) + }) + + describe(`when the account has authorized a ${key}`, () => { + const authorized = accounts[1] + beforeEach(async () => { + const sig = await getParsedSignatureOfAddress(web3, account, authorized) + await authorizationTest.fn(authorized, sig.v, sig.r, sig.s) + }) + + it('should return the account when passed the account', async () => { + assert.equal(await authorizationTest.getAccountFromActiveAuthorized(account), account) + }) + + it(`should return the account when passed the ${key}`, async () => { + assert.equal( + await authorizationTest.getAccountFromActiveAuthorized(authorized), + account + ) + }) + }) + }) + + describe(`#get${capitalize(key)}FromAccount()`, () => { + describe(`when the account has not authorized a ${key}`, () => { + it('should return the account when passed the account', async () => { + assert.equal(await authorizationTest.getAuthorizedFromAccount(account), account) + }) + + it('should revert when not passed an account', async () => { + await assertRevert(authorizationTest.getAuthorizedFromAccount(accounts[1]), account) + }) + }) + + describe(`when the account has authorized a ${key}`, () => { + const authorized = accounts[1] + + beforeEach(async () => { + const sig = await getParsedSignatureOfAddress(web3, account, authorized) + await authorizationTest.fn(authorized, sig.v, sig.r, sig.s) + }) + + it(`should return the ${key} when passed the account`, async () => { + assert.equal(await authorizationTest.getAuthorizedFromAccount(account), authorized) + }) + }) + }) + }) + }) + + describe('#lock()', () => { + const value = 1000 + + it("should increase the account's nonvoting locked gold balance", async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ value }) + assertEqualBN(await lockedGold.getAccountNonvotingLockedGold(account), value) + }) + + it("should increase the account's total locked gold balance", async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ value }) + assertEqualBN(await lockedGold.getAccountTotalLockedGold(account), value) + }) + + it('should increase the nonvoting locked gold balance', async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ value }) + assertEqualBN(await lockedGold.getNonvotingLockedGold(), value) + }) + + it('should increase the total locked gold balance', async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ value }) + assertEqualBN(await lockedGold.getTotalLockedGold(), value) + }) + + it('should emit a GoldLocked event', async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + const resp = await lockedGold.lock({ value }) + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertLogMatches(log, 'GoldLocked', { + account, + value: new BigNumber(value), + }) + }) + + it('should revert when the specified value is 0', async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await assertRevert(lockedGold.lock({ value: 0 })) + }) + + it('should revert when the account does not exist', async () => { + await assertRevert(lockedGold.lock({ value, from: accounts[1] })) + }) + }) + + describe('#unlock()', () => { + const value = 1000 + let availabilityTime: BigNumber + let resp: any + describe('when there are no balance requirements', () => { + beforeEach(async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ value }) + resp = await lockedGold.unlock(value) + availabilityTime = new BigNumber(unlockingPeriod).plus( + (await web3.eth.getBlock('latest')).timestamp + ) + }) + + it('should add a pending withdrawal', async () => { + const [values, timestamps] = await lockedGold.getPendingWithdrawals(account) + assert.equal(values.length, 1) + assert.equal(timestamps.length, 1) + assertEqualBN(values[0], value) + assertEqualBN(timestamps[0], availabilityTime) + }) + + it("should decrease the account's nonvoting locked gold balance", async () => { + assertEqualBN(await lockedGold.getAccountNonvotingLockedGold(account), 0) + }) + + it("should decrease the account's total locked gold balance", async () => { + assertEqualBN(await lockedGold.getAccountTotalLockedGold(account), 0) + }) + + it('should decrease the nonvoting locked gold balance', async () => { + assertEqualBN(await lockedGold.getNonvotingLockedGold(), 0) + }) + + it('should decrease the total locked gold balance', async () => { + assertEqualBN(await lockedGold.getTotalLockedGold(), 0) + }) + + it('should emit a GoldUnlocked event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertLogMatches(log, 'GoldUnlocked', { + account, + value: new BigNumber(value), + available: availabilityTime, + }) + }) + }) + + describe('when there are balance requirements', () => { + const balanceRequirement = 10 + beforeEach(async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ value }) + await mockValidators.setAccountBalanceRequirement(account, balanceRequirement) + }) + + describe('when unlocking would yield a locked gold balance less than the required value', () => { + describe('when the the current time is earlier than the requirement time', () => { + it('should revert', async () => { + await assertRevert(lockedGold.unlock(value)) + }) + }) + }) + + describe('when unlocking would yield a locked gold balance equal to the required value', () => { + it('should succeed', async () => { + await lockedGold.unlock(value - balanceRequirement) + }) + }) + }) + }) + + describe('#relock()', () => { + const value = 1000 + const index = 0 + let resp: any + describe('when a pending withdrawal exists', () => { + beforeEach(async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ value }) + await lockedGold.unlock(value) + resp = await lockedGold.relock(index) + }) + + it("should increase the account's nonvoting locked gold balance", async () => { + assertEqualBN(await lockedGold.getAccountNonvotingLockedGold(account), value) + }) + + it("should increase the account's total locked gold balance", async () => { + assertEqualBN(await lockedGold.getAccountTotalLockedGold(account), value) + }) + + it('should increase the nonvoting locked gold balance', async () => { + assertEqualBN(await lockedGold.getNonvotingLockedGold(), value) + }) + + it('should increase the total locked gold balance', async () => { + assertEqualBN(await lockedGold.getTotalLockedGold(), value) + }) + + it('should emit a GoldLocked event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertLogMatches(log, 'GoldLocked', { + account, + value: new BigNumber(value), + }) + }) + + it('should remove the pending withdrawal', async () => { + const [values, timestamps] = await lockedGold.getPendingWithdrawals(account) + assert.equal(values.length, 0) + assert.equal(timestamps.length, 0) + }) + }) + + describe('when a pending withdrawal does not exist', () => { + it('should revert', async () => { + await assertRevert(lockedGold.relock(index)) + }) + }) + }) + + describe('#withdraw()', () => { + const value = 1000 + const index = 0 + let resp: any + describe('when a pending withdrawal exists', () => { + beforeEach(async () => { + // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails + await lockedGold.lock({ value }) + resp = await lockedGold.unlock(value) + }) + + describe('when it is after the availablity time', () => { + beforeEach(async () => { + await timeTravel(unlockingPeriod, web3) + resp = await lockedGold.withdraw(index) + }) + + it('should remove the pending withdrawal', async () => { + const [values, timestamps] = await lockedGold.getPendingWithdrawals(account) + assert.equal(values.length, 0) + assert.equal(timestamps.length, 0) + }) + + it('should emit a GoldWithdrawn event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertLogMatches(log, 'GoldWithdrawn', { + account, + value: new BigNumber(value), + }) + }) + }) + + describe('when it is before the availablity time', () => { + it('should revert', async () => { + await assertRevert(lockedGold.withdraw(index)) + }) + }) + }) + + describe('when a pending withdrawal does not exist', () => { + it('should revert', async () => { + await assertRevert(lockedGold.withdraw(index)) + }) + }) + }) +}) diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 7a637b5c30b..d587f0fe60e 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -2,26 +2,32 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { assertContainSubset, assertEqualBN, + assertEqualBNArray, assertRevert, + assertSameAddress, NULL_ADDRESS, + mineBlocks, } from '@celo/protocol/lib/test-utils' import BigNumber from 'bignumber.js' import { MockLockedGoldContract, MockLockedGoldInstance, - MockRandomContract, - MockRandomInstance, + MockElectionContract, + MockElectionInstance, + MockStableTokenContract, + MockStableTokenInstance, RegistryContract, RegistryInstance, - ValidatorsContract, - ValidatorsInstance, + ValidatorsTestContract, + ValidatorsTestInstance, } from 'types' -import { toFixed } from '@celo/utils/lib/fixidity' +import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' -const Validators: ValidatorsContract = artifacts.require('Validators') +const Validators: ValidatorsTestContract = artifacts.require('ValidatorsTest') const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') +const MockElection: MockElectionContract = artifacts.require('MockElection') +const MockStableToken: MockStableTokenContract = artifacts.require('MockStableToken') const Registry: RegistryContract = artifacts.require('Registry') -const Random: MockRandomContract = artifacts.require('MockRandom') // @ts-ignore // TODO(mcortesi): Use BN @@ -29,28 +35,46 @@ Validators.numberFormat = 'BigNumber' const parseValidatorParams = (validatorParams: any) => { return { - identifier: validatorParams[0], - name: validatorParams[1], - url: validatorParams[2], - publicKeysData: validatorParams[3], - affiliation: validatorParams[4], + name: validatorParams[0], + publicKeysData: validatorParams[1], + affiliation: validatorParams[2], + score: validatorParams[3], } } const parseValidatorGroupParams = (groupParams: any) => { return { - identifier: groupParams[0], - name: groupParams[1], - url: groupParams[2], - members: groupParams[3], + name: groupParams[0], + members: groupParams[1], + commission: groupParams[2], } } +const HOUR = 60 * 60 +const DAY = 24 * HOUR +// Hard coded in ganache. +const EPOCH = 100 + +// TODO(asa): Test epoch payment distribution contract('Validators', (accounts: string[]) => { - let validators: ValidatorsInstance + let validators: ValidatorsTestInstance let registry: RegistryInstance let mockLockedGold: MockLockedGoldInstance - let random: MockRandomInstance + let mockElection: MockElectionInstance + const nonOwner = accounts[1] + + const balanceRequirements = { group: new BigNumber(1000), validator: new BigNumber(100) } + const deregistrationLockups = { + group: new BigNumber(100 * DAY), + validator: new BigNumber(60 * DAY), + } + const validatorScoreParameters = { + exponent: new BigNumber(5), + adjustmentSpeed: toFixed(0.25), + } + const validatorEpochPayment = new BigNumber(10000000000000) + const membershipHistoryLength = new BigNumber(3) + const maxGroupSize = new BigNumber(5) // A random 64 byte hex string. const publicKey = @@ -59,66 +83,43 @@ contract('Validators', (accounts: string[]) => { '4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' const blsPoP = '9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' - const publicKeysData = '0x' + publicKey + blsPublicKey + blsPoP - - const nonOwner = accounts[1] - const minElectableValidators = new BigNumber(4) - const maxElectableValidators = new BigNumber(6) - const registrationRequirement = { value: new BigNumber(100), noticePeriod: new BigNumber(60) } - const electionThreshold = new BigNumber(0) - const maxGroupSize = 10 - const identifier = 'test-identifier' const name = 'test-name' - const url = 'test-url' + const commission = toFixed(1 / 100) beforeEach(async () => { validators = await Validators.new() mockLockedGold = await MockLockedGold.new() - random = await Random.new() + mockElection = await MockElection.new() registry = await Registry.new() await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) - await registry.setAddressFor(CeloContractName.Random, random.address) + await registry.setAddressFor(CeloContractName.Election, mockElection.address) await validators.initialize( registry.address, - minElectableValidators, - maxElectableValidators, - registrationRequirement.value, - registrationRequirement.noticePeriod, - maxGroupSize, - electionThreshold + balanceRequirements.group, + balanceRequirements.validator, + deregistrationLockups.group, + deregistrationLockups.validator, + validatorScoreParameters.exponent, + validatorScoreParameters.adjustmentSpeed, + validatorEpochPayment, + membershipHistoryLength, + maxGroupSize ) }) const registerValidator = async (validator: string) => { - await mockLockedGold.setLockedCommitment( - validator, - registrationRequirement.noticePeriod, - registrationRequirement.value - ) + await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.validator) await validators.registerValidator( - identifier, name, - url, // @ts-ignore bytes type publicKeysData, - [registrationRequirement.noticePeriod], { from: validator } ) } const registerValidatorGroup = async (group: string) => { - await mockLockedGold.setLockedCommitment( - group, - registrationRequirement.noticePeriod, - registrationRequirement.value - ) - await validators.registerValidatorGroup( - identifier, - name, - url, - [registrationRequirement.noticePeriod], - { from: group } - ) + await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group) + await validators.registerValidatorGroup(name, commission, { from: group }) } const registerValidatorGroupWithMembers = async (group: string, members: string[]) => { @@ -126,7 +127,11 @@ contract('Validators', (accounts: string[]) => { for (const validator of members) { await registerValidator(validator) await validators.affiliate(group, { from: validator }) - await validators.addMember(validator, { from: group }) + if (validator == members[0]) { + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: group }) + } else { + await validators.addMember(validator, { from: group }) + } } } @@ -136,398 +141,509 @@ contract('Validators', (accounts: string[]) => { assert.equal(owner, accounts[0]) }) - it('should have set minElectableValidators', async () => { - const actualMinElectableValidators = await validators.minElectableValidators() - assertEqualBN(actualMinElectableValidators, minElectableValidators) + it('should have set the balance requirements', async () => { + const [group, validator] = await validators.getBalanceRequirements() + assertEqualBN(group, balanceRequirements.group) + assertEqualBN(validator, balanceRequirements.validator) + }) + + it('should have set the deregistration lockups', async () => { + const [group, validator] = await validators.getDeregistrationLockups() + assertEqualBN(group, deregistrationLockups.group) + assertEqualBN(validator, deregistrationLockups.validator) }) - it('should have set maxElectableValidators', async () => { - const actualMaxElectableValidators = await validators.maxElectableValidators() - assertEqualBN(actualMaxElectableValidators, maxElectableValidators) + it('should have set the validator score parameters', async () => { + const [exponent, adjustmentSpeed] = await validators.getValidatorScoreParameters() + assertEqualBN(exponent, validatorScoreParameters.exponent) + assertEqualBN(adjustmentSpeed, validatorScoreParameters.adjustmentSpeed) }) - it('should have set the registration requirements', async () => { - const [value, noticePeriod] = await validators.getRegistrationRequirement() - assertEqualBN(value, registrationRequirement.value) - assertEqualBN(noticePeriod, registrationRequirement.noticePeriod) + it('should have set the validator epoch payment', async () => { + const actual = await validators.validatorEpochPayment() + assertEqualBN(actual, validatorEpochPayment) + }) + + it('should have set the membership history length', async () => { + const actual = await validators.membershipHistoryLength() + assertEqualBN(actual, membershipHistoryLength) + }) + + it('should have set the max group size', async () => { + const actualMaxGroupSize = await validators.getMaxGroupSize() + assertEqualBN(actualMaxGroupSize, maxGroupSize) }) it('should not be callable again', async () => { await assertRevert( validators.initialize( registry.address, - minElectableValidators, - maxElectableValidators, - registrationRequirement.value, - registrationRequirement.noticePeriod, - maxGroupSize, - electionThreshold + balanceRequirements.group, + balanceRequirements.validator, + deregistrationLockups.group, + deregistrationLockups.validator, + validatorScoreParameters.exponent, + validatorScoreParameters.adjustmentSpeed, + validatorEpochPayment, + membershipHistoryLength, + maxGroupSize ) ) }) }) - describe('#setElectionThreshold', () => { - it('should set the election threshold', async () => { - const threshold = toFixed(1 / 10) - await validators.setElectionThreshold(threshold) - const result = await validators.getElectionThreshold() - assertEqualBN(result, threshold) - }) + describe('#setValidatorEpochPayment()', () => { + describe('when the payment is different', () => { + const newPayment = validatorEpochPayment.plus(1) - it('should revert when the threshold is larger than 100%', async () => { - const threshold = toFixed(new BigNumber('2')) - await assertRevert(validators.setElectionThreshold(threshold)) - }) - }) + describe('when called by the owner', () => { + let resp: any - describe('#setMinElectableValidators', () => { - const newMinElectableValidators = minElectableValidators.plus(1) - it('should set the minimum elected validators', async () => { - await validators.setMinElectableValidators(newMinElectableValidators) - assertEqualBN(await validators.minElectableValidators(), newMinElectableValidators) - }) + beforeEach(async () => { + resp = await validators.setValidatorEpochPayment(newPayment) + }) - it('should emit the MinElectableValidatorsSet event', async () => { - const resp = await validators.setMinElectableValidators(newMinElectableValidators) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'MinElectableValidatorsSet', - args: { - minElectableValidators: new BigNumber(newMinElectableValidators), - }, + it('should set the validator epoch payment', async () => { + assertEqualBN(await validators.validatorEpochPayment(), newPayment) + }) + + it('should emit the ValidatorEpochPaymentSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorEpochPaymentSet', + args: { + value: new BigNumber(newPayment), + }, + }) + }) + + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + validators.setValidatorEpochPayment(newPayment, { + from: nonOwner, + }) + ) + }) + }) }) - }) - it('should revert when the minElectableValidators is zero', async () => { - await assertRevert(validators.setMinElectableValidators(0)) + describe('when the payment is the same', () => { + it('should revert', async () => { + await assertRevert(validators.setValidatorEpochPayment(validatorEpochPayment)) + }) + }) }) + }) - it('should revert when the minElectableValidators is greater than maxElectableValidators', async () => { - await assertRevert(validators.setMinElectableValidators(maxElectableValidators.plus(1))) - }) + describe('#setMembershipHistoryLength()', () => { + describe('when the length is different', () => { + const newLength = membershipHistoryLength.plus(1) - it('should revert when the minElectableValidators is unchanged', async () => { - await assertRevert(validators.setMinElectableValidators(minElectableValidators)) - }) + describe('when called by the owner', () => { + let resp: any - it('should revert when called by anyone other than the owner', async () => { - await assertRevert( - validators.setMinElectableValidators(newMinElectableValidators, { from: nonOwner }) - ) - }) - }) + beforeEach(async () => { + resp = await validators.setMembershipHistoryLength(newLength) + }) - describe('#setMaxElectableValidators', () => { - const newMaxElectableValidators = maxElectableValidators.plus(1) - it('should set the maximum elected validators', async () => { - await validators.setMaxElectableValidators(newMaxElectableValidators) - assertEqualBN(await validators.maxElectableValidators(), newMaxElectableValidators) - }) + it('should set the membership history length', async () => { + assertEqualBN(await validators.membershipHistoryLength(), newLength) + }) - it('should emit the MaxElectableValidatorsSet event', async () => { - const resp = await validators.setMaxElectableValidators(newMaxElectableValidators) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'MaxElectableValidatorsSet', - args: { - maxElectableValidators: new BigNumber(newMaxElectableValidators), - }, + it('should emit the MembershipHistoryLengthSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'MembershipHistoryLengthSet', + args: { + length: new BigNumber(newLength), + }, + }) + }) + + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + validators.setMembershipHistoryLength(newLength, { + from: nonOwner, + }) + ) + }) + }) }) - }) - it('should revert when the maxElectableValidators is less than minElectableValidators', async () => { - await assertRevert(validators.setMaxElectableValidators(minElectableValidators.minus(1))) + describe('when the length is the same', () => { + it('should revert', async () => { + await assertRevert(validators.setMembershipHistoryLength(membershipHistoryLength)) + }) + }) }) + }) - it('should revert when the maxElectableValidators is unchanged', async () => { - await assertRevert(validators.setMaxElectableValidators(maxElectableValidators)) - }) + describe('#setMaxGroupSize()', () => { + describe('when the group size is different', () => { + const newSize = maxGroupSize.plus(1) - it('should revert when called by anyone other than the owner', async () => { - await assertRevert( - validators.setMaxElectableValidators(newMaxElectableValidators, { from: nonOwner }) - ) + describe('when called by the owner', () => { + let resp: any + + beforeEach(async () => { + resp = await validators.setMaxGroupSize(newSize) + }) + + it('should set the max group size', async () => { + assertEqualBN(await validators.maxGroupSize(), newSize) + }) + + it('should emit the MaxGroupSizeSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'MaxGroupSizeSet', + args: { + size: new BigNumber(newSize), + }, + }) + }) + + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + validators.setMaxGroupSize(newSize, { + from: nonOwner, + }) + ) + }) + }) + }) + + describe('when the size is the same', () => { + it('should revert', async () => { + await assertRevert(validators.setMaxGroupSize(maxGroupSize)) + }) + }) }) }) - describe('#setMaxGroupSize', () => { - const newMaxGroupSize = 11 - it('should set the maximum group size', async () => { - await validators.setMaxGroupSize(newMaxGroupSize) - assertEqualBN(await validators.maxGroupSize(), newMaxGroupSize) - }) + describe('#setBalanceRequirements()', () => { + describe('when the requirements are different', () => { + const newRequirements = { + group: balanceRequirements.group.plus(1), + validator: balanceRequirements.validator.plus(1), + } - it('should emit the MaxElectableValidatorsSet event', async () => { - const resp = await validators.setMaxGroupSize(newMaxGroupSize) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'MaxGroupSizeSet', - args: { - maxGroupSize: new BigNumber(newMaxGroupSize), - }, + describe('when called by the owner', () => { + let resp: any + + beforeEach(async () => { + resp = await validators.setBalanceRequirements( + newRequirements.group, + newRequirements.validator + ) + }) + + it('should set the group and validator requirements', async () => { + const [group, validator] = await validators.getBalanceRequirements() + assertEqualBN(group, newRequirements.group) + assertEqualBN(validator, newRequirements.validator) + }) + + it('should emit the BalanceRequirementsSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'BalanceRequirementsSet', + args: { + group: new BigNumber(newRequirements.group), + validator: new BigNumber(newRequirements.validator), + }, + }) + }) + + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + validators.setBalanceRequirements(newRequirements.group, newRequirements.validator, { + from: nonOwner, + }) + ) + }) + }) }) - }) - it('should revert when called by anyone other than the owner', async () => { - await assertRevert(validators.setMaxGroupSize(newMaxGroupSize, { from: nonOwner })) + describe('when the requirements are the same', () => { + it('should revert', async () => { + await assertRevert( + validators.setBalanceRequirements( + balanceRequirements.group, + balanceRequirements.validator + ) + ) + }) + }) }) }) - describe('#setRegistrationRequirement', () => { - const newValue = registrationRequirement.value.plus(1) - const newNoticePeriod = registrationRequirement.noticePeriod.plus(1) + describe('#setDeregistrationLockups()', () => { + describe('when the lockups are different', () => { + const newLockups = { + group: deregistrationLockups.group.plus(1), + validator: deregistrationLockups.validator.plus(1), + } - it('should set the value and notice period', async () => { - await validators.setRegistrationRequirement(newValue, newNoticePeriod) - const [value, noticePeriod] = await validators.getRegistrationRequirement() - assertEqualBN(value, newValue) - assertEqualBN(noticePeriod, newNoticePeriod) - }) + describe('when called by the owner', () => { + let resp: any - it('should emit the RegistrationRequirementSet event', async () => { - const resp = await validators.setRegistrationRequirement(newValue, newNoticePeriod) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'RegistrationRequirementSet', - args: { - value: new BigNumber(newValue), - noticePeriod: new BigNumber(newNoticePeriod), - }, + beforeEach(async () => { + resp = await validators.setDeregistrationLockups(newLockups.group, newLockups.validator) + }) + + it('should set the group and validator lockups', async () => { + const [group, validator] = await validators.getDeregistrationLockups() + assertEqualBN(group, newLockups.group) + assertEqualBN(validator, newLockups.validator) + }) + + it('should emit the DeregistrationLockupsSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'DeregistrationLockupsSet', + args: { + group: new BigNumber(newLockups.group), + validator: new BigNumber(newLockups.validator), + }, + }) + }) + + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + validators.setDeregistrationLockups(newLockups.group, newLockups.validator, { + from: nonOwner, + }) + ) + }) + }) }) - }) - it('should revert when the requirement is unchanged', async () => { - await assertRevert( - validators.setRegistrationRequirement( - registrationRequirement.value, - registrationRequirement.noticePeriod - ) - ) + describe('when the lockups are the same', () => { + it('should revert', async () => { + await assertRevert( + validators.setDeregistrationLockups( + deregistrationLockups.group, + deregistrationLockups.validator + ) + ) + }) + }) }) + }) - it('should revert when called by anyone other than the owner', async () => { - await assertRevert( - validators.setRegistrationRequirement(newValue, newNoticePeriod, { from: nonOwner }) - ) + describe('#setValidatorScoreParameters()', () => { + describe('when the parameters are different', () => { + const newParameters = { + exponent: validatorScoreParameters.exponent.plus(1), + adjustmentSpeed: validatorScoreParameters.adjustmentSpeed.plus(1), + } + + describe('when called by the owner', () => { + let resp: any + + beforeEach(async () => { + resp = await validators.setValidatorScoreParameters( + newParameters.exponent, + newParameters.adjustmentSpeed + ) + }) + + it('should set the exponent and adjustment speed', async () => { + const [exponent, adjustmentSpeed] = await validators.getValidatorScoreParameters() + assertEqualBN(exponent, newParameters.exponent) + assertEqualBN(adjustmentSpeed, newParameters.adjustmentSpeed) + }) + + it('should emit the ValidatorScoreParametersSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorScoreParametersSet', + args: { + exponent: new BigNumber(newParameters.exponent), + adjustmentSpeed: new BigNumber(newParameters.adjustmentSpeed), + }, + }) + }) + + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert( + validators.setValidatorScoreParameters( + newParameters.exponent, + newParameters.adjustmentSpeed, + { + from: nonOwner, + } + ) + ) + }) + }) + }) + + describe('when the requirements are the same', () => { + it('should revert', async () => { + await assertRevert( + validators.setValidatorScoreParameters( + validatorScoreParameters.exponent, + validatorScoreParameters.adjustmentSpeed + ) + ) + }) + }) }) }) - describe('#registerValidator', () => { - const validator = accounts[0] - beforeEach(async () => { - await mockLockedGold.setLockedCommitment( - validator, - registrationRequirement.noticePeriod, - registrationRequirement.value - ) - }) + describe('#setMaxGroupSize()', () => { + describe('when the size is different', () => { + describe('when called by the owner', () => { + let resp: any + const newSize = maxGroupSize.plus(1) - it('should mark the account as a validator', async () => { - await validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - [registrationRequirement.noticePeriod] - ) - assert.isTrue(await validators.isValidator(validator)) - }) + beforeEach(async () => { + resp = await validators.setMaxGroupSize(newSize) + }) - it('should add the account to the list of validators', async () => { - await validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - [registrationRequirement.noticePeriod] - ) - assert.deepEqual(await validators.getRegisteredValidators(), [validator]) - }) + it('should set the max group size', async () => { + const size = await validators.getMaxGroupSize() + assertEqualBN(size, newSize) + }) - it('should set the validator identifier, name, url, and public key', async () => { - await validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - [registrationRequirement.noticePeriod] - ) - const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) - assert.equal(parsedValidator.identifier, identifier) - assert.equal(parsedValidator.name, name) - assert.equal(parsedValidator.url, url) - assert.equal(parsedValidator.publicKeysData, publicKeysData) - }) - - it('should emit the ValidatorRegistered event', async () => { - const resp = await validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - [registrationRequirement.noticePeriod] - ) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorRegistered', - args: { - validator, - identifier, - name, - url, - publicKeysData, - }, + it('should emit the MaxGroupSizeSet event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'MaxGroupSizeSet', + args: { + size: new BigNumber(newSize), + }, + }) + }) + }) + + describe('when the size is the same', () => { + it('should revert', async () => { + await assertRevert(validators.setMaxGroupSize(maxGroupSize)) + }) }) }) - describe('when multiple commitment notice periods are provided', () => { - it('should accept a sufficient combination of commitments as stake', async () => { - // create registrationRequirement.value different locked commitments each - // with value 1 and unique noticePeriods greater than registrationRequirement.noticePeriod - const commitmentCount = registrationRequirement.value - const noticePeriods = [] - for (let i = 1; i <= commitmentCount.toNumber(); i++) { - const noticePeriod = registrationRequirement.noticePeriod.plus(i) - noticePeriods.push(noticePeriod) - await mockLockedGold.setLockedCommitment(validator, noticePeriod, 1) - } + describe('when called by a non-owner', () => { + it('should revert', async () => { + await assertRevert(validators.setMaxGroupSize(maxGroupSize, { from: nonOwner })) + }) + }) + }) - await validators.registerValidator( - identifier, + describe('#registerValidator', () => { + const validator = accounts[0] + let resp: any + describe('when the account is not a registered validator', () => { + let validatorRegistrationEpochNumber: number + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.validator) + resp = await validators.registerValidator( name, - url, // @ts-ignore bytes type - publicKeysData, - noticePeriods + publicKeysData ) + const blockNumber = (await web3.eth.getBlock('latest')).number + validatorRegistrationEpochNumber = Math.floor(blockNumber / EPOCH) + }) + + it('should mark the account as a validator', async () => { + assert.isTrue(await validators.isValidator(validator)) + }) + + it('should add the account to the list of validators', async () => { assert.deepEqual(await validators.getRegisteredValidators(), [validator]) }) - it('should revert when the combined commitment value is insufficient with all valid notice periods', async () => { - // create registrationRequirement.value - 1 different locked commitments each - // with value 1 and valid noticePeriods - const commitmentCount = registrationRequirement.value.minus(1) - const noticePeriods = [] - for (let i = 1; i <= commitmentCount.toNumber(); i++) { - const noticePeriod = registrationRequirement.noticePeriod.plus(i) - noticePeriods.push(noticePeriod) - await mockLockedGold.setLockedCommitment(validator, noticePeriod, 1) - } + it('should set the validator name and public key', async () => { + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assert.equal(parsedValidator.name, name) + assert.equal(parsedValidator.publicKeysData, publicKeysData) + }) - await assertRevert( - validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - noticePeriods - ) - ) + it('should set account balance requirements', async () => { + const requirement = await validators.getAccountBalanceRequirement(validator) + assertEqualBN(requirement, balanceRequirements.validator) }) - it('should revert when the combined commitment value of valid notice periods is insufficient', async () => { - // create registrationRequirement.value different locked commitments each - // with value 1, but with one noticePeriod that is less than - // registrationRequirement.noticePeriod - const commitmentCount = registrationRequirement.value.minus(1) - const invalidNoticePeriod = registrationRequirement.noticePeriod.minus(1) - const noticePeriods = [invalidNoticePeriod] - await mockLockedGold.setLockedCommitment(validator, invalidNoticePeriod, 1) - for (let i = 1; i < commitmentCount.toNumber(); i++) { - const noticePeriod = registrationRequirement.noticePeriod.plus(i) - noticePeriods.push(noticePeriod) - await mockLockedGold.setLockedCommitment(validator, noticePeriod, 1) - } + it('should set the validator membership history', async () => { + const membershipHistory = await validators.getMembershipHistory(validator) + assertEqualBNArray(membershipHistory[0], [validatorRegistrationEpochNumber]) + assert.deepEqual(membershipHistory[1], [NULL_ADDRESS]) + }) - await assertRevert( - validators.registerValidator( - identifier, + it('should emit the ValidatorRegistered event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorRegistered', + args: { + validator, name, - url, - // @ts-ignore bytes type publicKeysData, - noticePeriods - ) - ) + }, + }) }) }) describe('when the account is already a registered validator', () => { beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.validator) await validators.registerValidator( - identifier, name, - url, // @ts-ignore bytes type - publicKeysData, - [registrationRequirement.noticePeriod] - ) - }) - - it('should revert', async () => { - await assertRevert( - validators.registerValidator( - identifier, - name, - url, - // @ts-ignore bytes type - publicKeysData, - [registrationRequirement.noticePeriod] - ) + publicKeysData ) + assert.deepEqual(await validators.getRegisteredValidators(), [validator]) }) }) - describe('when the account is already a registered validator group', () => { + describe('when the account is already a registered validator', () => { beforeEach(async () => { - await validators.registerValidatorGroup(identifier, name, url, [ - registrationRequirement.noticePeriod, - ]) + await mockLockedGold.setAccountTotalLockedGold(validator, balanceRequirements.group) + await validators.registerValidatorGroup(name, commission) }) it('should revert', async () => { await assertRevert( validators.registerValidator( - identifier, name, - url, // @ts-ignore bytes type - publicKeysData, - [registrationRequirement.noticePeriod] + publicKeysData ) ) }) }) - describe('when the account does not meet the registration requirements', () => { + describe('when the account does not meet the balance requirements', () => { beforeEach(async () => { - await mockLockedGold.setLockedCommitment( + await mockLockedGold.setAccountTotalLockedGold( validator, - registrationRequirement.noticePeriod, - registrationRequirement.value.minus(1) + balanceRequirements.validator.minus(1) ) }) it('should revert', async () => { await assertRevert( validators.registerValidator( - identifier, name, - url, // @ts-ignore bytes type - publicKeysData, - [registrationRequirement.noticePeriod] + publicKeysData ) ) }) @@ -537,35 +653,51 @@ contract('Validators', (accounts: string[]) => { describe('#deregisterValidator', () => { const validator = accounts[0] const index = 0 - beforeEach(async () => { - await registerValidator(validator) - }) + let resp: any + describe('when the account is a registered validator', () => { + beforeEach(async () => { + await registerValidator(validator) + resp = await validators.deregisterValidator(index) + }) - it('should mark the account as not a validator', async () => { - await validators.deregisterValidator(index) - assert.isFalse(await validators.isValidator(validator)) - }) + it('should mark the account as not a validator', async () => { + assert.isFalse(await validators.isValidator(validator)) + }) - it('should remove the account from the list of validators', async () => { - await validators.deregisterValidator(index) - assert.deepEqual(await validators.getRegisteredValidators(), []) - }) + it('should remove the account from the list of validators', async () => { + assert.deepEqual(await validators.getRegisteredValidators(), []) + }) - it('should emit the ValidatorDeregistered event', async () => { - const resp = await validators.deregisterValidator(index) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorDeregistered', - args: { - validator, - }, + it('should preserve account balance requirements', async () => { + const requirement = await validators.getAccountBalanceRequirement(validator) + assertEqualBN(requirement, balanceRequirements.validator) + }) + + it('should set the validator deregistration timestamp', async () => { + const latestTimestamp = (await web3.eth.getBlock('latest')).timestamp + const [groupTimestamp, validatorTimestamp] = await validators.getDeregistrationTimestamps( + validator + ) + assertEqualBN(groupTimestamp, 0) + assertEqualBN(validatorTimestamp, latestTimestamp) + }) + + it('should emit the ValidatorDeregistered event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorDeregistered', + args: { + validator, + }, + }) }) }) describe('when the validator is affiliated with a validator group', () => { const group = accounts[1] beforeEach(async () => { + await registerValidator(validator) await registerValidatorGroup(group) await validators.affiliate(group) }) @@ -585,7 +717,7 @@ contract('Validators', (accounts: string[]) => { describe('when the validator is a member of that group', () => { beforeEach(async () => { - await validators.addMember(validator, { from: group }) + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: group }) }) it('should remove the validator from the group membership list', async () => { @@ -596,7 +728,7 @@ contract('Validators', (accounts: string[]) => { it('should emit the ValidatorGroupMemberRemoved event', async () => { const resp = await validators.deregisterValidator(index) - assert.equal(resp.logs.length, 4) + assert.equal(resp.logs.length, 3) const log = resp.logs[0] assertContainSubset(log, { event: 'ValidatorGroupMemberRemoved', @@ -608,31 +740,9 @@ contract('Validators', (accounts: string[]) => { }) describe('when the validator is the only member of that group', () => { - it('should emit the ValidatorGroupEmptied event', async () => { - const resp = await validators.deregisterValidator(index) - assert.equal(resp.logs.length, 4) - const log = resp.logs[1] - assertContainSubset(log, { - event: 'ValidatorGroupEmptied', - args: { - group, - }, - }) - }) - - describe('when that group has received votes', () => { - beforeEach(async () => { - const voter = accounts[2] - const weight = 10 - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) - }) - - it('should remove the group from the list of electable groups with votes', async () => { - await validators.deregisterValidator(index) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - }) + it('should should mark the group as ineligible for election', async () => { + await validators.deregisterValidator(index) + assert.isTrue(await mockElection.isIneligible(group)) }) }) }) @@ -715,7 +825,7 @@ contract('Validators', (accounts: string[]) => { describe('when the validator is a member of that group', () => { beforeEach(async () => { - await validators.addMember(validator, { from: group }) + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: group }) }) it('should remove the validator from the group membership list', async () => { @@ -726,7 +836,7 @@ contract('Validators', (accounts: string[]) => { it('should emit the ValidatorGroupMemberRemoved event', async () => { const resp = await validators.affiliate(otherGroup) - assert.equal(resp.logs.length, 4) + assert.equal(resp.logs.length, 3) const log = resp.logs[0] assertContainSubset(log, { event: 'ValidatorGroupMemberRemoved', @@ -738,31 +848,9 @@ contract('Validators', (accounts: string[]) => { }) describe('when the validator is the only member of that group', () => { - it('should emit the ValidatorGroupEmptied event', async () => { - const resp = await validators.affiliate(otherGroup) - assert.equal(resp.logs.length, 4) - const log = resp.logs[1] - assertContainSubset(log, { - event: 'ValidatorGroupEmptied', - args: { - group, - }, - }) - }) - - describe('when that group has received votes', () => { - beforeEach(async () => { - const voter = accounts[2] - const weight = 10 - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) - }) - - it('should remove the group from the list of electable groups with votes', async () => { - await validators.affiliate(otherGroup) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - }) + it('should should mark the group as ineligible for election', async () => { + await validators.affiliate(otherGroup) + assert.isTrue(await mockElection.isIneligible(group)) }) }) }) @@ -807,7 +895,7 @@ contract('Validators', (accounts: string[]) => { describe('when the validator is a member of the affiliated group', () => { beforeEach(async () => { - await validators.addMember(validator, { from: group }) + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: group }) }) it('should remove the validator from the group membership list', async () => { @@ -816,9 +904,21 @@ contract('Validators', (accounts: string[]) => { assert.deepEqual(parsedGroup.members, []) }) + it("should update the member's membership history", async () => { + await validators.deaffiliate() + const membershipHistory = await validators.getMembershipHistory(validator) + const expectedEpoch = new BigNumber( + Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + ) + assert.equal(membershipHistory[0].length, 1) + assertEqualBN(membershipHistory[0][0], expectedEpoch) + assert.equal(membershipHistory[1].length, 1) + assertSameAddress(membershipHistory[1][0], NULL_ADDRESS) + }) + it('should emit the ValidatorGroupMemberRemoved event', async () => { const resp = await validators.deaffiliate() - assert.equal(resp.logs.length, 3) + assert.equal(resp.logs.length, 2) const log = resp.logs[0] assertContainSubset(log, { event: 'ValidatorGroupMemberRemoved', @@ -830,31 +930,9 @@ contract('Validators', (accounts: string[]) => { }) describe('when the validator is the only member of that group', () => { - it('should emit the ValidatorGroupEmptied event', async () => { - const resp = await validators.deaffiliate() - assert.equal(resp.logs.length, 3) - const log = resp.logs[1] - assertContainSubset(log, { - event: 'ValidatorGroupEmptied', - args: { - group, - }, - }) - }) - - describe('when that group has received votes', () => { - beforeEach(async () => { - const voter = accounts[2] - const weight = 10 - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) - }) - - it('should remove the group from the list of electable groups with votes', async () => { - await validators.deaffiliate() - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - }) + it('should should mark the group as ineligible for election', async () => { + await validators.deaffiliate() + assert.isTrue(await mockElection.isIneligible(group)) }) }) }) @@ -871,52 +949,42 @@ contract('Validators', (accounts: string[]) => { describe('#registerValidatorGroup', () => { const group = accounts[0] - beforeEach(async () => { - await mockLockedGold.setLockedCommitment( - group, - registrationRequirement.noticePeriod, - registrationRequirement.value - ) - }) + let resp: any + describe('when the account is not a registered validator group', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group) + resp = await validators.registerValidatorGroup(name, commission) + }) - it('should mark the account as a validator group', async () => { - await validators.registerValidatorGroup(identifier, name, url, [ - registrationRequirement.noticePeriod, - ]) - assert.isTrue(await validators.isValidatorGroup(group)) - }) + it('should mark the account as a validator group', async () => { + assert.isTrue(await validators.isValidatorGroup(group)) + }) - it('should add the account to the list of validator groups', async () => { - await validators.registerValidatorGroup(identifier, name, url, [ - registrationRequirement.noticePeriod, - ]) - assert.deepEqual(await validators.getRegisteredValidatorGroups(), [group]) - }) + it('should add the account to the list of validator groups', async () => { + assert.deepEqual(await validators.getRegisteredValidatorGroups(), [group]) + }) - it('should set the validator group identifier, name, and url', async () => { - await validators.registerValidatorGroup(identifier, name, url, [ - registrationRequirement.noticePeriod, - ]) - const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) - assert.equal(parsedGroup.identifier, identifier) - assert.equal(parsedGroup.name, name) - assert.equal(parsedGroup.url, url) - }) + it('should set the validator group name and commission', async () => { + const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) + assert.equal(parsedGroup.name, name) + assertEqualBN(parsedGroup.commission, commission) + }) - it('should emit the ValidatorGroupRegistered event', async () => { - const resp = await validators.registerValidatorGroup(identifier, name, url, [ - registrationRequirement.noticePeriod, - ]) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupRegistered', - args: { - group, - identifier, - name, - url, - }, + it('should set account balance requirements', async () => { + const requirement = await validators.getAccountBalanceRequirement(group) + assertEqualBN(requirement, balanceRequirements.group) + }) + + it('should emit the ValidatorGroupRegistered event', async () => { + assert.equal(resp.logs.length, 1) + const log = resp.logs[0] + assertContainSubset(log, { + event: 'ValidatorGroupRegistered', + args: { + group, + name, + }, + }) }) }) @@ -926,45 +994,28 @@ contract('Validators', (accounts: string[]) => { }) it('should revert', async () => { - await assertRevert( - validators.registerValidatorGroup(identifier, name, url, [ - registrationRequirement.noticePeriod, - ]) - ) + await assertRevert(validators.registerValidatorGroup(name, commission)) }) }) describe('when the account is already a registered validator group', () => { beforeEach(async () => { - await validators.registerValidatorGroup(identifier, name, url, [ - registrationRequirement.noticePeriod, - ]) + await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group) + await validators.registerValidatorGroup(name, commission) }) it('should revert', async () => { - await assertRevert( - validators.registerValidatorGroup(identifier, name, url, [ - registrationRequirement.noticePeriod, - ]) - ) + await assertRevert(validators.registerValidatorGroup(name, commission)) }) }) - describe('when the account does not meet the registration requirements', () => { + describe('when the account does not meet the balance requirements', () => { beforeEach(async () => { - await mockLockedGold.setLockedCommitment( - group, - registrationRequirement.noticePeriod, - registrationRequirement.value.minus(1) - ) + await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group.minus(1)) }) it('should revert', async () => { - await assertRevert( - validators.registerValidatorGroup(identifier, name, url, [ - registrationRequirement.noticePeriod, - ]) - ) + await assertRevert(validators.registerValidatorGroup(name, commission)) }) }) }) @@ -972,22 +1023,35 @@ contract('Validators', (accounts: string[]) => { describe('#deregisterValidatorGroup', () => { const index = 0 const group = accounts[0] + let resp: any beforeEach(async () => { await registerValidatorGroup(group) + resp = await validators.deregisterValidatorGroup(index) }) it('should mark the account as not a validator group', async () => { - await validators.deregisterValidatorGroup(index) assert.isFalse(await validators.isValidatorGroup(group)) }) it('should remove the account from the list of validator groups', async () => { - await validators.deregisterValidatorGroup(index) assert.deepEqual(await validators.getRegisteredValidatorGroups(), []) }) + it('should preserve account balance requirements', async () => { + const requirement = await validators.getAccountBalanceRequirement(group) + assertEqualBN(requirement, balanceRequirements.group) + }) + + it('should set the group deregistration timestamp', async () => { + const latestTimestamp = (await web3.eth.getBlock('latest')).timestamp + const [groupTimestamp, validatorTimestamp] = await validators.getDeregistrationTimestamps( + group + ) + assertEqualBN(groupTimestamp, latestTimestamp) + assertEqualBN(validatorTimestamp, 0) + }) + it('should emit the ValidatorGroupDeregistered event', async () => { - const resp = await validators.deregisterValidatorGroup(index) assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { @@ -1009,9 +1073,10 @@ contract('Validators', (accounts: string[]) => { describe('when the validator group is not empty', () => { const validator = accounts[1] beforeEach(async () => { + await registerValidatorGroup(group) await registerValidator(validator) await validators.affiliate(group, { from: validator }) - await validators.addMember(validator) + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS) }) it('should revert', async () => { @@ -1023,20 +1088,31 @@ contract('Validators', (accounts: string[]) => { describe('#addMember', () => { const group = accounts[0] const validator = accounts[1] + let resp: any beforeEach(async () => { await registerValidator(validator) await registerValidatorGroup(group) await validators.affiliate(group, { from: validator }) + resp = await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS) }) it('should add the member to the list of members', async () => { - await validators.addMember(validator) const parsedGroup = parseValidatorGroupParams(await validators.getValidatorGroup(group)) assert.deepEqual(parsedGroup.members, [validator]) }) + it("should update the member's membership history", async () => { + const membershipHistory = await validators.getMembershipHistory(validator) + const expectedEpoch = new BigNumber( + Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + ) + assert.equal(membershipHistory[0].length, 1) + assertEqualBN(membershipHistory[0][0], expectedEpoch) + assert.equal(membershipHistory[1].length, 1) + assertSameAddress(membershipHistory[1][0], group) + }) + it('should emit the ValidatorGroupMemberAdded event', async () => { - const resp = await validators.addMember(validator) assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { @@ -1049,16 +1125,17 @@ contract('Validators', (accounts: string[]) => { }) it('should revert when the account is not a registered validator group', async () => { - await assertRevert(validators.addMember(validator, { from: accounts[2] })) + await assertRevert( + validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: accounts[2] }) + ) }) it('should revert when the member is not a registered validator', async () => { - await assertRevert(validators.addMember(accounts[2])) + await assertRevert(validators.addFirstMember(accounts[2], NULL_ADDRESS, NULL_ADDRESS)) }) it('should revert when trying to add too many members to group', async () => { await validators.setMaxGroupSize(1) - await validators.addMember(validator) await registerValidator(accounts[2]) await validators.affiliate(group, { from: accounts[2] }) await assertRevert(validators.addMember(accounts[2])) @@ -1070,15 +1147,11 @@ contract('Validators', (accounts: string[]) => { }) it('should revert', async () => { - await assertRevert(validators.addMember(validator)) + await assertRevert(validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS)) }) }) describe('when the validator is already a member of the group', () => { - beforeEach(async () => { - await validators.addMember(validator) - }) - it('should revert', async () => { await assertRevert(validators.addMember(validator)) }) @@ -1098,9 +1171,30 @@ contract('Validators', (accounts: string[]) => { assert.deepEqual(parsedGroup.members, []) }) + it("should update the member's membership history", async () => { + await validators.removeMember(validator) + const membershipHistory = await validators.getMembershipHistory(validator) + const expectedEpoch = new BigNumber( + Math.floor((await web3.eth.getBlock('latest')).number / EPOCH) + ) + + // Depending on test timing, we may or may not span an epoch boundary between registration + // and removal. + const numEntries = membershipHistory[0].length + assert.isTrue(numEntries == 1 || numEntries == 2) + assert.equal(membershipHistory[1].length, numEntries) + if (numEntries == 1) { + assertEqualBN(membershipHistory[0][0], expectedEpoch) + assertSameAddress(membershipHistory[1][0], NULL_ADDRESS) + } else { + assertEqualBN(membershipHistory[0][1], expectedEpoch) + assertSameAddress(membershipHistory[1][1], NULL_ADDRESS) + } + }) + it('should emit the ValidatorGroupMemberRemoved event', async () => { const resp = await validators.removeMember(validator) - assert.equal(resp.logs.length, 2) + assert.equal(resp.logs.length, 1) const log = resp.logs[0] assertContainSubset(log, { event: 'ValidatorGroupMemberRemoved', @@ -1112,31 +1206,9 @@ contract('Validators', (accounts: string[]) => { }) describe('when the validator is the only member of the group', () => { - it('should emit the ValidatorGroupEmptied event', async () => { - const resp = await validators.removeMember(validator) - assert.equal(resp.logs.length, 2) - const log = resp.logs[1] - assertContainSubset(log, { - event: 'ValidatorGroupEmptied', - args: { - group, - }, - }) - }) - - describe('when the group has received votes', () => { - beforeEach(async () => { - const voter = accounts[2] - const weight = 10 - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS, { from: voter }) - }) - - it('should remove the group from the list of electable groups with votes', async () => { - await validators.removeMember(validator) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - }) + it('should mark the group ineligible', async () => { + await validators.removeMember(validator) + assert.isTrue(await mockElection.isIneligible(group)) }) }) @@ -1207,320 +1279,242 @@ contract('Validators', (accounts: string[]) => { }) }) - describe('#vote', () => { - const weight = new BigNumber(5) - const voter = accounts[0] - const validator = accounts[1] - const group = accounts[2] + describe('#updateValidatorScore', () => { + const validator = accounts[0] beforeEach(async () => { - await registerValidatorGroupWithMembers(group, [validator]) - await mockLockedGold.setWeight(voter, weight) - }) - - it("should set the voter's vote", async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - assert.isTrue(await validators.isVoting(voter)) - assert.equal(await validators.voters(voter), group) - }) - - it('should add the group to the list of those receiving votes', async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, [group]) - }) - - it("should increment the validator group's vote total", async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - assertEqualBN(await validators.getVotesReceived(group), weight) - }) - - it('should emit the ValidatorGroupVoteCast event', async () => { - const resp = await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupVoteCast', - args: { - account: voter, - group, - weight: new BigNumber(weight), - }, - }) - }) - - describe('when the group had not previously received votes', () => { - it('should add the group to the list of electable groups with votes', async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, [group]) - }) - }) - - it('should revert when the group is not a registered validator group', async () => { - await assertRevert(validators.vote(accounts[3], NULL_ADDRESS, NULL_ADDRESS)) - }) - - describe('when the group is empty', () => { - beforeEach(async () => { - await validators.removeMember(validator, { from: group }) - }) - - it('should revert', async () => { - await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) - }) + await registerValidator(validator) }) - describe('when the account voting is frozen', () => { + describe('when 0 <= uptime <= 1.0', () => { + const uptime = new BigNumber(0.99) + // @ts-ignore + const epochScore = uptime.pow(validatorScoreParameters.exponent) + const adjustmentSpeed = fromFixed(validatorScoreParameters.adjustmentSpeed) beforeEach(async () => { - await mockLockedGold.setVotingFrozen(voter) + await validators.updateValidatorScore(validator, toFixed(uptime)) }) - it('should revert', async () => { - await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) + it('should update the validator score', async () => { + const expectedScore = adjustmentSpeed.times(epochScore) + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assertEqualBN(parsedValidator.score, toFixed(expectedScore)) }) - }) - describe('when the account has no weight', () => { - beforeEach(async () => { - await mockLockedGold.setWeight(voter, NULL_ADDRESS) - }) + describe('when the validator already has a non-zero score', () => { + beforeEach(async () => { + await validators.updateValidatorScore(validator, toFixed(uptime)) + }) - it('should revert', async () => { - await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) + it('should update the validator score', async () => { + let expectedScore = adjustmentSpeed.times(epochScore) + expectedScore = new BigNumber(1) + .minus(adjustmentSpeed) + .times(expectedScore) + .plus(expectedScore) + const parsedValidator = parseValidatorParams(await validators.getValidator(validator)) + assertEqualBN(parsedValidator.score, toFixed(expectedScore)) + }) }) }) - describe('when the account has an outstanding vote', () => { - beforeEach(async () => { - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - }) + describe('when uptime > 1.0', () => { + const uptime = 1.01 it('should revert', async () => { - await assertRevert(validators.vote(group, NULL_ADDRESS, NULL_ADDRESS)) + await assertRevert(validators.updateValidatorScore(validator, toFixed(uptime))) }) }) }) - describe('#revokeVote', () => { - const weight = 5 - const voter = accounts[0] - const validator = accounts[1] - const group = accounts[2] + describe('#updateMembershipHistory', () => { + const validator = accounts[0] + const groups = accounts.slice(1) + let validatorRegistrationEpochNumber: number beforeEach(async () => { - await registerValidatorGroupWithMembers(group, [validator]) - await mockLockedGold.setWeight(voter, weight) - await validators.vote(group, NULL_ADDRESS, NULL_ADDRESS) - }) - - it("should clear the voter's vote", async () => { - await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - assert.isFalse(await validators.isVoting(voter)) - assert.equal(await validators.voters(voter), NULL_ADDRESS) - }) - - it("should decrement the validator group's vote total", async () => { - await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - const [groups, votes] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) - assert.deepEqual(votes, []) - }) - - it('should emit the ValidatorGroupVoteRevoked event', async () => { - const resp = await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] - assertContainSubset(log, { - event: 'ValidatorGroupVoteRevoked', - args: { - account: voter, - group, - weight: new BigNumber(weight), - }, - }) + await registerValidator(validator) + const blockNumber = (await web3.eth.getBlock('latest')).number + validatorRegistrationEpochNumber = Math.floor(blockNumber / EPOCH) + for (const group of groups) { + await registerValidatorGroup(group) + } }) - describe('when the group had not received other votes', () => { - it('should remove the group from the list of electable groups with votes', async () => { - await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - const [groups] = await validators.getValidatorGroupVotes() - assert.deepEqual(groups, []) + describe('when changing groups in the same epoch', () => { + it('should overwrite the previous entry', async () => { + const numTests = 10 + // We store an entry upon registering the validator. + const expectedMembershipHistoryGroups = [NULL_ADDRESS] + const expectedMembershipHistoryEpochs = [new BigNumber(validatorRegistrationEpochNumber)] + for (let i = 0; i < numTests; i++) { + const blockNumber = (await web3.eth.getBlock('latest')).number + const epochNumber = Math.floor(blockNumber / EPOCH) + const blocksUntilNextEpoch = (epochNumber + 1) * EPOCH - blockNumber + await mineBlocks(blocksUntilNextEpoch, web3) + + let group = groups[0] + await validators.affiliate(group) + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { + from: group, + }) + let membershipHistory = await validators.getMembershipHistory(validator) + expectedMembershipHistoryGroups.push(group) + expectedMembershipHistoryEpochs.push(new BigNumber(epochNumber + 1)) + if (expectedMembershipHistoryGroups.length > membershipHistoryLength.toNumber()) { + expectedMembershipHistoryGroups.shift() + expectedMembershipHistoryEpochs.shift() + } + assertEqualBNArray(membershipHistory[0], expectedMembershipHistoryEpochs) + assert.deepEqual(membershipHistory[1], expectedMembershipHistoryGroups) + + group = groups[1] + await validators.affiliate(group) + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { + from: group, + }) + membershipHistory = await validators.getMembershipHistory(validator) + expectedMembershipHistoryGroups[expectedMembershipHistoryGroups.length - 1] = group + assertEqualBNArray(membershipHistory[0], expectedMembershipHistoryEpochs) + assert.deepEqual(membershipHistory[1], expectedMembershipHistoryGroups) + } }) }) - describe('when the account does not have an outstanding vote', () => { - beforeEach(async () => { - await validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS) - }) + describe('when changing groups more times than membership history length', () => { + it('should always store the most recent memberships', async () => { + // We store an entry upon registering the validator. + const expectedMembershipHistoryGroups = [NULL_ADDRESS] + const expectedMembershipHistoryEpochs = [new BigNumber(validatorRegistrationEpochNumber)] + for (let i = 0; i < membershipHistoryLength.plus(1).toNumber(); i++) { + const blockNumber = (await web3.eth.getBlock('latest')).number + const epochNumber = Math.floor(blockNumber / EPOCH) + const blocksUntilNextEpoch = (epochNumber + 1) * EPOCH - blockNumber + await mineBlocks(blocksUntilNextEpoch, web3) - it('should revert', async () => { - await assertRevert(validators.revokeVote(NULL_ADDRESS, NULL_ADDRESS)) + await validators.affiliate(groups[i]) + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { + from: groups[i], + }) + expectedMembershipHistoryGroups.push(groups[i]) + expectedMembershipHistoryEpochs.push(new BigNumber(epochNumber + 1)) + if (expectedMembershipHistoryGroups.length > membershipHistoryLength.toNumber()) { + expectedMembershipHistoryGroups.shift() + expectedMembershipHistoryEpochs.shift() + } + const membershipHistory = await validators.getMembershipHistory(validator) + assertEqualBNArray(membershipHistory[0], expectedMembershipHistoryEpochs) + assert.deepEqual(membershipHistory[1], expectedMembershipHistoryGroups) + } }) }) }) - describe('#getValidators', () => { - const group1 = accounts[0] - const group2 = accounts[1] - const group3 = accounts[2] - const validator1 = accounts[3] - const validator2 = accounts[4] - const validator3 = accounts[5] - const validator4 = accounts[6] - const validator5 = accounts[7] - const validator6 = accounts[8] - const validator7 = accounts[9] - - const hash1 = '0xa5b9d60f32436310afebcfda832817a68921beb782fabf7915cc0460b443116a' - const hash2 = '0xa832817a68921b10afebcfd0460b443116aeb782fabf7915cca5b9d60f324363' - - // If voterN votes for groupN: - // group1 gets 20 votes per member - // group2 gets 25 votes per member - // group3 gets 30 votes per member - // We cannot make any guarantee with respect to their ordering. - const voter1 = { address: accounts[0], weight: 80 } - const voter2 = { address: accounts[1], weight: 50 } - const voter3 = { address: accounts[2], weight: 30 } - const assertSameAddresses = (actual: string[], expected: string[]) => { - assert.sameMembers(actual.map((x) => x.toLowerCase()), expected.map((x) => x.toLowerCase())) - } - + describe('#getMembershipInLastEpoch', () => { + const validator = accounts[0] + const groups = accounts.slice(1) beforeEach(async () => { - await registerValidatorGroupWithMembers(group1, [ - validator1, - validator2, - validator3, - validator4, - ]) - await registerValidatorGroupWithMembers(group2, [validator5, validator6]) - await registerValidatorGroupWithMembers(group3, [validator7]) - - for (const voter of [voter1, voter2, voter3]) { - await mockLockedGold.setWeight(voter.address, voter.weight) + await registerValidator(validator) + for (const group of groups) { + await registerValidatorGroup(group) } - await random.revealAndCommit(hash1, hash1, NULL_ADDRESS) }) - describe('when a single group has >= minElectableValidators as members and received votes', () => { - beforeEach(async () => { - await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) - }) + describe('when changing groups more times than membership history length', () => { + it('should always return the correct membership for the last epoch', async () => { + for (let i = 0; i < membershipHistoryLength.plus(1).toNumber(); i++) { + const blockNumber = (await web3.eth.getBlock('latest')).number + const epochNumber = Math.floor(blockNumber / EPOCH) + const blocksUntilNextEpoch = (epochNumber + 1) * EPOCH - blockNumber + await mineBlocks(blocksUntilNextEpoch, web3) - it("should return that group's member list", async () => { - assertSameAddresses(await validators.getValidators(), [ - validator1, - validator2, - validator3, - validator4, - ]) - }) - }) + await validators.affiliate(groups[i]) + await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { + from: groups[i], + }) - describe("when > maxElectableValidators members's groups receive votes", () => { - beforeEach(async () => { - await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) - await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) - await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) + if (i == 0) { + assert.equal(await validators.getMembershipInLastEpoch(validator), NULL_ADDRESS) + } else { + assert.equal(await validators.getMembershipInLastEpoch(validator), groups[i - 1]) + } + } }) + }) + }) - it('should return maxElectableValidators elected validators', async () => { - assertSameAddresses(await validators.getValidators(), [ - validator1, - validator2, - validator3, - validator5, - validator6, - validator7, - ]) - }) + describe('#getEpochSize', () => { + it('should always return 100', async () => { + assertEqualBN(await validators.getEpochSize(), 100) }) + }) - describe('when different random values are provided', () => { + describe('#distributeEpochPayment', () => { + const validator = accounts[0] + const group = accounts[1] + let mockStableToken: MockStableTokenInstance + beforeEach(async () => { + await registerValidatorGroupWithMembers(group, [validator]) + mockStableToken = await MockStableToken.new() + await registry.setAddressFor(CeloContractName.StableToken, mockStableToken.address) + }) + + describe('when the validator score is non-zero', () => { + const uptime = new BigNumber(0.99) + const adjustmentSpeed = fromFixed(validatorScoreParameters.adjustmentSpeed) + // @ts-ignore + const expectedScore = adjustmentSpeed.times(uptime.pow(validatorScoreParameters.exponent)) + const expectedTotalPayment = expectedScore.times(validatorEpochPayment) + const expectedGroupPayment = expectedTotalPayment + .times(fromFixed(commission)) + .dp(0, BigNumber.ROUND_FLOOR) + const expectedValidatorPayment = expectedTotalPayment.minus(expectedGroupPayment) beforeEach(async () => { - await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) - await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) - await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) + await validators.updateValidatorScore(validator, toFixed(uptime)) }) - it('should return different results', async () => { - await random.revealAndCommit(hash1, hash1, NULL_ADDRESS) - const valsWithHash1 = (await validators.getValidators()).map((x) => x.toLowerCase()) - await random.revealAndCommit(hash2, hash2, NULL_ADDRESS) - const valsWithHash2 = (await validators.getValidators()).map((x) => x.toLowerCase()) - assert.sameMembers(valsWithHash1, valsWithHash2) - assert.notDeepEqual(valsWithHash1, valsWithHash2) - }) - }) + describe('when the validator and group meet the balance requirements', () => { + beforeEach(async () => { + await validators.distributeEpochPayment(validator) + }) - describe('when a group receives enough votes for > n seats but only has n members', () => { - beforeEach(async () => { - await mockLockedGold.setWeight(voter3.address, 1000) - await validators.vote(group3, NULL_ADDRESS, NULL_ADDRESS, { from: voter3.address }) - await validators.vote(group1, NULL_ADDRESS, group3, { from: voter1.address }) - await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) - }) + it('should pay the validator', async () => { + assertEqualBN(await mockStableToken.balanceOf(validator), expectedValidatorPayment) + }) - it('should elect only n members from that group', async () => { - assertSameAddresses(await validators.getValidators(), [ - validator7, - validator1, - validator2, - validator3, - validator5, - validator6, - ]) + it('should pay the group', async () => { + assertEqualBN(await mockStableToken.balanceOf(group), expectedGroupPayment) + }) }) - }) - describe('when an account has delegated validating to another address', () => { - const validatingDelegate = '0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95' - beforeEach(async () => { - await mockLockedGold.delegateValidating(validator3, validatingDelegate) - await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) - await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) - await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) - }) + describe('when the validator does not meet the balance requirements', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold( + validator, + balanceRequirements.validator.minus(1) + ) + await validators.distributeEpochPayment(validator) + }) - it('should return the validating delegate in place of the account', async () => { - assertSameAddresses(await validators.getValidators(), [ - validator1, - validator2, - validatingDelegate, - validator5, - validator6, - validator7, - ]) - }) - }) + it('should not pay the validator', async () => { + assertEqualBN(await mockStableToken.balanceOf(validator), 0) + }) - describe('when there are not enough electable validators', () => { - beforeEach(async () => { - await validators.vote(group2, NULL_ADDRESS, NULL_ADDRESS, { from: voter2.address }) - await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) + it('should not pay the group', async () => { + assertEqualBN(await mockStableToken.balanceOf(group), 0) + }) }) - it('should revert', async () => { - await assertRevert(validators.getValidators()) - }) - }) + describe('when the group does not meet the balance requirements', () => { + beforeEach(async () => { + await mockLockedGold.setAccountTotalLockedGold(group, balanceRequirements.group.minus(1)) + await validators.distributeEpochPayment(validator) + }) - describe('when election threshold is set to 20%', () => { - beforeEach(async () => { - const threshold = toFixed(1 / 5) - await validators.setElectionThreshold(threshold) - await validators.vote(group1, NULL_ADDRESS, NULL_ADDRESS, { from: voter1.address }) - await validators.vote(group2, NULL_ADDRESS, group1, { from: voter2.address }) - await validators.vote(group3, NULL_ADDRESS, group2, { from: voter3.address }) - }) - - it('should return the elected validators from two largest parties', async () => { - assertSameAddresses(await validators.getValidators(), [ - validator1, - validator2, - validator3, - validator4, - validator5, - validator6, - ]) + it('should not pay the validator', async () => { + assertEqualBN(await mockStableToken.balanceOf(validator), 0) + }) + + it('should not pay the group', async () => { + assertEqualBN(await mockStableToken.balanceOf(group), 0) + }) }) }) }) diff --git a/packages/protocol/test/identity/attestations.ts b/packages/protocol/test/identity/attestations.ts index d3d582d053e..79bfb042ccf 100644 --- a/packages/protocol/test/identity/attestations.ts +++ b/packages/protocol/test/identity/attestations.ts @@ -13,26 +13,31 @@ import { getPhoneHash } from '@celo/utils/lib/phoneNumbers' import BigNumber from 'bignumber.js' import { uniq } from 'lodash' import { - AttestationsContract, - AttestationsInstance, + TestAttestationsContract, + TestAttestationsInstance, MockLockedGoldContract, MockLockedGoldInstance, MockStableTokenContract, MockStableTokenInstance, - MockValidatorsContract, - MockValidatorsInstance, - RandomContract, - RandomInstance, + MockElectionInstance, + TestRandomContract, + TestRandomInstance, + MockElectionContract, RegistryContract, RegistryInstance, } from 'types' import { getParsedSignatureOfAddress } from '../../lib/signing-utils' -const Attestations: AttestationsContract = artifacts.require('Attestations') +/* We use a contract that behaves like the actual Attestations contract, but + * mocks the implementations of validator set getters. These rely on precompiled + * contracts, which are not available in our current ganache fork, which we use + * for Truffle unit tests. + */ +const Attestations: TestAttestationsContract = artifacts.require('TestAttestations') const MockStableToken: MockStableTokenContract = artifacts.require('MockStableToken') -const MockValidators: MockValidatorsContract = artifacts.require('MockValidators') +const MockElection: MockElectionContract = artifacts.require('MockElection') const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') -const Random: RandomContract = artifacts.require('Random') +const Random: TestRandomContract = artifacts.require('TestRandom') const Registry: RegistryContract = artifacts.require('Registry') const dataEncryptionKey = '0x02f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111' @@ -41,11 +46,11 @@ const longDataEncryptionKey = '02f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111' contract('Attestations', (accounts: string[]) => { - let attestations: AttestationsInstance + let attestations: TestAttestationsInstance let mockStableToken: MockStableTokenInstance let otherMockStableToken: MockStableTokenInstance - let random: RandomInstance - let mockValidators: MockValidatorsInstance + let random: TestRandomInstance + let mockElection: MockElectionInstance let mockLockedGold: MockLockedGoldInstance let registry: RegistryInstance const provider = new Web3.providers.HttpProvider('http://localhost:8545') @@ -137,19 +142,20 @@ contract('Attestations', (accounts: string[]) => { otherMockStableToken = await MockStableToken.new() attestations = await Attestations.new() random = await Random.new() - mockValidators = await MockValidators.new() - await Promise.all( - accounts.map((account) => mockValidators.addValidator(getValidatingKeyAddress(account))) - ) + random.addTestRandomness(0, '0x00') mockLockedGold = await MockLockedGold.new() await Promise.all( accounts.map((account) => - mockLockedGold.delegateValidating(account, getValidatingKeyAddress(account)) + mockLockedGold.authorizeValidator(account, getValidatingKeyAddress(account)) ) ) + mockElection = await MockElection.new() + await mockElection.setElectedValidators( + accounts.map((account) => getValidatingKeyAddress(account)) + ) registry = await Registry.new() await registry.setAddressFor(CeloContractName.Random, random.address) - await registry.setAddressFor(CeloContractName.Validators, mockValidators.address) + await registry.setAddressFor(CeloContractName.Election, mockElection.address) await registry.setAddressFor(CeloContractName.LockedGold, mockLockedGold.address) await attestations.initialize( registry.address, @@ -157,6 +163,7 @@ contract('Attestations', (accounts: string[]) => { [mockStableToken.address, otherMockStableToken.address], [attestationFee, attestationFee] ) + await attestations.__setValidators(accounts) }) describe('#initialize()', () => { diff --git a/packages/protocol/test/identity/random.ts b/packages/protocol/test/identity/random.ts new file mode 100644 index 00000000000..2ffca631849 --- /dev/null +++ b/packages/protocol/test/identity/random.ts @@ -0,0 +1,105 @@ +import { assertRevert, assertEqualBN, assertContainSubset } from '@celo/protocol/lib/test-utils' + +import { TestRandomContract, TestRandomInstance } from 'types' +import { BigNumber } from 'bignumber.js' + +const Random: TestRandomContract = artifacts.require('TestRandom') + +// @ts-ignore +// TODO(mcortesi): Use BN +Random.numberFormat = 'BigNumber' + +contract('Random', (accounts: string[]) => { + let random: TestRandomInstance + + beforeEach(async () => { + random = await Random.new() + random.initialize(256) + }) + + describe('#setRandomnessRetentionWindow()', () => { + it('should set the variable', async () => { + await random.setRandomnessBlockRetentionWindow(1000) + assertEqualBN(new BigNumber(1000), await random.randomnessBlockRetentionWindow()) + }) + + it('should emit the event', async () => { + const response = await random.setRandomnessBlockRetentionWindow(1000) + assert.equal(response.logs.length, 1) + const log = response.logs[0] + assertContainSubset(log, { + event: 'RandomnessBlockRetentionWindowSet', + args: { + value: new BigNumber(1000), + }, + }) + }) + + it('only owner can set', async () => { + assertRevert(random.setRandomnessBlockRetentionWindow(1000, { from: accounts[1] })) + }) + }) + + describe('#addTestRandomness', () => { + const randomValues = [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000001', + '0x0000000000000000000000000000000000000000000000000000000000000002', + '0x0000000000000000000000000000000000000000000000000000000000000003', + '0x0000000000000000000000000000000000000000000000000000000000000004', + '0x0000000000000000000000000000000000000000000000000000000000000005', + '0x0000000000000000000000000000000000000000000000000000000000000006', + '0x0000000000000000000000000000000000000000000000000000000000000007', + ] + it('should be able to simulate adding randomness', async () => { + await random.addTestRandomness(1, randomValues[1]) + await random.addTestRandomness(2, randomValues[2]) + await random.addTestRandomness(3, randomValues[3]) + await random.addTestRandomness(4, randomValues[4]) + assert.equal(randomValues[1], await random.getTestRandomness(1, 4)) + assert.equal(randomValues[2], await random.getTestRandomness(2, 4)) + assert.equal(randomValues[3], await random.getTestRandomness(3, 4)) + assert.equal(randomValues[4], await random.getTestRandomness(4, 4)) + }) + + describe('when changing history smaller', () => { + beforeEach(async () => { + await random.addTestRandomness(1, randomValues[1]) + await random.addTestRandomness(2, randomValues[2]) + await random.addTestRandomness(3, randomValues[3]) + await random.addTestRandomness(4, randomValues[4]) + await random.setRandomnessBlockRetentionWindow(2) + }) + it('can still add randomness', async () => { + await random.addTestRandomness(5, randomValues[5]) + assert.equal(randomValues[5], await random.getTestRandomness(5, 5)) + }) + it('cannot read old blocks', async () => { + assertRevert(random.getTestRandomness(3, 5)) + }) + }) + + describe('when changing history larger', () => { + beforeEach(async () => { + await random.setRandomnessBlockRetentionWindow(2) + await random.addTestRandomness(1, randomValues[1]) + await random.addTestRandomness(2, randomValues[2]) + await random.addTestRandomness(3, randomValues[3]) + await random.addTestRandomness(4, randomValues[4]) + await random.setRandomnessBlockRetentionWindow(4) + }) + it('can still add randomness', async () => { + await random.addTestRandomness(5, randomValues[5]) + assert.equal(randomValues[5], await random.getTestRandomness(5, 5)) + }) + it('cannot read old blocks', async () => { + assertRevert(random.getTestRandomness(1, 5)) + }) + it('old values are preserved', async () => { + await random.addTestRandomness(5, randomValues[5]) + await random.addTestRandomness(6, randomValues[6]) + assert.equal(randomValues[3], await random.getTestRandomness(3, 6)) + }) + }) + }) +}) diff --git a/packages/protocol/test/stability/exchange.ts b/packages/protocol/test/stability/exchange.ts index eddd3b9183a..36be55415bc 100644 --- a/packages/protocol/test/stability/exchange.ts +++ b/packages/protocol/test/stability/exchange.ts @@ -1,3 +1,4 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { assertEqualBN, assertLogMatches2, @@ -87,10 +88,10 @@ contract('Exchange', (accounts: string[]) => { beforeEach(async () => { registry = await Registry.new() goldToken = await GoldToken.new() - await registry.setAddressFor('GoldToken', goldToken.address) + await registry.setAddressFor(CeloContractName.GoldToken, goldToken.address) mockReserve = await MockReserve.new() - await registry.setAddressFor('Reserve', mockReserve.address) + await registry.setAddressFor(CeloContractName.Reserve, mockReserve.address) await mockReserve.setGoldToken(goldToken.address) stableToken = await StableToken.new() @@ -101,11 +102,13 @@ contract('Exchange', (accounts: string[]) => { decimals, registry.address, fixed1, - SECONDS_IN_A_WEEK + SECONDS_IN_A_WEEK, + [], + [] ) mockSortedOracles = await MockSortedOracles.new() - await registry.setAddressFor('SortedOracles', mockSortedOracles.address) + await registry.setAddressFor(CeloContractName.SortedOracles, mockSortedOracles.address) await mockSortedOracles.setMedianRate( stableToken.address, stableAmountForRate, @@ -125,8 +128,7 @@ contract('Exchange', (accounts: string[]) => { updateFrequency, minimumReports ) - - await stableToken.setMinter(exchange.address) + await registry.setAddressFor(CeloContractName.Exchange, exchange.address) }) describe('#initialize()', () => { @@ -588,9 +590,9 @@ contract('Exchange', (accounts: string[]) => { let oldGoldBalance: BigNumber let oldReserveGoldBalance: BigNumber beforeEach(async () => { - await stableToken.setMinter(owner) + await registry.setAddressFor(CeloContractName.Exchange, owner) await stableToken.mint(user, stableTokenBalance) - await stableToken.setMinter(exchange.address) + await registry.setAddressFor(CeloContractName.Exchange, exchange.address) oldReserveGoldBalance = await goldToken.balanceOf(mockReserve.address) await stableToken.approve(exchange.address, stableTokenBalance, { from: user }) diff --git a/packages/protocol/test/stability/sortedoracles.ts b/packages/protocol/test/stability/sortedoracles.ts index 2b2230956fa..bac7cce8526 100644 --- a/packages/protocol/test/stability/sortedoracles.ts +++ b/packages/protocol/test/stability/sortedoracles.ts @@ -253,10 +253,18 @@ contract('SortedOracles', (accounts: string[]) => { }) describe('#report', () => { + function expectedNumeratorFromGiven( + givenNumerator: number | BigNumber, + givenDenominator: number | BigNumber + ): BigNumber { + return expectedDenominator.times(givenNumerator).div(givenDenominator) + } + const numerator = 10 const denominator = 1 const expectedDenominator = new BigNumber(2).pow(64) - const expectedNumerator = expectedDenominator.times(numerator).div(denominator) + const expectedNumerator = expectedNumeratorFromGiven(numerator, denominator) + beforeEach(async () => { await sortedOracles.addOracle(aToken, anOracle) }) @@ -330,5 +338,92 @@ contract('SortedOracles', (accounts: string[]) => { sortedOracles.report(aToken, numerator, denominator, NULL_ADDRESS, NULL_ADDRESS) ) }) + + describe('when there exists exactly one other report, made by this oracle', () => { + const newNumerator = 12 + const newExpectedNumerator = expectedNumeratorFromGiven(newNumerator, denominator) + + beforeEach(async () => { + await sortedOracles.report(aToken, numerator, denominator, NULL_ADDRESS, NULL_ADDRESS, { + from: anOracle, + }) + }) + it('should reset the median rate', async () => { + const [initialNumerator, initialDenominator] = await sortedOracles.medianRate(aToken) + assertEqualBN(initialNumerator, expectedNumerator) + assertEqualBN(initialDenominator, expectedDenominator) + + await sortedOracles.report(aToken, newNumerator, denominator, NULL_ADDRESS, NULL_ADDRESS, { + from: anOracle, + }) + + const [actualNumerator, actualDenominator] = await sortedOracles.medianRate(aToken) + assertEqualBN(actualNumerator, newExpectedNumerator) + assertEqualBN(actualDenominator, expectedDenominator) + }) + it('should not change the number of total reports', async () => { + const initialNumReports = await sortedOracles.numRates(aToken) + await sortedOracles.report(aToken, newNumerator, denominator, NULL_ADDRESS, NULL_ADDRESS, { + from: anOracle, + }) + + assertEqualBN(initialNumReports, await sortedOracles.numRates(aToken)) + }) + }) + + describe('when there are multiple reports, the most recent one done by this oracle', () => { + const anotherOracle = accounts[6] + const anOracleNumerator1 = 2 + const anOracleNumerator2 = 3 + const anotherOracleNumerator = 1 + + const anOracleExpectedNumerator1 = expectedNumeratorFromGiven(anOracleNumerator1, denominator) + const anOracleExpectedNumerator2 = expectedNumeratorFromGiven(anOracleNumerator2, denominator) + + const anotherOracleExpectedNumerator = expectedNumeratorFromGiven( + anotherOracleNumerator, + denominator + ) + + beforeEach(async () => { + sortedOracles.addOracle(aToken, anotherOracle) + await sortedOracles.report(aToken, anotherOracleNumerator, 1, NULL_ADDRESS, NULL_ADDRESS, { + from: anotherOracle, + }) + await timeTravel(5, web3) + await sortedOracles.report(aToken, anOracleNumerator1, 1, anotherOracle, NULL_ADDRESS, { + from: anOracle, + }) + await timeTravel(5, web3) + + // confirm the setup worked + const initialRates = await sortedOracles.getRates(aToken) + assertEqualBN(initialRates['1'][0], anOracleExpectedNumerator1) + assertEqualBN(initialRates['1'][1], anotherOracleExpectedNumerator) + }) + + it('updates the list of rates correctly', async () => { + await sortedOracles.report(aToken, anOracleNumerator2, 1, anotherOracle, NULL_ADDRESS, { + from: anOracle, + }) + const resultRates = await sortedOracles.getRates(aToken) + assertEqualBN(resultRates['1'][0], anOracleExpectedNumerator2) + assertEqualBN(resultRates['1'][1], anotherOracleExpectedNumerator) + }) + + it('updates the latest timestamp', async () => { + const initialTimestamps = await sortedOracles.getTimestamps(aToken) + await sortedOracles.report(aToken, anOracleNumerator2, 1, anotherOracle, NULL_ADDRESS, { + from: anOracle, + }) + const resultTimestamps = await sortedOracles.getTimestamps(aToken) + + // the second timestamp, belonging to anotherOracle should be unchanged + assertEqualBN(initialTimestamps['1']['1'], resultTimestamps['1']['1']) + + // the most recent timestamp, belonging to anOracle in both cases, should change + assert.isTrue(resultTimestamps['1']['0'].gt(initialTimestamps['1']['0'])) + }) + }) }) }) diff --git a/packages/protocol/test/stability/stabletoken.ts b/packages/protocol/test/stability/stabletoken.ts index a78d5b3575c..722b5aae445 100644 --- a/packages/protocol/test/stability/stabletoken.ts +++ b/packages/protocol/test/stability/stabletoken.ts @@ -1,3 +1,4 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { assertLogMatches, assertLogMatches2, @@ -28,15 +29,17 @@ contract('StableToken', (accounts: string[]) => { beforeEach(async () => { registry = await Registry.new() stableToken = await StableToken.new() - await stableToken.initialize( + const response = await stableToken.initialize( 'Celo Dollar', 'cUSD', 18, registry.address, fixed1, - SECONDS_IN_A_WEEK + SECONDS_IN_A_WEEK, + [], + [] ) - initializationTime = (await web3.eth.getBlock('latest')).timestamp + initializationTime = (await web3.eth.getBlock(response.receipt.blockNumber)).timestamp }) describe('#initialize()', () => { @@ -86,7 +89,9 @@ contract('StableToken', (accounts: string[]) => { 18, registry.address, fixed1, - SECONDS_IN_A_WEEK + SECONDS_IN_A_WEEK, + [], + [] ) ) }) @@ -106,34 +111,32 @@ contract('StableToken', (accounts: string[]) => { }) }) - describe('#setMinter()', () => { - const minter = accounts[0] - it('should allow owner to set minter', async () => { - await stableToken.setMinter(minter) - assert.equal(await stableToken.minter(), minter) - }) - - it('should not allow anyone else to set minter', async () => { - await assertRevert(stableToken.setMinter(minter, { from: accounts[1] })) - }) - }) - describe('#mint()', () => { - const minter = accounts[0] + const exchange = accounts[0] + const validators = accounts[1] beforeEach(async () => { - await stableToken.setMinter(minter) + await registry.setAddressFor(CeloContractName.Exchange, exchange) + await registry.setAddressFor(CeloContractName.Validators, validators) }) - it('should allow minter to mint', async () => { - await stableToken.mint(minter, amountToMint) - const balance = (await stableToken.balanceOf(minter)).toNumber() + it('should allow the registered exchange contract to mint', async () => { + await stableToken.mint(exchange, amountToMint) + const balance = (await stableToken.balanceOf(exchange)).toNumber() + assert.equal(balance, amountToMint) + const supply = (await stableToken.totalSupply()).toNumber() + assert.equal(supply, amountToMint) + }) + + it('should allow the registered validators contract to mint', async () => { + await stableToken.mint(validators, amountToMint, { from: validators }) + const balance = (await stableToken.balanceOf(validators)).toNumber() assert.equal(balance, amountToMint) const supply = (await stableToken.totalSupply()).toNumber() assert.equal(supply, amountToMint) }) it('should not allow anyone else to mint', async () => { - await assertRevert(stableToken.mint(minter, amountToMint, { from: accounts[1] })) + await assertRevert(stableToken.mint(validators, amountToMint, { from: accounts[2] })) }) }) @@ -143,7 +146,7 @@ contract('StableToken', (accounts: string[]) => { const comment = 'tacos at lunch' beforeEach(async () => { - await stableToken.setMinter(sender) + await registry.setAddressFor(CeloContractName.Exchange, sender) await stableToken.mint(sender, amountToMint) }) @@ -251,7 +254,7 @@ contract('StableToken', (accounts: string[]) => { const mintAmount = 1000 beforeEach(async () => { - await stableToken.setMinter(minter) + await registry.setAddressFor(CeloContractName.Exchange, minter) await stableToken.mint(minter, mintAmount) }) @@ -350,7 +353,7 @@ contract('StableToken', (accounts: string[]) => { const minter = accounts[0] const amountToBurn = 5 beforeEach(async () => { - await stableToken.setMinter(minter) + await registry.setAddressFor(CeloContractName.Exchange, minter) await stableToken.mint(minter, amountToMint) }) @@ -376,7 +379,7 @@ contract('StableToken', (accounts: string[]) => { const amount = new BigNumber(10000000000000000000) beforeEach(async () => { - await stableToken.setMinter(sender) + await registry.setAddressFor(CeloContractName.Exchange, sender) await stableToken.mint(sender, amount.times(2)) await stableToken.setInflationParameters(inflationRate, SECONDS_IN_A_WEEK) await timeTravel(SECONDS_IN_A_WEEK, web3) @@ -438,7 +441,7 @@ contract('StableToken', (accounts: string[]) => { const transferAmount = 1 beforeEach(async () => { - await stableToken.setMinter(sender) + await registry.setAddressFor(CeloContractName.Exchange, sender) await stableToken.mint(sender, amountToMint) }) diff --git a/packages/react-components/__mocks__/country-data.js b/packages/react-components/__mocks__/country-data.js deleted file mode 100644 index 04c9a239334..00000000000 --- a/packages/react-components/__mocks__/country-data.js +++ /dev/null @@ -1,8 +0,0 @@ -export default (CountryData = { - callingCountries: { - all: [ - { name: 'USA', countryCallingCodes: ['1'], alpha2: 'US' }, - { name: 'UK', countryCallingCodes: ['33'], alpha2: 'GB' }, - ], - }, -}) diff --git a/packages/react-components/components/Avatar.test.tsx b/packages/react-components/components/Avatar.test.tsx new file mode 100644 index 00000000000..3b6868c799b --- /dev/null +++ b/packages/react-components/components/Avatar.test.tsx @@ -0,0 +1,57 @@ +import Avatar from '@celo/react-components/components/Avatar' +import * as React from 'react' +import * as renderer from 'react-test-renderer' + +const mockName = 'mockName' +const mockCountryCode = '+1' +const mockNumber = '+14155556666' +const mockAccount = '0x0000000000000000000000000000000000007E57' +const mockContact = { + recordID: 'mockRecordId', + displayName: mockName, + phoneNumbers: [{ label: 'mockLabel', number: mockNumber }], + thumbnailPath: 'mockThumbpath', +} + +describe(Avatar, () => { + it('renders correctly without contact and number', () => { + const tree = renderer.create( + + ) + expect(tree).toMatchSnapshot() + }) + it('renders correctly with number but without contact', () => { + const tree = renderer.create( + + ) + expect(tree).toMatchSnapshot() + }) + it('renders correctly with address but without contact', () => { + const tree = renderer.create( + + ) + expect(tree).toMatchSnapshot() + }) + it('renders correctly with contact', () => { + const tree = renderer.create( + + ) + expect(tree).toMatchSnapshot() + }) +}) diff --git a/packages/react-components/components/Avatar.tsx b/packages/react-components/components/Avatar.tsx index de4ad61bdb6..f3060b7aba6 100644 --- a/packages/react-components/components/Avatar.tsx +++ b/packages/react-components/components/Avatar.tsx @@ -6,13 +6,13 @@ import { StyleSheet, Text, View } from 'react-native' import { MinimalContact } from 'react-native-contacts' export interface Props { + name: string + iconSize: number + defaultCountryCode: string contact?: MinimalContact - name?: string address?: string e164Number?: string thumbnailPath?: string - defaultCountryCode: string - iconSize: number } export class Avatar extends React.PureComponent { diff --git a/packages/react-components/components/Link.tsx b/packages/react-components/components/Link.tsx index 988bafff829..f32bc2260ad 100644 --- a/packages/react-components/components/Link.tsx +++ b/packages/react-components/components/Link.tsx @@ -1,8 +1,9 @@ +import Touchable, { Props as TouchableProps } from '@celo/react-components/components/Touchable' import { fontStyles } from '@celo/react-components/styles/fonts' import * as React from 'react' -import { Text, TextStyle, TouchableOpacity, TouchableOpacityProps } from 'react-native' +import { Text, TextStyle } from 'react-native' -type Props = TouchableOpacityProps & { +type Props = TouchableProps & { style?: TextStyle | TextStyle[] testID?: string } @@ -11,9 +12,9 @@ export default class Link extends React.PureComponent { render() { const { onPress, style: extraStyle, children, disabled, testID } = this.props return ( - + {children} - + ) } } diff --git a/packages/react-components/components/PhoneNumberInput.test.tsx b/packages/react-components/components/PhoneNumberInput.test.tsx index ee98fc9c73a..c5f80b5926d 100644 --- a/packages/react-components/components/PhoneNumberInput.test.tsx +++ b/packages/react-components/components/PhoneNumberInput.test.tsx @@ -1,100 +1,41 @@ import PhoneNumberInput from '@celo/react-components/components/PhoneNumberInput' -import { shallow } from 'enzyme' import * as React from 'react' -import { Text } from 'react-native' -import Autocomplete from 'react-native-autocomplete-input' -import * as renderer from 'react-test-renderer' +import { fireEvent, render } from 'react-native-testing-library' describe('PhoneNumberInput', () => { - it('renders correctly with minimum props', () => { - const tree = renderer.create( - - ) - expect(tree).toMatchSnapshot() - }) - describe('when defaultCountry is falsy', () => { - it('renders an AutoComplete', () => { - const numberInput = shallow( + it('renders an AutoComplete and a country can be selected', () => { + const mockSetCountryCode = jest.fn() + const { getByTestId, toJSON } = render( ) - expect(numberInput.find(Autocomplete).props()).toEqual( - expect.objectContaining({ placeholder: 'Nations' }) - ) - }) - describe('#renderItem', () => { - it('returns JSX with country name', () => { - const mockSetCountryCode = jest.fn() - const numberInput = shallow( - - ) - const instance = numberInput.instance() - - // @ts-ignore - const renderedItem = shallow(instance.renderItem({ item: 'GB' })) - expect( - renderedItem - .find(Text) - .last() - .children() - .text() - ).toEqual('UK') - }) + expect(toJSON()).toMatchSnapshot() + const autocomplete = getByTestId('CountryNameField') + expect(autocomplete).toBeTruthy() + fireEvent.changeText(autocomplete, 'Canada') + expect(mockSetCountryCode).toHaveBeenCalledWith('+1') }) }) - describe('when defaultCountry is truthy', () => { - let onEndEditingPhoneNumber: () => void - - beforeEach(() => { - onEndEditingPhoneNumber = jest.fn() - }) - - const numberInput = () => { - return shallow( - - ) - } - - it('does not render an AutoComplete', () => { - expect(numberInput().find(Autocomplete).length).toEqual(0) - }) +}) - it('renders a TextInput', () => { - expect( - numberInput() - .find('ValidatedTextInput') - .props() - ).toEqual( - expect.objectContaining({ - placeholder: '1800-867-5309', - onEndEditing: onEndEditingPhoneNumber, - }) - ) - }) +describe('when defaultCountry is truthy', () => { + it('does not render an AutoComplete', () => { + const { queryByTestId, toJSON } = render( + + ) + expect(toJSON()).toMatchSnapshot() + const autocomplete = queryByTestId('CountryNameField') + expect(autocomplete).toBeFalsy() }) }) diff --git a/packages/react-components/components/SmallButton.tsx b/packages/react-components/components/SmallButton.tsx index df5724cbef1..b9744f9e585 100644 --- a/packages/react-components/components/SmallButton.tsx +++ b/packages/react-components/components/SmallButton.tsx @@ -19,7 +19,16 @@ const TOUCH_OVERFLOW = 7 export default class SmallButton extends React.Component { render() { - const { onPress, text, accessibilityLabel, solid, disabled, style, textStyle } = this.props + const { + onPress, + text, + accessibilityLabel, + solid, + disabled, + style, + textStyle, + children, + } = this.props return ( { }} style={[styles.button, solid ? styles.solid : styles.hollow, style]} > - - {text} - + <> + {children} + + {text} + + ) } @@ -54,12 +67,11 @@ const PADDING_HORIZONTAL = 16 const styles = StyleSheet.create({ button: { - minWidth: 160, - textAlign: 'center', - paddingVertical: PADDING_VERTICAL, - paddingHorizontal: PADDING_HORIZONTAL, + flexDirection: 'row', alignSelf: 'flex-start', alignItems: 'center', + paddingVertical: PADDING_VERTICAL, + paddingHorizontal: PADDING_HORIZONTAL, borderRadius: 2, }, solid: { @@ -73,6 +85,11 @@ const styles = StyleSheet.create({ borderColor: colors.celoGreen, }, text: { + textAlign: 'center', lineHeight: 20, }, + textPadding: { + paddingLeft: 10, + paddingTop: 7, + }, }) diff --git a/packages/react-components/components/TextInput.tsx b/packages/react-components/components/TextInput.tsx index b27b5a17c79..e5b289e1123 100644 --- a/packages/react-components/components/TextInput.tsx +++ b/packages/react-components/components/TextInput.tsx @@ -69,7 +69,7 @@ export class CTextInput extends React.Component { ( - WrappedTextInput: React.ComponentType

+ WrappedTextInput: React.ComponentType

, + pasteIconContainerStyle?: ViewStyle ) { return class WithTextInputLabeling extends React.Component

{ state = { @@ -55,13 +55,15 @@ export default function withTextInputPasteAware

( render() { const { isPasteIconVisible } = this.state - // TODO(Rossy) Use a more paste-y instead of copy looking icon when we have one return ( {isPasteIconVisible && ( - - + + )} diff --git a/packages/react-components/components/__snapshots__/Avatar.test.tsx.snap b/packages/react-components/components/__snapshots__/Avatar.test.tsx.snap new file mode 100644 index 00000000000..fceaa2db368 --- /dev/null +++ b/packages/react-components/components/__snapshots__/Avatar.test.tsx.snap @@ -0,0 +1,454 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Avatar renders correctly with address but without contact 1`] = ` + + + + + M + + + + + mockName + + + #000000000000000... + + +`; + +exports[`Avatar renders correctly with contact 1`] = ` + + + + + + + + mockName + + + + 🇺🇸 +1 + + + (415) 555-6666 + + + +`; + +exports[`Avatar renders correctly with number but without contact 1`] = ` + + + + + M + + + + + mockName + + + + 🇺🇸 +1 + + + (415) 555-6666 + + + +`; + +exports[`Avatar renders correctly without contact and number 1`] = ` + + + + + M + + + + + mockName + + +`; diff --git a/packages/react-components/components/__snapshots__/PhoneNumberInput.test.tsx.snap b/packages/react-components/components/__snapshots__/PhoneNumberInput.test.tsx.snap index 7453081dd8d..10ce963f25d 100644 --- a/packages/react-components/components/__snapshots__/PhoneNumberInput.test.tsx.snap +++ b/packages/react-components/components/__snapshots__/PhoneNumberInput.test.tsx.snap @@ -1,6 +1,254 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PhoneNumberInput renders correctly with minimum props 1`] = ` +exports[`PhoneNumberInput when defaultCountry is falsy renders an AutoComplete and a country can be selected 1`] = ` + + + + + + + + + + + + + + + + + + + +`; + +exports[`when defaultCountry is truthy does not render an AutoComplete 1`] = ` + > + 🇨🇦 + - USA + Canada - 1 + +1 { + static defaultProps = { + width: 20, + height: 25, + color: colors.celoGreen, + } + + render() { + return ( + + + + ) + } +} diff --git a/packages/react-components/package.json b/packages/react-components/package.json index e6d12cabe29..c2223fd6197 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -35,7 +35,8 @@ "react-dom": "16.8.3", "react-native": "^0.59.5", "react-navigation": "^3.9.0", - "react-test-renderer": "16.8.3" + "react-test-renderer": "16.8.3", + "react-native-testing-library": "^1.9.1" }, "peerDependencies": { "react": "*", diff --git a/packages/react-components/tslint.json b/packages/react-components/tslint.json index 242151051dd..47fdd7c81d7 100644 --- a/packages/react-components/tslint.json +++ b/packages/react-components/tslint.json @@ -6,6 +6,7 @@ "rules": { "no-relative-imports": true, "max-classes-per-file": [true, 2], - "no-global-arrow-functions": false + "no-global-arrow-functions": false, + "react-hooks-nesting": "error" } } diff --git a/packages/typescript/package.json b/packages/typescript/package.json index 3f2abf304b5..4ab14fda300 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -13,6 +13,7 @@ "tslint-config-prettier": "^1.18.0", "tslint-eslint-rules": "^5.4.0", "tslint-microsoft-contrib": "^6.2.0", - "tslint-react": "^4.1.0" + "tslint-react": "^4.1.0", + "tslint-react-hooks": "^2.2.1" } } diff --git a/packages/typescript/tslint.json b/packages/typescript/tslint.json index 190b1a6b0f0..1dfa49d77c5 100644 --- a/packages/typescript/tslint.json +++ b/packages/typescript/tslint.json @@ -1,5 +1,5 @@ { - "extends": ["tslint:latest", "tslint-react", "tslint-config-prettier"], + "extends": ["tslint:latest", "tslint-react", "tslint-react-hooks", "tslint-config-prettier"], "rulesDirectory": [ "../../../node_modules/tslint-eslint-rules/dist/rules", "../../../node_modules/tslint-microsoft-contrib", diff --git a/packages/utils/package.json b/packages/utils/package.json index 0485521e709..125c0769bc9 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -29,7 +29,7 @@ "web3-utils": "1.0.0-beta.37", "keccak256": "^1.0.0", "buffer-reverse": "^1.0.1", - "bigi": "^1.1.0" + "bigi": "^1.1.0" }, "devDependencies": { "@celo/typescript": "0.0.1", diff --git a/packages/utils/src/async.ts b/packages/utils/src/async.ts index f4a696d5ece..7b26b1f8ae7 100644 --- a/packages/utils/src/async.ts +++ b/packages/utils/src/async.ts @@ -71,7 +71,7 @@ export async function concurrentMap( const remaining = xs.length - i const sliceSize = Math.min(remaining, concurrency) const slice = xs.slice(i, i + sliceSize) - res = res.concat(await Promise.all(slice.map(mapFn))) + res = res.concat(await Promise.all(slice.map((elem, index) => mapFn(elem, i + index)))) } return res } diff --git a/packages/verification-pool-api/src/config.ts b/packages/verification-pool-api/src/config.ts index 39431b76904..bd3120984f3 100644 --- a/packages/verification-pool-api/src/config.ts +++ b/packages/verification-pool-api/src/config.ts @@ -19,6 +19,8 @@ export const networkid = functionConfig[CELO_ENV]['testnet-id'] export const appSignature = functionConfig[CELO_ENV]['app-signature'] export const smsAckTimeout = functionConfig[CELO_ENV]['sms-ack-timeout'] || 5000 // default 5 seconds +console.debug(`Config settings: app-signture:${appSignature}, networkId:${networkid}`) + // @ts-ignore export const web3 = new Web3(`https://${CELO_ENV}-infura.celo-testnet.org`) diff --git a/packages/verifier/package.json b/packages/verifier/package.json index dbc4636eddb..272283fea52 100644 --- a/packages/verifier/package.json +++ b/packages/verifier/package.json @@ -48,7 +48,7 @@ "react-native-languages": "^3.0.1", "react-native-modal-dropdown": "^0.6.2", "react-native-restart-android": "^0.0.7", - "react-native-splash-screen": "^3.1.1", + "react-native-splash-screen": "^3.2.0", "react-native-svg": "^9.8.4", "react-navigation": "^3.9.0", "react-redux": "^7.1.1", diff --git a/packages/verifier/src/components/__snapshots__/PhoneNumberInput.test.tsx.snap b/packages/verifier/src/components/__snapshots__/PhoneNumberInput.test.tsx.snap index 7286762fea3..f69f3e933a6 100644 --- a/packages/verifier/src/components/__snapshots__/PhoneNumberInput.test.tsx.snap +++ b/packages/verifier/src/components/__snapshots__/PhoneNumberInput.test.tsx.snap @@ -122,20 +122,14 @@ exports[`PhoneNumberInput when defaultCountry renders defaults 1`] = ` placeholderTextColor="#D1D5D8" rejectResponderTermination={true} style={ - Array [ - Object { - "fontFamily": "Hind-Regular", - }, - Object { - "borderColor": "#D1D5D8", - "borderRadius": 3, - "padding": 8, - }, - Object { - "backgroundColor": "#FFFFFF", - "flex": 1, - }, - ] + Object { + "backgroundColor": "#FFFFFF", + "borderColor": "#D1D5D8", + "borderRadius": 3, + "flex": 1, + "fontFamily": "Hind-Regular", + "padding": 8, + } } testID="PhoneNumberField" underlineColorAndroid="transparent" @@ -281,20 +275,14 @@ exports[`PhoneNumberInput when no defaultCountry renders AutoComplete 1`] = ` renderSeparator={null} renderTextInput={[Function]} style={ - Array [ - Object { - "fontFamily": "Hind-Regular", - }, - Object { - "borderColor": "#D1D5D8", - "borderRadius": 3, - "padding": 8, - }, - Object { - "backgroundColor": "#FFFFFF", - "flex": 1, - }, - ] + Object { + "backgroundColor": "#FFFFFF", + "borderColor": "#D1D5D8", + "borderRadius": 3, + "flex": 1, + "fontFamily": "Hind-Regular", + "padding": 8, + } } testID="CountryNameField" underlineColorAndroid="transparent" @@ -382,20 +370,14 @@ exports[`PhoneNumberInput when no defaultCountry renders AutoComplete 1`] = ` placeholderTextColor="#D1D5D8" rejectResponderTermination={true} style={ - Array [ - Object { - "fontFamily": "Hind-Regular", - }, - Object { - "borderColor": "#D1D5D8", - "borderRadius": 3, - "padding": 8, - }, - Object { - "backgroundColor": "#FFFFFF", - "flex": 1, - }, - ] + Object { + "backgroundColor": "#FFFFFF", + "borderColor": "#D1D5D8", + "borderRadius": 3, + "flex": 1, + "fontFamily": "Hind-Regular", + "padding": 8, + } } testID="PhoneNumberField" underlineColorAndroid="transparent" diff --git a/packages/verifier/tslint.json b/packages/verifier/tslint.json index 2da7638bf5f..099e1d034b7 100644 --- a/packages/verifier/tslint.json +++ b/packages/verifier/tslint.json @@ -6,6 +6,7 @@ "rules": { "no-relative-imports": true, "max-classes-per-file": [true, 2], - "no-global-arrow-functions": false + "no-global-arrow-functions": false, + "react-hooks-nesting": "error" } } diff --git a/packages/web/src/community/connect/FellowViewer.tsx b/packages/web/src/community/connect/FellowViewer.tsx index 7d8cf2ecb38..ae3676dc7de 100644 --- a/packages/web/src/community/connect/FellowViewer.tsx +++ b/packages/web/src/community/connect/FellowViewer.tsx @@ -16,7 +16,7 @@ const fellows = [ role: ' Go-to-Market', color: colors.purpleScreen, quote: - '“My grandparents were Mexican migrant farmers—I saw first hand how access to basic financial tools can save lives.”', + '“My grandparents were Mexican migrant farmers—I saw first hand how access to basic financial tools can change lives.”', text: 'Xochitl recently graduated from Stanford Graduate School of Business. Prior to Stanford, she was a Director at Cisco where she led expansion into 26 Emerging Markets. She is leveraging her background and expertise to explore Mexico as a potential launch country for Celo. Her key activities include country landscaping, user research, pilot scoping and implementation, analysis and final recommendations.', }, diff --git a/packages/web/src/shared/Button.3.tsx b/packages/web/src/shared/Button.3.tsx index 0cf933e5881..b2867ba2125 100644 --- a/packages/web/src/shared/Button.3.tsx +++ b/packages/web/src/shared/Button.3.tsx @@ -45,6 +45,7 @@ type PrimaryProps = { type InlineProps = { kind: BTN.INLINE + style?: TextStyle } & AllButtonProps type NavProps = { diff --git a/packages/web/src/shared/Footer.3.tsx b/packages/web/src/shared/Footer.3.tsx index d476b00ac3c..a7e5edd7580 100644 --- a/packages/web/src/shared/Footer.3.tsx +++ b/packages/web/src/shared/Footer.3.tsx @@ -1,7 +1,8 @@ import Link from 'next/link' import * as React from 'react' import { StyleSheet, Text, View } from 'react-native' -import { I18nProps, NameSpaces, withNamespaces } from 'src/i18n' +import { I18nProps, NameSpaces, Trans, withNamespaces } from 'src/i18n' +import Discord from 'src/icons/Discord' import Discourse from 'src/icons/Discourse' import MediumLogo from 'src/icons/MediumLogo' import Octocat from 'src/icons/Octocat' @@ -70,6 +71,11 @@ const Social = React.memo(function _Social() { + + + + + ) @@ -132,6 +138,13 @@ const Details = React.memo(function _Details({ t }: { t: I18nProps['t'] }) { {t('disclaimer')} + + + + Terms of Service + + + {t('copyRight')} @@ -139,6 +152,10 @@ const Details = React.memo(function _Details({ t }: { t: I18nProps['t'] }) { ) }) +function LinkButon({ children }) { + return