diff --git a/.bulldozer.yml b/.bulldozer.yml new file mode 100644 index 00000000000..c7787db9535 --- /dev/null +++ b/.bulldozer.yml @@ -0,0 +1,66 @@ +# Documentation for this file +# https://github.com/palantir/bulldozer#configuration + +# "version" is the configuration version, currently "1". +version: 1 + +# "merge" defines how and when pull requests are merged. If the section is +# missing, bulldozer will consider all pull requests and use default settings. +merge: + # "whitelist" defines the set of pull requests considered by bulldozer. If + # the section is missing, bulldozer considers all pull requests not excluded + # by the blacklist. + whitelist: + # Pull requests with any of these labels (case-insensitive) are added to + # the whitelist. + labels: ["automerge"] + + # "method" defines the merge method. The available options are "merge", + # "rebase", and "squash". + method: squash + + # "options" defines additional options for the individual merge methods. + options: + # "squash" options are only used when the merge method is "squash" + squash: + # "title" defines how the title of the commit message is created when + # generating a squash commit. The options are "pull_request_title", + # "first_commit_title", and "github_default_title". The default is + # "pull_request_title". + title: "pull_request_title" + + # "body" defines how the body of the commit message is created when + # generating a squash commit. The options are "pull_request_body", + # "summarize_commits", and "empty_body". The default is "empty_body". + body: "pull_request_body" + + # If "body" is "pull_request_body", then the commit message will be the + # part of the pull request body surrounded by "message_delimiter" + # strings. This is disabled (empty string) by default. + message_delimiter: ==commits== + + # "required_statuses" is a list of additional status contexts that must pass + # before bulldozer can merge a pull request. This is useful if you want to + # require extra testing for automated merges, but not for manual merges. + required_statuses: + - "ci/circleci: general-test" + - "ci/circleci: install_dependencies" + - "ci/circleci: lint-checks" + - "ci/circleci: mobile-test" + - "ci/circleci: mobile-test-build-app" + - "ci/circleci: protocol-test" + - "ci/circleci: verification-pool-api" + - "ci/circleci: end-to-end-geth-transfer-test" + - "ci/circleci: end-to-end-geth-sync-test" + - "ci/circleci: end-to-end-geth-governance-test" + + # If true, bulldozer will delete branches after their pull requests merge. + delete_after_merge: true + +# "update" defines how and when to update pull request branches. Unlike with +# merges, if this section is missing, bulldozer will not update any pull requests. +update: + # "whitelist" defines the set of pull requests that should be updated by + # bulldozer. It accepts the same keys as the whitelist in the "merge" block. + whitelist: + labels: ["automerge"] diff --git a/.circleci/config.yml b/.circleci/config.yml index d3ce211a131..1d31bd444ec 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,6 +7,9 @@ defaults: &defaults working_directory: ~/app docker: - image: celohq/node8:gcloud-deps + environment: + # To avoid ENOMEM problem when running node + NODE_OPTIONS: "--max-old-space-size=4096" android-defaults: &android-defaults <<: *defaults @@ -25,46 +28,53 @@ jobs: install_dependencies: <<: *defaults steps: - # The standard Circle CI checkout steps makes a shallow clone with the depth of 1 - # Which means we have no way of knowing which commits are specifically in this branch. - # We need that for incremental testing. - # This checkout assumes that a single branch will not have more than a certain number commits - # on top of master and if it does, then the incremental testing script will fail. The committer can - # than squash those commits. Which is OK since we squash commits on merge to master - # anyways. - - run: - name: Checkout (with last 100 commits) + + - restore_cache: + keys: + - source-v1-{{ .Branch }}-{{ .Revision }} + - source-v1-{{ .Branch }}- + - source-v1- + + - checkout + + - save_cache: + key: source-v1-{{ .Branch }}-{{ .Revision }} + paths: + - ".git" + + - run: + name: Verify setup for incremental testing command: | - mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config - git clone --depth 100 --no-single-branch --branch ${CIRCLE_BRANCH} git@github.com:celo-org/celo-monorepo.git ~/app + set -euo pipefail cd ~/app set -v # To get the "master" branch mapping git checkout master git checkout ${CIRCLE_BRANCH} - # Verify that these commands work, they are later called in the incremental testing script + # Verify that following commands work, they are later called in the incremental testing script # There output does not matter here, the fact that they finish successfully does. git rev-parse --abbrev-ref HEAD - git merge-base master $(git rev-parse --abbrev-ref HEAD) - git log --format=format:%H $(git merge-base master $(git rev-parse --abbrev-ref HEAD))..HEAD > /dev/null - attach_workspace: at: ~/app - restore_cache: - key: yarn-v2-{{ checksum "yarn.lock" }}-{{ arch }} + keys: + - yarn-v2-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }} + - yarn-v2-{{ arch }}-{{ .Branch }}- + - yarn-v2-{{ arch }}- + - yarn-v2- - run: name: Delete @celo dir from node_modules (if its there) command: rm -rf ~/app/node_modules/@celo - run: - name: yarn + name: Install dependencies command: | # Deals with yarn install flakiness which can come due to yarnpkg.com being # unreliable. For example, https://circleci.com/gh/celo-org/celo-monorepo/82685 yarn install || yarn install - - run: name: Fail if someone forgot to commit "yarn.lock" command: | @@ -72,62 +82,36 @@ jobs: echo "There are git differences after running yarn install" exit 1 fi - - run: npm rebuild scrypt - save_cache: - key: yarn-v2-{{ checksum "yarn.lock" }}-{{ arch }} + key: yarn-v2-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }} paths: - node_modules - packages/*/node_modules + + - run: + name: Build packages + command: | + # separate build to avoid ENOMEN in CI :( + yarn build --scope @celo/protocol + yarn build --scope docs + yarn build --scope @celo/walletkit + yarn build --ignore @celo/protocol --ignore docs --ignore @celo/walletkit + - persist_to_workspace: root: . - paths: . + paths: + - . lint-checks: <<: *defaults steps: - attach_workspace: at: ~/app - - - run: yarn run ci-lint - - build-all-packages: - <<: *defaults - steps: - - attach_workspace: - at: ~/app - - - run: - name: Build SDK - command: | - set -e - yarn --cwd=packages/contractkit build alfajores - - - run: - name: Build CLI - command: | - yarn --cwd=packages/cli setup:environment alfajores - yarn --cwd=packages/cli build - - - run: - name: Build mobile - command: yarn --cwd=packages/mobile build - - - run: - name: Build Verification Pool - command: yarn --cwd=packages/verification-pool-api compile-typescript - - - run: - name: Build Verifier - command: yarn --cwd=packages/verifier build:typescript - - - run: - name: Build Web - command: | - set -e - yarn --cwd=packages/web build + - run: yarn run prettify:diff + - run: yarn run lint general-test: <<: *defaults @@ -139,7 +123,7 @@ jobs: name: jest tests command: | mkdir -p test-results/jest - yarn run lerna --ignore @celo/mobile --ignore @celo/protocol --ignore @celo/celotool --ignore @celo/contractkit --ignore @celo/celocli run test + yarn run lerna --ignore @celo/mobile --ignore @celo/protocol --ignore @celo/celotool --ignore @celo/walletkit --ignore @celo/celocli run test mobile-test-build-app: working_directory: ~/app @@ -167,15 +151,15 @@ jobs: - run: name: Ensure translations are not missing command: | - set -euo pipefail - diff <(cat packages/mobile/locales/en-US/*.json | jq keys | sort) <(cat packages/mobile/locales/es-AR/*.json | jq keys | sort) + cd packages/mobile + ./scripts/verify_locales.sh - run: name: jest tests command: | mkdir -p test-results/jest # Tests fail with https://stackoverflow.com/questions/38558989/node-js-heap-out-of-memory without this - NODE_OPTIONS="--max-old-space-size=4096" yarn --cwd packages/mobile test + NODE_OPTIONS="--max-old-space-size=4096" yarn --cwd packages/mobile test:ci environment: JEST_JUNIT_OUTPUT: test-results/jest/junit.xml @@ -224,7 +208,7 @@ jobs: # Flaky tests - run them twice command: yarn --cwd packages/protocol test || yarn --cwd packages/protocol test - contractkit-test: + walletkit-test: <<: *defaults steps: - attach_workspace: @@ -234,17 +218,17 @@ jobs: command: | # Test alphanet set -euo pipefail - yarn --cwd=packages/contractkit build alfajores - yarn --cwd=packages/contractkit test + yarn --cwd=packages/walletkit build alfajores + yarn --cwd=packages/walletkit test - run: name: test alphanet staging command: | # Test alphanet set -euo pipefail - yarn --cwd=packages/contractkit build alfajoresstaging - yarn --cwd=packages/contractkit test - + yarn --cwd=packages/walletkit build alfajoresstaging + yarn --cwd=packages/walletkit test + cli-test: <<: *defaults steps: @@ -255,6 +239,15 @@ jobs: command: | set -euo pipefail yarn --cwd=packages/cli test + - run: + name: Fail if someone forgot to commit CLI docs + command: | + yarn --cwd=packages/cli docs + if [[ $(git status packages/docs/command-line-interface --porcelain) ]]; then + git --no-pager diff packages/docs/command-line-interface + echo "There are git differences after generating CLI docs" + exit 1 + fi end-to-end-geth-transfer-test: <<: *defaults @@ -275,12 +268,21 @@ jobs: 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}:/tmp/go/bin + 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 @@ -288,6 +290,8 @@ jobs: end-to-end-geth-governance-test: <<: *defaults + # Source: https://circleci.com/docs/2.0/configuration-reference/#resource_class + resource_class: medium+ steps: - attach_workspace: at: ~/app @@ -305,12 +309,21 @@ jobs: 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}:/tmp/go/bin + 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 @@ -335,11 +348,20 @@ jobs: 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}:/tmp/go/bin + 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 @@ -364,15 +386,19 @@ jobs: 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 - run: name: Run test command: | set -e - export PATH=${PATH}:/tmp/go/bin + 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_integration_sync.sh checkout master + ./ci_test_sync_with_network.sh checkout master web: working_directory: ~/app @@ -385,6 +411,24 @@ jobs: - run: mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config - run: cd packages/web && ./circle_deploy.sh + test-npm-package-install: + working_directory: ~/app + docker: + - image: celohq/node8:gcloud + steps: + - run: + name: Installing npm package - @celo/typescript + command: yarn add @celo/typescript + - run: + name: Installing npm package - @celo/utils + command: yarn add @celo/utils + - run: + name: Installing npm package - @celo/walletkit + command: yarn add @celo/walletkit + - run: + name: Installing npm package - @celo/celocli + command: yarn add @celo/celocli + workflows: version: 2 celo-monorepo-build: @@ -393,51 +437,53 @@ workflows: - lint-checks: requires: - install_dependencies - - build-all-packages: - requires: - - install_dependencies - - lint-checks - general-test: requires: - install_dependencies - - lint-checks - - contractkit-test: + - walletkit-test: requires: - install_dependencies - - lint-checks - cli-test: requires: - install_dependencies - - lint-checks - mobile-test: requires: - - install_dependencies - lint-checks - mobile-test-build-app: requires: - - install_dependencies - - lint-checks + - mobile-test - verification-pool-api: requires: - - install_dependencies - lint-checks - protocol-test: requires: - - install_dependencies - lint-checks + - walletkit-test - end-to-end-geth-transfer-test: requires: - - install_dependencies - lint-checks + - walletkit-test - end-to-end-geth-governance-test: requires: - - install_dependencies - lint-checks + - walletkit-test - end-to-end-geth-sync-test: requires: - - install_dependencies - lint-checks + - walletkit-test - end-to-end-geth-integration-sync-test: requires: - - install_dependencies - lint-checks + - walletkit-test + nightly: + triggers: + - schedule: + # 7 PM in UTC = noon in PDT. + # Best for test to fail during SF afternoon, so that, someone can fix it during the day time. + cron: "0 19 * * *" + filters: + branches: + only: + - master + jobs: + - test-npm-package-install diff --git a/.dockerignore b/.dockerignore index 6fa84791dec..d87868e8d46 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,20 @@ -packages/mobile node_modules **/node_modules -*node_modules \ No newline at end of file +*node_modules + +# root folders +.github +dockerfiles +.vscode + +# not docker packages +packages/analytics +packages/blockchain-api +packages/docs +packages/faucet +packages/helm-charts +packages/mobile +packages/notification-service +# packages/verification-pool-api +packages/verifier +packages/web diff --git a/.env b/.env index 631fc2f4b26..5bfed888bc6 100644 --- a/.env +++ b/.env @@ -16,16 +16,16 @@ BLOCKSCOUT_WEB_REPLICAS=3 BLOCKSCOUT_DB_SUFFIX= GETH_NODE_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/geth" -GETH_NODE_DOCKER_IMAGE_TAG="ac6c5bbb2af0f27400c77ef9c155056ff3b73d45" +GETH_NODE_DOCKER_IMAGE_TAG="f7095b78003062db9536e1d070772d20a3f81e93" GETH_BOOTNODE_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/geth-all" -GETH_BOOTNODE_DOCKER_IMAGE_TAG="ac6c5bbb2af0f27400c77ef9c155056ff3b73d45" +GETH_BOOTNODE_DOCKER_IMAGE_TAG="f7095b78003062db9536e1d070772d20a3f81e93" CELOTOOL_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -CELOTOOL_DOCKER_IMAGE_TAG="celotool-4c8adab747673a220ab6803c9e6a01ecca61e5b1" +CELOTOOL_DOCKER_IMAGE_TAG="celotool-552b1accf90404fdcd886670d150af0a5cae116f" TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-4c8adab747673a220ab6803c9e6a01ecca61e5b1" +TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-552b1accf90404fdcd886670d150af0a5cae116f" GETH_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet-production/geth-exporter" GETH_EXPORTER_DOCKER_IMAGE_TAG="ed7d21bd50592709173368cd697ef73c1774a261" diff --git a/.env.alfajores b/.env.alfajores index 3222aa1a6c7..196cc877c53 100644 --- a/.env.alfajores +++ b/.env.alfajores @@ -18,24 +18,24 @@ BLOCKSCOUT_SUBNETWORK_NAME="Alfajores" 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` -GETH_NODE_DOCKER_IMAGE_TAG="73c1d86b519c72722a924664304e6573cbcf77fa" +GETH_NODE_DOCKER_IMAGE_TAG="f7095b78003062db9536e1d070772d20a3f81e93" GETH_BOOTNODE_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/geth-all" # When upgrading change this to latest commit hash from the master of the geth repo # `geth $ git show | head -n 1` -GETH_BOOTNODE_DOCKER_IMAGE_TAG="73c1d86b519c72722a924664304e6573cbcf77fa" +GETH_BOOTNODE_DOCKER_IMAGE_TAG="f7095b78003062db9536e1d070772d20a3f81e93" CELOTOOL_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -CELOTOOL_DOCKER_IMAGE_TAG="celotool-0df549bcf4e147b2ac593aa7a19260ba17a77ff5" +CELOTOOL_DOCKER_IMAGE_TAG="celotool-552b1accf90404fdcd886670d150af0a5cae116f" TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-0df549bcf4e147b2ac593aa7a19260ba17a77ff5" +TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-552b1accf90404fdcd886670d150af0a5cae116f" GETH_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet-production/geth-exporter" -GETH_EXPORTER_DOCKER_IMAGE_TAG="ed7d21bd50592709173368cd697ef73c1774a261" +GETH_EXPORTER_DOCKER_IMAGE_TAG="6df683de7ae30d3fbca384abb14599d0e8130d35" # Genesis Vars -NETWORK_ID=44781 +NETWORK_ID=44782 CONSENSUS_TYPE="istanbul" PREDEPLOYED_CONTRACTS="REGISTRY" BLOCK_TIME=5 diff --git a/.env.alfajoresstaging b/.env.alfajoresstaging index 871816589e2..c88440b0c96 100644 --- a/.env.alfajoresstaging +++ b/.env.alfajoresstaging @@ -1,3 +1,5 @@ +# Don't use "//" for comments in this file. +# This file is meant to be executed as a bash script for testing. ENV_TYPE="staging" GETH_VERBOSITY=2 @@ -12,31 +14,37 @@ BLOCKSCOUT_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/blockscout" BLOCKSCOUT_WEB_DOCKER_IMAGE_TAG="web-f6c3e0888d1d0ef72dc8bf870808702b7fd13730" BLOCKSCOUT_INDEXER_DOCKER_IMAGE_TAG="indexer-f6c3e0888d1d0ef72dc8bf870808702b7fd13730" BLOCKSCOUT_WEB_REPLICAS=3 -BLOCKSCOUT_DB_SUFFIX="8" +# Increment this value everytime you redeploy blockscout. Or else the deployment will fail due to the +# existing database. +BLOCKSCOUT_DB_SUFFIX="10" BLOCKSCOUT_SUBNETWORK_NAME="Alfajores Staging" -GETH_NODE_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/geth" +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` -GETH_NODE_DOCKER_IMAGE_TAG="e494765666210d4ec3bf98d120ff826ac5051884" +GETH_NODE_DOCKER_IMAGE_TAG="f7095b78003062db9536e1d070772d20a3f81e93" GETH_BOOTNODE_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/geth-all" # When upgrading change this to latest commit hash from the master of the geth repo # `geth $ git show | head -n 1` -GETH_BOOTNODE_DOCKER_IMAGE_TAG="e494765666210d4ec3bf98d120ff826ac5051884" +GETH_BOOTNODE_DOCKER_IMAGE_TAG="f7095b78003062db9536e1d070772d20a3f81e93" CELOTOOL_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -CELOTOOL_DOCKER_IMAGE_TAG="d711a942226c47b92f8cba8f224306bd96ea02a0" +CELOTOOL_DOCKER_IMAGE_TAG="celotool-552b1accf90404fdcd886670d150af0a5cae116f" + +TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" +TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-552b1accf90404fdcd886670d150af0a5cae116f" GETH_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet-production/geth-exporter" -GETH_EXPORTER_DOCKER_IMAGE_TAG="ed7d21bd50592709173368cd697ef73c1774a261" +GETH_EXPORTER_DOCKER_IMAGE_TAG="6df683de7ae30d3fbca384abb14599d0e8130d35" # Genesis Vars NETWORK_ID=1101 CONSENSUS_TYPE="istanbul" PREDEPLOYED_CONTRACTS="REGISTRY" BLOCK_TIME=5 -EPOCH=17280 // Minimum epoch length is 1 day +# Minimum epoch length is 1 day +EPOCH=17280 # "og" -> our original 4 tx nodes, "${n}" -> for deriving n tx nodes from the MNEMONIC # NOTE: we only create static IPs when TX_NODES is set to "og" @@ -67,6 +75,4 @@ NOTIFICATION_SERVICE_FIREBASE_DB="https://console.firebase.google.com/u/0/projec PROMTOSD_SCRAPE_INTERVAL="5m" PROMTOSD_EXPORT_INTERVAL="5m" -AUCTION_CRON_SPEC="*/5 * * * *" - SMS_RETRIEVER_HASH_CODE=l5k6LvdPDXS diff --git a/.env.appintegration b/.env.appintegration deleted file mode 100644 index 966c17620d0..00000000000 --- a/.env.appintegration +++ /dev/null @@ -1,70 +0,0 @@ -ENV_TYPE="appintegration" - -GETH_VERBOSITY=3 - -KUBERNETES_CLUSTER_NAME="integration" -KUBERNETES_CLUSTER_ZONE="us-west1-a" -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-af8cefc512035b8fea9fd61e2828b9b9d6f2ae96" -BLOCKSCOUT_INDEXER_DOCKER_IMAGE_TAG="indexer-af8cefc512035b8fea9fd61e2828b9b9d6f2ae96" -BLOCKSCOUT_WEB_REPLICAS=3 -BLOCKSCOUT_DB_SUFFIX="5" -BLOCKSCOUT_SUBNETWORK_NAME="App Integration" - -GETH_NODE_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/geth" -GETH_NODE_DOCKER_IMAGE_TAG="94c7a784fe0e44737c4f2637b52ead0f6d98a26c" - -GETH_BOOTNODE_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/geth-all" -GETH_BOOTNODE_DOCKER_IMAGE_TAG="94c7a784fe0e44737c4f2637b52ead0f6d98a26c" - -CELOTOOL_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -CELOTOOL_DOCKER_IMAGE_TAG="d711a942226c47b92f8cba8f224306bd96ea02a0" - -GETH_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet-production/geth-exporter" -GETH_EXPORTER_DOCKER_IMAGE_TAG="ed7d21bd50592709173368cd697ef73c1774a261" - -# Genesis Vars -NETWORK_ID=1101 -CONSENSUS_TYPE="istanbul" -PREDEPLOYED_CONTRACTS="REGISTRY" -BLOCK_TIME=5 -EPOCH=3000000 - -# "og" -> our original 4 tx nodes, "${n}" -> for deriving n tx nodes from the MNEMONIC -# NOTE: we only create static IPs when TX_NODES is set to "og" -VALIDATORS=10 -TX_NODES=4 -STATIC_IPS_FOR_GETH_NODES=false - -ADMIN_RPC_ENABLED=false - -# Testnet vars -GETH_NODES_BACKUP_CRONJOB_ENABLED=true -CONTRACT_CRONJOBS_ENABLED=true -CLUSTER_CREATION_FLAGS="--enable-autoscaling --min-nodes 3 --max-nodes 8 --machine-type=n1-standard-4" - - -GETH_NODE_CPU_REQUEST=400m -GETH_NODE_MEMORY_REQUEST=2.5G - -VERIFICATION_REWARDS_ADDRESS="0xb4fdaf5f3cd313654aa357299ada901b1d2dd3b5" - -VERIFICATION_POOL_URL="https://us-central1-celo-testnet.cloudfunctions.net/handleVerificationRequestintegration/v0.1/sms/" -VERIFICATION_REWARDS_URL="https://us-central1-celo-testnet.cloudfunctions.net/handleVerificationRequestintegration/v0.1/rewards/" - -STACKDRIVER_MONITORING_DASHBOARD="https://app.google.stackdriver.com/dashboards/17701013576385040071?project=celo-testnet" -STACKDRIVER_NOTIFICATION_CHANNEL="12047595356119796119" -MOBILE_WALLET_PLAYSTORE_LINK="https://play.google.com/apps/internaltest/4700990475000634666" - -NOTIFICATION_SERVICE_FIREBASE_DB="https://console.firebase.google.com/u/0/project/celo-org-mobile/database/celo-org-mobile-int/data" - -PROMTOSD_SCRAPE_INTERVAL="5m" -PROMTOSD_EXPORT_INTERVAL="5m" - -AUCTION_CRON_SPEC="*/5 * * * *" - -SMS_RETRIEVER_HASH_CODE=l5k6LvdPDXS diff --git a/.env.integration b/.env.integration index 4db170aa9ad..58f45b32786 100644 --- a/.env.integration +++ b/.env.integration @@ -9,27 +9,27 @@ 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-fb9a5bd46a0968865ef30cc568a260f01cb7fdaf" -BLOCKSCOUT_INDEXER_DOCKER_IMAGE_TAG="indexer-fb9a5bd46a0968865ef30cc568a260f01cb7fdaf" +BLOCKSCOUT_WEB_DOCKER_IMAGE_TAG="web-f6c3e0888d1d0ef72dc8bf870808702b7fd13730" +BLOCKSCOUT_INDEXER_DOCKER_IMAGE_TAG="indexer-f6c3e0888d1d0ef72dc8bf870808702b7fd13730" BLOCKSCOUT_WEB_REPLICAS=3 -BLOCKSCOUT_DB_SUFFIX="15" +BLOCKSCOUT_DB_SUFFIX="18" BLOCKSCOUT_SUBNETWORK_NAME="Integration" 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` -GETH_NODE_DOCKER_IMAGE_TAG="56f21be62efed2397fe68a846d83174491ad6283" +GETH_NODE_DOCKER_IMAGE_TAG="f7095b78003062db9536e1d070772d20a3f81e93" GETH_BOOTNODE_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/geth-all" # When upgrading change this to latest commit hash from the master of the geth repo # `geth $ git show | head -n 1` -GETH_BOOTNODE_DOCKER_IMAGE_TAG="56f21be62efed2397fe68a846d83174491ad6283" +GETH_BOOTNODE_DOCKER_IMAGE_TAG="f7095b78003062db9536e1d070772d20a3f81e93" CELOTOOL_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -CELOTOOL_DOCKER_IMAGE_TAG="celotool-4c8adab747673a220ab6803c9e6a01ecca61e5b1" +CELOTOOL_DOCKER_IMAGE_TAG="celotool-552b1accf90404fdcd886670d150af0a5cae116f" TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-4c8adab747673a220ab6803c9e6a01ecca61e5b1" +TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-552b1accf90404fdcd886670d150af0a5cae116f" GETH_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet-production/geth-exporter" GETH_EXPORTER_DOCKER_IMAGE_TAG="ed7d21bd50592709173368cd697ef73c1774a261" @@ -43,8 +43,8 @@ EPOCH=1000 # "og" -> our original 4 tx nodes, "${n}" -> for deriving n tx nodes from the MNEMONIC # NOTE: we only create static IPs when TX_NODES is set to "og" -VALIDATORS=1 -TX_NODES=1 +VALIDATORS=20 +TX_NODES=2 STATIC_IPS_FOR_GETH_NODES=false ADMIN_RPC_ENABLED=false diff --git a/.env.mnemonic.appintegration.enc b/.env.mnemonic.appintegration.enc deleted file mode 100644 index 70a7f217c8c..00000000000 Binary files a/.env.mnemonic.appintegration.enc and /dev/null differ diff --git a/.env.mnemonic.pilot.enc b/.env.mnemonic.pilot.enc new file mode 100644 index 00000000000..1a8ef9f741a Binary files /dev/null and b/.env.mnemonic.pilot.enc differ diff --git a/.env.mnemonic.pilotstaging.enc b/.env.mnemonic.pilotstaging.enc new file mode 100644 index 00000000000..cceaa9f4d7d Binary files /dev/null and b/.env.mnemonic.pilotstaging.enc differ diff --git a/.env.pilot b/.env.pilot index ced0e8c70b6..3f4042a0435 100644 --- a/.env.pilot +++ b/.env.pilot @@ -12,7 +12,7 @@ BLOCKSCOUT_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/blockscout" BLOCKSCOUT_WEB_DOCKER_IMAGE_TAG="web-f6c3e0888d1d0ef72dc8bf870808702b7fd13730" BLOCKSCOUT_INDEXER_DOCKER_IMAGE_TAG="indexer-f6c3e0888d1d0ef72dc8bf870808702b7fd13730" BLOCKSCOUT_WEB_REPLICAS=3 -BLOCKSCOUT_DB_SUFFIX="1" +BLOCKSCOUT_DB_SUFFIX="2" BLOCKSCOUT_SUBNETWORK_NAME="Pilot" GETH_NODE_DOCKER_IMAGE_REPOSITORY="us.gcr.io/celo-testnet/geth" diff --git a/.env.pilotstaging b/.env.pilotstaging index 7b983bfcc62..744d0a4e4e1 100644 --- a/.env.pilotstaging +++ b/.env.pilotstaging @@ -12,7 +12,7 @@ BLOCKSCOUT_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/blockscout" BLOCKSCOUT_WEB_DOCKER_IMAGE_TAG="web-f6c3e0888d1d0ef72dc8bf870808702b7fd13730" BLOCKSCOUT_INDEXER_DOCKER_IMAGE_TAG="indexer-f6c3e0888d1d0ef72dc8bf870808702b7fd13730" BLOCKSCOUT_WEB_REPLICAS=3 -BLOCKSCOUT_DB_SUFFIX="16" +BLOCKSCOUT_DB_SUFFIX="3" BLOCKSCOUT_SUBNETWORK_NAME="Pilot Staging" GETH_NODE_DOCKER_IMAGE_REPOSITORY="us.gcr.io/celo-testnet/geth" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 12ac046425f..e81c48806aa 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,7 +11,7 @@ /packages/notification-service/ @jmrossy @cmcewen /packages/protocol/ @asaj @m-chrzan /packages/react-components/ @cmcewen @jmrossy -/packages/contractkit/ @ashishb @yerdua +/packages/walletkit/ @ashishb @yerdua /packages/transaction-metrics-exporter @nambrot /packages/typescript/ @cmcewen /packages/utils/ @jmrossy diff --git a/.gitignore b/.gitignore index cf5c2930335..15c9b933a1b 100644 --- a/.gitignore +++ b/.gitignore @@ -76,13 +76,10 @@ lerna-debug.log **/types/typechain/ # keys -.env.mnemonic -.env.mnemonic.alfajores -.env.mnemonic.alfajoresstaging -.env.mnemonic.appintegration -.env.mnemonic.integration -.env.mnemonic.integrationtesting +.env.mnemonic* +!.env.mnemonic*.enc # LicenseDisclaimer **/LicenseDisclaimer.txt +.terraform/ diff --git a/.mergify.yml b/.mergify.yml deleted file mode 100644 index bfcb3165ce5..00000000000 --- a/.mergify.yml +++ /dev/null @@ -1,29 +0,0 @@ -# Source: https://doc.mergify.io/getting-started.html#configuration -pull_request_rules: - - name: Automatically merge on CI success and code review - conditions: - # Add this label when you are ready to automerge the pull request. - - label=automerge - # At least one approval required - - "#approved-reviews-by>=1" - # No pending changes requested - - "#changes-requested-reviews-by=0" - # Only enable this when the pull request is being merged into master - - "base=master" - # List of all the tests that should pass. - - "status-success=ci/circleci: general-test" - - "status-success=ci/circleci: install_dependencies" - - "status-success=ci/circleci: lint-checks" - - "status-success=ci/circleci: mobile-test" - - "status-success=ci/circleci: protocol-test" - - "status-success=ci/circleci: verification-pool-api" - - "status-success=ci/circleci: end-to-end-geth-transfer-test" - - "status-success=ci/circleci: end-to-end-geth-sync-test" - - "status-success=ci/circleci: end-to-end-geth-governance-test" - actions: - merge: - method: squash - # https://doc.mergify.io/strict-workflow.html - strict: true - # Delete the branch - delete_head_branch: diff --git a/.prettierignore b/.prettierignore index 264ea8e386e..89bbc62ec64 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,21 +1,38 @@ package.json **/.next **/coverage -packages/functions/lib -packages/protocol/types/typechain +**/node_modules +**/privacy/* +.git/ + +packages/blockchain-api/dist + +packages/cli/lib + +packages/walletkit/lib/ +packages/walletkit/.artifacts/ +packages/walletkit/contracts/ +packages/walletkit/types/ +packages/dappkit/lib/ +packages/docs/_book +packages/faucet/dist/ +packages/mobile/src/geth/*.json +packages/notification-service/dist/ + +packages/protocol/build/ +packages/protocol/types/ packages/protocol/lib/**/*.js packages/protocol/scripts/**/*.js packages/protocol/migrations/**/*.js packages/protocol/tests/**/*.js -packages/protocol/types/**/*.js -**/privacy/* -packages/mobile/src/geth/*.json + packages/transaction-metrics-exporter/src/contracts/*.ts -packages/verification-pool-api/lib/**/*.js +packages/dappkit/lib/ +packages/utils/lib/ + +packages/verification-pool-api/lib/ packages/verification-pool-api/contracts/*.ts -packages/contractkit/.artifacts/ -packages/contractkit/contracts/ -packages/contractkit/lib/ -packages/contractkit/types/ + packages/web/dist -.git/ +packages/web/.next + diff --git a/SETUP.md b/SETUP.md index 641b7414c5b..3221d3db1a2 100644 --- a/SETUP.md +++ b/SETUP.md @@ -38,6 +38,30 @@ nvm alias default 8 brew install yarn ``` +### Optional: Install Rust + +We use Rust to build the [bls-zexe](https://github.com/celo-org/bls-zexe) repo, which Geth depends on. If you only use the monorepo, you probably don't need this. + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +Re-login to the system and run: + +```bash +rustup install 1.36.0 +rustup default 1.36.0 +``` + +If you're building Geth for Android, you require an NDK that has a cross-compilation toolchain. You can get it by appropriately defining the relevant environment variables, e.g.: + +```bash +export NDK_VERSION=android-ndk-r19c +export ANDROID_NDK=ndk_bundle/android-ndk-r19c +``` + +and running `make ndk_bundle`. This will download the NDK for your platform. + ### Java We need Java to be able to build and run Android to deploy the mobile app to @@ -142,6 +166,10 @@ yarn > github.com host key. Clone a repo or add the github host key to > `~/.ssh/known_hosts` and then try again. +> When removing a dependency via `yarn remove some-package`, be sure to also run `yarn postinstall` so +> you aren't left with freshly unpackaged modules. This is because we use `patch-package` +> and the `postinstall` step which uses it is not automatically run after using `yarn remove`. + ## Using an Android test device locally First, follow [these instructions to enable Developer Options][android dev options] diff --git a/cloudbuild.yaml b/cloudbuild.yaml index b5563bdd4f1..fdd720ccc54 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -3,33 +3,33 @@ steps: -- name: gcr.io/kaniko-project/executor:latest +- id: "docker:celotool" + name: gcr.io/kaniko-project/executor:latest args: [ - "--dockerfile=dockerfiles/monorepo/Dockerfile.celotool", + "--dockerfile=dockerfiles/celotool/Dockerfile", "--cache=true", "--destination=gcr.io/$PROJECT_ID/celo-monorepo:celotool-$COMMIT_SHA" ] - id: Build celotool docker image waitFor: ['-'] -- name: gcr.io/kaniko-project/executor:latest +- id: "docker:transaction-metrics-exporter" + name: gcr.io/kaniko-project/executor:latest args: [ - "--dockerfile=dockerfiles/monorepo/Dockerfile.transaction-metrics-exporter", + "--dockerfile=dockerfiles/transaction-metrics-exporter/Dockerfile", "--cache=true", "--destination=gcr.io/$PROJECT_ID/celo-monorepo:transaction-metrics-exporter-$COMMIT_SHA" ] - id: Build transaction metrics exporter docker image waitFor: ['-'] -- name: gcr.io/kaniko-project/executor:latest +- id: "docker:cli" + name: gcr.io/kaniko-project/executor:latest args: [ - "--dockerfile=dockerfiles/cli/Dockerfile.cli", + "--dockerfile=dockerfiles/cli/Dockerfile", "--cache=true", "--destination=gcr.io/$PROJECT_ID/celocli:$COMMIT_SHA", "--build-arg", "celo_env=alfajores" ] - id: Build CLI docker image waitFor: ['-'] timeout: 3000s diff --git a/dockerfiles/celotool/Dockerfile b/dockerfiles/celotool/Dockerfile new file mode 100644 index 00000000000..18bc09ad5d6 --- /dev/null +++ b/dockerfiles/celotool/Dockerfile @@ -0,0 +1,36 @@ +FROM node:8 +WORKDIR /celo-monorepo + +# Needed for gsutil +RUN apt-get update && \ + apt-get install -y lsb-release && \ + export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)" && \ + echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && \ + curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - && \ + apt-get update -y && \ + apt-get install -y google-cloud-sdk && \ + rm -rf /var/lib/apt/lists/* + + +# ensure yarn.lock is evaluated by kaniko cache diff +COPY lerna.json package.json yarn.lock ./ +COPY scripts/ scripts/ +COPY packages/utils/package.json packages/utils/ +COPY packages/typescript/package.json packages/typescript/ +COPY packages/walletkit/package.json packages/walletkit/ +COPY packages/verification-pool-api/package.json packages/verification-pool-api/ +COPY packages/celotool/package.json packages/celotool/ + +RUN yarn install --frozen-lockfile && yarn cache clean + +COPY packages/utils packages/utils/ +COPY packages/typescript packages/typescript/ +COPY packages/walletkit packages/walletkit/ +COPY packages/verification-pool-api packages/verification-pool-api/ +COPY packages/celotool packages/celotool/ + +# RUN yarn build + +ENV PATH="/celo-monorepo/packages/celotool/bin:${PATH}" + +CMD ["celotooljs.sh"] diff --git a/dockerfiles/cli/.dockerignore b/dockerfiles/cli/.dockerignore deleted file mode 100644 index bac7aa57360..00000000000 --- a/dockerfiles/cli/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -packages/mobile -node_modules/ -*/*/node_modules/ \ No newline at end of file diff --git a/dockerfiles/cli/Dockerfile.cli b/dockerfiles/cli/Dockerfile similarity index 96% rename from dockerfiles/cli/Dockerfile.cli rename to dockerfiles/cli/Dockerfile index 674ea0064ea..fedd1baca67 100644 --- a/dockerfiles/cli/Dockerfile.cli +++ b/dockerfiles/cli/Dockerfile @@ -49,7 +49,7 @@ RUN npm install @celo/celocli FROM node:8-alpine as final_image ARG network_name="alfajores" -ARG network_id="44781" +ARG network_id="44782" # Without musl-dev, geth will fail with a confusing "No such file or directory" error. # bash is required for start_geth.sh @@ -66,4 +66,4 @@ 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", "44781", "/root/.celo", "/celo/genesis.json", "/celo/static-nodes.json"] +ENTRYPOINT ["/celo/start_geth.sh", "/usr/local/bin/geth", "alfajores", "full", "44782", "/root/.celo", "/celo/genesis.json", "/celo/static-nodes.json"] diff --git a/dockerfiles/monorepo/.dockerignore b/dockerfiles/monorepo/.dockerignore deleted file mode 100644 index 6fa84791dec..00000000000 --- a/dockerfiles/monorepo/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -packages/mobile -node_modules -**/node_modules -*node_modules \ No newline at end of file diff --git a/dockerfiles/monorepo/Dockerfile b/dockerfiles/monorepo/Dockerfile deleted file mode 100644 index 827a17e7e08..00000000000 --- a/dockerfiles/monorepo/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM node:8 - -RUN apt-get update -RUN apt-get install lsb-release -y - -# Needed for gsutil -RUN export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)" && \ - echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && \ - curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - && \ - apt-get update -y && apt-get install google-cloud-sdk -y - -# ensure yarn.lock is evaluated by kaniko cache diff -COPY . /celo-monorepo -WORKDIR /celo-monorepo -RUN yarn install - -CMD ["/bin/sh"] diff --git a/dockerfiles/monorepo/Dockerfile.celotool b/dockerfiles/monorepo/Dockerfile.celotool deleted file mode 100644 index 0f1ed668996..00000000000 --- a/dockerfiles/monorepo/Dockerfile.celotool +++ /dev/null @@ -1,29 +0,0 @@ -FROM node:8 - -RUN apt-get update -RUN apt-get install lsb-release -y - -# Needed for gsutil -RUN export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)" && \ - echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && \ - curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - && \ - apt-get update -y && apt-get install google-cloud-sdk -y - -# ensure yarn.lock is evaluated by kaniko cache diff - -COPY lerna.json /celo-monorepo/lerna.json -COPY scripts /celo-monorepo/scripts -COPY package.json /celo-monorepo/package.json -COPY yarn.lock /celo-monorepo/yarn.lock -COPY .prettierrc.js /celo-monorepo/.prettierrc.js -COPY packages/utils /celo-monorepo/packages/utils -COPY packages/typescript /celo-monorepo/packages/typescript -COPY packages/contractkit /celo-monorepo/packages/contractkit -COPY packages/celotool /celo-monorepo/packages/celotool - -WORKDIR /celo-monorepo -RUN yarn install - -ENV PATH="/celo-monorepo/packages/celotool/bin:${PATH}" - -CMD ["celotooljs.sh"] diff --git a/dockerfiles/monorepo/Dockerfile.transaction-metrics-exporter b/dockerfiles/transaction-metrics-exporter/Dockerfile similarity index 94% rename from dockerfiles/monorepo/Dockerfile.transaction-metrics-exporter rename to dockerfiles/transaction-metrics-exporter/Dockerfile index 1ae4ba8ec77..c17ead39d0c 100644 --- a/dockerfiles/monorepo/Dockerfile.transaction-metrics-exporter +++ b/dockerfiles/transaction-metrics-exporter/Dockerfile @@ -18,7 +18,7 @@ COPY yarn.lock /celo-monorepo/yarn.lock COPY .prettierrc.js /celo-monorepo/.prettierrc.js COPY packages/utils /celo-monorepo/packages/utils COPY packages/typescript /celo-monorepo/packages/typescript -COPY packages/contractkit /celo-monorepo/packages/contractkit +COPY packages/walletkit /celo-monorepo/packages/walletkit COPY packages/celotool /celo-monorepo/packages/celotool COPY packages/transaction-metrics-exporter /celo-monorepo/packages/transaction-metrics-exporter diff --git a/package.json b/package.json index d25e5e2223c..e8413e51915 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,7 @@ "private": true, "scripts": { "install-pkg": "yarn install --link-duplicates", - "precommit": "pretty-quick --staged", - "ci-lint": "yarn run prettify:diff && yarn run lint-checks", - "lint-checks": "yarn run lerna run lint-checks", + "lint": "yarn lerna run lint", "prettify": "yarn run prettier --config .prettierrc.js --write '**/*.+(ts|tsx|js|jsx)'", "prettify:diff": "yarn run prettier --config .prettierrc.js --list-different '**/*.+(ts|tsx|js|jsx)'", "reset": "yarn reset-modules && yarn reset-cache", @@ -17,12 +15,13 @@ "reset-rn": "watchman watch-del-all; rm -rf $TMPDIR/metro-cache-*; rm -rf $TMPDIR/haste-map-*; rm -rf $TMPDIR/metro-symbolicate*", "reset-yarn": "yarn cache clean", "test": "yarn run lerna run test", + "build": "yarn run lerna run build", "report-coverage": "yarn run lerna run test-coverage", "test:watch": "node node_modules/jest/bin/jest.js --watch", - "postinstall": "yarn run lerna --concurrency 1 run postinstall && patch-package && sh scripts/custom_patch_packages.sh && yarn keys:decrypt", + "postinstall": "yarn run lerna run postinstall && patch-package && sh scripts/custom_patch_packages.sh && 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" }, "husky": { "hooks": { @@ -36,11 +35,8 @@ ], "nohoist": [ "@celo/verifier/react-native", - "@celo/verifier/react", "@celo/mobile/react-native", - "@celo/mobile/react", "@celo/react-components/react-native", - "@celo/react-components/react", "@celo/web/@timkendrick/monaco-editor", "@celo/web/@types/react-i18next", "@celo/web/next-i18next", @@ -51,7 +47,6 @@ "husky": "^3.0.0", "lerna": "^3.16.0", "patch-package": "^5.1.1", - "postinstall-postinstall": "^1.0.0", "prettier": "1.13.5", "pretty-quick": "^1.11.1", "solc": "0.5.8", diff --git a/packages/blockchain-api/app.alfajores.yaml b/packages/blockchain-api/app.alfajores.yaml index 393355fd56f..a5be2269de1 100644 --- a/packages/blockchain-api/app.alfajores.yaml +++ b/packages/blockchain-api/app.alfajores.yaml @@ -5,8 +5,8 @@ env_variables: DEPLOY_ENV: 'alfajores' BLOCKSCOUT_API: 'https://alfajores-blockscout.celo-testnet.org/api' # Pull addresses from the build artifacts of the network in protocol/build - CELO_GOLD_ADDRESS: '0x4813BFD311E132ade22c70dFf7e5DB045d26D070' - CELO_DOLLAR_ADDRESS: '0x299E74bdCD90d4E10f7957EF074ceE32d7e9089a' - FAUCET_ADDRESS: '0x456f41406B32c45D59E539e4BBA3D7898c3584dA ' + CELO_GOLD_ADDRESS: '0x11CD75C45638Ec9f41C0e8Df78fc756201E48ff2' + CELO_DOLLAR_ADDRESS: '0xd4b4fcaCAc9e23225680e89308E0a4C41Dd9C6B4' + FAUCET_ADDRESS: '0x456f41406B32c45D59E539e4BBA3D7898c3584dA' VERIFICATION_REWARDS_ADDRESS: '0xb4fdaf5f3cd313654aa357299ada901b1d2dd3b5' - ABE_ADDRESS: '0xbaf22812681355442465B59c476cE20a60823105' \ No newline at end of file + ABE_ADDRESS: '0x714f2879A4aa985508537f851FeBCfB26D7aF40D' \ No newline at end of file diff --git a/packages/blockchain-api/app.alfajoresstaging.yaml b/packages/blockchain-api/app.alfajoresstaging.yaml index 062a453f6e9..54848672853 100644 --- a/packages/blockchain-api/app.alfajoresstaging.yaml +++ b/packages/blockchain-api/app.alfajoresstaging.yaml @@ -5,8 +5,8 @@ env_variables: DEPLOY_ENV: 'alfajoresstaging' BLOCKSCOUT_API: 'https://alfajoresstaging-blockscout.celo-testnet.org/api' # Pull addresses from the build artifacts of the network in protocol/build - CELO_GOLD_ADDRESS: '0xD016D1654bb83d99b70c3Be3771C2719b2497bc1' - CELO_DOLLAR_ADDRESS: '0xaEeF69188629296468940f6a4479b37926EBCaDB' - FAUCET_ADDRESS: '0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95' + CELO_GOLD_ADDRESS: '0xbe7E2bf1d46f7F351a1785e707Eae8f252Af94Dd' + CELO_DOLLAR_ADDRESS: '0x4917775b54738BFf899C91f320d260Be413f1d16' + FAUCET_ADDRESS: '0xF4314cb9046bECe6AA54bb9533155434d0c76909' VERIFICATION_REWARDS_ADDRESS: '0xb4fdaf5f3cd313654aa357299ada901b1d2dd3b5' - ABE_ADDRESS: '0x13da91B4b00703915B74C9279B5F67a830dD8ec1' \ No newline at end of file + ABE_ADDRESS: '0xAfDF1963Dac816dd84B49B8d2179296Ab32D6519' \ No newline at end of file diff --git a/packages/blockchain-api/app.int.yaml b/packages/blockchain-api/app.int.yaml index 5dd469948ea..8df316a439b 100644 --- a/packages/blockchain-api/app.int.yaml +++ b/packages/blockchain-api/app.int.yaml @@ -5,8 +5,8 @@ env_variables: DEPLOY_ENV: 'integration' BLOCKSCOUT_API: 'https://integration-blockscout.celo-testnet.org/api' # Pull addresses from the build artifacts of the network in protocol/build - CELO_GOLD_ADDRESS: '0x9cb31fCD259E6Dd87566945c94E2BFaDcE179feA' - CELO_DOLLAR_ADDRESS: '0x7DFAA4B53E7d06E9e30C4426d9692453d94A8437' + CELO_GOLD_ADDRESS: '0x7255b11f3DEEF596F8AF021b30c92BDD46D8cEa6' + CELO_DOLLAR_ADDRESS: '0x0a4c0Dc85b16Ec6dFb2dbEDF1f9090AE34BaaF4f' FAUCET_ADDRESS: '0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95' VERIFICATION_REWARDS_ADDRESS: '0xb4fdaf5f3cd313654aa357299ada901b1d2dd3b5' - ABE_ADDRESS: '0x4Aa7660173C3C1D4d5F4cb4090b92389Ea1F0771' + ABE_ADDRESS: '0xD244bD4D25180e5966807B4c8b1aA551985E1A2D' diff --git a/packages/blockchain-api/app.pilot.yaml b/packages/blockchain-api/app.pilot.yaml new file mode 100644 index 00000000000..616f142ece9 --- /dev/null +++ b/packages/blockchain-api/app.pilot.yaml @@ -0,0 +1,12 @@ +runtime: nodejs8 +service: pilot +env_variables: + NODE_ENV: 'production' + DEPLOY_ENV: 'pilot' + BLOCKSCOUT_API: 'https://pilot-blockscout.celo-testnet.org/api' + # Pull addresses from the build artifacts of the network in protocol/build + CELO_GOLD_ADDRESS: '0x0ee512BC806Bd0Ca4C09211D035D2928FC43faA6' + CELO_DOLLAR_ADDRESS: '0xEd9f83788Cad26d5671F9D18520995dB005C03cd' + FAUCET_ADDRESS: '0x387bCb16Bfcd37AccEcF5c9eB2938E30d3aB8BF2' + VERIFICATION_REWARDS_ADDRESS: '0xb4fdaf5f3cd313654aa357299ada901b1d2dd3b5' + ABE_ADDRESS: '0x4C5cEB976901e9794baF239AF942eAD721aB21E0' diff --git a/packages/blockchain-api/app.pilotstaging.yaml b/packages/blockchain-api/app.pilotstaging.yaml new file mode 100644 index 00000000000..6b9f392e504 --- /dev/null +++ b/packages/blockchain-api/app.pilotstaging.yaml @@ -0,0 +1,12 @@ +runtime: nodejs8 +service: pilotstaging +env_variables: + NODE_ENV: 'development' + DEPLOY_ENV: 'pilotstaging' + BLOCKSCOUT_API: 'https://pilotstaging-blockscout.celo-testnet.org/api' + # Pull addresses from the build artifacts of the network in protocol/build + CELO_GOLD_ADDRESS: '0x4Bd6E33F6D8062E3f8F0B8CC43f59832c8f4c875' + CELO_DOLLAR_ADDRESS: '0x70aD391A6EDf249FB09Ac20c6Ceb0443dA0F40dE' + FAUCET_ADDRESS: '0x545DEBe3030B570731EDab192640804AC8Cf65CA' + VERIFICATION_REWARDS_ADDRESS: '0xb4fdaf5f3cd313654aa357299ada901b1d2dd3b5' + ABE_ADDRESS: '0x1b1c81d60d229aB4D9ED2D7ca73c44dFfC89e19A' diff --git a/packages/blockchain-api/appintegration.yaml b/packages/blockchain-api/appintegration.yaml deleted file mode 100644 index 85a6431fb96..00000000000 --- a/packages/blockchain-api/appintegration.yaml +++ /dev/null @@ -1,12 +0,0 @@ -runtime: nodejs8 -service: appintegration -env_variables: - NODE_ENV: 'development' - DEPLOY_ENV: 'appintegration' - BLOCKSCOUT_API: 'https://appintegration-blockscout.celo-testnet.org/api' - # Pull addresses from the build artifacts of the network in protocol/build - CELO_GOLD_ADDRESS: '0x140ad3e4625160f48208daf47ca7fdff40527f5f' - CELO_DOLLAR_ADDRESS: '0x94a076695b5c4050fbe57ab205e31c18bddb7cb5' - FAUCET_ADDRESS: '0xfee1a22f43beecb912b5a4912ba87527682ef0fc' - VERIFICATION_REWARDS_ADDRESS: '0xb4fdaf5f3cd313654aa357299ada901b1d2dd3b5' - ABE_ADDRESS: '0x979577f6f00d56dada982024f2e0a60c0cb7722d' diff --git a/packages/blockchain-api/package.json b/packages/blockchain-api/package.json index 6c25a0b0d52..a954913ca3e 100644 --- a/packages/blockchain-api/package.json +++ b/packages/blockchain-api/package.json @@ -12,16 +12,16 @@ "test": "jest --ci --silent --coverage --runInBand", "test:verbose": "jest --ci --verbose --runInBand", "lint": "tslint -c tslint.json --project tsconfig.json", - "lint-checks": "yarn run lint && tsc --project tsconfig.json --noEmit", "start": "node ./dist/index.js", "start:dev": "tsc-watch --onSuccess \"node ./dist/index.js\" ", - "build": "tsc", + "build": "tsc -p .", "gcp-build": "npm run build", "deploy": "gcloud app deploy" }, "dependencies": { "apollo-datasource-rest": "^0.3.1", "apollo-server-express": "^2.4.2", + "bignumber.js": "^7.2.0", "dotenv": "^6.1.0", "express": "^4.16.4", "graphql": "^14.1.1", diff --git a/packages/blockchain-api/src/blockscout.ts b/packages/blockchain-api/src/blockscout.ts index d772e3e0b9d..dd1205d3c95 100644 --- a/packages/blockchain-api/src/blockscout.ts +++ b/packages/blockchain-api/src/blockscout.ts @@ -1,4 +1,5 @@ import { RESTDataSource } from 'apollo-datasource-rest' +import BigNumber from 'bignumber.js' import { ABE_ADDRESS, BLOCKSCOUT_API, @@ -29,23 +30,23 @@ const MODULE_ACTIONS = { } interface BlockscoutTransaction { - value: number - txreceipt_status: number - transactionIndex: number + value: string + txreceipt_status: string + transactionIndex: string to: string - timeStamp: number - nonce: number - isError: number + timeStamp: string + nonce: string + isError: string input: string hash: string - gasUsed: number - gasPrice: number - gas: number + gasUsed: string + gasPrice: string + gas: string from: string - cumulativeGasUsed: number + cumulativeGasUsed: string contractAddress: string - confirmations: number - blockNumber: number + confirmations: string + blockNumber: string blockHash: string } @@ -66,6 +67,15 @@ export class BlockscoutAPI extends RESTDataSource { return result } + // LIMITATION: + // This function will only return Gold transfers that happened via the GoldToken + // contract. Any native transfers of Gold will be omitted because of how blockscout + // works. To get native transactions from blockscout, we'd need to use the param: + // "action: MODULE_ACTIONS.ACCOUNT.TX_LIST" + // However, the results returned from that API call do not have an easily-parseable + // representation of Token transfers, if they are included at all. Given that we + // expect native transfers to be exceedingly rare, the work to handle this is being + // skipped for now. TODO: (yerdua) [226] async getFeedEvents(args: EventArgs) { const rawTransactions = await this.getTokenTransactions(args) const events: EventInterface[] = [] @@ -94,12 +104,12 @@ export class BlockscoutAPI extends RESTDataSource { events.push({ type: EventTypes.EXCHANGE, - timestamp: inEvent.timeStamp, - block: inEvent.blockNumber, + timestamp: new BigNumber(inEvent.timeStamp).toNumber(), + block: new BigNumber(inEvent.blockNumber).toNumber(), inSymbol: CONTRACT_SYMBOL_MAPPING[inEvent.contractAddress.toLowerCase()], - inValue: inEvent.value / WEI_PER_GOLD, + inValue: new BigNumber(inEvent.value).dividedBy(WEI_PER_GOLD).toNumber(), outSymbol: CONTRACT_SYMBOL_MAPPING[outEvent.contractAddress.toLowerCase()], - outValue: outEvent.value / WEI_PER_GOLD, + outValue: new BigNumber(outEvent.value).dividedBy(WEI_PER_GOLD).toNumber(), hash: txhash, }) @@ -116,9 +126,9 @@ export class BlockscoutAPI extends RESTDataSource { ) events.push({ type, - timestamp: event.timeStamp, - block: event.blockNumber, - value: event.value / WEI_PER_GOLD, + timestamp: new BigNumber(event.timeStamp).toNumber(), + block: new BigNumber(event.blockNumber).toNumber(), + value: new BigNumber(event.value).dividedBy(WEI_PER_GOLD).toNumber(), address, comment, symbol: CONTRACT_SYMBOL_MAPPING[event.contractAddress.toLowerCase()] || 'unknown', @@ -145,9 +155,9 @@ export class BlockscoutAPI extends RESTDataSource { } rewards.push({ type: EventTypes.VERIFICATION_REWARD, - timestamp: t.timeStamp, - block: t.blockNumber, - value: t.value / WEI_PER_GOLD, + timestamp: new BigNumber(t.timeStamp).toNumber(), + block: new BigNumber(t.blockNumber).toNumber(), + value: new BigNumber(t.value).dividedBy(WEI_PER_GOLD).toNumber(), address: VERIFICATION_REWARDS_ADDRESS, comment: t.input ? formatCommentString(t.input) : '', symbol: CONTRACT_SYMBOL_MAPPING[t.contractAddress], diff --git a/packages/blockchain-api/tsconfig.json b/packages/blockchain-api/tsconfig.json index 97e1ea511fa..530633298ab 100644 --- a/packages/blockchain-api/tsconfig.json +++ b/packages/blockchain-api/tsconfig.json @@ -14,7 +14,8 @@ "skipLibCheck": true, "sourceMap": true, "strict": true, - "target": "es6" + "target": "es2017", + "lib": ["es7", "es2017"] }, "include": ["src/**/*", "index.d.ts"] } diff --git a/packages/celotool/ci_test_integration_sync.sh b/packages/celotool/ci_test_sync_with_network.sh similarity index 63% rename from packages/celotool/ci_test_integration_sync.sh rename to packages/celotool/ci_test_sync_with_network.sh index eb08571f69d..6a8d342380e 100755 --- a/packages/celotool/ci_test_integration_sync.sh +++ b/packages/celotool/ci_test_sync_with_network.sh @@ -5,9 +5,9 @@ set -euo pipefail # verifies that the sync works. # For testing a particular commit hash of Geth repo (usually, on Circle CI) -# Usage: ci_test.sh checkout +# Usage: ci_test_sync_with_network.sh checkout # For testing the local Geth dir (usually, for manual testing) -# Usage: ci_test.sh local +# Usage: ci_test_sync_with_network.sh local if [ "${1}" == "checkout" ]; then export GETH_DIR="/tmp/geth" @@ -34,27 +34,32 @@ test_ultralight_sync () { NETWORK_NAME=$1 echo "Testing ultralight sync with '${NETWORK_NAME}' network" # Run the sync in ultralight mode - geth_tests/integration_network_sync_test.sh ${NETWORK_NAME} ultralight + geth_tests/network_sync_test.sh ${NETWORK_NAME} ultralight # Get the epoch size by sourcing this file source ${CELO_MONOREPO_DIR}/.env.${NETWORK_NAME} # Verify what happened by reading the logs. ${CELO_MONOREPO_DIR}/node_modules/.bin/mocha -r ts-node/register ${CELO_MONOREPO_DIR}/packages/celotool/geth_tests/verify_ultralight_geth_logs.ts --gethlogfile ${GETH_LOG_FILE} --epoch ${EPOCH} } +# Some code in celotool requires this file to contain the MNEMONOIC. +# The value of MNEMONOIC does not matter. +if [[ ! -e ${CELO_MONOREPO_DIR}/.env.mnemonic ]]; then + echo "MNEMONOIC=anything random" > ${CELO_MONOREPO_DIR}/.env.mnemonic +fi + # Test syncing -geth_tests/integration_network_sync_test.sh integration full +export NETWORK_NAME="integration" +# Add an extra echo at the end to dump a new line, this makes the results a bit more readable. +geth_tests/network_sync_test.sh ${NETWORK_NAME} full && echo # This is broken, I am not sure why, therefore, commented for now. -# geth_tests/integration_network_sync_test.sh integration fast -geth_tests/integration_network_sync_test.sh integration light -# celolatest sync mode won't work once a network has crossed its first epoch. -# Therefore, disable this. -# geth_tests/integration_network_sync_test.sh integration celolatest -test_ultralight_sync integration - -geth_tests/integration_network_sync_test.sh appintegration full +# geth_tests/network_sync_test.sh ${NETWORK_NAME} fast && echo +geth_tests/network_sync_test.sh ${NETWORK_NAME} light && echo +test_ultralight_sync ${NETWORK_NAME} && echo + +#TODO(Kobi): disabled until alfajoresstaging is upgraded with BLS +#export NETWORK_NAME="alfajoresstaging" +#geth_tests/network_sync_test.sh ${NETWORK_NAME} full && echo # This is broken, I am not sure why, therefore, commented for now. -# geth_tests/integration_network_sync_test.sh integration fast -geth_tests/integration_network_sync_test.sh appintegration light -# This works since appintegration, as of now, has an unusually large epoch (30M * 5 seconds ~ 5 years) -geth_tests/integration_network_sync_test.sh appintegration celolatest -test_ultralight_sync appintegration +# geth_tests/network_sync_test.sh ${NETWORK_NAME} fast && echo +#geth_tests/network_sync_test.sh ${NETWORK_NAME} light && echo +#test_ultralight_sync ${NETWORK_NAME} && echo diff --git a/packages/celotool/geth_tests/governance_tests.ts b/packages/celotool/geth_tests/governance_tests.ts index b38eccc3a52..001fb6acdb1 100644 --- a/packages/celotool/geth_tests/governance_tests.ts +++ b/packages/celotool/geth_tests/governance_tests.ts @@ -1,9 +1,11 @@ import { erc20Abi, getContractAddress, + getEnode, getHooks, importGenesis, initAndStartGeth, + sleep, } from '@celo/celotool/geth_tests/src/lib/utils' import BigNumber from 'bignumber.js' import { strip0x } from '../src/lib/utils' @@ -52,6 +54,10 @@ const bondedDepositsAbi = [ { constant: false, inputs: [ + { + name: 'role', + type: 'uint8', + }, { name: 'delegate', type: 'address', @@ -69,7 +75,7 @@ const bondedDepositsAbi = [ type: 'bytes32', }, ], - name: 'delegateRewards', + name: 'delegateRole', outputs: [], payable: false, stateMutability: 'nonpayable', @@ -77,19 +83,108 @@ const bondedDepositsAbi = [ }, ] +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', + }, +] + describe('governance tests', () => { const gethConfig = { migrate: true, instances: [ - { name: 'validator', validating: true, syncmode: 'full', port: 30303, rpcport: 8545 }, + { 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 hooks: any = getHooks(gethConfig) let web3: any let bondedDeposits: any - let gasPrice: number - let goldTokenAddress: string + let validators: any let goldToken: any before(async function(this: any) { @@ -102,13 +197,12 @@ describe('governance tests', () => { const restart = async () => { await hooks.restart() web3 = new Web3('http://localhost:8545') - const bondedDepositsAddress = await getContractAddress('BondedDepositsProxy') - const checksumAddress = web3.utils.toChecksumAddress(bondedDepositsAddress) - bondedDeposits = new web3.eth.Contract(bondedDepositsAbi, checksumAddress) - goldTokenAddress = await getContractAddress('GoldTokenProxy') - const goldChecksumAddress = web3.utils.toChecksumAddress(goldTokenAddress) - goldToken = new web3.eth.Contract(erc20Abi, goldChecksumAddress) - gasPrice = parseInt(await web3.eth.getGasPrice(), 10) + bondedDeposits = new web3.eth.Contract( + bondedDepositsAbi, + await getContractAddress('BondedDepositsProxy') + ) + goldToken = new web3.eth.Contract(erc20Abi, await getContractAddress('GoldTokenProxy')) + validators = new web3.eth.Contract(validatorsAbi, await getContractAddress('ValidatorsProxy')) } const unlockAccount = async (address: string, web3: any) => { @@ -126,32 +220,148 @@ describe('governance tests', () => { v: signerWeb3.utils.hexToNumber(signature.slice(128, 130)), } } + + 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()) + // The validator group ID is the validator group keystore encrypted with validator 0's + // private key. + // @ts-ignore + const encryptionKey = `0x${gethConfig.instances[0].privateKey}` + const decryptedKeystore = web3.eth.accounts.decrypt(encryptedKeystore, encryptionKey) + return [groupAddress, decryptedKeystore.privateKey] + } + + const removeMember = async ( + groupWeb3: any, + group: string, + member: string, + txOptions: any = {} + ) => { + await unlockAccount(group, groupWeb3) + const tx = validators.methods.removeMember(member) + let gas = txOptions.gas + if (!gas) { + gas = await tx.estimateGas({ ...txOptions }) + } + return await tx.send({ from: group, ...txOptions, gas }) + } + + const addMember = async (groupWeb3: any, group: string, member: string, txOptions: any = {}) => { + await unlockAccount(group, groupWeb3) + const tx = validators.methods.addMember(member) + let gas = txOptions.gas + if (!gas) { + gas = await tx.estimateGas({ ...txOptions }) + } + return await tx.send({ from: group, ...txOptions, gas }) + } + const delegateRewards = async (account: string, delegate: string, txOptions: any = {}) => { - const delegateWeb3 = new Web3('http://localhost:8547') + 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 tx = bondedDeposits.methods.delegateRewards(delegate, v, r, s) + const rewardRole = 2 + const tx = bondedDeposits.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 await tx.send({ from: account, ...txOptions, gasPrice, gas }) + return await tx.send({ from: account, ...txOptions, gas }) } - // const redeemRewards = async (account: string, txOptions: any = {}) => { - // await unlockAccount(account, web3) - // const tx = bondedDeposits.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 await tx.send({ from: account, ...txOptions, gasPrice, gas }) - // } + const redeemRewards = async (account: string, txOptions: any = {}) => { + await unlockAccount(account, web3) + const tx = bondedDeposits.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 await tx.send({ from: account, ...txOptions, gas }) + } + + 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() + + const groupInstance = { + name: 'validatorGroup', + validating: false, + syncmode: 'full', + port: 30325, + wsport: 8567, + privateKey: groupPrivateKey.slice(2), + peers: [await getEnode(8545)], + } + await initAndStartGeth(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 members = await getValidatorGroupMembers() + const direction = members.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 subscription = groupWeb3.eth.subscribe('newBlockHeaders').on('data', changeValidatorSet) + // Wait for a few epochs while changing the validator set. + await sleep(epoch * 3) + subscription.unsubscribe() + // Wait for the current epoch to complete. + await sleep(epoch) + }) + + 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) + console.log(expectedEpochMembership) + for (let [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) + } + }) + }) describe('when a bonded deposit account with weight exists', () => { const account = '0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95' @@ -164,8 +374,8 @@ describe('governance tests', () => { name: 'delegate', validating: false, syncmode: 'full', - port: 30305, - rpcport: 8547, + port: 30325, + rpcport: 8567, privateKey: 'f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d', } await initAndStartGeth(hooks.gethBinaryPath, delegateInstance) @@ -174,12 +384,12 @@ describe('governance tests', () => { await delegateRewards(account, delegate) }) - // it('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) - // }) + 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) + }) }) describe('when adding any block', () => { @@ -207,7 +417,9 @@ describe('governance tests', () => { // 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. - const blockNumber = 255 + // We don't set the total supply until block rewards are paid out, which can happen once + // either BondedDeposits or Governance are registered. + const blockNumber = 275 const goldTotalSupply = await goldToken.methods.totalSupply().call({}, blockNumber) const balances = await Promise.all( addressesWithBalance.map( diff --git a/packages/celotool/geth_tests/integration_network_sync_test.sh b/packages/celotool/geth_tests/network_sync_test.sh similarity index 79% rename from packages/celotool/geth_tests/integration_network_sync_test.sh rename to packages/celotool/geth_tests/network_sync_test.sh index 4f25405187a..2997b623f55 100755 --- a/packages/celotool/geth_tests/integration_network_sync_test.sh +++ b/packages/celotool/geth_tests/network_sync_test.sh @@ -19,7 +19,8 @@ GENESIS_FILE_PATH="/tmp/genesis_ibft.json" GETH_BINARY="${GETH_DIR}/build/bin/geth --datadir ${DATA_DIR}" CELOTOOLJS="${CELO_MONOREPO_DIR}/packages/celotool/bin/celotooljs.sh" -${CELOTOOLJS} generate genesis-file --celo-env ${NETWORK_NAME} > ${GENESIS_FILE_PATH} +curl "https://www.googleapis.com/storage/v1/b/genesis_blocks/o/${NETWORK_NAME}?alt=media" --output ${GENESIS_FILE_PATH} + ${CELOTOOLJS} geth build --geth-dir ${GETH_DIR} rm -rf ${DATA_DIR} @@ -27,11 +28,12 @@ ${GETH_BINARY} init ${GENESIS_FILE_PATH} 1>/dev/null 2>/dev/null curl "https://www.googleapis.com/storage/v1/b/static_nodes/o/${NETWORK_NAME}?alt=media" --output ${DATA_DIR}/static-nodes.json echo "Running geth in the background..." +LOG_FILE="/tmp/geth_stdout" # Run geth in the background ${CELOTOOLJS} geth run \ --geth-dir ${GETH_DIR} \ --data-dir ${DATA_DIR} \ - --sync-mode ${SYNCMODE} 1>/tmp/geth_stdout 2>/tmp/geth_stderr & + --sync-mode ${SYNCMODE} 1>${LOG_FILE} 2>/tmp/geth_stderr & # let it sync sleep 20 latestBlock=$(${GETH_BINARY} attach -exec eth.blockNumber) @@ -40,6 +42,10 @@ echo "Latest block number is ${latestBlock}" pkill -9 geth if [ "$latestBlock" -eq "0" ]; then - echo "Sync is not working with network '${NETWORK_NAME}' in mode '${SYNCMODE}', see logs in /tmp/geth_stdout" + echo "Sync is not working with network '${NETWORK_NAME}' in mode '${SYNCMODE}', see logs in ${LOG_FILE}" + if test ${CI}; then + echo "Running on CI, dumping logs from ${LOG_FILE}..." + cat ${LOG_FILE} + fi exit 1 fi diff --git a/packages/celotool/geth_tests/src/lib/utils.ts b/packages/celotool/geth_tests/src/lib/utils.ts index fd0717393f8..46b662b6414 100644 --- a/packages/celotool/geth_tests/src/lib/utils.ts +++ b/packages/celotool/geth_tests/src/lib/utils.ts @@ -3,8 +3,9 @@ import { ConsensusType, generateGenesis, getPrivateKeysFor, + getValidators, privateKeyToPublicKey, - privateKeyToStrippedAddress, + Validator, } from '@celo/celotool/src/lib/generate_utils' import { getEnodeAddress } from '@celo/celotool/src/lib/geth' import { ensure0x } from '@celo/celotool/src/lib/utils' @@ -14,19 +15,21 @@ import fs from 'fs' import { join as joinPath, resolve as resolvePath } from 'path' import { Admin } from 'web3-eth-admin' -interface GethInstanceConfig { +export interface GethInstanceConfig { name: string validating: boolean syncmode: string port: number - rpcport: number + rpcport?: number + wsport?: number lightserv?: boolean privateKey?: string etherbase?: string peers?: string[] + pid?: number } -interface GethTestConfig { +export interface GethTestConfig { migrate?: boolean migrateTo?: number instances: GethInstanceConfig[] @@ -170,7 +173,7 @@ async function setupTestDir(testDir: string) { await execCmd('mkdir', [testDir]) } -function writeGenesis(validators: string[], path: string) { +function writeGenesis(validators: Validator[], path: string) { const blockTime = 0 const epochLength = 10 const genesis = generateGenesis( @@ -206,16 +209,18 @@ export async function importPrivateKey(gethBinaryPath: string, instance: GethIns ) } -export async function killPid(pid: number) { - await execCmd('kill', ['-9', pid.toString()]) -} - export async function killGeth() { console.info(`Killing ALL geth instances`) await execCmd('pkill', ['-9', 'geth'], { silent: true }) } -function addStaticPeers(datadir: string, enodes: string[]) { +export async function killInstance(instance: GethInstanceConfig) { + if (instance.pid) { + await execCmd('kill', ['-9', instance.pid.toString()]) + } +} + +export async function addStaticPeers(datadir: string, enodes: string[]) { fs.writeFileSync(`${datadir}/static-nodes.json`, JSON.stringify(enodes)) } @@ -238,28 +243,23 @@ export function sleep(seconds: number) { return new Promise((resolve) => setTimeout(resolve, seconds * 1000)) } -export async function getEnode(rpcPort: number) { - const admin = new Admin(`http://localhost:${rpcPort}`) +export async function getEnode(port: number, ws: boolean = false) { + let p = ws ? 'ws' : 'http' + const admin = new Admin(`${p}://localhost:${port}`) return (await admin.getNodeInfo()).enode } export async function startGeth(gethBinaryPath: string, instance: GethInstanceConfig) { const datadir = getDatadir(instance) - const { syncmode, port, rpcport, validating: mine } = instance + const { syncmode, port, rpcport, wsport, validating } = instance const privateKey = instance.privateKey || '' const lightserv = instance.lightserv || false - const unlock = instance.validating const etherbase = instance.etherbase || '' const gethArgs = [ '--datadir', datadir, - '--rpc', - '--rpcport', - rpcport.toString(), '--syncmode', syncmode, - '--wsorigins=*', - '--rpcapi=eth,net,web3,debug,admin,personal', '--debug', '--port', port.toString(), @@ -275,8 +275,23 @@ export async function startGeth(gethBinaryPath: string, instance: GethInstanceCo 'extip:127.0.0.1', ] - if (unlock) { - gethArgs.push('--password=/dev/null', `--unlock=0`) + if (rpcport) { + gethArgs.push( + '--rpc', + '--rpcport', + rpcport.toString(), + '--rpcapi=eth,net,web3,debug,admin,personal' + ) + } + + if (wsport) { + gethArgs.push( + '--wsorigins=*', + '--ws', + '--wsport', + wsport.toString(), + '--wsapi=eth,net,web3,debug,admin,personal' + ) } if (etherbase) { @@ -287,21 +302,24 @@ export async function startGeth(gethBinaryPath: string, instance: GethInstanceCo gethArgs.push('--lightserv=90') } - if (mine) { + if (validating) { + gethArgs.push('--password=/dev/null', `--unlock=0`) gethArgs.push('--mine', '--minerthreads=10', `--nodekeyhex=${privateKey}`) } const gethProcess = spawnWithLog(gethBinaryPath, gethArgs, `${datadir}/logs.txt`) + instance.pid = gethProcess.pid // Give some time for geth to come up - const isOpen = await waitForPortOpen('localhost', rpcport, 5) - if (!isOpen) { - console.error(`geth:${instance.name}: jsonRPC didn't open after 5 seconds`) - process.exit(1) - } else { - console.info(`geth:${instance.name}: jsonRPC port open ${rpcport}`) + const waitForPort = wsport ? wsport : rpcport + if (waitForPort) { + const isOpen = await waitForPortOpen('localhost', waitForPort, 5) + if (!isOpen) { + console.error(`geth:${instance.name}: jsonRPC didn't open after 5 seconds`) + process.exit(1) + } else { + console.info(`geth:${instance.name}: jsonRPC port open ${waitForPort}`) + } } - - return gethProcess.pid } export async function migrateContracts(validatorPrivateKeys: string[], to: number = 1000) { @@ -313,6 +331,8 @@ export async function migrateContracts(validatorPrivateKeys: string[], to: numbe 'testing', '-k', validatorPrivateKeys.map(ensure0x).join(','), + '-m', + '{ "validators": { "minElectableValidators": "1" } }', '-t', to.toString(), ] @@ -373,7 +393,7 @@ export function getHooks(gethConfig: GethTestConfig) { const validatorInstances = gethConfig.instances.filter((x: any) => x.validating) const numValidators = validatorInstances.length const validatorPrivateKeys = getPrivateKeysFor(AccountType.VALIDATOR, mnemonic, numValidators) - const validators = validatorPrivateKeys.map(privateKeyToStrippedAddress) + const validators = getValidators(mnemonic, numValidators) const validatorEnodes = validatorPrivateKeys.map((x: any, i: number) => getEnodeAddress(privateKeyToPublicKey(x), '127.0.0.1', validatorInstances[i].port) ) diff --git a/packages/celotool/geth_tests/sync_tests.ts b/packages/celotool/geth_tests/sync_tests.ts index 48e96444b60..08bb50213f2 100644 --- a/packages/celotool/geth_tests/sync_tests.ts +++ b/packages/celotool/geth_tests/sync_tests.ts @@ -1,8 +1,9 @@ import { getEnode, getHooks, + GethInstanceConfig, initAndStartGeth, - killPid, + killInstance, sleep, } from '@celo/celotool/geth_tests/src/lib/utils' import { assert } from 'chai' @@ -47,9 +48,9 @@ describe('sync tests', function(this: any) { const syncModes = ['full', 'fast', 'light', 'ultralight'] for (const syncmode of syncModes) { describe(`when syncing with a ${syncmode} node`, () => { - let gethPid: number | null = null + let syncInstance: GethInstanceConfig beforeEach(async () => { - const syncInstance = { + syncInstance = { name: syncmode, validating: false, syncmode, @@ -58,14 +59,11 @@ describe('sync tests', function(this: any) { lightserv: syncmode !== 'light' && syncmode !== 'ultralight', peers: [await getEnode(8553)], } - gethPid = await initAndStartGeth(hooks.gethBinaryPath, syncInstance) + await initAndStartGeth(hooks.gethBinaryPath, syncInstance) }) afterEach(() => { - if (gethPid) { - killPid(gethPid) - gethPid = null - } + killInstance(syncInstance) }) it('should sync the latest block', async () => { @@ -84,4 +82,28 @@ describe('sync tests', function(this: any) { }) }) } + describe(`when a validator's data directory is deleted`, () => { + let web3: any + beforeEach(async function(this: any) { + this.timeout(0) // Disable test timeout + web3 = new Web3('http://localhost:8545') + await hooks.restart() + }) + + it('should continue to block produce', async function(this: any) { + this.timeout(0) + const instance: GethInstanceConfig = gethConfig.instances[0] + await killInstance(instance) + await initAndStartGeth(hooks.gethBinaryPath, instance) + await sleep(60) // wait for round change / resync + const address = (await web3.eth.getAccounts())[0] + const currentBlock = await web3.eth.getBlock('latest') + for (let i = 0; i < gethConfig.instances.length; i++) { + if ((await web3.eth.getBlock(currentBlock.number - i)).miner == address) { + return // A block proposed by validator who lost randomness was found, hence randomness was recovered + } + } + assert.fail('Reset validator did not propose any new blocks') + }) + }) }) diff --git a/packages/celotool/package.json b/packages/celotool/package.json index f7d06b578fa..116bcc3c552 100644 --- a/packages/celotool/package.json +++ b/packages/celotool/package.json @@ -6,16 +6,20 @@ "author": "Celo", "license": "Apache-2.0", "dependencies": { - "@celo/contractkit": "0.0.1", + "@celo/walletkit": "^0.0.4", "@google-cloud/monitoring": "0.7.1", "@google-cloud/pubsub": "^0.28.1", "@google-cloud/storage": "^2.4.3", "@types/bip32": "^1.0.1", "@types/bip39": "^2.4.2", + "bignumber.js": "^7.2.0", "bip32": "^1.0.2", "bip39": "^2.5.0", + "buffer-reverse": "^1.0.1", "dotenv": "6.1.0", + "ecurve": "^1.0.6", "js-yaml": "^3.12.0", + "keccak256": "^1.0.0", "lodash": "^4.17.14", "moment": "^2.22.1", "node-fetch": "^2.2.0", @@ -29,6 +33,7 @@ "devDependencies": { "@types/dotenv": "^4.0.3", "@types/jest": "^24.0.13", + "@types/node-fetch": "^2.1.2", "@types/prompts": "^1.1.1", "@types/web3": "^1.0.18", "@types/yargs": "^12.0.1", @@ -40,7 +45,7 @@ "cli": "TS_NODE_FILES=true ts-node -r tsconfig-paths/register src/cli.ts", "test": "jest", "lint": "tslint -c tslint.json --project tsconfig.json", - "lint-checks": "yarn run lint && tsc --project tsconfig.json --noEmit" + "build": "tsc -p tsconfig.json " }, "private": true } diff --git a/packages/celotool/src/cmds/account/lookup.ts b/packages/celotool/src/cmds/account/lookup.ts index 02aef22e776..01c68cf7a91 100644 --- a/packages/celotool/src/cmds/account/lookup.ts +++ b/packages/celotool/src/cmds/account/lookup.ts @@ -1,9 +1,9 @@ /* tslint:disable no-console */ import { AccountArgv } from '@celo/celotool/src/cmds/account' import { portForwardAnd } from '@celo/celotool/src/lib/port_forward' -// @ts-ignore -import { Attestations, lookupPhoneNumbers } from '@celo/contractkit' import { PhoneNumberUtils } from '@celo/utils' +// @ts-ignore +import { Attestations, lookupPhoneNumbers } from '@celo/walletkit' import { switchToClusterFromEnv } from 'src/lib/cluster' import { Argv } from 'yargs' diff --git a/packages/celotool/src/cmds/account/verify.ts b/packages/celotool/src/cmds/account/verify.ts index 93e4811487e..5064d60ed60 100644 --- a/packages/celotool/src/cmds/account/verify.ts +++ b/packages/celotool/src/cmds/account/verify.ts @@ -1,5 +1,6 @@ import { AccountArgv } from '@celo/celotool/src/cmds/account' import { portForwardAnd } from '@celo/celotool/src/lib/port_forward' +import { PhoneNumberUtils } from '@celo/utils' import { ActionableAttestation, // @ts-ignore @@ -15,11 +16,10 @@ import { makeSetWalletAddressTx, StableToken, validateAttestationCode, -} from '@celo/contractkit' +} from '@celo/walletkit' // @ts-ignore -import { Attestations as AttestationsType } from '@celo/contractkit/lib/types/Attestations' -import { StableToken as StableTokenType } from '@celo/contractkit/lib/types/StableToken' -import { PhoneNumberUtils } from '@celo/utils' +import { Attestations as AttestationsType } from '@celo/walletkit/lib/types/Attestations' +import { StableToken as StableTokenType } from '@celo/walletkit/lib/types/StableToken' import prompts from 'prompts' import { switchToClusterFromEnv } from 'src/lib/cluster' import { sendTransaction } from 'src/lib/transactions' @@ -70,13 +70,19 @@ async function verifyCmd(argv: VerifyArgv) { const attestations = await Attestations(web3) const stableToken = await StableToken(web3) const phoneHash = PhoneNumberUtils.getPhoneHash(argv.phone) - await printCurrentCompletedAttestations(attestations, argv.phone, account) + let attestationsToComplete = await getActionableAttestations(attestations, phoneHash, account) + // Request more attestations - if (argv.num > 0) { - console.info(`Requesting ${argv.num} attestations`) - await requestMoreAttestations(attestations, stableToken, phoneHash, argv.num) + if (argv.num > attestationsToComplete.length) { + console.info(`Requesting ${argv.num - attestationsToComplete.length} attestations`) + await requestMoreAttestations( + attestations, + stableToken, + phoneHash, + argv.num - attestationsToComplete.length + ) } // Set the wallet address if not already appropriate @@ -87,8 +93,8 @@ async function verifyCmd(argv: VerifyArgv) { await sendTransaction(setWalletAddressTx) } + attestationsToComplete = await getActionableAttestations(attestations, phoneHash, account) // Find attestations we can reveal/verify - const attestationsToComplete = await getActionableAttestations(attestations, phoneHash, account) console.info(`Revealing ${attestationsToComplete.length} attestations`) await revealAttestations(attestationsToComplete, attestations, argv.phone) diff --git a/packages/celotool/src/cmds/deploy/destroy/ethstats.ts b/packages/celotool/src/cmds/deploy/destroy/ethstats.ts new file mode 100644 index 00000000000..af452eb963d --- /dev/null +++ b/packages/celotool/src/cmds/deploy/destroy/ethstats.ts @@ -0,0 +1,16 @@ +import { DestroyArgv } from '@celo/celotool/src/cmds/deploy/destroy' +import { createClusterIfNotExists, switchToClusterFromEnv } from '@celo/celotool/src/lib/cluster' +import { removeHelmRelease } from '@celo/celotool/src/lib/ethstats' + +export const command = 'ethstats' + +export const describe = 'destroy the ethstats package' + +export const builder = {} + +export const handler = async (argv: DestroyArgv) => { + await createClusterIfNotExists() + await switchToClusterFromEnv() + + await removeHelmRelease(argv.celoEnv) +} diff --git a/packages/celotool/src/cmds/deploy/destroy/vm-testnet.ts b/packages/celotool/src/cmds/deploy/destroy/vm-testnet.ts new file mode 100644 index 00000000000..760cd8c4d68 --- /dev/null +++ b/packages/celotool/src/cmds/deploy/destroy/vm-testnet.ts @@ -0,0 +1,29 @@ +import { DestroyArgv } from '@celo/celotool/src/cmds/deploy/destroy' +import { + destroyTerraformModule, + initTerraformModule, + planTerraformModule, +} from '@celo/celotool/src/lib/terraform' +import { confirmAction, envVar, fetchEnv } from '@celo/celotool/src/lib/utils' + +export const command = 'vm-testnet' +export const describe = 'destroy an existing VM-based testnet' + +export const builder = {} + +const terraformModule = 'testnet' + +export const handler = async (argv: DestroyArgv) => { + const envType = fetchEnv(envVar.ENV_TYPE) + console.info(`Destroying ${argv.celoEnv} in environment ${envType}`) + + console.info('Initializing...') + await initTerraformModule(terraformModule) + + console.info('Planning...') + await planTerraformModule(terraformModule, true) + + await confirmAction(`Are you sure you want to destroy ${argv.celoEnv} in environment ${envType}?`) + + await destroyTerraformModule(terraformModule) +} diff --git a/packages/celotool/src/cmds/deploy/initial/ethstats.ts b/packages/celotool/src/cmds/deploy/initial/ethstats.ts new file mode 100644 index 00000000000..9bad2752bb5 --- /dev/null +++ b/packages/celotool/src/cmds/deploy/initial/ethstats.ts @@ -0,0 +1,16 @@ +import { InitialArgv } from '@celo/celotool/src/cmds/deploy/initial' +import { createClusterIfNotExists, switchToClusterFromEnv } from '@celo/celotool/src/lib/cluster' +import { installHelmChart } from '@celo/celotool/src/lib/ethstats' + +export const command = 'ethstats' + +export const describe = 'deploy the ethstats package' + +export const builder = {} + +export const handler = async (argv: InitialArgv) => { + await createClusterIfNotExists() + await switchToClusterFromEnv() + + await installHelmChart(argv.celoEnv) +} diff --git a/packages/celotool/src/cmds/deploy/initial/vm-testnet.ts b/packages/celotool/src/cmds/deploy/initial/vm-testnet.ts new file mode 100644 index 00000000000..c5c9bf338a5 --- /dev/null +++ b/packages/celotool/src/cmds/deploy/initial/vm-testnet.ts @@ -0,0 +1,41 @@ +import { InitialArgv } from '@celo/celotool/src/cmds/deploy/initial' +import { uploadGenesisBlockToGoogleStorage } from 'src/lib/testnet-utils' +import { confirmAction, envVar, fetchEnv } from 'src/lib/utils' + +import { + applyTerraformModule, + initTerraformModule, + planTerraformModule, +} from '@celo/celotool/src/lib/terraform' + +export const command = 'vm-testnet' + +export const describe = 'deploy a testnet on a VM' + +export const builder = {} + +type VMTestnetInitialArgv = InitialArgv + +const terraformModule = 'testnet' + +export const handler = async (argv: VMTestnetInitialArgv) => { + const envType = fetchEnv(envVar.ENV_TYPE) + console.info(`Deploying ${argv.celoEnv} in environment ${envType}`) + + console.info('Initializing...') + await initTerraformModule(terraformModule) + + console.info('Planning...') + await planTerraformModule(terraformModule) + + await confirmAction( + `Are you sure you want to perform the above plan for Celo env ${ + argv.celoEnv + } in environment ${envType}?` + ) + + console.info('Applying...') + await applyTerraformModule(terraformModule) + + await uploadGenesisBlockToGoogleStorage(argv.celoEnv) +} diff --git a/packages/celotool/src/cmds/deploy/upgrade/ethstats.ts b/packages/celotool/src/cmds/deploy/upgrade/ethstats.ts new file mode 100644 index 00000000000..bfb25f31ae5 --- /dev/null +++ b/packages/celotool/src/cmds/deploy/upgrade/ethstats.ts @@ -0,0 +1,36 @@ +import { UpgradeArgv } from '@celo/celotool/src/cmds/deploy/upgrade' +import { createClusterIfNotExists, switchToClusterFromEnv } from '@celo/celotool/src/lib/cluster' +import { + installHelmChart, + removeHelmRelease, + upgradeHelmChart, +} from '@celo/celotool/src/lib/ethstats' +import yargs from 'yargs' + +export const command = 'ethstats' + +export const describe = 'upgrade the ethstats package' + +type EthstatsArgv = UpgradeArgv & { + reset: boolean +} + +export const builder = (argv: yargs.Argv) => { + return argv.option('reset', { + description: 'Destroy & redeploy the ethstats package', + default: false, + type: 'boolean', + }) +} + +export const handler = async (argv: EthstatsArgv) => { + await createClusterIfNotExists() + await switchToClusterFromEnv() + + if (argv.reset) { + await removeHelmRelease(argv.celoEnv) + await installHelmChart(argv.celoEnv) + } else { + await upgradeHelmChart(argv.celoEnv) + } +} diff --git a/packages/celotool/src/cmds/transactions/describe.ts b/packages/celotool/src/cmds/transactions/describe.ts index 214697975f4..d4865f68a3d 100644 --- a/packages/celotool/src/cmds/transactions/describe.ts +++ b/packages/celotool/src/cmds/transactions/describe.ts @@ -4,8 +4,7 @@ import { getContracts, parseFunctionCall, parseLog, -} from '@celo/contractkit' -import { CONTRACTS_TO_COPY, copyContractArtifacts, downloadArtifacts } from 'src/lib/artifacts' +} from '@celo/walletkit' import { getWeb3Client } from 'src/lib/blockchain' import { switchToClusterFromEnv } from 'src/lib/cluster' import * as yargs from 'yargs' @@ -26,12 +25,6 @@ export const builder = (argv: yargs.Argv) => { export const handler = async (argv: DescribeArgv) => { await switchToClusterFromEnv(false) - await downloadArtifacts(argv.celoEnv) - await copyContractArtifacts( - argv.celoEnv, - '../transaction-metrics-exporter/src/contracts', - CONTRACTS_TO_COPY - ) const web3 = await getWeb3Client(argv.celoEnv) diff --git a/packages/celotool/src/cmds/transactions/list.ts b/packages/celotool/src/cmds/transactions/list.ts index 3ba83ba5554..f99b8278867 100644 --- a/packages/celotool/src/cmds/transactions/list.ts +++ b/packages/celotool/src/cmds/transactions/list.ts @@ -5,7 +5,7 @@ import { getContracts, parseFunctionCall, parseLog, -} from '@celo/contractkit' +} from '@celo/walletkit' import moment from 'moment' import fetch from 'node-fetch' import { CONTRACTS_TO_COPY, copyContractArtifacts, downloadArtifacts } from 'src/lib/artifacts' diff --git a/packages/celotool/src/lib/artifacts.ts b/packages/celotool/src/lib/artifacts.ts index 93046254960..364c12cd55e 100644 --- a/packages/celotool/src/lib/artifacts.ts +++ b/packages/celotool/src/lib/artifacts.ts @@ -10,14 +10,11 @@ import { existsSync, mkdirSync, readFileSync, writeFile } from 'fs' import { promisify } from 'util' export const CONTRACTS_TO_COPY = [ - 'AddressBasedEncryption', - 'Auction', - 'BSTAuction', + 'Attestations', 'Escrow', 'Exchange', 'GoldToken', - 'Medianator', - 'MultiSig', + 'Registry', 'Reserve', 'StableToken', ] diff --git a/packages/celotool/src/lib/blockscout.ts b/packages/celotool/src/lib/blockscout.ts index 5a4bd220702..46f9f3c87c9 100644 --- a/packages/celotool/src/lib/blockscout.ts +++ b/packages/celotool/src/lib/blockscout.ts @@ -62,7 +62,7 @@ function helmParameters( `--set blockscout.db.password=${blockscoutDBPassword}`, `--set blockscout.db.connection_name=${blockscoutDBConnectionName.trim()}`, `--set blockscout.replicas=${fetchEnv('BLOCKSCOUT_WEB_REPLICAS')}`, - `--set blockscout.subnetwork=${fetchEnvOrFallback('BLOCKSCOUT_SUBNETWORK_NAME', celoEnv)}`, + `--set blockscout.subnetwork="${fetchEnvOrFallback('BLOCKSCOUT_SUBNETWORK_NAME', celoEnv)}"`, `--set promtosd.scrape_interval=${fetchEnv('PROMTOSD_SCRAPE_INTERVAL')}`, `--set promtosd.export_interval=${fetchEnv('PROMTOSD_EXPORT_INTERVAL')}`, ] diff --git a/packages/celotool/src/lib/bls_utils.ts b/packages/celotool/src/lib/bls_utils.ts new file mode 100644 index 00000000000..8706d568147 --- /dev/null +++ b/packages/celotool/src/lib/bls_utils.ts @@ -0,0 +1,85 @@ +// this is an implementation of a subset of BLS12-377 +const keccak256 = require('keccak256') +const ecurve = require('ecurve') +const BigInteger = require('bigi') +const reverse = require('buffer-reverse') + +const Curve = ecurve.Curve +const Point = ecurve.Point + +const p = BigInteger.fromHex( + '01ae3a4617c510eac63b05c06ca1493b1a22d9f300f5138f1ef3622fba094800170b5d44300000008508c00000000001' +) +const a = new BigInteger('0') +const b = new BigInteger('1') +const Gx = BigInteger.fromHex( + '8848defe740a67c8fc6225bf87ff5485951e2caa9d41bb188282c8bd37cb5cd5481512ffcd394eeab9b16eb21be9ef' +) +const Gy = BigInteger.fromHex( + '01914a69c5102eff1f674f5d30afeec4bd7fb348ca3e52d96d182ad44fb82305c2fe3d3634a9591afd82de55559c8ea6' +) +const n = BigInteger.fromHex('12ab655e9a2ca55660b44d1e5c37b00159aa76fed00000010a11800000000001', 16) +const h = new BigInteger('30631250834960419227450344600217059328') +const curve = new Curve(p, a, b, Gx, Gy, n, h) +const g = Point.fromAffine(curve, Gx, Gy) + +const MODULUSMASK = 31 + +export const blsPrivateKeyToProcessedPrivateKey = (privateKeyHex: string) => { + for (let i = 0; i < 256; i++) { + const originalPrivateKeyBytes = Buffer.from(privateKeyHex, 'hex') + + const iBuffer = new Buffer(1) + iBuffer[0] = i + const keyBytes = Buffer.concat([ + Buffer.from('ecdsatobls', 'utf8'), + iBuffer, + originalPrivateKeyBytes, + ]) + const privateKeyBLSBytes = keccak256(keyBytes) + + // tslint:disable-next-line:no-bitwise + privateKeyBLSBytes[0] &= MODULUSMASK + + const privateKeyNum = BigInteger.fromBuffer(privateKeyBLSBytes) + if (privateKeyNum.compareTo(n) >= 0) { + continue + } + + const privateKeyBytes = reverse(privateKeyNum.toBuffer()) + + return privateKeyBytes + } + + throw new Error("couldn't derive BLS key from ECDSA key") +} + +export const blsPrivateKeyToPublic = (privateKeyHex: string) => { + const privateKeyBytes = blsPrivateKeyToProcessedPrivateKey(privateKeyHex) + const privateKey = BigInteger.fromBuffer(reverse(privateKeyBytes)) + + const publicKey = g.multiply(privateKey) + const publicKeyXBytes = reverse(publicKey.affineX.toBuffer()) + const publicKeyYNum = BigInteger.fromBuffer(publicKey.affineY.toBuffer()) + const publicKeyYBytes = reverse(publicKey.affineY.toBuffer()) + let publicKeyXHex = publicKeyXBytes.toString('hex') + while (publicKeyXHex.length < 96) { + publicKeyXHex = publicKeyXHex + '00' + } + const publicKeyXBytesPadded = Buffer.from(publicKeyXHex, 'hex') + + if (publicKeyYNum.compareTo(p.subtract(new BigInteger('1')).shiftRight(1)) >= 0) { + // tslint:disable-next-line:no-bitwise + publicKeyXBytesPadded[publicKeyXBytesPadded.length - 1] |= 0x80 + } + publicKeyXHex = publicKeyXBytesPadded.toString('hex') + + let publicKeyYHex = publicKeyYBytes.toString('hex') + while (publicKeyYHex.length < 96) { + publicKeyYHex = publicKeyYHex + '00' + } + + const publicKeyHex = publicKeyXHex + + return publicKeyHex +} diff --git a/packages/celotool/src/lib/ethstats.ts b/packages/celotool/src/lib/ethstats.ts new file mode 100644 index 00000000000..945557aba39 --- /dev/null +++ b/packages/celotool/src/lib/ethstats.ts @@ -0,0 +1,37 @@ +import { installGenericHelmChart, removeGenericHelmChart } from 'src/lib/helm_deploy' +import { envVar, execCmdWithExitOnFailure, fetchEnv } from 'src/lib/utils' + +const helmChartPath = '../helm-charts/ethstats' + +export async function installHelmChart(celoEnv: string) { + return installGenericHelmChart(celoEnv, releaseName(celoEnv), helmChartPath, helmParameters()) +} + +export async function removeHelmRelease(celoEnv: string) { + await removeGenericHelmChart(releaseName(celoEnv)) +} + +export async function upgradeHelmChart(celoEnv: string) { + console.info(`Upgrading helm release ${releaseName(celoEnv)}`) + + const upgradeCmdArgs = `${releaseName( + celoEnv + )} ${helmChartPath} --namespace ${celoEnv} ${helmParameters().join(' ')}` + + if (process.env.CELOTOOL_VERBOSE === 'true') { + await execCmdWithExitOnFailure(`helm upgrade --debug --dry-run ${upgradeCmdArgs}`) + } + await execCmdWithExitOnFailure(`helm upgrade ${upgradeCmdArgs}`) + console.info(`Helm release ${releaseName(celoEnv)} upgrade successful`) +} + +function helmParameters() { + return [ + `--set domain.name=${fetchEnv(envVar.CLUSTER_DOMAIN_NAME)}`, + `--set ethstats.webSocketSecret="${fetchEnv(envVar.ETHSTATS_WEBSOCKETSECRET)}"`, + ] +} + +function releaseName(celoEnv: string) { + return `${celoEnv}-ethstats` +} diff --git a/packages/celotool/src/lib/generate_utils.ts b/packages/celotool/src/lib/generate_utils.ts index e0f73d5e8c0..c9b020dc8fb 100644 --- a/packages/celotool/src/lib/generate_utils.ts +++ b/packages/celotool/src/lib/generate_utils.ts @@ -1,3 +1,4 @@ +import { blsPrivateKeyToPublic } from '@celo/celotool/src/lib/bls_utils' import { CONTRACT_ADDRESSES, CONTRACT_OWNER_STORAGE_LOCATION, @@ -36,6 +37,11 @@ export enum ConsensusType { ISTANBUL = 'istanbul', } +export interface Validator { + address: string + blsPublicKey: string +} + export const MNEMONIC_ACCOUNT_TYPE_CHOICES = [ 'validator', 'load_testing', @@ -44,6 +50,10 @@ export const MNEMONIC_ACCOUNT_TYPE_CHOICES = [ 'faucet', ] +export const add0x = (str: string) => { + return '0x' + str +} + export const coerceMnemonicAccountType = (raw: string): AccountType => { const index = MNEMONIC_ACCOUNT_TYPE_CHOICES.indexOf(raw) if (index === -1) { @@ -86,16 +96,28 @@ export const getAddressesFor = (accountType: AccountType, mnemonic: string, n: n export const getStrippedAddressesFor = (accountType: AccountType, mnemonic: string, n: number) => getAddressesFor(accountType, mnemonic, n).map(strip0x) +export const getValidators = (mnemonic: string, n: number) => { + return range(0, n) + .map((i) => generatePrivateKey(mnemonic, AccountType.VALIDATOR, i)) + .map((key) => { + return { + address: privateKeyToAddress(key).slice(2), + blsPublicKey: blsPrivateKeyToPublic(key), + } + }) +} + export const generateGenesisFromEnv = (enablePetersburg: boolean = true) => { const validatorEnv = fetchEnv(envVar.VALIDATORS) const validators = validatorEnv === VALIDATOR_OG_SOURCE - ? OG_ACCOUNTS.map((account) => account.address) - : getStrippedAddressesFor( - AccountType.VALIDATOR, - fetchEnv(envVar.MNEMONIC), - parseInt(validatorEnv, 10) - ) + ? OG_ACCOUNTS.map((account) => { + return { + address: account.address, + blsPublicKey: blsPrivateKeyToPublic(account.privateKey), + } + }) + : getValidators(fetchEnv(envVar.MNEMONIC), parseInt(validatorEnv, 10)) // @ts-ignore if (![ConsensusType.CLIQUE, ConsensusType.ISTANBUL].includes(fetchEnv(envVar.CONSENSUS_TYPE))) { @@ -132,9 +154,9 @@ export const generateGenesisFromEnv = (enablePetersburg: boolean = true) => { ) } -const generateIstanbulExtraData = (validators: string[]) => { +const generateIstanbulExtraData = (validators: Validator[]) => { const istanbulVanity = 32 - const signatureVanity = 65 + const blsSignatureVanity = 192 return ( '0x' + @@ -142,17 +164,20 @@ const generateIstanbulExtraData = (validators: string[]) => { rlp // @ts-ignore .encode([ - validators.map((validator) => Buffer.from(validator, 'hex')), - [], - Buffer.from(repeat('0', signatureVanity * 2), 'hex'), - [], + validators.map((validator) => Buffer.from(validator.address, 'hex')), + validators.map((validator) => Buffer.from(validator.blsPublicKey, 'hex')), + new Buffer(0), + Buffer.from(repeat('0', blsSignatureVanity * 2), 'hex'), + new Buffer(0), + Buffer.from(repeat('0', blsSignatureVanity * 2), 'hex'), + new Buffer(0), ]) .toString('hex') ) } export const generateGenesis = ( - validators: string[], + validators: Validator[], consensusType: ConsensusType, contracts: string[], blockTime: number, @@ -184,7 +209,7 @@ export const generateGenesis = ( } for (const validator of validators) { - genesis.alloc[validator] = { + genesis.alloc[validator.address] = { balance: DEFAULT_BALANCE, } } @@ -193,7 +218,7 @@ export const generateGenesis = ( genesis.alloc[contract] = { code: PROXY_CONTRACT_CODE, storage: { - [CONTRACT_OWNER_STORAGE_LOCATION]: validators[0], + [CONTRACT_OWNER_STORAGE_LOCATION]: validators[0].address, }, balance: '0', } diff --git a/packages/celotool/src/lib/geth.ts b/packages/celotool/src/lib/geth.ts index be96da4d57c..d3e6d3821c5 100644 --- a/packages/celotool/src/lib/geth.ts +++ b/packages/celotool/src/lib/geth.ts @@ -12,9 +12,9 @@ import { sendTransaction, StableToken, unlockAccount, -} from '@celo/contractkit' -import { GoldToken as GoldTokenType } from '@celo/contractkit/types/GoldToken' -import { StableToken as StableTokenType } from '@celo/contractkit/types/StableToken' +} from '@celo/walletkit' +import { GoldToken as GoldTokenType } from '@celo/walletkit/types/GoldToken' +import { StableToken as StableTokenType } from '@celo/walletkit/types/StableToken' import BigNumber from 'bignumber.js' import fs from 'fs' import { range } from 'lodash' diff --git a/packages/celotool/src/lib/helm_deploy.ts b/packages/celotool/src/lib/helm_deploy.ts index 62b716ce6aa..15292fd990e 100644 --- a/packages/celotool/src/lib/helm_deploy.ts +++ b/packages/celotool/src/lib/helm_deploy.ts @@ -2,7 +2,7 @@ import { getKubernetesClusterRegion, switchToClusterFromEnv } from '@celo/celoto import { ensureAuthenticatedGcloudAccount } from '@celo/celotool/src/lib/gcloud_utils' import { generateGenesisFromEnv } from '@celo/celotool/src/lib/generate_utils' import { OG_ACCOUNTS } from '@celo/celotool/src/lib/genesis_constants' -import { getStatefulSetReplicas, scaleStatefulSet } from '@celo/celotool/src/lib/kubernetes' +import { getStatefulSetReplicas, scaleResource } from '@celo/celotool/src/lib/kubernetes' import { EnvTypes, envVar, @@ -639,15 +639,18 @@ export async function upgradeHelmChart(celoEnv: string) { export async function resetAndUpgradeHelmChart(celoEnv: string) { const txNodesSetName = `${celoEnv}-tx-nodes` const validatorsSetName = `${celoEnv}-validators` + const bootnodeName = `${celoEnv}-bootnode` // scale down nodes - await scaleStatefulSet(celoEnv, txNodesSetName, 0) - await scaleStatefulSet(celoEnv, validatorsSetName, 0) + await scaleResource(celoEnv, 'StatefulSet', txNodesSetName, 0) + await scaleResource(celoEnv, 'StatefulSet', validatorsSetName, 0) + await scaleResource(celoEnv, 'Deployment', bootnodeName, 0) await deletePersistentVolumeClaims(celoEnv) - await sleep(5000) + await sleep(10000) await upgradeHelmChart(celoEnv) + await sleep(10000) const numValdiators = parseInt(fetchEnv(envVar.VALIDATORS), 10) const numTxNodes = parseInt(fetchEnv(envVar.TX_NODES), 10) @@ -655,8 +658,9 @@ export async function resetAndUpgradeHelmChart(celoEnv: string) { // Note(trevor): helm upgrade only compares the current chart to the // previously deployed chart when deciding what needs changing, so we need // to manually scale up to account for when a node count is the same - await scaleStatefulSet(celoEnv, txNodesSetName, numTxNodes) - await scaleStatefulSet(celoEnv, validatorsSetName, numValdiators) + await scaleResource(celoEnv, 'StatefulSet', txNodesSetName, numTxNodes) + await scaleResource(celoEnv, 'StatefulSet', validatorsSetName, numValdiators) + await scaleResource(celoEnv, 'Deployment', bootnodeName, 1) } export async function removeHelmRelease(celoEnv: string) { diff --git a/packages/celotool/src/lib/kubernetes.ts b/packages/celotool/src/lib/kubernetes.ts index 048e92afe2e..48d6b63b890 100644 --- a/packages/celotool/src/lib/kubernetes.ts +++ b/packages/celotool/src/lib/kubernetes.ts @@ -2,13 +2,14 @@ import { envVar, execCmdWithExitOnFailure, fetchEnv } from '@celo/celotool/src/l const NUMBER_OF_TX_NODES = 4 -export async function scaleStatefulSet( +export async function scaleResource( celoEnv: string, + type: string, resourceName: string, replicaCount: number ) { await execCmdWithExitOnFailure( - `kubectl scale statefulset ${resourceName} --replicas=${replicaCount} --namespace ${celoEnv}` + `kubectl scale ${type} ${resourceName} --replicas=${replicaCount} --namespace ${celoEnv}` ) } diff --git a/packages/celotool/src/lib/terraform.ts b/packages/celotool/src/lib/terraform.ts new file mode 100644 index 00000000000..82d84b82374 --- /dev/null +++ b/packages/celotool/src/lib/terraform.ts @@ -0,0 +1,86 @@ +import fs from 'fs' +import path from 'path' + +import { generateGenesisFromEnv } from '@celo/celotool/src/lib/generate_utils' +import { envVar, execCmd, fetchEnv } from '@celo/celotool/src/lib/utils' + +const terraformModulesPath = path.join(__dirname, '../../../terraform-modules') + +// NOTE(trevor): The keys correspond to the variable names that Terraform expects +// and the values correspond to the names of the appropriate env variables +const terraformEnvVars: { [varName: string]: string } = { + block_time: envVar.BLOCK_TIME, + celo_env: envVar.CELOTOOL_CELOENV, + celotool_docker_image_repository: envVar.CELOTOOL_DOCKER_IMAGE_REPOSITORY, + celotool_docker_image_tag: envVar.CELOTOOL_DOCKER_IMAGE_TAG, + geth_verbosity: envVar.GETH_VERBOSITY, + geth_bootnode_docker_image_repository: envVar.GETH_BOOTNODE_DOCKER_IMAGE_REPOSITORY, + geth_bootnode_docker_image_tag: envVar.GETH_BOOTNODE_DOCKER_IMAGE_TAG, + geth_node_docker_image_repository: envVar.GETH_NODE_DOCKER_IMAGE_REPOSITORY, + geth_node_docker_image_tag: envVar.GETH_NODE_DOCKER_IMAGE_TAG, + mnemonic: envVar.MNEMONIC, + network_id: envVar.NETWORK_ID, + validator_count: envVar.VALIDATORS, + validator_geth_account_secret: envVar.GETH_ACCOUNT_SECRET, + verification_pool_url: envVar.VERIFICATION_POOL_URL, +} + +export function initTerraformModule(moduleName: string) { + return execTerraformCmd('init', getModulePath(moduleName), getEnvVarOptions()) +} + +export function planTerraformModule(moduleName: string, destroy: boolean = false) { + const planPath = getPlanPath(moduleName) + // Terraform requires an out directory to exist + const planDir = path.dirname(planPath) + if (!fs.existsSync(planDir)) { + fs.mkdirSync(planDir) + } + return execTerraformCmd( + 'plan', + getModulePath(moduleName), + `-out=${planPath}`, + getVarOptions(), + destroy ? '-destroy' : '' + ) +} + +export function applyTerraformModule(moduleName: string) { + return execTerraformCmd('apply', getPlanPath(moduleName)) +} + +export function destroyTerraformModule(moduleName: string) { + return execTerraformCmd('destroy', getModulePath(moduleName), getVarOptions(), '-force') +} + +function getModulePath(moduleName: string) { + return path.join(terraformModulesPath, moduleName) +} + +function getPlanPath(moduleName: string) { + return path.join(terraformModulesPath, 'plan', moduleName) +} + +function getVarOptions() { + const genesisBuffer = new Buffer(generateGenesisFromEnv()) + return [ + getEnvVarOptions(), + `-var='genesis_content_base64=${genesisBuffer.toString('base64')}'`, + ].join(' ') +} + +// Uses the `terraformEnvVars` mapping to create terraform cli options +// for each variable using values from env vars +function getEnvVarOptions() { + const nameValuePairs = Object.keys(terraformEnvVars).map( + (varName) => `-var='${varName}=${fetchEnv(terraformEnvVars[varName])}'` + ) + return nameValuePairs.join(' ') +} + +function execTerraformCmd(command: string, workspacePath: string, ...options: string[]) { + const optionsStr = options ? options.join(' ') : '' + const cmd = `terraform ${command} -input=false ${optionsStr} ${workspacePath}` + // use the middle two default arguments + return execCmd(cmd, {}, false, true) +} diff --git a/packages/celotool/src/lib/testnet-utils.ts b/packages/celotool/src/lib/testnet-utils.ts index c3c000ea245..cc323e52c65 100644 --- a/packages/celotool/src/lib/testnet-utils.ts +++ b/packages/celotool/src/lib/testnet-utils.ts @@ -1,4 +1,5 @@ -import { StaticNodeUtils } from '@celo/contractkit' +import { ensureAuthenticatedGcloudAccount } from '@celo/celotool/src/lib/gcloud_utils' +import { StaticNodeUtils } from '@celo/walletkit' import { Storage } from '@google-cloud/storage' import { writeFileSync } from 'fs' import { generateGenesisFromEnv } from 'src/lib/generate_utils' @@ -57,6 +58,7 @@ async function uploadFileToGoogleStorage( googleStorageFileName: string, makeFileWorldReadable: boolean ) { + await ensureAuthenticatedGcloudAccount() const storage = new Storage() await storage.bucket(googleStorageBucketName).upload(localFilePath, { destination: googleStorageFileName, diff --git a/packages/celotool/src/lib/transactions.ts b/packages/celotool/src/lib/transactions.ts index 3faa5557880..b5dbc1b17a1 100644 --- a/packages/celotool/src/lib/transactions.ts +++ b/packages/celotool/src/lib/transactions.ts @@ -1,10 +1,10 @@ -import { StableToken } from '@celo/contractkit' -import { StableToken as StableTokenType } from '@celo/contractkit/lib/types/StableToken' +import { StableToken } from '@celo/walletkit' +import { StableToken as StableTokenType } from '@celo/walletkit/lib/types/StableToken' import { awaitConfirmation, emptyTxLogger, sendTransactionAsync, -} from '@celo/contractkit/src/contract-utils' +} from '@celo/walletkit/src/contract-utils' import Web3 from 'web3' import { TransactionObject } from 'web3/eth/types' diff --git a/packages/celotool/src/lib/utils.ts b/packages/celotool/src/lib/utils.ts index a06e937da78..4b2aec1d73a 100644 --- a/packages/celotool/src/lib/utils.ts +++ b/packages/celotool/src/lib/utils.ts @@ -13,6 +13,7 @@ export interface CeloEnvArgv extends yargs.Argv { export enum envVar { BLOCK_TIME = 'BLOCK_TIME', + CELOTOOL_CELOENV = 'CELOTOOL_CELOENV', CELOTOOL_CONFIRMED = 'CELOTOOL_CONFIRMED', CELOTOOL_DOCKER_IMAGE_REPOSITORY = 'CELOTOOL_DOCKER_IMAGE_REPOSITORY', CELOTOOL_DOCKER_IMAGE_TAG = 'CELOTOOL_DOCKER_IMAGE_TAG', @@ -28,9 +29,14 @@ export enum envVar { CLUSTER_DOMAIN_NAME = 'CLUSTER_DOMAIN_NAME', ENV_TYPE = 'ENV_TYPE', EPOCH = 'EPOCH', + ETHSTATS_WEBSOCKETSECRET = 'ETHSTATS_WEBSOCKETSECRET', + GETH_ACCOUNT_SECRET = 'GETH_ACCOUNT_SECRET', + GETH_BOOTNODE_DOCKER_IMAGE_REPOSITORY = 'GETH_BOOTNODE_DOCKER_IMAGE_REPOSITORY', + GETH_BOOTNODE_DOCKER_IMAGE_TAG = 'GETH_BOOTNODE_DOCKER_IMAGE_TAG', GETH_NODES_BACKUP_CRONJOB_ENABLED = 'GETH_NODES_BACKUP_CRONJOB_ENABLED', GETH_NODE_DOCKER_IMAGE_REPOSITORY = 'GETH_NODE_DOCKER_IMAGE_REPOSITORY', GETH_NODE_DOCKER_IMAGE_TAG = 'GETH_NODE_DOCKER_IMAGE_TAG', + GETH_VERBOSITY = 'GETH_VERBOSITY', GETHTX1_NODE_ID = 'GETHTX1_NODE_ID', GETHTX2_NODE_ID = 'GETHTX2_NODE_ID', GETHTX3_NODE_ID = 'GETHTX3_NODE_ID', @@ -52,6 +58,7 @@ export enum envVar { TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG = 'TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG', TX_NODES = 'TX_NODES', VALIDATORS = 'VALIDATORS', + VERIFICATION_POOL_URL = 'VERIFICATION_POOL_URL', } export enum EnvTypes { @@ -64,30 +71,40 @@ export enum EnvTypes { export function execCmd( cmd: string, execOptions: any = {}, - rejectWithOutput = false + rejectWithOutput = false, + pipeOutput = false ): Promise<[string, string]> { return new Promise((resolve, reject) => { if (process.env.CELOTOOL_VERBOSE === 'true') { console.debug('$ ' + cmd) } - exec(cmd, { maxBuffer: 1024 * 1000, ...execOptions }, (err, stdout, stderr) => { - if (process.env.CELOTOOL_VERBOSE === 'true') { - console.debug(stdout.toString()) - } - if (err || process.env.CELOTOOL_VERBOSE === 'true') { - console.error(stderr.toString()) - } - if (err) { - if (rejectWithOutput) { - reject([err, stdout.toString(), stderr.toString()]) + const execProcess = exec( + cmd, + { maxBuffer: 1024 * 1000, ...execOptions }, + (err, stdout, stderr) => { + if (process.env.CELOTOOL_VERBOSE === 'true') { + console.debug(stdout.toString()) + } + if (err || process.env.CELOTOOL_VERBOSE === 'true') { + console.error(stderr.toString()) + } + if (err) { + if (rejectWithOutput) { + reject([err, stdout.toString(), stderr.toString()]) + } else { + reject(err) + } } else { - reject(err) + resolve([stdout.toString(), stderr.toString()]) } - } else { - resolve([stdout.toString(), stderr.toString()]) } - }) + ) + + if (pipeOutput) { + execProcess.stdout.pipe(process.stdout) + execProcess.stderr.pipe(process.stderr) + } }) } @@ -258,19 +275,22 @@ export async function doCheckOrPromptIfStagingOrProduction() { process.env.CELOTOOL_CONFIRMED !== 'true' && isValidStagingOrProductionEnv(process.env.CELOTOOL_CELOENV!) ) { - const response = await prompts({ - type: 'confirm', - name: 'confirmation', - message: - 'You are about to apply a possibly irreversable action on a staging/production environment. Are you sure? (y/n)', - }) + await confirmAction( + 'You are about to apply a possibly irreversable action on a staging/production environment. Are you sure?' + ) + process.env.CELOTOOL_CONFIRMED = 'true' + } +} - if (response.confirmation) { - process.env.CELOTOOL_CONFIRMED = 'true' - } else { - console.info('Aborting due to user response') - process.exit(0) - } +export async function confirmAction(message: string) { + const response = await prompts({ + type: 'confirm', + name: 'confirmation', + message: `${message} (y/n)`, + }) + if (!response.confirmation) { + console.info('Aborting due to user response') + process.exit(0) } } diff --git a/packages/celotool/tsconfig.json b/packages/celotool/tsconfig.json index 631e788fa86..3a272b62d7e 100644 --- a/packages/celotool/tsconfig.json +++ b/packages/celotool/tsconfig.json @@ -15,6 +15,7 @@ "noUnusedParameters": true, "resolveJsonModule": true, "strict": true, + "noEmit": true, "target": "es6", "esModuleInterop": true, "allowSyntheticDefaultImports": true, diff --git a/packages/cli/README.md b/packages/cli/README.md index 84162fbb439..8b6ec8ddf40 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,647 +1,13 @@ -# celo-cli +# celocli -Tool for transacting with the Celo protocol +Tool for interacting with the Celo Protocol. -[![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io) -[![Version](https://img.shields.io/npm/v/@celo/celocli.svg)](https://npmjs.org/package/celo-cli) -[![CircleCI](https://circleci.com/gh/celo-org/celo-monorepo/tree/master.svg?style=shield)](https://circleci.com/gh/celo-org/celo-monorepo/tree/master) -[![Downloads/week](https://img.shields.io/npm/dw/@celo/celocli.svg)](https://npmjs.org/package/celo-cli) -[![License](https://img.shields.io/npm/l/@celo/celocli.svg)](https://github.com/celo-org/celo-monorepo/blob/master/package.json) +## Development - +Use `yarn build:sdk ` to build the sdk for the target environment (CLI dependency). -- [celo-cli](#celo-cli) -- [Usage](#usage) -- [Commands](#commands) - +Use `yarn build` to compile the CLI. -# Usage +Use `yarn docs` to populate `packages/docs` with generated documentation. Generated files should be checked in, and CI will fail if CLI modifications cause changes in the docs which were not checked in. - - -```sh-session -$ npm install -g @celo/celocli -$ celocli COMMAND -running command... -$ celocli (-v|--version|version) -@celo/celocli/0.0.6 darwin-x64 node-v8.12.0 -$ celocli --help [COMMAND] -USAGE - $ celocli COMMAND -... -``` - - - -# Commands - - - -- [`celocli account:balance ACCOUNT`](#celocli-accountbalance-account) -- [`celocli account:new`](#celocli-accountnew) -- [`celocli account:transferdollar`](#celocli-accounttransferdollar) -- [`celocli account:transfergold`](#celocli-accounttransfergold) -- [`celocli account:unlock`](#celocli-accountunlock) -- [`celocli bonds:deposit`](#celocli-bondsdeposit) -- [`celocli bonds:list ACCOUNT`](#celocli-bondslist-account) -- [`celocli bonds:notify`](#celocli-bondsnotify) -- [`celocli bonds:register`](#celocli-bondsregister) -- [`celocli bonds:rewards`](#celocli-bondsrewards) -- [`celocli bonds:show ACCOUNT`](#celocli-bondsshow-account) -- [`celocli bonds:withdraw AVAILABILITYTIME`](#celocli-bondswithdraw-availabilitytime) -- [`celocli config:get`](#celocli-configget) -- [`celocli config:set`](#celocli-configset) -- [`celocli exchange:list`](#celocli-exchangelist) -- [`celocli exchange:selldollar SELLAMOUNT MINBUYAMOUNT FROM`](#celocli-exchangeselldollar-sellamount-minbuyamount-from) -- [`celocli exchange:sellgold SELLAMOUNT MINBUYAMOUNT FROM`](#celocli-exchangesellgold-sellamount-minbuyamount-from) -- [`celocli help [COMMAND]`](#celocli-help-command) -- [`celocli node:accounts`](#celocli-nodeaccounts) -- [`celocli validator:affiliation`](#celocli-validatoraffiliation) -- [`celocli validator:list`](#celocli-validatorlist) -- [`celocli validator:register`](#celocli-validatorregister) -- [`celocli validator:show VALIDATORADDRESS`](#celocli-validatorshow-validatoraddress) -- [`celocli validatorgroup:list`](#celocli-validatorgrouplist) -- [`celocli validatorgroup:member VALIDATORADDRESS`](#celocli-validatorgroupmember-validatoraddress) -- [`celocli validatorgroup:register`](#celocli-validatorgroupregister) -- [`celocli validatorgroup:show GROUPADDRESS`](#celocli-validatorgroupshow-groupaddress) -- [`celocli validatorgroup:vote`](#celocli-validatorgroupvote) - -## `celocli account:balance ACCOUNT` - -View token balances given account address - -``` -USAGE - $ celocli account:balance ACCOUNT - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - -EXAMPLE - balance 0x5409ed021d9299bf6814279a6a1411a7e866a631 -``` - -_See code: [src/commands/account/balance.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/balance.ts)_ - -## `celocli account:new` - -Creates a new account - -``` -USAGE - $ celocli account:new - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - -EXAMPLE - new -``` - -_See code: [src/commands/account/new.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/new.ts)_ - -## `celocli account:transferdollar` - -Transfer dollar - -``` -USAGE - $ celocli account:transferdollar - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - --amountInWei=amountInWei (required) Amount to transfer (in wei) - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the sender - --to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the receiver - -EXAMPLE - transfer --from 0xa0Af2E71cECc248f4a7fD606F203467B500Dd53B --to 0x5409ed021d9299bf6814279a6a1411a7e866a631 - --amountInWei 1 -``` - -_See code: [src/commands/account/transferdollar.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/transferdollar.ts)_ - -## `celocli account:transfergold` - -Transfer gold - -``` -USAGE - $ celocli account:transfergold - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - --amountInWei=amountInWei (required) Amount to transfer (in wei) - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the sender - --to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the receiver - -EXAMPLE - transfer --from 0xa0Af2E71cECc248f4a7fD606F203467B500Dd53B --to 0x5409ed021d9299bf6814279a6a1411a7e866a631 - --amountInWei 1 -``` - -_See code: [src/commands/account/transfergold.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/transfergold.ts)_ - -## `celocli account:unlock` - -Unlock an account address to send transactions - -``` -USAGE - $ celocli account:unlock - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - --account=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address - --password=password (required) - -EXAMPLE - unlock --account 0x5409ed021d9299bf6814279a6a1411a7e866a631 --password 1234 -``` - -_See code: [src/commands/account/unlock.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/unlock.ts)_ - -## `celocli bonds:deposit` - -Create a bonded deposit given notice period and gold amount - -``` -USAGE - $ celocli bonds:deposit - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - --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 bonded - deposit; - -EXAMPLE - deposit --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --noticePeriod 8640 --goldAmount 1000000000000000000 -``` - -_See code: [src/commands/bonds/deposit.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/bonds/deposit.ts)_ - -## `celocli bonds:list ACCOUNT` - -View information about all of the account's deposits - -``` -USAGE - $ celocli bonds:list ACCOUNT - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - -EXAMPLE - list 0x5409ed021d9299bf6814279a6a1411a7e866a631 -``` - -_See code: [src/commands/bonds/list.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/bonds/list.ts)_ - -## `celocli bonds:notify` - -Notify a bonded deposit given notice period and gold amount - -``` -USAGE - $ celocli bonds:notify - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - --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 bonded deposit; - -EXAMPLE - notify --noticePeriod=3600 --goldAmount=500 -``` - -_See code: [src/commands/bonds/notify.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/bonds/notify.ts)_ - -## `celocli bonds:register` - -Register an account for bonded deposit eligibility - -``` -USAGE - $ celocli bonds:register - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address - -EXAMPLE - register -``` - -_See code: [src/commands/bonds/register.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/bonds/register.ts)_ - -## `celocli bonds:rewards` - -Manage rewards for bonded deposit account - -``` -USAGE - $ celocli bonds:rewards - -OPTIONS - -d, --delegate=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d Delegate rewards to provided account - -h, --help show CLI help - -l, --logLevel=logLevel - -r, --redeem Redeem accrued rewards from bonded deposits - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address - -EXAMPLES - rewards --redeem - rewards --delegate=0x56e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 -``` - -_See code: [src/commands/bonds/rewards.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/bonds/rewards.ts)_ - -## `celocli bonds:show ACCOUNT` - -View bonded gold and corresponding account weight of a deposit given ID - -``` -USAGE - $ celocli bonds:show ACCOUNT - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - --availabilityTime=availabilityTime unix timestamp at which withdrawable; doubles as ID of a notified deposit - - --noticePeriod=noticePeriod duration (seconds) from notice to withdrawable; doubles as ID of a bonded - deposit; - -EXAMPLES - show 0x5409ed021d9299bf6814279a6a1411a7e866a631 --noticePeriod=3600 - show 0x5409ed021d9299bf6814279a6a1411a7e866a631 --availabilityTime=1562206887 -``` - -_See code: [src/commands/bonds/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/bonds/show.ts)_ - -## `celocli bonds:withdraw AVAILABILITYTIME` - -Withdraw notified deposit given availability time - -``` -USAGE - $ celocli bonds:withdraw AVAILABILITYTIME - -ARGUMENTS - AVAILABILITYTIME unix timestamp at which withdrawable; doubles as ID of a notified deposit - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address - -EXAMPLE - withdraw 3600 -``` - -_See code: [src/commands/bonds/withdraw.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/bonds/withdraw.ts)_ - -## `celocli config:get` - -Output network node configuration - -``` -USAGE - $ celocli config:get - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel -``` - -_See code: [src/commands/config/get.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/config/get.ts)_ - -## `celocli config:set` - -Configure running node information for propogating transactions to network - -``` -USAGE - $ celocli config:set - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - --node=node (required) [default: ws://localhost:8546] Node URL -``` - -_See code: [src/commands/config/set.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/config/set.ts)_ - -## `celocli exchange:list` - -List information about tokens on the exchange (all amounts in wei) - -``` -USAGE - $ celocli exchange:list - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - --amount=amount [default: 1000000000000000000] Amount of sellToken (in wei) to report rates for - -EXAMPLE - list -``` - -_See code: [src/commands/exchange/list.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/exchange/list.ts)_ - -## `celocli exchange:selldollar SELLAMOUNT MINBUYAMOUNT FROM` - -Sell Celo dollars for Celo gold on the exchange - -``` -USAGE - $ celocli exchange:selldollar SELLAMOUNT MINBUYAMOUNT FROM - -ARGUMENTS - SELLAMOUNT the amount of sellToken (in wei) to sell - MINBUYAMOUNT the minimum amount of buyToken (in wei) expected - FROM - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - -EXAMPLE - selldollar 100 300 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d -``` - -_See code: [src/commands/exchange/selldollar.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/exchange/selldollar.ts)_ - -## `celocli exchange:sellgold SELLAMOUNT MINBUYAMOUNT FROM` - -Sell Celo gold for Celo dollars on the exchange - -``` -USAGE - $ celocli exchange:sellgold SELLAMOUNT MINBUYAMOUNT FROM - -ARGUMENTS - SELLAMOUNT the amount of sellToken (in wei) to sell - MINBUYAMOUNT the minimum amount of buyToken (in wei) expected - FROM - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - -EXAMPLE - sellgold 100 300 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d -``` - -_See code: [src/commands/exchange/sellgold.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/exchange/sellgold.ts)_ - -## `celocli help [COMMAND]` - -display help for celocli - -``` -USAGE - $ celocli help [COMMAND] - -ARGUMENTS - COMMAND command to show help for - -OPTIONS - --all see all commands in CLI -``` - -_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v2.2.0/src/commands/help.ts)_ - -## `celocli node:accounts` - -List node accounts - -``` -USAGE - $ celocli node:accounts - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel -``` - -_See code: [src/commands/node/accounts.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/node/accounts.ts)_ - -## `celocli validator:affiliation` - -Manage affiliation to a ValidatorGroup - -``` -USAGE - $ celocli validator:affiliation - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Validator's address - --set=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d set affiliation to given address - --unset clear affiliation field - -EXAMPLES - affiliation --set 0x97f7333c51897469e8d98e7af8653aab468050a3 --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 - affiliation --unset --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 -``` - -_See code: [src/commands/validator/affiliation.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/affiliation.ts)_ - -## `celocli validator:list` - -List existing Validators - -``` -USAGE - $ celocli validator:list - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - -EXAMPLE - list -``` - -_See code: [src/commands/validator/list.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/list.ts)_ - -## `celocli validator:register` - -Register a new Validator - -``` -USAGE - $ celocli validator:register - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address for the Validator - --id=id (required) - --name=name (required) - --noticePeriod=noticePeriod (required) Notice Period for the Bonded deposit to use - --publicKey=0x (required) Public Key - --url=url (required) - -EXAMPLE - register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --id myID --name myNAme --noticePeriod 5184000 --url - "http://validator.com" --publicKey - 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf - 997eda082ae1 -``` - -_See code: [src/commands/validator/register.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/register.ts)_ - -## `celocli validator:show VALIDATORADDRESS` - -Show information about an existing Validator - -``` -USAGE - $ celocli validator:show VALIDATORADDRESS - -ARGUMENTS - VALIDATORADDRESS Validator's address - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - -EXAMPLE - show 0x97f7333c51897469E8D98E7af8653aAb468050a3 -``` - -_See code: [src/commands/validator/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/show.ts)_ - -## `celocli validatorgroup:list` - -List existing Validator Groups - -``` -USAGE - $ celocli validatorgroup:list - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - -EXAMPLE - list -``` - -_See code: [src/commands/validatorgroup/list.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validatorgroup/list.ts)_ - -## `celocli validatorgroup:member VALIDATORADDRESS` - -Register a new Validator Group - -``` -USAGE - $ celocli validatorgroup:member VALIDATORADDRESS - -ARGUMENTS - VALIDATORADDRESS Validator's address - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - --accept Accept a validatior whose affiliation is already set to the vgroup - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) ValidatorGroup's address - --remove Remove a validatior from the members list - -EXAMPLES - member --accept 0x97f7333c51897469e8d98e7af8653aab468050a3 - member --remove 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 -``` - -_See code: [src/commands/validatorgroup/member.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validatorgroup/member.ts)_ - -## `celocli validatorgroup:register` - -Register a new Validator Group - -``` -USAGE - $ celocli validatorgroup:register - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address for the Validator Group - --id=id (required) - --name=name (required) - --noticePeriod=noticePeriod (required) Notice Period for the Bonded deposit to use - --url=url (required) - -EXAMPLE - register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --id myID --name myNAme --noticePeriod 5184000 --url - "http://vgroup.com" -``` - -_See code: [src/commands/validatorgroup/register.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validatorgroup/register.ts)_ - -## `celocli validatorgroup:show GROUPADDRESS` - -Show information about an existing Validator Group - -``` -USAGE - $ celocli validatorgroup:show GROUPADDRESS - -ARGUMENTS - GROUPADDRESS ValidatorGroup's address - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - -EXAMPLE - show 0x97f7333c51897469E8D98E7af8653aAb468050a3 -``` - -_See code: [src/commands/validatorgroup/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validatorgroup/show.ts)_ - -## `celocli validatorgroup:vote` - -Vote for a Validator Group - -``` -USAGE - $ celocli validatorgroup:vote - -OPTIONS - -h, --help show CLI help - -l, --logLevel=logLevel - --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: [src/commands/validatorgroup/vote.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validatorgroup/vote.ts)_ - - +_See [@celo/dev-cli](https://github.com/celo-org/dev-cli) for how we customize doc generation._ diff --git a/packages/cli/import_contract_types_from_sdk.sh b/packages/cli/import_contract_types_from_sdk.sh index cb056211ed2..75a30e4b459 100755 --- a/packages/cli/import_contract_types_from_sdk.sh +++ b/packages/cli/import_contract_types_from_sdk.sh @@ -3,11 +3,11 @@ set -euo pipefail environment="$1" -yarn --cwd=../contractkit build $environment +yarn --cwd=../walletkit build $environment rm -rf ./src/generated mkdir -p ./src/generated/contracts mkdir -p ./src/generated/types -cp ../contractkit/contracts/*.ts ./src/generated/contracts -cp ../contractkit/types/*.d.ts ./src/generated/types +cp ../walletkit/contracts/*.ts ./src/generated/contracts +cp ../walletkit/types/*.d.ts ./src/generated/types diff --git a/packages/cli/package.json b/packages/cli/package.json index cfa97b3811a..514d645dc57 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -21,21 +21,17 @@ "node": ">=8.0.0" }, "scripts": { - "setup:environment": "yarn --cwd=packages/contractkit build-sdk", - "build": "rm -rf lib && yarn run prepack", + "build": "rm -rf lib && tsc -b", + "docs": "yarn oclif-dev readme --multi --dir=../docs/command-line-interface && yarn prettier ../docs/command-line-interface/*.md --write", "lint": "tslint -c tslint.json --project tsconfig.json", - "lint-checks": "yarn run lint", - "postpack": "rm -f oclif.manifest.json", - "posttest": "tslint -p . -t stylish", - "prepack": "tsc -b && oclif-dev manifest && oclif-dev readme", - "test": "export TZ=UTC && jest --ci --silent --coverage", - "version": "oclif-dev readme && git add README.md" + "test": "export TZ=UTC && jest --ci --silent" }, "dependencies": { - "@celo/contractkit": "^0.0.1", + "@celo/walletkit": "^0.0.4", "@oclif/command": "^1", "@oclif/config": "^1", "@oclif/plugin-help": "^2", + "ethereumjs-util": "^5.2.0", "bip32": "^1.0.2", "bip39": "^2.5.0", "bn.js": "^5.0.0", @@ -52,7 +48,7 @@ "web3": "1.0.0-beta.37" }, "devDependencies": { - "@oclif/dev-cli": "^1", + "@celo/dev-cli": "^2.0.3", "@types/bip32": "^1.0.1", "@types/bip39": "^2.4.2", "@types/elliptic": "^6.4.9", diff --git a/packages/cli/src/adapters/bonded-deposit.ts b/packages/cli/src/adapters/bonded-deposit.ts index 0653f52e285..263be39faed 100644 --- a/packages/cli/src/adapters/bonded-deposit.ts +++ b/packages/cli/src/adapters/bonded-deposit.ts @@ -1,9 +1,7 @@ +import { BondedDeposits } from '@celo/walletkit' import BN from 'bn.js' import Web3 from 'web3' import { TransactionObject } from 'web3/eth/types' - -import { BondedDeposits } from '@celo/contractkit' - import { Address, zip } from '../utils/helpers' export interface VotingDetails { @@ -25,6 +23,13 @@ export interface Deposits { weight: BN } } + +enum Roles { + validating, + voting, + rewards, +} + export class BondedDepositAdapter { public contractPromise: ReturnType @@ -45,7 +50,9 @@ export class BondedDepositAdapter { async getVotingDetails(accountOrVoterAddress: Address): Promise { const contract = await this.contract() - const accountAddress = await contract.methods.getAccountFromVoter(accountOrVoterAddress).call() + const accountAddress = await contract.methods + .getAccountFromDelegateAndRole(accountOrVoterAddress, Roles.voting) + .call() return { accountAddress, @@ -124,7 +131,7 @@ export class BondedDepositAdapter { const contract = await this.contract() const sig = await this.getParsedSignatureOfAddress(account, delegate) - return contract.methods.delegateRewards(delegate, sig.v, sig.r, sig.s) + return contract.methods.delegateRole(Roles.rewards, delegate, sig.v, sig.r, sig.s) } async getParsedSignatureOfAddress(address: string, signer: string) { diff --git a/packages/cli/src/adapters/validators.ts b/packages/cli/src/adapters/validators.ts index 9d315542deb..b487437756c 100644 --- a/packages/cli/src/adapters/validators.ts +++ b/packages/cli/src/adapters/validators.ts @@ -1,7 +1,7 @@ import Web3 from 'web3' import { TransactionObject } from 'web3/eth/types' -import { Validators } from '@celo/contractkit' +import { Validators } from '@celo/walletkit' import { Address, compareBN, eqAddress, NULL_ADDRESS, zip } from '../utils/helpers' import { BondedDepositAdapter } from './bonded-deposit' diff --git a/packages/cli/src/base.ts b/packages/cli/src/base.ts index 128535f4328..e406e5f8253 100644 --- a/packages/cli/src/base.ts +++ b/packages/cli/src/base.ts @@ -5,8 +5,8 @@ import { injectDebugProvider } from './utils/eth-debug-provider' export abstract class BaseCommand extends Command { static flags = { - logLevel: flags.string({ char: 'l' }), - help: flags.help({ char: 'h' }), + logLevel: flags.string({ char: 'l', hidden: true }), + help: flags.help({ char: 'h', hidden: true }), } private _web3: Web3 | null = null diff --git a/packages/cli/src/commands/account/balance.ts b/packages/cli/src/commands/account/balance.ts index 90fcfeb054b..8ab88b72953 100644 --- a/packages/cli/src/commands/account/balance.ts +++ b/packages/cli/src/commands/account/balance.ts @@ -1,4 +1,4 @@ -import { StableToken } from '@celo/contractkit' +import { StableToken } from '@celo/walletkit' import { BaseCommand } from '../../base' import { printValueMap } from '../../utils/cli' diff --git a/packages/cli/src/commands/account/transferdollar.ts b/packages/cli/src/commands/account/transferdollar.ts index 82daaf459f2..93d2c1fff7b 100644 --- a/packages/cli/src/commands/account/transferdollar.ts +++ b/packages/cli/src/commands/account/transferdollar.ts @@ -1,6 +1,6 @@ import Web3 from 'web3' -import { StableToken } from '@celo/contractkit' +import { StableToken } from '@celo/walletkit' import { flags } from '@oclif/command' import { BaseCommand } from '../../base' diff --git a/packages/cli/src/commands/account/transfergold.ts b/packages/cli/src/commands/account/transfergold.ts index a6774a899b6..e05e7578a03 100644 --- a/packages/cli/src/commands/account/transfergold.ts +++ b/packages/cli/src/commands/account/transfergold.ts @@ -1,6 +1,6 @@ import Web3 from 'web3' -import { GoldToken } from '@celo/contractkit' +import { GoldToken } from '@celo/walletkit' import { flags } from '@oclif/command' import { BaseCommand } from '../../base' diff --git a/packages/cli/src/commands/bonds/deposit.ts b/packages/cli/src/commands/bonds/deposit.ts index 8e1096cb333..90e42a83d40 100644 --- a/packages/cli/src/commands/bonds/deposit.ts +++ b/packages/cli/src/commands/bonds/deposit.ts @@ -1,6 +1,6 @@ import Web3 from 'web3' -import { BondedDeposits } from '@celo/contractkit' +import { BondedDeposits } from '@celo/walletkit' import { flags } from '@oclif/command' import { BaseCommand } from '../../base' diff --git a/packages/cli/src/commands/bonds/notify.ts b/packages/cli/src/commands/bonds/notify.ts index 5bd1c76c924..fd4aa88c3a4 100644 --- a/packages/cli/src/commands/bonds/notify.ts +++ b/packages/cli/src/commands/bonds/notify.ts @@ -1,4 +1,4 @@ -import { BondedDeposits } from '@celo/contractkit' +import { BondedDeposits } from '@celo/walletkit' import { flags } from '@oclif/command' import { BaseCommand } from '../../base' diff --git a/packages/cli/src/commands/bonds/register.ts b/packages/cli/src/commands/bonds/register.ts index d96128f742f..a7ae76bceeb 100644 --- a/packages/cli/src/commands/bonds/register.ts +++ b/packages/cli/src/commands/bonds/register.ts @@ -1,4 +1,4 @@ -import { BondedDeposits } from '@celo/contractkit' +import { BondedDeposits } from '@celo/walletkit' import { BaseCommand } from '../../base' import { displaySendTx } from '../../utils/cli' diff --git a/packages/cli/src/commands/bonds/withdraw.ts b/packages/cli/src/commands/bonds/withdraw.ts index a8de719c299..e1f3cafcf23 100644 --- a/packages/cli/src/commands/bonds/withdraw.ts +++ b/packages/cli/src/commands/bonds/withdraw.ts @@ -1,4 +1,4 @@ -import { BondedDeposits } from '@celo/contractkit' +import { BondedDeposits } from '@celo/walletkit' import { BaseCommand } from '../../base' import { BondArgs } from '../../utils/bonds' diff --git a/packages/cli/src/commands/exchange/list.ts b/packages/cli/src/commands/exchange/list.ts index 59429a59bf8..a89c0661da2 100644 --- a/packages/cli/src/commands/exchange/list.ts +++ b/packages/cli/src/commands/exchange/list.ts @@ -1,6 +1,6 @@ import { cli } from 'cli-ux' -import { Exchange } from '@celo/contractkit' +import { Exchange } from '@celo/walletkit' import { flags } from '@oclif/command' import { BaseCommand } from '../../base' diff --git a/packages/cli/src/commands/exchange/selldollar.ts b/packages/cli/src/commands/exchange/selldollar.ts index cd5cc499c0b..52d436f02fa 100644 --- a/packages/cli/src/commands/exchange/selldollar.ts +++ b/packages/cli/src/commands/exchange/selldollar.ts @@ -1,4 +1,4 @@ -import { StableToken } from '@celo/contractkit' +import { StableToken } from '@celo/walletkit' import { BaseCommand } from '../../base' import { doSwap, swapArguments } from '../../utils/exchange' diff --git a/packages/cli/src/commands/exchange/sellgold.ts b/packages/cli/src/commands/exchange/sellgold.ts index 9c9bb4261b8..a3e356f3a4e 100644 --- a/packages/cli/src/commands/exchange/sellgold.ts +++ b/packages/cli/src/commands/exchange/sellgold.ts @@ -1,4 +1,4 @@ -import { GoldToken } from '@celo/contractkit' +import { GoldToken } from '@celo/walletkit' import { BaseCommand } from '../../base' import { doSwap, swapArguments } from '../../utils/exchange' diff --git a/packages/cli/src/commands/validator/affiliation.ts b/packages/cli/src/commands/validator/affiliation.ts index 4a304647a3e..2aa8d1f9e98 100644 --- a/packages/cli/src/commands/validator/affiliation.ts +++ b/packages/cli/src/commands/validator/affiliation.ts @@ -1,4 +1,4 @@ -import { Validators } from '@celo/contractkit' +import { Validators } from '@celo/walletkit' import { flags } from '@oclif/command' import { BaseCommand } from '../../base' diff --git a/packages/cli/src/commands/validator/register.ts b/packages/cli/src/commands/validator/register.ts index 826c579080d..aba5b685e04 100644 --- a/packages/cli/src/commands/validator/register.ts +++ b/packages/cli/src/commands/validator/register.ts @@ -1,9 +1,10 @@ -import { Validators } from '@celo/contractkit' +import { Attestations, Validators } from '@celo/walletkit' import { flags } from '@oclif/command' import { BaseCommand } from '../../base' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' +import { getPubKeyFromAddrAndWeb3 } from '../../utils/helpers' export default class ValidatorRegister extends BaseCommand { static description = 'Register a new Validator' @@ -22,7 +23,7 @@ export default class ValidatorRegister extends BaseCommand { } static examples = [ - 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --id myID --name myNAme --noticePeriod 5184000 --url "http://validator.com" --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae1', + 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --id myID --name myNAme --noticePeriod 5184000 --url "http://validator.com" --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', ] async run() { const res = this.parse(ValidatorRegister) @@ -37,5 +38,13 @@ export default class ValidatorRegister extends BaseCommand { res.flags.noticePeriod ) ) + + // register encryption key on attestations contract + const attestations = await Attestations(this.web3, res.flags.from) + // TODO: Use a different key data encryption + const pubKey = await getPubKeyFromAddrAndWeb3(res.flags.from, this.web3) + // @ts-ignore + const setKeyTx = attestations.methods.setAccountDataEncryptionKey(pubKey) + await displaySendTx('Set encryption key', setKeyTx) } } diff --git a/packages/cli/src/commands/validatorgroup/member.ts b/packages/cli/src/commands/validatorgroup/member.ts index 0b1ee5554d5..eac41072e70 100644 --- a/packages/cli/src/commands/validatorgroup/member.ts +++ b/packages/cli/src/commands/validatorgroup/member.ts @@ -1,4 +1,4 @@ -import { Validators } from '@celo/contractkit' +import { Validators } from '@celo/walletkit' import { flags } from '@oclif/command' import { IArg } from '@oclif/parser/lib/args' diff --git a/packages/cli/src/commands/validatorgroup/register.ts b/packages/cli/src/commands/validatorgroup/register.ts index 6a32a788fda..0ef6a0619a5 100644 --- a/packages/cli/src/commands/validatorgroup/register.ts +++ b/packages/cli/src/commands/validatorgroup/register.ts @@ -1,4 +1,4 @@ -import { Validators } from '@celo/contractkit' +import { Validators } from '@celo/walletkit' import { flags } from '@oclif/command' import { BaseCommand } from '../../base' diff --git a/packages/cli/src/utils/exchange.ts b/packages/cli/src/utils/exchange.ts index 6509815fdaa..7a77ff1448a 100644 --- a/packages/cli/src/utils/exchange.ts +++ b/packages/cli/src/utils/exchange.ts @@ -1,6 +1,6 @@ import Web3 from 'web3' -import { CeloTokenType, Exchange } from '@celo/contractkit' +import { CeloTokenType, Exchange } from '@celo/walletkit' import { displaySendTx } from './cli' diff --git a/packages/cli/src/utils/helpers.ts b/packages/cli/src/utils/helpers.ts index b054aa45cd0..39a2b7a0473 100644 --- a/packages/cli/src/utils/helpers.ts +++ b/packages/cli/src/utils/helpers.ts @@ -1,4 +1,8 @@ import BN from 'bn.js' +import ethjsutil from 'ethereumjs-util' +import Web3 from 'web3' + +import assert = require('assert') export type Address = string @@ -26,4 +30,22 @@ export function eqAddress(a: Address, b: Address) { return a.replace('0x', '').toLowerCase() === b.replace('0x', '').toLowerCase() } +export async function getPubKeyFromAddrAndWeb3(addr: string, web3: Web3) { + const msg = new Buffer('dummy_msg_data') + const data = '0x' + msg.toString('hex') + // Note: Eth.sign typing displays incorrect parameter order + const sig = await web3.eth.sign(data, addr) + + const rawsig = ethjsutil.fromRpcSig(sig) + + const prefix = new Buffer('\x19Ethereum Signed Message:\n') + const prefixedMsg = ethjsutil.sha3(Buffer.concat([prefix, new Buffer(String(msg.length)), msg])) + const pubKey = ethjsutil.ecrecover(prefixedMsg, rawsig.v, rawsig.r, rawsig.s) + + const computedAddr = ethjsutil.pubToAddress(pubKey).toString('hex') + assert(eqAddress(computedAddr, addr), 'computed address !== addr') + + return pubKey +} + export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000' diff --git a/packages/cli/start_geth.sh b/packages/cli/start_geth.sh index c0d861550c1..3dc99208d4a 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 1101 -NETWORK_ID=${4:-"44781"} +# Default to 44782 +NETWORK_ID=${4:-"44782"} 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/dappkit/.gitignore b/packages/dappkit/.gitignore new file mode 100644 index 00000000000..7951405f85a --- /dev/null +++ b/packages/dappkit/.gitignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/packages/dappkit/package.json b/packages/dappkit/package.json new file mode 100644 index 00000000000..60a88209aeb --- /dev/null +++ b/packages/dappkit/package.json @@ -0,0 +1,18 @@ +{ + "name": "@celo/dappkit", + "version": "0.0.1-j", + "scripts": { + "build": "tsc" + }, + "dependencies": { + "@celo/walletkit": "0.0.4", + "@celo/utils": "0.0.5", + "expo": "^34.0.1" + }, + "devDependencies": { + "typescript": "3.3.3", + "@types/web3": "^1.0.18" + }, + "main": "./lib/index.js", + "files": ["src/**/*", "lib/**/*"] +} \ No newline at end of file diff --git a/packages/dappkit/src/index.ts b/packages/dappkit/src/index.ts new file mode 100644 index 00000000000..abc11d3b6f0 --- /dev/null +++ b/packages/dappkit/src/index.ts @@ -0,0 +1,112 @@ +import { + AccountAuthRequest, + DappKitRequestMeta, + DappKitRequestTypes, + DappKitResponseStatus, + parseDappkitResponseDepplink, + serializeDappKitRequestDeeplink, + SignTxRequest, + TxToSignParam, +} from '@celo/utils' +import { CeloTokenType, GoldToken, StableToken } from '@celo/walletkit' +import { Linking } from 'expo' +import Web3 from 'web3' +import { TransactionObject } from 'web3/eth/types' + +export { + AccountAuthRequest, + DappKitRequestMeta, + serializeDappKitRequestDeeplink, + SignTxRequest, +} from '@celo/utils/' + +export function listenToAccount(callback: (account: string) => void) { + return Linking.addEventListener('url', ({ url }: { url: string }) => { + try { + const dappKitResponse = parseDappkitResponseDepplink(url) + if ( + dappKitResponse.type === DappKitRequestTypes.ACCOUNT_ADDRESS && + dappKitResponse.status === DappKitResponseStatus.SUCCESS + ) { + callback(dappKitResponse.address) + } + } catch (error) {} + }) +} + +export function listenToSignedTxs(callback: (signedTxs: string[]) => void) { + return Linking.addEventListener('url', ({ url }: { url: string }) => { + try { + const dappKitResponse = parseDappkitResponseDepplink(url) + if ( + dappKitResponse.type === DappKitRequestTypes.SIGN_TX && + dappKitResponse.status === DappKitResponseStatus.SUCCESS + ) { + callback(dappKitResponse.rawTxs) + } + } catch (error) {} + }) +} + +export function requestAccountAddress(meta: DappKitRequestMeta) { + Linking.openURL(serializeDappKitRequestDeeplink(AccountAuthRequest(meta))) +} + +export enum GasCurrency { + cUSD = 'cUSD', + cGLD = 'cGLD', +} + +async function getGasCurrencyContract( + web3: Web3, + gasCurrency: GasCurrency +): Promise { + switch (gasCurrency) { + case GasCurrency.cUSD: + return StableToken(web3) + case GasCurrency.cGLD: + return GoldToken(web3) + default: + return StableToken(web3) + } +} + +export interface TxParams { + txId: string + tx: TransactionObject + from: string + to: string + gasCurrency: GasCurrency +} + +export async function requestTxSig( + web3: Web3, + txParams: TxParams[], + meta: DappKitRequestMeta +) { + const txs: TxToSignParam[] = await Promise.all( + txParams.map(async (txParam) => { + const gasCurrencyContract = await getGasCurrencyContract(web3, txParam.gasCurrency) + const estimatedTxParams = { + gasCurrency: gasCurrencyContract.options.address, + } + // @ts-ignore + const estimatedGas = await txParam.tx.estimateGas(estimatedTxParams) + + const nonce = await web3.eth.getTransactionCount(txParam.from) + return { + txData: txParam.tx.encodeABI(), + estimatedGas, + nonce, + gasCurrencyAddress: gasCurrencyContract._address, + ...txParam, + } + }) + ) + + // const url = Linking.makeUrl(returnPath) + + const request = SignTxRequest(txs, meta) + + Linking.openURL(serializeDappKitRequestDeeplink(request)) +} diff --git a/packages/dappkit/tsconfig.json b/packages/dappkit/tsconfig.json new file mode 100644 index 00000000000..fb4a8f39203 --- /dev/null +++ b/packages/dappkit/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "esModuleInterop": true, + "baseUrl": ".", + "declaration": true, + "jsx": "preserve", + "lib": ["dom", "es2015", "es2016"], + "module": "commonjs", + "moduleResolution": "node", + "noUnusedLocals": true, + "noUnusedParameters": true, + "removeComments": false, + "skipLibCheck": true, + "sourceMap": true, + "target": "es5", + "outDir": "lib", + "downlevelIteration": true + }, + "exclude": ["lib/", "node_modules"], + "include": ["src/*"] +} diff --git a/packages/docs/.gitignore b/packages/docs/.gitignore new file mode 100644 index 00000000000..d9608f8c803 --- /dev/null +++ b/packages/docs/.gitignore @@ -0,0 +1 @@ +_book/* \ No newline at end of file diff --git a/packages/docs/README.md b/packages/docs/README.md index 4221ed13378..81a0a9f7756 100644 --- a/packages/docs/README.md +++ b/packages/docs/README.md @@ -15,7 +15,7 @@ Celo’s purpose is to empower anyone with a smartphone anywhere in the world to The project aims to be a decentralized platform that is not controlled by any single entity, but instead developed, upgraded and operated by a broad community of individuals, organizations and partners. -Uniquely, Celo is oriented around providing the simplest possible experience for end users, who may have no familiarity with cryptocurrencies, and may be using low cost devices with limited connectivity. To achieve this, the project takes a full-stack approach, comprising both a protocol and applications that use that protocol. +Uniquely, Celo is oriented around providing the simplest possible experience for end users, who may have no familiarity with cryptocurrencies, and may be using low cost devices with limited connectivity. To achieve this, the project takes a full-stack approach, comprising of both a protocol and applications that use that protocol. The Celo protocol is an open, distributed cryptographic protocol that allows applications to make transactions with and perform computation on a family of cryptocurrencies, including ones pegged to ‘fiat’ currencies like the US Dollar. The [Celo Wallet](http://celo.org/build/wallet) app, the first of an ecosystem of applications, allows end users to manage accounts and make payments securely and simply by taking advantage of the innovations in the Celo protocol. @@ -25,7 +25,7 @@ Highlights include: Celo includes native support for multiple ERC20-like stable currencies pegged to ‘fiat’ currencies like the US dollar, to facilitate the use of Celo as a means of payment. -- **Accounts linked to Phone Numbers** +- **Accounts Linked to Phone Numbers** Celo maintains a secure decentralized mapping of phone numbers that allow wallet users to send and receive payments with their existing contacts simply and with confidence that the payment will reach the intended recipient. diff --git a/packages/docs/SUMMARY.md b/packages/docs/SUMMARY.md index d383e58cb96..b29f4bd5511 100644 --- a/packages/docs/SUMMARY.md +++ b/packages/docs/SUMMARY.md @@ -54,11 +54,12 @@ ## Command Line Interface - [Introduction](command-line-interface/introduction.md) -- [Exchange](command-line-interface/exchange.md) -- [Bonded Deposits](command-line-interface/bonded-deposits.md) -- [Validator Node](command-line-interface/validator-node.md) -- [Validator Groups](command-line-interface/validator-groups.md) +- [Config](command-line-interface/config.md) - [Account](command-line-interface/account.md) +- [Exchange](command-line-interface/exchange.md) +- [Bonded Deposits](command-line-interface/bonds.md) +- [Validator Node](command-line-interface/validator.md) +- [Validator Groups](command-line-interface/validatorgroup.md) ## Community diff --git a/packages/docs/book.json b/packages/docs/book.json new file mode 100644 index 00000000000..14c2c453462 --- /dev/null +++ b/packages/docs/book.json @@ -0,0 +1,3 @@ +{ + "plugins": ["hints", "page-ref"] +} diff --git a/packages/docs/celo-codebase/protocol/transactions/tx-comment-encyption.md b/packages/docs/celo-codebase/protocol/transactions/tx-comment-encyption.md index 4c7b838fd28..6d005951b34 100644 --- a/packages/docs/celo-codebase/protocol/transactions/tx-comment-encyption.md +++ b/packages/docs/celo-codebase/protocol/transactions/tx-comment-encyption.md @@ -2,7 +2,7 @@ ### **Overview** -As part of Celo’s identity protocol, a public encryption key is stored along with a user’s address in the `Attestations` contract. Both the address keypair and the encryption key pairkeypair are derived from the backup phrase. When sending a transaction the encryption key of theof the recipient is retrieved when getting his or her address. The comment is then encrypted using a 128-bit128 bit hybrid encryption scheme \(ECDH on secp256k1 with AES-128-CTR\). This system ensures that comments can only be read by the sending and receiving parties and that messages will be recovered when restoring a wallet from its backup phrase. +As part of Celo’s identity protocol, a public encryption key is stored along with a user’s address in the `Attestations` contract. Both the address key pair and the encryption key pair are derived from the backup phrase. When sending a transaction the encryption key of the recipient is retrieved when getting his or her address. The comment is then encrypted using a 128 bit hybrid encryption scheme \(ECDH on secp256k1 with AES-128-CTR\). This system ensures that comments can only be read by the sending and receiving parties and that messages will be recovered when restoring a wallet from its backup phrase. ### **Comment Encryption Technical Details** diff --git a/packages/docs/celo-codebase/style-guide/typescript.md b/packages/docs/celo-codebase/style-guide/typescript.md new file mode 100644 index 00000000000..c556e809130 --- /dev/null +++ b/packages/docs/celo-codebase/style-guide/typescript.md @@ -0,0 +1,64 @@ +# TypeScript Style Guide + +### Function parameters + +_Vanilla parameters_ are preferred over Object Destructuring. + +Example of Vanilla parameters: + +``` +export const tokenFetchFactory = ( + actionName, + contractGetter, + actionCreator, + tag, +) +``` + +Example of Object Destructuring: + +``` +export const tokenFetchFactory = ({ + actionName, + contractGetter, + actionCreator, + tag, +}: TokenFetchFactory) +``` + +This is for simplicity, with fewer lines and some evidence shows it's [faster](https://codeburst.io/es6s-function-destructuring-assignment-is-not-free-lunch-19caacc18137). + +### Function definitions: Arrow functions vs Vanilla functions + +In the root scope, _Vanilla functions_ are preferred over Arrow functions. + +This is because it's consistent with generator functions, simpler to understand, easier to debug, supports recursion and functions are hoisted, meaning no concern about definition order. + +### Class methods: anonymous functions vs native methods + +Anonymous functions are the preferred way. As shown in the example: + +``` +class myClass { + myMethod = () => {} +} +``` + +### Exporting variables only for testing + +When a variable is exported only for the propose of getting accessed by tests, a low dash should be added before the name. + +For example instead of doing this: + +``` +export myFunction{...} +``` + +This is the preferred way: + +``` +const myFunction{...} +export _myFunction = myFunction +``` + +In case it's necessary, a decorator could wrap the exported function to allow it only to be accessed during testing. diff --git a/packages/docs/command-line-interface/account.md b/packages/docs/command-line-interface/account.md index 9d37196c4de..7ecfc883e14 100644 --- a/packages/docs/command-line-interface/account.md +++ b/packages/docs/command-line-interface/account.md @@ -1,65 +1,91 @@ --- -description: The account module lets you interact with and monitor your Celo account. +description: Manage your account, send and receive Celo Gold and Celo Dollars --- -# Account - ## Commands ### Balance -View token balances given account address +View Celo Dollar and Gold balances given account address +``` USAGE + $ celocli account:balance ACCOUNT + +EXAMPLE + balance 0x5409ed021d9299bf6814279a6a1411a7e866a631 +``` -`$ celocli account:balance ACCOUNT` +_See code: [packages/cli/src/commands/account/balance.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/balance.ts)_ ### New Creates a new account +``` USAGE + $ celocli account:new -`$ celocli account:new` +EXAMPLE + new +``` -### Transferdollar +_See code: [packages/cli/src/commands/account/new.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/new.ts)_ -USAGE +### Transferdollar -`$ celocli account:transferdollar` +Transfer Celo Dollars -Options +``` +USAGE + $ celocli account:transferdollar -`--amountInWei=amountInWei` \(required\) Amount to transfer \(in wei\) +OPTIONS + --amountInWei=amountInWei (required) Amount to transfer (in wei) + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the sender + --to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the receiver -`--from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d` \(required\) Address of the sender +EXAMPLE + transferdollar --from 0xa0Af2E71cECc248f4a7fD606F203467B500Dd53B --to 0x5409ed021d9299bf6814279a6a1411a7e866a631 + --amountInWei 1 +``` -`--to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d` \(required\) Address of the receiver +_See code: [packages/cli/src/commands/account/transferdollar.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/transferdollar.ts)_ ### Transfergold -USAGE - -`$ celocli account:transfergold` +Transfer gold -Options +``` +USAGE + $ celocli account:transfergold -`--amountInWei=amountInWei` \(required\) Amount to transfer \(in wei\) +OPTIONS + --amountInWei=amountInWei (required) Amount to transfer (in wei) + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the sender + --to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the receiver -`--from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d` \(required\) Address of the sender +EXAMPLE + transfergold --from 0xa0Af2E71cECc248f4a7fD606F203467B500Dd53B --to 0x5409ed021d9299bf6814279a6a1411a7e866a631 + --amountInWei 1 +``` -`--to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d` \(required\) Address of the receiver +_See code: [packages/cli/src/commands/account/transfergold.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/transfergold.ts)_ ### Unlock -Unlock an account address to send transactions +Unlock an account address to send transactions or validate blocks +``` USAGE + $ celocli account:unlock -`$ celocli account:unlock` - -Options +OPTIONS + --account=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + --password=password (required) -`--account=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d` \(required\) Account Address +EXAMPLE + unlock --account 0x5409ed021d9299bf6814279a6a1411a7e866a631 --password 1234 +``` -`--password=password` \(required\) +_See code: [packages/cli/src/commands/account/unlock.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/unlock.ts)_ diff --git a/packages/docs/command-line-interface/bonded-deposits.md b/packages/docs/command-line-interface/bonded-deposits.md deleted file mode 100644 index e54da37b307..00000000000 --- a/packages/docs/command-line-interface/bonded-deposits.md +++ /dev/null @@ -1,144 +0,0 @@ ---- -description: >- - Celo Gold holders can earn rewards by bonding Celo Gold pending they are also - participating in validator elections and governance proposals. The bonds - module provides tools to manage bonded deposits. ---- - -# Bonded Deposits - -## **Commands** - -### **Register** - -Celo Gold holders that want to start earning rewards should use the register command to create an account. - -USAGE - -`$ celocli bonds:register` - -OPTIONS - -`--from
` \(required\) account address to sign transaction with - -EXAMPLE - -`celocli bonds:register --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d` - -### **Deposit** - -After account registration, account holders must put up a staking value and define a notice period using the deposit command. This command takes a Celo Gold amount and notice period as parameters. Upon transaction confirmation, a corresponding bonded deposit will be created on-chain. - -USAGE - -`$ celocli bonds:deposit` - -OPTIONS - -`--goldAmount ` \(required\) unit amount of gold token \(cGLD\) - -`--noticePeriod ` \(required\) duration \(seconds\) from notice to withdrawable; doubles as ID of a bonded deposit - -`--from
` \(required\) account address to sign transaction with - -EXAMPLE - -`celocli bonds:deposit --goldAmount=1000000000000000000 --noticePeriod=8640 --from= 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95` - -### **Rewards** - -To manage rewards accrued by bonded deposits, account holders can use the rewards command. Rewards can be delegated to an account managed by an ephemeral PPK pair with the delegate flag. - -USAGE - -`$ celocli bonds:rewards` - -OPTIONS - -`-d, --delegate
` Delegate rewards to provided account - -`--from
` \(required\) account address to sign transaction with - -EXAMPLES - -`celocli bonds:rewards --delegate 0x56e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95` - -### **Notify** - -Account holders who want to revoke their bonded deposits should use the notify command to convert a bonded deposit to a notified deposit. Since bonded deposits are keyed by notice period, this command takes a notice period to identify the bond for which to withdraw the Celo Gold amount. The remainder of the Celo Gold remains bonded with the same notice period. - -USAGE - -`$ celocli bonds:notify` - -OPTIONS - -`--goldAmount ` \(required\) unit amount of gold token \(cGLD\) - -`--noticePeriod ` \(required\) duration \(seconds\) from notice to withdrawable; doubles as ID of a bonded deposit - -`--from
` \(required\) account address to sign transaction with - -EXAMPLE - -`celocli bonds:notify --goldAmount=500 --noticePeriod=3600 --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95` - -### **Withdraw** - -After a notified deposits' notice period has elapsed, Celo Gold holders can use the withdraw command to receive the Celo Gold that was scheduled for withdrawal. This command takes an availability time as a parameter, which equates to \(time of notice + notice period\). These times should be retrieved using the list/show commands. - -USAGE - -`$ celocli bonds:withdraw AVAILABILITYTIME` - -ARGUMENTS - -`AVAILABILITYTIME ` unix timestamp at which withdrawable; doubles as ID of a notified deposit - -OPTIONS - -`--from
` \(required\) account address to sign transaction with - -EXAMPLE - -`celocli bonds:withdraw 1562206887 --from=0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95` - -### **List** - -To view information about all bonded and notified deposits for an account, users can use the list command. This command takes an account address as a parameter. - -USAGE - -`$ celocli bonds:list ACCOUNT` - -ARGUMENTS - -`ACCOUNT
` registered account address to query deposits for - -EXAMPLE - -`celocli bonds:list 0x5409ed021d9299bf6814279a6a1411a7e866a631` - -### **Show** - -To view specific information about an account's individual bonded or notified deposits, Celo Gold holders can use the show command. This command takes a notice period or availability time and an account address as parameters. - -USAGE - -`$ celocli bonds:show ACCOUNT` - -ARGUMENTS - -`ACCOUNT
` registered account address to query deposits for - -OPTIONS - -`--noticePeriod ` duration \(seconds\) from notice to withdrawable; doubles as ID of a bonded deposit - -`--availabilityTime ` unix timestamp at which withdrawable; doubles as ID of a notified deposit - -EXAMPLES - -`celocli bonds:show 0x5409ed021d9299bf6814279a6a1411a7e866a631 --noticePeriod=3600` - -`celocli bonds:show 0x5409ed021d9299bf6814279a6a1411a7e866a631 --availabilityTime=1562206887` diff --git a/packages/docs/command-line-interface/bonds.md b/packages/docs/command-line-interface/bonds.md new file mode 100644 index 00000000000..29dd3c0f708 --- /dev/null +++ b/packages/docs/command-line-interface/bonds.md @@ -0,0 +1,139 @@ +--- +description: Manage bonded deposits to participate in governance and earn rewards +--- + +## Commands + +### Deposit + +Create a bonded deposit given notice period and gold amount + +``` +USAGE + $ celocli bonds:deposit + +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 bonded + deposit; + +EXAMPLE + deposit --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --noticePeriod 8640 --goldAmount 1000000000000000000 +``` + +_See code: [packages/cli/src/commands/bonds/deposit.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/bonds/deposit.ts)_ + +### List + +View information about all of the account's deposits + +``` +USAGE + $ celocli bonds:list ACCOUNT + +EXAMPLE + list 0x5409ed021d9299bf6814279a6a1411a7e866a631 +``` + +_See code: [packages/cli/src/commands/bonds/list.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/bonds/list.ts)_ + +### Notify + +Notify a bonded deposit given notice period and gold amount + +``` +USAGE + $ celocli bonds:notify + +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 bonded deposit; + +EXAMPLE + notify --noticePeriod=3600 --goldAmount=500 +``` + +_See code: [packages/cli/src/commands/bonds/notify.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/bonds/notify.ts)_ + +### Register + +Register an account for bonded deposit eligibility + +``` +USAGE + $ celocli bonds:register + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + +EXAMPLE + register +``` + +_See code: [packages/cli/src/commands/bonds/register.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/bonds/register.ts)_ + +### Rewards + +Manage rewards for bonded deposit account + +``` +USAGE + $ celocli bonds:rewards + +OPTIONS + -d, --delegate=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d Delegate rewards to provided account + -r, --redeem Redeem accrued rewards from bonded deposits + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + +EXAMPLES + rewards --redeem + rewards --delegate=0x56e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 +``` + +_See code: [packages/cli/src/commands/bonds/rewards.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/bonds/rewards.ts)_ + +### Show + +View bonded gold and corresponding account weight of a deposit given ID + +``` +USAGE + $ celocli bonds:show ACCOUNT + +OPTIONS + --availabilityTime=availabilityTime unix timestamp at which withdrawable; doubles as ID of a notified deposit + + --noticePeriod=noticePeriod duration (seconds) from notice to withdrawable; doubles as ID of a bonded + deposit; + +EXAMPLES + show 0x5409ed021d9299bf6814279a6a1411a7e866a631 --noticePeriod=3600 + show 0x5409ed021d9299bf6814279a6a1411a7e866a631 --availabilityTime=1562206887 +``` + +_See code: [packages/cli/src/commands/bonds/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/bonds/show.ts)_ + +### Withdraw + +Withdraw notified deposit given availability time + +``` +USAGE + $ celocli bonds:withdraw AVAILABILITYTIME + +ARGUMENTS + AVAILABILITYTIME unix timestamp at which withdrawable; doubles as ID of a notified deposit + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + +EXAMPLE + withdraw 3600 +``` + +_See code: [packages/cli/src/commands/bonds/withdraw.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/bonds/withdraw.ts)_ diff --git a/packages/docs/command-line-interface/config.md b/packages/docs/command-line-interface/config.md new file mode 100644 index 00000000000..f313c741e4e --- /dev/null +++ b/packages/docs/command-line-interface/config.md @@ -0,0 +1,30 @@ +--- +description: Configure CLI options which persist across commands +--- + +## Commands + +### Get + +Output network node configuration + +``` +USAGE + $ celocli config:get +``` + +_See code: [packages/cli/src/commands/config/get.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/config/get.ts)_ + +### Set + +Configure running node information for propogating transactions to network + +``` +USAGE + $ celocli config:set + +OPTIONS + --node=node (required) [default: ws://localhost:8546] Node URL +``` + +_See code: [packages/cli/src/commands/config/set.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/config/set.ts)_ diff --git a/packages/docs/command-line-interface/exchange.md b/packages/docs/command-line-interface/exchange.md index 2e145dcd141..16fc35f9776 100644 --- a/packages/docs/command-line-interface/exchange.md +++ b/packages/docs/command-line-interface/exchange.md @@ -1,36 +1,60 @@ --- -description: >- - The exchange module allows you to interact with the Exchange - the smart - contract that ensures Celo Dollar’s stability and provides an always-liquid - Celo Dollar - Celo Gold decentralized exchange. +description: Commands for interacting with the Exchange --- -# Exchange - ## Commands ### List -List information about tokens on the exchange \(all amounts in wei\). +List information about tokens on the exchange (all amounts in wei) +``` USAGE + $ celocli exchange:list -`$ celocli exchange:list` +OPTIONS + --amount=amount [default: 1000000000000000000] Amount of sellToken (in wei) to report rates for -Options +EXAMPLE + list +``` -`--amount=amount` amount of sellToken to report rates for \(defaults to 1000000000000000000\) +_See code: [packages/cli/src/commands/exchange/list.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/exchange/list.ts)_ -### Selldollar, sellgold +### Selldollar -Commands for trading on the exchange. +Sell Celo dollars for Celo gold on the exchange +``` USAGE + $ celocli exchange:selldollar SELLAMOUNT MINBUYAMOUNT FROM + +ARGUMENTS + SELLAMOUNT the amount of sellToken (in wei) to sell + MINBUYAMOUNT the minimum amount of buyToken (in wei) expected + FROM + +EXAMPLE + selldollar 100 300 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d +``` -`$ celocli exchange:selldollar SELLAMOUNT MINBUYAMOUNT FROM` +_See code: [packages/cli/src/commands/exchange/selldollar.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/exchange/selldollar.ts)_ + +### Sellgold + +Sell Celo gold for Celo dollars on the exchange + +``` +USAGE + $ celocli exchange:sellgold SELLAMOUNT MINBUYAMOUNT FROM -`$ celocli exchange:sellgold SELLAMOUNT MINBUYAMOUNT FROM` +ARGUMENTS + SELLAMOUNT the amount of sellToken (in wei) to sell + MINBUYAMOUNT the minimum amount of buyToken (in wei) expected + FROM EXAMPLE + sellgold 100 300 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d +``` -`celocli exchange:selldollar 100 300 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d` +_See code: [packages/cli/src/commands/exchange/sellgold.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/exchange/sellgold.ts)_ diff --git a/packages/docs/command-line-interface/help.md b/packages/docs/command-line-interface/help.md new file mode 100644 index 00000000000..3f77f6244e5 --- /dev/null +++ b/packages/docs/command-line-interface/help.md @@ -0,0 +1,22 @@ +--- +description: display help for celocli +--- + +## Commands + +### Help + +display help for celocli + +``` +USAGE + $ celocli help [COMMAND] + +ARGUMENTS + COMMAND command to show help for + +OPTIONS + --all see all commands in CLI +``` + +_See code: [packages/cli/@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v2.2.0/src/commands/help.ts)_ diff --git a/packages/docs/command-line-interface/introduction.md b/packages/docs/command-line-interface/introduction.md index ffaa6bbeee7..25c74201f36 100644 --- a/packages/docs/command-line-interface/introduction.md +++ b/packages/docs/command-line-interface/introduction.md @@ -8,9 +8,19 @@ description: >- ## Getting Started +### NPM Package + +The Celo CLI is published as a node module on NPM. Assuming you have [npm installed](https://www.npmjs.com/get-npm), you can install the Celo CLI using the following command: + +`$ npm install -g @celo/celocli` + +{% hint style="info" %} +We are currently deploying the CLI with only Node v10.x LTS support. If you are running a different version of Node, consider using [NVM](https://github.com/nvm-sh/nvm#installation-and-update) to manage your node versions. e.g. with: `nvm install 10 && nvm use 10` +{% endhint %} + ### Docker Image -A docker image that runs the Celo Blockchain client in full sync mode which includes the Celo CLI is available for version pinning. +Additionally, if don't have NPM or are having trouble installing the Celo CLI with your version of node, you can use a docker image that runs the Celo Blockchain client in full sync mode which includes the Celo CLI. `$ docker pull us.gcr.io/celo-testnet/celocli:master` @@ -30,16 +40,6 @@ Make sure to kill the container when you are done. `$ docker kill celo_cli_container` -### NPM Package - -The Celo CLI is also published as a node module on NPM. Assuming you have [npm installed](https://www.npmjs.com/get-npm), you can install the Celo CLI using the following command: - -`$ npm install -g @celo/celocli` - -{% hint style="info" %} -We are currently deploying the CLI with only Node v10.x LTS support. If you are running a different version of Node, consider using [NVM](https://github.com/nvm-sh/nvm#installation-and-update) to manage your node versions. e.g. with: `nvm install 10 && nvm use 10` -{% endhint %} - ### Overview The tool is broken down into modules and commands with the following pattern: diff --git a/packages/docs/command-line-interface/node.md b/packages/docs/command-line-interface/node.md new file mode 100644 index 00000000000..f4c73dc394b --- /dev/null +++ b/packages/docs/command-line-interface/node.md @@ -0,0 +1,16 @@ +--- +description: Manage your full node +--- + +## Commands + +### Accounts + +List node accounts + +``` +USAGE + $ celocli node:accounts +``` + +_See code: [packages/cli/src/commands/node/accounts.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/node/accounts.ts)_ diff --git a/packages/docs/command-line-interface/validator-groups.md b/packages/docs/command-line-interface/validator-groups.md deleted file mode 100644 index 90b67ec8991..00000000000 --- a/packages/docs/command-line-interface/validator-groups.md +++ /dev/null @@ -1,127 +0,0 @@ -# Validator Groups - -The **validatorgroup** module provides information about validator groups and tools to manage and vote for them. - -### **List** - -To display the validator groups that exist in the Celo ecosystem use the **list** command. - -USAGE - -`$ celocli validatorgroup:list` - -OPTIONS - -`-h, --help` show CLI help - -`-l, --logLevel=logLevel` - -### **Register** - -To register an account as a new validator group use the **register** command. A validator group id, name, minimum notice period of 60 days for the bonded deposit, and a url are required as parameters. In addition, prior to registering as a validator group an account must have a minimum bonded deposit of one Celo Gold. - -USAGE - -`$ celocli validatorgroup:register` - -OPTIONS - -`-h, --help` show CLI help - -`-l, --logLevel=logLevel` - -`--from=ADDRESS` \(required\) Address for the Validator Group - -`--id=id` \(required\) - -`--name=name` \(required\) - -`--noticePeriod=noticePeriod` \(required\) Notice Period for the Bonded deposit to use - -`--url=url` \(required\) - -EXAMPLE - -`celocli validatorgroup:register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --id myID --name myNAme --noticePeriod 5184000 --url "http://vgroup.com"` - -### **Member** - -For validator group owners that want to manage the list of validators in their group should use the **member** command. They can either accept an incoming affiliation with **`--accept`** flag or remove a validator from their list with the **`--remove`** flag. This command takes a valiator’s address as parameter. - -USAGE - -`$ celocli validatorgroup:member VALIDATORADDRESS` - -ARGUMENTS - -`VALIDATORADDRESS` Validator's address - -OPTIONS - -`-h, --help` show CLI help - -`-l, --logLevel=logLevel` - -`--accept` Accept a validator whose affiliation is already set to the group - -`--from=ADDRESS` \(required\) ValidatorGroup's address - -`--remove` Remove a validator from the members list - -EXAMPLES - -`celocli validatorgroup:member --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d --accept 0x97f7333c51897469e8d98e7af8653aab468050a3` - -`celocli validatorgroup:member --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d --remove 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95` - -### **Show** - -To see information about validators in a group use the **show** command. The following information for each validator is shown: address, id, name, url, and member list. This command takes a validator group account address as a parameter. - -USAGE - -`$ celocli validatorgroup:show GROUPADDRESS` - -ARGUMENTS - -`GROUPADDRESS` ValidatorGroup's address - -OPTIONS - -`-h, --help` show CLI help - -`-l, --logLevel=logLevel` - -EXAMPLE - -`celocli validatorgroup:show 0x97f7333c51897469E8D98E7af8653aAb468050a3` - -### **Vote** - -Celo Gold holders can vote a validation group using the **vote** command. An Celo Gold holder can vote for at most one group per epoch. This command takes a voter and validator group addresses as parameters. - -USAGE - -`$ celocli validatorgroup:vote` - -OPTIONS - -`-h, --help` show CLI help - -`-l, --logLevel=logLevel` - -`--current` Show voter's current vote - -`--for=ADDRESS` Set vote for ValidatorGroup's address - -`--from=ADDRESS` \(required\) Voter's address - -`--revoke` Revoke voter's current vote - -EXAMPLES - -`celocli validatorgroup:vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --for 0x932fee04521f5fcb21949041bf161917da3f588b` - -`celocli validatorgroup:vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --revoke` - -`celocli validatorgroup:vote --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --current` diff --git a/packages/docs/command-line-interface/validator-node.md b/packages/docs/command-line-interface/validator-node.md deleted file mode 100644 index b3e42c3d6ab..00000000000 --- a/packages/docs/command-line-interface/validator-node.md +++ /dev/null @@ -1,87 +0,0 @@ -# Validator Node - -The **validator** module provides information about validators and tools to manage them. - -### **List** - -To display the validators that exist in the Celo ecosystem use the **list** command. - -USAGE - -`$ celocli validator:list` - -OPTIONS - -`-h, --help` show CLI help - -`-l, --logLevel=logLevel` - -### **Register** - -To register an account as a new validator use the **register** command. A validator id, name, minimum notice period of 60 days for the bonded deposit, a public key corresponding to the one used for participating in consensus, and a url are required as parameters. In addition, prior to registering as a validator group an account must have a minimum bonded deposit of one Celo Gold. - -USAGE - -`$ celocli validator:register` - -OPTIONS - -`-h, --help show CLI help` - -`-l, --logLevel=logLevel` - -`--from=ADDRESS` \(required\) Address for the Validator - -`--id=id` \(required\) - -`--name=name` \(required\) - -`--noticePeriod=noticePeriod` \(required\) Notice Period for the Bonded deposit to use - -`--publicKey=0x` \(required\) Public Key - -`--url=url` \(required\) - -### **Show** - -To see information about a validator use the **show** command. This command takes a validator address as a parameter. The following information is displayed: address, id, name, public key used for consensus, and url. - -USAGE - -`$ celocli validator:show VALIDATORADDRESS` - -ARGUMENTS - -`VALIDATORADDRESS` Validator's address - -OPTIONS - -`-h, --help` show CLI help - -`-l, --logLevel=logLevel` - -### **Affiliation** - -A validator must join a validator group to be eligible to participate in consensus. Validators can request to join a validator group with the **affiliation** command. Group membership is confirmed once the validator group owner accepts the affiliation request. This command takes a validator group address and validator address as parameters. - -USAGE - -`$ celocli validator:affiliation` - -OPTIONS - -`-h, --help` show CLI help - -`-l, --logLevel=logLevel` - -`--from=ADDRESS` \(required\) Validator's address - -`--set=ADDRESS` set affiliation to given address - -`--unset` clear affiliation field - -EXAMPLES - -`celocli validator:affiliation --set 0x97f7333c51897469e8d98e7af8653aab468050a3 --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95` - -`celocli validator:affiliation --unset --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95` diff --git a/packages/docs/command-line-interface/validator.md b/packages/docs/command-line-interface/validator.md new file mode 100644 index 00000000000..d42c4d4e561 --- /dev/null +++ b/packages/docs/command-line-interface/validator.md @@ -0,0 +1,83 @@ +--- +description: View validator information and register your own +--- + +## Commands + +### Affiliation + +Manage affiliation to a ValidatorGroup + +``` +USAGE + $ celocli validator:affiliation + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Validator's address + --set=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d set affiliation to given address + --unset clear affiliation field + +EXAMPLES + affiliation --set 0x97f7333c51897469e8d98e7af8653aab468050a3 --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 + affiliation --unset --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 +``` + +_See code: [packages/cli/src/commands/validator/affiliation.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/affiliation.ts)_ + +### List + +List existing Validators + +``` +USAGE + $ celocli validator:list + +EXAMPLE + list +``` + +_See code: [packages/cli/src/commands/validator/list.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/list.ts)_ + +### Register + +Register a new Validator + +``` +USAGE + $ celocli validator:register + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address for the Validator + --id=id (required) + --name=name (required) + --noticePeriod=noticePeriod (required) Notice Period for the Bonded deposit to use + --publicKey=0x (required) Public Key + --url=url (required) + +EXAMPLE + register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --id myID --name myNAme --noticePeriod 5184000 --url + "http://validator.com" --publicKey + 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf + 997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d + 785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d + 96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00 +``` + +_See code: [packages/cli/src/commands/validator/register.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/register.ts)_ + +### Show + +Show information about an existing Validator + +``` +USAGE + $ celocli validator:show VALIDATORADDRESS + +ARGUMENTS + VALIDATORADDRESS Validator's address + +EXAMPLE + show 0x97f7333c51897469E8D98E7af8653aAb468050a3 +``` + +_See code: [packages/cli/src/commands/validator/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/show.ts)_ diff --git a/packages/docs/command-line-interface/validatorgroup.md b/packages/docs/command-line-interface/validatorgroup.md new file mode 100644 index 00000000000..47670a39cbb --- /dev/null +++ b/packages/docs/command-line-interface/validatorgroup.md @@ -0,0 +1,103 @@ +--- +description: View validator group information and cast votes +--- + +## Commands + +### List + +List existing Validator Groups + +``` +USAGE + $ celocli validatorgroup:list + +EXAMPLE + list +``` + +_See code: [packages/cli/src/commands/validatorgroup/list.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validatorgroup/list.ts)_ + +### Member + +Register a new Validator Group + +``` +USAGE + $ celocli validatorgroup:member VALIDATORADDRESS + +ARGUMENTS + VALIDATORADDRESS Validator's address + +OPTIONS + --accept Accept a validator whose affiliation is already set to the group + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) ValidatorGroup's address + --remove Remove a validator from the members list + +EXAMPLES + member --accept 0x97f7333c51897469e8d98e7af8653aab468050a3 + member --remove 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 +``` + +_See code: [packages/cli/src/commands/validatorgroup/member.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validatorgroup/member.ts)_ + +### Register + +Register a new Validator Group + +``` +USAGE + $ celocli validatorgroup:register + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address for the Validator Group + --id=id (required) + --name=name (required) + --noticePeriod=noticePeriod (required) Notice Period for the Bonded deposit to use + --url=url (required) + +EXAMPLE + register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --id myID --name myName --noticePeriod 5184000 --url + "http://vgroup.com" +``` + +_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)_ + +### Show + +Show information about an existing Validator Group + +``` +USAGE + $ celocli validatorgroup:show GROUPADDRESS + +ARGUMENTS + GROUPADDRESS ValidatorGroup's address + +EXAMPLE + show 0x97f7333c51897469E8D98E7af8653aAb468050a3 +``` + +_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 b59558b0263..118e6bbd9ec 100644 --- a/packages/docs/getting-started/running-a-full-node.md +++ b/packages/docs/getting-started/running-a-full-node.md @@ -62,7 +62,7 @@ A bootnode's purpose is to help nodes find other nodes in the network. This comm **Step 6: Start the full node** 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 44781 --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 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 `` 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 3b72ef72e18..e2b2fc2d9b5 100644 --- a/packages/docs/getting-started/running-a-validator.md +++ b/packages/docs/getting-started/running-a-validator.md @@ -50,6 +50,15 @@ $ export CELO_VALIDATOR_GROUP_ADDRESS= $ export CELO_VALIDATOR_ADDRESS= ``` +In order to register the validator later on, generate a "proof of possession" - a signature proving you know your validator's BLS private key. Run this command: +`` $ docker run -v `pwd`:/root/.celo -it us.gcr.io/celo-testnet/celo-node:alfajores account proof-of-possession $CELO_VALIDATOR_ADDRESS `` + +It will prompt you for the passphrase you've chosen for the validator account. Let's save the resulting proof-of-possession to an environment variable: + +``` +$ export CELO_VALIDATOR_POP= +``` + ### Deploy the validator node Initialize the docker container, building from an image for the network and initializing Celo with the genesis block: @@ -66,7 +75,7 @@ In order to allow the node to sync with the network, give it the address for the 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 44781 --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 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 `` {% 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. @@ -74,7 +83,7 @@ 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 rig an election. -The `networkid` parameter value of `44781` indicates we are connecting the Alfajores Testnet. +The `networkid` parameter value of `44782` indicates we are connecting the Alfajores Testnet. ### Set up deposits @@ -109,10 +118,10 @@ Register your validator group: Register your validator: -`` $ celocli validator:register --id --name --url --from $CELO_VALIDATOR_ADDRESS --noticePeriod 5184000 --publicKey 0x`openssl rand -hex 64` `` +`` $ celocli validator:register --id --name --url --from $CELO_VALIDATOR_ADDRESS --noticePeriod 5184000 --publicKey 0x`openssl rand -hex 64`$CELO_VALIDATOR_POP `` {% hint style="info" %} -**Roadmap**: Note that the “publicKey” field is currently ignored, and thus can be set to any 128 character hex value. This will change when the Celo protocol moves to BLS signatures for consensus. +**Roadmap**: Note that the “publicKey” first part of the public key field is currently ignored, and thus can be set to any 128 character hex value. The rest is used for the BLS public key and proof-of-possession. {% endhint %} Affiliate your validator with your validator group. Note that you will not be a member of this group until the validator group accepts you: diff --git a/packages/docs/overview.md b/packages/docs/overview.md index dc2ed80e6ba..8cc183fa889 100644 --- a/packages/docs/overview.md +++ b/packages/docs/overview.md @@ -10,7 +10,7 @@ This page provides some background on blockchain technology and explores the Cel A **blockchain** or **cryptographic network** is a broad term used to describe a database maintained by a distributed set of computers that do not share a trust relationship or common ownership. This arrangement is referred to as **decentralized**. The content of a blockchain's database, or **ledger**, is authenticated using cryptographic techniques, preventing its contents being added to, edited or removed except according to a protocol operated by the network as a whole. -The code of the Celo Blockchain has shared ancestry with [Ethereum](https://www.ethereum.org), blockchain software for building general-purpose decentralized applications. Celo differs from Ethereum in several important areas as described in the following sections. However it inherits a number of key concepts. +The code of the Celo Blockchain has shared ancestry with [Ethereum](https://www.ethereum.org), blockchain software for building general-purpose decentralized applications. Celo differs from Ethereum in several important areas as described in the following sections. However, it inherits a number of key concepts. Ethereum applications are built using **smart contracts**. Smart contracts are programs written in languages like [Solidity](https://solidity.readthedocs.io/en/v0.5.10/) that produce bytecode for the **Ethereum Virtual Machine** or **EVM**, a runtime environment. Programs encoded in smart contracts receive messages and manipulate the blockchain ledger and are termed **on-chain**. @@ -34,7 +34,7 @@ The Celo stack is structured into the following logical layers: ![](https://storage.googleapis.com/celo-website/docs/full-stack-diagram.jpg) -- **Celo Blockchain**: An open cryptographic protocol that allows applications to make transactions with and run smart contracts in a secure and decentralized fashion. The Celo Blockchain has shared ancestry with [Ethereum](https://www.ethereum.org), and maintains full EVM compatibility for smart contracts. However it uses a [Byzantine Fault Tolerant](http://pmg.csail.mit.edu/papers/osdi99.pdf) \(BFT\) consensus mechanism rather than Proof of Work, and has different block format, transaction format, client synchronization protocols, and gas payment and pricing mechanisms. The network’s native asset is Celo Gold, which is also an ERC-20 token. +- **Celo Blockchain**: An open cryptographic protocol that allows applications to make transactions with and run smart contracts in a secure and decentralized fashion. The Celo Blockchain code has shared ancestry with [Ethereum](https://www.ethereum.org), and maintains full EVM compatibility for smart contracts. However it uses a [Byzantine Fault Tolerant](http://pmg.csail.mit.edu/papers/osdi99.pdf) \(BFT\) consensus mechanism rather than Proof of Work, and has different block format, transaction format, client synchronization protocols, and gas payment and pricing mechanisms. The network’s native asset is Celo Gold, which is also an ERC-20 token. - **Celo Core Contracts**: A set of smart contracts running on the Celo Blockchain that comprise much of the logic of the platform features including ERC-20 stable currencies, identity attestations, Proof of Stake and governance. These smart contracts are upgradeable and managed by the decentralized governance process. - **Applications:** Applications for end users built on the Celo platform. The Celo Wallet app, the first of an ecosystem of applications, allows end users to manage accounts and make payments securely and simply by taking advantage of the innovations in the Celo protocol. Applications take the form of external mobile or backend software: they interact with the Celo Blockchain to issue transactions and invoke code that forms the Celo Core Contracts’ API. Third parties can also deploy custom smart contracts that their own applications can invoke, which in turn can leverage Celo Core Contracts. Applications may use centralized cloud services to provide some of their functionality: in the case of the Celo Wallet, push notifications and a transaction activity feed. diff --git a/packages/docs/package.json b/packages/docs/package.json new file mode 100644 index 00000000000..ab8a9be9c34 --- /dev/null +++ b/packages/docs/package.json @@ -0,0 +1,23 @@ +{ + "name": "docs", + "version": "1.0.0", + "description": "Package for Celo Documentation", + "dependencies": { + "gitbook-plugin-hints": "^1.0.2", + "gitbook-plugin-page-ref": "^0.0.6", + "gitbook-plugin-highlight": "^2.0.3", + "gitbook-plugin-search": "^2.2.1", + "gitbook-plugin-lunr": "^1.2.0", + "gitbook-plugin-sharing": "^1.0.2", + "gitbook-plugin-fontsettings": "^2.0.0", + "gitbook-plugin-theme-default": "^1.0.7" + }, + "devDependencies": { + "gitbook-cli": "^2.3.2" + }, + "scripts": { + "build": "gitbook install && gitbook build" + }, + "author": "", + "license": "ISC" +} diff --git a/packages/faucet/.firebaserc b/packages/faucet/.firebaserc index d0e6db4206e..c38f1be09ee 100644 --- a/packages/faucet/.firebaserc +++ b/packages/faucet/.firebaserc @@ -9,6 +9,13 @@ "celo-faucet" ] } + }, + "celo-faucet-staging": { + "database": { + "faucet": [ + "celo-faucet-staging" + ] + } } } } \ No newline at end of file diff --git a/packages/faucet/README.md b/packages/faucet/README.md index 9021c429a1e..34b0ecfe0cf 100644 --- a/packages/faucet/README.md +++ b/packages/faucet/README.md @@ -4,6 +4,7 @@ Faucet firebase function requires a few configuration variables to work: - nodeUrl: The url for the node the faucet server will use to send transactions - stableTokenAddress: The StableToken contract's address +- goldTokenAddress: The GoldToken contract's address - faucetGoldAmount: The amount of gold to faucet on each request - faucetDollarAmount: The amount of dollars to faucet on each request @@ -27,22 +28,22 @@ yarn cli config:set --net alfajores --goldAmount 5000000000000000000 --dollarAmo You can verify with `yarn cli config:get --net alfajores` -### Setting StableToken Address +### Setting StableToken and GoldToken Addresses -To obtain the StableToken address on a given environment run: +To obtain the StableToken and GoldToken addresses on a given environment run: ```bash -celotooljs contract-addresses --e alfajores --contracts StableToken +celotooljs contract-addresses --e alfajores --contracts StableToken,GoldToken ``` Replace `alfajores` by proper environment To set the address for faucet, in directory: `packages/faucet`, run: -Replace `net` and `stableTokenAddress` with proper values +Replace `net`, `stableTokenAddress`, and `goldTokenAddress` with proper values ```bash -yarn cli config:set --net alfajores --stableTokenAddress 0x299E74bdCD90d4E10f7957EF074ceE32d7e9089a +yarn cli config:set --net alfajores --stableTokenAddress 0x299E74bdCD90d4E10f7957EF074ceE32d7e9089a --goldTokenAddress 0x4813BFD311E132ade22c70dFf7e5DB045d26D070 ``` You can verify with `yarn cli config:get --net alfajores` @@ -131,3 +132,13 @@ And then run: ```bash celotooljs account faucet -e alfajores --account 0xCEa3eF8e187490A9d85A1849D98412E5D27D1Bb3 ``` + +### How to deploy to staging + +1. `yarn firebase login` +2. `yarn deploy:staging` +3. Deployment can be seen at [https://console.firebase.google.com/project/celo-faucet-staging/overview](https://console.firebase.google.com/project/celo-faucet-staging/overview) +4. You can simulate the access at [https://console.firebase.google.com/project/celo-faucet-staging/database/celo-faucet-staging/rules](https://console.firebase.google.com/project/celo-faucet-staging/database/celo-faucet-staging/rules) + +`packages/web $ yarn run dev` +Go to [http://localhost:3000/build/wallet](http://localhost:3000/build/wallet) and perform submit, verify that no failure appears in the logs. diff --git a/packages/faucet/database-rules.bolt b/packages/faucet/database-rules.bolt index 6ccda948830..07436bddbec 100644 --- a/packages/faucet/database-rules.bolt +++ b/packages/faucet/database-rules.bolt @@ -26,7 +26,7 @@ path /{net}/requests { path /{net}/requests/{id} is Request { read() { true } - write() { isNew(this) } + write() { isAllowed(this) } } path /{net}/accounts/{account} is Account { @@ -43,6 +43,17 @@ isLoggedIn() { auth != null } isNew(ref) { prior(ref) == null } +// uid of ashishb+faucet@celo.org is gYOD4GPV86OlqQh33Loc5lh0Yo02 on celo-faucet +// This account can be seen/modified at https://console.firebase.google.com/project/celo-faucet/authentication/users +// +// uid of ashishb+faucet@celo.org is LLnGFlilnxbXrb5pZzvjiDeUJ6l2 on celo-faucet-staging +// This account can be seen/modified at https://console.firebase.google.com/project/celo-faucet-staging/authentication/users +isAllowed(ref) { + // TODO(ashishb): In the longer run, it would be better to choose only one uid based on whether + // we are on staging network or the production network. + return auth.uid == "gYOD4GPV86OlqQh33Loc5lh0Yo02" || auth.uid == "LLnGFlilnxbXrb5pZzvjiDeUJ6l2" +} + /** * Leaf Node Types diff --git a/packages/faucet/package.json b/packages/faucet/package.json index 7dfeb4ae581..b882b45593e 100644 --- a/packages/faucet/package.json +++ b/packages/faucet/package.json @@ -11,14 +11,11 @@ "scripts": { "preserve": "yarn run build", "serve": "cross-env NODE_ENV=production fi rebase serve", - "deploy": "firebase deploy", - "clean": "rimraf dist", - "build:contracts": "yarn run --cwd=../celotool cli copy-contract-artifacts --output-path=../faucet/src/generated --celo-env=integration --contracts=StableToken", - "build": "tsc --project .", - "lint:types-functions": "tsc --project . --noEmit", - "lint:functions": "tslint --project .", - "lint": "yarn lint:functions && yarn lint:types-functions", - "lint-checks": "yarn lint", + "deploy:staging": "firebase deploy --project celo-faucet-staging", + "deploy:prod": "firebase deploy --project celo-faucet", + "clean": "tsc -b . --clean", + "build": "tsc -b .", + "lint": "tslint --project .", "transfer-funds": "ts-node scripts/transfer-funds.ts", "cli": "ts-node scripts/cli.ts" }, diff --git a/packages/faucet/scripts/cli.ts b/packages/faucet/scripts/cli.ts index 259eb20f968..ba638beb870 100644 --- a/packages/faucet/scripts/cli.ts +++ b/packages/faucet/scripts/cli.ts @@ -130,6 +130,10 @@ yargs description: 'Name of network', demand: true, }) + .option('goldTokenAddress', { + type: 'string', + description: 'Address for gold token contract', + }) .option('stableTokenAddress', { type: 'string', description: 'Address for stable token contract', @@ -170,6 +174,7 @@ yargs inviteDollarAmount: args.inviteDollarAmount, escrowDollarAmount: args.escrowDollarAmount, nodeUrl: args.nodeUrl, + goldTokenAddress: args.goldTokenAddress, stableTokenAddress: args.stableTokenAddress, escrowAddress: args.escrowAddress, minAttestations: args.minAttestations, @@ -198,6 +203,7 @@ function setConfig(network: string, config: Partial ', @@ -57,6 +62,7 @@ yargs async function transferFunds(args: { nodeUrl: string stableTokenAddress: string + goldTokenAddress: string pk: string recipientAddress: string dryrun?: boolean @@ -66,7 +72,8 @@ async function transferFunds(args: { const web3 = await new Web3(args.nodeUrl) const pk = args.pk const to = args.recipientAddress - const celo = new CeloAdapter(web3, pk, args.stableTokenAddress) + // Escrow address is an empty string, because we don't need that contract in this function + const celo = new CeloAdapter(web3, pk, args.stableTokenAddress, '', args.goldTokenAddress) const printBalance = async (addr: string) => { console.log(`Account: ${addr}`) @@ -104,4 +111,4 @@ async function transferFunds(args: { await printBalance(to) } -// --nodeUrl http://35.247.98.50:8545 --gold 10000000000000000000 --dollar 10000000000000000000 --stableTokenAddress 0x7DFAA4B53E7d06E9e30C4426d9692453d94A8437 add67e37fdf5c26743d295b1af6d9b50f2785a6b60bc83a8f05bd1dd4b385c6c 0x22937E2c505374Ce7AaE95993fe7580c526a62b4 +// --nodeUrl http://35.247.98.50:8545 --gold 10000000000000000000 --dollar 10000000000000000000 --stableTokenAddress 0x7DFAA4B53E7d06E9e30C4426d9692453d94A8437 --goldTokenAddress 0x4813BFD311E132ade22c70dFf7e5DB045d26D070 add67e37fdf5c26743d295b1af6d9b50f2785a6b60bc83a8f05bd1dd4b385c6c 0x22937E2c505374Ce7AaE95993fe7580c526a62b4 diff --git a/packages/faucet/src/celo-adapter.ts b/packages/faucet/src/celo-adapter.ts index 7574ede91a4..99c2e1b2dac 100644 --- a/packages/faucet/src/celo-adapter.ts +++ b/packages/faucet/src/celo-adapter.ts @@ -1,12 +1,15 @@ import Web3 from 'web3' import getEscrowInstance from './contracts/Escrow' +import getGoldTokenInstance from './contracts/GoldToken' import getStableTokenInstance from './contracts/StableToken' import { Escrow } from './contracts/types/Escrow' +import { GoldToken } from './contracts/types/GoldToken' import { StableToken } from './contracts/types/StableToken' -import { getAddress, sendSimpleTx, sendTx } from './tx' +import { getAddress, sendTx } from './tx' export class CeloAdapter { public readonly defaultAddress: string + private readonly goldToken: GoldToken private readonly stableToken: StableToken private readonly escrow: Escrow private readonly privateKey: string @@ -15,10 +18,12 @@ export class CeloAdapter { private readonly web3: Web3, pk: string, private readonly stableTokenAddress: string, - private readonly escrowAddress: string + private readonly escrowAddress: string, + private readonly goldTokenAddress: string ) { this.privateKey = this.web3.utils.isHexStrict(pk) ? pk : '0x' + pk this.defaultAddress = getAddress(this.web3, this.privateKey) + this.goldToken = getGoldTokenInstance(this.web3, goldTokenAddress) this.stableToken = getStableTokenInstance(this.web3, stableTokenAddress) this.escrow = getEscrowInstance(this.web3, escrowAddress) } @@ -27,10 +32,9 @@ export class CeloAdapter { return } - transferGold(to: string, amount: string) { - return sendSimpleTx(this.web3, this.privateKey, { - to, - value: amount, + async transferGold(to: string, amount: string) { + return sendTx(this.web3, this.goldToken.methods.transfer(to, amount), this.privateKey, { + to: this.goldTokenAddress, }) } diff --git a/packages/faucet/src/config.ts b/packages/faucet/src/config.ts index ad08f503e32..7f7bf386d26 100644 --- a/packages/faucet/src/config.ts +++ b/packages/faucet/src/config.ts @@ -8,6 +8,7 @@ export interface NetworkConfig { inviteGoldAmount: string inviteDollarAmount: string escrowDollarAmount: string + goldTokenAddress: string stableTokenAddress: string escrowAddress: string expirarySeconds: number @@ -43,6 +44,7 @@ export function getNetworkConfig(net: string): NetworkConfig { inviteGoldAmount: config[net].invite_gold_amount, inviteDollarAmount: config[net].invite_dollar_amount, escrowDollarAmount: config[net].escrow_dollar_amount, + goldTokenAddress: config[net].gold_token_address, stableTokenAddress: config[net].stable_token_address, escrowAddress: config[net].escrow_address, expirarySeconds: Number(config[net].expirary_seconds), diff --git a/packages/faucet/src/contracts/GoldToken.ts b/packages/faucet/src/contracts/GoldToken.ts new file mode 100644 index 00000000000..ecbff6f713a --- /dev/null +++ b/packages/faucet/src/contracts/GoldToken.ts @@ -0,0 +1,300 @@ +import Web3 from 'web3' +import { GoldToken } from './types/GoldToken' + +const ABI = [ + { + constant: true, + inputs: [], + name: 'initialized', + outputs: [ + { + name: '', + type: 'bool', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'from', + type: 'address', + }, + { + indexed: true, + name: 'to', + type: 'address', + }, + { + indexed: false, + name: 'value', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + name: 'comment', + type: 'string', + }, + ], + name: 'TransferComment', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'owner', + type: 'address', + }, + { + indexed: true, + name: 'spender', + type: 'address', + }, + { + indexed: false, + name: 'value', + type: 'uint256', + }, + ], + name: 'Approval', + type: 'event', + }, + { + constant: false, + inputs: [], + name: 'initialize', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: false, + inputs: [ + { + name: 'to', + type: 'address', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'transfer', + outputs: [ + { + name: '', + type: 'bool', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: false, + inputs: [ + { + name: 'to', + type: 'address', + }, + { + name: 'value', + type: 'uint256', + }, + { + name: 'comment', + type: 'string', + }, + ], + name: 'transferWithComment', + outputs: [ + { + name: '', + type: 'bool', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: false, + inputs: [ + { + name: 'spender', + type: 'address', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'approve', + outputs: [ + { + name: '', + type: 'bool', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: false, + inputs: [ + { + name: 'from', + type: 'address', + }, + { + name: 'to', + type: 'address', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'transferFrom', + outputs: [ + { + name: '', + type: 'bool', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'name', + outputs: [ + { + name: '', + type: 'string', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'symbol', + outputs: [ + { + name: '', + type: 'string', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'decimals', + outputs: [ + { + name: '', + type: 'uint8', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [ + { + name: '', + type: 'uint256', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [ + { + name: 'owner', + type: 'address', + }, + { + name: 'spender', + type: 'address', + }, + ], + name: 'allowance', + outputs: [ + { + name: '', + type: 'uint256', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: false, + inputs: [ + { + name: 'amount', + type: 'uint256', + }, + ], + name: 'increaseSupply', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: true, + inputs: [ + { + name: 'owner', + type: 'address', + }, + ], + name: 'balanceOf', + outputs: [ + { + name: '', + type: 'uint256', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, +] + +export default function getInstance(web3: Web3, address: string): GoldToken { + return new web3.eth.Contract(ABI, address) as any +} diff --git a/packages/faucet/src/contracts/types/GoldToken.d.ts b/packages/faucet/src/contracts/types/GoldToken.d.ts new file mode 100644 index 00000000000..6ca6fb2b5ba --- /dev/null +++ b/packages/faucet/src/contracts/types/GoldToken.d.ts @@ -0,0 +1,91 @@ +/* Generated by ts-generator ver. 0.0.8 */ +/* tslint:disable */ + +import { EventEmitter } from 'events' +import Contract, { contractOptions, CustomOptions } from 'web3/eth/contract' +import { BlockType, TransactionObject } from 'web3/eth/types' +import { Provider } from 'web3/providers' +import { Callback, EventLog } from 'web3/types' + +export class GoldToken extends Contract { + constructor(jsonInterface: any[], address?: string, options?: CustomOptions) + _address: string + options: contractOptions + methods: { + allowance(owner: string, spender: string): TransactionObject + + balanceOf(owner: string): TransactionObject + + initialize(): TransactionObject + + transfer(to: string, value: number | string): TransactionObject + + transferWithComment( + to: string, + value: number | string, + comment: string + ): TransactionObject + + approve(spender: string, value: number | string): TransactionObject + + transferFrom(from: string, to: string, value: number | string): TransactionObject + + increaseSupply(amount: number | string): TransactionObject + + initialized(): TransactionObject + name(): TransactionObject + symbol(): TransactionObject + decimals(): TransactionObject + totalSupply(): TransactionObject + } + deploy(options: { data: string; arguments: any[] }): TransactionObject + events: { + Transfer( + options?: { + filter?: object + fromBlock?: BlockType + topics?: (null | string)[] + }, + cb?: Callback + ): EventEmitter + + TransferComment( + options?: { + filter?: object + fromBlock?: BlockType + topics?: (null | string)[] + }, + cb?: Callback + ): EventEmitter + + Approval( + options?: { + filter?: object + fromBlock?: BlockType + topics?: (null | string)[] + }, + cb?: Callback + ): EventEmitter + + allEvents: ( + options?: { + filter?: object + fromBlock?: BlockType + topics?: (null | string)[] + }, + cb?: Callback + ) => EventEmitter + } + getPastEvents( + event: string, + options?: { + filter?: object + fromBlock?: BlockType + toBlock?: BlockType + topics?: (null | string)[] + }, + cb?: Callback + ): Promise + setProvider(provider: Provider): void + clone(): GoldToken +} diff --git a/packages/faucet/src/database-helper.ts b/packages/faucet/src/database-helper.ts index 94d7242f7a8..9e4d45daffc 100644 --- a/packages/faucet/src/database-helper.ts +++ b/packages/faucet/src/database-helper.ts @@ -65,7 +65,8 @@ function buildHandleFaucet(request: RequestRecord, snap: DataSnapshot, config: N new Web3(config.nodeUrl), account.pk, config.stableTokenAddress, - config.escrowAddress + config.escrowAddress, + config.goldTokenAddress ) const goldTx = await celo.transferGold(request.beneficiary, config.faucetGoldAmount) const goldTxHash = await goldTx.getHash() @@ -93,7 +94,8 @@ function buildHandleInvite(request: RequestRecord, snap: DataSnapshot, config: N new Web3(config.nodeUrl), account.pk, config.stableTokenAddress, - config.escrowAddress + config.escrowAddress, + config.goldTokenAddress ) const { address: tempAddress, inviteCode } = generateInviteCode() const goldTx = await celo.transferGold(tempAddress, config.inviteGoldAmount) diff --git a/packages/faucet/src/test-driver.ts b/packages/faucet/src/test-driver.ts index d903aab56c5..00509334e3b 100644 --- a/packages/faucet/src/test-driver.ts +++ b/packages/faucet/src/test-driver.ts @@ -64,7 +64,8 @@ async function web3Playground() { web3, pk, '0x299E74bdCD90d4E10f7957EF074ceE32d7e9089a', - '0x202ec0cbd312425C266dd473754Ad1719948Bd35' + '0x202ec0cbd312425C266dd473754Ad1719948Bd35', + '0x4813BFD311E132ade22c70dFf7e5DB045d26D070' ) const printBalance = async (addr: string) => { diff --git a/packages/helm-charts/ethstats/Chart.yaml b/packages/helm-charts/ethstats/Chart.yaml new file mode 100644 index 00000000000..1ed7afc3ff1 --- /dev/null +++ b/packages/helm-charts/ethstats/Chart.yaml @@ -0,0 +1,8 @@ +name: ethstats +version: 0.0.1 +description: Chart which is used to deploy an ethstats setup for a celo testnet +keywords: +- ethereum +- blockchain +- ethstats +appVersion: v1.7.3 diff --git a/packages/helm-charts/ethstats/README.md b/packages/helm-charts/ethstats/README.md new file mode 100644 index 00000000000..de3b912936c --- /dev/null +++ b/packages/helm-charts/ethstats/README.md @@ -0,0 +1,15 @@ +# ethstats + +## Deploying on existing testnet + +Originally, the resources in this chart were in the `testnet` helm chart. +When upgrading a testnet that is currently running that was not deployed +with ethstats as a separate helm chart, you must: + +1. Upgrade the testnet: `celotool deploy upgrade testnet -e MY_ENV`. This + will remove the previous ethstats resources, even if there are otherwise no + changes to the testnet. Nodes in the testnet require an ethstats secret that + will be missing, so if any nodes are restarted they will hang until the secret + is created in the next step. + +2. Deploy the new separate ethstats: `celotool deploy initial ethstats -e MY_ENV`. diff --git a/packages/helm-charts/testnet/templates/ethstats.deployment.yaml b/packages/helm-charts/ethstats/templates/ethstats.deployment.yaml similarity index 72% rename from packages/helm-charts/testnet/templates/ethstats.deployment.yaml rename to packages/helm-charts/ethstats/templates/ethstats.deployment.yaml index 47fb649c679..0e456f7381e 100644 --- a/packages/helm-charts/testnet/templates/ethstats.deployment.yaml +++ b/packages/helm-charts/ethstats/templates/ethstats.deployment.yaml @@ -1,10 +1,10 @@ -apiVersion: apps/v1beta2 +apiVersion: apps/v1beta2 kind: Deployment metadata: - name: {{ template "ethereum.fullname" . }}-ethstats + name: {{ .Release.Name }} labels: - app: {{ template "ethereum.name" . }} - chart: {{ template "ethereum.chart" . }} + app: ethstats + chart: ethstats release: {{ .Release.Name }} heritage: {{ .Release.Service }} component: ethstats @@ -12,13 +12,13 @@ spec: replicas: 1 selector: matchLabels: - app: {{ template "ethereum.name" . }} + app: ethstats release: {{ .Release.Name }} component: ethstats template: metadata: labels: - app: {{ template "ethereum.name" . }} + app: ethstats release: {{ .Release.Name }} component: ethstats spec: @@ -37,9 +37,9 @@ spec: - name: WS_SECRET valueFrom: secretKeyRef: - name: {{ template "ethereum.fullname" . }}-ethstats + name: {{ .Release.Name }} key: WS_SECRET {{- with .Values.nodeSelector }} nodeSelector: {{ toYaml . | indent 8 }} - {{- end }} \ No newline at end of file + {{- end }} diff --git a/packages/helm-charts/ethstats/templates/ethstats.ingress.yaml b/packages/helm-charts/ethstats/templates/ethstats.ingress.yaml new file mode 100644 index 00000000000..fe968e0e804 --- /dev/null +++ b/packages/helm-charts/ethstats/templates/ethstats.ingress.yaml @@ -0,0 +1,25 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ .Release.Name }}-ingress + labels: + app: ethstats + chart: ethstats + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + component: ethstats + annotations: + kubernetes.io/tls-acme: "true" +spec: + tls: + - hosts: + - {{ .Release.Name }}.{{ .Values.domain.name }}.org + secretName: {{ .Release.Name }}-tls + rules: + - host: {{ .Release.Name }}.{{ .Values.domain.name }}.org + http: + paths: + - path: / + backend: + serviceName: {{ .Release.Name }} + servicePort: 80 diff --git a/packages/helm-charts/testnet/templates/ethstats.secret.yaml b/packages/helm-charts/ethstats/templates/ethstats.secret.yaml similarity index 58% rename from packages/helm-charts/testnet/templates/ethstats.secret.yaml rename to packages/helm-charts/ethstats/templates/ethstats.secret.yaml index ee5412227a7..1cb3319c659 100644 --- a/packages/helm-charts/testnet/templates/ethstats.secret.yaml +++ b/packages/helm-charts/ethstats/templates/ethstats.secret.yaml @@ -1,12 +1,12 @@ apiVersion: v1 kind: Secret metadata: - name: {{ template "ethereum.fullname" . }}-ethstats + name: {{ .Release.Name }} labels: - app: {{ template "ethereum.name" . }} - chart: {{ template "ethereum.chart" . }} + app: ethstats + chart: ethstats release: {{ .Release.Name }} heritage: {{ .Release.Service }} type: Opaque data: - WS_SECRET: {{ .Values.ethstats.webSocketSecret | b64enc | quote }} \ No newline at end of file + WS_SECRET: {{ .Values.ethstats.webSocketSecret | b64enc | quote }} diff --git a/packages/helm-charts/testnet/templates/ethstats.service.yaml b/packages/helm-charts/ethstats/templates/ethstats.service.yaml similarity index 58% rename from packages/helm-charts/testnet/templates/ethstats.service.yaml rename to packages/helm-charts/ethstats/templates/ethstats.service.yaml index 259e6969269..3943f7441d2 100644 --- a/packages/helm-charts/testnet/templates/ethstats.service.yaml +++ b/packages/helm-charts/ethstats/templates/ethstats.service.yaml @@ -1,19 +1,19 @@ kind: Service apiVersion: v1 metadata: - name: {{ template "ethereum.fullname" . }}-ethstats + name: {{ .Release.Name }} labels: - app: {{ template "ethereum.name" . }} - chart: {{ template "ethereum.chart" . }} + app: ethstats + chart: ethstats release: {{ .Release.Name }} heritage: {{ .Release.Service }} component: ethstats spec: selector: - app: {{ template "ethereum.name" . }} + app: ethstats release: {{ .Release.Name }} component: ethstats type: {{ .Values.ethstats.service.type }} ports: - port: 80 - targetPort: http \ No newline at end of file + targetPort: http diff --git a/packages/helm-charts/ethstats/values.yaml b/packages/helm-charts/ethstats/values.yaml new file mode 100644 index 00000000000..cc719bbcd7c --- /dev/null +++ b/packages/helm-charts/ethstats/values.yaml @@ -0,0 +1,12 @@ +imagePullPolicy: IfNotPresent + +# Node labels for pod assignment +# ref: https://kubernetes.io/docs/user-guide/node-selection/ +nodeSelector: {} + +ethstats: + image: + repository: ethereumex/eth-stats-dashboard + tag: v0.0.1 + service: + type: NodePort diff --git a/packages/helm-charts/load-test/templates/statefulset.yaml b/packages/helm-charts/load-test/templates/statefulset.yaml index dd41742c04a..5274c9fcdc9 100644 --- a/packages/helm-charts/load-test/templates/statefulset.yaml +++ b/packages/helm-charts/load-test/templates/statefulset.yaml @@ -109,7 +109,7 @@ spec: CELOTOOL="/celo-monorepo/packages/celotool/bin/celotooljs.sh"; ENV_NAME="{{ .Values.environment }}"; - cd /celo-monorepo/packages/contractkit && yarn run build $ENV_NAME + cd /celo-monorepo/packages/walletkit && yarn run build $ENV_NAME $CELOTOOL geth simulate-client --delay 5 --data-dir /root/.celo -e $ENV_NAME --private-key /root/.celo/pkey --blockscout {{ .Values.blockscoutProb }} --load-test-id "{{ .Values.loadTestID }}" resources: diff --git a/packages/helm-charts/testnet/README.md b/packages/helm-charts/testnet/README.md index 1bbda70841d..45c1f0f69cd 100644 --- a/packages/helm-charts/testnet/README.md +++ b/packages/helm-charts/testnet/README.md @@ -113,24 +113,20 @@ When you are finally happy with your changes to geth: The following table lists the configurable parameters of the vault chart and their default values. -| Parameter | Description | Default | -| --------------------------- | ------------------------------------------------------------------ | -------------------------------------- | -| `imagePullPolicy` | Container pull policy | `IfNotPresent` | -| `nodeSelector` | Node labels for pod assignmen | | -| `bootnode.image.repository` | bootnode container image to use | `ethereum/client-go` | -| `bootnode.image.tag` | bootnode container image tag to deploy | `alltools-v1.7.3` | -| `ethstats.image.repository` | ethstats container image to use | `ethereumex/eth-stats-dashboard` | -| `ethstats.image.tag` | ethstats container image tag to deploy | `latest` | -| `ethstats.webSocketSecret` | ethstats secret for posting data | `my-secret-for-connecting-to-ethstats` | -| `ethstats.service.type` | k8s service type for ethstats | `LoadBalancer` | -| `geth.image.repository` | geth container image to use | `ethereum/client-go` | -| `geth.image.tag` | geth container image tag to deploy | `v1.7.3` | -| `geth.tx.replicaCount` | geth transaction nodes replica count | `1` | -| `geth.miner.replicaCount` | geth miner nodes replica count | `1` | -| `geth.miner.account.secret` | geth account secret | `my-secret-account-password` | -| `geth.genesis.networkId` | Ethereum network id | `1101` | -| `geth.genesis.difficulty` | Ethereum network difficulty | `0x0400` | -| `geth.genesis.gasLimit` | Ethereum network gas limit | `0x8000000` | -| `geth.account.address` | Geth Account to be initially funded and deposited with mined Ether | | -| `geth.account.privateKey` | Geth Private Key | | -| `geth.account.secret` | Geth Account Secret | | +| Parameter | Description | Default | +| --------------------------- | ------------------------------------------------------------------ | ---------------------------- | +| `imagePullPolicy` | Container pull policy | `IfNotPresent` | +| `nodeSelector` | Node labels for pod assignmen | | +| `bootnode.image.repository` | bootnode container image to use | `ethereum/client-go` | +| `bootnode.image.tag` | bootnode container image tag to deploy | `alltools-v1.7.3` | +| `geth.image.repository` | geth container image to use | `ethereum/client-go` | +| `geth.image.tag` | geth container image tag to deploy | `v1.7.3` | +| `geth.tx.replicaCount` | geth transaction nodes replica count | `1` | +| `geth.miner.replicaCount` | geth miner nodes replica count | `1` | +| `geth.miner.account.secret` | geth account secret | `my-secret-account-password` | +| `geth.genesis.networkId` | Ethereum network id | `1101` | +| `geth.genesis.difficulty` | Ethereum network difficulty | `0x0400` | +| `geth.genesis.gasLimit` | Ethereum network gas limit | `0x8000000` | +| `geth.account.address` | Geth Account to be initially funded and deposited with mined Ether | | +| `geth.account.privateKey` | Geth Private Key | | +| `geth.account.secret` | Geth Account Secret | | diff --git a/packages/helm-charts/testnet/templates/ethstats.ingress.yaml b/packages/helm-charts/testnet/templates/ethstats.ingress.yaml deleted file mode 100644 index c3050ed4982..00000000000 --- a/packages/helm-charts/testnet/templates/ethstats.ingress.yaml +++ /dev/null @@ -1,25 +0,0 @@ -apiVersion: extensions/v1beta1 -kind: Ingress -metadata: - name: {{ template "ethereum.fullname" . }}-ethstats-ingress - labels: - app: {{ template "ethereum.name" . }} - chart: {{ template "ethereum.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} - component: ethstats - annotations: - kubernetes.io/tls-acme: "true" -spec: - tls: - - hosts: - - {{ .Release.Name }}-ethstats.{{ .Values.domain.name }}.org - secretName: {{ template "ethereum.fullname" . }}-ethstats-tls - rules: - - host: {{ .Release.Name }}-ethstats.{{ .Values.domain.name }}.org - http: - paths: - - path: / - backend: - serviceName: {{ template "ethereum.fullname" . }}-ethstats - servicePort: 80 diff --git a/packages/helm-charts/testnet/values.yaml b/packages/helm-charts/testnet/values.yaml index d9fdb9f661e..de38bf10cf8 100644 --- a/packages/helm-charts/testnet/values.yaml +++ b/packages/helm-charts/testnet/values.yaml @@ -13,13 +13,6 @@ bootnode: repository: gcr.io/celo-testnet/geth-all tag: fc254b550a4993956ac7aa3fcd8dd4db63b8c9d2 -ethstats: - image: - repository: ethereumex/eth-stats-dashboard - tag: v0.0.1 - service: - type: NodePort - geth: image: repository: gcr.io/celo-testnet/geth diff --git a/packages/mobile/.env b/packages/mobile/.env index f5996009f38..cc89808cc25 100644 --- a/packages/mobile/.env +++ b/packages/mobile/.env @@ -1,6 +1,6 @@ ENVIRONMENT=local -DEFAULT_TESTNET=alfajores -BLOCKCHAIN_API_URL=https://alfajores-dot-celo-testnet-production.appspot.com/ +DEFAULT_TESTNET=alfajoresstaging +BLOCKCHAIN_API_URL=https://alfajoresstaging-dot-celo-testnet.appspot.com/ DEV_SETTINGS_ACTIVE_INITIALLY=true FIREBASE_ENABLED=true SECRETS_KEY=debug diff --git a/packages/mobile/.env.integration b/packages/mobile/.env.integration index 661254b9be9..3c63c7582c5 100644 --- a/packages/mobile/.env.integration +++ b/packages/mobile/.env.integration @@ -1,6 +1,6 @@ ENVIRONMENT=integration -DEFAULT_TESTNET=alfajoresstaging -BLOCKCHAIN_API_URL=https://alfajoresstaging-dot-celo-testnet.appspot.com/ +DEFAULT_TESTNET=integration +BLOCKCHAIN_API_URL=https://integration-dot-celo-testnet.appspot.com/ DEV_SETTINGS_ACTIVE_INITIALLY=true FIREBASE_ENABLED=true SECRETS_KEY=integration diff --git a/packages/mobile/.env.pilot b/packages/mobile/.env.pilot new file mode 100644 index 00000000000..bca4f6a27b6 --- /dev/null +++ b/packages/mobile/.env.pilot @@ -0,0 +1,8 @@ +ENVIRONMENT=pilot +DEFAULT_TESTNET=pilot +BLOCKCHAIN_API_URL=https://pilot-dot-celo-testnet-production.appspot.com/ +DEV_SETTINGS_ACTIVE_INITIALLY=false +FIREBASE_ENABLED=true +SECRETS_KEY=pilot +INSTABUG_TOKEN= +INSTABUG_EVENTS=button,shake diff --git a/packages/mobile/.env.pilotstaging b/packages/mobile/.env.pilotstaging new file mode 100644 index 00000000000..e9326e4e381 --- /dev/null +++ b/packages/mobile/.env.pilotstaging @@ -0,0 +1,8 @@ +ENVIRONMENT=pilotstaging +DEFAULT_TESTNET=pilotstaging +BLOCKCHAIN_API_URL=https://pilotstaging-dot-celo-testnet.appspot.com/ +DEV_SETTINGS_ACTIVE_INITIALLY=true +FIREBASE_ENABLED=true +SECRETS_KEY=pilotstaging +INSTABUG_TOKEN= +INSTABUG_EVENTS=button,shake diff --git a/packages/mobile/.env.staging b/packages/mobile/.env.staging index 27345d026af..725da068458 100644 --- a/packages/mobile/.env.staging +++ b/packages/mobile/.env.staging @@ -1,6 +1,6 @@ ENVIRONMENT=staging -DEFAULT_TESTNET=alfajores -BLOCKCHAIN_API_URL=https://alfajores-dot-celo-testnet-production.appspot.com/ +DEFAULT_TESTNET=alfajoresstaging +BLOCKCHAIN_API_URL=https://alfajoresstaging-dot-celo-testnet.appspot.com/ DEV_SETTINGS_ACTIVE_INITIALLY=true FIREBASE_ENABLED=true SECRETS_KEY=staging diff --git a/packages/mobile/README.md b/packages/mobile/README.md index 4f47f24ac85..f97bd99a26e 100644 --- a/packages/mobile/README.md +++ b/packages/mobile/README.md @@ -39,7 +39,7 @@ export GRADLE_OPTS='-Dorg.gradle.daemon=true -Dorg.gradle.parallel=true -Dorg.gr **Note:** We've seen some issues running the metro bundler from iTerm -## Debugging +### Debugging In order to debug, you should run: @@ -55,12 +55,12 @@ console. In order to get a full picture, the console's filter should be set to You will probably want to open the dev menu again and enable `Live Reloading` and `Hot Reloading` to make development faster. -### (_Optional_) React Native debugger app +#### (_Optional_) React Native debugger app The [RN debugger app][rn debugger] bundles together the Redux and Chrome dev tools nicely. -## App Profiling +### App Profiling Start the emulator and load up the app. Then run the following to start react devtools. @@ -79,31 +79,42 @@ renders when no state has changed. Reducing renders can be done via pure components in react or overloading the should component update method [example here][rn optimize example]. -## Connecting to networks +### Connecting to networks By default, we have the `alfajores` network set up. If you have other testnets -that you want to use with the app, you can run +that you want to use with the app, update `.env.ENV-NAME` and `packages/mobile/.env.ENV-NAME` with the new network name and settings, then run ```bash yarn run build-sdk TESTNET ``` -## Snapshot Testing +before rebuilding the app. Note that this will assume the testnets have a corresponding `/blockchain-api` and `/notification-service` set up. + +## Testing + +To execute the suite of tests, run `yarn test` + +## Snapshot testing We use Jest [snapshot testing][jest] to assert that no intentional changes to the component tree have been made without explicit developer intention. See an -example at [`src/send/SendAmount.test.tsx`]. If your snapshot is -expected to deviate, you can update the snapshot with the `--updateSnapshot` +example at [`src/send/SendAmount.test.tsx`]. If your snapshot is expected +to deviate, you can update the snapshot with the `-u` or `--updateSnapshot` flag when running the test. -## React Component Unit Testing +### React Component Unit Testing -We use [react-native-testing-library][react-native-testing-library] to unit test react components. It allows for deep rendering -and interaction with the rendered tree to assert proper reactions to user -interaction and input. See an example at +We use [react-native-testing-library][react-native-testing-library] to unit test +react components. It allows for deep rendering and interaction with the rendered +tree to assert proper reactions to user interaction and input. See an example at [`src/send/SendAmount.test.tsx`] or read more about the [docs][rntl-docs] -## E2E testing +## Saga testing + +We use [redux-saga-test-plan][redux-saga-test-plan] to test complex sagas. +See [`src/identity/verification.test.ts`] for an example. + +### E2E testing We use [Detox][detox] for E2E testing. In order to run the tests locally, you must have the proper emulator set up. Emulator installation instructions are in @@ -168,3 +179,4 @@ $ adb kill-server && adb start-server [react-native-testing-library]: https://github.com/callstack/react-native-testing-library [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 diff --git a/packages/mobile/__mocks__/src/utils/androidPermissions.ts b/packages/mobile/__mocks__/src/utils/androidPermissions.ts new file mode 100644 index 00000000000..ce934a7c084 --- /dev/null +++ b/packages/mobile/__mocks__/src/utils/androidPermissions.ts @@ -0,0 +1,19 @@ +export function requestPhoneStatePermission() { + return true +} + +export function requestContactsPermission() { + return true +} + +export function requestCameraPermission() { + return true +} + +export function checkContactsPermission() { + return true +} + +export function checkCameraPermission() { + return true +} diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle index 99509b3d3c3..39a6807f1ae 100644 --- a/packages/mobile/android/app/build.gradle +++ b/packages/mobile/android/app/build.gradle @@ -122,7 +122,7 @@ android { minSdkVersion isDetoxTestBuild ? rootProject.ext.minSdkVersion : 18 targetSdkVersion rootProject.ext.targetSdkVersion versionCode appVersionCode - versionName "1.3.2" + versionName "1.4.0" testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "build_config_package", "org.celo.mobile" @@ -150,7 +150,7 @@ android { */ enable isDetoxTestBuild universalApk false // If true, also generate a universal APK - include "armeabi-v7a", "x86" // "arm64-v8a", "x86_64" + include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" } } buildTypes { diff --git a/packages/mobile/android/app/src/alfajores/res/values/styles.xml b/packages/mobile/android/app/src/alfajores/res/values/styles.xml deleted file mode 100644 index 319eb0ca100..00000000000 --- a/packages/mobile/android/app/src/alfajores/res/values/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/packages/mobile/android/app/src/debug/res/values/styles.xml b/packages/mobile/android/app/src/debug/res/values/styles.xml deleted file mode 100644 index 319eb0ca100..00000000000 --- a/packages/mobile/android/app/src/debug/res/values/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/packages/mobile/android/app/src/integration/res/values/styles.xml b/packages/mobile/android/app/src/integration/res/values/styles.xml deleted file mode 100644 index 319eb0ca100..00000000000 --- a/packages/mobile/android/app/src/integration/res/values/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/packages/mobile/android/app/src/main/AndroidManifest.xml b/packages/mobile/android/app/src/main/AndroidManifest.xml index 475d5c31d42..8345e97c403 100644 --- a/packages/mobile/android/app/src/main/AndroidManifest.xml +++ b/packages/mobile/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - @@ -13,11 +13,18 @@ - + + + + + + + diff --git a/packages/mobile/android/app/src/main/res/values/styles.xml b/packages/mobile/android/app/src/main/res/values/styles.xml index 097df3daac8..daa8b4b6a35 100644 --- a/packages/mobile/android/app/src/main/res/values/styles.xml +++ b/packages/mobile/android/app/src/main/res/values/styles.xml @@ -1,4 +1,6 @@ diff --git a/packages/mobile/android/app/src/main/res/xml/backup_rules.xml b/packages/mobile/android/app/src/main/res/xml/backup_rules.xml index eff70046103..832c7d4a04f 100644 --- a/packages/mobile/android/app/src/main/res/xml/backup_rules.xml +++ b/packages/mobile/android/app/src/main/res/xml/backup_rules.xml @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/packages/mobile/android/app/src/staging/res/values/styles.xml b/packages/mobile/android/app/src/staging/res/values/styles.xml deleted file mode 100644 index 097df3daac8..00000000000 --- a/packages/mobile/android/app/src/staging/res/values/styles.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/packages/mobile/android/build.gradle b/packages/mobile/android/build.gradle index 0bc3b5abdf6..11d61a81db8 100644 --- a/packages/mobile/android/build.gradle +++ b/packages/mobile/android/build.gradle @@ -12,7 +12,7 @@ buildscript { // Must pin to 16 because 17 uses androidX googlePlayServicesVersion = "16.+" // Change this to change the geth version - celoClientDirectory = "../../../../node_modules/@celo/client-integration/build/bin" + celoClientDirectory = new File(rootProject.projectDir, '../../../node_modules/@celo/client/build/bin') } repositories { google() diff --git a/packages/mobile/android/gradle.properties b/packages/mobile/android/gradle.properties index 3520c2c7a4c..34b856f3154 100644 --- a/packages/mobile/android/gradle.properties +++ b/packages/mobile/android/gradle.properties @@ -20,4 +20,4 @@ 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=1003002 \ No newline at end of file +VERSION_CODE=1004000 \ No newline at end of file diff --git a/packages/mobile/android/settings.gradle b/packages/mobile/android/settings.gradle index bc97b54d112..ea6f51a8924 100644 --- a/packages/mobile/android/settings.gradle +++ b/packages/mobile/android/settings.gradle @@ -2,13 +2,13 @@ rootProject.name = 'celo' include ':react-native-send-intent' project(':react-native-send-intent').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-send-intent/android') include ':react-native-webview' -project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android') +project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-webview/android') include ':instabug-reactnative' project(':instabug-reactnative').projectDir = new File(rootProject.projectDir, '../../../node_modules/instabug-reactnative/android') include ':@react-native-community_netinfo' -project(':@react-native-community_netinfo').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/netinfo/android') +project(':@react-native-community_netinfo').projectDir = new File(rootProject.projectDir, '../../../node_modules/@react-native-community/netinfo/android') include ':react-native-screens' -project(':react-native-screens').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-screens/android') +project(':react-native-screens').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-screens/android') include ':react-native-gesture-handler' project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android') include ':@segment_analytics-react-native-firebase' @@ -34,7 +34,7 @@ project(':react-native-languages').projectDir = new File(rootProject.projectDir, include ':react-native-config' project(':react-native-config').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-config/android') include ':react-native-firebase' -project(':react-native-firebase').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-firebase/android') +project(':react-native-firebase').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-firebase/android') include ':react-native-tcp' project(':react-native-tcp').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-tcp/android') include ':react-native-sentry' @@ -42,7 +42,7 @@ project(':react-native-sentry').projectDir = new File(rootProject.projectDir, '. include ':react-native-secure-randombytes' project(':react-native-secure-randombytes').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-secure-randombytes/android') include ':react-native-svg' -project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android') +project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-svg/android') include ':react-native-contacts' project(':react-native-contacts').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-contacts/android') include ':react-native-keep-awake' @@ -50,9 +50,9 @@ project(':react-native-keep-awake').projectDir = new File(rootProject.projectDir include ':react-native-device-info' project(':react-native-device-info').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-device-info/android') include ':react-native-fs' -project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fs/android') +project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-fs/android') include ':react-native-geth' -project(':react-native-geth').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-geth/android') +project(':react-native-geth').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-geth/android') include ':react-native-flag-secure-android' project(':react-native-flag-secure-android').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-flag-secure-android/android') include ':react-native-confirm-device-credentials' diff --git a/packages/mobile/fastlane/Fastfile b/packages/mobile/fastlane/Fastfile index a2c70c67f92..5edd80a1029 100644 --- a/packages/mobile/fastlane/Fastfile +++ b/packages/mobile/fastlane/Fastfile @@ -40,19 +40,19 @@ platform :android do 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: 'assemble', build_type: environment, project_dir: 'android/', flags: '-x bundle' + environment + 'JsAndAssets') + gradle(task: 'bundle', build_type: environment, project_dir: 'android/', flags: '-x bundle' + environment + 'JsAndAssets') end - desc 'Ship Integration to Playstore Alpha.' + desc 'Ship Integration to Playstore Internal' lane :integration do env = 'integration' - sdkEnv = 'alfajoresstaging' + sdkEnv = 'integration' clean build(environment: env, sdkEnv: sdkEnv) fastlane_supply(env, 'internal', env) end - desc 'Ship Staging to Playstore Alpha.' + desc 'Ship Staging to Playstore Internal' lane :staging do env = 'staging' sdkEnv = 'alfajoresstaging' diff --git a/packages/mobile/fastlane/README.md b/packages/mobile/fastlane/README.md index c32c8dada09..d8463966f90 100644 --- a/packages/mobile/fastlane/README.md +++ b/packages/mobile/fastlane/README.md @@ -42,7 +42,7 @@ Build the Android application - requires environment param fastlane android integration ``` -Ship Integration to Playstore Alpha. +Ship Integration to Playstore Internal ### android staging @@ -50,7 +50,7 @@ Ship Integration to Playstore Alpha. fastlane android staging ``` -Ship Staging to Playstore Alpha. +Ship Staging to Playstore Internal ### android production diff --git a/packages/mobile/fastlane/metadata/android/es-419/full_description.txt b/packages/mobile/fastlane/metadata/android/es-419/full_description.txt index cdef16413a7..a13a876c23d 100644 --- a/packages/mobile/fastlane/metadata/android/es-419/full_description.txt +++ b/packages/mobile/fastlane/metadata/android/es-419/full_description.txt @@ -10,7 +10,7 @@ AHORRA Y GASTA VALOR ESTABLE Envíe, solicite y guarde Celo dólares, que son estables en relación con los dólares estadounidenses. Verifique fácilmente la propiedad de su número de teléfono para comenzar. Después de eso, enviar o solicitar el pago a un amigo es fácil: todo lo que necesita es su número de teléfono y una pequeña tarifa. SEGURO SEGURO -Envíe de forma segura el valor a cualquier número de teléfono móvil o reciba un valor en su número de teléfono. Para mantener su valor seguro, proteja su billetera con una clave de respaldo. Esta clave protege sus fondos para que solo usted pueda acceder a ellos, ¡no la pierda! +Envíe de forma segura el valor a cualquier número de teléfono móvil o reciba un valor en su número de teléfono. Para mantener su valor seguro, proteja su monedero con una clave de respaldo. Esta clave protege sus fondos para que solo usted pueda acceder a ellos, ¡no la pierda! CAMBIO POR CELO GOLD Cambia Celo dólares por Celo gold. diff --git a/packages/mobile/fastlane/metadata/android/es-419/images/featureGraphic.jpg b/packages/mobile/fastlane/metadata/android/es-419/images/featureGraphic.jpg index f2f92b4d8c2..9fe198cae0e 100644 Binary files a/packages/mobile/fastlane/metadata/android/es-419/images/featureGraphic.jpg and b/packages/mobile/fastlane/metadata/android/es-419/images/featureGraphic.jpg differ diff --git a/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots01.jpg b/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots01.jpg old mode 100755 new mode 100644 index 553152e1303..9240f953918 Binary files a/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots01.jpg and b/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots01.jpg differ diff --git a/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots02.jpg b/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots02.jpg old mode 100755 new mode 100644 index 2ea8d098ece..30c95b03a42 Binary files a/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots02.jpg and b/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots02.jpg differ diff --git a/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots03.jpg b/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots03.jpg old mode 100755 new mode 100644 index bd4df3de8df..94d235f3fbc Binary files a/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots03.jpg and b/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots03.jpg differ diff --git a/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots04.jpg b/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots04.jpg old mode 100755 new mode 100644 index ee335f16e47..8dbe3318f76 Binary files a/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots04.jpg and b/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots04.jpg differ diff --git a/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots05.jpg b/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots05.jpg old mode 100755 new mode 100644 index 8e2946c7e85..7ff7f41f717 Binary files a/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots05.jpg and b/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots05.jpg differ diff --git a/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots06.jpg b/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots06.jpg deleted file mode 100755 index 541c606c30d..00000000000 Binary files a/packages/mobile/fastlane/metadata/android/es-419/images/phoneScreenshots/phoneScreenshots06.jpg and /dev/null differ diff --git a/packages/mobile/fastlane/metadata/android/es-419/images/promoGraphic.png b/packages/mobile/fastlane/metadata/android/es-419/images/promoGraphic.png new file mode 100644 index 00000000000..ed22a6db06f Binary files /dev/null and b/packages/mobile/fastlane/metadata/android/es-419/images/promoGraphic.png differ diff --git a/packages/mobile/fastlane/metadata/android/es-419/short_description.txt b/packages/mobile/fastlane/metadata/android/es-419/short_description.txt index d7309647f73..d3694ff0272 100644 --- a/packages/mobile/fastlane/metadata/android/es-419/short_description.txt +++ b/packages/mobile/fastlane/metadata/android/es-419/short_description.txt @@ -1 +1 @@ -Billetera digital segura que le permite enviar dinero a cualquier persona \ No newline at end of file +Monedero digital seguro que le permite enviar dinero a cualquier persona \ No newline at end of file diff --git a/packages/mobile/ios/celo-tvOS/Info.plist b/packages/mobile/ios/celo-tvOS/Info.plist index f743c1fa53b..0e28d433a13 100644 --- a/packages/mobile/ios/celo-tvOS/Info.plist +++ b/packages/mobile/ios/celo-tvOS/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.2 + 1.4.0 CFBundleSignature ???? CFBundleVersion - 4 + 5 LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/packages/mobile/ios/celo-tvOSTests/Info.plist b/packages/mobile/ios/celo-tvOSTests/Info.plist index c559ff1f1dc..85e889ee899 100644 --- a/packages/mobile/ios/celo-tvOSTests/Info.plist +++ b/packages/mobile/ios/celo-tvOSTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.2 + 1.4.0 CFBundleSignature ???? CFBundleVersion - 4 + 5 diff --git a/packages/mobile/ios/celo/Info.plist b/packages/mobile/ios/celo/Info.plist index c9b3440dd7c..7ed93a16b16 100644 --- a/packages/mobile/ios/celo/Info.plist +++ b/packages/mobile/ios/celo/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.2 + 1.4.0 CFBundleSignature ???? CFBundleVersion - 4 + 5 LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/packages/mobile/ios/celoTests/Info.plist b/packages/mobile/ios/celoTests/Info.plist index c559ff1f1dc..85e889ee899 100644 --- a/packages/mobile/ios/celoTests/Info.plist +++ b/packages/mobile/ios/celoTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.2 + 1.4.0 CFBundleSignature ???? CFBundleVersion - 4 + 5 diff --git a/packages/mobile/jest.config.js b/packages/mobile/jest.config.js index b3c87c5865e..105e439a2a8 100644 --- a/packages/mobile/jest.config.js +++ b/packages/mobile/jest.config.js @@ -9,7 +9,7 @@ module.exports = { moduleNameMapper: { '@celo/mobile': '', '^crypto-js$': '/node_modules/crypto-js', - 'react-native-svg': '/node_modules/react-native-svg-mock', + 'react-native-svg': '/../../node_modules/react-native-svg-mock', }, modulePathIgnorePatterns: ['/node_modules/(.*)/node_modules/react-native'], preset: 'react-native', diff --git a/packages/mobile/locales/en-US/global.json b/packages/mobile/locales/en-US/global.json index 7b1f1388b5e..c86526ecf1e 100644 --- a/packages/mobile/locales/en-US/global.json +++ b/packages/mobile/locales/en-US/global.json @@ -66,7 +66,9 @@ "importContactsFailed": "Failed to import contacts", "sendPaymentFailed": "Failure sending payment", "paymentRequestFailed": "Payment Request did not send", - "reclaimingEscrowedPaymentFailed": "Payment could not be reclaimed.", + "escrowTransferFailed": "Transfer to escrow failed", + "escrowWithdrawalFailed": "Withdrawal from escrow failed", + "reclaimingEscrowedPaymentFailed": "Payment could not be reclaimed", "connectingToCelo": "Connecting to the Celo Network...", "poorConnection": { "0": "Bad Connection", @@ -80,5 +82,9 @@ "thisVersionIsInsecure": "This version is insecure", "update": "Update", "qrFailedNoAddress": "QR code read failed. Reason: wallet address not found.", - "qrFailedInvalidAddress": "QR code read failed. Reason: wallet address invalid." + "qrFailedInvalidAddress": "QR code read failed. Reason: wallet address invalid.", + "corruptedChainDeleted": "Corrupted chain data has been deleted, please restart the app", + "web3FailedToSync": "Failing to sync, check your network connection", + "errorDuringSync": "Error occurred during sync, please try again later", + "calculateFeeFailed": "Could not calculate fee" } diff --git a/packages/mobile/locales/en-US/nuxNamePin1.json b/packages/mobile/locales/en-US/nuxNamePin1.json index 1665b45ca0a..b1dbb350ba1 100644 --- a/packages/mobile/locales/en-US/nuxNamePin1.json +++ b/packages/mobile/locales/en-US/nuxNamePin1.json @@ -92,7 +92,7 @@ "1": "Importing your contacts allows you to easily send and request payments from your community and see who is already using Celo.", "enable": "Enable Contact Access", - "loading": "Importing your contacts..." + "loading": "Finding friends on Celo..." }, "skip": "Skip" } diff --git a/packages/mobile/locales/en-US/sendFlow7.json b/packages/mobile/locales/en-US/sendFlow7.json index 2d9349dd6f5..21e8a3e552d 100644 --- a/packages/mobile/locales/en-US/sendFlow7.json +++ b/packages/mobile/locales/en-US/sendFlow7.json @@ -11,8 +11,6 @@ "characterLimitExceeded": "{{max}} Character Limit Exceeded", "invite": "Invite", "payRequest": "Pay Request", - "tryEnteringPhoneNumber": - "Try entering a phone number if you are unable to find who you're looking for", "sendTo": "Send to", "amount": "Amount", "celoDollarsAvailable": "Celo Dollars Available", @@ -32,11 +30,12 @@ "exchange": "Exchange", "inviteSendTo": "Invite & send to", "keepMoneySafe": "We'll keep this money safe until your friend creates a Celo account", - "searchFriends": "Try entering a phone number if you are unable to find who you’re looking for", + "searchFriends": "Try entering a phone number if you are unable to find who you're looking for", "inviteVerifyPayment": "Invite & Verify Payment", "total": "Total", "inviteFriends": "Invite Friends", "noResultsFor": "No results for", + "noContacts": "No contacts found", "searchForSomeone": "Search for someone using their name or phone number", "nameOrPhoneNumber": "Name or Phone Number", "pending": "Pending", @@ -48,6 +47,7 @@ "inviteSent": "Invite code sent!", "inviteFailed": "Failure sending invite", "loadingContacts": "Finding your contacts", + "QRCode": "QR Code", "saveCodeImage": "Save Code Image", "scanCode": "Scan Code", "writeStorageNeededForQrDownload": @@ -60,5 +60,6 @@ "reclaimPayment": "Reclaim Payment", "totalSent": "Total Sent", "totalRefunded": "Total Refunded", - "loadingVerificationStatus": "Checking recipient verfication..." + "loadingVerificationStatus": "Checking recipient verification...", + "askForContactsPermissionAction": "Get my Contacts" } diff --git a/packages/mobile/locales/es-AR/backupKeyFlow6.json b/packages/mobile/locales/es-AR/backupKeyFlow6.json index a2ac6d7cded..600acc54d44 100755 --- a/packages/mobile/locales/es-AR/backupKeyFlow6.json +++ b/packages/mobile/locales/es-AR/backupKeyFlow6.json @@ -11,9 +11,9 @@ "cancel": "Cancelar", "backupKeyImportance": { "0": - "Si pierde su teléfono o elimina la aplicación de Celo, perderá todo el oro y los dólares de su billetera.", + "Si pierde su teléfono o elimina la aplicación de Celo, perderá todo el oro y los dólares de su monedero.", "1": - "Puede hacer un respaldo de la billetera si escribe una clave de respaldo en un papel y la guarda en algún sitio seguro. Así, podrá restaurar la billetera en el futuro de ser necesario.", + "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 su teléfono. Asegúrese de escribir la clave de respaldo a mano y mantenerla segura." }, @@ -32,7 +32,7 @@ "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 su billetera." + "1": "No le muestre la frase a nadie. Si alguien la sabe, podrá acceder a su monedero." }, "learnYourKey": "Escriba o memorice su clave de respaldo.", "keyWillBeVerified": @@ -75,10 +75,10 @@ "seeBackupKey": "Ver clave de respaldo", "backupKeySet": "Clave de respaldo creada", "dontLoseIt": - "No pierda esta clave. Es de suma importancia que la mantenga en un lugar seguro, ya que es la única forma de desbloquear su billetera si pierde su celular.", + "No pierda esta clave. Es de suma importancia que la mantenga en un lugar seguro, ya que es la única forma de desbloquear su monedero si pierde su celular.", "done": "Listo", "whatsappMessage": - "Importante: por favor mantenga esto privado. \n\nTe estoy enviando la frase de respaldo a mi Billetera de Celo: ", + "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, su cuenta está congelada hasta que obtenga su clave de respaldo", "copyToClipboard": "Copiar al portapapeles", diff --git a/packages/mobile/locales/es-AR/global.json b/packages/mobile/locales/es-AR/global.json index c371629f3e5..e578c34cf4f 100755 --- a/packages/mobile/locales/es-AR/global.json +++ b/packages/mobile/locales/es-AR/global.json @@ -8,7 +8,7 @@ "next": "Siguiente", "downloadRewards": "Descargar Recompensas Celo", "chooseLanguage": "Elegir idioma", - "wallet": "Billetera", + "wallet": "Monedero", "payments": "Pagos", "send": "Envío", "exchange": "Cambio", @@ -30,9 +30,9 @@ "manageCeloDollars": "Administrar Celo Dólares", "sendCeloDollars": "Enviar Celo Dólares", "startEarning": "Comenzar a ganar", - "backToWallet": "Volver a la Billetera", + "backToWallet": "Volver a el Monedero", "exchangeForGold": "Cambiar a Oro", - "restoreCeloWallet": "Restaurar la billetera de Celo", + "restoreCeloWallet": "Restaurar el Monedero Celo", "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", @@ -62,12 +62,14 @@ "restartApp": "Reiniciar la aplicación", "loading": "Cargando…", "invalidBackup": "Clave de respaldo inválida", - "importBackupFailed": "No se pudo importar la billetera", + "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", "paymentRequestFailed": "No se pudo enviar la solicitud de pago", - "reclaimingEscrowedPaymentFailed": "El pago no pudo ser reclamado.", + "escrowTransferFailed": "Transferencia a la custodia falló", + "escrowWithdrawalFailed": "Retirada de la custodia fallida", + "reclaimingEscrowedPaymentFailed": "El pago no pudo ser reclamado", "connectingToCelo": "Conectando a la red de Celo...", "poorConnection": { "0": "Mala Conexión", @@ -80,7 +82,11 @@ "thisVersionIsInsecure": "Esta versión es insegura.", "update": "Actualizar", "qrFailedNoAddress": - "Código QR no se pudo leer. Motivo: no se encontró la dirección de la billetera.", + "Código QR no se pudo leer. Motivo: no se encontró la dirección de el monedero.", "qrFailedInvalidAddress": - "Código QR no se pudo leer. Razón: la dirección de la cartera no es válida." + "Código QR no se pudo leer. Razón: la dirección de la cartera no es válida.", + "corruptedChainDeleted": "Se ha borrado información corrupta, por favor reinicie la applicación", + "web3FailedToSync": "Fallo la sincronización, por favor verifique su conexión", + "errorDuringSync": "Ocurrió un error duranet la sincronización, por favor intente más tarde", + "calculateFeeFailed": "No se pudo calcular la comisión" } diff --git a/packages/mobile/locales/es-AR/nuxCurrencyPhoto4.json b/packages/mobile/locales/es-AR/nuxCurrencyPhoto4.json index c4a15a1d72b..4c8e541df89 100755 --- a/packages/mobile/locales/es-AR/nuxCurrencyPhoto4.json +++ b/packages/mobile/locales/es-AR/nuxCurrencyPhoto4.json @@ -12,7 +12,7 @@ "feeTransaction": "El envío de transacciones tiene una pequeña comisión", "sendCelo": "Enviar Celo Dólares a cualquier persona con un celular", "sendCeloDollars": "Enviar Celo Dólares", - "backToWallet": "Volver a la Billetera", + "backToWallet": "Volver a el Monedero", "celoLikeGold": "Celo Oro es como el oro real", "goldFluctuates": "Hay una cantidad limitada de Celo Oro, y su valor puede aumentar o disminuir", "exchange": "Cambie Oro a Dólares cuando quiera", @@ -20,8 +20,7 @@ "addressPhotos": "Celo usa sus contactos para mostrar fotos", "changePhotos": "Puede cambiar una foto si actualiza los contactos", "localPhotos": "Solo usted puede ver estas fotos desde su teléfono", - "allowContactAccess": - "Permita que la Billetera de Celo tenga acceso a las fotos de sus contactos", + "allowContactAccess": "Permita que el Monedero Celo tenga acceso a las fotos de sus contactos", "deny": "Denegar", "allow": "Permitir" } diff --git a/packages/mobile/locales/es-AR/nuxNamePin1.json b/packages/mobile/locales/es-AR/nuxNamePin1.json index 0268c0a23a2..ce3cc1af68b 100755 --- a/packages/mobile/locales/es-AR/nuxNamePin1.json +++ b/packages/mobile/locales/es-AR/nuxNamePin1.json @@ -13,7 +13,7 @@ "chooseCountryCode": "Código de país", "joinText": { "0": - "Bienvenido a Celo Wallet, una billetera digital que le permite enviar, recibir y guardar valor fácilmente.", + "Bienvenido a Celo Wallet, un monedero digital que le permite enviar, recibir y guardar valor fácilmente.", "1": "Al unirte a esta red, nos das permiso para recopilar información anónima sobre cómo utilizas la aplicación. Además, si verificas tu número de teléfono, se almacenará una copia ofuscada en la Red de Celo. Más información en", "2": "celo.org/terms" @@ -44,7 +44,7 @@ "askForInvite": { "0": "¿No tiene un código? ", "1": - "Solicite una invitación a alguien con Celo Wallet o regístrese para obtener una cuenta de testnet en ", + "Solicite una invitación a alguien con un Monedero Celo o regístrese para obtener una cuenta de testnet en", "2": "celo.org/build/wallet" } }, @@ -53,16 +53,16 @@ "InvitationCode": "Código de invitación", "optIn": "Inscribirse", "submitting": "Enviando ...", - "haveWallet": "¿Ya tiene una billetera de Celo? ", + "haveWallet": "¿Ya tiene un Monedero Celo? ", "importIt": "Importar", "cancel": "Cancelar", "important": "Importante", "createPin": { "title": "Crear un código PIN de 6 dígitos", - "intro": "Cree un PIN para proteger su billetera de Celo.", + "intro": "Cree un PIN para proteger su Monedero Celo.", "why": "En ciertos casos, necesitará este PIN para enviar Celo Dólares a sus amigos.", "warn": - "escriba su código PIN de 6 dígitos en un papel y guárdelo en algún sitio seguro. Se lo solicitaremos más tarde para acceder a su billetera.", + "escriba su código PIN de 6 dígitos en un papel y guárdelo en algún sitio seguro. Se lo solicitaremos más tarde para acceder a su monedero.", "yourPin": "Su PIN" }, "enableSecurity": "Habilitar seguridad", @@ -76,10 +76,12 @@ }, "systemAuth": { "title": "Configura la seguridad Celo", + "secure": + "Agregar un PIN a tu teléfono ayuda a que tus fondos en Celo se mantengan seguros y protegidos.", "intro": "Necesitaremos que ingreses el código de bloqueo de pantalla de tu dispositivo o tu huella, si el mismo lo soporta.", "why": - "Necesitamos este nivel de protección adicional para asegurar que el contenido de tu billetera se encuentra a salvo." + "Necesitamos este nivel de protección adicional para asegurar que el contenido de tu monedero se encuentra a salvo." }, "goToSystemSecuritySettingsActionLabel": "Vaya a seguridad", "enableSystemScreenLockFailedMessage": "Error al intentar habilitar el bloqueo de pantalla", @@ -93,7 +95,7 @@ "1": "Importar tus contactos te permite enviar y solicitar pagos fácilmente a tu comunidad y ver quién ya está usando Celo", "enable": "Habilitar acceso a contactos", - "loading": "Importar tus contactos..." + "loading": "Encontrar amigos en Celo..." }, "skip": "Saltar" } diff --git a/packages/mobile/locales/es-AR/nuxRestoreWallet3.json b/packages/mobile/locales/es-AR/nuxRestoreWallet3.json index d30d67df2f4..100cd0f810a 100755 --- a/packages/mobile/locales/es-AR/nuxRestoreWallet3.json +++ b/packages/mobile/locales/es-AR/nuxRestoreWallet3.json @@ -5,12 +5,12 @@ "fullName": "Nombre completo", "invitationCode": "Código de invitación", "submit": "Enviar", - "alreadyHaveWallet": "¿Ya tiene una billetera de Celo? Restáurela", - "restoreWallet": "Restaurar la billetera de Celo", + "alreadyHaveWallet": "¿Ya tiene un Monedero Celo? Restáurelo", + "restoreWallet": "Restaurar el Monedero Celo", "restoreYourWallet": { - "title": "Restaurar su billetera de Celo", + "title": "Restaurar su Monedero Celo", "userYourBackupKey": - "¿Ya tiene una billetera de Celo? Use su clave de respaldo segura para restaurar su billetera a este teléfono.", + "¿Ya tiene un Monedero Celo? Use su clave de respaldo segura para restaurar su monedero a este teléfono.", "warning": "¡Cuidado! ", "restoreInPrivate": "Hágalo en privado" }, diff --git a/packages/mobile/locales/es-AR/nuxVerification2.json b/packages/mobile/locales/es-AR/nuxVerification2.json index e4564a4c033..e286bde0e81 100755 --- a/packages/mobile/locales/es-AR/nuxVerification2.json +++ b/packages/mobile/locales/es-AR/nuxVerification2.json @@ -14,7 +14,7 @@ "country": "País", "phoneNumber": "Número de teléfono", "invalidPhone": "Número de teléfono inválido", - "allowSmsPermissions": "Permitir que la billetera de Celo envíe y vea mensajes SMS", + "allowSmsPermissions": "Permitir que el Monedero Celo envíe y vea mensajes de texto (SMS)", "dontAsk": "No volver a preguntar", "deny": "Denegar", "allow": "Permitir", diff --git a/packages/mobile/locales/es-AR/receiveFlow8.json b/packages/mobile/locales/es-AR/receiveFlow8.json index 4be26cc1831..aeb201db372 100755 --- a/packages/mobile/locales/es-AR/receiveFlow8.json +++ b/packages/mobile/locales/es-AR/receiveFlow8.json @@ -1,7 +1,7 @@ { "receivedPayment": "Pago recibido", "from": "de", - "celoWallet": "Billetera de Celo", + "celoWallet": "Monedero de Celo", "at": "a las", "receivedDollars": "Celo Dólares Recibidos", "verificationMessage": "La tasa de verificación fue pagada. ¡Bienvenido a Celo!", diff --git a/packages/mobile/locales/es-AR/sendFlow7.json b/packages/mobile/locales/es-AR/sendFlow7.json index c900d0f49c3..bc3f4ee0b68 100755 --- a/packages/mobile/locales/es-AR/sendFlow7.json +++ b/packages/mobile/locales/es-AR/sendFlow7.json @@ -11,7 +11,6 @@ "recent": "Recientes", "contacts": "Contactos", "invite": "Invitar", - "tryEnteringPhoneNumber": "Ingrese un número de teléfono si no encuentra a la persona que busca", "sendTo": "Enviar a", "amount": "Monto", "celoDollarsAvailable": "Celo Dólares disponibles", @@ -36,6 +35,7 @@ "total": "Total", "inviteFriends": "Invitar a amigos", "noResultsFor": "Sin resultados para", + "noContacts": "No se encontraron contactos", "searchForSomeone": "Busque a alguien según el nombre o el número de teléfono", "nameOrPhoneNumber": "Nombre o número de teléfono", "pending": "Pendiente", @@ -47,6 +47,7 @@ "inviteSent": "Código de invitación enviado!", "inviteFailed": "Falló el envío de la invitación", "loadingContacts": "Encontrando tus contactos", + "QRCode": "QR código", "saveCodeImage": "Guardar imagen de código", "scanCode": "Escanear código", "writeStorageNeededForQrDownload": @@ -60,5 +61,6 @@ "reclaimPayment": "Reclamar el Pago", "totalSent": "Total Enviado", "totalRefunded": "Total Reembolsado", - "loadingVerificationStatus": "Comprobando la verificación del destinatario..." + "loadingVerificationStatus": "Comprobando la verificación del destinatario...", + "askForContactsPermissionAction": "Recuperar mis contactos" } diff --git a/packages/mobile/locales/es-AR/walletFlow5.json b/packages/mobile/locales/es-AR/walletFlow5.json index 53f24c68f79..8e8ffb67544 100755 --- a/packages/mobile/locales/es-AR/walletFlow5.json +++ b/packages/mobile/locales/es-AR/walletFlow5.json @@ -11,7 +11,7 @@ "setBackupKey": "Configure su clave de respaldo para mejorar la seguridad y habilitar la recuperación de la cuenta", "activity": "Actividad", - "wallet": "Billetera", + "wallet": "Monedero", "send": "Envío", "sent": "Envió", "received": "Recibido", @@ -62,5 +62,5 @@ "1": "Un recordatorio amistoso de que está utilizando una compilación de Testnet - los saldos aquí no son reales." }, - "dismiss": "Despedir" + "dismiss": "Ocultar" } diff --git a/packages/mobile/locales/es.json b/packages/mobile/locales/es.json index f0b7f4f7ab3..e6c1894f262 100644 --- a/packages/mobile/locales/es.json +++ b/packages/mobile/locales/es.json @@ -1,5 +1,5 @@ { - "wallet": "Billetera", + "wallet": "Monedero", "send": "Enviar", "payment": "Pago", "exchange": "Intercambiar", diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 03f2af88245..595f61e39fe 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@celo/mobile", - "version": "1.3.2", + "version": "1.4.0", "author": "Celo", "license": "Apache-2.0", "private": true, @@ -9,11 +9,10 @@ "start:bg": "react-native start &", "start:android": "react-native run-android", "start:ios": "react-native run-ios", - "lint-checks": "yarn run lint && yarn run build:typescript --noEmit", "lint": "tslint -c tslint.json --project tsconfig.json", - "build": "yarn run build:typescript && yarn run build:metro", - "build:sdk": "build-sdk", - "build:typescript": "tsc", + "build": "yarn run build:ts && yarn run build:metro", + "build:sdk": "yarn --cwd ../walletkit build:for-env", + "build:ts": "tsc --noEmit", "build:metro": "echo 'NOT WORKING RIGHT NOW'", "build:gen-graphql-types": "gql-gen --schema http://localhost:8080/graphql --template graphql-codegen-typescript-template --out ./typings/ 'src/**/*.tsx'", "dev": "react-native run-android --appIdSuffix \"debug\"", @@ -24,9 +23,10 @@ "dev:send-debug-invite-code": "adb shell am broadcast -a com.android.vending.INSTALL_REFERRER -n org.celo.mobile.debug/com.androidbroadcastreceiverforreferrer.ReferrerBroadcastReceiver --es referrer \"invite-code%3D123\"", "dev:emulator": "emulator -avd Nexus_5X_API_28 &", "dev:remote": "remotedev --hostname=localhost --port=8000", - "test": "export TZ=UTC && jest --ci --silent --coverage --runInBand", - "test:verbose": "export TZ=UTC && jest --ci --verbose --runInBand", - "test:watch": "export TZ=UTC && jest --watch", + "test": "export TZ=UTC && jest --silent", + "test:ci": "yarn test --coverage --runInBand", + "test:watch": "yarn test --watch", + "test:verbose": "export TZ=UTC && jest --verbose", "test:build-e2e": "./scripts/build_e2e.sh", "test:run-e2e": "./scripts/run_e2e.sh", "test:detox": "CELO_TEST_CONFIG=e2e detox test -c android.emu.debug -a e2e/tmp/ --take-screenshots=failing --record-logs=failing --detectOpenHandles", @@ -35,7 +35,8 @@ "prepare": "patch-package", "postinstall": "sh scripts/fix_rn_version.sh; patch-package", "update-disclaimer": "yarn licenses generate-disclaimer > LicenseDisclaimer.txt && mkdir -p android/app/src/main/assets/custom && cp LicenseDisclaimer.txt android/app/src/main/assets/custom/LicenseDisclaimer.txt", - "test-licenses": "yarn licenses list --prod | grep '\\(─ GPL\\|─ (GPL-[1-9]\\.[0-9]\\+ OR GPL-[1-9]\\.[0-9]\\+)\\)' && echo 'Found GPL license(s). Use 'yarn licenses list --prod' to look up the offending package' || echo 'No GPL licenses found'" + "test-licenses": "yarn licenses list --prod | grep '\\(─ GPL\\|─ (GPL-[1-9]\\.[0-9]\\+ OR GPL-[1-9]\\.[0-9]\\+)\\)' && echo 'Found GPL license(s). Use 'yarn licenses list --prod' to look up the offending package' || echo 'No GPL licenses found'", + "verify-locales": "./scripts/verify_locales.sh" }, "rnpm": { "assets": [ @@ -43,11 +44,10 @@ ] }, "dependencies": { - "@celo/client": "4fd835d", - "@celo/client-integration": "npm:@celo/client@55cf94c", - "@celo/contractkit": "0.0.1", + "@celo/client": "f7095b7", + "@celo/walletkit": "^0.0.4", "@celo/react-native-sms-retriever": "git+https://github.com/celo-org/react-native-sms-retriever#d3a2fdb", - "@celo/utils": "0.0.1", + "@celo/utils": "^0.0.5", "@react-native-community/netinfo": "^2.0.4", "@segment/analytics-react-native": "^0.1.0-beta.0", "@segment/analytics-react-native-firebase": "^0.1.0-beta.0", @@ -76,6 +76,7 @@ "numeral": "^2.0.6", "react": "16.8.3", "react-apollo": "^2.4.1", + "react-async-hook": "^3.4.0", "react-i18next": "^8.3.8", "react-native": "0.59.10", "react-native-android-broadcast-receiver-for-referrer": "^1.0.7", diff --git a/packages/mobile/rn-cli.config.js b/packages/mobile/rn-cli.config.js index 33eeae82d4c..f17315d3837 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|contractkit|react-components).*` + `${escapedRoot}\/packages\/(?!mobile|utils|walletkit|react-components).*` ) const nestedRnRegex = new RegExp(`.*\/node_modules\/.*\/node_modules\/(react-native)\/.*`) const componentsRnRegex = new RegExp(`.*react-components\/node_modules\/(react-native)\/.*`) diff --git a/packages/mobile/scripts/verify_locales.sh b/packages/mobile/scripts/verify_locales.sh new file mode 100755 index 00000000000..8710a934f99 --- /dev/null +++ b/packages/mobile/scripts/verify_locales.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +if ! [ -x "$(command -v jq)" ]; then + echo "Error: jq is not installed." >&2 + exit 1 +fi + +cd "$(dirname "${BASH_SOURCE[0]}")" + +filter='paths | join(".") | [(input_filename | gsub(".*/|\\.json$";"")), .] | join("/")' + +en_keys=$(jq -r "$filter" ../locales/en-US/*.json | sort) +es_keys=$(jq -r "$filter" ../locales/es-AR/*.json | sort) + +diff <(echo "$en_keys") <(echo "$es_keys") + +exit $? diff --git a/packages/mobile/src/account/Account.tsx b/packages/mobile/src/account/Account.tsx index 72f99e3a17b..c62071de0c2 100644 --- a/packages/mobile/src/account/Account.tsx +++ b/packages/mobile/src/account/Account.tsx @@ -14,12 +14,12 @@ import SettingsItem from 'src/account/SettingsItem' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' import { resetAppOpenedState, setAnalyticsEnabled, setNumberVerified } from 'src/app/actions' -import BackButton from 'src/components/BackButton' import { FAQ_LINK, TOS_LINK } from 'src/config' import { features } from 'src/flags' import { Namespaces } from 'src/i18n' import { revokeVerification } from 'src/identity/actions' import { isPhoneNumberVerified } from 'src/identity/verification' +import { headerWithBackButton } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { RootState } from 'src/redux/reducers' @@ -66,13 +66,7 @@ const mapDispatchToProps = { } export class Account extends React.Component { - static navigationOptions = { - headerStyle: { - elevation: 0, - }, - headerLeftContainerStyle: { paddingHorizontal: 20 }, - headerLeft: , - } + static navigationOptions = headerWithBackButton state: State = { verified: undefined, diff --git a/packages/mobile/src/account/AccountInfo.tsx b/packages/mobile/src/account/AccountInfo.tsx index dde5b281ad7..c9472e0e0ba 100644 --- a/packages/mobile/src/account/AccountInfo.tsx +++ b/packages/mobile/src/account/AccountInfo.tsx @@ -1,19 +1,15 @@ import ContactCircle from '@celo/react-components/components/ContactCircle' import PhoneNumberWithFlag from '@celo/react-components/components/PhoneNumberWithFlag' -import PulsingDot from '@celo/react-components/components/PulsingDot' import QRCode from '@celo/react-components/icons/QRCode' -import colors from '@celo/react-components/styles/colors' import { fontStyles } from '@celo/react-components/styles/fonts' import * as React from 'react' import { StyleSheet, Text, TouchableOpacity, View } from 'react-native' import { connect } from 'react-redux' import { devModeTriggerClicked } from 'src/account/actions' -import { CTA_CIRCLE_SIZE } from 'src/account/Education' import { getUserContactDetails, UserContactDetails } from 'src/account/reducer' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' import { componentWithAnalytics } from 'src/analytics/wrapper' -import { isE2EEnv } from 'src/config' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { RootState } from 'src/redux/reducers' @@ -61,7 +57,7 @@ export class AccountInfo extends React.Component { } render() { - const { name, e164Number, photosNUXClicked, userContact, defaultCountryCode } = this.props + const { name, e164Number, userContact, defaultCountryCode } = this.props return ( @@ -75,14 +71,6 @@ export class AccountInfo extends React.Component { - {!photosNUXClicked && ( - - )} {!!name && ( { } } -export class Analytics extends React.PureComponent { - static navigationOptions = { - headerLeft: , - title: i18n.t('accountScreen10:analytics'), - headerRight: , - headerTitleStyle: [fontStyles.headerTitle, componentStyles.screenHeader], - } +export class Analytics extends React.Component { + static navigationOptions = () => ({ + ...headerWithCancelButton, + headerTitle: i18n.t('accountScreen10:analytics'), + }) render() { const { analyticsEnabled, t } = this.props diff --git a/packages/mobile/src/account/DollarEducation.test.tsx b/packages/mobile/src/account/DollarEducation.test.tsx index a1809330241..4f5f43096bf 100644 --- a/packages/mobile/src/account/DollarEducation.test.tsx +++ b/packages/mobile/src/account/DollarEducation.test.tsx @@ -1,23 +1,9 @@ -const { mockNavigationServiceFor } = require('test/utils') -const { navigateBack, navigate } = mockNavigationServiceFor('DollarEducation') - -import { shallow } from 'enzyme' import * as React from 'react' import 'react-native' import { Provider } from 'react-redux' import * as renderer from 'react-test-renderer' -import DollarEducation, { DollarEducation as DollarEducationRaw } from 'src/account/DollarEducation' -import Education from 'src/account/Education' -import { setEducationCompleted } from 'src/stableToken/actions' +import DollarEducation from 'src/account/DollarEducation' import { createMockStore } from 'test/utils' -const dollarEducationFactory = ( - simulateTarget: string, - setEduComplete: typeof setEducationCompleted = jest.fn() -) => { - const dollarEducation = shallow() - dollarEducation.find(Education).simulate(simulateTarget) - return dollarEducation -} describe('DollarEducation', () => { it('renders correctly', () => { @@ -28,29 +14,4 @@ describe('DollarEducation', () => { ) expect(tree).toMatchSnapshot() }) - - describe('#goToSend', () => { - it('sets EducationCompleted', () => { - const setEduComplete = jest.fn() - dollarEducationFactory('finish', setEduComplete) - expect(setEduComplete).toBeCalled() - }) - it('navigates to SendStack', () => { - dollarEducationFactory('finish') - expect(navigate).toBeCalled() - }) - }) - - describe('#goToWallet', () => { - it('sets EducationCompleted', () => { - const setEduComplete = jest.fn() - dollarEducationFactory('finishAlternate', setEduComplete) - expect(setEduComplete).toBeCalled() - }) - it('navigatesBack', () => { - const setEduComplete = jest.fn() - dollarEducationFactory('finishAlternate', setEduComplete) - expect(navigateBack).toBeCalled() - }) - }) }) diff --git a/packages/mobile/src/account/DollarEducation.tsx b/packages/mobile/src/account/DollarEducation.tsx index b22d9772c2a..067ba67b3f9 100644 --- a/packages/mobile/src/account/DollarEducation.tsx +++ b/packages/mobile/src/account/DollarEducation.tsx @@ -5,7 +5,7 @@ import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' import { componentWithAnalytics } from 'src/analytics/wrapper' import { sendBetweenPhones, sendFee, stabilityScale } from 'src/images/Images' -import { navigate, navigateBack } from 'src/navigator/NavigationService' +import { navigate, navigateHome } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { setEducationCompleted } from 'src/stableToken/actions' @@ -21,14 +21,15 @@ export class DollarEducation extends React.Component { goToSend = () => { this.props.setEducationCompleted() CeloAnalytics.track(CustomEventNames.send_dollar_nux) - navigate(Screens.SendStack) + navigate(Screens.Send) } - goToWallet = () => { + goToWalletHome = () => { this.props.setEducationCompleted() CeloAnalytics.track(CustomEventNames.wallet_dollar_nux) - navigateBack() + navigateHome() } + render() { const stepInfo = [ { @@ -54,7 +55,7 @@ export class DollarEducation extends React.Component { diff --git a/packages/mobile/src/account/EditProfile.tsx b/packages/mobile/src/account/EditProfile.tsx index d880fa0a7f8..0b0fe4c3cb9 100644 --- a/packages/mobile/src/account/EditProfile.tsx +++ b/packages/mobile/src/account/EditProfile.tsx @@ -9,8 +9,8 @@ import { connect } from 'react-redux' import { setName } from 'src/account/actions' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' -import CancelButton from 'src/components/CancelButton' import { Namespaces } from 'src/i18n' +import { headerWithCancelButton } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { RootState } from 'src/redux/reducers' @@ -32,9 +32,7 @@ const mapStateToProps = (state: RootState): StateProps => { } export class EditProfile extends React.Component { - static navigationOptions = { - headerLeft: , - } + static navigationOptions = headerWithCancelButton state = { name: this.props.name, diff --git a/packages/mobile/src/account/GoldEducation.test.tsx b/packages/mobile/src/account/GoldEducation.test.tsx index 4fef944d08f..59fd095b407 100644 --- a/packages/mobile/src/account/GoldEducation.test.tsx +++ b/packages/mobile/src/account/GoldEducation.test.tsx @@ -1,25 +1,10 @@ -const { mockNavigationServiceFor } = require('test/utils') -const { navigateBack, navigate } = mockNavigationServiceFor('GoldEducation') - -import { shallow } from 'enzyme' import * as React from 'react' import 'react-native' import { Provider } from 'react-redux' import * as renderer from 'react-test-renderer' -import Education from 'src/account/Education' -import GoldEducation, { GoldEducation as GoldEducationRaw } from 'src/account/GoldEducation' -import { setEducationCompleted } from 'src/goldToken/actions' +import GoldEducation from 'src/account/GoldEducation' import { createMockStore } from 'test/utils' -const goldEducationFactory = ( - simulateTarget: string, - setEduComplete: typeof setEducationCompleted = jest.fn() -) => { - const goldEducation = shallow() - goldEducation.find(Education).simulate(simulateTarget) - return goldEducation -} - describe('GoldEducation', () => { it('renders correctly', () => { const tree = renderer.create( @@ -29,29 +14,4 @@ describe('GoldEducation', () => { ) expect(tree).toMatchSnapshot() }) - - describe('#goToSend', () => { - it('sets EducationCompleted', () => { - const setEduComplete = jest.fn() - goldEducationFactory('finish', setEduComplete) - expect(setEduComplete).toBeCalled() - }) - it('navigates to SendStack', () => { - goldEducationFactory('finish') - expect(navigate).toBeCalled() - }) - }) - - describe('#goToWallet', () => { - it('sets EducationCompleted', () => { - const setEduComplete = jest.fn() - goldEducationFactory('finishAlternate', setEduComplete) - expect(setEduComplete).toBeCalled() - }) - it('navigatesBack', () => { - const setEduComplete = jest.fn() - goldEducationFactory('finishAlternate', setEduComplete) - expect(navigateBack).toBeCalled() - }) - }) }) diff --git a/packages/mobile/src/account/GoldEducation.tsx b/packages/mobile/src/account/GoldEducation.tsx index 74ea52c0943..188c8539bc9 100644 --- a/packages/mobile/src/account/GoldEducation.tsx +++ b/packages/mobile/src/account/GoldEducation.tsx @@ -6,7 +6,7 @@ import { CustomEventNames } from 'src/analytics/constants' import { componentWithAnalytics } from 'src/analytics/wrapper' import { setEducationCompleted } from 'src/goldToken/actions' import { exchangeIcon, goldValue, shinyGold } from 'src/images/Images' -import { navigate, navigateBack } from 'src/navigator/NavigationService' +import { navigate, navigateHome } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' interface DispatchProps { @@ -15,17 +15,19 @@ interface DispatchProps { type Props = DispatchProps export class GoldEducation extends React.Component { static navigationOptions = { header: null } + goToExchange = () => { this.props.setEducationCompleted() CeloAnalytics.track(CustomEventNames.exchange_gold_nux) navigate(Screens.ExchangeHomeScreen) } - goToWallet = () => { + goToWalletHome = () => { this.props.setEducationCompleted() CeloAnalytics.track(CustomEventNames.wallet_gold_nux) - navigateBack() + navigateHome() } + render() { const stepInfo = [ { @@ -51,7 +53,7 @@ export class GoldEducation extends React.Component { diff --git a/packages/mobile/src/account/Invite.test.tsx b/packages/mobile/src/account/Invite.test.tsx index 5cd36c5c55f..2587fddf254 100644 --- a/packages/mobile/src/account/Invite.test.tsx +++ b/packages/mobile/src/account/Invite.test.tsx @@ -4,10 +4,25 @@ import { Provider } from 'react-redux' import * as renderer from 'react-test-renderer' import Invite from 'src/account/Invite' import { createMockStore } from 'test/utils' -import { mockNavigation } from 'test/values' +import { mockE164NumberToInvitableRecipient, mockNavigation } from 'test/values' describe('Invite', () => { - it('renders correctly', () => { + it('renders correctly with recipients', () => { + const tree = renderer.create( + + {/* + // @ts-ignore */} + + + ) + expect(tree).toMatchSnapshot() + }) + + it('renders correctly with no recipients', () => { const tree = renderer.create( {/* diff --git a/packages/mobile/src/account/Invite.tsx b/packages/mobile/src/account/Invite.tsx index b010457ae07..bf49abe63aa 100644 --- a/packages/mobile/src/account/Invite.tsx +++ b/packages/mobile/src/account/Invite.tsx @@ -1,10 +1,8 @@ import colors from '@celo/react-components/styles/colors' -import { fontStyles } from '@celo/react-components/styles/fonts' -import { componentStyles } from '@celo/react-components/styles/styles' import * as React from 'react' import { withNamespaces, WithNamespaces } from 'react-i18next' import { StyleSheet, View } from 'react-native' -import { NavigationInjectedProps, NavigationScreenProps, withNavigation } from 'react-navigation' +import { NavigationInjectedProps, withNavigation } from 'react-navigation' import { connect } from 'react-redux' import { defaultCountryCodeSelector } from 'src/account/reducer' import { hideAlert, showError } from 'src/alert/actions' @@ -12,19 +10,22 @@ import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' import { componentWithAnalytics } from 'src/analytics/wrapper' import { ErrorMessages } from 'src/app/ErrorMessages' -import CancelButton from 'src/components/CancelButton' -import { ERROR_BANNER_DURATION } from 'src/config' -import { Namespaces } from 'src/i18n' +import { ALERT_BANNER_DURATION } from 'src/config' +import i18n, { Namespaces } from 'src/i18n' +import { importContacts } from 'src/identity/actions' import { e164NumberToAddressSelector, E164NumberToAddressType } from 'src/identity/reducer' +import { headerWithCancelButton } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' +import { filterRecipients, NumberToRecipient, Recipient } from 'src/recipients/recipient' +import RecipientPicker from 'src/recipients/RecipientPicker' +import { recipientCacheSelector } from 'src/recipients/reducer' import { RootState } from 'src/redux/reducers' -import RecipientPicker from 'src/send/RecipientPicker' -import { recipientCacheSelector } from 'src/send/reducers' -import { filterRecipients, NumberToRecipient, Recipient } from 'src/utils/recipient' +import { checkContactsPermission } from 'src/utils/androidPermissions' interface State { searchQuery: string + hasGivenPermission: boolean } interface Section { @@ -41,6 +42,13 @@ interface StateProps { interface DispatchProps { showError: typeof showError hideAlert: typeof hideAlert + importContacts: typeof importContacts +} + +const mapDispatchToProps = { + showError, + hideAlert, + importContacts, } type Props = StateProps & DispatchProps & WithNamespaces & NavigationInjectedProps @@ -52,22 +60,16 @@ const mapStateToProps = (state: RootState): StateProps => ({ }) class Invite extends React.Component { - static navigationOptions = ({ navigation }: NavigationScreenProps) => ({ - headerStyle: { - elevation: 0, - }, - headerTitle: navigation.getParam('title', ''), - headerTitleStyle: [fontStyles.headerTitle, componentStyles.screenHeader], - headerRight: , // This helps vertically center the title - headerLeft: , + static navigationOptions = () => ({ + ...headerWithCancelButton, + headerTitle: i18n.t('sendFlow7:invite'), }) - state: State = { - searchQuery: '', - } + state: State = { searchQuery: '', hasGivenPermission: true } async componentDidMount() { - this.props.navigation.setParams({ title: this.props.t('invite') }) + const granted = await checkContactsPermission() + this.setState({ hasGivenPermission: granted }) } updateToField = (value: string) => { @@ -84,7 +86,7 @@ class Invite extends React.Component { CeloAnalytics.track(CustomEventNames.friend_invited) navigate(Screens.InviteReview, { recipient }) } else { - this.props.showError(ErrorMessages.CANT_SELECT_INVALID_PHONE, ERROR_BANNER_DURATION) + this.props.showError(ErrorMessages.CANT_SELECT_INVALID_PHONE, ALERT_BANNER_DURATION) } } @@ -105,6 +107,11 @@ class Invite extends React.Component { .filter((section) => section.data.length > 0) } + onPermissionsAccepted = async () => { + this.props.importContacts() + this.setState({ hasGivenPermission: true }) + } + render() { return ( @@ -112,9 +119,11 @@ class Invite extends React.Component { sections={this.buildSections()} searchQuery={this.state.searchQuery} defaultCountryCode={this.props.defaultCountryCode} + hasAcceptedContactPermission={this.state.hasGivenPermission} onSelectRecipient={this.onSelectRecipient} onSearchQueryChanged={this.onSearchQueryChanged} showQRCode={false} + onPermissionsAccepted={this.onPermissionsAccepted} /> ) @@ -138,11 +147,8 @@ const style = StyleSheet.create({ }) export default componentWithAnalytics( - connect( + connect( mapStateToProps, - { - showError, - hideAlert, - } + mapDispatchToProps )(withNamespaces(Namespaces.sendFlow7)(withNavigation(Invite))) ) diff --git a/packages/mobile/src/account/InviteReview.test.tsx b/packages/mobile/src/account/InviteReview.test.tsx index c05746003fd..9db23d9bac7 100644 --- a/packages/mobile/src/account/InviteReview.test.tsx +++ b/packages/mobile/src/account/InviteReview.test.tsx @@ -14,6 +14,10 @@ jest.mock('src/geth/GethAwareButton', () => { return Button }) +jest.mock('src/identity/verification', () => { + return { isPhoneVerified: jest.fn(() => true) } +}) + describe('InviteReview', () => { it('renders correctly', () => { const tree = renderer.create( diff --git a/packages/mobile/src/account/InviteReview.tsx b/packages/mobile/src/account/InviteReview.tsx index 972039467dc..9452d14a47f 100644 --- a/packages/mobile/src/account/InviteReview.tsx +++ b/packages/mobile/src/account/InviteReview.tsx @@ -13,7 +13,7 @@ import { hideAlert, showError } from 'src/alert/actions' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' import componentWithAnalytics from 'src/analytics/wrapper' -import { ERROR_BANNER_DURATION } from 'src/config' +import { ALERT_BANNER_DURATION } from 'src/config' import GethAwareButton from 'src/geth/GethAwareButton' import { Namespaces } from 'src/i18n' import SMSLogo from 'src/icons/InviteSendReceive' @@ -21,11 +21,11 @@ import WhatsAppLogo from 'src/icons/WhatsAppLogo' import { isPhoneNumberVerified } from 'src/identity/verification' import { InviteBy, sendInvite } from 'src/invite/actions' import { navigateBack } from 'src/navigator/NavigationService' +import { Recipient } from 'src/recipients/recipient' import { RootState } from 'src/redux/reducers' import TransferConfirmationCard from 'src/send/TransferConfirmationCard' import { fetchDollarBalance } from 'src/stableToken/actions' import { TransactionTypes } from 'src/transactions/reducer' -import { Recipient } from 'src/utils/recipient' interface State { contactIsVerified: boolean @@ -114,7 +114,7 @@ export class InviteReview extends React.Component { this.props.hideAlert() if (!this.state.amountIsValid) { - this.props.showError(this.props.t('needMoreFundsToInvite'), ERROR_BANNER_DURATION) + this.props.showError(this.props.t('needMoreFundsToInvite'), ALERT_BANNER_DURATION) return } diff --git a/packages/mobile/src/account/Licenses.tsx b/packages/mobile/src/account/Licenses.tsx index d15eba02c54..da7e7480b7e 100644 --- a/packages/mobile/src/account/Licenses.tsx +++ b/packages/mobile/src/account/Licenses.tsx @@ -1,13 +1,10 @@ -import fontStyles from '@celo/react-components/styles/fonts' -import { componentStyles } from '@celo/react-components/styles/styles' import * as React from 'react' import { withNamespaces, WithNamespaces } from 'react-i18next' -import { Platform, StyleSheet, View } from 'react-native' +import { Platform, StyleSheet } from 'react-native' import { WebView } from 'react-native-webview' -import { NavigationScreenProps } from 'react-navigation' import componentWithAnalytics from 'src/analytics/wrapper' -import BackButton from 'src/components/BackButton' import i18n, { Namespaces } from 'src/i18n' +import { headerWithBackButton } from 'src/navigator/Headers' const licenseURI = Platform.select({ ios: './LicenseDisclaimer.txt', // For when iOS is implemented! @@ -17,14 +14,9 @@ const licenseURI = Platform.select({ type Props = {} & WithNamespaces class Licenses extends React.Component { - static navigationOptions = ({ navigation }: NavigationScreenProps) => ({ - headerStyle: { - elevation: 0, - }, + static navigationOptions = () => ({ + ...headerWithBackButton, headerTitle: i18n.t('accountScreen10:licenses'), - headerTitleStyle: [fontStyles.headerTitle, componentStyles.screenHeader], - headerRight: , // This helps vertically center the title - headerLeft: , }) render() { diff --git a/packages/mobile/src/account/PhotosEducation.test.tsx b/packages/mobile/src/account/PhotosEducation.test.tsx index 198ae80ccf5..c64dbb88fa7 100644 --- a/packages/mobile/src/account/PhotosEducation.test.tsx +++ b/packages/mobile/src/account/PhotosEducation.test.tsx @@ -1,13 +1,8 @@ -const { mockNavigationServiceFor } = require('test/utils') -const { navigateBack } = mockNavigationServiceFor('PhotosEducation') - -import { shallow } from 'enzyme' import * as React from 'react' import 'react-native' import { Provider } from 'react-redux' import * as renderer from 'react-test-renderer' -import Education from 'src/account/Education' -import PhotosEducation, { PhotosEducation as PhotosEducationRaw } from 'src/account/PhotosEducation' +import PhotosEducation from 'src/account/PhotosEducation' import { createMockStore } from 'test/utils' describe('PhotosEducation', () => { @@ -19,14 +14,4 @@ describe('PhotosEducation', () => { ) expect(tree).toMatchSnapshot() }) - - describe('behavior for finishing', () => { - it('sets photosNUXCompleted and navigates back', () => { - const setComplete = jest.fn() - const photoEd = shallow() - photoEd.find(Education).simulate('finish') - expect(setComplete).toBeCalled() - expect(navigateBack).toBeCalled() - }) - }) }) diff --git a/packages/mobile/src/account/PhotosEducation.tsx b/packages/mobile/src/account/PhotosEducation.tsx index b02a3854b5e..33b5c936499 100644 --- a/packages/mobile/src/account/PhotosEducation.tsx +++ b/packages/mobile/src/account/PhotosEducation.tsx @@ -5,7 +5,7 @@ import Education from 'src/account/Education' import { CustomEventNames } from 'src/analytics/constants' import { componentWithAnalytics } from 'src/analytics/wrapper' import { addressBook, bigPhoneAvatar, cameraUpload } from 'src/images/Images' -import { navigateBack } from 'src/navigator/NavigationService' +import { navigateHome } from 'src/navigator/NavigationService' interface DispatchProps { photosNUXCompleted: typeof photosNUXCompleted @@ -14,9 +14,10 @@ type Props = DispatchProps export class PhotosEducation extends React.Component { static navigationOptions = { header: null } - goToWallet = () => { + + goToWalletHome = () => { this.props.photosNUXCompleted() - navigateBack() + navigateHome() } render() { @@ -40,7 +41,9 @@ export class PhotosEducation extends React.Component { screenName: 'Photo_Nux_3', }, ] - return + return ( + + ) } } diff --git a/packages/mobile/src/account/Profile.tsx b/packages/mobile/src/account/Profile.tsx index 81f90888202..eb8e5f7e770 100644 --- a/packages/mobile/src/account/Profile.tsx +++ b/packages/mobile/src/account/Profile.tsx @@ -7,8 +7,8 @@ import { getUserContactDetails, UserContactDetails } from 'src/account/reducer' import SettingsItem from 'src/account/SettingsItem' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' -import CancelButton from 'src/components/CancelButton' import { Namespaces } from 'src/i18n' +import { headerWithCancelButton } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { RootState } from 'src/redux/reducers' @@ -31,9 +31,7 @@ const mapStateToProps = (state: RootState) => { } export class Profile extends React.Component { - static navigationOptions = { - headerLeft: , - } + static navigationOptions = headerWithCancelButton goToEditProfile = () => { CeloAnalytics.track(CustomEventNames.edit_name) diff --git a/packages/mobile/src/account/__snapshots__/Account.test.tsx.snap b/packages/mobile/src/account/__snapshots__/Account.test.tsx.snap index fbff11212b5..42d029fc3d5 100644 --- a/packages/mobile/src/account/__snapshots__/Account.test.tsx.snap +++ b/packages/mobile/src/account/__snapshots__/Account.test.tsx.snap @@ -171,57 +171,6 @@ exports[`Account renders correctly 1`] = ` /> - - - - - - - - - - - - - - - - - - - - `; diff --git a/packages/mobile/src/account/__snapshots__/EditProfile.test.tsx.snap b/packages/mobile/src/account/__snapshots__/EditProfile.test.tsx.snap index 7f90d2236ea..40c15eac623 100644 --- a/packages/mobile/src/account/__snapshots__/EditProfile.test.tsx.snap +++ b/packages/mobile/src/account/__snapshots__/EditProfile.test.tsx.snap @@ -42,7 +42,6 @@ exports[`renders the EditProfile Component 1`] = ` allowFontScaling={true} autoCorrect={false} autoFocus={true} - keyboardType="default" onBlur={[Function]} onChangeText={[Function]} onEndEditing={[Function]} @@ -50,7 +49,6 @@ exports[`renders the EditProfile Component 1`] = ` onSubmitEditing={[Function]} placeholder="yourName" rejectResponderTermination={true} - secureTextEntry={false} style={ Array [ Object { @@ -67,7 +65,6 @@ exports[`renders the EditProfile Component 1`] = ` }, ] } - textContentType="none" 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 c7e6bed4e7f..849499fbf47 100644 --- a/packages/mobile/src/account/__snapshots__/Invite.test.tsx.snap +++ b/packages/mobile/src/account/__snapshots__/Invite.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Invite renders correctly 1`] = ` +exports[`Invite renders correctly with no recipients 1`] = ` - + > + + - noResultsFor - - - "" + noContacts - - searchForSomeone - } data={Array []} @@ -225,43 +212,428 @@ exports[`Invite renders correctly 1`] = ` } } > - noResultsFor + noContacts + + + + + + +`; + +exports[`Invite renders correctly with recipients 1`] = ` + + + + + + + + + + + + + + + - "" + noContacts + + } + ListFooterComponent={[Function]} + data={ + Array [ + Object { + "data": Array [ + Object { + "contactId": "contactId", + "displayId": "14155550000", + "displayName": "John Doe", + "e164PhoneNumber": "+14155550000", + "kind": "Contact", + "phoneNumberLabel": "phoneNumLabel", + }, + ], + "key": "contacts", + }, + ] + } + disableVirtualization={false} + getItem={[Function]} + getItemCount={[Function]} + horizontal={false} + initialNumToRender={30} + keyExtractor={[Function]} + keyboardShouldPersistTaps="handled" + maxToRenderPerBatch={10} + onContentSizeChange={[Function]} + onEndReachedThreshold={2} + onLayout={[Function]} + onMomentumScrollEnd={[Function]} + onScroll={[Function]} + onScrollBeginDrag={[Function]} + onScrollEndDrag={[Function]} + renderItem={[Function]} + renderSectionHeader={[Function]} + scrollEventThrottle={50} + sections={ + Array [ + Object { + "data": Array [ + Object { + "contactId": "contactId", + "displayId": "14155550000", + "displayName": "John Doe", + "e164PhoneNumber": "+14155550000", + "kind": "Contact", + "phoneNumberLabel": "phoneNumLabel", + }, + ], + "key": "contacts", + }, + ] + } + stickyHeaderIndices={ + Array [ + 0, + ] + } + stickySectionHeadersEnabled={true} + updateCellsBatchingPeriod={50} + windowSize={21} + > + + + + + contacts + + + + + + + + + + J + + + + + + John Doe + + + + 14155550000 + + + + + + + - searchForSomeone + searchFriends diff --git a/packages/mobile/src/account/actions.ts b/packages/mobile/src/account/actions.ts index 75a484e4dfa..b898100208e 100644 --- a/packages/mobile/src/account/actions.ts +++ b/packages/mobile/src/account/actions.ts @@ -3,7 +3,7 @@ import { showError } from 'src/alert/actions' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { DefaultEventNames } from 'src/analytics/constants' import { ErrorMessages } from 'src/app/ErrorMessages' -import { ERROR_BANNER_DURATION, SUPPORTS_KEYSTORE } from 'src/config' +import { ALERT_BANNER_DURATION, SUPPORTS_KEYSTORE } from 'src/config' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { getPin as getPinCred, setPin as setPinCred } from 'src/pincode/PincodeViaAndroidKeystore' @@ -177,7 +177,7 @@ export const setPin = (pin: string) => async (dispatch: DispatchType, getState: Logger.info(TAG + '@setPin', 'pincode set') return true } else { - dispatch(showError(ErrorMessages.SET_PIN_FAILED, ERROR_BANNER_DURATION)) + dispatch(showError(ErrorMessages.SET_PIN_FAILED, ALERT_BANNER_DURATION)) return false } } diff --git a/packages/mobile/src/account/types.ts b/packages/mobile/src/account/types.ts index 35bbf9ac17f..8165778100c 100644 --- a/packages/mobile/src/account/types.ts +++ b/packages/mobile/src/account/types.ts @@ -11,6 +11,7 @@ export enum PaymentRequestStatuses { DECLINED = 'DECLINED', } +// TODO(Rossy) Find a better home for this export interface PaymentRequest { uid?: string amount: string diff --git a/packages/mobile/src/alert/AlertBanner.test.tsx b/packages/mobile/src/alert/AlertBanner.test.tsx index f7d0cf161c1..bbf088c3aad 100644 --- a/packages/mobile/src/alert/AlertBanner.test.tsx +++ b/packages/mobile/src/alert/AlertBanner.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import 'react-native' -import * as renderer from 'react-test-renderer' +import { render } from 'react-native-testing-library' import { AlertBanner } from 'src/alert/AlertBanner' describe('AlertBanner', () => { @@ -10,7 +10,7 @@ describe('AlertBanner', () => { describe('when message passed in', () => { it('renders message', () => { - const tree = renderer.create( + const { toJSON } = render( { }} /> ) - expect(tree).toMatchSnapshot() + expect(toJSON()).toMatchSnapshot() }) }) describe('when message and title passed in', () => { it('renders title with message', () => { - const tree = renderer.create( + const { toJSON } = render( { }} /> ) - expect(tree).toMatchSnapshot() + expect(toJSON()).toMatchSnapshot() }) }) describe('when error message passed in', () => { it('renders error message', () => { - const tree = renderer.create( + const { toJSON } = render( { }} /> ) - expect(tree).toMatchSnapshot() + expect(toJSON()).toMatchSnapshot() }) }) }) diff --git a/packages/mobile/src/alert/AlertBanner.tsx b/packages/mobile/src/alert/AlertBanner.tsx index 42a01063739..6c4fb658c4d 100644 --- a/packages/mobile/src/alert/AlertBanner.tsx +++ b/packages/mobile/src/alert/AlertBanner.tsx @@ -29,16 +29,16 @@ export class AlertBanner extends React.Component { render() { const { alert, hideAlert: hideAlertAction } = this.props - return alert !== null ? ( + return ( - ) : null + ) } } diff --git a/packages/mobile/src/alert/__snapshots__/AlertBanner.test.tsx.snap b/packages/mobile/src/alert/__snapshots__/AlertBanner.test.tsx.snap index 8b0ed8402c1..942bbbbb78e 100644 --- a/packages/mobile/src/alert/__snapshots__/AlertBanner.test.tsx.snap +++ b/packages/mobile/src/alert/__snapshots__/AlertBanner.test.tsx.snap @@ -2,25 +2,24 @@ exports[`AlertBanner when error message passed in renders error message 1`] = ` @@ -28,11 +27,16 @@ exports[`AlertBanner when error message passed in renders error message 1`] = ` style={ Object { "alignItems": "center", - "flex": 1, + "backgroundColor": "#FD785B", "flexDirection": "row", "justifyContent": "center", - "opacity": 1, "paddingHorizontal": 25, + "paddingVertical": 10, + "transform": Array [ + Object { + "translateY": -500, + }, + ], } } > @@ -68,7 +72,6 @@ exports[`AlertBanner when error message passed in renders error message 1`] = ` @@ -123,16 +125,20 @@ exports[`AlertBanner when message and title passed in renders title with message style={ Object { "alignItems": "center", - "flex": 1, + "backgroundColor": "#3C9BF4", "flexDirection": "row", "justifyContent": "center", - "opacity": 1, "paddingHorizontal": 25, + "paddingVertical": 10, + "transform": Array [ + Object { + "translateY": -500, + }, + ], } } > @@ -203,16 +208,20 @@ exports[`AlertBanner when message passed in renders message 1`] = ` style={ Object { "alignItems": "center", - "flex": 1, + "backgroundColor": "#3C9BF4", "flexDirection": "row", "justifyContent": "center", - "opacity": 1, "paddingHorizontal": 25, + "paddingVertical": 10, + "transform": Array [ + Object { + "translateY": -500, + }, + ], } } > { + handleDeepLink(event.url) } hideSplashScreen() { diff --git a/packages/mobile/src/app/ErrorMessages.ts b/packages/mobile/src/app/ErrorMessages.ts index e63c72aa535..0babed7ccd2 100644 --- a/packages/mobile/src/app/ErrorMessages.ts +++ b/packages/mobile/src/app/ErrorMessages.ts @@ -25,6 +25,8 @@ export enum ErrorMessages { INVITE_FAILED = 'inviteFailed', SEND_PAYMENT_FAILED = 'sendPaymentFailed', PAYMENT_REQUEST_FAILED = 'paymentRequestFailed', + ESCROW_TRANSFER_FAILED = 'escrowTransferFailed', + ESCROW_WITHDRAWAL_FAILED = 'escrowWithdrawalFailed', RECLAIMING_ESCROWED_PAYMENT_FAILED = 'reclaimingEscrowedPaymentFailed', EXCHANGE_RATE_FAILED = 'exchangeFlow9:errorRefreshingRate', EXCHANGE_RATE_CHANGE = 'exchangeFlow9:exchangeRateChange', @@ -35,4 +37,6 @@ export enum ErrorMessages { GAS_PRICE_UPDATE_FAILED = 'gasPriceUpdateFailed', QR_FAILED_NO_ADDRESS = 'qrFailedNoAddress', QR_FAILED_INVALID_ADDRESS = 'qrFailedInvalidAddress', + CORRUPTED_CHAIN_DELETED = 'corruptedChainDeleted', + CALCULATE_FEE_FAILED = 'calculateFeeFailed', } diff --git a/packages/mobile/src/app/actions.ts b/packages/mobile/src/app/actions.ts index 76290b1aa55..23761c7c351 100644 --- a/packages/mobile/src/app/actions.ts +++ b/packages/mobile/src/app/actions.ts @@ -6,7 +6,6 @@ const numeral = require('numeral') require('numeral/locales/es') export enum Actions { - SET_INVITE_CODE_ENTERED = 'APP/SET_INVITE_CODE_ENTERED', SET_LOGGED_IN = 'APP/SET_LOGGED_IN', SET_NUMBER_VERIFIED = 'APP/SET_NUMBER_VERIFIED', SET_LANGUAGE = 'APP/SET_LANGUAGE', @@ -17,11 +16,6 @@ export enum Actions { SET_ANALYTICS_ENABLED = 'APP/SET_ANALYTICS_ENABLED', } -interface SetInviteCodeEntered { - type: Actions.SET_INVITE_CODE_ENTERED - inviteCodeEntered: boolean -} - interface SetLoggedIn { type: Actions.SET_LOGGED_IN loggedIn: boolean @@ -55,7 +49,6 @@ interface SetAnalyticsEnabled { } export type ActionTypes = - | SetInviteCodeEntered | SetLoggedIn | SetNumberVerifiedAction | ResetAppOpenedState @@ -64,11 +57,6 @@ export type ActionTypes = | ExitBackupFlow | SetAnalyticsEnabled -export const setInviteCodeEntered = (inviteCodeEntered: boolean) => ({ - type: Actions.SET_INVITE_CODE_ENTERED, - inviteCodeEntered, -}) - export const setLoggedIn = (loggedIn: boolean) => ({ type: Actions.SET_LOGGED_IN, loggedIn, diff --git a/packages/mobile/src/app/reducers.ts b/packages/mobile/src/app/reducers.ts index 57b5c6e2d79..149af1ac856 100644 --- a/packages/mobile/src/app/reducers.ts +++ b/packages/mobile/src/app/reducers.ts @@ -2,7 +2,6 @@ import { Actions, ActionTypes } from 'src/app/actions' import { RootState } from 'src/redux/reducers' export interface State { - inviteCodeEntered: boolean loggedIn: boolean numberVerified: boolean language: string | null @@ -11,7 +10,6 @@ export interface State { } const initialState = { - inviteCodeEntered: false, loading: false, loggedIn: false, numberVerified: false, @@ -24,11 +22,6 @@ export const currentLanguageSelector = (state: RootState) => state.app.language export const appReducer = (state: State | undefined = initialState, action: ActionTypes): State => { switch (action.type) { - case Actions.SET_INVITE_CODE_ENTERED: - return { - ...state, - inviteCodeEntered: action.inviteCodeEntered, - } case Actions.SET_LOGGED_IN: return { ...state, @@ -47,9 +40,9 @@ export const appReducer = (state: State | undefined = initialState, action: Acti case Actions.RESET_APP_OPENED_STATE: return { ...state, - inviteCodeEntered: false, loggedIn: false, numberVerified: false, + language: null, } case Actions.ENTER_BACKUP_FLOW: return { diff --git a/packages/mobile/src/app/saga.ts b/packages/mobile/src/app/saga.ts index aae48a7ba28..817539ec527 100644 --- a/packages/mobile/src/app/saga.ts +++ b/packages/mobile/src/app/saga.ts @@ -1,7 +1,9 @@ +import { Linking } from 'react-native' import DeviceInfo from 'react-native-device-info' import { REHYDRATE } from 'redux-persist/es/constants' import { all, call, put, select, spawn, take } from 'redux-saga/effects' import { setLanguage } from 'src/app/actions' +import { handleDappkitDeepLink } from 'src/dappkit/dappkit' import { getVersionInfo } from 'src/firebase/firebase' import { waitForFirebaseAuth } from 'src/firebase/saga' import { NavActions, navigate } from 'src/navigator/NavigationService' @@ -55,7 +57,11 @@ export function* checkAppDeprecation() { export function* navigateToProperScreen() { yield all([take(REHYDRATE), take(NavActions.SET_NAVIGATOR)]) + + const deepLink = yield call(Linking.getInitialURL) + const inSync = yield call(clockInSync) const mappedState = yield select(mapStateToProps) + if (!mappedState) { navigate(Stacks.NuxStack) return @@ -75,7 +81,10 @@ export function* navigateToProperScreen() { yield put(setLanguage(language)) } - const inSync = yield call(clockInSync) + if (deepLink) { + handleDeepLink(deepLink) + return + } if (!language) { navigate(Stacks.NuxStack) @@ -98,6 +107,12 @@ export function* navigateToProperScreen() { } } +export function handleDeepLink(deepLink: string) { + Logger.debug(TAG, 'Handling deep link', deepLink) + handleDappkitDeepLink(deepLink) + // Other deep link handlers can go here later +} + export function* appSaga() { yield spawn(checkAppDeprecation) yield spawn(navigateToProperScreen) diff --git a/packages/mobile/src/backup/Backup.tsx b/packages/mobile/src/backup/Backup.tsx index 0a5bf1637d4..f2696ddba2e 100644 --- a/packages/mobile/src/backup/Backup.tsx +++ b/packages/mobile/src/backup/Backup.tsx @@ -44,6 +44,7 @@ const mapStateToProps = (state: RootState): StateProps => { export class Backup extends React.Component { static navigationOptions = { header: null } + state = { mnemonic: '', currentQuestion: -1, diff --git a/packages/mobile/src/backup/BackupComplete.tsx b/packages/mobile/src/backup/BackupComplete.tsx index ed74a46f357..ede7d22f430 100644 --- a/packages/mobile/src/backup/BackupComplete.tsx +++ b/packages/mobile/src/backup/BackupComplete.tsx @@ -22,9 +22,7 @@ interface State { } class BackupComplete extends React.Component { - static navigationOptions = { - header: null, - } + static navigationOptions = { header: null } state = { selectedAnswer: null, diff --git a/packages/mobile/src/backup/utils.test.ts b/packages/mobile/src/backup/utils.test.ts index 1beabc2d4ac..8aa545d86f6 100644 --- a/packages/mobile/src/backup/utils.test.ts +++ b/packages/mobile/src/backup/utils.test.ts @@ -22,12 +22,6 @@ jest.mock('react-native-bip39', () => { }) describe('backup/utils', () => { - beforeAll(() => { - const mockMath = Object.create(global.Math) - mockMath.random = () => 0.5 - global.Math = mockMath - }) - describe('createQuizWordList', () => { it('creates list correctly without dupes', async () => { const wordList = await createQuizWordList(mockMnemonic, 'en') @@ -39,10 +33,20 @@ describe('backup/utils', () => { }) describe('selectQuizWordOptions', () => { it('selects words correctly', async () => { + global.Math.random = () => 0.5 + const wordList = await createQuizWordList(mockMnemonic, 'en') const wordOptions = selectQuizWordOptions('crawl', wordList, 4) expect(wordOptions.length).toBe(4) expect(wordOptions[2]).toBe('crawl') }) + + it('does not have duplicates in word options', () => { + global.Math = Math + const wordList = ['a', 'b', 'c'] + const wordOptions = selectQuizWordOptions('d', wordList, 4) + expect(wordOptions.length).toBe(4) + expect(wordOptions).toEqual(expect.arrayContaining(['a', 'b', 'c', 'd'])) + }) }) }) diff --git a/packages/mobile/src/backup/utils.ts b/packages/mobile/src/backup/utils.ts index dfb05bb3e28..dbf7e760270 100644 --- a/packages/mobile/src/backup/utils.ts +++ b/packages/mobile/src/backup/utils.ts @@ -1,3 +1,4 @@ +import { sampleSize } from 'lodash' import { generateMnemonic, wordlists } from 'react-native-bip39' export async function createQuizWordList(mnemonic: string, language: string | null) { @@ -13,12 +14,17 @@ export async function createQuizWordList(mnemonic: string, language: string | nu export function selectQuizWordOptions(correctWord: string, allWords: string[], numOptions: number) { const wordOptions = [] const correctWordPosition = Math.floor(Math.random() * numOptions) + const randomWordIndexList = sampleSize([...Array(allWords.length).keys()], numOptions - 1) + let randomWordIndex: number = 0 + for (let i = 0; i < numOptions; i++) { - wordOptions.push( - i === correctWordPosition - ? correctWord - : allWords[Math.floor(Math.random() * allWords.length)] - ) + if (i === correctWordPosition) { + wordOptions.push(correctWord) + continue + } + + wordOptions.push(allWords[randomWordIndexList[randomWordIndex]]) + randomWordIndex += 1 } return wordOptions } diff --git a/packages/mobile/src/components/AccountOverview.test.tsx b/packages/mobile/src/components/AccountOverview.test.tsx index 905152393c0..91664707049 100644 --- a/packages/mobile/src/components/AccountOverview.test.tsx +++ b/packages/mobile/src/components/AccountOverview.test.tsx @@ -17,8 +17,8 @@ it('renders correctly when ready', () => { goldEducationCompleted={true} stableEducationCompleted={true} testID={'SnapshotAccountOverview'} - balanceOutOfSync={false} - refreshAllBalances={jest.fn()} + startBalanceAutorefresh={jest.fn()} + stopBalanceAutorefresh={jest.fn()} {...getMockI18nProps()} /> ) @@ -34,8 +34,8 @@ it('renders correctly when not ready', () => { goldEducationCompleted={true} stableEducationCompleted={true} testID={'SnapshotAccountOverview'} - balanceOutOfSync={false} - refreshAllBalances={jest.fn()} + startBalanceAutorefresh={jest.fn()} + stopBalanceAutorefresh={jest.fn()} {...getMockI18nProps()} /> ) @@ -51,8 +51,8 @@ it('renders correctly when transaction pending', () => { goldEducationCompleted={true} stableEducationCompleted={true} testID={'SnapshotAccountOverview'} - balanceOutOfSync={false} - refreshAllBalances={jest.fn()} + startBalanceAutorefresh={jest.fn()} + stopBalanceAutorefresh={jest.fn()} {...getMockI18nProps()} /> ) @@ -68,8 +68,8 @@ it("renders correctly when Gold education NUX flow hasn't been completed", () => goldEducationCompleted={false} stableEducationCompleted={true} testID={'SnapshotAccountOverview'} - balanceOutOfSync={false} - refreshAllBalances={jest.fn()} + startBalanceAutorefresh={jest.fn()} + stopBalanceAutorefresh={jest.fn()} {...getMockI18nProps()} /> ) @@ -85,25 +85,8 @@ it("renders correctly when Dollar education NUX flow hasn't been completed", () goldEducationCompleted={true} stableEducationCompleted={false} testID={'SnapshotAccountOverview'} - balanceOutOfSync={false} - refreshAllBalances={jest.fn()} - {...getMockI18nProps()} - /> - ) - expect(tree).toMatchSnapshot() -}) - -it('renders correctly when balance is out of sync', () => { - const tree = renderer.create( - ) diff --git a/packages/mobile/src/components/AccountOverview.tsx b/packages/mobile/src/components/AccountOverview.tsx index ed456c0d2e4..443658bef0f 100644 --- a/packages/mobile/src/components/AccountOverview.tsx +++ b/packages/mobile/src/components/AccountOverview.tsx @@ -1,5 +1,3 @@ -import PulsingDot from '@celo/react-components/components/PulsingDot' -import SmallButton from '@celo/react-components/components/SmallButton' import colors from '@celo/react-components/styles/colors' import fontStyles, { estimateFontSize } from '@celo/react-components/styles/fonts' import variables from '@celo/react-components/styles/variables' @@ -7,19 +5,16 @@ import * as React from 'react' import { withNamespaces, WithNamespaces } from 'react-i18next' import { StyleSheet, Text, TouchableOpacity, View } from 'react-native' import { connect } from 'react-redux' -import { CTA_CIRCLE_SIZE } from 'src/account/Education' import componentWithAnalytics from 'src/analytics/wrapper' import CurrencyDisplay from 'src/components/CurrencyDisplay' import Styles from 'src/components/Styles' -import { isE2EEnv } from 'src/config' import { ExchangeRatePair } from 'src/exchange/reducer' import { CURRENCIES, CURRENCY_ENUM as Tokens } from 'src/geth/consts' -import { refreshAllBalances } from 'src/home/actions' +import { startBalanceAutorefresh, stopBalanceAutorefresh } from 'src/home/actions' import { Namespaces } from 'src/i18n' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { RootState } from 'src/redux/reducers' -import { showRefreshBalanceMessage } from 'src/redux/selectors' import { getMoneyDisplayValue } from 'src/utils/formatting' interface StateProps { @@ -28,7 +23,6 @@ interface StateProps { stableEducationCompleted: boolean goldBalance: string | null dollarBalance: string | null - balanceOutOfSync: boolean } interface OwnProps { @@ -36,7 +30,8 @@ interface OwnProps { } interface DispatchProps { - refreshAllBalances: typeof refreshAllBalances + startBalanceAutorefresh: typeof startBalanceAutorefresh + stopBalanceAutorefresh: typeof stopBalanceAutorefresh } type Props = StateProps & DispatchProps & WithNamespaces & OwnProps @@ -48,7 +43,6 @@ const mapStateToProps = (state: RootState): StateProps => { stableEducationCompleted: state.stableToken.educationCompleted, goldBalance: state.goldToken.balance, dollarBalance: state.stableToken.balance, - balanceOutOfSync: showRefreshBalanceMessage(state), } } @@ -70,12 +64,16 @@ export class AccountOverview extends React.Component { return fontSize } - refreshBalances = () => { - this.props.refreshAllBalances() + componentDidMount() { + this.props.startBalanceAutorefresh() + } + + componentWillUnmount() { + this.props.stopBalanceAutorefresh() } render() { - const { t, testID, goldBalance, dollarBalance, balanceOutOfSync } = this.props + const { t, testID, goldBalance, dollarBalance } = this.props return ( @@ -94,16 +92,7 @@ export class AccountOverview extends React.Component { amount={dollarBalance} size={this.getFontSize(dollarBalance, !this.props.stableEducationCompleted)} type={Tokens.DOLLAR} - balanceOutOfSync={balanceOutOfSync} /> - {!this.props.stableEducationCompleted && ( - - )} @@ -120,30 +109,10 @@ export class AccountOverview extends React.Component { amount={goldBalance} size={this.getFontSize(goldBalance, !this.props.goldEducationCompleted)} type={Tokens.GOLD} - balanceOutOfSync={balanceOutOfSync} /> - {!this.props.goldEducationCompleted && ( - - )} - {balanceOutOfSync && ( - - {t('balanceNeedUpdating')} - - - )} ) @@ -203,30 +172,11 @@ const style = StyleSheet.create({ dot: { padding: 10, }, - balanceRefreshContainer: { - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - paddingBottom: 13, - paddingHorizontal: 50, - }, - balanceRefreshText: { - ...fontStyles.bodySmall, - color: colors.messageBlue, - paddingRight: 5, - }, - messageButton: { - ...fontStyles.messageText, - borderColor: colors.messageBlue, - minWidth: 0, - paddingVertical: 2, - paddingHorizontal: 5, - }, }) export default componentWithAnalytics( connect( mapStateToProps, - { refreshAllBalances } + { startBalanceAutorefresh, stopBalanceAutorefresh } )(withNamespaces(Namespaces.walletFlow5)(AccountOverview)) ) diff --git a/packages/mobile/src/components/CurrencyDisplay.tsx b/packages/mobile/src/components/CurrencyDisplay.tsx index b9fbeaf8e2e..74794d01325 100644 --- a/packages/mobile/src/components/CurrencyDisplay.tsx +++ b/packages/mobile/src/components/CurrencyDisplay.tsx @@ -10,21 +10,13 @@ interface Props { amount: string | null | number size: number type: CURRENCY_ENUM - balanceOutOfSync: boolean } const symbolRatio = 0.6 export default class CurrencyDisplay extends React.PureComponent { color() { - const { balanceOutOfSync } = this.props - if (balanceOutOfSync) { - return this.props.type === CURRENCY_ENUM.DOLLAR - ? colors.celoGreenInactiveExtra - : colors.celoGoldInactive - } else { - return this.props.type === CURRENCY_ENUM.DOLLAR ? colors.celoGreen : colors.celoGold - } + return this.props.type === CURRENCY_ENUM.DOLLAR ? colors.celoGreen : colors.celoGold } symbolStyle(fontSize: number) { diff --git a/packages/mobile/src/components/__snapshots__/AccountOverview.test.tsx.snap b/packages/mobile/src/components/__snapshots__/AccountOverview.test.tsx.snap index 06a3a4ca03f..d0efce52b6c 100644 --- a/packages/mobile/src/components/__snapshots__/AccountOverview.test.tsx.snap +++ b/packages/mobile/src/components/__snapshots__/AccountOverview.test.tsx.snap @@ -120,54 +120,6 @@ exports[`renders correctly when Dollar education NUX flow hasn't been completed 55.00 - - - - - - - - - - - - - -`; - -exports[`renders correctly when balance is out of sync 1`] = ` - - - - - - celoDollars cUSD - - - - - $ - - - 55.00 - - - - - - - - celoGold cGLD - - - - - 55.00 - - - - - balanceNeedUpdating - - - - refreshBalances - - - `; diff --git a/packages/mobile/src/config.ts b/packages/mobile/src/config.ts index 433fe5efcef..298d43a7a3b 100644 --- a/packages/mobile/src/config.ts +++ b/packages/mobile/src/config.ts @@ -27,7 +27,7 @@ export const FAQ_LINK = 'https://celo.org/faq' export const CELO_SUPPORT_EMAIL_ADDRESS = 'support@celo.org' export const BALANCE_OUT_OF_SYNC_THRESHOLD = 5 * 60 // 5 minutes -export const ERROR_BANNER_DURATION = 5000 +export const ALERT_BANNER_DURATION = 5000 export const INPUT_DEBOUNCE_TIME = 1000 // milliseconds export const SUPPORTS_KEYSTORE = Platform.Version >= 23 diff --git a/packages/mobile/src/dappkit/DappKitAccountScreen.tsx b/packages/mobile/src/dappkit/DappKitAccountScreen.tsx new file mode 100644 index 00000000000..3a568ebf44b --- /dev/null +++ b/packages/mobile/src/dappkit/DappKitAccountScreen.tsx @@ -0,0 +1,93 @@ +import FullscreenCTA from '@celo/react-components/components/FullscreenCTA' +import { componentStyles } from '@celo/react-components/styles/styles' +import { AccountAuthRequest } from '@celo/utils/src/dappkit' +import * as React from 'react' +import { withNamespaces, WithNamespaces } from 'react-i18next' +import { Text, View } from 'react-native' +import { NavigationParams, NavigationScreenProp } from 'react-navigation' +import { connect } from 'react-redux' +import { e164NumberSelector } from 'src/account/reducer' +import { approveAccountAuth } from 'src/dappkit/dappkit' +import { Namespaces } from 'src/i18n' +import { navigateHome } from 'src/navigator/NavigationService' +import { RootState } from 'src/redux/reducers' +import Logger from 'src/utils/Logger' +import { currentAccountSelector } from 'src/web3/selectors' + +const TAG = 'dappkit/DappKitAccountScreen' + +interface OwnProps { + errorMessage?: string + navigation?: NavigationScreenProp +} + +interface StateProps { + account: string | null + phoneNumber: string | null +} + +type Props = OwnProps & StateProps & WithNamespaces + +const mapStateToProps = (state: RootState): StateProps => ({ + account: currentAccountSelector(state), + phoneNumber: e164NumberSelector(state), +}) + +class DappKitAccountAuthScreen extends React.Component { + static navigationOptions = { header: null } + + getErrorMessage() { + return ( + this.props.errorMessage || + (this.props.navigation && this.props.navigation.getParam('errorMessage')) || + '' + ) + } + + linkBack = () => { + const { account, navigation, phoneNumber } = this.props + + if (!navigation) { + Logger.error(TAG, 'Missing navigation props') + return + } + + const request: AccountAuthRequest = navigation.getParam('dappKitRequest', null) + + if (!request) { + Logger.error(TAG, 'No request found in navigation props') + return + } + if (!account) { + Logger.error(TAG, 'No account set up for this wallet') + return + } + if (!phoneNumber) { + Logger.error(TAG, 'No phone number set up for this wallet') + return + } + + navigateHome({ dispatchAfterNavigate: approveAccountAuth(request) }) + } + + render() { + return ( + + + + {this.props.account} + + + + ) + } +} + +export default withNamespaces(Namespaces.global)( + connect(mapStateToProps)(DappKitAccountAuthScreen) +) diff --git a/packages/mobile/src/dappkit/DappKitTxSignScreen.tsx b/packages/mobile/src/dappkit/DappKitTxSignScreen.tsx new file mode 100644 index 00000000000..4473c92efa6 --- /dev/null +++ b/packages/mobile/src/dappkit/DappKitTxSignScreen.tsx @@ -0,0 +1,67 @@ +import FullscreenCTA from '@celo/react-components/components/FullscreenCTA' +import { componentStyles } from '@celo/react-components/styles/styles' +import { SignTxRequest } from '@celo/utils/src/dappkit' +import * as React from 'react' +import { withNamespaces, WithNamespaces } from 'react-i18next' +import { Text, View } from 'react-native' +import { NavigationParams, NavigationScreenProp } from 'react-navigation' +import { requestTxSignature } from 'src/dappkit/dappkit' +import { Namespaces } from 'src/i18n' +import { navigateHome } from 'src/navigator/NavigationService' +import Logger from 'src/utils/Logger' + +const TAG = 'dappkit/DappKitSignTxScreen' + +interface OwnProps { + errorMessage?: string + navigation?: NavigationScreenProp +} + +type Props = OwnProps & WithNamespaces + +class DappKitSignTxScreen extends React.Component { + static navigationOptions = { header: null } + + getErrorMessage() { + return ( + this.props.errorMessage || + (this.props.navigation && this.props.navigation.getParam('errorMessage')) || + '' + ) + } + + linkBack = () => { + if (!this.props.navigation) { + Logger.error(TAG, 'Missing navigation props') + return + } + + const request: SignTxRequest = this.props.navigation.getParam('dappKitRequest', null) + + if (!request) { + Logger.error(TAG, 'No request found in navigation props') + return + } + + navigateHome({ dispatchAfterNavigate: requestTxSignature(request) }) + } + + render() { + return ( + + + + Dapp + + + + ) + } +} + +export default withNamespaces(Namespaces.global)(DappKitSignTxScreen) diff --git a/packages/mobile/src/dappkit/dappkit.ts b/packages/mobile/src/dappkit/dappkit.ts new file mode 100644 index 00000000000..30140a5d857 --- /dev/null +++ b/packages/mobile/src/dappkit/dappkit.ts @@ -0,0 +1,102 @@ +import { + AccountAuthRequest, + AccountAuthResponseSuccess, + DappKitRequestTypes, + parseDappKitRequestDeeplink, + produceResponseDeeplink, + SignTxRequest, + SignTxResponseSuccess, +} from '@celo/utils/src/dappkit' +import { Linking } from 'react-native' +import { call, select, takeLeading } from 'redux-saga/effects' +import { e164NumberSelector } from 'src/account/reducer' +import { navigate } from 'src/navigator/NavigationService' +import { Screens } from 'src/navigator/Screens' +import Logger from 'src/utils/Logger' +import { web3 } from 'src/web3/contracts' +import { getConnectedUnlockedAccount } from 'src/web3/saga' +import { currentAccountSelector } from 'src/web3/selectors' + +const TAG = 'dappkit/dappkit' + +export enum actions { + APPROVE_ACCOUNT_AUTH = 'DAPPKIT/APPROVE_ACCOUNT_AUTH', + REQUEST_TX_SIGNATURE = 'DAPPKIT/REQUEST_TX_SIGNATURE', +} + +export interface ApproveAccountAuthAction { + type: actions.APPROVE_ACCOUNT_AUTH + request: AccountAuthRequest +} + +export const approveAccountAuth = (request: AccountAuthRequest): ApproveAccountAuthAction => ({ + type: actions.APPROVE_ACCOUNT_AUTH, + request, +}) + +export interface RequestTxSignatureAction { + type: actions.REQUEST_TX_SIGNATURE + request: SignTxRequest +} + +export const requestTxSignature = (request: SignTxRequest): RequestTxSignatureAction => ({ + type: actions.REQUEST_TX_SIGNATURE, + request, +}) + +function* respondToAccountAuth(action: ApproveAccountAuthAction) { + Logger.debug(TAG, 'Approving auth account') + const account = yield select(currentAccountSelector) + const phoneNumber = yield select(e164NumberSelector) + Linking.openURL( + produceResponseDeeplink(action.request, AccountAuthResponseSuccess(account, phoneNumber)) + ) +} + +// TODO Error handling here +function* produceTxSignature(action: RequestTxSignatureAction) { + Logger.debug(TAG, 'Producing tx signature') + + yield call(getConnectedUnlockedAccount) + const rawTxs = yield Promise.all( + action.request.txs.map(async (tx) => { + const signedTx = await web3.eth.signTransaction({ + from: tx.from, + to: tx.to, + gasPrice: '0', + gas: tx.estimatedGas, + data: tx.txData, + nonce: tx.nonce, + // @ts-ignore + gasCurrency: action.request.gasCurrency, + }) + return signedTx.raw + }) + ) + + Logger.debug(TAG, 'Txs signed, opening URL') + Linking.openURL(produceResponseDeeplink(action.request, SignTxResponseSuccess(rawTxs))) +} + +export function* dappKitSaga() { + yield takeLeading(actions.APPROVE_ACCOUNT_AUTH, respondToAccountAuth) + yield takeLeading(actions.REQUEST_TX_SIGNATURE, produceTxSignature) +} + +export function handleDappkitDeepLink(deepLink: string) { + try { + const dappKitRequest = parseDappKitRequestDeeplink(deepLink) + switch (dappKitRequest.type) { + case DappKitRequestTypes.ACCOUNT_ADDRESS: + navigate(Screens.DappKitAccountAuth, { dappKitRequest }) + break + case DappKitRequestTypes.SIGN_TX: + navigate(Screens.DappKitSignTxScreen, { dappKitRequest }) + break + default: + Logger.warn(TAG, 'Unsupported dapp request type') + } + } catch (error) { + Logger.debug(TAG, 'Deep link not valid for dappkit. Ignoring.') + } +} diff --git a/packages/mobile/src/escrow/EscrowedPaymentLineItem.tsx b/packages/mobile/src/escrow/EscrowedPaymentLineItem.tsx index 076a81fad25..8619b770405 100644 --- a/packages/mobile/src/escrow/EscrowedPaymentLineItem.tsx +++ b/packages/mobile/src/escrow/EscrowedPaymentLineItem.tsx @@ -1,21 +1,23 @@ import fontStyles from '@celo/react-components/styles/fonts' -import { getContactPhoneNumber } from '@celo/utils/src/contacts' import * as React from 'react' import { StyleSheet, Text } from 'react-native' import { EscrowedPayment } from 'src/escrow/actions' +import { divideByWei, getMoneyDisplayValue } from 'src/utils/formatting' interface Props { payment: EscrowedPayment } export default function EscrowedPaymentLineItem(props: Props) { - const { message, recipient } = props.payment - const recipientPhoneNumber = - typeof recipient === 'string' ? recipient : getContactPhoneNumber(recipient) + const { amount, message, recipientPhone } = props.payment return ( - {recipientPhoneNumber} - {message} + {recipientPhone} - {message} + + + {' '} + ${getMoneyDisplayValue(divideByWei(amount.toString()))} ) diff --git a/packages/mobile/src/escrow/ReclaimPaymentConfirmationCard.test.tsx b/packages/mobile/src/escrow/ReclaimPaymentConfirmationCard.test.tsx index cea98a3e0cb..214ab24265c 100644 --- a/packages/mobile/src/escrow/ReclaimPaymentConfirmationCard.test.tsx +++ b/packages/mobile/src/escrow/ReclaimPaymentConfirmationCard.test.tsx @@ -6,7 +6,7 @@ import * as renderer from 'react-test-renderer' import ReclaimPaymentConfirmationCard from 'src/escrow/ReclaimPaymentConfirmationCard' import {} from 'src/home/NotificationBox' import { createMockStore } from 'test/utils' -import { mockContactWithPhone } from 'test/values' +import { mockE164Number, mockRecipient } from 'test/values' const store = createMockStore() @@ -15,7 +15,8 @@ describe('ReclaimPaymentConfirmationCard', () => { const tree = renderer.create( {title} {titleIcon} - - {negative && '-'} - {currencySymbol} - {getMoneyDisplayValue(amount)} - + {amount && ( + + {negative && '-'} + {currencySymbol} + {getMoneyDisplayValue(amount)} + + )} + {hasError && ---} + {isLoading && ( + + + + )} ) } export interface OwnProps { - recipient: MinimalContact | string + recipientPhone: string + recipientContact?: RecipientWithContact amount: BigNumber comment?: string fee?: BigNumber + isLoadingFee?: boolean + feeError?: Error currency: CURRENCY_ENUM } @@ -72,23 +86,27 @@ const mapStateToProps = (state: RootState): StateProps => { type Props = OwnProps & StateProps & WithNamespaces class ReclaimPaymentConfirmationCard extends React.PureComponent { - renderFeeAndTotal = (total: BigNumber, currencySymbol: string, fee?: BigNumber) => { - if (!fee) { - return - } - + renderFeeAndTotal = ( + total: BigNumber, + currencySymbol: string, + fee: BigNumber | undefined, + isLoadingFee: boolean | undefined, + feeError: Error | undefined + ) => { const { t } = this.props - const amountWithFees = total.minus(this.props.fee || 0) + const amountWithFees = total.minus(fee || 0) return ( } negative={true} + isLoading={isLoadingFee} + hasError={!!feeError} /> { } render() { - const { recipient, amount, comment, fee, currency, defaultCountryCode } = this.props + const { + recipientPhone, + recipientContact, + amount, + comment, + fee, + isLoadingFee, + feeError, + currency, + defaultCountryCode, + } = this.props const currencySymbol = CURRENCIES[currency].symbol const currencyColor = getCurrencyColor(currency) return ( @@ -114,30 +142,23 @@ class ReclaimPaymentConfirmationCard extends React.PureComponent { {currencySymbol} - {getMoneyDisplayValue(amount.minus(this.props.fee || 0))} + {getMoneyDisplayValue(amount.minus(fee || 0))} - {typeof recipient !== 'string' && ( + {recipientContact && ( - {recipient.displayName} + {recipientContact.displayName} )} - {typeof recipient !== 'string' ? ( - - ) : ( - - )} + {!!comment && {comment}} - {this.renderFeeAndTotal(amount, currencySymbol, fee)} + {this.renderFeeAndTotal(amount, currencySymbol, fee, isLoadingFee, feeError)} ) } @@ -147,7 +168,7 @@ const style = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', - padding: 10, + padding: 20, }, amountContainer: { @@ -167,6 +188,8 @@ const style = StyleSheet.create({ feeContainer: { marginTop: 10, marginBottom: 10, + justifyContent: 'center', + alignItems: 'stretch', }, feeRow: { alignItems: 'center', @@ -174,24 +197,16 @@ const style = StyleSheet.create({ justifyContent: 'flex-start', }, lineItemRow: { - flex: 1, flexDirection: 'row', - alignItems: 'stretch', justifyContent: 'space-between', }, totalTitle: { lineHeight: 28, - marginLeft: 10, - left: 1, }, total: { - right: 1, - marginRight: 10, lineHeight: 28, }, totalGreen: { - right: 1, - marginRight: 10, lineHeight: 28, color: colors.celoGreen, }, @@ -200,6 +215,9 @@ const style = StyleSheet.create({ lineHeight: 40, height: 35, }, + loadingContainer: { + transform: [{ scale: 0.8 }], + }, contactName: { paddingTop: 6, textAlign: 'center', diff --git a/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.test.tsx b/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.test.tsx index 8d3ed1e5f2c..f197197cdc1 100644 --- a/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.test.tsx +++ b/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.test.tsx @@ -1,31 +1,95 @@ import BigNumber from 'bignumber.js' import * as React from 'react' +import { render, waitForElement } from 'react-native-testing-library' import { Provider } from 'react-redux' -import * as renderer from 'react-test-renderer' import ReclaimPaymentConfirmationScreen from 'src/escrow/ReclaimPaymentConfirmationScreen' -import { SHORT_CURRENCIES } from 'src/geth/consts' +import { getReclaimEscrowFee } from 'src/escrow/saga' +import { SHORT_CURRENCIES, WEI_PER_CELO } from 'src/geth/consts' import { createMockNavigationProp, createMockStore } from 'test/utils' -import { mockAccount, mockAccount2, mockContactWithPhone } from 'test/values' +import { mockAccount, mockAccount2, mockE164Number, mockRecipient } from 'test/values' + +const TEST_FEE = new BigNumber(10000000000000000) + +jest.mock('src/escrow/saga') + +const mockedGetReclaimEscrowFee = getReclaimEscrowFee as jest.Mock const store = createMockStore() describe('ReclaimPaymentConfirmationScreen', () => { - it('renders correctly', () => { + beforeAll(() => { + jest.useRealTimers() + }) + + beforeEach(() => { + mockedGetReclaimEscrowFee.mockClear() + }) + + it('renders correctly', async () => { + const navigation = createMockNavigationProp({ + senderAddress: mockAccount2, + recipientPhone: mockE164Number, + recipientContact: mockRecipient, + paymentID: mockAccount, + currency: SHORT_CURRENCIES.DOLLAR, + amount: new BigNumber(10 * WEI_PER_CELO), + timestamp: new BigNumber(10000), + expirySeconds: new BigNumber(50000), + }) + + mockedGetReclaimEscrowFee.mockImplementation(async () => TEST_FEE) + + const { queryByText, getByText, toJSON } = render( + + + + ) + + // Initial render + expect(toJSON()).toMatchSnapshot() + expect(queryByText('securityFee')).not.toBeNull() + expect(queryByText('-$0.01')).toBeNull() + + // Wait for fee to be calculated and displayed + await waitForElement(() => getByText('-$0.01')) + + expect(queryByText('$9.99')).not.toBeNull() + + expect(toJSON()).toMatchSnapshot() + }) + + it('renders correctly when fee calculation fails', async () => { const navigation = createMockNavigationProp({ senderAddress: mockAccount2, - recipient: mockContactWithPhone, + recipientPhone: mockE164Number, + recipientContact: mockRecipient, paymentID: mockAccount, currency: SHORT_CURRENCIES.DOLLAR, - amount: new BigNumber(10), + amount: new BigNumber(10 * WEI_PER_CELO), timestamp: new BigNumber(10000), expirySeconds: new BigNumber(50000), }) - const tree = renderer.create( + mockedGetReclaimEscrowFee.mockImplementation(async () => { + throw new Error('Calculate fee failed') + }) + + const { queryAllByText, queryByText, getByText, toJSON } = render( ) - expect(tree).toMatchSnapshot() + + // Initial render + expect(toJSON()).toMatchSnapshot() + expect(queryByText('securityFee')).not.toBeNull() + expect(queryByText('-$0.01')).toBeNull() + + // Wait for fee error + await waitForElement(() => getByText('---')) + + expect(queryAllByText('$10.00')).toHaveLength(2) + + expect(toJSON()).toMatchSnapshot() }) }) diff --git a/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.tsx b/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.tsx index 702f33a721e..3446e163a29 100644 --- a/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.tsx +++ b/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.tsx @@ -5,7 +5,7 @@ import { CURRENCY_ENUM } from '@celo/utils/src/currencies' import BigNumber from 'bignumber.js' import * as React from 'react' import { withNamespaces, WithNamespaces } from 'react-i18next' -import { StyleSheet, View } from 'react-native' +import { ActivityIndicator, StyleSheet, View } from 'react-native' import { NavigationInjectedProps } from 'react-navigation' import { connect } from 'react-redux' import { showError } from 'src/alert/actions' @@ -13,24 +13,29 @@ import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' import componentWithAnalytics from 'src/analytics/wrapper' import { ErrorMessages } from 'src/app/ErrorMessages' -import { ERROR_BANNER_DURATION } from 'src/config' +import { ALERT_BANNER_DURATION } from 'src/config' import { EscrowedPayment, reclaimPayment } from 'src/escrow/actions' import ReclaimPaymentConfirmationCard from 'src/escrow/ReclaimPaymentConfirmationCard' +import { FeeType } from 'src/fees/actions' +import CalculateFee, { CalculateFeeChildren } from 'src/fees/CalculateFee' +import { getFeeDollars } from 'src/fees/selectors' import { Namespaces } from 'src/i18n' -import { navigate, navigateBack } from 'src/navigator/NavigationService' -import { Screens } from 'src/navigator/Screens' +import { navigateBack } from 'src/navigator/NavigationService' import { RootState } from 'src/redux/reducers' -import { getSuggestedFeeDollars } from 'src/send/selectors' +import { isAppConnected } from 'src/redux/selectors' import DisconnectBanner from 'src/shared/DisconnectBanner' +import { divideByWei } from 'src/utils/formatting' import Logger from 'src/utils/Logger' import { currentAccountSelector } from 'src/web3/selectors' const TAG = 'escrow/ReclaimPaymentConfirmationScreen' interface StateProps { + isReclaiming: boolean e164PhoneNumber: string account: string | null - fee: BigNumber + dollarBalance: BigNumber + appConnected: boolean } interface DispatchProps { @@ -45,9 +50,11 @@ const mapDispatchToProps = { const mapStateToProps = (state: RootState): StateProps => { return { + isReclaiming: state.escrow.isReclaiming, e164PhoneNumber: state.account.e164PhoneNumber, account: currentAccountSelector(state), - fee: getSuggestedFeeDollars(state), + dollarBalance: new BigNumber(state.stableToken.balance || 0), + appConnected: isAppConnected(state), } } @@ -76,11 +83,9 @@ class ReclaimPaymentConfirmationScreen extends React.Component { this.props.reclaimPayment(escrowedPayment.paymentID) } catch (error) { Logger.error(TAG, 'Reclaiming escrowed payment failed, show error message', error) - this.props.showError(ErrorMessages.RECLAIMING_ESCROWED_PAYMENT_FAILED, ERROR_BANNER_DURATION) + this.props.showError(ErrorMessages.RECLAIMING_ESCROWED_PAYMENT_FAILED, ALERT_BANNER_DURATION) return } - - navigate(Screens.WalletHome) } onPressEdit = () => { @@ -94,33 +99,74 @@ class ReclaimPaymentConfirmationScreen extends React.Component { return } - render() { - const { t, fee } = this.props + renderFooter = () => { + return this.props.isReclaiming ? ( + + ) : null + } + + renderWithAsyncFee: CalculateFeeChildren = (asyncFee) => { + const { t, isReclaiming, appConnected } = this.props const payment = this.getReclaimPaymentInput() + const fee = asyncFee.result && getFeeDollars(asyncFee.result) + const convertedAmount = divideByWei(payment.amount.valueOf()) + + const currentBalance = this.props.dollarBalance + const userHasEnough = fee && fee.isLessThanOrEqualTo(currentBalance) return ( ) } + + render() { + const { account } = this.props + if (!account) { + throw Error('Account is required') + } + + const payment = this.getReclaimPaymentInput() + + return ( + // Note: intentionally passing a new child func here otherwise + // it doesn't re-render on state change since CalculateFee is a pure component + + {(asyncFee) => this.renderWithAsyncFee(asyncFee)} + + ) + } } const styles = StyleSheet.create({ diff --git a/packages/mobile/src/escrow/__snapshots__/ReclaimPaymentConfirmationCard.test.tsx.snap b/packages/mobile/src/escrow/__snapshots__/ReclaimPaymentConfirmationCard.test.tsx.snap index cd74d266f30..00923890cff 100644 --- a/packages/mobile/src/escrow/__snapshots__/ReclaimPaymentConfirmationCard.test.tsx.snap +++ b/packages/mobile/src/escrow/__snapshots__/ReclaimPaymentConfirmationCard.test.tsx.snap @@ -12,7 +12,7 @@ exports[`ReclaimPaymentConfirmationCard renders correctly for send payment confi Object { "flex": 1, "justifyContent": "center", - "padding": 10, + "padding": 20, }, ] } @@ -128,7 +128,7 @@ exports[`ReclaimPaymentConfirmationCard renders correctly for send payment confi ] } > - Alice The Person + John Doe - (209) 555-9790 + (415) 555-0000 + + + + + + $ + + + 10.00 + + + + + + John Doe + + + + +1 + + + (415) 555-0000 + + + + + + + + totalSent + + + + $ + 10.00 + + + + + + securityFee + + + + + + + + + + + + + totalRefunded + + + + $ + 10.00 + + + + + + + + + + + + + global:confirm + + + + + + + + + cancel + + + + + + + +`; + +exports[`ReclaimPaymentConfirmationScreen renders correctly 2`] = ` + + + + + + + + reclaimPayment + + + + + + + + + + + $ + + + 9.99 + + + + + + John Doe + + + + +1 + + + (415) 555-0000 + + + + + + + + totalSent + + + + $ + 10.00 + + + + + + securityFee + + + + + + + - + $ + 0.01 + + + + + + totalRefunded + + + + $ + 9.99 + + + + + + + + + + + + + global:confirm + + + + + + + + + cancel + + + + + + + +`; + +exports[`ReclaimPaymentConfirmationScreen renders correctly when fee calculation fails 1`] = ` + + + + + + + + reclaimPayment + + + + + + + + + + + $ + + + 10.00 + + + + + + John Doe + + + + +1 + + + (415) 555-0000 + + + + + + + + totalSent + + + + $ + 10.00 + + + + + + securityFee + + + + + + + + + + + + + totalRefunded + + + + $ + 10.00 + + + + + + + + + + + + + global:confirm + + + + + + + + + cancel + + + + + + + +`; + +exports[`ReclaimPaymentConfirmationScreen renders correctly when fee calculation fails 2`] = ` + + + + + + + + reclaimPayment + + + + + - 0.00 + 10.00 - Alice The Person + John Doe - (209) 555-9790 + (415) 555-0000 - - - $ - 0.00 + --- $ - 0.00 + 10.00 @@ -513,7 +2401,7 @@ exports[`ReclaimPaymentConfirmationScreen renders correctly 1`] = ` }, undefined, Object { - "backgroundColor": "#42D689", + "backgroundColor": "#A3EBC6", }, ] } @@ -530,8 +2418,8 @@ exports[`ReclaimPaymentConfirmationScreen renders correctly 1`] = ` style={ Object { "alignItems": "center", - "backgroundColor": "#42D689", - "borderColor": "#42D689", + "backgroundColor": "#A3EBC6", + "borderColor": "#A3EBC6", "borderRadius": 3, "borderWidth": 2, "flex": 1, diff --git a/packages/mobile/src/escrow/actions.ts b/packages/mobile/src/escrow/actions.ts index 2c103f3253b..f651d721436 100644 --- a/packages/mobile/src/escrow/actions.ts +++ b/packages/mobile/src/escrow/actions.ts @@ -1,10 +1,12 @@ import BigNumber from 'bignumber.js' -import { MinimalContact } from 'react-native-contacts' +import { ErrorMessages } from 'src/app/ErrorMessages' import { SHORT_CURRENCIES } from 'src/geth/consts' +import { RecipientWithContact } from 'src/recipients/recipient' export interface EscrowedPayment { senderAddress: string - recipient: MinimalContact | string + recipientPhone: string + recipientContact?: RecipientWithContact paymentID: string currency: SHORT_CURRENCIES amount: BigNumber @@ -13,22 +15,23 @@ export interface EscrowedPayment { expirySeconds: BigNumber } -// The number of seconds before the sender can revoke the payment. +// The number of seconds before the sender can reclaim the payment. export const EXPIRY_SECONDS = 432000 // 5 days in seconds export enum Actions { TRANSFER_PAYMENT = 'ESCROW/TRANSFER_PAYMENT', RECLAIM_PAYMENT = 'ESCROW/RECLAIM_PAYMENT', - GET_SENT_PAYMENTS = 'ESCROW/GET_SENT_PAYMENTS', + FETCH_SENT_PAYMENTS = 'ESCROW/FETCH_SENT_PAYMENTS', STORE_SENT_PAYMENTS = 'ESCROW/STORE_SENT_PAYMENTS', RESEND_PAYMENT = 'ESCROW/RESEND_PAYMENT', + RECLAIM_PAYMENT_SUCCESS = 'ESCROW/RECLAIM_PAYMENT_SUCCESS', + RECLAIM_PAYMENT_FAILURE = 'ESCROW/RECLAIM_PAYMENT_FAILURE', } export interface TransferPaymentAction { type: Actions.TRANSFER_PAYMENT phoneHash: string amount: BigNumber - txId: string tempWalletAddress: string } export interface ReclaimPaymentAction { @@ -36,8 +39,8 @@ export interface ReclaimPaymentAction { paymentID: string } -export interface GetSentPaymentsAction { - type: Actions.GET_SENT_PAYMENTS +export interface FetchSentPaymentsAction { + type: Actions.FETCH_SENT_PAYMENTS } export interface StoreSentPaymentsAction { @@ -50,23 +53,32 @@ export interface ResendPaymentAction { paymentId: string } +export interface ReclaimPaymentSuccessAction { + type: Actions.RECLAIM_PAYMENT_SUCCESS +} + +export interface ReclaimFailureAction { + type: Actions.RECLAIM_PAYMENT_FAILURE + error: ErrorMessages +} + export type ActionTypes = | TransferPaymentAction | ReclaimPaymentAction - | GetSentPaymentsAction + | FetchSentPaymentsAction | StoreSentPaymentsAction | ResendPaymentAction + | ReclaimPaymentSuccessAction + | ReclaimFailureAction export const transferEscrowedPayment = ( phoneHash: string, amount: BigNumber, - txId: string, tempWalletAddress: string ): TransferPaymentAction => ({ type: Actions.TRANSFER_PAYMENT, phoneHash, amount, - txId, tempWalletAddress, }) @@ -75,8 +87,8 @@ export const reclaimPayment = (paymentID: string): ReclaimPaymentAction => ({ paymentID, }) -export const getSentPayments = (): GetSentPaymentsAction => ({ - type: Actions.GET_SENT_PAYMENTS, +export const fetchSentPayments = (): FetchSentPaymentsAction => ({ + type: Actions.FETCH_SENT_PAYMENTS, }) export const storeSentPayments = (sentPayments: EscrowedPayment[]): StoreSentPaymentsAction => ({ @@ -88,3 +100,12 @@ export const resendPayment = (paymentId: string): ResendPaymentAction => ({ type: Actions.RESEND_PAYMENT, paymentId, }) + +export const reclaimPaymentSuccess = (): ReclaimPaymentSuccessAction => ({ + type: Actions.RECLAIM_PAYMENT_SUCCESS, +}) + +export const reclaimPaymentFailure = (error: ErrorMessages): ReclaimFailureAction => ({ + type: Actions.RECLAIM_PAYMENT_FAILURE, + error, +}) diff --git a/packages/mobile/src/escrow/reducer.ts b/packages/mobile/src/escrow/reducer.ts index 08053174371..175194f35db 100644 --- a/packages/mobile/src/escrow/reducer.ts +++ b/packages/mobile/src/escrow/reducer.ts @@ -1,10 +1,13 @@ import { Actions, ActionTypes, EscrowedPayment } from 'src/escrow/actions' +import { RootState } from 'src/redux/reducers' export interface State { + isReclaiming: boolean sentEscrowedPayments: EscrowedPayment[] } export const initialState = { + isReclaiming: false, sentEscrowedPayments: [], } @@ -15,7 +18,20 @@ export const escrowReducer = (state: State | undefined = initialState, action: A ...state, sentEscrowedPayments: action.sentPayments, } + case Actions.RECLAIM_PAYMENT: + return { + ...state, + isReclaiming: true, + } + case Actions.RECLAIM_PAYMENT_FAILURE: + case Actions.RECLAIM_PAYMENT_SUCCESS: + return { + ...state, + isReclaiming: false, + } default: return state } } + +export const sentEscrowedPaymentsSelector = (state: RootState) => state.escrow.sentEscrowedPayments diff --git a/packages/mobile/src/escrow/saga.ts b/packages/mobile/src/escrow/saga.ts index b6ec98d3683..68952a1aeea 100644 --- a/packages/mobile/src/escrow/saga.ts +++ b/packages/mobile/src/escrow/saga.ts @@ -1,156 +1,211 @@ -import { getEscrowContract, getStableTokenContract } from '@celo/contractkit' +import { getEscrowContract, getStableTokenContract } from '@celo/walletkit' +import { Escrow } from '@celo/walletkit/lib/types/Escrow' +import { StableToken } from '@celo/walletkit/types/StableToken' import BigNumber from 'bignumber.js' import { all, call, put, select, spawn, takeLeading } from 'redux-saga/effects' import { showError } from 'src/alert/actions' import { ErrorMessages } from 'src/app/ErrorMessages' -import { ERROR_BANNER_DURATION } from 'src/config' +import { ALERT_BANNER_DURATION } from 'src/config' import { Actions, EscrowedPayment, EXPIRY_SECONDS, + fetchSentPayments, ReclaimPaymentAction, + reclaimPaymentFailure, + reclaimPaymentSuccess, storeSentPayments, TransferPaymentAction, } from 'src/escrow/actions' -import { SHORT_CURRENCIES } from 'src/geth/consts' +import { sentEscrowedPaymentsSelector } from 'src/escrow/reducer' +import { CURRENCY_ENUM, SHORT_CURRENCIES } from 'src/geth/consts' import i18n from 'src/i18n' import { Actions as IdentityActions, EndVerificationAction } from 'src/identity/actions' import { NUM_ATTESTATIONS_REQUIRED } from 'src/identity/verification' +import { Invitees } from 'src/invite/actions' import { inviteesSelector } from 'src/invite/reducer' import { TEMP_PW } from 'src/invite/saga' import { isValidPrivateKey } from 'src/invite/utils' +import { navigate } from 'src/navigator/NavigationService' +import { Screens } from 'src/navigator/Screens' import { RootState } from 'src/redux/reducers' import { fetchDollarBalance } from 'src/stableToken/actions' -import { generateStandbyTransactionId } from 'src/transactions/actions' +import { addStandbyTransaction, generateStandbyTransactionId } from 'src/transactions/actions' +import { TransactionStatus, TransactionTypes } from 'src/transactions/reducer' import { sendAndMonitorTransaction } from 'src/transactions/saga' import { sendTransaction } from 'src/transactions/send' import Logger from 'src/utils/Logger' import { web3 } from 'src/web3/contracts' +import { fetchGasPrice } from 'src/web3/gas' import { getConnectedAccount, getConnectedUnlockedAccount } from 'src/web3/saga' const TAG = 'escrow/saga' function* transferStableTokenToEscrow(action: TransferPaymentAction) { + Logger.debug(TAG + '@transferToEscrow', 'Begin transfer to escrow') try { - const { phoneHash, amount, txId, tempWalletAddress } = action - const escrow = yield call(getEscrowContract, web3) - const stableToken = yield call(getStableTokenContract, web3) - - const account = yield call(getConnectedUnlockedAccount) + const { phoneHash, amount, tempWalletAddress } = action + const escrow: Escrow = yield call(getEscrowContract, web3) + const stableToken: StableToken = yield call(getStableTokenContract, web3) + const account: string = yield call(getConnectedUnlockedAccount) Logger.debug(TAG + '@transferToEscrow', 'Approving escrow transfer') const convertedAmount = web3.utils.toWei(amount.toString()) const approvalTx = stableToken.methods.approve(escrow.options.address, convertedAmount) - yield call(sendTransaction, approvalTx, account, TAG, txId) + yield call(sendTransaction, approvalTx, account, TAG, 'approval') Logger.debug(TAG + '@transferToEscrow', 'Transfering to escrow') + const transferTxId = generateStandbyTransactionId(escrow._address) + yield call(registerStandbyTransaction, transferTxId, amount.toString(), escrow._address) + const transferTx = escrow.methods.transfer( phoneHash, stableToken.options.address, - convertedAmount.toString(), + convertedAmount, EXPIRY_SECONDS, tempWalletAddress, NUM_ATTESTATIONS_REQUIRED ) - yield call(sendAndMonitorTransaction, txId, transferTx, account) - yield call(getSentPayments) + yield call(sendAndMonitorTransaction, transferTxId, transferTx, account) + yield put(fetchSentPayments()) } catch (e) { Logger.error(TAG + '@transferToEscrow', 'Error transfering to escrow', e) if (e.message === ErrorMessages.INCORRECT_PIN) { - yield put(showError(ErrorMessages.INCORRECT_PIN, ERROR_BANNER_DURATION)) + yield put(showError(ErrorMessages.INCORRECT_PIN, ALERT_BANNER_DURATION)) + } else { + yield put(showError(ErrorMessages.ESCROW_TRANSFER_FAILED, ALERT_BANNER_DURATION)) } - throw e } } +function* registerStandbyTransaction(id: string, value: string, address: string) { + yield put( + addStandbyTransaction({ + id, + type: TransactionTypes.SENT, + status: TransactionStatus.Pending, + value, + symbol: CURRENCY_ENUM.DOLLAR, + timestamp: Math.floor(Date.now() / 1000), + address, + comment: '', + }) + ) +} + function* withdrawFromEscrow(action: EndVerificationAction) { if (!action.success) { - Logger.debug( - TAG + '@withdrawFromEscrow', - 'Skipping escrow withdrawal because verification failed' - ) + Logger.debug(TAG + '@withdrawFromEscrow', 'Skipping withdrawal because verification failed') return } try { - const escrow = yield call(getEscrowContract, web3) - const account = yield call(getConnectedUnlockedAccount) + Logger.debug(TAG + '@withdrawFromEscrow', 'Withdrawing escrowed payment') + + const escrow: Escrow = yield call(getEscrowContract, web3) + const account: string = yield call(getConnectedUnlockedAccount) const inviteCode: string = yield select((state: RootState) => state.invite.redeemedInviteCode) - Logger.debug(TAG + '@withdrawFromEscrow', 'Withdrawing escrowed payment') if (!isValidPrivateKey(inviteCode)) { - Logger.error(TAG + '@withdrawFromEscrow', 'Invalid private key: ' + inviteCode) + Logger.warn(TAG + '@withdrawFromEscrow', 'Invalid private key, skipping escrow withdrawal') + return } const tempWalletAddress = web3.eth.accounts.privateKeyToAccount(inviteCode).address Logger.debug(TAG + '@withdrawFromEscrow', 'Added temp account to wallet: ' + tempWalletAddress) // Check if there is a payment associated with this invite code - const receivedPayment = yield call(getEscrowedPayment, tempWalletAddress) + const receivedPayment = yield call(getEscrowedPayment, escrow, tempWalletAddress) const value = new BigNumber(receivedPayment[3]) + if (!value.isGreaterThan(0)) { + Logger.warn(TAG + '@withdrawFromEscrow', 'Escrow payment is empty, skpping.') + return + } - if (value.toNumber() > 0) { - // Unlock temporary account - yield call(web3.eth.personal.unlockAccount, tempWalletAddress, TEMP_PW, 600) + // Unlock temporary account + yield call(web3.eth.personal.unlockAccount, tempWalletAddress, TEMP_PW, 600) - const msgHash = web3.utils.soliditySha3({ type: 'address', value: account }) + const msgHash = web3.utils.soliditySha3({ type: 'address', value: account }) - // using the temporary wallet account to sign a message. The message is the current account. - let signature = yield web3.eth.sign(msgHash, tempWalletAddress) - signature = signature.slice(2) - const r = `0x${signature.slice(0, 64)}` - const s = `0x${signature.slice(64, 128)}` - const v = web3.utils.hexToNumber(signature.slice(128, 130)) + // using the temporary wallet account to sign a message. The message is the current account. + let signature = yield web3.eth.sign(msgHash, tempWalletAddress) + signature = signature.slice(2) + const r = `0x${signature.slice(0, 64)}` + const s = `0x${signature.slice(64, 128)}` + const v = web3.utils.hexToNumber(signature.slice(128, 130)) - const withdrawTx = escrow.methods.withdraw(tempWalletAddress, v, r, s) - const txID = generateStandbyTransactionId(account) + const withdrawTx = escrow.methods.withdraw(tempWalletAddress, v, r, s) + const txID = generateStandbyTransactionId(account) - yield call(sendTransaction, withdrawTx, account, TAG, txID) + yield call(sendTransaction, withdrawTx, account, TAG, txID) - yield call(fetchDollarBalance) - yield call(getSentPayments) - Logger.showMessage(i18n.t('inviteFlow11:transferDollarsToAccount')) - } + yield put(fetchDollarBalance()) + Logger.showMessage(i18n.t('inviteFlow11:transferDollarsToAccount')) } catch (e) { Logger.error(TAG + '@withdrawFromEscrow', 'Error withdrawing payment from escrow', e) if (e.message === ErrorMessages.INCORRECT_PIN) { - yield put(showError(ErrorMessages.INCORRECT_PIN, ERROR_BANNER_DURATION)) + yield put(showError(ErrorMessages.INCORRECT_PIN, ALERT_BANNER_DURATION)) + } else { + yield put(showError(ErrorMessages.ESCROW_WITHDRAWAL_FAILED, ALERT_BANNER_DURATION)) } - throw e } } +async function createReclaimTransaction(paymentID: string) { + const escrow = await getEscrowContract(web3) + return escrow.methods.revoke(paymentID) +} + +export async function getReclaimEscrowFee(account: string, paymentID: string) { + // create mock transaction and get gas + const tx = await createReclaimTransaction(paymentID) + const txParams = { + from: account, + gasCurrency: (await getStableTokenContract(web3))._address, + } + const gas = new BigNumber(await tx.estimateGas(txParams)) + const gasPrice = new BigNumber(await fetchGasPrice()) + Logger.debug(`${TAG}/getReclaimEscrowFee`, `estimated gas: ${gas}`) + Logger.debug(`${TAG}/getReclaimEscrowFee`, `gas price: ${gasPrice}`) + const feeInWei = gas.multipliedBy(gasPrice) + Logger.debug(`${TAG}/getReclaimEscrowFee`, `New fee is: ${feeInWei}`) + return feeInWei +} + function* reclaimFromEscrow(action: ReclaimPaymentAction) { + Logger.debug(TAG + '@reclaimFromEscrow', 'Reclaiming escrowed payment') + try { const { paymentID } = action - const escrow = yield call(getEscrowContract, web3) const account = yield call(getConnectedUnlockedAccount) - Logger.debug(TAG + '@reclaimFromEscrow', 'Reclaiming escrowed payment') - const reclaimTx = escrow.methods.revoke(paymentID) - const txID = generateStandbyTransactionId(account) - yield call(sendTransaction, reclaimTx, account, TAG, txID) + const reclaimTx = yield call(createReclaimTransaction, paymentID) + yield call(sendTransaction, reclaimTx, account, TAG, 'escrow reclaim') - yield call(fetchDollarBalance) - yield call(getSentPayments) + yield put(fetchDollarBalance()) + yield put(fetchSentPayments()) + + yield call(navigate, Screens.WalletHome) + yield put(reclaimPaymentSuccess()) } catch (e) { Logger.error(TAG + '@reclaimFromEscrow', 'Error reclaiming payment from escrow', e) if (e.message === ErrorMessages.INCORRECT_PIN) { - yield put(showError(ErrorMessages.INCORRECT_PIN, ERROR_BANNER_DURATION)) + yield put(showError(ErrorMessages.INCORRECT_PIN, ALERT_BANNER_DURATION)) + } else { + yield put(showError(ErrorMessages.RECLAIMING_ESCROWED_PAYMENT_FAILED, ALERT_BANNER_DURATION)) } - throw e + yield put(reclaimPaymentFailure(e)) } } -function* getEscrowedPayment(paymentID: string) { - try { - const escrow = yield call(getEscrowContract, web3) +async function getEscrowedPayment(escrow: Escrow, paymentID: string) { + Logger.debug(TAG + '@getEscrowedPayment', 'Fetching escrowed payment') - Logger.debug(TAG + '@getEscrowedPayment', 'Fetching escrowed payment') - const payment = yield escrow.methods.escrowedPayments(paymentID).call() + try { + const payment = await escrow.methods.escrowedPayments(paymentID).call() return payment } catch (e) { Logger.error(TAG + '@getEscrowedPayment', 'Error fetching escrowed payment', e) @@ -158,25 +213,37 @@ function* getEscrowedPayment(paymentID: string) { } } -function* getSentPayments() { +function* doFetchSentPayments() { + Logger.debug(TAG + '@doFetchSentPayments', 'Fetching valid sent escrowed payments') + try { - const escrow = yield call(getEscrowContract, web3) - const account = yield call(getConnectedAccount) - const recipientsPhoneNumbers = yield select(inviteesSelector) + const escrow: Escrow = yield call(getEscrowContract, web3) + const account: string = yield call(getConnectedAccount) + const existingPayments: EscrowedPayment[] = yield select(sentEscrowedPaymentsSelector) + const existingPaymentsIds = new Set(existingPayments.map((p) => p.paymentID)) + + const sentPaymentIDs: string[] = yield escrow.methods.getSentPaymentIds(account).call() // Note: payment ids are currently temp wallet addresses + + const newPaymentIds = sentPaymentIDs.filter((id) => !existingPaymentsIds.has(id.toLowerCase())) + if (!newPaymentIds.length) { + Logger.debug(TAG + '@doFetchSentPayments', 'No new payments found') + return + } - Logger.debug(TAG + '@getSentPayments', 'Fetching valid sent escrowed payments') - const sentPaymentIDs: string[] = yield escrow.methods.getSentPaymentIds(account).call() const sentPayments = yield all( - sentPaymentIDs.map((paymentID) => call(getEscrowedPayment, paymentID)) + newPaymentIds.map((paymentID) => call(getEscrowedPayment, escrow, paymentID)) ) + const tempAddresstoRecipientPhoneNumber: Invitees = yield select(inviteesSelector) const sentPaymentsNotifications: EscrowedPayment[] = [] for (let i = 0; i < sentPayments.length; i++) { + const id = sentPaymentIDs[i].toLowerCase() const payment = sentPayments[i] + const recipientPhoneNumber = tempAddresstoRecipientPhoneNumber[id] const transformedPayment: EscrowedPayment = { + paymentID: id, senderAddress: payment[1], - recipient: recipientsPhoneNumbers[sentPaymentIDs[i]], - paymentID: sentPaymentIDs[i], + recipientPhone: recipientPhoneNumber, currency: SHORT_CURRENCIES.DOLLAR, // Only dollars can be escrowed amount: payment[3], timestamp: payment[6], @@ -184,10 +251,9 @@ function* getSentPayments() { } sentPaymentsNotifications.push(transformedPayment) } - yield put(storeSentPayments(sentPaymentsNotifications)) + yield put(storeSentPayments([...existingPayments, ...sentPaymentsNotifications])) } catch (e) { - Logger.error(TAG + '@getSentPayments', 'Error fetching sent escrowed payments', e) - throw e + Logger.error(TAG + '@doFetchSentPayments', 'Error fetching sent escrowed payments', e) } } @@ -199,17 +265,17 @@ export function* watchReclaimPayment() { yield takeLeading(Actions.RECLAIM_PAYMENT, reclaimFromEscrow) } -export function* watchGetSentPayments() { - yield takeLeading(Actions.GET_SENT_PAYMENTS, getSentPayments) +export function* watchFetchSentPayments() { + yield takeLeading(Actions.FETCH_SENT_PAYMENTS, doFetchSentPayments) } -function* watchVerificationEnd() { +export function* watchVerificationEnd() { yield takeLeading(IdentityActions.END_VERIFICATION, withdrawFromEscrow) } export function* escrowSaga() { yield spawn(watchTransferPayment) yield spawn(watchReclaimPayment) - yield spawn(watchGetSentPayments) + yield spawn(watchFetchSentPayments) yield spawn(watchVerificationEnd) } diff --git a/packages/mobile/src/exchange/ExchangeConfirmationCard.tsx b/packages/mobile/src/exchange/ExchangeConfirmationCard.tsx index d8bbdea28d9..5f1a0073f89 100644 --- a/packages/mobile/src/exchange/ExchangeConfirmationCard.tsx +++ b/packages/mobile/src/exchange/ExchangeConfirmationCard.tsx @@ -75,7 +75,6 @@ class ExchangeConfirmationCard extends React.PureComponent { amount={leftCurrencyAmount.toString()} size={36} type={this.props.token} - balanceOutOfSync={false} /> @@ -85,7 +84,6 @@ class ExchangeConfirmationCard extends React.PureComponent { amount={rightCurrencyAmount.toString()} size={36} type={this.takerToken()} - balanceOutOfSync={false} /> diff --git a/packages/mobile/src/exchange/ExchangeHomeScreen.tsx b/packages/mobile/src/exchange/ExchangeHomeScreen.tsx index 72b0e357404..89c796aa62c 100644 --- a/packages/mobile/src/exchange/ExchangeHomeScreen.tsx +++ b/packages/mobile/src/exchange/ExchangeHomeScreen.tsx @@ -16,7 +16,7 @@ import ExchangeRate from 'src/exchange/ExchangeRate' import { CURRENCY_ENUM as Token } from 'src/geth/consts' import { Namespaces } from 'src/i18n' import { navigate } from 'src/navigator/NavigationService' -import { Screens } from 'src/navigator/Screens' +import { Stacks } from 'src/navigator/Screens' import { RootState } from 'src/redux/reducers' import DisconnectBanner from 'src/shared/DisconnectBanner' import { getRateForMakerToken } from 'src/utils/currencyExchange' @@ -37,14 +37,10 @@ const mapStateToProps = (state: RootState): StateProps => ({ function goToTrade() { CeloAnalytics.track(CustomEventNames.exchange_button) - navigate(Screens.ExchangeStack) + navigate(Stacks.ExchangeStack) } export class ExchangeHomeScreen extends React.Component { - static navigationOptions = { - title: 'Exchange Home', - } - componentDidMount() { this.props.fetchExchangeRate() } diff --git a/packages/mobile/src/exchange/ExchangeTradeScreen.tsx b/packages/mobile/src/exchange/ExchangeTradeScreen.tsx index ef9bb2b82a4..78be941441d 100644 --- a/packages/mobile/src/exchange/ExchangeTradeScreen.tsx +++ b/packages/mobile/src/exchange/ExchangeTradeScreen.tsx @@ -10,7 +10,6 @@ import * as React from 'react' import { withNamespaces, WithNamespaces } from 'react-i18next' import { StyleSheet, Text, TextInput, View } from 'react-native' import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view' -import { NavigationScreenProps } from 'react-navigation' import { connect } from 'react-redux' import { hideAlert, showError } from 'src/alert/actions' import { errorSelector } from 'src/alert/reducer' @@ -18,13 +17,13 @@ import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames, DefaultEventNames } from 'src/analytics/constants' import componentWithAnalytics from 'src/analytics/wrapper' import { ErrorMessages } from 'src/app/ErrorMessages' -import CancelButton from 'src/components/CancelButton' -import { ERROR_BANNER_DURATION } from 'src/config' +import { ALERT_BANNER_DURATION } from 'src/config' import { fetchExchangeRate } from 'src/exchange/actions' import ExchangeRate from 'src/exchange/ExchangeRate' import { ExchangeRatePair } from 'src/exchange/reducer' import { CURRENCY_ENUM as Token } from 'src/geth/consts' import i18n, { Namespaces } from 'src/i18n' +import { headerWithCancelButton } from 'src/navigator/Headers' import { navigate, navigateBack } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { RootState } from 'src/redux/reducers' @@ -67,11 +66,9 @@ const mapStateToProps = (state: RootState): StateProps => ({ }) export class ExchangeTradeScreen extends React.Component { - static navigationOptions = ({ navigation }: NavigationScreenProps) => ({ + static navigationOptions = () => ({ + ...headerWithCancelButton, headerTitle: i18n.t(`${Namespaces.exchangeFlow9}:exchange`), - headerTitleStyle: [fontStyles.headerTitle, componentStyles.screenHeader], - headerRight: , // This helps vertically center the title - headerLeft: , }) state = { @@ -104,7 +101,7 @@ export class ExchangeTradeScreen extends React.Component { if (this.getMakerBalance().isLessThan(amount)) { this.props.showError( this.isDollar() ? ErrorMessages.NSF_DOLLARS : ErrorMessages.NSF_GOLD, - ERROR_BANNER_DURATION + ALERT_BANNER_DURATION ) } else { this.props.hideAlert() diff --git a/packages/mobile/src/exchange/actions.ts b/packages/mobile/src/exchange/actions.ts index 51e1b45de8e..ee01587fe33 100644 --- a/packages/mobile/src/exchange/actions.ts +++ b/packages/mobile/src/exchange/actions.ts @@ -3,15 +3,15 @@ import { getExchangeContract, getGoldTokenContract, getStableTokenContract, -} from '@celo/contractkit' -import { Exchange as ExchangeType } from '@celo/contractkit/types/Exchange' -import { GoldToken as GoldTokenType } from '@celo/contractkit/types/GoldToken' -import { StableToken as StableTokenType } from '@celo/contractkit/types/StableToken' +} from '@celo/walletkit' +import { Exchange as ExchangeType } from '@celo/walletkit/types/Exchange' +import { GoldToken as GoldTokenType } from '@celo/walletkit/types/GoldToken' +import { StableToken as StableTokenType } from '@celo/walletkit/types/StableToken' import BigNumber from 'bignumber.js' import { call, put, select } from 'redux-saga/effects' import { showError } from 'src/alert/actions' import { ErrorMessages } from 'src/app/ErrorMessages' -import { ERROR_BANNER_DURATION } from 'src/config' +import { ALERT_BANNER_DURATION } from 'src/config' import { ExchangeRatePair } from 'src/exchange/reducer' import { CURRENCY_ENUM as Tokens } from 'src/geth/consts' import { RootState } from 'src/redux/reducers' @@ -131,7 +131,7 @@ export function* doFetchExchangeRate(makerAmount?: BigNumber, makerToken?: Token ) } catch (error) { Logger.error(TAG, 'Error fetching exchange rate', error) - yield put(showError(ErrorMessages.EXCHANGE_RATE_FAILED, ERROR_BANNER_DURATION)) + yield put(showError(ErrorMessages.EXCHANGE_RATE_FAILED, ALERT_BANNER_DURATION)) } } @@ -202,7 +202,7 @@ export function* exchangeGoldAndStableTokens(action: ExchangeTokensAction) { TAG, `Not receiving enough ${makerToken} due to change in exchange rate. Exchange failed.` ) - yield put(showError(ErrorMessages.EXCHANGE_RATE_CHANGE, ERROR_BANNER_DURATION)) + yield put(showError(ErrorMessages.EXCHANGE_RATE_CHANGE, ALERT_BANNER_DURATION)) return } @@ -249,7 +249,7 @@ export function* exchangeGoldAndStableTokens(action: ExchangeTokensAction) { yield call(sendAndMonitorTransaction, txId, tx, account) } catch (error) { Logger.error(TAG, 'Error doing exchange', error) - yield put(showError(ErrorMessages.EXCHANGE_FAILED, ERROR_BANNER_DURATION)) + yield put(showError(ErrorMessages.EXCHANGE_FAILED, ALERT_BANNER_DURATION)) if (txId) { yield put(removeStandbyTransaction(txId)) } diff --git a/packages/mobile/src/fees/CalculateFee.tsx b/packages/mobile/src/fees/CalculateFee.tsx new file mode 100644 index 00000000000..a3f2a1642c0 --- /dev/null +++ b/packages/mobile/src/fees/CalculateFee.tsx @@ -0,0 +1,132 @@ +import { getStableTokenContract } from '@celo/walletkit' +import BigNumber from 'bignumber.js' +import React, { FunctionComponent, useEffect } from 'react' +import { useAsync, UseAsyncReturn } from 'react-async-hook' +import { connect } from 'react-redux' +import { showError } from 'src/alert/actions' +import { ErrorMessages } from 'src/app/ErrorMessages' +import { ALERT_BANNER_DURATION } from 'src/config' +import { getReclaimEscrowFee } from 'src/escrow/saga' +import { FeeType } from 'src/fees/actions' +import { getInvitationVerificationFee } from 'src/invite/saga' +import { getSendFee } from 'src/send/saga' + +export type CalculateFeeChildren = ( + asyncResult: UseAsyncReturn +) => React.ReactNode + +interface CommonProps { + children: CalculateFeeChildren +} + +interface InviteProps extends CommonProps { + feeType: FeeType.INVITE +} + +interface SendProps extends CommonProps { + feeType: FeeType.SEND + account: string + recipientAddress: string + amount: BigNumber + comment: string +} + +interface ExchangeProps extends CommonProps { + feeType: FeeType.EXCHANGE + // TODO +} + +interface ReclaimEscrowProps extends CommonProps { + feeType: FeeType.RECLAIM_ESCROW + account: string + paymentID: string +} + +type OwnProps = InviteProps | SendProps | ExchangeProps | ReclaimEscrowProps + +// TODO: remove this once we use TS 3.5 +type Omit = Pick> + +export type PropsWithoutChildren = + | Omit + | Omit + | Omit + | Omit + +interface DispatchProps { + showError: typeof showError +} + +type Props = DispatchProps & OwnProps + +const mapDispatchToProps = { + showError, +} + +function useAsyncShowError( + asyncFunction: ((...args: Args) => Promise) | (() => Promise), + params: Args, + showErrorFunction: typeof showError +): UseAsyncReturn { + const asyncResult = useAsync(asyncFunction, params) + + useEffect( + () => { + // Generic error banner + if (asyncResult.error) { + showErrorFunction(ErrorMessages.CALCULATE_FEE_FAILED, ALERT_BANNER_DURATION) + } + }, + [asyncResult.error] + ) + + return asyncResult +} + +const CalculateInviteFee: FunctionComponent = (props) => { + const asyncResult = useAsyncShowError(getInvitationVerificationFee, [], props.showError) + return props.children(asyncResult) as React.ReactElement +} + +const CalculateSendFee: FunctionComponent = (props) => { + const asyncResult = useAsyncShowError( + (account: string, recipientAddress: string, amount: BigNumber, comment: string) => + getSendFee(account, getStableTokenContract, { + recipientAddress, + amount: amount.valueOf(), + comment, + }), + [props.account, props.recipientAddress, props.amount, props.comment], + props.showError + ) + return props.children(asyncResult) as React.ReactElement +} + +const CalculateReclaimEscrowFee: FunctionComponent = ( + props +) => { + const asyncResult = useAsyncShowError( + getReclaimEscrowFee, + [props.account, props.paymentID], + props.showError + ) + return props.children(asyncResult) as React.ReactElement +} + +const CalculateFee = (props: Props) => { + switch (props.feeType) { + case FeeType.INVITE: + return + case FeeType.SEND: + return + case FeeType.RECLAIM_ESCROW: + return + } + + throw new Error(`Unsupported feeType: ${props.feeType}`) +} + +export default connect<{}, DispatchProps, OwnProps, {}>( + null, + mapDispatchToProps +)(CalculateFee) diff --git a/packages/mobile/src/fees/EstimateFee.tsx b/packages/mobile/src/fees/EstimateFee.tsx new file mode 100644 index 00000000000..0573cccb465 --- /dev/null +++ b/packages/mobile/src/fees/EstimateFee.tsx @@ -0,0 +1,34 @@ +import { useEffect } from 'react' +import { connect } from 'react-redux' +import { estimateFee as estimateFeeAction, FeeType } from 'src/fees/actions' +import { RootState } from 'src/redux/reducers' + +interface DispatchProps { + estimateFee: typeof estimateFeeAction +} + +interface OwnProps { + feeType: FeeType +} + +type Props = DispatchProps & OwnProps + +const mapDispatchToProps = { + estimateFee: estimateFeeAction, +} + +export function EstimateFee({ feeType, estimateFee }: Props) { + useEffect( + () => { + estimateFee(feeType) + }, + [feeType] + ) + + return null +} + +export default connect<{}, DispatchProps, OwnProps, RootState>( + null, + mapDispatchToProps +)(EstimateFee) diff --git a/packages/mobile/src/fees/actions.ts b/packages/mobile/src/fees/actions.ts new file mode 100644 index 00000000000..2e4e0db1e1e --- /dev/null +++ b/packages/mobile/src/fees/actions.ts @@ -0,0 +1,35 @@ +export enum Actions { + ESTIMATE_FEE = 'FEES/ESTIMATE_FEE', + FEE_ESTIMATED = 'FEES/FEE_ESTIMATED', +} + +export enum FeeType { + INVITE = 'invite', + SEND = 'send', + EXCHANGE = 'exchange', + RECLAIM_ESCROW = 'reclaim-escrow', +} + +export interface EstimateFeeAction { + type: Actions.ESTIMATE_FEE + feeType: FeeType +} + +export interface FeeEstimatedAction { + type: Actions.FEE_ESTIMATED + feeType: FeeType + feeInWei: string +} + +export type ActionTypes = EstimateFeeAction | FeeEstimatedAction + +export const estimateFee = (feeType: FeeType): EstimateFeeAction => ({ + type: Actions.ESTIMATE_FEE, + feeType, +}) + +export const feeEstimated = (feeType: FeeType, feeInWei: string): FeeEstimatedAction => ({ + type: Actions.FEE_ESTIMATED, + feeType, + feeInWei, +}) diff --git a/packages/mobile/src/fees/reducer.ts b/packages/mobile/src/fees/reducer.ts new file mode 100644 index 00000000000..65bbc961004 --- /dev/null +++ b/packages/mobile/src/fees/reducer.ts @@ -0,0 +1,51 @@ +import { combineReducers } from 'redux' +import { Actions, ActionTypes, FeeType } from 'src/fees/actions' + +interface FeeEstimateState { + feeInWei: string | null + lastUpdated: number | null +} + +const feeEstimateInitialState = { + feeInWei: null, + lastUpdated: null, +} + +function createEstimateReducer(feeType: FeeType) { + return function estimateReducer( + state: FeeEstimateState = feeEstimateInitialState, + action: ActionTypes + ): FeeEstimateState { + if (action.feeType !== feeType) { + return state + } + + switch (action.type) { + case Actions.FEE_ESTIMATED: + return { + ...state, + feeInWei: action.feeInWei, + } + default: + return state + } + } +} + +export interface State { + estimates: { + invite: FeeEstimateState + send: FeeEstimateState + exchange: FeeEstimateState + reclaimEscrow: FeeEstimateState + } +} + +const estimatesReducer = combineReducers({ + invite: createEstimateReducer(FeeType.INVITE), + send: createEstimateReducer(FeeType.SEND), + exchange: createEstimateReducer(FeeType.EXCHANGE), + reclaimEscrow: createEstimateReducer(FeeType.RECLAIM_ESCROW), +}) + +export const reducer = combineReducers({ estimates: estimatesReducer }) diff --git a/packages/mobile/src/fees/saga.test.ts b/packages/mobile/src/fees/saga.test.ts new file mode 100644 index 00000000000..6a769fa5235 --- /dev/null +++ b/packages/mobile/src/fees/saga.test.ts @@ -0,0 +1,28 @@ +import { expectSaga } from 'redux-saga-test-plan' +import { call, select } from 'redux-saga/effects' +import { estimateFee, feeEstimated, FeeType } from 'src/fees/actions' +import { watchEstimateFee } from 'src/fees/saga' +import { getInvitationVerificationFee } from 'src/invite/saga' +import { currentAccountSelector } from 'src/web3/selectors' +import { mockAccount } from 'test/values' + +describe(watchEstimateFee, () => { + beforeAll(() => { + jest.useRealTimers() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it('updates the default invite fee', async () => { + await expectSaga(watchEstimateFee) + .dispatch(estimateFee(FeeType.INVITE)) + .provide([ + [select(currentAccountSelector), mockAccount], + [call(getInvitationVerificationFee), '42'], + ]) + .put(feeEstimated(FeeType.INVITE, '42')) + .silentRun() + }) +}) diff --git a/packages/mobile/src/fees/saga.ts b/packages/mobile/src/fees/saga.ts new file mode 100644 index 00000000000..ac0e681dad4 --- /dev/null +++ b/packages/mobile/src/fees/saga.ts @@ -0,0 +1,58 @@ +import { getStableTokenContract } from '@celo/walletkit' +import { call, put, select, spawn, takeLeading } from 'redux-saga/effects' +import { getReclaimEscrowFee } from 'src/escrow/saga' +import { Actions, EstimateFeeAction, feeEstimated, FeeType } from 'src/fees/actions' +import { getInvitationVerificationFee } from 'src/invite/saga' +import { getSendFee } from 'src/send/saga' +import { CeloDefaultRecipient } from 'src/send/Send' +import Logger from 'src/utils/Logger' +import { currentAccountSelector } from 'src/web3/selectors' + +const TAG = 'fees/saga' + +export function* estimateFeeSaga({ feeType }: EstimateFeeAction) { + Logger.debug(TAG + '@estimateFeeSaga', `updating for ${feeType}`) + + // TODO: skip fee update if it was calculated recently + const account = yield select(currentAccountSelector) + + let feeInWei + + switch (feeType) { + case FeeType.INVITE: + feeInWei = yield call(getInvitationVerificationFee) + break + case FeeType.SEND: + // Just use default values here since it doesn't matter for fee estimation + feeInWei = yield call(getSendFee, account, getStableTokenContract, { + recipientAddress: CeloDefaultRecipient.address, + amount: '1', + comment: 'Coffee or Tea?', + }) + break + case FeeType.EXCHANGE: + // TODO + break + case FeeType.RECLAIM_ESCROW: + // Just use default values here since it doesn't matter for fee estimation + feeInWei = yield call( + getReclaimEscrowFee, + CeloDefaultRecipient.address, + CeloDefaultRecipient.address + ) + break + } + + if (feeInWei) { + Logger.debug(`${TAG}/estimateFeeSaga`, `New fee is: ${feeInWei}`) + yield put(feeEstimated(feeType, feeInWei)) + } +} + +export function* watchEstimateFee() { + yield takeLeading(Actions.ESTIMATE_FEE, estimateFeeSaga) +} + +export function* feesSaga() { + yield spawn(watchEstimateFee) +} diff --git a/packages/mobile/src/fees/selectors.ts b/packages/mobile/src/fees/selectors.ts new file mode 100644 index 00000000000..4c3ba57d81d --- /dev/null +++ b/packages/mobile/src/fees/selectors.ts @@ -0,0 +1,52 @@ +import BigNumber from 'bignumber.js' +import { createSelector } from 'reselect' +import { FeeType } from 'src/fees/actions' +import { RootState } from 'src/redux/reducers' +import { divideByWei } from 'src/utils/formatting' + +const getInviteFeeEstimateInWei = (state: RootState) => state.fees.estimates.invite.feeInWei +const getSendFeeEstimateInWei = (state: RootState) => state.fees.estimates.send.feeInWei +const getExchangeFeeEstimateInWei = (state: RootState) => state.fees.estimates.exchange.feeInWei +const getReclaimEscrowFeeEstimateInWei = (state: RootState) => + state.fees.estimates.reclaimEscrow.feeInWei + +export function getFeeDollars(feeInWei: BigNumber | string) { + const adjustedFee = divideByWei( + feeInWei instanceof BigNumber ? feeInWei.toString() : feeInWei, + 18 + ) + return new BigNumber(adjustedFee) +} + +const feeEstimateDollarsSelectorFactory = (feeSelector: (state: RootState) => string | null) => { + return createSelector(feeSelector, (feeInWei) => { + if (!feeInWei) { + return null + } + return getFeeDollars(feeInWei) + }) +} + +export const getInviteFeeEstimateDollars = feeEstimateDollarsSelectorFactory( + getInviteFeeEstimateInWei +) +export const getSendFeeEstimateDollars = feeEstimateDollarsSelectorFactory(getSendFeeEstimateInWei) +export const getExchangeFeeEstimateDollars = feeEstimateDollarsSelectorFactory( + getExchangeFeeEstimateInWei +) +export const getReclaimEscrowFeeEstimateDollars = feeEstimateDollarsSelectorFactory( + getReclaimEscrowFeeEstimateInWei +) + +export const getFeeEstimateDollars = (state: RootState, feeType: FeeType) => { + switch (feeType) { + case FeeType.INVITE: + return getInviteFeeEstimateDollars(state) + case FeeType.SEND: + return getSendFeeEstimateDollars(state) + case FeeType.EXCHANGE: + return getExchangeFeeEstimateDollars(state) + case FeeType.RECLAIM_ESCROW: + return getReclaimEscrowFeeEstimateDollars(state) + } +} diff --git a/packages/mobile/src/firebase/notifications.ts b/packages/mobile/src/firebase/notifications.ts index ce58405f273..1039ced6742 100644 --- a/packages/mobile/src/firebase/notifications.ts +++ b/packages/mobile/src/firebase/notifications.ts @@ -1,4 +1,3 @@ -import { getStableTokenContract } from '@celo/contractkit' import BigNumber from 'bignumber.js' import { Notification } from 'react-native-firebase/notifications' import { @@ -8,12 +7,12 @@ import { TransferNotificationData, } from 'src/account' import { showMessage } from 'src/alert/actions' -import { ERROR_BANNER_DURATION } from 'src/config' +import { ALERT_BANNER_DURATION } from 'src/config' import { resolveCurrency } from 'src/geth/consts' import { refreshAllBalances } from 'src/home/actions' -import { lookupPhoneNumberAddress } from 'src/identity/verification' +import { getRecipientFromPaymentRequest } from 'src/paymentRequest/utils' +import { getRecipientFromAddress } from 'src/recipients/recipient' import { DispatchType, GetStateType } from 'src/redux/reducers' -import { updateSuggestedFee } from 'src/send/actions' import { navigateToPaymentTransferReview, navigateToRequestedPaymentReview, @@ -21,7 +20,6 @@ import { import { TransactionTypes } from 'src/transactions/reducer' import { divideByWei } from 'src/utils/formatting' import Logger from 'src/utils/Logger' -import { getRecipientFromAddress, phoneNumberToRecipient } from 'src/utils/recipient' const TAG = 'FirebaseNotifications' @@ -32,37 +30,20 @@ const handlePaymentRequested = ( if (notificationState === NotificationReceiveState.APP_ALREADY_OPEN) { return } - const { e164NumberToAddress } = getState().identity - const { recipientCache } = getState().send - let requesterAddress = e164NumberToAddress[paymentRequest.requesterE164Number] - if (!requesterAddress) { - const resolvedAddress = await lookupPhoneNumberAddress(paymentRequest.requesterE164Number) - if (!resolvedAddress) { - Logger.error(TAG, 'Unable to resolve requester address') - return - } - requesterAddress = resolvedAddress + + if (!paymentRequest.requesterAddress) { + Logger.error(TAG, 'Payment request must specify a requester address') + return } - const recipient = phoneNumberToRecipient( - paymentRequest.requesterE164Number, - requesterAddress, - recipientCache - ) - const fee = await dispatch( - updateSuggestedFee(true, getStableTokenContract, { - recipientAddress: requesterAddress!, - amount: paymentRequest.amount, - comment: paymentRequest.comment, - }) - ) + const { recipientCache } = getState().recipients + const targetRecipient = getRecipientFromPaymentRequest(paymentRequest, recipientCache) navigateToRequestedPaymentReview({ - recipient, + recipient: targetRecipient, amount: new BigNumber(paymentRequest.amount), reason: paymentRequest.comment, - recipientAddress: requesterAddress!, - fee, + recipientAddress: targetRecipient.address, }) } @@ -73,7 +54,7 @@ const handlePaymentReceived = ( dispatch(refreshAllBalances()) if (notificationState !== NotificationReceiveState.APP_ALREADY_OPEN) { - const { recipientCache } = getState().send + const { recipientCache } = getState().recipients const { addressToE164Number } = getState().identity const address = transferNotification.sender.toLowerCase() @@ -81,7 +62,7 @@ const handlePaymentReceived = ( TransactionTypes.RECEIVED, new BigNumber(transferNotification.timestamp).toNumber(), { - value: new BigNumber(divideByWei(transferNotification.value)), + value: divideByWei(transferNotification.value), currency: resolveCurrency(transferNotification.currency), address: transferNotification.sender.toLowerCase(), comment: transferNotification.comment, @@ -97,7 +78,7 @@ export const handleNotification = ( notificationState: NotificationReceiveState ) => async (dispatch: DispatchType, getState: GetStateType) => { if (notificationState === NotificationReceiveState.APP_ALREADY_OPEN) { - dispatch(showMessage(notification.title, ERROR_BANNER_DURATION)) + dispatch(showMessage(notification.title, ALERT_BANNER_DURATION)) } switch (notification.data.type) { case NotificationTypes.PAYMENT_REQUESTED: diff --git a/packages/mobile/src/firebase/saga.ts b/packages/mobile/src/firebase/saga.ts index 577887da2f0..cf7896256ff 100644 --- a/packages/mobile/src/firebase/saga.ts +++ b/packages/mobile/src/firebase/saga.ts @@ -6,7 +6,7 @@ import { PaymentRequest, PaymentRequestStatuses, updatePaymentRequests } from 's import { showError } from 'src/alert/actions' import { Actions as AppActions } from 'src/app/actions' import { ErrorMessages } from 'src/app/ErrorMessages' -import { ERROR_BANNER_DURATION, FIREBASE_ENABLED } from 'src/config' +import { ALERT_BANNER_DURATION, FIREBASE_ENABLED } from 'src/config' import { Actions, firebaseAuthorized } from 'src/firebase/actions' import { initializeAuth, initializeCloudMessaging, setUserLanguage } from 'src/firebase/firebase' import Logger from 'src/utils/Logger' @@ -33,7 +33,7 @@ function* initializeFirebase() { if (!FIREBASE_ENABLED) { Logger.info(TAG, 'Firebase disabled') - yield put(showError(ErrorMessages.FIREBASE_DISABLED, ERROR_BANNER_DURATION)) + yield put(showError(ErrorMessages.FIREBASE_DISABLED, ALERT_BANNER_DURATION)) return } @@ -54,7 +54,7 @@ function* initializeFirebase() { return } catch (error) { Logger.error(TAG, 'Error while initializing firebase', error) - yield put(showError(ErrorMessages.FIREBASE_FAILED, ERROR_BANNER_DURATION)) + yield put(showError(ErrorMessages.FIREBASE_FAILED, ALERT_BANNER_DURATION)) } } diff --git a/packages/mobile/src/geth/consts.ts b/packages/mobile/src/geth/consts.ts index c93b6b838e3..a8776db5d3c 100644 --- a/packages/mobile/src/geth/consts.ts +++ b/packages/mobile/src/geth/consts.ts @@ -1,4 +1,4 @@ -export const WEI_PER_JEM = 1000000000000000000.0 +export const WEI_PER_CELO = 1000000000000000000.0 export const GAS_PER_TRANSACTION = 21001.0 export const GAS_PRICE_STALE_AFTER = 15000 // 1.5 seconds export const GAS_PRICE_PLACEHOLDER = 18000000000 diff --git a/packages/mobile/src/geth/geth.ts b/packages/mobile/src/geth/geth.ts index b43a1114c24..987ac733c6d 100644 --- a/packages/mobile/src/geth/geth.ts +++ b/packages/mobile/src/geth/geth.ts @@ -1,4 +1,4 @@ -import { GenesisBlockUtils, StaticNodeUtils } from '@celo/contractkit' +import { GenesisBlockUtils, StaticNodeUtils } from '@celo/walletkit' import BigNumber from 'bignumber.js' import DeviceInfo from 'react-native-device-info' import * as RNFS from 'react-native-fs' diff --git a/packages/mobile/src/geth/network-config.ts b/packages/mobile/src/geth/network-config.ts index 44f1da204bd..90167d80021 100644 --- a/packages/mobile/src/geth/network-config.ts +++ b/packages/mobile/src/geth/network-config.ts @@ -6,10 +6,6 @@ export default { nodeDir: `.${Testnets.integration}`, syncMode: SYNC_MODE_ULTRALIGHT, }, - [Testnets.appintegration]: { - nodeDir: `.${Testnets.appintegration}`, - syncMode: SYNC_MODE_ULTRALIGHT, - }, [Testnets.alfajoresstaging]: { nodeDir: `.${Testnets.alfajoresstaging}`, syncMode: SYNC_MODE_ULTRALIGHT, @@ -18,4 +14,12 @@ export default { nodeDir: `.${Testnets.alfajores}`, syncMode: SYNC_MODE_ULTRALIGHT, }, + [Testnets.pilot]: { + nodeDir: `.${Testnets.pilot}`, + syncMode: SYNC_MODE_ULTRALIGHT, + }, + [Testnets.pilotstaging]: { + nodeDir: `.${Testnets.pilotstaging}`, + syncMode: SYNC_MODE_ULTRALIGHT, + }, } diff --git a/packages/mobile/src/goldToken/saga.ts b/packages/mobile/src/goldToken/saga.ts index 2b96d2bd40d..06ebe9d00f8 100644 --- a/packages/mobile/src/goldToken/saga.ts +++ b/packages/mobile/src/goldToken/saga.ts @@ -1,4 +1,4 @@ -import { getGoldTokenContract } from '@celo/contractkit' +import { getGoldTokenContract } from '@celo/walletkit' import { spawn } from 'redux-saga/effects' import { CURRENCY_ENUM } from 'src/geth/consts' import { Actions, fetchGoldBalance, setBalance } from 'src/goldToken/actions' diff --git a/packages/mobile/src/home/NotificationBox.tsx b/packages/mobile/src/home/NotificationBox.tsx index f4c6875a5cd..13d8657784d 100644 --- a/packages/mobile/src/home/NotificationBox.tsx +++ b/packages/mobile/src/home/NotificationBox.tsx @@ -69,9 +69,7 @@ export class NotificationBox extends React.Component { escrowedPaymentReminderNotification = (): Array> => { const activeSentPayments = this.filterValidPayments() const activeSentPaymentNotifications: Array> = activeSentPayments.map( - (payment, i) => ( - - ) + (payment) => ) return activeSentPaymentNotifications } @@ -81,7 +79,7 @@ export class NotificationBox extends React.Component { const validSentPayments: EscrowedPayment[] = [] sentPayments.forEach((payment) => { const paymentExpiryTime = +payment.timestamp + +payment.expirySeconds - const currUnixTime = Date.now() + const currUnixTime = Date.now() / 1000 if (currUnixTime >= paymentExpiryTime) { validSentPayments.push(payment) } @@ -227,7 +225,11 @@ export class NotificationBox extends React.Component { } render() { - const notifications = [...this.paymentRequestsNotification(), ...this.generalNotifications()] + const notifications = [ + ...this.paymentRequestsNotification(), + ...this.escrowedPaymentReminderNotification(), + ...this.generalNotifications(), + ] if (!notifications || !notifications.length) { // No notifications, no slider diff --git a/packages/mobile/src/home/WalletHome.test.tsx b/packages/mobile/src/home/WalletHome.test.tsx new file mode 100644 index 00000000000..fd960447e93 --- /dev/null +++ b/packages/mobile/src/home/WalletHome.test.tsx @@ -0,0 +1,56 @@ +import * as React from 'react' +import { Provider } from 'react-redux' +import * as renderer from 'react-test-renderer' +import { WalletHome } from 'src/home/WalletHome' +import { createMockStore, getMockI18nProps } from 'test/utils' + +const TWO_DAYS_MS = 2 * 24 * 60 * 1000 + +const storeData = { + goldToken: { educationCompleted: true }, + account: { + backupCompleted: true, + dismissedEarnRewards: true, + dismissedInviteFriends: true, + accountCreationTime: new Date().getTime() - TWO_DAYS_MS, + paymentRequests: [], + }, +} + +jest.mock('src/components/AccountOverview') +jest.mock('src/home/TransactionsList') + +describe('Testnet banner', () => { + it('Shows testnet banner for 5 seconds', async () => { + const store = createMockStore({ + ...storeData, + account: { + backupCompleted: false, + }, + }) + const showMessageMock = jest.fn() + const tree = renderer.create( + + + + ) + + expect(tree).toMatchSnapshot() + expect(showMessageMock).toHaveBeenCalledWith('testnetAlert.1', 5000, null, 'testnetAlert.0') + }) +}) diff --git a/packages/mobile/src/home/WalletHome.tsx b/packages/mobile/src/home/WalletHome.tsx index 2e1c009c4b9..0a9fc380644 100644 --- a/packages/mobile/src/home/WalletHome.tsx +++ b/packages/mobile/src/home/WalletHome.tsx @@ -23,6 +23,7 @@ import { hideAlert, showMessage } from 'src/alert/actions' import componentWithAnalytics from 'src/analytics/wrapper' import { exitBackupFlow } from 'src/app/actions' import AccountOverview from 'src/components/AccountOverview' +import { ALERT_BANNER_DURATION } from 'src/config' import { refreshAllBalances, setLoading } from 'src/home/actions' import NotificationBox from 'src/home/NotificationBox' import { callToActNotificationSelector, getActiveNotificationCount } from 'src/home/selectors' @@ -31,11 +32,13 @@ import { Namespaces } from 'src/i18n' import { importContacts } from 'src/identity/actions' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' +import { withDispatchAfterNavigate } from 'src/navigator/WithDispatchAfterNavigate' +import { NumberToRecipient } from 'src/recipients/recipient' +import { recipientCacheSelector } from 'src/recipients/reducer' import { RootState } from 'src/redux/reducers' import { initializeSentryUserContext } from 'src/sentry/Sentry' import DisconnectBanner from 'src/shared/DisconnectBanner' import { resetStandbyTransactions } from 'src/transactions/actions' -import { NumberToRecipient } from 'src/utils/recipient' import { currentAccountSelector } from 'src/web3/selectors' const SCREEN_WIDTH = variables.width @@ -66,7 +69,7 @@ const mapStateToProps = (state: RootState): StateProps => ({ address: currentAccountSelector(state), activeNotificationCount: getActiveNotificationCount(state), callToActNotification: callToActNotificationSelector(state), - recipientCache: state.send.recipientCache, + recipientCache: recipientCacheSelector(state), }) const Header = () => { @@ -107,7 +110,7 @@ const SHADOW_STYLE = { x: 0, y: 1, } -export class WalletHome extends React.PureComponent { +export class WalletHome extends React.Component { animatedValue: Animated.Value headerOpacity: Animated.AnimatedInterpolation shadowOpacity: Animated.AnimatedInterpolation @@ -156,7 +159,7 @@ export class WalletHome extends React.PureComponent { showTestnetBanner = () => { const { t } = this.props - this.props.showMessage(t('testnetAlert.1'), null, t('dismiss'), t('testnetAlert.0')) + this.props.showMessage(t('testnetAlert.1'), ALERT_BANNER_DURATION, null, t('testnetAlert.0')) } importContactsIfNeeded = () => { @@ -265,18 +268,20 @@ const styles = StyleSheet.create({ }, }) -export default componentWithAnalytics( - connect( - mapStateToProps, - { - refreshAllBalances, - resetStandbyTransactions, - initializeSentryUserContext, - exitBackupFlow, - setLoading, - showMessage, - hideAlert, - importContacts, - } - )(withNamespaces(Namespaces.walletFlow5)(WalletHome)) +export default withDispatchAfterNavigate( + componentWithAnalytics( + connect( + mapStateToProps, + { + refreshAllBalances, + resetStandbyTransactions, + initializeSentryUserContext, + exitBackupFlow, + setLoading, + showMessage, + hideAlert, + importContacts, + } + )(withNamespaces(Namespaces.walletFlow5)(WalletHome)) + ) ) diff --git a/packages/mobile/src/home/__snapshots__/WalletHome.test.tsx.snap b/packages/mobile/src/home/__snapshots__/WalletHome.test.tsx.snap new file mode 100644 index 00000000000..900c2df9a57 --- /dev/null +++ b/packages/mobile/src/home/__snapshots__/WalletHome.test.tsx.snap @@ -0,0 +1,685 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testnet banner Shows testnet banner for 5 seconds 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + wallet + + + + + + + + + } + refreshing={false} + renderItem={[Function]} + renderSectionHeader={[Function]} + scrollEventThrottle={50} + sections={ + Array [ + Object { + "data": Array [ + Object {}, + ], + "renderItem": [Function], + "title": "activity", + }, + ] + } + stickyHeaderIndices={ + Array [ + 1, + ] + } + stickySectionHeadersEnabled={true} + style={ + Object { + "backgroundColor": "#FFFFFF", + "flex": 1, + "position": "relative", + } + } + updateCellsBatchingPeriod={50} + windowSize={21} + > + + + + + + + + + J + + + + + + + + + + + + + + + + + + John Doe + + + + +1 + + + (415) 555-6666 + + + + + + + + activity + + + + + + + + +`; diff --git a/packages/mobile/src/home/actions.ts b/packages/mobile/src/home/actions.ts index dae89919f17..c9940851da2 100644 --- a/packages/mobile/src/home/actions.ts +++ b/packages/mobile/src/home/actions.ts @@ -15,6 +15,8 @@ export enum Actions { ADD_NOTIFICATION = 'HOME/ADD_NOTIFICATION', SET_NOTIFICATION = 'HOME/SET_NOTIFICATION', REFRESH_BALANCES = 'HOME/REFRESH_BALANCES', + START_BALANCE_AUTOREFRESH = 'HOME/START_BALANCE_AUTOREFRESH', + STOP_BALANCE_AUTOREFRESH = 'HOME/STOP_BALANCE_AUTOREFRESH', } export const setLoading = (loading: boolean) => ({ @@ -42,3 +44,11 @@ export const setNotification = (notification: Notification, index: number) => ({ export const refreshAllBalances = () => ({ type: Actions.REFRESH_BALANCES, }) + +export const startBalanceAutorefresh = () => ({ + type: Actions.START_BALANCE_AUTOREFRESH, +}) + +export const stopBalanceAutorefresh = () => ({ + type: Actions.STOP_BALANCE_AUTOREFRESH, +}) diff --git a/packages/mobile/src/home/saga.test.ts b/packages/mobile/src/home/saga.test.ts index 1140f0fa301..84353c6bf76 100644 --- a/packages/mobile/src/home/saga.test.ts +++ b/packages/mobile/src/home/saga.test.ts @@ -2,7 +2,7 @@ import { expectSaga } from 'redux-saga-test-plan' import { call, put } from 'redux-saga/effects' import { fetchGoldBalance } from 'src/goldToken/actions' import { refreshAllBalances, setLoading } from 'src/home/actions' -import { refreshBalances, watchRefreshBalances, withLoading } from 'src/home/saga' +import { _watchRefreshBalances, refreshBalances, withLoading } from 'src/home/saga' import { fetchDollarBalance } from 'src/stableToken/actions' import { getConnectedAccount } from 'src/web3/saga' @@ -17,7 +17,7 @@ describe('refreshBalances', () => { describe('watchRefreshBalances', () => { test('reacts on REFRESH_BALANCES', async () => { - const p = expectSaga(watchRefreshBalances) + const p = expectSaga(_watchRefreshBalances) .put(setLoading(true)) .put(setLoading(false)) .provide([[call(getConnectedAccount), true]]) diff --git a/packages/mobile/src/home/saga.ts b/packages/mobile/src/home/saga.ts index 5525209a8b3..7d5165525bc 100644 --- a/packages/mobile/src/home/saga.ts +++ b/packages/mobile/src/home/saga.ts @@ -1,8 +1,19 @@ -import { call, put, spawn, takeLeading } from 'redux-saga/effects' -import { getSentPayments } from 'src/escrow/actions' +import { + call, + cancel, + delay, + fork, + put, + select, + spawn, + take, + takeLeading, +} from 'redux-saga/effects' +import { fetchSentPayments } from 'src/escrow/actions' import { fetchGoldBalance } from 'src/goldToken/actions' -import { Actions, setLoading } from 'src/home/actions' +import { Actions, refreshAllBalances, setLoading } from 'src/home/actions' import { withTimeout } from 'src/redux/sagas-helpers' +import { shouldUpdateBalance } from 'src/redux/selectors' import { fetchDollarBalance } from 'src/stableToken/actions' import Logger from 'src/utils/Logger' import { getConnectedAccount } from 'src/web3/saga' @@ -27,7 +38,7 @@ export function* refreshBalances() { yield call(getConnectedAccount) yield put(fetchDollarBalance()) yield put(fetchGoldBalance()) - yield put(getSentPayments()) + yield put(fetchSentPayments()) } export function* refreshBalancesWithLoadingSaga() { @@ -37,7 +48,25 @@ export function* refreshBalancesWithLoadingSaga() { ) } -export function* watchRefreshBalances() { +function* autoRefreshSaga() { + while (true) { + yield delay(10 * 1000) // sleep 10 seconds + if (yield select(shouldUpdateBalance)) { + put(refreshAllBalances()) + } + } +} + +function* autoRefreshWatcher() { + while (yield take(Actions.START_BALANCE_AUTOREFRESH)) { + // starts the task in the background + const autoRefresh = yield fork(autoRefreshSaga) + yield take(Actions.STOP_BALANCE_AUTOREFRESH) + yield cancel(autoRefresh) + } +} + +function* watchRefreshBalances() { yield takeLeading( Actions.REFRESH_BALANCES, withLoading(withTimeout(REFRESH_TIMEOUT, refreshBalances)) @@ -46,8 +75,11 @@ export function* watchRefreshBalances() { export function* homeSaga() { yield spawn(watchRefreshBalances) + yield spawn(autoRefreshWatcher) // This has been disabled due to the saga interference bug // depending on timing, it can block the sync progress updates and // keep us stuck on sync screen // yield spawn(refreshBalancesWithLoadingSaga) } + +export const _watchRefreshBalances = watchRefreshBalances diff --git a/packages/mobile/src/identity/commentKey.test.ts b/packages/mobile/src/identity/commentKey.test.ts index 8bf829180f0..95f6efd0a90 100644 --- a/packages/mobile/src/identity/commentKey.test.ts +++ b/packages/mobile/src/identity/commentKey.test.ts @@ -1,5 +1,4 @@ import { encryptComment } from 'src/identity/commentKey' -import { createMockContract } from 'test/utils' import { mockComment } from 'test/values' jest.mock('src/web3/actions', () => ({ @@ -7,10 +6,9 @@ jest.mock('src/web3/actions', () => ({ unlockAccount: jest.fn(async () => true), })) -jest.mock('@celo/contractkit', () => ({ - ...jest.requireActual('@celo/contractkit'), +jest.mock('@celo/walletkit', () => ({ + ...jest.requireActual('@celo/walletkit'), sendTransaction: jest.fn(async () => null), - getABEContract: jest.fn(async () => createMockContract({})), })) describe('Encrypt Comment', () => { diff --git a/packages/mobile/src/identity/commentKey.ts b/packages/mobile/src/identity/commentKey.ts index c2c96c950b6..ee1986a25b5 100644 --- a/packages/mobile/src/identity/commentKey.ts +++ b/packages/mobile/src/identity/commentKey.ts @@ -1,8 +1,8 @@ -import { getAttestationsContract, getDataEncryptionKey } from '@celo/contractkit' import { encryptComment as encryptCommentRaw, stripHexLeader, } from '@celo/utils/src/commentEncryption' +import { getAttestationsContract, getDataEncryptionKey } from '@celo/walletkit' import { web3 } from 'src/web3/contracts' export async function getCommentKey(address: string): Promise { diff --git a/packages/mobile/src/identity/contactMapping.test.ts b/packages/mobile/src/identity/contactMapping.test.ts index 95254d53cdb..ad0e7eaa284 100644 --- a/packages/mobile/src/identity/contactMapping.test.ts +++ b/packages/mobile/src/identity/contactMapping.test.ts @@ -1,4 +1,4 @@ -import { getAttestationsContract } from '@celo/contractkit' +import { getAttestationsContract } from '@celo/walletkit' import { expectSaga } from 'redux-saga-test-plan' import { throwError } from 'redux-saga-test-plan/providers' import { call, select } from 'redux-saga/effects' @@ -10,9 +10,9 @@ import { updateE164PhoneNumberAddresses } from 'src/identity/actions' import { doImportContacts } from 'src/identity/contactMapping' import { e164NumberToAddressSelector } from 'src/identity/reducer' import { waitForUserVerified } from 'src/identity/verification' -import { setRecipientCache } from 'src/send/actions' +import { setRecipientCache } from 'src/recipients/actions' +import { contactsToRecipients } from 'src/recipients/recipient' import { getAllContacts } from 'src/utils/contacts' -import { contactsToRecipients } from 'src/utils/recipient' import { web3 } from 'src/web3/contracts' import { getConnectedAccount } from 'src/web3/saga' import { createMockContract } from 'test/utils' diff --git a/packages/mobile/src/identity/contactMapping.ts b/packages/mobile/src/identity/contactMapping.ts index f048ef5f6e8..b3f0c3698a2 100644 --- a/packages/mobile/src/identity/contactMapping.ts +++ b/packages/mobile/src/identity/contactMapping.ts @@ -1,6 +1,6 @@ -import { getAttestationsContract, lookupPhoneNumbers } from '@celo/contractkit' -import { Attestations as AttestationsType } from '@celo/contractkit/types/Attestations' import { getPhoneHash } from '@celo/utils/src/phoneNumbers' +import { getAttestationsContract, lookupPhoneNumbers } from '@celo/walletkit' +import { Attestations as AttestationsType } from '@celo/walletkit/types/Attestations' import BigNumber from 'bignumber.js' import { chunk } from 'lodash' import { MinimalContact } from 'react-native-contacts' @@ -19,10 +19,11 @@ import { e164NumberToAddressSelector, E164NumberToAddressType, } from 'src/identity/reducer' -import { setRecipientCache } from 'src/send/actions' +import { setRecipientCache } from 'src/recipients/actions' +import { contactsToRecipients, NumberToRecipient } from 'src/recipients/recipient' +import { requestContactsPermission } from 'src/utils/androidPermissions' import { getAllContacts } from 'src/utils/contacts' import Logger from 'src/utils/Logger' -import { contactsToRecipients, NumberToRecipient } from 'src/utils/recipient' import { web3 } from 'src/web3/contracts' import { getConnectedAccount } from 'src/web3/saga' @@ -31,14 +32,18 @@ const MAPPING_CHUNK_SIZE = 25 const NUM_PARALLEL_REQUESTS = 3 export function* doImportContacts() { + Logger.debug(TAG, 'Importing user contacts') try { yield call(getConnectedAccount) - Logger.debug(TAG, 'Importing user contacts') + const result: boolean = yield call(requestContactsPermission) + if (!result) { + return Logger.warn(TAG, 'Contact permissions denied. Skipping import.') + } const contacts: MinimalContact[] = yield call(getAllContacts) if (!contacts || !contacts.length) { - return Logger.warn(TAG, 'Empty contacts list. Missing contacts permission?') + return Logger.warn(TAG, 'Empty contacts list. Skipping import.') } const defaultCountryCode: string = yield select(defaultCountryCodeSelector) @@ -226,7 +231,7 @@ export enum VerificationStatus { UNKNOWN = 2, } -export function getPhoneNumberAddress( +export function getAddressFromPhoneNumber( e164Number: string, e164NumberToAddress: E164NumberToAddressType ): string | null | undefined { @@ -237,11 +242,11 @@ export function getPhoneNumberAddress( return e164NumberToAddress[e164Number] } -export function getPhoneNumberVerificationStatus( +export function getVerificationStatusFromPhoneNumber( e164Number: string, e164NumberToAddress: E164NumberToAddressType ): VerificationStatus { - const address = getPhoneNumberAddress(e164Number, e164NumberToAddress) + const address = getAddressFromPhoneNumber(e164Number, e164NumberToAddress) // Undefined means the mapping has no entry for that number // or the entry has been cleared diff --git a/packages/mobile/src/identity/verification.test.ts b/packages/mobile/src/identity/verification.test.ts index 653b0abc2e4..2401738fdad 100644 --- a/packages/mobile/src/identity/verification.test.ts +++ b/packages/mobile/src/identity/verification.test.ts @@ -1,8 +1,4 @@ -import { - AttestationState, - getAttestationsContract, - getStableTokenContract, -} from '@celo/contractkit' +import { AttestationState, getAttestationsContract, getStableTokenContract } from '@celo/walletkit' import { expectSaga } from 'redux-saga-test-plan' import * as matchers from 'redux-saga-test-plan/matchers' import { throwError } from 'redux-saga-test-plan/providers' @@ -10,7 +6,7 @@ import { call, delay, select } from 'redux-saga/effects' import { e164NumberSelector } from 'src/account/reducer' import { showError } from 'src/alert/actions' import CeloAnalytics from 'src/analytics/CeloAnalytics' -import { CommonValues, CustomEventNames, DefaultEventNames } from 'src/analytics/constants' +import { CustomEventNames, DefaultEventNames } from 'src/analytics/constants' import { setNumberVerified } from 'src/app/actions' import { ErrorMessages } from 'src/app/ErrorMessages' import { cancelVerification, completeAttestationCode, endVerification } from 'src/identity/actions' @@ -19,15 +15,14 @@ import { AttestationCode, doVerificationFlow, ERROR_DURATION, - requestNeededAttestations, + requestAndRetrieveAttestations, startVerification, VERIFICATION_TIMEOUT, } from 'src/identity/verification' -import { sleep } from 'src/test/utils' import { web3 } from 'src/web3/contracts' import { getConnectedAccount, getConnectedUnlockedAccount } from 'src/web3/saga' import { privateCommentKeySelector } from 'src/web3/selectors' -import { createMockContract } from 'test/utils' +import { createMockContract, sleep } from 'test/utils' import { mockAccount, mockAccount2, @@ -112,31 +107,14 @@ describe('Start Verification Saga', () => { beforeEach(() => { MockedAnalytics.startTracking.mockReset() MockedAnalytics.stopTracking.mockReset() + MockedAnalytics.track.mockReset() }) - it('tracks success', async () => { - await expectSaga(startVerification) - .provide([[call(getConnectedAccount), null], [call(doVerificationFlow), true]]) - .run() - expect(MockedAnalytics.startTracking.mock.calls.length).toBe(1) - expect(MockedAnalytics.startTracking.mock.calls[0][0]).toBe(CustomEventNames.verification) - expect(MockedAnalytics.stopTracking.mock.calls.length).toBe(1) - expect(MockedAnalytics.stopTracking.mock.calls[0]).toEqual([ - CustomEventNames.verification, - { result: CommonValues.success }, - ]) - }) - it('tracks failure', async () => { await expectSaga(startVerification) .provide([[call(getConnectedAccount), null], [call(doVerificationFlow), false]]) .run() - expect(MockedAnalytics.startTracking.mock.calls.length).toBe(1) - expect(MockedAnalytics.startTracking.mock.calls[0][0]).toBe(CustomEventNames.verification) - expect(MockedAnalytics.stopTracking.mock.calls.length).toBe(1) - expect(MockedAnalytics.stopTracking.mock.calls[0]).toEqual([ - CustomEventNames.verification, - { result: CommonValues.failure }, - ]) + expect(MockedAnalytics.track.mock.calls.length).toBe(1) + expect(MockedAnalytics.track.mock.calls[0][0]).toBe(CustomEventNames.verification_failed) }) it('times out when verification takes too long', async () => { @@ -147,15 +125,9 @@ describe('Start Verification Saga', () => { [delay(VERIFICATION_TIMEOUT), 1000], ]) .run(2000) - expect(MockedAnalytics.startTracking.mock.calls.length).toBe(1) - expect(MockedAnalytics.startTracking.mock.calls[0][0]).toBe(CustomEventNames.verification) - expect(MockedAnalytics.stopTracking.mock.calls.length).toBe(1) - expect(MockedAnalytics.stopTracking.mock.calls[0]).toEqual([ - CustomEventNames.verification, - { result: CommonValues.timeout }, - ]) - expect(MockedAnalytics.track.mock.calls.length).toBe(1) - expect(MockedAnalytics.track.mock.calls[0][0]).toBe(DefaultEventNames.errorDisplayed) + expect(MockedAnalytics.track.mock.calls.length).toBe(2) + expect(MockedAnalytics.track.mock.calls[0][0]).toBe(CustomEventNames.verification_timed_out) + expect(MockedAnalytics.track.mock.calls[1][0]).toBe(DefaultEventNames.errorDisplayed) }) it('stops when the user cancels', async () => { @@ -163,13 +135,8 @@ describe('Start Verification Saga', () => { .provide([[call(getConnectedAccount), null], [call(doVerificationFlow), sleep(1500)]]) .dispatch(cancelVerification()) .run(2000) - expect(MockedAnalytics.startTracking.mock.calls.length).toBe(1) - expect(MockedAnalytics.startTracking.mock.calls[0][0]).toBe(CustomEventNames.verification) - expect(MockedAnalytics.stopTracking.mock.calls.length).toBe(1) - expect(MockedAnalytics.stopTracking.mock.calls[0]).toEqual([ - CustomEventNames.verification, - { result: CommonValues.cancel }, - ]) + expect(MockedAnalytics.track.mock.calls.length).toBe(1) + expect(MockedAnalytics.track.mock.calls[0][0]).toBe(CustomEventNames.verification_cancelled) }) }) @@ -239,7 +206,7 @@ describe('Do Verification Saga', () => { [call(getAttestationsContract, web3), attestationContract], [call(getStableTokenContract, web3), createMockContract({})], [select(e164NumberSelector), mockE164Number], - [matchers.call.fn(requestNeededAttestations), throwError(new Error('fake error'))], + [matchers.call.fn(requestAndRetrieveAttestations), throwError(new Error('fake error'))], ]) .put(showError(ErrorMessages.VERIFICATION_FAILURE, ERROR_DURATION)) .put(endVerification(false)) diff --git a/packages/mobile/src/identity/verification.ts b/packages/mobile/src/identity/verification.ts index d052ec298fa..f038670ea8c 100644 --- a/packages/mobile/src/identity/verification.ts +++ b/packages/mobile/src/identity/verification.ts @@ -1,3 +1,6 @@ +import { compressedPubKey } from '@celo/utils/src/commentEncryption' +import { getPhoneHash, isE164Number } from '@celo/utils/src/phoneNumbers' +import { areAddressesEqual } from '@celo/utils/src/signatureUtils' import { ActionableAttestation, extractAttestationCodeFromMessage, @@ -14,19 +17,16 @@ import { makeRevealTx, makeSetAccountTx, validateAttestationCode, -} from '@celo/contractkit' -import { Attestations as AttestationsType } from '@celo/contractkit/types/Attestations' -import { StableToken as StableTokenType } from '@celo/contractkit/types/StableToken' -import { compressedPubKey } from '@celo/utils/src/commentEncryption' -import { getPhoneHash, isE164Number } from '@celo/utils/src/phoneNumbers' -import { compareAddresses } from '@celo/utils/src/signatureUtils' +} from '@celo/walletkit' +import { Attestations as AttestationsType } from '@celo/walletkit/types/Attestations' +import { StableToken as StableTokenType } from '@celo/walletkit/types/StableToken' import BigNumber from 'bignumber.js' import { Task } from 'redux-saga' import { all, call, delay, fork, put, race, select, take, takeEvery } from 'redux-saga/effects' import { e164NumberSelector } from 'src/account/reducer' import { showError } from 'src/alert/actions' import CeloAnalytics from 'src/analytics/CeloAnalytics' -import { CommonValues, CustomEventNames } from 'src/analytics/constants' +import { CustomEventNames } from 'src/analytics/constants' import { setNumberVerified } from 'src/app/actions' import { ErrorMessages } from 'src/app/ErrorMessages' import { refreshAllBalances } from 'src/home/actions' @@ -83,7 +83,6 @@ export function* startVerification() { yield call(getConnectedAccount) Logger.debug(TAG, 'Starting verification') - CeloAnalytics.startTracking(CustomEventNames.verification) const { result, cancel, timeout } = yield race({ result: call(doVerificationFlow), @@ -92,26 +91,19 @@ export function* startVerification() { }) if (result === true) { + CeloAnalytics.track(CustomEventNames.verification_success) Logger.debug(TAG, 'Verification completed successfully') - CeloAnalytics.stopTracking(CustomEventNames.verification, { - result: CommonValues.success, - }) } else if (result === false) { + CeloAnalytics.track(CustomEventNames.verification_failed) Logger.debug(TAG, 'Verification failed') - CeloAnalytics.stopTracking(CustomEventNames.verification, { - result: CommonValues.failure, - }) } else if (cancel) { + CeloAnalytics.track(CustomEventNames.verification_cancelled) Logger.debug(TAG, 'Verification cancelled') - CeloAnalytics.stopTracking(CustomEventNames.verification, { - result: CommonValues.cancel, - }) } else if (timeout) { + CeloAnalytics.track(CustomEventNames.verification_timed_out) Logger.debug(TAG, 'Verification timed out') - CeloAnalytics.stopTracking(CustomEventNames.verification, { - result: CommonValues.timeout, - }) yield put(showError(ErrorMessages.VERIFICATION_TIMEOUT, ERROR_DURATION)) + yield put(endVerification(false)) // TODO #1955: Add logic in this case to request more SMS messages } Logger.debug(TAG, 'Done verification') @@ -129,7 +121,7 @@ export function* doVerificationFlow() { const attestationsContract: AttestationsType = yield call(getAttestationsContract, web3) const stableTokenContract: StableTokenType = yield call(getStableTokenContract, web3) - CeloAnalytics.trackSubEvent(CustomEventNames.verification, CustomEventNames.verification_setup) + CeloAnalytics.track(CustomEventNames.verification_setup) // Get all relevant info about the account's verification status const status: AttestationsStatus = yield call( @@ -139,10 +131,7 @@ export function* doVerificationFlow() { e164NumberHash ) - CeloAnalytics.trackSubEvent( - CustomEventNames.verification, - CustomEventNames.verification_get_status - ) + CeloAnalytics.track(CustomEventNames.verification_get_status) if (status.isVerified) { yield put(endVerification()) @@ -153,35 +142,17 @@ export function* doVerificationFlow() { // Mark codes completed in previous attempts yield put(completeAttestationCode(NUM_ATTESTATIONS_REQUIRED - status.numAttestationsRemaining)) - // Request any additional attestations needed to be verified - yield call( - requestNeededAttestations, + const attestations: ActionableAttestation[] = yield call( + requestAndRetrieveAttestations, attestationsContract, stableTokenContract, - status.numAttestationsRequestsNeeded, e164NumberHash, - account - ) - - CeloAnalytics.trackSubEvent( - CustomEventNames.verification, - CustomEventNames.verification_req_attestations + account, + status.numAttestationsRemaining ) - // Get actionable attestation details - const attestations: ActionableAttestation[] = yield call( - getActionableAttestations, - attestationsContract, - e164NumberHash, - account - ) const issuers = attestations.map((a) => a.issuer) - CeloAnalytics.trackSubEvent( - CustomEventNames.verification, - CustomEventNames.verification_get_attestations - ) - // Start listening for manual and/or auto message inputs const receiveMessageTask: Task = yield takeEvery( Actions.RECEIVE_ATTESTATION_MESSAGE, @@ -235,7 +206,38 @@ function* getE164NumberHash() { interface AttestationsStatus { isVerified: boolean // user has sufficiently many attestations? numAttestationsRemaining: number // number of attestations still needed - numAttestationsRequestsNeeded: number // number of new request txs needed +} + +// Requests if necessary additional attestations and returns all revealable attetations +export async function requestAndRetrieveAttestations( + attestationsContract: AttestationsType, + stableTokenContract: StableTokenType, + e164NumberHash: string, + account: string, + attestationsRemaining: number +) { + // The set of attestations we can reveal right now + let attestations: ActionableAttestation[] = await getActionableAttestations( + attestationsContract, + e164NumberHash, + account + ) + + while (attestations.length < attestationsRemaining) { + // Request any additional attestations beyond the original set + await requestAttestations( + attestationsContract, + stableTokenContract, + attestationsRemaining - attestations.length, + e164NumberHash, + account + ) + + // Check if we have a sufficient set now by fetching the new total set + attestations = await getActionableAttestations(attestationsContract, e164NumberHash, account) + } + + return attestations } async function getAttestationsStatus( @@ -254,8 +256,7 @@ async function getAttestationsStatus( const numAttestationRequests = new BigNumber(attestationStats[1]).toNumber() // Number of attestations remaining to be verified const numAttestationsRemaining = NUM_ATTESTATIONS_REQUIRED - numAttestationsCompleted - // Number of attestation requests that were not completed (some may be expired) - const numIncompleteAttestationRequests = numAttestationRequests - numAttestationsCompleted + Logger.debug( TAG + '@getAttestationsStatus', `${numAttestationsRemaining} verifications remaining. Total of ${numAttestationRequests} requested.` @@ -266,31 +267,16 @@ async function getAttestationsStatus( return { isVerified: true, numAttestationsRemaining, - numAttestationsRequestsNeeded: 0, } } - // Number of incomplete attestations that are still valid (not expired) - const numValidIncompleteAttestations = - numIncompleteAttestationRequests > 0 - ? (await getActionableAttestations(attestationsContract, e164NumberHash, account)).length - : 0 - - // Number of new attestion requests that will be made to satisfy verificaiton requirements - const numAttestationsRequestsNeeded = numAttestationsRemaining - numValidIncompleteAttestations - Logger.debug( - TAG + '@getAttestationsStatus', - `${numAttestationsRequestsNeeded} new attestation requests needed to fulfill ${numAttestationsRemaining} required attestations` - ) - return { isVerified: false, numAttestationsRemaining, - numAttestationsRequestsNeeded, } } -export async function requestNeededAttestations( +async function requestAttestations( attestationsContract: AttestationsType, stableTokenContract: StableTokenType, numAttestationsRequestsNeeded: number, @@ -301,7 +287,7 @@ export async function requestNeededAttestations( Logger.debug(`${TAG}@requestNeededAttestations`, 'No additional attestations requests needed') return } - + CeloAnalytics.track(CustomEventNames.verification_request_attestations) Logger.debug( `${TAG}@requestNeededAttestations`, `Approving ${numAttestationsRequestsNeeded} new attestations` @@ -313,6 +299,13 @@ export async function requestNeededAttestations( numAttestationsRequestsNeeded ) + const { + confirmation: approveConfirmationPromise, + transactionHash: approveTransactionHashPromise, + } = await sendTransactionPromises(approveTx, account, TAG, 'Approve Attestations') + + await approveTransactionHashPromise + Logger.debug( `${TAG}@requestNeededAttestations`, `Requesting ${numAttestationsRequestsNeeded} new attestations` @@ -325,16 +318,12 @@ export async function requestNeededAttestations( stableTokenContract ) - const { - confirmation: approveConfirmationPromise, - transactionHash: approveTransactionHashPromise, - } = await sendTransactionPromises(approveTx, account, TAG, 'Approve Attestations') - - await approveTransactionHashPromise await Promise.all([ - sendTransaction(requestTx, account, TAG, 'Request Attestations', REQUEST_TX_GAS), approveConfirmationPromise, + sendTransaction(requestTx, account, TAG, 'Request Attestations', REQUEST_TX_GAS), ]) + + CeloAnalytics.track(CustomEventNames.verification_requested_attestations) } function attestationCodeReceiver( @@ -426,23 +415,18 @@ function* revealAndCompleteAttestation( issuer: string ) { Logger.debug(TAG + '@revealAttestation', `Revealing an attestation for issuer: ${issuer}`) + CeloAnalytics.track(CustomEventNames.verification_reveal_attestation, { issuer }) const revealTx = yield call(makeRevealTx, attestationsContract, e164Number, issuer) yield call(sendTransaction, revealTx, account, TAG, `Reveal ${issuer}`) - CeloAnalytics.trackSubEvent( - CustomEventNames.verification, - CustomEventNames.verification_reveal_txs - ) + CeloAnalytics.track(CustomEventNames.verification_revealed_attestation, { issuer }) const code: AttestationCode = yield call(waitForAttestationCode, issuer) - CeloAnalytics.trackSubEvent( - CustomEventNames.verification, - CustomEventNames.verification_codes_received - ) - Logger.debug(TAG + '@revealAttestation', `Completing code for issuer: ${code.issuer}`) + CeloAnalytics.track(CustomEventNames.verification_complete_attestation, { issuer }) + const completeTx = makeCompleteTx( attestationsContract, e164NumberHash, @@ -452,10 +436,7 @@ function* revealAndCompleteAttestation( ) yield call(sendTransaction, completeTx, account, TAG, `Confirmation ${issuer}`) - CeloAnalytics.trackSubEvent( - CustomEventNames.verification, - CustomEventNames.verification_complete_txs - ) + CeloAnalytics.track(CustomEventNames.verification_completed_attestation, { issuer }) yield put(completeAttestationCode()) Logger.debug(TAG + '@revealAttestation', `Attestation for issuer ${issuer} completed`) @@ -486,15 +467,12 @@ async function setAccount( const currentWalletAddress = await getWalletAddress(attestationsContract, address) const currentWalletDEK = await getDataEncryptionKey(attestationsContract, address) if ( - !compareAddresses(currentWalletAddress, address) || - !compareAddresses(currentWalletDEK, dataKey) + !areAddressesEqual(currentWalletAddress, address) || + !areAddressesEqual(currentWalletDEK, dataKey) ) { const setAccountTx = makeSetAccountTx(attestationsContract, address, dataKey) await sendTransaction(setAccountTx, address, TAG, `Set Wallet Address & DEK`) - CeloAnalytics.trackSubEvent( - CustomEventNames.verification, - CustomEventNames.verification_set_account - ) + CeloAnalytics.track(CustomEventNames.verification_set_account) } } @@ -536,7 +514,7 @@ export function* revokeVerification() { // TODO(Rossy) This is currently only used in one place, would be better // to have consumer use the e164NumberToAddress map instead -export async function lookupPhoneNumberAddress(e164Number: string) { +export async function lookupAddressFromPhoneNumber(e164Number: string) { Logger.debug(TAG + '@lookupPhoneNumberAddress', `Checking Phone Number Address`) try { @@ -564,5 +542,5 @@ export async function isPhoneNumberVerified(phoneNumber: string | null | undefin return false } - return (await lookupPhoneNumberAddress(phoneNumber)) != null + return (await lookupAddressFromPhoneNumber(phoneNumber)) != null } diff --git a/packages/mobile/src/import/ImportContacts.tsx b/packages/mobile/src/import/ImportContacts.tsx index a71ec822d64..4b002565159 100644 --- a/packages/mobile/src/import/ImportContacts.tsx +++ b/packages/mobile/src/import/ImportContacts.tsx @@ -1,50 +1,78 @@ import Button, { BtnTypes } from '@celo/react-components/components/Button' import colors from '@celo/react-components/styles/colors' import { fontStyles } from '@celo/react-components/styles/fonts' +import { areAddressesEqual } from '@celo/utils/src/signatureUtils' import * as React from 'react' import { WithNamespaces, withNamespaces } from 'react-i18next' import { ActivityIndicator, ScrollView, StyleSheet, Text, View } from 'react-native' import { connect } from 'react-redux' +import { e164NumberSelector } from 'src/account/reducer' +import { errorSelector } from 'src/alert/reducer' import { componentWithAnalytics } from 'src/analytics/wrapper' +import { setNumberVerified } from 'src/app/actions' +import { ErrorMessages } from 'src/app/ErrorMessages' import DevSkipButton from 'src/components/DevSkipButton' +import GethAwareButton from 'src/geth/GethAwareButton' import { Namespaces } from 'src/i18n' import VerifyAddressBook from 'src/icons/VerifyAddressBook' import { denyImportContacts, importContacts } from 'src/identity/actions' +import { lookupAddressFromPhoneNumber } from 'src/identity/verification' +import { nuxNavigationOptionsNoBackButton } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' -import { Screens } from 'src/navigator/Screens' +import { Screens, Stacks } from 'src/navigator/Screens' import { RootState } from 'src/redux/reducers' -import DisconnectBanner from 'src/shared/DisconnectBanner' import { requestContactsPermission } from 'src/utils/androidPermissions' +import { currentAccountSelector } from 'src/web3/selectors' interface DispatchProps { importContacts: typeof importContacts denyImportContacts: typeof denyImportContacts + setNumberVerified: typeof setNumberVerified } interface StateProps { + error: ErrorMessages | null isLoadingImportContacts: boolean + e164Number: string + account: string | null +} + +interface State { + isSubmitting: boolean } type Props = WithNamespaces & DispatchProps & StateProps const mapStateToProps = (state: RootState): StateProps => { return { + error: errorSelector(state), isLoadingImportContacts: state.identity.isLoadingImportContacts, + e164Number: e164NumberSelector(state), + account: currentAccountSelector(state), } } -class ImportContacts extends React.Component { - static navigationOptions = { - headerStyle: { - elevation: 0, - }, - headerLeft: null, - headerRightContainerStyle: { paddingRight: 15 }, - headerRight: ( - - - - ), +const displayedErrors = [ErrorMessages.IMPORT_CONTACTS_FAILED] + +const hasDisplayedError = (error: ErrorMessages | null) => { + return error && displayedErrors.includes(error) +} + +class ImportContacts extends React.Component { + static navigationOptions = nuxNavigationOptionsNoBackButton + + static getDerivedStateFromProps(props: Props, state: State): State | null { + if (hasDisplayedError(props.error) && state.isSubmitting) { + return { + ...state, + isSubmitting: false, + } + } + return null + } + + state = { + isSubmitting: false, } componentDidUpdate(prevProps: Props) { @@ -53,11 +81,21 @@ class ImportContacts extends React.Component { } } - nextScreen = () => { - navigate(Screens.VerifyEducation) + nextScreen = async () => { + const { account, e164Number } = this.props + const currentlyVerifiedAddress = await lookupAddressFromPhoneNumber(e164Number) + if (account && areAddressesEqual(account, currentlyVerifiedAddress)) { + // Wallet was imported and user is already verified to their current phone number + this.props.setNumberVerified(true) + navigate(Stacks.AppStack) + } else { + // Not yet verified, navigate to verification flow + navigate(Screens.VerifyEducation) + } } onPressEnable = async () => { + this.setState({ isSubmitting: true }) const result = await requestContactsPermission() if (result) { this.props.importContacts() @@ -72,10 +110,11 @@ class ImportContacts extends React.Component { } render() { - const { t, isLoadingImportContacts } = this.props + const { t } = this.props + const { isSubmitting } = this.state return ( - + @@ -91,17 +130,18 @@ class ImportContacts extends React.Component { {t('importContactsPermission.1')} - {isLoadingImportContacts && ( - - - {t('importContactsPermission.loading')} - - - - )} - -