diff --git a/package-lock.json b/package-lock.json index 0d0e856..dcbabd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "axios": "0.27.2", "bch-message-lib": "2.2.1", "bootstrap": "5.2.0", + "qrcode.react": "4.2.0", "query-string": "7.1.1", "react": "19.0.0", "react-bootstrap": "2.10.7", @@ -14888,6 +14889,15 @@ "teleport": ">=0.2.0" } }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/qs": { "version": "6.10.3", "license": "BSD-3-Clause", diff --git a/package.json b/package.json index 66aec9d..7e86069 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "axios": "0.27.2", "bch-message-lib": "2.2.1", "bootstrap": "5.2.0", + "qrcode.react": "4.2.0", "query-string": "7.1.1", "react": "19.0.0", "react-bootstrap": "2.10.7", diff --git a/src/App.css b/src/App.css index 01396d1..7af9e30 100644 --- a/src/App.css +++ b/src/App.css @@ -31,3 +31,20 @@ padding: 0.5rem; } + +#address-switch { + cursor: pointer; +} + + +/* Remove input number arrows Chrome, Safari, Edge, Opera */ +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Remove input number arrows Firefox */ +input[type=number] { + --moz-appearance: textfield; +} \ No newline at end of file diff --git a/src/components/app-body/bch-send/balance-card.js b/src/components/app-body/bch-send/balance-card.js new file mode 100644 index 0000000..3fa541c --- /dev/null +++ b/src/components/app-body/bch-send/balance-card.js @@ -0,0 +1,53 @@ +/* + This card displays the users balance in BCH. +*/ + +// Global npm libraries +import React from 'react' +import { Container, Row, Col, Card } from 'react-bootstrap' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCoins } from '@fortawesome/free-solid-svg-icons' + +const BalanceCard = (props) => { + const { appData } = props + + const bchjs = appData.wallet.bchjs + const sats = appData.bchWalletState.bchBalance + const bchBalance = bchjs.BitcoinCash.toBitcoinCash(sats) + const usdBalance = bchjs.Util.floor2(bchBalance * appData.bchWalletState.bchUsdPrice) + + return ( + <> + + + +

Balance

+
+
+ + + + + USD: ${usdBalance} + + + + + + BCH: {bchBalance} + + + + + + Satoshis: {sats} + + + +
+
+ + ) +} + +export default BalanceCard diff --git a/src/components/app-body/bch-send/index.js b/src/components/app-body/bch-send/index.js new file mode 100644 index 0000000..b283759 --- /dev/null +++ b/src/components/app-body/bch-send/index.js @@ -0,0 +1,64 @@ +/* + This View allows sending and receiving of BCH +*/ + +// Global npm libraries +import React from 'react' +import { Container, Row, Col } from 'react-bootstrap' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCircleQuestion } from '@fortawesome/free-solid-svg-icons' + +// Local libraries +import RefreshBchBalanceButton from './refresh-bch-balance-button' +import SendCard from './send-card' +import BalanceCard from './balance-card' +import ReceiveCard from './receive-card' + +// Working array for storing modal output. +// this.modalBody = [] + +function BchSend ({ appData }) { + return ( + <> + + + + + + + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ + ) +} + +export default BchSend diff --git a/src/components/app-body/bch-send/receive-card.js b/src/components/app-body/bch-send/receive-card.js new file mode 100644 index 0000000..60387f9 --- /dev/null +++ b/src/components/app-body/bch-send/receive-card.js @@ -0,0 +1,93 @@ +/* + This card displays the users BCH and SLP address and QR code +*/ + +// Global npm libraries +import React, { useState } from 'react' +import { Container, Row, Col, Card, Form } from 'react-bootstrap' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faWallet } from '@fortawesome/free-solid-svg-icons' +import { QRCodeSVG } from 'qrcode.react' + +const ReceiveCard = ({ appData }) => { + const [addrSwitch, setAddrSwitch] = useState(false) + const [displayCopyMsg, setDisplayCopyMsg] = useState(false) + + // Determine which address to display + const addrToDisplay = !addrSwitch + ? appData.bchWalletState.cashAddress + : appData.bchWalletState.slpAddress + + // Copy the selected address to the clipboard when the QR image is clicked + const handleCopyAddress = async (value) => { + appData.appUtil.copyToClipboard(value) + + // Display the copied message + setDisplayCopyMsg(true) + + // Clear the copied message after some time + setTimeout(() => { + setDisplayCopyMsg(false) + }, 1000) + } + + // Event handler for address switch toggle + const handleAddrSwitchToggle = (event) => { + setAddrSwitch(event.target.checked) + } + + return ( + <> + + + +

