From 468ae16524de685a3398ad05d5c64bf8d780b1cb Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Wed, 11 Dec 2019 16:37:25 -0800 Subject: [PATCH 01/40] Add Twilio and attestation bot envs (#2194) --- .env.baklava | 5 +++++ .env.baklavastaging | 5 +++++ .env.mnemonic.baklava.enc | Bin 442 -> 608 bytes .env.mnemonic.baklavastaging.enc | Bin 397 -> 563 bytes 4 files changed, 10 insertions(+) diff --git a/.env.baklava b/.env.baklava index be10ac7c178..72a0c6aaa17 100644 --- a/.env.baklava +++ b/.env.baklava @@ -109,3 +109,8 @@ PROMTOSD_SCRAPE_INTERVAL="5m" PROMTOSD_EXPORT_INTERVAL="5m" SMS_RETRIEVER_HASH_CODE=l5k6LvdPDXS + +# Attestation Bot variables +ATTESTATION_BOT_INITIAL_WAIT_SECONDS=3 +ATTESTATION_BOT_IN_BETWEEN_WAIT_SECONDS=10 +ATTESTATION_BOT_MAX_ATTESTATIONS=3 diff --git a/.env.baklavastaging b/.env.baklavastaging index af4f5eba502..66ef872f316 100644 --- a/.env.baklavastaging +++ b/.env.baklavastaging @@ -108,3 +108,8 @@ PROMTOSD_SCRAPE_INTERVAL="5m" PROMTOSD_EXPORT_INTERVAL="5m" SMS_RETRIEVER_HASH_CODE=l5k6LvdPDXS + +# Attestation Bot variables +ATTESTATION_BOT_INITIAL_WAIT_SECONDS=3 +ATTESTATION_BOT_IN_BETWEEN_WAIT_SECONDS=10 +ATTESTATION_BOT_MAX_ATTESTATIONS=3 diff --git a/.env.mnemonic.baklava.enc b/.env.mnemonic.baklava.enc index 25754c1761efb2854cc06687d3a27f1a3e1f2484..05bfe011e91b34fc739714a689c72abbfb6cb3cd 100644 GIT binary patch literal 608 zcmV-m0-yZ~Bmi5&XOdbJ9>gQ60iaNba(FQuCvpGgK3Acgf+%TAe-ZGQ*Alk`02zV- zE@+-O4)9q10|_q`vdY@x*51KTttFV_ZWh;PFd0S9!47I{;~hJw^_#76wqT~=8(zAK zp>`S608YucRKJSx1Iq}Y@Us3lUcr3NWL7U?2yoBGH;|TG4Zeo3i}pQNU)vYL2T2x>&#Cd<^!_O-ce{ zyo2!Bd&ZC2^*9{(=&g)c>+|Rhh$_%}xr57kiI0a6DJMYUc5h$`wG!$k8Ga4xsEcrg z0KzF0=MaM(qm9BWNt77L-vCy2HEmUJ3eA^_;l+;xm6C!rp5r|9@pJgSU2eSXCz(5y$|due2Hu zE`$f;MGUz;wGSMhnk8>m43nN29m~vo(~2FmRt uuieCG+2xhlg8UJL>ULoJqjP8!g(;D*s=w*Lq3tC;q@8AW+#Z1**MCesfh6q! literal 442 zcmV;r0Y&}_Bmi5&XOcxZ-GA=7hRB8zaAs^?_8y)IyRru$)PG2nF7ek4q7sn<02zV- zF1&6onzZgAwBmTv(Tes0?e(L957^-`hC_E|Sv`d}B2Ab~t;}ju3(^{+PZqn{!>^kt zs5JGHAtO&JcVNTSuY$Eu-yiHJ5S!%;&Yebyzq&!#Dh?DBBCgz-Q9#&(>MyDnxzk_| zayO{*5UY3ZFtz9@-a1i=9Yddsm=tz&Kln!~x6V;<9GH$2O)`i;$M|d~3RP94?KZ?| zn5eqcW=W1DF0^08#MM}BMA!~ebswA)50gJ_a%8X8pwjsibQ;H=Mn%%8&gLZtcPQ}v z@YkGmy?uDcNzXEn@{(?p(juLB$aqVBR-GvHT4f|SAePHT_X5hW6V!M`Ux%e{nWYr7 zA3M{K;Z^QwR`f#~Nc3+T4*4YY3)5k~)rVut(Vb?hz%lVRwUd2Qav!!Hg*Kzo)Vjs> z+&GhB{Hv7_UT%5eNK&*2Sd=dx^HS;OntgH87rCPVphquE@at!F7}xfdLd$ kreanDRMAx>vp$p=!QZ<2vmCG6Ar8^m1)-?11@+&{dtq|gGynhq diff --git a/.env.mnemonic.baklavastaging.enc b/.env.mnemonic.baklavastaging.enc index 25835b6e7c2a80249bef5ebe2986b319a46c51f5..9c2429de1de442d137a6df6f1510094629bd2756 100644 GIT binary patch literal 563 zcmV-30?hpiBmi5&XOe{RQW;_f2C^ZbmCc||ULPohd~Z%lROEdHGr1}9_J z5ki$-nJuN|_SftwTP3}MSbxQ;8^LsAn}5K95a#!Z^*kmOMq?zF00ShBx$3KH(=ghImre*tNq|f0wHszj4i4DXmq-+B zDP(#`zE(F7L~d-I3yZ}0ELP`@-f$>sqQ8WxA}N8$A&&} zDiG6d8d002zV- zE@?GvL*()RuyGbSMtzDv5U0sk3*^16##)dkob%LWP4haGC^#d))H3lFqW-=LY$)H# zu0zYC>~=L<9CobLmIsa^h@?hCC_yA21-<(P3D&R{A|9$1A0|irK}wk(o*8l9ZdW{x zX(_m$P71xc!B7-q55He53i|(Nvl^0^b&R&NY)(njV9OaNcoD5SKj5}sbHJjcOKrE( z*3$pYS;qH_{AnNU+%}i>ls96O?l03Y<P%N8xQnGlYMrq{;@}Dmuq!P?!M7Y?iZVm?rE?GX{E8$V;5f^nnlpP3?lD0`_2&Uo72dnh From 8082dd5a8c96b9a86193d21dbca9a8d16a69ffd2 Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Wed, 11 Dec 2019 16:57:46 -0800 Subject: [PATCH 02/40] Indicate to run Twilio globally (#2193) --- packages/docs/getting-started/running-a-validator.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs/getting-started/running-a-validator.md b/packages/docs/getting-started/running-a-validator.md index 0d6971813bd..cddedde7b4b 100644 --- a/packages/docs/getting-started/running-a-validator.md +++ b/packages/docs/getting-started/running-a-validator.md @@ -568,7 +568,7 @@ Twilio is the most common and popular provider. For that you will need to provis | TWILIO_AUTH_TOKEN | The API authentication token | | TWILIO_BLACKLIST | A comma-sperated list of country codes you do not want to serve | -After you signed up for Twilio at [https://www.twilio.com/try-twilio](https://www.twilio.com/try-twilio), you should see your `ACCOUNT SID` and your `AUTH_TOKEN` in the top right of the console. You'll also want to enter in a credit card to fund the account. For most text messages, the costs will be very low. Find a more comprehensive price list at [https://www.twilio.com/sms/pricing](https://www.twilio.com/sms/pricing). If there are countries that you do not want to serve, you can specify them with the `TWILIO_BLACKLIST`. +After you signed up for Twilio at [https://www.twilio.com/try-twilio](https://www.twilio.com/try-twilio), you should see your `ACCOUNT SID` and your `AUTH_TOKEN` in the top right of the console. You'll also want to enter in a credit card to fund the account. For most text messages, the costs will be very low (and on mainnet easily exceeded by the attestation fee paid by the user). Find a more comprehensive price list at [https://www.twilio.com/sms/pricing](https://www.twilio.com/sms/pricing). If there are countries that you do not want to serve, you can specify them with the `TWILIO_BLACKLIST`. In any case, you'll want to adjust your Geo settings to serve phone numbers globally under [https://www.twilio.com/console/sms/settings/geo-permissions](https://www.twilio.com/console/sms/settings/geo-permissions). To actually be able to send SMS, you need to create a messaging service under [Programmable SMS > SMS](https://www.twilio.com/console/sms/services). The resulting `SID` you want to specify under the `TWILIO_MESSAGING_SERVICE_SID`. Now that you have provisioned your messaging service, you need to buy at least 1 phone number to send SMS from. You can do so under the `Numbers` option of the messaging service page. To maximize the chances of reliable and prompt SMS sending (and thus attestation fee revenue), you can buy numbers in many locales, and Twilio will intelligently select the best number to send each SMS. From df88ed5c02928550033758a10a07ce53f02298e2 Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Wed, 11 Dec 2019 17:15:32 -0800 Subject: [PATCH 03/40] Check sync status of attestation service (#2191) --- packages/attestation-service/src/db.ts | 29 +++++++++++++++++++--- packages/attestation-service/src/logger.ts | 8 +++--- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/packages/attestation-service/src/db.ts b/packages/attestation-service/src/db.ts index 06817c3275f..eacf80217c0 100644 --- a/packages/attestation-service/src/db.ts +++ b/packages/attestation-service/src/db.ts @@ -1,5 +1,6 @@ import { ContractKit, newKit } from '@celo/contractkit' import { FindOptions, Sequelize } from 'sequelize' +import { Block } from 'web3/eth/types' import { fetchEnv } from './env' import { rootLogger } from './logger' import Attestation, { AttestationModel, AttestationStatic } from './models/attestation' @@ -22,11 +23,31 @@ export let kit: ContractKit export async function initializeKit() { if (kit === undefined) { kit = newKit(fetchEnv('CELO_PROVIDER')) - const blockNumber = await kit.web3.eth.getBlockNumber() - if (blockNumber === 0) { - throw new Error( - 'Could not fetch latest block from web3 provider ' + fetchEnv('CELO_PROVIDER') + // Copied from @celo/cli/src/utils/helpers + try { + const syncProgress = await kit.web3.eth.isSyncing() + if (typeof syncProgress === 'boolean' && !syncProgress) { + const latestBlock: Block = await kit.web3.eth.getBlock('latest') + if (latestBlock && latestBlock.number > 0) { + // To catch the case in which syncing has happened in the past, + // has stopped, and hasn't started again, check for an old timestamp + // on the latest block + const ageOfBlock = Date.now() / 1000 - latestBlock.timestamp + if (ageOfBlock > 120) { + throw new Error( + `Latest block is ${ageOfBlock} seconds old, and syncing is not currently in progress` + ) + } + } + } else { + throw new Error('Node is not syncing') + } + } catch (error) { + rootLogger.error( + 'Initializing Kit failed, are you running your node and specified it with the "CELO_PROVIDER" env var?. It\' currently set as ' + + fetchEnv('CELO_PROVIDER') ) + throw error } } } diff --git a/packages/attestation-service/src/logger.ts b/packages/attestation-service/src/logger.ts index b23c6e01fc9..0a8402b3d6f 100644 --- a/packages/attestation-service/src/logger.ts +++ b/packages/attestation-service/src/logger.ts @@ -4,18 +4,18 @@ import { createStream } from 'bunyan-gke-stackdriver' import { fetchEnvOrDefault } from './env' const logLevel = fetchEnvOrDefault('LOG_LEVEL', 'info') as LogLevelString -const logFormat = fetchEnvOrDefault('LOG_FORMAT', 'json') +const logFormat = fetchEnvOrDefault('LOG_FORMAT', 'human') let stream: any switch (logFormat) { case 'stackdriver': stream = createStream(levelFromName[logLevel]) break - case 'human': - stream = { level: logLevel, stream: bunyanDebugStream() } + case 'json': + stream = { stream: process.stdout, level: logLevel } break default: - stream = { stream: process.stdout, level: logLevel } + stream = { level: logLevel, stream: bunyanDebugStream() } break } From f380ac79c9dca851545c1fb17f67b5cfdc20c257 Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Wed, 11 Dec 2019 17:45:24 -0800 Subject: [PATCH 04/40] Support more than 1 attesation bot at a time (#2192) --- .env | 2 +- .env.baklava | 2 +- .env.baklavastaging | 2 +- .env.integration | 2 +- packages/celotool/src/cmds/bots/auto-verify.ts | 10 +++++++++- packages/celotool/src/lib/migration-utils.ts | 2 +- .../templates/{deployment.yaml => statefulset.yaml} | 11 +++++++++-- 7 files changed, 23 insertions(+), 8 deletions(-) rename packages/helm-charts/attestation-bot/templates/{deployment.yaml => statefulset.yaml} (87%) diff --git a/.env b/.env index fa97fbfc3b3..7792064ed8a 100644 --- a/.env +++ b/.env @@ -34,7 +34,7 @@ GETH_BOOTNODE_DOCKER_IMAGE_REPOSITORY="us.gcr.io/celo-testnet/geth-all" GETH_BOOTNODE_DOCKER_IMAGE_TAG="master" CELOTOOL_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -CELOTOOL_DOCKER_IMAGE_TAG="celotool-6d4579212a7b49fdf54e2f4661c40ae8f4fbc4c5" +CELOTOOL_DOCKER_IMAGE_TAG="celotool-53016a800a510cddae73444377bcf366d753367a" CELOCLI_STANDALONE_IMAGE_REPOSITORY="gcr.io/celo-testnet/celocli-standalone" CELOCLI_STANDALONE_IMAGE_TAG="0.0.30-beta2" diff --git a/.env.baklava b/.env.baklava index 72a0c6aaa17..0a6f4413518 100644 --- a/.env.baklava +++ b/.env.baklava @@ -36,7 +36,7 @@ GETH_BOOTNODE_DOCKER_IMAGE_REPOSITORY="us.gcr.io/celo-testnet/geth-all" GETH_BOOTNODE_DOCKER_IMAGE_TAG="6e3766093331ef1cef8428c2320c60f62390adeb" CELOTOOL_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -CELOTOOL_DOCKER_IMAGE_TAG="celotool-dcad01808c05f77ea71b911ba331caab0fd739d3" +CELOTOOL_DOCKER_IMAGE_TAG="celotool-53016a800a510cddae73444377bcf366d753367a" CELOCLI_STANDALONE_IMAGE_REPOSITORY="gcr.io/celo-testnet/celocli-standalone" CELOCLI_STANDALONE_IMAGE_TAG="0.0.30-beta2" diff --git a/.env.baklavastaging b/.env.baklavastaging index 66ef872f316..05ef6c7faf0 100644 --- a/.env.baklavastaging +++ b/.env.baklavastaging @@ -36,7 +36,7 @@ GETH_BOOTNODE_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/geth-all" GETH_BOOTNODE_DOCKER_IMAGE_TAG="a85db20179b503b802430aef7085d35aa761c63d" CELOTOOL_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -CELOTOOL_DOCKER_IMAGE_TAG="celotool-51ae8e72be34a614346950b6fbd8ba3b94223947" +CELOTOOL_DOCKER_IMAGE_TAG="celotool-53016a800a510cddae73444377bcf366d753367a" CELOCLI_STANDALONE_IMAGE_REPOSITORY="gcr.io/celo-testnet/celocli-standalone" CELOCLI_STANDALONE_IMAGE_TAG="0.0.30-beta2" diff --git a/.env.integration b/.env.integration index 808efbf1c92..f270acd2f4c 100644 --- a/.env.integration +++ b/.env.integration @@ -34,7 +34,7 @@ GETH_BOOTNODE_DOCKER_IMAGE_REPOSITORY="us.gcr.io/celo-testnet/geth-all" GETH_BOOTNODE_DOCKER_IMAGE_TAG="803c05715bd12a3c148fa870e208cbdcebcc8cb8" CELOTOOL_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -CELOTOOL_DOCKER_IMAGE_TAG="celotool-5bea6d30cbe6aa4272b32a4d2cfed5567f422ea9" +CELOTOOL_DOCKER_IMAGE_TAG="celotool-53016a800a510cddae73444377bcf366d753367a" CELOCLI_STANDALONE_IMAGE_REPOSITORY="gcr.io/celo-testnet/celocli-standalone" CELOCLI_STANDALONE_IMAGE_TAG="0.0.30-beta2" diff --git a/packages/celotool/src/cmds/bots/auto-verify.ts b/packages/celotool/src/cmds/bots/auto-verify.ts index 3d04dc7f561..b4b78853e3a 100644 --- a/packages/celotool/src/cmds/bots/auto-verify.ts +++ b/packages/celotool/src/cmds/bots/auto-verify.ts @@ -34,6 +34,7 @@ interface AutoVerifyArgv extends BotsArgv { inBetweenWaitSeconds: number attestationMax: number celoProvider: string + index: number } export const builder = (yargs: Argv) => { @@ -58,6 +59,11 @@ export const builder = (yargs: Argv) => { description: 'The node to use', default: 'http://localhost:8545', }) + .option('index', { + type: 'number', + description: 'The index of the account to use', + default: 0, + }) } export const handler = async function autoVerify(argv: AutoVerifyArgv) { @@ -70,7 +76,9 @@ export const handler = async function autoVerify(argv: AutoVerifyArgv) { const kit = newKit(argv.celoProvider) const mnemonic = fetchEnv(envVar.MNEMONIC) // This really should be the ATTESTATION_BOT key, but somehow we can't get it to have cUSD - const clientKey = ensure0x(generatePrivateKey(mnemonic, AccountType.VALIDATOR, 0)) + const clientKey = ensure0x( + generatePrivateKey(mnemonic, AccountType.ATTESTATION_BOT, argv.index) + ) const clientAddress = privateKeyToAddress(clientKey) logger = logger.child({ address: clientAddress }) kit.addAccount(clientKey) diff --git a/packages/celotool/src/lib/migration-utils.ts b/packages/celotool/src/lib/migration-utils.ts index 7466b698252..1e99bda3026 100644 --- a/packages/celotool/src/lib/migration-utils.ts +++ b/packages/celotool/src/lib/migration-utils.ts @@ -36,7 +36,7 @@ function getAttestationKeys() { export function migrationOverrides() { const mnemonic = fetchEnv(envVar.MNEMONIC) const faucetedAccountAddresses = getFaucetedAccounts(mnemonic).map((account) => account.address) - const attestationBotAddresses = getAddressesFor(AccountType.ATTESTATION_BOT, mnemonic, 1) + const attestationBotAddresses = getAddressesFor(AccountType.ATTESTATION_BOT, mnemonic, 10) const initialAddresses = [...faucetedAccountAddresses, ...attestationBotAddresses] const initialBalance = fetchEnvOrFallback(envVar.FAUCET_CUSD_WEI, DEFAULT_FAUCET_CUSD_WEI) diff --git a/packages/helm-charts/attestation-bot/templates/deployment.yaml b/packages/helm-charts/attestation-bot/templates/statefulset.yaml similarity index 87% rename from packages/helm-charts/attestation-bot/templates/deployment.yaml rename to packages/helm-charts/attestation-bot/templates/statefulset.yaml index 69a4d287e60..a70d5dba753 100644 --- a/packages/helm-charts/attestation-bot/templates/deployment.yaml +++ b/packages/helm-charts/attestation-bot/templates/statefulset.yaml @@ -1,5 +1,5 @@ apiVersion: apps/v1beta2 -kind: Deployment +kind: StatefulSet metadata: name: {{ .Values.environment }}-attestation-bot labels: @@ -9,6 +9,7 @@ metadata: heritage: {{ .Release.Service }} component: attestation-bot spec: + podManagementPolicy: Parallel replicas: 1 selector: matchLabels: @@ -30,14 +31,20 @@ spec: - bash - "-c" - | + [[ $REPLICA_NAME =~ -([0-9]+)$ ]] || exit 1 + RID=${BASH_REMATCH[1]} CELOTOOL="/celo-monorepo/packages/celotool/bin/celotooljs.sh"; - $CELOTOOL bots auto-verify --initialWaitSeconds {{ .Values.initialWaitSeconds }} --inBetweenWaitSeconds {{ .Values.inBetweenWaitSeconds }} --attestationMax {{ .Values.maxAttestations }} --celoProvider https://{{ .Release.Namespace }}-forno.{{ .Values.domain.name }}.org + $CELOTOOL bots auto-verify --initialWaitSeconds {{ .Values.initialWaitSeconds }} --inBetweenWaitSeconds {{ .Values.inBetweenWaitSeconds }} --attestationMax {{ .Values.maxAttestations }} --celoProvider https://{{ .Release.Namespace }}-forno.{{ .Values.domain.name }}.org --index $RID resources: requests: memory: 256Mi cpu: 200m env: + - name: REPLICA_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name - name: MNEMONIC valueFrom: secretKeyRef: From f53406ccca0ff92dec7947cb4c37c117cd0bed41 Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Wed, 11 Dec 2019 17:59:34 -0800 Subject: [PATCH 05/40] Update baklava network ID in docs for 1.1 (#2214) --- .../docs/getting-started/reconnecting-to-network.md | 2 +- packages/docs/getting-started/running-a-full-node.md | 2 +- packages/docs/getting-started/running-a-validator.md | 10 +++++----- packages/protocol/truffle-config.js | 2 +- scripts/run-docker-validator-network.sh | 2 +- scripts/validator-config.rc | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/docs/getting-started/reconnecting-to-network.md b/packages/docs/getting-started/reconnecting-to-network.md index 8f11c950c23..3af7ccad0c2 100644 --- a/packages/docs/getting-started/reconnecting-to-network.md +++ b/packages/docs/getting-started/reconnecting-to-network.md @@ -91,7 +91,7 @@ First, pull the Celo image as described [here](running-a-validator.md#pull-the-c ```bash # On all machines export CELO_IMAGE=us.gcr.io/celo-testnet/celo-node:baklava -export NETWORK_ID=76172 +export NETWORK_ID=121119 docker pull $CELO_IMAGE ``` diff --git a/packages/docs/getting-started/running-a-full-node.md b/packages/docs/getting-started/running-a-full-node.md index 081c02d699a..025d249364e 100644 --- a/packages/docs/getting-started/running-a-full-node.md +++ b/packages/docs/getting-started/running-a-full-node.md @@ -24,7 +24,7 @@ First we are going to setup the environment variables required for `Baklava` net ```bash export CELO_IMAGE=us.gcr.io/celo-testnet/celo-node:baklava -export NETWORK_ID=76172 +export NETWORK_ID=121119 ``` ## Pull the Celo Docker image diff --git a/packages/docs/getting-started/running-a-validator.md b/packages/docs/getting-started/running-a-validator.md index cddedde7b4b..784146bbad6 100644 --- a/packages/docs/getting-started/running-a-validator.md +++ b/packages/docs/getting-started/running-a-validator.md @@ -131,7 +131,7 @@ First we are going to setup the main environment variables related with the `Bak ```bash export CELO_IMAGE=us.gcr.io/celo-testnet/celo-node:baklava -export NETWORK_ID=76172 +export NETWORK_ID=121119 ``` ### Pull the Celo Docker image @@ -195,7 +195,7 @@ To actually register as a validator, we'll need to generate a validating signer # On the validator machine # Note that you have to export $CELO_IMAGE and $NETWORK_ID on this machine export CELO_IMAGE=us.gcr.io/celo-testnet/celo-node:baklava -export NETWORK_ID=76172 +export NETWORK_ID=121119 mkdir celo-validator-node cd celo-validator-node docker run -v $PWD:/root/.celo -it $CELO_IMAGE account new @@ -256,7 +256,7 @@ You can then run the proxy with the following command. Be sure to replace ` docker run --name celo-proxy -it --restart always -p 30303:30303 -p 30303:30303/udp -p 30503:30503 -p 30503:30503/udp -v $PWD:/root/.celo $CELO_IMAGE --verbosity 3 --networkid $NETWORK_ID --syncmode full --proxy.proxy --proxy.proxiedvalidatoraddress $CELO_VALIDATOR_SIGNER_ADDRESS --proxy.internalendpoint :30503 --etherbase $CELO_VALIDATOR_SIGNER_ADDRESS --ethstats=-proxy@baklava-ethstats.celo-testnet.org ``` @@ -319,7 +319,7 @@ docker run --name celo-validator -it --restart always -p 30303:30303 -p 30303:30 The `mine` flag does not mean the node starts mining blocks, but rather starts trying to participate in the BFT consensus protocol. It cannot do this until it gets elected -- so next we need to stand for election. -The `networkid` parameter value of `76172` indicates we are connecting to the Baklava network, Stake Off Phase 1. +The `networkid` parameter value of `121119` indicates we are connecting to the Baklava network, Stake Off Phase 1. Note that if you are running the validator and the proxy on the same machine, then you should set the validator's listening port to something other than `30303`. E.g. you could use the flag `--port 30313` and set the docker port forwarding rules accordingly (e.g. use the flags `-p 30313:30313` and `-p 30313:30313/udp`). @@ -510,7 +510,7 @@ Just like with the Validator signer, we'll want to authorize a separate Attestat # On the Attestation machine # Note that you have to export CELO_IMAGE, NETWORK_ID and CELO_VALIDATOR_ADDRESS on this machine export CELO_IMAGE=us.gcr.io/celo-testnet/celo-node:baklava -export NETWORK_ID=76172 +export NETWORK_ID=121119 export CELO_VALIDATOR_ADDRESS= mkdir celo-attestations-node cd celo-attestations-node diff --git a/packages/protocol/truffle-config.js b/packages/protocol/truffle-config.js index 8255ccb1c6f..c5d4bc9f673 100644 --- a/packages/protocol/truffle-config.js +++ b/packages/protocol/truffle-config.js @@ -9,7 +9,7 @@ const argv = require('minimist')(process.argv.slice(2), { string: ['truffle_over const SOLC_VERSION = '0.5.8' const ALFAJORES_NETWORKID = 44785 -const BAKLAVA_NETWORKID = 76172 +const BAKLAVA_NETWORKID = 121119 const BAKLAVASTAGING_NETWORKID = 31416 const OG_FROM = '0xfeE1a22F43BeeCB912B5a4912ba87527682ef0fC' diff --git a/scripts/run-docker-validator-network.sh b/scripts/run-docker-validator-network.sh index 5527d799696..cb3002a6aec 100755 --- a/scripts/run-docker-validator-network.sh +++ b/scripts/run-docker-validator-network.sh @@ -8,7 +8,7 @@ COMMAND=${1:-"pull,accounts,run-validator,run-proxy,status,print-env"} DATA_DIR=${2:-"$HOME/.celo/network"} export CELO_IMAGE=${3:-"us.gcr.io/celo-testnet/celo-node:baklava"} -export NETWORK_ID=${4:-"76172"} +export NETWORK_ID=${4:-"121119"} export NETWORK_NAME=${5:-"baklava"} export DEFAULT_PASSWORD=${6:-"1234"} export CELO_IMAGE_ATTESTATION=${7:-"us.gcr.io/celo-testnet/celo-monorepo@sha256:90ea6739f9d239218245b5dce30e1bb5f05ac8dbc59f8e6f315502635c05ccb1"} diff --git a/scripts/validator-config.rc b/scripts/validator-config.rc index 9f2c97478c8..c69dcbafcf3 100644 --- a/scripts/validator-config.rc +++ b/scripts/validator-config.rc @@ -19,7 +19,7 @@ #CELO_VALIDATOR_SIGNER_BLS_SIGNATURE= #CELO_IMAGE="us.gcr.io/celo-testnet/celo-node:baklava" -#NETWORK_ID=76172 +#NETWORK_ID=121119 #NETWORK_NAME=baklava #CELO_IMAGE_ATTESTATION="us.gcr.io/celo-testnet/celo-monorepo:attestation-service-8f87dda20f1854c9532a5cbcf8ff556f48dff413" #CELO_PROVIDER="http://localhost:8545" From 42e449db5d2120d24616b162fa99929d76cfede7 Mon Sep 17 00:00:00 2001 From: Anna K Date: Wed, 11 Dec 2019 21:14:41 -0600 Subject: [PATCH 06/40] Update walletkit gateway fee to fix transactions in forno mode (#2211) --- packages/walletkit/src/contract-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/walletkit/src/contract-utils.ts b/packages/walletkit/src/contract-utils.ts index 58b807280ee..92d3cc163c3 100644 --- a/packages/walletkit/src/contract-utils.ts +++ b/packages/walletkit/src/contract-utils.ts @@ -413,7 +413,7 @@ export async function sendTransactionAsyncWithWeb3Signing( // fill the fields here. let feeCurrency = feeCurrencyContract._address const gatewayFeeRecipient = await web3.eth.getCoinbase() - const gatewayFee = defaultGatewayFee.toString() + const gatewayFee = '0x' + defaultGatewayFee.toString(16) Logger.debug(tag, `Gateway fee is ${gatewayFee} paid to ${gatewayFeeRecipient}`) const gasPrice = await getGasPrice(web3, feeCurrency) if (feeCurrency === undefined) { From 45dcf9cb75462e8a3a6526c2231a0cb159793a38 Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Wed, 11 Dec 2019 19:33:33 -0800 Subject: [PATCH 07/40] Minor baklava docs reconnection fixes (#2215) --- .../getting-started/reconnecting-to-network.md | 15 +++++++++++---- .../docs/getting-started/running-a-validator.md | 7 +++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/docs/getting-started/reconnecting-to-network.md b/packages/docs/getting-started/reconnecting-to-network.md index 3af7ccad0c2..696ca8e6de4 100644 --- a/packages/docs/getting-started/reconnecting-to-network.md +++ b/packages/docs/getting-started/reconnecting-to-network.md @@ -72,13 +72,12 @@ mv nodekey geth/nodekey ```bash # On your validator machine cd celo-validator-node -rm -rf geth* && rm static-nodes.json +rm -rf geth* ``` ```bash # On your Attestation machine cd celo-attestations-node -cd celo-accounts-node rm -rf geth* && rm static-nodes.json ``` @@ -95,6 +94,12 @@ export NETWORK_ID=121119 docker pull $CELO_IMAGE ``` +```bash +# On your attestation machine +export CELO_IMAGE_ATTESTATION=us.gcr.io/celo-testnet/celo-monorepo:attestation-service-baklava +docker pull $CELO_IMAGE_ATTESTATION +``` + ### Restart your Accounts node Follow [these instructions](running-a-validator.md#start-your-accounts-node) to restart your accounts node on your local machine. @@ -127,7 +132,7 @@ Next, follow [these instructions](running-a-validator.md#connect-the-validator-t ### Restart your Attestation node and service -Follow [these instructions](running-a-validator.md#running-the-attestation-service) to restart your Attestation node and service on your Attestation machine. Remember you do not have to create another account/proof-of-posession if you still have your keys. +Follow [these instructions](running-a-validator.md#running-the-attestation-service) to restart your Attestation node and service on your Attestation machine. ### Re-register your Validator @@ -147,6 +152,8 @@ echo CELO_VALIDATOR_SIGNER_PUBLIC_KEY=$CELO_VALIDATOR_SIGNER_PUBLIC_KEY echo CELO_VALIDATOR_SIGNER_SIGNATURE=$CELO_VALIDATOR_SIGNER_SIGNATURE echo CELO_VALIDATOR_SIGNER_BLS_PUBLIC_KEY=$CELO_VALIDATOR_SIGNER_BLS_PUBLIC_KEY echo CELO_VALIDATOR_SIGNER_BLS_SIGNATURE=$CELO_VALIDATOR_SIGNER_BLS_SIGNATURE +echo CELO_ATTESTATION_SIGNER_ADDRESS=$CELO_ATTESTATION_SIGNER_ADDRESS +echo CELO_ATTESTATION_SIGNER_SIGNATURE=$CELO_ATTESTATION_SIGNER_SIGNATURE ``` If any of the environment variables are missing, you have two options: @@ -163,6 +170,6 @@ First, make sure you have the latest version of the celocli. npm uninstall -g @celo/celocli && npm install -g @celo/celocli ``` -At this point you should be able to continue the steps described in the [Running a Validator](running-a-validator.md) documentation page, starting at the [Register the Accounts](running-a-validator.md#register-the-accounts) section. +At this point you should be able to continue the steps described in the [Running a Validator](running-a-validator.md) documentation page, starting at the [Register the Accounts](running-a-validator.md#register-the-accounts) section. Also remember to [register your Metadata](running-a-validator.md#registering-metadata) to be able to serve attestations and claim all your funds for the leaderboard. Note that if you were fauceted in phase 1.0 of The Great Celo Stakeoff, your accounts should have been included in the genesis block for subsequent phases, so you will not need to be fauceted again. diff --git a/packages/docs/getting-started/running-a-validator.md b/packages/docs/getting-started/running-a-validator.md index 784146bbad6..3de4621bf6d 100644 --- a/packages/docs/getting-started/running-a-validator.md +++ b/packages/docs/getting-started/running-a-validator.md @@ -657,6 +657,13 @@ If everything goes well users should be able to see your claims by running: celocli account:get-metadata $CELO_VALIDATOR_ADDRESS ``` +You can run the following command to test if you properly setup your attestation service: + +```bash +# On your local machine +celocli identity:test-attestation-service --from $CELO_VALIDATOR_ADDRESS --phoneNumber --message +``` + You should see that your claim for `$CELO_VALIDATOR_GROUP_ADDRESS` could not be verified! We need to create the corresponding claim from `$CELO_VALIDATOR_GROUP_ADDRESS` otherwise anyone could claim it! ```bash From 08d7b0dcadaee716c830f41deae498fb5fe573e0 Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Wed, 11 Dec 2019 20:38:09 -0800 Subject: [PATCH 08/40] Wait for only waitTime - 1 blocks (#2207) --- packages/contractkit/src/wrappers/Attestations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contractkit/src/wrappers/Attestations.ts b/packages/contractkit/src/wrappers/Attestations.ts index 8ec0fe35822..540d5618d55 100644 --- a/packages/contractkit/src/wrappers/Attestations.ts +++ b/packages/contractkit/src/wrappers/Attestations.ts @@ -139,7 +139,7 @@ export class AttestationsWrapper extends BaseWrapper { // TODO: Use subscription if provider supports while (Date.now() - startTime < timeoutSeconds * 1000) { const blockNumber = await this.kit.web3.eth.getBlockNumber() - if (blockNumber >= unselectedRequest.blockNumber + waitBlocks) { + if (blockNumber >= unselectedRequest.blockNumber + waitBlocks - 1) { return } await sleep(pollDurationSeconds * 1000) From 87aa69313c147321171f61ae146bd5d21d019276 Mon Sep 17 00:00:00 2001 From: Connor McEwen Date: Wed, 11 Dec 2019 21:09:51 -0800 Subject: [PATCH 09/40] Upgrade TS version (#2196) --- package.json | 2 +- packages/blockchain-api/package.json | 2 +- .../src/cmds/deploy/upgrade/ethstats.ts | 2 +- .../src/cmds/deploy/upgrade/leaderboard.ts | 2 +- .../src/cmds/deploy/upgrade/testnet.ts | 2 +- .../src/cmds/deploy/upgrade/vm-testnet.ts | 2 +- packages/cli/package.json | 2 +- packages/notification-service/package.json | 2 +- .../react-components/analytics/wrapper.tsx | 49 +++++++++++++------ .../react-components/components/TextInput.tsx | 4 +- yarn.lock | 10 ++-- 11 files changed, 49 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 569729e8734..f2e501809c2 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "ts-node": "^8.3.0", "tsconfig-paths": "^3.8.0", "tslint": "^5.20.0", - "typescript": "^3.5.3", + "typescript": "^3.7.3", "typescript-tslint-plugin": "^0.5.4" }, "dependencies": { diff --git a/packages/blockchain-api/package.json b/packages/blockchain-api/package.json index fcd26172452..2b190122466 100644 --- a/packages/blockchain-api/package.json +++ b/packages/blockchain-api/package.json @@ -35,7 +35,7 @@ "@types/web3": "^1.0.18", "jest-fetch-mock": "^2.1.2", "tsc-watch": "^1.0.31", - "typescript": "^3.5.3" + "typescript": "^3.7.3" }, "resolutions": { "**/cross-fetch": "3.0.4" diff --git a/packages/celotool/src/cmds/deploy/upgrade/ethstats.ts b/packages/celotool/src/cmds/deploy/upgrade/ethstats.ts index cd7fa33222f..b127f4299f3 100644 --- a/packages/celotool/src/cmds/deploy/upgrade/ethstats.ts +++ b/packages/celotool/src/cmds/deploy/upgrade/ethstats.ts @@ -23,7 +23,7 @@ export const handler = async (argv: EthstatsArgv) => { await createClusterIfNotExists() await switchToClusterFromEnv() - if (argv.reset) { + if (argv.reset === true) { await removeHelmRelease(argv.celoEnv) await installHelmChart(argv.celoEnv) } else { diff --git a/packages/celotool/src/cmds/deploy/upgrade/leaderboard.ts b/packages/celotool/src/cmds/deploy/upgrade/leaderboard.ts index 059eea67fd0..376ba09030f 100644 --- a/packages/celotool/src/cmds/deploy/upgrade/leaderboard.ts +++ b/packages/celotool/src/cmds/deploy/upgrade/leaderboard.ts @@ -23,7 +23,7 @@ export const handler = async (argv: LeaderboardArgv) => { await createClusterIfNotExists() await switchToClusterFromEnv() - if (argv.reset) { + if (argv.reset === true) { await removeHelmRelease(argv.celoEnv) await installHelmChart(argv.celoEnv) } else { diff --git a/packages/celotool/src/cmds/deploy/upgrade/testnet.ts b/packages/celotool/src/cmds/deploy/upgrade/testnet.ts index 1cae9afa895..9a0df3bb5f9 100644 --- a/packages/celotool/src/cmds/deploy/upgrade/testnet.ts +++ b/packages/celotool/src/cmds/deploy/upgrade/testnet.ts @@ -31,7 +31,7 @@ export const handler = async (argv: TestnetArgv) => { await upgradeStaticIPs(argv.celoEnv) - if (argv.reset) { + if (argv.reset === true) { await resetAndUpgradeHelmChart(argv.celoEnv) await uploadGenesisBlockToGoogleStorage(argv.celoEnv) } else { diff --git a/packages/celotool/src/cmds/deploy/upgrade/vm-testnet.ts b/packages/celotool/src/cmds/deploy/upgrade/vm-testnet.ts index aaa1d69e04d..7f0b5721e9b 100644 --- a/packages/celotool/src/cmds/deploy/upgrade/vm-testnet.ts +++ b/packages/celotool/src/cmds/deploy/upgrade/vm-testnet.ts @@ -31,7 +31,7 @@ export const handler = async (argv: VmTestnetArgv) => { await switchToClusterFromEnv() let onDeployFailed = () => Promise.resolve() - if (argv.reset) { + if (argv.reset === true) { onDeployFailed = () => untaintTestnet(argv.celoEnv) await taintTestnet(argv.celoEnv) } diff --git a/packages/cli/package.json b/packages/cli/package.json index 6a3b037d4c0..dd9ab0dce8c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -67,7 +67,7 @@ "@types/web3": "^1.0.18", "globby": "^8", "prettier": "1.13.5", - "typescript": "^3.5.3" + "typescript": "^3.7.3" }, "files": [ "README.md", diff --git a/packages/notification-service/package.json b/packages/notification-service/package.json index bce9ae88696..642b629c631 100644 --- a/packages/notification-service/package.json +++ b/packages/notification-service/package.json @@ -39,7 +39,7 @@ "@types/node-fetch": "^2.1.2", "@types/utf8": "^2.1.6", "@types/web3": "^1.0.18", - "typescript": "^3.5.3" + "typescript": "^3.7.3" }, "engines": { "node": "10" diff --git a/packages/react-components/analytics/wrapper.tsx b/packages/react-components/analytics/wrapper.tsx index fedb2665d74..1cd99e41136 100644 --- a/packages/react-components/analytics/wrapper.tsx +++ b/packages/react-components/analytics/wrapper.tsx @@ -4,29 +4,45 @@ import CeloAnalyticsType from '@celo/react-components/analytics/CeloAnalytics' import { DefaultEventNames } from '@celo/react-components/analytics/constants' import ReactNativeLogger from '@celo/react-components/services/ReactNativeLogger' import * as React from 'react' +// tslint:disable-next-line +import { Component, ComponentType, forwardRef, Ref } from 'react' function getDisplayName

(WrappedComponent: React.ComponentType

) { return WrappedComponent.displayName || WrappedComponent.name || 'Component' } -interface ForwardedRef { - forwardedRef?: React.Ref> -} - export default function Initializer(CeloAnalytics: CeloAnalyticsType, Logger: ReactNativeLogger) { - // Wrapper type: https://medium.com/@jrwebdev/react-higher-order-component-patterns-in-typescript-42278f7590fb + // Wrapper type: https://gist.github.com/OliverJAsh/d2f462b03b3e6c24f5588ca7915d010e // Component name: https://reactjs.org/docs/forwarding-refs.html - function componentWithAnalytics

( - WrappedComponent: React.ComponentType

- ): React.ComponentClass

{ - const displayName = getDisplayName(WrappedComponent) + function componentWithAnalytics( + ComposedComponent: ComponentType + ) { + const displayName = getDisplayName(ComposedComponent) + // @ts-ignore + type ComposedComponentInstance = InstanceType + type WrapperComponentProps = ComposedComponentProps & { + wrapperComponentProp: number + } + type WrapperComponentPropsWithForwardedRef = WrapperComponentProps & { + forwardedRef: Ref + } - class ComponentWithAnalytics extends React.Component

{ + class WrapperComponent extends Component { timestamp: number | undefined render() { - return + const { forwardedRef, wrapperComponentProp, ...composedComponentProps } = this.props + + return ( + + ) } trackEvent(event: any, props: any, attachDeviceInfo = false) { @@ -58,13 +74,16 @@ export default function Initializer(CeloAnalytics: CeloAnalyticsType, Logger: Re } } - function forwardRef(props: P, ref: any) { - return + function forward(props: WrapperComponentProps, ref: Ref) { + return } - forwardRef.displayName = `WithAnalytics(${displayName})` + forward.displayName = `WithAnalytics(${displayName})` - return hoistNonReactStatics(React.forwardRef(forwardRef), WrappedComponent) + return hoistNonReactStatics( + forwardRef(forward), + ComposedComponent + ) } return componentWithAnalytics } diff --git a/packages/react-components/components/TextInput.tsx b/packages/react-components/components/TextInput.tsx index 20d5650e8bc..ac5645eee42 100644 --- a/packages/react-components/components/TextInput.tsx +++ b/packages/react-components/components/TextInput.tsx @@ -11,7 +11,7 @@ import { StyleSheet, TextInput as RNTextInput, TextInputFocusEventData, - TextInputProps, + TextInputProps as RNTextInputProps, View, } from 'react-native' @@ -22,7 +22,7 @@ interface OwnProps { forwardedRef?: React.RefObject } -type Props = OwnProps & TextInputProps +type Props = OwnProps & RNTextInputProps interface State { isFocused: boolean diff --git a/yarn.lock b/yarn.lock index aab306eb44d..9f3b298b5aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -32098,16 +32098,16 @@ typescript@^3.2.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3.tgz#f1657fc7daa27e1a8930758ace9ae8da31403221" integrity sha512-Y21Xqe54TBVp+VDSNbuDYdGw0BpoR/Q6wo/+35M8PAU0vipahnyduJWirxxdxjsAkS7hue53x2zp8gz7F05u0A== -typescript@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" - integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== - typescript@^3.6.4: version "3.6.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.4.tgz#b18752bb3792bc1a0281335f7f6ebf1bbfc5b91d" integrity sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg== +typescript@^3.7.3: + version "3.7.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.3.tgz#b36840668a16458a7025b9eabfad11b66ab85c69" + integrity sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw== + typewise-core@^1.2, typewise-core@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/typewise-core/-/typewise-core-1.2.0.tgz#97eb91805c7f55d2f941748fa50d315d991ef195" From 521bbbb5fefb246d26993c4803bcd799a6f7e2e6 Mon Sep 17 00:00:00 2001 From: "Victor \"Nate\" Graf" Date: Wed, 11 Dec 2019 21:29:31 -0800 Subject: [PATCH 10/40] Doc changes to address frequently asked questions (#2209) --- .../cli/src/commands/election/activate.ts | 8 +- packages/cli/src/utils/checks.ts | 4 +- packages/docs/SUMMARY.md | 2 +- .../protocol/proof-of-stake/README.md | 2 +- .../docs/command-line-interface/election.md | 4 +- .../getting-started/running-a-validator.md | 91 +++++++++++-------- .../docs/getting-started/using-the-wallet.md | 2 +- 7 files changed, 67 insertions(+), 46 deletions(-) diff --git a/packages/cli/src/commands/election/activate.ts b/packages/cli/src/commands/election/activate.ts index 26b8d51ea56..c5474b5466a 100644 --- a/packages/cli/src/commands/election/activate.ts +++ b/packages/cli/src/commands/election/activate.ts @@ -1,17 +1,19 @@ import { sleep } from '@celo/utils/lib/async' import { flags } from '@oclif/command' +import { cli } from 'cli-ux' import { BaseCommand } from '../../base' import { newCheckBuilder } from '../../utils/checks' import { displaySendTx } from '../../utils/cli' import { Flags } from '../../utils/command' export default class ElectionVote extends BaseCommand { - static description = 'Activate pending votes in validator elections to begin earning rewards' + static description = + 'Activate pending votes in validator elections to begin earning rewards. To earn rewards as a voter, it is required to activate your pending votes at some point after the end of the epoch in which they were made.' static flags = { ...BaseCommand.flags, from: Flags.address({ required: true, description: "Voter's address" }), - wait: flags.boolean({ description: 'Wait until all pending votes become activatable' }), + wait: flags.boolean({ description: 'Wait until all pending votes can be activated' }), } static examples = [ @@ -33,9 +35,11 @@ export default class ElectionVote extends BaseCommand { if (hasPendingVotes) { if (res.flags.wait) { // Spin until pending votes become activatable. + cli.action.start(`Waiting until pending votes can be activated`) while (!(await election.hasActivatablePendingVotes(account))) { await sleep(1000) } + cli.action.stop() } const txos = await election.activate(account) for (const txo of txos) { diff --git a/packages/cli/src/utils/checks.ts b/packages/cli/src/utils/checks.ts index e5a3c7c2da2..47971c3e981 100644 --- a/packages/cli/src/utils/checks.ts +++ b/packages/cli/src/utils/checks.ts @@ -155,13 +155,13 @@ class CheckBuilder { isNotAccount = (address: Address) => this.addCheck( - `${address} is not an Account`, + `${address} is not a registered Account`, this.withAccounts((accs) => negate(accs.isAccount(address))) ) isSignerOrAccount = () => this.addCheck( - `${this.signer!} is Signer or Account`, + `${this.signer!} is Signer or registered Account`, this.withAccounts(async (accs) => { const res = (await accs.isAccount(this.signer!)) || (await accs.isSigner(this.signer!)) return res diff --git a/packages/docs/SUMMARY.md b/packages/docs/SUMMARY.md index 71d02461b4b..c79d8e6f44e 100644 --- a/packages/docs/SUMMARY.md +++ b/packages/docs/SUMMARY.md @@ -19,7 +19,7 @@ - [Celo Protocol](celo-codebase/protocol/README.md) - [Consensus](celo-codebase/protocol/consensus/README.md) - [Validator Set Differences](celo-codebase/protocol/consensus/validator-set-differences.md) - - [Locating Validators](celo-codebase/protocol/consensus/locating-validators.md) + - [Locating Nodes](celo-codebase/protocol/consensus/locating-nodes.md) - [Ultralight Sync](celo-codebase/protocol/consensus/ultralight-sync.md) - [Proof of Stake](celo-codebase/protocol/proof-of-stake/README.md) - [Validator Groups](celo-codebase/protocol/proof-of-stake/validator-groups.md) diff --git a/packages/docs/celo-codebase/protocol/proof-of-stake/README.md b/packages/docs/celo-codebase/protocol/proof-of-stake/README.md index fea399fb478..506379d0030 100644 --- a/packages/docs/celo-codebase/protocol/proof-of-stake/README.md +++ b/packages/docs/celo-codebase/protocol/proof-of-stake/README.md @@ -24,7 +24,7 @@ Most of Celo's Proof of Stake mechanism is implemented as smart contracts, and a - [`Validators.sol`](https://github.com/celo-org/celo-monorepo/blob/master/packages/protocol/contracts/governance/Validators.sol) handles registration, deregistration, staking, key management and epoch rewards for validators and validator groups, as well as routines to manage the members of groups. -- [`Elections.sol`](https://github.com/celo-org/celo-monorepo/blob/master/packages/protocol/contracts/governance/Elections.sol) manages Locked Gold voting and epoch rewards and runs Validator Elections. +- [`Election.sol`](https://github.com/celo-org/celo-monorepo/blob/master/packages/protocol/contracts/governance/Election.sol) manages Locked Gold voting and epoch rewards and runs Validator Elections. In Celo blockchain: diff --git a/packages/docs/command-line-interface/election.md b/packages/docs/command-line-interface/election.md index 6c295bcbfcb..a7eda461bdb 100644 --- a/packages/docs/command-line-interface/election.md +++ b/packages/docs/command-line-interface/election.md @@ -6,7 +6,7 @@ description: Participate in and view the state of Validator Elections ### Activate -Activate pending votes in validator elections to begin earning rewards +Activate pending votes in validator elections to begin earning rewards. To earn rewards as a voter, it is required to activate your pending votes at some point after the end of the epoch in which they were made. ``` USAGE @@ -14,7 +14,7 @@ USAGE OPTIONS --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Voter's address - --wait Wait until all pending votes become activatable + --wait Wait until all pending votes can be activated EXAMPLES activate --from 0x4443d0349e8b3075cba511a0a87796597602a0f1 diff --git a/packages/docs/getting-started/running-a-validator.md b/packages/docs/getting-started/running-a-validator.md index 3de4621bf6d..397afe3e641 100644 --- a/packages/docs/getting-started/running-a-validator.md +++ b/packages/docs/getting-started/running-a-validator.md @@ -10,15 +10,15 @@ While other Validator Groups will exist on the Celo Networks, the fastest way to Because of the importance of Validator security and availability, Validators are expected to run one or more additional "proxy" nodes. In this setup, the proxy node connects with the rest of the network, and the machine running the Validator communicates only with the proxy, ideally via a private network. -Additionally, Validators are expected to run an [Attestation Service](running-attestation-service.md) as part of the [lightweight identity protocol](../celo-codebase/protocol/identity), to provide attestations that allow users to map their phone number to a Celo address. +Additionally, Validators are expected to run an [Attestation Service](https://github.com/celo-org/celo-monorepo/tree/master/packages/attestation-service) as part of the [lightweight identity protocol](../celo-codebase/protocol/identity), to provide attestations that allow users to map their phone number to a Celo address. You can find more details about Celo mission and why to become a Validator [in our Medium article](https://medium.com/celohq/calling-all-chefs-become-a-celo-validator-c75d1c2909aa). -## Prerequisites +## Register for the Stake Off -### Register for the Stake Off +Participation in The Great Celo Stake Off is subject to these [Terms and Conditions](https://docs.google.com/document/d/1b5SzeRbq60nx50NeezAEMpwLkaBDQ9hjZc0QAh4Mbdk/). If you agree to those, register online via an [online form](https://docs.google.com/forms/d/e/1FAIpQLSfbn5hTJ4UIWpN92-o2qMTUB0UnrFsL0fm97XqGe4VhhN_r5A/viewform). **Once the C-Labs team receives your registration, they will send you instructions to get fauceted funds to run a Validator on the Baklava testnet.** Do this first. -Participation in The Great Celo Stake Off is subject to these [Terms and Conditions](https://docs.google.com/document/d/1b5SzeRbq60nx50NeezAEMpwLkaBDQ9hjZc0QAh4Mbdk/). If you agree to those, register online via an [online form](https://docs.google.com/forms/d/e/1FAIpQLSfbn5hTJ4UIWpN92-o2qMTUB0UnrFsL0fm97XqGe4VhhN_r5A/viewform). **Once the C-Labs team receive your registration, they will send you instructions to get fauceted.** Do this first. +## Prerequisites ### Hardware requirements @@ -96,10 +96,10 @@ Running a Celo Validator node requires the management of several different keys, | Name of the key | Purpose | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Account key | This is the key with the highest level of permissions, and is thus the most sensitive. It can be used to lock and unlock Celo Gold, and authorize vote, validator, and attestation keys. Note that the account key also has all of the permissions of the other keys. | | -| Validator signer key | This is the key that has permission to register and manage a Validator or Validator Group, and participate in BFT consensus. | | -| Vote signer key | This key can be used to vote in Validator elections and on-chain governance. | | -| Attestation signer key | This key is used to sign attestations in Celo's lightweight identity protocol. | | +| Account key | This is the key with the highest level of permissions, and is thus the most sensitive. It can be used to lock and unlock Celo Gold, and authorize vote, validator, and attestation keys. Note that the account key also has all of the permissions of the other keys. | +| Validator signer key | This is the key that has permission to register and manage a Validator or Validator Group, and participate in BFT consensus. | +| Vote signer key | This key can be used to vote in Validator elections and on-chain governance. | +| Attestation signer key | This key is used to sign attestations in Celo's lightweight identity protocol. | Note that account and signer keys must be unique and may not be reused. @@ -107,24 +107,24 @@ Note that account and signer keys must be unique and may not be reused. | Variable | Explanation | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | -| CELO_IMAGE | The Docker image used for the Validator and Proxy containers | | -| NETWORK_ID | The Celo Baklava network chain ID | | -| CELO_VALIDATOR_GROUP_ADDRESS | The account address for the Validator Group | | -| CELO_VALIDATOR_ADDRESS | The account address for the Validator | | -| CELO_VALIDATOR_SIGNER_ADDRESS | The address of the validator signer authorized by the validator account | | -| CELO_VALIDATOR_SIGNER_PUBLIC_KEY | The ECDSA public key associated with the validator signer address | | -| CELO_VALIDATOR_SIGNER_SIGNATURE | The proof-of-possession of the validator signer key | | -| CELO_VALIDATOR_SIGNER_BLS_PUBLIC_KEY | The BLS public key for the Validator instance | | -| CELO_VALIDATOR_SIGNER_BLS_SIGNATURE | A proof-of-possession of the BLS public key | | -| PROXY_ENODE | The enode address for the Validator proxy | | -| PROXY_INTERNAL_IP | (Optional) The internal IP address over which your Validator can communicate with your Proxy | | -| PROXY_EXTERNAL_IP | The external IP address of the Proxy. May be used by the Validator to communicate with the Proxy if PROXY_INTERNAL_IP is unspecified | | -| ATTESTATION_SIGNER_ADDRESS | The address of the attestation signer authorized by the validator account | | -| ATTESTATION_SIGNER_SIGNATURE | The proof-of-possession of the attestation signer key | | -| ATTESTATION_SERVICE_URL | The URL to access the deployed Attestation Service | | -| METADATA_URL | The URL to access the metadata file for your Attestation Service | | +| CELO_IMAGE | The Docker image used for the Validator and Proxy containers | +| NETWORK_ID | The Celo Baklava network chain ID | +| CELO_VALIDATOR_GROUP_ADDRESS | The account address for the Validator Group | +| CELO_VALIDATOR_ADDRESS | The account address for the Validator | +| CELO_VALIDATOR_SIGNER_ADDRESS | The address of the validator signer authorized by the validator account | +| CELO_VALIDATOR_SIGNER_PUBLIC_KEY | The ECDSA public key associated with the validator signer address | +| CELO_VALIDATOR_SIGNER_SIGNATURE | The proof-of-possession of the validator signer key | +| CELO_VALIDATOR_SIGNER_BLS_PUBLIC_KEY | The BLS public key for the Validator instance | +| CELO_VALIDATOR_SIGNER_BLS_SIGNATURE | A proof-of-possession of the BLS public key | +| PROXY_ENODE | The enode address for the Validator proxy | +| PROXY_INTERNAL_IP | (Optional) The internal IP address over which your Validator can communicate with your Proxy | +| PROXY_EXTERNAL_IP | The external IP address of the Proxy. May be used by the Validator to communicate with the Proxy if PROXY_INTERNAL_IP is unspecified | +| ATTESTATION_SIGNER_ADDRESS | The address of the attestation signer authorized by the validator account | +| ATTESTATION_SIGNER_SIGNATURE | The proof-of-possession of the attestation signer key | +| ATTESTATION_SERVICE_URL | The URL to access the deployed Attestation Service | +| METADATA_URL | The URL to access the metadata file for your Attestation Service | | DATABASE_URL | The URL under which your database is accessible, currently supported are `postgres://`, `mysql://` and `sqlite://` | -| APP_SIGNATURE | The hash with which clients can auto-read SMS messages on android | | +| APP_SIGNATURE | The hash with which clients can auto-read SMS messages on android | | SMS_PROVIDERS | A comma-separated list of providers you want to configure, we currently support `nexmo` & `twilio` | First we are going to setup the main environment variables related with the `Baklava` network. Run: @@ -144,7 +144,12 @@ docker pull $CELO_IMAGE ### Create the Validator and Validator Group accounts -First, you'll need to generate account keys for your Validator and Validator Group. These are the keys that will have access to your locked Celo Gold, and thus should be handled with care. For the purposes of this guide, we will be storing these keys on your local machine, but we recommend that you store them in a more secure manner. +First, you'll need to generate account keys for your Validator and Validator Group. + +{% hint style="danger" %} +These keys will control your locked Celo Gold, and thus should be handled with care. +Store and back these keys up in a secure manner, as there will be no way to recover if them if lost or stolen. +{% endhint %} ```bash # On your local machine @@ -154,7 +159,8 @@ docker run -v $PWD:/root/.celo -it $CELO_IMAGE account new docker run -v $PWD:/root/.celo -it $CELO_IMAGE account new ``` -This should generate two accounts in your current directory and print them out, set them in environment variables: +This will create a new keystore in the current directory with two new accounts. +Copy the addresses from the terminal and set the following environment variables: ```bash # On your local machine @@ -168,8 +174,8 @@ Next, we'll run a node on your local machine so that we can use these accounts t ```bash # On your local machine -docker run -v $PWD:/root/.celo $CELO_IMAGE init /celo/genesis.json -docker run -v $PWD:/root/.celo --entrypoint cp $CELO_IMAGE /celo/static-nodes.json /root/.celo/ +docker run -v $PWD:/root/.celo -it $CELO_IMAGE init /celo/genesis.json +docker run -v $PWD:/root/.celo -it --entrypoint cp $CELO_IMAGE /celo/static-nodes.json /root/.celo/ ``` To run the node: @@ -247,8 +253,8 @@ To avoid exposing the validator to the public internet, we are deploying a proxy export CELO_IMAGE=us.gcr.io/celo-testnet/celo-node:baklava mkdir celo-proxy-node cd celo-proxy-node -docker run -v $PWD:/root/.celo $CELO_IMAGE init /celo/genesis.json -docker run -v $PWD:/root/.celo --entrypoint cp $CELO_IMAGE /celo/static-nodes.json /root/.celo/ +docker run -v $PWD:/root/.celo -it $CELO_IMAGE init /celo/genesis.json +docker run -v $PWD:/root/.celo -it --entrypoint cp $CELO_IMAGE /celo/static-nodes.json /root/.celo/ ``` You can then run the proxy with the following command. Be sure to replace `` with the name you'd like to use for your Validator account. @@ -261,6 +267,10 @@ export CELO_VALIDATOR_SIGNER_ADDRESS= docker run --name celo-proxy -it --restart always -p 30303:30303 -p 30303:30303/udp -p 30503:30503 -p 30503:30503/udp -v $PWD:/root/.celo $CELO_IMAGE --verbosity 3 --networkid $NETWORK_ID --syncmode full --proxy.proxy --proxy.proxiedvalidatoraddress $CELO_VALIDATOR_SIGNER_ADDRESS --proxy.internalendpoint :30503 --etherbase $CELO_VALIDATOR_SIGNER_ADDRESS --ethstats=-proxy@baklava-ethstats.celo-testnet.org ``` +{% hint style="info" %} +You can detach from the running container by pressing `ctrl+p ctrl+q`, or start it without `-d` instead of `it` to start detached. Access the logs for a container in the background with the `docker logs` command. +{% endhint %} + ### Get your Proxy's connection info Once the proxy is running, we will need to retrieve its enode and IP address so that the validator will be able to connect to it. @@ -313,7 +323,7 @@ Once that is completed, go ahead and run the validator. Be sure to replace ` > .password -docker run -v $PWD:/root/.celo $CELO_IMAGE init /celo/genesis.json +docker run -v $PWD:/root/.celo -it $CELO_IMAGE init /celo/genesis.json docker run --name celo-validator -it --restart always -p 30303:30303 -p 30303:30303/udp -v $PWD:/root/.celo $CELO_IMAGE --verbosity 3 --networkid $NETWORK_ID --syncmode full --mine --istanbul.blockperiod=5 --istanbul.requesttimeout=3000 --etherbase $CELO_VALIDATOR_SIGNER_ADDRESS --nodiscover --proxy.proxied --proxy.proxyenodeurlpair=enode://$PROXY_ENODE@$PROXY_INTERNAL_IP:30503\;enode://$PROXY_ENODE@$PROXY_EXTERNAL_IP:30303 --unlock=$CELO_VALIDATOR_SIGNER_ADDRESS --password /root/.celo/.password --ethstats=@baklava-ethstats.celo-testnet.org ``` @@ -379,7 +389,7 @@ celocli lockedgold:show $CELO_VALIDATOR_ADDRESS ### Run for election -In order to be elected as a Validator, you will first need to register your group and Validator. Note that when registering a Validator Group, you need to specify a commission, which is the fraction of epoch rewards paid to the group by its members. +In order to be elected as a Validator, you will first need to register your group and Validator. Note that when registering a Validator Group, you need to specify a [commission](../celo-codebase/protocol/proof-of-stake/validator-groups.md#group-share), which is the fraction of epoch rewards paid to the group by its members. We don't want to use our account key for validating, so first let's authorize the validator signing key: @@ -493,13 +503,20 @@ celocli election:list If you find your Validator still not getting elected you may need to faucet yourself more funds and lock more gold in order to be able to cast more votes for your Validator Group! -You can check the status of your validator, including whether it is elected and signing blocks, by running: +You can check the status of your validator, including whether it is elected and signing blocks, at [baklava-ethstats.celo-testnet.org](https://baklava-ethstats.celo-testnet.org) or by running: ```bash # On your local machine with celocli >= 0.0.30-beta9 celocli validator:status --validator $CELO_VALIDATOR_ADDRESS ``` +You can see additional information about your validator, including uptime score, by running: + +```bash +# On your local machine +celocli validator:show $CELO_VALIDATOR_ADDRESS +``` + ### Running the Attestation Service As part of the [lightweight identity protocol](/celo-codebase/protocol/identity), Validators are expected to run an [Attestation Service](https://github.com/celo-org/celo-monorepo/tree/master/packages/attestation-service) to provide attestations that allow users to map their phone number to an account on Celo. @@ -514,8 +531,8 @@ export NETWORK_ID=121119 export CELO_VALIDATOR_ADDRESS= mkdir celo-attestations-node cd celo-attestations-node -docker run -v $PWD:/root/.celo $CELO_IMAGE init /celo/genesis.json -docker run -v $PWD:/root/.celo --entrypoint cp $CELO_IMAGE /celo/static-nodes.json /root/.celo/ +docker run -v $PWD:/root/.celo -it $CELO_IMAGE init /celo/genesis.json +docker run -v $PWD:/root/.celo -it --entrypoint cp $CELO_IMAGE /celo/static-nodes.json /root/.celo/ docker run -v $PWD:/root/.celo -it $CELO_IMAGE account new export CELO_ATTESTATION_SIGNER_ADDRESS= ``` @@ -713,7 +730,7 @@ screen -r -S celo-validator ### Stopping containers You can stop the Docker containers at any time without problem. If you stop your containers that means those containers stop providing service. -The data dir of the validator and the proxy are Docker volumes mounted in the containers from the `celo-data-dir` you created at the very beginning. So if you don't remove that folder, you can stop or restart the containers without losing any data. +The data dir of the validator and the proxy are Docker volumes mounted in the containers from the `celo-*-dir` you created at the very beginning. So if you don't remove that folder, you can stop or restart the containers without losing any data. You can stop the `celo-validator` and `celo-proxy` containers running: diff --git a/packages/docs/getting-started/using-the-wallet.md b/packages/docs/getting-started/using-the-wallet.md index 3556a3e8c17..ec18113f497 100644 --- a/packages/docs/getting-started/using-the-wallet.md +++ b/packages/docs/getting-started/using-the-wallet.md @@ -18,7 +18,7 @@ To download the app on the Play Store, go [here](https://play.google.com/store/a ### Running the Celo Wallet Locally -For more information on how to run the Celo Wallet locally, please refer to the [mobile wallet setup instructions](../../celo-codebase/wallet/intro.md) +For more information on how to run the Celo Wallet locally, please refer to the [mobile wallet setup instructions](../celo-codebase/wallet/intro.md) ### Get More Funds! From 014bb6b3a24173c00cbe31f2f2b57f81a7c12852 Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Wed, 11 Dec 2019 22:21:02 -0800 Subject: [PATCH 11/40] Fix bignumber display in CLI (#2212) --- packages/cli/package.json | 2 +- packages/cli/src/utils/cli.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index dd9ab0dce8c..bae8ef91cb4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@celo/celocli", "description": "CLI Tool for transacting with the Celo protocol", - "version": "0.0.30-beta9", + "version": "0.0.32-beta3", "author": "Celo", "license": "Apache-2.0", "repository": "celo-org/celo-monorepo", diff --git a/packages/cli/src/utils/cli.ts b/packages/cli/src/utils/cli.ts index 2fa78dc0925..c923ee17b5d 100644 --- a/packages/cli/src/utils/cli.ts +++ b/packages/cli/src/utils/cli.ts @@ -38,7 +38,11 @@ export function printValueMapRecursive(valueMap: Record) { function toStringValueMapRecursive(valueMap: Record, prefix: string): string { const printValue = (v: any): string => { if (typeof v === 'object' && v != null) { - if (v instanceof BigNumber) return v.toFixed() + if (BigNumber.isBigNumber(v)) { + const factor = new BigNumber(10).pow(18) + const extra = v.isGreaterThan(factor) ? `(~${v.div(factor).decimalPlaces(2)} 10^18)` : '' + return `${v.toFixed()} ${extra}` + } return '\n' + toStringValueMapRecursive(v, prefix + ' ') } return chalk`${v}` From bdff55c86f84aa2f4c716eac0af23e63f02b2440 Mon Sep 17 00:00:00 2001 From: Sebastian Gerske <13647606+H34D@users.noreply.github.com> Date: Thu, 12 Dec 2019 09:59:17 +0100 Subject: [PATCH 12/40] Fix/protocol test flakyness (#2155) --- .prettierignore | 2 +- packages/protocol/lib/test-utils.ts | 10 +++--- packages/protocol/runTests.js | 2 +- packages/protocol/test/baklava/freezable.ts | 6 ++-- packages/protocol/test/common/accounts.ts | 16 ++++----- .../protocol/test/common/gaspriceminimum.ts | 6 ++-- packages/protocol/test/common/linkedlist.ts | 3 +- .../test/governance/blockchainparams.ts | 4 +-- packages/protocol/test/governance/election.ts | 8 ++--- .../protocol/test/governance/epochrewards.ts | 22 +++++++----- .../protocol/test/governance/governance.ts | 22 ++++++------ .../protocol/test/governance/lockedgold.ts | 2 +- .../protocol/test/governance/validators.ts | 36 +++++++++---------- .../protocol/test/identity/attestations.ts | 29 ++++++--------- packages/protocol/test/identity/random.ts | 12 +++---- packages/protocol/test/stability/exchange.ts | 2 +- packages/protocol/test/stability/reserve.ts | 2 +- .../protocol/test/stability/sortedoracles.ts | 4 +-- .../protocol/test/stability/stabletoken.ts | 2 +- packages/protocol/tsconfig.json | 2 +- 20 files changed, 96 insertions(+), 96 deletions(-) diff --git a/.prettierignore b/.prettierignore index 07955f8eef4..a5147dbb317 100644 --- a/.prettierignore +++ b/.prettierignore @@ -21,7 +21,7 @@ packages/protocol/types/ !packages/protocol/lib/**/*.ts packages/protocol/scripts/**/*.js packages/protocol/migrations/**/*.js -packages/protocol/tests/**/*.js +packages/protocol/test/**/*.js packages/verification-pool-api/contracts/*.ts diff --git a/packages/protocol/lib/test-utils.ts b/packages/protocol/lib/test-utils.ts index 5e17a1f3669..7af5c0d6a76 100644 --- a/packages/protocol/lib/test-utils.ts +++ b/packages/protocol/lib/test-utils.ts @@ -41,7 +41,9 @@ export async function jsonRpc(web3: Web3, method: string, params: any[] = []): P jsonrpc: '2.0', method, params, - id: new Date().getTime(), + // salt id generation, milliseconds might not be + // enough to generate unique ids + id: new Date().getTime() + Math.floor(Math.random() * ( 1 + 100 - 1 )), }, // @ts-ignore (err: any, result: any) => { @@ -240,13 +242,13 @@ export function assertLogMatches( } export function assertEqualBN( - value: number | BN | BigNumber, + actual: number | BN | BigNumber, expected: number | BN | BigNumber, msg?: string ) { assert( - web3.utils.toBN(value).eq(web3.utils.toBN(expected)), - `expected ${expected.toString()} and got ${value.toString()}. ${msg || ''}` + web3.utils.toBN(actual).eq(web3.utils.toBN(expected)), + `expected ${expected.toString(10)} and got ${actual.toString(10)}. ${msg || ''}` ) } diff --git a/packages/protocol/runTests.js b/packages/protocol/runTests.js index 4450b490dde..c06403f47df 100644 --- a/packages/protocol/runTests.js +++ b/packages/protocol/runTests.js @@ -70,7 +70,7 @@ async function test() { const testFiles = glob.readdirSync(testGlob) if (testFiles.length === 0) { // tslint:disable-next-line: no-console - console.error(`No tests matched with ${argv._}`) + console.error(`No test files matched with ${testGlob}`) process.exit(1) } testArgs = testArgs.concat(testFiles) diff --git a/packages/protocol/test/baklava/freezable.ts b/packages/protocol/test/baklava/freezable.ts index 1c7431a80f6..0ba2e3be3e8 100644 --- a/packages/protocol/test/baklava/freezable.ts +++ b/packages/protocol/test/baklava/freezable.ts @@ -1,4 +1,4 @@ -import { assertSameAddress, assertLogMatches2, assertRevert } from '@celo/protocol/lib/test-utils' +import { assertLogMatches2, assertRevert, assertSameAddress } from '@celo/protocol/lib/test-utils' import { FreezableTestInstance } from 'types' contract('Freezable', (accounts: string[]) => { @@ -7,7 +7,7 @@ contract('Freezable', (accounts: string[]) => { beforeEach(async () => { freezableTest = await FreezableTest.new() - freezableTest.setFreezer(accounts[0]) + await freezableTest.setFreezer(accounts[0]) }) describe('_setFreezer', () => { @@ -55,7 +55,7 @@ contract('Freezable', (accounts: string[]) => { describe('unfreeze', () => { beforeEach(async () => { - freezableTest.freeze() + await freezableTest.freeze() }) it('should allow freezer to unfreeze the contract', async () => { diff --git a/packages/protocol/test/common/accounts.ts b/packages/protocol/test/common/accounts.ts index e2baf094fb7..b35d70b9120 100644 --- a/packages/protocol/test/common/accounts.ts +++ b/packages/protocol/test/common/accounts.ts @@ -1,5 +1,3 @@ -import { parseSolidityStringArray } from '@celo/utils/lib/parsing' -import { upperFirst } from 'lodash' import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { getParsedSignatureOfAddress } from '@celo/protocol/lib/signing-utils' import { @@ -8,6 +6,8 @@ import { assertRevert, NULL_ADDRESS, } from '@celo/protocol/lib/test-utils' +import { parseSolidityStringArray } from '@celo/utils/lib/parsing' +import { upperFirst } from 'lodash' import { AccountsContract, AccountsInstance, @@ -243,7 +243,7 @@ contract('Accounts', (accounts: string[]) => { describe('when the account has been created', () => { beforeEach(async () => { - accountsInstance.createAccount() + await accountsInstance.createAccount() }) it('should set the walletAddress', async () => { @@ -279,7 +279,7 @@ contract('Accounts', (accounts: string[]) => { describe('when the account has been created', () => { beforeEach(async () => { - accountsInstance.createAccount() + await accountsInstance.createAccount() }) it('should set the metadataURL', async () => { @@ -304,9 +304,9 @@ contract('Accounts', (accounts: string[]) => { it('returns multiple metadata URLs', async () => { const randomStrings = accounts.map((_) => web3.utils.randomHex(20).slice(2)) await Promise.all( - accounts.map(async (account, i) => { - await accountsInstance.createAccount({ from: account }) - await accountsInstance.setMetadataURL(randomStrings[i], { from: account }) + accounts.map(async (mappedAccount, i) => { + await accountsInstance.createAccount({ from: mappedAccount }) + await accountsInstance.setMetadataURL(randomStrings[i], { from: mappedAccount }) }) ) const [stringLengths, data] = await accountsInstance.batchGetMetadataURL(accounts) @@ -329,7 +329,7 @@ contract('Accounts', (accounts: string[]) => { describe('when the account has been created', () => { beforeEach(async () => { - accountsInstance.createAccount() + await accountsInstance.createAccount() }) it('should set the name', async () => { diff --git a/packages/protocol/test/common/gaspriceminimum.ts b/packages/protocol/test/common/gaspriceminimum.ts index 6b15f21d38d..8c3a0ae4342 100644 --- a/packages/protocol/test/common/gaspriceminimum.ts +++ b/packages/protocol/test/common/gaspriceminimum.ts @@ -160,14 +160,14 @@ contract('GasPriceMinimum', (accounts: string[]) => { const getUpdatedGasPriceMinimum = ( previousGasPriceMinimum, density, - targetDensity, - adjustmentSpeed + _targetDensity, + _adjustmentSpeed ) => { const one = new BigNumber(1) return previousGasPriceMinimum .times( one.plus( - fromFixed(adjustmentSpeed).times(fromFixed(density).minus(fromFixed(targetDensity))) + fromFixed(_adjustmentSpeed).times(fromFixed(density).minus(fromFixed(_targetDensity))) ) ) .plus(one) diff --git a/packages/protocol/test/common/linkedlist.ts b/packages/protocol/test/common/linkedlist.ts index 0ba0f660739..e2b44764812 100644 --- a/packages/protocol/test/common/linkedlist.ts +++ b/packages/protocol/test/common/linkedlist.ts @@ -45,8 +45,9 @@ contract('LinkedListTest', () => { describe('when inserting to a list with more items', () => { beforeEach(async () => { await linkedListTest.insert(firstKey, NULL_KEY, NULL_KEY) - for (let i = 1; i < keys.length; i++) + for (let i = 1; i < keys.length; i++) { await linkedListTest.insert(keys[i], NULL_KEY, keys[i - 1]) + } }) it('should revert if next is equal to key (beginning)', async () => { diff --git a/packages/protocol/test/governance/blockchainparams.ts b/packages/protocol/test/governance/blockchainparams.ts index 9e196c8203d..eba70d539ab 100644 --- a/packages/protocol/test/governance/blockchainparams.ts +++ b/packages/protocol/test/governance/blockchainparams.ts @@ -1,6 +1,6 @@ -import { assertContainSubset, assertRevert, assertEqualBN } from '@celo/protocol/lib/test-utils' -import { BlockchainParametersContract, BlockchainParametersInstance } from 'types' +import { assertContainSubset, assertEqualBN, assertRevert } from '@celo/protocol/lib/test-utils' import { BigNumber } from 'bignumber.js' +import { BlockchainParametersContract, BlockchainParametersInstance } from 'types' const BlockchainParameters: BlockchainParametersContract = artifacts.require('BlockchainParameters') diff --git a/packages/protocol/test/governance/election.ts b/packages/protocol/test/governance/election.ts index 186f6811d7c..0021f906830 100644 --- a/packages/protocol/test/governance/election.ts +++ b/packages/protocol/test/governance/election.ts @@ -375,10 +375,10 @@ contract('Election', (accounts: string[]) => { }) describe('when the voter has already voted for this group', () => { - let resp: any + let response: any beforeEach(async () => { await mockLockedGold.incrementNonvotingAccountBalance(voter, value) - resp = await election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS) + response = await election.vote(group, value, NULL_ADDRESS, NULL_ADDRESS) }) it('should not change the list of groups the account has voted for', async () => { @@ -416,8 +416,8 @@ contract('Election', (accounts: string[]) => { }) it('should emit the ValidatorGroupVoteCast event', async () => { - assert.equal(resp.logs.length, 1) - const log = resp.logs[0] + assert.equal(response.logs.length, 1) + const log = response.logs[0] assertContainSubset(log, { event: 'ValidatorGroupVoteCast', args: { diff --git a/packages/protocol/test/governance/epochrewards.ts b/packages/protocol/test/governance/epochrewards.ts index 3a813846e1a..5c3c7994395 100644 --- a/packages/protocol/test/governance/epochrewards.ts +++ b/packages/protocol/test/governance/epochrewards.ts @@ -4,22 +4,23 @@ import { assertEqualBN, assertEqualDpBN, assertRevert, + jsonRpc, timeTravel, } from '@celo/protocol/lib/test-utils' +import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { + EpochRewardsTestContract, + EpochRewardsTestInstance, MockElectionContract, MockElectionInstance, MockGoldTokenContract, MockGoldTokenInstance, MockSortedOraclesContract, MockSortedOraclesInstance, - EpochRewardsTestContract, - EpochRewardsTestInstance, RegistryContract, RegistryInstance, } from 'types' -import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' const EpochRewards: EpochRewardsTestContract = artifacts.require('EpochRewardsTest') const MockElection: MockElectionContract = artifacts.require('MockElection') @@ -34,7 +35,7 @@ EpochRewards.numberFormat = 'BigNumber' const YEAR = new BigNumber(365 * 24 * 60 * 60) const SUPPLY_CAP = new BigNumber(web3.utils.toWei('1000000000')) -const getExpectedTargetTotalSupply = (timeDelta: BigNumber) => { +const getExpectedTargetTotalSupply = (timeDelta: BigNumber): BigNumber => { const genesisSupply = new BigNumber(web3.utils.toWei('600000000')) const linearRewards = new BigNumber(web3.utils.toWei('200000000')) return genesisSupply @@ -68,10 +69,13 @@ contract('EpochRewards', (accounts: string[]) => { const mockStableTokenAddress = web3.utils.randomHex(20) const sortedOraclesDenominator = new BigNumber('0x10000000000000000') const timeTravelToDelta = async (timeDelta: BigNumber) => { - const currentTime = new BigNumber((await web3.eth.getBlock('latest')).timestamp) - const startTime = await epochRewards.startTime() - const desiredTime = startTime.plus(timeDelta) - await timeTravel(desiredTime.minus(currentTime).toNumber(), web3) + // mine beforehand, just in case + await jsonRpc(web3, 'evm_mine', []) + const currentTime: BigNumber = new BigNumber((await web3.eth.getBlock('latest')).timestamp) + const startTime: BigNumber = await epochRewards.startTime() + const desiredTime: BigNumber = startTime.plus(timeDelta) + const delta: number = desiredTime.minus(currentTime).toNumber() + await timeTravel(delta, web3) } beforeEach(async () => { @@ -354,7 +358,7 @@ contract('EpochRewards', (accounts: string[]) => { describe('#getTargetGoldTotalSupply()', () => { describe('when it has been fewer than 15 years since genesis', () => { - const timeDelta = YEAR.times(10) + const timeDelta: BigNumber = YEAR.times(10) beforeEach(async () => { await timeTravelToDelta(timeDelta) }) diff --git a/packages/protocol/test/governance/governance.ts b/packages/protocol/test/governance/governance.ts index cb8c47e33bf..daf22cb17b0 100644 --- a/packages/protocol/test/governance/governance.ts +++ b/packages/protocol/test/governance/governance.ts @@ -642,7 +642,7 @@ contract('Governance', (accounts: string[]) => { args: { destination, functionId: web3.utils.padRight(functionId, 64), - threshold: threshold, + threshold, }, }) }) @@ -675,7 +675,7 @@ contract('Governance', (accounts: string[]) => { args: { destination, functionId: web3.utils.padRight(functionId, 64), - threshold: threshold, + threshold, }, }) }) @@ -777,7 +777,7 @@ contract('Governance', (accounts: string[]) => { proposalId: new BigNumber(1), proposer: accounts[0], deposit: new BigNumber(minDeposit), - timestamp: timestamp, + timestamp, transactionCount: 0, }, }) @@ -842,7 +842,7 @@ contract('Governance', (accounts: string[]) => { proposalId: new BigNumber(1), proposer: accounts[0], deposit: new BigNumber(minDeposit), - timestamp: timestamp, + timestamp, transactionCount: 1, }, }) @@ -915,7 +915,7 @@ contract('Governance', (accounts: string[]) => { proposalId: new BigNumber(1), proposer: accounts[0], deposit: new BigNumber(minDeposit), - timestamp: timestamp, + timestamp, transactionCount: 2, }, }) @@ -1039,10 +1039,10 @@ contract('Governance', (accounts: string[]) => { // @ts-ignore: TODO(mcortesi) fix typings for TransactionDetails { value: minDeposit } ) - const otherAccount = accounts[1] - await accountsInstance.createAccount({ from: otherAccount }) - await mockLockedGold.setAccountTotalLockedGold(otherAccount, weight) - await governance.upvote(otherProposalId, proposalId, 0, { from: otherAccount }) + const otherAccount1 = accounts[1] + await accountsInstance.createAccount({ from: otherAccount1 }) + await mockLockedGold.setAccountTotalLockedGold(otherAccount1, weight) + await governance.upvote(otherProposalId, proposalId, 0, { from: otherAccount1 }) await timeTravel(queueExpiry, web3) }) @@ -1105,11 +1105,11 @@ contract('Governance', (accounts: string[]) => { describe('when the previously upvoted proposal is in the queue and expired', () => { const upvotedProposalId = 2 // Expire the upvoted proposal without dequeueing it. - const queueExpiry = 60 + const queueExpiry1 = 60 beforeEach(async () => { await governance.setQueueExpiry(60) await governance.upvote(proposalId, 0, 0) - await timeTravel(queueExpiry, web3) + await timeTravel(queueExpiry1, web3) await governance.propose( [transactionSuccess1.value], [transactionSuccess1.destination], diff --git a/packages/protocol/test/governance/lockedgold.ts b/packages/protocol/test/governance/lockedgold.ts index d34083225d2..c5351c8c5c5 100644 --- a/packages/protocol/test/governance/lockedgold.ts +++ b/packages/protocol/test/governance/lockedgold.ts @@ -40,7 +40,7 @@ const HOUR = 60 * 60 const DAY = 24 * HOUR contract('LockedGold', (accounts: string[]) => { - let account = accounts[0] + const account = accounts[0] const nonOwner = accounts[1] const unlockingPeriod = 3 * DAY let accountsInstance: AccountsInstance diff --git a/packages/protocol/test/governance/validators.ts b/packages/protocol/test/governance/validators.ts index 190e17d55b3..4db673d0595 100644 --- a/packages/protocol/test/governance/validators.ts +++ b/packages/protocol/test/governance/validators.ts @@ -3,8 +3,8 @@ import { getParsedSignatureOfAddress } from '@celo/protocol/lib/signing-utils' import { assertContainSubset, assertEqualBN, - assertEqualDpBN, assertEqualBNArray, + assertEqualDpBN, assertRevert, assertSameAddress, mineBlocks, @@ -152,7 +152,7 @@ contract('Validators', (accounts: string[]) => { for (const validator of members) { await registerValidator(validator) await validators.affiliate(group, { from: validator }) - if (validator == members[0]) { + if (validator === members[0]) { await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS, { from: group }) } else { await validators.addMember(validator, { from: group }) @@ -607,7 +607,7 @@ contract('Validators', (accounts: string[]) => { args: { validator, ecdsaPublicKey: publicKey, - blsPublicKey: blsPublicKey, + blsPublicKey, }, }) }) @@ -908,7 +908,7 @@ contract('Validators', (accounts: string[]) => { await validators.getMembershipHistory(validator) ) let expectedEntries = 1 - if (registrationEpoch != additionEpoch || additionEpoch != affiliationEpoch) { + if (registrationEpoch !== additionEpoch || additionEpoch !== affiliationEpoch) { expectedEntries = 2 } assert.equal(membershipHistory.epochs.length, expectedEntries) @@ -1036,7 +1036,7 @@ contract('Validators', (accounts: string[]) => { await validators.getMembershipHistory(validator) ) let expectedEntries = 1 - if (registrationEpoch != additionEpoch || additionEpoch != deaffiliationEpoch) { + if (registrationEpoch !== additionEpoch || additionEpoch !== deaffiliationEpoch) { expectedEntries = 2 } assert.equal(membershipHistory.epochs.length, expectedEntries) @@ -1434,7 +1434,7 @@ contract('Validators', (accounts: string[]) => { }) it("should update the member's membership history", async () => { - const expectedEntries = registrationEpoch == additionEpoch ? 1 : 2 + const expectedEntries = registrationEpoch === additionEpoch ? 1 : 2 const membershipHistory = parseMembershipHistory( await validators.getMembershipHistory(validator) ) @@ -1480,14 +1480,14 @@ contract('Validators', (accounts: string[]) => { assert.equal(expectedSizeHistory.length, 1) for (let i = 2; i < maxGroupSize.toNumber() + 1; i++) { const numMembers = i - const validator = accounts[i] - await registerValidator(validator) - await validators.affiliate(group, { from: validator }) + const validator1 = accounts[i] + await registerValidator(validator1) + await validators.affiliate(group, { from: validator1 }) await mockLockedGold.setAccountTotalLockedGold( group, groupLockedGoldRequirements.value.times(numMembers) ) - await validators.addMember(validator) + await validators.addMember(validator1) expectedSizeHistory.push((await web3.eth.getBlock('latest')).timestamp) const parsedGroup = parseValidatorGroupParams( await validators.getValidatorGroup(group) @@ -1579,9 +1579,9 @@ contract('Validators', (accounts: string[]) => { // Depending on test timing, we may or may not span an epoch boundary between registration // and removal. const numEntries = membershipHistory.epochs.length - assert.isTrue(numEntries == 1 || numEntries == 2) + assert.isTrue(numEntries === 1 || numEntries === 2) assert.equal(membershipHistory.groups.length, numEntries) - if (numEntries == 1) { + if (numEntries === 1) { assertEqualBN(membershipHistory.epochs[0], expectedEpoch) assertSameAddress(membershipHistory.groups[0], NULL_ADDRESS) } else { @@ -1758,15 +1758,15 @@ contract('Validators', (accounts: string[]) => { describe('#calculateGroupEpochScore', () => { describe('when all uptimes are in the interval [0, 1.0]', () => { - const testGroupUptimeCalculation = (uptimes) => { - const expected = uptimes + const testGroupUptimeCalculation = (_uptimes) => { + const expected = _uptimes .map((uptime) => new BigNumber(uptime)) .map((uptime) => uptime.pow(validatorScoreParameters.exponent.toNumber())) .reduce((sum, n) => sum.plus(n)) - .div(uptimes.length) + .div(_uptimes.length) it('should calculate the group score correctly', async () => { assertEqualDpBN( - fromFixed(await validators.calculateGroupEpochScore(uptimes.map(toFixed))), + fromFixed(await validators.calculateGroupEpochScore(_uptimes.map(toFixed))), expected, 8 ) @@ -1962,7 +1962,7 @@ contract('Validators', (accounts: string[]) => { from: groups[i], }) - if (i == 0) { + if (i === 0) { assert.equal(await validators.getMembershipInLastEpoch(validator), NULL_ADDRESS) } else { assert.equal(await validators.getMembershipInLastEpoch(validator), groups[i - 1]) @@ -1994,7 +1994,7 @@ contract('Validators', (accounts: string[]) => { group, groupLockedGoldRequirements.value.times(i) ) - if (i == 1) { + if (i === 1) { await validators.addFirstMember(validator, NULL_ADDRESS, NULL_ADDRESS) } else { await validators.addMember(validator) diff --git a/packages/protocol/test/identity/attestations.ts b/packages/protocol/test/identity/attestations.ts index b99f84c1857..a49f0b29c7f 100644 --- a/packages/protocol/test/identity/attestations.ts +++ b/packages/protocol/test/identity/attestations.ts @@ -136,7 +136,7 @@ contract('Attestations', (accounts: string[]) => { const mockValidators = await MockValidators.new() attestations = await Attestations.new() random = await Random.new() - random.addTestRandomness(0, '0x00') + await random.addTestRandomness(0, '0x00') mockLockedGold = await MockLockedGold.new() registry = await Registry.new() await accountsInstance.initialize(registry.address) @@ -497,10 +497,10 @@ contract('Attestations', (accounts: string[]) => { }) it('should no longer list the attestations in getCompletableAttestations', async () => { - const [ - attestationBlockNumbers, - _attestationIssuers, - ] = await attestations.getCompletableAttestations(phoneHash, caller) + const [attestationBlockNumbers] = await attestations.getCompletableAttestations( + phoneHash, + caller + ) assert.lengthOf(attestationBlockNumbers, 0) }) @@ -554,12 +554,12 @@ contract('Attestations', (accounts: string[]) => { }) it('should increment the number of completed verification requests', async () => { - let [numCompleted, numTotal] = await attestations.getAttestationStats(phoneHash, caller) + const [numCompleted] = await attestations.getAttestationStats(phoneHash, caller) assert.equal(numCompleted.toNumber(), 0) await attestations.complete(phoneHash, v, r, s) - ;[numCompleted, numTotal] = await attestations.getAttestationStats(phoneHash, caller) - assert.equal(numCompleted.toNumber(), 1) + const [numCompleted2, numTotal] = await attestations.getAttestationStats(phoneHash, caller) + assert.equal(numCompleted2.toNumber(), 1) assert.equal(numTotal.toNumber(), attestationsRequested) }) @@ -591,11 +591,8 @@ contract('Attestations', (accounts: string[]) => { it('should no longer list the attestation in getCompletableAttestationStats', async () => { await attestations.complete(phoneHash, v, r, s) - const [ - _attestationBlockNumbers, - attestationIssuers, - ] = await attestations.getCompletableAttestations(phoneHash, caller) - assert.equal(attestationIssuers.indexOf(issuer), -1) + const [attestationIssuers] = await attestations.getCompletableAttestations(phoneHash, caller) + assert.equal(attestationIssuers.indexOf(new BigNumber(issuer)), -1) }) it('should emit the AttestationCompleted event', async () => { @@ -635,11 +632,7 @@ contract('Attestations', (accounts: string[]) => { it('should mark the attestation by the issuer as complete', async () => { await attestations.complete(phoneHash, v, r, s) - const [status, _blockNumber] = await attestations.getAttestationState( - phoneHash, - caller, - issuer - ) + const [status] = await attestations.getAttestationState(phoneHash, caller, issuer) assert.equal(status.toNumber(), 2) }) }) diff --git a/packages/protocol/test/identity/random.ts b/packages/protocol/test/identity/random.ts index 2ffca631849..84f047db049 100644 --- a/packages/protocol/test/identity/random.ts +++ b/packages/protocol/test/identity/random.ts @@ -1,7 +1,7 @@ -import { assertRevert, assertEqualBN, assertContainSubset } from '@celo/protocol/lib/test-utils' +import { assertContainSubset, assertEqualBN, assertRevert } from '@celo/protocol/lib/test-utils' -import { TestRandomContract, TestRandomInstance } from 'types' import { BigNumber } from 'bignumber.js' +import { TestRandomContract, TestRandomInstance } from 'types' const Random: TestRandomContract = artifacts.require('TestRandom') @@ -14,7 +14,7 @@ contract('Random', (accounts: string[]) => { beforeEach(async () => { random = await Random.new() - random.initialize(256) + await random.initialize(256) }) describe('#setRandomnessRetentionWindow()', () => { @@ -36,7 +36,7 @@ contract('Random', (accounts: string[]) => { }) it('only owner can set', async () => { - assertRevert(random.setRandomnessBlockRetentionWindow(1000, { from: accounts[1] })) + await assertRevert(random.setRandomnessBlockRetentionWindow(1000, { from: accounts[1] })) }) }) @@ -75,7 +75,7 @@ contract('Random', (accounts: string[]) => { assert.equal(randomValues[5], await random.getTestRandomness(5, 5)) }) it('cannot read old blocks', async () => { - assertRevert(random.getTestRandomness(3, 5)) + await assertRevert(random.getTestRandomness(3, 5)) }) }) @@ -93,7 +93,7 @@ contract('Random', (accounts: string[]) => { assert.equal(randomValues[5], await random.getTestRandomness(5, 5)) }) it('cannot read old blocks', async () => { - assertRevert(random.getTestRandomness(1, 5)) + await assertRevert(random.getTestRandomness(1, 5)) }) it('old values are preserved', async () => { await random.addTestRandomness(5, randomValues[5]) diff --git a/packages/protocol/test/stability/exchange.ts b/packages/protocol/test/stability/exchange.ts index a75de0bb0bd..ce3c922403b 100644 --- a/packages/protocol/test/stability/exchange.ts +++ b/packages/protocol/test/stability/exchange.ts @@ -6,7 +6,7 @@ import { isSameAddress, timeTravel, } from '@celo/protocol/lib/test-utils' -import { fixed1, toFixed, fromFixed, multiply } from '@celo/utils/lib/fixidity' +import { fixed1, fromFixed, multiply, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import { ExchangeInstance, diff --git a/packages/protocol/test/stability/reserve.ts b/packages/protocol/test/stability/reserve.ts index 9ed594436ac..6edd67a6279 100644 --- a/packages/protocol/test/stability/reserve.ts +++ b/packages/protocol/test/stability/reserve.ts @@ -6,6 +6,7 @@ import { timeTravel, } from '@celo/protocol/lib/test-utils' import BigNumber from 'bignumber.js' +import BN = require('bn.js') import { MockGoldTokenInstance, MockSortedOraclesInstance, @@ -13,7 +14,6 @@ import { RegistryInstance, ReserveInstance, } from 'types' -import BN = require('bn.js') const Registry: Truffle.Contract = artifacts.require('Registry') const Reserve: Truffle.Contract = artifacts.require('Reserve') diff --git a/packages/protocol/test/stability/sortedoracles.ts b/packages/protocol/test/stability/sortedoracles.ts index bac7cce8526..2260e0128fb 100644 --- a/packages/protocol/test/stability/sortedoracles.ts +++ b/packages/protocol/test/stability/sortedoracles.ts @@ -4,8 +4,8 @@ import { assertRevert, matchAddress, matchAny, - timeTravel, NULL_ADDRESS, + timeTravel, } from '@celo/protocol/lib/test-utils' import BigNumber from 'bignumber.js' import { SortedOraclesContract, SortedOraclesInstance } from 'types' @@ -386,7 +386,7 @@ contract('SortedOracles', (accounts: string[]) => { ) beforeEach(async () => { - sortedOracles.addOracle(aToken, anotherOracle) + await sortedOracles.addOracle(aToken, anotherOracle) await sortedOracles.report(aToken, anotherOracleNumerator, 1, NULL_ADDRESS, NULL_ADDRESS, { from: anotherOracle, }) diff --git a/packages/protocol/test/stability/stabletoken.ts b/packages/protocol/test/stability/stabletoken.ts index 87557e3dbec..e25a5ccba89 100644 --- a/packages/protocol/test/stability/stabletoken.ts +++ b/packages/protocol/test/stability/stabletoken.ts @@ -236,7 +236,7 @@ contract('StableToken', (accounts: string[]) => { assertLogMatches2(res.logs[0], { event: 'InflationFactorUpdated', args: { - factor: factor, + factor, lastUpdated, }, }) diff --git a/packages/protocol/tsconfig.json b/packages/protocol/tsconfig.json index 33321908982..08064c34136 100644 --- a/packages/protocol/tsconfig.json +++ b/packages/protocol/tsconfig.json @@ -15,6 +15,6 @@ "target": "es5", "downlevelIteration": true }, - "exclude": ["test/", "node_modules"], + "exclude": ["node_modules"], "references": [{ "path": "../utils" }] } From 61bfc10746a1577f3b5cbc2e7b9adeb4170638fd Mon Sep 17 00:00:00 2001 From: Asa Oines Date: Thu, 12 Dec 2019 01:30:52 -0800 Subject: [PATCH 13/40] Improve reliability of e2e governance test (#2208) --- packages/celotool/src/e2e-tests/governance_tests.ts | 1 + packages/celotool/src/e2e-tests/utils.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/celotool/src/e2e-tests/governance_tests.ts b/packages/celotool/src/e2e-tests/governance_tests.ts index 8b77cf7c868..f0784a422e0 100644 --- a/packages/celotool/src/e2e-tests/governance_tests.ts +++ b/packages/celotool/src/e2e-tests/governance_tests.ts @@ -352,6 +352,7 @@ describe('governance tests', () => { // Wait for an epoch transition so we can activate our vote. await waitForEpochTransition(epoch) + await sleep(5) // Wait for an extra epoch transition to ensure everyone is connected to one another. await waitForEpochTransition(epoch) diff --git a/packages/celotool/src/e2e-tests/utils.ts b/packages/celotool/src/e2e-tests/utils.ts index 7f93d25fabe..93d770583e3 100644 --- a/packages/celotool/src/e2e-tests/utils.ts +++ b/packages/celotool/src/e2e-tests/utils.ts @@ -203,7 +203,7 @@ async function setupTestDir(testDir: string) { function writeGenesis(validators: Validator[], path: string, configOverrides: any = {}) { const genesis = generateGenesis({ validators, - blockTime: 0, + blockTime: 2, // Slow down block times to improve reliability. epoch: 10, lookbackwindow: 2, requestTimeout: 3000, From 5b84b23a3639c4022f536cb5825d4c6385b84b2f Mon Sep 17 00:00:00 2001 From: Jean Regisser Date: Thu, 12 Dec 2019 12:15:22 +0100 Subject: [PATCH 14/40] [BlockchainApi] Add ability to get exchange rates from/to cGLD or cUSD (#2005) --- packages/blockchain-api/.env | 1 + packages/blockchain-api/.gcloudignore | 3 + packages/blockchain-api/.gitignore | 3 +- .../__mocks__/@celo/contractkit.ts | 2 + .../__mocks__/firebase-admin.ts | 2 + packages/blockchain-api/app.alfajores.yaml | 1 + .../blockchain-api/app.alfajoresstaging.yaml | 1 + packages/blockchain-api/app.integration.yaml | 1 + packages/blockchain-api/app.pilot.yaml | 1 + packages/blockchain-api/app.pilotstaging.yaml | 1 + packages/blockchain-api/package.json | 1 + .../blockchain-api/serviceAccountKey.json.enc | Bin 0 -> 2429 bytes packages/blockchain-api/src/apolloServer.ts | 2 +- packages/blockchain-api/src/config.ts | 24 ++++ .../CurrencyConversionAPI.test.ts | 134 ++++++++++++++++++ .../CurrencyConversionAPI.ts | 98 +++++++++++++ .../ExchangeRateAPI.test.ts} | 83 ++++++----- .../ExchangeRateAPI.ts} | 45 +++--- .../GoldExchangeRateAPI.test.ts | 96 +++++++++++++ .../currencyConversion/GoldExchangeRateAPI.ts | 75 ++++++++++ .../src/currencyConversion/consts.ts | 5 + packages/blockchain-api/src/firebase.ts | 14 ++ packages/blockchain-api/src/schema.ts | 10 +- packages/mobile/src/firebase/db-rules.json | 9 ++ packages/notification-service/src/config.ts | 5 +- scripts/key_placer.sh | 1 + yarn.lock | 26 ++-- 27 files changed, 563 insertions(+), 81 deletions(-) create mode 100644 packages/blockchain-api/__mocks__/@celo/contractkit.ts create mode 100644 packages/blockchain-api/__mocks__/firebase-admin.ts create mode 100644 packages/blockchain-api/serviceAccountKey.json.enc create mode 100644 packages/blockchain-api/src/currencyConversion/CurrencyConversionAPI.test.ts create mode 100644 packages/blockchain-api/src/currencyConversion/CurrencyConversionAPI.ts rename packages/blockchain-api/{test/currencyConversion.test.ts => src/currencyConversion/ExchangeRateAPI.test.ts} (51%) rename packages/blockchain-api/src/{currencyConversion.ts => currencyConversion/ExchangeRateAPI.ts} (50%) create mode 100644 packages/blockchain-api/src/currencyConversion/GoldExchangeRateAPI.test.ts create mode 100644 packages/blockchain-api/src/currencyConversion/GoldExchangeRateAPI.ts create mode 100644 packages/blockchain-api/src/currencyConversion/consts.ts create mode 100644 packages/blockchain-api/src/firebase.ts diff --git a/packages/blockchain-api/.env b/packages/blockchain-api/.env index 04eedcecf08..a8b4881e5d2 100644 --- a/packages/blockchain-api/.env +++ b/packages/blockchain-api/.env @@ -1,6 +1,7 @@ DEPLOY_ENV=local EXCHANGE_RATES_API=https://apilayer.net/api BLOCKSCOUT_API=https://integration-blockscout.celo-testnet.org/api +FIREBASE_DB=https://celo-org-mobile-int.firebaseio.com FAUCET_ADDRESS=0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95 VERIFICATION_REWARDS_ADDRESS=0xb4fdaf5f3cd313654aa357299ada901b1d2dd3b5 WEB3_PROVIDER_URL=https://integration-forno.celo-testnet.org diff --git a/packages/blockchain-api/.gcloudignore b/packages/blockchain-api/.gcloudignore index b096109bc54..6a5a76e41cd 100644 --- a/packages/blockchain-api/.gcloudignore +++ b/packages/blockchain-api/.gcloudignore @@ -6,3 +6,6 @@ dist/ # Exclude dependencies node_modules/ .env + +# Exclude tests +*.test.ts diff --git a/packages/blockchain-api/.gitignore b/packages/blockchain-api/.gitignore index 596eabddabb..d8a70edba36 100644 --- a/packages/blockchain-api/.gitignore +++ b/packages/blockchain-api/.gitignore @@ -6,5 +6,6 @@ dist/ # Exclude dependencies node_modules/ -# keys +# Exclude secrets +serviceAccountKey.json src/secrets.json \ No newline at end of file diff --git a/packages/blockchain-api/__mocks__/@celo/contractkit.ts b/packages/blockchain-api/__mocks__/@celo/contractkit.ts new file mode 100644 index 00000000000..3c3c25c9e00 --- /dev/null +++ b/packages/blockchain-api/__mocks__/@celo/contractkit.ts @@ -0,0 +1,2 @@ +// Empty implementation because it currently adds ~8 secs for each test +// TODO: investigate why diff --git a/packages/blockchain-api/__mocks__/firebase-admin.ts b/packages/blockchain-api/__mocks__/firebase-admin.ts new file mode 100644 index 00000000000..3beab481632 --- /dev/null +++ b/packages/blockchain-api/__mocks__/firebase-admin.ts @@ -0,0 +1,2 @@ +export const initializeApp = jest.fn() +export const database = jest.fn() diff --git a/packages/blockchain-api/app.alfajores.yaml b/packages/blockchain-api/app.alfajores.yaml index 7e7bdce3a19..ef4dfb85af6 100644 --- a/packages/blockchain-api/app.alfajores.yaml +++ b/packages/blockchain-api/app.alfajores.yaml @@ -5,6 +5,7 @@ env_variables: DEPLOY_ENV: "alfajores" EXCHANGE_RATES_API: "https://apilayer.net/api" BLOCKSCOUT_API: "https://alfajores-blockscout.celo-testnet.org/api" + FIREBASE_DB: "https://celo-org-mobile-alfajores.firebaseio.com" # TODO Pull addresses from the build artifacts of the network in protocol/build FAUCET_ADDRESS: "0xCEa3eF8e187490A9d85A1849D98412E5D27D1Bb3" VERIFICATION_REWARDS_ADDRESS: "0xb4fdaf5f3cd313654aa357299ada901b1d2dd3b5" diff --git a/packages/blockchain-api/app.alfajoresstaging.yaml b/packages/blockchain-api/app.alfajoresstaging.yaml index 7379a4529c5..2d8efe9ddef 100644 --- a/packages/blockchain-api/app.alfajoresstaging.yaml +++ b/packages/blockchain-api/app.alfajoresstaging.yaml @@ -5,6 +5,7 @@ env_variables: DEPLOY_ENV: "alfajoresstaging" EXCHANGE_RATES_API: "https://apilayer.net/api" BLOCKSCOUT_API: "https://alfajoresstaging-blockscout.celo-testnet.org/api" + FIREBASE_DB: "https://celo-org-mobile-alfajoresstaging.firebaseio.com" # TODO Pull addresses from the build artifacts of the network in protocol/build FAUCET_ADDRESS: "0xF4314cb9046bECe6AA54bb9533155434d0c76909" VERIFICATION_REWARDS_ADDRESS: "0xb4fdaf5f3cd313654aa357299ada901b1d2dd3b5" diff --git a/packages/blockchain-api/app.integration.yaml b/packages/blockchain-api/app.integration.yaml index 69d7dfe97a5..df237830d74 100644 --- a/packages/blockchain-api/app.integration.yaml +++ b/packages/blockchain-api/app.integration.yaml @@ -5,6 +5,7 @@ env_variables: DEPLOY_ENV: "integration" EXCHANGE_RATES_API: "https://apilayer.net/api" BLOCKSCOUT_API: "https://integration-blockscout.celo-testnet.org/api" + FIREBASE_DB: "https://celo-org-mobile-int.firebaseio.com" # TODO Pull addresses from the build artifacts of the network in protocol/build FAUCET_ADDRESS: "0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95" VERIFICATION_REWARDS_ADDRESS: "0xb4fdaf5f3cd313654aa357299ada901b1d2dd3b5" diff --git a/packages/blockchain-api/app.pilot.yaml b/packages/blockchain-api/app.pilot.yaml index de2234b639e..dfbe45e10c3 100644 --- a/packages/blockchain-api/app.pilot.yaml +++ b/packages/blockchain-api/app.pilot.yaml @@ -5,6 +5,7 @@ env_variables: DEPLOY_ENV: "pilot" EXCHANGE_RATES_API: "https://apilayer.net/api" BLOCKSCOUT_API: "https://pilot-blockscout.celo-testnet.org/api" + FIREBASE_DB: "https://celo-org-mobile-pilot.firebaseio.com" # TODO Pull addresses from the build artifacts of the network in protocol/build FAUCET_ADDRESS: "0x387bCb16Bfcd37AccEcF5c9eB2938E30d3aB8BF2" VERIFICATION_REWARDS_ADDRESS: "0xb4fdaf5f3cd313654aa357299ada901b1d2dd3b5" diff --git a/packages/blockchain-api/app.pilotstaging.yaml b/packages/blockchain-api/app.pilotstaging.yaml index 6df02bcd67b..dd9e1958cd5 100644 --- a/packages/blockchain-api/app.pilotstaging.yaml +++ b/packages/blockchain-api/app.pilotstaging.yaml @@ -5,6 +5,7 @@ env_variables: DEPLOY_ENV: "pilotstaging" EXCHANGE_RATES_API: "https://apilayer.net/api" BLOCKSCOUT_API: "https://pilotstaging-blockscout.celo-testnet.org/api" + FIREBASE_DB: "https://celo-org-mobile-pilotstaging.firebaseio.com" # TODO Pull addresses from the build artifacts of the network in protocol/build FAUCET_ADDRESS: "0x545DEBe3030B570731EDab192640804AC8Cf65CA" VERIFICATION_REWARDS_ADDRESS: "0xb4fdaf5f3cd313654aa357299ada901b1d2dd3b5" diff --git a/packages/blockchain-api/package.json b/packages/blockchain-api/package.json index 2b190122466..d8ed27a330e 100644 --- a/packages/blockchain-api/package.json +++ b/packages/blockchain-api/package.json @@ -23,6 +23,7 @@ "bignumber.js": "^7.2.0", "dotenv": "^6.1.0", "express": "^4.16.4", + "firebase-admin": "^8.6.1", "graphql": "^14.1.1", "utf8": "^3.0.0", "web3-eth-abi": "1.0.0-beta.37" diff --git a/packages/blockchain-api/serviceAccountKey.json.enc b/packages/blockchain-api/serviceAccountKey.json.enc new file mode 100644 index 0000000000000000000000000000000000000000..dd9475ce349757fc1277be87ade6a859bbce46ee GIT binary patch literal 2429 zcmV-@34-*EVbLEBBbVI+a8q$``6oAYC~uu}!(>3$yWToTk002zV- zE<@TN69_;H$_%w8F2ai+JJo8eF>dD~ZhnPXQJ!7Pj-1`~Q|=S7owg@=A?2TG3g2p? z4y_t}%sY+d36ti7LIF#jdqwP>^V^5c_WW?Pffbkcb{KDWs){-)i+Y2Ehxc}>t&`*@ z<>pue{hv_P`s-?${eJJ(0o&QiG9qBg%!;GfqZN+iqI*>?)c{xV2?SctAuN{t5Z~ZN z+v7y1EK!P|zV@{9V{^CiXO?OM0Q}p(Qn{njK*k{c* z3Z&Zn^$1=N?V%FRq1`j9-r2S8!s^v6%-%uIPVHJzBmT8r*UUTgvV$=Mza2f@cyut9 zAMvG3KNf@A=P@tkdLRf${PrZS4xc-Tuk5!_n-3$E1K5W6Iut2_U2UN{J7gINy5WC5 z0^6D;oG`Cf8x>Kt+G5?WIJ|&m7-OVLf1CqEDBf#A#`Yt@wwfAL(}-z=9A*I(-%kAKQjqM%ga8AFTu5ChQ5 zbNHpe{o~|X%&Vx1@%+m%ifw3(o-SA7F@nXy88q4^%@JOxm6*X3V{QmRQR~R&nB5)$ zADm__skqBxUY^D)L&;~Ik_uQ;L$to6qSE37lE*iAuV80{QZip1L z5z#?4mL7zY=DtVHNdQQ&O(~~=F1KKuZC3%hvrNfol4Otr0cC+)e&Y*e4zro#d<*mh z+l}U(wLNcc#LK(8UqH{sLx}|bwrzQ~WH_o|!>%_w;@X7`1xeF~%qzU=65GUxvrsnT znDu|MU#}Hn9N%+z1eZ_hoM7L0MTW_N9^UAR)BFeBmgRiotO?IVnx1m?H7u2HoW$sR zO&`OpyX5lg^xd8R$s{MCkP9GS@9AZOfWW5{gQ4FBnG>Rf2RbULgRG#Mcn6qSncd*} zYv5RG{3-A`tOWKoBrWFbiF*=YIcuKH3g$=!v9J#TW3N{)_w5`S_i!fhpG8U0wU1qF|FPai+DWHG>@h$1D2QnIlDW zZ-3Doa(B-autFzCYBCsd7N}0`*7Z+E_Zw$I{+~dK+}0C+B&zP&eRaAy<*b`zDOop6ITaVcC~M>NUc+J z6Jx2*fQUyaDh;SGqe4g>bTtpDWH zZ0ZUaj`0ho@@g=pYFz>d6tw9z+<-WFt}HTdC56yHz)RyIEjG>#`zZ)kT_9U-UBoxY zFOM|Js7J-q4UIh;ey<#jWJB`wB2SV@N7VEuUa#Scvh+(euaQZ0@X4ZRi>3wf zbDGJ!GD{B8%ULeCM3YBAZk}@Jn`Ps&2`-pdP^vTG6<;YN?Gd|*=-_Wb@&KA>XOkkn zLr@>5_&1W~w#PbKU4%iTSyHIsw;_RV4SCO|{F9DU0k&8dvw84)!;IjKW=|5!`w*1f z=n8#mU`-iUrfY+ge@bn63!CDOEnORRLTow-Tc2vb*2m~ju4D! z%a>TT({2}>9jfTI04RHdFT(qqz@xVc|7nD(syjRv`9v)D?N^xNdJw)QiR>GgIj%cs zsXoh#MrffkU%;+ioSh$7M`H5jgZO+aA?O$#wqKY+&^)X&hTp>9`@of-5i}^AU~V&= zr2A{x!SSH!^EHx*((k|wMVCD)r9AU@r8vA&;Rt%oj={a$mnhJM53%!fkmul`wncye zl>Z2ro`$aGHBeV(f12_j!SO*S5XW371ySs>Vd4X^Ts};{unt-U82}hAZ~TLNyg?{v zH;;^%-ZAywqc~06ZWJ6mu7-~M5+b9Cj71JengW}73D_cpx?s(7RT;J>Um49~5>Yx> zEAR!SGxM-adrHdyM{k#1WvuxV7c~JkyP-(dY^B43>`X6KwrCdD4CoLIak-o1U#)Fr zuTvJOzhO_<^9Ma!9<;C+vvw7|%l0z5dZ vB+FnreO9<33rDW1Q;WIF3AicaZ literal 0 HcmV?d00001 diff --git a/packages/blockchain-api/src/apolloServer.ts b/packages/blockchain-api/src/apolloServer.ts index dc9031e833a..60771534fcc 100644 --- a/packages/blockchain-api/src/apolloServer.ts +++ b/packages/blockchain-api/src/apolloServer.ts @@ -1,6 +1,6 @@ import { ApolloServer } from 'apollo-server-express' import { BlockscoutAPI } from './blockscout' -import { CurrencyConversionAPI } from './currencyConversion' +import CurrencyConversionAPI from './currencyConversion/CurrencyConversionAPI' import { resolvers, typeDefs } from './schema' export interface DataSources { diff --git a/packages/blockchain-api/src/config.ts b/packages/blockchain-api/src/config.ts index 9eac8d86479..336aee37e44 100644 --- a/packages/blockchain-api/src/config.ts +++ b/packages/blockchain-api/src/config.ts @@ -14,10 +14,34 @@ function getSecrets(deployEnv: string) { return envSecrets } +export function getFirebaseAdminCreds(admin: any) { + // TODO: move project to celo-org-mobile + // until then, using serviceAccountKey for all envs + // tslint:disable-next-line: no-constant-condition + if (true /* DEPLOY_ENV === 'local' */) { + try { + const serviceAccount = require('../serviceAccountKey.json') + return admin.credential.cert(serviceAccount) + } catch (error) { + console.error( + 'Error: Could not initialize admin credentials. Is serviceAccountKey.json missing?', + error + ) + } + } else { + try { + return admin.credential.applicationDefault() + } catch (error) { + console.error('Error: Could not retrieve default app creds', error) + } + } +} + export const DEPLOY_ENV = (process.env.DEPLOY_ENV as string).toLowerCase() export const EXCHANGE_RATES_API = (process.env.EXCHANGE_RATES_API as string).toLowerCase() export const { EXCHANGE_RATES_API_ACCESS_KEY } = getSecrets(DEPLOY_ENV) export const BLOCKSCOUT_API = (process.env.BLOCKSCOUT_API as string).toLowerCase() +export const FIREBASE_DB = process.env.FIREBASE_DB export const FAUCET_ADDRESS = (process.env.FAUCET_ADDRESS as string).toLowerCase() export const VERIFICATION_REWARDS_ADDRESS = (process.env .VERIFICATION_REWARDS_ADDRESS as string).toLowerCase() diff --git a/packages/blockchain-api/src/currencyConversion/CurrencyConversionAPI.test.ts b/packages/blockchain-api/src/currencyConversion/CurrencyConversionAPI.test.ts new file mode 100644 index 00000000000..799448ecc85 --- /dev/null +++ b/packages/blockchain-api/src/currencyConversion/CurrencyConversionAPI.test.ts @@ -0,0 +1,134 @@ +import { InMemoryLRUCache } from 'apollo-server-caching' +import BigNumber from 'bignumber.js' +import CurrencyConversionAPI from './CurrencyConversionAPI' +import ExchangeRateAPI from './ExchangeRateAPI' +import GoldExchangeRateAPI from './GoldExchangeRateAPI' + +jest.mock('./ExchangeRateAPI') +jest.mock('./GoldExchangeRateAPI') + +const mockDefaultGetExchangeRate = ExchangeRateAPI.prototype.getExchangeRate as jest.Mock +mockDefaultGetExchangeRate.mockResolvedValue(new BigNumber(20)) + +const mockGoldGetExchangeRate = GoldExchangeRateAPI.prototype.getExchangeRate as jest.Mock +mockGoldGetExchangeRate.mockResolvedValue(new BigNumber(10)) + +describe('CurrencyConversionAPI', () => { + let currencyConversionAPI: CurrencyConversionAPI + + beforeEach(() => { + jest.clearAllMocks() + currencyConversionAPI = new CurrencyConversionAPI() + currencyConversionAPI.initialize({ context: {}, cache: new InMemoryLRUCache() }) + }) + + it('should retrieve rate for cGLD/cUSD', async () => { + const result = await currencyConversionAPI.getExchangeRate({ + sourceCurrencyCode: 'cGLD', + currencyCode: 'cUSD', + }) + expect(result).toEqual(new BigNumber(10)) + expect(mockDefaultGetExchangeRate).toHaveBeenCalledTimes(0) + expect(mockGoldGetExchangeRate).toHaveBeenCalledTimes(1) + }) + + it('should retrieve rate for cUSD/cGLD', async () => { + const result = await currencyConversionAPI.getExchangeRate({ + sourceCurrencyCode: 'cUSD', + currencyCode: 'cGLD', + }) + expect(result).toEqual(new BigNumber(10)) + expect(mockDefaultGetExchangeRate).toHaveBeenCalledTimes(0) + expect(mockGoldGetExchangeRate).toHaveBeenCalledTimes(1) + }) + + it('should retrieve rate for cGLD/USD', async () => { + const result = await currencyConversionAPI.getExchangeRate({ + sourceCurrencyCode: 'cGLD', + currencyCode: 'USD', + }) + expect(result).toEqual(new BigNumber(10)) + expect(mockDefaultGetExchangeRate).toHaveBeenCalledTimes(0) + expect(mockGoldGetExchangeRate).toHaveBeenCalledTimes(1) + }) + + it('should retrieve rate for USD/cGLD', async () => { + const result = await currencyConversionAPI.getExchangeRate({ + sourceCurrencyCode: 'USD', + currencyCode: 'cGLD', + }) + expect(result).toEqual(new BigNumber(10)) + expect(mockDefaultGetExchangeRate).toHaveBeenCalledTimes(0) + expect(mockGoldGetExchangeRate).toHaveBeenCalledTimes(1) + }) + + it('should retrieve rate for cGLD/MXN', async () => { + const result = await currencyConversionAPI.getExchangeRate({ + sourceCurrencyCode: 'cGLD', + currencyCode: 'MXN', + }) + expect(result).toEqual(new BigNumber(200)) + expect(mockDefaultGetExchangeRate).toHaveBeenCalledTimes(1) + expect(mockGoldGetExchangeRate).toHaveBeenCalledTimes(1) + }) + + it('should retrieve rate for MXN/cGLD', async () => { + const result = await currencyConversionAPI.getExchangeRate({ + sourceCurrencyCode: 'MXN', + currencyCode: 'cGLD', + }) + expect(result).toEqual(new BigNumber(200)) + expect(mockDefaultGetExchangeRate).toHaveBeenCalledTimes(1) + expect(mockGoldGetExchangeRate).toHaveBeenCalledTimes(1) + }) + + it('should retrieve rate for USD/MXN', async () => { + const result = await currencyConversionAPI.getExchangeRate({ + sourceCurrencyCode: 'USD', + currencyCode: 'MXN', + }) + expect(result).toEqual(new BigNumber(20)) + expect(mockDefaultGetExchangeRate).toHaveBeenCalledTimes(1) + expect(mockGoldGetExchangeRate).toHaveBeenCalledTimes(0) + }) + + it('should retrieve rate for MXN/USD', async () => { + const result = await currencyConversionAPI.getExchangeRate({ + sourceCurrencyCode: 'MXN', + currencyCode: 'USD', + }) + expect(result).toEqual(new BigNumber(20)) + expect(mockDefaultGetExchangeRate).toHaveBeenCalledTimes(1) + expect(mockGoldGetExchangeRate).toHaveBeenCalledTimes(0) + }) + + it('should retrieve rate for cUSD/MXN', async () => { + const result = await currencyConversionAPI.getExchangeRate({ + sourceCurrencyCode: 'cUSD', + currencyCode: 'MXN', + }) + expect(result).toEqual(new BigNumber(20)) + expect(mockDefaultGetExchangeRate).toHaveBeenCalledTimes(1) + expect(mockGoldGetExchangeRate).toHaveBeenCalledTimes(0) + }) + + it('should retrieve rate for MXN/cUSD', async () => { + const result = await currencyConversionAPI.getExchangeRate({ + sourceCurrencyCode: 'MXN', + currencyCode: 'cUSD', + }) + expect(result).toEqual(new BigNumber(20)) + expect(mockDefaultGetExchangeRate).toHaveBeenCalledTimes(1) + expect(mockGoldGetExchangeRate).toHaveBeenCalledTimes(0) + }) + + it('should return 1 when using the same currency code', async () => { + const result = await currencyConversionAPI.getExchangeRate({ + sourceCurrencyCode: 'ABC', + currencyCode: 'ABC', + }) + expect(result).toEqual(new BigNumber(1)) + expect(mockDefaultGetExchangeRate).toHaveBeenCalledTimes(0) + expect(mockGoldGetExchangeRate).toHaveBeenCalledTimes(0) + }) +}) diff --git a/packages/blockchain-api/src/currencyConversion/CurrencyConversionAPI.ts b/packages/blockchain-api/src/currencyConversion/CurrencyConversionAPI.ts new file mode 100644 index 00000000000..d6f97004cec --- /dev/null +++ b/packages/blockchain-api/src/currencyConversion/CurrencyConversionAPI.ts @@ -0,0 +1,98 @@ +import { DataSource, DataSourceConfig } from 'apollo-datasource' +import BigNumber from 'bignumber.js' +import { CurrencyConversionArgs } from '../schema' +import { CGLD, CUSD, USD } from './consts' +import ExchangeRateAPI from './ExchangeRateAPI' +import GoldExchangeRateAPI from './GoldExchangeRateAPI' + +function insertIf(condition: boolean, element: T) { + return condition ? [element] : [] +} + +export default class CurrencyConversionAPI extends DataSource { + exchangeRateAPI = new ExchangeRateAPI() + goldExchangeRateAPI = new GoldExchangeRateAPI() + + initialize(config: DataSourceConfig): void { + this.exchangeRateAPI.initialize(config) + this.goldExchangeRateAPI.initialize(config) + } + + async getExchangeRate({ + sourceCurrencyCode, + currencyCode, + timestamp, + }: CurrencyConversionArgs): Promise { + const fromCode = sourceCurrencyCode || USD + const toCode = currencyCode + + const steps = this.getConversionSteps(fromCode, toCode) + + const ratesPromises = [] + for (let i = 1; i < steps.length; i++) { + const prevCode = steps[i - 1] + const code = steps[i] + ratesPromises.push(this.getSupportedExchangeRate(prevCode, code, timestamp)) + } + + const rates = await Promise.all(ratesPromises) + + // Multiply all rates + return rates.reduce((acc, rate) => acc.multipliedBy(rate), new BigNumber(1)) + } + + // Get conversion steps given the data we have today + // Going from cGLD to local currency (or vice versa) is currently assumed to be the same as cGLD -> cUSD -> USD -> local currency. + // And similar to cUSD to local currency, but with one less step. + private getConversionSteps(fromCode: string, toCode: string) { + if (fromCode === toCode) { + // Same code, nothing to do + return [] + } else if (fromCode === CGLD || toCode === CGLD) { + // cGLD -> X (where X !== cUSD) + if (fromCode === CGLD && toCode !== CUSD) { + return [CGLD, CUSD, ...insertIf(toCode !== USD, USD), toCode] + } + // X -> cGLD (where X !== cUSD) + else if (fromCode !== CUSD && toCode === CGLD) { + return [fromCode, ...insertIf(fromCode !== USD, USD), CUSD, CGLD] + } + } else { + // cUSD -> X (where X !== USD) + if (fromCode === CUSD && toCode !== USD) { + return [CUSD, USD, toCode] + } + // X -> cUSD (where X !== USD) + else if (fromCode !== USD && toCode === CUSD) { + return [fromCode, USD, CUSD] + } + } + + return [fromCode, toCode] + } + + private getSupportedExchangeRate( + fromCode: string, + toCode: string, + timestamp?: number + ): Promise { + const pair = `${fromCode}/${toCode}` + + if (pair === 'cUSD/USD' || pair === 'USD/cUSD') { + // TODO: use real rates once we have the data + return Promise.resolve(new BigNumber(1)) + } else if (pair === 'cGLD/cUSD' || pair === 'cUSD/cGLD') { + return this.goldExchangeRateAPI.getExchangeRate({ + sourceCurrencyCode: fromCode, + currencyCode: toCode, + timestamp, + }) + } else { + return this.exchangeRateAPI.getExchangeRate({ + sourceCurrencyCode: fromCode, + currencyCode: toCode, + timestamp, + }) + } + } +} diff --git a/packages/blockchain-api/test/currencyConversion.test.ts b/packages/blockchain-api/src/currencyConversion/ExchangeRateAPI.test.ts similarity index 51% rename from packages/blockchain-api/test/currencyConversion.test.ts rename to packages/blockchain-api/src/currencyConversion/ExchangeRateAPI.test.ts index fd3ac01cfe0..d1713cff803 100644 --- a/packages/blockchain-api/test/currencyConversion.test.ts +++ b/packages/blockchain-api/src/currencyConversion/ExchangeRateAPI.test.ts @@ -1,6 +1,7 @@ import { InMemoryLRUCache } from 'apollo-server-caching' +import BigNumber from 'bignumber.js' import { FetchMock } from 'jest-fetch-mock' -import { CurrencyConversionAPI } from '../src/currencyConversion' +import ExchangeRateAPI from './ExchangeRateAPI' const mockFetch = fetch as FetchMock @@ -21,19 +22,29 @@ mockFetch.mockResponse(SUCCESS_RESULT, { }, }) -describe('Currency Conversion', () => { - let currencyConversionAPI: CurrencyConversionAPI +describe('ExchangeRateAPI', () => { + let exchangeRateAPI: ExchangeRateAPI beforeEach(() => { - currencyConversionAPI = new CurrencyConversionAPI() - currencyConversionAPI.initialize({ context: {}, cache: new InMemoryLRUCache() }) + exchangeRateAPI = new ExchangeRateAPI() + exchangeRateAPI.initialize({ context: {}, cache: new InMemoryLRUCache() }) jest.clearAllMocks() }) it('should retrieve exchange rates for given currency', async () => { - const result = await currencyConversionAPI.getExchangeRate({ currencyCode: 'MXN' }) - expect(result).toMatchObject({ rate: 20 }) - expect(fetchMock.mock.calls.length).toEqual(1) + const result = await exchangeRateAPI.getExchangeRate({ currencyCode: 'MXN' }) + expect(result).toEqual(new BigNumber(20)) + expect(fetchMock).toHaveBeenCalledTimes(1) + }) + + it('should throw when requesting an invalid currency code', async () => { + await expect( + exchangeRateAPI.getExchangeRate({ + sourceCurrencyCode: 'USD', + currencyCode: 'ABC', + }) + ).rejects.toThrow('No matching data for USD/ABC') + expect(fetchMock).toHaveBeenCalledTimes(1) }) describe('caching', () => { @@ -52,37 +63,37 @@ describe('Currency Conversion', () => { const cache = new InMemoryLRUCache() const now = Date.now() - currencyConversionAPI = new CurrencyConversionAPI() - currencyConversionAPI.initialize({ context: {}, cache }) - const result1 = await currencyConversionAPI.getExchangeRate({ + exchangeRateAPI = new ExchangeRateAPI() + exchangeRateAPI.initialize({ context: {}, cache }) + const result1 = await exchangeRateAPI.getExchangeRate({ currencyCode: 'MXN', timestamp: now, }) - expect(result1).toMatchObject({ rate: 20 }) + expect(result1).toEqual(new BigNumber(20)) expect(fetchMock).toHaveBeenCalledTimes(1) // Advance date to +12 hours - 1 millisecond Date.now = jest.fn(() => now + (12 * 3600 * 1000 - 1)) - currencyConversionAPI = new CurrencyConversionAPI() - currencyConversionAPI.initialize({ context: {}, cache }) - const result2 = await currencyConversionAPI.getExchangeRate({ + exchangeRateAPI = new ExchangeRateAPI() + exchangeRateAPI.initialize({ context: {}, cache }) + const result2 = await exchangeRateAPI.getExchangeRate({ currencyCode: 'MXN', timestamp: now, }) - expect(result2).toMatchObject({ rate: 20 }) + expect(result2).toEqual(new BigNumber(20)) expect(fetchMock).toHaveBeenCalledTimes(1) // Advance date to +12 hours + 1 millisecond Date.now = jest.fn(() => now + (12 * 3600 * 1000 + 1)) - currencyConversionAPI = new CurrencyConversionAPI() - currencyConversionAPI.initialize({ context: {}, cache }) - const result3 = await currencyConversionAPI.getExchangeRate({ + exchangeRateAPI = new ExchangeRateAPI() + exchangeRateAPI.initialize({ context: {}, cache }) + const result3 = await exchangeRateAPI.getExchangeRate({ currencyCode: 'MXN', timestamp: now, }) - expect(result3).toMatchObject({ rate: 20 }) + expect(result3).toEqual(new BigNumber(20)) expect(fetchMock).toHaveBeenCalledTimes(2) }) @@ -90,49 +101,49 @@ describe('Currency Conversion', () => { const cache = new InMemoryLRUCache() const now = Date.now() - 24 * 3600 * 1000 - currencyConversionAPI = new CurrencyConversionAPI() - currencyConversionAPI.initialize({ context: {}, cache }) - const result1 = await currencyConversionAPI.getExchangeRate({ + exchangeRateAPI = new ExchangeRateAPI() + exchangeRateAPI.initialize({ context: {}, cache }) + const result1 = await exchangeRateAPI.getExchangeRate({ currencyCode: 'MXN', timestamp: now, }) - expect(result1).toMatchObject({ rate: 20 }) + expect(result1).toEqual(new BigNumber(20)) expect(fetchMock).toHaveBeenCalledTimes(1) // Advance date to +12 hours - 1 millisecond Date.now = jest.fn(() => now + (12 * 3600 * 1000 - 1)) - currencyConversionAPI = new CurrencyConversionAPI() - currencyConversionAPI.initialize({ context: {}, cache }) - const result2 = await currencyConversionAPI.getExchangeRate({ + exchangeRateAPI = new ExchangeRateAPI() + exchangeRateAPI.initialize({ context: {}, cache }) + const result2 = await exchangeRateAPI.getExchangeRate({ currencyCode: 'MXN', timestamp: now, }) - expect(result2).toMatchObject({ rate: 20 }) + expect(result2).toEqual(new BigNumber(20)) expect(fetchMock).toHaveBeenCalledTimes(1) // Advance date to +12 hours + 1 millisecond Date.now = jest.fn(() => now + (12 * 3600 * 1000 + 1)) - currencyConversionAPI = new CurrencyConversionAPI() - currencyConversionAPI.initialize({ context: {}, cache }) - const result3 = await currencyConversionAPI.getExchangeRate({ + exchangeRateAPI = new ExchangeRateAPI() + exchangeRateAPI.initialize({ context: {}, cache }) + const result3 = await exchangeRateAPI.getExchangeRate({ currencyCode: 'MXN', timestamp: now, }) - expect(result3).toMatchObject({ rate: 20 }) + expect(result3).toEqual(new BigNumber(20)) expect(fetchMock).toHaveBeenCalledTimes(1) // Advance date to +10 years Date.now = jest.fn(() => now + 10 * 365 * 24 * 3600 * 1000) - currencyConversionAPI = new CurrencyConversionAPI() - currencyConversionAPI.initialize({ context: {}, cache }) - const result4 = await currencyConversionAPI.getExchangeRate({ + exchangeRateAPI = new ExchangeRateAPI() + exchangeRateAPI.initialize({ context: {}, cache }) + const result4 = await exchangeRateAPI.getExchangeRate({ currencyCode: 'MXN', timestamp: now, }) - expect(result4).toMatchObject({ rate: 20 }) + expect(result4).toEqual(new BigNumber(20)) expect(fetchMock).toHaveBeenCalledTimes(1) }) }) diff --git a/packages/blockchain-api/src/currencyConversion.ts b/packages/blockchain-api/src/currencyConversion/ExchangeRateAPI.ts similarity index 50% rename from packages/blockchain-api/src/currencyConversion.ts rename to packages/blockchain-api/src/currencyConversion/ExchangeRateAPI.ts index b61c6da4665..5a39752ad33 100644 --- a/packages/blockchain-api/src/currencyConversion.ts +++ b/packages/blockchain-api/src/currencyConversion/ExchangeRateAPI.ts @@ -1,7 +1,9 @@ import { RESTDataSource } from 'apollo-datasource-rest' -import { EXCHANGE_RATES_API, EXCHANGE_RATES_API_ACCESS_KEY } from './config' -import { CurrencyConversionArgs, ExchangeRate } from './schema' -import { formatDateString } from './utils' +import BigNumber from 'bignumber.js' +import { EXCHANGE_RATES_API, EXCHANGE_RATES_API_ACCESS_KEY } from '../config' +import { CurrencyConversionArgs } from '../schema' +import { formatDateString } from '../utils' +import { USD } from './consts' interface ExchangeRateApiResult { success: boolean @@ -13,38 +15,36 @@ interface ExchangeRateApiResult { // ttl in seconds! const MIN_TTL = 12 * 3600 // 12 hours -export class CurrencyConversionAPI extends RESTDataSource { +export default class ExchangeRateAPI extends RESTDataSource { constructor() { super() this.baseURL = EXCHANGE_RATES_API } async getExchangeRate({ + sourceCurrencyCode, currencyCode, timestamp, - }: CurrencyConversionArgs): Promise { - console.debug('Getting exchange rate', currencyCode, timestamp) - try { - if (!currencyCode) { - throw new Error('No currency code specified') - } + }: CurrencyConversionArgs): Promise { + console.debug('Getting exchange rate', sourceCurrencyCode, currencyCode, timestamp) + if (!currencyCode) { + throw new Error('No currency code specified') + } - const date = timestamp ? new Date(timestamp) : new Date() - const fetchedRate = await this.queryExchangeRate(currencyCode, date) + const date = timestamp ? new Date(timestamp) : new Date() + const fetchedRate = await this.queryExchangeRate(sourceCurrencyCode || USD, currencyCode, date) - return { rate: fetchedRate } - } catch (error) { - console.error('Error fetching exchange rate', error) - throw new Error('Failed to retrieve exchange rate') - } + return new BigNumber(fetchedRate) } - private async queryExchangeRate(currencyCode: string, date: Date) { - console.debug('Querying exchange rate', currencyCode, date) + private async queryExchangeRate(sourceCurrencyCode: string, currencyCode: string, date: Date) { + const pair = `${sourceCurrencyCode}/${currencyCode}` + console.debug('Querying exchange rate', pair, date) const path = `/historical` const params = { access_key: EXCHANGE_RATES_API_ACCESS_KEY, date: formatDateString(date), + source: sourceCurrencyCode, } const result = await this.get(path, params, { cacheOptions: { ttl: this.getCacheTtl(date) }, @@ -52,8 +52,11 @@ export class CurrencyConversionAPI extends RESTDataSource { if (result.success !== true) { throw new Error(`Invalid response result: ${JSON.stringify(result)}`) } - const rate = result.quotes[`USD${currencyCode}`] - console.debug('Retrieved rate', currencyCode, rate) + const rate = result.quotes[`${sourceCurrencyCode}${currencyCode}`] + if (rate === undefined) { + throw new Error(`No matching data for ${pair}`) + } + console.debug('Retrieved rate', pair, rate) return rate } diff --git a/packages/blockchain-api/src/currencyConversion/GoldExchangeRateAPI.test.ts b/packages/blockchain-api/src/currencyConversion/GoldExchangeRateAPI.test.ts new file mode 100644 index 00000000000..cc5aa03400c --- /dev/null +++ b/packages/blockchain-api/src/currencyConversion/GoldExchangeRateAPI.test.ts @@ -0,0 +1,96 @@ +import { InMemoryLRUCache } from 'apollo-server-caching' +import BigNumber from 'bignumber.js' +import GoldExchangeRateAPI from './GoldExchangeRateAPI' + +const MOCK_DATA_CGLD_CUSD = { + '-Lv5oAJU8dDHVfqXlKkE': { + exchangeRate: '10.1', + timestamp: 1575293596846, + }, + '-Lv5qbL_oSvKQ_452ZHh': { + exchangeRate: '10.2', + timestamp: 1575294235753, + }, + '-Lv5rRCAOhPJJqVmLiID': { + exchangeRate: '10.3', + timestamp: 1575294452181, + }, +} + +const MOCK_DATA_CUSD_CGLD = { + '-Lv5oAJVzoRhZw3fiViQ': { + exchangeRate: '0.1', + timestamp: 1575293596846, + }, + '-Lv5qbLcXmDPMJC5bnSk': { + exchangeRate: '0.2', + timestamp: 1575294235753, + }, + '-Lv5rRCDdh-73eP0jArN': { + exchangeRate: '0.3', + timestamp: 1575294452181, + }, +} + +const snapshot = { val: jest.fn(), exists: jest.fn(() => true) } + +const mockOnce = jest.fn(() => snapshot) + +const mockQuery = jest.fn(() => ({ + startAt: jest.fn().mockReturnThis(), + endAt: jest.fn().mockReturnThis(), + once: mockOnce, +})) + +jest.mock('firebase-admin', () => ({ + initializeApp: jest.fn(), + database: () => ({ + ref: jest.fn((path) => ({ + orderByChild: mockQuery, + })), + }), +})) + +describe('GoldExchangeRateAPI', () => { + let goldExchangeRateAPI: GoldExchangeRateAPI + + beforeEach(() => { + goldExchangeRateAPI = new GoldExchangeRateAPI() + goldExchangeRateAPI.initialize({ context: {}, cache: new InMemoryLRUCache() }) + jest.clearAllMocks() + }) + + it('should retrieve the closest exchange rate for cGLD/cUSD', async () => { + snapshot.val.mockReturnValueOnce(MOCK_DATA_CGLD_CUSD) + const result = await goldExchangeRateAPI.getExchangeRate({ + sourceCurrencyCode: 'cGLD', + currencyCode: 'cUSD', + timestamp: 1575294235653, + }) + expect(result).toEqual(new BigNumber(10.2)) + expect(mockOnce).toHaveBeenCalledTimes(1) + }) + + it('should retrieve the closest exchange rate for cUSD/cGLD', async () => { + snapshot.val.mockReturnValueOnce(MOCK_DATA_CUSD_CGLD) + const result = await goldExchangeRateAPI.getExchangeRate({ + sourceCurrencyCode: 'cUSD', + currencyCode: 'cGLD', + timestamp: 1575294235653, + }) + expect(result).toEqual(new BigNumber(0.2)) + expect(mockOnce).toHaveBeenCalledTimes(1) + }) + + it('should throw when requesting an invalid currency code', async () => { + snapshot.val.mockReturnValueOnce(null) + await expect( + goldExchangeRateAPI.getExchangeRate({ + sourceCurrencyCode: 'cUSD', + currencyCode: 'ABC', + timestamp: 1575294235653, + }) + ).rejects.toThrow('No matching data for cUSD/ABC') + expect(mockOnce).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/blockchain-api/src/currencyConversion/GoldExchangeRateAPI.ts b/packages/blockchain-api/src/currencyConversion/GoldExchangeRateAPI.ts new file mode 100644 index 00000000000..b7a8eeba746 --- /dev/null +++ b/packages/blockchain-api/src/currencyConversion/GoldExchangeRateAPI.ts @@ -0,0 +1,75 @@ +import { DataSource, DataSourceConfig } from 'apollo-datasource' +import BigNumber from 'bignumber.js' +import { database } from '../firebase' +import { CurrencyConversionArgs } from '../schema' +import { CUSD } from './consts' + +// Firebase stored exchange rate +interface ExchangeRateObject { + exchangeRate: string + timestamp: number // timestamp in milliseconds +} + +// Binary search in sorted array +function findClosestRate( + rates: ExchangeRateObject[], + timestamp: number +): ExchangeRateObject | undefined { + let lo = 0 + let hi = rates.length - 1 + while (lo <= hi) { + const mid = Math.floor((lo + hi) / 2) + const rate = rates[mid] + if (timestamp < rate.timestamp) { + hi = mid - 1 + } else if (timestamp > rate.timestamp) { + lo = mid + 1 + } else { + return rate + } + } + + // At this point lo = hi + 1 + const loRate = rates[lo] + const hiRate = rates[hi] + if (!loRate || !hiRate) { + return loRate || hiRate + } + + return loRate.timestamp - timestamp < timestamp - hiRate.timestamp ? loRate : hiRate +} + +export default class GoldExchangeRateAPI extends DataSource { + // TODO(jeanregisser): memoize results + // memoizedResults = new Map>(); + + initialize(config: DataSourceConfig): void { + // TODO(jeanregisser): keep config.cache + } + + // TODO(jeanregisser): add caching (using config.cache) + async getExchangeRate({ + sourceCurrencyCode, + currencyCode, + timestamp, + }: CurrencyConversionArgs): Promise { + const date = timestamp ? new Date(timestamp) : new Date() + + const pair = `${sourceCurrencyCode || CUSD}/${currencyCode}` + + const ref = database.ref(`exchangeRates/${pair}`) + const snapshot = await ref + .orderByChild('timestamp') + .startAt(date.getTime() - 30 * 60 * 1000) + .endAt(date.getTime() + 30 * 60 * 1000) + .once('value') + + const rates: ExchangeRateObject[] = Object.values(snapshot.val() || {}) + const closestItem = findClosestRate(rates, date.getTime()) + if (!closestItem) { + throw new Error(`No matching data for ${pair}`) + } + + return new BigNumber(closestItem.exchangeRate) + } +} diff --git a/packages/blockchain-api/src/currencyConversion/consts.ts b/packages/blockchain-api/src/currencyConversion/consts.ts new file mode 100644 index 00000000000..3d96e4f4c1b --- /dev/null +++ b/packages/blockchain-api/src/currencyConversion/consts.ts @@ -0,0 +1,5 @@ +import { CURRENCIES, CURRENCY_ENUM } from '@celo/utils' + +export const CGLD = CURRENCIES[CURRENCY_ENUM.GOLD].code +export const CUSD = CURRENCIES[CURRENCY_ENUM.DOLLAR].code +export const USD = 'USD' diff --git a/packages/blockchain-api/src/firebase.ts b/packages/blockchain-api/src/firebase.ts new file mode 100644 index 00000000000..5d593d8991a --- /dev/null +++ b/packages/blockchain-api/src/firebase.ts @@ -0,0 +1,14 @@ +import * as admin from 'firebase-admin' +import { FIREBASE_DB, getFirebaseAdminCreds } from './config' + +/** + * Initialize Firebase Admin SDK + */ +console.info('Initializing Firebase') +admin.initializeApp({ + credential: getFirebaseAdminCreds(admin), + databaseURL: FIREBASE_DB, + projectId: 'celo-org-mobile', +}) + +export const database = admin.database() diff --git a/packages/blockchain-api/src/schema.ts b/packages/blockchain-api/src/schema.ts index fdb68b57387..7b145924164 100644 --- a/packages/blockchain-api/src/schema.ts +++ b/packages/blockchain-api/src/schema.ts @@ -51,6 +51,7 @@ export interface ExchangeRate { } export interface CurrencyConversionArgs { + sourceCurrencyCode?: string currencyCode: string timestamp?: number } @@ -105,7 +106,11 @@ export const typeDefs = gql` offset: Int ): [Transfer] - currencyConversion(currencyCode: String!, timestamp: Float): ExchangeRate + currencyConversion( + sourceCurrencyCode: String + currencyCode: String! + timestamp: Float + ): ExchangeRate } ` @@ -126,7 +131,8 @@ export const resolvers = { args: CurrencyConversionArgs, { dataSources }: Context ) => { - return dataSources.currencyConversionAPI.getExchangeRate(args) + const rate = await dataSources.currencyConversionAPI.getExchangeRate(args) + return { rate: rate.toNumber() } }, }, // TODO(kamyar): see the comment about union causing problems diff --git a/packages/mobile/src/firebase/db-rules.json b/packages/mobile/src/firebase/db-rules.json index 2feb3ad4a88..9a97bba5bfb 100644 --- a/packages/mobile/src/firebase/db-rules.json +++ b/packages/mobile/src/firebase/db-rules.json @@ -30,6 +30,15 @@ "auth.uid != null && (query.orderByChild == 'requesterAddress' || query.orderByChild == 'requesteeAddress') && query.equalTo == root.child('users').child(auth.uid).child('address').val()", ".write": true, ".indexOn": ["requesterAddress", "requesteeAddress"] + }, + "exchangeRates": { + ".read": false, + ".write": false, + "$from": { + "$to": { + ".indexOn": ["timestamp"] + } + } } } } diff --git a/packages/notification-service/src/config.ts b/packages/notification-service/src/config.ts index f7effbe7938..1b46f20cf32 100644 --- a/packages/notification-service/src/config.ts +++ b/packages/notification-service/src/config.ts @@ -9,9 +9,10 @@ export function getFirebaseAdminCreds(admin: any) { try { const serviceAccount = require('../config/serviceAccountKey.json') return admin.credential.cert(serviceAccount) - } catch { + } catch (error) { console.error( - 'Error: Could not initialize admin credentials. Is serviceAccountKey.json missing?' + 'Error: Could not initialize admin credentials. Is serviceAccountKey.json missing?', + error ) } } else { diff --git a/scripts/key_placer.sh b/scripts/key_placer.sh index e7ea9ef826d..35125343fef 100755 --- a/scripts/key_placer.sh +++ b/scripts/key_placer.sh @@ -4,6 +4,7 @@ echo "Processing encrypted files" # Set list of secret files to encrypt and decrypt. files=( + "packages/blockchain-api/serviceAccountKey.json" "packages/blockchain-api/src/secrets.json" "packages/mobile/android/app/google-services.json" "packages/mobile/android/app/src/staging/google-services.json" diff --git a/yarn.lock b/yarn.lock index 9f3b298b5aa..5a7118c2cf5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11268,11 +11268,11 @@ cross-env@^5.1.3, cross-env@^5.1.6: is-windows "^1.0.0" cross-fetch@2.2.2, cross-fetch@3.0.4, cross-fetch@^2.1.0, cross-fetch@^2.1.1, cross-fetch@^2.2.2, cross-fetch@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.2.tgz#b7136491967031949c7f86b15903aef4fa3f1768" - integrity sha512-a4Z0EJ5Nck6QtMy9ZqloLfpvu2uMV3sBfMCR+CgSBCZc6z5KR4bfEiD3dkepH8iZgJMXQpTqf8FjMmvu/GMFkg== + version "3.0.4" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.4.tgz#7bef7020207e684a7638ef5f2f698e24d9eb283c" + integrity sha512-MSHgpjQqgbT/94D4CyADeNoYh52zMkCX4pcJvPP5WqPsLFMKjr2TCMg381ox5qI0ii2dPwaLx/00477knXqXVw== dependencies: - node-fetch "2.3.0" + node-fetch "2.6.0" whatwg-fetch "3.0.0" cross-spawn@5.1.0, cross-spawn@^5.0.1, cross-spawn@^5.1.0: @@ -22961,10 +22961,10 @@ node-fetch-npm@^2.0.2: json-parse-better-errors "^1.0.0" safe-buffer "^5.1.1" -node-fetch@2.3.0, node-fetch@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5" - integrity sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA== +node-fetch@2.6.0, node-fetch@^2.0.0-alpha.8, node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" + integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== node-fetch@^1.0.1, node-fetch@~1.7.1: version "1.7.3" @@ -22974,16 +22974,6 @@ node-fetch@^1.0.1, node-fetch@~1.7.1: encoding "^0.1.11" is-stream "^1.0.1" -node-fetch@^2.0.0-alpha.8, node-fetch@^2.1.2, node-fetch@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.2.0.tgz#4ee79bde909262f9775f731e3656d0db55ced5b5" - integrity sha512-OayFWziIxiHY8bCUyLX6sTpDH8Jsbp4FfYd1j1f7vZyfgkcOnAyM4oQR16f8a0s7Gl/viMGRey8eScYk4V4EZA== - -node-fetch@^2.5.0, node-fetch@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" - integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== - node-forge@0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.4.tgz#8e6e9f563a1e32213aa7508cded22aa791dbf986" From 5b4ef3894e1970ef79cd1e7f0765a62f8b6dca56 Mon Sep 17 00:00:00 2001 From: Aitor <1726644+aaitor@users.noreply.github.com> Date: Thu, 12 Dec 2019 14:48:03 +0100 Subject: [PATCH 15/40] Improvement facilitating to run a full node (#2130) --- scripts/run-docker-validator-network.sh | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/scripts/run-docker-validator-network.sh b/scripts/run-docker-validator-network.sh index cb3002a6aec..8ad3cf3a309 100755 --- a/scripts/run-docker-validator-network.sh +++ b/scripts/run-docker-validator-network.sh @@ -257,20 +257,25 @@ fi if [[ $COMMAND == *"run-fullnode"* ]]; then echo -e "* Let's run the full node ..." - cd $DATA_DIR + cd $FULLNODE_DIR docker rm -f celo-fullnode || echo -e "Container removed" - export CELO_ACCOUNT_ADDRESS=$($CELOCLI account:new |tail -1| cut -d' ' -f 2| tr -cd "[:alnum:]\n") + if [ -z ${CELO_ACCOUNT_ADDRESS+x} ]; then + echo "CELO_ACCOUNT_ADDRESS is unset, creating account"; + export CELO_ACCOUNT_ADDRESS=$(docker run -v $PWD:/root/.celo --entrypoint /bin/sh -it $CELO_IMAGE -c " printf '%s\n' $DEFAULT_PASSWORD $DEFAULT_PASSWORD | geth account new " |tail -1| cut -d'{' -f 2| tr -cd "[:alnum:]\n" ) + else + echo "CELO_ACCOUNT_ADDRESS is set to '$CELO_ACCOUNT_ADDRESS'"; + fi - docker run -v $PWD/fullnode:/root/.celo --entrypoint /bin/sh -it $CELO_IMAGE -c "wget https://www.googleapis.com/storage/v1/b/static_nodes/o/$NETWORK_NAME?alt=media -O /root/.celo/static-nodes.json" + docker run -v $PWD:/root/.celo --entrypoint /bin/sh -it $CELO_IMAGE -c "wget https://www.googleapis.com/storage/v1/b/static_nodes/o/$NETWORK_NAME?alt=media -O /root/.celo/static-nodes.json" - docker run -v $PWD/fullnode:/root/.celo --entrypoint /bin/sh -it $CELO_IMAGE -c "wget https://www.googleapis.com/storage/v1/b/genesis_blocks/o/$NETWORK_NAME?alt=media -O /root/.celo/genesis.json" - docker run -v $PWD/fullnode:/root/.celo $CELO_IMAGE init /root/.celo/genesis.json + docker run -v $PWD:/root/.celo --entrypoint /bin/sh -it $CELO_IMAGE -c "wget https://www.googleapis.com/storage/v1/b/genesis_blocks/o/$NETWORK_NAME?alt=media -O /root/.celo/genesis.json" + docker run -v $PWD:/root/.celo $CELO_IMAGE init /root/.celo/genesis.json echo -e "\tStarting the Full Node" - screen -S celo-fullnode -d -m docker run --name celo-fullnode --restart always -p 127.0.0.1:8545:8545 -p 127.0.0.1:8546:8546 -p 30303:30303 -p 30303:30303/udp -v $PWD/fullnode:/root/.celo $CELO_IMAGE --verbosity 3 --networkid $NETWORK_ID --syncmode full --rpc --rpcaddr 0.0.0.0 --rpcapi eth,net,web3,debug,admin,personal,txpool --lightserv 90 --lightpeers 1000 --maxpeers 1100 --etherbase $CELO_ACCOUNT_ADDRESS --ethstats=fullnode-$ETHSTATS_ARG + screen -S celo-fullnode -d -m docker run --name celo-fullnode --restart always -p 127.0.0.1:8545:8545 -p 127.0.0.1:8546:8546 -p 30303:30303 -p 30303:30303/udp -v $PWD:/root/.celo $CELO_IMAGE --verbosity 3 --networkid $NETWORK_ID --syncmode full --rpc --rpcaddr 0.0.0.0 --rpcapi eth,net,web3,debug,admin,personal,txpool --lightserv 90 --lightpeers 1000 --maxpeers 1100 --etherbase $CELO_ACCOUNT_ADDRESS --ethstats=fullnode-$ETHSTATS_ARG sleep 2s From d51af7e4fe131fb4f7b37d170d73b071c56e13d8 Mon Sep 17 00:00:00 2001 From: Roman Croessmann <42186452+rcroessmann@users.noreply.github.com> Date: Thu, 12 Dec 2019 15:21:48 +0100 Subject: [PATCH 16/40] Update documentation wrt. epoch rewards fractions (#2182) * Adjusting description to latest design. * Adding intial fraction going to carbon offsetting fund. * Improved wording wrt. reserve bolstering. * Update packages/docs/celo-codebase/protocol/proof-of-stake/carbon-offsetting-fund.md Co-Authored-By: Victor "Nate" Graf * Adding link to governance process. --- .../protocol/proof-of-stake/carbon-offsetting-fund.md | 2 +- .../celo-codebase/protocol/proof-of-stake/community-fund.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/docs/celo-codebase/protocol/proof-of-stake/carbon-offsetting-fund.md b/packages/docs/celo-codebase/protocol/proof-of-stake/carbon-offsetting-fund.md index 5bd2b1c1675..f2e3495987d 100644 --- a/packages/docs/celo-codebase/protocol/proof-of-stake/carbon-offsetting-fund.md +++ b/packages/docs/celo-codebase/protocol/proof-of-stake/carbon-offsetting-fund.md @@ -2,4 +2,4 @@ The Carbon Offsetting Fund provides for making the infrastructure of the Celo platform carbon-neutral, by making a transfer every epoch to an organization that commits to using those assets off-chain for carbon offsetting projects. -Through the on-chain governance process, Celo Gold holders can set an on-target carbon offset amount, and the address of a carbon offsetting partner to which to direct these transfers. The on-target amount is adjusted by the epoch [rewards mulitplier](epoch-rewards.md), as with all epoch rewards. +Through the [on-chain governance process](../governance.md), Celo Gold holders can set the fraction of the total desired epoch rewards, initially planned to be 0.5%, that is received by the carbon offsetting fund, and the address of a carbon offsetting partner to which to direct these transfers. The on-target amount is adjusted by the epoch [rewards mulitplier](epoch-rewards.md), as with all epoch rewards. diff --git a/packages/docs/celo-codebase/protocol/proof-of-stake/community-fund.md b/packages/docs/celo-codebase/protocol/proof-of-stake/community-fund.md index 45b726c7a84..b98f5d9b7b6 100644 --- a/packages/docs/celo-codebase/protocol/proof-of-stake/community-fund.md +++ b/packages/docs/celo-codebase/protocol/proof-of-stake/community-fund.md @@ -4,7 +4,7 @@ The Community Fund provides for general upkeep of the Celo platform. Celo Gold h The Community Fund receives assets from three sources: -- The Community Fund obtains a desired epoch payment defined as a fraction of the total desired epoch rewards \(governable, initially planned to be $$25\%$$\). This amount is subject to adjustment up or down in the event of under- or over-spending against the epoch rewards target schedule. It may also be reduced to [bolster the Reserve](#bolster-reserve). +- The Community Fund obtains a desired epoch reward defined as a fraction of the total desired epoch rewards \(governable, initially planned to be $$25\%$$\). This amount is subject to adjustment up or down in the event of under- or over-spending against the epoch rewards target schedule. The Community Fund epoch rewards may be redirected to [bolster the Reserve](#bolster-reserve). - The Community Fund is the default destination for slashed assets. @@ -12,4 +12,4 @@ The Community Fund receives assets from three sources: ## Bolstering the Reserve -The reserve automatically receives a fraction of the desired epoch payments to the Community Fund during times in which the reserve ratio \(the ratio of reserve value over stablecoin market capitalization\) is below a predefined target schedule. The size of the epoch payment to the reserve is calculated based on a half-life calculation to bring the reserve back to its target level. The reserve ratio target schedule as well as the half-life period \(initially planned to be 10 years\) are governable. +The rewards to the Community Fund are automatically redirected to the reserve during times in which the reserve ratio \(the ratio of reserve value over stablecoin market capitalization\) is below a cutoff value. This cutoff reduces from two to one over the first of 25 years in a linear fashion and remains at one afterwards. From e5c5fda14f999ccd0a7789b2b55c9124a1146c35 Mon Sep 17 00:00:00 2001 From: Jean Regisser Date: Thu, 12 Dec 2019 15:39:57 +0100 Subject: [PATCH 17/40] [Wallet] Fix crash on iOS when segment is enabled (#2222) --- packages/mobile/ios/Podfile | 5 +++++ packages/mobile/ios/Podfile.lock | 20 ++++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/mobile/ios/Podfile b/packages/mobile/ios/Podfile index 521e6466b0e..d8f60d7f287 100644 --- a/packages/mobile/ios/Podfile +++ b/packages/mobile/ios/Podfile @@ -45,6 +45,11 @@ target "celo" do pod "Firebase/Database" pod "GoogleUtilities", "~> 5.3.7" + # Using a custom fork so we can have a specific bug fix (crash when firebase already initialized) + # without upgrading to firebase 6 + # TODO: remove this once we upgrade to Firebase >= 6 + pod 'Segment-Firebase', :git => 'https://github.com/celo-org/analytics-ios-integration-firebase.git', :commit => 'e849d040239a7e56e02df96aabc0851e56a4cb19' + target "celoTests" do inherit! :search_paths end diff --git a/packages/mobile/ios/Podfile.lock b/packages/mobile/ios/Podfile.lock index b8dfee764a5..19125fed655 100644 --- a/packages/mobile/ios/Podfile.lock +++ b/packages/mobile/ios/Podfile.lock @@ -408,11 +408,11 @@ PODS: - React - RNSVG (9.11.1): - React - - Segment-Firebase (2.4.0): + - Segment-Firebase (2.5.0): - Analytics (~> 3.2) - Firebase/Core (~> 5.0) - - Segment-Firebase/Core (= 2.4.0) - - Segment-Firebase/Core (2.4.0): + - Segment-Firebase/Core (= 2.5.0) + - Segment-Firebase/Core (2.5.0): - Analytics (~> 3.2) - Firebase/Core (~> 5.0) - Sentry (4.4.0): @@ -488,6 +488,7 @@ DEPENDENCIES: - "RNSentry (from `../../../node_modules/@sentry/react-native`)" - RNShare (from `../../../node_modules/react-native-share`) - RNSVG (from `../../../node_modules/react-native-svg`) + - Segment-Firebase (from `https://github.com/celo-org/analytics-ios-integration-firebase.git`, commit `e849d040239a7e56e02df96aabc0851e56a4cb19`) - Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -512,7 +513,6 @@ SPEC REPOS: - leveldb-library - nanopb - Protobuf - - Segment-Firebase - Sentry EXTERNAL SOURCES: @@ -632,9 +632,17 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native-share" RNSVG: :path: "../../../node_modules/react-native-svg" + Segment-Firebase: + :commit: e849d040239a7e56e02df96aabc0851e56a4cb19 + :git: https://github.com/celo-org/analytics-ios-integration-firebase.git Yoga: :path: "../../../node_modules/react-native/ReactCommon/yoga" +CHECKOUT OPTIONS: + Segment-Firebase: + :commit: e849d040239a7e56e02df96aabc0851e56a4cb19 + :git: https://github.com/celo-org/analytics-ios-integration-firebase.git + SPEC CHECKSUMS: Analytics: 77fd5fb102a4a5eedafa2c2b0245ceb7b7c15e45 boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c @@ -714,10 +722,10 @@ SPEC CHECKSUMS: RNSentry: e57b6acd2424e185ba2e23ef78aa2186a40e83e0 RNShare: 8b171d4b43c1d886917fdd303bf7a4b87167b05c RNSVG: be27aa7c58819f97399388ae53d7fa0572f32c7f - Segment-Firebase: cac4b742228ef74d50ed886f3fc731e4bc8b7d29 + Segment-Firebase: 5ab5aa1e962148c60e5582734ad68b320c647cef Sentry: 26650184fe71eb7476dfd2737acb5ea6cc64b4b1 Yoga: 14927e37bd25376d216b150ab2a561773d57911f -PODFILE CHECKSUM: 0d3a8018c6a563abba8559d74620bf8b8b370fe8 +PODFILE CHECKSUM: c90541c405c955c1e09dcc49def3121d9e91ac81 COCOAPODS: 1.7.5 From 6e6543bede9a80c7f543026a93c2119ec320ae1a Mon Sep 17 00:00:00 2001 From: Ivan Sorokin Date: Thu, 12 Dec 2019 16:26:19 +0100 Subject: [PATCH 18/40] [Wallet] Redesigning notification lists (#1967) * Implement new notifications on Home screen * New design * Group notification if there are more then 2 of the same type --- packages/mobile/.env | 4 +- packages/mobile/locales/en-US/global.json | 6 +- .../mobile/locales/en-US/inviteFlow11.json | 5 +- .../locales/en-US/paymentRequestFlow.json | 8 +- packages/mobile/locales/en-US/sendFlow7.json | 1 - .../mobile/locales/en-US/walletFlow5.json | 12 +- packages/mobile/locales/es-419/global.json | 6 +- .../mobile/locales/es-419/inviteFlow11.json | 5 +- .../locales/es-419/paymentRequestFlow.json | 8 +- packages/mobile/locales/es-419/sendFlow7.json | 1 - .../mobile/locales/es-419/walletFlow5.json | 10 +- packages/mobile/src/app/ErrorBoundary.tsx | 3 +- packages/mobile/src/app/ErrorScreen.tsx | 3 +- packages/mobile/src/app/UpgradeScreen.tsx | 3 +- .../src/escrow/EscrowedPaymentLineItem.tsx | 35 +- .../escrow/EscrowedPaymentListItem.test.tsx | 1 + .../src/escrow/EscrowedPaymentListItem.tsx | 72 +- .../escrow/EscrowedPaymentListScreen.test.tsx | 15 +- .../src/escrow/EscrowedPaymentListScreen.tsx | 87 +- ...aymentReminderSummaryNotification.test.tsx | 2 +- ...owedPaymentReminderSummaryNotification.tsx | 56 +- .../escrow/ReclaimPaymentConfirmationCard.tsx | 11 +- .../EscrowedPaymentLineItem.test.tsx.snap | 38 +- .../EscrowedPaymentListItem.test.tsx.snap | 285 ++-- .../EscrowedPaymentListScreen.test.tsx.snap | 174 +-- ...tReminderSummaryNotification.test.tsx.snap | 617 +++++++++ ...claimPaymentConfirmationCard.test.tsx.snap | 1 - ...aimPaymentConfirmationScreen.test.tsx.snap | 3 - packages/mobile/src/escrow/saga.ts | 1 - .../mobile/src/home/NotificationBox.test.tsx | 52 +- packages/mobile/src/home/NotificationBox.tsx | 6 +- .../NotificationBox.test.tsx.snap | 585 ++++---- .../__snapshots__/WalletHome.test.tsx.snap | 4 +- packages/mobile/src/icons/PaymentsIcon.tsx | 4 +- packages/mobile/src/navigator/Navigator.tsx | 6 + .../mobile/src/navigator/TabNavigator.tsx | 2 +- ...omingPaymentRequestSummaryNotification.tsx | 2 - .../notifications/NotificationList.test.tsx | 25 + .../src/notifications/NotificationList.tsx | 99 ++ ...goingPaymentRequestSummaryNotification.tsx | 2 - .../notifications/SimpleNotification.test.tsx | 3 +- .../src/notifications/SimpleNotification.tsx | 1 - .../SummaryNotification.test.tsx | 23 + .../src/notifications/SummaryNotification.tsx | 69 + ...tReminderSummaryNotification.test.tsx.snap | 643 --------- ...ntRequestSummaryNotification.test.tsx.snap | 677 ---------- .../NotificationList.test.tsx.snap | 28 + ...ntRequestSummaryNotification.test.tsx.snap | 677 ---------- .../SimpleNotification.test.tsx.snap | 108 +- .../SummaryNotification.test.tsx.snap | 136 ++ .../IncomingPaymentRequestListItem.tsx | 68 +- .../IncomingPaymentRequestListScreen.test.tsx | 16 +- .../IncomingPaymentRequestListScreen.tsx | 106 +- ...PaymentRequestSummaryNotification.test.tsx | 2 +- ...omingPaymentRequestSummaryNotification.tsx | 104 ++ .../src/paymentRequest/NotificationAmount.tsx | 30 - .../OutgoingPaymentRequestListItem.tsx | 86 +- .../OutgoingPaymentRequestListScreen.test.tsx | 16 +- .../OutgoingPaymentRequestListScreen.tsx | 109 +- ...PaymentRequestSummaryNotification.test.tsx | 2 +- ...goingPaymentRequestSummaryNotification.tsx | 108 ++ .../paymentRequest/PaymentRequestBalance.tsx | 54 - .../PaymentRequestListEmpty.tsx | 17 - .../PaymentRequestNotificationInner.test.tsx | 1 - .../PaymentRequestNotificationInner.tsx | 26 +- ...comingPaymentRequestListItem.test.tsx.snap | 208 ++- ...mingPaymentRequestListScreen.test.tsx.snap | 948 +++++-------- ...ntRequestSummaryNotification.test.tsx.snap | 631 +++++++++ ...tgoingPaymentRequestListItem.test.tsx.snap | 327 +++-- ...oingPaymentRequestListScreen.test.tsx.snap | 1173 +++++++---------- ...ntRequestSummaryNotification.test.tsx.snap | 616 +++++++++ ...mentRequestNotificationInner.test.tsx.snap | 19 +- packages/mobile/src/send/Fee.tsx | 3 +- packages/mobile/src/send/FeeEducation.tsx | 3 +- packages/mobile/src/send/SendAmount.tsx | 2 +- packages/mobile/src/send/SendConfirmation.tsx | 4 +- .../__snapshots__/SendAmount.test.tsx.snap | 4 +- packages/mobile/src/send/saga.ts | 2 +- packages/mobile/src/set-clock/SetClock.tsx | 11 +- .../mobile/src/transactions/NoActivity.tsx | 3 +- .../src/transactions/TransferFeedIcon.tsx | 8 +- packages/mobile/src/web3/gas.ts | 4 +- packages/mobile/test/values.ts | 13 + .../components/BaseListItem.tsx | 50 - .../components/BaseNotification.tsx | 68 +- .../components/SummaryNotification.test.tsx | 40 + .../components/SummaryNotification.tsx | 81 ++ .../BaseNotification.test.tsx.snap | 65 +- .../SummaryNotification.test.tsx.snap | 108 ++ packages/react-components/styles/fonts.tsx | 8 +- packages/react-components/styles/styles.ts | 10 + 91 files changed, 4878 insertions(+), 4901 deletions(-) rename packages/mobile/src/{notifications => escrow}/EscrowedPaymentReminderSummaryNotification.test.tsx (91%) rename packages/mobile/src/{notifications => escrow}/EscrowedPaymentReminderSummaryNotification.tsx (53%) create mode 100644 packages/mobile/src/escrow/__snapshots__/EscrowedPaymentReminderSummaryNotification.test.tsx.snap create mode 100644 packages/mobile/src/notifications/NotificationList.test.tsx create mode 100644 packages/mobile/src/notifications/NotificationList.tsx create mode 100644 packages/mobile/src/notifications/SummaryNotification.test.tsx create mode 100644 packages/mobile/src/notifications/SummaryNotification.tsx delete mode 100644 packages/mobile/src/notifications/__snapshots__/EscrowedPaymentReminderSummaryNotification.test.tsx.snap delete mode 100644 packages/mobile/src/notifications/__snapshots__/IncomingPaymentRequestSummaryNotification.test.tsx.snap create mode 100644 packages/mobile/src/notifications/__snapshots__/NotificationList.test.tsx.snap delete mode 100644 packages/mobile/src/notifications/__snapshots__/OutgoingPaymentRequestSummaryNotification.test.tsx.snap create mode 100644 packages/mobile/src/notifications/__snapshots__/SummaryNotification.test.tsx.snap rename packages/mobile/src/{notifications => paymentRequest}/IncomingPaymentRequestSummaryNotification.test.tsx (95%) create mode 100644 packages/mobile/src/paymentRequest/IncomingPaymentRequestSummaryNotification.tsx delete mode 100644 packages/mobile/src/paymentRequest/NotificationAmount.tsx rename packages/mobile/src/{notifications => paymentRequest}/OutgoingPaymentRequestSummaryNotification.test.tsx (95%) create mode 100644 packages/mobile/src/paymentRequest/OutgoingPaymentRequestSummaryNotification.tsx delete mode 100644 packages/mobile/src/paymentRequest/PaymentRequestBalance.tsx delete mode 100644 packages/mobile/src/paymentRequest/PaymentRequestListEmpty.tsx create mode 100644 packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestSummaryNotification.test.tsx.snap create mode 100644 packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestSummaryNotification.test.tsx.snap delete mode 100644 packages/react-components/components/BaseListItem.tsx create mode 100644 packages/react-components/components/SummaryNotification.test.tsx create mode 100644 packages/react-components/components/SummaryNotification.tsx create mode 100644 packages/react-components/components/__snapshots__/SummaryNotification.test.tsx.snap diff --git a/packages/mobile/.env b/packages/mobile/.env index 20f99890196..f3ce19e1ed2 100644 --- a/packages/mobile/.env +++ b/packages/mobile/.env @@ -1,6 +1,6 @@ ENVIRONMENT=local DEFAULT_TESTNET=alfajoresstaging -# If ZERO_SYNC_ENABLED_INITIALLY, local geth will not run initially. +# If ZERO_SYNC_ENABLED_INITIALLY, local geth will not run initially. # If toggled on, it will use DEFAULT_SYNC_MODE. See src/geth/consts.ts for more info ZERO_SYNC_ENABLED_INITIALLY=false DEFAULT_SYNC_MODE=5 @@ -10,4 +10,4 @@ SHOW_TESTNET_BANNER=true SHOW_GET_INVITE_LINK=false DEV_SETTINGS_ACTIVE_INITIALLY=true # Enable for true hot reloading while dev-ing UI -DEV_RESTORE_NAV_STATE_ON_RELOAD=false \ No newline at end of file +DEV_RESTORE_NAV_STATE_ON_RELOAD=false diff --git a/packages/mobile/locales/en-US/global.json b/packages/mobile/locales/en-US/global.json index f49b975177d..2cc16aa1b4e 100644 --- a/packages/mobile/locales/en-US/global.json +++ b/packages/mobile/locales/en-US/global.json @@ -63,6 +63,7 @@ "decline": "Decline", "to": "To", "for": "For", + "reclaim": "Reclaim", "celoGold": "Celo Gold", "celoDollars": "{{CeloDollars}}", "oops": "Oops!", @@ -102,5 +103,8 @@ "localCurrencyTitle": "Select Currency", "or": "or", "accepted": "Accepted", - "processing": "Processing" + "processing": "Processing", + "moreWithCount": "+{{count }} more", + "unknown": "Unknown", + "emptyList": "Empty list" } diff --git a/packages/mobile/locales/en-US/inviteFlow11.json b/packages/mobile/locales/en-US/inviteFlow11.json index dfe8725351e..47f0f97393f 100644 --- a/packages/mobile/locales/en-US/inviteFlow11.json +++ b/packages/mobile/locales/en-US/inviteFlow11.json @@ -25,5 +25,8 @@ "inviteAnyone": "Invite Anyone in your address book to send and receive value", "inviteComplete": "Invite Complete", "inviteReceived": "Invite Received", - "pedningInvitations": "Pending Invitations" + "escrowPaymentNotificationTitle": "Invited ({{amount}}) {{mobile}}", + "escrowPaymentNotificationLine": "<0>{{recipientPhone}} for <1>{{amount}}", + "escrowPaymentNotificationLine_missingRecipientPhone": "<0>Unknown for <1>{{amount}}", + "defaultComment": "Invitation sent" } diff --git a/packages/mobile/locales/en-US/paymentRequestFlow.json b/packages/mobile/locales/en-US/paymentRequestFlow.json index 1d86f8de0c5..55d175c34ea 100644 --- a/packages/mobile/locales/en-US/paymentRequestFlow.json +++ b/packages/mobile/locales/en-US/paymentRequestFlow.json @@ -1,10 +1,12 @@ { "requests": "Requests", "empty": "You have no Payment Requests", - "incomingPaymentRequests": "Requests Waiting for You", - "outgoingPaymentRequests": "Payments You’ve Requested", "celoDollarBalance": "{{CeloDollar}} Balance", "requestDeclined": "Request Declined", "requestPaid": "Request Paid", - "paymentRequestUpdateFailed": "Cannot update payment request" + "paymentRequestUpdateFailed": "Cannot update payment request", + "incomingPaymentRequestNotificationTitle": "{{name}} requested {{amount}}", + "outgoingPaymentRequestNotificationTitle": "Requested {{amount}} from {{name}}", + "defaultComment": "Payment Requested", + "paymentRequestNotificationLine": "<0>{{displayName}} for <1>{{amount}}" } diff --git a/packages/mobile/locales/en-US/sendFlow7.json b/packages/mobile/locales/en-US/sendFlow7.json index 1a5502ff911..e1db576fb9d 100644 --- a/packages/mobile/locales/en-US/sendFlow7.json +++ b/packages/mobile/locales/en-US/sendFlow7.json @@ -14,7 +14,6 @@ "sendTo": "Send to", "amount": "Amount", "celoDollarsAvailable": "{{CeloDollars}} Available", - "for": "For", "groceriesRent": "Groceries, rent, etc. (Optional)", "fee": "Fee", "securityFee": "Security Fee", diff --git a/packages/mobile/locales/en-US/walletFlow5.json b/packages/mobile/locales/en-US/walletFlow5.json index 107d76fbe4b..f9665a58aa2 100644 --- a/packages/mobile/locales/en-US/walletFlow5.json +++ b/packages/mobile/locales/en-US/walletFlow5.json @@ -1,9 +1,7 @@ { "getStarted": "Get Started", - "incomingPaymentRequest": "Request Waiting for You", - "incomingPaymentRequestWithCount_plural": "{{count}} Requests Waiting on You", - "outgoingPaymentRequest": "Payment You’ve Requested", - "outgoingPaymentRequestWithCount_plural": "{{count}} Payments You’ve Requested", + "incomingPaymentRequests": "Requests from Others", + "outgoingPaymentRequests": "Payments You’ve Requested", "SMSError": "Error sending SMS", "notifications": "Notifications", "getBackupKey": "Get Backup Key", @@ -54,11 +52,7 @@ "maybeLater": "Maybe Later", "balanceNeedUpdating": "Balance needs updating", "refreshBalances": "Refresh Balances", - "reclaimPayment": "Reclaim Payment", - "sendMessage": "Send Message", - "escrowedPaymentReminderListItemTitle": "Remind {{mobile}} to Accept Payment", - "escrowedPaymentReminder": "Remind the recipient to Accept Payment", - "escrowedPaymentReminderWithCount_plural": "Remind {{count}} recipients to Accept Payment", + "escrowedPaymentReminder": "Pending Invitations", "escrowedPaymentReminderSms": "A friendly reminder that you haven't yet redeemed your Celo Dollars!", "testnetAlert": { diff --git a/packages/mobile/locales/es-419/global.json b/packages/mobile/locales/es-419/global.json index fdbed9dd927..e04d22b9661 100755 --- a/packages/mobile/locales/es-419/global.json +++ b/packages/mobile/locales/es-419/global.json @@ -63,6 +63,7 @@ "decline": "Disminución", "to": "Para", "for": "Por", + "reclaim": "Reclaim", "celoGold": "Celo Oro", "celoDollars": "{{CeloDollars}}", "oops": "¡Uy!", @@ -103,5 +104,8 @@ "localCurrencyTitle": "Seleccione el tipo de moneda", "or": "o", "accepted": "Aceptado", - "processing": "Procesando" + "processing": "Procesando", + "moreWithCount": "~~+{{count }} more", + "unknown": "~~Unknown", + "emptyList": "~~Empty list" } diff --git a/packages/mobile/locales/es-419/inviteFlow11.json b/packages/mobile/locales/es-419/inviteFlow11.json index 20a7df9ecc0..935be142290 100755 --- a/packages/mobile/locales/es-419/inviteFlow11.json +++ b/packages/mobile/locales/es-419/inviteFlow11.json @@ -25,5 +25,8 @@ "inviteAnyone": "Invite a cualquiera en tu lista de contactos para enviar y recibir valor", "inviteComplete": "Invitación completa", "inviteReceived": "Invitación recibida", - "pedningInvitations": "Invitaciones pendientes" + "escrowPaymentNotificationTitle": "~~Invited ({{amount}}) {{mobile}}", + "escrowPaymentNotificationLine": "~~<0>{{recipientPhone}} for <1>{{amount}}", + "escrowPaymentNotificationLine_missingRecipientPhone": "~~<0>Unknown for <1>{{amount}}", + "defaultComment": "~~Invitation sent" } diff --git a/packages/mobile/locales/es-419/paymentRequestFlow.json b/packages/mobile/locales/es-419/paymentRequestFlow.json index 5507196aa76..60692110920 100644 --- a/packages/mobile/locales/es-419/paymentRequestFlow.json +++ b/packages/mobile/locales/es-419/paymentRequestFlow.json @@ -1,10 +1,12 @@ { "requests": "Solicitudes", "empty": "No tienes solicitudes de pago", - "incomingPaymentRequests": "Pedidos esperandote", - "outgoingPaymentRequests": "Pagos que tu has pedido", "celoDollarBalance": "Saldo de {{CeloDollar}}", "requestDeclined": "Solicitud rechazada", "requestPaid": "Solicitud Pagada", - "paymentRequestUpdateFailed": "No se pudo actualizar el pedido de pago" + "paymentRequestUpdateFailed": "No se pudo actualizar el pedido de pago", + "incomingPaymentRequestNotificationTitle": "~~{{name}} requested {{amount}}", + "outgoingPaymentRequestNotificationTitle": "~~Requested {{amount}} from {{name}}", + "defaultComment": "~~Payment Requested", + "paymentRequestNotificationLine": "~~<0>{{displayName}} for <1>{{amount}}" } diff --git a/packages/mobile/locales/es-419/sendFlow7.json b/packages/mobile/locales/es-419/sendFlow7.json index e91aceb532d..a21064e1583 100755 --- a/packages/mobile/locales/es-419/sendFlow7.json +++ b/packages/mobile/locales/es-419/sendFlow7.json @@ -14,7 +14,6 @@ "sendTo": "Enviar a", "amount": "Monto", "celoDollarsAvailable": "{{CeloDollars}} disponibles", - "for": "Por", "groceriesRent": "Supermercado, alquiler, etc. (Opcional)", "fee": "Comisión", "securityFee": "Comisión de seguridad", diff --git a/packages/mobile/locales/es-419/walletFlow5.json b/packages/mobile/locales/es-419/walletFlow5.json index 1f105fe06ab..eaf9daa9f7f 100755 --- a/packages/mobile/locales/es-419/walletFlow5.json +++ b/packages/mobile/locales/es-419/walletFlow5.json @@ -1,9 +1,7 @@ { "getStarted": "Primeros pasos", - "incomingPaymentRequest": "Pedido esperandote", - "incomingPaymentRequestWithCount_plural": "{{count}} Pedidos esperandote", - "outgoingPaymentRequest": "Pago que tu has pedido", - "outgoingPaymentRequestWithCount_plural": "{{count}} Pagos que tu has pedido", + "incomingPaymentRequests": "~~Requests from Others", + "outgoingPaymentRequests": "~~Payments You’ve Requested", "SMSError": "Error al enviar SMS", "notifications": "Notificaciones", "getBackupKey": "Obtener clave de respaldo", @@ -55,11 +53,7 @@ "maybeLater": "quizas mas tarde", "balanceNeedUpdating": "Saldo necesita actualizarse", "refreshBalances": "Actualizar saldos", - "reclaimPayment": "Reclamar el Pago", - "sendMessage": "Enviar Mensaje", - "escrowedPaymentReminderListItemTitle": "Recuérdele a {{mobile #}} que acepte el pago", "escrowedPaymentReminder": "Reacuerda al receptor aceptar el pago", - "escrowedPaymentReminderWithCount_plural": "Recordar {{count}} receptores to aceptar el pago", "escrowedPaymentReminderSms": "¡Un recordatorio amistoso de que aún no ha canjeado sus dólares de celo!", "testnetAlert": { diff --git a/packages/mobile/src/app/ErrorBoundary.tsx b/packages/mobile/src/app/ErrorBoundary.tsx index c49650966a1..8344b9f57ac 100644 --- a/packages/mobile/src/app/ErrorBoundary.tsx +++ b/packages/mobile/src/app/ErrorBoundary.tsx @@ -5,6 +5,7 @@ import { withNamespaces, WithNamespaces } 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' interface State { childError: Error | null @@ -37,4 +38,4 @@ class ErrorBoundary extends React.Component { } } -export default withNamespaces('global')(ErrorBoundary) +export default withNamespaces(Namespaces.global)(ErrorBoundary) diff --git a/packages/mobile/src/app/ErrorScreen.tsx b/packages/mobile/src/app/ErrorScreen.tsx index 967c3e57c2d..2362a44b945 100644 --- a/packages/mobile/src/app/ErrorScreen.tsx +++ b/packages/mobile/src/app/ErrorScreen.tsx @@ -4,6 +4,7 @@ import * as React from 'react' import { withNamespaces, WithNamespaces } from 'react-i18next' import { Text, View } from 'react-native' import { NavigationParams, NavigationScreenProp } from 'react-navigation' +import { Namespaces } from 'src/i18n' import { deleteChainDataAndRestartApp, RESTART_APP_I18N_KEY } from 'src/utils/AppRestart' interface OwnProps { @@ -44,4 +45,4 @@ class ErrorScreen extends React.Component { } } -export default withNamespaces('global')(ErrorScreen) +export default withNamespaces(Namespaces.global)(ErrorScreen) diff --git a/packages/mobile/src/app/UpgradeScreen.tsx b/packages/mobile/src/app/UpgradeScreen.tsx index 63110ffe47f..82ebddb9521 100644 --- a/packages/mobile/src/app/UpgradeScreen.tsx +++ b/packages/mobile/src/app/UpgradeScreen.tsx @@ -2,6 +2,7 @@ import FullscreenCTA from '@celo/react-components/components/FullscreenCTA' import * as React from 'react' import { withNamespaces, WithNamespaces } from 'react-i18next' import { NavigationParams, NavigationScreenProp } from 'react-navigation' +import { Namespaces } from 'src/i18n' import { navigateToWalletPlayStorePage } from 'src/utils/linking' interface OwnProps { @@ -27,4 +28,4 @@ class UpgradeScreen extends React.Component { } } -export default withNamespaces('global')(UpgradeScreen) +export default withNamespaces(Namespaces.global)(UpgradeScreen) diff --git a/packages/mobile/src/escrow/EscrowedPaymentLineItem.tsx b/packages/mobile/src/escrow/EscrowedPaymentLineItem.tsx index 9cfc7b250be..2a6763e4e63 100644 --- a/packages/mobile/src/escrow/EscrowedPaymentLineItem.tsx +++ b/packages/mobile/src/escrow/EscrowedPaymentLineItem.tsx @@ -1,24 +1,34 @@ import fontStyles from '@celo/react-components/styles/fonts' import * as React from 'react' +import { Trans, withNamespaces, WithNamespaces } from 'react-i18next' import { StyleSheet, Text } from 'react-native' import { EscrowedPayment } from 'src/escrow/actions' +import { CURRENCIES, CURRENCY_ENUM } from 'src/geth/consts' +import { Namespaces } from 'src/i18n' import { divideByWei, getCentAwareMoneyDisplay } from 'src/utils/formatting' interface Props { payment: EscrowedPayment } -export default function EscrowedPaymentLineItem(props: Props) { - const { amount, message, recipientPhone } = props.payment +function EscrowedPaymentLineItem(props: Props & WithNamespaces) { + const { amount, recipientPhone } = props.payment return ( - - {recipientPhone} - {message} - - - {' '} - ${getCentAwareMoneyDisplay(divideByWei(amount.toString()))} - + + {{ recipientPhone }} for + {{ amount }} + ) } @@ -27,4 +37,11 @@ const styles = StyleSheet.create({ oneLine: { flexDirection: 'row', }, + phone: fontStyles.subSmall, + amount: { + ...fontStyles.subSmall, + ...fontStyles.semiBold, + }, }) + +export default withNamespaces(Namespaces.inviteFlow11)(EscrowedPaymentLineItem) diff --git a/packages/mobile/src/escrow/EscrowedPaymentListItem.test.tsx b/packages/mobile/src/escrow/EscrowedPaymentListItem.test.tsx index d90dc2022f9..b51b84b7d54 100644 --- a/packages/mobile/src/escrow/EscrowedPaymentListItem.test.tsx +++ b/packages/mobile/src/escrow/EscrowedPaymentListItem.test.tsx @@ -3,6 +3,7 @@ import 'react-native' import { Provider } from 'react-redux' import * as renderer from 'react-test-renderer' import EscrowedPaymentListItem from 'src/escrow/EscrowedPaymentListItem' + import { createMockStore } from 'test/utils' import { mockEscrowedPayment } from 'test/values' diff --git a/packages/mobile/src/escrow/EscrowedPaymentListItem.tsx b/packages/mobile/src/escrow/EscrowedPaymentListItem.tsx index 99958028bce..3595231f66c 100644 --- a/packages/mobile/src/escrow/EscrowedPaymentListItem.tsx +++ b/packages/mobile/src/escrow/EscrowedPaymentListItem.tsx @@ -1,18 +1,20 @@ 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 { Image, Platform, StyleSheet, View } from 'react-native' +import { Image, Platform, StyleSheet, Text, View } from 'react-native' import SendIntentAndroid from 'react-native-send-intent' 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 { EscrowedPayment } from 'src/escrow/actions' +import { CURRENCIES, CURRENCY_ENUM } from 'src/geth/consts' import { Namespaces } from 'src/i18n' import { inviteFriendsIcon } from 'src/images/Images' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' -import NotificationAmount from 'src/paymentRequest/NotificationAmount' +import { divideByWei, getCentAwareMoneyDisplay } from 'src/utils/formatting' import { navigateToURI } from 'src/utils/linking' import Logger from 'src/utils/Logger' @@ -25,14 +27,14 @@ type Props = OwnProps & WithNamespaces const TAG = 'EscrowedPaymentListItem' export class EscrowedPaymentListItem extends React.PureComponent { - onSendMessage = () => { + onRemind = () => { const { payment, t } = this.props const recipientPhoneNumber = payment.recipientPhone CeloAnalytics.track(CustomEventNames.clicked_escrowed_payment_send_message) // TODO: open up whatsapp/text message slider with pre populated message try { if (Platform.OS === 'android') { - SendIntentAndroid.sendSms(recipientPhoneNumber, t('escrowedPaymentReminderSms')) + SendIntentAndroid.sendSms(recipientPhoneNumber, t('walletFlow5:escrowedPaymentReminderSms')) } else { // TODO look into using MFMessageComposeViewController to prefill the body for iOS navigateToURI(`sms:${recipientPhoneNumber}`) @@ -51,44 +53,50 @@ export class EscrowedPaymentListItem extends React.PureComponent { } getCTA = () => { const { t } = this.props - return [ - { - text: t('sendMessage'), - onPress: this.onSendMessage, - }, - { - text: t('reclaimPayment'), - onPress: this.onReclaimPayment, - }, - ] + const ctas = [] + if (this.getDisplayName()) { + ctas.push({ + text: t('global:remind'), + onPress: this.onRemind, + }) + } + ctas.push({ + text: t('global:reclaim'), + onPress: this.onReclaimPayment, + }) + return ctas } - getTitle() { - const { t, payment } = this.props - const displayName = payment.recipientContact - ? payment.recipientContact.displayName - : payment.recipientPhone - return t('escrowedPaymentReminderListItemTitle', { mobile: displayName }) + getDisplayName() { + const { payment } = this.props + return payment.recipientContact ? payment.recipientContact.displayName : payment.recipientPhone } render() { - const { payment } = this.props - + const { t, payment } = this.props return ( - } - ctas={this.getCTA()} - roundedBorders={false} - callout={} - > - - + + } + ctas={this.getCTA()} + > + {payment.message || t('defaultComment')} + + ) } } const styles = StyleSheet.create({ + container: { + marginBottom: 16, + }, body: { marginTop: 5, flexDirection: 'row', @@ -103,5 +111,5 @@ const styles = StyleSheet.create({ }) export default componentWithAnalytics( - withNamespaces(Namespaces.walletFlow5)(EscrowedPaymentListItem) + withNamespaces(Namespaces.inviteFlow11)(EscrowedPaymentListItem) ) diff --git a/packages/mobile/src/escrow/EscrowedPaymentListScreen.test.tsx b/packages/mobile/src/escrow/EscrowedPaymentListScreen.test.tsx index 3aa4ceb77de..41bd814d619 100644 --- a/packages/mobile/src/escrow/EscrowedPaymentListScreen.test.tsx +++ b/packages/mobile/src/escrow/EscrowedPaymentListScreen.test.tsx @@ -1,3 +1,4 @@ +import BigNumber from 'bignumber.js' import * as React from 'react' import 'react-native' import { Provider } from 'react-redux' @@ -5,10 +6,18 @@ import * as renderer from 'react-test-renderer' import { escrowPaymentDouble } from 'src/escrow/__mocks__' import { EscrowedPayment } from 'src/escrow/actions' import EscrowedPaymentListScreen from 'src/escrow/EscrowedPaymentListScreen' -import { createMockStore } from 'test/utils' +import { createMockNavigationProp, createMockStore } from 'test/utils' +import { mockAccount, mockRecipient } from 'test/values' const payments = [escrowPaymentDouble({}), escrowPaymentDouble({}), escrowPaymentDouble({})] +const navigation = createMockNavigationProp({ + recipient: mockRecipient, + recipientAddress: mockAccount, + amount: new BigNumber(10), + reason: 'My Reason', +}) + function testStore(sentEscrowedPayments: EscrowedPayment[]) { return createMockStore({ stableToken: { balance: '120' }, @@ -22,7 +31,7 @@ describe('EscrowedPaymentListScreen', () => { const tree = renderer.create( - + ) expect(tree).toMatchSnapshot() @@ -33,7 +42,7 @@ describe('EscrowedPaymentListScreen', () => { const tree = renderer.create( - + ) expect(tree).toMatchSnapshot() diff --git a/packages/mobile/src/escrow/EscrowedPaymentListScreen.tsx b/packages/mobile/src/escrow/EscrowedPaymentListScreen.tsx index 292a6d84cfa..429216982e4 100644 --- a/packages/mobile/src/escrow/EscrowedPaymentListScreen.tsx +++ b/packages/mobile/src/escrow/EscrowedPaymentListScreen.tsx @@ -1,11 +1,7 @@ -import SectionHeader from '@celo/react-components/components/SectionHead' -import colors from '@celo/react-components/styles/colors' -import { componentStyles } from '@celo/react-components/styles/styles' -import variables from '@celo/react-components/styles/variables' import * as React from 'react' import { WithNamespaces, withNamespaces } from 'react-i18next' -import { ScrollView, StyleSheet, View } from 'react-native' -import SafeAreaView from 'react-native-safe-area-view' +import { View } from 'react-native' +import { NavigationInjectedProps } from 'react-navigation' import { connect } from 'react-redux' import { EscrowedPayment } from 'src/escrow/actions' import EscrowedPaymentListItem from 'src/escrow/EscrowedPaymentListItem' @@ -14,15 +10,14 @@ import { updatePaymentRequestStatus } from 'src/firebase/actions' import i18n, { Namespaces } from 'src/i18n' import { fetchPhoneAddresses } from 'src/identity/actions' import { e164NumberToAddressSelector, E164NumberToAddressType } from 'src/identity/reducer' -import { headerWithBackButton } from 'src/navigator/Headers' -import PaymentRequestBalance from 'src/paymentRequest/PaymentRequestBalance' -import PaymentRequestListEmpty from 'src/paymentRequest/PaymentRequestListEmpty' +import { + NotificationList, + titleWithBalanceNavigationOptions, + useBalanceInNavigationParam, +} from 'src/notifications/NotificationList' import { NumberToRecipient } from 'src/recipients/recipient' import { recipientCacheSelector } from 'src/recipients/reducer' import { RootState } from 'src/redux/reducers' -import DisconnectBanner from 'src/shared/DisconnectBanner' - -const { contentPadding } = variables interface StateProps { dollarBalance: string | null @@ -43,57 +38,31 @@ const mapStateToProps = (state: RootState): StateProps => ({ recipientCache: recipientCacheSelector(state), }) -type Props = WithNamespaces & StateProps & DispatchProps - -export class EscrowedPaymentListScreen extends React.Component { - static navigationOptions = () => ({ - ...headerWithBackButton, - headerTitle: i18n.t('inviteFlow11:pedningInvitations'), - }) +type Props = NavigationInjectedProps & WithNamespaces & StateProps & DispatchProps - renderRequest = (payment: EscrowedPayment, key: number, allPayments: EscrowedPayment[]) => { - return ( - - - {key < allPayments.length - 1 && } - - ) - } +export const listItemRenderer = (payment: EscrowedPayment, key: number | undefined = undefined) => { + return ( + + + + ) +} - render() { - return ( - - - - - {this.props.sentEscrowedPayments.length > 0 ? ( - - - {this.props.sentEscrowedPayments.map(this.renderRequest)} - - - ) : ( - - )} - - ) - } +const EscrowedPaymentListScreen = (props: Props) => { + const { dollarBalance, navigation } = props + useBalanceInNavigationParam(dollarBalance, navigation) + return ( + + ) } -const styles = StyleSheet.create({ - container: { - backgroundColor: colors.background, - flex: 1, - }, - separator: { - borderBottomColor: colors.darkLightest, - borderBottomWidth: 1, - marginLeft: 50, - }, - scrollArea: { - margin: contentPadding, - }, -}) +EscrowedPaymentListScreen.navigationOptions = titleWithBalanceNavigationOptions( + i18n.t('walletFlow5:escrowedPaymentReminder') +) export default connect( mapStateToProps, diff --git a/packages/mobile/src/notifications/EscrowedPaymentReminderSummaryNotification.test.tsx b/packages/mobile/src/escrow/EscrowedPaymentReminderSummaryNotification.test.tsx similarity index 91% rename from packages/mobile/src/notifications/EscrowedPaymentReminderSummaryNotification.test.tsx rename to packages/mobile/src/escrow/EscrowedPaymentReminderSummaryNotification.test.tsx index 916f47a6034..114d21192d7 100644 --- a/packages/mobile/src/notifications/EscrowedPaymentReminderSummaryNotification.test.tsx +++ b/packages/mobile/src/escrow/EscrowedPaymentReminderSummaryNotification.test.tsx @@ -3,7 +3,7 @@ import 'react-native' import { Provider } from 'react-redux' import * as renderer from 'react-test-renderer' import { escrowPaymentDouble } from 'src/escrow/__mocks__' -import EscrowedPaymentReminderSummaryNotification from 'src/notifications/EscrowedPaymentReminderSummaryNotification' +import EscrowedPaymentReminderSummaryNotification from 'src/escrow/EscrowedPaymentReminderSummaryNotification' import { createMockStore } from 'test/utils' const fakePayments = [escrowPaymentDouble({}), escrowPaymentDouble({})] diff --git a/packages/mobile/src/notifications/EscrowedPaymentReminderSummaryNotification.tsx b/packages/mobile/src/escrow/EscrowedPaymentReminderSummaryNotification.tsx similarity index 53% rename from packages/mobile/src/notifications/EscrowedPaymentReminderSummaryNotification.tsx rename to packages/mobile/src/escrow/EscrowedPaymentReminderSummaryNotification.tsx index dec063d8bb9..9c929d851f8 100644 --- a/packages/mobile/src/notifications/EscrowedPaymentReminderSummaryNotification.tsx +++ b/packages/mobile/src/escrow/EscrowedPaymentReminderSummaryNotification.tsx @@ -1,16 +1,17 @@ -import BaseNotification from '@celo/react-components/components/BaseNotification' import variables from '@celo/react-components/styles/variables' import * as React from 'react' import { WithNamespaces, withNamespaces } from 'react-i18next' -import { Image, StyleSheet, View } from 'react-native' +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 { inviteFriendsIcon } from 'src/images/Images' import { navigate } from 'src/navigator/NavigationService' import { Stacks } from 'src/navigator/Screens' +import SummaryNotification from 'src/notifications/SummaryNotification' interface OwnProps { payments: EscrowedPayment[] @@ -18,47 +19,28 @@ interface OwnProps { type Props = OwnProps & WithNamespaces -const PREVIEW_SIZE = 2 - export class EscrowedPaymentReminderSummaryNotification extends React.Component { - getTotal() { - return this.props.payments.length + onReview = () => { + CeloAnalytics.track(CustomEventNames.escrowed_payment_review) + navigate(Stacks.EscrowStack) } - getTitle() { - const { t } = this.props - return this.getTotal() > 1 - ? t('escrowedPaymentReminderWithCount_plural', { count: this.getTotal() }) - : t('escrowedPaymentReminder') - } - getCTA = () => { - return [ - { - text: this.props.t('review'), - onPress: () => { - CeloAnalytics.track(CustomEventNames.escrowed_payment_review) - navigate(Stacks.EscrowStack) - }, - }, - ] + itemRenderer = (item: EscrowedPayment) => { + return } + render() { - const { payments } = this.props - return ( - + items={payments} + title={t('escrowedPaymentReminder')} icon={} - ctas={this.getCTA()} - roundedBorders={true} - > - - - {payments.slice(0, PREVIEW_SIZE).map((payment, key) => { - return - })} - - - + onReview={this.onReview} + itemRenderer={this.itemRenderer} + /> ) } } diff --git a/packages/mobile/src/escrow/ReclaimPaymentConfirmationCard.tsx b/packages/mobile/src/escrow/ReclaimPaymentConfirmationCard.tsx index 9032fe5851e..9572ef396c2 100644 --- a/packages/mobile/src/escrow/ReclaimPaymentConfirmationCard.tsx +++ b/packages/mobile/src/escrow/ReclaimPaymentConfirmationCard.tsx @@ -74,10 +74,12 @@ class ReclaimPaymentConfirmationCard extends React.PureComponent { {recipientContact.displayName} )} - + {recipientPhone && ( + + )} +491522345678 - - - test message + for - - $ - 7 + 11870000 `; diff --git a/packages/mobile/src/escrow/__snapshots__/EscrowedPaymentListItem.test.tsx.snap b/packages/mobile/src/escrow/__snapshots__/EscrowedPaymentListItem.test.tsx.snap index ae3662c57c3..238259880f4 100644 --- a/packages/mobile/src/escrow/__snapshots__/EscrowedPaymentListItem.test.tsx.snap +++ b/packages/mobile/src/escrow/__snapshots__/EscrowedPaymentListItem.test.tsx.snap @@ -3,188 +3,183 @@ exports[`EscrowedPaymentReminderNotification renders correctly 1`] = ` - - + - - - - escrowedPaymentReminderListItemTitle - - - + + + + escrowPaymentNotificationTitle + - - - sendMessage - - + defaultComment + - + + global:remind + + + - reclaimPayment - + + global:reclaim + + - - - - $0 - - - `; diff --git a/packages/mobile/src/escrow/__snapshots__/EscrowedPaymentListScreen.test.tsx.snap b/packages/mobile/src/escrow/__snapshots__/EscrowedPaymentListScreen.test.tsx.snap index 889835478c9..c9b36d47934 100644 --- a/packages/mobile/src/escrow/__snapshots__/EscrowedPaymentListScreen.test.tsx.snap +++ b/packages/mobile/src/escrow/__snapshots__/EscrowedPaymentListScreen.test.tsx.snap @@ -9,91 +9,6 @@ exports[`EscrowedPaymentListScreen renders correctly with no payments 1`] = ` } } > - - - - - celoDollarBalance - - - $120 - - - - - - payments - - - empty + global:emptyList `; @@ -124,91 +39,6 @@ exports[`EscrowedPaymentListScreen renders correctly with payments 1`] = ` } } > - - - - - celoDollarBalance - - - $120 - - - - - - payments - - - empty + global:emptyList `; diff --git a/packages/mobile/src/escrow/__snapshots__/EscrowedPaymentReminderSummaryNotification.test.tsx.snap b/packages/mobile/src/escrow/__snapshots__/EscrowedPaymentReminderSummaryNotification.test.tsx.snap new file mode 100644 index 00000000000..bdd961b5ad7 --- /dev/null +++ b/packages/mobile/src/escrow/__snapshots__/EscrowedPaymentReminderSummaryNotification.test.tsx.snap @@ -0,0 +1,617 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EscrowedPaymentReminderSummaryNotification renders correctly 1`] = ` + + + + + + + + escrowedPaymentReminder + + + + + + + +491522345678 + for + + + 11870000 + + + + + +491522345678 + for + + + 11870000 + + + + + + + + walletFlow5:review + + + + + + + +`; + +exports[`EscrowedPaymentReminderSummaryNotification when more 1 requests renders just it alone 1`] = ` + + + + + + + + + + escrowPaymentNotificationTitle + + + + test message + + + + + global:remind + + + + + global:reclaim + + + + + + + + + +`; + +exports[`EscrowedPaymentReminderSummaryNotification when more than 2 requests renders just two 1`] = ` + + + + + + + + escrowedPaymentReminder + + + + + + + +491522345678 + for + + + 11870000 + + + + + +491522345678 + for + + + 11870000 + + + + + + + + walletFlow5:review + + + + + + + +`; diff --git a/packages/mobile/src/escrow/__snapshots__/ReclaimPaymentConfirmationCard.test.tsx.snap b/packages/mobile/src/escrow/__snapshots__/ReclaimPaymentConfirmationCard.test.tsx.snap index e3d542a6b57..9279431ea62 100644 --- a/packages/mobile/src/escrow/__snapshots__/ReclaimPaymentConfirmationCard.test.tsx.snap +++ b/packages/mobile/src/escrow/__snapshots__/ReclaimPaymentConfirmationCard.test.tsx.snap @@ -192,7 +192,6 @@ exports[`ReclaimPaymentConfirmationCard renders correctly for send payment confi "alignItems": "stretch", "justifyContent": "center", "marginBottom": 10, - "marginTop": 10, } } > diff --git a/packages/mobile/src/escrow/__snapshots__/ReclaimPaymentConfirmationScreen.test.tsx.snap b/packages/mobile/src/escrow/__snapshots__/ReclaimPaymentConfirmationScreen.test.tsx.snap index 7a196fd37ac..53ee77b7b9f 100644 --- a/packages/mobile/src/escrow/__snapshots__/ReclaimPaymentConfirmationScreen.test.tsx.snap +++ b/packages/mobile/src/escrow/__snapshots__/ReclaimPaymentConfirmationScreen.test.tsx.snap @@ -262,7 +262,6 @@ exports[`ReclaimPaymentConfirmationScreen renders correctly 1`] = ` "alignItems": "stretch", "justifyContent": "center", "marginBottom": 10, - "marginTop": 10, } } > @@ -907,7 +906,6 @@ exports[`ReclaimPaymentConfirmationScreen renders correctly when fee calculation "alignItems": "stretch", "justifyContent": "center", "marginBottom": 10, - "marginTop": 10, } } > @@ -1552,7 +1550,6 @@ exports[`ReclaimPaymentConfirmationScreen renders correctly when fee calculation "alignItems": "stretch", "justifyContent": "center", "marginBottom": 10, - "marginTop": 10, } } > diff --git a/packages/mobile/src/escrow/saga.ts b/packages/mobile/src/escrow/saga.ts index d49452a4723..92861b08953 100644 --- a/packages/mobile/src/escrow/saga.ts +++ b/packages/mobile/src/escrow/saga.ts @@ -243,7 +243,6 @@ function* doFetchSentPayments() { const sentPaymentsRaw = yield all( sentPaymentIDs.map((paymentID) => call(getEscrowedPayment, escrow, paymentID)) ) - const tempAddresstoRecipientPhoneNumber: Invitees = yield select(inviteesSelector) const sentPayments: EscrowedPayment[] = [] for (let i = 0; i < sentPaymentsRaw.length; i++) { diff --git a/packages/mobile/src/home/NotificationBox.test.tsx b/packages/mobile/src/home/NotificationBox.test.tsx index 840fb21a937..12a749e14b4 100644 --- a/packages/mobile/src/home/NotificationBox.test.tsx +++ b/packages/mobile/src/home/NotificationBox.test.tsx @@ -83,7 +83,23 @@ describe('NotificationBox', () => { expect(getByText('inviteFlow11:inviteFriendsToCelo')).toBeTruthy() }) - it('renders payment requests when they exist', () => { + it('renders incoming payment request when they exist', () => { + const store = createMockStore({ + ...storeDataNotificationsDisabled, + account: { + ...storeDataNotificationsDisabled.account, + incomingPaymentRequests: [mockPaymentRequests[0]], + }, + }) + const { getByText } = render( + + + + ) + expect(getByText('incomingPaymentRequestNotificationTitle')).toBeTruthy() + }) + + it('renders incoming payment requests when they exist', () => { const store = createMockStore({ ...storeDataNotificationsDisabled, account: { @@ -96,7 +112,39 @@ describe('NotificationBox', () => { ) - expect(getByText('incomingPaymentRequest')).toBeTruthy() + expect(getByText('incomingPaymentRequests')).toBeTruthy() + }) + + it('renders outgoing payment requests when they exist', () => { + const store = createMockStore({ + ...storeDataNotificationsDisabled, + account: { + ...storeDataNotificationsDisabled.account, + outgoingPaymentRequests: mockPaymentRequests, + }, + }) + const { getByText } = render( + + + + ) + expect(getByText('outgoingPaymentRequests')).toBeTruthy() + }) + + it('renders outgoing payment request when they exist', () => { + const store = createMockStore({ + ...storeDataNotificationsDisabled, + account: { + ...storeDataNotificationsDisabled.account, + outgoingPaymentRequests: [mockPaymentRequests[0]], + }, + }) + const { getByText } = render( + + + + ) + expect(getByText('outgoingPaymentRequestNotificationTitle')).toBeTruthy() }) it('renders verification reminder when not verified', () => { diff --git a/packages/mobile/src/home/NotificationBox.tsx b/packages/mobile/src/home/NotificationBox.tsx index 4943b5ab75b..774563b9d95 100644 --- a/packages/mobile/src/home/NotificationBox.tsx +++ b/packages/mobile/src/home/NotificationBox.tsx @@ -12,6 +12,7 @@ import { CustomEventNames } from 'src/analytics/constants' import { componentWithAnalytics } from 'src/analytics/wrapper' import { PROMOTE_REWARDS_APP } from 'src/config' import { EscrowedPayment } from 'src/escrow/actions' +import EscrowedPaymentReminderSummaryNotification from 'src/escrow/EscrowedPaymentReminderSummaryNotification' import { getReclaimableEscrowPayments } from 'src/escrow/saga' import { setEducationCompleted as setGoldEducationCompleted } from 'src/goldToken/actions' import i18n, { Namespaces } from 'src/i18n' @@ -24,10 +25,9 @@ import { } from 'src/images/Images' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' -import EscrowedPaymentReminderSummaryNotification from 'src/notifications/EscrowedPaymentReminderSummaryNotification' -import IncomingPaymentRequestSummaryNotification from 'src/notifications/IncomingPaymentRequestSummaryNotification' -import OutgoingPaymentRequestSummaryNotification from 'src/notifications/OutgoingPaymentRequestSummaryNotification' import SimpleNotification from 'src/notifications/SimpleNotification' +import IncomingPaymentRequestSummaryNotification from 'src/paymentRequest/IncomingPaymentRequestSummaryNotification' +import OutgoingPaymentRequestSummaryNotification from 'src/paymentRequest/OutgoingPaymentRequestSummaryNotification' import { RootState } from 'src/redux/reducers' import { isBackupTooLate } from 'src/redux/selectors' import { navigateToVerifierApp } from 'src/utils/linking' diff --git a/packages/mobile/src/home/__snapshots__/NotificationBox.test.tsx.snap b/packages/mobile/src/home/__snapshots__/NotificationBox.test.tsx.snap index 6de974ee123..cfbf818ddb3 100644 --- a/packages/mobile/src/home/__snapshots__/NotificationBox.test.tsx.snap +++ b/packages/mobile/src/home/__snapshots__/NotificationBox.test.tsx.snap @@ -24,73 +24,79 @@ exports[`NotificationBox renders correctly for with all notifications 1`] = ` } } > - + - - - - + + + - incomingPaymentRequest - - + + incomingPaymentRequests + @@ -118,6 +124,19 @@ exports[`NotificationBox renders correctly for with all notifications 1`] = ` } } > + + +14155550000 + for + + 12.34 + + + + +14155550000 - - - Rent request for June, it is already late!!! + for - $12.34 + 12.34 @@ -158,7 +201,7 @@ exports[`NotificationBox renders correctly for with all notifications 1`] = ` style={ Object { "flexDirection": "row", - "marginTop": 15, + "marginTop": 5, } } > @@ -195,7 +238,7 @@ exports[`NotificationBox renders correctly for with all notifications 1`] = ` ] } > - review + walletFlow5:review @@ -212,73 +255,79 @@ exports[`NotificationBox renders correctly for with all notifications 1`] = ` } } > - + - - - - + + + - backupKeyFlow6:yourBackupKey - - + + backupKeyFlow6:yourBackupKey + @@ -306,7 +355,7 @@ exports[`NotificationBox renders correctly for with all notifications 1`] = ` style={ Object { "flexDirection": "row", - "marginTop": 15, + "marginTop": 5, } } > @@ -360,73 +409,79 @@ exports[`NotificationBox renders correctly for with all notifications 1`] = ` } } > - + - - - - + + + - nuxVerification2:notification.title - - + + nuxVerification2:notification.title + @@ -454,7 +509,7 @@ exports[`NotificationBox renders correctly for with all notifications 1`] = ` style={ Object { "flexDirection": "row", - "marginTop": 15, + "marginTop": 5, } } > @@ -544,73 +599,79 @@ exports[`NotificationBox renders correctly for with all notifications 1`] = ` } } > - + - - - - + + + - global:celoGold - - + + global:celoGold + @@ -638,7 +699,7 @@ exports[`NotificationBox renders correctly for with all notifications 1`] = ` style={ Object { "flexDirection": "row", - "marginTop": 15, + "marginTop": 5, } } > @@ -728,73 +789,79 @@ exports[`NotificationBox renders correctly for with all notifications 1`] = ` } } > - + - - - - + + + - inviteFlow11:inviteFriendsToCelo - - + + inviteFlow11:inviteFriendsToCelo + @@ -822,7 +889,7 @@ exports[`NotificationBox renders correctly for with all notifications 1`] = ` style={ Object { "flexDirection": "row", - "marginTop": 15, + "marginTop": 5, } } > diff --git a/packages/mobile/src/home/__snapshots__/WalletHome.test.tsx.snap b/packages/mobile/src/home/__snapshots__/WalletHome.test.tsx.snap index f527ca797e0..61e335996f5 100644 --- a/packages/mobile/src/home/__snapshots__/WalletHome.test.tsx.snap +++ b/packages/mobile/src/home/__snapshots__/WalletHome.test.tsx.snap @@ -295,7 +295,7 @@ exports[`Testnet banner Renders when connected with backup complete 1`] = ` style={ Object { "color": "#2E3338", - "fontFamily": "Hind-Medium", + "fontFamily": "Hind-Bold", "fontSize": 14, "opacity": 0, } @@ -1660,7 +1660,7 @@ exports[`Testnet banner Shows testnet banner for 5 seconds 1`] = ` style={ Object { "color": "#2E3338", - "fontFamily": "Hind-Medium", + "fontFamily": "Hind-Bold", "fontSize": 14, "opacity": 0, } diff --git a/packages/mobile/src/icons/PaymentsIcon.tsx b/packages/mobile/src/icons/PaymentsIcon.tsx index d18699d8f7b..275680ebf58 100644 --- a/packages/mobile/src/icons/PaymentsIcon.tsx +++ b/packages/mobile/src/icons/PaymentsIcon.tsx @@ -10,8 +10,8 @@ interface Props { export default class PaymentsIcon extends React.PureComponent { static defaultProps = { - width: 28, - height: 28, + width: 34, + height: 34, color: colors.dark, } diff --git a/packages/mobile/src/navigator/Navigator.tsx b/packages/mobile/src/navigator/Navigator.tsx index 18cf8d530e9..77d446d8720 100644 --- a/packages/mobile/src/navigator/Navigator.tsx +++ b/packages/mobile/src/navigator/Navigator.tsx @@ -278,6 +278,12 @@ const AppStack = createStackNavigator( // Note, WalletHome isn't in this stack because it's part of the tab navigator [Screens.TabNavigator]: { screen: TabNavigator }, [Stacks.SendStack]: { screen: SendStack }, + // Adding this screen, so it possbile to go back to Home screen from it + [Screens.SendConfirmation]: { screen: SendConfirmation }, + // Adding this screen, so it possbile to go back to Home screen from it + [Screens.ReclaimPaymentConfirmationScreen]: { + screen: ReclaimPaymentConfirmationScreen, + }, [Stacks.QRSendStack]: { screen: QRSendStack }, [Stacks.ExchangeStack]: { screen: ExchangeStack }, [Stacks.IncomingRequestStack]: { screen: IncomingRequestStack }, diff --git a/packages/mobile/src/navigator/TabNavigator.tsx b/packages/mobile/src/navigator/TabNavigator.tsx index a7b5fdd3790..e879fc568ad 100644 --- a/packages/mobile/src/navigator/TabNavigator.tsx +++ b/packages/mobile/src/navigator/TabNavigator.tsx @@ -163,7 +163,7 @@ const styles = StyleSheet.create({ marginLeft: 3, }, alignPaymentIcon: { - marginBottom: 15, + marginBottom: 8, }, }) diff --git a/packages/mobile/src/notifications/IncomingPaymentRequestSummaryNotification.tsx b/packages/mobile/src/notifications/IncomingPaymentRequestSummaryNotification.tsx index 3c354797246..d73183456e0 100644 --- a/packages/mobile/src/notifications/IncomingPaymentRequestSummaryNotification.tsx +++ b/packages/mobile/src/notifications/IncomingPaymentRequestSummaryNotification.tsx @@ -95,7 +95,6 @@ export class IncomingPaymentRequestSummaryNotification extends React.Component

} ctas={this.getCTA()} - roundedBorders={true} > @@ -104,7 +103,6 @@ export class IncomingPaymentRequestSummaryNotification extends React.Component

diff --git a/packages/mobile/src/notifications/NotificationList.test.tsx b/packages/mobile/src/notifications/NotificationList.test.tsx new file mode 100644 index 00000000000..1ff90c52c26 --- /dev/null +++ b/packages/mobile/src/notifications/NotificationList.test.tsx @@ -0,0 +1,25 @@ +import * as React from 'react' +import { Text } from 'react-native' +import { Provider } from 'react-redux' +import * as renderer from 'react-test-renderer' +import { NotificationList } from 'src/notifications/NotificationList' +import { createMockStore } from 'test/utils' + +const props = () => ({ + dollarBalance: '123', + items: ['a'], + listItemRenderer: (item: string, key: number) => { + return {`test-${item}`} + }, +}) + +describe(NotificationList, () => { + it('renders correctly', () => { + const tree = renderer.create( + + {...props()} /> + + ) + expect(tree).toMatchSnapshot() + }) +}) diff --git a/packages/mobile/src/notifications/NotificationList.tsx b/packages/mobile/src/notifications/NotificationList.tsx new file mode 100644 index 00000000000..e25f0eff4c7 --- /dev/null +++ b/packages/mobile/src/notifications/NotificationList.tsx @@ -0,0 +1,99 @@ +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 React, { useEffect } from 'react' +import { ScrollView, StyleSheet, Text, View } from 'react-native' +import SafeAreaView from 'react-native-safe-area-view' +import { + NavigationParams, + NavigationProp, + NavigationScreenProp, + NavigationState, +} from 'react-navigation' +import { CURRENCIES, CURRENCY_ENUM } from 'src/geth/consts' +import i18n from 'src/i18n' +import { headerWithBackButton } from 'src/navigator/Headers' +import DisconnectBanner from 'src/shared/DisconnectBanner' +import { getCentAwareMoneyDisplay } from 'src/utils/formatting' + +const { contentPadding } = variables + +interface OwnProps { + dollarBalance: string | null + listItemRenderer: (item: T, key: number) => JSX.Element + items: T[] +} + +type Props = OwnProps + +export function NotificationList(props: Props) { + return ( + + + {props.items.length > 0 ? ( + + {props.items.map(props.listItemRenderer)} + + ) : ( + {i18n.t('global:emptyList')} + )} + + ) +} + +export function titleWithBalanceNavigationOptions(title: string) { + return ({ navigation }: { navigation: NavigationProp }) => ({ + ...headerWithBackButton, + headerTitle: ( + + {title && {title}} + + {(navigation.state.params && + navigation.state.params.dollarBalance && + CURRENCIES[CURRENCY_ENUM.DOLLAR].symbol + + i18n.t('sendFlow7:celoDollarsAvailable', { + CeloDollars: getCentAwareMoneyDisplay(navigation.state.params.dollarBalance || 0), + })) || + i18n.t('global:loading')} + + + ), + }) +} + +const styles = StyleSheet.create({ + container: { + backgroundColor: colors.background, + flex: 1, + }, + scrollArea: { + margin: contentPadding, + }, + balanceText: { + ...fontStyles.bodySmall, + color: colors.inactiveDark, + }, + empty: { + textAlign: 'center', + marginTop: 30, + }, + header: { + alignItems: 'center', + flex: 1, + }, +}) + +// Should be used together with titleWithBalanceNavigationOptions. +// Call this hook inside a Screen and assign +// Screen.navigationOptions = titleWithBalanceNavigationOptions(title) +export function useBalanceInNavigationParam( + dollarBalance: string | null, + navigation: NavigationScreenProp +) { + useEffect( + () => { + navigation.setParams({ dollarBalance }) + }, + [dollarBalance] + ) +} diff --git a/packages/mobile/src/notifications/OutgoingPaymentRequestSummaryNotification.tsx b/packages/mobile/src/notifications/OutgoingPaymentRequestSummaryNotification.tsx index f1ab00394b6..23f555ae5bc 100644 --- a/packages/mobile/src/notifications/OutgoingPaymentRequestSummaryNotification.tsx +++ b/packages/mobile/src/notifications/OutgoingPaymentRequestSummaryNotification.tsx @@ -95,7 +95,6 @@ export class OutgoingPaymentRequestSummaryNotification extends React.Component

} ctas={this.getCTA()} - roundedBorders={true} > @@ -104,7 +103,6 @@ export class OutgoingPaymentRequestSummaryNotification extends React.Component

diff --git a/packages/mobile/src/notifications/SimpleNotification.test.tsx b/packages/mobile/src/notifications/SimpleNotification.test.tsx index aae8da07a97..52384815a8d 100644 --- a/packages/mobile/src/notifications/SimpleNotification.test.tsx +++ b/packages/mobile/src/notifications/SimpleNotification.test.tsx @@ -1,11 +1,12 @@ import * as React from 'react' import 'react-native' import * as renderer from 'react-test-renderer' +import { placeholder } from 'src/images/Images' import SimpleNotification from 'src/notifications/SimpleNotification' const props = () => ({ text: 'Gold is where you can choose to store Celo dollars you have', - image: require('src/images/placeholder.png'), + image: placeholder, title: 'Test', ctaList: [{ text: 'it goes boom', onPress: jest.fn() }], }) diff --git a/packages/mobile/src/notifications/SimpleNotification.tsx b/packages/mobile/src/notifications/SimpleNotification.tsx index 200bc12dca9..1cf9e9b64b6 100644 --- a/packages/mobile/src/notifications/SimpleNotification.tsx +++ b/packages/mobile/src/notifications/SimpleNotification.tsx @@ -18,7 +18,6 @@ export function SimpleNotification(props: Props) { title={props.title} icon={} ctas={props.ctaList} - roundedBorders={true} > {props.text} diff --git a/packages/mobile/src/notifications/SummaryNotification.test.tsx b/packages/mobile/src/notifications/SummaryNotification.test.tsx new file mode 100644 index 00000000000..e9e05df4bb2 --- /dev/null +++ b/packages/mobile/src/notifications/SummaryNotification.test.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' +import { Text, View } from 'react-native' +import * as renderer from 'react-test-renderer' +import { placeholder } from 'src/images/Images' +import SummaryNotification from 'src/notifications/SummaryNotification' + +const props = () => ({ + image: placeholder, + icon: , + title: 'Test', + items: ['a'], + itemRenderer: (item: string, key: number) => { + return {`test-${item}`} + }, + onReview: jest.fn(), +}) + +describe(SummaryNotification, () => { + it('renders correctly', () => { + const tree = renderer.create( {...props()} />) + expect(tree).toMatchSnapshot() + }) +}) diff --git a/packages/mobile/src/notifications/SummaryNotification.tsx b/packages/mobile/src/notifications/SummaryNotification.tsx new file mode 100644 index 00000000000..a5befbbcde0 --- /dev/null +++ b/packages/mobile/src/notifications/SummaryNotification.tsx @@ -0,0 +1,69 @@ +import SummaryNotification from '@celo/react-components/components/SummaryNotification' +import fontStyles from '@celo/react-components/styles/fonts' +import * as React from 'react' +import i18n from 'src/i18n' + +import { StyleSheet, Text, View } from 'react-native' + +interface OwnProps { + title: string + icon: JSX.Element + onReview: () => void + itemRenderer: (item: T, key: number) => JSX.Element + items: T[] +} + +type Props = OwnProps + +const PREVIEW_SIZE = 2 + +function getAdditionalItemsCount(items: T[]) { + const total = items.length + if (total - PREVIEW_SIZE > 0) { + return ( + + + {i18n.t('global:moreWithCount', { count: total - PREVIEW_SIZE + 1 })} + + + ) + } +} + +// Payment Request notification for the notification center on home screen +function PaymentsSummaryNotification(props: Props) { + const { items, title, icon, onReview, itemRenderer } = props + + return ( + + + + {items + .slice(0, props.items.length > PREVIEW_SIZE ? PREVIEW_SIZE - 1 : PREVIEW_SIZE) + .map(itemRenderer)} + {getAdditionalItemsCount(items)} + + + + ) +} + +const styles = StyleSheet.create({ + body: { + marginTop: 5, + flexDirection: 'row', + }, + items: { + flex: 1, + }, + moreWithCountText: fontStyles.subSmall, +}) + +export default PaymentsSummaryNotification diff --git a/packages/mobile/src/notifications/__snapshots__/EscrowedPaymentReminderSummaryNotification.test.tsx.snap b/packages/mobile/src/notifications/__snapshots__/EscrowedPaymentReminderSummaryNotification.test.tsx.snap deleted file mode 100644 index 21c1761ff7e..00000000000 --- a/packages/mobile/src/notifications/__snapshots__/EscrowedPaymentReminderSummaryNotification.test.tsx.snap +++ /dev/null @@ -1,643 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EscrowedPaymentReminderSummaryNotification renders correctly 1`] = ` - - - - - - - escrowedPaymentReminderWithCount_plural - - - - - - - - +491522345678 - - - test message - - - - $ - 7 - - - - - +491522345678 - - - test message - - - - $ - 7 - - - - - - - - review - - - - - - - -`; - -exports[`EscrowedPaymentReminderSummaryNotification when more 1 requests renders just it alone 1`] = ` - - - - - - - escrowedPaymentReminder - - - - - - - - +491522345678 - - - test message - - - - $ - 7 - - - - - - - - review - - - - - - - -`; - -exports[`EscrowedPaymentReminderSummaryNotification when more than 2 requests renders just two 1`] = ` - - - - - - - escrowedPaymentReminderWithCount_plural - - - - - - - - +491522345678 - - - test message - - - - $ - 7 - - - - - +491522345678 - - - test message - - - - $ - 7 - - - - - - - - review - - - - - - - -`; diff --git a/packages/mobile/src/notifications/__snapshots__/IncomingPaymentRequestSummaryNotification.test.tsx.snap b/packages/mobile/src/notifications/__snapshots__/IncomingPaymentRequestSummaryNotification.test.tsx.snap deleted file mode 100644 index e56291ac63e..00000000000 --- a/packages/mobile/src/notifications/__snapshots__/IncomingPaymentRequestSummaryNotification.test.tsx.snap +++ /dev/null @@ -1,677 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`IncomingPaymentRequestSummaryNotification renders correctly 1`] = ` - - - - - - - incomingPaymentRequestWithCount - - - - - - - - +491522345678 - - - Dinner for me and the gals, PIZZAA! - - - $200,000 - - - - - +491522345678 - - - My Birthday Present. :) Am I not the best? Celebration. Bam! - - - $180.89 - - - - - - + - 1 - - - - - - - review - - - - - - - -`; - -exports[`IncomingPaymentRequestSummaryNotification when more 1 requests renders just it alone 1`] = ` - - - - - - - incomingPaymentRequest - - - - - - - - +491522345678 - - - Dinner for me and the gals, PIZZAA! - - - $200,000 - - - - - - - - review - - - - - - - -`; - -exports[`IncomingPaymentRequestSummaryNotification when more than 2 requests renders just two 1`] = ` - - - - - - - incomingPaymentRequestWithCount - - - - - - - - +491522345678 - - - Dinner for me and the gals, PIZZAA! - - - $200,000 - - - - - +491522345678 - - - My Birthday Present. :) Am I not the best? Celebration. Bam! - - - $180.89 - - - - - - + - 1 - - - - - - - review - - - - - - - -`; diff --git a/packages/mobile/src/notifications/__snapshots__/NotificationList.test.tsx.snap b/packages/mobile/src/notifications/__snapshots__/NotificationList.test.tsx.snap new file mode 100644 index 00000000000..9a076d94e37 --- /dev/null +++ b/packages/mobile/src/notifications/__snapshots__/NotificationList.test.tsx.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NotificationList renders correctly 1`] = ` + + + + + + test-a + + + + + +`; diff --git a/packages/mobile/src/notifications/__snapshots__/OutgoingPaymentRequestSummaryNotification.test.tsx.snap b/packages/mobile/src/notifications/__snapshots__/OutgoingPaymentRequestSummaryNotification.test.tsx.snap deleted file mode 100644 index d797a5c3241..00000000000 --- a/packages/mobile/src/notifications/__snapshots__/OutgoingPaymentRequestSummaryNotification.test.tsx.snap +++ /dev/null @@ -1,677 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`OutgoingPaymentRequestSummaryNotification renders correctly 1`] = ` - - - - - - - outgoingPaymentRequestWithCount - - - - - - - - +491522345678 - - - Dinner for me and the gals, PIZZAA! - - - $200,000 - - - - - +491522345678 - - - My Birthday Present. :) Am I not the best? Celebration. Bam! - - - $180.89 - - - - - - + - 1 - - - - - - - review - - - - - - - -`; - -exports[`OutgoingPaymentRequestSummaryNotification when more 1 requests renders just it alone 1`] = ` - - - - - - - outgoingPaymentRequest - - - - - - - - +491522345678 - - - Dinner for me and the gals, PIZZAA! - - - $200,000 - - - - - - - - review - - - - - - - -`; - -exports[`OutgoingPaymentRequestSummaryNotification when more than 2 requests renders just two 1`] = ` - - - - - - - outgoingPaymentRequestWithCount - - - - - - - - +491522345678 - - - Dinner for me and the gals, PIZZAA! - - - $200,000 - - - - - +491522345678 - - - My Birthday Present. :) Am I not the best? Celebration. Bam! - - - $180.89 - - - - - - + - 1 - - - - - - - review - - - - - - - -`; diff --git a/packages/mobile/src/notifications/__snapshots__/SimpleNotification.test.tsx.snap b/packages/mobile/src/notifications/__snapshots__/SimpleNotification.test.tsx.snap index 0f04a9f122a..eef2c2f0c62 100644 --- a/packages/mobile/src/notifications/__snapshots__/SimpleNotification.test.tsx.snap +++ b/packages/mobile/src/notifications/__snapshots__/SimpleNotification.test.tsx.snap @@ -1,73 +1,79 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SimpleNotification renders correctly 1`] = ` - + - - - - + + + - Test - - + + Test + @@ -95,7 +101,7 @@ exports[`SimpleNotification renders correctly 1`] = ` style={ Object { "flexDirection": "row", - "marginTop": 15, + "marginTop": 5, } } > diff --git a/packages/mobile/src/notifications/__snapshots__/SummaryNotification.test.tsx.snap b/packages/mobile/src/notifications/__snapshots__/SummaryNotification.test.tsx.snap new file mode 100644 index 00000000000..458502ecd1b --- /dev/null +++ b/packages/mobile/src/notifications/__snapshots__/SummaryNotification.test.tsx.snap @@ -0,0 +1,136 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PaymentsSummaryNotification renders correctly 1`] = ` + + + + + + + + Test + + + + + + test-a + + + + + + + walletFlow5:review + + + + + + + +`; diff --git a/packages/mobile/src/paymentRequest/IncomingPaymentRequestListItem.tsx b/packages/mobile/src/paymentRequest/IncomingPaymentRequestListItem.tsx index 30200b8132e..d692432e728 100644 --- a/packages/mobile/src/paymentRequest/IncomingPaymentRequestListItem.tsx +++ b/packages/mobile/src/paymentRequest/IncomingPaymentRequestListItem.tsx @@ -1,23 +1,22 @@ import BaseNotification from '@celo/react-components/components/BaseNotification' import ContactCircle from '@celo/react-components/components/ContactCircle' -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 BigNumber from 'bignumber.js' import * as React from 'react' import { WithNamespaces, withNamespaces } from 'react-i18next' -import { StyleSheet, Text, TouchableOpacity, View } from 'react-native' +import { Image, StyleSheet, Text, View } from 'react-native' import { PaymentRequestStatus } from 'src/account/types' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' import { updatePaymentRequestStatus } from 'src/firebase/actions' +import { CURRENCIES, CURRENCY_ENUM } from 'src/geth/consts' import { Namespaces } from 'src/i18n' +import { unknownUserIcon } from 'src/images/Images' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' -import NotificationAmount from 'src/paymentRequest/NotificationAmount' import { getRecipientThumbnail, Recipient } from 'src/recipients/recipient' import { TransactionTypes } from 'src/transactions/reducer' -import { multiplyByWei } from 'src/utils/formatting' +import { getCentAwareMoneyDisplay } from 'src/utils/formatting' import Logger from 'src/utils/Logger' interface OwnProps { @@ -30,6 +29,8 @@ interface OwnProps { type Props = OwnProps & WithNamespaces +const AVATAR_SIZE = 40 + export class IncomingPaymentRequestListItem extends React.Component { onPay = () => { const { amount, comment: reason, requester: recipient } = this.props @@ -51,7 +52,6 @@ export class IncomingPaymentRequestListItem extends React.Component { this.props.updatePaymentRequestStatus(id.toString(), PaymentRequestStatus.COMPLETED) Logger.showMessage(this.props.t('requestPaid')) CeloAnalytics.track(CustomEventNames.incoming_request_payment_pay) - this.onFinalized() } onPaymentDecline = () => { @@ -59,69 +59,61 @@ export class IncomingPaymentRequestListItem extends React.Component { this.props.updatePaymentRequestStatus(id.toString(), PaymentRequestStatus.DECLINED) Logger.showMessage(this.props.t('requestDeclined')) CeloAnalytics.track(CustomEventNames.incoming_request_payment_decline) - this.onFinalized() - } - - onFinalized = () => { - navigate(Screens.IncomingPaymentRequestListScreen) } getCTA = () => { return [ { - text: this.props.t('pay'), + text: this.props.t('global:pay'), onPress: this.onPay, }, { - text: this.props.t('decline'), + text: this.props.t('global:decline'), onPress: this.onPaymentDecline, }, ] } - isDisplayingNumber = () => { - return this.props.requester.displayId !== this.props.requester.displayName - } - render() { - const { requester } = this.props + const { requester, t } = this.props return ( - + + > + + } - title={requester.displayName} + title={t('incomingPaymentRequestNotificationTitle', { + name: requester.displayName, + amount: + CURRENCIES[CURRENCY_ENUM.DOLLAR].symbol + getCentAwareMoneyDisplay(this.props.amount), + })} ctas={this.getCTA()} - roundedBorders={false} - callout={} + onPress={this.onPay} > - - {this.isDisplayingNumber() && ( - - {this.props.requester.displayId} - - )} - {this.props.comment} - + {this.props.comment || t('defaultComment')} - + ) } } const styles = StyleSheet.create({ - comment: { - paddingTop: variables.contentPadding, + container: { + marginBottom: 16, }, - phoneNumber: { - color: colors.dark, + unknownUser: { + height: AVATAR_SIZE, + width: AVATAR_SIZE, + justifyContent: 'center', + alignItems: 'center', }, }) -export default withNamespaces(Namespaces.global)(IncomingPaymentRequestListItem) +export default withNamespaces(Namespaces.paymentRequestFlow)(IncomingPaymentRequestListItem) diff --git a/packages/mobile/src/paymentRequest/IncomingPaymentRequestListScreen.test.tsx b/packages/mobile/src/paymentRequest/IncomingPaymentRequestListScreen.test.tsx index 28831dff37b..2702ee76761 100644 --- a/packages/mobile/src/paymentRequest/IncomingPaymentRequestListScreen.test.tsx +++ b/packages/mobile/src/paymentRequest/IncomingPaymentRequestListScreen.test.tsx @@ -1,3 +1,4 @@ +import BigNumber from 'bignumber.js' import * as React from 'react' import 'react-native' import { Provider } from 'react-redux' @@ -5,8 +6,8 @@ import * as renderer from 'react-test-renderer' import { PaymentRequest } from 'src/account/types' import { paymentRequestDouble } from 'src/paymentRequest/__mocks__' import IncomingPaymentRequestListScreen from 'src/paymentRequest/IncomingPaymentRequestListScreen' -import { createMockStore } from 'test/utils' -import { mockAccount, mockE164Number } from 'test/values' +import { createMockNavigationProp, createMockStore } from 'test/utils' +import { mockAccount, mockE164Number, mockRecipient } from 'test/values' const requests = [ paymentRequestDouble({ @@ -29,6 +30,13 @@ const requests = [ }), ] +const navigation = createMockNavigationProp({ + recipient: mockRecipient, + recipientAddress: mockAccount, + amount: new BigNumber(10), + reason: 'My Reason', +}) + function testStore(incomingPaymentRequests: PaymentRequest[]) { return createMockStore({ stableToken: { balance: '120' }, @@ -42,7 +50,7 @@ describe('IncomingPaymentRequestListScreen', () => { const tree = renderer.create( - + ) expect(tree).toMatchSnapshot() @@ -53,7 +61,7 @@ describe('IncomingPaymentRequestListScreen', () => { const tree = renderer.create( - + ) expect(tree).toMatchSnapshot() diff --git a/packages/mobile/src/paymentRequest/IncomingPaymentRequestListScreen.tsx b/packages/mobile/src/paymentRequest/IncomingPaymentRequestListScreen.tsx index f607cb1f8fe..168677a1ec5 100644 --- a/packages/mobile/src/paymentRequest/IncomingPaymentRequestListScreen.tsx +++ b/packages/mobile/src/paymentRequest/IncomingPaymentRequestListScreen.tsx @@ -1,10 +1,6 @@ -import colors from '@celo/react-components/styles/colors' -import { componentStyles } from '@celo/react-components/styles/styles' -import variables from '@celo/react-components/styles/variables' -import * as React from 'react' +import React from 'react' import { WithNamespaces, withNamespaces } from 'react-i18next' -import { ScrollView, StyleSheet, View } from 'react-native' -import SafeAreaView from 'react-native-safe-area-view' +import { NavigationInjectedProps } from 'react-navigation' import { connect } from 'react-redux' import { getIncomingPaymentRequests } from 'src/account/selectors' import { PaymentRequest } from 'src/account/types' @@ -12,17 +8,16 @@ import { updatePaymentRequestStatus } from 'src/firebase/actions' import i18n, { Namespaces } from 'src/i18n' import { fetchPhoneAddresses } from 'src/identity/actions' import { e164NumberToAddressSelector, E164NumberToAddressType } from 'src/identity/reducer' -import { headerWithBackButton } from 'src/navigator/Headers' +import { + NotificationList, + titleWithBalanceNavigationOptions, + useBalanceInNavigationParam, +} from 'src/notifications/NotificationList' import IncomingPaymentRequestListItem from 'src/paymentRequest/IncomingPaymentRequestListItem' -import PaymentRequestBalance from 'src/paymentRequest/PaymentRequestBalance' -import PaymentRequestListEmpty from 'src/paymentRequest/PaymentRequestListEmpty' import { getRecipientFromPaymentRequest } from 'src/paymentRequest/utils' import { NumberToRecipient } from 'src/recipients/recipient' import { recipientCacheSelector } from 'src/recipients/reducer' import { RootState } from 'src/redux/reducers' -import DisconnectBanner from 'src/shared/DisconnectBanner' - -const { contentPadding } = variables interface StateProps { dollarBalance: string | null @@ -43,65 +38,44 @@ const mapStateToProps = (state: RootState): StateProps => ({ recipientCache: recipientCacheSelector(state), }) -type Props = WithNamespaces & StateProps & DispatchProps - -export class IncomingPaymentRequestListScreen extends React.Component { - static navigationOptions = () => ({ - ...headerWithBackButton, - headerTitle: i18n.t('paymentRequestFlow:incomingPaymentRequests'), - }) +type Props = NavigationInjectedProps & WithNamespaces & StateProps & DispatchProps - renderRequest = (request: PaymentRequest, key: number, allRequests: PaymentRequest[]) => { - const { recipientCache } = this.props - const requester = getRecipientFromPaymentRequest(request, recipientCache) +export const listItemRenderer = (params: { + recipientCache: NumberToRecipient + updatePaymentRequestStatus: typeof updatePaymentRequestStatus +}) => (request: PaymentRequest, key: number | undefined = undefined) => { + const requester = getRecipientFromPaymentRequest(request, params.recipientCache) - return ( - - - {key < allRequests.length - 1 && } - - ) - } + return ( + + ) +} - render() { - return ( - - - - {this.props.paymentRequests.length > 0 ? ( - - - {this.props.paymentRequests.map(this.renderRequest)} - - - ) : ( - - )} - - ) - } +const IncomingPaymentRequestListScreen = (props: Props) => { + const { recipientCache, dollarBalance, navigation } = props + useBalanceInNavigationParam(dollarBalance, navigation) + return ( + + ) } -const styles = StyleSheet.create({ - container: { - backgroundColor: colors.background, - flex: 1, - }, - separator: { - borderBottomColor: colors.darkLightest, - borderBottomWidth: 1, - marginLeft: 50, - }, - scrollArea: { - margin: contentPadding, - }, -}) +IncomingPaymentRequestListScreen.navigationOptions = titleWithBalanceNavigationOptions( + i18n.t('walletFlow5:incomingPaymentRequests') +) export default connect( mapStateToProps, diff --git a/packages/mobile/src/notifications/IncomingPaymentRequestSummaryNotification.test.tsx b/packages/mobile/src/paymentRequest/IncomingPaymentRequestSummaryNotification.test.tsx similarity index 95% rename from packages/mobile/src/notifications/IncomingPaymentRequestSummaryNotification.test.tsx rename to packages/mobile/src/paymentRequest/IncomingPaymentRequestSummaryNotification.test.tsx index 95c5891808c..70953924062 100644 --- a/packages/mobile/src/notifications/IncomingPaymentRequestSummaryNotification.test.tsx +++ b/packages/mobile/src/paymentRequest/IncomingPaymentRequestSummaryNotification.test.tsx @@ -4,7 +4,7 @@ import { Provider } from 'react-redux' import * as renderer from 'react-test-renderer' import { PaymentRequestStatus } from 'src/account/types' import { SHORT_CURRENCIES } from 'src/geth/consts' -import IncomingPaymentRequestSummaryNotification from 'src/notifications/IncomingPaymentRequestSummaryNotification' +import IncomingPaymentRequestSummaryNotification from 'src/paymentRequest/IncomingPaymentRequestSummaryNotification' import { createMockStore } from 'test/utils' const requesterE164Number = '+491522345678' diff --git a/packages/mobile/src/paymentRequest/IncomingPaymentRequestSummaryNotification.tsx b/packages/mobile/src/paymentRequest/IncomingPaymentRequestSummaryNotification.tsx new file mode 100644 index 00000000000..0ae36790ecb --- /dev/null +++ b/packages/mobile/src/paymentRequest/IncomingPaymentRequestSummaryNotification.tsx @@ -0,0 +1,104 @@ +import * as React from 'react' +import { WithNamespaces, withNamespaces } from 'react-i18next' +import { Image, StyleSheet } from 'react-native' +import { connect } from 'react-redux' +import { PaymentRequest } from 'src/account/types' +import CeloAnalytics from 'src/analytics/CeloAnalytics' +import { CustomEventNames } from 'src/analytics/constants' +import { updatePaymentRequestStatus } from 'src/firebase/actions' +import { Namespaces } from 'src/i18n' +import { + addressToE164NumberSelector, + AddressToE164NumberType, + e164NumberToAddressSelector, + E164NumberToAddressType, +} from 'src/identity/reducer' +import { sendDollar } from 'src/images/Images' +import { navigate } from 'src/navigator/NavigationService' +import { Stacks } from 'src/navigator/Screens' +import SummaryNotification from 'src/notifications/SummaryNotification' +import { listItemRenderer } from 'src/paymentRequest/IncomingPaymentRequestListScreen' +import PaymentRequestNotificationInner from 'src/paymentRequest/PaymentRequestNotificationInner' +import { NumberToRecipient, phoneNumberToRecipient } from 'src/recipients/recipient' +import { recipientCacheSelector } from 'src/recipients/reducer' +import { RootState } from 'src/redux/reducers' + +interface OwnProps { + requests: PaymentRequest[] +} + +interface DispatchProps { + updatePaymentRequestStatus: typeof updatePaymentRequestStatus +} + +type Props = OwnProps & DispatchProps & WithNamespaces & StateProps + +interface StateProps { + e164PhoneNumberAddressMapping: E164NumberToAddressType + addressToE164Number: AddressToE164NumberType + recipientCache: NumberToRecipient +} + +const mapStateToProps = (state: RootState): StateProps => ({ + e164PhoneNumberAddressMapping: e164NumberToAddressSelector(state), + addressToE164Number: addressToE164NumberSelector(state), + recipientCache: recipientCacheSelector(state), +}) + +// Payment Request notification for the notification center on home screen +export class IncomingPaymentRequestSummaryNotification extends React.Component { + getRequesterRecipient = (requesterE164Number: string) => { + return phoneNumberToRecipient( + requesterE164Number, + this.props.e164PhoneNumberAddressMapping[requesterE164Number], + this.props.recipientCache + ) + } + + onReview = () => { + CeloAnalytics.track(CustomEventNames.incoming_request_payment_review) + navigate(Stacks.IncomingRequestStack) + } + + itemRenderer = (item: PaymentRequest) => { + return ( + + ) + } + + render() { + const { recipientCache, requests, t } = this.props + + return requests.length === 1 ? ( + listItemRenderer({ + updatePaymentRequestStatus: this.props.updatePaymentRequestStatus, + recipientCache, + })(requests[0]) + ) : ( + + items={requests} + title={t('incomingPaymentRequests')} + icon={} + onReview={this.onReview} + itemRenderer={this.itemRenderer} + /> + ) + } +} + +const styles = StyleSheet.create({ + image: { + width: 40, + height: 40, + }, +}) + +export default connect( + mapStateToProps, + { updatePaymentRequestStatus } +)(withNamespaces(Namespaces.walletFlow5)(IncomingPaymentRequestSummaryNotification)) diff --git a/packages/mobile/src/paymentRequest/NotificationAmount.tsx b/packages/mobile/src/paymentRequest/NotificationAmount.tsx deleted file mode 100644 index d9748b8913d..00000000000 --- a/packages/mobile/src/paymentRequest/NotificationAmount.tsx +++ /dev/null @@ -1,30 +0,0 @@ -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 { StyleSheet, Text, View } from 'react-native' -import { CURRENCIES, CURRENCY_ENUM } from 'src/geth/consts' -import { divideByWei, getCentAwareMoneyDisplay } from 'src/utils/formatting' - -interface Props { - amount: BigNumber.Value -} - -export default class NotificationAmount extends React.PureComponent { - render() { - return ( - - - {CURRENCIES[CURRENCY_ENUM.DOLLAR].symbol + - getCentAwareMoneyDisplay(divideByWei(this.props.amount))} - - - ) - } -} - -const styles = StyleSheet.create({ - amount: { - color: colors.darkSecondary, - }, -}) diff --git a/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListItem.tsx b/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListItem.tsx index 1a90b7dec72..eb0c1386099 100644 --- a/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListItem.tsx +++ b/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListItem.tsx @@ -2,20 +2,19 @@ import BaseNotification from '@celo/react-components/components/BaseNotification import ContactCircle from '@celo/react-components/components/ContactCircle' 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 { StyleSheet, Text, View } from 'react-native' +import { Image, StyleSheet, Text, View } from 'react-native' import { PaymentRequestStatus } from 'src/account/types' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' import { updatePaymentRequestNotified, updatePaymentRequestStatus } from 'src/firebase/actions' +import { CURRENCIES, CURRENCY_ENUM } from 'src/geth/consts' import { Namespaces } from 'src/i18n' -import { navigate } from 'src/navigator/NavigationService' -import { Screens } from 'src/navigator/Screens' -import NotificationAmount from 'src/paymentRequest/NotificationAmount' +import { unknownUserIcon } from 'src/images/Images' import { getRecipientThumbnail, Recipient } from 'src/recipients/recipient' -import { multiplyByWei } from 'src/utils/formatting' +import { getCentAwareMoneyDisplay } from 'src/utils/formatting' +import Logger from 'src/utils/Logger' interface OwnProps { requester: Recipient @@ -26,14 +25,16 @@ interface OwnProps { updatePaymentRequestNotified: typeof updatePaymentRequestNotified } +const AVATAR_SIZE = 40 + type Props = OwnProps & WithNamespaces export class OutgoingPaymentRequestListItem extends React.Component { onRemind = () => { - const { id } = this.props + const { id, t } = this.props this.props.updatePaymentRequestNotified(id.toString(), false) CeloAnalytics.track(CustomEventNames.outgoing_request_payment_remind) - this.onFinalized() + Logger.showMessage(t('sendFlow7:requestSent')) } onCancel = () => { @@ -42,18 +43,14 @@ export class OutgoingPaymentRequestListItem extends React.Component { CeloAnalytics.track(CustomEventNames.outgoing_request_payment_cancel) } - onFinalized = () => { - navigate(Screens.OutgoingPaymentRequestListScreen) - } - getCTA = () => { return [ { - text: this.props.t('remind'), + text: this.props.t('global:remind'), onPress: this.onRemind, }, { - text: this.props.t('cancel'), + text: this.props.t('global:cancel'), onPress: this.onCancel, }, ] @@ -64,42 +61,47 @@ export class OutgoingPaymentRequestListItem extends React.Component { } render() { - const { requester } = this.props + const { requester, t } = this.props return ( - - } - title={requester.displayName} - ctas={this.getCTA()} - roundedBorders={false} - callout={} - > - - {this.isDisplayingNumber() && ( - - {this.props.requester.displayId} - - )} - {this.props.comment} - - + + + + + } + title={t('outgoingPaymentRequestNotificationTitle', { + name: requester.displayName, + amount: + CURRENCIES[CURRENCY_ENUM.DOLLAR].symbol + getCentAwareMoneyDisplay(this.props.amount), + })} + ctas={this.getCTA()} + > + {this.props.comment || t('defaultComment')} + + ) } } const styles = StyleSheet.create({ - comment: { - paddingTop: variables.contentPadding, - }, phoneNumber: { color: colors.dark, }, + container: { + marginBottom: 16, + }, + unknownUser: { + height: AVATAR_SIZE, + width: AVATAR_SIZE, + justifyContent: 'center', + alignItems: 'center', + }, }) -export default withNamespaces(Namespaces.global)(OutgoingPaymentRequestListItem) +export default withNamespaces(Namespaces.paymentRequestFlow)(OutgoingPaymentRequestListItem) diff --git a/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListScreen.test.tsx b/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListScreen.test.tsx index 3fc89e90531..4816c99a04d 100644 --- a/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListScreen.test.tsx +++ b/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListScreen.test.tsx @@ -1,3 +1,4 @@ +import BigNumber from 'bignumber.js' import * as React from 'react' import 'react-native' import { Provider } from 'react-redux' @@ -5,8 +6,8 @@ import * as renderer from 'react-test-renderer' import { PaymentRequest } from 'src/account/types' import { paymentRequestDouble } from 'src/paymentRequest/__mocks__' import OutgoingPaymentRequestListScreen from 'src/paymentRequest/OutgoingPaymentRequestListScreen' -import { createMockStore } from 'test/utils' -import { mockAccount, mockE164Number } from 'test/values' +import { createMockNavigationProp, createMockStore } from 'test/utils' +import { mockAccount, mockE164Number, mockRecipient } from 'test/values' const requests = [ paymentRequestDouble({ @@ -29,6 +30,13 @@ const requests = [ }), ] +const navigation = createMockNavigationProp({ + recipient: mockRecipient, + recipientAddress: mockAccount, + amount: new BigNumber(10), + reason: 'My Reason', +}) + function testStore(outgoingPaymentRequests: PaymentRequest[]) { return createMockStore({ stableToken: { balance: '120' }, @@ -42,7 +50,7 @@ describe('OutgoingPaymentRequestListScreen', () => { const tree = renderer.create( - + ) expect(tree).toMatchSnapshot() @@ -53,7 +61,7 @@ describe('OutgoingPaymentRequestListScreen', () => { const tree = renderer.create( - + ) expect(tree).toMatchSnapshot() diff --git a/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListScreen.tsx b/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListScreen.tsx index 9a55da1d062..236d8356965 100644 --- a/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListScreen.tsx +++ b/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListScreen.tsx @@ -1,10 +1,7 @@ -import colors from '@celo/react-components/styles/colors' -import { componentStyles } from '@celo/react-components/styles/styles' -import variables from '@celo/react-components/styles/variables' -import * as React from 'react' +import React from 'react' import { WithNamespaces, withNamespaces } from 'react-i18next' -import { ScrollView, StyleSheet, View } from 'react-native' -import SafeAreaView from 'react-native-safe-area-view' +import { View } from 'react-native' +import { NavigationInjectedProps } from 'react-navigation' import { connect } from 'react-redux' import { getOutgoingPaymentRequests } from 'src/account/selectors' import { PaymentRequest } from 'src/account/types' @@ -12,17 +9,16 @@ import { updatePaymentRequestNotified, updatePaymentRequestStatus } from 'src/fi import i18n, { Namespaces } from 'src/i18n' import { fetchPhoneAddresses } from 'src/identity/actions' import { e164NumberToAddressSelector, E164NumberToAddressType } from 'src/identity/reducer' -import { headerWithBackButton } from 'src/navigator/Headers' +import { + NotificationList, + titleWithBalanceNavigationOptions, + useBalanceInNavigationParam, +} from 'src/notifications/NotificationList' import OutgoingPaymentRequestListItem from 'src/paymentRequest/OutgoingPaymentRequestListItem' -import PaymentRequestBalance from 'src/paymentRequest/PaymentRequestBalance' -import PaymentRequestListEmpty from 'src/paymentRequest/PaymentRequestListEmpty' import { getRecipientFromPaymentRequest } from 'src/paymentRequest/utils' import { NumberToRecipient } from 'src/recipients/recipient' import { recipientCacheSelector } from 'src/recipients/reducer' import { RootState } from 'src/redux/reducers' -import DisconnectBanner from 'src/shared/DisconnectBanner' - -const { contentPadding } = variables interface StateProps { dollarBalance: string | null @@ -44,66 +40,43 @@ const mapStateToProps = (state: RootState): StateProps => ({ recipientCache: recipientCacheSelector(state), }) -type Props = WithNamespaces & StateProps & DispatchProps - -export class OutgoingPaymentRequestListScreen extends React.Component { - static navigationOptions = () => ({ - ...headerWithBackButton, - headerTitle: i18n.t('paymentRequestFlow:outgoingPaymentRequests'), - }) - - renderRequest = (request: PaymentRequest, key: number, allRequests: PaymentRequest[]) => { - const { recipientCache } = this.props - const requester = getRecipientFromPaymentRequest(request, recipientCache) +type Props = NavigationInjectedProps & WithNamespaces & StateProps & DispatchProps - return ( - - - {key < allRequests.length - 1 && } - - ) - } +export const listItemRenderer = (params: { + recipientCache: NumberToRecipient + updatePaymentRequestStatus: typeof updatePaymentRequestStatus + updatePaymentRequestNotified: typeof updatePaymentRequestNotified +}) => (request: PaymentRequest, key: number | undefined = undefined) => { + const requester = getRecipientFromPaymentRequest(request, params.recipientCache) + return ( + + + + ) +} - render() { - return ( - - - - {this.props.paymentRequests.length > 0 ? ( - - - {this.props.paymentRequests.map(this.renderRequest)} - - - ) : ( - - )} - - ) - } +const OutgoingPaymentRequestListScreen = (props: Props) => { + const { dollarBalance, navigation } = props + useBalanceInNavigationParam(dollarBalance, navigation) + return ( + + ) } -const styles = StyleSheet.create({ - container: { - backgroundColor: colors.background, - flex: 1, - }, - separator: { - borderBottomColor: colors.darkLightest, - borderBottomWidth: 1, - marginLeft: 50, - }, - scrollArea: { - margin: contentPadding, - }, -}) +OutgoingPaymentRequestListScreen.navigationOptions = titleWithBalanceNavigationOptions( + i18n.t('walletFlow5:outgoingPaymentRequests') +) export default connect( mapStateToProps, diff --git a/packages/mobile/src/notifications/OutgoingPaymentRequestSummaryNotification.test.tsx b/packages/mobile/src/paymentRequest/OutgoingPaymentRequestSummaryNotification.test.tsx similarity index 95% rename from packages/mobile/src/notifications/OutgoingPaymentRequestSummaryNotification.test.tsx rename to packages/mobile/src/paymentRequest/OutgoingPaymentRequestSummaryNotification.test.tsx index 69a70a2d610..86eae3550f6 100644 --- a/packages/mobile/src/notifications/OutgoingPaymentRequestSummaryNotification.test.tsx +++ b/packages/mobile/src/paymentRequest/OutgoingPaymentRequestSummaryNotification.test.tsx @@ -4,7 +4,7 @@ import { Provider } from 'react-redux' import * as renderer from 'react-test-renderer' import { PaymentRequestStatus } from 'src/account/types' import { SHORT_CURRENCIES } from 'src/geth/consts' -import OutgoingPaymentRequestSummaryNotification from 'src/notifications/OutgoingPaymentRequestSummaryNotification' +import OutgoingPaymentRequestSummaryNotification from 'src/paymentRequest/OutgoingPaymentRequestSummaryNotification' import { createMockStore } from 'test/utils' const requesterE164Number = '+491522345678' diff --git a/packages/mobile/src/paymentRequest/OutgoingPaymentRequestSummaryNotification.tsx b/packages/mobile/src/paymentRequest/OutgoingPaymentRequestSummaryNotification.tsx new file mode 100644 index 00000000000..12eef3f50d8 --- /dev/null +++ b/packages/mobile/src/paymentRequest/OutgoingPaymentRequestSummaryNotification.tsx @@ -0,0 +1,108 @@ +import * as React from 'react' +import { WithNamespaces, withNamespaces } from 'react-i18next' +import { Image, StyleSheet } from 'react-native' +import { connect } from 'react-redux' +import { PaymentRequest } from 'src/account/types' +import CeloAnalytics from 'src/analytics/CeloAnalytics' +import { CustomEventNames } from 'src/analytics/constants' +import { updatePaymentRequestNotified, updatePaymentRequestStatus } from 'src/firebase/actions' +import { Namespaces } from 'src/i18n' +import { + addressToE164NumberSelector, + AddressToE164NumberType, + e164NumberToAddressSelector, + E164NumberToAddressType, +} from 'src/identity/reducer' +import { sendDollar } from 'src/images/Images' +import { navigate } from 'src/navigator/NavigationService' +import { Stacks } from 'src/navigator/Screens' +import SummaryNotification from 'src/notifications/SummaryNotification' +import { listItemRenderer } from 'src/paymentRequest/OutgoingPaymentRequestListScreen' +import PaymentRequestNotificationInner from 'src/paymentRequest/PaymentRequestNotificationInner' +import { NumberToRecipient, phoneNumberToRecipient } from 'src/recipients/recipient' +import { recipientCacheSelector } from 'src/recipients/reducer' +import { RootState } from 'src/redux/reducers' + +interface OwnProps { + requests: PaymentRequest[] +} + +interface DispatchProps { + updatePaymentRequestStatus: typeof updatePaymentRequestStatus + updatePaymentRequestNotified: typeof updatePaymentRequestNotified +} + +type Props = OwnProps & DispatchProps & WithNamespaces & StateProps + +interface StateProps { + e164PhoneNumberAddressMapping: E164NumberToAddressType + addressToE164Number: AddressToE164NumberType + recipientCache: NumberToRecipient +} + +const mapStateToProps = (state: RootState): StateProps => ({ + e164PhoneNumberAddressMapping: e164NumberToAddressSelector(state), + addressToE164Number: addressToE164NumberSelector(state), + recipientCache: recipientCacheSelector(state), +}) + +// Payment Request notification for the notification center on home screen +export class OutgoingPaymentRequestSummaryNotification extends React.Component { + onReview = () => { + CeloAnalytics.track(CustomEventNames.outgoing_request_payment_review) + navigate(Stacks.OutgoingRequestStack) + } + + getRequesterRecipient = (requesterE164Number: string) => { + return phoneNumberToRecipient( + requesterE164Number, + this.props.e164PhoneNumberAddressMapping[requesterE164Number], + this.props.recipientCache + ) + } + + itemRenderer = (item: PaymentRequest) => { + return ( + + ) + } + + render() { + const { recipientCache, requests, t } = this.props + return requests.length === 1 ? ( + listItemRenderer({ + updatePaymentRequestStatus: this.props.updatePaymentRequestStatus, + updatePaymentRequestNotified: this.props.updatePaymentRequestNotified, + recipientCache, + })(requests[0]) + ) : ( + + items={requests} + title={t('outgoingPaymentRequests')} + icon={} + onReview={this.onReview} + itemRenderer={this.itemRenderer} + /> + ) + } +} + +const styles = StyleSheet.create({ + image: { + width: 40, + height: 40, + }, +}) + +export default connect( + mapStateToProps, + { + updatePaymentRequestStatus, + updatePaymentRequestNotified, + } +)(withNamespaces(Namespaces.walletFlow5)(OutgoingPaymentRequestSummaryNotification)) diff --git a/packages/mobile/src/paymentRequest/PaymentRequestBalance.tsx b/packages/mobile/src/paymentRequest/PaymentRequestBalance.tsx deleted file mode 100644 index 1ec709a2e06..00000000000 --- a/packages/mobile/src/paymentRequest/PaymentRequestBalance.tsx +++ /dev/null @@ -1,54 +0,0 @@ -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 BigNumber from 'bignumber.js' -import * as React from 'react' -import { WithNamespaces, withNamespaces } from 'react-i18next' -import { StyleSheet, Text, View } from 'react-native' -import { CURRENCIES, CURRENCY_ENUM } from 'src/geth/consts' -import { Namespaces } from 'src/i18n' -import CeloAccountIcon from 'src/icons/CeloAccountIcon' -import { getCentAwareMoneyDisplay } from 'src/utils/formatting' - -const { contentPadding } = variables - -interface Props { - dollarBalance: BigNumber | string | null -} - -class PaymentRequestBalance extends React.PureComponent { - render() { - return ( - - - - {this.props.t('celoDollarBalance')} - - {CURRENCIES[CURRENCY_ENUM.DOLLAR].symbol + - getCentAwareMoneyDisplay(this.props.dollarBalance || 0)} - - - - ) - } -} - -const styles = StyleSheet.create({ - balanceContainer: { - flexDirection: 'row', - alignItems: 'center', - padding: contentPadding, - flex: 0, - }, - balance: { - paddingStart: contentPadding, - flexDirection: 'row', - justifyContent: 'space-between', - flex: 1, - }, - balanceText: { - ...fontStyles.bodySmallSemiBold, - color: colors.celoGreen, - }, -}) -export default withNamespaces(Namespaces.paymentRequestFlow)(PaymentRequestBalance) diff --git a/packages/mobile/src/paymentRequest/PaymentRequestListEmpty.tsx b/packages/mobile/src/paymentRequest/PaymentRequestListEmpty.tsx deleted file mode 100644 index ffcd1e14562..00000000000 --- a/packages/mobile/src/paymentRequest/PaymentRequestListEmpty.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import fontStyles from '@celo/react-components/styles/fonts' -import * as React from 'react' -import { WithNamespaces, withNamespaces } from 'react-i18next' -import { StyleSheet, Text } from 'react-native' -import { Namespaces } from 'src/i18n' - -function PaymentRequestListEmpty(props: WithNamespaces) { - return {props.t('empty')} -} - -const styles = StyleSheet.create({ - empty: { - textAlign: 'center', - marginTop: 30, - }, -}) -export default withNamespaces(Namespaces.paymentRequestFlow)(PaymentRequestListEmpty) diff --git a/packages/mobile/src/paymentRequest/PaymentRequestNotificationInner.test.tsx b/packages/mobile/src/paymentRequest/PaymentRequestNotificationInner.test.tsx index dc65638cd21..4587cce8d57 100644 --- a/packages/mobile/src/paymentRequest/PaymentRequestNotificationInner.test.tsx +++ b/packages/mobile/src/paymentRequest/PaymentRequestNotificationInner.test.tsx @@ -8,7 +8,6 @@ it('renders correctly', () => { ) diff --git a/packages/mobile/src/paymentRequest/PaymentRequestNotificationInner.tsx b/packages/mobile/src/paymentRequest/PaymentRequestNotificationInner.tsx index a209bc9536b..9bffff77ca6 100644 --- a/packages/mobile/src/paymentRequest/PaymentRequestNotificationInner.tsx +++ b/packages/mobile/src/paymentRequest/PaymentRequestNotificationInner.tsx @@ -1,27 +1,33 @@ import fontStyles from '@celo/react-components/styles/fonts' import * as React from 'react' +import { Trans, withNamespaces, WithNamespaces } from 'react-i18next' import { StyleSheet, Text } from 'react-native' import { CURRENCIES, CURRENCY_ENUM } from 'src/geth/consts' +import { Namespaces } from 'src/i18n' import { Recipient } from 'src/recipients/recipient' import { getCentAwareMoneyDisplay } from 'src/utils/formatting' interface Props { requesterE164Number: string - comment: string amount: string requesterRecipient: Recipient | null } -export default function PaymentRequestNotificationInner(props: Props) { - const { requesterE164Number, comment: message, amount, requesterRecipient } = props +function PaymentRequestNotificationInner(props: Props & WithNamespaces) { + const { requesterE164Number, amount, requesterRecipient } = props + const displayName = (requesterRecipient && requesterRecipient.displayName) || requesterE164Number return ( - - {(requesterRecipient && requesterRecipient.displayName) || requesterE164Number} - {message} - - - {' ' + CURRENCIES[CURRENCY_ENUM.DOLLAR].symbol + getCentAwareMoneyDisplay(amount)} - + + {{ displayName }} for + {{ amount }} + ) } @@ -31,3 +37,5 @@ const styles = StyleSheet.create({ flexDirection: 'row', }, }) + +export default withNamespaces(Namespaces.paymentRequestFlow)(PaymentRequestNotificationInner) diff --git a/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestListItem.test.tsx.snap b/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestListItem.test.tsx.snap index f7a8102a3e9..d7bd9e5d8f0 100644 --- a/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestListItem.test.tsx.snap +++ b/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestListItem.test.tsx.snap @@ -2,54 +2,59 @@ exports[`IncomingPaymentRequestListItem renders correctly 1`] = ` - - 5 - + + - - - - 5126608970 - - + + incomingPaymentRequestNotificationTitle + - - - Hey thanks for the loan, Ill pay you back ASAP. LOVE YOU - - + } + > + Hey thanks for the loan, Ill pay you back ASAP. LOVE YOU + @@ -176,7 +185,7 @@ exports[`IncomingPaymentRequestListItem renders correctly 1`] = ` ] } > - pay + global:pay - decline + global:decline - - - - $24 - - - `; diff --git a/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestListScreen.test.tsx.snap b/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestListScreen.test.tsx.snap index 679bac25aa9..e4b4832edd9 100644 --- a/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestListScreen.test.tsx.snap +++ b/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestListScreen.test.tsx.snap @@ -9,66 +9,6 @@ exports[`IncomingPaymentRequestListScreen renders correctly with no requests 1`] } } > - - - - - celoDollarBalance - - - $120 - - - - empty + global:emptyList `; @@ -99,87 +39,32 @@ exports[`IncomingPaymentRequestListScreen renders correctly with requests 1`] = } } > - - - - - celoDollarBalance - - - $120 - - - - + @@ -210,7 +100,6 @@ exports[`IncomingPaymentRequestListScreen renders correctly with requests 1`] = style={ Object { "alignItems": "center", - "flexDirection": "column", "paddingRight": 16, } } @@ -236,29 +125,28 @@ exports[`IncomingPaymentRequestListScreen renders correctly with requests 1`] = }, Object { "backgroundColor": "#7AD6FE", - "borderRadius": 15, - "height": 30, - "width": 30, + "borderRadius": 20, + "height": 40, + "width": 40, }, ] } > - - + - + /> @@ -273,186 +161,139 @@ exports[`IncomingPaymentRequestListScreen renders correctly with requests 1`] = - +1555-867-5309 + incomingPaymentRequestNotificationTitle - + + + Just the best + - + + > + global:pay + + + - Just the best + global:decline - - - - pay - - - - - decline - - - - - - - $20 - - - - - + @@ -483,7 +329,6 @@ exports[`IncomingPaymentRequestListScreen renders correctly with requests 1`] = style={ Object { "alignItems": "center", - "flexDirection": "column", "paddingRight": 16, } } @@ -509,29 +354,28 @@ exports[`IncomingPaymentRequestListScreen renders correctly with requests 1`] = }, Object { "backgroundColor": "#7AD6FE", - "borderRadius": 15, - "height": 30, - "width": 30, + "borderRadius": 20, + "height": 40, + "width": 40, }, ] } > - - + - + /> @@ -546,186 +390,139 @@ exports[`IncomingPaymentRequestListScreen renders correctly with requests 1`] = - +14155550000 + incomingPaymentRequestNotificationTitle - + + + Just the best for the best. Thanos & Zeus Gods of ultimate Power + - + + > + global:pay + + + - Just the best for the best. Thanos & Zeus Gods of ultimate Power + global:decline - - - - pay - - - - - decline - - - - - - - $102 - - - - - + @@ -756,7 +558,6 @@ exports[`IncomingPaymentRequestListScreen renders correctly with requests 1`] = style={ Object { "alignItems": "center", - "flexDirection": "column", "paddingRight": 16, } } @@ -782,29 +583,28 @@ exports[`IncomingPaymentRequestListScreen renders correctly with requests 1`] = }, Object { "backgroundColor": "#7AD6FE", - "borderRadius": 15, - "height": 30, - "width": 30, + "borderRadius": 20, + "height": 40, + "width": 40, }, ] } > - - + - + /> @@ -819,169 +619,119 @@ exports[`IncomingPaymentRequestListScreen renders correctly with requests 1`] = - +14155550000 + incomingPaymentRequestNotificationTitle - + + + Just the best but less + - + + > + global:pay + + + - Just the best but less + global:decline - - - - pay - - - - - decline - - - - - - - $1 - - - diff --git a/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestSummaryNotification.test.tsx.snap b/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestSummaryNotification.test.tsx.snap new file mode 100644 index 00000000000..1bbe103e03d --- /dev/null +++ b/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestSummaryNotification.test.tsx.snap @@ -0,0 +1,631 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IncomingPaymentRequestSummaryNotification renders correctly 1`] = ` + + + + + + + + incomingPaymentRequests + + + + + + + +491522345678 + for + + + 200000.00 + + + + + global:moreWithCount + + + + + + + + walletFlow5:review + + + + + + + +`; + +exports[`IncomingPaymentRequestSummaryNotification when more 1 requests renders just it alone 1`] = ` + + + + + + + + + + + + + incomingPaymentRequestNotificationTitle + + + + Dinner for me and the gals, PIZZAA! + + + + + global:pay + + + + + global:decline + + + + + + + + +`; + +exports[`IncomingPaymentRequestSummaryNotification when more than 2 requests renders just two 1`] = ` + + + + + + + + incomingPaymentRequests + + + + + + + +491522345678 + for + + + 200000.00 + + + + + global:moreWithCount + + + + + + + + walletFlow5:review + + + + + + + +`; diff --git a/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestListItem.test.tsx.snap b/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestListItem.test.tsx.snap index 3bae290eae1..98b9287e8a0 100644 --- a/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestListItem.test.tsx.snap +++ b/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestListItem.test.tsx.snap @@ -3,231 +3,214 @@ exports[`OutgoingPaymentRequestListItem renders correctly 1`] = ` - + - - 5 - - - - - - - 5126608970 - - - - - - Hey thanks for the loan, Ill pay you back ASAP. LOVE YOU - + + + + + + outgoingPaymentRequestNotificationTitle + - - - remind - - + Hey thanks for the loan, Ill pay you back ASAP. LOVE YOU + - - cancel - + + global:remind + + + + + global:cancel + + - - - - $24 - - - `; diff --git a/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestListScreen.test.tsx.snap b/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestListScreen.test.tsx.snap index 1136356eb0b..28b2a269dc6 100644 --- a/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestListScreen.test.tsx.snap +++ b/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestListScreen.test.tsx.snap @@ -9,66 +9,6 @@ exports[`OutgoingPaymentRequestListScreen renders correctly with no requests 1`] } } > - - - - - celoDollarBalance - - - $120 - - - - empty + global:emptyList `; @@ -99,836 +39,655 @@ exports[`OutgoingPaymentRequestListScreen renders correctly with requests 1`] = } } > - - - - - celoDollarBalance - - - $120 - - - - + - - + - - - - - - - +1555-867-5309 - - - - - - + - Just the best - + "alignItems": "center", + "height": 40, + "justifyContent": "center", + "width": 40, + } + } + /> + + + + + outgoingPaymentRequestNotificationTitle + - - - remind - - + Just the best + - + + global:remind + + + - cancel - + + global:cancel + + - - - - $20 - - - - - + - - + - - - - - - - +14155550000 - - - - - - + - Just the best for the best. Thanos & Zeus Gods of ultimate Power - + "alignItems": "center", + "height": 40, + "justifyContent": "center", + "width": 40, + } + } + /> + + + + + outgoingPaymentRequestNotificationTitle + - - - remind - - + Just the best for the best. Thanos & Zeus Gods of ultimate Power + - + + global:remind + + + - cancel - + + global:cancel + + - - - - $102 - - - - - + - - + - - - - - - - +14155550000 - - - - - - + - Just the best but less - + "alignItems": "center", + "height": 40, + "justifyContent": "center", + "width": 40, + } + } + /> + + + + + outgoingPaymentRequestNotificationTitle + - - - remind - - + Just the best but less + - - cancel - + + global:remind + + + + + global:cancel + + - - - - $1 - - - diff --git a/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestSummaryNotification.test.tsx.snap b/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestSummaryNotification.test.tsx.snap new file mode 100644 index 00000000000..c24102a55ec --- /dev/null +++ b/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestSummaryNotification.test.tsx.snap @@ -0,0 +1,616 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OutgoingPaymentRequestSummaryNotification renders correctly 1`] = ` + + + + + + + + outgoingPaymentRequests + + + + + + + +491522345678 + for + + + 200000.00 + + + + + global:moreWithCount + + + + + + + + walletFlow5:review + + + + + + + +`; + +exports[`OutgoingPaymentRequestSummaryNotification when more 1 requests renders just it alone 1`] = ` + + + + + + + + + + + + + + outgoingPaymentRequestNotificationTitle + + + + Dinner for me and the gals, PIZZAA! + + + + + global:remind + + + + + global:cancel + + + + + + + + + +`; + +exports[`OutgoingPaymentRequestSummaryNotification when more than 2 requests renders just two 1`] = ` + + + + + + + + outgoingPaymentRequests + + + + + + + +491522345678 + for + + + 200000.00 + + + + + global:moreWithCount + + + + + + + + walletFlow5:review + + + + + + + +`; diff --git a/packages/mobile/src/paymentRequest/__snapshots__/PaymentRequestNotificationInner.test.tsx.snap b/packages/mobile/src/paymentRequest/__snapshots__/PaymentRequestNotificationInner.test.tsx.snap index 460680a6dab..b12b5f98459 100644 --- a/packages/mobile/src/paymentRequest/__snapshots__/PaymentRequestNotificationInner.test.tsx.snap +++ b/packages/mobile/src/paymentRequest/__snapshots__/PaymentRequestNotificationInner.test.tsx.snap @@ -12,19 +12,16 @@ exports[`renders correctly 1`] = ` > +14155552671 - - - Hey thanks for the loan, Ill pay you back ASAP. LOVE YOU + for - $24 + 24 `; diff --git a/packages/mobile/src/send/Fee.tsx b/packages/mobile/src/send/Fee.tsx index 5d5f2f25216..38df6188d2d 100644 --- a/packages/mobile/src/send/Fee.tsx +++ b/packages/mobile/src/send/Fee.tsx @@ -3,6 +3,7 @@ import BigNumber from 'bignumber.js' import * as React from 'react' import { withNamespaces, WithNamespaces } from 'react-i18next' import { StyleSheet, Text, View } from 'react-native' +import { Namespaces } from 'src/i18n' import { getMoneyDisplayValue } from 'src/utils/formatting' interface Props { @@ -26,4 +27,4 @@ const style = StyleSheet.create({ }, }) -export default withNamespaces('sendFlow7')(Fee) +export default withNamespaces(Namespaces.sendFlow7)(Fee) diff --git a/packages/mobile/src/send/FeeEducation.tsx b/packages/mobile/src/send/FeeEducation.tsx index 83538daf4ea..59db6b581b7 100644 --- a/packages/mobile/src/send/FeeEducation.tsx +++ b/packages/mobile/src/send/FeeEducation.tsx @@ -5,6 +5,7 @@ import * as React from 'react' import { withNamespaces, WithNamespaces } from 'react-i18next' import { Image, StyleSheet, Text, View } from 'react-native' import componentWithAnalytics from 'src/analytics/wrapper' +import { Namespaces } from 'src/i18n' import { sendFee } from 'src/images/Images' import { navigateBack } from 'src/navigator/NavigationService' @@ -55,4 +56,4 @@ const styles = StyleSheet.create({ }, }) -export default componentWithAnalytics(withNamespaces('sendFlow7')(FeeEducation)) +export default componentWithAnalytics(withNamespaces(Namespaces.sendFlow7)(FeeEducation)) diff --git a/packages/mobile/src/send/SendAmount.tsx b/packages/mobile/src/send/SendAmount.tsx index b5ad0af8fae..e4dcca3ad7b 100644 --- a/packages/mobile/src/send/SendAmount.tsx +++ b/packages/mobile/src/send/SendAmount.tsx @@ -404,7 +404,7 @@ export class SendAmount extends React.Component { lng={this.props.lng} /> { if (confirmationInput === '') { throw new Error('Confirmation input missing') } + confirmationInput.amount = new BigNumber(confirmationInput.amount) return confirmationInput } @@ -128,9 +129,8 @@ class SendConfirmation extends React.Component { const { onCancel } = this.getNavParams() if (onCancel) { onCancel() - } else { - navigateBack() } + navigateBack() } renderHeader = () => { diff --git a/packages/mobile/src/send/__snapshots__/SendAmount.test.tsx.snap b/packages/mobile/src/send/__snapshots__/SendAmount.test.tsx.snap index 5083af6303d..1b10e9f3866 100644 --- a/packages/mobile/src/send/__snapshots__/SendAmount.test.tsx.snap +++ b/packages/mobile/src/send/__snapshots__/SendAmount.test.tsx.snap @@ -327,7 +327,7 @@ exports[`SendAmount renders correctly for request payment confirmation 1`] = ` ] } > - for + global:for diff --git a/packages/mobile/src/send/saga.ts b/packages/mobile/src/send/saga.ts index 70ed086f169..a7e600d3f6c 100644 --- a/packages/mobile/src/send/saga.ts +++ b/packages/mobile/src/send/saga.ts @@ -42,7 +42,7 @@ export async function getSendTxGas( const tokenContract = await contractGetter(web3) const txParams = { from: account, feeCurrency: tokenContract._address } const gas = new BigNumber(await tx.estimateGas(txParams)) - Logger.debug(`${TAG}/getSendTxGas`, `Estimated gas of ${gas.toString()}}`) + Logger.debug(`${TAG}/getSendTxGas`, `Estimated gas of ${gas.toString()}`) return gas } diff --git a/packages/mobile/src/set-clock/SetClock.tsx b/packages/mobile/src/set-clock/SetClock.tsx index 4a3bf711852..910df788fa7 100644 --- a/packages/mobile/src/set-clock/SetClock.tsx +++ b/packages/mobile/src/set-clock/SetClock.tsx @@ -5,18 +5,25 @@ 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 { Image, StyleSheet, Text, View } from 'react-native' +import { Image, Platform, StyleSheet, Text, View } from 'react-native' import * as AndroidOpenSettings from 'react-native-android-open-settings' import { componentWithAnalytics } from 'src/analytics/wrapper' import { Namespaces } from 'src/i18n' import clockIcon from 'src/images/clock-icon.png' +import { navigate } from 'src/navigator/NavigationService' +import { Screens } from 'src/navigator/Screens' import { getLocalTimezone, getRemoteTime } from 'src/utils/time' export class SetClock extends React.Component { static navigationOptions = { header: null } goToSettings = () => { - return AndroidOpenSettings.dateSettings() + if (Platform.OS === 'android') { + return AndroidOpenSettings.dateSettings() + } else { + // TODO: Implement Date Setting on iOS + navigate(Screens.WalletHome) + } } render() { diff --git a/packages/mobile/src/transactions/NoActivity.tsx b/packages/mobile/src/transactions/NoActivity.tsx index 77a1be54c1b..c2f86823493 100644 --- a/packages/mobile/src/transactions/NoActivity.tsx +++ b/packages/mobile/src/transactions/NoActivity.tsx @@ -5,6 +5,7 @@ import { ApolloError } from 'apollo-boost' import * as React from 'react' import { withNamespaces, WithNamespaces } from 'react-i18next' import { ActivityIndicator, Image, StyleSheet, Text, View } from 'react-native' +import { Namespaces } from 'src/i18n' import { exchangeIcon, shinyDollar } from 'src/images/Images' import { navigate } from 'src/navigator/NavigationService' import { Stacks } from 'src/navigator/Screens' @@ -94,4 +95,4 @@ const styles = StyleSheet.create({ }, }) -export default withNamespaces('walletFlow5')(NoActivity) +export default withNamespaces(Namespaces.walletFlow5)(NoActivity) diff --git a/packages/mobile/src/transactions/TransferFeedIcon.tsx b/packages/mobile/src/transactions/TransferFeedIcon.tsx index 433b4a6b42c..e9b7a9cd4fe 100644 --- a/packages/mobile/src/transactions/TransferFeedIcon.tsx +++ b/packages/mobile/src/transactions/TransferFeedIcon.tsx @@ -6,7 +6,7 @@ import { coinsIcon, unknownUserIcon } from 'src/images/Images' import { getRecipientThumbnail, Recipient } from 'src/recipients/recipient' import { TransactionTypes } from 'src/transactions/reducer' -const avatarSize = 40 +const AVATAR_SIZE = 40 interface Props { type: TransactionTypes @@ -40,7 +40,7 @@ export default function TransferFeedIcon(props: Props) { return ( {} @@ -52,8 +52,8 @@ export default function TransferFeedIcon(props: Props) { const styles = StyleSheet.create({ image: { - height: avatarSize, - width: avatarSize, + height: AVATAR_SIZE, + width: AVATAR_SIZE, justifyContent: 'center', alignItems: 'center', }, diff --git a/packages/mobile/src/web3/gas.ts b/packages/mobile/src/web3/gas.ts index d0494315078..a14605a4a14 100644 --- a/packages/mobile/src/web3/gas.ts +++ b/packages/mobile/src/web3/gas.ts @@ -11,7 +11,7 @@ let gasPrice: BigNumber | null = null let gasPriceLastUpdated: number | null = null export async function getGasPrice(currency: CURRENCY_ENUM = CURRENCY_ENUM.DOLLAR) { - Logger.debug(`${TAG}}/getGasPrice`, 'Getting gas price') + Logger.debug(`${TAG}/getGasPrice`, 'Getting gas price') try { if ( @@ -24,7 +24,7 @@ export async function getGasPrice(currency: CURRENCY_ENUM = CURRENCY_ENUM.DOLLAR } return gasPrice } catch (error) { - Logger.error(`${TAG}}/getGasPrice`, 'Could not fetch and update gas price.', error) + Logger.error(`${TAG}/getGasPrice`, 'Could not fetch and update gas price.', error) throw new Error('Error fetching gas price') } } diff --git a/packages/mobile/test/values.ts b/packages/mobile/test/values.ts index bf1f742b68d..a25fe41231b 100644 --- a/packages/mobile/test/values.ts +++ b/packages/mobile/test/values.ts @@ -160,4 +160,17 @@ export const mockPaymentRequests: PaymentRequest[] = [ notified: true, type: NotificationTypes.PAYMENT_REQUESTED, }, + { + uid: 'fas12fbs4fa141241', + amount: '12.34', + timestamp: new Date('2019-06-04T16:17:55.239Z'), + requesterAddress: mockAccount2, + requesterE164Number: mockE164Number, + requesteeAddress: mockAccount, + currency: SHORT_CURRENCIES.DOLLAR, + comment: mockComment, + status: PaymentRequestStatus.REQUESTED, + notified: true, + type: NotificationTypes.PAYMENT_REQUESTED, + }, ] diff --git a/packages/react-components/components/BaseListItem.tsx b/packages/react-components/components/BaseListItem.tsx deleted file mode 100644 index 505dcd53f37..00000000000 --- a/packages/react-components/components/BaseListItem.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import fontStyles from '@celo/react-components/styles/fonts' -import { componentStyles } from '@celo/react-components/styles/styles' -import variables from '@celo/react-components/styles/variables' -import * as React from 'react' -import { StyleSheet, Text, View } from 'react-native' - -const { contentPadding } = variables - -interface Props { - icon?: React.ReactNode - title: string - children: React.ReactNode - callout?: React.ReactNode - roundedBorders?: boolean -} - -// just used by notifications for now, useful for out pattern of [icon | title / body | optionalCol] -export default function BaseListItem({ icon, title, children, callout, roundedBorders }: Props) { - return ( - - {icon && {icon}} - - {title} - {children} - - {callout && {callout}} - - ) -} - -const styles = StyleSheet.create({ - container: { - padding: contentPadding, - flexDirection: 'row', - justifyContent: 'space-between', - width: '100%', - }, - iconArea: { - paddingRight: contentPadding, - flexDirection: 'column', - alignItems: 'center', - }, - contentArea: { - justifyContent: 'space-between', - flex: 1, - }, - callout: { - paddingLeft: contentPadding, - }, -}) diff --git a/packages/react-components/components/BaseNotification.tsx b/packages/react-components/components/BaseNotification.tsx index 44312409fac..e158571a6ee 100644 --- a/packages/react-components/components/BaseNotification.tsx +++ b/packages/react-components/components/BaseNotification.tsx @@ -1,15 +1,20 @@ -import BaseListItem from '@celo/react-components/components/BaseListItem' import TextButton from '@celo/react-components/components/TextButton' +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 { elevationShadowStyle } from '@celo/react-components/styles/styles' +import variables from '@celo/react-components/styles/variables' import * as React from 'react' -import { StyleSheet, View } from 'react-native' +import { StyleSheet, Text, View } from 'react-native' + +const { contentPadding } = variables interface Props { icon?: React.ReactNode title: string children: React.ReactNode - callout?: React.ReactNode ctas: CTA[] - roundedBorders?: boolean + onPress?: () => unknown } export interface CTA { @@ -17,36 +22,59 @@ export interface CTA { text: string } -// For use in Notification Center and Payment Request Screen -export default function BaseNotification(props: Props) { +function Wrapper({ onPress, children }: { children: React.ReactNode; onPress?: () => unknown }) { + return onPress ? {children} : {children} +} + +export default function BaseNotification({ icon, title, children, ctas, onPress }: Props) { return ( - - - {props.children} - - {props.ctas.map((cta, j) => { - return ( - - {cta.text} - - ) - })} + + + {icon && {icon}} + + {title} + + {children} + + {ctas.map((cta, j) => ( + + {cta.text} + + ))} + + - + ) } const styles = StyleSheet.create({ ctas: { flexDirection: 'row', - marginTop: 15, + marginTop: 5, }, action: { paddingEnd: 15, }, body: { - minHeight: 70, + paddingTop: 8, + minHeight: 60, + justifyContent: 'space-between', + }, + container: { + padding: contentPadding, + flexDirection: 'row', + justifyContent: 'space-between', + width: '100%', + backgroundColor: colors.background, + }, + iconArea: { + paddingRight: contentPadding, + alignItems: 'center', + }, + contentArea: { justifyContent: 'space-between', + flex: 1, }, }) diff --git a/packages/react-components/components/SummaryNotification.test.tsx b/packages/react-components/components/SummaryNotification.test.tsx new file mode 100644 index 00000000000..65a3f7acf1f --- /dev/null +++ b/packages/react-components/components/SummaryNotification.test.tsx @@ -0,0 +1,40 @@ +import SummaryNotification from '@celo/react-components/components/SummaryNotification' +import { shallow } from 'enzyme' +import * as React from 'react' +import { Text } from 'react-native' +import * as renderer from 'react-test-renderer' + +const props = (onPress = jest.fn()) => ({ + text: 'Gold is where you can choose to store Celo dollars you have', + image: '', + title: 'Test', + reviewCTA: { text: 'it goes boom', onPress }, +}) + +describe(SummaryNotification, () => { + it('renders correctly', () => { + const tree = renderer.create( + + Test + + ) + expect(tree).toMatchSnapshot() + }) + describe('when ctas are pressed', () => { + it('calls the on press function', () => { + const clickHandler = jest.fn() + + const wrapper = shallow( + + Test + + ) + wrapper + .find('TextButton') + .first() + .simulate('press') + + expect(clickHandler).toHaveBeenCalled() + }) + }) +}) diff --git a/packages/react-components/components/SummaryNotification.tsx b/packages/react-components/components/SummaryNotification.tsx new file mode 100644 index 00000000000..8ac1fa64e0d --- /dev/null +++ b/packages/react-components/components/SummaryNotification.tsx @@ -0,0 +1,81 @@ +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 { elevationShadowStyle } from '@celo/react-components/styles/styles' +import variables from '@celo/react-components/styles/variables' +import * as React from 'react' +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native' + +const { contentPadding } = variables + +interface Props { + icon?: React.ReactNode + title: string + children: React.ReactNode + reviewCTA: CTA + onPress?: () => unknown +} + +export interface CTA { + onPress: () => unknown + text: string +} + +function Wrapper({ onPress, children }: { children: React.ReactNode; onPress?: () => unknown }) { + return onPress ? ( + {children} + ) : ( + {children} + ) +} + +export default function SummaryNotification({ icon, title, children, reviewCTA, onPress }: Props) { + return ( + + + {icon && {icon}} + + {title} + + {children} + + + {reviewCTA.text} + + + + + + + ) +} + +const styles = StyleSheet.create({ + ctas: { + flexDirection: 'row', + marginTop: 5, + }, + action: { + paddingEnd: 15, + }, + body: { + minHeight: 60, + justifyContent: 'space-between', + }, + container: { + padding: contentPadding, + flexDirection: 'row', + justifyContent: 'space-between', + width: '100%', + backgroundColor: colors.background, + }, + iconArea: { + paddingRight: contentPadding, + flexDirection: 'column', + alignItems: 'center', + }, + contentArea: { + justifyContent: 'space-between', + flex: 1, + }, +}) diff --git a/packages/react-components/components/__snapshots__/BaseNotification.test.tsx.snap b/packages/react-components/components/__snapshots__/BaseNotification.test.tsx.snap index d9d0c4ce4d9..06566f1fa26 100644 --- a/packages/react-components/components/__snapshots__/BaseNotification.test.tsx.snap +++ b/packages/react-components/components/__snapshots__/BaseNotification.test.tsx.snap @@ -1,45 +1,56 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`BaseNotification renders correctly 1`] = ` - + - - Test - - + + Test + @@ -50,7 +61,7 @@ exports[`BaseNotification renders correctly 1`] = ` style={ Object { "flexDirection": "row", - "marginTop": 15, + "marginTop": 5, } } > diff --git a/packages/react-components/components/__snapshots__/SummaryNotification.test.tsx.snap b/packages/react-components/components/__snapshots__/SummaryNotification.test.tsx.snap new file mode 100644 index 00000000000..12e835415ed --- /dev/null +++ b/packages/react-components/components/__snapshots__/SummaryNotification.test.tsx.snap @@ -0,0 +1,108 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SummaryNotification renders correctly 1`] = ` + + + + + Test + + + + Test + + + + + it goes boom + + + + + + + +`; diff --git a/packages/react-components/styles/fonts.tsx b/packages/react-components/styles/fonts.tsx index ad603c51f15..7de93d8052c 100644 --- a/packages/react-components/styles/fonts.tsx +++ b/packages/react-components/styles/fonts.tsx @@ -82,6 +82,12 @@ export const fontStyles = StyleSheet.create({ fontFamily: HindSilguri.Regular, color: colors.dark, }, + bodySmallSecondary: { + fontSize: 14, + lineHeight: 18, + fontFamily: HindSilguri.Regular, + color: colors.darkSecondary, + }, bodySmallBold: { fontSize: 14, lineHeight: 18, @@ -158,7 +164,7 @@ export const fontStyles = StyleSheet.create({ }, headerTitle: { fontSize: 14, - fontFamily: HindSilguri.Medium, + fontFamily: HindSilguri.Bold, color: colors.dark, }, headerButton: { diff --git a/packages/react-components/styles/styles.ts b/packages/react-components/styles/styles.ts index 96fae2f4993..869c5d6b737 100644 --- a/packages/react-components/styles/styles.ts +++ b/packages/react-components/styles/styles.ts @@ -4,6 +4,16 @@ import { StyleSheet } from 'react-native' export const TOP_BAR_HEIGHT = 56 +export function elevationShadowStyle(elevation: number) { + return { + elevation, + shadowColor: 'black', + shadowOffset: { width: 0, height: 0.5 * elevation }, + shadowOpacity: 0.3, + shadowRadius: 0.8 * elevation, + } +} + export const componentStyles = StyleSheet.create({ marginTop10: { marginTop: 10, From 5a1894fea1eb6f5006c1b2911c0cc7237cbf8626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Guti=C3=A9rrez?= Date: Thu, 12 Dec 2019 19:36:50 +0100 Subject: [PATCH 19/40] [Wallet] Disable skip button when the user enable contact access (#2224) --- packages/mobile/src/import/ImportContacts.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mobile/src/import/ImportContacts.tsx b/packages/mobile/src/import/ImportContacts.tsx index bf242a2643f..ccac76a3a5f 100644 --- a/packages/mobile/src/import/ImportContacts.tsx +++ b/packages/mobile/src/import/ImportContacts.tsx @@ -129,6 +129,7 @@ class ImportContacts extends React.Component { testID="importContactsEnable" />