- You must associate at least one Bitcoin address with your npub before you can pay a Contributor. Merit purchases from this address will be associated with your pubkey.
-
-
-
-
\ No newline at end of file
+
+ Merit purchases must be conducted with a Bitcoin address that is associated with your pubkey,
+ otherwise you will not recieve the Merits upon payment.
+
+ {#if $associatedAddresses.size == 0}You do not have any registered addresses{:else}
+ Your registered addresses:
+
@@ -71,7 +78,6 @@
{ba.Balance.toLocaleString()}
{ba.Address}
-
{/each}
diff --git a/src/components/BuyAMR.svelte b/src/components/BuyAMR.svelte
new file mode 100644
index 0000000..fbd9fcb
--- /dev/null
+++ b/src/components/BuyAMR.svelte
@@ -0,0 +1,71 @@
+
+
+
+ Buy Now
+
+ {#if !currentUser}
+
+
+ Heads up!
+ You need a nostr signing extension to use Nostrocket!
+
+ {:else}
+
+ Buy Merits from
+
+
+ To buy these merits you MUST send {auction.Merits / 100000000} BTC from one of your registered
+ addresses to {auction.RxAddress}.
+
+
+ Once the transaction has 2 confirmations the Merits will automatically be transferred to
+ your npub.
+
+
+ Todo: ask user to publish an event before making transaction so that multiple people don't
+ pay for the same Merits.
+
+
+ {/if}
+
+
diff --git a/src/components/NotifyMe.svelte b/src/components/NotifyMe.svelte
index 328e26e..9243b79 100644
--- a/src/components/NotifyMe.svelte
+++ b/src/components/NotifyMe.svelte
@@ -84,22 +84,25 @@
variant="nostr"
class="flex h-8 shrink-0 items-center justify-center rounded-sm"
>
- Tell me via DM when there are updates
+ Nostrocket is totally not ready yet but whatever
Subscribe for Updates
- Receive notifications about Nostrocket updates via Nostr DM or email.
+ Subscribe now and we'll ping you when there are new releases/features
{#if $currentUser}
-
+
{:else}
{/if}
+ If you don't use nostr, you can subscribe to updates with an email address instead
@@ -108,7 +111,9 @@
{emailError}
{/if}
-
+
diff --git a/src/lib/event_helpers/merits.ts b/src/lib/event_helpers/merits.ts
index 8ae6dcb..8f1266d 100644
--- a/src/lib/event_helpers/merits.ts
+++ b/src/lib/event_helpers/merits.ts
@@ -69,7 +69,7 @@ export class MeritRequest {
}
constructor(request: NDKEvent | string) {
if (typeof request == 'string') {
- console.log(69);
+ throw new Error('implement me');
} else {
this.LeadTime = 0;
this.LastLTUpdate = 0;
diff --git a/src/lib/event_helpers/rockets.ts b/src/lib/event_helpers/rockets.ts
index 97d0279..da575f1 100644
--- a/src/lib/event_helpers/rockets.ts
+++ b/src/lib/event_helpers/rockets.ts
@@ -1,21 +1,47 @@
-import { NDKEvent, type NDKTag } from '@nostr-dev-kit/ndk';
-import { MapOfVotes, MeritRequest, Votes } from './merits';
import { getAuthorizedZapper } from '@/helpers';
+import { BitcoinTipTag, txo, txs } from '@/stores/bitcoin';
+import { NDKEvent, type NDKTag } from '@nostr-dev-kit/ndk';
import validate from 'bitcoin-address-validation';
-import { BitcoinTipTag, bitcoinTip, txs } from '@/stores/bitcoin';
+import { sha256 } from 'js-sha256';
+import { MapOfVotes, MeritRequest, Votes } from './merits';
+import * as immutable from 'immutable';
+import { BloomFilter } from 'bloomfilter';
export class Rocket {
- UpsertBitcoinAssociation(association: BitcoinAssociation): NDKEvent {
+ Event: NDKEvent;
+ private Bloom(): BloomFilter {
+ let b = new BloomFilter(
+ 64 * 256, // bits to allocate.
+ 32 // number of hashes
+ );
+ let existing = this.Event.getMatchingTags('bloom');
+ if (existing.length == 1) {
+ b = new BloomFilter(JSON.parse(existing[0][existing[0].length - 1]), 32);
+ }
+ return b;
+ }
+ private AppendEventToBloom(id: string) {
+ let existing = this.Bloom();
+ existing.add(id);
+ this.Event.removeTag('bloom');
+ this.Event.tags.push(['bloom', '32', JSON.stringify([].slice.call(existing.buckets))]);
+ }
+ Included(id: string): boolean {
+ return this.Bloom().test(id);
+ }
+ UpsertBitcoinAssociation(association: BitcoinAssociation): NDKEvent | undefined {
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);
+ if (association.Validate() && association.Event && !this.Included(association.Event.id)) {
+ let existing = this.BitcoinAssociations().get(association.Address!);
+ if ((existing && existing.Pubkey != association.Pubkey) || !existing) {
+ this.PrepareForUpdate(association.Event.id);
+ 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;
}
@@ -29,14 +55,86 @@ export class Rocket {
ba.Address = split[1];
ba.Pubkey = split[0];
if (ba.Validate()) {
- a.set(ba.Pubkey, ba);
+ a.set(ba.Address, ba);
}
}
}
}
return a;
}
- Event: NDKEvent;
+ UpsertLeadTime(event: NDKEvent): NDKEvent {
+ //todo: validate that there are no current auctions that include this AMR
+ return new NDKEvent();
+ }
+ UpsertMeritTransfer(request: MeritPurchase): NDKEvent | undefined {
+ let event: NDKEvent | undefined = undefined;
+ if (this.PendingAMRAuctionsMap().get(request.auction.ID())) {
+ this.PrepareForUpdate();
+ let _event = new NDKEvent(this.Event.ndk, this.Event.rawEvent());
+ _event.created_at = Math.floor(new Date().getTime() / 1000);
+ //delete the auction
+ let auctionID = request.auction.ID();
+ let existing = _event.getMatchingTags('amr_auction');
+ _event.removeTag('amr_auction');
+ for (let t of existing) {
+ let amr = AMRAuctionFromTag(t, this.Event);
+ if (amr.ID() != auctionID) {
+ _event.tags.push(t);
+ }
+ }
+ _event.tags.push(['proof_raw', `txid:${request.tx.ID}`]);
+
+ let modifiedMerits: Map = new Map();
+ for (let id of request.auction.AMRIDs) {
+ let amr = this.ApprovedMeritRequests().get(id);
+ if (!amr) {
+ return event;
+ }
+ if (amr.LeadTime > 0) {
+ return event;
+ }
+ amr.Pubkey = request.buyer;
+ modifiedMerits.set(amr.ID, amr);
+ }
+ let existingMerits = this.ApprovedMeritRequests();
+ for (let [id, m] of modifiedMerits) {
+ existingMerits.set(id, m);
+ }
+ _event.removeTag('merit');
+ for (let [id, m] of existingMerits) {
+ _event.tags.push(m.Tag());
+ }
+ _event.tags.push([
+ 'swap',
+ `${request.auction.Merits}:${request.tx.Amount}:${Math.floor(new Date().getTime() / 1000)}`
+ ]);
+ let existingAssociation = this.BitcoinAssociations().get(request.tx.From);
+ if (
+ !existingAssociation ||
+ (existingAssociation && existingAssociation.Pubkey != request.buyer)
+ ) {
+ return event;
+ }
+ if (request.tx.Change) {
+ let existingAssociations = this.BitcoinAssociations();
+ _event.removeTag('address');
+ for (let [_, ba] of existingAssociations) {
+ if (ba.Address != request.tx.From) {
+ _event.tags.push(ba.Tag());
+ }
+ if (ba.Address == request.tx.From) {
+ ba.Address = request.tx.Change;
+ _event.tags.push(ba.Tag());
+ }
+ }
+ }
+
+ updateIgnitionAndParentTag(_event);
+ updateBitcoinTip(_event);
+ event = _event;
+ }
+ return event;
+ }
URL(): string {
let ignitionID = undefined;
@@ -181,47 +279,38 @@ export class Rocket {
}
return event;
}
- PendingAMRAuctions(): AMRAuction[] {
- let auctions: AMRAuction[] = [];
+ PendingAMRAuctionsMap(): Map {
+ let m = new Map();
for (let t of this.Event.getMatchingTags('amr_auction')) {
- if (t.length == 2) {
- let items = t[1].split(':');
- if (items.length == 6) {
- let a = new AMRAuction(this.Event);
- a.RxAddress = items[0];
- 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 failed = false;
- for (let id of a.AMRIDs) {
- let amr = amrs.get(id);
- if (!amr) {
- failed = true;
- } else {
- if (!a.Owner) {
- a.Owner = amr.Pubkey;
- } else if (a.Owner != amr.Pubkey) {
- failed = true;
- }
- }
- }
- if (!failed) {
- auctions.push(a);
+ let auction = AMRAuctionFromTag(t, this.Event);
+ if (auction.Validate()) {
+ let amrs = this.ApprovedMeritRequests();
+ let failed = false;
+ for (let id of auction.AMRIDs) {
+ let amr = amrs.get(id);
+ if (!amr) {
+ failed = true;
} else {
- throw new Error('this should not happen, bug!');
+ if (!auction.Owner) {
+ auction.Owner = amr.Pubkey;
+ } else if (auction.Owner != amr.Pubkey) {
+ failed = true;
+ }
}
}
+ if (!failed) {
+ m.set(auction.ID(), auction);
+ } else {
+ throw new Error('this should not happen, bug!');
+ }
}
}
- return auctions;
+ return m;
+ }
+ PendingAMRAuctions(): AMRAuction[] {
+ return Array.from(this.PendingAMRAuctionsMap(), ([_, amr]) => {
+ return amr;
+ });
}
CanThisAMRBeSold(amr: string): boolean {
let valid = true;
@@ -280,7 +369,7 @@ export class Rocket {
return event;
}
UpsertProduct(id: string, price: number, maxSales?: number): NDKEvent {
- this.PrepareForUpdate();
+ this.PrepareForUpdate(id);
let event = new NDKEvent(this.Event.ndk, this.Event.rawEvent());
event.created_at = Math.floor(new Date().getTime() / 1000);
let existingProducts = this.CurrentProducts();
@@ -353,9 +442,12 @@ export class Rocket {
}
this.Event.tags = newTags;
}
- PrepareForUpdate() {
+ PrepareForUpdate(id?: string) {
this.RemoveDuplicateTags();
this.RemoveProofs();
+ if (id) {
+ this.AppendEventToBloom(id);
+ }
this.Event.sig = undefined;
}
constructor(event: NDKEvent) {
@@ -419,8 +511,15 @@ export class RocketAMR {
LeadTime: number;
LeadTimeUpdate: number;
Merits: number;
- Extra: {eventAMR: AMRAuction};
+ Extra: { eventAMR: AMRAuction };
+ Tag(): NDKTag {
+ return [
+ 'merit',
+ `${this.Pubkey}:${this.ID}:${this.LeadTime}:${this.LeadTimeUpdate}:${this.Merits}`
+ ];
+ }
SatsOwed(): number {
+ //if rocket creator is acting as custodian instead of using a cashu mint
return 0;
}
SatsPaid(): number {
@@ -619,7 +718,12 @@ export async function ValidateZapPublisher(rocket: NDKEvent, zap: NDKEvent): Pro
});
}
-type AMRAuctionStatus = 'PENDING' | 'OPEN' | 'TX DETECTED' | 'SOLD & PENDING RATIFICATION' | 'CHECKING MEMPOOL';
+type AMRAuctionStatus =
+ | 'PENDING'
+ | 'OPEN'
+ | 'TX DETECTED'
+ | 'SOLD & PENDING RATIFICATION'
+ | 'CHECKING MEMPOOL';
export class AMRAuction {
AMRIDs: string[];
@@ -630,14 +734,14 @@ export class AMRAuction {
RocketD: string;
RocketP: string;
Merits: number;
- Event: NDKEvent;
+ Event: NDKEvent | undefined;
Extra: { rocket: Rocket };
- Status(
- rocket: Rocket,
- bitcoinTip: number,
- transactions?: txs
- ): AMRAuctionStatus {
- let status:AMRAuctionStatus = "PENDING"
+ ID(): string {
+ this.AMRIDs.sort();
+ return sha256(''.concat(...this.AMRIDs).trim());
+ }
+ Status(rocket: Rocket, bitcoinTip: number, transactions?: txs): AMRAuctionStatus {
+ let status: AMRAuctionStatus = 'PENDING';
if (transactions && transactions.Address != this.RxAddress) {
throw new Error('invalid address');
}
@@ -663,17 +767,14 @@ export class AMRAuction {
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
- ) {
+ found = true;
+ if (status == 'CHECKING MEMPOOL') {
+ if (Math.floor(new Date().getTime() / 1000) < transactions.LastUpdate + 60000) {
status = 'OPEN';
}
}
}
}
-
}
return status;
}
@@ -720,12 +821,14 @@ export class AMRAuction {
Validate(): boolean {
let valid = true;
if (
- this.Owner?.length != 64 ||
+ (this.Owner && this.Owner.length != 64) ||
!this.StartPrice ||
!this.EndPrice ||
!validate(this.RxAddress) ||
- this.RocketP.length != 64
+ this.RocketP.length != 64 ||
+ !this.Merits
) {
+ //console.log(780, this, (this.Owner && this.Owner.length != 64), !this.StartPrice, !this.EndPrice, !validate(this.RxAddress), this.RocketP.length != 64, !this.Merits)
valid = false;
}
for (let id of this.AMRIDs) {
@@ -804,6 +907,9 @@ export class BitcoinAssociation {
Address: string | undefined;
Event: NDKEvent;
Balance: number;
+ Tag(): NDKTag {
+ return ['address', `${this.Pubkey}:${this.Address}`];
+ }
Validate(): boolean {
let valid = true;
if (this.Pubkey.length != 64) {
@@ -891,3 +997,41 @@ export class Product {
this.Event = event;
}
}
+
+export class MeritPurchase {
+ auction: AMRAuction;
+ buyer: string;
+ tx: txo;
+ rocket: Rocket;
+ Validate(): boolean {
+ //todo: at least validate the utxo format
+ return true;
+ }
+ constructor(rocket: Rocket, auction: AMRAuction, buyer: string, tx: txo) {
+ this.rocket = rocket;
+ this.auction = auction;
+ this.buyer = buyer;
+ this.tx = tx;
+ }
+}
+
+function AMRAuctionFromTag(t: NDKTag, rocket: NDKEvent): AMRAuction {
+ let a = new AMRAuction(rocket);
+ if (t.length == 2) {
+ let items = t[1].split(':');
+ if (items.length == 6) {
+ a.RxAddress = items[0];
+ 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);
+ }
+ }
+ }
+ }
+ return a;
+}
diff --git a/src/lib/stores/bitcoin.ts b/src/lib/stores/bitcoin.ts
index 3746612..69008bb 100644
--- a/src/lib/stores/bitcoin.ts
+++ b/src/lib/stores/bitcoin.ts
@@ -134,6 +134,7 @@ export class txs {
let amount = 0;
let height = tx.status.block_height ? tx.status.block_height : 0;
let txid = tx.txid;
+ let change: string[] = [];
for (let vout of tx.vout) {
let address = vout.scriptpubkey_address;
if (address && address.trim() == this.Address) {
@@ -141,8 +142,11 @@ export class txs {
if (value) {
amount += parseInt(value, 10);
}
+ } else {
+ change.push(address);
}
}
+
for (let vin of tx.vin) {
let address = vin.prevout.scriptpubkey_address;
if (address && validate(address)) {
@@ -152,9 +156,12 @@ export class txs {
t.From = address;
t.To = this.Address;
t.ID = txid;
+ if (change.length == 1) {
+ t.Change = change[0];
+ }
possibles.set(address, t);
} else {
- console.log(156, vin)
+ console.log(156, vin);
}
}
}
@@ -174,5 +181,6 @@ export class txo {
To: string;
Amount: number;
Height: number;
+ Change: string;
constructor() {}
}
diff --git a/src/routes/buymerits/+page.svelte b/src/routes/buymerits/+page.svelte
index 1d9b6f4..b1b1c47 100644
--- a/src/routes/buymerits/+page.svelte
+++ b/src/routes/buymerits/+page.svelte
@@ -1,7 +1,6 @@
-{#if $noAssociatedBitcoinAddress}{/if}
+{#if $nostrocket}
+{/if}
{#if $currentUser}
{#each $pendingSales as [rocket, amr] (rocket.Event.id)}
@@ -172,9 +179,9 @@
}}>{p.RxAddress}
{#if p.Status(rocket, $bitcoinTip.height, $transactions.get(p.RxAddress)) == 'OPEN'}{/if}{#if p.Status(rocket, $bitcoinTip.height, $transactions.get(p.RxAddress)) == 'OPEN'}{/if}
{/each}