Receive

+
+
+ + + + + {displayCopyMsg ? 'Copied' : null} + + + + + + { handleCopyAddress(addrToDisplay) }} + /> + + + + +

{addrToDisplay}

+ +
+ + + + +
+ handleAddrSwitchToggle(e)} + /> + + + +
+
+
+
+ + ) +} + +export default ReceiveCard diff --git a/src/components/app-body/bch-send/refresh-balance.js b/src/components/app-body/bch-send/refresh-balance.js new file mode 100644 index 0000000..b95163f --- /dev/null +++ b/src/components/app-body/bch-send/refresh-balance.js @@ -0,0 +1,90 @@ +/* + This library exports a RefreshBalance functional Component and a + refreshBalance() function. + The RefreshBalance Component is rendered as a hidden Waiting modal. + When the refreshBalance() function is called, it causes the modal to + appear while the wallet balance is updated. Once updated, the modal is hidden + again. +*/ + +// Global npm libraries +import React, { useEffect, useState, useCallback } from 'react' + +// Local libraries +import WaitingModal from '../../waiting-modal' + +export default function RefreshBchBalance (props) { + // Dependency injections of props + const { ref } = props + + // State + const [showWaitingModal, setShowWaitingModal] = useState(false) + const [modalBody, setModalBody] = useState([]) + const [hideSpinner] = useState(false) + + // Add a new line to the waiting modal. + const addToModal = useCallback((inStr) => { + // console.log('addToModal() inStr: ', inStr) + setModalBody(prevBody => { + // console.log('prevBody: ', prevBody) + prevBody.push(inStr) + return prevBody + }) + }, []) + + // Update the balance of the wallet. + const handleRefreshBalance = useCallback(async (appData) => { + try { + setModalBody([]) + + // Throw up the waiting modal + setShowWaitingModal(true) + + addToModal('Updating wallet balance...') + + // Get handles on app data. + const walletState = appData.bchWalletState + const cashAddr = appData.bchWalletState.cashAddress + const wallet = appData.wallet + + // Get the latest balance of the wallet. + const newBalance = await wallet.getBalance({ bchAddress: cashAddr }) + + addToModal('Updating BCH per USD price...') + const bchUsdPrice = await wallet.getUsd() + + // Update the wallet state. + walletState.bchBalance = newBalance + walletState.bchUsdPrice = bchUsdPrice + appData.updateBchWalletState({ walletState, appData }) + + setShowWaitingModal(false) + setModalBody([]) + } catch (err) { + console.error('Error while trying to update BCH balance: ', err) + + addToModal([`Error: ${err.message}`]) + setShowWaitingModal(false) + } + }, [addToModal]) + + // add a ref to the handleRefreshBalance function + // This is used to call the function from the parent component. + useEffect(() => { + if (ref && !ref.current) ref.current = { handleRefreshBalance } + }, [ref, handleRefreshBalance]) + + return ( + <> + <> + {showWaitingModal && ( + + )} + + + ) +} diff --git a/src/components/app-body/bch-send/refresh-bch-balance-button.js b/src/components/app-body/bch-send/refresh-bch-balance-button.js new file mode 100644 index 0000000..80d7044 --- /dev/null +++ b/src/components/app-body/bch-send/refresh-bch-balance-button.js @@ -0,0 +1,40 @@ +/* + This component is displayed as a button. When clicked, it loads the + RefreshBchBalance component, which renders a waiting modal while the wallet + balance is refreshed. +*/ + +// Global npm libraries +import React, { useRef } from 'react' +import { Button } from 'react-bootstrap' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faRedo } from '@fortawesome/free-solid-svg-icons' + +// Local libraries +import RefreshBchBalance from './refresh-balance' + +function RefreshBchBalanceButton (props) { + // Dependency injections of props + const appData = props.appData + + // Child function references + const refreshBchBalanceRef = useRef() + + // Update the balance of the wallet. + async function handleButtonRefreshBalance (appData) { + // Call the child function + refreshBchBalanceRef.current.handleRefreshBalance(appData) + } + + return ( + <> + + + + + ) +} + +export default RefreshBchBalanceButton diff --git a/src/components/app-body/bch-send/send-card.js b/src/components/app-body/bch-send/send-card.js new file mode 100644 index 0000000..19b7d5e --- /dev/null +++ b/src/components/app-body/bch-send/send-card.js @@ -0,0 +1,332 @@ +/* + This component controls sending of BCH. +*/ + +// Global npm libraries +import React, { useState, useRef } from 'react' +import { Container, Row, Col, Card, Form, Button } from 'react-bootstrap' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faPaperPlane, faPaste, faRandom } from '@fortawesome/free-solid-svg-icons' + +// Local libraries +import WaitingModal from '../../waiting-modal' +import RefreshBchBalance from './refresh-balance' + +function SendCard (props) { + // Dependency injection through props + const appData = props.appData + + // Modal State + const [modalBody, setModalBody] = useState([]) + const [hideSpinner, setHideSpinner] = useState(false) + const [hideWaitingModal, setHideWaitingModal] = useState(true) + const [hideModal, setHideModal] = useState(true) + + // Form State + const [bchAddr, setBchAddr] = useState('') + const [amountStr, setAmountStr] = useState('') + const [amountUnits, setAmountUnits] = useState('USD') + const [oppositeUnits, setOppositeUnits] = useState('BCH') + const [oppositeQty, setOppositeQty] = useState(0) + + // Child function references + const refreshBchBalanceRef = useRef() + + // Update the balance of the wallet. + async function handleButtonRefreshBalance (appData) { + // Call the child function + refreshBchBalanceRef.current.handleRefreshBalance(appData) + } + + // Encapsulate the state for this component into a single object that can + // be passed around to subfunctions and subcomponents. + const sendCardData = { + modalBody, + setModalBody, + hideSpinner, + setHideSpinner, + hideWaitingModal, + setHideWaitingModal, + hideModal, + setHideModal, + bchAddr, + setBchAddr, + amountStr, + setAmountStr, + amountUnits, + setAmountUnits, + oppositeUnits, + setOppositeUnits, + oppositeQty, + setOppositeQty + } + + // This function is called when the modal is closed. + function onModalClose () { + sendCardData.setHideModal(true) + + handleButtonRefreshBalance(appData) + } + + async function pasteFromClipboard () { + try { + const addr = await appData.appUtil.readFromClipboard() + sendCardData.setBchAddr(addr) + } catch (err) { + // Browser implementation. Exit quietly. + } + } + + // This is an on-change event handler that updates the amount calculated in + // both BCH and USD as the user types. + function handleUpdateAmount (inObj = {}) { + try { + const { event, appData, sendCardData } = inObj + + // Update the state of the text box. + let amountStr = event.target.value + sendCardData.setAmountStr(amountStr) + if (!amountStr) amountStr = '0' + + // Convert the string to a number. + const amountQty = parseFloat(amountStr) + + const bchUsdPrice = appData.bchWalletState.bchUsdPrice + const bchjs = appData.wallet.bchjs + + // Initialize local variables + let oppositeQty = 0 + // const amountUsd = 0 + // const amountBch = 0 + + // Calculate the amount in the opposite units. + const currentUnit = sendCardData.amountUnits + if (currentUnit.includes('USD')) { + // Convert USD to BCH + oppositeQty = bchjs.Util.floor8(amountQty / bchUsdPrice) + // amountUsd = amountQty + // amountBch = oppositeQty + } else { + // Convert BCH to USD + oppositeQty = bchjs.Util.floor2(amountQty * bchUsdPrice) + // amountUsd = oppositeQty + // amountBch = amountQty + } + + // Update app state + sendCardData.setOppositeQty(oppositeQty) + } catch (err) { + /* exit quietly */ + console.log('Error: ', err) + } + } + + // This is a click event handler that toggles the units between BCH and USD. + function handleSwitchUnits ({ sendCardData }) { + // Toggle the unit + let newUnit = '' + let oppositeUnits = '' + const oldUnit = sendCardData.amountUnits + if (oldUnit.includes('USD')) { + newUnit = 'BCH' + oppositeUnits = 'USD' + } else { + newUnit = 'USD' + oppositeUnits = 'BCH' + } + + // Clear the Amount text box + sendCardData.setAmountStr('') + sendCardData.setOppositeQty(0) + + // Persist the new units. + sendCardData.setAmountUnits(newUnit) + sendCardData.setOppositeUnits(oppositeUnits) + } + + // Add a new line to the waiting modal. + function addToModal (inStr, sendCardData) { + sendCardData.setModalBody(prevBody => { + prevBody.push(inStr) + return prevBody + }) + } + + // Send BCH based to the address in the form, and the amount specified in the + // form. + async function handleSendBch ({ sendCardData, appData }) { + console.log('Sending BCH') + try { + // Clear the modal body + sendCardData.setModalBody([]) + sendCardData.setHideSpinner(false) + + // Open the modal + sendCardData.setHideModal(false) + + let amountBch + if (sendCardData.amountUnits === 'USD') { + amountBch = sendCardData.oppositeQty + } else { + amountBch = parseFloat(sendCardData.amountStr) + } + console.log('amountBch: ', amountBch) + + if (amountBch < 0.00000546) throw new Error('Trying to send less than dust.') + + let bchAddr = sendCardData.bchAddr + let infoStr = `Sending ${amountBch} BCH ($${sendCardData.amountUsd} USD) to ${bchAddr}` + console.log(infoStr) + + // Update modal + addToModal('Preparing to send bch...', sendCardData) + + const wallet = appData.wallet + const bchjs = wallet.bchjs + + // If the address is an SLP address, convert it to a cash address. + if (bchAddr.includes('simpleledger:')) { + bchAddr = bchjs.SLP.Address.toCashAddress(bchAddr) + } + + // Convert the BCH to satoshis + const sats = bchjs.BitcoinCash.toSatoshi(amountBch) + + // Update the wallets UTXOs + infoStr = 'Updating UTXOs...' + console.log(infoStr) + addToModal(infoStr, sendCardData) + await wallet.getUtxos() + + const receivers = [{ + address: bchAddr, + amountSat: sats + }] + const txid = await wallet.send(receivers) + + // Display TXID + infoStr = `txid: ${txid}` + // console.log(infoStr) + // modalBody.push(infoStr) + addToModal(infoStr, sendCardData) + + // Link to block explorer + const explorerUrl = `https://blockchair.com/bitcoin-cash/transaction/${txid}` + const explorerLink = (Block Explorer) + // modalBody.push(explorerLink) + addToModal(explorerLink, sendCardData) + + sendCardData.setHideSpinner(true) + sendCardData.setBchAddr('') + sendCardData.setAmountStr('') + } catch (err) { + console.log('Error in handleSendBch(): ', err) + + sendCardData.setModalBody([`Error: ${err.message}`]) + sendCardData.setHideSpinner(true) + } + } + + return ( + <> + { + hideModal + ? null + : () + } + + + + + + +

