Page MenuHomePhabricator

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/web/cashtab/src/components/Alias/Alias.js b/web/cashtab/src/components/Alias/Alias.js
index f562ff68b..2fd61cae1 100644
--- a/web/cashtab/src/components/Alias/Alias.js
+++ b/web/cashtab/src/components/Alias/Alias.js
@@ -1,455 +1,340 @@
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { WalletContext } from 'utils/context';
import PropTypes from 'prop-types';
import WalletLabel from 'components/Common/WalletLabel.js';
import {
ZeroBalanceHeader,
SidePaddingCtn,
WalletInfoCtn,
} from 'components/Common/Atoms';
import { AntdFormWrapper } from 'components/Common/EnhancedInputs';
import { Form, Input } from 'antd';
import { SmartButton } from 'components/Common/PrimaryButton';
import BalanceHeader from 'components/Common/BalanceHeader';
import BalanceHeaderFiat from 'components/Common/BalanceHeaderFiat';
import { Row, Col } from 'antd';
import { UserOutlined } from '@ant-design/icons';
import {
getWalletState,
fromSatoshisToXec,
getAliasRegistrationFee,
convertToEcashPrefix,
} from 'utils/cashMethods';
-import {
- isAliasAvailable,
- isAddressRegistered,
- getAllTxHistory,
- getOnchainAliasTxCount,
-} from 'utils/chronik';
+import { isAliasAvailable, isAddressRegistered } from 'utils/chronik';
import { currency } from 'components/Common/Ticker.js';
import { registerNewAlias } from 'utils/transactions';
import {
sendXecNotification,
errorNotification,
} from 'components/Common/Notifications';
+import { isAliasFormat } from 'utils/validation';
export const NamespaceCtn = styled.div`
width: 100%;
margin-top: 50px;
margin-bottom: 20px;
overflow-wrap: break-word;
h2 {
color: ${props => props.theme.contrast};
margin: 0 0 20px;
}
h3 {
color: ${props => props.theme.contrast};
margin: 0 0 10px;
}
white-space: pre-wrap;
`;
const StyledSpacer = styled.div`
height: 1px;
width: 100%;
background-color: ${props => props.theme.lightWhite};
margin: 60px 0 50px;
`;
const Alias = ({ passLoadingStatus }) => {
const ContextValue = React.useContext(WalletContext);
const {
wallet,
fiatPrice,
cashtabSettings,
chronik,
changeCashtabSettings,
- getAliasesFromLocalForage,
- updateAliases,
+ synchronizeAliasCache,
} = ContextValue;
const walletState = getWalletState(wallet);
const { balances, nonSlpUtxos } = walletState;
const [formData, setFormData] = useState({
aliasName: '',
});
const [isValidAliasInput, setIsValidAliasInput] = useState(false); // tracks whether to activate the registration button
const [activeWalletAliases, setActiveWalletAliases] = useState([]); // stores the list of aliases registered to this active wallet
useEffect(() => {
passLoadingStatus(false);
}, [balances.totalBalance]);
useEffect(async () => {
// only run this useEffect block if wallet is defined
if (!wallet || typeof wallet === 'undefined') {
return;
}
passLoadingStatus(true);
+ // check if alias cache is sync'ed with onchain tx count, if not, update
let cachedAliases;
- // retrieve cached aliases
try {
- cachedAliases = await getAliasesFromLocalForage();
+ cachedAliases = await synchronizeAliasCache(chronik);
} catch (err) {
- console.log(`Error retrieving aliases from local forage`, err);
- }
-
- // if alias cache exists, check if partial tx history retrieval is required
- if (cachedAliases && cachedAliases.paymentTxHistory.length > 0) {
- // get cached tx count
- const cachedAliasTxCount = cachedAliases.totalPaymentTxCount;
-
- // temporary log for reviewer
- console.log(`cached Alias Tx Count: `, cachedAliasTxCount);
-
- // get onchain tx count
- let onchainAliasTxCount = await getOnchainAliasTxCount(chronik);
-
- // temporary log for reviewer
- console.log(`onchain Alias Tx Count: `, onchainAliasTxCount);
-
- // condition where a partial alias tx history refresh is required
- if (cachedAliasTxCount !== onchainAliasTxCount) {
- // temporary log for reviewer
- console.log(`partial tx history retrieval required`);
-
- const onchainPages = Math.ceil(
- cachedAliasTxCount / currency.chronikTxsPerPage,
- );
-
- // execute a partial tx history retrieval instead of full history
- const pagesToTraverse = Math.ceil(
- onchainPages -
- cachedAliasTxCount / currency.chronikTxsPerPage,
- ); // how many pages to traverse backwards via chronik
- const partialAliasPaymentTxHistory = await getAllTxHistory(
- chronik,
- currency.aliasSettings.aliasPaymentHash160,
- pagesToTraverse,
- );
-
- // temporary log for reviewer
- console.log(
- `partial txs retrieved: `,
- partialAliasPaymentTxHistory.length,
- );
-
- // update cache with the latest alias transactions
- let allTxHistory = cachedAliases.paymentTxHistory; // starting point is what's currently cached
-
- if (allTxHistory) {
- // only concat non-duplicate entries from the partial tx history retrieval
- partialAliasPaymentTxHistory.forEach(element => {
- if (
- !JSON.stringify(allTxHistory).includes(
- JSON.stringify(element.txid),
- )
- ) {
- allTxHistory = allTxHistory.concat(element);
- // temporary log for reviewer
- console.log(
- `${element.txid} appended to allTxHistory`,
- );
- }
- });
- }
-
- // update cached alias list
- // updateAliases() handles the extraction of the aliases and generates the expected JSON format
- await updateAliases(allTxHistory);
-
- // temporary console log for reviewer
- console.log(`alias cache update complete`);
- } else {
- // temporary console log for reviewer
- console.log(
- `cachedAliases exist however partial alias cache refresh NOT required`,
- );
- }
- } else {
- // first time loading Alias, execute full tx history retrieval
- // temporary console log for reviewer
- console.log(
- `Alias.js: cachedAliases DOES NOT exist, retrieving full tx history`,
- );
-
- // get latest tx count for payment address
- const aliasPaymentTxHistory = await getAllTxHistory(
- chronik,
- currency.aliasSettings.aliasPaymentHash160,
- );
- const totalPaymentTxCount = aliasPaymentTxHistory.length;
-
- // temporary log for reviewer
- console.log(`onchain totalPaymentTxCount: ${totalPaymentTxCount}`);
-
- // temporary console log for reviewer
- if (cachedAliases) {
- console.log(
- `cached totalPaymentTxCount: `,
- cachedAliases.totalPaymentTxCount,
- );
- }
-
- // conditions where an alias refresh is required
- if (
- !cachedAliases ||
- !cachedAliases.totalPaymentTxCount ||
- cachedAliases.totalPaymentTxCount < totalPaymentTxCount
- ) {
- // temporary console log for reviewer
- console.log(`alias cache refresh required`);
-
- try {
- // update cached alias list
- // updateAliases() handles the extraction of the alias and generates the expected JSON format
- await updateAliases(aliasPaymentTxHistory);
-
- // temporary console log for reviewer
- console.log(`alias cache update complete`);
- } catch (err) {
- console.log(`Error updating alias cache in Alias.js`, err);
- }
- } else {
- // temporary console log for reviewer
- console.log(`alias cache refresh NOT required`);
- }
+ console.log(`Error synchronizing alias cache in Alias.js`, err);
}
// check whether the address is attached to an onchain alias on page load
const walletHasAlias = isAddressRegistered(wallet, cachedAliases);
// temporary console log for reviewer
console.log(
'Does this active wallet have an onchain alias? : ' +
walletHasAlias,
);
// retrieve aliases for this active wallet from cache for rendering on the frontend
if (
walletHasAlias &&
cachedAliases &&
cachedAliases.aliases.length > 0
) {
const thisAddress = convertToEcashPrefix(
wallet.Path1899.cashAddress,
);
// filter for aliases that matches this wallet's address
const registeredAliasesToWallet = cachedAliases.aliases.filter(
alias => alias.address === thisAddress,
);
setActiveWalletAliases(registeredAliasesToWallet);
}
passLoadingStatus(false);
}, [wallet.name]);
const registerAlias = async () => {
passLoadingStatus(true);
// note: input already validated via handleAliasNameInput()
const aliasInput = formData.aliasName;
+ // check if the user is trying to essentially register chicken.xec.xec
+ const doubleExtensionInput = isAliasFormat(aliasInput);
+ if (doubleExtensionInput) {
+ errorNotification(
+ null,
+ 'Please input an alias without the ".xec"',
+ 'Alias extension check',
+ );
+ passLoadingStatus(false);
+ return;
+ }
+
const aliasAvailable = await isAliasAvailable(chronik, aliasInput);
if (aliasAvailable) {
// calculate registration fee based on chars
const registrationFee = getAliasRegistrationFee(aliasInput);
console.log(
'Registration fee for ' +
aliasInput +
' is ' +
registrationFee +
' sats.',
);
console.log(
`Alias ${aliasInput} is available. Broadcasting registration transaction.`,
);
try {
const link = await registerNewAlias(
chronik,
wallet,
nonSlpUtxos,
currency.defaultFee,
aliasInput,
fromSatoshisToXec(registrationFee),
);
sendXecNotification(link);
} catch (err) {
handleAliasRegistrationError(err);
}
setIsValidAliasInput(true);
// set alias as pending until subsequent websocket notification on 1 conf on the registration tx
let tempactiveWalletAliases = activeWalletAliases;
const thisAddress = convertToEcashPrefix(
wallet.Path1899.cashAddress,
);
tempactiveWalletAliases.push({
alias: `${aliasInput} (Pending)`,
address: thisAddress,
});
setActiveWalletAliases(tempactiveWalletAliases);
} else {
// error notification on alias being unavailable
errorNotification(
null,
'This alias [' +
aliasInput +
'] has already been taken, please try another alias',
'Alias availability check',
);
}
passLoadingStatus(false);
};
const handleAliasNameInput = e => {
const { name, value } = e.target;
if (value && value.trim() !== '') {
setIsValidAliasInput(true);
} else {
setIsValidAliasInput(false);
}
setFormData(p => ({
...p,
[name]: value,
}));
};
function handleAliasRegistrationError(errorObj) {
// 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.error.includes(
'too-long-mempool-chain, too many unconfirmed ancestors [limit: 50] (code 64)',
)
) {
message = `The address you are trying to register has too many unconfirmed ancestors (limit 50). Registration will be possible after a block confirmation. Try again in about 10 minutes.`;
} else {
message =
errorObj.message || errorObj.error || JSON.stringify(errorObj);
}
errorNotification(errorObj, message, 'Registering Alias');
}
return (
<>
<WalletInfoCtn>
<WalletLabel
name={wallet.name}
cashtabSettings={cashtabSettings}
changeCashtabSettings={changeCashtabSettings}
></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}
cashtabSettings={cashtabSettings}
/>
{fiatPrice !== null && (
<BalanceHeaderFiat
balance={balances.totalBalance}
settings={cashtabSettings}
fiatPrice={fiatPrice}
/>
)}
</>
)}
</WalletInfoCtn>
<SidePaddingCtn>
<Row type="flex">
<Col span={24}>
<NamespaceCtn>
<h2>eCash Namespace Alias</h2>
</NamespaceCtn>
<SidePaddingCtn>
<AntdFormWrapper>
<Form
style={{
width: 'auto',
}}
>
<Form.Item>
<Input
addonAfter=" . xec"
placeholder="Enter a desired alias"
name="aliasName"
maxLength={
currency.aliasSettings
.aliasMaxLength
}
onChange={e =>
handleAliasNameInput(e)
}
/>
</Form.Item>
<Form.Item>
<SmartButton
disabled={!isValidAliasInput}
onClick={() => registerAlias()}
>
Register Alias
</SmartButton>
</Form.Item>
</Form>
</AntdFormWrapper>
<StyledSpacer />
<NamespaceCtn>
<h3>
<p>
<UserOutlined />
&emsp;Registered aliases
</p>
{activeWalletAliases &&
activeWalletAliases.length > 0
? activeWalletAliases
.map(
alias => alias.alias + '.xec',
)
.join('\n')
: 'N/A'}
</h3>
</NamespaceCtn>
</SidePaddingCtn>
</Col>
</Row>
</SidePaddingCtn>
</>
);
};
/*
passLoadingStatus must receive a default prop that is a function
in order to pass the rendering unit test in Alias.test.js
status => {console.log(status)} is an arbitrary stub function
*/
Alias.defaultProps = {
passLoadingStatus: status => {
console.log(status);
},
};
Alias.propTypes = {
passLoadingStatus: PropTypes.func,
};
export default Alias;
diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js
index b2f30ee8b..f0bafaf3b 100644
--- a/web/cashtab/src/components/Send/Send.js
+++ b/web/cashtab/src/components/Send/Send.js
@@ -1,1002 +1,1047 @@
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,
} from 'components/Common/EnhancedInputs';
import { ThemedMailOutlined } from 'components/Common/CustomIcons';
import { CustomCollapseCtn } from 'components/Common/StyledCollapse';
import { Form, message, Modal, Alert, Input } from 'antd';
import { Row, Col, Switch } from 'antd';
import PrimaryButton, { DisabledButton } from 'components/Common/PrimaryButton';
import useWindowDimensions from 'hooks/useWindowDimensions';
import {
sendXecNotification,
errorNotification,
} from 'components/Common/Notifications';
import { isMobile, isIOS, isSafari } from 'react-device-detect';
import { currency, parseAddressForParams } from 'components/Common/Ticker.js';
import { Event } from 'utils/GoogleAnalytics';
import {
fiatToCrypto,
shouldRejectAmountInput,
isValidXecAddress,
isValidEtokenAddress,
isValidXecSendAmount,
+ isAliasFormat,
} 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, fromSatoshisToXec, calcFee } from 'utils/cashMethods';
import { sendXec } from 'utils/transactions';
import ApiError from 'components/Common/ApiError';
import { formatFiatBalance, formatBalance } from 'utils/formatting';
import styled from 'styled-components';
import WalletLabel from 'components/Common/WalletLabel.js';
+import { getAddressFromAlias } from 'utils/chronik';
const { TextArea } = Input;
const TextAreaLabel = styled.div`
text-align: left;
color: ${props => props.theme.forms.text};
padding-left: 1px;
white-space: nowrap;
`;
const AmountPreviewCtn = styled.div`
display: flex;
flex-direction: column;
justify-content: top;
max-height: 1rem;
`;
const SendInputCtn = styled.div`
.ant-form-item-explain-error {
@media (max-width: 300px) {
font-size: 12px;
}
}
`;
const LocaleFormattedValue = styled.h3`
color: ${props => props.theme.contrast};
font-weight: bold;
margin-bottom: 0;
`;
const SendAddressHeader = styled.div`
display: flex;
align-items: center;
`;
const DestinationAddressSingleCtn = styled.div``;
const DestinationAddressMultiCtn = styled.div``;
const ExpandingAddressInputCtn = styled.div`
min-height: 14rem;
${DestinationAddressSingleCtn} {
overflow: hidden;
transition: ${props =>
props.open
? 'max-height 200ms ease-in, opacity 200ms ease-out'
: 'max-height 200ms cubic-bezier(0, 1, 0, 1), opacity 200ms ease-in'};
max-height: ${props => (props.open ? '0rem' : '12rem')};
opacity: ${props => (props.open ? 0 : 1)};
}
${DestinationAddressMultiCtn} {
overflow: hidden;
transition: ${props =>
props.open
? 'max-height 200ms ease-in, transform 200ms ease-out, opacity 200ms ease-in'
: 'max-height 200ms cubic-bezier(0, 1, 0, 1), transform 200ms ease-out'};
max-height: ${props => (props.open ? '13rem' : '0rem')};
transform: ${props =>
props.open ? 'translateY(0%)' : 'translateY(100%)'};
opacity: ${props => (props.open ? 1 : 0)};
}
`;
const PanelHeaderCtn = styled.div`
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
`;
const SendBCH = ({ 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,
changeCashtabSettings,
chronik,
+ getAliasesFromLocalForage,
} = ContextValue;
const walletState = getWalletState(wallet);
const { balances, nonSlpUtxos } = walletState;
// Modal settings
const [isOneToManyXECSend, setIsOneToManyXECSend] = useState(false);
const [opReturnMsg, setOpReturnMsg] = useState(false);
const [isEncryptedOptionalOpReturnMsg, setIsEncryptedOptionalOpReturnMsg] =
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 [aliasInputAddress, setAliasInputAddress] = 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 [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);
};
// 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(() => {
// 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).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.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 {
const link = await sendXec(
chronik,
wallet,
nonSlpUtxos,
currency.defaultFee,
opReturnMsg,
true, // indicate send mode is one to many
addressAndValueArray,
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];
-
+ let cleanAddress;
+ // check state on whether this is an alias or ecash address
+ if (aliasInputAddress) {
+ cleanAddress = aliasInputAddress;
+ // temporary log for reviewer
+ console.log(
+ `parsed address for ${address} is: ${cleanAddress}`,
+ );
+ } else {
+ // Get the non-alias param-free address
+ cleanAddress = address.split('?')[0];
+ }
// 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(
chronik,
wallet,
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 handleAddressChange = async e => {
const { value, name } = e.target;
let error = false;
let addressString = value;
// parse address for parameters
const addressInfo = parseAddressForParams(addressString);
+ const { address, queryString, amount } = addressInfo;
+
// validate address
const isValid = isValidXecAddress(addressInfo.address);
- 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 address`;
// If valid address but token format
if (isValidEtokenAddress(address)) {
error = `eToken addresses are not supported for ${currency.ticker} sends`;
}
}
+
+ // if input is invalid as an ecash address, check if it's a valid alias
+ // otherwise the invalid address error above will be displayed
+ const isAliasInput = isAliasFormat(address);
+ if (isAliasInput) {
+ // reset the invalid address check from above
+ error = false;
+
+ // extract alias without the `.xec`
+ const aliasName = address.slice(0, address.length - 4);
+ // extract alias address from cache
+ const aliasCacheObj = await getAliasesFromLocalForage();
+
+ const aliasAddress = getAddressFromAlias(
+ aliasName,
+ aliasCacheObj.aliases,
+ );
+
+ // if not found in alias cache, display input error
+ if (!aliasAddress) {
+ error = 'eCash Alias does not exist';
+ setAliasInputAddress(false);
+ } else {
+ // otherwise set parsed address to state for use in Send()
+ setAliasInputAddress(aliasAddress);
+ }
+ }
+
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 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 = 'Ensure each XEC address is valid';
setSendBchAddressError(error);
return setFormData(p => ({
...p,
[name]: value,
}));
}
if (!validValueString) {
error = 'Ensure each tx is at least 5.5 XEC';
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 onMax = async () => {
// Clear amt error
setSendBchAmountError(false);
// Set currency to BCH
setSelectedCurrency(currency.ticker);
try {
const txFeeSats = calcFee(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"
open={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}
cashtabSettings={cashtabSettings}
changeCashtabSettings={changeCashtabSettings}
></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}
cashtabSettings={cashtabSettings}
/>
<BalanceHeaderFiat
balance={balances.totalBalance}
settings={cashtabSettings}
fiatPrice={fiatPrice}
/>
</>
)}
</WalletInfoCtn>
<SidePaddingCtn>
<Row type="flex">
<Col span={24}>
<Form
style={{
width: 'auto',
marginTop: '40px',
}}
>
<SendAddressHeader>
{' '}
<FormLabel>Send to</FormLabel>
<TextAreaLabel>
Multiple Recipients:&nbsp;&nbsp;
<Switch
defaultunchecked="true"
checked={isOneToManyXECSend}
onChange={() => {
setIsOneToManyXECSend(
!isOneToManyXECSend,
);
setIsEncryptedOptionalOpReturnMsg(
false,
);
}}
style={{
marginBottom: '7px',
}}
/>
</TextAreaLabel>
</SendAddressHeader>
<ExpandingAddressInputCtn open={isOneToManyXECSend}>
<SendInputCtn>
<DestinationAddressSingleCtn>
<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: `Address`,
+ placeholder: currency
+ .aliasSettings.aliasEnabled
+ ? `Address or Alias`
+ : `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>
</DestinationAddressSingleCtn>
{priceApiError && (
<AlertMsg>
Error fetching fiat price. Setting
send by{' '}
{currency.fiatCurrencies[
cashtabSettings.fiatCurrency
].slug.toUpperCase()}{' '}
disabled
</AlertMsg>
)}
</SendInputCtn>
<>
<DestinationAddressMultiCtn>
<DestinationAddressMulti
validateStatus={
sendBchAddressError
? 'error'
: ''
}
help={
sendBchAddressError
? sendBchAddressError
: ''
}
inputProps={{
placeholder: `One 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>
</DestinationAddressMultiCtn>
</>
<AmountPreviewCtn>
{!priceApiError && !isOneToManyXECSend && (
<>
<LocaleFormattedValue>
{formatBalance(
formData.value,
userLocale,
)}{' '}
{selectedCurrency}
</LocaleFormattedValue>
<ConvertAmount>
{fiatPriceString !== '' && '='}{' '}
{fiatPriceString}
</ConvertAmount>
</>
)}
</AmountPreviewCtn>
</ExpandingAddressInputCtn>
{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: '1rem',
}}
>
{!balances.totalBalance ||
apiError ||
sendBchAmountError ||
sendBchAddressError ||
priceApiError ? (
<DisabledButton>Send</DisabledButton>
) : (
<>
{txInfoFromUrl ? (
<PrimaryButton
onClick={() =>
checkForConfirmationBeforeSendXec()
}
>
Send
</PrimaryButton>
) : (
<PrimaryButton
onClick={() => {
checkForConfirmationBeforeSendXec();
}}
>
Send
</PrimaryButton>
)}
</>
)}
</div>
<CustomCollapseCtn
panelHeader={
<PanelHeaderCtn>
<ThemedMailOutlined /> Message
</PanelHeaderCtn>
}
optionalDefaultActiveKey={
location &&
location.state &&
location.state.replyAddress
? ['1']
: ['0']
}
optionalKey="1"
>
<AntdFormWrapper
style={{
marginBottom: '20px',
}}
>
<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)`
: location &&
location.state &&
location.state.airdropTokenId
? `(max ${currency.opReturn.unencryptedAirdropMsgCharLimit} 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
: location &&
location.state &&
location.state.airdropTokenId
? currency.opReturn
.unencryptedAirdropMsgCharLimit
: currency.opReturn
.unencryptedMsgCharLimit
}
onKeyDown={e =>
e.keyCode == 13
? e.preventDefault()
: ''
}
/>
</AntdFormWrapper>
</CustomCollapseCtn>
{apiError && <ApiError />}
</Form>
</Col>
</Row>
</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 = {
passLoadingStatus: PropTypes.func,
};
export default SendBCH;
diff --git a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap
index ab92c3701..81c5a589b 100644
--- a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap
+++ b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap
@@ -1,2902 +1,2902 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP @generated
+// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Wallet with BCH balances 1`] = `
Array [
<div
className="sc-gipzik gABIqL"
>
<div
className="sc-gisBJw eendT"
>
<h4
className="sc-kjoXOD eJukkq"
>
MigrationTestAlpha
</h4>
<a
href="/configure"
onClick={[Function]}
>
<svg
className="sc-jTzLTM bTdWCF"
>
edit.svg
</svg>
</a>
<div>
<button
aria-checked={false}
className="ant-switch ant-switch-small"
onClick={[Function]}
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="switch"
type="button"
>
<div
className="ant-switch-handle"
/>
<span
className="ant-switch-inner"
>
<svg
className="sc-kAzzGY fUTkYj"
>
eye-invisible.svg
</svg>
</span>
</button>
</div>
</div>
<div
className="sc-bRBYWo euMChu"
>
You currently have 0
XEC
<br />
Deposit some funds to use this feature
</div>
</div>,
<div
className="sc-iRbamj jTHqpF"
>
<div
className="ant-row"
style={Object {}}
type="flex"
>
<div
className="ant-col ant-col-24"
style={Object {}}
>
<form
className="ant-form ant-form-horizontal"
onReset={[Function]}
onSubmit={[Function]}
style={
Object {
"marginTop": "40px",
"width": "auto",
}
}
>
<div
className="sc-ksYbfQ jGqaXi"
>
<label
className="sc-jlyJG kTtgoZ"
>
Send to
</label>
<div
className="sc-cHGsZl cjKbMT"
>
Multiple Recipients:  
<button
aria-checked={false}
className="ant-switch"
defaultunchecked="true"
onClick={[Function]}
onKeyDown={[Function]}
role="switch"
style={
Object {
"marginBottom": "7px",
}
}
type="button"
>
<div
className="ant-switch-handle"
/>
<span
className="ant-switch-inner"
/>
</button>
</div>
</div>
<div
className="sc-kvZOFW fusohh"
open={false}
>
<div
className="sc-kgAjT ewxwtD"
>
<div
className="sc-hmzhuo eRtqMp"
>
<div
className="sc-eNQAEJ kPLQzL"
>
<div
className="ant-form-item ant-form-item-with-help"
style={
Object {
"marginBottom": "0px",
}
}
>
<div
className="ant-row ant-form-item-row"
style={Object {}}
>
<div
className="ant-col ant-form-item-control"
style={Object {}}
>
<div
className="ant-form-item-control-input"
>
<div
className="ant-form-item-control-input-content"
>
<span
className="ant-input-group-wrapper"
>
<span
className="ant-input-wrapper ant-input-group"
>
<span
className="ant-input-affix-wrapper"
hidden={null}
onClick={[Function]}
style={null}
>
<span
className="ant-input-prefix"
>
<span
aria-label="wallet"
className="anticon anticon-wallet sc-bxivhb iBBRHU"
role="img"
>
<svg
aria-hidden="true"
data-icon="wallet"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 464H528V448h312v128zm0 264H184V184h656v200H496c-17.7 0-32 14.3-32 32v192c0 17.7 14.3 32 32 32h344v200zM580 512a40 40 0 1080 0 40 40 0 10-80 0z"
/>
</svg>
</span>
</span>
<input
autoComplete="off"
className="ant-input"
hidden={null}
name="address"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="Address"
required={true}
style={null}
type="text"
value=""
/>
</span>
<span
className="ant-input-group-addon"
>
<span
className="sc-dxgOiQ fraLQa"
onClick={[Function]}
>
<span
aria-label="qrcode"
className="anticon anticon-qrcode sc-ifAKCX jqgRJL"
role="img"
>
<svg
aria-hidden="true"
data-icon="qrcode"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M468 128H160c-17.7 0-32 14.3-32 32v308c0 4.4 3.6 8 8 8h332c4.4 0 8-3.6 8-8V136c0-4.4-3.6-8-8-8zm-56 284H192V192h220v220zm-138-74h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm194 210H136c-4.4 0-8 3.6-8 8v308c0 17.7 14.3 32 32 32h308c4.4 0 8-3.6 8-8V556c0-4.4-3.6-8-8-8zm-56 284H192V612h220v220zm-138-74h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm590-630H556c-4.4 0-8 3.6-8 8v332c0 4.4 3.6 8 8 8h332c4.4 0 8-3.6 8-8V160c0-17.7-14.3-32-32-32zm-32 284H612V192h220v220zm-138-74h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm194 210h-48c-4.4 0-8 3.6-8 8v134h-78V556c0-4.4-3.6-8-8-8H556c-4.4 0-8 3.6-8 8v332c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V644h78v102c0 4.4 3.6 8 8 8h190c4.4 0 8-3.6 8-8V556c0-4.4-3.6-8-8-8zM746 832h-48c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zm142 0h-48c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
</span>
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<label
className="sc-jlyJG kTtgoZ"
>
Amount
</label>
<div
className="sc-eNQAEJ kPLQzL"
>
<div
className="ant-form-item ant-form-item-with-help"
>
<div
className="ant-row ant-form-item-row"
style={Object {}}
>
<div
className="ant-col ant-form-item-control"
style={Object {}}
>
<div
className="ant-form-item-control-input"
>
<div
className="ant-form-item-control-input-content"
>
<span
className="ant-input-group ant-input-group-compact"
>
<span
className="ant-input-affix-wrapper"
onClick={[Function]}
style={
Object {
"textAlign": "left",
"width": "60%",
}
}
>
<span
className="ant-input-prefix"
>
<img
alt=""
height={16}
src="logo_primary.png"
width={16}
/>
</span>
<input
className="ant-input"
dollar={0}
hidden={null}
name="value"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onWheel={[Function]}
placeholder="Amount"
required={true}
step={0.01}
style={null}
type="number"
value=""
/>
</span>
<div
className="ant-select select-after ant-select-single ant-select-show-arrow"
onBlur={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
style={
Object {
"width": "30%",
}
}
>
<div
className="ant-select-selector"
onClick={[Function]}
onMouseDown={[Function]}
>
<span
className="ant-select-selection-search"
>
<input
aria-activedescendant="rc_select_TEST_OR_SSR_list_0"
aria-autocomplete="list"
aria-controls="rc_select_TEST_OR_SSR_list"
aria-haspopup="listbox"
aria-owns="rc_select_TEST_OR_SSR_list"
autoComplete="off"
className="ant-select-selection-search-input"
disabled={false}
id="rc_select_TEST_OR_SSR"
onChange={[Function]}
onCompositionEnd={[Function]}
onCompositionStart={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
onPaste={[Function]}
readOnly={true}
role="combobox"
style={
Object {
"opacity": 0,
}
}
type="search"
unselectable="on"
value=""
/>
</span>
<span
className="ant-select-selection-item"
title="XEC"
>
XEC
</span>
</div>
<span
aria-hidden={true}
className="ant-select-arrow"
onMouseDown={[Function]}
style={
Object {
"WebkitUserSelect": "none",
"userSelect": "none",
}
}
unselectable="on"
>
<span
aria-label="down"
className="anticon anticon-down ant-select-suffix"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>
</div>
<span
className="sc-kEYyzF fyKOGM"
disabled={false}
onClick={[Function]}
style={
Object {
"height": "55px",
"lineHeight": "55px",
"width": "10%",
}
}
>
max
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
className="sc-frDJqD gCEYuZ"
>
<div
className="sc-eNQAEJ kPLQzL"
>
<div
className="ant-form-item ant-form-item-with-help"
>
<div
className="ant-row ant-form-item-row"
style={Object {}}
>
<div
className="ant-col ant-form-item-control"
style={Object {}}
>
<div
className="ant-form-item-control-input"
>
<div
className="ant-form-item-control-input-content"
>
<textarea
autoComplete="off"
className="ant-input"
disabled={false}
name="address"
onChange={[Function]}
onCompositionEnd={[Function]}
onCompositionStart={[Function]}
onKeyDown={[Function]}
placeholder="One address & value per line, separated by comma
e.g.
ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8,500
ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,700"
prefix={<Styled(Component) />}
required={true}
style={
Object {
"height": "189px",
}
}
value=""
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
className="sc-TOsTZ iykQNe"
>
<h3
className="sc-cJSrbW kPKfAt"
>
0
XEC
</h3>
<div
className="sc-fMiknA fhbntc"
>
=
$ NaN USD
</div>
</div>
</div>
<div
style={
Object {
"paddingTop": "1rem",
}
}
>
<button
className="sc-cMljjf cgwtph"
>
Send
</button>
</div>
<div
className="sc-hSdWYo iRCZlN"
>
<div
className="ant-collapse ant-collapse-icon-position-start sc-cvbbAY eZzhXj"
role={null}
style={
Object {
"marginBottom": "24px",
}
}
>
<div
className="ant-collapse-item"
>
<div
aria-disabled={false}
aria-expanded={false}
className="ant-collapse-header"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0}
>
<div
className="ant-collapse-expand-icon"
onClick={null}
>
<span
aria-label="right"
className="anticon anticon-right ant-collapse-arrow"
role="img"
>
<svg
aria-hidden="true"
data-icon="right"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
/>
</svg>
</span>
</div>
<span
className="ant-collapse-header-text"
onClick={null}
>
<div
className="sc-iAyFgw gPlgsX"
>
<div
className="sc-hqyNC cNBzUr"
>
<svg
className="sc-jzJRlG kLfsyF"
>
mail.svg
</svg>
Message
</div>
</div>
</span>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>,
]
`;
exports[`Wallet with BCH balances and tokens 1`] = `
Array [
<div
className="sc-gipzik gABIqL"
>
<div
className="sc-gisBJw eendT"
>
<h4
className="sc-kjoXOD eJukkq"
>
MigrationTestAlpha
</h4>
<a
href="/configure"
onClick={[Function]}
>
<svg
className="sc-jTzLTM bTdWCF"
>
edit.svg
</svg>
</a>
<div>
<button
aria-checked={false}
className="ant-switch ant-switch-small"
onClick={[Function]}
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="switch"
type="button"
>
<div
className="ant-switch-handle"
/>
<span
className="ant-switch-inner"
>
<svg
className="sc-kAzzGY fUTkYj"
>
eye-invisible.svg
</svg>
</span>
</button>
</div>
</div>
<div
className="sc-bRBYWo euMChu"
>
You currently have 0
XEC
<br />
Deposit some funds to use this feature
</div>
</div>,
<div
className="sc-iRbamj jTHqpF"
>
<div
className="ant-row"
style={Object {}}
type="flex"
>
<div
className="ant-col ant-col-24"
style={Object {}}
>
<form
className="ant-form ant-form-horizontal"
onReset={[Function]}
onSubmit={[Function]}
style={
Object {
"marginTop": "40px",
"width": "auto",
}
}
>
<div
className="sc-ksYbfQ jGqaXi"
>
<label
className="sc-jlyJG kTtgoZ"
>
Send to
</label>
<div
className="sc-cHGsZl cjKbMT"
>
Multiple Recipients:  
<button
aria-checked={false}
className="ant-switch"
defaultunchecked="true"
onClick={[Function]}
onKeyDown={[Function]}
role="switch"
style={
Object {
"marginBottom": "7px",
}
}
type="button"
>
<div
className="ant-switch-handle"
/>
<span
className="ant-switch-inner"
/>
</button>
</div>
</div>
<div
className="sc-kvZOFW fusohh"
open={false}
>
<div
className="sc-kgAjT ewxwtD"
>
<div
className="sc-hmzhuo eRtqMp"
>
<div
className="sc-eNQAEJ kPLQzL"
>
<div
className="ant-form-item ant-form-item-with-help"
style={
Object {
"marginBottom": "0px",
}
}
>
<div
className="ant-row ant-form-item-row"
style={Object {}}
>
<div
className="ant-col ant-form-item-control"
style={Object {}}
>
<div
className="ant-form-item-control-input"
>
<div
className="ant-form-item-control-input-content"
>
<span
className="ant-input-group-wrapper"
>
<span
className="ant-input-wrapper ant-input-group"
>
<span
className="ant-input-affix-wrapper"
hidden={null}
onClick={[Function]}
style={null}
>
<span
className="ant-input-prefix"
>
<span
aria-label="wallet"
className="anticon anticon-wallet sc-bxivhb iBBRHU"
role="img"
>
<svg
aria-hidden="true"
data-icon="wallet"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 464H528V448h312v128zm0 264H184V184h656v200H496c-17.7 0-32 14.3-32 32v192c0 17.7 14.3 32 32 32h344v200zM580 512a40 40 0 1080 0 40 40 0 10-80 0z"
/>
</svg>
</span>
</span>
<input
autoComplete="off"
className="ant-input"
hidden={null}
name="address"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="Address"
required={true}
style={null}
type="text"
value=""
/>
</span>
<span
className="ant-input-group-addon"
>
<span
className="sc-dxgOiQ fraLQa"
onClick={[Function]}
>
<span
aria-label="qrcode"
className="anticon anticon-qrcode sc-ifAKCX jqgRJL"
role="img"
>
<svg
aria-hidden="true"
data-icon="qrcode"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M468 128H160c-17.7 0-32 14.3-32 32v308c0 4.4 3.6 8 8 8h332c4.4 0 8-3.6 8-8V136c0-4.4-3.6-8-8-8zm-56 284H192V192h220v220zm-138-74h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm194 210H136c-4.4 0-8 3.6-8 8v308c0 17.7 14.3 32 32 32h308c4.4 0 8-3.6 8-8V556c0-4.4-3.6-8-8-8zm-56 284H192V612h220v220zm-138-74h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm590-630H556c-4.4 0-8 3.6-8 8v332c0 4.4 3.6 8 8 8h332c4.4 0 8-3.6 8-8V160c0-17.7-14.3-32-32-32zm-32 284H612V192h220v220zm-138-74h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm194 210h-48c-4.4 0-8 3.6-8 8v134h-78V556c0-4.4-3.6-8-8-8H556c-4.4 0-8 3.6-8 8v332c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V644h78v102c0 4.4 3.6 8 8 8h190c4.4 0 8-3.6 8-8V556c0-4.4-3.6-8-8-8zM746 832h-48c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zm142 0h-48c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
</span>
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<label
className="sc-jlyJG kTtgoZ"
>
Amount
</label>
<div
className="sc-eNQAEJ kPLQzL"
>
<div
className="ant-form-item ant-form-item-with-help"
>
<div
className="ant-row ant-form-item-row"
style={Object {}}
>
<div
className="ant-col ant-form-item-control"
style={Object {}}
>
<div
className="ant-form-item-control-input"
>
<div
className="ant-form-item-control-input-content"
>
<span
className="ant-input-group ant-input-group-compact"
>
<span
className="ant-input-affix-wrapper"
onClick={[Function]}
style={
Object {
"textAlign": "left",
"width": "60%",
}
}
>
<span
className="ant-input-prefix"
>
<img
alt=""
height={16}
src="logo_primary.png"
width={16}
/>
</span>
<input
className="ant-input"
dollar={0}
hidden={null}
name="value"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onWheel={[Function]}
placeholder="Amount"
required={true}
step={0.01}
style={null}
type="number"
value=""
/>
</span>
<div
className="ant-select select-after ant-select-single ant-select-show-arrow"
onBlur={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
style={
Object {
"width": "30%",
}
}
>
<div
className="ant-select-selector"
onClick={[Function]}
onMouseDown={[Function]}
>
<span
className="ant-select-selection-search"
>
<input
aria-activedescendant="rc_select_TEST_OR_SSR_list_0"
aria-autocomplete="list"
aria-controls="rc_select_TEST_OR_SSR_list"
aria-haspopup="listbox"
aria-owns="rc_select_TEST_OR_SSR_list"
autoComplete="off"
className="ant-select-selection-search-input"
disabled={false}
id="rc_select_TEST_OR_SSR"
onChange={[Function]}
onCompositionEnd={[Function]}
onCompositionStart={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
onPaste={[Function]}
readOnly={true}
role="combobox"
style={
Object {
"opacity": 0,
}
}
type="search"
unselectable="on"
value=""
/>
</span>
<span
className="ant-select-selection-item"
title="XEC"
>
XEC
</span>
</div>
<span
aria-hidden={true}
className="ant-select-arrow"
onMouseDown={[Function]}
style={
Object {
"WebkitUserSelect": "none",
"userSelect": "none",
}
}
unselectable="on"
>
<span
aria-label="down"
className="anticon anticon-down ant-select-suffix"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>
</div>
<span
className="sc-kEYyzF fyKOGM"
disabled={false}
onClick={[Function]}
style={
Object {
"height": "55px",
"lineHeight": "55px",
"width": "10%",
}
}
>
max
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
className="sc-frDJqD gCEYuZ"
>
<div
className="sc-eNQAEJ kPLQzL"
>
<div
className="ant-form-item ant-form-item-with-help"
>
<div
className="ant-row ant-form-item-row"
style={Object {}}
>
<div
className="ant-col ant-form-item-control"
style={Object {}}
>
<div
className="ant-form-item-control-input"
>
<div
className="ant-form-item-control-input-content"
>
<textarea
autoComplete="off"
className="ant-input"
disabled={false}
name="address"
onChange={[Function]}
onCompositionEnd={[Function]}
onCompositionStart={[Function]}
onKeyDown={[Function]}
placeholder="One address & value per line, separated by comma
e.g.
ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8,500
ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,700"
prefix={<Styled(Component) />}
required={true}
style={
Object {
"height": "189px",
}
}
value=""
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
className="sc-TOsTZ iykQNe"
>
<h3
className="sc-cJSrbW kPKfAt"
>
0
XEC
</h3>
<div
className="sc-fMiknA fhbntc"
>
=
$ NaN USD
</div>
</div>
</div>
<div
style={
Object {
"paddingTop": "1rem",
}
}
>
<button
className="sc-cMljjf cgwtph"
>
Send
</button>
</div>
<div
className="sc-hSdWYo iRCZlN"
>
<div
className="ant-collapse ant-collapse-icon-position-start sc-cvbbAY eZzhXj"
role={null}
style={
Object {
"marginBottom": "24px",
}
}
>
<div
className="ant-collapse-item"
>
<div
aria-disabled={false}
aria-expanded={false}
className="ant-collapse-header"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0}
>
<div
className="ant-collapse-expand-icon"
onClick={null}
>
<span
aria-label="right"
className="anticon anticon-right ant-collapse-arrow"
role="img"
>
<svg
aria-hidden="true"
data-icon="right"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
/>
</svg>
</span>
</div>
<span
className="ant-collapse-header-text"
onClick={null}
>
<div
className="sc-iAyFgw gPlgsX"
>
<div
className="sc-hqyNC cNBzUr"
>
<svg
className="sc-jzJRlG kLfsyF"
>
mail.svg
</svg>
Message
</div>
</div>
</span>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>,
]
`;
exports[`Wallet with BCH balances and tokens and state field 1`] = `
Array [
<div
className="sc-gipzik gABIqL"
>
<div
className="sc-gisBJw eendT"
>
<h4
className="sc-kjoXOD eJukkq"
>
MigrationTestAlpha
</h4>
<a
href="/configure"
onClick={[Function]}
>
<svg
className="sc-jTzLTM bTdWCF"
>
edit.svg
</svg>
</a>
<div>
<button
aria-checked={false}
className="ant-switch ant-switch-small"
onClick={[Function]}
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="switch"
type="button"
>
<div
className="ant-switch-handle"
/>
<span
className="ant-switch-inner"
>
<svg
className="sc-kAzzGY fUTkYj"
>
eye-invisible.svg
</svg>
</span>
</button>
</div>
</div>
<div
className="sc-Rmtcm bTMyR"
>
<span
className="sc-eqIVtm fWPzjj"
>
0.06
XEC
</span>
</div>
</div>,
<div
className="sc-iRbamj jTHqpF"
>
<div
className="ant-row"
style={Object {}}
type="flex"
>
<div
className="ant-col ant-col-24"
style={Object {}}
>
<form
className="ant-form ant-form-horizontal"
onReset={[Function]}
onSubmit={[Function]}
style={
Object {
"marginTop": "40px",
"width": "auto",
}
}
>
<div
className="sc-ksYbfQ jGqaXi"
>
<label
className="sc-jlyJG kTtgoZ"
>
Send to
</label>
<div
className="sc-cHGsZl cjKbMT"
>
Multiple Recipients:  
<button
aria-checked={false}
className="ant-switch"
defaultunchecked="true"
onClick={[Function]}
onKeyDown={[Function]}
role="switch"
style={
Object {
"marginBottom": "7px",
}
}
type="button"
>
<div
className="ant-switch-handle"
/>
<span
className="ant-switch-inner"
/>
</button>
</div>
</div>
<div
className="sc-kvZOFW fusohh"
open={false}
>
<div
className="sc-kgAjT ewxwtD"
>
<div
className="sc-hmzhuo eRtqMp"
>
<div
className="sc-eNQAEJ kPLQzL"
>
<div
className="ant-form-item ant-form-item-with-help"
style={
Object {
"marginBottom": "0px",
}
}
>
<div
className="ant-row ant-form-item-row"
style={Object {}}
>
<div
className="ant-col ant-form-item-control"
style={Object {}}
>
<div
className="ant-form-item-control-input"
>
<div
className="ant-form-item-control-input-content"
>
<span
className="ant-input-group-wrapper"
>
<span
className="ant-input-wrapper ant-input-group"
>
<span
className="ant-input-affix-wrapper"
hidden={null}
onClick={[Function]}
style={null}
>
<span
className="ant-input-prefix"
>
<span
aria-label="wallet"
className="anticon anticon-wallet sc-bxivhb iBBRHU"
role="img"
>
<svg
aria-hidden="true"
data-icon="wallet"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 464H528V448h312v128zm0 264H184V184h656v200H496c-17.7 0-32 14.3-32 32v192c0 17.7 14.3 32 32 32h344v200zM580 512a40 40 0 1080 0 40 40 0 10-80 0z"
/>
</svg>
</span>
</span>
<input
autoComplete="off"
className="ant-input"
hidden={null}
name="address"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="Address"
required={true}
style={null}
type="text"
value=""
/>
</span>
<span
className="ant-input-group-addon"
>
<span
className="sc-dxgOiQ fraLQa"
onClick={[Function]}
>
<span
aria-label="qrcode"
className="anticon anticon-qrcode sc-ifAKCX jqgRJL"
role="img"
>
<svg
aria-hidden="true"
data-icon="qrcode"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M468 128H160c-17.7 0-32 14.3-32 32v308c0 4.4 3.6 8 8 8h332c4.4 0 8-3.6 8-8V136c0-4.4-3.6-8-8-8zm-56 284H192V192h220v220zm-138-74h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm194 210H136c-4.4 0-8 3.6-8 8v308c0 17.7 14.3 32 32 32h308c4.4 0 8-3.6 8-8V556c0-4.4-3.6-8-8-8zm-56 284H192V612h220v220zm-138-74h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm590-630H556c-4.4 0-8 3.6-8 8v332c0 4.4 3.6 8 8 8h332c4.4 0 8-3.6 8-8V160c0-17.7-14.3-32-32-32zm-32 284H612V192h220v220zm-138-74h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm194 210h-48c-4.4 0-8 3.6-8 8v134h-78V556c0-4.4-3.6-8-8-8H556c-4.4 0-8 3.6-8 8v332c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V644h78v102c0 4.4 3.6 8 8 8h190c4.4 0 8-3.6 8-8V556c0-4.4-3.6-8-8-8zM746 832h-48c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zm142 0h-48c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
</span>
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<label
className="sc-jlyJG kTtgoZ"
>
Amount
</label>
<div
className="sc-eNQAEJ kPLQzL"
>
<div
className="ant-form-item ant-form-item-with-help"
>
<div
className="ant-row ant-form-item-row"
style={Object {}}
>
<div
className="ant-col ant-form-item-control"
style={Object {}}
>
<div
className="ant-form-item-control-input"
>
<div
className="ant-form-item-control-input-content"
>
<span
className="ant-input-group ant-input-group-compact"
>
<span
className="ant-input-affix-wrapper"
onClick={[Function]}
style={
Object {
"textAlign": "left",
"width": "60%",
}
}
>
<span
className="ant-input-prefix"
>
<img
alt=""
height={16}
src="logo_primary.png"
width={16}
/>
</span>
<input
className="ant-input"
dollar={0}
hidden={null}
name="value"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onWheel={[Function]}
placeholder="Amount"
required={true}
step={0.01}
style={null}
type="number"
value=""
/>
</span>
<div
className="ant-select select-after ant-select-single ant-select-show-arrow"
onBlur={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
style={
Object {
"width": "30%",
}
}
>
<div
className="ant-select-selector"
onClick={[Function]}
onMouseDown={[Function]}
>
<span
className="ant-select-selection-search"
>
<input
aria-activedescendant="rc_select_TEST_OR_SSR_list_0"
aria-autocomplete="list"
aria-controls="rc_select_TEST_OR_SSR_list"
aria-haspopup="listbox"
aria-owns="rc_select_TEST_OR_SSR_list"
autoComplete="off"
className="ant-select-selection-search-input"
disabled={false}
id="rc_select_TEST_OR_SSR"
onChange={[Function]}
onCompositionEnd={[Function]}
onCompositionStart={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
onPaste={[Function]}
readOnly={true}
role="combobox"
style={
Object {
"opacity": 0,
}
}
type="search"
unselectable="on"
value=""
/>
</span>
<span
className="ant-select-selection-item"
title="XEC"
>
XEC
</span>
</div>
<span
aria-hidden={true}
className="ant-select-arrow"
onMouseDown={[Function]}
style={
Object {
"WebkitUserSelect": "none",
"userSelect": "none",
}
}
unselectable="on"
>
<span
aria-label="down"
className="anticon anticon-down ant-select-suffix"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>
</div>
<span
className="sc-kEYyzF fyKOGM"
disabled={false}
onClick={[Function]}
style={
Object {
"height": "55px",
"lineHeight": "55px",
"width": "10%",
}
}
>
max
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
className="sc-frDJqD gCEYuZ"
>
<div
className="sc-eNQAEJ kPLQzL"
>
<div
className="ant-form-item ant-form-item-with-help"
>
<div
className="ant-row ant-form-item-row"
style={Object {}}
>
<div
className="ant-col ant-form-item-control"
style={Object {}}
>
<div
className="ant-form-item-control-input"
>
<div
className="ant-form-item-control-input-content"
>
<textarea
autoComplete="off"
className="ant-input"
disabled={false}
name="address"
onChange={[Function]}
onCompositionEnd={[Function]}
onCompositionStart={[Function]}
onKeyDown={[Function]}
placeholder="One address & value per line, separated by comma
e.g.
ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8,500
ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,700"
prefix={<Styled(Component) />}
required={true}
style={
Object {
"height": "189px",
}
}
value=""
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
className="sc-TOsTZ iykQNe"
>
<h3
className="sc-cJSrbW kPKfAt"
>
0
XEC
</h3>
<div
className="sc-fMiknA fhbntc"
>
=
$ NaN USD
</div>
</div>
</div>
<div
style={
Object {
"paddingTop": "1rem",
}
}
>
<button
className="sc-jWBwVP kAXeLy"
onClick={[Function]}
>
Send
</button>
</div>
<div
className="sc-hSdWYo iRCZlN"
>
<div
className="ant-collapse ant-collapse-icon-position-start sc-cvbbAY eZzhXj"
role={null}
style={
Object {
"marginBottom": "24px",
}
}
>
<div
className="ant-collapse-item"
>
<div
aria-disabled={false}
aria-expanded={false}
className="ant-collapse-header"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0}
>
<div
className="ant-collapse-expand-icon"
onClick={null}
>
<span
aria-label="right"
className="anticon anticon-right ant-collapse-arrow"
role="img"
>
<svg
aria-hidden="true"
data-icon="right"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
/>
</svg>
</span>
</div>
<span
className="ant-collapse-header-text"
onClick={null}
>
<div
className="sc-iAyFgw gPlgsX"
>
<div
className="sc-hqyNC cNBzUr"
>
<svg
className="sc-jzJRlG kLfsyF"
>
mail.svg
</svg>
Message
</div>
</div>
</span>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>,
]
`;
exports[`Wallet without BCH balance 1`] = `
Array [
<div
className="sc-gipzik gABIqL"
>
<div
className="sc-gisBJw eendT"
>
<h4
className="sc-kjoXOD eJukkq"
>
MigrationTestAlpha
</h4>
<a
href="/configure"
onClick={[Function]}
>
<svg
className="sc-jTzLTM bTdWCF"
>
edit.svg
</svg>
</a>
<div>
<button
aria-checked={false}
className="ant-switch ant-switch-small"
onClick={[Function]}
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="switch"
type="button"
>
<div
className="ant-switch-handle"
/>
<span
className="ant-switch-inner"
>
<svg
className="sc-kAzzGY fUTkYj"
>
eye-invisible.svg
</svg>
</span>
</button>
</div>
</div>
<div
className="sc-bRBYWo euMChu"
>
You currently have 0
XEC
<br />
Deposit some funds to use this feature
</div>
</div>,
<div
className="sc-iRbamj jTHqpF"
>
<div
className="ant-row"
style={Object {}}
type="flex"
>
<div
className="ant-col ant-col-24"
style={Object {}}
>
<form
className="ant-form ant-form-horizontal"
onReset={[Function]}
onSubmit={[Function]}
style={
Object {
"marginTop": "40px",
"width": "auto",
}
}
>
<div
className="sc-ksYbfQ jGqaXi"
>
<label
className="sc-jlyJG kTtgoZ"
>
Send to
</label>
<div
className="sc-cHGsZl cjKbMT"
>
Multiple Recipients:  
<button
aria-checked={false}
className="ant-switch"
defaultunchecked="true"
onClick={[Function]}
onKeyDown={[Function]}
role="switch"
style={
Object {
"marginBottom": "7px",
}
}
type="button"
>
<div
className="ant-switch-handle"
/>
<span
className="ant-switch-inner"
/>
</button>
</div>
</div>
<div
className="sc-kvZOFW fusohh"
open={false}
>
<div
className="sc-kgAjT ewxwtD"
>
<div
className="sc-hmzhuo eRtqMp"
>
<div
className="sc-eNQAEJ kPLQzL"
>
<div
className="ant-form-item ant-form-item-with-help"
style={
Object {
"marginBottom": "0px",
}
}
>
<div
className="ant-row ant-form-item-row"
style={Object {}}
>
<div
className="ant-col ant-form-item-control"
style={Object {}}
>
<div
className="ant-form-item-control-input"
>
<div
className="ant-form-item-control-input-content"
>
<span
className="ant-input-group-wrapper"
>
<span
className="ant-input-wrapper ant-input-group"
>
<span
className="ant-input-affix-wrapper"
hidden={null}
onClick={[Function]}
style={null}
>
<span
className="ant-input-prefix"
>
<span
aria-label="wallet"
className="anticon anticon-wallet sc-bxivhb iBBRHU"
role="img"
>
<svg
aria-hidden="true"
data-icon="wallet"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 464H528V448h312v128zm0 264H184V184h656v200H496c-17.7 0-32 14.3-32 32v192c0 17.7 14.3 32 32 32h344v200zM580 512a40 40 0 1080 0 40 40 0 10-80 0z"
/>
</svg>
</span>
</span>
<input
autoComplete="off"
className="ant-input"
hidden={null}
name="address"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="Address"
required={true}
style={null}
type="text"
value=""
/>
</span>
<span
className="ant-input-group-addon"
>
<span
className="sc-dxgOiQ fraLQa"
onClick={[Function]}
>
<span
aria-label="qrcode"
className="anticon anticon-qrcode sc-ifAKCX jqgRJL"
role="img"
>
<svg
aria-hidden="true"
data-icon="qrcode"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M468 128H160c-17.7 0-32 14.3-32 32v308c0 4.4 3.6 8 8 8h332c4.4 0 8-3.6 8-8V136c0-4.4-3.6-8-8-8zm-56 284H192V192h220v220zm-138-74h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm194 210H136c-4.4 0-8 3.6-8 8v308c0 17.7 14.3 32 32 32h308c4.4 0 8-3.6 8-8V556c0-4.4-3.6-8-8-8zm-56 284H192V612h220v220zm-138-74h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm590-630H556c-4.4 0-8 3.6-8 8v332c0 4.4 3.6 8 8 8h332c4.4 0 8-3.6 8-8V160c0-17.7-14.3-32-32-32zm-32 284H612V192h220v220zm-138-74h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm194 210h-48c-4.4 0-8 3.6-8 8v134h-78V556c0-4.4-3.6-8-8-8H556c-4.4 0-8 3.6-8 8v332c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V644h78v102c0 4.4 3.6 8 8 8h190c4.4 0 8-3.6 8-8V556c0-4.4-3.6-8-8-8zM746 832h-48c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zm142 0h-48c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
</span>
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<label
className="sc-jlyJG kTtgoZ"
>
Amount
</label>
<div
className="sc-eNQAEJ kPLQzL"
>
<div
className="ant-form-item ant-form-item-with-help"
>
<div
className="ant-row ant-form-item-row"
style={Object {}}
>
<div
className="ant-col ant-form-item-control"
style={Object {}}
>
<div
className="ant-form-item-control-input"
>
<div
className="ant-form-item-control-input-content"
>
<span
className="ant-input-group ant-input-group-compact"
>
<span
className="ant-input-affix-wrapper"
onClick={[Function]}
style={
Object {
"textAlign": "left",
"width": "60%",
}
}
>
<span
className="ant-input-prefix"
>
<img
alt=""
height={16}
src="logo_primary.png"
width={16}
/>
</span>
<input
className="ant-input"
dollar={0}
hidden={null}
name="value"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onWheel={[Function]}
placeholder="Amount"
required={true}
step={0.01}
style={null}
type="number"
value=""
/>
</span>
<div
className="ant-select select-after ant-select-single ant-select-show-arrow"
onBlur={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
style={
Object {
"width": "30%",
}
}
>
<div
className="ant-select-selector"
onClick={[Function]}
onMouseDown={[Function]}
>
<span
className="ant-select-selection-search"
>
<input
aria-activedescendant="rc_select_TEST_OR_SSR_list_0"
aria-autocomplete="list"
aria-controls="rc_select_TEST_OR_SSR_list"
aria-haspopup="listbox"
aria-owns="rc_select_TEST_OR_SSR_list"
autoComplete="off"
className="ant-select-selection-search-input"
disabled={false}
id="rc_select_TEST_OR_SSR"
onChange={[Function]}
onCompositionEnd={[Function]}
onCompositionStart={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
onPaste={[Function]}
readOnly={true}
role="combobox"
style={
Object {
"opacity": 0,
}
}
type="search"
unselectable="on"
value=""
/>
</span>
<span
className="ant-select-selection-item"
title="XEC"
>
XEC
</span>
</div>
<span
aria-hidden={true}
className="ant-select-arrow"
onMouseDown={[Function]}
style={
Object {
"WebkitUserSelect": "none",
"userSelect": "none",
}
}
unselectable="on"
>
<span
aria-label="down"
className="anticon anticon-down ant-select-suffix"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>
</div>
<span
className="sc-kEYyzF fyKOGM"
disabled={false}
onClick={[Function]}
style={
Object {
"height": "55px",
"lineHeight": "55px",
"width": "10%",
}
}
>
max
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
className="sc-frDJqD gCEYuZ"
>
<div
className="sc-eNQAEJ kPLQzL"
>
<div
className="ant-form-item ant-form-item-with-help"
>
<div
className="ant-row ant-form-item-row"
style={Object {}}
>
<div
className="ant-col ant-form-item-control"
style={Object {}}
>
<div
className="ant-form-item-control-input"
>
<div
className="ant-form-item-control-input-content"
>
<textarea
autoComplete="off"
className="ant-input"
disabled={false}
name="address"
onChange={[Function]}
onCompositionEnd={[Function]}
onCompositionStart={[Function]}
onKeyDown={[Function]}
placeholder="One address & value per line, separated by comma
e.g.
ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8,500
ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,700"
prefix={<Styled(Component) />}
required={true}
style={
Object {
"height": "189px",
}
}
value=""
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
className="sc-TOsTZ iykQNe"
>
<h3
className="sc-cJSrbW kPKfAt"
>
0
XEC
</h3>
<div
className="sc-fMiknA fhbntc"
>
=
$ NaN USD
</div>
</div>
</div>
<div
style={
Object {
"paddingTop": "1rem",
}
}
>
<button
className="sc-cMljjf cgwtph"
>
Send
</button>
</div>
<div
className="sc-hSdWYo iRCZlN"
>
<div
className="ant-collapse ant-collapse-icon-position-start sc-cvbbAY eZzhXj"
role={null}
style={
Object {
"marginBottom": "24px",
}
}
>
<div
className="ant-collapse-item"
>
<div
aria-disabled={false}
aria-expanded={false}
className="ant-collapse-header"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0}
>
<div
className="ant-collapse-expand-icon"
onClick={null}
>
<span
aria-label="right"
className="anticon anticon-right ant-collapse-arrow"
role="img"
>
<svg
aria-hidden="true"
data-icon="right"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
/>
</svg>
</span>
</div>
<span
className="ant-collapse-header-text"
onClick={null}
>
<div
className="sc-iAyFgw gPlgsX"
>
<div
className="sc-hqyNC cNBzUr"
>
<svg
className="sc-jzJRlG kLfsyF"
>
mail.svg
</svg>
Message
</div>
</div>
</span>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>,
]
`;
exports[`Without wallet defined 1`] = `
Array [
<div
className="sc-gipzik gABIqL"
>
<div
className="sc-gisBJw eendT"
>
<a
href="/configure"
onClick={[Function]}
>
<svg
className="sc-jTzLTM bTdWCF"
>
edit.svg
</svg>
</a>
<div>
<button
aria-checked={false}
className="ant-switch ant-switch-small"
onClick={[Function]}
onKeyDown={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="switch"
type="button"
>
<div
className="ant-switch-handle"
/>
<span
className="ant-switch-inner"
>
<svg
className="sc-kAzzGY fUTkYj"
>
eye-invisible.svg
</svg>
</span>
</button>
</div>
</div>
<div
className="sc-bRBYWo euMChu"
>
You currently have 0
XEC
<br />
Deposit some funds to use this feature
</div>
</div>,
<div
className="sc-iRbamj jTHqpF"
>
<div
className="ant-row"
style={Object {}}
type="flex"
>
<div
className="ant-col ant-col-24"
style={Object {}}
>
<form
className="ant-form ant-form-horizontal"
onReset={[Function]}
onSubmit={[Function]}
style={
Object {
"marginTop": "40px",
"width": "auto",
}
}
>
<div
className="sc-ksYbfQ jGqaXi"
>
<label
className="sc-jlyJG kTtgoZ"
>
Send to
</label>
<div
className="sc-cHGsZl cjKbMT"
>
Multiple Recipients:  
<button
aria-checked={false}
className="ant-switch"
defaultunchecked="true"
onClick={[Function]}
onKeyDown={[Function]}
role="switch"
style={
Object {
"marginBottom": "7px",
}
}
type="button"
>
<div
className="ant-switch-handle"
/>
<span
className="ant-switch-inner"
/>
</button>
</div>
</div>
<div
className="sc-kvZOFW fusohh"
open={false}
>
<div
className="sc-kgAjT ewxwtD"
>
<div
className="sc-hmzhuo eRtqMp"
>
<div
className="sc-eNQAEJ kPLQzL"
>
<div
className="ant-form-item ant-form-item-with-help"
style={
Object {
"marginBottom": "0px",
}
}
>
<div
className="ant-row ant-form-item-row"
style={Object {}}
>
<div
className="ant-col ant-form-item-control"
style={Object {}}
>
<div
className="ant-form-item-control-input"
>
<div
className="ant-form-item-control-input-content"
>
<span
className="ant-input-group-wrapper"
>
<span
className="ant-input-wrapper ant-input-group"
>
<span
className="ant-input-affix-wrapper"
hidden={null}
onClick={[Function]}
style={null}
>
<span
className="ant-input-prefix"
>
<span
aria-label="wallet"
className="anticon anticon-wallet sc-bxivhb iBBRHU"
role="img"
>
<svg
aria-hidden="true"
data-icon="wallet"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 464H528V448h312v128zm0 264H184V184h656v200H496c-17.7 0-32 14.3-32 32v192c0 17.7 14.3 32 32 32h344v200zM580 512a40 40 0 1080 0 40 40 0 10-80 0z"
/>
</svg>
</span>
</span>
<input
autoComplete="off"
className="ant-input"
hidden={null}
name="address"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="Address"
required={true}
style={null}
type="text"
value=""
/>
</span>
<span
className="ant-input-group-addon"
>
<span
className="sc-dxgOiQ fraLQa"
onClick={[Function]}
>
<span
aria-label="qrcode"
className="anticon anticon-qrcode sc-ifAKCX jqgRJL"
role="img"
>
<svg
aria-hidden="true"
data-icon="qrcode"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M468 128H160c-17.7 0-32 14.3-32 32v308c0 4.4 3.6 8 8 8h332c4.4 0 8-3.6 8-8V136c0-4.4-3.6-8-8-8zm-56 284H192V192h220v220zm-138-74h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm194 210H136c-4.4 0-8 3.6-8 8v308c0 17.7 14.3 32 32 32h308c4.4 0 8-3.6 8-8V556c0-4.4-3.6-8-8-8zm-56 284H192V612h220v220zm-138-74h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm590-630H556c-4.4 0-8 3.6-8 8v332c0 4.4 3.6 8 8 8h332c4.4 0 8-3.6 8-8V160c0-17.7-14.3-32-32-32zm-32 284H612V192h220v220zm-138-74h56c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm194 210h-48c-4.4 0-8 3.6-8 8v134h-78V556c0-4.4-3.6-8-8-8H556c-4.4 0-8 3.6-8 8v332c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V644h78v102c0 4.4 3.6 8 8 8h190c4.4 0 8-3.6 8-8V556c0-4.4-3.6-8-8-8zM746 832h-48c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zm142 0h-48c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z"
/>
</svg>
</span>
</span>
</span>
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<label
className="sc-jlyJG kTtgoZ"
>
Amount
</label>
<div
className="sc-eNQAEJ kPLQzL"
>
<div
className="ant-form-item ant-form-item-with-help"
>
<div
className="ant-row ant-form-item-row"
style={Object {}}
>
<div
className="ant-col ant-form-item-control"
style={Object {}}
>
<div
className="ant-form-item-control-input"
>
<div
className="ant-form-item-control-input-content"
>
<span
className="ant-input-group ant-input-group-compact"
>
<span
className="ant-input-affix-wrapper"
onClick={[Function]}
style={
Object {
"textAlign": "left",
"width": "60%",
}
}
>
<span
className="ant-input-prefix"
>
<img
alt=""
height={16}
src="logo_primary.png"
width={16}
/>
</span>
<input
className="ant-input"
dollar={0}
hidden={null}
name="value"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onWheel={[Function]}
placeholder="Amount"
required={true}
step={0.01}
style={null}
type="number"
value=""
/>
</span>
<div
className="ant-select select-after ant-select-single ant-select-show-arrow"
onBlur={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
style={
Object {
"width": "30%",
}
}
>
<div
className="ant-select-selector"
onClick={[Function]}
onMouseDown={[Function]}
>
<span
className="ant-select-selection-search"
>
<input
aria-activedescendant="rc_select_TEST_OR_SSR_list_0"
aria-autocomplete="list"
aria-controls="rc_select_TEST_OR_SSR_list"
aria-haspopup="listbox"
aria-owns="rc_select_TEST_OR_SSR_list"
autoComplete="off"
className="ant-select-selection-search-input"
disabled={false}
id="rc_select_TEST_OR_SSR"
onChange={[Function]}
onCompositionEnd={[Function]}
onCompositionStart={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
onPaste={[Function]}
readOnly={true}
role="combobox"
style={
Object {
"opacity": 0,
}
}
type="search"
unselectable="on"
value=""
/>
</span>
<span
className="ant-select-selection-item"
title="XEC"
>
XEC
</span>
</div>
<span
aria-hidden={true}
className="ant-select-arrow"
onMouseDown={[Function]}
style={
Object {
"WebkitUserSelect": "none",
"userSelect": "none",
}
}
unselectable="on"
>
<span
aria-label="down"
className="anticon anticon-down ant-select-suffix"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>
</div>
<span
className="sc-kEYyzF fyKOGM"
disabled={false}
onClick={[Function]}
style={
Object {
"height": "55px",
"lineHeight": "55px",
"width": "10%",
}
}
>
max
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
className="sc-frDJqD gCEYuZ"
>
<div
className="sc-eNQAEJ kPLQzL"
>
<div
className="ant-form-item ant-form-item-with-help"
>
<div
className="ant-row ant-form-item-row"
style={Object {}}
>
<div
className="ant-col ant-form-item-control"
style={Object {}}
>
<div
className="ant-form-item-control-input"
>
<div
className="ant-form-item-control-input-content"
>
<textarea
autoComplete="off"
className="ant-input"
disabled={false}
name="address"
onChange={[Function]}
onCompositionEnd={[Function]}
onCompositionStart={[Function]}
onKeyDown={[Function]}
placeholder="One address & value per line, separated by comma
e.g.
ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8,500
ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,700"
prefix={<Styled(Component) />}
required={true}
style={
Object {
"height": "189px",
}
}
value=""
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
className="sc-TOsTZ iykQNe"
>
<h3
className="sc-cJSrbW kPKfAt"
>
0
XEC
</h3>
<div
className="sc-fMiknA fhbntc"
>
=
$ NaN USD
</div>
</div>
</div>
<div
style={
Object {
"paddingTop": "1rem",
}
}
>
<button
className="sc-cMljjf cgwtph"
>
Send
</button>
</div>
<div
className="sc-hSdWYo iRCZlN"
>
<div
className="ant-collapse ant-collapse-icon-position-start sc-cvbbAY eZzhXj"
role={null}
style={
Object {
"marginBottom": "24px",
}
}
>
<div
className="ant-collapse-item"
>
<div
aria-disabled={false}
aria-expanded={false}
className="ant-collapse-header"
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0}
>
<div
className="ant-collapse-expand-icon"
onClick={null}
>
<span
aria-label="right"
className="anticon anticon-right ant-collapse-arrow"
role="img"
>
<svg
aria-hidden="true"
data-icon="right"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
/>
</svg>
</span>
</div>
<span
className="ant-collapse-header-text"
onClick={null}
>
<div
className="sc-iAyFgw gPlgsX"
>
<div
className="sc-hqyNC cNBzUr"
>
<svg
className="sc-jzJRlG kLfsyF"
>
mail.svg
</svg>
Message
</div>
</div>
</span>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>,
]
`;
diff --git a/web/cashtab/src/hooks/useWallet.js b/web/cashtab/src/hooks/useWallet.js
index 541ea6588..b7ad5748b 100644
--- a/web/cashtab/src/hooks/useWallet.js
+++ b/web/cashtab/src/hooks/useWallet.js
@@ -1,1540 +1,1685 @@
import { useState, useEffect } from 'react';
import usePrevious from 'hooks/usePrevious';
import useInterval from './useInterval';
import BigNumber from 'bignumber.js';
import eCash from 'ecashjs-lib';
import coininfo from 'utils/coininfo';
import {
loadStoredWallet,
isValidStoredWallet,
isLegacyMigrationRequired,
getHashArrayFromWallet,
isActiveWebsocket,
getWalletBalanceFromUtxos,
toHash160,
} from 'utils/cashMethods';
import {
isValidCashtabSettings,
isValidCashtabCache,
isValidContactList,
parseInvalidSettingsForMigration,
parseInvalidCashtabCacheForMigration,
} from 'utils/validation';
import localforage from 'localforage';
import { currency } from 'components/Common/Ticker';
import {
xecReceivedNotification,
xecReceivedNotificationWebsocket,
eTokenReceivedNotification,
} from 'components/Common/Notifications';
import {
getUtxosChronik,
organizeUtxosByType,
getPreliminaryTokensArray,
finalizeTokensArray,
finalizeSlpUtxos,
getTxHistoryChronik,
parseChronikTx,
getAliasAndAddresses,
+ getOnchainAliasTxCount,
+ getAllTxHistory,
} from 'utils/chronik';
import { ChronikClient } from 'chronik-client';
import cashaddr from 'ecashaddrjs';
import * as bip39 from 'bip39';
import * as randomBytes from 'randombytes';
const useWallet = () => {
const [chronik, setChronik] = useState(
new ChronikClient(currency.chronikUrls[0]),
);
const previousChronik = usePrevious(chronik);
const [walletRefreshInterval, setWalletRefreshInterval] = useState(
currency.websocketDisconnectedRefreshInterval,
);
const [wallet, setWallet] = useState(false);
const [chronikWebsocket, setChronikWebsocket] = useState(null);
const [contactList, setContactList] = useState([{}]);
const [cashtabSettings, setCashtabSettings] = useState(false);
const [cashtabCache, setCashtabCache] = useState(
currency.defaultCashtabCache,
);
const [fiatPrice, setFiatPrice] = useState(null);
const [apiError, setApiError] = useState(false);
const [checkFiatInterval, setCheckFiatInterval] = useState(null);
const [hasUpdated, setHasUpdated] = useState(false);
const [loading, setLoading] = useState(true);
const [chronikIndex, setChronikIndex] = useState(0);
const { balances, tokens } = isValidStoredWallet(wallet)
? wallet.state
: {
balances: {},
tokens: [],
};
const previousBalances = usePrevious(balances);
const previousTokens = usePrevious(tokens);
const tryNextChronikUrl = () => {
console.log(`Error with chronik instance at ${chronik._url}`);
let currentChronikIndex = chronikIndex;
// How many chronik URLs are available?
const chronikUrlCount = currency.chronikUrls.length;
console.log(
`Cashtab has ${
chronikUrlCount - 1
} alternative chronik instances available`,
);
// If only one, exit
if (chronikUrlCount === 1) {
console.log(
`There are no backup chronik servers. Please contact an admin to fix the chronik server.`,
);
return;
} else if (currentChronikIndex < chronikUrlCount - 1) {
// If you have another one, use the next one
currentChronikIndex += 1;
} else {
// If you are at the "end" of the array, use the first one
currentChronikIndex = 0;
}
setChronikIndex(currentChronikIndex);
console.log(
`Creating new chronik client with URL ${currency.chronikUrls[currentChronikIndex]}`,
);
return setChronik(
new ChronikClient(currency.chronikUrls[currentChronikIndex]),
);
};
const deriveAccount = async ({ masterHDNode, path }) => {
const node = masterHDNode.derivePath(path);
const publicKey = node.getPublicKeyBuffer().toString('hex');
const cashAddress = cashaddr.encode(
'ecash',
'P2PKH',
node.getIdentifier(),
);
const hash160 = toHash160(cashAddress);
return {
publicKey,
hash160,
cashAddress,
fundingWif: node.keyPair.toWIF(),
};
};
const loadWalletFromStorageOnStartup = async setWallet => {
// get wallet object from localforage
const wallet = await getWallet();
// If wallet object in storage is valid, use it to set state on startup
if (isValidStoredWallet(wallet)) {
// Convert all the token balance figures to big numbers
const liveWalletState = loadStoredWallet(wallet.state);
wallet.state = liveWalletState;
setWallet(wallet);
return setLoading(false);
}
console.log(`Active wallet is not valid, loading params from API`);
// Loading will remain true until API calls populate this legacy wallet
setWallet(wallet);
};
const update = async ({ wallet }) => {
// Check if walletRefreshInterval is set to 10, i.e. this was called by websocket tx detection
// If walletRefreshInterval is 10, set it back to the usual refresh rate
if (walletRefreshInterval === 10) {
setWalletRefreshInterval(
currency.websocketConnectedRefreshInterval,
);
}
try {
if (!wallet) {
return;
}
/*
This strange data structure is necessary because chronik requires the hash160
of an address to tell you what utxos are at that address
*/
const hash160AndAddressObjArray = [
{
address: wallet.Path145.cashAddress,
hash160: wallet.Path145.hash160,
},
{
address: wallet.Path245.cashAddress,
hash160: wallet.Path245.hash160,
},
{
address: wallet.Path1899.cashAddress,
hash160: wallet.Path1899.hash160,
},
];
const chronikUtxos = await getUtxosChronik(
chronik,
hash160AndAddressObjArray,
);
const { preliminarySlpUtxos, nonSlpUtxos } =
organizeUtxosByType(chronikUtxos);
const preliminaryTokensArray =
getPreliminaryTokensArray(preliminarySlpUtxos);
const { tokens, updatedTokenInfoById, newTokensToCache } =
await finalizeTokensArray(
chronik,
preliminaryTokensArray,
cashtabCache.tokenInfoById,
);
// If you have more token info now, write this to local storage
if (newTokensToCache) {
writeTokenInfoByIdToCache(updatedTokenInfoById);
// Update the tokenInfoById key in cashtabCache
setCashtabCache({
...cashtabCache,
tokenInfoById: updatedTokenInfoById,
});
}
const slpUtxos = finalizeSlpUtxos(
preliminarySlpUtxos,
updatedTokenInfoById,
);
const {
parsedTxHistory,
txHistoryUpdatedTokenInfoById,
txHistoryNewTokensToCache,
} = await getTxHistoryChronik(
chronik,
wallet,
updatedTokenInfoById,
);
if (txHistoryNewTokensToCache) {
console.log(
`Uncached token info found in tx history, adding to cache`,
);
writeTokenInfoByIdToCache(txHistoryUpdatedTokenInfoById);
// Update the tokenInfoById key in cashtabCache
setCashtabCache({
...cashtabCache,
tokenInfoById: txHistoryUpdatedTokenInfoById,
});
}
// If you were missing any token info for tokens in this tx history, get it
const newState = {
balances: getWalletBalanceFromUtxos(nonSlpUtxos),
slpUtxos,
nonSlpUtxos,
tokens,
parsedTxHistory,
};
// Set wallet with new state field
wallet.state = newState;
setWallet(wallet);
// Write this state to indexedDb using localForage
writeWalletState(wallet, newState);
// If everything executed correctly, remove apiError
setApiError(false);
} catch (error) {
console.log(`Error in update({wallet})`);
console.log(error);
// Set this in state so that transactions are disabled until the issue is resolved
setApiError(true);
// Try another chronik instance
tryNextChronikUrl();
}
};
const getActiveWalletFromLocalForage = async () => {
let wallet;
try {
wallet = await localforage.getItem('wallet');
} catch (err) {
console.log(`Error in getActiveWalletFromLocalForage`, err);
wallet = null;
}
return wallet;
};
const getAliasesFromLocalForage = async () => {
let cachedAliases, cashtabCache;
try {
cashtabCache = await localforage.getItem('cashtabCache');
cachedAliases = cashtabCache.aliasCache;
} catch (err) {
console.log(`Error in getAliasesFromLocalForage`, err);
cachedAliases = null;
}
return cachedAliases;
};
const updateAliases = async totalPaymentTxHistory => {
if (!totalPaymentTxHistory) {
console.log(`Invalid totalPaymentTxHistory input`);
return false;
}
setLoading(true);
let updateSuccess = true;
// calculate total tx count for the alias payment address
const onChainTotalPaymentTx = totalPaymentTxHistory.length;
// extract an array of aliases and addresses from totalPaymentTxHistory
const aliasAndAddressListArray = getAliasAndAddresses(
totalPaymentTxHistory,
);
// for each record in aliasAndAddressListArray
let aliasCacheObject = {
totalPaymentTxCount: onChainTotalPaymentTx,
aliases: aliasAndAddressListArray,
paymentTxHistory: totalPaymentTxHistory,
};
cashtabCache.aliasCache = aliasCacheObject;
// set array into local forage
try {
await localforage.setItem('cashtabCache', cashtabCache);
} catch (err) {
console.log('Error in updateAliases', err);
updateSuccess = false;
}
setCashtabCache(cashtabCache);
setLoading(false);
return updateSuccess;
};
const getContactListFromLocalForage = async () => {
let contactListArray = [];
try {
contactListArray = await localforage.getItem('contactList');
} catch (err) {
console.log('Error in getContactListFromLocalForage', err);
contactListArray = null;
}
return contactListArray;
};
const updateContactList = async contactListArray => {
setLoading(true);
let updateSuccess = true;
try {
await localforage.setItem('contactList', contactListArray);
setContactList(contactListArray);
} catch (err) {
console.log('Error in updateContactList', err);
updateSuccess = false;
}
setLoading(false);
return updateSuccess;
};
const getWallet = async () => {
let wallet;
let existingWallet;
try {
existingWallet = await getActiveWalletFromLocalForage();
// existing wallet will be
// 1 - the 'wallet' value from localForage, if it exists
// 2 - false if it does not exist in localForage
// 3 - null if error
// If the wallet does not have Path1899, add it
// or each Path1899, Path145, Path245 does not have a public key, add them
if (existingWallet) {
if (isLegacyMigrationRequired(existingWallet)) {
console.log(
`Wallet does not have Path1899 or does not have public key`,
);
existingWallet = await migrateLegacyWallet(existingWallet);
}
}
// If not in localforage then existingWallet = false, check localstorage
if (!existingWallet) {
console.log(`no existing wallet, checking local storage`);
existingWallet = JSON.parse(
window.localStorage.getItem('wallet'),
);
console.log(`existingWallet from localStorage`, existingWallet);
// If you find it here, move it to indexedDb
if (existingWallet !== null) {
wallet = await getWalletDetails(existingWallet);
await localforage.setItem('wallet', wallet);
return wallet;
}
}
} catch (err) {
console.log(`Error in getWallet()`, err);
/*
Error here implies problem interacting with localForage or localStorage API
Have not seen this error in testing
In this case, you still want to return 'wallet' using the logic below based on
the determination of 'existingWallet' from the logic above
*/
}
if (existingWallet === null || !existingWallet) {
wallet = await getWalletDetails(existingWallet);
await localforage.setItem('wallet', wallet);
} else {
wallet = existingWallet;
}
return wallet;
};
const migrateLegacyWallet = async wallet => {
console.log(`migrateLegacyWallet`);
console.log(`legacyWallet`, wallet);
const mnemonic = wallet.mnemonic;
const rootSeedBuffer = await bip39.mnemonicToSeed(mnemonic, '');
const masterHDNode = eCash.HDNode.fromSeedBuffer(
rootSeedBuffer,
coininfo.bitcoincash.main.toBitcoinJS(),
);
const Path245 = await deriveAccount({
masterHDNode,
path: "m/44'/245'/0'/0/0",
});
const Path145 = await deriveAccount({
masterHDNode,
path: "m/44'/145'/0'/0/0",
});
const Path1899 = await deriveAccount({
masterHDNode,
path: "m/44'/1899'/0'/0/0",
});
wallet.Path245 = Path245;
wallet.Path145 = Path145;
wallet.Path1899 = Path1899;
try {
await localforage.setItem('wallet', wallet);
} catch (err) {
console.log(
`Error setting wallet to wallet indexedDb in migrateLegacyWallet()`,
);
console.log(err);
}
return wallet;
};
const writeTokenInfoByIdToCache = async tokenInfoById => {
console.log(`writeTokenInfoByIdToCache`);
const cashtabCache = currency.defaultCashtabCache;
cashtabCache.tokenInfoById = tokenInfoById;
try {
await localforage.setItem('cashtabCache', cashtabCache);
console.log(`cashtabCache successfully updated`);
} catch (err) {
console.log(`Error in writeCashtabCache()`, err);
}
};
const writeWalletState = async (wallet, newState) => {
// Add new state as an object on the active wallet
wallet.state = newState;
try {
await localforage.setItem('wallet', wallet);
} catch (err) {
console.log(`Error in writeWalletState()`);
console.log(err);
}
};
const getWalletDetails = async wallet => {
if (!wallet) {
return false;
}
// Since this info is in localforage now, only get the var
const mnemonic = wallet.mnemonic;
const rootSeedBuffer = await bip39.mnemonicToSeed(mnemonic, '');
const masterHDNode = eCash.HDNode.fromSeedBuffer(
rootSeedBuffer,
coininfo.bitcoincash.main.toBitcoinJS(),
);
const Path245 = await deriveAccount({
masterHDNode,
path: "m/44'/245'/0'/0/0",
});
const Path145 = await deriveAccount({
masterHDNode,
path: "m/44'/145'/0'/0/0",
});
const Path1899 = await deriveAccount({
masterHDNode,
path: "m/44'/1899'/0'/0/0",
});
let name = Path1899.cashAddress.slice(6, 11);
// Only set the name if it does not currently exist
if (wallet && wallet.name) {
name = wallet.name;
}
return {
mnemonic: wallet.mnemonic,
name,
Path245,
Path145,
Path1899,
};
};
const getSavedWallets = async activeWallet => {
setLoading(true);
let savedWallets;
try {
savedWallets = await localforage.getItem('savedWallets');
if (savedWallets === null) {
savedWallets = [];
}
} catch (err) {
console.log(`Error in getSavedWallets`, err);
savedWallets = [];
}
// Even though the active wallet is still stored in savedWallets, don't return it in this function
for (let i = 0; i < savedWallets.length; i += 1) {
if (
typeof activeWallet !== 'undefined' &&
activeWallet.name &&
savedWallets[i].name === activeWallet.name
) {
savedWallets.splice(i, 1);
}
}
setLoading(false);
return savedWallets;
};
const activateWallet = async (currentlyActiveWallet, walletToActivate) => {
/*
If the user is migrating from old version to this version, make sure to save the activeWallet
1 - check savedWallets for the previously active wallet
2 - If not there, add it
*/
console.log(`Activating wallet ${walletToActivate.name}`);
setHasUpdated(false);
// Get savedwallets
let savedWallets;
try {
savedWallets = await localforage.getItem('savedWallets');
} catch (err) {
console.log(
`Error in localforage.getItem("savedWallets") in activateWallet()`,
);
return false;
}
/*
When a legacy user runs cashtab.com/, their active wallet will be migrated to Path1899 by
the getWallet function. getWallet function also makes sure that each Path has a public key
Wallets in savedWallets are migrated when they are activated, in this function
Two cases to handle
1 - currentlyActiveWallet is valid but its stored keyvalue pair in savedWallets is not
> Update savedWallets so this saved wallet is valid
2 - walletToActivate is not valid (because it's a legacy saved wallet)
> Update walletToActivate before activation
*/
// Check savedWallets for currentlyActiveWallet
let walletInSavedWallets = false;
for (let i = 0; i < savedWallets.length; i += 1) {
if (savedWallets[i].name === currentlyActiveWallet.name) {
walletInSavedWallets = true;
// Make sure the savedWallet entry matches the currentlyActiveWallet entry
savedWallets[i] = currentlyActiveWallet;
console.log(
`Updating savedWallet ${savedWallets[i].name} to match state as currentlyActiveWallet ${currentlyActiveWallet.name}`,
);
}
}
// resave savedWallets
try {
// Set walletName as the active wallet
console.log(`Saving updated savedWallets`);
await localforage.setItem('savedWallets', savedWallets);
} catch (err) {
console.log(
`Error in localforage.setItem("savedWallets") in activateWallet() for unmigrated wallet`,
);
}
if (!walletInSavedWallets) {
console.log(`Wallet is not in saved Wallets, adding`);
savedWallets.push(currentlyActiveWallet);
// resave savedWallets
try {
// Set walletName as the active wallet
await localforage.setItem('savedWallets', savedWallets);
} catch (err) {
console.log(
`Error in localforage.setItem("savedWallets") in activateWallet()`,
);
}
}
// If wallet does not have Path1899, add it
// or each of the Path1899, Path145, Path245 does not have a public key, add them
// by calling migrateLagacyWallet()
if (isLegacyMigrationRequired(walletToActivate)) {
// Case 2, described above
console.log(
`Case 2: Wallet to activate is not in the most up to date Cashtab format`,
);
console.log(`walletToActivate`, walletToActivate);
walletToActivate = await migrateLegacyWallet(walletToActivate);
} else {
// Otherwise activate it as normal
// Now that we have verified the last wallet was saved, we can activate the new wallet
try {
await localforage.setItem('wallet', walletToActivate);
} catch (err) {
console.log(
`Error in localforage.setItem("wallet", walletToActivate) in activateWallet()`,
);
return false;
}
}
// Convert all the token balance figures to big numbers
// localforage does not preserve BigNumber type; loadStoredWallet restores BigNumber type
const liveWalletState = loadStoredWallet(walletToActivate.state);
walletToActivate.state = liveWalletState;
console.log(`Returning walletToActivate ${walletToActivate.name}`);
return walletToActivate;
};
const renameSavedWallet = async (oldName, newName) => {
setLoading(true);
// Load savedWallets
let savedWallets;
try {
savedWallets = await localforage.getItem('savedWallets');
} catch (err) {
console.log(
`Error in await localforage.getItem("savedWallets") in renameSavedWallet`,
err,
);
setLoading(false);
return false;
}
// Verify that no existing wallet has this name
for (let i = 0; i < savedWallets.length; i += 1) {
if (savedWallets[i].name === newName) {
// return an error
setLoading(false);
return false;
}
}
// change name of desired wallet
for (let i = 0; i < savedWallets.length; i += 1) {
if (savedWallets[i].name === oldName) {
// Replace the name of this entry with the new name
savedWallets[i].name = newName;
}
}
// resave savedWallets
try {
// Set walletName as the active wallet
await localforage.setItem('savedWallets', savedWallets);
} catch (err) {
console.log(
`Error in localforage.setItem("savedWallets", savedWallets) in renameSavedWallet()`,
err,
);
setLoading(false);
return false;
}
setLoading(false);
return true;
};
const renameActiveWallet = async (activeWallet, oldName, newName) => {
setLoading(true);
// Load savedWallets
let savedWallets;
try {
savedWallets = await localforage.getItem('savedWallets');
} catch (err) {
console.log(
`Error in await localforage.getItem("savedWallets") in renameSavedWallet`,
err,
);
setLoading(false);
return false;
}
// Verify that no existing wallet has this name
for (let i = 0; i < savedWallets.length; i += 1) {
if (savedWallets[i].name === newName) {
// return an error
setLoading(false);
return false;
}
}
// Change name of active wallet at its entry in savedWallets
for (let i = 0; i < savedWallets.length; i += 1) {
if (savedWallets[i].name === oldName) {
// Replace the name of this entry with the new name
savedWallets[i].name = newName;
}
}
// resave savedWallets
try {
// Set walletName as the active wallet
await localforage.setItem('savedWallets', savedWallets);
} catch (err) {
console.log(
`Error in localforage.setItem("wallet", wallet) in renameActiveWallet()`,
err,
);
setLoading(false);
return false;
}
// Change name of active wallet param in this function
activeWallet.name = newName;
// Update the active wallet entry in indexedDb
try {
await localforage.setItem('wallet', activeWallet);
} catch (err) {
console.log(
`Error in localforage.setItem("wallet", ${activeWallet.name}) in renameActiveWallet()`,
err,
);
setLoading(false);
return false;
}
// Only set the renamed activeWallet in state if no errors earlier in this function
setWallet(activeWallet);
setLoading(false);
return true;
};
const deleteWallet = async walletToBeDeleted => {
setLoading(true);
// delete a wallet
// returns true if wallet is successfully deleted
// otherwise returns false
// Load savedWallets
let savedWallets;
try {
savedWallets = await localforage.getItem('savedWallets');
} catch (err) {
console.log(
`Error in await localforage.getItem("savedWallets") in deleteWallet`,
err,
);
setLoading(false);
return false;
}
// Iterate over to find the wallet to be deleted
// Verify that no existing wallet has this name
let walletFoundAndRemoved = false;
for (let i = 0; i < savedWallets.length; i += 1) {
if (savedWallets[i].name === walletToBeDeleted.name) {
// Verify it has the same mnemonic too, that's a better UUID
if (savedWallets[i].mnemonic === walletToBeDeleted.mnemonic) {
// Delete it
savedWallets.splice(i, 1);
walletFoundAndRemoved = true;
}
}
}
// If you don't find the wallet, return false
if (!walletFoundAndRemoved) {
setLoading(false);
return false;
}
// Resave savedWallets less the deleted wallet
try {
// Set walletName as the active wallet
await localforage.setItem('savedWallets', savedWallets);
} catch (err) {
console.log(
`Error in localforage.setItem("savedWallets", savedWallets) in deleteWallet()`,
err,
);
setLoading(false);
return false;
}
setLoading(false);
return true;
};
const addNewSavedWallet = async importMnemonic => {
setLoading(true);
// Add a new wallet to savedWallets from importMnemonic or just new wallet
const lang = 'english';
// create 128 bit BIP39 mnemonic
const Bip39128BitMnemonic = importMnemonic
? importMnemonic
: bip39.generateMnemonic(128, randomBytes, bip39.wordlists[lang]);
const newSavedWallet = await getWalletDetails({
mnemonic: Bip39128BitMnemonic.toString(),
});
// Get saved wallets
let savedWallets;
try {
savedWallets = await localforage.getItem('savedWallets');
// If this doesn't exist yet, savedWallets === null
if (savedWallets === null) {
savedWallets = [];
}
} catch (err) {
console.log(
`Error in savedWallets = await localforage.getItem("savedWallets") in addNewSavedWallet()`,
err,
);
console.log(`savedWallets in error state`, savedWallets);
}
// If this wallet is from an imported mnemonic, make sure it does not already exist in savedWallets
if (importMnemonic) {
for (let i = 0; i < savedWallets.length; i += 1) {
// Check for condition "importing new wallet that is already in savedWallets"
if (savedWallets[i].mnemonic === importMnemonic) {
// set this as the active wallet to keep name history
console.log(
`Error: this wallet already exists in savedWallets`,
);
console.log(`Wallet not being added.`);
setLoading(false);
return false;
}
}
}
// add newSavedWallet
savedWallets.push(newSavedWallet);
// update savedWallets
try {
await localforage.setItem('savedWallets', savedWallets);
} catch (err) {
console.log(
`Error in localforage.setItem("savedWallets", activeWallet) called in createWallet with ${importMnemonic}`,
err,
);
console.log(`savedWallets`, savedWallets);
}
setLoading(false);
return true;
};
const createWallet = async importMnemonic => {
const lang = 'english';
// create 128 bit BIP39 mnemonic
const Bip39128BitMnemonic = importMnemonic
? importMnemonic
: bip39.generateMnemonic(128, randomBytes, bip39.wordlists[lang]);
const wallet = await getWalletDetails({
mnemonic: Bip39128BitMnemonic.toString(),
});
try {
await localforage.setItem('wallet', wallet);
} catch (err) {
console.log(
`Error setting wallet to wallet indexedDb in createWallet()`,
);
console.log(err);
}
// Since this function is only called from OnBoarding.js, also add this to the saved wallet
try {
await localforage.setItem('savedWallets', [wallet]);
} catch (err) {
console.log(
`Error setting wallet to savedWallets indexedDb in createWallet()`,
);
console.log(err);
}
return wallet;
};
// Parse chronik ws message for incoming tx notifications
const processChronikWsMsg = async (msg, wallet, fiatPrice) => {
// get the message type
const { type } = msg;
// For now, only act on "first seen" transactions, as the only logic to happen is first seen notifications
// Dev note: Other chronik msg types
// "BlockConnected", arrives as new blocks are found
// "Confirmed", arrives as subscribed + seen txid is confirmed in a block
if (type !== 'AddedToMempool') {
return;
}
// If you see a tx from your subscribed addresses added to the mempool, then the wallet utxo set has changed
// Update it
setWalletRefreshInterval(10);
// get txid info
const txid = msg.txid;
let incomingTxDetails;
try {
incomingTxDetails = await chronik.tx(txid);
} catch (err) {
// In this case, no notification
return console.log(
`Error in chronik.tx(${txid} while processing an incoming websocket tx`,
err,
);
}
// Get tokenInfoById from cashtabCache to parse this tx
let tokenInfoById = {};
try {
tokenInfoById = cashtabCache.tokenInfoById;
} catch (err) {
console.log(
`Error getting tokenInfoById from cache on incoming tx`,
err,
);
}
// parse tx for notification
const parsedChronikTx = parseChronikTx(
incomingTxDetails,
wallet,
tokenInfoById,
);
/* If this is an incoming eToken tx and parseChronikTx was not able to get genesis info
from cache, then get genesis info from API and add to cache */
if (parsedChronikTx.incoming) {
if (parsedChronikTx.isEtokenTx) {
let eTokenAmountReceived = parsedChronikTx.etokenAmount;
if (parsedChronikTx.genesisInfo.success) {
// Send this info to the notification function
eTokenReceivedNotification(
currency,
parsedChronikTx.genesisInfo.tokenTicker,
eTokenAmountReceived,
parsedChronikTx.genesisInfo.tokenName,
);
} else {
// Get genesis info from API and add to cache
try {
// Get the tokenID
const incomingTokenId = parsedChronikTx.slpMeta.tokenId;
// chronik call to genesis tx to get this info
const tokenGenesisInfo = await chronik.tx(
incomingTokenId,
);
const { genesisInfo } = tokenGenesisInfo.slpTxData;
// Add this to cashtabCache
let tokenInfoByIdUpdatedForThisToken = tokenInfoById;
tokenInfoByIdUpdatedForThisToken[incomingTokenId] =
genesisInfo;
writeTokenInfoByIdToCache(
tokenInfoByIdUpdatedForThisToken,
);
// Update the tokenInfoById key in cashtabCache
setCashtabCache({
...cashtabCache,
tokenInfoById: tokenInfoByIdUpdatedForThisToken,
});
// Calculate eToken amount with decimals
eTokenAmountReceived = new BigNumber(
parsedChronikTx.etokenAmount,
).shiftedBy(-1 * genesisInfo.decimals);
// Send this info to the notification function
eTokenReceivedNotification(
currency,
genesisInfo.tokenTicker,
eTokenAmountReceived,
genesisInfo.tokenName,
);
} catch (err) {
console.log(
`Error in getting and setting new token info for incoming eToken tx`,
err,
);
}
}
} else {
xecReceivedNotificationWebsocket(
parsedChronikTx.xecAmount,
cashtabSettings,
fiatPrice,
);
}
}
};
// Chronik websockets
const initializeWebsocket = async (chronik, wallet, fiatPrice) => {
// Because wallet is set to `false` before it is loaded, do nothing if you find this case
// Also return and wait for legacy migration if wallet is not migrated
const hash160Array = getHashArrayFromWallet(wallet);
if (!wallet || !hash160Array) {
return setChronikWebsocket(null);
}
const hasChronikUrlChanged = chronik !== previousChronik;
if (hasChronikUrlChanged) {
console.log(`Chronik URL has changed to ${chronik._url}.`);
}
let ws = chronikWebsocket;
// If chronik URL has changed and ws is not null, close existing websocket
if (hasChronikUrlChanged && ws !== null) {
console.log(`Closing websocket connection at ${ws._wsUrl}`);
ws.close();
}
// Initialize websocket if not in state or if chronik URL has changed
if (ws === null || hasChronikUrlChanged) {
console.log(`Opening websocket connection at ${chronik._wsUrl}`);
ws = chronik.ws({
onMessage: msg => {
processChronikWsMsg(msg, wallet, fiatPrice);
},
autoReconnect: true,
onReconnect: e => {
// Fired before a reconnect attempt is made:
console.log(
'Reconnecting websocket, disconnection cause: ',
e,
);
},
onConnect: e => {
console.log(`Chronik websocket connected`, e);
console.log(
`Websocket connected, adjusting wallet refresh interval to ${
currency.websocketConnectedRefreshInterval / 1000
}s`,
);
setWalletRefreshInterval(
currency.websocketConnectedRefreshInterval,
);
},
});
// Need to put ws in state here so that, if the connection fails, it can be cleared for the next chronik URL
setChronikWebsocket(ws);
// Wait for websocket to be connected:
await ws.waitForOpen();
} else {
/*
If the websocket connection is not null and the chronik URL has not changed, initializeWebsocket was called
because one of the websocket's dependencies changed
Update the onMessage method to get the latest dependencies (wallet, fiatPrice)
*/
ws.onMessage = msg => {
processChronikWsMsg(msg, wallet, fiatPrice);
};
}
// Check if current subscriptions match current wallet
let activeSubscriptionsMatchActiveWallet = true;
const previousWebsocketSubscriptions = ws._subs;
// If there are no previous subscriptions, then activeSubscriptionsMatchActiveWallet is certainly false
if (previousWebsocketSubscriptions.length === 0) {
activeSubscriptionsMatchActiveWallet = false;
} else {
const subscribedHash160Array = previousWebsocketSubscriptions.map(
function (subscription) {
return subscription.scriptPayload;
},
);
// Confirm that websocket is subscribed to every address in wallet hash160Array
for (let i = 0; i < hash160Array.length; i += 1) {
if (!subscribedHash160Array.includes(hash160Array[i])) {
activeSubscriptionsMatchActiveWallet = false;
}
}
}
// If you are already subscribed to the right addresses, exit here
// You get to this situation if fiatPrice changed but wallet.mnemonic did not
if (activeSubscriptionsMatchActiveWallet) {
// Put connected websocket in state
return setChronikWebsocket(ws);
}
// Unsubscribe to any active subscriptions
console.log(
`previousWebsocketSubscriptions`,
previousWebsocketSubscriptions,
);
if (previousWebsocketSubscriptions.length > 0) {
for (let i = 0; i < previousWebsocketSubscriptions.length; i += 1) {
const unsubHash160 =
previousWebsocketSubscriptions[i].scriptPayload;
ws.unsubscribe('p2pkh', unsubHash160);
console.log(`ws.unsubscribe('p2pkh', ${unsubHash160})`);
}
}
// Subscribe to addresses of current wallet
for (let i = 0; i < hash160Array.length; i += 1) {
ws.subscribe('p2pkh', hash160Array[i]);
console.log(`ws.subscribe('p2pkh', ${hash160Array[i]})`);
}
// Put connected websocket in state
return setChronikWebsocket(ws);
};
const handleUpdateWallet = async setWallet => {
await loadWalletFromStorageOnStartup(setWallet);
+
+ if (currency.aliasSettings.aliasEnabled) {
+ // only sync alias cache if alias feature is enabled
+ await synchronizeAliasCache(chronik);
+ }
+ };
+
+ const synchronizeAliasCache = async chronik => {
+ let cachedAliases;
+ // retrieve cached aliases
+ try {
+ cachedAliases = await getAliasesFromLocalForage();
+ } catch (err) {
+ console.log(
+ `synchronizeAliasCache(): Error retrieving aliases from local forage`,
+ err,
+ );
+ }
+
+ // if alias cache exists, check if partial tx history retrieval is required
+ if (cachedAliases && cachedAliases.paymentTxHistory.length > 0) {
+ // get cached tx count
+ const cachedAliasTxCount = cachedAliases.totalPaymentTxCount;
+
+ // temporary log for reviewer
+ console.log(`cached Alias Tx Count: `, cachedAliasTxCount);
+
+ // get onchain tx count
+ let onchainAliasTxCount = await getOnchainAliasTxCount(chronik);
+
+ // temporary log for reviewer
+ console.log(`onchain Alias Tx Count: `, onchainAliasTxCount);
+
+ // condition where a partial alias tx history refresh is required
+ if (cachedAliasTxCount !== onchainAliasTxCount) {
+ // temporary log for reviewer
+ console.log(`partial tx history retrieval required`);
+
+ const onchainPages = Math.ceil(
+ cachedAliasTxCount / currency.chronikTxsPerPage,
+ );
+
+ // execute a partial tx history retrieval instead of full history
+ const pagesToTraverse = Math.ceil(
+ onchainPages -
+ cachedAliasTxCount / currency.chronikTxsPerPage,
+ ); // how many pages to traverse backwards via chronik
+ const partialAliasPaymentTxHistory = await getAllTxHistory(
+ chronik,
+ currency.aliasSettings.aliasPaymentHash160,
+ pagesToTraverse,
+ );
+
+ // temporary log for reviewer
+ console.log(
+ `partial txs retrieved: `,
+ partialAliasPaymentTxHistory.length,
+ );
+
+ // update cache with the latest alias transactions
+ let allTxHistory = cachedAliases.paymentTxHistory; // starting point is what's currently cached
+
+ if (allTxHistory) {
+ // only concat non-duplicate entries from the partial tx history retrieval
+ partialAliasPaymentTxHistory.forEach(element => {
+ if (
+ !JSON.stringify(allTxHistory).includes(
+ JSON.stringify(element.txid),
+ )
+ ) {
+ allTxHistory = allTxHistory.concat(element);
+ // temporary log for reviewer
+ console.log(
+ `${element.txid} appended to allTxHistory`,
+ );
+ }
+ });
+ }
+
+ // update cached alias list
+ // updateAliases() handles the extraction of the aliases and generates the expected JSON format
+ await updateAliases(allTxHistory);
+
+ // temporary console log for reviewer
+ console.log(`alias cache update complete`);
+ } else {
+ // temporary console log for reviewer
+ console.log(
+ `cachedAliases exist however partial alias cache refresh NOT required`,
+ );
+ }
+ } else {
+ // first time loading Alias, execute full tx history retrieval
+ // temporary console log for reviewer
+ console.log(
+ `Alias.js: cachedAliases DOES NOT exist, retrieving full tx history`,
+ );
+
+ // get latest tx count for payment address
+ const aliasPaymentTxHistory = await getAllTxHistory(
+ chronik,
+ currency.aliasSettings.aliasPaymentHash160,
+ );
+ const totalPaymentTxCount = aliasPaymentTxHistory.length;
+
+ // temporary log for reviewer
+ console.log(`onchain totalPaymentTxCount: ${totalPaymentTxCount}`);
+
+ // temporary console log for reviewer
+ if (cachedAliases) {
+ console.log(
+ `cached totalPaymentTxCount: `,
+ cachedAliases.totalPaymentTxCount,
+ );
+ }
+
+ // conditions where an alias refresh is required
+ if (
+ !cachedAliases ||
+ !cachedAliases.totalPaymentTxCount ||
+ cachedAliases.totalPaymentTxCount < totalPaymentTxCount
+ ) {
+ // temporary console log for reviewer
+ console.log(`alias cache refresh required`);
+
+ try {
+ // update cached alias list
+ // updateAliases() handles the extraction of the alias and generates the expected JSON format
+ await updateAliases(aliasPaymentTxHistory);
+
+ // temporary console log for reviewer
+ console.log(`alias cache update complete`);
+ } catch (err) {
+ console.log(`Error updating alias cache in Alias.js`, err);
+ }
+ } else {
+ // temporary console log for reviewer
+ console.log(`alias cache refresh NOT required`);
+ }
+ }
+
+ return cachedAliases;
};
const loadCashtabSettings = async () => {
// get settings object from localforage
let localSettings;
try {
localSettings = await localforage.getItem('settings');
// If there is no keyvalue pair in localforage with key 'settings'
if (localSettings === null) {
// Create one with the default settings from Ticker.js
localforage.setItem('settings', currency.defaultSettings);
// Set state to default settings
setCashtabSettings(currency.defaultSettings);
return currency.defaultSettings;
}
} catch (err) {
console.log(`Error getting cashtabSettings`, err);
// TODO If they do not exist, write them
// TODO add function to change them
setCashtabSettings(currency.defaultSettings);
return currency.defaultSettings;
}
// If you found an object in localforage at the settings key, make sure it's valid
if (isValidCashtabSettings(localSettings)) {
setCashtabSettings(localSettings);
return localSettings;
}
// If a settings object is present but invalid, parse to find and add missing keys
let modifiedLocalSettings =
parseInvalidSettingsForMigration(localSettings);
if (isValidCashtabSettings(modifiedLocalSettings)) {
// modifiedLocalSettings placed in local storage
localforage.setItem('settings', modifiedLocalSettings);
setCashtabSettings(modifiedLocalSettings);
// update missing key in local storage without overwriting existing valid settings
return modifiedLocalSettings;
} else {
// if not valid, also set cashtabSettings to default
setCashtabSettings(currency.defaultSettings);
// Since this is returning default settings based on an error from reading storage, do not overwrite whatever is in storage
return currency.defaultSettings;
}
};
const loadContactList = async () => {
// get contactList object from localforage
let localContactList;
try {
localContactList = await localforage.getItem('contactList');
// If there is no keyvalue pair in localforage with key 'contactList'
if (localContactList === null) {
// Use an array containing a single empty object
localforage.setItem('contactList', [{}]);
setContactList([{}]);
return [{}];
}
} catch (err) {
console.log(`Error getting contactList`, err);
setContactList([{}]);
return [{}];
}
// If you found an object in localforage at the contactList key, make sure it's valid
if (isValidContactList(localContactList)) {
setContactList(localContactList);
return localContactList;
}
// if not valid, also set to default
setContactList([{}]);
return [{}];
};
const loadCashtabCache = async () => {
// get cache object from localforage
let localCashtabCache;
try {
localCashtabCache = await localforage.getItem('cashtabCache');
// If there is no keyvalue pair in localforage with key 'cashtabCache'
if (localCashtabCache === null) {
// Use the default
localforage.setItem(
'cashtabCache',
currency.defaultCashtabCache,
);
setCashtabCache(currency.defaultCashtabCache);
return currency.defaultCashtabCache;
}
} catch (err) {
console.log(`Error getting cashtabCache`, err);
setCashtabCache(currency.defaultCashtabCache);
return currency.defaultCashtabCache;
}
// If you found an object in localforage at the cashtabCache key, make sure it's valid
if (isValidCashtabCache(localCashtabCache)) {
setCashtabCache(localCashtabCache);
// temporary log for reviewer
console.log(`valid cashtabCache detected`);
return localCashtabCache;
}
// if not valid, parse the cache object, finds what param is missing, and sticks it in
const migratedCashtabCache =
parseInvalidCashtabCacheForMigration(localCashtabCache);
localforage.setItem('cashtabCache', migratedCashtabCache);
setCashtabCache(migratedCashtabCache);
// temporary log for reviewer
console.log(
`invalid cashtabCache detected, missing params initialized from currency.defaultCashtabCache`,
);
return currency.defaultCashtabCache;
};
// With different currency selections possible, need unique intervals for price checks
// Must be able to end them and set new ones with new currencies
const initializeFiatPriceApi = async selectedFiatCurrency => {
// Update fiat price and confirm it is set to make sure ap keeps loading state until this is updated
await fetchBchPrice(selectedFiatCurrency);
// Set interval for updating the price with given currency
const thisFiatInterval = setInterval(function () {
fetchBchPrice(selectedFiatCurrency);
}, 60000);
// set interval in state
setCheckFiatInterval(thisFiatInterval);
};
const clearFiatPriceApi = fiatPriceApi => {
// Clear fiat price check interval of previously selected currency
clearInterval(fiatPriceApi);
};
const changeCashtabSettings = async (key, newValue) => {
// Set loading to true as you do not want to display the fiat price of the last currency
// loading = true will lock the UI until the fiat price has updated
if (key !== 'balanceVisible') {
setLoading(true);
}
// Get settings from localforage
let currentSettings;
let newSettings;
try {
currentSettings = await localforage.getItem('settings');
} catch (err) {
console.log(`Error in changeCashtabSettings`, err);
// Set fiat price to null, which disables fiat sends throughout the app
setFiatPrice(null);
// Unlock the UI
setLoading(false);
return;
}
// Make sure function was called with valid params
if (currency.settingsValidation[key].includes(newValue)) {
// Update settings
newSettings = currentSettings;
newSettings[key] = newValue;
} else {
// Set fiat price to null, which disables fiat sends throughout the app
setFiatPrice(null);
// Unlock the UI
setLoading(false);
return;
}
// Set new settings in state so they are available in context throughout the app
setCashtabSettings(newSettings);
// If this settings change adjusted the fiat currency, update fiat price
if (key === 'fiatCurrency') {
clearFiatPriceApi(checkFiatInterval);
initializeFiatPriceApi(newValue);
}
// Write new settings in localforage
try {
await localforage.setItem('settings', newSettings);
} catch (err) {
console.log(
`Error writing newSettings object to localforage in changeCashtabSettings`,
err,
);
console.log(`newSettings`, newSettings);
// do nothing. If this happens, the user will see default currency next time they load the app.
}
setLoading(false);
};
// Parse for incoming XEC transactions
// hasUpdated is set to true in the useInterval function, and re-sets to false during activateWallet
// Do not show this notification if websocket connection is live; in this case the websocket will handle it
if (
!isActiveWebsocket(chronikWebsocket) &&
previousBalances &&
balances &&
'totalBalance' in previousBalances &&
'totalBalance' in balances &&
new BigNumber(balances.totalBalance)
.minus(previousBalances.totalBalance)
.gt(0) &&
hasUpdated
) {
xecReceivedNotification(
balances,
previousBalances,
cashtabSettings,
fiatPrice,
);
}
// Parse for incoming eToken transactions
// Do not show this notification if websocket connection is live; in this case the websocket will handle it
if (
!isActiveWebsocket(chronikWebsocket) &&
tokens &&
tokens[0] &&
tokens[0].balance &&
previousTokens &&
previousTokens[0] &&
previousTokens[0].balance &&
hasUpdated === true
) {
// If tokens length is greater than previousTokens length, a new token has been received
// Note, a user could receive a new token, AND more of existing tokens in between app updates
// In this case, the app will only notify about the new token
// TODO better handling for all possible cases to cover this
// TODO handle with websockets for better response time, less complicated calc
if (tokens.length > previousTokens.length) {
// Find the new token
const tokenIds = tokens.map(({ tokenId }) => tokenId);
const previousTokenIds = previousTokens.map(
({ tokenId }) => tokenId,
);
// An array with the new token Id
const newTokenIdArr = tokenIds.filter(
tokenId => !previousTokenIds.includes(tokenId),
);
// It's possible that 2 new tokens were received
// To do, handle this case
const newTokenId = newTokenIdArr[0];
// Find where the newTokenId is
const receivedTokenObjectIndex = tokens.findIndex(
x => x.tokenId === newTokenId,
);
// Calculate amount received
const receivedSlpQty =
tokens[receivedTokenObjectIndex].balance.toString();
const receivedSlpTicker =
tokens[receivedTokenObjectIndex].info.tokenTicker;
const receivedSlpName =
tokens[receivedTokenObjectIndex].info.tokenName;
// Notification if you received SLP
if (receivedSlpQty > 0) {
eTokenReceivedNotification(
currency,
receivedSlpTicker,
receivedSlpQty,
receivedSlpName,
);
}
//
} else {
// If tokens[i].balance > previousTokens[i].balance, a new SLP tx of an existing token has been received
// Note that tokens[i].balance is of type BigNumber
for (let i = 0; i < tokens.length; i += 1) {
if (
new BigNumber(tokens[i].balance).gt(
new BigNumber(previousTokens[i].balance),
)
) {
if (previousTokens[i].tokenId !== tokens[i].tokenId) {
console.log(
`TokenIds do not match, breaking from SLP notifications`,
);
// Then don't send the notification
// Also don't 'continue' ; this means you have sent a token, just stop iterating through
break;
}
const receivedSlpQty = new BigNumber(
tokens[i].balance,
).minus(new BigNumber(previousTokens[i].balance));
const receivedSlpTicker = tokens[i].info.tokenTicker;
const receivedSlpName = tokens[i].info.tokenName;
eTokenReceivedNotification(
currency,
receivedSlpTicker,
receivedSlpQty,
receivedSlpName,
);
}
}
}
}
// Update wallet according to defined interval
useInterval(async () => {
const wallet = await getWallet();
update({
wallet,
}).finally(() => {
setLoading(false);
if (!hasUpdated) {
setHasUpdated(true);
}
});
}, walletRefreshInterval);
const fetchBchPrice = async (
fiatCode = cashtabSettings ? cashtabSettings.fiatCurrency : 'usd',
) => {
// Split this variable out in case coingecko changes
const cryptoId = currency.coingeckoId;
// Keep this in the code, because different URLs will have different outputs require different parsing
const priceApiUrl = `https://api.coingecko.com/api/v3/simple/price?ids=${cryptoId}&vs_currencies=${fiatCode}&include_last_updated_at=true`;
let bchPrice;
let bchPriceJson;
try {
bchPrice = await fetch(priceApiUrl);
} catch (err) {
console.log(`Error fetching BCH Price`);
console.log(err);
}
try {
bchPriceJson = await bchPrice.json();
let bchPriceInFiat = bchPriceJson[cryptoId][fiatCode];
const validEcashPrice = typeof bchPriceInFiat === 'number';
if (validEcashPrice) {
setFiatPrice(bchPriceInFiat);
} else {
// If API price looks fishy, do not allow app to send using fiat settings
setFiatPrice(null);
}
} catch (err) {
console.log(`Error parsing price API response to JSON`);
console.log(err);
}
};
useEffect(async () => {
handleUpdateWallet(setWallet);
await loadContactList();
await loadCashtabCache();
const initialSettings = await loadCashtabSettings();
initializeFiatPriceApi(initialSettings.fiatCurrency);
}, []);
/*
Run initializeWebsocket(chronik, wallet, fiatPrice) each time chronik, wallet, or fiatPrice changes
Use wallet.mnemonic as the useEffect parameter here because we
want to run initializeWebsocket(chronik, wallet, fiatPrice) when a new unique wallet
is selected, not when the active wallet changes state
*/
useEffect(async () => {
await initializeWebsocket(chronik, wallet, fiatPrice);
}, [chronik, wallet.mnemonic, fiatPrice]);
return {
chronik,
wallet,
fiatPrice,
loading,
apiError,
contactList,
cashtabSettings,
cashtabCache,
changeCashtabSettings,
getActiveWalletFromLocalForage,
getWallet,
getWalletDetails,
getSavedWallets,
migrateLegacyWallet,
+ synchronizeAliasCache,
getContactListFromLocalForage,
getAliasesFromLocalForage,
updateAliases,
updateContactList,
createWallet: async importMnemonic => {
setLoading(true);
const newWallet = await createWallet(importMnemonic);
setWallet(newWallet);
update({
wallet: newWallet,
}).finally(() => setLoading(false));
},
activateWallet: async (currentlyActiveWallet, walletToActivate) => {
setLoading(true);
// Make sure that the wallet update interval is not called on the former wallet before this function completes
console.log(
`Suspending wallet update interval while new wallet is activated`,
);
setWalletRefreshInterval(
currency.websocketDisconnectedRefreshInterval,
);
const newWallet = await activateWallet(
currentlyActiveWallet,
walletToActivate,
);
console.log(`activateWallet gives newWallet ${newWallet.name}`);
// Changing the wallet here will cause `initializeWebsocket` to fire which will update the websocket interval on a successful connection
setWallet(newWallet);
// Immediately call update on this wallet to populate it in the latest format
// Use the instant interval of 10ms that the update function will cancel
setWalletRefreshInterval(10);
setLoading(false);
},
addNewSavedWallet,
renameSavedWallet,
renameActiveWallet,
deleteWallet,
};
};
export default useWallet;
diff --git a/web/cashtab/src/utils/validation.js b/web/cashtab/src/utils/validation.js
index 5c9c848ed..96319b27a 100644
--- a/web/cashtab/src/utils/validation.js
+++ b/web/cashtab/src/utils/validation.js
@@ -1,643 +1,647 @@
import BigNumber from 'bignumber.js';
import { currency } from 'components/Common/Ticker.js';
import { fromSatoshisToXec } from 'utils/cashMethods';
import cashaddr from 'ecashaddrjs';
import * as bip39 from 'bip39';
+export const isAliasFormat = address => {
+ return address.slice(-4) === '.xec';
+};
+
export const validateMnemonic = (
mnemonic,
wordlist = bip39.wordlists.english,
) => {
try {
if (!mnemonic || !wordlist) return false;
// Preprocess the words
const words = mnemonic.split(' ');
// Detect blank phrase
if (words.length === 0) return false;
// Check the words are valid
return bip39.validateMnemonic(mnemonic, wordlist);
} catch (err) {
console.log(err);
return false;
}
};
// 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))) {
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 isProbablyNotAScamTokenName = tokenName => {
// convert to lower case, trim leading and trailing spaces
// split, filter then join on ' ' for cases where user inputs multiple spaces
const sanitizedTokenName = tokenName
.toLowerCase()
.trim()
.split(' ')
.filter(string => string)
.join(' ');
return (
!currency.coingeckoTop500Names.includes(sanitizedTokenName) &&
// for cases where user adds spaces between e a c h letter
!currency.coingeckoTop500Names.includes(
sanitizedTokenName.split(' ').join(''),
) &&
// cross reference with coingeckoTop500Tickers
!currency.coingeckoTop500Tickers.includes(sanitizedTokenName) &&
!currency.coingeckoTop500Tickers.includes(
sanitizedTokenName.split(' ').join(''),
) &&
//cross reference with coingeckoTop500Ids
!currency.coingeckoTop500Ids.includes(sanitizedTokenName) &&
!currency.coingeckoTop500Ids.includes(
sanitizedTokenName.split(' ').join(''),
) &&
//cross reference with bannedFiatCurrencies
!currency.settingsValidation.fiatCurrency.includes(
sanitizedTokenName,
) &&
!currency.settingsValidation.fiatCurrency.includes(
sanitizedTokenName.split(' ').join(''),
) &&
//cross reference with bannedTickers
!currency.bannedTickers.includes(sanitizedTokenName) &&
!currency.bannedTickers.includes(
sanitizedTokenName.split(' ').join(''),
) &&
//cross reference with bannedNames
!currency.bannedNames.includes(sanitizedTokenName) &&
!currency.bannedNames.includes(sanitizedTokenName.split(' ').join(''))
);
};
export const isProbablyNotAScamTokenTicker = tokenTicker => {
// convert to lower case, trim leading and trailing spaces
// split, filter then join on ' ' for cases where user inputs multiple spaces
const sanitizedTokenTicker = tokenTicker
.toLowerCase()
.trim()
.split(' ')
.filter(string => string)
.join('');
return (
!currency.coingeckoTop500Tickers.includes(sanitizedTokenTicker) &&
// for cases where user adds spaces between e a c h letter
!currency.coingeckoTop500Tickers.includes(
sanitizedTokenTicker.split(' ').join(''),
) &&
//cross reference with coingeckoTop500Names
!currency.coingeckoTop500Names.includes(sanitizedTokenTicker) &&
!currency.coingeckoTop500Names.includes(
sanitizedTokenTicker.split(' ').join(''),
) &&
//cross reference with coingeckoTop500Ids
!currency.coingeckoTop500Ids.includes(sanitizedTokenTicker) &&
!currency.coingeckoTop500Ids.includes(
sanitizedTokenTicker.split(' ').join(''),
) &&
//cross reference with bannedFiatCurrencies
!currency.settingsValidation.fiatCurrency.includes(
sanitizedTokenTicker,
) &&
!currency.settingsValidation.fiatCurrency.includes(
sanitizedTokenTicker.split(' ').join(''),
) &&
//cross reference with bannedTickers
!currency.bannedTickers.includes(sanitizedTokenTicker) &&
!currency.bannedTickers.includes(
sanitizedTokenTicker.split(' ').join(''),
) &&
//cross reference with bannedNames
!currency.bannedNames.includes(sanitizedTokenTicker) &&
!currency.bannedNames.includes(sanitizedTokenTicker.split(' ').join(''))
);
};
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 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 parseInvalidCashtabCacheForMigration = invalidCashtabCache => {
// create a copy of the invalidCashtabCache
let migratedCashtabCache = invalidCashtabCache;
// determine if settings are invalid because it is missing a parameter
for (let param in currency.defaultCashtabCache) {
if (!Object.prototype.hasOwnProperty.call(invalidCashtabCache, param)) {
// adds the default setting for only that parameter
migratedCashtabCache[param] = currency.defaultCashtabCache[param];
}
}
return migratedCashtabCache;
};
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 isValidCashtabCache = cashtabCache => {
/*
Object must contain all keys listed in currency.defaultCashtabCache
The tokenInfoById object must have keys that are valid token IDs,
and at each one an object like:
{
"tokenTicker": "ST",
"tokenName": "ST",
"tokenDocumentUrl": "developer.bitcoin.com",
"tokenDocumentHash": "",
"decimals": 0,
"tokenId": "bf24d955f59351e738ecd905966606a6837e478e1982943d724eab10caad82fd"
}
i.e. an object that contains these keys
'tokenTicker' is a string
'tokenName' is a string
'tokenDocumentUrl' is a string
'tokenDocumentHash' is a string
'decimals' is a number
'tokenId' is a valid tokenId
The aliasCache object must have the following keys:
{
'aliases' is an array,
'paymentTxHistory' is an array,
'totalPaymentTxCount' is a number,
}
*/
// Check that every key in currency.defaultCashtabCache is also in this cashtabCache
const cashtabCacheKeys = Object.keys(currency.defaultCashtabCache);
for (let i = 0; i < cashtabCacheKeys.length; i += 1) {
const thisKey = cashtabCacheKeys[i];
if (thisKey in cashtabCache) {
continue;
}
return false;
}
// Check that tokenInfoById is expected type and that tokenIds are valid
const { tokenInfoById } = cashtabCache;
const tokenIds = Object.keys(tokenInfoById);
for (let i = 0; i < tokenIds.length; i += 1) {
const thisTokenId = tokenIds[i];
if (!isValidTokenId(thisTokenId)) {
return false;
}
const {
tokenTicker,
tokenName,
tokenDocumentUrl,
tokenDocumentHash,
decimals,
tokenId,
} = tokenInfoById[thisTokenId];
if (
typeof tokenTicker !== 'string' ||
typeof tokenName !== 'string' ||
typeof tokenDocumentUrl !== 'string' ||
typeof tokenDocumentHash !== 'string' ||
typeof decimals !== 'number' ||
!isValidTokenId(tokenId)
) {
return false;
}
}
// check the aliasCache object contains the aliases and paymentTxHistory arrays and the totalPaymentTxCount num
const { aliasCache } = cashtabCache;
if (!aliasCache) {
// temporary log for reviewer
console.log(`aliasCache in cashtabCache is false`);
return false;
}
const { aliases, paymentTxHistory, totalPaymentTxCount } = aliasCache;
if (
!Array.isArray(aliases) ||
!Array.isArray(paymentTxHistory) ||
typeof totalPaymentTxCount !== 'number'
) {
// temporary log for reviewer
console.log(`aliasCache in cashtabCache is false`);
return false;
}
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 isValidBchAddress = addr => {
/*
Returns true for a valid BCH address
Valid BCH address:
- May or may not have prefix `bitcoincash:`
- Checksum must validate for prefix `bitcoincash:`
A simple ledger address is not considered a valid bitcoincash address
*/
if (!addr) {
return false;
}
let isValidBchAddress;
let isPrefixedBchAddress;
// Check for possible prefix
if (addr.includes(':')) {
// Test for 'ecash:' prefix
isPrefixedBchAddress = addr.slice(0, 12) === 'bitcoincash:';
// Any address including ':' that doesn't start explicitly with 'bitcoincash:' is invalid
if (!isPrefixedBchAddress) {
isValidBchAddress = false;
return isValidBchAddress;
}
} else {
isPrefixedBchAddress = false;
}
// If no prefix, assume it is checksummed for an bitcoincash: prefix
const testedXecAddr = isPrefixedBchAddress ? addr : `bitcoincash:${addr}`;
try {
const decoded = cashaddr.decode(testedXecAddr);
if (decoded.prefix === 'bitcoincash') {
isValidBchAddress = true;
}
} catch (err) {
isValidBchAddress = false;
}
return isValidBchAddress;
};
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).toNumber()
);
};
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, Mar 2, 11:33 (1 d, 6 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5187631
Default Alt Text
(263 KB)

Event Timeline