mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex-taker-v2.git
synced 2026-09-21 16:52:01 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ad3d40591 | ||
|
|
a4027b2fcb | ||
|
|
192497b5cf | ||
|
|
fda2de5362 | ||
|
|
131720ed58 | ||
|
|
64b1aec4cf | ||
|
|
70ce941eba | ||
|
|
dfd5934ddb | ||
|
|
e0652714e8 | ||
|
|
838a9e3dfd | ||
|
|
6b9626aeb1 | ||
|
|
3ce258fd8c | ||
|
|
10aef2a397 | ||
|
|
a77147d077 | ||
|
|
aa7d6f4bc7 | ||
|
|
054205c545 | ||
|
|
686d51615a | ||
|
|
55b87d4e9d | ||
|
|
2208a1132f | ||
|
|
10d47954cf | ||
|
|
3b23edf758 | ||
|
|
99a299c2e3 | ||
|
|
ab02a66058 | ||
|
|
37e7061208 | ||
|
|
9bf61f5913 | ||
|
|
754f10a61b | ||
|
|
fcd3da20f6 | ||
|
|
7ee7d7ec1b | ||
|
|
634ec7836c | ||
|
|
688ea3fe71 | ||
|
|
91952761f9 | ||
|
|
a22c29cbce | ||
|
|
25ba833ab9 | ||
|
|
7ce2a0878b | ||
|
|
5826313c01 | ||
|
|
3c0fe9ab00 | ||
|
|
165141ea41 | ||
|
|
0951385872 | ||
|
|
b0487d78e9 | ||
|
|
2766867b26 | ||
|
|
37a124a3d3 | ||
|
|
a3f75af1d9 | ||
|
|
0d78498e4d | ||
|
|
051c600e9d | ||
|
|
28c515ea9b | ||
|
|
82b86724eb | ||
|
|
e8b8492f35 | ||
|
|
098f5e300e | ||
|
|
424cc3069b | ||
|
|
b3cebcf13e | ||
|
|
34bdd9ba87 | ||
|
|
d28f26e44d | ||
|
|
c7e55daeda | ||
|
|
6de9ad944c | ||
|
|
75b5169390 | ||
|
|
02367ce10f | ||
|
|
d5e9e15909 | ||
|
|
a689a9b3e6 | ||
|
|
40911990ac | ||
|
|
88198f28fe | ||
|
|
42d5ce7894 | ||
|
|
f2e94ae605 | ||
|
|
0e71568750 | ||
|
|
5ceb7e8e87 | ||
|
|
927b073557 | ||
|
|
80f95ee576 | ||
|
|
138c48559f | ||
|
|
77d83349c1 | ||
|
|
1b16f62b99 | ||
|
|
ab38407686 | ||
|
|
2d9fb823cd | ||
|
|
3c4d3c8a62 | ||
|
|
717a4c27a8 | ||
|
|
62903ccdcb | ||
|
|
6794fb8644 | ||
|
|
678e90cef0 | ||
|
|
f6e362c5ff | ||
|
|
0328f8b34b | ||
|
|
f4c17322dc | ||
|
|
b3e71b0353 | ||
|
|
f3cbb91927 | ||
|
|
89ffde9f39 | ||
|
|
c16ae834a5 | ||
|
|
a70e995027 | ||
|
|
d1d86cba9c | ||
|
|
9f4d82d440 | ||
|
|
946e66c45b | ||
|
|
71637e5ea3 | ||
|
|
d322ef16bb | ||
|
|
83f46c0297 | ||
|
|
a0189c2de9 | ||
|
|
eddd454ee0 | ||
|
|
829c6e226c | ||
|
|
55d608a7bf | ||
|
|
f064c7900e | ||
|
|
d88d12d486 | ||
|
|
5dccc1398f | ||
|
|
77c19e60b5 | ||
|
|
996f83f1a8 | ||
|
|
dc4a7f3501 | ||
|
|
9f4f972880 | ||
|
|
7981c7fb1f |
@@ -2,7 +2,7 @@
|
||||
|
||||
This is a web-based single page app (SPA) written in React. It is forked from [bch-wallet-web3-spa](https://github.com/Permissionless-Software-Foundation/bch-wallet-web3-spa) and replaces [bch-dex-taker-ui](https://github.com/Permissionless-Software-Foundation/bch-dex-taker-ui).
|
||||
|
||||
The purpose of this app is to provide a web-based interface for users to browse NFT and fungible tokens for sale on the SLP DEX protocol. This provides a simple 1-click purchase functionality. A different UI is used for selling tokens, as there are more requirements for sellers imposed by the SWaP protocol.
|
||||
The purpose of this app is to provide a web-based interface for users to browse NFT and fungible tokens for sale on the SLP DEX protocol. This provides a simple 1-click purchase functionality. A different UI ([bch-dex-ui-v3](https://github.com/Permissionless-Software-Foundation/bch-dex-ui-v3)) is used for selling tokens, as there are more requirements for sellers imposed by the SWaP protocol.
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
const webpack = require('webpack')
|
||||
|
||||
module.exports = function override (config) {
|
||||
config.resolve.fallback = {
|
||||
...config.resolve.fallback,
|
||||
crypto: require.resolve('crypto-browserify'),
|
||||
stream: require.resolve('stream-browserify'),
|
||||
vm: require.resolve('vm-browserify')
|
||||
}
|
||||
|
||||
config.plugins = (config.plugins || []).concat([
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser'
|
||||
})
|
||||
])
|
||||
|
||||
return config
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Generated
+11222
-4498
File diff suppressed because it is too large
Load Diff
+34
-6
@@ -7,23 +7,34 @@
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"axios": "0.27.2",
|
||||
"bch-dex-lib": "2.0.2",
|
||||
"bch-message-lib": "2.2.1",
|
||||
"bch-nostr": "1.3.4",
|
||||
"bch-token-sweep": "2.2.1",
|
||||
"bootstrap": "5.2.0",
|
||||
"crypto-browserify": "3.12.1",
|
||||
"nostr-tools": "2.10.4",
|
||||
"p2wdb": "2.4.1",
|
||||
"process": "0.11.10",
|
||||
"qrcode.react": "4.2.0",
|
||||
"query-string": "7.1.1",
|
||||
"react": "19.0.0",
|
||||
"react-bootstrap": "2.10.7",
|
||||
"react-dom": "19.0.0",
|
||||
"react-router-dom": "7.1.3",
|
||||
"react-scripts": "5.0.1",
|
||||
"stream-browserify": "3.0.0",
|
||||
"use-local-storage-state": "19.5.0",
|
||||
"use-query-params": "1.2.3"
|
||||
"use-query-params": "1.2.3",
|
||||
"vm-browserify": "1.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"start": "react-app-rewired start",
|
||||
"build": "react-app-rewired build",
|
||||
"test": "npm run lint",
|
||||
"eject": "react-app-rewired eject",
|
||||
"lint": "standard --env mocha --fix",
|
||||
"pub": "node deploy/publish-main.js",
|
||||
"pub:ghp": "./deploy/publish-gh-pages.sh"
|
||||
@@ -44,8 +55,25 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"minimal-slp-wallet": "5.8.5",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"husky": "9.1.7",
|
||||
"minimal-slp-wallet": "5.13.1",
|
||||
"react-app-rewired": "2.2.1",
|
||||
"semantic-release": "24.2.3",
|
||||
"standard": "17.0.0",
|
||||
"web3.storage": "4.3.0"
|
||||
},
|
||||
"release": {
|
||||
"publish": [
|
||||
{
|
||||
"path": "@semantic-release/npm",
|
||||
"npmPublish": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>BCH Web3 Template</title>
|
||||
<title>SLP DEX Buyer Wallet</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+22
@@ -31,3 +31,25 @@
|
||||
padding: 0.5rem;
|
||||
|
||||
}
|
||||
|
||||
#address-switch {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.buy-modal {
|
||||
top: 30%!important;
|
||||
width: auto!important;
|
||||
}
|
||||
|
||||
|
||||
/* Remove input number arrows Chrome, Safari, Edge, Opera */
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Remove input number arrows Firefox */
|
||||
input[type=number] {
|
||||
--moz-appearance: textfield;
|
||||
}
|
||||
+14
@@ -71,6 +71,20 @@ function App (props) {
|
||||
addToModal('Getting BCH spot price in USD', appData)
|
||||
await asyncLoad.getUSDExchangeRate(walletTemp, appData.updateBchWalletState, appData)
|
||||
|
||||
// Load the P2WDB libraries.
|
||||
addToModal('Loading P2WDB Libraries', appData)
|
||||
const { p2wdbRead, p2wdbWrite } = asyncLoad.getP2WDBLib({ bchWallet: walletTemp })
|
||||
|
||||
// Load the DEX library.
|
||||
addToModal('Loading DEX Library', appData)
|
||||
const dexLib = asyncLoad.getDexLib({ bchWallet: walletTemp, p2wdbRead, p2wdbWrite })
|
||||
appData.setDexLib(dexLib)
|
||||
|
||||
// Load the Nostr library.
|
||||
addToModal('Loading Nostr Library', appData)
|
||||
const nostrLib = asyncLoad.getNostrLib({ bchWallet: walletTemp })
|
||||
appData.setNostr(nostrLib)
|
||||
|
||||
// Update state
|
||||
appData.setShowStartModal(false)
|
||||
appData.setDenyClose(false)
|
||||
|
||||
@@ -14,7 +14,8 @@ function GetBalance (props) {
|
||||
const [textInput, setTextInput] = useState('')
|
||||
|
||||
// Button click handler
|
||||
const handleGetBalance = async (event) => {
|
||||
const handleGetBalance = async (e) => {
|
||||
e.preventDefault()
|
||||
try {
|
||||
// Exit on invalid input
|
||||
if (!textInput) return
|
||||
@@ -43,9 +44,9 @@ function GetBalance (props) {
|
||||
<Container>
|
||||
<Row>
|
||||
<Col className='text-break' style={{ textAlign: 'center' }}>
|
||||
<Form>
|
||||
<Form onSubmit={handleGetBalance}>
|
||||
<Form.Group className='mb-3' controlId='formBasicEmail'>
|
||||
<Form.Label>Enter a BCH address to check the balance.</Form.Label>
|
||||
<Form.Label>Enter any BCH address to query its balance on the blockchain.</Form.Label>
|
||||
<Form.Control type='text' placeholder='bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' onChange={e => setTextInput(e.target.value)} />
|
||||
</Form.Group>
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
This card displays the users balance in BCH.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React from 'react'
|
||||
import { Container, Row, Col, Card } from 'react-bootstrap'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faCoins } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const BalanceCard = (props) => {
|
||||
const { appData } = props
|
||||
|
||||
const bchjs = appData.wallet.bchjs
|
||||
const sats = appData.bchWalletState.bchBalance
|
||||
const bchBalance = bchjs.BitcoinCash.toBitcoinCash(sats)
|
||||
const usdBalance = bchjs.Util.floor2(bchBalance * appData.bchWalletState.bchUsdPrice)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title style={{ textAlign: 'center' }}>
|
||||
<h2><FontAwesomeIcon icon={faCoins} size='lg' /> Balance</h2>
|
||||
</Card.Title>
|
||||
<br />
|
||||
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<b>USD</b>: ${usdBalance}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
<b>BCH</b>: {bchBalance}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
<b>Satoshis</b>: {sats}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default BalanceCard
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
This View allows sending and receiving of BCH
|
||||
*/
|
||||
|
||||
// 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 RefreshBchBalanceButton from './refresh-bch-balance-button'
|
||||
import SendCard from './send-card'
|
||||
import BalanceCard from './balance-card'
|
||||
import ReceiveCard from './receive-card'
|
||||
|
||||
// Working array for storing modal output.
|
||||
// this.modalBody = []
|
||||
|
||||
function BchSend ({ appData }) {
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<RefreshBchBalanceButton
|
||||
appData={appData}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={6} style={{ textAlign: 'right' }}>
|
||||
<a href='https://youtu.be/KN1ZMWoLoGs' target='_blank' rel='noreferrer'>
|
||||
<FontAwesomeIcon icon={faCircleQuestion} size='lg' />
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
<Row>
|
||||
<Col style={{ textAlign: 'center' }}>
|
||||
<BalanceCard appData={appData} />
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
<SendCard
|
||||
appData={appData}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
<ReceiveCard appData={appData} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default BchSend
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
This card displays the users BCH and SLP address and QR code
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState } from 'react'
|
||||
import { Container, Row, Col, Card, Form } from 'react-bootstrap'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faWallet } from '@fortawesome/free-solid-svg-icons'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
|
||||
const ReceiveCard = ({ appData }) => {
|
||||
const [addrSwitch, setAddrSwitch] = useState(false)
|
||||
const [displayCopyMsg, setDisplayCopyMsg] = useState(false)
|
||||
|
||||
// Determine which address to display
|
||||
const addrToDisplay = !addrSwitch
|
||||
? appData.bchWalletState.cashAddress
|
||||
: appData.bchWalletState.slpAddress
|
||||
|
||||
// Copy the selected address to the clipboard when the QR image is clicked
|
||||
const handleCopyAddress = async (value) => {
|
||||
appData.appUtil.copyToClipboard(value)
|
||||
|
||||
// Display the copied message
|
||||
setDisplayCopyMsg(true)
|
||||
|
||||
// Clear the copied message after some time
|
||||
setTimeout(() => {
|
||||
setDisplayCopyMsg(false)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// Event handler for address switch toggle
|
||||
const handleAddrSwitchToggle = (event) => {
|
||||
setAddrSwitch(event.target.checked)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<Card.Body style={{ textAlign: 'center' }}>
|
||||
<Card.Title>
|
||||
<h2><FontAwesomeIcon icon={faWallet} size='lg' /> Receive</h2>
|
||||
</Card.Title>
|
||||
<br />
|
||||
|
||||
<Container>
|
||||
<Row>
|
||||
<Col style={{ color: 'green', marginBottom: '20px' }}>
|
||||
{displayCopyMsg ? 'Copied' : null}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
<QRCodeSVG
|
||||
style={{ cursor: 'pointer' }}
|
||||
className='qr-code'
|
||||
value={addrToDisplay}
|
||||
size={256}
|
||||
fgColor='#333'
|
||||
onClick={() => { handleCopyAddress(addrToDisplay) }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col style={{ marginTop: '20px' }}>
|
||||
<p>{addrToDisplay}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={4} />
|
||||
<Col xs={4}>
|
||||
<Form>
|
||||
<Form.Check
|
||||
type='switch'
|
||||
id='address-switch'
|
||||
onChange={e => handleAddrSwitchToggle(e)}
|
||||
/>
|
||||
</Form>
|
||||
</Col>
|
||||
<Col xs={4} />
|
||||
</Row>
|
||||
</Container>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReceiveCard
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
This library exports a RefreshBalance functional Component and a
|
||||
refreshBalance() function.
|
||||
The RefreshBalance Component is rendered as a hidden Waiting modal.
|
||||
When the refreshBalance() function is called, it causes the modal to
|
||||
appear while the wallet balance is updated. Once updated, the modal is hidden
|
||||
again.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useEffect, useState, useCallback } from 'react'
|
||||
|
||||
// Local libraries
|
||||
import WaitingModal from '../../waiting-modal'
|
||||
|
||||
export default function RefreshBchBalance (props) {
|
||||
// Dependency injections of props
|
||||
const { ref } = props
|
||||
|
||||
// State
|
||||
const [showWaitingModal, setShowWaitingModal] = useState(false)
|
||||
const [modalBody, setModalBody] = useState([])
|
||||
const [hideSpinner] = useState(false)
|
||||
|
||||
// Add a new line to the waiting modal.
|
||||
const addToModal = useCallback((inStr) => {
|
||||
// console.log('addToModal() inStr: ', inStr)
|
||||
setModalBody(prevBody => {
|
||||
// console.log('prevBody: ', prevBody)
|
||||
prevBody.push(inStr)
|
||||
return prevBody
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Update the balance of the wallet.
|
||||
const handleRefreshBalance = useCallback(async (appData) => {
|
||||
try {
|
||||
setModalBody([])
|
||||
|
||||
// Throw up the waiting modal
|
||||
setShowWaitingModal(true)
|
||||
|
||||
addToModal('Updating wallet balance...')
|
||||
|
||||
// Get handles on app data.
|
||||
const walletState = appData.bchWalletState
|
||||
const cashAddr = appData.bchWalletState.cashAddress
|
||||
const wallet = appData.wallet
|
||||
|
||||
// Get the latest balance of the wallet.
|
||||
const newBalance = await wallet.getBalance({ bchAddress: cashAddr })
|
||||
|
||||
addToModal('Updating BCH per USD price...')
|
||||
const bchUsdPrice = await wallet.getUsd()
|
||||
|
||||
// Update the wallet state.
|
||||
walletState.bchBalance = newBalance
|
||||
walletState.bchUsdPrice = bchUsdPrice
|
||||
appData.updateBchWalletState({ walletState, appData })
|
||||
|
||||
setShowWaitingModal(false)
|
||||
setModalBody([])
|
||||
} catch (err) {
|
||||
console.error('Error while trying to update BCH balance: ', err)
|
||||
|
||||
addToModal([`Error: ${err.message}`])
|
||||
setShowWaitingModal(false)
|
||||
}
|
||||
}, [addToModal])
|
||||
|
||||
// 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 = { handleRefreshBalance }
|
||||
}, [ref, handleRefreshBalance])
|
||||
|
||||
return (
|
||||
<>
|
||||
<>
|
||||
{showWaitingModal && (
|
||||
<WaitingModal
|
||||
heading='Refreshing BCH Balance'
|
||||
body={modalBody}
|
||||
hideSpinner={hideSpinner}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
This component is displayed as a button. When clicked, it loads the
|
||||
RefreshBchBalance component, which renders a waiting modal while the wallet
|
||||
balance is refreshed.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useRef } from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faRedo } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
// Local libraries
|
||||
import RefreshBchBalance from './refresh-balance'
|
||||
|
||||
function RefreshBchBalanceButton (props) {
|
||||
// Dependency injections of props
|
||||
const appData = props.appData
|
||||
|
||||
// Child function references
|
||||
const refreshBchBalanceRef = useRef()
|
||||
|
||||
// Update the balance of the wallet.
|
||||
async function handleButtonRefreshBalance (appData) {
|
||||
// Call the child function
|
||||
refreshBchBalanceRef.current.handleRefreshBalance(appData)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant='success' onClick={() => { handleButtonRefreshBalance(appData) }}>
|
||||
<FontAwesomeIcon icon={faRedo} size='lg' /> Refresh
|
||||
</Button>
|
||||
|
||||
<RefreshBchBalance appData={appData} ref={refreshBchBalanceRef} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RefreshBchBalanceButton
|
||||
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
This component controls sending of BCH.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState, useRef } from 'react'
|
||||
import { Container, Row, Col, Card, Form, Button } from 'react-bootstrap'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faPaperPlane, faPaste, faRandom } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
// Local libraries
|
||||
import WaitingModal from '../../waiting-modal'
|
||||
import RefreshBchBalance from './refresh-balance'
|
||||
|
||||
function SendCard (props) {
|
||||
// Dependency injection through props
|
||||
const appData = props.appData
|
||||
|
||||
// Modal State
|
||||
const [modalBody, setModalBody] = useState([])
|
||||
const [hideSpinner, setHideSpinner] = useState(false)
|
||||
const [hideWaitingModal, setHideWaitingModal] = useState(true)
|
||||
const [hideModal, setHideModal] = useState(true)
|
||||
|
||||
// Form State
|
||||
const [bchAddr, setBchAddr] = useState('')
|
||||
const [amountStr, setAmountStr] = useState('')
|
||||
const [amountUnits, setAmountUnits] = useState('USD')
|
||||
const [oppositeUnits, setOppositeUnits] = useState('BCH')
|
||||
const [oppositeQty, setOppositeQty] = useState(0)
|
||||
|
||||
// Child function references
|
||||
const refreshBchBalanceRef = useRef()
|
||||
|
||||
// Update the balance of the wallet.
|
||||
async function handleButtonRefreshBalance (appData) {
|
||||
// Call the child function
|
||||
refreshBchBalanceRef.current.handleRefreshBalance(appData)
|
||||
}
|
||||
|
||||
// Encapsulate the state for this component into a single object that can
|
||||
// be passed around to subfunctions and subcomponents.
|
||||
const sendCardData = {
|
||||
modalBody,
|
||||
setModalBody,
|
||||
hideSpinner,
|
||||
setHideSpinner,
|
||||
hideWaitingModal,
|
||||
setHideWaitingModal,
|
||||
hideModal,
|
||||
setHideModal,
|
||||
bchAddr,
|
||||
setBchAddr,
|
||||
amountStr,
|
||||
setAmountStr,
|
||||
amountUnits,
|
||||
setAmountUnits,
|
||||
oppositeUnits,
|
||||
setOppositeUnits,
|
||||
oppositeQty,
|
||||
setOppositeQty
|
||||
}
|
||||
|
||||
// This function is called when the modal is closed.
|
||||
function onModalClose () {
|
||||
sendCardData.setHideModal(true)
|
||||
|
||||
handleButtonRefreshBalance(appData)
|
||||
}
|
||||
|
||||
async function pasteFromClipboard () {
|
||||
try {
|
||||
const addr = await appData.appUtil.readFromClipboard()
|
||||
sendCardData.setBchAddr(addr)
|
||||
} catch (err) {
|
||||
// Browser implementation. Exit quietly.
|
||||
}
|
||||
}
|
||||
|
||||
// This is an on-change event handler that updates the amount calculated in
|
||||
// both BCH and USD as the user types.
|
||||
function handleUpdateAmount (inObj = {}) {
|
||||
try {
|
||||
const { event, appData, sendCardData } = inObj
|
||||
|
||||
// Update the state of the text box.
|
||||
let amountStr = event.target.value
|
||||
sendCardData.setAmountStr(amountStr)
|
||||
if (!amountStr) amountStr = '0'
|
||||
|
||||
// Convert the string to a number.
|
||||
const amountQty = parseFloat(amountStr)
|
||||
|
||||
const bchUsdPrice = appData.bchWalletState.bchUsdPrice
|
||||
const bchjs = appData.wallet.bchjs
|
||||
|
||||
// Initialize local variables
|
||||
let oppositeQty = 0
|
||||
// const amountUsd = 0
|
||||
// const amountBch = 0
|
||||
|
||||
// Calculate the amount in the opposite units.
|
||||
const currentUnit = sendCardData.amountUnits
|
||||
if (currentUnit.includes('USD')) {
|
||||
// Convert USD to BCH
|
||||
oppositeQty = bchjs.Util.floor8(amountQty / bchUsdPrice)
|
||||
// amountUsd = amountQty
|
||||
// amountBch = oppositeQty
|
||||
} else {
|
||||
// Convert BCH to USD
|
||||
oppositeQty = bchjs.Util.floor2(amountQty * bchUsdPrice)
|
||||
// amountUsd = oppositeQty
|
||||
// amountBch = amountQty
|
||||
}
|
||||
|
||||
// Update app state
|
||||
sendCardData.setOppositeQty(oppositeQty)
|
||||
} catch (err) {
|
||||
/* exit quietly */
|
||||
console.log('Error: ', err)
|
||||
}
|
||||
}
|
||||
|
||||
// This is a click event handler that toggles the units between BCH and USD.
|
||||
function handleSwitchUnits ({ sendCardData }) {
|
||||
// Toggle the unit
|
||||
let newUnit = ''
|
||||
let oppositeUnits = ''
|
||||
const oldUnit = sendCardData.amountUnits
|
||||
if (oldUnit.includes('USD')) {
|
||||
newUnit = 'BCH'
|
||||
oppositeUnits = 'USD'
|
||||
} else {
|
||||
newUnit = 'USD'
|
||||
oppositeUnits = 'BCH'
|
||||
}
|
||||
|
||||
// Clear the Amount text box
|
||||
sendCardData.setAmountStr('')
|
||||
sendCardData.setOppositeQty(0)
|
||||
|
||||
// Persist the new units.
|
||||
sendCardData.setAmountUnits(newUnit)
|
||||
sendCardData.setOppositeUnits(oppositeUnits)
|
||||
}
|
||||
|
||||
// Add a new line to the waiting modal.
|
||||
function addToModal (inStr, sendCardData) {
|
||||
sendCardData.setModalBody(prevBody => {
|
||||
prevBody.push(inStr)
|
||||
return prevBody
|
||||
})
|
||||
}
|
||||
|
||||
// Send BCH based to the address in the form, and the amount specified in the
|
||||
// form.
|
||||
async function handleSendBch ({ sendCardData, appData }) {
|
||||
console.log('Sending BCH')
|
||||
try {
|
||||
// Clear the modal body
|
||||
sendCardData.setModalBody([])
|
||||
sendCardData.setHideSpinner(false)
|
||||
|
||||
// Open the modal
|
||||
sendCardData.setHideModal(false)
|
||||
|
||||
let amountBch
|
||||
if (sendCardData.amountUnits === 'USD') {
|
||||
amountBch = sendCardData.oppositeQty
|
||||
} else {
|
||||
amountBch = parseFloat(sendCardData.amountStr)
|
||||
}
|
||||
console.log('amountBch: ', amountBch)
|
||||
|
||||
if (amountBch < 0.00000546) throw new Error('Trying to send less than dust.')
|
||||
|
||||
let bchAddr = sendCardData.bchAddr
|
||||
let infoStr = `Sending ${amountBch} BCH ($${sendCardData.amountUsd} USD) to ${bchAddr}`
|
||||
console.log(infoStr)
|
||||
|
||||
// Update modal
|
||||
addToModal('Preparing to send bch...', sendCardData)
|
||||
|
||||
const wallet = appData.wallet
|
||||
const bchjs = wallet.bchjs
|
||||
|
||||
// If the address is an SLP address, convert it to a cash address.
|
||||
if (bchAddr.includes('simpleledger:')) {
|
||||
bchAddr = bchjs.SLP.Address.toCashAddress(bchAddr)
|
||||
}
|
||||
|
||||
// Convert the BCH to satoshis
|
||||
const sats = bchjs.BitcoinCash.toSatoshi(amountBch)
|
||||
|
||||
// Update the wallets UTXOs
|
||||
infoStr = 'Updating UTXOs...'
|
||||
console.log(infoStr)
|
||||
addToModal(infoStr, sendCardData)
|
||||
await wallet.getUtxos()
|
||||
|
||||
const receivers = [{
|
||||
address: bchAddr,
|
||||
amountSat: sats
|
||||
}]
|
||||
const txid = await wallet.send(receivers)
|
||||
|
||||
// Display TXID
|
||||
infoStr = `txid: ${txid}`
|
||||
// console.log(infoStr)
|
||||
// modalBody.push(infoStr)
|
||||
addToModal(infoStr, sendCardData)
|
||||
|
||||
// Link to block explorer
|
||||
const explorerUrl = `https://blockchair.com/bitcoin-cash/transaction/${txid}`
|
||||
const explorerLink = (<a href={`${explorerUrl}`} target='_blank' rel='noreferrer'>Block Explorer</a>)
|
||||
// modalBody.push(explorerLink)
|
||||
addToModal(explorerLink, sendCardData)
|
||||
|
||||
sendCardData.setHideSpinner(true)
|
||||
sendCardData.setBchAddr('')
|
||||
sendCardData.setAmountStr('')
|
||||
} catch (err) {
|
||||
console.log('Error in handleSendBch(): ', err)
|
||||
|
||||
sendCardData.setModalBody([`Error: ${err.message}`])
|
||||
sendCardData.setHideSpinner(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
hideModal
|
||||
? null
|
||||
: (<WaitingModal
|
||||
heading='Sending BCH'
|
||||
body={modalBody}
|
||||
hideSpinner={hideSpinner}
|
||||
closeFunc={onModalClose}
|
||||
closeModalData={{ appData, sendCardData }}
|
||||
/>)
|
||||
}
|
||||
|
||||
<RefreshBchBalance appData={appData} ref={refreshBchBalanceRef} />
|
||||
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title style={{ textAlign: 'center' }}>
|
||||
<h2><FontAwesomeIcon icon={faPaperPlane} size='lg' /> Send</h2>
|
||||
</Card.Title>
|
||||
<br />
|
||||
|
||||
<Container>
|
||||
<Row>
|
||||
<Col style={{ textAlign: 'center' }}>
|
||||
<b>BCH Address:</b>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={12} style={{ textAlign: 'center' }}>
|
||||
<Form onSubmit={(e) => e.preventDefault()}>
|
||||
<Form.Group controlId='formBasicEmail' style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Form.Control
|
||||
style={{ marginRight: '1rem' }}
|
||||
type='text'
|
||||
placeholder='bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d'
|
||||
onChange={e => setBchAddr(e.target.value)}
|
||||
value={bchAddr}
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
style={{ cursor: 'pointer' }}
|
||||
icon={faPaste}
|
||||
size='lg'
|
||||
onClick={(e) => pasteFromClipboard()}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Col>
|
||||
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
<Row>
|
||||
<Col style={{ textAlign: 'center' }}>
|
||||
<b>Amount:</b>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<Form style={{ paddingBottom: '10px' }} onSubmit={(e) => { e.preventDefault(); handleSendBch({ sendCardData, appData }) }}>
|
||||
<Form.Group controlId='formBasicEmail' style={{ textAlign: 'center' }}>
|
||||
<Form.Control
|
||||
type='number'
|
||||
onChange={(event) => handleUpdateAmount({ event, appData, sendCardData })}
|
||||
value={amountStr}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<strong>Units</strong> : {amountUnits}
|
||||
<FontAwesomeIcon
|
||||
style={{ cursor: 'pointer', marginLeft: '5px' }}
|
||||
icon={faRandom}
|
||||
size='lg'
|
||||
onClick={(e) => handleSwitchUnits({ sendCardData, appData })}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={6} style={{ textAlign: 'right' }}>
|
||||
<strong>{oppositeUnits}</strong> : {oppositeQty}
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
<Row>
|
||||
<Col style={{ textAlign: 'center' }}>
|
||||
<Button onClick={(e) => handleSendBch({ sendCardData, appData })}>Send</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</Container>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SendCard
|
||||
@@ -77,7 +77,7 @@ const WalletImport = (props) => {
|
||||
<Container>
|
||||
<Row>
|
||||
<Col xs={12} className='text-break' style={{ textAlign: 'center' }}>
|
||||
<Form>
|
||||
<Form onSubmit={(e) => e.preventDefault()}>
|
||||
<Form.Group className='mb-3' controlId='formImportWallet' style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Form.Control
|
||||
style={{ margin: '1rem' }}
|
||||
|
||||
@@ -16,13 +16,16 @@ 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)
|
||||
const [blurredNostrPrivKey, setBlurredNostrPrivKey] = useState(true)
|
||||
|
||||
const [nostrKeyPair] = useState(bchWalletState.nostrKeyPair)
|
||||
|
||||
// Encapsulate component state into an object that can be passed to child functions
|
||||
const walletSummaryData = {
|
||||
@@ -64,6 +67,15 @@ function WalletSummary (props) {
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle the state of blurring for the private key
|
||||
const toggleNostrPrivateKeyBlur = (inObj = {}) => {
|
||||
try {
|
||||
setBlurredNostrPrivKey(!blurredNostrPrivKey)
|
||||
} catch (error) {
|
||||
console.error('Error toggling private key blur: ', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
@@ -151,6 +163,32 @@ function WalletSummary (props) {
|
||||
<CopyOnClick walletProp='hdPath' appData={appData} value={bchWalletState.hdPath} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{ padding: '25px', backgroundColor: '#eee' }}>
|
||||
<Col xs={10} sm={10} lg={8} style={{ padding: '10px' }}>
|
||||
<b>Nostr Priv Key:</b> <span className={blurredNostrPrivKey ? 'blurred' : null}>{nostrKeyPair.nsec}</span>
|
||||
</Col>
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
|
||||
<FontAwesomeIcon
|
||||
style={{ cursor: 'pointer' }}
|
||||
icon={eyeIcon.privateKey}
|
||||
size='lg'
|
||||
onClick={() => toggleNostrPrivateKeyBlur(nostrKeyPair.nsec)}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
|
||||
<CopyOnClick appData={appData} value={nostrKeyPair.nsec} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{ padding: '25px', backgroundColor: '#eee' }}>
|
||||
<Col xs={10} sm={10} lg={8} style={{ padding: '10px' }}>
|
||||
<b>Nostr Pub Key:</b> {nostrKeyPair.npub}
|
||||
</Col>
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }} />
|
||||
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
|
||||
<CopyOnClick appData={appData} value={nostrKeyPair.npub} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
This component is a View that allows the user to handle configuration
|
||||
settings for the app.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React from 'react'
|
||||
import ServerSelectView from './select-server-view'
|
||||
|
||||
function ConfigurationView (props) {
|
||||
const { appData } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<ServerSelectView appData={appData} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConfigurationView
|
||||
+2
-1
@@ -9,7 +9,7 @@ import { Container, Row, Col, Button } from 'react-bootstrap'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
const ServerSelect = (props) => {
|
||||
const { linkTo } = props
|
||||
const { linkTo, appData } = props
|
||||
|
||||
// Use the navigate function to navigate to the servers view
|
||||
const navigate = useNavigate()
|
||||
@@ -31,6 +31,7 @@ const ServerSelect = (props) => {
|
||||
<h5>
|
||||
Having trouble loading? Try selecting a different back-end server.
|
||||
</h5>
|
||||
<p style={{ fontStyle: 'italic' }}>Current Server : {appData.serverUrl}</p>
|
||||
<Button variant='warning' onClick={handleServerSelect}>
|
||||
Select a different back end server
|
||||
</Button>
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
This component is a View that allows the user to select a back end server
|
||||
from a list of servers.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState } from 'react'
|
||||
import { Row, Col, Form, Card } from 'react-bootstrap'
|
||||
|
||||
function ServerSelectView (props) {
|
||||
const { appData } = props
|
||||
const [selectedServer, setSelectedServer] = useState(appData.serverUrl)
|
||||
const servers = appData.servers
|
||||
|
||||
// Update server when dropdown selection changes
|
||||
const handleServerChange = (event) => {
|
||||
setSelectedServer(event.target.value)
|
||||
}
|
||||
|
||||
const onSaveServer = (serverUrl) => {
|
||||
console.log('server target: ', serverUrl)
|
||||
appData.updateLocalStorage({ serverUrl })
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className='m-3'>
|
||||
<Row className='mb-3 mt-3 mx-3'>
|
||||
<Col className='text-end'>
|
||||
<button
|
||||
className='btn btn-primary'
|
||||
style={{ minWidth: '100px' }}
|
||||
onClick={() => onSaveServer(selectedServer)}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card.Body>
|
||||
<Row>
|
||||
<Col style={{ textAlign: 'center' }}>
|
||||
<h2>Configuration</h2>
|
||||
<p>
|
||||
This page allows you to change configuration settings for different
|
||||
back end services. This page is for advanced users only.
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr />
|
||||
|
||||
<Row className='justify-content-center'>
|
||||
<Col xs={12} md={6}>
|
||||
<p>
|
||||
Select an alternative server below. The app will reload and use
|
||||
the selected server.
|
||||
</p>
|
||||
<Form.Select
|
||||
value={selectedServer}
|
||||
onChange={handleServerChange}
|
||||
className='mb-3'
|
||||
>
|
||||
{servers.map((server, i) => (
|
||||
<option key={`server-${i}`} value={server.value}>
|
||||
{server.value === appData.serverUrl ? `${server.label} (current)` : server.label}
|
||||
</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className='justify-content-center mt-3'>
|
||||
<Col xs={12} md={6} className='text-center'>
|
||||
<button
|
||||
className='btn btn-primary'
|
||||
style={{ minWidth: '100px' }}
|
||||
onClick={() => onSaveServer(selectedServer)}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServerSelectView
|
||||
@@ -15,8 +15,16 @@ import GetBalance from './balance'
|
||||
import Wallet from './bch-wallet'
|
||||
import Placeholder2 from './placeholder2'
|
||||
import Placeholder3 from './placeholder3'
|
||||
import ServerSelectView from './servers/select-server-view'
|
||||
import SelectServerButton from './servers/select-server-button'
|
||||
// import ServerSelectView from './servers/select-server-view'
|
||||
// import SelectServerButton from './servers/select-server-button'
|
||||
import NftsForSale from './nfts-for-sale'
|
||||
import BchSend from './bch-send'
|
||||
import SlpTokens from './slp-tokens'
|
||||
import SweepWif from './sweep/index.js'
|
||||
import SignMessage from './sign/index.js'
|
||||
import ServerSelectView from './configuration/select-server-view'
|
||||
import NostrPost from './nostr/nostr-post/index.js'
|
||||
import NostrRead from './nostr/nostr-read/index.js'
|
||||
|
||||
function AppBody (props) {
|
||||
// Dependency injection through props
|
||||
@@ -25,15 +33,24 @@ function AppBody (props) {
|
||||
return (
|
||||
<>
|
||||
<Routes>
|
||||
<Route path='/' element={<GetBalance wallet={appData.wallet} />} />
|
||||
<Route path='/' element={<NftsForSale appData={appData} />} />
|
||||
<Route path='/balance' element={<GetBalance wallet={appData.wallet} />} />
|
||||
<Route path='/bch' element={<BchSend appData={appData} />} />
|
||||
<Route path='/wallet' element={<Wallet appData={appData} />} />
|
||||
<Route path='/nfts-for-sale' element={<NftsForSale 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} />} />
|
||||
<Route path='/sweep' element={<SweepWif appData={appData} />} />
|
||||
<Route path='/sign' element={<SignMessage appData={appData} />} />
|
||||
<Route path='/configuration' element={<ServerSelectView appData={appData} />} />
|
||||
<Route path='/nostr-post' element={<NostrPost appData={appData} />} />
|
||||
<Route path='/nostr-read' element={<NostrRead appData={appData} />} />
|
||||
|
||||
</Routes>
|
||||
{/** Show in all paths except the servers view */}
|
||||
{appData.currentPath !== '/servers' && <SelectServerButton linkTo='/servers' />}
|
||||
{/* {appData.currentPath !== '/servers' && <SelectServerButton linkTo='/servers' appData={appData} />} */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
This component renders as a button. When clicked, it initiates the
|
||||
purchase of the token. This is the Signal part of the SWaP protocol.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState } from 'react'
|
||||
import { Button, Modal, Container, Row, Col, Spinner } from 'react-bootstrap'
|
||||
|
||||
function BuyButton (props) {
|
||||
const { token, appData, onSuccess } = props
|
||||
console.log('props: ', props)
|
||||
const [show, setShow] = useState(false) // show the modal
|
||||
const [onFetch, setOnFetch] = useState(false) // show the spinner
|
||||
const [error, setError] = useState(false) // show the error message
|
||||
// const [eventId, setEventId] = useState(false) // show the event id
|
||||
const [noteId, setNoteId] = useState(false) // show the note id
|
||||
const [success, setSuccess] = useState(false) // show the success message
|
||||
const [showConfirmation, setShowConfirmation] = useState(true) // show the confirmation view
|
||||
const [progressMsg, setProgressMsg] = useState([]) // show the progress message
|
||||
|
||||
// Handler for when user clicks the "Buy" button.
|
||||
const handleBuy = async () => {
|
||||
try {
|
||||
console.log('handleBuy()')
|
||||
console.log('token: ', token)
|
||||
console.log('appData: ', appData)
|
||||
setShowConfirmation(false)
|
||||
setOnFetch(true)
|
||||
|
||||
/* // dev-test success view
|
||||
setShowConfirmation(false)
|
||||
setSuccess(true)
|
||||
setNoteId('note12986q83gre76vl9dldpnnhej7y67h76xzw0tx0hm4mq6uradr6lskch7v9')
|
||||
setOnFetch(false)
|
||||
return
|
||||
*/
|
||||
|
||||
const progress = progressMsg
|
||||
progress.push(<p key='progress-msg1'>Generating counter offer transaction...</p>)
|
||||
setProgressMsg(progress)
|
||||
|
||||
const targetOffer = token.nostrEventId
|
||||
console.log('targetOffer: ', targetOffer)
|
||||
|
||||
// Generate a counter offer.
|
||||
const bchDexLib = appData.dexLib
|
||||
const { offerData, partialHex } = await bchDexLib.take.takeOffer(
|
||||
targetOffer
|
||||
)
|
||||
|
||||
progress.push(<p key='progress-msg2'>Uploading counter offer to Nostr...</p>)
|
||||
setProgressMsg(progress)
|
||||
|
||||
console.log('offerData: ', offerData)
|
||||
console.log('partialHex: ', partialHex)
|
||||
|
||||
// Upload the counter offer to Nostr.
|
||||
const nostr = appData.nostr
|
||||
const { eventId, noteId } = await nostr.testNostrUpload({
|
||||
offerData,
|
||||
partialHex
|
||||
})
|
||||
|
||||
console.log(
|
||||
`Counter Offer uploaded to Nostr with this event ID: ${eventId}`
|
||||
)
|
||||
console.log(`https://astral.psfoundation.info/${noteId}`)
|
||||
|
||||
setNoteId(noteId)
|
||||
setSuccess(true)
|
||||
setOnFetch(false)
|
||||
} catch (error) {
|
||||
setOnFetch(false)
|
||||
setError(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for when user requests to close the modal.
|
||||
const handleClose = () => {
|
||||
console.log('handleClose()')
|
||||
// Deny close if the purchase is in progress.
|
||||
if (onFetch) {
|
||||
return
|
||||
}
|
||||
|
||||
// Call onSuccess() callback if the modal close after a success purchase.
|
||||
if (success && onSuccess) {
|
||||
onSuccess()
|
||||
}
|
||||
// Reset the state.
|
||||
setError(false)
|
||||
setSuccess(false)
|
||||
setNoteId(false)
|
||||
setShow(false)
|
||||
setOnFetch(false)
|
||||
setShowConfirmation(true)
|
||||
}
|
||||
// Replace with dummy button until token data is loaded.
|
||||
if (!props.token.tokenData) {
|
||||
return (
|
||||
<>
|
||||
<Button variant='success'>Buy</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant='success'
|
||||
disabled={props.disabled}
|
||||
onClick={() => {
|
||||
setShow(true)
|
||||
}}
|
||||
>
|
||||
Buy
|
||||
</Button>
|
||||
|
||||
<Modal show={show} onHide={handleClose} dialogClassName='buy-modal'>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
{success
|
||||
? (
|
||||
<span style={{ color: 'green' }}>Successful Purchase</span>
|
||||
)
|
||||
: (
|
||||
'Processing Purchase'
|
||||
)}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Body className='text-center'>
|
||||
|
||||
{/** If the purchase is not confirmed, display the token details. and confirm button. */}
|
||||
{showConfirmation && (
|
||||
<Container className='text-start'>
|
||||
<Row>
|
||||
<Col xs={4}>
|
||||
<b>Ticker</b>:
|
||||
</Col>
|
||||
<Col xs={8}>{token.ticker}</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={4}>
|
||||
<b>Name</b>:
|
||||
</Col>
|
||||
<Col xs={8}>{props.token.tokenData.genesisData.name}</Col>
|
||||
</Row>
|
||||
<Row style={{ backgroundColor: '#eee' }}>
|
||||
<Col xs={4}>
|
||||
<b>Price</b>:
|
||||
</Col>
|
||||
<Col xs={8}>
|
||||
<strong>{token.usdPrice}</strong>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)}
|
||||
|
||||
{/** show purchase progress */}
|
||||
<Container style={{ wordBreak: 'break-word' }}>
|
||||
{onFetch && (
|
||||
<>
|
||||
<Spinner animation='border' variant='primary' />
|
||||
{progressMsg}
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<span style={{ color: 'red', fontStyle: 'italic' }}>{error}</span>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<>
|
||||
<p>
|
||||
Note ID :
|
||||
<a
|
||||
href={`https://astral.psfoundation.info/${noteId}`}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
{' '}
|
||||
{noteId}
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
What happens now:<br />
|
||||
The Seller software must finalize the sale by accepting the Counter Offer transaction you just generated.
|
||||
If their software is online, the token should appear in your wallet within a few minutes.
|
||||
Before it is accepted, you can cancel the purchase by Sweeping your Counter Offer UXTO back to your wallet.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer className='text-center'>
|
||||
{showConfirmation && (
|
||||
<Row style={{ width: '100%' }}>
|
||||
<Col xs={12} className='text-center'>
|
||||
<Button
|
||||
variant='success'
|
||||
style={{ minWidth: '100px' }}
|
||||
onClick={handleBuy}
|
||||
>
|
||||
Buy
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default BuyButton
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
Shows NFTs for sale on the DEX.
|
||||
|
||||
workflow:
|
||||
1. Load offers
|
||||
2. display token cards with current offers data
|
||||
3. Load tokens data
|
||||
4. add data to offers and update card with new data
|
||||
5. Load tokens icons
|
||||
6. add icons to offers and update card with new icons
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { Container, Row, Col, Button, Spinner } from 'react-bootstrap'
|
||||
import axios from 'axios'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faRedo } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
// Local libraries
|
||||
import config from '../../../config'
|
||||
import TokenCard from './token-card'
|
||||
// Global variables and constants
|
||||
const SERVER = config.dexServer
|
||||
|
||||
function NftsForSale (props) {
|
||||
// Dependency injection through props
|
||||
const appData = props.appData
|
||||
|
||||
const [offers, setOffers] = useState([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [offersAreLoaded, setOffersAreLoaded] = useState(false)
|
||||
const [iconsAreLoaded, setIconsAreLoaded] = useState(false)
|
||||
const [dataAreLoaded, setDataAreLoaded] = useState(false)
|
||||
|
||||
// Handler for refresh button
|
||||
const handleRefresh = () => {
|
||||
console.log('handling refresh')
|
||||
loadNftOffers()
|
||||
}
|
||||
|
||||
// Function to process token data
|
||||
const processTokenData = useCallback(async (offer) => {
|
||||
try {
|
||||
const { wallet, bchWalletState } = appData
|
||||
const { bchjs } = wallet
|
||||
|
||||
// Calculate USD price
|
||||
const rateInSats = parseInt(offer.rateInBaseUnit)
|
||||
const bchCost = bchjs.BitcoinCash.toBitcoinCash(rateInSats)
|
||||
const usdPrice = bchCost * bchWalletState.bchUsdPrice * offer.numTokens
|
||||
offer.usdPrice = `$${usdPrice.toFixed(3)}`
|
||||
|
||||
return offer
|
||||
} catch (err) {
|
||||
console.error('Error processing token:', err)
|
||||
return offer
|
||||
}
|
||||
}, [appData])
|
||||
|
||||
// Fetch offers
|
||||
const getNftOffers = useCallback(async (page = 0) => {
|
||||
try {
|
||||
setOffersAreLoaded(false)
|
||||
|
||||
const result = await axios.get(`${SERVER}/offer/list/nft/${page}`)
|
||||
const rawOffers = result.data
|
||||
console.log('rawOffers: ', rawOffers)
|
||||
|
||||
// Process each offer
|
||||
const processedOffers = []
|
||||
for (let i = 0; i < rawOffers.length; i++) {
|
||||
const offer = rawOffers[i]
|
||||
const processedOffer = await processTokenData(offer)
|
||||
processedOffers.push(processedOffer)
|
||||
}
|
||||
|
||||
setOffersAreLoaded(true)
|
||||
|
||||
return processedOffers
|
||||
} catch (err) {
|
||||
console.error('getNftOffers error:', err)
|
||||
setOffersAreLoaded(true)
|
||||
throw err
|
||||
}
|
||||
}, [processTokenData])
|
||||
|
||||
// This function loads the token data .
|
||||
const lazyLoadTokenData = useCallback(async (tokens) => {
|
||||
try {
|
||||
setDataAreLoaded(false)
|
||||
// map each token and fetch the token data
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const thisToken = tokens[i]
|
||||
|
||||
// data does not need to be downloaded, so continue with the next one
|
||||
if (thisToken.dataAlreadyDownloaded) continue
|
||||
|
||||
// Try to get token data.
|
||||
const tokenData = await appData.wallet.getTokenData(thisToken.tokenId)
|
||||
|
||||
if (tokenData) {
|
||||
// Set data to the token object , this can be used to display the token name in the token card component.
|
||||
thisToken.tokenData = tokenData
|
||||
}
|
||||
|
||||
// Mark token to prevent fetch token data again.
|
||||
thisToken.dataAlreadyDownloaded = true
|
||||
}
|
||||
|
||||
setDataAreLoaded(true)
|
||||
} catch (error) {
|
||||
setDataAreLoaded(true)
|
||||
}
|
||||
}, [appData])
|
||||
|
||||
// Fetch mutable data if it exist and get the token icon url
|
||||
const fetchTokenInfo = useCallback(async (token) => {
|
||||
try {
|
||||
// Get the token data
|
||||
const tokenData = token.tokenData
|
||||
|
||||
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 })
|
||||
console.log('json: ', json)
|
||||
if (!json) return false
|
||||
|
||||
let iconUrl = json.tokenIcon
|
||||
|
||||
if (json.fullSizedUrl && json.fullSizedUrl.includes('http')) {
|
||||
iconUrl = json.fullSizedUrl
|
||||
}
|
||||
// Return icon url
|
||||
return iconUrl
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}, [appData])
|
||||
|
||||
// This function loads the token icons from the ipfs gateways.
|
||||
const lazyLoadTokenIcons = useCallback(async (tokens) => {
|
||||
try {
|
||||
setIconsAreLoaded(false)
|
||||
// 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 fetchTokenInfo(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
|
||||
}
|
||||
|
||||
setIconsAreLoaded(true)
|
||||
} catch (error) {
|
||||
setIconsAreLoaded(true)
|
||||
}
|
||||
}, [fetchTokenInfo])
|
||||
|
||||
// Main function to load NFT offers
|
||||
const loadNftOffers = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
// Get tokens in offers
|
||||
const offers = await getNftOffers()
|
||||
console.log('offers: ', offers)
|
||||
setOffers(offers)
|
||||
// Load tokens data
|
||||
await lazyLoadTokenData(offers)
|
||||
// Load tokens icons
|
||||
await lazyLoadTokenIcons(offers)
|
||||
setIsLoading(false)
|
||||
} catch (err) {
|
||||
setIsLoading(false)
|
||||
console.error('Error loading NFT offers:', err)
|
||||
}
|
||||
}, [lazyLoadTokenData, lazyLoadTokenIcons, getNftOffers])
|
||||
|
||||
// Effect to load NFTs on component mount
|
||||
useEffect(() => {
|
||||
console.log('loading nfts for sale')
|
||||
loadNftOffers()
|
||||
}, [loadNftOffers])
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// This function generates a Token Card for each token in the wallet.
|
||||
function generateCards (offers) {
|
||||
console.log('generateCards() offerData: ', offers)
|
||||
|
||||
const tokens = offers
|
||||
|
||||
const tokenCards = []
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const thisToken = tokens[i]
|
||||
|
||||
const thisTokenCard = (
|
||||
<TokenCard
|
||||
appData={appData}
|
||||
token={thisToken}
|
||||
handleRefresh={handleRefresh}
|
||||
key={`${thisToken.tokenId + i}`}
|
||||
/>
|
||||
)
|
||||
tokenCards.push(thisTokenCard)
|
||||
}
|
||||
|
||||
return tokenCards
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<h1>NFTs for Sale</h1>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={12} style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
{/** Show spinner info if tokens are loaded but data is not loaded */
|
||||
offersAreLoaded && !dataAreLoaded && (
|
||||
<div style={{ borderRadius: '10px', backgroundColor: '#f0f0f0', padding: '10px', display: 'flex', justifyContent: 'center', alignItems: 'center', width: 'fit-content' }}>
|
||||
<span style={{ marginRight: '10px' }}>Loading Token Data </span>
|
||||
<Spinner animation='border' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{/** Show spinner info if tokens are loaded but icons are not loaded */
|
||||
dataAreLoaded && !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>
|
||||
|
||||
{!isLoading && (
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<Button variant='success' onClick={handleRefresh}>
|
||||
<FontAwesomeIcon icon={faRedo} size='lg' /> Refresh
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{!offersAreLoaded && (
|
||||
<Row className='d-block text-center'>
|
||||
<Spinner animation='border' variant='primary' style={{ maegin: '0 auto' }} />
|
||||
</Row>
|
||||
)}
|
||||
<Row>
|
||||
{offersAreLoaded && generateCards(offers)}
|
||||
</Row>
|
||||
|
||||
{/** Display a message if no tokens are found */}
|
||||
{offersAreLoaded && offers.length === 0 && (
|
||||
<Row className='text-center'>
|
||||
<span> No Offers found. </span>
|
||||
</Row>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default NftsForSale
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
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'
|
||||
|
||||
function InfoButton (props) {
|
||||
const [show, setShow] = useState(false)
|
||||
|
||||
const handleClose = () => {
|
||||
console.log('handleClose()')
|
||||
setShow(false)
|
||||
// props.instance.setState({ showModal: false })
|
||||
}
|
||||
|
||||
const handleOpen = () => {
|
||||
console.log('handleOpen()')
|
||||
setShow(true)
|
||||
}
|
||||
|
||||
// Replace with dummy button until token data is loaded.
|
||||
if (!props.token.tokenData) {
|
||||
return (
|
||||
<>
|
||||
<Button variant='info'>Info</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant='info' disabled={props.disabled} 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.tokenData.genesisData.name}</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={4}><b>Token ID</b>:</Col>
|
||||
<Col xs={8} style={{ wordBreak: 'break-all' }}>
|
||||
<a href={`https://explorer.tokentiger.com/?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.tokenData.genesisData.decimals}</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={4}><b>Token Type</b>:</Col>
|
||||
<Col xs={8}>{props.token.tokenType}</Col>
|
||||
</Row>
|
||||
|
||||
</Container>
|
||||
</Modal.Body>
|
||||
<Modal.Footer />
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default InfoButton
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
This Card component summarizes an SLP token.
|
||||
*/
|
||||
|
||||
// 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 BuyButton from './buy-button'
|
||||
|
||||
function TokenCard (props) {
|
||||
const { token, appData, handleRefresh } = props
|
||||
const [icon, setIcon] = useState(token.icon)
|
||||
const [tokenData, setTokenData] = useState(token.tokenData)
|
||||
|
||||
console.log('TokenCard() appData: ', appData)
|
||||
|
||||
// Update icon state every token.icon and token.tokenData changes
|
||||
useEffect(() => {
|
||||
setIcon(token.icon)
|
||||
setTokenData(token.tokenData)
|
||||
}, [token.icon, token.tokenData])
|
||||
|
||||
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', marginTop: '10px' }}>
|
||||
<h4>{token.ticker}</h4>
|
||||
</Card.Title>
|
||||
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
{tokenData && tokenData.genesisData ? tokenData.genesisData.name : null}
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
<Row>
|
||||
<Col>Price:</Col>
|
||||
<Col>{token.usdPrice}</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
<InfoButton token={token} disabled={!token.tokenData} />
|
||||
</Col>
|
||||
|
||||
<Col />
|
||||
|
||||
<Col>
|
||||
<BuyButton token={token} disabled={!token.tokenData} appData={appData} onSuccess={handleRefresh} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</Container>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</>
|
||||
)
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
{/* <Col>
|
||||
<InfoButton token={props.token} />
|
||||
</Col>
|
||||
|
||||
<Col>
|
||||
<FlagButton appData={props.appData} offer={props.token} />
|
||||
</Col>
|
||||
|
||||
<Col>
|
||||
<BuyNftButton appData={props.appData} offer={props.token} />
|
||||
</Col> */ }
|
||||
|
||||
export default TokenCard
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Component for posting nostr information.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React from 'react'
|
||||
import { Container } from 'react-bootstrap'
|
||||
import ProfilePost from './profile-post'
|
||||
import PublicPost from './public-post'
|
||||
|
||||
function NostrPost (props) {
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<h2 style={{
|
||||
textAlign: 'center',
|
||||
margin: '20px 0',
|
||||
padding: '10px',
|
||||
borderBottom: '2px solid #ccc'
|
||||
}}
|
||||
>
|
||||
Nostr Post
|
||||
</h2>
|
||||
<ProfilePost {...props} />
|
||||
<PublicPost {...props} />
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NostrPost
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
Component for posting nostr information on kind 0.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState } from 'react'
|
||||
import { Container, Form, Button, Spinner } from 'react-bootstrap'
|
||||
import Accordion from 'react-bootstrap/Accordion'
|
||||
import { finalizeEvent } from 'nostr-tools/pure'
|
||||
import { Relay } from 'nostr-tools/relay'
|
||||
import { hexToBytes } from '@noble/hashes/utils' // already an installed dependency
|
||||
|
||||
function ProfilePost (props) {
|
||||
const [accordionKey, setAccordionKey] = useState(null)
|
||||
const [onFetch, setOnFetch] = useState(false)
|
||||
const { bchWalletState } = props.appData
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
about: ''
|
||||
})
|
||||
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
const [successMsg, setSuccessMsg] = useState('')
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value
|
||||
})
|
||||
|
||||
setSuccessMsg('')
|
||||
setErrorMsg('')
|
||||
}
|
||||
// Post on nostr network
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
try {
|
||||
setErrorMsg('')
|
||||
setSuccessMsg('')
|
||||
setOnFetch(true)
|
||||
|
||||
const { nostrKeyPair } = bchWalletState
|
||||
|
||||
// Convert private key to binary
|
||||
const privateKeyBin = hexToBytes(nostrKeyPair.privHex)
|
||||
|
||||
// Relay list
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
|
||||
const formDataString = JSON.stringify(formData)
|
||||
|
||||
// Generate a post.
|
||||
const eventTemplate = {
|
||||
kind: 0,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
content: formDataString
|
||||
}
|
||||
console.log(`eventTemplate: ${JSON.stringify(eventTemplate, null, 2)}`)
|
||||
|
||||
// Sign the post
|
||||
const signedEvent = finalizeEvent(eventTemplate, privateKeyBin)
|
||||
console.log('signedEvent: ', signedEvent)
|
||||
|
||||
// Connect to a relay.
|
||||
const relay = await Relay.connect(psf)
|
||||
console.log(`connected to ${relay.url}`)
|
||||
|
||||
// Publish the message to the relay.
|
||||
const result = await relay.publish(signedEvent)
|
||||
console.log('result: ', result)
|
||||
|
||||
// Close the connection to the relay.
|
||||
relay.close()
|
||||
resetForm()
|
||||
setSuccessMsg('Post successfully published!')
|
||||
setOnFetch(false)
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
setErrorMsg(error.message || 'An error occurred while posting')
|
||||
setOnFetch(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAccordionChange = (key) => {
|
||||
setAccordionKey(key)
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
name: '',
|
||||
about: ''
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container className='mt-4'>
|
||||
<Accordion activeKey={accordionKey} onSelect={handleAccordionChange}>
|
||||
<Accordion.Item eventKey='0'>
|
||||
<Accordion.Header>Profile Post</Accordion.Header>
|
||||
<Accordion.Body>
|
||||
{errorMsg && (
|
||||
<div className='alert alert-danger' role='alert'>
|
||||
{errorMsg}
|
||||
</div>
|
||||
)}
|
||||
{successMsg && (
|
||||
<div className='alert alert-success' role='alert'>
|
||||
{successMsg}
|
||||
</div>
|
||||
)}
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group className='mb-3'>
|
||||
<Form.Label>Name</Form.Label>
|
||||
<Form.Control
|
||||
type='text'
|
||||
placeholder='Enter your name'
|
||||
name='name'
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className='mb-3'>
|
||||
<Form.Label>About</Form.Label>
|
||||
<Form.Control
|
||||
as='textarea'
|
||||
rows={3}
|
||||
placeholder='Tell us about yourself'
|
||||
name='about'
|
||||
value={formData.about}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<div className='d-flex justify-content-center'>
|
||||
{!onFetch && (
|
||||
<Button variant='primary' type='submit' disabled={onFetch}>
|
||||
Post
|
||||
</Button>
|
||||
)}
|
||||
{onFetch && <Spinner animation='border' variant='primary' />}
|
||||
</div>
|
||||
|
||||
</Form>
|
||||
</Accordion.Body>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfilePost
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
Component for posting nostr information on kind 1.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState } from 'react'
|
||||
import { Container, Form, Button, Spinner } from 'react-bootstrap'
|
||||
import Accordion from 'react-bootstrap/Accordion'
|
||||
import { finalizeEvent } from 'nostr-tools/pure'
|
||||
import { Relay } from 'nostr-tools/relay'
|
||||
import { hexToBytes } from '@noble/hashes/utils' // already an installed dependency
|
||||
|
||||
function PublicPost (props) {
|
||||
const [accordionKey, setAccordionKey] = useState(null)
|
||||
const [onFetch, setOnFetch] = useState(false)
|
||||
const { bchWalletState } = props.appData
|
||||
const [formData, setFormData] = useState({
|
||||
content: ''
|
||||
})
|
||||
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
const [successMsg, setSuccessMsg] = useState('')
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value
|
||||
})
|
||||
|
||||
setSuccessMsg('')
|
||||
setErrorMsg('')
|
||||
}
|
||||
// Post on nostr network
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
try {
|
||||
setErrorMsg('')
|
||||
setSuccessMsg('')
|
||||
setOnFetch(true)
|
||||
|
||||
const { nostrKeyPair } = bchWalletState
|
||||
|
||||
// Convert private key to binary
|
||||
const privateKeyBin = hexToBytes(nostrKeyPair.privHex)
|
||||
|
||||
// Relay list
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
|
||||
// Generate a post.
|
||||
const eventTemplate = {
|
||||
kind: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [['t', 'slpdex-socialmedia']],
|
||||
content: formData.content
|
||||
}
|
||||
console.log(`eventTemplate: ${JSON.stringify(eventTemplate, null, 2)}`)
|
||||
|
||||
// Sign the post
|
||||
const signedEvent = finalizeEvent(eventTemplate, privateKeyBin)
|
||||
console.log('signedEvent: ', signedEvent)
|
||||
|
||||
// Connect to a relay.
|
||||
const relay = await Relay.connect(psf)
|
||||
console.log(`connected to ${relay.url}`)
|
||||
|
||||
// Publish the message to the relay.
|
||||
const result = await relay.publish(signedEvent)
|
||||
console.log('result: ', result)
|
||||
|
||||
// Close the connection to the relay.
|
||||
relay.close()
|
||||
resetForm()
|
||||
setSuccessMsg('Post successfully published!')
|
||||
setOnFetch(false)
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
setErrorMsg(error.message || 'An error occurred while posting')
|
||||
setOnFetch(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAccordionChange = (key) => {
|
||||
setAccordionKey(key)
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
content: ''
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container className='mt-4'>
|
||||
<Accordion activeKey={accordionKey} onSelect={handleAccordionChange}>
|
||||
<Accordion.Item eventKey='0'>
|
||||
<Accordion.Header>Public Post</Accordion.Header>
|
||||
<Accordion.Body>
|
||||
{errorMsg && (
|
||||
<div className='alert alert-danger' role='alert'>
|
||||
{errorMsg}
|
||||
</div>
|
||||
)}
|
||||
{successMsg && (
|
||||
<div className='alert alert-success' role='alert'>
|
||||
{successMsg}
|
||||
</div>
|
||||
)}
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group className='mb-3'>
|
||||
<Form.Label>Message</Form.Label>
|
||||
<Form.Control
|
||||
as='textarea'
|
||||
rows={7}
|
||||
placeholder="What's happening?"
|
||||
name='content'
|
||||
value={formData.content}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<div className='d-flex justify-content-center'>
|
||||
{!onFetch && (
|
||||
<Button variant='primary' type='submit' disabled={onFetch}>
|
||||
Post
|
||||
</Button>
|
||||
)}
|
||||
{onFetch && <Spinner animation='border' variant='primary' />}
|
||||
</div>
|
||||
|
||||
</Form>
|
||||
</Accordion.Body>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PublicPost
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
Component for read nostr information.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState } from 'react'
|
||||
import { Container } from 'react-bootstrap'
|
||||
import ProfileRead from './profile-read.js'
|
||||
import PublicRead from './public-read.js'
|
||||
|
||||
function NostrRead (props) {
|
||||
const [profile, setProfile] = useState(false)
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<ProfileRead {...props} setProfile={setProfile} />
|
||||
<PublicRead {...props} profile={profile} />
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NostrRead
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Component for read nostr information kind 0
|
||||
*/
|
||||
// Global npm libraries
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Container, Button } from 'react-bootstrap'
|
||||
import CopyOnClick from '../../bch-wallet/copy-on-click.js'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { RelayPool } from 'nostr'
|
||||
|
||||
function ProfileRead (props) {
|
||||
const { bchWalletState } = props.appData
|
||||
const { setProfile } = props
|
||||
const [post, setPost] = useState({})
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const start = () => {
|
||||
const { nostrKeyPair } = bchWalletState
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
|
||||
const pool = RelayPool([psf])
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('subid', { limit: 5, kinds: [0], authors: [nostrKeyPair.pubHex] })
|
||||
})
|
||||
|
||||
pool.on('eose', relay => {
|
||||
console.log('Closing Relay')
|
||||
relay.close()
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
console.log('Received event:', ev)
|
||||
const profile = JSON.parse(ev.content)
|
||||
setPost(profile)
|
||||
setProfile(profile)
|
||||
})
|
||||
|
||||
setLoaded(true)
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
start()
|
||||
}
|
||||
}, [bchWalletState, loaded, setProfile])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className='d-flex flex-column flex-md-row align-items-center gap-4 mb-5 p-3 p-md-5 bg-light rounded-4 shadow-sm'>
|
||||
<div style={{ width: '100px', height: '100px' }} className='mb-3 mb-md-0'>
|
||||
<div
|
||||
className='rounded-circle bg-gradient shadow d-flex align-items-center justify-content-center'
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'linear-gradient(45deg, #6c757d, #495057)'
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUser} size='3x' color='#7c7c7d' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-grow-1 text-center text-md-start mb-3 mb-md-0 d-flex flex-column align-items-center align-items-md-start'>
|
||||
<h3 className='mb-2 fw-bold'>{post.name}</h3>
|
||||
<div className='text-muted small mb-3 d-flex align-items-center flex-column flex-md-row'>
|
||||
<span className='text-truncate me-2 mb-2 mb-md-0'>
|
||||
<span className='d-md-none'>
|
||||
{`${bchWalletState.nostrKeyPair.npub.slice(0, 8)}...${bchWalletState.nostrKeyPair.npub.slice(-5)}`}
|
||||
</span>
|
||||
<span className='d-none d-md-inline'>
|
||||
{bchWalletState.nostrKeyPair.npub}
|
||||
</span>
|
||||
</span>
|
||||
<CopyOnClick walletProp='npub' appData={props.appData} value={bchWalletState.nostrKeyPair.npub} />
|
||||
</div>
|
||||
<div className='fs-6 text-secondary'>{post.about}</div>
|
||||
</div>
|
||||
<div className='d-flex gap-2 align-items-center flex-wrap justify-content-center'>
|
||||
<Button
|
||||
variant='outline-danger'
|
||||
className='px-3 px-md-4 py-2 rounded-pill fw-semibold'
|
||||
style={{ minWidth: '120px' }}
|
||||
>
|
||||
<i className='bi bi-person-plus me-2' />
|
||||
Follow
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='px-3 px-md-4 py-2 rounded-pill fw-semibold'
|
||||
style={{ minWidth: '120px' }}
|
||||
>
|
||||
<i className='bi bi-chat-dots me-2' />
|
||||
Message
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfileRead
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Component for read nostr information kind 1
|
||||
*/
|
||||
// Global npm libraries
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Container, Card } from 'react-bootstrap'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { RelayPool } from 'nostr'
|
||||
|
||||
function PublicRead (props) {
|
||||
const { bchWalletState } = props.appData
|
||||
const [posts, setPosts] = useState([])
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Get Last post from a author
|
||||
const start = () => {
|
||||
const { nostrKeyPair } = bchWalletState
|
||||
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
|
||||
const pool = RelayPool([psf])
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('subid', { limit: 5, kinds: [1], authors: [nostrKeyPair.pubHex] })
|
||||
})
|
||||
|
||||
pool.on('eose', relay => {
|
||||
console.log('Closing Relay')
|
||||
relay.close()
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
console.log('Received event:', ev)
|
||||
setPosts(currentPosts => [...currentPosts, ev])
|
||||
})
|
||||
|
||||
setLoaded(true)
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
start()
|
||||
}
|
||||
}, [bchWalletState, loaded])
|
||||
|
||||
return (
|
||||
<Container className='mt-4'>
|
||||
<div>
|
||||
{posts.map((post, index) => (
|
||||
<Card key={index} className='mb-4 bg-light rounded-4 shadow-sm border-0'>
|
||||
<Card.Body className='p-3'>
|
||||
<div className='d-flex align-items-center gap-3 mb-3'>
|
||||
<div
|
||||
className='rounded-circle bg-gradient shadow d-flex align-items-center justify-content-center'
|
||||
style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
background: 'linear-gradient(45deg, #6c757d, #495057)'
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUser} size='1x' color='#7c7c7d' />
|
||||
</div>
|
||||
<div className='flex-grow-1'>
|
||||
<div className='fw-bold mb-1'>{props.profile?.name}</div>
|
||||
<small className='text-muted'>
|
||||
{`${bchWalletState.nostrKeyPair.npub.slice(0, 8)}...${bchWalletState.nostrKeyPair.npub.slice(-5)}`}
|
||||
</small>
|
||||
</div>
|
||||
<small className='text-muted'>
|
||||
{new Date(post.created_at * 1000).toLocaleString()}
|
||||
</small>
|
||||
</div>
|
||||
<div className='mb-4'>
|
||||
<div className='fs-5 ms-5 text-dark' style={{ lineHeight: '1.3' }}>
|
||||
{post.content}
|
||||
</div>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
))}
|
||||
{!posts.length && loaded && (
|
||||
<div className='text-center text-muted py-5 bg-light rounded-4 shadow-sm'>
|
||||
<i className='bi bi-inbox-fill fs-1 mb-3 d-block opacity-50' />
|
||||
<div>No posts found</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default PublicRead
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
This component is a View that allows the user to select a back end server
|
||||
from a list of servers.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React from 'react'
|
||||
import { Container, Row, Col, Button } from 'react-bootstrap'
|
||||
|
||||
function ServerSelectView (props) {
|
||||
const { appData } = props
|
||||
const selectedServer = appData.serverUrl // Selected server
|
||||
const servers = appData.servers // List of servers
|
||||
|
||||
// Update location ref after server url changes
|
||||
const handleReloadServer = (event) => {
|
||||
const target = event.target.id
|
||||
console.log('server target: ', target)
|
||||
|
||||
window.location.href = `/?restURL=${target}`
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col style={{ textAlign: 'center' }}>
|
||||
<h2>
|
||||
Select Alternative Server
|
||||
</h2>
|
||||
<p>
|
||||
Select an alternative server below. The app will reload and use
|
||||
the selected server.
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
{
|
||||
/** Map all server data and render a row for each server */
|
||||
servers.map((server, i) => {
|
||||
// Background color for each row
|
||||
const isEven = i % 2
|
||||
const backgroundColor = isEven ? '#eee' : '#fff'
|
||||
|
||||
return (
|
||||
<div key={`server-${i}`}>
|
||||
<Row
|
||||
style={{
|
||||
padding: '25px',
|
||||
backgroundColor
|
||||
}}
|
||||
>
|
||||
<Col xs={4}>
|
||||
<Button
|
||||
style={{ minWidth: 120 }}
|
||||
id={server.value}
|
||||
onClick={handleReloadServer}
|
||||
>
|
||||
{selectedServer === server.value ? 'Current' : 'Select'}
|
||||
</Button>
|
||||
</Col>
|
||||
|
||||
<Col xs={8}>
|
||||
{server.label}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServerSelectView
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
Component for signing a message with a WIF private key.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState } from 'react'
|
||||
import { Container, Row, Col, Form, Button } from 'react-bootstrap'
|
||||
import { faCopy } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
|
||||
function SignMessage (props) {
|
||||
// Convert class state to hooks
|
||||
const { wallet, appUtil } = props.appData
|
||||
|
||||
const [sign, setSign] = useState('')
|
||||
const [msg, setMsg] = useState('')
|
||||
const [bchAddr] = useState(wallet.walletInfo.cashAddress)
|
||||
const [slpAddr] = useState(wallet.walletInfo.slpAddress)
|
||||
const [err, setErr] = useState('')
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleSignMessage = (event) => {
|
||||
try {
|
||||
event.preventDefault()
|
||||
|
||||
if (!msg) throw new Error('Enter a message to sign.')
|
||||
|
||||
const bchjs = wallet.bchjs
|
||||
|
||||
const wif = props.appData.wallet.walletInfo.privateKey
|
||||
const sig = bchjs.BitcoinCash.signMessageWithPrivKey(wif, msg)
|
||||
|
||||
setSign(sig)
|
||||
setErr('')
|
||||
} catch (err) {
|
||||
console.log('Error in handleSignMessage(): ', err)
|
||||
setErr(err.message)
|
||||
setSign('')
|
||||
}
|
||||
}
|
||||
|
||||
// Function to copy the value to the clipboard.
|
||||
const handleCopyToClipboard = async (value) => {
|
||||
appUtil.copyToClipboard(value)
|
||||
|
||||
// show the copied message
|
||||
setCopied(true)
|
||||
|
||||
// hide copied message after 1 second
|
||||
setTimeout(function () {
|
||||
setCopied(false)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const copyIcon = (value) => {
|
||||
return <FontAwesomeIcon icon={faCopy} size='lg' onClick={() => handleCopyToClipboard(value)} style={{ cursor: 'pointer', marginLeft: '10px' }} />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<p>
|
||||
This view allows you cryptographically sign a message with your
|
||||
wallet. These signatures are used in a wide range of applications,
|
||||
such as gaining access to
|
||||
the <a href='https://t.me/psf_vip' target='_blank' rel='noreferrer'>PSF VIP Telegram channel</a>.
|
||||
</p>
|
||||
<p>
|
||||
Enter any message into the form below and click the button. This
|
||||
view will generate a cryptographic signature.
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col className='text-break'>
|
||||
<Form onSubmit={handleSignMessage}>
|
||||
<Form.Group className='mb-3' controlId='message'>
|
||||
<Form.Label><b>Enter a message to sign.</b></Form.Label>
|
||||
<Form.Control type='text' placeholder='' onChange={e => setMsg(e.target.value)} />
|
||||
</Form.Group>
|
||||
{err && <p style={{ color: 'red', marginBottom: '10px' }}>{`Error: ${err}`}</p>}
|
||||
|
||||
<Button variant='primary' onClick={handleSignMessage}>
|
||||
Sign Message
|
||||
</Button>
|
||||
</Form>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
{sign && (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Row>
|
||||
<Col>
|
||||
<p>
|
||||
<b>Signature:</b> {sign} {copyIcon(sign)}
|
||||
</p>
|
||||
<p>
|
||||
<b>BCH Address:</b> {bchAddr} {copyIcon(bchAddr)}
|
||||
</p>
|
||||
<p>
|
||||
<b>SLP Address:</b> {slpAddr} {copyIcon(slpAddr)}
|
||||
</p>
|
||||
</Col>
|
||||
|
||||
</Row>
|
||||
{copied && (
|
||||
<span style={{ color: 'green' }}>
|
||||
Copied!
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SignMessage
|
||||
@@ -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,230 @@
|
||||
/*
|
||||
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 (e) => {
|
||||
e.preventDefault()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Load address from clipboard
|
||||
const pasteFromClipboard = async () => {
|
||||
try {
|
||||
const address = await appData.appUtil.readFromClipboard()
|
||||
setSendToAddress(address)
|
||||
} catch (err) {
|
||||
console.warn('Error pasting from clipboard: ', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 onSubmit={(e) => e.preventDefault()}>
|
||||
<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
|
||||
style={{ cursor: 'pointer' }}
|
||||
icon={faPaste}
|
||||
size='lg'
|
||||
onClick={pasteFromClipboard}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<Row>
|
||||
<Col style={{ textAlign: 'center' }}>
|
||||
<b>Amount:</b>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col xs={10}>
|
||||
<Form style={{ paddingBottom: '10px' }} onSubmit={handleSendTokens}>
|
||||
<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
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
This Sweep component allows users to sweep a private key and transfer any
|
||||
BCH or SLP tokens into their wallet.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React from 'react'
|
||||
import { Container, Row, Col, Form, Button, Modal, Spinner } from 'react-bootstrap'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faCircleQuestion } from '@fortawesome/free-solid-svg-icons'
|
||||
import Sweeper from 'bch-token-sweep'
|
||||
|
||||
// let _this
|
||||
|
||||
const SweepWif = (props) => {
|
||||
const { appData } = props
|
||||
console.log('appData', appData)
|
||||
const [wifToSweep, setWifToSweep] = React.useState('')
|
||||
const [showModal, setShowModal] = React.useState(false)
|
||||
const [statusMsg, setStatusMsg] = React.useState('')
|
||||
const [hideSpinner, setHideSpinner] = React.useState(false)
|
||||
|
||||
// shouldRefreshOnModalClose: false
|
||||
|
||||
// Helper function to validate WIF
|
||||
const validateWIF = (WIF) => {
|
||||
if (typeof WIF !== 'string') return false
|
||||
if (WIF.length !== 52) return false
|
||||
if (WIF[0] !== 'L' && WIF[0] !== 'K') return false
|
||||
return true
|
||||
}
|
||||
|
||||
// Update wallet state function
|
||||
const updateWalletState = async () => {
|
||||
const wallet = appData.wallet
|
||||
const bchBalance = await wallet.getBalance({ bchAddress: wallet.walletInfo.cashAddress })
|
||||
await wallet.initialize()
|
||||
const slpTokens = await wallet.listTokens(wallet.walletInfo.cashAddress)
|
||||
appData.updateBchWalletState({ walletObj: { bchBalance, slpTokens }, appData })
|
||||
}
|
||||
|
||||
// Handle sweep function
|
||||
const handleSweep = async (e) => {
|
||||
if (e) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Sweeping this WIF: ${wifToSweep}`)
|
||||
|
||||
// Set modal initial state
|
||||
setShowModal(true)
|
||||
setHideSpinner(false)
|
||||
setStatusMsg('')
|
||||
|
||||
// Input validation
|
||||
const isWIF = validateWIF(wifToSweep)
|
||||
if (!isWIF) {
|
||||
setHideSpinner(true)
|
||||
setStatusMsg(<b style={{ color: 'red' }}>Input is not a WIF private key.</b>)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const walletWif = appData.wallet.walletInfo.privateKey
|
||||
const toAddr = appData.wallet.slpAddress
|
||||
|
||||
// Instance the Sweep library
|
||||
const sweep = new Sweeper(wifToSweep, walletWif, appData.wallet)
|
||||
await sweep.populateObjectFromNetwork()
|
||||
|
||||
// Constructing the sweep transaction
|
||||
const hex = await sweep.sweepTo(toAddr)
|
||||
const txid = await appData.wallet.ar.sendTx(hex)
|
||||
|
||||
// Generate status message
|
||||
const newStatusMsg = (
|
||||
<>
|
||||
<p>Sweep succeeded!</p>
|
||||
<p>Transaction ID: {txid}</p>
|
||||
<p>
|
||||
<a href={`https://blockchair.com/bitcoin-cash/transaction/${txid}`} target='_blank' rel='noreferrer'>
|
||||
TX on Blockchair BCH Block Explorer
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href={`https://token.fullstack.cash/transactions/?txid=${txid}`} target='_blank' rel='noreferrer'>
|
||||
TX on token explorer
|
||||
</a>
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
|
||||
setHideSpinner(true)
|
||||
setStatusMsg(newStatusMsg)
|
||||
setWifToSweep('')
|
||||
|
||||
await updateWalletState()
|
||||
} catch (err) {
|
||||
setHideSpinner(true)
|
||||
setStatusMsg(<b style={{ color: 'red' }}>{`Error: ${err.message}`}</b>)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in handleSweep(): ', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancelCounterOffers = async () => {
|
||||
try {
|
||||
console.log('Executing handleCancelCounterOffers()')
|
||||
|
||||
// Set modal initial state
|
||||
// setShowModal(true)
|
||||
// setHideSpinner(false)
|
||||
// setStatusMsg('')
|
||||
|
||||
// Get the keypair that holds Counter Offer UTXOs.
|
||||
const bchDexLib = appData.dexLib
|
||||
const keyPair = await bchDexLib.take.util.getKeyPair(1)
|
||||
const wif = keyPair.wif
|
||||
// console.log(`WIF: ${wif}`)
|
||||
|
||||
// Sweep the private key holding the Counter Offer UTXOs.
|
||||
setWifToSweep(wif)
|
||||
|
||||
await handleSweep()
|
||||
} catch (err) {
|
||||
console.error('Error in handleCancelCounterOffers(): ', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Modal component
|
||||
const getModal = () => (
|
||||
<Modal show={showModal} size='lg' onHide={() => setShowModal(false)}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Sweeping...</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Container>
|
||||
<Row>
|
||||
{!hideSpinner && (
|
||||
<Col style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<span style={{ marginRight: '10px' }}>Sweeping private key...</span> <Spinner animation='border' />
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
<br />
|
||||
{statusMsg && (
|
||||
<Row>
|
||||
<Col style={{ textAlign: 'center' }}>{statusMsg}</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Container>
|
||||
</Modal.Body>
|
||||
<Modal.Footer />
|
||||
</Modal>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col style={{ textAlign: 'right' }}>
|
||||
<a href='https://youtu.be/QW9xixHaEJE' target='_blank' rel='noreferrer'>
|
||||
<FontAwesomeIcon icon={faCircleQuestion} size='lg' />
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
<p>
|
||||
This View is used to 'sweep' a private key. This will transfer
|
||||
any BCH or SLP tokens from a paper wallet to your web wallet.
|
||||
Paper wallets are used to store BCH and tokens. You
|
||||
can <a href='https://paperwallet.fullstack.cash/' target='_blank' rel='noreferrer'>generate paper wallets here</a>.
|
||||
</p>
|
||||
<p>
|
||||
Paste the private key of a paper wallet below and click the button
|
||||
to sweep the funds. The private key must be in WIF format. It will
|
||||
start with the letter 'K' or 'L'.
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
<Form onSubmit={handleSweep}>
|
||||
<Form.Group controlId='formWif' style={{ textAlign: 'center' }}>
|
||||
<Form.Control
|
||||
type='text'
|
||||
placeholder='KzJqZxi5XSo36woCy7MFVNRPDpfp8x8FpkhRvrErKBBrDXRVY9Ft'
|
||||
onChange={(e) => setWifToSweep(e.target.value)}
|
||||
value={wifToSweep}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
<Row style={{ textAlign: 'center' }}>
|
||||
<Col>
|
||||
<Button variant='info' onClick={handleSweep}>
|
||||
Sweep
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
<hr />
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
<p>
|
||||
When a Buy order is created, the coins (UTXO) to pay for it are
|
||||
moved to a secondary address. Clicking the button below will
|
||||
sweep those funds back into this main wallet. This will also
|
||||
cancel/invalidate all open Counter Offers that you've created.
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row style={{ textAlign: 'center' }}>
|
||||
<Col>
|
||||
<Button onClick={handleCancelCounterOffers}>
|
||||
Sweep DEX Trading Wallet
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
{showModal && getModal()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SweepWif
|
||||
@@ -3,25 +3,25 @@
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Container, Row, Col } from 'react-bootstrap'
|
||||
|
||||
// Local libraries
|
||||
import config from '../../config'
|
||||
import Memo from './get-cid'
|
||||
// import Memo from './get-cid'
|
||||
|
||||
function Footer (props) {
|
||||
const [ipfsCid, setIpfsCid] = useState(config.ipfsCid)
|
||||
// const [ipfsCid, setIpfsCid] = useState(config.ipfsCid)
|
||||
const wallet = props.appData.wallet
|
||||
|
||||
// Retrieve the most up-to-date CID for the app on Filecoin from the BCH blockchain.
|
||||
useEffect(() => {
|
||||
async function fetchData () {
|
||||
try {
|
||||
const hash = await getUpdatedUrl(wallet)
|
||||
if (hash) {
|
||||
setIpfsCid(hash)
|
||||
}
|
||||
// const hash = await getUpdatedUrl(wallet)
|
||||
// if (hash) {
|
||||
// setIpfsCid(hash)
|
||||
// }
|
||||
} catch (err) {
|
||||
console.error('Error trying to retrieve Filecoin CID for the app from the BCH blockchain.')
|
||||
}
|
||||
@@ -32,56 +32,44 @@ function Footer (props) {
|
||||
return (
|
||||
<Container style={{ backgroundColor: '#ddd' }}>
|
||||
<Row style={{ padding: '25px' }}>
|
||||
<Col>
|
||||
<h6>Site Mirrors</h6>
|
||||
<ul>
|
||||
<li>
|
||||
<a href={config.ghPagesUrl} target='_blank' rel='noreferrer'>GitHub Pages</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={`https://${ipfsCid}.ipfs.dweb.link/`} target='_blank' rel='noreferrer'>Filecoin</a>
|
||||
</li>
|
||||
</ul>
|
||||
</Col>
|
||||
|
||||
<Col>
|
||||
<h6>Source Code</h6>
|
||||
<ul>
|
||||
<li>
|
||||
<a href={config.ghRepo} target='_blank' rel='noreferrer'>GitHub</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={config.radicleUrl} target='_blank' rel='noreferrer'>Radicle</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</Col>
|
||||
|
||||
<Col />
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
async function getUpdatedUrl (wallet) {
|
||||
try {
|
||||
// Exit if the wallet is not initialized.
|
||||
if (!wallet) return
|
||||
// async function getUpdatedUrl (wallet) {
|
||||
// try {
|
||||
// // Exit if the wallet is not initialized.
|
||||
// if (!wallet) return
|
||||
|
||||
// Initialize the memo library for retrieving data from the BCH blockchain.
|
||||
const memo = new Memo({ bchAddr: config.appBchAddr })
|
||||
await memo.initialize(wallet)
|
||||
const hash = await memo.findHash()
|
||||
// // Initialize the memo library for retrieving data from the BCH blockchain.
|
||||
// const memo = new Memo({ bchAddr: config.appBchAddr })
|
||||
// await memo.initialize(wallet)
|
||||
// const hash = await memo.findHash()
|
||||
|
||||
if (!hash) {
|
||||
console.error(
|
||||
`Could not find IPFS hash in transactions for address ${config.appBchAddr}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
// console.log(`latest IPFS hash: ${hash}`)
|
||||
// if (!hash) {
|
||||
// console.error(
|
||||
// `Could not find IPFS hash in transactions for address ${config.appBchAddr}`
|
||||
// )
|
||||
// return false
|
||||
// }
|
||||
// // console.log(`latest IPFS hash: ${hash}`)
|
||||
|
||||
return hash
|
||||
} catch (err) {
|
||||
console.log('Error in getUpdatedUrl(): ', err)
|
||||
}
|
||||
}
|
||||
// return hash
|
||||
// } catch (err) {
|
||||
// console.log('Error in getUpdatedUrl(): ', err)
|
||||
// }
|
||||
// }
|
||||
|
||||
export default Footer
|
||||
|
||||
@@ -31,19 +31,37 @@ function NavMenu (props) {
|
||||
<Navbar expanded={expanded} onToggle={setExpanded} expand='xxxl' bg='dark' variant='dark' style={{ paddingRight: '20px' }}>
|
||||
<Navbar.Brand href='#home' style={{ paddingLeft: '20px' }}>
|
||||
<Image src={Logo} thumbnail width='50' />{' '}
|
||||
PSF Web3 Demo
|
||||
DEX
|
||||
</Navbar.Brand>
|
||||
|
||||
<Navbar.Toggle aria-controls='responsive-navbar-nav' />
|
||||
<Navbar.Collapse id='responsive-navbar-nav'>
|
||||
<Nav className='mr-auto'>
|
||||
|
||||
<NavLink
|
||||
className={(currentPath === '/balance' || currentPath === '/') ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/balance'
|
||||
className={(currentPath === '/nfts-for-sale' || currentPath === '/') ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/nfts-for-sale'
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
Check Balance
|
||||
NFTs for Sale
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
className={(currentPath === '/bch') ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/bch'
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
BCH
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
className={currentPath === '/slp-tokens' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/slp-tokens'
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
Tokens
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
className={currentPath === '/wallet' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/wallet'
|
||||
@@ -52,19 +70,49 @@ function NavMenu (props) {
|
||||
>
|
||||
Wallet
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
className={currentPath === '/Placeholder2' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/Placeholder2'
|
||||
onClick={(e) => handleClickEvent(1)}
|
||||
>
|
||||
Placeholder2
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className={currentPath === '/placeholder3' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/placeholder3'
|
||||
className={(currentPath === '/balance') ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/balance'
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
Placeholder 3
|
||||
Check Balance
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
className={(currentPath === '/sweep') ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/sweep'
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
Sweep
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className={(currentPath === '/sign') ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/sign'
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
Sign
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className={currentPath === '/configuration' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/configuration'
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
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>
|
||||
</Nav>
|
||||
</Navbar.Collapse>
|
||||
|
||||
@@ -20,7 +20,7 @@ export function UninitializedView (props = {}) {
|
||||
denyClose={appData.denyClose}
|
||||
/>
|
||||
{
|
||||
props.asyncInitFinished
|
||||
appData.asyncInitFinished
|
||||
? <AppBody menuState={100} wallet={appData.wallet} appData={appData} />
|
||||
: null
|
||||
}
|
||||
|
||||
+7
-2
@@ -11,8 +11,13 @@ const config = {
|
||||
|
||||
// Backup Info that goes into the Footer.
|
||||
ghPagesUrl: 'https://permissionless-software-foundation.github.io/react-bootstrap-web3-spa/',
|
||||
ghRepo: 'https://github.com/Permissionless-Software-Foundation/react-bootstrap-web3-spa',
|
||||
radicleUrl: 'https://app.radicle.network/seeds/maple.radicle.garden/rad:git:hnrkd5cjwwb5tzx37hq9uqm5ubon7ee468xcy/remotes/hyyycncbn9qzqmobnhjq9rry6t4mbjiadzjoyhaknzxjcz3cxkpfpc'
|
||||
ghRepo: 'https://github.com/Permissionless-Software-Foundation/bch-dex-taker-v2',
|
||||
radicleUrl: 'https://app.radicle.network/seeds/maple.radicle.garden/rad:git:hnrkd5cjwwb5tzx37hq9uqm5ubon7ee468xcy/remotes/hyyycncbn9qzqmobnhjq9rry6t4mbjiadzjoyhaknzxjcz3cxkpfpc',
|
||||
|
||||
dexServer: 'https://dex-api.fullstack.cash',
|
||||
|
||||
nostrTopic: 'bch-dex-test-topic-02',
|
||||
nostrRelay: 'wss://nostr-relay.psfoundation.info'
|
||||
|
||||
}
|
||||
|
||||
|
||||
+25
-12
@@ -1,20 +1,30 @@
|
||||
import { useState } from 'react'
|
||||
import { useQueryParam, StringParam } from 'use-query-params'
|
||||
// import { useQueryParam, StringParam } from 'use-query-params'
|
||||
import useLocalStorageState from 'use-local-storage-state'
|
||||
import AppUtil from '../util'
|
||||
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
function useAppState () {
|
||||
const location = useLocation()
|
||||
|
||||
// Get the CashStack URL from query parameter or use default
|
||||
let [restURL] = useQueryParam('restURL', StringParam)
|
||||
if (!restURL) restURL = 'https://free-bch.fullstack.cash'
|
||||
// Load Local storage Data
|
||||
const [lsState, setLSState, { removeItem }] = useLocalStorageState('bchWalletState-template', {
|
||||
ssr: true,
|
||||
defaultValue: {
|
||||
serverUrl: 'https://free-bch.fullstack.cash' // Default server
|
||||
}
|
||||
})
|
||||
|
||||
const [serverUrl, setServerUrl] = useState(restURL)
|
||||
console.log('lsState: ', lsState)
|
||||
|
||||
// Initialize data states
|
||||
const [serverUrl, setServerUrl] = useState(lsState.serverUrl) // Default server url
|
||||
const [menuState, setMenuState] = useState(0)
|
||||
const [wallet, setWallet] = useState(false)
|
||||
const [servers, setServers] = useState([])
|
||||
const [dexLib, setDexLib] = useState(false)
|
||||
const [nostr, setNostr] = useState(false)
|
||||
|
||||
// Startup state management
|
||||
const [asyncInitStarted, setAsyncInitStarted] = useState(false)
|
||||
@@ -32,11 +42,11 @@ function useAppState () {
|
||||
// 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 [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)}`)
|
||||
@@ -117,8 +127,11 @@ function useAppState () {
|
||||
updateLocalStorage,
|
||||
updateBchWalletState,
|
||||
appUtil: new AppUtil(),
|
||||
currentPath: location.pathname
|
||||
|
||||
currentPath: location.pathname,
|
||||
dexLib,
|
||||
setDexLib,
|
||||
nostr,
|
||||
setNostr
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,16 @@
|
||||
|
||||
// Global npm libraries
|
||||
import axios from 'axios'
|
||||
import BchDexLib from 'bch-dex-lib'
|
||||
|
||||
// Local libraries
|
||||
import GistServers from './gist-servers'
|
||||
import Nostr from './nostr'
|
||||
import P2WDB from 'p2wdb'
|
||||
import { base58_to_binary as base58ToBinary } from 'base58-js'
|
||||
import { bytesToHex } from '@noble/hashes/utils' // already an installed dependency
|
||||
import { getPublicKey } from 'nostr-tools/pure'
|
||||
import * as nip19 from 'nostr-tools/nip19'
|
||||
|
||||
class AsyncLoad {
|
||||
constructor () {
|
||||
@@ -56,8 +63,14 @@ class AsyncLoad {
|
||||
await wallet.walletInfoPromise
|
||||
await wallet.initialize()
|
||||
console.log('starting to update wallet state.')
|
||||
|
||||
// Get Nostr key pair from WIF
|
||||
const nostrKeyPair = this.nostrKeyPairFromWIF(wallet.walletInfo.privateKey)
|
||||
console.log('nostrKeyPair: ', nostrKeyPair)
|
||||
const walletInfo = wallet.walletInfo
|
||||
walletInfo.nostrKeyPair = nostrKeyPair
|
||||
// Update the state of the wallet.
|
||||
appData.updateBchWalletState({ walletObj: wallet.walletInfo, appData })
|
||||
appData.updateBchWalletState({ walletObj: walletInfo, appData })
|
||||
console.log('finished updating wallet state.')
|
||||
// Save the mnemonic to local storage.
|
||||
if (!mnemonic) {
|
||||
@@ -80,6 +93,10 @@ class AsyncLoad {
|
||||
// Get the BCH balance of the wallet.
|
||||
const bchBalance = await wallet.getBalance({ bchAddress: wallet.walletInfo.cashAddress })
|
||||
|
||||
await wallet.walletInfoPromise
|
||||
await wallet.initialize()
|
||||
// console.log(`mnemonic: ${wallet.walletInfo.mnemonic}`)
|
||||
|
||||
// Update the state of the wallet with the balances
|
||||
updateBchWalletState({ walletObj: { bchBalance }, appData })
|
||||
|
||||
@@ -112,13 +129,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
|
||||
@@ -203,6 +213,76 @@ class AsyncLoad {
|
||||
return defaultOptions
|
||||
}
|
||||
}
|
||||
|
||||
// Load the BchDexLib library.
|
||||
getDexLib (inObj = {}) {
|
||||
try {
|
||||
const { bchWallet, p2wdbRead, p2wdbWrite } = inObj
|
||||
|
||||
const dexLib = new BchDexLib({ bchWallet, p2wdbRead, p2wdbWrite })
|
||||
|
||||
return dexLib
|
||||
} catch (error) {
|
||||
console.error('Error getting DexLib', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Load the Nostr library.
|
||||
getNostrLib (inObj = {}) {
|
||||
try {
|
||||
const { bchWallet } = inObj
|
||||
|
||||
const nostrLib = new Nostr({ bchWallet })
|
||||
|
||||
return nostrLib
|
||||
} catch (error) {
|
||||
console.error('Error in getNostrLib', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Load the P2WDB library.
|
||||
getP2WDBLib (inObj = {}) {
|
||||
try {
|
||||
const { bchWallet } = inObj
|
||||
|
||||
const p2wdbRead = new P2WDB.Read()
|
||||
const p2wdbWrite = new P2WDB.Write({ bchWallet })
|
||||
|
||||
return { p2wdbRead, p2wdbWrite }
|
||||
} catch (error) {
|
||||
console.error('Error in getP2WDBLib', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Get Nostr key pair from WIF
|
||||
nostrKeyPairFromWIF (WIF) {
|
||||
if (!WIF) return
|
||||
|
||||
// Extract the privaty key from the WIF, using this guide:
|
||||
// https://learnmeabitcoin.com/technical/keys/private-key/wif/
|
||||
const wifBuf = base58ToBinary(WIF)
|
||||
const privBuf = wifBuf.slice(1, 33)
|
||||
// console.log('privBuf: ', privBuf)
|
||||
// Convert the private key to a hex string
|
||||
const privHex = bytesToHex(privBuf)
|
||||
// Convert the private key to a Nostr NSEC key
|
||||
const nsec = nip19.nsecEncode(privBuf)
|
||||
|
||||
// Get the public key hex string from the private key
|
||||
const pubHex = getPublicKey(privBuf)
|
||||
// Convert the public key to a Nostr NPUB key
|
||||
const npub = nip19.npubEncode(pubHex)
|
||||
|
||||
return {
|
||||
privHex,
|
||||
pubHex,
|
||||
nsec,
|
||||
npub
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sleep (ms) {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
Implementing nostr natively in the browser, rather than using a library
|
||||
intended for node.js
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
// import { finalizeEvent } from '@chris.troutner/nostr-tools/pure'
|
||||
// import { Relay } from '@chris.troutner/nostr-tools/relay'
|
||||
// import BchNostr from 'bch-nostr'
|
||||
// import * as nip19 from '@chris.troutner/nostr-tools/nip19'
|
||||
|
||||
import { finalizeEvent } from 'nostr-tools/pure'
|
||||
import { Relay } from 'nostr-tools/relay'
|
||||
import BchNostr from 'bch-nostr'
|
||||
import * as nip19 from 'nostr-tools/nip19'
|
||||
import config from '../config/index.js'
|
||||
|
||||
class NostrBrowser {
|
||||
constructor (localConfig = {}) {
|
||||
if (!localConfig.bchWallet) {
|
||||
throw new Error('Instance of minimal-slp-wallet must be passed as wallet property when instantiating the bch-dex-lib library.')
|
||||
}
|
||||
|
||||
this.bchWallet = localConfig.bchWallet
|
||||
|
||||
this.bchNostr = new BchNostr({
|
||||
relayWs: config.nostrRelay,
|
||||
topic: config.nostrTopic
|
||||
})
|
||||
}
|
||||
|
||||
async testNostrUpload (inObj = {}) {
|
||||
try {
|
||||
console.log('testNostrUpload() executing.')
|
||||
// console.log('this.bchWallet: ', this.bchWallet)
|
||||
|
||||
const { offerData, partialHex } = inObj
|
||||
console.log('offerData: ', offerData)
|
||||
|
||||
const wif = this.bchWallet.walletInfo.privateKey
|
||||
// const { privKeyBuf, nostrPubKey } =
|
||||
// this.bchNostr.keys.createNostrPubKeyFromWif({ wif })
|
||||
const { privKeyBuf } =
|
||||
this.bchNostr.keys.createNostrPubKeyFromWif({ wif })
|
||||
|
||||
// Scaffold the Counter Offer from the Offer
|
||||
const counterOfferData = Object.assign({}, offerData)
|
||||
counterOfferData.partialTxHex = partialHex
|
||||
delete counterOfferData.nostrEventId
|
||||
delete counterOfferData._id
|
||||
counterOfferData.offerHash = offerData.nostrEventId
|
||||
|
||||
// Add P2WDB specific flag for signaling that this is a new Counter Offer.
|
||||
counterOfferData.dataType = 'counter-offer'
|
||||
console.log(`counterOfferData: ${JSON.stringify(counterOfferData, null, 2)}`)
|
||||
|
||||
const nostrData = {
|
||||
data: counterOfferData
|
||||
}
|
||||
|
||||
const msg = JSON.stringify(nostrData, null, 2)
|
||||
|
||||
// const inObj = {
|
||||
// kind: 867,
|
||||
// privKeyBuf: privKeyBuf,
|
||||
// nostrPubKey: nostrPubKey,
|
||||
// relayWs: 'wss://nostr-relay.psfoundation.info',
|
||||
// msg,
|
||||
// tags: [['t', 'bch-dex-test-topic-01']]
|
||||
// }
|
||||
|
||||
const relayWs = config.nostrRelay
|
||||
const eventTemplate = {
|
||||
kind: 867,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [['t', config.nostrTopic]],
|
||||
content: msg
|
||||
}
|
||||
|
||||
// Sign the post
|
||||
const signedEvent = finalizeEvent(eventTemplate, privKeyBuf)
|
||||
// console.log('signedEvent: ', signedEvent)
|
||||
const eventId = signedEvent.id
|
||||
|
||||
// Connect to a relay.
|
||||
const relay = await Relay.connect(relayWs, {
|
||||
/* global WebSocket */
|
||||
webSocket: WebSocket
|
||||
})
|
||||
// console.log(`connected to ${relay.url}`)
|
||||
|
||||
// Publish the message to the relay.
|
||||
await relay.publish(signedEvent)
|
||||
|
||||
// Close the connection to the relay.
|
||||
relay.close()
|
||||
|
||||
// const eventId = await this.bchNostr.post.uploadToNostr(inObj)
|
||||
|
||||
const noteId = this.eventId2note(eventId)
|
||||
|
||||
// const eventId = 'test1'
|
||||
// const noteId = 'test2'
|
||||
|
||||
return { eventId, noteId }
|
||||
} catch (err) {
|
||||
console.log('Error in nostr.js/testNostrUpload()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Convert an Event ID into a `noteabc..` syntax that Astral expects.
|
||||
// This can be used to generate a link to Astral to display the post.
|
||||
eventId2note (eventId) {
|
||||
return nip19.noteEncode(eventId)
|
||||
}
|
||||
}
|
||||
|
||||
export default NostrBrowser
|
||||
Reference in New Issue
Block a user