diff --git a/web/cashtab-v2/.dockerignore b/web/cashtab-v2/.dockerignore new file mode 100644 index 000000000..40b878db5 --- /dev/null +++ b/web/cashtab-v2/.dockerignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/web/cashtab-v2/.env b/web/cashtab-v2/.env new file mode 100644 index 000000000..e7bac11a0 --- /dev/null +++ b/web/cashtab-v2/.env @@ -0,0 +1,3 @@ +REACT_APP_NETWORK=mainnet +REACT_APP_BCHA_APIS=https://rest.kingbch.com/v4/,https://rest.bitcoinabc.org/v4/ +REACT_APP_BCHA_APIS_TEST=https://free-test.fullstack.cash/v3/ diff --git a/web/cashtab-v2/.eslintrc.js b/web/cashtab-v2/.eslintrc.js new file mode 100644 index 000000000..082864780 --- /dev/null +++ b/web/cashtab-v2/.eslintrc.js @@ -0,0 +1,22 @@ +module.exports = { + env: { + browser: true, + es2021: true, + node: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:jest/recommended', + ], + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 12, + sourceType: 'module', + }, + plugins: ['react', 'jest'], + rules: { 'jest/no-mocks-import': 'off' }, + settings: { react: { version: 'detect' } }, +}; diff --git a/web/cashtab-v2/.gitignore b/web/cashtab-v2/.gitignore new file mode 100644 index 000000000..b156dff39 --- /dev/null +++ b/web/cashtab-v2/.gitignore @@ -0,0 +1,29 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Chrome Extension testing +*.crx +*.zip +*.pem +dist/ diff --git a/web/cashtab-v2/.nvmrc b/web/cashtab-v2/.nvmrc new file mode 100644 index 000000000..3f10ffe7a --- /dev/null +++ b/web/cashtab-v2/.nvmrc @@ -0,0 +1 @@ +15 \ No newline at end of file diff --git a/web/cashtab-v2/Dockerfile b/web/cashtab-v2/Dockerfile new file mode 100644 index 000000000..895e317eb --- /dev/null +++ b/web/cashtab-v2/Dockerfile @@ -0,0 +1,34 @@ +# Multi-stage +# 1) Node image for building frontend assets +# 2) nginx stage to serve frontend assets + +# Stage 1 +FROM node:15-buster-slim AS builder + +# Install some dependencies before building +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y git && \ + apt-get install -y python + +WORKDIR /app + +# Copy only the package files and install necessary dependencies. +# This reduces cache busting when source files are changed. +COPY package.json . +COPY package-lock.json . +RUN npm ci + +# Copy the rest of the project files and build +COPY . . +RUN npm run build + +# Stage 2 +FROM nginx +COPY nginx.conf /etc/nginx/conf.d/default.conf +# Set working directory to nginx asset directory +# Copy static assets from builder stage +COPY --from=builder /app/build /usr/share/nginx/html/ +EXPOSE 80 +# Containers run nginx with global directives and daemon off +ENTRYPOINT ["nginx", "-g", "daemon off;"] diff --git a/web/cashtab-v2/README.md b/web/cashtab-v2/README.md new file mode 100644 index 000000000..35a64b9df --- /dev/null +++ b/web/cashtab-v2/README.md @@ -0,0 +1,137 @@ +# Cashtab + +## eCash Web Wallet + +![CashAppHome](./screenshots/ss-readme.png) + +### Features + +- Send & Receive XEC +- Import existing wallets + +## Development + +``` +npm install +npm start +``` + +Runs the app in the development mode.
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.
+You will also see any lint errors in the console. + +## Testing + +### 1. Regression Tests + +Existing functions that are impacted by your diff must be regression tested to ensure no unintended behavior. For example, if you're adding a function to facilitate One To Many XEC transactions, you must test that the existing One to One XEC transactions work as intended. + +### 2. Unit Tests + +Where applicable, add unit tests for new functions created into the corresponding \*.test.js files and they will get picked up as part of the unit test suite. + +Run the tests in watch mode (interactive): + +``` +npm test +``` + +Run the tests and generate a coverage report (non-interactive): + +``` +npm run test:coverage +``` + +You can then browse the HTML coverage report by opening the +`coverage/lcov-report/index.html` file in your web browser. + +### 3. System/Integration Tests + +Once your unit tests have passed successfully, execute the test plan outlined in your diff via manual testing of your new Cashtab feature. + +This includes: + +- testing across Chrome and Firefox browsers to pick up any browser specific issues +- testing via the Extension plugin (see Browser Extension below) to pick up on any extension specific issues + +### 4. Mobile Tests + +Ensure the latest feature functions correctly in a mobile setting and dimension. +Start by updating the build folder with your changes included. + +``` +npm run build +``` + +Then create a new site on [Netlify](https://www.netlify.com/) by choosing to "Deploy manually" and dragging in the /web/cashtab/build folder. Your diff will now be accessible via [projectname].netlify.app, which you can load up on your iOS or Android mobile devices for testing. + +### 5. Edge Tests + +If your diff is complex in nature, then consider potential edge cases which may not get picked up through the testing approaches above. + +This includes but is not limited to: + +- interactions with fresh new wallets with no transaction history +- interactions with wallets which have old transactions relevant to the diff +- interactions with applications outside of Cashtab such as ElectrumABC or outputs generated by custom nodejs scripts. + +## Production + +In the project directory, run: + +``` +npm run build +``` + +Builds the app for production to the `build` folder.
+It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
+Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +## Browser Extension + +1. `npm run extension` +2. Open Chrome or Brave +3. Navigate to `chrome://extensions/` (or `brave://extensions/`) +4. Enable Developer Mode +5. Click "Load unpacked" +6. Select the `extension/dist` folder that was created with `npm run extension` + +## Docker deployment + +``` +npm install +docker-compose build +docker-compose up +``` + +## Redundant APIs + +Cashtab accepts multiple instances of `bch-api` as its backend. Input your desired API URLs separated commas into the `REACT_APP_BCHA_APIS` variable. For example, to run Cashtab with three redundant APIs, use: + +``` +REACT_APP_BCHA_APIS=https://rest.kingbch.com/v4/,https://wallet-service-prod.bitframe.org/v4/,https://free-main.fullstack.cash/v4/ +``` + +You can also run Cashtab with a single API, e.g. + +``` +REACT_APP_BCHA_APIS=https://rest.kingbch.com/v4/ +``` + +Cashtab will start with the first API in your list. If it receives an error from that API, it will try the next one. + +Navigate to `localhost:8080` to see the app. + +## Cashtab Roadmap + +The following features are under active development: + +- Transaction history +- Simple Ledger Postage Protocol Support +- Cashtab browser extension diff --git a/web/cashtab-v2/docker-compose.yml b/web/cashtab-v2/docker-compose.yml new file mode 100644 index 000000000..000189599 --- /dev/null +++ b/web/cashtab-v2/docker-compose.yml @@ -0,0 +1,12 @@ +version: '2' + +services: + webserver: + container_name: cashtabwebserver + build: + context: . + dockerfile: Dockerfile + ports: + - 8080:80 + environment: + - NODE_ENV=production diff --git a/web/cashtab-v2/extension/README.md b/web/cashtab-v2/extension/README.md new file mode 100644 index 000000000..3f357de96 --- /dev/null +++ b/web/cashtab-v2/extension/README.md @@ -0,0 +1,9 @@ +# CashTab extension + +Some minor but important code changes are required to build CashTab as a browser extension. + +1. Add option to open extension in tab +2. Unique format of manifest.json with sha256 hash of any external scripts +3. CSS rules for extension pop-up sizing + +Source files unique to the browser extension are kept in the `extension/` directory. diff --git a/web/cashtab-v2/extension/public/manifest.json b/web/cashtab-v2/extension/public/manifest.json new file mode 100644 index 000000000..a8012d2e8 --- /dev/null +++ b/web/cashtab-v2/extension/public/manifest.json @@ -0,0 +1,30 @@ +{ + "manifest_version": 2, + + "name": "Cashtab", + "description": "A browser-integrated eCash wallet from Bitcoin ABC", + "version": "1.0.6", + "content_scripts": [ + { + "matches": ["file://*/*", "http://*/*", "https://*/*"], + "js": ["contentscript.js"], + "run_at": "document_idle", + "all_frames": true + } + ], + "background": { + "scripts": ["background.js"], + "persistent": false + }, + "browser_action": { + "default_popup": "index.html", + "default_title": "Cashtab" + }, + "icons": { + "16": "ecash16.png", + "48": "ecash48.png", + "128": "ecash128.png", + "192": "ecash192.png", + "512": "ecash512.png" + } +} diff --git a/web/cashtab-v2/extension/src/assets/popout.svg b/web/cashtab-v2/extension/src/assets/popout.svg new file mode 100644 index 000000000..cd00040d6 --- /dev/null +++ b/web/cashtab-v2/extension/src/assets/popout.svg @@ -0,0 +1,21 @@ + + + + popout + Created with Sketch. + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/cashtab-v2/extension/src/background.js b/web/cashtab-v2/extension/src/background.js new file mode 100644 index 000000000..cce83c6a3 --- /dev/null +++ b/web/cashtab-v2/extension/src/background.js @@ -0,0 +1,115 @@ +const extension = require('extensionizer'); + +const NOTIFICATION_HEIGHT = 600; +const NOTIFICATION_WIDTH = 400; + +let popupIsOpen = false; +let notificationIsOpen = false; +const openMetamaskTabsIDs = {}; +const requestAccountTabIds = {}; + +// This starts listening to the port created with `extension.runtime.connect` in contentscript.js +extension.runtime.onConnect.addListener(function (port) { + console.assert(port.name == 'cashtabPort'); + port.onMessage.addListener(function (msg) { + console.log('msg received in background.js'); + console.log(msg.text); + if (msg.text == `CashTab` && msg.txInfo) { + console.log(`Caught, opening popup`); + triggerUi(msg.txInfo); + } + }); +}); + +/** + * Opens the browser popup for user confirmation + */ +/* +Breaking this function down +1) Get all active tabs in browser +2) Determine if the extension UI is currently open +3) If extension is not open AND no other UI triggered popups are open, then open one + +Eventually will need similar model. Note that it actually goes much deeper than this in MetaMask. + +To start, just open a popup +*/ +async function triggerUi(txInfo) { + /* + const tabs = await extension.getActiveTabs(); + const currentlyActiveCashtabTab = Boolean(tabs.find(tab => openMetamaskTabsIDs[tab.id])); + if (!popupIsOpen && !currentlyActiveCashtabTab) { + await notificationManager.showPopup(); + } + */ + // Open a pop-up + let left = 0; + let top = 0; + try { + const lastFocused = await getLastFocusedWindow(); + // Position window in top right corner of lastFocused window. + top = lastFocused.top; + left = lastFocused.left + (lastFocused.width - NOTIFICATION_WIDTH); + } catch (_) { + // The following properties are more than likely 0, due to being + // opened from the background chrome process for the extension that + // has no physical dimensions + const { screenX, screenY, outerWidth } = window; + top = Math.max(screenY, 0); + left = Math.max(screenX + (outerWidth - NOTIFICATION_WIDTH), 0); + } + + console.log(`txInfo`); + console.log(txInfo); + + const queryString = Object.keys(txInfo) + .map(key => key + '=' + txInfo[key]) + .join('&'); + + // create new notification popup + const popupWindow = await openWindow({ + url: `index.html#/send?${queryString}`, + type: 'popup', + width: NOTIFICATION_WIDTH, + height: NOTIFICATION_HEIGHT, + left, + top, + }); +} + +async function openWindow(options) { + return new Promise((resolve, reject) => { + extension.windows.create(options, newWindow => { + const error = checkForError(); + if (error) { + return reject(error); + } + return resolve(newWindow); + }); + }); +} + +function checkForError() { + const { lastError } = extension.runtime; + if (!lastError) { + return undefined; + } + // if it quacks like an Error, its an Error + if (lastError.stack && lastError.message) { + return lastError; + } + // repair incomplete error object (eg chromium v77) + return new Error(lastError.message); +} + +async function getLastFocusedWindow() { + return new Promise((resolve, reject) => { + extension.windows.getLastFocused(windowObject => { + const error = checkForError(); + if (error) { + return reject(error); + } + return resolve(windowObject); + }); + }); +} diff --git a/web/cashtab-v2/extension/src/components/App.css b/web/cashtab-v2/extension/src/components/App.css new file mode 100644 index 000000000..aaa4956e4 --- /dev/null +++ b/web/cashtab-v2/extension/src/components/App.css @@ -0,0 +1,53 @@ +@import '~antd/dist/antd.less'; +@import '~@fortawesome/fontawesome-free/css/all.css'; +@import url('https://fonts.googleapis.com/css?family=Khula&display=swap&.css'); + +/* Hide scrollbars but keep functionality*/ +/* Hide scrollbar for Chrome, Safari and Opera */ +body::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar for IE, Edge and Firefox */ +body { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} +/* Hide up and down arros on input type="number" */ +/* Chrome, Safari, Edge, Opera */ +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Hide up and down arros on input type="number" */ +/* Firefox */ +input[type='number'] { + -moz-appearance: textfield; +} + +html, +body { + min-width: 400px; + min-height: 600px; + max-width: 100%; + overflow-x: hidden; +} + +/* Hide scroll bars on antd modals*/ +.ant-modal-wrap.ant-modal-centered::-webkit-scrollbar { + display: none; +} + +/* ITEMS BELOW TO BE MOVED TO STYLED COMPONENTS*/ + +/* useWallet.js, useBCH.js */ +@media (max-width: 768px) { + .ant-notification { + width: 100%; + top: 20px !important; + max-width: unset; + margin-right: 0; + } +} diff --git a/web/cashtab-v2/extension/src/components/App.js b/web/cashtab-v2/extension/src/components/App.js new file mode 100644 index 000000000..04b523792 --- /dev/null +++ b/web/cashtab-v2/extension/src/components/App.js @@ -0,0 +1,358 @@ +import React, { useState } from 'react'; +import 'antd/dist/antd.less'; +import { Spin } from 'antd'; +import { + CashLoadingIcon, + HomeIcon, + SendIcon, + ReceiveIcon, + SettingsIcon, + AirdropIcon, +} from '@components/Common/CustomIcons'; +import '../index.css'; +import styled, { ThemeProvider, createGlobalStyle } from 'styled-components'; +import { theme } from '@assets/styles/theme'; +import Home from '@components/Home/Home'; +import Receive from '@components/Receive/Receive'; +import Tokens from '@components/Tokens/Tokens'; +import Send from '@components/Send/Send'; +import SendToken from '@components/Send/SendToken'; +import Airdrop from '@components/Airdrop/Airdrop'; +import Configure from '@components/Configure/Configure'; +import NotFound from '@components/NotFound'; +import CashTab from '@assets/cashtab_xec.png'; +import './App.css'; +import { WalletContext } from '@utils/context'; +import { isValidStoredWallet } from '@utils/cashMethods'; +import { + Route, + Redirect, + Switch, + useLocation, + useHistory, +} from 'react-router-dom'; +// Extension-only import used for open in new tab link +import PopOut from '@assets/popout.svg'; + +const GlobalStyle = createGlobalStyle` + *::placeholder { + color: ${props => props.theme.forms.placeholder} !important; + } + *::selection { + background: ${props => props.theme.eCashBlue} !important; + } + .ant-modal-content, .ant-modal-header, .ant-modal-title { + background-color: ${props => props.theme.modal.background} !important; + color: ${props => props.theme.modal.color} !important; + } + .ant-modal-content svg { + fill: ${props => props.theme.modal.color}; + } + .ant-modal-footer button { + background-color: ${props => + props.theme.modal.buttonBackground} !important; + color: ${props => props.theme.modal.color} !important; + border-color: ${props => props.theme.modal.border} !important; + :hover { + background-color: ${props => props.theme.eCashBlue} !important; + } + } + .ant-modal-wrap > div > div.ant-modal-content > div > div > div.ant-modal-confirm-btns > button, .ant-modal > button, .ant-modal-confirm-btns > button, .ant-modal-footer > button, #cropControlsConfirm{ + border-radius: 3px; + border-radius: 3px; + background-color: ${props => + props.theme.modal.buttonBackground} !important; + color: ${props => props.theme.modal.color} !important; + border-color: ${props => props.theme.modal.border} !important; + :hover { + background-color: ${props => props.theme.eCashBlue} !important; + } + text-shadow: none !important; + text-shadow: none !important; + } + + .ant-modal-wrap > div > div.ant-modal-content > div > div > div.ant-modal-confirm-btns > button:hover,.ant-modal-confirm-btns > button:hover, .ant-modal-footer > button:hover, #cropControlsConfirm:hover { + color: ${props => props.theme.contrast}; + transition: all 0.3s; + background-color: ${props => props.theme.eCashBlue}; + border-color: ${props => props.theme.eCashBlue}; + } + .selectedCurrencyOption, .ant-select-dropdown { + text-align: left; + color: ${props => props.theme.contrast} !important; + background-color: ${props => + props.theme.collapses.expandedBackground} !important; + } + .cashLoadingIcon { + color: ${props => props.theme.eCashBlue} !important; + font-size: 48px !important; + } + .selectedCurrencyOption:hover { + color: ${props => props.theme.contrast} !important; + background-color: ${props => props.theme.eCashBlue} !important; + } + #addrSwitch, #cropSwitch { + .ant-switch-checked { + background-color: white !important; + } + } + #addrSwitch.ant-switch-checked, #cropSwitch.ant-switch-checked { + background-image: ${props => + props.theme.buttons.primary.backgroundImage} !important; + } + + .ant-slider-rail { + background-color: ${props => props.theme.forms.border} !important; + } + .ant-slider-track { + background-color: ${props => props.theme.eCashBlue} !important; + } + .ant-descriptions-bordered .ant-descriptions-row { + background: ${props => props.theme.contrast}; + } + .ant-modal-confirm-content, .ant-modal-confirm-title { + color: ${props => props.theme.contrast} !important; + } +`; + +const CustomApp = styled.div` + text-align: center; + font-family: 'Gilroy', sans-serif; + font-family: 'Poppins', sans-serif; + background-color: ${props => props.theme.backgroundColor}; + background-size: 100px 171px; + background-image: ${props => props.theme.backgroundImage}; + background-attachment: fixed; +`; + +const Footer = styled.div` + z-index: 2; + height: 80px; + border-top: 1px solid rgba(255, 255, 255, 0.5); + background-color: ${props => props.theme.footerBackground}; + position: fixed; + bottom: 0; + width: 500px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 50px; + @media (max-width: 768px) { + width: 100%; + padding: 0 20px; + } +`; + +export const NavButton = styled.button` + :focus, + :active { + outline: none; + } + cursor: pointer; + padding: 0; + background: none; + border: none; + font-size: 10px; + svg { + fill: ${props => props.theme.contrast}; + width: 26px; + height: auto; + } + ${({ active, ...props }) => + active && + ` + color: ${props.theme.navActive}; + svg { + fill: ${props.theme.navActive}; + } + `} +`; + +export const WalletBody = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 100%; + min-height: 100vh; +`; + +export const WalletCtn = styled.div` + position: relative; + width: 500px; + min-height: 100vh; + padding: 0 0 100px; + background: ${props => props.theme.walletBackground}; + -webkit-box-shadow: 0px 0px 24px 1px ${props => props.theme.shadow}; + -moz-box-shadow: 0px 0px 24px 1px ${props => props.theme.shadow}; + box-shadow: 0px 0px 24px 1px ${props => props.theme.shadow}; + @media (max-width: 768px) { + width: 100%; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + } +`; + +export const HeaderCtn = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 15px; +`; + +export const CashTabLogo = styled.img` + width: 120px; + @media (max-width: 768px) { + width: 110px; + } +`; + +// Extension only styled components +const OpenInTabBtn = styled.button` + background: none; + border: none; +`; + +const ExtTabImg = styled.img` + max-width: 20px; +`; + +const App = () => { + const ContextValue = React.useContext(WalletContext); + const { wallet, loading } = ContextValue; + const [loadingUtxosAfterSend, setLoadingUtxosAfterSend] = useState(false); + // If wallet is unmigrated, do not show page until it has migrated + // An invalid wallet will be validated/populated after the next API call, ETA 10s + const validWallet = isValidStoredWallet(wallet); + const location = useLocation(); + const history = useHistory(); + const selectedKey = + location && location.pathname ? location.pathname.substr(1) : ''; + // openInTab is an extension-only method + const openInTab = () => { + window.open(`index.html#/${selectedKey}`); + }; + + return ( + + + + + + + + + {/*Begin extension-only components*/} + openInTab()} + > + + + {/*End extension-only components*/} + + {/*Note that the extension does not support biometric security*/} + {/*Hence is not pulled in*/} + + + + + + + + + + + + + + ( + + )} + /> + + + + + + + + + + + {wallet ? ( + + ) : null} + + + + + ); +}; + +export default App; diff --git a/web/cashtab-v2/extension/src/contentscript.js b/web/cashtab-v2/extension/src/contentscript.js new file mode 100644 index 000000000..6caf80b4c --- /dev/null +++ b/web/cashtab-v2/extension/src/contentscript.js @@ -0,0 +1,35 @@ +const extension = require('extensionizer'); + +// Insert flag into window object to denote CashTab is available and active as a browser extension +// Could use a div or other approach for now, but emulate MetaMask this way so it is extensible to other items +// Try window object approach +var cashTabInject = document.createElement('script'); +cashTabInject.innerHTML = `window.bitcoinAbc = 'cashtab'`; +document.head.appendChild(cashTabInject); + +// Process page messages +// Chrome extensions communicate with web pages through the DOM +// Page sends a message to itself, chrome extension intercepts it +var port = extension.runtime.connect({ name: 'cashtabPort' }); +//console.log(`port: ${JSON.stringify(port)}`); +//console.log(port); + +window.addEventListener( + 'message', + function (event) { + if (typeof event.data.text !== 'undefined') { + console.log('Message received:'); + console.log(event.data.text); + } + + // We only accept messages from ourselves + if (event.source != window) return; + + if (event.data.type && event.data.type == 'FROM_PAGE') { + console.log(event); + console.log('Content script received: ' + event.data.text); + port.postMessage(event.data); + } + }, + false, +); diff --git a/web/cashtab-v2/manifest.json b/web/cashtab-v2/manifest.json new file mode 100644 index 000000000..b41a3e4ad --- /dev/null +++ b/web/cashtab-v2/manifest.json @@ -0,0 +1,41 @@ +{ + "name": "App", + "icons": [ + { + "src": "/android-icon-36x36.png", + "sizes": "36x36", + "type": "image/png", + "density": "0.75" + }, + { + "src": "/android-icon-48x48.png", + "sizes": "48x48", + "type": "image/png", + "density": "1.0" + }, + { + "src": "/android-icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "density": "1.5" + }, + { + "src": "/android-icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "density": "2.0" + }, + { + "src": "/android-icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "density": "3.0" + }, + { + "src": "/android-icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "density": "4.0" + } + ] +} diff --git a/web/cashtab-v2/nginx.conf b/web/cashtab-v2/nginx.conf new file mode 100644 index 000000000..fe1f64483 --- /dev/null +++ b/web/cashtab-v2/nginx.conf @@ -0,0 +1,41 @@ +gzip on; + +gzip_vary on; +gzip_proxied any; +gzip_comp_level 9; +gzip_buffers 16 8k; +gzip_http_version 1.1; +gzip_min_length 256; +gzip_types + application/atom+xml + application/geo+json + application/javascript + application/x-javascript + application/json + application/ld+json + application/manifest+json + application/rdf+xml + application/rss+xml + application/xhtml+xml + application/xml + font/eot + font/otf + font/ttf + image/svg+xml + text/css + text/javascript + text/plain + text/xml; + +server { + listen 80; + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} \ No newline at end of file diff --git a/web/cashtab-v2/public/browserconfig.xml b/web/cashtab-v2/public/browserconfig.xml new file mode 100644 index 000000000..12e825dda --- /dev/null +++ b/web/cashtab-v2/public/browserconfig.xml @@ -0,0 +1,2 @@ + +#ffffff \ No newline at end of file diff --git a/web/cashtab-v2/public/cashtab_bg.png b/web/cashtab-v2/public/cashtab_bg.png new file mode 100644 index 000000000..bed03771d Binary files /dev/null and b/web/cashtab-v2/public/cashtab_bg.png differ diff --git a/web/cashtab-v2/public/cashtab_twitter.png b/web/cashtab-v2/public/cashtab_twitter.png new file mode 100644 index 000000000..ee3268618 Binary files /dev/null and b/web/cashtab-v2/public/cashtab_twitter.png differ diff --git a/web/cashtab-v2/public/ecash128.png b/web/cashtab-v2/public/ecash128.png new file mode 100644 index 000000000..dc688f213 Binary files /dev/null and b/web/cashtab-v2/public/ecash128.png differ diff --git a/web/cashtab-v2/public/ecash16.png b/web/cashtab-v2/public/ecash16.png new file mode 100644 index 000000000..da2ef0ba9 Binary files /dev/null and b/web/cashtab-v2/public/ecash16.png differ diff --git a/web/cashtab-v2/public/ecash192.png b/web/cashtab-v2/public/ecash192.png new file mode 100644 index 000000000..192561d17 Binary files /dev/null and b/web/cashtab-v2/public/ecash192.png differ diff --git a/web/cashtab-v2/public/ecash48.png b/web/cashtab-v2/public/ecash48.png new file mode 100644 index 000000000..ac86afb20 Binary files /dev/null and b/web/cashtab-v2/public/ecash48.png differ diff --git a/web/cashtab-v2/public/ecash512.png b/web/cashtab-v2/public/ecash512.png new file mode 100644 index 000000000..a7d1ad32d Binary files /dev/null and b/web/cashtab-v2/public/ecash512.png differ diff --git a/web/cashtab-v2/public/favicon.ico b/web/cashtab-v2/public/favicon.ico new file mode 100644 index 000000000..a4f74e528 Binary files /dev/null and b/web/cashtab-v2/public/favicon.ico differ diff --git a/web/cashtab-v2/public/index.html b/web/cashtab-v2/public/index.html new file mode 100644 index 000000000..a434a6136 --- /dev/null +++ b/web/cashtab-v2/public/index.html @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + Cashtab + + + + +
+ + diff --git a/web/cashtab-v2/public/manifest.json b/web/cashtab-v2/public/manifest.json new file mode 100644 index 000000000..251b81c50 --- /dev/null +++ b/web/cashtab-v2/public/manifest.json @@ -0,0 +1,36 @@ +{ + "short_name": "Cashtab", + "name": "Cashtab", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "ecash48.png", + "type": "image/png", + "sizes": "48x48" + }, + { + "src": "ecash128.png", + "type": "image/png", + "sizes": "128x128" + }, + { + "src": "ecash192.png", + "type": "image/png", + "sizes": "192x192", + "purpose": "any maskable" + }, + { + "src": "ecash512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#273498", + "background_color": "#ffffff" +} diff --git a/web/cashtab-v2/public/robots.txt b/web/cashtab-v2/public/robots.txt new file mode 100644 index 000000000..01b0f9a10 --- /dev/null +++ b/web/cashtab-v2/public/robots.txt @@ -0,0 +1,2 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * diff --git a/web/cashtab-v2/screenshots/ss-readme.png b/web/cashtab-v2/screenshots/ss-readme.png new file mode 100644 index 000000000..633cf932d Binary files /dev/null and b/web/cashtab-v2/screenshots/ss-readme.png differ diff --git a/web/cashtab-v2/screenshots/ss01.png b/web/cashtab-v2/screenshots/ss01.png new file mode 100644 index 000000000..3da382db2 Binary files /dev/null and b/web/cashtab-v2/screenshots/ss01.png differ diff --git a/web/cashtab-v2/screenshots/ss02.png b/web/cashtab-v2/screenshots/ss02.png new file mode 100644 index 000000000..b1e6b91b0 Binary files /dev/null and b/web/cashtab-v2/screenshots/ss02.png differ diff --git a/web/cashtab-v2/screenshots/ss03.png b/web/cashtab-v2/screenshots/ss03.png new file mode 100644 index 000000000..254298db1 Binary files /dev/null and b/web/cashtab-v2/screenshots/ss03.png differ diff --git a/web/cashtab-v2/screenshots/ss04.png b/web/cashtab-v2/screenshots/ss04.png new file mode 100644 index 000000000..b41c0a673 Binary files /dev/null and b/web/cashtab-v2/screenshots/ss04.png differ diff --git a/web/cashtab-v2/scripts/addGenerated.sh b/web/cashtab-v2/scripts/addGenerated.sh new file mode 100755 index 000000000..5dae05384 --- /dev/null +++ b/web/cashtab-v2/scripts/addGenerated.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +export LC_ALL=C + +set -euo pipefail + +# Build the generated marker without having this script being marked as +# generated itself. +AT=@ +AT_GENERATED="${AT}generated" + +TOPLEVEL=$(git rev-parse --show-toplevel) + +# Note that this won't work if the with files containing a space in their name. +# This can be solved by using a null char delimiter (-d '\0') for read in +# combination with find's option -print0 to get a compatible output, but this is +# not supported by all find variants so we'll do without it for now. +find "${TOPLEVEL}/web/cashtab/src" -name "*.test.js.snap" | while read i; do + # It is possible to avoid the grep and have sed do it all, but: + # 1. This is a more complex sed usage + # 2. It will add the generated mark in files that have it already on another + # line. + if ! grep -q "${AT_GENERATED}" "${i}"; then + # The -i.bak option tells sed to do in-place replacement and create a backup + # file with a .bak suffix. This is mandatory for BSD/OSX compatibility. + sed -i.bak "1 s/$/ ${AT_GENERATED}/" "${i}" && rm -f "${i}.bak" + fi +done diff --git a/web/cashtab-v2/scripts/extension.sh b/web/cashtab-v2/scripts/extension.sh new file mode 100755 index 000000000..d30cede7f --- /dev/null +++ b/web/cashtab-v2/scripts/extension.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash + +export LC_ALL=C +set -euo pipefail + +# Build Cashtab as a Chrome/Brave browser extension + +# Create a working directory for stashing non-extension files +WORKDIR=$(mktemp -d) +echo Using workdir ${WORKDIR} + +# Delete workdir on script finish +function cleanup { + echo Deleting workdir ${WORKDIR} + rm -rf "${WORKDIR}" +} +trap cleanup EXIT + +# Stash web files that require extension changes in workdir +mv public/manifest.json ${WORKDIR} +mv src/components/App.js ${WORKDIR} +mv src/components/App.css ${WORKDIR} + +# Move extension src files into place for npm build +cp extension/src/assets/popout.svg src/assets/ +cp extension/public/manifest.json public/ +cp extension/src/components/App.js src/components/ +cp extension/src/components/App.css src/components/ + +# Delete the last extension build +if [ -d "extension/dist/" ]; then rm -Rf extension/dist/; fi + +# Build the extension +mkdir extension/dist/ +echo 'Building Extension...' + +# Required for extension build rules +export INLINE_RUNTIME_CHUNK=false +export GENERATE_SOURCEMAP=false + +npm run build + +# Copy extension build files to extension/ folder +cp -r build/* extension/dist + +# Browserify contentscript.js and background.js to pull in their imports +browserify extension/src/contentscript.js -o extension/dist/contentscript.js +browserify extension/src/background.js -o extension/dist/background.js + +# Delete extension build from build/ folder (reserved for web app builds) +rm -Rf build + +# Replace original web files +rm src/assets/popout.svg +rm public/manifest.json +rm src/components/App.js +rm src/components/App.css + +# Note, src/assets/popout.svg does not need to be replaced, not used by web app +mv ${WORKDIR}/manifest.json public/ +mv ${WORKDIR}/App.js src/components/ +mv ${WORKDIR}/App.css src/components/ + +echo 'Extension built and web files replaced!' \ No newline at end of file diff --git a/web/cashtab-v2/src/assets/airdrop-icon.svg b/web/cashtab-v2/src/assets/airdrop-icon.svg new file mode 100644 index 000000000..ad707930c --- /dev/null +++ b/web/cashtab-v2/src/assets/airdrop-icon.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + diff --git a/web/cashtab-v2/src/assets/alert-circle.svg b/web/cashtab-v2/src/assets/alert-circle.svg new file mode 100644 index 000000000..7c01d8f61 --- /dev/null +++ b/web/cashtab-v2/src/assets/alert-circle.svg @@ -0,0 +1 @@ +Alert Circle \ No newline at end of file diff --git a/web/cashtab-v2/src/assets/cashtab_xec.png b/web/cashtab-v2/src/assets/cashtab_xec.png new file mode 100644 index 000000000..51bd8541e Binary files /dev/null and b/web/cashtab-v2/src/assets/cashtab_xec.png differ diff --git a/web/cashtab-v2/src/assets/cog.svg b/web/cashtab-v2/src/assets/cog.svg new file mode 100644 index 000000000..10c3c4fda --- /dev/null +++ b/web/cashtab-v2/src/assets/cog.svg @@ -0,0 +1 @@ +Cog \ No newline at end of file diff --git a/web/cashtab-v2/src/assets/copy.svg b/web/cashtab-v2/src/assets/copy.svg new file mode 100644 index 000000000..37b371f6a --- /dev/null +++ b/web/cashtab-v2/src/assets/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/cashtab-v2/src/assets/edit.svg b/web/cashtab-v2/src/assets/edit.svg new file mode 100644 index 000000000..876a956c4 --- /dev/null +++ b/web/cashtab-v2/src/assets/edit.svg @@ -0,0 +1 @@ +Rename \ No newline at end of file diff --git a/web/cashtab-v2/src/assets/external-link-square-alt.svg b/web/cashtab-v2/src/assets/external-link-square-alt.svg new file mode 100644 index 000000000..e2b6e07ee --- /dev/null +++ b/web/cashtab-v2/src/assets/external-link-square-alt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/cashtab-v2/src/assets/fingerprint-solid.svg b/web/cashtab-v2/src/assets/fingerprint-solid.svg new file mode 100644 index 000000000..65a12406d --- /dev/null +++ b/web/cashtab-v2/src/assets/fingerprint-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/cashtab-v2/src/assets/flask.svg b/web/cashtab-v2/src/assets/flask.svg new file mode 100644 index 000000000..7f8593ab4 --- /dev/null +++ b/web/cashtab-v2/src/assets/flask.svg @@ -0,0 +1 @@ +Flask \ No newline at end of file diff --git a/web/cashtab-v2/src/assets/fonts/Poppins-Bold.ttf b/web/cashtab-v2/src/assets/fonts/Poppins-Bold.ttf new file mode 100755 index 000000000..b94d47f3a Binary files /dev/null and b/web/cashtab-v2/src/assets/fonts/Poppins-Bold.ttf differ diff --git a/web/cashtab-v2/src/assets/fonts/Poppins-Regular.ttf b/web/cashtab-v2/src/assets/fonts/Poppins-Regular.ttf new file mode 100755 index 000000000..be06e7fdc Binary files /dev/null and b/web/cashtab-v2/src/assets/fonts/Poppins-Regular.ttf differ diff --git a/web/cashtab-v2/src/assets/fonts/RobotoMono-Regular.ttf b/web/cashtab-v2/src/assets/fonts/RobotoMono-Regular.ttf new file mode 100755 index 000000000..7c4ce36a4 Binary files /dev/null and b/web/cashtab-v2/src/assets/fonts/RobotoMono-Regular.ttf differ diff --git a/web/cashtab-v2/src/assets/hammer-solid.svg b/web/cashtab-v2/src/assets/hammer-solid.svg new file mode 100644 index 000000000..7f2c70953 --- /dev/null +++ b/web/cashtab-v2/src/assets/hammer-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/cashtab-v2/src/assets/home.svg b/web/cashtab-v2/src/assets/home.svg new file mode 100644 index 000000000..94cf77d4c --- /dev/null +++ b/web/cashtab-v2/src/assets/home.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/web/cashtab-v2/src/assets/ios-paperplane.svg b/web/cashtab-v2/src/assets/ios-paperplane.svg new file mode 100644 index 000000000..04109dd0b --- /dev/null +++ b/web/cashtab-v2/src/assets/ios-paperplane.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + diff --git a/web/cashtab-v2/src/assets/logo_primary.png b/web/cashtab-v2/src/assets/logo_primary.png new file mode 100644 index 000000000..f739100b3 Binary files /dev/null and b/web/cashtab-v2/src/assets/logo_primary.png differ diff --git a/web/cashtab-v2/src/assets/logo_secondary.png b/web/cashtab-v2/src/assets/logo_secondary.png new file mode 100644 index 000000000..5223c3e1d Binary files /dev/null and b/web/cashtab-v2/src/assets/logo_secondary.png differ diff --git a/web/cashtab-v2/src/assets/logo_topright.png b/web/cashtab-v2/src/assets/logo_topright.png new file mode 100644 index 000000000..42b1251ff Binary files /dev/null and b/web/cashtab-v2/src/assets/logo_topright.png differ diff --git a/web/cashtab-v2/src/assets/receive.svg b/web/cashtab-v2/src/assets/receive.svg new file mode 100644 index 000000000..8b3aaeb92 --- /dev/null +++ b/web/cashtab-v2/src/assets/receive.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/web/cashtab-v2/src/assets/send.svg b/web/cashtab-v2/src/assets/send.svg new file mode 100644 index 000000000..f7b9fb467 --- /dev/null +++ b/web/cashtab-v2/src/assets/send.svg @@ -0,0 +1 @@ +Send \ No newline at end of file diff --git a/web/cashtab-v2/src/assets/styles/theme.js b/web/cashtab-v2/src/assets/styles/theme.js new file mode 100644 index 000000000..7aad7a75d --- /dev/null +++ b/web/cashtab-v2/src/assets/styles/theme.js @@ -0,0 +1,72 @@ +export const theme = { + eCashBlue: '#00ABE7', + eCashPurple: '#ff21d0', + darkBlue: '#273498', + contrast: '#fff', + backgroundImage: `url("/cashtab_bg.png")`, + backgroundColor: '#d5d5d7', + walletBackground: '#152b45', + walletInfoContainer: '#255173', + footerBackground: '#152b45', + navActive: '#00ABE7', + encryptionRed: '#DC143C', + genesisGreen: '#00e781', + receivedMessage: 'rgba(0,171,231,0.2)', + sentMessage: 'rgba(255, 255, 255, 0.1)', + lightWhite: 'rgba(255,255,255,0.4)', + dropdownText: '#000', + shadow: 'rgba(0, 0, 0, 0.4)', + switchButtonActiveText: '#fff', + advancedCollapse: { + background: '#255173', + color: '#fff', + icon: '#fff', + expandedBackground: 'rgba(0,0,0,0.2)', + }, + forms: { + error: '#FF21D0', + border: '#17171f', + text: '#fff', + addonBackground: '#255173', + addonForeground: '#fff', + selectionBackground: '#255173', + placeholder: 'rgba(255,255,255,0.3)', + highlightBox: '#00ABE7', + }, + icons: { outlined: '#fff' }, + settings: { + delete: '#CD0BC3', + background: 'rgba(0,0,0,0.4)', + }, + qr: { + background: '#fff', + }, + buttons: { + primary: { + backgroundImage: + 'linear-gradient(270deg, #0074C2 0%, #273498 100%)', + color: '#fff', + hoverShadow: '0px 3px 10px -5px rgba(0, 0, 0, 0.75)', + disabledOverlay: 'rgba(255, 255, 255, 0.5)', + }, + secondary: { + background: '#4b67e1', + color: '#fff', + hoverShadow: '0px 3px 10px -5px rgba(0, 0, 0, 0.75)', + disabledOverlay: 'rgba(255, 255, 255, 0.5)', + }, + styledLink: '#ffffff', + }, + collapses: { + background: '#255173', + expandedBackground: '#26415a', + border: '#17171f', + color: '#fff', + }, + modal: { + background: '#255173', + border: '#17171f', + color: '#fff', + buttonBackground: '#26415a', + }, +}; diff --git a/web/cashtab-v2/src/assets/tabcash.png b/web/cashtab-v2/src/assets/tabcash.png new file mode 100644 index 000000000..b00b35798 Binary files /dev/null and b/web/cashtab-v2/src/assets/tabcash.png differ diff --git a/web/cashtab-v2/src/assets/trashcan.svg b/web/cashtab-v2/src/assets/trashcan.svg new file mode 100644 index 000000000..8d244f7c2 --- /dev/null +++ b/web/cashtab-v2/src/assets/trashcan.svg @@ -0,0 +1 @@ +Delete \ No newline at end of file diff --git a/web/cashtab-v2/src/components/Airdrop/Airdrop.js b/web/cashtab-v2/src/components/Airdrop/Airdrop.js new file mode 100644 index 000000000..aa3d0ffc7 --- /dev/null +++ b/web/cashtab-v2/src/components/Airdrop/Airdrop.js @@ -0,0 +1,468 @@ +import React, { useState, useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import BigNumber from 'bignumber.js'; +import styled from 'styled-components'; +import { WalletContext } from '@utils/context'; +import { AntdFormWrapper } 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, + convertEtokenToEcashAddr, + fromSmallestDenomination, +} from '@utils/cashMethods'; +import { + isValidTokenId, + isValidXecAirdrop, + isValidAirdropOutputsArray, +} from '@utils/validation'; +import { CustomSpinner } from '@components/Common/CustomIcons'; +import * as etokenList from 'etoken-list'; +import { + ZeroBalanceHeader, + SidePaddingCtn, + WalletInfoCtn, +} from '@components/Common/Atoms'; +import WalletLabel from '@components/Common/WalletLabel.js'; +import { Link } from 'react-router-dom'; + +const AirdropActions = styled.div` + text-align: center; + width: 100%; + padding: 10px; + border-radius: 5px; + a { + color: ${props => props.theme.contrast}; + margin: 0; + font-size: 11px; + border: 1px solid ${props => props.theme.contrast}; + border-radius: 5px; + padding: 2px 10px; + opacity: 0.6; + } + a:hover { + opacity: 1; + border-color: ${props => props.theme.eCashBlue}; + color: ${props => props.theme.contrast}; + background: ${props => props.theme.eCashBlue}; + } + ${({ received, ...props }) => + received && + ` + text-align: left; + background: ${props.theme.receivedMessage}; + `} +`; + +// Note jestBCH is only used for unit tests; BCHJS must be mocked for jest +const Airdrop = ({ jestBCH, passLoadingStatus }) => { + const ContextValue = React.useContext(WalletContext); + const { wallet, fiatPrice, cashtabSettings } = ContextValue; + const location = useLocation(); + const walletState = getWalletState(wallet); + const { balances } = walletState; + + const [bchObj, setBchObj] = useState(false); + const [isAirdropCalcModalVisible, setIsAirdropCalcModalVisible] = + useState(false); + const [airdropCalcModalProgress, setAirdropCalcModalProgress] = useState(0); // the dynamic % progress bar + + useEffect(() => { + // jestBCH is only ever specified for unit tests, otherwise app will use getBCH(); + const BCH = jestBCH ? jestBCH : getBCH(); + + // set the BCH instance to state, for other functions to reference + setBchObj(BCH); + + if (location && location.state && location.state.airdropEtokenId) { + setFormData({ + ...formData, + tokenId: location.state.airdropEtokenId, + }); + handleTokenIdInput({ + target: { + value: location.state.airdropEtokenId, + }, + }); + } + }, []); + + const [formData, setFormData] = useState({ + tokenId: '', + totalAirdrop: '', + }); + + const [tokenIdIsValid, setTokenIdIsValid] = useState(null); + const [totalAirdropIsValid, setTotalAirdropIsValid] = useState(null); + const [airdropRecipients, setAirdropRecipients] = useState(''); + const [airdropOutputIsValid, setAirdropOutputIsValid] = useState(true); + const [etokenHolders, setEtokenHolders] = useState(new BigNumber(0)); + const [showAirdropOutputs, setShowAirdropOutputs] = useState(false); + + const { getBCH } = useBCH(); + + const handleTokenIdInput = e => { + const { name, value } = e.target; + setTokenIdIsValid(isValidTokenId(value)); + setFormData(p => ({ + ...p, + [name]: value, + })); + }; + + const handleTotalAirdropInput = e => { + const { name, value } = e.target; + setTotalAirdropIsValid(isValidXecAirdrop(value)); + setFormData(p => ({ + ...p, + [name]: value, + })); + }; + + const calculateXecAirdrop = async () => { + // display airdrop calculation message modal + setIsAirdropCalcModalVisible(true); + setShowAirdropOutputs(false); // hide any previous airdrop outputs + passLoadingStatus(true); + setAirdropCalcModalProgress(25); // updated progress bar to 25% + + let latestBlock; + try { + latestBlock = await bchObj.Blockchain.getBlockCount(); + } catch (err) { + errorNotification( + err, + 'Error retrieving latest block height', + 'bchObj.Blockchain.getBlockCount() error', + ); + setIsAirdropCalcModalVisible(false); + passLoadingStatus(false); + return; + } + + setAirdropCalcModalProgress(50); + + etokenList.Config.SetUrl(currency.tokenDbUrl); + + let airdropList; + try { + airdropList = await etokenList.List.GetAddressListFor( + formData.tokenId, + latestBlock, + true, + ); + } catch (err) { + errorNotification( + err, + 'Error retrieving airdrop recipients', + 'etokenList.List.GetAddressListFor() error', + ); + setIsAirdropCalcModalVisible(false); + passLoadingStatus(false); + return; + } + + if (!airdropList) { + errorNotification( + null, + 'No recipients found for tokenId ' + formData.tokenId, + 'Airdrop Calculation Error', + ); + setIsAirdropCalcModalVisible(false); + passLoadingStatus(false); + return; + } + + setAirdropCalcModalProgress(75); + + let totalTokenAmongstRecipients = new BigNumber(0); + let totalHolders = new BigNumber(airdropList.size); // amount of addresses that hold this eToken + setEtokenHolders(totalHolders); + + // keep a cumulative total of each eToken holding in each address in airdropList + airdropList.forEach( + index => + (totalTokenAmongstRecipients = totalTokenAmongstRecipients.plus( + new BigNumber(index), + )), + ); + + let circToAirdropRatio = new BigNumber(formData.totalAirdrop).div( + totalTokenAmongstRecipients, + ); + + let resultString = ''; + + airdropList.forEach( + (element, index) => + (resultString += + convertEtokenToEcashAddr(index) + + ',' + + new BigNumber(element) + .multipliedBy(circToAirdropRatio) + .decimalPlaces(currency.cashDecimals) + + '\n'), + ); + + resultString = resultString.substring(0, resultString.length - 1); // remove the final newline + setAirdropRecipients(resultString); + + setAirdropCalcModalProgress(100); + + if (!resultString) { + errorNotification( + null, + 'No holders found for eToken ID: ' + formData.tokenId, + 'Airdrop Calculation Error', + ); + return; + } + + // validate the airdrop values for each recipient + // Note: addresses are not validated as they are retrieved directly from onchain + setAirdropOutputIsValid(isValidAirdropOutputsArray(resultString)); + setShowAirdropOutputs(true); // display the airdrop outputs TextArea + setIsAirdropCalcModalVisible(false); + passLoadingStatus(false); + }; + + const handleAirdropCalcModalCancel = () => { + setIsAirdropCalcModalVisible(false); + passLoadingStatus(false); + }; + + let airdropCalcInputIsValid = tokenIdIsValid && totalAirdropIsValid; + + return ( + <> + + + {!balances.totalBalance ? ( + + You currently have 0 {currency.ticker} +
+ Deposit some funds to use this feature +
+ ) : ( + <> + + {fiatPrice !== null && ( + + )} + + )} +
+ + + + +
+ + + + + + +
+ +
+ + + handleTokenIdInput(e) + } + /> + + + + handleTotalAirdropInput(e) + } + /> + + + + calculateXecAirdrop() + } + disabled={ + !airdropCalcInputIsValid || + !tokenIdIsValid + } + > + Calculate Airdrop + + + {showAirdropOutputs && ( + <> + {!airdropOutputIsValid && + etokenHolders > 0 && ( + <> + +
+ + )} + + One to Many Airdrop Payment + Outputs +