diff --git a/src/components/AddressTypePicker.jsx b/src/components/AddressTypePicker.jsx index dc782274..0a4740ce 100644 --- a/src/components/AddressTypePicker.jsx +++ b/src/components/AddressTypePicker.jsx @@ -17,66 +17,62 @@ import { // Actions import { setAddressType } from "../actions/settingsActions"; -class AddressTypePicker extends React.Component { - handleTypeChange = (event) => { - const { setType } = this.props; +const AddressTypePicker = ({ setType, addressType, frozen }) => { + const handleTypeChange = (event) => { setType(event.target.value); }; - render() { - const { addressType, frozen } = this.props; - return ( - - - - - - } - name="type" - value={P2SH} - label={P2SH} - onChange={this.handleTypeChange} - checked={addressType === P2SH} - disabled={frozen} - /> - } - name="type" - value={P2SH_P2WSH} - label={P2SH_P2WSH} - onChange={this.handleTypeChange} - checked={addressType === P2SH_P2WSH} - disabled={frozen} - /> - } - name="type" - value={P2WSH} - label={P2WSH} - onChange={this.handleTypeChange} - checked={addressType === P2WSH} - disabled={frozen} - /> - - - - Choose ' - {P2WSH} - ' for best practices, ' - {P2SH} - ' for greatest compatibility. - - - - - - ); - } -} + return ( + + + + + + } + name="type" + value={P2SH} + label={P2SH} + onChange={handleTypeChange} + checked={addressType === P2SH} + disabled={frozen} + /> + } + name="type" + value={P2SH_P2WSH} + label={P2SH_P2WSH} + onChange={handleTypeChange} + checked={addressType === P2SH_P2WSH} + disabled={frozen} + /> + } + name="type" + value={P2WSH} + label={P2WSH} + onChange={handleTypeChange} + checked={addressType === P2WSH} + disabled={frozen} + /> + + + + Choose ' + {P2WSH} + ' for best practices, ' + {P2SH} + ' for greatest compatibility. + + + + + + ); +}; AddressTypePicker.propTypes = { addressType: PropTypes.string.isRequired, diff --git a/src/components/AppContainer.jsx b/src/components/AppContainer.jsx index 68a61bc7..a43c9597 100644 --- a/src/components/AppContainer.jsx +++ b/src/components/AppContainer.jsx @@ -1,31 +1,28 @@ -import React, { Component } from "react"; +import React, { useEffect } from "react"; import PropTypes from "prop-types"; import { createBrowserHistory } from "history"; import { connect } from "react-redux"; import App from "./App"; -class AppContainer extends Component { - history = createBrowserHistory(); +const AppContainer = ({ resetApp }) => { + const history = createBrowserHistory(); - componentDidMount() { - const { resetApp } = this.props; + useEffect(() => { // Listen for changes to the current location // and reset the redux store which is needed // to avoid conflicts in the state between views/pages - this.unlisten = this.history.listen(() => { + const unlisten = history.listen(() => { resetApp(); }); - } - componentWillUnmount() { - this.unlisten(); - } + return () => { + unlisten(); + }; + }, []); - render() { - return ; - } -} + return ; +}; AppContainer.propTypes = { resetApp: PropTypes.func.isRequired, diff --git a/src/components/ClientPicker/index.jsx b/src/components/ClientPicker/index.jsx index c04e2ee1..7b07da60 100644 --- a/src/components/ClientPicker/index.jsx +++ b/src/components/ClientPicker/index.jsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import PropTypes from "prop-types"; import { connect } from "react-redux"; import { @@ -30,149 +30,142 @@ import { import PrivateClientSettings from "./PrivateClientSettings"; -class ClientPicker extends React.Component { - static validatePassword() { +const ClientPicker = ({ + setType, + network, + setUrl, + setUrlError, + setUsername, + setUsernameError, + setPassword, + setPasswordError, + client, + onSuccess, + urlError, + usernameError, + passwordError, + privateNotes, +}) => { + const [urlEdited, setUrlEdited] = useState(false); + const [connectError, setConnectError] = useState(""); + const [connectSuccess, setConnectSuccess] = useState(false); + + const validatePassword = () => { return ""; - } + }; - static validateUsername() { + const validateUsername = () => { return ""; - } + }; - static validateUrl(host) { + const validateUrl = (host) => { const validhost = /^http(s)?:\/\/[^\s]+$/.exec(host); if (!validhost) return "Must be a valid URL."; return ""; - } - - constructor(props) { - super(props); - - this.state = { - urlEdited: false, - connectError: "", - connectSuccess: false, - }; - } - - handleTypeChange = (event) => { - const { setType, network, setUrl } = this.props; - const { urlEdited } = this.state; + }; - const type = event.target.value; - if (type === "private" && !urlEdited) { + const handleTypeChange = (event) => { + const clientType = event.target.value; + if (clientType === "private" && !urlEdited) { setUrl(`http://localhost:${network === "mainnet" ? 8332 : 18332}`); } - setType(type); + setType(clientType); }; - handleUrlChange = (event) => { - const { setUrl, setUrlError } = this.props; - const { urlEdited } = this.state; + const handleUrlChange = (event) => { const url = event.target.value; - const error = ClientPicker.validateUrl(url); - if (!urlEdited && !error) this.setState({ urlEdited: true }); + const error = validateUrl(url); + if (!urlEdited && !error) setUrlEdited(true); setUrl(url); setUrlError(error); }; - handleUsernameChange = (event) => { - const { setUsername, setUsernameError } = this.props; + const handleUsernameChange = (event) => { const username = event.target.value; - const error = ClientPicker.validateUsername(username); + const error = validateUsername(username); setUsername(username); setUsernameError(error); }; - handlePasswordChange = (event) => { - const { setPassword, setPasswordError } = this.props; + const handlePasswordChange = (event) => { const password = event.target.value; - const error = ClientPicker.validatePassword(password); + const error = validatePassword(password); setPassword(password); setPasswordError(error); }; - testConnection = async () => { - const { network, client, onSuccess } = this.props; - this.setState({ connectError: "", connectSuccess: false }); + const testConnection = async () => { + setConnectError(""); + setConnectSuccess(false); try { await fetchFeeEstimate(network, client); if (onSuccess) { onSuccess(); } - this.setState({ connectSuccess: true }); + setConnectSuccess(true); } catch (e) { - this.setState({ connectError: e.message }); + setConnectError(e.message); } }; - render() { - const { client, urlError, usernameError, passwordError, privateNotes } = - this.props; - const { connectSuccess, connectError } = this.state; - return ( - - - + return ( + + + + + + + + + } + name="clientType" + value="public" + label={Public} + onChange={handleTypeChange} + checked={client.type === "public"} + /> + } + name="clientType" + value="private" + label="Private" + onChange={handleTypeChange} + checked={client.type === "private"} + /> + + {client.type === "public" && ( + + {"'Public' uses the "} + blockstream.info + {" API. Switch to private to use a "} + bitcoind + {" node."} + + )} + {client.type === "private" && ( + handleUrlChange(event)} + handleUsernameChange={(event) => handleUsernameChange(event)} + handlePasswordChange={(event) => handlePasswordChange(event)} + client={client} + urlError={urlError} + usernameError={usernameError} + passwordError={passwordError} + privateNotes={privateNotes} + connectSuccess={connectSuccess} + connectError={connectError} + testConnection={() => testConnection()} + /> + )} + - - - - - } - name="clientType" - value="public" - label={Public} - onChange={this.handleTypeChange} - checked={client.type === "public"} - /> - } - name="clientType" - value="private" - label="Private" - onChange={this.handleTypeChange} - checked={client.type === "private"} - /> - - {client.type === "public" && ( - - {"'Public' uses the "} - blockstream.info - {" API. Switch to private to use a "} - bitcoind - {" node."} - - )} - {client.type === "private" && ( - this.handleUrlChange(event)} - handleUsernameChange={(event) => - this.handleUsernameChange(event) - } - handlePasswordChange={(event) => - this.handlePasswordChange(event) - } - client={client} - urlError={urlError} - usernameError={usernameError} - passwordError={passwordError} - privateNotes={privateNotes} - connectSuccess={connectSuccess} - connectError={connectError} - testConnection={() => this.testConnection()} - /> - )} - - - - - ); - } -} + + + ); +}; ClientPicker.propTypes = { client: PropTypes.shape({ diff --git a/src/components/Coldcard/ColdcardExtendedPublicKeyImporter.jsx b/src/components/Coldcard/ColdcardExtendedPublicKeyImporter.jsx index ddd551c3..c7aadee7 100644 --- a/src/components/Coldcard/ColdcardExtendedPublicKeyImporter.jsx +++ b/src/components/Coldcard/ColdcardExtendedPublicKeyImporter.jsx @@ -1,101 +1,81 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import PropTypes from "prop-types"; import { COLDCARD } from "unchained-wallets"; -import { Box, FormGroup, FormHelperText } from "@mui/material"; +import { Box, FormGroup } from "@mui/material"; import { MAINNET, P2SH } from "unchained-bitcoin"; import { ColdcardJSONReader } from "."; import IndirectExtendedPublicKeyImporter from "../Wallet/IndirectExtendedPublicKeyImporter"; -class ColdcardExtendedPublicKeyImporter extends React.Component { - constructor(props) { - super(props); - const coldcardBIP32Path = this.getColdcardBip32Path(); - this.state = { - COLDCARD_MULTISIG_BIP32_PATH: coldcardBIP32Path, - }; - } - - componentDidMount = () => { - const { extendedPublicKeyImporter, validateAndSetBIP32Path } = this.props; - const { COLDCARD_MULTISIG_BIP32_PATH } = this.state; - if (extendedPublicKeyImporter.method === COLDCARD) { - validateAndSetBIP32Path( - COLDCARD_MULTISIG_BIP32_PATH, - () => {}, - () => {}, - {} - ); - } - }; - - componentDidUpdate(prevProps) { - const { validateAndSetBIP32Path, network, addressType } = this.props; - const coldcardBIP32Path = this.getColdcardBip32Path(); - - // Any updates to the network/addressType we should set the BIP32Path - if ( - prevProps.network !== network || - prevProps.addressType !== addressType - ) { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({ COLDCARD_MULTISIG_BIP32_PATH: coldcardBIP32Path }); - validateAndSetBIP32Path( - coldcardBIP32Path, - () => {}, - () => {} - ); - } - } - +const ColdcardExtendedPublicKeyImporter = ({ + extendedPublicKeyImporter, + validateAndSetExtendedPublicKey, + validateAndSetBIP32Path, + validateAndSetRootFingerprint, + addressType, + network, + defaultBIP32Path, +}) => { // Unfortunately not possible to use our Multisig P2SH ROOT on a Coldcard atm // because they do not allow us to export m/45'/{0-1}'/0' yet. - getColdcardBip32Path = () => { - const { network, addressType, defaultBIP32Path } = this.props; + const getColdcardBip32Path = () => { const coinPath = network === MAINNET ? "0" : "1"; const coldcardP2SHPath = `m/45'/${coinPath}/0`; return addressType === P2SH ? coldcardP2SHPath : defaultBIP32Path; }; - render = () => { - const { - extendedPublicKeyImporter, - validateAndSetExtendedPublicKey, - validateAndSetBIP32Path, - validateAndSetRootFingerprint, - addressType, - network, - } = this.props; - const { extendedPublicKeyError, COLDCARD_MULTISIG_BIP32_PATH } = this.state; - return ( - - - - {extendedPublicKeyError} - - + const [coldcardMultisigBIP32Path, setColdcardMultisigBIP32Path] = useState( + getColdcardBip32Path() + ); + + const resetColdcardBIP32Path = () => { + validateAndSetBIP32Path( + coldcardMultisigBIP32Path, + () => {}, + () => {} ); }; - resetColdcardBIP32Path = () => { - const { validateAndSetBIP32Path } = this.props; - const { COLDCARD_MULTISIG_BIP32_PATH } = this.state; + useEffect(() => { + if (extendedPublicKeyImporter.method === COLDCARD) { + validateAndSetBIP32Path( + coldcardMultisigBIP32Path, + () => {}, + () => {}, + {} + ); + } + }, []); + + useEffect(() => { + const newColdcardBIP32Path = getColdcardBip32Path(); + // eslint-disable-next-line react/no-did-update-set-state + setColdcardMultisigBIP32Path(newColdcardBIP32Path); validateAndSetBIP32Path( - COLDCARD_MULTISIG_BIP32_PATH, + newColdcardBIP32Path, () => {}, () => {} ); - }; -} + // Any updates to the network/addressType we should set the BIP32Path + }, [network, addressType]); + + return ( + + + + + + ); +}; ColdcardExtendedPublicKeyImporter.propTypes = { extendedPublicKeyImporter: PropTypes.shape({ diff --git a/src/components/Coldcard/ColdcardFileReader.jsx b/src/components/Coldcard/ColdcardFileReader.jsx index e5432b9f..aa492b0a 100644 --- a/src/components/Coldcard/ColdcardFileReader.jsx +++ b/src/components/Coldcard/ColdcardFileReader.jsx @@ -1,5 +1,5 @@ // eslint-disable-next-line max-classes-per-file -import React, { Component } from "react"; +import React from "react"; import PropTypes from "prop-types"; import Dropzone from "react-dropzone"; import { Buffer } from "buffer/"; @@ -8,90 +8,28 @@ import { CloudUpload as UploadIcon } from "@mui/icons-material"; import { PSBT_MAGIC_HEX } from "unchained-bitcoin"; import styles from "./ColdcardFileReader.module.scss"; -class ColdcardFileReaderBase extends Component { - constructor(props) { - super(props); - this.state = { - fileType: props.fileType || "JSON", - }; - } - - render = () => { - const { - maxFileSize, - validFileFormats, - extendedPublicKeyImporter, - handleBIP32PathChange, - resetBIP32Path, - bip32PathIsDefault, - hasError, - errorMessage, - isTest, - } = this.props; - const { fileType } = this.state; - return ( - - {fileType === "JSON" && !isTest && ( - - - - - - {!bip32PathIsDefault() && ( - - )} - - - Use the default value if you don’t understand BIP32 paths. - - - )} -

- When you are ready, upload the {fileType} file from your Coldcard: -

- - - -

- {fileType === "JSON" ? "Upload The XPUB" : "Upload Signed PSBT"} -

-
-
-
- ); - }; - - singleAcceptedFile = (acceptedFiles, rejectedFiles) => { +const ColdcardFileReaderBase = ({ + maxFileSize, + validFileFormats, + extendedPublicKeyImporter, + handleBIP32PathChange, + resetBIP32Path, + bip32PathIsDefault, + hasError, + errorMessage, + isTest, + onReceive, + onReceivePSBT, + setError, + fileType = "JSON", +}) => { + const singleAcceptedFile = (acceptedFiles, rejectedFiles) => { return rejectedFiles.length === 0 && acceptedFiles.length === 1; }; - onDrop = async (acceptedFiles, rejectedFiles) => { - const { onReceive, onReceivePSBT, setError, hasError } = this.props; - const { fileType } = this.state; + const onDrop = async (acceptedFiles, rejectedFiles) => { if (hasError) return; // do not continue if the bip32path is invalid - if (this.singleAcceptedFile(acceptedFiles, rejectedFiles)) { + if (singleAcceptedFile(acceptedFiles, rejectedFiles)) { const file = acceptedFiles[0]; if (fileType === "JSON") { onReceive(await file.text()); @@ -120,7 +58,58 @@ class ColdcardFileReaderBase extends Component { setError(`This dropzone only accepts a single file.`); } }; -} + + return ( + + {fileType === "JSON" && !isTest && ( + + + + + + {!bip32PathIsDefault() && ( + + )} + + + Use the default value if you don’t understand BIP32 paths. + + + )} +

When you are ready, upload the {fileType} file from your Coldcard:

+ + + +

+ {fileType === "JSON" ? "Upload The XPUB" : "Upload Signed PSBT"} +

+
+
+
+ ); +}; ColdcardFileReaderBase.propTypes = { onReceive: PropTypes.func, @@ -147,7 +136,7 @@ ColdcardFileReaderBase.defaultProps = { resetBIP32Path: null, bip32PathIsDefault: null, errorMessage: "", - hasError: PropTypes.bool, + hasError: false, maxFileSize: 1048576, // 1MB fileType: "JSON", validFileFormats: ".json", @@ -155,14 +144,22 @@ ColdcardFileReaderBase.defaultProps = { isTest: false, }; -export class ColdcardJSONReader extends ColdcardFileReaderBase {} -ColdcardJSONReader.defaultProps = { - fileType: "JSON", - validFileFormats: ".json", +export const ColdcardJSONReader = (props) => { + return ( + + ); }; -export class ColdcardPSBTReader extends ColdcardFileReaderBase {} -ColdcardPSBTReader.defaultProps = { - fileType: "PSBT", - validFileFormats: ".psbt", +export const ColdcardPSBTReader = (props) => { + return ( + + ); }; diff --git a/src/components/Coldcard/ColdcardSignatureImporter.jsx b/src/components/Coldcard/ColdcardSignatureImporter.jsx index e3b6d7e4..cce18488 100644 --- a/src/components/Coldcard/ColdcardSignatureImporter.jsx +++ b/src/components/Coldcard/ColdcardSignatureImporter.jsx @@ -3,17 +3,16 @@ import PropTypes from "prop-types"; import IndirectSignatureImporter from "../ScriptExplorer/IndirectSignatureImporter"; import ColdcardSigner from "./ColdcardSigner"; -const ColdcardSignatureImporter = (props) => { - const { - signatureImporter, - extendedPublicKeyImporter, - inputs, - outputs, - inputsTotalSats, - fee, - validateAndSetSignature, - network, - } = props; +const ColdcardSignatureImporter = ({ + signatureImporter, + extendedPublicKeyImporter, + inputs, + outputs, + inputsTotalSats, + fee, + validateAndSetSignature, + network, +}) => { return ( { - const { walletName, interaction, setActive } = this.props; - const body = interaction.request().toBase64(); - const timestamp = moment().format("HHmm"); - const filename = `${timestamp}-${walletName}.psbt`; - downloadFile(body, filename); - setActive(); - }; - - handleWalletConfigDownloadClick = () => { - const { walletDetailsText } = this.props; - this.reshapeConfig(walletDetailsText); - }; - +const ColdcardSigner = ({ + onReceivePSBT, + setError, + walletDetailsText, + walletName, + interaction, + setActive, +}) => { // This tries to reshape it to a Coldcard Wallet Config via unchained-wallets - reshapeConfig = (walletDetails) => { + const reshapeConfig = (walletDetails) => { const walletConfig = JSON.parse(walletDetails); const { startingAddressIndex } = walletConfig; // If this is a config that's been rekeyed, note that in the name. @@ -40,28 +33,36 @@ class ColdcardSigner extends Component { ? "P2WSH-P2SH" : walletConfig.addressType; - const interaction = ConfigAdapter({ + const interactionAdapter = ConfigAdapter({ KEYSTORE: COLDCARD, jsonConfig: walletConfig, }); - const body = interaction.adapt(); + const body = interactionAdapter.adapt(); const filename = `wc-${walletConfig.name}.txt`; downloadFile(body, filename); }; + const handlePSBTDownloadClick = () => { + const body = interaction.request().toBase64(); + const timestamp = moment().format("HHmm"); + const filename = `${timestamp}-${walletName}.psbt`; + downloadFile(body, filename); + setActive(); + }; - render = () => { - const { onReceivePSBT, setError } = this.props; - return ( -
- - -
- ); + const handleWalletConfigDownloadClick = () => { + reshapeConfig(walletDetailsText); }; -} + + return ( +
+ + +
+ ); +}; ColdcardSigner.propTypes = { walletName: PropTypes.string.isRequired, diff --git a/src/components/Coldcard/ColdcardSigningButtons.jsx b/src/components/Coldcard/ColdcardSigningButtons.jsx index 41ceaa1b..c5381173 100644 --- a/src/components/Coldcard/ColdcardSigningButtons.jsx +++ b/src/components/Coldcard/ColdcardSigningButtons.jsx @@ -2,8 +2,10 @@ import React from "react"; import PropTypes from "prop-types"; import { Button } from "@mui/material"; -const ColdcardSigningButtons = (props) => { - const { handlePSBTDownloadClick, handleWalletConfigDownloadClick } = props; +const ColdcardSigningButtons = ({ + handlePSBTDownloadClick, + handleWalletConfigDownloadClick, +}) => { return ( <> @@ -215,7 +206,7 @@ ${redeemScriptLine}${scriptsSpacer}${witnessScriptLine} @@ -229,17 +220,15 @@ ${redeemScriptLine}${scriptsSpacer}${witnessScriptLine} {"your address details will be displayed here."}

); - } + }; - render() { - return ( - - - {this.body()} - - ); - } -} + return ( + + + {body()} + + ); +}; AddressGenerator.propTypes = { network: PropTypes.string.isRequired, diff --git a/src/components/CreateAddress/Conflict.jsx b/src/components/CreateAddress/Conflict.jsx index 8ce4ce2a..ce75dac1 100644 --- a/src/components/CreateAddress/Conflict.jsx +++ b/src/components/CreateAddress/Conflict.jsx @@ -11,9 +11,7 @@ import { } from "@mui/material"; import { Warning } from "@mui/icons-material"; -const Conflict = (props) => { - const { message } = props; - +const Conflict = ({ message }) => { return ( diff --git a/src/components/CreateAddress/ExtendedPublicKeyPublicKeyImporter.jsx b/src/components/CreateAddress/ExtendedPublicKeyPublicKeyImporter.jsx index 1324174e..badac0e5 100644 --- a/src/components/CreateAddress/ExtendedPublicKeyPublicKeyImporter.jsx +++ b/src/components/CreateAddress/ExtendedPublicKeyPublicKeyImporter.jsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import PropTypes from "prop-types"; import { convertExtendedPublicKey, @@ -13,101 +13,18 @@ import { Button, TextField, FormHelperText, Box, Grid } from "@mui/material"; const DEFAULT_BIP32_PATH = "m/0"; -class ExtendedPublicKeyPublicKeyImporter extends React.Component { - constructor(props) { - super(props); - - this.state = { - bip32Path: DEFAULT_BIP32_PATH, - extendedPublicKey: "", - extendedPublicKeyError: "", - bip32PathError: "", - conversionMessage: "", - }; - } - - render = () => { - const { - bip32Path, - extendedPublicKey, - extendedPublicKeyError, - bip32PathError, - conversionMessage, - } = this.state; - return ( -
- - - - {conversionMessage !== "" && ( - - - {conversionMessage}, this may indicate an invalid network setting, - if so correct setting, remove key and try again. - - - )} - - - - - - - Use the default value if you don’t understand BIP32 paths. - - - - {!this.bip32PathIsDefault() && ( - - )} - - - - - - -
- ); - }; - - import = () => { - const { network, validatePublicKey, onImport } = this.props; - const { extendedPublicKey, bip32Path } = this.state; +const ExtendedPublicKeyPublicKeyImporter = ({ + network, + validatePublicKey, + onImport, +}) => { + const [bip32Path, setBip32Path] = useState(DEFAULT_BIP32_PATH); + const [bip32PathError, setBip32PathError] = useState(""); + const [extendedPublicKey, setExtendedPublicKey] = useState(""); + const [extendedPublicKeyError, setExtendedPublicKeyError] = useState(""); + const [conversionMessage, setConversionMessage] = useState(""); + + const importData = () => { const publicKey = deriveChildPublicKey( extendedPublicKey, bip32Path, @@ -116,56 +33,43 @@ class ExtendedPublicKeyPublicKeyImporter extends React.Component { const error = validatePublicKey(publicKey); if (error) { - this.setState({ - bip32PathError: error, - }); + setBip32PathError(error); } else { onImport({ publicKey, bip32Path }); } }; - hasBIP32PathError = () => { - const { bip32PathError } = this.state; + const hasBIP32PathError = () => { return bip32PathError !== ""; }; - hasExtendedPublicKeyError = () => { - const { extendedPublicKeyError } = this.state; + const hasExtendedPublicKeyError = () => { return extendedPublicKeyError !== ""; }; - hasError = () => this.hasBIP32PathError() || this.hasExtendedPublicKeyError(); + const hasError = () => hasBIP32PathError() || hasExtendedPublicKeyError(); - handleBIP32PathChange = (event) => { + const handleBIP32PathChange = (event) => { const nextBIP32Path = event.target.value; const error = validateBIP32Path(nextBIP32Path, { mode: "unhardened", }); - this.setState({ - bip32Path: nextBIP32Path, - bip32PathError: error ?? "", - }); + setBip32Path(nextBIP32Path); + setBip32PathError(error ?? ""); }; - bip32PathIsDefault = () => { - const { bip32Path } = this.state; + const bip32PathIsDefault = () => { return bip32Path === DEFAULT_BIP32_PATH; }; - resetBIP32Path = () => { - this.setState({ - bip32Path: DEFAULT_BIP32_PATH, - bip32PathError: "", - }); + const resetBIP32Path = () => { + setBip32Path(DEFAULT_BIP32_PATH); + setBip32PathError(""); }; - handleExtendedPublicKeyChange = (event) => { - const { network } = this.props; - - const extendedPublicKey = event.target.value; - + const handleExtendedPublicKeyChange = (event) => { const networkError = validateExtendedPublicKeyForNetwork( extendedPublicKey, network @@ -178,11 +82,10 @@ class ExtendedPublicKeyPublicKeyImporter extends React.Component { network === "testnet" ? "tpub" : "xpub" ); } catch (error) { - this.setState({ - extendedPublicKey, - extendedPublicKeyError: error.message, - conversionMessage: "", - }); + setExtendedPublicKey(event.target.value); + setExtendedPublicKeyError(error.message); + setConversionMessage(""); + return; } } @@ -192,28 +95,94 @@ class ExtendedPublicKeyPublicKeyImporter extends React.Component { network ); if (validationError !== "") { - this.setState({ - extendedPublicKey, - extendedPublicKeyError: validationError, - conversionMessage: "", - }); + setExtendedPublicKey(event.target.value); + setExtendedPublicKeyError(validationError); + setConversionMessage(""); + return; } - const conversionMessage = + const newConversionMessage = actualExtendedPublicKey === extendedPublicKey ? "" : `Your extended public key has been converted from ${extendedPublicKey.slice( 0, 4 )} to ${actualExtendedPublicKey.slice(0, 4)}`; - - this.setState({ - extendedPublicKey: actualExtendedPublicKey, - extendedPublicKeyError: "", - conversionMessage, - }); + setExtendedPublicKey(actualExtendedPublicKey); + setExtendedPublicKeyError(""); + setConversionMessage(newConversionMessage); }; -} + + return ( +
+ + + + {conversionMessage !== "" && ( + + + {conversionMessage}, this may indicate an invalid network setting, + if so correct setting, remove key and try again. + + + )} + + + + + + + Use the default value if you don’t understand BIP32 paths. + + + + {!bip32PathIsDefault() && ( + + )} + + + + + + +
+ ); +}; ExtendedPublicKeyPublicKeyImporter.propTypes = { network: PropTypes.string.isRequired, diff --git a/src/components/CreateAddress/HardwareWalletPublicKeyImporter.jsx b/src/components/CreateAddress/HardwareWalletPublicKeyImporter.jsx index 15cf0c18..8c4a109b 100644 --- a/src/components/CreateAddress/HardwareWalletPublicKeyImporter.jsx +++ b/src/components/CreateAddress/HardwareWalletPublicKeyImporter.jsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useCallback, useState } from "react"; import PropTypes from "prop-types"; import { UNSUPPORTED, @@ -16,34 +16,102 @@ import { Button, TextField, FormHelperText, Box, Grid } from "@mui/material"; import InteractionMessages from "../InteractionMessages"; -class HardwareWalletPublicKeyImporter extends React.Component { - constructor(props) { - super(props); - this.state = { - bip32Path: props.defaultBIP32Path, - publicKeyError: "", - bip32PathError: "", - status: this.interaction().isSupported() ? PENDING : UNSUPPORTED, - }; - } - - interaction = () => { - const { network, method, defaultBIP32Path } = this.props; - const { bip32Path } = this.state; +const HardwareWalletPublicKeyImporter = ({ + enableChangeMethod, + disableChangeMethod, + validatePublicKey, + onImport, + network, + method, + defaultBIP32Path, +}) => { + const [bip32Path, setBip32Path] = useState(defaultBIP32Path); + const [bip32PathError, setBip32PathError] = useState(""); + const [publicKeyError, setPublicKeyError] = useState(""); + + const interaction = useCallback(() => { return ExportPublicKey({ network, keystore: method, bip32Path: bip32Path ?? defaultBIP32Path, }); + }, [network, method, bip32Path]); + + const [status, setStatus] = useState( + interaction().isSupported() ? PENDING : UNSUPPORTED + ); + + const importData = async () => { + disableChangeMethod(); + + setPublicKeyError(""); + setStatus(ACTIVE); + + try { + const publicKey = await interaction().run(); + const error = validatePublicKey(publicKey); + + if (error) { + setPublicKeyError(error); + setStatus(PENDING); + } else { + onImport({ publicKey, bip32Path }); + } + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + setPublicKeyError(e.message); + setStatus(PENDING); + } + + enableChangeMethod(); }; - render = () => { - const { status, bip32Path, publicKeyError } = this.state; - const interaction = this.interaction(); + const hasBIP32PathError = () => { + return ( + bip32PathError !== "" || + interaction().hasMessagesFor({ + state: status, + level: ERROR, + code: "bip32", + }) + ); + }; + + const checkBip32PathError = () => { + if (bip32PathError !== "") { + return bip32PathError; + } + return interaction().messageTextFor({ + state: status, + level: ERROR, + code: "bip32", + }); + }; + + const handleBIP32PathChange = (event) => { + const nextBIP32Path = event.target.value; + const error = validateBIP32Path(nextBIP32Path); + + setBip32Path(nextBIP32Path); + setBip32PathError(error ?? ""); + }; + + const bip32PathIsDefault = () => { + return bip32Path === defaultBIP32Path; + }; + + const resetBIP32Path = () => { + setBip32Path(defaultBIP32Path); + setBip32PathError(""); + }; + + const renderHardWareWalletPublicKeyImporter = () => { + const newInteraction = interaction(); if (status === UNSUPPORTED) { return ( - {interaction.messageTextFor({ status })} + {newInteraction.messageTextFor({ status })} ); } @@ -56,19 +124,19 @@ class HardwareWalletPublicKeyImporter extends React.Component { label="BIP32 Path" value={bip32Path} variant="standard" - onChange={this.handleBIP32PathChange} + onChange={handleBIP32PathChange} disabled={status !== PENDING} - error={this.hasBIP32PathError()} - helperText={this.bip32PathError()} + error={hasBIP32PathError()} + helperText={checkBip32PathError()} /> - {!this.bip32PathIsDefault() && ( + {!bip32PathIsDefault() && ( @@ -99,86 +167,8 @@ class HardwareWalletPublicKeyImporter extends React.Component { ); }; - - import = async () => { - const { - enableChangeMethod, - disableChangeMethod, - validatePublicKey, - onImport, - } = this.props; - const { bip32Path } = this.state; - disableChangeMethod(); - this.setState({ publicKeyError: "", status: ACTIVE }); - try { - const publicKey = await this.interaction().run(); - const error = validatePublicKey(publicKey); - - if (error) { - this.setState({ - publicKeyError: error, - status: PENDING, - }); - } else { - onImport({ publicKey, bip32Path }); - } - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - this.setState({ publicKeyError: e.message, status: PENDING }); - } - - enableChangeMethod(); - }; - - hasBIP32PathError = () => { - const { bip32PathError, status } = this.state; - return ( - bip32PathError !== "" || - this.interaction().hasMessagesFor({ - state: status, - level: ERROR, - code: "bip32", - }) - ); - }; - - bip32PathError = () => { - const { bip32PathError, status } = this.state; - if (bip32PathError !== "") { - return bip32PathError; - } - return this.interaction().messageTextFor({ - state: status, - level: ERROR, - code: "bip32", - }); - }; - - handleBIP32PathChange = (event) => { - const nextBIP32Path = event.target.value; - const error = validateBIP32Path(nextBIP32Path); - - this.setState({ - bip32Path: nextBIP32Path, - bip32PathError: error ?? "", - }); - }; - - bip32PathIsDefault = () => { - const { bip32Path } = this.state; - const { defaultBIP32Path } = this.props; - return bip32Path === defaultBIP32Path; - }; - - resetBIP32Path = () => { - const { defaultBIP32Path } = this.props; - this.setState({ - bip32Path: defaultBIP32Path, - bip32PathError: "", - }); - }; -} + return renderHardWareWalletPublicKeyImporter(); +}; HardwareWalletPublicKeyImporter.propTypes = { network: PropTypes.string.isRequired, diff --git a/src/components/CreateAddress/HermitPublicKeyImporter.jsx b/src/components/CreateAddress/HermitPublicKeyImporter.jsx new file mode 100644 index 00000000..3d85ec4f --- /dev/null +++ b/src/components/CreateAddress/HermitPublicKeyImporter.jsx @@ -0,0 +1,80 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import { HERMIT, ExportPublicKey } from "unchained-wallets"; +import { validateBIP32Path } from "unchained-bitcoin"; + +// Components +import { FormGroup, FormHelperText } from "@mui/material"; + +import HermitReader from "../Hermit/HermitReader"; + +const HermitPublicKeyImporter = ({ + network, + defaultBIP32Path, + disableChangeMethod, + validatePublicKey, + onImport, + enableChangeMethod, +}) => { + const [publicKeyError, setPublicKeyError] = useState(""); + + const interaction = () => { + return ExportPublicKey({ + keystore: HERMIT, + network, + bip32Path: defaultBIP32Path, + }); + }; + + const handleReaderStart = () => { + disableChangeMethod(); + }; + + const handleReaderSuccess = (data) => { + const { pubkey: nextPublicKey, bip32Path: nextBIP32Path } = data; + + enableChangeMethod(); + + const bip32PathError = validateBIP32Path(nextBIP32Path); + if (bip32PathError) { + setPublicKeyError(bip32PathError); + return; + } + + const newPublicKeyError = validatePublicKey(nextPublicKey); + if (newPublicKeyError) { + setPublicKeyError(newPublicKeyError); + return; + } + + onImport({ publicKey: nextPublicKey, bip32Path: nextBIP32Path }); + }; + + const handleReaderClear = () => { + setPublicKeyError(""); + enableChangeMethod(); + }; + return ( + + + {publicKeyError} + + ); +}; + +HermitPublicKeyImporter.propTypes = { + network: PropTypes.string.isRequired, + defaultBIP32Path: PropTypes.string.isRequired, + validatePublicKey: PropTypes.func.isRequired, + enableChangeMethod: PropTypes.func.isRequired, + disableChangeMethod: PropTypes.func.isRequired, + onImport: PropTypes.func.isRequired, +}; + +export default HermitPublicKeyImporter; diff --git a/src/components/CreateAddress/PublicKeyImporter.jsx b/src/components/CreateAddress/PublicKeyImporter.jsx index 300ca077..3f8e37e8 100644 --- a/src/components/CreateAddress/PublicKeyImporter.jsx +++ b/src/components/CreateAddress/PublicKeyImporter.jsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import PropTypes from "prop-types"; import { connect } from "react-redux"; import { validatePublicKey as baseValidatePublicKey } from "unchained-bitcoin"; @@ -29,7 +29,6 @@ import Conflict from "./Conflict"; import { setPublicKeyImporterName, setPublicKeyImporterBIP32Path, - resetPublicKeyImporterBIP32Path, setPublicKeyImporterMethod, setPublicKeyImporterPublicKey, setPublicKeyImporterFinalized, @@ -40,159 +39,42 @@ import { const XPUB = "xpub"; const TEXT = "text"; -class PublicKeyImporter extends React.Component { - constructor(props) { - super(props); - - this.state = { - disableChangeMethod: false, - }; - } - - componentDidUpdate(prevProps) { - const { number, setFinalized, addressType, publicKeyImporter } = this.props; - - if ( - prevProps.addressType !== addressType && - this.validatePublicKey(publicKeyImporter.publicKey) - ) { - setFinalized(number, false); - } - } - - title = () => { - const { number, totalSigners, publicKeyImporter, setName } = this.props; - return ( - - - - - - - -   - - - - - ); - }; - - renderImport = () => { - const { publicKeyImporter, number } = this.props; - const { disableChangeMethod } = this.state; - - return ( -
- - - {"< Select method >"} - Trezor - Ledger - Derive from extended public key - Enter as text - - - - {this.renderImportByMethod()} -
- ); - }; - - renderImportByMethod = () => { - const { publicKeyImporter, network, defaultBIP32Path } = this.props; - - if ( - publicKeyImporter.method === TREZOR || - publicKeyImporter.method === LEDGER - ) { - return ( - - ); - } - if (publicKeyImporter.method === XPUB) { - return ( - - ); - } - if (publicKeyImporter.method === TEXT) { - return ( - - ); - } - return null; - }; +const PublicKeyImporter = ({ + publicKeyImporter, + publicKeyImporters, + number, + setFinalized, + addressType, + totalSigners, + setName, + network, + defaultBIP32Path, + setMethod, + setPublicKey, + moveUp, + moveDown, + setBIP32Path, +}) => { + const [disableChangeMethod, setDisableChangeMethod] = useState(false); // // Method // - handleMethodChange = (event) => { - const { number, setMethod, setPublicKey } = this.props; + const handleMethodChange = (event) => { setMethod(number, event.target.value); setPublicKey(number, ""); }; - disableChangeMethod = () => { - this.setState({ disableChangeMethod: true }); - }; - - enableChangeMethod = () => { - this.setState({ disableChangeMethod: false }); - }; - // // State // - finalize = () => { - const { number, setFinalized } = this.props; + const finalize = () => { setFinalized(number, true); }; - reset = () => { - const { number, setPublicKey, setFinalized } = this.props; + const reset = () => { setPublicKey(number, ""); setFinalized(number, false); }; @@ -201,14 +83,12 @@ class PublicKeyImporter extends React.Component { // Position // - moveUp = (event) => { - const { moveUp, number } = this.props; + const handleMoveUp = (event) => { event.preventDefault(); moveUp(number); }; - moveDown = (event) => { - const { moveDown, number } = this.props; + const handleMoveDown = (event) => { event.preventDefault(); moveDown(number); }; @@ -217,8 +97,7 @@ class PublicKeyImporter extends React.Component { // BIP32 Path // - renderBIP32Path = () => { - const { publicKeyImporter } = this.props; + const renderBIP32Path = () => { if (publicKeyImporter.method !== TEXT) { return ( @@ -242,22 +121,21 @@ class PublicKeyImporter extends React.Component { // Public Key // - renderPublicKey = () => { - const { publicKeyImporter } = this.props; + const renderPublicKey = () => { return (

The following public key was imported:

- {this.renderBIP32Path()} + {renderBIP32Path()} +   + + + + ); - } -} + }; + + const renderImportByMethod = () => { + if ( + publicKeyImporter.method === TREZOR || + publicKeyImporter.method === LEDGER + ) { + return ( + setDisableChangeMethod(false)} + disableChangeMethod={() => setDisableChangeMethod(true)} + defaultBIP32Path={defaultBIP32Path} + network={network} + onImport={handleImport} + /> + ); + } + if (publicKeyImporter.method === XPUB) { + return ( + + ); + } + if (publicKeyImporter.method === TEXT) { + return ( + + ); + } + return null; + }; + const renderImport = () => { + return ( +
+ + + {"< Select method >"} + Trezor + Ledger + Derive from extended public key + Enter as text + + + + {renderImportByMethod()} +
+ ); + }; + + useEffect(() => { + if (validatePublicKey(publicKeyImporter.publicKey)) { + setFinalized(number, false); + } + }, [addressType]); + + const publicKeyError = validatePublicKey(publicKeyImporter.publicKey); + return ( + + + + {publicKeyImporter.method && + publicKeyImporter.method !== TEXT && + publicKeyImporter.conflict && ( + + )} + {publicKeyError.includes( + "does not support uncompressed public keys" + ) && } + {publicKeyImporter.finalized ? renderPublicKey() : renderImport()} + + + ); +}; PublicKeyImporter.propTypes = { network: PropTypes.string.isRequired, @@ -360,7 +333,6 @@ function mapStateToProps(state, ownProps) { const mapDispatchToProps = { setName: setPublicKeyImporterName, setBIP32Path: setPublicKeyImporterBIP32Path, - resetBIP32Path: resetPublicKeyImporterBIP32Path, setMethod: setPublicKeyImporterMethod, setPublicKey: setPublicKeyImporterPublicKey, setFinalized: setPublicKeyImporterFinalized, diff --git a/src/components/CreateAddress/TextPublicKeyImporter.jsx b/src/components/CreateAddress/TextPublicKeyImporter.jsx index 7a809b24..b440821a 100644 --- a/src/components/CreateAddress/TextPublicKeyImporter.jsx +++ b/src/components/CreateAddress/TextPublicKeyImporter.jsx @@ -1,77 +1,59 @@ -import React from "react"; +import React, { useState } from "react"; import PropTypes from "prop-types"; // Components import { Button, TextField, Box } from "@mui/material"; -class TextPublicKeyImporter extends React.Component { - constructor(props) { - super(props); +const TextPublicKeyImporter = ({ validatePublicKey, onImport }) => { + const [error, setError] = useState(""); + const [publicKey, setPublicKey] = useState(""); - this.state = { - error: "", - publicKey: "", - }; - } + const importData = () => { + const newError = validatePublicKey(publicKey); - render = () => { - const { error, publicKey } = this.state; - return ( - - - - - - - - ); - }; - - import = () => { - const { validatePublicKey, onImport } = this.props; - const { publicKey } = this.state; - const error = validatePublicKey(publicKey); - - if (error) { - this.setError(error); + if (newError) { + setError(newError); } else { onImport({ publicKey }); } }; - hasError = () => { - const { error } = this.state; + const hasError = () => { return error !== ""; }; - setError = (value) => { - this.setState({ error: value }); + const handleChange = (event) => { + setPublicKey(event.target.value); + setError(validatePublicKey(publicKey)); }; - handleChange = (event) => { - const publicKey = event.target.value; - const { validatePublicKey } = this.props; - const error = validatePublicKey(publicKey); - this.setState({ publicKey, error }); - }; -} + return ( + + + + + + + + ); +}; TextPublicKeyImporter.propTypes = { validatePublicKey: PropTypes.func.isRequired, diff --git a/src/components/CreateAddress/index.jsx b/src/components/CreateAddress/index.jsx index d6f0a777..d0d70dfb 100644 --- a/src/components/CreateAddress/index.jsx +++ b/src/components/CreateAddress/index.jsx @@ -15,74 +15,70 @@ import ImportAddressesButton from "../ImportAddressesButton"; import { clientPropTypes } from "../../proptypes"; import "../styles.css"; -class CreateAddress extends React.Component { - render = () => { - const { address, client } = this.props; - return ( - - - -

Address Generator

-
- - {this.renderPublicKeyImporters()} - - - - - - - - - - - - - - - - - If you plan to use this address with your own bitcoind node - you can import the address created here by switching for - "Public" to "Private". Otherwise no - action is needed here. - - } - privateNotes={ -
- -
- } - /> -
-
+const CreateAddress = ({ address, client, totalSigners }) => { + return ( + + + +

Address Generator

+ + + + + + + + + + + + + + + + + + + If you plan to use this address with your own bitcoind node + you can import the address created here by switching for + "Public" to "Private". Otherwise no action + is needed here. + + } + privateNotes={ +
+ +
+ } + /> +
+
+
+
+ ); +}; + +const PublicKeyImporters = ({ totalSigners }) => { + const publicKeyImporters = []; + for ( + let publicKeyImporterNum = 1; + publicKeyImporterNum <= totalSigners; + publicKeyImporterNum += 1 + ) { + publicKeyImporters.push( + + ); - }; - - renderPublicKeyImporters = () => { - const { totalSigners } = this.props; - const publicKeyImporters = []; - for ( - let publicKeyImporterNum = 1; - publicKeyImporterNum <= totalSigners; - publicKeyImporterNum += 1 - ) { - publicKeyImporters.push( - - - - ); - } - return publicKeyImporters; - }; -} + } + return publicKeyImporters; +}; function mapStateToProps(state) { return { diff --git a/src/components/EditableName.jsx b/src/components/EditableName.jsx index fd707141..91320d0a 100644 --- a/src/components/EditableName.jsx +++ b/src/components/EditableName.jsx @@ -1,28 +1,49 @@ -import React from "react"; +import React, { useState } from "react"; import PropTypes from "prop-types"; import { Grid, IconButton, TextField } from "@mui/material"; import { Check, Clear, Edit } from "@mui/icons-material"; -class EditableName extends React.Component { - constructor(props) { - super(props); - this.state = { - editing: false, - newName: "", - error: "", - }; - } +const EditableName = ({ name, setName, number }) => { + const [editing, setEditing] = useState(false); + const [newName, setNewName] = useState(name); + const [error, setError] = useState(""); - componentDidMount = () => { - const { name } = this.props; - this.setState({ newName: name }); + const hasError = () => { + return error !== ""; + }; + + const startEditing = (event) => { + event.preventDefault(); + setEditing(true); + setNewName(name); + }; + + const handleChange = (event) => { + const updatedName = event.target.value; + + if ( + updatedName === null || + updatedName === undefined || + updatedName === "" + ) { + setError("Name cannot be blank."); + } + setNewName(updatedName); }; - render = () => { - const { name } = this.props; - const { editing, newName, error } = this.state; + const submit = () => { + setName(number, newName); + setEditing(false); + }; + + const cancel = () => { + setEditing(false); + setNewName(name); + setError(""); + }; + + const renderEditableName = () => { if (editing) { - //
return ( @@ -31,12 +52,12 @@ class EditableName extends React.Component { label="Name" value={newName} variant="standard" - onChange={this.handleChange} + onChange={handleChange} onFocus={(event) => { setTimeout(event.target.select.bind(event.target), 20); }} - onKeyDown={(e) => (e.key === "Enter" ? this.submit() : null)} - error={this.hasError()} + onKeyDown={(e) => (e.key === "Enter" ? submit() : null)} + error={hasError()} helperText={error} /> @@ -45,8 +66,8 @@ class EditableName extends React.Component { @@ -57,7 +78,7 @@ class EditableName extends React.Component { data-cy="cancel-button" color="secondary" size="small" - onClick={this.cancel} + onClick={cancel} > @@ -67,11 +88,7 @@ class EditableName extends React.Component { } return ( - +   @@ -79,46 +96,15 @@ class EditableName extends React.Component { {name} ); }; - - hasError = () => { - const { error } = this.state; - return error !== ""; - }; - - startEditing = (event) => { - const { name } = this.props; - event.preventDefault(); - this.setState({ editing: true, newName: name }); - }; - - handleChange = (event) => { - const newName = event.target.value; - let error = ""; - if (newName === null || newName === undefined || newName === "") { - error = "Name cannot be blank."; - } - this.setState({ newName, error }); - }; - - submit = () => { - const { setName, number } = this.props; - const { newName } = this.state; - setName(number, newName); - this.setState({ editing: false }); - }; - - cancel = () => { - const { name } = this.props; - this.setState({ error: "", newName: name, editing: false }); - }; -} + return renderEditableName(); +}; EditableName.propTypes = { number: PropTypes.number.isRequired, diff --git a/src/components/ErrorNotification.jsx b/src/components/ErrorNotification.jsx index c52cfc3d..f310d93d 100644 --- a/src/components/ErrorNotification.jsx +++ b/src/components/ErrorNotification.jsx @@ -6,9 +6,7 @@ import { Snackbar, IconButton } from "@mui/material"; import { Close } from "@mui/icons-material"; import { clearErrorNotification as clearErrorNotificationAction } from "../actions/errorNotificationActions"; -const ErrorNotificationBase = (props) => { - const { open, message, clearErrorNotification } = props; - +const ErrorNotificationBase = ({ open, message, clearErrorNotification }) => { return ( { +const MultisigDetails = ({ network, multisig, showAddress }) => { + const renderScript = (name, script) => { const hex = scriptToHex(script); const ops = scriptToOps(script); return ( @@ -38,59 +38,56 @@ class MultisigDetails extends React.Component { ); }; - render() { - const { network, multisig, showAddress } = this.props; - const { address } = multisig; - const redeemScript = multisigRedeemScript(multisig); - const witnessScript = multisigWitnessScript(multisig); - return ( - - {showAddress && Address} + const { address } = multisig; + const redeemScript = multisigRedeemScript(multisig); + const witnessScript = multisigWitnessScript(multisig); + return ( + + {showAddress && Address} - - - {showAddress && ( - - -   - {externalLink( - blockExplorerAddressURL(address, network), - - )} - - )} + + + {showAddress && ( + + +   + {externalLink( + blockExplorerAddressURL(address, network), + + )} + + )} - - - - + + + + - - - + + + - - - + + + - - - + + - + + - {this.renderScript("Script", multisig)} - {redeemScript && this.renderScript("Redeem Script", redeemScript)} - {witnessScript && this.renderScript("Witness Script", witnessScript)} - - ); - } -} + {renderScript("Script", multisig)} + {redeemScript && renderScript("Redeem Script", redeemScript)} + {witnessScript && renderScript("Witness Script", witnessScript)} + + ); +}; MultisigDetails.propTypes = { multisig: PropTypes.shape({ diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 5e9a9527..ede40631 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -34,20 +34,15 @@ const useStyles = makeStyles((theme) => ({ }, })); -// This needs to be a class component because it uses a ref -// eslint-disable-next-line react/prefer-stateless-function -class NavItem extends React.Component { - render() { - const { href, title, classes, handleClose } = this.props; - return ( - - - {title} - - - ); - } -} +const NavItem = ({ href, title, classes, handleClose }) => { + return ( + + + {title} + + + ); +}; NavItem.propTypes = { href: PropTypes.string.isRequired, diff --git a/src/components/NetworkPicker.jsx b/src/components/NetworkPicker.jsx index e92884d1..ef31d0e9 100644 --- a/src/components/NetworkPicker.jsx +++ b/src/components/NetworkPicker.jsx @@ -19,53 +19,49 @@ import { // Actions import { setNetwork as setNetworkAction } from "../actions/settingsActions"; -class NetworkPicker extends React.Component { - handleNetworkChange = (event) => { - const { setNetwork } = this.props; +const NetworkPicker = ({ setNetwork, network, frozen }) => { + const handleNetworkChange = (event) => { setNetwork(event.target.value); }; - render() { - const { network, frozen } = this.props; - return ( - - - - - - } - name="network" - value="mainnet" - label={Mainnet} - onChange={this.handleNetworkChange} - checked={network === MAINNET} - disabled={frozen} - /> - } - name="network" - value="testnet" - label="Testnet" - onChange={this.handleNetworkChange} - checked={network === TESTNET} - disabled={frozen} - /> - - - - Choose 'Mainnet' if you don't understand the - difference. - - - - - - ); - } -} + return ( + + + + + + } + name="network" + value="mainnet" + label={Mainnet} + onChange={handleNetworkChange} + checked={network === MAINNET} + disabled={frozen} + /> + } + name="network" + value="testnet" + label="Testnet" + onChange={handleNetworkChange} + checked={network === TESTNET} + disabled={frozen} + /> + + + + Choose 'Mainnet' if you don't understand the + difference. + + + + + + ); +}; NetworkPicker.propTypes = { network: PropTypes.string.isRequired, diff --git a/src/components/QuorumPicker.jsx b/src/components/QuorumPicker.jsx index 79734470..49c1b1a0 100644 --- a/src/components/QuorumPicker.jsx +++ b/src/components/QuorumPicker.jsx @@ -23,14 +23,39 @@ import "./styles.css"; const MAX_TOTAL_SIGNERS = 7; -class QuorumPicker extends React.Component { - renderIncrementRequiredSigners = () => { - const { requiredSigners, totalSigners, frozen } = this.props; +const QuorumPicker = ({ + frozen, + requiredSigners, + setRequiredSigners, + totalSigners, + setTotalSigners, +}) => { + const handleIncrementRequiredSigners = (event) => { + setRequiredSigners(requiredSigners + 1); + event.preventDefault(); + }; + + const handleDecrementRequiredSigners = (event) => { + setRequiredSigners(requiredSigners - 1); + event.preventDefault(); + }; + + const handleIncrementTotalSigners = (event) => { + setTotalSigners(totalSigners + 1); + event.preventDefault(); + }; + + const handleDecrementTotalSigners = (event) => { + setTotalSigners(totalSigners - 1); + event.preventDefault(); + }; + + const renderIncrementRequiredSigners = () => { const disabled = requiredSigners === totalSigners || frozen; return ( @@ -38,13 +63,12 @@ class QuorumPicker extends React.Component { ); }; - renderDecrementRequiredSigners = () => { - const { requiredSigners, frozen } = this.props; + const renderDecrementRequiredSigners = () => { const disabled = requiredSigners === 1 || frozen; return ( @@ -52,13 +76,12 @@ class QuorumPicker extends React.Component { ); }; - renderIncrementTotalSigners = () => { - const { totalSigners, frozen } = this.props; + const renderIncrementTotalSigners = () => { const disabled = totalSigners === MAX_TOTAL_SIGNERS || frozen; return ( @@ -66,14 +89,13 @@ class QuorumPicker extends React.Component { ); }; - renderDecrementTotalSigners = () => { - const { requiredSigners, totalSigners, frozen } = this.props; + const renderDecrementTotalSigners = () => { const disabled = totalSigners === requiredSigners || totalSigners === 2 || frozen; return ( @@ -81,109 +103,69 @@ class QuorumPicker extends React.Component { ); }; - handleIncrementRequiredSigners = (event) => { - const { requiredSigners, setRequiredSigners } = this.props; - setRequiredSigners(requiredSigners + 1); - event.preventDefault(); - }; + return ( + + + + + + +   + - handleDecrementRequiredSigners = (event) => { - const { requiredSigners, setRequiredSigners } = this.props; - setRequiredSigners(requiredSigners - 1); - event.preventDefault(); - }; + + {renderIncrementRequiredSigners()} - handleIncrementTotalSigners = (event) => { - const { totalSigners, setTotalSigners } = this.props; - setTotalSigners(totalSigners + 1); - event.preventDefault(); - }; + + {requiredSigners} + - handleDecrementTotalSigners = (event) => { - const { totalSigners, setTotalSigners } = this.props; - setTotalSigners(totalSigners - 1); - event.preventDefault(); - }; + + +

Required

+
+
- render() { - const { requiredSigners, totalSigners } = this.props; + {renderDecrementRequiredSigners()} +
- return ( - - - - - - -   + + + of + - - {this.renderIncrementRequiredSigners()} - - - {requiredSigners} - - - - -

Required

-
-
- - {this.renderDecrementRequiredSigners()} -
+ + {renderIncrementTotalSigners()} - - - of - + + {totalSigners} - - {this.renderIncrementTotalSigners()} - - - {totalSigners} - - - - -

Total

-
-
- - {this.renderDecrementTotalSigners()} -
- -   + + +

Total

+
+ + {renderDecrementTotalSigners()}
-
-
-
- ); - } -} + +   + +
+
+
+
+ ); +}; QuorumPicker.propTypes = { totalSigners: PropTypes.number.isRequired, diff --git a/src/components/StartingAddressIndexPicker.jsx b/src/components/StartingAddressIndexPicker.jsx index 6d5de617..cd683740 100644 --- a/src/components/StartingAddressIndexPicker.jsx +++ b/src/components/StartingAddressIndexPicker.jsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import PropTypes from "prop-types"; import { connect } from "react-redux"; import { validateBIP32Index } from "unchained-bitcoin"; @@ -19,18 +19,17 @@ import { import { wrappedActions } from "../actions/utils"; import { SET_STARTING_ADDRESS_INDEX } from "../actions/settingsActions"; -class StartingAddressIndexPicker extends React.Component { - constructor(props) { - super(props); +const StartingAddressIndexPicker = ({ + startingAddressIndex, + setStartingAddressIndex, +}) => { + const [customIndex, setCustomIndex] = useState(startingAddressIndex !== 0); + const [startingAddressIndexField, setStartingAddressIndexField] = + useState(startingAddressIndex); + const [startingAddressIndexError, setStartingAddressIndexError] = + useState(""); - this.state = { - customIndex: props.startingAddressIndex !== 0, - startingAddressIndexField: props.startingAddressIndex, - }; - } - - handleIndexChange = (event) => { - const { setStartingAddressIndex } = this.props; + const handleIndexChange = (event) => { const index = event.target.value; const error = validateBIP32Index(index, { mode: "unhardened" }).replace( "BIP32", @@ -39,80 +38,70 @@ class StartingAddressIndexPicker extends React.Component { if (!error && index) { setStartingAddressIndex(parseInt(index, 10)); } - this.setState({ - startingAddressIndexField: index, - startingAddressIndexError: error, - }); + setStartingAddressIndexField(index); + setStartingAddressIndexError(error); }; - handleCustomIndexChange = (event) => { - const customIndex = event.target.value === "true"; - this.setState({ customIndex }); + const handleCustomIndexChange = (event) => { + setCustomIndex(event.target.value === "true"); }; - render() { - const { - customIndex, - startingAddressIndexField, - startingAddressIndexError, - } = this.state; - return ( - - - - - - - - + return ( + + + + + + + + + } + name="customIndex" + value="false" + label={Default (0)} + onChange={handleCustomIndexChange} + checked={!customIndex} + /> + } name="customIndex" - value="false" - label={Default (0)} - onChange={this.handleCustomIndexChange} - checked={!customIndex} + value="true" + label="Custom" + onChange={handleCustomIndexChange} + checked={customIndex} /> - - } - name="customIndex" - value="true" - label="Custom" - onChange={this.handleCustomIndexChange} - checked={customIndex} - /> - - - Use the default value if you do not understand how to use - starting address index. - - - {customIndex && ( - - )} - - - - - ); - } -} + + + Use the default value if you do not understand how to use + starting address index. + + + {customIndex && ( + + )} + + + + + ); +}; StartingAddressIndexPicker.propTypes = { startingAddressIndex: PropTypes.number.isRequired, diff --git a/src/components/UnsignedTransaction.jsx b/src/components/UnsignedTransaction.jsx index 680dfef2..4893e075 100644 --- a/src/components/UnsignedTransaction.jsx +++ b/src/components/UnsignedTransaction.jsx @@ -1,27 +1,28 @@ -import React from "react"; +import React, { useState } from "react"; import PropTypes from "prop-types"; import { connect } from "react-redux"; import { Button } from "@mui/material"; import Copyable from "./Copyable"; -class UnsignedTransaction extends React.Component { - constructor(props) { - super(props); - this.state = { - showUnsignedTransaction: false, - }; - } +const UnsignedTransaction = ({ unsignedTransaction, unsignedPSBT }) => { + const [showUnsignedTransaction, setShowUnsignedTransaction] = useState(false); - render = () => { - const { showUnsignedTransaction } = this.state; - const { unsignedTransaction, unsignedPSBT } = this.props; + const handleShowUnsignedTransaction = () => { + setShowUnsignedTransaction(true); + }; + + const handleHideUnsignedTransaction = () => { + setShowUnsignedTransaction(false); + }; + + const renderUnsignedTransaction = () => { if (showUnsignedTransaction) { const hex = unsignedTransaction.toHex(); return (
- @@ -40,21 +41,14 @@ class UnsignedTransaction extends React.Component { } return ( - ); }; - - handleShowUnsignedTransaction = () => { - this.setState({ showUnsignedTransaction: true }); - }; - - handleHideUnsignedTransaction = () => { - this.setState({ showUnsignedTransaction: false }); - }; -} + return renderUnsignedTransaction(); +}; UnsignedTransaction.propTypes = { unsignedTransaction: PropTypes.shape({