problem: bitcoin addresses not added to rocket

This commit is contained in:
gsovereignty
2024-08-03 17:20:28 +08:00
parent ee612fc36f
commit 0a1f94b377
11 changed files with 362 additions and 52 deletions

View File

@@ -0,0 +1,46 @@
<script lang="ts">
import * as Card from "@/components/ui/card";
import Heading from "./Heading.svelte";
import InputBitcoinAddress from "./InputBitcoinAddress.svelte";
import { Button } from "@/components/ui/button";
import { ndk } from "@/ndk";
import { currentUser } from "@/stores/session";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import validate from "bitcoin-address-validation";
let bitcoinAddress:string;
function publish(address:string) {
if (!$ndk.signer) {
throw new Error('no ndk signer found');
}
let author = $currentUser;
if (!author) {
throw new Error('no current user');
}
if (!validate(address)) {
throw new Error("invalid bitcoin address")
}
let event = new NDKEvent($ndk)
event.kind = 1413
event.tags.push(["onchain", address])
//todo: let user specify a rocket
console.log("todo: let user specify a rocket")
event.publish().then((x) => {
console.log(x);
}).catch(()=>{ console.log("failed to publish", event.rawEvent())});
}
</script>
<Heading title="Sponsor a Contributor" />
<Card.Root>
<Card.Header><Card.Title>Associate Bitcoin Address</Card.Title></Card.Header>
<Card.Content>
<div class="m-2 flex">
You must associate at least one Bitcoin address with your npub before you can pay a Contributor.
</div>
<div class="flex"><InputBitcoinAddress bind:bitcoinAddress /><Button on:click={()=>publish(bitcoinAddress)} class="mt-3 max-w-xs">Publish</Button></div>
</Card.Content>
</Card.Root>

View File

@@ -0,0 +1,67 @@
<script lang="ts">
import * as Card from '@/components/ui/card';
import * as Table from '@/components/ui/table';
import { Rocket } from '@/event_helpers/rockets';
import { ndk } from '@/ndk';
import { NDKKind } from '@nostr-dev-kit/ndk';
import { Avatar, Name } from '@nostr-dev-kit/ndk-svelte-components';
import { onDestroy } from 'svelte';
export let rocket: Rocket;
let _associationRequests = $ndk.storeSubscribe(
[{ '#a': [`31108:${rocket.Event.author.pubkey}:${rocket.Name()}`], kinds: [1413 as NDKKind] }],
{
subId: `${rocket.Name()}_bitcoin_associations`
}
);
onDestroy(() => {
_associationRequests?.unsubscribe();
});
</script>
<Card.Root class="sm:col-span-3">
<Card.Header class="px-7">
<Card.Title>Registered Bitcoin Addresses</Card.Title>
<Card.Description
>These people have registered a Bitcoin address and want to sponsor Contributors working on {rocket.Name()}</Card.Description
>
</Card.Header>
<Card.Content>
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>Sponsor</Table.Head>
<Table.Head class="hidden text-left md:table-cell">Address</Table.Head>
<Table.Head class="table-cell">Amount (Sats)</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each rocket.BitcoinAssociations() as [pubkey, ba], _ (pubkey)}
<Table.Row>
<Table.Cell>
<div class="flex flex-nowrap">
<Avatar
ndk={$ndk}
pubkey={pubkey}
class="h-10 w-10 flex-none rounded-full object-cover"
/>
<Name
ndk={$ndk}
pubkey={pubkey}
class="hidden max-w-32 truncate p-2 md:inline-block"
/>
</div>
</Table.Cell>
<Table.Cell class="hidden text-left md:table-cell">
{0}
</Table.Cell>
<Table.Cell class="table-cell">{ba.Address}</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</Card.Content>
</Card.Root>

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import { Input } from '@/components/ui/input';
import validate from 'bitcoin-address-validation';
export let bitcoinAddress: string;
$: bitcoinAddressInValid = true;
$: bitcoinAddressError = '';
$: if (bitcoinAddress) {
if (!validate(bitcoinAddress)) {
bitcoinAddressInValid = true;
bitcoinAddressError = 'Bitcoin address is invalid';
} else {
bitcoinAddressInValid = false;
bitcoinAddressError = '';
}
}
</script>
<div class="m-2 flex flex-col">
<div class="flex">
<Input
bind:value={bitcoinAddress}
type="text"
placeholder="Bitcoin Address for Payment"
class="m-1 max-w-xs"
/>
</div>
{#if bitcoinAddressError}
<div class="ml-4 p-0 text-sm text-red-500">{bitcoinAddressError}</div>
{/if}
</div>

View File

@@ -3,41 +3,40 @@
import { base } from '$app/paths';
import { Button } from '$lib/components/ui/button/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import { Name, Avatar } from '@nostr-dev-kit/ndk-svelte-components';
import { getMission, getRocketURL } from '@/helpers';
import type { NDKEvent } from '@nostr-dev-kit/ndk';
import { ChevronRight } from 'lucide-svelte';
import type { Rocket } from '@/event_helpers/rockets';
import { ndk } from '@/ndk';
import { Avatar, Name } from '@nostr-dev-kit/ndk-svelte-components';
import { ChevronRight } from 'lucide-svelte';
export let rocketEvent: NDKEvent;
export let rocket: Rocket;
//$page.url.searchParams.get("tab")
</script>
<Card.Root class="w-[350px]">
<Card.Header>
<Card.Title>{rocketEvent.getMatchingTags('d')[0][1]}</Card.Title>
<Card.Description>{getMission(rocketEvent)}</Card.Description>
<Card.Title>{rocket.Name()}</Card.Title>
<Card.Description>{rocket.Mission()}</Card.Description>
</Card.Header>
<Card.Content>
<div class="flex items-center gap-2">
<Avatar
ndk={$ndk}
pubkey={rocketEvent.pubkey}
pubkey={rocket.Event.pubkey}
class="h-5 w-5 flex-none rounded-full object-cover"
/>
<Name ndk={$ndk} pubkey={rocketEvent.pubkey} class="inline-block truncate" />
<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(rocketEvent.rawEvent());
console.log(rocket.Event.rawEvent());
}}
variant="outline">Print to Console</Button
>
<Button
on:click={() => {
goto(`${base}/rockets/${getRocketURL(rocketEvent)}`);
goto(`${base}/rockets/${rocket.URL()}`);
}}>View Full Rocket<ChevronRight class="h-4 w-4" /></Button
>
</Card.Footer>

