problem: can't list amrs for sale

This commit is contained in:
gsovereignty
2024-08-01 18:27:41 +08:00
parent 6368c7d580
commit 47f2e1666c
16 changed files with 704 additions and 73 deletions

29
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"@nostr-dev-kit/ndk-svelte": "^2.2.15",
"@nostr-dev-kit/ndk-svelte-components": "^2.2.16",
"@sveltejs/adapter-static": "^3.0.1",
"bitcoin-address-validation": "^2.2.3",
"bits-ui": "^0.21.10",
"clsx": "^2.1.1",
"cmdk-sv": "^0.0.17",
@@ -1676,6 +1677,19 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/base58-js": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/base58-js/-/base58-js-1.0.5.tgz",
"integrity": "sha512-LkkAPP8Zu+c0SVNRTRVDyMfKVORThX+rCViget00xdgLRrKkClCTz1T7cIrpr69ShwV5XJuuoZvMvJ43yURwkA==",
"engines": {
"node": ">= 8"
}
},
"node_modules/bech32": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
"integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg=="
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -1687,6 +1701,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bitcoin-address-validation": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/bitcoin-address-validation/-/bitcoin-address-validation-2.2.3.tgz",
"integrity": "sha512-1uGCGl26Ye8JG5qcExtFLQfuib6qEZWNDo1ZlLlwp/z7ygUFby3IxolgEfgMGaC+LG9csbVASLcH8fRLv7DIOg==",
"dependencies": {
"base58-js": "^1.0.0",
"bech32": "^2.0.0",
"sha256-uint8array": "^0.10.3"
}
},
"node_modules/bits-ui": {
"version": "0.21.10",
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-0.21.10.tgz",
@@ -4492,6 +4516,11 @@
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
"integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ=="
},
"node_modules/sha256-uint8array": {
"version": "0.10.7",
"resolved": "https://registry.npmjs.org/sha256-uint8array/-/sha256-uint8array-0.10.7.tgz",
"integrity": "sha512-1Q6JQU4tX9NqsDGodej6pkrUVQVNapLZnvkwIhddH/JqzBZF1fSaxSWNY6sziXBE8aEa2twtGkXUrwzGeZCMpQ=="
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@@ -44,6 +44,7 @@
"@nostr-dev-kit/ndk-svelte": "^2.2.15",
"@nostr-dev-kit/ndk-svelte-components": "^2.2.16",
"@sveltejs/adapter-static": "^3.0.1",
"bitcoin-address-validation": "^2.2.3",
"bits-ui": "^0.21.10",
"clsx": "^2.1.1",
"cmdk-sv": "^0.0.17",

View File

@@ -44,15 +44,15 @@
>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Calculate the sats</Dialog.Title>
<Dialog.Title>How much should you request?</Dialog.Title>
<Dialog.Description
>Calculate the sats of the request according to your hour rate.</Dialog.Description
>Use this form to calculate how many Sats you should claim. This information will be public and permanent.</Dialog.Description
>
</Dialog.Header>
<Label for="hourly_rate">Hour rate</Label>
<Input bind:value={hourly_rate} id="hourly_rate" placeholder="? USD / hour" />
<Label for="spent_minutes">Working hours</Label>
<Input bind:value={spent_minutes} id="spent_minutes" placeholder="? minutes" />
<Label for="hourly_rate">Your hourly rate in CuckLoserBucks</Label>
<Input bind:value={hourly_rate} id="hourly_rate" placeholder="USD hourly rate" />
<Label for="spent_minutes">Time spent solving this problem (minutes)</Label>
<Input bind:value={spent_minutes} id="spent_minutes" placeholder="minutes" />
{#if calcSats}
<div>Result: {calcSats.toFixed(0)} sats</div>
{/if}

View File

@@ -5,6 +5,8 @@
import { Avatar } from '@nostr-dev-kit/ndk-svelte-components';
import { NDKNip07Signer } from '@nostr-dev-kit/ndk';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
onMount(() => {
if (localStorage.getItem('signed-in')) {
@@ -48,6 +50,9 @@
<DropdownMenu.Content align="end">
<DropdownMenu.Label>My Account</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Item on:click={()=>{goto(`${base}/sellmerits`)}}>Sell Merits</DropdownMenu.Item>
<DropdownMenu.Item>Buy Merits</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item>Settings</DropdownMenu.Item>
<DropdownMenu.Item>Support</DropdownMenu.Item>
<DropdownMenu.Separator />

View File

@@ -5,6 +5,7 @@
import { getRocketURL, parseProblem } from '@/helpers';
import type { NDKEvent } from '@nostr-dev-kit/ndk';
import MeritSummaryCard from './MeritSummaryCard.svelte';
import { Rocket } from '@/event_helpers/rockets';
export let rocket: NDKEvent;
export let merit: MeritRequest;
@@ -37,6 +38,6 @@
<main
class="grid flex-1 items-start gap-4 p-4 sm:px-6 sm:py-0 md:gap-2 lg:grid-cols-3 xl:grid-cols-3"
>
<MeritSummaryCard {rocket} {merit} />
<MeritSummaryCard parsedRocket={new Rocket(rocket)} {merit} />
</main>
</div>

View File

@@ -12,17 +12,17 @@
import { onDestroy } from 'svelte';
import { derived } from 'svelte/store';
export let rocket: NDKEvent;
let parsedRocket = new Rocket(rocket);
//export let rocket: NDKEvent;
export let parsedRocket:Rocket;// = new Rocket(rocket);
let _merits = $ndk.storeSubscribe(
[{ '#a': [`31108:${rocket.author.pubkey}:${rocket.dTag}`], kinds: [1409 as NDKKind] }],
[{ '#a': [`31108:${parsedRocket.Event.author.pubkey}:${parsedRocket.Name()}`], kinds: [1409 as NDKKind] }],
{
subId: `${rocket.dTag}_merits`
subId: `${parsedRocket.Name()}_merits`
}
);
let _votes = $ndk.storeSubscribe({ '#a': [RocketATagFilter(rocket)], kinds: [1410 as NDKKind] });
let _votes = $ndk.storeSubscribe({ '#a': [RocketATagFilter(parsedRocket.Event)], kinds: [1410 as NDKKind] });
onDestroy(() => {
_merits?.unsubscribe();

View File

@@ -21,15 +21,15 @@
import MeritComment from './MeritComment.svelte';
export let merit: MeritRequest;
export let rocket: NDKEvent;
//export let rocket: NDKEvent;
let cuckPrice: number | undefined = undefined;
let result: VoteDirection | undefined;
let parsedRocket = new Rocket(rocket);
export let parsedRocket:Rocket;
let _votes = $ndk.storeSubscribe(
{ '#a': [RocketATagFilter(rocket)], '#e': [merit.ID], kinds: [1410 as NDKKind] },
{ '#a': [RocketATagFilter(parsedRocket.Event)], '#e': [merit.ID], kinds: [1410 as NDKKind] },
{
subId: merit.ID
}
@@ -80,7 +80,7 @@
updatedRocket.ndk = $ndk;
updatedRocket.sign().then(() => {
updatedRocket.publish().then(() => {
goto(`${base}/rockets/${getRocketURL(rocket)}`);
goto(`${base}/rockets/${getRocketURL(parsedRocket.Event)}`);
});
});
}
@@ -170,7 +170,7 @@
<Separator class="my-4" />
<div class="font-semibold">Votes</div>
{#if $votes.size == 0}<Alert
><Info />Waiting for existing <span class="italic">{new Rocket(rocket).Name()}</span> Merit
><Info />Waiting for existing <span class="italic">{parsedRocket.Name()}</span> Merit
holders to vote</Alert
>
{/if}
@@ -219,7 +219,7 @@
{:else if result == 'blackball'}
<span class="scroll-m-20 text-lg font-semibold tracking-tight md:text-xl">REJECTED</span>
{:else if !result}
<VoteOnMeritRequest {merit} rocket={new Rocket(rocket)} />
<VoteOnMeritRequest {merit} rocket={parsedRocket} />
{/if}
</Card.Footer>
</Card.Root>

View File

@@ -0,0 +1,43 @@
<script lang="ts">
import { base } from '$app/paths';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import { Rocket } from '@/event_helpers/rockets';
import { getRocketURL } from '@/helpers';
import type { NDKEvent } from '@nostr-dev-kit/ndk';
export let rocket: NDKEvent;
let parsedRocket = new Rocket(rocket)
</script>
<div class="flex flex-col sm:gap-4">
<header class="flex items-center">
<Breadcrumb.Root class="flex">
<Breadcrumb.List>
<Breadcrumb.Item>
<Breadcrumb.Link href={`${base}/rockets/${getRocketURL(rocket)}`}
>{rocket.getMatchingTags('d')[0][1]}</Breadcrumb.Link
>
</Breadcrumb.Item>
<Breadcrumb.Separator />
<Breadcrumb.Item>
<Breadcrumb.Link href={`${base}/rockets/${getRocketURL(rocket)}`}
>Merit Requests</Breadcrumb.Link
>
</Breadcrumb.Item>
<Breadcrumb.Separator />
<Breadcrumb.Item>
<Breadcrumb.Page>
Trade
</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</header>
<main
class="grid flex-1 items-start gap-4 p-4 sm:px-6 sm:py-0 md:gap-2 lg:grid-cols-3 xl:grid-cols-3"
>
Trade {parsedRocket.Name()} Merits
</main>
</div>

View File

@@ -11,6 +11,7 @@
import ProposedProducts from './ProposedProducts.svelte';
import Todo from './Todo.svelte';
import UpdateMission from './UpdateMission.svelte';
import { Rocket } from '@/event_helpers/rockets';
export let rocket: NDKEvent;
@@ -40,7 +41,7 @@
<ProposedProducts {rocket} />
<MeritRequests {rocket} />
<MeritRequests parsedRocket={new Rocket(rocket)} />
<Card.Root class="sm:col-span-3">
<Card.Header class="pb-3">
<Card.Title>Actions</Card.Title>

View File

@@ -36,6 +36,7 @@ export class MeritRequest {
}
IncludedInRocketState(rocket: Rocket): boolean {
let included = rocket.ApprovedMeritRequests();
console.log(39, rocket, included.get(this.ID))
return Boolean(included.get(this.ID));
}
BasicValidation(): boolean {

View File

@@ -1,6 +1,7 @@
import { NDKEvent, type NDKTag } from '@nostr-dev-kit/ndk';
import { MapOfVotes, MeritRequest, Votes } from './merits';
import { getAuthorizedZapper } from '@/helpers';
import validate from 'bitcoin-address-validation';
export class Rocket {
Event: NDKEvent;
@@ -37,12 +38,12 @@ export class Rocket {
return amr;
}
TotalMerits(): number {
let total = 0
let amr = this.ApprovedMeritRequests()
let total = 0;
let amr = this.ApprovedMeritRequests();
for (let [_, _amr] of amr) {
total += _amr.Merits
total += _amr.Merits;
}
return total
return total;
}
ValidateAMRProof(amrProof: NDKEvent): boolean {
let result = false;
@@ -114,7 +115,19 @@ export class Rocket {
event = new NDKEvent(this.Event.ndk, this.Event.rawEvent());
event.created_at = Math.floor(new Date().getTime() / 1000);
event.tags.push(['merit', `${request.Pubkey}:${request.ID}:0:0:${request.Merits}`]);
event.tags.push(['proof_full', JSON.stringify(signedProof)]);
event.tags.push(['proof_full', JSON.stringify(signedProof.rawEvent())]);
updateIgnitionAndParentTag(event);
}
return event;
}
UpsertAMRAuction(request: AMRAuction): NDKEvent | undefined {
let event: NDKEvent | undefined = undefined;
if (request.ValidateAgainstRocket(this)) {
this.PrepareForUpdate();
event = new NDKEvent(this.Event.ndk, this.Event.rawEvent());
event.created_at = Math.floor(new Date().getTime() / 1000);
event.tags.push(['proof_full', JSON.stringify(request.Event!.rawEvent())]);
updateIgnitionAndParentTag(event);
}
return event;
@@ -238,6 +251,7 @@ export class RocketAMR {
LeadTime: number;
LeadTimeUpdate: number;
Merits: number;
Extra: {};
SatsOwed(): number {
return 0;
}
@@ -436,3 +450,126 @@ export async function ValidateZapPublisher(rocket: NDKEvent, zap: NDKEvent): Pro
// }).catch(()=>{reject(false)})
});
}
export class AMRAuction {
AMRIDs: string[];
Owner: string|undefined;
StartPrice: number;
EndPrice: number;
RxAddress: string;
RocketD: string;
RocketP: string;
Merits: number;
Event: NDKEvent;
GenerateEvent(): NDKEvent {
let e = new NDKEvent();
e.kind = 1412;
e.created_at = Math.floor(new Date().getTime() / 1000);
for (let id of this.AMRIDs) {
e.tags.push(["request", id])
}
e.tags.push(['a', `31108:${this.RocketP}:${this.RocketD}`]);
//todo: allow user to set start and end auction price
e.tags.push(['price', this.StartPrice + ":" + this.EndPrice]);
e.tags.push(["onchain", this.RxAddress])
return e
}
Push(amr: RocketAMR) {
if (this.Owner && amr.Pubkey != this.Owner) {
throw new Error("invalid pubkey")
}
this.Owner = amr.Pubkey
this.AMRIDs.push(amr.ID);
this.StartPrice += amr.Merits;
this.EndPrice += amr.Merits;
this.Merits += amr.Merits;
}
Pop(amr: RocketAMR) {
if (this.AMRIDs.includes(amr.ID) && amr.Pubkey == this.Owner) {
let n:string[] = []
for (let id of this.AMRIDs) {
if (id != amr.ID) {
n.push(id)
}
}
this.AMRIDs = n;
//todo: allow user to set start/end price
this.StartPrice -= amr.Merits
this.EndPrice -= amr.Merits
this.Merits -= amr.Merits
}
}
Validate(): boolean {
let valid = true;
if (
this.Owner?.length != 64 ||
!this.StartPrice ||
!this.EndPrice ||
!validate(this.RxAddress) ||
this.RocketP.length != 64
) {
valid = false;
}
for (let id of this.AMRIDs) {
if (id.length != 64) {
valid = false;
}
}
return valid;
}
ValidateAgainstRocket(rocket: Rocket): boolean {
let valid = true;
for (let id of this.AMRIDs) {
let rocketAMR = rocket.ApprovedMeritRequests().get(id);
if (!rocketAMR || (rocketAMR && rocketAMR.Pubkey != this.Owner) || rocketAMR.LeadTime > 0) {
valid = false;
}
}
//todo: check if this AMR is already listed for sale
return valid;
}
constructor(rocket?: NDKEvent, event?: NDKEvent, address?: string) {
this.AMRIDs = [];
this.Merits = 0
this.EndPrice = 0
this.StartPrice = 0
if (rocket && !event) {
this.RxAddress = address?address:""
this.RocketD = rocket.dTag!;
this.RocketP = rocket.author.pubkey;
}
if (event && !rocket) {
this.Event = event
for (let id of event.getMatchingTags("request")) {
if (id && id.length == 2 && id[1].length == 64) {
this.AMRIDs.push(id[1])
}
}
this.Owner = event.author.pubkey;
let price = event.tagValue('price');
if (price) {
let _start = price.split(':')[0];
let _end = price.split(':')[1];
let start = parseInt(_start, 10);
let end = parseInt(_end, 10);
this.StartPrice = start;
this.EndPrice = end;
}
let address = event.tagValue('onchain');
if (address) {
if (validate(address)) {
this.RxAddress = address;
}
}
let _rocket = event.tagValue('a');
if (_rocket) {
if (_rocket.split(':').length == 3) {
this.RocketP = _rocket.split(':')[1];
this.RocketD = _rocket.split(':')[2];
}
}
}
}
}

View File

@@ -0,0 +1,54 @@
<script lang="ts">
import { ndk } from '@/ndk';
import { NDKEvent } from '@nostr-dev-kit/ndk';
import type { ExtendedBaseType, NDKEventStore } from '@nostr-dev-kit/ndk-svelte';
import { onDestroy } from 'svelte';
import { derived, type Readable } from 'svelte/store';
export let dTag: string; // = $page.url.searchParams.get('d');
export let pubkey: string; // = $page.url.searchParams.get('p');
let rocketEvents: NDKEventStore<NDKEvent> | undefined;
export let latestRocketEvent: Readable<ExtendedBaseType<NDKEvent> | undefined>;
let candidateProducts: Readable<ExtendedBaseType<NDKEvent>[]>;
onDestroy(() => {
rocketEvents?.unsubscribe();
});
rocketEvents = $ndk.storeSubscribe(
[
{ '#d': [dTag], authors: [pubkey], kinds: [31108 as number] },
{ '#a': [`31108:${pubkey}:${dTag}`] }
],
{ subId: dTag }
);
$: {
if (rocketEvents && !latestRocketEvent) {
latestRocketEvent = derived(rocketEvents, ($events) => {
let sorted = $events.filter((e) => {
return e.kind == 31108;
});
sorted = sorted.toSorted((a, b) => {
return a.created_at - b.created_at;
});
return sorted[0];
});
if ($latestRocketEvent && !candidateProducts) {
candidateProducts = derived(rocketEvents, ($events) => {
return $events.filter((e) => {
for (let p of $latestRocketEvent.getMatchingTags('product')) {
if (p[1].includes(e.id)) {
return false;
}
}
return e.kind == 1908;
});
});
}
}
}
</script>

View File

@@ -0,0 +1,166 @@
<script lang="ts">
import { currentUser } from '@/stores/session';
import Login from '../../components/Login.svelte';
import { ndk } from '@/ndk';
import { onDestroy } from 'svelte';
import { derived } from 'svelte/store';
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { type RocketAMR, AMRAuction, Rocket } from '@/event_helpers/rockets';
import * as Table from '@/components/ui/table';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { Checkbox } from '@/components/ui/checkbox';
import Button from '@/components/ui/button/button.svelte';
import { Input } from '@/components/ui/input';
let rocketEvents = $ndk.storeSubscribe([{ kinds: [31108 as number] }], { subId: 'all_rockets' });
onDestroy(() => {
rocketEvents?.unsubscribe();
});
let rockets = derived(rocketEvents, ($rocketEvents) => {
let m = new Map<string, NDKEvent>();
for (let e of $rocketEvents) {
let existing = m.get(e.pubkey + e.dTag);
if (!existing) {
existing = e;
}
if (e.created_at > existing) {
existing = e;
}
m.set(e.pubkey + e.dTag, e);
}
return Array.from(m, ([_, e]) => e);
});
let myMeritRequests = derived([currentUser, rockets], ([$currentUser, $rockets]) => {
let merits = new Map<Rocket, RocketAMR[]>();
if ($currentUser) {
for (let r of $rockets) {
let parsedRocket = new Rocket(r);
let _merits: RocketAMR[] = [];
for (let [_, amr] of parsedRocket.ApprovedMeritRequests()) {
if (amr.Pubkey == $currentUser.pubkey) {
_merits.push(amr);
}
}
merits.set(parsedRocket, _merits);
}
}
return merits;
});
let selected = new Map<string, AMRAuction>();
function toggleSelected(amr: RocketAMR, rocket: Rocket) {
if (selected.has(amr.ID)) {
selected.delete(amr.ID);
} else {
selected.set(amr.ID, new AMRAuction(amr, rocket.Event));
}
selected = selected;
}
function getTotal(list:Map<string, AMRAuction>):number {
let total = 0
for (let [_, amr] of list) {
total += amr.Merits
}
return total
}
function getAllForRocket(rocket:Rocket, selected:Map<string, AMRAuction>):Map<string, AMRAuction> {
let thisRocket = new Map<string, AMRAuction>()
for (let [_, amr] of selected) {
if (amr.RocketD == rocket.Name() && amr.RocketP == rocket.Event.author.pubkey) {
thisRocket.set(amr.AMRID, amr)
}
}
return thisRocket
}
function publish(sales:Map<string, AMRAuction>, address: string, rocket:Rocket) {
if (!$ndk.signer) {
throw new Error('no ndk signer found');
}
let e = new NDKEvent($ndk);
let author = $currentUser;
if (!author) {
throw new Error('no current user');
}
e.author = author;
e.kind = 1412;
e.created_at = Math.floor(new Date().getTime() / 1000);
for (let [_, amr] of sales) {
e.tags.push(["request", amr.AMRID])
}
e.tags.push(['a', `31108:${rocket.Event.pubkey}:${rocket.Event.dTag}`]);
//todo: allow user to set start and end auction price
let total = getTotal(getAllForRocket(rocket, sales)).toString()
e.tags.push(['price', total + ":" + total]);
e.tags.push(["onchain", address])
e.publish().then((x) => {
console.log(x, e);
//goto(`${base}/rockets/${getRocketURL(e)}`);
});
}
let bitcoinAddress:string = ""
</script>
<h1 class=" m-2 text-nowrap text-center text-xl">Trade your Merits for Sats</h1>
{#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 class="w-[10px]">Elegible</Table.Head>
<Table.Head>Merits</Table.Head>
<Table.Head class="text-right">Sats (approx)</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each amr as a, id (a.ID)}
<Table.Row class={selected.has(a.ID) ? 'bg-orange-500 hover:bg-orange-500' : ''}>
<Table.Cell
><Checkbox
id={a.ID}
checked={selected.has(a.ID)}
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.LeadTime == 0}</Table.Cell>
<Table.Cell>{a.Merits}</Table.Cell>
<Table.Cell class="text-right">{a.Merits}</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
{#if selected.size > 0}
<div class="m-2 flex">You are selling {getTotal(getAllForRocket(rocket, selected))} Merits</div>
<div class="m-2 flex">
<Input bind:value={bitcoinAddress} type="text" placeholder="Bitcoin Address for Payment" class="m-1 max-w-xs" />
<Button on:click={()=>{publish(getAllForRocket(rocket, selected), bitcoinAddress, rocket)}} class="m-1">Sell Now</Button>
</div>
{/if}
{/if}
{/each}
{:else}<Login />{/if}

View File

@@ -1,70 +1,31 @@
<script lang="ts">
import { page } from '$app/stores';
import { ndk } from '@/ndk';
import { NDKEvent } from '@nostr-dev-kit/ndk';
import type { ExtendedBaseType, NDKEventStore } from '@nostr-dev-kit/ndk-svelte';
import { onDestroy } from 'svelte';
import { derived, type Readable } from 'svelte/store';
import type { ExtendedBaseType } from '@nostr-dev-kit/ndk-svelte';
import { type Readable } from 'svelte/store';
import Heading from '../../../components/Heading.svelte';
import RocketDashboard from '../../../components/RocketDashboard.svelte';
import Rocket from '../../../route_helpers/Rocket.svelte';
//flow if we only have a d-tag: fetch all 31108's with this d-tag, sort by WoT, put Nostrocket Name Service one at the top. Dedupe same rocket (same state, shadows) from multiple users, just show them all as everyone agreeing.
//second pass: fetch ignition event for each, rebuild current state and validate all proofs, compute votepower and display only the states with > 50%.
let rIgnitionOrActual = $page.params.ignition;
let rName = $page.url.searchParams.get('d');
let rPubkey = $page.url.searchParams.get('p');
let dTag = $page.url.searchParams.get('d');
let pubkey = $page.url.searchParams.get('p');
let rocketEvents: NDKEventStore<NDKEvent> | undefined;
let latestRocketEvent: Readable<ExtendedBaseType<NDKEvent> | undefined>;
let candidateProducts: Readable<ExtendedBaseType<NDKEvent>[]>;
onDestroy(() => {
rocketEvents?.unsubscribe();
});
//if we don't have a d/p tags we just render the event provided
//todo: to find the latest by name alone we should use a new route and redirect to this page once we've got the relevant data
if (!rName && !rPubkey && rIgnitionOrActual.length == 64) {
if (!dTag && !pubkey && rIgnitionOrActual.length == 64) {
//this is the actual event ID the user wants to view so fetch the single event and render
//warn user that this information is probably out of date and let them reroute to get the latest
}
if (rName && rPubkey) {
if (dTag && pubkey) {
//the user wants the latest valid state of this rocket
rocketEvents = $ndk.storeSubscribe(
[
{ '#d': [rName], authors: [rPubkey], kinds: [31108 as number] },
{ '#a': [`31108:${rPubkey}:${rName}`] }
],
{ subId: rName }
);
}
$: {
if (rocketEvents && !latestRocketEvent) {
latestRocketEvent = derived(rocketEvents, ($events) => {
let sorted = $events.filter((e) => {
return e.kind == 31108;
});
sorted = sorted.toSorted((a, b) => {
return a.created_at - b.created_at;
});
return sorted[0];
});
if ($latestRocketEvent && !candidateProducts) {
candidateProducts = derived(rocketEvents, ($events) => {
return $events.filter((e) => {
for (let p of $latestRocketEvent.getMatchingTags('product')) {
if (p[1].includes(e.id)) {
return false;
}
}
return e.kind == 1908;
});
});
}
}
}
//todo: check that this zap is not already included in the payment JSON for the product
@@ -75,12 +36,15 @@
//todo: handle shadow events (fetch the shadowed event and render it instead)
</script>
{#if dTag && pubkey}
<Rocket bind:latestRocketEvent {dTag} {pubkey} />
{/if}
{#if latestRocketEvent && $latestRocketEvent}
<RocketDashboard rocket={$latestRocketEvent} />
{:else}
<Heading title="Fetching events for the requested rocket" />
IGNITION: {rIgnitionOrActual} <br />
NAME: {rName} <br />
PUBKEY: {rPubkey} <br />
NAME: {dTag} <br />
PUBKEY: {pubkey} <br />
{/if}

View File

@@ -0,0 +1,186 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import Button from '@/components/ui/button/button.svelte';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
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';
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { onDestroy } from 'svelte';
import { derived } from 'svelte/store';
import Login from '../../components/Login.svelte';
import MeritAuctions from '../../stateupdaters/MeritAuctions.svelte';
let rocketEvents = $ndk.storeSubscribe([{ kinds: [31108 as number] }], { subId: 'all_rockets' });
onDestroy(() => {
rocketEvents?.unsubscribe();
});
let rockets = derived(rocketEvents, ($rocketEvents) => {
let m = new Map<string, NDKEvent>();
for (let e of $rocketEvents) {
let existing = m.get(e.pubkey + e.dTag);
if (!existing) {
existing = e;
}
if (e.created_at > existing) {
existing = e;
}
m.set(e.pubkey + e.dTag, e);
}
return Array.from(m, ([_, e]) => e);
});
let myMeritRequests = derived([currentUser, rockets], ([$currentUser, $rockets]) => {
let merits = new Map<Rocket, RocketAMR[]>();
if ($currentUser) {
for (let r of $rockets) {
let parsedRocket = new Rocket(r);
let _merits: RocketAMR[] = [];
for (let [_, amr] of parsedRocket.ApprovedMeritRequests()) {
if (amr.Pubkey == $currentUser.pubkey) {
_merits.push(amr);
}
}
merits.set(parsedRocket, _merits);
}
}
return merits;
});
let selected_amrs = new Map<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;
}
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
}
// function getTotal(auction:AMRAuction):number {
// let total = 0
// for (let [_, amr] of list) {
// total += amr.Merits
// }
// return total
// }
// function getAllForRocket(rocket:Rocket, selected:Map<string, AMRAuction>):Map<string, AMRAuction> {
// let thisRocket = new Map<string, AMRAuction>()
// for (let [_, amr] of selected) {
// if (amr.RocketD == rocket.Name() && amr.RocketP == rocket.Event.author.pubkey) {
// thisRocket.set(amr.AMRID, amr)
// }
// }
// return thisRocket
// }
function publish(amr:AMRAuction, rocket:Rocket) {
if (!$ndk.signer) {
throw new Error('no ndk signer found');
}
let author = $currentUser;
if (!author) {
throw new Error('no current user');
}
amr.RxAddress = bitcoinAddress
if (!amr.Validate()) {throw new Error("invalid")}
if (!amr.ValidateAgainstRocket(rocket)) {throw new Error("invalid against rocket")}
let e = amr.GenerateEvent()
e.ndk = $ndk
e.author = author;
e.publish().then((x) => {
console.log(x, e);
selected_amrs = new Map<string, AMRAuction>();
//goto(`${base}/rockets/${getRocketURL(e)}`);
});
}
let bitcoinAddress:string = ""
</script>
<h1 class=" m-2 text-nowrap text-center text-xl">Trade your Merits for Sats</h1>
{#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 class="w-[10px]">Elegible</Table.Head>
<Table.Head>Merits</Table.Head>
<Table.Head class="text-right">Sats (approx)</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each amr as a, id (a.ID)}
<Table.Row class={getSelectedStatus(rocket.Event.id, a.ID, selected_amrs) ? 'bg-orange-500 hover:bg-orange-500' : ''}>
<Table.Cell
><Checkbox
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.LeadTime == 0}</Table.Cell>
<Table.Cell>{a.Merits}</Table.Cell>
<Table.Cell class="text-right">{a.Merits}</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
{#if selected_amrs.get(rocket.Event.id)}
<div class="m-2 flex">You are selling {selected_amrs.get(rocket.Event.id)?.Merits} Merits</div>
<div class="m-2 flex">
<Input bind:value={bitcoinAddress} type="text" placeholder="Bitcoin Address for Payment" class="m-1 max-w-xs" />
<Button on:click={()=>{publish(selected_amrs.get(rocket.Event.id), rocket)}} class="m-1">Sell Now</Button>
</div>
{/if}
{/if}
{/each}
{:else}<Login />{/if}
<MeritAuctions />

View File

@@ -0,0 +1,43 @@
<script lang="ts">
import { AMRAuction, Rocket } from "@/event_helpers/rockets";
import { ndk } from "@/ndk";
import { onDestroy } from "svelte";
import { derived } from "svelte/store";
let saleEvents = $ndk.storeSubscribe([{ kinds: [1412 as number] }], { subId: 'all_sale_requests' });
onDestroy(() => {
saleEvents?.unsubscribe();
});
export let rockets:Set<Rocket>|undefined = undefined;
let validAuctionRequests = derived(saleEvents, ($saleEvents) =>{
let provisional = new Map<string, AMRAuction>()
let valid = new Map<string, AMRAuction>()
for (let e of $saleEvents) {
let a = new AMRAuction(undefined, e)
if (a.Validate()) {
provisional.set(a.Event.id, a)
}
}
if (rockets) {
for (let [_, a] of provisional) {
for (let r of rockets) {
if (r.Name() == a.RocketD && a.RocketP == r.Event.pubkey) {
if (a.ValidateAgainstRocket(r)) {
valid.set(a.Event.id, a)
}
}
}
}
} else {valid = provisional}
return valid
})
validAuctionRequests.subscribe(requests=>{
})
</script>
{#each $validAuctionRequests as [_, a]}<span on:click={()=>{console.log(a)}}>{a.Event.id}</span>{/each}