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 @@
+
+
\ 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 @@
+
+
\ 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 (
+
+
+ {canplay ? null : }
+
+ );
+};
+
+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==