From a931316a5317b1fe6443a419f5ca856f8058692f Mon Sep 17 00:00:00 2001 From: Aaron Chen Date: Wed, 24 Nov 2021 01:28:39 +0800 Subject: [PATCH] Introduce QR based signer into MetaMask (#12065) * support qr based signer * add CSP for fire fox * get QR Hardware wallet name from device * fix qrHardware state missing in runtime * support qr based signer sign transaction * refine Request Signature modal ui * remove feature toggle * refine ui * fix notification is closing even there is a pending qr hardware transaction * add chinese translation, refine ui, fix qr process was breaking in some case * support import accounts by pubkeys * refine qr-based wallet ui and fix bugs * update @keystonehq/metamask-airgapped-keyring to fix that the signing hd path was inconsistent in some edge case * fix: avoid unnecessay navigation, fix ci * refactor qr-hardware-popover with @zxing/browser * update lavamoat policy, remove firefox CSP * refine qr reader ui, ignore unnecessary warning display * code refactor, use async functions insteads promise Co-authored-by: Soralit --- app/_locales/en/messages.json | 51 ++++ app/_locales/zh_CN/messages.json | 48 ++++ app/images/qrcode-wallet-demo.svg | 56 +++++ app/images/qrcode-wallet-logo.svg | 11 + app/scripts/controllers/app-state.js | 6 + app/scripts/metamask-controller.js | 71 +++++- app/scripts/platforms/extension.js | 7 +- lavamoat/browserify/beta/policy.json | 103 +++++++++ lavamoat/browserify/flask/policy.json | 103 +++++++++ lavamoat/browserify/main/policy.json | 103 +++++++++ package.json | 8 +- shared/constants/hardware-wallets.js | 10 +- .../account-menu/account-menu.component.js | 1 + .../app/qr-hardware-popover/base-reader.js | 217 ++++++++++++++++++ .../qr-hardware-popover/enhanced-reader.js | 67 ++++++ .../app/qr-hardware-popover/index.js | 3 + .../qr-hardware-popover.js | 102 ++++++++ .../qr-hardware-sign-request/index.js | 3 + .../qr-hardware-sign-request/player.js | 71 ++++++ .../qr-hardware-sign-request.component.js | 45 ++++ .../qr-hardware-sign-request/reader.js | 52 +++++ .../qr-hardware-wallet-importer/index.js | 3 + .../qr-hardware-wallet-importer.component.js | 37 +++ .../create-account/connect-hardware/index.js | 26 ++- .../connect-hardware/index.scss | 2 +- .../connect-hardware/select-hardware.js | 102 +++++++- ui/pages/home/home.component.js | 29 ++- ui/pages/home/home.container.js | 7 + ui/pages/routes/routes.component.js | 2 + ui/selectors/selectors.js | 43 ++++ ui/store/actions.js | 33 ++- yarn.lock | 192 +++++++++++++++- 32 files changed, 1567 insertions(+), 47 deletions(-) create mode 100644 app/images/qrcode-wallet-demo.svg create mode 100644 app/images/qrcode-wallet-logo.svg create mode 100644 ui/components/app/qr-hardware-popover/base-reader.js create mode 100644 ui/components/app/qr-hardware-popover/enhanced-reader.js create mode 100644 ui/components/app/qr-hardware-popover/index.js create mode 100644 ui/components/app/qr-hardware-popover/qr-hardware-popover.js create mode 100644 ui/components/app/qr-hardware-popover/qr-hardware-sign-request/index.js create mode 100644 ui/components/app/qr-hardware-popover/qr-hardware-sign-request/player.js create mode 100644 ui/components/app/qr-hardware-popover/qr-hardware-sign-request/qr-hardware-sign-request.component.js create mode 100644 ui/components/app/qr-hardware-popover/qr-hardware-sign-request/reader.js create mode 100644 ui/components/app/qr-hardware-popover/qr-hardware-wallet-importer/index.js create mode 100644 ui/components/app/qr-hardware-popover/qr-hardware-wallet-importer/qr-hardware-wallet-importer.component.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 3a3d89906..2878872a9 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1,4 +1,49 @@ { + "QRHardwareInvalidTransactionTitle": { + "message": "Error" + }, + "QRHardwareMismatchedSignId": { + "message": "Incongruent transaction data. Please check the transaction details." + }, + "QRHardwarePubkeyAccountOutOfRange": { + "message": "No more accounts. If you would like to access another account unlisted below, please reconnect your hardware wallet and select it." + }, + "QRHardwareScanInstructions": { + "message": "Place the QR code in front of your camera. The screen is blurred, but it will not affect the reading." + }, + "QRHardwareSignRequestCancel": { + "message": "Reject" + }, + "QRHardwareSignRequestDescription": { + "message": "After you’ve signed with your wallet, click on 'Get Signature' to receive the signature" + }, + "QRHardwareSignRequestGetSignature": { + "message": "Get Signature" + }, + "QRHardwareSignRequestSubtitle": { + "message": "Scan the QR code with your wallet" + }, + "QRHardwareSignRequestTitle": { + "message": "Request Signature" + }, + "QRHardwareUnknownQRCodeTitle": { + "message": "Error" + }, + "QRHardwareUnknownWalletQRCode": { + "message": "Invalid QR code. Please scan the sync QR code of the hardware wallet." + }, + "QRHardwareWalletImporterTitle": { + "message": "Scan QR Code" + }, + "QRHardwareWalletSteps1Description": { + "message": "Connect an airgapped hardware wallet that communicates through QR-codes. Officially supported airgapped hardware wallets include:" + }, + "QRHardwareWalletSteps1Title": { + "message": "QR-based HW Wallet" + }, + "QRHardwareWalletSteps2Description": { + "message": "AirGap Vault & Ngrave (Coming Soon)" + }, "about": { "message": "About" }, @@ -1306,6 +1351,12 @@ "message": "JSON File", "description": "format for importing an account" }, + "keystone": { + "message": "Keystone" + }, + "keystoneTutorial": { + "message": " (Tutorials)" + }, "knownAddressRecipient": { "message": "Known contract address." }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 8555b89a4..c65164366 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1,4 +1,46 @@ { + "QRHardwareInvalidTransactionTitle": { + "message": "非法交易" + }, + "QRHardwareMismatchedSignId": { + "message": "扫描的签名二维码不属于当前交易,请检查交易详情后重试。" + }, + "QRHardwarePubkeyAccountOutOfRange": { + "message": "暂无更多账户,若想切换到其他账户,请在硬件钱包中选择想要的账户重新同步。" + }, + "QRHardwareScanInstructions": { + "message": "为了保护您的隐私,屏幕是模糊的,但不影响对二维码的读取。" + }, + "QRHardwareSignRequestCancel": { + "message": "拒绝该交易" + }, + "QRHardwareSignRequestDescription": { + "message": "硬件钱包扫描上方二维码完成签名后,点击“获取签名”按钮扫描已签名的二维码" + }, + "QRHardwareSignRequestGetSignature": { + "message": "获取签名" + }, + "QRHardwareSignRequestSubtitle": { + "message": "用硬件钱包扫描二维码" + }, + "QRHardwareSignRequestTitle": { + "message": "获取签名" + }, + "QRHardwareUnknownQRCodeTitle": { + "message": "非法二维码" + }, + "QRHardwareUnknownWalletQRCode": { + "message": "请扫描硬件钱包的同步二维码。" + }, + "QRHardwareWalletImporterTitle": { + "message": "扫描二维码" + }, + "QRHardwareWalletSteps1Description": { + "message": "该类硬件钱包通过二维码实现通讯交互,做到完全脱网。官方支持的钱包有:" + }, + "QRHardwareWalletSteps2Description": { + "message": "AirGap Vault & Ngrave (即将上线)" + }, "about": { "message": "关于" }, @@ -835,6 +877,12 @@ "message": "JSON 文件", "description": "format for importing an account" }, + "keystone": { + "message": "铠石钱包" + }, + "keystoneTutorial": { + "message": " (使用教程)" + }, "knownAddressRecipient": { "message": "已知接收方地址。" }, diff --git a/app/images/qrcode-wallet-demo.svg b/app/images/qrcode-wallet-demo.svg new file mode 100644 index 000000000..61ed69eee --- /dev/null +++ b/app/images/qrcode-wallet-demo.svg @@ -0,0 +1,56 @@ + + + png-chahua + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/images/qrcode-wallet-logo.svg b/app/images/qrcode-wallet-logo.svg new file mode 100644 index 000000000..a88a7635e --- /dev/null +++ b/app/images/qrcode-wallet-logo.svg @@ -0,0 +1,11 @@ + + + qr-logo的副本2 + + + + + + + + \ No newline at end of file diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index f68579bad..3f1881786 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -16,6 +16,7 @@ export default class AppStateController extends EventEmitter { onInactiveTimeout, showUnlockRequest, preferencesStore, + qrHardwareStore, } = opts; super(); @@ -32,6 +33,7 @@ export default class AppStateController extends EventEmitter { recoveryPhraseReminderLastShown: new Date().getTime(), showTestnetMessageInDropdown: true, ...initState, + qrHardware: {}, }); this.timer = null; @@ -48,6 +50,10 @@ export default class AppStateController extends EventEmitter { } }); + qrHardwareStore.subscribe((state) => { + this.store.updateState({ qrHardware: state }); + }); + const { preferences } = preferencesStore.getState(); this._setInactiveTimeout(preferences.autoLockTimeLimit); } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 004879828..5eec84f24 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -15,6 +15,7 @@ import log from 'loglevel'; import TrezorKeyring from 'eth-trezor-keyring'; import LedgerBridgeKeyring from '@metamask/eth-ledger-bridge-keyring'; import LatticeKeyring from 'eth-lattice-keyring'; +import { MetaMaskKeyring as QRHardwareKeyring } from '@keystonehq/metamask-airgapped-keyring'; import EthQuery from 'eth-query'; import nanoid from 'nanoid'; import { ethErrors } from 'eth-rpc-errors'; @@ -41,7 +42,10 @@ import { SWAPS_CLIENT_ID, } from '../../shared/constants/swaps'; import { MAINNET_CHAIN_ID } from '../../shared/constants/network'; -import { KEYRING_TYPES } from '../../shared/constants/hardware-wallets'; +import { + DEVICE_NAMES, + KEYRING_TYPES, +} from '../../shared/constants/hardware-wallets'; import { UI_NOTIFICATIONS } from '../../shared/notifications'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { MILLISECOND } from '../../shared/constants/time'; @@ -286,6 +290,8 @@ export default class MetamaskController extends EventEmitter { }, }); + this.qrHardwareKeyring = new QRHardwareKeyring(); + this.appStateController = new AppStateController({ addUnlockListener: this.on.bind(this, 'unlock'), isUnlocked: this.isUnlocked.bind(this), @@ -293,6 +299,7 @@ export default class MetamaskController extends EventEmitter { onInactiveTimeout: () => this.setLocked(), showUnlockRequest: opts.showUserConfirmation, preferencesStore: this.preferencesController.store, + qrHardwareStore: this.qrHardwareKeyring.getMemStore(), }); const currencyRateMessenger = this.controllerMessenger.getRestricted({ @@ -432,6 +439,7 @@ export default class MetamaskController extends EventEmitter { TrezorKeyring, LedgerBridgeKeyring, LatticeKeyring, + QRHardwareKeyring, ]; this.keyringController = new KeyringController({ keyringTypes: additionalKeyrings, @@ -938,6 +946,28 @@ export default class MetamaskController extends EventEmitter { this, ), + // qr hardware devices + submitQRHardwareCryptoHDKey: nodeify( + this.qrHardwareKeyring.submitCryptoHDKey, + this.qrHardwareKeyring, + ), + submitQRHardwareCryptoAccount: nodeify( + this.qrHardwareKeyring.submitCryptoAccount, + this.qrHardwareKeyring, + ), + cancelSyncQRHardware: nodeify( + this.qrHardwareKeyring.cancelSync, + this.qrHardwareKeyring, + ), + submitQRHardwareSignature: nodeify( + this.qrHardwareKeyring.submitSignature, + this.qrHardwareKeyring, + ), + cancelQRHardwareSignRequest: nodeify( + this.qrHardwareKeyring.cancelSignRequest, + this.qrHardwareKeyring, + ), + // mobile fetchInfoToSync: nodeify(this.fetchInfoToSync, this), @@ -1647,13 +1677,16 @@ export default class MetamaskController extends EventEmitter { async getKeyringForDevice(deviceName, hdPath = null) { let keyringName = null; switch (deviceName) { - case 'trezor': + case DEVICE_NAMES.TREZOR: keyringName = TrezorKeyring.type; break; - case 'ledger': + case DEVICE_NAMES.LEDGER: keyringName = LedgerBridgeKeyring.type; break; - case 'lattice': + case DEVICE_NAMES.QR: + keyringName = QRHardwareKeyring.type; + break; + case DEVICE_NAMES.LATTICE: keyringName = LatticeKeyring.type; break; default: @@ -1670,7 +1703,7 @@ export default class MetamaskController extends EventEmitter { if (hdPath && keyring.setHdPath) { keyring.setHdPath(hdPath); } - if (deviceName === 'lattice') { + if (deviceName === DEVICE_NAMES.LATTICE) { keyring.appName = 'MetaMask'; } keyring.network = this.networkController.getProviderConfig().type; @@ -1740,6 +1773,18 @@ export default class MetamaskController extends EventEmitter { return true; } + /** + * get hardware account label + * + * @return string label + * */ + + getAccountLabel(name, index, hdPathDescription) { + return `${name[0].toUpperCase()}${name.slice(1)} ${ + parseInt(index, 10) + 1 + } ${hdPathDescription || ''}`.trim(); + } + /** * Imports an account from a Trezor or Ledger device. * @@ -1760,10 +1805,12 @@ export default class MetamaskController extends EventEmitter { this.preferencesController.setAddresses(newAccounts); newAccounts.forEach((address) => { if (!oldAccounts.includes(address)) { - const label = `${deviceName[0].toUpperCase()}${deviceName.slice(1)} ${ - parseInt(index, 10) + 1 - } ${hdPathDescription || ''}`.trim(); - // Set the account label to Trezor 1 / Ledger 1, etc + const label = this.getAccountLabel( + deviceName === DEVICE_NAMES.QR ? keyring.getName() : deviceName, + index, + hdPathDescription, + ); + // Set the account label to Trezor 1 / Ledger 1 / QR Hardware 1, etc this.preferencesController.setAccountLabel(address, label); // Select the account this.preferencesController.setSelectedAddress(address); @@ -2179,6 +2226,12 @@ export default class MetamaskController extends EventEmitter { }); } + case KEYRING_TYPES.QR: { + return Promise.reject( + new Error('QR hardware does not support eth_getEncryptionPublicKey.'), + ); + } + default: { const promise = this.encryptionPublicKeyManager.addUnapprovedMessageAsync( msgParams, diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index c2f931b49..722d96625 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -118,13 +118,14 @@ export default class ExtensionPlatform { ) { let extensionURL = extension.runtime.getURL('home.html'); + if (route) { + extensionURL += `#${route}`; + } + if (queryString) { extensionURL += `?${queryString}`; } - if (route) { - extensionURL += `#${route}`; - } this.openTab({ url: extensionURL }); if ( getEnvironmentType() !== ENVIRONMENT_TYPE_BACKGROUND && diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index c8a541438..024601e10 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -412,6 +412,47 @@ "Intl.getCanonicalLocales": true } }, + "@keystonehq/base-eth-keyring": { + "packages": { + "@ethereumjs/tx": true, + "@keystonehq/bc-ur-registry-eth": true, + "buffer": true, + "ethereumjs-util": true, + "hdkey": true, + "uuid": true + } + }, + "@keystonehq/bc-ur-registry": { + "globals": { + "define": true + }, + "packages": { + "@ngraveio/bc-ur": true, + "bs58check": true, + "buffer": true + } + }, + "@keystonehq/bc-ur-registry-eth": { + "packages": { + "@keystonehq/bc-ur-registry": true, + "buffer": true, + "ethereumjs-util": true, + "hdkey": true, + "uuid": true + } + }, + "@keystonehq/metamask-airgapped-keyring": { + "packages": { + "@ethereumjs/tx": true, + "@keystonehq/base-eth-keyring": true, + "@keystonehq/bc-ur-registry-eth": true, + "@metamask/obs-store": true, + "buffer": true, + "events": true, + "rlp": true, + "uuid": true + } + }, "@material-ui/core": { "globals": { "Image": true, @@ -619,6 +660,18 @@ "events": true } }, + "@ngraveio/bc-ur": { + "packages": { + "@apocentre/alias-sampling": true, + "assert": true, + "bignumber.js": true, + "buffer": true, + "cbor-sync": true, + "crc": true, + "jsbi": true, + "sha.js": true + } + }, "@popperjs/core": { "globals": { "Element": true, @@ -738,6 +791,23 @@ "util": true } }, + "@zxing/browser": { + "globals": { + "HTMLElement": true, + "HTMLImageElement": true, + "HTMLVideoElement": true, + "URL.createObjectURL": true, + "clearTimeout": true, + "console.error": true, + "console.warn": true, + "document": true, + "navigator": true, + "setTimeout": true + }, + "packages": { + "@zxing/library": true + } + }, "@zxing/library": { "globals": { "TextDecoder": true, @@ -975,6 +1045,9 @@ } }, "bn.js": { + "globals": { + "Buffer": true + }, "packages": { "browser-resolve": true } @@ -1099,6 +1172,14 @@ "buffer": true } }, + "cbor-sync": { + "globals": { + "define": true + }, + "packages": { + "buffer": true + } + }, "cids": { "packages": { "buffer": true, @@ -1191,6 +1272,11 @@ "is-buffer": true } }, + "crc": { + "packages": { + "buffer": true + } + }, "crc-32": { "globals": { "DO_NOT_EXPORT_CRC": true, @@ -2007,6 +2093,7 @@ "hdkey": { "packages": { "assert": true, + "bs58check": true, "coinstring": true, "crypto-browserify": true, "safe-buffer": true, @@ -2559,6 +2646,11 @@ "console.warn": true } }, + "jsbi": { + "globals": { + "define": true + } + }, "json-rpc-engine": { "packages": { "@metamask/safe-event-emitter": true, @@ -3769,6 +3861,17 @@ "define": true } }, + "qrcode.react": { + "globals": { + "Path2D": true, + "devicePixelRatio": true + }, + "packages": { + "prop-types": true, + "qr.js": true, + "react": true + } + }, "rabin-wasm": { "globals": { "Blob": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index c8a541438..024601e10 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -412,6 +412,47 @@ "Intl.getCanonicalLocales": true } }, + "@keystonehq/base-eth-keyring": { + "packages": { + "@ethereumjs/tx": true, + "@keystonehq/bc-ur-registry-eth": true, + "buffer": true, + "ethereumjs-util": true, + "hdkey": true, + "uuid": true + } + }, + "@keystonehq/bc-ur-registry": { + "globals": { + "define": true + }, + "packages": { + "@ngraveio/bc-ur": true, + "bs58check": true, + "buffer": true + } + }, + "@keystonehq/bc-ur-registry-eth": { + "packages": { + "@keystonehq/bc-ur-registry": true, + "buffer": true, + "ethereumjs-util": true, + "hdkey": true, + "uuid": true + } + }, + "@keystonehq/metamask-airgapped-keyring": { + "packages": { + "@ethereumjs/tx": true, + "@keystonehq/base-eth-keyring": true, + "@keystonehq/bc-ur-registry-eth": true, + "@metamask/obs-store": true, + "buffer": true, + "events": true, + "rlp": true, + "uuid": true + } + }, "@material-ui/core": { "globals": { "Image": true, @@ -619,6 +660,18 @@ "events": true } }, + "@ngraveio/bc-ur": { + "packages": { + "@apocentre/alias-sampling": true, + "assert": true, + "bignumber.js": true, + "buffer": true, + "cbor-sync": true, + "crc": true, + "jsbi": true, + "sha.js": true + } + }, "@popperjs/core": { "globals": { "Element": true, @@ -738,6 +791,23 @@ "util": true } }, + "@zxing/browser": { + "globals": { + "HTMLElement": true, + "HTMLImageElement": true, + "HTMLVideoElement": true, + "URL.createObjectURL": true, + "clearTimeout": true, + "console.error": true, + "console.warn": true, + "document": true, + "navigator": true, + "setTimeout": true + }, + "packages": { + "@zxing/library": true + } + }, "@zxing/library": { "globals": { "TextDecoder": true, @@ -975,6 +1045,9 @@ } }, "bn.js": { + "globals": { + "Buffer": true + }, "packages": { "browser-resolve": true } @@ -1099,6 +1172,14 @@ "buffer": true } }, + "cbor-sync": { + "globals": { + "define": true + }, + "packages": { + "buffer": true + } + }, "cids": { "packages": { "buffer": true, @@ -1191,6 +1272,11 @@ "is-buffer": true } }, + "crc": { + "packages": { + "buffer": true + } + }, "crc-32": { "globals": { "DO_NOT_EXPORT_CRC": true, @@ -2007,6 +2093,7 @@ "hdkey": { "packages": { "assert": true, + "bs58check": true, "coinstring": true, "crypto-browserify": true, "safe-buffer": true, @@ -2559,6 +2646,11 @@ "console.warn": true } }, + "jsbi": { + "globals": { + "define": true + } + }, "json-rpc-engine": { "packages": { "@metamask/safe-event-emitter": true, @@ -3769,6 +3861,17 @@ "define": true } }, + "qrcode.react": { + "globals": { + "Path2D": true, + "devicePixelRatio": true + }, + "packages": { + "prop-types": true, + "qr.js": true, + "react": true + } + }, "rabin-wasm": { "globals": { "Blob": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index c8a541438..024601e10 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -412,6 +412,47 @@ "Intl.getCanonicalLocales": true } }, + "@keystonehq/base-eth-keyring": { + "packages": { + "@ethereumjs/tx": true, + "@keystonehq/bc-ur-registry-eth": true, + "buffer": true, + "ethereumjs-util": true, + "hdkey": true, + "uuid": true + } + }, + "@keystonehq/bc-ur-registry": { + "globals": { + "define": true + }, + "packages": { + "@ngraveio/bc-ur": true, + "bs58check": true, + "buffer": true + } + }, + "@keystonehq/bc-ur-registry-eth": { + "packages": { + "@keystonehq/bc-ur-registry": true, + "buffer": true, + "ethereumjs-util": true, + "hdkey": true, + "uuid": true + } + }, + "@keystonehq/metamask-airgapped-keyring": { + "packages": { + "@ethereumjs/tx": true, + "@keystonehq/base-eth-keyring": true, + "@keystonehq/bc-ur-registry-eth": true, + "@metamask/obs-store": true, + "buffer": true, + "events": true, + "rlp": true, + "uuid": true + } + }, "@material-ui/core": { "globals": { "Image": true, @@ -619,6 +660,18 @@ "events": true } }, + "@ngraveio/bc-ur": { + "packages": { + "@apocentre/alias-sampling": true, + "assert": true, + "bignumber.js": true, + "buffer": true, + "cbor-sync": true, + "crc": true, + "jsbi": true, + "sha.js": true + } + }, "@popperjs/core": { "globals": { "Element": true, @@ -738,6 +791,23 @@ "util": true } }, + "@zxing/browser": { + "globals": { + "HTMLElement": true, + "HTMLImageElement": true, + "HTMLVideoElement": true, + "URL.createObjectURL": true, + "clearTimeout": true, + "console.error": true, + "console.warn": true, + "document": true, + "navigator": true, + "setTimeout": true + }, + "packages": { + "@zxing/library": true + } + }, "@zxing/library": { "globals": { "TextDecoder": true, @@ -975,6 +1045,9 @@ } }, "bn.js": { + "globals": { + "Buffer": true + }, "packages": { "browser-resolve": true } @@ -1099,6 +1172,14 @@ "buffer": true } }, + "cbor-sync": { + "globals": { + "define": true + }, + "packages": { + "buffer": true + } + }, "cids": { "packages": { "buffer": true, @@ -1191,6 +1272,11 @@ "is-buffer": true } }, + "crc": { + "packages": { + "buffer": true + } + }, "crc-32": { "globals": { "DO_NOT_EXPORT_CRC": true, @@ -2007,6 +2093,7 @@ "hdkey": { "packages": { "assert": true, + "bs58check": true, "coinstring": true, "crypto-browserify": true, "safe-buffer": true, @@ -2559,6 +2646,11 @@ "console.warn": true } }, + "jsbi": { + "globals": { + "define": true + } + }, "json-rpc-engine": { "packages": { "@metamask/safe-event-emitter": true, @@ -3769,6 +3861,17 @@ "define": true } }, + "qrcode.react": { + "globals": { + "Path2D": true, + "devicePixelRatio": true + }, + "packages": { + "prop-types": true, + "qr.js": true, + "react": true + } + }, "rabin-wasm": { "globals": { "Blob": true, diff --git a/package.json b/package.json index 98c2bec28..3a63336b7 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,8 @@ "@ethereumjs/tx": "^3.2.1", "@formatjs/intl-relativetimeformat": "^5.2.6", "@fortawesome/fontawesome-free": "^5.13.0", + "@keystonehq/bc-ur-registry-eth": "^0.6.8", + "@keystonehq/metamask-airgapped-keyring": "0.2.1", "@material-ui/core": "^4.11.0", "@metamask/contract-metadata": "^1.28.0", "@metamask/controllers": "^20.0.0", @@ -117,11 +119,13 @@ "@metamask/obs-store": "^5.0.0", "@metamask/post-message-stream": "^4.0.0", "@metamask/providers": "^8.1.1", + "@ngraveio/bc-ur": "^1.1.6", "@popperjs/core": "^2.4.0", "@reduxjs/toolkit": "^1.6.2", "@sentry/browser": "^6.0.0", "@sentry/integrations": "^6.0.0", - "@zxing/library": "^0.8.0", + "@zxing/browser": "^0.0.10", + "@zxing/library": "0.8.0", "analytics-node": "^3.4.0-beta.3", "await-semaphore": "^0.1.1", "base32-encode": "^1.2.0", @@ -181,6 +185,7 @@ "pump": "^3.0.0", "punycode": "^2.1.1", "qrcode-generator": "1.4.1", + "qrcode.react": "^1.0.1", "react": "^16.12.0", "react-dnd": "^3.0.2", "react-dnd-html5-backend": "^7.4.4", @@ -206,6 +211,7 @@ "swappable-obj-proxy": "^1.1.0", "textarea-caret": "^3.0.1", "unicode-confusables": "^0.1.1", + "uuid": "^8.3.2", "valid-url": "^1.0.9", "web3": "^0.20.7", "web3-stream-provider": "^4.0.0" diff --git a/shared/constants/hardware-wallets.js b/shared/constants/hardware-wallets.js index f32306472..105287800 100644 --- a/shared/constants/hardware-wallets.js +++ b/shared/constants/hardware-wallets.js @@ -1,5 +1,5 @@ /** - * Accounts can be instantiated from simple, HD or the two hardware wallet + * Accounts can be instantiated from simple, HD or the multiple hardware wallet * keyring types. Both simple and HD are treated as default but we do special * case accounts managed by a hardware wallet. */ @@ -7,6 +7,14 @@ export const KEYRING_TYPES = { LEDGER: 'Ledger Hardware', TREZOR: 'Trezor Hardware', LATTICE: 'Lattice Hardware', + QR: 'QR Hardware Wallet Device', +}; + +export const DEVICE_NAMES = { + LEDGER: 'ledger', + TREZOR: 'trezor', + QR: 'QR Hardware', + LATTICE: 'lattice', }; /** diff --git a/ui/components/app/account-menu/account-menu.component.js b/ui/components/app/account-menu/account-menu.component.js index f72077777..131d63336 100644 --- a/ui/components/app/account-menu/account-menu.component.js +++ b/ui/components/app/account-menu/account-menu.component.js @@ -239,6 +239,7 @@ export default class AccountMenu extends Component { case KEYRING_TYPES.TREZOR: case KEYRING_TYPES.LEDGER: case KEYRING_TYPES.LATTICE: + case KEYRING_TYPES.QR: label = t('hardware'); break; case 'Simple Key Pair': diff --git a/ui/components/app/qr-hardware-popover/base-reader.js b/ui/components/app/qr-hardware-popover/base-reader.js new file mode 100644 index 000000000..5743dc3c4 --- /dev/null +++ b/ui/components/app/qr-hardware-popover/base-reader.js @@ -0,0 +1,217 @@ +import React, { useEffect, useRef, useState } from 'react'; +import log from 'loglevel'; +import { URDecoder } from '@ngraveio/bc-ur'; +import PropTypes from 'prop-types'; +import { getEnvironmentType } from '../../../../app/scripts/lib/util'; +import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app'; +import WebcamUtils from '../../../helpers/utils/webcam-utils'; +import PageContainerFooter from '../../ui/page-container/page-container-footer/page-container-footer.component'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { SECOND } from '../../../../shared/constants/time'; +import EnhancedReader from './enhanced-reader'; + +const READY_STATE = { + ACCESSING_CAMERA: 'ACCESSING_CAMERA', + NEED_TO_ALLOW_ACCESS: 'NEED_TO_ALLOW_ACCESS', + READY: 'READY', +}; + +const BaseReader = ({ + isReadingWallet, + handleCancel, + handleSuccess, + setErrorTitle, +}) => { + const t = useI18nContext(); + const [ready, setReady] = useState(READY_STATE.ACCESSING_CAMERA); + const [error, setError] = useState(null); + const [urDecoder, setURDecoder] = useState(new URDecoder()); + + let permissionChecker = null; + const mounted = useRef(false); + + const reset = () => { + setReady(READY_STATE.ACCESSING_CAMERA); + setError(null); + setURDecoder(new URDecoder()); + }; + + const checkEnvironment = async () => { + try { + const { environmentReady } = await WebcamUtils.checkStatus(); + if ( + !environmentReady && + getEnvironmentType() !== ENVIRONMENT_TYPE_FULLSCREEN + ) { + const currentUrl = new URL(window.location.href); + const currentHash = currentUrl.hash; + const currentRoute = currentHash ? currentHash.substring(1) : null; + global.platform.openExtensionInBrowser(currentRoute); + } + } catch (e) { + if (mounted.current) { + setError(e); + } + } + // initial attempt is required to trigger permission prompt + // eslint-disable-next-line no-use-before-define + return initCamera(); + }; + + const checkPermissions = async () => { + try { + const { permissions } = await WebcamUtils.checkStatus(); + if (permissions) { + // Let the video stream load first... + await new Promise((resolve) => setTimeout(resolve, SECOND * 2)); + if (!mounted.current) { + return; + } + setReady(READY_STATE.READY); + } else if (mounted.current) { + // Keep checking for permissions + permissionChecker = setTimeout(checkPermissions, SECOND); + setReady(READY_STATE.NEED_TO_ALLOW_ACCESS); + } + } catch (e) { + if (mounted.current) { + setError(e); + } + } + }; + + const handleScan = (data) => { + try { + if (!data) { + return; + } + urDecoder.receivePart(data); + if (urDecoder.isComplete()) { + const result = urDecoder.resultUR(); + handleSuccess(result).catch(setError); + } + } catch (e) { + if (isReadingWallet) { + setErrorTitle(t('QRHardwareUnknownQRCodeTitle')); + } else { + setErrorTitle(t('QRHardwareInvalidTransactionTitle')); + } + setError(new Error(t('unknownQrCode'))); + } + }; + + const initCamera = () => { + try { + checkPermissions(); + } catch (e) { + if (!mounted.current) { + return; + } + if (e.name === 'NotAllowedError') { + log.info(`Permission denied: '${e}'`); + setReady(READY_STATE.NEED_TO_ALLOW_ACCESS); + } else { + setError(e); + } + } + }; + + useEffect(() => { + mounted.current = true; + checkEnvironment(); + return () => { + mounted.current = false; + clearTimeout(permissionChecker); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (ready === READY_STATE.READY) { + initCamera(); + } else if (ready === READY_STATE.NEED_TO_ALLOW_ACCESS) { + checkPermissions(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ready]); + + const tryAgain = () => { + clearTimeout(permissionChecker); + reset(); + checkEnvironment(); + }; + + const renderError = () => { + let title, msg; + if (error.type === 'NO_WEBCAM_FOUND') { + title = t('noWebcamFoundTitle'); + msg = t('noWebcamFound'); + } else if (error.message === t('unknownQrCode')) { + if (isReadingWallet) { + msg = t('QRHardwareUnknownWalletQRCode'); + } else { + msg = t('unknownQrCode'); + } + } else if (error.message === t('QRHardwareMismatchedSignId')) { + msg = t('QRHardwareMismatchedSignId'); + } else { + title = t('unknownCameraErrorTitle'); + msg = t('unknownCameraError'); + } + + return ( + <> +
+ +
+ {title ?
{title}
: null} +
{msg}
+ { + setErrorTitle(''); + handleCancel(); + }} + onSubmit={() => { + setErrorTitle(''); + tryAgain(); + }} + cancelText={t('cancel')} + submitText={t('tryAgain')} + submitButtonType="confirm" + /> + + ); + }; + + const renderVideo = () => { + let message; + if (ready === READY_STATE.ACCESSING_CAMERA) { + message = t('accessingYourCamera'); + } else if (ready === READY_STATE.READY) { + message = t('QRHardwareScanInstructions'); + } else if (ready === READY_STATE.NEED_TO_ALLOW_ACCESS) { + message = t('youNeedToAllowCameraAccess'); + } + return ( + <> +
+ +
+ {message &&
{message}
} + + ); + }; + + return ( +
{error ? renderError() : renderVideo()}
+ ); +}; + +BaseReader.propTypes = { + isReadingWallet: PropTypes.bool.isRequired, + handleCancel: PropTypes.func.isRequired, + handleSuccess: PropTypes.func.isRequired, + setErrorTitle: PropTypes.func.isRequired, +}; + +export default BaseReader; diff --git a/ui/components/app/qr-hardware-popover/enhanced-reader.js b/ui/components/app/qr-hardware-popover/enhanced-reader.js new file mode 100644 index 000000000..d1d696717 --- /dev/null +++ b/ui/components/app/qr-hardware-popover/enhanced-reader.js @@ -0,0 +1,67 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { BarcodeFormat, DecodeHintType } from '@zxing/library'; +import { BrowserQRCodeReader } from '@zxing/browser'; +import log from 'loglevel'; +import PropTypes from 'prop-types'; +import { MILLISECOND } from '../../../../shared/constants/time'; +import Spinner from '../../ui/spinner'; + +const EnhancedReader = ({ handleScan }) => { + const [canplay, setCanplay] = useState(false); + const codeReader = useMemo(() => { + const hint = new Map(); + hint.set(DecodeHintType.POSSIBLE_FORMATS, [BarcodeFormat.QR_CODE]); + return new BrowserQRCodeReader(hint, { + delayBetweenScanAttempts: MILLISECOND * 100, + delayBetweenScanSuccess: MILLISECOND * 100, + }); + }, []); + + useEffect(() => { + const videoElem = document.getElementById('video'); + const canplayListener = () => { + setCanplay(true); + }; + videoElem.addEventListener('canplay', canplayListener); + const promise = codeReader.decodeFromVideoDevice( + undefined, + 'video', + (result) => { + if (result) { + handleScan(result.getText()); + } + }, + ); + return () => { + videoElem.removeEventListener('canplay', canplayListener); + promise + .then((controls) => { + if (controls) { + controls.stop(); + } + }) + .catch(log.info); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+
+ ); +}; + +EnhancedReader.propTypes = { + handleScan: PropTypes.func.isRequired, +}; + +export default EnhancedReader; diff --git a/ui/components/app/qr-hardware-popover/index.js b/ui/components/app/qr-hardware-popover/index.js new file mode 100644 index 000000000..2ccbb2100 --- /dev/null +++ b/ui/components/app/qr-hardware-popover/index.js @@ -0,0 +1,3 @@ +import QRHardwarePopover from './qr-hardware-popover'; + +export default QRHardwarePopover; diff --git a/ui/components/app/qr-hardware-popover/qr-hardware-popover.js b/ui/components/app/qr-hardware-popover/qr-hardware-popover.js new file mode 100644 index 000000000..c3875c4e8 --- /dev/null +++ b/ui/components/app/qr-hardware-popover/qr-hardware-popover.js @@ -0,0 +1,102 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { getCurrentQRHardwareState } from '../../../selectors'; +import Popover from '../../ui/popover'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + cancelSyncQRHardware as cancelSyncQRHardwareAction, + cancelQRHardwareSignRequest as cancelQRHardwareSignRequestAction, + cancelTx, + cancelPersonalMsg, + cancelMsg, + cancelTypedMsg, +} from '../../../store/actions'; +import { MESSAGE_TYPE } from '../../../../shared/constants/app'; +import QRHardwareWalletImporter from './qr-hardware-wallet-importer'; +import QRHardwareSignRequest from './qr-hardware-sign-request'; + +const QRHardwarePopover = () => { + const t = useI18nContext(); + + const qrHardware = useSelector(getCurrentQRHardwareState); + const { sync, sign } = qrHardware; + const showWalletImporter = sync?.reading; + const showSignRequest = sign?.request; + const showPopover = showWalletImporter || showSignRequest; + const [errorTitle, setErrorTitle] = useState(''); + + const { txData } = useSelector((state) => { + return state.confirmTransaction; + }); + // the confirmTransaction's life cycle is not consistent with QR hardware wallet; + // the confirmTransaction will change after the previous tx is confirmed or cancel, + // we want to block the changing by sign request id; + const _txData = useMemo(() => { + return txData; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sign?.request?.requestId]); + + const dispatch = useDispatch(); + const walletImporterCancel = useCallback( + () => dispatch(cancelSyncQRHardwareAction()), + [dispatch], + ); + + const signRequestCancel = useCallback(() => { + let action = cancelTx; + switch (_txData.type) { + case MESSAGE_TYPE.PERSONAL_SIGN: { + action = cancelPersonalMsg; + break; + } + case MESSAGE_TYPE.ETH_SIGN: { + action = cancelMsg; + break; + } + case MESSAGE_TYPE.ETH_SIGN_TYPED_DATA: { + action = cancelTypedMsg; + break; + } + default: { + action = cancelTx; + } + } + dispatch(action(_txData)); + dispatch(cancelQRHardwareSignRequestAction()); + }, [dispatch, _txData]); + + const title = useMemo(() => { + let _title = ''; + if (showSignRequest) { + _title = t('QRHardwareSignRequestTitle'); + } else if (showWalletImporter) { + _title = t('QRHardwareWalletImporterTitle'); + } + if (errorTitle !== '') { + _title = errorTitle; + } + return _title; + }, [showSignRequest, showWalletImporter, t, errorTitle]); + return showPopover ? ( + + {showWalletImporter && ( + + )} + {showSignRequest && ( + + )} + + ) : null; +}; + +export default QRHardwarePopover; diff --git a/ui/components/app/qr-hardware-popover/qr-hardware-sign-request/index.js b/ui/components/app/qr-hardware-popover/qr-hardware-sign-request/index.js new file mode 100644 index 000000000..9a59357a1 --- /dev/null +++ b/ui/components/app/qr-hardware-popover/qr-hardware-sign-request/index.js @@ -0,0 +1,3 @@ +import QRHardwareSignRequest from './qr-hardware-sign-request.component'; + +export default QRHardwareSignRequest; diff --git a/ui/components/app/qr-hardware-popover/qr-hardware-sign-request/player.js b/ui/components/app/qr-hardware-popover/qr-hardware-sign-request/player.js new file mode 100644 index 000000000..c79dec547 --- /dev/null +++ b/ui/components/app/qr-hardware-popover/qr-hardware-sign-request/player.js @@ -0,0 +1,71 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import QRCode from 'qrcode.react'; +import { UR, UREncoder } from '@ngraveio/bc-ur'; +import PropTypes from 'prop-types'; +import Typography from '../../../ui/typography'; +import Box from '../../../ui/box'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { + ALIGN_ITEMS, + DISPLAY, + FLEX_DIRECTION, + TEXT_ALIGN, +} from '../../../../helpers/constants/design-system'; +import { PageContainerFooter } from '../../../ui/page-container'; + +const Player = ({ type, cbor, cancelQRHardwareSignRequest, toRead }) => { + const t = useI18nContext(); + const urEncoder = useMemo( + () => new UREncoder(new UR(Buffer.from(cbor, 'hex'), type), 400), + [cbor, type], + ); + const [currentQRCode, setCurrentQRCode] = useState(urEncoder.nextPart()); + useEffect(() => { + const id = setInterval(() => { + setCurrentQRCode(urEncoder.nextPart()); + }, 100); + return () => { + clearInterval(id); + }; + }, [urEncoder]); + + return ( + <> + + + {t('QRHardwareSignRequestSubtitle')} + + + + + + + + {t('QRHardwareSignRequestDescription')} + + + + + ); +}; + +Player.propTypes = { + type: PropTypes.string.isRequired, + cbor: PropTypes.string.isRequired, + cancelQRHardwareSignRequest: PropTypes.func.isRequired, + toRead: PropTypes.func.isRequired, +}; + +export default Player; diff --git a/ui/components/app/qr-hardware-popover/qr-hardware-sign-request/qr-hardware-sign-request.component.js b/ui/components/app/qr-hardware-popover/qr-hardware-sign-request/qr-hardware-sign-request.component.js new file mode 100644 index 000000000..efbc40729 --- /dev/null +++ b/ui/components/app/qr-hardware-popover/qr-hardware-sign-request/qr-hardware-sign-request.component.js @@ -0,0 +1,45 @@ +import React, { useCallback, useState } from 'react'; +import PropTypes from 'prop-types'; +import { submitQRHardwareSignature } from '../../../../store/actions'; +import Player from './player'; +import Reader from './reader'; + +const QRHardwareSignRequest = ({ request, handleCancel, setErrorTitle }) => { + const [status, setStatus] = useState('play'); + + const toRead = useCallback(() => setStatus('read'), []); + + const renderPlayer = () => { + const { payload } = request; + return ( + + ); + }; + + const renderReader = () => { + return ( + + ); + }; + + if (status === 'play') return renderPlayer(); + return renderReader(); +}; + +QRHardwareSignRequest.propTypes = { + request: PropTypes.object.isRequired, + handleCancel: PropTypes.func.isRequired, + setErrorTitle: PropTypes.func.isRequired, +}; + +export default QRHardwareSignRequest; diff --git a/ui/components/app/qr-hardware-popover/qr-hardware-sign-request/reader.js b/ui/components/app/qr-hardware-popover/qr-hardware-sign-request/reader.js new file mode 100644 index 000000000..2a5804a71 --- /dev/null +++ b/ui/components/app/qr-hardware-popover/qr-hardware-sign-request/reader.js @@ -0,0 +1,52 @@ +import React from 'react'; +import { ETHSignature } from '@keystonehq/bc-ur-registry-eth'; +import * as uuid from 'uuid'; +import PropTypes from 'prop-types'; +import BaseReader from '../base-reader'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; + +const Reader = ({ + submitQRHardwareSignature, + cancelQRHardwareSignRequest, + requestId, + setErrorTitle, +}) => { + const t = useI18nContext(); + const cancel = () => { + cancelQRHardwareSignRequest(); + }; + + const handleSuccess = async (ur) => { + if (ur.type === 'eth-signature') { + const ethSignature = ETHSignature.fromCBOR(ur.cbor); + const buffer = ethSignature.getRequestId(); + const signId = uuid.stringify(buffer); + if (signId === requestId) { + return await submitQRHardwareSignature(signId, ur.cbor.toString('hex')); + } + setErrorTitle(t('QRHardwareInvalidTransactionTitle')); + throw new Error(t('QRHardwareMismatchedSignId')); + } else { + setErrorTitle(t('QRHardwareInvalidTransactionTitle')); + throw new Error(t('unknownQrCode')); + } + }; + + return ( + + ); +}; + +Reader.propTypes = { + submitQRHardwareSignature: PropTypes.func.isRequired, + cancelQRHardwareSignRequest: PropTypes.func.isRequired, + requestId: PropTypes.string.isRequired, + setErrorTitle: PropTypes.func.isRequired, +}; + +export default Reader; diff --git a/ui/components/app/qr-hardware-popover/qr-hardware-wallet-importer/index.js b/ui/components/app/qr-hardware-popover/qr-hardware-wallet-importer/index.js new file mode 100644 index 000000000..55c7b34e2 --- /dev/null +++ b/ui/components/app/qr-hardware-popover/qr-hardware-wallet-importer/index.js @@ -0,0 +1,3 @@ +import QRHardwareWalletImporter from './qr-hardware-wallet-importer.component'; + +export default QRHardwareWalletImporter; diff --git a/ui/components/app/qr-hardware-popover/qr-hardware-wallet-importer/qr-hardware-wallet-importer.component.js b/ui/components/app/qr-hardware-popover/qr-hardware-wallet-importer/qr-hardware-wallet-importer.component.js new file mode 100644 index 000000000..0c5a6355b --- /dev/null +++ b/ui/components/app/qr-hardware-popover/qr-hardware-wallet-importer/qr-hardware-wallet-importer.component.js @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + submitQRHardwareCryptoAccount, + submitQRHardwareCryptoHDKey, +} from '../../../../store/actions'; +import BaseReader from '../base-reader'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; + +const QRHardwareWalletImporter = ({ handleCancel, setErrorTitle }) => { + const t = useI18nContext(); + const handleSuccess = async (ur) => { + if (ur.type === 'crypto-hdkey') { + return await submitQRHardwareCryptoHDKey(ur.cbor.toString('hex')); + } else if (ur.type === 'crypto-account') { + return await submitQRHardwareCryptoAccount(ur.cbor.toString('hex')); + } + setErrorTitle(t('QRHardwareUnknownQRCodeTitle')); + throw new Error(t('unknownQrCode')); + }; + + return ( + + ); +}; + +QRHardwareWalletImporter.propTypes = { + handleCancel: PropTypes.func.isRequired, + setErrorTitle: PropTypes.func.isRequired, +}; + +export default QRHardwareWalletImporter; diff --git a/ui/pages/create-account/connect-hardware/index.js b/ui/pages/create-account/connect-hardware/index.js index 6b6c3bab5..f95243b67 100644 --- a/ui/pages/create-account/connect-hardware/index.js +++ b/ui/pages/create-account/connect-hardware/index.js @@ -11,7 +11,10 @@ import { import { formatBalance } from '../../../helpers/utils/util'; import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { SECOND } from '../../../../shared/constants/time'; -import { LEDGER_TRANSPORT_TYPES } from '../../../../shared/constants/hardware-wallets'; +import { + DEVICE_NAMES, + LEDGER_TRANSPORT_TYPES, +} from '../../../../shared/constants/hardware-wallets'; import SelectHardware from './select-hardware'; import AccountList from './account-list'; @@ -76,7 +79,11 @@ class ConnectHardwareForm extends Component { } async checkIfUnlocked() { - for (const device of ['trezor', 'ledger', 'lattice']) { + for (const device of [ + DEVICE_NAMES.TREZOR, + DEVICE_NAMES.LEDGER, + DEVICE_NAMES.LATTICE, + ]) { const path = this.props.defaultHdPaths[device]; const unlocked = await this.props.checkHardwareStatus(device, path); if (unlocked) { @@ -176,9 +183,22 @@ class ConnectHardwareForm extends Component { this.setState({ error: this.context.t('ledgerTimeout'), }); + } else if ( + errorMessage + .toLowerCase() + .includes( + 'KeystoneError#pubkey_account.no_expected_account'.toLowerCase(), + ) + ) { + this.setState({ + error: this.context.t('QRHardwarePubkeyAccountOutOfRange'), + }); } else if ( errorMessage !== 'Window closed' && - errorMessage !== 'Popup closed' + errorMessage !== 'Popup closed' && + errorMessage + .toLowerCase() + .includes('KeystoneError#sync_cancel'.toLowerCase()) === false ) { this.setState({ error: errorMessage, diff --git a/ui/pages/create-account/connect-hardware/index.scss b/ui/pages/create-account/connect-hardware/index.scss index b796812cf..977b4af96 100644 --- a/ui/pages/create-account/connect-hardware/index.scss +++ b/ui/pages/create-account/connect-hardware/index.scss @@ -52,6 +52,7 @@ justify-content: center; border-radius: 5px; padding: 0; + margin-right: 15px; &__img { width: 95px; @@ -64,7 +65,6 @@ } &__btn:first-child { - margin-right: 15px; margin-left: 20px; } diff --git a/ui/pages/create-account/connect-hardware/select-hardware.js b/ui/pages/create-account/connect-hardware/select-hardware.js index 3f7e6cbe8..375b33f23 100644 --- a/ui/pages/create-account/connect-hardware/select-hardware.js +++ b/ui/pages/create-account/connect-hardware/select-hardware.js @@ -2,7 +2,10 @@ import classnames from 'classnames'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Button from '../../../components/ui/button'; -import { LEDGER_TRANSPORT_TYPES } from '../../../../shared/constants/hardware-wallets'; +import { + DEVICE_NAMES, + LEDGER_TRANSPORT_TYPES, +} from '../../../../shared/constants/hardware-wallets'; export default class SelectHardware extends Component { static contextTypes = { @@ -30,9 +33,9 @@ export default class SelectHardware extends Component { return ( + ); + } + renderButtons() { return ( <> @@ -89,6 +109,7 @@ export default class SelectHardware extends Component { style={{ margin: '10px 0 0 0' }} > {this.renderConnectToLatticeButton()} + {this.renderConnectToQRButton()} ); @@ -149,12 +170,14 @@ export default class SelectHardware extends Component { renderTutorialsteps() { switch (this.state.selectedDevice) { - case 'ledger': + case DEVICE_NAMES.LEDGER: return this.renderLedgerTutorialSteps(); - case 'trezor': + case DEVICE_NAMES.TREZOR: return this.renderTrezorTutorialSteps(); - case 'lattice': + case DEVICE_NAMES.LATTICE: return this.renderLatticeTutorialSteps(); + case DEVICE_NAMES.QR: + return this.renderQRHardwareWalletSteps(); default: return ''; } @@ -296,6 +319,65 @@ export default class SelectHardware extends Component { ); } + renderQRHardwareWalletSteps() { + const steps = []; + steps.push( + { + title: this.context.t('QRHardwareWalletSteps1Title'), + message: this.context.t('QRHardwareWalletSteps1Description'), + }, + { + message: ( + <> + + {this.context.t('keystone')} + + + {this.context.t('keystoneTutorial')} + + + ), + }, + { + message: this.context.t('QRHardwareWalletSteps2Description'), + }, + { + asset: 'qrcode-wallet-demo', + dimensions: { width: '225px', height: '75px' }, + }, + ); + return ( +
+ {steps.map((step, index) => ( +
+ {step.title &&

{step.title}

} +

{step.message}

+ {step.asset && ( + + )} +
+ ))} +
+ ); + } + renderConnectScreen() { return (
diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index ba99b1035..dc0f9b74b 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -91,6 +91,7 @@ export default class Home extends PureComponent { seedPhraseBackedUp: PropTypes.bool.isRequired, newNetworkAdded: PropTypes.string, setNewNetworkAdded: PropTypes.func.isRequired, + isSigningQRHardwareTransaction: PropTypes.bool.isRequired, }; state = { @@ -99,7 +100,7 @@ export default class Home extends PureComponent { canShowBlockageNotification: true, }; - componentDidMount() { + checkStatusAndNavigate() { const { firstPermissionsRequestId, history, @@ -111,11 +112,13 @@ export default class Home extends PureComponent { showAwaitingSwapScreen, swapsFetchParams, pendingConfirmations, + isSigningQRHardwareTransaction, } = this.props; - - // eslint-disable-next-line react/no-unused-state - this.setState({ mounted: true }); - if (isNotification && totalUnapprovedCount === 0) { + if ( + isNotification && + totalUnapprovedCount === 0 && + !isSigningQRHardwareTransaction + ) { global.platform.closeCurrentWindow(); } else if (!isNotification && showAwaitingSwapScreen) { history.push(AWAITING_SWAP_ROUTE); @@ -134,6 +137,12 @@ export default class Home extends PureComponent { } } + componentDidMount() { + // eslint-disable-next-line react/no-unused-state + this.setState({ mounted: true }); + this.checkStatusAndNavigate(); + } + static getDerivedStateFromProps( { firstPermissionsRequestId, @@ -144,11 +153,16 @@ export default class Home extends PureComponent { haveSwapsQuotes, showAwaitingSwapScreen, swapsFetchParams, + isSigningQRHardwareTransaction, }, { mounted }, ) { if (!mounted) { - if (isNotification && totalUnapprovedCount === 0) { + if ( + isNotification && + totalUnapprovedCount === 0 && + !isSigningQRHardwareTransaction + ) { return { closing: true }; } else if ( firstPermissionsRequestId || @@ -169,12 +183,15 @@ export default class Home extends PureComponent { showRestorePrompt, threeBoxLastUpdated, threeBoxSynced, + isNotification, } = this.props; if (!prevState.closing && this.state.closing) { global.platform.closeCurrentWindow(); } + isNotification && this.checkStatusAndNavigate(); + if (threeBoxSynced && showRestorePrompt && threeBoxLastUpdated === null) { setupThreeBox(); } diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js index 7fad95d83..7eb03edd9 100644 --- a/ui/pages/home/home.container.js +++ b/ui/pages/home/home.container.js @@ -16,6 +16,8 @@ import { getSortedNotificationsToShow, getShowRecoveryPhraseReminder, getNewNetworkAdded, + hasUnsignedQRHardwareTransaction, + hasUnsignedQRHardwareMessage, } from '../../selectors'; import { @@ -83,6 +85,10 @@ const mapStateToProps = (state) => { getWeb3ShimUsageStateForOrigin(state, originOfCurrentTab) === WEB3_SHIM_USAGE_ALERT_STATES.RECORDED; + const isSigningQRHardwareTransaction = + hasUnsignedQRHardwareTransaction(state) || + hasUnsignedQRHardwareMessage(state); + return { forgottenPassword, suggestedAssets, @@ -115,6 +121,7 @@ const mapStateToProps = (state) => { showRecoveryPhraseReminder: getShowRecoveryPhraseReminder(state), seedPhraseBackedUp, newNetworkAdded: getNewNetworkAdded(state), + isSigningQRHardwareTransaction, }; }; diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index 8e4ca834f..7e205f654 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -66,6 +66,7 @@ import { import { getEnvironmentType } from '../../../app/scripts/lib/util'; import ConfirmationPage from '../confirmation'; import OnboardingFlow from '../onboarding-flow/onboarding-flow'; +import QRHardwarePopover from '../../components/app/qr-hardware-popover'; export default class Routes extends Component { static propTypes = { @@ -321,6 +322,7 @@ export default class Routes extends Component { } }} > + {!this.hideAppHeader() && ( diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index cc8283262..200aff842 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -49,6 +49,7 @@ import { getLedgerWebHidConnectedStatus, getLedgerTransportStatus, } from '../ducks/app/app'; +import { MESSAGE_TYPE } from '../../shared/constants/app'; /** * One of the only remaining valid uses of selecting the network subkey of the @@ -82,6 +83,48 @@ export function getCurrentChainId(state) { return chainId; } +export function getCurrentQRHardwareState(state) { + const { qrHardware } = state.metamask; + return qrHardware || {}; +} + +export function hasUnsignedQRHardwareTransaction(state) { + const { txParams } = state.confirmTransaction.txData; + if (!txParams) return false; + const { from } = txParams; + const { keyrings } = state.metamask; + const qrKeyring = keyrings.find((kr) => kr.type === KEYRING_TYPES.QR); + if (!qrKeyring) return false; + return Boolean( + qrKeyring.accounts.find( + (account) => account.toLowerCase() === from.toLowerCase(), + ), + ); +} + +export function hasUnsignedQRHardwareMessage(state) { + const { type, msgParams } = state.confirmTransaction.txData; + if (!type || !msgParams) { + return false; + } + const { from } = msgParams; + const { keyrings } = state.metamask; + const qrKeyring = keyrings.find((kr) => kr.type === KEYRING_TYPES.QR); + if (!qrKeyring) return false; + switch (type) { + case MESSAGE_TYPE.ETH_SIGN_TYPED_DATA: + case MESSAGE_TYPE.ETH_SIGN: + case MESSAGE_TYPE.PERSONAL_SIGN: + return Boolean( + qrKeyring.accounts.find( + (account) => account.toLowerCase() === from.toLowerCase(), + ), + ); + default: + return false; + } +} + export function getCurrentKeyring(state) { const identity = getSelectedIdentity(state); diff --git a/ui/store/actions.js b/ui/store/actions.js index 1e43ac103..d9f8f7931 100644 --- a/ui/store/actions.js +++ b/ui/store/actions.js @@ -29,6 +29,7 @@ import { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-accoun import { getUnconnectedAccountAlertEnabledness } from '../ducks/metamask/metamask'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { + DEVICE_NAMES, LEDGER_TRANSPORT_TYPES, LEDGER_USB_VENDOR_ID, } from '../../shared/constants/hardware-wallets'; @@ -414,7 +415,7 @@ export function connectHardware(deviceName, page, hdPath, t) { await promisifiedBackground.establishLedgerTransportPreference(); } if ( - deviceName === 'ledger' && + deviceName === DEVICE_NAMES.LEDGER && ledgerTransportType === LEDGER_TRANSPORT_TYPES.WEBHID ) { const connectedDevices = await window.navigator.hid.requestDevice({ @@ -443,7 +444,8 @@ export function connectHardware(deviceName, page, hdPath, t) { dispatch(displayWarning(t('ledgerDeviceOpenFailureMessage'))); throw new Error(t('ledgerDeviceOpenFailureMessage')); } else { - dispatch(displayWarning(error.message)); + if (deviceName !== DEVICE_NAMES.QR) + dispatch(displayWarning(error.message)); throw error; } } finally { @@ -2989,3 +2991,30 @@ export async function detectNewTokens() { export function hideTestNetMessage() { return promisifiedBackground.setShowTestnetMessageInDropdown(false); } + +// QR Hardware Wallets +export async function submitQRHardwareCryptoHDKey(cbor) { + await promisifiedBackground.submitQRHardwareCryptoHDKey(cbor); +} + +export async function submitQRHardwareCryptoAccount(cbor) { + await promisifiedBackground.submitQRHardwareCryptoAccount(cbor); +} + +export function cancelSyncQRHardware() { + return async (dispatch) => { + dispatch(hideLoadingIndication()); + await promisifiedBackground.cancelSyncQRHardware(); + }; +} + +export async function submitQRHardwareSignature(requestId, cbor) { + await promisifiedBackground.submitQRHardwareSignature(requestId, cbor); +} + +export function cancelQRHardwareSignRequest() { + return async (dispatch) => { + dispatch(hideLoadingIndication()); + await promisifiedBackground.cancelQRHardwareSignRequest(); + }; +} diff --git a/yarn.lock b/yarn.lock index 91c307eb1..737fb8b61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -62,6 +62,11 @@ resolved "https://registry.yarnpkg.com/@agoric/transform-module/-/transform-module-0.4.1.tgz#9fb152364faf372e1bda535cb4ef89717724f57c" integrity sha512-4TJJHXeXAWu1FCA7yXCAZmhBNoGTB/BEAe2pv+J2X8W/mJTr9b395OkDCSRMpzvmSshLfBx6wT0D7dqWIWEC1w== +"@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.10.4", "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.5.5": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" @@ -2423,7 +2428,7 @@ ethers "^5.4.5" lodash "^4.17.21" -"@ethereumjs/common@^2.3.1", "@ethereumjs/common@^2.4.0": +"@ethereumjs/common@^2.0.0", "@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" integrity sha512-UdkhFWzWcJCZVsj1O/H8/oqj/0RVYjLc1OhPjBrQdALAkQHpCp8xXI4WLnuGTADqTdJZww0NtgwG+TRPkXt27w== @@ -2431,6 +2436,14 @@ crc-32 "^1.2.0" ethereumjs-util "^7.1.0" +"@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.1.1", "@ethereumjs/tx@^3.1.4", "@ethereumjs/tx@^3.2.0", "@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" @@ -3943,6 +3956,58 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@keystonehq/base-eth-keyring@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@keystonehq/base-eth-keyring/-/base-eth-keyring-0.3.1.tgz#c985803f7083f0a2e6ea55846905099c46573142" + integrity sha512-lbVLCMD3R4Ki8CThctZOjafKvJn0p2u19csuMrJHBlFllqu88vYoyfv3I/BPtOpnWqeC90Kta23w68FFUnV8Zg== + dependencies: + "@ethereumjs/tx" "3.0.0" + "@keystonehq/bc-ur-registry-eth" "^0.7.5" + ethereumjs-util "^7.0.8" + hdkey "^2.0.1" + uuid "^8.3.2" + +"@keystonehq/bc-ur-registry-eth@^0.6.8": + version "0.6.13" + resolved "https://registry.yarnpkg.com/@keystonehq/bc-ur-registry-eth/-/bc-ur-registry-eth-0.6.13.tgz#c1680930b1d3fed14857336bd4fb47a484dfac32" + integrity sha512-sQQMMiKlacxMOIGeH8l/m/j3sL2VaM7Zid/xvf6cogZ5EZ5pa8Jow8cgY/t7krTOOBp81/GglCbwCGC8RIOLqA== + 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.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@keystonehq/bc-ur-registry-eth/-/bc-ur-registry-eth-0.7.5.tgz#30a146e2b6ba01f73380530bbb6bd6a62d540a8b" + integrity sha512-9WcIe4WcqJxf/HKxKhnOBgEfre8/BB5Zi68iHFdw/pyfdYBfzU/nAn2/NB/ggqIHNGWO4zsRnBk85vbJ3QwQsQ== + 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@^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/metamask-airgapped-keyring@0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@keystonehq/metamask-airgapped-keyring/-/metamask-airgapped-keyring-0.2.1.tgz#d6a8dd75d97cf7911faa8c2a8b19a0168b74891e" + integrity sha512-LTBGLR8KaJycZLG9igOoIi1tdM2CDN07+dXVGHYnls6DWDN8v3DPzOeAuu1+7H+NDIZYUhGmaa1RcbBT3lY+Uw== + dependencies: + "@ethereumjs/tx" "^3.3.0" + "@keystonehq/base-eth-keyring" "^0.3.1" + "@keystonehq/bc-ur-registry-eth" "^0.7.5" + "@metamask/obs-store" "^7.0.0" + rlp "^2.2.6" + uuid "^8.3.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" @@ -4341,6 +4406,14 @@ readable-stream "^2.2.2" through2 "^2.0.3" +"@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/post-message-stream@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@metamask/post-message-stream/-/post-message-stream-4.0.0.tgz#72f120e562346ca86ccc9b3684023ad44265f0df" @@ -4389,6 +4462,19 @@ resolved "https://registry.yarnpkg.com/@multiformats/base-x/-/base-x-4.0.1.tgz#95ff0fa58711789d53aefb2590a8b7a4e715d121" integrity sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw== +"@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.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" @@ -6303,7 +6389,14 @@ resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== -"@zxing/library@^0.8.0": +"@zxing/browser@^0.0.10": + version "0.0.10" + resolved "https://registry.yarnpkg.com/@zxing/browser/-/browser-0.0.10.tgz#63c0a762fc2fd4ee946a20953ef24fab225698a9" + integrity sha512-P2wQc5fs+cjSc39zFS4UDhejWqdikf4FjuWIlFrzXD8fOsZ4ASfmLDKGeg7mRgmJq11oMKcVXvFFI6kcIKtxuQ== + optionalDependencies: + "@zxing/text-encoding" "^0.9.0" + +"@zxing/library@0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@zxing/library/-/library-0.8.0.tgz#accd9f3cd5c06fa40a95c2c1f61398c41548a9e3" integrity sha512-D7oopukr7cJ0Va01Er2zXiSPXvmvc6D1PpOq/THRvd/57yEsBs+setRsiDo7tSRnYHcw7FrRZSZ7rwyzNSLJeA== @@ -6312,7 +6405,7 @@ optionalDependencies: text-encoding "^0.6.4" -"@zxing/text-encoding@0.9.0": +"@zxing/text-encoding@0.9.0", "@zxing/text-encoding@^0.9.0": version "0.9.0" resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b" integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA== @@ -7268,6 +7361,16 @@ assert@^1.1.1, assert@^1.4.0, assert@^1.4.1: object-assign "^4.1.1" 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" + assertion-error@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c" @@ -8265,6 +8368,11 @@ base-x@3.0.8, base-x@^3.0.2, base-x@^3.0.8: dependencies: safe-buffer "^5.0.1" +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= + base32-encode@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/base32-encode/-/base32-encode-1.1.1.tgz#d022d86aca0002a751bbe1bf20eb4a9b1cef4e95" @@ -8282,6 +8390,13 @@ base32.js@~0.1.0: resolved "https://registry.yarnpkg.com/base32.js/-/base32.js-0.1.0.tgz#b582dec693c2f11e893cf064ee6ac5b6131a2202" integrity sha1-tYLexpPC8R6JPPBk7mrFthMaIgI= +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-arraybuffer@0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812" @@ -9055,6 +9170,13 @@ bs58@^2.0.1: resolved "https://registry.yarnpkg.com/bs58/-/bs58-2.0.1.tgz#55908d58f1982aba2008fa1bed8f91998a29bf8d" integrity sha1-VZCNWPGYKrogCPob7Y+RmYopv40= +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" + bs58check@2.1.2, bs58check@<3.0.0, bs58check@^2.0.0, bs58check@^2.1.1, bs58check@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" @@ -9154,7 +9276,7 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.0.5, buffer@^5.2.1, buffer@^5.4.2, buffer@^5.5.0, buffer@^5.6.0: +buffer@^5.0.5, buffer@^5.1.0, buffer@^5.2.1, buffer@^5.4.2, 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== @@ -10548,6 +10670,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" + crdts@~0.1.2: version "0.1.5" resolved "https://registry.yarnpkg.com/crdts/-/crdts-0.1.5.tgz#89413e8adfc3ab943300a890ee6392db5ba60c06" @@ -12346,6 +12475,11 @@ es6-map@^0.1.3, es6-map@^0.1.5: es6-symbol "~3.1.1" event-emitter "~0.3.5" +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-promise@^4.2.8: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" @@ -13327,7 +13461,7 @@ ethereumjs-util@^5.0.0, ethereumjs-util@^5.0.1, ethereumjs-util@^5.1.1, ethereum safe-buffer "^5.1.1" secp256k1 "^3.0.1" -ethereumjs-util@^7.0.10, ethereumjs-util@^7.0.2, ethereumjs-util@^7.0.9, ethereumjs-util@^7.1.0: +ethereumjs-util@^7.0.10, ethereumjs-util@^7.0.2, ethereumjs-util@^7.0.7, ethereumjs-util@^7.0.8, ethereumjs-util@^7.0.9, ethereumjs-util@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.0.tgz#e2b43a30bfcdbcb432a4eb42bd5f2393209b3fd5" integrity sha512-kR+vhu++mUDARrsMMhsjjzPduRVAeundLGXucGRHF3B4oEltOUspfgCVco4kckucj3FMlLaZHUl9n7/kdmr6Tw== @@ -16146,6 +16280,15 @@ hdkey@0.8.0: safe-buffer "^5.1.1" secp256k1 "^3.0.1" +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" + he@1.2.0, he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -17746,6 +17889,14 @@ is-my-json-valid@^2.10.0: jsonpointer "^4.0.0" xtend "^4.0.0" +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-negated-glob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" @@ -18731,6 +18882,11 @@ jsan@^3.1.13: resolved "https://registry.yarnpkg.com/jsan/-/jsan-3.1.13.tgz#4de8c7bf8d1cfcd020c313d438f930cec4b91d86" integrity sha512-9kGpCsGHifmw6oJet+y8HaCl14y7qgAsxVdV3pCHDySNR3BfDC30zgkssd7x5LRVAT22dnpbe9JdzzmXZnq9/g== +jsbi@^3.1.5: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsbi/-/jsbi-3.2.0.tgz#3500a08fb3e8e56cf0439964fc774a8762b151ed" + integrity sha512-nL7F2gCfPTXLRoS1ZABhzyYCib6L4bAjX9F6qutL4L2o0r+gDndWVlQ7A6bMa80RTN53R82hXTm6FRsdRxbLgQ== + jsbn@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" @@ -24851,11 +25007,25 @@ pushdata-bitcoin@^1.0.1: dependencies: bitcoin-ops "^1.3.0" +qr.js@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f" + integrity sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8= + qrcode-generator@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/qrcode-generator/-/qrcode-generator-1.4.1.tgz#bfb6760e05d12c39df8acd60a0d459bdb2fa0756" integrity sha512-KOdSAyFBPf0/5Z3mra4JfSbjrDlUn2J3YH8Rm33tRGbptxP4vhogLWysvkQp8mp5ix9u80Wfr4vxHXTeR9o0Ug== +qrcode.react@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-1.0.1.tgz#2834bb50e5e275ffe5af6906eff15391fe9e38a5" + integrity sha512-8d3Tackk8IRLXTo67Y+c1rpaiXjoz/Dd2HpcMdW//62/x8J1Nbho14Kh8x974t9prsLHN6XqVgcnRiBGFptQmg== + dependencies: + loose-envify "^1.4.0" + prop-types "^15.6.0" + qr.js "0.0.0" + qs@6.7.0, qs@^6.4.0, qs@^6.5.1, qs@^6.5.2: version "6.7.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" @@ -26859,7 +27029,7 @@ scss-parser@^1.0.4: dependencies: invariant "2.2.4" -secp256k1@4.0.2, secp256k1@^4.0.1: +secp256k1@4.0.2, secp256k1@^4.0.0, secp256k1@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.2.tgz#15dd57d0f0b9fdb54ac1fa1694f40e5e9a54f4a1" integrity sha512-UDar4sKvWAksIlfX3xIaQReADn+WFnHvbVujpcbr+9Sf/69odMwy2MUsz5CKLQgX9nsIyrjuxL2imVyoNHa3fg== @@ -27096,7 +27266,7 @@ setprototypeof@1.1.1: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== -sha.js@^2.4.0, sha.js@^2.4.8, sha.js@~2.4.4: +sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8, sha.js@~2.4.4: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== @@ -29161,9 +29331,9 @@ truncate-utf8-bytes@^1.0.0: utf8-byte-length "^1.0.1" ts-custom-error@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ts-custom-error/-/ts-custom-error-2.2.1.tgz#47086fbc34df5c7c2d4fba8c92d8767662066951" - integrity sha512-lHKZtU+PXkVuap6nlFZybIAFLUO8B3jbCs1VynBL8AUSAHfeG6HpztcBTDRp5I+fN5820N9kGg+eTIvr+le2yg== + version "2.2.2" + resolved "https://registry.yarnpkg.com/ts-custom-error/-/ts-custom-error-2.2.2.tgz#ee769cd6a9cf35dc2e9fedefbb3842f3a2fbceae" + integrity sha512-I0FEdfdatDjeigRqh1JFj67bcIKyRNm12UVGheBjs2pXgyELg2xeiQLVaWu1pVmNGXZVnz/fvycSU41moBIpOg== ts-dedent@^2.0.0: version "2.0.0" @@ -29905,7 +30075,7 @@ util@^0.11.0: dependencies: inherits "2.0.3" -util@^0.12.3, util@~0.12.0: +util@^0.12.0, util@^0.12.3, util@~0.12.0: version "0.12.4" resolved "https://registry.yarnpkg.com/util/-/util-0.12.4.tgz#66121a31420df8f01ca0c464be15dfa1d1850253" integrity sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==