Merge pull request #4 from Permissionless-Software-Foundation/dh-send-view

feat(send): Ported bch-send View
This commit is contained in:
Chris Troutner
2025-02-06 05:39:15 -07:00
committed by GitHub
11 changed files with 709 additions and 0 deletions
+10
View File
@@ -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",
+1
View File
@@ -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",
+17
View File
@@ -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;
}
@@ -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 (
<>
<Card>
<Card.Body>
<Card.Title style={{ textAlign: 'center' }}>
<h2><FontAwesomeIcon icon={faCoins} size='lg' /> Balance</h2>
</Card.Title>
<br />
<Container>
<Row>
<Col>
<b>USD</b>: ${usdBalance}
</Col>
</Row>
<Row>
<Col>
<b>BCH</b>: {bchBalance}
</Col>
</Row>
<Row>
<Col>
<b>Satoshis</b>: {sats}
</Col>
</Row>
</Container>
</Card.Body>
</Card>
</>
)
}
export default BalanceCard
+64
View File
@@ -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 (
<>
<Container>
<Row>
<Col xs={6}>
<RefreshBchBalanceButton
appData={appData}
/>
</Col>
<Col xs={6} style={{ textAlign: 'right' }}>
<a href='https://youtu.be/KN1ZMWoLoGs' target='_blank' rel='noreferrer'>
<FontAwesomeIcon icon={faCircleQuestion} size='lg' />
</a>
</Col>
</Row>
<br />
<Row>
<Col style={{ textAlign: 'center' }}>
<BalanceCard appData={appData} />
</Col>
</Row>
<br />
<Row>
<Col>
<SendCard
appData={appData}
/>
</Col>
</Row>
<br />
<Row>
<Col>
<ReceiveCard appData={appData} />
</Col>
</Row>
</Container>
</>
)
}
export default BchSend
@@ -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 (
<>
<Card>
<Card.Body style={{ textAlign: 'center' }}>
<Card.Title>
<h2><FontAwesomeIcon icon={faWallet} size='lg' /> Receive</h2>
</Card.Title>
<br />
<Container>
<Row>
<Col style={{ color: 'green', marginBottom: '20px' }}>
{displayCopyMsg ? 'Copied' : null}
</Col>
</Row>
<Row>
<Col>
<QRCodeSVG
style={{ cursor: 'pointer' }}
className='qr-code'
value={addrToDisplay}
size={256}
fgColor='#333'
onClick={() => { handleCopyAddress(addrToDisplay) }}
/>
</Col>
</Row>
<Row>
<Col style={{ marginTop: '20px' }}>
<p>{addrToDisplay}</p>
</Col>
</Row>
<Row>
<Col xs={4} />
<Col xs={4}>
<Form>
<Form.Check
type='switch'
id='address-switch'
onChange={e => handleAddrSwitchToggle(e)}
/>
</Form>
</Col>
<Col xs={4} />
</Row>
</Container>
</Card.Body>
</Card>
</>
)
}
export default ReceiveCard
@@ -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 && (
<WaitingModal
heading='Refreshing BCH Balance'
body={modalBody}
hideSpinner={hideSpinner}
/>
)}
</>
</>
)
}
@@ -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 (
<>
<Button variant='success' onClick={() => { handleButtonRefreshBalance(appData) }}>
<FontAwesomeIcon icon={faRedo} size='lg' /> Refresh
</Button>
<RefreshBchBalance appData={appData} ref={refreshBchBalanceRef} />
</>
)
}
export default RefreshBchBalanceButton
@@ -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 = (<a href={`${explorerUrl}`} target='_blank' rel='noreferrer'>Block Explorer</a>)
// 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
: (<WaitingModal
heading='Sending BCH'
body={modalBody}
hideSpinner={hideSpinner}
closeFunc={onModalClose}
closeModalData={{ appData, sendCardData }}
/>)
}
<RefreshBchBalance appData={appData} ref={refreshBchBalanceRef} />
<Card>
<Card.Body>
<Card.Title style={{ textAlign: 'center' }}>
<h2><FontAwesomeIcon icon={faPaperPlane} size='lg' /> Send</h2>
</Card.Title>
<br />
<Container>
<Row>
<Col style={{ textAlign: 'center' }}>
<b>BCH Address:</b>
</Col>
</Row>
<Row>
<Col xs={12} style={{ textAlign: 'center' }}>
<Form>
<Form.Group controlId='formBasicEmail' style={{ display: 'flex', alignItems: 'center' }}>
<Form.Control
style={{ marginRight: '1rem' }}
type='text'
placeholder='bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d'
onChange={e => setBchAddr(e.target.value)}
value={bchAddr}
/>
<FontAwesomeIcon
style={{ cursor: 'pointer' }}
icon={faPaste}
size='lg'
onClick={(e) => pasteFromClipboard()}
/>
</Form.Group>
</Form>
</Col>
</Row>
<br />
<Row>
<Col style={{ textAlign: 'center' }}>
<b>Amount:</b>
</Col>
</Row>
<Row>
<Col xs={12}>
<Form style={{ paddingBottom: '10px' }}>
<Form.Group controlId='formBasicEmail' style={{ textAlign: 'center' }}>
<Form.Control
type='number'
onChange={(event) => handleUpdateAmount({ event, appData, sendCardData })}
value={amountStr}
/>
</Form.Group>
</Form>
</Col>
</Row>
<Row>
<Col xs={6}>
<strong>Units</strong> : {amountUnits}
<FontAwesomeIcon
style={{ cursor: 'pointer', marginLeft: '5px' }}
icon={faRandom}
size='lg'
onClick={(e) => handleSwitchUnits({ sendCardData, appData })}
/>
</Col>
<Col xs={6} style={{ textAlign: 'right' }}>
<strong>{oppositeUnits}</strong> : {oppositeQty}
</Col>
</Row>
<br />
<Row>
<Col style={{ textAlign: 'center' }}>
<Button onClick={(e) => handleSendBch({ sendCardData, appData })}>Send</Button>
</Col>
</Row>
</Container>
</Card.Body>
</Card>
</>
)
}
export default SendCard
+2
View File
@@ -17,6 +17,7 @@ import Placeholder2 from './placeholder2'
import Placeholder3 from './placeholder3'
import ServerSelectView from './servers/select-server-view'
import SelectServerButton from './servers/select-server-button'
import BchSend from './bch-send'
function AppBody (props) {
// Dependency injection through props
@@ -27,6 +28,7 @@ function AppBody (props) {
<Routes>
<Route path='/' element={<GetBalance wallet={appData.wallet} />} />
<Route path='/balance' element={<GetBalance wallet={appData.wallet} />} />
<Route path='/bch' element={<BchSend appData={appData} />} />
<Route path='/wallet' element={<Wallet appData={appData} />} />
<Route path='/placeholder2' element={<Placeholder2 />} />
<Route path='/placeholder3' element={<Placeholder3 />} />
+7
View File
@@ -44,6 +44,13 @@ function NavMenu (props) {
>
Check Balance
</NavLink>
<NavLink
className={(currentPath === '/bch') ? 'nav-link-active' : 'nav-link-inactive'}
to='/bch'
onClick={handleClickEvent}
>
BCH
</NavLink>
<NavLink
className={currentPath === '/wallet' ? 'nav-link-active' : 'nav-link-inactive'}
to='/wallet'