Compare commits

..
23 Commits
Author SHA1 Message Date
Chris Troutner acb39ceff8 Merge pull request #15 from Permissionless-Software-Foundation/dh-cache-data
feat(cache): caching mutable data
2025-06-06 20:38:34 -07:00
Daniel Gonzalez 63b7bad1d1 Fixed merge conflicts 2025-06-06 21:24:09 -04:00
Chris Troutner c140255a26 Merge pull request #16 from Permissionless-Software-Foundation/dh-product-modal
feat(info): Added product info modal
2025-06-06 11:23:23 -07:00
Daniel Gonzalez 947cb43535 linting 2025-06-06 12:53:18 -04:00
Daniel Gonzalez 93980ed703 feat(info): Added product info modal 2025-06-06 12:51:44 -04:00
Daniel Gonzalez 36c81e7ce9 feat(cache): caching mutable data 2025-06-03 19:34:57 -04:00
Chris Troutner 6ad3d40591 Merge pull request #14 from Permissionless-Software-Foundation/dh-profile-style
feat(nostr): Initial Styling of Nostr Profile
2025-06-02 10:37:52 -07:00
Chris Troutner a4027b2fcb feat(tagged posts): Nostr posts made with this app are tagged for easy tracking in a global feed 2025-06-02 10:36:35 -07:00
Daniel Gonzalez 192497b5cf feat(nostr): Initial Styling of Nostr Profile 2025-06-02 12:19:43 -04:00
Chris Troutner fda2de5362 Merge pull request #13 from Permissionless-Software-Foundation/dh-kind1-posts
feat(nostr): Created UI for Kind 1 Nostr posts
2025-05-27 18:25:14 -07:00
Daniel Gonzalez 131720ed58 feat(nostr): Created UI for Kind 1 Nostr posts 2025-05-27 19:25:38 -04:00
Chris Troutner 64b1aec4cf Merge pull request #12 from Permissionless-Software-Foundation/dh-nostr-keys
fix(nostr): Display Nostr keys as npub and nsec
2025-05-27 08:25:18 -07:00
Daniel Gonzalez 70ce941eba fix(nostr): Display Nostr keys as npub and nsec 2025-05-26 18:47:31 -04:00
Chris Troutner dfd5934ddb Merge pull request #11 from Permissionless-Software-Foundation/dh-nostr-views
feat(nostr): Created nostr views
2025-05-25 07:46:57 -07:00
Chris Troutner e0652714e8 Merge branch 'ct-unstable' into dh-nostr-views 2025-05-25 07:43:03 -07:00
Chris Troutner 838a9e3dfd Adding LLM MD summary 2025-05-24 08:41:59 -07:00
Daniel Gonzalez 6b9626aeb1 feat(nostr): Created nostr views 2025-05-23 18:41:05 -04:00
Chris Troutner 3ce258fd8c Merge branch 'master' into ct-unstable 2025-05-22 08:18:07 -07:00
Chris Troutner 10aef2a397 Merge pull request #10 from Permissionless-Software-Foundation/dh-nsec-npub
feat(nostr): Generate Nostr nsec and npub
2025-05-22 08:17:47 -07:00
Daniel Gonzalez a77147d077 feat(nostr): Generate Nostr nsec and npub 2025-05-21 14:59:10 -04:00
Chris Troutner aa7d6f4bc7 using config value 2025-05-19 16:55:44 -07:00
Chris Troutner 054205c545 Fixing issue with sweeping funds 2025-05-19 16:48:47 -07:00
Chris Troutner 686d51615a fix(config): Moving nostr relay and topic to config file 2025-05-19 16:41:39 -07:00
20 changed files with 7637 additions and 23 deletions
File diff suppressed because it is too large Load Diff
+1242 -2
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -7,6 +7,7 @@
"@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",
@@ -22,6 +23,7 @@
"react": "19.0.0",
"react-bootstrap": "2.10.7",
"react-dom": "19.0.0",
"react-markdown": "10.1.0",
"react-router-dom": "7.1.3",
"react-scripts": "5.0.1",
"stream-browserify": "3.0.0",
@@ -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>
+6 -1
View File
@@ -23,7 +23,9 @@ 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'
import UserDataReview from './user-data-review'
function AppBody (props) {
// Dependency injection through props
const appData = props.appData
@@ -43,6 +45,9 @@ function AppBody (props) {
<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} />} />
<Route path='/user-data/:cid' element={<UserDataReview appData={appData} />} />
</Routes>
{/** Show in all paths except the servers view */}
{/* {appData.currentPath !== '/servers' && <SelectServerButton linkTo='/servers' appData={appData} />} */}
+50 -10
View File
@@ -98,7 +98,7 @@ function NftsForSale (props) {
// Try to get token data.
const tokenData = await appData.wallet.getTokenData(thisToken.tokenId)
console.log('tokenData', tokenData)
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
@@ -115,7 +115,7 @@ function NftsForSale (props) {
}, [appData])
// Fetch mutable data if it exist and get the token icon url
const fetchTokenInfo = useCallback(async (token) => {
const fetchTokenMutableData = useCallback(async (token) => {
try {
// Get the token data
const tokenData = token.tokenData
@@ -135,15 +135,16 @@ function NftsForSale (props) {
if (json.fullSizedUrl && json.fullSizedUrl.includes('http')) {
iconUrl = json.fullSizedUrl
}
const userData = json.userData
// Return icon url
return iconUrl
return { iconUrl, userData }
} catch (error) {
return false
}
}, [appData])
// This function loads the token icons from the ipfs gateways.
const lazyLoadTokenIcons = useCallback(async (tokens) => {
const lazyLoadMutableData = useCallback(async (tokens) => {
try {
setIconsAreLoaded(false)
// map each token and fetch the icon url
@@ -154,11 +155,12 @@ function NftsForSale (props) {
if (thisToken.iconAlreadyDownloaded) continue
// Try to get token icon url from mutable data.
const iconUrl = await fetchTokenInfo(thisToken)
const { iconUrl, userData } = await fetchTokenMutableData(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
thisToken.tokenData.userData = userData
}
// Mark token to prevent fetch token icon again.
@@ -169,7 +171,39 @@ function NftsForSale (props) {
} catch (error) {
setIconsAreLoaded(true)
}
}, [fetchTokenInfo])
}, [fetchTokenMutableData, appData])
// Check if exiist token data in the cache and add it to the tokens object
const reviewNftCachedData = useCallback(async (offers) => {
const cacheData = appData.nftForSaleCacheData
console.log(`Token data from cache: ${JSON.stringify(cacheData, null, 2)}`)
// Map all offers
for (let i = 0; i < offers.length; i++) {
const thisToken = offers[i]
const cacheToken = cacheData[thisToken.tokenId]
// If cache data exists, add it to the token object
if (cacheToken) {
thisToken.tokenData = cacheToken.tokenData
thisToken.icon = cacheToken.tokenIcon
}
}
}, [appData])
// Check if exiist token data in the cache and add it to the tokens object
const updateNFTCachedData = useCallback(async (offers) => {
// Map all offers
for (let i = 0; i < offers.length; i++) {
const thisToken = offers[i]
// save token icon and token data in cache
const newTokenCacheData = {
tokenIcon: thisToken.icon,
tokenData: thisToken.tokenData
}
console.log('newTokenCacheData: ', newTokenCacheData)
appData.updateNFTCachedData(thisToken.tokenId, newTokenCacheData)
}
}, [appData])
// Main function to load NFT offers
const loadNftOffers = useCallback(async () => {
@@ -178,17 +212,23 @@ function NftsForSale (props) {
// Get tokens in offers
const offers = await getNftOffers()
console.log('offers: ', offers)
// Review cached data to populate token data from cached data
await reviewNftCachedData(offers)
// set state to start displaying tokens cards.
setOffers(offers)
// Load tokens data
// Load tokens data for any updates to the token data
await lazyLoadTokenData(offers)
// Load tokens icons
await lazyLoadTokenIcons(offers)
// Load tokens icons from mutable data
await lazyLoadMutableData(offers)
// Update cached data with the latest retrieved data
await updateNFTCachedData(offers)
setIsLoading(false)
} catch (err) {
setIsLoading(false)
console.error('Error loading NFT offers:', err)
}
}, [lazyLoadTokenData, lazyLoadTokenIcons, getNftOffers])
}, [lazyLoadTokenData, lazyLoadMutableData, getNftOffers, reviewNftCachedData, updateNFTCachedData])
// Effect to load NFTs on component mount
useEffect(() => {
@@ -6,11 +6,12 @@
*/
// Global npm libraries
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import { Button, Modal, Container, Row, Col } from 'react-bootstrap'
function InfoButton (props) {
const [show, setShow] = useState(false)
const [mutableDataCid, setMutableDataCid] = useState(null)
const handleClose = () => {
console.log('handleClose()')
@@ -22,6 +23,32 @@ function InfoButton (props) {
console.log('handleOpen()')
setShow(true)
}
// 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
}
// Get token user data if it exists and verify if it contains media or markdown
useEffect(() => {
try {
const userDataStr = props.token.tokenData.userData
if (userDataStr) {
const userData = JSON.parse(userDataStr)
// If user data contains media or markdown, set the mutable data cid
if (userData?.media || userData?.markdown) {
setMutableDataCid(parseCid(props.token.tokenData.mutableData))
}
}
} catch (error) {
// Do nothing
}
}, [props.token, show])
// Replace with dummy button until token data is loaded.
if (!props.token.tokenData) {
@@ -70,6 +97,22 @@ function InfoButton (props) {
<Col xs={8}>{props.token.tokenType}</Col>
</Row>
{mutableDataCid && (
<Row>
<Col xs={4}><b>User Data</b>:</Col>
<Col xs={8}>
<a
href={`/user-data/${mutableDataCid}`}
target='_blank'
rel='noopener noreferrer'
className='btn btn-link p-0'
>
View User Data
</a>
</Col>
</Row>
)}
</Container>
</Modal.Body>
<Modal.Footer />
@@ -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
+4 -1
View File
@@ -41,7 +41,10 @@ const SweepWif = (props) => {
// Handle sweep function
const handleSweep = async (e) => {
e.preventDefault()
if (e) {
e.preventDefault()
}
try {
console.log(`Sweeping this WIF: ${wifToSweep}`)
@@ -0,0 +1,97 @@
/*
Search in the provided cid of a mutable data, get the data from the user data and show it
*/
// Global npm libraries
import React, { useState, useEffect } from 'react'
import { useParams } from 'react-router-dom'
import { Container, Row, Col, Tabs, Tab, Spinner } from 'react-bootstrap'
import ReactMarkdown from 'react-markdown'
function UserDataReview (props) {
const appData = props.appData
const [media, setMedia] = useState([])
const [markdown, setMarkdown] = useState('')
const [activeTab, setActiveTab] = useState('media')
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
// Get the id parameter from the URL
const { cid } = useParams()
useEffect(() => {
const loadMedia = async () => {
setLoading(true)
try {
const { json } = await appData.wallet.cid2json({ cid })
const userDataString = json.userData
if (userDataString) {
const userData = JSON.parse(userDataString)
setMedia(userData.media)
setMarkdown(userData.markdown)
}
} catch (error) {
setError(error.message)
}
setLoading(false)
}
loadMedia()
}, [cid, appData.wallet])
return (
<Container>
<Row>
<Col>
{error && <p className='text-danger'>{error}</p>}
{loading
? (
<div className='text-center my-5'>
<Spinner animation='border' role='status' variant='primary'>
<span className='visually-hidden'>Loading...</span>
</Spinner>
</div>
)
: (
<Tabs
activeKey={activeTab}
onSelect={(k) => setActiveTab(k)}
className='mb-4'
>
<Tab eventKey='media' title='Media'>
{media && media.length > 0
? (
<Row className='mt-3'>
{media.map((item, index) => (
<Col key={index} xs={12} sm={6} md={4} lg={4} className='mb-3'>
<img
src={item.url}
alt={`Review media ${index + 1}`}
style={{ width: '100%', height: '250px', objectFit: 'cover', borderRadius: '8px' }}
/>
</Col>
))}
</Row>
)
: (
<p className='mt-3'>No media content available</p>
)}
</Tab>
<Tab eventKey='markdown' title='Content'>
{markdown
? (
<div className='markdown-content mt-3'>
<ReactMarkdown>{markdown}</ReactMarkdown>
</div>
)
: (
<p className='mt-3'>No content available</p>
)}
</Tab>
</Tabs>
)}
</Col>
</Row>
</Container>
)
}
export default UserDataReview
+14
View File
@@ -100,6 +100,20 @@ function NavMenu (props) {
>
Configuration
</NavLink>
<NavLink
className={currentPath === '/nostr-post' ? 'nav-link-active' : 'nav-link-inactive'}
to='/nostr-post'
onClick={handleClickEvent}
>
Nostr Post
</NavLink>
<NavLink
className={currentPath === '/nostr-read' ? 'nav-link-active' : 'nav-link-inactive'}
to='/nostr-read'
onClick={handleClickEvent}
>
Nostr Profile
</NavLink>
</Nav>
</Navbar.Collapse>
</Navbar>
+4 -1
View File
@@ -14,7 +14,10 @@ const config = {
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'
dexServer: 'https://dex-api.fullstack.cash',
nostrTopic: 'bch-dex-test-topic-02',
nostrRelay: 'wss://nostr-relay.psfoundation.info'
}
+17 -1
View File
@@ -37,6 +37,9 @@ function useAppState () {
const [hideSpinner, setHideSpinner] = useState(false)
const [denyClose, setDenyClose] = useState(false)
// NFTs for sale stored data to improve performance
const [nftForSaleCacheData, setNftForSaleCacheData] = useState({})
// The wallet state makes this a true progressive web app (PWA). As
// balances, UTXOs, and tokens are retrieved, this state is updated.
// properties are enumerated here for the purpose of documentation.
@@ -96,6 +99,16 @@ function useAppState () {
}
}
// Update NFT for sale tokens data
function updateNFTCachedData (tokenId, data) {
const allCacheData = nftForSaleCacheData // Get all cache data
const cacheData = allCacheData[tokenId] || {} // Get cache data for the tokenId
const newCacheData = Object.assign({}, cacheData, data) // Merge the new data with the cache data
allCacheData[tokenId] = newCacheData // Update the cache data
setNftForSaleCacheData(allCacheData) // Update the state
}
return {
serverUrl,
setServerUrl,
@@ -131,7 +144,10 @@ function useAppState () {
dexLib,
setDexLib,
nostr,
setNostr
setNostr,
nftForSaleCacheData,
setNftForSaleCacheData,
updateNFTCachedData
}
}
+38 -1
View File
@@ -10,6 +10,10 @@ import BchDexLib from 'bch-dex-lib'
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 () {
@@ -59,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) {
@@ -246,6 +256,33 @@ class AsyncLoad {
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) {
+5 -4
View File
@@ -13,6 +13,7 @@ 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 = {}) {
@@ -23,8 +24,8 @@ class NostrBrowser {
this.bchWallet = localConfig.bchWallet
this.bchNostr = new BchNostr({
relayWs: 'wss://nostr-relay.psfoundation.info',
topic: 'bch-dex-test-topic-01'
relayWs: config.nostrRelay,
topic: config.nostrTopic
})
}
@@ -68,11 +69,11 @@ class NostrBrowser {
// tags: [['t', 'bch-dex-test-topic-01']]
// }
const relayWs = 'wss://nostr-relay.psfoundation.info'
const relayWs = config.nostrRelay
const eventTemplate = {
kind: 867,
created_at: Math.floor(Date.now() / 1000),
tags: [['t', 'bch-dex-test-topic-01']],
tags: [['t', config.nostrTopic]],
content: msg
}