Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

multi: Allow trezor ticket purchasing on testnet. #3848

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
403 changes: 398 additions & 5 deletions app/actions/TrezorActions.js

Large diffs are not rendered by default.

13 changes: 6 additions & 7 deletions app/components/SideBar/MenuLinks/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function useMenuLinks() {
const isTrezor = useSelector(sel.isTrezor);
const isLedger = useSelector(sel.isLedger);
const lnEnabled = useSelector(sel.lnEnabled);
const isTestNet = useSelector(sel.isTestNet);

const newActiveVoteProposalsCount = useSelector(
sel.newActiveVoteProposalsCount
Expand Down Expand Up @@ -54,14 +55,12 @@ export function useMenuLinks() {
if (!lnEnabled) {
links = links.filter((l) => l.key !== LN_KEY);
}
// TODO: Enable ticket purchacing for Trezor.
// TODO: Enable ticket purchacing for Trezor on mainnet.
if (isTrezor || isLedger) {
links = links.filter(
(l) => l.key !== DEX_KEY && l.key !== TICKETS_KEY && l.key !== GOV_KEY
);
links = links.filter((l) => l.key !== DEX_KEY && l.key !== GOV_KEY);
}
if (isLedger) {
links = links.filter((l) => l.key !== PRIV_KEY);
if (isLedger || (isTrezor && !isTestNet)) {
links = links.filter((l) => l.key !== PRIV_KEY && l.key !== TICKETS_KEY);
}
return links.map((link) => ({
...link,
Expand All @@ -70,7 +69,7 @@ export function useMenuLinks() {
0
)
}));
}, [notifProps, isTrezor, lnEnabled, isLedger]);
}, [notifProps, isTrezor, lnEnabled, isLedger, isTestNet]);

const [activeTabIndex, setActiveTabIndex] = useState(-1);
const history = useHistory();
Expand Down
6 changes: 5 additions & 1 deletion app/components/buttons/SendTransactionButton/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ export function useSendTransactionButton() {
dispatch(ca.signTransactionAttempt(passphrase, rawTx, acct));
};
const onAttemptSignTransactionTrezor = (rawUnsigTx, constructTxResponse) =>
dispatch(tza.signTransactionAttemptTrezor(rawUnsigTx, constructTxResponse));
dispatch(
tza.signTransactionAttemptTrezor(rawUnsigTx, [
constructTxResponse.changeIndex
])
);
const onAttemptSignTransactionLedger = (rawUnsigTx) =>
dispatch(ldgr.signTransactionAttemptLedger(rawUnsigTx));

Expand Down
5 changes: 3 additions & 2 deletions app/components/views/HomePage/HomePage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,12 @@ export default () => {
tsDate
} = useHomePage();

