From bb520f907ab08c2f6a18f461971e5874f91bc1c8 Mon Sep 17 00:00:00 2001 From: Matheus Degiovani Date: Tue, 10 Jul 2018 17:19:58 -0300 Subject: [PATCH] Trezor Setup Page --- app/actions/TrezorActions.js | 225 +++++++++++++++++- app/components/SideBar/MenuLinks/index.js | 7 +- app/components/modals/trezor/Modals.js | 6 + app/components/modals/trezor/WordModal.js | 79 ++++++ .../views/TrezorPage/ChangeLabel.js | 57 +++++ .../views/TrezorPage/ConfigButtons.js | 45 ++++ .../views/TrezorPage/FirmwareUpdate.js | 54 +++++ app/components/views/TrezorPage/Header.js | 9 + .../views/TrezorPage/NoDevicePage.js | 11 + app/components/views/TrezorPage/Page.js | 23 ++ .../views/TrezorPage/RecoveryButtons.js | 48 ++++ app/components/views/TrezorPage/index.js | 82 +++++++ app/connectors/routing.js | 1 + app/connectors/trezor.js | 11 + app/containers/Wallet.js | 2 + app/helpers/trezor.js | 209 ++++++++++++++++ .../docs/en/Warnings/TrezorFirmwareUpdate.md | 8 + app/i18n/docs/en/Warnings/TrezorWipe.md | 8 + app/i18n/docs/en/index.js | 2 + app/index.js | 3 + app/reducers/snackbar.js | 82 +++++++ app/reducers/trezor.js | 66 +++++ app/selectors.js | 2 + app/style/Header.less | 1 + app/style/Icons.less | 3 + app/style/MiscComponents.less | 12 + app/style/Trezor.less | 32 +++ app/style/icons/trezor-active.png | Bin 0 -> 2335 bytes app/style/icons/trezor-default.png | Bin 0 -> 870 bytes app/style/icons/trezor-hover.png | Bin 0 -> 2330 bytes 30 files changed, 1086 insertions(+), 2 deletions(-) create mode 100644 app/components/modals/trezor/WordModal.js create mode 100644 app/components/views/TrezorPage/ChangeLabel.js create mode 100644 app/components/views/TrezorPage/ConfigButtons.js create mode 100644 app/components/views/TrezorPage/FirmwareUpdate.js create mode 100644 app/components/views/TrezorPage/Header.js create mode 100644 app/components/views/TrezorPage/NoDevicePage.js create mode 100644 app/components/views/TrezorPage/Page.js create mode 100644 app/components/views/TrezorPage/RecoveryButtons.js create mode 100644 app/components/views/TrezorPage/index.js create mode 100644 app/helpers/trezor.js create mode 100644 app/i18n/docs/en/Warnings/TrezorFirmwareUpdate.md create mode 100644 app/i18n/docs/en/Warnings/TrezorWipe.md create mode 100644 app/style/icons/trezor-active.png create mode 100644 app/style/icons/trezor-default.png create mode 100644 app/style/icons/trezor-hover.png diff --git a/app/actions/TrezorActions.js b/app/actions/TrezorActions.js index 6934f4f469..edfe913fb7 100644 --- a/app/actions/TrezorActions.js +++ b/app/actions/TrezorActions.js @@ -2,9 +2,11 @@ import * as trezorjs from "trezor.js"; import trezorTransports from "trezor-link"; import * as wallet from "wallet"; import * as selectors from "selectors"; +import fs from "fs"; import { sprintf } from "sprintf-js"; import { rawHashToHex, rawToHex, hexToRaw } from "helpers"; import { publishTransactionAttempt } from "./ControlActions"; +import { model1_decred_homescreen } from "helpers/trezor"; import { EXTERNALREQUEST_TREZOR_BRIDGE } from "main_dev/externalRequests"; import { SIGNTX_ATTEMPT, SIGNTX_FAILED, SIGNTX_SUCCESS } from "./ControlActions"; @@ -121,6 +123,9 @@ export const TRZ_PIN_CANCELED = "TRZ_PIN_CANCELED"; export const TRZ_PASSPHRASE_REQUESTED = "TRZ_PASSPHRASE_REQUESTED"; export const TRZ_PASSPHRASE_ENTERED = "TRZ_PASSPHRASE_ENTERED"; export const TRZ_PASSPHRASE_CANCELED = "TRZ_PASSPHRASE_CANCELED"; +export const TRZ_WORD_REQUESTED = "TRZ_WORD_REQUESTED"; +export const TRZ_WORD_ENTERED = "TRZ_WORD_ENTERED"; +export const TRZ_WORD_CANCELED = "TRZ_WORD_CANCELED"; function setDeviceListeners(device, dispatch) { device.on("pin", (pinMessage, pinCallBack) => { @@ -130,6 +135,10 @@ function setDeviceListeners(device, dispatch) { device.on("passphrase", (passPhraseCallBack) => { dispatch({ passPhraseCallBack, type: TRZ_PASSPHRASE_REQUESTED }); }); + + device.on("word", (wordCallBack) => { + dispatch({ wordCallBack, type: TRZ_WORD_REQUESTED }); + }); } // deviceRun is the main function for executing trezor operations. This handles @@ -177,12 +186,13 @@ export const TRZ_CANCELOPERATION_FAILED = "TRZ_CANCELOPERATION_FAILED"; // than decrediton*. export const cancelCurrentOperation = () => async (dispatch, getState) => { const device = selectors.trezorDevice(getState()); - const { trezor: { pinCallBack, passPhraseCallBack } } = getState(); + const { trezor: { pinCallBack, passPhraseCallBack, wordCallBack } } = getState(); if (!device) return; try { if (pinCallBack) await pinCallBack("cancelled", null); else if (passPhraseCallBack) await passPhraseCallBack("cancelled", null); + else if (wordCallBack) await wordCallBack("cancelled", null); else await device.steal(); dispatch({ type: TRZ_CANCELOPERATION_SUCCESS }); @@ -205,6 +215,13 @@ export const submitPassPhrase = (passPhrase) => (dispatch, getState) => { passPhraseCallBack(null, passPhrase); }; +export const submitWord = (word) => (dispatch, getState) => { + const { trezor: { wordCallBack } } = getState(); + dispatch({ type: TRZ_WORD_ENTERED }); + if (!wordCallBack) return; + wordCallBack(null, word); +}; + // checkTrezorIsDcrwallet verifies whether the wallet currently running on // dcrwallet (presumably a watch only wallet created from a trezor provided // xpub) is the same wallet as the one of the currently connected trezor. This @@ -402,3 +419,209 @@ export function walletTxToRefTx(tx) { return txInfo; } + +export const TRZ_TOGGLEPINPROTECTION_ATTEMPT = "TRZ_TOGGLEPINPROTECTION_ATTEMPT"; +export const TRZ_TOGGLEPINPROTECTION_FAILED = "TRZ_TOGGLEPINPROTECTION_FAILED"; +export const TRZ_TOGGLEPINPROTECTION_SUCCESS = "TRZ_TOGGLEPINPROTECTION_SUCCESS"; + +export const togglePinProtection = () => async (dispatch, getState) => { + + const device = selectors.trezorDevice(getState()); + if (!device) { + dispatch({ error: "Device not connected", type: TRZ_TOGGLEPINPROTECTION_FAILED }); + return; + } + + const clearProtection = !!device.features.pin_protection; + dispatch({ clearProtection, type: TRZ_TOGGLEPINPROTECTION_ATTEMPT }); + + try { + await deviceRun(dispatch, getState, device, async session => { + await session.changePin(clearProtection); + }); + dispatch({ clearProtection, deviceLabel: device.features.label, type: TRZ_TOGGLEPINPROTECTION_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_TOGGLEPINPROTECTION_FAILED }); + } +}; + +export const TRZ_TOGGLEPASSPHRASEPROTECTION_ATTEMPT = "TRZ_TOGGLEPASSPHRASEPROTECTION_ATTEMPT"; +export const TRZ_TOGGLEPASSPHRASEPROTECTION_FAILED = "TRZ_TOGGLEPASSPHRASEPROTECTION_FAILED"; +export const TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS = "TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS"; + +export const togglePassPhraseProtection = () => async (dispatch, getState) => { + + const device = selectors.trezorDevice(getState()); + if (!device) { + dispatch({ error: "Device not connected", type: TRZ_TOGGLEPASSPHRASEPROTECTION_FAILED }); + return; + } + + const enableProtection = !device.features.passphrase_protection; + dispatch({ enableProtection, type: TRZ_TOGGLEPASSPHRASEPROTECTION_ATTEMPT }); + + try { + await deviceRun(dispatch, getState, device, async session => { + await session.togglePassphrase(enableProtection); + }); + dispatch({ enableProtection, deviceLabel: device.features.label, type: TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_TOGGLEPASSPHRASEPROTECTION_FAILED }); + } +}; + +export const TRZ_CHANGEHOMESCREEN_ATTEMPT = "TRZ_CHANGEHOMESCREEN_ATTEMPT"; +export const TRZ_CHANGEHOMESCREEN_FAILED = "TRZ_CHANGEHOMESCREEN_FAILED"; +export const TRZ_CHANGEHOMESCREEN_SUCCESS = "TRZ_CHANGEHOMESCREEN_SUCCESS"; + +export const changeToDecredHomeScreen = () => async (dispatch, getState) => { + const device = selectors.trezorDevice(getState()); + if (!device) { + dispatch({ error: "Device not connected", type: TRZ_TOGGLEPASSPHRASEPROTECTION_FAILED }); + return; + } + + dispatch({ type: TRZ_CHANGEHOMESCREEN_ATTEMPT }); + + try { + await deviceRun(dispatch, getState, device, async session => { + await session.changeHomescreen(model1_decred_homescreen); + }); + dispatch({ type: TRZ_CHANGEHOMESCREEN_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_CHANGEHOMESCREEN_FAILED }); + } +}; + +export const TRZ_CHANGELABEL_ATTEMPT = "TRZ_CHANGELABEL_ATTEMPT"; +export const TRZ_CHANGELABEL_FAILED = "TRZ_CHANGELABEL_FAILED"; +export const TRZ_CHANGELABEL_SUCCESS = "TRZ_CHANGELABEL_SUCCESS"; + +export const changeLabel = (label) => async (dispatch, getState) => { + const device = selectors.trezorDevice(getState()); + if (!device) { + dispatch({ error: "Device not connected", type: TRZ_CHANGELABEL_FAILED }); + return; + } + + dispatch({ type: TRZ_CHANGELABEL_ATTEMPT }); + + try { + await deviceRun(dispatch, getState, device, async session => { + await session.changeLabel(label); + }); + dispatch({ label, type: TRZ_CHANGELABEL_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_CHANGELABEL_FAILED }); + } +}; + +export const TRZ_WIPEDEVICE_ATTEMPT = "TRZ_WIPEDEVICE_ATTEMPT"; +export const TRZ_WIPEDEVICE_FAILED = "TRZ_WIPEDEVICE_FAILED"; +export const TRZ_WIPEDEVICE_SUCCESS = "TRZ_WIPEDEVICE_SUCCESS"; + +export const wipeDevice = () => async (dispatch, getState) => { + const device = selectors.trezorDevice(getState()); + if (!device) { + dispatch({ error: "Device not connected", type: TRZ_WIPEDEVICE_FAILED }); + return; + } + + dispatch({ type: TRZ_WIPEDEVICE_ATTEMPT }); + + try { + await deviceRun(dispatch, getState, device, async session => { + await session.wipeDevice(); + }); + dispatch({ type: TRZ_WIPEDEVICE_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_WIPEDEVICE_FAILED }); + } +}; + +export const TRZ_RECOVERDEVICE_ATTEMPT = "TRZ_RECOVERDEVICE_ATTEMPT"; +export const TRZ_RECOVERDEVICE_FAILED = "TRZ_RECOVERDEVICE_FAILED"; +export const TRZ_RECOVERDEVICE_SUCCESS = "TRZ_RECOVERDEVICE_SUCCESS"; + +export const recoverDevice = () => async (dispatch, getState) => { + const device = selectors.trezorDevice(getState()); + if (!device) { + dispatch({ error: "Device not connected", type: TRZ_RECOVERDEVICE_FAILED }); + return; + } + + dispatch({ type: TRZ_RECOVERDEVICE_ATTEMPT }); + + try { + await deviceRun(dispatch, getState, device, async session => { + const settings = { + word_count: 24, // FIXED at 24 (256 bits) + passphrase_protection: false, + pin_protection: false, + label: "New DCR Trezor", + dry_run: false, + }; + + await session.recoverDevice(settings); + }); + dispatch({ type: TRZ_RECOVERDEVICE_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_RECOVERDEVICE_FAILED }); + } +}; + +export const TRZ_INITDEVICE_ATTEMPT = "TRZ_INITDEVICE_ATTEMPT"; +export const TRZ_INITDEVICE_FAILED = "TRZ_INITDEVICE_FAILED"; +export const TRZ_INITDEVICE_SUCCESS = "TRZ_INITDEVICE_SUCCESS"; + +export const initDevice = () => async (dispatch, getState) => { + const device = selectors.trezorDevice(getState()); + if (!device) { + dispatch({ error: "Device not connected", type: TRZ_RECOVERDEVICE_FAILED }); + return; + } + + dispatch({ type: TRZ_INITDEVICE_ATTEMPT }); + + try { + await deviceRun(dispatch, getState, device, async session => { + const settings = { + strength: 256, // 24 words + passphrase_protection: false, + pin_protection: false, + label: "New DCR Trezor", + }; + + await session.resetDevice(settings); + }); + dispatch({ type: TRZ_INITDEVICE_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_INITDEVICE_FAILED }); + } +}; + +export const TRZ_UPDATEFIRMWARE_ATTEMPT = "TRZ_UPDATEFIRMWARE_ATTEMPT"; +export const TRZ_UPDATEFIRMWARE_FAILED = "TRZ_UPDATEFIRMWARE_FAILED"; +export const TRZ_UPDATEFIRMWARE_SUCCESS = "TRZ_UPDATEFIRMWARE_SUCCESS"; + +export const updateFirmware = (path) => async (dispatch, getState) => { + const device = selectors.trezorDevice(getState()); + if (!device) { + dispatch({ error: "Device not connected", type: TRZ_UPDATEFIRMWARE_FAILED }); + return; + } + + dispatch({ type: TRZ_UPDATEFIRMWARE_ATTEMPT }); + + try { + const rawFirmware = fs.readFileSync(path); + const hexFirmware = rawToHex(rawFirmware); + + await deviceRun(dispatch, getState, device, async session => { + await session.updateFirmware(hexFirmware); + }); + dispatch({ type: TRZ_UPDATEFIRMWARE_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_UPDATEFIRMWARE_FAILED }); + } +}; diff --git a/app/components/SideBar/MenuLinks/index.js b/app/components/SideBar/MenuLinks/index.js index 3a45be9baa..806506aa78 100644 --- a/app/components/SideBar/MenuLinks/index.js +++ b/app/components/SideBar/MenuLinks/index.js @@ -30,6 +30,11 @@ class MenuLinks extends React.Component { const idx = linkList.findIndex(l => l.path === "/governance"); idx === -1 ? null : linkList.splice(idx, 1); } + + this.links = [ ...linkList ]; + if (props.isTrezor) { + this.links.push({ path: "/trezor", link: , icon:"trezor" }); + } } componentDidMount() { @@ -77,7 +82,7 @@ class MenuLinks extends React.Component { return ( - { linkList.map(({ path, link, icon }) => + { this.links.map(({ path, link, icon }) => this._nodes.set(path, ref) } key={ path }> {link} diff --git a/app/components/modals/trezor/Modals.js b/app/components/modals/trezor/Modals.js index a391fc9eda..c38896eaea 100644 --- a/app/components/modals/trezor/Modals.js +++ b/app/components/modals/trezor/Modals.js @@ -1,6 +1,7 @@ import { trezor } from "connectors"; import PinModal from "./PinModal"; import PassPhraseModal from "./PassPhraseModal"; +import WordModal from "./WordModal"; import "style/Trezor.less"; @autobind @@ -24,6 +25,11 @@ class TrezorModals extends React.Component { {...this.props} onCancelModal={this.onCancelModal} />; + } else if (this.props.waitingForWord) { + return ; } else { return null; } diff --git a/app/components/modals/trezor/WordModal.js b/app/components/modals/trezor/WordModal.js new file mode 100644 index 0000000000..2fb0b34e18 --- /dev/null +++ b/app/components/modals/trezor/WordModal.js @@ -0,0 +1,79 @@ +import Modal from "../Modal"; +import { FormattedMessage as T } from "react-intl"; +import { ButtonsToolbar } from "../PassphraseModal"; +import Select from "react-select"; +import { word_list } from "helpers/trezor"; + +const input_options = word_list.map(w => ({ word: w })); + +@autobind +class WordModal extends React.Component { + constructor(props) { + super(props); + this.state = { word: "", value: null }; + } + + onCancelModal() { + this.setState({ word: "", value: null }); + this.props.onCancelModal(); + } + + onSubmit() { + if (!this.state.word) return; + this.props.submitWord(this.state.word); + this.setState({ word: "", value: null }); + } + + onWordChanged(value) { + this.setState({ word: value, value: { word: value } }); + } + + onSelectKeyDown(e) { + if (e.keyCode === 13 && this.state.word) { + this.onSubmit(); + } + } + + getSeedWords (input, callback) { + input = input.toLowerCase(); + const options = input_options + .filter(w => w.word.toLowerCase().substr(0, input.length) === input); + callback(null, { + options: options.slice(0, 5) + }); + } + + render() { + const { onCancelModal, onSubmit, onWordChanged, onSelectKeyDown, getSeedWords } = this; + + return ( + +

+

+ +
+ n && n.focus()} + autoFocus + simpleValue + multi={false} + clearable={false} + multi={false} + filterOptions={false} + valueKey="word" + labelKey="word" + loadOptions={getSeedWords} + onChange={onWordChanged} + value={this.state.value} + placeholder={} + onInputKeyDown={onSelectKeyDown} + /> +
+ + +
+ ); + } +} + +export default WordModal; diff --git a/app/components/views/TrezorPage/ChangeLabel.js b/app/components/views/TrezorPage/ChangeLabel.js new file mode 100644 index 0000000000..99c628f9ca --- /dev/null +++ b/app/components/views/TrezorPage/ChangeLabel.js @@ -0,0 +1,57 @@ +import { VerticalAccordion } from "shared"; +import { FormattedMessage as T } from "react-intl"; +import { TextInput } from "inputs"; +import { KeyBlueButton } from "buttons"; + +@autobind +class ChangeLabel extends React.Component { + constructor(props) { + super(props); + this.state = { newLabel: "" }; + } + + onChangeLabelClicked() { + this.props.onChangeLabel(this.state.newLabel); + } + + onNewLabelChanged(e) { + this.setState({ newLabel: e.target.value }); + } + + render() { + + const changeLabelHeader = ( + + + + ); + + const { loading } = this.props; + + return ( + + +
+
+ +
+
+ + + +
+
+
+ + ); + } +} + +export default ChangeLabel; diff --git a/app/components/views/TrezorPage/ConfigButtons.js b/app/components/views/TrezorPage/ConfigButtons.js new file mode 100644 index 0000000000..293979fb25 --- /dev/null +++ b/app/components/views/TrezorPage/ConfigButtons.js @@ -0,0 +1,45 @@ +import { VerticalAccordion } from "shared"; +import { FormattedMessage as T } from "react-intl"; +import { KeyBlueButton } from "buttons"; + +@autobind +class ConfigButtons extends React.Component { + constructor(props) { + super(props); + } + + render() { + + const ConfigButtonsHeader = ( + + + + ); + + const { loading, onTogglePinProtection, onTogglePassPhraseProtection, + onChangeHomeScreen } = this.props; + + return ( + + + + + + + + + + + + + + + ); + } +} + +export default ConfigButtons; diff --git a/app/components/views/TrezorPage/FirmwareUpdate.js b/app/components/views/TrezorPage/FirmwareUpdate.js new file mode 100644 index 0000000000..abd3f8a274 --- /dev/null +++ b/app/components/views/TrezorPage/FirmwareUpdate.js @@ -0,0 +1,54 @@ +import { VerticalAccordion } from "shared"; +import { FormattedMessage as T } from "react-intl"; +import { DangerButton } from "buttons"; +import { Documentation } from "shared"; +import { PathBrowseInput } from "inputs"; + +@autobind +class FirmwareUpdate extends React.Component { + constructor(props) { + super(props); + this.state = { path: "" }; + } + + onChangePath(path) { + this.setState({ path }); + } + + onUpdateFirmware() { + this.props.onUpdateFirmware(this.state.path); + } + + render() { + + const header = ( + + + + ); + + const { loading } = this.props; + + return ( + +
+ +
+ +

+
+ + + + +
+ + ); + } +} + +export default FirmwareUpdate; diff --git a/app/components/views/TrezorPage/Header.js b/app/components/views/TrezorPage/Header.js new file mode 100644 index 0000000000..2f271b7b0b --- /dev/null +++ b/app/components/views/TrezorPage/Header.js @@ -0,0 +1,9 @@ +import { FormattedMessage as T } from "react-intl"; +import { StandaloneHeader } from "layout"; + +export default () => + } + description={} + />; diff --git a/app/components/views/TrezorPage/NoDevicePage.js b/app/components/views/TrezorPage/NoDevicePage.js new file mode 100644 index 0000000000..10ff56cb58 --- /dev/null +++ b/app/components/views/TrezorPage/NoDevicePage.js @@ -0,0 +1,11 @@ +import { FormattedMessage as T } from "react-intl"; +import Header from "./Header"; +import { StandalonePage } from "layout"; + +export default () => ( + }> +
+ +
+
+); diff --git a/app/components/views/TrezorPage/Page.js b/app/components/views/TrezorPage/Page.js new file mode 100644 index 0000000000..7669fe68ef --- /dev/null +++ b/app/components/views/TrezorPage/Page.js @@ -0,0 +1,23 @@ +import { StandalonePage } from "layout"; +import Header from "./Header"; +import ChangeLabel from "./ChangeLabel"; +import ConfigButtons from "./ConfigButtons"; +import RecoveryButtons from "./RecoveryButtons"; +import FirmwareUpdate from "./FirmwareUpdate"; + +export default ({ + onTogglePinProtection, onTogglePassPhraseProtection, onChangeHomeScreen, + onChangeLabel, onWipeDevice, onRecoverDevice, onInitDevice, onUpdateFirmware, + loading, +}) => ( + }> + + + + + + + + +); diff --git a/app/components/views/TrezorPage/RecoveryButtons.js b/app/components/views/TrezorPage/RecoveryButtons.js new file mode 100644 index 0000000000..28696cee0a --- /dev/null +++ b/app/components/views/TrezorPage/RecoveryButtons.js @@ -0,0 +1,48 @@ +import { VerticalAccordion } from "shared"; +import { FormattedMessage as T } from "react-intl"; +import { DangerButton } from "buttons"; +import { Documentation } from "shared"; + +@autobind +class RecoveryButtons extends React.Component { + constructor(props) { + super(props); + } + + render() { + + const header = ( + + + + ); + + const { loading, onWipeDevice, onRecoverDevice, onInitDevice } = this.props; + + return ( + +
+ +
+ + + + + + + + + + + +
+ + ); + } +} + +export default RecoveryButtons; diff --git a/app/components/views/TrezorPage/index.js b/app/components/views/TrezorPage/index.js new file mode 100644 index 0000000000..3c27174e4f --- /dev/null +++ b/app/components/views/TrezorPage/index.js @@ -0,0 +1,82 @@ +import { trezor } from "connectors"; +import Page from "./Page"; +import NoDevicePage from "./NoDevicePage"; +import "style/Trezor.less"; + +@autobind +class TrezorPage extends React.Component { + + constructor(props) { + super(props); + } + + onTogglePinProtection() { + this.props.togglePinProtection(); + } + + onTogglePassPhraseProtection() { + this.props.togglePassPhraseProtection(); + } + + onChangeHomeScreen() { + this.props.changeToDecredHomeScreen(); + } + + onChangeLabel(newLabel) { + this.props.changeLabel(newLabel); + } + + onWipeDevice() { + this.props.wipeDevice(); + } + + onRecoverDevice() { + this.props.recoverDevice(); + } + + onUpdateFirmware(path) { + this.props.updateFirmware(path); + } + + onInitDevice() { + this.props.initDevice(); + } + + render() { + const { device } = this.props; + if (!device) return ; + + const loading = this.props.performingOperation; + + const { + onTogglePinProtection, + onTogglePassPhraseProtection, + onChangeHomeScreen, + onChangeLabel, + onWipeDevice, + onRecoverDevice, + onInitDevice, + onUpdateFirmware, + } = this; + + return ( + + ); + } +} + +export default trezor(TrezorPage); diff --git a/app/connectors/routing.js b/app/connectors/routing.js index 5e531da088..9edd2d7bba 100644 --- a/app/connectors/routing.js +++ b/app/connectors/routing.js @@ -7,6 +7,7 @@ import * as ca from "../actions/ClientActions"; const mapStateToProps = selectorMap({ location: sel.location, politeiaBetaEnabled: sel.politeiaBetaEnabled, // TODO: remove once politeia hits production + isTrezor: sel.isTrezor, }); const mapDispatchToProps = dispatch => bindActionCreators({ diff --git a/app/connectors/trezor.js b/app/connectors/trezor.js index 52429e017e..e4dca84497 100644 --- a/app/connectors/trezor.js +++ b/app/connectors/trezor.js @@ -8,13 +8,24 @@ const mapStateToProps = selectorMap({ isTrezor: sel.isTrezor, waitingForPin: sel.trezorWaitingForPin, waitingForPassPhrase: sel.trezorWaitingForPassPhrase, + waitingForWord: sel.trezorWaitingForWord, device: sel.trezorDevice, + performingOperation: sel.trezorPerformingOperation, }); const mapDispatchToProps = dispatch => bindActionCreators({ cancelCurrentOperation: trza.cancelCurrentOperation, submitPin: trza.submitPin, submitPassPhrase: trza.submitPassPhrase, + submitWord: trza.submitWord, + togglePinProtection: trza.togglePinProtection, + togglePassPhraseProtection: trza.togglePassPhraseProtection, + changeToDecredHomeScreen: trza.changeToDecredHomeScreen, + changeLabel: trza.changeLabel, + wipeDevice: trza.wipeDevice, + recoverDevice: trza.recoverDevice, + initDevice: trza.initDevice, + updateFirmware: trza.updateFirmware, }, dispatch); export default connect(mapStateToProps, mapDispatchToProps); diff --git a/app/containers/Wallet.js b/app/containers/Wallet.js index 01cc16dfc7..66f214ff04 100644 --- a/app/containers/Wallet.js +++ b/app/containers/Wallet.js @@ -14,6 +14,7 @@ import TransactionPage from "components/views/TransactionPage"; import TicketsPage from "components/views/TicketsPage"; import TutorialsPage from "components/views/TutorialsPage"; import GovernancePage from "components/views/GovernancePage"; +import TrezorPage from "components/views/TrezorPage"; import SideBar from "components/SideBar"; import { BlurableContainer } from "layout"; import { walletContainer, theming } from "connectors"; @@ -44,6 +45,7 @@ class Wallet extends React.Component { + diff --git a/app/helpers/trezor.js b/app/helpers/trezor.js new file mode 100644 index 0000000000..4d97efa979 --- /dev/null +++ b/app/helpers/trezor.js @@ -0,0 +1,209 @@ +export const model1_decred_homescreen = "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007ffe003ff00000000000000000000007fffc007fe0000000000000000000001ffff800ffc0000000000000000000003ffff001ff8000000000000000000000ffffe003ff0000000000000000000001ffffc007fe0000000000000000000003ffff800ffc0000000000000000000003ffff001ff80000000000000000000007fffe003ff00000000000000000000007ff00007fe0000000000000000000000ffc0000ffc0000000000000000000000ff80001fff0000000000000000000001ff00003fffc000000000000000000001ff00007ffff000000000000000000001fe0000fffff800000000000000000001fe0001fffff800000000000000000001fe0003fffffc00000000000000000001fe0000003ffe00000000000000000001fe00000007fe00000000000000000001fe00000003ff00000000000000000001fe00000001ff00000000000000000001ff00000001ff00000000000000000001ff80000000ff00000000000000000000ffc0000000ff00000000000000000000ffe0000000ff800000000000000000007ffc000000ff800000000000000000007fffff8000ff800000000000000000003fffff0000ff000000000000000000001ffffe0000ff000000000000000000000ffffc0001ff0000000000000000000003fff80001ff0000000000000000000001fff00003ff00000000000000000000007fe00007fe0000000000000000000000ffc0001ffe0000000000000000000001ff800ffffc0000000000000000000003ff001ffff80000000000000000000007fe003ffff8000000000000000000000ffc007ffff0000000000000000000001ff800ffffe0000000000000000000003ff001ffff80000000000000000000007fe003ffff0000000000000000000000ffc007fff80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + +export const word_list = [ + "abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", + "access", "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", "across", "act", + "action", "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit", + "adult", "advance", "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent", + "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album", "alcohol", "alert", + "alien", "all", "alley", "allow", "almost", "alone", "alpha", "already", "also", "alter", + "always", "amateur", "amazing", "among", "amount", "amused", "analyst", "anchor", "ancient", "anger", + "angle", "angry", "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique", + "anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic", + "area", "arena", "argue", "arm", "armed", "armor", "army", "around", "arrange", "arrest", + "arrive", "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset", + "assist", "assume", "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction", + "audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado", "avoid", "awake", + "aware", "away", "awesome", "awful", "awkward", "axis", "baby", "bachelor", "bacon", "badge", + "bag", "balance", "balcony", "ball", "bamboo", "banana", "banner", "bar", "barely", "bargain", + "barrel", "base", "basic", "basket", "battle", "beach", "bean", "beauty", "because", "become", + "beef", "before", "begin", "behave", "behind", "believe", "below", "belt", "bench", "benefit", + "best", "betray", "better", "between", "beyond", "bicycle", "bid", "bike", "bind", "biology", + "bird", "birth", "bitter", "black", "blade", "blame", "blanket", "blast", "bleak", "bless", + "blind", "blood", "blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body", + "boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring", "borrow", "boss", + "bottom", "bounce", "box", "boy", "bracket", "brain", "brand", "brass", "brave", "bread", + "breeze", "brick", "bridge", "brief", "bright", "bring", "brisk", "broccoli", "broken", "bronze", + "broom", "brother", "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb", + "bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", "business", "busy", + "butter", "buyer", "buzz", "cabbage", "cabin", "cable", "cactus", "cage", "cake", "call", + "calm", "camera", "camp", "can", "canal", "cancel", "candy", "cannon", "canoe", "canvas", + "canyon", "capable", "capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry", + "cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog", "catch", "category", + "cattle", "caught", "cause", "caution", "cave", "ceiling", "celery", "cement", "census", "century", + "cereal", "certain", "chair", "chalk", "champion", "change", "chaos", "chapter", "charge", "chase", + "chat", "cheap", "check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child", + "chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", "circle", + "citizen", "city", "civil", "claim", "clap", "clarify", "claw", "clay", "clean", "clerk", + "clever", "click", "client", "cliff", "climb", "clinic", "clip", "clock", "clog", "close", + "cloth", "cloud", "clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut", + "code", "coffee", "coil", "coin", "collect", "color", "column", "combine", "come", "comfort", + "comic", "common", "company", "concert", "conduct", "confirm", "congress", "connect", "consider", "control", + "convince", "cook", "cool", "copper", "copy", "coral", "core", "corn", "correct", "cost", + "cotton", "couch", "country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle", + "craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream", "credit", "creek", + "crew", "cricket", "crime", "crisp", "critic", "crop", "cross", "crouch", "crowd", "crucial", + "cruel", "cruise", "crumble", "crunch", "crush", "cry", "crystal", "cube", "culture", "cup", + "cupboard", "curious", "current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad", + "damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn", "day", "deal", + "debate", "debris", "decade", "december", "decide", "decline", "decorate", "decrease", "deer", "defense", + "define", "defy", "degree", "delay", "deliver", "demand", "demise", "denial", "dentist", "deny", + "depart", "depend", "deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk", + "despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram", "dial", "diamond", + "diary", "dice", "diesel", "diet", "differ", "digital", "dignity", "dilemma", "dinner", "dinosaur", + "direct", "dirt", "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", "distance", + "divert", "divide", "divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain", + "donate", "donkey", "donor", "door", "dose", "double", "dove", "draft", "dragon", "drama", + "drastic", "draw", "dream", "dress", "drift", "drill", "drink", "drip", "drive", "drop", + "drum", "dry", "duck", "dumb", "dune", "during", "dust", "dutch", "duty", "dwarf", + "dynamic", "eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo", + "ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight", "either", "elbow", + "elder", "electric", "elegant", "element", "elephant", "elevator", "elite", "else", "embark", "embody", + "embrace", "emerge", "emotion", "employ", "empower", "empty", "enable", "enact", "end", "endless", + "endorse", "enemy", "energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough", + "enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode", "equal", "equip", + "era", "erase", "erode", "erosion", "error", "erupt", "escape", "essay", "essence", "estate", + "eternal", "ethics", "evidence", "evil", "evoke", "evolve", "exact", "example", "excess", "exchange", + "excite", "exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit", + "exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend", "extra", "eye", + "eyebrow", "fabric", "face", "faculty", "fade", "faint", "faith", "fall", "false", "fame", + "family", "famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", "father", + "fatigue", "fault", "favorite", "feature", "february", "federal", "fee", "feed", "feel", "female", + "fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field", "figure", "file", + "film", "filter", "final", "find", "fine", "finger", "finish", "fire", "firm", "first", + "fiscal", "fish", "fit", "fitness", "fix", "flag", "flame", "flash", "flat", "flavor", + "flee", "flight", "flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly", + "foam", "focus", "fog", "foil", "fold", "follow", "food", "foot", "force", "forest", + "forget", "fork", "fortune", "forum", "forward", "fossil", "foster", "found", "fox", "fragile", + "frame", "frequent", "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen", + "fruit", "fuel", "fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy", + "gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment", "gas", "gasp", + "gate", "gather", "gauge", "gaze", "general", "genius", "genre", "gentle", "genuine", "gesture", + "ghost", "giant", "gift", "giggle", "ginger", "giraffe", "girl", "give", "glad", "glance", + "glare", "glass", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue", + "goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown", + "grab", "grace", "grain", "grant", "grape", "grass", "gravity", "great", "green", "grid", + "grief", "grit", "grocery", "group", "grow", "grunt", "guard", "guess", "guide", "guilt", + "guitar", "gun", "gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy", + "harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard", "head", "health", + "heart", "heavy", "hedgehog", "height", "hello", "helmet", "help", "hen", "hero", "hidden", + "high", "hill", "hint", "hip", "hire", "history", "hobby", "hockey", "hold", "hole", + "holiday", "hollow", "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital", + "host", "hotel", "hour", "hover", "hub", "huge", "human", "humble", "humor", "hundred", + "hungry", "hunt", "hurdle", "hurry", "hurt", "husband", "hybrid", "ice", "icon", "idea", + "identify", "idle", "ignore", "ill", "illegal", "illness", "image", "imitate", "immense", "immune", + "impact", "impose", "improve", "impulse", "inch", "include", "income", "increase", "index", "indicate", + "indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial", "inject", "injury", + "inmate", "inner", "innocent", "input", "inquiry", "insane", "insect", "inside", "inspire", "install", + "intact", "interest", "into", "invest", "invite", "involve", "iron", "island", "isolate", "issue", + "item", "ivory", "jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel", + "job", "join", "joke", "journey", "joy", "judge", "juice", "jump", "jungle", "junior", + "junk", "just", "kangaroo", "keen", "keep", "ketchup", "key", "kick", "kid", "kidney", + "kind", "kingdom", "kiss", "kit", "kitchen", "kite", "kitten", "kiwi", "knee", "knife", + "knock", "know", "lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language", + "laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law", "lawn", "lawsuit", + "layer", "lazy", "leader", "leaf", "learn", "leave", "lecture", "left", "leg", "legal", + "legend", "leisure", "lemon", "lend", "length", "lens", "leopard", "lesson", "letter", "level", + "liar", "liberty", "library", "license", "life", "lift", "light", "like", "limb", "limit", + "link", "lion", "liquid", "list", "little", "live", "lizard", "load", "loan", "lobster", + "local", "lock", "logic", "lonely", "long", "loop", "lottery", "loud", "lounge", "love", + "loyal", "lucky", "luggage", "lumber", "lunar", "lunch", "luxury", "lyrics", "machine", "mad", + "magic", "magnet", "maid", "mail", "main", "major", "make", "mammal", "man", "manage", + "mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin", "marine", "market", + "marriage", "mask", "mass", "master", "match", "material", "math", "matrix", "matter", "maximum", + "maze", "meadow", "mean", "measure", "meat", "mechanic", "medal", "media", "melody", "melt", + "member", "memory", "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message", + "metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind", "minimum", "minor", + "minute", "miracle", "mirror", "misery", "miss", "mistake", "mix", "mixed", "mixture", "mobile", + "model", "modify", "mom", "moment", "monitor", "monkey", "monster", "month", "moon", "moral", + "more", "morning", "mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie", + "much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music", "must", "mutual", + "myself", "mystery", "myth", "naive", "name", "napkin", "narrow", "nasty", "nation", "nature", + "near", "neck", "need", "negative", "neglect", "neither", "nephew", "nerve", "nest", "net", + "network", "neutral", "never", "news", "next", "nice", "night", "noble", "noise", "nominee", + "noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice", "novel", "now", + "nuclear", "number", "nurse", "nut", "oak", "obey", "object", "oblige", "obscure", "observe", + "obtain", "obvious", "occur", "ocean", "october", "odor", "off", "offer", "office", "often", + "oil", "okay", "old", "olive", "olympic", "omit", "once", "one", "onion", "online", + "only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit", "orchard", "order", + "ordinary", "organ", "orient", "original", "orphan", "ostrich", "other", "outdoor", "outer", "output", + "outside", "oval", "oven", "over", "own", "owner", "oxygen", "oyster", "ozone", "pact", + "paddle", "page", "pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper", + "parade", "parent", "park", "parrot", "party", "pass", "patch", "path", "patient", "patrol", + "pattern", "pause", "pave", "payment", "peace", "peanut", "pear", "peasant", "pelican", "pen", + "penalty", "pencil", "people", "pepper", "perfect", "permit", "person", "pet", "phone", "photo", + "phrase", "physical", "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot", + "pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet", "plastic", "plate", + "play", "please", "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar", + "pole", "police", "pond", "pony", "pool", "popular", "portion", "position", "possible", "post", + "potato", "pottery", "poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare", + "present", "pretty", "prevent", "price", "pride", "primary", "print", "priority", "prison", "private", + "prize", "problem", "process", "produce", "profit", "program", "project", "promote", "proof", "property", + "prosper", "protect", "proud", "provide", "public", "pudding", "pull", "pulp", "pulse", "pumpkin", + "punch", "pupil", "puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle", + "pyramid", "quality", "quantum", "quarter", "question", "quick", "quit", "quiz", "quote", "rabbit", + "raccoon", "race", "rack", "radar", "radio", "rail", "rain", "raise", "rally", "ramp", + "ranch", "random", "range", "rapid", "rare", "rate", "rather", "raven", "raw", "razor", + "ready", "real", "reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle", + "reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject", "relax", "release", + "relief", "rely", "remain", "remember", "remind", "remove", "render", "renew", "rent", "reopen", + "repair", "repeat", "replace", "report", "require", "rescue", "resemble", "resist", "resource", "response", + "result", "retire", "retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib", + "ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid", "ring", "riot", + "ripple", "risk", "ritual", "rival", "river", "road", "roast", "robot", "robust", "rocket", + "romance", "roof", "rookie", "room", "rose", "rotate", "rough", "round", "route", "royal", + "rubber", "rude", "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness", + "safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same", "sample", "sand", + "satisfy", "satoshi", "sauce", "sausage", "save", "say", "scale", "scan", "scare", "scatter", + "scene", "scheme", "school", "science", "scissors", "scorpion", "scout", "scrap", "screen", "script", + "scrub", "sea", "search", "season", "seat", "second", "secret", "section", "security", "seed", + "seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence", "series", "service", + "session", "settle", "setup", "seven", "shadow", "shaft", "shallow", "share", "shed", "shell", + "sheriff", "shield", "shift", "shine", "ship", "shiver", "shock", "shoe", "shoot", "shop", + "short", "shoulder", "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side", + "siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar", "simple", "since", + "sing", "siren", "sister", "situate", "six", "size", "skate", "sketch", "ski", "skill", + "skin", "skirt", "skull", "slab", "slam", "sleep", "slender", "slice", "slide", "slight", + "slim", "slogan", "slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth", + "snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social", "sock", "soda", + "soft", "solar", "soldier", "solid", "solution", "solve", "someone", "song", "soon", "sorry", + "sort", "soul", "sound", "soup", "source", "south", "space", "spare", "spatial", "spawn", + "speak", "special", "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin", + "spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray", "spread", "spring", + "spy", "square", "squeeze", "squirrel", "stable", "stadium", "staff", "stage", "stairs", "stamp", + "stand", "start", "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick", + "still", "sting", "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street", + "strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject", "submit", "subway", + "success", "such", "sudden", "suffer", "sugar", "suggest", "suit", "summer", "sun", "sunny", + "sunset", "super", "supply", "supreme", "sure", "surface", "surge", "surprise", "surround", "survey", + "suspect", "sustain", "swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim", + "swing", "switch", "sword", "symbol", "symptom", "syrup", "system", "table", "tackle", "tag", + "tail", "talent", "talk", "tank", "tape", "target", "task", "taste", "tattoo", "taxi", + "teach", "team", "tell", "ten", "tenant", "tennis", "tent", "term", "test", "text", + "thank", "that", "theme", "then", "theory", "there", "they", "thing", "this", "thought", + "three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger", "tilt", "timber", + "time", "tiny", "tip", "tired", "tissue", "title", "toast", "tobacco", "today", "toddler", + "toe", "together", "toilet", "token", "tomato", "tomorrow", "tone", "tongue", "tonight", "tool", + "tooth", "top", "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist", + "toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic", "train", "transfer", + "trap", "trash", "travel", "tray", "treat", "tree", "trend", "trial", "tribe", "trick", + "trigger", "trim", "trip", "trophy", "trouble", "truck", "true", "truly", "trumpet", "trust", + "truth", "try", "tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle", + "twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical", "ugly", "umbrella", + "unable", "unaware", "uncle", "uncover", "under", "undo", "unfair", "unfold", "unhappy", "uniform", + "unique", "unit", "universe", "unknown", "unlock", "until", "unusual", "unveil", "update", "upgrade", + "uphold", "upon", "upper", "upset", "urban", "urge", "usage", "use", "used", "useful", + "useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley", "valve", "van", + "vanish", "vapor", "various", "vast", "vault", "vehicle", "velvet", "vendor", "venture", "venue", + "verb", "verify", "version", "very", "vessel", "veteran", "viable", "vibrant", "vicious", "victory", + "video", "view", "village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual", + "vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote", "voyage", "wage", + "wagon", "wait", "walk", "wall", "walnut", "want", "warfare", "warm", "warrior", "wash", + "wasp", "waste", "water", "wave", "way", "wealth", "weapon", "wear", "weasel", "weather", + "web", "wedding", "weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat", + "wheel", "when", "where", "whip", "whisper", "wide", "width", "wife", "wild", "will", + "win", "window", "wine", "wing", "wink", "winner", "winter", "wire", "wisdom", "wise", + "wish", "witness", "wolf", "woman", "wonder", "wood", "wool", "word", "work", "world", + "worry", "worth", "wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year", + "yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo", +]; diff --git a/app/i18n/docs/en/Warnings/TrezorFirmwareUpdate.md b/app/i18n/docs/en/Warnings/TrezorFirmwareUpdate.md new file mode 100644 index 0000000000..f78fe840a8 --- /dev/null +++ b/app/i18n/docs/en/Warnings/TrezorFirmwareUpdate.md @@ -0,0 +1,8 @@ +**Warning!** Only use official firmware distributed by SatoshiLabs (Trezor +manufacturer) or other very trusted sources. Using a non standard firmware +or one obtained from an unreputable individual or company might result in **loss +or stealing of funds**. + +Also, ensure you have access to your seed, that it is valid and that it +corresponds to the funds in this wallet. Firmware updates might cause all data +in the device to be wiped, requiring a recover operation afterwards. diff --git a/app/i18n/docs/en/Warnings/TrezorWipe.md b/app/i18n/docs/en/Warnings/TrezorWipe.md new file mode 100644 index 0000000000..8b2766d4f8 --- /dev/null +++ b/app/i18n/docs/en/Warnings/TrezorWipe.md @@ -0,0 +1,8 @@ +**Warning!** Please ensure you have the seed for your trezor device stored in a +secure and accessible location, that it is valid, and that it is the correct key +for this wallet before performing a device wipe and recovery. + +Failure to do so will result in **loss of funds**. + +Please also note that Decrediton only supports recovering 24 word (256 bit) +seeds and that the words for trezor and dcrwallet imports are *different*. diff --git a/app/i18n/docs/en/index.js b/app/i18n/docs/en/index.js index 84245a1885..adf045f422 100644 --- a/app/i18n/docs/en/index.js +++ b/app/i18n/docs/en/index.js @@ -10,6 +10,8 @@ export { default as ScriptNotRedeemableInfo } from "./InfoModals/ScriptNotRedeem export { default as SeedCopyWarning } from "./Warnings/SeedCopy.md"; export { default as WalletCreationWarning } from "./Warnings/WalletCreation.md"; +export { default as TrezorWipeWarning } from "./Warnings/TrezorWipe.md"; +export { default as TrezorFirmwareUpdateWarning } from "./Warnings/TrezorFirmwareUpdate.md"; export { default as GetStartedTutorialPage01 } from "./GetStarted/TutorialPage01.md"; export { default as GetStartedTutorialPage02 } from "./GetStarted/TutorialPage02.md"; diff --git a/app/index.js b/app/index.js index 69f060d2d2..759506645c 100644 --- a/app/index.js +++ b/app/index.js @@ -389,12 +389,15 @@ var initialState = { deviceList: null, transportError: false, device: null, + performingOperation: false, waitingForPin: false, waitingForPassPhrase: false, + waitingForWord: false, pinCallBack: null, passPhraseCallBack: null, pinMessage: null, passPhraseMessage: null, + wordCallBack: null, }, locales: locales }; diff --git a/app/reducers/snackbar.js b/app/reducers/snackbar.js index 08ef94c530..28748b4ce3 100644 --- a/app/reducers/snackbar.js +++ b/app/reducers/snackbar.js @@ -43,6 +43,16 @@ import { GETWALLETSEEDSVC_FAILED, SPVSYNC_FAILED, } from "actions/WalletLoaderActions"; +import { + TRZ_TOGGLEPINPROTECTION_SUCCESS, TRZ_TOGGLEPINPROTECTION_FAILED, + TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS, TRZ_TOGGLEPASSPHRASEPROTECTION_FAILED, + TRZ_CHANGEHOMESCREEN_SUCCESS, TRZ_CHANGEHOMESCREEN_FAILED, + TRZ_CHANGELABEL_SUCCESS, TRZ_CHANGELABEL_FAILED, + TRZ_WIPEDEVICE_SUCCESS, TRZ_WIPEDEVICE_FAILED, + TRZ_RECOVERDEVICE_SUCCESS, TRZ_RECOVERDEVICE_FAILED, + TRZ_INITDEVICE_SUCCESS, TRZ_INITDEVICE_FAILED, + TRZ_UPDATEFIRMWARE_SUCCESS, TRZ_UPDATEFIRMWARE_FAILED, +} from "actions/TrezorActions"; import { GETACTIVEVOTE_FAILED, GETVETTED_FAILED, GETPROPOSAL_FAILED, @@ -201,6 +211,46 @@ const messages = defineMessages({ SPVSYNC_FAILED: { id: "spvSync.Failed", defaultMessage: "Error syncing SPV wallet: {originalError}" + }, + TRZ_TOGGLEPINPROTECTION_SUCCESS_ENABLED: { + id: "trezor.pinProtectionSuccess.enabled", + defaultMessage: "Pin protection has been enabled in trezor '{label}'" + }, + TRZ_TOGGLEPINPROTECTION_SUCCESS_DISABLED: { + id: "trezor.pinProtectionSuccess.disabled", + defaultMessage: "Pin protection has been disabled in trezor '{label}'" + }, + TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS_ENABLED: { + id: "trezor.passphraseProtectionSuccess.enabled", + defaultMessage: "Passphrase protection has been enabled in trezor '{label}'" + }, + TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS_DISABLED: { + id: "trezor.passphraseProtectionSuccess.disabled", + defaultMessage: "Passphrase protection has been disabled in trezor '{label}'" + }, + TRZ_CHANGEHOMESCREEN_SUCCESS: { + id: "trezor.changeHomeScreen.success", + defaultMessage: "Trezor home screen successfully changed" + }, + TRZ_CHANGELABEL_SUCCESS: { + id: "trezor.changeLabel.success", + defaultMessage: "Changed label on selected trezor to '{label}'" + }, + TRZ_WIPEDEVICE_SUCCESS: { + id: "trezor.wipeDevice.success", + defaultMessage: "Trezor device wiped" + }, + TRZ_RECOVERDEVICE_SUCCESS: { + id: "trezor.recoverDevice.success", + defaultMessage: "Trezor device recovered" + }, + TRZ_INITDEVICE_SUCCESS: { + id: "trezor.initDevice.success", + defaultMessage: "Trezor device initialized with new seed" + }, + TRZ_UPDATEFIRMWARE_SUCCESS: { + id: "trezor.updateFirmware.success", + defaultMessage: "Firmware updated on trezor device" } }); @@ -249,6 +299,11 @@ export default function snackbar(state = {}, action) { case REMOVESTAKEPOOLCONFIG: case SEEDCOPIEDTOCLIPBOARD: case PUBLISHUNMINEDTRANSACTIONS_SUCCESS: + case TRZ_CHANGEHOMESCREEN_SUCCESS: + case TRZ_WIPEDEVICE_SUCCESS: + case TRZ_RECOVERDEVICE_SUCCESS: + case TRZ_INITDEVICE_SUCCESS: + case TRZ_UPDATEFIRMWARE_SUCCESS: type = "Success"; message = messages[action.type] || messages.defaultSuccessMessage; break; @@ -284,6 +339,14 @@ export default function snackbar(state = {}, action) { case SPVSYNC_FAILED: case UPDATEVOTECHOICE_FAILED: case GETACCOUNTEXTENDEDKEY_FAILED: + case TRZ_TOGGLEPINPROTECTION_FAILED: + case TRZ_TOGGLEPASSPHRASEPROTECTION_FAILED: + case TRZ_CHANGEHOMESCREEN_FAILED: + case TRZ_CHANGELABEL_FAILED: + case TRZ_WIPEDEVICE_FAILED: + case TRZ_RECOVERDEVICE_FAILED: + case TRZ_INITDEVICE_FAILED: + case TRZ_UPDATEFIRMWARE_FAILED: type = "Error"; message = messages[action.type] || messages.defaultErrorMessage; values = { originalError: String(action.error) }; @@ -301,6 +364,25 @@ export default function snackbar(state = {}, action) { message = messages[ADDCUSTOMSTAKEPOOL_SUCCESS]; values = { host: action.poolInfo.Host }; break; + + case TRZ_TOGGLEPINPROTECTION_SUCCESS: + type = "Success"; + + message = messages["TRZ_TOGGLEPINPROTECTION_SUCCESS_" + (action.clearProtection ? "DISABLED" : "ENABLED")]; + values = { label: action.deviceLabel }; + + case TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS: + type = "Success"; + + message = messages["TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS_" + (action.enableProtection ? "ENABLED" : "DISABLED")]; + values = { label: action.deviceLabel }; + break; + + case TRZ_CHANGELABEL_SUCCESS: + type = "Success"; + message = messages[TRZ_CHANGELABEL_SUCCESS]; + values = { label: action.label }; + break; } if (message && type) { diff --git a/app/reducers/trezor.js b/app/reducers/trezor.js index 3e5458569a..f3ab5005fb 100644 --- a/app/reducers/trezor.js +++ b/app/reducers/trezor.js @@ -4,8 +4,20 @@ import { TRZ_SELECTEDDEVICE_CHANGED, TRZ_PIN_REQUESTED, TRZ_PIN_ENTERED, TRZ_PIN_CANCELED, TRZ_PASSPHRASE_REQUESTED, TRZ_PASSPHRASE_ENTERED, TRZ_PASSPHRASE_CANCELED, + TRZ_WORD_REQUESTED, TRZ_WORD_ENTERED, TRZ_WORD_CANCELED, TRZ_CANCELOPERATION_SUCCESS, + TRZ_TOGGLEPINPROTECTION_ATTEMPT, TRZ_TOGGLEPINPROTECTION_FAILED, TRZ_TOGGLEPINPROTECTION_SUCCESS, + TRZ_TOGGLEPASSPHRASEPROTECTION_ATTEMPT, TRZ_TOGGLEPASSPHRASEPROTECTION_FAILED, TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS, + TRZ_CHANGEHOMESCREEN_ATTEMPT, TRZ_CHANGEHOMESCREEN_FAILED, TRZ_CHANGEHOMESCREEN_SUCCESS, + TRZ_CHANGELABEL_ATTEMPT, TRZ_CHANGELABEL_FAILED, TRZ_CHANGELABEL_SUCCESS, + TRZ_WIPEDEVICE_ATTEMPT, TRZ_WIPEDEVICE_FAILED, TRZ_WIPEDEVICE_SUCCESS, + TRZ_RECOVERDEVICE_ATTEMPT, TRZ_RECOVERDEVICE_FAILED, TRZ_RECOVERDEVICE_SUCCESS, + TRZ_INITDEVICE_ATTEMPT, TRZ_INITDEVICE_FAILED, TRZ_INITDEVICE_SUCCESS, + TRZ_UPDATEFIRMWARE_ATTEMPT, TRZ_UPDATEFIRMWARE_FAILED, TRZ_UPDATEFIRMWARE_SUCCESS, } from "actions/TrezorActions"; +import { + SIGNTX_ATTEMPT, SIGNTX_FAILED, SIGNTX_SUCCESS +} from "actions/ControlActions"; export default function trezor(state = {}, action) { switch (action.type) { @@ -29,6 +41,7 @@ export default function trezor(state = {}, action) { deviceList: null, transportError: action.error, device: null, + performingOperation: false, }; case TRZ_SELECTEDDEVICE_CHANGED: return { ...state, @@ -39,6 +52,7 @@ export default function trezor(state = {}, action) { waitingForPin: true, pinCallBack: action.pinCallBack, pinMessage: action.pinMessage, + performingOperation: true, }; case TRZ_PIN_CANCELED: case TRZ_PIN_ENTERED: @@ -46,25 +60,77 @@ export default function trezor(state = {}, action) { waitingForPin: false, pinCallBack: null, pinMessage: null, + performingOperation: false, }; case TRZ_PASSPHRASE_REQUESTED: return { ...state, waitingForPassPhrase: true, passPhraseCallBack: action.passPhraseCallBack, + performingOperation: true, }; case TRZ_PASSPHRASE_CANCELED: case TRZ_PASSPHRASE_ENTERED: return { ...state, waitingForPassPhrase: false, passPhraseCallBack: null, + performingOperation: false, + }; + case TRZ_WORD_REQUESTED: + return { ...state, + waitingForWord: true, + wordCallBack: action.wordCallBack, + performingOperation: true, + }; + case TRZ_WORD_CANCELED: + case TRZ_WORD_ENTERED: + return { ...state, + waitingForWord: false, + wordCallBack: null, + performingOperation: false, }; case TRZ_CANCELOPERATION_SUCCESS: return { ...state, waitingForPin: false, pinCallBack: null, pinMessage: null, + wordCallBack: null, waitingForPassPhrase: false, passPhraseCallBack: null, + performingOperation: false, + waitingForWord: false, + }; + case SIGNTX_ATTEMPT: + case TRZ_TOGGLEPINPROTECTION_ATTEMPT: + case TRZ_TOGGLEPASSPHRASEPROTECTION_ATTEMPT: + case TRZ_CHANGEHOMESCREEN_ATTEMPT: + case TRZ_CHANGELABEL_ATTEMPT: + case TRZ_WIPEDEVICE_ATTEMPT: + case TRZ_RECOVERDEVICE_ATTEMPT: + case TRZ_INITDEVICE_ATTEMPT: + case TRZ_UPDATEFIRMWARE_ATTEMPT: + return { ...state, + performingOperation: true, + }; + case SIGNTX_FAILED: + case SIGNTX_SUCCESS: + case TRZ_TOGGLEPINPROTECTION_FAILED: + case TRZ_TOGGLEPINPROTECTION_SUCCESS: + case TRZ_TOGGLEPASSPHRASEPROTECTION_FAILED: + case TRZ_TOGGLEPASSPHRASEPROTECTION_SUCCESS: + case TRZ_CHANGEHOMESCREEN_FAILED: + case TRZ_CHANGEHOMESCREEN_SUCCESS: + case TRZ_CHANGELABEL_FAILED: + case TRZ_CHANGELABEL_SUCCESS: + case TRZ_WIPEDEVICE_FAILED: + case TRZ_WIPEDEVICE_SUCCESS: + case TRZ_RECOVERDEVICE_FAILED: + case TRZ_RECOVERDEVICE_SUCCESS: + case TRZ_INITDEVICE_FAILED: + case TRZ_INITDEVICE_SUCCESS: + case TRZ_UPDATEFIRMWARE_FAILED: + case TRZ_UPDATEFIRMWARE_SUCCESS: + return { ...state, + performingOperation: false, }; default: return state; diff --git a/app/selectors.js b/app/selectors.js index 16602fab5a..8d2f37df57 100644 --- a/app/selectors.js +++ b/app/selectors.js @@ -945,4 +945,6 @@ export const viewedProposalDetails = createSelector( export const trezorWaitingForPin = get([ "trezor", "waitingForPin" ]); export const trezorWaitingForPassPhrase = get([ "trezor", "waitingForPassPhrase" ]); +export const trezorWaitingForWord = get([ "trezor", "waitingForWord" ]); +export const trezorPerformingOperation = get([ "trezor", "performingOperation" ]); export const trezorDevice = get([ "trezor", "device" ]); diff --git a/app/style/Header.less b/app/style/Header.less index 129930846a..0e2f5b6c35 100644 --- a/app/style/Header.less +++ b/app/style/Header.less @@ -71,6 +71,7 @@ &.tickets { background-image: @tickets-page-icon; } &.transactions { background-image: @transactions-page-icon; } &.governance { background-image: @menu-governance-active; } + &.trezor { background-image: @menu-trezor-active; } &.tx-detail-icon-ticket { background-image: @ticket-live-icon; } &.tx-detail-icon-vote { background-image: @ticket-voted-icon; } diff --git a/app/style/Icons.less b/app/style/Icons.less index 5d343ce73e..17b7a8075f 100644 --- a/app/style/Icons.less +++ b/app/style/Icons.less @@ -117,6 +117,9 @@ @menu-transactions-active: url('@{icon-root}/transactions-active.svg'); @menu-transactions-default: url('@{icon-root}/transactions-default.svg'); @menu-transactions-hover: url('@{icon-root}/transactions-hover.svg'); +@menu-trezor-active: url('@{icon-root}/trezor-active.png'); +@menu-trezor-default: url('@{icon-root}/trezor-default.png'); +@menu-trezor-hover: url('@{icon-root}/trezor-hover.png'); @menu-tickets-active: url('@{icon-root}/tickets-active.svg'); @menu-tickets-default: url('@{icon-root}/tickets-default.svg'); @menu-tickets-hover: url('@{icon-root}/tickets-hover.svg'); diff --git a/app/style/MiscComponents.less b/app/style/MiscComponents.less index 6004f939d0..9a087d767f 100644 --- a/app/style/MiscComponents.less +++ b/app/style/MiscComponents.less @@ -363,6 +363,18 @@ background-image: @menu-governance-hover; } +.menu-link-active.menu-link.trezorIcon { + background-image: @menu-trezor-active; +} + +.menu-link.trezorIcon:not(.menu-link-active):hover { + background-image: @menu-trezor-hover; +} + +.menu-link.trezorIcon { + background-image: @menu-trezor-default; +} + .menu-caret { position: absolute; height: 52px; diff --git a/app/style/Trezor.less b/app/style/Trezor.less index d1a0e319eb..bf0890d25b 100644 --- a/app/style/Trezor.less +++ b/app/style/Trezor.less @@ -22,6 +22,38 @@ } } +.trezor-word-modal { + h1 { + margin: 0; + } +} + + .trezor-label { font-family: @font-family-monospaced; } + +.trezor-config-regular-buttons .button { + margin-right: 1em; + margin-top: 1em; +} + +.trezor-config-accordion { + border-bottom: 1px solid #dadfe2; + border-radius: 4px; + padding: 1.5em 1em 1em; + background-color: #fff; + + .vertical-accordion-header { + font-size: 22px; + margin-bottom: 0.5em; + } + + .input-and-unit { + margin-bottom: 0.5em; + } +} + +.trezor-word-select { + min-height: 10em; +} diff --git a/app/style/icons/trezor-active.png b/app/style/icons/trezor-active.png new file mode 100644 index 0000000000000000000000000000000000000000..0cf0512a80d34850811678ede4c19bca1bd05d1c GIT binary patch literal 2335 zcma)8c{CLI7oRbju}v~5ldTCU$!mB#O3YN28Nyhj#?F(am@FeyMiN5C8d+Lwm1RQq zWiW`$=<&!p#`YxJ$TEcHH=TFR@4SBJ{qZ~B@BQBUz4v@SpZh)Mo_lT#&eRYpBq;;{ z0H9cmo&`_8{~-ck-dSQ>n!{6I7h^*`0Qbjw)>xR#^9cH3?EC=$;e$T}2*}KW^PG?X ztcgBk3L+$a0QQ-z_Za{XD8}ktunHbo8h5;IPXtm|eJ?*M!r37uksad8HfI9yGG6Z8 zhmY!UDD#7T1Fg36vRNv|_7K+5_+faru4$0Jk8CdDxmMbB)AP`~*=XaI6*)u^ITH z#_k5om%2il0Q&O6U(7WV)gs>lhlI`p;{IB%^r!lg()$&%HZ75fFZADnhxixymp`w4 zI4jy9ARQ?AJ3?Q;6~b2VGj4HUO0!{pNR!s%C>2oG&hC`6lVd`E5$9kxkVTP?CCvmT z5NZIHuVybdHsvZ#2BP#qF&bXG0>KfpWx9u2RGs?%prLKl5(q3eLBsfVgJej*0zYcJ z{SNh#Uz^r%eB5H3G+-fIQ;I$qgqm!Jh6o&vpH^Z?Q12-R7lTQ@Q0QY3xJee$8QJkVDZABVZqzuH{g=8+nmMO3{=QT=?itb!n@sVesq#Sg~p^aTcNRjl5I9D zjbF(GH`#LH6A}>YVgmkCktHKD_r6jM<)tX0k~B>vs|$Mgt{?SiiCoN0wo_Me(q`irW%o4L zT9glBv_Iqx;D+J3#eXJwZpsFM%#tV4t4FA|<7symg7=pzb!su(EtBn)x9_JaP8wxK zHd5h!ik;>&=4O3wd_7Ei{ROrxdTxhiMjCs<6vK6feYb{WyKJ~wF4lft8UkUl43+b) zX8N)w$$si{oN6*7C}YU7H1tcr^uv)j#4ty?YL;zP+evP0PNouX!Le0Z?XOEim7Q&W&D`#^7t3|IM=n+KkT{YXvn5WH#B79V^i4C&d&*wPKGKQ}W1?Tr zZDJnpe2`lXpc~L78PX$ECE_|b|@-=Tb8ip+vkgPfjiMo!c zBUeA#osu)->iQro=trX65J@BqSng*-M6V9}pt*iWXiv549od0da(v4*z1kpTjGP+0 z;t4aA*k(af5C^zLcOK+pPpVj zXdd(K6UAe}@b8WrXd(&F1aZPIE*Z-oPc zfM$R_5mOOOI=g~lLcTi--tInHFIS;0Y_} zQ&{Bd2%r00TzY-pS(=a6!O z%d!ejm@NMwUuQ6}@W!Fac^^$KJ~MJBTyvKubAh=M-IrdUYbfwCjaWwHk3qe_8zoa9 zTk!0a{FM2t-O||$DA(bQXOHf_NI<+bOj*96!!aN8R_dn;rv%s6l2TA9a!hz#=>4UG zQO*ec7=G5mI>$`7hF1vf`;VKn4*0}uL1wDQzmEj@qxON6ERibb>Z4u9$@yPL>Cimc z=W!LcMwh(bjWx`7I;J! zGca%qgD@k*tT_@uLG}_)Usv`=>|7!Oj5+$OW(*8WpFLe1Ln>~)y}L25*+9f0Q23*5 zPEL)Tw6wj;^t2T&SG-E+r0*|&w#w_k_5LL)x~xD`AmBl-TKj_DzZUCf?az-Ef9|Ky zB+%o~qA1eX;iSVP=qA|Z&~njJ!bz~h!bM3Sk;7HtkwB-z5k;ZK4rdi6!S(1ACSEJ`AYsFGWFT_u<#wWdVD zRZO|>EZFAIQY74Ez|y2}i2Hxz%G}sxZ{}Z=2-JVRWp``(a#5hP5>F!sP^VzG0WdKf zDi`@(7^l2<<)hEnW{5>H3w9a&dp+@~%dhAz10Mllkh(5|m?X=edcM8_!AF+?-PLhO zb=qm}#2E@|ASK7lK%)Mgv_KSS+#fIF6 nSquX~e*2>f(Ev-f3<&}8o)a67yWUuR6(r{A>gTe~DWM4fmueJa literal 0 HcmV?d00001 diff --git a/app/style/icons/trezor-hover.png b/app/style/icons/trezor-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..40f98a9d66438a453d71864eb8ea6573e9fd1bbd GIT binary patch literal 2330 zcma)82{0S#7S19`5mig=B@fk>*48LhTkXV}atW@jY70fRwKRQ6Xwj;rw4te`ih7|` zxfR5c*1m_QwAQ$`SHx0cFE?i1o4I|<%$xr||2cE!%y+(j{(sInkL+yB_;@6E0001= zg}E`BquY<<7?g9CI+x{e6x17KW(+tyTCeJh(m5XPP;*Q;0KhABBtQT;ON`@$-L|kc zf&C8S5fyZPmuBh($@~7xX};IO_0r9QQTGb-WIIl=Z*^< zvLo?>Ronq^bE91bnUQFbhddczs6irp;6?`AKc@TO?CJL{bq-#LJaw@}F8&Lr8^%6n zyDi1Sq6A6KGIy2hbH2{pstLL4+i=Kl=`k{%{bFx&t4Fgk9AS|r|EDc%mrHvkmgHL^ zvn^OI{RQwAsK5(0$;x?y4_IA!B?IihiiN46Oq4gTRn_7JT&z^Z9w}-bi71L6H4hz$Gw^K#SfF zQwFF2+p^=t&BZ@`;nv|EbE!zt1lPf(dIx4+n{^pH)@zIu)3ReC7kmvbb4lGXR!e=| ztCmdiRQ!-h8RoAasjsG+nvWPf1T<7b8U7E+!WdJV1w~Qw14b_&rmt-qkzw5T$BEMcxZhH z&-Se)O>ONMdSz>vBlvb1(B#5`<6E3WShq;6%Tbm_4xz?rVWnQ7b?PZy5#(nVnZ=$Y zHQV>nDj2C??aA@X&ck2dP=+d#5o_6*whUSb&<5C8xc)2+E|z;JseSkaOXve_0VbEd z7{ScV^VSO9dl-z!HPk4*wq<+NJ(7ZGB~l!^Iomj8bWh&nNBCpMCW@neIk{I($H>-1x@%x2 z^5ZD=Q7=n9ugGnyb%Po2Z<@M)@AxQhWXY%LcIt}oYCXs_t<>iI0`@v{Sf7!oH5XY= zSbwnRP4;a;wzzOx!ElL{J39vARqT`6{8hZOCA^s8yIho^owi|6ZbXnuzd{P5Mohd@ zGu!*~%Kmf?AtbEa%IU>=;*&luT;dzzVBavfei=TyPhRln+#nH(){o$d9`|^@W;2W% z#Hlg5_-CBf`3g+W{3EZ7KkMw;n?nUA7C&LIFntkNmARO@d+d&Cn^HLo&$m$<3q{N1*_$dS{GyS& zTLR#xvVMp(qB&GnJi+HDL;XYm(em*2*6H2rX;{$xx>3c|HsuiBjasD)@6k%+Mwf@G2LI6s*?R*t-!h;D?yy_xVLq3%W0hN`KV;$(efzngbw-vKyf2QiHj; z?-HS^sPy;bcr~siSk*~!Q%4YHE@-cq1a>7{Nb+4ouHr}vet{yb8g+ej{`3llv&I@RgOTDy3x7>;~ zQeCQJU%JUIS2&E8jXWz3)Pa3Vf~W}0d+As`$_Ui8&`h=SCCJfH>lq0pzd2wL^?W`F z(i2ursYW*cWRlwxPf`%#^oPbJ>f(lD zF#r6ulzO^(558$ASP|2qQowe+lQN&T^lBZF3&cZFlC6JX`eRm{N`evLh7`7dNkQ`1ONK(v-G?Qu1vh!VW zj0U{ZiVv084j?JaFVqt6t4XyePO@AKWt@vk_gYQYnAMzmR)PG=2M6`_V}9*ViVx$e z)FT#I;C2!y(jssZq#lRLG?8*x?awFTVU3kq_9V@W$d_HJ})cmX4D*9%=F+V zZzO2PJdn2cm064;e0N8^0J#@@qP~`1p-G^wUm7=u)exHnI!~Y?X!jkcFSp2Db&yO> zBv6F&$L`T~JgulNuf#N8em&MMgn8XCK4u`RPbu1CZBE?0i69GgE`4~GGEftzwtz1E g^Hd;DIeq|pqgT00-(7L#Ofdip6C2|)L(iB$0J};$`~Uy| literal 0 HcmV?d00001