From 6e94c7bb35265c3fedcb6b54c538312de924ef66 Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 29 Aug 2024 22:36:23 +0800 Subject: [PATCH] problem: can't create real rockets --- src/components/CreateNewRocket.svelte | 186 +++++++++++++++++++++----- src/components/NotifyMe.svelte | 3 +- src/components/PayRocketName.svelte | 129 ++++++++++++++++++ src/lib/consts.ts | 11 ++ src/lib/event_helpers/help_thread.ts | 6 +- src/lib/event_helpers/rockets.ts | 29 ++++ src/routes/buymerits/+page.svelte | 6 +- src/routes/help/+page.svelte | 7 +- 8 files changed, 325 insertions(+), 52 deletions(-) create mode 100644 src/components/PayRocketName.svelte create mode 100644 src/lib/consts.ts diff --git a/src/components/CreateNewRocket.svelte b/src/components/CreateNewRocket.svelte index 146b046..f121372 100644 --- a/src/components/CreateNewRocket.svelte +++ b/src/components/CreateNewRocket.svelte @@ -7,32 +7,82 @@ import { Label } from '$lib/components/ui/label/index.js'; import * as Alert from '@/components/ui/alert'; import Checkbox from '@/components/ui/checkbox/checkbox.svelte'; - import { getRocketURL } from '@/helpers'; + import { Rocket, ZapRocketNamePurchase } from '@/event_helpers/rockets'; + import { formatSats, getRocketURL } from '@/helpers'; import { ndk } from '@/ndk'; import { BitcoinTipTag } from '@/stores/bitcoin'; import { currentUser, devmode, mainnet } from '@/stores/session'; import { NDKEvent } from '@nostr-dev-kit/ndk'; import type NDKSvelte from '@nostr-dev-kit/ndk-svelte'; - import type { NDKEventStore } from '@nostr-dev-kit/ndk-svelte'; import { Info, Terminal } from 'lucide-svelte'; import { onDestroy } from 'svelte'; - import { writable } from 'svelte/store'; + import { derived } from 'svelte/store'; + import PayRocketName from './PayRocketName.svelte'; + import { + BUY_ROCKET_NAME_ZAPPED_PUBKEY, + BUY_ROCKET_NAME_ZAPPED_EVENT, + BUY_ROCKET_NAME_FEE + } from '@/consts'; - let rockets: NDKEventStore | undefined; - const rocketsStore = writable([]); - let name: string = ''; - $: nameInvalid = true; - $: nameError = ''; + let rocketEvents = $ndk.storeSubscribe([{ kinds: [31108 as number] }], { subId: 'rockets' }); + let zaps = $ndk.storeSubscribe( + [ + { + '#a': [`31108:${BUY_ROCKET_NAME_ZAPPED_PUBKEY}:${BUY_ROCKET_NAME_ZAPPED_EVENT}`], + kinds: [9735] + } + ], + { + subId: BUY_ROCKET_NAME_ZAPPED_EVENT + '_zaps' + } + ); - rockets = $ndk.storeSubscribe([{ kinds: [31108 as number] }], { subId: 'rockets' }); onDestroy(() => { - rockets?.unsubscribe(); + rocketEvents?.unsubscribe(); + zaps?.unsubscribe(); + }); + + const rockets = derived(rocketEvents, ($rocketEvents) => { + return $rocketEvents.map((e) => new Rocket(e)); + }); + + let nostrocket = derived(rockets, ($rockets) => { + let rocket: Rocket | undefined = undefined; + for (let r of $rockets) { + if ( + r.Name() == BUY_ROCKET_NAME_ZAPPED_EVENT && + r.Event.pubkey == BUY_ROCKET_NAME_ZAPPED_PUBKEY + ) { + rocket = r; + } + } + return rocket; + }); + + let validZaps = derived(zaps, ($zaps) => { + let zapMap = new Map(); + for (let z of $zaps) { + let zapRocketNamePurchase = new ZapRocketNamePurchase(z); + if (zapRocketNamePurchase.Valid()) { + zapMap.set(zapRocketNamePurchase.ZapReceipt.id, zapRocketNamePurchase); + } + } + return zapMap; + }); + + let purchasedRocketNames = derived(validZaps, ($validZaps) => { + let my = []; + let all = []; + for (let z of $validZaps.values()) { + if (z.RocketName) { + all.push(z.RocketName); + if ($currentUser?.pubkey === z.BuyerPubkey) { + my.push(z.RocketName); + } + } + } + return { my, all }; }); - $: if (rockets) { - rockets.subscribe((events) => { - rocketsStore.set(events); - }); - } let userHasProfile = false; $: { @@ -46,27 +96,67 @@ userHasProfile = userHasProfile; } + let name: string = ''; + + $: nameValid = true; + $: nameError = ''; + $: canPublish = true; + const rocketNameValidator = /^\w{4,20}$/; - const nameIsUnique = (name: string, rocketEvents: NDKEvent[]) => { - return !rocketEvents.some((event) => event.tags[0][1] === name); - }; - - $: if (name) { - if (!rocketNameValidator.test(name)) { - nameInvalid = true; - nameError = 'Rocket names MUST be 4-20 alphanumeric characters'; - } else if (!$rocketsStore) { - // nameInvalid = true; - // nameError = 'Loading Nostr'; - } else if (!nameIsUnique(name, $rocketsStore)) { - // nameInvalid = true; - // nameError = 'Rocket names MUST be unique'; + + $: isPurchasedByOthers = + $purchasedRocketNames.all.includes(name) && !$purchasedRocketNames.my.includes(name); + $: isPurchasedByMe = $purchasedRocketNames.my.includes(name); + $: isPublished = $rockets.some((r) => r.Name() === name); + + // name is purchased by others -> name invalid, cannot publish + // name is purchased by me, name is publish -> name invalid, cannot publish + // name not purchased -> name valid -> mainnet -> buy name -> publish + // name not purchased -> name valid -> mainnet -> not buy name -> cannot publish + // name not purchased -> name valid -> testnet -> publish + // name is purchased by me -> name valid -> publish + + $: { + if (!name) { + nameValid = false; + nameError = ''; + } else if (name === 'NOSTROCKET' || (isPublished && (isPurchasedByMe || isPurchasedByOthers))) { + nameValid = false; + nameError = 'Please use another name'; + } else if (!rocketNameValidator.test(name)) { + nameValid = false; + nameError = 'Rocket name MUST be 4-20 alphanumeric characters'; + } else if (isPurchasedByOthers) { + nameValid = false; + nameError = 'Rocket name is already in use by someone else; you cannot use it'; + } else if (isPurchasedByMe && isPublished) { + nameValid = false; } else { - nameInvalid = false; + nameValid = true; nameError = ''; } } + $: { + if (nameValid) { + if (isPurchasedByMe && !isPublished) { + canPublish = true; + } else if (!$mainnet) { + if (isPurchasedByMe && isPublished) { + canPublish = false; + } else if (isPurchasedByOthers) { + canPublish = false; + } else { + canPublish = true; + } + } else { + canPublish = false; + } + } else { + canPublish = false; + } + } + function publish(ndk: NDKSvelte, name: string) { if (!ndk.signer) { throw new Error('no ndk signer found'); @@ -76,7 +166,7 @@ if (!author) { throw new Error('no current user'); } - if (nameInvalid) { + if (!nameValid) { throw new Error('name is invalid'); } e.author = author; @@ -124,20 +214,42 @@ +
+ + +
{nameError}
+ {#if $mainnet && nameValid && !isPurchasedByMe} +
+ To create a mainnet rocket, you need to pay {formatSats(BUY_ROCKET_NAME_FEE)} for a rocket name. +
+ {/if} {#if $devmode} - Purchased My Rocket Name: {$purchasedRocketNames.my.join(', ')} +
Purchased All Rocket Name: {$purchasedRocketNames.all.join(', ')}
+
isPurchasedByOthers: {isPurchasedByOthers}
+
isPurchasedByMe: {isPurchasedByMe}
+
isPublished: {isPublished}
+ {/if} + {#if $mainnet && $nostrocket && !isPurchasedByMe} + + {/if} diff --git a/src/components/NotifyMe.svelte b/src/components/NotifyMe.svelte index 665f7cf..4c61f19 100644 --- a/src/components/NotifyMe.svelte +++ b/src/components/NotifyMe.svelte @@ -12,6 +12,7 @@ import { NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk'; import { ExclamationSolid, TelegramBrand, TriangleExclamationSolid } from 'svelte-awesome-icons'; import { RefreshCcw } from 'lucide-svelte'; + import { NOSTROCKET_PUBKEY } from '@/consts'; export let menu = false; @@ -50,7 +51,7 @@ async function publishEncryptedDirectMessage(content: string) { const RECEIVER = new NDKUser({ - pubkey: 'd91191e30e00444b942c0e82cad470b32af171764c2275bee0bd99377efd4075' + pubkey: NOSTROCKET_PUBKEY }); const originalSigner = $ndk.signer; diff --git a/src/components/PayRocketName.svelte b/src/components/PayRocketName.svelte new file mode 100644 index 0000000..d1e5a85 --- /dev/null +++ b/src/components/PayRocketName.svelte @@ -0,0 +1,129 @@ + + +{#if rocketName} + + + + + + + + Buy rocket name now! + {#if !currentUser} + + + Heads up! + You need a nostr signing extension to use Nostrocket! + + {/if} + Pay now with Lightning + + {#if invoice} + +
+ + +
+ + {:else} + + {/if} +
+
+{/if} diff --git a/src/lib/consts.ts b/src/lib/consts.ts new file mode 100644 index 0000000..874045c --- /dev/null +++ b/src/lib/consts.ts @@ -0,0 +1,11 @@ +export const NOSTROCKET_PUBKEY = 'd91191e30e00444b942c0e82cad470b32af171764c2275bee0bd99377efd4075'; +export const BUY_ROCKET_NAME_FEE = 10000; +export const BUY_ROCKET_NAME_ZAPPED_PUBKEY = NOSTROCKET_PUBKEY; +export const BUY_ROCKET_NAME_ZAPPED_EVENT = 'NOSTROCKET'; +// export const BUY_ROCKET_NAME_ZAPPED_PUBKEY = +// 'f36d27618877c76bb0ccfd946beb23f95d56cd67f20d0733886f83d207b824e8'; +// export const BUY_ROCKET_NAME_ZAPPED_EVENT = 'test-rocket-bob'; + +export const HELP_THREAD_ROOT_EVENT_ID = + '850941b4b8259aea64fef1e5083dd81af0d9bf1bcf3df6e370bdddbc6f819f4c'; +export const HELP_THREAD_ROOT_AUTHOR_PUBKEY = NOSTROCKET_PUBKEY; diff --git a/src/lib/event_helpers/help_thread.ts b/src/lib/event_helpers/help_thread.ts index bbf0fba..0251148 100644 --- a/src/lib/event_helpers/help_thread.ts +++ b/src/lib/event_helpers/help_thread.ts @@ -1,12 +1,8 @@ +import { HELP_THREAD_ROOT_AUTHOR_PUBKEY, HELP_THREAD_ROOT_EVENT_ID } from '@/consts'; import { prepareNostrEvent } from '@/helpers'; import { NDKKind, type NDKEvent } from '@nostr-dev-kit/ndk'; import type NDKSvelte from '@nostr-dev-kit/ndk-svelte'; -export const HELP_THREAD_ROOT_EVENT_ID = - '850941b4b8259aea64fef1e5083dd81af0d9bf1bcf3df6e370bdddbc6f819f4c'; -const HELP_THREAD_ROOT_AUTHOR_PUBKEY = - 'd91191e30e00444b942c0e82cad470b32af171764c2275bee0bd99377efd4075'; - export interface TreeNote { id: string; pubkey: string; diff --git a/src/lib/event_helpers/rockets.ts b/src/lib/event_helpers/rockets.ts index d965e23..f24d1ca 100644 --- a/src/lib/event_helpers/rockets.ts +++ b/src/lib/event_helpers/rockets.ts @@ -709,6 +709,35 @@ export class ZapPurchase { } } +export class ZapRocketNamePurchase { + Amount: number; + RocketName: string | undefined; + BuyerPubkey: string; + ZapReceipt: NDKEvent; + ZapRequest(): NDKEvent | undefined { + return getZapRequest(this.ZapReceipt); + } + Valid(): boolean { + //todo: validate zapper pubkey is from a LSP specified in rocket + let valid = true; + if (this.BuyerPubkey.length != 64) { + valid = false; + } + return valid; + } + constructor(zapReceipt: NDKEvent) { + this.ZapReceipt = zapReceipt; + this.Amount = getZapAmount(this.ZapRequest()); + let zapRequest = this.ZapRequest(); + if (zapRequest) { + this.BuyerPubkey = zapRequest.pubkey; + if (zapRequest.tagValue('name')) { + this.RocketName = zapRequest.tagValue('name'); + } + } + } +} + function getZapRequest(zapReceipt: NDKEvent): NDKEvent | undefined { let zapRequestEvent: NDKEvent | undefined = undefined; let zapRequest = zapReceipt.getMatchingTags('description'); diff --git a/src/routes/buymerits/+page.svelte b/src/routes/buymerits/+page.svelte index 3672622..d8f5fa8 100644 --- a/src/routes/buymerits/+page.svelte +++ b/src/routes/buymerits/+page.svelte @@ -13,6 +13,7 @@ import { goto } from '$app/navigation'; import { base } from '$app/paths'; import * as Tabs from '$lib/components/ui/tabs/index.js'; + import { NOSTROCKET_PUBKEY } from '@/consts'; let rocketEvents = $ndk.storeSubscribe([{ kinds: [31108 as number] }], { subId: 'all_rockets' }); @@ -142,10 +143,7 @@ let nostrocket = derived(rockets, ($rockets) => { let rocket: Rocket | undefined = undefined; for (let r of $rockets) { - if ( - r.Name() == 'NOSTROCKET' && - r.Event.pubkey == 'd91191e30e00444b942c0e82cad470b32af171764c2275bee0bd99377efd4075' - ) { + if (r.Name() == 'NOSTROCKET' && r.Event.pubkey == NOSTROCKET_PUBKEY) { //we consume the current list of bitcoin addresses from Nostrocket as a service so that users don't need to add a new address for every rocket //todo: make this dependent on votepower not my pubkey //todo: also allow rockets to have their own list of addresses so they can be used without nostrocket diff --git a/src/routes/help/+page.svelte b/src/routes/help/+page.svelte index 586dad0..cc55f3b 100644 --- a/src/routes/help/+page.svelte +++ b/src/routes/help/+page.svelte @@ -10,12 +10,9 @@ import { Label } from '$lib/components/ui/label/index.js'; import { Textarea } from '$lib/components/ui/textarea/index.js'; import { devmode, currentUser } from '@/stores/session'; - import { - buildNoteTree, - HELP_THREAD_ROOT_EVENT_ID, - prepareQuestionNoteEvent - } from '@/event_helpers/help_thread'; + import { buildNoteTree, prepareQuestionNoteEvent } from '@/event_helpers/help_thread'; import type NDKSvelte from '@nostr-dev-kit/ndk-svelte'; + import { HELP_THREAD_ROOT_EVENT_ID } from '@/consts'; let notes = $ndk.storeSubscribe({ kinds: [1 as NDKKind],