Changeset View
Changeset View
Standalone View
Standalone View
web/cashtab/src/components/Send/Send.js
import React, { useState, useEffect } from 'react'; | import React, { useState, useEffect } from 'react'; | ||||
import { useLocation } from 'react-router-dom'; | import { useLocation } from 'react-router-dom'; | ||||
import PropTypes from 'prop-types'; | import PropTypes from 'prop-types'; | ||||
import { WalletContext } from 'utils/context'; | import { WalletContext } from 'utils/context'; | ||||
import { | import { | ||||
AntdFormWrapper, | AntdFormWrapper, | ||||
SendBchInput, | SendBchInput, | ||||
DestinationAddressSingle, | DestinationAddressSingle, | ||||
DestinationAddressMulti, | DestinationAddressMulti, | ||||
DestinationAddressSingleWithoutQRScan, | |||||
} from 'components/Common/EnhancedInputs'; | } from 'components/Common/EnhancedInputs'; | ||||
import { CustomCollapseCtn } from 'components/Common/StyledCollapse'; | import { CustomCollapseCtn } from 'components/Common/StyledCollapse'; | ||||
import { Form, message, Modal, Alert, Input } from 'antd'; | import { Form, message, Modal, Alert, Input } from 'antd'; | ||||
import { Row, Col, Switch } from 'antd'; | import { Row, Col, Switch } from 'antd'; | ||||
import PrimaryButton, { | import PrimaryButton, { DisabledButton } from 'components/Common/PrimaryButton'; | ||||
DisabledButton, | |||||
SmartButton, | |||||
} from 'components/Common/PrimaryButton'; | |||||
import useBCH from 'hooks/useBCH'; | import useBCH from 'hooks/useBCH'; | ||||
import useWindowDimensions from 'hooks/useWindowDimensions'; | import useWindowDimensions from 'hooks/useWindowDimensions'; | ||||
import { | import { | ||||
sendXecNotification, | sendXecNotification, | ||||
errorNotification, | errorNotification, | ||||
messageSignedNotification, | |||||
generalNotification, | |||||
} from 'components/Common/Notifications'; | } from 'components/Common/Notifications'; | ||||
import { isMobile, isIOS, isSafari } from 'react-device-detect'; | import { isMobile, isIOS, isSafari } from 'react-device-detect'; | ||||
import { currency, parseAddressForParams } from 'components/Common/Ticker.js'; | import { currency, parseAddressForParams } from 'components/Common/Ticker.js'; | ||||
import CopyToClipboard from 'components/Common/CopyToClipboard'; | |||||
import { Event } from 'utils/GoogleAnalytics'; | import { Event } from 'utils/GoogleAnalytics'; | ||||
import { | import { | ||||
fiatToCrypto, | fiatToCrypto, | ||||
shouldRejectAmountInput, | shouldRejectAmountInput, | ||||
isValidXecAddress, | isValidXecAddress, | ||||
isValidEtokenAddress, | isValidEtokenAddress, | ||||
isValidXecSendAmount, | isValidXecSendAmount, | ||||
} from 'utils/validation'; | } from 'utils/validation'; | ||||
import BalanceHeader from 'components/Common/BalanceHeader'; | import BalanceHeader from 'components/Common/BalanceHeader'; | ||||
import BalanceHeaderFiat from 'components/Common/BalanceHeaderFiat'; | import BalanceHeaderFiat from 'components/Common/BalanceHeaderFiat'; | ||||
import { | import { | ||||
ZeroBalanceHeader, | ZeroBalanceHeader, | ||||
ConvertAmount, | ConvertAmount, | ||||
AlertMsg, | AlertMsg, | ||||
WalletInfoCtn, | WalletInfoCtn, | ||||
SidePaddingCtn, | SidePaddingCtn, | ||||
FormLabel, | FormLabel, | ||||
} from 'components/Common/Atoms'; | } from 'components/Common/Atoms'; | ||||
import { | import { | ||||
getWalletState, | getWalletState, | ||||
convertToEcashPrefix, | |||||
toLegacyCash, | toLegacyCash, | ||||
toLegacyCashArray, | toLegacyCashArray, | ||||
fromSatoshisToXec, | fromSatoshisToXec, | ||||
calcFee, | calcFee, | ||||
} from 'utils/cashMethods'; | } from 'utils/cashMethods'; | ||||
import ApiError from 'components/Common/ApiError'; | import ApiError from 'components/Common/ApiError'; | ||||
import { formatFiatBalance, formatBalance } from 'utils/formatting'; | import { formatFiatBalance, formatBalance } from 'utils/formatting'; | ||||
import { | |||||
TokenParamLabel, | |||||
MessageVerificationParamLabel, | |||||
} from 'components/Common/Atoms'; | |||||
import { PlusSquareOutlined } from '@ant-design/icons'; | |||||
import styled from 'styled-components'; | import styled from 'styled-components'; | ||||
import WalletLabel from 'components/Common/WalletLabel.js'; | import WalletLabel from 'components/Common/WalletLabel.js'; | ||||
import { ThemedCopySolid } from 'components/Common/CustomIcons'; | |||||
const { TextArea } = Input; | const { TextArea } = Input; | ||||
const SignMessageLabel = styled.div` | |||||
text-align: left; | |||||
color: ${props => props.theme.forms.text}; | |||||
`; | |||||
const SignatureValidation = styled.div` | |||||
color: ${props => props.theme.encryptionRed}; | |||||
`; | |||||
const VerifyMessageLabel = styled.div` | |||||
text-align: left; | |||||
color: ${props => props.theme.forms.text}; | |||||
`; | |||||
const TextAreaLabel = styled.div` | const TextAreaLabel = styled.div` | ||||
text-align: left; | text-align: left; | ||||
color: ${props => props.theme.forms.text}; | color: ${props => props.theme.forms.text}; | ||||
padding-left: 1px; | padding-left: 1px; | ||||
`; | `; | ||||
const AmountPreviewCtn = styled.div` | const AmountPreviewCtn = styled.div` | ||||
margin-top: -30px; | margin-top: -30px; | ||||
`; | `; | ||||
const SendInputCtn = styled.div` | const SendInputCtn = styled.div` | ||||
.ant-form-item-with-help { | .ant-form-item-with-help { | ||||
margin-bottom: 32px; | margin-bottom: 32px; | ||||
} | } | ||||
`; | `; | ||||
const LocaleFormattedValue = styled.h3` | const LocaleFormattedValue = styled.h3` | ||||
color: ${props => props.theme.contrast}; | color: ${props => props.theme.contrast}; | ||||
font-weight: bold; | font-weight: bold; | ||||
margin-bottom: 0; | margin-bottom: 0; | ||||
`; | `; | ||||
const AddressCopyCtn = styled.div` | |||||
display: flex; | |||||
align-items: center; | |||||
gap: 0.5rem; | |||||
svg { | |||||
height: 30px; | |||||
width: 30px; | |||||
&:hover { | |||||
fill: ${props => props.theme.eCashBlue}; | |||||
cursor: pointer; | |||||
} | |||||
} | |||||
`; | |||||
// Note jestBCH is only used for unit tests; BCHJS must be mocked for jest | // Note jestBCH is only used for unit tests; BCHJS must be mocked for jest | ||||
const SendBCH = ({ jestBCH, passLoadingStatus }) => { | const SendBCH = ({ jestBCH, passLoadingStatus }) => { | ||||
// use balance parameters from wallet.state object and not legacy balances parameter from walletState, if user has migrated wallet | // use balance parameters from wallet.state object and not legacy balances parameter from walletState, if user has migrated wallet | ||||
// this handles edge case of user with old wallet who has not opened latest Cashtab version yet | // this handles edge case of user with old wallet who has not opened latest Cashtab version yet | ||||
// If the wallet object from ContextValue has a `state key`, then check which keys are in the wallet object | // If the wallet object from ContextValue has a `state key`, then check which keys are in the wallet object | ||||
// Else set it as blank | // Else set it as blank | ||||
const ContextValue = React.useContext(WalletContext); | const ContextValue = React.useContext(WalletContext); | ||||
const location = useLocation(); | const location = useLocation(); | ||||
const { | const { | ||||
BCH, | BCH, | ||||
wallet, | wallet, | ||||
fiatPrice, | fiatPrice, | ||||
apiError, | apiError, | ||||
cashtabSettings, | cashtabSettings, | ||||
changeCashtabSettings, | changeCashtabSettings, | ||||
chronik, | chronik, | ||||
} = ContextValue; | } = ContextValue; | ||||
const walletState = getWalletState(wallet); | const walletState = getWalletState(wallet); | ||||
const { balances, slpBalancesAndUtxos } = walletState; | const { balances, slpBalancesAndUtxos } = walletState; | ||||
// Modal settings | // Modal settings | ||||
const [showConfirmMsgToSign, setShowConfirmMsgToSign] = useState(false); | |||||
const [msgToSign, setMsgToSign] = useState(''); | |||||
const [signMessageIsValid, setSignMessageIsValid] = useState(null); | |||||
const [isOneToManyXECSend, setIsOneToManyXECSend] = useState(false); | const [isOneToManyXECSend, setIsOneToManyXECSend] = useState(false); | ||||
const [opReturnMsg, setOpReturnMsg] = useState(false); | const [opReturnMsg, setOpReturnMsg] = useState(false); | ||||
const [isEncryptedOptionalOpReturnMsg, setIsEncryptedOptionalOpReturnMsg] = | const [isEncryptedOptionalOpReturnMsg, setIsEncryptedOptionalOpReturnMsg] = | ||||
useState(false); | useState(false); | ||||
const [bchObj, setBchObj] = useState(false); | const [bchObj, setBchObj] = useState(false); | ||||
// Get device window width | // Get device window width | ||||
// If this is less than 769, the page will open with QR scanner open | // If this is less than 769, the page will open with QR scanner open | ||||
Show All 17 Lines | const SendBCH = ({ jestBCH, passLoadingStatus }) => { | ||||
const [selectedCurrency, setSelectedCurrency] = useState(currency.ticker); | const [selectedCurrency, setSelectedCurrency] = useState(currency.ticker); | ||||
// Support cashtab button from web pages | // Support cashtab button from web pages | ||||
const [txInfoFromUrl, setTxInfoFromUrl] = useState(false); | const [txInfoFromUrl, setTxInfoFromUrl] = useState(false); | ||||
// Show a confirmation modal on transactions created by populating form from web page button | // Show a confirmation modal on transactions created by populating form from web page button | ||||
const [isModalVisible, setIsModalVisible] = useState(false); | const [isModalVisible, setIsModalVisible] = useState(false); | ||||
const [messageSignature, setMessageSignature] = useState(''); | |||||
const [sigCopySuccess, setSigCopySuccess] = useState(''); | |||||
const [showConfirmMsgToVerify, setShowConfirmMsgToVerify] = useState(false); | |||||
const [messageVerificationAddr, setMessageVerificationAddr] = useState(''); | |||||
const [messageVerificationSig, setMessageVerificationSig] = useState(''); | |||||
const [messageVerificationMsg, setMessageVerificationMsg] = useState(''); | |||||
const [messageVerificationAddrIsValid, setMessageVerificationAddrIsValid] = | |||||
useState(false); | |||||
const [messageVerificationSigIsValid, setMessageVerificationSigIsValid] = | |||||
useState(false); | |||||
const [messageVerificationMsgIsValid, setMessageVerificationMsgIsValid] = | |||||
useState(false); | |||||
const [messageVerificationAddrError, setMessageVerificationAddrError] = | |||||
useState(false); | |||||
const [messageVerificationSigError, setMessageVerificationSigError] = | |||||
useState(false); | |||||
const [airdropFlag, setAirdropFlag] = useState(false); | const [airdropFlag, setAirdropFlag] = useState(false); | ||||
const userLocale = navigator.language; | const userLocale = navigator.language; | ||||
const clearInputForms = () => { | const clearInputForms = () => { | ||||
setFormData({ | setFormData({ | ||||
value: '', | value: '', | ||||
address: '', | address: '', | ||||
}); | }); | ||||
▲ Show 20 Lines • Show All 314 Lines • ▼ Show 20 Lines | const handleAddressChange = e => { | ||||
// Set address field to user input | // Set address field to user input | ||||
setFormData(p => ({ | setFormData(p => ({ | ||||
...p, | ...p, | ||||
[name]: value, | [name]: value, | ||||
})); | })); | ||||
}; | }; | ||||
const handleMessageVerificationAddrChange = e => { | |||||
const { value } = e.target; | |||||
let error = false; | |||||
let addressString = value; | |||||
// parse address for parameters | |||||
const addressInfo = parseAddressForParams(addressString); | |||||
// validate address | |||||
const isValid = isValidXecAddress(addressInfo.address); | |||||
const { address } = addressInfo; | |||||
// Is this valid address? | |||||
if (!isValid) { | |||||
error = `Invalid ${currency.ticker} address`; | |||||
// If valid address but token format | |||||
if (isValidEtokenAddress(address)) { | |||||
error = `eToken addresses are not supported for signature verifications`; | |||||
} | |||||
setMessageVerificationAddrIsValid(false); | |||||
} else { | |||||
setMessageVerificationAddrIsValid(true); | |||||
} | |||||
setMessageVerificationAddrError(error); | |||||
setMessageVerificationAddr(address); | |||||
}; | |||||
const handleMultiAddressChange = e => { | const handleMultiAddressChange = e => { | ||||
const { value, name } = e.target; | const { value, name } = e.target; | ||||
let error; | let error; | ||||
if (!value) { | if (!value) { | ||||
error = 'Input must not be blank'; | error = 'Input must not be blank'; | ||||
setSendBchAddressError(error); | setSendBchAddressError(error); | ||||
return setFormData(p => ({ | return setFormData(p => ({ | ||||
▲ Show 20 Lines • Show All 77 Lines • ▼ Show 20 Lines | const handleBchAmountChange = e => { | ||||
setSendBchAmountError(error); | setSendBchAmountError(error); | ||||
setFormData(p => ({ | setFormData(p => ({ | ||||
...p, | ...p, | ||||
[name]: value, | [name]: value, | ||||
})); | })); | ||||
}; | }; | ||||
const handleSignMsgChange = e => { | |||||
const { value } = e.target; | |||||
// validation | |||||
if (value && value.length && value.length < 150) { | |||||
setMsgToSign(value); | |||||
setSignMessageIsValid(true); | |||||
} else { | |||||
setSignMessageIsValid(false); | |||||
} | |||||
}; | |||||
const handleVerifyMsgChange = e => { | |||||
const { value } = e.target; | |||||
// validation | |||||
if (value && value.length && value.length < 150) { | |||||
setMessageVerificationMsgIsValid(true); | |||||
} else { | |||||
setMessageVerificationMsgIsValid(false); | |||||
} | |||||
setMessageVerificationMsg(value); | |||||
}; | |||||
const handleVerifySigChange = e => { | |||||
const { value } = e.target; | |||||
// validation | |||||
if (value && value.length && value.length === 88) { | |||||
setMessageVerificationSigIsValid(true); | |||||
setMessageVerificationSigError(false); | |||||
} else { | |||||
setMessageVerificationSigIsValid(false); | |||||
setMessageVerificationSigError('Invalid signature'); | |||||
} | |||||
setMessageVerificationSig(value); | |||||
}; | |||||
const verifyMessageBySig = async () => { | |||||
let verification; | |||||
try { | |||||
verification = await bchObj.BitcoinCash.verifyMessage( | |||||
toLegacyCash(messageVerificationAddr), | |||||
messageVerificationSig, | |||||
messageVerificationMsg, | |||||
); | |||||
} catch (err) { | |||||
errorNotification( | |||||
'Error', | |||||
'Unable to execute signature verification', | |||||
); | |||||
} | |||||
if (verification) { | |||||
generalNotification('Signature successfully verified', 'Verified'); | |||||
} else { | |||||
errorNotification( | |||||
'Error', | |||||
'Signature does not match address and message', | |||||
); | |||||
} | |||||
setShowConfirmMsgToVerify(false); | |||||
}; | |||||
const signMessageByPk = async () => { | |||||
try { | |||||
const messageSignature = | |||||
await BCH.BitcoinCash.signMessageWithPrivKey( | |||||
wallet.Path1899.fundingWif, | |||||
msgToSign, | |||||
); | |||||
setMessageSignature(messageSignature); | |||||
messageSignedNotification(messageSignature); | |||||
} catch (err) { | |||||
let message; | |||||
if (!err.error && !err.message) { | |||||
message = err.message || err.error || JSON.stringify(err); | |||||
} | |||||
errorNotification(err, message, 'Message Signing Error'); | |||||
throw err; | |||||
} | |||||
// Hide the modal | |||||
setShowConfirmMsgToSign(false); | |||||
setSigCopySuccess(''); | |||||
}; | |||||
const handleOnSigCopy = () => { | |||||
if (messageSignature != '') { | |||||
setSigCopySuccess('Signature copied to clipboard'); | |||||
} | |||||
}; | |||||
const onMax = async () => { | const onMax = async () => { | ||||
// Clear amt error | // Clear amt error | ||||
setSendBchAmountError(false); | setSendBchAmountError(false); | ||||
// Set currency to BCH | // Set currency to BCH | ||||
setSelectedCurrency(currency.ticker); | setSelectedCurrency(currency.ticker); | ||||
try { | try { | ||||
const txFeeSats = calcFee(slpBalancesAndUtxos.nonSlpUtxos); | const txFeeSats = calcFee(slpBalancesAndUtxos.nonSlpUtxos); | ||||
▲ Show 20 Lines • Show All 396 Lines • ▼ Show 20 Lines | return ( | ||||
} | } | ||||
/> | /> | ||||
</AntdFormWrapper> | </AntdFormWrapper> | ||||
</CustomCollapseCtn> | </CustomCollapseCtn> | ||||
{apiError && <ApiError />} | {apiError && <ApiError />} | ||||
</Form> | </Form> | ||||
</Col> | </Col> | ||||
</Row> | </Row> | ||||
<Modal | |||||
title={`Please review and confirm your message to be signed using this wallet.`} | |||||
open={showConfirmMsgToSign} | |||||
onOk={signMessageByPk} | |||||
onCancel={() => setShowConfirmMsgToSign(false)} | |||||
> | |||||
<TokenParamLabel>Message:</TokenParamLabel> {msgToSign} | |||||
<br /> | |||||
</Modal> | |||||
<CustomCollapseCtn panelHeader="Sign Message"> | |||||
<AntdFormWrapper> | |||||
<Form | |||||
size="small" | |||||
style={{ | |||||
width: 'auto', | |||||
}} | |||||
> | |||||
<Form.Item> | |||||
<SignMessageLabel>Message:</SignMessageLabel> | |||||
<TextArea | |||||
name="signMessage" | |||||
onChange={e => handleSignMsgChange(e)} | |||||
showCount | |||||
maxLength={150} | |||||
/> | |||||
</Form.Item> | |||||
<Form.Item> | |||||
<SignMessageLabel>Address:</SignMessageLabel> | |||||
{wallet && | |||||
wallet.Path1899 && | |||||
wallet.Path1899.cashAddress && ( | |||||
<AddressCopyCtn> | |||||
<Input | |||||
name="signMessageAddress" | |||||
disabled={true} | |||||
value={ | |||||
wallet && | |||||
wallet.Path1899 && | |||||
wallet.Path1899.cashAddress | |||||
? convertToEcashPrefix( | |||||
wallet.Path1899 | |||||
.cashAddress, | |||||
) | |||||
: '' | |||||
} | |||||
/> | |||||
<CopyToClipboard | |||||
data={convertToEcashPrefix( | |||||
wallet.Path1899.cashAddress, | |||||
)} | |||||
optionalOnCopyNotification={{ | |||||
title: 'Copied', | |||||
msg: `${convertToEcashPrefix( | |||||
wallet.Path1899 | |||||
.cashAddress, | |||||
)} copied to clipboard`, | |||||
}} | |||||
> | |||||
<ThemedCopySolid /> | |||||
</CopyToClipboard> | |||||
</AddressCopyCtn> | |||||
)} | |||||
</Form.Item> | |||||
<SmartButton | |||||
onClick={() => setShowConfirmMsgToSign(true)} | |||||
disabled={!signMessageIsValid} | |||||
> | |||||
<PlusSquareOutlined /> | |||||
Sign Message | |||||
</SmartButton> | |||||
<CopyToClipboard | |||||
data={messageSignature} | |||||
optionalOnCopyNotification={{ | |||||
title: 'Message signature copied to clipboard', | |||||
msg: `${messageSignature}`, | |||||
}} | |||||
> | |||||
<Form.Item> | |||||
<SignMessageLabel> | |||||
Signature: | |||||
</SignMessageLabel> | |||||
<TextArea | |||||
name="signMessageSignature" | |||||
placeholder="The signature will be generated upon signing of the message" | |||||
readOnly={true} | |||||
value={messageSignature} | |||||
onClick={() => handleOnSigCopy()} | |||||
/> | |||||
</Form.Item> | |||||
</CopyToClipboard> | |||||
{sigCopySuccess} | |||||
</Form> | |||||
</AntdFormWrapper> | |||||
</CustomCollapseCtn> | |||||
<Modal | |||||
title={`Please review and confirm your message, signature and address to be verified.`} | |||||
open={showConfirmMsgToVerify} | |||||
onOk={verifyMessageBySig} | |||||
onCancel={() => setShowConfirmMsgToVerify(false)} | |||||
> | |||||
<MessageVerificationParamLabel> | |||||
Message: | |||||
</MessageVerificationParamLabel>{' '} | |||||
{messageVerificationMsg} | |||||
<br /> | |||||
<MessageVerificationParamLabel> | |||||
Address: | |||||
</MessageVerificationParamLabel>{' '} | |||||
{messageVerificationAddr} | |||||
<br /> | |||||
<MessageVerificationParamLabel> | |||||
Signature: | |||||
</MessageVerificationParamLabel>{' '} | |||||
{messageVerificationSig} | |||||
<br /> | |||||
</Modal> | |||||
<CustomCollapseCtn panelHeader="Verify Message"> | |||||
<AntdFormWrapper> | |||||
<Form | |||||
size="small" | |||||
style={{ | |||||
width: 'auto', | |||||
}} | |||||
> | |||||
<Form.Item> | |||||
<VerifyMessageLabel> | |||||
Message: | |||||
</VerifyMessageLabel> | |||||
<TextArea | |||||
name="verifyMessage" | |||||
onChange={e => handleVerifyMsgChange(e)} | |||||
showCount | |||||
maxLength={150} | |||||
/> | |||||
</Form.Item> | |||||
<Form.Item> | |||||
<VerifyMessageLabel> | |||||
Address: | |||||
</VerifyMessageLabel> | |||||
<DestinationAddressSingleWithoutQRScan | |||||
validateStatus={ | |||||
messageVerificationAddrError | |||||
? 'error' | |||||
: '' | |||||
} | |||||
help={ | |||||
messageVerificationAddrError | |||||
? messageVerificationAddrError | |||||
: '' | |||||
} | |||||
inputProps={{ | |||||
placeholder: `${currency.ticker} Address`, | |||||
name: 'address', | |||||
onChange: e => | |||||
handleMessageVerificationAddrChange( | |||||
e, | |||||
), | |||||
required: true, | |||||
}} | |||||
></DestinationAddressSingleWithoutQRScan> | |||||
</Form.Item> | |||||
<Form.Item> | |||||
<VerifyMessageLabel> | |||||
Signature: | |||||
</VerifyMessageLabel> | |||||
<TextArea | |||||
name="verifySignature" | |||||
onChange={e => handleVerifySigChange(e)} | |||||
showCount | |||||
/> | |||||
<SignatureValidation> | |||||
{messageVerificationSigError} | |||||
</SignatureValidation> | |||||
</Form.Item> | |||||
<SmartButton | |||||
onClick={() => setShowConfirmMsgToVerify(true)} | |||||
disabled={ | |||||
!messageVerificationAddrIsValid || | |||||
!messageVerificationSigIsValid || | |||||
!messageVerificationMsgIsValid | |||||
} | |||||
> | |||||
<PlusSquareOutlined /> | |||||
Verify Message | |||||
</SmartButton> | |||||
</Form> | |||||
</AntdFormWrapper> | |||||
</CustomCollapseCtn> | |||||
</SidePaddingCtn> | </SidePaddingCtn> | ||||
</> | </> | ||||
); | ); | ||||
}; | }; | ||||
/* | /* | ||||
passLoadingStatus must receive a default prop that is a function | passLoadingStatus must receive a default prop that is a function | ||||
in order to pass the rendering unit test in Send.test.js | in order to pass the rendering unit test in Send.test.js | ||||
Show All 16 Lines |