From f18c31ad35390a3859ba2fd987567ae76f6d8bbb Mon Sep 17 00:00:00 2001 From: gsovereignty Date: Tue, 16 Jul 2024 12:30:33 +0800 Subject: [PATCH] problem: voting and rocket logic is spread out and messy --- src/components/AddProductToRocket.svelte | 51 +----- src/components/MeritRequests.svelte | 1 - src/components/MeritSummaryCard.svelte | 45 ++++- src/lib/event_helpers/merits.ts | 171 ++++++++++++++---- src/lib/event_helpers/rockets.ts | 149 +++++++++++++-- .../rockets/merits/[merit]/+page.svelte | 2 +- 6 files changed, 309 insertions(+), 110 deletions(-) diff --git a/src/components/AddProductToRocket.svelte b/src/components/AddProductToRocket.svelte index 2662b9c..49b9a4b 100644 --- a/src/components/AddProductToRocket.svelte +++ b/src/components/AddProductToRocket.svelte @@ -6,54 +6,21 @@ import { Input } from '$lib/components/ui/input/index.js'; import { Label } from '$lib/components/ui/label/index.js'; import * as Alert from '@/components/ui/alert'; - import { getRocketURL } from '@/helpers'; import { ndk } from '@/ndk'; import { currentUser } from '@/stores/session'; import { NDKEvent } from '@nostr-dev-kit/ndk'; import { Terminal } from 'lucide-svelte'; import Todo from './Todo.svelte'; + import { Rocket } from '@/event_helpers/rockets'; export let product: NDKEvent; export let rocket: NDKEvent; + let parsedRocket: Rocket = new Rocket(rocket); + let price: number = 0; let max: number = 0; - function updateIgnitionAndParentTag(rocket: NDKEvent) { - let existingIgnition = rocket.getMatchingTags('ignition'); - //let existingParent = rocket.getMatchingTags("parent") - removeIgnitionAndParentTag(rocket); - if (existingIgnition.length > 1) { - throw new Error('too many ignition tags!'); - } - if (existingIgnition.length == 0) { - rocket.tags.push(['ignition', rocket.id]); - } - if (existingIgnition.length == 1) { - if (existingIgnition[0][1].length == 64) { - rocket.tags.push(existingIgnition[0]); - } - if (existingIgnition[0][1] == 'this') { - rocket.tags.push(['ignition', rocket.id]); - } - } - rocket.tags.push(['parent', rocket.id]); - - } - - function removeIgnitionAndParentTag(rocket: NDKEvent) { - let existing = []; - for (let t of rocket.tags) { - existing.push(t); - } - rocket.tags = []; - for (let t of existing) { - if (t[0] !== 'ignition' && t[0] !== 'parent') { - rocket.tags.push(t); - } - } - } - function publish() { if (!$ndk.signer) { throw new Error('no ndk signer found'); @@ -67,16 +34,8 @@ console.log(rocket.author, author); throw new Error('you are not the creator of this rocket'); } - rocket.created_at = Math.floor(new Date().getTime() / 1000); - //todo validate d tag - rocket.tags.push([ - 'product', - `${product.id}:${price}:${rocket.created_at}:${max}`, - 'wss://relay.nostrocket.org', - JSON.stringify([]) - ]); - updateIgnitionAndParentTag(rocket) - rocket.publish().then((x) => { + let event = parsedRocket.UpsertProduct(product.id, price, max); + event.publish().then((x) => { console.log(x); goto(`${base}/products`); }); diff --git a/src/components/MeritRequests.svelte b/src/components/MeritRequests.svelte index 9148347..ffee078 100644 --- a/src/components/MeritRequests.svelte +++ b/src/components/MeritRequests.svelte @@ -58,7 +58,6 @@ {#each $merits as [id, merit], _ (id)} { - console.log(merit.Event.rawEvent()); goto(`${base}/rockets/merits/${merit.ID}`); }} class="cursor-pointer bg-accent" diff --git a/src/components/MeritSummaryCard.svelte b/src/components/MeritSummaryCard.svelte index c930555..fde2b48 100644 --- a/src/components/MeritSummaryCard.svelte +++ b/src/components/MeritSummaryCard.svelte @@ -1,15 +1,13 @@ @@ -116,14 +142,17 @@
Votes
- {#if $votes.size == 0}Waiting for existing {new Rocket(rocket).Name()} Merit holders to vote {/if} + {#if $votes.size == 0}Waiting for existing {new Rocket(rocket).Name()} Merit + holders to vote + {/if} {#each $votes as [id, vote], _ (id)} { console.log(vote.Event.rawEvent()); - goto(`${base}/rockets/merits/${vote.ID}`); }} class="cursor-pointer {vote.VoteDirection == 'ratify' ? 'bg-lime-600' diff --git a/src/lib/event_helpers/merits.ts b/src/lib/event_helpers/merits.ts index 2e2aeaf..d0424b0 100644 --- a/src/lib/event_helpers/merits.ts +++ b/src/lib/event_helpers/merits.ts @@ -31,7 +31,7 @@ export class MeritRequest { return _solution; } IncludedInRocketState(rocket: Rocket): boolean { - return true; + return false; } BasicValidation(): boolean { //todo: make a ValidateAgainstRocket and check that pubkey is in WoT @@ -85,36 +85,31 @@ export class MeritRequest { } export class Vote { - ID: string; - Request: string; - VoteDirection: VoteDirection | undefined; - Pubkey: string; - TimeStamp: number; + ID: string; + Request: string; + VoteDirection: VoteDirection | undefined; + Pubkey: string; + TimeStamp: number; Event: NDKEvent; BasicValidation(): boolean { - let valid = true; + let valid = true; if ( - !( - this.ID.length == 64 && - this.Request.length == 64 && - this.VoteDirection && - this.TimeStamp - ) + !(this.ID.length == 64 && this.Request.length == 64 && this.VoteDirection && this.TimeStamp) ) { valid = false; } return valid; } - ValidateAgainstRocket(rocket:Rocket):boolean { - let valid = true; - if (!(rocket.VotePowerForPubkey(this.Pubkey) > 0)) { - valid = false - } - return valid - } - ValidateAgainstMeritRequest(merit:MeritRequest):boolean { - return this.Request == merit.ID - } + ValidateAgainstRocket(rocket: Rocket): boolean { + let valid = true; + if (!(rocket.VotePowerForPubkey(this.Pubkey) > 0)) { + valid = false; + } + return valid; + } + ValidateAgainstMeritRequest(merit: MeritRequest): boolean { + return this.Request == merit.ID; + } RocketTag(): NDKTag | undefined { let tag: NDKTag | undefined = undefined; if (this.BasicValidation()) { @@ -146,22 +141,124 @@ export class Vote { } constructor(event: NDKEvent) { this.Event = event; - this.ID = event.id; - this.Pubkey = event.pubkey; - if (this.Event.created_at) { + this.ID = event.id; + this.Pubkey = event.pubkey; + if (this.Event.created_at) { this.TimeStamp = this.Event.created_at; } - for (let t of this.Event.getMatchingTags("vote")) { - if (t && t.length == 2 && (t[1] == "blackball" || t[1] == "ratify")) { - this.VoteDirection = t[1] - } - } - for (let t of this.Event.getMatchingTags("request")) { - if (t && t.length == 2 && t[1].length == 64) { - this.Request = t[1] - } - } + for (let t of this.Event.getMatchingTags('vote')) { + if (t && t.length == 2 && (t[1] == 'blackball' || t[1] == 'ratify')) { + this.VoteDirection = t[1]; + } + } + for (let t of this.Event.getMatchingTags('request')) { + if (t && t.length == 2 && t[1].length == 64) { + this.Request = t[1]; + } + } + if (!this.BasicValidation()) { + throw new Error('failed to create vote'); + } } } -export type VoteDirection = "blackball" | "ratify" \ No newline at end of file +export type VoteDirection = 'blackball' | 'ratify'; + +export class Votes { + Votes: Vote[]; + Request:string; + Results(): VoteResults { + let ratifiers = new Map(); + let blackballers = new Map(); + for (let v of this.Votes) { + if ((v.VoteDirection == 'blackball')) { + blackballers.set(v.ID, v); + } + if ((v.VoteDirection == 'ratify')) { + ratifiers.set(v.ID, v); + } + } + let results: VoteResults = new VoteResults( + new VoteTally(blackballers), + new VoteTally(ratifiers) + ); + return results; + } + constructor(votes: Vote[], request?:string) { + this.Votes = [] + for (let v of votes) { + if (!request) { + request = v.Request; + } + if (!this.Request) { + this.Request = request + } + if (v.Request == this.Request) { + this.Votes.push(v) + } + } + } +} + +export class VoteResults { + blackballers: VoteTally; + ratifiers: VoteTally; + Result(rocket: Rocket): VoteDirection | undefined { + let result: VoteDirection | undefined = undefined; + if (this.blackballers.TotalPercent(rocket) < 0.1 && this.ratifiers.TotalPercent(rocket) > 0.5) { + result = 'ratify'; + } + if (this.blackballers.TotalPercent(rocket) >= 0.1) { + result = 'blackball'; + } + return result; + } + constructor(blackballers: VoteTally, ratifiers: VoteTally) { + this.blackballers = blackballers; + this.ratifiers = ratifiers; + } +} + +export class VoteTally { + Votes: Map; + Direction: VoteDirection | undefined; + MeritRequest: string | undefined; + BasicValidation(): boolean { + let valid = true; + for (let [_, v] of this.Votes) { + if (!this.Direction) { + this.Direction = v.VoteDirection; + } + if (!this.MeritRequest) { + this.MeritRequest = v.Request; + } + if (v.VoteDirection != this.Direction || v.Request != this.MeritRequest) { + valid = false; + } + } + return valid; + } + Total(rocket: Rocket): number { + let total = 0; + if (this.BasicValidation()) { + for (let [_, v] of this.Votes) { + total += rocket.VotePowerForPubkey(v.Pubkey); + } + } + + return total; + } + TotalPercent(rocket: Rocket): number { + let result = undefined; + let total = this.Total(rocket); + return total / rocket.TotalVotePower(); + } + constructor(votes: Map) { + this.Votes = votes; + this.Direction = undefined; + this.MeritRequest = undefined; + if (!this.BasicValidation()) { + throw new Error('invalid votes detected'); + } + } +} \ No newline at end of file diff --git a/src/lib/event_helpers/rockets.ts b/src/lib/event_helpers/rockets.ts index 6604f53..833fad2 100644 --- a/src/lib/event_helpers/rockets.ts +++ b/src/lib/event_helpers/rockets.ts @@ -2,29 +2,144 @@ import { NDKEvent, type NDKTag } from '@nostr-dev-kit/ndk'; export class Rocket { Event: NDKEvent; - Name():string { - return this.Event.dTag! + Name(): string { + return this.Event.dTag!; } - VotePowerForPubkey(pubkey:string):number { - let votepower = 0 + VotePowerForPubkey(pubkey: string): number { + let votepower = 0; if (this.Event.pubkey == pubkey) { //todo: calculate votepower for pubkey based on approved merit requests - votepower++ + votepower++; } - return votepower + return votepower; } - constructor(event:NDKEvent) { + TotalVotePower(): number { + //todo: calculate votepower for pubkey based on approved merit requests + return 1; + } + ApprovedMeritRequests(): Map { + let amr = new Map(); + for (let m of this.Event.getMatchingTags('merit')) { + if (m && m.length == 2) { + let _amr = new RocketAMR(m[1]); + amr.set(_amr.ID, _amr); + } + } + return amr; + } + AppendAMR(amrProof: NDKEvent): NDKEvent | undefined { + //todo + let request: NDKEvent | undefined = undefined; + let votes: NDKEvent[] = []; + let _request = amrProof.getMatchingTags('request'); + if (_request.length == 1) { + try { + request = JSON.parse(_request[0][1]); + } catch {} + } + for (let v of amrProof.getMatchingTags('vote')) { + try { + votes.push(JSON.parse(v[1])); + } catch {} + } + return + //add the AMR to the rocket event, and also add a proof + } + UpsertProduct(id: string, price: number, maxSales?: number): NDKEvent { + let event = new NDKEvent(this.Event.ndk, this.Event.rawEvent()); + event.created_at = Math.floor(new Date().getTime() / 1000); + let existingProducts = this.CurrentProducts(); + let purchases = JSON.stringify([]); + let existingProduct = existingProducts.get(id); + if (existingProduct) { + purchases = existingProduct.PurchasesJSON(); + } + event.tags.push([ + 'product', + `${id}:${price}:${event.created_at}:${maxSales}`, + 'wss://relay.nostrocket.org', + purchases + ]); + updateIgnitionAndParentTag(event); + return event; + } + CurrentProducts(): Map { + return getMapOfProductsFromRocket(this.Event); + } + + constructor(event: NDKEvent) { this.Event = event; } } +function updateIgnitionAndParentTag(event: NDKEvent) { + let existingIgnition = event.getMatchingTags('ignition'); + //let existingParent = rocket.getMatchingTags("parent") + let existing = []; + for (let t of event.tags) { + existing.push(t); + } + event.tags = []; + for (let t of existing) { + if (t[0] !== 'ignition' && t[0] !== 'parent') { + event.tags.push(t); + } + } + if (existingIgnition.length > 1) { + throw new Error('too many ignition tags!'); + } + if (existingIgnition.length == 0) { + event.tags.push(['ignition', event.id]); + } + if (existingIgnition.length == 1) { + if (existingIgnition[0][1].length == 64) { + event.tags.push(existingIgnition[0]); + } + if (existingIgnition[0][1] == 'this') { + event.tags.push(['ignition', event.id]); + } + } + event.tags.push(['parent', event.id]); +} + +export class RocketAMR { + ID: string; + Pubkey: string; + LeadTime: number; + LeadTimeUpdate: number; + Merits: number; + Valid(): boolean { + let valid = true; + if (!(this.ID.length == 64 && this.Pubkey.length == 64 && this.Merits)) { + valid = false; + } + return valid; + } + constructor(meritString: string) { + let split = meritString.split(':'); + if (split.length == 5) { + this.Pubkey = split[0]; + this.ID = split[1]; + this.LeadTime = parseInt(split[2], 10); + this.LeadTimeUpdate = parseInt(split[3], 10); + this.Merits = parseInt(split[4], 10); + } + } +} + export class RocketProduct { ID: string; Price: number; ValidAfter: number; //unix time MaxPurchases: number; Purchases: Map; - + PurchasesJSON(): string { + let purchases = []; + for (let [_, p] of this.Purchases) { + purchases.push(`${p.ZapID}:${p.BuyerPubkey}:${p.WitnessedAt}`); + } + return JSON.stringify(purchases); + } constructor(tag: NDKTag) { this.Purchases = new Map(); this.ID = tag[1].split(':')[0]; @@ -138,23 +253,23 @@ function getZapRequest(zapReceipt: NDKEvent): NDKEvent | undefined { } function getZapAmount(zapRequest?: NDKEvent): number { - return getNumberFromTag("amount", zapRequest) + return getNumberFromTag('amount', zapRequest); } -export function getNumberFromTag(tag:string, event?: NDKEvent): number { +export function getNumberFromTag(tag: string, event?: NDKEvent): number { let amountTag = event?.getMatchingTags(tag); if (amountTag && amountTag[0] && amountTag[0][1]) { try { let amount = parseInt(amountTag[0][1], 10); - return amount + return amount; } catch { - console.log("ERROR: could not find number in tag: ", tag, event) + console.log('ERROR: could not find number in tag: ', tag, event); } } - return 0 + return 0; } -export function isValidUrl(string:string):boolean { +export function isValidUrl(string: string): boolean { try { new URL(string); return true; @@ -163,6 +278,6 @@ export function isValidUrl(string:string):boolean { } } -export function RocketATagFilter(rocket:NDKEvent):string { - return `31108:${rocket.pubkey}:${rocket.dTag}` -} \ No newline at end of file +export function RocketATagFilter(rocket: NDKEvent): string { + return `31108:${rocket.pubkey}:${rocket.dTag}`; +} diff --git a/src/routes/rockets/merits/[merit]/+page.svelte b/src/routes/rockets/merits/[merit]/+page.svelte index 8b11d86..083834f 100644 --- a/src/routes/rockets/merits/[merit]/+page.svelte +++ b/src/routes/rockets/merits/[merit]/+page.svelte @@ -11,7 +11,7 @@ let meritRequestID = $page.params.merit; - let meritRequest:MeritRequest | undefined; + let meritRequest:MeritRequest | undefined = undefined; $: { if (meritRequestID.length == 64 && !meritRequest) {