mirror of
https://github.com/smolgrrr/TAO.git
synced 2024-09-19 17:01:25 +00:00
sort by work feed
This commit is contained in:
parent
8231f7bf93
commit
20df7f8507
11
README.md
11
README.md
@ -1,4 +1,4 @@
|
||||
# The Wire: unstoppable free speech
|
||||
# The Wired: unstoppable free speech
|
||||
An app to facilitate unstoppable free speech on the internet.
|
||||
|
||||
## 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/>
|
||||
PoW: Spam and noise
|
||||
|
||||
### Wanting to add
|
||||
|
||||
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
|
||||
## Back up
|
||||
https://git.getwired.app/doot/TAO
|
@ -2,7 +2,7 @@ import { PropsWithChildren } from "react";
|
||||
|
||||
export default function CardContainer({ children }: PropsWithChildren) {
|
||||
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>
|
||||
);
|
||||
|
@ -31,10 +31,30 @@ const PostCard = ({
|
||||
const icon = getIconFromHash(event.pubkey);
|
||||
const metadataParsed = metadata ? getMetadata(metadata) : null;
|
||||
const [relatedEvents, setRelatedEvents] = useState<Event[]>([]);
|
||||
const [sumReplyPow, setReplySumPow] = useState(0);
|
||||
const [repostedEvent, setRepostedEvent] = useState<Event>();
|
||||
const [parsedEvent, setParsedEvent] = useState<Event>(event);
|
||||
|
||||
useEffect(() => {
|
||||
const allRelatedEvents = [event, ...(replies || [])];
|
||||
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]);
|
||||
|
||||
const handleClick = () => {
|
||||
@ -44,52 +64,61 @@ const PostCard = ({
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<CardContainer>
|
||||
<div className={`flex flex-col gap-2`}>
|
||||
<div className={`flex flex-col break-words ${type !== "OP" ? 'hover:cursor-pointer' : ''}`} onClick={handleClick}>
|
||||
<ContentPreview key={event.id} eventdata={event} />
|
||||
</div>
|
||||
{repliedTo && <div className="flex items-center mt-1" >
|
||||
<span className="text-xs text-gray-500">Reply to: </span>
|
||||
{uniqBy(repliedTo, 'pubkey').map((event, index) => (
|
||||
<div key={index}>
|
||||
{event.kind === 0 ? (
|
||||
<img className={`h-5 w-5 rounded-full`} src={getMetadata(event)?.picture} />
|
||||
) : (
|
||||
<div className={`h-4 w-4 ${getIconFromHash(event.pubkey)} rounded-full`} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>}
|
||||
<div className={`flex justify-between items-center ${type !== "OP" ? 'hover:cursor-pointer' : ''}`} onClick={handleClick}>
|
||||
{metadataParsed ?
|
||||
<img
|
||||
key = {key}
|
||||
className={`h-5 w-5 rounded-full`}
|
||||
src={metadataParsed?.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(event)}
|
||||
</div>
|
||||
<span className="text-neutral-700">·</span>
|
||||
<div className="text-xs font-semibold text-neutral-600">
|
||||
{timeAgo(event.created_at)}
|
||||
</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 className={`flex flex-col gap-2`} key={key}>
|
||||
<div className={`flex flex-col break-words ${type !== "OP" ? 'hover:cursor-pointer' : ''}`} onClick={handleClick}>
|
||||
<ContentPreview key={parsedEvent.id} eventdata={parsedEvent} />
|
||||
</div>
|
||||
{repliedTo && <div className="flex items-center mt-1" >
|
||||
<span className="text-xs text-gray-500">Reply to: </span>
|
||||
{uniqBy(repliedTo, 'pubkey').map((parsedEvent, index) => (
|
||||
<div key={index}>
|
||||
{event.kind === 0 ? (
|
||||
<img className={`h-5 w-5 rounded-full`} src={getMetadata(parsedEvent)?.picture} />
|
||||
) : (
|
||||
<div className={`h-4 w-4 ${getIconFromHash(parsedEvent.pubkey)} rounded-full`} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>}
|
||||
<div className={`flex justify-between items-center ${type !== "OP" ? 'hover:cursor-pointer' : ''}`} onClick={handleClick}>
|
||||
{metadataParsed ?
|
||||
<img
|
||||
key={key}
|
||||
className={`h-5 w-5 rounded-full`}
|
||||
src={metadataParsed?.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 ${verifyPow(parsedEvent) === 0 ? 'text-neutral-600' : 'text-sky-800'} gap-0.5`}>
|
||||
<CpuChipIcon className="h-4 w-4" /> {verifyPow(parsedEvent)}
|
||||
</div>
|
||||
{repostedEvent &&
|
||||
<div className={`inline-flex text-xs ${verifyPow(repostedEvent) === 0 ? 'text-neutral-600' : 'text-sky-800'}`}>
|
||||
+ <CpuChipIcon className="h-4 w-4" /> {verifyPow(repostedEvent)}
|
||||
</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>
|
||||
</CardContainer>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
@ -2,7 +2,6 @@ import PostCard from "../modals/PostCard";
|
||||
import { verifyPow } from "../../utils/mine";
|
||||
import { Event } from "nostr-tools";
|
||||
import NewNoteCard from "../forms/PostFormCard";
|
||||
import RepostCard from "../modals/RepostCard";
|
||||
import { useParams } from "react-router-dom";
|
||||
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">
|
||||
<NewNoteCard hashtag={id as string} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4">
|
||||
{sortedEvents.map((event) => (
|
||||
event.kind === 1 ?
|
||||
<div className="grid grid-cols-1 max-w-xl mx-auto gap-1 px-4">
|
||||
{sortedEvents.map((event) =>
|
||||
<PostCard
|
||||
event={event}
|
||||
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))}
|
||||
/>
|
||||
:
|
||||
<RepostCard
|
||||
event={event}
|
||||
/>
|
||||
))}
|
||||
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { verifyPow } from "../../utils/mine";
|
||||
import { Event } from "nostr-tools";
|
||||
import NewNoteCard from "../forms/PostFormCard";
|
||||
import RepostCard from "../modals/RepostCard";
|
||||
import { DEFAULT_DIFFICULTY } from "../../config";
|
||||
import PostCard from "../modals/PostCard";
|
||||
import { useFetchEvents } from "../../hooks/useFetchEvents";
|
||||
@ -14,10 +13,29 @@ const Home = () => {
|
||||
.filter((event) =>
|
||||
verifyPow(event) >= Number(filterDifficulty) &&
|
||||
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
|
||||
return (
|
||||
@ -25,17 +43,13 @@ const Home = () => {
|
||||
<div className="w-full px-4 sm:px-0 sm:max-w-xl mx-auto my-2">
|
||||
<NewNoteCard />
|
||||
</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) => (
|
||||
event.kind === 1 ?
|
||||
<PostCard
|
||||
event={event}
|
||||
metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null}
|
||||
replies={noteEvents.filter((e) => e.tags.some((tag) => tag[0] === "e" && tag[1] === event.id))}
|
||||
/>
|
||||
:
|
||||
<RepostCard
|
||||
event={event}
|
||||
key={event.postEvent.id}
|
||||
event={event.postEvent}
|
||||
metadata={metadataEvents.find((e) => e.pubkey === event.postEvent.pubkey && e.kind === 0) || null}
|
||||
replies={event.replies}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import PostCard from "../modals/PostCard";
|
||||
import { Event } from "nostr-tools";
|
||||
import RepostCard from "../modals/RepostCard";
|
||||
import { useFetchEvents } from "../../hooks/useFetchEvents";
|
||||
|
||||
const Notifications = () => {
|
||||
@ -65,31 +64,21 @@ const Notifications = () => {
|
||||
<div className={`grid grid-cols-1 gap-4 px-4 flex-grow ${notifsView ? 'hidden sm:block' : ''}`}>
|
||||
<span>Your Recent Posts</span>
|
||||
{sortedEvents.map((event) => (
|
||||
event.kind === 1 ?
|
||||
<PostCard
|
||||
event={event}
|
||||
metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null}
|
||||
replies={countReplies(event)}
|
||||
/>
|
||||
:
|
||||
<RepostCard
|
||||
event={event}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className={`grid grid-cols-1 gap-4 px-4 flex-grow ${notifsView ? '' : 'hidden sm:block'}`}>
|
||||
<span>Mentions</span>
|
||||
{sortedMentions.map((event) => (
|
||||
event.kind === 1 ?
|
||||
<PostCard
|
||||
event={event}
|
||||
metadata={metadataEvents.find((e) => e.pubkey === event.pubkey && e.kind === 0) || null}
|
||||
replies={countReplies(event)}
|
||||
/>
|
||||
:
|
||||
<RepostCard
|
||||
event={event}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -67,7 +67,7 @@ const Thread = () => {
|
||||
.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={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>
|
||||
<ThreadPostModal OPEvent={OPEvent} />
|
||||
<div className="col-span-full h-0.5 bg-neutral-900"/> {/* This is the white line separator */}
|
||||
|
2
pow_server/.gitignore
vendored
2
pow_server/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
go.sum
|
||||
main
|
@ -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"]
|
@ -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
|
||||
)
|
@ -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=
|
@ -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))
|
||||
}
|
Loading…
Reference in New Issue
Block a user