Project Path: bch-dex-taker-v2 Source Tree: ``` bch-dex-taker-v2 ├── PEDIGREE.md ├── src │ ├── index.js │ ├── util │ │ └── index.js │ ├── components │ │ ├── footer │ │ │ ├── index.js │ │ │ └── get-cid.js │ │ ├── nav-menu │ │ │ ├── index.js │ │ │ └── psf-logo.png │ │ ├── app-body │ │ │ ├── balance.js │ │ │ ├── index.js │ │ │ ├── bch-send │ │ │ │ ├── index.js │ │ │ │ ├── send-card.js │ │ │ │ ├── refresh-balance.js │ │ │ │ ├── balance-card.js │ │ │ │ ├── receive-card.js │ │ │ │ └── refresh-bch-balance-button.js │ │ │ ├── configuration │ │ │ │ ├── index.js │ │ │ │ ├── select-server-button.js │ │ │ │ └── select-server-view.js │ │ │ ├── sign │ │ │ │ └── index.js │ │ │ ├── slp-tokens │ │ │ │ ├── send-token-button.js │ │ │ │ ├── index.js │ │ │ │ ├── info-button.js │ │ │ │ ├── token-card.js │ │ │ │ └── refresh-tokens.js │ │ │ ├── placeholder2.js │ │ │ ├── nfts-for-sale │ │ │ │ ├── buy-button.js │ │ │ │ ├── index.js │ │ │ │ ├── info-button.js │ │ │ │ └── token-card.js │ │ │ ├── bch-wallet │ │ │ │ ├── optimize-wallet.js │ │ │ │ ├── index.js │ │ │ │ ├── clear-wallet.js │ │ │ │ ├── wallet-summary.css │ │ │ │ ├── import-wallet.js │ │ │ │ ├── wallet-summary.js │ │ │ │ ├── warning.js │ │ │ │ └── copy-on-click.js │ │ │ ├── sweep │ │ │ │ └── index.js │ │ │ └── placeholder3.js │ │ ├── starter-views.js │ │ ├── waiting-modal │ │ │ └── index.js │ │ └── load-scripts.js │ ├── hooks │ │ ├── state.js │ │ └── use-script.js │ ├── App.test.js │ ├── config │ │ └── index.js │ ├── App.css │ ├── services │ │ ├── async-load.js │ │ ├── nostr.js │ │ └── gist-servers.js │ └── App.js ├── README.md ├── LICENSE.md ├── img │ └── donation-qr.png ├── dev-docs │ └── README.md ├── config-overrides.js ├── package.json ├── deploy │ ├── publish-main.js │ ├── README.md │ ├── publish-pinata.js │ ├── publish-filecoin.js │ ├── publish-gh-pages.sh │ └── publish-bch.js └── public └── index.html ``` `/home/trout/work/psf/code/bch-dex-taker-v2/PEDIGREE.md`: ```md # Pedigree This code repository is forked from [bch-wallet-web3-spa](https://github.com/Permissionless-Software-Foundation/bch-wallet-web3-spa), and any updates to that upstream repository are pulled into this repository. ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/index.js`: ```js /* */ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' import { QueryParamProvider } from 'use-query-params' // Importing the Bootstrap CSS import 'bootstrap/dist/css/bootstrap.min.css' import { BrowserRouter } from 'react-router-dom' const root = ReactDOM.createRoot(document.getElementById('root')) root.render( {/* should be wrap all the components that use react-router-dom */} ) // Updating to React v18 // https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#updates-to-client-rendering-apis ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/util/index.js`: ```js /* A utility library for holding functions that are commonly used by many different areas of the app. */ class AppUtil { // Returns a promise that resolves 'ms' milliseconds. sleep (ms) { return new Promise(resolve => setTimeout(resolve, ms)) } // Copy a text to clipboard async copyToClipboard (text) { try { await navigator.clipboard.writeText(text) } catch (err) { console.error('Failed to copy text:', err) // document.body.removeChild(textarea) return false } } // Read text from clipboard async readFromClipboard () { try { const text = await navigator.clipboard.readText() return text } catch (err) { console.error('Failed to copy text:', err) // document.body.removeChild(textarea) return false } } } export default AppUtil ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/footer/index.js`: ```js /* A footer section for the SPA */ // Global npm libraries import React, { useEffect } from 'react' import { Container, Row, Col } from 'react-bootstrap' // Local libraries import config from '../../config' // import Memo from './get-cid' function Footer (props) { // const [ipfsCid, setIpfsCid] = useState(config.ipfsCid) const wallet = props.appData.wallet // Retrieve the most up-to-date CID for the app on Filecoin from the BCH blockchain. useEffect(() => { async function fetchData () { try { // const hash = await getUpdatedUrl(wallet) // if (hash) { // setIpfsCid(hash) // } } catch (err) { console.error('Error trying to retrieve Filecoin CID for the app from the BCH blockchain.') } } fetchData() }, [wallet]) return (
Source Code
) } // async function getUpdatedUrl (wallet) { // try { // // Exit if the wallet is not initialized. // if (!wallet) return // // Initialize the memo library for retrieving data from the BCH blockchain. // const memo = new Memo({ bchAddr: config.appBchAddr }) // await memo.initialize(wallet) // const hash = await memo.findHash() // if (!hash) { // console.error( // `Could not find IPFS hash in transactions for address ${config.appBchAddr}` // ) // return false // } // // console.log(`latest IPFS hash: ${hash}`) // return hash // } catch (err) { // console.log('Error in getUpdatedUrl(): ', err) // } // } export default Footer ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/footer/get-cid.js`: ```js /* This service file contains functions for retrieving an IPFS hash from the BCH blockchain, in a fasion similar to PS001: https://github.com/Permissionless-Software-Foundation/specifications/blob/master/ps001-media-sharing.md */ // Global npm libraries import BchMessage from 'bch-message-lib' // Local libraries import AppUtil from '../../util' class Memo { constructor (config) { this.config = config // Encapsulate dependencies this.util = new AppUtil() } // Instantiate the bch-message-lib library. async initialize (wallet) { try { // Throw an error if this class is instantiated without passing a BCH address. if (!this.config || !this.config.bchAddr) { throw new Error('Must pass a BCH address to Memo constructor.') } else { this.bchAddr = this.config.bchAddr } this.wallet = wallet this.bchMessage = new BchMessage({ wallet }) } catch (err) { console.error('Error in get-cid.js/initialize(): ', err.message) // console.log('Waiting 5 seconds before trying again.') // await this.util.sleep(5000) // this.initialize() } } // Walk the transactions associated with an address until a proper IPFS hash is // found. If one is not found, will return false. async findHash () { try { console.log(`Finding latest IPFS hash for address: ${this.bchAddr}...`) const txs = await this.bchMessage.memo.memoRead( this.bchAddr, 'IPFS UPDATE' ) // console.log(`txs: ${JSON.stringify(txs, null, 2)}`) // If the array is empty, then return false. if (txs.length === 0) return false const hash = txs[0].subject console.log(`...found this IPFS hash: ${hash}`) // The transactions should automatically be sorted by the bchMessage // library. So Just return the subject. return hash } catch (err) { console.warn('Could not find IPFS hash in transaction history.') return false } } } export default Memo ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/nav-menu/index.js`: ```js /* This component controlls the navigation menu. Inspired from this example: https://codesandbox.io/s/react-bootstrap-hamburger-menu-example-rnud4?from-embed */ // Global npm libraries import React, { useState } from 'react' import { Nav, Navbar, Image } from 'react-bootstrap' // Used for Navbar Style and Layouts . import { NavLink } from 'react-router-dom' // Used to navigate between routes // Assets import Logo from './psf-logo.png' function NavMenu (props) { // Get the current path const { currentPath } = props.appData // Navbar state const [expanded, setExpanded] = useState(false) // Handle click event const handleClickEvent = () => { // Collapse the navbar setExpanded(false) } return ( <> {' '} DEX ) } export default NavMenu ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/balance.js`: ```js /* Component for looking up the balance of a BCH address. */ // Global npm libraries import React, { useState } from 'react' import { Container, Row, Col, Form, Button, Spinner } from 'react-bootstrap' function GetBalance (props) { const { wallet } = props // State const [balance, setBalance] = useState('') const [textInput, setTextInput] = useState('') // Button click handler const handleGetBalance = async (e) => { e.preventDefault() try { // Exit on invalid input if (!textInput) return if (!textInput.includes('bitcoincash:')) return setBalance(
Retrieving balance...
) const balance = await wallet.getBalance({ bchAddress: textInput }) console.log('balance: ', balance) const bchBalance = wallet.bchjs.BitcoinCash.toBitcoinCash(balance) setBalance(`Balance: ${balance} sats, ${bchBalance} BCH`) } catch (err) { setBalance(

Error: {`${err.message}`}

) } } return ( <>
Enter any BCH address to query its balance on the blockchain. setTextInput(e.target.value)} />

{balance}
) } export default GetBalance ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/index.js`: ```js /* This Body component is a container for all the different Views of the app. Views are equivalent to 'pages' in a multi-page app. Views are hidden or displayed to simulate the use of pages in an SPA. The Body app contains all the Views and chooses which to show, based on the state of the Menu component. */ // Global npm libraries import React from 'react' import { Route, Routes } from 'react-router-dom' // Local libraries import GetBalance from './balance' import Wallet from './bch-wallet' import Placeholder2 from './placeholder2' 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' import SlpTokens from './slp-tokens' import SweepWif from './sweep/index.js' import SignMessage from './sign/index.js' import ServerSelectView from './configuration/select-server-view' function AppBody (props) { // Dependency injection through props const appData = props.appData return ( <> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> {/** Show in all paths except the servers view */} {/* {appData.currentPath !== '/servers' && } */} ) } export default AppBody ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/bch-send/index.js`: ```js /* 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 ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/bch-send/send-card.js`: ```js /* 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:
e.preventDefault()}> setBchAddr(e.target.value)} value={bchAddr} /> pasteFromClipboard()} />

Amount:
{ e.preventDefault(); handleSendBch({ sendCardData, appData }) }}> handleUpdateAmount({ event, appData, sendCardData })} value={amountStr} />
Units : {amountUnits} handleSwitchUnits({ sendCardData, appData })} /> {oppositeUnits} : {oppositeQty}
) } export default SendCard ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/bch-send/refresh-balance.js`: ```js /* 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 && ( )} ) } ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/bch-send/balance-card.js`: ```js /* 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 ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/bch-send/receive-card.js`: ```js /* 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 ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/bch-send/refresh-bch-balance-button.js`: ```js /* 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 ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/configuration/index.js`: ```js /* This component is a View that allows the user to handle configuration settings for the app. */ // Global npm libraries import React from 'react' import ServerSelectView from './select-server-view' function ConfigurationView (props) { const { appData } = props return ( <> ) } export default ConfigurationView ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/configuration/select-server-button.js`: ```js /* This component contains a drop-down form that lets the user select from a range of Global Back End servers. */ // Global npm libraries import React from 'react' import { Container, Row, Col, Button } from 'react-bootstrap' import { useNavigate } from 'react-router-dom' const ServerSelect = (props) => { const { linkTo, appData } = props // Use the navigate function to navigate to the servers view const navigate = useNavigate() // This is a click handler for the server select button. It brings up the // server selection View. const handleServerSelect = () => { console.log('This function should navigate to the server selection view.') navigate(linkTo) } return ( <>

Having trouble loading? Try selecting a different back-end server.

Current Server : {appData.serverUrl}


) } export default ServerSelect ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/configuration/select-server-view.js`: ```js /* This component is a View that allows the user to select a back end server from a list of servers. */ // Global npm libraries import React, { useState } from 'react' import { Row, Col, Form, Card } from 'react-bootstrap' function ServerSelectView (props) { const { appData } = props const [selectedServer, setSelectedServer] = useState(appData.serverUrl) const servers = appData.servers // Update server when dropdown selection changes const handleServerChange = (event) => { setSelectedServer(event.target.value) } const onSaveServer = (serverUrl) => { console.log('server target: ', serverUrl) appData.updateLocalStorage({ serverUrl }) window.location.href = '/' } return ( <>

Configuration

This page allows you to change configuration settings for different back end services. This page is for advanced users only.


Select an alternative server below. The app will reload and use the selected server.

{servers.map((server, i) => ( ))}
) } export default ServerSelectView ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/sign/index.js`: ```js /* Component for signing a message with a WIF private key. */ // Global npm libraries import React, { useState } from 'react' import { Container, Row, Col, Form, Button } from 'react-bootstrap' import { faCopy } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' function SignMessage (props) { // Convert class state to hooks const { wallet, appUtil } = props.appData const [sign, setSign] = useState('') const [msg, setMsg] = useState('') const [bchAddr] = useState(wallet.walletInfo.cashAddress) const [slpAddr] = useState(wallet.walletInfo.slpAddress) const [err, setErr] = useState('') const [copied, setCopied] = useState(false) const handleSignMessage = (event) => { try { event.preventDefault() if (!msg) throw new Error('Enter a message to sign.') const bchjs = wallet.bchjs const wif = props.appData.wallet.walletInfo.privateKey const sig = bchjs.BitcoinCash.signMessageWithPrivKey(wif, msg) setSign(sig) setErr('') } catch (err) { console.log('Error in handleSignMessage(): ', err) setErr(err.message) setSign('') } } // Function to copy the value to the clipboard. const handleCopyToClipboard = async (value) => { appUtil.copyToClipboard(value) // show the copied message setCopied(true) // hide copied message after 1 second setTimeout(function () { setCopied(false) }, 1000) } const copyIcon = (value) => { return handleCopyToClipboard(value)} style={{ cursor: 'pointer', marginLeft: '10px' }} /> } return ( <>

This view allows you cryptographically sign a message with your wallet. These signatures are used in a wide range of applications, such as gaining access to the PSF VIP Telegram channel.

Enter any message into the form below and click the button. This view will generate a cryptographic signature.

Enter a message to sign. setMsg(e.target.value)} /> {err &&

{`Error: ${err}`}

}

{sign && (

Signature: {sign} {copyIcon(sign)}

BCH Address: {bchAddr} {copyIcon(bchAddr)}

SLP Address: {slpAddr} {copyIcon(slpAddr)}

{copied && ( Copied! )}
)}
) } export default SignMessage ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/slp-tokens/send-token-button.js`: ```js /* This component renders as a button. When clicked, it opens up a modal for sending a quantity of tokens. This component requires state, because it's a complex form that is being manipulated by the user. */ // Global npm libraries import React, { useState } from 'react' import { Button, Modal, Container, Row, Col, Form, Spinner } from 'react-bootstrap' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faPaperPlane, faPaste } from '@fortawesome/free-solid-svg-icons' function SendTokenButton ({ token, appData, refreshTokens }) { // Convert class state to useState hooks const [showAddrWarning, setShowAddrWarning] = useState(false) const [showModal, setShowModal] = useState(false) const [statusMsg, setStatusMsg] = useState('') const [hideSpinner, setHideSpinner] = useState(true) const [shouldRefreshOnModalClose, setShouldRefreshOnModalClose] = useState(false) const [sendToAddress, setSendToAddress] = useState('') const [sendQtyStr, setSendQtyStr] = useState('') const [dialogFinished, setDialogFinished] = useState(true) // Handler functions const handleShowModal = () => setShowModal(true) const handleCloseModal = async () => { if (!dialogFinished) return if (shouldRefreshOnModalClose) { setShowModal(false) setShouldRefreshOnModalClose(false) setStatusMsg('') await refreshTokens() } else { setShowModal(false) setStatusMsg('') setSendToAddress('') setSendQtyStr('') } } const handleUpdateSendToAddr = (event) => { const value = event.target.value setSendToAddress(value) setShowAddrWarning(value.includes('bitcoincash')) } const handleGetMax = () => { setSendQtyStr(token.qty) } // Click handler that fires when the user clicks the 'Send' button. const handleSendTokens = async (e) => { e.preventDefault() try { setStatusMsg('Preparing to send tokens...') setHideSpinner(false) setDialogFinished(false) setShowAddrWarning(false) // Validate the quantity const qty = parseFloat(sendQtyStr) if (isNaN(qty)) throw new Error('Invalid send quantity') const wallet = appData.wallet const bchjs = wallet.bchjs // Validate the address let addr = sendToAddress if (addr.includes('simpleledger')) { addr = bchjs.SLP.Address.toCashAddress(addr) } if (!addr.includes('bitcoincash')) throw new Error('Invalid address') let infoStr = 'Updating UTXOs...' setStatusMsg(infoStr) await wallet.getUtxos() const receiver = [{ address: addr, tokenId: token.tokenId, qty }] infoStr = 'Generating and broadcasting transaction...' setStatusMsg(infoStr) const txid = await wallet.sendTokens(receiver, 3) console.log(`Token sent. TXID: ${txid}`) setStatusMsg(

Success! See on Block Explorer

) setHideSpinner(true) setSendQtyStr('') setSendToAddress('') setShouldRefreshOnModalClose(true) setDialogFinished(true) } catch (err) { console.error('Error in handleSendTokens(): ', err) setStatusMsg(`Error sending tokens: ${err.message}`) setHideSpinner(true) setDialogFinished(true) } } // Load address from clipboard const pasteFromClipboard = async () => { try { const address = await appData.appUtil.readFromClipboard() setSendToAddress(address) } catch (err) { console.warn('Error pasting from clipboard: ', err) } } // Modal JSX const getModal = () => { return ( Send Tokens: {token.ticker} {/* ... existing Modal.Body content ... */} SLP Address:
e.preventDefault()}>

Amount:
setSendQtyStr(e.target.value)} value={sendQtyStr} />


{showAddrWarning && ( <>

Warning: Careful! Not all Bitcoin Cash wallets are token-aware. If you send this token to a wallet that is not token-aware, it could be burned. It's best practice to only send tokens to 'simpleledger' addresses and not 'bitcoincash' addresses.


)} {statusMsg} {!hideSpinner && }
) } return ( <> {showModal && getModal()} ) } export default SendTokenButton ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/slp-tokens/index.js`: ```js /* This is the 'Token View'. It displays the SLP tokens in the wallet. */ // Global npm libraries import React, { useState, useEffect, useCallback } from 'react' import { Container, Row, Col, Spinner } from 'react-bootstrap' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faCircleQuestion } from '@fortawesome/free-solid-svg-icons' // Local libraries import TokenCard from './token-card' import RefreshTokenBalance from './refresh-tokens' const SlpTokens = (props) => { const [appData, setAppData] = useState(props.appData) const [iconsAreLoaded, setIconsAreLoaded] = useState(false) const [tokens, setTokens] = useState([]) const refreshTokenButtonRef = React.useRef() // Update the tokens state when the appData changes useEffect(() => { setTokens(appData.bchWalletState.slpTokens) }, [appData]) // This function is triggered when the token balance needs to be refreshed // from the blockchain. // This needs to happen after sending a token, to reflect the changed balance // within the wallet app. // This function triggers the on-click function within the refresh-tokens.js button. const refreshTokens = async () => { const newAppData = await refreshTokenButtonRef.current.handleRefreshTokenBalance() setAppData(newAppData) } // Get Cid from url const parseCid = (url) => { // get the cid from the url format 'ipfs://bafybeicem27xbzs65uvbcgykcmscsgln3lmhbfrcoec3gdttkdgtxv5acq if (url && url.includes('ipfs://')) { const cid = url.split('ipfs://')[1] return cid } return url } // Fetch mutable data if it exist and get the token icon url const fetchTokenIcon = useCallback(async (token) => { try { // Get the token data const tokenData = await appData.wallet.getTokenData(token.tokenId) if (!tokenData.mutableData) return false // Return false if no mutable data // Get the token icon from the mutable data const cid = parseCid(tokenData.mutableData) console.log('mutable data cid', cid) const { json } = await appData.wallet.cid2json({ cid }) if (!json) return false const iconUrl = json.tokenIcon // Return icon url return iconUrl } catch (error) { return false } }, [appData]) // This function loads the token icons from the ipfs gateways. const lazyLoadTokenIcons = useCallback(async () => { try { setIconsAreLoaded(false) const tokens = appData.bchWalletState.slpTokens setTokens(tokens) // update token state // map each token and fetch the icon url for (let i = 0; i < tokens.length; i++) { const thisToken = tokens[i] // Incon does not need to be downloaded, so continue with the next one if (thisToken.iconAlreadyDownloaded) continue // Try to get token icon url from mutable data. const iconUrl = await fetchTokenIcon(thisToken) console.log('iconUrl', iconUrl) if (iconUrl) { // Set the icon url to the token , this can be used to display the icon in the token card component. thisToken.icon = iconUrl } // Mark token to prevent fetch token icon again. thisToken.iconAlreadyDownloaded = true } appData.updateBchWalletState({ walletObj: { slpTokens: tokens }, appData }) setIconsAreLoaded(true) } catch (error) { setIconsAreLoaded(true) } }, [appData, fetchTokenIcon]) // Start to load the token icons when the component is mounted useEffect(() => { lazyLoadTokenIcons() }, [lazyLoadTokenIcons]) // Generate the token cards for each token in the wallet. const generateCards = () => { const tokens = appData.bchWalletState.slpTokens return tokens.map(thisToken => ( )) } return ( <> { !iconsAreLoaded && (
Loading Token Icons
) }

{generateCards()} {/** Display a message if no tokens are found */} {tokens.length === 0 && ( No tokens found in wallet )}
) } export default SlpTokens ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/slp-tokens/info-button.js`: ```js /* This component renders as a button. When clicked, it opens a modal that displays information about the token. This is a functional component with as little state as possible. */ // Global npm libraries import React, { useState } from 'react' import { Button, Modal, Container, Row, Col } from 'react-bootstrap' // Takes a string as input. If it matches a pattern for a link, a JSX object is // returned with a link. Otherwise the original string is returned. function linkIfUrl (url) { // Convert the URL into a link if it contains 'http' if (url.includes('http')) { url = ({url}) // } else if (url.includes('ipfs://')) { // Convert to a Filecoin link if its an IPFS reference. const cid = url.substring(7) url = ({url}) } return url } function InfoButton (props) { const [show, setShow] = useState(false) const handleClose = () => { setShow(false) // props.instance.setState({ showModal: false }) } const handleOpen = () => { setShow(true) } // Convert the url property of the token to a link, if it matches common patterns. let url = props.token.url url = linkIfUrl(props.token.url) // console.log('props.token: ', props.token) return ( <> Token Information Ticker: {props.token.ticker} Name: {props.token.name} Token ID: {props.token.tokenId} Decimals: {props.token.decimals} Token Type: {props.token.tokenType} URL: {url} ) } export default InfoButton ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/slp-tokens/token-card.js`: ```js /* This Card component summarizes an SLP token. if a token icon does not exist or cant be loaded , then display a default icon from Jdenticon library. */ // Global npm libraries import React, { useState, useEffect } from 'react' import { Container, Row, Col, Card } from 'react-bootstrap' import Jdenticon from '@chris.troutner/react-jdenticon' // Local libraries import InfoButton from './info-button' import SendTokenButton from './send-token-button' function TokenCard (props) { const { token } = props const [icon, setIcon] = useState(token.icon) // Update icon state every token.icon changes useEffect(() => { setIcon(token.icon) }, [token.icon]) return ( <> {/** If the icon is loaded, display it */ icon && ( { setIcon(null) // Set the icon to null if it fails to load the image url. }} /> ) } {/** If the icon is not loaded, display the Jdenticon */ !icon && ( ) }

{props.token.ticker}

{props.token.name}
Balance: {props.token.qty}
) } export default TokenCard ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/slp-tokens/refresh-tokens.js`: ```js /* This component is displayed as a button. When clicked, it displays a modal with a spinny gif, while the wallets SLP token list is updated from the blockchain and psf-slp-indexer. */ // Global npm libraries import React, { useState, useEffect, useCallback } from 'react' import { Button } from 'react-bootstrap' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faRedo } from '@fortawesome/free-solid-svg-icons' // Local libraries import WaitingModal from '../../waiting-modal' function RefreshTokenBalance ({ appData: initialAppData, ref, lazyLoadTokenIcons }) { const [appData, setAppData] = useState(initialAppData) const [modalBody, setModalBody] = useState([]) const [hideSpinner, setHideSpinner] = useState(false) const [hideWaitingModal, setHideWaitingModal] = useState(true) // Add a new line to the waiting modal. const addToModal = (inStr) => { setModalBody(prevBody => [...prevBody, inStr]) } // Update the balance of the wallet. const handleRefreshTokenBalance = useCallback(async () => { try { // Throw up the waiting modal setHideWaitingModal(false) addToModal('Updating token balance...') // Get handles on app data. const walletState = appData.bchWalletState const wallet = appData.wallet // Update the wallet UTXOs await wallet.initialize() const tokenList = await wallet.listTokens() // Copy tokens from old token state. for (let i = 0; i < tokenList.length; i++) { const thisToken = tokenList[i] // Look through the existing wallet state for the matching token. const existingToken = walletState.slpTokens.filter(x => x.tokenId === thisToken.tokenId) // If the current wallet state has an icon, copy it over. if (existingToken[0] && existingToken[0].icon) { thisToken.icon = existingToken[0].icon } } // Update the wallet state. walletState.slpTokens = tokenList appData.updateBchWalletState({ walletObj: walletState, appData }) const newAppData = { ...appData, bchWalletState: walletState } // Update state setHideWaitingModal(true) setAppData(newAppData) setModalBody([]) // Lazy load icons for any new tokens. await lazyLoadTokenIcons() return newAppData } catch (err) { console.error('Error while trying to update BCH balance: ', err) setModalBody([`Error: ${err.message}`]) setHideSpinner(true) } }, [appData, lazyLoadTokenIcons]) // 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 = { handleRefreshTokenBalance } }, [ref, handleRefreshTokenBalance]) return ( <> {!hideWaitingModal && ( )} ) } export default RefreshTokenBalance ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/placeholder2.js`: ```js /* This is a placeholder View */ // Global npm libraries import React, { useEffect } from 'react' function Placeholder2 (props) { useEffect(() => { console.log('Placeholder 2 loaded.') }, []) return ( <>

This is placeholder View #2

) } export default Placeholder2 ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/nfts-for-sale/buy-button.js`: ```js /* This component renders as a button. When clicked, it initiates the purchase of the token. This is the Signal part of the SWaP protocol. */ // Global npm libraries import React, { useState } from 'react' import { Button, Modal, Container, Row, Col, Spinner } from 'react-bootstrap' function BuyButton (props) { const { token, appData, onSuccess } = props console.log('props: ', props) const [show, setShow] = useState(false) // show the modal const [onFetch, setOnFetch] = useState(false) // show the spinner const [error, setError] = useState(false) // show the error message // const [eventId, setEventId] = useState(false) // show the event id const [noteId, setNoteId] = useState(false) // show the note id const [success, setSuccess] = useState(false) // show the success message const [showConfirmation, setShowConfirmation] = useState(true) // show the confirmation view const [progressMsg, setProgressMsg] = useState([]) // show the progress message // Handler for when user clicks the "Buy" button. const handleBuy = async () => { try { console.log('handleBuy()') console.log('token: ', token) console.log('appData: ', appData) setShowConfirmation(false) setOnFetch(true) /* // dev-test success view setShowConfirmation(false) setSuccess(true) setNoteId('note12986q83gre76vl9dldpnnhej7y67h76xzw0tx0hm4mq6uradr6lskch7v9') setOnFetch(false) return */ const progress = progressMsg progress.push(

Generating counter offer transaction...

) setProgressMsg(progress) const targetOffer = token.nostrEventId console.log('targetOffer: ', targetOffer) // Generate a counter offer. const bchDexLib = appData.dexLib const { offerData, partialHex } = await bchDexLib.take.takeOffer( targetOffer ) progress.push(

Uploading counter offer to Nostr...

) setProgressMsg(progress) console.log('offerData: ', offerData) console.log('partialHex: ', partialHex) // Upload the counter offer to Nostr. const nostr = appData.nostr const { eventId, noteId } = await nostr.testNostrUpload({ offerData, partialHex }) console.log( `Counter Offer uploaded to Nostr with this event ID: ${eventId}` ) console.log(`https://astral.psfoundation.info/${noteId}`) setNoteId(noteId) setSuccess(true) setOnFetch(false) } catch (error) { setOnFetch(false) setError(error.message) } } // Handler for when user requests to close the modal. const handleClose = () => { console.log('handleClose()') // Deny close if the purchase is in progress. if (onFetch) { return } // Call onSuccess() callback if the modal close after a success purchase. if (success && onSuccess) { onSuccess() } // Reset the state. setError(false) setSuccess(false) setNoteId(false) setShow(false) setOnFetch(false) setShowConfirmation(true) } // Replace with dummy button until token data is loaded. if (!props.token.tokenData) { return ( <> ) } return ( <> {success ? ( Successful Purchase ) : ( 'Processing Purchase' )} {/** If the purchase is not confirmed, display the token details. and confirm button. */} {showConfirmation && ( Ticker: {token.ticker} Name: {props.token.tokenData.genesisData.name} Price: {token.usdPrice} )} {/** show purchase progress */} {onFetch && ( <> {progressMsg} )} {error && ( {error} )} {success && ( <>

