sort by work feed

This commit is contained in:
smolgrrr 2024-08-25 14:43:01 +10:00
parent 8231f7bf93
commit 20df7f8507
14 changed files with 104 additions and 532 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -1,4 +1,4 @@
# The Wire: unstoppable free speech # The Wired: unstoppable free speech
An app to facilitate unstoppable free speech on the internet. An app to facilitate unstoppable free speech on the internet.
## Overview ## Overview
@ -6,10 +6,5 @@ Nostr at the core: censorship resistant, also stupid simple and permissionless <
Anon only: Names and Nyms carry reputations/ego and risk doxxing - Anons speak freely <br/> Anon only: Names and Nyms carry reputations/ego and risk doxxing - Anons speak freely <br/>
PoW: Spam and noise PoW: Spam and noise
### Wanting to add ## Back up
https://git.getwired.app/doot/TAO
Ways to make Nostr notes impossible to stop? <br/>
- Secure Scuttlebutt gossip and offline (?): Even in locked down countries, if we have a SSB-like medium, we only need one person to connect to a relay to share other ppl's messages<br/>
- Extremely lightweight relays and gossiping <br/>
- PWAs and bridges: A proliferation of similar, simple PWAs, and the sharing of notes through any means (telegram, email, pigeon) before getting to nostr <br/>
- API connection to do PoW externally to app if wanted

View File

