diff --git a/web/cashtab/.gitignore b/web/cashtab/.gitignore
index 8bbeb8790..99dcb0296 100644
--- a/web/cashtab/.gitignore
+++ b/web/cashtab/.gitignore
@@ -1,30 +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
-/dist
-/extension
# 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
\ No newline at end of file
+*.pem
+dist/
\ No newline at end of file
diff --git a/web/cashtab/README.md b/web/cashtab/README.md
index 6dd553f93..fe6a02533 100644
--- a/web/cashtab/README.md
+++ b/web/cashtab/README.md
@@ -1,77 +1,86 @@
# CashTab
## Bitcoin Cash Web Wallet
### Features
- Send & Receive BCH
- Import existing wallets
## Development
CashTab relies on some modules that retain legacy dependencies. NPM version 7 or later no longer supports automatic resolution of these peer dependencies. To successfully install modules such as `qrcode.react`, with NPM > 7, run `npm install` with the flag `--legacy-peer-deps`
```
npm install --legacy-peer-deps
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
### 'npm test'
### 'npm run test:coverage'
## 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 --legacy-peer-deps
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/v3/,https://wallet-service-prod.bitframe.org/v3/,https://free-main.fullstack.cash/v3/
```
You can also run CashTab with a single API, e.g.
```
REACT_APP_BCHA_APIS=https://rest.kingbch.com/v3/
```
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/extension/README.md b/web/cashtab/extension/README.md
new file mode 100644
index 000000000..3f357de96
--- /dev/null
+++ b/web/cashtab/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/extension/public/manifest.json b/web/cashtab/extension/public/manifest.json
new file mode 100644
index 000000000..6de20bd5d
--- /dev/null
+++ b/web/cashtab/extension/public/manifest.json
@@ -0,0 +1,20 @@
+{
+ "manifest_version": 2,
+
+ "name": "CashTab",
+ "description": "A browser-integrated BCHA wallet from Bitcoin ABC",
+ "version": "0.0.1",
+
+ "browser_action": {
+ "default_popup": "index.html",
+ "default_title": "CashTab"
+ },
+ "icons": {
+ "16": "bch16.png",
+ "48": "bch48.png",
+ "128": "bch128.png",
+ "192": "bch192.png",
+ "512": "bch512.png"
+ },
+ "content_security_policy": "script-src 'self' https://unpkg.com/minimal-slp-wallet-web-joey 'sha256-03cee7e881f6cdd32ce620d5787d7e6a0eaef8acc55557bded61dd6ad82ff0e6'; object-src 'self'"
+}
diff --git a/web/cashtab/extension/src/assets/popout.svg b/web/cashtab/extension/src/assets/popout.svg
new file mode 100644
index 000000000..dd9fa471e
--- /dev/null
+++ b/web/cashtab/extension/src/assets/popout.svg
@@ -0,0 +1,21 @@
+
+
\ No newline at end of file
diff --git a/web/cashtab/extension/src/components/App.css b/web/cashtab/extension/src/components/App.css
new file mode 100644
index 000000000..c41be5ad4
--- /dev/null
+++ b/web/cashtab/extension/src/components/App.css
@@ -0,0 +1,411 @@
+@import '~antd/dist/antd.less';
+@import '~@fortawesome/fontawesome-free/css/all.css';
+@import url('https://fonts.googleapis.com/css?family=Khula&display=swap&.css');
+
+@font-face {
+ font-family: 'Roboto Mono';
+ src: local('Roboto Mono'),
+ url(../assets/fonts/RobotoMono-Regular.ttf) format('truetype');
+ font-weight: normal;
+}
+
+aside::-webkit-scrollbar {
+ width: 0.3em;
+}
+aside::-webkit-scrollbar-track {
+ -webkit-box-shadow: inset 0 0 6px #13171f;
+}
+aside::-webkit-scrollbar-thumb {
+ background-color: darkgrey;
+ outline: 1px solid slategrey;
+}
+
+/* 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: 320px;
+ min-height: 600px;
+ max-width: 100%;
+ overflow-x: hidden;
+}
+
+.ant-modal-wrap.ant-modal-centered::-webkit-scrollbar {
+ display: none;
+}
+
+.App {
+ text-align: center;
+ font-family: 'Gilroy', sans-serif;
+ background-color: #fbfbfd;
+}
+.App-logo {
+ width: 100%;
+ display: block;
+}
+
+.logo img {
+ width: 100%;
+ min-width: 193px;
+ display: block;
+ padding-left: 24px;
+ padding-right: 34px;
+ padding-top: 24px;
+ max-width: 200px;
+}
+.ant-list-item-meta .ant-list-item-meta-content {
+ display: flex;
+}
+
+#react-qrcode-logo {
+ border-radius: 8px;
+}
+.App-header {
+ background-color: #282c34;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ font-size: calc(10px + 2vmin);
+ color: white;
+}
+
+.App-link {
+ color: #f59332;
+}
+.ant-menu-item-group-title {
+ padding-left: 30px;
+ font-size: 20px !important;
+ font-weight: 500 !important;
+}
+
+.ant-menu-item > span {
+ font-size: 14px !important;
+ font-weight: 500 !important;
+}
+
+.ant-card-actions > li > span:hover,
+.ant-btn:hover,
+.ant-btn:focus {
+ color: #f59332;
+ transition: color 0.3s;
+ background-color: white;
+}
+
+.ant-card-actions > li {
+ color: #3e3f42;
+}
+.anticon {
+ color: #3e3f42;
+}
+.ant-list-item-meta-description,
+.ant-list-item-meta-title {
+ color: #3e3f42;
+}
+
+.ant-list-item-meta-description > :first-child {
+ right: 20px !important;
+ position: absolute;
+}
+
+.ant-modal-body .ant-list-item-meta {
+ height: 85px;
+ width: 85px;
+ padding-left: 10px;
+ padding-top: 10px;
+ padding-bottom: 20px;
+ overflow: visible !important;
+}
+
+/* .ant-radio-group-solid .ant-radio-button-wrapper {
+ margin-top: 0px;
+}
+
+.ant-radio-group-solid .ant-radio-button-wrapper-checked {
+ border: none !important;
+ box-shadow: none !important;
+} */
+.identicon {
+ border-radius: 50%;
+ width: 200px;
+ height: 200px;
+ margin-top: -75px;
+ margin-left: -75px;
+ margin-bottom: 20px;
+ box-shadow: 1px 1px 2px 1px #444;
+}
+.ant-list-item-meta {
+ width: 40px;
+ height: 40px;
+}
+
+/* .ant-radio-group-solid .ant-radio-button-wrapper-checked {
+ background: #ff8d00 !important;
+}
+
+.ant-radio-group.ant-radio-group-solid.ant-radio-group-small {
+ font-size: 14px !important;
+ font-weight: 600 !important;
+ vertical-align: middle;
+ border-radius: 100px;
+ overflow: auto;
+ background: rgba(255, 255, 255, 0.5) !important;
+ margin-top: 14px;
+ margin-bottom: 10px;
+ cursor: pointer;
+} */
+
+input.ant-input,
+.ant-select-selection {
+ background-color: #fff !important;
+ box-shadow: none !important;
+ border: 1px solid #eaedf3 !important;
+ border-radius: 4px;
+ font-weight: bold;
+ color: rgb(62, 63, 66);
+ opacity: 1;
+ padding: 11px 5px;
+ height: 50px;
+}
+
+.ant-select-selection:hover {
+ border: 1px solid #eaedf3;
+}
+
+.ant-select-selection-selected-value {
+ color: rgb(62, 63, 66);
+}
+
+.ant-select-dropdown-menu-item {
+ color: #444;
+ background-color: #fff;
+}
+
+.ant-select-dropdown-menu-item-active,
+.ant-select-dropdown-menu-item:hover {
+ color: #fff;
+ background-color: #ff8d00 !important;
+}
+
+.ant-checkbox-inner {
+ border: 1px solid #eaedf3 !important;
+ background: white;
+}
+
+.ant-checkbox-inner::after {
+ border-color: white !important;
+}
+
+.ant-card-bordered {
+ border: 1px solid rgb(234, 237, 243);
+ border-radius: 8px;
+}
+
+.ant-card-actions {
+ border-top: 1px solid rgb(234, 237, 243);
+ border-bottom: 1px solid rgb(234, 237, 243);
+ border-bottom-left-radius: 8px;
+ border-bottom-right-radius: 8px;
+ box-shadow: 0px 5px 8px rgba(0, 0, 0, 0.35);
+}
+
+.ant-input-group-addon {
+ background-color: #f4f4f4 !important;
+ border: 1px solid rgb(234, 237, 243);
+ color: #3e3f42 !important;
+
+ * {
+ color: #3e3f42 !important;
+ }
+}
+
+.ant-menu-item.ant-menu-item-selected > * {
+ color: #fff !important;
+}
+
+.ant-menu-item.ant-menu-item-selected {
+ border: 0;
+ overflow: hidden;
+ text-align: left;
+ padding-left: 28px;
+ background-color: rgba(255, 255, 255, 0.2) !important;
+}
+
+.ant-btn {
+ border-radius: 8px;
+ background-color: #fff;
+ color: rgb(62, 63, 66);
+ font-weight: bold;
+}
+
+.ant-card-actions > li:not(:last-child) {
+ border-right: 0;
+}
+.ant-list-item-meta-avatar > img {
+ margin-left: -12px;
+ transform: translate(0, -6px);
+}
+
+.ant-list-item-meta-avatar > svg {
+ margin-right: -70px;
+}
+
+/* Removing these for ABC SLP warning
+.ant-alert-warning {
+ background-color: #20242d;
+ border: 1px solid #17171f;
+ border-radius: 0;
+}
+
+.ant-alert-message {
+ color: #fff;
+}
+*/
+
+.ant-layout-sider-dark {
+ background: linear-gradient(0deg, #040c3c, #212c6e);
+}
+
+.ant-menu-dark {
+ background: none;
+}
+
+.ant-layout-sider-zero-width-trigger.ant-layout-sider-zero-width-trigger-left
+ .anticon.anticon-bars {
+ color: #fff;
+ transform: scale(1.3);
+}
+
+.ant-layout-sider-zero-width-trigger.ant-layout-sider-zero-width-trigger-left {
+ background: #3e3f42;
+ border-radius: 0 8px 8px 0;
+}
+
+.ant-btn-group .ant-btn-primary:first-child:not(:last-child) {
+ border-right-color: transparent !important;
+}
+
+.ant-btn-group .ant-btn-primary:last-child:not(:first-child),
+.ant-btn-group .ant-btn-primary + .ant-btn-primary {
+ border-left-color: #20242d !important;
+}
+
+.audit {
+ a,
+ a:active {
+ color: #46464a;
+ }
+
+ a:hover {
+ color: #111117;
+ }
+}
+
+.dividends {
+ a,
+ a:active {
+ color: #111117;
+ }
+
+ a:hover {
+ color: #46464a;
+ }
+}
+
+.ant-popover-inner-content {
+ color: white;
+}
+
+.ant-modal-body .ant-card {
+ max-width: 100%;
+}
+
+.ant-upload.ant-upload-drag {
+ border: 1px solid #eaedf3;
+ border-radius: 8px;
+ background: #d3d3d3;
+}
+
+.ant-upload-list-item:hover .ant-upload-list-item-info {
+ background-color: #ffffff;
+}
+
+/* .ant-radio-button-wrapper {
+ border: none;
+}
+
+.ant-radio-button-wrapper-checked {
+ border-radius: none !important;
+} */
+
+/* .ant-radio-button-wrapper:first-child, .ant-radio-button-wrapper:last-child {
+ border-radius: 0 0 0 0;
+} */
+
+.ant-radio-group {
+ width: 100%;
+ margin-top: 10px;
+}
+
+.ant-radio-button-wrapper {
+ background: rgba(255, 255, 255, 0.2);
+ width: 104px;
+ border: none;
+ text-align: center;
+ color: #fff;
+}
+
+.ant-radio-button-wrapper:hover {
+ color: #fff;
+ background: rgba(255, 255, 255, 0.3);
+}
+
+.ant-radio-group-small .ant-radio-button-wrapper {
+ height: 35px;
+ line-height: 35px;
+}
+
+.ant-radio-button-wrapper-checked {
+ background: #ff8d00 !important;
+ border: none !important;
+}
+
+.ant-radio-button-wrapper:first-child {
+ border-radius: 100px 0 0 100px;
+}
+
+.ant-radio-button-wrapper:last-child {
+ border-radius: 0 100px 100px 0;
+}
+
+::selection {
+ background-color: #ff8d00;
+}
+
+@media (max-width: 768px) {
+ .ant-notification {
+ width: 100%;
+ top: 20px !important;
+ max-width: unset;
+ margin-right: 0;
+ }
+}
+
+@media (max-width: 350px) {
+ .ant-select-selection-selected-value {
+ font-size: 10px;
+ }
+}
diff --git a/web/cashtab/extension/src/components/App.js b/web/cashtab/extension/src/components/App.js
new file mode 100644
index 000000000..b86d2e024
--- /dev/null
+++ b/web/cashtab/extension/src/components/App.js
@@ -0,0 +1,278 @@
+import React from 'react';
+import 'antd/dist/antd.less';
+import '../index.css';
+import styled from 'styled-components';
+import { Layout, Tabs, Icon } from 'antd';
+import Wallet from './Wallet/Wallet';
+import Send from './Send/Send';
+import SendToken from './Send/SendToken';
+import Configure from './Configure/Configure';
+import NotFound from './NotFound';
+import CashTab from '../assets/cashtab.png';
+import PopOut from '../assets/popout.svg';
+import './App.css';
+import { WalletContext } from '../utils/context';
+import {
+ Route,
+ Redirect,
+ Switch,
+ useLocation,
+ useHistory,
+} from 'react-router-dom';
+
+const { Footer } = Layout;
+const { TabPane } = Tabs;
+
+const OpenInTabBtn = styled.button`
+ background: none;
+ border: none;
+`;
+const ExtTabImg = styled.img`
+ max-width: 20px;
+`;
+
+const StyledTabsMenu = styled.div`
+ .ant-layout-footer {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ padding: 0;
+ background-color: #fff;
+ left: 0;
+ border-radius: 20px;
+ border-top: 1px solid #e2e2e2;
+ @media (max-width: 768px) {
+ position: fixed;
+ }
+ }
+ .ant-tabs-nav .ant-tabs-tab {
+ padding: 30px 0 20px 0;
+ }
+ .ant-tabs-bar.ant-tabs-bottom-bar {
+ margin-top: 0;
+ border-top: none;
+ }
+ .ant-tabs-tab {
+ span {
+ font-size: 12px;
+ display: grid;
+ font-weight: bold;
+ }
+ .anticon {
+ color: rgb(148, 148, 148);
+ font-size: 24px;
+ margin-left: 8px;
+ margin-bottom: 3px;
+ }
+ }
+ .ant-tabs-tab:hover {
+ color: #ff8d00 !important;
+ .anticon {
+ color: #ff8d00;
+ }
+ }
+ .ant-tabs-tab-active.ant-tabs-tab {
+ color: #ff8d00;
+ .anticon {
+ color: #ff8d00;
+ }
+ }
+ .ant-tabs-tab-active.ant-tabs-tab {
+ color: #ff8d00;
+ .anticon {
+ color: #ff8d00;
+ }
+ }
+ .ant-tabs-tab-active:active {
+ color: #ff8d00 !important;
+ }
+ .ant-tabs-ink-bar {
+ display: none !important;
+ }
+ .ant-tabs-nav {
+ margin: -3.5px 0 0 0;
+ }
+`;
+
+export const WalletBody = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ min-height: 100vh;
+ background: linear-gradient(270deg, #040c3c, #212c6e);
+`;
+
+export const WalletCtn = styled.div`
+ position: relative;
+ width: 500px;
+ background-color: #fff;
+ min-height: 100vh;
+ padding-top: 30px;
+ padding: 10px 30px 100px 30px;
+ background: #fff;
+ -webkit-box-shadow: 0px 0px 24px 1px rgba(0, 0, 0, 1);
+ -moz-box-shadow: 0px 0px 24px 1px rgba(0, 0, 0, 1);
+ box-shadow: 0px 0px 24px 1px rgba(0, 0, 0, 1);
+ @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: center;
+ width: 100%;
+ padding: 20px 0 30px;
+ margin-bottom: 20px;
+ justify-content: space-between;
+ border-bottom: 1px solid #e2e2e2;
+
+ a {
+ color: #848484;
+
+ :hover {
+ color: #ff8d00;
+ }
+ }
+
+ @media (max-width: 768px) {
+ a {
+ font-size: 12px;
+ }
+ padding: 10px 0 20px;
+ }
+`;
+
+export const CashTabLogo = styled.img`
+ width: 120px;
+ @media (max-width: 768px) {
+ width: 110px;
+ }
+`;
+export const AbcLogo = styled.img`
+ width: 150px;
+ @media (max-width: 768px) {
+ width: 120px;
+ }
+`;
+
+const App = () => {
+ const ContextValue = React.useContext(WalletContext);
+ const { wallet } = ContextValue;
+ const location = useLocation();
+ const history = useHistory();
+ const selectedKey =
+ location && location.pathname ? location.pathname.substr(1) : '';
+ const openInTab = () => {
+ window.open(`index.html#/${selectedKey}`);
+ };
+
+ return (
+