diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js
index d8065f0feb2..a75d80f12ee 100644
--- a/app/components/Nav/Main/MainNavigator.js
+++ b/app/components/Nav/Main/MainNavigator.js
@@ -24,6 +24,7 @@ import RevealPrivateCredential from '../../Views/RevealPrivateCredential';
import WalletConnectSessions from '../../Views/WalletConnectSessions';
import OfflineMode from '../../Views/OfflineMode';
import QrScanner from '../../Views/QRScanner';
+import ConnectQRHardware from '../../Views/ConnectQRHardware';
import LockScreen from '../../Views/LockScreen';
import EnterPasswordSimple from '../../Views/EnterPasswordSimple';
import ChoosePassword from '../../Views/ChoosePassword';
@@ -321,6 +322,16 @@ const SetPasswordFlow = () => (
);
+const ConnectQRHardwareFlow = () => (
+
+
+
+);
+
const MainNavigator = () => (
(
+
diff --git a/app/components/Nav/Main/RootRPCMethodsUI.js b/app/components/Nav/Main/RootRPCMethodsUI.js
index dca19dd7f6e..ddbf3dd49df 100644
--- a/app/components/Nav/Main/RootRPCMethodsUI.js
+++ b/app/components/Nav/Main/RootRPCMethodsUI.js
@@ -45,7 +45,11 @@ import BigNumber from 'bignumber.js';
import { getTokenList } from '../../../reducers/tokens';
import { toLowerCaseEquals } from '../../../util/general';
import { ApprovalTypes } from '../../../core/RPCMethods/RPCMethodMiddleware';
+import { KEYSTONE_TX_CANCELED } from '../../../constants/error';
+import AnalyticsV2 from '../../../util/analyticsV2';
import { mockTheme, useAppThemeFromContext } from '../../../util/theme';
+import withQRHardwareAwareness from '../../UI/QRHardware/withQRHardwareAwareness';
+import QRSigningModal from '../../UI/QRHardware/QRSigningModal';
import { networkSwitched } from '../../../actions/onboardNetwork';
const hstInterface = new ethers.utils.Interface(abi);
@@ -193,7 +197,7 @@ const RootRPCMethodsUI = (props) => {
const autoSign = useCallback(
async (transactionMeta) => {
- const { TransactionController } = Engine.context;
+ const { TransactionController, KeyringController } = Engine.context;
try {
TransactionController.hub.once(`${transactionMeta.id}:finished`, (transactionMeta) => {
if (transactionMeta.status === 'submitted') {
@@ -213,12 +217,17 @@ const RootRPCMethodsUI = (props) => {
trackSwaps(ANALYTICS_EVENT_OPTS.SWAP_COMPLETED, transactionMeta);
}
});
+ await KeyringController.resetQRKeyringState();
await TransactionController.approveTransaction(transactionMeta.id);
} catch (error) {
- Alert.alert(strings('transactions.transaction_error'), error && error.message, [
- { text: strings('navigation.ok') },
- ]);
- Logger.error(error, 'error while trying to send transaction (Main)');
+ if (!error?.message.startsWith(KEYSTONE_TX_CANCELED)) {
+ Alert.alert(strings('transactions.transaction_error'), error && error.message, [
+ { text: strings('navigation.ok') },
+ ]);
+ Logger.error(error, 'error while trying to send transaction (Main)');
+ } else {
+ AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.QR_HARDWARE_TRANSACTION_CANCELED);
+ }
}
},
[props.swapsTransactions, trackSwaps]
@@ -380,6 +389,13 @@ const RootRPCMethodsUI = (props) => {
);
+ const renderQRSigningModal = () => {
+ const { isSigningQRObject, QRState, approveModalVisible, dappTransactionModalVisible } = props;
+ const shouldRenderThisModal =
+ !showPendingApproval && !approveModalVisible && !dappTransactionModalVisible && isSigningQRObject;
+ return shouldRenderThisModal && ;
+ };
+
const onWalletConnectSessionApproval = () => {
const { peerId } = walletConnectRequestInfo;
setWalletConnectRequest(false);
@@ -674,6 +690,7 @@ const RootRPCMethodsUI = (props) => {
{renderSwitchCustomNetworkModal()}
{renderAccountsApprovalModal()}
{renderWatchAssetModal()}
+ {renderQRSigningModal()}
);
};
@@ -720,6 +737,8 @@ RootRPCMethodsUI.propTypes = {
* Chain id
*/
chainId: PropTypes.string,
+ isSigningQRObject: PropTypes.bool,
+ QRState: PropTypes.object,
/**
* updates redux when network is switched
*/
@@ -744,4 +763,4 @@ const mapDispatchToProps = (dispatch) => ({
networkSwitched: ({ networkUrl, networkStatus }) => dispatch(networkSwitched({ networkUrl, networkStatus })),
});
-export default connect(mapStateToProps, mapDispatchToProps)(RootRPCMethodsUI);
+export default connect(mapStateToProps, mapDispatchToProps)(withQRHardwareAwareness(RootRPCMethodsUI));
diff --git a/app/components/UI/AccountApproval/__snapshots__/index.test.tsx.snap b/app/components/UI/AccountApproval/__snapshots__/index.test.tsx.snap
index e2f36364f54..3870d06cf44 100644
--- a/app/components/UI/AccountApproval/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/AccountApproval/__snapshots__/index.test.tsx.snap
@@ -12,6 +12,7 @@ exports[`AccountApproval should render correctly 1`] = `
}
dispatch={[Function]}
networkType="ropsten"
+ selectedAddress="0xe7E125654064EEa56229f273dA586F10DF96B0a1"
tokensLength={0}
/>
`;
diff --git a/app/components/UI/AccountApproval/index.js b/app/components/UI/AccountApproval/index.js
index 14fc964fc64..cb2e2677a31 100644
--- a/app/components/UI/AccountApproval/index.js
+++ b/app/components/UI/AccountApproval/index.js
@@ -11,6 +11,7 @@ import Device from '../../../util/device';
import NotificationManager from '../../../core/NotificationManager';
import AnalyticsV2 from '../../../util/analyticsV2';
import URL from 'url-parse';
+import { getAddressAccountType } from '../../../util/address';
import { ThemeContext, mockTheme } from '../../../util/theme';
import { ACCOUNT_APROVAL_MODAL_CONTAINER_ID, CANCEL_BUTTON_ID } from '../../../constants/test-ids';
@@ -78,6 +79,10 @@ class AccountApproval extends PureComponent {
* Callback triggered on account access rejection
*/
onCancel: PropTypes.func,
+ /**
+ * A string that represents the selected address
+ */
+ selectedAddress: PropTypes.string,
/**
* Number of tokens
*/
@@ -106,9 +111,10 @@ class AccountApproval extends PureComponent {
getAnalyticsParams = () => {
try {
- const { currentPageInformation, chainId, networkType } = this.props;
+ const { currentPageInformation, chainId, networkType, selectedAddress } = this.props;
const url = new URL(currentPageInformation?.url);
return {
+ account_type: getAddressAccountType(selectedAddress),
dapp_host_name: url?.host,
dapp_url: currentPageInformation?.url,
network_name: networkType,
@@ -219,6 +225,7 @@ class AccountApproval extends PureComponent {
const mapStateToProps = (state) => ({
accountsLength: Object.keys(state.engine.backgroundState.AccountTrackerController.accounts || {}).length,
+ selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress,
tokensLength: state.engine.backgroundState.TokensController.tokens.length,
networkType: state.engine.backgroundState.NetworkController.provider.type,
chainId: state.engine.backgroundState.NetworkController.provider.chainId,
diff --git a/app/components/UI/AccountInfoCard/index.js b/app/components/UI/AccountInfoCard/index.js
index 07d63b71b8a..1702dcd1f03 100644
--- a/app/components/UI/AccountInfoCard/index.js
+++ b/app/components/UI/AccountInfoCard/index.js
@@ -8,6 +8,8 @@ import { strings } from '../../../../locales/i18n';
import { connect } from 'react-redux';
import { renderAccountName, renderShortAddress } from '../../../util/address';
import { getTicker } from '../../../util/transactions';
+import Engine from '../../../core/Engine';
+import { QR_HARDWARE_WALLET_DEVICE } from '../../../constants/keyringTypes';
import Device from '../../../util/device';
import { ThemeContext, mockTheme } from '../../../util/theme';
@@ -19,7 +21,8 @@ const createStyles = (colors) =>
borderWidth: 1,
borderColor: colors.border.default,
borderRadius: 10,
- padding: 16,
+ padding: Device.isMediumDevice() ? 8 : 16,
+ alignItems: 'center',
},
identicon: {
marginRight: 8,
@@ -29,6 +32,7 @@ const createStyles = (colors) =>
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'flex-start',
+ marginRight: 8,
},
accountNameAndAddress: {
width: '100%',
@@ -42,18 +46,40 @@ const createStyles = (colors) =>
marginRight: 2,
color: colors.text.default,
},
+ accountNameSmall: {
+ fontSize: 12,
+ },
accountAddress: {
flexGrow: 1,
...fontStyles.bold,
fontSize: 16,
color: colors.text.default,
},
+ accountAddressSmall: {
+ fontSize: 12,
+ },
balanceText: {
...fontStyles.thin,
fontSize: 14,
alignSelf: 'flex-start',
color: colors.text.default,
},
+ balanceTextSmall: {
+ fontSize: 12,
+ },
+ tag: {
+ borderRadius: 14,
+ borderWidth: 1,
+ borderColor: colors.text.default,
+ padding: 4,
+ minWidth: 42,
+ },
+ tagText: {
+ textAlign: 'center',
+ fontSize: 8,
+ ...fontStyles.bold,
+ color: colors.text.default,
+ },
});
class AccountInfoCard extends PureComponent {
@@ -82,18 +108,44 @@ class AccountInfoCard extends PureComponent {
* Declares the operation being performed i.e. 'signing'
*/
operation: PropTypes.string,
+ /**
+ * Clarify should show fiat balance
+ */
+ showFiatBalance: PropTypes.bool,
/**
* Current selected ticker
*/
ticker: PropTypes.string,
};
+ state = {
+ isHardwareKeyring: false,
+ };
+
+ componentDidMount() {
+ const { KeyringController } = Engine.context;
+ const { selectedAddress } = this.props;
+ KeyringController.getAccountKeyringType(selectedAddress).then((type) => {
+ if (type === QR_HARDWARE_WALLET_DEVICE) {
+ this.setState({ isHardwareKeyring: true });
+ }
+ });
+ }
+
render() {
- const { accounts, selectedAddress, identities, conversionRate, currentCurrency, operation, ticker } =
- this.props;
+ const {
+ accounts,
+ selectedAddress,
+ identities,
+ conversionRate,
+ currentCurrency,
+ operation,
+ ticker,
+ showFiatBalance = true,
+ } = this.props;
+ const { isHardwareKeyring } = this.state;
const colors = this.context.colors || mockTheme.colors;
const styles = createStyles(colors);
-
const weiBalance = hexToBN(accounts[selectedAddress].balance);
const balance = `(${renderFromWei(weiBalance)} ${getTicker(ticker)})`;
const accountLabel = renderAccountName(selectedAddress, identities);
@@ -104,19 +156,34 @@ class AccountInfoCard extends PureComponent {
-
+
{accountLabel}
-
+
({address})
{operation === 'signing' ? null : (
-
- {strings('signature_request.balance_title')} {dollarBalance} {balance}
+
+ {strings('signature_request.balance_title')} {showFiatBalance ? dollarBalance : ''}{' '}
+ {balance}
)}
+ {isHardwareKeyring && (
+
+ {strings('transaction.hardware')}
+
+ )}
);
}
diff --git a/app/components/UI/AccountList/AccountElement/index.js b/app/components/UI/AccountList/AccountElement/index.js
index 33c61205680..2fcda6c4208 100644
--- a/app/components/UI/AccountList/AccountElement/index.js
+++ b/app/components/UI/AccountList/AccountElement/index.js
@@ -54,7 +54,7 @@ const createStyles = (colors) =>
},
importedView: {
flex: 0.5,
- alignItems: 'center',
+ alignItems: 'flex-start',
marginTop: 2,
},
accountMain: {
@@ -71,7 +71,6 @@ const createStyles = (colors) =>
...fontStyles.bold,
},
importedWrapper: {
- width: 73,
paddingHorizontal: 10,
paddingVertical: 3,
borderRadius: 10,
@@ -122,18 +121,21 @@ class AccountElement extends PureComponent {
render() {
const { disabled, updatedBalanceFromStore, ticker } = this.props;
- const { address, name, ens, isSelected, isImported, balanceError } = this.props.item;
+ const { address, name, ens, isSelected, isImported, balanceError, isQRHardware } = this.props.item;
const colors = this.context.colors || mockTheme.colors;
const styles = createStyles(colors);
const selected = isSelected ? : null;
- const imported = isImported ? (
-
-
- {strings('accounts.imported')}
-
-
- ) : null;
+ const tag =
+ isImported || isQRHardware ? (
+
+
+
+ {strings(isImported ? 'accounts.imported' : 'transaction.hardware')}
+
+
+
+ ) : null;
return (
true}>
@@ -161,7 +163,7 @@ class AccountElement extends PureComponent {
)}
- {!!imported && {imported}}
+ {!!tag && tag}
{selected}
diff --git a/app/components/UI/AccountList/index.js b/app/components/UI/AccountList/index.js
index 9adf0876ee6..6c5317b2a63 100644
--- a/app/components/UI/AccountList/index.js
+++ b/app/components/UI/AccountList/index.js
@@ -1,4 +1,5 @@
import React, { PureComponent } from 'react';
+import { KeyringTypes } from '@metamask/controllers';
import Engine from '../../../core/Engine';
import PropTypes from 'prop-types';
import {
@@ -46,12 +47,13 @@ const createStyles = (colors) =>
height: 5,
borderRadius: 4,
backgroundColor: colors.border.default,
+ opacity: Device.isAndroid() ? 0.6 : 0.5,
},
accountsWrapper: {
flex: 1,
},
footer: {
- height: Device.isIphoneX() ? 140 : 110,
+ height: Device.isIphoneX() ? 200 : 170,
paddingBottom: Device.isIphoneX() ? 30 : 0,
justifyContent: 'center',
flexDirection: 'column',
@@ -101,6 +103,10 @@ class AccountList extends PureComponent {
* function to be called when importing an account
*/
onImportAccount: PropTypes.func,
+ /**
+ * function to be called when connect to a QR hardware
+ */
+ onConnectHardware: PropTypes.func,
/**
* Current provider ticker
*/
@@ -221,6 +227,11 @@ class AccountList extends PureComponent {
});
};
+ connectHardware = () => {
+ this.props.onConnectHardware();
+ AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.CONNECT_HARDWARE_WALLET);
+ };
+
addAccount = async () => {
if (this.state.loading) return;
this.mounted && this.setState({ loading: true });
@@ -253,7 +264,19 @@ class AccountList extends PureComponent {
let ret = false;
for (const keyring of allKeyrings) {
if (keyring.accounts.includes(address)) {
- ret = keyring.type !== 'HD Key Tree';
+ ret = keyring.type === KeyringTypes.simple;
+ break;
+ }
+ }
+
+ return ret;
+ }
+
+ isQRHardware(allKeyrings, address) {
+ let ret = false;
+ for (const keyring of allKeyrings) {
+ if (keyring.accounts.includes(address)) {
+ ret = keyring.type === KeyringTypes.qr;
break;
}
}
@@ -314,6 +337,7 @@ class AccountList extends PureComponent {
const identityAddressChecksummed = toChecksumAddress(address);
const isSelected = identityAddressChecksummed === selectedAddress;
const isImported = this.isImported(allKeyrings, identityAddressChecksummed);
+ const isQRHardware = this.isQRHardware(allKeyrings, identityAddressChecksummed);
let balance = 0x0;
if (accounts[identityAddressChecksummed]) {
balance = accounts[identityAddressChecksummed].balance;
@@ -327,6 +351,7 @@ class AccountList extends PureComponent {
balance,
isSelected,
isImported,
+ isQRHardware,
balanceError,
};
});
@@ -391,6 +416,13 @@ class AccountList extends PureComponent {
>
{strings('accounts.import_account')}
+
+ {strings('accounts.connect_hardware')}
+
)}
diff --git a/app/components/UI/AccountOverview/index.js b/app/components/UI/AccountOverview/index.js
index bc23d6e2be3..52f9b866a58 100644
--- a/app/components/UI/AccountOverview/index.js
+++ b/app/components/UI/AccountOverview/index.js
@@ -18,7 +18,7 @@ import { newAssetTransaction } from '../../../actions/transaction';
import Device from '../../../util/device';
import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics';
import { renderFiat } from '../../../util/number';
-import { renderAccountName } from '../../../util/address';
+import { isQRHardwareAccount, renderAccountName } from '../../../util/address';
import { getEther } from '../../../util/transactions';
import { doENSReverseLookup, isDefaultAccountName } from '../../../util/ENSUtils';
import { isSwapsAllowed } from '../Swaps/utils';
@@ -61,6 +61,27 @@ const createStyles = (colors) =>
labelInput: {
marginBottom: Device.isAndroid() ? -10 : 0,
},
+ labelWrapper: {
+ flexDirection: 'row',
+ },
+ tag: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginTop: 2,
+ padding: 4,
+ paddingHorizontal: 8,
+ borderWidth: 1,
+ borderColor: colors.text.default,
+ height: 28,
+ borderRadius: 14,
+ },
+ tagText: {
+ fontSize: 12,
+ ...fontStyles.bold,
+ minWidth: 32,
+ textAlign: 'center',
+ color: colors.text.default,
+ },
addressWrapper: {
backgroundColor: colors.primary.muted,
borderRadius: 40,
@@ -314,6 +335,8 @@ class AccountOverview extends PureComponent {
if (!address) return null;
const { accountLabelEditable, accountLabel, ens } = this.state;
+ const isQRHardwareWalletAccount = isQRHardwareAccount(address);
+
return (
) : (
-
-
- {isDefaultAccountName(name) && ens ? ens : name}
-
-
+
+
+
+ {isDefaultAccountName(name) && ens ? ens : name}
+
+
+ {isQRHardwareWalletAccount && (
+
+ {strings('transaction.hardware')}
+
+ )}
+
)}
{fiatBalance}
diff --git a/app/components/UI/ApproveTransactionReview/__snapshots__/index.test.tsx.snap b/app/components/UI/ApproveTransactionReview/__snapshots__/index.test.tsx.snap
index 6e67892fbaf..3bcefd01f6a 100644
--- a/app/components/UI/ApproveTransactionReview/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/ApproveTransactionReview/__snapshots__/index.test.tsx.snap
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`ApproveTransactionModal should render correctly 1`] = ``;
+exports[`ApproveTransactionModal should render correctly 1`] = ``;
diff --git a/app/components/UI/ApproveTransactionReview/index.js b/app/components/UI/ApproveTransactionReview/index.js
index aa806758eb7..cadfd2df191 100644
--- a/app/components/UI/ApproveTransactionReview/index.js
+++ b/app/components/UI/ApproveTransactionReview/index.js
@@ -6,7 +6,7 @@ import { getApproveNavbar } from '../../UI/Navbar';
import { fontStyles } from '../../../styles/common';
import { connect } from 'react-redux';
import { getHost } from '../../../util/browser';
-import { safeToChecksumAddress, renderShortAddress } from '../../../util/address';
+import { safeToChecksumAddress, renderShortAddress, getAddressAccountType } from '../../../util/address';
import Engine from '../../../core/Engine';
import { strings } from '../../../../locales/i18n';
import { setTransactionObject } from '../../../actions/transaction';
@@ -32,6 +32,7 @@ import AccountInfoCard from '../../UI/AccountInfoCard';
import TransactionReviewDetailsCard from '../../UI/TransactionReview/TransactionReviewDetailsCard';
import Device from '../../../util/device';
import AppConstants from '../../../core/AppConstants';
+import { UINT256_HEX_MAX_VALUE } from '../../../constants/transaction';
import { WALLET_CONNECT_ORIGIN } from '../../../util/walletconnect';
import { withNavigation } from '@react-navigation/compat';
import { getNetworkName, isMainNet, isMainnetByChainId } from '../../../util/networks';
@@ -45,6 +46,8 @@ import { getTokenList } from '../../../reducers/tokens';
import TransactionReviewEIP1559 from '../../UI/TransactionReview/TransactionReviewEIP1559';
import ClipboardManager from '../../../core/ClipboardManager';
import { ThemeContext, mockTheme } from '../../../util/theme';
+import withQRHardwareAwareness from '../QRHardware/withQRHardwareAwareness';
+import QRSigningDetails from '../QRHardware/QRSigningDetails';
const { hexToBN } = util;
const createStyles = (colors) =>
@@ -60,7 +63,7 @@ const createStyles = (colors) =>
textAlign: 'center',
color: colors.text.default,
lineHeight: 34,
- marginVertical: 16,
+ marginVertical: 8,
paddingHorizontal: 16,
},
explanation: {
@@ -136,6 +139,12 @@ const createStyles = (colors) =>
actionViewWrapper: {
height: Device.isMediumDevice() ? 200 : 280,
},
+ actionViewChildren: {
+ height: 300,
+ },
+ actionViewQRObject: {
+ height: 648,
+ },
paddingHorizontal: {
paddingHorizontal: 16,
},
@@ -165,6 +174,10 @@ class ApproveTransactionReview extends PureComponent {
static navigationOptions = ({ navigation }) => getApproveNavbar('approve.title', navigation);
static propTypes = {
+ /**
+ * A string that represents the selected address
+ */
+ selectedAddress: PropTypes.string,
/**
* Callback triggered when this transaction is cancelled
*/
@@ -289,6 +302,8 @@ class ApproveTransactionReview extends PureComponent {
* Check if nickname is saved
*/
nicknameExists: PropTypes.bool,
+ isSigningQRObject: PropTypes.bool,
+ QRState: PropTypes.object,
};
state = {
@@ -353,13 +368,14 @@ class ApproveTransactionReview extends PureComponent {
getAnalyticsParams = () => {
try {
- const { activeTabUrl, transaction, onSetAnalyticsParams } = this.props;
+ const { activeTabUrl, transaction, onSetAnalyticsParams, selectedAddress } = this.props;
const { tokenSymbol, originalApproveAmount, encodedAmount } = this.state;
const { NetworkController } = Engine.context;
const { chainId, type } = NetworkController?.state?.provider || {};
const isDapp = !Object.values(AppConstants.DEEPLINKS).includes(transaction?.origin);
- const unlimited = encodedAmount === 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
+ const unlimited = encodedAmount === UINT256_HEX_MAX_VALUE;
const params = {
+ account_type: getAddressAccountType(selectedAddress),
dapp_host_name: transaction?.origin,
dapp_url: isDapp ? activeTabUrl : undefined,
network_name: type,
@@ -781,8 +797,33 @@ class ApproveTransactionReview extends PureComponent {
});
};
+ renderQRDetails() {
+ const { host, spenderAddress } = this.state;
+ const {
+ activeTabUrl,
+ transaction: { origin },
+ QRState,
+ } = this.props;
+ const styles = this.getStyles();
+ return (
+
+
+
+
+ );
+ }
+
render = () => {
const { viewDetails, editPermissionVisible } = this.state;
+ const { isSigningQRObject } = this.props;
return (
@@ -790,6 +831,8 @@ class ApproveTransactionReview extends PureComponent {
? this.renderTransactionReview()
: editPermissionVisible
? this.renderEditPermission()
+ : isSigningQRObject
+ ? this.renderQRDetails()
: this.renderDetails()}
);
@@ -798,6 +841,8 @@ class ApproveTransactionReview extends PureComponent {
const mapStateToProps = (state) => ({
accounts: state.engine.backgroundState.AccountTrackerController.accounts,
+ selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress,
+ conversionRate: state.engine.backgroundState.CurrencyRateController.conversionRate,
ticker: state.engine.backgroundState.NetworkController.provider.ticker,
transaction: getNormalizedTxState(state),
accountsLength: Object.keys(state.engine.backgroundState.AccountTrackerController.accounts || {}).length,
@@ -817,4 +862,7 @@ const mapDispatchToProps = (dispatch) => ({
ApproveTransactionReview.contextType = ThemeContext;
-export default connect(mapStateToProps, mapDispatchToProps)(withNavigation(ApproveTransactionReview));
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(withNavigation(withQRHardwareAwareness(ApproveTransactionReview)));
diff --git a/app/components/UI/DrawerView/index.js b/app/components/UI/DrawerView/index.js
index 6de1c087f74..234b6e33796 100644
--- a/app/components/UI/DrawerView/index.js
+++ b/app/components/UI/DrawerView/index.js
@@ -45,6 +45,7 @@ import { collectiblesSelector } from '../../../reducers/collectibles';
import { getCurrentRoute } from '../../../reducers/navigation';
import { ScrollView } from 'react-native-gesture-handler';
import { isZero } from '../../../util/lodash';
+import { KeyringTypes } from '@metamask/controllers';
import { ThemeContext, mockTheme } from '../../../util/theme';
import NetworkInfo from '../NetworkInfo';
import sanitizeUrl from '../../../util/sanitizeUrl';
@@ -233,7 +234,7 @@ const createStyles = (colors) =>
paddingVertical: 3,
borderRadius: 10,
borderWidth: 1,
- color: colors.icon.default,
+ borderColor: colors.icon.default,
},
importedText: {
color: colors.icon.default,
@@ -458,6 +459,31 @@ class DrawerView extends PureComponent {
return ret;
}
+ renderTag() {
+ let tag = null;
+ const colors = this.context.colors || mockTheme.colors;
+ const styles = createStyles(colors);
+ const { keyrings, selectedAddress } = this.props;
+ const allKeyrings = keyrings && keyrings.length ? keyrings : Engine.context.KeyringController.state.keyrings;
+ for (const keyring of allKeyrings) {
+ if (keyring.accounts.includes(selectedAddress)) {
+ if (keyring.type === KeyringTypes.simple) {
+ tag = strings('accounts.imported');
+ } else if (keyring.type === KeyringTypes.qr) {
+ tag = strings('transaction.hardware');
+ }
+ break;
+ }
+ }
+ return tag ? (
+
+
+ {tag}
+
+
+ ) : null;
+ }
+
async componentDidUpdate() {
const route = findRouteNameFromNavigatorState(this.props.navigation.dangerouslyGetState().routes);
if (!this.props.passwordSet || !this.props.seedphraseBackedUp) {
@@ -752,6 +778,12 @@ class DrawerView extends PureComponent {
this.hideDrawer();
};
+ onConnectHardware = () => {
+ this.toggleAccountsModal();
+ this.props.navigation.navigate('ConnectQRHardwareFlow');
+ this.hideDrawer();
+ };
+
hasBlockExplorer = (providerType) => {
const { frequentRpcList } = this.props;
if (providerType === RPC) {
@@ -1078,13 +1110,7 @@ class DrawerView extends PureComponent {
style={styles.accountAddress}
type={'short'}
/>
- {this.isCurrentAccountImported() && (
-
-
- {strings('accounts.imported')}
-
-
- )}
+ {this.renderTag()}
@@ -1242,6 +1268,7 @@ class DrawerView extends PureComponent {
keyrings={keyrings}
onAccountChange={this.onAccountChange}
onImportAccount={this.onImportAccount}
+ onConnectHardware={this.onConnectHardware}
ticker={ticker}
/>
diff --git a/app/components/UI/EthereumAddress/index.js b/app/components/UI/EthereumAddress/index.js
index 70d8db22bb1..d20668dd578 100644
--- a/app/components/UI/EthereumAddress/index.js
+++ b/app/components/UI/EthereumAddress/index.js
@@ -1,8 +1,7 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Text } from 'react-native';
-import { renderShortAddress, renderFullAddress, renderSlightlyLongAddress } from '../../../util/address';
-import { isValidAddress } from 'ethereumjs-util';
+import { formatAddress } from '../../../util/address';
/**
* View that renders an ethereum address
@@ -32,25 +31,10 @@ class EthereumAddress extends PureComponent {
this.state = {
ensName: null,
- address: this.formatAddress(address, type),
+ address: formatAddress(address, type),
};
}
- formatAddress(rawAddress, type) {
- let formattedAddress = rawAddress;
-
- if (isValidAddress(rawAddress)) {
- if (type && type === 'short') {
- formattedAddress = renderShortAddress(rawAddress);
- } else if (type && type === 'mid') {
- formattedAddress = renderSlightlyLongAddress(rawAddress);
- } else {
- formattedAddress = renderFullAddress(rawAddress);
- }
- }
- return formattedAddress;
- }
-
componentDidUpdate(prevProps) {
if (prevProps.address !== this.props.address) {
requestAnimationFrame(() => {
@@ -61,7 +45,7 @@ class EthereumAddress extends PureComponent {
formatAndResolveIfNeeded() {
const { address, type } = this.props;
- const formattedAddress = this.formatAddress(address, type);
+ const formattedAddress = formatAddress(address, type);
this.setState({ address: formattedAddress, ensName: null });
}
diff --git a/app/components/UI/HintModal/__snapshots__/index.test.tsx.snap b/app/components/UI/HintModal/__snapshots__/index.test.tsx.snap
index 9c6281c09c3..35b8ec3b688 100644
--- a/app/components/UI/HintModal/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/HintModal/__snapshots__/index.test.tsx.snap
@@ -72,7 +72,9 @@ exports[`HintModal should render correctly 1`] = `
style={
Object {
"color": "#24272A",
+ "fontFamily": "EuclidCircularB-Regular",
"fontSize": 14,
+ "fontWeight": "400",
"marginBottom": 16,
}
}
@@ -83,7 +85,9 @@ exports[`HintModal should render correctly 1`] = `
style={
Object {
"color": "#D73A49",
+ "fontFamily": "EuclidCircularB-Regular",
"fontSize": 14,
+ "fontWeight": "400",
"marginBottom": 16,
}
}
diff --git a/app/components/UI/HintModal/index.js b/app/components/UI/HintModal/index.js
index b53b7f6d61a..b71507f19d1 100644
--- a/app/components/UI/HintModal/index.js
+++ b/app/components/UI/HintModal/index.js
@@ -28,13 +28,13 @@ const createStyles = (colors) =>
},
leaveHint: {
fontSize: 14,
- ...fontStyles.regular,
+ ...fontStyles.normal,
color: colors.text.default,
marginBottom: 16,
},
noSeedphrase: {
fontSize: 14,
- ...fontStyles.regular,
+ ...fontStyles.normal,
color: colors.error.default,
marginBottom: 16,
},
diff --git a/app/components/UI/Identicon/index.js b/app/components/UI/Identicon/index.js
index de6c9d7231f..d43aa355d6c 100644
--- a/app/components/UI/Identicon/index.js
+++ b/app/components/UI/Identicon/index.js
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Image, View, ViewPropTypes } from 'react-native';
-import { toDataUrl } from '../../../util/blockies.js';
+import { toDataUrl } from '../../../util/blockies';
import FadeIn from 'react-native-fade-in-image';
import Jazzicon from 'react-native-jazzicon';
import { connect } from 'react-redux';
diff --git a/app/components/UI/MessageSign/__snapshots__/index.test.tsx.snap b/app/components/UI/MessageSign/__snapshots__/index.test.tsx.snap
index d3cb2d0218c..4c07d5402d6 100644
--- a/app/components/UI/MessageSign/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/MessageSign/__snapshots__/index.test.tsx.snap
@@ -1,36 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MessageSign should render correctly 1`] = `
-
-
-
- message
-
-
-
+ }
+/>
`;
diff --git a/app/components/UI/MessageSign/index.js b/app/components/UI/MessageSign/index.js
index 4ae55564162..1c4424d3b72 100644
--- a/app/components/UI/MessageSign/index.js
+++ b/app/components/UI/MessageSign/index.js
@@ -1,22 +1,25 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { StyleSheet, View, Text, InteractionManager } from 'react-native';
+import { connect } from 'react-redux';
import { fontStyles } from '../../../styles/common';
import Engine from '../../../core/Engine';
import SignatureRequest from '../SignatureRequest';
import ExpandedMessage from '../SignatureRequest/ExpandedMessage';
+import { KEYSTONE_TX_CANCELED } from '../../../constants/error';
import NotificationManager from '../../../core/NotificationManager';
import { strings } from '../../../../locales/i18n';
import { WALLET_CONNECT_ORIGIN } from '../../../util/walletconnect';
import URL from 'url-parse';
import AnalyticsV2 from '../../../util/analyticsV2';
+import { getAddressAccountType } from '../../../util/address';
import { ThemeContext, mockTheme } from '../../../util/theme';
const createStyles = (colors) =>
StyleSheet.create({
expandedMessage: {
textAlign: 'center',
- ...fontStyles.regular,
+ ...fontStyles.normal,
fontSize: 14,
color: colors.text.default,
},
@@ -31,8 +34,12 @@ const createStyles = (colors) =>
/**
* Component that supports eth_sign
*/
-export default class MessageSign extends PureComponent {
+class MessageSign extends PureComponent {
static propTypes = {
+ /**
+ * A string that represents the selected address
+ */
+ selectedAddress: PropTypes.string,
/**
* react-navigation object used for switching between screens
*/
@@ -69,11 +76,12 @@ export default class MessageSign extends PureComponent {
getAnalyticsParams = () => {
try {
- const { currentPageInformation } = this.props;
+ const { currentPageInformation, selectedAddress } = this.props;
const { NetworkController } = Engine.context;
const { chainId, type } = NetworkController?.state?.provider || {};
const url = new URL(currentPageInformation?.url);
return {
+ account_type: getAddressAccountType(selectedAddress),
dapp_host_name: url?.host,
dapp_url: currentPageInformation?.url,
network_name: type,
@@ -128,10 +136,20 @@ export default class MessageSign extends PureComponent {
this.props.onCancel();
};
- confirmSignature = () => {
- this.signMessage();
- AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_COMPLETED, this.getAnalyticsParams());
- this.props.onConfirm();
+ confirmSignature = async () => {
+ try {
+ await this.signMessage();
+ AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_COMPLETED, this.getAnalyticsParams());
+ this.props.onConfirm();
+ } catch (e) {
+ if (e?.message.startsWith(KEYSTONE_TX_CANCELED)) {
+ AnalyticsV2.trackEvent(
+ AnalyticsV2.ANALYTICS_EVENTS.QR_HARDWARE_TRANSACTION_CANCELED,
+ this.getAnalyticsParams()
+ );
+ this.props.onCancel();
+ }
+ }
};
getStyles = () => {
@@ -199,3 +217,9 @@ export default class MessageSign extends PureComponent {
}
MessageSign.contextType = ThemeContext;
+
+const mapStateToProps = (state) => ({
+ selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress,
+});
+
+export default connect(mapStateToProps)(MessageSign);
diff --git a/app/components/UI/MessageSign/index.test.tsx b/app/components/UI/MessageSign/index.test.tsx
index 92687b4204e..42b88b871ed 100644
--- a/app/components/UI/MessageSign/index.test.tsx
+++ b/app/components/UI/MessageSign/index.test.tsx
@@ -1,11 +1,30 @@
import React from 'react';
import { shallow } from 'enzyme';
import MessageSign from './';
+import configureMockStore from 'redux-mock-store';
+import { Provider } from 'react-redux';
+
+const mockStore = configureMockStore();
+const initialState = {
+ engine: {
+ backgroundState: {
+ PreferencesController: {
+ selectedAddress: '0x0',
+ },
+ },
+ },
+};
+const store = mockStore(initialState);
describe('MessageSign', () => {
it('should render correctly', () => {
const wrapper = shallow(
-
+
+
+
);
expect(wrapper).toMatchSnapshot();
});
diff --git a/app/components/UI/NetworkInfo/index.tsx b/app/components/UI/NetworkInfo/index.tsx
index aa929998f20..bcc7a3b2d57 100644
--- a/app/components/UI/NetworkInfo/index.tsx
+++ b/app/components/UI/NetworkInfo/index.tsx
@@ -13,6 +13,7 @@ import {
NETWORK_EDUCATION_MODAL_CLOSE_BUTTON_ID,
NETWORK_EDUCATION_MODAL_NETWORK_NAME_ID,
} from '../../../constants/test-ids';
+import { fontStyles } from '../../../styles/common';
const createStyles = (colors: {
background: { default: string };
@@ -29,7 +30,7 @@ const createStyles = (colors: {
},
title: {
fontSize: 16,
- fontWeight: 'bold',
+ ...fontStyles.bold,
marginVertical: 10,
textAlign: 'center',
color: colors.text.default,
@@ -63,7 +64,7 @@ const createStyles = (colors: {
},
messageTitle: {
fontSize: 14,
- fontWeight: 'bold',
+ ...fontStyles.bold,
marginBottom: 15,
textAlign: 'center',
color: colors.text.default,
@@ -76,6 +77,7 @@ const createStyles = (colors: {
borderColor: colors.border.muted,
},
rpcUrl: {
+ ...fontStyles.normal,
fontSize: 10,
color: colors.border.muted,
textAlign: 'center',
@@ -91,6 +93,7 @@ const createStyles = (colors: {
justifyContent: 'center',
},
unknownText: {
+ ...fontStyles.normal,
color: colors.text.default,
fontSize: 13,
},
diff --git a/app/components/UI/PersonalSign/__snapshots__/index.test.tsx.snap b/app/components/UI/PersonalSign/__snapshots__/index.test.tsx.snap
index 8ad1fb3a619..e8a189a4fc0 100644
--- a/app/components/UI/PersonalSign/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/PersonalSign/__snapshots__/index.test.tsx.snap
@@ -1,54 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PersonalSign should render correctly 1`] = `
-
-
-
-
-
-
-
-
-
-
+ }
+/>
`;
diff --git a/app/components/UI/PersonalSign/index.js b/app/components/UI/PersonalSign/index.js
index f118f057afc..51124ab57e0 100644
--- a/app/components/UI/PersonalSign/index.js
+++ b/app/components/UI/PersonalSign/index.js
@@ -1,6 +1,7 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { StyleSheet, View, Text, InteractionManager } from 'react-native';
+import { connect } from 'react-redux';
import { fontStyles } from '../../../styles/common';
import Engine from '../../../core/Engine';
import SignatureRequest from '../SignatureRequest';
@@ -11,6 +12,8 @@ import { strings } from '../../../../locales/i18n';
import { WALLET_CONNECT_ORIGIN } from '../../../util/walletconnect';
import URL from 'url-parse';
import AnalyticsV2 from '../../../util/analyticsV2';
+import { getAddressAccountType } from '../../../util/address';
+import { KEYSTONE_TX_CANCELED } from '../../../constants/error';
import { ThemeContext, mockTheme } from '../../../util/theme';
const createStyles = (colors) =>
@@ -35,8 +38,12 @@ const createStyles = (colors) =>
/**
* Component that supports personal_sign
*/
-export default class PersonalSign extends PureComponent {
+class PersonalSign extends PureComponent {
static propTypes = {
+ /**
+ * A string that represents the selected address
+ */
+ selectedAddress: PropTypes.string,
/**
* react-navigation object used for switching between screens
*/
@@ -73,12 +80,13 @@ export default class PersonalSign extends PureComponent {
getAnalyticsParams = () => {
try {
- const { currentPageInformation } = this.props;
+ const { currentPageInformation, selectedAddress } = this.props;
const { NetworkController } = Engine.context;
const { chainId, type } = NetworkController?.state?.provider || {};
const url = new URL(currentPageInformation?.url);
return {
+ account_type: getAddressAccountType(selectedAddress),
dapp_host_name: url?.host,
dapp_url: currentPageInformation?.url,
network_name: type,
@@ -133,10 +141,25 @@ export default class PersonalSign extends PureComponent {
this.props.onCancel();
};
- confirmSignature = () => {
- this.signMessage();
- AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_COMPLETED, this.getAnalyticsParams());
- this.props.onConfirm();
+ confirmSignature = async () => {
+ try {
+ await this.signMessage();
+ AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_COMPLETED, this.getAnalyticsParams());
+ this.props.onConfirm();
+ } catch (e) {
+ if (e?.message.startsWith(KEYSTONE_TX_CANCELED)) {
+ AnalyticsV2.trackEvent(
+ AnalyticsV2.ANALYTICS_EVENTS.QR_HARDWARE_TRANSACTION_CANCELED,
+ this.getAnalyticsParams()
+ );
+ this.props.onCancel();
+ }
+ }
+ };
+
+ getStyles = () => {
+ const colors = this.context.colors || mockTheme.colors;
+ return createStyles(colors);
};
getStyles = () => {
@@ -212,3 +235,9 @@ export default class PersonalSign extends PureComponent {
}
PersonalSign.contextType = ThemeContext;
+
+const mapStateToProps = (state) => ({
+ selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress,
+});
+
+export default connect(mapStateToProps)(PersonalSign);
diff --git a/app/components/UI/PersonalSign/index.test.tsx b/app/components/UI/PersonalSign/index.test.tsx
index 10684ae4a3f..1ae63f7fac3 100644
--- a/app/components/UI/PersonalSign/index.test.tsx
+++ b/app/components/UI/PersonalSign/index.test.tsx
@@ -1,11 +1,30 @@
import React from 'react';
import { shallow } from 'enzyme';
import PersonalSign from './';
+import configureMockStore from 'redux-mock-store';
+import { Provider } from 'react-redux';
+
+const mockStore = configureMockStore();
+const initialState = {
+ engine: {
+ backgroundState: {
+ PreferencesController: {
+ selectedAddress: '0x0',
+ },
+ },
+ },
+};
+const store = mockStore(initialState);
describe('PersonalSign', () => {
it('should render correctly', () => {
const wrapper = shallow(
-
+
+
+
);
expect(wrapper).toMatchSnapshot();
});
diff --git a/app/components/UI/QRHardware/AnimatedQRCode.tsx b/app/components/UI/QRHardware/AnimatedQRCode.tsx
new file mode 100644
index 00000000000..abae14b8c81
--- /dev/null
+++ b/app/components/UI/QRHardware/AnimatedQRCode.tsx
@@ -0,0 +1,52 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import QRCode from 'react-native-qrcode-svg';
+import { StyleSheet, View } from 'react-native';
+import { UR, UREncoder } from '@ngraveio/bc-ur';
+import { colors } from '../../../styles/common';
+
+interface IAnimatedQRCodeProps {
+ cbor: string;
+ type: string;
+ shouldPause: boolean;
+}
+
+const MAX_FRAGMENT_LENGTH = 400;
+const QR_CODE_SIZE = 250;
+
+const styles = StyleSheet.create({
+ wrapper: {
+ width: 300,
+ height: 300,
+ backgroundColor: colors.white,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+});
+
+const AnimatedQRCode = ({ cbor, type, shouldPause }: IAnimatedQRCodeProps) => {
+ const urEncoder = useMemo(
+ () => new UREncoder(new UR(Buffer.from(cbor, 'hex'), type), MAX_FRAGMENT_LENGTH),
+ [cbor, type]
+ );
+
+ const [currentQRCode, setCurrentQRCode] = useState(urEncoder.nextPart());
+
+ useEffect(() => {
+ if (!shouldPause) {
+ const id = setInterval(() => {
+ setCurrentQRCode(urEncoder.nextPart());
+ }, 250);
+ return () => {
+ clearInterval(id);
+ };
+ }
+ }, [urEncoder, shouldPause]);
+
+ return (
+
+
+
+ );
+};
+
+export default AnimatedQRCode;
diff --git a/app/components/UI/QRHardware/AnimatedQRScanner.tsx b/app/components/UI/QRHardware/AnimatedQRScanner.tsx
new file mode 100644
index 00000000000..bc303817884
--- /dev/null
+++ b/app/components/UI/QRHardware/AnimatedQRScanner.tsx
@@ -0,0 +1,227 @@
+/* eslint @typescript-eslint/no-var-requires: "off" */
+/* eslint @typescript-eslint/no-require-imports: "off" */
+
+'use strict';
+import React, { useCallback, useMemo, useState } from 'react';
+import { SafeAreaView, Image, Text, TouchableOpacity, View, StyleSheet } from 'react-native';
+import { RNCamera } from 'react-native-camera';
+import { colors, fontStyles } from '../../../styles/common';
+import Icon from 'react-native-vector-icons/Ionicons';
+import { strings } from '../../../../locales/i18n';
+import { URRegistryDecoder } from '@keystonehq/ur-decoder';
+import Modal from 'react-native-modal';
+import { UR } from '@ngraveio/bc-ur';
+import AnalyticsV2 from '../../../util/analyticsV2';
+import { SUPPORTED_UR_TYPE } from '../../../constants/qr';
+
+const styles = StyleSheet.create({
+ modal: {
+ margin: 0,
+ },
+ container: {
+ width: '100%',
+ height: '100%',
+ backgroundColor: colors.black,
+ },
+ preview: {
+ flex: 1,
+ },
+ innerView: {
+ flex: 1,
+ },
+ closeIcon: {
+ marginTop: 20,
+ marginRight: 20,
+ width: 40,
+ alignSelf: 'flex-end',
+ },
+ frame: {
+ width: 250,
+ height: 250,
+ alignSelf: 'center',
+ justifyContent: 'center',
+ marginTop: 100,
+ opacity: 0.5,
+ },
+ text: {
+ flex: 1,
+ fontSize: 17,
+ color: colors.white,
+ textAlign: 'center',
+ justifyContent: 'center',
+ marginTop: 100,
+ },
+ hint: {
+ backgroundColor: colors.whiteTransparent,
+ width: '100%',
+ height: 120,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ hintText: {
+ width: 240,
+ maxWidth: '80%',
+ color: colors.black,
+ textAlign: 'center',
+ fontSize: 16,
+ ...fontStyles.normal,
+ },
+ bold: {
+ ...fontStyles.bold,
+ },
+});
+
+const frameImage = require('images/frame.png'); // eslint-disable-line import/no-commonjs
+
+interface AnimatedQRScannerProps {
+ visible: boolean;
+ purpose: 'sync' | 'sign';
+ onScanSuccess: (ur: UR) => void;
+ onScanError: (error: string) => void;
+ hideModal: () => void;
+ pauseQRCode?: (x: boolean) => void;
+}
+
+const AnimatedQRScannerModal = (props: AnimatedQRScannerProps) => {
+ const { visible, onScanError, purpose, onScanSuccess, hideModal, pauseQRCode } = props;
+ const [urDecoder, setURDecoder] = useState(new URRegistryDecoder());
+ const [progress, setProgress] = useState(0);
+
+ let expectedURTypes: string[];
+ if (purpose === 'sync') {
+ expectedURTypes = [SUPPORTED_UR_TYPE.CRYPTO_HDKEY, SUPPORTED_UR_TYPE.CRYPTO_ACCOUNT];
+ } else {
+ expectedURTypes = [SUPPORTED_UR_TYPE.ETH_SIGNATURE];
+ }
+
+ const reset = useCallback(() => {
+ setURDecoder(new URRegistryDecoder());
+ setProgress(0);
+ }, []);
+
+ const hintText = useMemo(
+ () => (
+
+ {strings('connect_qr_hardware.hint_text')}
+
+ {strings(
+ purpose === 'sync' ? 'connect_qr_hardware.purpose_connect' : 'connect_qr_hardware.purpose_sign'
+ )}
+
+
+ ),
+ [purpose]
+ );
+
+ const onError = useCallback(
+ (error) => {
+ if (onScanError && error) {
+ AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.HARDWARE_WALLET_ERROR, {
+ purpose,
+ error,
+ });
+ onScanError(error.message);
+ }
+ },
+ [purpose, onScanError]
+ );
+
+ const onBarCodeRead = useCallback(
+ (response) => {
+ if (!visible) {
+ return;
+ }
+ if (!response.data) {
+ return;
+ }
+ try {
+ const content = response.data;
+ urDecoder.receivePart(content);
+ setProgress(Math.ceil(urDecoder.getProgress() * 100));
+ if (urDecoder.isError()) {
+ AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.HARDWARE_WALLET_ERROR, {
+ purpose,
+ error: urDecoder.resultError(),
+ });
+ onScanError(strings('transaction.unknown_qr_code'));
+ } else if (urDecoder.isSuccess()) {
+ const ur = urDecoder.resultUR();
+ if (expectedURTypes.includes(ur.type)) {
+ onScanSuccess(ur);
+ setProgress(0);
+ setURDecoder(new URRegistryDecoder());
+ } else if (purpose === 'sync') {
+ AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.HARDWARE_WALLET_ERROR, {
+ purpose,
+ received_ur_type: ur.type,
+ error: 'invalid `sync` qr code',
+ });
+ onScanError(strings('transaction.invalid_qr_code_sync'));
+ } else {
+ AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.HARDWARE_WALLET_ERROR, {
+ purpose,
+ received_ur_type: ur.type,
+ error: 'invalid `sign` qr code',
+ });
+ onScanError(strings('transaction.invalid_qr_code_sign'));
+ }
+ }
+ } catch (e) {
+ onScanError(strings('transaction.unknown_qr_code'));
+ }
+ },
+ [visible, urDecoder, onScanError, expectedURTypes, purpose, onScanSuccess]
+ );
+
+ const onStatusChange = useCallback(
+ (event) => {
+ if (event.cameraStatus === 'NOT_AUTHORIZED') {
+ onScanError(strings('transaction.no_camera_permission'));
+ }
+ },
+ [onScanError]
+ );
+
+ return (
+ {
+ reset();
+ pauseQRCode?.(false);
+ }}
+ onModalWillShow={() => pauseQRCode?.(true)}
+ >
+
+
+
+
+
+
+
+ {`${strings('qr_scanner.scanning')} ${
+ progress ? `${progress.toString()}%` : ''
+ }`}
+
+
+ {hintText}
+
+
+ );
+};
+
+export default AnimatedQRScannerModal;
diff --git a/app/components/UI/QRHardware/QRSigningDetails.tsx b/app/components/UI/QRHardware/QRSigningDetails.tsx
new file mode 100644
index 00000000000..fd8922d66c3
--- /dev/null
+++ b/app/components/UI/QRHardware/QRSigningDetails.tsx
@@ -0,0 +1,311 @@
+import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
+import Engine from '../../../core/Engine';
+import {
+ StyleSheet,
+ Text,
+ View,
+ ScrollView,
+ // eslint-disable-next-line react-native/split-platform-components
+ PermissionsAndroid,
+ Linking,
+ AppState,
+ AppStateStatus,
+} from 'react-native';
+import { strings } from '../../../../locales/i18n';
+import AnimatedQRCode from './AnimatedQRCode';
+import AnimatedQRScannerModal from './AnimatedQRScanner';
+import { fontStyles } from '../../../styles/common';
+import AccountInfoCard from '../AccountInfoCard';
+import ActionView from '../ActionView';
+import { IQRState } from './types';
+import { UR } from '@ngraveio/bc-ur';
+import { ETHSignature } from '@keystonehq/bc-ur-registry-eth';
+import { stringify as uuidStringify } from 'uuid';
+import Alert, { AlertType } from '../../Base/Alert';
+import AnalyticsV2 from '../../../util/analyticsV2';
+import { useNavigation } from '@react-navigation/native';
+import { mockTheme, useAppThemeFromContext } from '../../../util/theme';
+import Device from '../../../util/device';
+
+interface IQRSigningDetails {
+ QRState: IQRState;
+ successCallback?: () => void;
+ failureCallback?: (error: string) => void;
+ cancelCallback?: () => void;
+ confirmButtonMode?: 'normal' | 'sign' | 'confirm';
+ showCancelButton?: boolean;
+ tighten?: boolean;
+ showHint?: boolean;
+ shouldStartAnimated?: boolean;
+ bypassAndroidCameraAccessCheck?: boolean;
+}
+
+const createStyles = (colors: any) =>
+ StyleSheet.create({
+ wrapper: {
+ flex: 1,
+ },
+ container: {
+ flex: 1,
+ width: '100%',
+ flexDirection: 'column',
+ alignItems: 'center',
+ backgroundColor: colors.background.default,
+ },
+ accountInfoCardWrapper: {
+ paddingHorizontal: 24,
+ paddingBottom: 12,
+ },
+ containerTighten: {
+ paddingHorizontal: 8,
+ },
+ title: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ marginTop: 54,
+ marginBottom: 30,
+ },
+ titleTighten: {
+ marginTop: 12,
+ marginBottom: 6,
+ },
+ titleText: {
+ ...fontStyles.normal,
+ fontSize: 14,
+ color: colors.text.default,
+ },
+ description: {
+ marginVertical: 24,
+ alignItems: 'center',
+ ...fontStyles.normal,
+ fontSize: 14,
+ },
+ descriptionTighten: {
+ marginVertical: 12,
+ },
+ padding: {
+ height: 40,
+ },
+ descriptionText: {
+ ...fontStyles.normal,
+ fontSize: 14,
+ color: colors.text.default,
+ },
+ descriptionTextTighten: {
+ fontSize: 12,
+ },
+ errorText: {
+ ...fontStyles.normal,
+ fontSize: 12,
+ color: colors.error.default,
+ },
+ alert: {
+ marginHorizontal: 16,
+ marginTop: 12,
+ },
+ });
+
+const QRSigningDetails = ({
+ QRState,
+ successCallback,
+ failureCallback,
+ cancelCallback,
+ confirmButtonMode = 'confirm',
+ showCancelButton = false,
+ tighten = false,
+ showHint = true,
+ shouldStartAnimated = true,
+ bypassAndroidCameraAccessCheck = true,
+}: IQRSigningDetails) => {
+ const { colors } = useAppThemeFromContext() || mockTheme;
+ const styles = createStyles(colors);
+ const navigation = useNavigation();
+ const KeyringController = useMemo(() => {
+ const { KeyringController: keyring } = Engine.context as any;
+ return keyring;
+ }, []);
+ const [scannerVisible, setScannerVisible] = useState(false);
+ const [errorMessage, setErrorMessage] = useState('');
+ const [shouldPause, setShouldPause] = useState(false);
+ const [cameraError, setCameraError] = useState('');
+
+ // ios handled camera perfectly in this situation, we just need to check permission with android.
+ const [hasCameraPermission, setCameraPermission] = useState(Device.isIos() || bypassAndroidCameraAccessCheck);
+
+ const checkAndroidCamera = useCallback(() => {
+ PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.CAMERA).then((_hasPermission) => {
+ setCameraPermission(_hasPermission);
+ if (!_hasPermission) {
+ setCameraError(strings('transaction.no_camera_permission_android'));
+ } else {
+ setCameraError('');
+ }
+ });
+ }, []);
+
+ const handleAppState = useCallback(
+ (appState: AppStateStatus) => {
+ if (appState === 'active') {
+ checkAndroidCamera();
+ }
+ },
+ [checkAndroidCamera]
+ );
+
+ useEffect(() => {
+ if (Device.isAndroid() && !hasCameraPermission) {
+ checkAndroidCamera();
+ }
+ }, [checkAndroidCamera, hasCameraPermission]);
+
+ useEffect(() => {
+ AppState.addEventListener('change', handleAppState);
+ return () => {
+ AppState.removeEventListener('change', handleAppState);
+ };
+ }, [handleAppState]);
+
+ const [hasSentOrCanceled, setSentOrCanceled] = useState(false);
+
+ useEffect(() => {
+ navigation.addListener('beforeRemove', (e) => {
+ if (hasSentOrCanceled) {
+ return;
+ }
+ e.preventDefault();
+ KeyringController.cancelQRSignRequest().then(() => {
+ navigation.dispatch(e.data.action);
+ });
+ });
+ }, [KeyringController, hasSentOrCanceled, navigation]);
+
+ const resetError = () => {
+ setErrorMessage('');
+ };
+
+ const showScanner = () => {
+ setScannerVisible(true);
+ resetError();
+ };
+
+ const hideScanner = () => {
+ setScannerVisible(false);
+ };
+
+ const onCancel = useCallback(async () => {
+ await KeyringController.cancelQRSignRequest();
+ setSentOrCanceled(true);
+ hideScanner();
+ cancelCallback?.();
+ }, [KeyringController, cancelCallback]);
+
+ const onScanSuccess = useCallback(
+ (ur: UR) => {
+ hideScanner();
+ const signature = ETHSignature.fromCBOR(ur.cbor);
+ const buffer = signature.getRequestId();
+ const requestId = uuidStringify(buffer);
+ if (QRState.sign.request?.requestId === requestId) {
+ KeyringController.submitQRSignature(QRState.sign.request?.requestId as string, ur.cbor.toString('hex'));
+ setSentOrCanceled(true);
+ successCallback?.();
+ } else {
+ AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.HARDWARE_WALLET_ERROR, {
+ error: 'received signature request id is not matched with origin request',
+ });
+ setErrorMessage(strings('transaction.mismatched_qr_request_id'));
+ failureCallback?.(strings('transaction.mismatched_qr_request_id'));
+ }
+ },
+ [KeyringController, QRState.sign.request?.requestId, failureCallback, successCallback]
+ );
+ const onScanError = useCallback(
+ (_errorMessage: string) => {
+ hideScanner();
+ setErrorMessage(_errorMessage);
+ failureCallback?.(_errorMessage);
+ },
+ [failureCallback]
+ );
+
+ const renderAlert = () =>
+ errorMessage !== '' && (
+
+ {errorMessage}
+
+ );
+
+ const renderCameraAlert = () =>
+ cameraError !== '' && (
+
+ {cameraError}
+
+ );
+
+ return (
+
+ {QRState?.sign?.request && (
+
+
+
+
+
+
+ {renderAlert()}
+ {renderCameraAlert()}
+
+ {strings('transactions.sign_title_scan')}
+ {strings('transactions.sign_title_device')}
+
+
+ {showHint ? (
+
+
+ {strings('transactions.sign_description_1')}
+
+
+ {strings('transactions.sign_description_2')}
+
+
+ ) : !tighten ? (
+
+ ) : null}
+
+
+
+ )}
+
+
+ );
+};
+
+export default QRSigningDetails;
diff --git a/app/components/UI/QRHardware/QRSigningModal/index.tsx b/app/components/UI/QRHardware/QRSigningModal/index.tsx
new file mode 100644
index 00000000000..a6b44426563
--- /dev/null
+++ b/app/components/UI/QRHardware/QRSigningModal/index.tsx
@@ -0,0 +1,72 @@
+import React, { useState } from 'react';
+import Modal from 'react-native-modal';
+import { IQRState } from '../types';
+import { StyleSheet, View } from 'react-native';
+import QRSigningDetails from '../QRSigningDetails';
+import { mockTheme, useAppThemeFromContext } from '../../../../util/theme';
+
+interface IQRSigningModalProps {
+ isVisible: boolean;
+ QRState: IQRState;
+ onSuccess?: () => void;
+ onCancel?: () => void;
+ onFailure?: (error: string) => void;
+}
+
+const createStyles = (colors: any) =>
+ StyleSheet.create({
+ modal: {
+ margin: 0,
+ justifyContent: 'flex-end',
+ },
+ contentWrapper: {
+ justifyContent: 'flex-end',
+ height: 600,
+ backgroundColor: colors.background.default,
+ paddingTop: 24,
+ borderTopLeftRadius: 24,
+ borderTopRightRadius: 24,
+ },
+ });
+
+const QRSigningModal = ({ isVisible, QRState, onSuccess, onCancel, onFailure }: IQRSigningModalProps) => {
+ const { colors } = useAppThemeFromContext() || mockTheme;
+ const styles = createStyles(colors);
+ const [isModalCompleteShow, setModalCompleteShow] = useState(false);
+ return (
+ {
+ setModalCompleteShow(true);
+ }}
+ onModalHide={() => {
+ setModalCompleteShow(false);
+ }}
+ propagateSwipe
+ >
+
+
+
+
+ );
+};
+
+export default QRSigningModal;
diff --git a/app/components/UI/QRHardware/types.tsx b/app/components/UI/QRHardware/types.tsx
new file mode 100644
index 00000000000..d38811e976d
--- /dev/null
+++ b/app/components/UI/QRHardware/types.tsx
@@ -0,0 +1,14 @@
+export interface IQRState {
+ sync: {
+ reading: boolean;
+ };
+ sign: {
+ request?: {
+ requestId: string;
+ payload: {
+ cbor: string;
+ type: string;
+ };
+ };
+ };
+}
diff --git a/app/components/UI/QRHardware/withQRHardwareAwareness.tsx b/app/components/UI/QRHardware/withQRHardwareAwareness.tsx
new file mode 100644
index 00000000000..fe59dd46294
--- /dev/null
+++ b/app/components/UI/QRHardware/withQRHardwareAwareness.tsx
@@ -0,0 +1,60 @@
+import React, { Component, ComponentClass } from 'react';
+import Engine from '../../../core/Engine';
+import { IQRState } from './types';
+
+const withQRHardwareAwareness = (
+ Children: ComponentClass<{
+ QRState?: IQRState;
+ isSigningQRObject?: boolean;
+ isSyncingQRHardware?: boolean;
+ }>
+) => {
+ class QRHardwareAwareness extends Component {
+ state: {
+ QRState: IQRState;
+ } = {
+ QRState: {
+ sync: {
+ reading: false,
+ },
+ sign: {},
+ },
+ };
+
+ keyringState: any;
+
+ subscribeKeyringState = (value: any) => {
+ this.setState({
+ QRState: value,
+ });
+ };
+
+ componentDidMount() {
+ const { KeyringController } = Engine.context as any;
+ KeyringController.getQRKeyringState().then((store: any) => {
+ this.keyringState = store;
+ this.keyringState.subscribe(this.subscribeKeyringState);
+ });
+ }
+
+ componentWillUnmount() {
+ if (this.keyringState) {
+ this.keyringState.unsubscribe(this.subscribeKeyringState);
+ }
+ }
+
+ render() {
+ return (
+
+ );
+ }
+ }
+ return QRHardwareAwareness;
+};
+
+export default withQRHardwareAwareness;
diff --git a/app/components/UI/SignatureRequest/__snapshots__/index.test.tsx.snap b/app/components/UI/SignatureRequest/__snapshots__/index.test.tsx.snap
index ae519b6e3b5..4f578a598cd 100644
--- a/app/components/UI/SignatureRequest/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/SignatureRequest/__snapshots__/index.test.tsx.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SignatureRequest should render correctly 1`] = `
-
StyleSheet.create({
@@ -147,6 +149,8 @@ class SignatureRequest extends PureComponent {
* Expands the message box on press.
*/
toggleExpandedMessage: PropTypes.func,
+ isSigningQRObject: PropTypes.bool,
+ QRState: PropTypes.object,
};
/**
@@ -248,7 +252,7 @@ class SignatureRequest extends PureComponent {
);
};
- render() {
+ renderSignatureRequest() {
const { showWarning, currentPageInformation, type } = this.props;
let expandedHeight;
const styles = this.getStyles();
@@ -286,6 +290,27 @@ class SignatureRequest extends PureComponent {
);
}
+
+ renderQRDetails() {
+ const { QRState } = this.props;
+ const styles = this.getStyles();
+
+ return (
+
+
+
+ );
+ }
+
+ render() {
+ const { isSigningQRObject } = this.props;
+ return isSigningQRObject ? this.renderQRDetails() : this.renderSignatureRequest();
+ }
}
const mapStateToProps = (state) => ({
@@ -294,4 +319,4 @@ const mapStateToProps = (state) => ({
SignatureRequest.contextType = ThemeContext;
-export default connect(mapStateToProps)(SignatureRequest);
+export default connect(mapStateToProps)(withQRHardwareAwareness(SignatureRequest));
diff --git a/app/components/UI/TransactionElement/index.js b/app/components/UI/TransactionElement/index.js
index bbb5c79454c..351a64d7985 100644
--- a/app/components/UI/TransactionElement/index.js
+++ b/app/components/UI/TransactionElement/index.js
@@ -30,7 +30,6 @@ const createStyles = (colors) =>
},
actionContainerStyle: {
height: 25,
- width: 70,
padding: 0,
},
speedupActionContainerStyle: {
@@ -119,6 +118,9 @@ class TransactionElement extends PureComponent {
* Chain Id
*/
chainId: PropTypes.string,
+ signQRTransaction: PropTypes.func,
+ cancelUnsignedQRTransaction: PropTypes.func,
+ isQRHardwareAccount: PropTypes.bool,
};
state = {
@@ -246,10 +248,12 @@ class TransactionElement extends PureComponent {
identities,
chainId,
selectedAddress,
+ isQRHardwareAccount,
tx: { time, status },
} = this.props;
const { value, fiatValue = false, actionKey } = transactionElement;
- const renderTxActions = status === 'submitted' || status === 'approved';
+ const renderNormalActions = status === 'submitted' || (status === 'approved' && !isQRHardwareAccount);
+ const renderUnsignedQRActions = status === 'approved' && isQRHardwareAccount;
const accountImportTime = identities[selectedAddress]?.importTime;
return (
<>
@@ -269,12 +273,18 @@ class TransactionElement extends PureComponent {
)}
- {!!renderTxActions && (
+ {renderNormalActions && (
{this.renderSpeedUpButton()}
{this.renderCancelButton()}
)}
+ {renderUnsignedQRActions && (
+
+ {this.renderQRSignButton()}
+ {this.renderCancelUnsignedButton()}
+
+ )}
{accountImportTime <= time && this.renderImportTime()}
>
@@ -334,6 +344,14 @@ class TransactionElement extends PureComponent {
this.mounted && this.props.onSpeedUpAction(false);
};
+ showQRSigningModal = () => {
+ this.mounted && this.props.signQRTransaction(this.props.tx);
+ };
+
+ cancelUnsignedQRTransaction = () => {
+ this.mounted && this.props.cancelUnsignedQRTransaction(this.props.tx);
+ };
+
renderSpeedUpButton = () => {
const colors = this.context.colors || mockTheme.colors;
const styles = createStyles(colors);
@@ -350,6 +368,36 @@ class TransactionElement extends PureComponent {
);
};
+ renderQRSignButton = () => {
+ const colors = this.context.colors || mockTheme.colors;
+ const styles = createStyles(colors);
+ return (
+
+ {strings('transaction.sign_with_keystone')}
+
+ );
+ };
+
+ renderCancelUnsignedButton = () => {
+ const colors = this.context.colors || mockTheme.colors;
+ const styles = createStyles(colors);
+ return (
+
+ {strings('transaction.cancel')}
+
+ );
+ };
+
render() {
const { tx } = this.props;
const { detailsModalVisible, importModalVisible, transactionElement, transactionDetails } = this.state;
diff --git a/app/components/UI/TransactionReview/__snapshots__/index.test.tsx.snap b/app/components/UI/TransactionReview/__snapshots__/index.test.tsx.snap
index 92f9bfe3ac1..f3b3afd145f 100644
--- a/app/components/UI/TransactionReview/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/TransactionReview/__snapshots__/index.test.tsx.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TransactionReview should render correctly 1`] = `
-
StyleSheet.create({
@@ -53,6 +55,9 @@ const createStyles = (colors) =>
actionViewChildren: {
height: 330,
},
+ actionViewQRObject: {
+ height: 624,
+ },
accountInfoCardWrapper: {
paddingHorizontal: 24,
paddingBottom: 12,
@@ -192,6 +197,8 @@ class TransactionReview extends PureComponent {
* If it's a eip1559 network and dapp suggest legact gas then it should show a warning
*/
dappSuggestedGasWarning: PropTypes.bool,
+ isSigningQRObject: PropTypes.bool,
+ QRState: PropTypes.object,
};
state = {
@@ -319,7 +326,7 @@ class TransactionReview extends PureComponent {
return url;
}
- render = () => {
+ renderTransactionReview = () => {
const {
transactionConfirmed,
primaryCurrency,
@@ -408,6 +415,29 @@ class TransactionReview extends PureComponent {
>
);
};
+
+ renderQRDetails() {
+ const currentPageInformation = { url: this.getUrlFromBrowser() };
+ const { QRState } = this.props;
+ const styles = this.getStyles();
+ return (
+
+
+
+
+ );
+ }
+
+ render() {
+ const { isSigningQRObject } = this.props;
+ return isSigningQRObject ? this.renderQRDetails() : this.renderTransactionReview();
+ }
}
const mapStateToProps = (state) => ({
@@ -427,4 +457,4 @@ const mapStateToProps = (state) => ({
TransactionReview.contextType = ThemeContext;
-export default connect(mapStateToProps)(TransactionReview);
+export default connect(mapStateToProps)(withQRHardwareAwareness(TransactionReview));
diff --git a/app/components/UI/Transactions/__snapshots__/index.test.tsx.snap b/app/components/UI/Transactions/__snapshots__/index.test.tsx.snap
index 828e273e1f3..ed674696a11 100644
--- a/app/components/UI/Transactions/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/Transactions/__snapshots__/index.test.tsx.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Transactions should render correctly 1`] = `
-
StyleSheet.create({
@@ -162,6 +164,7 @@ class Transactions extends PureComponent {
* Indicates whether third party API mode is enabled
*/
thirdPartyApiMode: PropTypes.bool,
+ isSigningQRObject: PropTypes.bool,
};
static defaultProps = {
@@ -181,6 +184,7 @@ class Transactions extends PureComponent {
speedUpConfirmDisabled: false,
rpcBlockExplorer: undefined,
errorMsg: undefined,
+ isQRHardwareAccount: false,
};
existingGas = null;
@@ -210,6 +214,7 @@ class Transactions extends PureComponent {
blockExplorer = findBlockExplorerForRpc(rpcTarget, frequentRpcList) || NO_RPC_BLOCK_EXPLORER;
}
this.setState({ rpcBlockExplorer: blockExplorer });
+ this.setState({ isQRHardwareAccount: isQRHardwareAccount(this.props.selectedAddress) });
};
componentWillUnmount() {
@@ -422,15 +427,15 @@ class Transactions extends PureComponent {
this.setState({ errorMsg: e.message, cancel1559IsOpen: false, cancelIsOpen: false });
};
- speedUpTransaction = (EIP1559TransactionData) => {
+ speedUpTransaction = async (EIP1559TransactionData) => {
try {
if (EIP1559TransactionData) {
- Engine.context.TransactionController.speedUpTransaction(this.speedUpTxId, {
+ await Engine.context.TransactionController.speedUpTransaction(this.speedUpTxId, {
maxFeePerGas: `0x${EIP1559TransactionData?.suggestedMaxFeePerGasHex}`,
maxPriorityFeePerGas: `0x${EIP1559TransactionData?.suggestedMaxPriorityFeePerGasHex}`,
});
} else {
- Engine.context.TransactionController.speedUpTransaction(this.speedUpTxId);
+ await Engine.context.TransactionController.speedUpTransaction(this.speedUpTxId);
}
this.onSpeedUpCompleted();
} catch (e) {
@@ -438,15 +443,25 @@ class Transactions extends PureComponent {
}
};
- cancelTransaction = (EIP1559TransactionData) => {
+ signQRTransaction = async (tx) => {
+ const { KeyringController, TransactionController } = Engine.context;
+ await KeyringController.resetQRKeyringState();
+ await TransactionController.approveTransaction(tx.id);
+ };
+
+ cancelUnsignedQRTransaction = async (tx) => {
+ await Engine.context.TransactionController.cancelTransaction(tx.id);
+ };
+
+ cancelTransaction = async (EIP1559TransactionData) => {
try {
if (EIP1559TransactionData) {
- Engine.context.TransactionController.stopTransaction(this.cancelTxId, {
+ await Engine.context.TransactionController.stopTransaction(this.cancelTxId, {
maxFeePerGas: `0x${EIP1559TransactionData?.suggestedMaxFeePerGasHex}`,
maxPriorityFeePerGas: `0x${EIP1559TransactionData?.suggestedMaxPriorityFeePerGasHex}`,
});
} else {
- Engine.context.TransactionController.stopTransaction(this.cancelTxId);
+ await Engine.context.TransactionController.stopTransaction(this.cancelTxId);
}
this.onCancelCompleted();
} catch (e) {
@@ -460,6 +475,9 @@ class Transactions extends PureComponent {
i={index}
assetSymbol={this.props.assetSymbol}
onSpeedUpAction={this.onSpeedUpAction}
+ isQRHardwareAccount={this.state.isQRHardwareAccount}
+ signQRTransaction={this.signQRTransaction}
+ cancelUnsignedQRTransaction={this.cancelUnsignedQRTransaction}
onCancelAction={this.onCancelAction}
testID={'txn-item'}
onPressItem={this.toggleDetailsView}
@@ -495,11 +513,12 @@ class Transactions extends PureComponent {
};
renderUpdateTxEIP1559Gas = (isCancel) => {
+ const { isSigningQRObject } = this.props;
const colors = this.context.colors || mockTheme.colors;
const styles = createStyles(colors);
if (!this.existingGas) return null;
- if (this.existingGas.isEIP1559Transaction) {
+ if (this.existingGas.isEIP1559Transaction && !isSigningQRObject) {
return (
{
- const { submittedTransactions, confirmedTransactions, header } = this.props;
+ const { submittedTransactions, confirmedTransactions, header, isSigningQRObject } = this.props;
const { cancelConfirmDisabled, speedUpConfirmDisabled } = this.state;
const colors = this.context.colors || mockTheme.colors;
const styles = createStyles(colors);
@@ -579,32 +598,36 @@ class Transactions extends PureComponent {
scrollIndicatorInsets={{ right: 1 }}
/>
-
-
+ {!isSigningQRObject && this.state.cancelIsOpen && (
+
+ )}
+ {!isSigningQRObject && this.state.speedUpIsOpen && (
+
+ )}
({
showAlert: (config) => dispatch(showAlert(config)),
});
-export default connect(mapStateToProps, mapDispatchToProps)(Transactions);
+export default connect(mapStateToProps, mapDispatchToProps)(withQRHardwareAwareness(Transactions));
diff --git a/app/components/UI/TypedSign/__snapshots__/index.test.tsx.snap b/app/components/UI/TypedSign/__snapshots__/index.test.tsx.snap
index 302c7015a06..31243e40695 100644
--- a/app/components/UI/TypedSign/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/TypedSign/__snapshots__/index.test.tsx.snap
@@ -1,21 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TypedSign should render correctly 1`] = `
-
-
-
+ messageParams={
+ Object {
+ "data": Object {
+ "name": "Message",
+ "type": "string",
+ "value": "Hi, Alice!",
+ },
+ }
+ }
+/>
`;
diff --git a/app/components/UI/TypedSign/index.js b/app/components/UI/TypedSign/index.js
index 7874712de05..8aa649cad48 100644
--- a/app/components/UI/TypedSign/index.js
+++ b/app/components/UI/TypedSign/index.js
@@ -1,6 +1,7 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { StyleSheet, View, Text, InteractionManager } from 'react-native';
+import { connect } from 'react-redux';
import { fontStyles } from '../../../styles/common';
import Engine from '../../../core/Engine';
import SignatureRequest from '../SignatureRequest';
@@ -11,6 +12,8 @@ import { strings } from '../../../../locales/i18n';
import { WALLET_CONNECT_ORIGIN } from '../../../util/walletconnect';
import AnalyticsV2 from '../../../util/analyticsV2';
import URL from 'url-parse';
+import { getAddressAccountType } from '../../../util/address';
+import { KEYSTONE_TX_CANCELED } from '../../../constants/error';
import { ThemeContext, mockTheme } from '../../../util/theme';
const createStyles = (colors) =>
@@ -34,15 +37,19 @@ const createStyles = (colors) =>
height: 97,
},
msgKey: {
- fontWeight: 'bold',
+ ...fontStyles.bold,
},
});
/**
* Component that supports eth_signTypedData and eth_signTypedData_v3
*/
-export default class TypedSign extends PureComponent {
+class TypedSign extends PureComponent {
static propTypes = {
+ /**
+ * A string that represents the selected address
+ */
+ selectedAddress: PropTypes.string,
/**
* react-navigation object used for switching between screens
*/
@@ -79,11 +86,12 @@ export default class TypedSign extends PureComponent {
getAnalyticsParams = () => {
try {
- const { currentPageInformation, messageParams } = this.props;
+ const { currentPageInformation, messageParams, selectedAddress } = this.props;
const { NetworkController } = Engine.context;
const { chainId, type } = NetworkController?.state?.provider || {};
const url = new URL(currentPageInformation?.url);
return {
+ account_type: getAddressAccountType(selectedAddress),
dapp_host_name: url?.host,
dapp_url: currentPageInformation?.url,
network_name: type,
@@ -140,10 +148,20 @@ export default class TypedSign extends PureComponent {
this.props.onCancel();
};
- confirmSignature = () => {
- this.signMessage();
- AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_COMPLETED, this.getAnalyticsParams());
- this.props.onConfirm();
+ confirmSignature = async () => {
+ try {
+ await this.signMessage();
+ AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.SIGN_REQUEST_COMPLETED, this.getAnalyticsParams());
+ this.props.onConfirm();
+ } catch (e) {
+ if (e?.message.startsWith(KEYSTONE_TX_CANCELED)) {
+ AnalyticsV2.trackEvent(
+ AnalyticsV2.ANALYTICS_EVENTS.QR_HARDWARE_TRANSACTION_CANCELED,
+ this.getAnalyticsParams()
+ );
+ this.props.onCancel();
+ }
+ }
};
shouldTruncateMessage = (e) => {
@@ -251,3 +269,9 @@ export default class TypedSign extends PureComponent {
}
TypedSign.contextType = ThemeContext;
+
+const mapStateToProps = (state) => ({
+ selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress,
+});
+
+export default connect(mapStateToProps)(TypedSign);
diff --git a/app/components/UI/TypedSign/index.test.tsx b/app/components/UI/TypedSign/index.test.tsx
index 52e0e10e71d..a7e3500dc01 100644
--- a/app/components/UI/TypedSign/index.test.tsx
+++ b/app/components/UI/TypedSign/index.test.tsx
@@ -1,14 +1,30 @@
import React from 'react';
import { shallow } from 'enzyme';
import TypedSign from './';
+import { Provider } from 'react-redux';
+import configureMockStore from 'redux-mock-store';
+
+const mockStore = configureMockStore();
+const initialState = {
+ engine: {
+ backgroundState: {
+ PreferencesController: {
+ selectedAddress: '0x0',
+ },
+ },
+ },
+};
+const store = mockStore(initialState);
describe('TypedSign', () => {
it('should render correctly', () => {
const wrapper = shallow(
-
+
+
+
);
expect(wrapper).toMatchSnapshot();
});
diff --git a/app/components/UI/WhatsNewModal/index.js b/app/components/UI/WhatsNewModal/index.js
index b3c0c62dd08..ca0cac21299 100644
--- a/app/components/UI/WhatsNewModal/index.js
+++ b/app/components/UI/WhatsNewModal/index.js
@@ -43,7 +43,6 @@ const createStyles = (colors) =>
color: colors.primary.default,
textAlign: 'center',
...fontStyles.normal,
- fontWeight: '500',
},
header: {
flexDirection: 'row',
diff --git a/app/components/Views/Approval/__snapshots__/index.test.tsx.snap b/app/components/Views/Approval/__snapshots__/index.test.tsx.snap
index b9760485e4f..27f84ecc0c4 100644
--- a/app/components/Views/Approval/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Approval/__snapshots__/index.test.tsx.snap
@@ -14,6 +14,7 @@ exports[`Approval should render correctly 1`] = `
}
networkType="ropsten"
resetTransaction={[Function]}
+ selectedAddress="0x0"
showCustomNonce={false}
transaction={
Object {
diff --git a/app/components/Views/Approval/index.js b/app/components/Views/Approval/index.js
index c684444af2d..d58755a52c6 100644
--- a/app/components/Views/Approval/index.js
+++ b/app/components/Views/Approval/index.js
@@ -13,11 +13,12 @@ import Analytics from '../../../core/Analytics';
import { ANALYTICS_EVENT_OPTS } from '../../../util/analytics';
import { getTransactionReviewActionKey, getNormalizedTxState, getActiveTabUrl } from '../../../util/transactions';
import { strings } from '../../../../locales/i18n';
-import { safeToChecksumAddress } from '../../../util/address';
+import { getAddressAccountType, isQRHardwareAccount, safeToChecksumAddress } from '../../../util/address';
import { WALLET_CONNECT_ORIGIN } from '../../../util/walletconnect';
import Logger from '../../../util/Logger';
import AnalyticsV2 from '../../../util/analyticsV2';
import { GAS_ESTIMATE_TYPES } from '@metamask/controllers';
+import { KEYSTONE_TX_CANCELED } from '../../../constants/error';
import { ThemeContext, mockTheme } from '../../../util/theme';
const REVIEW = 'review';
@@ -36,6 +37,10 @@ const styles = StyleSheet.create({
*/
class Approval extends PureComponent {
static propTypes = {
+ /**
+ * A string that represents the selected address
+ */
+ selectedAddress: PropTypes.string,
/**
* react-navigation object used for switching between screens
*/
@@ -91,35 +96,49 @@ class Approval extends PureComponent {
navigation.setOptions(getTransactionOptionsTitle('approval.title', navigation, {}, colors));
};
- componentDidMount = () => {
- this.updateNavBar();
- };
-
componentDidUpdate = () => {
this.updateNavBar();
};
componentWillUnmount = () => {
- const { transactionHandled } = this.state;
- const { transaction } = this.props;
- if (!transactionHandled) {
- Engine.context.TransactionController.cancelTransaction(transaction.id);
+ try {
+ const { transactionHandled } = this.state;
+ const { transaction, selectedAddress } = this.props;
+ const { KeyringController } = Engine.context;
+ if (!transactionHandled) {
+ if (isQRHardwareAccount(selectedAddress)) {
+ KeyringController.cancelQRSignRequest();
+ } else {
+ Engine.context.TransactionController.cancelTransaction(transaction.id);
+ }
+ Engine.context.TransactionController.hub.removeAllListeners(`${transaction.id}:finished`);
+ AppState.removeEventListener('change', this.handleAppStateChange);
+ this.clear();
+ }
+ } catch (e) {
+ if (e) {
+ throw e;
+ }
}
- Engine.context.TransactionController.hub.removeAllListeners(`${transaction.id}:finished`);
- AppState.removeEventListener('change', this.handleAppStateChange);
- this.clear();
};
handleAppStateChange = (appState) => {
- if (appState !== 'active') {
- const { transaction } = this.props;
- transaction && transaction.id && Engine.context.TransactionController.cancelTransaction(transaction.id);
- this.props.toggleDappTransactionModal(false);
+ try {
+ if (appState !== 'active') {
+ const { transaction } = this.props;
+ transaction && transaction.id && Engine.context.TransactionController.cancelTransaction(transaction.id);
+ this.props.toggleDappTransactionModal(false);
+ }
+ } catch (e) {
+ if (e) {
+ throw e;
+ }
}
};
componentDidMount = () => {
const { navigation } = this.props;
+ this.updateNavBar();
AppState.addEventListener('change', this.handleAppStateChange);
navigation && navigation.setParams({ mode: REVIEW, dispatch: this.onModeChange });
@@ -175,9 +194,10 @@ class Approval extends PureComponent {
getAnalyticsParams = ({ gasEstimateType, gasSelected } = {}) => {
try {
- const { activeTabUrl, chainId, transaction, networkType } = this.props;
+ const { activeTabUrl, chainId, transaction, networkType, selectedAddress } = this.props;
const { selectedAsset } = transaction;
return {
+ account_type: getAddressAccountType(selectedAddress),
dapp_host_name: transaction?.origin,
dapp_url: activeTabUrl,
network_name: networkType,
@@ -227,7 +247,7 @@ class Approval extends PureComponent {
* Callback on confirm transaction
*/
onConfirm = async ({ gasEstimateType, EIP1559GasData, gasSelected }) => {
- const { TransactionController } = Engine.context;
+ const { TransactionController, KeyringController } = Engine.context;
const {
transactions,
transaction: { assetType, selectedAsset },
@@ -267,13 +287,18 @@ class Approval extends PureComponent {
const fullTx = transactions.find(({ id }) => id === transaction.id);
const updatedTx = { ...fullTx, transaction };
await TransactionController.updateTransaction(updatedTx);
+ await KeyringController.resetQRKeyringState();
await TransactionController.approveTransaction(transaction.id);
this.showWalletConnectNotification(true);
} catch (error) {
- Alert.alert(strings('transactions.transaction_error'), error && error.message, [
- { text: strings('navigation.ok') },
- ]);
- Logger.error(error, 'error while trying to send transaction (Approval)');
+ if (!error?.message.startsWith(KEYSTONE_TX_CANCELED)) {
+ Alert.alert(strings('transactions.transaction_error'), error && error.message, [
+ { text: strings('navigation.ok') },
+ ]);
+ Logger.error(error, 'error while trying to send transaction (Approval)');
+ } else {
+ AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.QR_HARDWARE_TRANSACTION_CANCELED);
+ }
this.setState({ transactionHandled: false });
}
AnalyticsV2.trackEvent(
@@ -390,6 +415,7 @@ class Approval extends PureComponent {
const mapStateToProps = (state) => ({
transaction: getNormalizedTxState(state),
transactions: state.engine.backgroundState.TransactionController.transactions,
+ selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress,
networkType: state.engine.backgroundState.NetworkController.provider.type,
showCustomNonce: state.settings.showCustomNonce,
chainId: state.engine.backgroundState.NetworkController.provider.chainId,
diff --git a/app/components/Views/Approval/index.test.tsx b/app/components/Views/Approval/index.test.tsx
index 819a0d12b4a..4e42846d025 100644
--- a/app/components/Views/Approval/index.test.tsx
+++ b/app/components/Views/Approval/index.test.tsx
@@ -33,6 +33,9 @@ const initialState = {
type: ROPSTEN,
},
},
+ PreferencesController: {
+ selectedAddress: '0x0',
+ },
},
},
};
diff --git a/app/components/Views/ApproveView/Approve/index.js b/app/components/Views/ApproveView/Approve/index.js
index 9aa72bf64b3..0351537fa5e 100644
--- a/app/components/Views/ApproveView/Approve/index.js
+++ b/app/components/Views/ApproveView/Approve/index.js
@@ -30,9 +30,10 @@ import EditGasFee1559 from '../../../UI/EditGasFee1559';
import EditGasFeeLegacy from '../../../UI/EditGasFeeLegacy';
import AppConstants from '../../../../core/AppConstants';
import { shallowEqual } from '../../../../util/general';
-import { ThemeContext, mockTheme } from '../../../../util/theme';
+import { KEYSTONE_TX_CANCELED } from '../../../../constants/error';
import GlobalAlert from '../../../UI/GlobalAlert';
import checkIfAddressIsSaved from '../../../../util/checkAddress';
+import { ThemeContext, mockTheme } from '../../../../util/theme';
const { BNToHex, hexToBN } = util;
@@ -433,7 +434,7 @@ class Approve extends PureComponent {
};
onConfirm = async () => {
- const { TransactionController } = Engine.context;
+ const { TransactionController, KeyringController } = Engine.context;
const { transactions, gasEstimateType } = this.props;
const { EIP1559GasData, LegacyGasData, transactionConfirmed } = this.state;
@@ -460,11 +461,16 @@ class Approve extends PureComponent {
const fullTx = transactions.find(({ id }) => id === transaction.id);
const updatedTx = { ...fullTx, transaction };
await TransactionController.updateTransaction(updatedTx);
+ await KeyringController.resetQRKeyringState();
await TransactionController.approveTransaction(transaction.id);
AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.APPROVAL_COMPLETED, this.getAnalyticsParams());
} catch (error) {
- Alert.alert(strings('transactions.transaction_error'), error && error.message, [{ text: 'OK' }]);
- Logger.error(error, 'error while trying to send transaction (Approve)');
+ if (!error?.message.startsWith(KEYSTONE_TX_CANCELED)) {
+ Alert.alert(strings('transactions.transaction_error'), error && error.message, [{ text: 'OK' }]);
+ Logger.error(error, 'error while trying to send transaction (Approve)');
+ } else {
+ AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.QR_HARDWARE_TRANSACTION_CANCELED);
+ }
this.setState({ transactionHandled: false });
}
this.setState({ transactionConfirmed: true });
diff --git a/app/components/Views/Asset/index.js b/app/components/Views/Asset/index.js
index 2cfc211420d..9c1f9cabc44 100644
--- a/app/components/Views/Asset/index.js
+++ b/app/components/Views/Asset/index.js
@@ -205,7 +205,9 @@ class Asset extends PureComponent {
const { chainId, transactions } = this.props;
if (transactions.length) {
- const sortedTransactions = sortTransactions(transactions);
+ const sortedTransactions = sortTransactions(transactions).filter(
+ (tx, index, self) => self.findIndex((_tx) => _tx.id === tx.id) === index
+ );
const filteredTransactions = sortedTransactions.filter((tx) => {
const filterResult = this.filter(tx);
if (filterResult) {
diff --git a/app/components/Views/ConnectQRHardware/Instruction/index.tsx b/app/components/Views/ConnectQRHardware/Instruction/index.tsx
new file mode 100644
index 00000000000..99cf0fc5432
--- /dev/null
+++ b/app/components/Views/ConnectQRHardware/Instruction/index.tsx
@@ -0,0 +1,129 @@
+/* eslint @typescript-eslint/no-var-requires: "off" */
+/* eslint @typescript-eslint/no-require-imports: "off" */
+
+import React from 'react';
+import { View, Text, StyleSheet, Image, ScrollView } from 'react-native';
+import { strings } from '../../../../../locales/i18n';
+import { KEYSTONE_SUPPORT, KEYSTONE_SUPPORT_VIDEO } from '../../../../constants/urls';
+import { fontStyles, colors as importedColors } from '../../../../styles/common';
+import { useAppThemeFromContext, mockTheme } from '../../../../util/theme';
+import StyledButton from '../../../UI/StyledButton';
+
+interface IConnectQRInstructionProps {
+ navigation: any;
+ onConnect: () => void;
+ renderAlert: () => Element;
+}
+
+const connectQRHardwareImg = require('images/connect-qr-hardware.png'); // eslint-disable-line import/no-commonjs
+
+const createStyles = (colors: any) =>
+ StyleSheet.create({
+ wrapper: {
+ flex: 1,
+ width: '100%',
+ alignItems: 'center',
+ },
+ container: {
+ flexDirection: 'column',
+ alignItems: 'center',
+ paddingHorizontal: 32,
+ },
+ scrollWrapper: {
+ width: '100%',
+ },
+ title: {
+ width: '100%',
+ marginTop: 40,
+ fontSize: 24,
+ marginBottom: 20,
+ ...fontStyles.normal,
+ color: colors.text.alternative,
+ },
+ textContainer: {
+ width: '100%',
+ marginTop: 20,
+ },
+ text: {
+ fontSize: 14,
+ marginBottom: 24,
+ ...fontStyles.normal,
+ color: colors.text.alternative,
+ },
+ link: {
+ color: colors.primary.default,
+ ...fontStyles.bold,
+ },
+ bottom: {
+ alignItems: 'center',
+ height: 80,
+ justifyContent: 'space-between',
+ },
+ button: {
+ padding: 5,
+ paddingHorizontal: '30%',
+ },
+ buttonText: {
+ color: importedColors.white,
+ ...fontStyles.normal,
+ },
+ image: {
+ width: 300,
+ height: 120,
+ marginTop: 40,
+ marginBottom: 40,
+ },
+ });
+
+const ConnectQRInstruction = (props: IConnectQRInstructionProps) => {
+ const { onConnect, renderAlert, navigation } = props;
+ const { colors } = useAppThemeFromContext() || mockTheme;
+ const styles = createStyles(colors);
+
+ const navigateToVideo = () => {
+ navigation.navigate('Webview', {
+ screen: 'SimpleWebview',
+ params: {
+ url: KEYSTONE_SUPPORT_VIDEO,
+ title: strings('connect_qr_hardware.description2'),
+ },
+ });
+ };
+ const navigateToTutorial = () => {
+ navigation.navigate('Webview', {
+ screen: 'SimpleWebview',
+ params: {
+ url: KEYSTONE_SUPPORT,
+ title: strings('connect_qr_hardware.description4'),
+ },
+ });
+ };
+ return (
+
+
+ {strings('connect_qr_hardware.title')}
+ {renderAlert()}
+
+ {strings('connect_qr_hardware.description1')}
+
+ {strings('connect_qr_hardware.description2')}
+
+ {strings('connect_qr_hardware.description3')}
+
+ {strings('connect_qr_hardware.description4')}
+
+ {strings('connect_qr_hardware.description5')}
+ {strings('connect_qr_hardware.description6')}
+
+
+
+
+
+ {strings('connect_qr_hardware.button_continue')}
+
+
+
+ );
+};
+
+export default ConnectQRInstruction;
diff --git a/app/components/Views/ConnectQRHardware/SelectQRAccounts/index.tsx b/app/components/Views/ConnectQRHardware/SelectQRAccounts/index.tsx
new file mode 100644
index 00000000000..52b02a7fc7e
--- /dev/null
+++ b/app/components/Views/ConnectQRHardware/SelectQRAccounts/index.tsx
@@ -0,0 +1,168 @@
+import React from 'react';
+import { View, Text, FlatList, StyleSheet, TouchableOpacity } from 'react-native';
+import { strings } from '../../../../../locales/i18n';
+import Icon from 'react-native-vector-icons/FontAwesome';
+import { fontStyles } from '../../../../styles/common';
+import CheckBox from '@react-native-community/checkbox';
+import { IAccount } from '../types';
+import { renderFromWei } from '../../../../util/number';
+import { useSelector } from 'react-redux';
+import { useNavigation } from '@react-navigation/native';
+import { getEtherscanAddressUrl } from '../../../../util/etherscan';
+import { mockTheme, useAppThemeFromContext } from '../../../../util/theme';
+import EthereumAddress from '../../../UI/EthereumAddress';
+import StyledButton from '../../../UI/StyledButton';
+
+interface ISelectQRAccountsProps {
+ canUnlock: boolean;
+ accounts: IAccount[];
+ nextPage: () => void;
+ prevPage: () => void;
+ toggleAccount: (index: number) => void;
+ onUnlock: () => void;
+ onForget: () => void;
+}
+
+const createStyle = (colors: any) =>
+ StyleSheet.create({
+ container: {
+ width: '100%',
+ paddingHorizontal: 32,
+ },
+ title: {
+ marginTop: 40,
+ fontSize: 24,
+ marginBottom: 24,
+ ...fontStyles.normal,
+ color: colors.text.default,
+ },
+ account: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ height: 36,
+ width: '100%',
+ paddingVertical: 4,
+ },
+ checkBox: {
+ marginRight: 8,
+ },
+ accountUnchecked: {
+ backgroundColor: colors.primary.muted,
+ },
+ accountChecked: {
+ backgroundColor: colors.primary.disabled,
+ },
+ number: {
+ ...fontStyles.normal,
+ color: colors.text.default,
+ },
+ address: {
+ marginLeft: 8,
+ fontSize: 15,
+ flexGrow: 1,
+ ...fontStyles.normal,
+ color: colors.text.default,
+ },
+ pagination: {
+ marginTop: 16,
+ alignSelf: 'flex-end',
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ paginationText: {
+ ...fontStyles.normal,
+ fontSize: 18,
+ color: colors.primary.default,
+ },
+ paginationItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginRight: 8,
+ },
+ bottom: {
+ alignItems: 'center',
+ marginTop: 150,
+ height: 100,
+ justifyContent: 'space-between',
+ },
+ button: {
+ width: '100%',
+ padding: 12,
+ },
+ });
+
+const SelectQRAccounts = (props: ISelectQRAccountsProps) => {
+ const { accounts, prevPage, nextPage, toggleAccount, onForget, onUnlock, canUnlock } = props;
+ const { colors } = useAppThemeFromContext() || mockTheme;
+ const styles = createStyle(colors);
+ const navigation = useNavigation();
+ const provider = useSelector((state: any) => state.engine.backgroundState.NetworkController.provider);
+
+ const toEtherscan = (address: string) => {
+ const accountLink = getEtherscanAddressUrl(provider.type, address);
+ navigation.navigate('Webview', {
+ screen: 'SimpleWebview',
+ params: {
+ url: accountLink,
+ },
+ });
+ };
+
+ return (
+
+ {strings('connect_qr_hardware.select_accounts')}
+ `address-${item.index}`}
+ renderItem={({ item }) => (
+
+ toggleAccount(item.index)}
+ boxType={'square'}
+ tintColors={{ true: colors.primary.default, false: colors.border.default }}
+ testID={'skip-backup-check'}
+ />
+ {item.index}
+
+ {renderFromWei(item.balance)} ETH
+ toEtherscan(item.address)}
+ color={colors.text.default}
+ />
+
+ )}
+ />
+
+
+
+ {strings('connect_qr_hardware.prev')}
+
+
+ {strings('connect_qr_hardware.next')}
+
+
+
+
+
+
+ {strings('connect_qr_hardware.unlock')}
+
+
+ {strings('connect_qr_hardware.forget')}
+
+
+
+ );
+};
+
+export default SelectQRAccounts;
diff --git a/app/components/Views/ConnectQRHardware/index.tsx b/app/components/Views/ConnectQRHardware/index.tsx
new file mode 100644
index 00000000000..02990a8f028
--- /dev/null
+++ b/app/components/Views/ConnectQRHardware/index.tsx
@@ -0,0 +1,295 @@
+import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
+import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+import Engine from '../../../core/Engine';
+import AnimatedQRScannerModal from '../../UI/QRHardware/AnimatedQRScanner';
+import SelectQRAccounts from './SelectQRAccounts';
+import ConnectQRInstruction from './Instruction';
+import Icon from 'react-native-vector-icons/FontAwesome';
+import BlockingActionModal from '../../UI/BlockingActionModal';
+import { strings } from '../../../../locales/i18n';
+import { IAccount } from './types';
+import { UR } from '@ngraveio/bc-ur';
+import Alert, { AlertType } from '../../Base/Alert';
+import AnalyticsV2 from '../../../util/analyticsV2';
+import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
+import Device from '../../../util/device';
+import { mockTheme, useAppThemeFromContext } from '../../../util/theme';
+import { SUPPORTED_UR_TYPE } from '../../../constants/qr';
+import { fontStyles } from '../../../styles/common';
+import Logger from '../../../util/Logger';
+
+interface IConnectQRHardwareProps {
+ navigation: any;
+}
+const createStyles = (colors: any) =>
+ StyleSheet.create({
+ container: {
+ flex: 1,
+ flexDirection: 'column',
+ alignItems: 'center',
+ },
+ navbarRightButton: {
+ alignSelf: 'flex-end',
+ paddingTop: 20,
+ paddingBottom: 10,
+ marginTop: Device.isIphoneX() ? 40 : 20,
+ },
+ closeIcon: {
+ fontSize: 28,
+ color: colors.text.default,
+ },
+ header: {
+ width: '100%',
+ paddingHorizontal: 32,
+ flexDirection: 'column',
+ alignItems: 'center',
+ },
+ close: {
+ alignSelf: 'flex-end',
+ width: 48,
+ height: 48,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ qrcode: {
+ alignSelf: 'flex-start',
+ },
+ error: {
+ ...fontStyles.normal,
+ fontSize: 14,
+ color: colors.red,
+ },
+ text: {
+ color: colors.text.default,
+ fontSize: 14,
+ ...fontStyles.normal,
+ },
+ });
+
+const ConnectQRHardware = ({ navigation }: IConnectQRHardwareProps) => {
+ const { colors } = useAppThemeFromContext() || mockTheme;
+ const styles = createStyles(colors);
+
+ const KeyringController = useMemo(() => {
+ const { KeyringController: keyring } = Engine.context as any;
+ return keyring;
+ }, []);
+
+ const AccountTrackerController = useMemo(() => (Engine.context as any).AccountTrackerController, []);
+
+ const [QRState, setQRState] = useState({
+ sync: {
+ reading: false,
+ },
+ });
+ const [scannerVisible, setScannerVisible] = useState(false);
+ const [blockingModalVisible, setBlockingModalVisible] = useState(false);
+ const [accounts, setAccounts] = useState<{ address: string; index: number; balance: string }[]>([]);
+ const [trackedAccounts, setTrackedAccounts] = useState<{ [p: string]: { balance: string } }>({});
+ const [checkedAccounts, setCheckedAccounts] = useState([]);
+ const [errorMsg, setErrorMsg] = useState('');
+ const resetError = useCallback(() => {
+ setErrorMsg('');
+ }, []);
+
+ const [existingAccounts, setExistingAccounts] = useState([]);
+
+ const showScanner = useCallback(() => {
+ setScannerVisible(true);
+ }, []);
+
+ const hideScanner = useCallback(() => {
+ setScannerVisible(false);
+ }, []);
+
+ useEffect(() => {
+ KeyringController.getAccounts().then((value: string[]) => {
+ setExistingAccounts(value);
+ });
+ }, [KeyringController]);
+
+ const subscribeKeyringState = useCallback((storeValue: any) => {
+ setQRState(storeValue);
+ }, []);
+
+ useEffect(() => {
+ let memStore: any;
+ KeyringController.getQRKeyringState().then((_memStore: any) => {
+ memStore = _memStore;
+ memStore.subscribe(subscribeKeyringState);
+ });
+ return () => {
+ if (memStore) {
+ memStore.unsubscribe(subscribeKeyringState);
+ }
+ };
+ }, [KeyringController, subscribeKeyringState]);
+
+ useEffect(() => {
+ const unTrackedAccounts: string[] = [];
+ accounts.forEach((account) => {
+ if (!trackedAccounts[account.address]) {
+ unTrackedAccounts.push(account.address);
+ }
+ });
+ if (unTrackedAccounts.length > 0) {
+ AccountTrackerController.syncBalanceWithAddresses(unTrackedAccounts).then((_trackedAccounts: any) => {
+ setTrackedAccounts(Object.assign({}, trackedAccounts, _trackedAccounts));
+ });
+ }
+ }, [AccountTrackerController, accounts, trackedAccounts]);
+
+ useEffect(() => {
+ if (QRState.sync.reading) {
+ showScanner();
+ } else {
+ hideScanner();
+ }
+ }, [QRState.sync, hideScanner, showScanner]);
+
+ const onConnectHardware = useCallback(async () => {
+ AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.CONTINUE_QR_HARDWARE_WALLET, {
+ device_type: 'QR Hardware',
+ });
+ resetError();
+ const _accounts = await KeyringController.connectQRHardware(0);
+ setAccounts(_accounts);
+ }, [KeyringController, resetError]);
+
+ const onScanSuccess = useCallback(
+ (ur: UR) => {
+ hideScanner();
+ AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.CONNECT_HARDWARE_WALLET_SUCCESS, {
+ device_type: 'QR Hardware',
+ });
+ if (ur.type === SUPPORTED_UR_TYPE.CRYPTO_HDKEY) {
+ KeyringController.submitQRCryptoHDKey(ur.cbor.toString('hex'));
+ } else {
+ KeyringController.submitQRCryptoAccount(ur.cbor.toString('hex'));
+ }
+ resetError();
+ },
+ [KeyringController, hideScanner, resetError]
+ );
+
+ const onScanError = useCallback(
+ (error: string) => {
+ hideScanner();
+ setErrorMsg(error);
+ },
+ [hideScanner]
+ );
+
+ const nextPage = useCallback(async () => {
+ resetError();
+ const _accounts = await KeyringController.connectQRHardware(1);
+ setAccounts(_accounts);
+ }, [KeyringController, resetError]);
+
+ const prevPage = useCallback(async () => {
+ resetError();
+ const _accounts = await KeyringController.connectQRHardware(-1);
+ setAccounts(_accounts);
+ }, [KeyringController, resetError]);
+
+ const onToggle = useCallback(
+ (index: number) => {
+ resetError();
+ if (!checkedAccounts.includes(index)) {
+ setCheckedAccounts([...checkedAccounts, index]);
+ } else {
+ setCheckedAccounts(checkedAccounts.filter((i) => i !== index));
+ }
+ },
+ [checkedAccounts, resetError]
+ );
+
+ const enhancedAccounts: IAccount[] = useMemo(
+ () =>
+ accounts.map((account) => {
+ let checked = false;
+ let exist = false;
+ if (checkedAccounts.includes(account.index)) checked = true;
+ if (existingAccounts.find((item) => item.toLowerCase() === account.address.toLowerCase())) {
+ exist = true;
+ checked = true;
+ }
+ return {
+ ...account,
+ checked,
+ exist,
+ balance: trackedAccounts[account.address]?.balance || '0x0',
+ };
+ }),
+ [accounts, checkedAccounts, existingAccounts, trackedAccounts]
+ );
+
+ const onUnlock = useCallback(async () => {
+ resetError();
+ setBlockingModalVisible(true);
+ try {
+ for (const account of checkedAccounts) {
+ await KeyringController.unlockQRHardwareWalletAccount(account);
+ }
+ } catch (err) {
+ Logger.log('Error: Connecting QR hardware wallet', err);
+ }
+ setBlockingModalVisible(false);
+ navigation.goBack();
+ }, [KeyringController, checkedAccounts, navigation, resetError]);
+
+ const onForget = useCallback(async () => {
+ resetError();
+ await KeyringController.forgetQRDevice();
+ navigation.goBack();
+ }, [KeyringController, navigation, resetError]);
+
+ const renderAlert = () =>
+ errorMsg !== '' && (
+
+ {errorMsg}
+
+ );
+
+ return (
+
+
+
+
+
+
+
+
+ {accounts.length <= 0 ? (
+
+ ) : (
+ 0}
+ accounts={enhancedAccounts}
+ nextPage={nextPage}
+ prevPage={prevPage}
+ toggleAccount={onToggle}
+ onUnlock={onUnlock}
+ onForget={onForget}
+ />
+ )}
+
+
+
+ {strings('connect_qr_hardware.please_wait')}
+
+
+ );
+};
+
+export default ConnectQRHardware;
diff --git a/app/components/Views/ConnectQRHardware/types.ts b/app/components/Views/ConnectQRHardware/types.ts
new file mode 100644
index 00000000000..8d205ebfe45
--- /dev/null
+++ b/app/components/Views/ConnectQRHardware/types.ts
@@ -0,0 +1,7 @@
+export interface IAccount {
+ address: string;
+ balance: string;
+ index: number;
+ checked: boolean;
+ exist: boolean;
+}
diff --git a/app/components/Views/ErrorBoundary/index.js b/app/components/Views/ErrorBoundary/index.js
index 7c834257431..71fc477b594 100644
--- a/app/components/Views/ErrorBoundary/index.js
+++ b/app/components/Views/ErrorBoundary/index.js
@@ -69,7 +69,6 @@ const createStyles = (colors) =>
color: colors.primary.default,
textAlign: 'center',
...fontStyles.normal,
- fontWeight: '500',
},
textContainer: {
marginTop: 24,
diff --git a/app/components/Views/QRScanner/__snapshots__/index.test.tsx.snap b/app/components/Views/QRScanner/__snapshots__/index.test.tsx.snap
index db8c6f2feb1..0bc3219ba23 100644
--- a/app/components/Views/QRScanner/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/QRScanner/__snapshots__/index.test.tsx.snap
@@ -1,151 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QrScanner should render correctly 1`] = `
-
-
-
- Camera not authorized
-
-
- }
- onBarCodeRead={[Function]}
- onMountError={[Function]}
- onStatusChange={[Function]}
- pendingAuthorizationView={
-
-
-
- }
- permissionDialogMessage=""
- permissionDialogTitle=""
- pictureSize="None"
- playSoundOnCapture={false}
- playSoundOnRecord={false}
- ratio="4:3"
- style={
- Object {
- "flex": 1,
- }
- }
- type={1}
- useCamera2Api={false}
- useNativeZoom={false}
- videoStabilizationMode={0}
- zoom={0}
- >
-
-
-
-
-
-
- scanning...
-
-
-
-
+/>
`;
diff --git a/app/components/Views/QRScanner/index.test.tsx b/app/components/Views/QRScanner/index.test.tsx
index 8244781b12a..6f92b756086 100644
--- a/app/components/Views/QRScanner/index.test.tsx
+++ b/app/components/Views/QRScanner/index.test.tsx
@@ -1,22 +1,40 @@
import React from 'react';
import { shallow } from 'enzyme';
import QrScanner from './';
+import { Provider } from 'react-redux';
+import configureMockStore from 'redux-mock-store';
const noop = () => null;
+const mockStore = configureMockStore();
+const initialState = {
+ engine: {
+ backgroundState: {
+ NetworkController: {
+ provider: {
+ chainId: 4,
+ },
+ },
+ },
+ },
+};
+const store = mockStore(initialState);
+
describe('QrScanner', () => {
it('should render correctly', () => {
const wrapper = shallow(
-
+
+
+
);
expect(wrapper).toMatchSnapshot();
});
diff --git a/app/components/Views/QRScanner/index.tsx b/app/components/Views/QRScanner/index.tsx
index 36e48291484..c7d95594e1f 100644
--- a/app/components/Views/QRScanner/index.tsx
+++ b/app/components/Views/QRScanner/index.tsx
@@ -8,11 +8,13 @@ import { RNCamera } from 'react-native-camera';
import { colors as importedColors } from '../../../styles/common';
import Icon from 'react-native-vector-icons/Ionicons';
import { parse } from 'eth-url-parser';
+import { isValidAddress } from 'ethereumjs-util';
import { strings } from '../../../../locales/i18n';
import SharedDeeplinkManager from '../../../core/DeeplinkManager';
import AppConstants from '../../../core/AppConstants';
import { failedSeedPhraseRequirements, isValidMnemonic } from '../../../util/validators';
import Engine from '../../../core/Engine';
+import { useSelector } from 'react-redux';
// TODO: This file needs typings
const styles = StyleSheet.create({
@@ -71,6 +73,8 @@ const QRScanner = ({ navigation, route }: Props) => {
const mountedRef = useRef(true);
const shouldReadBarCodeRef = useRef(true);
+ const currentChainId = useSelector((state: any) => state.engine.backgroundState.NetworkController.provider.chainId);
+
const goBack = useCallback(() => {
navigation.goBack();
onScanError?.('USER_CANCELLED');
@@ -128,14 +132,18 @@ const QRScanner = ({ navigation, route }: Props) => {
mountedRef.current = false;
return;
}
- // Let ethereum:address go forward
- if (content.split('ethereum:').length > 1 && !parse(content).function_name) {
+ // Let ethereum:address and address go forward
+ if (
+ (content.split('ethereum:').length > 1 && !parse(content).function_name) ||
+ (content.startsWith('0x') && isValidAddress(content))
+ ) {
+ const handledContent = content.startsWith('0x') ? `ethereum:${content}@${currentChainId}` : content;
shouldReadBarCodeRef.current = false;
- data = parse(content);
+ data = parse(handledContent);
const action = 'send-eth';
data = { ...data, action };
end();
- onScanSuccess(data, content);
+ onScanSuccess(data, handledContent);
return;
}
@@ -172,7 +180,7 @@ const QRScanner = ({ navigation, route }: Props) => {
end();
},
- [onStartScan, end, mountedRef, navigation, onScanSuccess]
+ [end, onStartScan, onScanSuccess, navigation, currentChainId]
);
const onError = useCallback(
diff --git a/app/components/Views/RevealPrivateCredential/index.js b/app/components/Views/RevealPrivateCredential/index.js
index 6955244abab..f70f6c39985 100644
--- a/app/components/Views/RevealPrivateCredential/index.js
+++ b/app/components/Views/RevealPrivateCredential/index.js
@@ -33,6 +33,7 @@ import { fontStyles } from '../../../styles/common';
import AnalyticsV2 from '../../../util/analyticsV2';
import Device from '../../../util/device';
import { strings } from '../../../../locales/i18n';
+import { isQRHardwareAccount } from '../../../util/address';
const createStyles = (colors) =>
StyleSheet.create({
@@ -293,7 +294,9 @@ class RevealPrivateCredential extends PureComponent {
}
} catch (e) {
let msg = strings('reveal_credential.warning_incorrect_password');
- if (e.toString().toLowerCase() !== WRONG_PASSWORD_ERROR.toLowerCase()) {
+ if (isQRHardwareAccount(selectedAddress)) {
+ msg = strings('reveal_credential.hardware_error');
+ } else if (e.toString().toLowerCase() !== WRONG_PASSWORD_ERROR.toLowerCase()) {
msg = strings('reveal_credential.unknown_error');
}
diff --git a/app/components/Views/Send/index.js b/app/components/Views/Send/index.js
index 2bbc1abcd7a..b06abc0895c 100644
--- a/app/components/Views/Send/index.js
+++ b/app/components/Views/Send/index.js
@@ -28,6 +28,8 @@ import { MAINNET } from '../../../constants/network';
import BigNumber from 'bignumber.js';
import { WalletDevice } from '@metamask/controllers/';
import { getTokenList } from '../../../reducers/tokens';
+import AnalyticsV2 from '../../../util/analyticsV2';
+import { KEYSTONE_TX_CANCELED } from '../../../constants/error';
import { ThemeContext, mockTheme } from '../../../util/theme';
const REVIEW = 'review';
@@ -457,7 +459,7 @@ class Send extends PureComponent {
* and returns to edit transaction
*/
onConfirm = async () => {
- const { TransactionController, AddressBookController } = Engine.context;
+ const { TransactionController, AddressBookController, KeyringController } = Engine.context;
this.setState({ transactionConfirmed: true });
const {
transaction: { selectedAsset, assetType },
@@ -476,7 +478,7 @@ class Send extends PureComponent {
TransactionTypes.MMM,
WalletDevice.MM_MOBILE
);
-
+ await KeyringController.resetQRKeyringState();
await TransactionController.approveTransaction(transactionMeta.id);
// Add to the AddressBook if it's an unkonwn address
@@ -524,10 +526,14 @@ class Send extends PureComponent {
this.removeCollectible();
});
} catch (error) {
- Alert.alert(strings('transactions.transaction_error'), error && error.message, [
- { text: strings('navigation.ok') },
- ]);
- Logger.error(error, 'error while trying to send transaction (Send)');
+ if (!error?.message.startsWith(KEYSTONE_TX_CANCELED)) {
+ Alert.alert(strings('transactions.transaction_error'), error && error.message, [
+ { text: strings('navigation.ok') },
+ ]);
+ Logger.error(error, 'error while trying to send transaction (Send)');
+ } else {
+ AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.QR_HARDWARE_TRANSACTION_CANCELED);
+ }
this.setState({ transactionConfirmed: false });
await this.reset();
}
diff --git a/app/components/Views/SendFlow/AddressInputs/index.js b/app/components/Views/SendFlow/AddressInputs/index.js
index ca6f28c4888..1c232bb1891 100644
--- a/app/components/Views/SendFlow/AddressInputs/index.js
+++ b/app/components/Views/SendFlow/AddressInputs/index.js
@@ -5,7 +5,7 @@ import AntIcon from 'react-native-vector-icons/AntDesign';
import FontAwesome from 'react-native-vector-icons/FontAwesome';
import PropTypes from 'prop-types';
import Identicon from '../../../UI/Identicon';
-import { renderShortAddress } from '../../../../util/address';
+import { isQRHardwareAccount, renderShortAddress } from '../../../../util/address';
import { strings } from '../../../../../locales/i18n';
import Text from '../../../Base/Text';
import { hasZeroWidthPoints } from '../../../../util/confusables';
@@ -71,6 +71,21 @@ const createStyles = (colors) =>
color: colors.text.default,
fontSize: 14,
},
+ accountNameLabel: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ accountNameLabelText: {
+ marginLeft: 4,
+ paddingHorizontal: 8,
+ ...fontStyles.bold,
+ color: colors.primary.alternative,
+ borderWidth: 1,
+ borderRadius: 8,
+ borderColor: colors.primary.alternative,
+ fontSize: 10,
+ },
textBalance: {
...fontStyles.normal,
fontSize: 12,
@@ -337,6 +352,7 @@ AddressTo.propTypes = {
export const AddressFrom = (props) => {
const { highlighted, onPressIcon, fromAccountName, fromAccountBalance, fromAccountAddress } = props;
+ const isHardwareAccount = isQRHardwareAccount(fromAccountAddress);
const { colors } = useAppThemeFromContext() || mockTheme;
const styles = createStyles(colors);
@@ -350,7 +366,12 @@ export const AddressFrom = (props) => {
- {fromAccountName}
+
+ {fromAccountName}
+ {isHardwareAccount && (
+ {strings('transaction.hardware')}
+ )}
+
{`${strings(
'transactions.address_from_balance'
)} ${fromAccountBalance}`}
diff --git a/app/components/Views/SendFlow/Amount/index.js b/app/components/Views/SendFlow/Amount/index.js
index c33c00dc7d7..43269eb8765 100644
--- a/app/components/Views/SendFlow/Amount/index.js
+++ b/app/components/Views/SendFlow/Amount/index.js
@@ -137,8 +137,7 @@ const createStyles = (colors) =>
flexDirection: 'row',
},
inputCurrencyText: {
- ...fontStyles.normal,
- fontWeight: fontStyles.light.fontWeight,
+ ...fontStyles.light,
color: colors.text.default,
fontSize: 44,
marginRight: 8,
@@ -148,8 +147,7 @@ const createStyles = (colors) =>
textTransform: 'uppercase',
},
textInput: {
- ...fontStyles.normal,
- fontWeight: fontStyles.light.fontWeight,
+ ...fontStyles.light,
fontSize: 44,
textAlign: 'center',
color: colors.text.default,
diff --git a/app/components/Views/SendFlow/Confirm/index.js b/app/components/Views/SendFlow/Confirm/index.js
index a812746b508..6304c73bf9d 100644
--- a/app/components/Views/SendFlow/Confirm/index.js
+++ b/app/components/Views/SendFlow/Confirm/index.js
@@ -62,6 +62,8 @@ import EditGasFee1559 from '../../../UI/EditGasFee1559';
import EditGasFeeLegacy from '../../../UI/EditGasFeeLegacy';
import CustomNonce from '../../../UI/CustomNonce';
import AppConstants from '../../../../core/AppConstants';
+import { getAddressAccountType, isQRHardwareAccount } from '../../../../util/address';
+import { KEYSTONE_TX_CANCELED } from '../../../../constants/error';
import { ThemeContext, mockTheme } from '../../../../util/theme';
const EDIT = 'edit';
@@ -103,8 +105,7 @@ const createStyles = (colors) =>
marginVertical: 3,
},
textAmount: {
- ...fontStyles.normal,
- fontWeight: fontStyles.light.fontWeight,
+ ...fontStyles.light,
color: colors.text.default,
fontSize: 44,
textAlign: 'center',
@@ -384,10 +385,11 @@ class Confirm extends PureComponent {
getAnalyticsParams = () => {
try {
const { selectedAsset, gasEstimateType, chainId, networkType } = this.props;
- const { gasSelected } = this.state;
+ const { gasSelected, fromSelectedAddress } = this.state;
return {
active_currency: { value: selectedAsset?.symbol, anonymous: true },
+ account_type: getAddressAccountType(fromSelectedAddress),
network_name: networkType,
chain_id: chainId,
gas_estimate_type: gasEstimateType,
@@ -821,7 +823,7 @@ class Confirm extends PureComponent {
};
onNext = async () => {
- const { TransactionController } = Engine.context;
+ const { TransactionController, KeyringController } = Engine.context;
const {
transactionState: { assetType },
navigation,
@@ -850,6 +852,7 @@ class Confirm extends PureComponent {
TransactionTypes.MMM,
WalletDevice.MM_MOBILE
);
+ await KeyringController.resetQRKeyringState();
await TransactionController.approveTransaction(transactionMeta.id);
await new Promise((resolve) => resolve(result));
@@ -871,10 +874,12 @@ class Confirm extends PureComponent {
navigation && navigation.dangerouslyGetParent()?.pop();
});
} catch (error) {
- Alert.alert(strings('transactions.transaction_error'), error && error.message, [
- { text: strings('navigation.ok') },
- ]);
- Logger.error(error, 'error while trying to send transaction (Confirm)');
+ if (!error?.message.startsWith(KEYSTONE_TX_CANCELED)) {
+ Alert.alert(strings('transactions.transaction_error'), error && error.message, [{ text: 'OK' }]);
+ Logger.error(error, 'error while trying to send transaction (Confirm)');
+ } else {
+ AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.QR_HARDWARE_TRANSACTION_CANCELED);
+ }
}
this.setState({ transactionConfirmed: false });
};
@@ -1216,6 +1221,7 @@ class Confirm extends PureComponent {
const checksummedAddress = transactionTo && toChecksumAddress(transactionTo);
const existingContact = checksummedAddress && addressBook[network] && addressBook[network][checksummedAddress];
const displayExclamation = !existingContact && !!confusableCollection.length;
+ const isQRHardwareWalletDevice = isQRHardwareAccount(fromSelectedAddress);
const AdressToComponent = () => (
{transactionConfirmed ? (
+ ) : isQRHardwareWalletDevice ? (
+ strings('transaction.confirm_with_qr_hardware')
) : (
strings('transaction.send')
)}
diff --git a/app/components/Views/TransactionsView/index.js b/app/components/Views/TransactionsView/index.js
index 8124c76f8f4..92fd6c7f866 100644
--- a/app/components/Views/TransactionsView/index.js
+++ b/app/components/Views/TransactionsView/index.js
@@ -46,8 +46,9 @@ const TransactionsView = ({
const confirmedTxs = [];
const submittedNonces = [];
- const allTransactionsSorted = sortTransactions(transactions);
-
+ const allTransactionsSorted = sortTransactions(transactions).filter(
+ (tx, index, self) => self.findIndex((_tx) => _tx.id === tx.id) === index
+ );
const allTransactions = allTransactionsSorted.filter((tx) => {
const filter = filterByAddressAndNetwork(tx, tokens, selectedAddress, chainId, network);
if (!filter) return false;
diff --git a/app/constants/error.ts b/app/constants/error.ts
index 9a56b45ab81..6ca8a9c20d8 100644
--- a/app/constants/error.ts
+++ b/app/constants/error.ts
@@ -2,3 +2,7 @@
export const NETWORK_ERROR_MISSING_NETWORK_ID = 'Missing network id';
export const NETWORK_ERROR_UNKNOWN_NETWORK_ID = 'Unknown network with id';
export const NETWORK_ERROR_MISSING_CHAIN_ID = 'Missing chain id';
+export const NETWORK_ERROR_UNKNOWN_CHAIN_ID = 'Unknown chain id';
+
+// QR hardware errors
+export const KEYSTONE_TX_CANCELED = 'KeystoneError#Tx_canceled';
diff --git a/app/constants/keyringTypes.ts b/app/constants/keyringTypes.ts
new file mode 100644
index 00000000000..2ffc75d2785
--- /dev/null
+++ b/app/constants/keyringTypes.ts
@@ -0,0 +1,3 @@
+export const HD_KEY_TREE = 'HD Key Tree';
+export const SIMPLE_KEY_PAIR = 'Simple Key Pair';
+export const QR_HARDWARE_WALLET_DEVICE = 'QR Hardware Wallet Device';
diff --git a/app/constants/qr.ts b/app/constants/qr.ts
new file mode 100644
index 00000000000..0bf6c81ad29
--- /dev/null
+++ b/app/constants/qr.ts
@@ -0,0 +1,6 @@
+// eslint-disable-next-line import/prefer-default-export
+export const SUPPORTED_UR_TYPE = {
+ CRYPTO_HDKEY: 'crypto-hdkey',
+ CRYPTO_ACCOUNT: 'crypto-account',
+ ETH_SIGNATURE: 'eth-signature',
+};
diff --git a/app/constants/transaction.ts b/app/constants/transaction.ts
index 036e133a457..7dc574ded56 100644
--- a/app/constants/transaction.ts
+++ b/app/constants/transaction.ts
@@ -3,3 +3,6 @@ export const TX_SUBMITTED = 'submitted';
export const TX_SIGNED = 'signed';
export const TX_PENDING = 'pending';
export const TX_CONFIRMED = 'confirmed';
+
+// Values
+export const UINT256_HEX_MAX_VALUE = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
diff --git a/app/constants/urls.ts b/app/constants/urls.ts
new file mode 100644
index 00000000000..5f474f50a7e
--- /dev/null
+++ b/app/constants/urls.ts
@@ -0,0 +1,3 @@
+// Keystone
+export const KEYSTONE_SUPPORT = 'https://keyst.one/mmm';
+export const KEYSTONE_SUPPORT_VIDEO = 'https://keyst.one/mmmvideo';
diff --git a/app/core/Engine.js b/app/core/Engine.js
index f9c91616c6a..9add2d945f7 100644
--- a/app/core/Engine.js
+++ b/app/core/Engine.js
@@ -27,6 +27,7 @@ import {
} from '@metamask/controllers';
import SwapsController, { swapsUtils } from '@metamask/swaps-controller';
import AsyncStorage from '@react-native-community/async-storage';
+import { MetaMaskKeyring as QRHardwareKeyring } from '@keystonehq/metamask-airgapped-keyring';
import Encryptor from './Encryptor';
import { toChecksumAddress } from 'ethereumjs-util';
import Networks, { isMainnetByChainId } from '../util/networks';
@@ -166,6 +167,8 @@ class Engine {
EIP1559APIEndpoint: 'https://gas-api.metaswap.codefi.network/networks//suggestedGasFees',
});
+ const additionalKeyrings = [QRHardwareKeyring];
+
const controllers = [
new KeyringController(
{
@@ -173,8 +176,9 @@ class Engine {
syncIdentities: preferencesController.syncIdentities.bind(preferencesController),
updateIdentities: preferencesController.updateIdentities.bind(preferencesController),
setSelectedAddress: preferencesController.setSelectedAddress.bind(preferencesController),
+ setAccountLabel: preferencesController.setAccountLabel.bind(preferencesController),
},
- { encryptor },
+ { encryptor, keyringTypes: additionalKeyrings },
initialState.KeyringController
),
new AccountTrackerController({
diff --git a/app/core/Vault.js b/app/core/Vault.js
index 06e95eda1ce..cc00dfb1004 100644
--- a/app/core/Vault.js
+++ b/app/core/Vault.js
@@ -1,6 +1,7 @@
import Engine from './Engine';
import Logger from '../util/Logger';
import { syncPrefs, syncAccounts } from '../util/sync';
+import { KeyringTypes } from '@metamask/controllers';
/**
* Returns current vault seed phrase
@@ -27,7 +28,9 @@ export const recreateVaultWithSamePassword = async (password = '', selectedAddre
let importedAccounts = [];
try {
// Get imported accounts
- const simpleKeyrings = KeyringController.state.keyrings.filter((keyring) => keyring.type === 'Simple Key Pair');
+ const simpleKeyrings = KeyringController.state.keyrings.filter(
+ (keyring) => keyring.type === KeyringTypes.simple
+ );
for (let i = 0; i < simpleKeyrings.length; i++) {
const simpleKeyring = simpleKeyrings[i];
const simpleKeyringAccounts = await Promise.all(
@@ -39,6 +42,9 @@ export const recreateVaultWithSamePassword = async (password = '', selectedAddre
Logger.error(e, 'error while trying to get imported accounts on recreate vault');
}
+ const qrKeyring = await KeyringController.getOrAddQRKeyring();
+ const serializedQRKeyring = await qrKeyring.serialize();
+
// Recreate keyring with password given to this method
await KeyringController.createNewVaultAndRestore(password, seedPhrase);
@@ -46,6 +52,8 @@ export const recreateVaultWithSamePassword = async (password = '', selectedAddre
const hdKeyring = KeyringController.state.keyrings[0];
const existingAccountCount = hdKeyring.accounts.length;
+ await KeyringController.restoreQRKeyring(serializedQRKeyring);
+
// Create previous accounts again
for (let i = 0; i < existingAccountCount - 1; i++) {
await KeyringController.addNewAccount();
diff --git a/app/images/connect-qr-hardware.png b/app/images/connect-qr-hardware.png
new file mode 100644
index 00000000000..8e5a252c4c6
Binary files /dev/null and b/app/images/connect-qr-hardware.png differ
diff --git a/app/styles/common.js b/app/styles/common.ts
similarity index 76%
rename from app/styles/common.js
rename to app/styles/common.ts
index a004e834306..61fe9c7db6b 100644
--- a/app/styles/common.js
+++ b/app/styles/common.ts
@@ -2,6 +2,8 @@
* Common styles and variables
*/
+import { TextStyle, ViewStyle } from 'react-native';
+
/**
* Map of color names to HEX values
*/
@@ -9,6 +11,7 @@ export const colors = {
black: '#24292E',
blackTransparent: 'rgba(0, 0, 0, 0.5)',
white: '#FFFFFF',
+ whiteTransparent: 'rgba(255, 255, 255, 0.7)',
yellow: '#FFD33D',
transparent: 'transparent',
shadow: '#6a737d',
@@ -17,7 +20,7 @@ export const colors = {
/**
* Map of reusable base styles
*/
-export const baseStyles = {
+export const baseStyles: Record = {
flexGrow: {
flex: 1,
},
@@ -29,7 +32,7 @@ export const baseStyles = {
/**
* Map of reusable fonts
*/
-export const fontStyles = {
+export const fontStyles: Record = {
normal: {
fontFamily: 'EuclidCircularB-Regular',
fontWeight: '400',
diff --git a/app/util/address/index.js b/app/util/address/index.js
index ee7146e9fb2..e7a08ebb888 100644
--- a/app/util/address/index.js
+++ b/app/util/address/index.js
@@ -1,8 +1,9 @@
-import { toChecksumAddress } from 'ethereumjs-util';
+import { toChecksumAddress, isValidAddress } from 'ethereumjs-util';
import Engine from '../../core/Engine';
import { strings } from '../../../locales/i18n';
import { tlc } from '../general';
import punycode from 'punycode/punycode';
+import { KeyringTypes } from '@metamask/controllers';
/**
* Returns full checksummed address
@@ -14,6 +15,27 @@ export function renderFullAddress(address) {
return address ? toChecksumAddress(address) : strings('transactions.tx_details_not_available');
}
+/**
+ * Method to format the address to a shorter version
+ * @param {String} rawAddress - Full public address
+ * @param {String} type - Format type
+ * @returns {String} Formatted address
+ */
+export const formatAddress = (rawAddress, type) => {
+ let formattedAddress = rawAddress;
+
+ if (isValidAddress(rawAddress)) {
+ if (type && type === 'short') {
+ formattedAddress = renderShortAddress(rawAddress);
+ } else if (type && type === 'mid') {
+ formattedAddress = renderSlightlyLongAddress(rawAddress);
+ } else {
+ formattedAddress = renderFullAddress(rawAddress);
+ }
+ }
+ return formattedAddress.toLowerCase();
+};
+
/**
* Returns short address format
*
@@ -67,6 +89,48 @@ export async function importAccountFromPrivateKey(private_key) {
return KeyringController.importAccountWithStrategy('privateKey', [pkey]);
}
+/**
+ * judge address is QR hardware account or not
+ *
+ * @param {String} address - String corresponding to an address
+ * @returns {Boolean} - Returns a boolean
+ */
+export function isQRHardwareAccount(address) {
+ const { KeyringController } = Engine.context;
+ const { keyrings } = KeyringController.state;
+ const qrKeyrings = keyrings.filter((keyring) => keyring.type === KeyringTypes.qr);
+ let qrAccounts = [];
+ for (const qrKeyring of qrKeyrings) {
+ qrAccounts = qrAccounts.concat(qrKeyring.accounts.map((account) => account.toLowerCase()));
+ }
+ return qrAccounts.includes(address.toLowerCase());
+}
+
+/**
+ * judge address's account type for tracking
+ *
+ * @param {String} address - String corresponding to an address
+ * @returns {String} - Returns address's account type
+ */
+export function getAddressAccountType(address) {
+ const { KeyringController } = Engine.context;
+ const { keyrings } = KeyringController.state;
+ const targetKeyring = keyrings.find((keyring) =>
+ keyring.accounts.map((account) => account.toLowerCase()).includes(address.toLowerCase())
+ );
+ if (targetKeyring) {
+ switch (targetKeyring.type) {
+ case KeyringTypes.qr:
+ return 'QR';
+ case KeyringTypes.simple:
+ return 'Imported';
+ default:
+ return 'MetaMask';
+ }
+ }
+ throw new Error(`The address: ${address} is not imported`);
+}
+
/**
* Validates an ENS name
*
diff --git a/app/util/analyticsV2.js b/app/util/analyticsV2.js
index 812b4961b06..2ab3bc7bd42 100644
--- a/app/util/analyticsV2.js
+++ b/app/util/analyticsV2.js
@@ -107,6 +107,12 @@ export const ANALYTICS_EVENTS_V2 = {
REVEAL_SRP_COMPLETED: generateOpt(`Reveal SRP Completed`),
// KEY MANAGMENT INVESTIGATION
ANDROID_HARDWARE_KEYSTORE: generateOpt('Android Hardware Keystore'),
+ // QR HARDWARE WALLET
+ CONNECT_HARDWARE_WALLET: generateOpt('Clicked Connect Hardware Wallet'),
+ CONTINUE_QR_HARDWARE_WALLET: generateOpt('Clicked Continue QR Hardware Wallet'),
+ CONNECT_HARDWARE_WALLET_SUCCESS: generateOpt('Connected Account with hardware wallet'),
+ QR_HARDWARE_TRANSACTION_CANCELED: generateOpt('User canceled QR hardware transaction'),
+ HARDWARE_WALLET_ERROR: generateOpt('Hardware wallet error'),
};
/**
diff --git a/attribution.txt b/attribution.txt
index a217a16a3d5..d288a0ae98c 100644
--- a/attribution.txt
+++ b/attribution.txt
@@ -4056,7 +4056,7 @@ SOFTWARE.
******************************
@keystonehq/base-eth-keyring
-0.3.2
+0.4.0
license: MIT
authors: aaronisme
@@ -4270,19 +4270,26 @@ authors: aaronisme
******************************
@keystonehq/bc-ur-registry-eth
-0.7.7 <>
+0.9.0 <>
license: ISC
authors: soralit
******************************
@keystonehq/metamask-airgapped-keyring
-0.2.2
+0.3.0
license: MIT
authors: aaronisme
******************************
+@keystonehq/ur-decoder
+0.3.0 <>
+license: ISC
+authors: soralit
+
+******************************
+
@metamask/contract-metadata
1.30.0
ISC License
@@ -5736,27 +5743,27 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@types/hoist-non-react-statics
3.3.1
- MIT License
-
- Copyright (c) Microsoft Corporation. All rights reserved.
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE
+ MIT License
+
+ Copyright (c) Microsoft Corporation. All rights reserved.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE
******************************
@@ -5790,27 +5797,27 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@types/istanbul-lib-report
3.0.0
- MIT License
-
- Copyright (c) Microsoft Corporation. All rights reserved.
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE
+ MIT License
+
+ Copyright (c) Microsoft Corporation. All rights reserved.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE
******************************
@@ -8188,30 +8195,30 @@ SOFTWARE.
big-integer
1.6.51
-This is free and unencumbered software released into the public domain.
-
-Anyone is free to copy, modify, publish, use, compile, sell, or
-distribute this software, either in source code form or as a compiled
-binary, for any purpose, commercial or non-commercial, and by any
-means.
-
-In jurisdictions that recognize copyright laws, the author or authors
-of this software dedicate any and all copyright interest in the
-software to the public domain. We make this dedication for the benefit
-of the public at large and to the detriment of our heirs and
-successors. We intend this dedication to be an overt act of
-relinquishment in perpetuity of all present and future rights to this
-software under copyright law.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
-ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
-
-For more information, please refer to
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to
******************************
@@ -10810,13 +10817,13 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
color-name
1.1.4
-The MIT License (MIT)
-Copyright (c) 2015 Dmitry Ivanov
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
+The MIT License (MIT)
+Copyright (c) 2015 Dmitry Ivanov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
@@ -11031,30 +11038,30 @@ SOFTWARE.
component-emitter
1.3.0
-(The MIT License)
-
-Copyright (c) 2014 Component contributors
-
-Permission is hereby granted, free of charge, to any person
-obtaining a copy of this software and associated documentation
-files (the "Software"), to deal in the Software without
-restriction, including without limitation the rights to use,
-copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the
-Software is furnished to do so, subject to the following
-conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
-OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
+(The MIT License)
+
+Copyright (c) 2014 Component contributors
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
******************************
@@ -11251,20 +11258,20 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
content-hash
2.5.2
-ISC License
-
-Copyright (c) 2018, Pierre-Louis Despaigne
-
-Permission to use, copy, modify, and/or distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ISC License
+
+Copyright (c) 2018, Pierre-Louis Despaigne
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
******************************
@@ -13894,27 +13901,27 @@ SOFTWARE.
ethereum-bloom-filters
1.0.10
-MIT License
-
-Copyright (c) 2019 Josh Stevens
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+MIT License
+
+Copyright (c) 2019 Josh Stevens
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
******************************
@@ -20657,201 +20664,201 @@ authors: undefined
json-schema
0.4.0
-Dojo is available under *either* the terms of the BSD 3-Clause "New" License *or* the
-Academic Free License version 2.1. As a recipient of Dojo, you may choose which
-license to receive this code under (except as noted in per-module LICENSE
-files). Some modules may not be the copyright of the Dojo Foundation. These
-modules contain explicit declarations of copyright in both the LICENSE files in
-the directories in which they reside and in the code itself. No external
-contributions are allowed under licenses which are fundamentally incompatible
-with the AFL-2.1 OR and BSD-3-Clause licenses that Dojo is distributed under.
-
-The text of the AFL-2.1 and BSD-3-Clause licenses is reproduced below.
-
--------------------------------------------------------------------------------
-BSD 3-Clause "New" License:
-**********************
-
-Copyright (c) 2005-2015, The Dojo Foundation
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
- * Redistributions of source code must retain the above copyright notice, this
- list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above copyright notice,
- this list of conditions and the following disclaimer in the documentation
- and/or other materials provided with the distribution.
- * Neither the name of the Dojo Foundation nor the names of its contributors
- may be used to endorse or promote products derived from this software
- without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
--------------------------------------------------------------------------------
-The Academic Free License, v. 2.1:
-**********************************
-
-This Academic Free License (the "License") applies to any original work of
-authorship (the "Original Work") whose owner (the "Licensor") has placed the
-following notice immediately following the copyright notice for the Original
-Work:
-
-Licensed under the Academic Free License version 2.1
-
-1) Grant of Copyright License. Licensor hereby grants You a world-wide,
-royalty-free, non-exclusive, perpetual, sublicenseable license to do the
-following:
-
-a) to reproduce the Original Work in copies;
-
-b) to prepare derivative works ("Derivative Works") based upon the Original
-Work;
-
-c) to distribute copies of the Original Work and Derivative Works to the
-public;
-
-d) to perform the Original Work publicly; and
-
-e) to display the Original Work publicly.
-
-2) Grant of Patent License. Licensor hereby grants You a world-wide,
-royalty-free, non-exclusive, perpetual, sublicenseable license, under patent
-claims owned or controlled by the Licensor that are embodied in the Original
-Work as furnished by the Licensor, to make, use, sell and offer for sale the
-Original Work and Derivative Works.
-
-3) Grant of Source Code License. The term "Source Code" means the preferred
-form of the Original Work for making modifications to it and all available
-documentation describing how to modify the Original Work. Licensor hereby
-agrees to provide a machine-readable copy of the Source Code of the Original
-Work along with each copy of the Original Work that Licensor distributes.
-Licensor reserves the right to satisfy this obligation by placing a
-machine-readable copy of the Source Code in an information repository
-reasonably calculated to permit inexpensive and convenient access by You for as
-long as Licensor continues to distribute the Original Work, and by publishing
-the address of that information repository in a notice immediately following
-the copyright notice that applies to the Original Work.
-
-4) Exclusions From License Grant. Neither the names of Licensor, nor the names
-of any contributors to the Original Work, nor any of their trademarks or
-service marks, may be used to endorse or promote products derived from this
-Original Work without express prior written permission of the Licensor. Nothing
-in this License shall be deemed to grant any rights to trademarks, copyrights,
-patents, trade secrets or any other intellectual property of Licensor except as
-expressly stated herein. No patent license is granted to make, use, sell or
-offer to sell embodiments of any patent claims other than the licensed claims
-defined in Section 2. No right is granted to the trademarks of Licensor even if
-such marks are included in the Original Work. Nothing in this License shall be
-interpreted to prohibit Licensor from licensing under different terms from this
-License any Original Work that Licensor otherwise would have a right to
-license.
-
-5) This section intentionally omitted.
-
-6) Attribution Rights. You must retain, in the Source Code of any Derivative
-Works that You create, all copyright, patent or trademark notices from the
-Source Code of the Original Work, as well as any notices of licensing and any
-descriptive text identified therein as an "Attribution Notice." You must cause
-the Source Code for any Derivative Works that You create to carry a prominent
-Attribution Notice reasonably calculated to inform recipients that You have
-modified the Original Work.
-
-7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that
-the copyright in and to the Original Work and the patent rights granted herein
-by Licensor are owned by the Licensor or are sublicensed to You under the terms
-of this License with the permission of the contributor(s) of those copyrights
-and patent rights. Except as expressly stated in the immediately proceeding
-sentence, the Original Work is provided under this License on an "AS IS" BASIS
-and WITHOUT WARRANTY, either express or implied, including, without limitation,
-the warranties of NON-INFRINGEMENT, MERCHANTABILITY or FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU.
-This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No
-license to Original Work is granted hereunder except under this disclaimer.
-
-8) Limitation of Liability. Under no circumstances and under no legal theory,
-whether in tort (including negligence), contract, or otherwise, shall the
-Licensor be liable to any person for any direct, indirect, special, incidental,
-or consequential damages of any character arising as a result of this License
-or the use of the Original Work including, without limitation, damages for loss
-of goodwill, work stoppage, computer failure or malfunction, or any and all
-other commercial damages or losses. This limitation of liability shall not
-apply to liability for death or personal injury resulting from Licensor's
-negligence to the extent applicable law prohibits such limitation. Some
-jurisdictions do not allow the exclusion or limitation of incidental or
-consequential damages, so this exclusion and limitation may not apply to You.
-
-9) Acceptance and Termination. If You distribute copies of the Original Work or
-a Derivative Work, You must make a reasonable effort under the circumstances to
-obtain the express assent of recipients to the terms of this License. Nothing
-else but this License (or another written agreement between Licensor and You)
-grants You permission to create Derivative Works based upon the Original Work
-or to exercise any of the rights granted in Section 1 herein, and any attempt
-to do so except under the terms of this License (or another written agreement
-between Licensor and You) is expressly prohibited by U.S. copyright law, the
-equivalent laws of other countries, and by international treaty. Therefore, by
-exercising any of the rights granted to You in Section 1 herein, You indicate
-Your acceptance of this License and all of its terms and conditions.
-
-10) Termination for Patent Action. This License shall terminate automatically
-and You may no longer exercise any of the rights granted to You by this License
-as of the date You commence an action, including a cross-claim or counterclaim,
-against Licensor or any licensee alleging that the Original Work infringes a
-patent. This termination provision shall not apply for an action alleging
-patent infringement by combinations of the Original Work with other software or
-hardware.
-
-11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this
-License may be brought only in the courts of a jurisdiction wherein the
-Licensor resides or in which Licensor conducts its primary business, and under
-the laws of that jurisdiction excluding its conflict-of-law provisions. The
-application of the United Nations Convention on Contracts for the International
-Sale of Goods is expressly excluded. Any use of the Original Work outside the
-scope of this License or after its termination shall be subject to the
-requirements and penalties of the U.S. Copyright Act, 17 U.S.C. § 101 et
-seq., the equivalent laws of other countries, and international treaty. This
-section shall survive the termination of this License.
-
-12) Attorneys Fees. In any action to enforce the terms of this License or
-seeking damages relating thereto, the prevailing party shall be entitled to
-recover its costs and expenses, including, without limitation, reasonable
-attorneys' fees and costs incurred in connection with such action, including
-any appeal of such action. This section shall survive the termination of this
-License.
-
-13) Miscellaneous. This License represents the complete agreement concerning
-the subject matter hereof. If any provision of this License is held to be
-unenforceable, such provision shall be reformed only to the extent necessary to
-make it enforceable.
-
-14) Definition of "You" in This License. "You" throughout this License, whether
-in upper or lower case, means an individual or a legal entity exercising rights
-under, and complying with all of the terms of, this License. For legal
-entities, "You" includes any entity that controls, is controlled by, or is
-under common control with you. For purposes of this definition, "control" means
-(i) the power, direct or indirect, to cause the direction or management of such
-entity, whether by contract or otherwise, or (ii) ownership of fifty percent
-(50%) or more of the outstanding shares, or (iii) beneficial ownership of such
-entity.
-
-15) Right to Use. You may use the Original Work in all ways not otherwise
-restricted or conditioned by this License or by law, and Licensor promises not
-to interfere with or be responsible for such uses by You.
-
-This license is Copyright (C) 2003-2004 Lawrence E. Rosen. All rights reserved.
-Permission is hereby granted to copy and distribute this license without
-modification. This license may not be modified without the express written
-permission of its copyright owner.
+Dojo is available under *either* the terms of the BSD 3-Clause "New" License *or* the
+Academic Free License version 2.1. As a recipient of Dojo, you may choose which
+license to receive this code under (except as noted in per-module LICENSE
+files). Some modules may not be the copyright of the Dojo Foundation. These
+modules contain explicit declarations of copyright in both the LICENSE files in
+the directories in which they reside and in the code itself. No external
+contributions are allowed under licenses which are fundamentally incompatible
+with the AFL-2.1 OR and BSD-3-Clause licenses that Dojo is distributed under.
+
+The text of the AFL-2.1 and BSD-3-Clause licenses is reproduced below.
+
+-------------------------------------------------------------------------------
+BSD 3-Clause "New" License:
+**********************
+
+Copyright (c) 2005-2015, The Dojo Foundation
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of the Dojo Foundation nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+-------------------------------------------------------------------------------
+The Academic Free License, v. 2.1:
+**********************************
+
+This Academic Free License (the "License") applies to any original work of
+authorship (the "Original Work") whose owner (the "Licensor") has placed the
+following notice immediately following the copyright notice for the Original
+Work:
+
+Licensed under the Academic Free License version 2.1
+
+1) Grant of Copyright License. Licensor hereby grants You a world-wide,
+royalty-free, non-exclusive, perpetual, sublicenseable license to do the
+following:
+
+a) to reproduce the Original Work in copies;
+
+b) to prepare derivative works ("Derivative Works") based upon the Original
+Work;
+
+c) to distribute copies of the Original Work and Derivative Works to the
+public;
+
+d) to perform the Original Work publicly; and
+
+e) to display the Original Work publicly.
+
+2) Grant of Patent License. Licensor hereby grants You a world-wide,
+royalty-free, non-exclusive, perpetual, sublicenseable license, under patent
+claims owned or controlled by the Licensor that are embodied in the Original
+Work as furnished by the Licensor, to make, use, sell and offer for sale the
+Original Work and Derivative Works.
+
+3) Grant of Source Code License. The term "Source Code" means the preferred
+form of the Original Work for making modifications to it and all available
+documentation describing how to modify the Original Work. Licensor hereby
+agrees to provide a machine-readable copy of the Source Code of the Original
+Work along with each copy of the Original Work that Licensor distributes.
+Licensor reserves the right to satisfy this obligation by placing a
+machine-readable copy of the Source Code in an information repository
+reasonably calculated to permit inexpensive and convenient access by You for as
+long as Licensor continues to distribute the Original Work, and by publishing
+the address of that information repository in a notice immediately following
+the copyright notice that applies to the Original Work.
+
+4) Exclusions From License Grant. Neither the names of Licensor, nor the names
+of any contributors to the Original Work, nor any of their trademarks or
+service marks, may be used to endorse or promote products derived from this
+Original Work without express prior written permission of the Licensor. Nothing
+in this License shall be deemed to grant any rights to trademarks, copyrights,
+patents, trade secrets or any other intellectual property of Licensor except as
+expressly stated herein. No patent license is granted to make, use, sell or
+offer to sell embodiments of any patent claims other than the licensed claims
+defined in Section 2. No right is granted to the trademarks of Licensor even if
+such marks are included in the Original Work. Nothing in this License shall be
+interpreted to prohibit Licensor from licensing under different terms from this
+License any Original Work that Licensor otherwise would have a right to
+license.
+
+5) This section intentionally omitted.
+
+6) Attribution Rights. You must retain, in the Source Code of any Derivative
+Works that You create, all copyright, patent or trademark notices from the
+Source Code of the Original Work, as well as any notices of licensing and any
+descriptive text identified therein as an "Attribution Notice." You must cause
+the Source Code for any Derivative Works that You create to carry a prominent
+Attribution Notice reasonably calculated to inform recipients that You have
+modified the Original Work.
+
+7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that
+the copyright in and to the Original Work and the patent rights granted herein
+by Licensor are owned by the Licensor or are sublicensed to You under the terms
+of this License with the permission of the contributor(s) of those copyrights
+and patent rights. Except as expressly stated in the immediately proceeding
+sentence, the Original Work is provided under this License on an "AS IS" BASIS
+and WITHOUT WARRANTY, either express or implied, including, without limitation,
+the warranties of NON-INFRINGEMENT, MERCHANTABILITY or FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU.
+This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No
+license to Original Work is granted hereunder except under this disclaimer.
+
+8) Limitation of Liability. Under no circumstances and under no legal theory,
+whether in tort (including negligence), contract, or otherwise, shall the
+Licensor be liable to any person for any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License
+or the use of the Original Work including, without limitation, damages for loss
+of goodwill, work stoppage, computer failure or malfunction, or any and all
+other commercial damages or losses. This limitation of liability shall not
+apply to liability for death or personal injury resulting from Licensor's
+negligence to the extent applicable law prohibits such limitation. Some
+jurisdictions do not allow the exclusion or limitation of incidental or
+consequential damages, so this exclusion and limitation may not apply to You.
+
+9) Acceptance and Termination. If You distribute copies of the Original Work or
+a Derivative Work, You must make a reasonable effort under the circumstances to
+obtain the express assent of recipients to the terms of this License. Nothing
+else but this License (or another written agreement between Licensor and You)
+grants You permission to create Derivative Works based upon the Original Work
+or to exercise any of the rights granted in Section 1 herein, and any attempt
+to do so except under the terms of this License (or another written agreement
+between Licensor and You) is expressly prohibited by U.S. copyright law, the
+equivalent laws of other countries, and by international treaty. Therefore, by
+exercising any of the rights granted to You in Section 1 herein, You indicate
+Your acceptance of this License and all of its terms and conditions.
+
+10) Termination for Patent Action. This License shall terminate automatically
+and You may no longer exercise any of the rights granted to You by this License
+as of the date You commence an action, including a cross-claim or counterclaim,
+against Licensor or any licensee alleging that the Original Work infringes a
+patent. This termination provision shall not apply for an action alleging
+patent infringement by combinations of the Original Work with other software or
+hardware.
+
+11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this
+License may be brought only in the courts of a jurisdiction wherein the
+Licensor resides or in which Licensor conducts its primary business, and under
+the laws of that jurisdiction excluding its conflict-of-law provisions. The
+application of the United Nations Convention on Contracts for the International
+Sale of Goods is expressly excluded. Any use of the Original Work outside the
+scope of this License or after its termination shall be subject to the
+requirements and penalties of the U.S. Copyright Act, 17 U.S.C. § 101 et
+seq., the equivalent laws of other countries, and international treaty. This
+section shall survive the termination of this License.
+
+12) Attorneys Fees. In any action to enforce the terms of this License or
+seeking damages relating thereto, the prevailing party shall be entitled to
+recover its costs and expenses, including, without limitation, reasonable
+attorneys' fees and costs incurred in connection with such action, including
+any appeal of such action. This section shall survive the termination of this
+License.
+
+13) Miscellaneous. This License represents the complete agreement concerning
+the subject matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent necessary to
+make it enforceable.
+
+14) Definition of "You" in This License. "You" throughout this License, whether
+in upper or lower case, means an individual or a legal entity exercising rights
+under, and complying with all of the terms of, this License. For legal
+entities, "You" includes any entity that controls, is controlled by, or is
+under common control with you. For purposes of this definition, "control" means
+(i) the power, direct or indirect, to cause the direction or management of such
+entity, whether by contract or otherwise, or (ii) ownership of fifty percent
+(50%) or more of the outstanding shares, or (iii) beneficial ownership of such
+entity.
+
+15) Right to Use. You may use the Original Work in all ways not otherwise
+restricted or conditioned by this License or by law, and Licensor promises not
+to interfere with or be responsible for such uses by You.
+
+This license is Copyright (C) 2003-2004 Lawrence E. Rosen. All rights reserved.
+Permission is hereby granted to copy and distribute this license without
+modification. This license may not be modified without the express written
+permission of its copyright owner.
******************************
@@ -24401,31 +24408,31 @@ THE SOFTWARE.
normalize-svg-path
1.1.0
-
-The MIT License
-
-Copyright © 2008-2013 Dmitry Baranovskiy (http://raphaeljs.com)
-Copyright © 2008-2013 Sencha Labs (http://sencha.com)
-Copyright © 2013 Jake Rosoman
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-'Software'), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+The MIT License
+
+Copyright © 2008-2013 Dmitry Baranovskiy (http://raphaeljs.com)
+Copyright © 2008-2013 Sencha Labs (http://sencha.com)
+Copyright © 2013 Jake Rosoman
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
@@ -25677,24 +25684,24 @@ THE SOFTWARE.
path
0.12.7
-Copyright Joyent, Inc. and other Node contributors. All rights reserved.
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to
-deal in the Software without restriction, including without limitation the
-rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-sell copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-IN THE SOFTWARE.
+Copyright Joyent, Inc. and other Node contributors. All rights reserved.
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
******************************
@@ -26233,26 +26240,26 @@ SOFTWARE.
pretty-hrtime
1.0.3
-Copyright (c) 2013 [Richardson & Sons, LLC](http://richardsonandsons.com/)
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+Copyright (c) 2013 [Richardson & Sons, LLC](http://richardsonandsons.com/)
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
@@ -28753,27 +28760,27 @@ SOFTWARE.
react-redux
7.2.4
-The MIT License (MIT)
-
-Copyright (c) 2015-present Dan Abramov
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+The MIT License (MIT)
+
+Copyright (c) 2015-present Dan Abramov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
******************************
@@ -33709,17 +33716,17 @@ SOFTWARE.
tslib
2.3.1
-Copyright (c) Microsoft Corporation.
-
-Permission to use, copy, modify, and/or distribute this software for any
-purpose with or without fee is hereby granted.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
-OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+Copyright (c) Microsoft Corporation.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
******************************
@@ -34003,6 +34010,67 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+******************************
+
+typescript
+4.6.2
+Apache License
+
+Version 2.0, January 2004
+
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of this License; and
+
+You must cause any modified files to carry prominent notices stating that You changed the files; and
+
+You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
+
+If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+
******************************
ua-parser-js
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 618d71b0b76..b6fd4762bc8 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -410,8 +410,8 @@ PODS:
- React-Core
- RNCAsyncStorage (1.12.1):
- React-Core
- - RNCCheckbox (0.4.2):
- - React
+ - RNCCheckbox (0.5.12):
+ - React-Core
- RNCClipboard (1.8.4):
- React-Core
- RNCMaskedView (0.2.6):
@@ -816,7 +816,7 @@ SPEC CHECKSUMS:
ReactNativePayments: a4e3ac915256a4e759c8a04338b558494a63a0f5
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
RNCAsyncStorage: b03032fdbdb725bea0bd9e5ec5a7272865ae7398
- RNCCheckbox: 357578d3b42652c78ee9a1bb9bcfc3195af6e161
+ RNCCheckbox: ed1b4ca295475b41e7251ebae046360a703b6eb5
RNCClipboard: ddd4d291537f1667209c9c405aaa4307297e252e
RNCMaskedView: c298b644a10c0c142055b3ae24d83879ecb13ccd
RNCPicker: cb57c823d5ce8d2d0b5dfb45ad97b737260dc59e
diff --git a/locales/languages/en.json b/locales/languages/en.json
index 87a596578d8..4906482e4bc 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -262,6 +262,7 @@
"send_to": "Send to",
"amount": "Amount",
"confirm": "Confirm",
+ "sign": "Sign",
"network_not_found_title": "Network not found",
"network_not_found_description": "Network with chain id {{chain_id}} not found in your wallet. Please add the network first.",
"network_missing_id": "Missing chain id."
@@ -404,12 +405,32 @@
"accounts": {
"create_new_account": "Create New Account",
"import_account": "Import an Account",
+ "connect_hardware": "Connect Hardware Wallet",
"imported": "IMPORTED",
"remove_account_title": "Account removal",
"remove_account_message": "Do you really want to remove this account?",
"no": "No",
"yes_remove_it": "Yes, remove it"
},
+ "connect_qr_hardware": {
+ "title": "Connect a QR-based hardware wallet",
+ "description1": "Connect an airgapped hardware wallet that communicates through QR-codes.",
+ "description2": "How it works?",
+ "description3": "Officially supported airgapped hardware wallets include:",
+ "description4": "Keystone (tutorial)",
+ "description5": "1. Unlock your Keystone",
+ "description6": "2. Tap the ··· Menu, then go to Sync >",
+ "button_continue": "Continue",
+ "hint_text": "Scan your Keystone wallet to ",
+ "purpose_connect": "connect",
+ "purpose_sign": "confirm the transaction",
+ "select_accounts": "Select an Account",
+ "prev": "PREV",
+ "next": "NEXT",
+ "unlock": "Unlock",
+ "forget": "Forget this device",
+ "please_wait": "Please wait"
+ },
"app_settings": {
"title": "Settings",
"current_conversion": "Base Currency",
@@ -611,6 +632,7 @@
"private_key_copied_time": "(stored for 1 minute)",
"warning_incorrect_password": "Incorrect password",
"unknown_error": "Couldn't unlock your account. Please try again.",
+ "hardware_error": "This is a hardware wallet account, you cannot export your private key.",
"seed_warning": "This is your wallet's 12 word phrase. This phrase can be used to take control of all of your current and future accounts, including the ability to send away any of their funds. Keep this phrase stored safely, DO NOT share it with anyone.",
"text": "TEXT",
"qr_code": "QR CODE",
@@ -663,6 +685,7 @@
"confirm": "Confirm"
},
"transaction": {
+ "hardware": "Hardware",
"alert": "ALERT",
"amount": "Amount",
"next": "Next",
@@ -673,6 +696,7 @@
"cancel": "Cancel",
"save": "Save",
"speedup": "Speed up",
+ "sign_with_keystone": "Sign with Keystone",
"from": "From",
"gas_fee": "Gas fee",
"gas_fee_fast": "FAST",
@@ -741,6 +765,7 @@
"token_id": "Token ID",
"not_enough_for_gas": "You have 0 {{ticker}} in your account to pay for transaction fees. Buy some {{ticker}} or deposit from another account.",
"send": "Send",
+ "confirm_with_qr_hardware": "Confirm with Keystone",
"confirmed": "Confirmed",
"pending": "Pending",
"submitted": "Submitted",
@@ -764,7 +789,13 @@
"similar_to": "is similar to",
"contains_zero_width": "contains zero width character",
"dapp_suggested_gas": "This gas fee has been suggested by %{origin}. It’s using legacy gas estimation which may be inaccurate. However, editing this gas fee may cause a problem with your transaction. Please reach out to %{origin} if you have questions.",
- "dapp_suggested_eip1559_gas": "This gas fee has been suggested by %{origin}. Overriding this may cause a problem with your transaction. Please reach out to %{origin} if you have questions."
+ "dapp_suggested_eip1559_gas": "This gas fee has been suggested by %{origin}. Overriding this may cause a problem with your transaction. Please reach out to %{origin} if you have questions.",
+ "unknown_qr_code": "Invalid QR code. Please retry.",
+ "invalid_qr_code_sync": "Invalid QR code. Please scan the sync QR code of the hardware wallet.",
+ "no_camera_permission": "Camera not authorized. Please give permission and retry",
+ "invalid_qr_code_sign": "Invalid QR code. Please check your hardware and retry.",
+ "no_camera_permission_android": "You need to grant MetaMask access to your camera to proceed. You may also need to change your system settings.",
+ "mismatched_qr_request_id": "Incongruent transaction data. Please use your Keystone to sign the QR code below and tap 'Get Signature'."
},
"custom_gas": {
"total": "Total",
@@ -922,7 +953,12 @@
"from_device_label": "from this device",
"import_wallet_row": "Account added to this device",
"import_wallet_label": "Account Added",
- "import_wallet_tip": "All future transactions made from this device will include a label \"from this device\" next to the timestamp. For transactions dated before adding the account, this history will not indicate which outgoing transactions originated from this device."
+ "import_wallet_tip": "All future transactions made from this device will include a label \"from this device\" next to the timestamp. For transactions dated before adding the account, this history will not indicate which outgoing transactions originated from this device.",
+ "sign_title_scan": "Scan ",
+ "sign_title_device": "with your Keystone",
+ "sign_description_1": "After you have signed with Keystone,",
+ "sign_description_2": "tap on Get Signature",
+ "sign_get_signature": "Get Signature"
},
"address_book": {
"recents": "Recents",
diff --git a/locales/languages/zh-cn.json b/locales/languages/zh-cn.json
index 5cea4256751..2a5d2ea0115 100644
--- a/locales/languages/zh-cn.json
+++ b/locales/languages/zh-cn.json
@@ -352,12 +352,30 @@
"accounts": {
"create_new_account": "创建新账户",
"import_account": "导入账户",
+ "connect_hardware": "连接硬件钱包",
"imported": "已导入",
"remove_account_title": "删除账户",
"remove_account_message": "是否确实想要删除此账户?",
"no": "否",
"yes_remove_it": "是,删除它"
},
+ "connect_qr_hardware": {
+ "title": "Connect a QR-based hardware wallet",
+ "description1": "Connect an airgapped hardware wallet that communicates through QR-codes.",
+ "description2": "How it works?",
+ "description3": "Officially supported airgapped hardware wallets include:",
+ "description4": "Keystone (tutorial)",
+ "button_continue": "Continue",
+ "hint_text": "Scan your Keystone wallet to ",
+ "purpose_connect": "connect",
+ "purpose_sign": "confirm the transaction",
+ "select_accounts": "Select an Account",
+ "prev": "PREV",
+ "next": "NEXT",
+ "unlock": "Unlock",
+ "forget": "Forget this device",
+ "please_wait": "Please wait"
+ },
"app_settings": {
"title": "设置",
"current_conversion": "基础货币",
@@ -553,6 +571,7 @@
"confirm": "确认"
},
"transaction": {
+ "hardware": "硬件",
"alert": "警报",
"amount": "数额",
"next": "下一步",
@@ -627,7 +646,12 @@
"tokenContractAddressWarning_2": "代币合约地址",
"tokenContractAddressWarning_3": ".如果将代币发送到此地址,您将失去代币。",
"smartContractAddressWarning": "此地址是智能合约地址。请确保您了解此地址的用途,否则您将面临资金损失风险。",
- "continueError": "我了解风险,继续"
+ "continueError": "我了解风险,继续",
+ "unknown_qr_code": "非法二维码,请重试",
+ "invalid_qr_code_sync": "非法二维码,请扫描硬件钱包的同步二维码。",
+ "no_camera_permission": "您未开启摄像头权限,请在设置中给予权限后重试",
+ "invalid_qr_code_sign": "非法二维码. 请检查您的硬件钱包并重试",
+ "mismatched_qr_request_id": "扫描的签名二维码不属于当前交易,请检查交易详情后重试。"
},
"custom_gas": {
"total": "总计",
diff --git a/package.json b/package.json
index 33739636955..da2acd1e22d 100644
--- a/package.json
+++ b/package.json
@@ -102,15 +102,19 @@
},
"dependencies": {
"@exodus/react-native-payments": "git+https://github.com/MetaMask/react-native-payments.git#dbc8cbbed570892d2fea5e3d183bf243e062c1e5",
+ "@keystonehq/bc-ur-registry-eth": "^0.7.7",
+ "@keystonehq/metamask-airgapped-keyring": "^0.3.0",
+ "@keystonehq/ur-decoder": "^0.3.0",
"@metamask/contract-metadata": "^1.30.0",
- "@metamask/controllers": "27.0.0",
+ "@metamask/controllers": "27.1.1",
"@metamask/design-tokens": "^1.5.1",
"@metamask/etherscan-link": "^2.0.0",
"@metamask/swaps-controller": "^6.6.0",
+ "@ngraveio/bc-ur": "^1.1.6",
"@react-native-clipboard/clipboard": "^1.8.4",
"@react-native-community/async-storage": "1.12.1",
"@react-native-community/blur": "^3.6.0",
- "@react-native-community/checkbox": "^0.4.2",
+ "@react-native-community/checkbox": "^0.5.12",
"@react-native-community/cookies": "^5.0.1",
"@react-native-community/netinfo": "6.0.0",
"@react-native-community/viewpager": "3.3.1",
diff --git a/yarn.lock b/yarn.lock
index c6efb414a97..ed8d45e12e7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,6 +2,11 @@
# yarn lockfile v1
+"@apocentre/alias-sampling@^0.5.3":
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/@apocentre/alias-sampling/-/alias-sampling-0.5.3.tgz#897ff181b48ad7b2bcb4ecf29400214888244f08"
+ integrity sha512-7UDWIIF9hIeJqfKXkNIzkVandlwLf1FWTSdrb9iXvOP8oF544JRXQjCbiTmCv2c9n44n/FIWtehhBfNuAx2CZA==
+
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.14.5":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb"
@@ -1231,6 +1236,14 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
+"@ethereumjs/common@^2.0.0", "@ethereumjs/common@^2.5.0":
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.6.0.tgz#feb96fb154da41ee2cc2c5df667621a440f36348"
+ integrity sha512-Cq2qS0FTu6O2VU1sgg+WyU9Ps0M6j/BEMHN+hRaECXCV/r0aI78u4N6p52QW/BDVhwWZpCdrvG8X7NJdzlpNUA==
+ dependencies:
+ crc-32 "^1.2.0"
+ ethereumjs-util "^7.1.3"
+
"@ethereumjs/common@^2.3.1", "@ethereumjs/common@^2.4.0":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.4.0.tgz#2d67f6e6ba22246c5c89104e6b9a119fb3039766"
@@ -1239,7 +1252,7 @@
crc-32 "^1.2.0"
ethereumjs-util "^7.1.0"
-"@ethereumjs/common@^2.5.0", "@ethereumjs/common@^2.6.3":
+"@ethereumjs/common@^2.6.3":
version "2.6.3"
resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.6.3.tgz#39ddece7300b336276bad6c02f6a9f1a082caa05"
integrity sha512-mQwPucDL7FDYIg9XQ8DL31CnIYZwGhU5hyOO5E+BMmT71G0+RHvIT5rIkLBirJEKxV6+Rcf9aEIY0kXInxUWpQ==
@@ -1247,6 +1260,14 @@
crc-32 "^1.2.0"
ethereumjs-util "^7.1.4"
+"@ethereumjs/tx@3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.0.0.tgz#8dfd91ed6e91e63996e37b3ddc340821ebd48c81"
+ integrity sha512-H9tfy6qgYxPXvt1TSObfVmVjlF43OoQqoPQ3PJsG2JiuqaMHj5ettV1pGFEC3FamENDBkl6vD6niQEvIlXv/VQ==
+ dependencies:
+ "@ethereumjs/common" "^2.0.0"
+ ethereumjs-util "^7.0.7"
+
"@ethereumjs/tx@^3.2.1", "@ethereumjs/tx@^3.3.0":
version "3.3.0"
resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.3.0.tgz#14ed1b7fa0f28e1cd61e3ecbdab824205f6a4378"
@@ -2135,6 +2156,109 @@
"@types/yargs" "^16.0.0"
chalk "^4.0.0"
+"@keystonehq/base-eth-keyring@^0.3.2":
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/@keystonehq/base-eth-keyring/-/base-eth-keyring-0.3.2.tgz#71efe1495d4931fab5fd0016c8722fe5d9657da9"
+ integrity sha512-y/kv8XNRSzqcSl7fvZklBKr4MVir1OJHzqM5vC+3yvzkh3mVtN8vUjAAxHtmVVVyM8rsdqK7aYZ7paexYdpnag==
+ dependencies:
+ "@ethereumjs/tx" "3.0.0"
+ "@keystonehq/bc-ur-registry-eth" "^0.7.7"
+ ethereumjs-util "^7.0.8"
+ hdkey "^2.0.1"
+ uuid "^8.3.2"
+
+"@keystonehq/base-eth-keyring@^0.4.0":
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/@keystonehq/base-eth-keyring/-/base-eth-keyring-0.4.0.tgz#7667d2b6e38fc90553ce934c0c60c89329315b92"
+ integrity sha512-CDlRNGdrHDHtBS0pAdrsjNNbyi7tn7mGrwmgiGQ6F8rhYXDZ/TcvYV1AXlzCe0eFyjPdMGdl+PgZRwBpVRtpQQ==
+ dependencies:
+ "@ethereumjs/tx" "3.0.0"
+ "@keystonehq/bc-ur-registry-eth" "^0.9.0"
+ ethereumjs-util "^7.0.8"
+ hdkey "^2.0.1"
+ uuid "^8.3.2"
+
+"@keystonehq/bc-ur-registry-eth@^0.6.12":
+ version "0.6.14"
+ resolved "https://registry.yarnpkg.com/@keystonehq/bc-ur-registry-eth/-/bc-ur-registry-eth-0.6.14.tgz#4b8c34d653278524eb574a2879910072ee621ae0"
+ integrity sha512-Zr0VAUJuzz5zfH2263AucdWPUYuclpd93Pmi/VzbML72sQLv8l83kQWmQpI+7639uV5dHcOj6JnD8FhCPYPRFQ==
+ dependencies:
+ "@keystonehq/bc-ur-registry" "^0.4.4"
+ ethereumjs-util "^7.0.8"
+ hdkey "^2.0.1"
+ uuid "^8.3.2"
+
+"@keystonehq/bc-ur-registry-eth@^0.7.7":
+ version "0.7.7"
+ resolved "https://registry.yarnpkg.com/@keystonehq/bc-ur-registry-eth/-/bc-ur-registry-eth-0.7.7.tgz#45267510900049050ef860a1e78fde9087b75972"
+ integrity sha512-2gZf18ogSCLjsn3PxGqwiOMz/G11v7byRnTLv5/wNJGzCqkMf86OKaVbUgsVokq0B5KAZtpCFQxj96rrFhPRiQ==
+ dependencies:
+ "@keystonehq/bc-ur-registry" "^0.4.4"
+ ethereumjs-util "^7.0.8"
+ hdkey "^2.0.1"
+ uuid "^8.3.2"
+
+"@keystonehq/bc-ur-registry-eth@^0.9.0":
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/@keystonehq/bc-ur-registry-eth/-/bc-ur-registry-eth-0.9.0.tgz#607428945029a06ec17ce3288caf53a0cbd8cc22"
+ integrity sha512-OVRT8Op+ZlOU9EBMxPBtQLrQZKzsV3DlfLq8P1T+Dq7WmGQNsRmQPchgju9qOlIIvmuAKaKdGXNN9W2qpTBAfA==
+ dependencies:
+ "@keystonehq/bc-ur-registry" "^0.5.0-alpha.5"
+ ethereumjs-util "^7.0.8"
+ hdkey "^2.0.1"
+ uuid "^8.3.2"
+
+"@keystonehq/bc-ur-registry@^0.4.4":
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/@keystonehq/bc-ur-registry/-/bc-ur-registry-0.4.4.tgz#3073fdd4b33cdcbd04526a313a7685891a4b4583"
+ integrity sha512-SBdKdAZfp3y14GTGrKjfJJHf4iXObjcm4/qKUZ92lj8HVR8mxHHGmHksjE328bJPTAsJPloLix4rTnWg+qgS2w==
+ dependencies:
+ "@ngraveio/bc-ur" "^1.1.5"
+ base58check "^2.0.0"
+ tslib "^2.3.0"
+
+"@keystonehq/bc-ur-registry@^0.5.0-alpha.5":
+ version "0.5.0-alpha.5"
+ resolved "https://registry.yarnpkg.com/@keystonehq/bc-ur-registry/-/bc-ur-registry-0.5.0-alpha.5.tgz#3d1a7eab980e8445c1596cdde704215c96d6b88a"
+ integrity sha512-T80XI+c8pWnkq9ZbuadlhFq/+8o4TcHtq+LQsK1XfjkhBqH75tcwim0310gKxavOhaSoC1i8dSqAnrFpj+5dJw==
+ dependencies:
+ "@ngraveio/bc-ur" "^1.1.5"
+ base58check "^2.0.0"
+ tslib "^2.3.0"
+
+"@keystonehq/metamask-airgapped-keyring@^0.2.2":
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/@keystonehq/metamask-airgapped-keyring/-/metamask-airgapped-keyring-0.2.2.tgz#8beb3e3fddf814be05e3c3178f973769b3ab40d7"
+ integrity sha512-1tuSNpc98jEoJ/9sptasxG6hm8KzESx0ShbKtPkNNKP+XxcDuMi632n03E748X98leCscI+M1WlRvsTryiEB8Q==
+ dependencies:
+ "@ethereumjs/tx" "^3.3.0"
+ "@keystonehq/base-eth-keyring" "^0.3.2"
+ "@keystonehq/bc-ur-registry-eth" "^0.7.7"
+ "@metamask/obs-store" "^7.0.0"
+ rlp "^2.2.6"
+ uuid "^8.3.2"
+
+"@keystonehq/metamask-airgapped-keyring@^0.3.0":
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/@keystonehq/metamask-airgapped-keyring/-/metamask-airgapped-keyring-0.3.0.tgz#3de02b268b28d9f2e2e728a10cad8cfc17870c3c"
+ integrity sha512-CkiQGRPYM8CBeb8GsrrsTXpdHACl9NnoeWGQDY7DXGiy3s6u7WQ6TXal7K+wAHdU4asBzTaK2SNPZ/eIvGiAfg==
+ dependencies:
+ "@ethereumjs/tx" "^3.3.0"
+ "@keystonehq/base-eth-keyring" "^0.4.0"
+ "@keystonehq/bc-ur-registry-eth" "^0.9.0"
+ "@metamask/obs-store" "^7.0.0"
+ rlp "^2.2.6"
+ uuid "^8.3.2"
+
+"@keystonehq/ur-decoder@^0.3.0":
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/@keystonehq/ur-decoder/-/ur-decoder-0.3.0.tgz#e9b17265371059c5e935e57d29fd0ae764882b4b"
+ integrity sha512-fZU4HVZWnK1JyUdlCi5jQN5SxEI+0XA9RubWIaFaje/O316Nc6+ZqjcYV1q27jvW8rBC+STzoUtqwQNQxalCiw==
+ dependencies:
+ "@keystonehq/bc-ur-registry-eth" "^0.6.12"
+ "@ngraveio/bc-ur" "^1.1.6"
+ typescript "^4.6.2"
+
"@lavamoat/allow-scripts@^1.0.6":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@lavamoat/allow-scripts/-/allow-scripts-1.0.6.tgz#fbdf7c35a5c2c2cff05ba002b7bc8f3355bda22c"
@@ -2153,19 +2277,20 @@
resolved "https://registry.yarnpkg.com/@lavamoat/preinstall-always-fail/-/preinstall-always-fail-1.0.0.tgz#e78a6e3d9e212a4fef869ec37d4f5fb498dea373"
integrity sha512-vD2DcC0ffJj1w2y1Lu0OU39wHmlPEd2tCDW04Bm6Kf4LyRnCHCezTsS8yzeSJ+4so7XP+TITuR5FGJRWxPb+GA==
-"@metamask/contract-metadata@^1.30.0", "@metamask/contract-metadata@^1.31.0":
+"@metamask/contract-metadata@^1.30.0", "@metamask/contract-metadata@^1.31.0", "@metamask/contract-metadata@^1.33.0":
version "1.30.0"
resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-1.30.0.tgz#fa8e1b0c3e7aaa963986088f691fb553ffbe3904"
integrity sha512-b2usYW/ptQYnE6zhUmr4T+nvOAQJK5ABcpKudyQANpy4K099elpv4aN0WcrcOcwV99NHOdMzFP3ZuG0HoAyOBQ==
-"@metamask/controllers@27.0.0":
- version "27.0.0"
- resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-27.0.0.tgz#23fb24960880047635a7e0b226375b843f385ad1"
- integrity sha512-ZMSniSWVJN6TGFwZv8o/N3jff0QKxW6/ZkhyE0Ikd6WB9WKt665OD/FgWbsJVBpzJ5fF4N7pziYULbVsag8pVQ==
+"@metamask/controllers@27.1.1":
+ version "27.1.1"
+ resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-27.1.1.tgz#b3288bfd05e381e9e32ed60b68a09b2855db1140"
+ integrity sha512-RzQ4zKsqmieYqAiVsIIazLTo9GYMcm9fDhYPJklP1M+bzm1k49GRFnZEfru3w/dPVY+wWgcDo/0ZWlOILbu3hg==
dependencies:
"@ethereumjs/common" "^2.3.1"
"@ethereumjs/tx" "^3.2.1"
- "@metamask/contract-metadata" "^1.31.0"
+ "@keystonehq/metamask-airgapped-keyring" "^0.3.0"
+ "@metamask/contract-metadata" "^1.33.0"
"@metamask/metamask-eth-abis" "3.0.0"
"@metamask/types" "^1.1.0"
"@types/uuid" "^8.3.0"
@@ -2177,7 +2302,7 @@
eth-json-rpc-infura "^5.1.0"
eth-keyring-controller "^6.2.1"
eth-method-registry "1.1.0"
- eth-phishing-detect "^1.1.14"
+ eth-phishing-detect "^1.1.16"
eth-query "^2.1.2"
eth-rpc-errors "^4.0.0"
eth-sig-util "^3.0.0"
@@ -2200,11 +2325,11 @@
"@metamask/controllers@^26.0.0":
version "26.0.0"
- resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-26.0.0.tgz#3df4a3071ffb26d357ba99f288d52fb9d913c35a"
- integrity sha512-iAWDoP/omxGzPfYyBFRNPJ32zcYvZHnUhIM2LyWoCwQj9ZYC1qh+dDX6I0O5jEeQcBrEb+Nl6AcnwHKVdEUz5Q==
+ resolved "git+https://github.com/MetaMask/controllers.git#d4e9507d9612f2d36c3f848333b33330a19b811b"
dependencies:
"@ethereumjs/common" "^2.3.1"
"@ethereumjs/tx" "^3.2.1"
+ "@keystonehq/metamask-airgapped-keyring" "^0.2.2"
"@metamask/contract-metadata" "^1.31.0"
"@metamask/metamask-eth-abis" "3.0.0"
"@metamask/types" "^1.1.0"
@@ -2268,6 +2393,14 @@
resolved "https://registry.yarnpkg.com/@metamask/mobile-provider/-/mobile-provider-2.1.0.tgz#685b2f6a55d24197af3f26de4dd0bb78e10ac83e"
integrity sha512-VuVUIZ5jEQmLaU8SJC8692crxtNncsxyR9q5j1J6epyMHUU75WTtQdq7VSsu1ghkmP9NXNAz3inlWOGsbT8lLA==
+"@metamask/obs-store@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@metamask/obs-store/-/obs-store-7.0.0.tgz#6cae5f28306bb3e83a381bc9ae22682316095bd3"
+ integrity sha512-Tr61Uu9CGXkCg5CZwOYRMQERd+y6fbtrtLd/PzDTPHO5UJpmSbU+7MPcQK7d1DwZCOCeCIvhmZSUCvYliC8uGw==
+ dependencies:
+ "@metamask/safe-event-emitter" "^2.0.0"
+ through2 "^2.0.3"
+
"@metamask/safe-event-emitter@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz#af577b477c683fad17c619a78208cede06f9605c"
@@ -2292,6 +2425,19 @@
resolved "https://registry.yarnpkg.com/@metamask/types/-/types-1.1.0.tgz#9bd14b33427932833c50c9187298804a18c2e025"
integrity sha512-EEV/GjlYkOSfSPnYXfOosxa3TqYtIW3fhg6jdw+cok/OhMgNn4wCfbENFqjytrHMU2f7ZKtBAvtiP5V8H44sSw==
+"@ngraveio/bc-ur@^1.1.5", "@ngraveio/bc-ur@^1.1.6":
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/@ngraveio/bc-ur/-/bc-ur-1.1.6.tgz#8f8c75fff22f6a5e4dfbc5a6b540d7fe8f42cd39"
+ integrity sha512-G+2XgjXde2IOcEQeCwR250aS43/Swi7gw0FuETgJy2c3HqF8f88SXDMsIGgJlZ8jXd0GeHR4aX0MfjXf523UZg==
+ dependencies:
+ "@apocentre/alias-sampling" "^0.5.3"
+ assert "^2.0.0"
+ bignumber.js "^9.0.1"
+ cbor-sync "^1.0.4"
+ crc "^3.8.0"
+ jsbi "^3.1.5"
+ sha.js "^2.4.11"
+
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -2364,10 +2510,10 @@
dependencies:
prop-types "^15.5.10"
-"@react-native-community/checkbox@^0.4.2":
- version "0.4.2"
- resolved "https://registry.yarnpkg.com/@react-native-community/checkbox/-/checkbox-0.4.2.tgz#109214058610200fcbb97e8761cf0cba8df5abd5"
- integrity sha512-vGetj36fOD+o3xCx8AGg56HqopetKQAWCEslD3oj8cs0xaGyRLQh3Febu13wMBOdaAQP85kITpaIET9DF4Apaw==
+"@react-native-community/checkbox@^0.5.12":
+ version "0.5.12"
+ resolved "https://registry.yarnpkg.com/@react-native-community/checkbox/-/checkbox-0.5.12.tgz#1f55a38bbcd5f10ca853db0c6a9f1c4beccc37af"
+ integrity sha512-Dd3eiAW7AkpRgndwKGdgQ6nNgEmZlVD8+KAOBqwjuZQ/M67BThXJ9Ni+ydpPjNm4e28KXmIILfkvhcDt0ZoFTQ==
"@react-native-community/cli-debugger-ui@^6.0.0-rc.0":
version "6.0.0-rc.0"
@@ -4191,6 +4337,16 @@ assert@1.4.1:
dependencies:
util "0.10.3"
+assert@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/assert/-/assert-2.0.0.tgz#95fc1c616d48713510680f2eaf2d10dd22e02d32"
+ integrity sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==
+ dependencies:
+ es6-object-assign "^1.1.0"
+ is-nan "^1.2.1"
+ object-is "^1.0.1"
+ util "^0.12.0"
+
assign-symbols@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
@@ -4533,6 +4689,11 @@ base-64@1.0.0:
resolved "https://registry.yarnpkg.com/base-64/-/base-64-1.0.0.tgz#09d0f2084e32a3fd08c2475b973788eee6ae8f4a"
integrity sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==
+base-x@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/base-x/-/base-x-1.1.0.tgz#42d3d717474f9ea02207f6d1aa1f426913eeb7ac"
+ integrity sha1-QtPXF0dPnqAiB/bRqh9CaRPut6w=
+
base-x@^3.0.2, base-x@^3.0.8:
version "3.0.8"
resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.8.tgz#1e1106c2537f0162e8b52474a557ebb09000018d"
@@ -4540,6 +4701,13 @@ base-x@^3.0.2, base-x@^3.0.8:
dependencies:
safe-buffer "^5.0.1"
+base58check@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/base58check/-/base58check-2.0.0.tgz#8046652d14bc87f063bd16be94a39134d3b61173"
+ integrity sha1-gEZlLRS8h/BjvRa+lKORNNO2EXM=
+ dependencies:
+ bs58 "^3.0.0"
+
base64-js@1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
@@ -4672,7 +4840,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.10.0, bn.js@^4.11.0, bn.js@^4.11.1, bn.js@^
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
-bn.js@^5.0.0, bn.js@^5.1.1, bn.js@^5.1.2:
+bn.js@^5.0.0, bn.js@^5.1.1, bn.js@^5.1.2, bn.js@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002"
integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==
@@ -4893,6 +5061,13 @@ browserslist@^4.17.5:
node-releases "^2.0.1"
picocolors "^1.0.0"
+bs58@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/bs58/-/bs58-3.1.0.tgz#d4c26388bf4804cac714141b1945aa47e5eb248e"
+ integrity sha1-1MJjiL9IBMrHFBQbGUWqR+XrJI4=
+ dependencies:
+ base-x "^1.1.0"
+
bs58@^4.0.0, bs58@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a"
@@ -4971,7 +5146,7 @@ buffer@^4.9.1:
ieee754 "^1.1.4"
isarray "^1.0.0"
-buffer@^5.0.5, buffer@^5.4.3, buffer@^5.5.0, buffer@^5.6.0:
+buffer@^5.0.5, buffer@^5.1.0, buffer@^5.4.3, buffer@^5.5.0, buffer@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
@@ -5781,6 +5956,13 @@ crc-32@^1.2.0:
exit-on-epipe "~1.0.1"
printj "~1.1.0"
+crc@^3.8.0:
+ version "3.8.0"
+ resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6"
+ integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==
+ dependencies:
+ buffer "^5.1.0"
+
create-ecdh@^4.0.0:
version "4.0.4"
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
@@ -6795,6 +6977,11 @@ es6-iterator@^2.0.3:
es5-ext "^0.10.35"
es6-symbol "^3.1.1"
+es6-object-assign@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c"
+ integrity sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=
+
es6-symbol@^3.1.1, es6-symbol@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
@@ -7496,6 +7683,13 @@ eth-phishing-detect@^1.1.14:
dependencies:
fast-levenshtein "^2.0.6"
+eth-phishing-detect@^1.1.16:
+ version "1.1.16"
+ resolved "https://registry.yarnpkg.com/eth-phishing-detect/-/eth-phishing-detect-1.1.16.tgz#637158d5774819e1a861f6d169e6d77d076a47fd"
+ integrity sha512-/o9arK5qFOKVdfZK9hJVAQP0eKXjAvImIKNBMfF9Nj1HGicD3wfsVuXDu1OHrxuEi6+4kYtD9wyAn/3G7g7VrA==
+ dependencies:
+ fast-levenshtein "^2.0.6"
+
eth-query@^2.1.0, eth-query@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/eth-query/-/eth-query-2.1.2.tgz#d6741d9000106b51510c72db92d6365456a6da5e"
@@ -7735,6 +7929,17 @@ ethereumjs-util@^7.0.2:
ethjs-util "0.1.6"
rlp "^2.2.4"
+ethereumjs-util@^7.0.7, ethereumjs-util@^7.0.8, ethereumjs-util@^7.1.3:
+ version "7.1.3"
+ resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.3.tgz#b55d7b64dde3e3e45749e4c41288238edec32d23"
+ integrity sha512-y+82tEbyASO0K0X1/SRhbJJoAlfcvq8JbrG4a5cjrOks7HS/36efU/0j2flxCPOUM++HFahk33kr/ZxyC4vNuw==
+ dependencies:
+ "@types/bn.js" "^5.1.0"
+ bn.js "^5.1.2"
+ create-hash "^1.1.2"
+ ethereum-cryptography "^0.1.3"
+ rlp "^2.2.4"
+
ethereumjs-util@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.0.tgz#e2b43a30bfcdbcb432a4eb42bd5f2393209b3fd5"
@@ -9191,6 +9396,15 @@ hastscript@^5.0.0:
property-information "^5.0.0"
space-separated-tokens "^1.0.0"
+hdkey@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/hdkey/-/hdkey-2.0.1.tgz#0a211d0c510bfc44fa3ec9d44b13b634641cad74"
+ integrity sha512-c+tl9PHG9/XkGgG0tD7CJpRVaE0jfZizDNmnErUAKQ4EjQSOcOUcV3EN9ZEZS8pZ4usaeiiK0H7stzuzna8feA==
+ dependencies:
+ bs58check "^2.1.2"
+ safe-buffer "^5.1.1"
+ secp256k1 "^4.0.0"
+
hermes-engine@~0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/hermes-engine/-/hermes-engine-0.9.0.tgz#84d9cfe84e8f6b1b2020d6e71b350cec84ed982f"
@@ -9840,6 +10054,14 @@ is-map@^2.0.1, is-map@^2.0.2:
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==
+is-nan@^1.2.1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d"
+ integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==
+ dependencies:
+ call-bind "^1.0.0"
+ define-properties "^1.1.3"
+
is-negative-zero@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
@@ -10604,6 +10826,11 @@ js-yaml@3.14.1, js-yaml@^3.13.0, js-yaml@^3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
+jsbi@^3.1.5:
+ version "3.2.5"
+ resolved "https://registry.yarnpkg.com/jsbi/-/jsbi-3.2.5.tgz#b37bb90e0e5c2814c1c2a1bcd8c729888a2e37d6"
+ integrity sha512-aBE4n43IPvjaddScbvWRA2YlTzKEynHzu7MqOyTipdHucf/VxS63ViCjxYRg86M8Rxwbt/GfzHl1kKERkt45fQ==
+
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
@@ -14944,6 +15171,13 @@ rlp@^2.0.0, rlp@^2.2.3, rlp@^2.2.4:
dependencies:
bn.js "^4.11.1"
+rlp@^2.2.6:
+ version "2.2.7"
+ resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.7.tgz#33f31c4afac81124ac4b283e2bd4d9720b30beaf"
+ integrity sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==
+ dependencies:
+ bn.js "^5.2.0"
+
rn-fetch-blob@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/rn-fetch-blob/-/rn-fetch-blob-0.12.0.tgz#ec610d2f9b3f1065556b58ab9c106eeb256f3cba"
@@ -15136,6 +15370,15 @@ secp256k1@^3.0.1:
nan "^2.14.0"
safe-buffer "^5.1.2"
+secp256k1@^4.0.0:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.3.tgz#c4559ecd1b8d3c1827ed2d1b94190d69ce267303"
+ integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==
+ dependencies:
+ elliptic "^6.5.4"
+ node-addon-api "^2.0.0"
+ node-gyp-build "^4.2.0"
+
secp256k1@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.2.tgz#15dd57d0f0b9fdb54ac1fa1694f40e5e9a54f4a1"
@@ -15306,7 +15549,7 @@ setprototypeof@1.2.0:
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
-sha.js@^2.4.0, sha.js@^2.4.8:
+sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8:
version "2.4.11"
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
@@ -16534,7 +16777,7 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.2.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
-tslib@^2.0.3, tslib@^2.1.0:
+tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
@@ -16657,6 +16900,11 @@ typescript@^4.4.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.2.tgz#6d618640d430e3569a1dfb44f7d7e600ced3ee86"
integrity sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==
+typescript@^4.6.2:
+ version "4.6.2"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4"
+ integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==
+
ua-parser-js@^0.7.18, ua-parser-js@^0.7.24:
version "0.7.31"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"