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 | |
|---|---|---|---|
|
|
aa2d92bfaf | ||
|
|
40467014f0 | ||
|
|
d94b3fc853 | ||
|
|
51ed5601fa | ||
|
|
d72d98a2d7 | ||
|
|
7237cb6c6a | ||
|
|
93c8eb42d3 | ||
|
|
fa3c71f0a1 |
Generated
+4
-4
@@ -15,7 +15,7 @@
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@noble/hashes": "1.8.0",
|
||||
"axios": "0.27.2",
|
||||
"bch-dex-lib": "2.2.0",
|
||||
"bch-dex-lib": "2.3.1",
|
||||
"bch-message-lib": "2.2.1",
|
||||
"bch-nostr": "1.3.4",
|
||||
"bch-token-sweep": "2.2.1",
|
||||
@@ -10043,9 +10043,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bch-dex-lib": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/bch-dex-lib/-/bch-dex-lib-2.2.0.tgz",
|
||||
"integrity": "sha512-SORqn4qNmbxfL2SbbapOpbcCB+FtqasR+GT0XRifdkXFjA3aDL0gwMsHjh8IzLZnKtPYSBcfgzKQFxEsXCJNug==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bch-dex-lib/-/bch-dex-lib-2.3.1.tgz",
|
||||
"integrity": "sha512-HlY21BpUUaSsuZNWF5LGPwSJU7m2kYbNMcS36o22RSHuO1kiBMONHwkV+v22kdbDk3T/Iz+AZN0mzJvH5neCXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@chris.troutner/retry-queue": "1.0.10",
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@noble/hashes": "1.8.0",
|
||||
"axios": "0.27.2",
|
||||
"bch-dex-lib": "2.2.0",
|
||||
"bch-dex-lib": "2.3.1",
|
||||
"bch-message-lib": "2.2.1",
|
||||
"bch-nostr": "1.3.4",
|
||||
"bch-token-sweep": "2.2.1",
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
This Card component displays a counter offer with token icon, name, and price.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState } from 'react'
|
||||
import { Container, Row, Col, Card, Button } from 'react-bootstrap'
|
||||
import Jdenticon from '@chris.troutner/react-jdenticon'
|
||||
|
||||
function CounterOfferCard (props) {
|
||||
const { offer } = props
|
||||
const [icon, setIcon] = useState(offer.tokenIcon)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Col xs={12} sm={6} lg={4} style={{ padding: '25px' }}>
|
||||
<Card className='shadow-sm'>
|
||||
<Card.Body style={{ textAlign: 'center', padding: '10px' }}>
|
||||
{/** If the icon is loaded, display it */}
|
||||
{icon && (
|
||||
<Card.Img
|
||||
src={icon}
|
||||
style={{ height: '100px', width: 'auto', margin: '0 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 && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: '10px' }}>
|
||||
<Jdenticon size='100' value={offer.tokenId} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card.Title style={{ textAlign: 'center', marginTop: '10px' }}>
|
||||
<h4>{offer.ticker}</h4>
|
||||
</Card.Title>
|
||||
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
{/* <strong>{offer.tokenName}</strong> */}
|
||||
<strong>Counter Offer UTXO</strong>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
<Row>
|
||||
{/* <Col>Price:</Col> */}
|
||||
<Col><strong>{offer.price}</strong></Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
<Row className='text-center'>
|
||||
<Col>
|
||||
<Button disabled variant='danger' size='sm'>
|
||||
Cancel
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CounterOfferCard
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
Shows Counter Offers created by the user.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Container, Row, Col, Spinner } from 'react-bootstrap'
|
||||
|
||||
// Local libraries
|
||||
import CounterOfferCard from './counter-offer-card'
|
||||
import AsyncLoad from '../../../services/async-load'
|
||||
|
||||
function CounterOffers (props) {
|
||||
const appData = props.appData
|
||||
|
||||
const [counterOffers, setCounterOffers] = useState([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
// Generate counter offer cards
|
||||
const generateCards = () => {
|
||||
return counterOffers.map((offer) => (
|
||||
<CounterOfferCard
|
||||
key={offer.id}
|
||||
offer={offer}
|
||||
appData={appData}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const loadWallet = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const { bchWalletState, serverUrl } = appData
|
||||
const asyncLoad = new AsyncLoad()
|
||||
await asyncLoad.loadWalletLib()
|
||||
const counterOfferWallet = await asyncLoad.getDerivatedWallet(serverUrl, bchWalletState.mnemonic, "m/44'/245'/0'/0/1")
|
||||
|
||||
const utxoStore = counterOfferWallet.utxos.utxoStore
|
||||
const bchUtxos = utxoStore.bchUtxos
|
||||
setCounterOffers(bchUtxos)
|
||||
console.log('counterOffers', bchUtxos)
|
||||
setIsLoading(false)
|
||||
} catch (error) {
|
||||
setIsLoading(false)
|
||||
console.log('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
loadWallet()
|
||||
}, [appData])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<h1>Counter Offers</h1>
|
||||
<p className='text-muted'>Your pending counter offers</p>
|
||||
</Col>
|
||||
</Row>
|
||||
{isLoading
|
||||
? (
|
||||
<Row className='text-center' style={{ padding: '50px' }}>
|
||||
<Col>
|
||||
<Spinner animation='border' role='status'>
|
||||
<span className='visually-hidden'>Loading...</span>
|
||||
</Spinner>
|
||||
<p className='mt-3 text-muted'>Loading counter offers...</p>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Row>
|
||||
{generateCards()}
|
||||
</Row>
|
||||
{counterOffers.length === 0 && (
|
||||
<Row className='text-center'>
|
||||
<Col>
|
||||
<p>No counter offers found.</p>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default CounterOffers
|
||||
@@ -30,6 +30,7 @@ import ContentCreators from './nostr/content-creators/index.js'
|
||||
import UserDataReview from './user-data-review'
|
||||
import Offers from './offers'
|
||||
import NostrChat from './nostr-chat'
|
||||
import CounterOffers from './counter-offers'
|
||||
function AppBody (props) {
|
||||
// Dependency injection through props
|
||||
const appData = props.appData
|
||||
@@ -55,6 +56,7 @@ function AppBody (props) {
|
||||
<Route path='/content-creators' element={<ContentCreators appData={appData} />} />
|
||||
<Route path='/user-data/:tokenId' element={<UserDataReview appData={appData} />} />
|
||||
<Route path='/offers' element={<Offers appData={appData} />} />
|
||||
<Route path='/counter-offers' element={<CounterOffers appData={appData} />} />
|
||||
<Route path='/nostr-chat' element={<NostrChat appData={appData} />} />
|
||||
</Routes>
|
||||
{/** Show in all paths except the servers view */}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// Global npm libraries
|
||||
import React, { useState } from 'react'
|
||||
import { Button, Modal, Container, Row, Col, Spinner } from 'react-bootstrap'
|
||||
import AsyncLoad from '../../../services/async-load'
|
||||
|
||||
function BuyButton (props) {
|
||||
const { token, appData, onSuccess } = props
|
||||
@@ -45,10 +46,19 @@ function BuyButton (props) {
|
||||
|
||||
// Generate a counter offer.
|
||||
const bchDexLib = appData.dexLib
|
||||
const { offerData, partialHex } = await bchDexLib.take.takeOffer(
|
||||
const { offerData, partialHex, counterOfferUtxo } = await bchDexLib.take.takeOffer(
|
||||
targetOffer
|
||||
)
|
||||
|
||||
// Get counter offer data
|
||||
const asyncLoad = new AsyncLoad()
|
||||
const { takerAddr, takerNpub, counterOfferAddr } = await asyncLoad.getCounterOfferMetadata(appData)
|
||||
|
||||
offerData.takerAddr = takerAddr
|
||||
offerData.takerNpub = takerNpub
|
||||
offerData.counterOfferAddr = counterOfferAddr
|
||||
offerData.counterOfferUtxo = counterOfferUtxo.txid
|
||||
|
||||
progress.push(<p key='progress-msg2'>Uploading counter offer to Nostr...</p>)
|
||||
setProgressMsg(progress)
|
||||
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
This component displays the seller's profile information for an NFT listing.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
function SellerProfile (props) {
|
||||
const { npub, appData } = props
|
||||
const [profile, setProfile] = useState(null)
|
||||
const [imageError, setImageError] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const { nostrQueries } = appData
|
||||
|
||||
// Get profile if npub is provided.
|
||||
useEffect(() => {
|
||||
const start = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const pubKey = nostrQueries.npubToHex(npub)
|
||||
const profile = await nostrQueries.getProfile(pubKey)
|
||||
setProfile(profile)
|
||||
} catch (error) {
|
||||
console.warn('Error fetching seller profile:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
if (npub) {
|
||||
start()
|
||||
} else {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [npub, nostrQueries])
|
||||
|
||||
// handle img url errors
|
||||
const handleImageError = (type) => {
|
||||
setImageError(true)
|
||||
}
|
||||
|
||||
// go to profile
|
||||
const goToProfile = () => {
|
||||
if (npub) {
|
||||
const profileUrl = `${window.location.origin}/profile/${npub}#single-view`
|
||||
window.open(profileUrl, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
// Get display name - use profile name, npub short form, or "Anonymous User"
|
||||
const getDisplayName = () => {
|
||||
if (profile?.name) return profile.name
|
||||
if (npub) {
|
||||
return npub.slice(0, 8) + '...' + npub.slice(-6)
|
||||
}
|
||||
return 'Anonymous User'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={goToProfile}
|
||||
className='seller-profile-container'
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #e9ecef',
|
||||
transition: 'all 0.2s ease',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
width: '100%'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#e9ecef'
|
||||
e.currentTarget.style.borderColor = '#dee2e6'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#f8f9fa'
|
||||
e.currentTarget.style.borderColor = '#e9ecef'
|
||||
}}
|
||||
>
|
||||
<div className='d-flex align-items-center justify-content-center gap-1'>
|
||||
{/* Seller Label */}
|
||||
<small
|
||||
className='text-muted fw-semibold'
|
||||
style={{
|
||||
fontSize: '0.65rem',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.3px'
|
||||
}}
|
||||
>
|
||||
Seller:
|
||||
</small>
|
||||
|
||||
{/* Profile Picture or Placeholder */}
|
||||
<div
|
||||
className='d-flex align-items-center justify-content-center'
|
||||
style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#e9ecef',
|
||||
border: '1.5px solid #dee2e6',
|
||||
overflow: 'hidden',
|
||||
flexShrink: 0,
|
||||
cursor: 'pointer',
|
||||
transition: 'transform 0.2s ease'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
goToProfile()
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'scale(1.1)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'scale(1)'
|
||||
}}
|
||||
>
|
||||
{isLoading
|
||||
? (
|
||||
<FontAwesomeIcon
|
||||
icon={faUser}
|
||||
style={{
|
||||
color: '#adb5bd',
|
||||
fontSize: '12px',
|
||||
opacity: 0.5
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: (profile?.picture && !imageError)
|
||||
? (
|
||||
<img
|
||||
src={profile.picture}
|
||||
alt='Seller profile'
|
||||
className='w-100 h-100'
|
||||
style={{ objectFit: 'cover' }}
|
||||
onError={() => handleImageError('picture')}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<FontAwesomeIcon
|
||||
icon={faUser}
|
||||
style={{
|
||||
color: '#6c757d',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Seller Name */}
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
goToProfile()
|
||||
}}
|
||||
className='cursor-pointer fw-medium'
|
||||
style={{
|
||||
color: '#495057',
|
||||
fontSize: '0.8rem',
|
||||
transition: 'color 0.2s ease',
|
||||
maxWidth: '120px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = '#007bff'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = '#495057'
|
||||
}}
|
||||
title={profile?.name || npub}
|
||||
>
|
||||
{getDisplayName()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SellerProfile
|
||||
@@ -10,14 +10,12 @@ import Jdenticon from '@chris.troutner/react-jdenticon'
|
||||
// Local libraries
|
||||
import InfoButton from './info-button'
|
||||
import BuyButton from './buy-button'
|
||||
|
||||
import SellerProfile from './seller-profile'
|
||||
function TokenCard (props) {
|
||||
const { token, appData, handleRefresh, hideBuyBtn } = 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(() => {
|
||||
console.log('setting icon')
|
||||
@@ -28,8 +26,14 @@ function TokenCard (props) {
|
||||
return (
|
||||
<>
|
||||
<Col xs={12} sm={6} lg={4} style={{ padding: '25px' }}>
|
||||
<Card>
|
||||
<Card.Body style={{ textAlign: 'center' }}>
|
||||
<Card className='shadow-sm'>
|
||||
<Card.Body style={{ textAlign: 'center', padding: '10px' }}>
|
||||
{/* Seller Profile Section */}
|
||||
<Row className='mb-2'>
|
||||
<Col className='text-center mb-2'>
|
||||
<SellerProfile npub={token.makerNpub} appData={appData} />
|
||||
</Col>
|
||||
</Row>
|
||||
{/** If the icon is loaded, display it */
|
||||
icon && (
|
||||
<Card.Img
|
||||
@@ -50,7 +54,6 @@ function TokenCard (props) {
|
||||
<Card.Title style={{ textAlign: 'center', marginTop: '10px' }}>
|
||||
<h4>{token.ticker}</h4>
|
||||
</Card.Title>
|
||||
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
@@ -8,7 +8,7 @@ import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { Container, Row, Col, Table, Button, Spinner } from 'react-bootstrap'
|
||||
import axios from 'axios'
|
||||
import { DatatableWrapper, TableBody, TableHeader } from 'react-bs-datatable'
|
||||
|
||||
import AsyncLoad from '../../../services/async-load'
|
||||
// Local libraries
|
||||
import config from '../../../config'
|
||||
import WaitingModal from '../../waiting-modal'
|
||||
@@ -100,10 +100,19 @@ function Offers (props) {
|
||||
|
||||
// Generate a counter offer.
|
||||
const bchDexLib = appData.dexLib
|
||||
const { offerData, partialHex } = await bchDexLib.take.takeOffer(
|
||||
const { offerData, partialHex, counterOfferUtxo } = await bchDexLib.take.takeOffer(
|
||||
targetOfferEventId
|
||||
)
|
||||
|
||||
// Get counter offer data
|
||||
const asyncLoad = new AsyncLoad()
|
||||
const { takerAddr, takerNpub, counterOfferAddr } = await asyncLoad.getCounterOfferMetadata(appData)
|
||||
|
||||
offerData.takerAddr = takerAddr
|
||||
offerData.takerNpub = takerNpub
|
||||
offerData.counterOfferAddr = counterOfferAddr
|
||||
offerData.counterOfferUtxo = counterOfferUtxo.txid
|
||||
|
||||
// Upload the counter offer to Nostr.
|
||||
const nostr = appData.nostr
|
||||
const { eventId, noteId } = await nostr.testNostrUpload({
|
||||
|
||||
@@ -58,6 +58,14 @@ function NavMenu (props) {
|
||||
Fungible Tokens
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
className={currentPath === '/counter-offers' ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
to='/counter-offers'
|
||||
onClick={handleClickEvent}
|
||||
>
|
||||
Counter Offers
|
||||
</NavLink>
|
||||
|
||||
<hr />
|
||||
|
||||
<NavLink
|
||||
|
||||
@@ -333,6 +333,59 @@ class AsyncLoad {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async getDerivatedWallet (restURL, mnemonic, hdPath = "m/44'/245'/0'/0/0", initialize = true) {
|
||||
try {
|
||||
const options = {
|
||||
interface: 'consumer-api',
|
||||
restURL,
|
||||
noUpdate: true,
|
||||
hdPath
|
||||
}
|
||||
|
||||
const wallet = new this.BchWallet(mnemonic, options)
|
||||
|
||||
// Wait for wallet to initialize.
|
||||
await wallet.walletInfoPromise
|
||||
if (initialize) {
|
||||
await wallet.initialize()
|
||||
}
|
||||
|
||||
return wallet
|
||||
} catch (error) {
|
||||
console.error('Error initStarterWallet: ', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Get buyer and counter offer data.
|
||||
async getCounterOfferMetadata (appData) {
|
||||
try {
|
||||
const { bchWalletState, serverUrl } = appData
|
||||
// Start async load lib
|
||||
const asyncLoad = new AsyncLoad()
|
||||
await asyncLoad.loadWalletLib()
|
||||
|
||||
// Buyer wallet data
|
||||
const buyerWallet = await asyncLoad.getDerivatedWallet(serverUrl, bchWalletState.mnemonic, "m/44'/245'/0'/0/0", false)
|
||||
const buyerAddr = buyerWallet.walletInfo.cashAddress
|
||||
const buyerKeyPair = asyncLoad.nostrKeyPairFromWIF(buyerWallet.walletInfo.privateKey)
|
||||
const buyerNpub = buyerKeyPair.npub
|
||||
|
||||
// Counter offer wallet data
|
||||
const counterOfferWallet = await asyncLoad.getDerivatedWallet(serverUrl, bchWalletState.mnemonic, "m/44'/245'/0'/0/1", false)
|
||||
const counterOfferAddr = counterOfferWallet.walletInfo.cashAddress
|
||||
|
||||
return {
|
||||
takerAddr: buyerAddr,
|
||||
takerNpub: buyerNpub,
|
||||
counterOfferAddr
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getCounterOfferMetadata()', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sleep (ms) {
|
||||
|
||||
Reference in New Issue
Block a user