Page MenuHomePhabricator

D10758.id31901.diff
No OneTemporary

D10758.id31901.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
@@ -17,6 +17,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",
@@ -5024,7 +5025,6 @@
},
"node_modules/base64-js": {
"version": "1.5.1",
- "dev": true,
"funding": [
{
"type": "github",
@@ -8161,9 +8161,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"
}
@@ -9341,6 +9341,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,
@@ -11402,7 +11469,6 @@
},
"node_modules/ieee754": {
"version": "1.2.1",
- "dev": true,
"funding": [
{
"type": "github",
@@ -32734,8 +32800,7 @@
}
},
"base64-js": {
- "version": "1.5.1",
- "dev": true
+ "version": "1.5.1"
},
"batch": {
"version": "0.6.1",
@@ -34958,9 +35023,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"
}
@@ -35724,6 +35789,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
@@ -37190,8 +37306,7 @@
"dev": true
},
"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/assets/styles/theme.js b/web/cashtab/src/assets/styles/theme.js
--- a/web/cashtab/src/assets/styles/theme.js
+++ b/web/cashtab/src/assets/styles/theme.js
@@ -79,6 +79,7 @@
hoverShadow: '0px 3px 10px -5px rgba(0, 0, 0, 0.75)',
disabledOverlay: 'rgba(255, 255, 255, 0.5)',
},
+ styledLink: '#0074c2',
},
collapses: {
background: '#fbfcfd',
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/Atoms.js b/web/cashtab/src/components/Common/Atoms.js
--- a/web/cashtab/src/components/Common/Atoms.js
+++ b/web/cashtab/src/components/Common/Atoms.js
@@ -1,4 +1,5 @@
import styled from 'styled-components';
+import { Link } from 'react-router-dom';
export const LoadingCtn = styled.div`
width: 100%;
@@ -61,3 +62,12 @@
font-size: 12px;
}
`;
+
+export const StyledLink = styled(Link)`
+ color: ${props => props.theme.buttons.styledLink};
+ text-decoration: none;
+ padding: 8px;
+ position: relative;
+ border: solid 1px silver;
+ border-radius: 10px;
+`;
diff --git a/web/cashtab/src/components/Common/CustomIcons.js b/web/cashtab/src/components/Common/CustomIcons.js
--- a/web/cashtab/src/components/Common/CustomIcons.js
+++ b/web/cashtab/src/components/Common/CustomIcons.js
@@ -72,3 +72,5 @@
<LoadingOutlined />
</LoadingBlock>
);
+
+export const customSpinner = <LoadingOutlined style={{ fontSize: 24 }} spin />;
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/Configure/__tests__/__snapshots__/Configure.test.js.snap b/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap
--- a/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap
+++ b/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap
@@ -2,7 +2,7 @@
exports[`Configure with a wallet 1`] = `
<div
- className="sc-eHgmQL dUpMKn"
+ className="sc-cvbbAY jgDZYj"
>
<h2>
<span
@@ -110,7 +110,7 @@
</div>
</div>
<div
- className="sc-cvbbAY JVsRj"
+ className="sc-jWBwVP hHpkpj"
/>
<h2>
<span
@@ -188,7 +188,7 @@
Import Wallet
</button>
<div
- className="sc-cvbbAY JVsRj"
+ className="sc-jWBwVP hHpkpj"
/>
<h2>
<span
@@ -305,7 +305,7 @@
</div>
</div>
<div
- className="sc-cvbbAY JVsRj"
+ className="sc-jWBwVP hHpkpj"
/>
<h2>
<span
@@ -330,7 +330,7 @@
General Settings
</h2>
<div
- className="sc-jWBwVP ibcxAa"
+ className="sc-brqgnP fpmgSk"
>
<div
className="title"
@@ -393,11 +393,11 @@
</button>
</div>
<div
- className="sc-cvbbAY JVsRj"
+ className="sc-jWBwVP hHpkpj"
/>
[
<a
- className="sc-eNQAEJ jpqDcE"
+ className="sc-hMqMXs hzZwKs"
href="https://docs.cashtab.com/docs/"
rel="noreferrer"
target="_blank"
@@ -411,7 +411,7 @@
exports[`Configure without a wallet 1`] = `
<div
- className="sc-eHgmQL dUpMKn"
+ className="sc-cvbbAY jgDZYj"
>
<h2>
<span
@@ -481,7 +481,7 @@
</div>
</div>
<div
- className="sc-cvbbAY JVsRj"
+ className="sc-jWBwVP hHpkpj"
/>
<h2>
<span
@@ -559,7 +559,7 @@
Import Wallet
</button>
<div
- className="sc-cvbbAY JVsRj"
+ className="sc-jWBwVP hHpkpj"
/>
<h2>
<span
@@ -676,7 +676,7 @@
</div>
</div>
<div
- className="sc-cvbbAY JVsRj"
+ className="sc-jWBwVP hHpkpj"
/>
<h2>
<span
@@ -701,7 +701,7 @@
General Settings
</h2>
<div
- className="sc-jWBwVP ibcxAa"
+ className="sc-brqgnP fpmgSk"
>
<div
className="title"
@@ -764,11 +764,11 @@
</button>
</div>
<div
- className="sc-cvbbAY JVsRj"
+ className="sc-jWBwVP hHpkpj"
/>
[
<a
- className="sc-eNQAEJ jpqDcE"
+ className="sc-hMqMXs hzZwKs"
href="https://docs.cashtab.com/docs/"
rel="noreferrer"
target="_blank"
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
@@ -170,6 +170,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/Send/__tests__/__snapshots__/Send.test.js.snap b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap
--- a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap
+++ b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap
@@ -35,7 +35,7 @@
type="button"
>
<div
- className="sc-kEYyzF IFceh"
+ className="sc-kkGfuU hADXNQ"
>
Switch to multiple recipients
</div>
@@ -325,7 +325,7 @@
</div>
</div>
<h3
- className="sc-iAyFgw bGmpty"
+ className="sc-hSdWYo fxSPpr"
>
0
@@ -406,7 +406,7 @@
</div>
</div>,
<div
- className="sc-eNQAEJ fJrcEs"
+ className="sc-hMqMXs eVQpDx"
>
Signatures
</div>,
@@ -491,7 +491,7 @@
type="button"
>
<div
- className="sc-kEYyzF IFceh"
+ className="sc-kkGfuU hADXNQ"
>
Switch to multiple recipients
</div>
@@ -781,7 +781,7 @@
</div>
</div>
<h3
- className="sc-iAyFgw bGmpty"
+ className="sc-hSdWYo fxSPpr"
>
0
@@ -862,7 +862,7 @@
</div>
</div>,
<div
- className="sc-eNQAEJ fJrcEs"
+ className="sc-hMqMXs eVQpDx"
>
Signatures
</div>,
@@ -954,7 +954,7 @@
type="button"
>
<div
- className="sc-kEYyzF IFceh"
+ className="sc-kkGfuU hADXNQ"
>
Switch to multiple recipients
</div>
@@ -1244,7 +1244,7 @@
</div>
</div>
<h3
- className="sc-iAyFgw bGmpty"
+ className="sc-hSdWYo fxSPpr"
>
0
@@ -1326,7 +1326,7 @@
</div>
</div>,
<div
- className="sc-eNQAEJ fJrcEs"
+ className="sc-hMqMXs eVQpDx"
>
Signatures
</div>,
@@ -1411,7 +1411,7 @@
type="button"
>
<div
- className="sc-kEYyzF IFceh"
+ className="sc-kkGfuU hADXNQ"
>
Switch to multiple recipients
</div>
@@ -1701,7 +1701,7 @@
</div>
</div>
<h3
- className="sc-iAyFgw bGmpty"
+ className="sc-hSdWYo fxSPpr"
>
0
@@ -1782,7 +1782,7 @@
</div>
</div>,
<div
- className="sc-eNQAEJ fJrcEs"
+ className="sc-hMqMXs eVQpDx"
>
Signatures
</div>,
@@ -1867,7 +1867,7 @@
type="button"
>
<div
- className="sc-kEYyzF IFceh"
+ className="sc-kkGfuU hADXNQ"
>
Switch to multiple recipients
</div>
@@ -2157,7 +2157,7 @@
</div>
</div>
<h3
- className="sc-iAyFgw bGmpty"
+ className="sc-hSdWYo fxSPpr"
>
0
@@ -2238,7 +2238,7 @@
</div>
</div>,
<div
- className="sc-eNQAEJ fJrcEs"
+ className="sc-hMqMXs eVQpDx"
>
Signatures
</div>,
diff --git a/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap b/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap
--- a/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap
+++ b/web/cashtab/src/components/Tokens/__tests__/__snapshots__/Tokens.test.js.snap
@@ -367,13 +367,13 @@
onClick={[Function]}
>
<div
- className="sc-hSdWYo tUbiI"
+ className="sc-eHgmQL jrkpGQ"
>
<div
- className="sc-kkGfuU dSxERO"
+ className="sc-iAyFgw mPXlu"
/>
<div
- className="sc-iAyFgw clPeTt"
+ className="sc-hSdWYo hZkZtp"
>
6.001
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,357 @@
+import React, { useState, useEffect } from 'react';
+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, Modal, Spin, Progress } 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,
+ convertEtokenToEcashAddr,
+ fromSmallestDenomination,
+} from '@utils/cashMethods';
+import {
+ isValidTokenId,
+ isValidXecDividend,
+ isValidDividendOutputsArray,
+} from '@utils/validation';
+import { customSpinner } from '@components/Common/CustomIcons';
+import { StyledLink } from '@components/Common/Atoms';
+import * as etokenList from 'etoken-list';
+
+// 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;
+
+ const [bchObj, setBchObj] = useState(false);
+ const [isDivCalcModalVisible, setIsDivCalcModalVisible] = useState(false);
+ const [divCalcModalProgress, setDivCalcModalProgress] = useState(0); // the dynamic % progress bar
+
+ useEffect(() => {
+ // jestBCH is only ever specified for unit tests, otherwise app will use getBCH();
+ const BCH = jestBCH ? jestBCH : getBCH();
+
+ // set the BCH instance to state, for other functions to reference
+ setBchObj(BCH);
+ }, []);
+
+ const [tokenId, setTokenId] = useState('');
+ const [tokenIdIsValid, setTokenIdIsValid] = useState(null);
+ const [totalDividend, setTotalDividend] = useState('');
+ const [totalDividendIsValid, setTotalDividendIsValid] = useState(null);
+ const [dividendRecipients, setDividendRecipients] = useState('');
+ const [dividendOutputIsValid, setDividendOutputIsValid] = useState(true);
+ const [etokenHolders, setEtokenHolders] = useState(new BigNumber(0));
+ const [showDividendOutputs, setShowDividendOutputs] = useState(false);
+
+ const { getBCH } = useBCH();
+
+ const handleTokenIdInput = e => {
+ const { value } = e.target;
+ setTokenIdIsValid(isValidTokenId(value));
+ setTokenId(value);
+ };
+
+ const handleTotalDividendInput = e => {
+ const { value } = e.target;
+ setTotalDividendIsValid(isValidXecDividend(value));
+ setTotalDividend(value);
+ };
+
+ const calculateXecDividend = async () => {
+ // display dividend calculation message modal
+ setIsDivCalcModalVisible(true);
+ setShowDividendOutputs(false); // hide any previous dividend outputs
+ passLoadingStatus(true);
+ setDivCalcModalProgress(25); // updated progress bar to 25%
+
+ const latestBlock = await getLatestBlockHeight(bchObj);
+
+ setDivCalcModalProgress(50);
+
+ 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;
+ }
+
+ setDivCalcModalProgress(75);
+
+ let totalTokenAmongstRecipients = new BigNumber(0);
+ let totalHolders = new 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(
+ new BigNumber(index),
+ )),
+ );
+
+ let circToDivRatio = new BigNumber(totalDividend).div(
+ totalTokenAmongstRecipients,
+ );
+
+ let resultString = '';
+
+ dividendList.forEach(
+ (element, index) =>
+ (resultString +=
+ convertEtokenToEcashAddr(index) +
+ ',' +
+ new BigNumber(element).multipliedBy(circToDivRatio) +
+ '\n'),
+ );
+
+ resultString = resultString.substring(0, resultString.length - 1); // remove the final newline
+ setDividendRecipients(resultString);
+
+ setDivCalcModalProgress(100);
+
+ 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));
+ setShowDividendOutputs(true); // display the dividend outputs TextArea
+ setIsDivCalcModalVisible(false);
+ passLoadingStatus(false);
+ };
+
+ const handleDivCalcModalCancel = () => {
+ // disable both the modal and the loading status so the user can re calculate without waiting for the current calculation to finish
+ setIsDivCalcModalVisible(false);
+ passLoadingStatus(false);
+ };
+
+ let dividendCalcInputIsValid = tokenIdIsValid && totalDividendIsValid;
+
+ return (
+ <>
+ {!balances.totalBalance ? (
+ ''
+ ) : (
+ <>
+ <BalanceHeader
+ balance={balances.totalBalance}
+ ticker={currency.ticker}
+ />
+ {fiatPrice !== null && !isNaN(balances.totalBalance) && (
+ <BalanceHeaderFiat
+ balance={balances.totalBalance}
+ settings={cashtabSettings}
+ fiatPrice={fiatPrice}
+ />
+ )}
+ </>
+ )}
+
+ <Modal
+ title="Querying the eCash blockchain"
+ visible={isDivCalcModalVisible}
+ okButtonProps={{ style: { display: 'none' } }}
+ onCancel={handleDivCalcModalCancel}
+ >
+ <Spin indicator={customSpinner} />
+ &emsp; Please wait... (this takes approx. 10 seconds)
+ <Progress percent={divCalcModalProgress} />
+ </Modal>
+
+ <Row type="flex">
+ <Col span={24}>
+ <AdvancedCollapse
+ style={{
+ marginBottom: '24px',
+ }}
+ >
+ <Panel header="XEC Airdrop Calculator" key="1">
+ <Alert
+ message={`Please ensure the qualifying eToken transactions to airdrop recipients have at least one confirmation. The airdrop calculator will not detect unconfirmed token balances.`}
+ type="warning"
+ />
+ <br />
+ <AntdFormWrapper>
+ <Form
+ style={{
+ width: 'auto',
+ }}
+ >
+ <Form.Item
+ validateStatus={
+ tokenIdIsValid === null ||
+ tokenIdIsValid
+ ? ''
+ : 'error'
+ }
+ help={
+ tokenIdIsValid === null ||
+ tokenIdIsValid
+ ? ''
+ : 'Invalid eToken ID'
+ }
+ >
+ <Input
+ addonBefore="eToken ID"
+ placeholder="Enter the eToken ID"
+ name="tokenId"
+ value={tokenId}
+ onChange={e =>
+ handleTokenIdInput(e)
+ }
+ />
+ </Form.Item>
+ <Form.Item
+ validateStatus={
+ totalDividendIsValid === null ||
+ totalDividendIsValid
+ ? ''
+ : 'error'
+ }
+ help={
+ totalDividendIsValid === null ||
+ totalDividendIsValid
+ ? ''
+ : 'Invalid total XEC airdrop'
+ }
+ >
+ <Input
+ addonBefore="Total XEC airdrop"
+ placeholder="Enter the total XEC airdrop"
+ name="totalDividend"
+ type="number"
+ value={totalDividend}
+ onChange={e =>
+ handleTotalDividendInput(e)
+ }
+ />
+ </Form.Item>
+ <Form.Item>
+ <SmartButton
+ onClick={() =>
+ calculateXecDividend()
+ }
+ disabled={!dividendCalcInputIsValid}
+ >
+ Calculate Pro-Rata Airdrop
+ </SmartButton>
+ </Form.Item>
+ {showDividendOutputs && (
+ <>
+ {!dividendOutputIsValid &&
+ etokenHolders > 0 && (
+ <>
+ <Alert
+ description={
+ 'At least one airdrop is below the minimum ' +
+ fromSmallestDenomination(
+ currency.dustSats,
+ ) +
+ ' XEC dust. Please increase the total XEC airdrop.'
+ }
+ type="error"
+ showIcon
+ />
+ <br />
+ </>
+ )}
+ <Form.Item>
+ One to Many Airdrop Payment
+ Outputs
+ <TextArea
+ name="dividendRecipients"
+ placeholder="Please input parameters above."
+ value={dividendRecipients}
+ rows="10"
+ readOnly
+ />
+ </Form.Item>
+ <Form.Item>
+ <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.Item>
+ </>
+ )}
+ </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/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap b/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap
--- a/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap
+++ b/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap
@@ -119,16 +119,16 @@
</div>
</div>,
<div
- className="sc-cJSrbW grtjkC"
+ className="sc-ksYbfQ gTGVRJ"
>
<div
- className="sc-ksYbfQ hhXWxr"
+ className="sc-hmzhuo lbkwkn"
onClick={[Function]}
>
XEC
</div>
<div
- className="sc-ksYbfQ hhXWxr nonactiveBtn"
+ className="sc-hmzhuo lbkwkn nonactiveBtn"
onClick={[Function]}
>
eToken
@@ -256,16 +256,16 @@
</div>
</div>,
<div
- className="sc-cJSrbW grtjkC"
+ className="sc-ksYbfQ gTGVRJ"
>
<div
- className="sc-ksYbfQ hhXWxr"
+ className="sc-hmzhuo lbkwkn"
onClick={[Function]}
>
XEC
</div>
<div
- className="sc-ksYbfQ hhXWxr nonactiveBtn"
+ className="sc-hmzhuo lbkwkn nonactiveBtn"
onClick={[Function]}
>
eToken
@@ -375,16 +375,16 @@
</div>
</div>,
<div
- className="sc-cJSrbW grtjkC"
+ className="sc-ksYbfQ gTGVRJ"
>
<div
- className="sc-ksYbfQ hhXWxr"
+ className="sc-hmzhuo lbkwkn"
onClick={[Function]}
>
XEC
</div>
<div
- className="sc-ksYbfQ hhXWxr nonactiveBtn"
+ className="sc-hmzhuo lbkwkn nonactiveBtn"
onClick={[Function]}
>
eToken
@@ -512,16 +512,16 @@
</div>
</div>,
<div
- className="sc-cJSrbW grtjkC"
+ className="sc-ksYbfQ gTGVRJ"
>
<div
- className="sc-ksYbfQ hhXWxr"
+ className="sc-hmzhuo lbkwkn"
onClick={[Function]}
>
XEC
</div>
<div
- className="sc-ksYbfQ hhXWxr nonactiveBtn"
+ className="sc-hmzhuo lbkwkn nonactiveBtn"
onClick={[Function]}
>
eToken
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,
+ convertEtokenToEcashAddr,
parseOpReturn,
} from '@utils/cashMethods';
@@ -345,4 +346,60 @@
const result = parseOpReturn(eTokenInputHex);
expect(result).toStrictEqual(mockParsedETokenOutputArray);
});
+
+ test('convertEtokenToEcashAddr successfully converts a valid eToken address to eCash', async () => {
+ const result = convertEtokenToEcashAddr(
+ 'etoken:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs',
+ );
+ expect(result).toStrictEqual(
+ 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8',
+ );
+ });
+
+ test('convertEtokenToEcashAddr successfully converts prefixless eToken address as input', async () => {
+ const result = convertEtokenToEcashAddr(
+ 'qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs',
+ );
+ expect(result).toStrictEqual(
+ 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8',
+ );
+ });
+
+ test('convertEtokenToEcashAddr throws error with an invalid eToken address as input', async () => {
+ const result = convertEtokenToEcashAddr('etoken:qpj9gcldpffcs');
+ expect(result).toStrictEqual(
+ new Error(
+ 'cashMethods.convertToEcashAddr() error: etoken:qpj9gcldpffcs is not a valid etoken address',
+ ),
+ );
+ });
+
+ test('convertEtokenToEcashAddr throws error with an ecash address as input', async () => {
+ const result = convertEtokenToEcashAddr(
+ 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8',
+ );
+ expect(result).toStrictEqual(
+ new Error(
+ 'cashMethods.convertToEcashAddr() error: ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8 is not a valid etoken address',
+ ),
+ );
+ });
+
+ test('convertEtokenToEcashAddr throws error with null input', async () => {
+ const result = convertEtokenToEcashAddr(null);
+ expect(result).toStrictEqual(
+ new Error(
+ 'cashMethods.convertToEcashAddr() error: No etoken address provided',
+ ),
+ );
+ });
+
+ test('convertEtokenToEcashAddr throws error with empty string input', async () => {
+ const result = convertEtokenToEcashAddr('');
+ expect(result).toStrictEqual(
+ new Error(
+ 'cashMethods.convertToEcashAddr() error: No etoken address provided',
+ ),
+ );
+ });
});
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
@@ -229,6 +229,40 @@
return wallet.state;
};
+export function convertEtokenToEcashAddr(eTokenAddress) {
+ if (!eTokenAddress) {
+ return new Error(
+ `cashMethods.convertToEcashAddr() error: No etoken address provided`,
+ );
+ }
+
+ // Confirm input is a valid eToken address
+ const isValidInput = isValidEtokenAddress(eTokenAddress);
+ if (!isValidInput) {
+ return new Error(
+ `cashMethods.convertToEcashAddr() error: ${eTokenAddress} is not a valid etoken address`,
+ );
+ }
+
+ // Check for etoken: prefix
+ const isPrefixedEtokenAddress = eTokenAddress.slice(0, 7) === 'etoken:';
+
+ // If no prefix, assume it is checksummed for an etoken: prefix
+ const testedEtokenAddr = isPrefixedEtokenAddress
+ ? eTokenAddress
+ : `etoken:${eTokenAddress}`;
+
+ let ecashAddress;
+ try {
+ const { type, hash } = cashaddr.decode(testedEtokenAddr);
+ ecashAddress = cashaddr.encode('ecash', type, hash);
+ } catch (err) {
+ return err;
+ }
+
+ return ecashAddress;
+}
+
export function convertToEcashPrefix(bitcoincashPrefixedAddress) {
// Prefix-less addresses may be valid, but the cashaddr.decode function used below
// will throw an error without a prefix. Hence, must ensure prefix to use that function.
@@ -487,3 +521,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() != '' &&
+ new BigNumber(xecDividend).gt(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:31 (16 h, 35 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5573409
Default Alt Text
D10758.id31901.diff (53 KB)

Event Timeline