diff --git a/web/cashtab/extension/src/components/App.js b/web/cashtab/extension/src/components/App.js index 21d97d338..1f37dba53 100644 --- a/web/cashtab/extension/src/components/App.js +++ b/web/cashtab/extension/src/components/App.js @@ -1,638 +1,642 @@ import React, { useState, useEffect } from 'react'; import 'antd/dist/antd.less'; import PropTypes from 'prop-types'; import { Spin, Modal } from 'antd'; import { CashLoadingIcon, HomeIcon, SendIcon, ReceiveIcon, SettingsIcon, AirdropIcon, ThemedSignAndVerifyMsg, } 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 SignVerifyMsg from 'components/SignVerifyMsg/SignVerifyMsg'; import NotFound from 'components/NotFound'; import Cashtab from 'assets/cashtab_xec.png'; import './App.css'; import { WalletContext } from 'utils/context'; import { isValidStoredWallet, convertToEcashPrefix } 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'; // Extension-only import used for interacting with the extension API import extension from 'extensionizer'; 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; } .ant-form-item-explain { div { color: ${props => props.theme.forms.text}; } } .ant-input-prefix { color: ${props => props.theme.eCashBlue}; } .ant-spin-nested-loading>div>.ant-spin .ant-spin-dot { top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; margin: auto !important; } .ant-spin-nested-loading>div>.ant-spin { position: fixed !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; } `; const NavWrapper = styled.div` cursor: pointer; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 1.3rem; margin-bottom: 5px; `; const NavIcon = styled.span` position: relative; background-color: ${props => props.clicked ? 'transparent' : props.theme.buttons.primary.color}; width: 2rem; height: 2px; display: inline-block; transition: transform 300ms, top 300ms, background-color 300ms; &::before, &::after { content: ''; background-color: ${props => props.theme.buttons.primary.color}; width: 2rem; height: 2px; display: inline-block; position: absolute; left: 0; transition: transform 300ms, top 300ms, background-color 300ms; } &::before { top: ${props => (props.clicked ? '0' : '-0.8rem')}; transform: ${props => (props.clicked ? 'rotate(135deg)' : 'rotate(0)')}; } &::after { top: ${props => (props.clicked ? '0' : '0.8rem')}; transform: ${props => props.clicked ? 'rotate(-135deg)' : 'rotate(0)'}; } `; const NavMenu = styled.div` position: fixed; float: right; margin-right: 1px; bottom: 5rem; display: flex; width: 8.23rem; flex-direction: column; border: ${props => (props.open ? '0.1px solid' : '0px solid')}; border-color: ${props => props.open ? props.theme.contrast : 'transparent'}; justify-content: center; align-items: center; overflow: hidden; @media (max-width: 768px) { right: 0; margin-right: 0; } transition: ${props => props.open ? 'max-height 1000ms ease-in-out , border-color 800ms ease-in-out, border-width 800ms ease-in-out' : 'max-height 300ms cubic-bezier(0, 1, 0, 1), border-color 600ms ease-in-out, border-width 800ms ease-in-out'}; max-height: ${props => (props.open ? '100rem' : '0')}; `; const NavItem = styled.button` display: flex; justify-content: right; align-items: center; width: 100%; white-space: nowrap; height: 3rem; background-color: ${props => props.theme.walletBackground}; border: none; color: ${props => props.theme.contrast}; gap: 6px; cursor: pointer; &:hover { color: ${props => props.theme.navActive}; svg { fill: ${props => props.theme.navActive}; } } svg { fill: ${props => props.theme.contrast}; max-width: 26px; height: auto; flex: 1; } p { flex: 2; margin-top: 5px; } `; 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); const [navMenuClicked, setNavMenuClicked] = useState(false); const [showApproveAddressShareModal, setShowApproveAddressShareModal] = useState(false); const [addressRequestTabId, setAddressRequestTabId] = useState(null); const [addressRequestTabUrl, setAddressRequestTabUrl] = useState(''); const handleNavMenuClick = () => setNavMenuClicked(!navMenuClicked); // 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}`); }; // Connect to extension messaging port const port = extension.runtime.connect({ name: 'cashtabPort' }); // Extension storage get method const getObjectFromExtensionStorage = async function (key) { return new Promise((resolve, reject) => { try { extension.storage.sync.get(key, function (value) { resolve(value[key]); }); } catch (err) { reject(err); } }); }; const copyAddressToExtensionStorage = async wallet => { // Get address from active wallet let address; try { address = wallet.Path1899.cashAddress; address = convertToEcashPrefix(address); console.log(`Address fetched from extension`, address); } catch (err) { // The wallet object can be 'false' when Cashtab first loads. In this case, we want this function to do nothing. return console.log( `Wallet not loaded yet, exiting copyAddressToExtension`, ); } // Save the address to extension storage API // Check for stored value const storedAddress = await getObjectFromExtensionStorage(['address']); console.log(`storedAddress`, storedAddress); if (address === storedAddress) { // No need to store it again console.log(`Active wallet address already in extension storage`); return; } // If the address has not been set (or if the user has changed wallets since it was last set), set it await extension.storage.sync.set({ address: address }, function () { console.log( `Address ${address} saved to storage under key 'address'`, ); }); }; const handleApprovedAddressShare = () => { console.log(`handleApprovedAddressShare called`); // Let the background script know you approved this request port.postMessage({ type: 'FROM_CASHTAB', text: 'Cashtab', addressRequestApproved: true, url: addressRequestTabUrl, tabId: addressRequestTabId, }); setShowApproveAddressShareModal(false); // Close the popup after user action window.close(); }; const handleRejectedAddressShare = () => { console.log(`handleRejectedAddressShare called`); // Let the background script know you denied this request port.postMessage({ type: 'FROM_CASHTAB', text: 'Cashtab', addressRequestApproved: false, url: addressRequestTabUrl, tabId: addressRequestTabId, }); setShowApproveAddressShareModal(false); // Close the popup after user action window.close(); }; useEffect(() => { copyAddressToExtensionStorage(wallet); }, [wallet]); useEffect(() => { // Parse for query string asking for user approval of sharing extension info with a web page // Do not set txInfo in state if query strings are not present if ( !window.location || !window.location.hash || window.location.hash === '#/wallet' ) { return; } try { let windowHash = window.location.hash; let queryStringArray = windowHash.split('#/wallet?'); let queryString = queryStringArray[1]; let queryStringParams = new URLSearchParams(queryString); let request = queryStringParams.get('request'); let tabId = queryStringParams.get('tabId'); let tabUrl = queryStringParams.get('tabUrl'); console.log(`request`, request); console.log(`tabId`, tabId); console.log(`tabUrl`, tabUrl); if (request !== 'addressRequest') { return; } // Open a modal that asks for user approval setAddressRequestTabId(tabId); setAddressRequestTabUrl(tabUrl); setShowApproveAddressShareModal(true); } catch (err) { // If you can't parse this, forget about it return; } // Modal onApprove function should post a message that gets to background.js }, []); return ( {showApproveAddressShareModal && ( handleApprovedAddressShare()} onCancel={() => handleRejectedAddressShare()} >

