diff --git a/.circleci/config.yml b/.circleci/config.yml index 2d9e0987e1d..2f5fe2d130e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,6 +35,8 @@ e2e-defaults: &e2e-defaults <<: *defaults docker: - image: celohq/circleci + environment: + CELO_BLOCKCHAIN_BRANCH_TO_TEST: master general: artifacts: @@ -44,6 +46,8 @@ general: jobs: install_dependencies: <<: *defaults + # Source: https://circleci.com/docs/2.0/configuration-reference/#resource_class + resource_class: medium+ steps: - restore_cache: keys: @@ -251,7 +255,7 @@ jobs: name: Run test itself command: | - cd ~/src/packages/mobile + cd ~/src/packages/mobile # detox sometimes without releasing the terminal and thus making the CI timout # 480s = 8 minutes timeout 480 yarn test:detox || echo "failed, try again" @@ -398,7 +402,7 @@ jobs: - run: name: Generate DevChain command: | - (cd packages/contractkit && yarn test:prepare) + (cd packages/contractkit && yarn test:reset) - run: name: Run Tests command: yarn --cwd=packages/contractkit test @@ -525,7 +529,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_transfers.sh checkout master + ./ci_test_transfers.sh checkout ${CELO_BLOCKCHAIN_BRANCH_TO_TEST} end-to-end-geth-blockchain-parameters-test: <<: *e2e-defaults @@ -543,7 +547,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_blockchain_parameters.sh checkout master + ./ci_test_blockchain_parameters.sh checkout ${CELO_BLOCKCHAIN_BRANCH_TO_TEST} end-to-end-geth-governance-test: <<: *e2e-defaults @@ -563,7 +567,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_governance.sh checkout master + ./ci_test_governance.sh checkout ${CELO_BLOCKCHAIN_BRANCH_TO_TEST} end-to-end-geth-sync-test: <<: *e2e-defaults @@ -582,7 +586,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_sync.sh checkout master + ./ci_test_sync.sh checkout ${CELO_BLOCKCHAIN_BRANCH_TO_TEST} end-to-end-geth-validator-order-test: <<: *e2e-defaults @@ -600,7 +604,7 @@ jobs: command: | set -e cd packages/celotool - ./ci_test_validator_order.sh checkout master + ./ci_test_validator_order.sh checkout ${CELO_BLOCKCHAIN_BRANCH_TO_TEST} web: working_directory: ~/app diff --git a/.env b/.env index ec46681708f..24243c5006f 100644 --- a/.env +++ b/.env @@ -12,31 +12,35 @@ CLUSTER_DOMAIN_NAME="celo-networks-dev" TESTNET_PROJECT_NAME="celo-testnet" BLOCKSCOUT_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/blockscout" -BLOCKSCOUT_DOCKER_IMAGE_TAG="5fba4843b3e78b5ab75d01766214cb24c6a40649" +BLOCKSCOUT_DOCKER_IMAGE_TAG="909682b7435fc3e05849211d96fb1dfbf76306f2" BLOCKSCOUT_WEB_REPLICAS=3 BLOCKSCOUT_DB_SUFFIX= ETHSTATS_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/ethstats" -ETHSTATS_DOCKER_IMAGE_TAG="0ffe524c625ea59e4492dc92c2e638689c36e4b0" +ETHSTATS_DOCKER_IMAGE_TAG="cd037ea1e18848466452ba9890c1f1bcd3f61009" +ETHSTATS_TRUSTED_ADDRESSES="" +ETHSTATS_BANNED_ADDRESSES="" + +FAUCET_GENESIS_ACCOUNTS=2 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="7ccc0a81920e6e09663855fc0ed46a98d634e74a" +GETH_NODE_DOCKER_IMAGE_TAG="master" 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="7ccc0a81920e6e09663855fc0ed46a98d634e74a" +GETH_BOOTNODE_DOCKER_IMAGE_TAG="master" CELOTOOL_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -CELOTOOL_DOCKER_IMAGE_TAG="celotool-dfdc3e8b26e98aa294b27e2b5621c184488a10db" +CELOTOOL_DOCKER_IMAGE_TAG="celotool-c8e3392aa2ca44ff83b4035700ece5fd12ed2b84" TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-d3d165a7db548d175cd703c86c20c1657c04368d" -ATTESTATION_SERVICE_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -ATTESTATION_SERVICE_DOCKER_IMAGE_TAG="attestation-service-74f329b014c40c7af19cf89b4c0d080c344d4a1c" +ATTESTATION_SERVICE_DOCKER_IMAGE_REPOSITORY="us.gcr.io/celo-testnet/celo-monorepo" +ATTESTATION_SERVICE_DOCKER_IMAGE_TAG="attestation-service-c8e3392aa2ca44ff83b4035700ece5fd12ed2b84" GETH_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet-production/geth-exporter" GETH_EXPORTER_DOCKER_IMAGE_TAG="ed7d21bd50592709173368cd697ef73c1774a261" @@ -46,6 +50,7 @@ NETWORK_ID=1101 CONSENSUS_TYPE="istanbul" BLOCK_TIME=1 EPOCH=1000 +LOOKBACK=12 ISTANBUL_REQUEST_TIMEOUT_MS=3000 # "og" -> our original 4 validators, "${n}" -> for deriving n validators from the MNEMONIC diff --git a/.env.alfajores b/.env.alfajores index 7b20ce9a53d..8089f0318e7 100644 --- a/.env.alfajores +++ b/.env.alfajores @@ -17,6 +17,8 @@ BLOCKSCOUT_WEB_REPLICAS=3 BLOCKSCOUT_DB_SUFFIX="7" BLOCKSCOUT_SUBNETWORK_NAME="Alfajores" +FAUCET_GENESIS_ACCOUNTS=2 + GETH_NODE_DOCKER_IMAGE_REPOSITORY="us.gcr.io/celo-testnet/geth" # When upgrading change this to latest commit hash from the master of the geth repo # `geth $ git show | head -n 1` diff --git a/.env.integration b/.env.integration index d34a1c869f4..86243ff42ca 100644 --- a/.env.integration +++ b/.env.integration @@ -11,29 +11,36 @@ CLUSTER_DOMAIN_NAME="celo-testnet" TESTNET_PROJECT_NAME="celo-testnet" BLOCKSCOUT_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/blockscout" -BLOCKSCOUT_DOCKER_IMAGE_TAG="ad86714d629c01272e0651dec1fb6a968c3cec71" +BLOCKSCOUT_DOCKER_IMAGE_TAG="909682b7435fc3e05849211d96fb1dfbf76306f2" BLOCKSCOUT_WEB_REPLICAS=3 -BLOCKSCOUT_DB_SUFFIX="25" +BLOCKSCOUT_DB_SUFFIX="29" BLOCKSCOUT_SUBNETWORK_NAME="Integration" +ETHSTATS_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/ethstats" +ETHSTATS_DOCKER_IMAGE_TAG="cd037ea1e18848466452ba9890c1f1bcd3f61009" +ETHSTATS_TRUSTED_ADDRESSES="" +ETHSTATS_BANNED_ADDRESSES="" + +FAUCET_GENESIS_ACCOUNTS=2 + 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="c1ae452c707f8bee91a9a0bf49193e78e9c8512e" +GETH_NODE_DOCKER_IMAGE_TAG="09a217ff58a95214cbc5189c933359707f4fdaf2" 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="c1ae452c707f8bee91a9a0bf49193e78e9c8512e" +GETH_BOOTNODE_DOCKER_IMAGE_TAG="09a217ff58a95214cbc5189c933359707f4fdaf2" CELOTOOL_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -CELOTOOL_DOCKER_IMAGE_TAG="celotool-2cb725c36b69e7ae608875610af080f4f3fa79bd" +CELOTOOL_DOCKER_IMAGE_TAG="celotool-5bea6d30cbe6aa4272b32a4d2cfed5567f422ea9" TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-2cb725c36b69e7ae608875610af080f4f3fa79bd" +TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-5bea6d30cbe6aa4272b32a4d2cfed5567f422ea9" -ATTESTATION_SERVICE_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -ATTESTATION_SERVICE_DOCKER_IMAGE_TAG="attestation-service-2cb725c36b69e7ae608875610af080f4f3fa79bd" +ATTESTATION_SERVICE_DOCKER_IMAGE_REPOSITORY="us.gcr.io/celo-testnet/celo-monorepo" +ATTESTATION_SERVICE_DOCKER_IMAGE_TAG="attestation-service-c8e3392aa2ca44ff83b4035700ece5fd12ed2b84" GETH_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet-production/geth-exporter" GETH_EXPORTER_DOCKER_IMAGE_TAG="ed7d21bd50592709173368cd697ef73c1774a261" @@ -45,7 +52,6 @@ BLOCK_TIME=3 EPOCH=1000 ISTANBUL_REQUEST_TIMEOUT_MS=3000 -# "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=20 TX_NODES=2 @@ -59,7 +65,6 @@ 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 diff --git a/.gitignore b/.gitignore index 73f080f6ee8..bc0cd564965 100644 --- a/.gitignore +++ b/.gitignore @@ -80,4 +80,7 @@ lerna-debug.log !.env.mnemonic*.enc .terraform/ -tsconfig.tsbuildinfo \ No newline at end of file +tsconfig.tsbuildinfo + +# git mergetool +*.orig diff --git a/SETUP.md b/SETUP.md index 13dbf50865d..2ddd793a2f9 100644 --- a/SETUP.md +++ b/SETUP.md @@ -1,6 +1,7 @@ # Celo Engineering Setup - [Celo Engineering Setup](#celo-engineering-setup) + - [Reading](#reading) - [Getting Everything Installed](#getting-everything-installed) - [MacOS](#macos) - [Xcode](#xcode) @@ -26,6 +27,10 @@ This is a living document! Please edit and update it as part of your onboarding process :-) +## Reading + +Review the README from each directory in [packages](packages/). + ## Getting Everything Installed Follow these steps to get everything that you need installed to develop and diff --git a/dockerfiles/attestation-service/Dockerfile b/dockerfiles/attestation-service/Dockerfile index 935a1e6c7c7..e1461e07612 100644 --- a/dockerfiles/attestation-service/Dockerfile +++ b/dockerfiles/attestation-service/Dockerfile @@ -8,6 +8,7 @@ COPY scripts/ scripts/ # Copy only pkg.json COPY packages/typescript/package.json packages/typescript/ COPY packages/utils/package.json packages/utils/ +COPY packages/dev-utils/package.json packages/dev-utils/ COPY packages/protocol/package.json packages/protocol/ COPY packages/contractkit/package.json packages/contractkit/ COPY packages/attestation-service/package.json packages/attestation-service/ @@ -17,6 +18,7 @@ RUN yarn install --frozen-lockfile --network-timeout 100000 && yarn cache clean # Copy the rest COPY packages/typescript packages/typescript/ COPY packages/utils packages/utils/ +COPY packages/dev-utils packages/dev-utils/ COPY packages/protocol packages/protocol/ COPY packages/contractkit packages/contractkit/ COPY packages/attestation-service packages/attestation-service/ diff --git a/dockerfiles/celotool/Dockerfile b/dockerfiles/celotool/Dockerfile index 851a8d057ef..da6a7409bcd 100644 --- a/dockerfiles/celotool/Dockerfile +++ b/dockerfiles/celotool/Dockerfile @@ -16,6 +16,7 @@ RUN apt-get update && \ COPY lerna.json package.json yarn.lock ./ COPY scripts/ scripts/ COPY packages/utils/package.json packages/utils/ +COPY packages/dev-utils/package.json packages/dev-utils/ COPY packages/typescript/package.json packages/typescript/ COPY packages/walletkit/package.json packages/walletkit/ COPY packages/contractkit/package.json packages/contractkit/ @@ -26,6 +27,7 @@ COPY packages/celotool/package.json packages/celotool/ RUN yarn install --network-timeout 100000 --frozen-lockfile && yarn cache clean COPY packages/utils packages/utils/ +COPY packages/dev-utils packages/dev-utils/ COPY packages/typescript packages/typescript/ COPY packages/walletkit packages/walletkit/ COPY packages/contractkit packages/contractkit/ diff --git a/packages/attestation-service/README.md b/packages/attestation-service/README.md new file mode 100644 index 00000000000..b1d0178a690 --- /dev/null +++ b/packages/attestation-service/README.md @@ -0,0 +1,45 @@ +# Attestation Service + +A service run by validators on the Celo network to send SMS messages, enabling attestations of user phone numbers and their accounts on the Celo network. + +### Configuration + +You can use the following environment variables to configure the attestation service: + +- `DATABASE_URL` - The URL under which your database is accessible, currently supported are `postgres://`, `mysql://` and `sqlite://` +- `CELO_PROVIDER` - The URL under which a celo blockchain node is reachable, i.e. something like `https://integration-forno.celo-testnet.org` +- `ACCOUNT_ADDRESS` - The address of the account on the `Accounts` smart contract +- `ATTESTATION_KEY` - The private key with which attestations should be signed. You could use your account key for attestations, but really you should authorize a dedicated attestation key +- `APP_SIGNATURE` - The hash with which clients can auto-read SMS messages on android +- `SMS_PROVIDERS` - A comma-separated list of providers you want to configure, we currently support: + +`nexmo` + +- `NEXMO_KEY` - The API key to the Nexmo API +- `NEXMO_SECRET` - The API secret to the Nexmo API +- `NEXMO_BLACKLIST` - A comma-sperated list of country codes you do not want to serve + +`twilio` + +- `TWILIO_ACCOUNT_SID` - The SID of your Twilio account +- `TWILIO_MESSAGE_SERVICE_SID` - The SID of the messaging service you want to use. The messaging service should have at least 1 phone number associated with it. +- `TWILIO_AUTH_TOKEN` - The auth token for your Twilio account + +### Operations + +This service uses `bunyan` for structured logging with JSON lines. You can pipe STDOUT to `yarn run bunyan` for a more human friendly output. The `LOG_LEVEL` environment variable can specify desired log levels. With `LOG_FORMAT=stackdriver` you can get stackdriver specific format to recover information such as error traces etc. + +This service exposes prometheus metrics under `/metrics`. + +### Running locally + +After checking out the source, you should create a local sqlite database by running: + +```sh +yarn run db:create:dev +yarn run db:migrate:dev +``` + +You will also have to set the environment variables in `.env.development` + +Then start the service with `yarn run dev` (you'll have to add the appropriate credentials for the text providers) diff --git a/packages/attestation-service/config/.env.development b/packages/attestation-service/config/.env.development index a8b1540c59f..76186201b78 100644 --- a/packages/attestation-service/config/.env.development +++ b/packages/attestation-service/config/.env.development @@ -3,5 +3,15 @@ CELO_PROVIDER=https://integration-forno.celo-testnet.org ACCOUNT_ADDRESS=0xE6e53b5fc2e18F51781f14a3ce5E7FD468247a15 ATTESTATION_KEY=x APP_SIGNATURE=x +SMS_PROVIDERS=twilio,nexmo NEXMO_KEY=x -NEXMO_SECRET=x \ No newline at end of file +NEXMO_SECRET=x +NEXMO_BLACKLIST= +TWILIO_ACCOUNT_SID=x +TWILIO_MESSAGING_SERVICE_SID=x +TWILIO_AUTH_TOKEN=x +TWILIO_BLACKLIST= +# Options: default, stackdriver +LOG_FORMAT= +# Options: fatal, error, warn, info, debug, trace +LOG_LEVEL= diff --git a/packages/attestation-service/index.d.ts b/packages/attestation-service/index.d.ts index ce576a76194..63299f7e48e 100644 --- a/packages/attestation-service/index.d.ts +++ b/packages/attestation-service/index.d.ts @@ -1 +1,2 @@ declare module 'nexmo' +declare module 'express-request-id' diff --git a/packages/attestation-service/migrations/20191015211858-create-attestation.js b/packages/attestation-service/migrations/20191015211858-create-attestation.js index aca5106d69c..e57863ec515 100644 --- a/packages/attestation-service/migrations/20191015211858-create-attestation.js +++ b/packages/attestation-service/migrations/20191015211858-create-attestation.js @@ -23,6 +23,14 @@ module.exports = { allowNull: false, type: Sequelize.STRING, }, + status: { + allowNull: false, + type: Sequelize.STRING, + }, + smsProvider: { + allowNull: false, + type: Sequelize.STRING, + }, createdAt: { allowNull: false, type: Sequelize.DATE, diff --git a/packages/attestation-service/nodemon.json b/packages/attestation-service/nodemon.json index fa8dc8662d2..6e1eeec2d3e 100644 --- a/packages/attestation-service/nodemon.json +++ b/packages/attestation-service/nodemon.json @@ -1,6 +1,6 @@ { "ignore": ["**/*.test.ts", "**/*.spec.ts", ".git", "node_modules"], "watch": ["src"], - "exec": "yarn start", + "exec": "yarn start-ts", "ext": "ts" } diff --git a/packages/attestation-service/package.json b/packages/attestation-service/package.json index d5bf54f11cd..9ee640d9a62 100644 --- a/packages/attestation-service/package.json +++ b/packages/attestation-service/package.json @@ -31,6 +31,8 @@ "@celo/utils": "^0.1.0", "bignumber.js": "^7.2.0", "body-parser": "1.19.0", + "bunyan": "1.8.12", + "bunyan-gke-stackdriver": "0.1.2", "debug": "^4.1.1", "dotenv": "8.0.0", "eth-lib": "^0.2.8", @@ -39,18 +41,24 @@ "nexmo": "2.4.2", "web3": "1.0.0-beta.37", "express": "4.17.1", + "express-rate-limit": "5.0.0", + "express-request-id": "1.4.1", "mysql2": "2.0.0-alpha1", "pg": "7.12.1", "pg-hstore": "2.3.3", + "prom-client": "11.2.0", "sequelize": "5.13.1", "sequelize-cli": "5.5.0", "sqlite3": "4.0.9", + "twilio": "^3.23.2", "yargs": "13.3.0" }, "devDependencies": { "@celo/protocol": "1.0.0", + "@types/bunyan": "1.8.4", "@types/dotenv": "4.0.3", "@types/debug": "^4.1.5", + "@types/express-rate-limit": "2.9.3", "@types/web3": "^1.0.18", "ts-node": "8.3.0", "nodemon": "1.19.1", diff --git a/packages/attestation-service/src/attestation.ts b/packages/attestation-service/src/attestation.ts deleted file mode 100644 index d0d1e004496..00000000000 --- a/packages/attestation-service/src/attestation.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { AttestationState } from '@celo/contractkit/lib/wrappers/Attestations' -import { attestToIdentifier, SignatureUtils } from '@celo/utils' -import { privateKeyToAddress } from '@celo/utils/lib/address' -import { retryAsyncWithBackOff } from '@celo/utils/lib/async' -import { Address, AddressType, E164Number, E164PhoneNumberType } from '@celo/utils/lib/io' -import express from 'express' -import * as t from 'io-ts' -import { existingAttestationRequest, kit, persistAttestationRequest } from './db' -import { sendSms } from './sms' - -export const AttestationRequestType = t.type({ - phoneNumber: E164PhoneNumberType, - account: AddressType, - issuer: AddressType, -}) - -export type AttestationRequest = t.TypeOf - -function getAttestationKey() { - if (process.env.ATTESTATION_KEY === undefined) { - console.error('Did not specify ATTESTATION_KEY') - throw new Error('Did not specify ATTESTATION_KEY') - } - - return process.env.ATTESTATION_KEY -} - -async function validateAttestationRequest(request: AttestationRequest) { - // check if it exists in the database - if ( - (await existingAttestationRequest(request.phoneNumber, request.account, request.issuer)) !== - null - ) { - throw new Error('Attestation already sent') - } - const key = getAttestationKey() - const address = privateKeyToAddress(key) - - // TODO: Check with the new Accounts.sol - if (address.toLowerCase() !== request.issuer.toLowerCase()) { - throw new Error(`Mismatching issuer, I am ${address}`) - } - - const attestations = await kit.contracts.getAttestations() - const state = await attestations.getAttestationState( - request.phoneNumber, - request.account, - request.issuer - ) - - if (state.attestationState !== AttestationState.Incomplete) { - throw new Error('No incomplete attestation found') - } - - // TODO: Check expiration - return -} - -async function validateAttestation( - attestationRequest: AttestationRequest, - attestationCode: string -) { - const attestations = await kit.contracts.getAttestations() - const isValid = await attestations.validateAttestationCode( - attestationRequest.phoneNumber, - attestationRequest.account, - attestationRequest.issuer, - attestationCode - ) - if (!isValid) { - throw new Error('Valid attestation could not be provided') - } - return -} - -function signAttestation(phoneNumber: E164Number, account: Address) { - const signature = attestToIdentifier(phoneNumber, account, getAttestationKey()) - - return SignatureUtils.serializeSignature(signature) -} - -function toBase64(str: string) { - return Buffer.from(str.slice(2), 'hex').toString('base64') -} - -function createAttestationTextMessage(attestationCode: string) { - return `<#> ${toBase64(attestationCode)} ${process.env.APP_SIGNATURE}` -} - -export async function handleAttestationRequest( - _req: express.Request, - res: express.Response, - attestationRequest: AttestationRequest -) { - let attestationCode - try { - await validateAttestationRequest(attestationRequest) - attestationCode = signAttestation(attestationRequest.phoneNumber, attestationRequest.account) - await validateAttestation(attestationRequest, attestationCode) - } catch (error) { - console.error(error) - res.status(422).json({ success: false, error: error.toString() }) - return - } - - try { - const textMessage = createAttestationTextMessage(attestationCode) - await persistAttestationRequest( - attestationRequest.phoneNumber, - attestationRequest.account, - attestationRequest.issuer - ) - await retryAsyncWithBackOff(sendSms, 10, [attestationRequest.phoneNumber, textMessage], 1000) - } catch (error) { - console.error(error) - res.status(500).json({ - success: false, - error: 'Something went wrong while attempting to send SMS, try again later', - }) - return - } - - res.json({ success: true }) -} diff --git a/packages/attestation-service/src/db.ts b/packages/attestation-service/src/db.ts index 5a75590d85c..06817c3275f 100644 --- a/packages/attestation-service/src/db.ts +++ b/packages/attestation-service/src/db.ts @@ -1,13 +1,17 @@ import { ContractKit, newKit } from '@celo/contractkit' -import { Sequelize } from 'sequelize' +import { FindOptions, Sequelize } from 'sequelize' import { fetchEnv } from './env' -import Attestation, { AttestationStatic } from './models/attestation' +import { rootLogger } from './logger' +import Attestation, { AttestationModel, AttestationStatic } from './models/attestation' export let sequelize: Sequelize | undefined export function initializeDB() { if (sequelize === undefined) { - sequelize = new Sequelize(fetchEnv('DATABASE_URL')) + sequelize = new Sequelize(fetchEnv('DATABASE_URL'), { + logging: (msg: string, sequelizeLogArgs: any) => + rootLogger.debug({ sequelizeLogArgs, component: 'sequelize' }, msg), + }) return sequelize.authenticate() as Promise } return Promise.resolve() @@ -15,15 +19,21 @@ export function initializeDB() { export let kit: ContractKit -export function initializeKit() { +export async function initializeKit() { if (kit === undefined) { kit = newKit(fetchEnv('CELO_PROVIDER')) + const blockNumber = await kit.web3.eth.getBlockNumber() + if (blockNumber === 0) { + throw new Error( + 'Could not fetch latest block from web3 provider ' + fetchEnv('CELO_PROVIDER') + ) + } } } let AttestationTable: AttestationStatic -async function getAttestationTable() { +export async function getAttestationTable() { if (AttestationTable) { return AttestationTable } @@ -31,18 +41,14 @@ async function getAttestationTable() { return AttestationTable } -export async function existingAttestationRequest( +export async function existingAttestationRequestRecord( phoneNumber: string, account: string, - issuer: string -): Promise { - return (await getAttestationTable()).findOne({ where: { phoneNumber, account, issuer } }) -} - -export async function persistAttestationRequest( - phoneNumber: string, - account: string, - issuer: string -) { - return (await getAttestationTable()).create({ phoneNumber, account, issuer }) + issuer: string, + options: FindOptions = {} +): Promise { + return (await getAttestationTable()).findOne({ + where: { phoneNumber, account, issuer }, + ...options, + }) } diff --git a/packages/attestation-service/src/env.ts b/packages/attestation-service/src/env.ts index 349ffa1a074..fc8e9306b08 100644 --- a/packages/attestation-service/src/env.ts +++ b/packages/attestation-service/src/env.ts @@ -1,7 +1,19 @@ +import * as dotenv from 'dotenv' + +if (process.env.CONFIG) { + dotenv.config({ path: process.env.CONFIG }) +} + export function fetchEnv(name: string): string { - if (process.env[name] === undefined) { + if (process.env[name] === undefined || process.env[name] === '') { console.error(`ENV var '${name}' was not defined`) throw new Error(`ENV var '${name}' was not defined`) } return process.env[name] as string } + +export function fetchEnvOrDefault(name: string, defaultValue: string): string { + return process.env[name] === undefined || process.env[name] === '' + ? defaultValue + : (process.env[name] as string) +} diff --git a/packages/attestation-service/src/index.ts b/packages/attestation-service/src/index.ts index 8b7d059f1f1..7a34fa5f663 100644 --- a/packages/attestation-service/src/index.ts +++ b/packages/attestation-service/src/index.ts @@ -1,24 +1,42 @@ -import * as dotenv from 'dotenv' import express from 'express' -import { AttestationRequestType, handleAttestationRequest } from './attestation' +import RateLimiter from 'express-rate-limit' +import requestIdMiddleware from 'express-request-id' +import * as PromClient from 'prom-client' import { initializeDB, initializeKit } from './db' -import { createValidatedHandler } from './request' +import { rootLogger } from './logger' +import { createValidatedHandler, loggerMiddleware } from './request' +import { + AttestationRequestType, + getAccountAddress, + getAttestationKey, + handleAttestationRequest, +} from './requestHandlers/attestation' +import { handleStatusRequest, StatusRequestType } from './requestHandlers/status' import { initializeSmsProviders } from './sms' async function init() { - if (process.env.CONFIG) { - dotenv.config({ path: process.env.CONFIG }) - } - await initializeDB() await initializeKit() + // TODO: Validate that the attestation key has been authorized by the account + getAttestationKey() + getAccountAddress() await initializeSmsProviders() const app = express() - app.use(express.json()) + app.use([express.json(), requestIdMiddleware(), loggerMiddleware]) const port = process.env.PORT || 3000 - app.listen(port, () => console.log(`Server running on ${port}!`)) + app.listen(port, () => rootLogger.info({ port }, 'Attestation Service started')) + const rateLimiter = new RateLimiter({ + windowMs: 5 * 60 * 100, // 5 minutes + max: 50, + // @ts-ignore + message: { status: false, error: 'Too many requests, please try again later' }, + }) + app.get('/metrics', (_req, res) => { + res.send(PromClient.register.metrics()) + }) + app.get('/status', rateLimiter, createValidatedHandler(StatusRequestType, handleStatusRequest)) app.post( '/attestations', createValidatedHandler(AttestationRequestType, handleAttestationRequest) @@ -26,7 +44,6 @@ async function init() { } init().catch((err) => { - console.error(`Error occurred while running server, exiting ....`) - console.error(err) + rootLogger.error({ err }) process.exit(1) }) diff --git a/packages/attestation-service/src/logger.ts b/packages/attestation-service/src/logger.ts new file mode 100644 index 00000000000..4badc84df01 --- /dev/null +++ b/packages/attestation-service/src/logger.ts @@ -0,0 +1,17 @@ +import Logger, { createLogger, levelFromName, LogLevelString, stdSerializers } from 'bunyan' +import { createStream } from 'bunyan-gke-stackdriver' +import { fetchEnvOrDefault } from './env' + +const logLevel = fetchEnvOrDefault('LOG_LEVEL', 'info') as LogLevelString +const logFormat = fetchEnvOrDefault('LOG_FORMAT', 'default') + +const stream = + logFormat === 'stackdriver' + ? createStream(levelFromName[logLevel]) + : { stream: process.stdout, level: logLevel } + +export const rootLogger: Logger = createLogger({ + name: 'attestation-service', + serializers: stdSerializers, + streams: [stream], +}) diff --git a/packages/attestation-service/src/metrics.ts b/packages/attestation-service/src/metrics.ts new file mode 100644 index 00000000000..cf9ed488b47 --- /dev/null +++ b/packages/attestation-service/src/metrics.ts @@ -0,0 +1,45 @@ +import { Counter } from 'prom-client' + +export const Counters = { + attestationRequestsTotal: new Counter({ + name: 'attestation_requests_total', + help: 'Counter for the number of attestation requests', + }), + attestationRequestsAlreadySent: new Counter({ + name: 'attestation_requests_already_sent', + help: 'Counter for the number of attestation requests that were already sent', + }), + attestationRequestsWrongIssuer: new Counter({ + name: 'attestation_requests_wrong_issuer', + help: 'Counter for the number of attestation requests that specified the wrong issuer', + }), + attestationRequestsWOIncompleteAttestation: new Counter({ + name: 'attestation_requests_without_incomplete_attestation', + help: + 'Counter for the number of attestation requests for which no incomplete attestations could be found', + }), + attestationRequestsValid: new Counter({ + name: 'attestation_requests_valid', + help: 'Counter for the number of requests involving valid attestation requests', + }), + attestationRequestsAttestationErrors: new Counter({ + name: 'attestation_requests_attestation_errors', + help: 'Counter for the number of requests for which producing the attestation failed', + }), + attestationRequestsUnableToServe: new Counter({ + name: 'attestation_requests_unable_to_serve', + help: 'Counter for the number of requests that could not be served', + }), + attestationRequestsSentSms: new Counter({ + name: 'attestation_requests_sent_sms', + help: 'Counter for the number of sms sent', + }), + attestationRequestsFailedToSendSms: new Counter({ + name: 'attestation_requests_failed_to_send_sms', + help: 'Counter for the number of sms that failed to send', + }), + attestationRequestUnexpectedErrors: new Counter({ + name: 'attestation_requests_unexpected_errors', + help: 'Counter for the number of unexpected errrors', + }), +} diff --git a/packages/attestation-service/src/models/attestation.ts b/packages/attestation-service/src/models/attestation.ts index a2b79e765b7..9ae3110cdfe 100644 --- a/packages/attestation-service/src/models/attestation.ts +++ b/packages/attestation-service/src/models/attestation.ts @@ -1,18 +1,44 @@ import { BuildOptions, DataTypes, Model, Sequelize } from 'sequelize' +import { SmsProviderType } from '../sms/base' -interface AttestationModel extends Model { +export interface AttestationModel extends Model { readonly id: number account: string phoneNumber: string issuer: string + status: AttestationStatus + smsProvider: SmsProviderType + + canSendSms: () => boolean +} + +export enum AttestationStatus { + DISPATCHING = 'DISPATCHING', + UNABLE_TO_SERVE = 'UNABLE_TO_SERVE', + FAILED = 'FAILED', + SENT = 'SMS_SEND_SUCCESS', + COMPLETE = 'COMPLETE', } export type AttestationStatic = typeof Model & (new (values?: object, options?: BuildOptions) => AttestationModel) -export default (sequelize: Sequelize) => - sequelize.define('Attestations', { +export default (sequelize: Sequelize) => { + const model = sequelize.define('Attestations', { account: DataTypes.STRING, phoneNumber: DataTypes.STRING, issuer: DataTypes.STRING, + status: DataTypes.STRING, + smsProvider: DataTypes.STRING, }) as AttestationStatic + + model.prototype.canSendSms = function() { + return [ + AttestationStatus.DISPATCHING, + AttestationStatus.FAILED, + AttestationStatus.UNABLE_TO_SERVE, + ].includes(this.status) + } + + return model +} diff --git a/packages/attestation-service/src/request.ts b/packages/attestation-service/src/request.ts index e3d7bd9b6c6..d3dee4bf017 100644 --- a/packages/attestation-service/src/request.ts +++ b/packages/attestation-service/src/request.ts @@ -1,13 +1,37 @@ +import Logger from 'bunyan' import express from 'express' import { isLeft } from 'fp-ts/lib/Either' import * as t from 'io-ts' +import { rootLogger } from './logger' + +export enum ErrorMessages { + UNKNOWN_ERROR = 'Something went wrong', +} + +export function asyncHandler(handler: (req: express.Request, res: Response) => Promise) { + return (req: express.Request, res: Response) => { + const handleUnknownError = (error: Error) => { + if (res.locals.logger) { + res.locals.logger.error(error) + } + respondWithError(res, 500, ErrorMessages.UNKNOWN_ERROR) + } + try { + handler(req, res) + .then(() => res.locals.logger.info({ res })) + .catch(handleUnknownError) + } catch (error) { + handleUnknownError(error) + } + } +} export function createValidatedHandler( requestType: t.Type, - handler: (req: express.Request, res: express.Response, parsedRequest: T) => Promise + handler: (req: express.Request, res: Response, parsedRequest: T) => Promise ) { - return async (req: express.Request, res: express.Response) => { - const parsedRequest = requestType.decode(req.body) + return asyncHandler(async (req: express.Request, res: Response) => { + const parsedRequest = requestType.decode({ ...req.query, ...req.body }) if (isLeft(parsedRequest)) { res.status(422).json({ success: false, @@ -15,14 +39,9 @@ export function createValidatedHandler( errors: serializeErrors(parsedRequest.left), }) } else { - try { - await handler(req, res, parsedRequest.right) - } catch (error) { - console.error(error) - res.status(500).json({ success: false, error: 'Something went wrong' }) - } + return handler(req, res, parsedRequest.right) } - } + }) } function serializeErrors(errors: t.Errors) { @@ -48,3 +67,26 @@ function serializeErrors(errors: t.Errors) { }) return serializedErrors } + +export function respondWithError(res: express.Response, statusCode: number, error: string) { + res.status(statusCode).json({ success: false, error }) +} + +export type Response = Omit & { + locals: { logger: Logger } & Omit +} + +export function loggerMiddleware( + req: express.Request, + res: express.Response, + next: express.NextFunction +) { + const requestLogger = rootLogger.child({ + // @ts-ignore express-request-id adds this + req_id: req.id, + }) + + res.locals.logger = requestLogger + requestLogger.info({ req }) + next() +} diff --git a/packages/attestation-service/src/requestHandlers/attestation.ts b/packages/attestation-service/src/requestHandlers/attestation.ts new file mode 100644 index 00000000000..7959f3632b4 --- /dev/null +++ b/packages/attestation-service/src/requestHandlers/attestation.ts @@ -0,0 +1,272 @@ +import { AttestationState } from '@celo/contractkit/lib/wrappers/Attestations' +import { attestToIdentifier, SignatureUtils } from '@celo/utils' +import { isValidPrivateKey, toChecksumAddress } from '@celo/utils/lib/address' +import { AddressType, E164PhoneNumberType } from '@celo/utils/lib/io' +import Logger from 'bunyan' +import { isValidAddress } from 'ethereumjs-util' +import express from 'express' +import * as t from 'io-ts' +import { Transaction } from 'sequelize' +import { existingAttestationRequestRecord, getAttestationTable, kit, sequelize } from '../db' +import { Counters } from '../metrics' +import { AttestationModel, AttestationStatus } from '../models/attestation' +import { respondWithError, Response } from '../request' +import { smsProviderFor } from '../sms' +import { SmsProviderType } from '../sms/base' +const SMS_SENDING_ERROR = 'Something went wrong while attempting to send SMS, try again later' +const ATTESTATION_ERROR = 'Valid attestation could not be provided' +const NO_INCOMPLETE_ATTESTATION_FOUND_ERROR = 'No incomplete attestation found' +const ATTESTATION_ALREADY_SENT_ERROR = 'Attestation already sent' +const COUNTRY_CODE_NOT_SERVED_ERROR = 'Your country code is not being served by this service' + +export const AttestationRequestType = t.type({ + phoneNumber: E164PhoneNumberType, + account: AddressType, + issuer: AddressType, +}) + +export type AttestationRequest = t.TypeOf + +export function getAttestationKey() { + if ( + process.env.ATTESTATION_KEY === undefined || + !isValidPrivateKey(process.env.ATTESTATION_KEY) + ) { + console.error('Did not specify valid ATTESTATION_KEY') + throw new Error('Did not specify valid ATTESTATION_KEY') + } + + return process.env.ATTESTATION_KEY +} + +export function getAccountAddress() { + if (process.env.ACCOUNT_ADDRESS === undefined || !isValidAddress(process.env.ACCOUNT_ADDRESS)) { + console.error('Did not specify valid ACCOUNT_ADDRESS') + throw new Error('Did not specify valid ACCOUNT_ADDRESS') + } + + return toChecksumAddress(process.env.ACCOUNT_ADDRESS) +} + +function toBase64(str: string) { + return Buffer.from(str.slice(2), 'hex').toString('base64') +} + +function createAttestationTextMessage(attestationCode: string) { + return `<#> celo://wallet/v/${toBase64(attestationCode)} ${process.env.APP_SIGNATURE}` +} + +async function ensureLockedRecord( + attestationRequest: AttestationRequest, + transaction: Transaction +) { + const AttestationTable = await getAttestationTable() + await AttestationTable.findOrCreate({ + where: { + phoneNumber: attestationRequest.phoneNumber, + account: attestationRequest.account, + issuer: attestationRequest.issuer, + }, + defaults: { + smsProvider: SmsProviderType.UNKNOWN, + status: AttestationStatus.DISPATCHING, + }, + transaction, + }) + + // Query to lock the record + const attestationRecord = await existingAttestationRequestRecord( + attestationRequest.phoneNumber, + attestationRequest.account, + attestationRequest.issuer, + { transaction, lock: Transaction.LOCK.UPDATE } + ) + + if (!attestationRecord) { + // This should never happen + throw new Error(`Somehow we did not get an attestation record`) + } + + if (!attestationRecord.canSendSms()) { + // Another transaction has locked on the record before we did + throw new Error(`Another process has already sent the sms`) + } + + return attestationRecord +} + +class AttestationRequestHandler { + logger: Logger + sequelizeLogger: (_msg: string, sequelizeLog: any) => void + constructor(public readonly attestationRequest: AttestationRequest, logger: Logger) { + this.logger = logger.child({ attestationRequest }) + this.sequelizeLogger = (msg: string, sequelizeLogArgs: any) => + this.logger.debug({ sequelizeLogArgs, component: 'sequelize' }, msg) + } + + async validateAttestationRequest() { + const attestationRecord = await existingAttestationRequestRecord( + this.attestationRequest.phoneNumber, + this.attestationRequest.account, + this.attestationRequest.issuer, + { logging: this.sequelizeLogger } + ) + // check if it exists in the database + if (attestationRecord && !attestationRecord.canSendSms()) { + Counters.attestationRequestsAlreadySent.inc() + throw new Error(ATTESTATION_ALREADY_SENT_ERROR) + } + const address = getAccountAddress() + + // TODO: Check with the new Accounts.sol + if (address.toLowerCase() !== this.attestationRequest.issuer.toLowerCase()) { + Counters.attestationRequestsWrongIssuer.inc() + throw new Error(`Mismatching issuer, I am ${address}`) + } + + const attestations = await kit.contracts.getAttestations() + const state = await attestations.getAttestationState( + this.attestationRequest.phoneNumber, + this.attestationRequest.account, + this.attestationRequest.issuer + ) + + if (state.attestationState !== AttestationState.Incomplete) { + Counters.attestationRequestsWOIncompleteAttestation.inc() + throw new Error(NO_INCOMPLETE_ATTESTATION_FOUND_ERROR) + } + + // TODO: Check expiration + return + } + + signAttestation() { + const signature = attestToIdentifier( + this.attestationRequest.phoneNumber, + this.attestationRequest.account, + getAttestationKey() + ) + + return SignatureUtils.serializeSignature(signature) + } + + async validateAttestation(attestationCode: string) { + const address = getAccountAddress() + const attestations = await kit.contracts.getAttestations() + const isValid = await attestations.validateAttestationCode( + this.attestationRequest.phoneNumber, + this.attestationRequest.account, + address, + attestationCode + ) + + if (!isValid) { + Counters.attestationRequestsAttestationErrors.inc() + throw new Error(ATTESTATION_ERROR) + } + return + } + + async sendSmsAndPersistAttestation(attestationCode: string) { + const textMessage = createAttestationTextMessage(attestationCode) + let attestationRecord: AttestationModel | null = null + + const transaction = await sequelize!.transaction({ logging: this.sequelizeLogger }) + + try { + attestationRecord = await ensureLockedRecord(this.attestationRequest, transaction) + const provider = smsProviderFor(this.attestationRequest.phoneNumber) + + if (!provider) { + await attestationRecord.update( + { status: AttestationStatus.UNABLE_TO_SERVE, smsProvider: SmsProviderType.UNKNOWN }, + { transaction, logging: this.sequelizeLogger } + ) + await transaction.commit() + Counters.attestationRequestsUnableToServe.inc() + return attestationRecord + } + + try { + await provider.sendSms(this.attestationRequest.phoneNumber, textMessage) + this.logger.info('Sent sms') + Counters.attestationRequestsSentSms.inc() + await attestationRecord.update( + { status: AttestationStatus.SENT, smsProvider: provider.type }, + { transaction, logging: this.sequelizeLogger } + ) + } catch (err) { + this.logger.error({ err }) + Counters.attestationRequestsFailedToSendSms.inc() + await attestationRecord.update( + { status: AttestationStatus.FAILED, smsProvider: provider.type }, + { transaction, logging: this.sequelizeLogger } + ) + } + + await transaction.commit() + } catch (err) { + this.logger.error({ err }) + await transaction.rollback() + } + + return attestationRecord + } + + respondAfterSendingSms(res: express.Response, attestationRecord: AttestationModel | null) { + if (!attestationRecord) { + this.logger.error({ err: 'Attestation Record was not created' }) + respondWithError(res, 500, SMS_SENDING_ERROR) + return + } + + switch (attestationRecord.status) { + case AttestationStatus.SENT: + res.status(201).json({ success: true }) + return + case AttestationStatus.FAILED: + respondWithError(res, 500, SMS_SENDING_ERROR) + return + case AttestationStatus.UNABLE_TO_SERVE: + respondWithError(res, 422, COUNTRY_CODE_NOT_SERVED_ERROR) + default: + this.logger.error({ + err: + 'Attestation Record should either be failed or sent, but was ' + + attestationRecord.status, + }) + respondWithError(res, 500, SMS_SENDING_ERROR) + return + } + } +} + +export async function handleAttestationRequest( + _req: express.Request, + res: Response, + attestationRequest: AttestationRequest +) { + const handler = new AttestationRequestHandler(attestationRequest, res.locals.logger) + let attestationCode + try { + Counters.attestationRequestsTotal.inc() + await handler.validateAttestationRequest() + Counters.attestationRequestsValid.inc() + attestationCode = handler.signAttestation() + await handler.validateAttestation(attestationCode) + } catch (err) { + handler.logger.info({ err }) + respondWithError(res, 422, err.toString()) + return + } + + try { + const attestationRecord = await handler.sendSmsAndPersistAttestation(attestationCode) + handler.respondAfterSendingSms(res, attestationRecord) + } catch (err) { + Counters.attestationRequestUnexpectedErrors.inc() + handler.logger.error({ err }) + respondWithError(res, 500, SMS_SENDING_ERROR) + return + } +} diff --git a/packages/attestation-service/src/requestHandlers/status.ts b/packages/attestation-service/src/requestHandlers/status.ts new file mode 100644 index 00000000000..9d1884a1b4d --- /dev/null +++ b/packages/attestation-service/src/requestHandlers/status.ts @@ -0,0 +1,47 @@ +import { privateKeyToAddress } from '@celo/utils/lib/address' +import { AttestationServiceStatusResponseType, SignatureType } from '@celo/utils/lib/io' +import { serializeSignature, signMessage } from '@celo/utils/lib/signatureUtils' +import express from 'express' +import * as t from 'io-ts' +import { ErrorMessages, respondWithError } from '../request' +import { blacklistRegionCodes, configuredSmsProviders } from '../sms' +import { getAccountAddress, getAttestationKey } from './attestation' + +export const SIGNATURE_PREFIX = 'attestation-service-status-signature:' +export const StatusRequestType = t.type({ + messageToSign: t.union([SignatureType, t.undefined]), +}) + +export type StatusRequest = t.TypeOf + +function produceSignature(message: string | undefined) { + if (!message) { + return undefined + } + const key = getAttestationKey() + const address = privateKeyToAddress(key) + return serializeSignature(signMessage(SIGNATURE_PREFIX + message, key, address)) +} + +export async function handleStatusRequest( + _req: express.Request, + res: express.Response, + statusRequest: StatusRequest +) { + try { + res + .json( + AttestationServiceStatusResponseType.encode({ + status: 'ok', + smsProviders: configuredSmsProviders(), + blacklistedRegionCodes: blacklistRegionCodes(), + accountAddress: getAccountAddress(), + signature: produceSignature(statusRequest.messageToSign), + }) + ) + .status(200) + } catch (error) { + console.error(error) + respondWithError(res, 500, ErrorMessages.UNKNOWN_ERROR) + } +} diff --git a/packages/attestation-service/src/sms.ts b/packages/attestation-service/src/sms.ts deleted file mode 100644 index 96faca6500f..00000000000 --- a/packages/attestation-service/src/sms.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { PhoneNumberUtil } from 'google-libphonenumber' -import Nexmo from 'nexmo' -import { fetchEnv } from './env' - -const phoneUtil = PhoneNumberUtil.getInstance() - -let nexmoClient: any -let nexmoNumbers: Array<{ - code: string - phoneNumber: string -}> = [] - -export async function initializeSmsProviders() { - nexmoClient = new Nexmo({ - apiKey: fetchEnv('NEXMO_KEY'), - apiSecret: fetchEnv('NEXMO_SECRET'), - }) - - const availableNumbers = await getAvailableNumbers() - - nexmoNumbers = availableNumbers.map((number: any) => ({ - phoneNumber: number.msisdn, - code: phoneUtil.getRegionCodeForNumber(phoneUtil.parse('+' + number.msisdn)), - })) - - console.log(nexmoNumbers) -} - -async function getAvailableNumbers(): Promise { - return new Promise((resolve, reject) => { - nexmoClient.number.get(null, (err: Error, responseData: any) => { - if (err) { - reject(err) - } else { - resolve(responseData.numbers) - } - }) - }) -} - -function getMatchingNumber(countryCode: string) { - const matchingNumber = nexmoNumbers.find((number) => number.code === countryCode) - if (matchingNumber !== undefined) { - return matchingNumber.phoneNumber - } - return nexmoNumbers[0].phoneNumber -} - -export async function sendSms(phoneNumber: string, message: string): Promise { - const countryCode = phoneUtil.getRegionCodeForNumber(phoneUtil.parse(phoneNumber)) - - if (!countryCode) { - throw new Error('could not extract country code') - } - - return new Promise((resolve, reject) => { - nexmoClient.message.sendSms( - getMatchingNumber(countryCode), - phoneNumber, - message, - (err: Error, responseData: any) => { - if (err) { - reject(err) - } else { - if (responseData.messages[0].status === '0') { - resolve(responseData.messages[0]) - } else { - reject(responseData.messages[0]['error-text']) - } - } - } - ) - }) -} diff --git a/packages/attestation-service/src/sms/base.ts b/packages/attestation-service/src/sms/base.ts new file mode 100644 index 00000000000..755d9762d6f --- /dev/null +++ b/packages/attestation-service/src/sms/base.ts @@ -0,0 +1,28 @@ +import { E164Number } from '@celo/utils/lib/io' +import { PhoneNumberUtil } from 'google-libphonenumber' +import { fetchEnvOrDefault } from '../env' +const phoneUtil = PhoneNumberUtil.getInstance() + +export abstract class SmsProvider { + abstract type: SmsProviderType + blacklistedRegionCodes: string[] = [] + + canServePhoneNumber(phoneNumber: E164Number) { + const countryCode = phoneUtil.getRegionCodeForNumber(phoneUtil.parse(phoneNumber)) + return !!countryCode && !this.blacklistedRegionCodes.includes(countryCode) + } + // Should throw Error when unsuccesful, return if successful + abstract sendSms(phoneNumber: E164Number, message: string): Promise +} + +export enum SmsProviderType { + NEXMO = 'nexmo', + UNKNOWN = 'unknown', + TWILIO = 'twilio', +} + +export function readBlacklistFromEnv(envVarName: string) { + return fetchEnvOrDefault(envVarName, '') + .split(',') + .filter((code) => code !== '') +} diff --git a/packages/attestation-service/src/sms/index.ts b/packages/attestation-service/src/sms/index.ts new file mode 100644 index 00000000000..ff77fa708af --- /dev/null +++ b/packages/attestation-service/src/sms/index.ts @@ -0,0 +1,47 @@ +import { intersection } from '@celo/utils/lib/collections' +import { E164Number } from '@celo/utils/lib/io' +import { fetchEnv } from '../env' +import { SmsProvider, SmsProviderType } from './base' +import { NexmoSmsProvider } from './nexmo' +import { TwilioSmsProvider } from './twilio' + +const smsProviders: SmsProvider[] = [] + +export async function initializeSmsProviders() { + const smsProvidersToConfigure = fetchEnv('SMS_PROVIDERS').split(',') as Array< + SmsProviderType | string + > + + if (smsProvidersToConfigure.length === 0) { + throw new Error('You have to specify at least one sms provider') + } + + for (const configuredSmsProvider of smsProvidersToConfigure) { + switch (configuredSmsProvider) { + case SmsProviderType.NEXMO: + const nexmoProvider = NexmoSmsProvider.fromEnv() + await nexmoProvider.initialize() + smsProviders.push(nexmoProvider) + break + case SmsProviderType.TWILIO: + const twilioProvider = TwilioSmsProvider.fromEnv() + await twilioProvider.initialize() + smsProviders.push(twilioProvider) + break + default: + throw new Error(`Unknown sms provider type specified: ${configuredSmsProvider}`) + } + } +} + +export function smsProviderFor(phoneNumber: E164Number) { + return smsProviders.find((provider) => provider.canServePhoneNumber(phoneNumber)) +} + +export function configuredSmsProviders() { + return smsProviders.map((provider) => provider.type) +} + +export function blacklistRegionCodes() { + return intersection(smsProviders.map((provider) => provider.blacklistedRegionCodes)) +} diff --git a/packages/attestation-service/src/sms/nexmo.ts b/packages/attestation-service/src/sms/nexmo.ts new file mode 100644 index 00000000000..cb126881c1e --- /dev/null +++ b/packages/attestation-service/src/sms/nexmo.ts @@ -0,0 +1,102 @@ +import { retryAsyncWithBackOff } from '@celo/utils/lib/async' +import { E164Number } from '@celo/utils/lib/io' +import { PhoneNumberUtil } from 'google-libphonenumber' +import Nexmo from 'nexmo' +import { fetchEnv } from '../env' +import { readBlacklistFromEnv, SmsProvider, SmsProviderType } from './base' + +const phoneUtil = PhoneNumberUtil.getInstance() + +export class NexmoSmsProvider extends SmsProvider { + static fromEnv() { + return new NexmoSmsProvider( + fetchEnv('NEXMO_KEY'), + fetchEnv('NEXMO_SECRET'), + readBlacklistFromEnv('NEXMO_BLACKLIST') + ) + } + type = SmsProviderType.NEXMO + client: any + nexmoNumbers: Array<{ + code: string + phoneNumber: string + }> = [] + + constructor(apiKey: string, apiSecret: string, blacklistedRegionCodes: string[]) { + super() + this.client = new Nexmo({ + apiKey, + apiSecret, + }) + + this.blacklistedRegionCodes = blacklistedRegionCodes + } + + initialize = async () => { + const availableNumbers = await this.getAvailableNumbers() + this.nexmoNumbers = availableNumbers.map((number: any) => ({ + phoneNumber: number.msisdn, + code: phoneUtil.getRegionCodeForNumber(phoneUtil.parse('+' + number.msisdn)), + })) + } + + sendSms = async (phoneNumber: E164Number, message: string): Promise => { + const countryCode = phoneUtil.getRegionCodeForNumber(phoneUtil.parse(phoneNumber)) + + if (!countryCode) { + throw new Error('could not extract country code') + } + + const nexmoNumber = this.getMatchingNumber(countryCode) + // Nexmo does not support sending more than 1 text message a second from some phone numbers, so just + // repeat with backoff + await retryAsyncWithBackOff( + () => this.sendSmsViaNexmo(nexmoNumber, phoneNumber, message), + 10, + [], + 1000 + ) + return + } + + private sendSmsViaNexmo(nexmoNumber: string, phoneNumber: string, message: string) { + return new Promise((resolve, reject) => { + this.client.message.sendSms( + nexmoNumber, + phoneNumber, + message, + (err: Error, responseData: any) => { + if (err) { + reject(err) + } else { + if (responseData.messages[0].status === '0') { + resolve(responseData.messages[0]) + } else { + reject(responseData.messages[0]['error-text']) + } + } + } + ) + }) + } + + private getAvailableNumbers = async (): Promise => { + return new Promise((resolve, reject) => { + this.client.number.get(null, (err: Error, responseData: any) => { + if (err) { + reject(err) + } else { + resolve(responseData.numbers) + } + }) + }) + } + + private getMatchingNumber = (countryCode: string) => { + const matchingNumber = this.nexmoNumbers.find((number) => number.code === countryCode) + if (matchingNumber !== undefined) { + return matchingNumber.phoneNumber + } + return this.nexmoNumbers[0].phoneNumber + } +} diff --git a/packages/attestation-service/src/sms/twilio.ts b/packages/attestation-service/src/sms/twilio.ts new file mode 100644 index 00000000000..60c036b5474 --- /dev/null +++ b/packages/attestation-service/src/sms/twilio.ts @@ -0,0 +1,48 @@ +import twilio, { Twilio } from 'twilio' +import { fetchEnv } from '../env' +import { readBlacklistFromEnv, SmsProvider, SmsProviderType } from './base' + +export class TwilioSmsProvider extends SmsProvider { + static fromEnv() { + return new TwilioSmsProvider( + fetchEnv('TWILIO_ACCOUNT_SID'), + fetchEnv('TWILIO_MESSAGING_SERVICE_SID'), + fetchEnv('TWILIO_AUTH_TOKEN'), + readBlacklistFromEnv('TWILIO_BLACKLIST') + ) + } + + client: Twilio + messagingServiceSid: string + type = SmsProviderType.TWILIO + + constructor( + twilioSid: string, + messagingServiceSid: string, + twilioAuthToken: string, + blacklistedRegionCodes: string[] + ) { + super() + this.client = twilio(twilioSid, twilioAuthToken) + this.messagingServiceSid = messagingServiceSid + this.blacklistedRegionCodes = blacklistedRegionCodes + } + + async initialize() { + // Ensure the messaging service exists + try { + await this.client.messaging.services.get(this.messagingServiceSid).fetch() + } catch (error) { + throw new Error(`Twilio Messaging Service could not be fetched: ${error}`) + } + } + + async sendSms(phoneNumber: string, message: string) { + await this.client.messages.create({ + body: message, + to: phoneNumber, + from: this.messagingServiceSid, + }) + return + } +} diff --git a/packages/celotool/README.md b/packages/celotool/README.md index 9abfa23c04e..e57021e5358 100644 --- a/packages/celotool/README.md +++ b/packages/celotool/README.md @@ -79,3 +79,20 @@ a few useful commands to make running a node really easy. - Build `celotooljs geth build --geth-dir -c` - Init `celotooljs geth init --geth-dir --data-dir -e ` - Run `celotooljs geth run --geth-dir --data-dir --sync-mode ` + +### How to Deploy a Test Network to the Cloud + +- Setup the environment variables: MNEMONIC, GETH_ACCOUNT_SECRET, and ETHSTATS_WEBSOCKETSECRET. + +- Deploy: `celotooljs deploy initial testnet -e yourname` + +- Get pods: `kubectl get pods -n yourname` + +- Start shell: `kubectl exec -n podname -it podname /bin/sh` + +- Tear down: `celotooljs deploy destroy testnet -e yourname` + +#### MacOS Setup + +- Install Helm 2.14.0 from https://get.helm.sh/ (Homebrew lacks this version.) + To get past the Unidentified Developer error: open the directory containing helm, then ctrl-click helm and select Open then Open again. Repeat for tiller. diff --git a/packages/celotool/package.json b/packages/celotool/package.json index 569b0fd5bca..1deb65bf3c0 100644 --- a/packages/celotool/package.json +++ b/packages/celotool/package.json @@ -18,7 +18,7 @@ "bignumber.js": "^7.2.0", "bip32": "^1.0.2", "bip39": "^2.5.0", - "bls12377js": "https://github.com/celo-org/bls12377js#cada1105f4a5e4c2ddd239c1874df3bf33144a10", + "bls12377js": "https://github.com/celo-org/bls12377js#ea09eba5c54fe63617af494a0c198fcc47582e0c", "dotenv": "6.1.0", "ecurve": "^1.0.6", "js-yaml": "^3.13.1", diff --git a/packages/celotool/src/cli.ts b/packages/celotool/src/cli.ts index ec0df9dd864..ae8613aec41 100755 --- a/packages/celotool/src/cli.ts +++ b/packages/celotool/src/cli.ts @@ -1,5 +1,5 @@ #!/usr/bin/env yarn run ts-node -r tsconfig-paths/register --cwd ../celotool -import * as yargs from 'yargs' +import yargs from 'yargs' // tslint:disable-next-line: no-unused-expression yargs diff --git a/packages/celotool/src/cmds/account.ts b/packages/celotool/src/cmds/account.ts index 355f35aa48a..b77e76a3058 100644 --- a/packages/celotool/src/cmds/account.ts +++ b/packages/celotool/src/cmds/account.ts @@ -1,5 +1,5 @@ import { addCeloEnvMiddleware, CeloEnvArgv } from 'src/lib/env-utils' -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'account ' diff --git a/packages/celotool/src/cmds/account/faucet.ts b/packages/celotool/src/cmds/account/faucet.ts index bea3ad7f493..bd0b54639a3 100644 --- a/packages/celotool/src/cmds/account/faucet.ts +++ b/packages/celotool/src/cmds/account/faucet.ts @@ -4,29 +4,42 @@ import { switchToClusterFromEnv } from 'src/lib/cluster' import { convertToContractDecimals } from 'src/lib/contract-utils' import { portForwardAnd } from 'src/lib/port_forward' import { validateAccountAddress } from 'src/lib/utils' -import * as yargs from 'yargs' +import yargs from 'yargs' import { AccountArgv } from '../account' export const command = 'faucet' -export const describe = 'command for fauceting an address with gold and dollars' +export const describe = 'command for fauceting an address with gold and/or dollars' interface FaucetArgv extends AccountArgv { account: string + gold: number + dollars: number } export const builder = (argv: yargs.Argv) => { - return argv.option('account', { - type: 'string', - description: 'Account to faucet', - demand: 'Please specify account to faucet', - coerce: (address) => { - if (!validateAccountAddress(address)) { - throw Error(`Receiver Address is invalid: "${address}"`) - } - return address - }, - }) + return argv + .option('account', { + type: 'string', + description: 'Account to faucet', + demand: 'Please specify account to faucet', + coerce: (address) => { + if (!validateAccountAddress(address)) { + throw Error(`Receiver Address is invalid: "${address}"`) + } + return address + }, + }) + .option('dollars', { + type: 'number', + description: 'Number of dollars to faucet', + default: 0, + }) + .option('gold', { + type: 'number', + description: 'Amount of gold to faucet', + default: 0, + }) } export const handler = async (argv: FaucetArgv) => { @@ -40,18 +53,26 @@ export const handler = async (argv: FaucetArgv) => { console.log(`Using account: ${account}`) kit.defaultAccount = account - const [goldToken, stableToken] = await Promise.all([ + const [goldToken, stableToken, reserve] = await Promise.all([ kit.contracts.getGoldToken(), kit.contracts.getStableToken(), + kit.contracts.getReserve(), ]) - const goldAmount = (await convertToContractDecimals(1, goldToken)).toString() - const stableTokenAmount = (await convertToContractDecimals(10, stableToken)).toString() - - console.log(`Fauceting ${goldAmount} Gold and ${stableTokenAmount} StableToken to ${address}`) - await Promise.all([ - goldToken.transfer(address, goldAmount).sendAndWaitForReceipt(), - stableToken.transfer(address, stableTokenAmount).sendAndWaitForReceipt(), - ]) + const goldAmount = await convertToContractDecimals(argv.gold, goldToken) + const stableTokenAmount = await convertToContractDecimals(argv.dollars, stableToken) + console.log( + `Fauceting ${goldAmount.toFixed()} Gold and ${stableTokenAmount.toFixed()} StableToken to ${address}` + ) + if (!goldAmount.isZero()) { + if (await reserve.isSpender(account)) { + await reserve.transferGold(address, goldAmount.toFixed()).sendAndWaitForReceipt() + } else { + await goldToken.transfer(address, goldAmount.toFixed()).sendAndWaitForReceipt() + } + } + if (stableTokenAmount.isZero()) { + await stableToken.transfer(address, stableTokenAmount.toFixed()).sendAndWaitForReceipt() + } } try { diff --git a/packages/celotool/src/cmds/account/revoke.ts b/packages/celotool/src/cmds/account/revoke.ts index 815dcb1516a..da0e946cc3d 100644 --- a/packages/celotool/src/cmds/account/revoke.ts +++ b/packages/celotool/src/cmds/account/revoke.ts @@ -2,7 +2,7 @@ import { downloadArtifacts } from 'src/lib/artifacts' import { switchToClusterFromEnv } from 'src/lib/cluster' import { portForwardAnd } from 'src/lib/port_forward' import { execCmd } from 'src/lib/utils' -import * as yargs from 'yargs' +import yargs from 'yargs' import { AccountArgv } from '../account' export const command = 'revoke' diff --git a/packages/celotool/src/cmds/account/verify.ts b/packages/celotool/src/cmds/account/verify.ts index d8245931955..6ac2bd9d1e9 100644 --- a/packages/celotool/src/cmds/account/verify.ts +++ b/packages/celotool/src/cmds/account/verify.ts @@ -9,7 +9,7 @@ import { concurrentMap } from '@celo/utils/lib/async' import { base64ToHex } from '@celo/utils/lib/attestations' import prompts from 'prompts' import { switchToClusterFromEnv } from 'src/lib/cluster' -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'verify' export const describe = 'command for requesting attestations for a phone number' @@ -149,7 +149,7 @@ async function verifyCode( attestationsToComplete: ActionableAttestation[] ) { const code = base64ToHex(base64Code) - const matchingIssuer = attestations.findMatchingIssuer( + const matchingIssuer = await attestations.findMatchingIssuer( phoneNumber, account, code, @@ -172,7 +172,9 @@ async function verifyCode( return } - const tx = await attestations.complete(phoneNumber, account, matchingIssuer, code).send() + const tx = await attestations + .complete(phoneNumber, account, matchingIssuer, code) + .then((x) => x.send()) return tx.waitReceipt() } diff --git a/packages/celotool/src/cmds/account/weekly_faucet.ts b/packages/celotool/src/cmds/account/weekly_faucet.ts index fa04ec537ef..8f8ffa08116 100644 --- a/packages/celotool/src/cmds/account/weekly_faucet.ts +++ b/packages/celotool/src/cmds/account/weekly_faucet.ts @@ -4,7 +4,7 @@ import { switchToClusterFromEnv } from 'src/lib/cluster' import { getBlockchainApiUrl } from 'src/lib/endpoints' import { portForwardAnd } from 'src/lib/port_forward' import { execCmd } from 'src/lib/utils' -import * as yargs from 'yargs' +import yargs from 'yargs' import { AccountArgv } from '../account' export const command = 'weekly-faucet' diff --git a/packages/celotool/src/cmds/backup.ts b/packages/celotool/src/cmds/backup.ts index 5f5d72f6dfc..db564f7bbb9 100644 --- a/packages/celotool/src/cmds/backup.ts +++ b/packages/celotool/src/cmds/backup.ts @@ -1,7 +1,7 @@ import { switchToClusterFromEnv } from 'src/lib/cluster' import { addCeloEnvMiddleware, CeloEnvArgv } from 'src/lib/env-utils' import { execCmdWithExitOnFailure } from 'src/lib/utils' -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'backup' diff --git a/packages/celotool/src/cmds/contract_addresses.ts b/packages/celotool/src/cmds/contract_addresses.ts index f06a849e8a3..23b07b01728 100644 --- a/packages/celotool/src/cmds/contract_addresses.ts +++ b/packages/celotool/src/cmds/contract_addresses.ts @@ -1,7 +1,7 @@ import * as fs from 'fs' import { CONTRACTS_TO_COPY, downloadArtifacts, getContractAddresses } from 'src/lib/artifacts' import { addCeloEnvMiddleware, CeloEnvArgv } from 'src/lib/env-utils' -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'contract-addresses' diff --git a/packages/celotool/src/cmds/copy_contract_artifacts.ts b/packages/celotool/src/cmds/copy_contract_artifacts.ts index ecd7fcc561e..ed3ceff74d0 100644 --- a/packages/celotool/src/cmds/copy_contract_artifacts.ts +++ b/packages/celotool/src/cmds/copy_contract_artifacts.ts @@ -1,6 +1,6 @@ import { CONTRACTS_TO_COPY, copyContractArtifacts, downloadArtifacts } from 'src/lib/artifacts' import { addCeloEnvMiddleware, CeloEnvArgv } from 'src/lib/env-utils' -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'copy-contract-artifacts' diff --git a/packages/celotool/src/cmds/copy_policies.ts b/packages/celotool/src/cmds/copy_policies.ts index 71ef05ddd28..91a4e0bb8fb 100644 --- a/packages/celotool/src/cmds/copy_policies.ts +++ b/packages/celotool/src/cmds/copy_policies.ts @@ -7,7 +7,7 @@ import { validateAndSwitchToEnv, } from 'src/lib/env-utils' import { deleteOtherPolicies, downloadPolicies, uploadPolicies } from 'src/lib/policies' -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'copy-policies' diff --git a/packages/celotool/src/cmds/deploy.ts b/packages/celotool/src/cmds/deploy.ts index dc110aa1909..7abad88cb6e 100644 --- a/packages/celotool/src/cmds/deploy.ts +++ b/packages/celotool/src/cmds/deploy.ts @@ -1,5 +1,5 @@ import { addCeloEnvMiddleware, CeloEnvArgv } from 'src/lib/env-utils' -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'deploy ' diff --git a/packages/celotool/src/cmds/deploy/destroy.ts b/packages/celotool/src/cmds/deploy/destroy.ts index 535583a82cb..d771e8eed03 100644 --- a/packages/celotool/src/cmds/deploy/destroy.ts +++ b/packages/celotool/src/cmds/deploy/destroy.ts @@ -1,4 +1,4 @@ -import * as yargs from 'yargs' +import yargs from 'yargs' import { DeployArgv } from '../deploy' export const command = 'destroy ' diff --git a/packages/celotool/src/cmds/deploy/initial.ts b/packages/celotool/src/cmds/deploy/initial.ts index 93b657c04fc..cd686443c34 100644 --- a/packages/celotool/src/cmds/deploy/initial.ts +++ b/packages/celotool/src/cmds/deploy/initial.ts @@ -1,4 +1,4 @@ -import * as yargs from 'yargs' +import yargs from 'yargs' import { DeployArgv } from '../deploy' export const command = 'initial ' diff --git a/packages/celotool/src/cmds/deploy/initial/contracts.ts b/packages/celotool/src/cmds/deploy/initial/contracts.ts index fdfe62f4e62..2521502ef6d 100644 --- a/packages/celotool/src/cmds/deploy/initial/contracts.ts +++ b/packages/celotool/src/cmds/deploy/initial/contracts.ts @@ -1,27 +1,19 @@ /* tslint:disable no-console */ import { newKit } from '@celo/contractkit' import { IdentityMetadataWrapper } from '@celo/contractkit/lib/identity' -import { - createAttestationServiceURLClaim, - createNameClaim, -} from '@celo/contractkit/lib/identity/claims/claim' +import { createAttestationServiceURLClaim } from '@celo/contractkit/lib/identity/claims/attestation-service-url' +import { createNameClaim } from '@celo/contractkit/lib/identity/claims/claim' import { concurrentMap } from '@celo/utils/lib/async' import { LocalSigner } from '@celo/utils/lib/signatureUtils' import { writeFileSync } from 'fs' import { uploadArtifacts } from 'src/lib/artifacts' import { switchToClusterFromEnv } from 'src/lib/cluster' import { envVar, fetchEnv } from 'src/lib/env-utils' -import { - AccountType, - generatePrivateKey, - getAddressesFor, - getPrivateKeysFor, - privateKeyToAddress, -} from 'src/lib/generate_utils' -import { OG_ACCOUNTS } from 'src/lib/genesis_constants' +import { privateKeyToAddress } from 'src/lib/generate_utils' +import { migrationOverrides, truffleOverrides, validatorKeys } from 'src/lib/migration-utils' import { portForwardAnd } from 'src/lib/port_forward' import { uploadFileToGoogleStorage } from 'src/lib/testnet-utils' -import { ensure0x, execCmd } from 'src/lib/utils' +import { execCmd } from 'src/lib/utils' import { InitialArgv } from '../../deploy/initial' export const command = 'contracts' @@ -32,28 +24,6 @@ export const builder = {} export const CLABS_VALIDATOR_METADATA_BUCKET = 'clabs_validator_metadata' -function minerForEnv() { - if (fetchEnv(envVar.VALIDATORS) === 'og') { - return ensure0x(OG_ACCOUNTS[0].address) - } else { - return privateKeyToAddress( - generatePrivateKey(fetchEnv(envVar.MNEMONIC), AccountType.VALIDATOR, 0) - ) - } -} - -function getValidatorKeys() { - if (fetchEnv(envVar.VALIDATORS) === 'og') { - return OG_ACCOUNTS.map((account) => account.privateKey).map(ensure0x) - } else { - return getPrivateKeysFor( - AccountType.VALIDATOR, - fetchEnv(envVar.MNEMONIC), - parseInt(fetchEnv(envVar.VALIDATORS), 10) - ).map(ensure0x) - } -} - function getAttestationServiceUrl(testnet: string, index: number) { return `https://${testnet}-attestation-service.${fetchEnv( envVar.CLUSTER_DOMAIN_NAME @@ -107,34 +77,14 @@ export const handler = async (argv: InitialArgv) => { console.log(`Deploying smart contracts to ${argv.celoEnv}`) const cb = async () => { - const mnemonic = fetchEnv(envVar.MNEMONIC) - const validatorKeys = getValidatorKeys() - const migrationOverrides = JSON.stringify({ - validators: { - validatorKeys, - }, - stableToken: { - initialBalances: { - addresses: getAddressesFor(AccountType.FAUCET, mnemonic, 2), - values: getAddressesFor(AccountType.FAUCET, mnemonic, 2).map( - () => '60000000000000000000000' - ), // 60k Celo Dollars - }, - }, - }) - - const truffleOverrides = JSON.stringify({ - from: minerForEnv(), - }) - await execCmd( - `yarn --cwd ../protocol run init-network -n ${ - argv.celoEnv - } -c '${truffleOverrides}' -m '${migrationOverrides}'` + `yarn --cwd ../protocol run init-network -n ${argv.celoEnv} -c '${JSON.stringify( + truffleOverrides() + )}' -m '${JSON.stringify(migrationOverrides())}'` ) console.info('Register Metadata for Clabs validators') - await concurrentMap(5, validatorKeys, (privateKey, index) => + await concurrentMap(5, validatorKeys(), (privateKey, index) => registerMetadata(argv.celoEnv, privateKey, index) ) } diff --git a/packages/celotool/src/cmds/deploy/initial/load-test.ts b/packages/celotool/src/cmds/deploy/initial/load-test.ts index 0501090633c..233b37433ef 100644 --- a/packages/celotool/src/cmds/deploy/initial/load-test.ts +++ b/packages/celotool/src/cmds/deploy/initial/load-test.ts @@ -1,7 +1,7 @@ import { switchToClusterFromEnv } from 'src/lib/cluster' import { fetchEnv } from 'src/lib/env-utils' import { installHelmChart } from 'src/lib/load-test' -import * as yargs from 'yargs' +import yargs from 'yargs' import { InitialArgv } from '../../deploy/initial' export const command = 'load-test' diff --git a/packages/celotool/src/cmds/deploy/initial/tracer-tool.ts b/packages/celotool/src/cmds/deploy/initial/tracer-tool.ts index 55ee2cefa48..d2dd391535d 100644 --- a/packages/celotool/src/cmds/deploy/initial/tracer-tool.ts +++ b/packages/celotool/src/cmds/deploy/initial/tracer-tool.ts @@ -1,7 +1,7 @@ import { switchToClusterFromEnv } from 'src/lib/cluster' import { installHelmChart } from 'src/lib/tracer-tool' import { execCmdWithExitOnFailure } from 'src/lib/utils' -import * as yargs from 'yargs' +import yargs from 'yargs' import { InitialArgv } from '../../deploy/initial' export const command = 'tracer-tool' diff --git a/packages/celotool/src/cmds/deploy/initial/verify-contracts.ts b/packages/celotool/src/cmds/deploy/initial/verify-contracts.ts new file mode 100644 index 00000000000..2744549b20c --- /dev/null +++ b/packages/celotool/src/cmds/deploy/initial/verify-contracts.ts @@ -0,0 +1,47 @@ +import { switchToClusterFromEnv } from 'src/lib/cluster' +import { getBlockscoutUrl } from 'src/lib/endpoints' +import { portForwardAnd } from 'src/lib/port_forward' +import { execCmd } from 'src/lib/utils' +import yargs from 'yargs' +import { InitialArgv } from '../../deploy/initial' + +export const command = 'verify-contracts' + +export const describe = 'verify the celo smart contracts in blockscout' + +export const builder = (argv: yargs.Argv) => { + return argv.option('contract', { + type: 'string', + description: 'Contract name if only one contract want to be verified', + default: 'all', + }) +} + +interface VerifyContractsInitialArgv extends InitialArgv { + contract: string +} + +export const handler = async (argv: VerifyContractsInitialArgv) => { + await switchToClusterFromEnv() + // Check if blockscout is deployed and online? + const blockscoutUrl = getBlockscoutUrl(argv) + + console.debug( + `Validating smart contracts ${argv.contract} in ${argv.celoEnv} for URL ${blockscoutUrl}` + ) + + const cb = async () => { + await execCmd( + `yarn --cwd ../protocol run verify -c ${argv.contract} -n ${argv.celoEnv} -b ${blockscoutUrl}` + ) + } + + try { + await portForwardAnd(argv.celoEnv, cb) + process.exit(0) + } catch (error) { + console.error(`Unable to verify contracts from ${argv.celoEnv} in ${blockscoutUrl}`) + console.error(error) + process.exit(1) + } +} diff --git a/packages/celotool/src/cmds/deploy/migrate.ts b/packages/celotool/src/cmds/deploy/migrate.ts index 7e2df568e47..c36cef8d85a 100644 --- a/packages/celotool/src/cmds/deploy/migrate.ts +++ b/packages/celotool/src/cmds/deploy/migrate.ts @@ -1,4 +1,4 @@ -import * as yargs from 'yargs' +import yargs from 'yargs' import { DeployArgv } from '../deploy' export const command = 'migrate ' diff --git a/packages/celotool/src/cmds/deploy/upgrade.ts b/packages/celotool/src/cmds/deploy/upgrade.ts index d2069f21b35..5b86da9f6cc 100644 --- a/packages/celotool/src/cmds/deploy/upgrade.ts +++ b/packages/celotool/src/cmds/deploy/upgrade.ts @@ -1,4 +1,4 @@ -import * as yargs from 'yargs' +import yargs from 'yargs' import { DeployArgv } from '../deploy' export const command = 'upgrade ' diff --git a/packages/celotool/src/cmds/deploy/upgrade/blockscout.ts b/packages/celotool/src/cmds/deploy/upgrade/blockscout.ts index fa1ffaae37b..265c244bf1c 100644 --- a/packages/celotool/src/cmds/deploy/upgrade/blockscout.ts +++ b/packages/celotool/src/cmds/deploy/upgrade/blockscout.ts @@ -4,7 +4,7 @@ import { switchToClusterFromEnv } from 'src/lib/cluster' import { fetchEnvOrFallback } from 'src/lib/env-utils' import { resetCloudSQLInstance, retrieveCloudSQLConnectionInfo } from 'src/lib/helm_deploy' import { execCmdWithExitOnFailure } from 'src/lib/utils' -import * as yargs from 'yargs' +import yargs from 'yargs' import { UpgradeArgv } from '../../deploy/upgrade' export const command = 'blockscout' diff --git a/packages/celotool/src/cmds/deploy/upgrade/contracts.ts b/packages/celotool/src/cmds/deploy/upgrade/contracts.ts index ca633eff4e1..e0983a2d1c8 100644 --- a/packages/celotool/src/cmds/deploy/upgrade/contracts.ts +++ b/packages/celotool/src/cmds/deploy/upgrade/contracts.ts @@ -1,5 +1,6 @@ import { downloadArtifacts, uploadArtifacts } from 'src/lib/artifacts' import { switchToClusterFromEnv } from 'src/lib/cluster' +import { migrationOverrides, truffleOverrides } from 'src/lib/migration-utils' import { portForwardAnd } from 'src/lib/port_forward' import { execCmd } from 'src/lib/utils' import { UpgradeArgv } from '../../deploy/upgrade' @@ -15,7 +16,11 @@ export const handler = async (argv: UpgradeArgv) => { console.info(`Upgrading smart contracts on ${argv.celoEnv}`) const cb = async () => { - await execCmd(`yarn --cwd ../protocol run migrate -n ${argv.celoEnv}`) + await execCmd( + `yarn --cwd ../protocol run migrate -n ${argv.celoEnv} -c '${JSON.stringify( + truffleOverrides() + )}' -m '${JSON.stringify(migrationOverrides())}'` + ) } try { diff --git a/packages/celotool/src/cmds/deploy/upgrade/faucet.ts b/packages/celotool/src/cmds/deploy/upgrade/faucet.ts index 1a908fd664b..60f7a6f3dfa 100644 --- a/packages/celotool/src/cmds/deploy/upgrade/faucet.ts +++ b/packages/celotool/src/cmds/deploy/upgrade/faucet.ts @@ -10,7 +10,7 @@ import { } from 'src/lib/generate_utils' import { portForwardAnd } from 'src/lib/port_forward' import { execCmd } from 'src/lib/utils' -import * as yargs from 'yargs' +import yargs from 'yargs' import { UpgradeArgv } from '../../deploy/upgrade' export const command = 'faucet' diff --git a/packages/celotool/src/cmds/deploy/upgrade/testnet.ts b/packages/celotool/src/cmds/deploy/upgrade/testnet.ts index 7e37df950bf..fc86a33b44f 100644 --- a/packages/celotool/src/cmds/deploy/upgrade/testnet.ts +++ b/packages/celotool/src/cmds/deploy/upgrade/testnet.ts @@ -5,7 +5,7 @@ import { uploadGenesisBlockToGoogleStorage, uploadStaticNodesToGoogleStorage, } from 'src/lib/testnet-utils' -import * as yargs from 'yargs' +import yargs from 'yargs' import { UpgradeArgv } from '../../deploy/upgrade' export const command = 'testnet' diff --git a/packages/celotool/src/cmds/fork_env.ts b/packages/celotool/src/cmds/fork_env.ts index 8974e9347a7..e03e3185fa2 100644 --- a/packages/celotool/src/cmds/fork_env.ts +++ b/packages/celotool/src/cmds/fork_env.ts @@ -3,7 +3,7 @@ import { readFileSync, writeFileSync } from 'fs' import { map, merge, reduce } from 'lodash' import path from 'path' import { CeloEnvArgv, genericEnvFilePath, isValidCeloEnv, monorepoRoot } from 'src/lib/env-utils' -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'fork-env ' export const describe = 'command for forking an environment off the default .env file' diff --git a/packages/celotool/src/cmds/gcp.ts b/packages/celotool/src/cmds/gcp.ts index d31afbc5fc1..438093b2fa9 100644 --- a/packages/celotool/src/cmds/gcp.ts +++ b/packages/celotool/src/cmds/gcp.ts @@ -1,4 +1,4 @@ -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'gcp ' diff --git a/packages/celotool/src/cmds/gcp/remove-leaked-forwarding-rules.ts b/packages/celotool/src/cmds/gcp/remove-leaked-forwarding-rules.ts index dde7b02741e..0f3e023ec90 100644 --- a/packages/celotool/src/cmds/gcp/remove-leaked-forwarding-rules.ts +++ b/packages/celotool/src/cmds/gcp/remove-leaked-forwarding-rules.ts @@ -1,6 +1,6 @@ import { zip } from 'lodash' import { execCmd, execCmdWithExitOnFailure } from 'src/lib/utils' -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'remove-leaked-forwarding-rules' diff --git a/packages/celotool/src/cmds/generate.ts b/packages/celotool/src/cmds/generate.ts index f3a0eae88da..74d75f19768 100644 --- a/packages/celotool/src/cmds/generate.ts +++ b/packages/celotool/src/cmds/generate.ts @@ -1,4 +1,4 @@ -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'generate ' export const describe = 'commands for generating network parameters' diff --git a/packages/celotool/src/cmds/generate/account-address.ts b/packages/celotool/src/cmds/generate/account-address.ts index 66434bd2f03..6b12f756ab9 100644 --- a/packages/celotool/src/cmds/generate/account-address.ts +++ b/packages/celotool/src/cmds/generate/account-address.ts @@ -1,6 +1,6 @@ /* tslint:disable no-console */ import { privateKeyToAddress } from 'src/lib/generate_utils' -import * as yargs from 'yargs' +import yargs from 'yargs' interface AccountAddressArgv { privateKey: string diff --git a/packages/celotool/src/cmds/generate/address-from-env.ts b/packages/celotool/src/cmds/generate/address-from-env.ts index 3c0337fae19..01d15f3f79b 100644 --- a/packages/celotool/src/cmds/generate/address-from-env.ts +++ b/packages/celotool/src/cmds/generate/address-from-env.ts @@ -5,7 +5,7 @@ import { getAddressFromEnv, MNEMONIC_ACCOUNT_TYPE_CHOICES, } from 'src/lib/generate_utils' -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'address-from-env' diff --git a/packages/celotool/src/cmds/generate/bip32.ts b/packages/celotool/src/cmds/generate/bip32.ts index e202d19773d..87487f6c9df 100644 --- a/packages/celotool/src/cmds/generate/bip32.ts +++ b/packages/celotool/src/cmds/generate/bip32.ts @@ -4,7 +4,7 @@ import { generatePrivateKey, MNEMONIC_ACCOUNT_TYPE_CHOICES, } from 'src/lib/generate_utils' -import * as yargs from 'yargs' +import yargs from 'yargs' interface Bip32Argv { mnemonic: string diff --git a/packages/celotool/src/cmds/generate/genesis-file.ts b/packages/celotool/src/cmds/generate/genesis-file.ts index 82f101871c0..28b4a63afe6 100644 --- a/packages/celotool/src/cmds/generate/genesis-file.ts +++ b/packages/celotool/src/cmds/generate/genesis-file.ts @@ -1,6 +1,6 @@ import { addCeloEnvMiddleware, CeloEnvArgv } from 'src/lib/env-utils' import { generateGenesisFromEnv } from 'src/lib/generate_utils' -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'genesis-file' diff --git a/packages/celotool/src/cmds/generate/public-key.ts b/packages/celotool/src/cmds/generate/public-key.ts index c4bdd4aef6c..43b33f6e9db 100644 --- a/packages/celotool/src/cmds/generate/public-key.ts +++ b/packages/celotool/src/cmds/generate/public-key.ts @@ -5,7 +5,7 @@ import { MNEMONIC_ACCOUNT_TYPE_CHOICES, privateKeyToPublicKey, } from 'src/lib/generate_utils' -import * as yargs from 'yargs' +import yargs from 'yargs' interface Bip32Argv { mnemonic: string diff --git a/packages/celotool/src/cmds/geth.ts b/packages/celotool/src/cmds/geth.ts index e6486af2c46..4198222c947 100644 --- a/packages/celotool/src/cmds/geth.ts +++ b/packages/celotool/src/cmds/geth.ts @@ -1,4 +1,4 @@ -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'geth ' diff --git a/packages/celotool/src/cmds/geth/build.ts b/packages/celotool/src/cmds/geth/build.ts index 56391aa4010..98e1d9c993c 100644 --- a/packages/celotool/src/cmds/geth/build.ts +++ b/packages/celotool/src/cmds/geth/build.ts @@ -1,5 +1,5 @@ import { execCmdWithExitOnFailure } from 'src/lib/utils' -import * as yargs from 'yargs' +import yargs from 'yargs' import { GethArgv } from '../geth' export const command = 'build' diff --git a/packages/celotool/src/cmds/geth/create_account.ts b/packages/celotool/src/cmds/geth/create_account.ts index c3c91d568e5..f21ed3418f6 100644 --- a/packages/celotool/src/cmds/geth/create_account.ts +++ b/packages/celotool/src/cmds/geth/create_account.ts @@ -5,7 +5,7 @@ import { switchToClusterFromEnv } from 'src/lib/cluster' import { addCeloEnvMiddleware, CeloEnvArgv } from 'src/lib/env-utils' import { fetchPassword } from 'src/lib/geth' import { addCeloGethMiddleware, execCmd, execCmdWithExitOnFailure } from 'src/lib/utils' -import * as yargs from 'yargs' +import yargs from 'yargs' import { GethArgv } from '../geth' export const command = 'create-account' diff --git a/packages/celotool/src/cmds/geth/get_gold_balance.ts b/packages/celotool/src/cmds/geth/get_gold_balance.ts index 4c1c9823d39..68215c6fe84 100644 --- a/packages/celotool/src/cmds/geth/get_gold_balance.ts +++ b/packages/celotool/src/cmds/geth/get_gold_balance.ts @@ -1,6 +1,6 @@ import { addCeloEnvMiddleware } from 'src/lib/env-utils' import { addCeloGethMiddleware, ensure0x, execCmdWithExitOnFailure } from 'src/lib/utils' -import * as yargs from 'yargs' +import yargs from 'yargs' import { GethArgv } from '../geth' export const command = 'get gold balance' diff --git a/packages/celotool/src/cmds/geth/init.ts b/packages/celotool/src/cmds/geth/init.ts index 151d16250d5..4377de63464 100644 --- a/packages/celotool/src/cmds/geth/init.ts +++ b/packages/celotool/src/cmds/geth/init.ts @@ -4,7 +4,7 @@ import { switchToClusterFromEnv } from 'src/lib/cluster' import { addCeloEnvMiddleware, CeloEnvArgv } from 'src/lib/env-utils' import { getEnodesAddresses, writeStaticNodes } from 'src/lib/geth' import { addCeloGethMiddleware, execCmdWithExitOnFailure } from 'src/lib/utils' -import * as yargs from 'yargs' +import yargs from 'yargs' import { GethArgv } from '../geth' const STATIC_NODES_FILE_NAME = 'static-nodes.json' diff --git a/packages/celotool/src/cmds/geth/run.ts b/packages/celotool/src/cmds/geth/run.ts index 37be0587dd9..43756798f58 100644 --- a/packages/celotool/src/cmds/geth/run.ts +++ b/packages/celotool/src/cmds/geth/run.ts @@ -2,7 +2,7 @@ import { spawnSync } from 'child_process' import fs from 'fs' import path from 'path' import { addCeloGethMiddleware, ensure0x, validateAccountAddress } from 'src/lib/utils' -import * as yargs from 'yargs' +import yargs from 'yargs' import { GethArgv } from '../geth' const STATIC_NODES_FILE_NAME = 'static-nodes.json' @@ -124,6 +124,7 @@ export const handler = async (argv: RunArgv) => { verbosity.toString(), '--consoleoutput=stdout', // Send all logs to stdout '--consoleformat=term', + '--istanbul.lookbackwindow=2', ] if (nodekeyhex !== null && nodekeyhex.length > 0) { diff --git a/packages/celotool/src/cmds/geth/simulate_client.ts b/packages/celotool/src/cmds/geth/simulate_client.ts index 4f1c0c9b4f9..ebbae49b55a 100644 --- a/packages/celotool/src/cmds/geth/simulate_client.ts +++ b/packages/celotool/src/cmds/geth/simulate_client.ts @@ -3,7 +3,7 @@ import { getBlockscoutClusterInternalUrl } from 'src/lib/endpoints' import { addCeloEnvMiddleware, CeloEnvArgv } from 'src/lib/env-utils' import { privateKeyToAddress } from 'src/lib/generate_utils' import { checkGethStarted, getWeb3AndTokensContracts, simulateClient, sleep } from 'src/lib/geth' -import * as yargs from 'yargs' +import yargs from 'yargs' import { GethArgv } from '../geth' export const command = 'simulate-client' diff --git a/packages/celotool/src/cmds/geth/static_nodes.ts b/packages/celotool/src/cmds/geth/static_nodes.ts index dbc6dd5822c..7dc71ff8198 100644 --- a/packages/celotool/src/cmds/geth/static_nodes.ts +++ b/packages/celotool/src/cmds/geth/static_nodes.ts @@ -1,7 +1,7 @@ import { switchToClusterFromEnv } from 'src/lib/cluster' import { addCeloEnvMiddleware, CeloEnvArgv } from 'src/lib/env-utils' import { getEnodesWithExternalIPAddresses, writeStaticNodes } from 'src/lib/geth' -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'static-nodes' diff --git a/packages/celotool/src/cmds/geth/trace.ts b/packages/celotool/src/cmds/geth/trace.ts index d648761bd68..480bc3c4f8d 100644 --- a/packages/celotool/src/cmds/geth/trace.ts +++ b/packages/celotool/src/cmds/geth/trace.ts @@ -1,7 +1,7 @@ import { getBlockscoutUrl } from 'src/lib/endpoints' import { addCeloEnvMiddleware, CeloEnvArgv } from 'src/lib/env-utils' import { checkGethStarted, getWeb3AndTokensContracts, traceTransactions } from 'src/lib/geth' -import * as yargs from 'yargs' +import yargs from 'yargs' import { GethArgv } from '../geth' export const command = 'trace ' diff --git a/packages/celotool/src/cmds/geth/transfer.ts b/packages/celotool/src/cmds/geth/transfer.ts index c234a81f01d..61264dfd0e7 100644 --- a/packages/celotool/src/cmds/geth/transfer.ts +++ b/packages/celotool/src/cmds/geth/transfer.ts @@ -1,6 +1,6 @@ import BigNumber from 'bignumber.js' import { checkGethStarted, getWeb3AndTokensContracts, transferERC20Token } from 'src/lib/geth' -import * as yargs from 'yargs' +import yargs from 'yargs' import { GethArgv } from '../geth' export const command = 'transfer ' diff --git a/packages/celotool/src/cmds/monitoring.ts b/packages/celotool/src/cmds/monitoring.ts index 0e5d79ec558..1d2eec94384 100644 --- a/packages/celotool/src/cmds/monitoring.ts +++ b/packages/celotool/src/cmds/monitoring.ts @@ -1,5 +1,5 @@ import { addCeloEnvMiddleware } from 'src/lib/env-utils' -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'monitoring ' diff --git a/packages/celotool/src/cmds/port_forward.ts b/packages/celotool/src/cmds/port_forward.ts index f1e1890db5d..ada31dbe9e9 100644 --- a/packages/celotool/src/cmds/port_forward.ts +++ b/packages/celotool/src/cmds/port_forward.ts @@ -1,7 +1,7 @@ import { switchToClusterFromEnv } from 'src/lib/cluster' import { addCeloEnvMiddleware, CeloEnvArgv } from 'src/lib/env-utils' import { defaultPortsString, portForward } from 'src/lib/port_forward' -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'port-forward' export const describe = 'command for port-forwarding to a specific network' diff --git a/packages/celotool/src/cmds/restore.ts b/packages/celotool/src/cmds/restore.ts index 9924ae372a0..761d7c4e601 100644 --- a/packages/celotool/src/cmds/restore.ts +++ b/packages/celotool/src/cmds/restore.ts @@ -1,7 +1,7 @@ import { switchToClusterFromEnv } from 'src/lib/cluster' import { addCeloEnvMiddleware, CeloEnvArgv } from 'src/lib/env-utils' import { execCmdWithExitOnFailure } from 'src/lib/utils' -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'restore' diff --git a/packages/celotool/src/cmds/ssh-vm-node.ts b/packages/celotool/src/cmds/ssh-vm-node.ts index 8d7871907c1..ad42348d2ec 100644 --- a/packages/celotool/src/cmds/ssh-vm-node.ts +++ b/packages/celotool/src/cmds/ssh-vm-node.ts @@ -1,6 +1,6 @@ import { addCeloEnvMiddleware, CeloEnvArgv, envVar, fetchEnv } from 'src/lib/env-utils' import { execCmd } from 'src/lib/utils' -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'ssh-vm-node [nodeIndex]' diff --git a/packages/celotool/src/cmds/switch.ts b/packages/celotool/src/cmds/switch.ts index ccf2b7d48d3..ccd9ed982f9 100644 --- a/packages/celotool/src/cmds/switch.ts +++ b/packages/celotool/src/cmds/switch.ts @@ -1,6 +1,6 @@ import { switchToClusterFromEnv } from 'src/lib/cluster' import { addCeloEnvMiddleware } from 'src/lib/env-utils' -import * as yargs from 'yargs' +import yargs from 'yargs' export const command = 'switch' diff --git a/packages/celotool/src/cmds/transactions/describe.ts b/packages/celotool/src/cmds/transactions/describe.ts index 4a192790425..2fb1c3249fd 100644 --- a/packages/celotool/src/cmds/transactions/describe.ts +++ b/packages/celotool/src/cmds/transactions/describe.ts @@ -6,7 +6,7 @@ import { } from '@celo/walletkit' import { getWeb3Client } from 'src/lib/blockchain' import { switchToClusterFromEnv } from 'src/lib/cluster' -import * as yargs from 'yargs' +import yargs from 'yargs' import { TransactionsArgv } from '../transactions' export const command = 'describe ' diff --git a/packages/celotool/src/cmds/transactions/list.ts b/packages/celotool/src/cmds/transactions/list.ts index fb9e18ebde0..6996d63735d 100644 --- a/packages/celotool/src/cmds/transactions/list.ts +++ b/packages/celotool/src/cmds/transactions/list.ts @@ -12,7 +12,7 @@ import { getWeb3Client } from 'src/lib/blockchain' import { switchToClusterFromEnv } from 'src/lib/cluster' import { getBlockscoutUrl } from 'src/lib/endpoints' import Web3 from 'web3' -import * as yargs from 'yargs' +import yargs from 'yargs' import { TransactionsArgv } from '../transactions' export const command = 'list
' diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 7f66cc0ee0e..6173d66a13e 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -1,5 +1,7 @@ +// tslint:disable-next-line: no-reference (Required to make this work w/ ts-node) +/// import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' -import { AccountsWrapper } from '@celo/contractkit/lib/wrappers/Accounts' +import { getBlsPoP, getBlsPublicKey } from '@celo/utils/lib/bls' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { assert } from 'chai' @@ -8,17 +10,116 @@ import { assertAlmostEqual, getContext, getEnode, + GethInstanceConfig, importGenesis, initAndStartGeth, sleep, + waitToFinishSyncing, } from './utils' +interface MemberSwapper { + swap(): Promise +} + +async function newMemberSwapper(kit: ContractKit, members: string[]): Promise { + let index = 0 + const group = (await kit.web3.eth.getAccounts())[0] + await Promise.all(members.slice(1).map((member) => removeMember(member))) + + async function removeMember(member: string) { + return (await kit.contracts.getValidators()) + .removeMember(member) + .sendAndWaitForReceipt({ from: group }) + } + + async function addMember(member: string) { + return (await (await kit.contracts.getValidators()).addMember( + group, + member + )).sendAndWaitForReceipt({ from: group }) + } + + async function getGroupMembers() { + const groupInfo = await (await kit._web3Contracts.getValidators()).methods + .getValidatorGroup(group) + .call() + return groupInfo[0] + } + + return { + async swap() { + const removedMember = members[index % members.length] + await removeMember(members[index % members.length]) + index = index + 1 + const addedMember = members[index % members.length] + await addMember(members[index % members.length]) + const groupMembers = await getGroupMembers() + assert.include(groupMembers, addedMember) + assert.notInclude(groupMembers, removedMember) + }, + } +} + +interface KeyRotator { + rotate(): Promise +} + +async function newKeyRotator( + kit: ContractKit, + web3s: Web3[], + privateKeys: string[] +): Promise { + let index = 0 + const validator = (await kit.web3.eth.getAccounts())[0] + const accountsWrapper = await kit.contracts.getAccounts() + + async function authorizeValidatorSigner(signer: string, signerWeb3: any) { + const signerKit = newKitFromWeb3(signerWeb3) + const pop = await (await signerKit.contracts.getAccounts()).generateProofOfSigningKeyPossession( + validator, + signer + ) + return (await accountsWrapper.authorizeValidatorSigner(signer, pop)).sendAndWaitForReceipt({ + from: validator, + }) + } + + async function updateValidatorBlsKey(signerPrivateKey: string) { + const blsPublicKey = getBlsPublicKey(signerPrivateKey) + const blsPop = getBlsPoP(validator, signerPrivateKey) + // TODO(asa): Send this from the signer instead. + const validatorsWrapper = await kit.contracts.getValidators() + return validatorsWrapper + .updateBlsPublicKey(blsPublicKey, blsPop) + .sendAndWaitForReceipt({ from: validator }) + } + + return { + async rotate() { + if (index < web3s.length) { + const signerWeb3 = web3s[index] + const signer: string = (await signerWeb3.eth.getAccounts())[0] + const signerPrivateKey = privateKeys[index] + await Promise.all([ + authorizeValidatorSigner(signer, signerWeb3), + updateValidatorBlsKey(signerPrivateKey), + ]) + index += 1 + assert.equal(await accountsWrapper.getValidatorSigner(validator), signer) + } + }, + } +} + +// TODO(asa): Test independent rotation of ecdsa, bls keys. describe('governance tests', () => { const gethConfig = { migrate: true, instances: [ + // Validators 0 and 1 are swapped in and out of the group. { name: 'validator0', validating: true, syncmode: 'full', port: 30303, rpcport: 8545 }, { name: 'validator1', validating: true, syncmode: 'full', port: 30305, rpcport: 8547 }, + // Validator 2 will authorize a validating key every other epoch. { 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 }, @@ -34,7 +135,7 @@ describe('governance tests', () => { let goldToken: any let registry: any let validators: any - let accounts: AccountsWrapper + let accounts: any let kit: ContractKit before(async function(this: any) { @@ -55,12 +156,7 @@ describe('governance tests', () => { registry = await kit._web3Contracts.getRegistry() election = await kit._web3Contracts.getElection() epochRewards = await kit._web3Contracts.getEpochRewards() - accounts = await kit.contracts.getAccounts() - } - - const unlockAccount = async (address: string, theWeb3: any) => { - // Assuming empty password - await theWeb3.eth.personal.unlockAccount(address, '', 1000) + accounts = await kit._web3Contracts.getAccounts() } const getValidatorGroupMembers = async (blockNumber?: number) => { @@ -79,9 +175,17 @@ describe('governance tests', () => { } } - const getValidatorGroupKeys = async () => { + const getValidatorSigner = async (address: string, blockNumber?: number) => { + if (blockNumber) { + return accounts.methods.getValidatorSigner(address).call({}, blockNumber) + } else { + return accounts.methods.getValidatorSigner(address).call() + } + } + + const getValidatorGroupPrivateKey = async () => { const [groupAddress] = await validators.methods.getRegisteredValidatorGroups().call() - const name = await accounts.getName(groupAddress) + const name = await accounts.methods.getName(groupAddress).call() const encryptedKeystore64 = name.split(' ')[1] const encryptedKeystore = JSON.parse(Buffer.from(encryptedKeystore64, 'base64').toString()) // The validator group ID is the validator group keystore encrypted with validator 0's @@ -89,43 +193,7 @@ describe('governance tests', () => { // @ts-ignore const encryptionKey = `0x${gethConfig.instances[0].privateKey}` const decryptedKeystore = web3.eth.accounts.decrypt(encryptedKeystore, encryptionKey) - return [groupAddress, decryptedKeystore.privateKey] - } - - const activate = async (account: string, txOptions: any = {}) => { - await unlockAccount(account, web3) - const [group] = await validators.methods.getRegisteredValidatorGroups().call() - const tx = election.methods.activate(group) - let gas = txOptions.gas - if (!gas) { - gas = await tx.estimateGas({ ...txOptions }) - } - return tx.send({ from: account, ...txOptions, gas }) - } - - const removeMember = async ( - groupWeb3: any, - group: string, - 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 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 tx.send({ from: group, ...txOptions, gas }) + return decryptedKeystore.privateKey } const isLastBlockOfEpoch = (blockNumber: number, epochSize: number) => { @@ -144,62 +212,123 @@ describe('governance tests', () => { const previousBalance = new BigNumber( await token.methods.balanceOf(address).call({}, blockNumber - 1) ) - assert.isNotNaN(currentBalance) - assert.isNotNaN(previousBalance) + assert.isFalse(currentBalance.isNaN()) + assert.isFalse(previousBalance.isNaN()) assertAlmostEqual(currentBalance.minus(previousBalance), expected) } describe('when the validator set is changing', () => { let epoch: number const blockNumbers: number[] = [] - let allValidators: string[] + let validatorAccounts: string[] before(async function(this: any) { this.timeout(0) // Disable test timeout 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(context.hooks.gethBinaryPath, groupInstance) - allValidators = await getValidatorGroupMembers() - assert.equal(allValidators.length, 5) + const groupPrivateKey = await getValidatorGroupPrivateKey() + const rotation0PrivateKey = + '0xa42ac9c99f6ab2c96ee6cae1b40d36187f65cd878737f6623cd363fb94ba7087' + const rotation1PrivateKey = + '0x4519cae145fb9499358be484ca60c80d8f5b7f9c13ff82c88ec9e13283e9de1a' + const additionalNodes: GethInstanceConfig[] = [ + { + name: 'validatorGroup', + validating: false, + syncmode: 'full', + port: 30313, + wsport: 8555, + rpcport: 8557, + privateKey: groupPrivateKey.slice(2), + peers: [await getEnode(8545)], + }, + ] + await Promise.all( + additionalNodes.map((nodeConfig) => + initAndStartGeth(context.hooks.gethBinaryPath, nodeConfig) + ) + ) + // Connect the validating nodes to the non-validating nodes, to test that announce messages + // are properly gossiped. + const additionalValidatingNodes = [ + { + name: 'validator2KeyRotation0', + validating: true, + syncmode: 'full', + lightserv: false, + port: 30315, + wsport: 8559, + privateKey: rotation0PrivateKey.slice(2), + peers: [await getEnode(8557)], + }, + { + name: 'validator2KeyRotation1', + validating: true, + syncmode: 'full', + lightserv: false, + port: 30317, + wsport: 8561, + privateKey: rotation1PrivateKey.slice(2), + peers: [await getEnode(8557)], + }, + ] + await Promise.all( + additionalValidatingNodes.map((nodeConfig) => + initAndStartGeth(context.hooks.gethBinaryPath, nodeConfig) + ) + ) + + validatorAccounts = await getValidatorGroupMembers() + assert.equal(validatorAccounts.length, 5) epoch = new BigNumber(await validators.methods.getEpochSize().call()).toNumber() assert.equal(epoch, 10) - // Give the nodes time to sync, and time for an epoch transition so we can activate our vote. + // Wait for an epoch transition so we can activate our vote. let blockNumber: number do { blockNumber = await web3.eth.getBlockNumber() await sleep(0.1) } while (blockNumber % epoch !== 1) + // Wait for an extra epoch transition to ensure everyone is connected to one another. + do { + blockNumber = await web3.eth.getBlockNumber() + await sleep(0.1) + } while (blockNumber % epoch !== 1) - await activate(allValidators[0]) - const groupWeb3 = new Web3('ws://localhost:8567') + // Prepare for member swapping. + const groupWeb3 = new Web3('ws://localhost:8555') + await waitToFinishSyncing(groupWeb3) const groupKit = newKitFromWeb3(groupWeb3) + const group: string = (await groupWeb3.eth.getAccounts())[0] + await (await groupKit.contracts.getElection()).activate(group) + validators = await groupKit._web3Contracts.getValidators() - const membersToSwap = [allValidators[0], allValidators[1]] - let includedMemberIndex = 1 - await removeMember(groupWeb3, groupAddress, membersToSwap[0]) + const membersToSwap = [validatorAccounts[0], validatorAccounts[1]] + const memberSwapper = await newMemberSwapper(groupKit, membersToSwap) + + // Prepare for key rotation. + const validatorWeb3 = new Web3('http://localhost:8549') + const authorizedWeb3s = [new Web3('ws://localhost:8559'), new Web3('ws://localhost:8561')] + await Promise.all(authorizedWeb3s.map((w) => waitToFinishSyncing(w))) + const authorizedPrivateKeys = [rotation0PrivateKey, rotation1PrivateKey] + const keyRotator = await newKeyRotator( + newKitFromWeb3(validatorWeb3), + authorizedWeb3s, + authorizedPrivateKeys + ) + let errorWhileChangingValidatorSet = '' const changeValidatorSet = async (header: any) => { - blockNumbers.push(header.number) - // At the start of epoch N, swap members so the validator set is different for epoch N + 1. - if (header.number % epoch === 1) { - const memberToRemove = membersToSwap[includedMemberIndex] - const memberToAdd = membersToSwap[(includedMemberIndex + 1) % 2] - await removeMember(groupWeb3, groupAddress, memberToRemove) - await addMember(groupWeb3, groupAddress, memberToAdd) - includedMemberIndex = (includedMemberIndex + 1) % 2 - const newMembers = await getValidatorGroupMembers() - assert.include(newMembers, memberToAdd) - assert.notInclude(newMembers, memberToRemove) + try { + blockNumbers.push(header.number) + // At the start of epoch N, perform actions so the validator set is different for epoch N + 1. + // Note that all of these actions MUST complete within the epoch. + if (header.number % epoch === 0 && errorWhileChangingValidatorSet === '') { + // 1. Swap validator0 and validator1 so one is a member of the group and the other is not. + // 2. Rotate keys for validator 2 by authorizing a new validating key. + await Promise.all([memberSwapper.swap(), keyRotator.rotate()]) + } + } catch (e) { + console.error(e) + errorWhileChangingValidatorSet = e } } @@ -210,10 +339,20 @@ describe('governance tests', () => { ;(subscription as any).unsubscribe() // Wait for the current epoch to complete. await sleep(epoch) + assert.equal(errorWhileChangingValidatorSet, '') }) - const getValidatorSetAtBlock = async (blockNumber: number): Promise => { - return election.methods.currentValidators().call({}, blockNumber) + const getValidatorSetSignersAtBlock = async (blockNumber: number): Promise => { + return election.methods.getCurrentValidatorSigners().call({}, blockNumber) + } + + const getValidatorSetAccountsAtBlock = async (blockNumber: number) => { + const signingKeys = await getValidatorSetSignersAtBlock(blockNumber) + return Promise.all( + signingKeys.map((address: string) => + accounts.methods.signerToAccount(address).call({}, blockNumber) + ) + ) } const getLastEpochBlock = (blockNumber: number) => { @@ -232,24 +371,44 @@ describe('governance tests', () => { } }) - it('should always return a validator set equal to the group members at the end of the last epoch', async () => { + it('should always return a validator set equal to the signing keys of the group members at the end of the last epoch', async function(this: any) { + this.timeout(0) for (const blockNumber of blockNumbers) { const lastEpochBlock = getLastEpochBlock(blockNumber) - const groupMembership = await getValidatorGroupMembers(lastEpochBlock) - const validatorSet = await getValidatorSetAtBlock(blockNumber) - assert.sameMembers(groupMembership, validatorSet) + const memberAccounts = await getValidatorGroupMembers(lastEpochBlock) + const memberSigners = await Promise.all( + memberAccounts.map((v: string) => getValidatorSigner(v, lastEpochBlock)) + ) + const validatorSetSigners = await getValidatorSetSignersAtBlock(blockNumber) + const validatorSetAccounts = await getValidatorSetAccountsAtBlock(blockNumber) + assert.sameMembers(memberSigners, validatorSetSigners) + assert.sameMembers(memberAccounts, validatorSetAccounts) } }) - it('should only have created blocks whose miner was in the current validator set', async () => { + it('should block propose in a round robin fashion', async () => { + let roundRobinOrder: string[] = [] for (const blockNumber of blockNumbers) { - const validatorSet = await getValidatorSetAtBlock(blockNumber) + const lastEpochBlock = getLastEpochBlock(blockNumber) + // Fetch the round robin order if it hasn't already been set for this epoch. + if (roundRobinOrder.length === 0 || blockNumber === lastEpochBlock + 1) { + const validatorSet = await getValidatorSetSignersAtBlock(blockNumber) + roundRobinOrder = await Promise.all( + validatorSet.map( + async (_, i) => (await web3.eth.getBlock(lastEpochBlock + i + 1)).miner + ) + ) + assert.sameMembers(roundRobinOrder, validatorSet) + } + const indexInEpoch = blockNumber - lastEpochBlock - 1 + const expectedProposer = roundRobinOrder[indexInEpoch % roundRobinOrder.length] const block = await web3.eth.getBlock(blockNumber) - assert.include(validatorSet.map((x) => x.toLowerCase()), block.miner.toLowerCase()) + assert.equal(block.miner.toLowerCase(), expectedProposer.toLowerCase()) } }) - it('should update the validator scores at the end of each epoch', async () => { + it('should update the validator scores at the end of each epoch', async function(this: any) { + this.timeout(0) const adjustmentSpeed = fromFixed( new BigNumber((await validators.methods.getValidatorScoreParameters().call())[1]) ) @@ -257,10 +416,10 @@ describe('governance tests', () => { const assertScoreUnchanged = async (validator: string, blockNumber: number) => { const score = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber))[2] + (await validators.methods.getValidator(validator).call({}, blockNumber)).score ) const previousScore = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber - 1))[2] + (await validators.methods.getValidator(validator).call({}, blockNumber - 1)).score ) assert.isFalse(score.isNaN()) assert.isFalse(previousScore.isNaN()) @@ -269,27 +428,27 @@ describe('governance tests', () => { const assertScoreChanged = async (validator: string, blockNumber: number) => { const score = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber))[2] + (await validators.methods.getValidator(validator).call({}, blockNumber)).score ) const previousScore = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber - 1))[2] + (await validators.methods.getValidator(validator).call({}, blockNumber - 1)).score ) assert.isFalse(score.isNaN()) assert.isFalse(previousScore.isNaN()) const expectedScore = adjustmentSpeed .times(uptime) .plus(new BigNumber(1).minus(adjustmentSpeed).times(fromFixed(previousScore))) - assert.equal(score.toFixed(), toFixed(expectedScore).toFixed()) + assertAlmostEqual(score, toFixed(expectedScore)) } for (const blockNumber of blockNumbers) { let expectUnchangedScores: string[] let expectChangedScores: string[] if (isLastBlockOfEpoch(blockNumber, epoch)) { - expectChangedScores = await getValidatorSetAtBlock(blockNumber) - expectUnchangedScores = allValidators.filter((x) => !expectChangedScores.includes(x)) + expectChangedScores = await getValidatorSetAccountsAtBlock(blockNumber) + expectUnchangedScores = validatorAccounts.filter((x) => !expectChangedScores.includes(x)) } else { - expectUnchangedScores = allValidators + expectUnchangedScores = validatorAccounts expectChangedScores = [] } @@ -303,7 +462,8 @@ describe('governance tests', () => { } }) - it('should distribute epoch payments at the end of each epoch', async () => { + it('should distribute epoch payments at the end of each epoch', async function(this: any) { + this.timeout(0) const commission = 0.1 const targetValidatorEpochPayment = new BigNumber( await epochRewards.methods.targetValidatorEpochPayment().call() @@ -316,9 +476,9 @@ describe('governance tests', () => { const getExpectedTotalPayment = async (validator: string, blockNumber: number) => { const score = new BigNumber( - (await validators.methods.getValidator(validator).call({}, blockNumber))[2] + (await validators.methods.getValidator(validator).call({}, blockNumber)).score ) - assert.isNotNaN(score) + assert.isFalse(score.isNaN()) // We need to calculate the rewards multiplier for the previous block, before // the rewards actually are awarded. const rewardsMultiplier = new BigNumber( @@ -333,10 +493,12 @@ describe('governance tests', () => { let expectUnchangedBalances: string[] let expectChangedBalances: string[] if (isLastBlockOfEpoch(blockNumber, epoch)) { - expectChangedBalances = await getValidatorSetAtBlock(blockNumber) - expectUnchangedBalances = allValidators.filter((x) => !expectChangedBalances.includes(x)) + expectChangedBalances = await getValidatorSetAccountsAtBlock(blockNumber) + expectUnchangedBalances = validatorAccounts.filter( + (x) => !expectChangedBalances.includes(x) + ) } else { - expectUnchangedBalances = allValidators + expectUnchangedBalances = validatorAccounts expectChangedBalances = [] } @@ -360,7 +522,8 @@ describe('governance tests', () => { } }) - it('should distribute epoch rewards at the end of each epoch', async () => { + it('should distribute epoch rewards at the end of each epoch', async function(this: any) { + this.timeout(0) const lockedGold = await kit._web3Contracts.getLockedGold() const governance = await kit._web3Contracts.getGovernance() const gasPriceMinimum = await kit._web3Contracts.getGasPriceMinimum() @@ -484,13 +647,13 @@ describe('governance tests', () => { ) const difference = currentTarget.minus(previousTarget) - // Assert equal to 10 decimal places due to rounding errors. + // Assert equal to 9 decimal places due to rounding errors. assert.equal( fromFixed(difference) - .dp(10) + .dp(9) .toFixed(), fromFixed(expected) - .dp(10) + .dp(9) .toFixed() ) } diff --git a/packages/celotool/src/e2e-tests/sync_tests.ts b/packages/celotool/src/e2e-tests/sync_tests.ts index 965bd1a45d8..5253f59e45c 100644 --- a/packages/celotool/src/e2e-tests/sync_tests.ts +++ b/packages/celotool/src/e2e-tests/sync_tests.ts @@ -7,6 +7,7 @@ import { initAndStartGeth, killInstance, sleep, + waitToFinishSyncing, } from './utils' describe('sync tests', function(this: any) { @@ -28,8 +29,6 @@ describe('sync tests', function(this: any) { await hooks.before() // Restart validator nodes. await hooks.restart() - // Give validators time to connect to eachother. - await sleep(40) const fullInstance = { name: 'full', validating: false, @@ -40,7 +39,8 @@ describe('sync tests', function(this: any) { peers: [await getEnode(8545)], } await initAndStartGeth(hooks.gethBinaryPath, fullInstance) - await sleep(3) + const web3 = new Web3('http://localhost:8553') + await waitToFinishSyncing(web3) }) after(hooks.after) @@ -66,17 +66,19 @@ describe('sync tests', function(this: any) { it('should sync the latest block', async () => { const validatingWeb3 = new Web3(`http://localhost:8545`) - const validatingFirstBlock = await validatingWeb3.eth.getBlock('latest') - await sleep(20) - const validatingLatestBlock = await validatingWeb3.eth.getBlock('latest') - await sleep(3) + const validatingFirstBlock = await validatingWeb3.eth.getBlockNumber() const syncWeb3 = new Web3(`http://localhost:8555`) - const syncLatestBlock = await syncWeb3.eth.getBlock('latest') - assert.isAbove(validatingLatestBlock.number, 1) + await waitToFinishSyncing(syncWeb3) + // Give the validators time to create more blocks. + await sleep(10) + const validatingLatestBlock = await validatingWeb3.eth.getBlockNumber() + await sleep(1) + const syncLatestBlock = await syncWeb3.eth.getBlockNumber() + assert.isAbove(validatingLatestBlock, 1) // Assert that the validator is still producing blocks. - assert.isAbove(validatingLatestBlock.number, validatingFirstBlock.number) + assert.isAbove(validatingLatestBlock, validatingFirstBlock) // Assert that the syncing node has synced with the validator. - assert.isAtLeast(syncLatestBlock.number, validatingLatestBlock.number) + assert.isAtLeast(syncLatestBlock, validatingLatestBlock) }) }) } diff --git a/packages/celotool/src/e2e-tests/transfer_tests.ts b/packages/celotool/src/e2e-tests/transfer_tests.ts index 25f994d2578..80016d596ce 100644 --- a/packages/celotool/src/e2e-tests/transfer_tests.ts +++ b/packages/celotool/src/e2e-tests/transfer_tests.ts @@ -75,10 +75,16 @@ const setIntrinsicGas = async (validatorUri: string, validatorAddress: string, g const kit = newKit(validatorUri) const parameters = await kit.contracts.getBlockchainParameters() await parameters - .setIntrinsicGasForAlternativeGasCurrency(gasCost.toString()) + .setIntrinsicGasForAlternativeFeeCurrency(gasCost.toString()) .sendAndWaitForReceipt({ from: validatorAddress }) } +// Intrinsic gas for a basic transaction +const INTRINSIC_GAS_FOR_TX = 21000 + +// Additional intrinsic gas for a transaction with fee currency specified +const ADDITIONAL_INTRINSIC_TX_GAS_COST = 166000 + /** Helper to watch balance changes over accounts */ interface BalanceWatcher { update(): Promise @@ -244,8 +250,9 @@ describe('Transfer tests', function(this: any) { txOptions: { gas?: number gasPrice?: string - gasCurrency?: string - gasFeeRecipient?: string + feeCurrency?: string + gatewayFeeRecipient?: string + gatewayFee?: string } = {} ) => { const res = await kit.sendTransaction({ @@ -264,8 +271,9 @@ describe('Transfer tests', function(this: any) { txOptions: { gas?: number gasPrice?: string - gasCurrency?: string - gasFeeRecipient?: string + feeCurrency?: string + gatewayFeeRecipient?: string + gatewayFee?: string } = {} ) => { const kitStableToken = await kit.contracts.getStableToken() @@ -277,10 +285,10 @@ describe('Transfer tests', function(this: any) { return res } - const getGasPriceMinimum = async (gasCurrency: string | undefined) => { + const getGasPriceMinimum = async (feeCurrency: string | undefined) => { const gasPriceMinimum = await kit._web3Contracts.getGasPriceMinimum() - if (gasCurrency) { - return gasPriceMinimum.methods.getGasPriceMinimum(gasCurrency).call() + if (feeCurrency) { + return gasPriceMinimum.methods.getGasPriceMinimum(feeCurrency).call() } else { return gasPriceMinimum.methods.gasPriceMinimum().call() } @@ -290,6 +298,7 @@ describe('Transfer tests', function(this: any) { total: BigNumber tip: BigNumber base: BigNumber + gateway: BigNumber } interface GasUsage { @@ -306,9 +315,9 @@ describe('Transfer tests', function(this: any) { const runTestTransaction = async ( txResult: TransactionResult, expectedGasUsed: number, - gasCurrency?: string + feeCurrency?: string ): Promise => { - const minGasPrice = await getGasPriceMinimum(gasCurrency) + const minGasPrice = await getGasPriceMinimum(feeCurrency) assert.isAbove(parseInt(minGasPrice, 10), 0) let ok = false @@ -320,22 +329,29 @@ describe('Transfer tests', function(this: any) { ok = false } + if (receipt != null && receipt.gasUsed !== expectedGasUsed) { + // tslint:disable-next-line: no-console + console.log('OOPSS: Different Gas', receipt.gasUsed, expectedGasUsed) + } + const gasVal = receipt ? receipt.gasUsed : expectedGasUsed assert.isAbove(gasVal, 0) const txHash = await txResult.getHash() const tx = await kit.web3.eth.getTransaction(txHash) - const gasPrice = tx.gasPrice - assert.isAbove(parseInt(gasPrice, 10), 0) - const txFee = new BigNumber(gasVal).times(gasPrice) + assert.isAbove(parseInt(tx.gasPrice, 10), 0) + const txFee = new BigNumber(gasVal).times(tx.gasPrice) const txFeeBase = new BigNumber(gasVal).times(minGasPrice) const txFeeTip = txFee.minus(txFeeBase) + const gatewayFee = new BigNumber(tx.gatewayFee || 0) + assert.equal(tx.gatewayFeeRecipient === null, gatewayFee.eq(0)) - const fees = { - total: txFee, + const fees: Fees = { + total: txFee.plus(gatewayFee), base: txFeeBase, tip: txFeeTip, + gateway: gatewayFee, } - const gas = { + const gas: GasUsage = { used: receipt && receipt.gasUsed, expected: expectedGasUsed, } @@ -355,14 +371,15 @@ describe('Transfer tests', function(this: any) { expectSuccess?: boolean txOptions?: { gas?: number - gasFeeRecipient?: string + gatewayFeeRecipient?: string + gatewayFee?: string } }) { let txRes: TestTxResults let balances: BalanceWatcher before(async () => { - const gasCurrency = + const feeCurrency = feeToken === CeloContract.StableToken ? await kit.registry.addressFor(CeloContract.StableToken) : undefined @@ -380,7 +397,7 @@ describe('Transfer tests', function(this: any) { transferToken === CeloContract.StableToken ? transferCeloDollars : transferCeloGold const txResult = await transferFn(FromAddress, ToAddress, TransferAmount, { ...txOptions, - gasCurrency, + feeCurrency, }) // Writing to an empty storage location (e.g. an uninitialized ERC20 account) costs 15k extra gas. @@ -391,7 +408,7 @@ describe('Transfer tests', function(this: any) { expectedGas += 15000 } - txRes = await runTestTransaction(txResult, expectedGas, gasCurrency) + txRes = await runTestTransaction(txResult, expectedGas, feeCurrency) await balances.update() }) @@ -406,7 +423,7 @@ describe('Transfer tests', function(this: any) { assertEqualBN(balances.delta(ToAddress, transferToken), TransferAmount)) if (transferToken === feeToken) { - it(`should decrement the sender's ${transferToken} balance by the transfer amount plus the gas fee`, () => { + it(`should decrement the sender's ${transferToken} balance by the transfer amount plus fees`, () => { const expectedBalanceChange = txRes.fees.total.plus(TransferAmount) assertEqualBN(balances.delta(FromAddress, transferToken).negated(), expectedBalanceChange) }) @@ -414,13 +431,13 @@ describe('Transfer tests', function(this: any) { it(`should decrement the sender's ${transferToken} balance by the transfer amount`, () => assertEqualBN(balances.delta(FromAddress, transferToken).negated(), TransferAmount)) - it(`should decrement the sender's ${feeToken} balance by the gas fee`, () => + it(`should decrement the sender's ${feeToken} balance by the total fees`, () => assertEqualBN(balances.delta(FromAddress, feeToken).negated(), txRes.fees.total)) } } else { it(`should fail`, () => assert.isFalse(txRes.ok)) - it(`should decrement the sender's ${feeToken} balance by the gas fee`, () => + it(`should decrement the sender's ${feeToken} balance by the total fees`, () => assertEqualBN(balances.delta(FromAddress, feeToken).negated(), txRes.fees.total)) it(`should not change the receiver's ${transferToken} balance`, () => { @@ -440,9 +457,8 @@ describe('Transfer tests', function(this: any) { } } - // TODO(nategraf): Replace gas fee recipient with gateway fee and adjust this check. - it.skip(`should increment the gas fee recipient's ${feeToken} balance by a portion of the gas fee`, () => - assertEqualBN(balances.delta(FeeRecipientAddress, feeToken), new BigNumber(0))) + it(`should increment the gateway fee recipient's ${feeToken} balance by the gateway fee`, () => + assertEqualBN(balances.delta(FeeRecipientAddress, feeToken), txRes.fees.gateway)) it(`should increment the infrastructure fund's ${feeToken} balance by the base portion of the gas fee`, () => assertEqualBN(balances.delta(governanceAddress, feeToken), txRes.fees.base)) @@ -463,94 +479,103 @@ describe('Transfer tests', function(this: any) { before(`start geth on sync: ${syncMode}`, () => startSyncNode(syncMode)) describe('Transfer CeloGold >', () => { - const GOLD_TRANSACTION_GAS_COST = 30005 - describe('with gasCurrency = CeloGold >', () => { + const GOLD_TRANSACTION_GAS_COST = 21000 + describe('with feeCurrency = CeloGold >', () => { if (syncMode === 'light' || syncMode === 'ultralight') { describe('when running in light/ultralight sync mode', () => { - describe('when not explicitly specifying a gas fee recipient', () => - testTransferToken({ - expectedGas: GOLD_TRANSACTION_GAS_COST, - transferToken: CeloContract.GoldToken, - feeToken: CeloContract.GoldToken, - })) - - describe('when explicitly specifying the gas fee recipient', () => { - describe("when using a peer's etherbase", () => - testTransferToken({ - expectedGas: GOLD_TRANSACTION_GAS_COST, - transferToken: CeloContract.GoldToken, - feeToken: CeloContract.GoldToken, - txOptions: { - gasFeeRecipient: FeeRecipientAddress, - }, - })) - - describe('when setting to an arbitrary address', () => { - it('should get rejected by the sending node before being added to the tx pool', async () => { - try { - const res = await transferCeloGold(FromAddress, ToAddress, TransferAmount, { - gasFeeRecipient: kit.web3.utils.randomHex(20), - }) - await res.waitReceipt() - } catch (error) { - assert.include( - error.toString(), - 'Returned error: no peer with etherbase found' - ) - } - }) + const recipient = (choice: string) => { + switch (choice) { + case 'peer': + return FeeRecipientAddress + case 'random': + return Web3.utils.randomHex(20) + default: + // unset + return undefined + } + } + const feeValue = (choice: string) => { + switch (choice) { + case 'sufficient': + return '0x10000' + case 'insufficient': + return '0x1' + default: + // unset + return undefined + } + } + for (const recipientChoice of ['peer', 'random', 'unset']) { + describe(`when the gateway fee recipient is ${recipientChoice}`, () => { + for (const feeValueChoice of ['sufficient', 'insufficient', 'unset']) { + describe(`when the gateway fee value is ${feeValueChoice}`, () => { + const txOptions = { + gatewayFeeRecipient: recipient(recipientChoice), + gatewayFee: feeValue(feeValueChoice), + } + if (recipientChoice === 'random' || feeValueChoice === 'insufficient') { + const errMsg = + recipientChoice === 'random' + ? 'no peer with etherbase found' + : 'gateway fee too low to broadcast to peers' + it('should get rejected by the sending node before being added to the tx pool', async () => { + try { + const res = await transferCeloGold( + FromAddress, + ToAddress, + TransferAmount, + txOptions + ) + await res.waitReceipt() + assert.fail('no error was thrown') + } catch (error) { + assert.include(error.toString(), `Returned error: ${errMsg}`) + } + }) + } else { + testTransferToken({ + expectedGas: GOLD_TRANSACTION_GAS_COST, + transferToken: CeloContract.GoldToken, + feeToken: CeloContract.GoldToken, + txOptions, + }) + } + }) + } }) - }) + } }) } else { testTransferToken({ expectedGas: GOLD_TRANSACTION_GAS_COST, transferToken: CeloContract.GoldToken, feeToken: CeloContract.GoldToken, - txOptions: { - gasFeeRecipient: FeeRecipientAddress, - }, }) } }) - describe('gasCurrency = CeloDollars >', () => { - const intrinsicGas = 155000 + describe('feeCurrency = CeloDollars >', () => { + const intrinsicGas = INTRINSIC_GAS_FOR_TX + ADDITIONAL_INTRINSIC_TX_GAS_COST + describe('when there is no demurrage', () => { describe('when setting a gas amount greater than the amount of gas necessary', () => testTransferToken({ - expectedGas: 164005, + expectedGas: intrinsicGas, transferToken: CeloContract.GoldToken, feeToken: CeloContract.StableToken, - txOptions: { - gasFeeRecipient: FeeRecipientAddress, - }, })) - describe('when setting a gas amount less than the amount of gas necessary but more than the intrinsic gas amount', () => { - const gas = intrinsicGas + 1000 - testTransferToken({ - expectedGas: gas, - transferToken: CeloContract.GoldToken, - feeToken: CeloContract.StableToken, - expectSuccess: false, - txOptions: { - gas, - gasFeeRecipient: FeeRecipientAddress, - }, - }) - }) - describe('when setting a gas amount less than the intrinsic gas amount', () => { it('should not add the transaction to the pool', async () => { const gas = intrinsicGas - 1 - const gasCurrency = await kit.registry.addressFor(CeloContract.StableToken) + const feeCurrency = await kit.registry.addressFor(CeloContract.StableToken) try { const res = await transferCeloGold(FromAddress, ToAddress, TransferAmount, { gas, - gasCurrency, + feeCurrency, }) await res.getHash() + assert.fail('no error was thrown') } catch (error) { assert.include(error.toString(), 'Returned error: intrinsic gas too low') } @@ -561,25 +586,19 @@ describe('Transfer tests', function(this: any) { }) describe('Transfer CeloDollars', () => { - describe('gasCurrency = CeloDollars >', () => { + describe('feeCurrency = CeloDollars >', () => { testTransferToken({ - expectedGas: 175303, + expectedGas: 207303, transferToken: CeloContract.StableToken, feeToken: CeloContract.StableToken, - txOptions: { - gasFeeRecipient: FeeRecipientAddress, - }, }) }) - describe('gasCurrency = CeloGold >', () => { + describe('feeCurrency = CeloGold >', () => { testTransferToken({ expectedGas: 41303, transferToken: CeloContract.StableToken, feeToken: CeloContract.GoldToken, - txOptions: { - gasFeeRecipient: FeeRecipientAddress, - }, }) }) }) @@ -588,7 +607,7 @@ describe('Transfer tests', function(this: any) { }) describe('Transfer with changed intrinsic gas cost >', () => { - const intrinsicGasForAlternativeGasCurrency = 34000 + const changedIntrinsicGasForAlternativeFeeCurrency = 34000 before(restartWithCleanNodes) @@ -597,50 +616,38 @@ describe('Transfer tests', function(this: any) { before(`start geth on sync: ${syncMode}`, async () => { try { await startSyncNode(syncMode) - await setIntrinsicGas('http://localhost:8545', validatorAddress, 34000) + await setIntrinsicGas( + 'http://localhost:8545', + validatorAddress, + changedIntrinsicGasForAlternativeFeeCurrency + ) } catch (err) { console.debug('some error', err) } }) describe('Transfer CeloGold >', () => { - describe('gasCurrency = CeloDollars >', () => { - const intrinsicGas = intrinsicGasForAlternativeGasCurrency + 21000 + describe('feeCurrency = CeloDollars >', () => { + const intrinsicGas = changedIntrinsicGasForAlternativeFeeCurrency + INTRINSIC_GAS_FOR_TX describe('when there is no demurrage', () => { describe('when setting a gas amount greater than the amount of gas necessary', () => testTransferToken({ - expectedGas: 64005, + expectedGas: intrinsicGas, transferToken: CeloContract.GoldToken, feeToken: CeloContract.StableToken, - txOptions: { - gasFeeRecipient: FeeRecipientAddress, - }, })) - describe('when setting a gas amount less than the amount of gas necessary but more than the intrinsic gas amount', () => { - const gas = intrinsicGas + 1000 - testTransferToken({ - expectedGas: gas, - transferToken: CeloContract.GoldToken, - feeToken: CeloContract.StableToken, - expectSuccess: false, - txOptions: { - gas, - gasFeeRecipient: FeeRecipientAddress, - }, - }) - }) - describe('when setting a gas amount less than the intrinsic gas amount', () => { it('should not add the transaction to the pool', async () => { const gas = intrinsicGas - 1 - const gasCurrency = await kit.registry.addressFor(CeloContract.StableToken) + const feeCurrency = await kit.registry.addressFor(CeloContract.StableToken) try { const res = await transferCeloGold(FromAddress, ToAddress, TransferAmount, { gas, - gasCurrency, + feeCurrency, }) await res.getHash() + assert.fail('no error was thrown') } catch (error) { assert.include(error.toString(), 'Returned error: intrinsic gas too low') } @@ -651,14 +658,11 @@ describe('Transfer tests', function(this: any) { }) describe('Transfer CeloDollars', () => { - describe('gasCurrency = CeloDollars >', () => { + describe('feeCurrency = CeloDollars >', () => { testTransferToken({ expectedGas: 75303, transferToken: CeloContract.StableToken, feeToken: CeloContract.StableToken, - txOptions: { - gasFeeRecipient: FeeRecipientAddress, - }, }) }) }) @@ -693,13 +697,11 @@ describe('Transfer tests', function(this: any) { await inflationManager.setInflationRateForNextTransfer(new BigNumber(2)) const stableTokenAddress = await kit.registry.addressFor(CeloContract.StableToken) - const expectedGasUsed = 164005 txRes = await runTestTransaction( await transferCeloGold(FromAddress, ToAddress, TransferAmount, { - gasCurrency: stableTokenAddress, - gasFeeRecipient: FeeRecipientAddress, + feeCurrency: stableTokenAddress, }), - expectedGasUsed, + 187000, stableTokenAddress ) @@ -723,7 +725,7 @@ describe('Transfer tests', function(this: any) { assertEqualBN(balances.delta(ToAddress, CeloContract.GoldToken), TransferAmount) }) - it("should halve the sender's Celo Dollar balance due to demurrage and decrement it by the gas fee", () => { + it("should halve the sender's Celo Dollar balance due to demurrage and decrement it by the total fees", () => { assertEqualBN( balances .initial(FromAddress, CeloContract.StableToken) @@ -733,87 +735,16 @@ describe('Transfer tests', function(this: any) { ) }) - // TODO(nategraf): Replace gas fee recipient with gateway fee and adjust this check. - it.skip("should increment the fee receipient's Celo Dollar balance by a portion of the gas fee", () => { + it("should halve the gateway fee recipient's Celo Dollar balance then increase it by the gateway fee", () => { assertEqualBN( balances .current(FeeRecipientAddress, CeloContract.StableToken) .minus(balances.initial(FeeRecipientAddress, CeloContract.StableToken).idiv(2)), - new BigNumber(0) - ) - }) - - it("should halve the infrastructure fund's Celo Dollar balance then increment it by the base portion of the gas fee", () => { - assertEqualBN( - balances - .current(governanceAddress, CeloContract.StableToken) - .minus(balances.initial(governanceAddress, CeloContract.StableToken).idiv(2)), - expectedFees.base - ) - }) - }) - - describe('when setting a gas amount less than the amount of gas necessary but more than the intrinsic gas amount', () => { - let balances: BalanceWatcher - let expectedFees: Fees - let txRes: TestTxResults - - before(async () => { - balances = await newBalanceWatcher(kit, [ - FromAddress, - ToAddress, - validatorAddress, - FeeRecipientAddress, - governanceAddress, - ]) - - await inflationManager.setInflationRateForNextTransfer(new BigNumber(2)) - - const intrinsicGas = 155000 - const gas = intrinsicGas + 1000 - txRes = await runTestTransaction( - await transferCeloGold(FromAddress, ToAddress, TransferAmount, { - gas, - gasCurrency: await kit.registry.addressFor(CeloContract.StableToken), - gasFeeRecipient: FeeRecipientAddress, - }), - gas, - await kit.registry.addressFor(CeloContract.StableToken) + expectedFees.gateway ) - - await balances.update() - expectedFees = txRes.fees }) - it('should fail', () => assert.isFalse(txRes.ok)) - - it("should not change the sender's Celo Gold balance", () => { - assertEqualBN(balances.delta(FromAddress, CeloContract.GoldToken), new BigNumber(0)) - }) - - it("should not change the receiver's Celo Gold balance", () => { - assertEqualBN(balances.delta(ToAddress, CeloContract.GoldToken), new BigNumber(0)) - }) - - it("should halve the sender's Celo Dollar balance due to demurrage and decrement it by the gas fee", () => { - assertEqualBN( - balances - .initial(FromAddress, CeloContract.StableToken) - .idiv(2) - .minus(balances.current(FromAddress, CeloContract.StableToken)), - expectedFees.total - ) - }) - - // TODO(nategraf): Replace gas fee recipient with gateway fee and adjust this check. - it.skip("should increment the fee recipient's Celo Dollar balance by a portion of the gas fee", () => { - assertEqualBN( - balances.delta(FeeRecipientAddress, CeloContract.StableToken), - new BigNumber(0) - ) - }) - - it(`should halve the infrastructure fund's Celo Dollar balance then increment it by the base portion of the gas fee`, () => { + it("should halve the infrastructure fund's Celo Dollar balance then increment it by the base portion of the gas fees", () => { assertEqualBN( balances .current(governanceAddress, CeloContract.StableToken) @@ -821,15 +752,6 @@ describe('Transfer tests', function(this: any) { expectedFees.base ) }) - - it('should halve the proposers Celo Dollar balance the increment it by the rest of the gas fee', () => { - assertEqualBN( - balances - .current(validatorAddress, CeloContract.StableToken) - .minus(balances.initial(validatorAddress, CeloContract.StableToken).idiv(2)), - expectedFees.tip - ) - }) }) }) }) diff --git a/packages/celotool/src/e2e-tests/utils.ts b/packages/celotool/src/e2e-tests/utils.ts index 98f669af9e8..05efcd8e954 100644 --- a/packages/celotool/src/e2e-tests/utils.ts +++ b/packages/celotool/src/e2e-tests/utils.ts @@ -2,6 +2,7 @@ import BigNumber from 'bignumber.js' import { assert } from 'chai' import { spawn, SpawnOptions } from 'child_process' import fs from 'fs' +import _ from 'lodash' import { join as joinPath, resolve as resolvePath } from 'path' import { Admin } from 'web3-eth-admin' import { @@ -34,6 +35,7 @@ export interface GethTestConfig { migrateTo?: number instances: GethInstanceConfig[] genesisConfig?: any + migrationOverrides?: any } const TEST_DIR = '/tmp/e2e' @@ -41,6 +43,12 @@ const GENESIS_PATH = `${TEST_DIR}/genesis.json` const NetworkId = 1101 const MonorepoRoot = resolvePath(joinPath(__dirname, '../..', '../..')) +export async function waitToFinishSyncing(web3: any) { + while ((await web3.eth.isSyncing()) || (await web3.eth.getBlockNumber()) === 0) { + await sleep(0.1) + } +} + export function assertAlmostEqual( actual: BigNumber, expected: BigNumber, @@ -99,6 +107,7 @@ export async function execCmdWithExitOnFailure( ) { const code = await execCmd(cmd, args, options) if (code !== 0) { + console.error('execCmd failed for: ' + [cmd].concat(args).join(' ')) process.exit(1) } } @@ -194,6 +203,7 @@ function writeGenesis(validators: Validator[], path: string, configOverrides: an validators, blockTime: 0, epoch: 10, + lookback: 2, requestTimeout: 3000, chainId: NetworkId, ...configOverrides, @@ -213,7 +223,7 @@ export async function init(gethBinaryPath: string, datadir: string, genesisPath: } export async function importPrivateKey(gethBinaryPath: string, instance: GethInstanceConfig) { - const keyFile = '/tmp/key.txt' + const keyFile = `/${getDatadir(instance)}/key.txt` fs.writeFileSync(keyFile, instance.privateKey) console.info(`geth:${instance.name}: import account`) await execCmdWithExitOnFailure( @@ -316,9 +326,13 @@ export async function startGeth(gethBinaryPath: string, instance: GethInstanceCo } if (validating) { - gethArgs.push('--password=/dev/null', `--unlock=0`) gethArgs.push('--mine', '--minerthreads=10', `--nodekeyhex=${privateKey}`) } + + if (privateKey) { + gethArgs.push('--password=/dev/null', `--unlock=0`) + } + const gethProcess = spawnWithLog(gethBinaryPath, gethArgs, `${datadir}/logs.txt`) instance.pid = gethProcess.pid @@ -338,23 +352,30 @@ export async function startGeth(gethBinaryPath: string, instance: GethInstanceCo export async function migrateContracts( validatorPrivateKeys: string[], + attestationKeys: string[], validators: string[], - to: number = 1000 + to: number = 1000, + overrides: any = {} ) { - const migrationOverrides = { - validators: { - validatorKeys: validatorPrivateKeys.map(ensure0x), - }, - election: { - minElectableValidators: '1', - }, - stableToken: { - initialBalances: { - addresses: validators.map(ensure0x), - values: validators.map(() => '10000000000000000000000'), + const migrationOverrides = _.merge( + { + election: { + minElectableValidators: '1', + }, + stableToken: { + initialBalances: { + addresses: validators.map(ensure0x), + values: validators.map(() => '10000000000000000000000'), + }, + }, + validators: { + validatorKeys: validatorPrivateKeys.map(ensure0x), + attestationKeys: attestationKeys.map(ensure0x), }, }, - } + overrides + ) + const args = [ '--cwd', `${MonorepoRoot}/packages/protocol`, @@ -423,6 +444,7 @@ export function getContext(gethConfig: GethTestConfig) { const validatorInstances = gethConfig.instances.filter((x: any) => x.validating) const numValidators = validatorInstances.length const validatorPrivateKeys = getPrivateKeysFor(AccountType.VALIDATOR, mnemonic, numValidators) + const attestationKeys = getPrivateKeysFor(AccountType.ATTESTATION, mnemonic, numValidators) const validators = getValidators(mnemonic, numValidators) const validatorEnodes = validatorPrivateKeys.map((x: any, i: number) => getEnodeAddress(privateKeyToPublicKey(x), '127.0.0.1', validatorInstances[i].port) @@ -444,7 +466,7 @@ export function getContext(gethConfig: GethTestConfig) { if (instance.validating) { // Automatically connect validator nodes to eachother. const otherValidators = validatorEnodes.filter( - (_: string, i: number) => i !== validatorIndex + (__: string, i: number) => i !== validatorIndex ) instance.peers = (instance.peers || []).concat(otherValidators) instance.privateKey = instance.privateKey || validatorPrivateKeys[validatorIndex] @@ -455,8 +477,10 @@ export function getContext(gethConfig: GethTestConfig) { if (gethConfig.migrate || gethConfig.migrateTo) { await migrateContracts( validatorPrivateKeys, + attestationKeys, validators.map((x) => x.address), - gethConfig.migrateTo + gethConfig.migrateTo, + gethConfig.migrationOverrides ) } await killGeth() @@ -471,16 +495,22 @@ export function getContext(gethConfig: GethTestConfig) { const restart = async () => { await killGeth() let validatorIndex = 0 + const validatorIndices: number[] = [] for (const instance of gethConfig.instances) { - await restoreDatadir(instance) - if (!instance.privateKey && instance.validating) { - instance.privateKey = validatorPrivateKeys[validatorIndex] - } - await startGeth(gethBinaryPath, instance) + validatorIndices.push(validatorIndex) if (instance.validating) { validatorIndex++ } } + await Promise.all( + gethConfig.instances.map(async (instance, i) => { + await restoreDatadir(instance) + if (!instance.privateKey && instance.validating) { + instance.privateKey = validatorPrivateKeys[validatorIndices[i]] + } + return startGeth(gethBinaryPath, instance) + }) + ) } const after = () => killGeth() diff --git a/packages/celotool/src/e2e-tests/validator_order_tests.ts b/packages/celotool/src/e2e-tests/validator_order_tests.ts index 8051b43cecf..bd688892964 100644 --- a/packages/celotool/src/e2e-tests/validator_order_tests.ts +++ b/packages/celotool/src/e2e-tests/validator_order_tests.ts @@ -42,6 +42,9 @@ describe('governance tests', () => { it('properly orders validators randomly', async function(this: any) { this.timeout(160000 /* 160 seconds */) + // If a consensus round fails during this test, the results are inconclusive. + // Retry up to two times to mitigate this issue. Restarting the nodes is not needed. + this.retries(2) const latestBlockNumber = (await web3.eth.getBlock('latest')).number const indexInEpoch = ((latestBlockNumber % EPOCH) + EPOCH - 1) % EPOCH diff --git a/packages/celotool/src/lib/env-utils.ts b/packages/celotool/src/lib/env-utils.ts index 2f74c270e6d..98b5e2afa52 100644 --- a/packages/celotool/src/lib/env-utils.ts +++ b/packages/celotool/src/lib/env-utils.ts @@ -29,9 +29,14 @@ export enum envVar { CLUSTER_DOMAIN_NAME = 'CLUSTER_DOMAIN_NAME', ENV_TYPE = 'ENV_TYPE', EPOCH = 'EPOCH', + LOOKBACK = 'LOOKBACK', ETHSTATS_DOCKER_IMAGE_REPOSITORY = 'ETHSTATS_DOCKER_IMAGE_REPOSITORY', ETHSTATS_DOCKER_IMAGE_TAG = 'ETHSTATS_DOCKER_IMAGE_TAG', - ETHSTATS_WEBSOCKETSECRET = 'ETHSTATS_WEBSOCKETSECRET', + ETHSTATS_TRUSTED_ADDRESSES = 'ETHSTATS_TRUSTED_ADDRESSES', + ETHSTATS_BANNED_ADDRESSES = 'ETHSTATS_BANNED_ADDRESSES', + FAUCET_GENESIS_ACCOUNTS = 'FAUCET_GENESIS_ACCOUNTS', + FAUCET_GENESIS_BALANCE = 'FAUCET_GENESIS_BALANCE', + ORACLE_GENESIS_BALANCE = 'ORACLE_GENESIS_BALANCE', 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', @@ -69,8 +74,9 @@ export enum envVar { TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_REPOSITORY = 'TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_REPOSITORY', TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG = 'TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG', TX_NODES = 'TX_NODES', + VALIDATOR_GENESIS_BALANCE = 'VALIDATOR_GENESIS_BALANCE', + VALIDATOR_ZERO_GENESIS_BALANCE = 'VALIDATOR_ZERO_GENESIS_BALANCE', VALIDATORS = 'VALIDATORS', - VERIFICATION_POOL_URL = 'VERIFICATION_POOL_URL', VM_BASED = 'VM_BASED', } diff --git a/packages/celotool/src/lib/ethstats.ts b/packages/celotool/src/lib/ethstats.ts index 370576dd6dd..21fd416b298 100644 --- a/packages/celotool/src/lib/ethstats.ts +++ b/packages/celotool/src/lib/ethstats.ts @@ -1,6 +1,7 @@ import { installGenericHelmChart, removeGenericHelmChart } from 'src/lib/helm_deploy' import { execCmdWithExitOnFailure } from 'src/lib/utils' -import { envVar, fetchEnv, isVmBased } from './env-utils' +import { envVar, fetchEnv, fetchEnvOrFallback } from './env-utils' +import { AccountType, getAddressesFor } from './generate_utils' const helmChartPath = '../helm-charts/ethstats' @@ -29,13 +30,26 @@ export async function upgradeHelmChart(celoEnv: string) { function helmParameters() { return [ `--set domain.name=${fetchEnv(envVar.CLUSTER_DOMAIN_NAME)}`, - `--set ethstats.createSecret=${isVmBased()}`, - `--set ethstats.webSocketSecret="${fetchEnv(envVar.ETHSTATS_WEBSOCKETSECRET)}"`, `--set ethstats.image.repository=${fetchEnv(envVar.ETHSTATS_DOCKER_IMAGE_REPOSITORY)}`, `--set ethstats.image.tag=${fetchEnv(envVar.ETHSTATS_DOCKER_IMAGE_TAG)}`, + `--set ethstats.trusted_addresses='{${generateAuthorizedAddresses()}}'`, + `--set ethstats.banned_addresses='{${fetchEnv(envVar.ETHSTATS_BANNED_ADDRESSES)}}'`, ] } function releaseName(celoEnv: string) { return `${celoEnv}-ethstats` } + +function generateAuthorizedAddresses() { + // TODO: Add the Proxy eth addresses when available + const mnemonic = fetchEnv(envVar.MNEMONIC) + const publicKeys = [] + const txNodes = parseInt(fetchEnv(envVar.TX_NODES), 0) + const validatorNodes = parseInt(fetchEnv(envVar.VALIDATORS), 0) + publicKeys.push(getAddressesFor(AccountType.TX_NODE, mnemonic, txNodes)) + publicKeys.push(getAddressesFor(AccountType.VALIDATOR, mnemonic, validatorNodes)) + + publicKeys.push(fetchEnvOrFallback(envVar.ETHSTATS_TRUSTED_ADDRESSES, '').split(',')) + return publicKeys.reduce((accumulator, value) => accumulator.concat(value), []) +} diff --git a/packages/celotool/src/lib/generate_utils.ts b/packages/celotool/src/lib/generate_utils.ts index 7acecab6357..2633757b842 100644 --- a/packages/celotool/src/lib/generate_utils.ts +++ b/packages/celotool/src/lib/generate_utils.ts @@ -4,14 +4,13 @@ import { ec as EC } from 'elliptic' import fs from 'fs' import { range, repeat } from 'lodash' import path from 'path' -import rlp from 'rlp' +import * as rlp from 'rlp' import Web3 from 'web3' import { envVar, fetchEnv, fetchEnvOrFallback, monorepoRoot } from './env-utils' import { CONTRACT_OWNER_STORAGE_LOCATION, GETH_CONFIG_OLD, ISTANBUL_MIX_HASH, - OG_ACCOUNTS, REGISTRY_ADDRESS, TEMPLATE, } from './genesis_constants' @@ -28,6 +27,7 @@ export enum AccountType { BOOTNODE = 3, FAUCET = 4, ATTESTATION = 5, + PRICE_ORACLE = 6, } export enum ConsensusType { @@ -38,6 +38,11 @@ export enum ConsensusType { export interface Validator { address: string blsPublicKey: string + balance: string +} +export interface AccountAndBalance { + address: string + balance: string } export const MNEMONIC_ACCOUNT_TYPE_CHOICES = [ @@ -47,6 +52,7 @@ export const MNEMONIC_ACCOUNT_TYPE_CHOICES = [ 'bootnode', 'faucet', 'attestation', + 'price_oracle', ] export const add0x = (str: string) => { @@ -83,8 +89,15 @@ export const privateKeyToAddress = (privateKey: string) => { export const privateKeyToStrippedAddress = (privateKey: string) => strip0x(privateKeyToAddress(privateKey)) -const DEFAULT_BALANCE = '1000000000000000000000000' -const VALIDATOR_OG_SOURCE = 'og' +const validatorZeroBalance = fetchEnvOrFallback( + envVar.VALIDATOR_ZERO_GENESIS_BALANCE, + '103010030000000000000000000' +) // 103,010,030 CG +const validatorBalance = fetchEnvOrFallback( + envVar.VALIDATOR_GENESIS_BALANCE, + '10011000000000000000000' +) // 10,011 CG +const faucetBalance = fetchEnvOrFallback(envVar.FAUCET_GENESIS_BALANCE, '10011000000000000000000') // 10,000 CG export const getPrivateKeysFor = (accountType: AccountType, mnemonic: string, n: number) => range(0, n).map((i) => generatePrivateKey(mnemonic, accountType, i)) @@ -96,11 +109,12 @@ export const getStrippedAddressesFor = (accountType: AccountType, mnemonic: stri getAddressesFor(accountType, mnemonic, n).map(strip0x) export const getValidators = (mnemonic: string, n: number) => { - return getPrivateKeysFor(AccountType.VALIDATOR, mnemonic, n).map((key) => { + return getPrivateKeysFor(AccountType.VALIDATOR, mnemonic, n).map((key, i) => { const blsKeyBytes = blsPrivateKeyToProcessedPrivateKey(key) return { address: strip0x(privateKeyToAddress(key)), blsPublicKey: bls12377js.BLS.privateToPublicBytes(blsKeyBytes).toString('hex'), + balance: i === 0 ? validatorZeroBalance : validatorBalance, } }) } @@ -114,16 +128,7 @@ export const getAddressFromEnv = (accountType: AccountType, n: number) => { export const generateGenesisFromEnv = (enablePetersburg: boolean = true) => { const mnemonic = fetchEnv(envVar.MNEMONIC) const validatorEnv = fetchEnv(envVar.VALIDATORS) - const validators = - validatorEnv === VALIDATOR_OG_SOURCE - ? OG_ACCOUNTS.map((account) => { - const blsKeyBytes = blsPrivateKeyToProcessedPrivateKey(account.privateKey) - return { - address: account.address, - blsPublicKey: bls12377js.BLS.privateToPublicBytes(blsKeyBytes).toString('hex'), - } - }) - : getValidators(mnemonic, parseInt(validatorEnv, 10)) + const validators = getValidators(mnemonic, parseInt(validatorEnv, 10)) const consensusType = fetchEnv(envVar.CONSENSUS_TYPE) as ConsensusType @@ -138,17 +143,40 @@ export const generateGenesisFromEnv = (enablePetersburg: boolean = true) => { 10 ) const epoch = parseInt(fetchEnvOrFallback(envVar.EPOCH, '30000'), 10) + // allow 12 blocks in prod for the uptime metric + const lookbackwindow = parseInt(fetchEnvOrFallback(envVar.LOOKBACK, '12'), 10) const chainId = parseInt(fetchEnv(envVar.NETWORK_ID), 10) - // Assing DEFAULT ammount of gold to 2 faucet accounts - const faucetAddresses = getStrippedAddressesFor(AccountType.FAUCET, mnemonic, 2) + // Allocate faucet accounts + const numFaucetAccounts = parseInt(fetchEnvOrFallback(envVar.FAUCET_GENESIS_ACCOUNTS, '0'), 10) + const initialAccounts = getStrippedAddressesFor( + AccountType.FAUCET, + mnemonic, + numFaucetAccounts + ).map((addr) => { + return { + address: addr, + balance: fetchEnvOrFallback(envVar.FAUCET_GENESIS_BALANCE, faucetBalance), + } + }) + + // Allocate oracle account(s) + initialAccounts.concat( + getStrippedAddressesFor(AccountType.PRICE_ORACLE, mnemonic, 1).map((addr) => { + return { + address: addr, + balance: fetchEnvOrFallback(envVar.ORACLE_GENESIS_BALANCE, '100000000000000000000'), + } + }) + ) return generateGenesis({ validators, consensusType, blockTime, - initialAccounts: faucetAddresses, + initialAccounts, epoch, + lookbackwindow, chainId, requestTimeout, enablePetersburg, @@ -157,23 +185,38 @@ export const generateGenesisFromEnv = (enablePetersburg: boolean = true) => { const generateIstanbulExtraData = (validators: Validator[]) => { const istanbulVanity = 32 - const blsSignatureVanity = 192 - + const blsSignatureVanity = 96 + const ecdsaSignatureVanity = 65 return ( '0x' + repeat('0', istanbulVanity * 2) + rlp - // @ts-ignore .encode([ + // Added validators validators.map((validator) => Buffer.from(validator.address, 'hex')), validators.map((validator) => Buffer.from(validator.blsPublicKey, 'hex')), + // Removed validators new Buffer(0), - Buffer.from(repeat('0', blsSignatureVanity * 2), 'hex'), - new Buffer(0), - Buffer.from(repeat('0', blsSignatureVanity * 2), 'hex'), - new Buffer(0), + // Seal + Buffer.from(repeat('0', ecdsaSignatureVanity * 2), 'hex'), + [ + // AggregatedSeal.Bitmap + new Buffer(0), + // AggregatedSeal.Signature + Buffer.from(repeat('0', blsSignatureVanity * 2), 'hex'), + // AggregatedSeal.Round + new Buffer(0), + ], + [ + // ParentAggregatedSeal.Bitmap + new Buffer(0), + // ParentAggregatedSeal.Signature + Buffer.from(repeat('0', blsSignatureVanity * 2), 'hex'), + // ParentAggregatedSeal.Round + new Buffer(0), + ], + // EpochData new Buffer(0), - Buffer.from(repeat('0', blsSignatureVanity * 2), 'hex'), ]) .toString('hex') ) @@ -185,15 +228,17 @@ export const generateGenesis = ({ initialAccounts: otherAccounts = [], blockTime, epoch, + lookbackwindow, chainId, requestTimeout, enablePetersburg = true, }: { validators: Validator[] consensusType?: ConsensusType - initialAccounts?: string[] + initialAccounts?: AccountAndBalance[] blockTime: number epoch: number + lookbackwindow: number chainId: number requestTimeout: number enablePetersburg?: boolean @@ -222,18 +267,19 @@ export const generateGenesis = ({ period: blockTime, requesttimeout: requestTimeout, epoch, + lookbackwindow, } } for (const validator of validators) { genesis.alloc[validator.address] = { - balance: DEFAULT_BALANCE, + balance: validator.balance, } } - for (const address of otherAccounts) { - genesis.alloc[address] = { - balance: DEFAULT_BALANCE, + for (const account of otherAccounts) { + genesis.alloc[account.address] = { + balance: account.balance, } } diff --git a/packages/celotool/src/lib/genesis_constants.ts b/packages/celotool/src/lib/genesis_constants.ts index 351bd10aa3e..711eb78cb12 100644 --- a/packages/celotool/src/lib/genesis_constants.ts +++ b/packages/celotool/src/lib/genesis_constants.ts @@ -39,26 +39,3 @@ export const CONTRACT_OWNER_STORAGE_LOCATION = '0x34dc5a2556b2030988481969696f29fed38d45813d8003f6c70e5c16ac92ae0f' export const ISTANBUL_MIX_HASH = '0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365' - -export const OG_ACCOUNTS = [ - { - name: 'gethminer1', - privateKey: 'a2d2c843bb2c6b6aed146343b8aec7d23a7cc050f41c6217760d46095bfc49cd', - address: 'feE1a22F43BeeCB912B5a4912ba87527682ef0fC', - }, - { - name: 'gethminer2', - privateKey: '898a61ff4d42360802c9897bc2df1298d2df9153cb761ca55b5dc1bb940f44dc', - address: '889F21CE69dcc25a4594f73230A55896d6703806', - }, - { - name: 'gethminer3', - privateKey: '6005018fe530da09942a016921a185cabef0fbcc10e63fe2b45805b2957f6ec9', - address: '5372d2bbBaBaAf1495182E31cF13dB0d18463B0E', - }, - { - name: 'gethminer4', - privateKey: '1310f2a9c32d52dbbabc28dbaa9ca4c5c826d59664320b597effa66155c72c61', - address: 'F71690ea7E0c67827d8968882FAC0c4cBBD65BCE', - }, -] diff --git a/packages/celotool/src/lib/geth.ts b/packages/celotool/src/lib/geth.ts index 16e645d163b..275fa67d97c 100644 --- a/packages/celotool/src/lib/geth.ts +++ b/packages/celotool/src/lib/geth.ts @@ -318,11 +318,11 @@ const transferAndTrace = async ( console.info('Transfer') const token = getRandomlyChoseToken(goldToken, stableToken) - const gasCurrencyToken = getRandomlyChoseToken(goldToken, stableToken) + const feeCurrencyToken = getRandomlyChoseToken(goldToken, stableToken) - const [tokenName, gasCurrencySymbol] = await Promise.all([ + const [tokenName, feeCurrencySymbol] = await Promise.all([ token.methods.symbol().call(), - gasCurrencyToken.methods.symbol().call(), + feeCurrencyToken.methods.symbol().call(), ]) const logMessage: any = { @@ -339,8 +339,8 @@ const transferAndTrace = async ( const txParams: any = {} // Fill txParams below if (getRandomInt(0, 2) === 3) { - txParams.gasCurrency = gasCurrencyToken._address - logMessage.gasCurrency = gasCurrencySymbol + txParams.feeCurrency = feeCurrencyToken._address + logMessage.feeCurrency = feeCurrencySymbol } const transferToken = new Promise(async (resolve) => { @@ -490,11 +490,11 @@ export const simulateClient = async ( try { const token = getRandomlyChoseToken(goldToken, stableToken) - const gasCurrencyToken = getRandomlyChoseToken(goldToken, stableToken) + const feeCurrencyToken = getRandomlyChoseToken(goldToken, stableToken) const [tokenSymbol] = await Promise.all([ token.methods.symbol().call(), - gasCurrencyToken.methods.symbol().call(), + feeCurrencyToken.methods.symbol().call(), ]) const txParams: any = {} diff --git a/packages/celotool/src/lib/helm_deploy.ts b/packages/celotool/src/lib/helm_deploy.ts index ff180d03774..549b9f43c3f 100644 --- a/packages/celotool/src/lib/helm_deploy.ts +++ b/packages/celotool/src/lib/helm_deploy.ts @@ -1,19 +1,11 @@ -import { entries, flatMap, range } from 'lodash' +import { entries, range } from 'lodash' import sleep from 'sleep-promise' import { getKubernetesClusterRegion, switchToClusterFromEnv } from './cluster' import { EnvTypes, envVar, fetchEnv, fetchEnvOrFallback, isProduction } from './env-utils' import { ensureAuthenticatedGcloudAccount } from './gcloud_utils' import { generateGenesisFromEnv } from './generate_utils' -import { OG_ACCOUNTS } from './genesis_constants' import { getStatefulSetReplicas, scaleResource } from './kubernetes' -import { - execCmd, - execCmdWithExitOnFailure, - getVerificationPoolRewardsURL, - getVerificationPoolSMSURL, - outputIncludes, - switchToProjectFromEnv, -} from './utils' +import { execCmd, execCmdWithExitOnFailure, outputIncludes, switchToProjectFromEnv } from './utils' const CLOUDSQL_SECRET_NAME = 'blockscout-cloudsql-credentials' const BACKUP_GCS_SECRET_NAME = 'backup-blockchain-credentials' @@ -514,18 +506,8 @@ async function helmParameters(celoEnv: string) { ] : [] - const gethAccountParameters = flatMap(OG_ACCOUNTS, (account) => [ - `--set geth.account.${account.name}.name=${account.name}`, - `--set geth.account.${account.name}.privateKey=${account.privateKey}`, - `--set geth.account.${account.name}.address=${account.address}`, - ]) - return [ `--set domain.name=${fetchEnv('CLUSTER_DOMAIN_NAME')}`, - `--set geth.miner.verificationpool=${fetchEnvOrFallback( - 'VERIFICATION_POOL_URL', - getVerificationPoolSMSURL(celoEnv) - )}`, `--set geth.verbosity=${fetchEnvOrFallback('GETH_VERBOSITY', '4')}`, `--set geth.node.cpu_request=${fetchEnv('GETH_NODE_CPU_REQUEST')}`, `--set geth.node.memory_request=${fetchEnv('GETH_NODE_MEMORY_REQUEST')}`, @@ -540,10 +522,6 @@ async function helmParameters(celoEnv: string) { `--set cluster.name=${fetchEnv('KUBERNETES_CLUSTER_NAME')}`, `--set bucket=${bucketName}`, `--set project.name=${fetchEnv('TESTNET_PROJECT_NAME')}`, - `--set verification.rewardsUrl=${fetchEnvOrFallback( - 'VERIFICATION_REWARDS_URL', - getVerificationPoolRewardsURL(celoEnv) - )}`, `--set celotool.image.repository=${fetchEnv('CELOTOOL_DOCKER_IMAGE_REPOSITORY')}`, `--set celotool.image.tag=${fetchEnv('CELOTOOL_DOCKER_IMAGE_TAG')}`, `--set promtosd.scrape_interval=${fetchEnv('PROMTOSD_SCRAPE_INTERVAL')}`, @@ -562,7 +540,6 @@ async function helmParameters(celoEnv: string) { `--set mnemonic="${fetchEnv('MNEMONIC')}"`, `--set contracts.cron_jobs.enabled=${fetchEnv('CONTRACT_CRONJOBS_ENABLED')}`, `--set geth.account.secret="${fetchEnv('GETH_ACCOUNT_SECRET')}"`, - `--set ethstats.webSocketSecret="${fetchEnv('ETHSTATS_WEBSOCKETSECRET')}"`, `--set geth.ping_ip_from_packet=${fetchEnvOrFallback('PING_IP_FROM_PACKET', 'false')}`, `--set geth.in_memory_discovery_table=${fetchEnvOrFallback( 'IN_MEMORY_DISCOVERY_TABLE', @@ -570,7 +547,6 @@ async function helmParameters(celoEnv: string) { )}`, ...productionTagOverrides, ...(await helmIPParameters(celoEnv)), - ...gethAccountParameters, ] } diff --git a/packages/celotool/src/lib/migration-utils.ts b/packages/celotool/src/lib/migration-utils.ts new file mode 100644 index 00000000000..8ea7bf23ddb --- /dev/null +++ b/packages/celotool/src/lib/migration-utils.ts @@ -0,0 +1,52 @@ +import { envVar, fetchEnv } from './env-utils' +import { + AccountType, + generatePrivateKey, + getAddressesFor, + getPrivateKeysFor, + privateKeyToAddress, +} from './generate_utils' +import { ensure0x } from './utils' + +export function minerForEnv() { + return privateKeyToAddress( + generatePrivateKey(fetchEnv(envVar.MNEMONIC), AccountType.VALIDATOR, 0) + ) +} + +export function validatorKeys() { + return getPrivateKeysFor( + AccountType.VALIDATOR, + fetchEnv(envVar.MNEMONIC), + parseInt(fetchEnv(envVar.VALIDATORS), 10) + ).map(ensure0x) +} + +function getAttestationKeys() { + return getPrivateKeysFor( + AccountType.ATTESTATION, + fetchEnv(envVar.MNEMONIC), + parseInt(fetchEnv(envVar.VALIDATORS), 10) + ).map(ensure0x) +} + +export function migrationOverrides() { + const mnemonic = fetchEnv(envVar.MNEMONIC) + return { + validators: { + validatorKeys: validatorKeys(), + attestationKeys: getAttestationKeys(), + }, + stableToken: { + initialAccounts: getAddressesFor(AccountType.FAUCET, mnemonic, 2), + values: getAddressesFor(AccountType.FAUCET, mnemonic, 2).map(() => '60000000000000000000000'), // 60k Celo Dollars + }, + oracles: getAddressesFor(AccountType.PRICE_ORACLE, mnemonic, 1), + } +} + +export function truffleOverrides() { + return { + from: minerForEnv(), + } +} diff --git a/packages/celotool/src/lib/utils.ts b/packages/celotool/src/lib/utils.ts index dfbf2887fa1..847af828e61 100644 --- a/packages/celotool/src/lib/utils.ts +++ b/packages/celotool/src/lib/utils.ts @@ -42,8 +42,12 @@ export function execCmd( ) if (pipeOutput) { - execProcess.stdout.pipe(process.stdout) - execProcess.stderr.pipe(process.stderr) + if (execProcess.stdout) { + execProcess.stdout.pipe(process.stdout) + } + if (execProcess.stderr) { + execProcess.stderr.pipe(process.stderr) + } } }) } @@ -92,14 +96,6 @@ export async function outputIncludes(cmd: string, matchString: string, matchMess return false } -export function getVerificationPoolSMSURL(celoEnv: string) { - return `https://us-central1-celo-testnet.cloudfunctions.net/handleVerificationRequest${celoEnv}/v0.1/sms/` -} - -export function getVerificationPoolRewardsURL(celoEnv: string) { - return `https://us-central1-celo-testnet.cloudfunctions.net/handleVerificationRequest${celoEnv}/v0.1/rewards/` -} - export async function retrieveTxNodeIpAddress(celoEnv: string, txNodeIndex: number) { if (isVmBased()) { const outputs = await getTestnetOutputs(celoEnv) diff --git a/packages/celotool/src/lib/vm-testnet-utils.ts b/packages/celotool/src/lib/vm-testnet-utils.ts index 9bcbde914a8..6eaec13a9b0 100644 --- a/packages/celotool/src/lib/vm-testnet-utils.ts +++ b/packages/celotool/src/lib/vm-testnet-utils.ts @@ -50,7 +50,6 @@ const testnetEnvVars: TerraformVars = { network_id: envVar.NETWORK_ID, tx_node_count: envVar.TX_NODES, validator_count: envVar.VALIDATORS, - verification_pool_url: envVar.VERIFICATION_POOL_URL, } const testnetNetworkEnvVars: TerraformVars = { @@ -392,7 +391,6 @@ function generateValidatorSecretEnvVars(accountType: AccountType, index: number) ), PRIVATE_KEY: privateKey, [envVar.GETH_ACCOUNT_SECRET]: fetchEnv(envVar.GETH_ACCOUNT_SECRET), - [envVar.ETHSTATS_WEBSOCKETSECRET]: fetchEnv(envVar.ETHSTATS_WEBSOCKETSECRET), [envVar.MNEMONIC]: mnemonic, } return formatEnvVars(secrets) diff --git a/packages/cli/jest.config.js b/packages/cli/jest.config.js index aa5c4c41f55..c30f58fd842 100644 --- a/packages/cli/jest.config.js +++ b/packages/cli/jest.config.js @@ -2,7 +2,7 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', testMatch: ['/src/**/?(*.)+(spec|test).ts?(x)'], - setupFilesAfterEnv: ['/src/test-utils/matchers.ts'], + setupFilesAfterEnv: ['@celo/dev-utils/lib/matchers'], globalSetup: '/src/test-utils/ganache.setup.ts', globalTeardown: '/src/test-utils/ganache.teardown.ts', } diff --git a/packages/cli/package.json b/packages/cli/package.json index 371e03eab0c..2c589cb8782 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@celo/celocli", "description": "CLI Tool for transacting with the Celo protocol", - "version": "0.0.29", + "version": "0.0.30-beta", "author": "Celo", "license": "Apache-2.0", "repository": "celo-org/celo-monorepo", @@ -26,7 +26,7 @@ "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", "prepack": "yarn run build && oclif-dev manifest && oclif-dev readme", - "test:reset": "yarn --cwd ../protocol devchain generate .devchain", + "test:reset": "yarn --cwd ../protocol devchain generate .devchain --migration_override ../dev-utils/src/migration-override.json", "test:livechain": "yarn --cwd ../protocol devchain run .devchain", "test": "TZ=UTC jest --runInBand" }, @@ -38,6 +38,7 @@ "@oclif/plugin-help": "^2", "bip32": "^1.0.2", "bip39": "^2.5.0", + "bls12377js": "https://github.com/celo-org/bls12377js#ea09eba5c54fe63617af494a0c198fcc47582e0c", "chalk": "^2.4.2", "cli-table": "^0.3.1", "cli-ux": "^5.3.1", @@ -54,6 +55,7 @@ }, "devDependencies": { "@celo/dev-cli": "^2.0.3", + "@celo/dev-utils": "0.0.1-dev", "@types/bip32": "^1.0.1", "@types/bip39": "^2.4.2", "@types/cli-table": "^0.3.0", @@ -77,16 +79,16 @@ "commands": "./lib/commands", "topics": { "account": { - "description": "Manage your account, send and receive Celo Gold and Celo Dollars" + "description": "Manage your account, keys, and metadata" }, "config": { "description": "Configure CLI options which persist across commands" }, "election": { - "description": "View and manage validator elections" + "description": "Participate in and view the state of Validator Elections" }, "exchange": { - "description": "Commands for interacting with the Exchange" + "description": "Exchange Celo Dollars and Celo Gold via the stability mechanism" }, "lockedgold": { "description": "View and manage locked Celo Gold" @@ -94,11 +96,14 @@ "node": { "description": "Manage your full node" }, + "transfer": { + "description": "Transfer Celo Gold and Celo Dollars" + }, "validator": { - "description": "View and manage validators" + "description": "View and manage Validators" }, "validatorgroup": { - "description": "View and manage validator groups" + "description": "View and manage Validator Groups" } }, "bin": "celocli", diff --git a/packages/cli/src/base.ts b/packages/cli/src/base.ts index a3085dc2847..bebfa8c90dc 100644 --- a/packages/cli/src/base.ts +++ b/packages/cli/src/base.ts @@ -1,5 +1,7 @@ import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' +import { CeloProvider } from '@celo/contractkit/lib/providers/celo-provider' import { Command, flags } from '@oclif/command' +import { ParserOutput } from '@oclif/parser/lib/parse' import Web3 from 'web3' import { getNodeUrl } from './utils/config' import { injectDebugProvider } from './utils/eth-debug-provider' @@ -9,6 +11,7 @@ export abstract class BaseCommand extends Command { static flags = { logLevel: flags.string({ char: 'l', hidden: true }), help: flags.help({ char: 'h', hidden: true }), + privateKey: flags.string({ hidden: true }), } // This specifies whether the node needs to be synced before the command @@ -39,6 +42,11 @@ export abstract class BaseCommand extends Command { if (!this._kit) { this._kit = newKitFromWeb3(this.web3) } + + const res: ParserOutput = this.parse() + if (res.flags && res.flags.privateKey) { + this._kit.addAccount(res.flags.privateKey) + } return this._kit } @@ -59,8 +67,16 @@ export abstract class BaseCommand extends Command { finally(arg: Error | undefined): Promise { try { - // Close the web3 connection or the CLI hangs forever. + // If local-signing accounts are added, the debug wrapper is itself wrapped + // with a CeloProvider. This class has a stop() function that handles closing + // the connection for underlying providers + if (this.web3.currentProvider instanceof CeloProvider) { + const celoProvider = this.web3.currentProvider as CeloProvider + celoProvider.stop() + } + if (this._originalProvider && this._originalProvider.hasOwnProperty('connection')) { + // Close the web3 connection or the CLI hangs forever. const connection = this._originalProvider.connection if (connection.hasOwnProperty('_connection')) { connection._connection.close() diff --git a/packages/cli/src/commands/account/authorize.test.ts b/packages/cli/src/commands/account/authorize.test.ts index 1127bd6afcf..4d3340c1e75 100644 --- a/packages/cli/src/commands/account/authorize.test.ts +++ b/packages/cli/src/commands/account/authorize.test.ts @@ -1,5 +1,5 @@ +import { testWithGanache } from '@celo/dev-utils/lib/ganache-test' import Web3 from 'web3' -import { testWithGanache } from '../../test-utils/ganache-test' import Authorize from './authorize' import Register from './register' @@ -8,14 +8,32 @@ process.env.NO_SYNCCHECK = 'true' testWithGanache('account:authorize cmd', (web3: Web3) => { test('can authorize account', async () => { const accounts = await web3.eth.getAccounts() - await Register.run(['--from', accounts[0], '--name', 'Chapulin Colorado']) - await Authorize.run(['--from', accounts[0], '--role', 'validation', '--to', accounts[1]]) + await Register.run(['--from', accounts[0]]) + await Authorize.run([ + '--from', + accounts[0], + '--role', + 'validator', + '--signer', + accounts[1], + '--pop', + '0x1b9fca4bbb5bfb1dbe69ef1cddbd9b4202dcb6b134c5170611e1e36ecfa468d7b46c85328d504934fce6c2a1571603a50ae224d2b32685e84d4d1a1eebad8452eb', + ]) }) test('fails if from is not an account', async () => { const accounts = await web3.eth.getAccounts() await expect( - Authorize.run(['--from', accounts[0], '--role', 'validation', '--to', accounts[1]]) + Authorize.run([ + '--from', + accounts[0], + '--role', + 'validator', + '--signer', + accounts[1], + '--pop', + '0x1b9fca4bbb5bfb1dbe69ef1cddbd9b4202dcb6b134c5170611e1e36ecfa468d7b46c85328d504934fce6c2a1571603a50ae224d2b32685e84d4d1a1eebad8452eb', + ]) ).rejects.toThrow() }) }) diff --git a/packages/cli/src/commands/account/authorize.ts b/packages/cli/src/commands/account/authorize.ts index f34239dae7c..55d63e24034 100644 --- a/packages/cli/src/commands/account/authorize.ts +++ b/packages/cli/src/commands/account/authorize.ts @@ -5,40 +5,36 @@ import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' export default class Authorize extends BaseCommand { - static description = 'Authorize an attestation, validation or vote signing key' + static description = + 'Keep your locked Gold more secure by authorizing alternative keys to be used for signing attestations, voting, or validating. By doing so, you can continue to participate in the protocol why keeping the key with access to your locked Gold in cold storage. You must include a "proof-of-possession" of the key being authorized, which can be generated with the "account:proof-of-possession" command.' static flags = { ...BaseCommand.flags, from: Flags.address({ required: true }), role: flags.string({ char: 'r', - options: ['vote', 'validation', 'attestation'], + options: ['vote', 'validator', 'attestation'], description: 'Role to delegate', + required: true, }), - to: Flags.address({ required: true }), + pop: flags.string({ + description: 'Proof-of-possession of the signer key', + required: true, + }), + signer: Flags.address({ required: true }), } static args = [] static examples = [ - 'authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role vote --to 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', + 'authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role vote --signer 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb --pop 0x1b9fca4bbb5bfb1dbe69ef1cddbd9b4202dcb6b134c5170611e1e36ecfa468d7b46c85328d504934fce6c2a1571603a50ae224d2b32685e84d4d1a1eebad8452eb', ] async run() { const res = this.parse(Authorize) - - if (!res.flags.role) { - this.error(`Specify role with --role`) - return - } - - if (!res.flags.to) { - this.error(`Specify authorized address with --to`) - return - } - this.kit.defaultAccount = res.flags.from const accounts = await this.kit.contracts.getAccounts() + const sig = accounts.parseSignatureOfAddress(res.flags.from, res.flags.signer, res.flags.pop) await newCheckBuilder(this) .isAccount(res.flags.from) @@ -46,11 +42,11 @@ export default class Authorize extends BaseCommand { let tx: any if (res.flags.role === 'vote') { - tx = await accounts.authorizeVoteSigner(res.flags.from, res.flags.to) - } else if (res.flags.role === 'validation') { - tx = await accounts.authorizeValidationSigner(res.flags.from, res.flags.to) + tx = await accounts.authorizeVoteSigner(res.flags.signer, sig) + } else if (res.flags.role === 'validator') { + tx = await accounts.authorizeValidatorSigner(res.flags.signer, sig) } else if (res.flags.role === 'attestation') { - tx = await accounts.authorizeAttestationSigner(res.flags.from, res.flags.to) + tx = await accounts.authorizeAttestationSigner(res.flags.signer, sig) } else { this.error(`Invalid role provided`) return diff --git a/packages/cli/src/commands/account/balance.ts b/packages/cli/src/commands/account/balance.ts index d5f44440467..d7800d91d03 100644 --- a/packages/cli/src/commands/account/balance.ts +++ b/packages/cli/src/commands/account/balance.ts @@ -3,13 +3,13 @@ import { printValueMap } from '../../utils/cli' import { Args } from '../../utils/command' export default class Balance extends BaseCommand { - static description = 'View Celo Dollar and Gold balances given account address' + static description = 'View Celo Dollar and Gold balances for an address' static flags = { ...BaseCommand.flags, } - static args = [Args.address('account')] + static args = [Args.address('address')] static examples = ['balance 0x5409ed021d9299bf6814279a6a1411a7e866a631'] @@ -19,8 +19,8 @@ export default class Balance extends BaseCommand { const goldToken = await this.kit.contracts.getGoldToken() const stableToken = await this.kit.contracts.getStableToken() const balances = { - goldBalance: await goldToken.balanceOf(args.account), - dollarBalance: await stableToken.balanceOf(args.account), + goldBalance: await goldToken.balanceOf(args.address), + dollarBalance: await stableToken.balanceOf(args.address), } printValueMap(balances) } diff --git a/packages/cli/src/commands/account/claim-attestation-service-url.ts b/packages/cli/src/commands/account/claim-attestation-service-url.ts index 8235cf45a34..43bd3d8aa8f 100644 --- a/packages/cli/src/commands/account/claim-attestation-service-url.ts +++ b/packages/cli/src/commands/account/claim-attestation-service-url.ts @@ -1,4 +1,4 @@ -import { createAttestationServiceURLClaim } from '@celo/contractkit/lib/identity/claims/claim' +import { createAttestationServiceURLClaim } from '@celo/contractkit/lib/identity/claims/attestation-service-url' import { Flags } from '../../utils/command' import { ClaimCommand } from '../../utils/identity' export default class ClaimAttestationServiceUrl extends ClaimCommand { diff --git a/packages/cli/src/commands/account/claim-keybase.ts b/packages/cli/src/commands/account/claim-keybase.ts index 21aa9a3522b..07787990526 100644 --- a/packages/cli/src/commands/account/claim-keybase.ts +++ b/packages/cli/src/commands/account/claim-keybase.ts @@ -1,4 +1,4 @@ -import { SignedClaim } from '@celo/contractkit/lib/identity/claims/claim' +import { hashOfClaim } from '@celo/contractkit/lib/identity/claims/claim' import { createKeybaseClaim, KeybaseClaim, @@ -35,18 +35,15 @@ export default class ClaimKeybase extends ClaimCommand { const address = toChecksumAddress(res.flags.from) const username = res.flags.username const metadata = this.readMetadata() - const signedClaim = await this.addClaim(metadata, createKeybaseClaim(username)) + const claim = createKeybaseClaim(username) + const signature = await this.signer.sign(hashOfClaim(claim)) + await this.addClaim(metadata, claim) this.writeMetadata(metadata) try { - await this.uploadProof( - signedClaim.payload as KeybaseClaim, - signedClaim.signature, - username, - address - ) + await this.uploadProof(claim, signature, username, address) } catch (error) { - this.printManualInstruction(signedClaim, username, address) + this.printManualInstruction(claim, signature, username, address) } } @@ -56,7 +53,7 @@ export default class ClaimKeybase extends ClaimCommand { username: string, address: string ) { - const signedClaim = { payload: claim, signature } + const signedClaim = { claim, signature } try { cli.action.start(`Attempting to automate keybase proof`) const publicFolderPrefix = `/keybase/public/${username}/` @@ -86,7 +83,6 @@ export default class ClaimKeybase extends ClaimCommand { } } async uploadProof(claim: KeybaseClaim, signature: string, username: string, address: string) { - const signedClaim = { payload: claim, signature } try { if ( (await commandExists('keybase')) && @@ -96,7 +92,7 @@ export default class ClaimKeybase extends ClaimCommand { ) { await this.attemptAutomaticProofUpload(claim, signature, username, address) } else { - this.printManualInstruction(signedClaim, username, address) + this.printManualInstruction(claim, signature, username, address) } } catch (error) { cli.action.stop('Error') @@ -104,13 +100,18 @@ export default class ClaimKeybase extends ClaimCommand { 'Could not automatically finish the proving, please complete this step manually.\n\n ' + error ) - this.printManualInstruction(signedClaim, username, address) + this.printManualInstruction(claim, signature, username, address) } } - printManualInstruction(claim: SignedClaim, username: string, address: string) { + printManualInstruction( + claim: KeybaseClaim, + signature: string, + username: string, + address: string + ) { const fileName = proofFileName(address) - writeFileSync(fileName, JSON.stringify(claim)) + writeFileSync(fileName, JSON.stringify({ claim, signature })) console.info( `\nProving a keybase claim requires you to publish the signed claim on your Keybase file system to prove ownership. We saved it for you under ${fileName}. It should be hosted in your public folder at ${keybaseFilePathToProof}/${fileName}, so that it is available under ${targetURL( username, diff --git a/packages/cli/src/commands/account/claims.test.ts b/packages/cli/src/commands/account/claims.test.ts index df2994703b3..83a723647ba 100644 --- a/packages/cli/src/commands/account/claims.test.ts +++ b/packages/cli/src/commands/account/claims.test.ts @@ -1,9 +1,9 @@ import { IdentityMetadataWrapper, newKitFromWeb3 } from '@celo/contractkit' import { ClaimTypes } from '@celo/contractkit/lib/identity' +import { testWithGanache } from '@celo/dev-utils/lib/ganache-test' import { readFileSync, writeFileSync } from 'fs' import { tmpdir } from 'os' import Web3 from 'web3' -import { testWithGanache } from '../../test-utils/ganache-test' import ClaimAccount from './claim-account' import ClaimDomain from './claim-domain' import ClaimName from './claim-name' @@ -11,7 +11,7 @@ import CreateMetadata from './create-metadata' import RegisterMetadata from './register-metadata' process.env.NO_SYNCCHECK = 'true' -testWithGanache('account:authorize cmd', (web3: Web3) => { +testWithGanache('account metadata cmds', (web3: Web3) => { let account: string let accounts: string[] beforeEach(async () => { diff --git a/packages/cli/src/commands/account/proof-of-possession.ts b/packages/cli/src/commands/account/proof-of-possession.ts new file mode 100644 index 00000000000..7c5b0f90cb6 --- /dev/null +++ b/packages/cli/src/commands/account/proof-of-possession.ts @@ -0,0 +1,48 @@ +import { serializeSignature } from '@celo/utils/lib/signatureUtils' +import { flags } from '@oclif/command' +import { BaseCommand } from '../../base' +import { printValueMap } from '../../utils/cli' +import { Flags } from '../../utils/command' +export default class ProofOfPossession extends BaseCommand { + static description = + 'Generate proof-of-possession to be used to authorize a signer. See the "account:authorize" command for more details.' + + static flags = { + ...BaseCommand.flags, + signer: Flags.address({ + required: true, + description: 'Address of the signer key to prove possession of.', + }), + account: Flags.address({ + required: true, + description: 'Address of the account that needs to proove possession of the signer key.', + }), + privateKey: flags.string({ + description: + 'Optional. The signer private key, only necessary if the key is not being managed by a locally running node.', + }), + } + + static examples = [ + 'proof-of-possession --account 0x5409ed021d9299bf6814279a6a1411a7e866a631 --signer 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb', + ] + + async run() { + const res = this.parse(ProofOfPossession) + const accounts = await this.kit.contracts.getAccounts() + if (res.flags.privateKey) { + const pop = await accounts.generateProofOfSigningKeyPossessionLocally( + res.flags.account, + res.flags.signer, + res.flags.privateKey + ) + printValueMap({ signature: serializeSignature(pop) }) + } else { + const pop = await accounts.generateProofOfSigningKeyPossession( + res.flags.account, + res.flags.signer + ) + printValueMap({ signature: serializeSignature(pop) }) + } + } +} diff --git a/packages/cli/src/commands/account/register.test.ts b/packages/cli/src/commands/account/register.test.ts index 444dd71a0bc..0759f320d84 100644 --- a/packages/cli/src/commands/account/register.test.ts +++ b/packages/cli/src/commands/account/register.test.ts @@ -1,5 +1,5 @@ +import { testWithGanache } from '@celo/dev-utils/lib/ganache-test' import Web3 from 'web3' -import { testWithGanache } from '../../test-utils/ganache-test' import Register from './register' process.env.NO_SYNCCHECK = 'true' diff --git a/packages/cli/src/commands/account/register.ts b/packages/cli/src/commands/account/register.ts index ea121e9cdfc..c7c4b4791fd 100644 --- a/packages/cli/src/commands/account/register.ts +++ b/packages/cli/src/commands/account/register.ts @@ -5,17 +5,21 @@ import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' export default class Register extends BaseCommand { - static description = 'Register an account' + static description = + 'Register an account on-chain. This allows you to lock Gold, which is a pre-requisite for registering a Validator or Group, participating in Validator elections and on-chain Governance, and earning epoch rewards.' static flags = { ...BaseCommand.flags, - name: flags.string({ required: true }), + name: flags.string(), from: Flags.address({ required: true }), } static args = [] - static examples = ['register'] + static examples = [ + 'register --from 0x5409ed021d9299bf6814279a6a1411a7e866a631', + 'register --from 0x5409ed021d9299bf6814279a6a1411a7e866a631 --name test-account', + ] async run() { const res = this.parse(Register) @@ -26,6 +30,8 @@ export default class Register extends BaseCommand { .isNotAccount(res.flags.from) .runChecks() await displaySendTx('register', accounts.createAccount()) - await displaySendTx('setName', accounts.setName(res.flags.name)) + if (res.flags.name) { + await displaySendTx('setName', accounts.setName(res.flags.name)) + } } } diff --git a/packages/cli/src/commands/account/transferdollar.ts b/packages/cli/src/commands/account/transferdollar.ts deleted file mode 100644 index 164e8d9f392..00000000000 --- a/packages/cli/src/commands/account/transferdollar.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { flags } from '@oclif/command' -import BigNumber from 'bignumber.js' -import { BaseCommand } from '../../base' -import { displaySendTx } from '../../utils/cli' -import { Flags } from '../../utils/command' - -export default class DollarTransfer extends BaseCommand { - static description = 'Transfer Celo Dollars' - - static flags = { - ...BaseCommand.flags, - from: Flags.address({ required: true, description: 'Address of the sender' }), - to: Flags.address({ required: true, description: 'Address of the receiver' }), - amountInWei: flags.string({ required: true, description: 'Amount to transfer (in wei)' }), - } - - static examples = [ - 'transferdollar --from 0xa0Af2E71cECc248f4a7fD606F203467B500Dd53B --to 0x5409ed021d9299bf6814279a6a1411a7e866a631 --amountInWei 1', - ] - - async run() { - const res = this.parse(DollarTransfer) - - const from: string = res.flags.from - const to: string = res.flags.to - const amountInWei = new BigNumber(res.flags.amountInWei) - - this.kit.defaultAccount = from - - const goldToken = await this.kit.contracts.getGoldToken() - const stableToken = await this.kit.contracts.getStableToken() - // Units of all balances are in wei, unless specified. - // Check the balance before - const goldBalanceFromBefore = await goldToken.balanceOf(from) - const dollarBalanceFromBefore = await stableToken.balanceOf(from) - - // Perform the transfer - await displaySendTx('dollar.Transfer', stableToken.transfer(to, amountInWei.toString())) - - // Check the balance after - const goldBalanceFromAfter = await goldToken.balanceOf(from) - const dollarBalanceFromAfter = await stableToken.balanceOf(from) - - // Get gas cost - const goldDifference = goldBalanceFromBefore.minus(goldBalanceFromAfter) - const dollarDifference = dollarBalanceFromBefore.minus(dollarBalanceFromAfter) - const gasCostInWei = goldDifference - this.log( - `Transferred ${amountInWei} from ${from} to ${to}, gas cost: ${gasCostInWei.toString()}` - ) - this.log( - `Dollar Balance of sender ${from} went down by ${dollarDifference.toString()} wei,` + - `final balance: ${dollarBalanceFromAfter.toString()} Celo Dollars wei` - ) - this.log( - `Gold Balance of sender ${from} went down by ${goldDifference.toString()} wei, ` + - `final balance: ${goldBalanceFromAfter.toString()} Celo Gold wei` - ) - } -} diff --git a/packages/cli/src/commands/account/transfergold.ts b/packages/cli/src/commands/account/transfergold.ts deleted file mode 100644 index 768fab2c83c..00000000000 --- a/packages/cli/src/commands/account/transfergold.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { flags } from '@oclif/command' -import BigNumber from 'bignumber.js' -import { BaseCommand } from '../../base' -import { displaySendTx } from '../../utils/cli' -import { Flags } from '../../utils/command' - -export default class GoldTransfer extends BaseCommand { - static description = 'Transfer gold' - - static flags = { - ...BaseCommand.flags, - from: Flags.address({ required: true, description: 'Address of the sender' }), - to: Flags.address({ required: true, description: 'Address of the receiver' }), - amountInWei: flags.string({ required: true, description: 'Amount to transfer (in wei)' }), - } - - static examples = [ - 'transfergold --from 0xa0Af2E71cECc248f4a7fD606F203467B500Dd53B --to 0x5409ed021d9299bf6814279a6a1411a7e866a631 --amountInWei 1', - ] - - async run() { - const res = this.parse(GoldTransfer) - - const from: string = res.flags.from - const to: string = res.flags.to - const amountInWei = new BigNumber(res.flags.amountInWei) - - this.kit.defaultAccount = from - // Units of all balances are in wei, unless specified. - // Check the balance before - const goldToken = await this.kit.contracts.getGoldToken() - - // Check the balance before - const balanceFromBeforeInWei = await goldToken.balanceOf(from) - - // Perform the transfer - await displaySendTx('gold.Transfer', goldToken.transfer(to, amountInWei.toString())) - - // Check the balance after - const balanceFromAfterInWei = await goldToken.balanceOf(from) - - // Get gas cost - const differenceInWei = balanceFromBeforeInWei.minus(balanceFromAfterInWei) - const gasCostInWei = differenceInWei.minus(amountInWei) - this.log( - `Transferred ${amountInWei} from ${from} to ${to}, gas cost: ${gasCostInWei.toString()} wei` - ) - this.log( - `Balance of sender ${from} went down by ${differenceInWei.toString()} wei, final balance: ${balanceFromAfterInWei} Celo Gold wei` - ) - } -} diff --git a/packages/cli/src/commands/config/set.ts b/packages/cli/src/commands/config/set.ts index a525ca4a5ef..1a4283d9247 100644 --- a/packages/cli/src/commands/config/set.ts +++ b/packages/cli/src/commands/config/set.ts @@ -10,7 +10,7 @@ export default class Set extends BaseCommand { node: flags.string({ required: true, description: 'Node URL', - default: 'ws://localhost:8546', + default: 'http://localhost:8545', }), } diff --git a/packages/cli/src/commands/election/activate.ts b/packages/cli/src/commands/election/activate.ts new file mode 100644 index 00000000000..26b8d51ea56 --- /dev/null +++ b/packages/cli/src/commands/election/activate.ts @@ -0,0 +1,51 @@ +import { sleep } from '@celo/utils/lib/async' +import { flags } from '@oclif/command' +import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class ElectionVote extends BaseCommand { + static description = 'Activate pending votes in validator elections to begin earning rewards' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: "Voter's address" }), + wait: flags.boolean({ description: 'Wait until all pending votes become activatable' }), + } + + static examples = [ + 'activate --from 0x4443d0349e8b3075cba511a0a87796597602a0f1', + 'activate --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --wait', + ] + async run() { + const res = this.parse(ElectionVote) + + this.kit.defaultAccount = res.flags.from + await newCheckBuilder(this, res.flags.from) + .isSignerOrAccount() + .runChecks() + + const election = await this.kit.contracts.getElection() + const accounts = await this.kit.contracts.getAccounts() + const account = await accounts.voteSignerToAccount(res.flags.from) + const hasPendingVotes = await election.hasPendingVotes(account) + if (hasPendingVotes) { + if (res.flags.wait) { + // Spin until pending votes become activatable. + while (!(await election.hasActivatablePendingVotes(account))) { + await sleep(1000) + } + } + const txos = await election.activate(account) + for (const txo of txos) { + await displaySendTx('activate', txo, { from: res.flags.from }) + } + if (txos.length === 0) { + this.log(`Pending votes not yet activatable. Consider using the --wait flag.`) + } + } else { + this.log(`No pending votes to activate`) + } + } +} diff --git a/packages/cli/src/commands/election/current.ts b/packages/cli/src/commands/election/current.ts new file mode 100644 index 00000000000..5eba5fe88d0 --- /dev/null +++ b/packages/cli/src/commands/election/current.ts @@ -0,0 +1,32 @@ +import { cli } from 'cli-ux' +import { BaseCommand } from '../../base' + +export default class ElectionCurrent extends BaseCommand { + static description = + 'Outputs the set of validators currently participating in BFT to create blocks. The validator set is re-elected at the end of every epoch.' + + static flags = { + ...BaseCommand.flags, + } + + static examples = ['current'] + + async run() { + cli.action.start('Fetching currently elected Validators') + const election = await this.kit.contracts.getElection() + const validators = await this.kit.contracts.getValidators() + const signers = await election.getCurrentValidatorSigners() + const validatorList = await Promise.all( + signers.map((addr) => validators.getValidatorFromSigner(addr)) + ) + cli.action.stop() + cli.table(validatorList, { + address: {}, + name: {}, + affiliation: {}, + score: {}, + ecdsaPublicKey: {}, + blsPublicKey: {}, + }) + } +} diff --git a/packages/cli/src/commands/election/list.ts b/packages/cli/src/commands/election/list.ts new file mode 100644 index 00000000000..b0195723394 --- /dev/null +++ b/packages/cli/src/commands/election/list.ts @@ -0,0 +1,26 @@ +import { cli } from 'cli-ux' +import { BaseCommand } from '../../base' + +export default class List extends BaseCommand { + static description = + 'Prints the list of validator groups, the number of votes they have received, the number of additional votes they are able to receive, and whether or not they are eleigible to elect validators.' + + static flags = { + ...BaseCommand.flags, + } + + static examples = ['list'] + + async run() { + cli.action.start('Fetching validator group vote totals') + const election = await this.kit.contracts.getElection() + const groupVotes = await election.getValidatorGroupsVotes() + cli.action.stop() + cli.table(groupVotes, { + address: {}, + votes: { get: (g) => g.votes.toFixed() }, + capacity: { get: (g) => g.capacity.toFixed() }, + eligible: {}, + }) + } +} diff --git a/packages/cli/src/commands/election/revoke.ts b/packages/cli/src/commands/election/revoke.ts new file mode 100644 index 00000000000..db009ea9b75 --- /dev/null +++ b/packages/cli/src/commands/election/revoke.ts @@ -0,0 +1,41 @@ +import { flags } from '@oclif/command' +import BigNumber from 'bignumber.js' +import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class ElectionRevoke extends BaseCommand { + static description = 'Revoke votes for a Validator Group in validator elections.' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: "Voter's address" }), + for: Flags.address({ + description: "ValidatorGroup's address", + required: true, + }), + value: flags.string({ description: 'Value of votes to revoke', required: true }), + } + + static examples = [ + 'revoke --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --for 0x932fee04521f5fcb21949041bf161917da3f588b, --value 1000000', + ] + async run() { + const res = this.parse(ElectionRevoke) + + this.kit.defaultAccount = res.flags.from + await newCheckBuilder(this, res.flags.from) + .isSignerOrAccount() + .isValidatorGroup(res.flags.for) + .runChecks() + + const election = await this.kit.contracts.getElection() + const accounts = await this.kit.contracts.getAccounts() + const account = await accounts.voteSignerToAccount(res.flags.from) + const txos = await election.revoke(account, res.flags.for, new BigNumber(res.flags.value)) + for (const txo of txos) { + await displaySendTx('revoke', txo, { from: res.flags.from }) + } + } +} diff --git a/packages/cli/src/commands/election/run.ts b/packages/cli/src/commands/election/run.ts new file mode 100644 index 00000000000..3026f7b3e27 --- /dev/null +++ b/packages/cli/src/commands/election/run.ts @@ -0,0 +1,32 @@ +import { cli } from 'cli-ux' +import { BaseCommand } from '../../base' + +export default class ElectionRun extends BaseCommand { + static description = + 'Runs a "mock" election and prints out the validators that would be elected if the epoch ended right now.' + + static flags = { + ...BaseCommand.flags, + } + + static examples = ['run'] + + async run() { + cli.action.start('Running mock election') + const election = await this.kit.contracts.getElection() + const validators = await this.kit.contracts.getValidators() + const signers = await election.getCurrentValidatorSigners() + const validatorList = await Promise.all( + signers.map((addr) => validators.getValidatorFromSigner(addr)) + ) + cli.action.stop() + cli.table(validatorList, { + address: {}, + name: {}, + affiliation: {}, + score: {}, + ecdsaPublicKey: {}, + blsPublicKey: {}, + }) + } +} diff --git a/packages/cli/src/commands/election/show.ts b/packages/cli/src/commands/election/show.ts new file mode 100644 index 00000000000..345e6337bf1 --- /dev/null +++ b/packages/cli/src/commands/election/show.ts @@ -0,0 +1,48 @@ +import { flags } from '@oclif/command' +import { IArg } from '@oclif/parser/lib/args' +import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' +import { printValueMapRecursive } from '../../utils/cli' +import { Args } from '../../utils/command' + +export default class ElectionShow extends BaseCommand { + static description = 'Show election information about a voter or registered Validator Group' + + static flags = { + ...BaseCommand.flags, + voter: flags.boolean({ + exclusive: ['group'], + description: 'Show information about an account voting in Validator elections', + }), + group: flags.boolean({ + exclusive: ['voter'], + description: 'Show information about a group running in Validator elections', + }), + } + + static args: IArg[] = [ + Args.address('address', { description: "Voter or Validator Groups's address" }), + ] + + static examples = ['show 0x97f7333c51897469E8D98E7af8653aAb468050a3'] + + async run() { + const res = this.parse(ElectionShow) + const address = res.args.address + const election = await this.kit.contracts.getElection() + + if (res.flags.group) { + await newCheckBuilder(this) + .isValidatorGroup(address) + .runChecks() + const groupVotes = await election.getValidatorGroupVotes(address) + printValueMapRecursive(groupVotes) + } else if (res.flags.voter) { + await newCheckBuilder(this) + .isAccount(address) + .runChecks() + const voter = await election.getVoter(address) + printValueMapRecursive(voter) + } + } +} diff --git a/packages/cli/src/commands/election/validatorset.ts b/packages/cli/src/commands/election/validatorset.ts deleted file mode 100644 index 74f56f97874..00000000000 --- a/packages/cli/src/commands/election/validatorset.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { BaseCommand } from '../../base' - -export default class ValidatorSet extends BaseCommand { - static description = 'Outputs the current validator set' - - static flags = { - ...BaseCommand.flags, - } - - static examples = ['validatorset'] - - async run() { - const election = await this.kit.contracts.getElection() - const validatorSet = await election.getValidatorSetAddresses() - - validatorSet.forEach((validator: string) => console.log(validator)) - } -} diff --git a/packages/cli/src/commands/election/vote.ts b/packages/cli/src/commands/election/vote.ts index cf957d74755..0132f5a93c4 100644 --- a/packages/cli/src/commands/election/vote.ts +++ b/packages/cli/src/commands/election/vote.ts @@ -1,6 +1,7 @@ import { flags } from '@oclif/command' import BigNumber from 'bignumber.js' import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' @@ -11,7 +12,7 @@ export default class ElectionVote extends BaseCommand { ...BaseCommand.flags, from: Flags.address({ required: true, description: "Voter's address" }), for: Flags.address({ - description: "Set vote for ValidatorGroup's address", + description: "ValidatorGroup's address", required: true, }), value: flags.string({ description: 'Amount of Gold used to vote for group', required: true }), @@ -24,6 +25,11 @@ export default class ElectionVote extends BaseCommand { const res = this.parse(ElectionVote) this.kit.defaultAccount = res.flags.from + await newCheckBuilder(this, res.flags.from) + .isSignerOrAccount() + .isValidatorGroup(res.flags.for) + .runChecks() + const election = await this.kit.contracts.getElection() const tx = await election.vote(res.flags.for, new BigNumber(res.flags.value)) await displaySendTx('vote', tx) diff --git a/packages/cli/src/commands/exchange/dollars.ts b/packages/cli/src/commands/exchange/dollars.ts new file mode 100644 index 00000000000..69c335ca533 --- /dev/null +++ b/packages/cli/src/commands/exchange/dollars.ts @@ -0,0 +1,47 @@ +import { flags } from '@oclif/command' +import BigNumber from 'bignumber.js' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class ExchangeDollars extends BaseCommand { + static description = 'Exchange Celo Dollars for Celo Gold via the stability mechanism' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ + required: true, + description: 'The address with Celo Dollars to exchange', + }), + value: Flags.address({ + required: true, + description: 'The value of Celo Dollars to exchange for Celo Gold', + }), + for: Flags.address({ + required: true, + description: 'The minimum value of Celo Gold to receive in return', + }), + commission: flags.string({ required: true }), + } + + static args = [] + + static examples = [ + 'dollars --value 10000000000000 --for 50000000000000 --from 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', + ] + + async run() { + const res = this.parse(ExchangeDollars) + const sellAmount = new BigNumber(res.flags.value) + const minBuyAmount = new BigNumber(res.flags.for) + + this.kit.defaultAccount = res.flags.from + const stableToken = await this.kit.contracts.getStableToken() + const exchange = await this.kit.contracts.getExchange() + + await displaySendTx('approve', stableToken.approve(exchange.address, sellAmount.toFixed())) + + const exchangeTx = exchange.exchange(sellAmount.toFixed(), minBuyAmount.toFixed(), false) + await displaySendTx('exchange', exchangeTx) + } +} diff --git a/packages/cli/src/commands/exchange/gold.ts b/packages/cli/src/commands/exchange/gold.ts new file mode 100644 index 00000000000..68e0fa9fa04 --- /dev/null +++ b/packages/cli/src/commands/exchange/gold.ts @@ -0,0 +1,44 @@ +import { flags } from '@oclif/command' +import BigNumber from 'bignumber.js' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class ExchangeGold extends BaseCommand { + static description = 'Exchange Celo Gold for Celo Dollars via the stability mechanism' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: 'The address with Celo Gold to exchange' }), + value: Flags.address({ + required: true, + description: 'The value of Celo Gold to exchange for Celo Dollars', + }), + for: Flags.address({ + required: true, + description: 'The minimum value of Celo Dollars to receive in return', + }), + commission: flags.string({ required: true }), + } + + static args = [] + + static examples = [ + 'gold --value 5000000000000 --for 100000000000000 --from 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', + ] + + async run() { + const res = this.parse(ExchangeGold) + const sellAmount = new BigNumber(res.flags.value) + const minBuyAmount = new BigNumber(res.flags.for) + + this.kit.defaultAccount = res.flags.from + const goldToken = await this.kit.contracts.getGoldToken() + const exchange = await this.kit.contracts.getExchange() + + await displaySendTx('approve', goldToken.approve(exchange.address, sellAmount.toFixed())) + + const exchangeTx = exchange.exchange(sellAmount.toFixed(), minBuyAmount.toFixed(), true) + await displaySendTx('exchange', exchangeTx) + } +} diff --git a/packages/cli/src/commands/exchange/selldollar.ts b/packages/cli/src/commands/exchange/selldollar.ts deleted file mode 100644 index b407bbe5b76..00000000000 --- a/packages/cli/src/commands/exchange/selldollar.ts +++ /dev/null @@ -1,27 +0,0 @@ -import BigNumber from 'bignumber.js' -import { BaseCommand } from '../../base' -import { displaySendTx } from '../../utils/cli' -import { swapArguments } from '../../utils/exchange' - -export default class SellDollar extends BaseCommand { - static description = 'Sell Celo dollars for Celo gold on the exchange' - - static args = swapArguments - - static examples = ['selldollar 100 300 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d'] - - async run() { - const { args } = this.parse(SellDollar) - const sellAmount = new BigNumber(args.sellAmount) - const minBuyAmount = new BigNumber(args.minBuyAmount) - - this.kit.defaultAccount = args.from - const stableToken = await this.kit.contracts.getStableToken() - const exchange = await this.kit.contracts.getExchange() - - await displaySendTx('approve', stableToken.approve(exchange.address, sellAmount.toString())) - - const exchangeTx = exchange.exchange(sellAmount.toString(), minBuyAmount.toString(), false) - await displaySendTx('exchange', exchangeTx) - } -} diff --git a/packages/cli/src/commands/exchange/sellgold.ts b/packages/cli/src/commands/exchange/sellgold.ts deleted file mode 100644 index 3423eedca70..00000000000 --- a/packages/cli/src/commands/exchange/sellgold.ts +++ /dev/null @@ -1,27 +0,0 @@ -import BigNumber from 'bignumber.js' -import { BaseCommand } from '../../base' -import { displaySendTx } from '../../utils/cli' -import { swapArguments } from '../../utils/exchange' - -export default class SellGold extends BaseCommand { - static description = 'Sell Celo gold for Celo dollars on the exchange' - - static args = swapArguments - - static examples = ['sellgold 100 300 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d'] - - async run() { - const { args } = this.parse(SellGold) - const sellAmount = new BigNumber(args.sellAmount) - const minBuyAmount = new BigNumber(args.minBuyAmount) - - this.kit.defaultAccount = args.from - const goldToken = await this.kit.contracts.getGoldToken() - const exchange = await this.kit.contracts.getExchange() - - await displaySendTx('approve', goldToken.approve(exchange.address, sellAmount.toString())) - - const exchangeTx = exchange.exchange(sellAmount.toString(), minBuyAmount.toString(), true) - await displaySendTx('exchange', exchangeTx) - } -} diff --git a/packages/cli/src/commands/exchange/list.ts b/packages/cli/src/commands/exchange/show.ts similarity index 59% rename from packages/cli/src/commands/exchange/list.ts rename to packages/cli/src/commands/exchange/show.ts index 18e06e52b7e..97139cd7afa 100644 --- a/packages/cli/src/commands/exchange/list.ts +++ b/packages/cli/src/commands/exchange/show.ts @@ -2,13 +2,13 @@ import { flags } from '@oclif/command' import { cli } from 'cli-ux' import { BaseCommand } from '../../base' -export default class List extends BaseCommand { - static description = 'List information about tokens on the exchange (all amounts in wei)' +export default class ExchangeShow extends BaseCommand { + static description = 'Show the current exchange rates offered by the Exchange' static flags = { ...BaseCommand.flags, amount: flags.string({ - description: 'Amount of sellToken (in wei) to report rates for', + description: 'Amount of the token being exchanged to report rates for', default: '1000000000000000000', }), } @@ -18,7 +18,7 @@ export default class List extends BaseCommand { static examples = ['list'] async run() { - const { flags: parsedFlags } = this.parse(List) + const { flags: parsedFlags } = this.parse(ExchangeShow) cli.action.start('Fetching exchange rates...') const exchange = await this.kit.contracts.getExchange() @@ -26,7 +26,7 @@ export default class List extends BaseCommand { const goldForDollar = await exchange.getBuyTokenAmount(parsedFlags.amount as string, false) cli.action.stop() - this.log(`${parsedFlags.amount} cGLD => ${dollarForGold.toString()} cUSD`) - this.log(`${parsedFlags.amount} cUSD => ${goldForDollar.toString()} cGLD`) + this.log(`${parsedFlags.amount} cGLD => ${dollarForGold.toFixed()} cUSD`) + this.log(`${parsedFlags.amount} cUSD => ${goldForDollar.toFixed()} cGLD`) } } diff --git a/packages/cli/src/commands/account/lock.ts b/packages/cli/src/commands/lockedgold/lock.ts similarity index 64% rename from packages/cli/src/commands/account/lock.ts rename to packages/cli/src/commands/lockedgold/lock.ts index b46b49df012..3cf093e41cd 100644 --- a/packages/cli/src/commands/account/lock.ts +++ b/packages/cli/src/commands/lockedgold/lock.ts @@ -19,7 +19,7 @@ export default class Lock extends BaseCommand { static args = [] static examples = [ - 'lock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --value 1000000000000000000', + 'lock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --value 10000000000000000000000', ] async run() { @@ -30,13 +30,24 @@ export default class Lock extends BaseCommand { const value = new BigNumber(res.flags.value) await newCheckBuilder(this) - .addCheck(`Value [${value.toString()}] is >= 0`, () => value.gt(0)) + .addCheck(`Value [${value.toFixed()}] is not > 0`, () => value.gt(0)) .isAccount(address) - .hasEnoughGold(address, value) .runChecks() const lockedGold = await this.kit.contracts.getLockedGold() + const pendingWithdrawalsValue = await lockedGold.getPendingWithdrawalsTotalValue(address) + const relockValue = BigNumber.minimum(pendingWithdrawalsValue, value) + const lockValue = value.minus(relockValue) + + await newCheckBuilder(this) + .hasEnoughGold(address, lockValue) + .runChecks() + + const txos = await lockedGold.relock(address, relockValue) + for (const txo of txos) { + await displaySendTx('relock', txo, { from: address }) + } const tx = lockedGold.lock() - await displaySendTx('lock', tx, { value: value.toString() }) + await displaySendTx('lock', tx, { value: lockValue.toFixed() }) } } diff --git a/packages/cli/src/commands/lockedgold/show.ts b/packages/cli/src/commands/lockedgold/show.ts index b959faa599a..f6f21de0902 100644 --- a/packages/cli/src/commands/lockedgold/show.ts +++ b/packages/cli/src/commands/lockedgold/show.ts @@ -4,7 +4,8 @@ import { printValueMapRecursive } from '../../utils/cli' import { Args } from '../../utils/command' export default class Show extends BaseCommand { - static description = 'Show Locked Gold information for a given account' + static description = + 'Show Locked Gold information for a given account. This includes the total amount of locked gold, the amount being used for voting in Validator Elections, and any pending withdrawals that have been initiated via "lockedgold:unlock".' static flags = { ...BaseCommand.flags, diff --git a/packages/cli/src/commands/lockedgold/unlock.ts b/packages/cli/src/commands/lockedgold/unlock.ts index 2438a474aac..6eff5df54fa 100644 --- a/packages/cli/src/commands/lockedgold/unlock.ts +++ b/packages/cli/src/commands/lockedgold/unlock.ts @@ -6,7 +6,8 @@ import { Flags } from '../../utils/command' import { LockedGoldArgs } from '../../utils/lockedgold' export default class Unlock extends BaseCommand { - static description = 'Unlocks Celo Gold, which can be withdrawn after the unlocking period.' + static description = + 'Unlocks Celo Gold, which can be withdrawn after the unlocking period. Unlocked gold will appear as a "pending withdrawal" until the unlocking period is over, after which it can be withdrawn via "lockedgold:withdraw".' static flags = { ...BaseCommand.flags, diff --git a/packages/cli/src/commands/lockedgold/withdraw.ts b/packages/cli/src/commands/lockedgold/withdraw.ts index 6ad636ce8cf..2738495c5fe 100644 --- a/packages/cli/src/commands/lockedgold/withdraw.ts +++ b/packages/cli/src/commands/lockedgold/withdraw.ts @@ -4,7 +4,8 @@ import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' export default class Withdraw extends BaseCommand { - static description = 'Withdraw unlocked gold whose unlocking period has passed.' + static description = + 'Withdraw any pending withdrawals created via "lockedgold:unlock" that have become available.' static flags = { ...BaseCommand.flags, @@ -31,7 +32,7 @@ export default class Withdraw extends BaseCommand { const pendingWithdrawal = pendingWithdrawals[i] if (pendingWithdrawal.time.isLessThan(currentTime)) { console.log( - `Found available pending withdrawal of value ${pendingWithdrawal.value.toString()}, withdrawing` + `Found available pending withdrawal of value ${pendingWithdrawal.value.toFixed()}, withdrawing` ) await displaySendTx('withdraw', lockedgold.withdraw(i)) madeWithdrawal = true @@ -45,9 +46,9 @@ export default class Withdraw extends BaseCommand { const remainingPendingWithdrawals = await lockedgold.getPendingWithdrawals(flags.from) for (const pendingWithdrawal of remainingPendingWithdrawals) { console.log( - `Pending withdrawal of value ${pendingWithdrawal.value.toString()} available for withdrawal in ${pendingWithdrawal.time + `Pending withdrawal of value ${pendingWithdrawal.value.toFixed()} available for withdrawal in ${pendingWithdrawal.time .minus(currentTime) - .toString()} seconds.` + .toFixed()} seconds.` ) } } diff --git a/packages/cli/src/commands/network/parameters.ts b/packages/cli/src/commands/network/parameters.ts index a87439ae45b..27369aa1edf 100644 --- a/packages/cli/src/commands/network/parameters.ts +++ b/packages/cli/src/commands/network/parameters.ts @@ -2,7 +2,8 @@ import { BaseCommand } from '../../base' import { printValueMapRecursive } from '../../utils/cli' export default class Parameters extends BaseCommand { - static description = 'View network parameters' + static description = + 'View parameters of the network, including but not limited to configuration for the various Celo core smart contracts.' static flags = { ...BaseCommand.flags, diff --git a/packages/cli/src/commands/node/accounts.ts b/packages/cli/src/commands/node/accounts.ts index 10a9299d826..541eebb7efe 100644 --- a/packages/cli/src/commands/node/accounts.ts +++ b/packages/cli/src/commands/node/accounts.ts @@ -1,7 +1,7 @@ import { BaseCommand } from '../../base' export default class NodeAccounts extends BaseCommand { - static description = 'List node accounts' + static description = 'List the addresses that this node has the private keys for.' static flags = { ...BaseCommand.flags, diff --git a/packages/cli/src/commands/oracle/rates.ts b/packages/cli/src/commands/oracle/rates.ts new file mode 100644 index 00000000000..18c0d41cb7a --- /dev/null +++ b/packages/cli/src/commands/oracle/rates.ts @@ -0,0 +1,34 @@ +import { CeloContract } from '@celo/contractkit' +import { cli } from 'cli-ux' +import { BaseCommand } from '../../base' + +export default class GetRates extends BaseCommand { + static description = 'Get the current set oracle-reported rates for the given token' + + static flags = { + ...BaseCommand.flags, + } + + static args = [ + { + name: 'token', + required: true, + description: 'Token to get the rates for', + options: [CeloContract.StableToken], + default: CeloContract.StableToken, + }, + ] + + static example = ['rates StableToken', 'rates'] + + async run() { + const res = this.parse(GetRates) + const sortedOracles = await this.kit.contracts.getSortedOracles() + + const rates = await sortedOracles.getRates(res.args.token) + cli.table(rates, { + address: {}, + rate: { get: (r) => r.rate.toNumber() }, + }) + } +} diff --git a/packages/cli/src/commands/oracle/report.ts b/packages/cli/src/commands/oracle/report.ts new file mode 100644 index 00000000000..1b1e9846dab --- /dev/null +++ b/packages/cli/src/commands/oracle/report.ts @@ -0,0 +1,67 @@ +import { CeloContract } from '@celo/contractkit' +import { flags } from '@oclif/command' +import BigNumber from 'bignumber.js' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class ReportPrice extends BaseCommand { + static description = + 'Report the price of Celo Gold in a specified token (currently just Celo Dollar, aka: "StableToken")' + + static args = [ + { + name: 'token', + required: true, + default: CeloContract.StableToken, + description: 'Token to report on', + options: [CeloContract.StableToken], + }, + ] + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: 'Address of the oracle account' }), + numerator: flags.string({ + required: true, + description: 'Amount of the specified token equal to the amount of cGLD in the denominator', + }), + denominator: flags.string({ + required: true, + description: 'Amount of cGLD equal to the numerator. Defaults to 1 if left blank', + default: '1', + }), + } + + static example = [ + 'report StableToken --numerator 1.02 --from 0x8c349AAc7065a35B7166f2659d6C35D75A3893C1', + 'report StableToken --numerator 102 --denominator 100 --from 0x8c349AAc7065a35B7166f2659d6C35D75A3893C1', + 'report --numerator 0.99 --from 0x8c349AAc7065a35B7166f2659d6C35D75A3893C1', + ] + + async run() { + const res = this.parse(ReportPrice) + const sortedOracles = await this.kit.contracts.getSortedOracles() + let numerator = new BigNumber(res.flags.numerator) + let denominator = new BigNumber(res.flags.denominator) + if (numerator.decimalPlaces() > 0) { + const multiplier = new BigNumber(10).pow(numerator.decimalPlaces()) + numerator = numerator.multipliedBy(multiplier) + denominator = denominator.multipliedBy(multiplier) + } + + await displaySendTx( + 'sortedOracles.report', + await sortedOracles.report( + res.args.token, + numerator.toNumber(), + denominator.toNumber(), + res.flags.from + ) + ) + this.log( + `Reported oracle value of ${numerator.div(denominator).toFixed()} ${ + res.args.token + } for 1 CeloGold` + ) + } +} diff --git a/packages/cli/src/commands/transfer/dollars.ts b/packages/cli/src/commands/transfer/dollars.ts new file mode 100644 index 00000000000..4c7d26b0c0e --- /dev/null +++ b/packages/cli/src/commands/transfer/dollars.ts @@ -0,0 +1,34 @@ +import { flags } from '@oclif/command' +import BigNumber from 'bignumber.js' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class TransferDollars extends BaseCommand { + static description = 'Transfer Celo Dollars to a specified address.' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: 'Address of the sender' }), + to: Flags.address({ required: true, description: 'Address of the receiver' }), + value: flags.string({ required: true, description: 'Amount to transfer (in wei)' }), + } + + static examples = [ + 'dollars --from 0xa0Af2E71cECc248f4a7fD606F203467B500Dd53B --to 0x5409ed021d9299bf6814279a6a1411a7e866a631 --value 1000000000000000000', + ] + + async run() { + const res = this.parse(TransferDollars) + + const from: string = res.flags.from + const to: string = res.flags.to + const value = new BigNumber(res.flags.value) + + this.kit.defaultAccount = from + + const stableToken = await this.kit.contracts.getStableToken() + // Perform the transfer + await displaySendTx('dollar.Transfer', stableToken.transfer(to, value.toFixed())) + } +} diff --git a/packages/cli/src/commands/transfer/gold.ts b/packages/cli/src/commands/transfer/gold.ts new file mode 100644 index 00000000000..d56ae4ef7d5 --- /dev/null +++ b/packages/cli/src/commands/transfer/gold.ts @@ -0,0 +1,34 @@ +import { flags } from '@oclif/command' +import BigNumber from 'bignumber.js' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class TransferGold extends BaseCommand { + static description = 'Transfer Celo Gold to a specified address.' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: 'Address of the sender' }), + to: Flags.address({ required: true, description: 'Address of the receiver' }), + value: flags.string({ required: true, description: 'Amount to transfer (in wei)' }), + } + + static examples = [ + 'transfergold --from 0xa0Af2E71cECc248f4a7fD606F203467B500Dd53B --to 0x5409ed021d9299bf6814279a6a1411a7e866a631 --value 10000000000000000000', + ] + + async run() { + const res = this.parse(TransferGold) + + const from: string = res.flags.from + const to: string = res.flags.to + const value = new BigNumber(res.flags.value) + + this.kit.defaultAccount = from + const goldToken = await this.kit.contracts.getGoldToken() + + // Perform the transfer + await displaySendTx('gold.Transfer', goldToken.transfer(to, value.toFixed())) + } +} diff --git a/packages/cli/src/commands/validator/affiliate.ts b/packages/cli/src/commands/validator/affiliate.ts index 26dda68f8a4..13f0bc5b16a 100644 --- a/packages/cli/src/commands/validator/affiliate.ts +++ b/packages/cli/src/commands/validator/affiliate.ts @@ -5,7 +5,8 @@ import { displaySendTx } from '../../utils/cli' import { Args, Flags } from '../../utils/command' export default class ValidatorAffiliate extends BaseCommand { - static description = 'Affiliate to a ValidatorGroup' + static description = + "Affiliate a Validator with a Validator Group. This allows the Validator Group to add that Validator as a member. If the Validator is already a member of a Validator Group, affiliating with a different Group will remove the Validator from the first group's members." static flags = { ...BaseCommand.flags, diff --git a/packages/cli/src/commands/validator/deaffiliate.ts b/packages/cli/src/commands/validator/deaffiliate.ts index 54846cc13c4..6488364b012 100644 --- a/packages/cli/src/commands/validator/deaffiliate.ts +++ b/packages/cli/src/commands/validator/deaffiliate.ts @@ -4,7 +4,8 @@ import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' export default class ValidatorDeAffiliate extends BaseCommand { - static description = 'DeAffiliate to a ValidatorGroup' + static description = + 'Deaffiliate a Validator from a Validator Group, and remove it from the Group if it is also a member.' static flags = { ...BaseCommand.flags, diff --git a/packages/cli/src/commands/validator/deregister.ts b/packages/cli/src/commands/validator/deregister.ts index 9c6184c73f1..133d3ea2380 100644 --- a/packages/cli/src/commands/validator/deregister.ts +++ b/packages/cli/src/commands/validator/deregister.ts @@ -4,7 +4,8 @@ import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' export default class ValidatorDeregister extends BaseCommand { - static description = 'Deregister a Validator' + static description = + 'Deregister a Validator. Approximately 60 days after deregistration, the 10,000 Gold locked up to register the Validator will become possible to unlock. Note that deregistering a Validator will also deaffiliate and remove the Validator from any Group it may be an affiliate or member of.' static flags = { ...BaseCommand.flags, diff --git a/packages/cli/src/commands/validator/list.ts b/packages/cli/src/commands/validator/list.ts index af742764809..fd3530fb1c3 100644 --- a/packages/cli/src/commands/validator/list.ts +++ b/packages/cli/src/commands/validator/list.ts @@ -2,7 +2,8 @@ import { cli } from 'cli-ux' import { BaseCommand } from '../../base' export default class ValidatorList extends BaseCommand { - static description = 'List existing Validators' + static description = + 'List registered Validators, their name (if provided), affiliation, uptime score, and public keys used for validating.' static flags = { ...BaseCommand.flags, @@ -21,8 +22,10 @@ export default class ValidatorList extends BaseCommand { cli.table(validatorList, { address: {}, name: {}, - publicKey: {}, affiliation: {}, + score: { get: (v) => v.score.toFixed() }, + ecdsaPublicKey: {}, + blsPublicKey: {}, }) } } diff --git a/packages/cli/src/commands/validator/publicKey.ts b/packages/cli/src/commands/validator/publicKey.ts deleted file mode 100644 index cfe11ab125a..00000000000 --- a/packages/cli/src/commands/validator/publicKey.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { BaseCommand } from '../../base' -import { newCheckBuilder } from '../../utils/checks' -import { displaySendTx } from '../../utils/cli' -import { Flags } from '../../utils/command' -import { getPubKeyFromAddrAndWeb3 } from '../../utils/helpers' - -export default class ValidatorPublicKey extends BaseCommand { - static description = 'Manage BLS public key data for a validator' - - static flags = { - ...BaseCommand.flags, - from: Flags.address({ required: true, description: "Validator's address" }), - publicKey: Flags.publicKey({ required: true }), - } - - static examples = [ - 'publickey --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', - ] - async run() { - const res = this.parse(ValidatorPublicKey) - this.kit.defaultAccount = res.flags.from - const validators = await this.kit.contracts.getValidators() - const accounts = await this.kit.contracts.getAccounts() - - await newCheckBuilder(this, res.flags.from) - .isSignerOrAccount() - .canSignValidatorTxs() - .signerAccountIsValidator() - .runChecks() - - await displaySendTx( - 'updatePublicKeysData', - validators.updatePublicKeysData(res.flags.publicKey as any) - ) - - // register encryption key on accounts contract - // TODO: Use a different key data encryption - const pubKey = await getPubKeyFromAddrAndWeb3(res.flags.from, this.web3) - // TODO fix typing - const setKeyTx = accounts.setAccountDataEncryptionKey(pubKey as any) - await displaySendTx('Set encryption key', setKeyTx) - } -} diff --git a/packages/cli/src/commands/validator/register.ts b/packages/cli/src/commands/validator/register.ts index bc50dd33dd6..033778e308c 100644 --- a/packages/cli/src/commands/validator/register.ts +++ b/packages/cli/src/commands/validator/register.ts @@ -1,8 +1,8 @@ +import { addressToPublicKey } from '@celo/utils/lib/signatureUtils' import { BaseCommand } from '../../base' import { newCheckBuilder } from '../../utils/checks' 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' @@ -10,12 +10,14 @@ export default class ValidatorRegister extends BaseCommand { static flags = { ...BaseCommand.flags, from: Flags.address({ required: true, description: 'Address for the Validator' }), - publicKey: Flags.publicKey({ required: true }), + blsKey: Flags.blsPublicKey({ required: true }), + blsPop: Flags.blsProofOfPossession({ required: true }), } static examples = [ - 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --publicKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', + 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --blsKey 0x4fa3f67fc913878b068d1fa1cdddc54913d3bf988dbe5a36a20fa888f20d4894c408a6773f3d7bde11154f2a3076b700d345a42fd25a0e5e83f4db5586ac7979ac2053cd95d8f2efd3e959571ceccaa743e02cf4be3f5d7aaddb0b06fc9aff00 --blsPop 0xcdb77255037eb68897cd487fdd85388cbda448f617f874449d4b11588b0b7ad8ddc20d9bb450b513bb35664ea3923900', ] + async run() { const res = this.parse(ValidatorRegister) this.kit.defaultAccount = res.flags.from @@ -28,15 +30,17 @@ export default class ValidatorRegister extends BaseCommand { .canSignValidatorTxs() .signerMeetsValidatorBalanceRequirements() .runChecks() - - await displaySendTx( - 'registerValidator', - validators.registerValidator(res.flags.publicKey as any) + const tx = await validators.registerValidator( + res.flags.from, + res.flags.blsKey, + res.flags.blsPop ) + await displaySendTx('registerValidator', tx) + // register encryption key on accounts contract // TODO: Use a different key data encryption - const pubKey = await getPubKeyFromAddrAndWeb3(res.flags.from, this.web3) + const pubKey = await addressToPublicKey(res.flags.from, this.web3.eth.sign) // TODO fix typing const setKeyTx = accounts.setAccountDataEncryptionKey(pubKey as any) await displaySendTx('Set encryption key', setKeyTx) diff --git a/packages/cli/src/commands/validator/requirements.ts b/packages/cli/src/commands/validator/requirements.ts index 0cb871b268f..69aa4ac16f0 100644 --- a/packages/cli/src/commands/validator/requirements.ts +++ b/packages/cli/src/commands/validator/requirements.ts @@ -2,7 +2,8 @@ import { BaseCommand } from '../../base' import { printValueMap } from '../../utils/cli' export default class ValidatorRequirements extends BaseCommand { - static description = 'Get Requirements for Validators' + static description = + 'List the Locked Gold requirements for registering a Validator. This consists of a value, which is the amount of Celo Gold that needs to be locked in order to register, and a duration, which is the amount of time that Gold must stay locked following the deregistration of the Validator.' static flags = { ...BaseCommand.flags, diff --git a/packages/cli/src/commands/validator/show.ts b/packages/cli/src/commands/validator/show.ts index 17ed3bdcf29..1cde4b17516 100644 --- a/packages/cli/src/commands/validator/show.ts +++ b/packages/cli/src/commands/validator/show.ts @@ -5,7 +5,7 @@ import { printValueMap } from '../../utils/cli' import { Args } from '../../utils/command' export default class ValidatorShow extends BaseCommand { - static description = 'Show information about an existing Validator' + static description = 'Show information about a registered Validator.' static flags = { ...BaseCommand.flags, diff --git a/packages/cli/src/commands/validator/update-bls-public-key.ts b/packages/cli/src/commands/validator/update-bls-public-key.ts new file mode 100644 index 00000000000..3c31ca102a8 --- /dev/null +++ b/packages/cli/src/commands/validator/update-bls-public-key.ts @@ -0,0 +1,35 @@ +import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class ValidatorUpdateBlsPublicKey extends BaseCommand { + static description = + 'Update the BLS public key for a Validator to be used in consensus. Regular (ECDSA and BLS) key rotation is recommended for Validator operational security.' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: "Validator's address" }), + blsKey: Flags.blsPublicKey({ required: true }), + blsPop: Flags.blsProofOfPossession({ required: true }), + } + + static examples = [ + 'update-bls-key --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --blsKey 0x4fa3f67fc913878b068d1fa1cdddc54913d3bf988dbe5a36a20fa888f20d4894c408a6773f3d7bde11154f2a3076b700d345a42fd25a0e5e83f4db5586ac7979ac2053cd95d8f2efd3e959571ceccaa743e02cf4be3f5d7aaddb0b06fc9aff00 --blsPop 0xcdb77255037eb68897cd487fdd85388cbda448f617f874449d4b11588b0b7ad8ddc20d9bb450b513bb35664ea3923900', + ] + async run() { + const res = this.parse(ValidatorUpdateBlsPublicKey) + this.kit.defaultAccount = res.flags.from + const validators = await this.kit.contracts.getValidators() + await newCheckBuilder(this, res.flags.from) + .isSignerOrAccount() + .canSignValidatorTxs() + .signerAccountIsValidator() + .runChecks() + + await displaySendTx( + 'updateBlsPublicKey', + validators.updateBlsPublicKey(res.flags.blsKey as any, res.flags.blsPop as any) + ) + } +} diff --git a/packages/cli/src/commands/validatorgroup/commission.ts b/packages/cli/src/commands/validatorgroup/commission.ts index 19c7263bda5..fae157fb5e0 100644 --- a/packages/cli/src/commands/validatorgroup/commission.ts +++ b/packages/cli/src/commands/validatorgroup/commission.ts @@ -6,7 +6,8 @@ import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' export default class ValidatorGroupCommission extends BaseCommand { - static description = 'Update the commission for an existing validator group' + static description = + 'Update the commission for a registered Validator Group. This represents the share of the epoch rewards given to elected Validators that goes to the group they are a member of.' static flags = { ...BaseCommand.flags, diff --git a/packages/cli/src/commands/validatorgroup/deregister.ts b/packages/cli/src/commands/validatorgroup/deregister.ts index 57c0aeefdbb..bafb6cd95b6 100644 --- a/packages/cli/src/commands/validatorgroup/deregister.ts +++ b/packages/cli/src/commands/validatorgroup/deregister.ts @@ -4,7 +4,8 @@ import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' export default class ValidatorGroupDeRegister extends BaseCommand { - static description = 'Deregister a ValidatorGroup' + static description = + 'Deregister a Validator Group. Approximately 60 days after deregistration, the 10,000 Gold locked up to register the Validator Group will become possible to unlock. Note that the Group must be empty (i.e. no members) before deregistering.' static flags = { ...BaseCommand.flags, diff --git a/packages/cli/src/commands/validatorgroup/list.ts b/packages/cli/src/commands/validatorgroup/list.ts index 096520b8ff9..a7e6482e449 100644 --- a/packages/cli/src/commands/validatorgroup/list.ts +++ b/packages/cli/src/commands/validatorgroup/list.ts @@ -2,7 +2,8 @@ import { cli } from 'cli-ux' import { BaseCommand } from '../../base' export default class ValidatorGroupList extends BaseCommand { - static description = 'List existing Validator Groups' + static description = + 'List registered Validator Groups, their names (if provided), commission, and members.' static flags = { ...BaseCommand.flags, diff --git a/packages/cli/src/commands/validatorgroup/member.ts b/packages/cli/src/commands/validatorgroup/member.ts index c4130c66dae..7be1af0d60b 100644 --- a/packages/cli/src/commands/validatorgroup/member.ts +++ b/packages/cli/src/commands/validatorgroup/member.ts @@ -28,9 +28,9 @@ export default class ValidatorGroupMembers extends BaseCommand { static args: IArg[] = [Args.address('validatorAddress', { description: "Validator's address" })] static examples = [ - 'member --accept 0x97f7333c51897469e8d98e7af8653aab468050a3 ', - 'member --remove 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95', - 'member --reorder 3 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95', + 'member --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 --accept 0x97f7333c51897469e8d98e7af8653aab468050a3', + 'member --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 --remove 0x97f7333c51897469e8d98e7af8653aab468050a3', + 'member --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 --reorder 3 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95', ] async run() { diff --git a/packages/cli/src/commands/validatorgroup/register.ts b/packages/cli/src/commands/validatorgroup/register.ts index 4c9975a1a87..ef3366b71db 100644 --- a/packages/cli/src/commands/validatorgroup/register.ts +++ b/packages/cli/src/commands/validatorgroup/register.ts @@ -11,7 +11,11 @@ export default class ValidatorGroupRegister extends BaseCommand { static flags = { ...BaseCommand.flags, from: Flags.address({ required: true, description: 'Address for the Validator Group' }), - commission: flags.string({ required: true }), + commission: flags.string({ + required: true, + description: + 'The share of the epoch rewards given to elected Validators that goes to the group.', + }), } static examples = ['register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --commission 0.1'] diff --git a/packages/cli/src/test-utils/ganache.setup.ts b/packages/cli/src/test-utils/ganache.setup.ts index de02dd978c3..8e9006bf714 100644 --- a/packages/cli/src/test-utils/ganache.setup.ts +++ b/packages/cli/src/test-utils/ganache.setup.ts @@ -1,61 +1,6 @@ -// @ts-ignore -import * as ganache from '@celo/ganache-cli' +import baseSetup from '@celo/dev-utils/lib/ganache-setup' import * as path from 'path' -const MNEMONIC = 'concert load couple harbor equip island argue ramp clarify fence smart topic' - -export async function startGanache(datadir: string, opts: { verbose?: boolean } = {}) { - const logFn = opts.verbose - ? // tslint:disable-next-line: no-console - (...args: any[]) => console.log(...args) - : () => { - /*nothing*/ - } - - const server = ganache.server({ - default_balance_ether: 1000000, - logger: { - log: logFn, - }, - network_id: 1101, - db_path: datadir, - mnemonic: MNEMONIC, - gasLimit: 7000000, - allowUnlimitedContractSize: true, - }) - - await new Promise((resolve, reject) => { - server.listen(8545, (err: any, blockchain: any) => { - if (err) { - reject(err) - } else { - resolve(blockchain) - } - }) - }) - - return () => - new Promise((resolve, reject) => { - server.close((err: any) => { - if (err) { - reject(err) - } else { - resolve() - } - }) - }) -} - export default function setup() { - const DATADIR = path.resolve(path.join(__dirname, '../../.devchain')) - // console.log('Starting Ganache: datadir=', DATADIR) - return startGanache(DATADIR) - .then((stopGanache) => { - ;(global as any).stopGanache = stopGanache - }) - .catch((err) => { - console.error('Error starting ganache, Doing `yarn test:prepare` might help') - console.error(err) - process.exit(1) - }) + return baseSetup(path.resolve(path.join(__dirname, '../../.devchain'))) } diff --git a/packages/cli/src/test-utils/ganache.teardown.ts b/packages/cli/src/test-utils/ganache.teardown.ts index 27400b9a1aa..c37e9bb00a0 100644 --- a/packages/cli/src/test-utils/ganache.teardown.ts +++ b/packages/cli/src/test-utils/ganache.teardown.ts @@ -1,7 +1,2 @@ -export default function tearDown() { - console.log('Stopping ganache') - return (global as any).stopGanache().catch((err: any) => { - console.error('error stopping ganache') - console.error(err) - }) -} +import teardown from '@celo/dev-utils/lib/ganache-teardown' +export default teardown diff --git a/packages/cli/src/utils/checks.ts b/packages/cli/src/utils/checks.ts index 0e79ab09874..d51d3b21df4 100644 --- a/packages/cli/src/utils/checks.ts +++ b/packages/cli/src/utils/checks.ts @@ -73,7 +73,7 @@ class CheckBuilder { 'Signer can sign Validator Txs', this.withAccounts((lg) => lg - .activeValidationSignerToAccount(this.signer!) + .validatorSignerToAccount(this.signer!) .then(() => true) .catch(() => false) ) diff --git a/packages/cli/src/utils/command.ts b/packages/cli/src/utils/command.ts index dee09ae0690..36f48889364 100644 --- a/packages/cli/src/utils/command.ts +++ b/packages/cli/src/utils/command.ts @@ -1,3 +1,4 @@ +import { BLS_POP_SIZE, BLS_PUBLIC_KEY_SIZE } from '@celo/utils/lib/bls' import { URL_REGEX } from '@celo/utils/lib/io' import { flags } from '@oclif/command' import { CLIError } from '@oclif/errors' @@ -5,14 +6,21 @@ import { IArg, ParseFn } from '@oclif/parser/lib/args' import { pathExistsSync } from 'fs-extra' import Web3 from 'web3' -const parsePublicKey: ParseFn = (input) => { - // Check that the string starts with 0x and has byte length of ecdsa pub key (64 bytes) + bls pub key (48 bytes) + proof of pos (96 bytes) - if (Web3.utils.isHex(input) && input.length === 418 && input.startsWith('0x')) { +const parseBytes = (input: string, length: number, msg: string) => { + // Check that the string starts with 0x and has byte length of `length`. + if (Web3.utils.isHex(input) && input.length === length * 2 + 2 && input.startsWith('0x')) { return input } else { - throw new CLIError(`${input} is not a public key`) + throw new CLIError(msg) } } + +const parseBlsPublicKey: ParseFn = (input) => { + return parseBytes(input, BLS_PUBLIC_KEY_SIZE, `${input} is not a BLS public key`) +} +const parseBlsProofOfPossession: ParseFn = (input) => { + return parseBytes(input, BLS_POP_SIZE, `${input} is not a BLS proof-of-possession`) +} const parseAddress: ParseFn = (input) => { if (Web3.utils.isAddress(input)) { return input @@ -54,9 +62,14 @@ export const Flags = { description: 'Account Address', helpValue: '0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', }), - publicKey: flags.build({ - parse: parsePublicKey, - description: 'Public Key', + blsPublicKey: flags.build({ + parse: parseBlsPublicKey, + description: 'BLS Public Key', + helpValue: '0x', + }), + blsProofOfPossession: flags.build({ + parse: parseBlsProofOfPossession, + description: 'BLS Proof-of-Possession', helpValue: '0x', }), url: flags.build({ diff --git a/packages/cli/src/utils/helpers.ts b/packages/cli/src/utils/helpers.ts index d95c1354c57..ae0400c9df3 100644 --- a/packages/cli/src/utils/helpers.ts +++ b/packages/cli/src/utils/helpers.ts @@ -1,30 +1,9 @@ -import { eqAddress } from '@celo/utils/lib/address' -import ethjsutil from 'ethereumjs-util' import Web3 from 'web3' import { Block } from 'web3/eth/types' import { failWith } from './cli' -import assert = require('assert') - -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 async function nodeIsSynced(web3: Web3): Promise { + return true if (process.env.NO_SYNCCHECK) { return true } diff --git a/packages/cli/src/utils/identity.ts b/packages/cli/src/utils/identity.ts index 3fe62723fd4..788e955f43e 100644 --- a/packages/cli/src/utils/identity.ts +++ b/packages/cli/src/utils/identity.ts @@ -1,7 +1,10 @@ import { ContractKit } from '@celo/contractkit' import { ClaimTypes, IdentityMetadataWrapper } from '@celo/contractkit/lib/identity' -import { Claim, hashOfClaim, verifyClaim } from '@celo/contractkit/lib/identity/claims/claim' -import { VERIFIABLE_CLAIM_TYPES } from '@celo/contractkit/lib/identity/claims/types' +import { Claim, validateClaim, verifyClaim } from '@celo/contractkit/lib/identity/claims/claim' +import { + VALIDATABLE_CLAIM_TYPES, + VERIFIABLE_CLAIM_TYPES, +} from '@celo/contractkit/lib/identity/claims/types' import { concurrentMap } from '@celo/utils/lib/async' import { NativeSigner } from '@celo/utils/lib/signatureUtils' import { cli } from 'cli-ux' @@ -38,17 +41,17 @@ export abstract class ClaimCommand extends BaseCommand { } } + protected get signer() { + const res = this.parse(this.self) + const address = toChecksumAddress(res.flags.from) + return NativeSigner(this.kit.web3.eth.sign, address) + } + protected async addClaim(metadata: IdentityMetadataWrapper, claim: Claim) { try { cli.action.start(`Add claim`) - const res = this.parse(this.self) - const address = toChecksumAddress(res.flags.from) - const signedClaim = await metadata.addClaim( - claim, - NativeSigner(this.kit.web3.eth.sign, address) - ) + await metadata.addClaim(claim, this.signer) cli.action.stop() - return signedClaim } catch (error) { cli.action.stop(`Error: ${error}`) throw error @@ -80,35 +83,50 @@ export const claimFlags = { export const claimArgs = [Args.file('file', { description: 'Path of the metadata file' })] export const displayMetadata = async (metadata: IdentityMetadataWrapper, kit: ContractKit) => { - const accounts = await kit.contracts.getAccounts() + const metadataURLGetter = async (address: string) => { + const accounts = await kit.contracts.getAccounts() + return accounts.getMetadataURL(address) + } + const data = await concurrentMap(5, metadata.claims, async (claim) => { - const verifiable = VERIFIABLE_CLAIM_TYPES.includes(claim.payload.type) - const status = await verifyClaim(claim, metadata.data.meta.address, accounts.getMetadataURL) + const verifiable = VERIFIABLE_CLAIM_TYPES.includes(claim.type) + const validatable = VALIDATABLE_CLAIM_TYPES.includes(claim.type) + const status = verifiable + ? await verifyClaim(claim, metadata.data.meta.address, metadataURLGetter) + : validatable + ? await validateClaim(claim, metadata.data.meta.address, kit) + : 'N/A' let extra = '' - switch (claim.payload.type) { + switch (claim.type) { case ClaimTypes.ATTESTATION_SERVICE_URL: - extra = `URL: ${claim.payload.url}` + extra = `URL: ${claim.url}` break case ClaimTypes.DOMAIN: - extra = `Domain: ${claim.payload.domain}` + extra = `Domain: ${claim.domain}` break case ClaimTypes.KEYBASE: - extra = `Username: ${claim.payload.username}` + extra = `Username: ${claim.username}` break case ClaimTypes.NAME: - extra = `Name: "${claim.payload.name}"` + extra = `Name: "${claim.name}"` break default: - extra = JSON.stringify(claim.payload) + extra = JSON.stringify(claim) break } return { - type: claim.payload.type, + type: claim.type, extra, - verifiable: verifiable ? 'Yes' : 'No', - status: verifiable ? (status ? `Invalid: ${status}` : 'Valid!') : 'N/A', - createdAt: moment.unix(claim.payload.timestamp).fromNow(), - hash: hashOfClaim(claim.payload), + status: verifiable + ? status + ? `Could not verify: ${status}` + : 'Verified!' + : validatable + ? status + ? `Invalid: ${status}` + : `Valid!` + : 'N/A', + createdAt: moment.unix(claim.timestamp).fromNow(), } }) @@ -117,10 +135,8 @@ export const displayMetadata = async (metadata: IdentityMetadataWrapper, kit: Co { type: { header: 'Type' }, extra: { header: 'Value' }, - verifiable: { header: 'Verifiable' }, status: { header: 'Status' }, createdAt: { header: 'Created At' }, - hash: { header: 'Hash' }, }, {} ) diff --git a/packages/cli/src/utils/lockedgold.ts b/packages/cli/src/utils/lockedgold.ts index 9e9aee7645e..f37c8976e69 100644 --- a/packages/cli/src/utils/lockedgold.ts +++ b/packages/cli/src/utils/lockedgold.ts @@ -1,10 +1,6 @@ export const LockedGoldArgs = { - pendingWithdrawalIndexArg: { - name: 'pendingWithdrawalIndex', - description: 'index of pending withdrawal whose unlocking period has passed', - }, valueArg: { name: 'value', - description: 'unit amount of Celo Gold (cGLD)', + description: 'The unit amount of Celo Gold (cGLD)', }, } diff --git a/packages/contractkit/README.md b/packages/contractkit/README.md index 1f357c9a263..39ade7b7bc3 100644 --- a/packages/contractkit/README.md +++ b/packages/contractkit/README.md @@ -6,7 +6,7 @@ ContractKit supports the following functionality: - Connect to a node - Access web3 object to interact with node's Json RPC API -- Send Transaction with celo's extra fields: (gasCurrency) +- Send Transaction with celo's extra fields: (feeCurrency) - Simple interface to interact with cGold and cDollar - Simple interface to interact with Celo Core contracts - Utilities @@ -52,7 +52,7 @@ async function getKit(myAddress: string) { // default from kit.defaultAccount = myAddress // paid gas in celo dollars - await kit.setGasCurrency(CeloContract.StableToken) + await kit.setFeeCurrency(CeloContract.StableToken) return kit } ``` @@ -121,7 +121,7 @@ The complete list of Celo Core contracts is: - LockedGold - Escrow - Exchange -- GasCurrencyWhitelist +- FeeCurrencyWhitelist - GasPriceMinimum - GoldToken - Governance @@ -146,10 +146,11 @@ const goldTokenAddress = await kit.registry.addressFor(CeloContract.GoldToken) ### Sending Custom Transactions -Celo transaction object is not the same as Ethereum's. There are two new fields present: +Celo transaction object is not the same as Ethereum's. There are three new fields present: -- gasCurrency (address of the ERC20 contract to use to pay for gas) -- gasFeeRecipient (address of the beneficiary for the gas, the full node) +- feeCurrency (address of the ERC20 contract to use to pay for gas and the gateway fee) +- gatewayFeeRecipient (coinbase address of the full serving the light client's trasactions) +- gatewayFee (value paid to the gateway fee recipient, denominated in the fee currency) This means that using `web3.eth.sendTransaction` or `myContract.methods.transfer().send()` should be avoided. diff --git a/packages/contractkit/jest.config.js b/packages/contractkit/jest.config.js index aa5c4c41f55..c30f58fd842 100644 --- a/packages/contractkit/jest.config.js +++ b/packages/contractkit/jest.config.js @@ -2,7 +2,7 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', testMatch: ['/src/**/?(*.)+(spec|test).ts?(x)'], - setupFilesAfterEnv: ['/src/test-utils/matchers.ts'], + setupFilesAfterEnv: ['@celo/dev-utils/lib/matchers'], globalSetup: '/src/test-utils/ganache.setup.ts', globalTeardown: '/src/test-utils/ganache.teardown.ts', } diff --git a/packages/contractkit/package.json b/packages/contractkit/package.json index f20cff2dfd8..2b9f0a8a09c 100644 --- a/packages/contractkit/package.json +++ b/packages/contractkit/package.json @@ -20,7 +20,8 @@ "clean:all": "yarn clean && rm -rf src/generated", "build:gen": "yarn --cwd ../protocol build", "prepublishOnly": "yarn build:gen && yarn build", - "test:prepare": "yarn --cwd ../protocol devchain generate .devchain --migration_override src/test-utils/migration-override.json", + "test:reset": "yarn --cwd ../protocol devchain generate .devchain --migration_override ../dev-utils/src/migration-override.json", + "test:livechain": "yarn --cwd ../protocol devchain run .devchain", "test": "jest --runInBand", "lint": "tslint -c tslint.json --project ." }, @@ -40,6 +41,7 @@ "web3-utils": "1.0.0-beta.37" }, "devDependencies": { + "@celo/dev-utils": "0.0.1-dev", "@celo/ganache-cli": "git+https://github.com/celo-org/ganache-cli.git#9d77e02", "@celo/protocol": "1.0.0", "@types/debug": "^4.1.5", diff --git a/packages/contractkit/src/base.ts b/packages/contractkit/src/base.ts index 9ebd96abc9b..6d0cced9e70 100644 --- a/packages/contractkit/src/base.ts +++ b/packages/contractkit/src/base.ts @@ -8,7 +8,7 @@ export enum CeloContract { EpochRewards = 'EpochRewards', Escrow = 'Escrow', Exchange = 'Exchange', - GasCurrencyWhitelist = 'GasCurrencyWhitelist', + FeeCurrencyWhitelist = 'FeeCurrencyWhitelist', GasPriceMinimum = 'GasPriceMinimum', GoldToken = 'GoldToken', Governance = 'Governance', diff --git a/packages/contractkit/src/contract-cache.ts b/packages/contractkit/src/contract-cache.ts index 760f6359889..f35ac479023 100644 --- a/packages/contractkit/src/contract-cache.ts +++ b/packages/contractkit/src/contract-cache.ts @@ -24,7 +24,7 @@ const WrapperFactories = { // [CeloContract.EpochRewards]?: EpochRewardsWrapper, [CeloContract.Escrow]: EscrowWrapper, [CeloContract.Exchange]: ExchangeWrapper, - // [CeloContract.GasCurrencyWhitelist]: GasCurrencyWhitelistWrapper, + // [CeloContract.FeeCurrencyWhitelist]: FeeCurrencyWhitelistWrapper, [CeloContract.GasPriceMinimum]: GasPriceMinimumWrapper, [CeloContract.GoldToken]: GoldTokenWrapper, [CeloContract.Governance]: GovernanceWrapper, @@ -49,7 +49,7 @@ interface WrapperCacheMap { // [CeloContract.EpochRewards]?: EpochRewardsWrapper [CeloContract.Escrow]?: EscrowWrapper [CeloContract.Exchange]?: ExchangeWrapper - // [CeloContract.GasCurrencyWhitelist]?: GasCurrencyWhitelistWrapper, + // [CeloContract.FeeCurrencyWhitelist]?: FeeCurrencyWhitelistWrapper, [CeloContract.GasPriceMinimum]?: GasPriceMinimumWrapper [CeloContract.GoldToken]?: GoldTokenWrapper [CeloContract.Governance]?: GovernanceWrapper @@ -95,8 +95,8 @@ export class WrapperCache { getExchange() { return this.getContract(CeloContract.Exchange) } - // getGasCurrencyWhitelist() { - // return this.getWrapper(CeloContract.GasCurrencyWhitelist, newGasCurrencyWhitelist) + // getFeeCurrencyWhitelist() { + // return this.getWrapper(CeloContract.FeeCurrencyWhitelist, newFeeCurrencyWhitelist) // } getGasPriceMinimum() { return this.getContract(CeloContract.GasPriceMinimum) diff --git a/packages/contractkit/src/identity/claims/account.test.ts b/packages/contractkit/src/identity/claims/account.test.ts index ca61a57e86c..0f7f6ff0083 100644 --- a/packages/contractkit/src/identity/claims/account.test.ts +++ b/packages/contractkit/src/identity/claims/account.test.ts @@ -1,11 +1,11 @@ +import { ACCOUNT_ADDRESSES, ACCOUNT_PRIVATE_KEYS } from '@celo/dev-utils/lib/ganache-setup' +import { testWithGanache } from '@celo/dev-utils/lib/ganache-test' import { privateKeyToAddress, privateKeyToPublicKey } from '@celo/utils/lib/address' import { NativeSigner } from '@celo/utils/lib/signatureUtils' import { newKitFromWeb3 } from '../../kit' -import { testWithGanache } from '../../test-utils/ganache-test' -import { ACCOUNT_ADDRESSES, ACCOUNT_PRIVATE_KEYS } from '../../test-utils/ganache.setup' import { IdentityMetadataWrapper } from '../metadata' import { createAccountClaim, MetadataURLGetter } from './account' -import { SignedClaim, verifyClaim } from './claim' +import { Claim, verifyClaim } from './claim' testWithGanache('Account claims', (web3) => { const kit = newKitFromWeb3(web3) @@ -52,7 +52,7 @@ testWithGanache('Account claims', (web3) => { }) describe('verifying', () => { - let signedClaim: SignedClaim + let claim: Claim let otherMetadata: IdentityMetadataWrapper let metadataUrlGetter: MetadataURLGetter @@ -68,10 +68,8 @@ testWithGanache('Account claims', (web3) => { IdentityMetadataWrapper.fetchFromURL = () => Promise.resolve(otherMetadata) const metadata = IdentityMetadataWrapper.fromEmpty(address) - signedClaim = await metadata.addClaim( - createAccountClaim(otherAddress), - NativeSigner(kit.web3.eth.sign, address) - ) + claim = createAccountClaim(otherAddress) + await metadata.addClaim(claim, NativeSigner(kit.web3.eth.sign, address)) }) afterEach(() => { @@ -84,14 +82,14 @@ testWithGanache('Account claims', (web3) => { }) it('indicates that the metadata url could not be retrieved', async () => { - const error = await verifyClaim(signedClaim, address, metadataUrlGetter) + const error = await verifyClaim(claim, address, metadataUrlGetter) expect(error).toContain('could not be retrieved') }) }) describe('when the metadata URL is set, but does not contain the address claim', () => { it('indicates that the metadata does not contain the counter claim', async () => { - const error = await verifyClaim(signedClaim, address, metadataUrlGetter) + const error = await verifyClaim(claim, address, metadataUrlGetter) expect(error).toContain('did not claim') }) }) @@ -105,7 +103,7 @@ testWithGanache('Account claims', (web3) => { }) it('returns undefined succesfully', async () => { - const error = await verifyClaim(signedClaim, address, metadataUrlGetter) + const error = await verifyClaim(claim, address, metadataUrlGetter) expect(error).toBeUndefined() }) }) diff --git a/packages/contractkit/src/identity/claims/attestation-service-url.test.ts b/packages/contractkit/src/identity/claims/attestation-service-url.test.ts new file mode 100644 index 00000000000..491aa519176 --- /dev/null +++ b/packages/contractkit/src/identity/claims/attestation-service-url.test.ts @@ -0,0 +1,36 @@ +import { ACCOUNT_ADDRESSES } from '@celo/dev-utils/lib/ganache-setup' +import { testWithGanache } from '@celo/dev-utils/lib/ganache-test' +import { NativeSigner } from '@celo/utils/lib/signatureUtils' +import { newKitFromWeb3 } from '../../kit' +import { IdentityMetadataWrapper } from '../metadata' +import { createAttestationServiceURLClaim } from './attestation-service-url' + +testWithGanache('AttestationServiceURL claims', (web3) => { + const kit = newKitFromWeb3(web3) + const url = 'https://example.com' + const address = ACCOUNT_ADDRESSES[0] + + it('can make a claim', async () => { + const metadata = IdentityMetadataWrapper.fromEmpty(address) + await metadata.addClaim( + createAttestationServiceURLClaim(url), + NativeSigner(kit.web3.eth.sign, address) + ) + }) + + it('can overwrite the existing claim', async () => { + const metadata = IdentityMetadataWrapper.fromEmpty(address) + await metadata.addClaim( + createAttestationServiceURLClaim(url), + NativeSigner(kit.web3.eth.sign, address) + ) + + const newUrl = 'https://example.com/new' + await metadata.addClaim( + createAttestationServiceURLClaim(newUrl), + NativeSigner(kit.web3.eth.sign, address) + ) + + expect(metadata.claims).toHaveLength(1) + }) +}) diff --git a/packages/contractkit/src/identity/claims/attestation-service-url.ts b/packages/contractkit/src/identity/claims/attestation-service-url.ts new file mode 100644 index 00000000000..fdee00d15f5 --- /dev/null +++ b/packages/contractkit/src/identity/claims/attestation-service-url.ts @@ -0,0 +1,80 @@ +import { eqAddress } from '@celo/utils/lib/address' +import { AttestationServiceStatusResponseType, UrlType } from '@celo/utils/lib/io' +import { verifySignature } from '@celo/utils/lib/signatureUtils' +import fetch from 'cross-fetch' +import { isLeft } from 'fp-ts/lib/Either' +import * as t from 'io-ts' +import { Address } from '../../base' +import { ContractKit } from '../../kit' +import { ClaimTypes, now, TimestampType } from './types' + +const SIGNATURE_PREFIX = 'attestation-service-status-signature:' + +export const AttestationServiceURLClaimType = t.type({ + type: t.literal(ClaimTypes.ATTESTATION_SERVICE_URL), + timestamp: TimestampType, + url: UrlType, +}) + +export type AttestationServiceURLClaim = t.TypeOf + +export const createAttestationServiceURLClaim = (url: string): AttestationServiceURLClaim => ({ + url, + timestamp: now(), + type: ClaimTypes.ATTESTATION_SERVICE_URL, +}) + +export async function validateAttestationServiceUrl( + claim: AttestationServiceURLClaim, + address: Address, + kit: ContractKit +): Promise { + try { + const randomMessage = kit.web3.utils.randomHex(32) + + const url = claim.url + '/status?messageToSign=' + randomMessage + + const resp = await fetch(url) + + if (!resp.ok) { + return `Could not request sucessfully from ${url}", received status ${resp.status}` + } + + const jsonResp = await resp.json() + + const parsedResponse = AttestationServiceStatusResponseType.decode(jsonResp) + + if (isLeft(parsedResponse)) { + return `Response from ${url} could not be parsed successfully` + } + + const claimedAccountAddress = parsedResponse.right.accountAddress + if (!eqAddress(claimedAccountAddress, address)) { + return `The service claims ${claimedAccountAddress}, but metadata claims ${address}` + } + + const accounts = await kit.contracts.getAccounts() + + const attestationKeyAddress = await accounts.getAttestationSigner(address) + + // Uncomment this once we opt in by specifying an attestation key + // if (attestationKeyAddress === '0x0' || eqAddress(address, attestationKeyAddress)) { + // return `The account has not specified a separate attestation key` + // } + + if ( + !parsedResponse.right.signature || + !verifySignature( + SIGNATURE_PREFIX + randomMessage, + parsedResponse.right.signature, + attestationKeyAddress + ) + ) { + return `The service's attestation key differs from the smart contract registered one` + } + + return + } catch (error) { + return `Could not validate attestation service claim: ${error}` + } +} diff --git a/packages/contractkit/src/identity/claims/claim.ts b/packages/contractkit/src/identity/claims/claim.ts index c3258d4e4dd..c49bfcbbfa5 100644 --- a/packages/contractkit/src/identity/claims/claim.ts +++ b/packages/contractkit/src/identity/claims/claim.ts @@ -1,16 +1,15 @@ -import { JSONStringType, UrlType } from '@celo/utils/lib/io' -import { hashMessage, parseSignature } from '@celo/utils/lib/signatureUtils' +import { hashMessage } from '@celo/utils/lib/signatureUtils' import * as t from 'io-ts' +import { ContractKit } from '../../kit' import { AccountClaim, AccountClaimType, MetadataURLGetter, verifyAccountClaim } from './account' +import { + AttestationServiceURLClaim, + AttestationServiceURLClaimType, + validateAttestationServiceUrl, +} from './attestation-service-url' import { KeybaseClaim, KeybaseClaimType, verifyKeybaseClaim } from './keybase' import { ClaimTypes, now, SignatureType, TimestampType } from './types' -const AttestationServiceURLClaimType = t.type({ - type: t.literal(ClaimTypes.ATTESTATION_SERVICE_URL), - timestamp: TimestampType, - url: UrlType, -}) - const DomainClaimType = t.type({ type: t.literal(ClaimTypes.DOMAIN), timestamp: TimestampType, @@ -30,18 +29,12 @@ export const ClaimType = t.union([ KeybaseClaimType, NameClaimType, ]) -export const SignedClaimType = t.type({ - payload: ClaimType, - signature: SignatureType, -}) -export const SerializedSignedClaimType = t.type({ - payload: JSONStringType, +export const SignedClaimType = t.type({ + claim: ClaimType, signature: SignatureType, }) -export type SignedClaim = t.TypeOf -export type AttestationServiceURLClaim = t.TypeOf export type DomainClaim = t.TypeOf export type NameClaim = t.TypeOf export type Claim = @@ -61,22 +54,11 @@ export type ClaimPayload = K extends typeof ClaimTypes.DOM ? AttestationServiceURLClaim : AccountClaim -export const isOfType = (type: K) => ( - data: SignedClaim['payload'] -): data is ClaimPayload => data.type === type - -export function verifySignature(serializedPayload: string, signature: string, signer: string) { - const hash = hashMessage(serializedPayload) - try { - parseSignature(hash, signature, signer) - return true - } catch (error) { - return false - } -} +export const isOfType = (type: K) => (data: Claim): data is ClaimPayload => + data.type === type /** - * Verifies a claim made by an account + * Verifies a claim made by an account, i.e. whether a claim can be verified to be correct * @param claim The claim to verify * @param address The address that is making the claim * @param metadataURLGetter A function that can retrieve the metadata URL for a given account address, @@ -84,15 +66,31 @@ export function verifySignature(serializedPayload: string, signature: string, si * @returns If valid, returns undefined. If invalid or unable to verify, returns a string with the error */ export async function verifyClaim( - claim: SignedClaim, + claim: Claim, address: string, metadataURLGetter: MetadataURLGetter ) { - switch (claim.payload.type) { + switch (claim.type) { case ClaimTypes.KEYBASE: - return verifyKeybaseClaim(claim.payload, address) + return verifyKeybaseClaim(claim, address) case ClaimTypes.ACCOUNT: - return verifyAccountClaim(claim.payload, address, metadataURLGetter) + return verifyAccountClaim(claim, address, metadataURLGetter) + default: + break + } + return +} + +/** + * Validates a claim made by an account, i.e. whether the claim is usable + * @param claim The claim to validate + * @param address The address that is making the claim + * @returns If valid, returns undefined. If invalid or unable to validate, returns a string with the error + */ +export async function validateClaim(claim: Claim, address: string, kit: ContractKit) { + switch (claim.type) { + case ClaimTypes.ATTESTATION_SERVICE_URL: + return validateAttestationServiceUrl(claim, address, kit) default: break } @@ -103,16 +101,15 @@ export function hashOfClaim(claim: Claim) { return hashMessage(serializeClaim(claim)) } +export function hashOfClaims(claims: Claim[]) { + const hashes = claims.map(hashOfClaim) + return hashMessage(hashes.join('')) +} + export function serializeClaim(claim: Claim) { return JSON.stringify(claim) } -export const createAttestationServiceURLClaim = (url: string): AttestationServiceURLClaim => ({ - url, - timestamp: now(), - type: ClaimTypes.ATTESTATION_SERVICE_URL, -}) - export const createNameClaim = (name: string): NameClaim => ({ name, timestamp: now(), diff --git a/packages/contractkit/src/identity/claims/keybase.ts b/packages/contractkit/src/identity/claims/keybase.ts index eb70fc74922..d8a555b91a0 100644 --- a/packages/contractkit/src/identity/claims/keybase.ts +++ b/packages/contractkit/src/identity/claims/keybase.ts @@ -1,7 +1,8 @@ import { Address } from '@celo/utils/lib/address' +import { verifySignature } from '@celo/utils/lib/signatureUtils' import { isLeft } from 'fp-ts/lib/Either' import * as t from 'io-ts' -import { serializeClaim, SignedClaimType, verifySignature } from './claim' +import { hashOfClaim, SignedClaimType } from './claim' import { ClaimTypes, now, TimestampType } from './types' export const KeybaseClaimType = t.type({ @@ -39,7 +40,7 @@ export async function verifyKeybaseClaim( } const hasValidSiganture = verifySignature( - serializeClaim(parsedClaim.right.payload), + hashOfClaim(parsedClaim.right.claim), parsedClaim.right.signature, signer ) @@ -48,7 +49,7 @@ export async function verifyKeybaseClaim( return 'Claim does not contain a valid signature' } - const parsedKeybaseClaim = KeybaseClaimType.decode(parsedClaim.right.payload) + const parsedKeybaseClaim = KeybaseClaimType.decode(parsedClaim.right.claim) if (isLeft(parsedKeybaseClaim)) { return 'Hosted claim is not a Keybase claim' } diff --git a/packages/contractkit/src/identity/claims/types.ts b/packages/contractkit/src/identity/claims/types.ts index 9f87682785b..96cc0cd9923 100644 --- a/packages/contractkit/src/identity/claims/types.ts +++ b/packages/contractkit/src/identity/claims/types.ts @@ -16,3 +16,8 @@ export enum ClaimTypes { } export const VERIFIABLE_CLAIM_TYPES = [ClaimTypes.KEYBASE, ClaimTypes.ACCOUNT] + +// Claims whose status can be validated +export const VALIDATABLE_CLAIM_TYPES = [ClaimTypes.ATTESTATION_SERVICE_URL] + +export const SINGULAR_CLAIM_TYPES = [ClaimTypes.NAME, ClaimTypes.ATTESTATION_SERVICE_URL] diff --git a/packages/contractkit/src/identity/metadata.test.ts b/packages/contractkit/src/identity/metadata.test.ts index d793cf33c07..42876bfea38 100644 --- a/packages/contractkit/src/identity/metadata.test.ts +++ b/packages/contractkit/src/identity/metadata.test.ts @@ -1,7 +1,7 @@ +import { ACCOUNT_ADDRESSES } from '@celo/dev-utils/lib/ganache-setup' +import { testWithGanache } from '@celo/dev-utils/lib/ganache-test' import { NativeSigner } from '@celo/utils/lib/signatureUtils' import { newKitFromWeb3 } from '../kit' -import { testWithGanache } from '../test-utils/ganache-test' -import { ACCOUNT_ADDRESSES } from '../test-utils/ganache.setup' import { createNameClaim } from './claims/claim' import { ClaimTypes, IdentityMetadataWrapper } from './metadata' diff --git a/packages/contractkit/src/identity/metadata.ts b/packages/contractkit/src/identity/metadata.ts index 47ce0383192..1e0c15a1567 100644 --- a/packages/contractkit/src/identity/metadata.ts +++ b/packages/contractkit/src/identity/metadata.ts @@ -1,30 +1,21 @@ -import { AddressType } from '@celo/utils/lib/io' -import { Signer } from '@celo/utils/lib/signatureUtils' +import { AddressType, SignatureType } from '@celo/utils/lib/io' +import { Signer, verifySignature } from '@celo/utils/lib/signatureUtils' import fetch from 'cross-fetch' import { isLeft } from 'fp-ts/lib/Either' import { readFileSync } from 'fs' import * as t from 'io-ts' import { PathReporter } from 'io-ts/lib/PathReporter' -import { - Claim, - ClaimPayload, - hashOfClaim, - isOfType, - serializeClaim, - SerializedSignedClaimType, - SignedClaim, - SignedClaimType, - verifySignature, -} from './claims/claim' -import { ClaimTypes } from './claims/types' +import { Claim, ClaimPayload, ClaimType, hashOfClaims, isOfType } from './claims/claim' +import { ClaimTypes, SINGULAR_CLAIM_TYPES } from './claims/types' export { ClaimTypes } from './claims/types' const MetaType = t.type({ address: AddressType, + signature: SignatureType, }) export const IdentityMetadataType = t.type({ - claims: t.array(SignedClaimType), + claims: t.array(ClaimType), meta: MetaType, }) export type IdentityMetadata = t.TypeOf @@ -37,6 +28,7 @@ export class IdentityMetadataWrapper { claims: [], meta: { address, + signature: '', }, }) } @@ -56,42 +48,34 @@ export class IdentityMetadataWrapper { static fromRawString(rawData: string) { const data = JSON.parse(rawData) - const validatedMeta = MetaType.decode(data.meta) - if (isLeft(validatedMeta)) { - throw new Error('Meta payload is invalid: ' + PathReporter.report(validatedMeta).join(', ')) - } - - const address = validatedMeta.right.address + const validatedData = IdentityMetadataType.decode(data) - const verifySignatureAndParse = (claim: any) => { - const parsedClaim = SerializedSignedClaimType.decode(claim) - if (isLeft(parsedClaim)) { - throw new Error(`Serialized claim is not of the right format: ${claim}`) - } - if (!verifySignature(parsedClaim.right.payload, parsedClaim.right.signature, address)) { - throw new Error(`Could not verify signature of the claim: ${claim.payload}`) - } - return { - payload: JSON.parse(parsedClaim.right.payload), - signature: parsedClaim.right.signature, - } + if (isLeft(validatedData)) { + // TODO: We could probably return a more useful error in the future + throw new Error(PathReporter.report(validatedData).join(', ')) } - // TODO: Validate that data.claims is an array - const parsedData = { - claims: data.claims.map(verifySignatureAndParse), - meta: validatedMeta.right, + // Verify signature on the data + const claims = validatedData.right.claims + const hash = hashOfClaims(claims) + if ( + claims.length > 0 && + !verifySignature(hash, validatedData.right.meta.signature, validatedData.right.meta.address) + ) { + throw new Error('Signature could not be validated') } - // Here we are mostly validating the shape of the claims - const validatedData = IdentityMetadataType.decode(parsedData) + const res = new IdentityMetadataWrapper(validatedData.right) - if (isLeft(validatedData)) { - // TODO: We could probably return a more useful error in the future - throw new Error(PathReporter.report(validatedData).join(', ')) - } + // Verify that singular claim types appear at most once + SINGULAR_CLAIM_TYPES.forEach((claimType) => { + const results = res.filterClaims(claimType) + if (results.length > 1) { + throw new Error(`Found ${results.length} claims of type ${claimType}, should be at most 1`) + } + }) - return new IdentityMetadataWrapper(validatedData.right) + return res } constructor(data: IdentityMetadata) { @@ -102,12 +86,13 @@ export class IdentityMetadataWrapper { return this.data.claims } + hashOfClaims() { + return hashOfClaims(this.data.claims) + } + toString() { return JSON.stringify({ - claims: this.data.claims.map((claim) => ({ - payload: serializeClaim(claim.payload), - signature: claim.signature, - })), + claims: this.data.claims, meta: this.data.meta, }) } @@ -123,25 +108,23 @@ export class IdentityMetadataWrapper { default: break } - const signedClaim = await this.signClaim(claim, signer) - this.data.claims.push(signedClaim) - return signedClaim + + if (SINGULAR_CLAIM_TYPES.includes(claim.type)) { + const index = this.data.claims.findIndex(isOfType(claim.type)) + if (index !== -1) { + this.data.claims.splice(index, 1) + } + } + + this.data.claims.push(claim) + this.data.meta.signature = await signer.sign(this.hashOfClaims()) } findClaim(type: K): ClaimPayload | undefined { - return this.data.claims.map((x) => x.payload).find(isOfType(type)) + return this.data.claims.find(isOfType(type)) } filterClaims(type: K): Array> { - return this.data.claims.map((x) => x.payload).filter(isOfType(type)) - } - - private signClaim = async (claim: Claim, signer: Signer): Promise => { - const messageHash = hashOfClaim(claim) - const signature = await signer.sign(messageHash) - return { - payload: claim, - signature, - } + return this.data.claims.filter(isOfType(type)) } } diff --git a/packages/contractkit/src/index.ts b/packages/contractkit/src/index.ts index 7ca7df24dff..426d38658d7 100644 --- a/packages/contractkit/src/index.ts +++ b/packages/contractkit/src/index.ts @@ -3,7 +3,7 @@ import Web3 from 'web3' export { Address, AllContracts, CeloContract, CeloToken, NULL_ADDRESS } from './base' export { IdentityMetadataWrapper } from './identity' export * from './kit' -export { CeloTransactionObject } from './wrappers/BaseWrapper' +export { CeloTransactionObject, CeloTransactionParams } from './wrappers/BaseWrapper' /** * Creates a new web3 instance diff --git a/packages/contractkit/src/kit.test.ts b/packages/contractkit/src/kit.test.ts index f0c0008e065..0ec09fea4db 100644 --- a/packages/contractkit/src/kit.test.ts +++ b/packages/contractkit/src/kit.test.ts @@ -73,11 +73,11 @@ describe('kit.sendTransactionObject()', () => { test('should forward txoptions to txo.send()', async () => { const txo = txoStub() - await kit.sendTransactionObject(txo, { gas: 555, gasCurrency: 'XXX', from: '0xAAFFF' }) + await kit.sendTransactionObject(txo, { gas: 555, feeCurrency: 'XXX', from: '0xAAFFF' }) expect(txo.send).toBeCalledWith({ gasPrice: '0', gas: 555, - gasCurrency: 'XXX', + feeCurrency: 'XXX', from: '0xAAFFF', }) }) diff --git a/packages/contractkit/src/kit.ts b/packages/contractkit/src/kit.ts index d2d2c0e30df..630a4f8a118 100644 --- a/packages/contractkit/src/kit.ts +++ b/packages/contractkit/src/kit.ts @@ -51,7 +51,7 @@ export interface NetworkConfig { export interface KitOptions { gasInflationFactor: number - gasCurrency: Address | null + feeCurrency: Address | null from?: Address } @@ -66,7 +66,7 @@ export class ContractKit { private config: KitOptions constructor(readonly web3: Web3) { this.config = { - gasCurrency: null, + feeCurrency: null, gasInflationFactor: 1.3, } @@ -120,8 +120,8 @@ export class ContractKit { * Set CeloToken to use to pay for gas fees * @param token cUsd or cGold */ - async setGasCurrency(token: CeloToken): Promise { - this.config.gasCurrency = + async setFeeCurrency(token: CeloToken): Promise { + this.config.feeCurrency = token === CeloContract.GoldToken ? null : await this.registry.addressFor(token) } @@ -153,19 +153,19 @@ export class ContractKit { } /** - * Set the ERC20 address for the token to use to pay for gas fees. + * Set the ERC20 address for the token to use to pay for transaction fees. * The ERC20 must be whitelisted for gas. * * Set to `null` to use cGold * * @param address ERC20 address */ - set defaultGasCurrency(address: Address | null) { - this.config.gasCurrency = address + set defaultFeeCurrency(address: Address | null) { + this.config.feeCurrency = address } - get defaultGasCurrency() { - return this.config.gasCurrency + get defaultFeeCurrency() { + return this.config.feeCurrency } isListening(): Promise { @@ -230,8 +230,8 @@ export class ContractKit { gasPrice: '0', } - if (this.config.gasCurrency) { - defaultTx.gasCurrency = this.config.gasCurrency + if (this.config.feeCurrency) { + defaultTx.feeCurrency = this.config.feeCurrency } return { diff --git a/packages/contractkit/src/providers/celo-private-keys-subprovider.ts b/packages/contractkit/src/providers/celo-private-keys-subprovider.ts index 7137c255260..6f42c9c1518 100644 --- a/packages/contractkit/src/providers/celo-private-keys-subprovider.ts +++ b/packages/contractkit/src/providers/celo-private-keys-subprovider.ts @@ -1,4 +1,5 @@ import { Callback, ErrorCallback, PrivateKeyWalletSubprovider } from '@0x/subproviders' +import BigNumber from 'bignumber.js' import debugFactory from 'debug' import { JSONRPCRequestPayload } from 'ethereum-types' import Web3 from 'web3' @@ -11,6 +12,10 @@ const debug = debugFactory('kit:providers:celo-private-keys-subprovider') // https://github.com/celo-org/celo-blockchain/blob/027dba2e4584936cc5a8e8993e4e27d28d5247b8/internal/ethapi/api.go#L1222 const DefaultGasLimit = 90000 +// Default gateway fee to send the serving full-node on each transaction. +// TODO(nategraf): Provide a method of fecthing the gateway fee value from the full-node peer. +const DefaultGatewayFee = new BigNumber(10000) + function getPrivateKeyWithout0xPrefix(privateKey: string) { return privateKey.toLowerCase().startsWith('0x') ? privateKey.substring(2) : privateKey } @@ -41,7 +46,7 @@ export class CeloPrivateKeysWalletProvider extends PrivateKeyWalletSubprovider { private readonly accountAddressToPrivateKey = new Map() private chainId: number | null = null - private gasFeeRecipient: string | null = null + private gatewayFeeRecipient: string | null = null constructor(readonly privateKey: string) { // This won't accept a privateKey with 0x prefix and will call that an invalid key. @@ -93,7 +98,8 @@ export class CeloPrivateKeysWalletProvider extends PrivateKeyWalletSubprovider { } } - public async signTransactionAsync(txParams: CeloPartialTxParams): Promise { + public async signTransactionAsync(txParamsInput: CeloPartialTxParams): Promise { + const txParams = { ...txParamsInput } // Make a copy of the input so it can be mutated. debug('signTransactionAsync: txParams are %o', txParams) if (!this.canSign(txParams.from)) { // If `handleRequest` works correctly then this code path should never trigger. @@ -114,20 +120,20 @@ export class CeloPrivateKeysWalletProvider extends PrivateKeyWalletSubprovider { txParams.nonce = await this.getNonce(txParams.from) } - if (isEmpty(txParams.gasFeeRecipient)) { - txParams.gasFeeRecipient = await this.getCoinbase() - if (isEmpty(txParams.gasFeeRecipient)) { - // Fail early. The validator nodes will reject a transaction missing - // gas fee recipient anyways. - throw new Error( - 'Gas fee recipient is missing, cannot retrieve it' + - ' from web3.eth.getCoinbase() either cannot process transaction' - ) - } + if (isEmpty(txParams.gatewayFeeRecipient)) { + txParams.gatewayFeeRecipient = await this.getCoinbase() + } + if (!isEmpty(txParams.gatewayFeeRecipient) && isEmpty(txParams.gatewayFee)) { + txParams.gatewayFee = DefaultGatewayFee.toString(16) } + debug( + 'Gateway fee for the transaction is %s paid to %s', + txParams.gatewayFee, + txParams.gatewayFeeRecipient + ) if (isEmpty(txParams.gasPrice)) { - txParams.gasPrice = await this.getGasPrice(txParams.gasCurrency) + txParams.gasPrice = await this.getGasPrice(txParams.feeCurrency) } debug('Gas price for the transaction is %s', txParams.gasPrice) @@ -180,32 +186,32 @@ export class CeloPrivateKeysWalletProvider extends PrivateKeyWalletSubprovider { } private async getCoinbase(): Promise { - if (this.gasFeeRecipient === null) { + if (this.gatewayFeeRecipient === null) { debug('getCoinbase fetching Coinbase...') // Reference: https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_coinbase const result = await this.emitPayloadAsync({ method: 'eth_coinbase', params: [], }) - this.gasFeeRecipient = result.result.toString() - debug('getCoinbase gas fee recipient is %s', this.gasFeeRecipient) + this.gatewayFeeRecipient = result.result.toString() + debug('getCoinbase gateway fee recipient is %s', this.gatewayFeeRecipient) } - if (this.gasFeeRecipient == null) { + if (this.gatewayFeeRecipient == null) { throw new Error( `Coinbase is null, we are not connected to a full node, cannot sign transactions locally` ) } - return this.gasFeeRecipient + return this.gatewayFeeRecipient } - private async getGasPrice(gasCurrency: string | undefined): Promise { + private async getGasPrice(feeCurrency: string | undefined): Promise { // Gold Token - if (!gasCurrency) { + if (!feeCurrency) { return this.getGasPriceInCeloGold() } throw new Error( `celo-private-keys-subprovider@getGasPrice: gas price for ` + - `currency ${gasCurrency} cannot be computed in the CeloPrivateKeysWalletProvider, ` + + `currency ${feeCurrency} cannot be computed in the CeloPrivateKeysWalletProvider, ` + ' pass it explicitly' ) } diff --git a/packages/contractkit/src/test-utils/PromiEventStub.ts b/packages/contractkit/src/test-utils/PromiEventStub.ts index 2897ddb52d0..c1dd41c5ad0 100644 --- a/packages/contractkit/src/test-utils/PromiEventStub.ts +++ b/packages/contractkit/src/test-utils/PromiEventStub.ts @@ -12,6 +12,9 @@ interface PromiEventStub extends PromiEvent { export function promiEventSpy(): PromiEventStub { const ee = new EventEmitter() const pe: PromiEventStub = { + finally: () => { + throw new Error('not implemented') + }, catch: () => { throw new Error('not implemented') }, diff --git a/packages/contractkit/src/test-utils/ganache-test.ts b/packages/contractkit/src/test-utils/ganache-test.ts deleted file mode 100644 index d1805081f8c..00000000000 --- a/packages/contractkit/src/test-utils/ganache-test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import * as fs from 'fs' -import Web3 from 'web3' -import { JsonRPCResponse } from 'web3/providers' -import { injectDebugProvider } from '../providers/debug-provider' - -// This file specifies accounts available when ganache is running. These are derived -// from the MNEMONIC -export const NetworkConfig = JSON.parse( - fs.readFileSync('src/test-utils/migration-override.json').toString() -) - -export function jsonRpcCall(web3: Web3, method: string, params: any[]): Promise { - return new Promise((resolve, reject) => { - web3.currentProvider.send( - { - id: new Date().getTime(), - jsonrpc: '2.0', - method, - params, - }, - (err: Error | null, res?: JsonRPCResponse) => { - if (err) { - reject(err) - } else if (!res) { - reject(new Error('no response')) - } else if (res.error) { - reject( - new Error( - `Failed JsonRPCResponse: method: ${method} params: ${params} error: ${JSON.stringify( - res.error - )}` - ) - ) - } else { - resolve(res.result) - } - } - ) - }) -} - -export function evmRevert(web3: Web3, snapId: string): Promise { - return jsonRpcCall(web3, 'evm_revert', [snapId]) -} - -export function evmSnapshot(web3: Web3) { - return jsonRpcCall(web3, 'evm_snapshot', []) -} - -export function testWithGanache(name: string, fn: (web3: Web3) => void) { - const web3 = new Web3('http://localhost:8545') - injectDebugProvider(web3) - - describe(name, () => { - let snapId: string | null = null - - beforeEach(async () => { - if (snapId != null) { - await evmRevert(web3, snapId) - } - snapId = await evmSnapshot(web3) - }) - - afterAll(async () => { - if (snapId != null) { - await evmRevert(web3, snapId) - } - }) - - fn(web3) - }) -} diff --git a/packages/contractkit/src/test-utils/ganache.setup.ts b/packages/contractkit/src/test-utils/ganache.setup.ts index 44cbb558393..1f646b1b054 100644 --- a/packages/contractkit/src/test-utils/ganache.setup.ts +++ b/packages/contractkit/src/test-utils/ganache.setup.ts @@ -1,84 +1,8 @@ -// @ts-ignore -import * as ganache from '@celo/ganache-cli' +import baseSetup from '@celo/dev-utils/lib/ganache-setup' +// Has to import the matchers somewhere so that typescript knows the matchers have been made available +import _unused from '@celo/dev-utils/lib/matchers' import * as path from 'path' -const MNEMONIC = 'concert load couple harbor equip island argue ramp clarify fence smart topic' -export const ACCOUNT_PRIVATE_KEYS = [ - '0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d', - '0x5d862464fe9303452126c8bc94274b8c5f9874cbd219789b3eb2128075a76f72', - '0xdf02719c4df8b9b8ac7f551fcb5d9ef48fa27eef7a66453879f4d8fdc6e78fb1', - '0xff12e391b79415e941a94de3bf3a9aee577aed0731e297d5cfa0b8a1e02fa1d0', - '0x752dd9cf65e68cfaba7d60225cbdbc1f4729dd5e5507def72815ed0d8abc6249', - '0xefb595a0178eb79a8df953f87c5148402a224cdf725e88c0146727c6aceadccd', - '0x83c6d2cc5ddcf9711a6d59b417dc20eb48afd58d45290099e5987e3d768f328f', - '0xbb2d3f7c9583780a7d3904a2f55d792707c345f21de1bacb2d389934d82796b2', - '0xb2fd4d29c1390b71b8795ae81196bfd60293adf99f9d32a0aff06288fcdac55f', - '0x23cb7121166b9a2f93ae0b7c05bde02eae50d64449b2cbb42bc84e9d38d6cc89', -] -export const ACCOUNT_ADDRESSES = [ - '0x5409ED021D9299bf6814279A6A1411A7e866A631', - '0x6Ecbe1DB9EF729CBe972C83Fb886247691Fb6beb', - '0xE36Ea790bc9d7AB70C55260C66D52b1eca985f84', - '0xE834EC434DABA538cd1b9Fe1582052B880BD7e63', - '0x78dc5D2D739606d31509C31d654056A45185ECb6', - '0xA8dDa8d7F5310E4A9E24F8eBA77E091Ac264f872', - '0x06cEf8E666768cC40Cc78CF93d9611019dDcB628', - '0x4404ac8bd8F9618D27Ad2f1485AA1B2cFD82482D', - '0x7457d5E02197480Db681D3fdF256c7acA21bDc12', - '0x91c987bf62D25945dB517BDAa840A6c661374402', -] -export async function startGanache(datadir: string, opts: { verbose?: boolean } = {}) { - const logFn = opts.verbose - ? // tslint:disable-next-line: no-console - (...args: any[]) => console.log(...args) - : () => { - /*nothing*/ - } - - const server = ganache.server({ - default_balance_ether: 1000000, - logger: { - log: logFn, - }, - network_id: 1101, - db_path: datadir, - mnemonic: MNEMONIC, - gasLimit: 10000000, - allowUnlimitedContractSize: true, - }) - - await new Promise((resolve, reject) => { - server.listen(8545, (err: any, blockchain: any) => { - if (err) { - reject(err) - } else { - resolve(blockchain) - } - }) - }) - - return () => - new Promise((resolve, reject) => { - server.close((err: any) => { - if (err) { - reject(err) - } else { - resolve() - } - }) - }) -} - export default function setup() { - const DATADIR = path.resolve(path.join(__dirname, '../../.devchain')) - // console.log('Starting Ganache: datadir=', DATADIR) - return startGanache(DATADIR) - .then((stopGanache) => { - ;(global as any).stopGanache = stopGanache - }) - .catch((err) => { - console.error('Error starting ganache, Doing `yarn test:prepare` might help') - console.error(err) - process.exit(1) - }) + return baseSetup(path.resolve(path.join(__dirname, '../../.devchain'))) } diff --git a/packages/contractkit/src/test-utils/ganache.teardown.ts b/packages/contractkit/src/test-utils/ganache.teardown.ts index 27400b9a1aa..c37e9bb00a0 100644 --- a/packages/contractkit/src/test-utils/ganache.teardown.ts +++ b/packages/contractkit/src/test-utils/ganache.teardown.ts @@ -1,7 +1,2 @@ -export default function tearDown() { - console.log('Stopping ganache') - return (global as any).stopGanache().catch((err: any) => { - console.error('error stopping ganache') - console.error(err) - }) -} +import teardown from '@celo/dev-utils/lib/ganache-teardown' +export default teardown diff --git a/packages/contractkit/src/test-utils/matchers.ts b/packages/contractkit/src/test-utils/matchers.ts deleted file mode 100644 index 992831e6c95..00000000000 --- a/packages/contractkit/src/test-utils/matchers.ts +++ /dev/null @@ -1,42 +0,0 @@ -import BigNumber from 'bignumber.js' - -declare global { - namespace jest { - interface Matchers { - toBeBigNumber(): R - toEqBigNumber(expected: BigNumber | string | number): R - } - } -} - -expect.extend({ - toBeBigNumber(received: any) { - const pass = BigNumber.isBigNumber(received) - if (pass) { - return { - message: () => `expected ${received} not to be BigNumber`, - pass: true, - } - } else { - return { - message: () => `expected ${received} to be bigNumber`, - pass: false, - } - } - }, - toEqBigNumber(received: BigNumber, _expected: BigNumber | string | number) { - const expected = new BigNumber(_expected) - const pass = expected.eq(received) - if (pass) { - return { - message: () => `expected ${received.toString()} not to equal ${expected.toString()}`, - pass: true, - } - } else { - return { - message: () => `expected ${received.toString()} to equal ${expected.toString()}`, - pass: false, - } - } - }, -}) diff --git a/packages/contractkit/src/utils/signing-utils.ts b/packages/contractkit/src/utils/signing-utils.ts index 6aa843b951b..2c12334d8a3 100644 --- a/packages/contractkit/src/utils/signing-utils.ts +++ b/packages/contractkit/src/utils/signing-utils.ts @@ -52,8 +52,9 @@ export async function signTransaction(txn: any, privateKey: string) { transaction.data = tx.data || '0x' transaction.value = tx.value || '0x' transaction.chainId = '0x' + Number(tx.chainId).toString(16) - transaction.gasCurrency = tx.gasCurrency || '0x' - transaction.gasFeeRecipient = tx.gasFeeRecipient || '0x' + transaction.feeCurrency = tx.feeCurrency || '0x' + transaction.gatewayFeeRecipient = tx.gatewayFeeRecipient || '0x' + transaction.gatewayFee = tx.gatewayFee || '0x' // This order should match the order in Geth. // https://github.com/celo-org/celo-blockchain/blob/027dba2e4584936cc5a8e8993e4e27d28d5247b8/core/types/transaction.go#L65 @@ -61,8 +62,9 @@ export async function signTransaction(txn: any, privateKey: string) { Bytes.fromNat(transaction.nonce), Bytes.fromNat(transaction.gasPrice), Bytes.fromNat(transaction.gas), - transaction.gasCurrency.toLowerCase(), - transaction.gasFeeRecipient.toLowerCase(), + transaction.feeCurrency.toLowerCase(), + transaction.gatewayFeeRecipient.toLowerCase(), + Bytes.fromNat(transaction.gatewayFee), transaction.to.toLowerCase(), Bytes.fromNat(transaction.value), transaction.data, @@ -79,21 +81,21 @@ export async function signTransaction(txn: any, privateKey: string) { ) const rawTx = RLP.decode(rlpEncoded) - .slice(0, 8) + .slice(0, 9) .concat(Account.decodeSignature(signature)) - rawTx[8] = makeEven(trimLeadingZero(rawTx[8])) rawTx[9] = makeEven(trimLeadingZero(rawTx[9])) rawTx[10] = makeEven(trimLeadingZero(rawTx[10])) + rawTx[11] = makeEven(trimLeadingZero(rawTx[11])) const rawTransaction = RLP.encode(rawTx) const values = RLP.decode(rawTransaction) result = { messageHash: hash, - v: trimLeadingZero(values[8]), - r: trimLeadingZero(values[9]), - s: trimLeadingZero(values[10]), + v: trimLeadingZero(values[9]), + r: trimLeadingZero(values[10]), + s: trimLeadingZero(values[11]), rawTransaction, } } catch (e) { @@ -133,18 +135,19 @@ export function recoverTransaction(rawTx: string): [CeloTx, string] { nonce: rawValues[0].toLowerCase() === '0x' ? 0 : parseInt(rawValues[0], 16), gasPrice: rawValues[1].toLowerCase() === '0x' ? 0 : parseInt(rawValues[1], 16), gas: rawValues[2].toLowerCase() === '0x' ? 0 : parseInt(rawValues[2], 16), - gasCurrency: rawValues[3], - gasFeeRecipient: rawValues[4], - to: rawValues[5], - value: rawValues[6], - data: rawValues[7], - chainId: rawValues[8], + feeCurrency: rawValues[3], + gatewayFeeRecipient: rawValues[4], + gatewayFee: rawValues[5], + to: rawValues[6], + value: rawValues[7], + data: rawValues[8], + chainId: rawValues[9], } - const signature = Account.encodeSignature(rawValues.slice(8, 11)) - const recovery = Bytes.toNumber(rawValues[8]) + const signature = Account.encodeSignature(rawValues.slice(9, 12)) + const recovery = Bytes.toNumber(rawValues[9]) // tslint:disable-next-line:no-bitwise const extraData = recovery < 35 ? [] : [Bytes.fromNumber((recovery - 35) >> 1), '0x', '0x'] - const signingData = rawValues.slice(0, 8).concat(extraData) + const signingData = rawValues.slice(0, 9).concat(extraData) const signingDataHex = RLP.encode(signingData) const signer = Account.recover(Hash.keccak256(signingDataHex), signature) return [celoTx, signer] diff --git a/packages/contractkit/src/utils/signing.test.ts b/packages/contractkit/src/utils/signing.test.ts index 9940d9e1043..cb5ad282814 100644 --- a/packages/contractkit/src/utils/signing.test.ts +++ b/packages/contractkit/src/utils/signing.test.ts @@ -1,6 +1,6 @@ +import { ACCOUNT_ADDRESSES, ACCOUNT_PRIVATE_KEYS } from '@celo/dev-utils/lib/ganache-setup' +import { testWithGanache } from '@celo/dev-utils/lib/ganache-test' import { LocalSigner, NativeSigner, parseSignature } from '@celo/utils/lib/signatureUtils' -import { testWithGanache } from '../test-utils/ganache-test' -import { ACCOUNT_ADDRESSES, ACCOUNT_PRIVATE_KEYS } from '../test-utils/ganache.setup' // This only really tests signatureUtils in @celo/utils, but is tested here // to avoid the web3/ganache setup in @celo/utils diff --git a/packages/contractkit/src/utils/tx-signing.test.ts b/packages/contractkit/src/utils/tx-signing.test.ts index 072f0738ba0..7c5f98c09f7 100644 --- a/packages/contractkit/src/utils/tx-signing.test.ts +++ b/packages/contractkit/src/utils/tx-signing.test.ts @@ -1,10 +1,10 @@ +import { testWithGanache } from '@celo/dev-utils/lib/ganache-test' import debugFactory from 'debug' import * as util from 'util' import Web3 from 'web3' import { Provider } from 'web3/providers' import { generateAccountAddressFromPrivateKey } from '../providers/celo-private-keys-subprovider' import { CeloProvider } from '../providers/celo-provider' -import { testWithGanache } from '../test-utils/ganache-test' import { recoverTransaction } from './signing-utils' import { CeloTx } from './tx-signing' import { addLocalAccount } from './web3-utils' @@ -61,25 +61,35 @@ async function verifyLocalSigning(web3: Web3, celoTransaction: CeloTx): Promise< parseInt(celoTransaction.gasPrice.toString(), 16) ) } - if (celoTransaction.gasCurrency != null) { + if (celoTransaction.feeCurrency != null) { debug( - 'Checking gas Currency actual %o expected %o', - signedCeloTransaction.gasCurrency, - celoTransaction.gasCurrency + 'Checking fee currency actual %o expected %o', + signedCeloTransaction.feeCurrency, + celoTransaction.feeCurrency ) - expect(signedCeloTransaction.gasCurrency!.toLowerCase()).toEqual( - celoTransaction.gasCurrency.toLowerCase() + expect(signedCeloTransaction.feeCurrency!.toLowerCase()).toEqual( + celoTransaction.feeCurrency.toLowerCase() ) } - if (celoTransaction.gasFeeRecipient != null) { + if (celoTransaction.gatewayFeeRecipient != null) { debug( - 'Checking gas fee recipient actual ' + - `${signedCeloTransaction.gasFeeRecipient} expected ${celoTransaction.gasFeeRecipient}` + 'Checking gateway fee recipient actual ' + + `${signedCeloTransaction.gatewayFeeRecipient} expected ${ + celoTransaction.gatewayFeeRecipient + }` ) - expect(signedCeloTransaction.gasFeeRecipient!.toLowerCase()).toEqual( - celoTransaction.gasFeeRecipient.toLowerCase() + expect(signedCeloTransaction.gatewayFeeRecipient!.toLowerCase()).toEqual( + celoTransaction.gatewayFeeRecipient.toLowerCase() ) } + if (celoTransaction.gatewayFee != null) { + debug( + 'Checking gateway fee value actual %o expected %o', + signedCeloTransaction.gatewayFee, + celoTransaction.gatewayFee.toString() + ) + expect(signedCeloTransaction.gatewayFee).toEqual(celoTransaction.gatewayFee.toString()) + } if (celoTransaction.data != null) { debug(`Checking data actual ${signedCeloTransaction.data} expected ${celoTransaction.data}`) expect(signedCeloTransaction.data!.toLowerCase()).toEqual(celoTransaction.data.toLowerCase()) @@ -96,25 +106,29 @@ async function verifyLocalSigningInAllPermutations( const badNonce = 100 const gas = 10 const gasPrice = 99 - const gasCurrency = '0x124356' - const gasFeeRecipient = '0x1234' + const feeCurrency = '0x124356' + const gatewayFeeRecipient = '0x1234' + const gatewayFee = '0x5678' const data = '0xabcdef' + // tslint:disable:no-bitwise // Test all possible combinations for rigor. for (let i = 0; i < 128; i++) { const celoTransaction: CeloTx = { from, to, value: amountInWei, - nonce: i % 2 === 0 ? nonce : undefined, - gas: i % 4 === 0 ? gas : undefined, - gasPrice: i % 8 === 0 ? gasPrice : undefined, - gasCurrency: i % 16 === 0 ? gasCurrency : undefined, - gasFeeRecipient: i % 32 === 0 ? gasFeeRecipient : undefined, - data: i % 64 === 0 ? data : undefined, + nonce: i & 1 ? nonce : undefined, + gas: i & 2 ? gas : undefined, + gasPrice: i & 4 ? gasPrice : undefined, + feeCurrency: i & 8 ? feeCurrency : undefined, + gatewayFeeRecipient: i & 16 ? gatewayFeeRecipient : undefined, + gatewayFee: i & 32 ? gatewayFee : undefined, + data: i & 64 ? data : undefined, } await verifyLocalSigning(web3, celoTransaction) } + // tslint:enable:no-bitwise // A special case. // An incorrect nonce will only work, if no implict calls to estimate gas are required. diff --git a/packages/contractkit/src/utils/tx-signing.ts b/packages/contractkit/src/utils/tx-signing.ts index 29850b85877..ee48e94e7e2 100644 --- a/packages/contractkit/src/utils/tx-signing.ts +++ b/packages/contractkit/src/utils/tx-signing.ts @@ -2,11 +2,13 @@ import { PartialTxParams } from '@0x/subproviders' import { Tx } from 'web3/eth/types' export interface CeloTx extends Tx { - gasCurrency?: string - gasFeeRecipient?: string + feeCurrency?: string + gatewayFeeRecipient?: string + gatewayFee?: string } export interface CeloPartialTxParams extends PartialTxParams { - gasCurrency?: string - gasFeeRecipient?: string + feeCurrency?: string + gatewayFeeRecipient?: string + gatewayFee?: string } diff --git a/packages/contractkit/src/web3-contract-cache.ts b/packages/contractkit/src/web3-contract-cache.ts index 2eab4113c6e..a719346b0e7 100644 --- a/packages/contractkit/src/web3-contract-cache.ts +++ b/packages/contractkit/src/web3-contract-cache.ts @@ -7,7 +7,7 @@ import { newElection } from './generated/Election' import { newEpochRewards } from './generated/EpochRewards' import { newEscrow } from './generated/Escrow' import { newExchange } from './generated/Exchange' -import { newGasCurrencyWhitelist } from './generated/GasCurrencyWhitelist' +import { newFeeCurrencyWhitelist } from './generated/FeeCurrencyWhitelist' import { newGasPriceMinimum } from './generated/GasPriceMinimum' import { newGoldToken } from './generated/GoldToken' import { newGovernance } from './generated/Governance' @@ -30,7 +30,7 @@ const ContractFactories = { [CeloContract.EpochRewards]: newEpochRewards, [CeloContract.Escrow]: newEscrow, [CeloContract.Exchange]: newExchange, - [CeloContract.GasCurrencyWhitelist]: newGasCurrencyWhitelist, + [CeloContract.FeeCurrencyWhitelist]: newFeeCurrencyWhitelist, [CeloContract.GasPriceMinimum]: newGasPriceMinimum, [CeloContract.GoldToken]: newGoldToken, [CeloContract.Governance]: newGovernance, @@ -79,8 +79,8 @@ export class Web3ContractCache { getExchange() { return this.getContract(CeloContract.Exchange) } - getGasCurrencyWhitelist() { - return this.getContract(CeloContract.GasCurrencyWhitelist) + getFeeCurrencyWhitelist() { + return this.getContract(CeloContract.FeeCurrencyWhitelist) } getGasPriceMinimum() { return this.getContract(CeloContract.GasPriceMinimum) diff --git a/packages/contractkit/src/wrappers/Accounts.test.ts b/packages/contractkit/src/wrappers/Accounts.test.ts new file mode 100644 index 00000000000..5063015339b --- /dev/null +++ b/packages/contractkit/src/wrappers/Accounts.test.ts @@ -0,0 +1,74 @@ +import { testWithGanache } from '@celo/dev-utils/lib/ganache-test' +import { parseSignature } from '@celo/utils/lib/signatureUtils' +import Web3 from 'web3' +import { newKitFromWeb3 } from '../kit' +import { AccountsWrapper } from './Accounts' +import { LockedGoldWrapper } from './LockedGold' +import { ValidatorsWrapper } from './Validators' + +/* +TEST NOTES: +- In migrations: The only account that has cUSD is accounts[0] +*/ + +const minLockedGoldValue = Web3.utils.toWei('10000', 'ether') // 10k gold + +// Random hex strings +const blsPublicKey = + '0x4fa3f67fc913878b068d1fa1cdddc54913d3bf988dbe5a36a20fa888f20d4894c408a6773f3d7bde11154f2a3076b700d345a42fd25a0e5e83f4db5586ac7979ac2053cd95d8f2efd3e959571ceccaa743e02cf4be3f5d7aaddb0b06fc9aff00' +const blsPoP = + '0xcdb77255037eb68897cd487fdd85388cbda448f617f874449d4b11588b0b7ad8ddc20d9bb450b513bb35664ea3923900' + +testWithGanache('Accounts Wrapper', (web3) => { + const kit = newKitFromWeb3(web3) + let accounts: string[] = [] + let accountsInstance: AccountsWrapper + let validators: ValidatorsWrapper + let lockedGold: LockedGoldWrapper + + const registerAccountWithLockedGold = async (account: string) => { + if (!(await accountsInstance.isAccount(account))) { + await accountsInstance.createAccount().sendAndWaitForReceipt({ from: account }) + } + await lockedGold.lock().sendAndWaitForReceipt({ from: account, value: minLockedGoldValue }) + } + + const getParsedSignatureOfAddress = async (address: string, signer: string) => { + const addressHash = web3.utils.soliditySha3({ type: 'address', value: address }) + const signature = await web3.eth.sign(addressHash, signer) + return parseSignature(addressHash, signature, signer) + } + + beforeAll(async () => { + accounts = await web3.eth.getAccounts() + validators = await kit.contracts.getValidators() + lockedGold = await kit.contracts.getLockedGold() + accountsInstance = await kit.contracts.getAccounts() + }) + + const setupValidator = async (validatorAccount: string) => { + await registerAccountWithLockedGold(validatorAccount) + await (await validators + // @ts-ignore + .registerValidator(validatorAccount, blsPublicKey, blsPoP)).sendAndWaitForReceipt({ + from: validatorAccount, + }) + } + + test('SBAT authorize validator key when not a validator', async () => { + const account = accounts[0] + const signer = accounts[1] + await accountsInstance.createAccount() + const sig = await getParsedSignatureOfAddress(account, signer) + await accountsInstance.authorizeValidatorSigner(signer, sig) + }) + + test('SBAT authorize validator key when a validator', async () => { + const account = accounts[0] + const signer = accounts[1] + await accountsInstance.createAccount() + await setupValidator(account) + const sig = await getParsedSignatureOfAddress(account, signer) + await accountsInstance.authorizeValidatorSigner(signer, sig) + }) +}) diff --git a/packages/contractkit/src/wrappers/Accounts.ts b/packages/contractkit/src/wrappers/Accounts.ts index 01afb340612..bf369c9818a 100644 --- a/packages/contractkit/src/wrappers/Accounts.ts +++ b/packages/contractkit/src/wrappers/Accounts.ts @@ -1,3 +1,12 @@ +import { + hashMessageWithPrefix, + LocalSigner, + NativeSigner, + parseSignature, + Signature, + signedMessageToPublicKey, + Signer, +} from '@celo/utils/lib/signatureUtils' import Web3 from 'web3' import { Address } from '../base' import { Accounts } from '../generated/types/Accounts' @@ -9,11 +18,6 @@ import { toTransactionObject, } from '../wrappers/BaseWrapper' -enum SignerRole { - Attestation, - Validation, - Vote, -} /** * Contract for handling deposits needed for voting. */ @@ -40,12 +44,21 @@ export class AccountsWrapper extends BaseWrapper { this.contract.methods.getVoteSigner ) /** - * Returns the validation signere for the specified account. + * Returns the validator signer for the specified account. * @param account The address of the account. * @return The address with which the account can register a validator or group. */ - getValidationSigner: (account: string) => Promise
= proxyCall( - this.contract.methods.getValidationSigner + getValidatorSigner: (account: string) => Promise
= proxyCall( + this.contract.methods.getValidatorSigner + ) + + /** + * Returns the account address given the signer for voting + * @param signer Address that is authorized to sign the tx as voter + * @return The Account address + */ + voteSignerToAccount: (signer: Address) => Promise
= proxyCall( + this.contract.methods.voteSignerToAccount ) /** @@ -53,8 +66,18 @@ export class AccountsWrapper extends BaseWrapper { * @param signer Address that is authorized to sign the tx as validator * @return The Account address */ - activeValidationSignerToAccount: (signer: Address) => Promise
= proxyCall( - this.contract.methods.activeValidationSignerToAccount + validatorSignerToAccount: (signer: Address) => Promise
= proxyCall( + this.contract.methods.validatorSignerToAccount + ) + + /** + * Returns the account associated with `signer`. + * @param signer The address of the account or previously authorized signer. + * @dev Fails if the `signer` is not an account or previously authorized signer. + * @return The associated account. + */ + signerToAccount: (signer: Address) => Promise
= proxyCall( + this.contract.methods.signerToAccount ) /** @@ -69,44 +92,110 @@ export class AccountsWrapper extends BaseWrapper { * @param address The address of the account * @return Returns `true` if account exists. Returns `false` otherwise. */ - isSigner: (address: string) => Promise = proxyCall(this.contract.methods.isAuthorized) + isSigner: (address: string) => Promise = proxyCall( + this.contract.methods.isAuthorizedSigner + ) /** * Authorize an attestation signing key on behalf of this account to another address. - * @param account Address of the active account. - * @param attestationSigner The address of the signing key to authorize. + * @param signer The address of the signing key to authorize. + * @param proofOfSigningKeyPossession The account address signed by the signer address. * @return A CeloTransactionObject */ async authorizeAttestationSigner( - account: Address, - attestationSigner: Address + signer: Address, + proofOfSigningKeyPossession: Signature ): Promise> { - return this.authorizeSigner(SignerRole.Attestation, account, attestationSigner) + return toTransactionObject( + this.kit, + this.contract.methods.authorizeAttestationSigner( + signer, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + proofOfSigningKeyPossession.s + ) + ) } /** * Authorizes an address to sign votes on behalf of the account. - * @param account Address of the active account. - * @param voteSigner The address of the vote signing key to authorize. + * @param signer The address of the vote signing key to authorize. + * @param proofOfSigningKeyPossession The account address signed by the signer address. * @return A CeloTransactionObject */ async authorizeVoteSigner( - account: Address, - voteSigner: Address + signer: Address, + proofOfSigningKeyPossession: Signature ): Promise> { - return this.authorizeSigner(SignerRole.Vote, account, voteSigner) + return toTransactionObject( + this.kit, + this.contract.methods.authorizeVoteSigner( + signer, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + proofOfSigningKeyPossession.s + ) + ) } /** * Authorizes an address to sign consensus messages on behalf of the account. - * @param account Address of the active account. - * @param validationSigner The address of the signing key to authorize. + * @param signer The address of the signing key to authorize. + * @param proofOfSigningKeyPossession The account address signed by the signer address. * @return A CeloTransactionObject */ - async authorizeValidationSigner( - account: Address, - validationSigner: Address + async authorizeValidatorSigner( + signer: Address, + proofOfSigningKeyPossession: Signature ): Promise> { - return this.authorizeSigner(SignerRole.Validation, account, validationSigner) + const validators = await this.kit.contracts.getValidators() + const account = this.kit.defaultAccount || (await this.kit.web3.eth.getAccounts())[0] + if (await validators.isValidator(account)) { + const message = this.kit.web3.utils.soliditySha3({ type: 'address', value: account }) + const prefixedMsg = hashMessageWithPrefix(message) + const pubKey = signedMessageToPublicKey( + prefixedMsg, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + proofOfSigningKeyPossession.s + ) + return toTransactionObject( + this.kit, + this.contract.methods.authorizeValidatorSigner( + signer, + pubKey, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + // @ts-ignore Typescript does not support overloading. + proofOfSigningKeyPossession.s + ) + ) + } else { + return toTransactionObject( + this.kit, + this.contract.methods.authorizeValidatorSigner( + signer, + proofOfSigningKeyPossession.v, + proofOfSigningKeyPossession.r, + proofOfSigningKeyPossession.s + ) + ) + } + } + + async generateProofOfSigningKeyPossession(account: Address, signer: Address) { + return this.getParsedSignatureOfAddress( + account, + signer, + NativeSigner(this.kit.web3.eth.sign, signer) + ) + } + + async generateProofOfSigningKeyPossessionLocally( + account: Address, + signer: Address, + privateKey: string + ) { + return this.getParsedSignatureOfAddress(account, signer, LocalSigner(privateKey)) } /** @@ -168,25 +257,14 @@ export class AccountsWrapper extends BaseWrapper { */ setWalletAddress = proxySend(this.kit, this.contract.methods.setWalletAddress) - private authorizeFns = { - [SignerRole.Attestation]: this.contract.methods.authorizeAttestationSigner, - [SignerRole.Validation]: this.contract.methods.authorizeValidationSigner, - [SignerRole.Vote]: this.contract.methods.authorizeVoteSigner, - } - - private async authorizeSigner(role: SignerRole, account: Address, signer: Address) { - const sig = await this.getParsedSignatureOfAddress(account, signer) - // TODO(asa): Pass default tx "from" argument. - return toTransactionObject(this.kit, this.authorizeFns[role](signer, sig.v, sig.r, sig.s)) + parseSignatureOfAddress(address: Address, signer: string, signature: string) { + const hash = Web3.utils.soliditySha3({ type: 'address', value: address }) + return parseSignature(hash, signature, signer) } - private async getParsedSignatureOfAddress(address: Address, signer: string) { + private async getParsedSignatureOfAddress(address: Address, signer: string, signerFn: Signer) { const hash = Web3.utils.soliditySha3({ type: 'address', value: address }) - const signature = (await this.kit.web3.eth.sign(hash, signer)).slice(2) - return { - r: `0x${signature.slice(0, 64)}`, - s: `0x${signature.slice(64, 128)}`, - v: Web3.utils.hexToNumber(signature.slice(128, 130)) + 27, - } + const signature = await signerFn.sign(hash) + return parseSignature(hash, signature, signer) } } diff --git a/packages/contractkit/src/wrappers/Attestations.ts b/packages/contractkit/src/wrappers/Attestations.ts index 878a84a8561..a78df0510bd 100644 --- a/packages/contractkit/src/wrappers/Attestations.ts +++ b/packages/contractkit/src/wrappers/Attestations.ts @@ -1,4 +1,4 @@ -import { PhoneNumberUtils, SignatureUtils } from '@celo/utils' +import { base64ToHex, PhoneNumberUtils, SignatureUtils } from '@celo/utils' import { concurrentMap, sleep } from '@celo/utils/lib/async' import { notEmpty, zip3 } from '@celo/utils/lib/collections' import { parseSolidityStringArray } from '@celo/utils/lib/parsing' @@ -60,6 +60,17 @@ function attestationMessageToSign(phoneHash: string, account: Address) { return messageHash } +function sanitizeBase64(base64String: string) { + // Replace occurrences of ¿ with _. Unsure why that is happening right now + return base64String.replace(/(¿|§)/gi, '_') +} + +const attestationCodeRegex = new RegExp(/(.* |^)([a-zA-Z0-9=\+\/_-]{87,88})($| .*)/) + +function messageContainsAttestationCode(message: string) { + return attestationCodeRegex.test(message) +} + interface GetCompletableAttestationsResponse { 0: string[] 1: string[] @@ -200,7 +211,7 @@ export class AttestationsWrapper extends BaseWrapper { async approveAttestationFee(attestationsRequested: number) { const tokenContract = await this.kit.contracts.getContract(CeloContract.StableToken) const fee = await this.attestationFeeRequired(attestationsRequested) - return tokenContract.approve(this.address, fee.toString()) + return tokenContract.approve(this.address, fee.toFixed()) } /** @@ -246,6 +257,20 @@ export class AttestationsWrapper extends BaseWrapper { return withAttestationServiceURLs.filter(notEmpty) } + extractAttestationCodeFromMessage(message: string) { + const sanitizedMessage = sanitizeBase64(message) + + if (!messageContainsAttestationCode(sanitizedMessage)) { + return null + } + + const matches = sanitizedMessage.match(attestationCodeRegex) + if (!matches || matches.length < 3) { + return null + } + return base64ToHex(matches[2]) + } + /** * Completes an attestation with the corresponding code * @param phoneNumber The phone number of the attestation @@ -253,10 +278,12 @@ export class AttestationsWrapper extends BaseWrapper { * @param issuer The issuer of the attestation * @param code The code received by the validator */ - complete(phoneNumber: string, account: Address, issuer: Address, code: string) { + async complete(phoneNumber: string, account: Address, issuer: Address, code: string) { const phoneHash = PhoneNumberUtils.getPhoneHash(phoneNumber) + const accounts = await this.kit.contracts.getAccounts() + const attestationSigner = await accounts.getAttestationSigner(issuer) const expectedSourceMessage = attestationMessageToSign(phoneHash, account) - const { r, s, v } = parseSignature(expectedSourceMessage, code, issuer.toLowerCase()) + const { r, s, v } = parseSignature(expectedSourceMessage, code, attestationSigner) return toTransactionObject(this.kit, this.contract.methods.complete(phoneHash, v, r, s)) } @@ -267,17 +294,20 @@ export class AttestationsWrapper extends BaseWrapper { * @param code The code received by the validator * @param issuers The list of potential issuers */ - findMatchingIssuer( + async findMatchingIssuer( phoneNumber: string, account: Address, code: string, issuers: string[] - ): string | null { + ): Promise { const phoneHash = PhoneNumberUtils.getPhoneHash(phoneNumber) + const accounts = await this.kit.contracts.getAccounts() + const expectedSourceMessage = attestationMessageToSign(phoneHash, account) for (const issuer of issuers) { - const expectedSourceMessage = attestationMessageToSign(phoneHash, account) + const attestationSigner = await accounts.getAttestationSigner(issuer) + try { - parseSignature(expectedSourceMessage, code, issuer.toLowerCase()) + parseSignature(expectedSourceMessage, code, attestationSigner) return issuer } catch (error) { console.log(error) @@ -403,9 +433,11 @@ export class AttestationsWrapper extends BaseWrapper { issuer: Address, code: string ) { + const accounts = await this.kit.contracts.getAccounts() + const attestationSigner = await accounts.getAttestationSigner(issuer) const phoneHash = PhoneNumberUtils.getPhoneHash(phoneNumber) const expectedSourceMessage = attestationMessageToSign(phoneHash, account) - const { r, s, v } = parseSignature(expectedSourceMessage, code, issuer.toLowerCase()) + const { r, s, v } = parseSignature(expectedSourceMessage, code, attestationSigner) const result = await this.contract.methods .validateAttestationCode(phoneHash, account, v, r, s) .call() diff --git a/packages/contractkit/src/wrappers/BaseWrapper.ts b/packages/contractkit/src/wrappers/BaseWrapper.ts index ba9d9e7b920..852171391d3 100644 --- a/packages/contractkit/src/wrappers/BaseWrapper.ts +++ b/packages/contractkit/src/wrappers/BaseWrapper.ts @@ -33,7 +33,7 @@ export function toNumber(input: string) { } export function parseNumber(input: NumberLike) { - return new BigNumber(input).toString(10) + return new BigNumber(input).toFixed() } export function parseBytes(input: string): Array { @@ -215,19 +215,20 @@ export function toTransactionObject( return new CeloTransactionObject(kit, txo, defaultParams) } +export type CeloTransactionParams = Omit export class CeloTransactionObject { constructor( private kit: ContractKit, readonly txo: TransactionObject, - readonly defaultParams?: Omit + readonly defaultParams?: CeloTransactionParams ) {} /** send the transaction to the chain */ - send = (params?: Omit): Promise => { + send = (params?: CeloTransactionParams): Promise => { return this.kit.sendTransactionObject(this.txo, { ...this.defaultParams, ...params }) } /** send the transaction and waits for the receipt */ - sendAndWaitForReceipt = (params?: Omit): Promise => + sendAndWaitForReceipt = (params?: CeloTransactionParams): Promise => this.send(params).then((result) => result.waitReceipt()) } diff --git a/packages/contractkit/src/wrappers/BlockchainParameters.ts b/packages/contractkit/src/wrappers/BlockchainParameters.ts index 78f58b5d2f3..99af7a40aac 100644 --- a/packages/contractkit/src/wrappers/BlockchainParameters.ts +++ b/packages/contractkit/src/wrappers/BlockchainParameters.ts @@ -8,9 +8,9 @@ export class BlockchainParametersWrapper extends BaseWrapper { - activate = proxySend(this.kit, this.contract.methods.activate) /** * Returns the minimum and maximum number of validators that can be elected. * @returns The minimum and maximum number of validators that can be elected. @@ -80,6 +77,18 @@ export class ElectionWrapper extends BaseWrapper { toNumber ) + /** + * Returns get current validator signers using the precompiles. + * @return List of current validator signers. + */ + getCurrentValidatorSigners = proxyCall(this.contract.methods.getCurrentValidatorSigners) + /** + * Returns a list of elected validators with seats allocated to groups via the D'Hondt method. + * @return The list of elected validators. + * @dev See https://en.wikipedia.org/wiki/D%27Hondt_method#Allocation for more information. + */ + electValidatorSigners = proxyCall(this.contract.methods.electValidatorSigners) + /** * Returns the total votes for `group` made by `account`. * @param group The address of the validator group. @@ -101,6 +110,51 @@ export class ElectionWrapper extends BaseWrapper { this.contract.methods.getGroupsVotedForByAccount ) + async getVotesForGroupByAccount(account: Address, group: Address): Promise { + const pending = await this.contract.methods + .getPendingVotesForGroupByAccount(group, account) + .call() + const active = await this.contract.methods + .getActiveVotesForGroupByAccount(group, account) + .call() + return { + group, + pending: toBigNumber(pending), + active: toBigNumber(active), + } + } + + async getVoter(account: Address): Promise { + const groups = await this.contract.methods.getGroupsVotedForByAccount(account).call() + const votes = await Promise.all(groups.map((g) => this.getVotesForGroupByAccount(account, g))) + return { address: account, votes } + } + + /** + * Returns whether or not the account has any pending votes. + * @param account The address of the account casting votes. + * @return The groups that `account` has voted for. + */ + async hasPendingVotes(account: Address): Promise { + const groups: string[] = await this.contract.methods.getGroupsVotedForByAccount(account).call() + const isNotPending = await Promise.all( + groups.map(async (g) => + toBigNumber( + await this.contract.methods.getPendingVotesForGroupByAccount(account, g).call() + ).isZero() + ) + ) + return !isNotPending.every((a: boolean) => a) + } + + async hasActivatablePendingVotes(account: Address): Promise { + const groups = await this.contract.methods.getGroupsVotedForByAccount(account).call() + const isActivatable = await Promise.all( + groups.map((g: string) => this.contract.methods.hasActivatablePendingVotes(account, g).call()) + ) + return isActivatable.some((a: boolean) => a) + } + /** * Returns current configuration parameters. */ @@ -117,48 +171,90 @@ export class ElectionWrapper extends BaseWrapper { } } - /** - * Returns the addresses in the current validator set. - */ - async getValidatorSetAddresses(): Promise { - const numberValidators = await this.numberValidatorsInCurrentSet() - - const validatorAddressPromises = [] - - for (let i = 0; i < numberValidators; i++) { - validatorAddressPromises.push(this.validatorAddressFromCurrentSet(i)) + async getValidatorGroupVotes(address: Address): Promise { + const votes = await this.contract.methods.getTotalVotesForGroup(address).call() + const eligible = await this.contract.methods.getGroupEligibility(address).call() + const numVotesReceivable = await this.contract.methods.getNumVotesReceivable(address).call() + return { + address, + votes: toBigNumber(votes), + capacity: toBigNumber(numVotesReceivable).minus(votes), + eligible, } - - return Promise.all(validatorAddressPromises) } - /** * Returns the current registered validator groups and their total votes and eligibility. */ async getValidatorGroupsVotes(): Promise { const validators = await this.kit.contracts.getValidators() - const validatorGroupAddresses = (await validators.getRegisteredValidatorGroups()).map( - (g) => g.address + const groups = (await validators.getRegisteredValidatorGroups()).map((g) => g.address) + return concurrentMap(5, groups, (g) => this.getValidatorGroupVotes(g)) + } + + _activate = proxySend(this.kit, this.contract.methods.activate) + + /** + * Activates any activatable pending votes. + * @param account The account with pending votes to activate. + */ + async activate(account: Address): Promise>> { + const groups = await this.contract.methods.getGroupsVotedForByAccount(account).call() + const isActivatable = await Promise.all( + groups.map((g) => this.contract.methods.hasActivatablePendingVotes(account, g).call()) ) - const validatorGroupVotes = await Promise.all( - validatorGroupAddresses.map((g) => this.contract.methods.getTotalVotesForGroup(g).call()) + const groupsActivatable = groups.filter((_, i) => isActivatable[i]) + return groupsActivatable.map((g) => this._activate(g)) + } + + async revokePending( + account: Address, + group: Address, + value: BigNumber + ): Promise> { + const groups = await this.contract.methods.getGroupsVotedForByAccount(account).call() + const index = groups.indexOf(group) + const { lesser, greater } = await this.findLesserAndGreaterAfterVote(group, value.times(-1)) + + return toTransactionObject( + this.kit, + this.contract.methods.revokePending(group, value.toFixed(), lesser, greater, index) ) - const validatorGroupEligible = await Promise.all( - validatorGroupAddresses.map((g) => this.contract.methods.getGroupEligibility(g).call()) + } + + async revokeActive( + account: Address, + group: Address, + value: BigNumber + ): Promise> { + const groups = await this.contract.methods.getGroupsVotedForByAccount(account).call() + const index = groups.indexOf(group) + const { lesser, greater } = await this.findLesserAndGreaterAfterVote(group, value.times(-1)) + + return toTransactionObject( + this.kit, + this.contract.methods.revokeActive(group, value.toFixed(), lesser, greater, index) ) - return validatorGroupAddresses.map((a, i) => ({ - address: a, - votes: toBigNumber(validatorGroupVotes[i]), - eligible: validatorGroupEligible[i], - })) } - /** - * Returns the current eligible validator groups and their total votes. - */ - async getEligibleValidatorGroupsVotes(): Promise { - const res = await this.contract.methods.getTotalVotesForEligibleValidatorGroups().call() - return zip((a, b) => ({ address: a, votes: new BigNumber(b), eligible: true }), res[0], res[1]) + async revoke( + account: Address, + group: Address, + value: BigNumber + ): Promise>> { + const vote = await this.getVotesForGroupByAccount(account, group) + if (value.gt(vote.pending.plus(vote.active))) { + throw new Error(`can't revoke more votes for ${group} than have been made by ${account}`) + } + const txos = [] + const pendingValue = BigNumber.minimum(vote.pending, value) + if (!pendingValue.isZero()) { + txos.push(await this.revokePending(account, group, pendingValue)) + } + if (pendingValue.lt(value)) { + const activeValue = value.minus(pendingValue) + txos.push(await this.revokeActive(account, group, activeValue)) + } + return txos } /** @@ -175,7 +271,24 @@ export class ElectionWrapper extends BaseWrapper { return toTransactionObject( this.kit, - this.contract.methods.vote(validatorGroup, value.toString(), lesser, greater) + this.contract.methods.vote(validatorGroup, value.toFixed(), lesser, greater) + ) + } + + /** + * Returns the current eligible validator groups and their total votes. + */ + private async getEligibleValidatorGroupsVotes(): Promise { + const res = await this.contract.methods.getTotalVotesForEligibleValidatorGroups().call() + return zip( + (a, b) => ({ + address: a, + votes: new BigNumber(b), + capacity: new BigNumber(0), + eligible: true, + }), + res[0], + res[1] ) } @@ -194,6 +307,8 @@ export class ElectionWrapper extends BaseWrapper { currentVotes.push({ address: votedGroup, votes: voteWeight, + // Not used for the purposes of finding lesser and greater. + capacity: new BigNumber(0), eligible: true, }) } diff --git a/packages/contractkit/src/wrappers/Exchange.test.ts b/packages/contractkit/src/wrappers/Exchange.test.ts index 3f44b1f1b6c..e1d26804f0c 100644 --- a/packages/contractkit/src/wrappers/Exchange.test.ts +++ b/packages/contractkit/src/wrappers/Exchange.test.ts @@ -1,5 +1,5 @@ +import { testWithGanache } from '@celo/dev-utils/lib/ganache-test' import { newKitFromWeb3 } from '../kit' -import { testWithGanache } from '../test-utils/ganache-test' import { ExchangeWrapper } from './Exchange' /* diff --git a/packages/contractkit/src/wrappers/GasPriceMinimum.ts b/packages/contractkit/src/wrappers/GasPriceMinimum.ts index dc97d22726e..8f39be4b614 100644 --- a/packages/contractkit/src/wrappers/GasPriceMinimum.ts +++ b/packages/contractkit/src/wrappers/GasPriceMinimum.ts @@ -12,11 +12,18 @@ export interface GasPriceMinimumConfig { * Stores the gas price minimum */ export class GasPriceMinimumWrapper extends BaseWrapper { + /** + * Query current gas price minimum in gGLD. + * @returns current gas price minimum in cGLD + */ + gasPriceMinimum = proxyCall(this.contract.methods.gasPriceMinimum, undefined, toBigNumber) + /** * Query current gas price minimum. * @returns current gas price minimum in the requested currency */ - gasPriceMinimum = proxyCall(this.contract.methods.gasPriceMinimum, undefined, toBigNumber) + getGasPriceMinimum = proxyCall(this.contract.methods.getGasPriceMinimum, undefined, toBigNumber) + /** * Query target density parameter. * @returns the current block density targeted by the gas price minimum algorithm. diff --git a/packages/contractkit/src/wrappers/GoldToken.test.ts b/packages/contractkit/src/wrappers/GoldToken.test.ts index 72486d56da1..cc6b026620a 100644 --- a/packages/contractkit/src/wrappers/GoldToken.test.ts +++ b/packages/contractkit/src/wrappers/GoldToken.test.ts @@ -1,5 +1,5 @@ +import { testWithGanache } from '@celo/dev-utils/lib/ganache-test' import { newKitFromWeb3 } from '../kit' -import { testWithGanache } from '../test-utils/ganache-test' import { GoldTokenWrapper } from './GoldTokenWrapper' testWithGanache('GoldToken Wrapper', (web3) => { diff --git a/packages/contractkit/src/wrappers/LockedGold.test.ts b/packages/contractkit/src/wrappers/LockedGold.test.ts new file mode 100644 index 00000000000..60017fafea6 --- /dev/null +++ b/packages/contractkit/src/wrappers/LockedGold.test.ts @@ -0,0 +1,44 @@ +import { testWithGanache } from '@celo/dev-utils/lib/ganache-test' +import { newKitFromWeb3 } from '../kit' +import { AccountsWrapper } from './Accounts' +import { LockedGoldWrapper } from './LockedGold' + +testWithGanache('Validators Wrapper', (web3) => { + const kit = newKitFromWeb3(web3) + let accounts: AccountsWrapper + let lockedGold: LockedGoldWrapper + + // Arbitrary value. + const value = 120938732980 + let account: string + beforeAll(async () => { + account = (await web3.eth.getAccounts())[0] + kit.defaultAccount = account + lockedGold = await kit.contracts.getLockedGold() + accounts = await kit.contracts.getAccounts() + await accounts.createAccount().sendAndWaitForReceipt() + }) + + test('SBAT lock gold', async () => { + await lockedGold.lock().sendAndWaitForReceipt({ value }) + }) + + test('SBAT unlock gold', async () => { + await lockedGold.lock().sendAndWaitForReceipt({ value }) + await lockedGold.unlock(value).sendAndWaitForReceipt() + }) + + test('SBAT relock gold', async () => { + // Make 5 pending withdrawals. + await lockedGold.lock().sendAndWaitForReceipt({ value: value * 5 }) + await lockedGold.unlock(value).sendAndWaitForReceipt() + await lockedGold.unlock(value).sendAndWaitForReceipt() + await lockedGold.unlock(value).sendAndWaitForReceipt() + await lockedGold.unlock(value).sendAndWaitForReceipt() + await lockedGold.unlock(value).sendAndWaitForReceipt() + // Re-lock 2.5 of them + const txos = await lockedGold.relock(account, value * 2.5) + await Promise.all(txos.map((txo) => txo.sendAndWaitForReceipt())) + // + }) +}) diff --git a/packages/contractkit/src/wrappers/LockedGold.ts b/packages/contractkit/src/wrappers/LockedGold.ts index 12740459b99..9e059e3e319 100644 --- a/packages/contractkit/src/wrappers/LockedGold.ts +++ b/packages/contractkit/src/wrappers/LockedGold.ts @@ -66,13 +66,59 @@ export class LockedGoldWrapper extends BaseWrapper { tupleParser(parseNumber) ) + async getPendingWithdrawalsTotalValue(account: Address) { + const pendingWithdrawals = await this.getPendingWithdrawals(account) + // Ensure there are enough pending withdrawals to relock. + const values = pendingWithdrawals.map((pw: PendingWithdrawal) => pw.value) + const reducer = (total: BigNumber, pw: BigNumber) => pw.plus(total) + return values.reduce(reducer, new BigNumber(0)) + } + + /** + * Relocks gold that has been unlocked but not withdrawn. + * @param value The value to relock from pending withdrawals. + */ + async relock(account: Address, value: NumberLike): Promise>> { + const pendingWithdrawals = await this.getPendingWithdrawals(account) + // Ensure there are enough pending withdrawals to relock. + const totalValue = await this.getPendingWithdrawalsTotalValue(account) + if (totalValue.isLessThan(value)) { + throw new Error(`Not enough pending withdrawals to relock ${value}`) + } + // Assert pending withdrawals are sorted by time (increasing), so that we can re-lock starting + // with those furthest away from being available (at the end). + const throwIfNotSorted = (pw: PendingWithdrawal, i: number) => { + if (i > 0 && !pw.time.isGreaterThanOrEqualTo(pendingWithdrawals[i - 1].time)) { + throw new Error('Pending withdrawals not sorted by timestamp') + } + } + pendingWithdrawals.forEach(throwIfNotSorted) + + let remainingToRelock = new BigNumber(value) + const relockPw = ( + acc: Array>, + pw: PendingWithdrawal, + i: number + ) => { + const valueToRelock = BigNumber.minimum(pw.value, remainingToRelock) + if (valueToRelock.isZero()) { + remainingToRelock = remainingToRelock.minus(valueToRelock) + acc.push(this._relock(i, valueToRelock)) + } + return acc + } + return pendingWithdrawals.reduceRight(relockPw, []) as Array> + } + /** * Relocks gold that has been unlocked but not withdrawn. - * @param index The index of the pending withdrawal to relock. + * @param index The index of the pending withdrawal to relock from. + * @param value The value to relock from the specified pending withdrawal. */ - relock: (index: number) => CeloTransactionObject = proxySend( + _relock: (index: number, value: NumberLike) => CeloTransactionObject = proxySend( this.kit, - this.contract.methods.relock + this.contract.methods.relock, + tupleParser(parseNumber, parseNumber) ) /** diff --git a/packages/contractkit/src/wrappers/Reserve.ts b/packages/contractkit/src/wrappers/Reserve.ts index d0513fd570e..dfb6d39bb66 100644 --- a/packages/contractkit/src/wrappers/Reserve.ts +++ b/packages/contractkit/src/wrappers/Reserve.ts @@ -1,6 +1,6 @@ import BigNumber from 'bignumber.js' import { Reserve } from '../generated/types/Reserve' -import { BaseWrapper, proxyCall, toBigNumber } from './BaseWrapper' +import { BaseWrapper, proxyCall, proxySend, toBigNumber } from './BaseWrapper' export interface ReserveConfig { tobinTaxStalenessThreshold: BigNumber @@ -19,6 +19,9 @@ export class ReserveWrapper extends BaseWrapper { undefined, toBigNumber ) + isSpender: (account: string) => Promise = proxyCall(this.contract.methods.isSpender) + transferGold = proxySend(this.kit, this.contract.methods.transferGold) + /** * Returns current configuration parameters. */ diff --git a/packages/contractkit/src/wrappers/SortedOracles.test.ts b/packages/contractkit/src/wrappers/SortedOracles.test.ts index cc9cbb26e9f..72311fe9d11 100644 --- a/packages/contractkit/src/wrappers/SortedOracles.test.ts +++ b/packages/contractkit/src/wrappers/SortedOracles.test.ts @@ -1,6 +1,6 @@ +import { NetworkConfig, testWithGanache } from '@celo/dev-utils/lib/ganache-test' import { Address, CeloContract } from '../base' import { newKitFromWeb3 } from '../kit' -import { NetworkConfig, testWithGanache } from '../test-utils/ganache-test' import { OracleRate, SortedOraclesWrapper } from './SortedOracles' /* @@ -211,7 +211,7 @@ testWithGanache('SortedOracles Wrapper', (web3) => { describe('#reportExpirySeconds', () => { it('returns the number of seconds after which a report expires', async () => { const result = await sortedOracles.reportExpirySeconds() - expect(result).toEqBigNumber(3600) + expect(result).toEqBigNumber(600) }) }) diff --git a/packages/contractkit/src/wrappers/StableToken.test.ts b/packages/contractkit/src/wrappers/StableToken.test.ts index 3525c7dbaef..ed9708b9d67 100644 --- a/packages/contractkit/src/wrappers/StableToken.test.ts +++ b/packages/contractkit/src/wrappers/StableToken.test.ts @@ -1,5 +1,5 @@ +import { testWithGanache } from '@celo/dev-utils/lib/ganache-test' import { newKitFromWeb3 } from '../kit' -import { testWithGanache } from '../test-utils/ganache-test' import { StableTokenWrapper } from './StableTokenWrapper' /* diff --git a/packages/contractkit/src/wrappers/Validators.test.ts b/packages/contractkit/src/wrappers/Validators.test.ts index c8b0dfcde1c..55bccca0fe6 100644 --- a/packages/contractkit/src/wrappers/Validators.test.ts +++ b/packages/contractkit/src/wrappers/Validators.test.ts @@ -1,7 +1,7 @@ +import { testWithGanache } from '@celo/dev-utils/lib/ganache-test' import BigNumber from 'bignumber.js' import Web3 from 'web3' import { newKitFromWeb3 } from '../kit' -import { testWithGanache } from '../test-utils/ganache-test' import { AccountsWrapper } from './Accounts' import { LockedGoldWrapper } from './LockedGold' import { ValidatorsWrapper } from './Validators' @@ -11,17 +11,12 @@ TEST NOTES: - In migrations: The only account that has cUSD is accounts[0] */ -const minLockedGoldValue = Web3.utils.toWei('10', 'ether') // 10 gold +const minLockedGoldValue = Web3.utils.toWei('10000', 'ether') // 10k gold -// A random 64 byte hex string. -const publicKey = - 'ea0733ad275e2b9e05541341a97ee82678c58932464fad26164657a111a7e37a9fa0300266fb90e2135a1f1512350cb4e985488a88809b14e3cbe415e76e82b2' const blsPublicKey = - '4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' + '0x4fa3f67fc913878b068d1fa1cdddc54913d3bf988dbe5a36a20fa888f20d4894c408a6773f3d7bde11154f2a3076b700d345a42fd25a0e5e83f4db5586ac7979ac2053cd95d8f2efd3e959571ceccaa743e02cf4be3f5d7aaddb0b06fc9aff00' const blsPoP = - '9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' - -const publicKeysData = '0x' + publicKey + blsPublicKey + blsPoP + '0xcdb77255037eb68897cd487fdd85388cbda448f617f874449d4b11588b0b7ad8ddc20d9bb450b513bb35664ea3923900' testWithGanache('Validators Wrapper', (web3) => { const kit = newKitFromWeb3(web3) @@ -54,12 +49,11 @@ testWithGanache('Validators Wrapper', (web3) => { const setupValidator = async (validatorAccount: string) => { await registerAccountWithLockedGold(validatorAccount) // set account1 as the validator - await validators - .registerValidator( - // @ts-ignore - publicKeysData - ) - .sendAndWaitForReceipt({ from: validatorAccount }) + await (await validators + // @ts-ignore + .registerValidator(validatorAccount, blsPublicKey, blsPoP)).sendAndWaitForReceipt({ + from: validatorAccount, + }) } test('SBAT registerValidatorGroup', async () => { diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 548f877d823..33cc4b27466 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -1,6 +1,7 @@ import { eqAddress } from '@celo/utils/lib/address' import { zip } from '@celo/utils/lib/collections' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' +import { addressToPublicKey } from '@celo/utils/lib/signatureUtils' import BigNumber from 'bignumber.js' import { Address, NULL_ADDRESS } from '../base' import { Validators } from '../generated/types/Validators' @@ -18,7 +19,8 @@ import { export interface Validator { address: Address - publicKey: string + ecdsaPublicKey: string + blsPublicKey: string affiliation: string | null score: BigNumber } @@ -57,7 +59,6 @@ export class ValidatorsWrapper extends BaseWrapper { this.contract.methods.updateCommission(toFixed(commission).toFixed()) ) } - updatePublicKeysData = proxySend(this.kit, this.contract.methods.updatePublicKeysData) /** * Returns the Locked Gold requirements for validators. * @returns The Locked Gold requirements for validators. @@ -98,11 +99,45 @@ export class ValidatorsWrapper extends BaseWrapper { } } + /** + * Returns the account associated with `signer`. + * @param signer The address of an account or currently authorized validator signer. + * @dev Fails if the `signer` is not an account or currently authorized validator. + * @return The associated account. + */ + async validatorSignerToAccount(signerAddress: Address) { + const accounts = await this.kit.contracts.getAccounts() + return accounts.validatorSignerToAccount(signerAddress) + } + + /** + * Returns the account associated with `signer`. + * @param signer The address of the account or previously authorized signer. + * @dev Fails if the `signer` is not an account or previously authorized signer. + * @return The associated account. + */ async signerToAccount(signerAddress: Address) { const accounts = await this.kit.contracts.getAccounts() - return accounts.activeValidationSignerToAccount(signerAddress) + return accounts.signerToAccount(signerAddress) } + /** + * Updates a validator's BLS key. + * @param blsPublicKey The BLS public key that the validator is using for consensus, should pass proof + * of possession. 48 bytes. + * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the + * account address. 96 bytes. + * @return True upon success. + */ + updateBlsPublicKey: ( + blsPublicKey: string, + blsPop: string + ) => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.updateBlsPublicKey, + tupleParser(parseBytes, parseBytes) + ) + /** * Returns whether a particular account has a registered validator. * @param account The account. @@ -147,12 +182,20 @@ export class ValidatorsWrapper extends BaseWrapper { const res = await this.contract.methods.getValidator(address).call() return { address, - publicKey: res[0] as any, - affiliation: res[1], - score: fromFixed(new BigNumber(res[2])), + // @ts-ignore Incorrect type for bytes + ecdsaPublicKey: res.ecdsaPublicKey, + // @ts-ignore Incorrect type for bytes + blsPublicKey: res.blsPublicKey, + affiliation: res.affiliation, + score: fromFixed(new BigNumber(res.score)), } } + async getValidatorFromSigner(address: Address): Promise { + const account = await this.signerToAccount(address) + return this.getValidator(account) + } + /** Get ValidatorGroup information */ async getValidatorGroup(address: Address): Promise { const res = await this.contract.methods.getValidatorGroup(address).call() @@ -214,21 +257,22 @@ export class ValidatorsWrapper extends BaseWrapper { * Registers a validator unaffiliated with any validator group. * * Fails if the account is already a validator or validator group. - * Fails if the account does not have sufficient weight. * - * @param publicKeysData Comprised of three tightly-packed elements: - * - publicKey - The public key that the validator is using for consensus, should match - * msg.sender. 64 bytes. - * - blsPublicKey - The BLS public key that the validator is using for consensus, should pass - * proof of possession. 48 bytes. - * - blsPoP - The BLS public key proof of possession. 96 bytes. + * @param validatorAddress The address that the validator is using for consensus, should match + * the validator signer. + * @param blsPublicKey The BLS public key that the validator is using for consensus, should pass proof + * of possession. 48 bytes. + * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the + * account address. 96 bytes. */ - - registerValidator: (publicKeysData: string) => CeloTransactionObject = proxySend( - this.kit, - this.contract.methods.registerValidator, - tupleParser(parseBytes) - ) + async registerValidator(validatorAddress: Address, blsPublicKey: string, blsPop: string) { + const ecdsaPublicKey = await addressToPublicKey(validatorAddress, this.kit.web3.eth.sign) + return toTransactionObject( + this.kit, + // @ts-ignore incorrect typing for bytes type + this.contract.methods.registerValidator(ecdsaPublicKey, blsPublicKey, blsPop) + ) + } /** * De-registers a validator, removing it from the group for which it is a member. diff --git a/packages/contractkit/types/web3.d.ts b/packages/contractkit/types/web3.d.ts index ff242361673..293be32f57e 100644 --- a/packages/contractkit/types/web3.d.ts +++ b/packages/contractkit/types/web3.d.ts @@ -2,7 +2,14 @@ import 'web3/eth/types' declare module 'web3/eth/types' { export interface Tx { - // gasFeeRecipient?: string - gasCurrency?: string + feeCurrency?: string + gatewayFeeRecipient?: string + gatewayFee?: string + } + + export interface Transaction { + feeCurrency?: string + gatewayFeeRecipient?: string + gatewayFee?: string } } diff --git a/packages/dappkit/README.md b/packages/dappkit/README.md index e54b0b2df95..0a0155e4b4c 100644 --- a/packages/dappkit/README.md +++ b/packages/dappkit/README.md @@ -126,7 +126,7 @@ requestTxSig( tx: txObject, from: this.state.address, to: stableToken.contract.options.address, - gasCurrency: GasCurrency.cUSD + feeCurrency: FeeCurrency.cUSD } ], { requestId, dappName, callback } diff --git a/packages/dappkit/src/index.ts b/packages/dappkit/src/index.ts index 9111ed0dfbf..38484e804ff 100644 --- a/packages/dappkit/src/index.ts +++ b/packages/dappkit/src/index.ts @@ -98,19 +98,19 @@ export function requestAccountAddress(meta: DappKitRequestMeta) { Linking.openURL(serializeDappKitRequestDeeplink(AccountAuthRequest(meta))) } -export enum GasCurrency { +export enum FeeCurrency { cUSD = 'cUSD', cGLD = 'cGLD', } -async function getGasCurrencyContractAddress( +async function getFeeCurrencyContractAddress( kit: ContractKit, - gasCurrency: GasCurrency + feeCurrency: FeeCurrency ): Promise { - switch (gasCurrency) { - case GasCurrency.cUSD: + switch (feeCurrency) { + case FeeCurrency.cUSD: return kit.registry.addressFor(CeloContract.StableToken) - case GasCurrency.cGLD: + case FeeCurrency.cGLD: return kit.registry.addressFor(CeloContract.GoldToken) default: return kit.registry.addressFor(CeloContract.StableToken) @@ -121,7 +121,7 @@ export interface TxParams { tx: TransactionObject from: string to?: string - gasCurrency?: GasCurrency + feeCurrency?: FeeCurrency estimatedGas?: number value?: string } @@ -135,12 +135,12 @@ export async function requestTxSig( const baseNonce = await kit.web3.eth.getTransactionCount(txParams[0].from) const txs: TxToSignParam[] = await Promise.all( txParams.map(async (txParam, index) => { - const gasCurrency = txParam.gasCurrency ? txParam.gasCurrency : GasCurrency.cGLD - const gasCurrencyContractAddress = await getGasCurrencyContractAddress(kit, gasCurrency) + const feeCurrency = txParam.feeCurrency ? txParam.feeCurrency : FeeCurrency.cGLD + const feeCurrencyContractAddress = await getFeeCurrencyContractAddress(kit, feeCurrency) const value = txParam.value === undefined ? '0' : txParam.value const estimatedTxParams = { - gasCurrency: gasCurrencyContractAddress, + feeCurrency: feeCurrencyContractAddress, from: txParam.from, value, } as any @@ -153,7 +153,7 @@ export async function requestTxSig( txData: txParam.tx.encodeABI(), estimatedGas, nonce: baseNonce + index, - gasCurrencyAddress: gasCurrencyContractAddress, + feeCurrencyAddress: feeCurrencyContractAddress, value, ...txParam, } diff --git a/packages/dev-utils/.gitignore b/packages/dev-utils/.gitignore new file mode 100644 index 00000000000..7951405f85a --- /dev/null +++ b/packages/dev-utils/.gitignore @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json new file mode 100644 index 00000000000..b74380d201e --- /dev/null +++ b/packages/dev-utils/package.json @@ -0,0 +1,28 @@ +{ + "name": "@celo/dev-utils", + "version": "0.0.1-dev", + "description": "util package for celo packages that should only be a devDependency", + "main": "./lib/index.js", + "types": "./lib/index.d.ts", + "author": "Celo", + "license": "Apache-2.0", + "homepage": "https://github.com/celo-org/celo-monorepo/tree/master/packages/dev-utils", + "repository": "https://github.com/celo-org/celo-monorepo/tree/master/packages/dev-utils", + "keywords": [ + "celo" + ], + "scripts": { + "build": "tsc -b .", + "lint": "tslint -c tslint.json --project ." + }, + "dependencies": { + "bignumber.js": "^7.2.0", + "web3": "1.0.0-beta.37" + }, + "devDependencies": { + + }, + "engines": { + "node": ">=8.13.0" + } +} diff --git a/packages/dev-utils/src/ganache-setup.ts b/packages/dev-utils/src/ganache-setup.ts new file mode 100644 index 00000000000..0e18fcd5bc5 --- /dev/null +++ b/packages/dev-utils/src/ganache-setup.ts @@ -0,0 +1,82 @@ +// @ts-ignore +import * as ganache from '@celo/ganache-cli' + +const MNEMONIC = 'concert load couple harbor equip island argue ramp clarify fence smart topic' +export const ACCOUNT_PRIVATE_KEYS = [ + '0xf2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e0164837257d', + '0x5d862464fe9303452126c8bc94274b8c5f9874cbd219789b3eb2128075a76f72', + '0xdf02719c4df8b9b8ac7f551fcb5d9ef48fa27eef7a66453879f4d8fdc6e78fb1', + '0xff12e391b79415e941a94de3bf3a9aee577aed0731e297d5cfa0b8a1e02fa1d0', + '0x752dd9cf65e68cfaba7d60225cbdbc1f4729dd5e5507def72815ed0d8abc6249', + '0xefb595a0178eb79a8df953f87c5148402a224cdf725e88c0146727c6aceadccd', + '0x83c6d2cc5ddcf9711a6d59b417dc20eb48afd58d45290099e5987e3d768f328f', + '0xbb2d3f7c9583780a7d3904a2f55d792707c345f21de1bacb2d389934d82796b2', + '0xb2fd4d29c1390b71b8795ae81196bfd60293adf99f9d32a0aff06288fcdac55f', + '0x23cb7121166b9a2f93ae0b7c05bde02eae50d64449b2cbb42bc84e9d38d6cc89', +] +export const ACCOUNT_ADDRESSES = [ + '0x5409ED021D9299bf6814279A6A1411A7e866A631', + '0x6Ecbe1DB9EF729CBe972C83Fb886247691Fb6beb', + '0xE36Ea790bc9d7AB70C55260C66D52b1eca985f84', + '0xE834EC434DABA538cd1b9Fe1582052B880BD7e63', + '0x78dc5D2D739606d31509C31d654056A45185ECb6', + '0xA8dDa8d7F5310E4A9E24F8eBA77E091Ac264f872', + '0x06cEf8E666768cC40Cc78CF93d9611019dDcB628', + '0x4404ac8bd8F9618D27Ad2f1485AA1B2cFD82482D', + '0x7457d5E02197480Db681D3fdF256c7acA21bDc12', + '0x91c987bf62D25945dB517BDAa840A6c661374402', +] + +export async function startGanache(datadir: string, opts: { verbose?: boolean } = {}) { + const logFn = opts.verbose + ? // tslint:disable-next-line: no-console + (...args: any[]) => console.log(...args) + : () => { + /*nothing*/ + } + + const server = ganache.server({ + default_balance_ether: 1000000, + logger: { + log: logFn, + }, + network_id: 1101, + db_path: datadir, + mnemonic: MNEMONIC, + gasLimit: 10000000, + allowUnlimitedContractSize: true, + }) + + await new Promise((resolve, reject) => { + server.listen(8545, (err: any, blockchain: any) => { + if (err) { + reject(err) + } else { + resolve(blockchain) + } + }) + }) + + return () => + new Promise((resolve, reject) => { + server.close((err: any) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) +} + +export default function setup(dataDir: string) { + return startGanache(dataDir) + .then((stopGanache) => { + ;(global as any).stopGanache = stopGanache + }) + .catch((err) => { + console.error('Error starting ganache, Doing `yarn test:prepare` might help') + console.error(err) + process.exit(1) + }) +} diff --git a/packages/dev-utils/src/ganache-teardown.ts b/packages/dev-utils/src/ganache-teardown.ts new file mode 100644 index 00000000000..27400b9a1aa --- /dev/null +++ b/packages/dev-utils/src/ganache-teardown.ts @@ -0,0 +1,7 @@ +export default function tearDown() { + console.log('Stopping ganache') + return (global as any).stopGanache().catch((err: any) => { + console.error('error stopping ganache') + console.error(err) + }) +} diff --git a/packages/cli/src/test-utils/ganache-test.ts b/packages/dev-utils/src/ganache-test.ts similarity index 93% rename from packages/cli/src/test-utils/ganache-test.ts rename to packages/dev-utils/src/ganache-test.ts index 6581e91d652..f61174654e6 100644 --- a/packages/cli/src/test-utils/ganache-test.ts +++ b/packages/dev-utils/src/ganache-test.ts @@ -1,5 +1,8 @@ import Web3 from 'web3' import { JsonRPCResponse } from 'web3/providers' +import migrationOverride from './migration-override.json' + +export const NetworkConfig = migrationOverride export function jsonRpcCall(web3: Web3, method: string, params: any[]): Promise { return new Promise((resolve, reject) => { diff --git a/packages/cli/src/test-utils/matchers.ts b/packages/dev-utils/src/matchers.ts similarity index 100% rename from packages/cli/src/test-utils/matchers.ts rename to packages/dev-utils/src/matchers.ts diff --git a/packages/contractkit/src/test-utils/migration-override.json b/packages/dev-utils/src/migration-override.json similarity index 100% rename from packages/contractkit/src/test-utils/migration-override.json rename to packages/dev-utils/src/migration-override.json diff --git a/packages/dev-utils/tsconfig.json b/packages/dev-utils/tsconfig.json new file mode 100644 index 00000000000..e3749ab6cf6 --- /dev/null +++ b/packages/dev-utils/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../typescript/tsconfig.library.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "resolveJsonModule": true + }, + "include": ["src", "types/", "src/migration-override.json"] +} diff --git a/packages/dev-utils/tslint.json b/packages/dev-utils/tslint.json new file mode 100644 index 00000000000..036f000683b --- /dev/null +++ b/packages/dev-utils/tslint.json @@ -0,0 +1,9 @@ +{ + "extends": ["@celo/typescript/tslint.json"], + "rules": { + "no-global-arrow-functions": false, + "no-console": false, + "member-ordering": false, + "max-classes-per-file": false + } +} diff --git a/packages/docs/SUMMARY.md b/packages/docs/SUMMARY.md index a051c9e3553..b2e49e28e38 100644 --- a/packages/docs/SUMMARY.md +++ b/packages/docs/SUMMARY.md @@ -68,10 +68,15 @@ - [Introduction](command-line-interface/introduction.md) - [Config](command-line-interface/config.md) - [Account](command-line-interface/account.md) +- [Election](command-line-interface/election.md) - [Exchange](command-line-interface/exchange.md) - [Locked Gold](command-line-interface/lockedgold.md) +- [Network](command-line-interface/network.md) +- [Node](command-line-interface/node.md) +- [Transfer](command-line-interface/transfer.md) - [Validator Node](command-line-interface/validator.md) - [Validator Groups](command-line-interface/validatorgroup.md) +- [Help](command-line-interface/help.md) ## Community diff --git a/packages/docs/celo-codebase/protocol/transactions/erc20-transaction-fees.md b/packages/docs/celo-codebase/protocol/transactions/erc20-transaction-fees.md index ba5a46f8501..59e9aaac5d9 100644 --- a/packages/docs/celo-codebase/protocol/transactions/erc20-transaction-fees.md +++ b/packages/docs/celo-codebase/protocol/transactions/erc20-transaction-fees.md @@ -2,4 +2,4 @@ As in Ethereum, transaction fees play a critical role in the Celo protocol as a safeguard against denial-of-service attacks. In order to simplify the process of sending funds, these fees can be paid in ERC20 tokens, and not just the native token of the protocol, Celo Gold. This means that a user sending Celo Dollars to friends or family will be able to pay their transaction fee out of their Celo Dollar balance, and do not need to hold a separate balance of Celo Gold in order to make transactions. -The protocol maintains a governable whitelist of smart contract addresses which can be used to pay for transaction fees. These smart contracts implement an extension of the ERC20 interface, with additional functions that allow the protocol to debit and credit transaction fees. When creating a transaction, users can specify the address of the currency they would like to use to pay for gas via the `gasCurrency` field. Leaving this field empty will result in the native currency, Celo Gold, being used. Note that transactions that specify non-Celo Gold gas currencies will cost approximately 100k additional gas. Note that this number is expected to drop significantly in the near future. +The protocol maintains a governable whitelist of smart contract addresses which can be used to pay for transaction fees. These smart contracts implement an extension of the ERC20 interface, with additional functions that allow the protocol to debit and credit transaction fees. When creating a transaction, users can specify the address of the currency they would like to use to pay for gas via the `feeCurrency` field. Leaving this field empty will result in the native currency, Celo Gold, being used. Note that transactions that specify non-Celo Gold gas currencies will cost approximately 100k additional gas. Note that this number is expected to drop significantly in the near future. diff --git a/packages/docs/command-line-interface/account.md b/packages/docs/command-line-interface/account.md index 91f4cd511cf..b1829761ead 100644 --- a/packages/docs/command-line-interface/account.md +++ b/packages/docs/command-line-interface/account.md @@ -1,36 +1,39 @@ --- -description: Manage your account, send and receive Celo Gold and Celo Dollars +description: Manage your account, keys, and metadata --- ## Commands ### Authorize -Authorize an attestation, validation or vote signing key +Keep your locked Gold more secure by authorizing alternative keys to be used for signing attestations, voting, or validating. By doing so, you can continue to participate in the protocol why keeping the key with access to your locked Gold in cold storage. You must include a "proof-of-possession" of the key being authorized, which can be generated with the "account:proof-of-possession" command. ``` USAGE $ celocli account:authorize OPTIONS - -r, --role=vote|validation|attestation Role to delegate - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address - --to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + -r, --role=vote|validator|attestation (required) Role to delegate + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address + --pop=pop (required) Proof-of-possession of the signer key + --signer=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address EXAMPLE - authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role vote --to - 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d + authorize --from 0x5409ED021D9299bf6814279A6A1411A7e866A631 --role vote --signer + 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb --pop + 0x1b9fca4bbb5bfb1dbe69ef1cddbd9b4202dcb6b134c5170611e1e36ecfa468d7b46c85328d504934fce6c2a1571603a50ae224d2b32685e84d4d + 1a1eebad8452eb ``` _See code: [packages/cli/src/commands/account/authorize.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/authorize.ts)_ ### Balance -View Celo Dollar and Gold balances given account address +View Celo Dollar and Gold balances for an address ``` USAGE - $ celocli account:balance ACCOUNT + $ celocli account:balance ADDRESS EXAMPLE balance 0x5409ed021d9299bf6814279a6a1411a7e866a631 @@ -197,41 +200,47 @@ EXAMPLE _See code: [packages/cli/src/commands/account/isvalidator.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/isvalidator.ts)_ -### Lock +### New -Locks Celo Gold to be used in governance and validator elections. +Creates a new account ``` USAGE - $ celocli account:lock - -OPTIONS - --from=from (required) - --value=value (required) unit amount of Celo Gold (cGLD) + $ celocli account:new EXAMPLE - lock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --value 1000000000000000000 + new ``` -_See code: [packages/cli/src/commands/account/lock.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/lock.ts)_ +_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)_ -### New +### Proof-of-possession -Creates a new account +Generate proof-of-possession to be used to authorize a signer. See the "account:authorize" command for more details. ``` USAGE - $ celocli account:new + $ celocli account:proof-of-possession + +OPTIONS + --account=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the account that needs to proove + possession of the signer key. + + --privateKey=privateKey Optional. The signer private key, only necessary if the key is + not being managed by a locally running node. + + --signer=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the signer key to prove possession of. EXAMPLE - new + proof-of-possession --account 0x5409ed021d9299bf6814279a6a1411a7e866a631 --signer + 0x6ecbe1db9ef729cbe972c83fb886247691fb6beb ``` -_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)_ +_See code: [packages/cli/src/commands/account/proof-of-possession.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/proof-of-possession.ts)_ ### Register -Register an account +Register an account on-chain. This allows you to lock Gold, which is a pre-requisite for registering a Validator or Group, participating in Validator elections and on-chain Governance, and earning epoch rewards. ``` USAGE @@ -239,10 +248,11 @@ USAGE OPTIONS --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address - --name=name (required) + --name=name -EXAMPLE - register +EXAMPLES + register --from 0x5409ed021d9299bf6814279a6a1411a7e866a631 + register --from 0x5409ed021d9299bf6814279a6a1411a7e866a631 --name test-account ``` _See code: [packages/cli/src/commands/account/register.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/register.ts)_ @@ -282,46 +292,6 @@ EXAMPLE _See code: [packages/cli/src/commands/account/show-metadata.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/show-metadata.ts)_ -### Transferdollar - -Transfer Celo Dollars - -``` -USAGE - $ celocli account:transferdollar - -OPTIONS - --amountInWei=amountInWei (required) Amount to transfer (in wei) - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the sender - --to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the receiver - -EXAMPLE - transferdollar --from 0xa0Af2E71cECc248f4a7fD606F203467B500Dd53B --to 0x5409ed021d9299bf6814279a6a1411a7e866a631 - --amountInWei 1 -``` - -_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 - -Transfer gold - -``` -USAGE - $ celocli account:transfergold - -OPTIONS - --amountInWei=amountInWei (required) Amount to transfer (in wei) - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the sender - --to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the receiver - -EXAMPLE - transfergold --from 0xa0Af2E71cECc248f4a7fD606F203467B500Dd53B --to 0x5409ed021d9299bf6814279a6a1411a7e866a631 - --amountInWei 1 -``` - -_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 or validate blocks diff --git a/packages/docs/command-line-interface/config.md b/packages/docs/command-line-interface/config.md index f313c741e4e..6b2cc926d64 100644 --- a/packages/docs/command-line-interface/config.md +++ b/packages/docs/command-line-interface/config.md @@ -24,7 +24,7 @@ USAGE $ celocli config:set OPTIONS - --node=node (required) [default: ws://localhost:8546] Node URL + --node=node (required) [default: http://localhost:8545] 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/election.md b/packages/docs/command-line-interface/election.md index 6ed0013466e..b7fb0140bd1 100644 --- a/packages/docs/command-line-interface/election.md +++ b/packages/docs/command-line-interface/election.md @@ -1,22 +1,110 @@ --- -description: View and manage validator elections +description: Participate in and view the state of Validator Elections --- ## Commands -### Validatorset +### Activate -Outputs the current validator set +Activate pending votes in validator elections to begin earning rewards ``` USAGE - $ celocli election:validatorset + $ celocli election:activate + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Voter's address + --wait Wait until all pending votes become activatable + +EXAMPLES + activate --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 + activate --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --wait +``` + +_See code: [packages/cli/src/commands/election/activate.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/election/activate.ts)_ + +### Current + +Outputs the set of validators currently participating in BFT to create blocks. The validator set is re-elected at the end of every epoch. + +``` +USAGE + $ celocli election:current + +EXAMPLE + current +``` + +_See code: [packages/cli/src/commands/election/current.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/election/current.ts)_ + +### List + +Prints the list of validator groups, the number of votes they have received, the number of additional votes they are able to receive, and whether or not they are eleigible to elect validators. + +``` +USAGE + $ celocli election:list + +EXAMPLE + list +``` + +_See code: [packages/cli/src/commands/election/list.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/election/list.ts)_ + +### Revoke + +Revoke votes for a Validator Group in validator elections. + +``` +USAGE + $ celocli election:revoke + +OPTIONS + --for=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) ValidatorGroup's address + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Voter's address + --value=value (required) Value of votes to revoke + +EXAMPLE + revoke --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 --for 0x932fee04521f5fcb21949041bf161917da3f588b, --value + 1000000 +``` + +_See code: [packages/cli/src/commands/election/revoke.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/election/revoke.ts)_ + +### Run + +Runs a "mock" election and prints out the validators that would be elected if the epoch ended right now. + +``` +USAGE + $ celocli election:run + +EXAMPLE + run +``` + +_See code: [packages/cli/src/commands/election/run.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/election/run.ts)_ + +### Show + +Show election information about a voter or registered Validator Group + +``` +USAGE + $ celocli election:show ADDRESS + +ARGUMENTS + ADDRESS Voter or Validator Groups's address + +OPTIONS + --group Show information about a group running in Validator elections + --voter Show information about an account voting in Validator elections EXAMPLE - validatorset + show 0x97f7333c51897469E8D98E7af8653aAb468050a3 ``` -_See code: [packages/cli/src/commands/election/validatorset.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/election/validatorset.ts)_ +_See code: [packages/cli/src/commands/election/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/election/show.ts)_ ### Vote @@ -27,7 +115,7 @@ USAGE $ celocli election:vote OPTIONS - --for=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Set vote for ValidatorGroup's address + --for=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) ValidatorGroup's address --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Voter's address --value=value (required) Amount of Gold used to vote for group diff --git a/packages/docs/command-line-interface/exchange.md b/packages/docs/command-line-interface/exchange.md index 16fc35f9776..9dad11f6ce0 100644 --- a/packages/docs/command-line-interface/exchange.md +++ b/packages/docs/command-line-interface/exchange.md @@ -1,60 +1,62 @@ --- -description: Commands for interacting with the Exchange +description: Exchange Celo Dollars and Celo Gold via the stability mechanism --- ## Commands -### List +### Dollars -List information about tokens on the exchange (all amounts in wei) +Exchange Celo Dollars for Celo Gold via the stability mechanism ``` USAGE - $ celocli exchange:list + $ celocli exchange:dollars OPTIONS - --amount=amount [default: 1000000000000000000] Amount of sellToken (in wei) to report rates for + --commission=commission (required) + --for=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) The minimum value of Celo Gold to receive in return + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) The address with Celo Dollars to exchange + --value=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) The value of Celo Dollars to exchange for Celo Gold EXAMPLE - list + dollars --value 10000000000000 --for 50000000000000 --from 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d ``` -_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)_ +_See code: [packages/cli/src/commands/exchange/dollars.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/exchange/dollars.ts)_ -### Selldollar +### Gold -Sell Celo dollars for Celo gold on the exchange +Exchange Celo Gold for Celo Dollars via the stability mechanism ``` USAGE - $ celocli exchange:selldollar SELLAMOUNT MINBUYAMOUNT FROM + $ celocli exchange:gold -ARGUMENTS - SELLAMOUNT the amount of sellToken (in wei) to sell - MINBUYAMOUNT the minimum amount of buyToken (in wei) expected - FROM +OPTIONS + --commission=commission (required) + --for=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) The minimum value of Celo Dollars to receive in return + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) The address with Celo Gold to exchange + --value=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) The value of Celo Gold to exchange for Celo Dollars EXAMPLE - selldollar 100 300 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d + gold --value 5000000000000 --for 100000000000000 --from 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d ``` -_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)_ +_See code: [packages/cli/src/commands/exchange/gold.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/exchange/gold.ts)_ -### Sellgold +### Show -Sell Celo gold for Celo dollars on the exchange +Show the current exchange rates offered by the Exchange ``` USAGE - $ celocli exchange:sellgold SELLAMOUNT MINBUYAMOUNT FROM + $ celocli exchange:show -ARGUMENTS - SELLAMOUNT the amount of sellToken (in wei) to sell - MINBUYAMOUNT the minimum amount of buyToken (in wei) expected - FROM +OPTIONS + --amount=amount [default: 1000000000000000000] Amount of the token being exchanged to report rates for EXAMPLE - sellgold 100 300 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d + list ``` -_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)_ +_See code: [packages/cli/src/commands/exchange/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/exchange/show.ts)_ diff --git a/packages/docs/command-line-interface/introduction.md b/packages/docs/command-line-interface/introduction.md index 25c74201f36..e28f7fa398b 100644 --- a/packages/docs/command-line-interface/introduction.md +++ b/packages/docs/command-line-interface/introduction.md @@ -40,6 +40,10 @@ Make sure to kill the container when you are done. `$ docker kill celo_cli_container` +### **Prerequisites** + +- **You have a full node running.** See the [Running a Full Node](running-a-full-node.md) instructions for more details on running a full node. + ### Overview The tool is broken down into modules and commands with the following pattern: diff --git a/packages/docs/command-line-interface/lockedgold.md b/packages/docs/command-line-interface/lockedgold.md index 49c0dabf34a..c792e3f056b 100644 --- a/packages/docs/command-line-interface/lockedgold.md +++ b/packages/docs/command-line-interface/lockedgold.md @@ -4,9 +4,27 @@ description: View and manage locked Celo Gold ## Commands +### Lock + +Locks Celo Gold to be used in governance and validator elections. + +``` +USAGE + $ celocli lockedgold:lock + +OPTIONS + --from=from (required) + --value=value (required) The unit amount of Celo Gold (cGLD) + +EXAMPLE + lock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --value 10000000000000000000000 +``` + +_See code: [packages/cli/src/commands/lockedgold/lock.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/lockedgold/lock.ts)_ + ### Show -Show Locked Gold information for a given account +Show Locked Gold information for a given account. This includes the total amount of locked gold, the amount being used for voting in Validator Elections, and any pending withdrawals that have been initiated via "lockedgold:unlock". ``` USAGE @@ -20,7 +38,7 @@ _See code: [packages/cli/src/commands/lockedgold/show.ts](https://github.com/cel ### Unlock -Unlocks Celo Gold, which can be withdrawn after the unlocking period. +Unlocks Celo Gold, which can be withdrawn after the unlocking period. Unlocked gold will appear as a "pending withdrawal" until the unlocking period is over, after which it can be withdrawn via "lockedgold:withdraw". ``` USAGE @@ -28,7 +46,7 @@ USAGE OPTIONS --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address - --value=value (required) unit amount of Celo Gold (cGLD) + --value=value (required) The unit amount of Celo Gold (cGLD) EXAMPLE unlock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --value 500000000 @@ -38,7 +56,7 @@ _See code: [packages/cli/src/commands/lockedgold/unlock.ts](https://github.com/c ### Withdraw -Withdraw unlocked gold whose unlocking period has passed. +Withdraw any pending withdrawals created via "lockedgold:unlock" that have become available. ``` USAGE diff --git a/packages/docs/command-line-interface/network.md b/packages/docs/command-line-interface/network.md index 094ec8a2738..b9b193e1645 100644 --- a/packages/docs/command-line-interface/network.md +++ b/packages/docs/command-line-interface/network.md @@ -1,12 +1,12 @@ --- -description: View network parameters +description: View parameters of the network, including but not limited to configuration for the various Celo core smart contracts. --- ## Commands ### Parameters -View network parameters +View parameters of the network, including but not limited to configuration for the various Celo core smart contracts. ``` USAGE diff --git a/packages/docs/command-line-interface/node.md b/packages/docs/command-line-interface/node.md index 63a938881ca..031ad811a85 100644 --- a/packages/docs/command-line-interface/node.md +++ b/packages/docs/command-line-interface/node.md @@ -6,7 +6,7 @@ description: Manage your full node ### Accounts -List node accounts +List the addresses that this node has the private keys for. ``` USAGE diff --git a/packages/docs/command-line-interface/oracle.md b/packages/docs/command-line-interface/oracle.md new file mode 100644 index 00000000000..28a690d0bf7 --- /dev/null +++ b/packages/docs/command-line-interface/oracle.md @@ -0,0 +1,51 @@ +--- +description: Get the current set oracle-reported rates for the given token +--- + +## Commands + +### Rates + +Get the current set oracle-reported rates for the given token + +``` +USAGE + $ celocli oracle:rates TOKEN + +ARGUMENTS + TOKEN (StableToken) [default: StableToken] Token to get the rates for + +EXAMPLES + rates StableToken + rates +``` + +_See code: [packages/cli/src/commands/oracle/rates.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/oracle/rates.ts)_ + +### Report + +Report the price of Celo Gold in a specified token (currently just Celo Dollar, aka: "StableToken") + +``` +USAGE + $ celocli oracle:report TOKEN + +ARGUMENTS + TOKEN (StableToken) [default: StableToken] Token to report on + +OPTIONS + --denominator=denominator (required) [default: 1] Amount of cGLD equal to the numerator. + Defaults to 1 if left blank + + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the oracle account + + --numerator=numerator (required) Amount of the specified token equal to the amount of + cGLD in the denominator + +EXAMPLES + report StableToken --numerator 1.02 --from 0x8c349AAc7065a35B7166f2659d6C35D75A3893C1 + report StableToken --numerator 102 --denominator 100 --from 0x8c349AAc7065a35B7166f2659d6C35D75A3893C1 + report --numerator 0.99 --from 0x8c349AAc7065a35B7166f2659d6C35D75A3893C1 +``` + +_See code: [packages/cli/src/commands/oracle/report.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/oracle/report.ts)_ diff --git a/packages/docs/command-line-interface/transfer.md b/packages/docs/command-line-interface/transfer.md new file mode 100644 index 00000000000..640ae7f409a --- /dev/null +++ b/packages/docs/command-line-interface/transfer.md @@ -0,0 +1,45 @@ +--- +description: Transfer Celo Gold and Celo Dollars +--- + +## Commands + +### Dollars + +Transfer Celo Dollars to a specified address. + +``` +USAGE + $ celocli transfer:dollars + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the sender + --to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the receiver + --value=value (required) Amount to transfer (in wei) + +EXAMPLE + dollars --from 0xa0Af2E71cECc248f4a7fD606F203467B500Dd53B --to 0x5409ed021d9299bf6814279a6a1411a7e866a631 --value + 1000000000000000000 +``` + +_See code: [packages/cli/src/commands/transfer/dollars.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/transfer/dollars.ts)_ + +### Gold + +Transfer Celo Gold to a specified address. + +``` +USAGE + $ celocli transfer:gold + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the sender + --to=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address of the receiver + --value=value (required) Amount to transfer (in wei) + +EXAMPLE + transfergold --from 0xa0Af2E71cECc248f4a7fD606F203467B500Dd53B --to 0x5409ed021d9299bf6814279a6a1411a7e866a631 --value + 10000000000000000000 +``` + +_See code: [packages/cli/src/commands/transfer/gold.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/transfer/gold.ts)_ diff --git a/packages/docs/command-line-interface/validator.md b/packages/docs/command-line-interface/validator.md index 308a79300f3..fc7e1f015c2 100644 --- a/packages/docs/command-line-interface/validator.md +++ b/packages/docs/command-line-interface/validator.md @@ -1,12 +1,12 @@ --- -description: View and manage validators +description: View and manage Validators --- ## Commands ### Affiliate -Affiliate to a ValidatorGroup +Affiliate a Validator with a Validator Group. This allows the Validator Group to add that Validator as a member. If the Validator is already a member of a Validator Group, affiliating with a different Group will remove the Validator from the first group's members. ``` USAGE @@ -26,7 +26,7 @@ _See code: [packages/cli/src/commands/validator/affiliate.ts](https://github.com ### Deaffiliate -DeAffiliate to a ValidatorGroup +Deaffiliate a Validator from a Validator Group, and remove it from the Group if it is also a member. ``` USAGE @@ -43,7 +43,7 @@ _See code: [packages/cli/src/commands/validator/deaffiliate.ts](https://github.c ### Deregister -Deregister a Validator +Deregister a Validator. Approximately 60 days after deregistration, the 10,000 Gold locked up to register the Validator will become possible to unlock. Note that deregistering a Validator will also deaffiliate and remove the Validator from any Group it may be an affiliate or member of. ``` USAGE @@ -60,7 +60,7 @@ _See code: [packages/cli/src/commands/validator/deregister.ts](https://github.co ### List -List existing Validators +List registered Validators, their name (if provided), affiliation, uptime score, and public keys used for validating. ``` USAGE @@ -72,28 +72,6 @@ EXAMPLE _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)_ -### PublicKey - -Manage BLS public key data for a validator - -``` -USAGE - $ celocli validator:publicKey - -OPTIONS - --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Validator's address - --publicKey=0x (required) Public Key - -EXAMPLE - publickey --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --publicKey - 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf - 997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d - 785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d - 96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00 -``` - -_See code: [packages/cli/src/commands/validator/publicKey.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/publicKey.ts)_ - ### Register Register a new Validator @@ -103,22 +81,22 @@ USAGE $ celocli validator:register OPTIONS + --blsKey=0x (required) BLS Public Key + --blsPop=0x (required) BLS Proof-of-Possession --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address for the Validator - --publicKey=0x (required) Public Key EXAMPLE - register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --publicKey - 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf - 997eda082ae19d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d - 785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d - 96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00 + register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --blsKey + 0x4fa3f67fc913878b068d1fa1cdddc54913d3bf988dbe5a36a20fa888f20d4894c408a6773f3d7bde11154f2a3076b700d345a42fd25a0e5e83f4 + db5586ac7979ac2053cd95d8f2efd3e959571ceccaa743e02cf4be3f5d7aaddb0b06fc9aff00 --blsPop + 0xcdb77255037eb68897cd487fdd85388cbda448f617f874449d4b11588b0b7ad8ddc20d9bb450b513bb35664ea3923900 ``` _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)_ ### Requirements -Get Requirements for Validators +List the Locked Gold requirements for registering a Validator. This consists of a value, which is the amount of Celo Gold that needs to be locked in order to register, and a duration, which is the amount of time that Gold must stay locked following the deregistration of the Validator. ``` USAGE @@ -132,7 +110,7 @@ _See code: [packages/cli/src/commands/validator/requirements.ts](https://github. ### Show -Show information about an existing Validator +Show information about a registered Validator. ``` USAGE @@ -146,3 +124,25 @@ EXAMPLE ``` _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)_ + +### Update-bls-public-key + +Update the BLS public key for a Validator to be used in consensus. Regular (ECDSA and BLS) key rotation is recommended for Validator operational security. + +``` +USAGE + $ celocli validator:update-bls-public-key + +OPTIONS + --blsKey=0x (required) BLS Public Key + --blsPop=0x (required) BLS Proof-of-Possession + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Validator's address + +EXAMPLE + update-bls-key --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --blsKey + 0x4fa3f67fc913878b068d1fa1cdddc54913d3bf988dbe5a36a20fa888f20d4894c408a6773f3d7bde11154f2a3076b700d345a42fd25a0e5e83f4 + db5586ac7979ac2053cd95d8f2efd3e959571ceccaa743e02cf4be3f5d7aaddb0b06fc9aff00 --blsPop + 0xcdb77255037eb68897cd487fdd85388cbda448f617f874449d4b11588b0b7ad8ddc20d9bb450b513bb35664ea3923900 +``` + +_See code: [packages/cli/src/commands/validator/update-bls-public-key.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/update-bls-public-key.ts)_ diff --git a/packages/docs/command-line-interface/validatorgroup.md b/packages/docs/command-line-interface/validatorgroup.md index ab77089ffc9..638b396b59e 100644 --- a/packages/docs/command-line-interface/validatorgroup.md +++ b/packages/docs/command-line-interface/validatorgroup.md @@ -1,12 +1,12 @@ --- -description: View and manage validator groups +description: View and manage Validator Groups --- ## Commands ### Commission -Update the commission for an existing validator group +Update the commission for a registered Validator Group. This represents the share of the epoch rewards given to elected Validators that goes to the group they are a member of. ``` USAGE @@ -24,7 +24,7 @@ _See code: [packages/cli/src/commands/validatorgroup/commission.ts](https://gith ### Deregister -Deregister a ValidatorGroup +Deregister a Validator Group. Approximately 60 days after deregistration, the 10,000 Gold locked up to register the Validator Group will become possible to unlock. Note that the Group must be empty (i.e. no members) before deregistering. ``` USAGE @@ -41,7 +41,7 @@ _See code: [packages/cli/src/commands/validatorgroup/deregister.ts](https://gith ### List -List existing Validator Groups +List registered Validator Groups, their names (if provided), commission, and members. ``` USAGE @@ -71,9 +71,9 @@ OPTIONS --reorder=reorder Reorder a validator within the members list EXAMPLES - member --accept 0x97f7333c51897469e8d98e7af8653aab468050a3 - member --remove 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 - member --reorder 3 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 + member --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 --accept 0x97f7333c51897469e8d98e7af8653aab468050a3 + member --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 --remove 0x97f7333c51897469e8d98e7af8653aab468050a3 + member --from 0x47e172f6cfb6c7d01c1574fa3e2be7cc73269d95 --reorder 3 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)_ @@ -87,7 +87,9 @@ USAGE $ celocli validatorgroup:register OPTIONS - --commission=commission (required) + --commission=commission (required) The share of the epoch rewards given to elected + Validators that goes to the group. + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Address for the Validator Group EXAMPLE diff --git a/packages/docs/command-line-interface/validatorset.md b/packages/docs/command-line-interface/validatorset.md deleted file mode 100644 index b2860ff1254..00000000000 --- a/packages/docs/command-line-interface/validatorset.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -description: Outputs the current validator set ---- - -## Commands - -### Validatorset - -Outputs the current validator set - -``` -USAGE - $ celocli validatorset - -EXAMPLE - validatorset -``` - -_See code: [packages/cli/src/commands/validatorset.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validatorset.ts)_ diff --git a/packages/docs/developer-resources/contractkit/README.md b/packages/docs/developer-resources/contractkit/README.md index 0f090268a1d..92443c52498 100644 --- a/packages/docs/developer-resources/contractkit/README.md +++ b/packages/docs/developer-resources/contractkit/README.md @@ -6,7 +6,7 @@ ContractKit supports the following functionality: - Connect to a node - Access web3 object to interact with node's Json RPC API -- Send Transaction with celo's extra fields: (gasCurrency) +- Send Transaction with celo's extra fields: (feeCurrency, gatewayFeeRecipient and gatewayFee) - Simple interface to interact with cGold and cDollar - Simple interface to interact with Celo Core contracts - Utilities diff --git a/packages/docs/developer-resources/contractkit/examples.md b/packages/docs/developer-resources/contractkit/examples.md index 8bee1042417..794495de0ff 100644 --- a/packages/docs/developer-resources/contractkit/examples.md +++ b/packages/docs/developer-resources/contractkit/examples.md @@ -66,10 +66,11 @@ const goldTokenAddress = await kit.registry.addressFor(CeloContract.GoldToken) ## Sending Custom Transactions -Celo transaction object is not the same as Ethereum's. There are two new fields present: +Celo transaction object is not the same as Ethereum's. There are three new fields present: -- gasCurrency (address of the ERC20 contract to use to pay for gas) -- gasFeeRecipient (address of the beneficiary for the gas, the full node) +- feeCurrency (address of the ERC20 contract to use to pay for gas and the gateway fee) +- gatewayFeeRecipient (coinbase address of the full serving the light client's trasactions) +- gatewayFee (value paid to the gateway fee recipient, denominated in the fee currency) This means that using `web3.eth.sendTransaction` or `myContract.methods.transfer().send()` should be avoided. diff --git a/packages/docs/developer-resources/contractkit/setup.md b/packages/docs/developer-resources/contractkit/setup.md index afdbf88d4d7..9b0040126b8 100644 --- a/packages/docs/developer-resources/contractkit/setup.md +++ b/packages/docs/developer-resources/contractkit/setup.md @@ -40,7 +40,7 @@ import { CeloContract } from '@celo/contractkit' // default from kit.defaultAccount = myAddress // paid gas in celo dollars -await kit.setGasCurrency(CeloContract.StableToken) +await kit.setFeeCurrency(CeloContract.StableToken) ``` You're ready to start using ContractKit! See the [Examples](examples.md) section to learn more. diff --git a/packages/docs/developer-resources/dappkit/usage.md b/packages/docs/developer-resources/dappkit/usage.md index 3943b1eb3a3..55400aac4e3 100644 --- a/packages/docs/developer-resources/dappkit/usage.md +++ b/packages/docs/developer-resources/dappkit/usage.md @@ -114,7 +114,7 @@ requestTxSig( tx: txObject, from: this.state.address, to: stableToken.contract.options.address, - gasCurrency: GasCurrency.cUSD + feeCurrency: FeeCurrency.cUSD } ], { requestId, dappName, callback } diff --git a/packages/docs/getting-started/running-a-full-node.md b/packages/docs/getting-started/running-a-full-node.md index 95aae11f3b2..1367dddea64 100644 --- a/packages/docs/getting-started/running-a-full-node.md +++ b/packages/docs/getting-started/running-a-full-node.md @@ -1,6 +1,15 @@ # Running a Full Node -This section explains how to get a full node running on the [Alfajores Testnet](alfajores-testnet.md), using a Docker image that was built for this purpose. +- [Running a Full Node](#running-a-full-node) + - [Prerequisites](#prerequisites) + - [Celo Networks](#celo-networks) + - [Pull the Celo Docker image](#pull-the-celo-docker-image) + - [Set up a data directory](#set-up-a-data-directory) + - [Create an account and get its address](#create-an-account-and-get-its-address) + - [Configure the node](#configure-the-node) + - [Start the node](#start-the-node) + +This section explains how to get a full node running on the [Alfajores Testnet](alfajores-testnet.md) and Baklava Beta Network, using a Docker image that was built for this purpose. Full nodes play a special purpose in the Celo ecosystem, acting as a bridge between the mobile wallets \(running as light clients\) and the validator nodes. To make sure that full nodes are rewarded for this service, the Celo protocol includes full node incentives. Every time a light client sends a new transaction, a portion of the transaction fees will go to the full node that gossips the transaction to other full nodes and validators. @@ -14,11 +23,25 @@ For this reason, despite the fact that Celo uses a proof-of-stake protocol, user A note about conventions: The code you'll see on this page is bash commands and their output. -A $ signifies the bash prompt. Everything following it is the command you should run in a terminal. The $ isn't part of the command, so don't copy it. - When you see text in angle brackets <>, replace them and the text inside with your own value of what it refers to. Don't include the <> in the command. {% endhint %} +## **Celo Networks** + +First we are going to setup the environment depending on the network we want to use (`Baklava` or `Alfajores`). Run: + +```bash +# If you want to connect to Baklava: +export CELO_NETWORK=baklava +export CELO_IMAGE=us.gcr.io/celo-testnet/celo-node +export NETWORK_ID=1101 + +# If you want to connect to Alfajores: +export CELO_NETWORK=alfajores +export CELO_IMAGE=us.gcr.io/celo-testnet/celo-node +export NETWORK_ID=44785 +``` + ## **Pull the Celo Docker image** We're going to use a Docker image containing the Celo node software in this tutorial. @@ -27,15 +50,17 @@ If you are re-running these instructions, the Celo Docker image may have been up Run: -`$ docker pull us.gcr.io/celo-testnet/celo-node:alfajores` +```bash +docker pull $CELO_IMAGE:$CELO_NETWORK +``` ## **Set up a data directory** First, create the directory that will store your node's configuration and its copy of the blockchain. This directory can be named anything you'd like, but here's a default you can use. The commands below create a directory and then navigate into it. The rest of the steps assume you are running the commands from inside this directory. -``` -$ mkdir celo-data-dir -$ cd celo-data-dir +```bash +mkdir celo-data-dir +cd celo-data-dir ``` ## **Create an account and get its address** @@ -44,13 +69,17 @@ In this step, you'll create an account on the network. If you've already done th Run the command to create a new account: -`` $ docker run -v `pwd`:/root/.celo --entrypoint /bin/sh -it us.gcr.io/celo-testnet/celo-node:alfajores -c "geth account new" `` +```bash +docker run -v $PWD:/root/.celo --entrypoint /bin/sh -it $CELO_IMAGE:$CELO_NETWORK -c "geth account new" +``` It will prompt you for a passphrase, ask you to confirm it, and then will output your account address: `Address: {}` Save this address to an environment variables, so that you can reference it below (don't include the braces): -`$ export CELO_ACCOUNT_ADDRESS=` +```bash +export CELO_ACCOUNT_ADDRESS= +``` _Note: this environment variable will only persist while you have this terminal window open. If you want this environment variable to be available in the future, you can add it to your `~/.bash_profile_ @@ -58,17 +87,23 @@ _Note: this environment variable will only persist while you have this terminal The genesis block is the first block in the chain, and is specific to each network. This command gets the `genesis.json` file for alfajores and uses it to initialize your nodes' data directory. -`` $ docker run -v `pwd`:/root/.celo us.gcr.io/celo-testnet/celo-node:alfajores init /celo/genesis.json `` +```bash +docker run -v $PWD:/root/.celo $CELO_IMAGE:$CELO_NETWORK init /celo/genesis.json +``` In order to allow the node to sync with the network, give it the address of existing nodes in the network: -`` $ docker run -v `pwd`:/root/.celo --entrypoint cp us.gcr.io/celo-testnet/celo-node:alfajores /celo/static-nodes.json /root/.celo/ `` +```bash +docker run -v $PWD:/root/.celo --entrypoint cp $CELO_IMAGE:$CELO_NETWORK /celo/static-nodes.json /root/.celo/ +``` ## **Start the 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 44785 --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 `` +```bash +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 $CELO_IMAGE:$CELO_NETWORK --verbosity 3 --networkid $NETWORK_ID --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. @@ -80,7 +115,7 @@ INFO [07-16|14:04:48.941] Imported new chain segment blocks=335 t INFO [07-16|14:04:56.944] Imported new chain segment blocks=472 txs=0 mgas=0.000 elapsed=8.003s mgasps=0.000 number=1927 hash=4f1010…1414c1 age=4h52m31s cache=2.34mB ``` -You will have fully synced with the network once you have pulled the latest block number, which you can lookup by visiting at the [Alfajores Testnet Stats](https://alfajores-ethstats.celo-testnet.org/) page. +You will have fully synced with the network once you have pulled the latest block number, which you can lookup by visiting at the [Baklava Testnet Stats](https://baklava-ethstats.celo-testnet.org/) or [Alfajores Testnet Stats](https://alfajores-ethstats.celo-testnet.org/) pages. {% 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. diff --git a/packages/docs/getting-started/running-a-validator-alfajores.md b/packages/docs/getting-started/running-a-validator-alfajores.md new file mode 100644 index 00000000000..18e7c920bac --- /dev/null +++ b/packages/docs/getting-started/running-a-validator-alfajores.md @@ -0,0 +1,114 @@ +# Running a Validator in Alfajores Network + +- [Running a Validator in Alfajores Network](#running-a-validator-in-alfajores-network) + - [Instructions](#instructions) + - [Pull the Celo Docker image](#pull-the-celo-docker-image) + - [Create accounts](#create-accounts) + - [Deploy the Validator node](#deploy-the-validator-node) + - [Running the Attestation Service](#running-the-attestation-service) + +This section explains how to get a Validator node running on the Alfajores network, using a Docker image that was built for this purpose. Most of this process is the same as running a full node, but with a few additional steps. + +This section is specific for Alfajores Network. You can find more details about running a Validator in different networks at [Running a Validator page](running-a-validator.md). + +## Instructions + +First we are going to setup the main environment variables related with the `Alfajores` network. Run: + +```bash +export CELO_IMAGE=us.gcr.io/celo-testnet/celo-node:alfajores +export NETWORK_ID=44785 +export URL_VERIFICATION_POOL=https://us-central1-celo-testnet-production.cloudfunctions.net/handleVerificationRequestalfajores/v0.1/sms/ +``` + +### Pull the Celo Docker image + +In all the commands we are going to see the `CELO_IMAGE` variable to refer to the right Docker image to use. Now we can get the Docker image: + +```bash +docker pull $CELO_IMAGE +``` + +### Create accounts + +Create and cd into the directory where you want to store the data and any other files needed to run your node. You can name this whatever you’d like, but here’s a default you can use: + +```bash +mkdir celo-alfajores-dir +cd celo-alfajores-dir +``` + +Create two accounts, one for the Validator and one for Validator Group, and get their addresses if you don’t already have them. If you already have your accounts, you can skip this step. + +To create your two accounts, run this command twice: + +```bash +docker run -v $PWD:/root/.celo --entrypoint /bin/sh -it $CELO_IMAGE -c "geth account new" +``` + +It will prompt you for a passphrase, ask you to confirm it, and then will output your account address: `Address: {}` + +{% hint style="danger" %} +**Warning**: There is a known issue running geth inside Docker that happens eventually. So if that command fails, please check [this page](https://forum.celo.org/t/setting-up-a-validator-faq/90). +{% endhint %} + +Let's save these addresses to environment variables, so that you can reference it later (don't include the braces): + +```bash +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: + +```bash +docker run -v $PWD:/root/.celo --entrypoint /bin/sh -it $CELO_IMAGE -c "geth 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: + +```bash +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 found inside the Docker image: + +```bash +docker run -v $PWD:/root/.celo $CELO_IMAGE init /celo/genesis.json +``` + +To participate in consensus, we need to set up our nodekey for our account. We can do so via the following command \(it will prompt you for your passphrase\): + +```bash +docker run -v $PWD:/root/.celo --entrypoint /bin/sh -it $CELO_IMAGE -c "geth account set-node-key $CELO_VALIDATOR_ADDRESS" +``` + +In order to allow the node to sync with the network, give it the address of existing nodes in the network: + +```bash +docker run -v $PWD:/root/.celo --entrypoint cp $CELO_IMAGE /celo/static-nodes.json /root/.celo/ +``` + +Start up the node: + +```bash +docker run --name celo-validator --restart always -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 $CELO_IMAGE --verbosity 3 --networkid 44785 --syncmode full --rpc --rpcaddr 0.0.0.0 --rpcapi eth,net,web3,debug,admin,personal --maxpeers 1100 --mine --miner.verificationpool=$URL_VERIFICATION_POOL --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 the interfaces of the Docker container. 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. +{% endhint %} + +The `mine` flag will tell geth to try participating in the BFT consensus protocol, which is analogous to mining on the Ethereum PoW network. It will not be allowed to validate until it gets elected -- so next we need to stand for election. + +The `networkid` parameter value of `44785` indicates we are connecting the Alfajores Testnet. + +### Running the Attestation Service + +As part of the [lightweight identity protocol](/celo-codebase/protocol/identity), Validators are expected to run an [Attestation Service](https://github.com/celo-org/celo-monorepo/tree/master/packages/attestation-service) to provide attestations that allow users to map their phone number to an account on Celo. + +You can find the complete instructions about how to run the [Attestation Service at the documentation page](running-attestation-service.md). + +Now you may need to wait for your node to complete a full sync. You can check on the sync status with `celocli node:synced`. Your node will be fully synced when it has downloaded and processed the latest block, which you can see on the [Alfajores Testnet Stats](https://alfajores-ethstats.celo-testnet.org/) page. diff --git a/packages/docs/getting-started/running-a-validator-baklava.md b/packages/docs/getting-started/running-a-validator-baklava.md new file mode 100644 index 00000000000..a0e3f6cdf3e --- /dev/null +++ b/packages/docs/getting-started/running-a-validator-baklava.md @@ -0,0 +1,180 @@ +# Running a Validator in Baklava Network + +- [Running a Validator in Baklava Network](#running-a-validator-in-baklava-network) + - [Instructions](#instructions) + - [Environment variables](#environment-variables) + - [Pull the Celo Docker image](#pull-the-celo-docker-image) + - [Create accounts](#create-accounts) + - [Deploy the Validator and Proxy nodes](#deploy-the-validator-and-proxy-nodes) + - [Running the Attestation Service](#running-the-attestation-service) + - [Reference Script](#reference-script) + +This section explains how to get a Validator node running on the Baklava network, using a Docker image that was built for this purpose. Most of this process is the same as running a full node, but with a few additional steps. + +This section is specific for Baklava Network. You can find more details about running a Validator in different networks at [Running a Validator page](running-a-validator.md). + +## Instructions + +If you are re-running these instructions, the Celo Docker image may have been updated, and it's important to get the latest version. + +To run a complete Validator it's necessary to execute the following components: + +- The Validator software +- A Proxy that acts as an intermediary for the Validator requests +- The Attestation Service + +The Proxy is not mandatory but highly recommended. It allows to protect the Validator node from outside connections and hide the Validator behind that Proxy from other nodes of the network. + +### Environment variables + +| Variable | Explanation | +| ----------------------------- | ---------------------------------------------------------------- | +| CELO_IMAGE | The Docker image used for the Validator and Proxy containers | | +| NETWORK_ID | The Celo network chain ID | | +| URL_VERIFICATION_POOL | URL for the Verification pool for the attestation process | | +| CELO_VALIDATOR_GROUP_ADDRESS | The public address for the validation group | | +| CELO_VALIDATOR_ADDRESS | The public address for the Validator instance | | +| CELO_PROXY_ADDRESS | The public address for the Proxy instance | | +| CELO_VALIDATOR_BLS_PUBLIC_KEY | The BLS public key for the Validator instance | | +| CELO_VALIDATOR_BLS_SIGNATURE | A proof-of-possession of the BLS public key | | +| PROXY_ENODE | The enode address for the Validator proxy | | +| PROXY_IP | The Proxy container internal IP address from docker pool address | | +| ATTESTATION_KEY | The private key for the account used in the Attestation Service | | +| ATTESTATION_SERVICE_URL | The URL to access the Attestation Service deployed | | +| METADATA_URL | The URL to access the metadata file for your Attestation Service | | + +First we are going to setup the main environment variables related with the `Baklava` network. Run: + +```bash +export CELO_IMAGE=us.gcr.io/celo-testnet/celo-node:baklava +export NETWORK_ID=1101 +``` + +### Pull the Celo Docker image + +In all the commands we are going to see the `CELO_IMAGE` variable to refer to the right Docker image to use. Now we can get the Docker image: + +```bash +docker pull $CELO_IMAGE +``` + +### Create accounts + +At this point we need to create the accounts that will be used by the Validator and the Proxy. We create and cd into the directory where you want to store the data and any other files needed to run your node. You can name this whatever you’d like, but here’s a default you can use: + +```bash +mkdir -p celo-data-dir/proxy celo-data-dir/validator +cd celo-data-dir +``` + +We are going to need to create 3 accounts, 2 for the Validator and 1 for the Proxy. + +First we create three accounts, one for the Validator, one for the Validator Group and the last one for the Proxy. You can generate their addresses using the below commands if you don’t already have them. If you already have some accounts, you can skip this step. + +To create the accounts needed, run the following commands. The first two create the accounts for the Validator, the third one for the Proxy: + +```bash +docker run -v $PWD/validator:/root/.celo --entrypoint /bin/sh -it $CELO_IMAGE -c "geth account new" +docker run -v $PWD/validator:/root/.celo --entrypoint /bin/sh -it $CELO_IMAGE -c "geth account new" +docker run -v $PWD/proxy:/root/.celo --entrypoint /bin/sh -it $CELO_IMAGE -c "geth account new" +``` + +Those commands will prompt you for a passphrase, ask you to confirm it, and then will output your account address: `Address: {}` + +{% hint style="danger" %} +**Warning**: There is a known issue running geth inside Docker that happens eventually. So if that command fails, please check [this page](https://forum.celo.org/t/setting-up-a-validator-faq/90). +{% endhint %} + +Let's save these addresses to environment variables, so that you can reference it later (don't include the braces): + +```bash +export CELO_VALIDATOR_GROUP_ADDRESS= +export CELO_VALIDATOR_ADDRESS= +export CELO_PROXY_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 to generate this "proof-of-possession", which consists of a the BLS public key and a signature: + +```bash +docker run -v $PWD/validator:/root/.celo --entrypoint /bin/sh -it $CELO_IMAGE -c "geth 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 two environment variables: + +```bash +export CELO_VALIDATOR_BLS_PUBLIC_KEY= +export CELO_VALIDATOR_BLS_SIGNATURE= +``` + +### Deploy the Validator and Proxy nodes + +We initialize the Docker containers for the Validator and the Proxy, building from an image for the network and initializing Celo with the genesis block found inside the Docker image: + +```bash +docker run -v $PWD/proxy:/root/.celo $CELO_IMAGE init /celo/genesis.json +docker run -v $PWD/validator:/root/.celo $CELO_IMAGE init /celo/genesis.json +``` + +To participate in consensus, we need to set up our nodekey for our accounts. We can do so via the following commands \(it will prompt you for your passphrase\): + +```bash +docker run -v $PWD/proxy:/root/.celo --entrypoint /bin/sh -it $CELO_IMAGE -c "geth account set-node-key $CELO_PROXY_ADDRESS" +docker run -v $PWD/validator:/root/.celo --entrypoint /bin/sh -it $CELO_IMAGE -c "geth account set-node-key $CELO_VALIDATOR_ADDRESS" +``` + +{% hint style="danger" %} +**Warning**: There is a known issue running geth inside Docker that happens eventually. So if that command fails, please check [this page](https://forum.celo.org/t/setting-up-a-validator-faq/90). +{% endhint %} + +In order to allow the node to sync with the network, give it the address of existing nodes in the network: + +```bash +docker run -v $PWD/proxy:/root/.celo --entrypoint cp $CELO_IMAGE /celo/static-nodes.json /root/.celo/ +docker run -v $PWD/validator:/root/.celo --entrypoint cp $CELO_IMAGE /celo/static-nodes.json /root/.celo/ +``` + +At this point we are ready to start up the Proxy: + +```bash +docker run --name celo-proxy --restart always -p 8545:8545 -p 8546:8546 -p 30303:30303 -p 30303:30303/udp -p 30503:30503 -p 30503:30503/udp -v $PWD/proxy:/root/.celo $CELO_IMAGE --verbosity 3 --networkid $NETWORK_ID --syncmode full --rpc --rpcaddr 0.0.0.0 --rpcapi eth,net,web3,debug --maxpeers 1100 --etherbase=$CELO_PROXY_ADDRESS --proxy.proxy --proxy.proxiedvalidatoraddress $CELO_VALIDATOR_ADDRESS --proxy.internalendpoint :30503 +``` + +Now we need to obtain the Proxy enode and ip addresses, running the following commands: + +```bash +export PROXY_ENODE=$(docker exec celo-proxy geth --exec "admin.nodeInfo['enode'].split('//')[1].split('@')[0]" attach | tr -d '"') +export PROXY_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' celo-proxy) +``` + +Now we can start up the Validator node: + +```bash +docker run -v $PWD/validator:/root/.celo --entrypoint sh --rm $CELO_IMAGE -c "echo $DEFAULT_PASSWORD > /root/.celo/.password" +docker run --name celo-validator --restart always -p 127.0.0.1:8547:8545 -p 127.0.0.1:8548:8546 -p 30304:30303 -p 30304:30303/udp -v $PWD/validator:/root/.celo $CELO_IMAGE --verbosity 3 --networkid $NETWORK_ID --syncmode full --rpc --rpcaddr 0.0.0.0 --rpcapi eth,net,web3,debug --maxpeers 125 --mine --istanbul.blockperiod=5 --istanbul.requesttimeout=3000 --etherbase $CELO_VALIDATOR_ADDRESS --nodiscover --proxy.proxied --proxy.proxyenodeurlpair=enode://$PROXY_ENODE@$PROXY_IP:30503\;enode://$PROXY_ENODE@$PROXY_IP:30503 --unlock=$CELO_VALIDATOR_ADDRESS --password /root/.celo/.password +``` + +{% 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 the interfaces of the Docker container. 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. +{% endhint %} + +The `mine` flag does not mean the node starts mining blocks, but rather starts trying to participate in the BFT consensus protocol. It cannot do this until it gets elected -- so next we need to stand for election. + +The `networkid` parameter value of `44785` indicates we are connecting the Baklava Beta network. + +### Running the Attestation Service + +As part of the [lightweight identity protocol](/celo-codebase/protocol/identity), Validators are expected to run an [Attestation Service](https://github.com/celo-org/celo-monorepo/tree/master/packages/attestation-service) to provide attestations that allow users to map their phone number to an account on Celo. + +You can find the complete instructions about how to run the [Attestation Service at the documentation page](running-attestation-service.md). + +You’re all set! Note that elections are finalized at the end of each epoch, roughly once an hour in the Baklava Testnet. After that hour, if you get elected, your node will start participating BFT consensus and validating blocks. Users requesting attestations will hit your registered Attestation Service. + +### Reference Script + +You can use (and modify if you want) this [reference bash script](../../../scripts/run-docker-validator-network.sh) automating all the above steps. It requires Docker and screen. + +You can see all the options using the following command: + +```bash +./run-docker-validator-network.sh help +``` diff --git a/packages/docs/getting-started/running-a-validator.md b/packages/docs/getting-started/running-a-validator.md index 7763df91187..59477ba0d77 100644 --- a/packages/docs/getting-started/running-a-validator.md +++ b/packages/docs/getting-started/running-a-validator.md @@ -1,29 +1,42 @@ # Running a Validator -This section explains how to get a validator node running on the network, using a Docker image that was built for this purpose. Most of this process is the same as running a full node, but with a few additional steps. +- [Running a Validator](#running-a-validator) + - [Prerequisites](#prerequisites) + - [Hardware requirements](#hardware-requirements) + - [Software requirements](#software-requirements) + - [Celo Networks](#celo-networks) + - [Obtain and lock up some Celo Gold for staking](#obtain-and-lock-up-some-celo-gold-for-staking) + - [Baklava](#baklava) + - [Alfajores](#alfajores) + - [Lock up Celo Gold](#lock-up-celo-gold) + - [Run for election](#run-for-election) -Validators help secure the Celo network by participating in Celo’s Proof of Stake protocol. Validators are organized into Validator Groups, analogous to parties in representative democracies. A validator group is essentially an ordered list of validators, along with metadata like name and URL. +This section explains how to get a Validator node running on the network, using a Docker image that was built for this purpose. Most of this process is the same as running a full node, but with a few additional steps. -Just as anyone in a democracy can create their own political party, or seek to get selected to represent a party in an election, any Celo user can create a validator group and add themselves to it, or set up a potential validator and work to get an existing validator group to include them. +Validators help secure the Celo network by participating in Celo’s Proof of Stake protocol. Validators are organized into Validator Groups, analogous to parties in representative democracies. A Validator Group is essentially an ordered list of Validators, along with metadata like name and URL. -While other Validator Groups will exist on the Alfajores Testnet, the fastest way to get up and running with a validator will be to register a Validator Group, register a Validator, and add that Validator to your Validator Group. The addresses used to register Validator Groups and Validators must be unique, which will require that you create two accounts in the step-by-step guide below. +Just as anyone in a democracy can create their own political party, or seek to get selected to represent a party in an election, any Celo user can create a Validator group and add themselves to it, or set up a potential Validator and work to get an existing Validator group to include them. + +While other Validator Groups will exist on the Celo Networks, the fastest way to get up and running with a Validator will be to register a Validator Group, register a Validator, and add that Validator to your Validator Group. The addresses used to register Validator Groups and Validators must be unique, which will require that you create two accounts in the step-by-step guide below. + +You can find more details about Celo mission and why becoming a Validator [at the following page](https://medium.com/celohq/calling-all-chefs-become-a-celo-validator-c75d1c2909aa). {% hint style="info" %} -If you are starting up a validator, please consider leaving it running for a few weeks to support the network. +If you are starting up a Validator, please consider leaving it running for a few weeks to support the network. {% endhint %} -## **Prerequisites** +## Prerequisites ### Hardware requirements -Because Celo network is based in Proof of Stake, the hardware requirements are not very high. Proof of Stake consensus is not so CPU intensive as Proof of Work but has a higher requirements of network connectivity and lantency. Here you have a list of the standard requirements for running a validator node: +Because Celo network is based in Proof of Stake, the hardware requirements are not very high. Proof of Stake consensus is not so CPU intensive as Proof of Work but has a higher requirements of network connectivity and latency. Here you have a list of the standard requirements for running a Validator node: - Memory: 8 GB RAM - CPU: Quad core 3GHz (64-bit) - Disk: 256 GB of SSD storage - Network: At least 1 GB input/output dual Ethernet -It is recommended to run the validator node in an environment that facilitates a 24/7 execution. Deployments in a top-tier datacenter facilitates the security and better uptimes. +It is recommended to run the Validator node in an environment that facilitates a 24/7 execution. Deployments in a top-tier datacenter facilitates the security and better uptimes. ### Software requirements @@ -46,167 +59,126 @@ The code you'll see on this page is bash commands and their output. When you see text in angle brackets <>, replace them and the text inside with your own value of what it refers to. Don't include the <> in the command. {% endhint %} -## **Pull the Celo Docker image** - -We're going to use a Docker image containing the Celo node software in this tutorial. - -If you are re-running these instructions, the Celo Docker image may have been updated, and it's important to get the latest version. - -Run: - -```bash -docker pull us.gcr.io/celo-testnet/celo-node:alfajores` -``` +## Celo Networks -## **Create accounts** +Celo provides different networks for different purposes. You can find the specifics about how to run a Validator in the Celo networks in the following documentation pages: -Create and cd into the directory where you want to store the data and any other files needed to run your node. You can name this whatever you’d like, but here’s a default you can use: +- [Running a Validator in Baklava Network](running-a-validator-baklava.md) +- [Running a Validator in Alfajores Network](running-a-validator-alfajores.md) -```bash -mkdir celo-data-dir -cd celo-data-dir -``` +In this documentation pages we're going to use a Docker image containing the Celo node software. -Create two accounts, one for the Validator and one for Validator Group, and get their addresses if you don’t already have them. If you already have your accounts, you can skip this step. +You can use also this [reference script](../../../scripts/run-docker-validator-network.sh) for running the Celo validator stack using Docker containers. -To create your two accounts, run this command twice: +### Obtain and lock up some Celo Gold for staking -```bash -docker run -v $PWD:/root/.celo --entrypoint /bin/sh -it us.gcr.io/celo-testnet/celo-node:alfajores -c "geth account new" -``` +#### Baklava -It will prompt you for a passphrase, ask you to confirm it, and then will output your account address: `Address: {}` +To participate in The Great Celo Stake Off (aka TGCSO) and get fauceted it's necessary to register online via an [online form](https://docs.google.com/forms/d/e/1FAIpQLSfbn5hTJ4UIWpN92-o2qMTUB0UnrFsL0fm97XqGe4VhhN_r5A/viewform). -Let's save these addresses to environment variables, so that you can reference it later (don't include the braces): +#### Alfajores -```bash -export CELO_VALIDATOR_GROUP_ADDRESS= -export CELO_VALIDATOR_ADDRESS= -``` +Visit the [Alfajores Celo Faucet](https://celo.org/build/faucet) to send **both** of your accounts some funds. -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: +In a new tab, unlock your accounts so that you can send transactions. This only unlocks the accounts for the lifetime of the Validator that's running, so be sure to unlock `$CELO_VALIDATOR_ADDRESS` again if your node gets restarted: ```bash -docker run -v $PWD:/root/.celo --entrypoint /bin/sh -it us.gcr.io/celo-testnet/celo-node:alfajores -c "geth account proof-of-possession $CELO_VALIDATOR_ADDRESS" +# You will be prompted for your password. +celocli account:unlock --account $CELO_VALIDATOR_GROUP_ADDRESS +celocli account:unlock --account $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: +In a new tab, make a locked Gold account for both of your addresses by running the Celo CLI. This will allow you to stake Celo Gold, which is required to register a Validator and Validator Groups: ```bash -export CELO_VALIDATOR_POP= +celocli account:register --from $CELO_VALIDATOR_GROUP_ADDRESS --name +celocli account:register --from $CELO_VALIDATOR_ADDRESS --name ``` -## Deploy the validator node +#### Lock up Celo Gold -Initialize the docker container, building from an image for the network and initializing Celo with the genesis block: +Lock up Celo Gold for both accounts in order to secure the right to register a Validator and Validator Group. The current requirement is 10k Celo Gold to register a validator, and 10k Celo Gold _per member validator_ to register a Validator Group. For Validators, this gold remains locked for approximately 60 days following deregistration. For groups, this gold remains locked for approximately 60 days following the removal of the Nth validator from the group. ```bash -docker run -v $PWD:/root/.celo us.gcr.io/celo-testnet/celo-node:alfajores init /celo/genesis.json +celocli lockedgold:lock --from $CELO_VALIDATOR_GROUP_ADDRESS --value 10000000000000000000000 +celocli lockedgold:lock --from $CELO_VALIDATOR_ADDRESS --value 10000000000000000000000 ``` -To participate in consensus, we need to set up our nodekey for our account. We can do so via the following command \(it will prompt you for your passphrase\): +### Run for election -```bash -docker run -v $PWD:/root/.celo --entrypoint /bin/sh -it us.gcr.io/celo-testnet/celo-node:alfajores -c "geth account set-node-key $CELO_VALIDATOR_ADDRESS" -``` +In order to be elected as a Validator, you will first need to register your group and Validator. Note that when registering a Validator Group, you need to specify a commission, which is the fraction of epoch rewards paid to the group by its members. -In order to allow the node to sync with the network, give it the address of existing nodes in the network: +Register your Validator Group: ```bash -docker run -v $PWD:/root/.celo --entrypoint cp us.gcr.io/celo-testnet/celo-node:alfajores /celo/static-nodes.json /root/.celo/ +celocli validatorgroup:register --from $CELO_VALIDATOR_GROUP_ADDRESS --commission 0.1 ``` -Start up the node: +Register your Validator: ```bash -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 44785 --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 +celocli validator:register --from $CELO_VALIDATOR_ADDRESS --blsKey $CELO_VALIDATOR_BLS_PUBLIC_KEY --blsPop $CELO_VALIDATOR_BLS_SIGNATURE ``` -{% 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. -{% endhint %} - -The `mine` flag will tell geth to try participating in the BFT consensus protocol, which is analogous to mining on the Ethereum PoW network. It will not be allowed to validate until it gets elected -- so next we need to stand for election. - -The `networkid` parameter value of `44785` indicates we are connecting the Alfajores Testnet. - -Now you may need to wait for your node to complete a full sync. You can check on the sync status with `celocli node:synced`. Your node will be fully synced when it has downloaded and processed the latest block, which you can see on the [Alfajores Testnet Stats](https://alfajores-ethstats.celo-testnet.org/) page. - -## Obtain and lock up some Celo Gold for staking - -Visit the [Alfajores Faucet](https://celo.org/build/faucet) to send **both** of your accounts some funds. - -In a new tab, unlock your accounts so that you can send transactions. This only unlocks the accounts for the lifetime of the validator that's running, so be sure to unlock `$CELO_VALIDATOR_ADDRESS` again if your node gets restarted: +Affiliate your Validator with your Validator Group. Note that you will not be a member of this group until the Validator Group accepts you: ```bash -# You will be prompted for your password. -celocli account:unlock --account $CELO_VALIDATOR_GROUP_ADDRESS -celocli account:unlock --account $CELO_VALIDATOR_ADDRESS +celocli validator:affiliate $CELO_VALIDATOR_GROUP_ADDRESS --from $CELO_VALIDATOR_ADDRESS ``` -In a new tab, make a locked Gold account for both of your addresses by running the Celo CLI. This will allow you to stake Celo Gold, which is required to register a validator and validator groups: +Accept the affiliation: ```bash -celocli account:register --from $CELO_VALIDATOR_GROUP_ADDRESS --name -celocli account:register --from $CELO_VALIDATOR_ADDRESS --name +celocli validatorgroup:member --accept $CELO_VALIDATOR_ADDRESS --from $CELO_VALIDATOR_GROUP_ADDRESS ``` -Make a locked Gold commitment for both accounts in order to secure the right to register a validator and validator group. The current requirement is 1 Celo Gold with a notice period of 60 days. If you choose to stake more gold, or a longer notice period, be sure to use those values below: +Use both accounts to vote for your Validator Group: ```bash -celocli lockedgold:lockup --from $CELO_VALIDATOR_GROUP_ADDRESS --goldAmount 1000000000000000000 --noticePeriod 5184000 -celocli lockedgold:lockup --from $CELO_VALIDATOR_ADDRESS --goldAmount 1000000000000000000 --noticePeriod 5184000 +celocli election:vote --from $CELO_VALIDATOR_ADDRESS --for $CELO_VALIDATOR_GROUP_ADDRESS --value 10000000000000000000000 +celocli election:vote --from $CELO_VALIDATOR_GROUP_ADDRESS --for $CELO_VALIDATOR_GROUP_ADDRESS --value 10000000000000000000000 ``` -## Run for election - -In order to be elected as a validator, you will first need to register your group and validator and give them each an an ID, which people will know them by (e.g. `Awesome Validators Inc.` and `Alice's Awesome Validator`). +You’re all set! Note that elections are finalized at the end of each epoch, roughly once an hour in the Alfajores or Baklava Testnets. After that hour, if you get elected, your node will start participating BFT consensus and validating blocks. -Register your validator group: +You can inspect the current state of voting by running: ```bash -celocli validatorgroup:register --id --from $CELO_VALIDATOR_GROUP_ADDRESS --noticePeriod 5184000 +celocli election:list ``` -Register your validator: +If you find your Validator still not getting elected you may need to faucet yourself more funds and lock more gold in order to be able to cast more votes for your Validator Group! + +At any moment you can check the currently elected validators by running the following command: ```bash -celocli validator:register --id --from $CELO_VALIDATOR_ADDRESS --noticePeriod 5184000 --publicKey 0x`openssl rand -hex 64`$CELO_VALIDATOR_POP +celocli election:current ``` -{% hint style="info" %} -**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: +### Stop Validating -```bash -celocli validator:affiliation --set $CELO_VALIDATOR_GROUP_ADDRESS --from $CELO_VALIDATOR_ADDRESS -``` +If for some reason you need to stop running your Validator, please do one or all of the following so that it will no longer be chosen as a participant in BFT: -Accept the affiliation: +- Deregister your validator: ```bash -celocli validatorgroup:member --accept $CELO_VALIDATOR_ADDRESS --from $CELO_VALIDATOR_GROUP_ADDRESS +celocli validator:deaffiliate --from $CELO_VALIDATOR_ADDRESS +celocli validator:deregister --from $CELO_VALIDATOR_ADDRESS ``` -Use both accounts to vote for your validator group: +- Stop voting for your validator group: ```bash -celocli validatorgroup:vote --from $CELO_VALIDATOR_ADDRESS --for $CELO_VALIDATOR_GROUP_ADDRESS -celocli validatorgroup:vote --from $CELO_VALIDATOR_GROUP_ADDRESS --for $CELO_VALIDATOR_GROUP_ADDRESS +celocli election:revoke --from $CELO_VALIDATOR_ADDRESS --for $CELO_VALIDATOR_GROUP_ADDRESS --value 10000000000000000000000 +celocli election:revoke --from $CELO_VALIDATOR_GROUP_ADDRESS --for $CELO_VALIDATOR_GROUP_ADDRESS --value 10000000000000000000000 ``` -You’re all set! Note that elections are finalized at the end of each epoch, roughly once an hour in the Alfajores Testnet. After that hour, if you get elected, your node will start participating BFT consensus and validating blocks. - -You can inspect the current state of voting by running: +- Deregister your validator group: ```bash -celocli validatorgroup:list +celocli validatorgroup:deregister --from $CELO_VALIDATOR_GORUP_ADDRESS ``` -If you find your validator still not getting elected you may need to faucet yourself more funds and bond a greater deposit to command more voting weight! - {% hint style="info" %} **Roadmap**: Different parameters will govern elections in a Celo production network. Epochs are likely to be daily, rather than hourly. Running a Validator will also include setting up proxy nodes to protect against DDoS attacks, and using hardware wallets to secure the key used to sign blocks. We plan to update these instructions with more details soon. {% endhint %} diff --git a/packages/docs/getting-started/running-attestation-service.md b/packages/docs/getting-started/running-attestation-service.md new file mode 100644 index 00000000000..739e019bba9 --- /dev/null +++ b/packages/docs/getting-started/running-attestation-service.md @@ -0,0 +1,138 @@ +# Running the Attestation Service + +- [Running the Attestation Service](#running-the-attestation-service) + - [Environment variables](#environment-variables) + - [Sms Providers](#sms-providers) + - [Nexmo](#nexmo) + - [Twilio](#twilio) + - [Accounts Configuration](#accounts-configuration) \* [Database Configuration](#database-configuration) + - [Executing the Attestation Service](#executing-the-attestation-service) + +As part of the [lightweight identity protocol](/celo-codebase/protocol/identity), validators are expected to run an Attestation Service to provide attestations that allow users to map their phone number to an account on Celo. The Attestation Service is a simple Node.js application that can be run with a Docker image. + +## Environment variables + +The service needs the following environment variables: + +| Variable | Explanation | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| DATABASE_URL | The URL under which your database is accessible, currently supported are `postgres://`, `mysql://` and `sqlite://` | | +| CELO_PROVIDER | The URL under which a celo blockchain node is reachable, i.e. something like `https://integration-forno.celo-testnet.org` | | +| ACCOUNT_ADDRESS | The address of the validator account | | +| ATTESTATION_PRIVATE_KEY | The private key with which attestations should be signed. You could use your account key for attestations, but really you should authorize a dedicated attestation key | | +| APP_SIGNATURE | The hash with which clients can auto-read SMS messages on android | | +| SMS_PROVIDERS | A comma-separated list of providers you want to configure, we currently support `nexmo` & `twilio` | | + +## Sms Providers + +Currently the Sms providers supported are Nexmo & Twilio. You can create your user account in the provider of your election using the [Nexmo Sign Up form](https://dashboard.nexmo.com/sign-up) or the [Twilio Sign Up form](https://www.twilio.com/try-twilio). + +### Nexmo + +Here is the list of the enviromnet variables needed to use the Nexmo SMS broker: + +| Variable | Explanation | +| --------------- | --------------------------------------------------------------- | +| NEXMO_KEY | The API key to the Nexmo API | +| NEXMO_SECRET | The API secret to the Nexmo API | +| NEXMO_BLACKLIST | A comma-sperated list of country codes you do not want to serve | + +### Twilio + +If you prefer using Twilio, this is list of the variables to use: + +| Variable | Explanation | +| ---------------------------- | --------------------------------------------------------------- | +| TWILIO_ACCOUNT_SID | The Twilio account ID | +| TWILIO_MESSAGING_SERVICE_SID | The Twilio Message Service ID. Starts by `MG` | +| TWILIO_AUTH_TOKEN | The API authentication token | +| TWILIO_BLACKLIST | A comma-sperated list of country codes you do not want to serve | + +## Accounts Configuration + +First we need to create an account for getting the attestation key needed to sign the attestations. Run: + +```bash +celocli account:new +``` + +We copy the account details and assign the Private Key to the `ATTESTATION_PRIVATE_KEY` environment variable: + +```bash +export ATTESTATION_PRIVATE_KEY=0x +export ATTESTATION_ADDRESS= +``` + +You can create a proof of posession of this attestation key by running the CLI commands (remember to prefix the key with 0x) + +```bash +celocli account:proof-of-possession --signer $ATTESTATION_ADDRESS --account $CELO_VALIDATOR_ADDRESS --privateKey $ATTESTATION_KEY +``` + +That will give you a signature that you can then use to authorize the key: + +```bash +celocli account:authorize --from $CELO_VALIDATOR_ADDRESS -r attestation --pop SIGNATURE --signer $ATTESTATION_ADDRESS +``` + +The Attestation Service needs to connect to a Web3 Provider. This is going to depend on the network you want to connect. So depending on which network you are making available the service, you need to configure the `CELO_PROVIDER` variable pointing to that. + +For example: + +```bash +# Web3 provider for Alfajores network +export CELO_PROVIDER="https://alfajores-forno.celo-testnet.org/" +``` + +#### Database Configuration + +For storing and retrieving the attestation requests the service needs a database to persist that information. Currently `sqlite`, `postgres` and `mysql` are supported. For testing purposes you can use `sqlite` but it's recommended to run a stand-alone database server using `mysql` or `postgres` if your intention is running the Attestation Service in a production environment. + +So for specifying the database url you need to setup the `DATABASE_URL` variable: + +```bash +export DATABASE_URL="sqlite://db/dev.db" +export DATABASE_URL="mysql://user:password@mysql.example.com:3306/attestation-service" +export DATABASE_URL="postgres://user:password@postgres.example.com:5432/attestation-service" +``` + +You can find the migration scripts for creating the schema at the `celo-monorepo`, `packages/attestation-service` folder. From there, after setting up the `DATABASE_URL` env variable you can run the following commands: + +```bash +yarn run db:create +yarn run db:migrate +``` + +## Executing the Attestation Service + +The following command for running the Attestation Service is using Nexmo, but you can adapt for using Twilio easily: + +```bash +docker run -e ATTESTATION_KEY=$ATTESTATION_KEY -e ACCOUNT_ADDRESS=$CELO_VALIDATOR_ADDRESS -e CELO_PROVIDER=$CELO_PROVIDER -e DATABASE_URL=$DATABASE_URL -e SMS_PROVIDERS=nexmo -e NEXMO_KEY=$NEXMO_KEY -e NEXMO_SECRET=$NEXMO_SECRET -e NEXMO_BLACKLIST=$NEXMO_BLACKLIST -p 3000:80 us.gcr.io/celo-testnet/attestation-service:$CELO_NETWORK +``` + +In order for users to request attestations from your service, you need to register the endpoint under which your service is reachable in your [metadata](/celo-codebase/protocol/identity/metadata). + +```bash +celocli identity:create-metadata ./metadata.json +``` + +The `ATTESTATION_SERVICE_URL` variable stores the URL to access the Attestation Service deployed. In the following command we specify the URL where this Attestation Service is: + +```bash +celocli identity:change-attestation-service-url ./metadata.json --url $ATTESTATION_SERVICE_URL +``` + +And then host your metadata somewhere reachable via HTTP. You can register your metadata URL with: + +```bash +celocli identity:register-metadata --url --from $CELO_VALIDATOR_ADDRESS +``` + +You can use for testing a gist url (i.e: `https://gist.github.com/john.doe/a29f83d478c9daa2ac52596ba9778391`) or similar where you have publicly available your metadata. + +If everything goes well users should see that you are ready for attestations by running: + +```bash +celocli identity:get-metadata $CELO_VALIDATOR_ADDRESS +``` diff --git a/packages/docs/getting-started/using-the-cli.md b/packages/docs/getting-started/using-the-cli.md deleted file mode 100644 index 7185cb2452e..00000000000 --- a/packages/docs/getting-started/using-the-cli.md +++ /dev/null @@ -1,23 +0,0 @@ -# Using the CLI - -This section describes how to make a transaction using the Celo CLI. Doing so is easy and quick once you have fauceted yourself some funds and have a full node running. - -### **Prerequisites** - -- **You have Docker installed.** If you don’t have it already, follow the instructions here: [Get Started with Docker](https://www.docker.com/get-started). It will involve creating or signing in with a Docker account, downloading a desktop app, and then launching the app to be able to use the Docker CLI. If you are running on a Linux server, follow the instructions for your distro [here](https://docs.docker.com/install/#server). You may be required to run Docker with sudo depending on your installation environment. -- **You have celocli installed.** - - See to [Command Line Interface \(CLI\)](../command-line-interface/introduction.md) for instructions on how to get set up. - -- **You have a full node running.** See the [Running a Full Node](running-a-full-node.md) instructions for more details on running a full node. -- **You have fauceted yourself.** See the [Faucet](faucet.md) instructions for help funding your account with testnet tokens. - -### **Sending a payment** - -Unlock your accounts so that you can send transactions: - -`$ celocli account:unlock --account $YOUR_ADDRESS --password ` - -Send a payment to another account: - -`$ celocli account:transferdollar --from $YOUR_ADDRESS --amountInWei $AMOUNT --to $DESTINATION_ADDRESS` diff --git a/packages/faucet/src/protocol/signing-utils.ts b/packages/faucet/src/protocol/signing-utils.ts index 722d2de0b3e..ef641ac3217 100644 --- a/packages/faucet/src/protocol/signing-utils.ts +++ b/packages/faucet/src/protocol/signing-utils.ts @@ -51,15 +51,17 @@ export async function signTransaction(web3: Web3, txn: any, privateKey: string) transaction.data = tx.data || '0x' transaction.value = tx.value || '0x' transaction.chainId = web3.utils.numberToHex(tx.chainId) - transaction.gasCurrency = tx.gasCurrency || '0x' - transaction.gasFeeRecipient = tx.gasFeeRecipient || '0x' + transaction.feeCurrency = tx.feeCurrency || '0x' + transaction.gatewayFeeRecipient = tx.gatewayFeeRecipient || '0x' + transaction.gatewayFee = tx.gatewayFee || '0x' const rlpEncoded = RLP.encode([ bytes.fromNat(transaction.nonce), bytes.fromNat(transaction.gasPrice), bytes.fromNat(transaction.gas), - transaction.gasCurrency.toLowerCase(), - transaction.gasFeeRecipient.toLowerCase(), + transaction.feeCurrency.toLowerCase(), + transaction.gatewayFeeRecipient.toLowerCase(), + bytes.fromNat(transaction.gatewayFee), transaction.to.toLowerCase(), bytes.fromNat(transaction.value), transaction.data, @@ -68,7 +70,7 @@ export async function signTransaction(web3: Web3, txn: any, privateKey: string) '0x', ]) - const messagehash = hash.keccak256(rlpEncoded) + const messageHash = hash.keccak256(rlpEncoded) const signature = Account.makeSigner(nat.toNumber(transaction.chainId || '0x1') * 2 + 35)( hash.keccak256(rlpEncoded), @@ -76,21 +78,21 @@ export async function signTransaction(web3: Web3, txn: any, privateKey: string) ) const rawTx = RLP.decode(rlpEncoded) - .slice(0, 8) + .slice(0, 9) .concat(Account.decodeSignature(signature)) - rawTx[8] = makeEven(trimLeadingZero(rawTx[8])) rawTx[9] = makeEven(trimLeadingZero(rawTx[9])) rawTx[10] = makeEven(trimLeadingZero(rawTx[10])) + rawTx[11] = makeEven(trimLeadingZero(rawTx[11])) const rawTransaction = RLP.encode(rawTx) const values = RLP.decode(rawTransaction) result = { - messagehash, - v: trimLeadingZero(values[8]), - r: trimLeadingZero(values[9]), - s: trimLeadingZero(values[10]), + messageHash, + v: trimLeadingZero(values[9]), + r: trimLeadingZero(values[10]), + s: trimLeadingZero(values[11]), rawTransaction, } } catch (e) { diff --git a/packages/helm-charts/attestation-service/templates/attestation.statefulset.yaml b/packages/helm-charts/attestation-service/templates/attestation.statefulset.yaml index 3ea8034c1f0..98fe876658c 100644 --- a/packages/helm-charts/attestation-service/templates/attestation.statefulset.yaml +++ b/packages/helm-charts/attestation-service/templates/attestation.statefulset.yaml @@ -40,6 +40,8 @@ spec: echo -n $RID >/root/.celo/replica_id echo "Generating private key for rid=$RID" celotooljs.sh generate bip32 --mnemonic "$MNEMONIC" --accountType validator --index $RID > /root/.celo/pkey + celotooljs.sh generate bip32 --mnemonic "$MNEMONIC" --accountType attestation --index $RID > /root/.celo/attestationKey + celotooljs.sh generate account-address --private-key `cat /root/.celo/pkey` > /root/.celo/address env: - name: MNEMONIC valueFrom: @@ -62,7 +64,7 @@ spec: - "-c" - | yarn db:migrate - ATTESTATION_KEY=0x`cat /root/.celo/pkey` yarn start + ATTESTATION_KEY=0x`cat /root/.celo/attestationKey` ACCOUNT_ADDRESS=`cat /root/.celo/address` yarn start ports: - name: http containerPort: 3000 @@ -78,7 +80,11 @@ spec: - name: CELO_PROVIDER value: https://{{ .Release.Namespace }}-forno.{{ .Values.domain.name }}.org - name: APP_SIGNATURE - value: {{ .Values.attestation_service.sms_retriever_hash_code }}} + value: {{ .Values.attestation_service.sms_retriever_hash_code }} + - name: SMS_PROVIDERS + value: nexmo + - name: LOG_FORMAT + value: stackdriver - name: NEXMO_KEY valueFrom: secretKeyRef: diff --git a/packages/helm-charts/ethstats/templates/ethstats.deployment.yaml b/packages/helm-charts/ethstats/templates/ethstats.deployment.yaml index 0e456f7381e..f60ab444caa 100644 --- a/packages/helm-charts/ethstats/templates/ethstats.deployment.yaml +++ b/packages/helm-charts/ethstats/templates/ethstats.deployment.yaml @@ -26,6 +26,33 @@ spec: - name: ethstats image: {{ .Values.ethstats.image.repository }}:{{ .Values.ethstats.image.tag }} imagePullPolicy: {{ .Values.imagePullPolicy }} + command: + - /bin/sh + - -c + args: + - | + cat <<'EOF' > /celostats-server/lib/utils/config.js + var trusted = [ + {{- range .Values.ethstats.trusted_addresses }} + {{- if . }} + "{{lower . }}", + {{- end }} + {{- end }} + ] + var banned = [ + {{- range .Values.ethstats.banned_addresses }} + {{- if . }} + "{{lower . }}", + {{- end }} + {{- end }} + ] + module.exports = { + trusted: trusted, + banned: banned, + reserved: [] + }; + EOF + npm start ports: - name: http containerPort: 3000 @@ -33,12 +60,6 @@ spec: requests: cpu: 100m memory: 250Mi - env: - - name: WS_SECRET - valueFrom: - secretKeyRef: - name: {{ .Release.Name }} - key: WS_SECRET {{- with .Values.nodeSelector }} nodeSelector: {{ toYaml . | indent 8 }} diff --git a/packages/helm-charts/ethstats/templates/ethstats.secret.yaml b/packages/helm-charts/ethstats/templates/ethstats.secret.yaml deleted file mode 100644 index e39b31416f4..00000000000 --- a/packages/helm-charts/ethstats/templates/ethstats.secret.yaml +++ /dev/null @@ -1,14 +0,0 @@ -{{ if .Values.ethstats.createSecret }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ .Release.Name }} - labels: - app: ethstats - chart: ethstats - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -type: Opaque -data: - WS_SECRET: {{ .Values.ethstats.webSocketSecret | b64enc | quote }} -{{ end }} diff --git a/packages/helm-charts/ethstats/values.yaml b/packages/helm-charts/ethstats/values.yaml index 0228995fc9f..2cb2c9e1ee9 100644 --- a/packages/helm-charts/ethstats/values.yaml +++ b/packages/helm-charts/ethstats/values.yaml @@ -10,3 +10,5 @@ ethstats: tag: latest service: type: NodePort + trusted_addresses: [] + banned_addresses: [] diff --git a/packages/helm-charts/testnet/scripts/create-network.sh b/packages/helm-charts/testnet/scripts/create-network.sh index 1253cc5a74f..0845dbb0558 100755 --- a/packages/helm-charts/testnet/scripts/create-network.sh +++ b/packages/helm-charts/testnet/scripts/create-network.sh @@ -4,7 +4,6 @@ set -euo pipefail NAMESPACE="" RELEASE="" DOMAIN_NAME_OPT="" -VERIFICATION_POOL_URL="https://verification-pool-integration.celo.org/v0.1/sms/" ACTION=install TEST_OPT="" ZONE="us-west1-a" @@ -17,7 +16,6 @@ while getopts ':utn:r:z:d:v:a:' flag; do r) RELEASE="${OPTARG}" ;; z) ZONE="${OPTARG}" ;; d) DOMAIN_NAME_OPT="--set domain.name=${OPTARG}" ;; - v) VERIFICATION_POOL_URL="${OPTARG}" ;; a) VERIFICATION_REWARDS_ADDRESS="${OPTARG}" ;; *) echo "Unexpected option ${flag}" ;; esac @@ -55,7 +53,7 @@ if [ "$ACTION" = "install" ]; then echo "Deploying new environment..." helm install ./testnet --name $RELEASE --namespace $NAMESPACE \ - $DOMAIN_NAME_OPT $TEST_OPT --set miner.verificationpool=$VERIFICATION_POOL_URL \ + $DOMAIN_NAME_OPT $TEST_OPT \ --set miner.verificationrewards=$VERIFICATION_REWARDS_ADDRESS \ --set blockscout.db.username=$BLOCKSCOUT_DB_USERNAME \ --set blockscout.db.password=$BLOCKSCOUT_DB_PASSWORD \ @@ -70,7 +68,7 @@ elif [ "$ACTION" = "upgrade" ]; then echo "Upgrading existing environment..." helm upgrade $RELEASE ./testnet \ - $DOMAIN_NAME_OPT $TEST_OPT --set miner.verificationpool=$VERIFICATION_POOL_URL \ + $DOMAIN_NAME_OPT $TEST_OPT \ --set miner.verificationrewards=$VERIFICATION_REWARDS_ADDRESS \ --set blockscout.db.username=$BLOCKSCOUT_DB_USERNAME \ --set blockscout.db.password=$BLOCKSCOUT_DB_PASSWORD \ diff --git a/packages/helm-charts/testnet/templates/_helpers.tpl b/packages/helm-charts/testnet/templates/_helpers.tpl index 9fa6663aebe..6744a723271 100644 --- a/packages/helm-charts/testnet/templates/_helpers.tpl +++ b/packages/helm-charts/testnet/templates/_helpers.tpl @@ -195,10 +195,9 @@ spec: --consoleformat=json \ --consoleoutput=stdout \ --etherbase=${ACCOUNT_ADDRESS} \ - --ethstats=${HOSTNAME}:${ETHSTATS_SECRET}@${ETHSTATS_SVC} \ + --ethstats=${HOSTNAME}@${ETHSTATS_SVC} \ --metrics \ --mine \ - --miner.verificationpool=${VERIFICATION_POOL_URL} \ --networkid=${NETWORK_ID} \ --nodekey=/root/.celo/account/{{ .Node.name}}PrivateKey \ --password=/root/.celo/account/accountSecret \ @@ -239,11 +238,6 @@ spec: env: - name: ETHSTATS_SVC value: {{ template "ethereum.fullname" . }}-ethstats.{{ .Release.Namespace }} - - name: ETHSTATS_SECRET - valueFrom: - secretKeyRef: - name: {{ template "ethereum.fullname" . }}-ethstats - key: WS_SECRET - name: ACCOUNT_ADDRESS value: {{ .Node.address }} - name: NETWORK_ID @@ -251,10 +245,6 @@ spec: configMapKeyRef: name: {{ template "ethereum.fullname" . }}-geth-config key: networkid - - name: VERIFICATION_POOL_URL - value: {{ .Values.geth.miner.verificationpool }} - - name: VERIFICATION_REWARDS_URL - value: {{ .Values.verification.rewardsUrl }} {{ include "celo.geth-exporter-container" . | indent 6 }} {{ include "celo.prom-to-sd-container" (dict "Values" .Values "Release" .Release "Chart" .Chart "component" "geth" "metricsPort" "9200" "metricsPath" "filteredmetrics" "containerNameLabel" .Node.name ) | indent 6 }} initContainers: @@ -312,7 +302,7 @@ spec: - "geth --bootnodes=`cat /root/.celo/bootnodes` \ --consoleformat=json \ --consoleoutput=stdout \ - --ethstats=${HOSTNAME}:${ETHSTATS_SECRET}@${ETHSTATS_SVC} \ + --ethstats=${HOSTNAME}@${ETHSTATS_SVC} \ --lightpeers 250 \ --lightserv 90 \ --metrics \ @@ -352,11 +342,6 @@ spec: env: - name: ETHSTATS_SVC value: {{ template "ethereum.fullname" . }}-ethstats.{{ .Release.Namespace }} - - name: ETHSTATS_SECRET - valueFrom: - secretKeyRef: - name: {{ template "ethereum.fullname" . }}-ethstats - key: WS_SECRET - name: TARGET_GAS_LIMIT value: {{ .Values.geth.genesis.gasLimit | quote }} - name: NETWORK_ID diff --git a/packages/helm-charts/testnet/templates/ethstats.secret.yaml b/packages/helm-charts/testnet/templates/ethstats.secret.yaml deleted file mode 100644 index acbbbb5ad49..00000000000 --- a/packages/helm-charts/testnet/templates/ethstats.secret.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: {{ template "ethereum.fullname" . }}-ethstats - labels: - app: {{ template "ethereum.name" . }} - chart: {{ template "ethereum.chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -type: Opaque -data: - WS_SECRET: {{ .Values.ethstats.webSocketSecret | b64enc | quote }} diff --git a/packages/helm-charts/testnet/templates/txnode.statefulset.yaml b/packages/helm-charts/testnet/templates/txnode.statefulset.yaml index fc2df7e21bb..e32a75fe09f 100644 --- a/packages/helm-charts/testnet/templates/txnode.statefulset.yaml +++ b/packages/helm-charts/testnet/templates/txnode.statefulset.yaml @@ -124,30 +124,21 @@ spec: --networkid=${NETWORK_ID} \ --syncmode=full \ ${NAT_FLAG} \ - --ethstats=${HOSTNAME}:${ETHSTATS_SECRET}@${ETHSTATS_SVC} \ - --miner.verificationpool=${VERIFICATION_POOL_URL} \ + --ethstats=${HOSTNAME}@${ETHSTATS_SVC} \ --consoleformat=json \ --consoleoutput=stdout \ --verbosity={{ .Values.geth.verbosity }} \ - --ethstats=${HOSTNAME}:${ETHSTATS_SECRET}@${ETHSTATS_SVC} \ --metrics \ ${PING_IP_FROM_PACKET_FLAG} \ ${IN_MEMORY_DISCOVERY_TABLE_FLAG} env: - name: ETHSTATS_SVC value: {{ template "ethereum.fullname" . }}-ethstats.{{ .Release.Namespace }} - - name: ETHSTATS_SECRET - valueFrom: - secretKeyRef: - name: {{ template "ethereum.fullname" . }}-ethstats - key: WS_SECRET - name: NETWORK_ID valueFrom: configMapKeyRef: name: {{ template "ethereum.fullname" . }}-geth-config key: networkid - - name: VERIFICATION_POOL_URL - value: {{ .Values.geth.miner.verificationpool }} - name: STATIC_IPS_FOR_GETH_NODES value: "{{ default "false" .Values.geth.static_ips }}" - name: PING_IP_FROM_PACKET diff --git a/packages/helm-charts/testnet/templates/validators.statefulset.yaml b/packages/helm-charts/testnet/templates/validators.statefulset.yaml index e33b39119f6..ed86ddc9d29 100644 --- a/packages/helm-charts/testnet/templates/validators.statefulset.yaml +++ b/packages/helm-charts/testnet/templates/validators.statefulset.yaml @@ -145,12 +145,10 @@ spec: --etherbase=${ACCOUNT_ADDRESS} \ --networkid=${NETWORK_ID} \ --syncmode=full \ - --ethstats=${HOSTNAME}:${ETHSTATS_SECRET}@${ETHSTATS_SVC} \ - --miner.verificationpool=${VERIFICATION_POOL_URL} \ + --ethstats=${HOSTNAME}@${ETHSTATS_SVC} \ --consoleformat=json \ --consoleoutput=stdout \ --verbosity={{ .Values.geth.verbosity }} \ - --ethstats=${HOSTNAME}:${ETHSTATS_SECRET}@${ETHSTATS_SVC} \ ${FAULTY_NODE_FLAGS} \ --istanbul.blockperiod={{ .Values.geth.blocktime }} \ --istanbul.requesttimeout={{ .Values.geth.istanbulrequesttimeout }} \ @@ -167,18 +165,11 @@ spec: fieldPath: status.podIP - name: ETHSTATS_SVC value: {{ template "ethereum.fullname" . }}-ethstats.{{ .Release.Namespace }} - - name: ETHSTATS_SECRET - valueFrom: - secretKeyRef: - name: {{ template "ethereum.fullname" . }}-ethstats - key: WS_SECRET - name: NETWORK_ID valueFrom: configMapKeyRef: name: {{ template "ethereum.fullname" . }}-geth-config key: networkid - - name: VERIFICATION_POOL_URL - value: {{ .Values.geth.miner.verificationpool }} - name: FAULTY_NODES value: {{ .Values.geth.faultyValidators | quote }} - name: FAULTY_NODE_TYPE diff --git a/packages/mobile/README.md b/packages/mobile/README.md index 621cdd665f8..f94f1b4a615 100644 --- a/packages/mobile/README.md +++ b/packages/mobile/README.md @@ -89,19 +89,15 @@ components in react or overloading the should component update method ### 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, 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 -``` - -before rebuilding the app. Note that this will assume the testnets have a corresponding `/blockchain-api` and `/notification-service` set up. +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 rebuild the app. Note that this will assume the testnets +have a corresponding `/blockchain-api` and `/notification-service` set up. ### Running Wallet app in ZeroSync mode By default, the mobile wallet app runs geth in ultralight sync mode where all the epoch headers are fetched. The default sync mode is defined in [packages/mobile/.env](https://github.com/celo-org/celo-monorepo/blob/master/packages/mobile/.env#L4) file. -To run wallet in zero sync mode, where it would connect to the remote nodes and sign transactions in web3, change the default sync mode in the aforementioned file to -1. The mode has only been tested on Android and is hard-coded to be [crash](https://github.com/celo-org/celo-monorepo/blob/aeddeefbfb230db51d2ef76d50c5f882644a1cd3/packages/mobile/src/web3/contracts.ts#L73) on iOS. +To run the wallet in zero sync mode, using a trusted node rather than the local geth node as a provider, turn it on from the Celo Lite page in settings or update the zero sync initially enabled parameter in the .env file linked above. When zero sync mode is turned back off, the wallet will switch to the default sync mode as specified in the .env file. By default, the trusted node is `https://{TESTNET}-forno.celo-testnet.org/`, however any trusted node can be used by updating `DEFAULT_FORNO_URL`. In zero sync mode, the wallet signs transactions locally in web3 then sends them to the trusted node. ## Testing diff --git a/packages/mobile/ios/Podfile.lock b/packages/mobile/ios/Podfile.lock index 9d1a4834a09..6b3ea829a97 100644 --- a/packages/mobile/ios/Podfile.lock +++ b/packages/mobile/ios/Podfile.lock @@ -120,7 +120,7 @@ PODS: - GTMSessionFetcher/Core (1.2.2) - leveldb-library (1.20) - lottie-ios (3.1.3) - - lottie-react-native (3.2.1): + - lottie-react-native (3.3.2): - lottie-ios (~> 3.1.3) - React - nanopb (0.3.901): @@ -661,7 +661,7 @@ SPEC CHECKSUMS: GTMSessionFetcher: 61bb0f61a4cb560030f1222021178008a5727a23 leveldb-library: 08cba283675b7ed2d99629a4bc5fd052cd2bb6a5 lottie-ios: 496ac5cea1bbf1a7bd1f1f472f3232eb1b8d744b - lottie-react-native: b123a79529cc732201091f585c62c89bb4747252 + lottie-react-native: 2a1a82bb326ae51331a5520de0cf706733c6db69 nanopb: 2901f78ea1b7b4015c860c2fdd1ea2fee1a18d48 Protobuf: 1097ca58584c8d9be81bfbf2c5ff5975648dd87a RCTRequired: c639d59ed389cfb1f1203f65c2ea946d8ec586e2 diff --git a/packages/mobile/ios/celo/AppDelegate.m b/packages/mobile/ios/celo/AppDelegate.m index 6e04239cbce..5c5610af6d2 100644 --- a/packages/mobile/ios/celo/AppDelegate.m +++ b/packages/mobile/ios/celo/AppDelegate.m @@ -12,12 +12,14 @@ #import #import #import +#import @import Firebase; #import "RNFirebaseNotifications.h" #import "RNFirebaseMessaging.h" #import "RNSplashScreen.h" + // Use same key as react-native-secure-key-store // so we don't reset already working installs static NSString * const kHasRunBeforeKey = @"RnSksIsAppInstalled"; @@ -93,4 +95,8 @@ - (void)resetKeychainIfNecessary [defaults synchronize]; } +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation +{ + return [RCTLinkingManager application:application openURL:url sourceApplication:sourceApplication annotation:annotation]; +} @end diff --git a/packages/mobile/ios/celo/Info.plist b/packages/mobile/ios/celo/Info.plist index a34ccca2254..fc3e18d9e04 100644 --- a/packages/mobile/ios/celo/Info.plist +++ b/packages/mobile/ios/celo/Info.plist @@ -2,8 +2,6 @@ - UIUserInterfaceStyle - Light CFBundleDevelopmentRegion en CFBundleDisplayName @@ -22,8 +20,21 @@ 1.5.1 CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + celo + + + CFBundleVersion 14 + ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS NSAppTransportSecurity @@ -63,9 +74,9 @@ UIInterfaceOrientationPortrait + UIUserInterfaceStyle + Light UIViewControllerBasedStatusBarAppearance - ITSAppUsesNonExemptEncryption - diff --git a/packages/mobile/locales/en-US/global.json b/packages/mobile/locales/en-US/global.json index 204ccb7b440..d20428da701 100644 --- a/packages/mobile/locales/en-US/global.json +++ b/packages/mobile/locales/en-US/global.json @@ -94,5 +94,8 @@ "calculateFeeFailed": "Could not calculate fee", "failedToSwitchSyncModes": "Failed to switch sync modes", "gold": "Gold", - "localCurrencyTitle": "Select Currency" + "localCurrencyTitle": "Select Currency", + "or": "or", + "accepted": "Accepted", + "processing": "Processing" } diff --git a/packages/mobile/locales/en-US/nuxNamePin1.json b/packages/mobile/locales/en-US/nuxNamePin1.json index d6e699559d4..e5307c16a84 100644 --- a/packages/mobile/locales/en-US/nuxNamePin1.json +++ b/packages/mobile/locales/en-US/nuxNamePin1.json @@ -1,13 +1,6 @@ { - "selectCountry": "Select Country", "chooseLanguage": "Choose Language", "continue": "Continue", - "secureAsset": - "Celo keeps your assets secure and enables you to send and receive value with anyone", - "stableAsset": - "{{CeloDollars}} are a stable digital asset that tracks to the value of the US {{Dollar}}", - "verifyNumber": "Verify your phone number so other Celo users can find you and send you value", - "syncNetwork": "Syncing with Network", "welcomeCelo": "Welcome to Celo Wallet", "chooseCountryCode": "Country Code", "chooseCountry": "Country", @@ -20,33 +13,17 @@ "phoneNumber": "000 000 0000", "inviteCodeText": { "title": "Enter Invite Code", - "copyInvite": { - "0": "Copy invite ", - "1": "from Messages App and come back" - }, - "openMessages": { - "message": "Open Messages", - "hint": { - "0": "Hint ", - "1": "Copy the whole SMS message" - } - }, - "pasteInviteCode": { - "message": "Paste Invite Code!", - "hint": "Looks like you've copied a code" - }, - "validating": "Great! Validating copied invite code", - "inviteAccepted": "🎉 Invite Accepted!", - "askForInvite": { - "0": "Request an invite from a friend on Celo or sign up for the {{testnet}} network at ", - "1": "celo.org/app" - } + "body": + "If you have an invite code, please enter it here. You can copy and paste the entire message.", + "codeHeader": "Invite Code", + "codePlaceholder": "Celo code: am9hBM3tiA+CuNb...", + "noCode": "Don’t have a code? ", + "requestCodeNoFaucet": " Request an invite from a friend with a Celo Wallet or ", + "requestCodeFromFaucet": "Request an invite at ", + "faucetLink": "celo.org/app", + "skip": "continue without." }, - "enterFullName": "Please enter your first and last name", "fullName": "Full Name", - "InvitationCode": "Invitation Code", - "optIn": "Opt In", - "submitting": "Submitting ...", "importIt": "Restore Existing Wallet", "cancel": "Cancel", "important": "Important", diff --git a/packages/mobile/locales/en-US/nuxVerification2.json b/packages/mobile/locales/en-US/nuxVerification2.json index 2ad59cba206..ac63138f251 100644 --- a/packages/mobile/locales/en-US/nuxVerification2.json +++ b/packages/mobile/locales/en-US/nuxVerification2.json @@ -58,7 +58,6 @@ "codeHeader3": "Third Code", "codesMissing": "I didn’t receive three codes", "tip": "Typing? Try copying and pasting the code.", - "codeAccepted": "Accepted", "sendingCodes": "Sending verification codes..." }, "missingCodesModal": { diff --git a/packages/mobile/locales/es-419/global.json b/packages/mobile/locales/es-419/global.json index 438e970a108..b3d4e7b5a29 100755 --- a/packages/mobile/locales/es-419/global.json +++ b/packages/mobile/locales/es-419/global.json @@ -95,5 +95,8 @@ "calculateFeeFailed": "No se pudo calcular la comisión", "failedToSwitchSyncModes": "Error al cambiar de red modos", "gold": "Oro", - "localCurrencyTitle": "Seleccione el tipo de moneda" + "localCurrencyTitle": "Seleccione el tipo de moneda", + "or": "o", + "accepted": "Aceptado", + "processing": "Procesando" } diff --git a/packages/mobile/locales/es-419/nuxNamePin1.json b/packages/mobile/locales/es-419/nuxNamePin1.json index 5e5149443a9..a213c68becf 100755 --- a/packages/mobile/locales/es-419/nuxNamePin1.json +++ b/packages/mobile/locales/es-419/nuxNamePin1.json @@ -1,14 +1,6 @@ { - "selectCountry": "Seleccionar país", "chooseLanguage": "Elegir idioma", "continue": "Continuar", - "secureAsset": - "Celo mantiene tu patrimonio seguro y le permite enviar capital a quien quiera y recibirlos de cualquier persona", - "stableAsset": - "Los {{CeloDollars}} constituyen un activo digital estable que está alineado con el valor del dólar estadounidense", - "verifyNumber": - "Verifique tu número de teléfono para conectarse con otros usuarios y recibir dinero", - "syncNetwork": "Sincronizando con la red", "welcomeCelo": "Te damos la bienvenida a Celo", "chooseCountryCode": "Código de país", "chooseCountry": "País", @@ -20,34 +12,18 @@ }, "phoneNumber": "000 000 0000", "inviteCodeText": { - "title": "Ingresa el código de invitación", - "copyInvite": { - "0": "Copia el código de invitación ", - "1": "desde la aplicación de mensajes y vuelve." - }, - "openMessages": { - "message": "Abrir mensajes", - "hint": { - "0": "Pista: ", - "1": "Copia el mensaje SMS completo" - } - }, - "pasteInviteCode": { - "message": "Pegar código de invitación!", - "hint": "Parece que has copiado un código" - }, - "validating": "Genial! Validando el código de invitación copiado", - "inviteAccepted": "🎉 Invitación aceptada!", - "askForInvite": { - "0": "Solicite una invitación de un amigo en Celo o regístrese en la red {{testnet}} en ", - "1": "celo.org/app" - } + "title": "Ingresa el Código de Invitación", + "body": + "Si tiene un código de invitación, ingréselo aquí. Puede copiar y pegar todo el mensaje.", + "codeHeader": "Código de Invitación", + "codePlaceholder": "Celo código: am9hBM3tiA+CuNb...", + "noCode": "¿No tienen un código? ", + "requestCodeNoFaucet": " Solicite una invitación de un amigo con una billetera Celo o ", + "requestCodeFromFaucet": "Solicite una invitación en ", + "faucetLink": "celo.org/app", + "skip": "continuar sin." }, - "enterFullName": "Ingresa tu nombre y apellido", "fullName": "Nombre completo", - "InvitationCode": "Código de invitación", - "optIn": "Inscribirse", - "submitting": "Enviando ...", "importIt": "Restaurar tu monedero existente", "cancel": "Cancelar", "important": "Importante", diff --git a/packages/mobile/locales/es-419/nuxVerification2.json b/packages/mobile/locales/es-419/nuxVerification2.json index a68e321ebb1..c7ed7ebcf97 100755 --- a/packages/mobile/locales/es-419/nuxVerification2.json +++ b/packages/mobile/locales/es-419/nuxVerification2.json @@ -60,7 +60,6 @@ "codeHeader3": "Tercer código", "codesMissing": "No recibí tres códigos", "tip": "¿Está escribiendo el código? ¿Por qué no lo copia y pega?", - "codeAccepted": "Aceptado", "sendingCodes": "Envío de códigos de verificación..." }, "missingCodesModal": { diff --git a/packages/mobile/package.json b/packages/mobile/package.json index ec7b799ec48..ef30ddb2c93 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -72,7 +72,7 @@ "js-sha3": "^0.7.0", "lodash": "^4.17.14", "lottie-ios": "3.1.3", - "lottie-react-native": "^3.2.1", + "lottie-react-native": "^3.3.2", "moment": "^2.22.1", "moment-timezone": "^0.5.23", "node-libs-react-native": "^1.0.3", diff --git a/packages/mobile/src/app/App.tsx b/packages/mobile/src/app/App.tsx index fd16b3c3afb..852fbc3c0e9 100644 --- a/packages/mobile/src/app/App.tsx +++ b/packages/mobile/src/app/App.tsx @@ -11,9 +11,9 @@ import { PersistGate } from 'redux-persist/integration/react' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { DefaultEventNames } from 'src/analytics/constants' import { apolloClient } from 'src/apollo/index' +import { openDeepLink } from 'src/app/actions' import AppLoading from 'src/app/AppLoading' import ErrorBoundary from 'src/app/ErrorBoundary' -import { handleDeepLink } from 'src/app/saga' import i18n from 'src/i18n' import Navigator from 'src/navigator/NavigatorWrapper' import { persistor, store } from 'src/redux/store' @@ -62,7 +62,7 @@ export class App extends React.Component { } handleOpenURL = (event: any) => { - handleDeepLink(event.url) + store.dispatch(openDeepLink(event.url)) } hideSplashScreen() { diff --git a/packages/mobile/src/app/actions.ts b/packages/mobile/src/app/actions.ts index 804cfaaab4d..997cfa5afcc 100644 --- a/packages/mobile/src/app/actions.ts +++ b/packages/mobile/src/app/actions.ts @@ -10,6 +10,7 @@ export enum Actions { SET_LOGGED_IN = 'APP/SET_LOGGED_IN', SET_NUMBER_VERIFIED = 'APP/SET_NUMBER_VERIFIED', SET_LANGUAGE = 'APP/SET_LANGUAGE', + OPEN_DEEP_LINK = 'APP/OPEN_DEEP_LINK', RESET_APP_OPENED_STATE = 'APP/RESET_APP_OPENED_STATE', ENTER_BACKUP_FLOW = 'APP/ENTER_BACKUP_FLOW', EXIT_BACKUP_FLOW = 'APP/EXIT_BACKUP_FLOW', @@ -35,6 +36,11 @@ export interface SetLanguage { language: string } +export interface OpenDeepLink { + type: Actions.OPEN_DEEP_LINK + deepLink: string +} + interface ResetAppOpenedState { type: Actions.RESET_APP_OPENED_STATE } @@ -71,6 +77,7 @@ export type ActionTypes = | SetNumberVerifiedAction | ResetAppOpenedState | SetLanguage + | OpenDeepLink | EnterBackupFlow | ExitBackupFlow | SetAnalyticsEnabled @@ -101,6 +108,13 @@ export const setLanguage = (language: string, nextScreen?: Screens) => { } } +export const openDeepLink = (deepLink: string) => { + return { + type: Actions.OPEN_DEEP_LINK, + deepLink, + } +} + export const resetAppOpenedState = () => ({ type: Actions.RESET_APP_OPENED_STATE, }) diff --git a/packages/mobile/src/app/saga.test.ts b/packages/mobile/src/app/saga.test.ts index 419f7df6f5b..3541e0cb2cc 100644 --- a/packages/mobile/src/app/saga.test.ts +++ b/packages/mobile/src/app/saga.test.ts @@ -3,15 +3,19 @@ import { expectSaga } from 'redux-saga-test-plan' import { call, select } from 'redux-saga/effects' import { getPincode } from 'src/account/saga' import CeloAnalytics from 'src/analytics/CeloAnalytics' -import { finishPinVerification, startPinVerification } from 'src/app/actions' +import { finishPinVerification, openDeepLink, startPinVerification } from 'src/app/actions' import { checkAppDeprecation, + handleDeepLink, navigatePinProtected, navigateToProperScreen, waitForRehydrate, } from 'src/app/saga' +import { handleDappkitDeepLink } from 'src/dappkit/dappkit' import { isAppVersionDeprecated } from 'src/firebase/firebase' import { UNLOCK_DURATION } from 'src/geth/consts' +import { receiveAttestationMessage } from 'src/identity/actions' +import { CodeInputType } from 'src/identity/verification' import { NavActions, navigate } from 'src/navigator/NavigationService' import { Screens, Stacks } from 'src/navigator/Screens' import { web3 } from 'src/web3/contracts' @@ -22,6 +26,8 @@ jest.mock('src/utils/time', () => ({ clockInSync: () => true, })) +jest.mock('src/dappkit/dappkit') + const MockedAnalytics = CeloAnalytics as any const initialState = { @@ -90,6 +96,18 @@ describe('App saga', () => { .run() expect(navigate).toHaveBeenCalledWith(testRoute.routeName, testRoute.params) }) + + it('Handles Dappkit deep link', async () => { + const deepLink = 'celo://wallet/dappkit?abcdsa' + await expectSaga(handleDeepLink, openDeepLink(deepLink)).run() + expect(handleDappkitDeepLink).toHaveBeenCalledWith(deepLink) + }) + + it('Handles verification deep link', async () => { + await expectSaga(handleDeepLink, openDeepLink('celo://wallet/v/12345')) + .put(receiveAttestationMessage('12345', CodeInputType.DEEP_LINK)) + .run() + }) }) navigationSagaTest('Navigates to the nux stack with no state', null, Stacks.NuxStack) diff --git a/packages/mobile/src/app/saga.ts b/packages/mobile/src/app/saga.ts index 69fd8c5be70..b7539178246 100644 --- a/packages/mobile/src/app/saga.ts +++ b/packages/mobile/src/app/saga.ts @@ -8,6 +8,7 @@ import { Actions, finishPinVerification, NavigatePinProtected, + OpenDeepLink, setLanguage, startPinVerification, } from 'src/app/actions' @@ -15,6 +16,8 @@ import { ErrorMessages } from 'src/app/ErrorMessages' import { handleDappkitDeepLink } from 'src/dappkit/dappkit' import { isAppVersionDeprecated } from 'src/firebase/firebase' import { UNLOCK_DURATION } from 'src/geth/consts' +import { receiveAttestationMessage } from 'src/identity/actions' +import { CodeInputType } from 'src/identity/verification' import { NavActions, navigate } from 'src/navigator/NavigationService' import { Screens, Stacks } from 'src/navigator/Screens' import { PersistedRootState } from 'src/redux/reducers' @@ -24,6 +27,7 @@ import { toggleZeroSyncMode } from 'src/web3/actions' import { isInitiallyZeroSyncMode, web3 } from 'src/web3/contracts' import { getAccount } from 'src/web3/saga' import { zeroSyncSelector } from 'src/web3/selectors' +import { parse } from 'url' const TAG = 'app/saga' @@ -130,10 +134,19 @@ export function* navigateToProperScreen() { } } -export function handleDeepLink(deepLink: string) { +export function* handleDeepLink(action: OpenDeepLink) { + const { deepLink } = action Logger.debug(TAG, 'Handling deep link', deepLink) - handleDappkitDeepLink(deepLink) - // Other deep link handlers can go here later + const rawParams = parse(deepLink, true) + if (rawParams.path) { + if (rawParams.path.startsWith('/v/')) { + yield put(receiveAttestationMessage(rawParams.path.substr(3), CodeInputType.DEEP_LINK)) + } + + if (rawParams.path.startsWith('/dappkit')) { + handleDappkitDeepLink(deepLink) + } + } } export function* navigatePinProtected(action: NavigatePinProtected) { @@ -161,9 +174,14 @@ export function* watchNavigatePinProtected() { yield takeLatest(Actions.NAVIGATE_PIN_PROTECTED, navigatePinProtected) } +export function* watchDeepLinks() { + yield takeLatest(Actions.OPEN_DEEP_LINK, handleDeepLink) +} + export function* appSaga() { yield spawn(navigateToProperScreen) yield spawn(toggleToProperSyncMode) yield spawn(checkAppDeprecation) yield spawn(watchNavigatePinProtected) + yield spawn(watchDeepLinks) } diff --git a/packages/mobile/src/components/Carousel.tsx b/packages/mobile/src/components/Carousel.tsx index ab07bf35d5c..872bda6836f 100644 --- a/packages/mobile/src/components/Carousel.tsx +++ b/packages/mobile/src/components/Carousel.tsx @@ -25,7 +25,7 @@ export interface CarouselItem { function renderItem({ item, index }: { item: CarouselItem; index: number }) { return ( - + {item.icon} @@ -43,7 +43,7 @@ function Carousel(props: OwnProps) { return ( {/* For some reason the carousel is adding a bunch of item height, wrapping to cut it off*/} - + { + it('renders correctly for all CodeRowStatus states', () => { + ;[ + CodeRowStatus.DISABLED, + CodeRowStatus.INPUTTING, + CodeRowStatus.PROCESSING, + CodeRowStatus.RECEIVED, + CodeRowStatus.ACCEPTED, + ].map((status) => { + const { toJSON } = render( + + ) + expect(toJSON()).toMatchSnapshot() + }) + }) +}) diff --git a/packages/mobile/src/components/CodeRow.tsx b/packages/mobile/src/components/CodeRow.tsx new file mode 100644 index 00000000000..152c82fe1f0 --- /dev/null +++ b/packages/mobile/src/components/CodeRow.tsx @@ -0,0 +1,155 @@ +import TextInput from '@celo/react-components/components/TextInput' +import withTextInputPasteAware from '@celo/react-components/components/WithTextInputPasteAware' +import Checkmark from '@celo/react-components/icons/Checkmark' +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 { ActivityIndicator, StyleSheet, Text, View } from 'react-native' +import { Namespaces } from 'src/i18n' + +const CodeInput = withTextInputPasteAware(TextInput) + +export enum CodeRowStatus { + DISABLED, // input disabled + INPUTTING, // input enabled + PROCESSING, // is the inputted code being processed + RECEIVED, // is the inputted code recieved but not yet confirmed + ACCEPTED, // has the code been accepted and completed +} + +export interface CodeRowProps { + status: CodeRowStatus + inputValue: string + inputPlaceholder: string + onInputChange: (value: string) => void + shouldShowClipboard: (value: string) => boolean +} + +type Props = CodeRowProps & WithNamespaces + +function CodeRow({ + status, + inputValue, + inputPlaceholder, + onInputChange, + shouldShowClipboard, + t, +}: Props) { + if (status === CodeRowStatus.DISABLED) { + return ( + + {inputPlaceholder} + + ) + } + + if (status === CodeRowStatus.INPUTTING) { + return ( + + ) + } + + const shortenedInput = inputValue && inputValue.substr(0, 25) + '...' + + if (status === CodeRowStatus.PROCESSING) { + return ( + + {shortenedInput || t('processing')} + + + ) + } + + if (status === CodeRowStatus.RECEIVED) { + return ( + + + {shortenedInput || t('processing')} + + + ) + } + + if (status === CodeRowStatus.ACCEPTED) { + return ( + + + {shortenedInput || t('accepted')} + + + + + + ) + } + + return null +} + +const styles = StyleSheet.create({ + codeInput: { + ...componentStyles.roundedBorder, + flex: 0, + backgroundColor: '#FFF', + borderColor: colors.inputBorder, + height: 50, + marginVertical: 5, + }, + codeReceivedContainer: { + justifyContent: 'center', + marginVertical: 5, + paddingHorizontal: 10, + backgroundColor: colors.darkLightest, + borderRadius: 3, + height: 50, + }, + checkmarkContainer: { + backgroundColor: colors.darkLightest, + position: 'absolute', + top: 3, + right: 3, + padding: 10, + }, + codeProcessingContainer: { + ...componentStyles.roundedBorder, + backgroundColor: '#FFF', + position: 'relative', + justifyContent: 'center', + marginVertical: 5, + paddingHorizontal: 10, + borderColor: colors.inputBorder, + height: 50, + }, + codeInputSpinner: { + backgroundColor: '#FFF', + position: 'absolute', + top: 5, + right: 3, + padding: 10, + }, + codeInputDisabledContainer: { + justifyContent: 'center', + paddingHorizontal: 10, + marginVertical: 5, + borderColor: colors.inputBorder, + borderRadius: 3, + borderWidth: 1, + height: 50, + backgroundColor: '#F0F0F0', + }, + codeValue: { + ...fontStyles.body, + fontSize: 15, + color: colors.darkSecondary, + }, +}) + +export default withNamespaces(Namespaces.global)(CodeRow) diff --git a/packages/mobile/src/components/__snapshots__/CodeRow.test.tsx.snap b/packages/mobile/src/components/__snapshots__/CodeRow.test.tsx.snap new file mode 100644 index 00000000000..1ac49b96b1d --- /dev/null +++ b/packages/mobile/src/components/__snapshots__/CodeRow.test.tsx.snap @@ -0,0 +1,232 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CodeRow renders correctly for all CodeRowStatus states 1`] = ` + + + placeholder + + +`; + +exports[`CodeRow renders correctly for all CodeRowStatus states 2`] = ` + + + + + +`; + +exports[`CodeRow renders correctly for all CodeRowStatus states 3`] = ` + + + test... + + + +`; + +exports[`CodeRow renders correctly for all CodeRowStatus states 4`] = ` + + + test... + + +`; + +exports[`CodeRow renders correctly for all CodeRowStatus states 5`] = ` + + + test... + + + + + + + + +`; diff --git a/packages/mobile/src/dappkit/dappkit.ts b/packages/mobile/src/dappkit/dappkit.ts index ae469cca7fc..3e1a848bd87 100644 --- a/packages/mobile/src/dappkit/dappkit.ts +++ b/packages/mobile/src/dappkit/dappkit.ts @@ -68,7 +68,7 @@ function* produceTxSignature(action: RequestTxSignatureAction) { nonce: tx.nonce, value: tx.value, // @ts-ignore - gasCurrency: action.request.gasCurrency, + feeCurrency: action.request.feeCurrency, } if (tx.to) { params.to = tx.to diff --git a/packages/mobile/src/escrow/saga.ts b/packages/mobile/src/escrow/saga.ts index 063a0403996..803a6caeb36 100644 --- a/packages/mobile/src/escrow/saga.ts +++ b/packages/mobile/src/escrow/saga.ts @@ -173,7 +173,7 @@ export async function getReclaimEscrowGas(account: string, paymentID: string) { const tx = await createReclaimTransaction(paymentID) const txParams = { from: account, - gasCurrency: (await getStableTokenContract(web3))._address, + feeCurrency: (await getStableTokenContract(web3))._address, } const gas = new BigNumber(await tx.estimateGas(txParams)) Logger.debug(`${TAG}/getReclaimEscrowGas`, `Estimated gas of ${gas.toString()}}`) diff --git a/packages/mobile/src/home/WalletHome.test.tsx b/packages/mobile/src/home/WalletHome.test.tsx index c812edc72f8..3dcf5f10efb 100644 --- a/packages/mobile/src/home/WalletHome.test.tsx +++ b/packages/mobile/src/home/WalletHome.test.tsx @@ -2,7 +2,7 @@ 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' +import { createMockStore, createMockStoreAppDisconnected, getMockI18nProps } from 'test/utils' const TWO_DAYS_MS = 2 * 24 * 60 * 1000 @@ -54,4 +54,54 @@ describe('Testnet banner', () => { expect(tree).toMatchSnapshot() expect(showMessageMock).toHaveBeenCalledWith('testnetAlert.1', 5000, null, 'testnetAlert.0') }) + it('Renders when disconnected', async () => { + const store = createMockStoreAppDisconnected() + const tree = renderer.create( + + + + ) + expect(tree).toMatchSnapshot() + }) + it('Renders when connected with backup complete', async () => { + const store = createMockStore() + const tree = renderer.create( + + + + ) + expect(tree).toMatchSnapshot() + }) }) diff --git a/packages/mobile/src/home/WalletHome.tsx b/packages/mobile/src/home/WalletHome.tsx index d70a9707fa2..f25a84032d2 100644 --- a/packages/mobile/src/home/WalletHome.tsx +++ b/packages/mobile/src/home/WalletHome.tsx @@ -260,7 +260,7 @@ const styles = StyleSheet.create({ backgroundColor: colors.background, position: 'relative', }, - banner: { paddingVertical: 15 }, + banner: { paddingVertical: 15, marginTop: 50 }, containerFeed: { paddingBottom: 40, }, diff --git a/packages/mobile/src/home/__snapshots__/WalletHome.test.tsx.snap b/packages/mobile/src/home/__snapshots__/WalletHome.test.tsx.snap index 2d96405d159..f527ca797e0 100644 --- a/packages/mobile/src/home/__snapshots__/WalletHome.test.tsx.snap +++ b/packages/mobile/src/home/__snapshots__/WalletHome.test.tsx.snap @@ -1,5 +1,1370 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Testnet banner Renders when connected with backup complete 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} + > + + + + + + + $ + + + 0.00 + + + + + + + + + 0.00 + + + + global:celoDollars + + + + + + + + activity + + + + + + + + + +`; + +exports[`Testnet banner Renders when disconnected 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + connectingToCelo + + + + + + + + + + + + + + + + + + + + + + + + + + } + 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} + > + + + + + + + $ + + + 0.00 + + + + + + + + + 0.00 + + + + global:celoDollars + + + + + + + + activity + + + + + + + + + +`; + exports[`Testnet banner Shows testnet banner for 5 seconds 1`] = ` { - static defaultProps = { - width: 40, - } - animation: LottieView | null | undefined - componentDidMount() { - // Note(Rossy): This should not be required but the animation does not autoplay on iOS - // Possibly related: https://github.com/react-native-community/lottie-react-native/issues/581 - setTimeout(() => { - if (this.animation) { - this.animation.play() - } - }, 10) - } - render() { return ( { this.animation = animation }} source={require('./dancingRings.json')} - autoPlay={false} + autoPlay={true} loop={false} style={style} onAnimationFinish={this.props.onAnimationFinish} diff --git a/packages/mobile/src/icons/LoadingSpinner.tsx b/packages/mobile/src/icons/LoadingSpinner.tsx index 07751c1b539..e729e21baeb 100644 --- a/packages/mobile/src/icons/LoadingSpinner.tsx +++ b/packages/mobile/src/icons/LoadingSpinner.tsx @@ -12,16 +12,6 @@ export default class LoadingSpinner extends React.PureComponent { animation: LottieView | null | undefined - componentDidMount() { - // Note(Rossy): This should not be required but the animation does not autoplay on iOS - // Possibly related: https://github.com/react-native-community/lottie-react-native/issues/581 - setTimeout(() => { - if (this.animation) { - this.animation.play() - } - }, 10) - } - render() { return ( { } export const attestationCodesSelector = (state: RootState) => state.identity.attestationCodes +export const acceptedAttestationCodesSelector = (state: RootState) => + state.identity.acceptedAttestationCodes export const e164NumberToAddressSelector = (state: RootState) => state.identity.e164NumberToAddress export const addressToE164NumberSelector = (state: RootState) => state.identity.addressToE164Number diff --git a/packages/mobile/src/identity/verification.ts b/packages/mobile/src/identity/verification.ts index 95405fd427d..42cf99b5e37 100644 --- a/packages/mobile/src/identity/verification.ts +++ b/packages/mobile/src/identity/verification.ts @@ -1,6 +1,6 @@ +import { eqAddress } from '@celo/utils/src/address' 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, @@ -40,7 +40,7 @@ import { resetVerification, setVerificationStatus, } from 'src/identity/actions' -import { attestationCodesSelector } from 'src/identity/reducer' +import { acceptedAttestationCodesSelector, attestationCodesSelector } from 'src/identity/reducer' import { startAutoSmsRetrieval } from 'src/identity/smsRetrieval' import { sendTransaction, sendTransactionPromises } from 'src/transactions/send' import Logger from 'src/utils/Logger' @@ -73,6 +73,7 @@ export enum VerificationStatus { export enum CodeInputType { AUTOMATIC = 'automatic', MANUAL = 'manual', + DEEP_LINK = 'deepLink', } export interface AttestationCode { @@ -365,20 +366,23 @@ function attestationCodeReceiver( throw new Error('No code extracted from message') } - const issuer = findMatchingIssuer(e164NumberHash, account, code, allIssuers) - if (!issuer) { - throw new Error('No issuer found for attestion code') - } - - const existingCode = yield call(getCodeForIssuer, issuer) + const existingCode = yield call(isCodeAlreadyAccepted, code) if (existingCode) { Logger.warn(TAG + '@attestationCodeReceiver', 'Code already exists in store, skipping.') - if (action.inputType === CodeInputType.MANUAL) { + if ( + CodeInputType.MANUAL === action.inputType || + CodeInputType.DEEP_LINK === action.inputType + ) { yield put(showError(ErrorMessages.REPEAT_ATTESTATION_CODE)) } return } + const issuer = findMatchingIssuer(e164NumberHash, account, code, allIssuers) + if (!issuer) { + throw new Error('No issuer found for attestion code') + } + Logger.debug(TAG + '@attestationCodeReceiver', `Received code for issuer ${issuer}`) CeloAnalytics.track(CustomEventNames.verification_validate_code_start, { issuer }) @@ -409,6 +413,11 @@ function* getCodeForIssuer(issuer: string) { return existingCodes.find((c) => c.issuer === issuer) } +function* isCodeAlreadyAccepted(code: string) { + const existingCodes: AttestationCode[] = yield select(acceptedAttestationCodesSelector) + return existingCodes.find((c) => c.code === code) +} + function* revealNeededAttestations( attestationsContract: AttestationsType, account: string, @@ -487,10 +496,7 @@ function* setAccount(attestationsContract: AttestationsType, address: string, da Logger.debug(TAG, 'Setting wallet address and public data encryption key') const currentWalletAddress = yield call(getWalletAddress, attestationsContract, address) const currentWalletDEK = yield call(getDataEncryptionKey, attestationsContract, address) - if ( - !areAddressesEqual(currentWalletAddress, address) || - !areAddressesEqual(currentWalletDEK, dataKey) - ) { + if (!eqAddress(currentWalletAddress, address) || !eqAddress(currentWalletDEK, dataKey)) { const setAccountTx = makeSetAccountTx(attestationsContract, address, dataKey) yield call(sendTransaction, setAccountTx, address, TAG, `Set Wallet Address & DEK`) CeloAnalytics.track(CustomEventNames.verification_set_account) diff --git a/packages/mobile/src/import/saga.ts b/packages/mobile/src/import/saga.ts index f3d40c96680..5be02a1c3bf 100644 --- a/packages/mobile/src/import/saga.ts +++ b/packages/mobile/src/import/saga.ts @@ -1,4 +1,4 @@ -import { ensureHexLeader } from '@celo/utils/src/signatureUtils' +import { ensureHexLeader } from '@celo/utils/src/address' import BigNumber from 'bignumber.js' import { validateMnemonic } from 'bip39' import { mnemonicToSeedHex } from 'react-native-bip39' diff --git a/packages/mobile/src/invite/EnterInviteCode.test.tsx b/packages/mobile/src/invite/EnterInviteCode.test.tsx index 3dd617b1e88..92d00605a44 100644 --- a/packages/mobile/src/invite/EnterInviteCode.test.tsx +++ b/packages/mobile/src/invite/EnterInviteCode.test.tsx @@ -2,15 +2,9 @@ import * as React from 'react' import { Clipboard } from 'react-native' import RNInstallReferrer from 'react-native-install-referrer' import SendIntentAndroid from 'react-native-send-intent' -import { - fireEvent, - flushMicrotasksQueue, - render, - waitForElement, -} from 'react-native-testing-library' +import { fireEvent, flushMicrotasksQueue, render } from 'react-native-testing-library' import { Provider } from 'react-redux' import * as renderer from 'react-test-renderer' -import { ErrorMessages } from 'src/app/ErrorMessages' import EnterInviteCode, { EnterInviteCode as EnterInviteCodeClass, } from 'src/invite/EnterInviteCode' @@ -66,36 +60,27 @@ describe('EnterInviteCode Screen', () => { expect(tree).toMatchSnapshot() }) - it('renders with an error', () => { - const store = createMockStore({ alert: { underlyingError: ErrorMessages.INVALID_INVITATION } }) - const tree = renderer.create( - - - - ) - expect(tree).toMatchSnapshot() - }) - it('works with partial invite text in clipboard', async () => { - const store = createMockStore() const redeem = jest.fn() clipboardGetStringMock.mockResolvedValue(PARTIAL_INVITE) const wrapper = render( - + ) - const button = await waitForElement(() => wrapper.getByTestId('pasteMessageButton')) - fireEvent.press(button) + const input = wrapper.getByPlaceholder('inviteCodeText.codePlaceholder') + fireEvent.changeText(input, VALID_INVITE) await flushMicrotasksQueue() expect(redeem).toHaveBeenCalledWith(PARTIAL_INVITE_KEY) }) @@ -103,22 +88,22 @@ describe('EnterInviteCode Screen', () => { it('calls redeem invite with valid invite key in clipboard', async () => { const redeem = jest.fn() clipboardGetStringMock.mockResolvedValue(VALID_INVITE) - const wrapper = render( + render( ) - const button = await waitForElement(() => wrapper.getByTestId('pasteMessageButton')) - fireEvent.press(button) await flushMicrotasksQueue() expect(redeem).toHaveBeenCalledWith(VALID_INVITE_KEY) }) @@ -126,68 +111,69 @@ describe('EnterInviteCode Screen', () => { it('does not proceed with an invalid invite key in clipboard', async () => { const redeem = jest.fn() clipboardGetStringMock.mockResolvedValue('abc') - const wrapper = render( + render( ) - fireEvent.press(wrapper.getByTestId('openMessageButton')) await flushMicrotasksQueue() - expect(wrapper.queryByTestId('pasteMessageButton')).toBeNull() expect(redeem).not.toHaveBeenCalled() }) it('calls redeem invite with valid invite key in install referrer data', async () => { const redeem = jest.fn() getReferrerMock.mockResolvedValue(VALID_REFERRER_INVITE) - const wrapper = render( + render( ) - const button = await waitForElement(() => wrapper.getByTestId('pasteMessageButton')) - fireEvent.press(button) + await flushMicrotasksQueue() expect(redeem).toHaveBeenCalledWith(VALID_REFERRER_INVITE_KEY) }) it('does not proceed with an invalid invite key in install referrer data', async () => { const redeem = jest.fn() getReferrerMock.mockResolvedValue(INVALID_REFERRER_INVITE) - const wrapper = render( + render( ) - fireEvent.press(wrapper.getByTestId('openMessageButton')) await flushMicrotasksQueue() - expect(wrapper.queryByTestId('pasteMessageButton')).toBeNull() expect(redeem).not.toHaveBeenCalled() }) }) diff --git a/packages/mobile/src/invite/EnterInviteCode.tsx b/packages/mobile/src/invite/EnterInviteCode.tsx index 3f4240df3a0..ca89f911e31 100644 --- a/packages/mobile/src/invite/EnterInviteCode.tsx +++ b/packages/mobile/src/invite/EnterInviteCode.tsx @@ -1,61 +1,49 @@ import Button, { BtnTypes } from '@celo/react-components/components/Button' -import SmallButton from '@celo/react-components/components/SmallButton' -import SmsCeloSwap from '@celo/react-components/icons/SmsCeloSwap' +import KeyboardAwareScrollView from '@celo/react-components/components/KeyboardAwareScrollView' +import KeyboardSpacer from '@celo/react-components/components/KeyboardSpacer' 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 _ from 'lodash' import * as React from 'react' import { WithNamespaces, withNamespaces } from 'react-i18next' -import { - ActivityIndicator, - AppState, - AppStateStatus, - Clipboard, - Platform, - ScrollView, - StyleSheet, - Text, - View, -} from 'react-native' +import { ActivityIndicator, Clipboard, StyleSheet, Text, View } from 'react-native' import SafeAreaView from 'react-native-safe-area-view' -import SendIntentAndroid from 'react-native-send-intent' import { connect } from 'react-redux' import { hideAlert, showError } from 'src/alert/actions' import { componentWithAnalytics } from 'src/analytics/wrapper' -import { ErrorMessages } from 'src/app/ErrorMessages' +import CodeRow, { CodeRowStatus } from 'src/components/CodeRow' import DevSkipButton from 'src/components/DevSkipButton' -import { CELO_FAUCET_LINK, DEFAULT_TESTNET, SHOW_GET_INVITE_LINK } from 'src/config' +import { CELO_FAUCET_LINK, SHOW_GET_INVITE_LINK } from 'src/config' import { Namespaces } from 'src/i18n' -import { redeemInvite } from 'src/invite/actions' +import { redeemInvite, skipInvite } from 'src/invite/actions' import { extractValidInviteCode, getValidInviteCodeFromReferrerData } from 'src/invite/utils' import { nuxNavigationOptionsNoBackButton } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { RootState } from 'src/redux/reducers' import { navigateToURI } from 'src/utils/linking' -import Logger from 'src/utils/Logger' import { currentAccountSelector } from 'src/web3/selectors' interface StateProps { redeemComplete: boolean isRedeemingInvite: boolean + isSkippingInvite: boolean account: string | null } interface State { - appState: AppStateStatus - validCode: string | null + inputValue: string } interface DispatchProps { redeemInvite: typeof redeemInvite + skipInvite: typeof skipInvite showError: typeof showError hideAlert: typeof hideAlert } const mapDispatchToProps = { redeemInvite, + skipInvite, showError, hideAlert, } @@ -64,6 +52,7 @@ const mapStateToProps = (state: RootState): StateProps => { return { redeemComplete: state.invite.redeemComplete, isRedeemingInvite: state.invite.isRedeemingInvite, + isSkippingInvite: state.invite.isSkippingInvite, account: currentAccountSelector(state), } } @@ -74,64 +63,28 @@ export class EnterInviteCode extends React.Component { static navigationOptions = nuxNavigationOptionsNoBackButton state: State = { - appState: AppState.currentState, - validCode: null, + inputValue: '', } async componentDidMount() { - AppState.addEventListener('change', this.handleAppStateChange) await this.checkIfValidCodeInClipboard() await this.checkForReferrerCode() } - componentWillUnmount() { - AppState.removeEventListener('change', this.handleAppStateChange) - } - checkForReferrerCode = async () => { const validCode = await getValidInviteCodeFromReferrerData() if (validCode) { - this.setState({ validCode }) + this.setState({ inputValue: validCode }) + this.props.redeemInvite(validCode) } } checkIfValidCodeInClipboard = async () => { const message = await Clipboard.getString() - const validCode = extractValidInviteCode(message) - if (validCode) { - this.setState({ validCode }) - } - } - - handleAppStateChange = async (nextAppState: AppStateStatus) => { - if (this.state.appState.match(/inactive|background/) && nextAppState === 'active') { - await this.checkIfValidCodeInClipboard() + if (extractValidInviteCode(message)) { + this.onInputChange(message) } - this.setState({ appState: nextAppState }) } - - onPressOpenMessage = () => { - if (Platform.OS === 'android') { - SendIntentAndroid.openSMSApp() - } else { - navigateToURI('sms:') - } - } - - onPressPaste = async () => { - this.props.hideAlert() - const { validCode } = this.state - - Logger.debug('Extracted invite code:', validCode || '') - - if (!validCode) { - this.props.showError(ErrorMessages.INVALID_INVITATION) - return - } - - this.props.redeemInvite(validCode) - } - onPressImportClick = async () => { navigate(Screens.ImportWallet) } @@ -144,90 +97,90 @@ export class EnterInviteCode extends React.Component { navigateToURI(CELO_FAUCET_LINK) } + onPressSkip = () => { + this.props.skipInvite() + } + + onInputChange = (value: string) => { + const inviteCode = extractValidInviteCode(value) + if (inviteCode) { + this.setState({ inputValue: inviteCode }) + this.props.redeemInvite(inviteCode) + } else { + this.setState({ inputValue: value }) + } + } + + shouldShowClipboard = (value: string) => { + return !!extractValidInviteCode(value) + } + render() { - const { t, isRedeemingInvite, redeemComplete, account } = this.props - const { validCode } = this.state + const { t, isRedeemingInvite, isSkippingInvite, redeemComplete, account } = this.props + const { inputValue } = this.state + + let codeStatus = CodeRowStatus.INPUTTING + if (isRedeemingInvite) { + codeStatus = CodeRowStatus.PROCESSING + } else if (redeemComplete) { + codeStatus = CodeRowStatus.ACCEPTED + } return ( - - - - - {t('inviteCodeText.title')} - - - {!redeemComplete && ( - - {t('inviteCodeText.copyInvite.0')} - {t('inviteCodeText.copyInvite.1')} - - )} - - {redeemComplete ? ( - - {t('inviteCodeText.inviteAccepted')} - + + + + + {t('inviteCodeText.title')} + + {t('inviteCodeText.body')} + {t('inviteCodeText.codeHeader')} + + + {isSkippingInvite && ( + + + + )} + + {t('inviteCodeText.noCode')} + {SHOW_GET_INVITE_LINK ? ( + <> + {t('inviteCodeText.requestCodeFromFaucet')} + + {t('inviteCodeText.faucetLink')} + + {' ' + t('global:or') + ' '} + + {t('inviteCodeText.skip')} + + ) : ( - !isRedeemingInvite && - (!validCode ? ( - - - - {t('inviteCodeText.openMessages.hint.0')} - - {t('inviteCodeText.openMessages.hint.1')} - - - - ) : ( - - {t('inviteCodeText.pasteInviteCode.hint')} - - - )) + <> + {t('inviteCodeText.requestCodeNoFaucet')} + + {t('inviteCodeText.skip')} + + )} - {isRedeemingInvite && - !redeemComplete && ( - - {t('inviteCodeText.validating')} - - - )} - - - + + - {SHOW_GET_INVITE_LINK && ( - - {t('inviteCodeText.askForInvite.0', { testnet: _.startCase(DEFAULT_TESTNET) })} - - {t('inviteCodeText.askForInvite.1')} - - - )}