`;
diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js
index 3c368826d..c70d12562 100644
--- a/web/cashtab/src/components/Send/Send.js
+++ b/web/cashtab/src/components/Send/Send.js
@@ -1,545 +1,548 @@
import React, { useState, useEffect } from 'react';
import { WalletContext } from '@utils/context';
import { Form, notification, message, Modal, Alert } from 'antd';
import { CashLoader } from '@components/Common/CustomIcons';
import { Row, Col } from 'antd';
import Paragraph from 'antd/lib/typography/Paragraph';
import PrimaryButton, {
SecondaryButton,
} from '@components/Common/PrimaryButton';
import {
SendBchInput,
FormItemWithQRCodeAddon,
} from '@components/Common/EnhancedInputs';
import useBCH from '@hooks/useBCH';
import useWindowDimensions from '@hooks/useWindowDimensions';
import { isMobile, isIOS, isSafari } from 'react-device-detect';
import {
currency,
isValidTokenPrefix,
parseAddress,
toLegacy,
} from '@components/Common/Ticker.js';
import { Event } from '@utils/GoogleAnalytics';
import { fiatToCrypto, shouldRejectAmountInput } from '@utils/validation';
import { BalanceHeader } from '@components/Common/BalanceHeader';
import { BalanceHeaderFiat } from '@components/Common/BalanceHeaderFiat';
import {
ZeroBalanceHeader,
ConvertAmount,
AlertMsg,
} from '@components/Common/Atoms';
import { getWalletState } from '@utils/cashMethods';
+import { CashReceivedNotificationIcon } from '@components/Common/CustomIcons';
// Note jestBCH is only used for unit tests; BCHJS must be mocked for jest
const SendBCH = ({ jestBCH, passLoadingStatus }) => {
// use balance parameters from wallet.state object and not legacy balances parameter from walletState, if user has migrated wallet
// this handles edge case of user with old wallet who has not opened latest Cashtab version yet
// If the wallet object from ContextValue has a `state key`, then check which keys are in the wallet object
// Else set it as blank
const ContextValue = React.useContext(WalletContext);
const { wallet, fiatPrice, apiError, cashtabSettings } = ContextValue;
const walletState = getWalletState(wallet);
const { balances, slpBalancesAndUtxos } = walletState;
// 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 = width < 769 && isMobile && !(isIOS && !isSafari);
const [formData, setFormData] = useState({
dirty: true,
value: '',
address: '',
});
const [queryStringText, setQueryStringText] = useState(null);
const [sendBchAddressError, setSendBchAddressError] = useState(false);
const [sendBchAmountError, setSendBchAmountError] = useState(false);
const [selectedCurrency, setSelectedCurrency] = useState(currency.ticker);
// Support cashtab button from web pages
const [txInfoFromUrl, setTxInfoFromUrl] = useState(false);
// Show a confirmation modal on transactions created by populating form from web page button
const [isModalVisible, setIsModalVisible] = useState(false);
const showModal = () => {
setIsModalVisible(true);
};
const handleOk = () => {
setIsModalVisible(false);
submit();
};
const handleCancel = () => {
setIsModalVisible(false);
};
const { getBCH, getRestUrl, sendBch, calcFee } = useBCH();
// jestBCH is only ever specified for unit tests, otherwise app will use getBCH();
const BCH = jestBCH ? jestBCH : getBCH();
// 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
// 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,
});
}
}
async function submit() {
setFormData({
...formData,
dirty: false,
});
if (
!formData.address ||
!formData.value ||
Number(formData.value) <= 0
) {
return;
}
// Event("Category", "Action", "Label")
// Track number of BCHA send transactions and whether users
// are sending BCHA or USD
Event('Send.js', 'Send', selectedCurrency);
passLoadingStatus(true);
const { address, value } = formData;
// Get the param-free address
let cleanAddress = address.split('?')[0];
// Ensure address has bitcoincash: prefix and checksum
cleanAddress = toLegacy(cleanAddress);
let hasValidCashPrefix;
try {
hasValidCashPrefix = cleanAddress.startsWith(
currency.legacyPrefix + ':',
);
} catch (err) {
hasValidCashPrefix = false;
console.log(`toLegacy() returned an error:`, cleanAddress);
}
if (!hasValidCashPrefix) {
// set loading to false and set address validation to false
// Now that the no-prefix case is handled, this happens when user tries to send
// BCHA to an SLPA address
passLoadingStatus(false);
setSendBchAddressError(
`Destination is not a valid ${currency.ticker} address`,
);
return;
}
// Calculate the amount in BCH
let bchValue = value;
if (selectedCurrency !== 'XEC') {
bchValue = fiatToCrypto(value, fiatPrice);
}
try {
const link = await sendBch(
BCH,
wallet,
slpBalancesAndUtxos.nonSlpUtxos,
cleanAddress,
bchValue,
currency.defaultFee,
);
notification.success({
message: 'Success',
description: (
- Transaction successful. Click or tap here for more
- details
+ Transaction successful. Click to view in block
+ explorer.
),
- duration: 5,
+ duration: 3,
+ icon: ,
+ style: { width: '100%' },
});
} catch (e) {
// 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 (!e.error && !e.message) {
message = `Transaction failed: no response from ${getRestUrl()}.`;
} else if (
/Could not communicate with full node or other external service/.test(
e.error,
)
) {
message = 'Could not communicate with API. Please try again.';
} else if (
e.error &&
e.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 = e.message || e.error || JSON.stringify(e);
}
notification.error({
message: 'Error',
description: message,
duration: 5,
});
console.error(e);
}
}
const handleAddressChange = e => {
const { value, name } = e.target;
let error = false;
let addressString = value;
// parse address
const addressInfo = parseAddress(BCH, addressString);
/*
Model
addressInfo =
{
address: '',
isValid: false,
queryString: '',
amount: null,
};
*/
const { address, isValid, queryString, amount } = addressInfo;
// If query string,
// Show an alert that only amount and currency.ticker are supported
setQueryStringText(queryString);
// Is this valid address?
if (!isValid) {
error = `Invalid ${currency.ticker} address`;
// If valid address but token format
if (isValidTokenPrefix(address)) {
error = `Token addresses are not supported for ${currency.ticker} sends`;
}
}
setSendBchAddressError(error);
// Set amount if it's in the query string
if (amount !== null) {
// Set currency to BCHA
setSelectedCurrency(currency.ticker);
// Use this object to mimic user input and get validation for the value
let amountObj = {
target: {
name: 'value',
value: amount,
},
};
handleBchAmountChange(amountObj);
setFormData({
...formData,
value: amount,
});
}
// Set address field to user input
setFormData(p => ({
...p,
[name]: value,
}));
};
const 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(BCH, slpBalancesAndUtxos.nonSlpUtxos);
const txFeeBch = txFeeSats / 10 ** currency.cashDecimals;
let value =
balances.totalBalance - txFeeBch >= 0
? (balances.totalBalance - txFeeBch).toFixed(
currency.cashDecimals,
)
: 0;
setFormData({
...formData,
value,
});
} catch (err) {
console.log(`Error in onMax:`);
console.log(err);
message.error(
'Unable to calculate the max value due to network errors',
);
}
};
// Display price in USD below input field for send amount, if it can be calculated
let fiatPriceString = '';
if (fiatPrice !== null && !isNaN(formData.value)) {
if (selectedCurrency === currency.ticker) {
fiatPriceString = `${
cashtabSettings
? `${
currency.fiatCurrencies[cashtabSettings.fiatCurrency]
.symbol
} `
: '$ '
} ${(fiatPrice * Number(formData.value)).toFixed(2)} ${
cashtabSettings && cashtabSettings.fiatCurrency
? cashtabSettings.fiatCurrency.toUpperCase()
: 'USD'
}`;
} else {
fiatPriceString = `${
formData.value ? fiatToCrypto(formData.value, fiatPrice) : '0'
} ${currency.ticker}`;
}
}
const priceApiError = fiatPrice === null && selectedCurrency !== 'XEC';
return (
<>
Are you sure you want to send {formData.value}{' '}
{currency.ticker} to {formData.address}?
{!balances.totalBalance ? (
You currently have 0 {currency.ticker}
Deposit some funds to use this feature
) : (
<>
{fiatPrice !== null && (
)}
>
)}
>
);
};
/*
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);
},
};
export default SendBCH;
diff --git a/web/cashtab/src/components/Send/SendToken.js b/web/cashtab/src/components/Send/SendToken.js
index d81366466..5e737db72 100644
--- a/web/cashtab/src/components/Send/SendToken.js
+++ b/web/cashtab/src/components/Send/SendToken.js
@@ -1,459 +1,462 @@
import React, { useState, useEffect } from 'react';
import { WalletContext } from '@utils/context';
import {
Form,
notification,
message,
Row,
Col,
Alert,
Descriptions,
} from 'antd';
import Paragraph from 'antd/lib/typography/Paragraph';
import PrimaryButton, {
SecondaryButton,
} from '@components/Common/PrimaryButton';
import { CashLoader } from '@components/Common/CustomIcons';
import {
FormItemWithMaxAddon,
FormItemWithQRCodeAddon,
} from '@components/Common/EnhancedInputs';
import useBCH from '@hooks/useBCH';
import { BalanceHeader } from '@components/Common/BalanceHeader';
import { Redirect } from 'react-router-dom';
import useWindowDimensions from '@hooks/useWindowDimensions';
import { isMobile, isIOS, isSafari } from 'react-device-detect';
import { Img } from 'react-image';
import makeBlockie from 'ethereum-blockies-base64';
import BigNumber from 'bignumber.js';
import {
currency,
parseAddress,
isValidTokenPrefix,
} from '@components/Common/Ticker.js';
import { Event } from '@utils/GoogleAnalytics';
import {
getWalletState,
convertEtokenToSimpleledger,
} from '@utils/cashMethods';
+import { TokenReceivedNotificationIcon } from '@components/Common/CustomIcons';
const SendToken = ({ tokenId, jestBCH, passLoadingStatus }) => {
const { wallet, apiError } = React.useContext(WalletContext);
const walletState = getWalletState(wallet);
const { tokens, slpBalancesAndUtxos } = walletState;
const token = tokens.find(token => token.tokenId === tokenId);
const [tokenStats, setTokenStats] = useState(null);
const [queryStringText, setQueryStringText] = useState(null);
const [sendTokenAddressError, setSendTokenAddressError] = useState(false);
const [sendTokenAmountError, setSendTokenAmountError] = 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 = width < 769 && isMobile && !(isIOS && !isSafari);
const [formData, setFormData] = useState({
dirty: true,
value: '',
address: '',
});
const { getBCH, getRestUrl, sendToken, getTokenStats } = useBCH();
// jestBCH is only ever specified for unit tests, otherwise app will use getBCH();
const BCH = jestBCH ? jestBCH : getBCH();
// Fetch token stats if you do not have them and API did not return an error
if (tokenStats === null) {
getTokenStats(BCH, tokenId).then(
result => {
setTokenStats(result);
},
err => {
console.log(`Error getting token stats: ${err}`);
},
);
}
async function submit() {
setFormData({
...formData,
dirty: false,
});
if (
!formData.address ||
!formData.value ||
Number(formData.value <= 0) ||
sendTokenAmountError
) {
return;
}
// Event("Category", "Action", "Label")
// Track number of SLPA send transactions and
// SLPA token IDs
Event('SendToken.js', 'Send', tokenId);
passLoadingStatus(true);
const { address, value } = formData;
// Clear params from address
let cleanAddress = address.split('?')[0];
// Convert to simpleledger prefix if etoken
cleanAddress = convertEtokenToSimpleledger(cleanAddress);
try {
const link = await sendToken(BCH, wallet, slpBalancesAndUtxos, {
tokenId: tokenId,
tokenReceiverAddress: cleanAddress,
amount: value,
});
notification.success({
message: 'Success',
description: (
- Transaction successful. Click or tap here for more
- details
+ Transaction successful. Click to view in block
+ explorer.
),
- duration: 5,
+ duration: 3,
+ icon: ,
+ style: { width: '100%' },
});
} catch (e) {
passLoadingStatus(false);
let message;
if (!e.error && !e.message) {
message = `Transaction failed: no response from ${getRestUrl()}.`;
} else if (
/Could not communicate with full node or other external service/.test(
e.error,
)
) {
message = 'Could not communicate with API. Please try again.';
} else {
message = e.message || e.error || JSON.stringify(e);
}
console.log(e);
notification.error({
message: 'Error',
description: message,
duration: 3,
});
console.error(e);
}
}
const handleSlpAmountChange = e => {
let error = false;
const { value, name } = e.target;
// test if exceeds balance using BigNumber
let isGreaterThanBalance = false;
if (!isNaN(value)) {
const bigValue = new BigNumber(value);
// Returns 1 if greater, -1 if less, 0 if the same, null if n/a
isGreaterThanBalance = bigValue.comparedTo(token.balance);
}
// Validate value for > 0
if (isNaN(value)) {
error = 'Amount must be a number';
} else if (value <= 0) {
error = 'Amount must be greater than 0';
} else if (token && token.balance && isGreaterThanBalance === 1) {
error = `Amount cannot exceed your ${token.info.tokenTicker} balance of ${token.balance}`;
} else if (!isNaN(value) && value.toString().includes('.')) {
if (value.toString().split('.')[1].length > token.info.decimals) {
error = `This token only supports ${token.info.decimals} decimal places`;
}
}
setSendTokenAmountError(error);
setFormData(p => ({
...p,
[name]: value,
}));
};
const handleTokenAddressChange = e => {
const { value, name } = e.target;
// validate for token address
// validate for parameters
// show warning that query strings are not supported
let error = false;
let addressString = value;
const addressInfo = parseAddress(BCH, addressString, true);
/*
Model
addressInfo =
{
address: '',
isValid: false,
queryString: '',
amount: null,
};
*/
const { address, isValid, queryString } = addressInfo;
// If query string,
// Show an alert that only amount and currency.ticker are supported
setQueryStringText(queryString);
// Is this valid address?
if (!isValid) {
error = 'Address is not a valid etoken: address';
// If valid address but token format
} else if (!isValidTokenPrefix(address)) {
error = `Cashtab only supports sending to ${currency.tokenPrefixes[0]} prefixed addresses`;
}
setSendTokenAddressError(error);
setFormData(p => ({
...p,
[name]: value,
}));
};
const onMax = async () => {
// Clear this error before updating field
setSendTokenAmountError(false);
try {
let value = token.balance;
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',
);
}
};
useEffect(() => {
// 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
passLoadingStatus(false);
}, [token]);
return (
<>
{!token && }
{token && (
<>
{tokenStats !== null && (
{token.info.decimals}
{token.tokenId}
{tokenStats && (
<>
{tokenStats.documentUri}
{tokenStats.timestampUnix !==
null
? new Date(
tokenStats.timestampUnix *
1000,
).toLocaleDateString()
: 'Just now (Genesis tx confirming)'}
{tokenStats.containsBaton
? 'No'
: 'Yes'}
{tokenStats.initialTokenQty.toLocaleString()}
{tokenStats.totalBurned.toLocaleString()}
{tokenStats.totalMinted.toLocaleString()}
{tokenStats.circulatingSupply.toLocaleString()}
>
)}
)}
>
)}
>
);
};
/*
passLoadingStatus must receive a default prop that is a function
in order to pass the rendering unit test in SendToken.test.js
status => {console.log(status)} is an arbitrary stub function
*/
SendToken.defaultProps = {
passLoadingStatus: status => {
console.log(status);
},
};
export default SendToken;
diff --git a/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap b/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap
index 7a3df08c9..f10a6f3db 100644
--- a/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap
+++ b/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap
@@ -1,254 +1,254 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP @generated
exports[`Wallet with BCH balances and tokens 1`] = `null`;
exports[`Wallet with BCH balances and tokens and state field 1`] = `
Array [
6.001
TBS
,
,
]
`;
exports[`Without wallet defined 1`] = `null`;
diff --git a/web/cashtab/src/components/Tokens/CreateTokenForm.js b/web/cashtab/src/components/Tokens/CreateTokenForm.js
index f90cf9a48..43beaac58 100644
--- a/web/cashtab/src/components/Tokens/CreateTokenForm.js
+++ b/web/cashtab/src/components/Tokens/CreateTokenForm.js
@@ -1,378 +1,380 @@
import React, { useState } from 'react';
import { AntdFormWrapper } from '@components/Common/EnhancedInputs';
import { TokenCollapse } from '@components/Common/StyledCollapse';
import { currency } from '@components/Common/Ticker.js';
import { WalletContext } from '@utils/context';
import {
isValidTokenName,
isValidTokenTicker,
isValidTokenDecimals,
isValidTokenInitialQty,
isValidTokenDocumentUrl,
} from '@utils/validation';
import { PlusSquareOutlined } from '@ant-design/icons';
import { SmartButton } from '@components/Common/PrimaryButton';
import { Collapse, Form, Input, Modal, notification } from 'antd';
const { Panel } = Collapse;
import Paragraph from 'antd/lib/typography/Paragraph';
import { TokenParamLabel } from '@components/Common/Atoms';
+import { TokenReceivedNotificationIcon } from '@components/Common/CustomIcons';
const CreateTokenForm = ({
BCH,
getRestUrl,
createToken,
disabled,
passLoadingStatus,
}) => {
const { wallet } = React.useContext(WalletContext);
// New Token Name
const [newTokenName, setNewTokenName] = useState('');
const [newTokenNameIsValid, setNewTokenNameIsValid] = useState(null);
const handleNewTokenNameInput = e => {
const { value } = e.target;
// validation
setNewTokenNameIsValid(isValidTokenName(value));
setNewTokenName(value);
};
// New Token Ticker
const [newTokenTicker, setNewTokenTicker] = useState('');
const [newTokenTickerIsValid, setNewTokenTickerIsValid] = useState(null);
const handleNewTokenTickerInput = e => {
const { value } = e.target;
// validation
setNewTokenTickerIsValid(isValidTokenTicker(value));
setNewTokenTicker(value);
};
// New Token Decimals
const [newTokenDecimals, setNewTokenDecimals] = useState(0);
const [newTokenDecimalsIsValid, setNewTokenDecimalsIsValid] = useState(
true,
);
const handleNewTokenDecimalsInput = e => {
const { value } = e.target;
// validation
setNewTokenDecimalsIsValid(isValidTokenDecimals(value));
// Also validate the supply here if it has not yet been set
if (newTokenInitialQtyIsValid !== null) {
setNewTokenInitialQtyIsValid(
isValidTokenInitialQty(value, newTokenDecimals),
);
}
setNewTokenDecimals(value);
};
// New Token Initial Quantity
const [newTokenInitialQty, setNewTokenInitialQty] = useState('');
const [newTokenInitialQtyIsValid, setNewTokenInitialQtyIsValid] = useState(
null,
);
const handleNewTokenInitialQtyInput = e => {
const { value } = e.target;
// validation
setNewTokenInitialQtyIsValid(
isValidTokenInitialQty(value, newTokenDecimals),
);
setNewTokenInitialQty(value);
};
// New Token document URL
const [newTokenDocumentUrl, setNewTokenDocumentUrl] = useState('');
// Start with this as true, field is not required
const [
newTokenDocumentUrlIsValid,
setNewTokenDocumentUrlIsValid,
] = useState(true);
const handleNewTokenDocumentUrlInput = e => {
const { value } = e.target;
// validation
setNewTokenDocumentUrlIsValid(isValidTokenDocumentUrl(value));
setNewTokenDocumentUrl(value);
};
// New Token fixed supply
// Only allow creation of fixed supply tokens until Minting support is added
// New Token document hash
// Do not include this; questionable value to casual users and requires significant complication
// Only enable CreateToken button if all form entries are valid
let tokenGenesisDataIsValid =
newTokenNameIsValid &&
newTokenTickerIsValid &&
newTokenDecimalsIsValid &&
newTokenInitialQtyIsValid &&
newTokenDocumentUrlIsValid;
// Modal settings
const [showConfirmCreateToken, setShowConfirmCreateToken] = useState(false);
const createPreviewedToken = async () => {
passLoadingStatus(true);
// If data is for some reason not valid here, bail out
if (!tokenGenesisDataIsValid) {
return;
}
// data must be valid and user reviewed to get here
const configObj = {
name: newTokenName,
ticker: newTokenTicker,
documentUrl:
newTokenDocumentUrl === ''
? 'https://cashtabapp.com/'
: newTokenDocumentUrl,
decimals: newTokenDecimals,
initialQty: newTokenInitialQty,
documentHash: '',
};
// create token with data in state fields
try {
const link = await createToken(
BCH,
wallet,
currency.defaultFee,
configObj,
);
notification.success({
message: 'Success',
description: (
- Token created! Click or tap here for more details
+ Token created! Click to view in block explorer.
),
- duration: 5,
+ icon: ,
+ style: { width: '100%' },
});
} catch (e) {
// 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 (!e.error && !e.message) {
message = `Transaction failed: no response from ${getRestUrl()}.`;
} else if (
/Could not communicate with full node or other external service/.test(
e.error,
)
) {
message = 'Could not communicate with API. Please try again.';
} else if (
e.error &&
e.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 = e.message || e.error || JSON.stringify(e);
}
notification.error({
message: 'Error',
description: message,
duration: 5,
});
console.error(e);
}
// Hide the modal
setShowConfirmCreateToken(false);
// Stop spinner
passLoadingStatus(false);
};
return (
<>
setShowConfirmCreateToken(false)}
>
Name: {newTokenName}
Ticker: {newTokenTicker}
Decimals: {newTokenDecimals}
Supply: {newTokenInitialQty}
Document URL:{' '}
{newTokenDocumentUrl === ''
? 'https://cashtabapp.com/'
: newTokenDocumentUrl}
<>
handleNewTokenNameInput(e)
}
/>
handleNewTokenTickerInput(e)
}
/>
handleNewTokenDecimalsInput(e)
}
/>
handleNewTokenInitialQtyInput(e)
}
/>
handleNewTokenDocumentUrlInput(e)
}
/>
setShowConfirmCreateToken(true)}
disabled={!tokenGenesisDataIsValid}
>
Create Token
>
>
);
};
/*
passLoadingStatus must receive a default prop that is a function
in order to pass the rendering unit test in CreateTokenForm.test.js
status => {console.log(status)} is an arbitrary stub function
*/
CreateTokenForm.defaultProps = {
passLoadingStatus: status => {
console.log(status);
},
};
export default CreateTokenForm;
diff --git a/web/cashtab/src/hooks/useWallet.js b/web/cashtab/src/hooks/useWallet.js
index 664d49bb7..051deb62a 100644
--- a/web/cashtab/src/hooks/useWallet.js
+++ b/web/cashtab/src/hooks/useWallet.js
@@ -1,1130 +1,1140 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect } from 'react';
import Paragraph from 'antd/lib/typography/Paragraph';
import { notification } from 'antd';
import useAsyncTimeout from '@hooks/useAsyncTimeout';
import usePrevious from '@hooks/usePrevious';
import useBCH from '@hooks/useBCH';
import BigNumber from 'bignumber.js';
import {
fromSmallestDenomination,
loadStoredWallet,
isValidStoredWallet,
} from '@utils/cashMethods';
import { isValidCashtabSettings } from '@utils/validation';
import localforage from 'localforage';
import { currency } from '@components/Common/Ticker';
import isEmpty from 'lodash.isempty';
import isEqual from 'lodash.isequal';
+import {
+ CashReceivedNotificationIcon,
+ TokenReceivedNotificationIcon,
+} from '@components/Common/CustomIcons';
const useWallet = () => {
const [wallet, setWallet] = useState(false);
const [cashtabSettings, setCashtabSettings] = useState(false);
const [fiatPrice, setFiatPrice] = useState(null);
const [apiError, setApiError] = useState(false);
const [checkFiatInterval, setCheckFiatInterval] = useState(null);
const {
getBCH,
getUtxos,
getHydratedUtxoDetails,
getSlpBalancesAndUtxos,
getTxHistory,
getTxData,
addTokenTxData,
} = useBCH();
const [loading, setLoading] = useState(true);
const [apiIndex, setApiIndex] = useState(0);
const [BCH, setBCH] = useState(getBCH(apiIndex));
const { balances, tokens, utxos } = isValidStoredWallet(wallet)
? wallet.state
: {
balances: {},
tokens: [],
utxos: null,
};
const previousBalances = usePrevious(balances);
const previousTokens = usePrevious(tokens);
const previousWallet = usePrevious(wallet);
const previousUtxos = usePrevious(utxos);
// If you catch API errors, call this function
const tryNextAPI = () => {
let currentApiIndex = apiIndex;
// How many APIs do you have?
const apiString = process.env.REACT_APP_BCHA_APIS;
const apiArray = apiString.split(',');
console.log(`You have ${apiArray.length} APIs to choose from`);
console.log(`Current selection: ${apiIndex}`);
// If only one, exit
if (apiArray.length === 0) {
console.log(
`There are no backup APIs, you are stuck with this error`,
);
return;
} else if (currentApiIndex < apiArray.length - 1) {
currentApiIndex += 1;
console.log(
`Incrementing API index from ${apiIndex} to ${currentApiIndex}`,
);
} else {
// Otherwise use the first option again
console.log(`Retrying first API index`);
currentApiIndex = 0;
}
//return setApiIndex(currentApiIndex);
console.log(`Setting Api Index to ${currentApiIndex}`);
setApiIndex(currentApiIndex);
return setBCH(getBCH(currentApiIndex));
// If you have more than one, use the next one
// If you are at the "end" of the array, use the first one
};
const normalizeSlpBalancesAndUtxos = (slpBalancesAndUtxos, wallet) => {
const Accounts = [wallet.Path245, wallet.Path145, wallet.Path1899];
slpBalancesAndUtxos.nonSlpUtxos.forEach(utxo => {
const derivatedAccount = Accounts.find(
account => account.cashAddress === utxo.address,
);
utxo.wif = derivatedAccount.fundingWif;
});
return slpBalancesAndUtxos;
};
const normalizeBalance = slpBalancesAndUtxos => {
const totalBalanceInSatoshis = slpBalancesAndUtxos.nonSlpUtxos.reduce(
(previousBalance, utxo) => previousBalance + utxo.value,
0,
);
return {
totalBalanceInSatoshis,
totalBalance: fromSmallestDenomination(totalBalanceInSatoshis),
};
};
const deriveAccount = async (BCH, { masterHDNode, path }) => {
const node = BCH.HDNode.derivePath(masterHDNode, path);
const cashAddress = BCH.HDNode.toCashAddress(node);
const slpAddress = BCH.SLP.Address.toSLPAddress(cashAddress);
return {
cashAddress,
slpAddress,
fundingWif: BCH.HDNode.toWIF(node),
fundingAddress: BCH.SLP.Address.toSLPAddress(cashAddress),
legacyAddress: BCH.SLP.Address.toLegacyAddress(cashAddress),
};
};
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);
}
// Loading will remain true until API calls populate this legacy wallet
setWallet(wallet);
};
const haveUtxosChanged = (wallet, utxos, previousUtxos) => {
// Relevant points for this array comparing exercise
// https://stackoverflow.com/questions/13757109/triple-equal-signs-return-false-for-arrays-in-javascript-why
// https://stackoverflow.com/questions/7837456/how-to-compare-arrays-in-javascript
// If this is initial state
if (utxos === null) {
// Then make sure to get slpBalancesAndUtxos
return true;
}
// If this is the first time the wallet received utxos
if (typeof utxos === 'undefined') {
// Then they have certainly changed
return true;
}
if (typeof previousUtxos === 'undefined') {
// Compare to what you have in localStorage on startup
// If previousUtxos are undefined, see if you have previousUtxos in wallet state
// If you do, and it has everything you need, set wallet state with that instead of calling hydrateUtxos on all utxos
if (isValidStoredWallet(wallet)) {
// Convert all the token balance figures to big numbers
const liveWalletState = loadStoredWallet(wallet.state);
wallet.state = liveWalletState;
return setWallet(wallet);
}
// If wallet in storage is a legacy wallet or otherwise does not have all state fields,
// then assume utxos have changed
return true;
}
// return true for empty array, since this means you definitely do not want to skip the next API call
if (utxos && utxos.length === 0) {
return true;
}
// If wallet is valid, compare what exists in written wallet state instead of former api call
let utxosToCompare = previousUtxos;
if (isValidStoredWallet(wallet)) {
try {
utxosToCompare = wallet.state.utxos;
} catch (err) {
console.log(`Error setting utxos to wallet.state.utxos`, err);
console.log(`Wallet at err`, wallet);
// If this happens, assume utxo set has changed
return true;
}
}
// Compare utxo sets
return !isEqual(utxos, utxosToCompare);
};
const update = async ({ wallet }) => {
//console.log(`tick()`);
//console.time("update");
try {
if (!wallet) {
return;
}
const cashAddresses = [
wallet.Path245.cashAddress,
wallet.Path145.cashAddress,
wallet.Path1899.cashAddress,
];
const utxos = await getUtxos(BCH, cashAddresses);
// If an error is returned or utxos from only 1 address are returned
if (!utxos || isEmpty(utxos) || utxos.error || utxos.length < 2) {
// Throw error here to prevent more attempted api calls
// as you are likely already at rate limits
throw new Error('Error fetching utxos');
}
// Need to call with wallet as a parameter rather than trusting it is in state, otherwise can sometimes get wallet=false from haveUtxosChanged
const utxosHaveChanged = haveUtxosChanged(
wallet,
utxos,
previousUtxos,
);
// If the utxo set has not changed,
if (!utxosHaveChanged) {
// remove api error here; otherwise it will remain if recovering from a rate
// limit error with an unchanged utxo set
setApiError(false);
// then wallet.state has not changed and does not need to be updated
//console.timeEnd("update");
return;
}
const hydratedUtxoDetails = await getHydratedUtxoDetails(
BCH,
utxos,
);
const slpBalancesAndUtxos = await getSlpBalancesAndUtxos(
hydratedUtxoDetails,
);
const txHistory = await getTxHistory(BCH, cashAddresses);
const parsedTxHistory = await getTxData(BCH, txHistory);
const parsedWithTokens = await addTokenTxData(BCH, parsedTxHistory);
console.log(`slpBalancesAndUtxos`, slpBalancesAndUtxos);
if (typeof slpBalancesAndUtxos === 'undefined') {
console.log(`slpBalancesAndUtxos is undefined`);
throw new Error('slpBalancesAndUtxos is undefined');
}
const { tokens } = slpBalancesAndUtxos;
const newState = {
balances: {},
tokens: [],
slpBalancesAndUtxos: [],
};
newState.slpBalancesAndUtxos = normalizeSlpBalancesAndUtxos(
slpBalancesAndUtxos,
wallet,
);
newState.balances = normalizeBalance(slpBalancesAndUtxos);
newState.tokens = tokens;
newState.parsedTxHistory = parsedWithTokens;
newState.utxos = utxos;
newState.hydratedUtxoDetails = hydratedUtxoDetails;
// 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);
//console.timeEnd("update");
// Try another endpoint
console.log(`Trying next API...`);
tryNextAPI();
}
//console.timeEnd("update");
};
const getActiveWalletFromLocalForage = async () => {
let wallet;
try {
wallet = await localforage.getItem('wallet');
} catch (err) {
console.log(`Error in getActiveWalletFromLocalForage`, err);
wallet = null;
}
return wallet;
};
/*
const getSavedWalletsFromLocalForage = async () => {
let savedWallets;
try {
savedWallets = await localforage.getItem('savedWallets');
} catch (err) {
console.log(`Error in getSavedWalletsFromLocalForage`, err);
savedWallets = null;
}
return savedWallets;
};
*/
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
if (existingWallet && !existingWallet.Path1899) {
console.log(`Wallet does not have Path1899`);
existingWallet = await migrateLegacyWallet(BCH, 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 (BCH, wallet) => {
console.log(`migrateLegacyWallet`);
console.log(`legacyWallet`, wallet);
const NETWORK = process.env.REACT_APP_NETWORK;
const mnemonic = wallet.mnemonic;
const rootSeedBuffer = await BCH.Mnemonic.toSeed(mnemonic);
let masterHDNode;
if (NETWORK === `mainnet`) {
masterHDNode = BCH.HDNode.fromSeed(rootSeedBuffer);
} else {
masterHDNode = BCH.HDNode.fromSeed(rootSeedBuffer, 'testnet');
}
const Path1899 = await deriveAccount(BCH, {
masterHDNode,
path: "m/44'/1899'/0'/0/0",
});
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 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 NETWORK = process.env.REACT_APP_NETWORK;
const mnemonic = wallet.mnemonic;
const rootSeedBuffer = await BCH.Mnemonic.toSeed(mnemonic);
let masterHDNode;
if (NETWORK === `mainnet`) {
masterHDNode = BCH.HDNode.fromSeed(rootSeedBuffer);
} else {
masterHDNode = BCH.HDNode.fromSeed(rootSeedBuffer, 'testnet');
}
const Path245 = await deriveAccount(BCH, {
masterHDNode,
path: "m/44'/245'/0'/0/0",
});
const Path145 = await deriveAccount(BCH, {
masterHDNode,
path: "m/44'/145'/0'/0/0",
});
const Path1899 = await deriveAccount(BCH, {
masterHDNode,
path: "m/44'/1899'/0'/0/0",
});
let name = Path1899.cashAddress.slice(12, 17);
// 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 => {
let savedWallets;
try {
savedWallets = await localforage.getItem('savedWallets');
if (savedWallets === null) {
savedWallets = [];
}
} catch (err) {
console.log(`Error in getSavedWallets`);
console.log(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);
}
}
return savedWallets;
};
const activateWallet = async 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
*/
let currentlyActiveWallet;
try {
currentlyActiveWallet = await localforage.getItem('wallet');
} catch (err) {
console.log(
`Error in localforage.getItem("wallet") in activateWallet()`,
);
return 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 cashtabapp.com/, their active wallet will be migrated to Path1899 by
the getWallet function
Wallets in savedWallets are migrated when they are activated, in this function
Two cases to handle
1 - currentlyActiveWallet has Path1899, but its stored keyvalue pair in savedWallets does not
> Update savedWallets so that Path1899 is added to currentlyActiveWallet
2 - walletToActivate does not have Path1899
> Update walletToActivate with Path1899 before activation
*/
// Need to handle a similar situation with state
// If you find the activeWallet in savedWallets but without state, resave active wallet with state
// Note you do not have the Case 2 described above here, as wallet state is added in the update() function of useWallet.js
// Also note, since state can be expected to change frequently (unlike path deriv), you will likely save it every time you activate a new wallet
// Check savedWallets for currentlyActiveWallet
let walletInSavedWallets = false;
for (let i = 0; i < savedWallets.length; i += 1) {
if (savedWallets[i].name === currentlyActiveWallet.name) {
walletInSavedWallets = true;
// Check savedWallets for unmigrated currentlyActiveWallet
if (!savedWallets[i].Path1899) {
// Case 1, described above
savedWallets[i].Path1899 = currentlyActiveWallet.Path1899;
}
/*
Update wallet state
Note, this makes previous `walletUnmigrated` variable redundant
savedWallets[i] should always be updated, since wallet state can be expected to change most of the time
*/
savedWallets[i].state = currentlyActiveWallet.state;
}
}
// 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() 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
if (!walletToActivate.Path1899) {
// Case 2, described above
console.log(`Case 2: Wallet to activate does not have Path1899`);
console.log(
`Wallet to activate from SavedWallets does not have Path1899`,
);
console.log(`walletToActivate`, walletToActivate);
walletToActivate = await migrateLegacyWallet(BCH, 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;
}
}
// Make sure stored wallet is in correct format to be used as live wallet
if (isValidStoredWallet(walletToActivate)) {
// Convert all the token balance figures to big numbers
const liveWalletState = loadStoredWallet(walletToActivate.state);
walletToActivate.state = liveWalletState;
}
return walletToActivate;
};
const renameWallet = async (oldName, newName) => {
// Load savedWallets
let savedWallets;
try {
savedWallets = await localforage.getItem('savedWallets');
} catch (err) {
console.log(
`Error in await localforage.getItem("savedWallets") in renameWallet`,
);
console.log(err);
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
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 renameWallet()`,
);
return false;
}
return true;
};
const deleteWallet = async walletToBeDeleted => {
// 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`,
);
console.log(err);
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) {
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()`,
);
return false;
}
return true;
};
const addNewSavedWallet = async importMnemonic => {
// Add a new wallet to savedWallets from importMnemonic or just new wallet
const lang = 'english';
// create 128 bit BIP39 mnemonic
const Bip39128BitMnemonic = importMnemonic
? importMnemonic
: BCH.Mnemonic.generate(128, BCH.Mnemonic.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()`,
);
console.log(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.`);
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}`,
);
console.log(`savedWallets`, savedWallets);
console.log(err);
}
return true;
};
const createWallet = async importMnemonic => {
const lang = 'english';
// create 128 bit BIP39 mnemonic
const Bip39128BitMnemonic = importMnemonic
? importMnemonic
: BCH.Mnemonic.generate(128, BCH.Mnemonic.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;
};
const validateMnemonic = (
mnemonic,
wordlist = BCH.Mnemonic.wordLists().english,
) => {
let mnemonicTestOutput;
try {
mnemonicTestOutput = BCH.Mnemonic.validate(mnemonic, wordlist);
if (mnemonicTestOutput === 'Valid mnemonic') {
return true;
} else {
return false;
}
} catch (err) {
console.log(err);
return false;
}
};
const handleUpdateWallet = async setWallet => {
await loadWalletFromStorageOnStartup(setWallet);
};
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 not valid, also set cashtabSettings to default
setCashtabSettings(currency.defaultSettings);
return currency.defaultSettings;
};
// 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
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 (
Object.keys(currentSettings).includes(key) &&
currency.settingsValidation[key].includes(newValue)
) {
// Update settings
newSettings = currentSettings;
newSettings[key] = newValue;
}
// 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
if (
previousBalances &&
balances &&
'totalBalance' in previousBalances &&
'totalBalance' in balances &&
new BigNumber(balances.totalBalance)
.minus(previousBalances.totalBalance)
.gt(0)
) {
notification.success({
message: 'Transaction received',
description: (
+{' '}
{parseFloat(
Number(
balances.totalBalance -
previousBalances.totalBalance,
).toFixed(currency.cashDecimals),
).toLocaleString()}{' '}
{currency.ticker}{' '}
{cashtabSettings &&
cashtabSettings.fiatCurrency &&
`(${
currency.fiatCurrencies[
cashtabSettings.fiatCurrency
].symbol
}${(
Number(
balances.totalBalance -
previousBalances.totalBalance,
) * fiatPrice
).toFixed(
currency.cashDecimals,
)} ${cashtabSettings.fiatCurrency.toUpperCase()})`}
),
duration: 3,
+ icon: ,
+ style: { width: '100%' },
});
}
- // Parse for incoming SLP transactions
+ // Parse for incoming eToken transactions
if (
tokens &&
tokens[0] &&
tokens[0].balance &&
previousTokens &&
previousTokens[0] &&
previousTokens[0].balance
) {
// 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,
);
//console.log(`tokenIds`, tokenIds);
//console.log(`previousTokenIds`, previousTokenIds);
// 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];
//console.log(newTokenId);
// How much of this tokenId did you get?
// would be at
// Find where the newTokenId is
const receivedTokenObjectIndex = tokens.findIndex(
x => x.tokenId === newTokenId,
);
//console.log(`receivedTokenObjectIndex`, receivedTokenObjectIndex);
// Calculate amount received
//console.log(`receivedTokenObject:`, tokens[receivedTokenObjectIndex]);
const receivedSlpQty = tokens[
receivedTokenObjectIndex
].balance.toString();
const receivedSlpTicker =
tokens[receivedTokenObjectIndex].info.tokenTicker;
const receivedSlpName =
tokens[receivedTokenObjectIndex].info.tokenName;
//console.log(`receivedSlpQty`, receivedSlpQty);
// Notification if you received SLP
if (receivedSlpQty > 0) {
notification.success({
message: `${currency.tokenTicker} transaction received: ${receivedSlpTicker}`,
description: (
You received {receivedSlpQty} {receivedSlpName}
),
- duration: 5,
+ duration: 3,
+ icon: ,
+ style: { width: '100%' },
});
}
//
} 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 (tokens[i].balance.gt(previousTokens[i].balance)) {
// Received this token
// console.log(`previousTokenId`, previousTokens[i].tokenId);
// console.log(`currentTokenId`, tokens[i].tokenId);
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 = tokens[i].balance.minus(
previousTokens[i].balance,
);
const receivedSlpTicker = tokens[i].info.tokenTicker;
const receivedSlpName = tokens[i].info.tokenName;
notification.success({
- message: `${currency.tokenTicker} Transaction received: ${receivedSlpTicker}`,
+ message: `${currency.tokenTicker} transaction received: ${receivedSlpTicker}`,
description: (
You received {receivedSlpQty.toString()}{' '}
{receivedSlpName}
),
- duration: 5,
+ duration: 3,
+ icon: ,
+ style: { width: '100%' },
});
}
}
}
}
// Update wallet every 10s
useAsyncTimeout(async () => {
const wallet = await getWallet();
update({
wallet,
}).finally(() => {
setLoading(false);
});
}, 10000);
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);
//console.log(`bchPrice`, bchPrice);
} catch (err) {
console.log(`Error fetching BCH Price`);
console.log(err);
}
try {
bchPriceJson = await bchPrice.json();
//console.log(`bchPriceJson`, bchPriceJson);
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);
const initialSettings = await loadCashtabSettings();
initializeFiatPriceApi(initialSettings.fiatCurrency);
}, []);
return {
BCH,
wallet,
fiatPrice,
loading,
apiError,
cashtabSettings,
changeCashtabSettings,
getActiveWalletFromLocalForage,
getWallet,
validateMnemonic,
getWalletDetails,
getSavedWallets,
migrateLegacyWallet,
createWallet: async importMnemonic => {
setLoading(true);
const newWallet = await createWallet(importMnemonic);
setWallet(newWallet);
update({
wallet: newWallet,
}).finally(() => setLoading(false));
},
activateWallet: async walletToActivate => {
setLoading(true);
const newWallet = await activateWallet(walletToActivate);
setWallet(newWallet);
if (isValidStoredWallet(walletToActivate)) {
// If you have all state parameters needed in storage, immediately load the wallet
setLoading(false);
} else {
// If the wallet is missing state parameters in storage, wait for API info
// This handles case of unmigrated legacy wallet
update({
wallet: newWallet,
}).finally(() => setLoading(false));
}
},
addNewSavedWallet,
renameWallet,
deleteWallet,
};
};
export default useWallet;