mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo-client.git
synced 2026-09-21 16:52:02 -07:00
feat(tokens): Ported slp-token View
This commit is contained in:
@@ -18,6 +18,7 @@ import Placeholder3 from './placeholder3'
|
||||
import ServerSelectView from './servers/select-server-view'
|
||||
import SelectServerButton from './servers/select-server-button'
|
||||
import BchSend from './bch-send'
|
||||
import SlpTokens from './slp-tokens'
|
||||
|
||||
function AppBody (props) {
|
||||
// Dependency injection through props
|
||||
@@ -30,6 +31,7 @@ function AppBody (props) {
|
||||
<Route path='/balance' element={<GetBalance wallet={appData.wallet} />} />
|
||||
<Route path='/bch' element={<BchSend appData={appData} />} />
|
||||
<Route path='/wallet' element={<Wallet appData={appData} />} />
|
||||
<Route path='/slp-tokens' element={<SlpTokens appData={appData} />} />
|
||||
<Route path='/placeholder2' element={<Placeholder2 />} />
|
||||
<Route path='/placeholder3' element={<Placeholder3 />} />
|
||||
<Route path='/servers' element={<ServerSelectView appData={appData} />} />
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
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 => (
|
||||
<TokenCard
|
||||
appData={appData}
|
||||
token={thisToken}
|
||||
key={`${thisToken.tokenId}`}
|
||||
refreshTokens={refreshTokens}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<RefreshTokenBalance
|
||||
appData={appData}
|
||||
ref={refreshTokenButtonRef}
|
||||
lazyLoadTokenIcons={lazyLoadTokenIcons}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={6} style={{ textAlign: 'right' }}>
|
||||
<a href='https://youtu.be/f1no5-QHTr4' target='_blank' rel='noreferrer'>
|
||||
<FontAwesomeIcon icon={faCircleQuestion} size='lg' />
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={12} style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
{
|
||||
!iconsAreLoaded && (
|
||||
<div style={{ borderRadius: '10px', backgroundColor: '#f0f0f0', padding: '10px', display: 'flex', justifyContent: 'center', alignItems: 'center', width: 'fit-content' }}>
|
||||
<span style={{ marginRight: '10px' }}>Loading Token Icons </span>
|
||||
<Spinner animation='border' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
<Row>
|
||||
{generateCards()}
|
||||
</Row>
|
||||
{/** Display a message if no tokens are found */}
|
||||
{tokens.length === 0 && (
|
||||
<Row className='text-center'>
|
||||
<span> No tokens found in wallet </span>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SlpTokens
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
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 = (<a href={url} target='_blank' rel='noreferrer'>{url}</a>)
|
||||
|
||||
//
|
||||
} else if (url.includes('ipfs://')) {
|
||||
// Convert to a Filecoin link if its an IPFS reference.
|
||||
|
||||
const cid = url.substring(7)
|
||||
url = (<a href={`https://${cid}.ipfs.dweb.link/data.json`} target='_blank' rel='noreferrer'>{url}</a>)
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Button variant='info' onClick={handleOpen}>Info</Button>
|
||||
<Modal show={show} onHide={handleClose}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Token Information</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col xs={4}><b>Ticker</b>:</Col>
|
||||
<Col xs={8}>{props.token.ticker}</Col>
|
||||
</Row>
|
||||
|
||||
<Row style={{ backgroundColor: '#eee' }}>
|
||||
<Col xs={4}><b>Name</b>:</Col>
|
||||
<Col xs={8}>{props.token.name}</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={4}><b>Token ID</b>:</Col>
|
||||
<Col xs={8} style={{ wordBreak: 'break-all' }}>
|
||||
<a href={`https://token.fullstack.cash/?tokenid=${props.token.tokenId}`} target='_blank' rel='noreferrer'>
|
||||
{props.token.tokenId}
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row style={{ backgroundColor: '#eee' }}>
|
||||
<Col xs={4}><b>Decimals</b>:</Col>
|
||||
<Col xs={8}>{props.token.decimals}</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={4}><b>Token Type</b>:</Col>
|
||||
<Col xs={8}>{props.token.tokenType}</Col>
|
||||
</Row>
|
||||
|
||||
<Row style={{ backgroundColor: '#eee', wordBreak: 'break-all' }}>
|
||||
<Col xs={4}><b>URL</b>:</Col>
|
||||
<Col xs={8}>{url}</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Modal.Body>
|
||||
<Modal.Footer />
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default InfoButton
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
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 (
|
||||
<>
|
||||
<Button variant='success' onClick={handleRefreshTokenBalance}>
|
||||
<FontAwesomeIcon icon={faRedo} size='lg' /> Refresh
|
||||
</Button>
|
||||
|
||||
{!hideWaitingModal && (
|
||||
<WaitingModal
|
||||
heading='Refreshing Token List'
|
||||
body={modalBody}
|
||||
hideSpinner={hideSpinner}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RefreshTokenBalance
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
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 () => {
|
||||
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(<p>Success! <a href={`https://token.fullstack.cash/transactions/?txid=${txid}`} target='_blank' rel='noreferrer'>See on Block Explorer</a></p>)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Modal JSX
|
||||
const getModal = () => {
|
||||
return (
|
||||
<Modal show={showModal} size='lg' onHide={handleCloseModal}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title><FontAwesomeIcon icon={faPaperPlane} size='lg' /> Send Tokens: <span style={{ color: 'red' }}>{token.ticker}</span></Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Container>
|
||||
{/* ... existing Modal.Body content ... */}
|
||||
<Row>
|
||||
<Col style={{ textAlign: 'center' }}>
|
||||
<b>SLP Address:</b>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={10}>
|
||||
<Form>
|
||||
<Form.Group controlId='formBasicEmail' style={{ textAlign: 'center' }}>
|
||||
<Form.Control
|
||||
type='text'
|
||||
placeholder='simpleledger:qqlrzp23w08434twmvr4fxw672whkjy0pyxpgpyg0n'
|
||||
onChange={handleUpdateSendToAddr}
|
||||
value={sendToAddress}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Col>
|
||||
|
||||
<Col xs={2}>
|
||||
<FontAwesomeIcon
|
||||
icon={faPaste}
|
||||
size='lg'
|
||||
onClick={() => { /** paste from clipboard */ }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<Row>
|
||||
<Col style={{ textAlign: 'center' }}>
|
||||
<b>Amount:</b>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={10}>
|
||||
<Form style={{ paddingBottom: '10px' }}>
|
||||
<Form.Group controlId='formBasicEmail' style={{ textAlign: 'center' }}>
|
||||
<Form.Control
|
||||
type='text'
|
||||
onChange={e => setSendQtyStr(e.target.value)}
|
||||
value={sendQtyStr}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Col>
|
||||
|
||||
<Col xs={2}>
|
||||
<Button onClick={handleGetMax}>Max</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
<Row>
|
||||
<Col style={{ textAlign: 'center' }}>
|
||||
<Button onClick={handleSendTokens}>Send</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
{showAddrWarning && (
|
||||
<>
|
||||
<Row>
|
||||
<Col style={{ textAlign: 'center' }}>
|
||||
<p style={{ color: 'orange' }}>
|
||||
<b>Warning</b>: 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.
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
<Row>
|
||||
<Col xs={10}>
|
||||
{statusMsg}
|
||||
</Col>
|
||||
|
||||
<Col xs={2}>
|
||||
{!hideSpinner && <Spinner animation='border' />}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</Container>
|
||||
</Modal.Body>
|
||||
<Modal.Footer />
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant='info' onClick={handleShowModal}>Send</Button>
|
||||
{showModal && getModal()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SendTokenButton
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
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 (
|
||||
<>
|
||||
<Col xs={12} sm={6} lg={4} style={{ padding: '25px' }}>
|
||||
<Card>
|
||||
<Card.Body style={{ textAlign: 'center' }}>
|
||||
{/** If the icon is loaded, display it */
|
||||
icon && (
|
||||
<Card.Img
|
||||
src={icon}
|
||||
style={{ height: '100px', width: 'auto' }}
|
||||
onError={(e) => {
|
||||
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 && (
|
||||
<Jdenticon size='100' value={token.tokenId} />
|
||||
)
|
||||
}
|
||||
<Card.Title style={{ textAlign: 'center' }}>
|
||||
<h4>{props.token.ticker}</h4>
|
||||
</Card.Title>
|
||||
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
{props.token.name}
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
<Row>
|
||||
<Col>Balance:</Col>
|
||||
<Col>{props.token.qty}</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
<InfoButton token={props.token} />
|
||||
</Col>
|
||||
<Col>
|
||||
<SendTokenButton
|
||||
token={props.token}
|
||||
appData={props.appData}
|
||||
refreshTokens={props.refreshTokens}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TokenCard
|
||||
@@ -59,10 +59,17 @@ function NavMenu (props) {
|
||||
>
|
||||
Wallet
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className={currentPath === '/slp-tokens' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/slp-tokens'
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
Tokens
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className={currentPath === '/Placeholder2' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/Placeholder2'
|
||||
onClick={(e) => handleClickEvent(1)}
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
Placeholder2
|
||||
</NavLink>
|
||||
|
||||
@@ -4,6 +4,7 @@ import useLocalStorageState from 'use-local-storage-state'
|
||||
import AppUtil from '../util'
|
||||
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
function useAppState () {
|
||||
const location = useLocation()
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
// Global npm libraries
|
||||
import axios from 'axios'
|
||||
|
||||
// Local libraries
|
||||
import GistServers from './gist-servers'
|
||||
|
||||
@@ -112,13 +111,6 @@ class AsyncLoad {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user