From 579c1993c3ef907258fcb6fd9a1a27b6e5773c1a Mon Sep 17 00:00:00 2001 From: Ashish Bhatia Date: Tue, 1 Oct 2019 23:49:41 -0700 Subject: [PATCH 01/11] [wallet]Run geth in an infura-like mode (#1108) --- packages/mobile/.env | 4 +- packages/mobile/index.js | 2 +- packages/mobile/src/account/Account.test.tsx | 4 + .../src/account/DollarEducation.test.tsx | 4 + .../mobile/src/account/EditProfile.test.tsx | 4 + .../mobile/src/account/Education.test.tsx | 4 + .../mobile/src/account/GoldEducation.test.tsx | 4 + packages/mobile/src/account/Invite.test.tsx | 4 + .../mobile/src/account/InviteReview.test.tsx | 9 + .../src/account/PhotosEducation.test.tsx | 4 + packages/mobile/src/account/Profile.test.tsx | 4 + packages/mobile/src/app/App.test.tsx | 4 + packages/mobile/src/app/Debug.test.tsx | 4 + packages/mobile/src/app/saga.test.ts | 9 + .../src/components/AccountOverview.test.tsx | 4 + packages/mobile/src/config.ts | 1 + .../escrow/EscrowedPaymentLineItem.test.tsx | 4 + .../ReclaimPaymentConfirmationCard.test.tsx | 4 + .../ReclaimPaymentConfirmationScreen.test.tsx | 9 + packages/mobile/src/escrow/saga.ts | 28 ++- .../src/exchange/ExchangeTradeScreen.test.tsx | 4 + packages/mobile/src/exchange/actions.ts | 3 +- packages/mobile/src/fees/saga.test.ts | 10 + packages/mobile/src/geth/consts.ts | 16 +- packages/mobile/src/geth/network-config.ts | 13 +- packages/mobile/src/geth/saga.ts | 10 + .../src/home/CeloDollarsOverview.test.tsx | 4 + .../mobile/src/home/NotificationBox.test.tsx | 9 + packages/mobile/src/home/WalletHome.test.tsx | 9 + packages/mobile/src/home/saga.test.ts | 4 + .../mobile/src/identity/commentKey.test.ts | 4 + .../src/identity/contactMapping.test.ts | 4 + .../mobile/src/identity/verification.test.ts | 4 + packages/mobile/src/identity/verification.ts | 3 +- .../mobile/src/import/ImportContacts.test.tsx | 4 + .../mobile/src/import/ImportWallet.test.tsx | 4 + .../src/invite/EnterInviteCode.test.tsx | 4 + packages/mobile/src/invite/JoinCelo.test.tsx | 4 + packages/mobile/src/invite/reducer.test.ts | 4 + packages/mobile/src/invite/saga.test.ts | 27 +++ packages/mobile/src/invite/saga.ts | 27 ++- .../mobile/src/language/Language.test.tsx | 4 + .../SelectLocalCurrency.test.tsx | 4 + .../mobile/src/{btoa.ts => missingGlobals.ts} | 4 + ...crowedPaymentReminderNotification.test.tsx | 4 + ...PaymentrequestSummaryNotification.test.tsx | 4 + .../PaymentRequestConfirmation.test.tsx | 9 + .../PaymentRequestListItem.test.tsx | 4 + .../PaymentRequestListScreen.test.tsx | 4 + .../src/pincode/PincodeConfirmation.test.tsx | 4 + .../mobile/src/pincode/PincodeSet.test.tsx | 4 + packages/mobile/src/qrcode/QRCode.test.tsx | 4 + .../src/recipients/RecipientItem.test.tsx | 4 + .../mobile/src/recipients/recipient.test.ts | 4 + packages/mobile/src/redux/sagas.test.ts | 4 + packages/mobile/src/send/SendAmount.test.tsx | 4 + .../mobile/src/send/SendConfirmation.test.tsx | 9 + .../send/TransferConfirmationCard.test.tsx | 4 + .../src/send/TransferReviewCard.test.tsx | 9 + packages/mobile/src/send/saga.test.ts | 9 + .../mobile/src/shared/BackupPrompt.test.tsx | 4 + .../src/shared/DisconnectBanner.test.tsx | 4 + packages/mobile/src/stableToken/saga.test.ts | 4 + .../transactions/ExchangeFeedItem.test.tsx | 4 + .../src/transactions/NoActivity.test.tsx | 4 + .../src/transactions/TransactionFeed.test.tsx | 4 + .../transactions/TransferFeedItem.test.tsx | 4 + packages/mobile/src/transactions/saga.ts | 2 +- packages/mobile/src/transactions/send.ts | 49 +++- packages/mobile/src/verify/Verify.test.tsx | 4 + packages/mobile/src/web3/actions.ts | 2 + packages/mobile/src/web3/contracts.ts | 63 ++++- packages/mobile/src/web3/gas.test.ts | 4 + packages/mobile/src/web3/saga.test.ts | 1 + packages/mobile/src/web3/saga.ts | 106 ++++++-- packages/mobile/src/web3/testnets.ts | 3 + packages/walletkit/index.ts | 2 + packages/walletkit/src/contract-utils.ts | 229 +++++++++++++++++- packages/walletkit/src/new-web3-utils.ts | 161 ++++++++++++ packages/walletkit/src/signing-utils.ts | 72 +++++- packages/walletkit/src/transaction-utils.ts | 46 +++- .../walletkit/test/transaction-utils.test.ts | 6 +- 82 files changed, 1093 insertions(+), 66 deletions(-) rename packages/mobile/src/{btoa.ts => missingGlobals.ts} (61%) create mode 100644 packages/walletkit/src/new-web3-utils.ts diff --git a/packages/mobile/.env b/packages/mobile/.env index 41e2cacdb03..3b0e51934b2 100644 --- a/packages/mobile/.env +++ b/packages/mobile/.env @@ -1,6 +1,8 @@ ENVIRONMENT=local DEFAULT_TESTNET=integration +# -1 == ZeroSync, 5 == Ultralight, see src/geth/consts.ts for more info +DEFAULT_SYNC_MODE=5 DEV_SETTINGS_ACTIVE_INITIALLY=true FIREBASE_ENABLED=true SECRETS_KEY=debug -SHOW_TESTNET_BANNER=true \ No newline at end of file +SHOW_TESTNET_BANNER=true diff --git a/packages/mobile/index.js b/packages/mobile/index.js index 0d0495436cf..46bbce686d1 100644 --- a/packages/mobile/index.js +++ b/packages/mobile/index.js @@ -1,5 +1,5 @@ import 'node-libs-react-native/globals' -import 'src/btoa' +import 'src/missingGlobals' import { AppRegistry } from 'react-native' import Logger from 'src/utils/Logger' import App from 'src/app/App' diff --git a/packages/mobile/src/account/Account.test.tsx b/packages/mobile/src/account/Account.test.tsx index 50b3485b967..df035339d5e 100644 --- a/packages/mobile/src/account/Account.test.tsx +++ b/packages/mobile/src/account/Account.test.tsx @@ -12,6 +12,10 @@ import { createMockStore } from 'test/utils' jest.useFakeTimers() +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('Account', () => { it('renders correctly', () => { const tree = renderer.create( diff --git a/packages/mobile/src/account/DollarEducation.test.tsx b/packages/mobile/src/account/DollarEducation.test.tsx index 4f5f43096bf..ee7e3cf8490 100644 --- a/packages/mobile/src/account/DollarEducation.test.tsx +++ b/packages/mobile/src/account/DollarEducation.test.tsx @@ -5,6 +5,10 @@ import * as renderer from 'react-test-renderer' import DollarEducation from 'src/account/DollarEducation' import { createMockStore } from 'test/utils' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('DollarEducation', () => { it('renders correctly', () => { const tree = renderer.create( diff --git a/packages/mobile/src/account/EditProfile.test.tsx b/packages/mobile/src/account/EditProfile.test.tsx index abbb731c1ac..b14985812e6 100644 --- a/packages/mobile/src/account/EditProfile.test.tsx +++ b/packages/mobile/src/account/EditProfile.test.tsx @@ -6,6 +6,10 @@ import { setName } from 'src/account/actions' import { EditProfile } from 'src/account/EditProfile' import { createMockStore, getMockI18nProps } from 'test/utils' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + it('renders the EditProfile Component', () => { const store = createMockStore() const tree = renderer.create( diff --git a/packages/mobile/src/account/Education.test.tsx b/packages/mobile/src/account/Education.test.tsx index 8b5f8964fe4..47ea1435d1e 100644 --- a/packages/mobile/src/account/Education.test.tsx +++ b/packages/mobile/src/account/Education.test.tsx @@ -26,6 +26,10 @@ const educationProps = { onFinish: jest.fn(), } +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('Education', () => { it('renders correctly', () => { const tree = renderer.create( diff --git a/packages/mobile/src/account/GoldEducation.test.tsx b/packages/mobile/src/account/GoldEducation.test.tsx index 59fd095b407..aed6c94c281 100644 --- a/packages/mobile/src/account/GoldEducation.test.tsx +++ b/packages/mobile/src/account/GoldEducation.test.tsx @@ -5,6 +5,10 @@ import * as renderer from 'react-test-renderer' import GoldEducation from 'src/account/GoldEducation' import { createMockStore } from 'test/utils' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('GoldEducation', () => { it('renders correctly', () => { const tree = renderer.create( diff --git a/packages/mobile/src/account/Invite.test.tsx b/packages/mobile/src/account/Invite.test.tsx index 2587fddf254..ebda67b487c 100644 --- a/packages/mobile/src/account/Invite.test.tsx +++ b/packages/mobile/src/account/Invite.test.tsx @@ -6,6 +6,10 @@ import Invite from 'src/account/Invite' import { createMockStore } from 'test/utils' import { mockE164NumberToInvitableRecipient, mockNavigation } from 'test/values' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('Invite', () => { it('renders correctly with recipients', () => { const tree = renderer.create( diff --git a/packages/mobile/src/account/InviteReview.test.tsx b/packages/mobile/src/account/InviteReview.test.tsx index 9db23d9bac7..177aed917a1 100644 --- a/packages/mobile/src/account/InviteReview.test.tsx +++ b/packages/mobile/src/account/InviteReview.test.tsx @@ -18,6 +18,15 @@ jest.mock('src/identity/verification', () => { return { isPhoneVerified: jest.fn(() => true) } }) +jest.mock('src/web3/contracts', () => ({ + web3: { + utils: { + fromWei: jest.fn((x: any) => x / 1e18), + }, + }, + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('InviteReview', () => { it('renders correctly', () => { const tree = renderer.create( diff --git a/packages/mobile/src/account/PhotosEducation.test.tsx b/packages/mobile/src/account/PhotosEducation.test.tsx index c64dbb88fa7..7986a2d9085 100644 --- a/packages/mobile/src/account/PhotosEducation.test.tsx +++ b/packages/mobile/src/account/PhotosEducation.test.tsx @@ -5,6 +5,10 @@ import * as renderer from 'react-test-renderer' import PhotosEducation from 'src/account/PhotosEducation' import { createMockStore } from 'test/utils' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('PhotosEducation', () => { it('renders correctly', () => { const tree = renderer.create( diff --git a/packages/mobile/src/account/Profile.test.tsx b/packages/mobile/src/account/Profile.test.tsx index 61083866354..e8e20fcdc62 100644 --- a/packages/mobile/src/account/Profile.test.tsx +++ b/packages/mobile/src/account/Profile.test.tsx @@ -9,6 +9,10 @@ import { Screens } from 'src/navigator/Screens' import { createMockStore } from 'test/utils' import { mockNavigation } from 'test/values' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + function profileFactory() { return ( diff --git a/packages/mobile/src/app/App.test.tsx b/packages/mobile/src/app/App.test.tsx index 45d557a9ea2..ed4a8200f8b 100644 --- a/packages/mobile/src/app/App.test.tsx +++ b/packages/mobile/src/app/App.test.tsx @@ -25,6 +25,10 @@ jest.mock('src/redux/sagas', () => { } }) +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('App', () => { it('renders an ApolloProvider', () => { const wrapper = shallow() diff --git a/packages/mobile/src/app/Debug.test.tsx b/packages/mobile/src/app/Debug.test.tsx index ef0d7d5fefa..8b7fa7d08db 100644 --- a/packages/mobile/src/app/Debug.test.tsx +++ b/packages/mobile/src/app/Debug.test.tsx @@ -5,6 +5,10 @@ import * as renderer from 'react-test-renderer' import Debug from 'src/app/Debug' import { createMockStore } from 'test/utils' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('Debug', () => { it('renders correctly', () => { const tree = renderer.create( diff --git a/packages/mobile/src/app/saga.test.ts b/packages/mobile/src/app/saga.test.ts index c71e8900c91..ab8940b2e3d 100644 --- a/packages/mobile/src/app/saga.test.ts +++ b/packages/mobile/src/app/saga.test.ts @@ -19,6 +19,15 @@ jest.mock('src/firebase/firebase', () => ({ getVersionInfo: jest.fn(async () => ({ deprecated: false })), })) +jest.mock('src/web3/contracts', () => ({ + web3: { + utils: { + fromWei: jest.fn((x: any) => x / 1e18), + }, + }, + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + const { navigate } = require('src/navigator/NavigationService') const { getVersionInfo } = require('src/firebase/firebase') diff --git a/packages/mobile/src/components/AccountOverview.test.tsx b/packages/mobile/src/components/AccountOverview.test.tsx index 91664707049..40540837a40 100644 --- a/packages/mobile/src/components/AccountOverview.test.tsx +++ b/packages/mobile/src/components/AccountOverview.test.tsx @@ -8,6 +8,10 @@ import { getMockI18nProps } from 'test/utils' const SAMPLE_BALANCE = '55.00001' const exchangeRatePair: ExchangeRatePair = { goldMaker: '0.11', dollarMaker: '10' } +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + it('renders correctly when ready', () => { const tree = renderer.create( ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + it('renders correctly', () => { const tree = renderer.create( ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('ReclaimPaymentConfirmationCard', () => { it('renders correctly for send payment confirmation', () => { const tree = renderer.create( diff --git a/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.test.tsx b/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.test.tsx index b48b1ae029f..076835df0a9 100644 --- a/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.test.tsx +++ b/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.test.tsx @@ -12,6 +12,15 @@ const TEST_FEE = new BigNumber(10000000000000000) jest.mock('src/escrow/saga') +jest.mock('src/web3/contracts', () => ({ + web3: { + utils: { + fromWei: jest.fn((x: any) => x / 1e18), + }, + }, + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + const mockedGetReclaimEscrowFee = getReclaimEscrowFee as jest.Mock const store = createMockStore() diff --git a/packages/mobile/src/escrow/saga.ts b/packages/mobile/src/escrow/saga.ts index b744396a1b7..3e402a22c1a 100644 --- a/packages/mobile/src/escrow/saga.ts +++ b/packages/mobile/src/escrow/saga.ts @@ -34,7 +34,7 @@ import { TransactionStatus, TransactionTypes } from 'src/transactions/reducer' import { sendAndMonitorTransaction } from 'src/transactions/saga' import { sendTransaction } from 'src/transactions/send' import Logger from 'src/utils/Logger' -import { web3 } from 'src/web3/contracts' +import { addLocalAccount, isZeroSyncMode, web3 } from 'src/web3/contracts' import { getConnectedAccount, getConnectedUnlockedAccount } from 'src/web3/saga' const TAG = 'escrow/saga' @@ -105,14 +105,19 @@ function* withdrawFromEscrow(action: EndVerificationAction) { const escrow: Escrow = yield call(getEscrowContract, web3) const account: string = yield call(getConnectedUnlockedAccount) - const inviteCode: string = yield select((state: RootState) => state.invite.redeemedInviteCode) + const tmpWalletPrivateKey: string = yield select( + (state: RootState) => state.invite.redeemedInviteCode + ) - if (!isValidPrivateKey(inviteCode)) { + if (!isValidPrivateKey(tmpWalletPrivateKey)) { Logger.warn(TAG + '@withdrawFromEscrow', 'Invalid private key, skipping escrow withdrawal') return } - const tempWalletAddress = web3.eth.accounts.privateKeyToAccount(inviteCode).address + const tempWalletAddress = web3.eth.accounts.privateKeyToAccount(tmpWalletPrivateKey).address + if (isZeroSyncMode()) { + addLocalAccount(web3, tmpWalletPrivateKey) + } Logger.debug(TAG + '@withdrawFromEscrow', 'Added temp account to wallet: ' + tempWalletAddress) // Check if there is a payment associated with this invite code @@ -123,13 +128,22 @@ function* withdrawFromEscrow(action: EndVerificationAction) { return } - // Unlock temporary account - yield call(web3.eth.personal.unlockAccount, tempWalletAddress, TEMP_PW, 600) + if (isZeroSyncMode()) { + Logger.info( + TAG + '@withdrawFromEscrow', + 'Geth free mode is on, no need to unlock the temporary account' + ) + } else { + // Unlock temporary account + yield call(web3.eth.personal.unlockAccount, tempWalletAddress, TEMP_PW, 600) + } const msgHash = web3.utils.soliditySha3({ type: 'address', value: account }) + Logger.debug(TAG + '@withdrawFromEscrow', `Signing message hash ${msgHash}`) // using the temporary wallet account to sign a message. The message is the current account. - let signature = yield web3.eth.sign(msgHash, tempWalletAddress) + let signature: string = (yield web3.eth.accounts.sign(msgHash, tmpWalletPrivateKey)).signature + Logger.debug(TAG + '@withdrawFromEscrow', `Signed message hash signature is ${signature}`) signature = signature.slice(2) const r = `0x${signature.slice(0, 64)}` const s = `0x${signature.slice(64, 128)}` diff --git a/packages/mobile/src/exchange/ExchangeTradeScreen.test.tsx b/packages/mobile/src/exchange/ExchangeTradeScreen.test.tsx index 3d6945a0d0f..946bb0777ba 100644 --- a/packages/mobile/src/exchange/ExchangeTradeScreen.test.tsx +++ b/packages/mobile/src/exchange/ExchangeTradeScreen.test.tsx @@ -18,6 +18,10 @@ jest.mock('src/shared/DisconnectBanner', () => ({ DisconnectBanner: () => null, })) +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + const store = createMockStore({ exchange: { exchangeRatePair, diff --git a/packages/mobile/src/exchange/actions.ts b/packages/mobile/src/exchange/actions.ts index 6b8a0ffea80..8990daadeed 100644 --- a/packages/mobile/src/exchange/actions.ts +++ b/packages/mobile/src/exchange/actions.ts @@ -27,6 +27,7 @@ import { roundDown } from 'src/utils/formatting' import Logger from 'src/utils/Logger' import { web3 } from 'src/web3/contracts' import { getConnectedAccount, getConnectedUnlockedAccount } from 'src/web3/saga' +import * as util from 'util' const TAG = 'exchange/actions' const LARGE_DOLLARS_SELL_AMOUNT_IN_WEI = new BigNumber(1000 * 1000000000000000000) // To estimate exchange rate from exchange contract @@ -233,7 +234,7 @@ export function* exchangeGoldAndStableTokens(action: ExchangeTokensAction) { return } yield call(sendTransaction, approveTx, account, TAG, 'approval') - Logger.debug(TAG, `Transaction approved: ${approveTx}`) + Logger.debug(TAG, `Transaction approved: ${util.inspect(approveTx.arguments)}`) const tx = exchangeContract.methods.exchange( convertedMakerAmount.toString(), diff --git a/packages/mobile/src/fees/saga.test.ts b/packages/mobile/src/fees/saga.test.ts index 0d2b3d9dc7d..04029f0a0da 100644 --- a/packages/mobile/src/fees/saga.test.ts +++ b/packages/mobile/src/fees/saga.test.ts @@ -18,6 +18,16 @@ jest.mock('@celo/walletkit', () => ({ }, })) +jest.mock('src/web3/contracts', () => ({ + web3: { + utils: { + fromWei: jest.fn((x: any) => x / 1e18), + toWei: jest.fn((x: any) => x * 1e18), + }, + }, + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe(estimateFeeSaga, () => { beforeAll(() => { jest.useRealTimers() diff --git a/packages/mobile/src/geth/consts.ts b/packages/mobile/src/geth/consts.ts index 344dc7b1653..af65dfb3768 100644 --- a/packages/mobile/src/geth/consts.ts +++ b/packages/mobile/src/geth/consts.ts @@ -1,11 +1,17 @@ export const WEI_PER_CELO = 1000000000000000000.0 export const UNLOCK_DURATION = 600 -// Valid sync mode values can be seen at https://github.com/celo-org/celo-blockchain/blob/8be27f7c044e35dbf63a42500a79805f1bddcfb8/mobile/geth.go#L43-L47 -// Anything invalid will cause Geth to panic and app to crash. -export const SYNC_MODE_LIGHT = 3 -// Value of 4 corresponds to a deprecated sync mode. -export const SYNC_MODE_ULTRALIGHT = 5 +export enum GethSyncMode { + // Sync mode to imply no local geth node but an infura-like setup. + // This mode is internal to Celo wallet app and won't be propagated to + // geth node. + ZeroSync = -1, + // Valid sync mode values can be seen at https://github.com/celo-org/celo-blockchain/blob/8be27f7c044e35dbf63a42500a79805f1bddcfb8/mobile/geth.go#L43-L47 + // Anything invalid will cause Geth to panic and app to crash. + Light = 3, + // Value of 4 corresponds to a deprecated sync mode. + Ultralight = 5, +} // Re-export from utils for convinience since we use these often export { diff --git a/packages/mobile/src/geth/network-config.ts b/packages/mobile/src/geth/network-config.ts index 9148cd3d303..4fac1eacbea 100644 --- a/packages/mobile/src/geth/network-config.ts +++ b/packages/mobile/src/geth/network-config.ts @@ -1,30 +1,29 @@ -import { SYNC_MODE_ULTRALIGHT } from 'src/geth/consts' -import { Testnets } from 'src/web3/testnets' +import { DEFAULT_SYNC_MODE, Testnets } from 'src/web3/testnets' export default { [Testnets.integration]: { nodeDir: `.${Testnets.integration}`, - syncMode: SYNC_MODE_ULTRALIGHT, + syncMode: DEFAULT_SYNC_MODE, blockchainApiUrl: 'https://integration-dot-celo-testnet.appspot.com/', }, [Testnets.alfajoresstaging]: { nodeDir: `.${Testnets.alfajoresstaging}`, - syncMode: SYNC_MODE_ULTRALIGHT, + syncMode: DEFAULT_SYNC_MODE, blockchainApiUrl: 'https://alfajoresstaging-dot-celo-testnet.appspot.com/', }, [Testnets.alfajores]: { nodeDir: `.${Testnets.alfajores}`, - syncMode: SYNC_MODE_ULTRALIGHT, + syncMode: DEFAULT_SYNC_MODE, blockchainApiUrl: 'https://alfajores-dot-celo-testnet-production.appspot.com/', }, [Testnets.pilot]: { nodeDir: `.${Testnets.pilot}`, - syncMode: SYNC_MODE_ULTRALIGHT, + syncMode: DEFAULT_SYNC_MODE, blockchainApiUrl: 'https://pilot-dot-celo-testnet-production.appspot.com/', }, [Testnets.pilotstaging]: { nodeDir: `.${Testnets.pilotstaging}`, - syncMode: SYNC_MODE_ULTRALIGHT, + syncMode: DEFAULT_SYNC_MODE, blockchainApiUrl: 'https://pilotstaging-dot-celo-testnet.appspot.com/', }, } diff --git a/packages/mobile/src/geth/saga.ts b/packages/mobile/src/geth/saga.ts index 876f05477b6..40f682d8325 100644 --- a/packages/mobile/src/geth/saga.ts +++ b/packages/mobile/src/geth/saga.ts @@ -11,6 +11,7 @@ import { InitializationState, isGethConnectedSelector } from 'src/geth/reducer' import { navigateToError } from 'src/navigator/NavigationService' import { restartApp } from 'src/utils/AppRestart' import Logger from 'src/utils/Logger' +import { isZeroSyncMode } from 'src/web3/contracts' const gethEmitter = new NativeEventEmitter(NativeModules.RNGeth) @@ -41,6 +42,9 @@ export function* waitForGethConnectivity() { } function* waitForGethInstance() { + if (isZeroSyncMode()) { + return GethInitOutcomes.SUCCESS + } try { const gethInstance = yield call(getGeth) if (gethInstance == null) { @@ -129,6 +133,12 @@ function createNewBlockChannel() { function* monitorGeth() { const newBlockChannel = yield createNewBlockChannel() + if (isZeroSyncMode()) { + yield put(setGethConnected(true)) + yield delay(GETH_MONITOR_DELAY) + return + } + while (true) { try { const { newBlock } = yield race({ diff --git a/packages/mobile/src/home/CeloDollarsOverview.test.tsx b/packages/mobile/src/home/CeloDollarsOverview.test.tsx index fee9ed981e0..0e8be90d443 100644 --- a/packages/mobile/src/home/CeloDollarsOverview.test.tsx +++ b/packages/mobile/src/home/CeloDollarsOverview.test.tsx @@ -4,6 +4,10 @@ import * as renderer from 'react-test-renderer' import CeloDollarsOverview from 'src/home/CeloDollarsOverview' import { createMockStore } from 'test/utils' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('CeloDollarsOverview', () => { it('renders correctly', () => { const tree = renderer.create( diff --git a/packages/mobile/src/home/NotificationBox.test.tsx b/packages/mobile/src/home/NotificationBox.test.tsx index 7ed27c174cf..55e31e18cdc 100644 --- a/packages/mobile/src/home/NotificationBox.test.tsx +++ b/packages/mobile/src/home/NotificationBox.test.tsx @@ -19,6 +19,15 @@ const storeData = { }, } +jest.mock('src/web3/contracts', () => ({ + web3: { + utils: { + fromWei: jest.fn((x: any) => x / 1e18), + }, + }, + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('NotificationBox', () => { it('Simple test', () => { // const store = createMockStore(storeData) diff --git a/packages/mobile/src/home/WalletHome.test.tsx b/packages/mobile/src/home/WalletHome.test.tsx index c812edc72f8..b580739b170 100644 --- a/packages/mobile/src/home/WalletHome.test.tsx +++ b/packages/mobile/src/home/WalletHome.test.tsx @@ -20,6 +20,15 @@ const storeData = { jest.mock('src/components/AccountOverview') jest.mock('src/home/TransactionsList') +jest.mock('src/web3/contracts', () => ({ + web3: { + utils: { + fromWei: jest.fn((x: any) => x / 1e18), + }, + }, + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('Testnet banner', () => { it('Shows testnet banner for 5 seconds', async () => { const store = createMockStore({ diff --git a/packages/mobile/src/home/saga.test.ts b/packages/mobile/src/home/saga.test.ts index 58357d456fc..e15a4d831ff 100644 --- a/packages/mobile/src/home/saga.test.ts +++ b/packages/mobile/src/home/saga.test.ts @@ -16,6 +16,10 @@ import { getConnectedAccount } from 'src/web3/saga' jest.useRealTimers() +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('refreshBalances', () => { test('ask for balance when geth and account are ready', () => expectSaga(refreshBalances) diff --git a/packages/mobile/src/identity/commentKey.test.ts b/packages/mobile/src/identity/commentKey.test.ts index 95f6efd0a90..210f73d279f 100644 --- a/packages/mobile/src/identity/commentKey.test.ts +++ b/packages/mobile/src/identity/commentKey.test.ts @@ -11,6 +11,10 @@ jest.mock('@celo/walletkit', () => ({ sendTransaction: jest.fn(async () => null), })) +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('Encrypt Comment', () => { it('Empty comment', async () => { expect(await encryptComment('', 'toAddr', 'fromAddr')).toBe('') diff --git a/packages/mobile/src/identity/contactMapping.test.ts b/packages/mobile/src/identity/contactMapping.test.ts index ad0e7eaa284..b1376a93475 100644 --- a/packages/mobile/src/identity/contactMapping.test.ts +++ b/packages/mobile/src/identity/contactMapping.test.ts @@ -33,6 +33,10 @@ const e164NumberRecipients = recipients!.e164NumberToRecipients const otherRecipients = recipients!.otherRecipients const allRecipients = { ...e164NumberRecipients, ...otherRecipients } +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('Import Contacts Saga', () => { it('imports contacts and creates contact mappings correctly', async () => { const attestationsContract = createMockContract(attestationsStub) diff --git a/packages/mobile/src/identity/verification.test.ts b/packages/mobile/src/identity/verification.test.ts index 2401738fdad..3ca2985921c 100644 --- a/packages/mobile/src/identity/verification.test.ts +++ b/packages/mobile/src/identity/verification.test.ts @@ -50,6 +50,10 @@ jest.mock('@celo/utils', () => ({ SignatureUtils: { parseSignature: jest.fn(() => ({ r: 'r', s: 's', v: 'v' })) }, })) +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + const attestationCode0: AttestationCode = { code: 'ab8049b95ac02e989aae8b61fddc10fe9b3ac3c6aebcd3e68be495570b2d3da15aabc691ab88de69648f988fab653ac943f67404e532cfd1013627f56365f36501', diff --git a/packages/mobile/src/identity/verification.ts b/packages/mobile/src/identity/verification.ts index 12f6d57f89c..c18e4d8a179 100644 --- a/packages/mobile/src/identity/verification.ts +++ b/packages/mobile/src/identity/verification.ts @@ -399,7 +399,7 @@ function* revealNeededAttestations( ) { Logger.debug(TAG + '@revealNeededAttestations', `Revealing ${attestations.length} attestations`) yield all( - attestations.map((attestation) => { + attestations.map((attestation, index) => { return call( revealAndCompleteAttestation, attestationsContract, @@ -422,6 +422,7 @@ function* revealAndCompleteAttestation( Logger.debug(TAG + '@revealAttestation', `Revealing an attestation for issuer: ${issuer}`) CeloAnalytics.track(CustomEventNames.verification_reveal_attestation, { issuer }) const revealTx = yield call(makeRevealTx, attestationsContract, e164Number, issuer) + // Crude way to prevent sendTransaction being called in parallel and use the same nonces. yield call(sendTransaction, revealTx, account, TAG, `Reveal ${issuer}`) CeloAnalytics.track(CustomEventNames.verification_revealed_attestation, { issuer }) diff --git a/packages/mobile/src/import/ImportContacts.test.tsx b/packages/mobile/src/import/ImportContacts.test.tsx index fa586bc1f5c..61efcbf5093 100644 --- a/packages/mobile/src/import/ImportContacts.test.tsx +++ b/packages/mobile/src/import/ImportContacts.test.tsx @@ -4,6 +4,10 @@ import * as renderer from 'react-test-renderer' import ImportContacts from 'src/import/ImportContacts' import { createMockStore, getMockI18nProps } from 'test/utils' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('ImportContacts Screen', () => { it('renders correctly', () => { const store = createMockStore() diff --git a/packages/mobile/src/import/ImportWallet.test.tsx b/packages/mobile/src/import/ImportWallet.test.tsx index 9e69dc3424e..9c54bbc201b 100644 --- a/packages/mobile/src/import/ImportWallet.test.tsx +++ b/packages/mobile/src/import/ImportWallet.test.tsx @@ -17,6 +17,10 @@ jest.mock('src/geth/GethAwareButton', () => { return Button }) +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + const SPANISH_MNEMONIC = 'avance colmo poema momia cofre pata res verso secta cinco tubería yacer eterno observar ojo tabaco seta ruina bebé oral miembro gato suelo violín'.normalize( 'NFD' ) diff --git a/packages/mobile/src/invite/EnterInviteCode.test.tsx b/packages/mobile/src/invite/EnterInviteCode.test.tsx index 1e904d42f72..caf22d3fc7c 100644 --- a/packages/mobile/src/invite/EnterInviteCode.test.tsx +++ b/packages/mobile/src/invite/EnterInviteCode.test.tsx @@ -18,6 +18,10 @@ import { createMockStore, getMockI18nProps } from 'test/utils' SendIntentAndroid.openSMSApp = jest.fn() +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('EnterInviteCode Screen', () => { it('renders correctly', () => { const store = createMockStore() diff --git a/packages/mobile/src/invite/JoinCelo.test.tsx b/packages/mobile/src/invite/JoinCelo.test.tsx index ae8ca62221f..6981a708e12 100644 --- a/packages/mobile/src/invite/JoinCelo.test.tsx +++ b/packages/mobile/src/invite/JoinCelo.test.tsx @@ -8,6 +8,10 @@ import { ErrorMessages } from 'src/app/ErrorMessages' import JoinCelo, { JoinCelo as JoinCeloClass } from 'src/invite/JoinCelo' import { createMockStore, getMockI18nProps } from 'test/utils' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('JoinCeloScreen', () => { it('renders correctly', () => { const store = createMockStore() diff --git a/packages/mobile/src/invite/reducer.test.ts b/packages/mobile/src/invite/reducer.test.ts index e8da2c8cf07..f0a9005654a 100644 --- a/packages/mobile/src/invite/reducer.test.ts +++ b/packages/mobile/src/invite/reducer.test.ts @@ -2,6 +2,10 @@ import { storeInviteeData } from 'src/invite/actions' import { initialState, inviteReducer as reducer } from 'src/invite/reducer' import { mockAccount, mockE164Number } from 'test/values' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('invite/reducer', () => { describe(reducer, () => { it('returns the default state', () => { diff --git a/packages/mobile/src/invite/saga.test.ts b/packages/mobile/src/invite/saga.test.ts index 93e650875d6..540793015e9 100644 --- a/packages/mobile/src/invite/saga.test.ts +++ b/packages/mobile/src/invite/saga.test.ts @@ -50,6 +50,33 @@ jest.mock('src/transactions/send', () => ({ sendTransaction: async () => true, })) +jest.mock('src/web3/contracts', () => ({ + web3: { + eth: { + accounts: { + privateKeyToAccount: () => + '0x1129eb2fbccdc663f4923a6495c35b096249812b589f7c4cd1dba01e1edaf724', + wallet: { + add: () => null, + }, + create: () => ({ + address: '0x1129eb2fbccdc663f4923a6495c35b096249812b589f7c4cd1dba01e1edaf724', + privateKey: '0x1129eb2fbccdc663f4923a6495c35b096249812b589f7c4cd1dba01e1edaf724', + }), + }, + personal: { + importRawKey: () => '0x1129eb2fbccdc663f4923a6495c35b096249812b589f7c4cd1dba01e1edaf724', + unlockAccount: async () => true, + }, + }, + utils: { + fromWei: (x: any) => x / 1e18, + sha3: (x: any) => `a sha3 hash`, + }, + }, + isZeroSyncMode: () => false, +})) + SendIntentAndroid.sendSms = jest.fn() const state = createMockStore({ web3: { account: mockAccount } }).getState() diff --git a/packages/mobile/src/invite/saga.ts b/packages/mobile/src/invite/saga.ts index 44f1557982b..216d8dfb222 100644 --- a/packages/mobile/src/invite/saga.ts +++ b/packages/mobile/src/invite/saga.ts @@ -39,7 +39,7 @@ import { waitForTransactionWithId } from 'src/transactions/saga' import { sendTransaction } from 'src/transactions/send' import { dynamicLink } from 'src/utils/dynamicLink' import Logger from 'src/utils/Logger' -import { web3 } from 'src/web3/contracts' +import { addLocalAccount, isZeroSyncMode, web3 } from 'src/web3/contracts' import { getConnectedUnlockedAccount, getOrCreateAccount } from 'src/web3/saga' const TAG = 'invite/saga' @@ -212,7 +212,6 @@ export function* redeemInviteSaga({ inviteCode }: RedeemInviteAction) { yield put(redeemInviteFailure()) yield put(showError(ErrorMessages.REDEEM_INVITE_TIMEOUT)) } - Logger.debug(TAG, 'Done Redeem invite') } export function* doRedeemInvite(inviteCode: string) { @@ -220,7 +219,6 @@ export function* doRedeemInvite(inviteCode: string) { try { const tempAccount = web3.eth.accounts.privateKeyToAccount(inviteCode).address Logger.debug(`TAG@doRedeemInvite`, 'Invite code contains temp account', tempAccount) - const tempAccountBalanceWei: BigNumber = yield call(getAccountBalance, tempAccount) if (tempAccountBalanceWei.isLessThanOrEqualTo(0)) { yield put(showError(ErrorMessages.EMPTY_INVITE_CODE)) @@ -246,9 +244,21 @@ export function* doRedeemInvite(inviteCode: string) { async function addTempAccountToWallet(inviteCode: string) { Logger.debug(TAG + '@addTempAccountToWallet', 'Attempting to add temp wallet') try { - // @ts-ignore - const tempAccount = await web3.eth.personal.importRawKey(stripHexLeader(inviteCode), TEMP_PW) - Logger.debug(TAG + '@addTempAccountToWallet', 'Account added', tempAccount) + let tempAccount: string | null = null + if (isZeroSyncMode()) { + tempAccount = web3.eth.accounts.privateKeyToAccount(inviteCode).address + Logger.debug( + TAG + '@redeemInviteCode', + 'web3 is connected:', + String(await web3.eth.net.isListening()) + ) + addLocalAccount(web3, inviteCode) + } else { + // Import account into the local geth node + // @ts-ignore + tempAccount = await web3.eth.personal.importRawKey(stripHexLeader(inviteCode), TEMP_PW) + } + Logger.debug(TAG + '@addTempAccountToWallet', 'Account added', tempAccount!) } catch (e) { if (e.toString().includes('account already exists')) { Logger.warn(TAG + '@addTempAccountToWallet', 'Account already exists, using it') @@ -274,8 +284,9 @@ export async function withdrawFundsFromTempAccount( newAccount: string ) { Logger.debug(TAG + '@withdrawFundsFromTempAccount', 'Unlocking temporary account') - await web3.eth.personal.unlockAccount(tempAccount, TEMP_PW, 600) - + if (!isZeroSyncMode()) { + await web3.eth.personal.unlockAccount(tempAccount, TEMP_PW, 600) + } const tempAccountBalance = new BigNumber(web3.utils.fromWei(tempAccountBalanceWei.toString())) Logger.debug(TAG + '@withdrawFundsFromTempAccount', 'Creating send transaction') diff --git a/packages/mobile/src/language/Language.test.tsx b/packages/mobile/src/language/Language.test.tsx index e1a0d8b718e..ef235bbf527 100644 --- a/packages/mobile/src/language/Language.test.tsx +++ b/packages/mobile/src/language/Language.test.tsx @@ -4,6 +4,10 @@ import * as renderer from 'react-test-renderer' import { Language } from 'src/language/Language' import { getMockI18nProps } from 'test/utils' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + it('renders correctly', () => { const navigation: any = { getParam: jest.fn() } const tree = renderer.create( diff --git a/packages/mobile/src/localCurrency/SelectLocalCurrency.test.tsx b/packages/mobile/src/localCurrency/SelectLocalCurrency.test.tsx index 905347cf2fd..809fa9c8b96 100644 --- a/packages/mobile/src/localCurrency/SelectLocalCurrency.test.tsx +++ b/packages/mobile/src/localCurrency/SelectLocalCurrency.test.tsx @@ -4,6 +4,10 @@ import { Provider } from 'react-redux' import SelectLocalCurrency from 'src/localCurrency/SelectLocalCurrency' import { createMockStore } from 'test/utils' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('SelectLocalCurrency', () => { it('renders correctly', () => { const { toJSON } = render( diff --git a/packages/mobile/src/btoa.ts b/packages/mobile/src/missingGlobals.ts similarity index 61% rename from packages/mobile/src/btoa.ts rename to packages/mobile/src/missingGlobals.ts index cf882050e11..feccdb07ea8 100644 --- a/packages/mobile/src/btoa.ts +++ b/packages/mobile/src/missingGlobals.ts @@ -1,6 +1,7 @@ // TODO remove, move to celo/utils export interface Global { btoa: any + URL: any self: any } @@ -9,3 +10,6 @@ if (typeof global.self === 'undefined') { global.self = global } global.btoa = require('Base64').btoa +// Without this, one will see a confusing error +// similar to https://imgur.com/a/7rnLIh5 +global.URL = require('whatwg-url').URL diff --git a/packages/mobile/src/notifications/EscrowedPaymentReminderNotification.test.tsx b/packages/mobile/src/notifications/EscrowedPaymentReminderNotification.test.tsx index 5e65d4ac75d..6e808a4377e 100644 --- a/packages/mobile/src/notifications/EscrowedPaymentReminderNotification.test.tsx +++ b/packages/mobile/src/notifications/EscrowedPaymentReminderNotification.test.tsx @@ -8,6 +8,10 @@ import { mockEscrowedPayment } from 'test/values' const store = createMockStore() +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('EscrowedPaymentReminderNotification', () => { it('renders correctly', () => { const tree = renderer.create( diff --git a/packages/mobile/src/notifications/PaymentrequestSummaryNotification.test.tsx b/packages/mobile/src/notifications/PaymentrequestSummaryNotification.test.tsx index fd90a879c82..447994a27b1 100644 --- a/packages/mobile/src/notifications/PaymentrequestSummaryNotification.test.tsx +++ b/packages/mobile/src/notifications/PaymentrequestSummaryNotification.test.tsx @@ -57,6 +57,10 @@ const fakeRequests = [ ] const store = createMockStore() +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('PaymentRequestSummaryNotification', () => { it('renders correctly', () => { const tree = renderer.create( diff --git a/packages/mobile/src/paymentRequest/PaymentRequestConfirmation.test.tsx b/packages/mobile/src/paymentRequest/PaymentRequestConfirmation.test.tsx index dbae2b9ef79..21932d6beaa 100644 --- a/packages/mobile/src/paymentRequest/PaymentRequestConfirmation.test.tsx +++ b/packages/mobile/src/paymentRequest/PaymentRequestConfirmation.test.tsx @@ -15,6 +15,15 @@ const store = createMockStore({ }, }) +jest.mock('src/web3/contracts', () => ({ + web3: { + utils: { + fromWei: jest.fn((x: any) => x / 1e18), + }, + }, + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('PaymentRequestConfirmation', () => { it('renders correctly for request payment confirmation', () => { const navigation = createMockNavigationProp({ diff --git a/packages/mobile/src/paymentRequest/PaymentRequestListItem.test.tsx b/packages/mobile/src/paymentRequest/PaymentRequestListItem.test.tsx index e83825a0be4..99028b7e9d6 100644 --- a/packages/mobile/src/paymentRequest/PaymentRequestListItem.test.tsx +++ b/packages/mobile/src/paymentRequest/PaymentRequestListItem.test.tsx @@ -19,6 +19,10 @@ const commonProps = { }, } +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('PaymentRequestListItem', () => { it('renders correctly', () => { // @ts-ignore -- kind is not assignable? diff --git a/packages/mobile/src/paymentRequest/PaymentRequestListScreen.test.tsx b/packages/mobile/src/paymentRequest/PaymentRequestListScreen.test.tsx index 15e5bc0c15b..5a5a6975694 100644 --- a/packages/mobile/src/paymentRequest/PaymentRequestListScreen.test.tsx +++ b/packages/mobile/src/paymentRequest/PaymentRequestListScreen.test.tsx @@ -29,6 +29,10 @@ const requests = [ }), ] +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + function testStore(paymentRequests: PaymentRequest[]) { return createMockStore({ stableToken: { balance: '120' }, diff --git a/packages/mobile/src/pincode/PincodeConfirmation.test.tsx b/packages/mobile/src/pincode/PincodeConfirmation.test.tsx index 5ac9d659ee2..05b370e548a 100644 --- a/packages/mobile/src/pincode/PincodeConfirmation.test.tsx +++ b/packages/mobile/src/pincode/PincodeConfirmation.test.tsx @@ -4,6 +4,10 @@ import * as renderer from 'react-test-renderer' import PincodeConfirmation from 'src/pincode/PincodeConfirmation' import { createMockNavigationProp, createMockStore } from 'test/utils' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('PincodeConfirmation', () => { it('renders correctly', () => { const navigation = createMockNavigationProp({ diff --git a/packages/mobile/src/pincode/PincodeSet.test.tsx b/packages/mobile/src/pincode/PincodeSet.test.tsx index 8bf9d0e762a..b0048758c7c 100644 --- a/packages/mobile/src/pincode/PincodeSet.test.tsx +++ b/packages/mobile/src/pincode/PincodeSet.test.tsx @@ -4,6 +4,10 @@ import { Provider } from 'react-redux' import PincodeSet from 'src/pincode/PincodeSet' import { createMockStore } from 'test/utils' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('Pincode', () => { it('renders correctly', () => { const { toJSON, getByTestId } = render( diff --git a/packages/mobile/src/qrcode/QRCode.test.tsx b/packages/mobile/src/qrcode/QRCode.test.tsx index b101d4a7547..f5c4bc1c2f3 100644 --- a/packages/mobile/src/qrcode/QRCode.test.tsx +++ b/packages/mobile/src/qrcode/QRCode.test.tsx @@ -14,6 +14,10 @@ const commonProps = { ...getMockI18nProps(), } +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('QRCode', () => { const store = createMockStore({ account: { name: mockName }, diff --git a/packages/mobile/src/recipients/RecipientItem.test.tsx b/packages/mobile/src/recipients/RecipientItem.test.tsx index 0bea0999e62..b74b5863ba6 100644 --- a/packages/mobile/src/recipients/RecipientItem.test.tsx +++ b/packages/mobile/src/recipients/RecipientItem.test.tsx @@ -4,6 +4,10 @@ import * as renderer from 'react-test-renderer' import RecipientItem from 'src/recipients/RecipientItem' import { mockRecipient } from 'test/values' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe(RecipientItem, () => { it('renders correctly', () => { const tree = renderer.create( diff --git a/packages/mobile/src/recipients/recipient.test.ts b/packages/mobile/src/recipients/recipient.test.ts index 513bce852ca..f9b0460e030 100644 --- a/packages/mobile/src/recipients/recipient.test.ts +++ b/packages/mobile/src/recipients/recipient.test.ts @@ -1,6 +1,10 @@ import { contactsToRecipients, RecipientKind } from 'src/recipients/recipient' import { mockAccount, mockContactList, mockDisplayNumber, mockE164Number } from 'test/values' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('contactsToRecipients', () => { it('returns a recipient per phone number', () => { const countryCode = '+1' diff --git a/packages/mobile/src/redux/sagas.test.ts b/packages/mobile/src/redux/sagas.test.ts index a64b257ff29..c17bfbb7e75 100644 --- a/packages/mobile/src/redux/sagas.test.ts +++ b/packages/mobile/src/redux/sagas.test.ts @@ -2,6 +2,10 @@ import { expectSaga } from 'redux-saga-test-plan' import { withTimeout } from 'src/redux/sagas-helpers' import { sleep } from 'test/utils' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('withTimeout Saga', () => { test('returns the fn results if no timeout', () => expectSaga(withTimeout(50000, async () => ({ hello: 'world' }))) diff --git a/packages/mobile/src/send/SendAmount.test.tsx b/packages/mobile/src/send/SendAmount.test.tsx index 83de0e3ddae..76a102fea15 100644 --- a/packages/mobile/src/send/SendAmount.test.tsx +++ b/packages/mobile/src/send/SendAmount.test.tsx @@ -31,6 +31,10 @@ const storeData = { const TEXT_PLACEHOLDER = 'groceriesRent' const AMOUNT_PLACEHOLDER = 'amount' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('SendAmount', () => { beforeAll(() => { jest.useRealTimers() diff --git a/packages/mobile/src/send/SendConfirmation.test.tsx b/packages/mobile/src/send/SendConfirmation.test.tsx index 56eb58b8d3b..18ea4793f2e 100644 --- a/packages/mobile/src/send/SendConfirmation.test.tsx +++ b/packages/mobile/src/send/SendConfirmation.test.tsx @@ -11,6 +11,15 @@ const TEST_FEE = new BigNumber(10000000000000000) jest.mock('src/send/saga') +jest.mock('src/web3/contracts', () => ({ + web3: { + utils: { + fromWei: jest.fn((x: any) => x / 1e18), + }, + }, + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + const mockedGetSendFee = getSendFee as jest.Mock const store = createMockStore({ diff --git a/packages/mobile/src/send/TransferConfirmationCard.test.tsx b/packages/mobile/src/send/TransferConfirmationCard.test.tsx index db680a48457..cc2585633cd 100644 --- a/packages/mobile/src/send/TransferConfirmationCard.test.tsx +++ b/packages/mobile/src/send/TransferConfirmationCard.test.tsx @@ -20,6 +20,10 @@ const store = createMockStore({ }, }) +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('TransferConfirmationCard', () => { it('renders correctly for verification fee drilldown', () => { const props = { diff --git a/packages/mobile/src/send/TransferReviewCard.test.tsx b/packages/mobile/src/send/TransferReviewCard.test.tsx index 085ac221582..766a4a612f3 100644 --- a/packages/mobile/src/send/TransferReviewCard.test.tsx +++ b/packages/mobile/src/send/TransferReviewCard.test.tsx @@ -17,6 +17,15 @@ const store = createMockStore({ }, }) +jest.mock('src/web3/contracts', () => ({ + web3: { + utils: { + fromWei: jest.fn((x: any) => x / 1e18), + }, + }, + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('TransferReviewCard', () => { it('renders correctly for send review', () => { const props = { diff --git a/packages/mobile/src/send/saga.test.ts b/packages/mobile/src/send/saga.test.ts index 68824361c73..5dbcbfe6d9e 100644 --- a/packages/mobile/src/send/saga.test.ts +++ b/packages/mobile/src/send/saga.test.ts @@ -25,6 +25,15 @@ jest.mock('src/navigator/NavigationService', () => ({ navigate: jest.fn(), })) +jest.mock('src/web3/contracts', () => ({ + web3: { + utils: { + fromWei: jest.fn((x: any) => x / 1e18), + }, + }, + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + const { navigate } = require('src/navigator/NavigationService') describe(watchQrCodeDetections, () => { diff --git a/packages/mobile/src/shared/BackupPrompt.test.tsx b/packages/mobile/src/shared/BackupPrompt.test.tsx index c7cfa62ee16..f05c0181ada 100644 --- a/packages/mobile/src/shared/BackupPrompt.test.tsx +++ b/packages/mobile/src/shared/BackupPrompt.test.tsx @@ -6,6 +6,10 @@ import { getMockI18nProps } from 'test/utils' const time = 1552353116086 +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('BackupPrompt', () => { it('renders correctly', () => { const tree = renderer.create( diff --git a/packages/mobile/src/shared/DisconnectBanner.test.tsx b/packages/mobile/src/shared/DisconnectBanner.test.tsx index 552dfd07f45..01c3fb5c27b 100644 --- a/packages/mobile/src/shared/DisconnectBanner.test.tsx +++ b/packages/mobile/src/shared/DisconnectBanner.test.tsx @@ -4,6 +4,10 @@ import * as renderer from 'react-test-renderer' import DisconnectBanner from 'src/shared/DisconnectBanner' import { createMockStore, createMockStoreAppDisconnected } from 'test/utils' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + it('renders banner when app is disconnected', () => { const store = createMockStoreAppDisconnected() const tree = renderer.create( diff --git a/packages/mobile/src/stableToken/saga.test.ts b/packages/mobile/src/stableToken/saga.test.ts index d81baf75eb6..8c57cbe472a 100644 --- a/packages/mobile/src/stableToken/saga.test.ts +++ b/packages/mobile/src/stableToken/saga.test.ts @@ -29,6 +29,10 @@ jest.mock('src/web3/actions', () => ({ unlockAccount: jest.fn(async () => true), })) +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + const { unlockAccount } = require('src/web3/actions') const state = createMockStore().getState() diff --git a/packages/mobile/src/transactions/ExchangeFeedItem.test.tsx b/packages/mobile/src/transactions/ExchangeFeedItem.test.tsx index b31364a7cb2..31716785b97 100644 --- a/packages/mobile/src/transactions/ExchangeFeedItem.test.tsx +++ b/packages/mobile/src/transactions/ExchangeFeedItem.test.tsx @@ -8,6 +8,10 @@ import { ExchangeFeedItem } from 'src/transactions/ExchangeFeedItem' import { TransactionStatus, TransactionTypes } from 'src/transactions/reducer' import { createMockStore, getMockI18nProps } from 'test/utils' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('ExchangeFeedItem', () => { let dateNowSpy: any beforeAll(() => { diff --git a/packages/mobile/src/transactions/NoActivity.test.tsx b/packages/mobile/src/transactions/NoActivity.test.tsx index 071d2c5e5e5..b82e5395083 100644 --- a/packages/mobile/src/transactions/NoActivity.test.tsx +++ b/packages/mobile/src/transactions/NoActivity.test.tsx @@ -4,6 +4,10 @@ import * as renderer from 'react-test-renderer' import NoActivity from 'src/transactions/NoActivity' import { FeedType } from 'src/transactions/TransactionFeed' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + it('renders loading', () => { const tree = renderer.create() expect(tree).toMatchSnapshot() diff --git a/packages/mobile/src/transactions/TransactionFeed.test.tsx b/packages/mobile/src/transactions/TransactionFeed.test.tsx index 3209998fc5e..37816bf26d7 100644 --- a/packages/mobile/src/transactions/TransactionFeed.test.tsx +++ b/packages/mobile/src/transactions/TransactionFeed.test.tsx @@ -14,6 +14,10 @@ import { createMockStore } from 'test/utils' jest.mock('src/utils/time.ts') +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + const standbyTransactions: StandbyTransaction[] = [ { id: '0110', diff --git a/packages/mobile/src/transactions/TransferFeedItem.test.tsx b/packages/mobile/src/transactions/TransferFeedItem.test.tsx index d78239565a6..658303be78c 100644 --- a/packages/mobile/src/transactions/TransferFeedItem.test.tsx +++ b/packages/mobile/src/transactions/TransferFeedItem.test.tsx @@ -26,6 +26,10 @@ const invitee = { const encryptedMockComment = 'BAChYK3v1R/Y1ixIKqhpT6BW9AqigzaHfCl/MTu4Sg6fp1ckDUHR4qMOyxG3UiMe1GrlpJ+Ce66NJh6VemaWkHD7tU3TCbyUHsLHXBwJ0nBwLqt9Lvqrp4MO7unbFYCofqhjZKH+9g3OFBr6TwvSg/JaY7CZiSjq0FPiA+hcmScJBJl12DcGnB+cNl97n7tdCGQZj+LY/ktPdPzH9wUtTNx+UKDjHfF06pWRPd3d7k0rO+ww01cKuh+8aBdS1oMA8HPFUttM2pcigqD1uTWaD/LCnGjYR5nVfSj5luaI/lqinRGHcCPlFzmflqbS3kpaCM/dolP8By7UC8V8leQ3tMI/JsrusWTRFkctBTCEqmk/Pd8/ezPVae8813EisGlsDC7Uxq3VDhkPMTVwrT2NjplqQ6CCLQ4aKvFAdZEo3e/iJWlXa5RKMTiRmpNjb5vhlIC0bWnAkMC17r/5poawS3SjWR+5RLFD+vsj0x/gErZaUCXxOVdiR1CURh1qZ9VyEUTxm1ZnZpC+tg==' +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('transfer feed item renders correctly', () => { it('for sent transaction', () => { const tree = renderer.create( diff --git a/packages/mobile/src/transactions/saga.ts b/packages/mobile/src/transactions/saga.ts index dd07453ba2c..d19a8777a71 100644 --- a/packages/mobile/src/transactions/saga.ts +++ b/packages/mobile/src/transactions/saga.ts @@ -38,7 +38,7 @@ export function* sendAndMonitorTransaction( currency?: CURRENCY_ENUM ) { try { - Logger.debug(TAG + '@sendAndMonitorTransaction', 'Sending transaction with id: ', txId) + Logger.debug(TAG + '@sendAndMonitorTransaction', `Sending transaction with id: ${txId}`) const { transactionHash, confirmation } = yield call( sendTransactionPromises, diff --git a/packages/mobile/src/transactions/send.ts b/packages/mobile/src/transactions/send.ts index fa9bb79acbb..f9626a8945e 100644 --- a/packages/mobile/src/transactions/send.ts +++ b/packages/mobile/src/transactions/send.ts @@ -2,13 +2,15 @@ import { awaitConfirmation, getStableTokenContract, sendTransactionAsync, + sendTransactionAsyncWithWeb3Signing, SendTransactionLogEvent, SendTransactionLogEventType, } from '@celo/walletkit' import CeloAnalytics from 'src/analytics/CeloAnalytics' import { CustomEventNames } from 'src/analytics/constants' +import { DEFAULT_INFURA_URL } from 'src/config' import Logger from 'src/utils/Logger' -import { web3 } from 'src/web3/contracts' +import { isZeroSyncMode, web3 } from 'src/web3/contracts' import { TransactionObject } from 'web3/eth/types' // As per https://www.typescriptlang.org/docs/handbook/advanced-types.html#exhaustiveness-checking @@ -61,8 +63,38 @@ export const sendTransactionPromises = async ( txId: string, staticGas?: number | undefined ) => { + Logger.debug( + 'transactions/send@sendTransactionPromises', + `Going to send a transaction with id ${txId}` + ) const stableToken = await getStableTokenContract(web3) - return sendTransactionAsync(tx, account, stableToken, getLogger(tag, txId), staticGas) + // This if-else case is temprary and will disappear once we move from `walletkit` to `contractkit`. + if (isZeroSyncMode()) { + // In dev mode, verify that we are actually able to connect to the network. This + // ensures that we get a more meaningful error if the infura server is down, which + // can happen with networks without SLA guarantees like `integration`. + if (__DEV__) { + await verifyUrlWorksOrThrow(DEFAULT_INFURA_URL) + } + Logger.debug( + 'transactions/send@sendTransactionPromises', + `Sending transaction with id ${txId} using web3 signing` + ) + return sendTransactionAsyncWithWeb3Signing( + web3, + tx, + account, + stableToken, + getLogger(tag, txId), + staticGas + ) + } else { + Logger.debug( + 'transactions/send@sendTransactionPromises', + `Sending transaction with id ${txId} using geth signing` + ) + return sendTransactionAsync(tx, account, stableToken, getLogger(tag, txId), staticGas) + } } // Send a transaction and await for its confirmation @@ -76,3 +108,16 @@ export const sendTransaction = async ( ) => { return sendTransactionPromises(tx, account, tag, txId, staticGas).then(awaitConfirmation) } + +async function verifyUrlWorksOrThrow(url: string) { + try { + await fetch(url) + } catch (e) { + Logger.error( + 'contracts@verifyUrlWorksOrThrow', + `Failed to perform HEAD request to url: \"${url}\"`, + e + ) + throw new Error(`Failed to perform HEAD request to url: \"${url}\", is it working?`) + } +} diff --git a/packages/mobile/src/verify/Verify.test.tsx b/packages/mobile/src/verify/Verify.test.tsx index a4200af9d84..db9a1327332 100644 --- a/packages/mobile/src/verify/Verify.test.tsx +++ b/packages/mobile/src/verify/Verify.test.tsx @@ -11,6 +11,10 @@ import { mockAttestationMessage } from 'test/values' const store = createMockStore({}) +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + it('renders the Education step correctly', () => { const tree = renderer.create( diff --git a/packages/mobile/src/web3/actions.ts b/packages/mobile/src/web3/actions.ts index 88d9a8521f9..2b75e6c692d 100644 --- a/packages/mobile/src/web3/actions.ts +++ b/packages/mobile/src/web3/actions.ts @@ -76,11 +76,13 @@ export const updateWeb3SyncProgress = (payload: { export const checkSyncProgress = () => ({ type: Actions.REQUEST_SYNC_PROGRESS }) +// Note: This returns Promise export function getLatestBlock() { Logger.debug(TAG, 'Getting latest block') return web3.eth.getBlock('latest') } +// Note: This returns Promise export function getBlock(blockNumber: number) { Logger.debug(TAG, 'Getting block ' + blockNumber) return web3.eth.getBlock(blockNumber) diff --git a/packages/mobile/src/web3/contracts.ts b/packages/mobile/src/web3/contracts.ts index e1c6e889e22..baa06022feb 100644 --- a/packages/mobile/src/web3/contracts.ts +++ b/packages/mobile/src/web3/contracts.ts @@ -1,14 +1,25 @@ +import { addLocalAccount as web3utilsAddLocalAccount } from '@celo/walletkit' import { Platform } from 'react-native' import { DocumentDirectoryPath } from 'react-native-fs' import * as net from 'react-native-tcp' +import { DEFAULT_INFURA_URL } from 'src/config' +import { GethSyncMode } from 'src/geth/consts' +import config from 'src/geth/network-config' import Logger from 'src/utils/Logger' import { DEFAULT_TESTNET, Testnets } from 'src/web3/testnets' import Web3 from 'web3' +import { Provider } from 'web3/providers' // Logging tag const tag = 'web3/contracts' -const getWeb3IpcProvider = (testnet: Testnets) => { +export const web3: Web3 = getWeb3() + +export function isZeroSyncMode(): boolean { + return config[DEFAULT_TESTNET].syncMode === GethSyncMode.ZeroSync +} + +function getIpcProvider(testnet: Testnets) { Logger.debug(tag, 'creating IPCProvider...') const ipcProvider = new Web3.providers.IpcProvider( @@ -48,20 +59,54 @@ const getWeb3IpcProvider = (testnet: Testnets) => { return ipcProvider } -const getWeb3HttpProvider = (testnet: Testnets) => { - Logger.debug(tag, 'creating HttpProvider...') +// Use Http provider on iOS until we add support for local socket on iOS in react-native-tcp +function getWeb3HttpProviderForIos(): Provider { + Logger.debug(tag, 'creating HttpProvider for iOS...') const httpProvider = new Web3.providers.HttpProvider('http://localhost:8545') - Logger.debug(tag, 'created HttpProvider') + Logger.debug(tag, 'created HttpProvider for iOS') return httpProvider } -// Use Http provider on iOS until we add support for local socket on iOS in react-native-tcp -export const getWeb3Provider = Platform.OS === 'ios' ? getWeb3HttpProvider : getWeb3IpcProvider +function getWebSocketProvider(url: string): Provider { + Logger.debug(tag, 'creating HttpProvider...') + const provider = new Web3.providers.HttpProvider(url) + Logger.debug(tag, 'created HttpProvider') + // In the future, we might decide to over-ride the error handler via the following code. + // provider.on('error', () => { + // Logger.showError('Error occurred') + // }) + return provider +} -export const setWeb3Provider = (testnet: Testnets) => { - web3.setProvider(getWeb3Provider(testnet)) +function getWeb3(): Web3 { + Logger.info(`Initializing web3, platform: ${Platform.OS}, geth free mode: ${isZeroSyncMode()}`) + + if (isZeroSyncMode() && Platform.OS === 'ios') { + throw new Error('Zero sync mode is currently not supported on iOS') + } else if (isZeroSyncMode()) { + // Geth free mode + const url = DEFAULT_INFURA_URL + Logger.debug('contracts@getWeb3', `Connecting to url ${url}`) + return new Web3(getWebSocketProvider(url)) + } else if (Platform.OS === 'ios') { + // iOS + local geth + return new Web3(getWeb3HttpProviderForIos()) + } else { + return new Web3(getIpcProvider(DEFAULT_TESTNET)) + } } -export let web3 = new Web3(getWeb3Provider(DEFAULT_TESTNET)) +export function addLocalAccount(web3Instance: Web3, privateKey: string) { + if (!isZeroSyncMode()) { + throw new Error('addLocalAccount can only be called in Zero sync mode') + } + if (!web3Instance) { + throw new Error(`web3 instance is ${web3Instance}`) + } + if (!privateKey) { + throw new Error(`privateKey is ${privateKey}`) + } + web3utilsAddLocalAccount(web3Instance, privateKey) +} diff --git a/packages/mobile/src/web3/gas.test.ts b/packages/mobile/src/web3/gas.test.ts index 9f68c102778..34a1f5053c3 100644 --- a/packages/mobile/src/web3/gas.test.ts +++ b/packages/mobile/src/web3/gas.test.ts @@ -6,6 +6,10 @@ jest.mock('@celo/walletkit', () => ({ }, })) +jest.mock('src/web3/contracts', () => ({ + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), +})) + describe('getGasPrice', () => { it('refreshes the gas price correctly', async () => { const gasPrice = await getGasPrice() diff --git a/packages/mobile/src/web3/saga.test.ts b/packages/mobile/src/web3/saga.test.ts index 604d67892e3..1c1cce653fb 100644 --- a/packages/mobile/src/web3/saga.test.ts +++ b/packages/mobile/src/web3/saga.test.ts @@ -34,6 +34,7 @@ jest.mock('src/web3/contracts', () => ({ getBlock: jest.fn(() => ({ number: LAST_BLOCK_NUMBER })), }, }, + isZeroSyncMode: jest.fn().mockReturnValueOnce(false), })) const state = createMockStore({ web3: { account: mockAccount } }).getState() diff --git a/packages/mobile/src/web3/saga.ts b/packages/mobile/src/web3/saga.ts index b085f963771..9d31d742dc6 100644 --- a/packages/mobile/src/web3/saga.ts +++ b/packages/mobile/src/web3/saga.ts @@ -1,5 +1,7 @@ import { deriveCEK } from '@celo/utils/src/commentEncryption' +import { getAccountAddressFromPrivateKey } from '@celo/walletkit' import { generateMnemonic, mnemonicToSeedHex } from 'react-native-bip39' +import * as RNFS from 'react-native-fs' import { REHYDRATE } from 'redux-persist/es/constants' import { call, delay, put, race, select, take } from 'redux-saga/effects' import { setAccountCreationTime } from 'src/account/actions' @@ -23,7 +25,7 @@ import { setPrivateCommentKey, updateWeb3SyncProgress, } from 'src/web3/actions' -import { web3 } from 'src/web3/contracts' +import { addLocalAccount, isZeroSyncMode, web3 } from 'src/web3/contracts' import { currentAccountSelector } from 'src/web3/selectors' import { Block } from 'web3/eth/types' @@ -37,6 +39,11 @@ const BLOCK_CHAIN_CORRUPTION_ERROR = "Error: CONNECTION ERROR: Couldn't connect // checks if web3 claims it is currently syncing and attempts to wait for it to complete export function* checkWeb3SyncProgress() { + if (isZeroSyncMode()) { + // In this mode, the check seems to fail with + // web3/saga/checking web3 sync progress: Error: Invalid JSON RPC response: "": + return true + } while (true) { try { Logger.debug(TAG, 'checkWeb3SyncProgress', 'Checking sync progress') @@ -134,20 +141,34 @@ export function* assignAccountFromPrivateKey(key: string) { } let account: string - try { - // @ts-ignore - account = yield call(web3.eth.personal.importRawKey, String(key), pincode) - } catch (e) { - if (e.toString().includes('account already exists')) { - account = currentAccount - Logger.warn(TAG + '@assignAccountFromPrivateKey', 'Importing same account as current one') - } else { - Logger.error(TAG + '@assignAccountFromPrivateKey', 'Error importing raw key') - throw e + if (isZeroSyncMode()) { + const privateKey = String(key) + Logger.debug(TAG + '@assignAccountFromPrivateKey', 'Init web3 with private key') + addLocalAccount(web3, privateKey) + // Save the account to a local file on the disk. + // This is only required in Geth free mode because if geth is running + // it has its own mechanism to save the encrypted key in its keystore. + account = getAccountAddressFromPrivateKey(privateKey) + yield savePrivateKeyToLocalDisk(account, privateKey, pincode) + } else { + try { + // @ts-ignore + account = yield call(web3.eth.personal.importRawKey, String(key), pincode) + } catch (e) { + if (e.toString().includes('account already exists')) { + account = currentAccount + Logger.debug( + TAG + '@assignAccountFromPrivateKey', + 'Importing same account as current one' + ) + } else { + Logger.error(TAG + '@assignAccountFromPrivateKey', 'Error importing raw key') + throw e + } } + yield call(web3.eth.personal.unlockAccount, account, pincode, UNLOCK_DURATION) } - yield call(web3.eth.personal.unlockAccount, account, pincode, UNLOCK_DURATION) Logger.debug( TAG + '@assignAccountFromPrivateKey', `Created account from mnemonic and added to wallet: ${account}` @@ -157,7 +178,9 @@ export function* assignAccountFromPrivateKey(key: string) { yield put(setAccountCreationTime()) yield call(assignDataKeyFromPrivateKey, key) - web3.eth.defaultAccount = account + if (!isZeroSyncMode()) { + web3.eth.defaultAccount = account + } return account } catch (e) { Logger.error( @@ -174,6 +197,43 @@ function* assignDataKeyFromPrivateKey(key: string) { yield put(setPrivateCommentKey(privateCEK)) } +function getPrivateKeyFilePath(account: string): string { + return `${RNFS.DocumentDirectoryPath}/private_key_for_${account}.txt` +} + +function ensureAddressAndKeyMatch(address: string, privateKey: string) { + const generatedAddress = getAccountAddressFromPrivateKey(privateKey) + if (address.toLowerCase() !== generatedAddress.toLowerCase()) { + throw new Error( + `Address from private key: ${generatedAddress}, ` + `address of sender ${address}` + ) + } + console.debug(`signing-utils@ensureCorrectSigner: sender and private key match`) +} + +async function savePrivateKeyToLocalDisk( + account: string, + privateKey: string, + encryptionPassword: string +) { + ensureAddressAndKeyMatch(account, privateKey) + const filePath = getPrivateKeyFilePath(account) + Logger.debug('savePrivateKeyToLocalDisk', `Writing private key to ${filePath}`) + // TODO(ashishb): Store encrypted private key instead + await RNFS.writeFile(getPrivateKeyFilePath(account), privateKey) +} + +// Reads and returns unencrypted private key +export async function readPrivateKeyFromLocalDisk( + account: string, + encryptionPassword: string +): Promise { + const filePath = getPrivateKeyFilePath(account) + Logger.debug('readPrivateKeyFromLocalDisk', `Reading private key from ${filePath}`) + // TODO(ashishb): Read and decrypt private key instead + return RNFS.readFile(getPrivateKeyFilePath(account)) +} + // Wait for account to exist and then return it export function* getAccount() { while (true) { @@ -204,6 +264,8 @@ async function isLocked(address: string) { return false } +let accountAlreadyAddedInZeroSyncMode = false + export function* unlockAccount(account: string) { Logger.debug(TAG + '@unlockAccount', `Unlocking account: ${account}`) try { @@ -213,9 +275,21 @@ export function* unlockAccount(account: string) { } const pincode = yield call(getPincode) - yield call(web3.eth.personal.unlockAccount, account, pincode, UNLOCK_DURATION) - Logger.debug(TAG + '@unlockAccount', `Account unlocked: ${account}`) - return true + if (isZeroSyncMode()) { + if (accountAlreadyAddedInZeroSyncMode) { + Logger.info(TAG + 'unlockAccount', `Account ${account} already added to web3 for signing`) + } else { + Logger.info(TAG + '@unlockAccount', `unlockDuration is ignored in Geth free mode`) + const privateKey: string = yield readPrivateKeyFromLocalDisk(account, pincode) + addLocalAccount(web3, privateKey) + accountAlreadyAddedInZeroSyncMode = true + } + return true + } else { + yield call(web3.eth.personal.unlockAccount, account, pincode, UNLOCK_DURATION) + Logger.debug(TAG + '@unlockAccount', `Account unlocked: ${account}`) + return true + } } catch (error) { Logger.error(TAG + '@unlockAccount', 'Web3 account unlock failed', error) return false diff --git a/packages/mobile/src/web3/testnets.ts b/packages/mobile/src/web3/testnets.ts index 0664119e7cc..1cec4ced506 100644 --- a/packages/mobile/src/web3/testnets.ts +++ b/packages/mobile/src/web3/testnets.ts @@ -1,3 +1,5 @@ +import { GethSyncMode } from 'src/geth/consts' + import Config from 'react-native-config' export enum Testnets { @@ -9,3 +11,4 @@ export enum Testnets { } export const DEFAULT_TESTNET: Testnets = Config.DEFAULT_TESTNET +export const DEFAULT_SYNC_MODE: GethSyncMode = parseInt(Config.DEFAULT_SYNC_MODE, 10) diff --git a/packages/walletkit/index.ts b/packages/walletkit/index.ts index 26c4ef46dde..8b3e759e85c 100644 --- a/packages/walletkit/index.ts +++ b/packages/walletkit/index.ts @@ -64,6 +64,7 @@ export { SendTransaction, sendTransaction, sendTransactionAsync, + sendTransactionAsyncWithWeb3Signing, SendTransactionLogEvent, SendTransactionLogEventType, TxLogger, @@ -96,3 +97,4 @@ export { GoogleStorageUtils } export { Logger as WalletKitLogger, LogLevel as WalletKitLogLevel } export { StaticNodeUtils } export { Web3Utils } +export { addLocalAccount, getAccountAddressFromPrivateKey } from './src/new-web3-utils' diff --git a/packages/walletkit/src/contract-utils.ts b/packages/walletkit/src/contract-utils.ts index 01d5ecfe3db..8f8de6d9599 100644 --- a/packages/walletkit/src/contract-utils.ts +++ b/packages/walletkit/src/contract-utils.ts @@ -1,12 +1,17 @@ import BigNumber from 'bignumber.js' import { values } from 'lodash' +import sleep from 'sleep-promise' +import * as util from 'util' import Web3 from 'web3' import Contract from 'web3/eth/contract' import { TransactionObject } from 'web3/eth/types' import { TransactionReceipt } from 'web3/types' import * as ContractList from '../contracts/index' +import { GasPriceMinimum as GasPriceMinimumType } from '../types/GasPriceMinimum' import { GoldToken } from '../types/GoldToken' import { StableToken } from '../types/StableToken' +import { getGasPriceMinimumContract } from './contracts' +import { getGoldTokenAddress } from './erc20-utils' import { Logger } from './logger' const gasInflateFactor = 1.3 @@ -23,6 +28,11 @@ export function selectContractByAddress(contracts: Contract[], address: string) /** * Util function to send a transaction and log it's progression * Throws error on tx failure + * + * TODO(ashishb): This function won't work with locally signed transactions. + * I am not fixing it since we are going to move to contractkit soon and + * for now, mobile app only uses sendTransactionAsync function which works + * with locally signed transactions. */ // tslint:disable:ban-types export async function sendTransaction( @@ -204,7 +214,26 @@ function Exception(error: Error): Exception { return { type: SendTransactionLogEventType.Exception, error } } -// +async function getGasPrice( + web3: Web3, + gasCurrency: string | undefined +): Promise { + // Gold Token + if (gasCurrency === undefined) { + return String(await web3.eth.getGasPrice()) + } + const gasPriceMinimum: GasPriceMinimumType = await getGasPriceMinimumContract(web3) + const gasPrice: string = await gasPriceMinimum.methods.getGasPriceMinimum(gasCurrency).call() + Logger.debug('contract-utils@getGasPrice', `Gas price is ${gasPrice}`) + return String(parseInt(gasPrice, 10) * 10) +} + +// Maps account address to current nonce. This ensures that transactions +// being sent too close to each other do not end up having the +// same nonce. This does not have to be persisted across app restarts since +// nonce calculation will be correct over the order of seconds (restart time). +const currentNonce = new Map() + /** * sendTransactionAsync mainly abstracts the sending of a transaction in a promise like * interface. Use the higher-order sendTransactionFactory as a consumer to configure @@ -314,6 +343,204 @@ export async function sendTransactionAsync( } } +/** + * sendTransactionAsyncWithWeb3Signing is same as sendTransactionAsync except it fills + * in the missing fields and locally signs the transaction. It will fail if the `from` + * is not one of the account whose local signing keys are available. This method + * should only be used in Zero sync (infura-like) mode where Geth is running + * remotely. + * + * This separate function is temporary and contractkit uses a unified function + * for both web3 (local) and remote (geth) siging. + * + * @param tx The transaction object itself + * @param account The address from which the transaction should be sent + * @param gasCurrencyContract The contract instance of the Token in which to pay gas for + * @param logger An object whose log level functions can be passed a function to pass + * a transaction ID + */ +export async function sendTransactionAsyncWithWeb3Signing( + web3: Web3, + tx: TransactionObject, + account: string, + gasCurrencyContract: StableToken | GoldToken, + logger: TxLogger = emptyTxLogger, + estimatedGas?: number | undefined +): Promise { + const tag = 'contract-utils@sendTransactionAsyncWithWeb3Signing' + // @ts-ignore + const resolvers: TxPromiseResolvers = {} + // @ts-ignore + const rejectors: TxPromiseReject = {} + + const receipt: Promise = new Promise((resolve, reject) => { + resolvers.receipt = resolve + rejectors.receipt = reject + }) + + const transactionHash: Promise = new Promise((resolve, reject) => { + resolvers.transactionHash = resolve + rejectors.transactionHash = reject + }) + + const confirmation: Promise = new Promise((resolve, reject) => { + resolvers.confirmation = resolve + rejectors.confirmation = reject + }) + + const rejectAll = (error: Error) => { + values(rejectors).map((reject) => { + // @ts-ignore + reject(error) + }) + } + + try { + logger(Started) + const txParams: any = { + from: account, + gasCurrency: gasCurrencyContract._address, + gasPrice: '0', + } + + if (estimatedGas === undefined) { + estimatedGas = Math.round((await tx.estimateGas(txParams)) * gasInflateFactor) + logger(EstimatedGas(estimatedGas)) + } + // Ideally, we should fill these fields in CeloProvider but as of now, + // we don't have access to web3 inside it, so, in the short-term + // fill the fields here. + let gasCurrency = gasCurrencyContract._address + const gasFeeRecipient = await web3.eth.getCoinbase() + Logger.debug(tag, `Gas fee recipient is ${gasFeeRecipient}`) + const gasPrice = await getGasPrice(web3, gasCurrency) + if (gasCurrency === undefined) { + gasCurrency = await getGoldTokenAddress(web3) + } + Logger.debug(tag, `Gas currency: ${gasCurrency}`) + + let recievedTxHash: string | null = null + let alreadyInformedResolversAboutConfirmation = false + const informAboutConfirmation = () => { + // Don't inform more than once. + if (alreadyInformedResolversAboutConfirmation) { + Logger.debug(tag, `Already Informed Resolvers About Confirmation`) + return + } + alreadyInformedResolversAboutConfirmation = true + logger(Confirmed) + if (resolvers.confirmation) { + resolvers.confirmation(true) + } else { + Logger.debug(tag, 'resolver.confirmation is null') + } + } + + // Use pending transaction count, so that, multiple transactions can be sent without + // waiting for the earlier ones to be confirmed. + let nonce: number = await web3.eth.getTransactionCount(account, 'pending') + if (!currentNonce.has(account)) { + Logger.debug(tag, `Initializing current nonce for ${account} to ${nonce}`) + currentNonce.set(account, nonce) + } else if (nonce <= currentNonce.get(account)!) { + const newNonce = currentNonce.get(account)! + Logger.debug( + 'contract-utils@sendTransactionAsync', + `nonce is ${nonce} which is less than existing known nonce (${currentNonce.get( + account + )}), setting it to ${newNonce}` + ) + nonce = newNonce + } + Logger.debug(tag, `sendTransactionAsync@nonce is ${nonce}`) + Logger.debug(tag, `sendTransactionAsync@sending from ${account}`) + + const celoTx = { + from: account, + nonce, + // @ts-ignore + gasCurrency, + gas: estimatedGas, + // Hack to prevent web3 from adding the suggested gold gas price, allowing geth to add + // the suggested price in the selected gasCurrency. + gasPrice, + gasFeeRecipient, + } + // Increment and store nonce for the next call to sendTransaction. + currentNonce.set(account, nonce + 1) + try { + await tx.send(celoTx) + } catch (e) { + Logger.debug(tag, `Ignoring error: ${util.inspect(e)}`) + Logger.debug(tag, `error message: ${e.message}`) + // Ideally, I want to only ignore error whose messsage contains + // "Failed to subscribe to new newBlockHeaders" but seems like another wrapped + // error (listed below) gets thrown and there is no way to catch that. + // "Failed to subscribe to new newBlockHeaders" is thrown when the wallet kit is connected + // to a remote node via https. + // { [Error: [object Object]] + // line: 352771, + // column: 24, + // sourceURL: 'http://localhost:8081/index.delta?platform=android&dev=true&minify=false' } + // contract-utils@sendTransactionAsync: error: { [Error: Failed to subscribe to new newBlockHeaders to confi + // rm the transaction receipts. + // { + // "line": 352771, + // "column": 24, + // "sourceURL": "http://localhost:8081/index.delta?platform=android&dev=true&minify=false" + // }] + // line: 157373, + // column: 24, + // sourceURL: 'http://localhost:8081/index.delta?platform=android&dev=true&minify=false' } + if (e.message.indexOf('Failed to subscribe to new newBlockHeaders') >= 0) { + // Ignore this error + Logger.warn(tag, `Expected error ignored: ${JSON.stringify(e)}`) + } else { + Logger.debug(tag, `Unexpected error ignored: ${util.inspect(e)}`) + } + const signedTxn = await web3.eth.signTransaction(celoTx) + recievedTxHash = web3.utils.sha3(signedTxn.raw) + Logger.info(tag, `Locally calculated recievedTxHash is ${recievedTxHash}`) + logger(TransactionHashReceived(recievedTxHash)) + if (resolvers.transactionHash) { + resolvers.transactionHash(recievedTxHash) + } + } + // This code is required for infura-like setup. + // When mobile client directly connects to the remote full node then + // it gets `receipt` but not other notifications. + let sleepTimeInSecs = 1 + for (let i = 0; i < 10; i++) { + await sleep(sleepTimeInSecs * 1000) + // Exponential backoff + sleepTimeInSecs *= 2 + if (recievedTxHash === null) { + continue + } + const txReceipt = await web3.eth.getTransactionReceipt(recievedTxHash) + if (txReceipt === null) { + continue + } + const txStatus = txReceipt.status + Logger.info(tag, `Transaction status of hash ${recievedTxHash}: ${txStatus}`) + if (txStatus === true) { + informAboutConfirmation() + break + } + } + } catch (error) { + Logger.warn(tag, `Transaction failed with error "${util.inspect(error)}"`) + logger(Exception(error)) + rejectAll(error) + } + + return { + receipt, + transactionHash, + confirmation, + } +} + export type CeloContract = Contract & { name: string } export interface Contracts { [name: string]: CeloContract diff --git a/packages/walletkit/src/new-web3-utils.ts b/packages/walletkit/src/new-web3-utils.ts new file mode 100644 index 00000000000..2c8256e092e --- /dev/null +++ b/packages/walletkit/src/new-web3-utils.ts @@ -0,0 +1,161 @@ +import { + Callback, + ErrorCallback, + JSONRPCRequestPayload, + Subprovider, + Web3ProviderEngine, +} from '@0x/subproviders' +import * as util from 'util' +import Web3 from 'web3' +import { JsonRPCResponse, Provider } from 'web3/providers' +import { Logger } from './logger' +import { CeloProvider } from './transaction-utils' + +export function getAccountAddressFromPrivateKey(privateKey: string): string { + if (!privateKey.toLowerCase().startsWith('0x')) { + privateKey = '0x' + privateKey + } + return new Web3().eth.accounts.privateKeyToAccount(privateKey).address +} + +// This web3 has signing ability. +export function addLocalAccount(web3: Web3, privateKey: string) { + const celoProvider = new CeloProvider(privateKey) + const existingProvider = web3.currentProvider + let providerEngine: Web3ProviderEngine + if (existingProvider instanceof Web3ProviderEngine) { + Logger.debug( + 'new-web3-utils/addLocalAccount', + 'Existing provider is already a Web3ProviderEngine' + ) + providerEngine = addLocalAccountToExistingProvider( + existingProvider as Web3ProviderEngine, + celoProvider + ) + } else { + providerEngine = createNewProviderWithLocalAccount(existingProvider, celoProvider) + web3.setProvider(providerEngine) + } + + providerEngine.start() + Logger.debug('new-web3-utils/addLocalAccount', 'Providers configured') + providerEngine.stop() +} + +function addLocalAccountToExistingProvider( + existingProvider: Web3ProviderEngine, + localAccountProvider: CeloProvider +): Web3ProviderEngine { + if (isAccountAlreadyAdded(existingProvider, localAccountProvider)) { + return existingProvider + } + + // When a private key has already been added, add a new one before + // all other providers. + // I could not find a better way to do this, so, I had to + // access the private `_providers` field of providerEngine + // @ts-ignore-next-line + existingProvider._providers.splice(0, 0, localAccountProvider) + localAccountProvider.setEngine(existingProvider) + return existingProvider +} + +function createNewProviderWithLocalAccount( + existingProvider: Provider, + localAccountProvider: CeloProvider +): Web3ProviderEngine { + // Create a Web3 Provider Engine + const providerEngine = new Web3ProviderEngine() + // Compose our Providers, order matters + // Celo provider provides signing + providerEngine.addProvider(localAccountProvider) + // Use the existing provider to route all other requests + const subprovider = new SubproviderWithLogging(existingProvider) + Logger.debug('new-web3-utils/createNewProviderWithLocalAccount', 'Setting up providers...') + providerEngine.addProvider(subprovider) + return providerEngine +} + +function isAccountAlreadyAdded( + existingProvider: Web3ProviderEngine, + localAccountProvider: CeloProvider +): boolean { + const alreadyAddedAddresses: string[] = [] + // @ts-ignore-next-line + const providers = existingProvider._providers + for (const provider of providers) { + if (provider instanceof CeloProvider) { + const accounts: string[] = (provider as CeloProvider).getAccounts() + alreadyAddedAddresses.push.apply(alreadyAddedAddresses, accounts) + } + } + + const localAccountAddresses: string[] = localAccountProvider.getAccounts() + for (const localAccountAddress of localAccountAddresses) { + if (alreadyAddedAddresses.indexOf(localAccountAddress) < 0) { + Logger.debug( + 'new-web3-utils/isAccountAlreadyAdded', + `Account ${localAccountAddress} is not already added, ` + + `existing accounts are ${alreadyAddedAddresses}` + ) + return false + } + } + Logger.debug( + 'new-web3-utils/addLocalAccount', + `Account ${localAccountAddresses} is already added` + ) + return true +} + +class SubproviderWithLogging extends Subprovider { + private _provider: Provider + + constructor(readonly provider: Provider) { + super() + this._provider = provider + } + /** + * @param payload JSON RPC request payload + * @param next A callback to pass the request to the next subprovider in the stack + * @param end A callback called once the subprovider is done handling the request + */ + handleRequest( + payload: JSONRPCRequestPayload, + _next: Callback, + end: ErrorCallback + ): Promise { + Logger.debug( + 'new-web3-utils/addLocalAccount', + `SubproviderWithLogging@handleRequest: ${util.inspect(payload)}` + ) + // Inspired from https://github.com/MetaMask/web3-provider-engine/pull/19/ + return this._provider.send(payload, (err: null | Error, response?: JsonRPCResponse) => { + if (err != null) { + Logger.verbose( + 'new-web3-utils/addLocalAccount', + `SubproviderWithLogging@response is error: ${err}` + ) + end(err) + return + } + if (response == null) { + end(new Error(`Response is null for ${JSON.stringify(payload)}`)) + return + } + if (response.error != null) { + Logger.verbose( + 'new-web3-utils/addLocalAccount', + `SubproviderWithLogging@response includes error: ${response}` + ) + end(new Error(response.error)) + return + } + Logger.debug( + 'new-web3-utils/addLocalAccount', + `SubproviderWithLogging@response: ${util.inspect(response)}` + ) + end(null, response.result) + }) + } +} diff --git a/packages/walletkit/src/signing-utils.ts b/packages/walletkit/src/signing-utils.ts index 9a405e237e0..fee335763f2 100644 --- a/packages/walletkit/src/signing-utils.ts +++ b/packages/walletkit/src/signing-utils.ts @@ -1,12 +1,19 @@ import { account as Account, bytes as Bytes, hash as Hash, nat as Nat, RLP } from 'eth-lib' import { extend, isNull, isUndefined } from 'lodash' +import * as util from 'util' import * as helpers from 'web3-core-helpers' import * as utils from 'web3-utils' import { Logger } from './logger' +import { getAccountAddressFromPrivateKey } from './new-web3-utils' +import { CeloPartialTxParams } from './transaction-utils' // Original code taken from // https://github.com/ethereum/web3.js/blob/1.x/packages/web3-eth-accounts/src/index.js +// Debug-mode only: Turn this on to verify that the signing key matches the sender +// before signing as well as the recovered signer matches the original signer. +const DEBUG_MODE_CHECK_SIGNER = false + function isNot(value: any) { return isUndefined(value) || isNull(value) } @@ -25,19 +32,58 @@ function makeEven(hex: string) { return hex } -export async function signTransaction(txn: any, privateKey: string) { +function ensureCorrectSigner(sender: string, privateKey: string) { + if (DEBUG_MODE_CHECK_SIGNER) { + Logger.debug( + 'signing-utils@ensureCorrectSigner', + `Checking that sender (${sender}) and ${privateKey} match...` + ) + const generatedAddress = getAccountAddressFromPrivateKey(privateKey) + if (sender.toLowerCase() !== generatedAddress.toLowerCase()) { + throw new Error( + `Address from private key: ${generatedAddress}, ` + `address of sender ${sender}` + ) + } + Logger.debug('signing-utils@ensureCorrectSigner', 'sender and private key match') + } +} + +export async function signTransaction(txn: CeloPartialTxParams, privateKey: string) { + ensureCorrectSigner(txn.from, privateKey) + let result: any + Logger.debug('SigningUtils@signTransaction', `Received ${util.inspect(txn)}`) if (!txn) { throw new Error('No transaction object given!') } - const signed = (tx: any) => { + const signed = (tx: any): any => { + if (isNot(tx.gasCurrency)) { + Logger.info( + 'SigningUtils@signTransaction', + `Invalid transaction: Gas currency is \"${tx.gasCurrency}\"` + ) + throw new Error(`Invalid transaction: Gas currency is \"${tx.gasCurrency}\"`) + } + if (isNot(tx.gasFeeRecipient)) { + Logger.info( + 'SigningUtils@signTransaction', + `Invalid transaction: Gas fee recipient is \"${tx.gasFeeRecipient}\"` + ) + throw new Error(`Invalid transaction: Gas fee recipient is \"${tx.gasFeeRecipient}\"`) + } + if (!tx.gas && !tx.gasLimit) { + Logger.info( + 'SigningUtils@signTransaction', + `Invalid transaction: Gas is \"${tx.gas}\" and gas limit is \"${tx.gasLimit}\"` + ) throw new Error('"gas" is missing') } if (tx.nonce < 0 || tx.gas < 0 || tx.gasPrice < 0 || tx.chainId < 0) { + Logger.info('SigningUtils@signTransaction', 'Gas, gasPrice, nonce or chainId is lower than 0') throw new Error('Gas, gasPrice, nonce or chainId is lower than 0') } @@ -49,8 +95,6 @@ export async function signTransaction(txn: any, privateKey: string) { transaction.data = tx.data || '0x' transaction.value = tx.value || '0x' transaction.chainId = utils.numberToHex(tx.chainId) - transaction.gasCurrency = tx.gasCurrency || '0x' - transaction.gasFeeRecipient = tx.gasFeeRecipient || '0x' const rlpEncoded = RLP.encode([ Bytes.fromNat(transaction.nonce), @@ -95,6 +139,26 @@ export async function signTransaction(txn: any, privateKey: string) { throw e } + if (DEBUG_MODE_CHECK_SIGNER) { + Logger.debug( + 'transaction-utils@getRawTransaction@Signing', + `Signed result of \"${util.inspect(tx)}\" is \"${util.inspect(result)}\"` + ) + const recoveredSigner = recoverTransaction(result.rawTransaction).toLowerCase() + if (recoveredSigner !== txn.from) { + throw new Error( + `transaction-utils@getRawTransaction@Signing: Signer mismatch ${recoveredSigner} != ${ + txn.from + }, retrying...` + ) + } else { + Logger.debug( + 'transaction-utils@getRawTransaction@Signing', + `Recovered signer is same as sender, code is working correctly: ${txn.from}` + ) + } + } + return result } diff --git a/packages/walletkit/src/transaction-utils.ts b/packages/walletkit/src/transaction-utils.ts index f1b30e598bd..f5da2b4485b 100644 --- a/packages/walletkit/src/transaction-utils.ts +++ b/packages/walletkit/src/transaction-utils.ts @@ -1,8 +1,15 @@ -import { PartialTxParams, PrivateKeyWalletSubprovider } from '@0x/subproviders' +import { + Callback, + ErrorCallback, + JSONRPCRequestPayload, + PartialTxParams, + PrivateKeyWalletSubprovider, +} from '@0x/subproviders' import { BigNumber } from 'bignumber.js' import Web3 from 'web3' import { Tx } from 'web3/eth/types' import { Logger } from './logger' +import { getAccountAddressFromPrivateKey } from './new-web3-utils' import { signTransaction } from './signing-utils' export interface CeloTransaction extends Tx { @@ -21,6 +28,7 @@ export class CeloProvider extends PrivateKeyWalletSubprovider { } private readonly _celoPrivateKey: string // Always 0x prefixed + private readonly accountAddress: string // hex-encoded, lower case alphabets private _chainId: number | null = null constructor(privateKey: string) { @@ -28,6 +36,42 @@ export class CeloProvider extends PrivateKeyWalletSubprovider { super(CeloProvider.getPrivateKeyWithout0xPrefix(privateKey)) // Prefix 0x here or else the signed transaction produces dramatically different signer!!! this._celoPrivateKey = '0x' + CeloProvider.getPrivateKeyWithout0xPrefix(privateKey) + this.accountAddress = getAccountAddressFromPrivateKey(this._celoPrivateKey).toLowerCase() + Logger.debug('CeloProvider@construct', `CeloProvider Account address: ${this.accountAddress}`) + } + + public getAccounts(): string[] { + return [this.accountAddress] + } + + public async handleRequest( + payload: JSONRPCRequestPayload, + next: Callback, + end: ErrorCallback + ): Promise { + let signingRequired = false + switch (payload.method) { + case 'eth_sendTransaction': // fallthrough + case 'eth_signTransaction': // fallthrough + case 'eth_sign': // fallthrough + case 'personal_sign': // fallthrough + case 'eth_signTypedData': + signingRequired = true + } + // Either + // signing is not required or + // signing is required and this class is the correct one to sign + const shouldPassToSuperClassForHandling = + !signingRequired || + (payload.params[0].from !== undefined && + payload.params[0].from !== null && + payload.params[0].from.toLowerCase() === this.accountAddress) + if (shouldPassToSuperClassForHandling) { + return super.handleRequest(payload, next, end) + } else { + // Pass it to the next handler to sign + next() + } } public async signTransactionAsync(txParams: CeloPartialTxParams): Promise { diff --git a/packages/walletkit/test/transaction-utils.test.ts b/packages/walletkit/test/transaction-utils.test.ts index ad67dcb3d6f..a8f03ab0b72 100644 --- a/packages/walletkit/test/transaction-utils.test.ts +++ b/packages/walletkit/test/transaction-utils.test.ts @@ -1,4 +1,5 @@ import { BigNumber } from 'bignumber.js' +import { getGoldTokenAddress } from '../src/erc20-utils' import { Logger, LogLevel } from '../src/logger' import { recoverTransaction } from '../src/signing-utils' import { getRawTransaction } from '../src/transaction-utils' @@ -24,6 +25,7 @@ describe('Transaction Utils V2', () => { const to = accountAddress const amountInWei = new BigNumber(1e18) const gasFees = new BigNumber(1000 * 1000) + const gasCurrency = await getGoldTokenAddress(web3) const nonce = await web3.eth.getTransactionCount(from) const rawTransaction: string = await getRawTransaction( @@ -33,7 +35,9 @@ describe('Transaction Utils V2', () => { nonce, amountInWei, gasFees, - new BigNumber(gasPrice) + new BigNumber(gasPrice), + await web3.eth.getCoinbase(), + gasCurrency ) const recoveredSigner = recoverTransaction(rawTransaction) expect(recoveredSigner).toEqual(from) From c80f75500770c0f8f3b964ba678ab90ecfa50170 Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Wed, 2 Oct 2019 12:51:59 +0200 Subject: [PATCH 02/11] Add CLI commands around identity metadata (#1167) --- .../change-attestation-service-url.ts | 29 +++++ .../src/commands/identity/change-domain.ts | 30 +++++ .../cli/src/commands/identity/change-name.ts | 30 +++++ .../src/commands/identity/create-metadata.ts | 25 ++++ .../cli/src/commands/identity/get-metadata.ts | 23 +--- .../commands/identity/register-metadata.ts | 28 +++++ .../src/commands/identity/show-metadata.ts | 24 ++++ packages/cli/src/utils/command.ts | 31 +++++ packages/cli/src/utils/identity.ts | 41 +++++++ packages/contractkit/src/identity/metadata.ts | 27 +++-- .../docs/command-line-interface/identity.md | 114 +++++++++++++++++- 11 files changed, 373 insertions(+), 29 deletions(-) create mode 100644 packages/cli/src/commands/identity/change-attestation-service-url.ts create mode 100644 packages/cli/src/commands/identity/change-domain.ts create mode 100644 packages/cli/src/commands/identity/change-name.ts create mode 100644 packages/cli/src/commands/identity/create-metadata.ts create mode 100644 packages/cli/src/commands/identity/register-metadata.ts create mode 100644 packages/cli/src/commands/identity/show-metadata.ts create mode 100644 packages/cli/src/utils/identity.ts diff --git a/packages/cli/src/commands/identity/change-attestation-service-url.ts b/packages/cli/src/commands/identity/change-attestation-service-url.ts new file mode 100644 index 00000000000..dd40512a657 --- /dev/null +++ b/packages/cli/src/commands/identity/change-attestation-service-url.ts @@ -0,0 +1,29 @@ +import { createAttestationServiceURLClaim } from '@celo/contractkit/lib/identity' +import { IArg } from '@oclif/parser/lib/args' +import { BaseCommand } from '../../base' +import { Args, Flags } from '../../utils/command' +import { modifyMetadata } from '../../utils/identity' + +export default class ChangeAttestationServiceUrl extends BaseCommand { + static description = 'Change the URL of the attestation service in a local metadata file' + + static flags = { + ...BaseCommand.flags, + url: Flags.url({ + required: true, + description: 'The url you want to claim', + }), + } + + static args: IArg[] = [Args.file('file', { description: 'Path of the metadata file' })] + + static examples = ['change-attestation-service-url ~/metadata.json'] + + async run() { + const res = this.parse(ChangeAttestationServiceUrl) + modifyMetadata(res.args.file, (metadata) => { + const claim = createAttestationServiceURLClaim(res.flags.url) + metadata.addClaim(claim) + }) + } +} diff --git a/packages/cli/src/commands/identity/change-domain.ts b/packages/cli/src/commands/identity/change-domain.ts new file mode 100644 index 00000000000..6a088f1b4c0 --- /dev/null +++ b/packages/cli/src/commands/identity/change-domain.ts @@ -0,0 +1,30 @@ +import { createDomainClaim } from '@celo/contractkit/lib/identity' +import { flags } from '@oclif/command' +import { IArg } from '@oclif/parser/lib/args' +import { BaseCommand } from '../../base' +import { Args } from '../../utils/command' +import { modifyMetadata } from '../../utils/identity' + +export default class ChangeDomain extends BaseCommand { + static description = 'Change the domain in a local metadata file' + + static flags = { + ...BaseCommand.flags, + domain: flags.string({ + required: true, + description: 'The domain you want to claim', + }), + } + + static args: IArg[] = [Args.file('file', { description: 'Path of the metadata file' })] + + static examples = ['change-domain ~/metadata.json'] + + async run() { + const res = this.parse(ChangeDomain) + modifyMetadata(res.args.file, (metadata) => { + const claim = createDomainClaim(res.flags.domain) + metadata.addClaim(claim) + }) + } +} diff --git a/packages/cli/src/commands/identity/change-name.ts b/packages/cli/src/commands/identity/change-name.ts new file mode 100644 index 00000000000..cd3769883eb --- /dev/null +++ b/packages/cli/src/commands/identity/change-name.ts @@ -0,0 +1,30 @@ +import { createNameClaim } from '@celo/contractkit/lib/identity' +import { flags } from '@oclif/command' +import { IArg } from '@oclif/parser/lib/args' +import { BaseCommand } from '../../base' +import { Args } from '../../utils/command' +import { modifyMetadata } from '../../utils/identity' + +export default class ChangeName extends BaseCommand { + static description = 'Change the name in a local metadata file' + + static flags = { + ...BaseCommand.flags, + name: flags.string({ + required: true, + description: 'The name you want to claim', + }), + } + + static args: IArg[] = [Args.file('file', { description: 'Path of the metadata file' })] + + static examples = ['change-name ~/metadata.json'] + + async run() { + const res = this.parse(ChangeName) + modifyMetadata(res.args.file, (metadata) => { + const claim = createNameClaim(res.flags.name) + metadata.addClaim(claim) + }) + } +} diff --git a/packages/cli/src/commands/identity/create-metadata.ts b/packages/cli/src/commands/identity/create-metadata.ts new file mode 100644 index 00000000000..0e3543e248d --- /dev/null +++ b/packages/cli/src/commands/identity/create-metadata.ts @@ -0,0 +1,25 @@ +import { IdentityMetadataWrapper } from '@celo/contractkit/lib/identity' +import { IArg } from '@oclif/parser/lib/args' +import { writeFileSync } from 'fs' +import { BaseCommand } from '../../base' +import { Args } from '../../utils/command' + +export default class CreateMetadata extends BaseCommand { + static description = 'Create an empty metadata file' + + static flags = { + ...BaseCommand.flags, + } + + static args: IArg[] = [ + Args.newFile('file', { description: 'Path where the metadata should be saved' }), + ] + + static examples = ['create-metadata ~/metadata.json'] + + async run() { + const { args } = this.parse(CreateMetadata) + const metadata = new IdentityMetadataWrapper(IdentityMetadataWrapper.emptyData) + writeFileSync(args.file, metadata.toString()) + } +} diff --git a/packages/cli/src/commands/identity/get-metadata.ts b/packages/cli/src/commands/identity/get-metadata.ts index 42f8ed52f4e..30f03a832d0 100644 --- a/packages/cli/src/commands/identity/get-metadata.ts +++ b/packages/cli/src/commands/identity/get-metadata.ts @@ -1,8 +1,8 @@ -import { ClaimTypes, IdentityMetadataWrapper } from '@celo/contractkit/lib/identity' +import { IdentityMetadataWrapper } from '@celo/contractkit/lib/identity' import { IArg } from '@oclif/parser/lib/args' -import moment from 'moment' import { BaseCommand } from '../../base' import { Args } from '../../utils/command' +import { displayMetadata } from '../../utils/identity' export default class GetMetadata extends BaseCommand { static description = 'Show information about an address' @@ -28,25 +28,8 @@ export default class GetMetadata extends BaseCommand { try { const metadata = await IdentityMetadataWrapper.fetchFromURL(metadataURL) - console.info('Metadata contains the following claims: \n') - - metadata.claims.forEach((claim) => { - switch (claim.payload.type) { - case ClaimTypes.ATTESTATION_SERVICE_URL: - console.info(`Attestation Service Claim`) - console.info(`URL: ${claim.payload.url}`) - break - case ClaimTypes.NAME: - console.info(`Name Claim`) - console.info(`Name: "${claim.payload.name}"`) - break - default: - break - } - - console.info(`(claim created ${moment.unix(claim.payload.timestamp).fromNow()})\n`) - }) + displayMetadata(metadata) } catch (error) { console.error('Metadata could not be retrieved from ', metadataURL) } diff --git a/packages/cli/src/commands/identity/register-metadata.ts b/packages/cli/src/commands/identity/register-metadata.ts new file mode 100644 index 00000000000..a2643e13936 --- /dev/null +++ b/packages/cli/src/commands/identity/register-metadata.ts @@ -0,0 +1,28 @@ +import { BaseCommand } from '../../base' +import { displaySendTx } from '../../utils/cli' +import { Flags } from '../../utils/command' + +export default class RegisterMetadata extends BaseCommand { + static description = 'Register metadata about an address' + + static flags = { + ...BaseCommand.flags, + from: Flags.address({ + required: true, + description: 'Addess of the account to set metadata for', + }), + url: Flags.url({ + required: true, + description: 'The url to the metadata you want to register', + }), + } + + static examples = ['register-metadata --url https://www.celo.org --from 0x0'] + + async run() { + const { flags } = this.parse(RegisterMetadata) + this.kit.defaultAccount = flags.from + const attestations = await this.kit.contracts.getAttestations() + await displaySendTx('registerMetadata', attestations.setMetadataURL(flags.url)) + } +} diff --git a/packages/cli/src/commands/identity/show-metadata.ts b/packages/cli/src/commands/identity/show-metadata.ts new file mode 100644 index 00000000000..94b52162b80 --- /dev/null +++ b/packages/cli/src/commands/identity/show-metadata.ts @@ -0,0 +1,24 @@ +import { IdentityMetadataWrapper } from '@celo/contractkit/lib/identity' +import { IArg } from '@oclif/parser/lib/args' +import { BaseCommand } from '../../base' +import { Args } from '../../utils/command' +import { displayMetadata } from '../../utils/identity' + +export default class ShowMetadata extends BaseCommand { + static description = 'Show the data in a local metadata file' + + static flags = { + ...BaseCommand.flags, + } + + static args: IArg[] = [Args.file('file', { description: 'Path of the metadata file' })] + + static examples = ['show-metadata ~/metadata.json'] + + async run() { + const res = this.parse(ShowMetadata) + const metadata = IdentityMetadataWrapper.fromFile(res.args.file) + console.info(`Metadata at ${res.args.file} contains the following claims: \n`) + displayMetadata(metadata) + } +} diff --git a/packages/cli/src/utils/command.ts b/packages/cli/src/utils/command.ts index 6b1aef1f4df..742a3b64596 100644 --- a/packages/cli/src/utils/command.ts +++ b/packages/cli/src/utils/command.ts @@ -1,5 +1,6 @@ import { flags } from '@oclif/command' import { IArg, ParseFn } from '@oclif/parser/lib/args' +import { pathExistsSync } from 'fs-extra' import Web3 from 'web3' import { failWith } from './cli' @@ -18,6 +19,28 @@ const parseAddress: ParseFn = (input) => { return failWith(`${input} is not a valid address`) } } + +const parsePath: ParseFn = (input) => { + if (pathExistsSync(input)) { + return input + } else { + return failWith(`File at "${input}" does not exist`) + } +} + +// from http://urlregex.com/ +const URL_REGEX = new RegExp( + /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/ +) + +const parseUrl: ParseFn = (input) => { + if (URL_REGEX.test(input)) { + return input + } else { + return failWith(`"${input}" is not a valid URL`) + } +} + type Omit = Pick> type ArgBuilder = (name: string, args?: Partial, 'name' | 'parse'>>) => IArg export function argBuilder(parser: ParseFn): ArgBuilder { @@ -40,8 +63,16 @@ export const Flags = { description: 'Public Key', helpValue: '0x', }), + url: flags.build({ + parse: parseUrl, + description: 'URL', + helpValue: 'htttps://www.celo.org', + }), } export const Args = { address: argBuilder(parseAddress), + file: argBuilder(parsePath), + // TODO: Check that the file path is possible + newFile: argBuilder((x) => x), } diff --git a/packages/cli/src/utils/identity.ts b/packages/cli/src/utils/identity.ts new file mode 100644 index 00000000000..e8b868918f6 --- /dev/null +++ b/packages/cli/src/utils/identity.ts @@ -0,0 +1,41 @@ +import { + ClaimTypes, + IdentityMetadata, + IdentityMetadataWrapper, +} from '@celo/contractkit/lib/identity' +import { writeFileSync } from 'fs' +import moment from 'moment' + +export const displayMetadata = (metadata: IdentityMetadata) => { + metadata.claims.forEach((claim) => { + switch (claim.payload.type) { + case ClaimTypes.ATTESTATION_SERVICE_URL: + console.info(`Attestation Service Claim`) + console.info(`URL: ${claim.payload.url}`) + break + case ClaimTypes.NAME: + console.info(`Name Claim`) + console.info(`Name: "${claim.payload.name}"`) + break + case ClaimTypes.DOMAIN: + console.info('Domain Claim') + console.info(`Domain: ${claim.payload.domain}`) + break + default: + console.info(`Unknown Claim`) + console.info(JSON.stringify(claim.payload)) + break + } + + console.info(`(claim created ${moment.unix(claim.payload.timestamp).fromNow()})\n`) + }) +} + +export const modifyMetadata = ( + filePath: string, + operation: (metadata: IdentityMetadataWrapper) => void +) => { + const metadata = IdentityMetadataWrapper.fromFile(filePath) + operation(metadata) + writeFileSync(filePath, metadata.toString()) +} diff --git a/packages/contractkit/src/identity/metadata.ts b/packages/contractkit/src/identity/metadata.ts index 9057ae74b72..f7a6fcd4534 100644 --- a/packages/contractkit/src/identity/metadata.ts +++ b/packages/contractkit/src/identity/metadata.ts @@ -1,11 +1,12 @@ import fetch from 'cross-fetch' import { isLeft } from 'fp-ts/lib/Either' +import { readFileSync } from 'fs' import * as t from 'io-ts' import { PathReporter } from 'io-ts/lib/PathReporter' export enum ClaimTypes { ATTESTATION_SERVICE_URL = 'ATTESTATION_SERVICE_URL', - DNS = 'DNS', + DOMAIN = 'DOMAIN', NAME = 'NAME', PROFILE_PICTURE = 'PROFILE_PICTURE', TWITTER = 'TWITTER', @@ -21,8 +22,8 @@ const AttestationServiceURLClaimType = t.type({ url: UrlType, }) -const DnsClaimType = t.type({ - type: t.literal(ClaimTypes.DNS), +const DomainClaimType = t.type({ + type: t.literal(ClaimTypes.DOMAIN), timestamp: TimestampType, domain: t.string, }) @@ -33,7 +34,7 @@ const NameClaimType = t.type({ name: t.string, }) -export const ClaimType = t.union([AttestationServiceURLClaimType, DnsClaimType, NameClaimType]) +export const ClaimType = t.union([AttestationServiceURLClaimType, DomainClaimType, NameClaimType]) export const SignedClaimType = t.type({ payload: ClaimType, signature: SignatureType, @@ -45,13 +46,13 @@ export const IdentityMetadataType = t.type({ export type SignedClaim = t.TypeOf export type AttestationServiceURLClaim = t.TypeOf -export type DnsClaim = t.TypeOf +export type DomainClaim = t.TypeOf export type NameClaim = t.TypeOf export type IdentityMetadata = t.TypeOf -export type Claim = AttestationServiceURLClaim | DnsClaim | NameClaim +export type Claim = AttestationServiceURLClaim | DomainClaim | NameClaim -type ClaimPayload = K extends typeof ClaimTypes.DNS - ? DnsClaim +type ClaimPayload = K extends typeof ClaimTypes.DOMAIN + ? DomainClaim : K extends typeof ClaimTypes.NAME ? NameClaim : AttestationServiceURLClaim const isOfType = (type: K) => ( @@ -73,6 +74,10 @@ export class IdentityMetadataWrapper { return this.fromRawString(await resp.text()) } + static fromFile(path: string) { + return this.fromRawString(readFileSync(path, 'utf-8')) + } + static fromRawString(rawData: string) { const data = JSON.parse(rawData) // TODO: We should validate: @@ -141,3 +146,9 @@ export const createNameClaim = (name: string): NameClaim => ({ timestamp: now(), type: ClaimTypes.NAME, }) + +export const createDomainClaim = (domain: string): DomainClaim => ({ + domain, + timestamp: now(), + type: ClaimTypes.DOMAIN, +}) diff --git a/packages/docs/command-line-interface/identity.md b/packages/docs/command-line-interface/identity.md index 511613919d1..07b85c8591d 100644 --- a/packages/docs/command-line-interface/identity.md +++ b/packages/docs/command-line-interface/identity.md @@ -1,9 +1,86 @@ --- -description: Show information about an address +description: Change the URL of the attestation service in a local metadata file --- ## Commands +### Change-attestation-service-url + +Change the URL of the attestation service in a local metadata file + +``` +USAGE + $ celocli identity:change-attestation-service-url FILE + +ARGUMENTS + FILE Path of the metadata file + +OPTIONS + --url=htttps://www.celo.org (required) The url you want to claim + +EXAMPLE + change-attestation-service-url ~/metadata.json +``` + +_See code: [packages/cli/src/commands/identity/change-attestation-service-url.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/identity/change-attestation-service-url.ts)_ + +### Change-domain + +Change the domain in a local metadata file + +``` +USAGE + $ celocli identity:change-domain FILE + +ARGUMENTS + FILE Path of the metadata file + +OPTIONS + --domain=domain (required) The domain you want to claim + +EXAMPLE + change-domain ~/metadata.json +``` + +_See code: [packages/cli/src/commands/identity/change-domain.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/identity/change-domain.ts)_ + +### Change-name + +Change the name in a local metadata file + +``` +USAGE + $ celocli identity:change-name FILE + +ARGUMENTS + FILE Path of the metadata file + +OPTIONS + --name=name (required) The name you want to claim + +EXAMPLE + change-name ~/metadata.json +``` + +_See code: [packages/cli/src/commands/identity/change-name.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/identity/change-name.ts)_ + +### Create-metadata + +Create an empty metadata file + +``` +USAGE + $ celocli identity:create-metadata FILE + +ARGUMENTS + FILE Path where the metadata should be saved + +EXAMPLE + create-metadata ~/metadata.json +``` + +_See code: [packages/cli/src/commands/identity/create-metadata.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/identity/create-metadata.ts)_ + ### Get-metadata Show information about an address @@ -20,3 +97,38 @@ EXAMPLE ``` _See code: [packages/cli/src/commands/identity/get-metadata.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/identity/get-metadata.ts)_ + +### Register-metadata + +Register metadata about an address + +``` +USAGE + $ celocli identity:register-metadata + +OPTIONS + --from=0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d (required) Addess of the account to set metadata for + --url=htttps://www.celo.org (required) The url to the metadata you want to register + +EXAMPLE + register-metadata --url https://www.celo.org --from 0x0 +``` + +_See code: [packages/cli/src/commands/identity/register-metadata.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/identity/register-metadata.ts)_ + +### Show-metadata + +Show the data in a local metadata file + +``` +USAGE + $ celocli identity:show-metadata FILE + +ARGUMENTS + FILE Path of the metadata file + +EXAMPLE + show-metadata ~/metadata.json +``` + +_See code: [packages/cli/src/commands/identity/show-metadata.ts](https://github.com/celo-org/celo-monorepo/tree/master/packages/cli/src/commands/identity/show-metadata.ts)_ From 59616469e941f9b54857025779e52dc28d360226 Mon Sep 17 00:00:00 2001 From: Rohit Dua <8ohit.dua@gmail.com> Date: Wed, 2 Oct 2019 18:35:37 +0200 Subject: [PATCH 03/11] Improve QR Code scan ability (#1036) * qr scan library upgrade + google ML function * QR scan check bounce * add white border to QR code image for scanning support * typescript changes QRGen code * typescript fixes qrGen * update test snapshot QRCode * add test for QRGen * minor change; review changes * update QRGen test * revert google ml function for qr scan * update QRGen tests * update QRGen tests * qrGen minor fix * merge master * minor fix QRGen * remove stale snapshot qrGen * gradle fix qrGen --- packages/mobile/android/app/build.gradle | 2 + packages/mobile/android/build.gradle | 6 + packages/mobile/package.json | 2 +- packages/mobile/src/index.d.ts | 1 + packages/mobile/src/qrcode/QRCode.tsx | 2 +- packages/mobile/src/qrcode/QRGen.tsx | 117 + .../src/qrcode/QRGenTests/QRGen.test.tsx | 50 + .../__snapshots__/QRGen.test.tsx.snap | 1971 +++++++++++++++++ packages/mobile/src/qrcode/QRScanner.tsx | 18 +- .../qrcode/__snapshots__/QRCode.test.tsx.snap | 21 +- packages/mobile/src/qrcode/utils.ts | 11 +- yarn.lock | 8 +- 12 files changed, 2186 insertions(+), 23 deletions(-) create mode 100644 packages/mobile/src/qrcode/QRGen.tsx create mode 100644 packages/mobile/src/qrcode/QRGenTests/QRGen.test.tsx create mode 100644 packages/mobile/src/qrcode/QRGenTests/__snapshots__/QRGen.test.tsx.snap diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle index 163969888ad..92bb7bddb05 100644 --- a/packages/mobile/android/app/build.gradle +++ b/packages/mobile/android/app/build.gradle @@ -129,6 +129,7 @@ android { testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "build_config_package", "org.celo.mobile" + missingDimensionStrategy 'react-native-camera', 'general' } signingConfigs { release { @@ -217,6 +218,7 @@ android { packagingOptions { exclude 'META-INF/-no-jdk.kotlin_module' + exclude 'META-INF/androidx.exifinterface_exifinterface.version' } } diff --git a/packages/mobile/android/build.gradle b/packages/mobile/android/build.gradle index 49d4d748c41..3a5899da172 100644 --- a/packages/mobile/android/build.gradle +++ b/packages/mobile/android/build.gradle @@ -43,6 +43,12 @@ allprojects { flatDir { dirs celoClientDirectory } + maven { + url "https://www.jitpack.io" + } + maven { + url "https://maven.google.com" + } } } diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 6d81a05cddd..c53adb707f4 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -82,7 +82,7 @@ "react-native-android-open-settings": "^1.2.0", "react-native-autocomplete-input": "^3.6.0", "react-native-bip39": "git://github.com/celo-org/react-native-bip39#1488fa1", - "react-native-camera": "^1.9.1", + "react-native-camera": "2.9.0", "react-native-clock-sync": "^1.0.0", "react-native-config": "https://github.com/luggit/react-native-config#89a602b", "react-native-confirm-device-credentials": "^2.0.0", diff --git a/packages/mobile/src/index.d.ts b/packages/mobile/src/index.d.ts index e6ff2f58ff1..bfc7f5a544c 100644 --- a/packages/mobile/src/index.d.ts +++ b/packages/mobile/src/index.d.ts @@ -43,3 +43,4 @@ declare module 'web3-core-helpers' declare module '@ungap/url-search-params' declare module 'react-native-install-referrer' declare module 'react-native-secure-key-store' +declare module 'qrcode' diff --git a/packages/mobile/src/qrcode/QRCode.tsx b/packages/mobile/src/qrcode/QRCode.tsx index 6be3ee41cad..6290884d679 100644 --- a/packages/mobile/src/qrcode/QRCode.tsx +++ b/packages/mobile/src/qrcode/QRCode.tsx @@ -7,7 +7,6 @@ import variables from '@celo/react-components/styles/variables' import * as React from 'react' import { WithNamespaces, withNamespaces } from 'react-i18next' import { StyleSheet, View } from 'react-native' -import QRCode from 'react-native-qrcode-svg' import { connect } from 'react-redux' import { UserContactDetails, userContactDetailsSelector } from 'src/account/reducer' import { AvatarSelf } from 'src/components/AvatarSelf' @@ -15,6 +14,7 @@ import i18n, { Namespaces } from 'src/i18n' import { headerWithBackButton } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' +import QRCode from 'src/qrcode/QRGen' import { RootState } from 'src/redux/reducers' import { shareQRCode, SVG } from 'src/send/actions' import { currentAccountSelector } from 'src/web3/selectors' diff --git a/packages/mobile/src/qrcode/QRGen.tsx b/packages/mobile/src/qrcode/QRGen.tsx new file mode 100644 index 00000000000..4062f96b373 --- /dev/null +++ b/packages/mobile/src/qrcode/QRGen.tsx @@ -0,0 +1,117 @@ +import _QRCode from 'qrcode' +import React, { PureComponent } from 'react' +import Svg, { Path, Rect } from 'react-native-svg' + +export function genMatrix(value: any, errorCorrectionLevel: string) { + const arr = Array.prototype.slice.call( + _QRCode.create(value, { errorCorrectionLevel }).modules.data, + 0 + ) + const sqrt = Math.sqrt(arr.length) + return arr.reduce( + (rows, key, index) => + (index % sqrt === 0 ? rows.push([key]) : rows[rows.length - 1].push(key)) && rows, + [] + ) +} + +/* calculate the size of the cell and draw the path */ +export function calculateMatrix(props: any) { + const { value, size, ecl, onError } = props + try { + const reducedSize = size - 20 + const matrix = genMatrix(value, ecl) + const cellSize = reducedSize / matrix.length + return { + cellSize, + path: transformMatrixIntoPath(cellSize, matrix), + } + } catch (error) { + if (onError && typeof onError === 'function') { + onError(error) + } else { + // Pass the error when no handler presented + throw error + } + } + return {} +} + +/* project the matrix into path draw */ +export function transformMatrixIntoPath(cellSize: number, matrix: any) { + // adjust origin + let d = '' + matrix.forEach((row: any, i: number) => { + let needDraw = false + row.forEach((column: any, j: number) => { + if (column) { + if (!needDraw) { + d += `M${cellSize * j + 10} ${cellSize / 2 + cellSize * i + 10} ` + needDraw = true + } + if (needDraw && j === matrix.length - 1) { + d += `L${cellSize * (j + 1) + 10} ${cellSize / 2 + cellSize * i + 10} ` + } + } else { + if (needDraw) { + d += `L${cellSize * j + 10} ${cellSize / 2 + cellSize * i + 10} ` + needDraw = false + } + } + }) + }) + return d +} + +interface QRProps { + value: string + size: number + color: string + backgroundColor: string + getRef: any + ecl: string + onError: any +} + +interface QRState { + cellSize?: number | undefined + path?: string | undefined +} + +/** + * A simple component for displaying QR Code using svg + */ +export default class QRCode extends PureComponent { + static defaultProps = { + value: 'This is a QR Code.', + size: 100, + color: 'black', + backgroundColor: 'white', + ecl: 'M', + onError: undefined, + } + + static getDerivedStateFromProps(props: any, state: any) { + // if value has changed, re-calculateMatrix + if (props.value !== state.value || props.size !== state.size) { + return calculateMatrix(props) + } + return null + } + + constructor(props: any) { + super(props) + this.state = calculateMatrix(props) + } + + render() { + const { getRef, size, color, backgroundColor } = this.props + const { cellSize, path } = this.state + return ( + + + {path && cellSize && } + + ) + } +} diff --git a/packages/mobile/src/qrcode/QRGenTests/QRGen.test.tsx b/packages/mobile/src/qrcode/QRGenTests/QRGen.test.tsx new file mode 100644 index 00000000000..6585ffa21de --- /dev/null +++ b/packages/mobile/src/qrcode/QRGenTests/QRGen.test.tsx @@ -0,0 +1,50 @@ +import * as React from 'react' +import * as renderer from 'react-test-renderer' +import QRCode, { genMatrix } from 'src/qrcode/QRGen' + +describe('QRCode', () => { + it('renders correctly', () => { + const tree = renderer.create().toJSON() + expect(tree).toMatchSnapshot() + }) + + // Simulate big data passed to QRCode and check if onError Callback + // Called properly + it('calls onError in case of issue with code generating', () => { + const onErrorMock = jest.fn() + // Rendering with big amount of data that should + // throw an exception + renderer.create( + + ) + expect(onErrorMock.mock.calls.length).toBe(2) + }) + + it('does not call onError in case if value is fine', () => { + const onErrorMock = jest.fn() + renderer.create() + expect(onErrorMock.mock.calls.length).toBe(0) + }) +}) + +describe('QRCode Matrix', () => { + it('generates with ecl:M correctly', () => { + const matrix = genMatrix('test', 'M') + expect(matrix).toMatchSnapshot() + }) + + it('generates with ecl:L correctly', () => { + const matrix = genMatrix('test', 'L') + expect(matrix).toMatchSnapshot() + }) + + it('generates with ecl:H correctly', () => { + const matrix = genMatrix('test', 'H') + expect(matrix).toMatchSnapshot() + }) + + it('generates with ecl:Q correctly', () => { + const matrix = genMatrix('test', 'Q') + expect(matrix).toMatchSnapshot() + }) +}) diff --git a/packages/mobile/src/qrcode/QRGenTests/__snapshots__/QRGen.test.tsx.snap b/packages/mobile/src/qrcode/QRGenTests/__snapshots__/QRGen.test.tsx.snap new file mode 100644 index 00000000000..4818cfc985f --- /dev/null +++ b/packages/mobile/src/qrcode/QRGenTests/__snapshots__/QRGen.test.tsx.snap @@ -0,0 +1,1971 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`QRCode Matrix generates with ecl:H correctly 1`] = ` +Array [ + Array [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + Array [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + ], + Array [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + ], + Array [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + Array [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + Array [ + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + ], + Array [ + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + ], + Array [ + 1, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + ], + Array [ + 0, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + ], + Array [ + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + ], + Array [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + ], + Array [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + ], + Array [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + ], + Array [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + ], + Array [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + ], +] +`; + +exports[`QRCode Matrix generates with ecl:L correctly 1`] = ` +Array [ + Array [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + Array [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + ], + Array [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + ], + Array [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + Array [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + Array [ + 1, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + ], + Array [ + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + ], + Array [ + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + ], + Array [ + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + ], + Array [ + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + ], + Array [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + ], + Array [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + ], + Array [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + ], + Array [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 1, + ], + Array [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + ], +] +`; + +exports[`QRCode Matrix generates with ecl:M correctly 1`] = ` +Array [ + Array [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + Array [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + ], + Array [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + ], + Array [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + Array [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + Array [ + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + ], + Array [ + 0, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + ], + Array [ + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + ], + Array [ + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + ], + Array [ + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + ], + Array [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + ], + Array [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 1, + ], + Array [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 1, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 1, + ], + Array [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + ], + Array [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + 1, + ], +] +`; + +exports[`QRCode Matrix generates with ecl:Q correctly 1`] = ` +Array [ + Array [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + Array [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 1, + 0, + 1, + ], + Array [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + ], + Array [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + Array [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + Array [ + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + ], + Array [ + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + ], + Array [ + 1, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 1, + 1, + 0, + ], + Array [ + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 0, + 0, + ], + Array [ + 0, + 0, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + ], + Array [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + ], + Array [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 0, + 1, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + ], + Array [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 0, + 0, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 1, + 0, + 0, + ], + Array [ + 1, + 0, + 1, + 1, + 1, + 0, + 1, + 0, + 1, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + ], + Array [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 0, + 0, + ], + Array [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 1, + 0, + ], +] +`; + +exports[`QRCode renders correctly 1`] = ` + + + + +`; diff --git a/packages/mobile/src/qrcode/QRScanner.tsx b/packages/mobile/src/qrcode/QRScanner.tsx index fbebf6ff60a..6c398cfaa38 100644 --- a/packages/mobile/src/qrcode/QRScanner.tsx +++ b/packages/mobile/src/qrcode/QRScanner.tsx @@ -17,6 +17,10 @@ import { handleBarcodeDetected } from 'src/send/actions' import { requestCameraPermission } from 'src/utils/androidPermissions' import Logger from 'src/utils/Logger' +enum BarcodeTypes { + QR_CODE = 'QR_CODE', +} + interface DispatchProps { handleBarcodeDetected: typeof handleBarcodeDetected } @@ -37,6 +41,7 @@ class QRScanner extends React.Component { state = { camera: false, + qrSubmitted: false, } async componentDidMount() { @@ -48,7 +53,15 @@ class QRScanner extends React.Component { navigate(Screens.QRCode) return } - this.setState({ camera: true }) + this.setState({ camera: true, qrSubmitted: false }) + } + + onBardCodeDetected = (rawData: any) => { + if (rawData.type === BarcodeTypes.QR_CODE && !this.state.qrSubmitted) { + this.setState({ qrSubmitted: true }, () => { + this.props.handleBarcodeDetected(rawData) + }) + } } render() { @@ -64,8 +77,9 @@ class QRScanner extends React.Component { // @ts-ignore style={styles.preview} type={RNCamera.Constants.Type.back} - onBarCodeRead={this.props.handleBarcodeDetected} + onBarCodeRead={this.onBardCodeDetected} barCodeTypes={[RNCamera.Constants.BarCodeType.qr]} + flashMode={RNCamera.Constants.FlashMode.auto} captureAudio={false} autoFocus={RNCamera.Constants.AutoFocus.on} > diff --git a/packages/mobile/src/qrcode/__snapshots__/QRCode.test.tsx.snap b/packages/mobile/src/qrcode/__snapshots__/QRCode.test.tsx.snap index c07b9e0dafe..7a7e96a54bd 100644 --- a/packages/mobile/src/qrcode/__snapshots__/QRCode.test.tsx.snap +++ b/packages/mobile/src/qrcode/__snapshots__/QRCode.test.tsx.snap @@ -103,12 +103,21 @@ exports[`QRCode renders correctly 1`] = ` - + + + + Date: Wed, 2 Oct 2019 10:20:45 -0700 Subject: [PATCH 04/11] Fix verification pool validation (#1176) * Update verification pool validatino * Use different env name in infura * Add https --- packages/mobile/package.json | 2 +- packages/verification-pool-api/.gitignore | 1 + packages/verification-pool-api/package.json | 4 +- packages/verification-pool-api/src/config.ts | 4 +- .../verification-pool-api/src/database.ts | 9 +- .../verification-pool-api/src/validation.ts | 94 +-------- yarn.lock | 199 ++++++++++++++++-- 7 files changed, 206 insertions(+), 107 deletions(-) diff --git a/packages/mobile/package.json b/packages/mobile/package.json index c53adb707f4..99b80cc07ed 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -46,7 +46,7 @@ ] }, "dependencies": { - "@celo/client": "ae27fd3", + "@celo/client": "640a41f", "@celo/react-native-sms-retriever": "git+https://github.com/celo-org/react-native-sms-retriever#d3a2fdb", "@celo/utils": "^0.1.0", "@celo/walletkit": "^0.0.14", diff --git a/packages/verification-pool-api/.gitignore b/packages/verification-pool-api/.gitignore index 7c5b492a6c3..27e8deb2f33 100644 --- a/packages/verification-pool-api/.gitignore +++ b/packages/verification-pool-api/.gitignore @@ -2,3 +2,4 @@ lib/ keys/ .firebase/ schema.json +firebase-debug.log \ No newline at end of file diff --git a/packages/verification-pool-api/package.json b/packages/verification-pool-api/package.json index b980438363a..bb3a18c742d 100644 --- a/packages/verification-pool-api/package.json +++ b/packages/verification-pool-api/package.json @@ -22,11 +22,13 @@ "test:verbose": "jest --ci --verbose --runInBand --detectOpenHandles" }, "dependencies": { + "@celo/contractkit": "^0.1.5", + "@celo/utils": "^0.1.0", "bignumber.js": "^7.2.0", "eth-lib": "^0.2.8", "ethereumjs-util": "^5.2.0", "express": "^4.16.4", - "firebase-admin": "^7.0.0", + "firebase-admin": "^8.6.0", "firebase-functions": "^3.2.0", "google-libphonenumber": "^3.2.4", "lodash": "^4.17.14", diff --git a/packages/verification-pool-api/src/config.ts b/packages/verification-pool-api/src/config.ts index d64419a957e..45182e5f433 100644 --- a/packages/verification-pool-api/src/config.ts +++ b/packages/verification-pool-api/src/config.ts @@ -16,13 +16,11 @@ export const twilioPhoneNum = functionConfig.shared['twilio-phone-number'] export const alwaysUseTwilio = functionConfig.shared['always-use-twilio'] === 'true' export const fcmKey = functionConfig.shared.fcmkey export const networkid = functionConfig[CELO_ENV]['testnet-id'] -export const hostIP = functionConfig[CELO_ENV]['tx-ip'] -export const hostPort = functionConfig[CELO_ENV]['tx-port'] export const appSignature = functionConfig[CELO_ENV]['app-signature'] export const smsAckTimeout = functionConfig[CELO_ENV]['sms-ack-timeout'] || 5000 // default 5 seconds // @ts-ignore -export const web3 = new Web3(`http://${hostIP}:${hostPort}`) +export const web3 = new Web3(`https://${CELO_ENV}-infura.celo-testnet.org`) let twilioClient: any let nexmoClient: any diff --git a/packages/verification-pool-api/src/database.ts b/packages/verification-pool-api/src/database.ts index 7488f580031..b06dd8691f7 100644 --- a/packages/verification-pool-api/src/database.ts +++ b/packages/verification-pool-api/src/database.ts @@ -76,9 +76,14 @@ export async function saveMessage( messageState: MessageState.DISPATCHING, } console.info('Saving new message to db') - return (await database() + const result = await database() .ref(`/${CELO_ENV}/messages`) - .push(message)).key + .push(message) + if (result.key) { + return result.key + } else { + throw new Error('Unable to save message') + } } export function deleteMessage(id: string) { diff --git a/packages/verification-pool-api/src/validation.ts b/packages/verification-pool-api/src/validation.ts index b56ae8b4325..ad1b111ec29 100644 --- a/packages/verification-pool-api/src/validation.ts +++ b/packages/verification-pool-api/src/validation.ts @@ -1,104 +1,22 @@ +import { newKitFromWeb3 } from '@celo/contractkit' import * as ethjsutil from 'ethereumjs-util' -// @ts-ignore -import * as Web3Utils from 'web3-utils' -import { getAttestations } from './config' +import { web3 } from './config' + +const kit = newKitFromWeb3(web3) export function parseBase64(source: string) { return ethjsutil.bufferToHex(Buffer.from(source, 'base64')) } -// TODO: Copied from @celo/utils, should be removed once usable as a dependency -function isE164Number(phoneNumber: string) { - const E164RegEx = /^\+[1-9][0-9]{1,14}$/ - return E164RegEx.test(phoneNumber) -} - -// TODO: Copied from @celo/utils, should be removed once usable as a dependency -function getPhoneHash(phoneNumber: string): string { - if (!phoneNumber || !isE164Number(phoneNumber)) { - throw Error('Attempting to hash a non-e164 number: ' + phoneNumber) - } - return Web3Utils.soliditySha3({ type: 'string', value: phoneNumber }) -} - -// TODO: Copied from @celo/utils, should be removed once usable as a dependency -function attestationMessageToSign(phoneHash: string, account: string) { - const messageHash: string = Web3Utils.soliditySha3( - { type: 'bytes32', value: phoneHash }, - { type: 'address', value: account } - ) - return messageHash -} - -// TODO: Copied from @celo/utils, should be removed once usable as a dependency -function parseSignatureAsVrs(signature: string) { - let v: number = parseInt(signature.slice(0, 2), 16) - const r: string = `0x${signature.slice(2, 66)}` - const s: string = `0x${signature.slice(66, 130)}` - if (v < 27) { - v += 27 - } - return { v, r, s } -} - -// TODO: Copied from @celo/utils, should be removed once usable as a dependency -function parseSignatureAsRsv(signature: string) { - const r: string = `0x${signature.slice(0, 64)}` - const s: string = `0x${signature.slice(64, 128)}` - let v: number = parseInt(signature.slice(128, 130), 16) - if (v < 27) { - v += 27 - } - return { r, s, v } -} - -// TODO: Copied from @celo/utils, should be removed once usable as a dependency -function isValidSignature(signer: string, message: string, v: number, r: string, s: string) { - try { - const publicKey = ethjsutil.ecrecover( - ethjsutil.toBuffer(message), - v, - ethjsutil.toBuffer(r), - ethjsutil.toBuffer(s) - ) - const retrievedAddress = ethjsutil.bufferToHex(ethjsutil.pubToAddress(publicKey)) - return signer === retrievedAddress - } catch (err) { - return false - } -} - -// TODO: Copied from @celo/utils, should be removed once usable as a dependency -function parseSignature(messageHash: string, signature: string, signer: string) { - let { r, s, v } = parseSignatureAsRsv(signature.slice(2)) - if (isValidSignature(signer, messageHash, v, r, s)) { - return { v, r, s } - } - - ;({ r, s, v } = parseSignatureAsVrs(signature.slice(2))) - if (isValidSignature(signer, messageHash, v, r, s)) { - return { v, r, s } - } - - throw new Error('Unable to parse signature') -} - export async function validateRequest( phoneNumber: string, account: string, message: string, issuer: string ) { - const attestations = await getAttestations() - const phoneHash = getPhoneHash(phoneNumber) - const expectedSourceMessage = attestationMessageToSign(phoneHash, account) - const { r, s, v } = parseSignature(expectedSourceMessage, message, issuer.toLowerCase()) - try { - const issuerFromSignature: string = await attestations.methods - .validateAttestationCode(phoneHash, account, v, r, s) - .call() - return issuerFromSignature.toLowerCase() === issuer.toLowerCase() + const attestations = await kit.contracts.getAttestations() + return await attestations.validateAttestationCode(phoneNumber, account, issuer, message) } catch (e) { console.error('Error validating attestation', e) return false diff --git a/yarn.lock b/yarn.lock index 8f984caea2f..b38486e9da9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2243,10 +2243,10 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@celo/client@ae27fd3": - version "0.0.138" - resolved "https://registry.yarnpkg.com/@celo/client/-/client-0.0.138.tgz#6ecae5fc9c84903d6308482fc042f24ad81a3a6d" - integrity sha512-aR6Uj2RChw/YOYARUk6p5N8gTKaoBGq2a5RZNOi1vuyP+r9tC3m+aDXNJIYtCwqUw0UG6cYpLndi9LTBAbbe6w== +"@celo/client@640a41f": + version "0.0.142" + resolved "https://registry.yarnpkg.com/@celo/client/-/client-0.0.142.tgz#ea15cf543f512c6d7ad1bc6611d3836c5bf06f40" + integrity sha512-F5CEEsKNegFJzb9btk9OpUWTSL9gDNAqoi4fEZ+fEeYJR6763PPW8CHub+KrwslALcCeuSQiRSFzvtuCq85SgA== "@celo/contractkit@0.1.1": version "0.1.1" @@ -2466,6 +2466,11 @@ resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.4.0.tgz#bb2c651f3b275fef549050cff28af752839c75c0" integrity sha512-8erNMHc0V26gA6Nj4W9laVrQrXHsj9K2TEM7eL2IQogGSHLL4vet3UNekYfcGQ2cjfvwUjMzd+BNS/8S7GnfiA== +"@firebase/app-types@0.4.4": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.4.4.tgz#6423e9b284018109d3658a6ecf7f82ca0e288bbb" + integrity sha512-s3rKk6aGnzNTx6bzdtOdJpg2XXMiadLk/hv/PjiFEI/m+8MEKz+wkl+eGwvzxFyOd6BD5GVy637boGFqRt20FQ== + "@firebase/app-types@^0.3.7": version "0.3.7" resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.3.7.tgz#9b00609ca7a992de77a62254296111ae05ee0128" @@ -2528,6 +2533,13 @@ resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.4.0.tgz#71a711a3f666fac905422e130731930e2bcca582" integrity sha512-2piRYW7t+2s/P1NPpcI/3+8Y5l2WnJhm9KACoXW5zmoAPlya8R1aEaR2dNHLNePTMHdg04miEDD9fEz4xUqzZA== +"@firebase/database-types@0.4.4": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.4.4.tgz#d2337413a5d88b326c199cb3008a5fccc9626001" + integrity sha512-n+CKrqfxKII6VMl44HFCPIB8xS78isRiA2V6SYJe4Mo6vauKggRBm8Tdt1SKmBco5zHhhYkhaYzU7bfPYm54Ag== + dependencies: + "@firebase/app-types" "0.4.4" + "@firebase/database@0.4.6": version "0.4.6" resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.4.6.tgz#eae68a5bee62119b12a44dab3691dbe641a3614f" @@ -2550,6 +2562,17 @@ faye-websocket "0.11.1" tslib "1.9.0" +"@firebase/database@^0.5.1": + version "0.5.5" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.5.5.tgz#f636dd04f955136e1f406f35b1dfe66375a1c7a4" + integrity sha512-rsOQCXoNLSKw7V2RL7xJhGAMS5bj8d/Y4/KfRwoaMsKUdfaogbZEdSgxMjKd00PKIiYa7TSAch0KziEujtAfuA== + dependencies: + "@firebase/database-types" "0.4.4" + "@firebase/logger" "0.1.25" + "@firebase/util" "0.2.28" + faye-websocket "0.11.3" + tslib "1.10.0" + "@firebase/firestore-types@1.4.1": version "1.4.1" resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-1.4.1.tgz#04a0863888293b05482afc789731729341475505" @@ -2614,6 +2637,11 @@ resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.1.17.tgz#7a70a8c605dfaa069771ca9663e30fd08a6b2513" integrity sha512-vCuurlKhvEjN5SGbIGfHhVhMsRM6RknAjbEKbT6CgmD6fUnNH2oE1MwbafBK3nLc7i9sQFwZUU/fi4P0Nu/McQ== +"@firebase/logger@0.1.25": + version "0.1.25" + resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.1.25.tgz#2f95832a62ae634a10242bdeedd84b62d9e5c8ef" + integrity sha512-/lRhuepVcCCnQ2jcO5Hr08SYdmZDTQU9fdPdzg+qXJ9k/QnIrD2RbswXQcL6mmae3uPpX7fFXQAoScJ9pzp50w== + "@firebase/logger@0.1.5": version "0.1.5" resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.1.5.tgz#c698f933cfe5d8c31f3c1425a38b54505cac9255" @@ -2689,6 +2717,13 @@ dependencies: tslib "1.9.3" +"@firebase/util@0.2.28": + version "0.2.28" + resolved "https://registry.yarnpkg.com/@firebase/util/-/util-0.2.28.tgz#9a17198b6ea416f1fb9edaea9f353d9a1f517f51" + integrity sha512-ZQMAWtXj8y5kvB6izs0aTM/jG+WO8HpqhXA/EwD6LckJ+1P5LnAhaLZt1zR4HpuCE+jeP5I32Id5RJ/aifFs6A== + dependencies: + tslib "1.10.0" + "@firebase/util@0.2.6": version "0.2.6" resolved "https://registry.yarnpkg.com/@firebase/util/-/util-0.2.6.tgz#03262ea2f94de60c4ce844069bdedcc7819d3dde" @@ -2751,7 +2786,7 @@ pify "^4.0.0" retry-request "^4.0.0" -"@google-cloud/common@^2.0.0": +"@google-cloud/common@^2.0.0", "@google-cloud/common@^2.1.1": version "2.2.2" resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-2.2.2.tgz#bac80e32f860cee64f02b6ab218264990e64c715" integrity sha512-AgMdDgLeYlEG17tXtMCowE7mplm907pcugtfJYYAp06HNe9RDnunUIY5KMnn9yikYl7NXNofARC+hwG77Zsa4g== @@ -2782,6 +2817,17 @@ protobufjs "^6.8.6" through2 "^3.0.0" +"@google-cloud/firestore@^2.0.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@google-cloud/firestore/-/firestore-2.3.0.tgz#079854c58fe43362595d5e54afd577bfc0059909" + integrity sha512-U5t5QSn+mAx9cw5QiIISDIVzICjxZ2o2+Qs/824I7BU6HkcElgxddgTcACF1z1xl2ZXxmz6NSBWmMr23Uhcilg== + dependencies: + bun "^0.0.12" + deep-equal "^1.0.1" + functional-red-black-tree "^1.0.1" + google-gax "^1.1.2" + through2 "^3.0.0" + "@google-cloud/logging@^5.3.1": version "5.3.1" resolved "https://registry.yarnpkg.com/@google-cloud/logging/-/logging-5.3.1.tgz#233908d6afb81ed4aff25090f898bcb7e526fd1e" @@ -3002,6 +3048,34 @@ through2 "^3.0.0" xdg-basedir "^3.0.0" +"@google-cloud/storage@^3.0.2": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-3.3.1.tgz#37fce6a4cc029556e71fa0b9bcdb9032241a892b" + integrity sha512-+OoVaJbETDh9cyDoRGe1MJE2Rhrd6JXjSP7GVpdxCUkwYw8pLll/Oz+84JEB82GebQlxG+GVHGcKYTVcSobB5g== + dependencies: + "@google-cloud/common" "^2.1.1" + "@google-cloud/paginator" "^2.0.0" + "@google-cloud/promisify" "^1.0.0" + arrify "^2.0.0" + compressible "^2.0.12" + concat-stream "^2.0.0" + date-and-time "^0.10.0" + duplexify "^3.5.0" + extend "^3.0.2" + gaxios "^2.0.1" + gcs-resumable-upload "^2.2.4" + hash-stream-validation "^0.2.1" + mime "^2.2.0" + mime-types "^2.0.8" + onetime "^5.1.0" + p-limit "^2.2.0" + pumpify "^2.0.0" + readable-stream "^3.4.0" + snakeize "^0.1.0" + stream-events "^1.0.1" + through2 "^3.0.0" + xdg-basedir "^4.0.0" + "@grpc/grpc-js@^0.3.0": version "0.3.5" resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-0.3.5.tgz#849235a60e961672232111cb221021242252206a" @@ -10623,6 +10697,18 @@ configstore@^4.0.0: write-file-atomic "^2.0.0" xdg-basedir "^3.0.0" +configstore@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.0.tgz#37de662c7a49b5fe8dbcf8f6f5818d2d81ed852b" + integrity sha512-eE/hvMs7qw7DlcB5JPRnthmrITuHMmACUJAp89v6PT6iOqzoLS7HRWhBtuHMlhNHo2AhUSA/3Dh1bKNJHcublQ== + dependencies: + dot-prop "^5.1.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + unique-string "^2.0.0" + write-file-atomic "^3.0.0" + xdg-basedir "^4.0.0" + connect-query@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/connect-query/-/connect-query-1.0.0.tgz#de44f577209da2404d1fc04692d1a4118e582119" @@ -11161,6 +11247,11 @@ crypto-random-string@^1.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + css-in-js-utils@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz#3b472b398787291b47cfe3e44fecfdd9e914ba99" @@ -11360,6 +11451,11 @@ data-urls@^1.0.0: whatwg-mimetype "^2.0.0" whatwg-url "^6.4.0" +date-and-time@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-0.10.0.tgz#53825b774167b55fbdf0bbd0f17f19357df7bc70" + integrity sha512-IbIzxtvK80JZOVsWF6+NOjunTaoFVYxkAQoyzmflJyuRCJAJebehy48mPiCAedcGp4P7/UO3QYRWa0fe6INftg== + date-fns@^1.27.2: version "1.30.1" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" @@ -11872,7 +11968,7 @@ diagnostics@^1.1.1: enabled "1.0.x" kuler "1.0.x" -dicer@0.3.0: +dicer@0.3.0, dicer@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA== @@ -12085,6 +12181,13 @@ dot-prop@^4.1.0, dot-prop@^4.2.0: dependencies: is-obj "^1.0.0" +dot-prop@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.1.0.tgz#bdd8c986a77b83e3fca524e53786df916cabbd8a" + integrity sha512-n1oC6NBF+KM9oVXtjmen4Yo7HyAVWV2UUl50dCYJdw2924K6dX9bf9TTTWaKtYlRn0FEtxG27KS80ayVLixxJA== + dependencies: + is-obj "^2.0.0" + dot-prop@~2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-2.4.0.tgz#848e28f7f1d50740c6747ab3cb07670462b6f89c" @@ -14557,6 +14660,20 @@ firebase-admin@^7.0.0: "@google-cloud/firestore" "^1.0.1" "@google-cloud/storage" "^2.3.0" +firebase-admin@^8.6.0: + version "8.6.0" + resolved "https://registry.yarnpkg.com/firebase-admin/-/firebase-admin-8.6.0.tgz#ed81d6b1a0b2b1f4033c47e33595e85634d0a68b" + integrity sha512-+JqOinU5bYUkg434LqEBXrHMrIBhL/+HwWEgbZpS1sBKHQRJK7LlcBrayqxvQKwJzgh5xs/JTInTmkozXk7h1w== + dependencies: + "@firebase/database" "^0.5.1" + "@types/node" "^8.0.53" + dicer "^0.3.0" + jsonwebtoken "8.1.0" + node-forge "0.7.4" + optionalDependencies: + "@google-cloud/firestore" "^2.0.0" + "@google-cloud/storage" "^3.0.2" + firebase-bolt@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/firebase-bolt/-/firebase-bolt-0.8.3.tgz#3e977275624f4d481aed54fea3bf54a305383310" @@ -15415,6 +15532,18 @@ gcs-resumable-upload@^1.0.0: pumpify "^1.5.1" stream-events "^1.0.4" +gcs-resumable-upload@^2.2.4: + version "2.2.5" + resolved "https://registry.yarnpkg.com/gcs-resumable-upload/-/gcs-resumable-upload-2.2.5.tgz#13e832130eb8c12be6d80f4bdc6fc0c410f73ad1" + integrity sha512-r98Hnxza8oYT21MzpziAB2thz3AURGz54+osWtczxGNxH7Fodb0HVUEtfqTwBS5vcf9RnKwR7c0EMaI8R39feg== + dependencies: + abort-controller "^3.0.0" + configstore "^5.0.0" + gaxios "^2.0.0" + google-auth-library "^5.0.0" + pumpify "^2.0.0" + stream-events "^1.0.4" + generate-function@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-1.1.0.tgz#54c21b080192b16d9877779c5bb81666e772365f" @@ -16219,7 +16348,7 @@ google-gax@^0.25.0: semver "^5.5.1" walkdir "^0.3.2" -google-gax@^1.0.0: +google-gax@^1.0.0, google-gax@^1.1.2: version "1.6.2" resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-1.6.2.tgz#1619d68b9f5573cf3e271ae0ac59c763f2271476" integrity sha512-khiWcACZ5m3b9x26gLgDZvO66OqxiRRaqHt+Zq2YMJVO5fHzkY247HAXdRTUDIqpEHfOz324FkY1ePiawyjn+g== @@ -18218,6 +18347,11 @@ is-obj@^1.0.0: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + is-object@^1.0.1, is-object@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" @@ -20855,6 +20989,13 @@ make-dir@^2.0.0, make-dir@^2.1.0: pify "^4.0.1" semver "^5.6.0" +make-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.0.tgz#1b5f39f6b9270ed33f9f054c5c0f84304989f801" + integrity sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw== + dependencies: + semver "^6.0.0" + make-error@1.x, make-error@^1.1.1: version "1.3.5" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" @@ -21665,6 +21806,11 @@ mimic-fn@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + mimic-response@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.0.tgz#df3d3652a73fded6b9b0b24146e6fd052353458e" @@ -23636,6 +23782,13 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" +onetime@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" + integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== + dependencies: + mimic-fn "^2.1.0" + open@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/open/-/open-6.3.0.tgz#60d0b845ee38fae0631f5d739a21bd40e3d2a527" @@ -30883,6 +31036,11 @@ tsconfig-paths@^3.8.0: minimist "^1.2.0" strip-bom "^3.0.0" +tslib@1.10.0, tslib@^1: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + tslib@1.9.0, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8" @@ -30893,11 +31051,6 @@ tslib@1.9.3, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== -tslib@^1: - version "1.10.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" - integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== - tslib@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.1.tgz#a5d1f0532a49221c87755cfcc89ca37197242ba7" @@ -31464,6 +31617,13 @@ unique-string@^1.0.0: dependencies: crypto-random-string "^1.0.0" +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + unique-temp-dir@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unique-temp-dir/-/unique-temp-dir-1.0.0.tgz#6dce95b2681ca003eebfb304a415f9cbabcc5385" @@ -33562,6 +33722,16 @@ write-file-atomic@^2.4.2: imurmurhash "^0.1.4" signal-exit "^3.0.2" +write-file-atomic@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.0.tgz#1b64dbbf77cb58fd09056963d63e62667ab4fb21" + integrity sha512-EIgkf60l2oWsffja2Sf2AL384dx328c0B+cIYPTQq5q2rOYuDV00/iPFBOUiDKKwKMOhkymH8AidPaRvzfxY+Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + write-file-atomic@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.1.4.tgz#b1f52dc2e8dc0e3cb04d187a25f758a38a90ca3b" @@ -33691,6 +33861,11 @@ xdg-basedir@^3.0.0: resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + xhr-request-promise@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/xhr-request-promise/-/xhr-request-promise-0.1.2.tgz#343c44d1ee7726b8648069682d0f840c83b4261d" From 1522211e2b4b93e4131ebbfcd699fdeb7f2ba0a3 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Wed, 2 Oct 2019 22:24:12 +0200 Subject: [PATCH 05/11] [Wallet] Add support for address pasting in send input field (#1180) * Add support for detecting addresses in a clipboard * Fix pasting into send field * Improve modularity and typing of text input components --- .../android/app/src/main/AndroidManifest.xml | 2 + .../__snapshots__/Invite.test.tsx.snap | 340 ++++++++++-------- .../mobile/src/recipients/RecipientPicker.tsx | 25 +- packages/mobile/src/send/LabeledTextInput.tsx | 106 ------ packages/mobile/src/send/SendAmount.tsx | 19 +- .../__snapshots__/SendAmount.test.tsx.snap | 15 +- .../components/PhoneNumberInput.tsx | 6 +- .../react-components/components/TextInput.tsx | 7 +- .../react-components/components/Touchable.tsx | 7 +- .../components/ValidatedTextInput.tsx | 69 ++-- .../components/WithTextInputLabeling.tsx | 88 +++++ .../components/WithTextInputPasteAware.tsx | 85 +++++ 12 files changed, 435 insertions(+), 334 deletions(-) delete mode 100644 packages/mobile/src/send/LabeledTextInput.tsx create mode 100644 packages/react-components/components/WithTextInputLabeling.tsx create mode 100644 packages/react-components/components/WithTextInputPasteAware.tsx diff --git a/packages/mobile/android/app/src/main/AndroidManifest.xml b/packages/mobile/android/app/src/main/AndroidManifest.xml index 95abf4ae8f9..edcb3ca60f7 100644 --- a/packages/mobile/android/app/src/main/AndroidManifest.xml +++ b/packages/mobile/android/app/src/main/AndroidManifest.xml @@ -10,6 +10,8 @@ + + diff --git a/packages/mobile/src/account/__snapshots__/Invite.test.tsx.snap b/packages/mobile/src/account/__snapshots__/Invite.test.tsx.snap index 5c9c9dcb039..5d9d366db17 100644 --- a/packages/mobile/src/account/__snapshots__/Invite.test.tsx.snap +++ b/packages/mobile/src/account/__snapshots__/Invite.test.tsx.snap @@ -18,108 +18,124 @@ exports[`Invite renders correctly with no recipients 1`] = ` > - - - - - - - + + + + + + + > + + } + onBlur={[Function]} + onChangeText={[Function]} + onFocus={[Function]} + placeholder="nameOrPhoneNumber" + placeholderTextColor="rgba(0, 0, 0, .4)" + rejectResponderTermination={true} + shouldShowClipboard={[Function]} + style={ + Array [ + Object { + "fontFamily": "Hind-Regular", + }, + Object { + "borderColor": "#D1D5D8", + "borderRadius": 3, + "padding": 8, + }, + Object { + "backgroundColor": "#FFFFFF", + "flex": 1, + }, + ] + } + underlineColorAndroid="transparent" + value="" + /> + - - - - - - - + + + + + + + > + + } + onBlur={[Function]} + onChangeText={[Function]} + onFocus={[Function]} + placeholder="nameOrPhoneNumber" + placeholderTextColor="rgba(0, 0, 0, .4)" + rejectResponderTermination={true} + shouldShowClipboard={[Function]} + style={ + Array [ + Object { + "fontFamily": "Hind-Regular", + }, + Object { + "borderColor": "#D1D5D8", + "borderRadius": 3, + "padding": 8, + }, + Object { + "backgroundColor": "#FFFFFF", + "flex": 1, + }, + ] + } + underlineColorAndroid="transparent" + value="" + /> + (TextInput) +) + const goToQrCodeScreen = () => { navigate(Screens.QRScanner) } @@ -235,11 +242,13 @@ export class RecipientPicker extends React.Component { return ( - } + style={style.textInput} + shouldShowClipboard={isValidAddress} /> {this.props.showQRCode && } { - state = { - active: false, - } - - onFocus = () => { - this.setState({ active: true }) - } - - onBlur = () => { - this.setState({ active: false }) - } - - render() { - const props = this.props - return ( - - - {props.title ? ( - - {props.title} - - ) : ( - - )} - - - - - ) - } -} - -const style = StyleSheet.create({ - titleContainer: { - minWidth: 45, - justifyContent: 'center', - alignItems: 'center', - flexDirection: 'column', - }, - title: { - color: colors.darkSecondary, - alignSelf: 'center', - lineHeight: 30, - }, - divider: { - width: 1, - height: 35, - }, - textInput: { - flex: 1, - height: 54, - marginHorizontal: 8, - }, -}) diff --git a/packages/mobile/src/send/SendAmount.tsx b/packages/mobile/src/send/SendAmount.tsx index c2b7a09d331..30f4cbdab85 100644 --- a/packages/mobile/src/send/SendAmount.tsx +++ b/packages/mobile/src/send/SendAmount.tsx @@ -1,5 +1,11 @@ import Button, { BtnTypes } from '@celo/react-components/components/Button' import LoadingLabel from '@celo/react-components/components/LoadingLabel' +import TextInput, { TextInputProps } from '@celo/react-components/components/TextInput' +import ValidatedTextInput, { + DecimalValidatorProps, + ValidatedTextInputProps, +} from '@celo/react-components/components/ValidatedTextInput' +import withTextInputLabeling from '@celo/react-components/components/WithTextInputLabeling' import colors from '@celo/react-components/styles/colors' import { fontStyles } from '@celo/react-components/styles/fonts' import { componentStyles } from '@celo/react-components/styles/styles' @@ -47,13 +53,17 @@ import { RecipientKind, } from 'src/recipients/recipient' import { RootState } from 'src/redux/reducers' -import LabeledTextInput from 'src/send/LabeledTextInput' import { ConfirmationInput } from 'src/send/SendConfirmation' import DisconnectBanner from 'src/shared/DisconnectBanner' import { fetchDollarBalance } from 'src/stableToken/actions' import { TransactionTypes } from 'src/transactions/reducer' import { getBalanceColor, getFeeDisplayValue, getMoneyDisplayValue } from 'src/utils/formatting' +const AmountInput = withTextInputLabeling>( + ValidatedTextInput +) +const CommentInput = withTextInputLabeling(TextInput) + interface State { amount: string reason: string @@ -367,13 +377,13 @@ export class SendAmount extends React.Component { labelTextStyle={fontStyles.center} /> - { validator={ValidatorKind.Decimal} lng={this.props.lng} /> - { {countryCallingCode} - style={[style.inputBox, style.phoneNumberInput]} placeholderTextColor={colors.inactive} onChangeText={this.onChangePhoneNumber} diff --git a/packages/react-components/components/TextInput.tsx b/packages/react-components/components/TextInput.tsx index 65d034af30f..b27b5a17c79 100644 --- a/packages/react-components/components/TextInput.tsx +++ b/packages/react-components/components/TextInput.tsx @@ -16,7 +16,7 @@ import { } from 'react-native' interface OwnProps { - onChangeText: (pin1: string) => void + onChangeText: (value: string) => void testID?: string showClearButton?: boolean forwardedRef?: React.RefObject @@ -60,7 +60,7 @@ export class CTextInput extends React.Component { value = '', showClearButton = true, forwardedRef, - ...props + ...passThroughProps } = this.props const { isFocused = false } = this.state @@ -71,7 +71,7 @@ export class CTextInput extends React.Component { ref={forwardedRef} style={[fontStyles.regular, style.borderedText, style.numberInput]} value={value} - {...props} + {...passThroughProps} onFocus={this.handleInputFocus} onBlur={this.handleInputBlur} /> @@ -98,6 +98,7 @@ const TextInput = React.forwardRef((props: Props, ref: React.RefObject { : Touchable.SelectableBackground() } render() { - const props = { ...this.props } - delete props.onPress + const { onPress, children, ...passThroughProps } = this.props return ( - - {this.props.children} + + {children} ) } diff --git a/packages/react-components/components/ValidatedTextInput.tsx b/packages/react-components/components/ValidatedTextInput.tsx index 59303bb8983..6e93b8f3c19 100644 --- a/packages/react-components/components/ValidatedTextInput.tsx +++ b/packages/react-components/components/ValidatedTextInput.tsx @@ -1,51 +1,52 @@ /** - * TextInput with input validation, interchangeable with `./TextInput.tsx` and - * React Native's TextInput if input validation is required. Set the - * `nativeInput` prop to `true` for RN TextInput replacement without visual - * changes. + * TextInput with input validation, interchangeable with `./TextInput.tsx` */ import TextInput from '@celo/react-components/components/TextInput' import { validateInput, ValidatorKind } from '@celo/utils/src/inputValidation' import * as React from 'react' -import { TextInput as RNTextInput, TextInputProps } from 'react-native' +import { KeyboardType, TextInputProps } from 'react-native' interface OwnProps { value: string + onChangeText: (input: string) => void + keyboardType: KeyboardType numberOfDecimals?: number placeholder?: string lng?: string - nativeInput?: boolean - onChangeText: (input: string) => void } -// Required props when validator type is phone -interface PhoneValidatorProps { +export interface PhoneValidatorProps { validator: ValidatorKind.Phone countryCallingCode: string - // Following props unused w/ 'phone' validator but required to be defined - customValidator?: CustomValidatorProps['customValidator'] } -interface NumberValidatorProps { - validator?: ValidatorKind.Integer | ValidatorKind.Decimal - // Following props unused w/ number validators but required to be defined - countryCallingCode?: string - customValidator?: CustomValidatorProps['customValidator'] +export interface IntegerValidatorProps { + validator: ValidatorKind.Integer +} + +export interface DecimalValidatorProps { + validator: ValidatorKind.Decimal + numberOfDecimals: number } -interface CustomValidatorProps { +// Required props for a custom input validator +export interface CustomValidatorProps { validator: ValidatorKind.Custom customValidator: (input: string) => string - // Following props unused w/ 'custom' but required to be defined - countryCallingCode?: string } -export type ValidatorProps = PhoneValidatorProps | NumberValidatorProps | CustomValidatorProps +export type ValidatorProps = + | PhoneValidatorProps + | IntegerValidatorProps + | DecimalValidatorProps + | CustomValidatorProps -type Props = OwnProps & ValidatorProps & TextInputProps +export type ValidatedTextInputProps = OwnProps & V & TextInputProps -export default class ValidatedTextInput extends React.Component { +export default class ValidatedTextInput extends React.Component< + ValidatedTextInputProps +> { onChangeText = (input: string): void => { const validated = validateInput(input, this.props) // Don't propagate change if new change is invalid @@ -53,38 +54,28 @@ export default class ValidatedTextInput extends React.Component { return } - if (!this.props.onChangeText) { - return + if (this.props.onChangeText) { + this.props.onChangeText(validated) } - - this.props.onChangeText(validated) } getMaxLength = () => { - if (!this.props.numberOfDecimals) { + const { numberOfDecimals, validator, value, lng } = this.props + + if (validator !== ValidatorKind.Decimal || !numberOfDecimals) { return undefined } - const { value, lng } = this.props - const decimalPos = lng && lng.startsWith('es') ? value.indexOf(',') : value.indexOf('.') if (decimalPos === -1) { return undefined } - return decimalPos + this.props.numberOfDecimals + 1 + return decimalPos + (numberOfDecimals as number) + 1 } render() { - const { nativeInput = false } = this.props - return nativeInput ? ( - - ) : ( + return ( ( + WrappedTextInput: React.ComponentType

+) { + return class WithTextInputLabeling extends React.Component

{ + state = { + active: false, + } + + onFocus = () => { + this.setState({ active: true }) + } + + onBlur = () => { + this.setState({ active: false }) + } + + render() { + const { icon, title, labelStyle } = this.props + const { active } = this.state + + if (!title && !icon) { + throw new Error('Must provide either icon or title for labeled text input') + } + + return ( + + + {icon ? ( + icon + ) : ( + {title} + )} + + + + + ) + } + } +} + +const style = StyleSheet.create({ + titleContainer: { + minWidth: 45, + justifyContent: 'center', + alignItems: 'center', + flexDirection: 'column', + }, + title: { + color: colors.darkSecondary, + alignSelf: 'center', + lineHeight: 30, + }, + divider: { + width: 1, + height: 35, + }, +}) diff --git a/packages/react-components/components/WithTextInputPasteAware.tsx b/packages/react-components/components/WithTextInputPasteAware.tsx new file mode 100644 index 00000000000..a11d3d4824a --- /dev/null +++ b/packages/react-components/components/WithTextInputPasteAware.tsx @@ -0,0 +1,85 @@ +// HOC to add a paste button to a text input + +import TouchableDefault from '@celo/react-components/components/Touchable' +import Copy from '@celo/react-components/icons/Copy' +import colors from '@celo/react-components/styles/colors' +import * as React from 'react' +import { AppState, Clipboard, StyleSheet, TextInputProps, View } from 'react-native' + +interface PasteAwareProps { + value: string + shouldShowClipboard: (value: string) => boolean + onChangeText: (text: string) => void +} + +export default function withTextInputPasteAware