Note ID : {' '} {noteId}

What happens now:
The Seller software must finalize the sale by accepting the Counter Offer transaction you just generated. If their software is online, the token should appear in your wallet within a few minutes. Before it is accepted, you can cancel the purchase by Sweeping your Counter Offer UXTO back to your wallet.

)}
{showConfirmation && ( )}
) } export default BuyButton ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/nfts-for-sale/index.js`: ```js /* Shows NFTs for sale on the DEX. workflow: 1. Load offers 2. display token cards with current offers data 3. Load tokens data 4. add data to offers and update card with new data 5. Load tokens icons 6. add icons to offers and update card with new icons */ // Global npm libraries import React, { useState, useEffect, useCallback } from 'react' import { Container, Row, Col, Button, Spinner } from 'react-bootstrap' import axios from 'axios' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faRedo } from '@fortawesome/free-solid-svg-icons' // Local libraries import config from '../../../config' import TokenCard from './token-card' // Global variables and constants const SERVER = config.dexServer function NftsForSale (props) { // Dependency injection through props const appData = props.appData const [offers, setOffers] = useState([]) const [isLoading, setIsLoading] = useState(false) const [offersAreLoaded, setOffersAreLoaded] = useState(false) const [iconsAreLoaded, setIconsAreLoaded] = useState(false) const [dataAreLoaded, setDataAreLoaded] = useState(false) // Handler for refresh button const handleRefresh = () => { console.log('handling refresh') loadNftOffers() } // Function to process token data const processTokenData = useCallback(async (offer) => { try { const { wallet, bchWalletState } = appData const { bchjs } = wallet // Calculate USD price const rateInSats = parseInt(offer.rateInBaseUnit) const bchCost = bchjs.BitcoinCash.toBitcoinCash(rateInSats) const usdPrice = bchCost * bchWalletState.bchUsdPrice * offer.numTokens offer.usdPrice = `$${usdPrice.toFixed(3)}` return offer } catch (err) { console.error('Error processing token:', err) return offer } }, [appData]) // Fetch offers const getNftOffers = useCallback(async (page = 0) => { try { setOffersAreLoaded(false) const result = await axios.get(`${SERVER}/offer/list/nft/${page}`) const rawOffers = result.data console.log('rawOffers: ', rawOffers) // Process each offer const processedOffers = [] for (let i = 0; i < rawOffers.length; i++) { const offer = rawOffers[i] const processedOffer = await processTokenData(offer) processedOffers.push(processedOffer) } setOffersAreLoaded(true) return processedOffers } catch (err) { console.error('getNftOffers error:', err) setOffersAreLoaded(true) throw err } }, [processTokenData]) // This function loads the token data . const lazyLoadTokenData = useCallback(async (tokens) => { try { setDataAreLoaded(false) // map each token and fetch the token data for (let i = 0; i < tokens.length; i++) { const thisToken = tokens[i] // data does not need to be downloaded, so continue with the next one if (thisToken.dataAlreadyDownloaded) continue // Try to get token data. const tokenData = await appData.wallet.getTokenData(thisToken.tokenId) if (tokenData) { // Set data to the token object , this can be used to display the token name in the token card component. thisToken.tokenData = tokenData } // Mark token to prevent fetch token data again. thisToken.dataAlreadyDownloaded = true } setDataAreLoaded(true) } catch (error) { setDataAreLoaded(true) } }, [appData]) // Fetch mutable data if it exist and get the token icon url const fetchTokenInfo = useCallback(async (token) => { try { // Get the token data const tokenData = token.tokenData if (!tokenData.mutableData) return false // Return false if no mutable data // Get the token icon from the mutable data const cid = parseCid(tokenData.mutableData) console.log('mutable data cid', cid) const { json } = await appData.wallet.cid2json({ cid }) console.log('json: ', json) if (!json) return false let iconUrl = json.tokenIcon if (json.fullSizedUrl && json.fullSizedUrl.includes('http')) { iconUrl = json.fullSizedUrl } // Return icon url return iconUrl } catch (error) { return false } }, [appData]) // This function loads the token icons from the ipfs gateways. const lazyLoadTokenIcons = useCallback(async (tokens) => { try { setIconsAreLoaded(false) // map each token and fetch the icon url for (let i = 0; i < tokens.length; i++) { const thisToken = tokens[i] // Incon does not need to be downloaded, so continue with the next one if (thisToken.iconAlreadyDownloaded) continue // Try to get token icon url from mutable data. const iconUrl = await fetchTokenInfo(thisToken) console.log('iconUrl', iconUrl) if (iconUrl) { // Set the icon url to the token , this can be used to display the icon in the token card component. thisToken.icon = iconUrl } // Mark token to prevent fetch token icon again. thisToken.iconAlreadyDownloaded = true } setIconsAreLoaded(true) } catch (error) { setIconsAreLoaded(true) } }, [fetchTokenInfo]) // Main function to load NFT offers const loadNftOffers = useCallback(async () => { try { setIsLoading(true) // Get tokens in offers const offers = await getNftOffers() console.log('offers: ', offers) setOffers(offers) // Load tokens data await lazyLoadTokenData(offers) // Load tokens icons await lazyLoadTokenIcons(offers) setIsLoading(false) } catch (err) { setIsLoading(false) console.error('Error loading NFT offers:', err) } }, [lazyLoadTokenData, lazyLoadTokenIcons, getNftOffers]) // Effect to load NFTs on component mount useEffect(() => { console.log('loading nfts for sale') loadNftOffers() }, [loadNftOffers]) // Get Cid from url const parseCid = (url) => { // get the cid from the url format 'ipfs://bafybeicem27xbzs65uvbcgykcmscsgln3lmhbfrcoec3gdttkdgtxv5acq if (url && url.includes('ipfs://')) { const cid = url.split('ipfs://')[1] return cid } return url } // This function generates a Token Card for each token in the wallet. function generateCards (offers) { console.log('generateCards() offerData: ', offers) const tokens = offers const tokenCards = [] for (let i = 0; i < tokens.length; i++) { const thisToken = tokens[i] const thisTokenCard = ( ) tokenCards.push(thisTokenCard) } return tokenCards } return (

