From 6b9626aeb1bc2f045f5d2bcc7cad264b7332e5ed Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Fri, 23 May 2025 18:41:05 -0400 Subject: [PATCH] feat(nostr): Created nostr views --- .../app-body/bch-wallet/wallet-summary.js | 34 +--- src/components/app-body/index.js | 5 + src/components/app-body/nostr/nostr-post.js | 158 ++++++++++++++++++ src/components/app-body/nostr/nostr-read.js | 67 ++++++++ src/components/nav-menu/index.js | 14 ++ src/services/async-load.js | 32 +++- 6 files changed, 277 insertions(+), 33 deletions(-) create mode 100644 src/components/app-body/nostr/nostr-post.js create mode 100644 src/components/app-body/nostr/nostr-read.js diff --git a/src/components/app-body/bch-wallet/wallet-summary.js b/src/components/app-body/bch-wallet/wallet-summary.js index 470d8ce..0a1b888 100644 --- a/src/components/app-body/bch-wallet/wallet-summary.js +++ b/src/components/app-body/bch-wallet/wallet-summary.js @@ -3,7 +3,7 @@ */ // Global npm libraries -import React, { useCallback, useEffect, useState } from 'react' +import React, { useState } from 'react' import { Container, Row, Col, Card } from 'react-bootstrap' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faWallet, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons' @@ -12,9 +12,6 @@ import { faWallet, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons' // Local libraries import './wallet-summary.css' import CopyOnClick from './copy-on-click' -import { getPublicKey } from 'nostr-tools/pure' -import { base58_to_binary as base58ToBinary } from 'base58-js' -import { bytesToHex } from '@noble/hashes/utils' // already an installed dependency function WalletSummary (props) { // Props @@ -28,10 +25,7 @@ function WalletSummary (props) { const [blurredPrivateKey, setBlurredPrivateKey] = useState(true) const [blurredNostrPrivKey, setBlurredNostrPrivKey] = useState(true) - const [nostrKeyPair, setNostrKeyPair] = useState({ - privHex: '', - pubHex: '' - }) + const [nostrKeyPair] = useState(bchWalletState.nostrKeyPair) // Encapsulate component state into an object that can be passed to child functions const walletSummaryData = { @@ -82,30 +76,6 @@ function WalletSummary (props) { } } - const nostrKeyPairFromWIF = useCallback((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) - - const privHex = bytesToHex(privBuf) - console.log('BCH & Nostr private key (HEX format): ', privHex) - - const pubHex = getPublicKey(privBuf) - console.log('nostrPubKey: ', pubHex) - - return { - privHex, - pubHex - } - }, []) - - useEffect(() => { - setNostrKeyPair(nostrKeyPairFromWIF(bchWalletState.privateKey)) - }, [nostrKeyPairFromWIF, bchWalletState]) return ( <> diff --git a/src/components/app-body/index.js b/src/components/app-body/index.js index 6973738..f52238a 100644 --- a/src/components/app-body/index.js +++ b/src/components/app-body/index.js @@ -23,6 +23,8 @@ 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.js' +import NostrRead from './nostr/nostr-read.js' function AppBody (props) { // Dependency injection through props @@ -43,6 +45,9 @@ function AppBody (props) { } /> } /> } /> + } /> + } /> + {/** Show in all paths except the servers view */} {/* {appData.currentPath !== '/servers' && } */} diff --git a/src/components/app-body/nostr/nostr-post.js b/src/components/app-body/nostr/nostr-post.js new file mode 100644 index 0000000..4b2b126 --- /dev/null +++ b/src/components/app-body/nostr/nostr-post.js @@ -0,0 +1,158 @@ +/* +Component for posting nostr information. +*/ + +// 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 NostrPost (props) { + const [accordionKey, setAccordionKey] = useState('1') + 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 ( + <> + + + + Post + + {errorMsg && ( +
+ {errorMsg} +
+ )} + {successMsg && ( +
+ {successMsg} +
+ )} +
+ + Name + + + + + About + + + +
+ {!onFetch && ( + + )} + {onFetch && } +
+ +
+
+
+
+
+ + ) +} + +export default NostrPost diff --git a/src/components/app-body/nostr/nostr-read.js b/src/components/app-body/nostr/nostr-read.js new file mode 100644 index 0000000..bf83f94 --- /dev/null +++ b/src/components/app-body/nostr/nostr-read.js @@ -0,0 +1,67 @@ +// Global npm libraries +import React, { useEffect, useState } from 'react' +import { Container } from 'react-bootstrap' + +import Accordion from 'react-bootstrap/Accordion' + +import { RelayPool } from 'nostr' + +function NostrRead (props) { + const { bchWalletState } = props.appData + const [posts, setPosts] = useState([]) + const [accordionKey, setAccordionKey] = useState('0') + 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: 10, 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) + setPosts(currentPosts => [...currentPosts, ev]) + }) + + setLoaded(true) + } + + if (!loaded) { + start() + } + }, [bchWalletState, loaded]) + + const handleAccordionChange = (key) => { + setAccordionKey(key) + } + + return ( + + + + Read ( {posts.length} post found ) + + {posts.map((post, index) => ( +
+ {post.content} +
+ ))} +
+
+
+
+ ) +} + +export default NostrRead diff --git a/src/components/nav-menu/index.js b/src/components/nav-menu/index.js index dba776f..0eb2a87 100644 --- a/src/components/nav-menu/index.js +++ b/src/components/nav-menu/index.js @@ -100,6 +100,20 @@ function NavMenu (props) { > Configuration + + Nostr Post + + + Nostr Profile + diff --git a/src/services/async-load.js b/src/services/async-load.js index 1cf1bbf..05bf0b8 100644 --- a/src/services/async-load.js +++ b/src/services/async-load.js @@ -10,6 +10,9 @@ 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' class AsyncLoad { constructor () { @@ -59,8 +62,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) + + 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 +255,27 @@ 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) + + const privHex = bytesToHex(privBuf) + console.log('BCH & Nostr private key (HEX format): ', privHex) + + const pubHex = getPublicKey(privBuf) + + return { + privHex, + pubHex + } + } } function sleep (ms) {