diff --git a/app/actions/TrezorActions.js b/app/actions/TrezorActions.js index b43a067f00..23b365ad5c 100644 --- a/app/actions/TrezorActions.js +++ b/app/actions/TrezorActions.js @@ -8,6 +8,7 @@ import { accountPath, addressPath } from "helpers/trezor"; +import { putUint16, rawHashToHex } from "helpers/byteActions"; import { publishTransactionAttempt } from "./ControlActions"; import * as cfgConstants from "constants/config"; import { @@ -24,6 +25,7 @@ import { SIGNMESSAGE_SUCCESS } from "./ControlActions"; import { getAmountFromTxInputs, getTxFromInputs } from "./TransactionActions"; +import { blake256 } from "walletCrypto"; const session = require("trezor-connect").default; const { @@ -40,6 +42,15 @@ const NOBACKUP = "no-backup"; const TRANSPORT_ERROR = "transport-error"; const TRANSPORT_START = "transport-start"; const BOOTLOADER_MODE = "bootloader"; +const testVotingKey = "PtWTXsGfk2YeqcmrRty77EsynNBtxWLLbsVEeTS8bKAGFoYF3qTNq"; +const testVotingAddr = "TsmfmUitQApgnNxQypdGd2x36djCCpDpERU"; +const SERTYPE_NOWITNESS = 1; +const OP_SSGEN_STR = "bb"; +const OP_SSRTX_STR = "bc"; +const OP_TGEN_STR = "c3"; +const STAKE_REVOCATION = "SSRTX"; +const STAKE_GENERATION = "SSGen"; +const TREASURY_GENERATION = "TGen"; let setListeners = false; @@ -441,8 +452,38 @@ const checkTrezorIsDcrwallet = () => async (dispatch, getState) => { if (addrValidResp.index !== 0) throw "Wallet replied with wrong index."; }; +// setStakeInputTypes adds a field to input that denotes stake spends. SSRTX +function setStakeInputTypes(inputs, refTxs) { + const refs = {}; + refTxs.forEach((ref) => (refs[ref.hash] = ref.bin_outputs)); + // Search reference txs for the script that will be signed and determine if + // spending a stake output by comparing the first opcode to SSRTX or SSGEN + // opcodes. + for (let i = 0; i < inputs.length; i++) { + const input = inputs[i]; + const bin_outputs = refs[input.prev_hash]; + if (!bin_outputs) continue; + let s = bin_outputs[input.prev_index].script_pubkey; + if (s.length < 2) { + continue; + } + s = s.slice(0, 2); + switch (s) { + case OP_SSGEN_STR: + input.decred_staking_spend = STAKE_GENERATION; + break; + case OP_SSRTX_STR: + input.decred_staking_spend = STAKE_REVOCATION; + break; + case OP_TGEN_STR: + input.decred_staking_spend = TREASURY_GENERATION; + break; + } + } +} + export const signTransactionAttemptTrezor = - (rawUnsigTx, constructTxResponse) => async (dispatch, getState) => { + (rawUnsigTx, changeIndexes) => async (dispatch, getState) => { dispatch({ type: SIGNTX_ATTEMPT }); const { @@ -451,11 +492,9 @@ export const signTransactionAttemptTrezor = } = getState(); const chainParams = selectors.chainParams(getState()); - debug && console.log("construct tx response", constructTxResponse); + debug && console.log("construct tx response", rawUnsigTx); try { - const changeIndex = constructTxResponse.changeIndex; - const decodedUnsigTxResp = wallet.decodeRawTransaction( Buffer.from(rawUnsigTx, "hex"), chainParams @@ -472,13 +511,17 @@ export const signTransactionAttemptTrezor = chainParams, txCompletedInputs, inputTxs, - changeIndex + changeIndexes ); const refTxs = await Promise.all( inputTxs.map((inpTx) => walletTxToRefTx(walletService, inpTx)) ); + // Determine if this is paying from a stakegen or revocation, which are + // special cases. + setStakeInputTypes(inputs, refTxs); + const payload = await deviceRun(dispatch, getState, async () => { await dispatch(checkTrezorIsDcrwallet()); @@ -494,6 +537,7 @@ export const signTransactionAttemptTrezor = dispatch({ type: SIGNTX_SUCCESS }); dispatch(publishTransactionAttempt(hexToBytes(signedRaw))); + return signedRaw; } catch (error) { dispatch({ error, type: SIGNTX_FAILED }); } @@ -546,6 +590,7 @@ export const signMessageAttemptTrezor = getSignMessageSignature: payload.signature, type: SIGNMESSAGE_SUCCESS }); + return payload.signature; } catch (error) { dispatch({ error, type: SIGNMESSAGE_FAILED }); } @@ -960,3 +1005,350 @@ export const getWalletCreationMasterPubKey = throw error; } }; + +export const TRZ_PURCHASETICKET_ATTEMPT = "TRZ_PURCHASETICKET_ATTEMPT"; +export const TRZ_PURCHASETICKET_FAILED = "TRZ_PURCHASETICKET_FAILED"; +export const TRZ_PURCHASETICKET_SUCCESS = "TRZ_PURCHASETICKET_SUCCESS"; + +// ticketInOuts creates inputs and outputs for use with a trezor signature +// request of a ticket. +async function ticketInsOuts( + getState, + decodedTicket, + decodedInp, + refTxs, + votingAddr +) { + const { + grpc: { walletService } + } = getState(); + const chainParams = selectors.chainParams(getState()); + const ticketOutN = decodedTicket.inputs[0].outputIndex; + const inAddr = decodedInp.outputs[ticketOutN].decodedScript.address; + let addrValidResp = await wallet.validateAddress(walletService, inAddr); + const inAddr_n = addressPath( + addrValidResp.index, + 1, + WALLET_ACCOUNT, + chainParams.HDCoinType + ); + const commitAddr = decodedTicket.outputs[1].decodedScript.address; + addrValidResp = await wallet.validateAddress(walletService, commitAddr); + const commitAddr_n = addressPath( + addrValidResp.index, + 1, + WALLET_ACCOUNT, + chainParams.HDCoinType + ); + const inputAmt = decodedTicket.inputs[0].valueIn.toString(); + const ticketInput = { + address_n: inAddr_n, + prev_hash: decodedTicket.inputs[0].prevTxId, + prev_index: ticketOutN, + amount: inputAmt + }; + const sstxsubmission = { + script_type: "PAYTOADDRESS", + address: votingAddr, + amount: decodedTicket.outputs[0].value.toString() + }; + const ticketsstxcommitment = { + script_type: "PAYTOADDRESS", + address_n: commitAddr_n, + amount: inputAmt + }; + const ticketsstxchange = { + script_type: "PAYTOADDRESS", + address: decodedTicket.outputs[2].decodedScript.address, + amount: "0" + }; + const inputs = [ticketInput]; + const outputs = [sstxsubmission, ticketsstxcommitment, ticketsstxchange]; + return { inputs, outputs }; +} + +export const purchaseTicketsAttempt = + (accountNum, numTickets, vsp) => async (dispatch, getState) => { + dispatch({ type: TRZ_PURCHASETICKET_ATTEMPT }); + + if (noDevice(getState)) { + dispatch({ + error: "Device not connected", + type: TRZ_PURCHASETICKET_FAILED + }); + return; + } + + const { + grpc: { walletService } + } = getState(); + const chainParams = selectors.chainParams(getState()); + + try { + // TODO: Enable on mainnet. The following todo on crypto magic must be + // implemented first. Revocation logic and a re-fee payment method must be + // added. + if (chainParams.trezorCoinName != "Decred Testnet") + throw "can only be used on testnet"; + // TODO: Fill this with deterministic crypto magic. + const votingKey = testVotingKey; + const votingAddr = testVotingAddr; + const res = await wallet.purchaseTickets( + walletService, + accountNum, + numTickets, + false, + vsp, + {} + ); + const splitTx = res.splitTx; + const decodedInp = await wallet.decodeTransactionLocal( + splitTx, + chainParams + ); + const changeIndexes = []; + for (let i = 0; i < decodedInp.outputs.length; i++) { + changeIndexes.push(i); + } + const signedSplitTx = await signTransactionAttemptTrezor( + splitTx, + changeIndexes + )(dispatch, getState); + if (!signedSplitTx) throw "failed to sign splittx"; + const refTxs = await walletTxToRefTx(walletService, decodedInp); + + for (const ticket of res.ticketsList) { + const decodedTicket = await wallet.decodeTransactionLocal( + ticket, + chainParams + ); + refTxs.hash = decodedTicket.inputs[0].prevTxId; + const { inputs, outputs } = await ticketInsOuts( + getState, + decodedTicket, + decodedInp, + refTxs, + votingAddr + ); + const payload = await deviceRun(dispatch, getState, async () => { + const res = await session.signTransaction({ + coin: chainParams.trezorCoinName, + inputs: inputs, + outputs: outputs, + refTxs: [refTxs], + decredStakingTicket: true + }); + return res.payload; + }); + + const signedRaw = payload.serializedTx; + dispatch(publishTransactionAttempt(hexToBytes(signedRaw))); + // Pay fee. + console.log( + "waiting 5 seconds for the ticket to propogate throughout the network" + ); + await new Promise((r) => setTimeout(r, 5000)); + const host = "https://" + vsp.host; + await payVSPFee( + host, + signedRaw, + signedSplitTx, + votingKey, + accountNum.value, + true, + dispatch, + getState + ); + } + dispatch({ type: TRZ_PURCHASETICKET_SUCCESS }); + } catch (error) { + dispatch({ error, type: TRZ_PURCHASETICKET_FAILED }); + } + }; + +// payVSPFee attempts to contact a vsp about a ticket and pay the fee if +// necessary. It will search transacitons for a suitable fee transaction before +// attempting to pay if newTicket is false. +async function payVSPFee( + host, + txHex, + parentTxHex, + votingKey, + accountNum, + newTicket, + dispatch, + getState +) { + const { + grpc: { walletService } + } = getState(); + // Gather information about the ticket. + const chainParams = selectors.chainParams(getState()); + const txBytes = hexToBytes(txHex); + const decodedTicket = await wallet.decodeTransactionLocal( + txBytes, + chainParams + ); + const commitmentAddr = decodedTicket.outputs[1].decodedScript.address; + + const prefix = txBytes.slice(0, decodedTicket.prefixOffset); + prefix.set(putUint16(SERTYPE_NOWITNESS), 2); + const txid = rawHashToHex(blake256(prefix)); + + // Request fee info from the vspd. + let req = { + timestamp: +new Date(), + tickethash: txid, + tickethex: txHex, + parenthex: parentTxHex + }; + let jsonStr = JSON.stringify(req); + let sig = await signMessageAttemptTrezor(commitmentAddr, jsonStr)( + dispatch, + getState + ); + if (!sig) throw "unable to sign fee address message"; + wallet.allowVSPHost(host); + // This will throw becuase of http.status 400 if already paid. + // TODO: Investigate whether other fee payment errors will cause this to + // throw. Other fee payment errors should continue, and we should only stop + // here if already paid or the ticket is not found by the vsp. + let res = null; + try { + res = await wallet.getVSPFeeAddress({ host, sig, req }); + } catch (error) { + if (error.response && error.response.data && error.response.data.message) { + // NOTE: already paid is error.response.data.code == 3 + throw error.response.data.message; + } + throw error; + } + const payAddr = res.data.feeaddress; + const fee = res.data.feeamount; + + // Find the fee transaction or make a new one. + let feeTx = null; + // Do not search for the fee tx of a new ticket. + if (!newTicket) { + feeTx = await findFeeTx(payAddr, fee, dispatch, getState); + } + if (!feeTx) { + const outputs = [{ destination: payAddr, amount: fee }]; + const txResp = await wallet.constructTransaction( + walletService, + accountNum, + 0, + outputs + ); + const unsignedTx = txResp.unsignedTransaction; + const decodedInp = await wallet.decodeTransactionLocal( + unsignedTx, + chainParams + ); + let changeIndex = 0; + for (const out of decodedInp.outputs) { + const addr = out.decodedScript.address; + const addrValidResp = await wallet.validateAddress(walletService, addr); + if (addrValidResp.isInternal) { + break; + } + changeIndex++; + } + const success = await signTransactionAttemptTrezor(unsignedTx, [ + changeIndex + ])(dispatch, getState); + if (!success) throw "unable to sign fee tx"; + for (let i = 0; i < 5; i++) { + console.log( + "waiting 5 seconds for the fee tx to propogate throughout the network" + ); + await new Promise((r) => setTimeout(r, 5000)); + feeTx = await findFeeTx(payAddr, fee, dispatch, getState); + if (feeTx) break; + } + if (!feeTx) throw "unable to find fee tx " + rawToHex(unsignedTx); + } + + // Send ticket fee data and voting chioces back to the vsp. + const { + grpc: { votingService } + } = getState(); + const voteChoicesRes = await wallet.getVoteChoices(votingService); + const voteChoices = {}; + for (const choice of voteChoicesRes.choicesList) { + voteChoices[choice.agendaId] = choice.choiceId; + } + req = { + timestamp: +new Date(), + tickethash: txid, + feetx: feeTx, + votingkey: votingKey, + votechoices: voteChoices + }; + jsonStr = JSON.stringify(req); + sig = await signMessageAttemptTrezor(commitmentAddr, jsonStr)( + dispatch, + getState + ); + if (!sig) throw "unable to sign fee tx message"; + wallet.allowVSPHost(host); + await wallet.payVSPFee({ host, sig, req }); +} + +// findFeeTx searches unmined and recent transactions for a tx that pays to +// FeeAddr of the amount feeAmt. It stops searching below a resonable depth for +// a ticket. +async function findFeeTx(feeAddr, feeAmt, dispatch, getState) { + const { + grpc: { walletService } + } = getState(); + const chainParams = selectors.chainParams(getState()); + // findFee looks for a transaction the paid out exactl feeAmt and has an + // output address that matches feeAddr. + const findFee = async (res) => { + for (const credit of res) { + if (credit.txType != "sent" && credit.txType != "regular") continue; + const sentAmt = Math.abs(credit.amount + credit.fee); + if (sentAmt == feeAmt) { + const tx = await wallet.decodeTransactionLocal( + hexToBytes(credit.rawTx), + chainParams + ); + if ( + tx.outputs.find( + (e) => e.decodedScript && e.decodedScript.address == feeAddr + ) + ) + return credit.rawTx; + } + } + return null; + }; + // First search mempool. + const { unmined } = await wallet.getTransactions(walletService, -1, -1, 0); + const feeTx = await findFee(unmined); + if (feeTx) return feeTx; + // TODO: Take these constants from the chainparams. + const ticketMaturity = 256; + const ticketExpiry = 40960; + const { currentBlockHeight } = getState().grpc; + let start = currentBlockHeight - 100; + let end = currentBlockHeight; + const maxAge = currentBlockHeight - (ticketMaturity + ticketExpiry); + const blockIncrement = 100; + // Search mined txs in reverse order up until a ticket must have expired on + // mainnet. + while (start > maxAge) { + const { mined } = await wallet.getTransactions( + walletService, + start, + end, + 0 + ); + start -= blockIncrement; + end -= blockIncrement; + const feeTx = await findFee(mined); + if (feeTx) return feeTx; + } + return null; +} diff --git a/app/components/SideBar/AccountsList/AccountsList.jsx b/app/components/SideBar/AccountsList/AccountsList.jsx index 84802e5c17..b03ddbe892 100644 --- a/app/components/SideBar/AccountsList/AccountsList.jsx +++ b/app/components/SideBar/AccountsList/AccountsList.jsx @@ -20,7 +20,8 @@ const AccountsList = ({ className={classNames( style.extended, isShowingAccounts && style.showingAccounts - )}> + )} + >
{balances.map( ({ hidden, total, accountName, accountNumber }) => @@ -32,7 +33,8 @@ const AccountsList = ({ isMixed(accountNumber) && style.mixed, isChange(accountNumber) && style.unmixed )} - key={accountName}> + key={accountName} + >
{accountName === "default" ? ( diff --git a/app/components/SideBar/Logo/Logo.jsx b/app/components/SideBar/Logo/Logo.jsx index b4ccecd89d..7d922052b5 100644 --- a/app/components/SideBar/Logo/Logo.jsx +++ b/app/components/SideBar/Logo/Logo.jsx @@ -20,7 +20,8 @@ const Logo = React.memo( id="sidebar.isWatchingOnlyTooltip" m="This is a watch-only wallet with limited functionality." /> - }> + } + >
)} @@ -46,7 +47,8 @@ const Logo = React.memo( the background: Privacy Mixer, Ticket Auto Buyer, Purchase Ticket Attempt`} /> - }> + } + >
)} diff --git a/app/components/SideBar/MenuBottom/MenuBottomExpanded.jsx b/app/components/SideBar/MenuBottom/MenuBottomExpanded.jsx index 1ad26f939d..2e93d5eff2 100644 --- a/app/components/SideBar/MenuBottom/MenuBottomExpanded.jsx +++ b/app/components/SideBar/MenuBottom/MenuBottomExpanded.jsx @@ -26,12 +26,14 @@ const MenuBarExpanded = ({ className={styles.short} onMouseEnter={rescanRequest ? null : onShowAccounts} onMouseLeave={rescanRequest ? null : onHideAccounts} - onWheel={onAccountsListWheel}> + onWheel={onAccountsListWheel} + >
+ )} + >
:
diff --git a/app/components/SideBar/MenuBottom/SettingsIconLink/SettingsIconLink.jsx b/app/components/SideBar/MenuBottom/SettingsIconLink/SettingsIconLink.jsx index dd91a87f66..22e513b2c0 100644 --- a/app/components/SideBar/MenuBottom/SettingsIconLink/SettingsIconLink.jsx +++ b/app/components/SideBar/MenuBottom/SettingsIconLink/SettingsIconLink.jsx @@ -6,7 +6,8 @@ import styles from "./SettingsIconLink.module.css"; const SettingsIconLink = ({ className }) => ( }> + content={} + >
diff --git a/app/components/SideBar/MenuBottom/SpvIcon/SpvIcon.jsx b/app/components/SideBar/MenuBottom/SpvIcon/SpvIcon.jsx index ac03e408eb..024a18ddb5 100644 --- a/app/components/SideBar/MenuBottom/SpvIcon/SpvIcon.jsx +++ b/app/components/SideBar/MenuBottom/SpvIcon/SpvIcon.jsx @@ -12,7 +12,8 @@ const SpvIcon = ({ isSPV }) => ( ) : ( ) - }> + } + >
); diff --git a/app/components/SideBar/MenuLinks/MenuLinks.jsx b/app/components/SideBar/MenuLinks/MenuLinks.jsx index b755aa4da2..31393a6190 100644 --- a/app/components/SideBar/MenuLinks/MenuLinks.jsx +++ b/app/components/SideBar/MenuLinks/MenuLinks.jsx @@ -29,7 +29,8 @@ const MenuLinks = () => { "tab-default-background": getThemeProperty(theme, "sidebar-color"), "tab-default-color": getThemeProperty(theme, "sidebar-color"), "tab-secondary-active-color": getThemeProperty(theme, "sidebar-color") - }}> + }} + > { styles.tabs, expandSideBar && styles.expanded, sidebarOnBottom && styles.onBottom - )}> + )} + > {menuLinks.map((menuLink, index) => { const menuLinkLabel = () => (
+ data-testid={`menuLinkContent-${menuLink.icon}`} + >
{ + placement={sidebarOnBottom ? "top" : "right"} + > {menuLinkLabel()} ); diff --git a/app/components/SideBar/SideBar.jsx b/app/components/SideBar/SideBar.jsx index a1b7d83ae7..0567d74177 100644 --- a/app/components/SideBar/SideBar.jsx +++ b/app/components/SideBar/SideBar.jsx @@ -43,7 +43,8 @@ const SideBar = () => { uiAnimations && style.animated, !expandSideBar && style.sidebarReduced, !expandSideBar && sidebarOnBottom && style.sidebarOnBottom - )}> + )} + > { className={classNames( style.sidebarMain, isShowingAccounts && style.isShowingAccounts - )}> + )} + > + onClick={onDismissMessage} + > )} diff --git a/app/components/Snackbar/Notification/Transaction.jsx b/app/components/Snackbar/Notification/Transaction.jsx index 324e4e3239..c74e48778a 100644 --- a/app/components/Snackbar/Notification/Transaction.jsx +++ b/app/components/Snackbar/Notification/Transaction.jsx @@ -53,7 +53,8 @@ const Transaction = ({ )} @@ -89,7 +90,8 @@ const Transaction = ({ + to={`/transactions/history/${message.txHash}`} + > {message.txHash} diff --git a/app/components/Snackbar/index.js b/app/components/Snackbar/index.js index 62bfa0f096..e1470da0e9 100644 --- a/app/components/Snackbar/index.js +++ b/app/components/Snackbar/index.js @@ -78,7 +78,8 @@ const Snackbar = () => { className={snackbarClasses(message || "")} onMouseEnter={clearHideTimer} onMouseLeave={enableHideTimer} - style={{ bottom: "0px" }}> + style={{ bottom: "0px" }} + > { onMouseEnter={clearHideTimer} onMouseLeave={enableHideTimer} style={s.style} - ref={(ref) => animatedNotifRef(s.key, ref)}> + ref={(ref) => animatedNotifRef(s.key, ref)} + > { !disabled && onClick?.(e); }} - hidden={hidden}> + hidden={hidden} + > {loading ? : children} ); diff --git a/app/components/buttons/CopyToClipboardButton/CopyToClipboardButton.jsx b/app/components/buttons/CopyToClipboardButton/CopyToClipboardButton.jsx index a7e552fa57..8d00439a68 100644 --- a/app/components/buttons/CopyToClipboardButton/CopyToClipboardButton.jsx +++ b/app/components/buttons/CopyToClipboardButton/CopyToClipboardButton.jsx @@ -22,10 +22,8 @@ const CopyToClipboardButton = ({ textToCopy, className }) => { return (
+ className={classNames(styles.success, isSuccessHidden && styles.hidden)} + >
{ onAddAllowedRequestType(requestType).then(() => onClick && onClick()) - }> + } + > {children} ); diff --git a/app/components/buttons/EyeFilterMenu/EyeFilterMenu.jsx b/app/components/buttons/EyeFilterMenu/EyeFilterMenu.jsx index c23f4d9454..5d66207f96 100644 --- a/app/components/buttons/EyeFilterMenu/EyeFilterMenu.jsx +++ b/app/components/buttons/EyeFilterMenu/EyeFilterMenu.jsx @@ -48,7 +48,8 @@ const EyeFilterMenu = ({ )} onClick={(e) => onMenuChanged(e, { value: option.value, key: option.key }) - }> + } + > {option.label}
))} @@ -60,7 +61,8 @@ const EyeFilterMenu = ({ return (
+ ref={wrapperRef} + >
+ className={styles.kebab} + >
(
wallet.openExternalURL(href)}> + onClick={onClick ? onClick : () => wallet.openExternalURL(href)} + >
{title}
{subtitle}
diff --git a/app/components/buttons/ModalButton.jsx b/app/components/buttons/ModalButton.jsx index debe7e2ba1..ccc4780382 100644 --- a/app/components/buttons/ModalButton.jsx +++ b/app/components/buttons/ModalButton.jsx @@ -10,7 +10,8 @@ const defaultButton = ({ diff --git a/app/components/buttons/PathButton/PathButton.jsx b/app/components/buttons/PathButton/PathButton.jsx index ff2897942a..9b22da1edf 100644 --- a/app/components/buttons/PathButton/PathButton.jsx +++ b/app/components/buttons/PathButton/PathButton.jsx @@ -5,7 +5,8 @@ import styles from "./PathButton.module.css"; const PathButton = ({ disabled, onClick }) => ( }> + content={} + >
onClick("right") : null}> + onClick={activeButton == "left" ? () => onClick("right") : null} + > {rightText}
diff --git a/app/components/buttons/ToggleSwitch/ToggleSwitch.jsx b/app/components/buttons/ToggleSwitch/ToggleSwitch.jsx index 472dcbdb3c..64ae2639b2 100644 --- a/app/components/buttons/ToggleSwitch/ToggleSwitch.jsx +++ b/app/components/buttons/ToggleSwitch/ToggleSwitch.jsx @@ -25,7 +25,8 @@ const ToggleSwitch = ({ contentClassName={classNames(styles.tooltip, tooltipClassName)} content={ disabled ? disabledText : enabled ? enabledText : notEnabledText - }> + } + > { stackOffset="sign" width={chartSize.width} height={chartSize.height} - data={displayData}> + data={displayData} + > {
+ style={{ background: entry.fill }} + >
{entry.dataKey}: {
+ style={{ background: entry.fill }} + >
{ stackOffset="sign" width={chartSize.width} height={chartSize.height} - data={displayData}> + data={displayData} + > { stackOffset="sign" width={chartSize.width} height={chartSize.height} - data={displayData}> + data={displayData} + > { stackOffset="sign" width={chartSize.width} height={chartSize.height} - data={displayData}> + data={displayData} + > { stackOffset="sign" width={chartSize.width} height={chartSize.height} - data={displayData}> + data={displayData} + > }> + content={} + >
@@ -191,7 +192,8 @@ const AnimatedLinearProgressFull = ({ <> + className={styles.openWalletButton} + > + className={styles.cancelLoadingButton} + > ) diff --git a/app/components/indicators/LinearProgress/LinearProgressSmall.jsx b/app/components/indicators/LinearProgress/LinearProgressSmall.jsx index 2dc54efabe..273f6cb26b 100644 --- a/app/components/indicators/LinearProgress/LinearProgressSmall.jsx +++ b/app/components/indicators/LinearProgress/LinearProgressSmall.jsx @@ -8,13 +8,15 @@ const LinearProgressSmall = ({ value, min, max, className, barClassName }) => ( style.small, className && className )} - data-testid="linear-prgress-small"> + data-testid="linear-prgress-small" + >
+ style={{ width: `${((value - min) / (max - min)) * 100}%` }} + >
); diff --git a/app/components/indicators/LoadingMoreTickets/LoadingMoreTickets.jsx b/app/components/indicators/LoadingMoreTickets/LoadingMoreTickets.jsx index edd47b9576..0951a6bf22 100644 --- a/app/components/indicators/LoadingMoreTickets/LoadingMoreTickets.jsx +++ b/app/components/indicators/LoadingMoreTickets/LoadingMoreTickets.jsx @@ -41,15 +41,13 @@ const LoadingMoreTicketsIndicator = ({ return stakeTransactionsCancel ? (
+ className={classNames(styles.loadingMoreTickets, styles.isRow, className)} + > {startRequestHeight && ( <>
+ className={classNames(styles.isRow, styles.loadingMoreTicketsInfo)} + >
- }> + } + >