diff --git a/.env b/.env index fa97fbfc3b3..f02f2b558a5 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" @@ -47,10 +47,10 @@ ORACLE_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/oracle" ORACLE_DOCKER_IMAGE_TAG="default" TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-d405b99872097291c8e07d2445559588547938b0" +TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-8e69cf86010b62e283d5f9285f181fca5483733e" ATTESTATION_SERVICE_DOCKER_IMAGE_REPOSITORY="us.gcr.io/celo-testnet/celo-monorepo" -ATTESTATION_SERVICE_DOCKER_IMAGE_TAG="attestation-service-5ee799c0a726a44113cc0a9b26932531019a07a4" +ATTESTATION_SERVICE_DOCKER_IMAGE_TAG="attestation-service-8e69cf86010b62e283d5f9285f181fca5483733e" GETH_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet-production/geth-exporter" GETH_EXPORTER_DOCKER_IMAGE_TAG="ed7d21bd50592709173368cd697ef73c1774a261" diff --git a/.env.baklava b/.env.baklava index be10ac7c178..a7dc8bed6e8 100644 --- a/.env.baklava +++ b/.env.baklava @@ -13,7 +13,7 @@ CLUSTER_DOMAIN_NAME="celo-testnet" TESTNET_PROJECT_NAME="celo-testnet-production" BLOCKSCOUT_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/blockscout" -BLOCKSCOUT_DOCKER_IMAGE_TAG="9ebd645aa670a5195641ca03c9f9717b5ea897cc" +BLOCKSCOUT_DOCKER_IMAGE_TAG="982ffa2a1ddba3b5ccfe0915bd902ab8671ddc99" BLOCKSCOUT_WEB_REPLICAS=3 # Increment this value everytime you redeploy blockscout. Or else the deployment will fail due to the # existing database. @@ -28,15 +28,15 @@ ETHSTATS_BANNED_ADDRESSES="" GETH_NODE_DOCKER_IMAGE_REPOSITORY="us.gcr.io/celo-testnet/geth" # When upgrading change this to latest commit hash from the master of the geth repo # `geth $ git show | head -n 1` -GETH_NODE_DOCKER_IMAGE_TAG="6e3766093331ef1cef8428c2320c60f62390adeb" +GETH_NODE_DOCKER_IMAGE_TAG="5d4224eb0e13683107f3a8ea8d7cb668e676b4fc" GETH_BOOTNODE_DOCKER_IMAGE_REPOSITORY="us.gcr.io/celo-testnet/geth-all" # When upgrading change this to latest commit hash from the master of the geth repo # `geth $ git show | head -n 1` -GETH_BOOTNODE_DOCKER_IMAGE_TAG="6e3766093331ef1cef8428c2320c60f62390adeb" +GETH_BOOTNODE_DOCKER_IMAGE_TAG="5d4224eb0e13683107f3a8ea8d7cb668e676b4fc" 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" @@ -49,16 +49,16 @@ ORACLE_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/oracle" ORACLE_DOCKER_IMAGE_TAG="baklava" TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-4f63a422e0af6efca536b569f3005de209eb1cfd" +TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-8e69cf86010b62e283d5f9285f181fca5483733e" ATTESTATION_SERVICE_DOCKER_IMAGE_REPOSITORY="us.gcr.io/celo-testnet/celo-monorepo" -ATTESTATION_SERVICE_DOCKER_IMAGE_TAG="attestation-service-dcad01808c05f77ea71b911ba331caab0fd739d3" +ATTESTATION_SERVICE_DOCKER_IMAGE_TAG="attestation-service-8e69cf86010b62e283d5f9285f181fca5483733e" GETH_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet-production/geth-exporter" GETH_EXPORTER_DOCKER_IMAGE_TAG="ed7d21bd50592709173368cd697ef73c1774a261" # Genesis Vars -NETWORK_ID=76172 +NETWORK_ID=121119 CONSENSUS_TYPE="istanbul" BLOCK_TIME=5 # Minimum epoch length is 15 minutes @@ -103,9 +103,15 @@ STACKDRIVER_NOTIFICATION_APPLICATIONS_PREFIX="notification-service-,blockchain-a MOBILE_WALLET_PLAYSTORE_LINK="https://play.google.com/apps/internaltest/4700990475000634666" -# NOTIFICATION_SERVICE_FIREBASE_DB="https://console.firebase.google.com/u/0/project/celo-org-mobile/database/celo-org-mobile-int/data" - PROMTOSD_SCRAPE_INTERVAL="5m" PROMTOSD_EXPORT_INTERVAL="5m" SMS_RETRIEVER_HASH_CODE=l5k6LvdPDXS + +LEADERBOARD_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" +LEADERBOARD_DOCKER_IMAGE_TAG="leaderboard-e84c5086e94193fdef430008bd44129e4151bb6b" + +# Attestation Bot variables +ATTESTATION_BOT_INITIAL_WAIT_SECONDS=600 +ATTESTATION_BOT_IN_BETWEEN_WAIT_SECONDS=600 +ATTESTATION_BOT_MAX_ATTESTATIONS=90 diff --git a/.env.baklavastaging b/.env.baklavastaging index af4f5eba502..9989f27887c 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" @@ -49,10 +49,10 @@ ORACLE_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/oracle" ORACLE_DOCKER_IMAGE_TAG="baklavastaging" TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-51ae8e72be34a614346950b6fbd8ba3b94223947" +TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-8e69cf86010b62e283d5f9285f181fca5483733e" ATTESTATION_SERVICE_DOCKER_IMAGE_REPOSITORY="us.gcr.io/celo-testnet/celo-monorepo" -ATTESTATION_SERVICE_DOCKER_IMAGE_TAG="attestation-service-c8e3392aa2ca44ff83b4035700ece5fd12ed2b84" +ATTESTATION_SERVICE_DOCKER_IMAGE_TAG="attestation-service-8e69cf86010b62e283d5f9285f181fca5483733e" GETH_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet-production/geth-exporter" GETH_EXPORTER_DOCKER_IMAGE_TAG="ed7d21bd50592709173368cd697ef73c1774a261" @@ -102,9 +102,12 @@ STACKDRIVER_NOTIFICATION_APPLICATIONS_PREFIX="notification-service-,blockchain-a MOBILE_WALLET_PLAYSTORE_LINK="https://play.google.com/apps/internaltest/4700990475000634666" -# NOTIFICATION_SERVICE_FIREBASE_DB="https://console.firebase.google.com/u/0/project/celo-org-mobile/database/celo-org-mobile-int/data" - PROMTOSD_SCRAPE_INTERVAL="5m" PROMTOSD_EXPORT_INTERVAL="5m" 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.integration b/.env.integration index 808efbf1c92..94fd821b55c 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" @@ -47,10 +47,10 @@ ORACLE_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/oracle" ORACLE_DOCKER_IMAGE_TAG="default" TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet/celo-monorepo" -TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-d405b99872097291c8e07d2445559588547938b0" +TRANSACTION_METRICS_EXPORTER_DOCKER_IMAGE_TAG="transaction-metrics-exporter-8e69cf86010b62e283d5f9285f181fca5483733e" ATTESTATION_SERVICE_DOCKER_IMAGE_REPOSITORY="us.gcr.io/celo-testnet/celo-monorepo" -ATTESTATION_SERVICE_DOCKER_IMAGE_TAG="attestation-service-5ee799c0a726a44113cc0a9b26932531019a07a4" +ATTESTATION_SERVICE_DOCKER_IMAGE_TAG="attestation-service-8e69cf86010b62e283d5f9285f181fca5483733e" GETH_EXPORTER_DOCKER_IMAGE_REPOSITORY="gcr.io/celo-testnet-production/geth-exporter" GETH_EXPORTER_DOCKER_IMAGE_TAG="ed7d21bd50592709173368cd697ef73c1774a261" diff --git a/.env.mnemonic.baklava.enc b/.env.mnemonic.baklava.enc index 25754c1761e..05bfe011e91 100644 Binary files a/.env.mnemonic.baklava.enc and b/.env.mnemonic.baklava.enc differ diff --git a/.env.mnemonic.baklavastaging.enc b/.env.mnemonic.baklavastaging.enc index 25835b6e7c2..9c2429de1de 100644 Binary files a/.env.mnemonic.baklavastaging.enc and b/.env.mnemonic.baklavastaging.enc differ 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/package.json b/package.json index 569729e8734..583dd7f2b59 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": { @@ -76,6 +76,7 @@ "**/deep-extend": "^0.5.1", "**/extend": "^3.0.2", "**/cross-fetch": "^3.0.2", - "sha3": "1.2.3" + "sha3": "1.2.3", + "bignumber.js": "9.0.0" } } 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 } diff --git a/packages/blockchain-api/.env b/packages/blockchain-api/.env index 04eedcecf08..e3601c7664d 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-integration.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..5489ece4f70 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-integration.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 fcd26172452..36a3ff43dc9 100644 --- a/packages/blockchain-api/package.json +++ b/packages/blockchain-api/package.json @@ -18,11 +18,13 @@ }, "dependencies": { "@celo/contractkit": "0.2.0", + "@celo/utils": "^0.1.0", "apollo-datasource-rest": "^0.3.1", "apollo-server-express": "^2.4.2", "bignumber.js": "^7.2.0", "dotenv": "^6.1.0", "express": "^4.16.4", + "firebase-admin": "^8.6.1", "graphql": "^14.1.1", "utf8": "^3.0.0", "web3-eth-abi": "1.0.0-beta.37" @@ -35,7 +37,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/blockchain-api/serviceAccountKey.json.enc b/packages/blockchain-api/serviceAccountKey.json.enc new file mode 100644 index 00000000000..dd9475ce349 Binary files /dev/null and b/packages/blockchain-api/serviceAccountKey.json.enc differ 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/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/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/celotool/src/cmds/transactions/describe.ts b/packages/celotool/src/cmds/transactions/describe.ts index 2fb1c3249fd..72f30865bf3 100644 --- a/packages/celotool/src/cmds/transactions/describe.ts +++ b/packages/celotool/src/cmds/transactions/describe.ts @@ -1,14 +1,10 @@ -import { - constructFunctionABICache, - getContracts, - parseFunctionCall, - parseLog, -} from '@celo/walletkit' +import { newKitFromWeb3 } from '@celo/contractkit' +import { newBlockExplorer } from '@celo/contractkit/lib/explorer/block-explorer' +import { newLogExplorer } from '@celo/contractkit/lib/explorer/log-explorer' import { getWeb3Client } from 'src/lib/blockchain' import { switchToClusterFromEnv } from 'src/lib/cluster' import yargs from 'yargs' import { TransactionsArgv } from '../transactions' - export const command = 'describe ' export const describe = 'fetch a transaction, attempt parsing and print it out to STDOUT' @@ -27,9 +23,9 @@ export const handler = async (argv: DescribeArgv) => { await switchToClusterFromEnv(false) const web3 = await getWeb3Client(argv.celoEnv) - - const contracts = await getContracts(web3) - const functionABICache = constructFunctionABICache(Object.values(contracts), web3) + const kit = await newKitFromWeb3(web3) + const blockExplorer = await newBlockExplorer(kit) + const logExplorer = await newLogExplorer(kit) const transaction = await web3.eth.getTransaction(argv.transactionHash) const receipt = await web3.eth.getTransactionReceipt(argv.transactionHash) @@ -41,13 +37,7 @@ export const handler = async (argv: DescribeArgv) => { console.info(receipt) } - const res = parseFunctionCall(transaction, functionABICache, web3) - - if (res === null) { - return - } - - const [parsedTransaction, transactionContract] = res + const parsedTransaction = blockExplorer.tryParseTx(transaction) if (parsedTransaction === null) { return @@ -58,7 +48,7 @@ export const handler = async (argv: DescribeArgv) => { if (receipt.logs) { receipt.logs.forEach((log) => { - const parsedLog = parseLog(parsedTransaction, log, transactionContract, web3) + const parsedLog = logExplorer.tryParseLog(log) if (parsedLog === null) { return diff --git a/packages/celotool/src/cmds/transactions/list.ts b/packages/celotool/src/cmds/transactions/list.ts index 6b794ed22bc..e8dea170676 100644 --- a/packages/celotool/src/cmds/transactions/list.ts +++ b/packages/celotool/src/cmds/transactions/list.ts @@ -1,11 +1,6 @@ -import { - constructFunctionABICache, - FunctionABICache, - getContracts, - parseFunctionCall, - parseLog, -} from '@celo/walletkit' -import moment from 'moment' +import { newKitFromWeb3 } from '@celo/contractkit' +import { BlockExplorer, newBlockExplorer } from '@celo/contractkit/lib/explorer/block-explorer' +import { LogExplorer, newLogExplorer } from '@celo/contractkit/lib/explorer/log-explorer' import fetch from 'node-fetch' import { CONTRACTS_TO_COPY, copyContractArtifacts, downloadArtifacts } from 'src/lib/artifacts' import { getWeb3Client } from 'src/lib/blockchain' @@ -41,62 +36,57 @@ export const handler = async (argv: ListArgv) => { const web3 = await getWeb3Client(argv.celoEnv) const blockscoutURL = getBlockscoutUrl(argv.celoEnv) + const kit = await newKitFromWeb3(web3) + const blockExplorer = await newBlockExplorer(kit) + const logExplorer = await newLogExplorer(kit) const resp = await fetch( `${blockscoutURL}/api?module=account&action=txlist&address=${argv.address}&sort=desc` ) const jsonResp = await resp.json() - const contracts = await getContracts(web3) - const functionABICache = constructFunctionABICache(Object.values(contracts), web3) if (jsonResp.result === undefined) { return } for (const blockscoutTx of jsonResp.result) { - await fetchTx(web3, functionABICache, blockscoutTx) + await fetchTx(web3, blockExplorer, logExplorer, blockscoutTx) } process.exit(0) } async function fetchTx( web3: Web3, - functionABICache: FunctionABICache, + blockExplorer: BlockExplorer, + logExplorer: LogExplorer, blockscoutTx: { hash: string; timeStamp: string } ) { const transaction = await web3.eth.getTransaction(blockscoutTx.hash) const receipt = await web3.eth.getTransactionReceipt(blockscoutTx.hash) - const res = parseFunctionCall(transaction, functionABICache, web3) + const parsedTransaction = blockExplorer.tryParseTx(transaction) - if (res === null) { - return - } - - const [parsedTransaction, transactionContract] = res - - console.info('\n' + moment.unix(parseInt(blockscoutTx.timeStamp, 10)).fromNow()) if (parsedTransaction === null) { console.info(`Unparsable Transaction: ${transaction.hash}`) return } console.info( - `${parsedTransaction.contractName}#${parsedTransaction.functionName}(${JSON.stringify( - parsedTransaction.parameters - )}) ${parsedTransaction.transactionHash}` + `${parsedTransaction.callDetails.contract}#${ + parsedTransaction.callDetails.function + }(${JSON.stringify(parsedTransaction.callDetails.parameters)}) ${parsedTransaction.tx.hash}` ) if (receipt.logs) { receipt.logs.forEach((log) => { try { - const parsedLog = parseLog(parsedTransaction, log, transactionContract, web3) + const parsedLog = logExplorer.tryParseLog(log) if (parsedLog === null) { console.info(`\tParsed log is null for log "${log.address}"`) return } - console.info(`\t${parsedLog.logName}(${JSON.stringify(parsedLog.parameters)})`) + console.info(`\t${parsedLog.event}(${JSON.stringify(parsedLog.returnValues)})`) } catch (e) { console.error(`Error while parsing log ${log}`) } 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, 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/cli/package.json b/packages/cli/package.json index 6a3b037d4c0..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", @@ -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/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/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}` diff --git a/packages/contractkit/src/explorer/log-explorer.ts b/packages/contractkit/src/explorer/log-explorer.ts index ddf85536ba6..a29b37418dd 100644 --- a/packages/contractkit/src/explorer/log-explorer.ts +++ b/packages/contractkit/src/explorer/log-explorer.ts @@ -95,6 +95,11 @@ export class LogExplorer { const returnValues = abi.decodeLog(matchedAbi.inputs || [], log.data || '', log.topics.slice(1)) delete (returnValues as any).__length__ + Object.keys(returnValues).forEach((key) => { + if (Number.parseInt(key, 10) >= 0) { + delete (returnValues as any)[key] + } + }) const logEvent: EventLog & { signature: string } = { address: log.address, 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) 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/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. 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/reconnecting-to-network.md b/packages/docs/getting-started/reconnecting-to-network.md index 8f11c950c23..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 ``` @@ -91,10 +90,16 @@ 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 ``` +```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-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 0d6971813bd..9e9fce1f7d8 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,31 +107,31 @@ 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: ```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 @@ -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: @@ -195,7 +201,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 @@ -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. @@ -256,11 +262,15 @@ 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 ``` +{% 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,13 +323,13 @@ 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 ``` 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`). @@ -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. @@ -510,12 +527,12 @@ 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 -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= ``` @@ -568,7 +585,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. @@ -657,6 +674,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 @@ -670,7 +694,7 @@ celocli account:register-metadata --url --from $CELO_VALIDA Now when you run `celocli account:get-metadata $CELO_VALIDATOR_ADDRESS`, you should see your claim for the group account to be verified. By now, you should have setup your Validator account appropriately. Note that you need to add these claims for any other addresses that are yours to calculate your score for the leaderboard appropriately. {% hint style="tip" %} -Congratulations on setting up your validator. If you want to win the TGCSO, it may be helpful to familiar with the inner workings of the Celo network. Dig into the [protocol documentation](../celo-codebase/protocol) for more information. +Congratulations on setting up your validator. If you want to win the TGCSO, it may be helpful to get familiar with the inner workings of the Celo network. Dig into the [protocol documentation](../celo-codebase/protocol) for more information. {% endhint %} ## Deployment Tips @@ -706,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: @@ -747,5 +771,5 @@ celocli election:revoke --from $CELO_VALIDATOR_GROUP_ADDRESS --for $CELO_VALIDAT - Deregister your validator group: ```bash -celocli validatorgroup:deregister --from $CELO_VALIDATOR_GORUP_ADDRESS +celocli validatorgroup:deregister --from $CELO_VALIDATOR_GROUP_ADDRESS ``` 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! 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: 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/README.md b/packages/mobile/README.md index 2c4a4038ce8..9b37e54625a 100644 --- a/packages/mobile/README.md +++ b/packages/mobile/README.md @@ -93,11 +93,13 @@ that you want to use with the app, update `.env.ENV-NAME` and `packages/mobile/. the new network name and settings, then rebuild the app. Note that this will assume the testnets have a corresponding `/blockchain-api` and `/notification-service` set up. -### Running Wallet app in ZeroSync mode +### Running Wallet app in zero sync (Data Saver) mode By default, the mobile wallet app runs geth in ultralight sync mode where all the epoch headers are fetched. The default sync mode is defined in [packages/mobile/.env](https://github.com/celo-org/celo-monorepo/blob/master/packages/mobile/.env#L4) file. -To run the wallet in zero sync mode, using a trusted node rather than the local geth node as a provider, turn it on from the Celo Lite page in settings or update the zero sync initially enabled parameter in the .env file linked above. When zero sync mode is turned back off, the wallet will switch to the default sync mode as specified in the .env file. By default, the trusted node is `https://{TESTNET}-forno.celo-testnet.org`, however any trusted node can be used by updating `DEFAULT_FORNO_URL`. In zero sync mode, the wallet signs transactions locally in web3 then sends them to the trusted node. +To run the wallet in zero sync (Data Saver) mode, using a trusted node rather than the local geth node as a provider, turn it on from the Data Saver page in settings or update the `ZERO_SYNC_ENABLED_INITIALLY` parameter in the .env file linked above. When zero sync mode is turned back off, the wallet will switch to the default sync mode as specified in the .env file. By default, the trusted node is `https://{TESTNET}-forno.celo-testnet.org`, however any trusted node can be used by updating `DEFAULT_FORNO_URL`. In zero sync mode, the wallet signs transactions locally in web3 then sends them to the trusted node. + +To debug network requests in zero sync mode, we use Charles, a proxy for monitoring network traffic to see Celo JSON RPC calls and responses. Follow instructions [here](https://community.tealiumiq.com/t5/Tealium-for-Android/Setting-up-Charles-to-Proxy-your-Android-Device/ta-p/5121) to configure Charles to proxy a test device. ## Testing diff --git a/packages/mobile/android/app/src/main/AndroidManifest.xml b/packages/mobile/android/app/src/main/AndroidManifest.xml index 0c14ec2f6e7..79fea4bca67 100644 --- a/packages/mobile/android/app/src/main/AndroidManifest.xml +++ b/packages/mobile/android/app/src/main/AndroidManifest.xml @@ -12,7 +12,7 @@ - + diff --git a/packages/mobile/android/app/src/main/res/xml/network_security_config.xml b/packages/mobile/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 00000000000..eb128dd411e --- /dev/null +++ b/packages/mobile/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,11 @@ + + + localhost + + + + + + + + \ No newline at end of file diff --git a/packages/mobile/index.js b/packages/mobile/index.js index 6de3dbacf65..0e27af847e7 100644 --- a/packages/mobile/index.js +++ b/packages/mobile/index.js @@ -1,5 +1,7 @@ +// Order is important, please don't change it unless you know what you're doing :D import 'node-libs-react-native/globals' import 'src/missingGlobals' +import 'src/forceCommunityAsyncStorage' import { AppRegistry } from 'react-native' import Logger from 'src/utils/Logger' import App from 'src/app/App' 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 diff --git a/packages/mobile/locales/en-US/accountScreen10.json b/packages/mobile/locales/en-US/accountScreen10.json index 2d07dbfcdfd..6ce6eb1c94c 100644 --- a/packages/mobile/locales/en-US/accountScreen10.json +++ b/packages/mobile/locales/en-US/accountScreen10.json @@ -9,13 +9,13 @@ "shareAnalytics": "Share Analytics", "shareAnalytics_detail": "We collect anonymized data about how you use Celo to help improve the application for everyone.", - "celoLite": "Celo Lite", - "enableCeloLite": "Enable Celo Lite", - "celoLiteDetail": - "Celo Lite mode allows you to communicate with the Celo Network through a trusted node. You can always change this mode in app settings.", + "dataSaver": "Data Saver", + "enableDataSaver": "Enable Data Saver", + "dataSaverDetail": + "Data Saver mode allows you to communicate with the Celo Network through a trusted node. You can always change this mode in app settings.", "restartModal": { - "header": "Restart To Switch Off Celo Lite", - "body": "To switch Celo Lite on and off repeatedly, you will need to restart the app.", + "header": "Restart To Switch Off Data Saver", + "body": "To switch Data Saver on and off repeatedly, you will need to restart the app.", "restart": "Restart to Switch" }, "testFaqLink": "Celo Wallet FAQ", 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/accountScreen10.json b/packages/mobile/locales/es-419/accountScreen10.json index 40ceecc0bb6..c9150cff121 100755 --- a/packages/mobile/locales/es-419/accountScreen10.json +++ b/packages/mobile/locales/es-419/accountScreen10.json @@ -9,13 +9,13 @@ "shareAnalytics": "Compartir estadisticas de uso", "shareAnalytics_detail": "Recopilamos datos anónimos sobre cómo utiliza Celo para ayudar a mejorar la aplicación para todos.", - "celoLite": "Celo Lite", - "enableCeloLite": "Habilitar Celo Lite", - "celoLiteDetail": - "El modo Celo Lite te permite comunicarte con la Red Celo a través de un nodo confiable. Puedes cambiar este modo en la configuración de la aplicación.", + "dataSaver": "Data Saver", + "enableDataSaver": "Habilitar Data Saver", + "dataSaverDetail": + "El modo Data Saver te permite comunicarte con la Red Celo a través de un nodo confiable. Puedes cambiar este modo en la configuración de la aplicación.", "restartModal": { - "header": "Reiniciar para apagar Celo Lite", - "body": "Para encender y apagar Celo Lite repetidamente, deberá reiniciar la aplicación.", + "header": "Reiniciar para apagar Data Saver", + "body": "Para encender y apagar Data Saver repetidamente, deberá reiniciar la aplicación.", "restart": "Reiniciar para alternar" }, "testFaqLink": "Las Preguntas Frecuentes del Monedero Celo", 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/package.json b/packages/mobile/package.json index fdf53fdf4ad..2fbc7ea19a0 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -5,17 +5,14 @@ "license": "Apache-2.0", "private": true, "scripts": { - "start": "react-native start", - "start:bg": "react-native start &", "lint": "tslint -c tslint.json --project tsconfig.json", "build": "yarn run build:ts && yarn run build:metro", "build:sdk": "yarn --cwd ../walletkit build:for-env", "build:ts": "tsc --noEmit", "build:metro": "echo 'NOT WORKING RIGHT NOW'", "build:gen-graphql-types": "gql-gen --schema http://localhost:8080/graphql --template graphql-codegen-typescript-template --out ./typings/ 'src/**/*.tsx'", - "predev": "./scripts/pre-dev.sh", - "dev": "react-native run-android --appIdSuffix \"debug\" --no-packager && yarn start || echo 'Could not start metro server'", - "dev:ios": "react-native run-ios --simulator \"iPhone 11\"", + "dev:android": "./scripts/run_app.sh -p android", + "dev:ios": "./scripts/run_app.sh -p ios", "dev:show-menu": "adb devices | grep '\t' | awk '{print $1}' | sed 's/\\s//g' | xargs -I {} adb -s {} shell input keyevent 82", "dev:clear-data": "adb shell pm clear org.celo.mobile.debug", "dev:clean-android": "cd android && ./gradlew clean", diff --git a/packages/mobile/scripts/pre-dev.sh b/packages/mobile/scripts/pre-dev.sh deleted file mode 100755 index 0d33045da86..00000000000 --- a/packages/mobile/scripts/pre-dev.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ==================================== -# Tasks to run before running yarn dev -# ==================================== - -# Detect network from .env and build the sdk for it -ENV_FILENAME="${ENVFILE:-.env}" -export $(grep -v '^#' $ENV_FILENAME | xargs) -echo "Building sdk for testnet $DEFAULT_TESTNET" -yarn build:sdk $DEFAULT_TESTNET -echo "Done building sdk" -echo "Jetifying react native libraries" -cd ../../ && yarn run jetify -echo "Jetified" \ No newline at end of file diff --git a/packages/mobile/scripts/run_app.sh b/packages/mobile/scripts/run_app.sh new file mode 100755 index 00000000000..d2a9165ffdc --- /dev/null +++ b/packages/mobile/scripts/run_app.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ==================================== +# Configure and run the mobile app +# ==================================== + +# Flags: +# -n: Name of the network to run on +# -p: Platform (android or ios) +# -f: Fast (skip steps not required unless network or depedencies changes) +# -r: Hot Reload (Restore nav state on reload) + +NETWORK="" +PLATFORM="" +FAST=false +HOT_RELOAD=false +while getopts 'n:p:fr' flag; do + case "${flag}" in + n) NETWORK="$OPTARG" ;; + p) PLATFORM="$OPTARG" ;; + f) FAST=true ;; + r) HOT_RELOAD=true ;; + *) error "Unexpected option ${flag}" ;; + esac +done + +[ -z "$PLATFORM" ] && echo "Need to set the PLATFORM via the -p flag" && exit 1; + +# Get machine type (needed later) +unameOut="$(uname -s)" +case "${unameOut}" in + Linux*) MACHINE=Linux;; + Darwin*) MACHINE=Mac;; + CYGWIN*) MACHINE=Cygwin;; + MINGW*) MACHINE=MinGw;; + *) MACHINE="UNKNOWN:${unameOut}" +esac +echo "Machine type: $MACHINE" +echo "Current directory: `pwd`" + +# Read values from the .env file and put them in env vars +ENV_FILENAME="${ENVFILE:-.env}" +export $(grep -v '^#' $ENV_FILENAME | xargs) + +if [ -z "$NETWORK" ]; then + echo "No network set." + read -p "Use $DEFAULT_TESTNET network set in .env file (y/n)? " + if [[ $REPLY =~ ^[Yy]$ ]]; then + NETWORK=$DEFAULT_TESTNET + else + echo "No network chosen. Exiting." + exit 1 + fi +fi + +# Set DEFAULT_TESTNET in .env file +sed -i.bak "s/DEFAULT_TESTNET=.*/DEFAULT_TESTNET=$NETWORK/g" $ENV_FILENAME + +# Set Hot Reload (saved nav state) in .env file +sed -i.bak "s/DEV_RESTORE_NAV_STATE_ON_RELOAD=.*/DEV_RESTORE_NAV_STATE_ON_RELOAD=$HOT_RELOAD/g" $ENV_FILENAME + +# Set Firebase settings in google service config files +ANDROID_GSERVICES_PATH="./android/app/src/debug/google-services.json" +IOS_GSERVICES_PATH="./ios/GoogleService-Info.plist" +sed -i.bak "s/celo-org-mobile-.*firebaseio.com/celo-org-mobile-$NETWORK.firebaseio.com/g" $ANDROID_GSERVICES_PATH +sed -i.bak "s/celo-org-mobile-.*firebaseio.com/celo-org-mobile-$NETWORK.firebaseio.com/g" $IOS_GSERVICES_PATH + +# Cleanup artifacts from in-place sed replacement on BSD based systems (macOS) +rm -f $ENV_FILENAME.bak +rm -f $ANDROID_GSERVICES_PATH.bak +rm -f $IOS_GSERVICES_PATH.bak + + +# Build Wallet Kit for env +if [ "$FAST" = false ]; then + echo "Building sdk for testnet $NETWORK" + yarn build:sdk $NETWORK + echo "Done building sdk" +fi + +# Build the app and run it +if [ $PLATFORM = "android" ]; then + echo "Using platform android" + + NUM_DEVICES=`adb devices -l | wc -l` + if [ $NUM_DEVICES -lt 3 ]; then + echo "No android devices found" + exit 1 + fi + + # Run jettify to fix non-android-x compatible libs + if [ "$FAST" = false ]; then + echo "Jetifying react native libraries" + cd ../../ && yarn run jetify && cd packages/mobile + echo "Jetified" + fi + + if [ $MACHINE = "Mac" ]; then + echo "Starting packager in new terminal" + RN_START_CMD="cd `pwd`;yarn react-native start" + OSASCRIPT_CMD="tell application \"Terminal\" to do script \"$RN_START_CMD\"" + echo "FULLCMD: $OSASCRIPT_CMD" + osascript -e "$OSASCRIPT_CMD" + # Run android without packager because RN cli doesn't work with yarn workspaces + yarn react-native run-android --appIdSuffix "debug" --no-packager + else + # Run android without packager because RN cli doesn't work with yarn workspaces + yarn react-native run-android --appIdSuffix "debug" --no-packager + yarn react-native start + fi + +elif [ $PLATFORM = "ios" ]; then + echo "Using platform ios" + # TODO have iOS build and start from command line + echo -e "\nFor now ios must be build and run from xcode\nStarting RN bundler\n" + yarn react-native start + +else + echo "Invalid value for platform, must be 'android' or 'ios'" + exit 1 +fi + diff --git a/packages/mobile/scripts/run_e2e.sh b/packages/mobile/scripts/run_e2e.sh index 38990a72a9f..54740a719b8 100755 --- a/packages/mobile/scripts/run_e2e.sh +++ b/packages/mobile/scripts/run_e2e.sh @@ -39,7 +39,7 @@ bash ./scripts/unlock.sh echo "Killing previous metro server (if any)" react-native-kill-packager || echo 'Failed to kill for some reason' echo "Start metro server" -yarn start:bg +yarn react-native start & echo "Waiting for device to connect to Wifi, this is a good proxy the device is ready" diff --git a/packages/mobile/src/account/Account.tsx b/packages/mobile/src/account/Account.tsx index c86a44ffa95..6f3caabc634 100644 --- a/packages/mobile/src/account/Account.tsx +++ b/packages/mobile/src/account/Account.tsx @@ -112,8 +112,8 @@ export class Account extends React.Component { navigate(Screens.Analytics, { nextScreen: Screens.Account }) } - goToCeloLite() { - navigate(Screens.CeloLite, { nextScreen: Screens.Account }) + goToDataSaver() { + navigate(Screens.DataSaver, { nextScreen: Screens.Account }) } goToFAQ() { @@ -238,7 +238,7 @@ export class Account extends React.Component { )} - + { +describe('DataSaver', () => { it('renders correctly', () => { const tree = renderer.create( - + ) expect(tree).toMatchSnapshot() diff --git a/packages/mobile/src/account/CeloLite.tsx b/packages/mobile/src/account/DataSaver.tsx similarity index 92% rename from packages/mobile/src/account/CeloLite.tsx rename to packages/mobile/src/account/DataSaver.tsx index 7faaa3704e0..a6ec7c7a048 100644 --- a/packages/mobile/src/account/CeloLite.tsx +++ b/packages/mobile/src/account/DataSaver.tsx @@ -38,10 +38,10 @@ interface State { modalVisible: boolean } -export class CeloLite extends React.Component { +export class DataSaver extends React.Component { static navigationOptions = () => ({ ...headerWithBackButton, - headerTitle: i18n.t('accountScreen10:celoLite'), + headerTitle: i18n.t('accountScreen10:dataSaver'), }) state = { @@ -78,9 +78,9 @@ export class CeloLite extends React.Component { - {t('enableCeloLite')} + {t('enableDataSaver')} @@ -139,4 +139,4 @@ const styles = StyleSheet.create({ export default connect( mapStateToProps, mapDispatchToProps -)(withNamespaces(Namespaces.accountScreen10)(CeloLite)) +)(withNamespaces(Namespaces.accountScreen10)(DataSaver)) diff --git a/packages/mobile/src/account/__snapshots__/Account.test.tsx.snap b/packages/mobile/src/account/__snapshots__/Account.test.tsx.snap index 84bbf0a1eee..c51cb1dfc09 100644 --- a/packages/mobile/src/account/__snapshots__/Account.test.tsx.snap +++ b/packages/mobile/src/account/__snapshots__/Account.test.tsx.snap @@ -647,7 +647,7 @@ exports[`Account renders correctly 1`] = ` } } > - celoLite + dataSaver - celoLite + dataSaver - enableCeloLite + enableDataSaver - celoLiteDetail + dataSaverDetail diff --git a/packages/mobile/src/apollo/index.ts b/packages/mobile/src/apollo/index.ts index ef326cf1ee4..0c918e83887 100644 --- a/packages/mobile/src/apollo/index.ts +++ b/packages/mobile/src/apollo/index.ts @@ -1,7 +1,7 @@ +import AsyncStorage from '@react-native-community/async-storage' import ApolloClient from 'apollo-boost' import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory' import { persistCache } from 'apollo-cache-persist' -import { AsyncStorage } from 'react-native' import introspectionQueryResultData from 'src/apollo/fragmentTypes.json' import config from 'src/geth/networkConfig' import Logger from 'src/utils/Logger' 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/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/mobile/src/forceCommunityAsyncStorage.ts b/packages/mobile/src/forceCommunityAsyncStorage.ts new file mode 100644 index 00000000000..e81266cd4cd --- /dev/null +++ b/packages/mobile/src/forceCommunityAsyncStorage.ts @@ -0,0 +1,15 @@ +import AsyncStorage from '@react-native-community/async-storage' +import ReactNative from 'react-native' + +// Monkey patch React Native so it uses the community implementation of AsyncStorage. +// This is to fix the problem of loosing AsyncStorage data on iOS when some modules +// import AsyncStorage from React Native Core and some others import from React Native Community. +// Since both implementations currently write to the same file on disk, the last one to write wins +// and erases the existing data written by the other. +// See https://github.com/react-native-community/async-storage/issues/118#issuecomment-500138053 +// TODO: remove this once React Native removes AsyncStorage from Core. +Object.defineProperty(ReactNative, 'AsyncStorage', { + get() { + return AsyncStorage + }, +}) 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/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" />