From d2b2418e21171f5e77f459065c7fc57f3194a5a1 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Fri, 13 Sep 2024 17:47:31 +0000 Subject: [PATCH] debits draft (pre db) --- src/custom-nip19.ts | 40 ++++++++- src/services/main/debitManager.ts | 94 ++++++++++++++++++++++ src/services/main/index.ts | 37 +++++++++ src/services/storage/debitStorage.ts | 30 +++++++ src/services/storage/entity/DebitAccess.ts | 30 +++++++ src/services/storage/index.ts | 3 + 6 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 src/services/main/debitManager.ts create mode 100644 src/services/storage/debitStorage.ts create mode 100644 src/services/storage/entity/DebitAccess.ts diff --git a/src/custom-nip19.ts b/src/custom-nip19.ts index baf2f8e5..7a7364a9 100644 --- a/src/custom-nip19.ts +++ b/src/custom-nip19.ts @@ -30,7 +30,11 @@ export enum PriceType { variable = 1, spontaneous = 2, } - +export type DebitPointer = { + pubkey: string, + relay: string, + pointerId?: string, +} type TLV = { [t: number]: Uint8Array[] } @@ -86,6 +90,23 @@ export const encodeNoffer = (offer: OfferPointer): string => { const words = bech32.toWords(data) return bech32.encode("noffer", words, 5000); } +export const encodeNdebit = (debit: DebitPointer): string => { + let relay = debit.relay + if (!relay) { + const settings = LoadNosrtSettingsFromEnv() + relay = settings.relays[0] + } + const o: TLV = { + 0: [hexToBytes(debit.pubkey)], + 1: [utf8Encoder.encode(relay)], + } + if (debit.pointerId) { + o[2] = [utf8Encoder.encode(debit.pointerId)] + } + const data = encodeTLV(o); + const words = bech32.toWords(data) + return bech32.encode("ndebit", words, 5000); +} const parseTLV = (data: Uint8Array): TLV => { const result: TLV = {} @@ -123,6 +144,23 @@ export const decodeNoffer = (noffer: string): OfferPointer => { price: tlv[4] ? Number(new BigUint64Array(tlv[4][0])[0]) : undefined } } +export const decodeNdebit = (noffer: string): DebitPointer => { + const { prefix, words } = bech32.decode(noffer, 5000) + if (prefix !== "ndebit") { + 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') + if (!tlv[1]?.[0]) throw new Error('missing TLV 1 for noffer') + return { + pubkey: bytesToHex(tlv[0][0]), + relay: utf8Decoder.decode(tlv[1][0]), + pointerId: tlv[2] ? utf8Decoder.decode(tlv[2][0]) : undefined + } +} export const decodeNprofile = (nprofile: string): CustomProfilePointer => { const { prefix, words } = bech32.decode(nprofile, 5000) diff --git a/src/services/main/debitManager.ts b/src/services/main/debitManager.ts new file mode 100644 index 00000000..88d1a13a --- /dev/null +++ b/src/services/main/debitManager.ts @@ -0,0 +1,94 @@ +import * as Types from "../../../proto/autogenerated/ts/types.js"; +import ApplicationManager from "./applicationManager.js"; +import { DebitKeyType } from "../storage/entity/DebitAccess.js"; +import Storage from '../storage/index.js' +import LND from "../lnd/lnd.js" +import { ERROR, getLogger } from "../helpers/logger.js"; +export type NdebitData = { pointer?: string, bolt11: string, amount_sats: number } +export type NdebitSuccess = { res: 'ok', preimage: string } +export type NdebitFailure = { res: 'GFY', error: string, code: number } +const nip68errs = { + 1: "Request Denied Warning", + 2: "Temporary Failure", + 3: "Expired Request", + 4: "Rate Limited", + 5: "Invalid Amount", + 6: "Invalid Request", +} +export type NdebitResponse = NdebitSuccess | NdebitFailure +type HandleNdebitRes = { ok: false, debitRes: NdebitFailure } | { ok: true, op: Types.UserOperation, appUserId: string, debitRes: NdebitSuccess } +export class DebitManager { + applicationManager: ApplicationManager + storage: Storage + lnd: LND + logger = getLogger({ component: 'DebitManager' }) + constructor(storage: Storage) { + this.storage = storage + } + + payNdebitInvoice = async (appId: string, requestorPub: string, pointerdata: NdebitData): Promise => { + try { + return await this.doNdebit(appId, requestorPub, pointerdata) + } catch (e: any) { + this.logger(ERROR, e.message || e) + return { ok: false, debitRes: { res: 'GFY', error: nip68errs[1], code: 1 } } + } + } + + doNdebit = async (appId: string, requestorPub: string, pointerdata: NdebitData): Promise => { + const { amount_sats, bolt11, pointer } = pointerdata + if (!bolt11) { + return { ok: false, debitRes: { res: 'GFY', error: nip68errs[6], code: 6 } } + } + const decoded = await this.lnd.DecodeInvoice(bolt11) + if (decoded.numSatoshis === 0) { + return { ok: false, debitRes: { res: 'GFY', error: nip68errs[6], code: 6 } } + } + if (amount_sats && amount_sats !== decoded.numSatoshis) { + return { ok: false, debitRes: { res: 'GFY', error: nip68errs[5], code: 5 } } + } + if (!pointer) { + // TODO: debit from app owner balance + return { ok: false, debitRes: { res: 'GFY', error: nip68errs[2], code: 2 } } + } + const split = pointer.split(':') + + let keyType: DebitKeyType + let key: string + let appUserId: string + if (split.length === 1) { + keyType = 'pubKey' + key = requestorPub + appUserId = split[0] + } else { + keyType = 'simpleId' + key = split[0] + appUserId = split[1] + } + const authorization = await this.storage.debitStorage.GetDebitAccess(appUserId, key, keyType) + if (!authorization) { + return { ok: false, debitRes: { res: 'GFY', error: nip68errs[1], code: 1 } } + } + const payment = await this.applicationManager.PayAppUserInvoice(appId, { amount: 0, invoice: bolt11, user_identifier: appUserId }) + await this.storage.debitStorage.IncrementDebitAccess(appUserId, key, keyType, payment.amount_paid + payment.service_fee + payment.network_fee) + const op = this.newPaymentOperation(payment, bolt11) + return { ok: true, op, appUserId, debitRes: { res: 'ok', preimage: payment.preimage } } + } + + newPaymentOperation = (payment: Types.PayInvoiceResponse, bolt11: string) => { + return { + amount: payment.amount_paid, + paidAtUnix: Date.now() / 1000, + inbound: false, + type: Types.UserOperationType.OUTGOING_INVOICE, + identifier: bolt11, + operationId: payment.operation_id, + network_fee: payment.network_fee, + service_fee: payment.service_fee, + confirmed: true, + tx_hash: "", + internal: payment.network_fee === 0 + } + } +} + diff --git a/src/services/main/index.ts b/src/services/main/index.ts index b582a575..cb0d4dd6 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -22,6 +22,9 @@ import { RugPullTracker } from "./rugPullTracker.js" import { AdminManager } from "./adminManager.js" import { Unlocker } from "./unlocker.js" import { defaultInvoiceExpiry } from "../storage/paymentStorage.js" +import { DebitPointer } from "../../custom-nip19.js" +import { DebitKeyType } from "../storage/entity/DebitAccess.js" +import { DebitManager, NdebitData } from "./debitManager.js" type UserOperationsSub = { id: string @@ -32,6 +35,7 @@ type UserOperationsSub = { } const appTag = "Lightning.Pub" export type NofferData = { offer: string, amount?: number } + export default class { storage: Storage lnd: LND @@ -46,6 +50,7 @@ export default class { metricsManager: MetricsManager liquidityManager: LiquidityManager liquidityProvider: LiquidityProvider + debitManager: DebitManager utils: Utils rugPullTracker: RugPullTracker unlocker: Unlocker @@ -67,6 +72,7 @@ export default class { this.productManager = new ProductManager(this.storage, this.paymentManager, this.settings) this.applicationManager = new ApplicationManager(this.storage, this.settings, this.paymentManager) this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager) + this.debitManager = new DebitManager(this.storage) } Stop() { @@ -313,6 +319,24 @@ export default class { this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) return } + + handleNip68Debit = async (pointerdata: NdebitData, event: NostrEvent) => { + const res = await this.debitManager.payNdebitInvoice(event.appId, event.pub, pointerdata) + if (!res.ok) { + const e = newNdebitResponse(JSON.stringify(res.debitRes), event) + this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) + return + } + const { op, appUserId, debitRes } = res + const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' } + const app = await this.storage.applicationStorage.GetApplication(event.appId) + const appUser = await this.storage.applicationStorage.GetApplicationUser(app, appUserId) + if (appUser.nostr_public_key) { + this.nostrSend({ type: 'app', appId: event.appId }, { type: 'content', content: JSON.stringify(message), pub: appUser.nostr_public_key }) + } + const e = newNdebitResponse(JSON.stringify(debitRes), event) + this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) + } } const codeToMessage = (code: number) => { @@ -338,3 +362,16 @@ const newNofferResponse = (content: string, event: NostrEvent): UnsignedEvent => ], } } + +const newNdebitResponse = (content: string, event: NostrEvent): UnsignedEvent => { + return { + content, + created_at: Math.floor(Date.now() / 1000), + kind: 21002, + pubkey: "", + tags: [ + ['p', event.pub], + ['e', event.id], + ], + } +} diff --git a/src/services/storage/debitStorage.ts b/src/services/storage/debitStorage.ts new file mode 100644 index 00000000..0e52ac62 --- /dev/null +++ b/src/services/storage/debitStorage.ts @@ -0,0 +1,30 @@ +import { DataSource, EntityManager } from "typeorm" +import UserStorage from './userStorage.js'; +import TransactionsQueue from "./transactionsQueue.js"; +import { DebitAccess, DebitKeyType } from "./entity/DebitAccess.js"; +export default class { + DB: DataSource | EntityManager + txQueue: TransactionsQueue + constructor(DB: DataSource | EntityManager, txQueue: TransactionsQueue) { + this.DB = DB + this.txQueue = txQueue + } + + async AddDebitAccess(appUserId: string, key: string, keyType: DebitKeyType, entityManager = this.DB) { + const entry = entityManager.getRepository(DebitAccess).create({ + app_user_id: appUserId, + key: key, + key_type: keyType + + }) + return this.txQueue.PushToQueue({ exec: async db => db.getRepository(DebitAccess).save(entry), dbTx: false }) + } + + async GetDebitAccess(appUserId: string, key: string, keyType: DebitKeyType) { + return this.DB.getRepository(DebitAccess).findOne({ where: { app_user_id: appUserId, key, key_type: keyType } }) + } + + async IncrementDebitAccess(appUserId: string, key: string, keyType: DebitKeyType, amount: number) { + return this.DB.getRepository(DebitAccess).increment({ app_user_id: appUserId, key, key_type: keyType }, 'total_debits', amount) + } +} \ No newline at end of file diff --git a/src/services/storage/entity/DebitAccess.ts b/src/services/storage/entity/DebitAccess.ts new file mode 100644 index 00000000..70d65673 --- /dev/null +++ b/src/services/storage/entity/DebitAccess.ts @@ -0,0 +1,30 @@ +import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from "typeorm" +import { User } from "./User.js" +import { Application } from "./Application.js" +import { ApplicationUser } from "./ApplicationUser.js" +export type DebitKeyType = 'simpleId' | 'pubKey' +@Entity() +@Index("unique_debit_access", ["app_user_id", "key", "key_type"], { unique: true }) +export class DebitAccess { + + @PrimaryGeneratedColumn() + serial_id: number + + @Column() + app_user_id: string + + @Column() + key: string + + @Column() + key_type: DebitKeyType + + @Column({ default: 0 }) + total_debits: number + + @CreateDateColumn() + created_at: Date + + @UpdateDateColumn() + updated_at: Date +} diff --git a/src/services/storage/index.ts b/src/services/storage/index.ts index 0988c265..a37b1de1 100644 --- a/src/services/storage/index.ts +++ b/src/services/storage/index.ts @@ -10,6 +10,7 @@ import TransactionsQueue, { TX } from "./transactionsQueue.js"; import EventsLogManager from "./eventsLog.js"; import { LiquidityStorage } from "./liquidityStorage.js"; import { StateBundler } from "./stateBundler.js"; +import DebitStorage from "./debitStorage.js" export type StorageSettings = { dbSettings: DbSettings eventLogPath: string @@ -28,6 +29,7 @@ export default class { paymentStorage: PaymentStorage metricsStorage: MetricsStorage liquidityStorage: LiquidityStorage + debitStorage: DebitStorage eventsLog: EventsLogManager stateBundler: StateBundler constructor(settings: StorageSettings) { @@ -44,6 +46,7 @@ export default class { this.paymentStorage = new PaymentStorage(this.DB, this.userStorage, this.txQueue) this.metricsStorage = new MetricsStorage(this.settings) this.liquidityStorage = new LiquidityStorage(this.DB, this.txQueue) + this.debitStorage = new DebitStorage(this.DB, this.txQueue) try { if (this.settings.dataDir) fs.mkdirSync(this.settings.dataDir) } catch (e) { } const executedMetricsMigrations = await this.metricsStorage.Connect(metricsMigrations) return { executedMigrations, executedMetricsMigrations };