Skip to content

Commit 520f67b

Browse files
committed
multi: Allow trezor ticket purchasing on testnet.
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.
1 parent 372e7f6 commit 520f67b

File tree

15 files changed

+503
-22
lines changed

15 files changed

+503
-22
lines changed

app/actions/TrezorActions.js

Lines changed: 397 additions & 5 deletions
Large diffs are not rendered by default.

app/components/buttons/SendTransactionButton/hooks.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ export function useSendTransactionButton() {
1414
dispatch(ca.signTransactionAttempt(passphrase, rawTx, acct));
1515
};
1616
const onAttemptSignTransactionTrezor = (rawUnsigTx, constructTxResponse) =>
17-
dispatch(tza.signTransactionAttemptTrezor(rawUnsigTx, constructTxResponse));
17+
dispatch(
18+
tza.signTransactionAttemptTrezor(rawUnsigTx, [
19+
constructTxResponse.changeIndex
20+
])
21+
);
1822

1923
return {
2024
unsignedTransaction,

app/components/views/TicketsPage/PurchaseTab/PurchaseTabPage.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export function PurchaseTabPage({
7272
isVSPListingEnabled,
7373
onEnableVSPListing,
7474
getRunningIndicator,
75+
isPurchasingTicketsTrezor,
7576
...props
7677
}) {
7778
return (
@@ -115,7 +116,9 @@ export function PurchaseTabPage({
115116
isLoading,
116117
rememberedVspHost,
117118
toggleRememberVspHostCheckBox,
118-
getRunningIndicator
119+
getRunningIndicator,
120+
isPurchasingTicketsTrezor,
121+
isWatchingOnly
119122
}}
120123
/>
121124
)}

app/components/views/TicketsPage/PurchaseTab/PurchaseTicketsForm/PurchaseTicketsForm.jsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ const PurchaseTicketsForm = ({
3636
rememberedVspHost,
3737
toggleRememberVspHostCheckBox,
3838
notMixedAccounts,
39-
getRunningIndicator
39+
getRunningIndicator,
40+
isPurchasingTicketsTrezor
4041
}) => {
4142
const intl = useIntl();
4243
return (
@@ -149,7 +150,10 @@ const PurchaseTicketsForm = ({
149150
</div>
150151
<div className={styles.buttonsArea}>
151152
{isWatchingOnly ? (
152-
<PiUiButton disabled={!isValid} onClick={onPurchaseTickets}>
153+
<PiUiButton
154+
disabled={!isValid}
155+
loading={isPurchasingTicketsTrezor}
156+
onClick={onPurchaseTickets}>
153157
{purchaseLabel()}
154158
</PiUiButton>
155159
) : isLoading ? (

app/components/views/TicketsPage/PurchaseTab/hooks.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useCallback, useMemo } from "react";
33
import { useSettings } from "hooks";
44
import { EXTERNALREQUEST_STAKEPOOL_LISTING } from "constants";
55

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

2528
const rememberedVspHost = useSelector(sel.getRememberedVspHost);
2629
const visibleAccounts = useSelector(sel.visibleAccounts);
@@ -54,9 +57,16 @@ export const usePurchaseTab = () => {
5457
[dispatch]
5558
);
5659
const purchaseTicketsAttempt = useCallback(
57-
(passphrase, account, numTickets, vsp) =>
58-
dispatch(ca.purchaseTicketsAttempt(passphrase, account, numTickets, vsp)),
59-
[dispatch]
60+
(passphrase, account, numTickets, vsp) => {
61+
if (isTrezor) {
62+
dispatch(trezorPurchseTicketsAttempt(account, numTickets, vsp));
63+
} else {
64+
dispatch(
65+
ca.purchaseTicketsAttempt(passphrase, account, numTickets, vsp)
66+
);
67+
}
68+
},
69+
[dispatch, isTrezor]
6070
);
6171

6272
const setRememberedVspHost = useCallback(
@@ -140,6 +150,7 @@ export const usePurchaseTab = () => {
140150
vsp,
141151
setVSP,
142152
numTicketsToBuy,
143-
setNumTicketsToBuy
153+
setNumTicketsToBuy,
154+
isPurchasingTicketsTrezor
144155
};
145156
};

app/helpers/msgTx.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ export function decodeRawTransaction(rawTx) {
268268
position += 4;
269269
tx.expiry = rawTx.readUInt32LE(position);
270270
position += 4;
271+
tx.prefixOffset = position;
271272
}
272273

273274
if (tx.serType !== SERTYPE_NOWITNESS) {

app/helpers/trezor.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ export const addressPath = (index, branch, account, coinType) => {
2828
};
2929

3030
// walletTxToBtcjsTx is a aux function to convert a tx decoded by the decred wallet (ie,
31-
// returned from wallet.decoreRawTransaction call) into a bitcoinjs-compatible
31+
// returned from wallet.decodeRawTransaction call) into a bitcoinjs-compatible
3232
// transaction (to be used in trezor).
3333
export const walletTxToBtcjsTx = async (
3434
walletService,
3535
chainParams,
3636
tx,
3737
inputTxs,
38-
changeIndex
38+
changeIndexes
3939
) => {
4040
const inputs = tx.inputs.map(async (inp) => {
4141
const addr = inp.outpointAddress;
@@ -81,7 +81,7 @@ export const walletTxToBtcjsTx = async (
8181
const addrValidResp = await wallet.validateAddress(walletService, addr);
8282
if (!addrValidResp.isValid) throw "Not a valid address: " + addr;
8383
let address_n = null;
84-
if (i === changeIndex && addrValidResp.isMine) {
84+
if (changeIndexes.includes(i) && addrValidResp.isMine) {
8585
const addrIndex = addrValidResp.index;
8686
const addrBranch = addrValidResp.isInternal ? 1 : 0;
8787
address_n = addressPath(
@@ -124,7 +124,10 @@ export const walletTxToRefTx = async (walletService, tx) => {
124124
const outputs = tx.outputs.map(async (outp) => {
125125
const addr = outp.decodedScript.address;
126126
const addrValidResp = await wallet.validateAddress(walletService, addr);
127-
if (!addrValidResp.isValid) throw new Error("Not a valid address: " + addr);
127+
// Scripts with zero value can be ignored as they are not a concern when
128+
// spending from an outpoint.
129+
if (outp.value != 0 && !addrValidResp.isValid)
130+
throw new Error("Not a valid address: " + addr);
128131
return {
129132
amount: outp.value,
130133
script_pubkey: rawToHex(outp.script),

app/main_dev/externalRequests.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ export const installSessionHandlers = (mainLogger) => {
121121
`connect-src ${connectSrc}; `;
122122
}
123123

124+
const requestURL = new URL(details.url);
125+
const maybeVSPReqType = `stakepool_${requestURL.protocol}//${requestURL.host}`;
126+
const isVSPRequest = allowedExternalRequests[maybeVSPReqType];
127+
124128
if (isDev && /^http[s]?:\/\//.test(details.url)) {
125129
// In development (when accessing via the HMR server) we need to overwrite
126130
// the origin, otherwise electron fails to contact external servers due
@@ -144,6 +148,12 @@ export const installSessionHandlers = (mainLogger) => {
144148
newHeaders["Access-Control-Allow-Headers"] = "Content-Type";
145149
}
146150

151+
if (isVSPRequest && details.method === "OPTIONS") {
152+
statusLine = "OK";
153+
newHeaders["Access-Control-Allow-Headers"] =
154+
"Content-Type,vsp-client-signature";
155+
}
156+
147157
const globalCfg = getGlobalCfg();
148158
const cfgAllowedVSPs = globalCfg.get(cfgConstants.ALLOWED_VSP_HOSTS, []);
149159
if (cfgAllowedVSPs.some((url) => details.url.includes(url))) {
@@ -204,6 +214,10 @@ export const allowVSPRequests = (stakePoolHost) => {
204214

205215
addAllowedURL(stakePoolHost + "/api/v3/vspinfo");
206216
addAllowedURL(stakePoolHost + "/api/v3/ticketstatus");
217+
addAllowedURL(stakePoolHost + "/api/v3/feeaddress");
218+
addAllowedURL(stakePoolHost + "/api/v3/payfee");
219+
addAllowedURL(stakePoolHost + "/api/ticketstatus");
220+
allowedExternalRequests[reqType] = true;
207221
};
208222

209223
export const reloadAllowedExternalRequests = () => {

app/middleware/vspapi.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,25 @@ export function getVSPTicketStatus({ host, sig, json }, cb) {
6666
.then((resp) => cb(resp, null, host))
6767
.catch((error) => cb(null, error, host));
6868
}
69+
70+
// getFeeAddress gets a ticket`s fee address.
71+
export function getFeeAddress({ host, sig, req }, cb) {
72+
console.log(req);
73+
POST(host + "/api/v3/feeaddress", sig, req)
74+
.then((resp) => cb(resp, null, host))
75+
.catch((error) => cb(null, error, host));
76+
}
77+
78+
// payFee infomrs of a ticket`s fee payment.
79+
export function payFee({ host, sig, req }, cb) {
80+
console.log(req);
81+
POST(host + "/api/v3/payfee", sig, req)
82+
.then((resp) => cb(resp, null, host))
83+
.catch((error) => cb(null, error, host));
84+
}
85+
86+
export function getTicketStatus({ host, vspClientSig, request }, cb) {
87+
POST(host + "/api/ticketstatus", vspClientSig, request)
88+
.then((resp) => cb(resp, null, host))
89+
.catch((error) => cb(null, error, host));
90+
}

app/reducers/trezor.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import {
1919
TRZ_PASSPHRASE_REQUESTED,
2020
TRZ_PASSPHRASE_ENTERED,
2121
TRZ_PASSPHRASE_CANCELED,
22+
TRZ_PURCHASETICKET_ATTEMPT,
23+
TRZ_PURCHASETICKET_FAILED,
24+
TRZ_PURCHASETICKET_SUCCESS,
2225
TRZ_WORD_REQUESTED,
2326
TRZ_WORD_ENTERED,
2427
TRZ_WORD_CANCELED,
@@ -279,6 +282,12 @@ export default function trezor(state = {}, action) {
279282
performingOperation: false,
280283
performingTogglePassphraseOnDeviceProtection: false
281284
};
285+
case TRZ_PURCHASETICKET_ATTEMPT:
286+
return {
287+
...state,
288+
performingOperation: true,
289+
purchasingTickets: true
290+
};
282291
case SIGNTX_FAILED:
283292
case SIGNTX_SUCCESS:
284293
case TRZ_CHANGEHOMESCREEN_FAILED:
@@ -305,7 +314,6 @@ export default function trezor(state = {}, action) {
305314
performingOperation: false,
306315
performingUpdate: false
307316
};
308-
309317
case TRZ_TOGGLEPINPROTECTION_FAILED:
310318
case TRZ_TOGGLEPINPROTECTION_SUCCESS:
311319
return {
@@ -325,6 +333,13 @@ export default function trezor(state = {}, action) {
325333
performingOperation: false,
326334
performingTogglePassphraseOnDeviceProtection: false
327335
};
336+
case TRZ_PURCHASETICKET_FAILED:
337+
case TRZ_PURCHASETICKET_SUCCESS:
338+
return {
339+
...state,
340+
performingOperation: false,
341+
purchasingTickets: false
342+
};
328343
case CLOSEWALLET_SUCCESS:
329344
return { ...state, enabled: false };
330345
default:

0 commit comments

Comments
 (0)