View File

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

View File

@@ -5,10 +5,68 @@ import validate from 'bitcoin-address-validation';
import { BitcoinTipTag } from '@/stores/bitcoin';
export class Rocket {
UpsertBitcoinAssociation(association: BitcoinAssociation): NDKEvent {
let event: NDKEvent | undefined = undefined;
if (true) { //todo: check if exists
this.PrepareForUpdate();
event = new NDKEvent(this.Event.ndk, this.Event.rawEvent());
event.created_at = Math.floor(new Date().getTime() / 1000);
event.tags.push(["address", `${association.Pubkey}:${association.Address}`])
event.tags.push(['proof_full', JSON.stringify(association.Event.rawEvent())]);
updateIgnitionAndParentTag(event);
updateBitcoinTip(event);
}
return event;
}
BitcoinAssociations():Map<string, BitcoinAssociation> {
let a = new Map<string, BitcoinAssociation>()
for (let t of this.Event.getMatchingTags("address")) {
if (t.length == 2) {
let split = t[1].split(":")
if (split.length == 2) {
let ba = new BitcoinAssociation()
ba.Address = split[1]
ba.Pubkey = split[0]
if (ba.Validate()) {
a.set(ba.Pubkey, ba)
}
}
}
}
return a
}
Event: NDKEvent;
URL(): string {
let ignitionID = undefined;
if (
this.Event.getMatchingTags('ignition') &&
this.Event.getMatchingTags('ignition')[0] &&
this.Event.getMatchingTags('ignition')[0][1]
) {
ignitionID = this.Event.getMatchingTags('ignition')[0][1];
}
if (!ignitionID) {
ignitionID = this.Event.id;
}
let d = this.Event.getMatchingTags('d')[0][1];
let p = this.Event.pubkey;
return `${ignitionID}?d=${d}&p=${p}`;
}
Name(): string {
return this.Event.dTag!;
}
Mission(): string {
if (
this.Event.getMatchingTags('mission') &&
this.Event.getMatchingTags('mission')[0] &&
this.Event.getMatchingTags('mission')[0][1]
) {
return this.Event.getMatchingTags('mission')[0][1];
}
return '';
}
Products(): RocketProduct[] {
let _products: RocketProduct[] = [];
for (let p of this.Event.getMatchingTags('product')) {
@@ -118,7 +176,7 @@ export class Rocket {
event.tags.push(['merit', `${request.Pubkey}:${request.ID}:0:0:${request.Merits}`]);
event.tags.push(['proof_full', JSON.stringify(signedProof.rawEvent())]);
updateIgnitionAndParentTag(event);
updateBitcionTip(event);
updateBitcoinTip(event);
}
return event;
}
@@ -133,33 +191,32 @@ export class Rocket {
a.StartPrice = parseInt(items[2], 10);
a.EndPrice = parseInt(items[3], 10);
a.Merits = parseInt(items[4], 10);
let ids = items[5].match(/.{1,64}/g);
if (ids) {
for (let id of ids) {
a.AMRIDs.push(id);
}
}
let amrs = this.ApprovedMeritRequests()
let amrs = this.ApprovedMeritRequests();
let failed = false;
for (let id of a.AMRIDs) {
let amr = amrs.get(id)
let amr = amrs.get(id);
if (!amr) {
failed = true
failed = true;
} else {
if (!a.Owner) {
a.Owner = amr.Pubkey
a.Owner = amr.Pubkey;
} else if (a.Owner != amr.Pubkey) {
failed = true
failed = true;
}
}
}
if (!failed) {
auctions.push(a)
auctions.push(a);
} else {
throw new Error("this should not happen, bug!")
throw new Error('this should not happen, bug!');
}
}
}
}
@@ -214,7 +271,7 @@ export class Rocket {
]); //<merit request ID:start price:end price:start height:rx address>
event.tags.push(['proof_full', JSON.stringify(request.Event!.rawEvent())]);
updateIgnitionAndParentTag(event);
updateBitcionTip(event);
updateBitcoinTip(event);
}
if (invalid) {
event = undefined;
@@ -238,7 +295,7 @@ export class Rocket {
purchases
]);
updateIgnitionAndParentTag(event);
updateBitcionTip(event);
updateBitcoinTip(event);
return event;
}
UpdateMission(mission: string): NDKEvent {
@@ -248,7 +305,7 @@ export class Rocket {
event.removeTag('mission');
event.tags.push(['mission', mission]);
updateIgnitionAndParentTag(event);
updateBitcionTip(event);
updateBitcoinTip(event);
return event;
}
CurrentProducts(): Map<string, RocketProduct> {
@@ -335,7 +392,7 @@ function updateIgnitionAndParentTag(event: NDKEvent) {
event.tags.push(['parent', event.id]);
}
function updateBitcionTip(event: NDKEvent) {
function updateBitcoinTip(event: NDKEvent) {
let existingBitcoinTip = event.getMatchingTags('bitcoin');
let existing = [];
for (let t of event.tags) {
@@ -639,7 +696,7 @@ export class AMRAuction {
}
for (let pending of rocket.PendingAMRAuctions()) {
if (pending.AMRIDs.includes(id)) {
valid = false
valid = false;
}
}
}
@@ -693,3 +750,27 @@ export class AMRAuction {
}
}
}
export class BitcoinAssociation {
Pubkey: string;
Address: string | undefined;
Event: NDKEvent;
Validate(): boolean {
let valid = true;
if (this.Pubkey.length != 64) {
valid = false;
}
if ((this.Address && !validate(this.Address)) || !this.Address) {
valid = false;
}
return valid;
}
constructor(event?: NDKEvent) {
if (event) {
this.Pubkey = event.pubkey;
this.Address = event.tagValue('onchain');
this.Event = event;
}
}
}

View File

@@ -19,16 +19,7 @@ export function getRocketURL(e: NDKEvent): string {
return `${ignitionID}?d=${d}&p=${p}`;
}
export function getMission(rocketEvent: NDKEvent): string {
if (
rocketEvent.getMatchingTags('mission') &&
rocketEvent.getMatchingTags('mission')[0] &&
rocketEvent.getMatchingTags('mission')[0][1]
) {
return rocketEvent.getMatchingTags('mission')[0][1];
}
return '';
}
export function unixTimeNow() {
return Math.floor(new Date().getTime() / 1000);

View File

@@ -8,6 +8,7 @@
import { Avatar } from '@nostr-dev-kit/ndk-svelte-components';
import { onDestroy } from 'svelte';
import { derived } from 'svelte/store';
import AssociateBitcoinAddress from '../../components/AssociateBitcoinAddress.svelte';
import Login from '../../components/Login.svelte';
import MeritAuctions from '../../stateupdaters/MeritAuctions.svelte';
let rocketEvents = $ndk.storeSubscribe([{ kinds: [31108 as number] }], { subId: 'all_rockets' });
@@ -40,10 +41,21 @@
return merits;
});
let noAssociatedBitcoinAddress = derived([currentUser, pendingSales], ([$currentUser, $pendingSales])=>{
let show = false
if ($currentUser) {
for (let [r, _] of $pendingSales) {
if (!r.BitcoinAssociations().get($currentUser.pubkey)) {
show = true
}
}
}
return show
})
</script>
<h1 class=" m-2 text-nowrap text-center text-xl">Sponsor a Contributor</h1>
{#if $noAssociatedBitcoinAddress}<AssociateBitcoinAddress />{/if}
{#if $currentUser}
{#each $pendingSales as [rocket, amr]}

View File

@@ -12,6 +12,7 @@
import Login from '../../components/Login.svelte';
import CreateAMRAuction from '../../components/CreateAMRAuction.svelte';
import MeritAuctions from '../../stateupdaters/MeritAuctions.svelte';
import Heading from '../../components/Heading.svelte';
let rocketEvents = $ndk.storeSubscribe([{ kinds: [31108 as number] }], { subId: 'all_rockets' });
onDestroy(() => {
rocketEvents?.unsubscribe();
@@ -101,8 +102,7 @@
// return thisRocket
// }
</script>
<h1 class=" m-2 text-nowrap text-center text-xl">Trade your Merits for Sats</h1>
<Heading title="Trade your Merits for Sats" />
{#if $currentUser}
{#each $myMeritRequests as [rocket, amr]}
@@ -122,7 +122,7 @@
</Table.Header>
<Table.Body>
{#each rocket.PendingAMRAuctions().filter(r=>{return Boolean(r.Owner == $currentUser.pubkey)}) as p}
<Table.Row class="bg-purple-500">
<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>

View File

@@ -0,0 +1,63 @@
<script lang="ts">
import { BitcoinAssociation, Rocket } from '@/event_helpers/rockets';
import { ndk } from '@/ndk';
import { currentUser } from '@/stores/session';
import { onDestroy } from 'svelte';
import { derived, type Readable } from 'svelte/store';
let associations = $ndk.storeSubscribe([{ kinds: [1413 as number] }], {
subId: 'all_association_requests'
});
onDestroy(() => {
associations?.unsubscribe();
});
export let rockets: Readable<Rocket[]>;
let validAssociationRequests = derived([associations, rockets], ([$associationEvents, $rockets]) => {
let valid = new Map<string, BitcoinAssociation>();
for (let e of $associationEvents) {
let a = new BitcoinAssociation(e)
if (a.Validate()) {
valid.set(a.Event.id, a);
}
}
return valid
});
let validAgainstMyRocket = derived([currentUser, validAssociationRequests, rockets], ([$currentUser, $associationRequests, $rockets]) => {
let valid:{rocket:Rocket, association:BitcoinAssociation}[] = []
if ($currentUser) {
if ($rockets.length > 0) {
for (let [_, a] of $associationRequests) {
for (let r of $rockets) {
if (r.VotePowerForPubkey($currentUser.pubkey)) {
if (!r.BitcoinAssociations().get(a.Pubkey)) { //todo: get current list of Bitcoin associations, if (this is not included)
valid.push({rocket:r, association:a})
}
}
}
}
}
}
return valid;
})
validAgainstMyRocket.subscribe((requests) => {
if ($rockets && $rockets.length > 0 && currentUser && $currentUser) {
for (let {rocket,association} of requests) {
if (rocket.VotePowerForPubkey($currentUser.pubkey)) {
console.log("todo: check which rocket user has specified")
let e = rocket.UpsertBitcoinAssociation(association)
if (e) {
e.ndk = $ndk
e.publish().then(x=>{
console.log(x, e)
})
}
}
}
}
});
</script>

View File

@@ -1,27 +1,44 @@
<script lang="ts">
import type { NDKEventStore } from '@nostr-dev-kit/ndk-svelte';
import Heading from '../../components/Heading.svelte';
import type { NDKEvent } from '@nostr-dev-kit/ndk';
import { onDestroy } from 'svelte';
import { Rocket } from '@/event_helpers/rockets';
import { ndk } from '@/ndk';
import Todo from '../../components/Todo.svelte';
import { onDestroy } from 'svelte';
import { derived } from 'svelte/store';
import Heading from '../../components/Heading.svelte';
import RocketCard from '../../components/RocketCard.svelte';
import Todo from '../../components/Todo.svelte';
import AssociateBitcoinAddress from '../../stateupdaters/AssociateBitcoinAddress.svelte';
let _rockets = $ndk.storeSubscribe([{ kinds: [31108 as number] }], { subId: 'rockets' });
let entries: NDKEventStore<NDKEvent> | undefined;
onDestroy(() => {
entries?.unsubscribe();
_rockets?.unsubscribe();
});
entries = $ndk.storeSubscribe([{ kinds: [31108 as number] }], { subId: 'rockets' });
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) {
console.log(e)
existing = new Rocket(e)
}
if (existing.Event.created_at <= e.created_at) {
_r.set(`${e.pubkey}${e.dTag}`, existing)
}
}
return Array.from(_r, ([_, r])=>{return r})
})
//todo: until we have namerocket working, just manually dedupe rockets based on my pubkey
//todo: write a recognizer/validator for rocket events
</script>
<Heading title="Rockets" />
{#if entries && $entries}
{#if rockets && $rockets}
<AssociateBitcoinAddress {rockets} />
<Todo text={['render these in a nicer way, maybe a grid or something']} />
{#each $entries as rocketEvent (rocketEvent.id)}
<RocketCard {rocketEvent} />
{#each $rockets as rocket (`${rocket.Event.pubkey}${rocket.Name()}`)}
<RocketCard {rocket} />
{/each}
{/if}