mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex-taker-v2.git
synced 2026-09-21 16:52:01 -07:00
Merge branch 'master' into ct-unstable
This commit is contained in:
Generated
+15403
-18541
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@
|
||||
"query-string": "7.1.1",
|
||||
"react": "19.0.0",
|
||||
"react-bootstrap": "2.10.7",
|
||||
"react-bs-datatable": "3.15.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-router-dom": "7.1.3",
|
||||
|
||||
@@ -27,6 +27,7 @@ import NostrPost from './nostr/nostr-post/index.js'
|
||||
import NostrRead from './nostr/nostr-read/index.js'
|
||||
import GlobalFeed from './nostr/global-feed/index.js'
|
||||
import UserDataReview from './user-data-review'
|
||||
import Offers from './offers'
|
||||
function AppBody (props) {
|
||||
// Dependency injection through props
|
||||
const appData = props.appData
|
||||
@@ -50,6 +51,7 @@ function AppBody (props) {
|
||||
<Route path='/nostr-read' element={<NostrRead appData={appData} />} />
|
||||
<Route path='/global-feed' element={<GlobalFeed appData={appData} />} />
|
||||
<Route path='/user-data/:cid' element={<UserDataReview appData={appData} />} />
|
||||
<Route path='/offers' element={<Offers appData={appData} />} />
|
||||
</Routes>
|
||||
{/** Show in all paths except the servers view */}
|
||||
{/* {appData.currentPath !== '/servers' && <SelectServerButton linkTo='/servers' appData={appData} />} */}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
This React components downloads the active Offers from the REST API and
|
||||
displays them in a data table.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { Container, Row, Col, Table, Button, Spinner } from 'react-bootstrap'
|
||||
import axios from 'axios'
|
||||
import { DatatableWrapper, TableBody, TableHeader } from 'react-bs-datatable'
|
||||
|
||||
// Local libraries
|
||||
import config from '../../../config'
|
||||
import WaitingModal from '../../waiting-modal'
|
||||
|
||||
// Global variables and constants
|
||||
const SERVER = `${config.dexServer}/`
|
||||
|
||||
const TABLE_HEADERS = [
|
||||
{
|
||||
prop: 'ticker',
|
||||
title: 'Ticker',
|
||||
isFilterable: true
|
||||
},
|
||||
{
|
||||
prop: 'tokenId',
|
||||
title: 'Token ID'
|
||||
},
|
||||
{
|
||||
prop: 'buyOrSell',
|
||||
title: 'Type'
|
||||
},
|
||||
{
|
||||
prop: 'smallNostrEventId',
|
||||
title: 'Event ID'
|
||||
},
|
||||
{
|
||||
prop: 'numTokens',
|
||||
title: 'Quantity'
|
||||
},
|
||||
{
|
||||
prop: 'usdPrice',
|
||||
title: 'Price (USD)'
|
||||
},
|
||||
{
|
||||
prop: 'button',
|
||||
title: 'Action'
|
||||
}
|
||||
]
|
||||
|
||||
function Offers (props) {
|
||||
const [appData] = useState(props.appData)
|
||||
const [offers, setOffers] = useState([])
|
||||
|
||||
// Modal state
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [modalBody, setModalBody] = useState([])
|
||||
const [hideSpinner, setHideSpinner] = useState(false)
|
||||
const [denyClose, setDenyClose] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// Given a large string, it will return a string with the first and last
|
||||
// four characters.
|
||||
const cutString = (str) => {
|
||||
try {
|
||||
const subTxid = str.slice(0, 4)
|
||||
const subTxid2 = str.slice(-4)
|
||||
return `${subTxid}...${subTxid2}`
|
||||
} catch (err) {
|
||||
console.warn('Error in cutString() ', err)
|
||||
}
|
||||
}
|
||||
|
||||
// REST request to get data from avax-dex
|
||||
const getOffers = async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${SERVER}offer/list/fungible/0`,
|
||||
data: {}
|
||||
}
|
||||
const result = await axios.request(options)
|
||||
return result.data
|
||||
} catch (err) {
|
||||
console.warn('Error in getOffers() ', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get Offer data and manipulate it for the sake of presentation.
|
||||
const handleOffers = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
// Get raw offer data.
|
||||
const offerRawData = await getOffers()
|
||||
console.log('offerRawData: ', offerRawData)
|
||||
if (!offerRawData || offerRawData.length === 0) {
|
||||
setIsLoading(false)
|
||||
setOffers([])
|
||||
return
|
||||
}
|
||||
// Formatted Data
|
||||
const formattedOffers = []
|
||||
|
||||
for (let i = 0; i < offerRawData.length; i++) {
|
||||
const thisOffer = offerRawData[i]
|
||||
|
||||
// Get and format the token ID
|
||||
const tokenId = thisOffer.tokenId
|
||||
const smallTokenId = cutString(tokenId)
|
||||
thisOffer.tokenId = (<a href={`https://token.fullstack.cash/?tokenid=${tokenId}`} target='_blank' rel='noreferrer'>{smallTokenId}</a>)
|
||||
|
||||
// Get and format the P2WDB ID
|
||||
const nostrEventId = thisOffer.nostrEventId
|
||||
const smallNostrEventId = cutString(nostrEventId)
|
||||
thisOffer.smallNostrEventId = smallNostrEventId
|
||||
thisOffer.button = (<Button text='Buy' variant='success' size='lg' id={nostrEventId} onClick={handleBuy}>Buy</Button>)
|
||||
|
||||
// thisOffer.p2wdbHash = (<a href={`https://p2wdb.fullstack.cash/entry/hash/${p2wdbHash}`} target='_blank' rel='noreferrer'>{smallP2wdbHash}</a>)
|
||||
|
||||
// Convert sats to BCH, and then calculate cost in USD.
|
||||
const bchjs = appData.wallet.bchjs
|
||||
const rateInSats = parseInt(thisOffer.rateInBaseUnit)
|
||||
const bchCost = bchjs.BitcoinCash.toBitcoinCash(rateInSats)
|
||||
const usdPrice = bchCost * appData.bchWalletState.bchUsdPrice
|
||||
const priceStr = `$${usdPrice.toFixed(3)}`
|
||||
thisOffer.usdPrice = priceStr
|
||||
|
||||
formattedOffers.push(thisOffer)
|
||||
}
|
||||
|
||||
setOffers(formattedOffers)
|
||||
setIsLoading(false)
|
||||
}, [appData])
|
||||
|
||||
const handleBuy = async (event) => {
|
||||
try {
|
||||
console.log('Buy button clicked. Event: ', event)
|
||||
|
||||
const targetOfferEventId = event.target.id
|
||||
console.log('targetOfferEventId: ', targetOfferEventId)
|
||||
|
||||
// Initialize modal
|
||||
setShowModal(true)
|
||||
setModalBody(['Generating Counter Offer...', '(This can take a couple minutes)'])
|
||||
setHideSpinner(false)
|
||||
setDenyClose(true)
|
||||
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${SERVER}offer/take`,
|
||||
data: {
|
||||
nostrEventId: targetOfferEventId
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios.request(options)
|
||||
const { eventId, noteId } = result.data
|
||||
console.log(`Event Id : ${eventId}`)
|
||||
console.log(`Note Id ${noteId}`)
|
||||
|
||||
// Add link to output
|
||||
const newModalBody = []
|
||||
newModalBody.push('Success!')
|
||||
// newModalBody.push(<a href={`https://p2wdb.fullstack.cash/entry/hash/${p2wdbHash}`} target='_blank' rel='noreferrer'>P2WDB Entry</a>)
|
||||
|
||||
newModalBody.push('What happens next:')
|
||||
newModalBody.push('The money has not yet left your wallet! It is still under your control.')
|
||||
newModalBody.push('If the sellers node is online, they will accept the Counter Offer you just generated in a few minutes.')
|
||||
newModalBody.push('If the tokens never show up, you can sweep the funds back into your wallet.')
|
||||
|
||||
setModalBody(newModalBody)
|
||||
setHideSpinner(true)
|
||||
setDenyClose(false)
|
||||
} catch (error) {
|
||||
console.warn('Error in handleBuy() ', error)
|
||||
setModalBody(['Buy failed: ', error.message])
|
||||
setHideSpinner(true)
|
||||
setDenyClose(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Retrieve initial offer data
|
||||
handleOffers()
|
||||
|
||||
// Get data and update the table periodically.
|
||||
const interval = setInterval(() => {
|
||||
handleOffers()
|
||||
}, 30000)
|
||||
|
||||
// Cleanup interval on component unmount
|
||||
return () => clearInterval(interval)
|
||||
}, [handleOffers]) // Empty dependency array means this effect runs once on mount
|
||||
|
||||
const heading = 'Generating Counter Offer...'
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModal && (
|
||||
<WaitingModal
|
||||
heading={heading}
|
||||
body={modalBody}
|
||||
hideSpinner={hideSpinner}
|
||||
denyClose={denyClose}
|
||||
/>
|
||||
)}
|
||||
<Container>
|
||||
<Row>
|
||||
<Col className='text-break' style={{ textAlign: 'center' }}>
|
||||
{!isLoading && (
|
||||
<DatatableWrapper body={offers} headers={TABLE_HEADERS}>
|
||||
<Table>
|
||||
<TableHeader />
|
||||
<TableBody />
|
||||
</Table>
|
||||
</DatatableWrapper>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
{isLoading && (
|
||||
<Row>
|
||||
<Col className='text-break' style={{ textAlign: 'center' }}>
|
||||
<Spinner animation='border' />
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Offers
|
||||
@@ -43,9 +43,43 @@ function NavMenu (props) {
|
||||
to='/nfts-for-sale'
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
NFTs for Sale
|
||||
NFTs
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
className={currentPath === '/offers' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/offers'
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
Fungible Tokens
|
||||
</NavLink>
|
||||
|
||||
<hr />
|
||||
|
||||
<NavLink
|
||||
className={currentPath === '/global-feed' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/global-feed'
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
Global Feed
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className={currentPath === '/nostr-post' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/nostr-post'
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
Nostr Post
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className={currentPath === '/nostr-read' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/nostr-read'
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
Nostr Profile
|
||||
</NavLink>
|
||||
|
||||
<hr />
|
||||
|
||||
<NavLink
|
||||
className={(currentPath === '/bch') ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/bch'
|
||||
@@ -100,27 +134,7 @@ function NavMenu (props) {
|
||||
>
|
||||
Configuration
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className={currentPath === '/nostr-post' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/nostr-post'
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
Nostr Post
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className={currentPath === '/nostr-read' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/nostr-read'
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
Nostr Profile
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className={currentPath === '/global-feed' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/global-feed'
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
Global Feed
|
||||
</NavLink>
|
||||
|
||||
</Nav>
|
||||
</Navbar.Collapse>
|
||||
</Navbar>
|
||||
|
||||
Reference in New Issue
Block a user