@ -2,7 +2,7 @@ import { PropsWithChildren } from "react";
export default function CardContainer({ children }: PropsWithChildren) { export default function CardContainer({ children }: PropsWithChildren) {
return ( return (
<div className="card break-inside-avoid mb-4 h-min"> <div className="card break-inside-avoid mb-1 h-min">
<div className="card-body">{children}</div> <div className="card-body">{children}</div>
</div> </div>
); );

View File

@ -31,10 +31,30 @@ const PostCard = ({
const icon = getIconFromHash(event.pubkey); const icon = getIconFromHash(event.pubkey);
const metadataParsed = metadata ? getMetadata(metadata) : null; const metadataParsed = metadata ? getMetadata(metadata) : null;
const [relatedEvents, setRelatedEvents] = useState<Event[]>([]); const [relatedEvents, setRelatedEvents] = useState<Event[]>([]);
const [sumReplyPow, setReplySumPow] = useState(0);
const [repostedEvent, setRepostedEvent] = useState<Event>();
const [parsedEvent, setParsedEvent] = useState<Event>(event);
useEffect(() => { useEffect(() => {
const allRelatedEvents = [event, ...(replies || [])]; const allRelatedEvents = [event, ...(replies || [])];
setRelatedEvents(allRelatedEvents); setRelatedEvents(allRelatedEvents);
if (event.kind === 6) {
setRepostedEvent(event)
setParsedEvent(JSON.parse(event.content));
}
// Adjusting the sum calculation to account for exponential growth in work
const sum = replies.reduce((acc, reply) => {
const difficulty = verifyPow(reply);
// Skip adding to the sum if difficulty is 0, assuming 0 means no work was done.
// Adjust this logic if verifyPow uses a different scale or interpretation.
return difficulty > 0 ? acc + Math.pow(2, difficulty) : acc;
}, 0);
// Check if sum is greater than 0 to avoid -Infinity in log2 calculation
const equivalentDifficulty = sum > 0 ? Math.log2(sum) : 0;
setReplySumPow(equivalentDifficulty);
}, [event, replies]); }, [event, replies]);
const handleClick = () => { const handleClick = () => {
@ -44,52 +64,61 @@ const PostCard = ({
} }
}; };
return ( return (
<CardContainer> <CardContainer>
<div className={`flex flex-col gap-2`}> <div className={`flex flex-col gap-2`} key={key}>
<div className={`flex flex-col break-words ${type !== "OP" ? 'hover:cursor-pointer' : ''}`} onClick={handleClick}> <div className={`flex flex-col break-words ${type !== "OP" ? 'hover:cursor-pointer' : ''}`} onClick={handleClick}>
<ContentPreview key={event.id} eventdata={event} /> <ContentPreview key={parsedEvent.id} eventdata={parsedEvent} />
</div> </div>
{repliedTo && <div className="flex items-center mt-1" > {repliedTo && <div className="flex items-center mt-1" >
<span className="text-xs text-gray-500">Reply to: </span> <span className="text-xs text-gray-500">Reply to: </span>
{uniqBy(repliedTo, 'pubkey').map((event, index) => ( {uniqBy(repliedTo, 'pubkey').map((parsedEvent, index) => (
<div key={index}> <div key={index}>
{event.kind === 0 ? ( {event.kind === 0 ? (
<img className={`h-5 w-5 rounded-full`} src={getMetadata(event)?.picture} /> <img className={`h-5 w-5 rounded-full`} src={getMetadata(parsedEvent)?.picture} />
) : ( ) : (
<div className={`h-4 w-4 ${getIconFromHash(event.pubkey)} rounded-full`} /> <div className={`h-4 w-4 ${getIconFromHash(parsedEvent.pubkey)} rounded-full`} />
)} )}
</div> </div>
))} ))}
</div>} </div>}
<div className={`flex justify-between items-center ${type !== "OP" ? 'hover:cursor-pointer' : ''}`} onClick={handleClick}> <div className={`flex justify-between items-center ${type !== "OP" ? 'hover:cursor-pointer' : ''}`} onClick={handleClick}>
{metadataParsed ? {metadataParsed ?
<img <img
key = {key} key={key}
className={`h-5 w-5 rounded-full`} className={`h-5 w-5 rounded-full`}
src={metadataParsed?.picture ?? icon} src={metadataParsed?.picture ?? icon}
alt="" alt=""
loading="lazy" loading="lazy"
decoding="async"/> decoding="async" />
: :
<div className={`h-4 w-4 ${icon} rounded-full`} /> <div className={`h-4 w-4 ${icon} rounded-full`} />
} }
<div className="flex items-center ml-auto gap-2.5"> <div className="flex items-center ml-auto gap-2.5">
<div className="inline-flex text-xs text-neutral-600 gap-0.5"> <div className={`inline-flex text-xs ${verifyPow(parsedEvent) === 0 ? 'text-neutral-600' : 'text-sky-800'} gap-0.5`}>
<CpuChipIcon className="h-4 w-4" /> {verifyPow(event)} <CpuChipIcon className="h-4 w-4" /> {verifyPow(parsedEvent)}
</div> </div>
<span className="text-neutral-700">·</span> {repostedEvent &&
<div className="text-xs font-semibold text-neutral-600"> <div className={`inline-flex text-xs ${verifyPow(repostedEvent) === 0 ? 'text-neutral-600' : 'text-sky-800'}`}>
{timeAgo(event.created_at)} + <CpuChipIcon className="h-4 w-4" /> {verifyPow(repostedEvent)}
</div>
<span className="text-neutral-700">·</span>
<div className="inline-flex items-center gap-1">
<FolderIcon className="h-4 w-4 text-neutral-600" />
<span className="text-xs text-neutral-600">{replies.length}</span>
</div> </div>
}
<span className="text-neutral-700">·</span>
<div className="min-w-20 inline-flex items-center text-neutral-600">
<FolderIcon className="h-4 w-4" />
<span className="text-xs pl-1">{replies.length}</span>
(
<CpuChipIcon className={`h-4 w-4 ${sumReplyPow === 0 ? 'text-neutral-600' : 'text-sky-800'}`} />
<span className={`text-xs ${sumReplyPow === 0 ? 'text-neutral-600' : 'text-sky-800'}`}>{sumReplyPow.toFixed(0)}</span>)
</div>
<span className="text-neutral-700">·</span>
<div className="min-w-6 text-xs font-semibold text-neutral-600">
{timeAgo(event.created_at)}
</div> </div>
</div> </div>
</div> </div>
</div>
</CardContainer> </CardContainer>
); );
}; };

View File

@ -1,119 +0,0 @@
// import CardContainer from "./CardContainer";
import { CpuChipIcon } from "@heroicons/react/24/outline";
// import { parseContent } from "../../utils/content";
import { Event, nip19 } from "nostr-tools";
import { getMetadata, Metadata } from "../../utils/getMetadata";
// import { renderMedia } from "../../utils/FileUpload";
import { getIconFromHash, timeAgo } from "../../utils/cardUtils";
import { verifyPow } from "../../utils/mine";
import { useNavigate } from 'react-router-dom';
import { subNoteOnce } from "../../utils/subscriptions";
import { useEffect, useState } from "react";
import ContentPreview from "./CardModals/TextModal";
interface RepostProps {
key?: string | number;
event: Event;
}
const RepostCard = ({
key,
event
}: RepostProps) => {
const repostedEvent = JSON.parse(event.content);
// const { files } = parseContent(repostedEvent);
const icon = getIconFromHash(event.pubkey);
const navigate = useNavigate();
const [cachedMetadataEvents, setCachedMetadataEvents] = useState<Event[]>(
JSON.parse(localStorage.getItem("cachedMetadataEvents") || "[]")
);
const [metadata, setMetadata] = useState<Metadata>()
// Define your callback function for subGlobalFeed
const onEvent = (event: Event, relay: string) => {
const existingEvent = cachedMetadataEvents.find((e) => e.pubkey === event.pubkey)
if (existingEvent) {
setMetadata(getMetadata(existingEvent))
}
else if (!existingEvent && event.kind === 0 && event.pubkey === repostedEvent.pubkey && metadata == null) {
setMetadata(getMetadata(event))
setCachedMetadataEvents((prevMetadataEvents) => {
// Check if the event already exists in the cached metadata events
const existingEvent = prevMetadataEvents.find((e) => e.id === event.id || e.pubkey === event.pubkey)
if (!existingEvent) {
// If the event doesn't exist, add it to the cached metadata events
return [...prevMetadataEvents, event];
} else if (existingEvent && existingEvent.created_at < event.created_at) {
// Remove any existing metadata event with the same pubkey and id
const updatedMetadataEvents = prevMetadataEvents.filter(
(e) => e.id !== existingEvent.id
);
// Add the new metadata event
return [...updatedMetadataEvents, event];
}
// If the event already exists, return the previous cached metadata events
return prevMetadataEvents;
});
}
};
useEffect(() => {
subNoteOnce(repostedEvent.id, onEvent);
}, [repostedEvent.id]);
// Save the cached metadataEvents to localStorage
useEffect(() => {
localStorage.setItem("cachedMetadataEvents", JSON.stringify(cachedMetadataEvents));
}, [cachedMetadataEvents]);
const handleClick = () => {
navigate(`/thread/${nip19.noteEncode(repostedEvent.id)}`);
};
return (
<div>
<div className="ml-1 flex text-sm text-neutral-600 gap-2.5">
Repost
@
<span className="inline-flex"><CpuChipIcon className="h-5 w-5" /> {verifyPow(event)}</span>
</div>
<div className="rounded-lg border border-neutral-700">
<div className="card break-inside-avoid h-min">
<div className="card-body">
<div className={`flex flex-col gap-2`}>
<div className={`flex flex-col break-words hover:cursor-pointer`} onClick={handleClick}>
<ContentPreview key={repostedEvent.id} eventdata={repostedEvent} />
</div>
<div className={`flex justify-between items-center hover:cursor-pointer`} onClick={handleClick}>
{metadata ?
<img
key = {key}
className={`h-5 w-5 rounded-full`}
src={metadata?.picture ?? icon}
alt=""
loading="lazy"
decoding="async"/>
:
<div className={`h-4 w-4 ${icon} rounded-full`} />
}
<div className="flex items-center ml-auto gap-2.5">
<div className="inline-flex text-xs text-neutral-600 gap-0.5">
<CpuChipIcon className="h-4 w-4" /> {verifyPow(repostedEvent)}
</div>
<span className="text-neutral-700">·</span>
<div className="text-xs font-semibold text-neutral-600">
{timeAgo(repostedEvent.created_at)}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default RepostCard;

View File

@ -2,7 +2,6 @@ import PostCard from "../modals/PostCard";
import { verifyPow } from "../../utils/mine"; import { verifyPow } from "../../utils/mine";
import { Event } from "nostr-tools"; import { Event } from "nostr-tools";
import NewNoteCard from "../forms/PostFormCard"; import NewNoteCard from "../forms/PostFormCard";
import RepostCard from "../modals/RepostCard";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useFetchEvents } from "../../hooks/useFetchEvents"; import { useFetchEvents } from "../../hooks/useFetchEvents";
@ -36,19 +35,15 @@ const HashtagPage = () => {
<div className="w-full px-4 sm:px-0 sm:max-w-xl mx-auto my-2"> <div className="w-full px-4 sm:px-0 sm:max-w-xl mx-auto my-2">
<NewNoteCard hashtag={id as string} /> <NewNoteCard hashtag={id as string} />
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4"> <div className="grid grid-cols-1 max-w-xl mx-auto gap-1 px-4">
{sortedEvents.map((event) => ( {sortedEvents.map((event) =>
event.kind === 1 ?
<PostCard <PostCard
event={event} event={event}
metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null} metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null}
replies={sortedEvents.filter((e) => e.tags.some((tag) => tag[0] === "e" && tag[1] === event.id))} replies={sortedEvents.filter((e) => e.tags.some((tag) => tag[0] === "e" && tag[1] === event.id))}
/> />
:
<RepostCard )}
event={event}
/>
))}
</div> </div>
</main> </main>
); );