The web page {addressRequestTabUrl} is requesting your eCash address.

)} {/*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}
); }; App.propTypes = { match: PropTypes.string, }; export default App; diff --git a/web/cashtab/src/components/App.js b/web/cashtab/src/components/App.js index c54165ead..8ce0214db 100644 --- a/web/cashtab/src/components/App.js +++ b/web/cashtab/src/components/App.js @@ -1,526 +1,530 @@ import React, { useState } from 'react'; import 'antd/dist/antd.less'; import PropTypes from 'prop-types'; import { Spin } from 'antd'; import { CashLoadingIcon, HomeIcon, SendIcon, ReceiveIcon, SettingsIcon, AirdropIcon, ThemedSignAndVerifyMsg, } 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 SignVerifyMsg from 'components/SignVerifyMsg/SignVerifyMsg'; 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'; // Easter egg imports not used in extension/src/components/App.js import TabCash from 'assets/tabcash.png'; import { checkForTokenById } from 'utils/tokenMethods.js'; // Biometric security import not used in extension/src/components/App.js import ProtectableComponentWrapper from './Authentication/ProtectableComponentWrapper'; import ServiceWorkerWrapper from './Common/ServiceWorkerWrapper'; 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; 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; } .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; } .ant-form-item-explain { div { color: ${props => props.theme.forms.text}; } } .ant-input-prefix { color: ${props => props.theme.eCashBlue}; } .ant-spin-nested-loading>div>.ant-spin .ant-spin-dot { top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; margin: auto !important; } .ant-spin-nested-loading>div>.ant-spin { position: fixed !important; } `; const CustomApp = styled.div` text-align: center; 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; } `; const NavWrapper = styled.div` cursor: pointer; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 1.3rem; margin-bottom: 5px; `; const NavIcon = styled.span` position: relative; background-color: ${props => props.clicked ? 'transparent' : props.theme.buttons.primary.color}; width: 2rem; height: 2px; display: inline-block; transition: transform 300ms, top 300ms, background-color 300ms; &::before, &::after { content: ''; background-color: ${props => props.theme.buttons.primary.color}; width: 2rem; height: 2px; display: inline-block; position: absolute; left: 0; transition: transform 300ms, top 300ms, background-color 300ms; } &::before { top: ${props => (props.clicked ? '0' : '-0.8rem')}; transform: ${props => (props.clicked ? 'rotate(135deg)' : 'rotate(0)')}; } &::after { top: ${props => (props.clicked ? '0' : '0.8rem')}; transform: ${props => props.clicked ? 'rotate(-135deg)' : 'rotate(0)'}; } `; const NavMenu = styled.div` position: fixed; float: right; margin-right: 1px; bottom: 5rem; display: flex; width: 8.23rem; flex-direction: column; border: ${props => (props.open ? '1px solid' : '0px solid')}; border-color: ${props => props.open ? props.theme.contrast : 'transparent'}; justify-content: center; align-items: center; @media (max-width: 768px) { right: 0; margin-right: 0; } overflow: hidden; transition: ${props => props.open ? 'max-height 1000ms ease-in-out , border-color 800ms ease-in-out, border-width 800ms ease-in-out' : 'max-height 300ms cubic-bezier(0, 1, 0, 1), border-color 600ms ease-in-out, border-width 800ms ease-in-out'}; max-height: ${props => (props.open ? '100rem' : '0')}; `; const NavItem = styled.button` display: flex; justify-content: right; align-items: center; width: 100%; white-space: nowrap; height: 3rem; background-color: ${props => props.theme.walletBackground}; border: none; color: ${props => props.theme.contrast}; gap: 6px; cursor: pointer; &:hover { color: ${props => props.theme.navActive}; svg { fill: ${props => props.theme.navActive}; } } svg { fill: ${props => props.theme.contrast}; max-width: 26px; height: auto; flex: 1; } p { flex: 2; margin-top: 5px; } `; 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: center; width: 100%; padding: 15px 0; `; export const CashTabLogo = styled.img` width: 120px; @media (max-width: 768px) { width: 110px; } `; // AbcLogo styled component not included in extension, replaced by open in new tab link export const AbcLogo = styled.img` width: 150px; @media (max-width: 768px) { width: 120px; } `; // Easter egg styled component not used in extension/src/components/App.js export const EasterEgg = styled.img` position: fixed; bottom: -195px; margin: 0; right: 10%; transition-property: bottom; transition-duration: 1.5s; transition-timing-function: ease-out; :hover { bottom: 0; } @media screen and (max-width: 1250px) { display: none; } `; const App = () => { const ContextValue = React.useContext(WalletContext); const { wallet, loading } = ContextValue; const [loadingUtxosAfterSend, setLoadingUtxosAfterSend] = useState(false); const [navMenuClicked, setNavMenuClicked] = useState(false); const handleNavMenuClick = () => setNavMenuClicked(!navMenuClicked); // 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) : ''; // Easter egg boolean not used in extension/src/components/App.js const hasTab = validWallet ? checkForTokenById( wallet.state.tokens, '50d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e', ) : false; return ( {/*Begin component not included in extension as desktop only*/} {hasTab && ( )} {/*End component not included in extension as desktop only*/} ( )} /> + + + {wallet ? ( ) : null} ); }; App.propTypes = { match: PropTypes.string, }; export default App; diff --git a/web/cashtab/src/components/SignVerifyMsg/SignVerifyMsg.js b/web/cashtab/src/components/SignVerifyMsg/SignVerifyMsg.js new file mode 100644 index 000000000..9a3143d2a --- /dev/null +++ b/web/cashtab/src/components/SignVerifyMsg/SignVerifyMsg.js @@ -0,0 +1,468 @@ +import React, { useState, useEffect } from 'react'; +import styled from 'styled-components'; +import PropTypes from 'prop-types'; +import { Form, Modal, Input } from 'antd'; +import { WalletContext } from 'utils/context'; +import { + TokenParamLabel, + MessageVerificationParamLabel, + WalletInfoCtn, + ZeroBalanceHeader, + SidePaddingCtn, +} from 'components/Common/Atoms'; +import WalletLabel from 'components/Common/WalletLabel.js'; +import BalanceHeader from 'components/Common/BalanceHeader'; +import BalanceHeaderFiat from 'components/Common/BalanceHeaderFiat'; +import { + errorNotification, + messageSignedNotification, + generalNotification, +} from 'components/Common/Notifications'; +import { CustomCollapseCtn } from 'components/Common/StyledCollapse'; +import { + AntdFormWrapper, + DestinationAddressSingleWithoutQRScan, +} from 'components/Common/EnhancedInputs'; +const { TextArea } = Input; +import { + toLegacyCash, + convertToEcashPrefix, + getWalletState, +} from 'utils/cashMethods'; +import CopyToClipboard from 'components/Common/CopyToClipboard'; +import { ThemedCopySolid } from 'components/Common/CustomIcons'; +import { SmartButton } from 'components/Common/PrimaryButton'; +import { PlusSquareOutlined } from '@ant-design/icons'; +import { currency, parseAddressForParams } from 'components/Common/Ticker.js'; +import { isValidXecAddress, isValidEtokenAddress } from 'utils/validation'; + +const Wrapper = styled.div` + .ant-collapse { + &:first-child { + margin-top: 30px; + } + } +`; + +const SignMessageLabel = styled.div` + text-align: left; + color: ${props => props.theme.forms.text}; +`; +const AddressCopyCtn = styled.div` + display: flex; + align-items: center; + gap: 0.5rem; + + svg { + height: 30px; + width: 30px; + &:hover { + fill: ${props => props.theme.eCashBlue}; + cursor: pointer; + } + } +`; + +const VerifyMessageLabel = styled.div` + text-align: left; + color: ${props => props.theme.forms.text}; +`; + +const SignatureValidation = styled.div` + color: ${props => props.theme.encryptionRed}; +`; + +const SignVerifyMsg = ({ jestBCH }) => { + const ContextValue = React.useContext(WalletContext); + const { BCH, wallet, fiatPrice, cashtabSettings, changeCashtabSettings } = + ContextValue; + const walletState = getWalletState(wallet); + const { balances } = walletState; + const [messageSignature, setMessageSignature] = useState(''); + const [showConfirmMsgToSign, setShowConfirmMsgToSign] = useState(false); + const [msgToSign, setMsgToSign] = useState(''); + const [sigCopySuccess, setSigCopySuccess] = useState(''); + const [signMessageIsValid, setSignMessageIsValid] = useState(null); + const [showConfirmMsgToVerify, setShowConfirmMsgToVerify] = useState(false); + const [messageVerificationAddr, setMessageVerificationAddr] = useState(''); + const [bchObj, setBchObj] = useState(false); + const [messageVerificationSig, setMessageVerificationSig] = useState(''); + const [messageVerificationMsg, setMessageVerificationMsg] = useState(''); + const [messageVerificationMsgIsValid, setMessageVerificationMsgIsValid] = + useState(false); + const [messageVerificationAddrError, setMessageVerificationAddrError] = + useState(false); + const [messageVerificationAddrIsValid, setMessageVerificationAddrIsValid] = + useState(false); + const [messageVerificationSigIsValid, setMessageVerificationSigIsValid] = + useState(false); + const [messageVerificationSigError, setMessageVerificationSigError] = + useState(false); + const signMessageByPk = async () => { + try { + const messageSignature = + await BCH.BitcoinCash.signMessageWithPrivKey( + wallet.Path1899.fundingWif, + msgToSign, + ); + setMessageSignature(messageSignature); + messageSignedNotification(messageSignature); + } catch (err) { + let message; + if (!err.error && !err.message) { + message = err.message || err.error || JSON.stringify(err); + } + errorNotification(err, message, 'Message Signing Error'); + throw err; + } + // Hide the modal + setShowConfirmMsgToSign(false); + setSigCopySuccess(''); + }; + + const handleSignMsgChange = e => { + const { value } = e.target; + // validation + if (value && value.length && value.length < 150) { + setMsgToSign(value); + setSignMessageIsValid(true); + } else { + setSignMessageIsValid(false); + } + }; + + const verifyMessageBySig = async () => { + let verification; + try { + verification = await bchObj.BitcoinCash.verifyMessage( + toLegacyCash(messageVerificationAddr), + messageVerificationSig, + messageVerificationMsg, + ); + } catch (err) { + errorNotification( + 'Error', + 'Unable to execute signature verification', + ); + } + + if (verification) { + generalNotification('Signature successfully verified', 'Verified'); + } else { + errorNotification( + 'Error', + 'Signature does not match address and message', + ); + } + + setShowConfirmMsgToVerify(false); + }; + + const handleOnSigCopy = () => { + if (messageSignature != '') { + setSigCopySuccess('Signature copied to clipboard'); + } + }; + + const handleVerifyMsgChange = e => { + const { value } = e.target; + + // validation + if (value && value.length && value.length < 150) { + setMessageVerificationMsgIsValid(true); + } else { + setMessageVerificationMsgIsValid(false); + } + + setMessageVerificationMsg(value); + }; + + const handleVerifySigChange = e => { + const { value } = e.target; + + // validation + if (value && value.length && value.length === 88) { + setMessageVerificationSigIsValid(true); + setMessageVerificationSigError(false); + } else { + setMessageVerificationSigIsValid(false); + setMessageVerificationSigError('Invalid signature'); + } + + setMessageVerificationSig(value); + }; + + const handleMessageVerificationAddrChange = e => { + const { value } = e.target; + let error = false; + let addressString = value; + // parse address for parameters + const addressInfo = parseAddressForParams(addressString); + // validate address + const isValid = isValidXecAddress(addressInfo.address); + + const { address } = addressInfo; + + // Is this valid address? + if (!isValid) { + error = `Invalid ${currency.ticker} address`; + // If valid address but token format + if (isValidEtokenAddress(address)) { + error = `eToken addresses are not supported for signature verifications`; + } + setMessageVerificationAddrIsValid(false); + } else { + setMessageVerificationAddrIsValid(true); + } + setMessageVerificationAddrError(error); + setMessageVerificationAddr(address); + }; + + useEffect(() => { + // jestBCH is only ever specified for unit tests, otherwise app will use getBCH(); + const activeBCH = jestBCH ? jestBCH : BCH; + + // set the BCH instance to state, for other functions to reference + setBchObj(activeBCH); + }, [BCH]); + + return ( + + + + {!balances.totalBalance ? ( + + You currently have 0 {currency.ticker} +
+ Deposit some funds to use this feature +
+ ) : ( + <> + + + + + )} +
{' '} + + setShowConfirmMsgToSign(false)} + > + Message: {msgToSign} +
+
+ + +
+ + Message: +