Send

+
+
+ + + + + BCH Address: + + + + + +
+ + setBchAddr(e.target.value)} + value={bchAddr} + /> + pasteFromClipboard()} + /> + +
+ + +
+
+ + + + Amount: + + + + + +
+ + handleUpdateAmount({ event, appData, sendCardData })} + value={amountStr} + /> + +
+ +
+ + + Units : {amountUnits} + handleSwitchUnits({ sendCardData, appData })} + /> + + + {oppositeUnits} : {oppositeQty} + + +
+ + + + + + + +
+
+
+ + ) +} + +export default SendCard diff --git a/src/components/app-body/index.js b/src/components/app-body/index.js index 289b571..71aa877 100644 --- a/src/components/app-body/index.js +++ b/src/components/app-body/index.js @@ -18,6 +18,7 @@ import Placeholder3 from './placeholder3' import ServerSelectView from './servers/select-server-view' import SelectServerButton from './servers/select-server-button' import NftsForSale from './nfts-for-sale' +import BchSend from './bch-send' function AppBody (props) { // Dependency injection through props @@ -28,6 +29,7 @@ function AppBody (props) { } /> } /> + } /> } /> } /> } /> diff --git a/src/components/nav-menu/index.js b/src/components/nav-menu/index.js index 2744181..f7caad0 100644 --- a/src/components/nav-menu/index.js +++ b/src/components/nav-menu/index.js @@ -53,6 +53,13 @@ function NavMenu (props) { Check Balance + + BCH +