debits draft (pre db)
This commit is contained in:
parent
127d3a07fd
commit
d2b2418e21
6 changed files with 233 additions and 1 deletions
|
|
@ -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)
|
||||
|
|
|
|||
94
src/services/main/debitManager.ts
Normal file
94
src/services/main/debitManager.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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],
|
||||
],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
src/services/storage/debitStorage.ts
Normal file
30
src/services/storage/debitStorage.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
30
src/services/storage/entity/DebitAccess.ts
Normal file
30
src/services/storage/entity/DebitAccess.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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 };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue