Merge pull request #14 from smolgrrr/refactor-submit

Add remote PoW server
This commit is contained in:
smolgrrr 2023-11-21 21:42:57 +11:00 committed by GitHub
commit 51927fefc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 291 additions and 97 deletions

View File

@ -5,6 +5,7 @@
"dependencies": { "dependencies": {
"@emoji-mart/data": "^1.1.2", "@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1", "@emoji-mart/react": "^1.1.1",
"@headlessui/react": "latest",
"@heroicons/react": "^2.0.18", "@heroicons/react": "^2.0.18",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/node": "^17.0.45", "@types/node": "^17.0.45",

View File

@ -11,37 +11,7 @@ import { publish } from "../../utils/relays";
import { renderMedia, attachFile } from "../../utils/FileUpload"; import { renderMedia, attachFile } from "../../utils/FileUpload";
import { EmojiPicker } from "./Emojis/emoji-picker"; import { EmojiPicker } from "./Emojis/emoji-picker";
import customEmojis from './custom_emojis.json'; import customEmojis from './custom_emojis.json';
import { useSubmitForm } from "./handleSubmit";
const useWorkers = (numCores: number, unsigned: UnsignedEvent, difficulty: string, deps: any[]) => {
const [messageFromWorker, setMessageFromWorker] = useState(null);
const [doingWorkProgress, setDoingWorkProgress] = useState(0);
const startWork = () => {
const workers = Array(numCores).fill(null).map(() => new Worker(new URL("../../powWorker", import.meta.url)));
workers.forEach((worker, index) => {
worker.onmessage = (event) => {
if (event.data.status === 'progress') {
console.log(`Worker progress: Checked ${event.data.currentNonce} nonces.`);
setDoingWorkProgress(event.data.currentNonce);
} else if (event.data.found) {
setMessageFromWorker(event.data.event);
// Terminate all workers once a solution is found
workers.forEach(w => w.terminate());
}
};
worker.postMessage({
unsigned,
difficulty,
nonceStart: index, // Each worker starts from its index
nonceStep: numCores // Each worker increments by the total number of workers
});
});
};
return { startWork, messageFromWorker, doingWorkProgress };
};
interface FormProps { interface FormProps {
refEvent?: NostrEvent; refEvent?: NostrEvent;
@ -60,26 +30,18 @@ const NewNoteCard = ({
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
const [comment, setComment] = useState(""); const [comment, setComment] = useState("");
const [file, setFile] = useState(""); const [file, setFile] = useState("");
const [sk, setSk] = useState(generatePrivateKey());
const [unsigned, setUnsigned] = useState<UnsignedEvent>({ const [unsigned, setUnsigned] = useState<UnsignedEvent>({
kind: 1, kind: 1,
tags: [], tags: [],
content: "", content: "",
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
pubkey: getPublicKey(sk), pubkey: "",
}); });
const [difficulty, setDifficulty] = useState( const [difficulty, setDifficulty] = useState(
localStorage.getItem("difficulty") || "21" localStorage.getItem("difficulty") || "21"
); );
const [fileSizeError, setFileSizeError] = useState(false); const [fileSizeError, setFileSizeError] = useState(false);
const [uploadingFile, setUploadingFile] = useState(false); const [uploadingFile, setUploadingFile] = useState(false);
const [doingWorkProp, setDoingWorkProp] = useState(false);
// Initialize the worker outside of any effects
const numCores = navigator.hardwareConcurrency || 4;
const { startWork, messageFromWorker, doingWorkProgress } = useWorkers(numCores, unsigned, difficulty, [unsigned]);
useEffect(() => { useEffect(() => {
if (refEvent && tagType && unsigned.tags.length === 0) { if (refEvent && tagType && unsigned.tags.length === 0) {
@ -111,31 +73,21 @@ const NewNoteCard = ({
...prevUnsigned, ...prevUnsigned,
content: `${comment} ${file}`, content: `${comment} ${file}`,
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
pubkey: getPublicKey(sk),
})); }));
}, [comment, file]); }, [comment, file]);
useEffect(() => { const { handleSubmit: originalHandleSubmit, doingWorkProp, doingWorkProgress } = useSubmitForm(unsigned, difficulty);
setDoingWorkProp(false);
if (messageFromWorker) {
try {
const signedEvent = finishEvent(messageFromWorker, sk);
publish(signedEvent);
setComment(""); const handleSubmit = async (event: React.FormEvent) => {
setFile(""); await originalHandleSubmit(event);
setSk(generatePrivateKey()); setComment("");
setUnsigned(prevUnsigned => ({ setFile("");
...prevUnsigned, setUnsigned(prevUnsigned => ({
content: '', ...prevUnsigned,
created_at: Math.floor(Date.now() / 1000), content: '',
pubkey: getPublicKey(sk), created_at: Math.floor(Date.now() / 1000)
})); }));
} catch (error) { };
setComment(error + " " + comment);
}
}
}, [messageFromWorker]);
//Emoji stuff //Emoji stuff
const emojiRef = useRef(null); const emojiRef = useRef(null);
@ -180,11 +132,7 @@ const NewNoteCard = ({
method="post" method="post"
encType="multipart/form-data" encType="multipart/form-data"
className="" className=""
onSubmit={(event) => { onSubmit={handleSubmit}
event.preventDefault();
startWork();
setDoingWorkProp(true);
}}
> >
<input type="hidden" name="MAX_FILE_SIZE" defaultValue={2.5 * 1024 * 1024} /> <input type="hidden" name="MAX_FILE_SIZE" defaultValue={2.5 * 1024 * 1024} />
<div className="px-4 flex flex-col rounded-lg"> <div className="px-4 flex flex-col rounded-lg">
@ -276,8 +224,8 @@ const NewNoteCard = ({
{doingWorkProp ? ( {doingWorkProp ? (
<div className="flex animate-pulse text-sm text-gray-300"> <div className="flex animate-pulse text-sm text-gray-300">
<CpuChipIcon className="h-4 w-4 ml-auto" /> <CpuChipIcon className="h-4 w-4 ml-auto" />
<span>Generating Proof-of-Work:</span> <span>Generating Proof-of-Work.</span>
<span>iteration {doingWorkProgress}</span> {doingWorkProgress && <span>Current iteration {doingWorkProgress}</span>}
</div> </div>
) : null} ) : null}
<div id="postFormError" className="text-red-500" /> <div id="postFormError" className="text-red-500" />

View File

@ -0,0 +1,98 @@
import { useState, useEffect } from "react";
import { generatePrivateKey, getPublicKey, finishEvent, UnsignedEvent } from "nostr-tools";
import { publish } from "../../utils/relays";
const useWorkers = (numCores: number, unsigned: UnsignedEvent, difficulty: string, deps: any[]) => {
const [messageFromWorker, setMessageFromWorker] = useState(null);
const [doingWorkProgress, setDoingWorkProgress] = useState(0);
const startWork = () => {
const workers = Array(numCores).fill(null).map(() => new Worker(new URL("../../powWorker", import.meta.url)));
workers.forEach((worker, index) => {
worker.onmessage = (event) => {
if (event.data.status === 'progress') {
console.log(`Worker progress: Checked ${event.data.currentNonce} nonces.`);
setDoingWorkProgress(event.data.currentNonce);
} else if (event.data.found) {
setMessageFromWorker(event.data.event);
// Terminate all workers once a solution is found
workers.forEach(w => w.terminate());
}
};
worker.postMessage({
unsigned,
difficulty,
nonceStart: index, // Each worker starts from its index
nonceStep: numCores // Each worker increments by the total number of workers
});
});
};
return { startWork, messageFromWorker, doingWorkProgress };
};
export const useSubmitForm = (unsigned: UnsignedEvent, difficulty: string) => {
const [doingWorkProp, setDoingWorkProp] = useState(false);
const [sk, setSk] = useState(generatePrivateKey());
const unsignedWithPubkey = { ...unsigned, pubkey: getPublicKey(sk) };
const powServer = useState(localStorage.getItem('powserver') || '');
const [unsignedPoWEvent, setUnsignedPoWEvent] = useState<UnsignedEvent>()
// Initialize the worker outside of any effects
const numCores = navigator.hardwareConcurrency || 4;
const { startWork, messageFromWorker, doingWorkProgress } = useWorkers(numCores, unsignedWithPubkey, difficulty, [unsignedWithPubkey]);
console.log(powServer[0])
useEffect(() => {
if (unsignedPoWEvent) {
setDoingWorkProp(false);
const signedEvent = finishEvent(unsignedPoWEvent, sk);
publish(signedEvent);
}
}, [unsignedPoWEvent]);
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
setDoingWorkProp(true);
console.log(powServer[0])
if (powServer[0]) {
const inEventFormat = { ...unsignedWithPubkey, sig: "" };
const powRequest = {
req_event: inEventFormat,
difficulty: difficulty
};
fetch(`${powServer[0]}/powgen`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(powRequest)
})
.then(response => response.json())
.then(data => {
console.log(data);
// handle the response data
setUnsignedPoWEvent(data.event)
})
.catch(error => {
console.error('Error:', error);
});
} else {
startWork();
}
};
useEffect(() => {
if (messageFromWorker) {
setUnsignedPoWEvent(messageFromWorker);
}
}, [messageFromWorker]);
return { handleSubmit, doingWorkProp, doingWorkProgress };
};

View File

@ -1,6 +1,8 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { XMarkIcon } from "@heroicons/react/24/solid"; import { XMarkIcon } from "@heroicons/react/24/solid";
import { ArrowUpOnSquareIcon, PlusCircleIcon } from '@heroicons/react/24/outline'; import { ArrowUpOnSquareIcon, PlusCircleIcon } from '@heroicons/react/24/outline';
import { Dialog, Transition } from '@headlessui/react';
import { Fragment } from 'react';
declare global { declare global {
interface Navigator { interface Navigator {
@ -44,7 +46,21 @@ const AddToHomeScreenPrompt: React.FC = () => {
} }
return ( return (
<div className="overscroll-contain fixed inset-0 bg-gray-800/40 flex items-center justify-center animate-fade-in"> <Transition appear show={inMobileBrowser} as={Fragment}>
<Dialog
as="div"
className="fixed inset-0 z-10 overflow-y-auto"
onClose={() => setInMobileBrowser(false)}
>
<div className="min-h-screen px-4 text-center">
<Dialog.Overlay className="fixed inset-0 bg-gray-800 opacity-40" />
<span
className="inline-block h-screen align-middle"
aria-hidden="true"
>
&#8203;
</span>
<div className="fixed bottom-0 left-0 right-0 p-4 bg-neutral-900 rounded-lg m-2 border border-neutral-700 shadow-md flex justify-between items-center animate-slide-up"> <div className="fixed bottom-0 left-0 right-0 p-4 bg-neutral-900 rounded-lg m-2 border border-neutral-700 shadow-md flex justify-between items-center animate-slide-up">
<div className="flex flex-col text-white"> <div className="flex flex-col text-white">
<span className="font-semibold">Stay Wired</span> <span className="font-semibold">Stay Wired</span>
@ -65,8 +81,10 @@ const AddToHomeScreenPrompt: React.FC = () => {
<button className="absolute top-2 right-2" onClick={() => {setInMobileBrowser(!inMobileBrowser);}}> <button className="absolute top-2 right-2" onClick={() => {setInMobileBrowser(!inMobileBrowser);}}>
<XMarkIcon className="h-6 w-6 text-white" /> <XMarkIcon className="h-6 w-6 text-white" />
</button> </button>
</div> </div>
</div> </div>
</Dialog>
</Transition>
); );
}; };

View File

@ -1,25 +1,61 @@
import React, { useEffect, useState } from 'react'; import React, { useState } from 'react';
// import {powEvent} from './system';
// import {publish} from './relays';
import { addRelay } from '../utils/relays';
import { CpuChipIcon } from '@heroicons/react/24/outline'; import { CpuChipIcon } from '@heroicons/react/24/outline';
type TestResponse = {
timeTaken: string;
hashrate: string;
};
const Settings = () => { const Settings = () => {
const [filterDifficulty, setFilterDifficulty] = useState(localStorage.getItem('filterDifficulty') || 20); const [filterDifficulty, setFilterDifficulty] = useState(localStorage.getItem('filterDifficulty') || 20);
const [difficulty, setDifficulty] = useState(localStorage.getItem('difficulty') || 21); const [difficulty, setDifficulty] = useState(localStorage.getItem('difficulty') || 21);
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
const [powServer, setPowServer] = useState(localStorage.getItem('powserver') || '');
const [testDiff, setTestDiff] = useState('21')
const [testResult, setTestResult] = useState<TestResponse>()
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
localStorage.setItem('filterDifficulty', String(filterDifficulty)); localStorage.setItem('filterDifficulty', String(filterDifficulty));
localStorage.setItem('difficulty', String(difficulty)); localStorage.setItem('difficulty', String(difficulty));
localStorage.setItem('powserver', String(powServer));
const eventData = { const eventData = {
difficulty: String(difficulty), difficulty: String(difficulty),
filterDifficulty: String(filterDifficulty), filterDifficulty: String(filterDifficulty),
powServer: String(powServer),
}; };
const event = new CustomEvent('settingsChanged', { detail: eventData }); const event = new CustomEvent('settingsChanged', { detail: eventData });
window.dispatchEvent(event); window.dispatchEvent(event);
}; };
console.log(powServer)
const handleTest = () => {
setTestResult({ timeTaken: '...', hashrate: '...' });
console.log(powServer[0])
if (powServer[0]) {
const testRequest = {
Difficulty: testDiff
};
fetch(`${powServer}/test`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(testRequest)
})
.then(response => response.json())
.then(data => {
console.log(data);
// handle the response data
setTestResult(data)
})
.catch(error => {
console.error('Error:', error);
});
}
};
return ( return (
<div className="settings-page bg-black text-white p-8 flex flex-col h-full"> <div className="settings-page bg-black text-white p-8 flex flex-col h-full">
@ -56,7 +92,47 @@ const Settings = () => {
/> />
</div> </div>
</div> </div>
<button className="bg-black border text-white font-bold py-2 px-4 rounded"> <div className='pb-4'>
<span onClick={() => setShowAdvancedSettings(!showAdvancedSettings)} className="">
{">"} Advanced Settings
</span>
{showAdvancedSettings && (
<><div className={`transition-height duration-200 ease-in-out overflow-hidden ${showAdvancedSettings ? 'h-auto' : 'h-0'} w-full md:w-1/3 px-2 mb-4 md:mb-0`}>
<label className="block text-xs mb-2" htmlFor="powServer">
Remote PoW Server:
</label>
<input
id="powServer"
type="text"
value={powServer}
onChange={e => setPowServer(e.target.value)}
className="w-full px-3 py-2 border rounded-md bg-black"
/>
</div>
<div className="px-2">
<label className="block text-xs mb-2" htmlFor="powServer">
Test Your PoW Server (difficulty):
</label>
<input
id="testAPI"
type="text"
value={testDiff}
onChange={e => setTestDiff(e.target.value)}
className="w-12 px-3 py-2 border rounded-md bg-black"
/>
<button type="button" onClick={handleTest} className="bg-black border text-white font-bold py-2 px-4 rounded">
Test
</button>
{testResult && (
<span>Time: {testResult.timeTaken}s with a hashrate of {testResult.hashrate}</span>
)}
</div>
</>
)}
</div>
<button
type="submit"
className="bg-black border text-white font-bold py-2 px-4 rounded">
Save Settings Save Settings
</button> </button>
</form> </form>
@ -76,7 +152,7 @@ const Settings = () => {
</a> </a>
<div> <div>
<span>Found a bug? dm me: <a className="underline" href="https://njump.me/npub13azv2cf3kd3xdzcwqxlgcudjg7r9nzak37usnn7h374lkpvd6rcq4k8m54">doot</a> or <a className="underline" href="mailto:smolgrrr@protonmail.com">smolgrrr@protonmail.com</a></span> <span>Found a bug? dm me: <a className="underline" href="https://njump.me/npub13azv2cf3kd3xdzcwqxlgcudjg7r9nzak37usnn7h374lkpvd6rcq4k8m54">doot</a> or <a className="underline" href="mailto:smolgrrr@protonmail.com">smolgrrr@protonmail.com</a></span>
<img className="h-16" src="doot.jpeg"/> <img className="h-16" src="doot.jpeg" />
</div> </div>
</div> </div>
</div> </div>

View File

@ -11,15 +11,9 @@ import (
"github.com/nbd-wtf/go-nostr/nip13" "github.com/nbd-wtf/go-nostr/nip13"
) )
// EventContent struct for request body
type EventContent struct {
Content string `json:"content"`
Pubkey string `json:"pubkey"`
}
// PowRequest struct for the POST request // PowRequest struct for the POST request
type PowRequest struct { type PowRequest struct {
ReqEvent EventContent `json:"req_event"` ReqEvent *nostr.Event `json:"req_event"`
Difficulty string `json:"difficulty"` Difficulty string `json:"difficulty"`
} }
@ -44,41 +38,100 @@ func handlePOW(w http.ResponseWriter, r *http.Request) {
return return
} }
// Create a nostr Event // Generate proof of work for the event
unsignedEvent := &nostr.Event{ generatedEvent, err := nip13.Generate(powReq.ReqEvent, difficulty, 3*time.Hour)
Kind: nostr.KindTextNote, if err != nil {
CreatedAt: nostr.Now(), http.Error(w, err.Error(), http.StatusInternalServerError)
Content: powReq.ReqEvent.Content, return
PubKey: powReq.ReqEvent.Pubkey, }
// Create a response struct
type Response struct {
Event *nostr.Event `json:"event"`
}
// Respond with the generated event and the time taken
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(Response{Event: generatedEvent})
}
// PowRequest struct for the POST request
type TestRequest struct {
Difficulty string `json:"difficulty"`
}
// handlePOW is the handler function for the "/powgen" endpoint
func handleTest(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
return
}
var powReq PowRequest
err := json.NewDecoder(r.Body).Decode(&powReq)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
difficulty, err := strconv.Atoi(powReq.Difficulty)
if err != nil {
// handle error
http.Error(w, err.Error(), http.StatusBadRequest)
return
} }
// Start the timer // Start the timer
start := time.Now() start := time.Now()
// Generate proof of work for the event event := &nostr.Event{
generatedEvent, err := nip13.Generate(unsignedEvent, difficulty, 3*time.Hour) Kind: nostr.KindTextNote,
Content: "It's just me mining my own business",
PubKey: "a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243",
}
pow, err := nip13.Generate(event, difficulty, 3*time.Hour)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
// Calculate the duration in milliseconds // Calculate the duration in milliseconds
iterations, _ := strconv.ParseFloat(generatedEvent.Tags[0][1], 64) iterations, _ := strconv.ParseFloat(pow.Tags[0][1], 64)
timeTaken := time.Since(start).Seconds()
hashrate := iterations / time.Since(start).Seconds() hashrate := iterations / time.Since(start).Seconds()
// Create a response struct // Create a response struct
type Response struct { type Response struct {
Event *nostr.Event `json:"event"` TimeTaken float64 `json:"timeTaken"`
Hashrate float64 `json:"hashrate"` Hashrate float64 `json:"hashrate"`
} }
// Respond with the generated event and the time taken // Respond with the generated event and the time taken
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(Response{Event: generatedEvent, Hashrate: hashrate}) json.NewEncoder(w).Encode(Response{TimeTaken: timeTaken, Hashrate: hashrate})
}
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Set headers
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
// If it's a preflight request, respond with 200
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
// Next
next.ServeHTTP(w, r)
})
} }
func main() { func main() {
http.HandleFunc("/powgen", handlePOW) http.Handle("/powgen", corsMiddleware(http.HandlerFunc(handlePOW)))
http.Handle("/test", corsMiddleware(http.HandlerFunc(handleTest)))
log.Fatal(http.ListenAndServe("0.0.0.0:8080", nil)) log.Fatal(http.ListenAndServe("0.0.0.0:8080", nil))
} }