problem: merit auctions sometimes appear valid when they are already sold

This commit is contained in:
gsovereignty
2024-08-05 21:49:45 +08:00
parent 8309139b85
commit b8e0aa9b9b
5 changed files with 154 additions and 89 deletions

View File

@@ -2,7 +2,7 @@ import { NDKEvent, type NDKTag } from '@nostr-dev-kit/ndk';
import { MapOfVotes, MeritRequest, Votes } from './merits'; import { MapOfVotes, MeritRequest, Votes } from './merits';
import { getAuthorizedZapper } from '@/helpers'; import { getAuthorizedZapper } from '@/helpers';
import validate from 'bitcoin-address-validation'; import validate from 'bitcoin-address-validation';
import { BitcoinTipTag, txs } from '@/stores/bitcoin'; import { BitcoinTipTag, bitcoinTip, txs } from '@/stores/bitcoin';
export class Rocket { export class Rocket {
UpsertBitcoinAssociation(association: BitcoinAssociation): NDKEvent { UpsertBitcoinAssociation(association: BitcoinAssociation): NDKEvent {
@@ -619,6 +619,8 @@ export async function ValidateZapPublisher(rocket: NDKEvent, zap: NDKEvent): Pro
}); });
} }
type AMRAuctionStatus = 'PENDING' | 'OPEN' | 'TX DETECTED' | 'SOLD & PENDING RATIFICATION' | 'CHECKING MEMPOOL';
export class AMRAuction { export class AMRAuction {
AMRIDs: string[]; AMRIDs: string[];
Owner: string | undefined; Owner: string | undefined;
@@ -630,32 +632,50 @@ export class AMRAuction {
Merits: number; Merits: number;
Event: NDKEvent; Event: NDKEvent;
Extra: { rocket: Rocket }; Extra: { rocket: Rocket };
Status(rocket: Rocket, transactions?: txs): string { Status(
let status = 'PENDING'; rocket: Rocket,
bitcoinTip: number,
transactions?: txs
): AMRAuctionStatus {
let status:AMRAuctionStatus = "PENDING"
if (transactions && transactions.Address != this.RxAddress) { if (transactions && transactions.Address != this.RxAddress) {
throw new Error('invalid address'); throw new Error('invalid address');
} }
for (let pending of rocket.PendingAMRAuctions()) {
this.AMRIDs.sort()
pending.AMRIDs.sort()
if (
pending.Owner == this.Owner &&
pending.Merits == this.Merits &&
pending.RxAddress == this.RxAddress &&
pending.AMRIDs[0] == this.AMRIDs[0] //todo: check whole array
) {
status = "OPEN"
}
}
if (transactions) { if (transactions) {
status = 'CHECKING MEMPOOL';
for (let [t, txo] of transactions.From()) { for (let [t, txo] of transactions.From()) {
//todo: implement pricing based on block height //todo: implement pricing based on block height
if (txo.Amount == this.EndPrice && txo.To == this.RxAddress) { if (txo.Amount == this.EndPrice && txo.To == this.RxAddress) {
status = "SOLD" if (txo.Height > 0 && txo.Height < bitcoinTip) {
status = 'SOLD & PENDING RATIFICATION';
} else {
status = 'TX DETECTED';
}
} }
} }
let found = false;
for (let pending of rocket.PendingAMRAuctions()) {
this.AMRIDs.sort();
pending.AMRIDs.sort();
if (
pending.Owner == this.Owner &&
pending.Merits == this.Merits &&
pending.RxAddress == this.RxAddress &&
pending.AMRIDs[0] == this.AMRIDs[0] //todo: check whole array
) {
found = true
if (status == "CHECKING MEMPOOL") {
if (
Math.floor(new Date().getTime() / 1000) < transactions.LastUpdate + 60000
) {
status = 'OPEN';
}
}
}
}
} }
return status return status;
} }
GenerateEvent(): NDKEvent { GenerateEvent(): NDKEvent {
let e = new NDKEvent(); let e = new NDKEvent();

View File

@@ -19,17 +19,45 @@ export function BitcoinTipTag(): string[] {
} }
export async function getBitcoinTip() { export async function getBitcoinTip() {
getBitcoinTipBlockstream();
getBitcoinTipMempool();
}
async function getBitcoinTipBlockstream() {
try { try {
const response = await fetch('https://blockstream.info/api/blocks/tip'); const response = await fetch('https://blockstream.info/api/blocks/tip');
const _json = await response.json(); const _json = await response.json();
if (_json[0]) { if (_json[0]) {
let r: BitcoinTip = { let r: BitcoinTip = {
height: _json[0].height, height: _json[0].height,
hash: _json[0].id hash: _json[0].id
}; };
bitcoinTip.set(r); if (r.hash && r.height) {
return r; bitcoinTip.set(r);
}} catch { return r;
}
}
} catch {
return null;
}
return null;
}
async function getBitcoinTipMempool() {
try {
const response = await fetch('https://mempool.space/api/blocks/tip');
const _json = await response.json();
if (_json[0]) {
let r: BitcoinTip = {
height: _json[0].height,
hash: _json[0].id
};
if (r.hash && r.height) {
bitcoinTip.set(r);
return r;
}
}
} catch {
return null; return null;
} }
return null; return null;
@@ -66,7 +94,7 @@ export async function getBalance(address: string): Promise<number> {
}); });
} }
export async function getIncomingTransactions(address: string):Promise<JSON> { export async function getIncomingTransactions(address: string): Promise<JSON> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!validate(address)) { if (!validate(address)) {
reject('invalid address'); reject('invalid address');
@@ -80,7 +108,7 @@ export async function getIncomingTransactions(address: string):Promise<JSON> {
response response
.json() .json()
.then((j) => { .then((j) => {
resolve(j) resolve(j);
}) })
.catch((x) => reject(x)); .catch((x) => reject(x));
} }
@@ -98,40 +126,44 @@ export async function getIncomingTransactions(address: string):Promise<JSON> {
export class txs { export class txs {
Address: string; Address: string;
LastUpdate: number; LastUpdate: number;
LastAttempt: number;
Data: JSON; Data: JSON;
From():Map<string, txo> { From(): Map<string, txo> {
let possibles = new Map<string, txo>() let possibles = new Map<string, txo>();
for (let tx of this.Data) { for (let tx of this.Data) {
let amount = 0 let amount = 0;
let height = tx.status.block_height; let height = tx.status.block_height ? tx.status.block_height : 0;
let txid = tx.txid; let txid = tx.txid;
for (let vout of tx.vout) { for (let vout of tx.vout) {
let address = vout.scriptpubkey_address let address = vout.scriptpubkey_address;
if (address && address.trim() == this.Address) { if (address && address.trim() == this.Address) {
let value = vout.value let value = vout.value;
if (value) { if (value) {
amount += parseInt(value, 10) amount += parseInt(value, 10);
} }
} }
} }
for (let vin of tx.vin) { for (let vin of tx.vin) {
let address = vin.prevout.scriptpubkey_address let address = vin.prevout.scriptpubkey_address;
if (address && validate(address)) { if (address && validate(address)) {
let t = new txo() let t = new txo();
t.Amount = amount t.Amount = amount;
t.Height = height t.Height = height;
t.From = address t.From = address;
t.To = this.Address t.To = this.Address;
t.ID = txid t.ID = txid;
possibles.set(address, t) possibles.set(address, t);
} else {
console.log(156, vin)
} }
} }
} }
return possibles return possibles;
} }
constructor(address: string) { constructor(address: string) {
this.Address = address.trim(); this.Address = address.trim();
this.LastUpdate = 0; this.LastUpdate = 0;
this.LastAttempt = 0;
this.Data = JSON.parse('[]'); this.Data = JSON.parse('[]');
} }
} }
@@ -142,7 +174,5 @@ export class txo {
To: string; To: string;
Amount: number; Amount: number;
Height: number; Height: number;
constructor() { constructor() {}
}
}
}

View File

@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { ndk } from '@/ndk';
import { getBitcoinTip } from '@/stores/bitcoin';
import { currentUser, prepareUserSession } from '@/stores/session';
import type { NDKUser } from '@nostr-dev-kit/ndk';
import { ModeWatcher } from 'mode-watcher'; import { ModeWatcher } from 'mode-watcher';
import { onMount } from 'svelte';
import '../app.css'; import '../app.css';
import SidePanelLayout from '../layouts/SidePanelLayout.svelte'; import SidePanelLayout from '../layouts/SidePanelLayout.svelte';
import { ndk } from '@/ndk';
import type { NDKUser } from '@nostr-dev-kit/ndk';
import { currentUser, prepareUserSession } from '@/stores/session';
import { unixTimeNow } from '@/helpers';
import { getBitcoinTip } from '@/stores/bitcoin';
let sessionStarted = false; let sessionStarted = false;
let connected = false; let connected = false;
@@ -27,17 +27,11 @@
sessionStarted = true; sessionStarted = true;
} }
let lastRequestTime = 0; onMount(()=>{getBitcoinTip();})
$: { setInterval(function () {
if (unixTimeNow() > lastRequestTime + 30000) { getBitcoinTip();
getBitcoinTip().then((x) => { }, 2* 60 * 1000);
if (x) {
lastRequestTime = unixTimeNow();
}
});
}
}
</script> </script>
<ModeWatcher defaultMode="dark" /> <ModeWatcher defaultMode="dark" />

