diff --git a/src/components/admin-lte/orders/details.js b/src/components/admin-lte/orders/details.js
new file mode 100644
index 0000000..e70f009
--- /dev/null
+++ b/src/components/admin-lte/orders/details.js
@@ -0,0 +1,188 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Row, Col, Box, Button } from 'adminlte-2-react'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+// import JSONPretty from "react-json-pretty"
+
+let _this
+class Details extends React.Component {
+ constructor (props) {
+ super(props)
+
+ _this = this
+
+ this.state = {
+ order: {}
+ }
+ }
+
+ render () {
+ const { order } = _this.state
+ return (
+ <>
+
+
+
+
+
+
+
+ Details
+
+ {order._id && (
+
+
+ Message Type:
+ {order.messageType}
+
+
+ Message Class:
+ {order.messageClass}
+
+
+ Create At:
+ {new Date(order.localTimestamp).toLocaleString()}
+
+
+ ID:
+ {order._id}
+
+
+ Token Id:
+ {order.tokenId}
+
+
+
+ Order Type :
+ {order.buyOrSell}
+
+
+
+ Rate in Stats :
+ {order.rateInSats}
+
+
+
+ Min. Sats to Exchange :
+ {order.minSatsToExchange}
+
+
+
+ Num Tokens:
+ {order.numTokens}
+
+
+
+ Utxo TxId:
+ {order.utxoTxid}
+
+
+
+ Utxo Vout:
+ {order.utxoVout}
+
+
+
+ Tx Hex :
+ {order.txHex}
+
+
+
+
+ Timestamp :
+ {order.timestamp}
+
+
+
+ LocalTimestamp :
+ {order.localTimestamp}
+
+
+
+ p2wdbHash :
+ {order.p2wdbHash}
+
+
+
+ Offer Hash :
+ {order.offerHash}
+
+
+
+ Address References :
+ {order.addrReferences}
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ >
+ )
+ }
+
+ componentDidMount () {
+ _this.handleData()
+ }
+
+ componentDidUpdate () {
+ if (_this.props.order._id !== _this.state.order._id) {
+ _this.handleData()
+ }
+ }
+
+ handleData () {
+ try {
+ const { order } = _this.props
+ console.log('order', order)
+ _this.setState({
+ order: order.data
+ })
+ } catch (err) {
+ console.warn('Error in handleData()', err)
+ }
+ }
+
+ handleClose () {
+ _this.props.onClose()
+ }
+
+ // Detects if the input is a string and converts to json object
+ isJson (data) {
+ try {
+ JSON.parse(data)
+ return true
+ } catch (error) {
+ return false
+ }
+ }
+}
+Details.propTypes = {
+ order: PropTypes.object,
+ onClose: PropTypes.func,
+ onTake: PropTypes.func
+}
+export default Details
diff --git a/src/components/admin-lte/orders/index.js b/src/components/admin-lte/orders/index.js
new file mode 100644
index 0000000..cd457e8
--- /dev/null
+++ b/src/components/admin-lte/orders/index.js
@@ -0,0 +1,321 @@
+import React from 'react'
+import { Row, Col, Content, Box, DataTable, Button, Inputs } from 'adminlte-2-react'
+import Details from './details'
+import './orders.css'
+import Spinner from '../../../images/loader.gif'
+const { Text } = Inputs
+
+const axios = require('axios').default
+const siteConfig = require('../../site-config')
+
+let SERVER = siteConfig.bchDexUrl
+
+// const EXPLORER_URL = 'https://explorer.bitcoin.com/bch/tx/'
+
+let _this
+class Orders extends React.Component {
+ constructor (props) {
+ super(props)
+ _this = this
+ this.state = {
+ showEntry: false,
+ data: [],
+ orders: [],
+ orderData: null,
+ showTakeModal: false,
+ takeInput: ''
+ }
+
+ this.firstColumns = [
+ { title: 'Ticker', data: 'ticker' },
+ {
+ title: 'Token Id',
+ data: 'tokenId',
+ render: id => (
+ {id.subString}
+ )
+ },
+ {
+ title: 'Type',
+ data: 'buyOrSell'
+ },
+ {
+ title: 'Offer hash',
+ data: 'offerHash',
+ render: hash => (
+ {hash.subString}
+ )
+ },
+ { title: 'Qty', data: 'qty' },
+ { title: 'Sats Each', data: 'satsEach' },
+ { title: 'Sats Total', data: 'satsTotal' },
+ { title: 'USD Total', data: 'usdTotal' },
+ {
+ title: '',
+ data: 'take',
+ // render: order => (order.data.orderStatus === 'posted'
+ // ? (
+ //
+ //
+ //
+ // )
+ // :
+ // )
+ render: order => (
+
+
+
+ )
+ }
+ ]
+ }
+
+ render () {
+ const { data, orderData } = _this.state
+ return (
+ <>
+
+
+ {orderData && (
+
+
+
+ )}
+
+
+ {
+ console.log(data)
+ if (data.key === 'hashHandler') { _this.handleHashClick(data) }
+
+ if (data.key === 'takeHandler') { _this.handleTake(data) }
+ }
+ }}
+ />
+
+
+
+
+ {
+ _this.state.showTakeModal && (
+
+
+
+

+
Taking the other side of the trade...
+
+
+
+ )
+ }
+ >
+ )
+ }
+
+ async componentDidMount () {
+ _this.handleOrders()
+
+ // Get data and update the table
+ // every 20 seconds
+ setInterval(() => {
+ _this.handleOrders()
+ }, 30000)
+ }
+
+ async handleOrders () {
+ const orders = await _this.getOrders()
+ await _this.generateDataTable(orders)
+ }
+
+ // REST petition to Get data fron the pw2db
+ async getOrders () {
+ try {
+ // console.log('SERVER: ', SERVER)
+
+ const options = {
+ method: 'GET',
+ url: `${SERVER}/offer/list/`,
+ data: {}
+ }
+ const result = await axios.request(options)
+ // console.log('result.data', result.data)
+
+ _this.setState({
+ orders: result.data
+ })
+
+ return result.data
+ } catch (err) {
+ console.warn('Error in getOrders() ', err)
+ }
+ }
+
+ // Generate table content
+ async generateDataTable (dataArr = []) {
+ try {
+ const data = []
+ // console.log(`dataArr: ${JSON.stringify(dataArr, null, 2)}`)
+
+ if(!this.props.bchWallet) {
+ console.log(`BCH wallet is not initialized. Can not calculate price of BCH or tokens.`)
+ return
+ }
+
+ // Get the spot price for BCH.
+ const bchSpotPrice = await this.props.bchWallet.getUsd()
+
+ // console.log('bchSpotPrice: ', bchSpotPrice)
+
+ for (let i = 0; i < dataArr.length; i++) {
+ const order = dataArr[i]
+ // console.log(`order: ${JSON.stringify(order, null, 2)}`)
+
+ const satsTotal = order.numTokens * order.rateInBaseUnit
+ let usdTotal = bchSpotPrice * this.props.bchWallet.bchjs.BitcoinCash.toBitcoinCash(satsTotal)
+ usdTotal = `$${this.props.bchWallet.bchjs.Util.floor8(usdTotal)}`
+
+ const row = {
+ ticker: order.ticker,
+ tokenId: {
+ tokenId: order.tokenId,
+ subString: _this.cutString(order.tokenId)
+ },
+ // createdAt row data
+ createdAt: new Date(order.timestamp).toLocaleString(),
+ // Transaction id row data
+ buyOrSell: order.buyOrSell,
+ // Hash row data
+ offerHash: {
+ key: 'hashHandler',
+ subString: _this.cutString(order.p2wdbHash),
+ order: order.p2wdbHash,
+ data: order
+ },
+ qty: order.numTokens,
+ satsEach: order.rateInBaseUnit,
+ satsTotal,
+ usdTotal,
+ orderStatus: order.offerStatus,
+ take: {
+ key: 'takeHandler',
+ data: order
+ }
+ }
+ data.push(row)
+ }
+
+ _this.setState({ data })
+ } catch (err) {
+ console.warn('Error in generateDataTable() ', err)
+ }
+ }
+
+ cutString (txid) {
+ try {
+ const subTxid = txid.slice(0, 4)
+ const subTxid2 = txid.slice(-4)
+ return `${subTxid}...${subTxid2}`
+ } catch (err) {
+ console.warn('Error in cutString() ', err)
+ }
+ }
+
+ handleHashClick (data) {
+ try {
+ // data.isValid = data.isValid.toString()
+ _this.setState({
+ orderData: data
+ })
+ } catch (err) {
+ _this.setState({
+ orderData: null
+ })
+ console.warn('Error in handleHashClick() ', err)
+ }
+ }
+
+ handleClose () {
+ _this.setState({
+ orderData: null,
+ showTakeModal: false
+ })
+ }
+
+ async handleTake (order) {
+ console.log('handleTake() order: ', order)
+
+ const offerCid = order.data.p2wdbHash
+ console.log('offerCid: ', offerCid)
+
+ // if (!order.data.status === 'posted') return
+
+ // TODO: Future functionality.
+ // Throw up a modal to query how much they want to take.
+ // _this.setState({
+ // tankenInput: '',
+ // showTakeModal: true
+ // })
+
+ // Show the modal
+ _this.setState({
+ showTakeModal: true
+ })
+
+ const options = {
+ method: 'POST',
+ url: `${SERVER}/offer/take/`,
+ data: {
+ offerCid
+ }
+ }
+ const result = await axios.request(options)
+ console.log('result of taking offer: ', result)
+
+ // Hide the modal
+ _this.setState({
+ showTakeModal: false
+ })
+ }
+
+ handleModalInputs (event) {
+ const value = event.target.value
+ _this.setState({
+ [event.target.name]: value
+ })
+ }
+}
+
+export default Orders
diff --git a/src/components/admin-lte/orders/orders.css b/src/components/admin-lte/orders/orders.css
new file mode 100644
index 0000000..c102b42
--- /dev/null
+++ b/src/components/admin-lte/orders/orders.css
@@ -0,0 +1,29 @@
+.btn-close-entry{
+ width: 150px;
+}
+.action-handler{
+ color: #3c8dbc;
+ cursor: pointer;
+}
+.details-data-content{
+ text-align: left;
+}
+.take-form-wrapper{
+ /* display: flex;
+ justify-content: center;
+ flex-direction: column; */
+ text-align: center;
+}
+#OrdersTable> tbody >.odd{
+ height: 55px!important;
+}
+#OrdersTable> tbody > .even{
+ height: 55px!important;
+}
+.take-btn-lg{
+ width: 150px;
+}
+.take-btn-table-wrapper{
+ text-align: end;
+ padding-right: 10px;
+}
\ No newline at end of file
diff --git a/src/components/admin-lte/tokens/index.js b/src/components/admin-lte/tokens/index.js
index 8531486..00f5aef 100644
--- a/src/components/admin-lte/tokens/index.js
+++ b/src/components/admin-lte/tokens/index.js
@@ -3,11 +3,14 @@ import PropTypes from 'prop-types'
import { Content, Row, Col, Box, Button } from 'adminlte-2-react'
import TokenCard from './token-card'
import TokenModal from './token-modal'
+import SellModal from './sell-modal'
import Spinner from '../../../images/loader.gif'
// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import SendTokens from './send-tokens'
// import { SlpMutableData } from 'slp-mutable-data'
+
let _this
+
class Tokens extends React.Component {
constructor (props) {
super(props)
@@ -16,6 +19,7 @@ class Tokens extends React.Component {
tokens: [],
selectedTokenToView: '',
showModal: false,
+ showSellModal: false,
inFetch: true,
errMsg: '',
selectedTokenToSend: '',
@@ -99,6 +103,7 @@ class Tokens extends React.Component {
id={`token-${i}`}
token={val}
showToken={_this.showToken}
+ showSellModal={_this.showSellModal}
selectToken={_this.selectToken}
/>
@@ -112,6 +117,7 @@ class Tokens extends React.Component {
)}
)}
+
+
+
>
)
}
@@ -196,6 +214,14 @@ class Tokens extends React.Component {
_this.onHandleToggleModal()
}
+ // This is called by the token-card when the 'Sell' button is clicked.
+ showSellModal (selectedTokenToView) {
+ _this.setState({
+ selectedTokenToView
+ })
+ _this.onHandleToggleSellModal()
+ }
+
selectToken (selectedTokenToSend) {
_this.setState({
selectedTokenToSend
@@ -215,6 +241,15 @@ class Tokens extends React.Component {
}
}
+ onHandleToggleSellModal (refresh = null) {
+ _this.setState({
+ showSellModal: !_this.state.showSellModal
+ })
+ if (refresh) {
+ _this.handleGetTokens(true)
+ }
+ }
+
handleError (error) {
let errMsg = ''
if (error.message) {
diff --git a/src/components/admin-lte/tokens/sell-modal.js b/src/components/admin-lte/tokens/sell-modal.js
new file mode 100644
index 0000000..a047e6a
--- /dev/null
+++ b/src/components/admin-lte/tokens/sell-modal.js
@@ -0,0 +1,429 @@
+/*
+ This modal controlls the selling of tokens.
+ It was adapted from token-modal.js and contains references to 'burn' tokens.
+*/
+
+// Global npm libraries
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Content, Row, Col, Box, Button, Inputs } from 'adminlte-2-react'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import axios from 'axios'
+
+// Local libraries
+import './token.css'
+const siteConfig = require('../../site-config')
+
+let _this
+const { Text } = Inputs
+
+class SellModal extends React.Component {
+ constructor (props) {
+ super(props)
+ _this = this
+ this.state = {
+ copySuccess: '',
+ isBurnView: false,
+ txId: '',
+ errMsg: '',
+ inFetch: false,
+ sellQty: 1,
+ pricePerToken: 0.02
+ }
+
+ this.modalFooter = (
+ <>
+
+
+ >
+ )
+
+ this.confirmFooter = (
+ <>
+ this.sellTokens(false)} />
+ this.sellTokens(true)}
+ />
+ >
+ )
+
+ this.onDoneFooter = (
+ <>
+ this.sellTokens(false)}
+ />
+ >
+ )
+ }
+
+ render () {
+ const token = _this.props.token
+
+ return (
+ <>
+
+
+
+
+ {_this.state.isBurnView && !_this.state.txId && (
+
+
+ Are you sure you want to sell {`${this.state.sellQty} `}
+ tokens at {`$${this.state.pricePerToken} `} per token?
+
+
+ )}
+
+ {_this.state.isBurnView && _this.state.txId && (
+
+ )}
+
+ {_this.state.isBurnView && _this.state.errMsg && (
+
+ )}
+
+ {!_this.state.isBurnView && (
+
+
+
+
+
+
+
+
+ TokenId:
+
+
+ {token.tokenId}
+
+
+ {_this.state.copySuccess === 'tokenId'
+ ? (
+
+ Copied!
+
+ )
+ : (
+
+ _this.copyToClipBoard('tokenId')}
+ icon='copy'
+ />
+ )}
+
+
+
+
+
+
+
+
+
+ Ticker:
+
+
+ {token.ticker}
+
+
+
+
+
+
+
+
+
+ Balance:
+
+
+ {token.qty}
+
+
+
+
+
+
+
+
+
+ Sell Qty:
+
+
+
+
+
+
+
+
+
+
+
+
+ Price Per
Token (USD):
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ >
+ )
+ }
+
+ // Update the quantity of tokens to sell
+ handleSellQty(event) {
+ const value = event.target.value
+ // console.log('value: ', value)
+
+ _this.setState({
+ sellQty: parseFloat(value)
+ })
+
+ // console.log('_this.state.sellQty: ', _this.state.sellQty)
+ }
+
+ handlePricePerToken(event) {
+ const value = event.target.value
+ // console.log('value: ', value)
+
+ _this.setState({
+ pricePerToken: parseFloat(value)
+ })
+ }
+
+ // copy info to clipboard
+ copyToClipBoard (key) {
+ const val = _this.props.token[key]
+ const textArea = document.createElement('textarea')
+ textArea.value = val // copyText.textContent;
+ document.body.appendChild(textArea)
+ textArea.select()
+ document.execCommand('Copy')
+ textArea.remove()
+
+ _this.handleCopySuccess(key)
+ }
+
+ handleCopySuccess (key) {
+ _this.setState({
+ copySuccess: key
+ })
+ setTimeout(() => {
+ _this.setState({
+ copySuccess: ''
+ })
+ }, 1000)
+ }
+
+ // This is called when the user clicks the 'Sell' button to place their order.
+ handleConfirm () {
+ // Verify that inputs are valid.
+ if(isNaN(_this.state.sellQty)) {
+ alert('Token quantity must be a number.')
+ return
+ }
+ if(isNaN(_this.state.pricePerToken)) {
+ alert('Price per token must be a number.')
+ return
+ }
+
+ _this.setState({
+ isBurnView: true
+ })
+ }
+
+ handleModal () {
+ // if txId exist refresh tokens on close
+ _this.props.handleOnHide(_this.state.txId)
+
+ setTimeout(() => {
+ _this.setState({
+ isBurnView: false,
+ txId: '',
+ errMsg: '',
+ inFetch: false
+ })
+ }, 200)
+ }
+
+ async sellTokens(isConfirmed) {
+ try {
+ // Dismiss
+ if (!isConfirmed) {
+ _this.handleModal()
+ return
+ }
+
+ console.log('Putting in order to sell tokens...')
+
+ // Throw up spinny-waiting-gif
+ _this.setState({
+ inFetch: true
+ })
+
+ const { bchWallet, token } = _this.props
+ console.log('token: ', token)
+
+ // Error if trying to sell more tokens than the wallet holds.
+ if(token.qty < _this.state.sellQty) {
+ alert(`Error: The wallet has ${token.qty} tokens, which is less than the ${_this.state.sellQty} tokens you are trying to sell.`)
+ }
+
+ console.log(`bch-dex server: ${siteConfig.bchDexUrl}`)
+
+ // Convert the dollar amount of tokens to sats.
+ const bchSpotPrice = await bchWallet.getUsd()
+ console.log('bchSpotPrice: ', bchSpotPrice)
+ const bchPerToken = _this.state.pricePerToken/bchSpotPrice
+ console.log('bchPerToken: ', bchPerToken)
+ const satsPerToken = Math.floor(bchWallet.bchjs.BitcoinCash.toSatoshi(bchPerToken))
+ console.log('satsPerToken: ', satsPerToken)
+
+ const orderObj = {
+ order: {
+ lokadId: 'SWP',
+ messageType: 1,
+ messageClass: 1,
+ tokenId: token.tokenId,
+ buyOrSell: 'sell',
+ numTokens: _this.state.sellQty,
+ rateInBaseUnit: satsPerToken,
+ minUnitsToExchange: satsPerToken*_this.state.sellQty,
+ }
+ }
+
+ const result = await axios.post(`${siteConfig.bchDexUrl}/order`, orderObj)
+ console.log(`result.data: `, result.data)
+
+ // console.log('starting sleep')
+ // await bchWallet.bchjs.Util.sleep(5000)
+ // console.log('ended sleep')
+
+ // Remove spinny-waiting-gif and display the TXID of the transaction.
+ _this.setState({
+ txId: result.data.hash,
+ inFetch: false
+ })
+ } catch(error) {
+ console.warn(error)
+ _this.setState({
+ errMsg: error.message,
+ inFetch: false
+ })
+ }
+ }
+
+ // Deprecated. This handled the burn API call.
+ async handleBurnAll (isConfirmed) {
+ try {
+ // Dismiss
+ if (!isConfirmed) {
+ _this.handleModal()
+ return
+ }
+
+ /**
+ * BURN ALL
+ *
+ */
+ _this.setState({
+ inFetch: true
+ })
+ const { bchWallet, token } = _this.props
+
+ const result = await bchWallet.burnAll(token.tokenId)
+ console.log('burn txid: ', result)
+ _this.setState({
+ txId: result,
+ inFetch: false
+ })
+ } catch (error) {
+ console.warn(error)
+ _this.setState({
+ errMsg: error.message,
+ inFetch: false
+ })
+ }
+ }
+}
+
+SellModal.propTypes = {
+ token: PropTypes.object.isRequired,
+ show: PropTypes.bool.isRequired,
+ handleOnHide: PropTypes.func.isRequired,
+ bchWallet: PropTypes.object, // get minimal-slp-wallet instance
+ explorerURL: PropTypes.string
+}
+
+export default SellModal
diff --git a/src/components/admin-lte/tokens/token-card.js b/src/components/admin-lte/tokens/token-card.js
index b445c83..c35d730 100644
--- a/src/components/admin-lte/tokens/token-card.js
+++ b/src/components/admin-lte/tokens/token-card.js
@@ -3,7 +3,9 @@ import PropTypes from 'prop-types'
import { Row, Col, Box, Button } from 'adminlte-2-react'
import Jdenticon from 'react-jdenticon'
import './token.css'
+
let _this
+
class TokenCard extends React.Component {
constructor (props) {
super(props)
@@ -42,7 +44,7 @@ class TokenCard extends React.Component {
-
+
-
+
+
+ {
+ _this.props.showSellModal(token)
+ }}
+ />
+
+
+
{
return [
+ {
+ active: true,
+ key: 'Orders',
+ component: ,
+ menuItem:
+ },
{
key: 'Tokens',
component: ,
diff --git a/src/components/site-config.js b/src/components/site-config.js
index 9cfc033..bf1c3e2 100644
--- a/src/components/site-config.js
+++ b/src/components/site-config.js
@@ -23,10 +23,24 @@ const config = {
// Interface used by minial-slp-wallet
interface: 'consumer-api',
- restURL: 'https://free-bch.fullstack.cash'
+ restURL: 'https://free-bch.fullstack.cash',
// restURL: 'http://localhost:5005'
// interface: 'rest-api',
// restURL: 'https://bchn.fullstack.cash/v5/'
+
+ // Set to true if you want to manually override auto-detection of the DEX URL,
+ // and use the setting below.
+ manualDexUrl: false,
+ // Default URL for bch-dex
+ bchDexUrl: 'http://localhost:5700'
+}
+
+// Attempt to auto-detect the URL for the bch-dex.
+if(window && window.document && window.document.domain) {
+ if(window.document.domain.includes('192.168') ||
+ window.document.domain.includes('localhost')) {
+ config.bchDexUrl = `http://${window.document.domain}:5700`
+ }
}
module.exports = config