mirror of
https://github.com/smolgrrr/TAO.git
synced 2024-09-20 09:21:25 +00:00
Merge pull request #14 from smolgrrr/refactor-submit
Add remote PoW server
This commit is contained in:
commit
51927fefc4
@ -5,6 +5,7 @@
|
||||
"dependencies": {
|
||||
"@emoji-mart/data": "^1.1.2",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@headlessui/react": "latest",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^17.0.45",
|
||||
|
@ -11,37 +11,7 @@ import { publish } from "../../utils/relays";
|
||||
import { renderMedia, attachFile } from "../../utils/FileUpload";
|
||||
import { EmojiPicker } from "./Emojis/emoji-picker";
|
||||
import customEmojis from './custom_emojis.json';
|
||||
|
||||
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 };
|
||||
};
|
||||
import { useSubmitForm } from "./handleSubmit";
|
||||
|
||||
interface FormProps {
|
||||
refEvent?: NostrEvent;
|
||||
@ -60,26 +30,18 @@ const NewNoteCard = ({
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [comment, setComment] = useState("");
|
||||
const [file, setFile] = useState("");
|
||||
const [sk, setSk] = useState(generatePrivateKey());
|
||||
const [unsigned, setUnsigned] = useState<UnsignedEvent>({
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: "",
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: getPublicKey(sk),
|
||||
pubkey: "",
|
||||
});
|
||||
const [difficulty, setDifficulty] = useState(
|
||||
localStorage.getItem("difficulty") || "21"
|
||||
);
|
||||
const [fileSizeError, setFileSizeError] = 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(() => {
|
||||
if (refEvent && tagType && unsigned.tags.length === 0) {
|
||||
@ -111,31 +73,21 @@ const NewNoteCard = ({
|
||||
...prevUnsigned,
|
||||
content: `${comment} ${file}`,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: getPublicKey(sk),
|
||||
}));
|
||||
}, [comment, file]);
|
||||
|
||||
useEffect(() => {
|
||||
setDoingWorkProp(false);
|
||||
if (messageFromWorker) {
|
||||
try {
|
||||
const signedEvent = finishEvent(messageFromWorker, sk);
|
||||
publish(signedEvent);
|
||||
const { handleSubmit: originalHandleSubmit, doingWorkProp, doingWorkProgress } = useSubmitForm(unsigned, difficulty);
|
||||
|
||||
setComment("");
|
||||
setFile("");
|
||||
setSk(generatePrivateKey());
|
||||
setUnsigned(prevUnsigned => ({
|
||||
...prevUnsigned,
|
||||
content: '',
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: getPublicKey(sk),
|
||||
}));
|
||||
} catch (error) {
|
||||
setComment(error + " " + comment);
|
||||
}
|
||||
}
|
||||
}, [messageFromWorker]);
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
await originalHandleSubmit(event);
|
||||
setComment("");
|
||||
setFile("");
|
||||
setUnsigned(prevUnsigned => ({
|
||||
...prevUnsigned,
|
||||
content: '',
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
}));
|
||||
};
|
||||
|
||||
//Emoji stuff
|
||||
const emojiRef = useRef(null);
|
||||
@ -180,11 +132,7 @@ const NewNoteCard = ({
|
||||
method="post"
|
||||
encType="multipart/form-data"
|
||||
className=""
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
startWork();
|
||||
setDoingWorkProp(true);
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<input type="hidden" name="MAX_FILE_SIZE" defaultValue={2.5 * 1024 * 1024} />
|
||||
<div className="px-4 flex flex-col rounded-lg">
|
||||
@ -276,8 +224,8 @@ const NewNoteCard = ({
|
||||
{doingWorkProp ? (
|
||||
<div className="flex animate-pulse text-sm text-gray-300">
|
||||
<CpuChipIcon className="h-4 w-4 ml-auto" />
|
||||
<span>Generating Proof-of-Work:</span>
|
||||
<span>iteration {doingWorkProgress}</span>
|
||||
<span>Generating Proof-of-Work.</span>
|
||||
{doingWorkProgress && <span>Current iteration {doingWorkProgress}</span>}
|
||||
</div>
|
||||
) : null}
|
||||
<div id="postFormError" className="text-red-500" />
|
||||
|
98
client/src/components/Forms/handleSubmit.ts
Normal file
98
client/src/components/Forms/handleSubmit.ts
Normal 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 };
|
||||
};
|
@ -1,6 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||
import { ArrowUpOnSquareIcon, PlusCircleIcon } from '@heroicons/react/24/outline';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
declare global {
|
||||
interface Navigator {
|
||||
@ -44,7 +46,21 @@ const AddToHomeScreenPrompt: React.FC = () => {
|
||||
}
|
||||
|
||||
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"
|
||||
>
|
||||
​
|
||||
</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="flex flex-col text-white">
|
||||
<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);}}>
|
||||
<XMarkIcon className="h-6 w-6 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,25 +1,61 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
// import {powEvent} from './system';
|
||||
// import {publish} from './relays';
|
||||
import { addRelay } from '../utils/relays';
|
||||
import React, { useState } from 'react';
|
||||
import { CpuChipIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
type TestResponse = {
|
||||
timeTaken: string;
|
||||
hashrate: string;
|
||||
};
|
||||
|
||||
const Settings = () => {
|
||||
const [filterDifficulty, setFilterDifficulty] = useState(localStorage.getItem('filterDifficulty') || 20);
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
localStorage.setItem('filterDifficulty', String(filterDifficulty));
|
||||
localStorage.setItem('difficulty', String(difficulty));
|
||||
localStorage.setItem('powserver', String(powServer));
|
||||
|
||||
const eventData = {
|
||||
difficulty: String(difficulty),
|
||||
filterDifficulty: String(filterDifficulty),
|
||||
powServer: String(powServer),
|
||||
};
|
||||
const event = new CustomEvent('settingsChanged', { detail: eventData });
|
||||
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 (
|
||||
<div className="settings-page bg-black text-white p-8 flex flex-col h-full">
|
||||
@ -56,7 +92,47 @@ const Settings = () => {
|
||||
/>
|
||||
</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
|
||||
</button>
|
||||
</form>
|
||||
@ -76,7 +152,7 @@ const Settings = () => {
|
||||
</a>
|
||||
<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>
|
||||
<img className="h-16" src="doot.jpeg"/>
|
||||
<img className="h-16" src="doot.jpeg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,15 +11,9 @@ import (
|
||||
"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
|
||||
type PowRequest struct {
|
||||
ReqEvent EventContent `json:"req_event"`
|
||||
ReqEvent *nostr.Event `json:"req_event"`
|
||||
Difficulty string `json:"difficulty"`
|
||||
}
|
||||
|
||||
@ -44,41 +38,100 @@ func handlePOW(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create a nostr Event
|
||||
unsignedEvent := &nostr.Event{
|
||||
Kind: nostr.KindTextNote,
|
||||
CreatedAt: nostr.Now(),
|
||||
Content: powReq.ReqEvent.Content,
|
||||
PubKey: powReq.ReqEvent.Pubkey,
|
||||
// Generate proof of work for the event
|
||||
generatedEvent, err := nip13.Generate(powReq.ReqEvent, difficulty, 3*time.Hour)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 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 := time.Now()
|
||||
|
||||
// Generate proof of work for the event
|
||||
generatedEvent, err := nip13.Generate(unsignedEvent, difficulty, 3*time.Hour)
|
||||
event := &nostr.Event{
|
||||
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 {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
// Create a response struct
|
||||
type Response struct {
|
||||
Event *nostr.Event `json:"event"`
|
||||
Hashrate float64 `json:"hashrate"`
|
||||
TimeTaken float64 `json:"timeTaken"`
|
||||
Hashrate float64 `json:"hashrate"`
|
||||
}
|
||||
|
||||
// Respond with the generated event and the time taken
|
||||
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() {
|
||||
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))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user