From 9fb082bd86be208ed73a10e1af9f7fa63c8c1e3d Mon Sep 17 00:00:00 2001 From: Matheus Degiovani Date: Fri, 6 Jul 2018 16:41:49 -0300 Subject: [PATCH] Trezor sign tx support --- app/actions/TrezorActions.js | 184 +++++++++++++++++- .../buttons/SendTransactionButton.js | 48 +++-- app/connectors/send.js | 4 + app/selectors.js | 10 +- app/wallet/constants.js | 6 +- 5 files changed, 232 insertions(+), 20 deletions(-) diff --git a/app/actions/TrezorActions.js b/app/actions/TrezorActions.js index c6cdf25a4a..24ae084b02 100644 --- a/app/actions/TrezorActions.js +++ b/app/actions/TrezorActions.js @@ -1,7 +1,30 @@ import * as trezorjs from "trezor.js"; import trezorTransports from "trezor-link"; import * as wallet from "wallet"; +import * as selectors from "selectors"; +import { sprintf } from "sprintf-js"; +import { rawHashToHex, rawToHex, hexToRaw } from "helpers"; +import { publishTransactionAttempt } from "./ControlActions"; + import { EXTERNALREQUEST_TREZOR_BRIDGE } from "main_dev/externalRequests"; +import { SIGNTX_ATTEMPT, SIGNTX_FAILED, SIGNTX_SUCCESS } from "./ControlActions"; + +const hardeningConstant = 0x80000000; + +// Right now (2018-07-06) dcrwallet only supports a single account on watch only +// wallets. Therefore we are limited to using this single account when signing +// transactions via trezor. +const WALLET_ACCOUNT = 0; + +function addressPath(index, branch, account, coinType) { + return [ + (44 | hardeningConstant) >>> 0, // purpose + ((coinType || 0)| hardeningConstant) >>> 0, // coin type + ((account || 0) | hardeningConstant) >>> 0, // account + (branch || 0) >>> 0, // branch + index >>> 0 // index + ]; +} export const TRZ_LOADDEVICELIST_ATTEMPT = "TRZ_LOADDEVICELIST_ATTEMPT"; export const TRZ_LOADDEVICELIST_FAILED = "TRZ_LOADDEVICELIST_FAILED"; @@ -78,7 +101,7 @@ export const loadDeviceList = () => (dispatch, getState) => { }); devList.on("disconnectUnacquired", device => { - console.log("disconnect unacquired", device); + console.log("d.catch(error => dispatch({ error, type: SIGNTX_FAILED }));isconnect unacquired", device); }); }); @@ -89,3 +112,162 @@ export const selectDevice = (path) => async (dispatch, getState) => { if (!devList.devices[path]) return; dispatch({ device: devList.devices[path], type: TRZ_SELECTEDDEVICE_CHANGED }); }; + +export const signTransactionAttemptTrezor = (rawUnsigTx, constructTxResponse) => async (dispatch, getState) => { + dispatch({ type: SIGNTX_ATTEMPT }); + + const { grpc: { decodeMessageService, walletService } } = getState(); + const chainParams = selectors.chainParams(getState()); + + const device = selectors.trezorDevice(getState()); + // TODO: handle not having device + + console.log("construct tx response", constructTxResponse); + + try { + const decodedUnsigTxResp = await wallet.decodeTransaction(decodeMessageService, rawUnsigTx); + const decodedUnsigTx = decodedUnsigTxResp.getTransaction(); + const inputTxs = await wallet.getInputTransactions(walletService, + decodeMessageService, decodedUnsigTx); + const refTxs = inputTxs.map(walletTxToRefTx); + + const changeIndex = constructTxResponse.getChangeIndex(); + const txInfo = await dispatch(walletTxToBtcjsTx(decodedUnsigTx, + changeIndex, inputTxs)); + + const signedRaw = await device.run(async session => { + const signedResp = await session.signTx(txInfo.inputs, txInfo.outputs, + refTxs, chainParams.trezorCoinName, 0); + return signedResp.message.serialized.serialized_tx; + }); + + dispatch({ type: SIGNTX_SUCCESS }); + dispatch(publishTransactionAttempt(hexToRaw(signedRaw))); + + } catch (error) { + dispatch({ error, type: SIGNTX_FAILED }); + } +}; + +// walletTxToBtcjsTx converts a tx decoded by the decred wallet (ie, +// returned from the decodeRawTransaction call) into a bitcoinjs-compatible +// transaction (to be used in trezor) +export const walletTxToBtcjsTx = (tx, changeIndex, inputTxs) => async (dispatch, getState) => { + const { grpc: { walletService } } = getState(); + const chainParams = selectors.chainParams(getState()); + + const inputTxsMap = inputTxs.reduce((m, tx) => { + m[rawHashToHex(tx.getTransactionHash())] = tx; + return m; + }, {}); + + const inputs = []; + for (const inp of tx.getInputsList()) { + const inputTx = inputTxsMap[rawHashToHex(inp.getPreviousTransactionHash())]; + if (!inputTx) throw "Cannot sign transaction without knowing source tx " + + rawHashToHex(inp.getPreviousTransactionHash()); + + const inputTxOut = inputTx.getOutputsList()[inp.getPreviousTransactionIndex()]; + if (!inputTxOut) throw sprintf("Trying to use unknown outpoint %s:%d as input", + rawHashToHex(inp.getPreviousTransactionHash()), inp.getPreviousTransactionIndex()); + + const addr = inputTxOut.getAddressesList()[0]; + if (!addr) throw sprintf("Outpoint %s:%d does not have addresses.", + rawHashToHex(inp.getPreviousTransactionHash()), inp.getPreviousTransactionIndex()); + + const addrValidResp = await wallet.validateAddress(walletService, addr); + if (!addrValidResp.getIsValid()) throw "Input has an invalid address " + addr; + + // Trezor firmware (mcu) currently (2018-06-25) only support signing + // when all inputs of the transaction are from the wallet. This happens + // due to the fact that trezor firmware re-calculates the source + // pkscript given the address_n of the input, instead of using it (the + // pkscript) directly when hashing the tx prior to signing. This needs + // to be changed so that we can perform more advanced types of + // transactions. + if (!addrValidResp.getIsMine()) throw "Trezor only supports signing when all inputs are from the wallet."; + + const addrIndex = addrValidResp.getIndex(); + const addrBranch = addrValidResp.getIsInternal() ? 1 : 0; + inputs.push({ + prev_hash: rawHashToHex(inp.getPreviousTransactionHash()), + prev_index: inp.getPreviousTransactionIndex(), + amount: inp.getAmountIn(), + sequence: inp.getSequence(), + address_n: addressPath(addrIndex, addrBranch, WALLET_ACCOUNT, + chainParams.HDCoinType), + + // FIXME: this needs to be supported on trezor.js. + // decredTree: inp.getTree(), + // decredScriptVersion: 0, + }); + } + + const outputs = []; + for (const outp of tx.getOutputsList()) { + if (outp.getAddressesList().length != 1) { + // TODO: this will be true on OP_RETURNs. Support those. + throw "Output has different number of addresses than expected"; + } + + let addr = outp.getAddressesList()[0]; + const addrValidResp = await wallet.validateAddress(walletService, addr); + if (!addrValidResp.getIsValid()) throw "Not a valid address: " + addr; + let address_n = null; + + if (outp.getIndex() === changeIndex) { + const addrIndex = addrValidResp.getIndex(); + const addrBranch = addrValidResp.getIsInternal() ? 1 : 0; + address_n = addressPath(addrIndex, addrBranch, WALLET_ACCOUNT, + chainParams.HDCoinType); + addr = null; + } + + outputs.push({ + amount: outp.getValue(), + script_type: "PAYTOADDRESS", // needs to change on OP_RETURNs + address: addr, + address_n: address_n, + }); + } + + const txInfo = { + lock_time: tx.getLockTime(), + version: tx.getVersion(), + expiry: tx.getExpiry(), + inputs, + outputs + }; + + return txInfo; +}; + +// walletTxToRefTx converts a tx decoded by the decred wallet into a trezor +// RefTransaction object to be used with SignTx. +export function walletTxToRefTx(tx) { + const inputs = tx.getInputsList().map(inp => ({ + amount: inp.getAmountIn(), + prev_hash: rawHashToHex(inp.getPreviousTransactionHash()), + prev_index: inp.getPreviousTransactionIndex(), + + // TODO: this needs to be supported on trezor.js + // decredTree: inp.getTree(), + // decredScriptVersion: 0, + })); + + const bin_outputs = tx.getOutputsList().map(outp => ({ + amount: outp.getValue(), + script_pubkey: rawToHex(outp.getScript()), + })); + + const txInfo = { + hash: rawHashToHex(tx.getTransactionHash()), + lock_time: tx.getLockTime(), + version: tx.getVersion(), + expiry: tx.getExpiry(), + inputs, + bin_outputs, + }; + + return txInfo; +} diff --git a/app/components/buttons/SendTransactionButton.js b/app/components/buttons/SendTransactionButton.js index 9102838256..510e93e210 100644 --- a/app/components/buttons/SendTransactionButton.js +++ b/app/components/buttons/SendTransactionButton.js @@ -1,5 +1,6 @@ import { send } from "connectors"; import { PassphraseModalButton } from "./index"; +import KeyBlueButton from "./KeyBlueButton"; import { FormattedMessage as T } from "react-intl"; @autobind @@ -16,22 +17,41 @@ class SendTransactionButton extends React.Component { onSubmit && onSubmit(); } + async onAttemptSignTransactionTrezor() { + const { unsignedTransaction, onAttemptSignTransactionTrezor, + constructTxResponse, disabled, onSubmit } = this.props; + if (disabled || !onAttemptSignTransactionTrezor) return; + await onAttemptSignTransactionTrezor(unsignedTransaction, constructTxResponse); + onSubmit && onSubmit(); + } + render() { - const { disabled, isSendingTransaction, onShow, showModal, children } = this.props; + const { disabled, isSendingTransaction, children, isTrezor } = this.props; - return ( - } - modalDescription={children} - showModal={showModal} - onShow={onShow} - disabled={disabled || isSendingTransaction} - className="content-send" - onSubmit={this.onAttemptSignTransaction} - loading={isSendingTransaction} - buttonLabel={} - /> - ); + if (isTrezor) { + return ( + + + + ); + } else { + return ( + } + modalDescription={children} + disabled={disabled || isSendingTransaction} + className="content-send" + onSubmit={this.onAttemptSignTransaction} + loading={isSendingTransaction} + buttonLabel={} + /> + ); + } } } diff --git a/app/connectors/send.js b/app/connectors/send.js index cfc96d5c08..ca97bd9f4e 100644 --- a/app/connectors/send.js +++ b/app/connectors/send.js @@ -3,6 +3,7 @@ import { bindActionCreators } from "redux"; import { selectorMap } from "../fp"; import * as sel from "../selectors"; import * as ca from "../actions/ControlActions"; +import * as tza from "../actions/TrezorActions"; const mapStateToProps = selectorMap({ defaultSpendingAccount: sel.defaultSpendingAccount, @@ -18,6 +19,8 @@ const mapStateToProps = selectorMap({ unitDivisor: sel.unitDivisor, constructTxLowBalance: sel.constructTxLowBalance, isTransactionsSendTabDisabled: sel.isTransactionsSendTabDisabled, + constructTxResponse: sel.constructTxResponse, + isTrezor: sel.isTrezor, }); const mapDispatchToProps = dispatch => bindActionCreators({ @@ -26,6 +29,7 @@ const mapDispatchToProps = dispatch => bindActionCreators({ onClearTransaction: ca.clearTransaction, getNextAddressAttempt: ca.getNextAddressAttempt, validateAddress: ca.validateAddress, + onAttemptSignTransactionTrezor: tza.signTransactionAttemptTrezor, }, dispatch); export default connect(mapStateToProps, mapDispatchToProps); diff --git a/app/selectors.js b/app/selectors.js index 3b3135d32f..96bbc9fec2 100644 --- a/app/selectors.js +++ b/app/selectors.js @@ -611,7 +611,7 @@ export const defaultSpendingAccount = createSelector( export const changePassphraseRequestAttempt = get([ "control", "changePassphraseRequestAttempt" ]); export const constructTxLowBalance = get([ "control", "constructTxLowBalance" ]); -const constructTxResponse = get([ "control", "constructTxResponse" ]); +export const constructTxResponse = get([ "control", "constructTxResponse" ]); const constructTxRequestAttempt = get([ "control", "constructTxRequestAttempt" ]); const signTransactionRequestAttempt = get([ "control", "signTransactionRequestAttempt" ]); export const signTransactionError = get([ "control", "signTransactionError" ]); @@ -909,10 +909,12 @@ export const stakeRewardsStats = createSelector( export const modalVisible = get([ "control", "modalVisible" ]); -export const isSignMessageDisabled = isWatchingOnly; +export const isTrezor = get([ "trezor", "enabled" ]); + +export const isSignMessageDisabled = and(isWatchingOnly, not(isTrezor)); export const isCreateAccountDisabled = isWatchingOnly; export const isChangePassPhraseDisabled = isWatchingOnly; -export const isTransactionsSendTabDisabled = isWatchingOnly; +export const isTransactionsSendTabDisabled = and(isWatchingOnly, not(isTrezor)); export const isTicketPurchaseTabDisabled = isWatchingOnly; export const politeiaURL = createSelector( @@ -940,3 +942,5 @@ export const viewedProposalDetails = createSelector( [ proposalDetails, viewedProposalToken ], (proposals, token) => proposals[token] ); + +export const trezorDevice = get([ "trezor", "device" ]); diff --git a/app/wallet/constants.js b/app/wallet/constants.js index 45749b4219..1f9095b9c1 100644 --- a/app/wallet/constants.js +++ b/app/wallet/constants.js @@ -7,13 +7,14 @@ export const TestNetParams = { SStxChangeMaturity: 1, GenesisTimestamp: 1489550400, TargetTimePerBlock: 2 * 60, // in seconds + WorkDiffWindowSize: 144, // no way to know which one the wallet is using right now, so we record both // types for the moment. LegacyHDCoinType: 11, HDCoinType: 1, - WorkDiffWindowSize: 144, + trezorCoinName: "Decred Testnet", }; export const MainNetParams = { @@ -23,11 +24,12 @@ export const MainNetParams = { SStxChangeMaturity: 1, GenesisTimestamp: 1454954400, TargetTimePerBlock: 5 * 60, // in seconds + WorkDiffWindowSize: 144, // no way to know which one the wallet is using right now, so we record both // types for the moment. LegacyHDCoinType: 20, HDCoinType: 42, - WorkDiffWindowSize: 144, + trezorCoinName: "Decred", };