// TODO: Enable ticket purchacing for Trezor.
// TODO: Enable ticket purchacing for Trezor on mainnet.
const isTrezor = useSelector(sel.isTrezor);
const isLedger = useSelector(sel.isLedger);
const isTestNet = useSelector(sel.isTestNet);
let recentTickets, tabs;
if (isTrezor || isLedger) {
if ((isTrezor && !isTestNet) || isLedger) {
tabs = [balanceTab, transactionsTab];
} else {
recentTickets = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export function PurchaseTabPage({
isVSPListingEnabled,
onEnableVSPListing,
getRunningIndicator,
isPurchasingTicketsTrezor,
...props
}) {
return (
Expand Down Expand Up @@ -115,7 +116,9 @@ export function PurchaseTabPage({
isLoading,
rememberedVspHost,
toggleRememberVspHostCheckBox,
getRunningIndicator
getRunningIndicator,
isPurchasingTicketsTrezor,
isWatchingOnly
}}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ const PurchaseTicketsForm = ({
rememberedVspHost,
toggleRememberVspHostCheckBox,
notMixedAccounts,
getRunningIndicator
getRunningIndicator,
isPurchasingTicketsTrezor
}) => {
const intl = useIntl();
return (
Expand Down Expand Up @@ -149,7 +150,10 @@ const PurchaseTicketsForm = ({
</div>
<div className={styles.buttonsArea}>
{isWatchingOnly ? (
<PiUiButton disabled={!isValid} onClick={onPurchaseTickets}>
<PiUiButton
disabled={!isValid}
loading={isPurchasingTicketsTrezor}
onClick={onPurchaseTickets}>
{purchaseLabel()}
</PiUiButton>
) : isLoading ? (
Expand Down
19 changes: 15 additions & 4 deletions app/components/views/TicketsPage/PurchaseTab/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useCallback, useMemo } from "react";
import { useSettings } from "hooks";
import { EXTERNALREQUEST_STAKEPOOL_LISTING } from "constants";

import { purchaseTicketsAttempt as trezorPurchseTicketsAttempt } from "actions/TrezorActions.js";
import * as vspa from "actions/VSPActions";
import * as ca from "actions/ControlActions.js";
import * as sel from "selectors";
Expand All @@ -21,6 +22,8 @@ export const usePurchaseTab = () => {
const ticketAutoBuyerRunning = useSelector(sel.getTicketAutoBuyerRunning);
const isLoading = useSelector(sel.purchaseTicketsRequestAttempt);
const notMixedAccounts = useSelector(sel.getNotMixedAccounts);
const isTrezor = useSelector(sel.isTrezor);
const isPurchasingTicketsTrezor = useSelector(sel.isPurchasingTicketsTrezor);

const rememberedVspHost = useSelector(sel.getRememberedVspHost);
const visibleAccounts = useSelector(sel.visibleAccounts);
Expand Down Expand Up @@ -54,9 +57,16 @@ export const usePurchaseTab = () => {
[dispatch]
);
const purchaseTicketsAttempt = useCallback(
(passphrase, account, numTickets, vsp) =>
dispatch(ca.purchaseTicketsAttempt(passphrase, account, numTickets, vsp)),
[dispatch]
(passphrase, account, numTickets, vsp) => {
if (isTrezor) {
dispatch(trezorPurchseTicketsAttempt(account, numTickets, vsp));
} else {
dispatch(
ca.purchaseTicketsAttempt(passphrase, account, numTickets, vsp)
);
}
},
[dispatch, isTrezor]
);

const setRememberedVspHost = useCallback(
Expand Down Expand Up @@ -140,6 +150,7 @@ export const usePurchaseTab = () => {
vsp,
setVSP,
numTicketsToBuy,
setNumTicketsToBuy
setNumTicketsToBuy,
isPurchasingTicketsTrezor
};
};
1 change: 1 addition & 0 deletions app/helpers/msgTx.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ export function decodeRawTransaction(rawTx) {
position += 4;
tx.expiry = rawTx.readUInt32LE(position);
position += 4;
tx.prefixOffset = position;
}

if (tx.serType !== SERTYPE_NOWITNESS) {
Expand Down
11 changes: 7 additions & 4 deletions app/helpers/trezor.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ export const addressPath = (index, branch, account, coinType) => {
};

// walletTxToBtcjsTx is a aux function to convert a tx decoded by the decred wallet (ie,
// returned from wallet.decoreRawTransaction call) into a bitcoinjs-compatible
// returned from wallet.decodeRawTransaction call) into a bitcoinjs-compatible
// transaction (to be used in trezor).
export const walletTxToBtcjsTx = async (
walletService,
chainParams,
tx,
inputTxs,
changeIndex
changeIndexes
) => {
const inputs = tx.inputs.map(async (inp) => {
const addr = inp.outpointAddress;
Expand Down Expand Up @@ -81,7 +81,7 @@ export const walletTxToBtcjsTx = async (
const addrValidResp = await wallet.validateAddress(walletService, addr);
if (!addrValidResp.isValid) throw "Not a valid address: " + addr;
let address_n = null;
if (i === changeIndex && addrValidResp.isMine) {
if (changeIndexes.includes(i) && addrValidResp.isMine) {
const addrIndex = addrValidResp.index;
const addrBranch = addrValidResp.isInternal ? 1 : 0;
address_n = addressPath(
Expand Down Expand Up @@ -124,7 +124,10 @@ export const walletTxToRefTx = async (walletService, tx) => {
const outputs = tx.outputs.map(async (outp) => {
const addr = outp.decodedScript.address;
const addrValidResp = await wallet.validateAddress(walletService, addr);
if (!addrValidResp.isValid) throw new Error("Not a valid address: " + addr);
// Scripts with zero value can be ignored as they are not a concern when
// spending from an outpoint.
if (outp.value != 0 && !addrValidResp.isValid)
throw new Error("Not a valid address: " + addr);
return {
amount: outp.value,
script_pubkey: rawToHex(outp.script),
Expand Down
14 changes: 14 additions & 0 deletions app/main_dev/externalRequests.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ export const installSessionHandlers = (mainLogger) => {
`connect-src ${connectSrc}; `;
}

const requestURL = new URL(details.url);
const maybeVSPReqType = `stakepool_${requestURL.protocol}//${requestURL.host}`;
const isVSPRequest = allowedExternalRequests[maybeVSPReqType];

if (isDev && /^http[s]?:\/\//.test(details.url)) {
// In development (when accessing via the HMR server) we need to overwrite
// the origin, otherwise electron fails to contact external servers due
Expand All @@ -144,6 +148,12 @@ export const installSessionHandlers = (mainLogger) => {
newHeaders["Access-Control-Allow-Headers"] = "Content-Type";
}

if (isVSPRequest && details.method === "OPTIONS") {
statusLine = "OK";
newHeaders["Access-Control-Allow-Headers"] =
"Content-Type,vsp-client-signature";
}

const globalCfg = getGlobalCfg();
const cfgAllowedVSPs = globalCfg.get(cfgConstants.ALLOWED_VSP_HOSTS, []);
if (cfgAllowedVSPs.some((url) => details.url.includes(url))) {
Expand Down Expand Up @@ -204,6 +214,10 @@ export const allowVSPRequests = (stakePoolHost) => {

addAllowedURL(stakePoolHost + "/api/v3/vspinfo");
addAllowedURL(stakePoolHost + "/api/v3/ticketstatus");
addAllowedURL(stakePoolHost + "/api/v3/feeaddress");
addAllowedURL(stakePoolHost + "/api/v3/payfee");
addAllowedURL(stakePoolHost + "/api/ticketstatus");
allowedExternalRequests[reqType] = true;
};

export const reloadAllowedExternalRequests = () => {
Expand Down
22 changes: 22 additions & 0 deletions app/middleware/vspapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,25 @@ export function getVSPTicketStatus({ host, sig, json }, cb) {
.then((resp) => cb(resp, null, host))
.catch((error) => cb(null, error, host));
}

// getFeeAddress gets a ticket`s fee address.
export function getFeeAddress({ host, sig, req }, cb) {
console.log(req);
POST(host + "/api/v3/feeaddress", sig, req)
.then((resp) => cb(resp, null, host))
.catch((error) => cb(null, error, host));
}

// payFee informs of a ticket`s fee payment.
export function payFee({ host, sig, req }, cb) {
console.log(req);
POST(host + "/api/v3/payfee", sig, req)
.then((resp) => cb(resp, null, host))
.catch((error) => cb(null, error, host));
}

export function getTicketStatus({ host, vspClientSig, request }, cb) {
POST(host + "/api/ticketstatus", vspClientSig, request)
.then((resp) => cb(resp, null, host))
.catch((error) => cb(null, error, host));
}
17 changes: 16 additions & 1 deletion app/reducers/trezor.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import {
TRZ_PASSPHRASE_REQUESTED,
TRZ_PASSPHRASE_ENTERED,
TRZ_PASSPHRASE_CANCELED,
TRZ_PURCHASETICKET_ATTEMPT,
TRZ_PURCHASETICKET_FAILED,
TRZ_PURCHASETICKET_SUCCESS,
TRZ_WORD_REQUESTED,
TRZ_WORD_ENTERED,
TRZ_WORD_CANCELED,
Expand Down Expand Up @@ -284,6 +287,12 @@ export default function trezor(state = {}, action) {
performingOperation: false,
performingTogglePassphraseOnDeviceProtection: false
};
case TRZ_PURCHASETICKET_ATTEMPT:
return {
...state,
performingOperation: true,
purchasingTickets: true
};
case SIGNTX_FAILED:
case SIGNTX_SUCCESS:
case TRZ_CHANGEHOMESCREEN_FAILED:
Expand All @@ -310,7 +319,6 @@ export default function trezor(state = {}, action) {
performingOperation: false,
performingUpdate: false
};

case TRZ_TOGGLEPINPROTECTION_FAILED:
case TRZ_TOGGLEPINPROTECTION_SUCCESS:
return {
Expand All @@ -330,6 +338,13 @@ export default function trezor(state = {}, action) {
performingOperation: false,
performingTogglePassphraseOnDeviceProtection: false
};
case TRZ_PURCHASETICKET_FAILED:
case TRZ_PURCHASETICKET_SUCCESS:
return {
...state,
performingOperation: false,
purchasingTickets: false
};
case CLOSEWALLET_SUCCESS:
return { ...state };
case TRZ_UDEV_ERROR:
Expand Down
1 change: 1 addition & 0 deletions app/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,7 @@ export const confirmationDialogModalVisible = bool(
export const isTrezor = get(["trezor", "enabled"]);
export const isPerformingTrezorUpdate = get(["trezor", "performingUpdate"]);
export const trezorUdevError = get(["trezor", "udevError"]);
export const isPurchasingTicketsTrezor = get(["trezor", "purchasingTickets"]);

export const isLedger = get(["ledger", "enabled"]);

Expand Down
2 changes: 2 additions & 0 deletions app/wallet/control.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ export const purchaseTickets = (
resObj.ticketHashes = response
.getTicketHashesList()
.map((v) => rawHashToHex(v));
resObj.splitTx = Buffer.from(response.getSplitTx());
resObj.ticketsList = response.getTicketsList().map((v) => Buffer.from(v));
resolve(resObj);
});
});
Expand Down
5 changes: 5 additions & 0 deletions app/wallet/vsp.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ const promisifyReqLogNoData = (fnName, Req) =>
);

export const getVSPInfo = promisifyReqLogNoData("getVSPInfo", api.getVSPInfo);
export const getVSPFeeAddress = promisifyReqLogNoData(
"getFeeAddress",
api.getFeeAddress
);
export const payVSPFee = promisifyReqLogNoData("getFeeAddress", api.payFee);
export const getVSPTicketStatus = promisifyReqLogNoData(
"getVSPTicketStatus",
api.getVSPTicketStatus
Expand Down
3 changes: 3 additions & 0 deletions test/data/decodedTransactions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const decodedPurchasedTicketTx = {
"version": 1,
"prefixOffset": 172,
"serType": 0,
"numInputs": 1,
"inputs": [
Expand Down Expand Up @@ -56,6 +57,7 @@ export const decodedPurchasedTicketTx = {
// multiTxPrefix is a tx prefix in the format of how our decodeTxs are. We get
// this format from wallet.decodeRawTransaction().
export const multiTxPrefix = {
prefixOffset: 211,
serType: 1, // TxSerializeNoWitness,
version: 1,
numInputs: 1,
Expand Down Expand Up @@ -113,6 +115,7 @@ export const multiTxPrefix = {

export const decodedVoteTx = {
"version": 1,
"prefixOffset": 201,
"serType": 0,
"numInputs": 2,
"inputs": [
Expand Down
9 changes: 5 additions & 4 deletions test/unit/components/buttons/SendTransactionButton.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,11 @@ test("render SendTransactionButton when trezor is enabled", async () => {
const button = screen.getByRole("button");
user.click(button);
expect(mockSignTransactionAttempt).not.toHaveBeenCalled();
expect(mockSignTransactionAttemptTrezor).toHaveBeenCalledWith(
testUnsignedTransaction,
testConstructTxResponse
);
expect(
mockSignTransactionAttemptTrezor
).toHaveBeenCalledWith(testUnsignedTransaction, [
testConstructTxResponse.changeIndex
]);
await wait(() => expect(mockOnSubmit).toHaveBeenCalled());
});

Expand Down
Loading