View File

@ -1,7 +1,6 @@
import { verifyPow } from "../../utils/mine"; import { verifyPow } from "../../utils/mine";
import { Event } from "nostr-tools"; import { Event } from "nostr-tools";
import NewNoteCard from "../forms/PostFormCard"; import NewNoteCard from "../forms/PostFormCard";
import RepostCard from "../modals/RepostCard";
import { DEFAULT_DIFFICULTY } from "../../config"; import { DEFAULT_DIFFICULTY } from "../../config";
import PostCard from "../modals/PostCard"; import PostCard from "../modals/PostCard";
import { useFetchEvents } from "../../hooks/useFetchEvents"; import { useFetchEvents } from "../../hooks/useFetchEvents";
@ -14,10 +13,29 @@ const Home = () => {
.filter((event) => .filter((event) =>
verifyPow(event) >= Number(filterDifficulty) && verifyPow(event) >= Number(filterDifficulty) &&
event.kind !== 0 && event.kind !== 0 &&
(event.kind !== 1 || !event.tags.some((tag) => tag[0] === "e" || tag[0] === "a")) (event.kind !== 1 || !event.tags.some((tag) => tag[0] === "e" || tag[0] === "p"))
) )
const sortedEvents = postEvents.sort((a, b) => b.created_at - a.created_at); const postEventsWithReplies = postEvents.map((event) => {
const totalWork = Math.pow(2, verifyPow(event))
+ noteEvents.filter((e) => e.tags.some((tag) => tag[0] === "e" && tag[1] === event.id))
.reduce((acc, reply) => acc + Math.pow(2, verifyPow(reply)), 0);
return {
postEvent: event,
replies: noteEvents.filter((e) => e.tags.some((tag) => tag[0] === "e" && tag[1] === event.id)),
totalWork: totalWork, // Add total work here
};
});
const sortedEvents = postEventsWithReplies
.sort((a, b) => {
// Sort by total work in descending order
const workDiff = b.totalWork - a.totalWork;
if (workDiff !== 0) return workDiff;
// If total work is the same, sort by created_at in descending order
return b.postEvent.created_at - a.postEvent.created_at;
});
// Render the component // Render the component
return ( return (
@ -25,17 +43,13 @@ const Home = () => {
<div className="w-full px-4 sm:px-0 sm:max-w-xl mx-auto my-2"> <div className="w-full px-4 sm:px-0 sm:max-w-xl mx-auto my-2">
<NewNoteCard /> <NewNoteCard />
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4"> <div className="grid grid-cols-1 max-w-xl mx-auto gap-1 px-4">
{sortedEvents.map((event) => ( {sortedEvents.map((event) => (
event.kind === 1 ?
<PostCard <PostCard
event={event} key={event.postEvent.id}
metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null} event={event.postEvent}
replies={noteEvents.filter((e) => e.tags.some((tag) => tag[0] === "e" && tag[1] === event.id))} metadata={metadataEvents.find((e) => e.pubkey === event.postEvent.pubkey && e.kind === 0) || null}
/> replies={event.replies}
:
<RepostCard
event={event}
/> />
))} ))}
</div> </div>

View File

@ -1,7 +1,6 @@
import { useState, useCallback } from "react"; import { useState, useCallback } from "react";
import PostCard from "../modals/PostCard"; import PostCard from "../modals/PostCard";
import { Event } from "nostr-tools"; import { Event } from "nostr-tools";
import RepostCard from "../modals/RepostCard";
import { useFetchEvents } from "../../hooks/useFetchEvents"; import { useFetchEvents } from "../../hooks/useFetchEvents";
const Notifications = () => { const Notifications = () => {
@ -65,31 +64,21 @@ const Notifications = () => {
<div className={`grid grid-cols-1 gap-4 px-4 flex-grow ${notifsView ? 'hidden sm:block' : ''}`}> <div className={`grid grid-cols-1 gap-4 px-4 flex-grow ${notifsView ? 'hidden sm:block' : ''}`}>
<span>Your Recent Posts</span> <span>Your Recent Posts</span>
{sortedEvents.map((event) => ( {sortedEvents.map((event) => (
event.kind === 1 ?
<PostCard <PostCard
event={event} event={event}
metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null} metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null}
replies={countReplies(event)} replies={countReplies(event)}
/> />
:
<RepostCard
event={event}
/>
))} ))}
</div> </div>
<div className={`grid grid-cols-1 gap-4 px-4 flex-grow ${notifsView ? '' : 'hidden sm:block'}`}> <div className={`grid grid-cols-1 gap-4 px-4 flex-grow ${notifsView ? '' : 'hidden sm:block'}`}>
<span>Mentions</span> <span>Mentions</span>
{sortedMentions.map((event) => ( {sortedMentions.map((event) => (
event.kind === 1 ?
<PostCard <PostCard
event={event} event={event}
metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null} metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null}
replies={countReplies(event)} replies={countReplies(event)}
/> />
:
<RepostCard
event={event}
/>
))} ))}
</div> </div>
</div> </div>

View File

@ -67,7 +67,7 @@ const Thread = () => {
.sort((a, b) => a.created_at - b.created_at).map((event, index) => ( .sort((a, b) => a.created_at - b.created_at).map((event, index) => (
<PostCard event={event} metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null} replies={countReplies(event)} /> <PostCard event={event} metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null} replies={countReplies(event)} />
))} ))}
<PostCard event={OPEvent} metadata={metadataEvents.find((e) => e.pubkey === OPEvent.pubkey && e.kind === 0) || null} replies={countReplies(OPEvent)} type={'OP'}/> <PostCard event={OPEvent} metadata={metadataEvents.find((e) => e.pubkey === OPEvent.pubkey && e.kind === 0) || null} replies={replyEvents} type={'OP'}/>
</div> </div>
<ThreadPostModal OPEvent={OPEvent} /> <ThreadPostModal OPEvent={OPEvent} />
<div className="col-span-full h-0.5 bg-neutral-900"/> {/* This is the white line separator */} <div className="col-span-full h-0.5 bg-neutral-900"/> {/* This is the white line separator */}

