diff --git a/package-lock.json b/package-lock.json
index b054b2d..c04fb03 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -28,8 +28,9 @@
"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-markdown": "10.1.0",
"react-router-dom": "7.1.3",
"react-scripts": "5.0.1",
"stream-browserify": "3.0.0",
@@ -21343,6 +21344,23 @@
}
}
},
+ "node_modules/react-bs-datatable": {
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/react-bs-datatable/-/react-bs-datatable-3.15.0.tgz",
+ "integrity": "sha512-kcj3r4SbjnlyNAg/muOiTg4TGGtEqGYie5HjLB+V83oDigiMY/Sgv0gyNIE0qgHxxNkICkJOeLesC7EsYCy5lg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "@fortawesome/fontawesome-svg-core": "6.x",
+ "@fortawesome/free-solid-svg-icons": "6.x",
+ "@fortawesome/react-fontawesome": "0.x",
+ "bootstrap": "5.x",
+ "react": ">=16.8",
+ "react-bootstrap": "2.x"
+ }
+ },
"node_modules/react-dev-utils": {
"version": "12.0.1",
"license": "MIT",
diff --git a/package.json b/package.json
index f9c6546..d6c4619 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/app-body/index.js b/src/components/app-body/index.js
index bc349b6..8a19a0a 100644
--- a/src/components/app-body/index.js
+++ b/src/components/app-body/index.js
@@ -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) {
} />
} />
} />
+ } />
{/** Show in all paths except the servers view */}
{/* {appData.currentPath !== '/servers' && } */}
diff --git a/src/components/app-body/offers/index.js b/src/components/app-body/offers/index.js
new file mode 100644
index 0000000..1c8e19b
--- /dev/null
+++ b/src/components/app-body/offers/index.js
@@ -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 = ({smallTokenId})
+
+ // Get and format the P2WDB ID
+ const nostrEventId = thisOffer.nostrEventId
+ const smallNostrEventId = cutString(nostrEventId)
+ thisOffer.smallNostrEventId = smallNostrEventId
+ thisOffer.button = ()
+
+ // thisOffer.p2wdbHash = ({smallP2wdbHash})
+
+ // 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(P2WDB Entry)
+
+ 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 && (
+
+ )}
+
+
+
+ {!isLoading && (
+
+
+
+ )}
+
+
+ {isLoading && (
+
+
+
+
+
+ )}
+
+ >
+ )
+}
+
+export default Offers
diff --git a/src/components/nav-menu/index.js b/src/components/nav-menu/index.js
index 573b1e4..63ff4d5 100644
--- a/src/components/nav-menu/index.js
+++ b/src/components/nav-menu/index.js
@@ -121,6 +121,13 @@ function NavMenu (props) {
>
Global Feed
+
+ Fungible Tokens
+