NFTs for Sale

{/** Show spinner info if tokens are loaded but data is not loaded */ offersAreLoaded && !dataAreLoaded && (
Loading Token Data
) } {/** Show spinner info if tokens are loaded but icons are not loaded */ dataAreLoaded && !iconsAreLoaded && (
Loading Token Icons
) }
{!isLoading && ( )} {!offersAreLoaded && ( )} {offersAreLoaded && generateCards(offers)} {/** Display a message if no tokens are found */} {offersAreLoaded && offers.length === 0 && ( No Offers found. )}
) } export default NftsForSale ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/nfts-for-sale/info-button.js`: ```js /* This component renders as a button. When clicked, it opens a modal that displays information about the token. This is a functional component with as little state as possible. */ // Global npm libraries import React, { useState } from 'react' import { Button, Modal, Container, Row, Col } from 'react-bootstrap' function InfoButton (props) { const [show, setShow] = useState(false) const handleClose = () => { console.log('handleClose()') setShow(false) // props.instance.setState({ showModal: false }) } const handleOpen = () => { console.log('handleOpen()') setShow(true) } // Replace with dummy button until token data is loaded. if (!props.token.tokenData) { return ( <> ) } return ( <> Token Information Ticker: {props.token.ticker} Name: {props.token.tokenData.genesisData.name} Token ID: {props.token.tokenId} Decimals: {props.token.tokenData.genesisData.decimals} Token Type: {props.token.tokenType} ) } export default InfoButton ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/nfts-for-sale/token-card.js`: ```js /* This Card component summarizes an SLP token. */ // Global npm libraries import React, { useState, useEffect } from 'react' import { Container, Row, Col, Card } from 'react-bootstrap' import Jdenticon from '@chris.troutner/react-jdenticon' // Local libraries import InfoButton from './info-button' import BuyButton from './buy-button' function TokenCard (props) { const { token, appData, handleRefresh } = props const [icon, setIcon] = useState(token.icon) const [tokenData, setTokenData] = useState(token.tokenData) console.log('TokenCard() appData: ', appData) // Update icon state every token.icon and token.tokenData changes useEffect(() => { setIcon(token.icon) setTokenData(token.tokenData) }, [token.icon, token.tokenData]) return ( <> {/** If the icon is loaded, display it */ icon && ( { setIcon(null) // Set the icon to null if it fails to load the image url. }} /> ) } {/** If the icon is not loaded, display the Jdenticon */ !icon && ( ) }

