mirror of
https://github.com/aljazceru/hypergolic.git
synced 2025-12-19 06:24:20 +01:00
problem: merit auctions sometimes appear valid when they are already sold
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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() {}
|
||||||
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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} -->
|
|
||||||
|
|||||||
Reference in New Issue
Block a user