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 { sha256 } from 'js-sha256'; import { MapOfVotes, MeritRequest, Votes } from './merits'; import * as immutable from 'immutable'; import { BloomFilter } from 'bloomfilter'; export class Rocket { 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 (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; } BitcoinAssociations(): Map { let a = new Map(); 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.Address, ba); } } } } return a; } 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; 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(): Map { let _products = new Map(); for (let p of this.Event.getMatchingTags('product')) { let rp = new RocketProduct(p); _products.set(rp.ID, rp); } return _products; } VotePowerForPubkey(pubkey: string): number { let votepower = 0; if (this.Event.pubkey == pubkey) { //todo: calculate votepower for pubkey based on approved merit requests votepower++; } return votepower; } TotalVotePower(): number { //todo: calculate votepower for pubkey based on approved merit requests return 1; } ApprovedMeritRequests(): Map { let amr = new Map(); for (let m of this.Event.getMatchingTags('merit')) { if (m && m.length == 2) { let _amr = new RocketAMR(m[1]); amr.set(_amr.ID, _amr); } } return amr; } TotalMerits(): number { let total = 0; let amr = this.ApprovedMeritRequests(); for (let [_, _amr] of amr) { total += _amr.Merits; } return total; } ValidateAMRProof(amrProof: NDKEvent): boolean { let result = false; if (this.VotePowerForPubkey(amrProof.pubkey) > 0 && amrProof.verifySignature(true)) { let request: NDKEvent | undefined = undefined; let votes: NDKEvent[] = []; let _request = amrProof.getMatchingTags('request'); if (_request.length == 1) { try { let __request = new NDKEvent(undefined, JSON.parse(_request[0][1])); if (__request.verifySignature(true)) { request = __request; } } catch {} } for (let v of amrProof.getMatchingTags('vote')) { try { let vEv = new NDKEvent(undefined, JSON.parse(v[1])); if (vEv.verifySignature(true)) votes.push(vEv); } catch {} } if (request && votes.length > 0) { let parsedRequest = new MeritRequest(request); let mapOfVotes = new MapOfVotes(votes, this, parsedRequest).Votes; let parsedVotes = new Votes(Array.from(mapOfVotes, ([_, v]) => v)); let voteDirection = parsedVotes.Results().Result(this); if ( voteDirection && voteDirection == 'ratify' && !parsedRequest.IncludedInRocketState(this) ) { //note: if it is included in the rocket state, we might be validating this against a previous state result = true; } } } return result; } CreateUnsignedAMRProof(request: MeritRequest, votes: Votes): NDKEvent | undefined { let proof: NDKEvent | undefined = undefined; let hasInvalidSig = false; if (request && request.Event && request.Event.sig && votes.Votes.length > 0) { if (!request.Event.verifySignature(true)) { hasInvalidSig = true; } for (let v of votes.Votes) { if (!(v.Event.sig && v.Event.verifySignature(true))) { hasInvalidSig = true; } } let result = votes.Results().Result(this); if (result && result == 'ratify' && !request.IncludedInRocketState(this) && !hasInvalidSig) { let e = new NDKEvent(); e.kind = 1411; e.tags.push(['request', JSON.stringify(request.Event.rawEvent())]); for (let v of votes.Votes) { e.tags.push(['vote', JSON.stringify(v.Event.rawEvent())]); } proof = e; } } return proof; } UpsertAMR(request: MeritRequest, signedProof: NDKEvent): NDKEvent | undefined { let event: NDKEvent | undefined = undefined; if (this.ValidateAMRProof(signedProof)) { this.PrepareForUpdate(); 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.rawEvent())]); updateIgnitionAndParentTag(event); updateBitcoinTip(event); } return event; } PendingAMRAuctionsMap(): Map { let m = new Map(); for (let t of this.Event.getMatchingTags('amr_auction')) { 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 { 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 m; } PendingAMRAuctions(): AMRAuction[] { return Array.from(this.PendingAMRAuctionsMap(), ([_, amr]) => { return amr; }); } CanThisAMRBeSold(amr: string): boolean { let valid = true; let existing = this.ApprovedMeritRequests().get(amr); if (!existing) { valid = false; } if (existing && existing.LeadTime > 0) { valid = false; } let pending = this.PendingAMRAuctions(); for (let p of pending) { if (p.AMRIDs.includes(amr)) { valid = false; } } return valid; } UpsertAMRAuction(request: AMRAuction): NDKEvent | undefined { //todo: validate that all items in the request exist and the total amount is correct, from same pubkey let event: NDKEvent | undefined = undefined; let invalid = false; if (request.ValidateAgainstRocket(this)) { this.PrepareForUpdate(); event = new NDKEvent(this.Event.ndk, this.Event.rawEvent()); event.created_at = Math.floor(new Date().getTime() / 1000); let totalMerits = 0; let requestIDs: string = ''; for (let id of request.AMRIDs) { let amr = this.ApprovedMeritRequests().get(id); if (!amr) { invalid = true; } else { if (amr.LeadTime > 0 || amr.Pubkey != request.Owner) { invalid = true; } else { totalMerits += amr.Merits; requestIDs += id; } } } if (totalMerits != request.Merits) { invalid = true; } event.tags.push([ 'amr_auction', `${request.RxAddress}:${0}:${request.StartPrice}:${request.EndPrice}:${request.Merits}:${requestIDs}` ]); // event.tags.push(['proof_full', JSON.stringify(request.Event!.rawEvent())]); updateIgnitionAndParentTag(event); updateBitcoinTip(event); } if (invalid) { event = undefined; } return event; } UpsertProduct(id: string, price: number, maxSales?: number): NDKEvent { 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(); let purchases = JSON.stringify([]); let existingProduct = existingProducts.get(id); if (existingProduct) { purchases = existingProduct.PurchasesJSON(); } event.tags.push([ 'product', `${id}:${price}:${event.created_at}:${maxSales}`, 'wss://relay.nostrocket.org', purchases ]); updateIgnitionAndParentTag(event); updateBitcoinTip(event); return event; } UpdateMission(mission: string): NDKEvent { this.PrepareForUpdate(); let event = new NDKEvent(this.Event.ndk, this.Event.rawEvent()); event.created_at = Math.floor(new Date().getTime() / 1000); event.removeTag('mission'); event.tags.push(['mission', mission]); updateIgnitionAndParentTag(event); updateBitcoinTip(event); return event; } CurrentProducts(): Map { return getMapOfProductsFromRocket(this.Event); } RemoveDuplicateTags() { function iterate(event: NDKEvent): NDKEvent { let purged = 0; for (let i = 0; i < event.tags.length; i++) { for (let j = i + 1; j < event.tags.length; j++) { // quick elimination by comparing sub-array lengths if (event.tags[i].length !== event.tags[j].length) { continue; } // look for dupes var dupe = true; for (var k = 0; k < event.tags[i].length; k++) { if (event.tags[i][k] !== event.tags[j][k]) { dupe = false; break; } } // if a dupe then remove it if (dupe) { purged++; event.tags.splice(j, 1); } } } if (purged > 0) { return iterate(event); } else { return event; } } this.Event = iterate(this.Event); } RemoveProofs() { let newTags: NDKTag[] = []; for (let t of this.Event.tags) { if (!t[0].includes('proof') && t[0] != 'client') { newTags.push(t); } } this.Event.tags = newTags; } PrepareForUpdate(id?: string) { this.RemoveDuplicateTags(); this.RemoveProofs(); if (id) { this.AppendEventToBloom(id); } this.Event.sig = undefined; } constructor(event: NDKEvent) { this.Event = event; } } function updateIgnitionAndParentTag(event: NDKEvent) { let existingIgnition = event.getMatchingTags('ignition'); //let existingParent = rocket.getMatchingTags("parent") let existing = []; for (let t of event.tags) { existing.push(t); } event.tags = []; for (let t of existing) { if (t[0] !== 'ignition' && t[0] !== 'parent') { event.tags.push(t); } } if (existingIgnition.length > 1) { throw new Error('too many ignition tags!'); } if (existingIgnition.length == 0) { event.tags.push(['ignition', event.id]); } if (existingIgnition.length == 1) { if (existingIgnition[0][1].length == 64) { event.tags.push(existingIgnition[0]); } if (existingIgnition[0][1] == 'this') { event.tags.push(['ignition', event.id]); } } event.tags.push(['parent', event.id]); } function updateBitcoinTip(event: NDKEvent) { let existingBitcoinTip = event.getMatchingTags('bitcoin'); let existing = []; for (let t of event.tags) { existing.push(t); } event.tags = []; for (let t of existing) { if (t[0] !== 'bitcoin') { event.tags.push(t); } } if (existingBitcoinTip.length > 1) { throw new Error('too many bitcoin tip tags!'); } else { event.tags.push(BitcoinTipTag()); } } export class RocketAMR { //todo: also add a query for sats tags to find payments for this AMR ID: string; Pubkey: string; LeadTime: number; LeadTimeUpdate: number; Merits: number; 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 { return 0; } Valid(): boolean { let valid = true; if (!(this.ID.length == 64 && this.Pubkey.length == 64 && this.Merits)) { valid = false; } return valid; } constructor(meritString: string) { let split = meritString.split(':'); if (split.length == 5) { this.Pubkey = split[0]; this.ID = split[1]; this.LeadTime = parseInt(split[2], 10); this.LeadTimeUpdate = parseInt(split[3], 10); this.Merits = parseInt(split[4], 10); } } } export class RocketProduct { ID: string; Price: number; ValidAfter: number; //unix time MaxPurchases: number; Purchases: Map; PurchasesJSON(): string { let purchases = []; for (let [_, p] of this.Purchases) { purchases.push(`${p.ZapID}:${p.BuyerPubkey}:${p.WitnessedAt}`); } return JSON.stringify(purchases); } constructor(tag: NDKTag) { this.Purchases = new Map(); this.ID = tag[1].split(':')[0]; this.Price = parseInt(tag[1].split(':')[1], 10); this.ValidAfter = parseInt(tag[1].split(':')[2], 10); this.MaxPurchases = parseInt(tag[1].split(':')[3], 10); let purchases = JSON.parse(tag[3]); for (let p of purchases) { let payment = new ProductPayment(p); this.Purchases.set(payment.ZapID, payment); } } } //ProductPayment takes the payment string from a product tag on a rocket event export class ProductPayment { ZapID: string; BuyerPubkey: string; WitnessedAt: number; constructor(purchase: string) { this.ZapID = purchase.split(':')[0]; this.BuyerPubkey = purchase.split(':')[1]; this.WitnessedAt = parseInt(purchase.split(':')[2], 10); } } export function getMapOfProductsFromRocket(rocket: NDKEvent): Map { let productIDs = new Map(); for (let product of rocket.getMatchingTags('product')) { if (product.length > 1 && product[1].split(':') && product[1].split(':').length > 0) { productIDs.set(product[1].split(':')[0], new RocketProduct(product)); } } return productIDs; } export class ZapPurchase { Amount: number; ProductID: string; BuyerPubkey: string; ZapReceipt: NDKEvent; ZapRequest(): NDKEvent | undefined { return getZapRequest(this.ZapReceipt); } IncludedInRocketState(rocket: NDKEvent): boolean { let thisProduct = this.ProductFromRocket(rocket); if (thisProduct) { return thisProduct.Purchases.get(this.ZapReceipt.id) ? true : false; } else { return false; } } ProductFromRocket(rocket: NDKEvent): RocketProduct | undefined { let productsInRocket = getMapOfProductsFromRocket(rocket); return productsInRocket.get(this.ProductID); } ValidAmount(rocket: NDKEvent): boolean { if (this.Amount < 1) { return false; } if (this.IncludedInRocketState(rocket)) { return true; } let product = this.ProductFromRocket(rocket); if (product && this.Amount / 1000 >= product.Price) { return true; } return false; } Valid(rocket: NDKEvent): boolean { //todo: validate zapper pubkey is from a LSP specified in rocket let valid = true; if (!this.ValidAmount(rocket)) { valid = false; } if (!this.ProductID) { valid = false; } if (this.ProductID && this.ProductID.length != 64) { valid = false; } if (this.BuyerPubkey.length != 64) { valid = false; } return valid; } constructor(zapReceipt: NDKEvent) { this.ZapReceipt = zapReceipt; this.Amount = getZapAmount(this.ZapRequest()); let zapRequest = this.ZapRequest(); if (zapRequest) { this.BuyerPubkey = zapRequest.pubkey; let products = zapRequest.getMatchingTags('product'); if (products.length == 1 && products[0] && products[0][1] && products[0][1].length == 64) { this.ProductID = products[0][1]; } } } } function getZapRequest(zapReceipt: NDKEvent): NDKEvent | undefined { let zapRequestEvent: NDKEvent | undefined = undefined; let zapRequest = zapReceipt.getMatchingTags('description'); if (zapRequest.length == 1) { let zapRequestJSON = JSON.parse(zapRequest[0][1]); if (zapRequestJSON) { zapRequestEvent = new NDKEvent(zapReceipt.ndk, zapRequestJSON); } } return zapRequestEvent; } function getZapAmount(zapRequest?: NDKEvent): number { return getNumberFromTag('amount', zapRequest); } export function getNumberFromTag(tag: string, event?: NDKEvent): number { let amountTag = event?.getMatchingTags(tag); if (amountTag && amountTag[0] && amountTag[0][1]) { try { let amount = parseInt(amountTag[0][1], 10); return amount; } catch { console.log('ERROR: could not find number in tag: ', tag, event); } } return 0; } export function isValidUrl(string: string): boolean { try { new URL(string); return true; } catch (err) { return false; } } export function RocketATagFilter(rocket: NDKEvent): string { return `31108:${rocket.pubkey}:${rocket.dTag}`; } export async function ValidateZapPublisher(rocket: NDKEvent, zap: NDKEvent): Promise { return new Promise((resolve, reject) => { getAuthorizedZapper(rocket) .then((pubkey) => { if (pubkey == zap.pubkey) { resolve(true); } else { reject(); } }) .catch(reject); // let z = new NDKZap({ ndk: rocket.ndk!, zappedEvent: rocket, zappedUser: rocket.author }); // z.getZapEndpoint().then(x=>{ // console.log(x) // resolve(true) // }).catch(()=>{reject(false)}) }); } type AMRAuctionStatus = | 'PENDING' | 'OPEN' | 'TX DETECTED' | 'SOLD & PENDING RATIFICATION' | 'CHECKING MEMPOOL'; export class AMRAuction { AMRIDs: string[]; Owner: string | undefined; StartPrice: number; EndPrice: number; RxAddress: string; RocketD: string; RocketP: string; Merits: number; Event: NDKEvent | undefined; Extra: { rocket: Rocket }; 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'); } if (transactions) { status = 'CHECKING MEMPOOL'; for (let [t, txo] of transactions.From()) { //todo: implement pricing based on block height if (txo.Amount == this.EndPrice && txo.To == this.RxAddress) { 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; } 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(['merits', this.Merits.toString()]); 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 && this.Owner.length != 64) || !this.StartPrice || !this.EndPrice || !validate(this.RxAddress) || 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) { 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; } for (let pending of rocket.PendingAMRAuctions()) { if (pending.AMRIDs.includes(id)) { valid = false; } } } 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 merits = event.tagValue('merits'); if (merits) { let int = parseInt(merits, 10); this.Merits = int; } 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]; } } } } } export class BitcoinAssociation { Pubkey: string; 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) { valid = false; } if ((this.Address && !validate(this.Address)) || !this.Address) { valid = false; } return valid; } constructor(event?: NDKEvent) { this.Balance = 0; if (event) { this.Pubkey = event.pubkey; this.Address = event.tagValue('onchain'); this.Event = event; } } } export class Product { Event: NDKEvent; Group(): string { let s = this.Name(); //let regex = /\[\w+\]/i; //if (regex.test(this.Name())) { let g = this.Name().substring(this.Name().indexOf('[') + 1, this.Name().lastIndexOf(']')); if (g.length > 0) { s = g; } //} return s; } Option(): string { let result = ''; let group = this.Name().substring(this.Name().indexOf('['), this.Name().lastIndexOf(']') + 1); if (group.length > 0) { for (let s of this.Name().trim().split(group)) { if (s.trim().length > 0) { result = s.trim(); } } } return result; } ID(): string { return this.Event.id; } Name(): string { return this.Event.getMatchingTags('name')[0][1]; } Description(): string { return this.Event.getMatchingTags('description')[0][1]; } CoverImage(): string { return this.Event.getMatchingTags('cover')[0][1]; } Validate(): boolean { let test = 0; if ( this.Event.getMatchingTags('name') && this.Event.getMatchingTags('name')[0] && this.Event.getMatchingTags('name')[0][1] ) { test++; } if ( this.Event.getMatchingTags('description') && this.Event.getMatchingTags('description')[0] && this.Event.getMatchingTags('description')[0][1] ) { test++; } if ( this.Event.getMatchingTags('cover') && this.Event.getMatchingTags('cover')[0] && this.Event.getMatchingTags('cover')[0][1] ) { test++; } return test == 3; } constructor(event: NDKEvent) { 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; }