{token.ticker}

{tokenData && tokenData.genesisData ? tokenData.genesisData.name : null}
Price: {token.usdPrice}
) } // eslint-disable-next-line {/* */ } export default TokenCard ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/bch-wallet/optimize-wallet.js`: ```js /* This component allows the user to optimize their wallet by consolidating UTXOs. This speeds up all the network calls and results in an improved UX. */ // Global npm libraries import React, { useState } from 'react' import { Container, Row, Col, Card, Button } from 'react-bootstrap' // Local libraries import WaitingModal from '../../waiting-modal' function OptimizeWallet (props) { // State const [showModal, setShowModal] = useState(false) const [modalBody, setModalBody] = useState([]) const [hideSpinner, setHideSpinner] = useState(false) const [denyClose, setDenyClose] = useState(false) // Get props values const { wallet } = props.appData // Optimize wallet const handleOptimize = async () => { console.log('Optimize Wallet button clicked.') // Show waiting modal setShowModal(true) setModalBody(['Optimizing wallet...']) setDenyClose(true) // Optimize wallet await wallet.optimize() // Show success modal setShowModal(true) setModalBody(['Your wallet has been optimized!']) setDenyClose(false) setHideSpinner(true) try { // Get all UTXOs in the wallet const utxos = wallet.utxos.utxoStore console.log('utxos: ', utxos) // Add up all the UTXOs const bchUtxoCnt = utxos.bchUtxos.length let fungibleUtxoCnt = utxos.slpUtxos.type1.tokens.length if (!fungibleUtxoCnt) fungibleUtxoCnt = 0 let nftUtxoCnt = utxos.slpUtxos.nft.length if (!nftUtxoCnt) nftUtxoCnt = 0 const totalUtxos = bchUtxoCnt + fungibleUtxoCnt + nftUtxoCnt console.log(`bchUtxoCnt: ${bchUtxoCnt}, fungibleUtxoCnt: ${fungibleUtxoCnt}, nftUtxoCnt: ${nftUtxoCnt}`) console.log(`total UTXO count: ${totalUtxos}`) if (totalUtxos > 10) { const newModalBody = [ 'Your wallet has been optimized!', 'Your wallet still has more than 10 UTXOs. Increased numbers of UTXOs slow down performance. If you have several tokens in your wallet, it is recommended that you store them in a paper wallet. Here is a video explaining how to do that:' ] newModalBody.push(Video: How to Store SLP Tokens on a Paper Wallet) newModalBody.push(Generate a Paper Wallet) setModalBody(newModalBody) } } catch (err) { console.log('Error while trying to count total number of UTXOs: ', err) } } return ( <>