View File

@ -1,2 +0,0 @@
go.sum
main

View File

@ -1,23 +0,0 @@
# Start from the latest golang base image
FROM golang:latest
# Set the Current Working Directory inside the container
WORKDIR /app
# Copy go mod and sum files
COPY go.mod go.sum ./
# Download all dependencies. Dependencies will be cached if the go.mod and go.sum files are not changed
RUN go mod download
# Copy the source from the current directory to the Working Directory inside the container
COPY . .
# Build the Go app
RUN go build -o main .
# Expose port 8080 to the outside world
EXPOSE 8080
# Command to run the executable
CMD ["./main"]

View File

@ -1,27 +0,0 @@
module pow_server
go 1.21.4
require (
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.2.0 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/nbd-wtf/go-nostr v0.25.7 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect
golang.org/x/sys v0.8.0 // indirect
)

View File

@ -1,54 +0,0 @@
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I=
github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/nbd-wtf/go-nostr v0.25.7 h1:DcGOSgKVr/L6w62tRtKeV2t46sRyFcq9pWcyIFkh0eM=
github.com/nbd-wtf/go-nostr v0.25.7/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU=
github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -1,225 +0,0 @@
package main
import (
"encoding/hex"
"encoding/json"
"errors"
"log"
"math/bits"
"net/http"
"runtime"
"strconv"
"time"
"github.com/nbd-wtf/go-nostr"
)
var (
ErrDifficultyTooLow = errors.New("nip13: insufficient difficulty")
ErrGenerateTimeout = errors.New("nip13: generating proof of work took too long")
)
// Difficulty counts the number of leading zero bits in an event ID.
// It returns a negative number if the event ID is malformed.
func Difficulty(eventID string) int {
if len(eventID) != 64 {
return -1
}
var zeros int
for i := 0; i < 64; i += 2 {
if eventID[i:i+2] == "00" {
zeros += 8
continue
}
var b [1]byte
if _, err := hex.Decode(b[:], []byte{eventID[i], eventID[i+1]}); err != nil {
return -1
}
zeros += bits.LeadingZeros8(b[0])
break
}
return zeros
}
// Generate performs proof of work on the specified event until either the target
// difficulty is reached or the function runs for longer than the timeout.
// The latter case results in ErrGenerateTimeout.
//
// Upon success, the returned event always contains a "nonce" tag with the target difficulty
// commitment, and an updated event.CreatedAt.
func Generate(event *nostr.Event, targetDifficulty int, nonceStart int, nonceStep int) (*nostr.Event, error) {
nonce := nonceStart
tag := nostr.Tag{"nonce", strconv.Itoa(nonceStep), strconv.Itoa(targetDifficulty)}
event.Tags = append(event.Tags, tag)
for {
nonce += nonceStep
tag[1] = strconv.Itoa(nonce)
event.CreatedAt = nostr.Now()
if Difficulty(event.GetID()) >= targetDifficulty {
return event, nil
}
}
}
func generatePOW(event *nostr.Event, difficulty int, numCores int) (*nostr.Event, error) {
resultChan := make(chan *nostr.Event)
errorChan := make(chan error)
for i := 0; i < numCores; i++ {
go func(nonceStart int, nonceStep int) {
generatedEvent, err := Generate(event, difficulty, nonceStart, nonceStep)
if err != nil {
errorChan <- err
return
}
resultChan <- generatedEvent
}(i, numCores)
}
select {
case result := <-resultChan:
return result, nil
case err := <-errorChan:
return nil, err
}
}
// PowRequest struct for the POST request
type PowRequest struct {
ReqEvent *nostr.Event `json:"req_event"`
Difficulty string `json:"difficulty"`
}
// handlePOW is the handler function for the "/powgen" endpoint
func handlePOW(w http.ResponseWriter, r *http.Request, numCores int) {
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
}
// Generate proof of work for the event
generatedEvent, err := generatePOW(powReq.ReqEvent, difficulty, numCores)
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, numCores int) {
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()
event := &nostr.Event{
Kind: nostr.KindTextNote,
Content: "It's just me mining my own business",
PubKey: "a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243",
}
pow, err := generatePOW(event, difficulty, numCores)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Calculate the duration in milliseconds
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 {
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{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 handlePOWWithCores(numCores int) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
handlePOW(w, r, numCores)
}
}
func handleTestWithCores(numCores int) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
handleTest(w, r, numCores)
}
}
func main() {
numCores := runtime.NumCPU()
http.Handle("/powgen", corsMiddleware(handlePOWWithCores(numCores)))
http.Handle("/test", corsMiddleware(handleTestWithCores(numCores)))
log.Fatal(http.ListenAndServe("0.0.0.0:8080", nil))
}