Page MenuHomePhabricator

No OneTemporary

diff --git a/web/cashtab/src/components/Airdrop/Airdrop.js b/web/cashtab/src/components/Airdrop/Airdrop.js
index c4db1abe8..79f2a5080 100644
--- a/web/cashtab/src/components/Airdrop/Airdrop.js
+++ b/web/cashtab/src/components/Airdrop/Airdrop.js
@@ -1,847 +1,844 @@
import React, { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import PropTypes from 'prop-types';
import BigNumber from 'bignumber.js';
import styled from 'styled-components';
import { WalletContext } from 'utils/context';
import {
AntdFormWrapper,
DestinationAddressMulti,
InputAmountSingle,
} from 'components/Common/EnhancedInputs';
import { CustomCollapseCtn } from 'components/Common/StyledCollapse';
import { Form, Alert, Input, Modal, Spin, Progress } from 'antd';
const { TextArea } = Input;
import { Row, Col, Switch } from 'antd';
import { SmartButton } from 'components/Common/PrimaryButton';
import useBCH from 'hooks/useBCH';
import { errorNotification } from 'components/Common/Notifications';
import { currency } from 'components/Common/Ticker.js';
import BalanceHeader from 'components/Common/BalanceHeader';
import BalanceHeaderFiat from 'components/Common/BalanceHeaderFiat';
import CopyToClipboard from 'components/Common/CopyToClipboard';
import {
getWalletState,
convertEtokenToEcashAddr,
fromSatoshisToXec,
convertToEcashPrefix,
convertEcashtoEtokenAddr,
} from 'utils/cashMethods';
import {
isValidTokenId,
isValidXecAirdrop,
isValidAirdropOutputsArray,
isValidAirdropExclusionArray,
} from 'utils/validation';
import { CustomSpinner } from 'components/Common/CustomIcons';
import * as etokenList from 'etoken-list';
import {
ZeroBalanceHeader,
SidePaddingCtn,
WalletInfoCtn,
} from 'components/Common/Atoms';
import WalletLabel from 'components/Common/WalletLabel.js';
import { Link } from 'react-router-dom';
const AirdropActions = styled.div`
text-align: center;
width: 100%;
padding: 10px;
border-radius: 5px;
display: flex;
justify-content: center;
a {
color: ${props => props.theme.contrast};
margin: 0;
font-size: 11px;
border: 1px solid ${props => props.theme.contrast};
border-radius: 5px;
padding: 2px 10px;
opacity: 0.6;
}
a:hover {
opacity: 1;
border-color: ${props => props.theme.eCashBlue};
color: ${props => props.theme.contrast};
background: ${props => props.theme.eCashBlue};
}
${({ received, ...props }) =>
received &&
`
text-align: left;
background: ${props.theme.receivedMessage};
`}
`;
const AirdropOptions = styled.div`
text-align: left;
color: ${props => props.theme.contrast};
`;
const StyledModal = styled(Modal)`
.ant-progress-text {
color: ${props => props.theme.lightWhite} !important;
}
`;
// Note jestBCH is only used for unit tests; BCHJS must be mocked for jest
const Airdrop = ({ jestBCH, passLoadingStatus }) => {
const ContextValue = React.useContext(WalletContext);
const { wallet, fiatPrice, cashtabSettings } = ContextValue;
const location = useLocation();
const walletState = getWalletState(wallet);
const { balances } = walletState;
const [bchObj, setBchObj] = useState(false);
const [isAirdropCalcModalVisible, setIsAirdropCalcModalVisible] =
useState(false);
const [airdropCalcModalProgress, setAirdropCalcModalProgress] = useState(0); // the dynamic % progress bar
useEffect(() => {
// jestBCH is only ever specified for unit tests, otherwise app will use getBCH();
const BCH = jestBCH ? jestBCH : getBCH();
// set the BCH instance to state, for other functions to reference
setBchObj(BCH);
if (location && location.state && location.state.airdropEtokenId) {
setFormData({
...formData,
tokenId: location.state.airdropEtokenId,
});
handleTokenIdInput({
target: {
value: location.state.airdropEtokenId,
},
});
}
}, []);
const [formData, setFormData] = useState({
tokenId: '',
totalAirdrop: '',
});
const [tokenIdIsValid, setTokenIdIsValid] = useState(null);
const [totalAirdropIsValid, setTotalAirdropIsValid] = useState(null);
const [airdropRecipients, setAirdropRecipients] = useState('');
const [airdropOutputIsValid, setAirdropOutputIsValid] = useState(true);
const [etokenHolders, setEtokenHolders] = useState(parseInt(0));
const [showAirdropOutputs, setShowAirdropOutputs] = useState(false);
const [ignoreOwnAddress, setIgnoreOwnAddress] = useState(false);
const [ignoreRecipientsBelowDust, setIgnoreRecipientsBelowDust] =
useState(false);
const [ignoreMintAddress, setIgnoreMintAddress] = useState(false);
// flag to reflect the exclusion list checkbox
const [ignoreCustomAddresses, setIgnoreCustomAddresses] = useState(false);
// the exclusion list values
const [ignoreCustomAddressesList, setIgnoreCustomAddressesList] =
useState(false);
const [
ignoreCustomAddressesListIsValid,
setIgnoreCustomAddressesListIsValid,
] = useState(false);
const [ignoreCustomAddressListError, setIgnoreCustomAddressListError] =
useState(false);
// flag to reflect the ignore minimum etoken balance switch
const [ignoreMinEtokenBalance, setIgnoreMinEtokenBalance] = useState(false);
const [ignoreMinEtokenBalanceAmount, setIgnoreMinEtokenBalanceAmount] =
useState(new BigNumber(0));
const [
ignoreMinEtokenBalanceAmountIsValid,
setIgnoreMinEtokenBalanceAmountIsValid,
] = useState(false);
const [
ignoreMinEtokenBalanceAmountError,
setIgnoreMinEtokenBalanceAmountError,
] = useState(false);
const { getBCH } = useBCH();
const handleTokenIdInput = e => {
const { name, value } = e.target;
setTokenIdIsValid(isValidTokenId(value));
setFormData(p => ({
...p,
[name]: value,
}));
};
const handleTotalAirdropInput = e => {
const { name, value } = e.target;
setTotalAirdropIsValid(isValidXecAirdrop(value));
setFormData(p => ({
...p,
[name]: value,
}));
};
const handleMinEtokenBalanceChange = e => {
const { value } = e.target;
if (new BigNumber(value).gt(new BigNumber(0))) {
setIgnoreMinEtokenBalanceAmountIsValid(true);
setIgnoreMinEtokenBalanceAmountError(false);
} else {
setIgnoreMinEtokenBalanceAmountError(
'Minimum eToken balance must be greater than 0',
);
setIgnoreMinEtokenBalanceAmountIsValid(false);
}
setIgnoreMinEtokenBalanceAmount(value);
};
const calculateXecAirdrop = async () => {
// display airdrop calculation message modal
setIsAirdropCalcModalVisible(true);
setShowAirdropOutputs(false); // hide any previous airdrop outputs
passLoadingStatus(true);
setAirdropCalcModalProgress(25); // updated progress bar to 25%
let latestBlock;
try {
latestBlock = await bchObj.Blockchain.getBlockCount();
} catch (err) {
errorNotification(
err,
'Error retrieving latest block height',
'bchObj.Blockchain.getBlockCount() error',
);
setIsAirdropCalcModalVisible(false);
passLoadingStatus(false);
return;
}
setAirdropCalcModalProgress(50);
etokenList.Config.SetUrl(currency.tokenDbUrl);
let airdropList;
try {
airdropList = await etokenList.List.GetAddressListFor(
formData.tokenId,
latestBlock,
true,
);
} catch (err) {
errorNotification(
err,
'Error retrieving airdrop recipients',
'etokenList.List.GetAddressListFor() error',
);
setIsAirdropCalcModalVisible(false);
passLoadingStatus(false);
return;
}
// if Ignore Own Address option is checked, then filter out from recipients list
if (ignoreOwnAddress) {
const ownEtokenAddress = convertToEcashPrefix(
wallet.Path1899.slpAddress,
);
airdropList.delete(ownEtokenAddress);
}
// if Ignore eToken Minter option is checked, then filter out from recipients list
if (ignoreMintAddress) {
// extract the eToken mint address
let genesisTx;
try {
genesisTx = await bchObj.RawTransactions.getRawTransaction(
formData.tokenId,
true,
);
} catch (err) {
errorNotification(
null,
'Unable to retrieve minting address for eToken ID: ' +
formData.tokenId,
'getRawTransaction Error',
);
setIsAirdropCalcModalVisible(false);
passLoadingStatus(false);
return;
}
const mintEcashAddress = convertToEcashPrefix(
genesisTx.vout[1].scriptPubKey.addresses[0],
); //vout[0] is always the OP_RETURN output
const mintEtokenAddress =
convertEcashtoEtokenAddr(mintEcashAddress);
// remove the mint address from the recipients list
airdropList.delete(mintEtokenAddress);
}
// filter out addresses from the exclusion list if the option is checked
if (ignoreCustomAddresses && ignoreCustomAddressesListIsValid) {
const addressStringArray = ignoreCustomAddressesList.split(',');
for (let i = 0; i < addressStringArray.length; i++) {
airdropList.delete(
convertEcashtoEtokenAddr(addressStringArray[i]),
);
}
}
// if the minimum etoken balance option is enabled
if (ignoreMinEtokenBalance) {
const minEligibleBalance = ignoreMinEtokenBalanceAmount;
// initial filtering of recipients with less than minimum eToken balance
for (let [key, value] of airdropList) {
if (new BigNumber(value).isLessThan(minEligibleBalance)) {
airdropList.delete(key);
}
}
}
if (!airdropList) {
errorNotification(
null,
'No recipients found for tokenId ' + formData.tokenId,
'Airdrop Calculation Error',
);
setIsAirdropCalcModalVisible(false);
passLoadingStatus(false);
return;
}
// if the ignore minimum payment threshold option is enabled
if (ignoreRecipientsBelowDust) {
// minimum airdrop threshold
- const minEligibleAirdrop = new BigNumber(
- fromSatoshisToXec(currency.dustSats),
- );
-
+ const minEligibleAirdrop = fromSatoshisToXec(currency.dustSats);
// first calculation on expected pro rata airdrops
let initialTotalTokenAmongstRecipients = new BigNumber(0);
let initialTotalHolders = new BigNumber(airdropList.size); // amount of addresses that hold this eToken
setEtokenHolders(initialTotalHolders);
// keep a cumulative total of each eToken holding in each address in airdropList
airdropList.forEach(
index =>
(initialTotalTokenAmongstRecipients =
initialTotalTokenAmongstRecipients.plus(
new BigNumber(index),
)),
);
let initialCircToAirdropRatio = new BigNumber(
formData.totalAirdrop,
).div(initialTotalTokenAmongstRecipients);
// initial filtering of recipients with less than minimum payout amount
for (let [key, value] of airdropList) {
const proRataAirdrop = new BigNumber(value).multipliedBy(
initialCircToAirdropRatio,
);
if (proRataAirdrop.isLessThan(minEligibleAirdrop)) {
airdropList.delete(key);
}
}
// if the list becomes empty after initial filtering
if (!airdropList) {
errorNotification(
null,
'No recipients after filtering minimum payouts',
'Airdrop Calculation Error',
);
setIsAirdropCalcModalVisible(false);
passLoadingStatus(false);
return;
}
}
setAirdropCalcModalProgress(75);
let totalTokenAmongstRecipients = new BigNumber(0);
let totalHolders = parseInt(airdropList.size); // amount of addresses that hold this eToken
setEtokenHolders(totalHolders);
// keep a cumulative total of each eToken holding in each address in airdropList
airdropList.forEach(
index =>
(totalTokenAmongstRecipients = totalTokenAmongstRecipients.plus(
new BigNumber(index),
)),
);
let circToAirdropRatio = new BigNumber(formData.totalAirdrop).div(
totalTokenAmongstRecipients,
);
let resultString = '';
airdropList.forEach(
(element, index) =>
(resultString +=
convertEtokenToEcashAddr(index) +
',' +
new BigNumber(element)
.multipliedBy(circToAirdropRatio)
.decimalPlaces(currency.cashDecimals) +
'\n'),
);
resultString = resultString.substring(0, resultString.length - 1); // remove the final newline
setAirdropRecipients(resultString);
setAirdropCalcModalProgress(100);
if (!resultString) {
errorNotification(
null,
'No holders found for eToken ID: ' + formData.tokenId,
'Airdrop Calculation Error',
);
setIsAirdropCalcModalVisible(false);
passLoadingStatus(false);
return;
}
// validate the airdrop values for each recipient
// Note: addresses are not validated as they are retrieved directly from onchain
setAirdropOutputIsValid(isValidAirdropOutputsArray(resultString));
setShowAirdropOutputs(true); // display the airdrop outputs TextArea
setIsAirdropCalcModalVisible(false);
passLoadingStatus(false);
};
const handleIgnoreMinEtokenBalanceAmt = e => {
setIgnoreMinEtokenBalance(e);
};
const handleAirdropCalcModalCancel = () => {
setIsAirdropCalcModalVisible(false);
passLoadingStatus(false);
};
const handleIgnoreOwnAddress = e => {
setIgnoreOwnAddress(e);
};
const handleIgnoreRecipientBelowDust = e => {
setIgnoreRecipientsBelowDust(e);
};
const handleIgnoreMintAddress = e => {
setIgnoreMintAddress(e);
};
const handleIgnoreCustomAddresses = e => {
setIgnoreCustomAddresses(e);
};
const handleIgnoreCustomAddressesList = e => {
// if the checkbox is not checked then skip the input validation
if (!ignoreCustomAddresses) {
return;
}
let customAddressList = e.target.value;
// remove all whitespaces via regex
customAddressList = customAddressList.replace(/ /g, '');
// validate the exclusion list input
const addressListIsValid =
isValidAirdropExclusionArray(customAddressList);
setIgnoreCustomAddressesListIsValid(addressListIsValid);
if (!addressListIsValid) {
setIgnoreCustomAddressListError(
'Invalid address detected in ignore list',
);
} else {
setIgnoreCustomAddressListError(false); // needs to be explicitly set in order to refresh the error state from prior invalidation
}
// commit the ignore list to state
setIgnoreCustomAddressesList(customAddressList);
};
let airdropCalcInputIsValid = tokenIdIsValid && totalAirdropIsValid;
// if the ignore min etoken balance and exclusion list options are in use, add the relevant validation to the total pre-calculation validation
if (ignoreMinEtokenBalance && ignoreCustomAddresses) {
// both enabled
airdropCalcInputIsValid =
ignoreMinEtokenBalanceAmountIsValid &&
tokenIdIsValid &&
totalAirdropIsValid &&
ignoreCustomAddressesListIsValid;
} else if (ignoreMinEtokenBalance && !ignoreCustomAddresses) {
// ignore minimum etoken balance option only
airdropCalcInputIsValid =
ignoreMinEtokenBalanceAmountIsValid &&
tokenIdIsValid &&
totalAirdropIsValid;
} else if (!ignoreMinEtokenBalance && ignoreCustomAddresses) {
// ignore custom addresses only
airdropCalcInputIsValid =
tokenIdIsValid &&
totalAirdropIsValid &&
ignoreCustomAddressesListIsValid;
}
return (
<>
<WalletInfoCtn>
<WalletLabel name={wallet.name}></WalletLabel>
{!balances.totalBalance ? (
<ZeroBalanceHeader>
You currently have 0 {currency.ticker}
<br />
Deposit some funds to use this feature
</ZeroBalanceHeader>
) : (
<>
<BalanceHeader
balance={balances.totalBalance}
ticker={currency.ticker}
/>
{fiatPrice !== null && (
<BalanceHeaderFiat
balance={balances.totalBalance}
settings={cashtabSettings}
fiatPrice={fiatPrice}
/>
)}
</>
)}
</WalletInfoCtn>
<StyledModal
title="Querying the eCash blockchain"
visible={isAirdropCalcModalVisible}
okButtonProps={{ style: { display: 'none' } }}
onCancel={handleAirdropCalcModalCancel}
>
<Spin indicator={CustomSpinner} />
<Progress percent={airdropCalcModalProgress} />
</StyledModal>
<br />
<SidePaddingCtn>
<Row type="flex">
<Col span={24}>
<CustomCollapseCtn
panelHeader="XEC Airdrop Calculator"
optionalDefaultActiveKey={
location &&
location.state &&
location.state.airdropEtokenId
? ['1']
: ['0']
}
optionalKey="1"
>
<Alert
message={`Please ensure the qualifying eToken transactions to airdrop recipients have at least one confirmation. The airdrop calculator will not detect unconfirmed token balances.`}
type="warning"
/>
<br />
<AntdFormWrapper>
<Form
style={{
width: 'auto',
}}
>
<Form.Item
validateStatus={
tokenIdIsValid === null ||
tokenIdIsValid
? ''
: 'error'
}
help={
tokenIdIsValid === null ||
tokenIdIsValid
? ''
: 'Invalid eToken ID'
}
>
<Input
addonBefore="eToken ID"
placeholder="Enter the eToken ID"
name="tokenId"
value={formData.tokenId}
onChange={e =>
handleTokenIdInput(e)
}
/>
</Form.Item>
<Form.Item
validateStatus={
totalAirdropIsValid === null ||
totalAirdropIsValid
? ''
: 'error'
}
help={
totalAirdropIsValid === null ||
totalAirdropIsValid
? ''
: 'Invalid total XEC airdrop'
}
>
<Input
addonBefore="Total XEC airdrop"
placeholder="Enter the total XEC airdrop"
name="totalAirdrop"
type="number"
value={formData.totalAirdrop}
onChange={e =>
handleTotalAirdropInput(e)
}
/>
</Form.Item>
<Form.Item>
<AirdropOptions>
<Switch
onChange={() =>
handleIgnoreOwnAddress(
prev => !prev,
)
}
defaultunchecked="true"
checked={ignoreOwnAddress}
/>
&ensp;Ignore my own address
</AirdropOptions>
</Form.Item>
<Form.Item>
<AirdropOptions>
<Switch
onChange={() =>
handleIgnoreRecipientBelowDust(
prev => !prev,
)
}
defaultunchecked="true"
checked={
ignoreRecipientsBelowDust
}
/>
&ensp;Ignore airdrops below min.
payment (
{fromSatoshisToXec(
currency.dustSats,
- )}{' '}
+ ).toString()}{' '}
XEC)
</AirdropOptions>
</Form.Item>
<Form.Item>
<AirdropOptions>
<Switch
onChange={() =>
handleIgnoreMintAddress(
prev => !prev,
)
}
defaultunchecked="true"
checked={ignoreMintAddress}
/>
&ensp;Ignore eToken minter address
</AirdropOptions>
</Form.Item>
<Form.Item>
<AirdropOptions>
<Switch
onChange={() =>
handleIgnoreMinEtokenBalanceAmt(
prev => !prev,
)
}
defaultunchecked="true"
checked={ignoreMinEtokenBalance}
style={{
marginBottom: '5px',
}}
/>
&ensp;Minimum eToken holder balance
{ignoreMinEtokenBalance && (
<InputAmountSingle
validateStatus={
ignoreMinEtokenBalanceAmountError
? 'error'
: ''
}
help={
ignoreMinEtokenBalanceAmountError
? ignoreMinEtokenBalanceAmountError
: ''
}
inputProps={{
placeholder:
'Minimum eToken balance',
onChange: e =>
handleMinEtokenBalanceChange(
e,
),
value: ignoreMinEtokenBalanceAmount,
}}
/>
)}
</AirdropOptions>
</Form.Item>
<Form.Item>
<AirdropOptions>
<Switch
onChange={() =>
handleIgnoreCustomAddresses(
prev => !prev,
)
}
defaultunchecked="true"
checked={ignoreCustomAddresses}
style={{
marginBottom: '5px',
}}
/>
&ensp;Ignore custom addresses
{ignoreCustomAddresses && (
<DestinationAddressMulti
validateStatus={
ignoreCustomAddressListError
? 'error'
: ''
}
help={
ignoreCustomAddressListError
? ignoreCustomAddressListError
: ''
}
inputProps={{
placeholder: `If more than one XEC address, separate them by comma \ne.g. \necash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8,ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed`,
name: 'address',
onChange: e =>
handleIgnoreCustomAddressesList(
e,
),
required:
ignoreCustomAddresses,
disabled:
!ignoreCustomAddresses,
}}
/>
)}
</AirdropOptions>
</Form.Item>
<Form.Item>
<SmartButton
onClick={() =>
calculateXecAirdrop()
}
disabled={
!airdropCalcInputIsValid ||
!tokenIdIsValid
}
>
Calculate Airdrop
</SmartButton>
</Form.Item>
{showAirdropOutputs && (
<>
{!ignoreRecipientsBelowDust &&
!airdropOutputIsValid &&
etokenHolders > 0 && (
<>
<Alert
description={
'At least one airdrop is below the minimum ' +
fromSatoshisToXec(
currency.dustSats,
- ) +
+ ).toString() +
' XEC dust. Please increase the total XEC airdrop.'
}
type="error"
showIcon
/>
<br />
</>
)}
<Form.Item>
One to Many Airdrop Payment
Outputs
<TextArea
name="airdropRecipients"
placeholder="Please input parameters above."
value={airdropRecipients}
rows="10"
readOnly
/>
</Form.Item>
<Form.Item>
<AirdropActions>
<Link
type="text"
to={{
pathname: `/send`,
state: {
airdropRecipients:
airdropRecipients,
airdropTokenId:
formData.tokenId,
},
}}
disabled={
!airdropRecipients
}
>
Copy to Send screen
</Link>
&nbsp;&nbsp;
<CopyToClipboard
data={airdropRecipients}
optionalOnCopyNotification={{
title: 'Copied',
msg: 'Airdrop recipients copied to clipboard',
}}
>
<Link
type="text"
disabled={
!airdropRecipients
}
to={'#'}
>
Copy to Clipboard
</Link>
</CopyToClipboard>
</AirdropActions>
</Form.Item>
</>
)}
</Form>
</AntdFormWrapper>
</CustomCollapseCtn>
</Col>
</Row>
</SidePaddingCtn>
</>
);
};
/*
passLoadingStatus must receive a default prop that is a function
in order to pass the rendering unit test in Airdrop.test.js
status => {console.log(status)} is an arbitrary stub function
*/
Airdrop.defaultProps = {
passLoadingStatus: status => {
console.log(status);
},
};
Airdrop.propTypes = {
jestBCH: PropTypes.object,
passLoadingStatus: PropTypes.func,
};
export default Airdrop;
diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js
index 245f14820..e63f6c3ec 100644
--- a/web/cashtab/src/components/Send/Send.js
+++ b/web/cashtab/src/components/Send/Send.js
@@ -1,1333 +1,1333 @@
import React, { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import PropTypes from 'prop-types';
import { WalletContext } from 'utils/context';
import {
AntdFormWrapper,
SendBchInput,
DestinationAddressSingle,
DestinationAddressMulti,
DestinationAddressSingleWithoutQRScan,
} from 'components/Common/EnhancedInputs';
import { CustomCollapseCtn } from 'components/Common/StyledCollapse';
import { Form, message, Modal, Alert, Input } from 'antd';
import { Row, Col, Switch } from 'antd';
import PrimaryButton, {
DisabledButton,
SmartButton,
} from 'components/Common/PrimaryButton';
import useBCH from 'hooks/useBCH';
import useWindowDimensions from 'hooks/useWindowDimensions';
import {
sendXecNotification,
errorNotification,
messageSignedNotification,
generalNotification,
} from 'components/Common/Notifications';
import { isMobile, isIOS, isSafari } from 'react-device-detect';
import { currency, parseAddressForParams } from 'components/Common/Ticker.js';
import CopyToClipboard from 'components/Common/CopyToClipboard';
import { Event } from 'utils/GoogleAnalytics';
import {
fiatToCrypto,
shouldRejectAmountInput,
isValidXecAddress,
isValidEtokenAddress,
isValidXecSendAmount,
} from 'utils/validation';
import BalanceHeader from 'components/Common/BalanceHeader';
import BalanceHeaderFiat from 'components/Common/BalanceHeaderFiat';
import {
ZeroBalanceHeader,
ConvertAmount,
AlertMsg,
WalletInfoCtn,
SidePaddingCtn,
FormLabel,
} from 'components/Common/Atoms';
import {
getWalletState,
convertToEcashPrefix,
toLegacyCash,
toLegacyCashArray,
fromSatoshisToXec,
} from 'utils/cashMethods';
import ApiError from 'components/Common/ApiError';
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 WalletLabel from 'components/Common/WalletLabel.js';
import { ThemedCopySolid } from 'components/Common/CustomIcons';
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`
text-align: left;
color: ${props => props.theme.forms.text};
padding-left: 1px;
`;
const AmountPreviewCtn = styled.div`
margin-top: -30px;
`;
const SendInputCtn = styled.div`
.ant-form-item-with-help {
margin-bottom: 32px;
}
`;
const LocaleFormattedValue = styled.h3`
color: ${props => props.theme.contrast};
font-weight: bold;
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
const SendBCH = ({ jestBCH, passLoadingStatus }) => {
// 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
// If the wallet object from ContextValue has a `state key`, then check which keys are in the wallet object
// Else set it as blank
const ContextValue = React.useContext(WalletContext);
const location = useLocation();
const { wallet, fiatPrice, apiError, cashtabSettings } = ContextValue;
const walletState = getWalletState(wallet);
const { balances, slpBalancesAndUtxos } = walletState;
// Modal settings
const [showConfirmMsgToSign, setShowConfirmMsgToSign] = useState(false);
const [msgToSign, setMsgToSign] = useState('');
const [signMessageIsValid, setSignMessageIsValid] = useState(null);
const [isOneToManyXECSend, setIsOneToManyXECSend] = useState(false);
const [opReturnMsg, setOpReturnMsg] = useState(false);
const [isEncryptedOptionalOpReturnMsg, setIsEncryptedOptionalOpReturnMsg] =
useState(false);
const [bchObj, setBchObj] = useState(false);
// Get device window width
// If this is less than 769, the page will open with QR scanner open
const { width } = useWindowDimensions();
// Load with QR code open if device is mobile and NOT iOS + anything but safari
const scannerSupported =
cashtabSettings &&
cashtabSettings.autoCameraOn === true &&
width < 769 &&
isMobile &&
!(isIOS && !isSafari);
const [formData, setFormData] = useState({
value: '',
address: '',
airdropTokenId: '',
});
const [queryStringText, setQueryStringText] = useState(null);
const [sendBchAddressError, setSendBchAddressError] = useState(false);
const [sendBchAmountError, setSendBchAmountError] = useState(false);
const [selectedCurrency, setSelectedCurrency] = useState(currency.ticker);
// Support cashtab button from web pages
const [txInfoFromUrl, setTxInfoFromUrl] = useState(false);
// Show a confirmation modal on transactions created by populating form from web page button
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 userLocale = navigator.language;
const clearInputForms = () => {
setFormData({
value: '',
address: '',
});
setOpReturnMsg(''); // OP_RETURN message has its own state field
};
const checkForConfirmationBeforeSendXec = () => {
if (txInfoFromUrl) {
setIsModalVisible(true);
} else if (cashtabSettings.sendModal) {
setIsModalVisible(cashtabSettings.sendModal);
} else {
// if the user does not have the send confirmation enabled in settings then send directly
send();
}
};
const handleOk = () => {
setIsModalVisible(false);
send();
};
const handleCancel = () => {
setIsModalVisible(false);
};
const { getBCH, getRestUrl, sendXec, calcFee, signPkMessage } = useBCH();
// If the balance has changed, unlock the UI
// This is redundant, if backend has refreshed in 1.75s timeout below, UI will already be unlocked
useEffect(() => {
passLoadingStatus(false);
}, [balances.totalBalance]);
useEffect(() => {
// jestBCH is only ever specified for unit tests, otherwise app will use getBCH();
const BCH = jestBCH ? jestBCH : getBCH();
// set the BCH instance to state, for other functions to reference
setBchObj(BCH);
// Manually parse for txInfo object on page load when Send.js is loaded with a query string
// if this was routed from Wallet screen's Reply to message link then prepopulate the address and value field
if (location && location.state && location.state.replyAddress) {
setFormData({
address: location.state.replyAddress,
- value: `${fromSatoshisToXec(currency.dustSats)}`,
+ value: `${fromSatoshisToXec(currency.dustSats).toString()}`,
});
}
// if this was routed from the Contact List
if (location && location.state && location.state.contactSend) {
setFormData({
address: location.state.contactSend,
});
}
// if this was routed from the Airdrop screen's Airdrop Calculator then
// switch to multiple recipient mode and prepopulate the recipients field
if (
location &&
location.state &&
location.state.airdropRecipients &&
location.state.airdropTokenId
) {
setIsOneToManyXECSend(true);
setFormData({
address: location.state.airdropRecipients,
airdropTokenId: location.state.airdropTokenId,
});
// validate the airdrop outputs from the calculator
handleMultiAddressChange({
target: {
value: location.state.airdropRecipients,
},
});
setAirdropFlag(true);
}
// Do not set txInfo in state if query strings are not present
if (
!window.location ||
!window.location.hash ||
window.location.hash === '#/send'
) {
return;
}
const txInfoArr = window.location.hash.split('?')[1].split('&');
// Iterate over this to create object
const txInfo = {};
for (let i = 0; i < txInfoArr.length; i += 1) {
let txInfoKeyValue = txInfoArr[i].split('=');
let key = txInfoKeyValue[0];
let value = txInfoKeyValue[1];
txInfo[key] = value;
}
console.log(`txInfo from page params`, txInfo);
setTxInfoFromUrl(txInfo);
populateFormsFromUrl(txInfo);
}, []);
function populateFormsFromUrl(txInfo) {
if (txInfo && txInfo.address && txInfo.value) {
setFormData({
address: txInfo.address,
value: txInfo.value,
});
}
}
function handleSendXecError(errorObj, oneToManyFlag) {
// Set loading to false here as well, as balance may not change depending on where error occured in try loop
passLoadingStatus(false);
let message;
if (!errorObj.error && !errorObj.message) {
message = `Transaction failed: no response from ${getRestUrl()}.`;
} else if (
/Could not communicate with full node or other external service/.test(
errorObj.error,
)
) {
message = 'Could not communicate with API. Please try again.';
} else if (
errorObj.error &&
errorObj.error.includes(
'too-long-mempool-chain, too many unconfirmed ancestors [limit: 50] (code 64)',
)
) {
message = `The ${currency.ticker} you are trying to send has too many unconfirmed ancestors to send (limit 50). Sending will be possible after a block confirmation. Try again in about 10 minutes.`;
} else {
message =
errorObj.message || errorObj.error || JSON.stringify(errorObj);
}
if (oneToManyFlag) {
errorNotification(errorObj, message, 'Sending XEC one to many');
} else {
errorNotification(errorObj, message, 'Sending XEC');
}
}
async function send() {
setFormData({
...formData,
});
if (isOneToManyXECSend) {
// this is a one to many XEC send transactions
// ensure multi-recipient input is not blank
if (!formData.address) {
return;
}
// Event("Category", "Action", "Label")
// Track number of XEC send-to-many transactions
Event('Send.js', 'SendToMany', selectedCurrency);
passLoadingStatus(true);
const { address } = formData;
//convert each line from TextArea input
let addressAndValueArray = address.split('\n');
try {
// construct array of XEC->BCH addresses due to bch-api constraint
let cleanAddressAndValueArray =
toLegacyCashArray(addressAndValueArray);
const link = await sendXec(
bchObj,
wallet,
slpBalancesAndUtxos.nonSlpUtxos,
currency.defaultFee,
opReturnMsg,
true, // indicate send mode is one to many
cleanAddressAndValueArray,
null,
null,
false, // one to many tx msg can't be encrypted
airdropFlag,
formData.airdropTokenId,
);
sendXecNotification(link);
clearInputForms();
setAirdropFlag(false);
} catch (e) {
handleSendXecError(e, isOneToManyXECSend);
}
} else {
// standard one to one XEC send transaction
if (
!formData.address ||
!formData.value ||
Number(formData.value) <= 0
) {
return;
}
// Event("Category", "Action", "Label")
// Track number of BCHA send transactions and whether users
// are sending BCHA or USD
Event('Send.js', 'Send', selectedCurrency);
passLoadingStatus(true);
const { address, value } = formData;
// Get the param-free address
let cleanAddress = address.split('?')[0];
// Ensure address has bitcoincash: prefix and checksum
cleanAddress = toLegacyCash(cleanAddress);
// Calculate the amount in BCH
let bchValue = value;
if (selectedCurrency !== 'XEC') {
bchValue = fiatToCrypto(value, fiatPrice);
}
// encrypted message limit truncation
let optionalOpReturnMsg;
if (isEncryptedOptionalOpReturnMsg) {
optionalOpReturnMsg = opReturnMsg.substring(
0,
currency.opReturn.encryptedMsgCharLimit,
);
} else {
optionalOpReturnMsg = opReturnMsg;
}
try {
const link = await sendXec(
bchObj,
wallet,
slpBalancesAndUtxos.nonSlpUtxos,
currency.defaultFee,
optionalOpReturnMsg,
false, // sendToMany boolean flag
null, // address array not applicable for one to many tx
cleanAddress,
bchValue,
isEncryptedOptionalOpReturnMsg,
);
sendXecNotification(link);
clearInputForms();
} catch (e) {
handleSendXecError(e, isOneToManyXECSend);
}
}
}
const handleAddressChange = e => {
const { value, name } = e.target;
let error = false;
let addressString = value;
// parse address for parameters
const addressInfo = parseAddressForParams(addressString);
// validate address
const isValid = isValidXecAddress(addressInfo.address);
/*
Model
addressInfo =
{
address: '',
queryString: '',
amount: null,
};
*/
const { address, queryString, amount } = addressInfo;
// If query string,
// Show an alert that only amount and currency.ticker are supported
setQueryStringText(queryString);
// 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 ${currency.ticker} sends`;
}
}
setSendBchAddressError(error);
// Set amount if it's in the query string
if (amount !== null) {
// Set currency to BCHA
setSelectedCurrency(currency.ticker);
// Use this object to mimic user input and get validation for the value
let amountObj = {
target: {
name: 'value',
value: amount,
},
};
handleBchAmountChange(amountObj);
setFormData({
...formData,
value: amount,
});
}
// Set address field to user input
setFormData(p => ({
...p,
[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 { value, name } = e.target;
let error;
if (!value) {
error = 'Input must not be blank';
setSendBchAddressError(error);
return setFormData(p => ({
...p,
[name]: value,
}));
}
//convert each line from the <TextArea> input into array
let addressStringArray = value.split('\n');
const arrayLength = addressStringArray.length;
// loop through each row in the <TextArea> input
for (let i = 0; i < arrayLength; i++) {
if (addressStringArray[i].trim() === '') {
// if this line is a line break or bunch of spaces
error = 'Empty spaces and rows must be removed';
setSendBchAddressError(error);
return setFormData(p => ({
...p,
[name]: value,
}));
}
let addressString = addressStringArray[i].split(',')[0];
let valueString = addressStringArray[i].split(',')[1];
const validAddress = isValidXecAddress(addressString);
const validValueString = isValidXecSendAmount(valueString);
if (!validAddress) {
error = `Invalid XEC address: ${addressString}, ${valueString}`;
setSendBchAddressError(error);
return setFormData(p => ({
...p,
[name]: value,
}));
}
if (!validValueString) {
error = `Amount must be at least ${fromSatoshisToXec(
currency.dustSats,
- )} XEC: ${addressString}, ${valueString}`;
+ ).toString()} XEC: ${addressString}, ${valueString}`;
setSendBchAddressError(error);
return setFormData(p => ({
...p,
[name]: value,
}));
}
}
// If iterate to end of array with no errors, then there is no error msg
setSendBchAddressError(false);
// Set address field to user input
setFormData(p => ({
...p,
[name]: value,
}));
};
const handleSelectedCurrencyChange = e => {
setSelectedCurrency(e);
// Clear input field to prevent accidentally sending 1 BCH instead of 1 USD
setFormData(p => ({
...p,
value: '',
}));
};
const handleBchAmountChange = e => {
const { value, name } = e.target;
let bchValue = value;
const error = shouldRejectAmountInput(
bchValue,
selectedCurrency,
fiatPrice,
balances.totalBalance,
);
setSendBchAmountError(error);
setFormData(p => ({
...p,
[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 signPkMessage(
bchObj,
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 () => {
// Clear amt error
setSendBchAmountError(false);
// Set currency to BCH
setSelectedCurrency(currency.ticker);
try {
const txFeeSats = calcFee(bchObj, slpBalancesAndUtxos.nonSlpUtxos);
const txFeeBch = txFeeSats / 10 ** currency.cashDecimals;
let value =
balances.totalBalance - txFeeBch >= 0
? (balances.totalBalance - txFeeBch).toFixed(
currency.cashDecimals,
)
: 0;
setFormData({
...formData,
value,
});
} catch (err) {
console.log(`Error in onMax:`);
console.log(err);
message.error(
'Unable to calculate the max value due to network errors',
);
}
};
// Display price in USD below input field for send amount, if it can be calculated
let fiatPriceString = '';
if (fiatPrice !== null && !isNaN(formData.value)) {
if (selectedCurrency === currency.ticker) {
// calculate conversion to fiatPrice
fiatPriceString = `${(fiatPrice * Number(formData.value)).toFixed(
2,
)}`;
// formats to fiat locale style
fiatPriceString = formatFiatBalance(
Number(fiatPriceString),
userLocale,
);
// insert symbol and currency before/after the locale formatted fiat balance
fiatPriceString = `${
cashtabSettings
? `${
currency.fiatCurrencies[cashtabSettings.fiatCurrency]
.symbol
} `
: '$ '
} ${fiatPriceString} ${
cashtabSettings && cashtabSettings.fiatCurrency
? cashtabSettings.fiatCurrency.toUpperCase()
: 'USD'
}`;
} else {
fiatPriceString = `${
formData.value
? formatFiatBalance(
Number(fiatToCrypto(formData.value, fiatPrice)),
userLocale,
)
: formatFiatBalance(0, userLocale)
} ${currency.ticker}`;
}
}
const priceApiError = fiatPrice === null && selectedCurrency !== 'XEC';
return (
<>
<Modal
title="Confirm Send"
visible={isModalVisible}
onOk={handleOk}
onCancel={handleCancel}
>
<p>
{isOneToManyXECSend
? `are you sure you want to send the following One to Many transaction?
${formData.address}`
: `Are you sure you want to send ${formData.value}${' '}
${selectedCurrency} to ${formData.address}?`}
</p>
</Modal>
<WalletInfoCtn>
<WalletLabel name={wallet.name}></WalletLabel>
{!balances.totalBalance ? (
<ZeroBalanceHeader>
You currently have 0 {currency.ticker}
<br />
Deposit some funds to use this feature
</ZeroBalanceHeader>
) : (
<>
<BalanceHeader
balance={balances.totalBalance}
ticker={currency.ticker}
/>
<BalanceHeaderFiat
balance={balances.totalBalance}
settings={cashtabSettings}
fiatPrice={fiatPrice}
/>
</>
)}
</WalletInfoCtn>
<SidePaddingCtn>
<Row type="flex">
<Col span={24}>
<Form
style={{
width: 'auto',
marginTop: '40px',
}}
>
{!isOneToManyXECSend ? (
<SendInputCtn>
<FormLabel>Send to</FormLabel>
<DestinationAddressSingle
style={{ marginBottom: '0px' }}
loadWithCameraOpen={
location &&
location.state &&
location.state.replyAddress
? false
: scannerSupported
}
validateStatus={
sendBchAddressError ? 'error' : ''
}
help={
sendBchAddressError
? sendBchAddressError
: ''
}
onScan={result =>
handleAddressChange({
target: {
name: 'address',
value: result,
},
})
}
inputProps={{
placeholder: `${currency.ticker} Address`,
name: 'address',
onChange: e =>
handleAddressChange(e),
required: true,
value: formData.address,
}}
></DestinationAddressSingle>
<FormLabel>Amount</FormLabel>
<SendBchInput
activeFiatCode={
cashtabSettings &&
cashtabSettings.fiatCurrency
? cashtabSettings.fiatCurrency.toUpperCase()
: 'USD'
}
validateStatus={
sendBchAmountError ? 'error' : ''
}
help={
sendBchAmountError
? sendBchAmountError
: ''
}
onMax={onMax}
inputProps={{
name: 'value',
dollar:
selectedCurrency === 'USD'
? 1
: 0,
placeholder: 'Amount',
onChange: e =>
handleBchAmountChange(e),
required: true,
value: formData.value,
disabled: priceApiError,
}}
selectProps={{
value: selectedCurrency,
disabled: queryStringText !== null,
onChange: e =>
handleSelectedCurrencyChange(e),
}}
></SendBchInput>
{priceApiError && (
<AlertMsg>
Error fetching fiat price. Setting
send by{' '}
{currency.fiatCurrencies[
cashtabSettings.fiatCurrency
].slug.toUpperCase()}{' '}
disabled
</AlertMsg>
)}
</SendInputCtn>
) : (
<>
<FormLabel>Send to</FormLabel>
<DestinationAddressMulti
validateStatus={
sendBchAddressError ? 'error' : ''
}
help={
sendBchAddressError
? sendBchAddressError
: ''
}
inputProps={{
placeholder: `One XEC address & value per line, separated by comma \ne.g. \necash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8,500 \necash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,700`,
name: 'address',
onChange: e =>
handleMultiAddressChange(e),
required: true,
value: formData.address,
}}
></DestinationAddressMulti>
</>
)}
{!priceApiError && !isOneToManyXECSend && (
<AmountPreviewCtn>
<LocaleFormattedValue>
{formatBalance(
formData.value,
userLocale,
)}{' '}
{selectedCurrency}
</LocaleFormattedValue>
<ConvertAmount>
{fiatPriceString !== '' && '='}{' '}
{fiatPriceString}
</ConvertAmount>
</AmountPreviewCtn>
)}
{queryStringText && (
<Alert
message={`You are sending a transaction to an address including query parameters "${queryStringText}." Only the "amount" parameter, in units of ${currency.ticker} satoshis, is currently supported.`}
type="warning"
/>
)}
<div
style={{
paddingTop: '12px',
}}
>
{!balances.totalBalance ||
apiError ||
sendBchAmountError ||
sendBchAddressError ||
priceApiError ? (
<DisabledButton>Send</DisabledButton>
) : (
<>
{txInfoFromUrl ? (
<PrimaryButton
onClick={() =>
checkForConfirmationBeforeSendXec()
}
>
Send
</PrimaryButton>
) : (
<PrimaryButton
onClick={() => {
checkForConfirmationBeforeSendXec();
}}
>
Send
</PrimaryButton>
)}
</>
)}
</div>
<CustomCollapseCtn
panelHeader="Advanced"
optionalDefaultActiveKey={
location &&
location.state &&
location.state.replyAddress
? ['1']
: ['0']
}
optionalKey="1"
>
<AntdFormWrapper
style={{
marginBottom: '20px',
}}
>
<TextAreaLabel>
Multiple Recipients:&nbsp;&nbsp;
<Switch
defaultunchecked="true"
checked={isOneToManyXECSend}
onChange={() => {
setIsOneToManyXECSend(
!isOneToManyXECSend,
);
setIsEncryptedOptionalOpReturnMsg(
false,
);
}}
style={{
marginBottom: '7px',
}}
/>
</TextAreaLabel>
<TextAreaLabel>
Message:&nbsp;&nbsp;
<Switch
disabled={isOneToManyXECSend}
style={{
marginBottom: '7px',
}}
checkedChildren="Private"
unCheckedChildren="Public"
defaultunchecked="true"
checked={
isEncryptedOptionalOpReturnMsg
}
onChange={() => {
setIsEncryptedOptionalOpReturnMsg(
prev => !prev,
);
setIsOneToManyXECSend(false);
}}
/>
</TextAreaLabel>
{isEncryptedOptionalOpReturnMsg ? (
<Alert
style={{
marginBottom: '10px',
}}
description="Please note encrypted messages can only be sent to wallets with at least 1 outgoing transaction."
type="warning"
showIcon
/>
) : (
<Alert
style={{
marginBottom: '10px',
}}
description="Please note this message will be public."
type="warning"
showIcon
/>
)}
<TextArea
name="opReturnMsg"
placeholder={
isEncryptedOptionalOpReturnMsg
? `(max ${currency.opReturn.encryptedMsgCharLimit} characters)`
: `(max ${currency.opReturn.unencryptedMsgCharLimit} characters)`
}
value={
opReturnMsg
? isEncryptedOptionalOpReturnMsg
? opReturnMsg.substring(
0,
currency.opReturn
.encryptedMsgCharLimit +
1,
)
: opReturnMsg
: ''
}
onChange={e =>
setOpReturnMsg(e.target.value)
}
showCount
maxLength={
isEncryptedOptionalOpReturnMsg
? currency.opReturn
.encryptedMsgCharLimit
: currency.opReturn
.unencryptedMsgCharLimit
}
onKeyDown={e =>
e.keyCode == 13
? e.preventDefault()
: ''
}
/>
</AntdFormWrapper>
</CustomCollapseCtn>
{apiError && <ApiError />}
</Form>
</Col>
</Row>
<Modal
title={`Please review and confirm your message to be signed using this wallet.`}
visible={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 />
&nbsp;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.`}
visible={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 />
&nbsp;Verify Message
</SmartButton>
</Form>
</AntdFormWrapper>
</CustomCollapseCtn>
</SidePaddingCtn>
</>
);
};
/*
passLoadingStatus must receive a default prop that is a function
in order to pass the rendering unit test in Send.test.js
status => {console.log(status)} is an arbitrary stub function
*/
SendBCH.defaultProps = {
passLoadingStatus: status => {
console.log(status);
},
};
SendBCH.propTypes = {
jestBCH: PropTypes.object,
passLoadingStatus: PropTypes.func,
};
export default SendBCH;
diff --git a/web/cashtab/src/hooks/__tests__/migrations.test.js b/web/cashtab/src/hooks/__tests__/migrations.test.js
index bacf7d807..18515b0a7 100644
--- a/web/cashtab/src/hooks/__tests__/migrations.test.js
+++ b/web/cashtab/src/hooks/__tests__/migrations.test.js
@@ -1,135 +1,131 @@
import { currency } from '../../components/Common/Ticker';
import BigNumber from 'bignumber.js';
import BCHJS from '@psf/bch-js';
import { fromSatoshisToXec, fromXecToSatoshis } from 'utils/cashMethods';
describe('Testing functions for upgrading Cashtab', () => {
it('Replacement currency.dustSats parameter parsing matches legacy DUST parameter', () => {
- expect(
- parseFloat(
- new BigNumber(
- fromSatoshisToXec(currency.dustSats, 8).toString(),
- ).toFixed(8),
- ),
- ).toBe(0.0000055);
+ expect(fromSatoshisToXec(currency.dustSats, 8).toNumber()).toBe(
+ 0.0000055,
+ );
});
it('Replicate 8-decimal return value from instance of toSatoshi in TransactionBuilder with fromXecToSatoshis', () => {
const BCH = new BCHJS();
const testSendAmount = '0.12345678';
expect(
parseInt(fromXecToSatoshis(new BigNumber(testSendAmount), 8)),
).toBe(BCH.BitcoinCash.toSatoshi(Number(testSendAmount).toFixed(8)));
});
it('Replicate 2-decimal return value from instance of toSatoshi in TransactionBuilder with fromXecToSatoshis', () => {
const BCH = new BCHJS();
const testSendAmount = '0.12';
expect(
parseInt(fromXecToSatoshis(new BigNumber(testSendAmount), 8)),
).toBe(BCH.BitcoinCash.toSatoshi(Number(testSendAmount).toFixed(8)));
});
it('Replicate 8-decimal return value from instance of toSatoshi in remainder comparison with fromXecToSatoshis', () => {
const BCH = new BCHJS();
expect(
parseFloat(fromXecToSatoshis(new BigNumber('0.00000546'), 8)),
).toBe(
BCH.BitcoinCash.toSatoshi(
parseFloat(new BigNumber('0.00000546').toFixed(8)),
),
);
});
it('fromXecToSatoshis() returns false if input is not a BigNumber', () => {
const testInput = 132.12345678;
expect(fromXecToSatoshis(testInput)).toBe(false);
});
it(`fromXecToSatoshis() returns false if input is a BigNumber with more decimals than specified by cashDecimals parameter`, () => {
const testInput = new BigNumber('132.123456789');
expect(fromXecToSatoshis(testInput, 8)).toBe(false);
});
it(`fromXecToSatoshis() returns expected value if input is a BigNumber with 8 decimal places`, () => {
const testInput = new BigNumber('100.12345678');
expect(fromXecToSatoshis(testInput, 8)).toStrictEqual(
new BigNumber('10012345678'),
);
});
it(`fromXecToSatoshis() returns expected value if input is a BigNumber with 2 decimal places`, () => {
const testInput = new BigNumber('100.12');
expect(fromXecToSatoshis(testInput, 2)).toStrictEqual(
new BigNumber('10012'),
);
});
it(`fromXecToSatoshis() returns expected value if input is a BigNumber with 1 decimal place`, () => {
const testInput = new BigNumber('100.1');
expect(fromXecToSatoshis(testInput, 8)).toStrictEqual(
new BigNumber('10010000000'),
);
});
it('fromXecToSatoshis() returns exact result as toSatoshi but in BigNumber format', () => {
const BCH = new BCHJS();
const testAmount = new BigNumber('0.12345678');
// Match legacy implementation, inputting a BigNumber converted to a string by .toFixed(8)
const testAmountInSatoshis = BCH.BitcoinCash.toSatoshi(
testAmount.toFixed(8),
);
const testAmountInCashDecimals = fromXecToSatoshis(testAmount, 8);
expect(testAmountInSatoshis).toStrictEqual(12345678);
expect(testAmountInCashDecimals).toStrictEqual(
new BigNumber(testAmountInSatoshis),
);
});
it(`BigNumber version of remainder variable is equivalent to Math.floor version`, () => {
// Test case for sending 0.12345678 BCHA
let satoshisToSendTest = fromXecToSatoshis(
new BigNumber('0.12345678'),
8,
);
// Assume total BCHA available in utxos is 500 sats higher than 0.123456578 BCHA
let originalAmountTest = satoshisToSendTest.plus(500);
// Assume 229 byte tx fee
let txFeeTest = 229;
expect(
Math.floor(
originalAmountTest.minus(satoshisToSendTest).minus(txFeeTest),
),
).toStrictEqual(
parseInt(
originalAmountTest.minus(satoshisToSendTest).minus(txFeeTest),
),
);
});
it(`Using parseInt on a BigNumber returns output type required for Transaction Builder`, () => {
const remainder = new BigNumber('12345678');
expect(parseInt(remainder)).toStrictEqual(12345678);
});
it('Replicates return value from instance of toBitcoinCash with fromSatoshisToXec and cashDecimals = 8', () => {
const BCH = new BCHJS();
const testSendAmount = '12345678';
- expect(fromSatoshisToXec(testSendAmount, 8)).toBe(
+ expect(fromSatoshisToXec(testSendAmount, 8).toNumber()).toBe(
BCH.BitcoinCash.toBitcoinCash(testSendAmount),
);
});
it('Replicates largest possible digits return value from instance of toBitcoinCash with fromSatoshisToXec and cashDecimals = 8', () => {
const BCH = new BCHJS();
const testSendAmount = '1000000012345678';
- expect(fromSatoshisToXec(testSendAmount, 8)).toBe(
+ expect(fromSatoshisToXec(testSendAmount, 8).toNumber()).toBe(
BCH.BitcoinCash.toBitcoinCash(testSendAmount),
);
});
it('Replicates smallest unit value return value from instance of toBitcoinCash with fromSatoshisToXec and cashDecimals = 8', () => {
const BCH = new BCHJS();
const testSendAmount = '1';
- expect(fromSatoshisToXec(testSendAmount, 8)).toBe(
+ expect(fromSatoshisToXec(testSendAmount, 8).toNumber()).toBe(
BCH.BitcoinCash.toBitcoinCash(testSendAmount),
);
});
it(`Converts dust limit in satoshis to dust limit in current app setting`, () => {
expect(fromSatoshisToXec(currency.dustSats, 8).toString()).toBe(
'0.0000055',
);
});
});
diff --git a/web/cashtab/src/utils/__tests__/cashMethods.test.js b/web/cashtab/src/utils/__tests__/cashMethods.test.js
index b542a7050..4be5104bb 100644
--- a/web/cashtab/src/utils/__tests__/cashMethods.test.js
+++ b/web/cashtab/src/utils/__tests__/cashMethods.test.js
@@ -1,1721 +1,1725 @@
import BigNumber from 'bignumber.js';
import {
fromSatoshisToXec,
batchArray,
flattenContactList,
flattenBatchedHydratedUtxos,
loadStoredWallet,
isValidStoredWallet,
fromLegacyDecimals,
convertToEcashPrefix,
checkNullUtxosForTokenStatus,
confirmNonEtokenUtxos,
isLegacyMigrationRequired,
toLegacyCash,
toLegacyToken,
toLegacyCashArray,
convertEtokenToEcashAddr,
parseOpReturn,
isExcludedUtxo,
whichUtxosWereAdded,
whichUtxosWereConsumed,
addNewHydratedUtxos,
removeConsumedUtxos,
getUtxoCount,
areAllUtxosIncludedInIncrementallyHydratedUtxos,
convertEcashtoEtokenAddr,
getHashArrayFromWallet,
parseChronikTx,
checkWalletForTokenInfo,
isActiveWebsocket,
parseXecSendValue,
getChangeAddressFromInputUtxos,
generateOpReturnScript,
generateTxInput,
generateTxOutput,
signAndBuildTx,
fromXecToSatoshis,
getWalletBalanceFromUtxos,
} from 'utils/cashMethods';
import { currency } from 'components/Common/Ticker';
import {
unbatchedArray,
arrayBatchedByThree,
} from '../__mocks__/mockBatchedArrays';
import {
validAddressArrayInput,
validAddressArrayInputMixedPrefixes,
validAddressArrayOutput,
validLargeAddressArrayInput,
validLargeAddressArrayOutput,
invalidAddressArrayInput,
} from '../__mocks__/mockAddressArray';
import {
unflattenedHydrateUtxosResponse,
flattenedHydrateUtxosResponse,
} from '../__mocks__/flattenBatchedHydratedUtxosMocks';
import {
cachedUtxos,
utxosLoadedFromCache,
} from '../__mocks__/mockCachedUtxos';
import {
validStoredWallet,
invalidStoredWallet,
} from '../__mocks__/mockStoredWallets';
import {
mockTxDataResults,
mockNonEtokenUtxos,
mockTxDataResultsWithEtoken,
mockHydratedUtxosWithNullValues,
mockHydratedUtxosWithNullValuesSetToFalse,
} from '../__mocks__/nullUtxoMocks';
import {
missingPath1899Wallet,
missingPublicKeyInPath1899Wallet,
missingPublicKeyInPath145Wallet,
missingPublicKeyInPath245Wallet,
notLegacyWallet,
missingHash160,
} from '../__mocks__/mockLegacyWalletsUtils';
import {
shortCashtabMessageInputHex,
longCashtabMessageInputHex,
shortExternalMessageInputHex,
longExternalMessageInputHex,
shortSegmentedExternalMessageInputHex,
longSegmentedExternalMessageInputHex,
mixedSegmentedExternalMessageInputHex,
mockParsedShortCashtabMessageArray,
mockParsedLongCashtabMessageArray,
mockParsedShortExternalMessageArray,
mockParsedLongExternalMessageArray,
mockParsedShortSegmentedExternalMessageArray,
mockParsedLongSegmentedExternalMessageArray,
mockParsedMixedSegmentedExternalMessageArray,
eTokenInputHex,
mockParsedETokenOutputArray,
mockAirdropHexOutput,
mockParsedAirdropMessageArray,
} from '../__mocks__/mockOpReturnParsedArray';
import {
excludedUtxoAlpha,
excludedUtxoBeta,
includedUtxoAlpha,
includedUtxoBeta,
previousUtxosObjUtxoArray,
previousUtxosTemplate,
currentUtxosAfterSingleXecReceiveTxTemplate,
utxosAddedBySingleXecReceiveTxTemplate,
previousUtxosBeforeSingleXecReceiveTx,
currentUtxosAfterSingleXecReceiveTx,
utxosAddedBySingleXecReceiveTx,
currentUtxosAfterMultiXecReceiveTxTemplate,
utxosAddedByMultiXecReceiveTxTemplate,
previousUtxosBeforeMultiXecReceiveTx,
currentUtxosAfterMultiXecReceiveTx,
utxosAddedByMultiXecReceiveTx,
currentUtxosAfterEtokenReceiveTxTemplate,
utxosAddedByEtokenReceiveTxTemplate,
previousUtxosBeforeEtokenReceiveTx,
currentUtxosAfterEtokenReceiveTx,
utxosAddedByEtokenReceiveTx,
previousUtxosBeforeSendAllTxTemplate,
currentUtxosAfterSendAllTxTemplate,
previousUtxosBeforeSendAllTx,
currentUtxosAfterSendAllTx,
previousUtxosBeforeSingleXecSendTx,
currentUtxosAfterSingleXecSendTx,
utxosAddedBySingleXecSendTx,
currentUtxosAfterSingleXecSendTxTemplate,
utxosAddedBySingleXecSendTxTemplate,
currentUtxosAfterEtokenSendTxTemplate,
utxosAddedByEtokenSendTxTemplate,
previousUtxosBeforeEtokenSendTx,
currentUtxosAfterEtokenSendTx,
utxosAddedByEtokenSendTx,
utxosConsumedByEtokenSendTx,
utxosConsumedByEtokenSendTxTemplate,
utxosConsumedBySingleXecSendTx,
utxosConsumedBySingleXecSendTxTemplate,
utxosConsumedBySendAllTx,
utxosConsumedBySendAllTxTemplate,
hydratedUtxoDetailsBeforeAddingTemplate,
hydratedUtxoDetailsAfterAddingSingleUtxoTemplate,
newHydratedUtxosSingleTemplate,
addedHydratedUtxosOverTwenty,
existingHydratedUtxoDetails,
existingHydratedUtxoDetailsAfterAdd,
hydratedUtxoDetailsBeforeConsumedTemplate,
consumedUtxoTemplate,
hydratedUtxoDetailsAfterRemovingConsumedUtxoTemplate,
consumedUtxos,
hydratedUtxoDetailsBeforeRemovingConsumedUtxos,
hydratedUtxoDetailsAfterRemovingConsumedUtxos,
consumedUtxosMoreThanTwenty,
hydratedUtxoDetailsAfterRemovingMoreThanTwentyConsumedUtxos,
consumedUtxosMoreThanTwentyInRandomObjects,
utxoCountMultiTemplate,
utxoCountSingleTemplate,
incrementalUtxosTemplate,
incrementallyHydratedUtxosTemplate,
incrementallyHydratedUtxosTemplateMissing,
utxosAfterSentTxIncremental,
incrementallyHydratedUtxosAfterProcessing,
incrementallyHydratedUtxosAfterProcessingOneMissing,
} from '../__mocks__/incrementalUtxoMocks';
import mockLegacyWallets from 'hooks/__mocks__/mockLegacyWallets';
import BCHJS from '@psf/bch-js';
import sendBCHMock from '../../hooks/__mocks__/sendBCH';
import {
lambdaHash160s,
lambdaIncomingXecTx,
lambdaOutgoingXecTx,
lambdaIncomingEtokenTx,
lambdaOutgoingEtokenTx,
activeWebsocketAlpha,
disconnectedWebsocketAlpha,
unsubscribedWebsocket,
} from '../__mocks__/chronikWs';
import mockReturnGetSlpBalancesAndUtxos from '../../hooks/__mocks__/mockReturnGetSlpBalancesAndUtxos';
import {
mockOneToOneSendXecTxBuilderObj,
mockOneToManySendXecTxBuilderObj,
} from '../__mocks__/mockTxBuilderObj';
import {
mockSingleInputUtxo,
mockMultipleInputUtxos,
mockSingleOutput,
mockMultipleOutputs,
} from '../__mocks__/mockTxBuilderData';
it(`getChangeAddressFromInputUtxos() returns a correct change address from a valid inputUtxo`, () => {
const BCH = new BCHJS();
const { wallet } = sendBCHMock;
const inputUtxo = [
{
height: 669639,
tx_hash:
'0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58',
tx_pos: 0,
value: 1000,
txid: '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58',
vout: 0,
isValid: false,
address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl',
wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH',
},
];
const changeAddress = getChangeAddressFromInputUtxos(
BCH,
inputUtxo,
wallet,
);
expect(changeAddress).toStrictEqual(inputUtxo[0].address);
});
it(`getChangeAddressFromInputUtxos() throws error upon a malformed input utxo`, () => {
const BCH = new BCHJS();
const { wallet } = sendBCHMock;
const invalidInputUtxo = [
{
height: 669639,
tx_hash:
'0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58',
tx_pos: 0,
value: 1000,
txid: '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58',
vout: 0,
isValid: false,
wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH',
},
];
let thrownError;
try {
getChangeAddressFromInputUtxos(BCH, invalidInputUtxo, wallet);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid input utxo');
});
it(`getChangeAddressFromInputUtxos() throws error upon a valid input utxo with invalid address param`, () => {
const BCH = new BCHJS();
const { wallet } = sendBCHMock;
const invalidInputUtxo = [
{
height: 669639,
tx_hash:
'0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58',
tx_pos: 0,
value: 1000,
address: 'bitcoincash:1qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', // invalid cash address
txid: '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58',
vout: 0,
isValid: false,
wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH',
},
];
let thrownError;
try {
getChangeAddressFromInputUtxos(BCH, invalidInputUtxo, wallet);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid input utxo');
});
it(`getChangeAddressFromInputUtxos() throws an error upon a null inputUtxos param`, () => {
const BCH = new BCHJS();
const { wallet } = sendBCHMock;
const inputUtxo = null;
let thrownError;
try {
getChangeAddressFromInputUtxos(BCH, inputUtxo, wallet);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual(
'Invalid getChangeAddressFromWallet input parameter',
);
});
it(`parseXecSendValue() correctly parses the value for a valid one to one send XEC transaction`, () => {
expect(parseXecSendValue(false, '550', null)).toStrictEqual(
new BigNumber(550),
);
});
it(`parseXecSendValue() correctly parses the value for a valid one to many send XEC transaction`, () => {
const destinationAddressAndValueArray = [
'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,6',
'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,6',
'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,6',
];
expect(
parseXecSendValue(true, null, destinationAddressAndValueArray),
).toStrictEqual(new BigNumber(18));
});
it(`parseXecSendValue() correctly throws error when singleSendValue is invalid for a one to one send XEC transaction`, () => {
let errorThrown;
try {
parseXecSendValue(false, null, 550);
} catch (err) {
errorThrown = err;
}
expect(errorThrown.message).toStrictEqual('Invalid singleSendValue');
});
it(`parseXecSendValue() correctly throws error when destinationAddressAndValueArray is invalid for a one to many send XEC transaction`, () => {
let errorThrown;
try {
parseXecSendValue(true, null, null);
} catch (err) {
errorThrown = err;
}
expect(errorThrown.message).toStrictEqual(
'Invalid destinationAddressAndValueArray',
);
});
it(`parseXecSendValue() correctly throws error when the total value for a one to one send XEC transaction is below dust`, () => {
let errorThrown;
try {
parseXecSendValue(false, '4.5', null);
} catch (err) {
errorThrown = err;
}
expect(errorThrown.message).toStrictEqual('dust');
});
it(`parseXecSendValue() correctly throws error when the total value for a one to many send XEC transaction is below dust`, () => {
const destinationAddressAndValueArray = [
'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,2',
'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,2',
'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,1',
];
let errorThrown;
try {
parseXecSendValue(true, null, destinationAddressAndValueArray);
} catch (err) {
errorThrown = err;
}
expect(errorThrown.message).toStrictEqual('dust');
});
it('generateOpReturnScript() correctly generates an encrypted message script', () => {
const BCH = new BCHJS();
const optionalOpReturnMsg = 'testing generateOpReturnScript()';
const encryptionFlag = true;
const airdropFlag = false;
const airdropTokenId = null;
const mockEncryptedEj =
'04688f9907fe3c7c0b78a73c4ab4f75e15e7e2b79641add519617086126fe6f6b1405a14eed48e90c9c8c0fc77f0f36984a78173e76ce51f0a44af94b59e9da703c9ff82758cfdb9cc46437d662423400fb731d3bfc1df0599279356ca261213fbb40d398c041e1bac966afed1b404581ab1bcfcde1fa039d53b7c7b70e8edf26d64bea9fbeed24cc80909796e6af5863707fa021f2a2ebaa2fe894904702be19d';
const encodedScript = generateOpReturnScript(
BCH,
optionalOpReturnMsg,
encryptionFlag,
airdropFlag,
airdropTokenId,
mockEncryptedEj,
);
expect(encodedScript.toString('hex')).toBe(
'6a04657461624d420130343638386639393037666533633763306237386137336334616234663735653135653765326237393634316164643531393631373038363132366665366636623134303561313465656434386539306339633863306663373766306633363938346137383137336537366365353166306134346166393462353965396461373033633966663832373538636664623963633436343337643636323432333430306662373331643362666331646630353939323739333536636132363132313366626234306433393863303431653162616339363661666564316234303435383161623162636663646531666130333964353362376337623730653865646632366436346265613966626565643234636338303930393739366536616635383633373037666130323166326132656261613266653839343930343730326265313964',
);
});
it('generateOpReturnScript() correctly generates an un-encrypted non-airdrop message script', () => {
const BCH = new BCHJS();
const optionalOpReturnMsg = 'testing generateOpReturnScript()';
const encryptionFlag = false;
const airdropFlag = false;
const encodedScript = generateOpReturnScript(
BCH,
optionalOpReturnMsg,
encryptionFlag,
airdropFlag,
);
expect(encodedScript.toString('hex')).toBe(
'6a04007461622074657374696e672067656e65726174654f7052657475726e5363726970742829',
);
});
it('generateOpReturnScript() correctly generates an un-encrypted airdrop message script', () => {
const BCH = new BCHJS();
const optionalOpReturnMsg = 'testing generateOpReturnScript()';
const encryptionFlag = false;
const airdropFlag = true;
const airdropTokenId =
'1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e';
const encodedScript = generateOpReturnScript(
BCH,
optionalOpReturnMsg,
encryptionFlag,
airdropFlag,
airdropTokenId,
);
expect(encodedScript.toString('hex')).toBe(
'6a0464726f70201c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e04007461622074657374696e672067656e65726174654f7052657475726e5363726970742829',
);
});
it('generateOpReturnScript() correctly generates an un-encrypted airdrop with no message script', () => {
const BCH = new BCHJS();
const optionalOpReturnMsg = null;
const encryptionFlag = false;
const airdropFlag = true;
const airdropTokenId =
'1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e';
const encodedScript = generateOpReturnScript(
BCH,
optionalOpReturnMsg,
encryptionFlag,
airdropFlag,
airdropTokenId,
);
expect(encodedScript.toString('hex')).toBe(
'6a0464726f70201c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e0400746162',
);
});
it('generateOpReturnScript() correctly throws an error on an invalid encryption input', () => {
const BCH = new BCHJS();
const optionalOpReturnMsg = null;
const encryptionFlag = true;
const airdropFlag = false;
const airdropTokenId = null;
const mockEncryptedEj = null; // invalid given encryptionFlag is true
let thrownError;
try {
generateOpReturnScript(
BCH,
optionalOpReturnMsg,
encryptionFlag,
airdropFlag,
airdropTokenId,
mockEncryptedEj,
);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid OP RETURN script input');
});
it('generateOpReturnScript() correctly throws an error on an invalid airdrop input', () => {
const BCH = new BCHJS();
const optionalOpReturnMsg = null;
const encryptionFlag = false;
const airdropFlag = true;
const airdropTokenId = null; // invalid given airdropFlag is true
let thrownError;
try {
generateOpReturnScript(
BCH,
optionalOpReturnMsg,
encryptionFlag,
airdropFlag,
airdropTokenId,
);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid OP RETURN script input');
});
it(`generateTxInput() returns an input object for a valid one to one XEC tx`, async () => {
const BCH = new BCHJS();
const isOneToMany = false;
const utxos = mockReturnGetSlpBalancesAndUtxos.nonSlpUtxos;
let txBuilder = new BCH.TransactionBuilder();
const destinationAddressAndValueArray = null;
const satoshisToSend = new BigNumber(2184);
const feeInSatsPerByte = currency.defaultFee;
const inputObj = generateTxInput(
BCH,
isOneToMany,
utxos,
txBuilder,
destinationAddressAndValueArray,
satoshisToSend,
feeInSatsPerByte,
);
expect(inputObj.txBuilder).not.toStrictEqual(null);
expect(inputObj.totalInputUtxoValue).toStrictEqual(new BigNumber(701000));
expect(inputObj.txFee).toStrictEqual(752);
expect(inputObj.inputUtxos.length).not.toStrictEqual(0);
});
it(`generateTxInput() returns an input object for a valid one to many XEC tx`, async () => {
const BCH = new BCHJS();
const isOneToMany = true;
const utxos = mockReturnGetSlpBalancesAndUtxos.nonSlpUtxos;
let txBuilder = new BCH.TransactionBuilder();
const destinationAddressAndValueArray = [
'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,3000',
'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,3000',
'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,3000',
];
const satoshisToSend = new BigNumber(900000);
const feeInSatsPerByte = currency.defaultFee;
const inputObj = generateTxInput(
BCH,
isOneToMany,
utxos,
txBuilder,
destinationAddressAndValueArray,
satoshisToSend,
feeInSatsPerByte,
);
expect(inputObj.txBuilder).not.toStrictEqual(null);
expect(inputObj.totalInputUtxoValue).toStrictEqual(new BigNumber(1401000));
expect(inputObj.txFee).toStrictEqual(1186);
expect(inputObj.inputUtxos.length).not.toStrictEqual(0);
});
it(`generateTxInput() throws error for a one to many XEC tx with invalid destinationAddressAndValueArray input`, async () => {
const BCH = new BCHJS();
const isOneToMany = true;
const utxos = mockReturnGetSlpBalancesAndUtxos.nonSlpUtxos;
let txBuilder = new BCH.TransactionBuilder();
const destinationAddressAndValueArray = null; // invalid since isOneToMany is true
const satoshisToSend = new BigNumber(900000);
const feeInSatsPerByte = currency.defaultFee;
let thrownError;
try {
generateTxInput(
BCH,
isOneToMany,
utxos,
txBuilder,
destinationAddressAndValueArray,
satoshisToSend,
feeInSatsPerByte,
);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid tx input parameter');
});
it(`generateTxInput() throws error for a one to many XEC tx with invalid utxos input`, async () => {
const BCH = new BCHJS();
const isOneToMany = true;
const utxos = null;
let txBuilder = new BCH.TransactionBuilder();
const destinationAddressAndValueArray = [
'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,3000',
'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,3000',
'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,3000',
];
const satoshisToSend = new BigNumber(900000);
const feeInSatsPerByte = currency.defaultFee;
let thrownError;
try {
generateTxInput(
BCH,
isOneToMany,
utxos,
txBuilder,
destinationAddressAndValueArray,
satoshisToSend,
feeInSatsPerByte,
);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid tx input parameter');
});
it(`generateTxOutput() returns a txBuilder instance for a valid one to one XEC tx`, () => {
// txbuilder output params
const BCH = new BCHJS();
const { destinationAddress, wallet } = sendBCHMock;
const isOneToMany = false;
- const singleSendValue = new BigNumber(
- fromSatoshisToXec(
- mockOneToOneSendXecTxBuilderObj.transaction.tx.outs[0].value,
- ),
+ const singleSendValue = fromSatoshisToXec(
+ mockOneToOneSendXecTxBuilderObj.transaction.tx.outs[0].value,
);
const totalInputUtxoValue =
mockOneToOneSendXecTxBuilderObj.transaction.inputs[0].value;
const satoshisToSend = fromXecToSatoshis(new BigNumber(singleSendValue));
// for unit test purposes, calculate fee by subtracting satoshisToSend from totalInputUtxoValue
// no change output to be subtracted in this tx
const txFee = new BigNumber(totalInputUtxoValue).minus(
new BigNumber(satoshisToSend),
);
const destinationAddressAndValueArray = null;
let txBuilder = new BCH.TransactionBuilder();
const changeAddress = wallet.Path1899.cashAddress;
const outputObj = generateTxOutput(
BCH,
isOneToMany,
singleSendValue,
satoshisToSend,
totalInputUtxoValue,
destinationAddress,
destinationAddressAndValueArray,
changeAddress,
txFee,
txBuilder,
);
expect(outputObj.toString()).toStrictEqual(
mockOneToOneSendXecTxBuilderObj.toString(),
);
});
it(`generateTxOutput() returns a txBuilder instance for a valid one to many XEC tx`, () => {
// txbuilder output params
const BCH = new BCHJS();
const { destinationAddress, wallet } = sendBCHMock;
const isOneToMany = true;
const singleSendValue = null;
const totalInputUtxoValue =
mockOneToManySendXecTxBuilderObj.transaction.inputs[0].value +
mockOneToManySendXecTxBuilderObj.transaction.inputs[1].value +
mockOneToManySendXecTxBuilderObj.transaction.inputs[2].value;
const satoshisToSend = new BigNumber(
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[0].value +
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[1].value +
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[2].value,
);
// for unit test purposes, calculate fee by subtracting satoshisToSend and change amount from totalInputUtxoValue
const txFee = new BigNumber(totalInputUtxoValue)
.minus(satoshisToSend)
.minus(
new BigNumber(
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[3].value,
),
); // change value
const destinationAddressAndValueArray = toLegacyCashArray(
validAddressArrayInput,
);
let txBuilder = new BCH.TransactionBuilder();
const changeAddress = wallet.Path1899.cashAddress;
const outputObj = generateTxOutput(
BCH,
isOneToMany,
singleSendValue,
satoshisToSend,
totalInputUtxoValue,
destinationAddress,
destinationAddressAndValueArray,
changeAddress,
txFee,
txBuilder,
);
expect(outputObj.toString()).toStrictEqual(
mockOneToManySendXecTxBuilderObj.toString(),
);
});
it(`generateTxOutput() throws an error on invalid input params for a one to one XEC tx`, () => {
// txbuilder output params
const BCH = new BCHJS();
const { wallet } = sendBCHMock;
const isOneToMany = false;
const singleSendValue = null; // invalid due to singleSendValue being mandatory when isOneToMany is false
const totalInputUtxoValue =
mockOneToOneSendXecTxBuilderObj.transaction.inputs[0].value;
const satoshisToSend = fromXecToSatoshis(new BigNumber(singleSendValue));
// for unit test purposes, calculate fee by subtracting satoshisToSend from totalInputUtxoValue
// no change output to be subtracted in this tx
const txFee = new BigNumber(totalInputUtxoValue).minus(satoshisToSend);
const destinationAddressAndValueArray = null;
let txBuilder = new BCH.TransactionBuilder();
const changeAddress = wallet.Path1899.cashAddress;
let thrownError;
try {
generateTxOutput(
BCH,
isOneToMany,
singleSendValue,
satoshisToSend,
totalInputUtxoValue,
null,
destinationAddressAndValueArray,
changeAddress,
txFee,
txBuilder,
);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid tx input parameter');
});
it(`generateTxOutput() throws an error on invalid input params for a one to many XEC tx`, () => {
// txbuilder output params
const BCH = new BCHJS();
const { wallet } = sendBCHMock;
const isOneToMany = true;
const singleSendValue = null;
const totalInputUtxoValue =
mockOneToManySendXecTxBuilderObj.transaction.inputs[0].value +
mockOneToManySendXecTxBuilderObj.transaction.inputs[1].value +
mockOneToManySendXecTxBuilderObj.transaction.inputs[2].value;
const satoshisToSend = new BigNumber(
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[0].value +
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[1].value +
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[2].value,
);
// for unit test purposes, calculate fee by subtracting satoshisToSend and change amount from totalInputUtxoValue
const txFee = new BigNumber(totalInputUtxoValue)
.minus(satoshisToSend)
.minus(
new BigNumber(
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[3].value,
),
); // change value
const destinationAddressAndValueArray = null; // invalid as this is mandatory when isOneToMany is true
let txBuilder = new BCH.TransactionBuilder();
const changeAddress = wallet.Path1899.cashAddress;
let thrownError;
try {
generateTxOutput(
BCH,
isOneToMany,
singleSendValue,
satoshisToSend,
totalInputUtxoValue,
null,
destinationAddressAndValueArray,
changeAddress,
txFee,
txBuilder,
);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid tx input parameter');
});
it(`signAndBuildTx() successfully returns a raw tx hex for a tx with a single input and a single output`, () => {
// txbuilder output params
const BCH = new BCHJS();
let txBuilder = new BCH.TransactionBuilder();
// add inputs to txBuilder
txBuilder.addInput(
mockSingleInputUtxo[0].txid,
mockSingleInputUtxo[0].vout,
);
// add outputs to txBuilder
const outputAddressAndValue = mockSingleOutput.split(',');
txBuilder.addOutput(
outputAddressAndValue[0], // address
parseInt(fromXecToSatoshis(new BigNumber(outputAddressAndValue[1]))), // value
);
const rawTxHex = signAndBuildTx(BCH, mockSingleInputUtxo, txBuilder);
expect(rawTxHex).toStrictEqual(
'0200000001582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006b483045022100f0064dc6ab95765ed22047aa952e509b9ce7d24e384c2c4f06b064e5cc4fc87a02207f00266cc8261c203e2832ae1e700b6035aebe3ef5ff18e9eabbdee6815b086041210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff0158020000000000001976a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac00000000',
);
});
it(`signAndBuildTx() successfully returns a raw tx hex for a tx with a single input and multiple outputs`, () => {
// txbuilder output params
const BCH = new BCHJS();
let txBuilder = new BCH.TransactionBuilder();
// add inputs to txBuilder
txBuilder.addInput(
mockSingleInputUtxo[0].txid,
mockSingleInputUtxo[0].vout,
);
// add outputs to txBuilder
for (let i = 0; i < mockMultipleOutputs.length; i++) {
const outputAddressAndValue = mockMultipleOutputs[i].split(',');
txBuilder.addOutput(
outputAddressAndValue[0], // address
parseInt(
fromXecToSatoshis(new BigNumber(outputAddressAndValue[1])),
), // value
);
}
const rawTxHex = signAndBuildTx(BCH, mockSingleInputUtxo, txBuilder);
expect(rawTxHex).toStrictEqual(
'0200000001582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006a47304402200aba1829f51c420d6c37c8e50021b8541d2d62590ad5c67eaeee535e959377390220272318cbdf8161c399a2592c3e86ede499276c1d2e3506300f58bee786f2de6541210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff0326020000000000001976a914f627e51001a51a1a92d8927808701373cf29267f88ac26020000000000001976a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac26020000000000001976a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac00000000',
);
});
it(`signAndBuildTx() successfully returns a raw tx hex for a tx with multiple inputs and a single output`, () => {
// txbuilder output params
const BCH = new BCHJS();
let txBuilder = new BCH.TransactionBuilder();
// add inputs to txBuilder
for (let i = 0; i < mockMultipleInputUtxos.length; i++) {
txBuilder.addInput(
mockMultipleInputUtxos[i].txid,
mockMultipleInputUtxos[i].vout,
);
}
// add outputs to txBuilder
const outputAddressAndValue = mockSingleOutput.split(',');
txBuilder.addOutput(
outputAddressAndValue[0], // address
parseInt(fromXecToSatoshis(new BigNumber(outputAddressAndValue[1]))), // value
);
const rawTxHex = signAndBuildTx(BCH, mockMultipleInputUtxos, txBuilder);
expect(rawTxHex).toStrictEqual(
'0200000003582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006a4730440220041f2c5674222f802d49783301dfbece1ac0f4cef6d0c3b8ad83a46b24481d5002200e977c1990a946e66e4cbdc2c057788e01a5d038ff9b281da95870d88c85da0c41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff7313e804af08113dfa290515390a8ec3ac01448118f2eb556ee168a96ee6acdd000000006b483045022100f9ac95c2febb445393f36ad51e221a77b2813ea8643c349d597ca19db71e829202200464567d95250c86665d3ad567f3ae1826f8ea3907cfb8fe7f36b9f72b18239c41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff960dd2f0c47e8a3cf1486b046d879f45a047da3b51aedfb5594138ac857214f1000000006a47304402207807c0bb717e1955f87db9110e91e1701c614b0cc5fa544e7cff1591a9ba45db022067f86ecac7c6d9ee8e00033fc0071f34833775f4410f72036cd958f50c60bfc141210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff0158020000000000001976a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac00000000',
);
});
it(`signAndBuildTx() successfully returns a raw tx hex for a tx with multiple inputs and multiple outputs`, () => {
// txbuilder output params
const BCH = new BCHJS();
let txBuilder = new BCH.TransactionBuilder();
// add inputs to txBuilder
for (let i = 0; i < mockMultipleInputUtxos.length; i++) {
txBuilder.addInput(
mockMultipleInputUtxos[i].txid,
mockMultipleInputUtxos[i].vout,
);
}
// add outputs to txBuilder
for (let i = 0; i < mockMultipleOutputs.length; i++) {
const outputAddressAndValue = mockMultipleOutputs[i].split(',');
txBuilder.addOutput(
outputAddressAndValue[0], // address
parseInt(
fromXecToSatoshis(new BigNumber(outputAddressAndValue[1])),
), // value
);
}
const rawTxHex = signAndBuildTx(BCH, mockMultipleInputUtxos, txBuilder);
expect(rawTxHex).toStrictEqual(
'0200000003582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006a47304402205cca1aacd87779f218d153222f56eefc14331803a588bae7c9b10f8b1df32e4502203579bb35e53b2e8562d7728bc3a205739f50e85076919bc16b56eec421b8d01b41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff7313e804af08113dfa290515390a8ec3ac01448118f2eb556ee168a96ee6acdd000000006b483045022100aafc986d0a45200c01ccb390db2961eb6c62c7a16e4d8a1df6725b3a3e4a98ec0220565afbea0c53b1d4a8073a54e4d167f5cc36c941c541897b20b9bb6ad5694fac41210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff960dd2f0c47e8a3cf1486b046d879f45a047da3b51aedfb5594138ac857214f1000000006a473044022065c92eddc1ea2a4aa057c6ee68b85018ee97787eb8921427a635c141c6fdd4bc022034e92f6c129cd66ab5522615e7f48965187a751690198f79154114ed974bb2d441210352cbc218d193ceaf4fb38a772856380173db7a908905e3190841b3174c7ae22dffffffff0326020000000000001976a914f627e51001a51a1a92d8927808701373cf29267f88ac26020000000000001976a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac26020000000000001976a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac00000000',
);
});
it(`signAndBuildTx() throws error on an empty inputUtxo param`, () => {
// txbuilder output params
const BCH = new BCHJS();
let txBuilder = new BCH.TransactionBuilder();
let thrownError;
try {
signAndBuildTx(BCH, [], txBuilder);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid buildTx parameter');
});
it(`signAndBuildTx() throws error on a null inputUtxo param`, () => {
// txbuilder output params
const BCH = new BCHJS();
let txBuilder = new BCH.TransactionBuilder();
const inputUtxo = null; // invalid input param
let thrownError;
try {
signAndBuildTx(BCH, inputUtxo, txBuilder);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid buildTx parameter');
});
describe('Correctly executes cash utility functions', () => {
it(`Correctly converts smallest base unit to smallest decimal for cashDecimals = 2`, () => {
- expect(fromSatoshisToXec(1, 2)).toBe(0.01);
+ expect(fromSatoshisToXec(1, 2)).toStrictEqual(new BigNumber(0.01));
});
it(`Correctly converts largest base unit to smallest decimal for cashDecimals = 2`, () => {
- expect(fromSatoshisToXec(1000000012345678, 2)).toBe(10000000123456.78);
+ expect(fromSatoshisToXec(1000000012345678, 2)).toStrictEqual(
+ new BigNumber(10000000123456.78),
+ );
});
it(`Correctly converts smallest base unit to smallest decimal for cashDecimals = 8`, () => {
- expect(fromSatoshisToXec(1, 8)).toBe(0.00000001);
+ expect(fromSatoshisToXec(1, 8)).toStrictEqual(
+ new BigNumber(0.00000001),
+ );
});
it(`Correctly converts largest base unit to smallest decimal for cashDecimals = 8`, () => {
- expect(fromSatoshisToXec(1000000012345678, 8)).toBe(10000000.12345678);
+ expect(fromSatoshisToXec(1000000012345678, 8)).toStrictEqual(
+ new BigNumber(10000000.12345678),
+ );
});
it(`Correctly converts an array of length 10 to an array of 4 arrays, each with max length 3`, () => {
expect(batchArray(unbatchedArray, 3)).toStrictEqual(
arrayBatchedByThree,
);
});
it(`If array length is less than batch size, return original array as first and only element of new array`, () => {
expect(batchArray(unbatchedArray, 20)).toStrictEqual([unbatchedArray]);
});
it(`Flattens hydrateUtxos from Promise.all() response into array that can be parsed by getSlpBalancesAndUtxos`, () => {
expect(
flattenBatchedHydratedUtxos(unflattenedHydrateUtxosResponse),
).toStrictEqual(flattenedHydrateUtxosResponse);
});
it(`Accepts a cachedWalletState that has not preserved BigNumber object types, and returns the same wallet state with BigNumber type re-instituted`, () => {
expect(loadStoredWallet(cachedUtxos)).toStrictEqual(
utxosLoadedFromCache,
);
});
it(`Correctly determines a wallet's balance from its set of non-eToken utxos (nonSlpUtxos)`, () => {
expect(
getWalletBalanceFromUtxos(
validStoredWallet.state.slpBalancesAndUtxos.nonSlpUtxos,
),
).toStrictEqual(validStoredWallet.state.balances);
});
it(`Correctly determines a wallet's zero balance from its empty set of non-eToken utxos (nonSlpUtxos)`, () => {
expect(
getWalletBalanceFromUtxos(
utxosLoadedFromCache.slpBalancesAndUtxos.nonSlpUtxos,
),
).toStrictEqual(utxosLoadedFromCache.balances);
});
it(`Recognizes a stored wallet as valid if it has all required fields`, () => {
expect(isValidStoredWallet(validStoredWallet)).toBe(true);
});
it(`Recognizes a stored wallet as invalid if it is missing required fields`, () => {
expect(isValidStoredWallet(invalidStoredWallet)).toBe(false);
});
it(`Converts a legacy BCH amount to an XEC amount`, () => {
expect(fromLegacyDecimals(0.00000546, 2)).toStrictEqual(5.46);
});
it(`Leaves a legacy BCH amount unchanged if currency.cashDecimals is 8`, () => {
expect(fromLegacyDecimals(0.00000546, 8)).toStrictEqual(0.00000546);
});
it(`convertToEcashPrefix converts a bitcoincash: prefixed address to an ecash: prefixed address`, () => {
expect(
convertToEcashPrefix(
'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr',
),
).toBe('ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035');
});
it(`convertToEcashPrefix returns an ecash: prefix address unchanged`, () => {
expect(
convertToEcashPrefix(
'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035',
),
).toBe('ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035');
});
it(`toLegacyToken returns an etoken: prefix address as simpleledger:`, () => {
expect(
toLegacyToken('etoken:qz2708636snqhsxu8wnlka78h6fdp77ar5tv2tzg4r'),
).toBe('simpleledger:qz2708636snqhsxu8wnlka78h6fdp77ar5syue64fa');
});
it(`toLegacyToken returns an prefixless valid etoken address in simpleledger: format with prefix`, () => {
expect(
toLegacyToken('qz2708636snqhsxu8wnlka78h6fdp77ar5tv2tzg4r'),
).toBe('simpleledger:qz2708636snqhsxu8wnlka78h6fdp77ar5syue64fa');
});
it(`Correctly parses utxo vout tx data to confirm the transactions are not eToken txs`, () => {
expect(checkNullUtxosForTokenStatus(mockTxDataResults)).toStrictEqual(
mockNonEtokenUtxos,
);
});
it(`Correctly parses utxo vout tx data and screens out an eToken by asm field`, () => {
expect(
checkNullUtxosForTokenStatus(mockTxDataResultsWithEtoken),
).toStrictEqual([]);
});
it(`Changes isValid from 'null' to 'false' for confirmed nonEtokenUtxos`, () => {
expect(
confirmNonEtokenUtxos(
mockHydratedUtxosWithNullValues,
mockNonEtokenUtxos,
),
).toStrictEqual(mockHydratedUtxosWithNullValuesSetToFalse);
});
it(`Recognizes a wallet with missing Path1889 is a Legacy Wallet and requires migration`, () => {
expect(isLegacyMigrationRequired(missingPath1899Wallet)).toBe(true);
});
it(`Recognizes a wallet with missing PublicKey in Path1889 is a Legacy Wallet and requires migration`, () => {
expect(
isLegacyMigrationRequired(missingPublicKeyInPath1899Wallet),
).toBe(true);
});
it(`Recognizes a wallet with missing PublicKey in Path145 is a Legacy Wallet and requires migration`, () => {
expect(isLegacyMigrationRequired(missingPublicKeyInPath145Wallet)).toBe(
true,
);
});
it(`Recognizes a wallet with missing PublicKey in Path245 is a Legacy Wallet and requires migration`, () => {
expect(isLegacyMigrationRequired(missingPublicKeyInPath245Wallet)).toBe(
true,
);
});
it(`Recognizes a wallet with missing Hash160 values is a Legacy Wallet and requires migration`, () => {
expect(isLegacyMigrationRequired(missingHash160)).toBe(true);
});
it(`Recognizes a latest, current wallet that does not require migration`, () => {
expect(isLegacyMigrationRequired(notLegacyWallet)).toBe(false);
});
test('toLegacyCash() converts a valid ecash: prefix address to a valid bitcoincash: prefix address', async () => {
const result = toLegacyCash(
'ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc',
);
expect(result).toStrictEqual(
'bitcoincash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gjykk3wa0',
);
});
test('toLegacyCash() converts a valid ecash: prefixless address to a valid bitcoincash: prefix address', async () => {
const result = toLegacyCash(
'qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc',
);
expect(result).toStrictEqual(
'bitcoincash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gjykk3wa0',
);
});
test('toLegacyCash throws error if input address has invalid checksum', async () => {
const result = toLegacyCash(
'ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25m',
);
expect(result).toStrictEqual(
new Error(
'ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25m is not a valid ecash address',
),
);
});
test('toLegacyCash() throws error with input of etoken: address', async () => {
const result = toLegacyCash(
'etoken:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4g9htlunl0',
);
expect(result).toStrictEqual(
new Error(
'etoken:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4g9htlunl0 is not a valid ecash address',
),
);
});
test('toLegacyCash() throws error with input of legacy address', async () => {
const result = toLegacyCash('13U6nDrkRsC3Eb1pxPhNY8XJ5W9zdp6rNk');
expect(result).toStrictEqual(
new Error(
'13U6nDrkRsC3Eb1pxPhNY8XJ5W9zdp6rNk is not a valid ecash address',
),
);
});
test('toLegacyCash() throws error with input of bitcoincash: address', async () => {
const result = toLegacyCash(
'bitcoincash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gjykk3wa0',
);
expect(result).toStrictEqual(
new Error(
'bitcoincash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gjykk3wa0 is not a valid ecash address',
),
);
});
test('toLegacyCashArray throws error if the addressArray input is null', async () => {
const result = toLegacyCashArray(null);
expect(result).toStrictEqual(new Error('Invalid addressArray input'));
});
test('toLegacyCashArray throws error if the addressArray input is empty', async () => {
const result = toLegacyCashArray([]);
expect(result).toStrictEqual(new Error('Invalid addressArray input'));
});
test('toLegacyCashArray throws error if the addressArray input is a number', async () => {
const result = toLegacyCashArray(12345);
expect(result).toStrictEqual(new Error('Invalid addressArray input'));
});
test('toLegacyCashArray throws error if the addressArray input is undefined', async () => {
const result = toLegacyCashArray(undefined);
expect(result).toStrictEqual(new Error('Invalid addressArray input'));
});
test('toLegacyCashArray successfully converts a standard sized valid addressArray input', async () => {
const result = toLegacyCashArray(validAddressArrayInput);
expect(result).toStrictEqual(validAddressArrayOutput);
});
test('toLegacyCashArray successfully converts a standard sized valid addressArray input including prefixless ecash addresses', async () => {
const result = toLegacyCashArray(validAddressArrayInputMixedPrefixes);
expect(result).toStrictEqual(validAddressArrayOutput);
});
test('toLegacyCashArray successfully converts a large valid addressArray input', async () => {
const result = toLegacyCashArray(validLargeAddressArrayInput);
expect(result).toStrictEqual(validLargeAddressArrayOutput);
});
test('toLegacyCashArray throws an error on an addressArray with invalid addresses', async () => {
const result = toLegacyCashArray(invalidAddressArrayInput);
expect(result).toStrictEqual(
new Error(
'ecash:qrqgwxrrrrrrrrrrrrrrrrrrrrrrrrr7zsvk is not a valid ecash address',
),
);
});
test('parseOpReturn() successfully parses a short cashtab message', async () => {
const result = parseOpReturn(shortCashtabMessageInputHex);
expect(result).toStrictEqual(mockParsedShortCashtabMessageArray);
});
test('parseOpReturn() successfully parses a long cashtab message where an additional PUSHDATA1 is present', async () => {
const result = parseOpReturn(longCashtabMessageInputHex);
expect(result).toStrictEqual(mockParsedLongCashtabMessageArray);
});
test('parseOpReturn() successfully parses a short external message', async () => {
const result = parseOpReturn(shortExternalMessageInputHex);
expect(result).toStrictEqual(mockParsedShortExternalMessageArray);
});
test('parseOpReturn() successfully parses a long external message where an additional PUSHDATA1 is present', async () => {
const result = parseOpReturn(longExternalMessageInputHex);
expect(result).toStrictEqual(mockParsedLongExternalMessageArray);
});
test('parseOpReturn() successfully parses an external message that is segmented into separate short parts', async () => {
const result = parseOpReturn(shortSegmentedExternalMessageInputHex);
expect(result).toStrictEqual(
mockParsedShortSegmentedExternalMessageArray,
);
});
test('parseOpReturn() successfully parses an external message that is segmented into separate long parts', async () => {
const result = parseOpReturn(longSegmentedExternalMessageInputHex);
expect(result).toStrictEqual(
mockParsedLongSegmentedExternalMessageArray,
);
});
test('parseOpReturn() successfully parses an external message that is segmented into separate long and short parts', async () => {
const result = parseOpReturn(mixedSegmentedExternalMessageInputHex);
expect(result).toStrictEqual(
mockParsedMixedSegmentedExternalMessageArray,
);
});
test('parseOpReturn() successfully parses an eToken output', async () => {
const result = parseOpReturn(eTokenInputHex);
expect(result).toStrictEqual(mockParsedETokenOutputArray);
});
test('parseOpReturn() successfully parses an airdrop transaction', async () => {
const result = parseOpReturn(mockAirdropHexOutput);
// verify the hex output is parsed correctly
expect(result).toStrictEqual(mockParsedAirdropMessageArray);
// verify airdrop hex prefix is contained in the array returned from parseOpReturn()
expect(
result.find(
element => element === currency.opReturn.appPrefixesHex.airdrop,
),
).toStrictEqual(currency.opReturn.appPrefixesHex.airdrop);
});
test('isExcludedUtxo returns true for a utxo with different tx_pos and same txid as an existing utxo in the set', async () => {
expect(
isExcludedUtxo(excludedUtxoAlpha, previousUtxosObjUtxoArray),
).toBe(true);
});
test('isExcludedUtxo returns true for a utxo with different value and same txid as an existing utxo in the set', async () => {
expect(
isExcludedUtxo(excludedUtxoBeta, previousUtxosObjUtxoArray),
).toBe(true);
});
test('isExcludedUtxo returns false for a utxo with different tx_pos and same txid', async () => {
expect(
isExcludedUtxo(includedUtxoAlpha, previousUtxosObjUtxoArray),
).toBe(false);
});
test('isExcludedUtxo returns false for a utxo with different value and same txid', async () => {
expect(
isExcludedUtxo(includedUtxoBeta, previousUtxosObjUtxoArray),
).toBe(false);
});
test('whichUtxosWereAdded correctly identifies a single added utxo after a received XEC tx [template]', async () => {
expect(
whichUtxosWereAdded(
previousUtxosTemplate,
currentUtxosAfterSingleXecReceiveTxTemplate,
),
).toStrictEqual(utxosAddedBySingleXecReceiveTxTemplate);
});
test('whichUtxosWereAdded correctly identifies a single added utxo after a received XEC tx', async () => {
expect(
whichUtxosWereAdded(
previousUtxosBeforeSingleXecReceiveTx,
currentUtxosAfterSingleXecReceiveTx,
),
).toStrictEqual(utxosAddedBySingleXecReceiveTx);
});
test('whichUtxosWereAdded correctly identifies multiple added utxos with the same txid [template]', async () => {
expect(
whichUtxosWereAdded(
previousUtxosTemplate,
currentUtxosAfterMultiXecReceiveTxTemplate,
),
).toStrictEqual(utxosAddedByMultiXecReceiveTxTemplate);
});
test('whichUtxosWereAdded correctly identifies multiple added utxos with the same txid', async () => {
expect(
whichUtxosWereAdded(
previousUtxosBeforeMultiXecReceiveTx,
currentUtxosAfterMultiXecReceiveTx,
),
).toStrictEqual(utxosAddedByMultiXecReceiveTx);
});
test('whichUtxosWereAdded correctly identifies an added utxos from received eToken tx [template]', async () => {
expect(
whichUtxosWereAdded(
previousUtxosTemplate,
currentUtxosAfterEtokenReceiveTxTemplate,
),
).toStrictEqual(utxosAddedByEtokenReceiveTxTemplate);
});
test('whichUtxosWereAdded correctly identifies an added utxos from received eToken tx', async () => {
expect(
whichUtxosWereAdded(
previousUtxosBeforeEtokenReceiveTx,
currentUtxosAfterEtokenReceiveTx,
),
).toStrictEqual(utxosAddedByEtokenReceiveTx);
});
test('whichUtxosWereAdded correctly identifies no utxos were added in a send all XEC tx (no change) [template]', async () => {
expect(
whichUtxosWereAdded(
previousUtxosBeforeSendAllTxTemplate,
currentUtxosAfterSendAllTxTemplate,
),
).toStrictEqual(false);
});
test('whichUtxosWereAdded correctly identifies no utxos were added in a send all XEC tx (no change)', async () => {
expect(
whichUtxosWereAdded(
previousUtxosBeforeSendAllTx,
currentUtxosAfterSendAllTx,
),
).toStrictEqual(false);
});
test('whichUtxosWereAdded correctly identifies an added utxo from a single send XEC tx', async () => {
expect(
whichUtxosWereAdded(
previousUtxosBeforeSingleXecSendTx,
currentUtxosAfterSingleXecSendTx,
),
).toStrictEqual(utxosAddedBySingleXecSendTx);
});
test('whichUtxosWereAdded correctly identifies an added utxo from a single send XEC tx [template]', async () => {
expect(
whichUtxosWereAdded(
previousUtxosTemplate,
currentUtxosAfterSingleXecSendTxTemplate,
),
).toStrictEqual(utxosAddedBySingleXecSendTxTemplate);
});
test('whichUtxosWereAdded correctly identifies added change utxos from a send eToken tx [template]', async () => {
expect(
whichUtxosWereAdded(
previousUtxosTemplate,
currentUtxosAfterEtokenSendTxTemplate,
),
).toStrictEqual(utxosAddedByEtokenSendTxTemplate);
});
test('whichUtxosWereAdded correctly identifies added change utxos from a send eToken tx', async () => {
expect(
whichUtxosWereAdded(
previousUtxosBeforeEtokenSendTx,
currentUtxosAfterEtokenSendTx,
),
).toStrictEqual(utxosAddedByEtokenSendTx);
});
test('whichUtxosWereConsumed correctly identifies no utxos consumed after a received XEC tx [template]', async () => {
expect(
whichUtxosWereConsumed(
previousUtxosTemplate,
currentUtxosAfterSingleXecReceiveTxTemplate,
),
).toStrictEqual(false);
});
test('whichUtxosWereConsumed correctly identifies no utxos consumed a received XEC tx', async () => {
expect(
whichUtxosWereConsumed(
previousUtxosBeforeSingleXecReceiveTx,
currentUtxosAfterSingleXecReceiveTx,
),
).toStrictEqual(false);
});
test('whichUtxosWereConsumed correctly identifies no consumed utxos after receiving an XEC multi-send tx [template]', async () => {
expect(
whichUtxosWereConsumed(
previousUtxosTemplate,
currentUtxosAfterMultiXecReceiveTxTemplate,
),
).toStrictEqual(false);
});
test('whichUtxosWereConsumed correctly identifies no consumed utxos after receiving an XEC multi-send tx', async () => {
expect(
whichUtxosWereConsumed(
previousUtxosBeforeMultiXecReceiveTx,
currentUtxosAfterMultiXecReceiveTx,
),
).toStrictEqual(false);
});
test('whichUtxosWereConsumed correctly identifies consumed utxos from a single send XEC tx', async () => {
expect(
whichUtxosWereConsumed(
previousUtxosBeforeSingleXecSendTx,
currentUtxosAfterSingleXecSendTx,
),
).toStrictEqual(utxosConsumedBySingleXecSendTx);
});
test('whichUtxosWereConsumed correctly identifies consumed utxos from a send all XEC tx [template]', async () => {
expect(
whichUtxosWereConsumed(
previousUtxosBeforeSendAllTxTemplate,
currentUtxosAfterSendAllTxTemplate,
),
).toStrictEqual(utxosConsumedBySendAllTxTemplate);
});
test('whichUtxosWereConsumed correctly identifies consumed utxos from a send all XEC tx', async () => {
expect(
whichUtxosWereConsumed(
previousUtxosBeforeSendAllTx,
currentUtxosAfterSendAllTx,
),
).toStrictEqual(utxosConsumedBySendAllTx);
});
test('whichUtxosWereConsumed correctly identifies consumed utxos from a single send XEC tx [template]', async () => {
expect(
whichUtxosWereConsumed(
previousUtxosTemplate,
currentUtxosAfterSingleXecSendTxTemplate,
),
).toStrictEqual(utxosConsumedBySingleXecSendTxTemplate);
});
test('whichUtxosWereConsumed correctly identifies consumed utxos from a send eToken tx [template]', async () => {
expect(
whichUtxosWereConsumed(
previousUtxosTemplate,
currentUtxosAfterEtokenSendTxTemplate,
),
).toStrictEqual(utxosConsumedByEtokenSendTxTemplate);
});
test('whichUtxosWereConsumed correctly identifies consumed utxos from a send eToken tx', async () => {
expect(
whichUtxosWereConsumed(
previousUtxosBeforeEtokenSendTx,
currentUtxosAfterEtokenSendTx,
),
).toStrictEqual(utxosConsumedByEtokenSendTx);
});
test('addNewHydratedUtxos correctly adds new utxos object to existing hydratedUtxoDetails object', async () => {
expect(
addNewHydratedUtxos(
newHydratedUtxosSingleTemplate,
hydratedUtxoDetailsBeforeAddingTemplate,
),
).toStrictEqual(hydratedUtxoDetailsAfterAddingSingleUtxoTemplate);
});
test('addNewHydratedUtxos correctly adds more than 20 new hydrated utxos to existing hydratedUtxoDetails object', async () => {
expect(
addNewHydratedUtxos(
addedHydratedUtxosOverTwenty,
existingHydratedUtxoDetails,
),
).toStrictEqual(existingHydratedUtxoDetailsAfterAdd);
});
test('removeConsumedUtxos correctly removes a single utxo from hydratedUtxoDetails - template', async () => {
expect(
removeConsumedUtxos(
consumedUtxoTemplate,
hydratedUtxoDetailsBeforeConsumedTemplate,
),
).toStrictEqual(hydratedUtxoDetailsAfterRemovingConsumedUtxoTemplate);
});
test('removeConsumedUtxos correctly removes a single utxo from hydratedUtxoDetails', async () => {
expect(
removeConsumedUtxos(
consumedUtxos,
hydratedUtxoDetailsBeforeRemovingConsumedUtxos,
),
).toStrictEqual(hydratedUtxoDetailsAfterRemovingConsumedUtxos);
});
test('removeConsumedUtxos correctly removes more than twenty utxos from hydratedUtxoDetails', async () => {
expect(
removeConsumedUtxos(
consumedUtxosMoreThanTwenty,
hydratedUtxoDetailsBeforeRemovingConsumedUtxos,
),
).toStrictEqual(
hydratedUtxoDetailsAfterRemovingMoreThanTwentyConsumedUtxos,
);
});
test('removeConsumedUtxos correctly removes more than twenty utxos from multiple utxo objects from hydratedUtxoDetails', async () => {
expect(
removeConsumedUtxos(
consumedUtxosMoreThanTwentyInRandomObjects,
hydratedUtxoDetailsBeforeRemovingConsumedUtxos,
),
).toStrictEqual(
hydratedUtxoDetailsAfterRemovingMoreThanTwentyConsumedUtxos,
);
});
test('getUtxoCount correctly calculates the total for a utxo object with empty addresses [template]', async () => {
expect(getUtxoCount(utxoCountSingleTemplate)).toStrictEqual(1);
});
test('getUtxoCount correctly calculates the total for multiple utxos [template]', async () => {
expect(getUtxoCount(utxoCountMultiTemplate)).toStrictEqual(12);
});
test('areAllUtxosIncludedInIncrementallyHydratedUtxos correctly identifies all utxos are in incrementally hydrated utxos [template]', async () => {
expect(
areAllUtxosIncludedInIncrementallyHydratedUtxos(
incrementalUtxosTemplate,
incrementallyHydratedUtxosTemplate,
),
).toBe(true);
});
test('areAllUtxosIncludedInIncrementallyHydratedUtxos returns false if a utxo in the utxo set is not in incrementally hydrated utxos [template]', async () => {
expect(
areAllUtxosIncludedInIncrementallyHydratedUtxos(
incrementalUtxosTemplate,
incrementallyHydratedUtxosTemplateMissing,
),
).toBe(false);
});
test('areAllUtxosIncludedInIncrementallyHydratedUtxos correctly identifies all utxos are in incrementally hydrated utxos', async () => {
expect(
areAllUtxosIncludedInIncrementallyHydratedUtxos(
utxosAfterSentTxIncremental,
incrementallyHydratedUtxosAfterProcessing,
),
).toBe(true);
});
test('areAllUtxosIncludedInIncrementallyHydratedUtxos returns false if a utxo in the utxo set is not in incrementally hydrated utxos', async () => {
expect(
areAllUtxosIncludedInIncrementallyHydratedUtxos(
utxosAfterSentTxIncremental,
incrementallyHydratedUtxosAfterProcessingOneMissing,
),
).toBe(false);
});
test('areAllUtxosIncludedInIncrementallyHydratedUtxos returns false if utxo set is invalid', async () => {
expect(
areAllUtxosIncludedInIncrementallyHydratedUtxos(
{},
incrementallyHydratedUtxosAfterProcessing,
),
).toBe(false);
});
test('convertEtokenToEcashAddr successfully converts a valid eToken address to eCash', async () => {
const result = convertEtokenToEcashAddr(
'etoken:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs',
);
expect(result).toStrictEqual(
'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8',
);
});
test('convertEtokenToEcashAddr successfully converts prefixless eToken address as input', async () => {
const result = convertEtokenToEcashAddr(
'qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs',
);
expect(result).toStrictEqual(
'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8',
);
});
test('convertEtokenToEcashAddr throws error with an invalid eToken address as input', async () => {
const result = convertEtokenToEcashAddr('etoken:qpj9gcldpffcs');
expect(result).toStrictEqual(
new Error(
'cashMethods.convertToEcashAddr() error: etoken:qpj9gcldpffcs is not a valid etoken address',
),
);
});
test('convertEtokenToEcashAddr throws error with an ecash address as input', async () => {
const result = convertEtokenToEcashAddr(
'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8',
);
expect(result).toStrictEqual(
new Error(
'cashMethods.convertToEcashAddr() error: ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8 is not a valid etoken address',
),
);
});
test('convertEtokenToEcashAddr throws error with null input', async () => {
const result = convertEtokenToEcashAddr(null);
expect(result).toStrictEqual(
new Error(
'cashMethods.convertToEcashAddr() error: No etoken address provided',
),
);
});
test('convertEtokenToEcashAddr throws error with empty string input', async () => {
const result = convertEtokenToEcashAddr('');
expect(result).toStrictEqual(
new Error(
'cashMethods.convertToEcashAddr() error: No etoken address provided',
),
);
});
test('convertEcashtoEtokenAddr successfully converts a valid ecash address into an etoken address', async () => {
const eCashAddress = 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8';
const eTokenAddress =
'etoken:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs';
const result = convertEcashtoEtokenAddr(eCashAddress);
expect(result).toStrictEqual(eTokenAddress);
});
test('convertEcashtoEtokenAddr successfully converts a valid prefix-less ecash address into an etoken address', async () => {
const eCashAddress = 'qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8';
const eTokenAddress =
'etoken:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs';
const result = convertEcashtoEtokenAddr(eCashAddress);
expect(result).toStrictEqual(eTokenAddress);
});
test('convertEcashtoEtokenAddr throws error with invalid ecash address input', async () => {
const eCashAddress = 'ecash:qpaNOTVALIDADDRESSwu8';
const result = convertEcashtoEtokenAddr(eCashAddress);
expect(result).toStrictEqual(
new Error(eCashAddress + ' is not a valid ecash address'),
);
});
test('convertEcashtoEtokenAddr throws error with a valid etoken address input', async () => {
const eTokenAddress =
'etoken:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs';
const result = convertEcashtoEtokenAddr(eTokenAddress);
expect(result).toStrictEqual(
new Error(eTokenAddress + ' is not a valid ecash address'),
);
});
test('convertEcashtoEtokenAddr throws error with a valid bitcoincash address input', async () => {
const bchAddress =
'bitcoincash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9g0vsgy56s';
const result = convertEcashtoEtokenAddr(bchAddress);
expect(result).toStrictEqual(
new Error(bchAddress + ' is not a valid ecash address'),
);
});
test('convertEcashtoEtokenPrefix throws error with null ecash address input', async () => {
const eCashAddress = null;
const result = convertEcashtoEtokenAddr(eCashAddress);
expect(result).toStrictEqual(
new Error(eCashAddress + ' is not a valid ecash address'),
);
});
it(`flattenContactList flattens contactList array by returning an array of addresses`, () => {
expect(
flattenContactList([
{
address: 'ecash:qpdkc5p7f25hwkxsr69m3evlj4h7wqq9xcgmjc8sxr',
name: 'Alpha',
},
{
address: 'ecash:qpq235n3l3u6ampc8slapapnatwfy446auuv64ylt2',
name: 'Beta',
},
{
address: 'ecash:qz50e58nkeg2ej2f34z6mhwylp6ven8emy8pp52r82',
name: 'Gamma',
},
]),
).toStrictEqual([
'ecash:qpdkc5p7f25hwkxsr69m3evlj4h7wqq9xcgmjc8sxr',
'ecash:qpq235n3l3u6ampc8slapapnatwfy446auuv64ylt2',
'ecash:qz50e58nkeg2ej2f34z6mhwylp6ven8emy8pp52r82',
]);
});
it(`flattenContactList flattens contactList array of length 1 by returning an array of 1 address`, () => {
expect(
flattenContactList([
{
address: 'ecash:qpdkc5p7f25hwkxsr69m3evlj4h7wqq9xcgmjc8sxr',
name: 'Alpha',
},
]),
).toStrictEqual(['ecash:qpdkc5p7f25hwkxsr69m3evlj4h7wqq9xcgmjc8sxr']);
});
it(`flattenContactList returns an empty array for invalid input`, () => {
expect(flattenContactList(false)).toStrictEqual([]);
});
it(`getHashArrayFromWallet returns false for a legacy wallet`, () => {
expect(
getHashArrayFromWallet(mockLegacyWallets.legacyAlphaMainnet),
).toBe(false);
});
it(`Successfully extracts a hash160 array from a migrated wallet object`, () => {
expect(
getHashArrayFromWallet(
mockLegacyWallets.migratedLegacyAlphaMainnet,
),
).toStrictEqual([
'960c9ed561f1699f0c49974d50b3bb7cdc118625',
'2be0e0c999e7e77a443ea726f82c441912fca92b',
'ba8257db65f40359989c7b894c5e88ed7b6344f6',
]);
});
it(`Successfully parses an incoming XEC tx`, () => {
expect(
parseChronikTx(lambdaIncomingXecTx, lambdaHash160s),
).toStrictEqual({
incoming: true,
xecAmount: '42',
isEtokenTx: false,
});
});
it(`Successfully parses an outgoing XEC tx`, () => {
expect(
parseChronikTx(lambdaOutgoingXecTx, lambdaHash160s),
).toStrictEqual({
incoming: false,
xecAmount: '222',
isEtokenTx: false,
});
});
it(`Successfully parses an incoming eToken tx`, () => {
expect(
parseChronikTx(lambdaIncomingEtokenTx, lambdaHash160s),
).toStrictEqual({
incoming: true,
xecAmount: '5.46',
isEtokenTx: true,
slpMeta: {
tokenId:
'4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3',
tokenType: 'FUNGIBLE',
txType: 'SEND',
},
etokenAmount: '12',
});
});
it(`Successfully parses an outgoing eToken tx`, () => {
expect(
parseChronikTx(lambdaOutgoingEtokenTx, lambdaHash160s),
).toStrictEqual({
incoming: false,
xecAmount: '5.46',
isEtokenTx: true,
slpMeta: {
tokenId:
'4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3',
tokenType: 'FUNGIBLE',
txType: 'SEND',
},
etokenAmount: '17',
});
});
it(`Returns decimals, name, and ticker for an eToken stored in wallet object`, () => {
expect(
checkWalletForTokenInfo(
'4bd147fc5d5ff26249a9299c46b80920c0b81f59a60e05428262160ebee0b0c3',
validStoredWallet,
),
).toStrictEqual({
decimals: 0,
name: 'Covid19 Lifetime Immunity',
ticker: 'NOCOVID',
});
});
it(`Returns false for an eToken not stored in a wallet object`, () => {
expect(
checkWalletForTokenInfo(
'98183238638ecb4ddc365056e22de0e8a05448c1e6084bae247fae5a74ad4f48',
validStoredWallet,
),
).toBe(false);
});
it(`isActiveWebsocket returns true for an active chronik websocket connection`, () => {
expect(isActiveWebsocket(activeWebsocketAlpha)).toBe(true);
});
it(`isActiveWebsocket returns false for a disconnected chronik websocket connection`, () => {
expect(isActiveWebsocket(disconnectedWebsocketAlpha)).toBe(false);
});
it(`isActiveWebsocket returns false for a null chronik websocket connection`, () => {
expect(isActiveWebsocket(null)).toBe(false);
});
it(`isActiveWebsocket returns false for an active websocket connection with no subscriptions`, () => {
expect(isActiveWebsocket(unsubscribedWebsocket)).toBe(false);
});
});
diff --git a/web/cashtab/src/utils/cashMethods.js b/web/cashtab/src/utils/cashMethods.js
index d0060a2e1..55be92791 100644
--- a/web/cashtab/src/utils/cashMethods.js
+++ b/web/cashtab/src/utils/cashMethods.js
@@ -1,1452 +1,1452 @@
import { currency } from 'components/Common/Ticker';
import {
isValidXecAddress,
isValidEtokenAddress,
isValidBchApiUtxoObject,
isValidContactList,
} from 'utils/validation';
import BigNumber from 'bignumber.js';
import cashaddr from 'ecashaddrjs';
import useBCH from '../hooks/useBCH';
export const generateTxInput = (
BCH,
isOneToMany,
utxos,
txBuilder,
destinationAddressAndValueArray,
satoshisToSend,
feeInSatsPerByte,
) => {
const { calcFee } = useBCH();
let txInputObj = {};
const inputUtxos = [];
let txFee = 0;
let totalInputUtxoValue = new BigNumber(0);
try {
if (
!BCH ||
(isOneToMany && !destinationAddressAndValueArray) ||
!utxos ||
!txBuilder ||
!satoshisToSend ||
!feeInSatsPerByte
) {
throw new Error('Invalid tx input parameter');
}
// A normal tx will have 2 outputs, destination and change
// A one to many tx will have n outputs + 1 change output, where n is the number of recipients
const txOutputs = isOneToMany
? destinationAddressAndValueArray.length + 1
: 2;
for (let i = 0; i < utxos.length; i++) {
const utxo = utxos[i];
totalInputUtxoValue = totalInputUtxoValue.plus(utxo.value);
const vout = utxo.vout;
const txid = utxo.txid;
// add input with txid and index of vout
txBuilder.addInput(txid, vout);
inputUtxos.push(utxo);
txFee = calcFee(BCH, inputUtxos, txOutputs, feeInSatsPerByte);
if (totalInputUtxoValue.minus(satoshisToSend).minus(txFee).gte(0)) {
break;
}
}
} catch (err) {
console.log(`generateTxInput() error: ` + err);
throw err;
}
txInputObj.txBuilder = txBuilder;
txInputObj.totalInputUtxoValue = totalInputUtxoValue;
txInputObj.inputUtxos = inputUtxos;
txInputObj.txFee = txFee;
return txInputObj;
};
export const getChangeAddressFromInputUtxos = (BCH, inputUtxos, wallet) => {
if (!BCH || !inputUtxos || !wallet) {
throw new Error('Invalid getChangeAddressFromWallet input parameter');
}
// Assume change address is input address of utxo at index 0
let changeAddress;
// Validate address
try {
changeAddress = inputUtxos[0].address;
BCH.Address.isCashAddress(changeAddress);
} catch (err) {
throw new Error('Invalid input utxo');
}
return changeAddress;
};
/*
* Parse the total value of a send XEC tx and checks whether it is more than dust
* One to many: isOneToMany is true, singleSendValue is null
* One to one: isOneToMany is false, destinationAddressAndValueArray is null
* Returns the aggregate send value in BigNumber format
*/
export const parseXecSendValue = (
isOneToMany,
singleSendValue,
destinationAddressAndValueArray,
) => {
let value = new BigNumber(0);
try {
if (isOneToMany) {
// this is a one to many XEC transaction
if (
!destinationAddressAndValueArray ||
!destinationAddressAndValueArray.length
) {
throw new Error('Invalid destinationAddressAndValueArray');
}
const arrayLength = destinationAddressAndValueArray.length;
for (let i = 0; i < arrayLength; i++) {
// add the total value being sent in this array of recipients
// each array row is: 'eCash address, send value'
value = BigNumber.sum(
value,
new BigNumber(
destinationAddressAndValueArray[i].split(',')[1],
),
);
}
} else {
// this is a one to one XEC transaction then check singleSendValue
// note: one to many transactions won't be sending a singleSendValue param
if (!singleSendValue) {
throw new Error('Invalid singleSendValue');
}
value = new BigNumber(singleSendValue);
}
// If user is attempting to send an aggregate value that is less than minimum accepted by the backend
if (
value.lt(
new BigNumber(fromSatoshisToXec(currency.dustSats).toString()),
)
) {
// Throw the same error given by the backend attempting to broadcast such a tx
throw new Error('dust');
}
} catch (err) {
console.log('Error in parseXecSendValue: ' + err);
throw err;
}
return value;
};
/*
* Generates an OP_RETURN script to reflect the various send XEC permutations
* involving messaging, encryption, eToken IDs and airdrop flags.
*
* Returns the final encoded script object
*/
export const generateOpReturnScript = (
BCH,
optionalOpReturnMsg,
encryptionFlag,
airdropFlag,
airdropTokenId,
encryptedEj,
) => {
// encrypted mesage is mandatory when encryptionFlag is true
// airdrop token id is mandatory when airdropFlag is true
if (
!BCH ||
(encryptionFlag && !encryptedEj) ||
(airdropFlag && !airdropTokenId)
) {
throw new Error('Invalid OP RETURN script input');
}
// Note: script.push(Buffer.from(currency.opReturn.opReturnPrefixHex, 'hex')); actually evaluates to '016a'
// instead of keeping the hex string intact. This behavour is specific to the initial script array element.
// To get around this, the bch-js approach of directly using the opReturn prefix in decimal form for the initial entry is used here.
let script = [currency.opReturn.opReturnPrefixDec]; // initialize script with the OP_RETURN op code (6a) in decimal form (106)
try {
if (encryptionFlag) {
// if the user has opted to encrypt this message
// add the encrypted cashtab messaging prefix and encrypted msg to script
script.push(
Buffer.from(
currency.opReturn.appPrefixesHex.cashtabEncrypted,
'hex',
), // 65746162
);
// add the encrypted message to script
script.push(Buffer.from(encryptedEj));
} else {
// this is an un-encrypted message
if (airdropFlag) {
// if this was routed from the airdrop component
// add the airdrop prefix to script
script.push(
Buffer.from(
currency.opReturn.appPrefixesHex.airdrop,
'hex',
), // drop
);
// add the airdrop token ID to script
script.push(Buffer.from(airdropTokenId, 'hex'));
}
// add the cashtab prefix to script
script.push(
Buffer.from(currency.opReturn.appPrefixesHex.cashtab, 'hex'), // 00746162
);
// add the un-encrypted message to script if supplied
if (optionalOpReturnMsg) {
script.push(Buffer.from(optionalOpReturnMsg));
}
}
} catch (err) {
console.log('Error in generateOpReturnScript(): ' + err);
throw err;
}
const data = BCH.Script.encode(script);
return data;
};
export const generateTxOutput = (
BCH,
isOneToMany,
singleSendValue,
satoshisToSend,
totalInputUtxoValue,
destinationAddress,
destinationAddressAndValueArray,
changeAddress,
txFee,
txBuilder,
) => {
try {
if (
!BCH ||
(isOneToMany && !destinationAddressAndValueArray) ||
(!isOneToMany && !destinationAddress && !singleSendValue) ||
!changeAddress ||
!satoshisToSend ||
!totalInputUtxoValue ||
!txFee ||
!txBuilder
) {
throw new Error('Invalid tx input parameter');
}
// amount to send back to the remainder address.
const remainder = new BigNumber(totalInputUtxoValue)
.minus(satoshisToSend)
.minus(txFee);
if (remainder.lt(0)) {
throw new Error(`Insufficient funds`);
}
if (isOneToMany) {
// for one to many mode, add the multiple outputs from the array
let arrayLength = destinationAddressAndValueArray.length;
for (let i = 0; i < arrayLength; i++) {
// add each send tx from the array as an output
let outputAddress =
destinationAddressAndValueArray[i].split(',')[0];
let outputValue = new BigNumber(
destinationAddressAndValueArray[i].split(',')[1],
);
txBuilder.addOutput(
BCH.Address.toCashAddress(outputAddress),
parseInt(fromXecToSatoshis(outputValue)),
);
}
} else {
// for one to one mode, add output w/ single address and amount to send
txBuilder.addOutput(
BCH.Address.toCashAddress(destinationAddress),
parseInt(fromXecToSatoshis(singleSendValue)),
);
}
// if a remainder exists, return to change address as the final output
if (remainder.gte(new BigNumber(currency.dustSats))) {
txBuilder.addOutput(changeAddress, parseInt(remainder));
}
} catch (err) {
console.log('Error in generateTxOutput(): ' + err);
throw err;
}
return txBuilder;
};
export const signAndBuildTx = (BCH, inputUtxos, txBuilder) => {
if (
!BCH ||
!inputUtxos ||
inputUtxos.length === 0 ||
!txBuilder ||
// txBuilder.transaction.tx.ins is empty until the inputUtxos are signed
txBuilder.transaction.tx.outs.length === 0
) {
throw new Error('Invalid buildTx parameter');
}
// Sign the transactions with the HD node.
for (let i = 0; i < inputUtxos.length; i++) {
const utxo = inputUtxos[i];
try {
txBuilder.sign(
i,
BCH.ECPair.fromWIF(utxo.wif),
undefined,
txBuilder.hashTypes.SIGHASH_ALL,
utxo.value,
);
} catch (err) {
throw new Error('Error signing input utxos');
}
}
let hex;
try {
// build tx
const tx = txBuilder.build();
// output rawhex
hex = tx.toHex();
} catch (err) {
throw new Error('Transaction build failed');
}
return hex;
};
export function parseOpReturn(hexStr) {
if (
!hexStr ||
typeof hexStr !== 'string' ||
hexStr.substring(0, 2) !== currency.opReturn.opReturnPrefixHex
) {
return false;
}
hexStr = hexStr.slice(2); // remove the first byte i.e. 6a
/*
* @Return: resultArray is structured as follows:
* resultArray[0] is the transaction type i.e. eToken prefix, cashtab prefix, external message itself if unrecognized prefix
* resultArray[1] is the actual cashtab message or the 2nd part of an external message
* resultArray[2 - n] are the additional messages for future protcols
*/
let resultArray = [];
let message = '';
let hexStrLength = hexStr.length;
for (let i = 0; hexStrLength !== 0; i++) {
// part 1: check the preceding byte value for the subsequent message
let byteValue = hexStr.substring(0, 2);
let msgByteSize = 0;
if (byteValue === currency.opReturn.opPushDataOne) {
// if this byte is 4c then the next byte is the message byte size - retrieve the message byte size only
msgByteSize = parseInt(hexStr.substring(2, 4), 16); // hex base 16 to decimal base 10
hexStr = hexStr.slice(4); // strip the 4c + message byte size info
} else {
// take the byte as the message byte size
msgByteSize = parseInt(hexStr.substring(0, 2), 16); // hex base 16 to decimal base 10
hexStr = hexStr.slice(2); // strip the message byte size info
}
// part 2: parse the subsequent message based on bytesize
const msgCharLength = 2 * msgByteSize;
message = hexStr.substring(0, msgCharLength);
if (i === 0 && message === currency.opReturn.appPrefixesHex.eToken) {
// add the extracted eToken prefix to array then exit loop
resultArray[i] = currency.opReturn.appPrefixesHex.eToken;
break;
} else if (
i === 0 &&
message === currency.opReturn.appPrefixesHex.cashtab
) {
// add the extracted Cashtab prefix to array
resultArray[i] = currency.opReturn.appPrefixesHex.cashtab;
} else if (
i === 0 &&
message === currency.opReturn.appPrefixesHex.cashtabEncrypted
) {
// add the Cashtab encryption prefix to array
resultArray[i] = currency.opReturn.appPrefixesHex.cashtabEncrypted;
} else if (
i === 0 &&
message === currency.opReturn.appPrefixesHex.airdrop
) {
// add the airdrop prefix to array
resultArray[i] = currency.opReturn.appPrefixesHex.airdrop;
} else {
// this is either an external message or a subsequent cashtab message loop to extract the message
resultArray[i] = message;
}
// strip out the parsed message
hexStr = hexStr.slice(msgCharLength);
hexStrLength = hexStr.length;
}
return resultArray;
}
export const fromLegacyDecimals = (
amount,
cashDecimals = currency.cashDecimals,
) => {
// Input 0.00000546 BCH
// Output 5.46 XEC or 0.00000546 BCH, depending on currency.cashDecimals
const amountBig = new BigNumber(amount);
const conversionFactor = new BigNumber(10 ** (8 - cashDecimals));
const amountSmallestDenomination = amountBig
.times(conversionFactor)
.toNumber();
return amountSmallestDenomination;
};
export const fromSatoshisToXec = (
amount,
cashDecimals = currency.cashDecimals,
) => {
const amountBig = new BigNumber(amount);
const multiplier = new BigNumber(10 ** (-1 * cashDecimals));
const amountInBaseUnits = amountBig.times(multiplier);
- return amountInBaseUnits.toNumber();
+ return amountInBaseUnits;
};
export const fromXecToSatoshis = (
sendAmount,
cashDecimals = currency.cashDecimals,
) => {
// Replace the BCH.toSatoshi method with an equivalent function that works for arbitrary decimal places
// Example, for an 8 decimal place currency like Bitcoin
// Input: a BigNumber of the amount of Bitcoin to be sent
// Output: a BigNumber of the amount of satoshis to be sent, or false if input is invalid
// Validate
// Input should be a BigNumber with no more decimal places than cashDecimals
const isValidSendAmount =
BigNumber.isBigNumber(sendAmount) && sendAmount.dp() <= cashDecimals;
if (!isValidSendAmount) {
return false;
}
const conversionFactor = new BigNumber(10 ** cashDecimals);
const sendAmountSmallestDenomination = sendAmount.times(conversionFactor);
return sendAmountSmallestDenomination;
};
export const batchArray = (inputArray, batchSize) => {
// take an array of n elements, return an array of arrays each of length batchSize
const batchedArray = [];
for (let i = 0; i < inputArray.length; i += batchSize) {
const tempArray = inputArray.slice(i, i + batchSize);
batchedArray.push(tempArray);
}
return batchedArray;
};
export const flattenBatchedHydratedUtxos = batchedHydratedUtxoDetails => {
// Return same result as if only the bulk API call were made
// to do this, just need to move all utxos under one slpUtxos
/*
given
[
{
slpUtxos: [
{
utxos: [],
address: '',
}
],
},
{
slpUtxos: [
{
utxos: [],
address: '',
}
],
}
]
return [
{
slpUtxos: [
{
utxos: [],
address: ''
},
{
utxos: [],
address: ''
},
]
}
*/
const flattenedBatchedHydratedUtxos = { slpUtxos: [] };
for (let i = 0; i < batchedHydratedUtxoDetails.length; i += 1) {
const theseSlpUtxos = batchedHydratedUtxoDetails[i].slpUtxos[0];
flattenedBatchedHydratedUtxos.slpUtxos.push(theseSlpUtxos);
}
return flattenedBatchedHydratedUtxos;
};
export const flattenContactList = contactList => {
/*
Converts contactList from array of objects of type {address: <valid XEC address>, name: <string>} to array of addresses only
If contact list is invalid, returns and empty array
*/
if (!isValidContactList(contactList)) {
return [];
}
let flattenedContactList = [];
for (let i = 0; i < contactList.length; i += 1) {
const thisAddress = contactList[i].address;
flattenedContactList.push(thisAddress);
}
return flattenedContactList;
};
export const loadStoredWallet = walletStateFromStorage => {
// Accept cached tokens array that does not save BigNumber type of BigNumbers
// Return array with BigNumbers converted
// See BigNumber.js api for how to create a BigNumber object from an object
// https://mikemcl.github.io/bignumber.js/
const liveWalletState = walletStateFromStorage;
const { slpBalancesAndUtxos, tokens } = liveWalletState;
for (let i = 0; i < tokens.length; i += 1) {
const thisTokenBalance = tokens[i].balance;
thisTokenBalance._isBigNumber = true;
tokens[i].balance = new BigNumber(thisTokenBalance);
}
// Also confirm balance is correct
// Necessary step in case currency.decimals changed since last startup
const balancesRebased = getWalletBalanceFromUtxos(
slpBalancesAndUtxos.nonSlpUtxos,
);
liveWalletState.balances = balancesRebased;
return liveWalletState;
};
export const getWalletBalanceFromUtxos = nonSlpUtxos => {
const totalBalanceInSatoshis = nonSlpUtxos.reduce(
(previousBalance, utxo) => previousBalance + utxo.value,
0,
);
return {
totalBalanceInSatoshis,
- totalBalance: fromSatoshisToXec(totalBalanceInSatoshis),
+ totalBalance: fromSatoshisToXec(totalBalanceInSatoshis).toNumber(),
};
};
export const isValidStoredWallet = walletStateFromStorage => {
return (
typeof walletStateFromStorage === 'object' &&
'state' in walletStateFromStorage &&
typeof walletStateFromStorage.state === 'object' &&
'balances' in walletStateFromStorage.state &&
'utxos' in walletStateFromStorage.state &&
'hydratedUtxoDetails' in walletStateFromStorage.state &&
'slpBalancesAndUtxos' in walletStateFromStorage.state &&
'tokens' in walletStateFromStorage.state
);
};
export const getWalletState = wallet => {
if (!wallet || !wallet.state) {
return {
balances: { totalBalance: 0, totalBalanceInSatoshis: 0 },
hydratedUtxoDetails: {},
tokens: [],
slpBalancesAndUtxos: {},
parsedTxHistory: [],
utxos: [],
};
}
return wallet.state;
};
export function convertEtokenToEcashAddr(eTokenAddress) {
if (!eTokenAddress) {
return new Error(
`cashMethods.convertToEcashAddr() error: No etoken address provided`,
);
}
// Confirm input is a valid eToken address
const isValidInput = isValidEtokenAddress(eTokenAddress);
if (!isValidInput) {
return new Error(
`cashMethods.convertToEcashAddr() error: ${eTokenAddress} is not a valid etoken address`,
);
}
// Check for etoken: prefix
const isPrefixedEtokenAddress = eTokenAddress.slice(0, 7) === 'etoken:';
// If no prefix, assume it is checksummed for an etoken: prefix
const testedEtokenAddr = isPrefixedEtokenAddress
? eTokenAddress
: `etoken:${eTokenAddress}`;
let ecashAddress;
try {
const { type, hash } = cashaddr.decode(testedEtokenAddr);
ecashAddress = cashaddr.encode('ecash', type, hash);
} catch (err) {
return err;
}
return ecashAddress;
}
export function convertToEcashPrefix(bitcoincashPrefixedAddress) {
// Prefix-less addresses may be valid, but the cashaddr.decode function used below
// will throw an error without a prefix. Hence, must ensure prefix to use that function.
const hasPrefix = bitcoincashPrefixedAddress.includes(':');
if (hasPrefix) {
// Is it bitcoincash: or simpleledger:
const { type, hash, prefix } = cashaddr.decode(
bitcoincashPrefixedAddress,
);
let newPrefix;
if (prefix === 'bitcoincash') {
newPrefix = 'ecash';
} else if (prefix === 'simpleledger') {
newPrefix = 'etoken';
} else {
return bitcoincashPrefixedAddress;
}
const convertedAddress = cashaddr.encode(newPrefix, type, hash);
return convertedAddress;
} else {
return bitcoincashPrefixedAddress;
}
}
export function convertEcashtoEtokenAddr(eCashAddress) {
const isValidInput = isValidXecAddress(eCashAddress);
if (!isValidInput) {
return new Error(`${eCashAddress} is not a valid ecash address`);
}
// Check for ecash: prefix
const isPrefixedEcashAddress = eCashAddress.slice(0, 6) === 'ecash:';
// If no prefix, assume it is checksummed for an ecash: prefix
const testedEcashAddr = isPrefixedEcashAddress
? eCashAddress
: `ecash:${eCashAddress}`;
let eTokenAddress;
try {
const { type, hash } = cashaddr.decode(testedEcashAddr);
eTokenAddress = cashaddr.encode('etoken', type, hash);
} catch (err) {
return new Error('eCash to eToken address conversion error');
}
return eTokenAddress;
}
export function toLegacyCash(addr) {
// Confirm input is a valid ecash address
const isValidInput = isValidXecAddress(addr);
if (!isValidInput) {
return new Error(`${addr} is not a valid ecash address`);
}
// Check for ecash: prefix
const isPrefixedXecAddress = addr.slice(0, 6) === 'ecash:';
// If no prefix, assume it is checksummed for an ecash: prefix
const testedXecAddr = isPrefixedXecAddress ? addr : `ecash:${addr}`;
let legacyCashAddress;
try {
const { type, hash } = cashaddr.decode(testedXecAddr);
legacyCashAddress = cashaddr.encode(currency.legacyPrefix, type, hash);
} catch (err) {
return err;
}
return legacyCashAddress;
}
export function toLegacyCashArray(addressArray) {
let cleanArray = []; // array of bch converted addresses to be returned
if (
addressArray === null ||
addressArray === undefined ||
!addressArray.length ||
addressArray === ''
) {
return new Error('Invalid addressArray input');
}
const arrayLength = addressArray.length;
for (let i = 0; i < arrayLength; i++) {
let addressValueArr = addressArray[i].split(',');
let address = addressValueArr[0];
let value = addressValueArr[1];
// NB that toLegacyCash() includes address validation; will throw error for invalid address input
const legacyAddress = toLegacyCash(address);
if (legacyAddress instanceof Error) {
return legacyAddress;
}
let convertedArrayData = legacyAddress + ',' + value + '\n';
cleanArray.push(convertedArrayData);
}
return cleanArray;
}
export function toLegacyToken(addr) {
// Confirm input is a valid ecash address
const isValidInput = isValidEtokenAddress(addr);
if (!isValidInput) {
return new Error(`${addr} is not a valid etoken address`);
}
// Check for ecash: prefix
const isPrefixedEtokenAddress = addr.slice(0, 7) === 'etoken:';
// If no prefix, assume it is checksummed for an ecash: prefix
const testedEtokenAddr = isPrefixedEtokenAddress ? addr : `etoken:${addr}`;
let legacyTokenAddress;
try {
const { type, hash } = cashaddr.decode(testedEtokenAddr);
legacyTokenAddress = cashaddr.encode('simpleledger', type, hash);
} catch (err) {
return err;
}
return legacyTokenAddress;
}
export const confirmNonEtokenUtxos = (hydratedUtxos, nonEtokenUtxos) => {
// scan through hydratedUtxoDetails
for (let i = 0; i < hydratedUtxos.length; i += 1) {
// Find utxos with txids matching nonEtokenUtxos
if (nonEtokenUtxos.includes(hydratedUtxos[i].txid)) {
// Confirm that such utxos are not eToken utxos
hydratedUtxos[i].isValid = false;
}
}
return hydratedUtxos;
};
export const checkNullUtxosForTokenStatus = txDataResults => {
const nonEtokenUtxos = [];
for (let j = 0; j < txDataResults.length; j += 1) {
const thisUtxoTxid = txDataResults[j].txid;
const thisUtxoVout = txDataResults[j].details.vout;
// Iterate over outputs
for (let k = 0; k < thisUtxoVout.length; k += 1) {
const thisOutput = thisUtxoVout[k];
if (thisOutput.scriptPubKey.type === 'nulldata') {
const asmOutput = thisOutput.scriptPubKey.asm;
if (asmOutput.includes('OP_RETURN 5262419')) {
// then it's an eToken tx that has not been properly validated
// Do not include it in nonEtokenUtxos
// App will ignore it until SLPDB is able to validate it
/*
console.log(
`utxo ${thisUtxoTxid} requires further eToken validation, ignoring`,
);*/
} else {
// Otherwise it's just an OP_RETURN tx that SLPDB has some issue with
// It should still be in the user's utxo set
// Include it in nonEtokenUtxos
/*
console.log(
`utxo ${thisUtxoTxid} is not an eToken tx, adding to nonSlpUtxos`,
);
*/
nonEtokenUtxos.push(thisUtxoTxid);
}
}
}
}
return nonEtokenUtxos;
};
/* Converts a serialized buffer containing encrypted data into an object
* that can be interpreted by the ecies-lite library.
*
* For reference on the parsing logic in this function refer to the link below on the segment of
* ecies-lite's encryption function where the encKey, macKey, iv and cipher are sliced and concatenated
* https://github.com/tibetty/ecies-lite/blob/8fd97e80b443422269d0223ead55802378521679/index.js#L46-L55
*
* A similar PSF implmentation can also be found at:
* https://github.com/Permissionless-Software-Foundation/bch-encrypt-lib/blob/master/lib/encryption.js
*
* For more detailed overview on the ecies encryption scheme, see https://cryptobook.nakov.com/asymmetric-key-ciphers/ecies-public-key-encryption
*/
export const convertToEncryptStruct = encryptionBuffer => {
// based on ecies-lite's encryption logic, the encryption buffer is concatenated as follows:
// [ epk + iv + ct + mac ] whereby:
// - The first 32 or 64 chars of the encryptionBuffer is the epk
// - Both iv and ct params are 16 chars each, hence their combined substring is 32 chars from the end of the epk string
// - within this combined iv/ct substring, the first 16 chars is the iv param, and ct param being the later half
// - The mac param is appended to the end of the encryption buffer
// validate input buffer
if (!encryptionBuffer) {
throw new Error(
'cashmethods.convertToEncryptStruct() error: input must be a buffer',
);
}
try {
// variable tracking the starting char position for string extraction purposes
let startOfBuf = 0;
// *** epk param extraction ***
// The first char of the encryptionBuffer indicates the type of the public key
// If the first char is 4, then the public key is 64 chars
// If the first char is 3 or 2, then the public key is 32 chars
// Otherwise this is not a valid encryption buffer compatible with the ecies-lite library
let publicKey;
switch (encryptionBuffer[0]) {
case 4:
publicKey = encryptionBuffer.slice(0, 65); // extract first 64 chars as public key
break;
case 3:
case 2:
publicKey = encryptionBuffer.slice(0, 33); // extract first 32 chars as public key
break;
default:
throw new Error(`Invalid type: ${encryptionBuffer[0]}`);
}
// *** iv and ct param extraction ***
startOfBuf += publicKey.length; // sets the starting char position to the end of the public key (epk) in order to extract subsequent iv and ct substrings
const encryptionTagLength = 32; // the length of the encryption tag (i.e. mac param) computed from each block of ciphertext, and is used to verify no one has tampered with the encrypted data
const ivCtSubstring = encryptionBuffer.slice(
startOfBuf,
encryptionBuffer.length - encryptionTagLength,
); // extract the substring containing both iv and ct params, which is after the public key but before the mac param i.e. the 'encryption tag'
const ivbufParam = ivCtSubstring.slice(0, 16); // extract the first 16 chars of substring as the iv param
const ctbufParam = ivCtSubstring.slice(16); // extract the last 16 chars as substring the ct param
// *** mac param extraction ***
const macParam = encryptionBuffer.slice(
encryptionBuffer.length - encryptionTagLength,
encryptionBuffer.length,
); // extract the mac param appended to the end of the buffer
return {
iv: ivbufParam,
epk: publicKey,
ct: ctbufParam,
mac: macParam,
};
} catch (err) {
console.error(`useBCH.convertToEncryptStruct() error: `, err);
throw err;
}
};
export const getPublicKey = async (BCH, address) => {
try {
const publicKey = await BCH.encryption.getPubKey(address);
return publicKey.publicKey;
} catch (err) {
if (err['error'] === 'No transaction history.') {
throw new Error(
'Cannot send an encrypted message to a wallet with no outgoing transactions',
);
} else {
throw err;
}
}
};
export const isLegacyMigrationRequired = wallet => {
// If the wallet does not have Path1899,
// Or each Path1899, Path145, Path245 does not have a public key
// Then it requires migration
if (
!wallet.Path1899 ||
!wallet.Path1899.publicKey ||
!wallet.Path1899.hash160 ||
!wallet.Path145.publicKey ||
!wallet.Path145.hash160 ||
!wallet.Path245.publicKey ||
!wallet.Path245.hash160
) {
return true;
}
return false;
};
export const isExcludedUtxo = (utxo, utxoArray) => {
/*
utxo is a single utxo of model
{
height: 724992
tx_hash: "8d4bdedb7c4443412e0c2f316a330863aef54d9ba73560ca60cca6408527b247"
tx_pos: 0
value: 10200
}
utxoArray is an array of utxos
*/
let isExcludedUtxo = true;
const { tx_hash, tx_pos, value } = utxo;
for (let i = 0; i < utxoArray.length; i += 1) {
const thisUtxo = utxoArray[i];
// NOTE
// You can't match height, as this changes from 0 to blockheight after confirmation
//const thisUtxoHeight = thisUtxo.height;
const thisUtxoTxid = thisUtxo.tx_hash;
const thisUtxoTxPos = thisUtxo.tx_pos;
const thisUtxoValue = thisUtxo.value;
// If you find a utxo such that each object key is identical
if (
tx_hash === thisUtxoTxid &&
tx_pos === thisUtxoTxPos &&
value === thisUtxoValue
) {
// Then this utxo is not excluded from the array
isExcludedUtxo = false;
}
}
return isExcludedUtxo;
};
export const whichUtxosWereAdded = (previousUtxos, currentUtxos) => {
let utxosAddedFlag = false;
const utxosAdded = [];
// Iterate over currentUtxos
// For each currentUtxo -- does it exist in previousUtxos?
// If no, it's added
// Note that the inputs are arrays of arrays, model
/*
previousUtxos = [{address: 'string', utxos: []}, ...]
*/
// Iterate over the currentUtxos array of {address: 'string', utxos: []} objects
for (let i = 0; i < currentUtxos.length; i += 1) {
// Take the first object
const thisCurrentUtxoObject = currentUtxos[i];
const thisCurrentUtxoObjectAddress = thisCurrentUtxoObject.address;
const thisCurrentUtxoObjectUtxos = thisCurrentUtxoObject.utxos;
// Iterate over the previousUtxos array of {address: 'string', utxos: []} objects
for (let j = 0; j < previousUtxos.length; j += 1) {
const thisPreviousUtxoObject = previousUtxos[j];
const thisPreviousUtxoObjectAddress =
thisPreviousUtxoObject.address;
// When you find the utxos object at the same address
if (
thisCurrentUtxoObjectAddress === thisPreviousUtxoObjectAddress
) {
// Create a utxosAddedObject with the address
const utxosAddedObject = {
address: thisCurrentUtxoObjectAddress,
utxos: [],
};
utxosAdded.push(utxosAddedObject);
// Grab the previousUtxoObject utxos array. thisCurrentUtxoObjectUtxos has changed compared to thisPreviousUtxoObjectUtxos
const thisPreviousUtxoObjectUtxos =
thisPreviousUtxoObject.utxos;
// To see if any utxos exist in thisCurrentUtxoObjectUtxos that do not exist in thisPreviousUtxoObjectUtxos
// iterate over thisPreviousUtxoObjectUtxos for each utxo in thisCurrentUtxoObjectUtxos
for (let k = 0; k < thisCurrentUtxoObjectUtxos.length; k += 1) {
const thisCurrentUtxo = thisCurrentUtxoObjectUtxos[k];
if (
isExcludedUtxo(
thisCurrentUtxo,
thisPreviousUtxoObjectUtxos,
)
) {
// If thisCurrentUtxo was not in the corresponding previous utxos
// Then it was added
utxosAdded[j].utxos.push(thisCurrentUtxo);
utxosAddedFlag = true;
}
}
}
}
}
// If utxos were added, return them
if (utxosAddedFlag) {
return utxosAdded;
}
// Else return false
return utxosAddedFlag;
};
export const whichUtxosWereConsumed = (previousUtxos, currentUtxos) => {
let utxosConsumedFlag = false;
const utxosConsumed = [];
// Iterate over previousUtxos
// For each previousUtxo -- does it exist in currentUtxos?
// If no, it's consumed
// Note that the inputs are arrays of arrays, model
/*
previousUtxos = [{address: 'string', utxos: []}, ...]
*/
// Iterate over the previousUtxos array of {address: 'string', utxos: []} objects
for (let i = 0; i < previousUtxos.length; i += 1) {
// Take the first object
const thisPreviousUtxoObject = previousUtxos[i];
const thisPreviousUtxoObjectAddress = thisPreviousUtxoObject.address;
const thisPreviousUtxoObjectUtxos = thisPreviousUtxoObject.utxos;
// Iterate over the currentUtxos array of {address: 'string', utxos: []} objects
for (let j = 0; j < currentUtxos.length; j += 1) {
const thisCurrentUtxoObject = currentUtxos[j];
const thisCurrentUtxoObjectAddress = thisCurrentUtxoObject.address;
// When you find the utxos object at the same address
if (
thisCurrentUtxoObjectAddress === thisPreviousUtxoObjectAddress
) {
// Create a utxosConsumedObject with the address
const utxosConsumedObject = {
address: thisCurrentUtxoObjectAddress,
utxos: [],
};
utxosConsumed.push(utxosConsumedObject);
// Grab the currentUtxoObject utxos array. thisCurrentUtxoObjectUtxos has changed compared to thisPreviousUtxoObjectUtxos
const thisCurrentUtxoObjectUtxos = thisCurrentUtxoObject.utxos;
// To see if any utxos exist in thisPreviousUtxoObjectUtxos that do not exist in thisCurrentUtxoObjectUtxos
// iterate over thisCurrentUtxoObjectUtxos for each utxo in thisPreviousUtxoObjectUtxos
for (
let k = 0;
k < thisPreviousUtxoObjectUtxos.length;
k += 1
) {
const thisPreviousUtxo = thisPreviousUtxoObjectUtxos[k];
// If thisPreviousUtxo was not in the corresponding current utxos
if (
isExcludedUtxo(
thisPreviousUtxo,
thisCurrentUtxoObjectUtxos,
)
) {
// Then it was consumed
utxosConsumed[j].utxos.push(thisPreviousUtxo);
utxosConsumedFlag = true;
}
}
}
}
}
// If utxos were consumed, return them
if (utxosConsumedFlag) {
return utxosConsumed;
}
// Else return false
return utxosConsumedFlag;
};
export const addNewHydratedUtxos = (
addedHydratedUtxos,
hydratedUtxoDetails,
) => {
const theseAdditionalHydratedUtxos = addedHydratedUtxos.slpUtxos;
for (let i = 0; i < theseAdditionalHydratedUtxos.length; i += 1) {
const thisHydratedUtxoObj = theseAdditionalHydratedUtxos[i];
hydratedUtxoDetails.slpUtxos.push(thisHydratedUtxoObj);
}
return hydratedUtxoDetails;
// Add hydrateUtxos(addedUtxos) to hydratedUtxoDetails
/*
e.g. add this
{
"slpUtxos":
[
{
"utxos": [
{
"height": 725886,
"tx_hash": "29985c01444bf80ade764e5d40d7ec2c12317e03301243170139c75f20c51f78",
"tx_pos": 0,
"value": 3300,
"txid": "29985c01444bf80ade764e5d40d7ec2c12317e03301243170139c75f20c51f78",
"vout": 0,
"isValid": false
}
],
"address": "bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr"
}
]
}
to this
{
"slpUtxos":
[
{
"utxos": [
{
"height": 725886,
"tx_hash": "29985c01444bf80ade764e5d40d7ec2c12317e03301243170139c75f20c51f78",
"tx_pos": 0,
"value": 3300,
"txid": "29985c01444bf80ade764e5d40d7ec2c12317e03301243170139c75f20c51f78",
"vout": 0,
"isValid": false
}
... up to 20
],
"address": "bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr"
},
{
"utxos": [
{
"height": 725886,
"tx_hash": "29985c01444bf80ade764e5d40d7ec2c12317e03301243170139c75f20c51f78",
"tx_pos": 0,
"value": 3300,
"txid": "29985c01444bf80ade764e5d40d7ec2c12317e03301243170139c75f20c51f78",
"vout": 0,
"isValid": false
}
... up to 20
],
"address": "bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr"
}
,
... a bunch of these in batches of 20
]
}
*/
};
export const removeConsumedUtxos = (consumedUtxos, hydratedUtxoDetails) => {
let hydratedUtxoDetailsWithConsumedUtxosRemoved = hydratedUtxoDetails;
const slpUtxosArray = hydratedUtxoDetails.slpUtxos;
// Iterate over consumedUtxos
// Every utxo in consumedUtxos must be removed from hydratedUtxoDetails
for (let i = 0; i < consumedUtxos.length; i += 1) {
const thisConsumedUtxoObject = consumedUtxos[i]; // {address: 'string', utxos: [{},{},...{}]}
const thisConsumedUtxoObjectAddr = thisConsumedUtxoObject.address;
const thisConsumedUtxoObjectUtxoArray = thisConsumedUtxoObject.utxos;
for (let j = 0; j < thisConsumedUtxoObjectUtxoArray.length; j += 1) {
const thisConsumedUtxo = thisConsumedUtxoObjectUtxoArray[j];
// Iterate through slpUtxosArray to find thisConsumedUtxo
slpUtxosArrayLoop: for (
let k = 0;
k < slpUtxosArray.length;
k += 1
) {
const thisSlpUtxosArrayUtxoObject = slpUtxosArray[k]; // {address: 'string', utxos: [{},{},...{}]}
const thisSlpUtxosArrayUtxoObjectAddr =
thisSlpUtxosArrayUtxoObject.address;
// If this address matches the address of the consumed utxo, check for a consumedUtxo match
// Note, slpUtxos may have many utxo objects with the same address, need to check them all until you find and remove this consumed utxo
if (
thisConsumedUtxoObjectAddr ===
thisSlpUtxosArrayUtxoObjectAddr
) {
const thisSlpUtxosArrayUtxoObjectUtxoArray =
thisSlpUtxosArrayUtxoObject.utxos;
// Iterate to find it and remove it
for (
let m = 0;
m < thisSlpUtxosArrayUtxoObjectUtxoArray.length;
m += 1
) {
const thisHydratedUtxo =
thisSlpUtxosArrayUtxoObjectUtxoArray[m];
if (
thisConsumedUtxo.tx_hash ===
thisHydratedUtxo.tx_hash &&
thisConsumedUtxo.tx_pos ===
thisHydratedUtxo.tx_pos &&
thisConsumedUtxo.value === thisHydratedUtxo.value
) {
// remove it
hydratedUtxoDetailsWithConsumedUtxosRemoved.slpUtxos[
k
].utxos.splice(m, 1);
// go to the next consumedUtxo
break slpUtxosArrayLoop;
}
}
}
}
}
}
return hydratedUtxoDetailsWithConsumedUtxosRemoved;
};
export const getUtxoCount = utxos => {
// return how many utxos
// return false if input is invalid
/*
Both utxos and hydratedUtxoDetails.slpUtxos are build like so
[
{
address: 'string',
utxos: [{}, {}, {}...{}]
},
{
address: 'string',
utxos: [{}, {}, {}...{}]
},
{
address: 'string',
utxos: [{}, {}, {}...{}]
},
]
We want a function that quickly determines how many utxos are here
*/
// First, validate that you are getting a valid bch-api utxo set
// if you are not, then return false -- which would cause areAllUtxosIncludedInIncrementallyHydratedUtxos to return false and calculate utxo set the legacy way
const isValidUtxoObject = isValidBchApiUtxoObject(utxos);
if (!isValidUtxoObject) {
return false;
}
let utxoCount = 0;
for (let i = 0; i < utxos.length; i += 1) {
const thisUtxoArrLength = utxos[i].utxos.length;
utxoCount += thisUtxoArrLength;
}
return utxoCount;
};
export const areAllUtxosIncludedInIncrementallyHydratedUtxos = (
utxos,
incrementallyHydratedUtxos,
) => {
let incrementallyHydratedUtxosIncludesAllUtxosInLatestUtxoApiFetch = false;
// check
const { slpUtxos } = incrementallyHydratedUtxos;
// Iterate over utxos array
for (let i = 0; i < utxos.length; i += 1) {
const thisUtxoObject = utxos[i];
const thisUtxoObjectAddr = thisUtxoObject.address;
const thisUtxoObjectUtxos = thisUtxoObject.utxos;
let utxoFound;
for (let j = 0; j < thisUtxoObjectUtxos.length; j += 1) {
const thisUtxo = thisUtxoObjectUtxos[j];
utxoFound = false;
// Now iterate over slpUtxos to find it
slpUtxosLoop: for (let k = 0; k < slpUtxos.length; k += 1) {
const thisSlpUtxosObject = slpUtxos[k];
const thisSlpUtxosObjectAddr = thisSlpUtxosObject.address;
if (thisUtxoObjectAddr === thisSlpUtxosObjectAddr) {
const thisSlpUtxosObjectUtxos = thisSlpUtxosObject.utxos;
for (
let m = 0;
m < thisSlpUtxosObjectUtxos.length;
m += 1
) {
const thisSlpUtxo = thisSlpUtxosObjectUtxos[m];
if (
thisUtxo.tx_hash === thisSlpUtxo.tx_hash &&
thisUtxo.tx_pos === thisSlpUtxo.tx_pos &&
thisUtxo.value === thisSlpUtxo.value
) {
utxoFound = true;
// goto next utxo
break slpUtxosLoop;
}
}
}
if (k === slpUtxos.length - 1 && !utxoFound) {
// return false
return incrementallyHydratedUtxosIncludesAllUtxosInLatestUtxoApiFetch;
}
}
}
}
// It's possible that hydratedUtxoDetails includes every utxo from the utxos array, but for some reason also includes additional utxos
const utxosInUtxos = getUtxoCount(utxos);
const utxosInIncrementallyHydratedUtxos = getUtxoCount(slpUtxos);
if (
!utxosInUtxos ||
!utxosInIncrementallyHydratedUtxos ||
utxosInUtxos !== utxosInIncrementallyHydratedUtxos
) {
return incrementallyHydratedUtxosIncludesAllUtxosInLatestUtxoApiFetch;
}
// If you make it here, good to go
incrementallyHydratedUtxosIncludesAllUtxosInLatestUtxoApiFetch = true;
return incrementallyHydratedUtxosIncludesAllUtxosInLatestUtxoApiFetch;
};
export const getHashArrayFromWallet = wallet => {
// If the wallet has wallet.Path1899.hash160, it's migrated and will have all of them
// Return false for an umigrated wallet
const hash160Array =
wallet && wallet.Path1899 && 'hash160' in wallet.Path1899
? [
wallet.Path245.hash160,
wallet.Path145.hash160,
wallet.Path1899.hash160,
]
: false;
return hash160Array;
};
export const parseChronikTx = (tx, walletHash160s) => {
const { inputs, outputs } = tx;
// Assign defaults
let incoming = true;
let xecAmount = new BigNumber(0);
let etokenAmount = new BigNumber(0);
const isEtokenTx = 'slpTxData' in tx && typeof tx.slpTxData !== 'undefined';
// Iterate over inputs to see if this is an incoming tx (incoming === true)
for (let i = 0; i < inputs.length; i += 1) {
const thisInput = inputs[i];
const thisInputSendingHash160 = thisInput.outputScript;
for (let j = 0; j < walletHash160s.length; j += 1) {
const thisWalletHash160 = walletHash160s[j];
if (thisInputSendingHash160.includes(thisWalletHash160)) {
// Then this is an outgoing tx
incoming = false;
// Break out of this for loop once you know this is an incoming tx
break;
}
}
}
// Iterate over outputs to get the amount sent
for (let i = 0; i < outputs.length; i += 1) {
const thisOutput = outputs[i];
const thisOutputReceivedAtHash160 = thisOutput.outputScript;
// Find amounts at your wallet's addresses
for (let j = 0; j < walletHash160s.length; j += 1) {
const thisWalletHash160 = walletHash160s[j];
if (thisOutputReceivedAtHash160.includes(thisWalletHash160)) {
// If incoming tx, this is amount received by the user's wallet
// if outgoing tx (incoming === false), then this is a change amount
const thisOutputAmount = new BigNumber(thisOutput.value);
xecAmount = incoming
? xecAmount.plus(thisOutputAmount)
: xecAmount.minus(thisOutputAmount);
// Parse token qty if token tx
// Note: edge case this is a token tx that sends XEC to Cashtab recipient but token somewhere else
if (isEtokenTx) {
try {
const thisEtokenAmount = new BigNumber(
thisOutput.slpToken.amount,
);
etokenAmount = incoming
? etokenAmount.plus(thisEtokenAmount)
: etokenAmount.minus(thisEtokenAmount);
} catch (err) {
// edge case described above; in this case there is zero eToken value for this Cashtab recipient, so add 0
etokenAmount.plus(new BigNumber(0));
}
}
}
}
// Output amounts not at your wallet are sent amounts if !incoming
if (!incoming) {
const thisOutputAmount = new BigNumber(thisOutput.value);
xecAmount = xecAmount.plus(thisOutputAmount);
if (isEtokenTx) {
try {
const thisEtokenAmount = new BigNumber(
thisOutput.slpToken.amount,
);
etokenAmount = etokenAmount.plus(thisEtokenAmount);
} catch (err) {
// NB the edge case described above cannot exist in an outgoing tx
// because the eTokens sent originated from this wallet
}
}
}
}
// Convert from sats to XEC
xecAmount = xecAmount.shiftedBy(-1 * currency.cashDecimals);
// Convert from BigNumber to string
xecAmount = xecAmount.toString();
etokenAmount = etokenAmount.toString();
// Return eToken specific fields if eToken tx
if (isEtokenTx) {
const { slpMeta } = tx.slpTxData;
return {
incoming,
xecAmount,
isEtokenTx,
etokenAmount,
slpMeta,
};
}
// Otherwise do not include these fields
return { incoming, xecAmount, isEtokenTx };
};
export const checkWalletForTokenInfo = (tokenId, wallet) => {
/*
Check wallet for cached information about a given tokenId
Return {decimals: tokenDecimals, name: tokenName, ticker: tokenTicker}
If this tokenId does not exist in wallet, return false
*/
try {
const { tokens } = wallet.state;
for (let i = 0; i < tokens.length; i += 1) {
const thisTokenId = tokens[i].tokenId;
if (tokenId === thisTokenId) {
return {
decimals: tokens[i].info.decimals,
ticker: tokens[i].info.tokenTicker,
name: tokens[i].info.tokenName,
};
}
}
} catch (err) {
return false;
}
return false;
};
export const isActiveWebsocket = ws => {
// Return true if websocket is connected and subscribed
// Otherwise return false
return (
ws !== null &&
ws &&
'_ws' in ws &&
'readyState' in ws._ws &&
ws._ws.readyState === 1 &&
'_subs' in ws &&
ws._subs.length > 0
);
};
diff --git a/web/cashtab/src/utils/validation.js b/web/cashtab/src/utils/validation.js
index 838d5a22f..1439f8ece 100644
--- a/web/cashtab/src/utils/validation.js
+++ b/web/cashtab/src/utils/validation.js
@@ -1,460 +1,459 @@
import BigNumber from 'bignumber.js';
import { currency } from 'components/Common/Ticker.js';
import { fromSatoshisToXec } from 'utils/cashMethods';
import cashaddr from 'ecashaddrjs';
// Validate cash amount
export const shouldRejectAmountInput = (
cashAmount,
selectedCurrency,
fiatPrice,
totalCashBalance,
) => {
// Take cashAmount as input, a string from form input
let error = false;
let testedAmount = new BigNumber(cashAmount);
if (selectedCurrency !== currency.ticker) {
// Ensure no more than currency.cashDecimals decimal places
testedAmount = new BigNumber(fiatToCrypto(cashAmount, fiatPrice));
}
// Validate value for > 0
if (isNaN(testedAmount)) {
error = 'Amount must be a number';
} else if (testedAmount.lte(0)) {
error = 'Amount must be greater than 0';
- } else if (
- testedAmount.lt(fromSatoshisToXec(currency.dustSats).toString())
- ) {
+ } else if (testedAmount.lt(fromSatoshisToXec(currency.dustSats))) {
error = `Send amount must be at least ${fromSatoshisToXec(
currency.dustSats,
).toString()} ${currency.ticker}`;
} else if (testedAmount.gt(totalCashBalance)) {
error = `Amount cannot exceed your ${currency.ticker} balance`;
} else if (!isNaN(testedAmount) && testedAmount.toString().includes('.')) {
if (
testedAmount.toString().split('.')[1].length > currency.cashDecimals
) {
error = `${currency.ticker} transactions do not support more than ${currency.cashDecimals} decimal places`;
}
}
// return false if no error, or string error msg if error
return error;
};
export const fiatToCrypto = (
fiatAmount,
fiatPrice,
cashDecimals = currency.cashDecimals,
) => {
let cryptoAmount = new BigNumber(fiatAmount)
.div(new BigNumber(fiatPrice))
.toFixed(cashDecimals);
return cryptoAmount;
};
export const isValidTokenName = tokenName => {
return (
typeof tokenName === 'string' &&
tokenName.length > 0 &&
tokenName.length < 68
);
};
export const isValidTokenTicker = tokenTicker => {
return (
typeof tokenTicker === 'string' &&
tokenTicker.length > 0 &&
tokenTicker.length < 13
);
};
export const isValidTokenDecimals = tokenDecimals => {
return ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].includes(
tokenDecimals,
);
};
export const isValidTokenInitialQty = (tokenInitialQty, tokenDecimals) => {
const minimumQty = new BigNumber(1 / 10 ** tokenDecimals);
const tokenIntialQtyBig = new BigNumber(tokenInitialQty);
return (
tokenIntialQtyBig.gte(minimumQty) &&
tokenIntialQtyBig.lt(100000000000) &&
tokenIntialQtyBig.dp() <= tokenDecimals
);
};
export const isValidTokenDocumentUrl = tokenDocumentUrl => {
const urlPattern = new RegExp(
'^(https?:\\/\\/)?' + // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
'(\\#[-a-z\\d_]*)?$',
'i',
); // fragment locator
const urlTestResult = urlPattern.test(tokenDocumentUrl);
return (
tokenDocumentUrl === '' ||
(typeof tokenDocumentUrl === 'string' &&
tokenDocumentUrl.length >= 0 &&
tokenDocumentUrl.length < 68 &&
urlTestResult)
);
};
export const isValidTokenStats = tokenStats => {
return (
typeof tokenStats === 'object' &&
'timestampUnix' in tokenStats &&
'documentUri' in tokenStats &&
'containsBaton' in tokenStats &&
'initialTokenQty' in tokenStats &&
'totalMinted' in tokenStats &&
'totalBurned' in tokenStats &&
'circulatingSupply' in tokenStats
);
};
export const isValidCashtabSettings = settings => {
try {
let isValidSettingParams = true;
for (let param in currency.defaultSettings) {
if (
!Object.prototype.hasOwnProperty.call(settings, param) ||
!currency.settingsValidation[param].includes(settings[param])
) {
isValidSettingParams = false;
break;
}
}
const isValid = typeof settings === 'object' && isValidSettingParams;
return isValid;
} catch (err) {
return false;
}
};
export const parseInvalidSettingsForMigration = invalidCashtabSettings => {
// create a copy of the invalidCashtabSettings
let migratedCashtabSettings = invalidCashtabSettings;
// determine if settings are invalid because it is missing a parameter
for (let param in currency.defaultSettings) {
if (
!Object.prototype.hasOwnProperty.call(invalidCashtabSettings, param)
) {
// adds the default setting for only that parameter
migratedCashtabSettings[param] = currency.defaultSettings[param];
}
}
return migratedCashtabSettings;
};
export const isValidContactList = contactList => {
/*
A valid contact list is an array of objects
An empty contact list looks like [{}]
Although a valid contact list does not contain duplicated addresses, this is not checked here.
This is checked for when contacts are added. Duplicate addresses will not break the app if a user
somehow sideloads a contact list with everything valid except some addresses are duplicated.
*/
if (!Array.isArray(contactList)) {
return false;
}
for (let i = 0; i < contactList.length; i += 1) {
const contactObj = contactList[i];
// Must have keys 'address' and 'name'
if (
typeof contactObj === 'object' &&
'address' in contactObj &&
'name' in contactObj
) {
// Address must be a valid XEC address, name must be a string
if (
isValidXecAddress(contactObj.address) &&
typeof contactObj.name === 'string'
) {
continue;
}
return false;
} else {
// Check for empty object in an array of length 1, the default blank contactList
if (
contactObj &&
Object.keys(contactObj).length === 0 &&
Object.getPrototypeOf(contactObj) === Object.prototype &&
contactList.length === 1
) {
// [{}] is valid, default blank
// But a list with random blanks is not valid
return true;
}
return false;
}
}
// If you get here, it's good
return true;
};
export const isValidXecAddress = addr => {
/*
Returns true for a valid XEC address
Valid XEC address:
- May or may not have prefix `ecash:`
- Checksum must validate for prefix `ecash:`
An eToken address is not considered a valid XEC address
*/
if (!addr) {
return false;
}
let isValidXecAddress;
let isPrefixedXecAddress;
// Check for possible prefix
if (addr.includes(':')) {
// Test for 'ecash:' prefix
isPrefixedXecAddress = addr.slice(0, 6) === 'ecash:';
// Any address including ':' that doesn't start explicitly with 'ecash:' is invalid
if (!isPrefixedXecAddress) {
isValidXecAddress = false;
return isValidXecAddress;
}
} else {
isPrefixedXecAddress = false;
}
// If no prefix, assume it is checksummed for an ecash: prefix
const testedXecAddr = isPrefixedXecAddress ? addr : `ecash:${addr}`;
try {
const decoded = cashaddr.decode(testedXecAddr);
if (decoded.prefix === 'ecash') {
isValidXecAddress = true;
}
} catch (err) {
isValidXecAddress = false;
}
return isValidXecAddress;
};
export const isValidEtokenAddress = addr => {
/*
Returns true for a valid eToken address
Valid eToken address:
- May or may not have prefix `etoken:`
- Checksum must validate for prefix `etoken:`
An XEC address is not considered a valid eToken address
*/
if (!addr) {
return false;
}
let isValidEtokenAddress;
let isPrefixedEtokenAddress;
// Check for possible prefix
if (addr.includes(':')) {
// Test for 'etoken:' prefix
isPrefixedEtokenAddress = addr.slice(0, 7) === 'etoken:';
// Any token address including ':' that doesn't start explicitly with 'etoken:' is invalid
if (!isPrefixedEtokenAddress) {
isValidEtokenAddress = false;
return isValidEtokenAddress;
}
} else {
isPrefixedEtokenAddress = false;
}
// If no prefix, assume it is checksummed for an etoken: prefix
const testedEtokenAddr = isPrefixedEtokenAddress ? addr : `etoken:${addr}`;
try {
const decoded = cashaddr.decode(testedEtokenAddr);
if (decoded.prefix === 'etoken') {
isValidEtokenAddress = true;
}
} catch (err) {
isValidEtokenAddress = false;
}
return isValidEtokenAddress;
};
export const isValidXecSendAmount = xecSendAmount => {
// A valid XEC send amount must be a number higher than the app dust limit
return (
xecSendAmount !== null &&
typeof xecSendAmount !== 'undefined' &&
!isNaN(parseFloat(xecSendAmount)) &&
- parseFloat(xecSendAmount) >= fromSatoshisToXec(currency.dustSats)
+ parseFloat(xecSendAmount) >=
+ fromSatoshisToXec(currency.dustSats).toNumber()
);
};
export const isValidUtxo = utxo => {
let isValidUtxo = false;
try {
isValidUtxo =
'height' in utxo &&
typeof utxo.height === 'number' &&
'tx_hash' in utxo &&
typeof utxo.tx_hash === 'string' &&
'tx_pos' in utxo &&
typeof utxo.tx_pos === 'number' &&
'value' in utxo &&
typeof utxo.value === 'number';
} catch (err) {
return false;
}
return isValidUtxo;
};
export const isValidBchApiUtxoObject = bchApiUtxoObject => {
/*
[
{
address: 'string',
utxos: [{}, {}, {}...{}]
},
{
address: 'string',
utxos: [{}, {}, {}...{}]
},
{
address: 'string',
utxos: [{}, {}, {}...{}]
},
]
*/
let isValidBchApiUtxoObject = false;
// Must be an array
if (!Array.isArray(bchApiUtxoObject)) {
return isValidBchApiUtxoObject;
}
// Do not accept an empty array
if (bchApiUtxoObject.length < 1) {
return isValidBchApiUtxoObject;
}
for (let i = 0; i < bchApiUtxoObject.length; i += 1) {
let thisUtxoObject = bchApiUtxoObject[i];
if ('address' in thisUtxoObject && 'utxos' in thisUtxoObject) {
const thisUtxoArray = thisUtxoObject.utxos;
if (Array.isArray(thisUtxoArray)) {
// do not validate each individual utxo in the array
// we are only validating the object structure here
continue;
} else {
return isValidBchApiUtxoObject;
}
} else {
return isValidBchApiUtxoObject;
}
}
isValidBchApiUtxoObject = true;
return isValidBchApiUtxoObject;
};
export const isValidEtokenBurnAmount = (tokenBurnAmount, maxAmount) => {
// A valid eToken burn amount must be between 1 and the wallet's token balance
return (
tokenBurnAmount !== null &&
maxAmount !== null &&
typeof tokenBurnAmount !== 'undefined' &&
typeof maxAmount !== 'undefined' &&
new BigNumber(tokenBurnAmount).gt(0) &&
new BigNumber(tokenBurnAmount).lte(maxAmount)
);
};
// XEC airdrop field validations
export const isValidTokenId = tokenId => {
// disable no-useless-escape for regex
//eslint-disable-next-line
const format = /[ `!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~]/;
const specialCharCheck = format.test(tokenId);
return (
typeof tokenId === 'string' &&
tokenId.length === 64 &&
tokenId.trim() != '' &&
!specialCharCheck
);
};
export const isValidNewWalletNameLength = newWalletName => {
return (
typeof newWalletName === 'string' &&
newWalletName.length > 0 &&
newWalletName.length <= currency.localStorageMaxCharacters &&
newWalletName.length !== ''
);
};
export const isValidXecAirdrop = xecAirdrop => {
return (
typeof xecAirdrop === 'string' &&
xecAirdrop.length > 0 &&
xecAirdrop.trim() != '' &&
new BigNumber(xecAirdrop).gt(0)
);
};
export const isValidAirdropOutputsArray = airdropOutputsArray => {
if (!airdropOutputsArray) {
return false;
}
let isValid = true;
// split by individual rows
const addressStringArray = airdropOutputsArray.split('\n');
for (let i = 0; i < addressStringArray.length; i++) {
const substring = addressStringArray[i].split(',');
let valueString = substring[1];
// if the XEC being sent is less than dust sats or contains extra values per line
if (
new BigNumber(valueString).lt(
fromSatoshisToXec(currency.dustSats),
) ||
substring.length !== 2
) {
isValid = false;
}
}
return isValid;
};
export const isValidAirdropExclusionArray = airdropExclusionArray => {
if (!airdropExclusionArray || airdropExclusionArray.length === 0) {
return false;
}
let isValid = true;
// split by comma as the delimiter
const addressStringArray = airdropExclusionArray.split(',');
// parse and validate each address in array
for (let i = 0; i < addressStringArray.length; i++) {
if (!isValidXecAddress(addressStringArray[i])) {
return false;
}
}
return isValid;
};

File Metadata

Mime Type
text/x-diff
Expires
Sun, Apr 27, 12:40 (16 h, 6 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5573576
Default Alt Text
(239 KB)

Event Timeline