From ee5f3c2743c5206e1db974025171d2dc1fbb14bd Mon Sep 17 00:00:00 2001 From: boufni95 Date: Tue, 3 Sep 2024 14:48:31 +0000 Subject: [PATCH] code in pllace --- src/custom-nip19.ts | 40 ++++++++++++-- src/nostrMiddleware.ts | 43 ++------------- src/services/main/applicationManager.ts | 4 +- src/services/main/index.ts | 71 +++++++++++++++++++++++-- src/services/main/paymentManager.ts | 4 +- src/services/main/productManager.ts | 2 +- src/services/nostr/handler.ts | 3 ++ 7 files changed, 114 insertions(+), 53 deletions(-) diff --git a/src/custom-nip19.ts b/src/custom-nip19.ts index 9c1a7eb1..64de7ba8 100644 --- a/src/custom-nip19.ts +++ b/src/custom-nip19.ts @@ -22,8 +22,14 @@ export type OfferPointer = { pubkey: string, relays?: string[], offer: string + priceType: 'fixed' | 'spontaneous' | 'variable', + price?: number +} +enum PriceType { + fixed = 0, + variable = 1, + spontaneous = 2, } - type TLV = { [t: number]: Uint8Array[] } @@ -67,11 +73,17 @@ export const encodeNoffer = (offer: OfferPointer): string => { const settings = LoadNosrtSettingsFromEnv() relays = settings.relays } - const data = encodeTLV({ + const typeAsNum = Number(PriceType[offer.priceType]) + const o: TLV = { 0: [hexToBytes(offer.pubkey)], 1: (relays).map(url => utf8Encoder.encode(url)), - 2: [utf8Encoder.encode(offer.offer)] - }); + 2: [utf8Encoder.encode(offer.offer)], + 3: [new Uint8Array([typeAsNum])], + } + if (offer.price) { + o[4] = [new Uint8Array(new BigUint64Array([BigInt(offer.price)]).buffer)] + } + const data = encodeTLV(o); const words = bech32.toWords(data) return bech32.encode("noffer", words, 5000); } @@ -91,6 +103,24 @@ const parseTLV = (data: Uint8Array): TLV => { return result } +export const decodeNoffer = (noffer: string): OfferPointer => { + const { prefix, words } = bech32.decode(noffer, 5000) + if (prefix !== "noffer") { + throw new Error("Expected nprofile prefix"); + } + const data = new Uint8Array(bech32.fromWords(words)) + + const tlv = parseTLV(data); + if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for noffer') + if (tlv[0][0].length !== 32) throw new Error('TLV 0 should be 32 bytes') + + return { + pubkey: bytesToHex(tlv[0][0]), + relays: tlv[1] ? tlv[1].map(d => utf8Decoder.decode(d)) : [], + bridge: tlv[2] ? tlv[2].map(d => utf8Decoder.decode(d)) : [] + } +} + export const decodeNprofile = (nprofile: string): CustomProfilePointer => { const { prefix, words } = bech32.decode(nprofile, 5000) if (prefix !== "nprofile") { @@ -107,4 +137,4 @@ export const decodeNprofile = (nprofile: string): CustomProfilePointer => { relays: tlv[1] ? tlv[1].map(d => utf8Decoder.decode(d)) : [], bridge: tlv[2] ? tlv[2].map(d => utf8Decoder.decode(d)) : [] } -} \ No newline at end of file +} diff --git a/src/nostrMiddleware.ts b/src/nostrMiddleware.ts index 8ffe86c5..2bf563ff 100644 --- a/src/nostrMiddleware.ts +++ b/src/nostrMiddleware.ts @@ -7,7 +7,7 @@ import { ERROR, getLogger } from "./services/helpers/logger.js"; import { UnsignedEvent } from "./services/nostr/tools/event.js"; import { defaultInvoiceExpiry } from "./services/storage/paymentStorage.js"; import { Application } from "./services/storage/entity/Application.js"; - +type NofferData = { offer: string, amount?: number } export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings, onClientEvent: (e: { requestId: string }, fromPub: string) => void): { Stop: () => void, Send: NostrSend } => { const log = getLogger({}) const nostrTransport = NewNostrTransport(serverMethods, { @@ -49,9 +49,8 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett return } if (event.kind === 21001) { - const offerReq = j as { offer: string } - handleNofferEvent(mainHandler, offerReq, event) - .then(e => nostr.Send({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } })) + const offerReq = j as NofferData + mainHandler.handleNip69Noffer(offerReq, event) return } if (!j.rpcName) { @@ -69,40 +68,4 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett return { Stop: () => nostr.Stop, Send: (...args) => nostr.Send(...args) } } -// TODO: move this to paymentManager -const handleNofferEvent = async (mainHandler: Main, offerReq: { offer: string }, event: NostrEvent): Promise => { - const app = await mainHandler.storage.applicationStorage.GetApplication(event.appId) - try { - const offer = offerReq.offer - let invoice: string - const split = offer.split(':') - if (split.length === 1) { - const user = await mainHandler.storage.applicationStorage.GetApplicationUser(app, split[0]) - //TODO: add prop def for amount - const userInvoice = await mainHandler.paymentManager.NewInvoice(user.user.user_id, { amountSats: 1000, memo: "free offer" }, { expiry: defaultInvoiceExpiry, linkedApplication: app }) - invoice = userInvoice.invoice - } else if (split[0] === 'p') { - const product = await mainHandler.productManager.NewProductInvoice(split[1]) - invoice = product.invoice - } else { - return newNofferResponse(JSON.stringify({ code: 1, message: 'Invalid Offer' }), app, event) - } - return newNofferResponse(JSON.stringify({ bolt11: invoice }), app, event) - } catch (e: any) { - getLogger({ component: "noffer" })(ERROR, e.message || e) - return newNofferResponse(JSON.stringify({ code: 1, message: 'Invalid Offer' }), app, event) - } -} -const newNofferResponse = (content: string, app: Application, event: NostrEvent): UnsignedEvent => { - return { - content, - created_at: Math.floor(Date.now() / 1000), - kind: 21001, - pubkey: app.nostr_public_key!, - tags: [ - ['p', event.pub], - ['e', event.id], - ], - } -} diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index d44307e6..b7331cc2 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -161,7 +161,7 @@ export default class { service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps }, - noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: u.identifier }), + noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: u.identifier, priceType: 'spontaneous' }), max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true) } } @@ -199,7 +199,7 @@ export default class { network_max_fee_bps: this.settings.lndSettings.feeRateBps, network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit, service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps - }, noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: user.identifier }) + }, noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: user.identifier, priceType: 'spontaneous' }) } } diff --git a/src/services/main/index.ts b/src/services/main/index.ts index 2a8de39e..efcffa76 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -12,7 +12,7 @@ import AppUserManager from "./appUserManager.js" import { Application } from '../storage/entity/Application.js' import { UserReceivingInvoice } from '../storage/entity/UserReceivingInvoice.js' import { UnsignedEvent } from '../nostr/tools/event.js' -import { NostrSend } from '../nostr/handler.js' +import { NostrEvent, NostrSend } from '../nostr/handler.js' import MetricsManager from '../metrics/index.js' import { LoggedEvent } from '../storage/eventsLog.js' import { LiquidityProvider } from "./liquidityProvider.js" @@ -21,6 +21,7 @@ import { Utils } from "../helpers/utilsWrapper.js" import { RugPullTracker } from "./rugPullTracker.js" import { AdminManager } from "./adminManager.js" import { Unlocker } from "./unlocker.js" +import { defaultInvoiceExpiry } from "../storage/paymentStorage.js" type UserOperationsSub = { id: string @@ -30,6 +31,7 @@ type UserOperationsSub = { newOutgoingTx: (operation: Types.UserOperation) => void } const appTag = "Lightning.Pub" + export default class { storage: Storage lnd: LND @@ -46,9 +48,9 @@ export default class { liquidityProvider: LiquidityProvider utils: Utils rugPullTracker: RugPullTracker - unlocker:Unlocker + unlocker: Unlocker nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") } - constructor(settings: MainSettings, storage: Storage, adminManager: AdminManager, utils: Utils,unlocker:Unlocker) { + constructor(settings: MainSettings, storage: Storage, adminManager: AdminManager, utils: Utils, unlocker: Unlocker) { this.settings = settings this.storage = storage this.utils = utils @@ -272,4 +274,67 @@ export default class { log({ unsigned: event }) this.nostrSend({ type: 'app', appId: invoice.linkedApplication.app_id }, { type: 'event', event }) } + + async getNofferInvoice(offerReq: { offer: string, amount?: number }, appId: string): Promise<{ success: true, invoice: string } | { success: false, code: number }> { + try { + const { remote } = await this.lnd.ChannelBalance() + const { offer, amount } = offerReq + const split = offer.split(':') + if (split.length === 1) { + if (!amount || isNaN(amount) || amount < 10 || amount > remote) { + return { success: false, code: 5 } + } + const res = await this.applicationManager.AddAppUserInvoice(appId, { + http_callback_url: "", payer_identifier: split[0], receiver_identifier: split[0], + invoice_req: { amountSats: amount, memo: "free offer" } + }) + return { success: true, invoice: res.invoice } + } else if (split[0] === 'p') { + const product = await this.productManager.NewProductInvoice(split[1]) + return { success: true, invoice: product.invoice } + } else { + return { success: false, code: 1 } + } + } catch (e: any) { + getLogger({ component: "noffer" })(ERROR, e.message || e) + return { success: false, code: 1 } + } + } + + async handleNip69Noffer(offerReq: { offer: string }, event: NostrEvent) { + const offerInvoice = await this.getNofferInvoice(offerReq, event.appId) + if (!offerInvoice.success) { + const code = offerInvoice.code + const e = newNofferResponse(JSON.stringify({ code, message: codeToMessage(code) }), event) + this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) + return + } + const e = newNofferResponse(JSON.stringify({ bolt11: offerInvoice.invoice }), event) + this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) + return + } } + +const codeToMessage = (code: number) => { + switch (code) { + case 1: return 'Invalid Offer' + case 2: return 'Temporary Failure' + case 3: return 'Expired Offer' + case 4: return 'Unsupported Feature' + case 5: return 'Invalid Amount' + default: throw new Error("unknown error code" + code) + } +} + +const newNofferResponse = (content: string, event: NostrEvent): UnsignedEvent => { + return { + content, + created_at: Math.floor(Date.now() / 1000), + kind: 21001, + pubkey: "", + tags: [ + ['p', event.pub], + ['e', event.id], + ], + } +} \ No newline at end of file diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 9b7dbe60..babd6ff3 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -135,7 +135,7 @@ export default class { //}) return } - console.log({p}) + console.log({ p }) const paymentRes = await this.lnd.GetPayment(p.paymentIndex) const payment = paymentRes.payments[0] if (!payment || Number(payment.paymentIndex) !== p.paymentIndex) { @@ -337,7 +337,7 @@ export default class { const pendingPayment = await this.storage.txQueue.PushToQueue({ dbTx: true, description: "payment started", exec: async tx => { await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, invoice, tx) - return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: routingFeeLimit }, linkedApplication, provider,tx) + return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: routingFeeLimit }, linkedApplication, provider, tx) } }) this.log("ready to pay") diff --git a/src/services/main/productManager.ts b/src/services/main/productManager.ts index 3fd4dd4c..bf7a93dd 100644 --- a/src/services/main/productManager.ts +++ b/src/services/main/productManager.ts @@ -26,7 +26,7 @@ export default class { id: newProduct.product_id, name: newProduct.name, price_sats: newProduct.price_sats, - noffer: encodeNoffer({ pubkey: user.user_id, offer: offer }) + noffer: encodeNoffer({ pubkey: user.user_id, offer: offer, priceType: 'fixed', price: newProduct.price_sats }) } } diff --git a/src/services/nostr/handler.ts b/src/services/nostr/handler.ts index 9830cec4..e38dda2b 100644 --- a/src/services/nostr/handler.ts +++ b/src/services/nostr/handler.ts @@ -189,6 +189,9 @@ export default class Handler { const content = await encryptData(data.event.content, getSharedSecret(keys.privateKey, data.encrypt.toPub)) toSign.content = encodePayload(content) } + if (!toSign.pubkey) { + toSign.pubkey = keys.publicKey + } } const signed = finishEvent(toSign, keys.privateKey)