diff --git a/app/components/Nav/App/__snapshots__/index.test.js.snap b/app/components/Nav/App/__snapshots__/index.test.js.snap index 83e5a735d4e..6eec58f9b46 100644 --- a/app/components/Nav/App/__snapshots__/index.test.js.snap +++ b/app/components/Nav/App/__snapshots__/index.test.js.snap @@ -160,11 +160,13 @@ exports[`App should render correctly 1`] = ` "childRouters": Object { "AdvancedSettings": null, "CompanySettings": null, + "ExperimentalSettings": null, "GeneralSettings": null, "RevealPrivateCredentialView": null, "SecuritySettings": null, "Settings": null, "SyncWithExtensionView": null, + "WalletConnectSessionsView": null, }, "getActionCreators": [Function], "getActionForPathAndParams": [Function], @@ -534,11 +536,13 @@ exports[`App should render correctly 1`] = ` "childRouters": Object { "AdvancedSettings": null, "CompanySettings": null, + "ExperimentalSettings": null, "GeneralSettings": null, "RevealPrivateCredentialView": null, "SecuritySettings": null, "Settings": null, "SyncWithExtensionView": null, + "WalletConnectSessionsView": null, }, "getActionCreators": [Function], "getActionForPathAndParams": [Function], diff --git a/app/components/Nav/Main/__snapshots__/index.test.js.snap b/app/components/Nav/Main/__snapshots__/index.test.js.snap index a0c600cb0d6..a79e97cd38c 100644 --- a/app/components/Nav/Main/__snapshots__/index.test.js.snap +++ b/app/components/Nav/Main/__snapshots__/index.test.js.snap @@ -168,11 +168,13 @@ exports[`Main should render correctly 1`] = ` "childRouters": Object { "AdvancedSettings": null, "CompanySettings": null, + "ExperimentalSettings": null, "GeneralSettings": null, "RevealPrivateCredentialView": null, "SecuritySettings": null, "Settings": null, "SyncWithExtensionView": null, + "WalletConnectSessionsView": null, }, "getActionCreators": [Function], "getActionForPathAndParams": [Function], @@ -428,11 +430,13 @@ exports[`Main should render correctly 1`] = ` "childRouters": Object { "AdvancedSettings": null, "CompanySettings": null, + "ExperimentalSettings": null, "GeneralSettings": null, "RevealPrivateCredentialView": null, "SecuritySettings": null, "Settings": null, "SyncWithExtensionView": null, + "WalletConnectSessionsView": null, }, "getActionCreators": [Function], "getActionForPathAndParams": [Function], diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index f5db73089c4..6706c43510f 100644 --- a/app/components/Nav/Main/index.js +++ b/app/components/Nav/Main/index.js @@ -16,6 +16,7 @@ import GeneralSettings from '../../Views/GeneralSettings'; import AdvancedSettings from '../../Views/AdvancedSettings'; import AppInformation from '../../UI/AppInformation'; import SecuritySettings from '../../Views/SecuritySettings'; +import ExperimentalSettings from '../../Views/ExperimentalSettings'; import Wallet from '../../Views/Wallet'; import TransactionsView from '../../Views/TransactionsView'; import SyncWithExtension from '../../Views/SyncWithExtension'; @@ -25,6 +26,7 @@ import Collectible from '../../Views/Collectible'; import CollectibleView from '../../Views/CollectibleView'; import Send from '../../Views/Send'; import RevealPrivateCredential from '../../Views/RevealPrivateCredential'; +import WalletConnectSessions from '../../Views/WalletConnectSessions'; import QrScanner from '../../Views/QRScanner'; import LockScreen from '../../Views/LockScreen'; import ProtectYourAccount from '../../Views/ProtectYourAccount'; @@ -49,6 +51,13 @@ import { colors } from '../../../styles/common'; import LockManager from '../../../core/LockManager'; import OnboardingWizard from '../../UI/OnboardingWizard'; import FadeOutOverlay from '../../UI/FadeOutOverlay'; +import { hexToBN, fromWei } from '../../../util/number'; +import { setTransactionObject } from '../../../actions/transaction'; +import PersonalSign from '../../UI/PersonalSign'; +import TypedSign from '../../UI/TypedSign'; +import Modal from 'react-native-modal'; +import WalletConnect from '../../../core/WalletConnect'; +import WalletConnectSessionApproval from '../../UI/WalletConnectSessionApproval'; const styles = StyleSheet.create({ flex: { @@ -59,6 +68,10 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: 'center', alignItems: 'center' + }, + bottomModal: { + justifyContent: 'flex-end', + margin: 0 } }); @@ -138,6 +151,9 @@ const MainNavigator = createStackNavigator( SecuritySettings: { screen: SecuritySettings }, + ExperimentalSettings: { + screen: ExperimentalSettings + }, CompanySettings: { screen: AppInformation }, @@ -146,6 +162,9 @@ const MainNavigator = createStackNavigator( }, RevealPrivateCredentialView: { screen: RevealPrivateCredential + }, + WalletConnectSessionsView: { + screen: WalletConnectSessions } }) }, @@ -266,11 +285,24 @@ class Main extends Component { /** * Current onboarding wizard step */ - wizardStep: PropTypes.number + wizardStep: PropTypes.number, + /** + * Action that sets a transaction + */ + setTransactionObject: PropTypes.func, + /** + * Object containing the information for the current transaction + */ + transaction: PropTypes.object }; state = { - forceReload: false + forceReload: false, + signMessage: false, + signMessageParams: { data: '' }, + signType: '', + walletConnectRequest: false, + walletConnectRequestInfo: {} }; backgroundMode = false; @@ -290,7 +322,6 @@ class Main extends Component { TransactionsNotificationManager.init(this.props.navigation); this.pollForIncomingTransactions(); AppState.addEventListener('change', this.handleAppStateChange); - this.lockManager = new LockManager(this.props.navigation, this.props.lockTime); PushNotification.configure({ @@ -314,6 +345,55 @@ class Main extends Component { } } }); + + Engine.context.TransactionController.hub.on('unapprovedTransaction', this.onUnapprovedTransaction); + + Engine.context.PersonalMessageManager.hub.on('unapprovedMessage', messageParams => { + const { title: currentPageTitle, url: currentPageUrl } = messageParams.meta; + delete messageParams.meta; + this.setState({ + signMessage: true, + signMessageParams: messageParams, + signType: 'personal', + currentPageTitle, + currentPageUrl + }); + }); + + Engine.context.TypedMessageManager.hub.on('unapprovedMessage', messageParams => { + const { title: currentPageTitle, url: currentPageUrl } = messageParams.meta; + delete messageParams.meta; + this.setState({ + signMessage: true, + signMessageParams: messageParams, + signType: 'typed', + currentPageTitle, + currentPageUrl + }); + }); + + WalletConnect.hub.on('walletconnectSessionRequest', peerInfo => { + this.setState({ walletConnectRequest: true, walletConnectRequestInfo: peerInfo }); + }); + WalletConnect.init(); + }; + + onUnapprovedTransaction = transactionMeta => { + if (this.props.transaction.value || this.props.transaction.to) { + return; + } + const { + transaction: { value, gas, gasPrice } + } = transactionMeta; + transactionMeta.transaction.value = hexToBN(value); + transactionMeta.transaction.readableValue = fromWei(transactionMeta.transaction.value); + transactionMeta.transaction.gas = hexToBN(gas); + transactionMeta.transaction.gasPrice = hexToBN(gasPrice); + this.props.setTransactionObject({ + ...{ symbol: 'ETH', type: 'ETHER_TRANSACTION', assetType: 'ETH', id: transactionMeta.id }, + ...transactionMeta.transaction + }); + this.props.navigation.push('ApprovalView'); }; handleAppStateChange = appState => { @@ -365,6 +445,9 @@ class Main extends Component { componentWillUnmount() { AppState.removeEventListener('change', this.handleAppStateChange); this.lockManager.stopListening(); + Engine.context.PersonalMessageManager.hub.removeAllListeners(); + Engine.context.TypedMessageManager.hub.removeAllListeners(); + Engine.context.TransactionController.hub.removeListener('unapprovedTransaction', this.onUnapprovedTransaction); } /** @@ -375,24 +458,127 @@ class Main extends Component { return wizardStep !== 5 && wizardStep > 0 && ; }; + onSignAction = () => { + this.setState({ signMessage: false }); + }; + + renderSigningModal = () => { + const { signMessage, signMessageParams, signType, currentPageTitle, currentPageUrl } = this.state; + return ( + + {signType === 'personal' && ( + + )} + {signType === 'typed' && ( + + )} + + ); + }; + + onWalletConnectSessionApproval = () => { + const { peerId } = this.state.walletConnectRequestInfo; + this.setState({ + walletConnectRequest: false, + walletConnectRequestInfo: {} + }); + WalletConnect.hub.emit('walletconnectSessionRequest::approved', peerId); + }; + + onWalletConnectSessionRejected = () => { + const peerId = this.state.walletConnectRequestInfo.peerId; + this.setState({ + walletConnectRequest: false, + walletConnectRequestInfo: {} + }); + WalletConnect.hub.emit('walletconnectSessionRequest::rejected', peerId); + }; + + renderWalletConnectSessionRequestModal = () => { + const { walletConnectRequest, walletConnectRequestInfo } = this.state; + + const meta = walletConnectRequestInfo.peerMeta || null; + + return ( + + + + ); + }; + render() { const { forceReload } = this.state; return ( - - {!forceReload ? : this.renderLoader()} - {this.renderOnboardingWizard()} - - - - + + + {!forceReload ? : this.renderLoader()} + {this.renderOnboardingWizard()} + + + + + {this.renderSigningModal()} + {this.renderWalletConnectSessionRequestModal()} + ); } } const mapStateToProps = state => ({ lockTime: state.settings.lockTime, - wizardStep: state.wizard.step + wizardStep: state.wizard.step, + transaction: state.transaction +}); + +const mapDispatchToProps = dispatch => ({ + setTransactionObject: asset => dispatch(setTransactionObject(asset)) }); -export default connect(mapStateToProps)(Main); +export default connect( + mapStateToProps, + mapDispatchToProps +)(Main); diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js index d635042ace5..88dede90ae2 100644 --- a/app/components/UI/Navbar/index.js +++ b/app/components/UI/Navbar/index.js @@ -14,6 +14,7 @@ import URL from 'url-parse'; import { strings } from '../../../../locales/i18n'; import AppConstants from '../../../core/AppConstants'; import TabCountIcon from '../../UI/Tabs/TabCountIcon'; +import WalletConnect from '../../../core/WalletConnect'; const HOMEPAGE_URL = 'about:blank'; const styles = StyleSheet.create({ @@ -438,6 +439,8 @@ export function getWalletNavbarOptions(title, navigation) { const onScanSuccess = data => { if (data.target_address) { navigation.navigate('SendView', { txMeta: data }); + } else if (data.walletConnectURI) { + WalletConnect.newSession(data.walletConnectURI); } }; diff --git a/app/components/UI/WalletConnectSessionApproval/__snapshots__/index.test.js.snap b/app/components/UI/WalletConnectSessionApproval/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..bf4849675a0 --- /dev/null +++ b/app/components/UI/WalletConnectSessionApproval/__snapshots__/index.test.js.snap @@ -0,0 +1,290 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WalletConnectSessionApproval should render correctly 1`] = ` + + + + + WALLETCONNECT REQUEST + + + + + + + + + + + + + + + + + + + + + + + Account 1 + + + + + + + + would like to + : + + + + View your + + + public address + + + + + + By clicking connect, you allow this dapp to view your public address. This is an important security step to protect your data from potential phishing risks. + + + + +`; diff --git a/app/components/UI/WalletConnectSessionApproval/index.js b/app/components/UI/WalletConnectSessionApproval/index.js new file mode 100644 index 00000000000..79b5b18adde --- /dev/null +++ b/app/components/UI/WalletConnectSessionApproval/index.js @@ -0,0 +1,246 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { StyleSheet, Text, View } from 'react-native'; +import Icon from 'react-native-vector-icons/FontAwesome'; +import ActionView from '../ActionView'; +import ElevatedView from 'react-native-elevated-view'; +import Identicon from '../Identicon'; +import { strings } from '../../../../locales/i18n'; +import { colors, fontStyles } from '../../../styles/common'; +import DeviceSize from '../../../util/DeviceSize'; +import WebsiteIcon from '../WebsiteIcon'; +import { renderAccountName } from '../../../util/address'; + +const styles = StyleSheet.create({ + root: { + backgroundColor: colors.white, + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + minHeight: '70%', + paddingBottom: DeviceSize.isIphoneX() ? 20 : 0 + }, + wrapper: { + paddingHorizontal: 25 + }, + title: { + ...fontStyles.bold, + color: colors.fontPrimary, + fontSize: 14, + marginVertical: 24, + textAlign: 'center' + }, + intro: { + ...fontStyles.normal, + textAlign: 'center', + color: colors.fontPrimary, + fontSize: 20, + marginVertical: 24 + }, + dappTitle: { + ...fontStyles.bold, + color: colors.fontPrimary, + fontSize: 20 + }, + permissions: { + alignItems: 'center', + borderBottomWidth: 1, + borderColor: colors.grey100, + borderTopWidth: 1, + display: 'flex', + flexDirection: 'row', + paddingHorizontal: 8, + paddingVertical: 16 + }, + permissionText: { + ...fontStyles.normal, + color: colors.fontPrimary, + flexGrow: 1, + flexShrink: 1, + fontSize: 14 + }, + permission: { + ...fontStyles.bold, + color: colors.fontPrimary, + fontSize: 14 + }, + warning: { + ...fontStyles.normal, + color: colors.fontPrimary, + fontSize: 14, + marginTop: 24 + }, + header: { + alignItems: 'flex-start', + display: 'flex', + flexDirection: 'row', + marginBottom: 12 + }, + headerTitle: { + ...fontStyles.normal, + color: colors.fontPrimary, + fontSize: 16, + textAlign: 'center' + }, + selectedAddress: { + ...fontStyles.normal, + color: colors.fontPrimary, + fontSize: 16, + marginTop: 12, + textAlign: 'center' + }, + headerUrl: { + ...fontStyles.normal, + color: colors.fontSecondary, + fontSize: 12, + textAlign: 'center' + }, + dapp: { + alignItems: 'center', + paddingHorizontal: 14, + width: '50%' + }, + graphic: { + alignItems: 'center', + position: 'absolute', + top: 12, + width: '100%' + }, + check: { + alignItems: 'center', + height: 2, + width: '33%' + }, + border: { + borderColor: colors.grey400, + borderStyle: 'dashed', + borderWidth: 1, + left: 0, + overflow: 'hidden', + position: 'absolute', + top: 12, + width: '100%', + zIndex: 1 + }, + checkWrapper: { + alignItems: 'center', + backgroundColor: colors.green500, + borderRadius: 12, + height: 24, + position: 'relative', + width: 24, + zIndex: 2 + }, + checkIcon: { + color: colors.white, + fontSize: 14, + lineHeight: 24 + }, + icon: { + borderRadius: 27, + marginBottom: 12, + height: 54, + width: 54 + } +}); + +/** + * WalletConnect request approval component + */ +class WalletConnectSessionApproval extends Component { + static propTypes = { + /** + * Object containing current page title, url, and icon href + */ + currentPageInformation: PropTypes.object, + /** + * Callback triggered on account access approval + */ + onConfirm: PropTypes.func, + /** + * Callback triggered on account access rejection + */ + onCancel: PropTypes.func, + /** + /* Identities object required to get account name + */ + identities: PropTypes.object, + /** + * A string that represents the selected address + */ + selectedAddress: PropTypes.string + }; + + render = () => { + const { + currentPageInformation: { title, url }, + onConfirm, + onCancel, + selectedAddress, + identities + } = this.props; + return ( + + + + {strings('accountApproval.walletconnect_title')} + + + + + + + + + {title} + + + {url} + + + + + + + + + + + + + + {renderAccountName(selectedAddress, identities)} + + + + + {title} + {strings('accountApproval.action')}: + + + + {strings('accountApproval.permission')} + {strings('accountApproval.address')} + + + + {strings('accountApproval.warning')} + + + + ); + }; +} + +const mapStateToProps = state => ({ + selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + identities: state.engine.backgroundState.PreferencesController.identities +}); + +export default connect(mapStateToProps)(WalletConnectSessionApproval); diff --git a/app/components/UI/WalletConnectSessionApproval/index.test.js b/app/components/UI/WalletConnectSessionApproval/index.test.js new file mode 100644 index 00000000000..e1feeefedac --- /dev/null +++ b/app/components/UI/WalletConnectSessionApproval/index.test.js @@ -0,0 +1,29 @@ +import React from 'react'; +import WalletConnectSessionApproval from './'; +import { shallow } from 'enzyme'; +import configureMockStore from 'redux-mock-store'; + +const mockStore = configureMockStore(); + +describe('WalletConnectSessionApproval', () => { + it('should render correctly', () => { + const initialState = { + engine: { + backgroundState: { + PreferencesController: { + selectedAddress: '0xe7E125654064EEa56229f273dA586F10DF96B0a1', + identities: { '0xe7E125654064EEa56229f273dA586F10DF96B0a1': { name: 'Account 1' } } + } + } + } + }; + + const wrapper = shallow( + , + { + context: { store: mockStore(initialState) } + } + ); + expect(wrapper.dive()).toMatchSnapshot(); + }); +}); diff --git a/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap b/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap index 4f2475c4806..60fd15309b9 100644 --- a/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap +++ b/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap @@ -172,48 +172,6 @@ exports[`Browser should render correctly 1`] = ` onSubmit={[Function]} /> - { + const { PersonalMessageManager } = Engine.context; + try { + const rawSig = await PersonalMessageManager.addUnapprovedMessageAsync({ + data: payload.params[1], + from: payload.params[0], + ...this.getPageMeta() + }); + return Promise.resolve({ result: rawSig, jsonrpc: payload.jsonrpc, id: payload.id }); + } catch (error) { + return Promise.reject({ error: error.message, jsonrpc: payload.jsonrpc, id: payload.id }); + } + }, + personal_sign: async payload => { + const { PersonalMessageManager } = Engine.context; + try { + const rawSig = await PersonalMessageManager.addUnapprovedMessageAsync({ + data: payload.params[0], + from: payload.params[1], + ...this.getPageMeta() + }); + return Promise.resolve({ result: rawSig, jsonrpc: payload.jsonrpc, id: payload.id }); + } catch (error) { + return Promise.reject({ error: error.message, jsonrpc: payload.jsonrpc, id: payload.id }); + } + }, + eth_signTypedData: async payload => { + const { TypedMessageManager } = Engine.context; + try { + const rawSig = await TypedMessageManager.addUnapprovedMessageAsync( + { + data: payload.params[0], + from: payload.params[1], + ...this.getPageMeta() + }, + 'V1' + ); + return Promise.resolve({ result: rawSig, jsonrpc: payload.jsonrpc, id: payload.id }); + } catch (error) { + return Promise.reject({ error: error.message, jsonrpc: payload.jsonrpc, id: payload.id }); + } + }, + eth_signTypedData_v3: async payload => { + const { TypedMessageManager } = Engine.context; + try { + const rawSig = await TypedMessageManager.addUnapprovedMessageAsync( + { + data: payload.params[1], + from: payload.params[0], + ...this.getPageMeta() + }, + 'V3' + ); + return Promise.resolve({ result: rawSig, jsonrpc: payload.jsonrpc, id: payload.id }); + } catch (error) { + return Promise.reject({ error: error.message, jsonrpc: payload.jsonrpc, id: payload.id }); + } + }, eth_requestAccounts: ({ hostname, params }) => { const { approvedHosts, privacyMode, selectedAddress } = this.props; const promise = new Promise((resolve, reject) => { @@ -575,17 +636,6 @@ export class BrowserTab extends PureComponent { await this.setState({ entryScriptWeb3: updatedentryScriptWeb3 + SPA_urlChangeListener }); - Engine.context.TransactionController.hub.on('unapprovedTransaction', this.onUnapprovedTransaction); - - Engine.context.PersonalMessageManager.hub.on('unapprovedMessage', messageParams => { - if (!this.isTabActive()) return false; - this.setState({ signMessage: true, signMessageParams: messageParams, signType: 'personal' }); - }); - Engine.context.TypedMessageManager.hub.on('unapprovedMessage', messageParams => { - if (!this.isTabActive()) return false; - this.setState({ signMessage: true, signMessageParams: messageParams, signType: 'typed' }); - }); - Engine.context.AssetsController.hub.on('pendingSuggestedAsset', suggestedAssetMeta => { if (!this.isTabActive()) return false; this.setState({ watchAsset: true, suggestedAssetMeta }); @@ -643,25 +693,6 @@ export class BrowserTab extends PureComponent { return true; }; - onUnapprovedTransaction = transactionMeta => { - if (!this.isTabActive()) return false; - if (this.props.transaction.value || this.props.transaction.to) { - return; - } - const { - transaction: { value, gas, gasPrice } - } = transactionMeta; - transactionMeta.transaction.value = hexToBN(value); - transactionMeta.transaction.readableValue = fromWei(transactionMeta.transaction.value); - transactionMeta.transaction.gas = hexToBN(gas); - transactionMeta.transaction.gasPrice = hexToBN(gasPrice); - this.props.setTransactionObject({ - ...{ symbol: 'ETH', type: 'ETHER_TRANSACTION', assetType: 'ETH', id: transactionMeta.id }, - ...transactionMeta.transaction - }); - this.props.navigation.push('ApprovalView'); - }; - async loadUrl() { if (!this.isTabActive()) return; const { navigation } = this.props; @@ -703,10 +734,7 @@ export class BrowserTab extends PureComponent { componentWillUnmount() { this.mounted = false; // Remove all Engine listeners - Engine.context.PersonalMessageManager.hub.removeAllListeners(); - Engine.context.TypedMessageManager.hub.removeAllListeners(); Engine.context.AssetsController.hub.removeAllListeners(); - Engine.context.TransactionController.hub.removeListener('unapprovedTransaction', this.onUnapprovedTransaction); Engine.context.TransactionController.hub.removeListener('networkChange', this.reload); if (Platform.OS === 'ios') { this.state.scrollAnim && this.state.scrollAnim.removeAllListeners(); @@ -920,9 +948,6 @@ export class BrowserTab extends PureComponent { ipfsWebsite: false, showApprovalDialog: false, showPhishingModal: false, - signMessage: false, - signMessageParams: { data: '' }, - signType: '', timeout: false, url: HOMEPAGE_URL, scrollAnim, @@ -1517,46 +1542,6 @@ export class BrowserTab extends PureComponent { ); }; - onSignAction = () => { - this.setState({ signMessage: false }); - }; - - renderSigningModal = () => { - const { signMessage, signMessageParams, signType, currentPageTitle, currentPageUrl } = this.state; - return ( - - {signType === 'personal' && ( - - )} - {signType === 'typed' && ( - - )} - - ); - }; - onCancelWatchAsset = () => { this.setState({ watchAsset: false }); }; @@ -1610,7 +1595,7 @@ export class BrowserTab extends PureComponent { backdropOpacity={0.7} animationInTiming={300} animationOutTiming={300} - onSwipeComplete={this.onSignAction} + onSwipeComplete={this.onAccountsReject} swipeDirection={'down'} > ) : null} {!isHidden && this.renderUrlModal()} - {!isHidden && this.renderSigningModal()} {!isHidden && this.renderApprovalModal()} {!isHidden && this.renderPhishingModal()} {!isHidden && this.renderWatchAssetModal()} @@ -1760,7 +1744,6 @@ const mapStateToProps = state => ({ privacyMode: state.privacy.privacyMode, searchEngine: state.settings.searchEngine, whitelist: state.browser.whitelist, - transaction: state.transaction, activeTab: state.browser.activeTab }); diff --git a/app/components/Views/ExperimentalSettings/__snapshots__/index.test.js.snap b/app/components/Views/ExperimentalSettings/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..20a1380162d --- /dev/null +++ b/app/components/Views/ExperimentalSettings/__snapshots__/index.test.js.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ExperimentalSettings should render correctly 1`] = ` +<_class + style={ + Object { + "backgroundColor": "#FFFFFF", + "flex": 1, + "padding": 24, + "paddingBottom": 48, + } + } +> + + + + + WalletConnect Sessions + + + View the list of active WalletConnect sessions + + + VIEW SESSIONS + + + + + +`; diff --git a/app/components/Views/ExperimentalSettings/index.js b/app/components/Views/ExperimentalSettings/index.js new file mode 100644 index 00000000000..a6f28734ba5 --- /dev/null +++ b/app/components/Views/ExperimentalSettings/index.js @@ -0,0 +1,78 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { StyleSheet, Text, ScrollView, View } from 'react-native'; +import StyledButton from '../../UI/StyledButton'; +import { colors, fontStyles } from '../../../styles/common'; +import { getNavigationOptionsTitle } from '../../UI/Navbar'; +import { strings } from '../../../../locales/i18n'; + +const styles = StyleSheet.create({ + wrapper: { + backgroundColor: colors.white, + flex: 1, + padding: 24, + paddingBottom: 48 + }, + title: { + ...fontStyles.normal, + color: colors.fontPrimary, + fontSize: 20, + lineHeight: 20 + }, + desc: { + ...fontStyles.normal, + color: colors.grey500, + fontSize: 14, + lineHeight: 20, + marginTop: 12 + }, + setting: { + marginTop: 50 + }, + firstSetting: { + marginTop: 0 + }, + clearHistoryConfirm: { + marginTop: 18 + }, + inner: { + paddingBottom: 112 + } +}); + +/** + * Main view for app Experimental Settings + */ +export default class ExperimentalSettings extends Component { + static propTypes = { + /** + /* navigation object required to push new views + */ + navigation: PropTypes.object + }; + + static navigationOptions = ({ navigation }) => + getNavigationOptionsTitle(strings('app_settings.experimental_title'), navigation); + + goToWalletConnectSessions = () => { + this.props.navigation.navigate('WalletConnectSessionsView'); + }; + + render = () => ( + + + + {strings('experimental_settings.wallet_connect_dapps')} + {strings('experimental_settings.wallet_connect_dapps_desc')} + + {strings('experimental_settings.wallet_connect_dapps_cta').toUpperCase()} + + + + + ); +} diff --git a/app/components/Views/ExperimentalSettings/index.test.js b/app/components/Views/ExperimentalSettings/index.test.js new file mode 100644 index 00000000000..5c186d0fd6b --- /dev/null +++ b/app/components/Views/ExperimentalSettings/index.test.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import ExperimentalSettings from './'; +import configureMockStore from 'redux-mock-store'; + +describe('ExperimentalSettings', () => { + const mockStore = configureMockStore(); + + it('should render correctly', () => { + const initialState = { + privacy: { approvedHosts: {}, privacyMode: true }, + browser: { history: [] }, + settings: { lockTime: 1000 }, + engine: { + backgroundState: { + PreferencesController: { selectedAddress: '0x', identities: { '0x': { name: 'Account 1' } } }, + AccountTrackerController: { accounts: {} } + } + } + }; + + const wrapper = shallow( + , + { + context: { store: mockStore(initialState) } + } + ); + expect(wrapper.dive()).toMatchSnapshot(); + }); +}); diff --git a/app/components/Views/QRScanner/index.js b/app/components/Views/QRScanner/index.js index ca714b70b6c..4009b314342 100644 --- a/app/components/Views/QRScanner/index.js +++ b/app/components/Views/QRScanner/index.js @@ -91,6 +91,9 @@ export default class QrScanner extends Component { } else if (content.split('metamask-sync:').length > 1) { this.shouldReadBarCode = false; data = { content }; + } else if (content.split('wc:').length > 1) { + this.shouldReadBarCode = false; + data = { walletConnectURI: content }; } else { // EIP-945 allows scanning arbitrary data data = content; diff --git a/app/components/Views/Settings/__snapshots__/index.test.js.snap b/app/components/Views/Settings/__snapshots__/index.test.js.snap index 35498a93d7e..bba472242a1 100644 --- a/app/components/Views/Settings/__snapshots__/index.test.js.snap +++ b/app/components/Views/Settings/__snapshots__/index.test.js.snap @@ -26,6 +26,11 @@ exports[`Settings should render correctly 1`] = ` onPress={[Function]} title="Security & Privacy" /> + + { + navigation.push('ExperimentalSettings'); + }} + /> { diff --git a/app/components/Views/WalletConnectSessions/__snapshots__/index.test.js.snap b/app/components/Views/WalletConnectSessions/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..05925be6e1f --- /dev/null +++ b/app/components/Views/WalletConnectSessions/__snapshots__/index.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WalletConnectSessions should render correctly 1`] = `""`; diff --git a/app/components/Views/WalletConnectSessions/index.js b/app/components/Views/WalletConnectSessions/index.js new file mode 100644 index 00000000000..576e733a905 --- /dev/null +++ b/app/components/Views/WalletConnectSessions/index.js @@ -0,0 +1,168 @@ +import React, { Component } from 'react'; +import { Alert, ScrollView, SafeAreaView, StyleSheet, View, Text, TouchableOpacity } from 'react-native'; +import { colors, fontStyles } from '../../../styles/common'; +import { strings } from '../../../../locales/i18n'; +import { getNavigationOptionsTitle } from '../../UI/Navbar'; +import WebsiteIcon from '../../UI/WebsiteIcon'; +import AsyncStorage from '@react-native-community/async-storage'; +import ActionSheet from 'react-native-actionsheet'; +import WalletConnect from '../../../core/WalletConnect'; +import Logger from '../../../util/Logger'; + +const styles = StyleSheet.create({ + wrapper: { + backgroundColor: colors.white, + flex: 1 + }, + scrollviewContent: { + paddingTop: 20 + }, + websiteIcon: { + width: 44, + height: 44 + }, + row: { + flexDirection: 'row', + paddingVertical: 10, + paddingHorizontal: 20, + borderBottomColor: colors.grey000, + borderBottomWidth: 1 + }, + info: { + marginLeft: 20, + flex: 1 + }, + name: { + ...fontStyles.bold, + fontSize: 16, + marginBottom: 10 + }, + desc: { + marginBottom: 10, + ...fontStyles.normal, + fontSize: 12 + }, + url: { + marginBottom: 10, + ...fontStyles.normal, + fontSize: 12, + color: colors.fontSecondary + }, + emptyWrapper: { + flex: 1, + justifyContent: 'center', + alignItems: 'center' + }, + emptyText: { + ...fontStyles.normal, + fontSize: 16 + } +}); + +/** + * View that displays all the active WalletConnect Sessions + */ +export default class WalletConnectSessions extends Component { + static navigationOptions = ({ navigation }) => + getNavigationOptionsTitle(strings(`experimental_settings.wallet_connect_dapps`), navigation); + + state = { + sessions: [] + }; + + actionSheet = null; + + sessionToRemove = null; + + componentDidMount() { + this.loadSessions(); + } + + loadSessions = async () => { + let sessions = []; + const sessionData = await AsyncStorage.getItem('@MetaMask:walletconnectSessions'); + if (sessionData) { + sessions = JSON.parse(sessionData); + } + this.setState({ ready: true, sessions }); + }; + + renderDesc = meta => { + const { description } = meta; + if (description) { + return {meta.description}; + } + return null; + }; + + onLongPress = session => { + this.sessionToRemove = session; + this.actionSheet.show(); + }; + + createActionSheetRef = ref => { + this.actionSheet = ref; + }; + + onActionSheetPress = index => (index === 0 ? this.killSession() : null); + + killSession = async () => { + try { + await WalletConnect.killSession(this.sessionToRemove.peerId); + Alert.alert( + strings('walletconnect_sessions.session_ended_title'), + strings('walletconnect_sessions.session_ended_desc') + ); + this.loadSessions(); + } catch (e) { + Logger.error('WC: Failed to kill session', e); + } + }; + + renderSessions = () => { + const { sessions } = this.state; + return sessions.map(session => ( + this.onLongPress(session)} + key={`session_${session.peerId}`} + style={styles.row} + > + + + {session.peerMeta.name} + {session.peerId} + {session.peerMeta.url} + {this.renderDesc(session.peerMeta)} + + + )); + }; + + renderEmpty = () => ( + + {strings('walletconnect_sessions.no_active_sessions')} + + ); + + render = () => { + const { ready, sessions } = this.state; + if (!ready) return null; + + return ( + + + {sessions.length ? this.renderSessions() : this.renderEmpty()} + + + + ); + }; +} diff --git a/app/components/Views/WalletConnectSessions/index.test.js b/app/components/Views/WalletConnectSessions/index.test.js new file mode 100644 index 00000000000..5df63ba5303 --- /dev/null +++ b/app/components/Views/WalletConnectSessions/index.test.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import WalletConnectSessions from './'; + +describe('WalletConnectSessions', () => { + it('should render correctly', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/core/Engine.js b/app/core/Engine.js index cd2754b24c4..3a527f2801e 100644 --- a/app/core/Engine.js +++ b/app/core/Engine.js @@ -77,60 +77,6 @@ class Engine { } catch (error) { end(error); } - }, - eth_sign: async (payload, next, end) => { - const { PersonalMessageManager } = this.datamodel.context; - try { - const rawSig = await PersonalMessageManager.addUnapprovedMessageAsync({ - data: payload.params[1], - from: payload.params[0] - }); - end(undefined, rawSig); - } catch (error) { - end(error); - } - }, - personal_sign: async (payload, next, end) => { - const { PersonalMessageManager } = this.datamodel.context; - try { - const rawSig = await PersonalMessageManager.addUnapprovedMessageAsync({ - data: payload.params[0], - from: payload.params[1] - }); - end(undefined, rawSig); - } catch (error) { - end(error); - } - }, - eth_signTypedData: async (payload, next, end) => { - const { TypedMessageManager } = this.datamodel.context; - try { - const rawSig = await TypedMessageManager.addUnapprovedMessageAsync( - { - data: payload.params[0], - from: payload.params[1] - }, - 'V1' - ); - end(undefined, rawSig); - } catch (error) { - end(error); - } - }, - eth_signTypedData_v3: async (payload, next, end) => { - const { TypedMessageManager } = this.datamodel.context; - try { - const rawSig = await TypedMessageManager.addUnapprovedMessageAsync( - { - data: payload.params[1], - from: payload.params[0] - }, - 'V3' - ); - end(undefined, rawSig); - } catch (error) { - end(error); - } } }, getAccounts: (end, payload) => { diff --git a/app/core/WalletConnect.js b/app/core/WalletConnect.js new file mode 100644 index 00000000000..8bd54c999c9 --- /dev/null +++ b/app/core/WalletConnect.js @@ -0,0 +1,273 @@ +import RNWalletConnect from '@walletconnect/react-native'; +import Engine from './Engine'; +import Logger from '../util/Logger'; +// eslint-disable-next-line import/no-nodejs-modules +import { EventEmitter } from 'events'; +import AsyncStorage from '@react-native-community/async-storage'; + +const hub = new EventEmitter(); +let connectors = []; +const CLIENT_OPTIONS = { + clientMeta: { + // Required + description: 'MetaMask Mobile app', + url: 'https://metamask.io', + icons: ['https://raw.githubusercontent.com/MetaMask/brand-resources/master/SVG/metamask-fox.svg'], + name: 'MetaMask', + ssl: true + } +}; + +const persistSessions = async () => { + const sessions = connectors + .filter(connector => connector && connector.walletConnector && connector && connector.walletConnector.connected) + .map(connector => connector.walletConnector.session); + + await AsyncStorage.setItem('@MetaMask:walletconnectSessions', JSON.stringify(sessions)); +}; + +class WalletConnect { + selectedAddress = null; + chainId = null; + + constructor(options) { + this.walletConnector = new RNWalletConnect(options, CLIENT_OPTIONS); + /** + * Subscribe to session requests + */ + this.walletConnector.on('session_request', async (error, payload) => { + if (error) { + throw error; + } + + try { + await this.sessionRequest(payload.params[0]); + + const { network } = Engine.context.NetworkController.state; + this.selectedAddress = Engine.context.PreferencesController.state.selectedAddress; + const approveData = { + chainId: parseInt(network, 10), + accounts: [this.selectedAddress] + }; + this.walletConnector.approveSession(approveData); + persistSessions(); + } catch (e) { + this.walletConnector.rejectSession(); + } + }); + + /** + * Subscribe to call requests + */ + this.walletConnector.on('call_request', async (error, payload) => { + if (error) { + throw error; + } + + const meta = this.walletConnector.session.peerMeta; + + if (payload.method) { + if (payload.method === 'eth_sendTransaction') { + const { TransactionController } = Engine.context; + try { + const txParams = {}; + txParams.to = payload.params[0].to; + txParams.from = payload.params[0].from; + txParams.value = payload.params[0].value; + txParams.gasLimit = payload.params[0].gasLimit; + txParams.gasPrice = payload.params[0].gasPrice; + if (payload.params[0].data && payload.params[0].data.toString() !== '0x') { + txParams.data = payload.params[0].data; + } + + const hash = await (await TransactionController.addTransaction(txParams)).result; + this.walletConnector.approveRequest({ + id: payload.id, + result: hash + }); + } catch (error) { + this.walletConnector.rejectRequest({ + id: payload.id, + error + }); + } + } else if (payload.method === 'eth_sign' || payload.method === 'personal_sign') { + const { PersonalMessageManager } = Engine.context; + + try { + const rawSig = await PersonalMessageManager.addUnapprovedMessageAsync({ + data: payload.params[1], + from: payload.params[0], + meta: { + title: meta && meta.name, + url: meta && meta.url, + icon: meta && meta.icons && meta.icons[0] + } + }); + this.walletConnector.approveRequest({ + id: payload.id, + result: rawSig + }); + } catch (error) { + this.walletConnector.rejectRequest({ + id: payload.id, + error + }); + } + } else if (payload.method && payload.method === 'eth_signTypedData') { + const { TypedMessageManager } = Engine.context; + try { + const rawSig = await TypedMessageManager.addUnapprovedMessageAsync( + { + data: payload.params[1], + from: payload.params[0], + meta: { + title: meta && meta.name, + url: meta && meta.url, + icon: meta && meta.icons && meta.icons[0] + } + }, + 'V3' + ); + + this.walletConnector.approveRequest({ + id: payload.id, + result: rawSig + }); + } catch (error) { + this.walletConnector.rejectRequest({ + id: payload.id, + error + }); + } + } + } + }); + + this.walletConnector.on('disconnect', error => { + if (error) { + throw error; + } + + // delete walletConnector + this.walletConnector = null; + persistSessions(); + }); + + this.walletConnector.on('session_update', (error, payload) => { + if (error) { + throw error; + } + Logger.log('session_update FROM WC:', error, payload); + }); + + Engine.context.TransactionController.hub.on('networkChange', this.onNetworkChange); + Engine.context.PreferencesController.subscribe(this.onAccountChange); + const { selectedAddress } = Engine.context.PreferencesController.state; + const { network } = Engine.context.NetworkController.state; + + this.selectedAddress = selectedAddress; + this.chainId = network; + } + + onAccountChange = () => { + const { selectedAddress } = Engine.context.PreferencesController.state; + + if (selectedAddress !== this.selectedAddress) { + this.selectedAddress = selectedAddress; + this.updateSession(); + } + }; + + onNetworkChange = () => { + const { network } = Engine.context.NetworkController.state; + // Wait while the network is set + if (network !== 'loading' && network !== this.chainId) { + this.chainId = network; + this.updateSession(); + } + }; + + updateSession = () => { + const { network } = Engine.context.NetworkController.state; + const { selectedAddress } = Engine.context.PreferencesController.state; + const sessionData = { + chainId: parseInt(network, 10), + accounts: [selectedAddress] + }; + this.walletConnector.updateSession(sessionData); + }; + + killSession = () => { + this.walletConnector && this.walletConnector.killSession(); + this.walletConnector = null; + }; + + sessionRequest = peerInfo => + new Promise((resolve, reject) => { + hub.emit('walletconnectSessionRequest', peerInfo); + + hub.on('walletconnectSessionRequest::approved', peerId => { + if (peerInfo.peerId === peerId) { + resolve(true); + } + }); + hub.on('walletconnectSessionRequest::rejected', peerId => { + if (peerInfo.peerId === peerId) { + reject(false); + } + }); + }); +} + +const instance = { + async init() { + const sessionData = await AsyncStorage.getItem('@MetaMask:walletconnectSessions'); + if (sessionData) { + const sessions = JSON.parse(sessionData); + sessions.forEach(session => { + connectors.push(new WalletConnect({ session })); + }); + } + }, + connectors() { + return connectors; + }, + newSession(uri) { + connectors.push(new WalletConnect({ uri })); + }, + getSessions: async () => { + let sessions = []; + const sessionData = await AsyncStorage.getItem('@MetaMask:walletconnectSessions'); + if (sessionData) { + sessions = JSON.parse(sessionData); + } + return sessions; + }, + killSession: async id => { + // 1) First kill the session + const connectorToKill = connectors.find( + connector => connector && connector.walletConnector && connector.walletConnector.session.peerId === id + ); + if (connectorToKill) { + await connectorToKill.killSession(); + } + // 2) Remove from the list of connectors + connectors = connectors.filter( + connector => + connector && + connector.walletConnector && + connector.walletConnector.connected && + connector.walletConnector.session.peerId !== id + ); + // 3) Persist the list + await persistSessions(); + }, + hub, + shutdown() { + Engine.context.TransactionController.hub.removeAllListeners(); + Engine.context.PreferencesController.unsubscribe(); + } +}; + +export default instance; diff --git a/app/util/networks.js b/app/util/networks.js index dc2897c3426..717d8bca4d3 100644 --- a/app/util/networks.js +++ b/app/util/networks.js @@ -10,21 +10,25 @@ const NetworkList = { mainnet: { name: 'Ethereum Main Network', networkId: 1, + chanId: 1, color: '#3cc29e' }, ropsten: { name: 'Ropsten Test Network', networkId: 3, + chainId: 3, color: '#ff4a8d' }, kovan: { name: 'Kovan Test Network', networkId: 42, + chainId: 42, color: '#7057ff' }, rinkeby: { name: 'Rinkeby Test Network', networkId: 4, + chainId: 4, color: '#f6c343' }, rpc: { diff --git a/locales/en.json b/locales/en.json index da237094f9f..c979619a536 100644 --- a/locales/en.json +++ b/locales/en.json @@ -297,6 +297,8 @@ "security_title": "Security & Privacy", "security_desc": "Privacy settings, MetaMetrics, private key and wallet seed phrase", "info_title": "About MetaMask", + "experimental_title": "Experimental", + "experimental_desc": "WalletConnect & more...", "legal_title": "Legal", "conversion_title": "Currency conversion", "conversion_desc": "Display fiat values in using a specific currency throughout the application.", @@ -577,6 +579,7 @@ }, "accountApproval": { "title": "CONNECT REQUEST", + "walletconnect_title": "WALLETCONNECT REQUEST", "action": "would like to", "connect": "CONNECT", "cancel": "CANCEL", @@ -766,5 +769,18 @@ "public_address": "Public Address", "public_address_qr_code": "Public Address QR Code", "coming_soon": "Coming soon..." - } + }, + "experimental_settings": { + "wallet_connect_dapps": "WalletConnect Sessions", + "wallet_connect_dapps_desc": "View the list of active WalletConnect sessions", + "wallet_connect_dapps_cta": "VIEW SESSIONS" + }, + "walletconnect_sessions": { + "no_active_sessions": "You have no active sessions", + "end_session_title": "End Session", + "end": "End", + "cancel": "Cancel", + "session_ended_title": "Session Ended", + "session_ended_desc": "The selected session has been terminated" + } } diff --git a/locales/es.json b/locales/es.json index 253ff68edad..77114fcc6c3 100644 --- a/locales/es.json +++ b/locales/es.json @@ -296,6 +296,8 @@ "security_title": "Seguridad y privacidad", "security_desc": "Opciones de privacidad, MetaMetrics y frase semilla de la billetera", "info_title": "Acerca de MetaMask", + "experimental_title": "Experimental", + "experimental_desc": "WalletConnect & más...", "legal_title": "Legal", "conversion_title": "Conversión de moneda", "conversion_desc": "Mostrar valores en fiat usando una moneda específica en toda la aplicación.", @@ -567,6 +569,7 @@ }, "accountApproval": { "title": "SOLICITUD DE CONEXIÓN ", + "walletconnect_title": "SOLICITUD DE CONEXIÓN VIA WALLETCONNECT ", "action": "le gustaría", "connect": "CONECTAR", "cancel": "CANCELAR", @@ -754,5 +757,18 @@ "public_address": "Dirección Pública", "public_address_qr_code": "Código QR de Dirección Pública", "coming_soon": "Pronto..." - } + }, + "experimental_settings": { + "wallet_connect_dapps": "Sesiones de WalletConnect", + "wallet_connect_dapps_desc": "Accede a la lista de sesiones activas de WalletConnect", + "wallet_connect_dapps_cta": "VER SESIONES" + }, + "walletconnect_sessions": { + "no_active_sessions": "No tienes sesiones activas", + "end_session_title": "Finalizar Sesión", + "end": "Finalizar", + "cancel": "Cancelar", + "session_ended_title": "Sesión Finalizada", + "session_ended_desc": "La sesión seleccionada ha sido terminada" + } } diff --git a/package-lock.json b/package-lock.json index 6145c37284a..e42cca827b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2532,6 +2532,47 @@ "integrity": "sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA==", "dev": true }, + "@walletconnect/core": { + "version": "1.0.0-beta.18", + "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-1.0.0-beta.18.tgz", + "integrity": "sha512-xWs9WkXFlTwSCkUzqXVF49BtCHwtzyAQL9x1MCFWTSGlqK8xapSH+2PEq4k2CHw/Zjh22oK/qBvuYgCdo+RFuw==", + "requires": { + "@walletconnect/types": "^1.0.0-beta.17", + "@walletconnect/utils": "^1.0.0-beta.18" + } + }, + "@walletconnect/react-native": { + "version": "1.0.0-beta.18", + "resolved": "https://registry.npmjs.org/@walletconnect/react-native/-/react-native-1.0.0-beta.18.tgz", + "integrity": "sha512-VL4GcXi1WdnnfjnvSkrPR4lDKhF3hdc31tFo7iuu4bZa52qeMTE36NBxSix+Nr1PlINE375K+ErqtuiBMq0FkQ==", + "requires": { + "@walletconnect/core": "^1.0.0-beta.18", + "@walletconnect/types": "^1.0.0-beta.17", + "@walletconnect/utils": "^1.0.0-beta.18" + } + }, + "@walletconnect/types": { + "version": "1.0.0-beta.18", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-1.0.0-beta.18.tgz", + "integrity": "sha512-cqhVcyNdXEDvnUUD8r7cWI6ZHeIsIOriJMIkOuxslWbaHadnJ0SGStJ9sSINibAMD/ZUEabQZRWA18BxL4jRvQ==" + }, + "@walletconnect/utils": { + "version": "1.0.0-beta.18", + "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-1.0.0-beta.18.tgz", + "integrity": "sha512-IgpJyHmtgRmset5gZZ9gTGziZQ5fKWtTud41vosKtQhgvh8+HQkPVWPqj/KxQupLFWK3/ACo1CqlxoKAjctpOQ==", + "requires": { + "@walletconnect/types": "^1.0.0-beta.17", + "js-sha3": "^0.8.0", + "lodash.isnumber": "^3.0.3" + }, + "dependencies": { + "js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + } + } + }, "@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -13100,6 +13141,11 @@ "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", "dev": true }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", diff --git a/package.json b/package.json index 030b65536cf..83dcd0ff8d2 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "dependencies": { "@react-native-community/async-storage": "1.2.0", "@tradle/react-native-http": "2.0.1", + "@walletconnect/react-native": "1.0.0-beta.22", "babel-plugin-transform-inline-environment-variables": "0.4.3", "base-64": "0.1.0", "bignumber.js": "8.1.1",