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 98d58d4
Show file tree
Hide file tree
Showing 22 changed files with 517 additions and 29 deletions.
402 changes: 397 additions & 5 deletions app/actions/TrezorActions.js

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions app/components/Snackbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ const Snackbar = () => {
className={snackbarClasses(message || "")}
onMouseEnter={clearHideTimer}
onMouseLeave={enableHideTimer}
style={{ bottom: "0px" }}>
style={{ bottom: "0px" }}
>
<Notification
{...{
topNotification: i === 0,
Expand Down Expand Up @@ -143,7 +144,8 @@ const Snackbar = () => {
onMouseEnter={clearHideTimer}
onMouseLeave={enableHideTimer}
style={s.style}
ref={(ref) => animatedNotifRef(s.key, ref)}>
ref={(ref) => animatedNotifRef(s.key, ref)}
>
<Notification
{...{
topNotification: i === 0,
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 @@ -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
3 changes: 2 additions & 1 deletion app/components/views/GetStartedPage/SetupWallet/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ export const useWalletSetup = (settingUpWalletRef) => {
<ExternalLink
href={
"https://docs.decred.org/wallets/decrediton/migrations"
}>
}
>
<T id="getstarted.setAccountsPass.docs" m="Decred docs" />
</ExternalLink>
)
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
3 changes: 2 additions & 1 deletion app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,8 @@ const render = () =>
<ThemeProvider
themes={themes}
defaultThemeName={defaultThemeName}
fonts={fonts}>
fonts={fonts}
>
<Provider store={store}>
<ConnectedRouter history={history}>
<Switch>
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
6 changes: 4 additions & 2 deletions test/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ function render(ui, renderOptions) {
messages={locale.messages}
formats={locale.formats}
defaultFormats={defaultFormats}
key={locale.key}>
key={locale.key}
>
{children}
<div id="modal-portal" />
</IntlProvider>
Expand All @@ -93,7 +94,8 @@ function render(ui, renderOptions) {
<ThemeProvider
themes={themes}
defaultThemeName={initialState.settings.currentSettings.theme}
fonts={fonts}>
fonts={fonts}
>
<Provider store={store}>
<ConnectedRouter history={history}>
<Switch>
Expand Down
3 changes: 2 additions & 1 deletion test/unit/components/SideBar/LastBlockTime.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ const Wrapper = ({ lastBlockTimestamp, setTimeout, clearTimeout }) => {
messages={locale.messages}
formats={locale.formats}
defaultFormats={defaultFormats}
key={locale.key}>
key={locale.key}
>
<LastBlockTime
lastBlockTimestamp={lastBlockTimestamp}
setTimeout={setTimeout}
Expand Down
2 changes: 1 addition & 1 deletion test/unit/components/buttons/SendTransactionButton.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ test("render SendTransactionButton when trezor is enabled", async () => {
expect(mockSignTransactionAttempt).not.toHaveBeenCalled();
expect(mockSignTransactionAttemptTrezor).toHaveBeenCalledWith(
testUnsignedTransaction,
testConstructTxResponse
[testConstructTxResponse.changeIndex]
);
await wait(() => expect(mockOnSubmit).toHaveBeenCalled());
});
Expand Down
3 changes: 2 additions & 1 deletion test/unit/components/shared/VerticalAccordion.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const TestVerticalAccordionContainer = ({ disabled = false }) => {
headerClassName={testHeaderClassName}
arrowClassName={testArrowClassName}
show={show}
onToggleAccordion={() => setShow((e) => !e)}>
onToggleAccordion={() => setShow((e) => !e)}
>
<TestContent />
</VerticalAccordion>
);
Expand Down
Loading

0 comments on commit 98d58d4

Please sign in to comment.