View File

@@ -3,14 +3,14 @@
import * as Table from '@/components/ui/table'; import * as Table from '@/components/ui/table';
import { AMRAuction, Rocket } from '@/event_helpers/rockets'; import { AMRAuction, Rocket } from '@/event_helpers/rockets';
import { ndk } from '@/ndk'; import { ndk } from '@/ndk';
import { getIncomingTransactions, txs } from '@/stores/bitcoin'; import { bitcoinTip, getIncomingTransactions, txs } from '@/stores/bitcoin';
import { currentUser } from '@/stores/session'; import { currentUser } from '@/stores/session';
import { NDKEvent } from '@nostr-dev-kit/ndk'; import { NDKEvent } from '@nostr-dev-kit/ndk';
import { Avatar } from '@nostr-dev-kit/ndk-svelte-components'; import { Avatar } from '@nostr-dev-kit/ndk-svelte-components';
import validate from 'bitcoin-address-validation';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { derived } from 'svelte/store'; import { derived } from 'svelte/store';
import AssociateBitcoinAddress from '../../components/AssociateBitcoinAddress.svelte'; import AssociateBitcoinAddress from '../../components/AssociateBitcoinAddress.svelte';
import Heading from '../../components/Heading.svelte';
import Login from '../../components/Login.svelte'; import Login from '../../components/Login.svelte';
import MeritAuctions from '../../stateupdaters/MeritAuctions.svelte'; import MeritAuctions from '../../stateupdaters/MeritAuctions.svelte';
let rocketEvents = $ndk.storeSubscribe([{ kinds: [31108 as number] }], { subId: 'all_rockets' }); let rocketEvents = $ndk.storeSubscribe([{ kinds: [31108 as number] }], { subId: 'all_rockets' });
@@ -44,9 +44,6 @@
}); });
let _transactions = new Map<string, txs>(); let _transactions = new Map<string, txs>();
let transactions = derived(pendingSales, ($pendingSales) => { let transactions = derived(pendingSales, ($pendingSales) => {
for (let [r, s] of $pendingSales) { for (let [r, s] of $pendingSales) {
for (let amr of s) { for (let amr of s) {
@@ -54,32 +51,59 @@
_transactions.set(amr.RxAddress, new txs(amr.RxAddress)); _transactions.set(amr.RxAddress, new txs(amr.RxAddress));
} }
let existing = _transactions.get(amr.RxAddress)!; let existing = _transactions.get(amr.RxAddress)!;
if (Math.floor(new Date().getTime() / 1000) > existing.LastUpdate + 10000) { if (
existing.LastUpdate = Math.floor(new Date().getTime() / 1000); Math.floor(new Date().getTime() / 1000) > existing.LastAttempt + 10000
) {
existing.LastAttempt = Math.floor(new Date().getTime() / 1000);
getIncomingTransactions(amr.RxAddress).then((result) => { getIncomingTransactions(amr.RxAddress).then((result) => {
if (result) {
existing.LastUpdate = Math.floor(new Date().getTime() / 1000);
}
if (result.length > 0) { if (result.length > 0) {
existing.Data = result; existing.Data = result;
_transactions.set(amr.RxAddress, existing); _transactions.set(amr.RxAddress, existing);
_transactions = _transactions; _transactions = _transactions;
} }
}); }).catch(c=>{console.log(c)});
} }
} }
} }
return _transactions; return _transactions;
}); });
transactions.subscribe((t) => { let soldButNotInState = derived(
//console.log(82, t) [pendingSales, transactions, bitcoinTip, currentUser],
([$pendingSales, $transactions, $bitcoinTip, $currentUser]) => {
if ($currentUser) {
for (let [r, p] of $pendingSales) {
if (r.VotePowerForPubkey($currentUser.pubkey) > 0) {
for (let ps of p) {
if (
ps.Status(r, $bitcoinTip.height, $transactions.get(ps.RxAddress)) ==
'SOLD & PENDING RATIFICATION'
) {
}
}
}
}
}
}
);
soldButNotInState.subscribe((t) => {
if (t) console.log(t);
}); });
transactions.subscribe((t) => {});
let noAssociatedBitcoinAddress = derived( let noAssociatedBitcoinAddress = derived(
[currentUser, pendingSales], [currentUser, pendingSales],
([$currentUser, $pendingSales]) => { ([$currentUser, $pendingSales]) => {
let show = false; let show = false;
if ($currentUser) { if ($currentUser) {
for (let [r, _] of $pendingSales) { for (let [r, a] of $pendingSales) {
if (!r.BitcoinAssociations().get($currentUser.pubkey)) { if (a.length > 0 && !r.BitcoinAssociations().get($currentUser.pubkey)) {
console.log($currentUser.pubkey, r.Name());
show = true; show = true;
} }
} }
@@ -94,14 +118,7 @@
{#if $currentUser} {#if $currentUser}
{#each $pendingSales as [rocket, amr]} {#each $pendingSales as [rocket, amr]}
{#if amr.length > 0} {#if amr.length > 0}
<h1 <Heading title={`ROCKET: ${rocket.Name()}`} />
on:click={() => {
console.log(rocket.Event.rawEvent(), rocket.PendingAMRAuctions());
}}
>
ROCKET: {rocket.Name()}
</h1>
<Table.Root> <Table.Root>
<Table.Header> <Table.Header>
<Table.Row> <Table.Row>
@@ -129,13 +146,19 @@
> >
<Table.Cell>{p.Merits}</Table.Cell> <Table.Cell>{p.Merits}</Table.Cell>
<Table.Cell class="text-right">{p.Merits}</Table.Cell> <Table.Cell class="text-right">{p.Merits}</Table.Cell>
<Table.Cell>{p.Status(rocket, _transactions.get(p.RxAddress))}</Table.Cell> <Table.Cell
>{p.Status(rocket, $bitcoinTip.height, $transactions.get(p.RxAddress))}</Table.Cell
>
<Table.Cell <Table.Cell
on:click={() => { on:click={() => {
console.log(_transactions.get(p.RxAddress)?.From()); console.log($transactions.get(p.RxAddress)?.From());
}}>{p.RxAddress}</Table.Cell }}>{p.RxAddress}</Table.Cell
> >
<Table.Cell>{#if p.Status(rocket, _transactions.get(p.RxAddress)) == "OPEN"}<Button>BUY NOW</Button>{/if}</Table.Cell> <Table.Cell
>{#if p.Status(rocket, $bitcoinTip.height, $transactions.get(p.RxAddress)) == 'OPEN'}<Button
>BUY NOW</Button
>{/if}</Table.Cell
>
</Table.Row> </Table.Row>
{/each} {/each}
</Table.Body> </Table.Body>

View File

@@ -57,5 +57,3 @@
//todo: validate and publish rocket updates //todo: validate and publish rocket updates
}); });
</script> </script>
<!-- {#each $validAuctionRequests as [_, a]}<span on:click={()=>{console.log(a)}}>{a.Event.id}</span>{/each} -->