From f7c26ee38a72e7efe08bae122b1327cbe3df7faa Mon Sep 17 00:00:00 2001 From: boufni95 Date: Mon, 24 Nov 2025 18:41:19 +0000 Subject: [PATCH] fix payment stream --- proto/autogenerated/client.md | 15 --- proto/autogenerated/go/http_client.go | 2 - proto/autogenerated/go/types.go | 15 --- proto/autogenerated/ts/http_client.ts | 1 - proto/autogenerated/ts/nostr_client.ts | 16 ---- proto/autogenerated/ts/nostr_transport.ts | 16 ---- proto/autogenerated/ts/types.ts | 60 ------------ proto/service/methods.proto | 7 -- proto/service/structs.proto | 7 -- src/services/main/appUserManager.ts | 17 ---- src/services/main/applicationManager.ts | 30 ++++-- src/services/main/debitManager.ts | 46 +++------- src/services/main/debitTypes.ts | 6 +- src/services/main/index.ts | 1 + src/services/main/liquidityProvider.ts | 106 +++++----------------- src/services/main/paymentManager.ts | 65 ++++++------- src/services/serverMethods/index.ts | 7 -- 17 files changed, 90 insertions(+), 327 deletions(-) diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index 11a57227..42f5621b 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -275,11 +275,6 @@ The nostr server will send back a message response, and inside the body there wi - input: [PayInvoiceRequest](#PayInvoiceRequest) - output: [PayInvoiceResponse](#PayInvoiceResponse) -- PayInvoiceStream - - auth type: __User__ - - input: [PayInvoiceRequest](#PayInvoiceRequest) - - output: [InvoicePaymentStream](#InvoicePaymentStream) - - PingSubProcesses - auth type: __Metrics__ - This methods has an __empty__ __request__ body @@ -865,13 +860,6 @@ The nostr server will send back a message response, and inside the body there wi - input: [PayInvoiceRequest](#PayInvoiceRequest) - output: [PayInvoiceResponse](#PayInvoiceResponse) -- PayInvoiceStream - - auth type: __User__ - - http method: __post__ - - http route: __/api/user/invoice/pay/stream__ - - input: [PayInvoiceRequest](#PayInvoiceRequest) - - output: [InvoicePaymentStream](#InvoicePaymentStream) - - PingSubProcesses - auth type: __Metrics__ - http method: __post__ @@ -1280,9 +1268,6 @@ The nostr server will send back a message response, and inside the body there wi - __token__: _string_ - __url__: _string_ -### InvoicePaymentStream - - __update__: _[InvoicePaymentStream_update](#InvoicePaymentStream_update)_ - ### LatestBundleMetricReq - __limit__: _number_ *this field is optional diff --git a/proto/autogenerated/go/http_client.go b/proto/autogenerated/go/http_client.go index 7072488f..8c20a0a6 100644 --- a/proto/autogenerated/go/http_client.go +++ b/proto/autogenerated/go/http_client.go @@ -121,7 +121,6 @@ type Client struct { PayAddress func(req PayAddressRequest) (*PayAddressResponse, error) PayAppUserInvoice func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error) PayInvoice func(req PayInvoiceRequest) (*PayInvoiceResponse, error) - PayInvoiceStream func(req PayInvoiceRequest) (*InvoicePaymentStream, error) PingSubProcesses func() error RequestNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) ResetDebit func(req DebitOperation) error @@ -1836,7 +1835,6 @@ func NewClient(params ClientParams) *Client { } return &res, nil }, - // server streaming method: PayInvoiceStream not implemented PingSubProcesses: func() error { auth, err := params.RetrieveMetricsAuth() if err != nil { diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index 4779edde..5b9fb05d 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -353,9 +353,6 @@ type HttpCreds struct { Token string `json:"token"` Url string `json:"url"` } -type InvoicePaymentStream struct { - Update *InvoicePaymentStream_update `json:"update"` -} type LatestBundleMetricReq struct { Limit int64 `json:"limit"` } @@ -770,18 +767,6 @@ type DebitRule_rule struct { Expiration_rule *DebitExpirationRule `json:"expiration_rule"` Frequency_rule *FrequencyRule `json:"frequency_rule"` } -type InvoicePaymentStream_update_type string - -const ( - ACK InvoicePaymentStream_update_type = "ack" - DONE InvoicePaymentStream_update_type = "done" -) - -type InvoicePaymentStream_update struct { - Type InvoicePaymentStream_update_type `json:"type"` - Ack *Empty `json:"ack"` - Done *PayInvoiceResponse `json:"done"` -} type LiveDebitRequest_debit_type string const ( diff --git a/proto/autogenerated/ts/http_client.ts b/proto/autogenerated/ts/http_client.ts index 66f27b92..9c37e1b3 100644 --- a/proto/autogenerated/ts/http_client.ts +++ b/proto/autogenerated/ts/http_client.ts @@ -881,7 +881,6 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, - PayInvoiceStream: async (request: Types.PayInvoiceRequest, cb: (v:ResultError | ({ status: 'OK' }& Types.InvoicePaymentStream)) => void): Promise => { throw new Error('http streams are not supported')}, PingSubProcesses: async (): Promise => { const auth = await params.retrieveMetricsAuth() if (auth === null) throw new Error('retrieveMetricsAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_client.ts b/proto/autogenerated/ts/nostr_client.ts index 002561bb..38795d01 100644 --- a/proto/autogenerated/ts/nostr_client.ts +++ b/proto/autogenerated/ts/nostr_client.ts @@ -755,22 +755,6 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, - PayInvoiceStream: async (request: Types.PayInvoiceRequest, cb: (res:ResultError | ({ status: 'OK' }& Types.InvoicePaymentStream)) => void): Promise => { - const auth = await params.retrieveNostrUserAuth() - if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') - const nostrRequest: NostrRequest = {} - nostrRequest.body = request - subscribe(params.pubDestination, {rpcName:'PayInvoiceStream',authIdentifier:auth, ...nostrRequest }, (data) => { - if (data.status === 'ERROR' && typeof data.reason === 'string') return cb(data) - if (data.status === 'OK') { - const result = data - if(!params.checkResult) return cb({ status: 'OK', ...result }) - const error = Types.InvoicePaymentStreamValidate(result) - if (error === null) { return cb({ status: 'OK', ...result }) } else return cb({ status: 'ERROR', reason: error.message }) - } - return cb({ status: 'ERROR', reason: 'invalid response' }) - }) - }, PingSubProcesses: async (): Promise => { const auth = await params.retrieveNostrMetricsAuth() if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_transport.ts b/proto/autogenerated/ts/nostr_transport.ts index c0d08bd0..cd9e708f 100644 --- a/proto/autogenerated/ts/nostr_transport.ts +++ b/proto/autogenerated/ts/nostr_transport.ts @@ -1190,22 +1190,6 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break - case 'PayInvoiceStream': - try { - if (!methods.PayInvoiceStream) throw new Error('method: PayInvoiceStream is not implemented') - const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) - stats.guard = process.hrtime.bigint() - authCtx = authContext - const request = req.body - const error = Types.PayInvoiceRequestValidate(request) - stats.validate = process.hrtime.bigint() - if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) - methods.PayInvoiceStream({rpcName:'PayInvoiceStream', ctx:authContext , req: request ,cb: (response, err) => { - stats.handle = process.hrtime.bigint() - if (err) { logErrorAndReturnResponse(err, err.message, res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback)} else { res({status: 'OK', ...response});opts.metricsCallback([{ ...info, ...stats, ...authContext }])} - }}) - }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } - break case 'PingSubProcesses': try { if (!methods.PingSubProcesses) throw new Error('method: PingSubProcesses is not implemented') diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index 5e97fbef..f4d1764b 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -262,9 +262,6 @@ export type PayAppUserInvoice_Output = ResultError | ({ status: 'OK' } & PayInvo export type PayInvoice_Input = {rpcName:'PayInvoice', req: PayInvoiceRequest} export type PayInvoice_Output = ResultError | ({ status: 'OK' } & PayInvoiceResponse) -export type PayInvoiceStream_Input = {rpcName:'PayInvoiceStream', req: PayInvoiceRequest, cb:(res: InvoicePaymentStream, err:Error|null)=> void} -export type PayInvoiceStream_Output = ResultError | { status: 'OK' } - export type PingSubProcesses_Input = {rpcName:'PingSubProcesses'} export type PingSubProcesses_Output = ResultError | { status: 'OK' } @@ -392,7 +389,6 @@ export type ServerMethods = { PayAddress?: (req: PayAddress_Input & {ctx: UserContext }) => Promise PayAppUserInvoice?: (req: PayAppUserInvoice_Input & {ctx: AppContext }) => Promise PayInvoice?: (req: PayInvoice_Input & {ctx: UserContext }) => Promise - PayInvoiceStream?: (req: PayInvoiceStream_Input & {ctx: UserContext }) => Promise PingSubProcesses?: (req: PingSubProcesses_Input & {ctx: MetricsContext }) => Promise RequestNPubLinkingToken?: (req: RequestNPubLinkingToken_Input & {ctx: AppContext }) => Promise ResetDebit?: (req: ResetDebit_Input & {ctx: UserContext }) => Promise @@ -2054,25 +2050,6 @@ export const HttpCredsValidate = (o?: HttpCreds, opts: HttpCredsOptions = {}, pa return null } -export type InvoicePaymentStream = { - update: InvoicePaymentStream_update -} -export const InvoicePaymentStreamOptionalFields: [] = [] -export type InvoicePaymentStreamOptions = OptionsBaseMessage & { - checkOptionalsAreSet?: [] - update_Options?: InvoicePaymentStream_updateOptions -} -export const InvoicePaymentStreamValidate = (o?: InvoicePaymentStream, opts: InvoicePaymentStreamOptions = {}, path: string = 'InvoicePaymentStream::root.'): Error | null => { - if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') - if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') - - const updateErr = InvoicePaymentStream_updateValidate(o.update, opts.update_Options, `${path}.update`) - if (updateErr !== null) return updateErr - - - return null -} - export type LatestBundleMetricReq = { limit?: number } @@ -4435,43 +4412,6 @@ export const DebitRule_ruleValidate = (o?: DebitRule_rule, opts:DebitRule_ruleOp if (frequency_ruleErr !== null) return frequency_ruleErr - break - default: - return new Error(path + ': unknown type '+ stringType) - } - return null -} -export enum InvoicePaymentStream_update_type { - ACK = 'ack', - DONE = 'done', -} -export const enumCheckInvoicePaymentStream_update_type = (e?: InvoicePaymentStream_update_type): boolean => { - for (const v in InvoicePaymentStream_update_type) if (e === v) return true - return false -} -export type InvoicePaymentStream_update = - {type:InvoicePaymentStream_update_type.ACK, ack:Empty}| - {type:InvoicePaymentStream_update_type.DONE, done:PayInvoiceResponse} - -export type InvoicePaymentStream_updateOptions = { - ack_Options?: EmptyOptions - done_Options?: PayInvoiceResponseOptions -} -export const InvoicePaymentStream_updateValidate = (o?: InvoicePaymentStream_update, opts:InvoicePaymentStream_updateOptions = {}, path: string = 'InvoicePaymentStream_update::root.'): Error | null => { - if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') - const stringType: string = o.type - switch (o.type) { - case InvoicePaymentStream_update_type.ACK: - const ackErr = EmptyValidate(o.ack, opts.ack_Options, `${path}.ack`) - if (ackErr !== null) return ackErr - - - break - case InvoicePaymentStream_update_type.DONE: - const doneErr = PayInvoiceResponseValidate(o.done, opts.done_Options, `${path}.done`) - if (doneErr !== null) return doneErr - - break default: return new Error(path + ': unknown type '+ stringType) diff --git a/proto/service/methods.proto b/proto/service/methods.proto index eb253572..4cff7379 100644 --- a/proto/service/methods.proto +++ b/proto/service/methods.proto @@ -517,13 +517,6 @@ service LightningPub { option (nostr) = true; } - rpc PayInvoiceStream(structs.PayInvoiceRequest) returns (stream structs.InvoicePaymentStream){ - option (auth_type) = "User"; - option (http_method) = "post"; - option (http_route) = "/api/user/invoice/pay/stream"; - option (nostr) = true; - } - rpc GetPaymentState(structs.GetPaymentStateRequest) returns (structs.PaymentState){ option (auth_type) = "User"; option (http_method) = "post"; diff --git a/proto/service/structs.proto b/proto/service/structs.proto index c33e812a..78746db4 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -479,13 +479,6 @@ message PayInvoiceResponse{ int64 latest_balance = 6; } -message InvoicePaymentStream { - oneof update { - Empty ack = 1; - PayInvoiceResponse done = 2; - } -} - message GetPaymentStateRequest{ string invoice = 1; diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index 44ee5467..f8d41b16 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -111,23 +111,6 @@ export default class { }) } - async PayInvoiceStream(ctx: Types.UserContext, req: Types.PayInvoiceRequest, cb: (res: Types.InvoicePaymentStream, err: Error | null) => void) { - return await this.applicationManager.PayAppUserInvoiceStream(ctx.app_id, { - amount: req.amount, - invoice: req.invoice, - user_identifier: ctx.app_user_id, - debit_npub: req.debit_npub, - fee_limit_sats: req.fee_limit_sats - }, cb) - } - async PayAddress(ctx: Types.UserContext, req: Types.PayInvoiceRequest): Promise { - return this.applicationManager.PayAppUserInvoice(ctx.app_id, { - amount: req.amount, - invoice: req.invoice, - user_identifier: ctx.app_user_id - }) - } - async EnrollMessagingToken(ctx: Types.UserContext, req: Types.MessagingToken): Promise { const app = await this.storage.applicationStorage.GetApplication(ctx.app_id); const user = await this.storage.applicationStorage.GetApplicationUser(app, ctx.app_user_id); diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index 94729dd4..d2cc56fc 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -10,6 +10,7 @@ import { Application } from '../storage/entity/Application.js' import { ZapInfo } from '../storage/entity/UserReceivingInvoice.js' import { nofferEncode, ndebitEncode, OfferPriceType, nmanageEncode } from '@shocknet/clink-sdk' import SettingsManager from './settingsManager.js' +import { NostrSend, SendData, SendInitiator } from '../nostr/handler.js' const TOKEN_EXPIRY_TIME = 2 * 60 * 1000 // 2 minutes, in milliseconds type NsecLinkingData = { @@ -17,7 +18,7 @@ type NsecLinkingData = { expiry: number } export default class { - + _nostrSend: NostrSend | null = null storage: Storage settings: SettingsManager paymentManager: PaymentManager @@ -33,6 +34,17 @@ export default class { this.StartLinkingTokenInterval() } + attachNostrSend = (nostrSend: NostrSend) => { + this._nostrSend = nostrSend + } + + nostrSend: NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => { + if (!this._nostrSend) { + throw new Error("No nostrSend attached") + } + this._nostrSend(initiator, data, relays) + } + StartLinkingTokenInterval() { this.linkingTokenInterval = setInterval(() => { const now = Date.now(); @@ -234,15 +246,21 @@ export default class { async PayAppUserInvoice(appId: string, req: Types.PayAppUserInvoiceRequest): Promise { const app = await this.storage.applicationStorage.GetApplication(appId) const appUser = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) - const paid = await this.paymentManager.PayInvoice(appUser.user.user_id, req, app) + const paid = await this.paymentManager.PayInvoice(appUser.user.user_id, req, app, pendingOp => { + this.notifyAppUserPayment(appUser, pendingOp) + }) + this.notifyAppUserPayment(appUser, paid.operation) getLogger({ appName: app.name })(appUser.identifier, "invoice paid", paid.amount_paid, "sats") return paid } - async PayAppUserInvoiceStream(appId: string, req: Types.PayAppUserInvoiceRequest, cb: (res: Types.InvoicePaymentStream, err: Error | null) => void) { - const app = await this.storage.applicationStorage.GetApplication(appId) - const appUser = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) - return this.paymentManager.PayInvoiceStream(appUser.user.user_id, req, app, cb) + notifyAppUserPayment = (appUser: ApplicationUser, op: Types.UserOperation) => { + const balance = appUser.user.balance_sats + const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = + { operation: op, requestId: "GetLiveUserOperations", status: 'OK', latest_balance: balance } + if (appUser.nostr_public_key) { // TODO - fix before support for http streams + this.nostrSend({ type: 'app', appId: appUser.application.app_id }, { type: 'content', content: JSON.stringify(message), pub: appUser.nostr_public_key }) + } } async SendAppUserToAppUserPayment(appId: string, req: Types.SendAppUserToAppUserPaymentRequest): Promise { diff --git a/src/services/main/debitManager.ts b/src/services/main/debitManager.ts index 9b4ca69f..11270629 100644 --- a/src/services/main/debitManager.ts +++ b/src/services/main/debitManager.ts @@ -34,6 +34,7 @@ export class DebitManager { attachNostrSend = (nostrSend: NostrSend) => { this._nostrSend = nostrSend } + nostrSend: NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => { if (!this._nostrSend) { throw new Error("No nostrSend attached") @@ -87,11 +88,9 @@ export class DebitManager { paySingleInvoice = async (ctx: Types.UserContext, { invoice, npub, request_id }: { invoice: string, npub: string, request_id: string }) => { try { this.logger("🔍 [DEBIT REQUEST] Paying single invoice") - const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) - const appUser = await this.storage.applicationStorage.GetApplicationUser(app, ctx.app_user_id) - const { op, payment } = await this.sendDebitPayment(ctx.app_id, ctx.app_user_id, npub, invoice) + const { payment } = await this.sendDebitPayment(ctx.app_id, ctx.app_user_id, npub, invoice) const debitRes: NdebitSuccess = { res: 'ok', preimage: payment.preimage } - this.notifyPaymentSuccess(appUser, debitRes, op, { appId: ctx.app_id, pub: npub, id: request_id }) + this.notifyPaymentSuccess(debitRes, { appId: ctx.app_id, pub: npub, id: request_id }) } catch (e: any) { this.logger("❌ [DEBIT REQUEST] Error in single invoice payment") this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: npub, id: request_id, appId: ctx.app_id }) @@ -124,9 +123,9 @@ export class DebitManager { const appUser = await this.storage.applicationStorage.GetApplicationUser(app, ctx.app_user_id) this.validateAccessRules(access, app, appUser) this.logger("🔍 [DEBIT REQUEST] Sending debit payment") - const { op, payment } = await this.sendDebitPayment(ctx.app_id, ctx.app_user_id, npub, invoice) + const { payment } = await this.sendDebitPayment(ctx.app_id, ctx.app_user_id, npub, invoice) const debitRes: NdebitSuccess = { res: 'ok', preimage: payment.preimage } - this.notifyPaymentSuccess(appUser, debitRes, op, { appId: ctx.app_id, pub: npub, id: request_id }) + this.notifyPaymentSuccess(debitRes, { appId: ctx.app_id, pub: npub, id: request_id }) } catch (e: any) { this.logger("❌ [DEBIT REQUEST] Error in debit authorization") this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: npub, id: request_id, appId: ctx.app_id }) @@ -157,8 +156,8 @@ export class DebitManager { this.handleAuthRequired(pointerdata, event, res) return } - const { op, debitRes } = res - this.notifyPaymentSuccess(appUser, debitRes, op, event) + const { debitRes } = res + this.notifyPaymentSuccess(debitRes, event) } handleAuthRequired = (data: NdebitData, event: NostrEvent, res: AuthRequiredRes) => { @@ -170,13 +169,7 @@ export class DebitManager { this.nostrSend({ type: 'app', appId: event.appId }, { type: 'content', content: JSON.stringify(message), pub: res.appUser.nostr_public_key }) } - notifyPaymentSuccess = (appUser: ApplicationUser, debitRes: NdebitSuccess, op: Types.UserOperation, event: { pub: string, id: string, appId: string }) => { - const balance = appUser.user.balance_sats - const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = - { operation: op, requestId: "GetLiveUserOperations", status: 'OK', latest_balance: balance } - if (appUser.nostr_public_key) { // TODO - fix before support for http streams - this.nostrSend({ type: 'app', appId: event.appId }, { type: 'content', content: JSON.stringify(message), pub: appUser.nostr_public_key }) - } + notifyPaymentSuccess = (debitRes: NdebitSuccess, event: { pub: string, id: string, appId: string }) => { this.sendDebitResponse(debitRes, event) } @@ -290,15 +283,14 @@ export class DebitManager { } await this.validateAccessRules(authorization, app, appUser) this.logger("🔍 [DEBIT REQUEST] Sending requested debit payment") - const { op, payment } = await this.sendDebitPayment(appId, appUserId, requestorPub, bolt11) - return { status: 'invoicePaid', op, app, appUser, debitRes: { res: 'ok', preimage: payment.preimage } } + const { payment } = await this.sendDebitPayment(appId, appUserId, requestorPub, bolt11) + return { status: 'invoicePaid', app, appUser, debitRes: { res: 'ok', preimage: payment.preimage } } } sendDebitPayment = async (appId: string, appUserId: string, requestorPub: string, bolt11: string) => { const payment = await this.applicationManager.PayAppUserInvoice(appId, { amount: 0, invoice: bolt11, user_identifier: appUserId, debit_npub: requestorPub }) await this.storage.debitStorage.IncrementDebitAccess(appUserId, requestorPub, payment.amount_paid + payment.service_fee + payment.network_fee) - const op = this.newPaymentOperation(payment, bolt11) - return { payment, op } + return { payment } } validateAccessRules = async (access: DebitAccess, app: Application, appUser: ApplicationUser): Promise => { @@ -329,21 +321,5 @@ export class DebitManager { } return true } - - newPaymentOperation = (payment: Types.PayInvoiceResponse, bolt11: string) => { - return { - amount: payment.amount_paid, - paidAtUnix: Math.floor(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/debitTypes.ts b/src/services/main/debitTypes.ts index 83aca293..719de95f 100644 --- a/src/services/main/debitTypes.ts +++ b/src/services/main/debitTypes.ts @@ -1,9 +1,9 @@ import * as Types from "../../../proto/autogenerated/ts/types.js"; -import { DebitAccessRules } from '../storage/entity/DebitAccess.js'; +import { DebitAccessRules } from '../storage/entity/DebitAccess.js'; import { Application } from '../storage/entity/Application.js'; import { ApplicationUser } from '../storage/entity/ApplicationUser.js'; import { UnsignedEvent } from 'nostr-tools'; -import { NdebitFailure, NdebitSuccess, RecurringDebitTimeUnit } from "@shocknet/clink-sdk"; +import { NdebitFailure, NdebitSuccess, RecurringDebitTimeUnit } from "@shocknet/clink-sdk"; export const expirationRuleName = 'expiration' export const frequencyRuleName = 'frequency' @@ -96,7 +96,7 @@ export const nofferErrors = { } export type AuthRequiredRes = { status: 'authRequired', liveDebitReq: Types.LiveDebitRequest, app: Application, appUser: ApplicationUser } export type HandleNdebitRes = { status: 'fail', debitRes: NdebitFailure } - | { status: 'invoicePaid', op: Types.UserOperation, app: Application, appUser: ApplicationUser, debitRes: NdebitSuccess } + | { status: 'invoicePaid', app: Application, appUser: ApplicationUser, debitRes: NdebitSuccess } | AuthRequiredRes | { status: 'authOk', debitRes: NdebitSuccess } diff --git a/src/services/main/index.ts b/src/services/main/index.ts index d691d94c..2afe09ff 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -115,6 +115,7 @@ export default class { this.offerManager.attachNostrSend(f) this.managementManager.attachNostrSend(f) this.utils.attachNostrSend(f) + this.applicationManager.attachNostrSend(f) //this.webRTC.attachNostrSend(f) } diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 7335b78c..43f20bdc 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -9,12 +9,6 @@ import Storage from '../storage/index.js' import SettingsManager from './settingsManager.js' import { LiquiditySettings } from './settings.js' export type LiquidityRequest = { action: 'spend' | 'receive', amount: number } -/* export type CumulativeFees = { - networkFeeBps: number; - networkFeeFixed: number; - serviceFeeBps: number; -} -export type BeaconData = { type: 'service', name: string, avatarUrl?: string, nextRelay?: string, fees?: CumulativeFees } */ export type nostrCallback = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void } export class LiquidityProvider { getSettings: () => LiquiditySettings @@ -34,12 +28,10 @@ export class LiquidityProvider { utils: Utils pendingPayments: Record = {} feesCache: { networkFeeBps: number, networkFeeFixed: number, serviceFeeBps: number } | null = null - // unreachableSince: number | null = null - // reconnecting = false lastSeenBeacon = 0 latestReceivedBalance = 0 incrementProviderBalance: (balance: number) => Promise - // rand = Math.random() + pendingPaymentsAck: Record = {} // make the sub process accept client constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise) { this.utils = utils @@ -79,10 +71,6 @@ export class LiquidityProvider { } IsReady = () => { - /* const elapsed = this.unreachableSince ? Date.now() - this.unreachableSince : 0 - if (!this.reconnecting && elapsed > 1000 * 60 * 5) { - this.GetUserState().then(() => this.reconnecting = false) - } */ const seenInPast2Minutes = Date.now() - this.lastSeenBeacon < 1000 * 60 * 2 return this.ready && !this.getSettings().disableLiquidityProvider && seenInPast2Minutes } @@ -125,6 +113,9 @@ export class LiquidityProvider { await this.invoicePaidCb(res.operation.identifier, res.operation.amount, 'provider') this.incrementProviderBalance(res.operation.amount) this.latestReceivedBalance = res.latest_balance + if (!res.operation.inbound && !res.operation.confirmed) { + delete this.pendingPaymentsAck[res.operation.identifier] + } } catch (err: any) { this.log("error processing incoming invoice", err.message) } @@ -168,69 +159,36 @@ export class LiquidityProvider { return Math.floor((this.latestReceivedBalance - networkFeeFixed) / div) } - /* GetLatestMaxWithdrawable = async () => { - if (!this.IsReady()) { - return 0 - } - const res = await this.GetUserState() - if (res.status === 'ERROR') { - this.log("error getting user info", res.reason) - return 0 - } - return res.max_withdrawable - } */ - GetLatestBalance = () => { if (!this.IsReady()) { return 0 } return this.latestReceivedBalance - /* const res = await this.GetUserState() - if (res.status === 'ERROR') { - this.log("error getting user info", res.reason) - return 0 - } - return res.balance */ } GetPendingBalance = async () => { return Object.values(this.pendingPayments).reduce((a, b) => a + b, 0) } - CalculateExpectedFeeLimit = (amount: number, info: Types.UserInfo) => { - const serviceFeeRate = info.service_fee_bps / 10000 + CalculateExpectedFeeLimit = (amount: number) => { + const fees = this.GetFees() + const serviceFeeRate = fees.serviceFeeBps / 10000 const serviceFee = Math.ceil(serviceFeeRate * amount) - const networkMaxFeeRate = info.network_max_fee_bps / 10000 - const networkFeeLimit = Math.ceil(amount * networkMaxFeeRate + info.network_max_fee_fixed) + const networkMaxFeeRate = fees.networkFeeBps / 10000 + const networkFeeLimit = Math.ceil(amount * networkMaxFeeRate + fees.networkFeeFixed) return serviceFee + networkFeeLimit } - GetExpectedFeeLimit = async (amount: number) => { - if (!this.IsReady()) { - throw new Error("liquidity provider is not ready yet, disabled or unreachable") - } - const state = await this.GetUserState() - if (state.status === 'ERROR') { - throw new Error(state.reason) - } - return this.CalculateExpectedFeeLimit(amount, state) - } - - CanProviderHandle = async (req: LiquidityRequest): Promise => { + CanProviderHandle = async (req: LiquidityRequest): Promise => { if (!this.IsReady()) { this.log("provider is not ready") return false } - const state = await this.GetUserState() - if (state.status === 'ERROR') { - this.log("error getting user state", state.reason) - return false - } - const maxW = state.max_withdrawable + const maxW = this.GetMaxWithdrawable() if (req.action === 'spend' && maxW < req.amount) { return false } - return state + return true } AddInvoice = async (amount: number, memo: string, from: 'user' | 'system', expiry: number) => { @@ -257,38 +215,22 @@ export class LiquidityProvider { if (!this.IsReady()) { throw new Error("liquidity provider is not ready yet, disabled or unreachable") } - /* const userInfo = await this.GetUserState() - if (userInfo.status === 'ERROR') { - throw new Error(userInfo.reason) - } */ - const feeLimitToUse = feeLimit ? feeLimit : await this.GetExpectedFeeLimit(decodedAmount) - this.pendingPayments[invoice] = decodedAmount + feeLimitToUse //this.CalculateExpectedFeeLimit(decodedAmount, userInfo) - let acked = false + const feeLimitToUse = feeLimit ? feeLimit : this.CalculateExpectedFeeLimit(decodedAmount) + this.pendingPayments[invoice] = decodedAmount + feeLimitToUse const timeout = setTimeout(() => { - this.log("10 seconds passed, still waiting for ack") - this.GetUserState() + if (!this.pendingPaymentsAck[invoice]) { + return + } + this.log("10 seconds passed without a payment ack, locking provider until the next beacon") + this.lastSeenBeacon = 0 }, 1000 * 10) - const res = await new Promise((resolve, reject) => { - this.client.PayInvoiceStream({ invoice, amount: 0, fee_limit_sats: feeLimitToUse }, (resp) => { - if (resp.status === 'ERROR') { - this.log("error paying invoice", resp.reason) - reject(new Error(resp.reason)) - return - } - if (resp.update.type === Types.InvoicePaymentStream_update_type.ACK) { - this.log("acked") - clearTimeout(timeout) - acked = true - return - } - resolve(resp.update.done) - }) - }) - //const res = await this.client.PayInvoice({ invoice, amount: 0, fee_limit_sats: feeLimitToUse }) - /* if (res.status === 'ERROR') { + this.pendingPaymentsAck[invoice] = true + const res = await this.client.PayInvoice({ invoice, amount: 0, fee_limit_sats: feeLimitToUse }) + delete this.pendingPaymentsAck[invoice] + if (res.status === 'ERROR') { this.log("error paying invoice", res.reason) throw new Error(res.reason) - } */ + } const totalPaid = res.amount_paid + res.network_fee + res.service_fee this.incrementProviderBalance(-totalPaid).then(() => { delete this.pendingPayments[invoice] }) this.latestReceivedBalance = res.latest_balance diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 4019167b..280c014f 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -251,27 +251,6 @@ export default class { const div = 1 + (totalBps / 10000) const max = Math.floor((balance - networkFeeFixed) / div) return { max, serviceFeeBps, networkFeeBps, networkFeeFixed } - - /* if (this.lnd.liquidProvider.IsReady()) { - const fees = this.lnd.liquidProvider.GetFees() - const providerServiceFee = fees.serviceFeeBps / 10000 - const providerNetworkFee = fees.networkFeeBps / 10000 - const div = 1 + serviceFee + providerServiceFee + providerNetworkFee - const max = Math.floor((balance - fees.networkFeeFixed) / div) - const networkFeeBps = fees.networkFeeBps + fees.serviceFeeBps - return { max, serviceFeeBps: outgoingAppUserInvoiceFeeBps, networkFeeBps, networkFeeFixed: fees.networkFeeFixed } - } - const { feeFixedLimit, feeRateLimit, feeRateBps } = this.settings.getSettings().lndSettings - const div = 1 + serviceFee + feeRateLimit - const max = Math.floor((balance - feeFixedLimit) / div) - return { max, serviceFeeBps: outgoingAppUserInvoiceFeeBps, networkFeeBps: feeRateBps, networkFeeFixed: feeFixedLimit } */ - /* let maxWithinServiceFee = 0 - if (appUser) { - maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee))) - } else { - maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.getSettings().serviceFeeSettings.outgoingAppInvoiceFee))) - } - return this.lnd.GetMaxWithinLimit(maxWithinServiceFee) */ } async DecodeInvoice(req: Types.DecodeInvoiceRequest): Promise { const decoded = await this.lnd.DecodeInvoice(req.invoice) @@ -280,17 +259,7 @@ export default class { } } - async PayInvoiceStream(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application, cb: (res: Types.InvoicePaymentStream, err: Error | null) => void) { - const ack = () => cb({ update: { type: Types.InvoicePaymentStream_update_type.ACK, ack: {} } }, null) - try { - const paid = await this.PayInvoice(userId, req, linkedApplication, ack) - cb({ update: { type: Types.InvoicePaymentStream_update_type.DONE, done: paid } }, null) - } catch (err: any) { - cb({ update: { type: Types.InvoicePaymentStream_update_type.ACK, ack: {} } }, err) - } - } - - async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application, ack?: () => void): Promise { + async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application, ack?: (op: Types.UserOperation) => void): Promise { await this.watchDog.PaymentRequested() const maybeBanned = await this.storage.userStorage.GetUser(userId) if (maybeBanned.locked) { @@ -328,13 +297,16 @@ export default class { } const user = await this.storage.userStorage.GetUser(userId) this.storage.eventsLog.LogEvent({ type: 'invoice_payment', userId, appId: linkedApplication.app_id, appUserId: "", balance: user.balance_sats, data: req.invoice, amount: payAmount }) + const opId = `${Types.UserOperationType.OUTGOING_INVOICE}-${paymentInfo.serialId}` + const operation = this.newInvoicePaymentOperation({ invoice: req.invoice, opId, amount: paymentInfo.amtPaid, networkFee: paymentInfo.networkFee, serviceFee: serviceFee, confirmed: true }) return { preimage: paymentInfo.preimage, amount_paid: paymentInfo.amtPaid, - operation_id: `${Types.UserOperationType.OUTGOING_INVOICE}-${paymentInfo.serialId}`, + operation_id: opId, network_fee: paymentInfo.networkFee, service_fee: serviceFee, - latest_balance: user.balance_sats + latest_balance: user.balance_sats, + operation } } @@ -353,7 +325,7 @@ export default class { return { use: 'provider', feeLimit: inputLimit || use.feeLimit } } - async PayExternalInvoice(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, amountForLnd: number, feeLimit?: number }, linkedApplication: Application, debitNpub?: string, ack?: () => void) { + async PayExternalInvoice(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, amountForLnd: number, feeLimit?: number }, linkedApplication: Application, debitNpub?: string, ack?: (op: Types.UserOperation) => void) { if (this.settings.getSettings().serviceSettings.disableExternalPayments) { throw new Error("something went wrong sending payment, please try again later") } @@ -379,7 +351,9 @@ export default class { return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: routingFeeLimit }, linkedApplication, provider, tx, debitNpub) }, "payment started") this.log("ready to pay") - ack?.() + const opId = `${Types.UserOperationType.OUTGOING_INVOICE}-${pendingPayment.serial_id}` + const op = this.newInvoicePaymentOperation({ invoice, opId, amount: payAmount, networkFee: routingFeeLimit, serviceFee: serviceFee, confirmed: false }) + ack?.(op) try { const payment = await this.lnd.PayInvoice(invoice, amountForLnd, routingFeeLimit, payAmount, { useProvider: use === 'provider', from: 'user' }, index => { this.storage.paymentStorage.SetExternalPaymentIndex(pendingPayment.serial_id, index) @@ -414,10 +388,8 @@ export default class { } catch (err) { await this.storage.userStorage.IncrementUserBalance(userId, totalAmountToDecrement, "internal_payment_refund:" + internalInvoice.invoice) this.utils.stateBundler.AddTxPointFailed('paidAnInvoice', payAmount, { used: 'internal', from: 'user' }, linkedApplication.app_id) - throw err } - } @@ -732,6 +704,23 @@ export default class { } } + newInvoicePaymentOperation = (opInfo: { invoice: string, opId: string, amount: number, networkFee: number, serviceFee: number, confirmed: boolean }): Types.UserOperation => { + const { invoice, opId, amount, networkFee, serviceFee, confirmed } = opInfo + return { + amount: amount, + paidAtUnix: Math.floor(Date.now() / 1000), + inbound: false, + type: Types.UserOperationType.OUTGOING_INVOICE, + identifier: invoice, + operationId: opId, + network_fee: networkFee, + service_fee: serviceFee, + confirmed, + tx_hash: "", + internal: networkFee === 0 + } + } + async GetPaymentState(userId: string, req: Types.GetPaymentStateRequest): Promise { const user = await this.storage.userStorage.GetUser(userId) if (user.locked) { diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index 16051c68..c9f92932 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -158,13 +158,6 @@ export default (mainHandler: Main): Types.ServerMethods => { if (err != null) throw new Error(err.message) return mainHandler.appUserManager.PayInvoice(ctx, req) }, - PayInvoiceStream: async ({ ctx, req, cb }) => { - const err = Types.PayInvoiceRequestValidate(req, { - invoice_CustomCheck: invoice => invoice !== '' - }) - if (err != null) throw new Error(err.message) - mainHandler.appUserManager.PayInvoiceStream(ctx, req, cb) - }, GetLnurlWithdrawLink: ({ ctx }) => mainHandler.paymentManager.GetLnurlWithdrawLink(ctx), GetLnurlWithdrawInfo: async ({ ctx, query }) => { if (!query.k1) {