( + WrappedTextInput: React.ComponentType

+) { + return class WithTextInputLabeling extends React.Component

{ + state = { + isPasteIconVisible: false, + } + + async componentDidMount() { + AppState.addEventListener('change', this.checkClipboardContents) + await this.checkClipboardContents() + } + + componentWillUnmount() { + AppState.removeEventListener('change', this.checkClipboardContents) + } + + checkClipboardContents = async () => { + try { + const clipboardContent = await Clipboard.getString() + if ( + clipboardContent && + clipboardContent !== this.props.value && + this.props.shouldShowClipboard(clipboardContent) + ) { + this.setState({ isPasteIconVisible: true }) + } else { + this.setState({ isPasteIconVisible: false }) + } + } catch (error) { + console.error('Error checking clipboard contents', error) + } + } + + onPressPate = async () => { + const clipboardContents = await Clipboard.getString() + this.props.onChangeText(clipboardContents) + this.setState({ isPasteIconVisible: false }) + } + + render() { + const { isPasteIconVisible } = this.state + + // TODO(Rossy) Use a more paste-y instead of copy looking icon when we have one + return ( + + + {isPasteIconVisible && ( + + + + )} + + ) + } + } +} + +const style = StyleSheet.create({ + container: { + position: 'relative', + }, + pasteIconContainer: { + position: 'absolute', + right: 16, + top: 18, + width: 20, + height: 25, + zIndex: 100, + }, +}) From b53f2d0781039befd2171320f0b96713c77f9fdb Mon Sep 17 00:00:00 2001 From: Ashish Bhatia Date: Wed, 2 Oct 2019 13:41:54 -0700 Subject: [PATCH 06/11] [codecov]Fix codecov errors (#1147) --- .codecov.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.codecov.yml b/.codecov.yml index 6fc6187f84c..3a86dac35c8 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -12,6 +12,9 @@ coverage: flags: protocol threshold: 5% target: 90% + if_no_uploads: error + if_not_found: success + if_ci_failed: error patch: default: off From b47728ee129cef5c6549478bef2c9324e6457799 Mon Sep 17 00:00:00 2001 From: Ashish Bhatia Date: Wed, 2 Oct 2019 16:10:59 -0700 Subject: [PATCH 07/11] [ContractKit]Fill more fields before web3 signing (#1133) --- .../celo-private-keys-subprovider.ts | 48 +++++-- .../contractkit/src/utils/signing-utils.ts | 32 +++-- .../contractkit/src/utils/tx-signing.test.ts | 121 +++++++++++++++--- 3 files changed, 165 insertions(+), 36 deletions(-) diff --git a/packages/contractkit/src/providers/celo-private-keys-subprovider.ts b/packages/contractkit/src/providers/celo-private-keys-subprovider.ts index 9c472cf7af1..7137c255260 100644 --- a/packages/contractkit/src/providers/celo-private-keys-subprovider.ts +++ b/packages/contractkit/src/providers/celo-private-keys-subprovider.ts @@ -7,6 +7,10 @@ import { CeloPartialTxParams } from '../utils/tx-signing' const debug = debugFactory('kit:providers:celo-private-keys-subprovider') +// Same as geth +// https://github.com/celo-org/celo-blockchain/blob/027dba2e4584936cc5a8e8993e4e27d28d5247b8/internal/ethapi/api.go#L1222 +const DefaultGasLimit = 90000 + function getPrivateKeyWithout0xPrefix(privateKey: string) { return privateKey.toLowerCase().startsWith('0x') ? privateKey.substring(2) : privateKey } @@ -18,6 +22,16 @@ export function generateAccountAddressFromPrivateKey(privateKey: string): string return new Web3().eth.accounts.privateKeyToAccount(privateKey).address } +function isEmpty(value: string | undefined) { + return ( + value === undefined || + value === null || + value === '0' || + value.toLowerCase() === '0x' || + value.toLowerCase() === '0x0' + ) +} + /** * This class supports storing multiple private keys for signing. * The base class PrivateKeyWalletSubprovider only supports one key. @@ -29,7 +43,7 @@ export class CeloPrivateKeysWalletProvider extends PrivateKeyWalletSubprovider { private chainId: number | null = null private gasFeeRecipient: string | null = null - constructor(privateKey: string) { + constructor(readonly privateKey: string) { // This won't accept a privateKey with 0x prefix and will call that an invalid key. super(getPrivateKeyWithout0xPrefix(privateKey)) this.addAccount(privateKey) @@ -100,9 +114,9 @@ export class CeloPrivateKeysWalletProvider extends PrivateKeyWalletSubprovider { txParams.nonce = await this.getNonce(txParams.from) } - if (txParams.gasFeeRecipient == null) { + if (isEmpty(txParams.gasFeeRecipient)) { txParams.gasFeeRecipient = await this.getCoinbase() - if (txParams.gasFeeRecipient == null) { + if (isEmpty(txParams.gasFeeRecipient)) { // Fail early. The validator nodes will reject a transaction missing // gas fee recipient anyways. throw new Error( @@ -112,9 +126,15 @@ export class CeloPrivateKeysWalletProvider extends PrivateKeyWalletSubprovider { } } - if (txParams.gasPrice == null) { - txParams.gasPrice = await this.getGasPrice() + if (isEmpty(txParams.gasPrice)) { + txParams.gasPrice = await this.getGasPrice(txParams.gasCurrency) + } + debug('Gas price for the transaction is %s', txParams.gasPrice) + + if (isEmpty(txParams.gas)) { + txParams.gas = String(DefaultGasLimit) } + debug('Max gas fee for the transaction is %s', txParams.gas) const signedTx = await signTransaction(txParams, this.getPrivateKeyFor(txParams.from)) const rawTransaction = signedTx.rawTransaction.toString('hex') @@ -178,15 +198,27 @@ export class CeloPrivateKeysWalletProvider extends PrivateKeyWalletSubprovider { return this.gasFeeRecipient } - private async getGasPrice(): Promise { - debug('getGasPrice fetching gas price...') + private async getGasPrice(gasCurrency: string | undefined): Promise { + // Gold Token + if (!gasCurrency) { + return this.getGasPriceInCeloGold() + } + throw new Error( + `celo-private-keys-subprovider@getGasPrice: gas price for ` + + `currency ${gasCurrency} cannot be computed in the CeloPrivateKeysWalletProvider, ` + + ' pass it explicitly' + ) + } + + private async getGasPriceInCeloGold(): Promise { + debug('getGasPriceInCeloGold fetching gas price...') // Reference: https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gasprice const result = await this.emitPayloadAsync({ method: 'eth_gasPrice', params: [], }) const gasPriceInHex = result.result.toString() - debug('getGasPrice gas price is %s', parseInt(gasPriceInHex.substr(2), 16)) + debug('getGasPriceInCeloGold gas price is %s', parseInt(gasPriceInHex.substr(2), 16)) return gasPriceInHex } } diff --git a/packages/contractkit/src/utils/signing-utils.ts b/packages/contractkit/src/utils/signing-utils.ts index 660682b9aa8..6aa843b951b 100644 --- a/packages/contractkit/src/utils/signing-utils.ts +++ b/packages/contractkit/src/utils/signing-utils.ts @@ -3,6 +3,7 @@ import debugFactory from 'debug' import { account as Account, bytes as Bytes, hash as Hash, nat as Nat, RLP } from 'eth-lib' // @ts-ignore-next-line import * as helpers from 'web3-core-helpers' +import { CeloTx } from './tx-signing' const debug = debugFactory('kit:tx:sign') @@ -54,6 +55,8 @@ export async function signTransaction(txn: any, privateKey: string) { transaction.gasCurrency = tx.gasCurrency || '0x' transaction.gasFeeRecipient = tx.gasFeeRecipient || '0x' + // This order should match the order in Geth. + // https://github.com/celo-org/celo-blockchain/blob/027dba2e4584936cc5a8e8993e4e27d28d5247b8/core/types/transaction.go#L65 const rlpEncoded = RLP.encode([ Bytes.fromNat(transaction.nonce), Bytes.fromNat(transaction.gasPrice), @@ -121,15 +124,28 @@ export async function signTransaction(txn: any, privateKey: string) { return signed(txn) } -// Recover sender address from a raw transaction. -export function recoverTransaction(rawTx: string): string { - const values = RLP.decode(rawTx) - debug('signing-utils@recoverTransaction: values are %s', values) - const signature = Account.encodeSignature(values.slice(8, 11)) - const recovery = Bytes.toNumber(values[8]) +// Recover transaction and sender address from a raw transaction. +// This is used for testing. +export function recoverTransaction(rawTx: string): [CeloTx, string] { + const rawValues = RLP.decode(rawTx) + debug('signing-utils@recoverTransaction: values are %s', rawValues) + const celoTx: CeloTx = { + nonce: rawValues[0].toLowerCase() === '0x' ? 0 : parseInt(rawValues[0], 16), + gasPrice: rawValues[1].toLowerCase() === '0x' ? 0 : parseInt(rawValues[1], 16), + gas: rawValues[2].toLowerCase() === '0x' ? 0 : parseInt(rawValues[2], 16), + gasCurrency: rawValues[3], + gasFeeRecipient: rawValues[4], + to: rawValues[5], + value: rawValues[6], + data: rawValues[7], + chainId: rawValues[8], + } + const signature = Account.encodeSignature(rawValues.slice(8, 11)) + const recovery = Bytes.toNumber(rawValues[8]) // tslint:disable-next-line:no-bitwise const extraData = recovery < 35 ? [] : [Bytes.fromNumber((recovery - 35) >> 1), '0x', '0x'] - const signingData = values.slice(0, 8).concat(extraData) + const signingData = rawValues.slice(0, 8).concat(extraData) const signingDataHex = RLP.encode(signingData) - return Account.recover(Hash.keccak256(signingDataHex), signature) + const signer = Account.recover(Hash.keccak256(signingDataHex), signature) + return [celoTx, signer] } diff --git a/packages/contractkit/src/utils/tx-signing.test.ts b/packages/contractkit/src/utils/tx-signing.test.ts index 2a9d63c9a86..072f0738ba0 100644 --- a/packages/contractkit/src/utils/tx-signing.test.ts +++ b/packages/contractkit/src/utils/tx-signing.test.ts @@ -22,22 +22,103 @@ debug(`Account Address 1: ${ACCOUNT_ADDRESS1}`) debug(`Private key 2: ${PRIVATE_KEY2}`) debug(`Account Address 2: ${ACCOUNT_ADDRESS2}`) -async function verifyLocalSigning(web3: Web3, from: string, to: string): Promise { - const amountInWei: string = Web3.utils.toWei('1', 'ether') - const gasFees: string = Web3.utils.toWei('1', 'mwei') - debug('Signer Testing using Account: %s', from) - const celoTransaction: CeloTx = { - from, - to, - value: amountInWei, - gas: gasFees, - } +async function verifyLocalSigning(web3: Web3, celoTransaction: CeloTx): Promise { + debug('Signer Testing using Account: %s', celoTransaction.from) const signedTransaction = await web3.eth.signTransaction(celoTransaction) debug('Singer Testing: Signed transaction %o', signedTransaction) const rawTransaction: string = signedTransaction.raw - const recoveredSigner = recoverTransaction(rawTransaction) - debug('Transaction was signed by "%s", recovered signer is "%s"', from, recoveredSigner) - expect(recoveredSigner).toEqual(from) + const [signedCeloTransaction, recoveredSigner] = recoverTransaction(rawTransaction) + debug( + 'Transaction was signed by "%s", recovered signer is "%s"', + celoTransaction.from, + recoveredSigner + ) + expect(recoveredSigner.toLowerCase()).toEqual(celoTransaction.from!.toLowerCase()) + + if (celoTransaction.nonce != null) { + debug( + 'Checking nonce actual: %o expected: %o', + signedCeloTransaction.nonce, + parseInt(celoTransaction.nonce.toString(), 16) + ) + expect(signedCeloTransaction.nonce).toEqual(parseInt(celoTransaction.nonce.toString(), 16)) + } + if (celoTransaction.gas != null) { + debug( + 'Checking gas actual %o expected %o', + signedCeloTransaction.gas, + parseInt(celoTransaction.gas.toString(), 16) + ) + expect(signedCeloTransaction.gas).toEqual(parseInt(celoTransaction.gas.toString(), 16)) + } + if (celoTransaction.gasPrice != null) { + debug( + 'Checking gas price actual %o expected %o', + signedCeloTransaction.gasPrice, + parseInt(celoTransaction.gasPrice.toString(), 16) + ) + expect(signedCeloTransaction.gasPrice).toEqual( + parseInt(celoTransaction.gasPrice.toString(), 16) + ) + } + if (celoTransaction.gasCurrency != null) { + debug( + 'Checking gas Currency actual %o expected %o', + signedCeloTransaction.gasCurrency, + celoTransaction.gasCurrency + ) + expect(signedCeloTransaction.gasCurrency!.toLowerCase()).toEqual( + celoTransaction.gasCurrency.toLowerCase() + ) + } + if (celoTransaction.gasFeeRecipient != null) { + debug( + 'Checking gas fee recipient actual ' + + `${signedCeloTransaction.gasFeeRecipient} expected ${celoTransaction.gasFeeRecipient}` + ) + expect(signedCeloTransaction.gasFeeRecipient!.toLowerCase()).toEqual( + celoTransaction.gasFeeRecipient.toLowerCase() + ) + } + if (celoTransaction.data != null) { + debug(`Checking data actual ${signedCeloTransaction.data} expected ${celoTransaction.data}`) + expect(signedCeloTransaction.data!.toLowerCase()).toEqual(celoTransaction.data.toLowerCase()) + } +} + +async function verifyLocalSigningInAllPermutations( + web3: Web3, + from: string, + to: string +): Promise { + const amountInWei: string = Web3.utils.toWei('1', 'ether') + const nonce = 0 + const badNonce = 100 + const gas = 10 + const gasPrice = 99 + const gasCurrency = '0x124356' + const gasFeeRecipient = '0x1234' + const data = '0xabcdef' + + // Test all possible combinations for rigor. + for (let i = 0; i < 128; i++) { + const celoTransaction: CeloTx = { + from, + to, + value: amountInWei, + nonce: i % 2 === 0 ? nonce : undefined, + gas: i % 4 === 0 ? gas : undefined, + gasPrice: i % 8 === 0 ? gasPrice : undefined, + gasCurrency: i % 16 === 0 ? gasCurrency : undefined, + gasFeeRecipient: i % 32 === 0 ? gasFeeRecipient : undefined, + data: i % 64 === 0 ? data : undefined, + } + await verifyLocalSigning(web3, celoTransaction) + } + + // A special case. + // An incorrect nonce will only work, if no implict calls to estimate gas are required. + await verifyLocalSigning(web3, { from, to, nonce: badNonce, gas, gasPrice }) } testWithGanache('Transaction Utils', (web3: Web3) => { @@ -55,27 +136,27 @@ testWithGanache('Transaction Utils', (web3: Web3) => { web3.currentProvider = originalProvider }) - describe('Signer Testing with single local account', () => { + describe('Signer Testing with single local account and pay gas in Celo Gold', () => { it('Test1 should be able to sign and get the signer back with single local account', async () => { - jest.setTimeout(20 * 1000) + jest.setTimeout(60 * 1000) addLocalAccount(web3, PRIVATE_KEY1) - await verifyLocalSigning(web3, ACCOUNT_ADDRESS1, ACCOUNT_ADDRESS2) + await verifyLocalSigningInAllPermutations(web3, ACCOUNT_ADDRESS1, ACCOUNT_ADDRESS2) }) }) describe('Signer Testing with multiple local accounts', () => { it('Test2 should be able to sign with first account and get the signer back with multiple local accounts', async () => { - jest.setTimeout(20 * 1000) + jest.setTimeout(60 * 1000) addLocalAccount(web3, PRIVATE_KEY1) addLocalAccount(web3, PRIVATE_KEY2) - await verifyLocalSigning(web3, ACCOUNT_ADDRESS1, ACCOUNT_ADDRESS2) + await verifyLocalSigningInAllPermutations(web3, ACCOUNT_ADDRESS1, ACCOUNT_ADDRESS2) }) it('Test3 should be able to sign with second account and get the signer back with multiple local accounts', async () => { - jest.setTimeout(20 * 1000) + jest.setTimeout(60 * 1000) addLocalAccount(web3, PRIVATE_KEY1) addLocalAccount(web3, PRIVATE_KEY2) - await verifyLocalSigning(web3, ACCOUNT_ADDRESS2, ACCOUNT_ADDRESS1) + await verifyLocalSigningInAllPermutations(web3, ACCOUNT_ADDRESS2, ACCOUNT_ADDRESS1) }) }) }) From 6bc3f92b16ef6fe9a05cfdad8230ebf3a267dd96 Mon Sep 17 00:00:00 2001 From: Ashish Bhatia Date: Wed, 2 Oct 2019 16:46:43 -0700 Subject: [PATCH 08/11] Move docker images to use node v10 (#1183) --- README-dev.md | 4 ++-- dockerfiles/attestation-service/Dockerfile | 2 +- dockerfiles/celotool/Dockerfile | 2 +- dockerfiles/cli/Dockerfile | 4 ++-- dockerfiles/cloudbuild/Dockerfile | 2 +- dockerfiles/transaction-metrics-exporter/Dockerfile | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README-dev.md b/README-dev.md index faf4c60bd44..c17c72c8261 100644 --- a/README-dev.md +++ b/README-dev.md @@ -14,7 +14,7 @@ Before publishing a new celocli package, test in isolation using Docker. This co ``` # To test utils package, change $PWD/packages/cli to $PWD/packages/utils # To test contractkit package, change $PWD/packages/contractkit to $PWD/packages/contractkit -celo-monorepo $ docker run -v $PWD/packages/cli:/tmp/npm_package -it --entrypoint bash node:8 +celo-monorepo $ docker run -v $PWD/packages/cli:/tmp/npm_package -it --entrypoint bash node:10fn root@e0d56700584f:/# mkdir /tmp/tmp1 && cd /tmp/tmp1 root@e0d56700584f:/tmp/tmp1# npm install /tmp/npm_package/ ``` @@ -43,7 +43,7 @@ Once you publish do some manual tests, for example, after publishing `celocli` ``` # Docker for an isolated environment again -celo-monorepo $ docker run -it --entrypoint bash node:8 +celo-monorepo $ docker run -it --entrypoint bash node:10 root@e0d56700584f:/# mkdir /tmp/tmp1 && cd /tmp/tmp1 root@e0d56700584f:/tmp/tmp1# npm install @celo/celocli@0.0.20 /tmp/tmp1# ./node_modules/.bin/celocli diff --git a/dockerfiles/attestation-service/Dockerfile b/dockerfiles/attestation-service/Dockerfile index 46567d73272..935a1e6c7c7 100644 --- a/dockerfiles/attestation-service/Dockerfile +++ b/dockerfiles/attestation-service/Dockerfile @@ -1,4 +1,4 @@ -FROM node:8 +FROM node:10 WORKDIR /celo-monorepo # ensure yarn.lock is evaluated by kaniko cache diff diff --git a/dockerfiles/celotool/Dockerfile b/dockerfiles/celotool/Dockerfile index 3f400c83bdf..e8fff06851d 100644 --- a/dockerfiles/celotool/Dockerfile +++ b/dockerfiles/celotool/Dockerfile @@ -1,4 +1,4 @@ -FROM node:8 +FROM node:10 WORKDIR /celo-monorepo # Needed for gsutil diff --git a/dockerfiles/cli/Dockerfile b/dockerfiles/cli/Dockerfile index 4392547cb2e..2bfc8f36fc3 100644 --- a/dockerfiles/cli/Dockerfile +++ b/dockerfiles/cli/Dockerfile @@ -34,7 +34,7 @@ ADD https://www.googleapis.com/storage/v1/b/genesis_blocks/o/${celo_env}?alt=med ADD https://www.googleapis.com/storage/v1/b/static_nodes/o/${celo_env}?alt=media /celo/static-nodes.json # Build Celocli -FROM node:8-alpine as node +FROM node:10-alpine as node ARG celo_env @@ -46,7 +46,7 @@ WORKDIR /celo-monorepo/ RUN npm install @celo/celocli # Build the combined image -FROM node:8-alpine as final_image +FROM node:10-alpine as final_image ARG network_name="alfajores" ARG network_id="44782" diff --git a/dockerfiles/cloudbuild/Dockerfile b/dockerfiles/cloudbuild/Dockerfile index 8c5f1ac8f37..e4a895a0537 100644 --- a/dockerfiles/cloudbuild/Dockerfile +++ b/dockerfiles/cloudbuild/Dockerfile @@ -1,4 +1,4 @@ -FROM node:8 +FROM node:10 RUN apt-get update -y RUN apt-get install lsb-release libudev-dev libusb-dev -y --no-install-recommends apt-utils diff --git a/dockerfiles/transaction-metrics-exporter/Dockerfile b/dockerfiles/transaction-metrics-exporter/Dockerfile index ced9dd66e2a..eed19259522 100644 --- a/dockerfiles/transaction-metrics-exporter/Dockerfile +++ b/dockerfiles/transaction-metrics-exporter/Dockerfile @@ -1,4 +1,4 @@ -FROM node:8 +FROM node:10 WORKDIR /celo-monorepo # Needed for gsutil From b919b271db1c9b07bced2b587e8219cfabc88b98 Mon Sep 17 00:00:00 2001 From: Mariano Cortesi Date: Wed, 2 Oct 2019 21:05:56 -0300 Subject: [PATCH 09/11] [ck] consistent send tx object in kit (#1191) --- packages/contractkit/src/kit.test.ts | 84 ++++++++++++++ packages/contractkit/src/kit.ts | 105 +++++++++++++----- .../contractkit/src/utils/send-tx.test.ts | 79 ------------- packages/contractkit/src/utils/send-tx.ts | 51 --------- .../contractkit/src/wrappers/BaseWrapper.ts | 13 +-- 5 files changed, 167 insertions(+), 165 deletions(-) create mode 100644 packages/contractkit/src/kit.test.ts delete mode 100644 packages/contractkit/src/utils/send-tx.test.ts delete mode 100644 packages/contractkit/src/utils/send-tx.ts diff --git a/packages/contractkit/src/kit.test.ts b/packages/contractkit/src/kit.test.ts new file mode 100644 index 00000000000..54adbf0354d --- /dev/null +++ b/packages/contractkit/src/kit.test.ts @@ -0,0 +1,84 @@ +import { TransactionObject, Tx } from 'web3/eth/types' +import PromiEvent from 'web3/promiEvent' +import { TransactionReceipt } from 'web3/types' +import { newKit } from './kit' +import { promiEventSpy } from './test-utils/PromiEventStub' + +interface TransactionObjectStub extends TransactionObject { + sendMock: jest.Mock, [Tx | undefined]> + estimateGasMock: jest.Mock, []> + resolveHash(hash: string): void + resolveReceipt(receipt: TransactionReceipt): void + rejectHash(error: any): void + rejectReceipt(receipt: TransactionReceipt, error: any): void +} + +export function txoStub(): TransactionObjectStub { + const estimateGasMock = jest.fn() + const peStub = promiEventSpy() + const sendMock = jest.fn().mockReturnValue(peStub) + + const pe: TransactionObjectStub = { + arguments: [], + call: () => { + throw new Error('not implemented') + }, + encodeABI: () => { + throw new Error('not implemented') + }, + estimateGas: estimateGasMock, + send: sendMock, + sendMock, + estimateGasMock, + resolveHash: peStub.resolveHash, + rejectHash: peStub.rejectHash, + resolveReceipt: peStub.resolveReceipt, + rejectReceipt: peStub.resolveReceipt, + } + return pe +} + +describe('kit.sendTransactionObject()', () => { + const kit = newKit('http://') + + test('should send transaction on simple case', async () => { + const txo = txoStub() + txo.estimateGasMock.mockResolvedValue(1000) + const txRes = await kit.sendTransactionObject(txo) + + txo.resolveHash('HASH') + txo.resolveReceipt('Receipt' as any) + + await expect(txRes.getHash()).resolves.toBe('HASH') + await expect(txRes.waitReceipt()).resolves.toBe('Receipt') + }) + + test('should not estimateGas if gas is provided', async () => { + const txo = txoStub() + await kit.sendTransactionObject(txo, { gas: 555 }) + expect(txo.estimateGasMock).not.toBeCalled() + }) + + test('should use inflation factor on gas', async () => { + const txo = txoStub() + txo.estimateGasMock.mockResolvedValue(1000) + kit.gasInflactionFactor = 2 + await kit.sendTransactionObject(txo) + expect(txo.send).toBeCalledWith( + expect.objectContaining({ + gas: 1000 * 2, + }) + ) + }) + + test('should forward txoptions to txo.send()', async () => { + const txo = txoStub() + await kit.sendTransactionObject(txo, { gas: 555, gasCurrency: 'XXX', from: '0xAAFFF' }) + expect(txo.send).toBeCalledWith({ + gasPrice: '0', + gas: 555, + gasCurrency: 'XXX', + from: '0xAAFFF', + }) + }) +}) diff --git a/packages/contractkit/src/kit.ts b/packages/contractkit/src/kit.ts index 61fdebc0817..706b03b53ce 100644 --- a/packages/contractkit/src/kit.ts +++ b/packages/contractkit/src/kit.ts @@ -1,9 +1,9 @@ +import debugFactory from 'debug' import Web3 from 'web3' import { TransactionObject, Tx } from 'web3/eth/types' import { AddressRegistry } from './address-registry' import { Address, CeloContract, CeloToken } from './base' import { WrapperCache } from './contract-cache' -import { sendTransaction, TxOptions } from './utils/send-tx' import { toTxResult, TransactionResult } from './utils/tx-result' import { addLocalAccount } from './utils/web3-utils' import { Web3ContractCache } from './web3-contract-cache' @@ -17,6 +17,8 @@ import { SortedOraclesConfig } from './wrappers/SortedOracles' import { StableTokenConfig } from './wrappers/StableTokenWrapper' import { ValidatorConfig } from './wrappers/Validators' +const debug = debugFactory('kit:kit') + export function newKit(url: string) { return newKitFromWeb3(new Web3(url)) } @@ -37,14 +39,21 @@ export interface NetworkConfig { validators: ValidatorConfig } +export interface KitOptions { + gasInflationFactor: number + gasCurrency: Address | null + from?: Address +} + export class ContractKit { readonly registry: AddressRegistry readonly _web3Contracts: Web3ContractCache readonly contracts: WrapperCache - private _defaultOptions: TxOptions + private config: KitOptions constructor(readonly web3: Web3) { - this._defaultOptions = { + this.config = { + gasCurrency: null, gasInflationFactor: 1.3, } @@ -92,8 +101,8 @@ export class ContractKit { } async setGasCurrency(token: CeloToken) { - this._defaultOptions.gasCurrency = - token === CeloContract.GoldToken ? undefined : await this.registry.addressFor(token) + this.config.gasCurrency = + token === CeloContract.GoldToken ? null : await this.registry.addressFor(token) } addAccount(privateKey: string) { @@ -101,7 +110,7 @@ export class ContractKit { } set defaultAccount(address: Address) { - this._defaultOptions.from = address + this.config.from = address this.web3.eth.defaultAccount = address } @@ -109,12 +118,20 @@ export class ContractKit { return this.web3.eth.defaultAccount } - get defaultOptions(): Readonly { - return { ...this._defaultOptions } + set gasInflactionFactor(factor: number) { + this.config.gasInflationFactor = factor + } + + get gasInflationFactor() { + return this.config.gasInflationFactor + } + + set defaultGasCurrency(address: Address | null) { + this.config.gasCurrency = address } - setGasCurrencyAddress(address: Address) { - this._defaultOptions.gasCurrency = address + get defaultGasCurrency() { + return this.config.gasCurrency } isListening(): Promise { @@ -125,27 +142,59 @@ export class ContractKit { return this.web3.eth.isSyncing() } - sendTransaction(tx: Tx): TransactionResult { - const promiEvent = this.web3.eth.sendTransaction({ - from: this._defaultOptions.from, - // TODO this won't work for locally signed TX - gasPrice: '0', - // @ts-ignore - gasCurrency: this._defaultOptions.gasCurrency, - // TODO needed for locally signed tx, ignored by now (celo-blockchain with set it) - // gasFeeRecipient: this.defaultOptions.gasFeeRecipient, - ...tx, - }) - return toTxResult(promiEvent) + async sendTransaction(tx: Tx): Promise { + tx = this.fillTxDefaults(tx) + + let gas = tx.gas + if (gas == null) { + gas = Math.round( + (await this.web3.eth.estimateGas({ ...tx })) * this.config.gasInflationFactor + ) + debug('estimatedGas: %s', gas) + } + + return toTxResult( + this.web3.eth.sendTransaction({ + ...tx, + gas, + }) + ) } - sendTransactionObject( + async sendTransactionObject( txObj: TransactionObject, - options?: TxOptions + tx?: Omit ): Promise { - return sendTransaction(txObj, { - ...this._defaultOptions, - ...options, - }) + tx = this.fillTxDefaults(tx) + + let gas = tx.gas + if (gas == null) { + gas = Math.round((await txObj.estimateGas({ ...tx })) * this.config.gasInflationFactor) + debug('estimatedGas: %s', gas) + } + + return toTxResult( + txObj.send({ + ...tx, + gas, + }) + ) + } + + private fillTxDefaults(tx?: Tx): Tx { + const defaultTx: Tx = { + from: this.config.from, + // gasPrice:0 means the node will compute gasPrice on it's own + gasPrice: '0', + } + + if (this.config.gasCurrency) { + defaultTx.gasCurrency = this.config.gasCurrency + } + + return { + ...defaultTx, + ...tx, + } } } diff --git a/packages/contractkit/src/utils/send-tx.test.ts b/packages/contractkit/src/utils/send-tx.test.ts deleted file mode 100644 index 9b9d151e94e..00000000000 --- a/packages/contractkit/src/utils/send-tx.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { TransactionObject, Tx } from 'web3/eth/types' -import PromiEvent from 'web3/promiEvent' -import { TransactionReceipt } from 'web3/types' -import { promiEventSpy } from '../test-utils/PromiEventStub' -import { sendTransaction } from './send-tx' - -interface TransactionObjectStub extends TransactionObject { - sendMock: jest.Mock, [Tx | undefined]> - estimateGasMock: jest.Mock, []> - resolveHash(hash: string): void - resolveReceipt(receipt: TransactionReceipt): void - rejectHash(error: any): void - rejectReceipt(receipt: TransactionReceipt, error: any): void -} - -export function txoStub(): TransactionObjectStub { - const estimateGasMock = jest.fn() - const peStub = promiEventSpy() - const sendMock = jest.fn().mockReturnValue(peStub) - - const pe: TransactionObjectStub = { - arguments: [], - call: () => { - throw new Error('not implemented') - }, - encodeABI: () => { - throw new Error('not implemented') - }, - estimateGas: estimateGasMock, - send: sendMock, - sendMock, - estimateGasMock, - resolveHash: peStub.resolveHash, - rejectHash: peStub.rejectHash, - resolveReceipt: peStub.resolveReceipt, - rejectReceipt: peStub.resolveReceipt, - } - return pe -} - -test('should send transaction on simple case', async () => { - const txo = txoStub() - txo.estimateGasMock.mockResolvedValue(1000) - const txRes = await sendTransaction(txo) - - txo.resolveHash('HASH') - txo.resolveReceipt('Receipt' as any) - - await expect(txRes.getHash()).resolves.toBe('HASH') - await expect(txRes.waitReceipt()).resolves.toBe('Receipt') -}) - -test('should not estimateGas if gas is provided', async () => { - const txo = txoStub() - await sendTransaction(txo, { estimatedGas: 555 }) - expect(txo.estimateGasMock).not.toBeCalled() -}) - -test('should use inflation factor on gas', async () => { - const txo = txoStub() - txo.estimateGasMock.mockResolvedValue(1000) - await sendTransaction(txo, { gasInflationFactor: 2 }) - expect(txo.send).toBeCalledWith( - expect.objectContaining({ - gas: 1000 * 2, - }) - ) -}) - -test('should forward txoptions to txo.send()', async () => { - const txo = txoStub() - await sendTransaction(txo, { estimatedGas: 555, gasCurrency: 'XXX', from: '0xAAFFF' }) - expect(txo.send).toBeCalledWith({ - gasPrice: '0', - gas: 555, - gasCurrency: 'XXX', - from: '0xAAFFF', - }) -}) diff --git a/packages/contractkit/src/utils/send-tx.ts b/packages/contractkit/src/utils/send-tx.ts deleted file mode 100644 index 815b5700bd3..00000000000 --- a/packages/contractkit/src/utils/send-tx.ts +++ /dev/null @@ -1,51 +0,0 @@ -import debugFactory from 'debug' -import { TransactionObject } from 'web3/eth/types' -import { Address } from '../base' -import { toTxResult, TransactionResult } from './tx-result' - -const debug = debugFactory('kit:tx:send') - -export interface TxOptions { - gasInflationFactor?: number - gasFeeRecipient?: Address - gasCurrency?: Address | undefined - from?: Address - estimatedGas?: number | undefined -} - -/** - * sendTransaction mainly abstracts the sending of a transaction in a promise like - * interface. - */ -export async function sendTransaction( - tx: TransactionObject, - txOptions: TxOptions = {} -): Promise { - const txParams: any = { - from: txOptions.from, - gasCurrency: txOptions.gasCurrency, - gasPrice: '0', - } - - let gas = txOptions.estimatedGas - if (gas === undefined) { - const inflactionFactor = txOptions.gasInflationFactor || 1 - gas = Math.round((await tx.estimateGas(txParams)) * inflactionFactor) - debug('estimatedGas: %s', gas) - // logger(EstimatedGas(estimatedGas)) - } - - const promiEvent = tx.send({ - from: txOptions.from, - // @ts-ignore - gasCurrency: txOptions.gasCurrency, - // TODO needed for locally signed tx, ignored by now (celo-blockchain with set it) - // gasFeeRecipient: txOptions.gasFeeRecipient, - gas, - // Hack to prevent web3 from adding the suggested gold gas price, allowing geth to add - // the suggested price in the selected gasCurrency. - // TODO this won't work for locally signed TX - gasPrice: '0', - }) - return toTxResult(promiEvent) -} diff --git a/packages/contractkit/src/wrappers/BaseWrapper.ts b/packages/contractkit/src/wrappers/BaseWrapper.ts index 3f7bf48e439..cd8d816f39d 100644 --- a/packages/contractkit/src/wrappers/BaseWrapper.ts +++ b/packages/contractkit/src/wrappers/BaseWrapper.ts @@ -1,10 +1,9 @@ import { zip } from '@celo/utils/lib/collections' import BigNumber from 'bignumber.js' import Contract from 'web3/eth/contract' -import { TransactionObject } from 'web3/eth/types' +import { TransactionObject, Tx } from 'web3/eth/types' import { TransactionReceipt } from 'web3/types' import { ContractKit } from '../kit' -import { TxOptions } from '../utils/send-tx' import { TransactionResult } from '../utils/tx-result' type Method = (...args: I) => TransactionObject @@ -22,8 +21,8 @@ export abstract class BaseWrapper { export interface CeloTransactionObject { txo: TransactionObject - send(options?: TxOptions): Promise - sendAndWaitForReceipt(options?: TxOptions): Promise + send(params?: Omit): Promise + sendAndWaitForReceipt(params?: Omit): Promise } export function toBigNumber(input: string) { @@ -202,9 +201,9 @@ export function proxySend(kit: ContractKit, txo: TransactionObject): CeloTransactionObject { return { - send: (options?: TxOptions) => kit.sendTransactionObject(txo, options), + send: (params?: Omit) => kit.sendTransactionObject(txo, params), txo, - sendAndWaitForReceipt: (options?: TxOptions) => - kit.sendTransactionObject(txo, options).then((result) => result.waitReceipt()), + sendAndWaitForReceipt: (params?: Omit) => + kit.sendTransactionObject(txo, params).then((result) => result.waitReceipt()), } } From a70d7aaeb7044aad123af4caa2da6cdf41e1affd Mon Sep 17 00:00:00 2001 From: Mariano Cortesi Date: Wed, 2 Oct 2019 21:39:55 -0300 Subject: [PATCH 10/11] [contractkit] Document methods (#1195) --- packages/contractkit/README.md | 46 +++++-- packages/contractkit/src/address-registry.ts | 10 ++ packages/contractkit/src/contract-cache.ts | 8 ++ packages/contractkit/src/index.ts | 4 + packages/contractkit/src/kit.ts | 39 +++++- packages/contractkit/src/utils/tx-result.ts | 12 ++ .../contractkit/src/web3-contract-cache.ts | 11 ++ .../contractkit/src/wrappers/BaseWrapper.ts | 13 ++ .../src/wrappers/GoldTokenWrapper.ts | 39 ++++++ .../contractkit/src/wrappers/LockedGold.ts | 102 ++++++++++++++-- .../src/wrappers/StableTokenWrapper.ts | 113 +++++++++++++++--- 11 files changed, 353 insertions(+), 44 deletions(-) diff --git a/packages/contractkit/README.md b/packages/contractkit/README.md index 700818b091b..aada1998189 100644 --- a/packages/contractkit/README.md +++ b/packages/contractkit/README.md @@ -36,9 +36,7 @@ const kit = newKit('https://alfajores-infura.celo-testnet.org:8545') To access web3: ```ts -const web3 = kit.web3 - -web3.eth.getBalance(someAddress) +await kit.web3.eth.getBalance(someAddress) ``` ### Setting Default Tx Options @@ -46,12 +44,17 @@ web3.eth.getBalance(someAddress) `kit` allows you to set default transaction options: ```ts -import { CeloContract } from '@celo/contractkit' - -// default from -kit.defaultAccount = myAddress -// paid gas in celo dollars -await kit.setGasCurrency(CeloContract.StableToken) +import { newKit, CeloContract } from '@celo/contractkit' + +async function getKit(myAddress: string) { + const kit = newKit('https://alfajores-infura.celo-testnet.org:8545') + + // default from + kit.defaultAccount = myAddress + // paid gas in celo dollars + await kit.setGasCurrency(CeloContract.StableToken) + return kit +} ``` ### Interacting with cGold & cDollar @@ -60,7 +63,7 @@ celo-blockchain has two initial coins: cGold and cDollar (stableToken). Both implement the ERC20 standard, and to interact with them is as simple as: ```ts -const goldtoken = await kit.contract.getGoldToken() +const goldtoken = await kit.contracts.getGoldToken() const balance = await goldtoken.balanceOf(someAddress) ``` @@ -80,7 +83,7 @@ const receipt = await tx.waitReceipt() To interact with cDollar, is the same but with a different contract: ```ts -const stabletoken = await kit.contract.getStableToken() +const stabletoken = await kit.contracts.getStableToken() ``` ### Interacting with Other Contracts @@ -92,6 +95,9 @@ For the moment, we have contract wrappers for: - Exchange (Uniswap kind exchange between Gold and Stable tokens) - Validators - LockedGold +- GoldToken +- StableToken +- Attestations In the following weeks will add wrapper for all other contracts @@ -107,6 +113,24 @@ const web3Exchange = await kit._web3Contracts.getExchange() We expose native wrappers for all Celo core contracts. +The complete list of Celo Core contracts is: + + - Attestations + - LockedGold + - Escrow + - Exchange + - GasCurrencyWhitelist + - GasPriceMinimum + - GoldToken + - Governance + - MultiSig + - Random + - Registry + - Reserve + - SortedOracles + - StableToken + - Validators + ## A Note About Contract Addresses Celo Core Contracts addresses, can be obtained by looking at the `Registry` contract. diff --git a/packages/contractkit/src/address-registry.ts b/packages/contractkit/src/address-registry.ts index 88799673500..68efca73dfc 100644 --- a/packages/contractkit/src/address-registry.ts +++ b/packages/contractkit/src/address-registry.ts @@ -10,6 +10,9 @@ const debug = debugFactory('kit:registry') // Registry contract is always predeployed to this address const REGISTRY_CONTRACT_ADDRESS = '0x000000000000000000000000000000000000ce10' +/** + * Celo Core Contract's Address Registry + */ export class AddressRegistry { private readonly registry: Registry private readonly cache: Map = new Map() @@ -19,6 +22,9 @@ export class AddressRegistry { this.registry = newRegistry(kit.web3, REGISTRY_CONTRACT_ADDRESS) } + /** + * Get the address for a `CeloContract` + */ async addressFor(contract: CeloContract): Promise

{ if (!this.cache.has(contract)) { debug('Fetching address from Registry for %s', contract) @@ -35,6 +41,10 @@ export class AddressRegistry { return cachedAddress } + /** + * Get the address for all possible `CeloContract` + */ + async allAddresses(): Promise> { const res: Partial> = {} for (const contract of AllContracts) { diff --git a/packages/contractkit/src/contract-cache.ts b/packages/contractkit/src/contract-cache.ts index c7aa1270524..18f163bd4f0 100644 --- a/packages/contractkit/src/contract-cache.ts +++ b/packages/contractkit/src/contract-cache.ts @@ -50,6 +50,11 @@ interface WrapperCacheMap { [CeloContract.Validators]?: ValidatorsWrapper } +/** + * Kit ContractWrappers factory & cache. + * + * Provides access to all contract wrappers for celo core contracts + */ export class WrapperCache { // private wrapperCache: Map = new Map() private wrapperCache: WrapperCacheMap = {} @@ -99,6 +104,9 @@ export class WrapperCache { return this.getContract(CeloContract.Validators) } + /** + * Get Contract wrapper + */ public async getContract(contract: C) { if (this.wrapperCache[contract] == null) { const instance = await this.kit._web3Contracts.getContract(contract) diff --git a/packages/contractkit/src/index.ts b/packages/contractkit/src/index.ts index 84877b7a0a7..1c8b6bdecf4 100644 --- a/packages/contractkit/src/index.ts +++ b/packages/contractkit/src/index.ts @@ -6,6 +6,10 @@ export * from './kit' export { CeloTransactionObject } from './wrappers/BaseWrapper' export { Roles } from './wrappers/LockedGold' +/** + * Creates a new web3 instance + * @param url node url + */ export function newWeb3(url: string) { return new Web3(url) } diff --git a/packages/contractkit/src/kit.ts b/packages/contractkit/src/kit.ts index 706b03b53ce..ffdc769da74 100644 --- a/packages/contractkit/src/kit.ts +++ b/packages/contractkit/src/kit.ts @@ -19,10 +19,18 @@ import { ValidatorConfig } from './wrappers/Validators' const debug = debugFactory('kit:kit') +/** + * Creates a new instance of `ContractKit` give a nodeUrl + * @param url CeloBlockchain node url + */ export function newKit(url: string) { return newKitFromWeb3(new Web3(url)) } +/** + * Creates a new instance of `ContractKit` give a web3 instance + * @param web3 Web3 instance + */ export function newKitFromWeb3(web3: Web3) { return new ContractKit(web3) } @@ -46,8 +54,11 @@ export interface KitOptions { } export class ContractKit { + /** core contract's address registry */ readonly registry: AddressRegistry + /** factory for core contract's native web3 wrappers */ readonly _web3Contracts: Web3ContractCache + /** factory for core contract's kit wrappers */ readonly contracts: WrapperCache private config: KitOptions @@ -100,7 +111,11 @@ export class ContractKit { } } - async setGasCurrency(token: CeloToken) { + /** + * Set CeloToken to use to pay for gas fees + * @param token cUsd or cGold + */ + async setGasCurrency(token: CeloToken): Promise { this.config.gasCurrency = token === CeloContract.GoldToken ? null : await this.registry.addressFor(token) } @@ -109,11 +124,17 @@ export class ContractKit { addLocalAccount(this.web3, privateKey) } + /** + * Set default account for generated transactions (eg. tx.from ) + */ set defaultAccount(address: Address) { this.config.from = address this.web3.eth.defaultAccount = address } + /** + * Default account for generated transactions (eg. tx.from) + */ get defaultAccount(): Address { return this.web3.eth.defaultAccount } @@ -126,6 +147,14 @@ export class ContractKit { return this.config.gasInflationFactor } + /** + * Set the ERC20 address for the token to use to pay for gas fees. + * The ERC20 must be whitelisted for gas. + * + * Set to `null` to use cGold + * + * @param address ERC20 address + */ set defaultGasCurrency(address: Address | null) { this.config.gasCurrency = address } @@ -142,6 +171,14 @@ export class ContractKit { return this.web3.eth.isSyncing() } + /** + * Send a transaction to celo-blockchain. + * + * Similar to `web3.eth.sendTransaction()` but with following differences: + * - applies kit tx's defaults + * - estimatesGas before sending + * - returns a `TransactionResult` instead of `PromiEvent` + */ async sendTransaction(tx: Tx): Promise { tx = this.fillTxDefaults(tx) diff --git a/packages/contractkit/src/utils/tx-result.ts b/packages/contractkit/src/utils/tx-result.ts index 6ae849bb9cc..1d4ba0ed678 100644 --- a/packages/contractkit/src/utils/tx-result.ts +++ b/packages/contractkit/src/utils/tx-result.ts @@ -5,10 +5,20 @@ import { TransactionReceipt } from 'web3/types' const debug = debugFactory('kit:tx:result') +/** + * Transforms a `PromiEvent` to a `TransactionResult`. + * + * PromiEvents are returned by web3 when we do a `contract.method.xxx.send()` + */ export function toTxResult(pe: PromiEvent) { return new TransactionResult(pe) } +/** + * Replacement interface for web3's `PromiEvent`. Instead of emiting events + * to signal different stages, eveything is exposed as a promise. Which ends + * up being nicer when doing promise/async based programming. + */ export class TransactionResult { private hashFuture = new Future() private receiptFuture = new Future() @@ -34,10 +44,12 @@ export class TransactionResult { }) as any) } + /** Get (& wait for) transaction hash */ getHash() { return this.hashFuture.wait() } + /** Get (& wait for) transaction receipt */ waitReceipt() { return this.receiptFuture.wait() } diff --git a/packages/contractkit/src/web3-contract-cache.ts b/packages/contractkit/src/web3-contract-cache.ts index 872afd68bae..d6475497aa2 100644 --- a/packages/contractkit/src/web3-contract-cache.ts +++ b/packages/contractkit/src/web3-contract-cache.ts @@ -38,6 +38,14 @@ const ContractFactories = { type CFType = typeof ContractFactories type ContractCacheMap = { [K in keyof CFType]?: ReturnType } +/** + * Native Web3 contracts factory and cache. + * + * Exposes accessors to all `CeloContract` web3 contracts. + * + * Mostly a private cache, kit users would normally use + * a contract wrapper + */ export class Web3ContractCache { private cacheMap: ContractCacheMap = {} @@ -86,6 +94,9 @@ export class Web3ContractCache { return this.getContract(CeloContract.Validators) } + /** + * Get native web3 contract wrapper + */ async getContract(contract: C) { if (this.cacheMap[contract] == null) { debug('Initiating contract %s', contract) diff --git a/packages/contractkit/src/wrappers/BaseWrapper.ts b/packages/contractkit/src/wrappers/BaseWrapper.ts index cd8d816f39d..8da2ff80fb9 100644 --- a/packages/contractkit/src/wrappers/BaseWrapper.ts +++ b/packages/contractkit/src/wrappers/BaseWrapper.ts @@ -6,13 +6,16 @@ import { TransactionReceipt } from 'web3/types' import { ContractKit } from '../kit' import { TransactionResult } from '../utils/tx-result' +/** Represents web3 native contract Method */ type Method = (...args: I) => TransactionObject export type NumberLike = string | number | BigNumber +/** Base ContractWrapper */ export abstract class BaseWrapper { constructor(protected readonly kit: ContractKit, protected readonly contract: T) {} + /** Contract address */ get address(): string { // TODO fix typings return (this.contract as any)._address @@ -20,15 +23,20 @@ export abstract class BaseWrapper { } export interface CeloTransactionObject { + /** web3 native TransactionObject. Normally not used */ txo: TransactionObject + /** send the transaction to the chain */ send(params?: Omit): Promise + /** send the transaction and waits for the receipt */ sendAndWaitForReceipt(params?: Omit): Promise } +/** Parse string -> BigNumber */ export function toBigNumber(input: string) { return new BigNumber(input) } +/** Parse string -> int */ export function toNumber(input: string) { return parseInt(input, 10) } @@ -39,10 +47,15 @@ export function parseNumber(input: NumberLike) { type Parser = (input: A) => B +/** Identity Parser */ export function identity(a: A) { return a } +/** + * Tuple parser + * Useful to map different input arguments + */ export function tupleParser(parser0: Parser): (...args: [A0]) => [B0] export function tupleParser( parser0: Parser, diff --git a/packages/contractkit/src/wrappers/GoldTokenWrapper.ts b/packages/contractkit/src/wrappers/GoldTokenWrapper.ts index baa3eb62eba..30a1b30f0ea 100644 --- a/packages/contractkit/src/wrappers/GoldTokenWrapper.ts +++ b/packages/contractkit/src/wrappers/GoldTokenWrapper.ts @@ -13,11 +13,13 @@ export class GoldTokenWrapper extends BaseWrapper { * @returns Amount of allowance. */ allowance = proxyCall(this.contract.methods.allowance, undefined, toBigNumber) + /** * Returns the name of the token. * @returns Name of the token. */ name = proxyCall(this.contract.methods.name, undefined, (a: any) => a.toString()) + /** * Returns the three letter symbol of the token. * @returns Symbol of the token. @@ -28,14 +30,51 @@ export class GoldTokenWrapper extends BaseWrapper { * @returns Number of decimals. */ decimals = proxyCall(this.contract.methods.decimals, undefined, toNumber) + /** * Returns the total supply of the token, that is, the amount of tokens currently minted. * @returns Total supply. */ totalSupply = proxyCall(this.contract.methods.totalSupply, undefined, toBigNumber) + + /** + * Approve a user to transfer Celo Gold on behalf of another user. + * @param spender The address which is being approved to spend Celo Gold. + * @param value The amount of Celo Gold approved to the spender. + * @return True if the transaction succeeds. + */ approve = proxySend(this.kit, this.contract.methods.approve) + + /** + * Transfers Celo Gold from one address to another with a comment. + * @param to The address to transfer Celo Gold to. + * @param value The amount of Celo Gold to transfer. + * @param comment The transfer comment + * @return True if the transaction succeeds. + */ transferWithComment = proxySend(this.kit, this.contract.methods.transferWithComment) + + /** + * Transfers Celo Gold from one address to another. + * @param to The address to transfer Celo Gold to. + * @param value The amount of Celo Gold to transfer. + * @return True if the transaction succeeds. + */ transfer = proxySend(this.kit, this.contract.methods.transfer) + + /** + * Transfers Celo Gold from one address to another on behalf of a user. + * @param from The address to transfer Celo Gold from. + * @param to The address to transfer Celo Gold to. + * @param value The amount of Celo Gold to transfer. + * @return True if the transaction succeeds. + */ transferFrom = proxySend(this.kit, this.contract.methods.transferFrom) + + /** + * Gets the balance of the specified address. + * @param owner The address to query the balance of. + * @return The balance of the specified address. + */ balanceOf = (account: Address) => this.kit.web3.eth.getBalance(account).then(toBigNumber) } diff --git a/packages/contractkit/src/wrappers/LockedGold.ts b/packages/contractkit/src/wrappers/LockedGold.ts index a63e34b8d3c..87ee8c46df4 100644 --- a/packages/contractkit/src/wrappers/LockedGold.ts +++ b/packages/contractkit/src/wrappers/LockedGold.ts @@ -16,6 +16,7 @@ import { export interface VotingDetails { accountAddress: Address voterAddress: Address + /** vote's weight */ weight: BigNumber } @@ -47,19 +48,86 @@ export interface LockedGoldConfig { * Contract for handling deposits needed for voting. */ export class LockedGoldWrapper extends BaseWrapper { - notifyCommitment = proxySend(this.kit, this.contract.methods.notifyCommitment) - createAccount = proxySend(this.kit, this.contract.methods.createAccount) - withdrawCommitment = proxySend(this.kit, this.contract.methods.withdrawCommitment) - redeemRewards = proxySend(this.kit, this.contract.methods.redeemRewards) - newCommitment = proxySend(this.kit, this.contract.methods.newCommitment) - extendCommitment = proxySend(this.kit, this.contract.methods.extendCommitment) + /** + * Notifies a Locked Gold commitment, allowing funds to be withdrawn after the notice + * period. + * @param value The amount of the commitment to eventually withdraw. + * @param noticePeriod The notice period of the Locked Gold commitment. + * @return CeloTransactionObject + */ + notifyCommitment: ( + value: string | number, + noticePeriod: string | number + ) => CeloTransactionObject = proxySend(this.kit, this.contract.methods.notifyCommitment) + + /** + * Creates an account. + * @return CeloTransactionObject + */ + createAccount: () => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.createAccount + ) + + /** + * Withdraws a notified commitment after the duration of the notice period. + * @param availabilityTime The availability time of the notified commitment. + * @return CeloTransactionObject + */ + withdrawCommitment: ( + availabilityTime: string | number + ) => CeloTransactionObject = proxySend(this.kit, this.contract.methods.withdrawCommitment) + + /** + * Redeems rewards accrued since the last redemption for the specified account. + * @return CeloTransactionObject + */ + redeemRewards: () => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.redeemRewards + ) + + /** + * Adds a Locked Gold commitment to `msg.sender`'s account. + * @param noticePeriod The notice period for the commitment. + * @return CeloTransactionObject + */ + newCommitment: (noticePeriod: string | number) => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.newCommitment + ) + + /** + * Rebonds a notified commitment, with notice period >= the remaining time to + * availability. + * + * @param value The amount of the commitment to rebond. + * @param availabilityTime The availability time of the notified commitment. + * @return CeloTransactionObject + */ + extendCommitment: ( + value: string | number, + availabilityTime: string | number + ) => CeloTransactionObject = proxySend(this.kit, this.contract.methods.extendCommitment) + + /** + * Returns whether or not a specified account is voting. + * @param account The address of the account. + * @return Whether or not the account is voting. + */ isVoting = proxyCall(this.contract.methods.isVoting) + /** * Query maximum notice period. * @returns Current maximum notice period. */ maxNoticePeriod = proxyCall(this.contract.methods.maxNoticePeriod, undefined, toBigNumber) + /** + * Returns the weight of a specified account. + * @param _account The address of the account. + * @return The weight of the specified account. + */ getAccountWeight = proxyCall(this.contract.methods.getAccountWeight, undefined, toBigNumber) /** * Get the delegate for a role. @@ -81,6 +149,10 @@ export class LockedGoldWrapper extends BaseWrapper { } } + /** + * Get voting details for an address + * @param accountOrVoterAddress Accout or Voter address + */ async getVotingDetails(accountOrVoterAddress: Address): Promise { const accountAddress = await this.contract.methods .getAccountFromDelegateAndRole(accountOrVoterAddress, Roles.Voting) @@ -93,12 +165,12 @@ export class LockedGoldWrapper extends BaseWrapper { } } - async getLockedCommitmentValue(account: string, noticePeriod: string): Promise { + async getLockedCommitmentValue(account: Address, noticePeriod: string): Promise { const commitment = await this.contract.methods.getLockedCommitment(account, noticePeriod).call() return this.getValueFromCommitment(commitment) } - async getLockedCommitments(account: string): Promise { + async getLockedCommitments(account: Address): Promise { return this.zipAccountTimesAndValuesToCommitments( account, this.contract.methods.getNoticePeriods, @@ -106,12 +178,12 @@ export class LockedGoldWrapper extends BaseWrapper { ) } - async getNotifiedCommitmentValue(account: string, availTime: string): Promise { + async getNotifiedCommitmentValue(account: Address, availTime: string): Promise { const commitment = await this.contract.methods.getNotifiedCommitment(account, availTime).call() return this.getValueFromCommitment(commitment) } - async getNotifiedCommitments(account: string): Promise { + async getNotifiedCommitments(account: Address): Promise { return this.zipAccountTimesAndValuesToCommitments( account, this.contract.methods.getAvailabilityTimes, @@ -119,7 +191,11 @@ export class LockedGoldWrapper extends BaseWrapper { ) } - async getCommitments(account: string): Promise { + /** + * Get commitments for an Account + * @param account Account address + */ + async getCommitments(account: Address): Promise { const locked = await this.getLockedCommitments(account) const notified = await this.getNotifiedCommitments(account) const weight = await this.getAccountWeight(account) @@ -193,7 +269,7 @@ export class LockedGoldWrapper extends BaseWrapper { return new BigNumber(commitment[0]) } - private async getParsedSignatureOfAddress(address: string, signer: string) { + private async getParsedSignatureOfAddress(address: Address, signer: string) { const hash = Web3.utils.soliditySha3({ type: 'address', value: address }) const signature = (await this.kit.web3.eth.sign(hash, signer)).slice(2) return { @@ -204,7 +280,7 @@ export class LockedGoldWrapper extends BaseWrapper { } private async zipAccountTimesAndValuesToCommitments( - account: string, + account: Address, timesFunc: (account: string) => TransactionObject, valueFunc: (account: string, time: string) => Promise ) { diff --git a/packages/contractkit/src/wrappers/StableTokenWrapper.ts b/packages/contractkit/src/wrappers/StableTokenWrapper.ts index da23172394c..ebe68a056d8 100644 --- a/packages/contractkit/src/wrappers/StableTokenWrapper.ts +++ b/packages/contractkit/src/wrappers/StableTokenWrapper.ts @@ -2,6 +2,8 @@ import BigNumber from 'bignumber.js' import { StableToken } from '../generated/types/StableToken' import { BaseWrapper, + CeloTransactionObject, + NumberLike, parseNumber, proxyCall, proxySend, @@ -29,53 +31,75 @@ export interface StableTokenConfig { */ export class StableTokenWrapper extends BaseWrapper { /** - * Querying allowance. - * @param from Account who has given the allowance. - * @param to Address of account to whom the allowance was given. - * @returns Amount of allowance. + * Gets the amount of owner's StableToken allowed to be spent by spender. + * @param accountOwner The owner of the StableToken. + * @param spender The spender of the StableToken. + * @return The amount of StableToken owner is allowing spender to spend. */ allowance = proxyCall(this.contract.methods.allowance, undefined, toBigNumber) + /** - * Returns the name of the token. - * @returns Name of the token. + * @return The name of the stable token. */ - name = proxyCall(this.contract.methods.name, undefined, (a: any) => a.toString()) + name: () => Promise = proxyCall(this.contract.methods.name) + /** - * Returns the three letter symbol of the token. - * @returns Symbol of the token. + * @return The symbol of the stable token. */ - symbol = proxyCall(this.contract.methods.symbol, undefined, (a: any) => a.toString()) + symbol: () => Promise = proxyCall(this.contract.methods.symbol) + /** - * Returns the number of decimals used in the token. - * @returns Number of decimals. + * @return The number of decimal places to which StableToken is divisible. */ decimals = proxyCall(this.contract.methods.decimals, undefined, toNumber) + /** * Returns the total supply of the token, that is, the amount of tokens currently minted. * @returns Total supply. */ totalSupply = proxyCall(this.contract.methods.totalSupply, undefined, toBigNumber) - balanceOf = proxyCall(this.contract.methods.balanceOf, undefined, toBigNumber) + + /** + * Gets the balance of the specified address using the presently stored inflation factor. + * @param owner The address to query the balance of. + * @return The balance of the specified address. + */ + balanceOf: (owner: string) => Promise = proxyCall( + this.contract.methods.balanceOf, + undefined, + toBigNumber + ) + minter = proxyCall(this.contract.methods.minter) owner = proxyCall(this.contract.methods.owner) - valueToUnits = proxyCall( + + /** + * Returns the units for a given value given the current inflation factor. + * @param value The value to convert to units. + * @return The units corresponding to `value` given the current inflation factor. + * @dev We don't compute the updated inflationFactor here because + * we assume any function calling this will have updated the inflation factor. + */ + valueToUnits: (value: NumberLike) => Promise = proxyCall( this.contract.methods.valueToUnits, tupleParser(parseNumber), toBigNumber ) - unitsToValue = proxyCall( + /** + * Returns the value of a given number of units given the current inflation factor. + * @param units The units to convert to value. + * @return The value corresponding to `units` given the current inflation factor. + */ + unitsToValue: (units: NumberLike) => Promise = proxyCall( this.contract.methods.unitsToValue, tupleParser(parseNumber), toBigNumber ) - approve = proxySend(this.kit, this.contract.methods.approve) mint = proxySend(this.kit, this.contract.methods.mint) burn = proxySend(this.kit, this.contract.methods.burn) - transferWithComment = proxySend(this.kit, this.contract.methods.transferWithComment) - transfer = proxySend(this.kit, this.contract.methods.transfer) - transferFrom = proxySend(this.kit, this.contract.methods.transferFrom) + setInflationParameters = proxySend(this.kit, this.contract.methods.setInflationParameters) /** @@ -109,4 +133,55 @@ export class StableTokenWrapper extends BaseWrapper { inflationParameters: res[3], } } + + /** + * Approve a user to transfer StableToken on behalf of another user. + * @param spender The address which is being approved to spend StableToken. + * @param value The amount of StableToken approved to the spender. + * @return True if the transaction succeeds. + */ + approve: (spender: string, value: string | number) => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.approve + ) + + /** + * Transfer token for a specified address + * @param to The address to transfer to. + * @param value The amount to be transferred. + * @param comment The transfer comment. + * @return True if the transaction succeeds. + */ + transferWithComment: ( + to: string, + value: string | number, + comment: string + ) => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.transferWithComment + ) + + /** + * Transfers `value` from `msg.sender` to `to` + * @param to The address to transfer to. + * @param value The amount to be transferred. + */ + + transfer: (to: string, value: string | number) => CeloTransactionObject = proxySend( + this.kit, + this.contract.methods.transfer + ) + + /** + * Transfers StableToken from one address to another on behalf of a user. + * @param from The address to transfer StableToken from. + * @param to The address to transfer StableToken to. + * @param value The amount of StableToken to transfer. + * @return True if the transaction succeeds. + */ + transferFrom: ( + from: string, + to: string, + value: string | number + ) => CeloTransactionObject = proxySend(this.kit, this.contract.methods.transferFrom) } From 21cbb94730d2f6d1b0b32cfab81f66f7c6831ced Mon Sep 17 00:00:00 2001 From: Mariano Cortesi Date: Thu, 3 Oct 2019 09:03:33 -0300 Subject: [PATCH 11/11] Make packages depend on git vesrion (not npm) (#1192) Several packages were depending on a fixed npm version; and not the package in the git repo; which defeat the monorepo purpose. --- packages/attestation-service/package.json | 4 +- packages/celotool/package.json | 4 +- packages/cli/package.json | 4 +- packages/contractkit/package.json | 4 +- packages/dappkit/package.json | 4 +- packages/mobile/package.json | 2 +- packages/notification-service/package.json | 4 +- packages/protocol/package.json | 2 +- packages/react-components/package.json | 2 +- .../transaction-metrics-exporter/package.json | 4 +- packages/utils/package.json | 2 +- packages/verification-pool-api/package.json | 4 +- packages/verifier/package.json | 2 +- packages/walletkit/package.json | 2 +- packages/web/package.json | 2 +- yarn.lock | 70 ------------------- 16 files changed, 23 insertions(+), 93 deletions(-) diff --git a/packages/attestation-service/package.json b/packages/attestation-service/package.json index 39cfc583511..f6674eab312 100644 --- a/packages/attestation-service/package.json +++ b/packages/attestation-service/package.json @@ -25,8 +25,8 @@ "lint": "tslint -c tslint.json --project ." }, "dependencies": { - "@celo/contractkit": "0.1.1", - "@celo/utils": "^0.1.0", + "@celo/contractkit": "0.2.0-dev", + "@celo/utils": "0.1.2-dev", "bignumber.js": "^7.2.0", "body-parser": "1.19.0", "debug": "^4.1.1", diff --git a/packages/celotool/package.json b/packages/celotool/package.json index fd4d2039aba..68b67744f7f 100644 --- a/packages/celotool/package.json +++ b/packages/celotool/package.json @@ -7,9 +7,9 @@ "license": "Apache-2.0", "dependencies": { "@celo/verification-pool-api": "^1.0.0", - "@celo/utils": "^0.1.0", + "@celo/utils": "0.1.2-dev", "@celo/walletkit": "^0.0.14", - "@celo/contractkit": "0.1.6", + "@celo/contractkit": "0.2.0-dev", "@google-cloud/monitoring": "0.7.1", "@google-cloud/pubsub": "^0.28.1", "@google-cloud/storage": "^2.4.3", diff --git a/packages/cli/package.json b/packages/cli/package.json index 6f1db7a1408..3df04df18d2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -29,8 +29,8 @@ "test": "TZ=UTC jest" }, "dependencies": { - "@celo/utils": "^0.1.0", - "@celo/contractkit": "^0.1.1", + "@celo/utils": "0.1.2-dev", + "@celo/contractkit": "0.2.0-dev", "@oclif/command": "^1", "@oclif/config": "^1", "@oclif/plugin-help": "^2", diff --git a/packages/contractkit/package.json b/packages/contractkit/package.json index d461819180e..120396551df 100644 --- a/packages/contractkit/package.json +++ b/packages/contractkit/package.json @@ -1,6 +1,6 @@ { "name": "@celo/contractkit", - "version": "0.1.6", + "version": "0.2.0-dev", "description": "Celo's ContractKit to interact with Celo network", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -26,7 +26,7 @@ }, "dependencies": { "@0x/subproviders": "^5.0.0", - "@celo/utils": "^0.1.0", + "@celo/utils": "0.1.2-dev", "@types/debug": "^4.1.5", "bignumber.js": "^7.2.0", "cross-fetch": "3.0.4", diff --git a/packages/dappkit/package.json b/packages/dappkit/package.json index 64ecf5686a3..109af1a11db 100644 --- a/packages/dappkit/package.json +++ b/packages/dappkit/package.json @@ -5,8 +5,8 @@ "build": "tsc" }, "dependencies": { - "@celo/contractkit": "0.1.5", - "@celo/utils": "^0.1.0", + "@celo/contractkit": "0.2.0-dev", + "@celo/utils": "0.1.2-dev", "expo": "^34.0.1", "expo-contacts": "6.0.0", "libphonenumber-js": "^1.7.22" diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 99b80cc07ed..79e71d9f0f2 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -48,7 +48,7 @@ "dependencies": { "@celo/client": "640a41f", "@celo/react-native-sms-retriever": "git+https://github.com/celo-org/react-native-sms-retriever#d3a2fdb", - "@celo/utils": "^0.1.0", + "@celo/utils": "0.1.2-dev", "@celo/walletkit": "^0.0.14", "@react-native-community/netinfo": "^2.0.4", "@segment/analytics-react-native": "^1.1.0-beta.1", diff --git a/packages/notification-service/package.json b/packages/notification-service/package.json index b38c17970d6..d1a6618db44 100644 --- a/packages/notification-service/package.json +++ b/packages/notification-service/package.json @@ -16,8 +16,8 @@ "deploy": "./deploy.sh" }, "dependencies": { - "@celo/contractkit": "0.1.5", - "@celo/utils": "^0.1.0", + "@celo/contractkit": "0.2.0-dev", + "@celo/utils": "0.1.2-dev", "async-polling": "^0.2.1", "bignumber.js": "^7.2.0", "dotenv": "^6.0.0", diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 2a53f18b185..47662055610 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -42,7 +42,7 @@ "@0x/sol-profiler": "^3.0.0", "@0x/sol-trace": "^2.0.16", "@0x/subproviders": "^5.0.0", - "@celo/utils": "^0.1.0", + "@celo/utils": "0.1.2-dev", "apollo-client": "^2.4.13", "bls12377js": "https://github.com/celo-org/bls12377js#4f596cabb659c4f8969ae4b617f185f2bc74cbbb", "chai-subset": "^1.6.0", diff --git a/packages/react-components/package.json b/packages/react-components/package.json index 408f8e65e47..f24d49f0c55 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -14,7 +14,7 @@ "test:watch": "export TZ=UTC && jest --watch" }, "dependencies": { - "@celo/utils": "^0.1.0", + "@celo/utils": "0.1.2-dev", "hoist-non-react-statics": "^3.3.0", "lodash": "^4.17.14", "react-native-autocomplete-input": "^3.6.0", diff --git a/packages/transaction-metrics-exporter/package.json b/packages/transaction-metrics-exporter/package.json index df546f94e81..e83dda25706 100644 --- a/packages/transaction-metrics-exporter/package.json +++ b/packages/transaction-metrics-exporter/package.json @@ -7,8 +7,8 @@ "license": "Apache-2.0", "private": true, "dependencies": { - "@celo/contractkit": "^0.1.1", - "@celo/utils": "^0.1.0", + "@celo/contractkit": "0.2.0-dev", + "@celo/utils": "0.1.2-dev", "express": "4.16.4", "prom-client": "11.2.0" }, diff --git a/packages/utils/package.json b/packages/utils/package.json index 0485521e709..32d186d2ffe 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@celo/utils", - "version": "0.1.1", + "version": "0.1.2-dev", "description": "Celo common utils", "author": "Celo", "license": "Apache-2.0", diff --git a/packages/verification-pool-api/package.json b/packages/verification-pool-api/package.json index bb3a18c742d..d070098eba2 100644 --- a/packages/verification-pool-api/package.json +++ b/packages/verification-pool-api/package.json @@ -22,8 +22,8 @@ "test:verbose": "jest --ci --verbose --runInBand --detectOpenHandles" }, "dependencies": { - "@celo/contractkit": "^0.1.5", - "@celo/utils": "^0.1.0", + "@celo/contractkit": "0.2.0-dev", + "@celo/utils": "0.1.2-dev", "bignumber.js": "^7.2.0", "eth-lib": "^0.2.8", "ethereumjs-util": "^5.2.0", diff --git a/packages/verifier/package.json b/packages/verifier/package.json index 5119768b01f..028a343f0e2 100644 --- a/packages/verifier/package.json +++ b/packages/verifier/package.json @@ -26,7 +26,7 @@ }, "dependencies": { "@celo/react-components": "1.0.0", - "@celo/utils": "^0.1.0", + "@celo/utils": "0.1.2-dev", "@react-native-community/netinfo": "^2.0.4", "@segment/analytics-react-native": "^1.1.0-beta.1", "@segment/analytics-react-native-firebase": "^1.1.0-beta.1", diff --git a/packages/walletkit/package.json b/packages/walletkit/package.json index 757c8e38ef9..473554f5f7f 100644 --- a/packages/walletkit/package.json +++ b/packages/walletkit/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@0x/subproviders": "^4.1.0", - "@celo/utils": "^0.1.0", + "@celo/utils": "0.1.2-dev", "@google-cloud/storage": "^2.3.3", "babel-jest": "^24.8.0", "bignumber.js": "^7.2.0", diff --git a/packages/web/package.json b/packages/web/package.json index 223a24e0f2b..de595a4315a 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -19,7 +19,7 @@ "test-licenses": "yarn licenses list --prod | grep '\\(─ GPL\\|─ (GPL-[1-9]\\.[0-9]\\+ OR GPL-[1-9]\\.[0-9]\\+)\\)' && echo 'Found GPL license(s). Use 'yarn licenses list --prod' to look up the offending package' || echo 'No GPL licenses found'" }, "dependencies": { - "@celo/utils": "^0.1.0", + "@celo/utils": "0.1.2-dev", "@segment/in-eu": "^0.2.1", "@sentry/browser": "^5.6.2", "@sentry/node": "^5.6.2", diff --git a/yarn.lock b/yarn.lock index b38486e9da9..ffa4029db39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2248,34 +2248,6 @@ resolved "https://registry.yarnpkg.com/@celo/client/-/client-0.0.142.tgz#ea15cf543f512c6d7ad1bc6611d3836c5bf06f40" integrity sha512-F5CEEsKNegFJzb9btk9OpUWTSL9gDNAqoi4fEZ+fEeYJR6763PPW8CHub+KrwslALcCeuSQiRSFzvtuCq85SgA== -"@celo/contractkit@0.1.1": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@celo/contractkit/-/contractkit-0.1.1.tgz#bd85812d883b58fbc8d9897a59e2f7311cc0e2eb" - integrity sha512-GQbLuLUEgKtwy2I8ZVi4P0HBVhh9MlT5S90vIrGznb9eZ2vwGahfOxHapVKcxw2I4tbVBNTTWWpcjzOtJZCY7Q== - dependencies: - "@celo/utils" "^0.0.6-beta5" - "@types/debug" "^4.1.5" - bignumber.js "^7.2.0" - debug "^4.1.1" - web3 "1.0.0-beta.37" - web3-utils "1.0.0-beta.37" - -"@celo/contractkit@0.1.5": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@celo/contractkit/-/contractkit-0.1.5.tgz#097052ac8f672f03480a1226f11f0c398c35c75e" - integrity sha512-vW8/ZluqJXx4SOB64XyFZjR3Z6qzcCUjXk6ec5613NH8DC/7jzxPCnbPsnxL4R+zuEajDWKfjjeH94Y1uLgVGA== - dependencies: - "@0x/subproviders" "^5.0.0" - "@celo/utils" "0.1.0" - "@types/debug" "^4.1.5" - bignumber.js "^7.2.0" - debug "^4.1.1" - eth-lib "^0.2.8" - web3 "1.0.0-beta.37" - web3-core-helpers "1.0.0-beta.37" - web3-eth-abi "1.0.0-beta.37" - web3-utils "1.0.0-beta.37" - "@celo/dev-cli@^2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@celo/dev-cli/-/dev-cli-2.0.3.tgz#67f61dfee373ad8b412925e386cf122aabada5f5" @@ -2314,43 +2286,6 @@ version "1.0.3" resolved "git+https://github.com/celo-org/react-native-sms-retriever#d3a2fdb148e3427c9a8c1c87acb61e79e2afcfee" -"@celo/utils@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@celo/utils/-/utils-0.1.0.tgz#081e0c9a7ea3e7b50cbc88e89b0f7ff8d5578499" - integrity sha512-sSE17+lu0dqG+Zo+H/aZYFXq436+Kd9xhXk5a36RtONXuJY8q6jfaXMFoBJ0Wft19rJM24e9F2aM84HM/IiUTQ== - dependencies: - "@umpirsky/country-list" "git://github.com/umpirsky/country-list#05fda51" - bigi "^1.1.0" - bignumber.js "^7.2.0" - bn.js "4.11.8" - buffer-reverse "^1.0.1" - country-data "^0.0.31" - crypto-js "^3.1.9-1" - elliptic "^6.4.1" - ethereumjs-util "^5.2.0" - futoin-hkdf "^1.0.3" - google-libphonenumber "^3.2.4" - keccak256 "^1.0.0" - lodash "^4.17.14" - web3-utils "1.0.0-beta.37" - -"@celo/utils@^0.0.6-beta5": - version "0.0.6-beta5" - resolved "https://registry.yarnpkg.com/@celo/utils/-/utils-0.0.6-beta5.tgz#a558cfc9dc68d9ec15888798c95cd6e5f1d3fe67" - integrity sha512-TGIDjr1aCcNRVv2Z3DZkjd71K/vNd6C18BPZw8VfKzSFyt0UcxkbN/X+loqvJSrwymtYgu/s6JdIQD3t14xiJA== - dependencies: - "@umpirsky/country-list" "git://github.com/umpirsky/country-list#05fda51" - bignumber.js "^7.2.0" - bn.js "4.11.8" - country-data "^0.0.31" - crypto-js "^3.1.9-1" - elliptic "^6.4.1" - ethereumjs-util "^5.2.0" - futoin-hkdf "^1.0.3" - google-libphonenumber "^3.2.1" - lodash "^4.17.14" - web3-utils "1.0.0-beta.37" - "@cnakazawa/watch@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" @@ -16367,11 +16302,6 @@ google-gax@^1.0.0, google-gax@^1.1.2: semver "^6.0.0" walkdir "^0.4.0" -google-libphonenumber@^3.2.1: - version "3.2.5" - resolved "https://registry.yarnpkg.com/google-libphonenumber/-/google-libphonenumber-3.2.5.tgz#2ebe6437fd3dbbffd65f4339ad1ba93b3dc56836" - integrity sha512-Y0r7MFCI11UDLn0KaMPBEInhROyIOkWkQIyvWMFVF2I+h+sHE3vbl5a7FVe39td6u/w+nlKDdUMP9dMOZyv+2Q== - google-libphonenumber@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/google-libphonenumber/-/google-libphonenumber-3.2.4.tgz#91d3fe62ca531f154165e6580b1c55ff6bd53abf"