Skip to content

Commit

Permalink
Trezor sign tx support
Browse files Browse the repository at this point in the history
  • Loading branch information
matheusd committed Oct 10, 2018
1 parent 314b2f9 commit 9fb082b
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 20 deletions.
184 changes: 183 additions & 1 deletion app/actions/TrezorActions.js
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
});

});
Expand All @@ -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;
}
48 changes: 34 additions & 14 deletions app/components/buttons/SendTransactionButton.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { send } from "connectors";
import { PassphraseModalButton } from "./index";
import KeyBlueButton from "./KeyBlueButton";
import { FormattedMessage as T } from "react-intl";

@autobind
Expand All @@ -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 (
<PassphraseModalButton
modalTitle={<T id="send.sendConfirmations" m="Transaction Confirmation" />}
modalDescription={children}
showModal={showModal}
onShow={onShow}
disabled={disabled || isSendingTransaction}
className="content-send"
onSubmit={this.onAttemptSignTransaction}
loading={isSendingTransaction}
buttonLabel={<T id="send.sendBtn" m="Send" />}
/>
);
if (isTrezor) {
return (
<KeyBlueButton
onClick={this.onAttemptSignTransactionTrezor}
disabled={disabled || isSendingTransaction}
className="content-send"
loading={isSendingTransaction}
>
<T id="send.sendBtn" m="Send" />
</KeyBlueButton>
);
} else {
return (
<PassphraseModalButton
modalTitle={<T id="send.sendConfirmations" m="Transaction Confirmation" />}
modalDescription={children}
disabled={disabled || isSendingTransaction}
className="content-send"
onSubmit={this.onAttemptSignTransaction}
loading={isSendingTransaction}
buttonLabel={<T id="send.sendBtn" m="Send" />}
/>
);
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions app/connectors/send.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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({
Expand All @@ -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);
10 changes: 7 additions & 3 deletions app/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" ]);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -940,3 +942,5 @@ export const viewedProposalDetails = createSelector(
[ proposalDetails, viewedProposalToken ],
(proposals, token) => proposals[token]
);

export const trezorDevice = get([ "trezor", "device" ]);
6 changes: 4 additions & 2 deletions app/wallet/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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",
};

0 comments on commit 9fb082b

Please sign in to comment.