Skip to content

Commit

Permalink
multi: Allow trezor ticket purchasing on testnet.
Browse files Browse the repository at this point in the history
Add purchaseTicketsV3 which purchases tickets using a watching only
trezor wallet from a vsp with api v3. Errors if not on testnet.

Add separate payVSPFee function that will pay a tickets fee if not
already paid, and throws if already paid.

Correct purchase ticket button for watching only wallets. It no longer
asks for a password. Connect purchasing through trezor when isTrezor is
true.

Add vsp v3 endpoints "feeaddress" and "payfee" to allowed external
requests.

Change wallet/control.js to not require an unlocked wallet when signTx
is false.

Add headers to OPTIONS externalRequest. These are required by CORS when
making POST requests.
  • Loading branch information
JoeGruffins committed Dec 28, 2022
1 parent 372e7f6 commit 3b6dbee
Show file tree
Hide file tree
Showing 15 changed files with 528 additions and 21 deletions.
402 changes: 397 additions & 5 deletions app/actions/TrezorActions.js

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion app/components/buttons/SendTransactionButton/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,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
])
);

return {
unsignedTransaction,
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,18 @@ const PurchaseTicketsForm = ({
rememberedVspHost,
toggleRememberVspHostCheckBox,
notMixedAccounts,
getRunningIndicator
getRunningIndicator,
isLegacy,
dismissBackupRedeemScript,
onDismissBackupRedeemScript,
isShowingAdvanced,
onToggleShowAdvanced,
getQuickBarComponent,
getAdvancedComponent,
willEnter,
willLeave,
toggleShowVsp,
isPurchasingTicketsTrezor
}) => {
const intl = useIntl();
return (
Expand Down Expand Up @@ -149,7 +160,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
33 changes: 30 additions & 3 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,8 +57,31 @@ export const usePurchaseTab = () => {
[dispatch]
);
const purchaseTicketsAttempt = useCallback(
(passphrase, account, numTickets, vsp) =>
dispatch(ca.purchaseTicketsAttempt(passphrase, account, numTickets, vsp)),
(passphrase, account, numTickets, vsp) => {
if (isTrezor) {
dispatch(trezorPurchseTicketsAttempt(account, numTickets, vsp));
} else {
dispatch(
ca.purchaseTicketsAttempt(passphrase, account, numTickets, vsp)
);
}
},
[dispatch, isTrezor]
);
const onEnableTicketAutoBuyer = useCallback(
(passphrase, account, balanceToMaintain, vsp) =>
dispatch(
ca.startTicketBuyerV3Attempt(
passphrase,
account,
balanceToMaintain,
vsp
)
),
[dispatch]
);
const onDisableTicketAutoBuyer = useCallback(
() => dispatch(ca.ticketBuyerCancel()),
[dispatch]
);

Expand Down Expand Up @@ -140,6 +166,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 infomrs 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 @@ -279,6 +282,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 @@ -305,7 +314,6 @@ export default function trezor(state = {}, action) {
performingOperation: false,
performingUpdate: false
};

case TRZ_TOGGLEPINPROTECTION_FAILED:
case TRZ_TOGGLEPINPROTECTION_SUCCESS:
return {
Expand All @@ -325,6 +333,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, enabled: false };
default:
Expand Down
1 change: 1 addition & 0 deletions app/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,7 @@ export const confirmationDialogModalVisible = bool(

export const isTrezor = get(["trezor", "enabled"]);
export const isPerformingTrezorUpdate = get(["trezor", "performingUpdate"]);
export const isPurchasingTicketsTrezor = get(["trezor", "purchasingTickets"]);

export const isSignMessageDisabled = and(isWatchingOnly, not(isTrezor));
export const isChangePassPhraseDisabled = isWatchingOnly;
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

0 comments on commit 3b6dbee

Please sign in to comment.