feat(wallet): Ported Wallet View

This commit is contained in:
Daniel Gonzalez
2025-01-28 19:38:44 -04:00
parent 6eae18c2a0
commit 6975534a4d
16 changed files with 957 additions and 46 deletions
+85 -4
View File
@@ -1,13 +1,17 @@
{
"name": "react-bootstrap-web3-spa",
"version": "2.0.1",
"name": "bch-wallet-web3-spa",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "react-bootstrap-web3-spa",
"version": "2.0.1",
"name": "bch-wallet-web3-spa",
"version": "1.0.0",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-regular-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.2",
"axios": "0.27.2",
"bch-message-lib": "2.2.1",
"bootstrap": "5.2.0",
@@ -16,6 +20,7 @@
"react-bootstrap": "2.10.7",
"react-dom": "19.0.0",
"react-scripts": "5.0.1",
"use-local-storage-state": "19.5.0",
"use-query-params": "1.2.3"
},
"devDependencies": {
@@ -2057,6 +2062,64 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-regular-svg-icons": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
"integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
"integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/react-fontawesome": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
"integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.8.1"
},
"peerDependencies": {
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
"react": ">=16.3"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.9.5",
"license": "Apache-2.0",
@@ -12792,6 +12855,8 @@
"node_modules/node-fetch": {
"name": "@achingbrain/node-fetch",
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-iTASGs+HTFK5E4ZqcMsHmeJ4zodyq8L38lZV33jwqcBJYoUt3HjN4+ot+O9/0b+ke8ddE7UgOtVuZN/OkV19/g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -17340,6 +17405,22 @@
"requires-port": "^1.0.0"
}
},
"node_modules/use-local-storage-state": {
"version": "19.5.0",
"resolved": "https://registry.npmjs.org/use-local-storage-state/-/use-local-storage-state-19.5.0.tgz",
"integrity": "sha512-sUJAyFvsmqMpBhdwaRr7GTKkkoxb6PWeNVvpBDrLuwQF1PpbJRKIbOYeLLeqJI7B3wdfFlLLCBbmOdopiSTBOw==",
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/astoilkov"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/use-query-params": {
"version": "1.2.3",
"license": "ISC",
+5
View File
@@ -2,6 +2,10 @@
"name": "bch-wallet-web3-spa",
"version": "1.0.0",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-regular-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.2",
"axios": "0.27.2",
"bch-message-lib": "2.2.1",
"bootstrap": "5.2.0",
@@ -10,6 +14,7 @@
"react-bootstrap": "2.10.7",
"react-dom": "19.0.0",
"react-scripts": "5.0.1",
"use-local-storage-state": "19.5.0",
"use-query-params": "1.2.3"
},
"scripts": {
+15 -1
View File
@@ -30,6 +30,7 @@ function App (props) {
return prevBody
})
}, [])
/** Load all required data before component start. */
useEffect(() => {
async function asyncEffect () {
@@ -55,8 +56,21 @@ function App (props) {
addToModal('Initializing wallet', appData)
console.log(`Initializing wallet with back end server ${appData.serverUrl}`)
const walletTemp = await asyncLoad.initWallet(appData.serverUrl)
const walletTemp = await asyncLoad.initWallet(appData.serverUrl, appData.lsState.mnemonic, appData)
appData.setWallet(walletTemp)
// appData.updateBchWalletState({ walletObj: walletTemp.walletInfo, appData })
// Get the BCH balance of the wallet.
addToModal('Getting BCH balance', appData)
await asyncLoad.getWalletBchBalance(walletTemp, appData.updateBchWalletState, appData)
// Get the SLP tokens held by the wallet.
addToModal('Getting SLP tokens', appData)
await asyncLoad.getSlpTokenBalances(walletTemp, appData.updateBchWalletState, appData)
// Get the BCH spot price
addToModal('Getting BCH spot price in USD', appData)
await asyncLoad.getUSDExchangeRate(walletTemp, appData.updateBchWalletState, appData)
// Update state
appData.setShowStartModal(false)
@@ -0,0 +1,59 @@
/*
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 (
<>
<Container>
<Row>
<Col>
<Card>
<Card.Body>
<Card.Title style={{ textAlign: 'center' }}>
<h2>
<FontAwesomeIcon icon={faTriangleExclamation} />{' '}
<span>Clear Local Storage</span>
</h2>
</Card.Title>
<Card.Text style={{ textAlign: 'center' }}>
Clicking the button below will clear the Local Storage, which
will reload the app with a newly created wallet.
<br />
<b>
Be sure to write down your 12-word mnemonic to back
up your wallet before clicking the button!
</b>.
<br /><br />
<Button variant='danger' onClick={handleClearLocalStorage}>
Delete Wallet
</Button>
</Card.Text>
</Card.Body>
</Card>
</Col>
</Row>
</Container>
</>
)
}
export default WalletClear
@@ -0,0 +1,55 @@
/*
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 && (
<FontAwesomeIcon
icon={faCopy} size='lg'
id={`${walletProp}-icon`}
onClick={(e) => handleCopyToClipboard(e)}
style={{ cursor: 'pointer' }}
/>
)}
{!iconVis && (
<span
id={`${walletProp}-word`}
style={{ color: 'green' }}
>
Copied!
</span>
)}
</>
)
}
export default CopyOnClick
@@ -0,0 +1,118 @@
/*
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 (
<>
<Container>
<Row>
<Col>
<Card>
<Card.Body>
<Card.Title style={{ textAlign: 'center' }}>
<h2>
<FontAwesomeIcon icon={faFileExport} />{' '}
<span>Import Wallet</span>
</h2>
</Card.Title>
<Card.Text style={{ textAlign: 'center' }}>
Enter a 12 word mnemonic below to import your wallet into
this app. The app will reload and use the new mnemonic.
</Card.Text>
<Container>
<Row>
<Col xs={12} className='text-break' style={{ textAlign: 'center' }}>
<Form>
<Form.Group className='mb-3' controlId='formImportWallet' style={{ display: 'flex', alignItems: 'center' }}>
<Form.Control
style={{ margin: '1rem' }}
type='text'
value={newMnemonic}
onChange={handleImportMnemonic}
/>
<FontAwesomeIcon
icon={faPaste}
size='lg'
onClick={(e) => pasteFromClipboard(e)}
style={{ cursor: 'pointer' }}
/>
</Form.Group>
</Form>
</Col>
</Row>
<Row>
<Col style={{ textAlign: 'center' }}>
<Button variant='primary' onClick={handleImportWallet}>
Import
</Button>
</Col>
</Row>
<br />
</Container>
</Card.Body>
</Card>
</Col>
</Row>
</Container>
</>
)
}
export default WalletImport
@@ -0,0 +1,47 @@
/*
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 (
<>
<Container>
<Row>
<Col style={{ textAlign: 'right' }}>
<a href='https://youtu.be/0R00cppN0fA' target='_blank' rel='noreferrer'>
<FontAwesomeIcon icon={faCircleQuestion} size='lg' />
</a>
</Col>
</Row>
</Container>
<WebWalletWarning />
<br />
<WalletSummary appData={appData} />
<br />
<WalletClear appData={appData} />
<br />
<WalletImport appData={appData} />
<br />
<OptimizeWallet appData={appData} />
</>
)
}
export default BchWallet
@@ -0,0 +1,113 @@
/*
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(<a href='https://youtu.be/mRniqpgWdjg' target='_blank' rel='noreferrer'>Video: How to Store SLP Tokens on a Paper Wallet</a>)
newModalBody.push(<a href='https://paperwallet.fullstack.cash/' target='_blank' rel='noreferrer'>Generate a Paper Wallet</a>)
setModalBody(newModalBody)
}
} catch (err) {
console.log('Error while trying to count total number of UTXOs: ', err)
}
}
return (
<>
<Container>
<Row>
<Col>
<Card>
<Card.Body>
<Card.Title style={{ textAlign: 'center' }}>
<h2>Optimize Wallet</h2>
</Card.Title>
<Card.Text style={{ textAlign: 'center' }}>
Clicking the button below will optimize your wallet and make it
function faster.
<br /><br />
<i>How it works</i>: 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.
<br /><br />
<Button variant='primary' onClick={handleOptimize}>
Optimize Wallet
</Button>
</Card.Text>
</Card.Body>
</Card>
</Col>
</Row>
</Container>
{showModal && (
<WaitingModal
heading='Optimizing Wallet'
body={modalBody}
hideSpinner={hideSpinner}
denyClose={denyClose}
/>
)}
</>
)
}
export default OptimizeWallet
@@ -0,0 +1,4 @@
.blurred {
filter: blur(6px);
-webkit-filter: blur(6px);
}
@@ -0,0 +1,165 @@
/*
This component displays a summary of the wallet.
*/
// Global npm libraries
import React, { 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'
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)
// 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)
}
}
return (
<>
<Container>
<Row>
<Col>
<Card>
<Card.Body>
<Card.Title style={{ textAlign: 'center' }}>
<h2>
<FontAwesomeIcon icon={faWallet} />{' '}
<span>My Wallet</span>
</h2>
</Card.Title>
<Container>
<Row style={{ padding: '25px' }}>
<Col xs={12} sm={10} lg={8} style={{ padding: '10px' }}>
<b>Mnemonic:</b> <span className={blurredMnemonic ? 'blurred' : null}>{bchWalletState.mnemonic}</span>
</Col>
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
<FontAwesomeIcon
style={{ cursor: 'pointer' }}
icon={eyeIcon.mnemonic}
size='lg'
onClick={() => toggleMnemonicBlur({ walletSummaryData })}
/>
</Col>
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
<CopyOnClick walletProp='mnemonic' appData={appData} value={bchWalletState.mnemonic} />
</Col>
</Row>
<Row style={{ padding: '25px', backgroundColor: '#eee' }}>
<Col xs={12} sm={10} lg={8} style={{ padding: '10px' }}>
<b>Private Key:</b> <span className={blurredPrivateKey ? 'blurred' : null}>{bchWalletState.privateKey}</span>
</Col>
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
<FontAwesomeIcon
style={{ cursor: 'pointer' }}
icon={eyeIcon.privateKey}
size='lg'
onClick={() => togglePrivateKeyBlur({ walletSummaryData })}
/>
</Col>
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
<CopyOnClick walletProp='privateKey' appData={appData} value={bchWalletState.privateKey} />
</Col>
</Row>
<Row style={{ padding: '25px' }}>
<Col xs={12} sm={10} lg={8} style={{ padding: '10px' }}>
<b>Cash Address:</b> {bchWalletState.cashAddress}
</Col>
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }} />
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
<CopyOnClick walletProp='cashAddress' appData={appData} value={bchWalletState.cashAddress} />
</Col>
</Row>
<Row style={{ padding: '25px', backgroundColor: '#eee' }}>
<Col xs={12} sm={10} lg={8} style={{ padding: '10px' }}>
<b>SLP Address:</b> {bchWalletState.slpAddress}
</Col>
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }} />
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
<CopyOnClick walletProp='slpAddress' appData={appData} value={bchWalletState.slpAddress} />
</Col>
</Row>
<Row style={{ padding: '25px' }}>
<Col xs={12} sm={10} lg={8} style={{ padding: '10px' }}>
<b>Legacy Address:</b> {bchWalletState.legacyAddress}
</Col>
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }} />
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
<CopyOnClick walletProp='legacyAddress' appData={appData} value={bchWalletState.legacyAddress} />
</Col>
</Row>
<Row style={{ padding: '25px', backgroundColor: '#eee' }}>
<Col xs={10} sm={10} lg={8} style={{ padding: '10px' }}>
<b>HD Path:</b> {bchWalletState.hdPath}
</Col>
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }} />
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
<CopyOnClick walletProp='hdPath' appData={appData} value={bchWalletState.hdPath} />
</Col>
</Row>
</Container>
</Card.Body>
</Card>
</Col>
</Row>
</Container>
</>
)
}
export default WalletSummary
@@ -0,0 +1,49 @@
/*
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 (
<>
<Container>
<Row>
<Col>
<Card>
<Card.Body>
<Card.Title style={{ textAlign: 'center' }}>
<h2>
<FontAwesomeIcon icon={faTriangleExclamation} />{' '}
<span>Web Wallets are Insecure</span>
</h2>
</Card.Title>
<Card.Text style={{ textAlign: 'center' }}>
This is an open source, non-custodial web wallet
supporting Bitcoin Cash (BCH) and SLP tokens.
It is optimized for convenience and not security.
<br />
<b>Do not store large amounts of money on a web wallet.
</b>
<br /><br />
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.
</Card.Text>
</Card.Body>
</Card>
</Col>
</Row>
</Container>
</>
)
}
export default WebWalletWarning
+5 -2
View File
@@ -11,6 +11,7 @@ import React from 'react'
// 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'
@@ -28,11 +29,13 @@ function AppBody (props) {
case 0:
return (<GetBalance wallet={appData.wallet} />)
case 1:
return (<Placeholder2 />)
return (<Wallet appData={appData} />)
case 2:
return (<Placeholder2 />)
case 3:
return (<Placeholder3 />)
// Special Views
// Special Views
case 100:
return (<ServerSelectView appData={appData} />)
default:
+7
View File
@@ -35,6 +35,13 @@ function NavMenu (props) {
>
Check Balance
</Nav.Link>
<Nav.Link
className={selectedMenu === 1 ? 'nav-link-active' : 'nav-link-inactive'}
href='#'
onClick={(e) => handleClickEvent(1)}
>
Wallet
</Nav.Link>
<Nav.Link
className={selectedMenu === 1 ? 'nav-link-active' : 'nav-link-inactive'}
href='#'
+70 -1
View File
@@ -1,5 +1,7 @@
import { useState } from 'react'
import { useQueryParam, StringParam } from 'use-query-params'
import useLocalStorageState from 'use-local-storage-state'
import AppUtil from '../util'
function useAppState () {
// Get the CashStack URL from query parameter or use default
@@ -22,6 +24,65 @@ function useAppState () {
const [hideSpinner, setHideSpinner] = useState(false)
const [denyClose, setDenyClose] = useState(false)
// The wallet state makes this a true progressive web app (PWA). As
// balances, UTXOs, and tokens are retrieved, this state is updated.
// properties are enumerated here for the purpose of documentation.
// Local storage
const [lsState, setLSState, { removeItem }] = useLocalStorageState('bchWalletState', {
ssr: true,
defaultValue: {}
})
console.log('lsState: ', lsState)
const removeLocalStorageItem = removeItem
const updateLocalStorage = (lsObj) => {
// console.log(`updateLocalStorage() input: ${JSON.stringify(lsObj, null, 2)}`)
// Progressively overwrite the LocalStorage state.
const newObj = Object.assign({}, lsState, lsObj)
// console.log(`updateLocalStorage() output: ${JSON.stringify(newObj, null, 2)}`)
setLSState(newObj)
}
const [bchWalletState, setBchWalletState] = useState({
mnemonic: undefined,
address: undefined,
cashAddress: undefined,
slpAddress: undefined,
privateKey: undefined,
publicKey: undefined,
legacyAddress: undefined,
hdPath: undefined,
bchBalance: 0,
slpTokens: [],
bchUsdPrice: 150
})
// This function is passed to child components in order to update the wallet
// state. This function is important to make this wallet a PWA.
function updateBchWalletState (inObj = {}) {
try {
const { walletObj, appData } = inObj
// Debuging
// console.log('updateBchWalletState() walletObj: ', walletObj)
// console.log('updateBchWalletState() appData: ', appData)
appData.setBchWalletState(oldState => {
const newBchWalletState = Object.assign({}, oldState, walletObj)
// console.log('newBchWalletState: ', newBchWalletState)
return newBchWalletState
})
// console.log(`New wallet state: ${JSON.stringify(bchWalletState, null, 2)}`)
} catch (err) {
console.error('Error in App.js updateBchWalletState()')
throw err
}
}
return {
serverUrl,
setServerUrl,
@@ -44,7 +105,15 @@ function useAppState () {
hideSpinner,
setHideSpinner,
denyClose,
setDenyClose
setDenyClose,
bchWalletState,
setBchWalletState,
lsState,
setLSState,
removeLocalStorageItem,
updateLocalStorage,
updateBchWalletState,
appUtil: new AppUtil()
}
}
+136 -38
View File
@@ -16,73 +16,171 @@ class AsyncLoad {
// Load the minimal-slp-wallet which comes in as a <script> file and is
// attached to the global 'window' object.
async loadWalletLib () {
do {
// console.log('window.SlpWallet: ', window.SlpWallet)
if (typeof window !== 'undefined' && window.SlpWallet) {
this.BchWallet = window.SlpWallet
try {
do {
if (typeof window !== 'undefined' && window.SlpWallet) {
this.BchWallet = window.SlpWallet
return this.BchWallet
} else {
console.log('Waiting for wallet library to load...')
}
return this.BchWallet
} else {
console.log('Waiting for wallet library to load...')
}
await sleep(1000)
} while (!this.BchWallet)
await sleep(1000)
} while (!this.BchWallet)
} catch (error) {
console.error('Error loading wallet library: ', error)
throw error
}
}
// Initialize the BCH wallet
async initWallet (restURL) {
const options = {
interface: 'consumer-api',
restURL,
noUpdate: true
async initWallet (restURL, mnemonic, appData) {
try {
const options = {
interface: 'consumer-api',
restURL,
noUpdate: true
}
let wallet
if (mnemonic) {
// Load the wallet from the mnemonic, if it's available from local storage.
wallet = new this.BchWallet(mnemonic, options)
} else {
// Generate a new mnemonic and wallet.
wallet = new this.BchWallet(null, options)
}
// Wait for wallet to initialize.
await wallet.walletInfoPromise
await wallet.initialize()
console.log('starting to update wallet state.')
// Update the state of the wallet.
appData.updateBchWalletState({ walletObj: wallet.walletInfo, appData })
console.log('finished updating wallet state.')
// Save the mnemonic to local storage.
if (!mnemonic) {
const newMnemonic = wallet.walletInfo.mnemonic
appData.updateLocalStorage({ mnemonic: newMnemonic })
}
this.wallet = wallet
return wallet
} catch (error) {
console.error('Error initializing wallet: ', error)
throw error
}
}
const wallet = new this.BchWallet(null, options)
// Get the BCH balance of the wallet.
async getWalletBchBalance (wallet, updateBchWalletState, appData) {
try {
// Get the BCH balance of the wallet.
const bchBalance = await wallet.getBalance({ bchAddress: wallet.walletInfo.cashAddress })
await wallet.walletInfoPromise
// console.log(`mnemonic: ${wallet.walletInfo.mnemonic}`)
// Update the state of the wallet with the balances
updateBchWalletState({ walletObj: { bchBalance }, appData })
this.wallet = wallet
return true
} catch (error) {
console.error('Error getting wallet BCH balance: ', error)
throw error
}
}
return wallet
// Get the spot exchange rate for BCH in USD.
async getUSDExchangeRate (wallet, updateBchWalletState, appData) {
try {
const bchUsdPrice = await wallet.getUsd()
// Update the state of the wallet
updateBchWalletState({ walletObj: { bchUsdPrice }, appData })
return true
} catch (error) {
console.error('Error getting USD exchange rate: ', error)
throw error
}
}
// Get a list of SLP tokens held by the wallet.
async getSlpTokenBalances (wallet, updateBchWalletState, appData) {
try {
// Get token information from the wallet. This will also initialize the UTXO store.
const slpTokens = await wallet.listTokens(wallet.walletInfo.cashAddress)
// console.log('slpTokens: ', slpTokens)
// Add an icon property to each token.
slpTokens.map(x => {
x.icon = ''// (<Jdenticon size='100' value={x.tokenId} />)
x.iconNeedsDownload = true
return true
})
console.log('slpTokens: ', slpTokens)
// Update the state of the wallet with the balances
updateBchWalletState({ walletObj: { slpTokens }, appData })
return true
} catch (error) {
console.error('Error getting SLP tokens', error)
throw error
}
}
// Get token data for a given Token ID
async getTokenData (tokenId) {
const tokenData = await this.wallet.getTokenData(tokenId)
try {
const tokenData = await this.wallet.getTokenData(tokenId)
// Convert the IPFS CIDs into actual data.
tokenData.immutableData = await this.getIpfsData(tokenData.immutableData)
tokenData.mutableData = await this.getIpfsData(tokenData.mutableData)
// Convert the IPFS CIDs into actual data.
tokenData.immutableData = await this.getIpfsData(tokenData.immutableData)
tokenData.mutableData = await this.getIpfsData(tokenData.mutableData)
return tokenData
return tokenData
} catch (error) {
console.error('Error getting Token data', error)
throw error
}
}
// Get data about a Group token
async getGroupData (tokenId) {
const tokenData = await this.getTokenData(tokenId)
try {
const tokenData = await this.getTokenData(tokenId)
const groupData = {
immutableData: tokenData.immutableData,
mutableData: tokenData.mutableData,
nfts: tokenData.genesisData.nfts,
tokenId: tokenData.genesisData.tokenId
const groupData = {
immutableData: tokenData.immutableData,
mutableData: tokenData.mutableData,
nfts: tokenData.genesisData.nfts,
tokenId: tokenData.genesisData.tokenId
}
return groupData
} catch (error) {
console.error('Error getting Token group data', error)
throw error
}
return groupData
}
// Given an IPFS URI, this will download and parse the JSON data.
async getIpfsData (ipfsUri) {
const cid = ipfsUri.slice(7)
try {
const cid = ipfsUri.slice(7)
const downloadUrl = `https://${cid}.ipfs.dweb.link/data.json`
const downloadUrl = `https://${cid}.ipfs.dweb.link/data.json`
const response = await axios.get(downloadUrl)
const data = response.data
const response = await axios.get(downloadUrl)
const data = response.data
return data
return data
} catch (error) {
console.error('Error getting IPFS data', error)
throw error
}
}
// Get a list of alternative back end servers.
+24
View File
@@ -8,6 +8,30 @@ class AppUtil {
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