Optimize Wallet

Clicking the button below will optimize your wallet and make it function faster.

How it works: By consolidating as many UTXOs in your wallet as possible, it reduces the total number of UTXOs in your wallet. Fewer UTXOs in your wallet make all network calls faster, and results in an improved user experience.

{showModal && ( )} ) } export default OptimizeWallet ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/bch-wallet/index.js`: ```js /* This component controlls the Wallet View. */ // 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 WebWalletWarning from './warning' import WalletSummary from './wallet-summary' import WalletClear from './clear-wallet' import WalletImport from './import-wallet' import OptimizeWallet from './optimize-wallet' function BchWallet (props) { // Dependency injection through props const appData = props.appData console.log('appData: ', appData) return ( <>



) } export default BchWallet ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/bch-wallet/clear-wallet.js`: ```js /* This Card component is used to clear the Local Storage and reset the wallet. */ // Global npm libraries import React from 'react' import { Container, Row, Col, Card, Button } from 'react-bootstrap' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faTriangleExclamation } from '@fortawesome/free-solid-svg-icons' const WalletClear = (props) => { const { removeLocalStorageItem } = props.appData // Delete wallet data from Local Storage and reload the app. const handleClearLocalStorage = () => { console.log('Deleting wallet and reloading page.') // Delete the mnemonic from Local Storage removeLocalStorageItem('mnemonic') // Reload the app. window.location.reload() } return ( <>

