problem: not clear which rockets are for testing, and which are real

This commit is contained in:
Bob
2024-08-08 23:31:02 +08:00
parent 2186f6d1f3
commit 4db25ed767
6 changed files with 326 additions and 194 deletions

View File

@@ -4,15 +4,18 @@
import Button from '@/components/ui/button/button.svelte';
import { ndk } from '@/ndk';
import { currentUser } from '@/stores/session';
import ExclamationTriangle from 'svelte-radix/ExclamationTriangle.svelte';
import * as Alert from '$lib/components/ui/alert/index.js';
import validate from 'bitcoin-address-validation';
export let amrAuction: AMRAuction | undefined;
export let rocket: Rocket;
export let selected_amrs: Map<string, AMRAuction>;
export let selected_amrs: AMRAuction | undefined;
let bitcoinAddress: string = '';
$: bitcoinAddressInValid = true;
$: bitcoinAddressError = '';
$: isTestRocket = rocket.Name().toLowerCase().includes('test');
$: if (bitcoinAddress) {
if (!validate(bitcoinAddress)) {
@@ -49,7 +52,7 @@
console.log('AMRAuction', e);
e.publish().then((x) => {
console.log(x, e);
selected_amrs = new Map<string, AMRAuction>();
selected_amrs = undefined;
//goto(`${base}/rockets/${getRocketURL(e)}`);
});
}
@@ -60,6 +63,15 @@
You are selling {amrAuction.Merits} Merits
</div>
<div class="m-2 flex flex-col">
{#if isTestRocket}
<Alert.Root variant="destructive">
<ExclamationTriangle class="h-4 w-4" />
<Alert.Title>Warning</Alert.Title>
<Alert.Description
>Please do not enter a real Bitcoin address, as this is a test rocket.</Alert.Description
>
</Alert.Root>
{/if}
<div class="flex">
<Input
bind:value={bitcoinAddress}

View File

@@ -9,10 +9,11 @@
import { ChevronRight } from 'lucide-svelte';
export let rocket: Rocket;
//$page.url.searchParams.get("tab")
</script>
<Card.Root class="w-[350px]">
<Card.Root class="flex w-[350px] flex-col justify-between">
<Card.Header>
<Card.Title>{rocket.Name()}</Card.Title>
<Card.Description>{rocket.Mission()}</Card.Description>
@@ -27,17 +28,19 @@
<Name ndk={$ndk} pubkey={rocket.Event.pubkey} class="inline-block truncate" />
</div>
</Card.Content>
<Card.Footer class="flex justify-between">
<Button
on:click={() => {
console.log(rocket.Event.rawEvent());
}}
variant="outline">Print to Console</Button
>
<Button
on:click={() => {
goto(`${base}/rockets/${rocket.URL()}`);
}}>View Full Rocket<ChevronRight class="h-4 w-4" /></Button
>
<Card.Footer>
<div class="flex justify-between gap-2">
<Button
on:click={() => {
console.log(rocket.Event.rawEvent());
}}
variant="outline">Print to Console</Button
>
<Button
on:click={() => {
goto(`${base}/rockets/${rocket.URL()}`);
}}>View Full Rocket<ChevronRight class="h-4 w-4" /></Button
>
</div>
</Card.Footer>
</Card.Root>

View File

@@ -0,0 +1,103 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { Checkbox } from '@/components/ui/checkbox';
import * as Table from '@/components/ui/table';
import { currentUser } from '@/stores/session';
import { type RocketAMR, AMRAuction, Rocket } from '@/event_helpers/rockets';
import CreateAMRAuction from './CreateAMRAuction.svelte';
import Heading from './Heading.svelte';
export let rocket: Rocket;
export let amr: RocketAMR[];
let selected_amrs: AMRAuction | undefined;
function toggleSelected(amr: RocketAMR) {
if (!selected_amrs) {
selected_amrs = new AMRAuction(rocket.Event);
}
let existing = selected_amrs;
if (existing.AMRIDs.includes(amr.ID)) {
existing.Pop(amr);
} else {
existing.Push(amr);
}
selected_amrs = existing;
}
function getSelectedStatus(id: string, data: AMRAuction): boolean {
let has = false;
let amr = data;
if (amr) {
has = amr.AMRIDs.includes(id);
}
return has;
}
</script>
{#if $currentUser && amr.length > 0}
<Heading title={`ROCKET: ${rocket.Name()}`} />
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head class="w-[100px]">Selected</Table.Head>
<Table.Head class="w-[10px]">AMR</Table.Head>
<Table.Head>Merits</Table.Head>
<Table.Head>Status</Table.Head>
<Table.Head>Receiving Address</Table.Head>
<Table.Head class="text-right">Sats (approx)</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each rocket.PendingAMRAuctions().filter((r) => {
return Boolean(r.Owner == $currentUser.pubkey);
}) as p}
<Table.Row class="bg-purple-500 hover:bg-purple-600">
<Table.Cell><Checkbox /></Table.Cell>
<Table.Cell>{p.AMRIDs.length > 1 ? 'multiple' : p.AMRIDs[0].substring(0, 12)}</Table.Cell>
<Table.Cell>{p.Merits}</Table.Cell>
<Table.Cell>Pending</Table.Cell>
<Table.Cell>{p.RxAddress}</Table.Cell>
<Table.Cell class="text-right">{p.Merits}</Table.Cell>
</Table.Row>
{/each}
{#each amr as a, id (a.ID)}
{#if rocket.CanThisAMRBeSold(a.ID)}
<Table.Row
class={getSelectedStatus(a.ID, selected_amrs)
? 'bg-orange-500 hover:bg-orange-500'
: ''}
>
<Table.Cell
><Checkbox
disabled={Boolean(a.Extra?.eventAMR)}
id={a.ID}
checked={getSelectedStatus(a.ID, selected_amrs)}
on:click={() => {
toggleSelected(a);
}}
/></Table.Cell
>
<Table.Cell>
<span
class="cursor-pointer font-medium underline"
on:click={() => {
goto(`${base}/rockets/merits/${a.ID}`);
}}
>
{a.ID.substring(0, 6)}
</span>
</Table.Cell>
<Table.Cell>{a.Merits}</Table.Cell>
<Table.Cell>{a.Extra?.eventAMR ? 'pending' : 'Eligible'}</Table.Cell>
<Table.Cell>{a.Extra?.eventAMR?.RxAddress}</Table.Cell>
<Table.Cell class="text-right">{a.Merits}</Table.Cell>
</Table.Row>
{/if}
{/each}
</Table.Body>
</Table.Root>
<CreateAMRAuction {rocket} amrAuction={selected_amrs} bind:selected_amrs />
{/if}

View File

@@ -7,6 +7,9 @@
import Heading from '../../components/Heading.svelte';
import { Product, Rocket } from '@/event_helpers/rockets';
import ProductGroup from '../../components/ProductGroup.svelte';
import * as Tabs from '$lib/components/ui/tabs/index.js';
import ExclamationTriangle from 'svelte-radix/ExclamationTriangle.svelte';
import * as Alert from '$lib/components/ui/alert/index.js';
let rockets: NDKEventStore<NDKEvent> | undefined;
let products: NDKEventStore<NDKEvent> | undefined;
@@ -25,43 +28,95 @@
return $rockets;
});
let productsToRender = derived([rocketsWithProducts, products], ([$rocketsWP, $products]) => {
let data = new Map<Rocket, Map<string, Product[]>>();
let productMap = new Map($products.map((e) => [e.id, e]));
for (let r of $rocketsWP) {
let events: Product[] = [];
for (let p of r.getMatchingTags('product')) {
let productEvent = productMap.get(p[1].split(':')[0]);
if (productEvent) {
events.push(new Product(productEvent));
}
}
if (events.length > 0) {
data.set(new Rocket(r), groups(events));
}
}
let productsToRenderStore = derived(
[rocketsWithProducts, products],
([$rocketsWP, $products]) => {
let mainnet: Map<Rocket, Map<string, Product[]>> = new Map();
let testnet: Map<Rocket, Map<string, Product[]>> = new Map();
function groups(products: Product[]): Map</* group name*/ string, Product[]> {
return products.reduce((acc, product) => {
const group = product.Group();
if (!acc.has(group)) {
acc.set(group, []);
let productMap = new Map($products.map((e) => [e.id, e]));
for (let r of $rocketsWP) {
let events: Product[] = [];
for (let p of r.getMatchingTags('product')) {
let productEvent = productMap.get(p[1].split(':')[0]);
if (productEvent) {
events.push(new Product(productEvent));
}
}
acc.get(group)!.push(product);
return acc;
}, new Map<string, Product[]>());
if (events.length > 0) {
let groupedProducts = groups(events);
if (r.dTag!.toLowerCase().includes('test')) {
testnet.set(new Rocket(r), groupedProducts);
} else {
mainnet.set(new Rocket(r), groupedProducts);
}
}
}
function groups(products: Product[]): Map</* group name*/ string, Product[]> {
return products.reduce((acc, product) => {
const group = product.Group();
if (!acc.has(group)) {
acc.set(group, []);
}
acc.get(group)!.push(product);
return acc;
}, new Map<string, Product[]>());
}
return { mainnet, testnet };
}
return data;
);
let mainnet: Map<Rocket, Map<string, Product[]>> = new Map();
let testnet: Map<Rocket, Map<string, Product[]>> = new Map();
productsToRenderStore.subscribe(($productsToRenderStore) => {
mainnet = $productsToRenderStore.mainnet;
testnet = $productsToRenderStore.testnet;
});
</script>
{#if productsToRender && $productsToRender}
{#each $productsToRender as [rocket, groups] (rocket.Event.id)}
<Heading title={rocket.Event.dTag} />
<div class="grid gap-4" style="grid-template-columns: repeat(auto-fit, 350px);">
{#each groups as [identifier, products] (identifier)}
<ProductGroup {products} {rocket} />
<Heading title="Products" />
<Tabs.Root value="mainnet">
<Tabs.List>
<Tabs.Trigger value="mainnet">Mainnet</Tabs.Trigger>
<Tabs.Trigger value="testnet">Testnet</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="mainnet">
{#if mainnet.size > 0}
{#each mainnet as [rocket, groups] (rocket.Event.id)}
<Heading title={rocket.Event.dTag} />
<div class="grid gap-4" style="grid-template-columns: repeat(auto-fit, 350px);">
{#each groups as [identifier, products] (identifier)}
<ProductGroup {products} {rocket} />
{/each}
</div>
{/each}
</div>
{/each}
{/if}
{:else}
<Alert.Root>
<Alert.Description
>Currently, there are no products on the mainnet; you can check the testnet.</Alert.Description
>
</Alert.Root>
{/if}
</Tabs.Content>
<Tabs.Content value="testnet">
<Alert.Root class="my-2">
<ExclamationTriangle class="h-4 w-4" />
<Alert.Title>Note</Alert.Title>
<Alert.Description
>The following products are for testing purposes only. Please do not send real Bitcoin.</Alert.Description
>
</Alert.Root>
{#each testnet as [rocket, groups] (rocket.Event.id)}
<Heading title={rocket.Event.dTag} />
<div class="grid gap-4" style="grid-template-columns: repeat(auto-fit, 350px);">
{#each groups as [identifier, products] (identifier)}
<ProductGroup {products} {rocket} />
{/each}
</div>
{/each}
</Tabs.Content>
</Tabs.Root>

View File

@@ -1,8 +1,4 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { Checkbox } from '@/components/ui/checkbox';
import * as Table from '@/components/ui/table';
import { type RocketAMR, AMRAuction, Rocket } from '@/event_helpers/rockets';
import { ndk } from '@/ndk';
import { currentUser } from '@/stores/session';
@@ -10,10 +6,10 @@
import { onDestroy, onMount } from 'svelte';
import { derived } from 'svelte/store';
import Login from '../../components/Login.svelte';
import CreateAMRAuction from '../../components/CreateAMRAuction.svelte';
import SellMeritsTable from '../../components/SellMeritsTable.svelte';
import MeritAuctions from '../../stateupdaters/MeritAuctions.svelte';
import Heading from '../../components/Heading.svelte';
import type { NDKEventStore, ExtendedBaseType } from '@nostr-dev-kit/ndk-svelte';
import * as Tabs from '$lib/components/ui/tabs/index.js';
let rocketEvents = $ndk.storeSubscribe([{ kinds: [31108 as number] }], { subId: 'all_rockets' });
let amrAuctionEvents = $ndk.storeSubscribe([{ kinds: [1412 as number] }], {
subId: 'my_auctions'
@@ -31,7 +27,7 @@
if (!existing) {
existing = e;
}
if (e.created_at > existing) {
if (e.created_at > existing.created_at) {
existing = e;
}
m.set(e.pubkey + e.dTag, e);
@@ -57,13 +53,14 @@
}
);
let myMeritRequests = derived(
let meritRequestStore = derived(
[currentUser, rockets, myAmrAuctionEvents],
([$currentUser, $rockets, $myAmrAuctionEvents]) => {
let merits = new Map<Rocket, RocketAMR[]>();
let mainnet: Map<Rocket, RocketAMR[]> = new Map();
let testnet: Map<Rocket, RocketAMR[]> = new Map();
if ($currentUser) {
for (let r of $rockets) {
//let parsedRocket = new Rocket(r);
let _merits: RocketAMR[] = [];
for (let [_, amr] of r.ApprovedMeritRequests()) {
let amrAuction = $myAmrAuctionEvents.get(amr.ID);
@@ -74,47 +71,26 @@
_merits.push(amr);
}
}
merits.set(r, _merits);
if (r.Name().toLowerCase().includes('test')) {
testnet.set(r, _merits);
} else {
mainnet.set(r, _merits);
}
}
}
return merits;
return { mainnet, testnet };
}
);
let selected_amrs = new Map</* rocket id */ string, AMRAuction>();
function toggleSelected(amr: RocketAMR, rocket: Rocket) {
if (!selected_amrs.has(rocket.Event.id)) {
selected_amrs.set(rocket.Event.id, new AMRAuction(rocket.Event));
}
let existing = selected_amrs.get(rocket.Event.id)!;
if (existing.AMRIDs.includes(amr.ID)) {
existing.Pop(amr);
} else {
existing.Push(amr);
}
selected_amrs.set(rocket.Event.id, existing);
selected_amrs = selected_amrs;
}
let mainnet: Map<Rocket, RocketAMR[]> = new Map();
let testnet: Map<Rocket, RocketAMR[]> = new Map();
function getSelectedStatus(rocket: string, id: string, data: Map<string, AMRAuction>): boolean {
let has = false;
let amr = data.get(rocket);
if (amr) {
has = amr.AMRIDs.includes(id);
}
return has;
}
function getMerits(rocket: string, data: Map<string, AMRAuction>): number {
let m = data.get(rocket);
console.log(m);
let merits = 0;
if (m && m.Merits) {
merits = m.Merits;
}
return merits;
}
meritRequestStore.subscribe(($meritRequestStore) => {
mainnet = $meritRequestStore.mainnet;
testnet = $meritRequestStore.testnet;
});
// function getTotal(auction:AMRAuction):number {
// let total = 0
@@ -138,77 +114,21 @@
<Heading title="Trade your Merits for Sats" />
{#if $currentUser}
{#each $myMeritRequests as [rocket, amr]}
{#if amr.length > 0}
<h1>ROCKET: {rocket.Name()}</h1>
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head class="w-[100px]">Selected</Table.Head>
<Table.Head class="w-[10px]">AMR</Table.Head>
<Table.Head>Merits</Table.Head>
<Table.Head>Status</Table.Head>
<Table.Head>Receiving Address</Table.Head>
<Table.Head class="text-right">Sats (approx)</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each rocket.PendingAMRAuctions().filter((r) => {
return Boolean(r.Owner == $currentUser.pubkey);
}) as p}
<Table.Row class="bg-purple-500 hover:bg-purple-600">
<Table.Cell><Checkbox /></Table.Cell>
<Table.Cell
>{p.AMRIDs.length > 1 ? 'multiple' : p.AMRIDs[0].substring(0, 12)}</Table.Cell
>
<Table.Cell>{p.Merits}</Table.Cell>
<Table.Cell>Pending</Table.Cell>
<Table.Cell>{p.RxAddress}</Table.Cell>
<Table.Cell class="text-right">{p.Merits}</Table.Cell>
</Table.Row>
{/each}
{#each amr as a, id (a.ID)}
{#if rocket.CanThisAMRBeSold(a.ID)}
<Table.Row
class={getSelectedStatus(rocket.Event.id, a.ID, selected_amrs)
? 'bg-orange-500 hover:bg-orange-500'
: ''}
>
<Table.Cell
><Checkbox
disabled={Boolean(a.Extra?.eventAMR)}
id={a.ID}
checked={getSelectedStatus(rocket.Event.id, a.ID, selected_amrs)}
on:click={() => {
toggleSelected(a, rocket);
}}
/></Table.Cell
>
<Table.Cell
><span
class="cursor-pointer font-medium underline"
on:click={() => {
goto(`${base}/rockets/merits/${a.ID}`);
}}>{a.ID.substring(0, 6)}</span
></Table.Cell
>
<Table.Cell>{a.Merits}</Table.Cell>
<Table.Cell>{a.Extra?.eventAMR ? 'pending' : 'Eligible'}</Table.Cell>
<Table.Cell>{a.Extra?.eventAMR?.RxAddress}</Table.Cell>
<Table.Cell class="text-right">{a.Merits}</Table.Cell>
</Table.Row>
{/if}
{/each}
</Table.Body>
</Table.Root>
<CreateAMRAuction
{rocket}
amrAuction={selected_amrs.get(rocket.Event.id)}
bind:selected_amrs
/>
{/if}
{/each}
<Tabs.Root value="mainnet">
<Tabs.List>
<Tabs.Trigger value="mainnet">Mainnet</Tabs.Trigger>
<Tabs.Trigger value="testnet">Testnet</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="mainnet">
{#each mainnet as [rocket, amr]}
<SellMeritsTable {rocket} {amr} />
{/each}
</Tabs.Content>
<Tabs.Content value="testnet">
{#each testnet as [rocket, amr]}
<SellMeritsTable {rocket} {amr} />
{/each}
</Tabs.Content>
</Tabs.Root>
{:else}<Login />{/if}
<MeritAuctions {rockets} />

View File

@@ -3,19 +3,20 @@
import { ndk } from '@/ndk';
import { onDestroy } from 'svelte';
import { derived } from 'svelte/store';
import * as Tabs from '$lib/components/ui/tabs/index.js';
import Heading from '../../components/Heading.svelte';
import RocketCard from '../../components/RocketCard.svelte';
import Todo from '../../components/Todo.svelte';
import ExclamationTriangle from 'svelte-radix/ExclamationTriangle.svelte';
import * as Alert from '$lib/components/ui/alert/index.js';
import AssociateBitcoinAddress from '../../stateupdaters/AssociateBitcoinAddress.svelte';
let _rockets = $ndk.storeSubscribe([{ kinds: [31108 as number] }], { subId: 'rockets' });
let _rockets = $ndk.storeSubscribe([{ kinds: [31108 as number] }], { subId: 'rockets' });
onDestroy(() => {
_rockets?.unsubscribe();
});
let rockets = derived(_rockets, ($rockets) => {
let _r = new Map<string, Rocket>();
for (let e of $rockets) {
let existing = _r.get(`${e.pubkey}${e.dTag}`);
if (!existing) {
@@ -23,44 +24,82 @@
}
const existingCreatedAt = existing.Event?.created_at ?? 0;
const newCreatedAt = e.created_at ?? 0;
if (existingCreatedAt <= newCreatedAt) {
_r.set(`${e.pubkey}${e.dTag}`, existing);
}
}
let rocketArray = Array.from(_r.values());
rocketArray.sort((a, b) => {
// First condition: "Nostrocket" at the top
if (a.Name() === 'Nostrocket') return -1;
if (b.Name() === 'Nostrocket') return 1;
// Second condition: "test" rockets grouped underneath
const aIsTest = a.Name().toLowerCase().includes('test');
const bIsTest = b.Name().toLowerCase().includes('test');
if (aIsTest && !bIsTest) return 1;
if (!aIsTest && bIsTest) return -1;
// Default sorting by created_at, handling undefined
const aCreatedAt = a.Event?.created_at ?? 0;
const bCreatedAt = b.Event?.created_at ?? 0;
return aCreatedAt - bCreatedAt;
});
rocketArray.sort((a, b) => sortRockets(a, b));
return rocketArray;
});
//todo: until we have namerocket working, just manually dedupe rockets based on my pubkey
//todo: write a recognizer/validator for rocket events
function sortRockets(a: Rocket, b: Rocket): number {
// First condition: "Nostrocket" at the top
if (a.Name() === 'Nostrocket') return 1;
if (b.Name() === 'Nostrocket') return -1;
// Second condition: "test" rockets grouped underneath
const aIsTest = a.Name().toLowerCase().includes('test');
const bIsTest = b.Name().toLowerCase().includes('test');
if (aIsTest && !bIsTest) return -1;
if (!aIsTest && bIsTest) return 1;
// Default sorting by created_at, handling undefined
const aCreatedAt = a.Event?.created_at ?? 0;
const bCreatedAt = b.Event?.created_at ?? 0;
return bCreatedAt - aCreatedAt;
}
function splitRockets(rocketArray: Rocket[]): { mainnet: Rocket[]; testnet: Rocket[] } {
let mainnet: Rocket[] = [];
let testnet: Rocket[] = [];
for (let rocket of rocketArray) {
if (rocket.Name().toLowerCase().includes('test')) {
testnet.push(rocket);
} else {
mainnet.push(rocket);
}
}
return { mainnet, testnet };
}
let rocketStore = derived(rockets, (rocketArray) => splitRockets(rocketArray));
let mainnet: Rocket[] = [];
let testnet: Rocket[] = [];
rocketStore.subscribe(($rocketStore) => {
mainnet = $rocketStore.mainnet;
testnet = $rocketStore.testnet;
});
</script>
<Heading title="Rockets" />
{#if rockets && $rockets}
<AssociateBitcoinAddress {rockets} />
<Todo text={['render these in a nicer way, maybe a grid or something']} />
{#each $rockets as rocket (`${rocket.Event.pubkey}${rocket.Name()}`)}
<RocketCard {rocket} />
{/each}
<Tabs.Root value="mainnet">
<Tabs.List>
<Tabs.Trigger value="mainnet">Mainnet</Tabs.Trigger>
<Tabs.Trigger value="testnet">Testnet</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="mainnet">
<div class="grid gap-2" style="grid-template-columns: repeat(auto-fit, 350px);">
{#each mainnet as rocket (`${rocket.Event.pubkey}${rocket.Name()}`)}
<RocketCard {rocket} />
{/each}
</div>
</Tabs.Content>
<Tabs.Content value="testnet">
<Alert.Root class="my-2">
<ExclamationTriangle class="h-4 w-4" />
<Alert.Title>Note</Alert.Title>
<Alert.Description
>The following rocket is for testing purposes only. Any rocket with "test" in its name is
intended solely for testing.</Alert.Description
>
</Alert.Root>
<div class="grid gap-2" style="grid-template-columns: repeat(auto-fit, 350px);">
{#each testnet as rocket (`${rocket.Event.pubkey}${rocket.Name()}`)}
<RocketCard {rocket} />
{/each}
</div>
</Tabs.Content>
</Tabs.Root>
{/if}