Skip to content

Commit

Permalink
Merge pull request #126 from bob2402/buy-rocket-name
Browse files Browse the repository at this point in the history
problem: can't create real rockets
  • Loading branch information
gsovereignty authored Aug 30, 2024
2 parents 0e04103 + 6e94c7b commit 5d4fa91
Show file tree
Hide file tree
Showing 8 changed files with 325 additions and 52 deletions.
186 changes: 149 additions & 37 deletions src/components/CreateNewRocket.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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<NDKEvent> | undefined;
const rocketsStore = writable<NDKEvent[]>([]);
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<string, ZapRocketNamePurchase>();
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;
$: {
Expand All @@ -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');
Expand All @@ -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;
Expand Down Expand Up @@ -124,20 +214,42 @@
<Label for="name" class="text-right">Name</Label>
<Input bind:value={name} id="name" placeholder="Name-of-your-rocket" class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="isMainnet" class="text-right">Mainnet</Label>
<Checkbox id="isMainnet" bind:checked={$mainnet}></Checkbox>
</div>
</div>
<div class="m-0 p-0 text-sm text-red-500">{nameError}</div>
{#if $mainnet && nameValid && !isPurchasedByMe}
<div class="m-0 p-0 text-sm">
To create a mainnet rocket, you need to pay {formatSats(BUY_ROCKET_NAME_FEE)} for a rocket name.
</div>
{/if}
{#if $devmode}
<Checkbox
<div>Purchased My Rocket Name: {$purchasedRocketNames.my.join(', ')}</div>
<div>Purchased All Rocket Name: {$purchasedRocketNames.all.join(', ')}</div>
<div>isPurchasedByOthers: {isPurchasedByOthers}</div>
<div>isPurchasedByMe: {isPurchasedByMe}</div>
<div>isPublished: {isPublished}</div>
<Button
variant="outline"
on:click={() => {
mainnet.set(true);
}}
/>
console.log($validZaps);
}}>Print Zaps</Button
>
{/if}
<Dialog.Footer>
{#if $mainnet && $nostrocket && !isPurchasedByMe}
<PayRocketName
disabled={!nameValid || isPurchasedByOthers}
rocketName={name}
nostrocket={$nostrocket}
/>
{/if}
<Button
disabled={nameInvalid || !$currentUser}
disabled={!canPublish || !$currentUser}
on:click={() => {
publish($ndk, `${name}${$mainnet ? '' : '-test'}`);
publish($ndk, name);
}}
type="submit">Publish</Button
>
Expand Down
3 changes: 2 additions & 1 deletion src/components/NotifyMe.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -50,7 +51,7 @@
async function publishEncryptedDirectMessage(content: string) {
const RECEIVER = new NDKUser({
pubkey: 'd91191e30e00444b942c0e82cad470b32af171764c2275bee0bd99377efd4075'
pubkey: NOSTROCKET_PUBKEY
});
const originalSigner = $ndk.signer;
Expand Down
129 changes: 129 additions & 0 deletions src/components/PayRocketName.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import { Input } from '$lib/components/ui/input';
import * as Alert from '@/components/ui/alert';
import type { Rocket } from '@/event_helpers/rockets';
import { formatSats } from '@/helpers';
import { ndk } from '@/ndk';
import { currentUser } from '@/stores/session';
import { NDKZap } from '@nostr-dev-kit/ndk';
import { Check, Terminal } from 'lucide-svelte';
import { cubicOut } from 'svelte/easing';
import { tweened } from 'svelte/motion';
import { requestProvider } from 'webln';
import CopyButton from './CopyButton.svelte';
import QrCodeSvg from './QrCodeSvg.svelte';
import { BUY_ROCKET_NAME_FEE } from '@/consts';
export let nostrocket: Rocket;
export let rocketName: string;
export let disabled: boolean;
let invoice: string | null;
let paymentInitiated: boolean;
let paymentFinished: boolean;
const scale = tweened(0, { duration: 1000, easing: cubicOut });
async function zap() {
if (nostrocket && rocketName) {
const z = new NDKZap({
ndk: $ndk,
zappedEvent: nostrocket.Event,
zappedUser: nostrocket.Event.author
});
invoice = await z.createZapRequest(
BUY_ROCKET_NAME_FEE * 1000,
`Purchase of rocket name: ${rocketName}`,
[['name', rocketName]]
);
}
}
async function payWithWebLn() {
paymentInitiated = true;
try {
if (!invoice) {
throw Error('invoice not found');
}
const webln = await requestProvider();
const response = await webln.sendPayment(invoice);
if (response && response.preimage) {
console.log(response.preimage);
paymentFinished = true;
await scale.set(1);
await new Promise((resolve) => setTimeout(resolve, 1000)); // allow 1 second before resetting payment/dialog states
open = false;
paymentFinished = false;
await scale.set(0);
}
} catch (error) {
console.error(error);
} finally {
paymentInitiated = false;
}
}
let previousRocketName: string;
$: if (rocketName !== previousRocketName) {
invoice = null;
previousRocketName = rocketName;
}
let open: boolean;
</script>

{#if rocketName}
<Dialog.Root bind:open>
<Dialog.Trigger>
<Button {disabled}>
{#if open}
Confirming...
{:else}
Buy Now for {formatSats(BUY_ROCKET_NAME_FEE)}
{/if}
</Button>
</Dialog.Trigger>

<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>Buy rocket name now!</Dialog.Title>
{#if !currentUser}
<Alert.Root>
<Terminal class="h-4 w-4" />
<Alert.Title>Heads up!</Alert.Title>
<Alert.Description
>You need a nostr signing extension to use Nostrocket!</Alert.Description
>
</Alert.Root>
{/if}
<Dialog.Description>Pay now with Lightning</Dialog.Description>
</Dialog.Header>
{#if invoice}
<QrCodeSvg content={invoice} />
<div class="flex gap-2">
<Input bind:value={invoice} readonly />
<CopyButton text={invoice} />
</div>
<Button on:click={payWithWebLn}>
{#if paymentFinished}
<div style="transform: scale({$scale});">
<Check class="me-2 text-white" color="white" />
</div>
{:else if paymentInitiated}
Confirming payment...
{:else}
Pay with WebLN
{/if}
</Button>
{:else}
<Button on:click={zap}>Create invoice</Button>
{/if}
</Dialog.Content>
</Dialog.Root>
{/if}
11 changes: 11 additions & 0 deletions src/lib/consts.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 5d4fa91

Please sign in to comment.