{' '} Clear Local Storage

Clicking the button below will clear the Local Storage, which will reload the app with a newly created wallet.
Be sure to write down your 12-word mnemonic to back up your wallet before clicking the button! .

) } export default WalletClear ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/bch-wallet/wallet-summary.css`: ```css .blurred { filter: blur(6px); -webkit-filter: blur(6px); } ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/bch-wallet/import-wallet.js`: ```js /* This component allows the user to import a new wallet using a 12-word mnemonic. */ // Global npm libraries import React, { useCallback } from 'react' import { Container, Row, Col, Card, Button, Form } from 'react-bootstrap' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faFileExport, faPaste } from '@fortawesome/free-solid-svg-icons' // import { Clipboard } from '@capacitor/clipboard' const WalletImport = (props) => { const [newMnemonic, setNewMnemonic] = React.useState('') const { appData } = props // Load mnemonic from clipboard const pasteFromClipboard = async () => { try { const mnemonic = await appData.appUtil.readFromClipboard() setNewMnemonic(mnemonic) } catch (err) { console.warn('Error pasting from clipboard: ', err) } } // Handle input change for mnemonic const handleImportMnemonic = async (event) => { const inputStr = event.target.value const formattedInput = inputStr.toLowerCase() setNewMnemonic(formattedInput) } // Ensure the mnemonic is valid. If it is, then replace the current mnemonic // in LocalStorage and reload the page. const handleImportWallet = useCallback(async (event) => { try { const mnemonic = newMnemonic const wallet = appData.wallet const bchjs = wallet.bchjs // Verify the mnemonic is valid. const isValid = bchjs.Mnemonic.validate(mnemonic, bchjs.Mnemonic.wordLists().english) if (isValid.includes('is not in wordlist')) { console.log('Mnemonic is NOT valid') } else { console.log('Mnemonic is valid') } // Replace the old mnemonic in LocalStorage with the new one. appData.updateLocalStorage({ mnemonic }) // Reload the app. window.location.reload() } catch (error) { console.warn('Error importing wallet: ', error) } }, [newMnemonic, appData]) return ( <>

