import { action, runInAction } from 'mobx'; import { LNURLPaySuccessAction } from 'js-lnurl'; import { schnorr } from '@noble/curves/secp256k1'; import { hexToBytes } from '@noble/hashes/utils'; import hashjs from 'hash.js'; import { nip19, // @ts-ignore:next-line finishEvent, // @ts-ignore:next-line generatePrivateKey, // @ts-ignore:next-line getPublicKey, // @ts-ignore:next-line relayInit } from 'nostr-tools'; import Storage from '../storage'; import SettingsStore from './SettingsStore'; import NodeInfoStore from './NodeInfoStore'; export interface LnurlPayTransaction { paymentHash: string; domain: string; lnurl: string; metadata_hash: string; successAction: LNURLPaySuccessAction; time: number; metadata?: Metadata; // only after an independent load from Storage. } interface Metadata { metadata: string; } interface LnurlPayMetadataEntry { metadata: string; last_stored: number; } export default class LnurlPayStore { paymentHash: string | undefined; domain: string | undefined; successAction: LNURLPaySuccessAction | undefined; settingsStore: SettingsStore; nodeInfoStore: NodeInfoStore; // Zaplocker isZaplocker: boolean | undefined; isPmtHashSigValid: boolean | undefined; isRelaysSigValid: boolean | undefined; zaplockerNpub: string | undefined; relays: Array | undefined; paymentRequest: string | undefined; lightningAddress: string | undefined; constructor(settingsStore: SettingsStore, nodeInfoStore: NodeInfoStore) { this.settingsStore = settingsStore; this.nodeInfoStore = nodeInfoStore; } @action public reset = () => { this.paymentHash = undefined; this.domain = undefined; this.successAction = undefined; this.isZaplocker = undefined; this.isPmtHashSigValid = undefined; this.isRelaysSigValid = undefined; this.zaplockerNpub = undefined; this.relays = undefined; this.paymentRequest = undefined; this.lightningAddress = undefined; }; public load = async (paymentHash: string): Promise => { let lnurlpaytx: any = await Storage.getItem('lnurlpay:' + paymentHash); if (lnurlpaytx) { lnurlpaytx = JSON.parse(lnurlpaytx); const metadata: any = await Storage.getItem( 'lnurlpay:' + lnurlpaytx.metadata_hash ); if (metadata) { lnurlpaytx.metadata = JSON.parse(metadata); } } return lnurlpaytx; }; public keep = async ( paymentHash: string, domain: string, lnurl: string, metadata: string, descriptionHash: string, successAction: LNURLPaySuccessAction, pmthash_sig?: string, user_pubkey?: string, relays?: Array, relays_sig?: string, pr?: string, lightningAddress?: string ) => { this.reset(); const now = new Date().getTime(); const transactionData: LnurlPayTransaction = { paymentHash, domain, lnurl, successAction, time: now, metadata_hash: descriptionHash }; const metadataEntry: LnurlPayMetadataEntry = { metadata, last_stored: now }; await Storage.setItem('lnurlpay:' + paymentHash, transactionData); await Storage.setItem('lnurlpay:' + descriptionHash, metadataEntry); runInAction(() => { this.paymentHash = paymentHash; this.successAction = successAction; this.domain = domain; if (pr) this.paymentRequest = pr; if (lightningAddress) this.lightningAddress = lightningAddress; // Zaplocker if (user_pubkey) { this.isZaplocker = true; try { this.zaplockerNpub = nip19.npubEncode(user_pubkey); } catch (e) {} if (pmthash_sig) { const pmtHashBytes = hexToBytes(pmthash_sig); this.isPmtHashSigValid = schnorr.verify( pmtHashBytes, paymentHash, user_pubkey ); } if (relays && relays_sig) { this.relays = relays; const relaysBytes = hexToBytes(relays_sig); this.isRelaysSigValid = schnorr.verify( relaysBytes, hashjs .sha256() .update(JSON.stringify(relays)) .digest('hex'), user_pubkey ); } } }); }; public broadcastAttestation = async () => { const hash = this.paymentHash; const invoice = this.paymentRequest; const relays = this.relays; if (!hash || !invoice || !relays) return; // create ephemeral key const sk = generatePrivateKey(); const pk = getPublicKey(sk); const hashpk = getPublicKey(hash); const event = { kind: 55869, pubkey: pk, created_at: Math.floor(Date.now() / 1000), tags: [['p', hashpk]], content: invoice }; // this calculates the event id and signs the event in a single step const signedEvent = finishEvent(event, sk); console.log('signedEvent', signedEvent); await Promise.all( relays.map(async (relayItem: string) => { const relay = relayInit(relayItem); relay.on('connect', () => { console.log(`connected to ${relay.url}`); }); relay.on('error', () => { console.log(`failed to connect to ${relay.url}`); }); await relay.connect(); await relay.publish(signedEvent); console.log('event.id', signedEvent.id); const eventReceived = await relay.get({ ids: [signedEvent.id] }); console.log('eventReceived', eventReceived); return; }) ); console.log('broadcast complete'); return; }; }