diff --git a/.circleci/config.yml b/.circleci/config.yml index 21992dcc764..93e460e9f3a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -406,15 +406,6 @@ jobs: name: Run Tests command: yarn --cwd=packages/contractkit test - - run: - name: Install and test the npm package - command: | - set -euo pipefail - cd packages/contractkit - yarn pack - cd /tmp - npm install ~/app/packages/contractkit/*.tgz - walletkit-test: <<: *defaults steps: @@ -436,15 +427,6 @@ jobs: yarn --cwd=packages/walletkit build alfajoresstaging yarn --cwd=packages/walletkit test - - run: - name: Install and test the npm package - command: | - set -euo pipefail - cd packages/walletkit - yarn pack - cd /tmp - npm install ~/app/packages/walletkit/*.tgz - cli-test: <<: *defaults steps: diff --git a/.env b/.env index f02f2b558a5..1a6ba62284f 100644 --- a/.env +++ b/.env @@ -17,9 +17,10 @@ BLOCKSCOUT_WEB_REPLICAS=3 BLOCKSCOUT_DB_SUFFIX= ETHSTATS_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/ethstats" -ETHSTATS_DOCKER_IMAGE_TAG="cd037ea1e18848466452ba9890c1f1bcd3f61009" +ETHSTATS_DOCKER_IMAGE_TAG="150e7758ec3bde70e68dead7867360a254f90d84" ETHSTATS_TRUSTED_ADDRESSES="" ETHSTATS_BANNED_ADDRESSES="" +ETHSTATS_RESERVED_ADDRESSES="" FAUCET_GENESIS_ACCOUNTS=2 diff --git a/.env.baklava b/.env.baklava index a7dc8bed6e8..f0912c87a02 100644 --- a/.env.baklava +++ b/.env.baklava @@ -21,9 +21,10 @@ BLOCKSCOUT_DB_SUFFIX="0" BLOCKSCOUT_SUBNETWORK_NAME="Baklava" ETHSTATS_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/ethstats" -ETHSTATS_DOCKER_IMAGE_TAG="efe7d0887c37bf308c5a5154be282cac34226a73" +ETHSTATS_DOCKER_IMAGE_TAG="150e7758ec3bde70e68dead7867360a254f90d84" ETHSTATS_TRUSTED_ADDRESSES="" ETHSTATS_BANNED_ADDRESSES="" +ETHSTATS_RESERVED_ADDRESSES="" 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 diff --git a/.env.baklavastaging b/.env.baklavastaging index 9989f27887c..b01b9e3e465 100644 --- a/.env.baklavastaging +++ b/.env.baklavastaging @@ -21,9 +21,10 @@ BLOCKSCOUT_DB_SUFFIX="0" BLOCKSCOUT_SUBNETWORK_NAME="Baklava Staging" ETHSTATS_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/ethstats" -ETHSTATS_DOCKER_IMAGE_TAG="bbc89603d872af1c6b37cf00e8220ed26237fc91" +ETHSTATS_DOCKER_IMAGE_TAG="150e7758ec3bde70e68dead7867360a254f90d84" ETHSTATS_TRUSTED_ADDRESSES="" ETHSTATS_BANNED_ADDRESSES="" +ETHSTATS_RESERVED_ADDRESSES="" 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 diff --git a/README-dev.md b/README-dev.md index 97d29247eae..ebbccb4dacb 100644 --- a/README-dev.md +++ b/README-dev.md @@ -9,7 +9,7 @@ Many packages depend on other packages within the monorepo. When this happens, f 3. To differentiate published vs unpublished version. Master version (in package.json) must end with suffix `-dev` and should not be published. 4. If a developer want to publish a version; then after publishing it needs to set master version to next `-dev` version and change all package.json that require on it. -To check which pakages need amending, you can run (in the root pkg): +To check which packages need amending, you can run (in the root pkg): yarn check:packages @@ -23,29 +23,44 @@ A practical example: ## How to publish a new npm package -First checkout the alfajores branch. +> Note: Packages with mainline versions (i.e. without a `-foo` suffix) should be published from the `master` branch. -``` -celo-monorepo $ git checkout alfajores -celo-monorepo $ yarn -``` +> Note: All packages are prefixed with "@celo/" and only members of the [Celo NPM organization](https://www.npmjs.com/settings/celo/members) can publish new packages or update the existing ones. + +### Update the version numbers to an unpublished version + +It is important to ensure that the `master` branch is ahead of the published package on NPM, otherwise `yarn` may use the published version of the package rather than the local development version. + +In order to maintain this, create and merge a PR to the `master` branch bumping the package version to the next number that will be published. (i.e. If you are about to publish `x.y.z`, set the package version to `x.y.z+1`) +Update all references to that package in the monorepo to the new version (i.e. `x.y.z+1`) +Prefer appending a `-dev` suffix to the version number to ensure an internal dependency will never be mistaken for a published one. + +> Note: Services deployed to App Engine must only depend on published NPM packages. These packages are `verification-pool-api`, `blockchain-api` and `notification-service`. + +### Checkout the commit to be published and verify version numbers + +Checkout the commit that will become the new published version (i.e. `git checkout HEAD~1` assuming that the commit for bumping the version number is `HEAD`) -Before publishing a new celocli package, test in isolation using Docker. This confirms that it is locally installable and does not have implict dependency on rest of the celo-monorepo or have an implicit dependency which is an explicit dependency of another celo-monorepo package. +Check the `package.json` file and remove the `-dev` suffix if present. Additionally remove the `-dev` suffix from any internal dependencies and use ensure they are published (e.g. `@celo/utils`) + +### Verify installation in Docker + +Test installation in isolation using Docker. +This confirms that it is locally installable and does not have implicit dependency on rest of the `celo-monorepo` or have an implicit dependency which is an explicit dependency of another `celo-monorepo` package. ``` -# To test utils package, change $PWD/packages/cli to $PWD/packages/utils -# To test contractkit package, change $PWD/packages/cli to $PWD/packages/contractkit -celo-monorepo $ docker run -v $PWD/packages/cli:/tmp/npm_package -it --entrypoint bash node:10 +# Specify the package to test. e.g. celocli, contractkit, utils +celo-monorepo $ PACKAGE=cli +celo-monorepo $ docker run --rm -v $PWD/packages/${PACKAGE}:/tmp/npm_package -it --entrypoint bash node:10 root@e0d56700584f:/# mkdir /tmp/tmp1 && cd /tmp/tmp1 root@e0d56700584f:/tmp/tmp1# npm install /tmp/npm_package/ ``` -After testing, exit the docker container, and publish the package. Do note that all our packages are prefixed with "@celo/" and only members listed [here](https://www.npmjs.com/settings/celo/members) can publish new packages or update the existing ones. +### Publish the package ``` # Publish the package publicly celo-monorepo/packages/cli $ yarn publish --access=public -# Increment the version number, after testing, we will push that commit to GitHub ``` Let's say the published package version number 0.0.20, verify that it is installable @@ -57,14 +72,20 @@ Let's say the published package version number 0.0.20, verify that it is install Add a tag with the most recent git commit of the published branch. Note that this commit comes before package.json is updated with the new package version. ``` -$ npm dist-tag add @ [] +$ npm dist-tag add @ +``` + +Additionally, if this version is intended to be used on a deployed network (e.g. `baklava` or `alfajores`), tag the version with all appropriate network names. + +``` +$ npm dist-tag add @ ``` Once you publish do some manual tests, for example, after publishing `celocli` ``` # Docker for an isolated environment again -celo-monorepo $ docker run -it --entrypoint bash node:10 +celo-monorepo $ docker run --rm -it --entrypoint bash node:10 root@e0d56700584f:/# mkdir /tmp/tmp1 && cd /tmp/tmp1 root@e0d56700584f:/tmp/tmp1# npm install @celo/celocli@0.0.20 /tmp/tmp1# ./node_modules/.bin/celocli @@ -93,6 +114,3 @@ mnemonic: wall school patrol also peasant enroll build merit health reduce junio privateKey: a9531609ca3d1c224e0742a4bb9b9e2fae67cc9d872797869092804e1500d67c publicKey: 0429b83753806f2b61ddab2e8a139214c3c8a5dfd0557557830b13342f2490bad6f61767e1b0707be51685e5e13683e6fa276333cbdb06f07768a09b8070e27259accountAddress: 0xf63e0F60DFcd84090D2863e0Dfa452B871fdC6d7 ``` - -Now push your changes `git push origin alfajores`. -If you don't have access to the repo, you might have to open a PR. diff --git a/packages/attestation-service/package.json b/packages/attestation-service/package.json index 04f6f34b68a..566ee596f71 100644 --- a/packages/attestation-service/package.json +++ b/packages/attestation-service/package.json @@ -28,7 +28,7 @@ }, "dependencies": { "@celo/contractkit": "0.2.8-dev", - "@celo/utils": "^0.1.0", + "@celo/utils": "0.1.5-dev", "bignumber.js": "^7.2.0", "body-parser": "1.19.0", "bunyan": "1.8.12", diff --git a/packages/celotool/package.json b/packages/celotool/package.json index 5a7f1f0e40b..3fe12bd98a4 100644 --- a/packages/celotool/package.json +++ b/packages/celotool/package.json @@ -7,7 +7,7 @@ "license": "Apache-2.0", "dependencies": { "@celo/verification-pool-api": "^1.0.0", - "@celo/utils": "^0.1.0", + "@celo/utils": "0.1.5-dev", "@celo/walletkit": "^0.0.15", "@celo/contractkit": "0.2.8-dev", "@google-cloud/monitoring": "0.7.1", diff --git a/packages/celotool/src/cmds/deploy/upgrade/hotfix.ts b/packages/celotool/src/cmds/deploy/upgrade/hotfix.ts new file mode 100644 index 00000000000..c36675f0d8f --- /dev/null +++ b/packages/celotool/src/cmds/deploy/upgrade/hotfix.ts @@ -0,0 +1,157 @@ +// This is a more unusual Celotool command. It basically helps you to execute Hotfixes on testnets. Because constructing proposals is difficult to do via a CLI, you should define them here in code. There are two examples below that you can start from. + +import { newKit } from '@celo/contractkit' +import { + ProposalBuilder, + proposalToHash, + proposalToJSON, +} from '@celo/contractkit/lib/governance/proposals' +import { privateKeyToAddress } from '@celo/utils/lib/address' +import { concurrentMap } from '@celo/utils/lib/async' +import { getFornoUrl } from 'src/lib/endpoints' +import { envVar, fetchEnv } from 'src/lib/env-utils' +import { AccountType, getPrivateKeysFor } from 'src/lib/generate_utils' +import yargs from 'yargs' +import { UpgradeArgv } from '../../deploy/upgrade' + +export const command = 'hotfix' + +export const describe = 'runs a hotfix' + +type EthstatsArgv = UpgradeArgv & {} + +export const builder = (argv: yargs.Argv) => { + return argv +} + +export const handler = async (argv: EthstatsArgv) => { + try { + const kit = newKit(getFornoUrl(argv.celoEnv)) + const governance = await kit.contracts.getGovernance() + const keys = getPrivateKeysFor( + AccountType.VALIDATOR, + fetchEnv(envVar.MNEMONIC), + parseInt(fetchEnv(envVar.VALIDATORS), 10) + ) + const addresses = keys.map(privateKeyToAddress) + + console.info('Add keys to ContractKit') + for (const key of keys) { + kit.addAccount(key) + } + + // Here you'll want to assert the current state + // Example A: Update a var on a Celo Core Contract + // const attestations = await kit.contracts.getAttestations() + // const currentNumber = await attestations.attestationExpiryBlocks() + // if (currentNumber !== 727) { + // throw new Error(`Expected current number to be 727, but was ${currentNumber}`) + // } + + // Example B: Repoint a Celo Core Contract proxy + // const validatorsProxyAddress = await kit.registry.addressFor(CeloContract.Validators) + // const currentValidatorsImplementationAddress = await getImplementationOfProxy( + // kit.web3, + // validatorsProxyAddress + // ) + // const desiredImplementationAddress = '0xd18620a5eBE0235023602bB4d490E1e96703EddD' + // console.info('Current Implementation Address: ', currentValidatorsImplementationAddress) + + // console.info('\nBuild Proposal') + + const proposalBuilder = new ProposalBuilder(kit) + + // Example A + // proposalBuilder.addJsonTx({ + // contract: CeloContract.Attestations, + // function: 'setAttestationExpiryBlocks', + // // @ts-ignore + // args: [728], + // value: '0', + // }) + + // Example B + // proposalBuilder.addProxyRepointingTx(validatorsProxyAddress, desiredImplementationAddress) + + const proposal = await proposalBuilder.build() + if (proposal.length === 0) { + console.error('\nPlease see examples in hotfix.ts and add transactions') + process.exit(1) + } + const proposalHash = proposalToHash(kit, proposal) + + // If your proposal is just made of Celo Registry contract methods, you can print it out + console.info('Proposal: ', await proposalToJSON(kit, proposal)) + console.info(`Proposal Hash: ${proposalHash.toString('hex')}`) + + console.info('\nWhitelist the hotfix') + await concurrentMap(25, addresses, async (address, index) => { + try { + await governance.whitelistHotfix(proposalHash).sendAndWaitForReceipt({ from: address }) + } catch (error) { + console.error(`Error whitelisting for validator ${index} (${address}): ${error}`) + } + }) + + let hotfixRecord = await governance.getHotfixRecord(proposalHash) + console.info('Hotfix Record: ', hotfixRecord) + + console.info('\nApprove the hotfix') + await governance.approveHotfix(proposalHash).sendAndWaitForReceipt({ from: addresses[0] }) + hotfixRecord = await governance.getHotfixRecord(proposalHash) + console.info('Hotfix Record: ', hotfixRecord) + + // This is on master, but not on baklava yet + const canPass = await governance.isHotfixPassing(proposalHash) + const tally = await governance.hotfixWhitelistValidatorTally(proposalHash) + + if (!canPass) { + throw new Error(`Hotfix cannot pass. Currently tally is ${tally}`) + } + + console.info('\nPrepare the hotfix') + await governance.prepareHotfix(proposalHash).sendAndWaitForReceipt({ from: addresses[0] }) + hotfixRecord = await governance.getHotfixRecord(proposalHash) + console.info('\nHotfix Record: ', hotfixRecord) + + if (hotfixRecord.preparedEpoch.toNumber() === 0) { + console.error('Hotfix could not be prepared') + throw new Error() + } + console.info('\nExecute the hotfix') + await governance.executeHotfix(proposal).sendAndWaitForReceipt({ from: addresses[0] }) + + hotfixRecord = await governance.getHotfixRecord(proposalHash) + console.info('\nHotfix Record: ', hotfixRecord) + + if (!hotfixRecord.executed) { + console.error('Hotfix could somehow not be executed') + throw new Error() + } + + // Assert any state to be sure it worked + + // Example A + // const newNumber = await attestations.attestationExpiryBlocks() + // if (newNumber !== 728) { + // throw new Error(`Expected current number to be 728, but was ${newNumber}`) + // } + + // Example B + // const newValidatorsImplementationAddress = await getImplementationOfProxy( + // kit.web3, + // validatorsProxyAddress + // ) + // if (!eqAddress(newValidatorsImplementationAddress, desiredImplementationAddress)) { + // throw new Error( + // `Expected new implementation address to be ${desiredImplementationAddress}, but was ${newValidatorsImplementationAddress}` + // ) + // } + + console.info('Hotfix successfully executed!') + process.exit(0) + } catch (error) { + console.error(error) + process.exit(1) + } +} diff --git a/packages/celotool/src/cmds/transactions/list.ts b/packages/celotool/src/cmds/transactions/list.ts index e8dea170676..0c6d96b18d5 100644 --- a/packages/celotool/src/cmds/transactions/list.ts +++ b/packages/celotool/src/cmds/transactions/list.ts @@ -73,7 +73,7 @@ async function fetchTx( console.info( `${parsedTransaction.callDetails.contract}#${ parsedTransaction.callDetails.function - }(${JSON.stringify(parsedTransaction.callDetails.parameters)}) ${parsedTransaction.tx.hash}` + }(${JSON.stringify(parsedTransaction.callDetails.paramMap)}) ${parsedTransaction.tx.hash}` ) if (receipt.logs) { diff --git a/packages/celotool/src/e2e-tests/transfer_tests.ts b/packages/celotool/src/e2e-tests/transfer_tests.ts index dbc05144410..c486daa813f 100644 --- a/packages/celotool/src/e2e-tests/transfer_tests.ts +++ b/packages/celotool/src/e2e-tests/transfer_tests.ts @@ -584,7 +584,7 @@ describe('Transfer tests', function(this: any) { }) describe('Transfer CeloDollars', () => { - const evmGasCost = 20303 + const evmGasCost = 20325 describe('feeCurrency = CeloDollars >', () => { testTransferToken({ expectedGas: evmGasCost + INTRINSIC_TX_GAS_COST + ADDITIONAL_INTRINSIC_TX_GAS_COST, @@ -660,7 +660,7 @@ describe('Transfer tests', function(this: any) { describe('Transfer CeloDollars', () => { describe('feeCurrency = CeloDollars >', () => { testTransferToken({ - expectedGas: 75303, + expectedGas: 75325, transferToken: CeloContract.StableToken, feeToken: CeloContract.StableToken, }) diff --git a/packages/celotool/src/lib/env-utils.ts b/packages/celotool/src/lib/env-utils.ts index 04023c45974..344d78892f6 100644 --- a/packages/celotool/src/lib/env-utils.ts +++ b/packages/celotool/src/lib/env-utils.ts @@ -39,6 +39,7 @@ export enum envVar { ETHSTATS_DOCKER_IMAGE_TAG = 'ETHSTATS_DOCKER_IMAGE_TAG', ETHSTATS_TRUSTED_ADDRESSES = 'ETHSTATS_TRUSTED_ADDRESSES', ETHSTATS_BANNED_ADDRESSES = 'ETHSTATS_BANNED_ADDRESSES', + ETHSTATS_RESERVED_ADDRESSES = 'ETHSTATS_RESERVED_ADDRESSES', FAUCET_GENESIS_ACCOUNTS = 'FAUCET_GENESIS_ACCOUNTS', FAUCET_GENESIS_BALANCE = 'FAUCET_GENESIS_BALANCE', ORACLE_GENESIS_BALANCE = 'ORACLE_GENESIS_BALANCE', diff --git a/packages/celotool/src/lib/ethstats.ts b/packages/celotool/src/lib/ethstats.ts index 21fd416b298..8bde5c56e90 100644 --- a/packages/celotool/src/lib/ethstats.ts +++ b/packages/celotool/src/lib/ethstats.ts @@ -1,12 +1,18 @@ import { installGenericHelmChart, removeGenericHelmChart } from 'src/lib/helm_deploy' import { execCmdWithExitOnFailure } from 'src/lib/utils' +import { getBlockscoutUrl } from './endpoints' import { envVar, fetchEnv, fetchEnvOrFallback } from './env-utils' import { AccountType, getAddressesFor } from './generate_utils' const helmChartPath = '../helm-charts/ethstats' export async function installHelmChart(celoEnv: string) { - return installGenericHelmChart(celoEnv, releaseName(celoEnv), helmChartPath, helmParameters()) + return installGenericHelmChart( + celoEnv, + releaseName(celoEnv), + helmChartPath, + helmParameters(celoEnv) + ) } export async function removeHelmRelease(celoEnv: string) { @@ -18,7 +24,7 @@ export async function upgradeHelmChart(celoEnv: string) { const upgradeCmdArgs = `${releaseName( celoEnv - )} ${helmChartPath} --namespace ${celoEnv} ${helmParameters().join(' ')}` + )} ${helmChartPath} --namespace ${celoEnv} ${helmParameters(celoEnv).join(' ')}` if (process.env.CELOTOOL_VERBOSE === 'true') { await execCmdWithExitOnFailure(`helm upgrade --debug --dry-run ${upgradeCmdArgs}`) @@ -27,13 +33,24 @@ export async function upgradeHelmChart(celoEnv: string) { console.info(`Helm release ${releaseName(celoEnv)} upgrade successful`) } -function helmParameters() { +function helmParameters(celoEnv: string) { return [ `--set domain.name=${fetchEnv(envVar.CLUSTER_DOMAIN_NAME)}`, `--set ethstats.image.repository=${fetchEnv(envVar.ETHSTATS_DOCKER_IMAGE_REPOSITORY)}`, `--set ethstats.image.tag=${fetchEnv(envVar.ETHSTATS_DOCKER_IMAGE_TAG)}`, - `--set ethstats.trusted_addresses='{${generateAuthorizedAddresses()}}'`, - `--set ethstats.banned_addresses='{${fetchEnv(envVar.ETHSTATS_BANNED_ADDRESSES)}}'`, + `--set ethstats.trusted_addresses='${String(generateAuthorizedAddresses()).replace( + /,/g, + '\\,' + )}'`, + `--set ethstats.banned_addresses='${String(fetchEnv(envVar.ETHSTATS_BANNED_ADDRESSES)).replace( + /,/g, + '\\,' + )}'`, + `--set ethstats.reserved_addresses='${String( + fetchEnv(envVar.ETHSTATS_RESERVED_ADDRESSES) + ).replace(/,/g, '\\,')}'`, + `--set ethstats.network_name='Celo ${celoEnv}'`, + `--set ethstats.blockscout_url='${getBlockscoutUrl(celoEnv)}'`, ] } @@ -51,5 +68,5 @@ function generateAuthorizedAddresses() { publicKeys.push(getAddressesFor(AccountType.VALIDATOR, mnemonic, validatorNodes)) publicKeys.push(fetchEnvOrFallback(envVar.ETHSTATS_TRUSTED_ADDRESSES, '').split(',')) - return publicKeys.reduce((accumulator, value) => accumulator.concat(value), []) + return publicKeys.reduce((accumulator, value) => accumulator.concat(value), []).filter((_) => !!_) } diff --git a/packages/cli/package.json b/packages/cli/package.json index bae8ef91cb4..7ed42a0aa03 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -32,7 +32,7 @@ }, "dependencies": { "@celo/contractkit": "0.2.8-dev", - "@celo/utils": "^0.1.0", + "@celo/utils": "0.1.5-dev", "@oclif/command": "^1", "@oclif/config": "^1", "@oclif/plugin-help": "^2", diff --git a/packages/cli/src/base.ts b/packages/cli/src/base.ts index 0c1eb29ce53..71d01bc960d 100644 --- a/packages/cli/src/base.ts +++ b/packages/cli/src/base.ts @@ -12,6 +12,12 @@ export abstract class LocalCommand extends Command { static flags = { logLevel: flags.string({ char: 'l', hidden: true }), help: flags.help({ char: 'h', hidden: true }), + truncate: flags.boolean({ + default: true, + hidden: true, + allowNo: true, + description: 'Truncate fields to fit line', + }), } // TODO(yorke): implement log(msg) switch on logLevel with chalk colored output diff --git a/packages/cli/src/commands/account/unlock.ts b/packages/cli/src/commands/account/unlock.ts index 50dcae56613..5418a3c5c47 100644 --- a/packages/cli/src/commands/account/unlock.ts +++ b/packages/cli/src/commands/account/unlock.ts @@ -1,29 +1,31 @@ import { flags } from '@oclif/command' +import { IArg } from '@oclif/parser/lib/args' import { cli } from 'cli-ux' import { BaseCommand } from '../../base' -import { Flags } from '../../utils/command' +import { Args } from '../../utils/command' export default class Unlock extends BaseCommand { static description = 'Unlock an account address to send transactions or validate blocks' static flags = { ...BaseCommand.flags, - account: Flags.address({ required: true }), password: flags.string({ required: false }), } - static examples = ['unlock --account 0x5409ed021d9299bf6814279a6a1411a7e866a631'] + static args: IArg[] = [Args.address('account', { description: 'Account address' })] + + static examples = ['unlock 0x5409ed021d9299bf6814279a6a1411a7e866a631'] requireSynced = false async run() { const res = this.parse(Unlock) - // Unlock till geth exits + // Unlock until geth exits // Source: https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_unlockaccount const unlockDurationInMs = 0 const password = res.flags.password || (await cli.prompt('Password', { type: 'hide', required: false })) - this.web3.eth.personal.unlockAccount(res.flags.account, password, unlockDurationInMs) + this.web3.eth.personal.unlockAccount(res.args.account, password, unlockDurationInMs) } } diff --git a/packages/cli/src/commands/election/current.ts b/packages/cli/src/commands/election/current.ts index b56c8e209ac..5b636889b76 100644 --- a/packages/cli/src/commands/election/current.ts +++ b/packages/cli/src/commands/election/current.ts @@ -11,6 +11,7 @@ export default class ElectionCurrent extends BaseCommand { } async run() { + const res = this.parse(ElectionCurrent) cli.action.start('Fetching currently elected Validators') const election = await this.kit.contracts.getElection() const validators = await this.kit.contracts.getValidators() @@ -19,6 +20,6 @@ export default class ElectionCurrent extends BaseCommand { signers.map((addr) => validators.getValidatorFromSigner(addr)) ) cli.action.stop() - cli.table(validatorList, validatorTable) + cli.table(validatorList, validatorTable, { 'no-truncate': !res.flags.truncate }) } } diff --git a/packages/cli/src/commands/election/list.ts b/packages/cli/src/commands/election/list.ts index c3f66077702..9aeef7c1c73 100644 --- a/packages/cli/src/commands/election/list.ts +++ b/packages/cli/src/commands/election/list.ts @@ -12,16 +12,21 @@ export default class List extends BaseCommand { static examples = ['list'] async run() { + const res = this.parse(List) 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: {}, - name: {}, - votes: { get: (g) => g.votes.toFixed() }, - capacity: { get: (g) => g.capacity.toFixed() }, - eligible: {}, - }) + cli.table( + groupVotes, + { + address: {}, + name: {}, + votes: { get: (g) => g.votes.toFixed() }, + capacity: { get: (g) => g.capacity.toFixed() }, + eligible: {}, + }, + { 'no-truncate': !res.flags.truncate } + ) } } diff --git a/packages/cli/src/commands/election/run.ts b/packages/cli/src/commands/election/run.ts index 34a9fba3b6f..0de00e6e796 100644 --- a/packages/cli/src/commands/election/run.ts +++ b/packages/cli/src/commands/election/run.ts @@ -11,6 +11,7 @@ export default class ElectionRun extends BaseCommand { } async run() { + const res = this.parse(ElectionRun) cli.action.start('Running mock election') const election = await this.kit.contracts.getElection() const validators = await this.kit.contracts.getValidators() @@ -19,6 +20,6 @@ export default class ElectionRun extends BaseCommand { signers.map((addr) => validators.getValidatorFromSigner(addr)) ) cli.action.stop() - cli.table(validatorList, validatorTable) + cli.table(validatorList, validatorTable, { 'no-truncate': !res.flags.truncate }) } } diff --git a/packages/cli/src/commands/exchange/dollars.ts b/packages/cli/src/commands/exchange/dollars.ts index 11b39d4d287..df9755083ef 100644 --- a/packages/cli/src/commands/exchange/dollars.ts +++ b/packages/cli/src/commands/exchange/dollars.ts @@ -1,3 +1,4 @@ +import BigNumber from 'bignumber.js' import { BaseCommand } from '../../base' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' @@ -15,22 +16,23 @@ export default class ExchangeDollars extends BaseCommand { required: true, description: 'The value of Celo Dollars to exchange for Celo Gold', }), - for: Flags.wei({ - required: true, - description: 'The minimum value of Celo Gold to receive in return', + forAtLeast: Flags.wei({ + description: 'Optional, the minimum value of Celo Gold to receive in return', + default: new BigNumber(0), }), } static args = [] static examples = [ - 'dollars --value 10000000000000 --for 50000000000000 --from 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', + 'dollars --value 10000000000000 --from 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', + 'dollars --value 10000000000000 --forAtLeast 50000000000000 --from 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', ] async run() { const res = this.parse(ExchangeDollars) const sellAmount = res.flags.value - const minBuyAmount = res.flags.for + const minBuyAmount = res.flags.forAtLeast this.kit.defaultAccount = res.flags.from const stableToken = await this.kit.contracts.getStableToken() @@ -38,7 +40,7 @@ export default class ExchangeDollars extends BaseCommand { await displaySendTx('approve', stableToken.approve(exchange.address, sellAmount.toFixed())) - const exchangeTx = exchange.exchange(sellAmount.toFixed(), minBuyAmount.toFixed(), false) + const exchangeTx = exchange.exchange(sellAmount.toFixed(), minBuyAmount!.toFixed(), false) await displaySendTx('exchange', exchangeTx) } } diff --git a/packages/cli/src/commands/exchange/gold.ts b/packages/cli/src/commands/exchange/gold.ts index e476e3ef5b1..e528d83270c 100644 --- a/packages/cli/src/commands/exchange/gold.ts +++ b/packages/cli/src/commands/exchange/gold.ts @@ -1,3 +1,4 @@ +import BigNumber from 'bignumber.js' import { BaseCommand } from '../../base' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' @@ -12,22 +13,23 @@ export default class ExchangeGold extends BaseCommand { required: true, description: 'The value of Celo Gold to exchange for Celo Dollars', }), - for: Flags.wei({ - required: true, - description: 'The minimum value of Celo Dollars to receive in return', + forAtLeast: Flags.wei({ + description: 'Optional, the minimum value of Celo Dollars to receive in return', + default: new BigNumber(0), }), } static args = [] static examples = [ - 'gold --value 5000000000000 --for 100000000000000 --from 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', + 'gold --value 5000000000000 --from 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', + 'gold --value 5000000000000 --forAtLeast 100000000000000 --from 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d', ] async run() { const res = this.parse(ExchangeGold) const sellAmount = res.flags.value - const minBuyAmount = res.flags.for + const minBuyAmount = res.flags.forAtLeast this.kit.defaultAccount = res.flags.from const goldToken = await this.kit.contracts.getGoldToken() @@ -35,7 +37,7 @@ export default class ExchangeGold extends BaseCommand { await displaySendTx('approve', goldToken.approve(exchange.address, sellAmount.toFixed())) - const exchangeTx = exchange.exchange(sellAmount.toFixed(), minBuyAmount.toFixed(), true) + const exchangeTx = exchange.exchange(sellAmount.toFixed(), minBuyAmount!.toFixed(), true) await displaySendTx('exchange', exchangeTx) } } diff --git a/packages/cli/src/commands/governance/approve.ts b/packages/cli/src/commands/governance/approve.ts new file mode 100644 index 00000000000..65ef2770026 --- /dev/null +++ b/packages/cli/src/commands/governance/approve.ts @@ -0,0 +1,24 @@ +import { flags } from '@oclif/command' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class Approve extends BaseCommand { + static description = 'Approve a dequeued governance proposal' + + static flags = { + ...BaseCommand.flags, + proposalID: flags.string({ required: true, description: 'UUID of proposal to approve' }), + from: Flags.address({ required: true, description: "Approver's address" }), + } + + static examples = [] + + async run() { + const res = this.parse(Approve) + + const governance = await this.kit.contracts.getGovernance() + const tx = await governance.approve(res.flags.proposalID) + await displaySendTx('approveTx', tx, { from: res.flags.from }) + } +} diff --git a/packages/cli/src/commands/governance/approvehotfix.ts b/packages/cli/src/commands/governance/approvehotfix.ts new file mode 100644 index 00000000000..d5be8da8db4 --- /dev/null +++ b/packages/cli/src/commands/governance/approvehotfix.ts @@ -0,0 +1,26 @@ +import { flags } from '@oclif/command' +import { toBuffer } from 'ethereumjs-util' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class ApproveHotfix extends BaseCommand { + static description = 'Approve a governance hotfix' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: "Approver's address" }), + hash: flags.string({ required: true, description: 'Hash of hotfix transactions' }), + } + + static examples = [] + + async run() { + const res = this.parse(ApproveHotfix) + + const governance = await this.kit.contracts.getGovernance() + const hash = toBuffer(res.flags.hash) as Buffer + const tx = governance.approveHotfix(hash) + await displaySendTx('approveHotfixTx', tx, { from: res.flags.from }) + } +} diff --git a/packages/cli/src/commands/governance/execute.ts b/packages/cli/src/commands/governance/execute.ts new file mode 100644 index 00000000000..b1f00b936c5 --- /dev/null +++ b/packages/cli/src/commands/governance/execute.ts @@ -0,0 +1,25 @@ +import { flags } from '@oclif/command' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class Execute extends BaseCommand { + static description = 'Execute a passing governance proposal' + + static flags = { + ...BaseCommand.flags, + proposalID: flags.string({ required: true, description: 'UUID of proposal to execute' }), + from: Flags.address({ required: true, description: "Executor's address" }), + } + + static examples = [] + + async run() { + const res = this.parse(Execute) + + const governance = await this.kit.contracts.getGovernance() + + const tx = await governance.execute(res.flags.proposalID) + await displaySendTx('executeTx', tx, { from: res.flags.from }) + } +} diff --git a/packages/cli/src/commands/governance/executehotfix.ts b/packages/cli/src/commands/governance/executehotfix.ts new file mode 100644 index 00000000000..0c9902160d4 --- /dev/null +++ b/packages/cli/src/commands/governance/executehotfix.ts @@ -0,0 +1,26 @@ +import { flags } from '@oclif/command' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' +import { buildProposalFromJsonFile } from '../../utils/governance' + +export default class ExecuteHotfix extends BaseCommand { + static description = 'Execute a governance hotfix prepared for the current epoch' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: "Executors's address" }), + jsonTransactions: flags.string({ required: true, description: 'Path to json transactions' }), + } + + static examples = [] + + async run() { + const res = this.parse(ExecuteHotfix) + + const governance = await this.kit.contracts.getGovernance() + const hotfix = await buildProposalFromJsonFile(this.kit, res.flags.jsonTransactions) + const tx = governance.executeHotfix(hotfix) + await displaySendTx('executeHotfixTx', tx, { from: res.flags.from }) + } +} diff --git a/packages/cli/src/commands/governance/preparehotfix.ts b/packages/cli/src/commands/governance/preparehotfix.ts new file mode 100644 index 00000000000..d192d5c4fcc --- /dev/null +++ b/packages/cli/src/commands/governance/preparehotfix.ts @@ -0,0 +1,26 @@ +import { flags } from '@oclif/command' +import { toBuffer } from 'ethereumjs-util' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class PrepareHotfix extends BaseCommand { + static description = 'Prepare a governance hotfix for execution in the current epoch' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: "Preparer's address" }), + hash: flags.string({ required: true, description: 'Hash of hotfix transactions' }), + } + + static examples = [] + + async run() { + const res = this.parse(PrepareHotfix) + + const governance = await this.kit.contracts.getGovernance() + const hash = toBuffer(res.flags.hash) as Buffer + const tx = governance.prepareHotfix(hash) + await displaySendTx('prepareHotfixTx', tx, { from: res.flags.from }) + } +} diff --git a/packages/cli/src/commands/governance/propose.ts b/packages/cli/src/commands/governance/propose.ts new file mode 100644 index 00000000000..981714053d2 --- /dev/null +++ b/packages/cli/src/commands/governance/propose.ts @@ -0,0 +1,30 @@ +import { flags } from '@oclif/command' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' +import { buildProposalFromJsonFile } from '../../utils/governance' + +export default class Propose extends BaseCommand { + static description = 'Submit a governance proposal' + + static flags = { + ...BaseCommand.flags, + jsonTransactions: flags.string({ required: true, description: 'Path to json transactions' }), + deposit: flags.string({ required: true, description: 'Amount of Gold to attach to proposal' }), + from: Flags.address({ required: true, description: "Proposer's address" }), + } + + static examples = [] + + async run() { + const res = this.parse(Propose) + + const governance = await this.kit.contracts.getGovernance() + + const proposal = await buildProposalFromJsonFile(this.kit, res.flags.jsonTransactions) + const tx = governance.propose(proposal) + await displaySendTx('proposeTx', tx, { from: res.flags.from, value: res.flags.deposit }) + // const proposalID = await tx.txo.call() + // this.log(`ProposalID: ${proposalID}`) + } +} diff --git a/packages/cli/src/commands/governance/upvote.ts b/packages/cli/src/commands/governance/upvote.ts new file mode 100644 index 00000000000..4f772df5d1b --- /dev/null +++ b/packages/cli/src/commands/governance/upvote.ts @@ -0,0 +1,24 @@ +import { flags } from '@oclif/command' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class Upvote extends BaseCommand { + static description = 'Upvote a queued governance proposal' + + static flags = { + ...BaseCommand.flags, + proposalID: flags.string({ required: true, description: 'UUID of proposal to upvote' }), + from: Flags.address({ required: true, description: "Upvoter's address" }), + } + + static examples = [] + + async run() { + const res = this.parse(Upvote) + + const governance = await this.kit.contracts.getGovernance() + const tx = await governance.upvote(res.flags.proposalID, res.flags.from) + await displaySendTx('upvoteTx', tx, { from: res.flags.from }) + } +} diff --git a/packages/cli/src/commands/governance/vote.ts b/packages/cli/src/commands/governance/vote.ts new file mode 100644 index 00000000000..7bac4f0b8ed --- /dev/null +++ b/packages/cli/src/commands/governance/vote.ts @@ -0,0 +1,29 @@ +import { VoteValue } from '@celo/contractkit/lib/wrappers/Governance' +import { flags } from '@oclif/command' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class Vote extends BaseCommand { + static description = 'Vote on an approved governance proposal' + + static voteOptions = ['Abstain', 'No', 'Yes'] + + static flags = { + ...BaseCommand.flags, + proposalID: flags.string({ required: true, description: 'UUID of proposal to vote on' }), + vote: flags.enum({ options: Vote.voteOptions, required: true, description: 'Vote' }), + from: Flags.address({ required: true, description: "Voter's address" }), + } + + static examples = [] + + async run() { + const res = this.parse(Vote) + + const governance = await this.kit.contracts.getGovernance() + + const tx = await governance.vote(res.flags.proposalID, res.flags.vote as keyof typeof VoteValue) + await displaySendTx('voteTx', tx, { from: res.flags.from }) + } +} diff --git a/packages/cli/src/commands/governance/whitelisthotfix.ts b/packages/cli/src/commands/governance/whitelisthotfix.ts new file mode 100644 index 00000000000..97bd72bc4e7 --- /dev/null +++ b/packages/cli/src/commands/governance/whitelisthotfix.ts @@ -0,0 +1,26 @@ +import { flags } from '@oclif/command' +import { toBuffer } from 'ethereumjs-util' +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class WhitelistHotfix extends BaseCommand { + static description = 'Whitelist a governance hotfix' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ required: true, description: "Whitelister's address" }), + hash: flags.string({ required: true, description: 'Hash of hotfix transactions' }), + } + + static examples = [] + + async run() { + const res = this.parse(WhitelistHotfix) + + const governance = await this.kit.contracts.getGovernance() + const hash = toBuffer(res.flags.hash) as Buffer + const tx = governance.whitelistHotfix(hash) + await displaySendTx('whitelistHotfixTx', tx, { from: res.flags.from }) + } +} diff --git a/packages/cli/src/commands/oracle/rates.ts b/packages/cli/src/commands/oracle/rates.ts index 18c0d41cb7a..e08eeffe43d 100644 --- a/packages/cli/src/commands/oracle/rates.ts +++ b/packages/cli/src/commands/oracle/rates.ts @@ -26,9 +26,13 @@ export default class GetRates extends BaseCommand { 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() }, - }) + cli.table( + rates, + { + address: {}, + rate: { get: (r) => r.rate.toNumber() }, + }, + { 'no-truncate': !res.flags.truncate } + ) } } diff --git a/packages/cli/src/commands/rewards/show.ts b/packages/cli/src/commands/rewards/show.ts new file mode 100644 index 00000000000..a2468410b6e --- /dev/null +++ b/packages/cli/src/commands/rewards/show.ts @@ -0,0 +1,243 @@ +import { Address } from '@celo/contractkit/lib/base' +import { GroupVoterReward, VoterReward } from '@celo/contractkit/lib/wrappers/Election' +import { Validator, ValidatorReward } from '@celo/contractkit/lib/wrappers/Validators' +import { eqAddress } from '@celo/utils/lib/address' +import { flags } from '@oclif/command' +import BigNumber from 'bignumber.js' +import { cli } from 'cli-ux' +import { BaseCommand } from '../../base' +import { newCheckBuilder } from '../../utils/checks' +import { Flags } from '../../utils/command' + +interface ExplainedVoterReward extends VoterReward { + validators: Validator[] +} + +interface ExplainedGroupVoterReward extends GroupVoterReward { + validators: Validator[] +} + +export default class Show extends BaseCommand { + static description = + 'Show rewards information about a voter, registered Validator, or Validator Group' + + static flags = { + ...BaseCommand.flags, + voter: Flags.address({ description: 'Voter to show rewards for' }), + validator: Flags.address({ description: 'Validator to show rewards for' }), + group: Flags.address({ description: 'Validator Group to show rewards for' }), + epochs: flags.integer({ + default: 1, + description: 'Show results for the last N epochs', + }), + } + + static args = [] + + static examples = ['show --address 0x5409ed021d9299bf6814279a6a1411a7e866a631'] + + async run() { + const res = this.parse(Show) + const filter = + Boolean(res.flags.voter) || Boolean(res.flags.validator) || Boolean(res.flags.group) + const election = await this.kit.contracts.getElection() + const validators = await this.kit.contracts.getValidators() + const currentEpoch = (await validators.getEpochNumber()).toNumber() + const checkBuilder = newCheckBuilder(this) + const epochs = Math.max(1, res.flags.epochs || 1) + + if (res.flags.validator) { + if (res.flags.voter || res.flags.group) { + throw Error('Cannot select --validator with --voter or --group') + } + checkBuilder.isValidator(res.flags.validator) + } + if (res.flags.group) { + checkBuilder.isValidatorGroup(res.flags.group) + } + if (res.flags.voter) { + checkBuilder.isAccount(res.flags.voter) + } + await checkBuilder.runChecks() + + let voterRewards: ExplainedVoterReward[] = [] + let groupVoterRewards: ExplainedGroupVoterReward[] = [] + let validatorRewards: ValidatorReward[] = [] + let validatorGroupRewards: ValidatorReward[] = [] + + // Accumulate the rewards from each epoch + for ( + let epochNumber = Math.max(0, currentEpoch - epochs); + epochNumber < currentEpoch; + epochNumber++ + ) { + if (!filter || res.flags.voter) { + const electedValidators = await election.getElectedValidators(epochNumber) + if (!filter) { + const epochGroupVoterRewards = await election.getGroupVoterRewards(epochNumber) + groupVoterRewards = groupVoterRewards.concat( + epochGroupVoterRewards.map( + (e: GroupVoterReward): ExplainedGroupVoterReward => ({ + ...e, + validators: filterValidatorsByGroup(electedValidators, e.group.address), + }) + ) + ) + } else if (res.flags.voter) { + const address = res.flags.voter + const epochVoterRewards = await election.getVoterRewards(address, epochNumber) + voterRewards = voterRewards.concat( + epochVoterRewards.map( + (e: VoterReward): ExplainedVoterReward => ({ + ...e, + validators: filterValidatorsByGroup(electedValidators, e.group.address), + }) + ) + ) + } + } + + if (!filter || res.flags.validator || res.flags.group) { + const epochValidatorRewards: ValidatorReward[] = await validators.getValidatorRewards( + epochNumber + ) + + if (!filter || res.flags.validator) { + const address = res.flags.validator + validatorRewards = validatorRewards.concat( + address + ? epochValidatorRewards.filter((e: ValidatorReward) => + eqAddress(e.validator.address, address) + ) + : epochValidatorRewards + ) + } + + if (!filter || res.flags.group) { + const address = res.flags.group + validatorGroupRewards = validatorGroupRewards.concat( + address + ? epochValidatorRewards.filter((e: ValidatorReward) => + eqAddress(e.group.address, address) + ) + : epochValidatorRewards + ) + } + } + } + + // At the end of each epoch: R, the total amount of rewards in gold to be allocated to stakers + // for this epoch is programmatically derived from considering the tradeoff between paying rewards + // now vs. saving rewards for later. + // + // Every validator group has a slashing penalty M, initially M=1.0. All rewards to the group and to + // voters for the group are weighted by this factor. + // + // Let T be the total gold voting for groups eligible for rewards in this epoch. For each account + // holder, for each group, the amount of gold the account holder has voting for that group is increased + // by average_epoch_score_of_elected_validators_in_group * account_gold_voting_for_group * R * M / T. + if (voterRewards.length > 0) { + console.info('') + console.info('Voter rewards:') + cli.table( + voterRewards, + { + address: {}, + addressPayment: {}, + group: { get: (e) => e.group.address }, + averageValidatorScore: { get: (e) => averageValidatorScore(e.validators).toFixed() }, + epochNumber: {}, + }, + { 'no-truncate': !res.flags.truncate } + ) + } else if (groupVoterRewards.length > 0) { + console.info('') + console.info('Group voter rewards:') + cli.table( + groupVoterRewards, + { + groupName: { get: (e) => e.group.name }, + group: { get: (e) => e.group.address }, + groupVoterPayment: {}, + averageValidatorScore: { get: (e) => averageValidatorScore(e.validators).toFixed() }, + epochNumber: {}, + }, + { 'no-truncate': !res.flags.truncate } + ) + } + + // Each validator maintains a running validator score Sv: + // + // - At the end of an epoch, if a validator was elected, define its uptime: + // U = counter / (blocks_in_epoch - [9]) + // + // - Define the validator’s epoch score: + // Sve = U ^ k, where k is governable. + // + // - If the validator is elected, Sv = min(Sve, Sve * x + Sv-1 * (1 -x)) where 0 < x < 1 and is + // governable. Otherwise, Sv = Sv-1 + // + // At the end of each epoch, provided that the validator and its group have the required minimum + // stake, Validators are paid Pv * Sv * M * (1 - C) Celo Dollars where + // C is group share for the group the validator was a member of when it was elected and + // Pv is the max payout to validators and is governable. + if (validatorRewards.length > 0) { + console.info('') + console.info('Validator rewards:') + cli.table( + validatorRewards, + { + validatorName: { get: (e) => e.validator.name }, + validator: { get: (e) => e.validator.address }, + validatorPayment: {}, + validatorScore: { get: (e) => e.validator.score.toFixed() }, + group: { get: (e) => e.group.address }, + epochNumber: {}, + }, + { 'no-truncate': !res.flags.truncate } + ) + } + + // At the end of each epoch, for each validator that was elected, the group a validator was + // elected as a member of is paid Pv * Sv * C * M Celo Dollars where: + // C is the current group share for the group the validator was a member of when it was elected, + // Pv is the max payout to validators during this epoch programmatically derived from + // considering the tradeoff between paying rewards now vs. saving rewards for later, and + // M is the group’s current slashing penalty (M=1 initially, 0 0) { + console.info('') + console.info('Validator Group rewards:') + cli.table( + validatorGroupRewards, + { + groupName: { get: (e) => e.group.name }, + group: { get: (e) => e.group.address }, + groupPayment: {}, + validator: { get: (e) => e.validator.address }, + validatorScore: { get: (e) => e.validator.score.toFixed() }, + epochNumber: {}, + }, + { 'no-truncate': !res.flags.truncate } + ) + } + + if ( + voterRewards.length === 0 && + groupVoterRewards.length === 0 && + validatorRewards.length === 0 && + validatorGroupRewards.length === 0 + ) { + console.info('No rewards.') + } + } +} + +function filterValidatorsByGroup(validators: Validator[], group: Address) { + return validators.filter((v) => eqAddress(v.affiliation || '', group)) +} + +function averageValidatorScore(validators: Validator[]): BigNumber { + return validators + .reduce((sumScore: BigNumber, v: Validator) => sumScore.plus(v.score), new BigNumber(0)) + .dividedBy(validators.length || 1) +} diff --git a/packages/cli/src/commands/validator/list.ts b/packages/cli/src/commands/validator/list.ts index becf519984a..941c53147f6 100644 --- a/packages/cli/src/commands/validator/list.ts +++ b/packages/cli/src/commands/validator/list.ts @@ -1,5 +1,4 @@ import { Validator } from '@celo/contractkit/src/wrappers/Validators' -import { flags } from '@oclif/command' import { cli } from 'cli-ux' import { BaseCommand } from '../../base' @@ -19,10 +18,6 @@ export default class ValidatorList extends BaseCommand { static flags = { ...BaseCommand.flags, - 'no-truncate': flags.boolean({ - description: "Don't truncate fields to fit line", - required: false, - }), } static examples = ['list'] @@ -35,6 +30,6 @@ export default class ValidatorList extends BaseCommand { const validatorList = await validators.getRegisteredValidators() cli.action.stop() - cli.table(validatorList, validatorTable, { 'no-truncate': res.flags['no-truncate'] }) + cli.table(validatorList, validatorTable, { 'no-truncate': !res.flags.truncate }) } } diff --git a/packages/cli/src/commands/validator/status.ts b/packages/cli/src/commands/validator/status.ts index bcf81dc417b..781047ff0ab 100644 --- a/packages/cli/src/commands/validator/status.ts +++ b/packages/cli/src/commands/validator/status.ts @@ -4,108 +4,128 @@ import { concurrentMap } from '@celo/utils/lib/async' import { bitIsSet, parseBlockExtraData } from '@celo/utils/lib/istanbul' import { flags } from '@oclif/command' import { cli } from 'cli-ux' +import { Block } from 'web3/eth/types' import { BaseCommand } from '../../base' import { newCheckBuilder } from '../../utils/checks' import { Flags } from '../../utils/command' +interface ValidatorStatusEntry { + name: string + address: Address + signer: Address + elected: boolean + frontRunner: boolean + signatures: number + proposed: number +} + +export const statusTable = { + address: {}, + name: {}, + signer: {}, + elected: {}, + frontRunner: {}, + proposed: {}, + signatures: { get: (v: ValidatorStatusEntry) => Math.round(v.signatures * 100) + '%' }, +} + export default class ValidatorStatus extends BaseCommand { static description = - 'Show information about whether the validator signer is elected and validating. This command will check that the validator meets the registration requirements, and its signer is currently elected and actively signing blocks.' + 'Shows the consensus status of a validator. This command will show whether a validator is currently elected, would be elected if an election were to be run right now, and the percentage of blocks signed and number of blocks successfully proposed within a given window.' static flags = { ...BaseCommand.flags, + validator: Flags.address({ + description: 'address of the validator to check if elected and validating', + exclusive: ['all', 'signer'], + }), signer: Flags.address({ description: 'address of the signer to check if elected and validating', - exclusive: ['validator'], + exclusive: ['validator', 'all'], }), - validator: Flags.address({ - description: 'address of the validator to check if elected and validating', - exclusive: ['signer'], + all: flags.boolean({ + description: 'get the status of all registered validators', + exclusive: ['validator', 'signer'], }), lookback: flags.integer({ description: 'how many blocks to look back for signer activity', default: 100, }), + 'no-truncate': flags.boolean({ + description: "Don't truncate fields to fit line", + required: false, + }), } static examples = [ 'status --validator 0x5409ED021D9299bf6814279A6A1411A7e866A631', - 'status --signer 0x738337030fAeb1E805253228881d844b5332fB4c', - 'status --signer 0x738337030fAeb1E805253228881d844b5332fB4c --lookback 100', + 'status --all --lookback 100', ] requireSynced = true + async getStatus( + signer: Address, + blocks: Block[], + electedSigners: Address[], + frontRunnerSigners: Address[] + ): Promise { + const accounts = await this.kit.contracts.getAccounts() + const validator = await accounts.signerToAccount(signer) + const name = (await accounts.getName(validator)) || '' + const electedIndex = electedSigners.map((a) => eqAddress(a, signer)).indexOf(true) + const frontRunnerIndex = frontRunnerSigners.map((a) => eqAddress(a, signer)).indexOf(true) + const proposedCount = blocks.filter((b) => b.miner === signer).length + let signedCount = 0 + if (electedIndex >= 0) { + signedCount = blocks.filter((b) => + bitIsSet(parseBlockExtraData(b.extraData).parentAggregatedSeal.bitmap, electedIndex) + ).length + } + return { + name, + address: validator, + signer, + elected: electedIndex >= 0, + frontRunner: frontRunnerIndex >= 0, + proposed: proposedCount, + signatures: signedCount / blocks.length, + } + } + async run() { const res = this.parse(ValidatorStatus) - - // Check that the specified validator or signer meets the validator requirements. - const checker = newCheckBuilder(this, res.flags.signer) - if (res.flags.validator) { - const account = res.flags.validator - checker - .isAccount(account) - .isValidator(account) - .meetsValidatorBalanceRequirements(account) - } else if (res.flags.signer) { - checker - .isSignerOrAccount() - .signerMeetsValidatorBalanceRequirements() - .signerAccountIsValidator() + const accounts = await this.kit.contracts.getAccounts() + const validators = await this.kit.contracts.getValidators() + let signers: string[] = [] + const checker = newCheckBuilder(this) + if (res.flags.signer) { + signers = [res.flags.signer] + const validator = await accounts.signerToAccount(res.flags.signer) + checker.isAccount(validator).isValidator(validator) + await checker.runChecks() + } else if (res.flags.validator) { + checker.isAccount(res.flags.validator).isValidator(res.flags.validator) + await checker.runChecks() + const signer = await accounts.getValidatorSigner(res.flags.validator) + signers = [signer] } else { - this.error('Either validator or signer must be specified') - } - await checker.runChecks() - - // Get the signer from the validator account if not provided. - let signer: Address = res.flags.signer || '' - if (!signer) { - const accounts = await this.kit.contracts.getAccounts() - signer = await accounts.getValidatorSigner(res.flags.validator!) - console.info(`Identified ${signer} as the authorized validator signer`) + signers = await concurrentMap(10, await validators.getRegisteredValidatorsAddresses(), (a) => + accounts.getValidatorSigner(a) + ) } - // Determine if the signer is elected, and get their index in the validator set. const election = await this.kit.contracts.getElection() - const signers = await election.getCurrentValidatorSigners() - const signerIndex = signers.map((a) => eqAddress(a, signer)).indexOf(true) - if (signerIndex < 0) { - // Determine whether the signer will be elected at the next epoch to provide a helpful error. - const frontrunners = await election.electValidatorSigners() - if (frontrunners.some((a) => eqAddress(a, signer))) { - this.error( - `Signer ${signer} is not elected for this epoch, but would be elected if an election were to be held now. Please wait until the next epoch.` - ) - } else { - this.error( - `Signer ${signer} is not elected for this epoch, and would not be elected if an election were to be held now.` - ) - } - } - console.info('Signer has been elected for this epoch') - - if (((res.flags && res.flags.lookback) || 0) <= 0) { - return - } - - // Retrieve blocks to examine for the singers signature. - cli.action.start(`Retreiving the last ${res.flags.lookback} blocks`) + const electedSigners = await election.getCurrentValidatorSigners() + const frontRunnerSigners = await election.electValidatorSigners() const latest = await this.web3.eth.getBlock('latest') - const blocks = await concurrentMap(10, [...Array(res.flags.lookback).keys()].slice(1), (i) => + const blocks = await concurrentMap(10, [...Array(res.flags.lookback).keys()], (i) => this.web3.eth.getBlock(latest.number - i) ) - blocks.splice(0, 0, latest) + const validatorStatuses = await concurrentMap(10, signers, (s) => + this.getStatus(s, blocks, electedSigners, frontRunnerSigners) + ) cli.action.stop() - - const signedCount = blocks.filter((b) => - bitIsSet(parseBlockExtraData(b.extraData).parentAggregatedSeal.bitmap, signerIndex) - ).length - if (signedCount === 0) { - this.error(`Signer has not signed any of the last ${res.flags.lookback} blocks`) - } - console.info(`Signer has signed ${signedCount} of the last ${res.flags.lookback} blocks`) - - const proposedCount = blocks.filter((b) => b.miner === signer).length - console.info(`Signer has proposed ${proposedCount} of the last ${res.flags.lookback} blocks`) + cli.table(validatorStatuses, statusTable, { 'no-truncate': res.flags['no-truncate'] }) } } diff --git a/packages/cli/src/commands/validatorgroup/list.ts b/packages/cli/src/commands/validatorgroup/list.ts index a7e6482e449..3d4d14e83eb 100644 --- a/packages/cli/src/commands/validatorgroup/list.ts +++ b/packages/cli/src/commands/validatorgroup/list.ts @@ -12,18 +12,22 @@ export default class ValidatorGroupList extends BaseCommand { static examples = ['list'] async run() { - this.parse(ValidatorGroupList) + const res = this.parse(ValidatorGroupList) cli.action.start('Fetching Validator Groups') const validators = await this.kit.contracts.getValidators() const vgroups = await validators.getRegisteredValidatorGroups() cli.action.stop() - cli.table(vgroups, { - address: {}, - name: {}, - commission: { get: (r) => r.commission.toFixed() }, - members: { get: (r) => r.members.length }, - }) + cli.table( + vgroups, + { + address: {}, + name: {}, + commission: { get: (r) => r.commission.toFixed() }, + members: { get: (r) => r.members.length }, + }, + { 'no-truncate': !res.flags.truncate } + ) } } diff --git a/packages/cli/src/utils/command.ts b/packages/cli/src/utils/command.ts index f2a6f1476d9..12343fb0f64 100644 --- a/packages/cli/src/utils/command.ts +++ b/packages/cli/src/utils/command.ts @@ -1,4 +1,4 @@ -import { ensureHexLeader, stripHexLeader } from '@celo/utils/lib/address' +import { ensureLeading0x, trimLeading0x } from '@celo/utils/lib/address' import { BLS_POP_SIZE, BLS_PUBLIC_KEY_SIZE } from '@celo/utils/lib/bls' import { URL_REGEX } from '@celo/utils/lib/io' import { isE164NumberStrict } from '@celo/utils/lib/phoneNumbers' @@ -13,14 +13,14 @@ const parseBytes = (input: string, length: number, msg: string) => { // Check that the string is hex and and has byte length of `length`. const expectedLength = input.startsWith('0x') ? length * 2 + 2 : length * 2 if (Web3.utils.isHex(input) && input.length === expectedLength) { - return ensureHexLeader(input) + return ensureLeading0x(input) } else { throw new CLIError(msg) } } const parseEcdsaPublicKey: ParseFn = (input) => { - const stripped = stripHexLeader(input) + const stripped = trimLeading0x(input) // ECDSA public keys may be passed as 65 byte values. When this happens, we drop the first byte. if (stripped.length === 65 * 2) { return parseBytes(stripped.slice(2), 64, `${input} is not an ECDSA public key`) diff --git a/packages/cli/src/utils/governance.ts b/packages/cli/src/utils/governance.ts new file mode 100644 index 00000000000..a7d778b97dc --- /dev/null +++ b/packages/cli/src/utils/governance.ts @@ -0,0 +1,14 @@ +import { ContractKit } from '@celo/contractkit' +import { + ProposalBuilder, + ProposalTransactionJSON, +} from '@celo/contractkit/lib/governance/proposals' +import { readFileSync } from 'fs-extra' + +export const buildProposalFromJsonFile = async (kit: ContractKit, jsonFile: string) => { + const builder = new ProposalBuilder(kit) + const jsonString = readFileSync(jsonFile).toString() + const jsonTransactions: ProposalTransactionJSON[] = JSON.parse(jsonString) + jsonTransactions.forEach((tx) => builder.addJsonTx(tx)) + return builder.build() +} diff --git a/packages/contractkit/package.json b/packages/contractkit/package.json index 01bc146260d..94d949de749 100644 --- a/packages/contractkit/package.json +++ b/packages/contractkit/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@0x/subproviders": "^5.0.0", - "@celo/utils": "^0.1.0", + "@celo/utils": "0.1.5-dev", "@types/debug": "^4.1.5", "bignumber.js": "^7.2.0", "cross-fetch": "3.0.4", diff --git a/packages/contractkit/src/explorer/block-explorer.ts b/packages/contractkit/src/explorer/block-explorer.ts index 30315f78e62..f105b8023a2 100644 --- a/packages/contractkit/src/explorer/block-explorer.ts +++ b/packages/contractkit/src/explorer/block-explorer.ts @@ -7,7 +7,8 @@ import { ContractDetails, mapFromPairs, obtainKitContractDetails } from './base' export interface CallDetails { contract: string function: string - parameters: Record + paramMap: Record + argList: any[] } export interface ParsedTx { @@ -93,15 +94,15 @@ export class BlockExplorer { return null } - const parameters: Record = abi.decodeParameters( - matchedAbi.inputs!, - encodedParameters - ) + const parameters = abi.decodeParameters(matchedAbi.inputs!, encodedParameters) - // remove number & number keys + // remove numbers and number keys from parameters and build ordered list of arguments + const args = new Array(parameters.__length__) delete parameters.__length__ Object.keys(parameters).forEach((key) => { - if (Number.parseInt(key, 10) >= 0) { + const argIndex = parseInt(key, 10) + if (argIndex >= 0) { + args[argIndex] = parameters[key] delete parameters[key] } }) @@ -109,7 +110,8 @@ export class BlockExplorer { const callDetails: CallDetails = { contract: contractMapping.details.name, function: matchedAbi.name!, - parameters, + paramMap: parameters, + argList: args, } return { diff --git a/packages/contractkit/src/explorer/log-explorer.ts b/packages/contractkit/src/explorer/log-explorer.ts index a29b37418dd..b30a0cdabb3 100644 --- a/packages/contractkit/src/explorer/log-explorer.ts +++ b/packages/contractkit/src/explorer/log-explorer.ts @@ -1,26 +1,9 @@ import { Address } from '@celo/utils/lib/address' import abi, { ABIDefinition } from 'web3-eth-abi' -import { Block, Transaction } from 'web3/eth/types' import { EventLog, Log, TransactionReceipt } from 'web3/types' import { ContractKit } from '../kit' import { ContractDetails, mapFromPairs, obtainKitContractDetails } from './base' -export interface CallDetails { - contract: string - function: string - parameters: Record -} - -export interface ParsedTx { - callDetails: CallDetails - tx: Transaction -} - -export interface ParsedBlock { - block: Block - parsedTx: ParsedTx[] -} - interface ContractMapping { details: ContractDetails logMapping: Map diff --git a/packages/contractkit/src/governance/index.ts b/packages/contractkit/src/governance/index.ts new file mode 100644 index 00000000000..7f8c39ce00f --- /dev/null +++ b/packages/contractkit/src/governance/index.ts @@ -0,0 +1 @@ +export * from './proposals' diff --git a/packages/contractkit/src/governance/proposals.ts b/packages/contractkit/src/governance/proposals.ts new file mode 100644 index 00000000000..b4eab5368ba --- /dev/null +++ b/packages/contractkit/src/governance/proposals.ts @@ -0,0 +1,97 @@ +import { concurrentMap } from '@celo/utils/lib/async' +import { keccak256 } from 'ethereumjs-util' +import Contract from 'web3/eth/contract' +import { Transaction, TransactionObject } from 'web3/eth/types' +import { CeloContract } from '../base' +import { obtainKitContractDetails } from '../explorer/base' +import { BlockExplorer } from '../explorer/block-explorer' +import { ABI as GovernanceABI } from '../generated/Governance' +import { ContractKit } from '../kit' +import { CeloTransactionObject, valueToString } from '../wrappers/BaseWrapper' +import { GovernanceWrapper, Proposal, ProposalTransaction } from '../wrappers/Governance' +import { setImplementationOnProxy } from './proxy' + +export const PROPOSE_PARAM_ABI_TYPES = (GovernanceABI.find( + (abiEntry) => abiEntry.name! === 'propose' +)!.inputs! as Array<{ type: string }>).map((abiInput) => abiInput.type) + +export const proposalToEncodedParams = (kit: ContractKit, proposal: Proposal) => + kit.web3.eth.abi.encodeParameters(PROPOSE_PARAM_ABI_TYPES, GovernanceWrapper.toParams(proposal)) + +export const proposalToHash = (kit: ContractKit, proposal: Proposal) => + keccak256(proposalToEncodedParams(kit, proposal)) as Buffer + +export interface ProposalTransactionJSON { + contract: CeloContract + function: string + args: any[] + value: string +} + +export const proposalToJSON = async (kit: ContractKit, proposal: Proposal) => { + const contractDetails = await obtainKitContractDetails(kit) + const blockExplorer = new BlockExplorer(kit, contractDetails) + + return concurrentMap(4, proposal, async (tx) => { + const parsedTx = blockExplorer.tryParseTx(tx as Transaction) + if (parsedTx == null) { + throw new Error(`Unable to parse ${tx} with block explorer`) + } + return { + contract: parsedTx.callDetails.contract as CeloContract, + function: parsedTx.callDetails.function, + args: parsedTx.callDetails.argList, + value: parsedTx.tx.value, + } + }) +} + +type ProposalTxParams = Pick +export class ProposalBuilder { + constructor( + private readonly kit: ContractKit, + private readonly builders: Array<() => Promise> = [] + ) {} + + build = async () => concurrentMap(4, this.builders, (builder) => builder()) + + fromWeb3tx = (tx: TransactionObject, params: ProposalTxParams): ProposalTransaction => ({ + value: params.value, + to: params.to, + input: tx.encodeABI(), + }) + + addProxyRepointingTx = (proxyAddress: string, newImplementationAddress: string) => { + this.addWeb3Tx(setImplementationOnProxy(newImplementationAddress), { + to: proxyAddress, + value: '0', + }) + } + + addWeb3Tx = (tx: TransactionObject, params: ProposalTxParams) => + this.builders.push(async () => this.fromWeb3tx(tx, params)) + + addTx(tx: CeloTransactionObject, params: Partial = {}) { + const to = tx.defaultParams && tx.defaultParams.to ? tx.defaultParams.to : params.to + const value = tx.defaultParams && tx.defaultParams.value ? tx.defaultParams.value : params.value + if (!to || !value) { + throw new Error("Transaction parameters 'to' and/or 'value' not provided") + } + this.addWeb3Tx(tx.txo, { to, value: valueToString(value) }) + } + + addJsonTx = (tx: ProposalTransactionJSON) => + this.builders.push(async () => { + const contract = await this.kit._web3Contracts.getContract(tx.contract) + const methodName = tx.function + const method = (contract.methods as Contract['methods'])[methodName] + if (!method) { + throw new Error(`Method ${methodName} not found on ${tx.contract}`) + } + const txo = method(...tx.args) + if (!txo) { + throw new Error(`Arguments ${tx.args} did not match ${methodName} signature`) + } + return this.fromWeb3tx(txo, { to: contract._address, value: tx.value }) + }) +} diff --git a/packages/contractkit/src/governance/proxy.ts b/packages/contractkit/src/governance/proxy.ts new file mode 100644 index 00000000000..2c8c7c72438 --- /dev/null +++ b/packages/contractkit/src/governance/proxy.ts @@ -0,0 +1,46 @@ +import Web3 from 'web3' + +const PROXY_ABI = [ + { + constant: true, + inputs: [], + name: '_getImplementation', + outputs: [ + { + name: 'implementation', + type: 'address', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: false, + inputs: [ + { + name: 'implementation', + type: 'address', + }, + ], + name: '_setImplementation', + outputs: [], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, +] + +export const getImplementationOfProxy = async ( + web3: Web3, + proxyContractAddress: string +): Promise => { + const proxyWeb3Contract = new web3.eth.Contract(PROXY_ABI, proxyContractAddress) + return proxyWeb3Contract.methods._getImplementation().call() +} + +export const setImplementationOnProxy = (address: string) => { + const web3 = new Web3() + const proxyWeb3Contract = new web3.eth.Contract(PROXY_ABI) + return proxyWeb3Contract.methods._setImplementation(address) +} diff --git a/packages/contractkit/src/kit.ts b/packages/contractkit/src/kit.ts index 630a4f8a118..c9f7bcdb45c 100644 --- a/packages/contractkit/src/kit.ts +++ b/packages/contractkit/src/kit.ts @@ -239,4 +239,11 @@ export class ContractKit { ...tx, } } + + async getLastBlockNumberForEpoch(epochNumber: number): Promise { + const validators = await this.contracts.getValidators() + const epochSize = await validators.getEpochSize() + // Reverses protocol/contracts getEpochNumber() + return (epochNumber + 1) * epochSize.toNumber() + } } diff --git a/packages/contractkit/src/wrappers/Accounts.ts b/packages/contractkit/src/wrappers/Accounts.ts index d7e64a113fa..76bc6e00fa5 100644 --- a/packages/contractkit/src/wrappers/Accounts.ts +++ b/packages/contractkit/src/wrappers/Accounts.ts @@ -85,12 +85,14 @@ export class AccountsWrapper extends BaseWrapper { /** * Returns the account associated with `signer`. * @param signer The address of the account or previously authorized signer. + * @param blockNumber Height of result, defaults to tip. * @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 - ) + signerToAccount(signer: Address, blockNumber?: number): Promise
{ + // @ts-ignore: Expected 0-1 arguments, but got 2 + return this.contract.methods.signerToAccount(signer).call({}, blockNumber) + } /** * Check if an account already exists. @@ -237,8 +239,12 @@ export class AccountsWrapper extends BaseWrapper { /** * Returns the set name for the account * @param account Account + * @param blockNumber Height of result, defaults to tip. */ - getName = proxyCall(this.contract.methods.getName) + async getName(account: Address, blockNumber?: number): Promise { + // @ts-ignore: Expected 0-1 arguments, but got 2 + return this.contract.methods.getName(account).call({}, blockNumber) + } /** * Returns the set data encryption key for the account diff --git a/packages/contractkit/src/wrappers/Attestations.ts b/packages/contractkit/src/wrappers/Attestations.ts index 8ec0fe35822..f75087adcec 100644 --- a/packages/contractkit/src/wrappers/Attestations.ts +++ b/packages/contractkit/src/wrappers/Attestations.ts @@ -10,11 +10,12 @@ import { ClaimTypes, IdentityMetadataWrapper } from '../identity' import { BaseWrapper, proxyCall, - toBigNumber, - toNumber, toTransactionObject, tupleParser, + valueToBigNumber, + valueToInt, } from './BaseWrapper' + const parseSignature = SignatureUtils.parseSignature export interface AttestationStat { @@ -70,11 +71,11 @@ interface GetCompletableAttestationsResponse { } function parseGetCompletableAttestations(response: GetCompletableAttestationsResponse) { const metadataURLs = parseSolidityStringArray( - response[2].map(toNumber), + response[2].map(valueToInt), (response[3] as unknown) as string ) - return zip3(response[0].map(toNumber), response[1], metadataURLs).map( + return zip3(response[0].map(valueToInt), response[1], metadataURLs).map( ([blockNumber, issuer, metadataURL]) => ({ blockNumber, issuer, metadataURL }) ) } @@ -87,7 +88,7 @@ export class AttestationsWrapper extends BaseWrapper { attestationExpiryBlocks = proxyCall( this.contract.methods.attestationExpiryBlocks, undefined, - toNumber + valueToInt ) /** @@ -98,13 +99,13 @@ export class AttestationsWrapper extends BaseWrapper { attestationRequestFees = proxyCall( this.contract.methods.attestationRequestFees, undefined, - toBigNumber + valueToBigNumber ) selectIssuersWaitBlocks = proxyCall( this.contract.methods.selectIssuersWaitBlocks, undefined, - toNumber + valueToInt ) /** @@ -116,8 +117,8 @@ export class AttestationsWrapper extends BaseWrapper { this.contract.methods.getUnselectedRequest, tupleParser(PhoneNumberUtils.getPhoneHash, (x: string) => x), (res) => ({ - blockNumber: toNumber(res[0]), - attestationsRequested: toNumber(res[1]), + blockNumber: valueToInt(res[0]), + attestationsRequested: valueToInt(res[1]), attestationRequestFeeToken: res[2], }) ) @@ -182,7 +183,7 @@ export class AttestationsWrapper extends BaseWrapper { ) => Promise = proxyCall( this.contract.methods.getAttestationStats, tupleParser(PhoneNumberUtils.getPhoneHash, stringIdentity), - (stat) => ({ completed: toNumber(stat[0]), total: toNumber(stat[1]) }) + (stat) => ({ completed: valueToInt(stat[0]), total: valueToInt(stat[1]) }) ) /** @@ -356,11 +357,10 @@ export class AttestationsWrapper extends BaseWrapper { // Unfortunately can't be destructured const stats = await this.contract.methods.batchGetAttestationStats(phoneNumberHashes).call() - const toNum = (n: string) => new BigNumber(n).toNumber() - const matches = stats[0].map(toNum) + const matches = stats[0].map(valueToInt) const addresses = stats[1] - const completed = stats[2].map(toNum) - const total = stats[3].map(toNum) + const completed = stats[2].map(valueToInt) + const total = stats[3].map(valueToInt) // Map of phone hash -> (Map of address -> AttestationStat) const result: Record> = {} diff --git a/packages/contractkit/src/wrappers/BaseWrapper.ts b/packages/contractkit/src/wrappers/BaseWrapper.ts index 852171391d3..9a52f2966ca 100644 --- a/packages/contractkit/src/wrappers/BaseWrapper.ts +++ b/packages/contractkit/src/wrappers/BaseWrapper.ts @@ -1,16 +1,15 @@ +import { ensureLeading0x, hexToBuffer } from '@celo/utils/lib/address' import { zip } from '@celo/utils/lib/collections' import BigNumber from 'bignumber.js' import Contract from 'web3/eth/contract' -import { TransactionObject, Tx } from 'web3/eth/types' -import { TransactionReceipt } from 'web3/types' +import { BlockType, TransactionObject, Tx } from 'web3/eth/types' +import { EventLog, TransactionReceipt } from 'web3/types' import { ContractKit } from '../kit' import { TransactionResult } from '../utils/tx-result' /** Represents web3 native contract Method */ type Method = (...args: I) => TransactionObject -export type NumberLike = string | number | BigNumber - /** Base ContractWrapper */ export abstract class BaseWrapper { constructor(protected readonly kit: ContractKit, protected readonly contract: T) {} @@ -20,32 +19,51 @@ export abstract class BaseWrapper { // TODO fix typings return (this.contract as any)._address } -} -/** Parse string -> BigNumber */ -export function toBigNumber(input: string) { - return new BigNumber(input) + /** Contract getPastEvents */ + protected getPastEvents( + event: string, + options?: { + filter?: object + fromBlock?: BlockType + toBlock?: BlockType + topics?: string[] + } + ): Promise { + return this.contract.getPastEvents(event, options) + } } -/** Parse string -> int */ -export function toNumber(input: string) { - return parseInt(input, 10) -} +export const valueToBigNumber = (input: BigNumber.Value) => new BigNumber(input) -export function parseNumber(input: NumberLike) { - return new BigNumber(input).toFixed() -} +export const valueToString = (input: BigNumber.Value) => valueToBigNumber(input).toFixed() -export function parseBytes(input: string): Array { - return input as any -} +export const valueToInt = (input: BigNumber.Value) => + valueToBigNumber(input) + .integerValue() + .toNumber() + +export const valueToFrac = (numerator: BigNumber.Value, denominator: BigNumber.Value) => + valueToBigNumber(numerator).div(valueToBigNumber(denominator)) + +export const stringToBuffer = hexToBuffer + +export const bufferToString = (buf: Buffer) => ensureLeading0x(buf.toString('hex')) + +type SolBytes = Array +const toBytes = (input: any): SolBytes => input +const fromBytes = (input: SolBytes): any => input as any + +export const stringToBytes = (input: string) => toBytes(ensureLeading0x(input)) + +export const bufferToBytes = (input: Buffer) => stringToBytes(bufferToString(input)) + +export const bytesToString = (input: SolBytes): string => fromBytes(input) type Parser = (input: A) => B /** Identity Parser */ -export function identity(a: A) { - return a -} +export const identity = (a: A) => a /** * Tuple parser diff --git a/packages/contractkit/src/wrappers/Election.ts b/packages/contractkit/src/wrappers/Election.ts index 3db0ff2ec0a..f4f080d3f7d 100644 --- a/packages/contractkit/src/wrappers/Election.ts +++ b/packages/contractkit/src/wrappers/Election.ts @@ -1,19 +1,22 @@ import { eqAddress } from '@celo/utils/lib/address' -import { concurrentMap } from '@celo/utils/lib/async' +import { concurrentMap, concurrentValuesMap } from '@celo/utils/lib/async' import { zip } from '@celo/utils/lib/collections' import BigNumber from 'bignumber.js' +import { EventLog } from 'web3/types' import { Address, NULL_ADDRESS } from '../base' import { Election } from '../generated/types/Election' +import { Validator, ValidatorGroup } from './Validators' + import { BaseWrapper, CeloTransactionObject, identity, proxyCall, proxySend, - toBigNumber, - toNumber, toTransactionObject, tupleParser, + valueToBigNumber, + valueToInt, } from './BaseWrapper' export interface ValidatorGroupVote { @@ -29,12 +32,25 @@ export interface Voter { votes: GroupVote[] } +export interface VoterReward { + address: Address + addressPayment: BigNumber + group: ValidatorGroup + epochNumber: number +} + export interface GroupVote { group: Address pending: BigNumber active: BigNumber } +export interface GroupVoterReward { + group: ValidatorGroup + groupVoterPayment: BigNumber + epochNumber: number +} + export interface ElectableValidators { min: BigNumber max: BigNumber @@ -56,7 +72,7 @@ export class ElectionWrapper extends BaseWrapper { */ async electableValidators(): Promise { const { min, max } = await this.contract.methods.electableValidators().call() - return { min: toBigNumber(min), max: toBigNumber(max) } + return { min: valueToBigNumber(min), max: valueToBigNumber(max) } } /** * Returns the current election threshold. @@ -65,7 +81,7 @@ export class ElectionWrapper extends BaseWrapper { electabilityThreshold = proxyCall( this.contract.methods.getElectabilityThreshold, undefined, - toBigNumber + valueToBigNumber ) validatorSignerAddressFromCurrentSet: (index: number) => Promise
= proxyCall( this.contract.methods.validatorSignerAddressFromCurrentSet, @@ -75,14 +91,18 @@ export class ElectionWrapper extends BaseWrapper { numberValidatorsInCurrentSet = proxyCall( this.contract.methods.numberValidatorsInCurrentSet, undefined, - toNumber + valueToInt ) /** * Returns get current validator signers using the precompiles. * @return List of current validator signers. */ - getCurrentValidatorSigners = proxyCall(this.contract.methods.getCurrentValidatorSigners) + async getCurrentValidatorSigners(blockNumber?: number): Promise { + // @ts-ignore: Expected 0-1 arguments, but got 2 + return this.contract.methods.getCurrentValidatorSigners().call({}, blockNumber) + } + /** * Returns a list of elected validators with seats allocated to groups via the D'Hondt method. * @return The list of elected validators. @@ -91,16 +111,26 @@ export class ElectionWrapper extends BaseWrapper { electValidatorSigners = proxyCall(this.contract.methods.electValidatorSigners) /** - * Returns the total votes for `group` made by `account`. + * Returns the total votes for `group`. * @param group The address of the validator group. - * @param account The address of the voting account. - * @return The total votes for `group` made by `account`. + * @return The total votes for `group`. */ - getTotalVotesForGroup = proxyCall( - this.contract.methods.getTotalVotesForGroup, - undefined, - toBigNumber - ) + async getTotalVotesForGroup(group: Address, blockNumber?: number): Promise { + // @ts-ignore: Expected 0-1 arguments, but got 2 + const votes = await this.contract.methods.getTotalVotesForGroup(group).call({}, blockNumber) + return valueToBigNumber(votes) + } + + /** + * Returns the active votes for `group`. + * @param group The address of the validator group. + * @return The active votes for `group`. + */ + async getActiveVotesForGroup(group: Address, blockNumber?: number): Promise { + // @ts-ignore: Expected 0-1 arguments, but got 2 + const votes = await this.contract.methods.getActiveVotesForGroup(group).call({}, blockNumber) + return valueToBigNumber(votes) + } /** * Returns the groups that `account` has voted for. @@ -111,23 +141,37 @@ export class ElectionWrapper extends BaseWrapper { this.contract.methods.getGroupsVotedForByAccount ) - async getVotesForGroupByAccount(account: Address, group: Address): Promise { + async getVotesForGroupByAccount( + account: Address, + group: Address, + blockNumber?: number + ): Promise { const pending = await this.contract.methods .getPendingVotesForGroupByAccount(group, account) - .call() + // @ts-ignore: Expected 0-1 arguments, but got 2 + .call({}, blockNumber) + const active = await this.contract.methods .getActiveVotesForGroupByAccount(group, account) - .call() + // @ts-ignore: Expected 0-1 arguments, but got 2 + .call({}, blockNumber) + return { group, - pending: toBigNumber(pending), - active: toBigNumber(active), + pending: valueToBigNumber(pending), + active: valueToBigNumber(active), } } - async getVoter(account: Address): Promise { - const groups = await this.contract.methods.getGroupsVotedForByAccount(account).call() - const votes = await Promise.all(groups.map((g) => this.getVotesForGroupByAccount(account, g))) + async getVoter(account: Address, blockNumber?: number): Promise { + const groups: Address[] = await this.contract.methods + .getGroupsVotedForByAccount(account) + // @ts-ignore: Expected 0-1 arguments, but got 2 + .call({}, blockNumber) + + const votes = await concurrentMap(10, groups, (g) => + this.getVotesForGroupByAccount(account, g, blockNumber) + ) return { address: account, votes } } @@ -140,7 +184,7 @@ export class ElectionWrapper extends BaseWrapper { const groups: string[] = await this.contract.methods.getGroupsVotedForByAccount(account).call() const isPending = await Promise.all( groups.map(async (g) => - toBigNumber( + valueToBigNumber( await this.contract.methods.getPendingVotesForGroupByAccount(g, account).call() ).isGreaterThan(0) ) @@ -168,7 +212,7 @@ export class ElectionWrapper extends BaseWrapper { return { electableValidators: res[0], electabilityThreshold: res[1], - maxNumGroupsVotedFor: toBigNumber(res[2]), + maxNumGroupsVotedFor: valueToBigNumber(res[2]), } } @@ -181,8 +225,8 @@ export class ElectionWrapper extends BaseWrapper { return { address, name, - votes: toBigNumber(votes), - capacity: toBigNumber(numVotesReceivable).minus(votes), + votes: valueToBigNumber(votes), + capacity: valueToBigNumber(numVotesReceivable).minus(votes), eligible, } } @@ -330,4 +374,75 @@ export class ElectionWrapper extends BaseWrapper { greater: newIdx === currentVotes.length - 1 ? NULL_ADDRESS : currentVotes[newIdx + 1].address, } } + + /** + * Retrieves the set of validatorsparticipating in BFT at epochNumber. + * @param epochNumber The epoch to retrieve the elected validator set at. + */ + async getElectedValidators(epochNumber: number): Promise { + const blockNumber = await this.kit.getLastBlockNumberForEpoch(epochNumber) + const signers = await this.getCurrentValidatorSigners(blockNumber) + const validators = await this.kit.contracts.getValidators() + return concurrentMap(10, signers, (addr) => + validators.getValidatorFromSigner(addr, blockNumber) + ) + } + + /** + * Retrieves GroupVoterRewards at epochNumber. + * @param epochNumber The epoch to retrieve GroupVoterRewards at. + */ + async getGroupVoterRewards(epochNumber: number): Promise { + const blockNumber = await this.kit.getLastBlockNumberForEpoch(epochNumber) + const events = await this.getPastEvents('EpochRewardsDistributedToVoters', { + fromBlock: blockNumber, + toBlock: blockNumber, + }) + const validators = await this.kit.contracts.getValidators() + const validatorGroup: ValidatorGroup[] = await concurrentMap(10, events, (e: EventLog) => + validators.getValidatorGroup(e.returnValues.group, true, blockNumber) + ) + return events.map( + (e: EventLog, index: number): GroupVoterReward => ({ + epochNumber, + group: validatorGroup[index], + groupVoterPayment: e.returnValues.value, + }) + ) + } + + /** + * Retrieves VoterRewards for address at epochNumber. + * @param address The address to retrieve VoterRewards for. + * @param epochNumber The epoch to retrieve VoterRewards at. + */ + async getVoterRewards(address: Address, epochNumber: number): Promise { + const blockNumber = await this.kit.getLastBlockNumberForEpoch(epochNumber) + const voter = await this.getVoter(address, blockNumber) + const activeVoterVotes: Record = {} + for (const vote of voter.votes) { + const group: string = vote.group.toLowerCase() + activeVoterVotes[group] = vote.active + } + const activeGroupVotes: Record = await concurrentValuesMap( + 10, + activeVoterVotes, + (_, group: string) => this.getTotalVotesForGroup(group, blockNumber) + ) + + const groupVoterRewards = await this.getGroupVoterRewards(epochNumber) + const voterRewards = groupVoterRewards.filter( + (e: GroupVoterReward) => e.group.address.toLowerCase() in activeVoterVotes + ) + return voterRewards.map( + (e: GroupVoterReward): VoterReward => ({ + address, + addressPayment: e.groupVoterPayment.times( + activeVoterVotes[e.group.address].dividedBy(activeGroupVotes[e.group.address]) + ), + group: e.group, + epochNumber: e.epochNumber, + }) + ) + } } diff --git a/packages/contractkit/src/wrappers/Exchange.ts b/packages/contractkit/src/wrappers/Exchange.ts index 1df5ce9acfa..583c5decbf1 100644 --- a/packages/contractkit/src/wrappers/Exchange.ts +++ b/packages/contractkit/src/wrappers/Exchange.ts @@ -4,12 +4,12 @@ import { BaseWrapper, CeloTransactionObject, identity, - NumberLike, - parseNumber, proxyCall, proxySend, - toBigNumber, tupleParser, + valueToBigNumber, + valueToFrac, + valueToString, } from './BaseWrapper' export interface ExchangeConfig { @@ -28,25 +28,25 @@ export class ExchangeWrapper extends BaseWrapper { * Query spread parameter * @returns Current spread charged on exchanges */ - spread = proxyCall(this.contract.methods.spread, undefined, toBigNumber) + spread = proxyCall(this.contract.methods.spread, undefined, valueToBigNumber) /** * Query reserve fraction parameter * @returns Current fraction to commit to the gold bucket */ - reserveFraction = proxyCall(this.contract.methods.reserveFraction, undefined, toBigNumber) + reserveFraction = proxyCall(this.contract.methods.reserveFraction, undefined, valueToBigNumber) /** * Query update frequency parameter * @returns The time period that needs to elapse between bucket * updates */ - updateFrequency = proxyCall(this.contract.methods.updateFrequency, undefined, toBigNumber) + updateFrequency = proxyCall(this.contract.methods.updateFrequency, undefined, valueToBigNumber) /** * Query minimum reports parameter * @returns The minimum number of fresh reports that need to be * present in the oracle to update buckets * commit to the gold bucket */ - minimumReports = proxyCall(this.contract.methods.minimumReports, undefined, toBigNumber) + minimumReports = proxyCall(this.contract.methods.minimumReports, undefined, valueToBigNumber) /** * @dev Returns the amount of buyToken a user would get for sellAmount of sellToken @@ -54,10 +54,13 @@ export class ExchangeWrapper extends BaseWrapper { * @param sellGold `true` if gold is the sell token * @return The corresponding buyToken amount. */ - getBuyTokenAmount: (sellAmount: NumberLike, sellGold: boolean) => Promise = proxyCall( + getBuyTokenAmount: ( + sellAmount: BigNumber.Value, + sellGold: boolean + ) => Promise = proxyCall( this.contract.methods.getBuyTokenAmount, - tupleParser(parseNumber, identity), - toBigNumber + tupleParser(valueToString, identity), + valueToBigNumber ) /** @@ -67,10 +70,13 @@ export class ExchangeWrapper extends BaseWrapper { * @param sellGold `true` if gold is the sell token * @return The corresponding sellToken amount. */ - getSellTokenAmount: (buyAmount: NumberLike, sellGold: boolean) => Promise = proxyCall( + getSellTokenAmount: ( + buyAmount: BigNumber.Value, + sellGold: boolean + ) => Promise = proxyCall( this.contract.methods.getSellTokenAmount, - tupleParser(parseNumber, identity), - toBigNumber + tupleParser(valueToString, identity), + valueToBigNumber ) /** @@ -83,7 +89,7 @@ export class ExchangeWrapper extends BaseWrapper { this.contract.methods.getBuyAndSellBuckets, undefined, (callRes: { 0: string; 1: string }) => - [toBigNumber(callRes[0]), toBigNumber(callRes[1])] as [BigNumber, BigNumber] + [valueToBigNumber(callRes[0]), valueToBigNumber(callRes[1])] as [BigNumber, BigNumber] ) /** @@ -96,13 +102,13 @@ export class ExchangeWrapper extends BaseWrapper { * @return The amount of buyToken that was transfered */ exchange: ( - sellAmount: NumberLike, - minBuyAmount: NumberLike, + sellAmount: BigNumber.Value, + minBuyAmount: BigNumber.Value, sellGold: boolean ) => CeloTransactionObject = proxySend( this.kit, this.contract.methods.exchange, - tupleParser(parseNumber, parseNumber, identity) + tupleParser(valueToString, valueToString, identity) ) /** @@ -112,7 +118,7 @@ export class ExchangeWrapper extends BaseWrapper { * @param minUsdAmount The minimum amount of cUsd the user has to receive for this * transaction to succeed */ - sellGold = (amount: NumberLike, minUSDAmount: NumberLike) => + sellGold = (amount: BigNumber.Value, minUSDAmount: BigNumber.Value) => this.exchange(amount, minUSDAmount, true) /** @@ -122,7 +128,7 @@ export class ExchangeWrapper extends BaseWrapper { * @param minGoldAmount The minimum amount of cGold the user has to receive for this * transaction to succeed */ - sellDollar = (amount: NumberLike, minGoldAmount: NumberLike) => + sellDollar = (amount: BigNumber.Value, minGoldAmount: BigNumber.Value) => this.exchange(amount, minGoldAmount, false) /** @@ -130,14 +136,14 @@ export class ExchangeWrapper extends BaseWrapper { * @param sellAmount The amount of cUsd the user is selling to the exchange * @return The corresponding cGold amount. */ - quoteUsdSell = (sellAmount: NumberLike) => this.getBuyTokenAmount(sellAmount, false) + quoteUsdSell = (sellAmount: BigNumber.Value) => this.getBuyTokenAmount(sellAmount, false) /** * Returns the amount of cUsd a user would get for sellAmount of cGold * @param sellAmount The amount of cGold the user is selling to the exchange * @return The corresponding cUsd amount. */ - quoteGoldSell = (sellAmount: NumberLike) => this.getBuyTokenAmount(sellAmount, true) + quoteGoldSell = (sellAmount: BigNumber.Value) => this.getBuyTokenAmount(sellAmount, true) /** * Returns the amount of cGold a user would need to exchange to receive buyAmount of @@ -145,7 +151,7 @@ export class ExchangeWrapper extends BaseWrapper { * @param buyAmount The amount of cUsd the user would like to purchase. * @return The corresponding cGold amount. */ - quoteUsdBuy = (buyAmount: NumberLike) => this.getSellTokenAmount(buyAmount, false) + quoteUsdBuy = (buyAmount: BigNumber.Value) => this.getSellTokenAmount(buyAmount, false) /** * Returns the amount of cUsd a user would need to exchange to receive buyAmount of @@ -153,7 +159,7 @@ export class ExchangeWrapper extends BaseWrapper { * @param buyAmount The amount of cGold the user would like to purchase. * @return The corresponding cUsd amount. */ - quoteGoldBuy = (buyAmount: NumberLike) => this.getSellTokenAmount(buyAmount, true) + quoteGoldBuy = (buyAmount: BigNumber.Value) => this.getSellTokenAmount(buyAmount, true) /** * @dev Returns the current configuration of the exchange contract @@ -179,9 +185,9 @@ export class ExchangeWrapper extends BaseWrapper { * @param sellGold `true` if gold is the sell token * @return The exchange rate (number of sellTokens received for one buyToken). */ - async getExchangeRate(buyAmount: NumberLike, sellGold: boolean): Promise { + async getExchangeRate(buyAmount: BigNumber.Value, sellGold: boolean): Promise { const takerAmount = await this.getBuyTokenAmount(buyAmount, sellGold) - return new BigNumber(buyAmount).dividedBy(takerAmount) // Number of sellTokens received for one buyToken + return valueToFrac(buyAmount, takerAmount) // Number of sellTokens received for one buyToken } /** @@ -189,12 +195,12 @@ export class ExchangeWrapper extends BaseWrapper { * @param buyAmount The amount of cUsd in wei to estimate the exchange rate at * @return The exchange rate (number of cGold received for one cUsd) */ - getUsdExchangeRate = (buyAmount: NumberLike) => this.getExchangeRate(buyAmount, false) + getUsdExchangeRate = (buyAmount: BigNumber.Value) => this.getExchangeRate(buyAmount, false) /** * Returns the exchange rate for cGold estimated at the buyAmount * @param buyAmount The amount of cGold in wei to estimate the exchange rate at * @return The exchange rate (number of cUsd received for one cGold) */ - getGoldExchangeRate = (buyAmount: NumberLike) => this.getExchangeRate(buyAmount, true) + getGoldExchangeRate = (buyAmount: BigNumber.Value) => this.getExchangeRate(buyAmount, true) } diff --git a/packages/contractkit/src/wrappers/GasPriceMinimum.ts b/packages/contractkit/src/wrappers/GasPriceMinimum.ts index 8f39be4b614..905f3755893 100644 --- a/packages/contractkit/src/wrappers/GasPriceMinimum.ts +++ b/packages/contractkit/src/wrappers/GasPriceMinimum.ts @@ -1,6 +1,6 @@ import BigNumber from 'bignumber.js' import { GasPriceMinimum } from '../generated/types/GasPriceMinimum' -import { BaseWrapper, proxyCall, toBigNumber } from './BaseWrapper' +import { BaseWrapper, proxyCall, valueToBigNumber } from './BaseWrapper' export interface GasPriceMinimumConfig { gasPriceMinimum: BigNumber @@ -16,24 +16,28 @@ export class GasPriceMinimumWrapper extends BaseWrapper { * Query current gas price minimum in gGLD. * @returns current gas price minimum in cGLD */ - gasPriceMinimum = proxyCall(this.contract.methods.gasPriceMinimum, undefined, toBigNumber) + gasPriceMinimum = proxyCall(this.contract.methods.gasPriceMinimum, undefined, valueToBigNumber) /** * Query current gas price minimum. * @returns current gas price minimum in the requested currency */ - getGasPriceMinimum = proxyCall(this.contract.methods.getGasPriceMinimum, undefined, toBigNumber) + getGasPriceMinimum = proxyCall( + this.contract.methods.getGasPriceMinimum, + undefined, + valueToBigNumber + ) /** * Query target density parameter. * @returns the current block density targeted by the gas price minimum algorithm. */ - targetDensity = proxyCall(this.contract.methods.targetDensity, undefined, toBigNumber) + targetDensity = proxyCall(this.contract.methods.targetDensity, undefined, valueToBigNumber) /** * Query adjustment speed parameter * @returns multiplier that impacts how quickly gas price minimum is adjusted. */ - adjustmentSpeed = proxyCall(this.contract.methods.adjustmentSpeed, undefined, toBigNumber) + adjustmentSpeed = proxyCall(this.contract.methods.adjustmentSpeed, undefined, valueToBigNumber) /** * Returns current configuration parameters. */ diff --git a/packages/contractkit/src/wrappers/GoldTokenWrapper.ts b/packages/contractkit/src/wrappers/GoldTokenWrapper.ts index cd0de2cd150..c55a0f95c2d 100644 --- a/packages/contractkit/src/wrappers/GoldTokenWrapper.ts +++ b/packages/contractkit/src/wrappers/GoldTokenWrapper.ts @@ -2,9 +2,10 @@ // after the move to node 10. This allows types to be inferred without // referencing '@celo/utils/node_modules/bignumber.js' import _ from 'bignumber.js' + import { Address } from '../base' import { GoldToken } from '../generated/types/GoldToken' -import { BaseWrapper, proxyCall, proxySend, toBigNumber, toNumber } from './BaseWrapper' +import { BaseWrapper, proxyCall, proxySend, valueToBigNumber, valueToInt } from './BaseWrapper' /** * ERC-20 contract for Celo native currency. @@ -16,7 +17,7 @@ export class GoldTokenWrapper extends BaseWrapper { * @param to Address of account to whom the allowance was given. * @returns Amount of allowance. */ - allowance = proxyCall(this.contract.methods.allowance, undefined, toBigNumber) + allowance = proxyCall(this.contract.methods.allowance, undefined, valueToBigNumber) /** * Returns the name of the token. @@ -33,13 +34,13 @@ export class GoldTokenWrapper extends BaseWrapper { * Returns the number of decimals used in the token. * @returns Number of decimals. */ - decimals = proxyCall(this.contract.methods.decimals, undefined, toNumber) + decimals = proxyCall(this.contract.methods.decimals, undefined, valueToInt) /** * Returns the total supply of the token, that is, the amount of tokens currently minted. * @returns Total supply. */ - totalSupply = proxyCall(this.contract.methods.totalSupply, undefined, toBigNumber) + totalSupply = proxyCall(this.contract.methods.totalSupply, undefined, valueToBigNumber) /** * Approve a user to transfer Celo Gold on behalf of another user. @@ -94,5 +95,5 @@ export class GoldTokenWrapper extends BaseWrapper { * @param owner The address to query the balance of. * @return The balance of the specified address. */ - balanceOf = (account: Address) => this.kit.web3.eth.getBalance(account).then(toBigNumber) + balanceOf = (account: Address) => this.kit.web3.eth.getBalance(account).then(valueToBigNumber) } diff --git a/packages/contractkit/src/wrappers/Governance.test.ts b/packages/contractkit/src/wrappers/Governance.test.ts new file mode 100644 index 00000000000..df2f8deb992 --- /dev/null +++ b/packages/contractkit/src/wrappers/Governance.test.ts @@ -0,0 +1,263 @@ +import { NetworkConfig, testWithGanache, timeTravel } from '@celo/dev-utils/lib/ganache-test' +import { Address } from '@celo/utils/lib/address' +import { concurrentMap } from '@celo/utils/lib/async' +import BigNumber from 'bignumber.js' +import Web3 from 'web3' +import { CeloContract } from '..' +import { Registry } from '../generated/types/Registry' +import { ProposalBuilder } from '../governance' +import { newKitFromWeb3 } from '../kit' +import { AccountsWrapper } from './Accounts' +import { GovernanceWrapper, Proposal, VoteValue } from './Governance' +import { LockedGoldWrapper } from './LockedGold' + +const expConfig = NetworkConfig.governance + +testWithGanache('Governance Wrapper', (web3: Web3) => { + const ONE_SEC = 1000 + const kit = newKitFromWeb3(web3) + const minDeposit = web3.utils.toWei(expConfig.minDeposit.toString(), 'ether') + const ONE_USD = web3.utils.toWei('1', 'ether') + + let accounts: Address[] = [] + let governance: GovernanceWrapper + let lockedGold: LockedGoldWrapper + let accountWrapper: AccountsWrapper + let registry: Registry + + beforeAll(async () => { + accounts = await web3.eth.getAccounts() + kit.defaultAccount = accounts[0] + governance = await kit.contracts.getGovernance() + registry = await kit._web3Contracts.getRegistry() + lockedGold = await kit.contracts.getLockedGold() + accountWrapper = await kit.contracts.getAccounts() + + await concurrentMap(4, accounts.slice(0, 4), async (account) => { + await accountWrapper.createAccount().sendAndWaitForReceipt({ from: account }) + await lockedGold.lock().sendAndWaitForReceipt({ from: account, value: ONE_USD }) + }) + }, 5 * ONE_SEC) + + type Repoint = [CeloContract, Address] + + const registryRepointProposal = async (repoints: Repoint[]) => { + const builder = new ProposalBuilder(kit) + repoints.forEach((repoint) => + builder.addWeb3Tx(registry.methods.setAddressFor(...repoint), { + to: registry._address, + value: '0', + }) + ) + return builder.build() + } + + // const verifyRepointResult = (repoints: Repoint[]) => + // concurrentMap(4, repoints, async (repoint) => { + // const newAddress = await registry.methods.getAddressForStringOrDie(repoint[0]).call() + // expect(newAddress).toBe(repoint[1]) + // }) + + it('#getConfig', async () => { + const config = await governance.getConfig() + expect(config.concurrentProposals).toEqBigNumber(expConfig.concurrentProposals) + expect(config.dequeueFrequency).toEqBigNumber(expConfig.dequeueFrequency) + expect(config.minDeposit).toEqBigNumber(minDeposit) + expect(config.queueExpiry).toEqBigNumber(expConfig.queueExpiry) + expect(config.stageDurations.Approval).toEqBigNumber(expConfig.approvalStageDuration) + expect(config.stageDurations.Referendum).toEqBigNumber(expConfig.referendumStageDuration) + expect(config.stageDurations.Execution).toEqBigNumber(expConfig.executionStageDuration) + }) + + describe('Proposals', () => { + const repoints: Repoint[] = [ + [CeloContract.Random, '0x0000000000000000000000000000000000000001'], + [CeloContract.Escrow, '0x0000000000000000000000000000000000000002'], + ] + const proposalID = new BigNumber(1) + + let proposal: Proposal + beforeAll(async () => (proposal = await registryRepointProposal(repoints))) + + const proposeFn = async (proposer: Address) => + governance.propose(proposal).sendAndWaitForReceipt({ from: proposer, value: minDeposit }) + + const upvoteFn = async (upvoter: Address, shouldTimeTravel = true) => { + const tx = await governance.upvote(proposalID, upvoter) + await tx.sendAndWaitForReceipt({ from: upvoter }) + if (shouldTimeTravel) { + await timeTravel(expConfig.dequeueFrequency, web3) + await governance.dequeueProposalsIfReady().sendAndWaitForReceipt() + } + } + + // protocol/truffle-config defines approver address as accounts[0] + const approveFn = async () => { + const tx = await governance.approve(proposalID) + await tx.sendAndWaitForReceipt({ from: accounts[0] }) + await timeTravel(expConfig.approvalStageDuration, web3) + } + + const voteFn = async (voter: Address) => { + const tx = await governance.vote(proposalID, 'Yes') + await tx.sendAndWaitForReceipt({ from: voter }) + await timeTravel(expConfig.referendumStageDuration, web3) + } + + it('#propose', async () => { + await proposeFn(accounts[0]) + + const proposalRecord = await governance.getProposalRecord(proposalID) + expect(proposalRecord.metadata.proposer).toBe(accounts[0]) + expect(proposalRecord.metadata.transactionCount).toBe(proposal.length) + expect(proposalRecord.proposal).toStrictEqual(proposal) + expect(proposalRecord.stage).toBe('Queued') + }) + + it('#upvote', async () => { + await proposeFn(accounts[0]) + // shouldTimeTravel is false so getUpvotes isn't on dequeued proposal + await upvoteFn(accounts[1], false) + + const voteWeight = await governance.getVoteWeight(accounts[1]) + const upvotes = await governance.getUpvotes(proposalID) + expect(upvotes).toEqBigNumber(voteWeight) + }) + + it('#revokeUpvote', async () => { + await proposeFn(accounts[0]) + // shouldTimeTravel is false so revoke isn't on dequeued proposal + await upvoteFn(accounts[1], false) + + const before = await governance.getUpvotes(proposalID) + const upvoteRecord = await governance.getUpvoteRecord(accounts[1]) + + const tx = await governance.revokeUpvote(accounts[1]) + await tx.sendAndWaitForReceipt({ from: accounts[1] }) + + const after = await governance.getUpvotes(proposalID) + expect(after).toEqBigNumber(before.minus(upvoteRecord.upvotes)) + }) + + it('#approve', async () => { + await proposeFn(accounts[0]) + await upvoteFn(accounts[1]) + await approveFn() + + const approved = await governance.isApproved(proposalID) + expect(approved).toBeTruthy() + }) + + it('#vote', async () => { + await proposeFn(accounts[0]) + await upvoteFn(accounts[1]) + await approveFn() + await voteFn(accounts[2]) + + const voteWeight = await governance.getVoteWeight(accounts[2]) + const yesVotes = (await governance.getVotes(proposalID))[VoteValue.Yes] + expect(yesVotes).toEqBigNumber(voteWeight) + }) + + it( + '#execute', + async () => { + await proposeFn(accounts[0]) + await upvoteFn(accounts[1]) + await approveFn() + await voteFn(accounts[2]) + + const tx = await governance.execute(proposalID) + await tx.sendAndWaitForReceipt() + + const exists = await governance.proposalExists(proposalID) + expect(exists).toBeFalsy() + + // await verifyRepointResult(repoints) + }, + 10 * ONE_SEC + ) + }) + + // Disabled until validator set precompile is available in ganache + // https://github.com/celo-org/celo-monorepo/issues/1737 + + // describe('Hotfixes', () => { + // const repoints: Repoint[] = [ + // [CeloContract.Random, '0x0000000000000000000000000000000000000003'], + // [CeloContract.Escrow, '0x0000000000000000000000000000000000000004'], + // ] + + // let hotfixProposal: Proposal + // let hotfixHash: Buffer + // beforeAll(async () => { + // hotfixProposal = await registryRepointProposal(repoints) + // hotfixHash = proposalToHash(kit, hotfixProposal) + // }) + + // const whitelistFn = async (whitelister: Address) => { + // const tx = governance.whitelistHotfix(proposalToHash(kit, hotfixProposal)) + // await tx.sendAndWaitForReceipt({ from: whitelister }) + // } + + // // validator keys correspond to accounts 6-9 + // const whitelistQuorumFn = () => concurrentMap(1, accounts.slice(6, 10), whitelistFn) + + // // protocol/truffle-config defines approver address as accounts[0] + // const approveFn = async () => { + // const tx = governance.approveHotfix(proposalToHash(kit, hotfixProposal)) + // await tx.sendAndWaitForReceipt({ from: accounts[0] }) + // } + + // const prepareFn = async () => { + // const tx = governance.prepareHotfix(hotfixHash) + // await tx.sendAndWaitForReceipt() + // } + + // it('#whitelistHotfix', async () => { + // await whitelistFn(accounts[9]) + + // const whitelisted = await governance.isHotfixWhitelistedBy(hotfixHash, accounts[9]) + // expect(whitelisted).toBeTruthy() + // }) + + // it('#approveHotfix', async () => { + // await approveFn() + + // const record = await governance.getHotfixRecord(hotfixHash) + // expect(record.approved).toBeTruthy() + // }) + + // it( + // '#prepareHotfix', + // async () => { + // await whitelistQuorumFn() + // await approveFn() + // await prepareFn() + + // const validators = await kit.contracts.getValidators() + // const record = await governance.getHotfixRecord(hotfixHash) + // expect(record.preparedEpoch).toBe(await validators.getEpochNumber()) + // }, + // 10 * ONE_SEC + // ) + + // it( + // '#executeHotfix', + // async () => { + // await whitelistQuorumFn() + // await approveFn() + // await prepareFn() + + // const tx = governance.executeHotfix(hotfixProposal) + // await tx.sendAndWaitForReceipt() + + // const record = await governance.getHotfixRecord(hotfixHash) + // expect(record.executed).toBeTruthy() + + // await verifyRepointResult(repoints) + // }, + // 10 * ONE_SEC + // ) + // }) +}) diff --git a/packages/contractkit/src/wrappers/Governance.ts b/packages/contractkit/src/wrappers/Governance.ts index 42ad32543a8..52c3c492acf 100644 --- a/packages/contractkit/src/wrappers/Governance.ts +++ b/packages/contractkit/src/wrappers/Governance.ts @@ -1,21 +1,94 @@ +import { concurrentMap } from '@celo/utils/lib/async' +import { zip } from '@celo/utils/lib/collections' import BigNumber from 'bignumber.js' +import { Transaction } from 'web3/eth/types' +import { Address } from '../base' import { Governance } from '../generated/types/Governance' -import { BaseWrapper, proxyCall, toBigNumber } from './BaseWrapper' +import { + BaseWrapper, + bufferToBytes, + bufferToString, + bytesToString, + identity, + proxyCall, + proxySend, + stringToBuffer, + toTransactionObject, + tupleParser, + valueToBigNumber, + valueToInt, + valueToString, +} from './BaseWrapper' -export interface StageDurations { - approval: BigNumber // seconds - referendum: BigNumber // seconds - execution: BigNumber // seconds +export enum ProposalStage { + None = 'None', + Queued = 'Queued', + Approval = 'Approval', + Referendum = 'Referendum', + Execution = 'Execution', + Expiration = 'Expiration', +} + +export interface ProposalStageDurations { + [ProposalStage.Approval]: BigNumber // seconds + [ProposalStage.Referendum]: BigNumber // seconds + [ProposalStage.Execution]: BigNumber // seconds } export interface GovernanceConfig { concurrentProposals: BigNumber dequeueFrequency: BigNumber // seconds minDeposit: BigNumber - queueExpiry: BigNumber // seconds - stageDurations: StageDurations + queueExpiry: BigNumber + stageDurations: ProposalStageDurations +} + +export interface ProposalMetadata { + proposer: Address + deposit: BigNumber + timestamp: BigNumber + transactionCount: number +} + +export type ProposalParams = Parameters +export type ProposalTransaction = Pick +export type Proposal = ProposalTransaction[] + +export interface ProposalRecord { + stage: ProposalStage + metadata: ProposalMetadata + upvotes: BigNumber + votes: Votes + proposal: Proposal +} + +export interface UpvoteRecord { + proposalID: BigNumber + upvotes: BigNumber +} + +export enum VoteValue { + None = 'NONE', + Abstain = 'Abstain', + No = 'No', + Yes = 'Yes', +} + +export interface Votes { + [VoteValue.Yes]: BigNumber + [VoteValue.No]: BigNumber + [VoteValue.Abstain]: BigNumber +} + +export interface HotfixRecord { + hash: Buffer + approved: boolean + executed: boolean + preparedEpoch: BigNumber } +const ZERO_BN = new BigNumber(0) + /** * Contract managing voting for governance proposals. */ @@ -24,32 +97,36 @@ export class GovernanceWrapper extends BaseWrapper { * Querying number of possible concurrent proposals. * @returns Current number of possible concurrent proposals. */ - concurrentProposals = proxyCall(this.contract.methods.concurrentProposals, undefined, toBigNumber) + concurrentProposals = proxyCall( + this.contract.methods.concurrentProposals, + undefined, + valueToBigNumber + ) /** * Query proposal dequeue frequency. * @returns Current proposal dequeue frequency in seconds. */ - dequeueFrequency = proxyCall(this.contract.methods.dequeueFrequency, undefined, toBigNumber) + dequeueFrequency = proxyCall(this.contract.methods.dequeueFrequency, undefined, valueToBigNumber) /** * Query minimum deposit required to make a proposal. * @returns Current minimum deposit. */ - minDeposit = proxyCall(this.contract.methods.minDeposit, undefined, toBigNumber) + minDeposit = proxyCall(this.contract.methods.minDeposit, undefined, valueToBigNumber) /** * Query queue expiry parameter. * @return The number of seconds a proposal can stay in the queue before expiring. */ - queueExpiry = proxyCall(this.contract.methods.queueExpiry, undefined, toBigNumber) + queueExpiry = proxyCall(this.contract.methods.queueExpiry, undefined, valueToBigNumber) /** * Query durations of different stages in proposal lifecycle. * @returns Durations for approval, referendum and execution stages in seconds. */ - async stageDurations(): Promise { + async stageDurations(): Promise { const res = await this.contract.methods.stageDurations().call() return { - approval: toBigNumber(res[0]), - referendum: toBigNumber(res[1]), - execution: toBigNumber(res[2]), + [ProposalStage.Approval]: valueToBigNumber(res[0]), + [ProposalStage.Referendum]: valueToBigNumber(res[1]), + [ProposalStage.Execution]: valueToBigNumber(res[2]), } } @@ -72,4 +149,431 @@ export class GovernanceWrapper extends BaseWrapper { stageDurations: res[4], } } + + /** + * Returns the metadata associated with a given proposal. + * @param proposalID Governance proposal UUID + */ + getProposalMetadata: (proposalID: BigNumber.Value) => Promise = proxyCall( + this.contract.methods.getProposal, + tupleParser(valueToString), + (res) => ({ + proposer: res[0], + deposit: valueToBigNumber(res[1]), + timestamp: valueToBigNumber(res[2]), + transactionCount: valueToInt(res[3]), + }) + ) + + /** + * Returns the transaction at the given index associated with a given proposal. + * @param proposalID Governance proposal UUID + * @param txIndex Transaction index + */ + getProposalTransaction: ( + proposalID: BigNumber.Value, + txIndex: number + ) => Promise = proxyCall( + this.contract.methods.getProposalTransaction, + tupleParser(valueToString, valueToString), + (res) => ({ + value: res[0], + to: res[1], + input: bytesToString(res[2]), + }) + ) + + static toParams = (proposal: Proposal): ProposalParams => { + const data = proposal.map((tx) => stringToBuffer(tx.input)) + return [ + proposal.map((tx) => tx.value), + proposal.map((tx) => tx.to), + bufferToBytes(Buffer.concat(data)), + data.map((inp) => inp.length), + ] + } + + /** + * Returns whether a given proposal is approved. + * @param proposalID Governance proposal UUID + */ + isApproved: (proposalID: BigNumber.Value) => Promise = proxyCall( + this.contract.methods.isApproved, + tupleParser(valueToString) + ) + + /** + * Returns the approver address for proposals and hotfixes. + */ + getApprover = proxyCall(this.contract.methods.approver) + + getProposalStage = proxyCall( + this.contract.methods.getProposalStage, + tupleParser(valueToString), + (res) => Object.keys(ProposalStage)[valueToInt(res)] as ProposalStage + ) + + /** + * Returns the proposal associated with a given id. + * @param proposalID Governance proposal UUID + */ + async getProposal(proposalID: BigNumber.Value): Promise { + const metadata = await this.getProposalMetadata(proposalID) + const txIndices = Array.from(Array(metadata.transactionCount).keys()) + return concurrentMap(4, txIndices, (idx) => this.getProposalTransaction(proposalID, idx)) + } + + /** + * Returns the stage, metadata, upvotes, votes, and transactions associated with a given proposal. + * @param proposalID Governance proposal UUID + */ + async getProposalRecord(proposalID: BigNumber.Value): Promise { + const metadata = await this.getProposalMetadata(proposalID) + const proposal = await this.getProposal(proposalID) + const stage = await this.getProposalStage(proposalID) + + let upvotes = ZERO_BN + let votes = { [VoteValue.Yes]: ZERO_BN, [VoteValue.No]: ZERO_BN, [VoteValue.Abstain]: ZERO_BN } + if (stage === ProposalStage.Queued) { + upvotes = await this.getUpvotes(proposalID) + } else if (stage >= ProposalStage.Referendum && stage < ProposalStage.Expiration) { + votes = await this.getVotes(proposalID) + } + + return { + proposal, + metadata, + stage, + upvotes, + votes, + } + } + + /** + * Returns whether a given proposal is passing relative to the constitution's threshold. + * @param proposalID Governance proposal UUID + */ + isProposalPassing = proxyCall(this.contract.methods.isProposalPassing, tupleParser(valueToString)) + + /** + * Submits a new governance proposal. + * @param proposal Governance proposal + */ + propose = proxySend(this.kit, this.contract.methods.propose, GovernanceWrapper.toParams) + + /** + * Returns whether a governance proposal exists with the given ID. + * @param proposalID Governance proposal UUID + */ + proposalExists: (proposalID: BigNumber.Value) => Promise = proxyCall( + this.contract.methods.proposalExists, + tupleParser(valueToString) + ) + + /** + * Returns the current upvoted governance proposal ID and applied vote weight (zeroes if none). + * @param upvoter Address of upvoter + */ + getUpvoteRecord: (upvoter: Address) => Promise = proxyCall( + this.contract.methods.getUpvoteRecord, + tupleParser(identity), + (o) => ({ + proposalID: valueToBigNumber(o[0]), + upvotes: valueToBigNumber(o[1]), + }) + ) + + /** + * Returns whether a given proposal is queued. + * @param proposalID Governance proposal UUID + */ + isQueued = proxyCall(this.contract.methods.isQueued, tupleParser(valueToString)) + + /** + * Returns the upvotes applied to a given proposal. + * @param proposalID Governance proposal UUID + */ + getUpvotes = proxyCall( + this.contract.methods.getUpvotes, + tupleParser(valueToString), + valueToBigNumber + ) + + /** + * Returns the yes, no, and abstain votes applied to a given proposal. + * @param proposalID Governance proposal UUID + */ + getVotes = proxyCall( + this.contract.methods.getVoteTotals, + tupleParser(valueToString), + (res): Votes => ({ + [VoteValue.Yes]: valueToBigNumber(res[0]), + [VoteValue.No]: valueToBigNumber(res[1]), + [VoteValue.Abstain]: valueToBigNumber(res[2]), + }) + ) + + /** + * Returns the proposal queue as list of upvote records. + */ + getQueue = proxyCall(this.contract.methods.getQueue, undefined, (arraysObject) => + zip( + (_id, _upvotes) => ({ + proposalID: valueToBigNumber(_id), + upvotes: valueToBigNumber(_upvotes), + }), + arraysObject[0], + arraysObject[1] + ) + ) + + /** + * Returns the proposal dequeue as list of proposal IDs. + */ + getDequeue = proxyCall(this.contract.methods.getDequeue, undefined, (arrayObject) => + arrayObject.map(valueToBigNumber) + ) + + /** + * Dequeues any queued proposals if `dequeueFrequency` seconds have elapsed since the last dequeue + */ + dequeueProposalsIfReady = proxySend(this.kit, this.contract.methods.dequeueProposalsIfReady) + + /** + * Returns the number of votes that will be applied to a proposal for a given voter. + * @param voter Address of voter + */ + async getVoteWeight(voter: Address) { + const lockedGoldContract = await this.kit.contracts.getLockedGold() + return lockedGoldContract.getAccountTotalLockedGold(voter) + } + + private getIndex(id: BigNumber.Value, array: BigNumber[]) { + const index = array.findIndex((bn) => bn.isEqualTo(id)) + if (index === -1) { + throw new Error(`ID ${id} not found in array ${array}`) + } + return index + } + + private async getDequeueIndex(proposalID: BigNumber.Value, dequeue?: BigNumber[]) { + if (!dequeue) { + dequeue = await this.getDequeue() + } + return this.getIndex(proposalID, dequeue) + } + + private async getQueueIndex(proposalID: BigNumber.Value, queue?: UpvoteRecord[]) { + if (!queue) { + queue = await this.getQueue() + } + return { index: this.getIndex(proposalID, queue.map((record) => record.proposalID)), queue } + } + + private async lesserAndGreater(proposalID: BigNumber.Value, _queue?: UpvoteRecord[]) { + const { index, queue } = await this.getQueueIndex(proposalID, _queue) + return { + lesserID: index === 0 ? ZERO_BN : queue[index - 1].proposalID, + greaterID: index === queue.length - 1 ? ZERO_BN : queue[index + 1].proposalID, + } + } + + private sortedQueue(queue: UpvoteRecord[]) { + return queue.sort((a, b) => a.upvotes.comparedTo(b.upvotes)) + } + + private async withUpvoteRevoked(upvoter: Address, _queue?: UpvoteRecord[]) { + const upvoteRecord = await this.getUpvoteRecord(upvoter) + const { index, queue } = await this.getQueueIndex(upvoteRecord.proposalID, _queue) + queue[index].upvotes = queue[index].upvotes.minus(upvoteRecord.upvotes) + return { + queue: this.sortedQueue(queue), + upvoteRecord, + } + } + + private async withUpvoteApplied( + upvoter: Address, + proposalID: BigNumber.Value, + _queue?: UpvoteRecord[] + ) { + const { index, queue } = await this.getQueueIndex(proposalID, _queue) + const weight = await this.getVoteWeight(upvoter) + queue[index].upvotes = queue[index].upvotes.plus(weight) + return this.sortedQueue(queue) + } + + private async lesserAndGreaterAfterRevoke(upvoter: Address) { + const { queue, upvoteRecord } = await this.withUpvoteRevoked(upvoter) + return this.lesserAndGreater(upvoteRecord.proposalID, queue) + } + + private async lesserAndGreaterAfterUpvote(upvoter: Address, proposalID: BigNumber.Value) { + const upvoteRecord = await this.getUpvoteRecord(upvoter) + const queue = upvoteRecord.proposalID.isZero() + ? await this.getQueue() + : (await this.withUpvoteRevoked(upvoter)).queue + const upvoteQueue = await this.withUpvoteApplied(upvoter, proposalID, queue) + return this.lesserAndGreater(proposalID, upvoteQueue) + } + + /** + * Applies provided upvoter's upvote to given proposal. + * @param proposalID Governance proposal UUID + * @param upvoter Address of upvoter + */ + async upvote(proposalID: BigNumber.Value, upvoter: Address) { + const { lesserID, greaterID } = await this.lesserAndGreaterAfterUpvote(upvoter, proposalID) + return toTransactionObject( + this.kit, + this.contract.methods.upvote( + valueToString(proposalID), + valueToString(lesserID), + valueToString(greaterID) + ) + ) + } + + /** + * Revokes provided upvoter's upvote. + * @param upvoter Address of upvoter + */ + async revokeUpvote(upvoter: Address) { + const { lesserID, greaterID } = await this.lesserAndGreaterAfterRevoke(upvoter) + return toTransactionObject( + this.kit, + this.contract.methods.revokeUpvote(valueToString(lesserID), valueToString(greaterID)) + ) + } + + /** + * Approves given proposal, allowing it to later move to `referendum`. + * @param proposalID Governance proposal UUID + * @notice Only the `approver` address will succeed in sending this transaction + */ + async approve(proposalID: BigNumber.Value) { + const proposalIndex = await this.getDequeueIndex(proposalID) + return toTransactionObject( + this.kit, + this.contract.methods.approve(valueToString(proposalID), proposalIndex) + ) + } + + /** + * Applies `sender`'s vote choice to a given proposal. + * @param proposalID Governance proposal UUID + * @param vote Choice to apply (yes, no, abstain) + */ + async vote(proposalID: BigNumber.Value, vote: keyof typeof VoteValue) { + const proposalIndex = await this.getDequeueIndex(proposalID) + const voteNum = Object.keys(VoteValue).indexOf(vote) + return toTransactionObject( + this.kit, + this.contract.methods.vote(valueToString(proposalID), proposalIndex, voteNum) + ) + } + + /** + * Returns `voter`'s vote choice on a given proposal. + * @param proposalID Governance proposal UUID + * @param voter Address of voter + */ + async getVoteValue(proposalID: BigNumber.Value, voter: Address) { + const proposalIndex = await this.getDequeueIndex(proposalID) + const res = await this.contract.methods.getVoteRecord(voter, proposalIndex).call() + return Object.keys(VoteValue)[valueToInt(res[1])] as VoteValue + } + + /** + * Executes a given proposal's associated transactions. + * @param proposalID Governance proposal UUID + */ + async execute(proposalID: BigNumber.Value) { + const proposalIndex = await this.getDequeueIndex(proposalID) + return toTransactionObject( + this.kit, + this.contract.methods.execute(valueToString(proposalID), proposalIndex) + ) + } + + /** + * Returns approved, executed, and prepared status associated with a given hotfix. + * @param hash keccak256 hash of hotfix's associated abi encoded transactions + */ + async getHotfixRecord(hash: Buffer): Promise { + const res = await this.contract.methods.getHotfixRecord(bufferToString(hash)).call() + return { + hash, + approved: res[0], + executed: res[1], + preparedEpoch: valueToBigNumber(res[2]), + } + } + + /** + * Returns whether a given hotfix has been whitelisted by a given address. + * @param hash keccak256 hash of hotfix's associated abi encoded transactions + * @param whitelister address of whitelister + */ + isHotfixWhitelistedBy = proxyCall( + this.contract.methods.isHotfixWhitelistedBy, + tupleParser(bufferToString, (s: Address) => identity
(s)) + ) + + /** + * Returns whether a given hotfix can be passed. + * @param hash keccak256 hash of hotfix's associated abi encoded transactions + */ + isHotfixPassing = proxyCall(this.contract.methods.isHotfixPassing, tupleParser(bufferToString)) + + /** + * Returns the number of validators that whitelisted the hotfix + * @param hash keccak256 hash of hotfix's associated abi encoded transactions + */ + hotfixWhitelistValidatorTally = proxyCall( + this.contract.methods.hotfixWhitelistValidatorTally, + tupleParser(bufferToString) + ) + + /** + * Marks the given hotfix whitelisted by `sender`. + * @param hash keccak256 hash of hotfix's associated abi encoded transactions + */ + whitelistHotfix = proxySend( + this.kit, + this.contract.methods.whitelistHotfix, + tupleParser(bufferToString) + ) + + /** + * Marks the given hotfix approved by `sender`. + * @param hash keccak256 hash of hotfix's associated abi encoded transactions + * @notice Only the `approver` address will succeed in sending this transaction + */ + approveHotfix = proxySend( + this.kit, + this.contract.methods.approveHotfix, + tupleParser(bufferToString) + ) + + /** + * Marks the given hotfix prepared for current epoch if quorum of validators have whitelisted it. + * @param hash keccak256 hash of hotfix's associated abi encoded transactions + */ + prepareHotfix = proxySend( + this.kit, + this.contract.methods.prepareHotfix, + tupleParser(bufferToString) + ) + + /** + * Executes a given sequence of transactions if the corresponding hash is prepared and approved. + * @param hotfix Governance hotfix proposal + * @notice keccak256 hash of abi encoded transactions computed on-chain + */ + executeHotfix = proxySend( + this.kit, + this.contract.methods.executeHotfix, + GovernanceWrapper.toParams + ) } diff --git a/packages/contractkit/src/wrappers/LockedGold.test.ts b/packages/contractkit/src/wrappers/LockedGold.test.ts index 60017fafea6..1e538010866 100644 --- a/packages/contractkit/src/wrappers/LockedGold.test.ts +++ b/packages/contractkit/src/wrappers/LockedGold.test.ts @@ -16,7 +16,9 @@ testWithGanache('Validators Wrapper', (web3) => { kit.defaultAccount = account lockedGold = await kit.contracts.getLockedGold() accounts = await kit.contracts.getAccounts() - await accounts.createAccount().sendAndWaitForReceipt() + if (!(await accounts.isAccount(account))) { + await accounts.createAccount().sendAndWaitForReceipt({ from: account }) + } }) test('SBAT lock gold', async () => { diff --git a/packages/contractkit/src/wrappers/LockedGold.ts b/packages/contractkit/src/wrappers/LockedGold.ts index d68fb41d08a..f14326bdeb9 100644 --- a/packages/contractkit/src/wrappers/LockedGold.ts +++ b/packages/contractkit/src/wrappers/LockedGold.ts @@ -5,12 +5,11 @@ import { LockedGold } from '../generated/types/LockedGold' import { BaseWrapper, CeloTransactionObject, - NumberLike, - parseNumber, proxyCall, proxySend, - toBigNumber, tupleParser, + valueToBigNumber, + valueToString, } from '../wrappers/BaseWrapper' export interface VotingDetails { @@ -61,10 +60,10 @@ export class LockedGoldWrapper extends BaseWrapper { * Unlocks gold that becomes withdrawable after the unlocking period. * @param value The amount of gold to unlock. */ - unlock: (value: NumberLike) => CeloTransactionObject = proxySend( + unlock: (value: BigNumber.Value) => CeloTransactionObject = proxySend( this.kit, this.contract.methods.unlock, - tupleParser(parseNumber) + tupleParser(valueToString) ) async getPendingWithdrawalsTotalValue(account: Address) { @@ -79,7 +78,10 @@ export class LockedGoldWrapper extends BaseWrapper { * 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>> { + async relock( + account: Address, + value: BigNumber.Value + ): Promise>> { const pendingWithdrawals = await this.getPendingWithdrawals(account) // Ensure there are enough pending withdrawals to relock. const totalValue = await this.getPendingWithdrawalsTotalValue(account) @@ -116,10 +118,10 @@ export class LockedGoldWrapper extends BaseWrapper { * @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, value: NumberLike) => CeloTransactionObject = proxySend( + _relock: (index: number, value: BigNumber.Value) => CeloTransactionObject = proxySend( this.kit, this.contract.methods.relock, - tupleParser(parseNumber, parseNumber) + tupleParser(valueToString, valueToString) ) /** @@ -130,7 +132,7 @@ export class LockedGoldWrapper extends BaseWrapper { getAccountTotalLockedGold = proxyCall( this.contract.methods.getAccountTotalLockedGold, undefined, - toBigNumber + valueToBigNumber ) /** @@ -141,7 +143,7 @@ export class LockedGoldWrapper extends BaseWrapper { getAccountNonvotingLockedGold = proxyCall( this.contract.methods.getAccountNonvotingLockedGold, undefined, - toBigNumber + valueToBigNumber ) /** @@ -149,7 +151,7 @@ export class LockedGoldWrapper extends BaseWrapper { */ async getConfig(): Promise { return { - unlockingPeriod: toBigNumber(await this.contract.methods.unlockingPeriod().call()), + unlockingPeriod: valueToBigNumber(await this.contract.methods.unlockingPeriod().call()), } } @@ -177,9 +179,10 @@ export class LockedGoldWrapper extends BaseWrapper { async getPendingWithdrawals(account: string) { const withdrawals = await this.contract.methods.getPendingWithdrawals(account).call() return zip( - (time, value) => - // tslint:disable-next-line: no-object-literal-type-assertion - ({ time: toBigNumber(time), value: toBigNumber(value) } as PendingWithdrawal), + (time, value): PendingWithdrawal => ({ + time: valueToBigNumber(time), + value: valueToBigNumber(value), + }), withdrawals[1], withdrawals[0] ) diff --git a/packages/contractkit/src/wrappers/Reserve.ts b/packages/contractkit/src/wrappers/Reserve.ts index dfb6d39bb66..8e169cc194c 100644 --- a/packages/contractkit/src/wrappers/Reserve.ts +++ b/packages/contractkit/src/wrappers/Reserve.ts @@ -1,6 +1,6 @@ import BigNumber from 'bignumber.js' import { Reserve } from '../generated/types/Reserve' -import { BaseWrapper, proxyCall, proxySend, toBigNumber } from './BaseWrapper' +import { BaseWrapper, proxyCall, proxySend, valueToBigNumber } from './BaseWrapper' export interface ReserveConfig { tobinTaxStalenessThreshold: BigNumber @@ -17,7 +17,7 @@ export class ReserveWrapper extends BaseWrapper { tobinTaxStalenessThreshold = proxyCall( this.contract.methods.tobinTaxStalenessThreshold, undefined, - toBigNumber + valueToBigNumber ) isSpender: (account: string) => Promise = proxyCall(this.contract.methods.isSpender) transferGold = proxySend(this.kit, this.contract.methods.transferGold) diff --git a/packages/contractkit/src/wrappers/SortedOracles.ts b/packages/contractkit/src/wrappers/SortedOracles.ts index 56657d08248..bb8668570b6 100644 --- a/packages/contractkit/src/wrappers/SortedOracles.ts +++ b/packages/contractkit/src/wrappers/SortedOracles.ts @@ -6,8 +6,10 @@ import { BaseWrapper, CeloTransactionObject, proxyCall, - toBigNumber, toTransactionObject, + valueToBigNumber, + valueToFrac, + valueToInt, } from './BaseWrapper' export enum MedianRelation { @@ -43,7 +45,7 @@ export class SortedOraclesWrapper extends BaseWrapper { async numRates(token: CeloToken): Promise { const tokenAddress = await this.kit.registry.addressFor(token) const response = await this.contract.methods.numRates(tokenAddress).call() - return toBigNumber(response).toNumber() + return valueToInt(response) } /** @@ -56,7 +58,7 @@ export class SortedOraclesWrapper extends BaseWrapper { const tokenAddress = await this.kit.registry.addressFor(token) const response = await this.contract.methods.medianRate(tokenAddress).call() return { - rate: toBigNumber(response[0]).div(toBigNumber(response[1])), + rate: valueToFrac(response[0], response[1]), } } @@ -75,7 +77,11 @@ export class SortedOraclesWrapper extends BaseWrapper { * Returns the report expiry parameter. * @returns Current report expiry. */ - reportExpirySeconds = proxyCall(this.contract.methods.reportExpirySeconds, undefined, toBigNumber) + reportExpirySeconds = proxyCall( + this.contract.methods.reportExpirySeconds, + undefined, + valueToBigNumber + ) /** * Updates an oracle value and the median. @@ -150,7 +156,7 @@ export class SortedOraclesWrapper extends BaseWrapper { const medRelIndex = parseInt(response[2][i], 10) rates.push({ address: response[0][i], - rate: toBigNumber(response[1][i]).div(denominator), + rate: valueToFrac(response[1][i], denominator), medianRelation: medRelIndex, }) } @@ -158,7 +164,7 @@ export class SortedOraclesWrapper extends BaseWrapper { } private async getInternalDenominator(): Promise { - return toBigNumber(await this.contract.methods.DENOMINATOR().call()) + return valueToBigNumber(await this.contract.methods.DENOMINATOR().call()) } private async findLesserAndGreaterKeys( @@ -172,7 +178,7 @@ export class SortedOraclesWrapper extends BaseWrapper { // This is how the contract calculates the rate from the numerator and denominator. // To figure out where this new report goes in the list, we need to compare this // value with the other rates - const value = toBigNumber(numerator.toString()).div(toBigNumber(denominator.toString())) + const value = valueToFrac(numerator, denominator) let greaterKey = NULL_ADDRESS let lesserKey = NULL_ADDRESS diff --git a/packages/contractkit/src/wrappers/StableToken.test.ts b/packages/contractkit/src/wrappers/StableToken.test.ts index ed9708b9d67..44f95e159bf 100644 --- a/packages/contractkit/src/wrappers/StableToken.test.ts +++ b/packages/contractkit/src/wrappers/StableToken.test.ts @@ -2,10 +2,7 @@ import { testWithGanache } from '@celo/dev-utils/lib/ganache-test' import { newKitFromWeb3 } from '../kit' import { StableTokenWrapper } from './StableTokenWrapper' -/* -TEST NOTES: -- In migrations: The only account that has cUSD is accounts[0] -*/ +// TEST NOTES: balances defined in test-utils/migration-override testWithGanache('StableToken Wrapper', (web3) => { const ONE_USD = web3.utils.toWei('1', 'ether') diff --git a/packages/contractkit/src/wrappers/StableTokenWrapper.ts b/packages/contractkit/src/wrappers/StableTokenWrapper.ts index 558d55b539d..8c664fb81fe 100644 --- a/packages/contractkit/src/wrappers/StableTokenWrapper.ts +++ b/packages/contractkit/src/wrappers/StableTokenWrapper.ts @@ -4,13 +4,12 @@ import { StableToken } from '../generated/types/StableToken' import { BaseWrapper, CeloTransactionObject, - NumberLike, - parseNumber, proxyCall, proxySend, - toBigNumber, - toNumber, tupleParser, + valueToBigNumber, + valueToInt, + valueToString, } from './BaseWrapper' export interface InflationParameters { @@ -37,7 +36,7 @@ export class StableTokenWrapper extends BaseWrapper { * @param spender The spender of the StableToken. * @return The amount of StableToken owner is allowing spender to spend. */ - allowance = proxyCall(this.contract.methods.allowance, undefined, toBigNumber) + allowance = proxyCall(this.contract.methods.allowance, undefined, valueToBigNumber) /** * @return The name of the stable token. @@ -52,13 +51,13 @@ export class StableTokenWrapper extends BaseWrapper { /** * @return The number of decimal places to which StableToken is divisible. */ - decimals = proxyCall(this.contract.methods.decimals, undefined, toNumber) + decimals = proxyCall(this.contract.methods.decimals, undefined, valueToInt) /** * Returns the total supply of the token, that is, the amount of tokens currently minted. * @returns Total supply. */ - totalSupply = proxyCall(this.contract.methods.totalSupply, undefined, toBigNumber) + totalSupply = proxyCall(this.contract.methods.totalSupply, undefined, valueToBigNumber) /** * Gets the balance of the specified address using the presently stored inflation factor. @@ -68,7 +67,7 @@ export class StableTokenWrapper extends BaseWrapper { balanceOf: (owner: string) => Promise = proxyCall( this.contract.methods.balanceOf, undefined, - toBigNumber + valueToBigNumber ) owner = proxyCall(this.contract.methods.owner) @@ -80,10 +79,10 @@ export class StableTokenWrapper extends BaseWrapper { * @dev We don't compute the updated inflationFactor here because * we assume any function calling this will have updated the inflation factor. */ - valueToUnits: (value: NumberLike) => Promise = proxyCall( + valueToUnits: (value: BigNumber.Value) => Promise = proxyCall( this.contract.methods.valueToUnits, - tupleParser(parseNumber), - toBigNumber + tupleParser(valueToString), + valueToBigNumber ) /** @@ -91,10 +90,10 @@ export class StableTokenWrapper extends BaseWrapper { * @param units The units to convert to value. * @return The value corresponding to `units` given the current inflation factor. */ - unitsToValue: (units: NumberLike) => Promise = proxyCall( + unitsToValue: (units: BigNumber.Value) => Promise = proxyCall( this.contract.methods.unitsToValue, - tupleParser(parseNumber), - toBigNumber + tupleParser(valueToString), + valueToBigNumber ) /** @@ -123,10 +122,10 @@ export class StableTokenWrapper extends BaseWrapper { async getInflationParameters(): Promise { const res = await this.contract.methods.getInflationParameters().call() return { - rate: fromFixed(toBigNumber(res[0])), - factor: fromFixed(toBigNumber(res[1])), - updatePeriod: toBigNumber(res[2]), - factorLastUpdated: toBigNumber(res[3]), + rate: fromFixed(valueToBigNumber(res[0])), + factor: fromFixed(valueToBigNumber(res[1])), + updatePeriod: valueToBigNumber(res[2]), + factorLastUpdated: valueToBigNumber(res[3]), } } diff --git a/packages/contractkit/src/wrappers/Validators.ts b/packages/contractkit/src/wrappers/Validators.ts index 6edf76a3cab..a9a938a44b4 100644 --- a/packages/contractkit/src/wrappers/Validators.ts +++ b/packages/contractkit/src/wrappers/Validators.ts @@ -1,19 +1,21 @@ import { eqAddress } from '@celo/utils/lib/address' +import { concurrentMap } from '@celo/utils/lib/async' import { zip } from '@celo/utils/lib/collections' import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' +import { EventLog } from 'web3/types' import { Address, NULL_ADDRESS } from '../base' import { Validators } from '../generated/types/Validators' import { BaseWrapper, CeloTransactionObject, - parseBytes, proxyCall, proxySend, - toBigNumber, - toNumber, + stringToBytes, toTransactionObject, tupleParser, + valueToBigNumber, + valueToInt, } from './BaseWrapper' export interface Validator { @@ -34,6 +36,14 @@ export interface ValidatorGroup { commission: BigNumber } +export interface ValidatorReward { + validator: Validator + validatorPayment: BigNumber + group: ValidatorGroup + groupPayment: BigNumber + epochNumber: number +} + export interface LockedGoldRequirements { value: BigNumber duration: BigNumber @@ -68,8 +78,8 @@ export class ValidatorsWrapper extends BaseWrapper { async getValidatorLockedGoldRequirements(): Promise { const res = await this.contract.methods.getValidatorLockedGoldRequirements().call() return { - value: toBigNumber(res[0]), - duration: toBigNumber(res[1]), + value: valueToBigNumber(res[0]), + duration: valueToBigNumber(res[1]), } } @@ -80,8 +90,8 @@ export class ValidatorsWrapper extends BaseWrapper { async getGroupLockedGoldRequirements(): Promise { const res = await this.contract.methods.getGroupLockedGoldRequirements().call() return { - value: toBigNumber(res[0]), - duration: toBigNumber(res[1]), + value: valueToBigNumber(res[0]), + duration: valueToBigNumber(res[1]), } } @@ -92,7 +102,7 @@ export class ValidatorsWrapper extends BaseWrapper { getAccountLockedGoldRequirement = proxyCall( this.contract.methods.getAccountLockedGoldRequirement, undefined, - toBigNumber + valueToBigNumber ) /** @@ -107,7 +117,7 @@ export class ValidatorsWrapper extends BaseWrapper { return { validatorLockedGoldRequirements: res[0], groupLockedGoldRequirements: res[1], - maxGroupSize: toBigNumber(res[2]), + maxGroupSize: valueToBigNumber(res[2]), } } @@ -128,9 +138,9 @@ export class ValidatorsWrapper extends BaseWrapper { * @dev Fails if the `signer` is not an account or previously authorized signer. * @return The associated account. */ - async signerToAccount(signerAddress: Address) { + async signerToAccount(signerAddress: Address, blockNumber?: number) { const accounts = await this.kit.contracts.getAccounts() - return accounts.signerToAccount(signerAddress) + return accounts.signerToAccount(signerAddress, blockNumber) } /** @@ -147,7 +157,7 @@ export class ValidatorsWrapper extends BaseWrapper { ) => CeloTransactionObject = proxySend( this.kit, this.contract.methods.updateBlsPublicKey, - tupleParser(parseBytes, parseBytes) + tupleParser(stringToBytes, stringToBytes) ) /** @@ -190,10 +200,12 @@ export class ValidatorsWrapper extends BaseWrapper { } /** Get Validator information */ - async getValidator(address: Address): Promise { - const res = await this.contract.methods.getValidator(address).call() + async getValidator(address: Address, blockNumber?: number): Promise { + // @ts-ignore: Expected 0-1 arguments, but got 2 + const res = await this.contract.methods.getValidator(address).call({}, blockNumber) const accounts = await this.kit.contracts.getAccounts() - const name = (await accounts.getName(address)) || '' + const name = (await accounts.getName(address, blockNumber)) || '' + return { name, address, @@ -205,22 +217,24 @@ export class ValidatorsWrapper extends BaseWrapper { } } - async getValidatorFromSigner(address: Address): Promise { - const account = await this.signerToAccount(address) - return this.getValidator(account) + async getValidatorFromSigner(address: Address, blockNumber?: number): Promise { + const account = await this.signerToAccount(address, blockNumber) + return this.getValidator(account, blockNumber) } /** Get ValidatorGroup information */ async getValidatorGroup( address: Address, - getAffiliates: boolean = true + getAffiliates: boolean = true, + blockNumber?: number ): Promise { - const res = await this.contract.methods.getValidatorGroup(address).call() + // @ts-ignore: Expected 0-1 arguments, but got 2 + const res = await this.contract.methods.getValidatorGroup(address).call({}, blockNumber) const accounts = await this.kit.contracts.getAccounts() - const name = (await accounts.getName(address)) || '' + const name = (await accounts.getName(address, blockNumber)) || '' let affiliates: Validator[] = [] if (getAffiliates) { - const validators = await this.getRegisteredValidators() + const validators = await this.getRegisteredValidators(blockNumber) affiliates = validators .filter((v) => v.affiliation === address) .filter((v) => !res[0].includes(v.address)) @@ -243,21 +257,21 @@ export class ValidatorsWrapper extends BaseWrapper { this.contract.methods.getMembershipHistory, undefined, (res) => - // tslint:disable-next-line: no-object-literal-type-assertion - zip((epoch, group) => ({ epoch: toNumber(epoch), group } as GroupMembership), res[0], res[1]) + zip((epoch, group): GroupMembership => ({ epoch: valueToInt(epoch), group }), res[0], res[1]) ) /** Get the size (amount of members) of a ValidatorGroup */ getValidatorGroupSize: (group: Address) => Promise = proxyCall( this.contract.methods.getGroupNumMembers, undefined, - toNumber + valueToInt ) /** Get list of registered validator addresses */ - getRegisteredValidatorsAddresses: () => Promise = proxyCall( - this.contract.methods.getRegisteredValidators - ) + async getRegisteredValidatorsAddresses(blockNumber?: number): Promise { + // @ts-ignore: Expected 0-1 arguments, but got 2 + return this.contract.methods.getRegisteredValidators().call({}, blockNumber) + } /** Get list of registered validator group addresses */ getRegisteredValidatorGroupsAddresses: () => Promise = proxyCall( @@ -265,15 +279,15 @@ export class ValidatorsWrapper extends BaseWrapper { ) /** Get list of registered validators */ - async getRegisteredValidators(): Promise { - const vgAddresses = await this.getRegisteredValidatorsAddresses() - return Promise.all(vgAddresses.map((addr) => this.getValidator(addr))) + async getRegisteredValidators(blockNumber?: number): Promise { + const vgAddresses = await this.getRegisteredValidatorsAddresses(blockNumber) + return concurrentMap(10, vgAddresses, (addr) => this.getValidator(addr, blockNumber)) } /** Get list of registered validator groups */ async getRegisteredValidatorGroups(): Promise { const vgAddresses = await this.getRegisteredValidatorGroupsAddresses() - return Promise.all(vgAddresses.map((addr) => this.getValidatorGroup(addr, false))) + return concurrentMap(10, vgAddresses, (addr) => this.getValidatorGroup(addr, false)) } /** @@ -289,7 +303,20 @@ export class ValidatorsWrapper extends BaseWrapper { * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the * account address. 96 bytes. */ - registerValidator = proxySend(this.kit, this.contract.methods.registerValidator) + + getEpochNumber = proxyCall(this.contract.methods.getEpochNumber, undefined, valueToBigNumber) + + getEpochSize = proxyCall(this.contract.methods.getEpochSize, undefined, valueToBigNumber) + + registerValidator: ( + ecdsaPublicKey: string, + blsPublicKey: string, + blsPop: string + ) => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.registerValidator, + tupleParser(stringToBytes, stringToBytes, stringToBytes) + ) /** * De-registers a validator, removing it from the group for which it is a member. @@ -419,4 +446,31 @@ export class ValidatorsWrapper extends BaseWrapper { this.contract.methods.reorderMember(validator, nextMember, prevMember) ) } + + /** + * Retrieves ValidatorRewards for epochNumber. + * @param epochNumber The epoch to retrieve ValidatorRewards at. + */ + async getValidatorRewards(epochNumber: number): Promise { + const blockNumber = await this.kit.getLastBlockNumberForEpoch(epochNumber) + const events = await this.getPastEvents('ValidatorEpochPaymentDistributed', { + fromBlock: blockNumber, + toBlock: blockNumber, + }) + const validator: Validator[] = await concurrentMap(10, events, (e: EventLog) => + this.getValidator(e.returnValues.validator, blockNumber) + ) + const validatorGroup: ValidatorGroup[] = await concurrentMap(10, events, (e: EventLog) => + this.getValidatorGroup(e.returnValues.group, true, blockNumber) + ) + return events.map( + (e: EventLog, index: number): ValidatorReward => ({ + epochNumber, + validator: validator[index], + validatorPayment: e.returnValues.validatorPayment, + group: validatorGroup[index], + groupPayment: e.returnValues.groupPayment, + }) + ) + } } diff --git a/packages/dappkit/package.json b/packages/dappkit/package.json index 64ecf5686a3..4059bc74c28 100644 --- a/packages/dappkit/package.json +++ b/packages/dappkit/package.json @@ -6,7 +6,7 @@ }, "dependencies": { "@celo/contractkit": "0.1.5", - "@celo/utils": "^0.1.0", + "@celo/utils": "0.1.5-dev", "expo": "^34.0.1", "expo-contacts": "6.0.0", "libphonenumber-js": "^1.7.22" @@ -16,4 +16,4 @@ }, "main": "./lib/index.js", "files": ["src/**/*", "lib/**/*"] -} \ No newline at end of file +} diff --git a/packages/dev-utils/src/ganache-setup.ts b/packages/dev-utils/src/ganache-setup.ts index 3c45a52211f..81b8f0ef56b 100644 --- a/packages/dev-utils/src/ganache-setup.ts +++ b/packages/dev-utils/src/ganache-setup.ts @@ -43,7 +43,7 @@ export async function startGanache(datadir: string, opts: { verbose?: boolean } network_id: 1101, db_path: datadir, mnemonic: MNEMONIC, - gasLimit: 15000000, + gasLimit: 20000000, allowUnlimitedContractSize: true, }) diff --git a/packages/dev-utils/src/ganache-test.ts b/packages/dev-utils/src/ganache-test.ts index f61174654e6..546a8b90165 100644 --- a/packages/dev-utils/src/ganache-test.ts +++ b/packages/dev-utils/src/ganache-test.ts @@ -34,6 +34,11 @@ export function jsonRpcCall(web3: Web3, method: string, params: any[]): Promi }) } +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]) } diff --git a/packages/dev-utils/src/migration-override.json b/packages/dev-utils/src/migration-override.json index 7e2bec1145d..e45e4e3dad8 100644 --- a/packages/dev-utils/src/migration-override.json +++ b/packages/dev-utils/src/migration-override.json @@ -1,8 +1,18 @@ { "stableToken": { "initialBalances": { - "addresses": ["0x5409ed021d9299bf6814279a6a1411a7e866a631"], - "values": ["10000000000000000000000"] + "addresses": [ + "0x5409ED021D9299bf6814279A6A1411A7e866A631", + "0x6Ecbe1DB9EF729CBe972C83Fb886247691Fb6beb", + "0xE36Ea790bc9d7AB70C55260C66D52b1eca985f84", + "0xE834EC434DABA538cd1b9Fe1582052B880BD7e63" + ], + "values": [ + "50000000000000000000000", + "50000000000000000000000", + "50000000000000000000000", + "50000000000000000000000" + ] }, "oracles": [ "0x5409ED021D9299bf6814279A6A1411A7e866A631", @@ -10,5 +20,14 @@ "0x06cEf8E666768cC40Cc78CF93d9611019dDcB628", "0x7457d5E02197480Db681D3fdF256c7acA21bDc12" ] + }, + "governance": { + "dequeueFrequency": 30, + "queueExpiry": 1000, + "approvalStageDuration": 100, + "referendumStageDuration": 100, + "executionStageDuration": 100, + "minDeposit": 1, + "concurrentProposals": 5 } } diff --git a/packages/docs/celo-codebase/protocol/identity/metadata.md b/packages/docs/celo-codebase/protocol/identity/metadata.md index 99f885e03a9..585ada2cb4d 100644 --- a/packages/docs/celo-codebase/protocol/identity/metadata.md +++ b/packages/docs/celo-codebase/protocol/identity/metadata.md @@ -36,29 +36,29 @@ You can interact with metadata files easily through the [CLI](../../../command-l You can create an empty metadata file with: ```bash -$celocli account:create-metadata ./metadata.json --from $ACCOUNT_ADDRESS +celocli account:create-metadata ./metadata.json --from $ACCOUNT_ADDRESS ``` You can add claims with various commands: ```bash -$celocli account:claim-attestation-service-url ./metadata.json --from $ACCOUNT_ADDRESS --url $ATTESTATION_SERVICE_URL +celocli account:claim-attestation-service-url ./metadata.json --from $ACCOUNT_ADDRESS --url $ATTESTATION_SERVICE_URL ``` You can display the claims in your file and their status with: ```bash -$celocli account:show-metadata ./metadata.json +celocli account:show-metadata ./metadata.json ``` Once you are satisfied with your claims, you can upload your file to your own web site or a site that will host the file (for example, [https://gist.github.com](https://gist.github.com)) and then register it with the `Accounts` smart contract by running: ```bash -$celocli account:register-metadata --url $METADATA_URL --from $ACCOUNT_ADDRESS +celocli account:register-metadata --url $METADATA_URL --from $ACCOUNT_ADDRESS ``` Then, anyone can lookup your claims and verify them by running: ```bash -$celocli account:get-metadata $ACCOUNT_ADDRESS +celocli account:get-metadata $ACCOUNT_ADDRESS ``` diff --git a/packages/docs/command-line-interface/account.md b/packages/docs/command-line-interface/account.md index 96bdbaf4344..db3b6ee2c3c 100644 --- a/packages/docs/command-line-interface/account.md +++ b/packages/docs/command-line-interface/account.md @@ -318,14 +318,16 @@ Unlock an account address to send transactions or validate blocks ``` USAGE - $ celocli account:unlock + $ celocli account:unlock ACCOUNT + +ARGUMENTS + ACCOUNT Account address OPTIONS - --account=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Account Address --password=password EXAMPLE - unlock --account 0x5409ed021d9299bf6814279a6a1411a7e866a631 + unlock 0x5409ed021d9299bf6814279a6a1411a7e866a631 ``` _See code: [packages/cli/src/commands/account/unlock.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/account/unlock.ts)_ diff --git a/packages/docs/command-line-interface/exchange.md b/packages/docs/command-line-interface/exchange.md index a7cff726ec4..a1085cfb007 100644 --- a/packages/docs/command-line-interface/exchange.md +++ b/packages/docs/command-line-interface/exchange.md @@ -13,12 +13,16 @@ USAGE $ celocli exchange:dollars OPTIONS - --for=10000000000000000000000 (required) The minimum value of Celo Gold to receive in return + --forAtLeast=10000000000000000000000 [default: 0] Optional, the minimum value of Celo Gold to receive in + return + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) The address with Celo Dollars to exchange + --value=10000000000000000000000 (required) The value of Celo Dollars to exchange for Celo Gold -EXAMPLE - dollars --value 10000000000000 --for 50000000000000 --from 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d +EXAMPLES + dollars --value 10000000000000 --from 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d + dollars --value 10000000000000 --forAtLeast 50000000000000 --from 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d ``` _See code: [packages/cli/src/commands/exchange/dollars.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/exchange/dollars.ts)_ @@ -32,12 +36,16 @@ USAGE $ celocli exchange:gold OPTIONS - --for=10000000000000000000000 (required) The minimum value of Celo Dollars to receive in return + --forAtLeast=10000000000000000000000 [default: 0] Optional, the minimum value of Celo Dollars to receive + in return + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) The address with Celo Gold to exchange + --value=10000000000000000000000 (required) The value of Celo Gold to exchange for Celo Dollars -EXAMPLE - gold --value 5000000000000 --for 100000000000000 --from 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d +EXAMPLES + gold --value 5000000000000 --from 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d + gold --value 5000000000000 --forAtLeast 100000000000000 --from 0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d ``` _See code: [packages/cli/src/commands/exchange/gold.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/exchange/gold.ts)_ diff --git a/packages/docs/command-line-interface/governance.md b/packages/docs/command-line-interface/governance.md new file mode 100644 index 00000000000..24311162e4f --- /dev/null +++ b/packages/docs/command-line-interface/governance.md @@ -0,0 +1,142 @@ +--- +description: Approve a dequeued governance proposal +--- + +## Commands + +### Approve + +Approve a dequeued governance proposal + +``` +USAGE + $ celocli governance:approve + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Approver's address + --proposalID=proposalID (required) UUID of proposal to approve +``` + +_See code: [packages/cli/src/commands/governance/approve.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/governance/approve.ts)_ + +### Approvehotfix + +Approve a governance hotfix + +``` +USAGE + $ celocli governance:approvehotfix + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Approver's address + --hash=hash (required) Hash of hotfix transactions +``` + +_See code: [packages/cli/src/commands/governance/approvehotfix.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/governance/approvehotfix.ts)_ + +### Execute + +Execute a passing governance proposal + +``` +USAGE + $ celocli governance:execute + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Executor's address + --proposalID=proposalID (required) UUID of proposal to execute +``` + +_See code: [packages/cli/src/commands/governance/execute.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/governance/execute.ts)_ + +### Executehotfix + +Execute a governance hotfix prepared for the current epoch + +``` +USAGE + $ celocli governance:executehotfix + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Executors's address + --jsonTransactions=jsonTransactions (required) Path to json transactions +``` + +_See code: [packages/cli/src/commands/governance/executehotfix.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/governance/executehotfix.ts)_ + +### Preparehotfix + +Prepare a governance hotfix for execution in the current epoch + +``` +USAGE + $ celocli governance:preparehotfix + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Preparer's address + --hash=hash (required) Hash of hotfix transactions +``` + +_See code: [packages/cli/src/commands/governance/preparehotfix.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/governance/preparehotfix.ts)_ + +### Propose + +Submit a governance proposal + +``` +USAGE + $ celocli governance:propose + +OPTIONS + --deposit=deposit (required) Amount of Gold to attach to proposal + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Proposer's address + --jsonTransactions=jsonTransactions (required) Path to json transactions +``` + +_See code: [packages/cli/src/commands/governance/propose.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/governance/propose.ts)_ + +### Upvote + +Upvote a queued governance proposal + +``` +USAGE + $ celocli governance:upvote + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Upvoter's address + --proposalID=proposalID (required) UUID of proposal to upvote +``` + +_See code: [packages/cli/src/commands/governance/upvote.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/governance/upvote.ts)_ + +### Vote + +Vote on an approved governance proposal + +``` +USAGE + $ celocli governance:vote + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Voter's address + --proposalID=proposalID (required) UUID of proposal to vote on + --vote=(Abstain|No|Yes) (required) Vote +``` + +_See code: [packages/cli/src/commands/governance/vote.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/governance/vote.ts)_ + +### Whitelisthotfix + +Whitelist a governance hotfix + +``` +USAGE + $ celocli governance:whitelisthotfix + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Whitelister's address + --hash=hash (required) Hash of hotfix transactions +``` + +_See code: [packages/cli/src/commands/governance/whitelisthotfix.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/governance/whitelisthotfix.ts)_ diff --git a/packages/docs/command-line-interface/rewards.md b/packages/docs/command-line-interface/rewards.md new file mode 100644 index 00000000000..edafa4d0dc8 --- /dev/null +++ b/packages/docs/command-line-interface/rewards.md @@ -0,0 +1,25 @@ +--- +description: Show rewards information about a voter, registered Validator, or Validator Group +--- + +## Commands + +### Show + +Show rewards information about a voter, registered Validator, or Validator Group + +``` +USAGE + $ celocli rewards:show + +OPTIONS + --epochs=epochs [default: 1] Show results for the last N epochs + --group=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d Validator Group to show rewards for + --validator=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d Validator to show rewards for + --voter=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d Voter to show rewards for + +EXAMPLE + show --address 0x5409ed021d9299bf6814279a6a1411a7e866a631 +``` + +_See code: [packages/cli/src/commands/rewards/show.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/rewards/show.ts)_ diff --git a/packages/docs/command-line-interface/validator.md b/packages/docs/command-line-interface/validator.md index 76f13806119..e4b397432a4 100644 --- a/packages/docs/command-line-interface/validator.md +++ b/packages/docs/command-line-interface/validator.md @@ -85,9 +85,6 @@ List registered Validators, their name (if provided), affiliation, uptime score, USAGE $ celocli validator:list -OPTIONS - --no-truncate Don't truncate fields to fit line - EXAMPLE list ``` @@ -152,24 +149,27 @@ _See code: [packages/cli/src/commands/validator/show.ts](https://github.com/celo ### Status -Show information about whether the validator signer is elected and validating. This command will check that the validator meets the registration requirements, and its signer is currently elected and actively signing blocks. +Shows the consensus status of a validator. This command will show whether a validator is currently elected, would be elected if an election were to be run right now, and the percentage of blocks signed and number of blocks successfully proposed within a given window. ``` USAGE $ celocli validator:status OPTIONS + --all get the status of all registered validators + --lookback=lookback [default: 100] how many blocks to look back for signer activity + --no-truncate Don't truncate fields to fit line + --signer=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d address of the signer to check if elected and validating --validator=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d address of the validator to check if elected and validating EXAMPLES status --validator 0x5409ED021D9299bf6814279A6A1411A7e866A631 - status --signer 0x738337030fAeb1E805253228881d844b5332fB4c - status --signer 0x738337030fAeb1E805253228881d844b5332fB4c --lookback 100 + status --all --lookback 100 ``` _See code: [packages/cli/src/commands/validator/status.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/validator/status.ts)_ diff --git a/packages/docs/getting-started/running-a-validator.md b/packages/docs/getting-started/running-a-validator.md index 2a0393e63cc..5ae913642db 100644 --- a/packages/docs/getting-started/running-a-validator.md +++ b/packages/docs/getting-started/running-a-validator.md @@ -634,7 +634,7 @@ docker run --name celo-attestation-service -it --restart always --entrypoint /bi ## Registering Metadata -We are using [Metadata]()(../celo-codebase/protocol/identity/metadata) to allow accounts to make certain claims without having to do so on-chain. For us to complete the process, we have to make two claims: +We are using [Metadata](../celo-codebase/protocol/identity/metadata) to allow accounts to make certain claims without having to do so on-chain. For us to complete the process, we have to make two claims: 1. Under which URL users can request attestations from 2. Which accounts belong together for the purpose of the leaderboard diff --git a/packages/helm-charts/ethstats/templates/ethstats.deployment.yaml b/packages/helm-charts/ethstats/templates/ethstats.deployment.yaml index f60ab444caa..7b702132def 100644 --- a/packages/helm-charts/ethstats/templates/ethstats.deployment.yaml +++ b/packages/helm-charts/ethstats/templates/ethstats.deployment.yaml @@ -26,32 +26,20 @@ spec: - name: ethstats image: {{ .Values.ethstats.image.repository }}:{{ .Values.ethstats.image.tag }} imagePullPolicy: {{ .Values.imagePullPolicy }} + env: + - name: TRUSTED_ADDRESSES + value: {{ .Values.ethstats.trusted_addresses }} + - name: BANNED_ADDRESSES + value: {{ .Values.ethstats.banned_addresses }} + - name: RESERVED_ADDRESSES + value: {{ .Values.ethstats.reserved_addresses }} command: - /bin/sh - -c args: - | - cat <<'EOF' > /celostats-server/lib/utils/config.js - var trusted = [ - {{- range .Values.ethstats.trusted_addresses }} - {{- if . }} - "{{lower . }}", - {{- end }} - {{- end }} - ] - var banned = [ - {{- range .Values.ethstats.banned_addresses }} - {{- if . }} - "{{lower . }}", - {{- end }} - {{- end }} - ] - module.exports = { - trusted: trusted, - banned: banned, - reserved: [] - }; - EOF + sed -i "s%###NETWORK_NAME###%{{ .Values.ethstats.network_name }}%g" /celostats-server/dist/js/netstats.min.js + sed -i "s%###BLOCKSCOUT_URL###%{{ .Values.ethstats.blockscout_url }}%g" /celostats-server/dist/js/netstats.min.js npm start ports: - name: http diff --git a/packages/mobile/README.md b/packages/mobile/README.md index 9b37e54625a..a8508dca422 100644 --- a/packages/mobile/README.md +++ b/packages/mobile/README.md @@ -38,7 +38,9 @@ export GRADLE_OPTS='-Dorg.gradle.daemon=true -Dorg.gradle.parallel=true -Dorg.gr 3. Compile the project and start the bundler with ```bash - yarn run dev + yarn run dev:android + OR + yarn run dev:ios ``` This will build the app in a device (physical or emulated) and open a diff --git a/packages/mobile/__mocks__/react-i18next.tsx b/packages/mobile/__mocks__/react-i18next.js similarity index 64% rename from packages/mobile/__mocks__/react-i18next.tsx rename to packages/mobile/__mocks__/react-i18next.js index a463a5a5fee..b14a4edcfeb 100644 --- a/packages/mobile/__mocks__/react-i18next.tsx +++ b/packages/mobile/__mocks__/react-i18next.js @@ -1,3 +1,4 @@ +// Taken from https://github.com/i18next/react-i18next/blob/master/example/test-jest/__mocks__/react-i18next.js const React = require('react') const reactI18next = require('react-i18next') @@ -21,7 +22,8 @@ const renderNodes = (reactNodes) => { if (hasChildren(child)) { const inner = renderNodes(getChildren(child)) return React.cloneElement(child, { ...child.props, key: i }, inner) - } else if (typeof child === 'object' && !isElement) { + } + if (typeof child === 'object' && !isElement) { return Object.keys(child).reduce((str, childKey) => `${str}${child[childKey]}`, '') } @@ -29,26 +31,22 @@ const renderNodes = (reactNodes) => { }) } -// this mock makes sure any components using the translate HoC receive the t function as a prop -function withNamespaces() { - return (Component) => { - const t = (k) => k - const wrapper = (props) => - wrapper.displayName = Component.displayName || Component.name || 'NameSpacedComponent' - return wrapper - } -} +const useMock = [(k) => k, {}] +useMock.t = (k) => k +useMock.i18n = { language: 'en' } module.exports = { - withNamespaces, + // this mock makes sure any components using the translate HoC receive the t function as a prop + withTranslation: () => (Component) => (props) => ( + k} i18n={{ language: 'en' }} {...props} /> + ), Trans: ({ children }) => renderNodes(children), - NamespacesConsumer: ({ children }) => children((k) => k, { i18n: {} }), + Translation: ({ children }) => children((k) => k, { i18n: {} }), + useTranslation: () => useMock, // mock if needed - Interpolate: reactI18next.Interpolate, I18nextProvider: reactI18next.I18nextProvider, - loadNamespaces: reactI18next.loadNamespaces, - reactI18nextModule: reactI18next.reactI18nextModule, + initReactI18next: reactI18next.initReactI18next, setDefaults: reactI18next.setDefaults, getDefaults: reactI18next.getDefaults, setI18n: reactI18next.setI18n, diff --git a/packages/mobile/__mocks__/src/i18n.ts b/packages/mobile/__mocks__/src/i18n.ts index df661a9a3ce..acd237d8de7 100644 --- a/packages/mobile/__mocks__/src/i18n.ts +++ b/packages/mobile/__mocks__/src/i18n.ts @@ -1,4 +1,13 @@ -export default { language: 'EN', t: (key: string) => key, changeLanguage: () => {} } -export const createScopedT = jest.fn(() => jest.fn()) +import hoistStatics from 'hoist-non-react-statics' +import { withTranslation as withTranslationI18Next } from 'react-i18next' + +export default { + language: 'EN', + t: (key: string) => key, + changeLanguage: () => {}, +} export enum Namespaces {} + +export const withTranslation = (namespace: any) => (component: React.ComponentType) => + hoistStatics(withTranslationI18Next(namespace)(component), component) diff --git a/packages/mobile/ios/Podfile.lock b/packages/mobile/ios/Podfile.lock index 19125fed655..7da0ef76860 100644 --- a/packages/mobile/ios/Podfile.lock +++ b/packages/mobile/ios/Podfile.lock @@ -387,7 +387,7 @@ PODS: - React - RNFS (2.14.1): - React - - RNGestureHandler (1.4.1): + - RNGestureHandler (1.5.2): - React - RNLocalize (1.3.0): - React @@ -712,7 +712,7 @@ SPEC CHECKSUMS: RNExitApp: c4e052df2568b43bec8a37c7cd61194d4cfee2c3 RNFirebase: ac0de8b24c6f91ae9459575491ed6a77327619c6 RNFS: a8fbe7060fa49157d819466404794ad9c58e58cf - RNGestureHandler: 4cb47a93019c1a201df2644413a0a1569a51c8aa + RNGestureHandler: 946a7691e41df61e2c4b1884deab41a4cdc3afff RNLocalize: cebb57c3c1e5479806204ce135b26bdd79d17e8a RNPermissions: 9525b0f4d209fdf9a373d52d1492ae2943e691e5 RNRandomBytes: e5680396032547c8ceb494768532459dc6911b76 diff --git a/packages/mobile/locales/en-US/nuxVerification2.json b/packages/mobile/locales/en-US/nuxVerification2.json index 15a430a3aa0..c83814c41d3 100644 --- a/packages/mobile/locales/en-US/nuxVerification2.json +++ b/packages/mobile/locales/en-US/nuxVerification2.json @@ -25,7 +25,7 @@ "Celo Phone number verification works by associating your Celo Wallet with your phone number.", "section1Header": "Do I need to complete this?", "section1Body": - "Verification is not required. However, if you do not verify, others on the Celo network cannot send value to you using your phone number. They must use QR codes or addresses.", + "Verification is not required. However, without verifying, it is only possible to send and receive value using QR codes or addresses.", "section2Header": "Security and Privacy", "section2Body": "To protect your privacy, only an obfuscated version of your phone number is stored on the Celo blockchain." @@ -41,7 +41,7 @@ "header": "Skip Verification?", "body1": "Verifying allows others to send value to your phone number.", "body2": - "Without verification, you can still receive payments but only using Celo addresses or QR codes." + "Without verification, you can still send payments, but only using Celo addresses or QR codes." }, "interstitial": { "header": "Almost Done", diff --git a/packages/mobile/locales/en-US/sendFlow7.json b/packages/mobile/locales/en-US/sendFlow7.json index e1db576fb9d..896b3fadaf4 100644 --- a/packages/mobile/locales/en-US/sendFlow7.json +++ b/packages/mobile/locales/en-US/sendFlow7.json @@ -38,7 +38,7 @@ "inviteFriends": "Invite Friends", "noResultsFor": "No results for", "noContacts": "No contacts found", - "searchForSomeone": "Search for someone using their name or phone number", + "searchForSomeone": "Search for someone using their name, phone number, or address", "nameOrPhoneNumber": "Name or Phone Number", "pending": "Pending", "sentPayment": "Sent Payment", @@ -71,5 +71,20 @@ "askForContactsPermissionAction": "Get my Contacts", "newAccountBalance": "New account balance: ", "estimatingFee": "Estimating fee ", - "estimatedFee": "Estimated fee: " + "estimatedFee": "Estimated fee: ", + "verificationCta": { + "header": "Sending to a friend?", + "body": "Verify your phone and sync your contacts to quickly send to your friends.", + "cta": "Start Verification" + }, + "importContactsCta": { + "header": "Import Contacts", + "body": "Please enable Contacts in your phone’s Settings to find your friends by name.", + "cta": "Settings" + }, + "contactSyncProgress": { + "header": "Syncing Contacts", + "progress": "{{current}} of {{total}}", + "importing": "Importing..." + } } diff --git a/packages/mobile/locales/es-419/nuxVerification2.json b/packages/mobile/locales/es-419/nuxVerification2.json index 63345a412b5..4a371fa574c 100755 --- a/packages/mobile/locales/es-419/nuxVerification2.json +++ b/packages/mobile/locales/es-419/nuxVerification2.json @@ -25,7 +25,7 @@ "La verificación del número telefónico de Celo consiste en asociar su billetera de Celo con su teléfono.", "section1Header": "¿Tengo que completar este paso?", "section1Body": - "La verificación no es obligatoria. Cuando la completa, las personas de la red de Celo le podrán enviar valores usando su número telefónico. De lo contrario, deberán usar códigos QR o direcciones.", + "La verificación no es obligatoria. Sin verificar, solo es posible enviar y recibir valor utilizando códigos QR o direcciones.", "section2Header": "Seguridad y privacidad", "section2Body": "Para proteger su privacidad, solo se guarda una versión oculta de su teléfono en la cadena de bloques de Celo." diff --git a/packages/mobile/locales/es-419/sendFlow7.json b/packages/mobile/locales/es-419/sendFlow7.json index a21064e1583..23756a103f5 100755 --- a/packages/mobile/locales/es-419/sendFlow7.json +++ b/packages/mobile/locales/es-419/sendFlow7.json @@ -38,7 +38,7 @@ "inviteFriends": "Invitar a amigos", "noResultsFor": "Sin resultados para", "noContacts": "No se encontraron contactos", - "searchForSomeone": "Busque a alguien según el nombre o el número de teléfono", + "searchForSomeone": "Busque a alguien según el nombre, el número de teléfono, o el dirección", "nameOrPhoneNumber": "Nombre o número de teléfono", "pending": "Pendiente", "sentPayment": "Enviar pago", @@ -71,5 +71,22 @@ "askForContactsPermissionAction": "Accede a tus contactos", "newAccountBalance": "Nuevo saldo de cuenta: ", "estimatingFee": "Estimando tarifa ", - "estimatedFee": "Tarifa estimada: " + "estimatedFee": "Tarifa estimada: ", + "verificationCta": { + "header": "¿Enviar a un amigo?", + "body": + "Verifique su teléfono y sincronice sus contactos para enviarlos rápidamente a sus amigos.", + "cta": "Iniciar verificación" + }, + "importContactsCta": { + "header": "Importar contactos", + "body": + "Habilite Contactos en la Configuración de su teléfono para encontrar a sus amigos por nombre.", + "cta": "Configuraciones" + }, + "contactSyncProgress": { + "header": "Sincronización de Contactos", + "progress": "{{current}} de {{total}}", + "importing": "Importar..." + } } diff --git a/packages/mobile/package.json b/packages/mobile/package.json index e9c5ba69f36..9b75b13a7f3 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -44,7 +44,7 @@ "@celo/contractkit": "0.2.8-dev", "@celo/react-components": "1.0.0", "@celo/react-native-sms-retriever": "git+https://github.com/celo-org/react-native-sms-retriever#b88e502", - "@celo/utils": "^0.1.1", + "@celo/utils": "0.1.5-dev", "@celo/walletkit": "^0.0.15", "@react-native-community/async-storage": "^1.6.2", "@react-native-community/netinfo": "^4.4.0", @@ -65,7 +65,7 @@ "fuzzysort": "^1.1.4", "google-libphonenumber": "^3.2.4", "graphql": "^14.1.1", - "i18next": "^11.9.1", + "i18next": "^19.0.2", "js-sha3": "^0.7.0", "lodash": "^4.17.14", "lottie-ios": "3.1.3", @@ -77,7 +77,7 @@ "react": "16.9.0", "react-apollo": "^2.5.8", "react-async-hook": "^3.4.0", - "react-i18next": "^8.3.8", + "react-i18next": "^11.2.7", "react-native": "^0.61.2", "react-native-android-open-settings": "^1.3.0", "react-native-bip39": "git://github.com/celo-org/react-native-bip39#1488fa1", @@ -91,7 +91,7 @@ "react-native-firebase": "^5.5.6", "react-native-flag-secure-android": "git://github.com/kristiansorens/react-native-flag-secure-android#e234251", "react-native-fs": "^2.14.1", - "react-native-gesture-handler": "^1.4.1", + "react-native-gesture-handler": "^1.5.2", "react-native-geth": "https://github.com/celo-org/react-native-geth#6f993d9", "react-native-install-referrer": "git://github.com/celo-org/react-native-install-referrer#343bf3d", "react-native-keep-awake": "^4.0.0", diff --git a/packages/mobile/scripts/run_app.sh b/packages/mobile/scripts/run_app.sh index 7a417c41479..cffde243930 100755 --- a/packages/mobile/scripts/run_app.sh +++ b/packages/mobile/scripts/run_app.sh @@ -98,7 +98,7 @@ if [ $PLATFORM = "android" ]; then if [ $MACHINE = "Mac" ]; then echo "Starting packager in new terminal" - RN_START_CMD="cd `pwd`;yarn react-native start" + RN_START_CMD="cd `pwd` && (yarn react-native start || yarn react-native start)" OSASCRIPT_CMD="tell application \"Terminal\" to do script \"$RN_START_CMD\"" echo "FULLCMD: $OSASCRIPT_CMD" osascript -e "$OSASCRIPT_CMD" diff --git a/packages/mobile/src/account/Account.tsx b/packages/mobile/src/account/Account.tsx index 6f3caabc634..1a0280917a2 100644 --- a/packages/mobile/src/account/Account.tsx +++ b/packages/mobile/src/account/Account.tsx @@ -4,7 +4,7 @@ import { fontStyles } from '@celo/react-components/styles/fonts' import { anonymizedPhone, isE164Number } from '@celo/utils/src/phoneNumbers' import * as Sentry from '@sentry/react-native' import * as React from 'react' -import { WithNamespaces, withNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { Clipboard, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native' import DeviceInfo from 'react-native-device-info' import SafeAreaView from 'react-native-safe-area-view' @@ -17,7 +17,7 @@ import { resetAppOpenedState, setAnalyticsEnabled, setNumberVerified } from 'src import { AvatarSelf } from 'src/components/AvatarSelf' import { FAQ_LINK, TOS_LINK } from 'src/config' import { features } from 'src/flags' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { revokeVerification } from 'src/identity/actions' import { headerWithBackButton } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' @@ -43,7 +43,7 @@ interface StateProps { numberVerified: boolean } -type Props = StateProps & DispatchProps & WithNamespaces +type Props = StateProps & DispatchProps & WithTranslation interface State { version: string @@ -129,6 +129,10 @@ export class Account extends React.Component { Logger.showMessage('App onboarding state reset.') } + toggleNumberVerified = () => { + this.props.setNumberVerified(!this.props.numberVerified) + } + revokeNumberVerification = async () => { if (!isE164Number(this.props.e164PhoneNumber)) { Logger.showMessage('Cannot revoke verificaton: number invalid') @@ -178,11 +182,17 @@ export class Account extends React.Component { Revoke Number Verification */} + + + Toggle verification done + + Reset app opened state + Reset backup state @@ -315,4 +325,4 @@ const style = StyleSheet.create({ export default connect( mapStateToProps, mapDispatchToProps -)(withNamespaces(Namespaces.accountScreen10)(Account)) +)(withTranslation(Namespaces.accountScreen10)(Account)) diff --git a/packages/mobile/src/account/Analytics.tsx b/packages/mobile/src/account/Analytics.tsx index da0b9801e85..7a13f986667 100644 --- a/packages/mobile/src/account/Analytics.tsx +++ b/packages/mobile/src/account/Analytics.tsx @@ -2,11 +2,11 @@ import SettingsSwitchItem from '@celo/react-components/components/SettingsSwitch import colors from '@celo/react-components/styles/colors' import fontStyles from '@celo/react-components/styles/fonts' import * as React from 'react' -import { WithNamespaces, withNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { ScrollView, StyleSheet, Text } from 'react-native' import { connect } from 'react-redux' import { setAnalyticsEnabled } from 'src/app/actions' -import i18n, { Namespaces } from 'src/i18n' +import i18n, { Namespaces, withTranslation } from 'src/i18n' import { headerWithBackButton } from 'src/navigator/Headers' import { RootState } from 'src/redux/reducers' @@ -18,7 +18,7 @@ interface DispatchProps { setAnalyticsEnabled: typeof setAnalyticsEnabled } -type Props = StateProps & DispatchProps & WithNamespaces +type Props = StateProps & DispatchProps & WithTranslation const mapStateToProps = (state: RootState): StateProps => { return { @@ -58,4 +58,4 @@ const style = StyleSheet.create({ export default connect( mapStateToProps, { setAnalyticsEnabled } -)(withNamespaces(Namespaces.accountScreen10)(Analytics)) +)(withTranslation(Namespaces.accountScreen10)(Analytics)) diff --git a/packages/mobile/src/account/DataSaver.tsx b/packages/mobile/src/account/DataSaver.tsx index a6ec7c7a048..24f3b84f8e0 100644 --- a/packages/mobile/src/account/DataSaver.tsx +++ b/packages/mobile/src/account/DataSaver.tsx @@ -3,11 +3,11 @@ import TextButton from '@celo/react-components/components/TextButton' import colors from '@celo/react-components/styles/colors' import fontStyles from '@celo/react-components/styles/fonts' import * as React from 'react' -import { WithNamespaces, withNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { ScrollView, StyleSheet, Text, View } from 'react-native' import Modal from 'react-native-modal' import { connect } from 'react-redux' -import i18n, { Namespaces } from 'src/i18n' +import i18n, { Namespaces, withTranslation } from 'src/i18n' import { headerWithBackButton } from 'src/navigator/Headers' import { RootState } from 'src/redux/reducers' import { toggleZeroSyncMode } from 'src/web3/actions' @@ -21,7 +21,7 @@ interface DispatchProps { toggleZeroSyncMode: typeof toggleZeroSyncMode } -type Props = StateProps & DispatchProps & WithNamespaces +type Props = StateProps & DispatchProps & WithTranslation const mapDispatchToProps = { toggleZeroSyncMode, @@ -139,4 +139,4 @@ const styles = StyleSheet.create({ export default connect( mapStateToProps, mapDispatchToProps -)(withNamespaces(Namespaces.accountScreen10)(DataSaver)) +)(withTranslation(Namespaces.accountScreen10)(DataSaver)) diff --git a/packages/mobile/src/account/EditProfile.test.tsx b/packages/mobile/src/account/EditProfile.test.tsx index abbb731c1ac..89b62c66014 100644 --- a/packages/mobile/src/account/EditProfile.test.tsx +++ b/packages/mobile/src/account/EditProfile.test.tsx @@ -2,15 +2,14 @@ import * as React from 'react' import 'react-native' import { Provider } from 'react-redux' import * as renderer from 'react-test-renderer' -import { setName } from 'src/account/actions' -import { EditProfile } from 'src/account/EditProfile' -import { createMockStore, getMockI18nProps } from 'test/utils' +import EditProfile from 'src/account/EditProfile' +import { createMockStore } from 'test/utils' it('renders the EditProfile Component', () => { const store = createMockStore() const tree = renderer.create( - + ) expect(tree).toMatchSnapshot() diff --git a/packages/mobile/src/account/EditProfile.tsx b/packages/mobile/src/account/EditProfile.tsx index 66a82a40506..74ee88111ab 100644 --- a/packages/mobile/src/account/EditProfile.tsx +++ b/packages/mobile/src/account/EditProfile.tsx @@ -3,13 +3,13 @@ import colors from '@celo/react-components/styles/colors' import fontStyles from '@celo/react-components/styles/fonts' import variables from '@celo/react-components/styles/variables' import * as React from 'react' -import { WithNamespaces, withNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { ScrollView, StyleSheet } from 'react-native' import { connect } from 'react-redux' import { setName } from 'src/account/actions' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { headerWithBackButton } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' @@ -23,7 +23,7 @@ interface DispatchProps { setName: typeof setName } -type Props = StateProps & DispatchProps & WithNamespaces +type Props = StateProps & DispatchProps & WithTranslation const mapStateToProps = (state: RootState): StateProps => { return { @@ -94,4 +94,4 @@ export default connect( { setName, } -)(withNamespaces(Namespaces.accountScreen10)(EditProfile)) +)(withTranslation(Namespaces.accountScreen10)(EditProfile)) diff --git a/packages/mobile/src/account/Education.tsx b/packages/mobile/src/account/Education.tsx index 3e2c280891f..7f621e8dde3 100644 --- a/packages/mobile/src/account/Education.tsx +++ b/packages/mobile/src/account/Education.tsx @@ -3,13 +3,13 @@ import Touchable from '@celo/react-components/components/Touchable' import colors from '@celo/react-components/styles/colors' import { fontStyles } from '@celo/react-components/styles/fonts' import * as React from 'react' -import { WithNamespaces, withNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { Dimensions, Image, StyleSheet, Text, View } from 'react-native' import Swiper from 'react-native-swiper' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' import { componentWithAnalytics } from 'src/analytics/wrapper' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { placeholder } from 'src/images/Images' import { navigateBack } from 'src/navigator/NavigationService' @@ -37,7 +37,7 @@ interface CustomizedProps { onFinishAlternate?: () => void } -type Props = WithNamespaces & CustomizedProps +type Props = WithTranslation & CustomizedProps class Education extends React.Component { state = { @@ -219,4 +219,4 @@ const style = StyleSheet.create({ }, }) -export default componentWithAnalytics(withNamespaces(Namespaces.nuxCurrencyPhoto4)(Education)) +export default componentWithAnalytics(withTranslation(Namespaces.nuxCurrencyPhoto4)(Education)) diff --git a/packages/mobile/src/account/Invite.tsx b/packages/mobile/src/account/Invite.tsx index afe0e90e638..c0c840e97ba 100644 --- a/packages/mobile/src/account/Invite.tsx +++ b/packages/mobile/src/account/Invite.tsx @@ -1,9 +1,11 @@ +import TextInput, { TextInputProps } from '@celo/react-components/components/TextInput' +import withTextInputLabeling from '@celo/react-components/components/WithTextInputLabeling' import colors from '@celo/react-components/styles/colors' import * as React from 'react' -import { withNamespaces, WithNamespaces } from 'react-i18next' -import { StyleSheet } from 'react-native' +import { WithTranslation } from 'react-i18next' +import { StyleSheet, View } from 'react-native' import SafeAreaView from 'react-native-safe-area-view' -import { NavigationInjectedProps, withNavigation } from 'react-navigation' +import { NavigationInjectedProps } from 'react-navigation' import { connect } from 'react-redux' import { defaultCountryCodeSelector } from 'src/account/reducer' import { hideAlert, showError } from 'src/alert/actions' @@ -11,7 +13,9 @@ import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' import { componentWithAnalytics } from 'src/analytics/wrapper' import { ErrorMessages } from 'src/app/ErrorMessages' -import i18n, { Namespaces } from 'src/i18n' +import i18n, { Namespaces, withTranslation } from 'src/i18n' +import ContactPermission from 'src/icons/ContactPermission' +import Search from 'src/icons/Search' import { importContacts } from 'src/identity/actions' import { e164NumberToAddressSelector, E164NumberToAddressType } from 'src/identity/reducer' import { headerWithCancelButton } from 'src/navigator/Headers' @@ -21,11 +25,15 @@ import { filterRecipients, NumberToRecipient, Recipient } from 'src/recipients/r import RecipientPicker from 'src/recipients/RecipientPicker' import { recipientCacheSelector } from 'src/recipients/reducer' import { RootState } from 'src/redux/reducers' -import { checkContactsPermission } from 'src/utils/permissions' +import { SendCallToAction } from 'src/send/SendCallToAction' +import { navigateToPhoneSettings } from 'src/utils/linking' +import { requestContactsPermission } from 'src/utils/permissions' + +const InviteSearchInput = withTextInputLabeling(TextInput) interface State { searchQuery: string - hasGivenPermission: boolean + hasGivenContactPermission: boolean } interface Section { @@ -51,7 +59,7 @@ const mapDispatchToProps = { importContacts, } -type Props = StateProps & DispatchProps & WithNamespaces & NavigationInjectedProps +type Props = StateProps & DispatchProps & WithTranslation & NavigationInjectedProps const mapStateToProps = (state: RootState): StateProps => ({ defaultCountryCode: defaultCountryCodeSelector(state), @@ -65,19 +73,30 @@ class Invite extends React.Component { headerTitle: i18n.t('sendFlow7:invite'), }) - state: State = { searchQuery: '', hasGivenPermission: true } + state: State = { searchQuery: '', hasGivenContactPermission: true } async componentDidMount() { - const granted = await checkContactsPermission() - this.setState({ hasGivenPermission: granted }) + await this.tryImportContacts() } - updateToField = (value: string) => { - this.setState({ searchQuery: value }) + tryImportContacts = async () => { + const { recipientCache } = this.props + + // If we've imported already + if (Object.keys(recipientCache).length) { + return + } + + const hasGivenContactPermission = await requestContactsPermission() + this.setState({ hasGivenContactPermission }) + + if (hasGivenContactPermission) { + this.props.importContacts() + } } onSearchQueryChanged = (searchQuery: string) => { - this.updateToField(searchQuery) + this.setState({ searchQuery }) } onSelectRecipient = (recipient: Recipient) => { @@ -90,6 +109,10 @@ class Invite extends React.Component { } } + onPressContactsSettings = () => { + navigateToPhoneSettings() + } + buildSections = (): Section[] => { const { t, recipientCache } = this.props // Only recipients without an address are invitable @@ -108,24 +131,43 @@ class Invite extends React.Component { })) .filter((section) => section.data.length > 0) } + renderListHeader = () => { + const { t } = this.props + const { hasGivenContactPermission } = this.state + + if (hasGivenContactPermission) { + return null + } - onPermissionsAccepted = async () => { - this.props.importContacts() - this.setState({ hasGivenPermission: true }) + return ( + } + header={t('importContactsCta.header')} + body={t('importContactsCta.body')} + cta={t('importContactsCta.cta')} + onPressCta={this.onPressContactsSettings} + /> + ) } render() { return ( + + } + placeholder={this.props.t('nameOrPhoneNumber')} + /> + ) @@ -137,11 +179,16 @@ const style = StyleSheet.create({ flex: 1, backgroundColor: colors.background, }, + textInputContainer: { + paddingBottom: 5, + borderBottomColor: colors.listBorder, + borderBottomWidth: 1, + }, }) export default componentWithAnalytics( connect( mapStateToProps, mapDispatchToProps - )(withNamespaces(Namespaces.sendFlow7)(withNavigation(Invite))) + )(withTranslation(Namespaces.sendFlow7)(Invite)) ) diff --git a/packages/mobile/src/account/InviteReview.tsx b/packages/mobile/src/account/InviteReview.tsx index 7e9efdf1ceb..4ba5e3a585c 100644 --- a/packages/mobile/src/account/InviteReview.tsx +++ b/packages/mobile/src/account/InviteReview.tsx @@ -5,7 +5,7 @@ import colors from '@celo/react-components/styles/colors' import { CURRENCY_ENUM } from '@celo/utils/src/currencies' import BigNumber from 'bignumber.js' import * as React from 'react' -import { withNamespaces, WithNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { ActivityIndicator, StyleSheet, View } from 'react-native' import SafeAreaView from 'react-native-safe-area-view' import { NavigationInjectedProps } from 'react-navigation' @@ -15,7 +15,7 @@ import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' import componentWithAnalytics from 'src/analytics/wrapper' import GethAwareButton from 'src/geth/GethAwareButton' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import SMSLogo from 'src/icons/InviteSendReceive' import WhatsAppLogo from 'src/icons/WhatsAppLogo' import { InviteBy, sendInvite } from 'src/invite/actions' @@ -31,7 +31,7 @@ interface State { amountIsValid: boolean } -type Props = StateProps & DispatchProps & NavigationInjectedProps & WithNamespaces +type Props = StateProps & DispatchProps & NavigationInjectedProps & WithTranslation interface StateProps { inviteInProgress: boolean @@ -206,5 +206,5 @@ export default componentWithAnalytics( connect( mapStateToProps, mapDispatchToProps - )(withNamespaces(Namespaces.inviteFlow11)(InviteReview)) + )(withTranslation(Namespaces.inviteFlow11)(InviteReview)) ) diff --git a/packages/mobile/src/account/Licenses.tsx b/packages/mobile/src/account/Licenses.tsx index 45f23ef589f..b15eb2b773b 100644 --- a/packages/mobile/src/account/Licenses.tsx +++ b/packages/mobile/src/account/Licenses.tsx @@ -1,9 +1,9 @@ import * as React from 'react' -import { withNamespaces, WithNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { Platform, StyleSheet } from 'react-native' import { WebView } from 'react-native-webview' import componentWithAnalytics from 'src/analytics/wrapper' -import i18n, { Namespaces } from 'src/i18n' +import i18n, { Namespaces, withTranslation } from 'src/i18n' import { headerWithBackButton } from 'src/navigator/Headers' const licenseURI = Platform.select({ @@ -11,7 +11,7 @@ const licenseURI = Platform.select({ android: 'file:///android_asset/custom/LicenseDisclaimer.txt', }) -type Props = {} & WithNamespaces +type Props = {} & WithTranslation class Licenses extends React.Component { static navigationOptions = () => ({ @@ -36,4 +36,4 @@ const styles = StyleSheet.create({ }, }) -export default componentWithAnalytics(withNamespaces(Namespaces.accountScreen10)(Licenses)) +export default componentWithAnalytics(withTranslation(Namespaces.accountScreen10)(Licenses)) diff --git a/packages/mobile/src/account/Profile.tsx b/packages/mobile/src/account/Profile.tsx index ee712ead8b5..e2a967b5aca 100644 --- a/packages/mobile/src/account/Profile.tsx +++ b/packages/mobile/src/account/Profile.tsx @@ -1,13 +1,13 @@ import ContactCircle from '@celo/react-components/components/ContactCircle' import * as React from 'react' -import { WithNamespaces, withNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { ScrollView, StyleSheet, View } from 'react-native' import { connect } from 'react-redux' import { UserContactDetails, userContactDetailsSelector } from 'src/account/reducer' import SettingsItem from 'src/account/SettingsItem' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { headerWithCancelButton } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' @@ -22,7 +22,7 @@ interface OwnProps { navigation: any } -type Props = OwnProps & StateProps & WithNamespaces +type Props = OwnProps & StateProps & WithTranslation const mapStateToProps = (state: RootState) => { return { name: state.account.name, @@ -88,5 +88,5 @@ const style = StyleSheet.create({ }) export default connect(mapStateToProps)( - withNamespaces(Namespaces.accountScreen10)(Profile) + withTranslation(Namespaces.accountScreen10)(Profile) ) diff --git a/packages/mobile/src/account/__snapshots__/Account.test.tsx.snap b/packages/mobile/src/account/__snapshots__/Account.test.tsx.snap index c51cb1dfc09..9ca50152262 100644 --- a/packages/mobile/src/account/__snapshots__/Account.test.tsx.snap +++ b/packages/mobile/src/account/__snapshots__/Account.test.tsx.snap @@ -2101,6 +2101,36 @@ exports[`Account renders correctly when dev mode active 1`] = ` } } > + + + + Toggle verification done + + + + + + + + - + - - + - - - - - - } - onBlur={[Function]} - onChangeText={[Function]} - onFocus={[Function]} - placeholder="nameOrPhoneNumber" - placeholderTextColor="rgba(0, 0, 0, .4)" - rejectResponderTermination={true} - shouldShowClipboard={[Function]} - style={ - Object { - "backgroundColor": "#FFFFFF", - "borderColor": "#D1D5D8", - "borderRadius": 3, - "flex": 1, - "fontFamily": "Hind-Regular", - "padding": 8, - } + Object { + "backgroundColor": "#FFFFFF", + "borderColor": "#D1D5D8", + "borderRadius": 3, + "flex": 1, + "fontFamily": "Hind-Regular", + "padding": 8, } - underlineColorAndroid="transparent" - value="" - /> - + } + underlineColorAndroid="transparent" + value="" + /> + + } + ListHeaderComponent={[Function]} data={Array []} disableVirtualization={false} getItem={[Function]} @@ -174,7 +172,7 @@ exports[`Invite renders correctly with no recipients 1`] = ` horizontal={false} initialNumToRender={30} keyExtractor={[Function]} - keyboardShouldPersistTaps="handled" + keyboardShouldPersistTaps="always" maxToRenderPerBatch={10} onContentSizeChange={[Function]} onEndReachedThreshold={2} @@ -193,6 +191,9 @@ exports[`Invite renders correctly with no recipients 1`] = ` windowSize={21} > + + + + + + - + - - + - - - - - - } - onBlur={[Function]} - onChangeText={[Function]} - onFocus={[Function]} - placeholder="nameOrPhoneNumber" - placeholderTextColor="rgba(0, 0, 0, .4)" - rejectResponderTermination={true} - shouldShowClipboard={[Function]} - style={ - Object { - "backgroundColor": "#FFFFFF", - "borderColor": "#D1D5D8", - "borderRadius": 3, - "flex": 1, - "fontFamily": "Hind-Regular", - "padding": 8, - } + Object { + "backgroundColor": "#FFFFFF", + "borderColor": "#D1D5D8", + "borderRadius": 3, + "flex": 1, + "fontFamily": "Hind-Regular", + "padding": 8, } - underlineColorAndroid="transparent" - value="" - /> - + } + underlineColorAndroid="transparent" + value="" + /> + + } - ListFooterComponent={[Function]} + ListHeaderComponent={[Function]} data={ Array [ Object { @@ -423,7 +421,7 @@ exports[`Invite renders correctly with recipients 1`] = ` horizontal={false} initialNumToRender={30} keyExtractor={[Function]} - keyboardShouldPersistTaps="handled" + keyboardShouldPersistTaps="always" maxToRenderPerBatch={10} onContentSizeChange={[Function]} onEndReachedThreshold={2} @@ -458,6 +456,9 @@ exports[`Invite renders correctly with recipients 1`] = ` windowSize={21} > + - - - - searchFriends - - diff --git a/packages/mobile/src/alert/__snapshots__/AlertBanner.test.tsx.snap b/packages/mobile/src/alert/__snapshots__/AlertBanner.test.tsx.snap index 09394d193b6..2c88e15bd2d 100644 --- a/packages/mobile/src/alert/__snapshots__/AlertBanner.test.tsx.snap +++ b/packages/mobile/src/alert/__snapshots__/AlertBanner.test.tsx.snap @@ -42,13 +42,14 @@ exports[`AlertBanner when error message passed in renders error message 1`] = ` "marginHorizontal": 5, } } + testID="ErrorIcon" > { showRestartButtonTimer: number | null = null @@ -77,4 +77,4 @@ const styles = StyleSheet.create({ }, }) -export default withNamespaces(Namespaces.global)(AppLoading) +export default withTranslation(Namespaces.global)(AppLoading) diff --git a/packages/mobile/src/app/ErrorBoundary.tsx b/packages/mobile/src/app/ErrorBoundary.tsx index 8344b9f57ac..091e0f3801d 100644 --- a/packages/mobile/src/app/ErrorBoundary.tsx +++ b/packages/mobile/src/app/ErrorBoundary.tsx @@ -1,11 +1,11 @@ import { getErrorMessage } from '@celo/utils/src/displayFormatting' import * as Sentry from '@sentry/react-native' import * as React from 'react' -import { withNamespaces, WithNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { DefaultEventNames } from 'src/analytics/constants' import ErrorScreen from 'src/app/ErrorScreen' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' interface State { childError: Error | null @@ -15,7 +15,7 @@ interface OwnProps { children: React.ReactChild } -type Props = OwnProps & WithNamespaces +type Props = OwnProps & WithTranslation class ErrorBoundary extends React.Component { state: State = { @@ -38,4 +38,4 @@ class ErrorBoundary extends React.Component { } } -export default withNamespaces(Namespaces.global)(ErrorBoundary) +export default withTranslation(Namespaces.global)(ErrorBoundary) diff --git a/packages/mobile/src/app/ErrorScreen.tsx b/packages/mobile/src/app/ErrorScreen.tsx index 2362a44b945..b3cc8b583bb 100644 --- a/packages/mobile/src/app/ErrorScreen.tsx +++ b/packages/mobile/src/app/ErrorScreen.tsx @@ -1,10 +1,10 @@ import FullscreenCTA from '@celo/react-components/components/FullscreenCTA' import { componentStyles } from '@celo/react-components/styles/styles' import * as React from 'react' -import { withNamespaces, WithNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { Text, View } from 'react-native' import { NavigationParams, NavigationScreenProp } from 'react-navigation' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { deleteChainDataAndRestartApp, RESTART_APP_I18N_KEY } from 'src/utils/AppRestart' interface OwnProps { @@ -12,7 +12,7 @@ interface OwnProps { navigation?: NavigationScreenProp } -type Props = OwnProps & WithNamespaces +type Props = OwnProps & WithTranslation class ErrorScreen extends React.Component { static navigationOptions = { header: null } @@ -45,4 +45,4 @@ class ErrorScreen extends React.Component { } } -export default withNamespaces(Namespaces.global)(ErrorScreen) +export default withTranslation(Namespaces.global)(ErrorScreen) diff --git a/packages/mobile/src/app/UpgradeScreen.tsx b/packages/mobile/src/app/UpgradeScreen.tsx index 82ebddb9521..9ce24dcdba0 100644 --- a/packages/mobile/src/app/UpgradeScreen.tsx +++ b/packages/mobile/src/app/UpgradeScreen.tsx @@ -1,8 +1,8 @@ import FullscreenCTA from '@celo/react-components/components/FullscreenCTA' import * as React from 'react' -import { withNamespaces, WithNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { NavigationParams, NavigationScreenProp } from 'react-navigation' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { navigateToWalletPlayStorePage } from 'src/utils/linking' interface OwnProps { @@ -10,7 +10,7 @@ interface OwnProps { navigation?: NavigationScreenProp } -type Props = OwnProps & WithNamespaces +type Props = OwnProps & WithTranslation class UpgradeScreen extends React.Component { static navigationOptions = { header: null } @@ -28,4 +28,4 @@ class UpgradeScreen extends React.Component { } } -export default withNamespaces(Namespaces.global)(UpgradeScreen) +export default withTranslation(Namespaces.global)(UpgradeScreen) diff --git a/packages/mobile/src/app/actions.ts b/packages/mobile/src/app/actions.ts index 997cfa5afcc..45b19dd2f61 100644 --- a/packages/mobile/src/app/actions.ts +++ b/packages/mobile/src/app/actions.ts @@ -2,10 +2,12 @@ import { NavigationParams } from 'react-navigation' import i18n from 'src/i18n' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' - +import Logger from 'src/utils/Logger' const numeral = require('numeral') require('numeral/locales/es') +const TAG = 'app/actions' + export enum Actions { SET_LOGGED_IN = 'APP/SET_LOGGED_IN', SET_NUMBER_VERIFIED = 'APP/SET_NUMBER_VERIFIED', @@ -97,7 +99,9 @@ export const setNumberVerified = (numberVerified: boolean) => ({ export const setLanguage = (language: string, nextScreen?: Screens) => { numeral.locale(language.substring(0, 2)) - i18n.changeLanguage(language) + i18n + .changeLanguage(language) + .catch((reason: any) => Logger.error(TAG, 'Failed to change i18n language', reason)) if (nextScreen) { navigate(nextScreen) diff --git a/packages/mobile/src/app/saga.ts b/packages/mobile/src/app/saga.ts index b7539178246..183f9f1bfa6 100644 --- a/packages/mobile/src/app/saga.ts +++ b/packages/mobile/src/app/saga.ts @@ -43,7 +43,6 @@ interface PersistedStateProps { redeemComplete: boolean account: string | null hasSeenVerificationNux: boolean - askedContactsPermission: boolean } const mapStateToProps = (state: PersistedRootState): PersistedStateProps | null => { @@ -57,7 +56,6 @@ const mapStateToProps = (state: PersistedRootState): PersistedStateProps | null redeemComplete: state.invite.redeemComplete, account: state.web3.account, hasSeenVerificationNux: state.identity.hasSeenVerificationNux, - askedContactsPermission: state.identity.askedContactsPermission, } } @@ -103,7 +101,6 @@ export function* navigateToProperScreen() { redeemComplete, account, hasSeenVerificationNux, - askedContactsPermission, } = mappedState if (language) { @@ -125,8 +122,6 @@ export function* navigateToProperScreen() { navigate(Screens.PincodeEducation) } else if (!redeemComplete && !account) { navigate(Screens.EnterInviteCode) - } else if (!askedContactsPermission) { - navigate(Screens.ImportContacts) } else if (!hasSeenVerificationNux) { navigate(Screens.VerificationEducationScreen) } else { diff --git a/packages/mobile/src/backup/BackupComplete.tsx b/packages/mobile/src/backup/BackupComplete.tsx index 0f804109832..47c777ded09 100644 --- a/packages/mobile/src/backup/BackupComplete.tsx +++ b/packages/mobile/src/backup/BackupComplete.tsx @@ -1,13 +1,13 @@ import colors from '@celo/react-components/styles/colors' import { fontStyles } from '@celo/react-components/styles/fonts' import * as React from 'react' -import { WithNamespaces, withNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { StyleSheet, Text, View } from 'react-native' import SafeAreaView from 'react-native-safe-area-view' import { connect } from 'react-redux' import componentWithAnalytics from 'src/analytics/wrapper' import { exitBackupFlow } from 'src/app/actions' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import NuxLogo from 'src/icons/NuxLogo' import { navigate, navigateHome } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' @@ -22,7 +22,7 @@ interface DispatchProps { exitBackupFlow: typeof exitBackupFlow } -type Props = StateProps & DispatchProps & WithNamespaces +type Props = StateProps & DispatchProps & WithTranslation const mapStateToProps = (state: RootState): StateProps => { return { @@ -97,5 +97,5 @@ export default componentWithAnalytics( { exitBackupFlow, } - )(withNamespaces(Namespaces.backupKeyFlow6)(BackupComplete)) + )(withTranslation(Namespaces.backupKeyFlow6)(BackupComplete)) ) diff --git a/packages/mobile/src/backup/BackupIntroduction.tsx b/packages/mobile/src/backup/BackupIntroduction.tsx index d619ce0d7b6..d275acaacf3 100644 --- a/packages/mobile/src/backup/BackupIntroduction.tsx +++ b/packages/mobile/src/backup/BackupIntroduction.tsx @@ -2,7 +2,7 @@ import Button, { BtnTypes } from '@celo/react-components/components/Button' import colors from '@celo/react-components/styles/colors' import { fontStyles } from '@celo/react-components/styles/fonts' import * as React from 'react' -import { WithNamespaces, withNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { ActivityIndicator, Image, ScrollView, StyleSheet, Text, View } from 'react-native' import SafeAreaView from 'react-native-safe-area-view' import { connect } from 'react-redux' @@ -11,7 +11,7 @@ import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' import componentWithAnalytics from 'src/analytics/wrapper' import { enterBackupFlow, exitBackupFlow, navigatePinProtected } from 'src/app/actions' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import backupIcon from 'src/images/backup-icon.png' import { headerWithBackButton } from 'src/navigator/Headers' import { navigate, navigateBack } from 'src/navigator/NavigationService' @@ -34,7 +34,7 @@ interface DispatchProps { navigatePinProtected: typeof navigatePinProtected } -type Props = WithNamespaces & StateProps & DispatchProps +type Props = WithTranslation & StateProps & DispatchProps const mapStateToProps = (state: RootState): StateProps => { return { @@ -234,5 +234,5 @@ export default componentWithAnalytics( exitBackupFlow, navigatePinProtected, } - )(withNamespaces(Namespaces.backupKeyFlow6)(BackupIntroduction)) + )(withTranslation(Namespaces.backupKeyFlow6)(BackupIntroduction)) ) diff --git a/packages/mobile/src/backup/BackupPhrase.tsx b/packages/mobile/src/backup/BackupPhrase.tsx index 5baaae6bc26..39f2cda6879 100644 --- a/packages/mobile/src/backup/BackupPhrase.tsx +++ b/packages/mobile/src/backup/BackupPhrase.tsx @@ -3,7 +3,7 @@ import Switch from '@celo/react-components/components/Switch' import colors from '@celo/react-components/styles/colors' import { fontStyles } from '@celo/react-components/styles/fonts' import * as React from 'react' -import { WithNamespaces, withNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { ScrollView, StyleSheet, Text, View } from 'react-native' import SafeAreaView from 'react-native-safe-area-view' import { connect } from 'react-redux' @@ -17,7 +17,7 @@ import BackupPhraseContainer, { BackupPhraseType, } from 'src/backup/BackupPhraseContainer' import { getStoredMnemonic } from 'src/backup/utils' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { headerWithBackButton } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' @@ -38,7 +38,7 @@ interface DispatchProps { hideAlert: typeof hideAlert } -type Props = StateProps & DispatchProps & WithNamespaces +type Props = StateProps & DispatchProps & WithTranslation const mapStateToProps = (state: RootState): StateProps => { return { @@ -173,5 +173,5 @@ export default componentWithAnalytics( connect( mapStateToProps, { showError, hideAlert } - )(withNamespaces(Namespaces.backupKeyFlow6)(BackupPhrase)) + )(withTranslation(Namespaces.backupKeyFlow6)(BackupPhrase)) ) diff --git a/packages/mobile/src/backup/BackupPhraseContainer.tsx b/packages/mobile/src/backup/BackupPhraseContainer.tsx index d016a75655a..de6daa99461 100644 --- a/packages/mobile/src/backup/BackupPhraseContainer.tsx +++ b/packages/mobile/src/backup/BackupPhraseContainer.tsx @@ -3,11 +3,11 @@ import withTextInputPasteAware from '@celo/react-components/components/WithTextI import colors from '@celo/react-components/styles/colors' import { fontStyles } from '@celo/react-components/styles/fonts' import * as React from 'react' -import { WithNamespaces, withNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { Clipboard, Platform, StyleSheet, Text, TextInput, View, ViewStyle } from 'react-native' import FlagSecure from 'react-native-flag-secure-android' import { isValidBackupPhrase, isValidSocialBackupPhrase } from 'src/backup/utils' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import Logger from 'src/utils/Logger' const PhraseInput = withTextInputPasteAware(TextInput, { top: undefined, right: 12, bottom: 12 }) @@ -31,7 +31,7 @@ type Props = { style?: ViewStyle onChangeText?: (value: string) => void testID?: string -} & WithNamespaces +} & WithTranslation export class BackupPhraseContainer extends React.Component { async componentDidMount() { @@ -166,4 +166,4 @@ const styles = StyleSheet.create({ }, }) -export default withNamespaces(Namespaces.backupKeyFlow6)(BackupPhraseContainer) +export default withTranslation(Namespaces.backupKeyFlow6)(BackupPhraseContainer) diff --git a/packages/mobile/src/backup/BackupQuiz.tsx b/packages/mobile/src/backup/BackupQuiz.tsx index ade48a2e64c..17ee7b6d746 100644 --- a/packages/mobile/src/backup/BackupQuiz.tsx +++ b/packages/mobile/src/backup/BackupQuiz.tsx @@ -6,7 +6,7 @@ import colors from '@celo/react-components/styles/colors' import fontStyles from '@celo/react-components/styles/fonts' import * as _ from 'lodash' import * as React from 'react' -import { withNamespaces, WithNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { ScrollView, StyleSheet, Text, View } from 'react-native' import SafeAreaView from 'react-native-safe-area-view' import { NavigationInjectedProps } from 'react-navigation' @@ -16,7 +16,7 @@ import { showError } from 'src/alert/actions' import componentWithAnalytics from 'src/analytics/wrapper' import { ErrorMessages } from 'src/app/ErrorMessages' import DevSkipButton from 'src/components/DevSkipButton' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { headerWithBackButton } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' @@ -41,7 +41,7 @@ interface DispatchProps { showError: typeof showError } -type Props = WithNamespaces & DispatchProps & NavigationInjectedProps +type Props = WithTranslation & DispatchProps & NavigationInjectedProps export class BackupQuiz extends React.Component { static navigationOptions = () => ({ @@ -219,7 +219,7 @@ export default componentWithAnalytics( connect<{}, DispatchProps, {}, RootState>( null, { setBackupCompleted, showError } - )(withNamespaces(Namespaces.backupKeyFlow6)(BackupQuiz)) + )(withTranslation(Namespaces.backupKeyFlow6)(BackupQuiz)) ) const styles = StyleSheet.create({ diff --git a/packages/mobile/src/backup/BackupSocial.tsx b/packages/mobile/src/backup/BackupSocial.tsx index 6c8fcb9e795..1f4c5b47ca8 100644 --- a/packages/mobile/src/backup/BackupSocial.tsx +++ b/packages/mobile/src/backup/BackupSocial.tsx @@ -4,7 +4,7 @@ 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 { WithTranslation } from 'react-i18next' import { ScrollView, StyleSheet, Text, View } from 'react-native' import SafeAreaView from 'react-native-safe-area-view' import { connect } from 'react-redux' @@ -17,7 +17,7 @@ import BackupPhraseContainer, { BackupPhraseType, } from 'src/backup/BackupPhraseContainer' import { getStoredMnemonic, splitMnemonic } from 'src/backup/utils' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { headerWithBackButton } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' @@ -40,7 +40,7 @@ interface DispatchProps { showError: typeof showError } -type Props = WithNamespaces & StateProps & DispatchProps +type Props = WithTranslation & StateProps & DispatchProps const mapStateToProps = (state: RootState): StateProps => { return { @@ -185,5 +185,5 @@ export default componentWithAnalytics( setSocialBackupCompleted, showError, } - )(withNamespaces(Namespaces.backupKeyFlow6)(BackupSocial)) + )(withTranslation(Namespaces.backupKeyFlow6)(BackupSocial)) ) diff --git a/packages/mobile/src/backup/BackupSocialIntro.tsx b/packages/mobile/src/backup/BackupSocialIntro.tsx index 43d4ddef689..dd47b2cd1d4 100644 --- a/packages/mobile/src/backup/BackupSocialIntro.tsx +++ b/packages/mobile/src/backup/BackupSocialIntro.tsx @@ -2,7 +2,7 @@ import Button, { BtnTypes } from '@celo/react-components/components/Button' import colors from '@celo/react-components/styles/colors' import { fontStyles } from '@celo/react-components/styles/fonts' import * as React from 'react' -import { WithNamespaces, withNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { ActivityIndicator, Image, ScrollView, StyleSheet, Text } from 'react-native' import SafeAreaView from 'react-native-safe-area-view' import { NavigationInjectedProps } from 'react-navigation' @@ -11,7 +11,7 @@ import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' import componentWithAnalytics from 'src/analytics/wrapper' import { exitBackupFlow, navigatePinProtected } from 'src/app/actions' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import backupIcon from 'src/images/backup-icon.png' import { headerWithBackButton } from 'src/navigator/Headers' import { navigate, navigateHome } from 'src/navigator/NavigationService' @@ -31,7 +31,7 @@ interface NavigationProps { incomingFromBackupFlow: boolean } -type Props = WithNamespaces & StateProps & DispatchProps & NavigationInjectedProps +type Props = WithTranslation & StateProps & DispatchProps & NavigationInjectedProps const mapStateToProps = (state: RootState): StateProps => { return { @@ -133,5 +133,5 @@ export default componentWithAnalytics( exitBackupFlow, navigatePinProtected, } - )(withNamespaces(Namespaces.backupKeyFlow6)(BackupSocialIntro)) + )(withTranslation(Namespaces.backupKeyFlow6)(BackupSocialIntro)) ) diff --git a/packages/mobile/src/backup/__snapshots__/BackupPhraseContainer.test.tsx.snap b/packages/mobile/src/backup/__snapshots__/BackupPhraseContainer.test.tsx.snap index 0ae35036d8b..19fb573c020 100644 --- a/packages/mobile/src/backup/__snapshots__/BackupPhraseContainer.test.tsx.snap +++ b/packages/mobile/src/backup/__snapshots__/BackupPhraseContainer.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`wrapper renders correctly for input backup phrase 1`] = ` +exports[`renders correctly for input backup phrase 1`] = ` `; -exports[`wrapper renders correctly for input social backup phrase 1`] = ` +exports[`renders correctly for input social backup phrase 1`] = ` `; -exports[`wrapper renders correctly for readonly backup phrase 1`] = ` +exports[`renders correctly for readonly backup phrase 1`] = ` `; -exports[`wrapper renders correctly for readonly social backup phrase 1`] = ` +exports[`renders correctly for readonly social backup phrase 1`] = ` { return { @@ -180,5 +180,5 @@ export default componentWithAnalytics( connect( mapStateToProps, { startBalanceAutorefresh, stopBalanceAutorefresh } - )(withNamespaces(Namespaces.walletFlow5)(AccountOverview)) + )(withTranslation(Namespaces.walletFlow5)(AccountOverview)) ) diff --git a/packages/mobile/src/components/Avatar.tsx b/packages/mobile/src/components/Avatar.tsx index 4e64423131a..f5e103f5217 100644 --- a/packages/mobile/src/components/Avatar.tsx +++ b/packages/mobile/src/components/Avatar.tsx @@ -6,11 +6,11 @@ import { Avatar as BaseAvatar } from '@celo/react-components/components/Avatar' import * as React from 'react' -import { withNamespaces, WithNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { Image, StyleSheet } from 'react-native' import { useSelector } from 'react-redux' import { defaultCountryCodeSelector } from 'src/account/reducer' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { unknownUserIcon } from 'src/images/Images' import { getRecipientThumbnail, Recipient } from 'src/recipients/recipient' @@ -24,7 +24,7 @@ interface OwnProps { iconSize?: number } -type Props = OwnProps & WithNamespaces +type Props = OwnProps & WithTranslation function getDisplayName({ name, recipient, e164Number, address, t }: Props) { if (name) { @@ -75,4 +75,4 @@ const style = StyleSheet.create({ }, }) -export default withNamespaces(Namespaces.sendFlow7)(Avatar) +export default withTranslation(Namespaces.sendFlow7)(Avatar) diff --git a/packages/mobile/src/components/CancelButton.tsx b/packages/mobile/src/components/CancelButton.tsx index a1695fb5f7e..f5e5ceb9156 100644 --- a/packages/mobile/src/components/CancelButton.tsx +++ b/packages/mobile/src/components/CancelButton.tsx @@ -1,17 +1,17 @@ import Touchable from '@celo/react-components/components/Touchable' import { fontStyles } from '@celo/react-components/styles/fonts' import * as React from 'react' -import { withNamespaces, WithNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { StyleSheet, Text } from 'react-native' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { navigateBack } from 'src/navigator/NavigationService' type Props = { eventName?: CustomEventNames onCancel?: () => void -} & WithNamespaces +} & WithTranslation class CancelButton extends React.PureComponent { cancel = () => { @@ -47,4 +47,4 @@ const styles = StyleSheet.create({ }, }) -export default withNamespaces(Namespaces.global)(CancelButton) +export default withTranslation(Namespaces.global)(CancelButton) diff --git a/packages/mobile/src/components/CodeRow.tsx b/packages/mobile/src/components/CodeRow.tsx index 152c82fe1f0..001523f202c 100644 --- a/packages/mobile/src/components/CodeRow.tsx +++ b/packages/mobile/src/components/CodeRow.tsx @@ -5,9 +5,9 @@ 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 { WithTranslation } from 'react-i18next' import { ActivityIndicator, StyleSheet, Text, View } from 'react-native' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' const CodeInput = withTextInputPasteAware(TextInput) @@ -27,7 +27,7 @@ export interface CodeRowProps { shouldShowClipboard: (value: string) => boolean } -type Props = CodeRowProps & WithNamespaces +type Props = CodeRowProps & WithTranslation function CodeRow({ status, @@ -152,4 +152,4 @@ const styles = StyleSheet.create({ }, }) -export default withNamespaces(Namespaces.global)(CodeRow) +export default withTranslation(Namespaces.global)(CodeRow) diff --git a/packages/mobile/src/dappkit/DappKitAccountScreen.tsx b/packages/mobile/src/dappkit/DappKitAccountScreen.tsx index 204e65c4cec..4f40c5aaf1f 100644 --- a/packages/mobile/src/dappkit/DappKitAccountScreen.tsx +++ b/packages/mobile/src/dappkit/DappKitAccountScreen.tsx @@ -3,14 +3,14 @@ import colors from '@celo/react-components/styles/colors' import fontStyles from '@celo/react-components/styles/fonts' import { AccountAuthRequest } from '@celo/utils/src/dappkit' import * as React from 'react' -import { withNamespaces, WithNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { ScrollView, StyleSheet, Text, View } from 'react-native' import SafeAreaView from 'react-native-safe-area-view' import { NavigationParams, NavigationScreenProp } from 'react-navigation' import { connect } from 'react-redux' import { e164NumberSelector } from 'src/account/reducer' import { approveAccountAuth } from 'src/dappkit/dappkit' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import DappkitExchangeIcon from 'src/icons/DappkitExchange' import { navigateBack, navigateHome } from 'src/navigator/NavigationService' import { RootState } from 'src/redux/reducers' @@ -32,7 +32,7 @@ interface StateProps { phoneNumber: string | null } -type Props = OwnProps & StateProps & WithNamespaces +type Props = OwnProps & StateProps & WithTranslation const mapStateToProps = (state: RootState): StateProps => ({ account: currentAccountSelector(state), @@ -186,5 +186,5 @@ const styles = StyleSheet.create({ }) export default connect(mapStateToProps)( - withNamespaces(Namespaces.dappkit)(DappKitAccountAuthScreen) + withTranslation(Namespaces.dappkit)(DappKitAccountAuthScreen) ) diff --git a/packages/mobile/src/dappkit/DappKitSignTxScreen.tsx b/packages/mobile/src/dappkit/DappKitSignTxScreen.tsx index b15cbae052a..353d7a6f29c 100644 --- a/packages/mobile/src/dappkit/DappKitSignTxScreen.tsx +++ b/packages/mobile/src/dappkit/DappKitSignTxScreen.tsx @@ -3,13 +3,13 @@ import colors from '@celo/react-components/styles/colors' import fontStyles from '@celo/react-components/styles/fonts' import { SignTxRequest } from '@celo/utils/src/dappkit' import * as React from 'react' -import { withNamespaces, WithNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native' import SafeAreaView from 'react-native-safe-area-view' import { NavigationParams, NavigationScreenProp } from 'react-navigation' import { connect } from 'react-redux' import { requestTxSignature } from 'src/dappkit/dappkit' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import DappkitExchangeIcon from 'src/icons/DappkitExchange' import { navigate, navigateBack, navigateHome } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' @@ -30,7 +30,7 @@ interface DispatchProps { requestTxSignature: typeof requestTxSignature } -type Props = OwnProps & DispatchProps & WithNamespaces +type Props = OwnProps & DispatchProps & WithTranslation const mapDispatchToProps = { requestTxSignature, @@ -178,4 +178,4 @@ const styles = StyleSheet.create({ export default connect( null, mapDispatchToProps -)(withNamespaces(Namespaces.dappkit)(DappKitSignTxScreen)) +)(withTranslation(Namespaces.dappkit)(DappKitSignTxScreen)) diff --git a/packages/mobile/src/dappkit/DappKitTxDataScreen.tsx b/packages/mobile/src/dappkit/DappKitTxDataScreen.tsx index 3d6f6c72cbd..efaa5dfb3ad 100644 --- a/packages/mobile/src/dappkit/DappKitTxDataScreen.tsx +++ b/packages/mobile/src/dappkit/DappKitTxDataScreen.tsx @@ -1,14 +1,14 @@ import colors from '@celo/react-components/styles/colors' import fontStyles from '@celo/react-components/styles/fonts' import * as React from 'react' -import { WithNamespaces, withNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { ScrollView, StyleSheet, Text } from 'react-native' import SafeAreaView from 'react-native-safe-area-view' import { NavigationInjectedProps } from 'react-navigation' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { headerWithBackButton } from 'src/navigator/Headers' -type Props = NavigationInjectedProps & WithNamespaces +type Props = NavigationInjectedProps & WithTranslation class DappKitTxDataScreen extends React.Component { static navigationOptions = headerWithBackButton @@ -51,4 +51,4 @@ const styles = StyleSheet.create({ }, }) -export default withNamespaces(Namespaces.dappkit)(DappKitTxDataScreen) +export default withTranslation(Namespaces.dappkit)(DappKitTxDataScreen) diff --git a/packages/mobile/src/escrow/EscrowedPaymentLineItem.tsx b/packages/mobile/src/escrow/EscrowedPaymentLineItem.tsx index 2a6763e4e63..dcb37ac42e9 100644 --- a/packages/mobile/src/escrow/EscrowedPaymentLineItem.tsx +++ b/packages/mobile/src/escrow/EscrowedPaymentLineItem.tsx @@ -1,6 +1,6 @@ import fontStyles from '@celo/react-components/styles/fonts' import * as React from 'react' -import { Trans, withNamespaces, WithNamespaces } from 'react-i18next' +import { Trans, WithTranslation, withTranslation } from 'react-i18next' import { StyleSheet, Text } from 'react-native' import { EscrowedPayment } from 'src/escrow/actions' import { CURRENCIES, CURRENCY_ENUM } from 'src/geth/consts' @@ -11,7 +11,7 @@ interface Props { payment: EscrowedPayment } -function EscrowedPaymentLineItem(props: Props & WithNamespaces) { +function EscrowedPaymentLineItem(props: Props & WithTranslation) { const { amount, recipientPhone } = props.payment return ( @@ -44,4 +44,4 @@ const styles = StyleSheet.create({ }, }) -export default withNamespaces(Namespaces.inviteFlow11)(EscrowedPaymentLineItem) +export default withTranslation(Namespaces.inviteFlow11)(EscrowedPaymentLineItem) diff --git a/packages/mobile/src/escrow/EscrowedPaymentListItem.tsx b/packages/mobile/src/escrow/EscrowedPaymentListItem.tsx index 3595231f66c..809bad3b995 100644 --- a/packages/mobile/src/escrow/EscrowedPaymentListItem.tsx +++ b/packages/mobile/src/escrow/EscrowedPaymentListItem.tsx @@ -1,7 +1,7 @@ import BaseNotification from '@celo/react-components/components/BaseNotification' import fontStyles from '@celo/react-components/styles/fonts' import * as React from 'react' -import { WithNamespaces, withNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { Image, Platform, StyleSheet, Text, View } from 'react-native' import SendIntentAndroid from 'react-native-send-intent' import CeloAnalytics from 'src/analytics/CeloAnalytics' @@ -10,7 +10,7 @@ import { componentWithAnalytics } from 'src/analytics/wrapper' import { ErrorMessages } from 'src/app/ErrorMessages' import { EscrowedPayment } from 'src/escrow/actions' import { CURRENCIES, CURRENCY_ENUM } from 'src/geth/consts' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { inviteFriendsIcon } from 'src/images/Images' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' @@ -22,7 +22,7 @@ interface OwnProps { payment: EscrowedPayment } -type Props = OwnProps & WithNamespaces +type Props = OwnProps & WithTranslation const TAG = 'EscrowedPaymentListItem' @@ -111,5 +111,5 @@ const styles = StyleSheet.create({ }) export default componentWithAnalytics( - withNamespaces(Namespaces.inviteFlow11)(EscrowedPaymentListItem) + withTranslation(Namespaces.inviteFlow11)(EscrowedPaymentListItem) ) diff --git a/packages/mobile/src/escrow/EscrowedPaymentListScreen.tsx b/packages/mobile/src/escrow/EscrowedPaymentListScreen.tsx index 429216982e4..b2656f05a04 100644 --- a/packages/mobile/src/escrow/EscrowedPaymentListScreen.tsx +++ b/packages/mobile/src/escrow/EscrowedPaymentListScreen.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { WithNamespaces, withNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { View } from 'react-native' import { NavigationInjectedProps } from 'react-navigation' import { connect } from 'react-redux' @@ -7,7 +7,7 @@ import { EscrowedPayment } from 'src/escrow/actions' import EscrowedPaymentListItem from 'src/escrow/EscrowedPaymentListItem' import { getReclaimableEscrowPayments } from 'src/escrow/saga' import { updatePaymentRequestStatus } from 'src/firebase/actions' -import i18n, { Namespaces } from 'src/i18n' +import i18n, { Namespaces, withTranslation } from 'src/i18n' import { fetchPhoneAddresses } from 'src/identity/actions' import { e164NumberToAddressSelector, E164NumberToAddressType } from 'src/identity/reducer' import { @@ -38,7 +38,7 @@ const mapStateToProps = (state: RootState): StateProps => ({ recipientCache: recipientCacheSelector(state), }) -type Props = NavigationInjectedProps & WithNamespaces & StateProps & DispatchProps +type Props = NavigationInjectedProps & WithTranslation & StateProps & DispatchProps export const listItemRenderer = (payment: EscrowedPayment, key: number | undefined = undefined) => { return ( @@ -70,4 +70,4 @@ export default connect( updatePaymentRequestStatus, fetchPhoneAddresses, } -)(withNamespaces(Namespaces.global)(EscrowedPaymentListScreen)) +)(withTranslation(Namespaces.global)(EscrowedPaymentListScreen)) diff --git a/packages/mobile/src/escrow/EscrowedPaymentReminderSummaryNotification.tsx b/packages/mobile/src/escrow/EscrowedPaymentReminderSummaryNotification.tsx index 9c929d851f8..98902c50e9d 100644 --- a/packages/mobile/src/escrow/EscrowedPaymentReminderSummaryNotification.tsx +++ b/packages/mobile/src/escrow/EscrowedPaymentReminderSummaryNotification.tsx @@ -1,13 +1,13 @@ import variables from '@celo/react-components/styles/variables' import * as React from 'react' -import { WithNamespaces, withNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { Image, StyleSheet } from 'react-native' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' import { EscrowedPayment } from 'src/escrow/actions' import EscrowedPaymentLineItem from 'src/escrow/EscrowedPaymentLineItem' import { listItemRenderer } from 'src/escrow/EscrowedPaymentListScreen' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { inviteFriendsIcon } from 'src/images/Images' import { navigate } from 'src/navigator/NavigationService' import { Stacks } from 'src/navigator/Screens' @@ -17,7 +17,7 @@ interface OwnProps { payments: EscrowedPayment[] } -type Props = OwnProps & WithNamespaces +type Props = OwnProps & WithTranslation export class EscrowedPaymentReminderSummaryNotification extends React.Component { onReview = () => { @@ -63,4 +63,4 @@ const styles = StyleSheet.create({ }, }) -export default withNamespaces(Namespaces.walletFlow5)(EscrowedPaymentReminderSummaryNotification) +export default withTranslation(Namespaces.walletFlow5)(EscrowedPaymentReminderSummaryNotification) diff --git a/packages/mobile/src/escrow/ReclaimPaymentConfirmationCard.tsx b/packages/mobile/src/escrow/ReclaimPaymentConfirmationCard.tsx index a24d4915861..fef0064ad64 100644 --- a/packages/mobile/src/escrow/ReclaimPaymentConfirmationCard.tsx +++ b/packages/mobile/src/escrow/ReclaimPaymentConfirmationCard.tsx @@ -6,13 +6,13 @@ import { fontStyles } from '@celo/react-components/styles/fonts' import { componentStyles } from '@celo/react-components/styles/styles' import BigNumber from 'bignumber.js' import * as React from 'react' -import { withNamespaces, WithNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { StyleSheet, Text, View } from 'react-native' import { connect } from 'react-redux' import FeeIcon from 'src/components/FeeIcon' import LineItemRow from 'src/components/LineItemRow' import { CURRENCIES, CURRENCY_ENUM } from 'src/geth/consts' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import Logo from 'src/icons/Logo' import { RecipientWithContact } from 'src/recipients/recipient' import { RootState } from 'src/redux/reducers' @@ -38,7 +38,7 @@ const mapStateToProps = (state: RootState): StateProps => { } } -type Props = OwnProps & StateProps & WithNamespaces +type Props = OwnProps & StateProps & WithTranslation class ReclaimPaymentConfirmationCard extends React.PureComponent { render() { @@ -132,5 +132,5 @@ const style = StyleSheet.create({ }) export default connect(mapStateToProps)( - withNamespaces(Namespaces.sendFlow7)(ReclaimPaymentConfirmationCard) + withTranslation(Namespaces.sendFlow7)(ReclaimPaymentConfirmationCard) ) diff --git a/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.tsx b/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.tsx index 6668460d62f..694527f945b 100644 --- a/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.tsx +++ b/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.tsx @@ -3,7 +3,7 @@ import ReviewHeader from '@celo/react-components/components/ReviewHeader' import colors from '@celo/react-components/styles/colors' import { CURRENCY_ENUM } from '@celo/utils/src/currencies' import * as React from 'react' -import { withNamespaces, WithNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { ActivityIndicator, StyleSheet } from 'react-native' import SafeAreaView from 'react-native-safe-area-view' import { NavigationInjectedProps } from 'react-navigation' @@ -18,7 +18,7 @@ import ReclaimPaymentConfirmationCard from 'src/escrow/ReclaimPaymentConfirmatio import { FeeType } from 'src/fees/actions' import CalculateFee, { CalculateFeeChildren } from 'src/fees/CalculateFee' import { getFeeDollars } from 'src/fees/selectors' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { navigateBack } from 'src/navigator/NavigationService' import { RootState } from 'src/redux/reducers' import { isAppConnected } from 'src/redux/selectors' @@ -57,7 +57,7 @@ const mapStateToProps = (state: RootState): StateProps => { } } -type Props = NavigationInjectedProps & DispatchProps & StateProps & WithNamespaces +type Props = NavigationInjectedProps & DispatchProps & StateProps & WithTranslation class ReclaimPaymentConfirmationScreen extends React.Component { static navigationOptions = { header: null } @@ -177,5 +177,5 @@ export default componentWithAnalytics( connect( mapStateToProps, mapDispatchToProps - )(withNamespaces(Namespaces.sendFlow7)(ReclaimPaymentConfirmationScreen)) + )(withTranslation(Namespaces.sendFlow7)(ReclaimPaymentConfirmationScreen)) ) diff --git a/packages/mobile/src/escrow/saga.ts b/packages/mobile/src/escrow/saga.ts index 32d2ebb1187..bdec1dca65f 100644 --- a/packages/mobile/src/escrow/saga.ts +++ b/packages/mobile/src/escrow/saga.ts @@ -1,4 +1,4 @@ -import { ensureHexLeader } from '@celo/utils/src/address' +import { ensureLeading0x } from '@celo/utils/src/address' import { getEscrowContract, getStableTokenContract } from '@celo/walletkit' import { Escrow } from '@celo/walletkit/lib/types/Escrow' import { StableToken } from '@celo/walletkit/types/StableToken' @@ -145,7 +145,7 @@ function* withdrawFromEscrow() { signature = signature.slice(2) const r = `0x${signature.slice(0, 64)}` const s = `0x${signature.slice(64, 128)}` - const v = web3.utils.hexToNumber(ensureHexLeader(signature.slice(128, 130))) + const v = web3.utils.hexToNumber(ensureLeading0x(signature.slice(128, 130))) const withdrawTx = escrow.methods.withdraw(tempWalletAddress, v, r, s) const txID = generateStandbyTransactionId(account) diff --git a/packages/mobile/src/exchange/ExchangeConfirmationCard.tsx b/packages/mobile/src/exchange/ExchangeConfirmationCard.tsx index 48e6bd2c36b..190ada7b64c 100644 --- a/packages/mobile/src/exchange/ExchangeConfirmationCard.tsx +++ b/packages/mobile/src/exchange/ExchangeConfirmationCard.tsx @@ -2,14 +2,14 @@ import colors from '@celo/react-components/styles/colors' import { fontStyles } from '@celo/react-components/styles/fonts' import BigNumber from 'bignumber.js' import * as React from 'react' -import { withNamespaces, WithNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { StyleSheet, Text, View } from 'react-native' import CurrencyDisplay from 'src/components/CurrencyDisplay' import FeeIcon from 'src/components/FeeIcon' import LineItemRow from 'src/components/LineItemRow' import ExchangeRate from 'src/exchange/ExchangeRate' import { CURRENCY_ENUM } from 'src/geth/consts' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { useDollarsToLocalAmount, useLocalCurrencyCode, @@ -28,7 +28,7 @@ export interface ExchangeConfirmationCardProps { newGoldBalance?: BigNumber } -type Props = ExchangeConfirmationCardProps & WithNamespaces +type Props = ExchangeConfirmationCardProps & WithTranslation const getTakerToken = (props: Props) => { return props.makerToken === CURRENCY_ENUM.DOLLAR ? CURRENCY_ENUM.GOLD : CURRENCY_ENUM.DOLLAR @@ -202,4 +202,4 @@ const styles = StyleSheet.create({ }, }) -export default withNamespaces(Namespaces.exchangeFlow9)(ExchangeConfirmationCard) +export default withTranslation(Namespaces.exchangeFlow9)(ExchangeConfirmationCard) diff --git a/packages/mobile/src/exchange/ExchangeHomeScreen.tsx b/packages/mobile/src/exchange/ExchangeHomeScreen.tsx index 68913f0bbcd..a9aec08aed1 100644 --- a/packages/mobile/src/exchange/ExchangeHomeScreen.tsx +++ b/packages/mobile/src/exchange/ExchangeHomeScreen.tsx @@ -3,7 +3,7 @@ import ScrollContainer from '@celo/react-components/components/ScrollContainer' import SectionHeadNew from '@celo/react-components/components/SectionHeadNew' import BigNumber from 'bignumber.js' import * as React from 'react' -import { withNamespaces, WithNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { StyleSheet, View } from 'react-native' import SafeAreaView from 'react-native-safe-area-view' import { connect } from 'react-redux' @@ -13,7 +13,7 @@ import { fetchExchangeRate } from 'src/exchange/actions' import Activity from 'src/exchange/Activity' import ExchangeRate from 'src/exchange/ExchangeRate' import { CURRENCY_ENUM } from 'src/geth/consts' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { navigate } from 'src/navigator/NavigationService' import { Stacks } from 'src/navigator/Screens' import { RootState } from 'src/redux/reducers' @@ -30,7 +30,7 @@ interface DispatchProps { fetchExchangeRate: typeof fetchExchangeRate } -type Props = StateProps & DispatchProps & WithNamespaces +type Props = StateProps & DispatchProps & WithTranslation const mapStateToProps = (state: RootState): StateProps => ({ exchangeRate: getRateForMakerToken(state.exchange.exchangeRatePair, CURRENCY_ENUM.DOLLAR), @@ -111,7 +111,7 @@ export default componentWithAnalytics( { fetchExchangeRate, } - )(withNamespaces(Namespaces.exchangeFlow9)(ExchangeHomeScreen)) + )(withTranslation(Namespaces.exchangeFlow9)(ExchangeHomeScreen)) ) const styles = StyleSheet.create({ diff --git a/packages/mobile/src/exchange/ExchangeRate.tsx b/packages/mobile/src/exchange/ExchangeRate.tsx index 95521fdaba0..5fedf10d272 100644 --- a/packages/mobile/src/exchange/ExchangeRate.tsx +++ b/packages/mobile/src/exchange/ExchangeRate.tsx @@ -2,10 +2,10 @@ import colors from '@celo/react-components/styles/colors' import fontStyles from '@celo/react-components/styles/fonts' import BigNumber from 'bignumber.js' import * as React from 'react' -import { withNamespaces, WithNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { StyleSheet, Text, View } from 'react-native' import { CURRENCIES, CURRENCY_ENUM } from 'src/geth/consts' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { getExchangeRateDisplayValue } from 'src/utils/formatting' interface ExchangeRateProps { @@ -14,7 +14,7 @@ interface ExchangeRateProps { rate: BigNumber } -type Props = ExchangeRateProps & WithNamespaces +type Props = ExchangeRateProps & WithTranslation export class ExchangeRate extends React.PureComponent { render() { @@ -60,4 +60,4 @@ const styles = StyleSheet.create({ }, }) -export default withNamespaces(Namespaces.exchangeFlow9)(ExchangeRate) +export default withTranslation(Namespaces.exchangeFlow9)(ExchangeRate) diff --git a/packages/mobile/src/exchange/ExchangeReview.test.tsx b/packages/mobile/src/exchange/ExchangeReview.test.tsx index fe09b3aff3f..d6cc82eeb77 100644 --- a/packages/mobile/src/exchange/ExchangeReview.test.tsx +++ b/packages/mobile/src/exchange/ExchangeReview.test.tsx @@ -2,10 +2,10 @@ import BigNumber from 'bignumber.js' import * as React from 'react' import { Provider } from 'react-redux' import * as renderer from 'react-test-renderer' -import { ExchangeReview } from 'src/exchange/ExchangeReview' +import ExchangeReview from 'src/exchange/ExchangeReview' import { ExchangeRatePair } from 'src/exchange/reducer' import { CURRENCY_ENUM } from 'src/geth/consts' -import { createMockNavigationProp, createMockStore, getMockI18nProps } from 'test/utils' +import { createMockNavigationProp, createMockStore } from 'test/utils' const exchangeRatePair: ExchangeRatePair = { goldMaker: '0.11', dollarMaker: '10' } @@ -25,7 +25,7 @@ const store = createMockStore({ }, }) -describe(ExchangeReview, () => { +describe('ExchangeReview', () => { it('renders correctly', () => { const navigation = createMockNavigationProp({ makerToken: CURRENCY_ENUM.GOLD, @@ -43,7 +43,6 @@ describe(ExchangeReview, () => { fetchExchangeRate={jest.fn()} exchangeRatePair={exchangeRatePair} exchangeTokens={jest.fn()} - {...getMockI18nProps()} /> ) diff --git a/packages/mobile/src/exchange/ExchangeReview.tsx b/packages/mobile/src/exchange/ExchangeReview.tsx index 9cb976f62b2..979bed39243 100644 --- a/packages/mobile/src/exchange/ExchangeReview.tsx +++ b/packages/mobile/src/exchange/ExchangeReview.tsx @@ -5,7 +5,7 @@ import { fontStyles } from '@celo/react-components/styles/fonts' import { componentStyles } from '@celo/react-components/styles/styles' import BigNumber from 'bignumber.js' import * as React from 'react' -import { withNamespaces, WithNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { ScrollView, StyleSheet, Text, View } from 'react-native' import SafeAreaView from 'react-native-safe-area-view' import { NavigationInjectedProps } from 'react-navigation' @@ -17,7 +17,7 @@ import FeeIcon from 'src/components/FeeIcon' import { exchangeTokens, fetchExchangeRate } from 'src/exchange/actions' import { ExchangeRatePair } from 'src/exchange/reducer' import { CURRENCY_ENUM } from 'src/geth/consts' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { exchangeHeader } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' @@ -55,7 +55,7 @@ interface State { inputAmount: BigNumber } -type Props = StateProps & WithNamespaces & DispatchProps & NavigationInjectedProps +type Props = StateProps & WithTranslation & DispatchProps & NavigationInjectedProps const mapStateToProps = (state: RootState): StateProps => ({ exchangeRatePair: state.exchange.exchangeRatePair, @@ -273,5 +273,5 @@ export default componentWithAnalytics( connect( mapStateToProps, { exchangeTokens, fetchExchangeRate } - )(withNamespaces(Namespaces.exchangeFlow9)(ExchangeReview)) + )(withTranslation(Namespaces.exchangeFlow9)(ExchangeReview)) ) diff --git a/packages/mobile/src/exchange/ExchangeTradeScreen.tsx b/packages/mobile/src/exchange/ExchangeTradeScreen.tsx index 4350f8cd96c..041174e7cd2 100644 --- a/packages/mobile/src/exchange/ExchangeTradeScreen.tsx +++ b/packages/mobile/src/exchange/ExchangeTradeScreen.tsx @@ -1,13 +1,13 @@ import Button, { BtnTypes } from '@celo/react-components/components/Button' +import HorizontalLine from '@celo/react-components/components/HorizontalLine' 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 { parseInputAmount } from '@celo/utils/src/parsing' import BigNumber from 'bignumber.js' import * as React from 'react' -import { withNamespaces, WithNamespaces } from 'react-i18next' +import { WithTranslation } from 'react-i18next' import { StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native' import SafeAreaView from 'react-native-safe-area-view' import { NavigationInjectedProps } from 'react-navigation' @@ -20,7 +20,7 @@ import { DOLLAR_TRANSACTION_MIN_AMOUNT, GOLD_TRANSACTION_MIN_AMOUNT } from 'src/ import { fetchExchangeRate } from 'src/exchange/actions' import { ExchangeRatePair } from 'src/exchange/reducer' import { CURRENCIES, CURRENCY_ENUM } from 'src/geth/consts' -import { Namespaces } from 'src/i18n' +import { Namespaces, withTranslation } from 'src/i18n' import { exchangeHeader } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' @@ -54,7 +54,7 @@ interface DispatchProps { hideAlert: typeof hideAlert } -type Props = StateProps & DispatchProps & NavigationInjectedProps & WithNamespaces +type Props = StateProps & DispatchProps & NavigationInjectedProps & WithTranslation const mapStateToProps = (state: RootState): StateProps => ({ exchangeRatePair: state.exchange.exchangeRatePair, @@ -226,54 +226,44 @@ export class ExchangeTradeScreen extends React.Component { forceInset={{ top: 'never', bottom: 'always' }} style={styles.container} > - + - - - - - - - {t('exchangeAmount', { tokenName: this.getInputTokenDisplayText() })} - - - - {t('switchTo', { tokenName: this.getOppositeInputTokenDisplayText() })} - - - - - - - - - {t('inputSubtotal', { - goldOrSubtotal: this.isDollarInput() ? t('global:celoGold') : t('subtotal'), - rate: getMoneyDisplayValue(exchangeRateDisplay, CURRENCY_ENUM.DOLLAR, true), - })} + + + + {t('exchangeAmount', { tokenName: this.getInputTokenDisplayText() })} + + + + {t('switchTo', { tokenName: this.getOppositeInputTokenDisplayText() })} - {this.getSubtotalDisplayValue()} - + - - - + + + + {/* */} + + + {t('inputSubtotal', { + goldOrSubtotal: this.isDollarInput() ? t('global:celoGold') : t('subtotal'), + rate: getMoneyDisplayValue(exchangeRateDisplay, CURRENCY_ENUM.DOLLAR, true), + })} + + {this.getSubtotalDisplayValue()} + +