diff --git a/.circleci/config.yml b/.circleci/config.yml index 2d9e0987e1d..d4406607d0f 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: @@ -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..2893ec9c1aa 100644 --- a/.env +++ b/.env @@ -22,21 +22,21 @@ ETHSTATS_DOCKER_IMAGE_TAG="0ffe524c625ea59e4492dc92c2e638689c36e4b0" 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 +46,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/.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 index 84893323b1a..b1d0178a690 100644 --- a/packages/attestation-service/README.md +++ b/packages/attestation-service/README.md @@ -19,6 +19,18 @@ You can use the following environment variables to configure the attestation ser - `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: diff --git a/packages/attestation-service/config/.env.development b/packages/attestation-service/config/.env.development index e5d83040ec2..76186201b78 100644 --- a/packages/attestation-service/config/.env.development +++ b/packages/attestation-service/config/.env.development @@ -3,7 +3,15 @@ CELO_PROVIDER=https://integration-forno.celo-testnet.org ACCOUNT_ADDRESS=0xE6e53b5fc2e18F51781f14a3ce5E7FD468247a15 ATTESTATION_KEY=x APP_SIGNATURE=x -SMS_PROVIDERS=nexmo +SMS_PROVIDERS=twilio,nexmo NEXMO_KEY=x 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/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 42a2b651e77..00000000000 --- a/packages/attestation-service/src/attestation.ts +++ /dev/null @@ -1,258 +0,0 @@ -import { AttestationState } from '@celo/contractkit/lib/wrappers/Attestations' -import { attestToIdentifier, SignatureUtils } from '@celo/utils' -import { - Address, - isValidPrivateKey, - privateKeyToAddress, - toChecksumAddress, -} from '@celo/utils/lib/address' -import { AddressType, E164Number, E164PhoneNumberType } from '@celo/utils/lib/io' -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 { AttestationModel, AttestationStatus } from './models/attestation' -import { respondWithError } 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) -} - -async function validateAttestationRequest(request: AttestationRequest) { - const attestationRecord = await existingAttestationRequestRecord( - request.phoneNumber, - request.account, - request.issuer - ) - // check if it exists in the database - if (attestationRecord && !attestationRecord.canSendSms()) { - console.log(attestationRecord.canSendSms()) - throw new Error(ATTESTATION_ALREADY_SENT_ERROR) - } - const address = getAccountAddress() - - // 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_ERROR) - } - - // TODO: Check expiration - return -} - -async function validateAttestation( - attestationRequest: AttestationRequest, - attestationCode: string -) { - const key = getAttestationKey() - const address = privateKeyToAddress(key) - const attestations = await kit.contracts.getAttestations() - const isValid = await attestations.validateAttestationCode( - attestationRequest.phoneNumber, - attestationRequest.account, - address, - attestationCode - ) - - if (!isValid) { - throw new Error(ATTESTATION_ERROR) - } - 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}` -} - -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 -} - -async function sendSmsAndPersistAttestation( - attestationRequest: AttestationRequest, - attestationCode: string -) { - const textMessage = createAttestationTextMessage(attestationCode) - let attestationRecord: AttestationModel | null = null - - const transaction = await sequelize!.transaction() - - try { - attestationRecord = await ensureLockedRecord(attestationRequest, transaction) - const provider = smsProviderFor(attestationRequest.phoneNumber) - - if (!provider) { - await attestationRecord.update( - { status: AttestationStatus.UNABLE_TO_SERVE, smsProvider: SmsProviderType.UNKNOWN }, - { transaction } - ) - await transaction.commit() - return attestationRecord - } - - try { - await provider.sendSms(attestationRequest.phoneNumber, textMessage) - await attestationRecord.update( - { status: AttestationStatus.SENT, smsProvider: provider.type }, - { transaction } - ) - } catch (error) { - await attestationRecord.update( - { status: AttestationStatus.FAILED, smsProvider: provider.type }, - { transaction } - ) - } - - await transaction.commit() - } catch (error) { - console.error(error) - await transaction.rollback() - } - - return attestationRecord -} - -function respondAfterSendingSms(res: express.Response, attestationRecord: AttestationModel | null) { - if (!attestationRecord) { - console.error('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: - console.error( - '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: 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) - respondWithError(res, 422, error.toString()) - return - } - - try { - const attestationRecord = await sendSmsAndPersistAttestation( - attestationRequest, - attestationCode - ) - respondAfterSendingSms(res, attestationRecord) - } catch (error) { - console.error(error) - respondWithError(res, 500, SMS_SENDING_ERROR) - return - } -} diff --git a/packages/attestation-service/src/db.ts b/packages/attestation-service/src/db.ts index feec25c5bfd..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 { FindOptions, Sequelize } from 'sequelize' import { fetchEnv } from './env' +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() 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 38cce839c13..7a34fa5f663 100644 --- a/packages/attestation-service/src/index.ts +++ b/packages/attestation-service/src/index.ts @@ -1,27 +1,42 @@ -import * as dotenv from 'dotenv' import express from 'express' -import { AttestationRequestType, getAttestationKey, 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() { - console.info(process.env.CONFIG) - 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) @@ -29,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 24e24e7d092..9ae3110cdfe 100644 --- a/packages/attestation-service/src/models/attestation.ts +++ b/packages/attestation-service/src/models/attestation.ts @@ -33,14 +33,6 @@ export default (sequelize: Sequelize) => { }) as AttestationStatic model.prototype.canSendSms = function() { - console.log( - this.status, - [ - AttestationStatus.DISPATCHING, - AttestationStatus.FAILED, - AttestationStatus.UNABLE_TO_SERVE, - ].includes(this.status) - ) return [ AttestationStatus.DISPATCHING, AttestationStatus.FAILED, diff --git a/packages/attestation-service/src/request.ts b/packages/attestation-service/src/request.ts index ea46ee6f515..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) { @@ -52,3 +71,22 @@ function serializeErrors(errors: t.Errors) { 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..dd7354fb789 --- /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 `<#> ${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/base.ts b/packages/attestation-service/src/sms/base.ts index 326aa1c6900..755d9762d6f 100644 --- a/packages/attestation-service/src/sms/base.ts +++ b/packages/attestation-service/src/sms/base.ts @@ -1,5 +1,6 @@ import { E164Number } from '@celo/utils/lib/io' import { PhoneNumberUtil } from 'google-libphonenumber' +import { fetchEnvOrDefault } from '../env' const phoneUtil = PhoneNumberUtil.getInstance() export abstract class SmsProvider { @@ -17,4 +18,11 @@ export abstract class SmsProvider { 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 index 58c6a701e03..ff77fa708af 100644 --- a/packages/attestation-service/src/sms/index.ts +++ b/packages/attestation-service/src/sms/index.ts @@ -1,28 +1,35 @@ +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 configuredSmsProviders = fetchEnv('SMS_PROVIDERS').split(',') as Array< + const smsProvidersToConfigure = fetchEnv('SMS_PROVIDERS').split(',') as Array< SmsProviderType | string > - if (configuredSmsProviders.length === 0) { + if (smsProvidersToConfigure.length === 0) { throw new Error('You have to specify at least one sms provider') } - for (const configuredSmsProvider of configuredSmsProviders) { + for (const configuredSmsProvider of smsProvidersToConfigure) { switch (configuredSmsProvider) { case SmsProviderType.NEXMO: - const provider = NexmoSmsProvider.fromEnv() - await provider.initialize() - smsProviders.push(provider) + const nexmoProvider = NexmoSmsProvider.fromEnv() + await nexmoProvider.initialize() + smsProviders.push(nexmoProvider) break - default: + case SmsProviderType.TWILIO: + const twilioProvider = TwilioSmsProvider.fromEnv() + await twilioProvider.initialize() + smsProviders.push(twilioProvider) break + default: + throw new Error(`Unknown sms provider type specified: ${configuredSmsProvider}`) } } } @@ -30,3 +37,11 @@ export async function initializeSmsProviders() { 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 index 90d4107a898..cb126881c1e 100644 --- a/packages/attestation-service/src/sms/nexmo.ts +++ b/packages/attestation-service/src/sms/nexmo.ts @@ -3,7 +3,7 @@ import { E164Number } from '@celo/utils/lib/io' import { PhoneNumberUtil } from 'google-libphonenumber' import Nexmo from 'nexmo' import { fetchEnv } from '../env' -import { SmsProvider, SmsProviderType } from './base' +import { readBlacklistFromEnv, SmsProvider, SmsProviderType } from './base' const phoneUtil = PhoneNumberUtil.getInstance() @@ -12,7 +12,7 @@ export class NexmoSmsProvider extends SmsProvider { return new NexmoSmsProvider( fetchEnv('NEXMO_KEY'), fetchEnv('NEXMO_SECRET'), - fetchEnv('NEXMO_BLACKLIST').split(',') + readBlacklistFromEnv('NEXMO_BLACKLIST') ) } type = SmsProviderType.NEXMO 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..8827a2e2f51 100644 --- a/packages/celotool/src/cmds/account/faucet.ts +++ b/packages/celotool/src/cmds/account/faucet.ts @@ -4,7 +4,7 @@ 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' 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 39aea87e9b4..bbb13416cb5 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -14,6 +14,7 @@ import { importGenesis, initAndStartGeth, sleep, + waitToFinishSyncing, } from './utils' interface MemberSwapper { @@ -290,17 +291,22 @@ describe('governance tests', () => { 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(validatorAccounts[0]) // Prepare for member swapping. const groupWeb3 = new Web3('ws://localhost:8555') + await waitToFinishSyncing(groupWeb3) const groupKit = newKitFromWeb3(groupWeb3) validators = await groupKit._web3Contracts.getValidators() const membersToSwap = [validatorAccounts[0], validatorAccounts[1]] @@ -309,6 +315,7 @@ describe('governance tests', () => { // 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), @@ -344,7 +351,7 @@ describe('governance tests', () => { }) const getValidatorSetSignersAtBlock = async (blockNumber: number): Promise => { - return election.methods.currentValidators().call({}, blockNumber) + return election.methods.getCurrentValidatorSigners().call({}, blockNumber) } const getValidatorSetAccountsAtBlock = async (blockNumber: number) => { @@ -439,7 +446,7 @@ describe('governance tests', () => { 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) { 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 02941a966bb..c1a9421b0d7 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, @@ -342,23 +352,33 @@ 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', + }, + reserve: { + goldBalance: 100000, + }, + 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`, @@ -427,6 +447,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) @@ -448,7 +469,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] @@ -459,8 +480,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() @@ -475,16 +498,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..218c32cca93 100644 --- a/packages/celotool/src/lib/env-utils.ts +++ b/packages/celotool/src/lib/env-utils.ts @@ -29,6 +29,7 @@ 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', @@ -70,7 +71,6 @@ export enum envVar { TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG = 'TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG', TX_NODES = 'TX_NODES', VALIDATORS = 'VALIDATORS', - VERIFICATION_POOL_URL = 'VERIFICATION_POOL_URL', VM_BASED = 'VM_BASED', } diff --git a/packages/celotool/src/lib/generate_utils.ts b/packages/celotool/src/lib/generate_utils.ts index c9dbc6d5130..f9b8a4e446f 100644 --- a/packages/celotool/src/lib/generate_utils.ts +++ b/packages/celotool/src/lib/generate_utils.ts @@ -4,7 +4,7 @@ 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 { @@ -28,6 +28,7 @@ export enum AccountType { BOOTNODE = 3, FAUCET = 4, ATTESTATION = 5, + PRICE_ORACLE = 6, } export enum ConsensusType { @@ -47,6 +48,7 @@ export const MNEMONIC_ACCOUNT_TYPE_CHOICES = [ 'bootnode', 'faucet', 'attestation', + 'price_oracle', ] export const add0x = (str: string) => { @@ -138,17 +140,22 @@ 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) + const oracleAddress = getStrippedAddressesFor(AccountType.PRICE_ORACLE, mnemonic, 1) + return generateGenesis({ validators, consensusType, blockTime, - initialAccounts: faucetAddresses, + initialAccounts: faucetAddresses.concat(oracleAddress), epoch, + lookbackwindow, chainId, requestTimeout, enablePetersburg, @@ -157,12 +164,12 @@ 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')), @@ -170,7 +177,7 @@ const generateIstanbulExtraData = (validators: Validator[]) => { // Removed validators new Buffer(0), // Seal - Buffer.from(repeat('0', blsSignatureVanity * 2), 'hex'), + Buffer.from(repeat('0', ecdsaSignatureVanity * 2), 'hex'), [ // AggregatedSeal.Bitmap new Buffer(0), @@ -200,6 +207,7 @@ export const generateGenesis = ({ initialAccounts: otherAccounts = [], blockTime, epoch, + lookbackwindow, chainId, requestTimeout, enablePetersburg = true, @@ -209,6 +217,7 @@ export const generateGenesis = ({ initialAccounts?: string[] blockTime: number epoch: number + lookbackwindow: number chainId: number requestTimeout: number enablePetersburg?: boolean @@ -237,6 +246,7 @@ export const generateGenesis = ({ period: blockTime, requesttimeout: requestTimeout, epoch, + lookbackwindow, } } 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..0b232df2d84 100644 --- a/packages/celotool/src/lib/helm_deploy.ts +++ b/packages/celotool/src/lib/helm_deploy.ts @@ -6,14 +6,7 @@ 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' @@ -522,10 +515,6 @@ async function helmParameters(celoEnv: string) { 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 +529,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')}`, 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..d69ad6019dd 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 = { 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 3e7444c9503..b7a071af5cd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -38,7 +38,7 @@ "@oclif/plugin-help": "^2", "bip32": "^1.0.2", "bip39": "^2.5.0", - "bls12377js": "https://github.com/celo-org/bls12377js#cada1105f4a5e4c2ddd239c1874df3bf33144a10", + "bls12377js": "https://github.com/celo-org/bls12377js#ea09eba5c54fe63617af494a0c198fcc47582e0c", "chalk": "^2.4.2", "cli-table": "^0.3.1", "cli-ux": "^5.3.1", @@ -55,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", 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 fada8a235c7..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' 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 6cb91fb8ce9..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' 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/election/current.ts b/packages/cli/src/commands/election/current.ts new file mode 100644 index 00000000000..26fda2f61da --- /dev/null +++ b/packages/cli/src/commands/election/current.ts @@ -0,0 +1,31 @@ +import { cli } from 'cli-ux' +import { BaseCommand } from '../../base' + +export default class ElectionCurrent extends BaseCommand { + static description = 'Outputs the currently elected validator set' + + 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..80bc4acd2c6 --- /dev/null +++ b/packages/cli/src/commands/election/list.ts @@ -0,0 +1,25 @@ +import { cli } from 'cli-ux' +import { BaseCommand } from '../../base' + +export default class List extends BaseCommand { + static description = 'Outputs the validator groups and their vote totals' + + 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: {}, + capacity: {}, + eligible: {}, + }) + } +} diff --git a/packages/cli/src/commands/election/run.ts b/packages/cli/src/commands/election/run.ts new file mode 100644 index 00000000000..ee1ea8089f8 --- /dev/null +++ b/packages/cli/src/commands/election/run.ts @@ -0,0 +1,31 @@ +import { cli } from 'cli-ux' +import { BaseCommand } from '../../base' + +export default class ElectionRun extends BaseCommand { + static description = 'Runs an mock election and outputs the validators that were elected' + + 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..0d60baecf87 --- /dev/null +++ b/packages/cli/src/commands/election/show.ts @@ -0,0 +1,32 @@ +import { IArg } from '@oclif/parser/lib/args' +import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' +import { printValueMap } from '../../utils/cli' +import { Args } from '../../utils/command' + +export default class ElectionShow extends BaseCommand { + static description = 'Show election information about an existing Validator Group' + + static flags = { + ...BaseCommand.flags, + } + + static args: IArg[] = [ + Args.address('groupAddress', { description: "Validator Groups's address" }), + ] + + static examples = ['show 0x97f7333c51897469E8D98E7af8653aAb468050a3'] + + async run() { + const { args } = this.parse(ElectionShow) + const address = args.groupAddress + const election = await this.kit.contracts.getElection() + + await newCheckBuilder(this) + .isValidatorGroup(address) + .runChecks() + + const groupVotes = await election.getValidatorGroupVotes(address) + printValueMap(groupVotes) + } +} 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/account/lock.ts b/packages/cli/src/commands/lockedgold/lock.ts similarity index 72% rename from packages/cli/src/commands/account/lock.ts rename to packages/cli/src/commands/lockedgold/lock.ts index b46b49df012..6e64e3c0ec7 100644 --- a/packages/cli/src/commands/account/lock.ts +++ b/packages/cli/src/commands/lockedgold/lock.ts @@ -28,15 +28,22 @@ export default class Lock extends BaseCommand { this.kit.defaultAccount = address const value = new BigNumber(res.flags.value) + 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) .addCheck(`Value [${value.toString()}] is >= 0`, () => value.gt(0)) .isAccount(address) - .hasEnoughGold(address, value) + .hasEnoughGold(address, lockValue) .runChecks() - const lockedGold = await this.kit.contracts.getLockedGold() + 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.toString() }) } } 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/validator/list.ts b/packages/cli/src/commands/validator/list.ts index af742764809..f15d60d7799 100644 --- a/packages/cli/src/commands/validator/list.ts +++ b/packages/cli/src/commands/validator/list.ts @@ -2,7 +2,7 @@ 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' static flags = { ...BaseCommand.flags, @@ -21,8 +21,10 @@ export default class ValidatorList extends BaseCommand { cli.table(validatorList, { address: {}, name: {}, - publicKey: {}, affiliation: {}, + score: {}, + ecdsaPublicKey: {}, + blsPublicKey: {}, }) } } diff --git a/packages/cli/src/commands/validator/register.ts b/packages/cli/src/commands/validator/register.ts index 0753ae27088..8812e48901a 100644 --- a/packages/cli/src/commands/validator/register.ts +++ b/packages/cli/src/commands/validator/register.ts @@ -16,7 +16,7 @@ export default class ValidatorRegister extends BaseCommand { } static examples = [ - 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --ecdsaKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae1 --blsKey 0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae300 --blsPop 0x05d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', + 'register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --ecdsaKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf997eda082ae1 --blsKey 0x4fa3f67fc913878b068d1fa1cdddc54913d3bf988dbe5a36a20fa888f20d4894c408a6773f3d7bde11154f2a3076b700d345a42fd25a0e5e83f4db5586ac7979ac2053cd95d8f2efd3e959571ceccaa743e02cf4be3f5d7aaddb0b06fc9aff00 --blsPop 0xcdb77255037eb68897cd487fdd85388cbda448f617f874449d4b11588b0b7ad8ddc20d9bb450b513bb35664ea3923900', ] async run() { diff --git a/packages/cli/src/commands/validator/update-bls-public-key.ts b/packages/cli/src/commands/validator/update-bls-public-key.ts index 70521743dea..e4511839fbf 100644 --- a/packages/cli/src/commands/validator/update-bls-public-key.ts +++ b/packages/cli/src/commands/validator/update-bls-public-key.ts @@ -14,7 +14,7 @@ export default class ValidatorUpdateBlsPublicKey extends BaseCommand { } static examples = [ - 'update-bls-key --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --blsKey 0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae300 --blsPop 0x05d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00', + 'update-bls-key --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --blsKey 0x4fa3f67fc913878b068d1fa1cdddc54913d3bf988dbe5a36a20fa888f20d4894c408a6773f3d7bde11154f2a3076b700d345a42fd25a0e5e83f4db5586ac7979ac2053cd95d8f2efd3e959571ceccaa743e02cf4be3f5d7aaddb0b06fc9aff00 --blsPop 0xcdb77255037eb68897cd487fdd85388cbda448f617f874449d4b11588b0b7ad8ddc20d9bb450b513bb35664ea3923900', ] async run() { const res = this.parse(ValidatorUpdateBlsPublicKey) 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/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/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..290e70aa4c7 100644 --- a/packages/contractkit/package.json +++ b/packages/contractkit/package.json @@ -20,7 +20,7 @@ "clean:all": "yarn clean && rm -rf src/generated", "build:gen": "yarn --cwd ../protocol build", "prepublishOnly": "yarn build:gen && yarn build", - "test:prepare": "yarn --cwd ../protocol devchain generate .devchain --migration_override src/test-utils/migration-override.json", + "test:prepare": "yarn --cwd ../protocol devchain generate .devchain --migration_override ../dev-utils/src/migration-override.json", "test": "jest --runInBand", "lint": "tslint -c tslint.json --project ." }, @@ -40,6 +40,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..4c718c65fd8 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() } + 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 5539627b1b4..00000000000 --- a/packages/contractkit/src/test-utils/ganache-test.ts +++ /dev/null @@ -1,76 +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 async function timeTravel(seconds: number, web3: Web3) { - await jsonRpcCall(web3, 'evm_increaseTime', [seconds]) - await jsonRpcCall(web3, 'evm_mine', []) -} - -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 index 95bd22d4a18..193a5f8abe5 100644 --- a/packages/contractkit/src/wrappers/Accounts.test.ts +++ b/packages/contractkit/src/wrappers/Accounts.test.ts @@ -1,7 +1,7 @@ +import { testWithGanache } from '@celo/dev-utils/lib/ganache-test' import { addressToPublicKey, parseSignature } from '@celo/utils/lib/signatureUtils' 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,13 +11,13 @@ 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 // Random hex strings const blsPublicKey = - '0x4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' + '0x4fa3f67fc913878b068d1fa1cdddc54913d3bf988dbe5a36a20fa888f20d4894c408a6773f3d7bde11154f2a3076b700d345a42fd25a0e5e83f4db5586ac7979ac2053cd95d8f2efd3e959571ceccaa743e02cf4be3f5d7aaddb0b06fc9aff00' const blsPoP = - '0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' + '0xcdb77255037eb68897cd487fdd85388cbda448f617f874449d4b11588b0b7ad8ddc20d9bb450b513bb35664ea3923900' testWithGanache('Accounts Wrapper', (web3) => { const kit = newKitFromWeb3(web3) diff --git a/packages/contractkit/src/wrappers/Accounts.ts b/packages/contractkit/src/wrappers/Accounts.ts index a995bc664e6..bb5bb9b3c86 100644 --- a/packages/contractkit/src/wrappers/Accounts.ts +++ b/packages/contractkit/src/wrappers/Accounts.ts @@ -58,6 +58,16 @@ export class AccountsWrapper extends BaseWrapper { 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 + ) + /** * Check if an account already exists. * @param account The address of the account diff --git a/packages/contractkit/src/wrappers/Attestations.ts b/packages/contractkit/src/wrappers/Attestations.ts index 31d03dbea0c..f3354549342 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' @@ -61,6 +61,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[] @@ -247,6 +258,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 @@ -254,10 +279,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)) } @@ -268,17 +295,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 73208940cd0..66a87952aca 100644 --- a/packages/contractkit/src/wrappers/BaseWrapper.ts +++ b/packages/contractkit/src/wrappers/BaseWrapper.ts @@ -220,19 +220,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 { valueToInt ) + /** + * 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. @@ -117,48 +116,24 @@ 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: valueToBigNumber(votes), + capacity: valueToBigNumber(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 validatorGroupVotes = await Promise.all( - validatorGroupAddresses.map((g) => this.contract.methods.getTotalVotesForGroup(g).call()) - ) - const validatorGroupEligible = await Promise.all( - validatorGroupAddresses.map((g) => this.contract.methods.getGroupEligibility(g).call()) - ) - return validatorGroupAddresses.map((a, i) => ({ - address: a, - votes: valueToBigNumber(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]) + const groups = (await validators.getRegisteredValidatorGroups()).map((g) => g.address) + return concurrentMap(5, groups, (g) => this.getValidatorGroupVotes(g)) } /** @@ -179,6 +154,23 @@ export class ElectionWrapper extends BaseWrapper { ) } + /** + * 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] + ) + } + async findLesserAndGreaterAfterVote( votedGroup: Address, voteWeight: BigNumber @@ -194,6 +186,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 c916f6e81dc..905f3755893 100644 --- a/packages/contractkit/src/wrappers/GasPriceMinimum.ts +++ b/packages/contractkit/src/wrappers/GasPriceMinimum.ts @@ -12,11 +12,22 @@ 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, valueToBigNumber) + /** * Query current gas price minimum. * @returns current gas price minimum in the requested currency */ - gasPriceMinimum = proxyCall(this.contract.methods.gasPriceMinimum, undefined, valueToBigNumber) + getGasPriceMinimum = proxyCall( + this.contract.methods.getGasPriceMinimum, + undefined, + valueToBigNumber + ) + /** * 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/Governance.ts b/packages/contractkit/src/wrappers/Governance.ts index a66fda9f533..fa2a9fc2894 100644 --- a/packages/contractkit/src/wrappers/Governance.ts +++ b/packages/contractkit/src/wrappers/Governance.ts @@ -357,7 +357,7 @@ export class GovernanceWrapper extends BaseWrapper { return queue.sort((a, b) => a.upvotes.comparedTo(b.upvotes)) } - private queueIndexOfproposalID(queue: UpvoteRecord[], proposalID: BigNumber.Value) { + private indexOfProposalID(queue: UpvoteRecord[], proposalID: BigNumber.Value) { const idx = queue.findIndex((qp) => qp.proposalID.isEqualTo(proposalID)) if (idx === -1) { throw new Error(`Proposal ${proposalID} not in queue`) @@ -366,7 +366,7 @@ export class GovernanceWrapper extends BaseWrapper { } private lesserAndGreater(queue: UpvoteRecord[], proposalID: BigNumber.Value) { - const idx = this.queueIndexOfproposalID(queue, proposalID) + const idx = this.indexOfProposalID(queue, proposalID) return { lesserID: idx === 0 ? ZERO_BN : queue[idx - 1].proposalID, greaterID: idx === queue.length - 1 ? ZERO_BN : queue[idx + 1].proposalID, @@ -375,7 +375,7 @@ export class GovernanceWrapper extends BaseWrapper { private async withUpvoteRevoked(queue: UpvoteRecord[], upvoter: Address) { const upvoteRecord = await this.getUpvoteRecord(upvoter) - const queueIndex = this.queueIndexOfproposalID(queue, upvoteRecord.proposalID) + const queueIndex = this.indexOfProposalID(queue, upvoteRecord.proposalID) queue[queueIndex].upvotes = queue[queueIndex].upvotes.minus(upvoteRecord.upvotes) return { queue: this.sortedQueue(queue), @@ -389,7 +389,7 @@ export class GovernanceWrapper extends BaseWrapper { upvoter: Address ) { const weight = await this.getVoteWeight(upvoter) - const queueIndex = this.queueIndexOfproposalID(queue, proposalID) + const queueIndex = this.indexOfProposalID(queue, proposalID) queue[queueIndex].upvotes = queue[queueIndex].upvotes.plus(weight) return this.sortedQueue(queue) } 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 01fb5cd5b5c..23c6f913cec 100644 --- a/packages/contractkit/src/wrappers/LockedGold.ts +++ b/packages/contractkit/src/wrappers/LockedGold.ts @@ -65,13 +65,59 @@ export class LockedGoldWrapper extends BaseWrapper { tupleParser(valueToString) ) + 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: BigNumber.Value) => CeloTransactionObject = proxySend( this.kit, - this.contract.methods.relock + this.contract.methods.relock, + tupleParser(valueToString, valueToString) ) /** 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 0dd89503ae0..44f95e159bf 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' // TEST NOTES: balances defined in test-utils/migration-override diff --git a/packages/contractkit/src/wrappers/Validators.test.ts b/packages/contractkit/src/wrappers/Validators.test.ts index e90aec9cdfe..5ae5444d0f8 100644 --- a/packages/contractkit/src/wrappers/Validators.test.ts +++ b/packages/contractkit/src/wrappers/Validators.test.ts @@ -1,8 +1,8 @@ +import { testWithGanache } from '@celo/dev-utils/lib/ganache-test' import { addressToPublicKey } from '@celo/utils/lib/signatureUtils' 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' @@ -12,12 +12,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 const blsPublicKey = - '0x4d23d8cd06f30b1fa7cf368e2f5399ab04bb6846c682f493a98a607d3dfb7e53a712bb79b475c57b0ac2785460f91301' + '0x4fa3f67fc913878b068d1fa1cdddc54913d3bf988dbe5a36a20fa888f20d4894c408a6773f3d7bde11154f2a3076b700d345a42fd25a0e5e83f4db5586ac7979ac2053cd95d8f2efd3e959571ceccaa743e02cf4be3f5d7aaddb0b06fc9aff00' const blsPoP = - '0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae30005d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d740501' + '0xcdb77255037eb68897cd487fdd85388cbda448f617f874449d4b11588b0b7ad8ddc20d9bb450b513bb35664ea3923900' testWithGanache('Validators Wrapper', (web3) => { const kit = newKitFromWeb3(web3) diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index c1d5b0ca846..cc63b2f4d08 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -98,11 +98,28 @@ export class ValidatorsWrapper extends BaseWrapper { } } - async signerToAccount(signerAddress: Address) { + /** + * 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.signerToAccount(signerAddress) + } + /** * Updates a validator's BLS key. * @param blsPublicKey The BLS public key that the validator is using for consensus, should pass proof @@ -164,13 +181,20 @@ export class ValidatorsWrapper extends BaseWrapper { const res = await this.contract.methods.getValidator(address).call() return { address, - ecdsaPublicKey: res[0] as any, - blsPublicKey: res[1] as any, - affiliation: res[2], - score: fromFixed(new BigNumber(res[3])), + // @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() 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/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 c22e3daa1e3..ac4aca29214 100644 --- a/packages/docs/command-line-interface/account.md +++ b/packages/docs/command-line-interface/account.md @@ -200,24 +200,6 @@ 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 - -Locks Celo Gold to be used in governance and validator elections. - -``` -USAGE - $ celocli account:lock - -OPTIONS - --from=from (required) - --value=value (required) unit amount of Celo Gold (cGLD) - -EXAMPLE - lock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --value 1000000000000000000 -``` - -_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)_ - ### New Creates a new account diff --git a/packages/docs/command-line-interface/election.md b/packages/docs/command-line-interface/election.md index 6ed0013466e..6cd348d462d 100644 --- a/packages/docs/command-line-interface/election.md +++ b/packages/docs/command-line-interface/election.md @@ -4,19 +4,64 @@ description: View and manage validator elections ## Commands -### Validatorset +### Current -Outputs the current validator set +Outputs the currently elected validator set ``` USAGE - $ celocli election:validatorset + $ celocli election:current EXAMPLE - validatorset + current ``` -_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/current.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/election/current.ts)_ + +### List + +Outputs the validator groups and their vote totals + +``` +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)_ + +### Run + +Runs an mock election and outputs the validators that were elected + +``` +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 an existing Validator Group + +``` +USAGE + $ celocli election:show GROUPADDRESS + +ARGUMENTS + GROUPADDRESS Validator Groups's address + +EXAMPLE + show 0x97f7333c51897469E8D98E7af8653aAb468050a3 +``` + +_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 diff --git a/packages/docs/command-line-interface/lockedgold.md b/packages/docs/command-line-interface/lockedgold.md index 49c0dabf34a..949255f3f15 100644 --- a/packages/docs/command-line-interface/lockedgold.md +++ b/packages/docs/command-line-interface/lockedgold.md @@ -4,6 +4,24 @@ 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) unit amount of Celo Gold (cGLD) + +EXAMPLE + lock --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --value 1000000000000000000 +``` + +_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 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/validator.md b/packages/docs/command-line-interface/validator.md index 8bfdea3e57d..35e0ac1e1bd 100644 --- a/packages/docs/command-line-interface/validator.md +++ b/packages/docs/command-line-interface/validator.md @@ -60,7 +60,7 @@ _See code: [packages/cli/src/commands/validator/deregister.ts](https://github.co ### List -List existing Validators +List registered Validators ``` USAGE @@ -90,9 +90,9 @@ EXAMPLE register --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --ecdsaKey 0xc52f3fab06e22a54915a8765c4f6826090cfac5e40282b43844bf1c0df83aaa632e55b67869758f2291d1aabe0ebecc7cbf4236aaa45e3e0cfbf 997eda082ae1 --blsKey - 0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae300 --blsPop - 0x05d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26 - dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00 + 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)_ @@ -143,9 +143,9 @@ OPTIONS EXAMPLE update-bls-key --from 0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 --blsKey - 0x9d3e1d8f49f6b0d8e9a03d80ca07b1d24cf1cc0557bdcc04f5e17a46e35d02d0d411d956dbd5d2d2464eebd7b74ae300 --blsPop - 0x05d223780d785d2abc5644fac7ac29fb0e302bdc80c81a5d45018b68b1045068a4b3a4861c93037685fd0d252d7405011220a66a6257562d0c26 - dabf64485a1d96bad27bb1c0fd6080a75b0ec9f75b50298a2a8e04b02b2688c8104fca61fb00 + 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/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-validator.md b/packages/docs/getting-started/running-a-validator.md index 7763df91187..e073bc3bf06 100644 --- a/packages/docs/getting-started/running-a-validator.md +++ b/packages/docs/getting-started/running-a-validator.md @@ -119,7 +119,7 @@ docker run -v $PWD:/root/.celo --entrypoint cp us.gcr.io/celo-testnet/celo-node: Start up the node: ```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 +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 --etherbase $CELO_VALIDATOR_ADDRESS ``` {% hint style="danger" %} 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/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..beac599c889 100644 --- a/packages/helm-charts/testnet/templates/_helpers.tpl +++ b/packages/helm-charts/testnet/templates/_helpers.tpl @@ -198,7 +198,6 @@ spec: --ethstats=${HOSTNAME}:${ETHSTATS_SECRET}@${ETHSTATS_SVC} \ --metrics \ --mine \ - --miner.verificationpool=${VERIFICATION_POOL_URL} \ --networkid=${NETWORK_ID} \ --nodekey=/root/.celo/account/{{ .Node.name}}PrivateKey \ --password=/root/.celo/account/accountSecret \ @@ -251,10 +250,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: diff --git a/packages/helm-charts/testnet/templates/txnode.statefulset.yaml b/packages/helm-charts/testnet/templates/txnode.statefulset.yaml index fc2df7e21bb..44b4f441c08 100644 --- a/packages/helm-charts/testnet/templates/txnode.statefulset.yaml +++ b/packages/helm-charts/testnet/templates/txnode.statefulset.yaml @@ -125,7 +125,6 @@ spec: --syncmode=full \ ${NAT_FLAG} \ --ethstats=${HOSTNAME}:${ETHSTATS_SECRET}@${ETHSTATS_SVC} \ - --miner.verificationpool=${VERIFICATION_POOL_URL} \ --consoleformat=json \ --consoleoutput=stdout \ --verbosity={{ .Values.geth.verbosity }} \ @@ -146,8 +145,6 @@ spec: 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..7b658e6bc7c 100644 --- a/packages/helm-charts/testnet/templates/validators.statefulset.yaml +++ b/packages/helm-charts/testnet/templates/validators.statefulset.yaml @@ -146,7 +146,6 @@ spec: --networkid=${NETWORK_ID} \ --syncmode=full \ --ethstats=${HOSTNAME}:${ETHSTATS_SECRET}@${ETHSTATS_SVC} \ - --miner.verificationpool=${VERIFICATION_POOL_URL} \ --consoleformat=json \ --consoleoutput=stdout \ --verbosity={{ .Values.geth.verbosity }} \ @@ -177,8 +176,6 @@ spec: 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/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/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 ( { 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')} - - - )}