{' '} Import Wallet

Enter a 12 word mnemonic below to import your wallet into this app. The app will reload and use the new mnemonic.
e.preventDefault()}> pasteFromClipboard(e)} style={{ cursor: 'pointer' }} />

) } export default WalletImport ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/bch-wallet/wallet-summary.js`: ```js /* This component displays a summary of the wallet. */ // Global npm libraries import React, { useCallback, useEffect, useState } from 'react' import { Container, Row, Col, Card } from 'react-bootstrap' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faWallet, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons' // import { Clipboard } from '@capacitor/clipboard' // Local libraries import './wallet-summary.css' import CopyOnClick from './copy-on-click' import { getPublicKey } from 'nostr-tools/pure' import { base58_to_binary as base58ToBinary } from 'base58-js' import { bytesToHex } from '@noble/hashes/utils' // already an installed dependency function WalletSummary (props) { // Props const appData = props.appData const bchWalletState = appData.bchWalletState console.log('wallet summary state: ', bchWalletState) // State const [blurredMnemonic, setBlurredMnemonic] = useState(true) const [blurredPrivateKey, setBlurredPrivateKey] = useState(true) const [blurredNostrPrivKey, setBlurredNostrPrivKey] = useState(true) const [nostrKeyPair, setNostrKeyPair] = useState({ privHex: '', pubHex: '' }) // Encapsulate component state into an object that can be passed to child functions const walletSummaryData = { blurredMnemonic, setBlurredMnemonic, blurredPrivateKey, setBlurredPrivateKey } // Eye icon state const eyeIcon = { mnemonic: blurredMnemonic ? faEyeSlash : faEye, privateKey: blurredPrivateKey ? faEyeSlash : faEye } // Toggle the state of blurring for the mnemonic const toggleMnemonicBlur = (inObj = {}) => { try { const { walletSummaryData } = inObj // toggle the state of blurring const blurredState = walletSummaryData.blurredMnemonic walletSummaryData.setBlurredMnemonic(!blurredState) } catch (error) { console.error('Error toggling mnemonic blur: ', error) } } // Toggle the state of blurring for the private key const togglePrivateKeyBlur = (inObj = {}) => { try { const { walletSummaryData } = inObj // toggle the state of blurring const blurredState = walletSummaryData.blurredPrivateKey walletSummaryData.setBlurredPrivateKey(!blurredState) } catch (error) { console.error('Error toggling private key blur: ', error) } } // Toggle the state of blurring for the private key const toggleNostrPrivateKeyBlur = (inObj = {}) => { try { setBlurredNostrPrivKey(!blurredNostrPrivKey) } catch (error) { console.error('Error toggling private key blur: ', error) } } const nostrKeyPairFromWIF = useCallback((WIF) => { if (!WIF) return // Extract the privaty key from the WIF, using this guide: // https://learnmeabitcoin.com/technical/keys/private-key/wif/ const wifBuf = base58ToBinary(WIF) const privBuf = wifBuf.slice(1, 33) // console.log('privBuf: ', privBuf) const privHex = bytesToHex(privBuf) console.log('BCH & Nostr private key (HEX format): ', privHex) const pubHex = getPublicKey(privBuf) console.log('nostrPubKey: ', pubHex) return { privHex, pubHex } }, []) useEffect(() => { setNostrKeyPair(nostrKeyPairFromWIF(bchWalletState.privateKey)) }, [nostrKeyPairFromWIF, bchWalletState]) return ( <>

{' '} My Wallet

Mnemonic: {bchWalletState.mnemonic} toggleMnemonicBlur({ walletSummaryData })} /> Private Key: {bchWalletState.privateKey} togglePrivateKeyBlur({ walletSummaryData })} /> Cash Address: {bchWalletState.cashAddress} SLP Address: {bchWalletState.slpAddress} Legacy Address: {bchWalletState.legacyAddress} HD Path: {bchWalletState.hdPath} Nostr Priv Key: {nostrKeyPair.privHex} toggleNostrPrivateKeyBlur(nostrKeyPair.privHex)} /> Nostr Pub Key: {nostrKeyPair.pubHex}
) } export default WalletSummary ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/bch-wallet/warning.js`: ```js /* This component is a visual warning against storing large sums of money in a web wallet. */ // Global npm libraries import React from 'react' import { Container, Row, Col, Card } from 'react-bootstrap' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faTriangleExclamation } from '@fortawesome/free-solid-svg-icons' const WebWalletWarning = () => { return ( <>

{' '} Web Wallets are Insecure

This is an open source, non-custodial web wallet supporting Bitcoin Cash (BCH) and SLP tokens. It is optimized for convenience and not security.
Do not store large amounts of money on a web wallet.

