Page MenuHomePhabricator

D10758.id31742.diff
No OneTemporary

D10758.id31742.diff

diff --git a/web/cashtab/extension/src/components/App.js b/web/cashtab/extension/src/components/App.js
--- a/web/cashtab/extension/src/components/App.js
+++ b/web/cashtab/extension/src/components/App.js
@@ -10,11 +10,13 @@
CaretRightOutlined,
SettingFilled,
AppstoreAddOutlined,
+ UngroupOutlined,
} from '@ant-design/icons';
import Wallet from '@components/Wallet/Wallet';
import Tokens from '@components/Tokens/Tokens';
import Send from '@components/Send/Send';
import SendToken from '@components/Send/SendToken';
+import Tools from '@components/Tools/Tools';
import Configure from '@components/Configure/Configure';
import NotFound from '@components/NotFound';
import CashTab from '@assets/cashtab_xec.png';
@@ -101,10 +103,10 @@
outline: none;
}
cursor: pointer;
- padding: 24px 12px 12px 12px;
- margin: 0 28px;
+ padding: 24px 6px 12px 12px;
+ margin: 0 18px;
@media (max-width: 475px) {
- margin: 0 20px;
+ margin: 0 10px;
}
@media (max-width: 420px) {
margin: 0 12px;
@@ -114,7 +116,7 @@
}
background-color: ${props => props.theme.footer.background};
border: none;
- font-size: 12px;
+ font-size: 10px;
font-weight: bold;
.anticon {
display: block;
@@ -274,6 +276,13 @@
/>
)}
/>
+ <Route path="/tools">
+ <Tools
+ passLoadingStatus={
+ setLoadingUtxosAfterSend
+ }
+ />
+ </Route>
<Route path="/configure">
<Configure />
</Route>
@@ -306,6 +315,13 @@
<CaretRightOutlined />
Send
</NavButton>
+ <NavButton
+ active={selectedKey === 'tools'}
+ onClick={() => history.push('/tools')}
+ >
+ <UngroupOutlined />
+ Tools
+ </NavButton>
<NavButton
active={selectedKey === 'configure'}
onClick={() => history.push('/configure')}
diff --git a/web/cashtab/package-lock.json b/web/cashtab/package-lock.json
--- a/web/cashtab/package-lock.json
+++ b/web/cashtab/package-lock.json
@@ -18,6 +18,7 @@
"ecashaddrjs": "^1.0.1",
"ecies-lite": "^1.0.7",
"ethereum-blockies-base64": "^1.0.2",
+ "etoken-list": "^1.0.1",
"localforage": "^1.9.0",
"lodash.isempty": "^4.4.0",
"lodash.isequal": "^4.5.0",
@@ -4987,7 +4988,6 @@
},
"node_modules/base64-js": {
"version": "1.5.1",
- "dev": true,
"funding": [
{
"type": "github",
@@ -8121,9 +8121,9 @@
}
},
"node_modules/ecashaddrjs": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/ecashaddrjs/-/ecashaddrjs-1.0.1.tgz",
- "integrity": "sha512-yozijdSLtuzi0vBj1BJ7MfAy/l7HQSGBfog9Ad+8M5sKT2gkSg5fTx0Rwh1z9heJJunagHXJ8DGsziPH8BKpeQ==",
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/ecashaddrjs/-/ecashaddrjs-1.0.7.tgz",
+ "integrity": "sha512-KsvHYLlYtLr/GBkEPiwwQDIDBzqRx61qC34n1puHKOjVE4Uwg3syHccjFCqNynLa6T6xI0Rd7ByCRUJcuJcoIw==",
"dependencies": {
"big-integer": "1.6.36"
}
@@ -9110,6 +9110,73 @@
"pnglib": "0.0.1"
}
},
+ "node_modules/etoken-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/etoken-list/-/etoken-list-1.0.1.tgz",
+ "integrity": "sha512-k64wg2JVWmAdOwMggZswidnL9jD3qRUW2Tvo1s03ubIhyx/vYSw8LrxpKmor67x6h31EdzR0TD2pEYrFj7ra7w==",
+ "dependencies": {
+ "axios": "^0.19.2",
+ "big.js": "^5.2.2",
+ "buffer": "^5.6.0",
+ "ecashaddrjs": "^1.0.6"
+ }
+ },
+ "node_modules/etoken-list/node_modules/axios": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
+ "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
+ "deprecated": "Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410",
+ "dependencies": {
+ "follow-redirects": "1.5.10"
+ }
+ },
+ "node_modules/etoken-list/node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/etoken-list/node_modules/debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/etoken-list/node_modules/follow-redirects": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
+ "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
+ "dependencies": {
+ "debug": "=3.1.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/etoken-list/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
"node_modules/eventemitter3": {
"version": "4.0.7",
"dev": true,
@@ -11113,7 +11180,6 @@
},
"node_modules/ieee754": {
"version": "1.2.1",
- "dev": true,
"funding": [
{
"type": "github",
@@ -31983,8 +32049,7 @@
}
},
"base64-js": {
- "version": "1.5.1",
- "dev": true
+ "version": "1.5.1"
},
"batch": {
"version": "0.6.1",
@@ -34203,9 +34268,9 @@
}
},
"ecashaddrjs": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/ecashaddrjs/-/ecashaddrjs-1.0.1.tgz",
- "integrity": "sha512-yozijdSLtuzi0vBj1BJ7MfAy/l7HQSGBfog9Ad+8M5sKT2gkSg5fTx0Rwh1z9heJJunagHXJ8DGsziPH8BKpeQ==",
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/ecashaddrjs/-/ecashaddrjs-1.0.7.tgz",
+ "integrity": "sha512-KsvHYLlYtLr/GBkEPiwwQDIDBzqRx61qC34n1puHKOjVE4Uwg3syHccjFCqNynLa6T6xI0Rd7ByCRUJcuJcoIw==",
"requires": {
"big-integer": "1.6.36"
}
@@ -34856,6 +34921,57 @@
"pnglib": "0.0.1"
}
},
+ "etoken-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/etoken-list/-/etoken-list-1.0.1.tgz",
+ "integrity": "sha512-k64wg2JVWmAdOwMggZswidnL9jD3qRUW2Tvo1s03ubIhyx/vYSw8LrxpKmor67x6h31EdzR0TD2pEYrFj7ra7w==",
+ "requires": {
+ "axios": "^0.19.2",
+ "big.js": "^5.2.2",
+ "buffer": "^5.6.0",
+ "ecashaddrjs": "^1.0.6"
+ },
+ "dependencies": {
+ "axios": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
+ "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
+ "requires": {
+ "follow-redirects": "1.5.10"
+ }
+ },
+ "buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "requires": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "follow-redirects": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
+ "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
+ "requires": {
+ "debug": "=3.1.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ }
+ }
+ },
"eventemitter3": {
"version": "4.0.7",
"dev": true
@@ -36282,8 +36398,7 @@
"requires": {}
},
"ieee754": {
- "version": "1.2.1",
- "dev": true
+ "version": "1.2.1"
},
"iferr": {
"version": "0.1.5",
diff --git a/web/cashtab/package.json b/web/cashtab/package.json
--- a/web/cashtab/package.json
+++ b/web/cashtab/package.json
@@ -14,6 +14,7 @@
"ecashaddrjs": "^1.0.1",
"ecies-lite": "^1.0.7",
"ethereum-blockies-base64": "^1.0.2",
+ "etoken-list": "^1.0.1",
"localforage": "^1.9.0",
"lodash.isempty": "^4.4.0",
"lodash.isequal": "^4.5.0",
diff --git a/web/cashtab/src/components/App.js b/web/cashtab/src/components/App.js
--- a/web/cashtab/src/components/App.js
+++ b/web/cashtab/src/components/App.js
@@ -10,11 +10,13 @@
CaretRightOutlined,
SettingFilled,
AppstoreAddOutlined,
+ UngroupOutlined,
} from '@ant-design/icons';
import Wallet from '@components/Wallet/Wallet';
import Tokens from '@components/Tokens/Tokens';
import Send from '@components/Send/Send';
import SendToken from '@components/Send/SendToken';
+import Tools from '@components/Tools/Tools';
import Configure from '@components/Configure/Configure';
import NotFound from '@components/NotFound';
import CashTab from '@assets/cashtab_xec.png';
@@ -106,10 +108,10 @@
outline: none;
}
cursor: pointer;
- padding: 24px 12px 12px 12px;
- margin: 0 28px;
+ padding: 24px 6px 12px 12px;
+ margin: 0 18px;
@media (max-width: 475px) {
- margin: 0 20px;
+ margin: 0 10px;
}
@media (max-width: 420px) {
margin: 0 12px;
@@ -119,7 +121,7 @@
}
background-color: ${props => props.theme.footer.background};
border: none;
- font-size: 10.5px;
+ font-size: 10px;
font-weight: bold;
.anticon {
display: block;
@@ -306,6 +308,13 @@
/>
)}
/>
+ <Route path="/tools">
+ <Tools
+ passLoadingStatus={
+ setLoadingUtxosAfterSend
+ }
+ />
+ </Route>
<Route path="/configure">
<Configure />
</Route>
@@ -339,6 +348,13 @@
<CaretRightOutlined />
Send
</NavButton>
+ <NavButton
+ active={selectedKey === 'tools'}
+ onClick={() => history.push('/tools')}
+ >
+ <UngroupOutlined />
+ Tools
+ </NavButton>
<NavButton
active={selectedKey === 'configure'}
onClick={() => history.push('/configure')}
diff --git a/web/cashtab/src/components/Common/Notifications.js b/web/cashtab/src/components/Common/Notifications.js
--- a/web/cashtab/src/components/Common/Notifications.js
+++ b/web/cashtab/src/components/Common/Notifications.js
@@ -170,6 +170,15 @@
});
};
+const generalNotification = msg => {
+ const notificationStyle = getDeviceNotificationStyle();
+ notification.success({
+ message: msg,
+ icon: <MessageSignedNotificationIcon />,
+ style: notificationStyle,
+ });
+};
+
export {
sendXecNotification,
createTokenNotification,
@@ -179,4 +188,5 @@
eTokenReceivedNotification,
errorNotification,
messageSignedNotification,
+ generalNotification,
};
diff --git a/web/cashtab/src/components/Common/Ticker.js b/web/cashtab/src/components/Common/Ticker.js
--- a/web/cashtab/src/components/Common/Ticker.js
+++ b/web/cashtab/src/components/Common/Ticker.js
@@ -22,6 +22,7 @@
tokenLogo: tokenLogo,
tokenPrefixes: ['etoken'],
tokenIconsUrl: 'https://etoken-icons.s3.us-west-2.amazonaws.com',
+ tokenDbUrl: 'https://tokendb.kingbch.com',
txHistoryCount: 10,
xecApiBatchSize: 20,
defaultSettings: { fiatCurrency: 'usd' },
diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js
--- a/web/cashtab/src/components/Send/Send.js
+++ b/web/cashtab/src/components/Send/Send.js
@@ -166,6 +166,15 @@
});
}
+ // if this was routed from the Tool screen's Dividend Calculator then
+ // switch to multiple recipient mode and prepopulate the recipients field
+ if (location && location.state && location.state.dividendRecipients) {
+ setIsOneToManyXECSend(true);
+ setFormData({
+ address: location.state.dividendRecipients,
+ });
+ }
+
// Do not set txInfo in state if query strings are not present
if (
!window.location ||
diff --git a/web/cashtab/src/components/Tools/Tools.js b/web/cashtab/src/components/Tools/Tools.js
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/components/Tools/Tools.js
@@ -0,0 +1,348 @@
+import React, { useState, useEffect } from 'react';
+import { Link } from 'react-router-dom';
+import PropTypes from 'prop-types';
+import BigNumber from 'bignumber.js';
+import { WalletContext } from '@utils/context';
+import { AntdFormWrapper } from '@components/Common/EnhancedInputs';
+import { AdvancedCollapse } from '@components/Common/StyledCollapse';
+import { Form, Alert, Collapse, Input } from 'antd';
+const { Panel } = Collapse;
+const { TextArea } = Input;
+import { Row, Col } from 'antd';
+import { SmartButton } from '@components/Common/PrimaryButton';
+import useBCH from '@hooks/useBCH';
+import {
+ errorNotification,
+ generalNotification,
+} from '@components/Common/Notifications';
+import { currency } from '@components/Common/Ticker.js';
+import BalanceHeader from '@components/Common/BalanceHeader';
+import BalanceHeaderFiat from '@components/Common/BalanceHeaderFiat';
+import {
+ getWalletState,
+ getLatestBlockHeight,
+ convertToEcashAddr,
+ fromSmallestDenomination,
+} from '@utils/cashMethods';
+import styled from 'styled-components';
+import {
+ isValidTokenId,
+ isValidXecDividend,
+ isValidDividendOutputsArray,
+} from '@utils/validation';
+import * as etokenList from 'etoken-list';
+
+const StyledLink = styled(Link)`
+ color: #0074c2;
+ text-decoration: none;
+ padding: 8px;
+ position: relative;
+ border: solid 1px silver;
+ border-radius: 10px;
+`;
+
+// Note jestBCH is only used for unit tests; BCHJS must be mocked for jest
+const Tools = ({ jestBCH, passLoadingStatus }) => {
+ const ContextValue = React.useContext(WalletContext);
+ const { wallet, fiatPrice, cashtabSettings } = ContextValue;
+ const walletState = getWalletState(wallet);
+ const { balances } = walletState;
+ // Modal settings
+
+ const [bchObj, setBchObj] = useState(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(() => {
+ // jestBCH is only ever specified for unit tests, otherwise app will use getBCH();
+ const BCH = jestBCH ? jestBCH : getBCH();
+
+ // set the BCH instance to state, for other functions to reference
+ setBchObj(BCH);
+ checkCalculateDividendBtn();
+ }, []);
+
+ const [tokenId, setTokenId] = useState('');
+ const [tokenIdIsValid, setTokenIdIsValid] = useState(null);
+ const [totalDividend, setTotalDividend] = useState('');
+ const [totalDividendIsValid, setTotalDividendIsValid] = useState(null);
+ const [dividendRecipients, setDividendRecipients] = useState('');
+ const [showCalculateDividendBtn, setShowCalculateDividendBtn] =
+ useState(false);
+ const [dividendOutputIsValid, setDividendOutputIsValid] = useState(true);
+ const [etokenHolders, setEtokenHolders] = useState('prorata');
+
+ const { getBCH } = useBCH();
+
+ const handleTokenIdInput = e => {
+ const { value } = e.target;
+
+ setTokenIdIsValid(isValidTokenId(value));
+ setTokenId(value);
+ checkCalculateDividendBtn();
+ };
+
+ const handleTotalDividendInput = e => {
+ const { value } = e.target;
+
+ setTotalDividendIsValid(isValidXecDividend(value));
+ setTotalDividend(value);
+ checkCalculateDividendBtn();
+ };
+
+ const checkCalculateDividendBtn = () => {
+ let showButton = tokenIdIsValid && totalDividendIsValid;
+
+ // if the above fields are valid, enable the calculate dividend button
+ setShowCalculateDividendBtn(showButton);
+ };
+
+ function handlePaste(event) {
+ event.preventDefault();
+ }
+
+ const calculateXecDividend = async () => {
+ passLoadingStatus(true);
+ generalNotification(
+ 'Querying the blockchain, please wait... (this takes approx. 10 seconds)',
+ );
+
+ const latestBlock = await getLatestBlockHeight(bchObj);
+
+ etokenList.Config.SetUrl(currency.tokenDbUrl);
+
+ let dividendList;
+ try {
+ dividendList = await etokenList.List.GetAddressListFor(
+ tokenId,
+ latestBlock,
+ true,
+ );
+ } catch (err) {
+ console.log('Tools.calculateXecDividend() error: ' + err);
+ throw err;
+ }
+
+ let totalTokenAmongstRecipients = BigNumber(0);
+ let totalHolders = BigNumber(dividendList.size); // amount of addresses that hold this eToken
+ setEtokenHolders(totalHolders);
+
+ // keep a cumulative total of each eToken holding in each address in dividendList
+ dividendList.forEach(
+ index =>
+ (totalTokenAmongstRecipients = totalTokenAmongstRecipients.plus(
+ BigNumber(index),
+ )),
+ );
+
+ let circToDivRatio = BigNumber(totalDividend).div(
+ totalTokenAmongstRecipients,
+ );
+
+ let resultString = '';
+
+ dividendList.forEach(
+ (element, index) =>
+ (resultString +=
+ convertToEcashAddr(index) +
+ ',' +
+ BigNumber(element).multipliedBy(circToDivRatio) +
+ '\n'),
+ );
+
+ resultString = resultString.substring(0, resultString.length - 1); // remove the final newline
+ setDividendRecipients(resultString);
+
+ if (!resultString) {
+ errorNotification(
+ null,
+ 'No holders found for eToken ID: ' + tokenId,
+ 'Dividend Calculation Error',
+ );
+ }
+
+ // validate the dividend values for each recipient
+ // Note: addresses are not validated as they are retrieved directly from onchain
+ setDividendOutputIsValid(isValidDividendOutputsArray(resultString));
+
+ passLoadingStatus(false);
+ };
+
+ return (
+ <>
+ {!balances.totalBalance ? (
+ ''
+ ) : (
+ <>
+ <BalanceHeader
+ balance={balances.totalBalance}
+ ticker={currency.ticker}
+ />
+ {fiatPrice !== null && !isNaN(balances.totalBalance) && (
+ <BalanceHeaderFiat
+ balance={balances.totalBalance}
+ settings={cashtabSettings}
+ fiatPrice={fiatPrice}
+ />
+ )}
+ </>
+ )}
+ <Row type="flex">
+ <Col span={24}>
+ <AdvancedCollapse
+ style={{
+ marginBottom: '24px',
+ }}
+ >
+ <Panel header="XEC Dividend Calculator" key="1">
+ <Alert
+ message={`Please ensure the qualifying eToken transactions to dividend recipients have at least one confirmation. The dividend calculator will not detect unconfirmed token balances.`}
+ type="warning"
+ />
+ <br />
+ <AntdFormWrapper>
+ <Form
+ style={{
+ width: 'auto',
+ }}
+ >
+ <Form.Item
+ validateStatus={
+ tokenIdIsValid === null ||
+ tokenIdIsValid
+ ? ''
+ : 'error'
+ }
+ help={
+ tokenIdIsValid === null ||
+ tokenIdIsValid
+ ? ''
+ : 'Invalid eToken ID'
+ }
+ >
+ <Input
+ addonBefore="eToken ID"
+ placeholder="Enter the eToken ID"
+ name="tokenId"
+ value={tokenId}
+ autoComplete="off"
+ onChange={e =>
+ handleTokenIdInput(e)
+ }
+ />
+ </Form.Item>
+ <Form.Item
+ validateStatus={
+ totalDividendIsValid === null ||
+ totalDividendIsValid
+ ? ''
+ : 'error'
+ }
+ help={
+ totalDividendIsValid === null ||
+ totalDividendIsValid
+ ? ''
+ : 'Invalid total XEC dividend'
+ }
+ >
+ <Input
+ addonBefore="Total XEC Dividend"
+ placeholder="Enter the total XEC dividend"
+ name="totalDividend"
+ type="number"
+ value={totalDividend}
+ onPaste={handlePaste}
+ autoComplete="off"
+ onChange={e =>
+ handleTotalDividendInput(e)
+ }
+ />
+ </Form.Item>
+ <Form.Item>
+ One to Many Dividend Payment Outputs
+ <TextArea
+ name="dividendRecipients"
+ placeholder="Please input parameters above."
+ value={dividendRecipients}
+ rows="10"
+ readOnly
+ />
+ </Form.Item>
+ {!dividendOutputIsValid &&
+ etokenHolders > 0 ? (
+ <Alert
+ description={
+ 'At least one dividend is below the minimum ' +
+ fromSmallestDenomination(
+ currency.dustSats,
+ ) +
+ ' XEC dust. Please increase the total XEC dividend.'
+ }
+ type="error"
+ showIcon
+ />
+ ) : (
+ ''
+ )}
+ <br />
+ <SmartButton
+ onClick={() => calculateXecDividend()}
+ disabled={!showCalculateDividendBtn}
+ >
+ Calculate Pro-Rata Dividend
+ </SmartButton>
+ <StyledLink
+ type="text"
+ to={{
+ pathname: `/send`,
+ state: {
+ dividendRecipients:
+ dividendRecipients,
+ },
+ }}
+ disabled={!dividendRecipients}
+ >
+ Copy to Send screen
+ </StyledLink>
+ &nbsp;&nbsp;
+ <StyledLink
+ type="text"
+ disabled={!dividendRecipients}
+ to={'#'}
+ onClick={() => {
+ navigator.clipboard.writeText(
+ dividendRecipients,
+ );
+ generalNotification(
+ 'Copied to clipboard',
+ );
+ }}
+ >
+ Copy to Clipboard
+ </StyledLink>
+ </Form>
+ </AntdFormWrapper>
+ </Panel>
+ </AdvancedCollapse>
+ </Col>
+ </Row>
+ </>
+ );
+};
+
+Tools.defaultProps = {
+ passLoadingStatus: status => {
+ console.log(status);
+ },
+};
+
+Tools.propTypes = {
+ jestBCH: PropTypes.object,
+ passLoadingStatus: PropTypes.func,
+};
+
+export default Tools;
diff --git a/web/cashtab/src/utils/__mocks__/mockXecDividendRecipients.js b/web/cashtab/src/utils/__mocks__/mockXecDividendRecipients.js
new file mode 100644
--- /dev/null
+++ b/web/cashtab/src/utils/__mocks__/mockXecDividendRecipients.js
@@ -0,0 +1,13 @@
+export const validXecDividendList =
+ 'ecash:qrqgwxnaxlfagezvr2zj4s9yee6rrs96dyguh7zsvk,7\n' +
+ 'ecash:qzsha6zk9m0f3hlfe5q007zdwnzvn3vwuuzel2lfzv,67\n' +
+ 'ecash:qqlkyzmeupf7q8t2ttf2u8xgyk286pg4wyz0v403dj,4376\n' +
+ 'ecash:qz2taa43tljkvnvkeqv9pyx337hmg0zclqfqjrqst4,673728\n' +
+ 'ecash:qp0hlj26nwjpk9c3f0umjz7qmwpzfh0fhckq4zj9s6,23673\n';
+
+export const invalidXecDividendList =
+ 'ecash:qrqgwxnaxlfagezvr2zj4s9yee6rrs96dyguh7zsvk,7\n' +
+ 'ecash:qzsha6zk9m0f3hlfe5q007zdwnzvn3vwuuzel2lfzv,3\n' +
+ 'ecash:qqlkyzmeupf7q8t2ttf2u8xgyk286pg4wyz0v403dj,4376\n' +
+ 'ecash:qz2taa43tljkvnvkeqv9pyx337hmg0zclqfqjrqst4,673728\n' +
+ 'ecash:qp0hlj26nwjpk9c3f0umjz7qmwpzfh0fhckq4zj9s6,23673\n';
diff --git a/web/cashtab/src/utils/__tests__/cashMethods.test.js b/web/cashtab/src/utils/__tests__/cashMethods.test.js
--- a/web/cashtab/src/utils/__tests__/cashMethods.test.js
+++ b/web/cashtab/src/utils/__tests__/cashMethods.test.js
@@ -13,6 +13,7 @@
toLegacyCash,
toLegacyToken,
toLegacyCashArray,
+ convertToEcashAddr,
} from '@utils/cashMethods';
import {
@@ -279,4 +280,60 @@
),
);
});
+
+ test('convertToEcashAddr successfully converts a valid eToken address to eCash', async () => {
+ const result = convertToEcashAddr(
+ 'etoken:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs',
+ );
+ expect(result).toStrictEqual(
+ 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8',
+ );
+ });
+
+ test('convertToEcashAddr successfully converts prefixless eToken address as input', async () => {
+ const result = convertToEcashAddr(
+ 'qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs',
+ );
+ expect(result).toStrictEqual(
+ 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8',
+ );
+ });
+
+ test('convertToEcashAddr throws error with an invalid eToken address as input', async () => {
+ const result = convertToEcashAddr('etoken:qpj9gcldpffcs');
+ expect(result).toStrictEqual(
+ new Error(
+ 'cashMethods.convertToEcashAddr() error: etoken:qpj9gcldpffcs is not a valid etoken address',
+ ),
+ );
+ });
+
+ test('convertToEcashAddr throws error with an ecash address as input', async () => {
+ const result = convertToEcashAddr(
+ 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8',
+ );
+ expect(result).toStrictEqual(
+ new Error(
+ 'cashMethods.convertToEcashAddr() error: ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8 is not a valid etoken address',
+ ),
+ );
+ });
+
+ test('convertToEcashAddr throws error with null input', async () => {
+ const result = convertToEcashAddr(null);
+ expect(result).toStrictEqual(
+ new Error(
+ 'cashMethods.convertToEcashAddr() error: No etoken address provided',
+ ),
+ );
+ });
+
+ test('convertToEcashAddr throws error with empty string input', async () => {
+ const result = convertToEcashAddr('');
+ expect(result).toStrictEqual(
+ new Error(
+ 'cashMethods.convertToEcashAddr() error: No etoken address provided',
+ ),
+ );
+ });
});
diff --git a/web/cashtab/src/utils/__tests__/validation.test.js b/web/cashtab/src/utils/__tests__/validation.test.js
--- a/web/cashtab/src/utils/__tests__/validation.test.js
+++ b/web/cashtab/src/utils/__tests__/validation.test.js
@@ -12,6 +12,9 @@
isValidEtokenAddress,
isValidXecSendAmount,
isValidSendToMany,
+ isValidTokenId,
+ isValidXecDividend,
+ isValidDividendOutputsArray,
} from '../validation';
import { currency } from '@components/Common/Ticker.js';
import { fromSmallestDenomination } from '@utils/cashMethods';
@@ -21,6 +24,10 @@
noCovidStatsInvalid,
cGenStatsValid,
} from '../__mocks__/mockTokenStats';
+import {
+ validXecDividendList,
+ invalidXecDividendList,
+} from '../__mocks__/mockXecDividendRecipients';
describe('Validation utils', () => {
it(`Returns 'false' if ${currency.ticker} send amount is a valid send amount`, () => {
@@ -350,4 +357,71 @@
const testXecSendAmount = undefined;
expect(isValidXecSendAmount(testXecSendAmount)).toBe(false);
});
+ it(`isValidTokenId accepts valid token ID`, () => {
+ const testValidTokenId =
+ '1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e';
+ expect(isValidTokenId(testValidTokenId)).toBe(true);
+ });
+ it(`isValidTokenId rejects null`, () => {
+ const testValidTokenId = null;
+ expect(isValidTokenId(testValidTokenId)).toBe(false);
+ });
+ it(`isValidTokenId rejects undefined`, () => {
+ const testValidTokenId = undefined;
+ expect(isValidTokenId(testValidTokenId)).toBe(false);
+ });
+ it(`isValidTokenId rejects empty string`, () => {
+ const testValidTokenId = '';
+ expect(isValidTokenId(testValidTokenId)).toBe(false);
+ });
+ it(`isValidTokenId rejects special character input`, () => {
+ const testValidTokenId = '^&$%@&^$@&%$!';
+ expect(isValidTokenId(testValidTokenId)).toBe(false);
+ });
+ it(`isValidTokenId rejects non-alphanumeric input`, () => {
+ const testValidTokenId = 99999999999;
+ expect(isValidTokenId(testValidTokenId)).toBe(false);
+ });
+ it(`isValidXecDividend accepts valid Total Dividend Amount`, () => {
+ const testDividendTotal = '1000000';
+ expect(isValidXecDividend(testDividendTotal)).toBe(true);
+ });
+ it(`isValidXecDividend rejects null`, () => {
+ const testDividendTotal = null;
+ expect(isValidXecDividend(testDividendTotal)).toBe(false);
+ });
+ it(`isValidXecDividend rejects undefined`, () => {
+ const testDividendTotal = undefined;
+ expect(isValidXecDividend(testDividendTotal)).toBe(false);
+ });
+ it(`isValidXecDividend rejects empty string`, () => {
+ const testDividendTotal = '';
+ expect(isValidXecDividend(testDividendTotal)).toBe(false);
+ });
+ it(`isValidTokenId rejects an alphanumeric input`, () => {
+ const testDividendTotal = 'a73hsyujs3737';
+ expect(isValidXecDividend(testDividendTotal)).toBe(false);
+ });
+ it(`isValidTokenId rejects a number !> 0 in string format`, () => {
+ const testDividendTotal = '0';
+ expect(isValidXecDividend(testDividendTotal)).toBe(false);
+ });
+ it(`isValidDividendOutputsArray accepts a dividend list with valid XEC values`, () => {
+ expect(isValidDividendOutputsArray(validXecDividendList)).toBe(true);
+ });
+ it(`isValidDividendOutputsArray rejects a dividend list with invalid XEC values`, () => {
+ expect(isValidDividendOutputsArray(invalidXecDividendList)).toBe(false);
+ });
+ it(`isValidDividendOutputsArray rejects null`, () => {
+ const testDividendListValues = null;
+ expect(isValidDividendOutputsArray(testDividendListValues)).toBe(false);
+ });
+ it(`isValidDividendOutputsArray rejects undefined`, () => {
+ const testDividendListValues = undefined;
+ expect(isValidDividendOutputsArray(testDividendListValues)).toBe(false);
+ });
+ it(`isValidDividendOutputsArray rejects empty string`, () => {
+ const testDividendListValues = '';
+ expect(isValidDividendOutputsArray(testDividendListValues)).toBe(false);
+ });
});
diff --git a/web/cashtab/src/utils/cashMethods.js b/web/cashtab/src/utils/cashMethods.js
--- a/web/cashtab/src/utils/cashMethods.js
+++ b/web/cashtab/src/utils/cashMethods.js
@@ -163,6 +163,40 @@
return wallet.state;
};
+export function convertToEcashAddr(eTokenAddress) {
+ if (!eTokenAddress) {
+ return new Error(
+ `cashMethods.convertToEcashAddr() error: No etoken address provided`,
+ );
+ }
+
+ // Confirm input is a valid eToken address
+ const isValidInput = isValidEtokenAddress(eTokenAddress);
+ if (!isValidInput) {
+ return new Error(
+ `cashMethods.convertToEcashAddr() error: ${eTokenAddress} is not a valid etoken address`,
+ );
+ }
+
+ // Check for etoken: prefix
+ const isPrefixedEtokenAddress = eTokenAddress.slice(0, 7) === 'etoken:';
+
+ // If no prefix, assume it is checksummed for an etoken: prefix
+ const testedEtokenAddr = isPrefixedEtokenAddress
+ ? eTokenAddress
+ : `etoken:${eTokenAddress}`;
+
+ let ecashAddress;
+ try {
+ const { type, hash } = cashaddr.decode(testedEtokenAddr);
+ ecashAddress = cashaddr.encode('ecash', type, hash);
+ } catch (err) {
+ return err;
+ }
+
+ return ecashAddress;
+}
+
export function convertToEcashPrefix(bitcoincashPrefixedAddress) {
// Prefix-less addresses may be valid, but the cashaddr.decode function used below
// will throw an error without a prefix. Hence, must ensure prefix to use that function.
@@ -421,3 +455,13 @@
return false;
};
+
+export const getLatestBlockHeight = async BCH => {
+ try {
+ const latestBlock = await BCH.Blockchain.getBlockCount();
+ return latestBlock;
+ } catch (err) {
+ console.log('cashMethods.getLatestBlockHeight() error: ' + err);
+ throw err;
+ }
+};
diff --git a/web/cashtab/src/utils/validation.js b/web/cashtab/src/utils/validation.js
--- a/web/cashtab/src/utils/validation.js
+++ b/web/cashtab/src/utils/validation.js
@@ -175,7 +175,6 @@
return isValidXecAddress;
};
-
export const isValidEtokenAddress = addr => {
/*
Returns true for a valid eToken address
@@ -226,3 +225,50 @@
parseFloat(xecSendAmount) >= fromSmallestDenomination(currency.dustSats)
);
};
+
+// XEC dividend field validations
+export const isValidTokenId = tokenId => {
+ const format = /[ `!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~]/;
+ const specialCharCheck = format.test(tokenId);
+
+ return (
+ typeof tokenId === 'string' &&
+ tokenId.length > 0 &&
+ tokenId.trim() != '' &&
+ !specialCharCheck
+ );
+};
+
+export const isValidXecDividend = xecDividend => {
+ return (
+ typeof xecDividend === 'string' &&
+ xecDividend.length > 0 &&
+ xecDividend.trim() != '' &&
+ BigNumber(xecDividend) > 0
+ );
+};
+
+export const isValidDividendOutputsArray = dividendOutputsArray => {
+ if (!dividendOutputsArray) {
+ return false;
+ }
+
+ const addressStringArray = dividendOutputsArray.split('\n');
+ const arrayLength = addressStringArray.length;
+ let isValid = true;
+
+ for (let i = 0; i < arrayLength; i++) {
+ let valueString = addressStringArray[i].split(',')[1];
+
+ // if the XEC being sent is less than dust sats
+ if (
+ new BigNumber(valueString).lt(
+ fromSmallestDenomination(currency.dustSats),
+ )
+ ) {
+ isValid = false;
+ }
+ }
+
+ return isValid;
+};

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 26, 11:15 (12 h, 34 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5573380
Default Alt Text
D10758.id31742.diff (41 KB)

Event Timeline