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
+