Note: Scammers frequently copy this open source code to build apps for stealing people's money. Be sure you trust the source serving you this app.
) } export default WebWalletWarning ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/bch-wallet/copy-on-click.js`: ```js /* This component is visually represented with a copy icon. A wallet property is passed as a prop. When clicked, the wallet property is copied to the system clipboard. */ // Global npm libraries import React, { useCallback, useState } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faCopy } from '@fortawesome/free-solid-svg-icons' const CopyOnClick = (props) => { // State const [iconVis, setIconVis] = useState(true) // Props const { appData, walletProp, value } = props // App Util const { appUtil } = appData // Function to copy the value to the clipboard. const handleCopyToClipboard = useCallback(async (event) => { appUtil.copyToClipboard(value) // hide icon in order to show the copied message setIconVis(false) // restart icon visibility after 1 second setTimeout(function () { setIconVis(true) }, 1000) }, [value, appUtil]) return ( <> {iconVis && ( handleCopyToClipboard(e)} style={{ cursor: 'pointer' }} /> )} {!iconVis && ( Copied! )} ) } export default CopyOnClick ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/sweep/index.js`: ```js /* This Sweep component allows users to sweep a private key and transfer any BCH or SLP tokens into their wallet. */ // Global npm libraries import React from 'react' import { Container, Row, Col, Form, Button, Modal, Spinner } from 'react-bootstrap' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faCircleQuestion } from '@fortawesome/free-solid-svg-icons' import Sweeper from 'bch-token-sweep' // let _this const SweepWif = (props) => { const { appData } = props console.log('appData', appData) const [wifToSweep, setWifToSweep] = React.useState('') const [showModal, setShowModal] = React.useState(false) const [statusMsg, setStatusMsg] = React.useState('') const [hideSpinner, setHideSpinner] = React.useState(false) // shouldRefreshOnModalClose: false // Helper function to validate WIF const validateWIF = (WIF) => { if (typeof WIF !== 'string') return false if (WIF.length !== 52) return false if (WIF[0] !== 'L' && WIF[0] !== 'K') return false return true } // Update wallet state function const updateWalletState = async () => { const wallet = appData.wallet const bchBalance = await wallet.getBalance({ bchAddress: wallet.walletInfo.cashAddress }) await wallet.initialize() const slpTokens = await wallet.listTokens(wallet.walletInfo.cashAddress) appData.updateBchWalletState({ walletObj: { bchBalance, slpTokens }, appData }) } // Handle sweep function const handleSweep = async (e) => { if (e) { e.preventDefault() } try { console.log(`Sweeping this WIF: ${wifToSweep}`) // Set modal initial state setShowModal(true) setHideSpinner(false) setStatusMsg('') // Input validation const isWIF = validateWIF(wifToSweep) if (!isWIF) { setHideSpinner(true) setStatusMsg(Input is not a WIF private key.) return } try { const walletWif = appData.wallet.walletInfo.privateKey const toAddr = appData.wallet.slpAddress // Instance the Sweep library const sweep = new Sweeper(wifToSweep, walletWif, appData.wallet) await sweep.populateObjectFromNetwork() // Constructing the sweep transaction const hex = await sweep.sweepTo(toAddr) const txid = await appData.wallet.ar.sendTx(hex) // Generate status message const newStatusMsg = ( <>

Sweep succeeded!

Transaction ID: {txid}

TX on Blockchair BCH Block Explorer

TX on token explorer

) setHideSpinner(true) setStatusMsg(newStatusMsg) setWifToSweep('') await updateWalletState() } catch (err) { setHideSpinner(true) setStatusMsg({`Error: ${err.message}`}) } } catch (err) { console.error('Error in handleSweep(): ', err) } } const handleCancelCounterOffers = async () => { try { console.log('Executing handleCancelCounterOffers()') // Set modal initial state // setShowModal(true) // setHideSpinner(false) // setStatusMsg('') // Get the keypair that holds Counter Offer UTXOs. const bchDexLib = appData.dexLib const keyPair = await bchDexLib.take.util.getKeyPair(1) const wif = keyPair.wif // console.log(`WIF: ${wif}`) // Sweep the private key holding the Counter Offer UTXOs. setWifToSweep(wif) await handleSweep() } catch (err) { console.error('Error in handleCancelCounterOffers(): ', err) } } // Modal component const getModal = () => ( setShowModal(false)}> Sweeping... {!hideSpinner && ( Sweeping private key... )}
{statusMsg && ( {statusMsg} )}
) return ( <>

This View is used to 'sweep' a private key. This will transfer any BCH or SLP tokens from a paper wallet to your web wallet. Paper wallets are used to store BCH and tokens. You can generate paper wallets here.

Paste the private key of a paper wallet below and click the button to sweep the funds. The private key must be in WIF format. It will start with the letter 'K' or 'L'.

setWifToSweep(e.target.value)} value={wifToSweep} />




When a Buy order is created, the coins (UTXO) to pay for it are moved to a secondary address. Clicking the button below will sweep those funds back into this main wallet. This will also cancel/invalidate all open Counter Offers that you've created.

{showModal && getModal()} ) } export default SweepWif ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/app-body/placeholder3.js`: ```js /* This is a placeholder View */ // Global npm libraries import React, { useEffect } from 'react' function Placeholder3 (props) { useEffect(() => { console.log('Placeholder 3 loaded.') }, []) return ( <>

This is placeholder View #3

) } export default Placeholder3 ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/starter-views.js`: ```js /** * This file contains the views that are displayed before and after the BCH wallet is initialized. */ import React from 'react' import WaitingModal from './waiting-modal' import AppBody from './app-body' // This is rendered *before* the BCH wallet is initialized. export function UninitializedView (props = {}) { // console.log('UninitializedView props: ', props) const { appData } = props const heading = 'Connecting to BCH blockchain...' return ( <> { appData.asyncInitFinished ? : null } ) } // This is rendered *after* the BCH wallet is initialized. export function InitializedView (props) { const { appData } = props return ( <>
) } ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/waiting-modal/index.js`: ```js /* This 'Waiting Modal' component displays a spinner animation and a status log. It's used to inform the user that the app is waiting for something, and to display progress. */ // Global npm libraries import React, { useState } from 'react' import { Container, Row, Col, Modal, Spinner } from 'react-bootstrap' function ModalTemplate (props) { // State const [show, setShow] = useState(true) // Dependency injection of props const denyClose = props.denyClose // Determins if user is allowed to close modal. const closeFunc = props.closeFunc // Optional function called after modal is closed. const heading = props.heading // Title of the modal const body = props.body // Body of the modal const hideSpinner = props.hideSpinner // Hide the animated spinner // This function is called when the modal is closed const handleClose = () => { console.log(`props.denyClose: ${denyClose}`) if (denyClose) return setShow(false) if (closeFunc) { closeFunc() } } // const handleShow = () => setShow(true) return ( {heading} {hideSpinner ? null : } ) } // This function populates the body of the modal. It expects props.body to be // an array of strings. function BodyList (props) { const items = props.body // console.log('BodyList items: ', items) const listItems = [] // Paragraphs for (let i = 0; i < items.length; i++) { listItems.push(

{items[i]}

) } return ( listItems ) } // export default WaitingModal export default ModalTemplate ``` `/home/trout/work/psf/code/bch-dex-taker-v2/src/components/load-scripts.js`: ```js /* Load