debits draft (pre db)

This commit is contained in:
boufni95 2024-09-13 17:47:31 +00:00
parent 127d3a07fd
commit d2b2418e21
6 changed files with 233 additions and 1 deletions

View file

@ -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)

View file

@ -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<HandleNdebitRes> => {
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<HandleNdebitRes> => {
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
}
}
}

View file

@ -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],
],
}
}

View file

@ -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<DebitAccess>({ 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)
}
}

View file

@ -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
}

View file

@ -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 };