diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index 5bad6c03..11a57227 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -1104,6 +1104,13 @@ The nostr server will send back a message response, and inside the body there wi - __nostr_pub__: _string_ - __user_identifier__: _string_ +### BeaconData + - __avatarUrl__: _string_ *this field is optional + - __fees__: _[CumulativeFees](#CumulativeFees)_ *this field is optional + - __name__: _string_ + - __nextRelay__: _string_ *this field is optional + - __type__: _string_ + ### BundleData - __available_chunks__: ARRAY of: _number_ - __base_64_data__: ARRAY of: _string_ @@ -1149,6 +1156,11 @@ The nostr server will send back a message response, and inside the body there wi ### CreateOneTimeInviteLinkResponse - __invitation_link__: _string_ +### CumulativeFees + - __networkFeeBps__: _number_ + - __networkFeeFixed__: _number_ + - __serviceFeeBps__: _number_ + ### DebitAuthorization - __authorized__: _boolean_ - __debit_id__: _string_ @@ -1290,6 +1302,7 @@ The nostr server will send back a message response, and inside the body there wi - __request_id__: _string_ ### LiveUserOperation + - __latest_balance__: _number_ - __operation__: _[UserOperation](#UserOperation)_ ### LndChannels @@ -1487,6 +1500,7 @@ The nostr server will send back a message response, and inside the body there wi ### PayInvoiceResponse - __amount_paid__: _number_ + - __latest_balance__: _number_ - __network_fee__: _number_ - __operation_id__: _string_ - __preimage__: _string_ diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index 7a744c79..4779edde 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -177,6 +177,13 @@ type BannedAppUser struct { Nostr_pub string `json:"nostr_pub"` User_identifier string `json:"user_identifier"` } +type BeaconData struct { + Avatarurl string `json:"avatarUrl"` + Fees *CumulativeFees `json:"fees"` + Name string `json:"name"` + Nextrelay string `json:"nextRelay"` + Type string `json:"type"` +} type BundleData struct { Available_chunks []int64 `json:"available_chunks"` Base_64_data []string `json:"base_64_data"` @@ -222,6 +229,11 @@ type CreateOneTimeInviteLinkRequest struct { type CreateOneTimeInviteLinkResponse struct { Invitation_link string `json:"invitation_link"` } +type CumulativeFees struct { + Networkfeebps int64 `json:"networkFeeBps"` + Networkfeefixed int64 `json:"networkFeeFixed"` + Servicefeebps int64 `json:"serviceFeeBps"` +} type DebitAuthorization struct { Authorized bool `json:"authorized"` Debit_id string `json:"debit_id"` @@ -363,7 +375,8 @@ type LiveManageRequest struct { Request_id string `json:"request_id"` } type LiveUserOperation struct { - Operation *UserOperation `json:"operation"` + Latest_balance int64 `json:"latest_balance"` + Operation *UserOperation `json:"operation"` } type LndChannels struct { Open_channels []OpenChannel `json:"open_channels"` @@ -559,11 +572,12 @@ type PayInvoiceRequest struct { Invoice string `json:"invoice"` } type PayInvoiceResponse struct { - Amount_paid int64 `json:"amount_paid"` - Network_fee int64 `json:"network_fee"` - Operation_id string `json:"operation_id"` - Preimage string `json:"preimage"` - Service_fee int64 `json:"service_fee"` + Amount_paid int64 `json:"amount_paid"` + Latest_balance int64 `json:"latest_balance"` + Network_fee int64 `json:"network_fee"` + Operation_id string `json:"operation_id"` + Preimage string `json:"preimage"` + Service_fee int64 `json:"service_fee"` } type PayerData struct { Data map[string]string `json:"data"` diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index 921f9a82..5e97fbef 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -983,6 +983,48 @@ export const BannedAppUserValidate = (o?: BannedAppUser, opts: BannedAppUserOpti return null } +export type BeaconData = { + avatarUrl?: string + fees?: CumulativeFees + name: string + nextRelay?: string + type: string +} +export type BeaconDataOptionalField = 'avatarUrl' | 'fees' | 'nextRelay' +export const BeaconDataOptionalFields: BeaconDataOptionalField[] = ['avatarUrl', 'fees', 'nextRelay'] +export type BeaconDataOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: BeaconDataOptionalField[] + avatarUrl_CustomCheck?: (v?: string) => boolean + fees_Options?: CumulativeFeesOptions + name_CustomCheck?: (v: string) => boolean + nextRelay_CustomCheck?: (v?: string) => boolean + type_CustomCheck?: (v: string) => boolean +} +export const BeaconDataValidate = (o?: BeaconData, opts: BeaconDataOptions = {}, path: string = 'BeaconData::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') + + if ((o.avatarUrl || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('avatarUrl')) && typeof o.avatarUrl !== 'string') return new Error(`${path}.avatarUrl: is not a string`) + if (opts.avatarUrl_CustomCheck && !opts.avatarUrl_CustomCheck(o.avatarUrl)) return new Error(`${path}.avatarUrl: custom check failed`) + + if (typeof o.fees === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('fees')) { + const feesErr = CumulativeFeesValidate(o.fees, opts.fees_Options, `${path}.fees`) + if (feesErr !== null) return feesErr + } + + + if (typeof o.name !== 'string') return new Error(`${path}.name: is not a string`) + if (opts.name_CustomCheck && !opts.name_CustomCheck(o.name)) return new Error(`${path}.name: custom check failed`) + + if ((o.nextRelay || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('nextRelay')) && typeof o.nextRelay !== 'string') return new Error(`${path}.nextRelay: is not a string`) + if (opts.nextRelay_CustomCheck && !opts.nextRelay_CustomCheck(o.nextRelay)) return new Error(`${path}.nextRelay: custom check failed`) + + if (typeof o.type !== 'string') return new Error(`${path}.type: is not a string`) + if (opts.type_CustomCheck && !opts.type_CustomCheck(o.type)) return new Error(`${path}.type: custom check failed`) + + return null +} + export type BundleData = { available_chunks: number[] base_64_data: string[] @@ -1256,6 +1298,34 @@ export const CreateOneTimeInviteLinkResponseValidate = (o?: CreateOneTimeInviteL return null } +export type CumulativeFees = { + networkFeeBps: number + networkFeeFixed: number + serviceFeeBps: number +} +export const CumulativeFeesOptionalFields: [] = [] +export type CumulativeFeesOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + networkFeeBps_CustomCheck?: (v: number) => boolean + networkFeeFixed_CustomCheck?: (v: number) => boolean + serviceFeeBps_CustomCheck?: (v: number) => boolean +} +export const CumulativeFeesValidate = (o?: CumulativeFees, opts: CumulativeFeesOptions = {}, path: string = 'CumulativeFees::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') + + if (typeof o.networkFeeBps !== 'number') return new Error(`${path}.networkFeeBps: is not a number`) + if (opts.networkFeeBps_CustomCheck && !opts.networkFeeBps_CustomCheck(o.networkFeeBps)) return new Error(`${path}.networkFeeBps: custom check failed`) + + if (typeof o.networkFeeFixed !== 'number') return new Error(`${path}.networkFeeFixed: is not a number`) + if (opts.networkFeeFixed_CustomCheck && !opts.networkFeeFixed_CustomCheck(o.networkFeeFixed)) return new Error(`${path}.networkFeeFixed: custom check failed`) + + if (typeof o.serviceFeeBps !== 'number') return new Error(`${path}.serviceFeeBps: is not a number`) + if (opts.serviceFeeBps_CustomCheck && !opts.serviceFeeBps_CustomCheck(o.serviceFeeBps)) return new Error(`${path}.serviceFeeBps: custom check failed`) + + return null +} + export type DebitAuthorization = { authorized: boolean debit_id: string @@ -2112,17 +2182,22 @@ export const LiveManageRequestValidate = (o?: LiveManageRequest, opts: LiveManag } export type LiveUserOperation = { + latest_balance: number operation: UserOperation } export const LiveUserOperationOptionalFields: [] = [] export type LiveUserOperationOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] + latest_balance_CustomCheck?: (v: number) => boolean operation_Options?: UserOperationOptions } export const LiveUserOperationValidate = (o?: LiveUserOperation, opts: LiveUserOperationOptions = {}, path: string = 'LiveUserOperation::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') + if (typeof o.latest_balance !== 'number') return new Error(`${path}.latest_balance: is not a number`) + if (opts.latest_balance_CustomCheck && !opts.latest_balance_CustomCheck(o.latest_balance)) return new Error(`${path}.latest_balance: custom check failed`) + const operationErr = UserOperationValidate(o.operation, opts.operation_Options, `${path}.operation`) if (operationErr !== null) return operationErr @@ -3287,6 +3362,7 @@ export const PayInvoiceRequestValidate = (o?: PayInvoiceRequest, opts: PayInvoic export type PayInvoiceResponse = { amount_paid: number + latest_balance: number network_fee: number operation_id: string preimage: string @@ -3296,6 +3372,7 @@ export const PayInvoiceResponseOptionalFields: [] = [] export type PayInvoiceResponseOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] amount_paid_CustomCheck?: (v: number) => boolean + latest_balance_CustomCheck?: (v: number) => boolean network_fee_CustomCheck?: (v: number) => boolean operation_id_CustomCheck?: (v: string) => boolean preimage_CustomCheck?: (v: string) => boolean @@ -3308,6 +3385,9 @@ export const PayInvoiceResponseValidate = (o?: PayInvoiceResponse, opts: PayInvo if (typeof o.amount_paid !== 'number') return new Error(`${path}.amount_paid: is not a number`) if (opts.amount_paid_CustomCheck && !opts.amount_paid_CustomCheck(o.amount_paid)) return new Error(`${path}.amount_paid: custom check failed`) + if (typeof o.latest_balance !== 'number') return new Error(`${path}.latest_balance: is not a number`) + if (opts.latest_balance_CustomCheck && !opts.latest_balance_CustomCheck(o.latest_balance)) return new Error(`${path}.latest_balance: custom check failed`) + if (typeof o.network_fee !== 'number') return new Error(`${path}.network_fee: is not a number`) if (opts.network_fee_CustomCheck && !opts.network_fee_CustomCheck(o.network_fee)) return new Error(`${path}.network_fee: custom check failed`) diff --git a/proto/service/structs.proto b/proto/service/structs.proto index 0676a461..c33e812a 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -476,6 +476,7 @@ message PayInvoiceResponse{ string operation_id = 3; int64 service_fee = 4; int64 network_fee = 5; + int64 latest_balance = 6; } message InvoicePaymentStream { @@ -613,6 +614,7 @@ message GetProductBuyLinkResponse { message LiveUserOperation { UserOperation operation = 1; + int64 latest_balance = 2; } message MigrationUpdate { optional ClosureMigration closure = 1; @@ -833,3 +835,18 @@ message MessagingToken { string device_id = 1; string firebase_messaging_token = 2; } + + +message CumulativeFees { + int64 networkFeeBps = 1; + int64 networkFeeFixed = 2; + int64 serviceFeeBps = 3; +} + +message BeaconData { + string type = 1; + string name = 2; + optional string avatarUrl = 3; + optional string nextRelay = 4; + optional CumulativeFees fees = 5; +} \ No newline at end of file diff --git a/src/e2e.ts b/src/e2e.ts index d46e2d59..5e0fda11 100644 --- a/src/e2e.ts +++ b/src/e2e.ts @@ -25,7 +25,10 @@ const start = async () => { const nostrSettings = settingsManager.getSettings().nostrRelaySettings log("initializing nostr middleware") const { Send } = nostrMiddleware(serverMethods, mainHandler, - { ...nostrSettings, apps, clients: [liquidityProviderInfo] }, + { + ...nostrSettings, apps, clients: [liquidityProviderInfo], + providerDestinationPub: settingsManager.getSettings().liquiditySettings.liquidityProviderPub + }, (e, p) => mainHandler.liquidityProvider.onEvent(e, p) ) log("starting server") diff --git a/src/index.ts b/src/index.ts index aa4c3c0b..0b53b7ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,10 +25,13 @@ const start = async () => { const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn const serverMethods = GetServerMethods(mainHandler) log("initializing nostr middleware") - const relays = mainHandler.settings.getSettings().nostrRelaySettings.relays - const maxEventContentLength = mainHandler.settings.getSettings().nostrRelaySettings.maxEventContentLength + const relays = settingsManager.getSettings().nostrRelaySettings.relays + const maxEventContentLength = settingsManager.getSettings().nostrRelaySettings.maxEventContentLength const { Send, Stop, Ping, Reset } = nostrMiddleware(serverMethods, mainHandler, - { relays, maxEventContentLength, apps, clients: [liquidityProviderInfo] }, + { + relays, maxEventContentLength, apps, clients: [liquidityProviderInfo], + providerDestinationPub: settingsManager.getSettings().liquiditySettings.liquidityProviderPub + }, (e, p) => mainHandler.liquidityProvider.onEvent(e, p) ) exitHandler(() => { Stop(); mainHandler.Stop() }) @@ -43,7 +46,7 @@ const start = async () => { } adminManager.setAppNprofile(appNprofile) const Server = NewServer(serverMethods, serverOptions(mainHandler)) - Server.Listen(mainHandler.settings.getSettings().serviceSettings.servicePort) + Server.Listen(settingsManager.getSettings().serviceSettings.servicePort) } start() diff --git a/src/nostrMiddleware.ts b/src/nostrMiddleware.ts index cc1db630..da1172f3 100644 --- a/src/nostrMiddleware.ts +++ b/src/nostrMiddleware.ts @@ -79,7 +79,7 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett nostrTransport({ ...j, appId: event.appId }, res => { nostr.Send({ type: 'app', appId: event.appId }, { type: 'content', pub: event.pub, content: JSON.stringify({ ...res, requestId: j.requestId }) }) }, event.startAtNano, event.startAtMs) - }) + }, beacon => mainHandler.liquidityProvider.onBeaconEvent(beacon)) // Mark nostr connected/ready after initial subscription tick mainHandler.adminManager.setNostrConnected(true) diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index 07b84c16..44ee5467 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -69,7 +69,7 @@ export default class { throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing } const nostrSettings = this.settings.getSettings().nostrRelaySettings - const { max, networkFeeBps, networkFeeFixed, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats, true) + const { max, networkFeeBps, networkFeeFixed, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats) return { userId: ctx.user_id, balance: user.balance_sats, diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index 011533f5..94729dd4 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -47,12 +47,13 @@ export default class { }, 60 * 1000); // 1 minute } - async StartAppsServiceBeacon(publishBeacon: (app: Application) => void) { + async StartAppsServiceBeacon(publishBeacon: (app: Application, fees: Types.CumulativeFees) => void) { this.serviceBeaconInterval = setInterval(async () => { try { + const fees = this.paymentManager.GetAllFees() const apps = await this.storage.applicationStorage.GetApplications() apps.forEach(app => { - publishBeacon(app) + publishBeacon(app, fees) }) } catch (e) { this.log("error in beacon", (e as any).message) @@ -154,7 +155,7 @@ export default class { const ndebitString = ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }) log("🔗 [DEBUG] Generated ndebit for user", { userId: u.user.user_id, ndebit: ndebitString }) - const { max, networkFeeBps, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true) + const { max, networkFeeBps, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats) return { identifier: u.identifier, info: { @@ -212,7 +213,7 @@ export default class { const app = await this.storage.applicationStorage.GetApplication(appId) const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) const nostrSettings = this.settings.getSettings().nostrRelaySettings - const { max, networkFeeBps, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true) + const { max, networkFeeBps, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats) return { max_withdrawable: max, identifier: req.user_identifier, info: { userId: user.user.user_id, balance: user.user.balance_sats, diff --git a/src/services/main/debitManager.ts b/src/services/main/debitManager.ts index 126a469c..9b4ca69f 100644 --- a/src/services/main/debitManager.ts +++ b/src/services/main/debitManager.ts @@ -9,9 +9,11 @@ import { ApplicationUser } from '../storage/entity/ApplicationUser.js'; import { NostrEvent, NostrSend, SendData, SendInitiator } from '../nostr/handler.js'; import { UnsignedEvent } from 'nostr-tools'; import { Ndebit, NdebitData, NdebitFailure, NdebitSuccess, RecurringDebitTimeUnit } from "@shocknet/clink-sdk"; -import { debitAccessRulesToDebitRules, newNdebitResponse,debitRulesToDebitAccessRules, - nofferErrors, AuthRequiredRes, HandleNdebitRes, expirationRuleName, - frequencyRuleName,IntervalTypeToSeconds,unitToIntervalType } from "./debitTypes.js"; +import { + debitAccessRulesToDebitRules, newNdebitResponse, debitRulesToDebitAccessRules, + nofferErrors, AuthRequiredRes, HandleNdebitRes, expirationRuleName, + frequencyRuleName, IntervalTypeToSeconds, unitToIntervalType +} from "./debitTypes.js"; export class DebitManager { @@ -72,7 +74,7 @@ export class DebitManager { this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: req.npub, id: req.request_id, appId: ctx.app_id }) return case Types.DebitResponse_response_type.INVOICE: - await this.paySingleInvoice(ctx, {invoice: req.response.invoice, npub: req.npub, request_id: req.request_id}) + await this.paySingleInvoice(ctx, { invoice: req.response.invoice, npub: req.npub, request_id: req.request_id }) return case Types.DebitResponse_response_type.AUTHORIZE: await this.handleAuthorization(ctx, req.response.authorize, { npub: req.npub, request_id: req.request_id }) @@ -82,7 +84,7 @@ export class DebitManager { } } - paySingleInvoice = async (ctx: Types.UserContext, {invoice,npub,request_id}:{invoice:string, npub:string, request_id:string}) => { + 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) @@ -97,7 +99,7 @@ export class DebitManager { } } - handleAuthorization = async (ctx: Types.UserContext,debit:Types.DebitToAuthorize, {npub,request_id}:{ npub:string, request_id:string})=>{ + handleAuthorization = async (ctx: Types.UserContext, debit: Types.DebitToAuthorize, { npub, request_id }: { npub: string, request_id: string }) => { this.logger("🔍 [DEBIT REQUEST] Handling authorization", { npub, request_id, @@ -130,7 +132,7 @@ export class DebitManager { this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: npub, id: request_id, appId: ctx.app_id }) throw e } - + } handleNip68Debit = async (pointerdata: NdebitData, event: NostrEvent) => { @@ -144,7 +146,7 @@ export class DebitManager { pointerdata }) const res = await this.payNdebitInvoice(event, pointerdata) - this.logger("🔍 [DEBIT REQUEST] Sending ",res.status," response") + this.logger("🔍 [DEBIT REQUEST] Sending ", res.status, " response") if (res.status === 'fail' || res.status === 'authOk') { const e = newNdebitResponse(JSON.stringify(res.debitRes), event) this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) @@ -159,7 +161,7 @@ export class DebitManager { this.notifyPaymentSuccess(appUser, debitRes, op, event) } - handleAuthRequired = (data:NdebitData, event: NostrEvent, res: AuthRequiredRes) => { + handleAuthRequired = (data: NdebitData, event: NostrEvent, res: AuthRequiredRes) => { if (!res.appUser.nostr_public_key) { this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: event.pub, id: event.id, appId: event.appId }) return @@ -169,7 +171,9 @@ export class DebitManager { } notifyPaymentSuccess = (appUser: ApplicationUser, debitRes: NdebitSuccess, op: Types.UserOperation, event: { pub: string, id: string, appId: string }) => { - const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' } + 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 }) } diff --git a/src/services/main/index.ts b/src/services/main/index.ts index 00f65a68..d691d94c 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -103,8 +103,8 @@ export default class { } StartBeacons() { - this.applicationManager.StartAppsServiceBeacon(app => { - this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url }) + this.applicationManager.StartAppsServiceBeacon((app, fees) => { + this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, fees }) }) } @@ -373,8 +373,9 @@ export default class { getLogger({ appName: app.name })("cannot notify user, not a nostr user") return } - - const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' } + const balance = user.user.balance_sats + const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = + { operation: op, requestId: "GetLiveUserOperations", status: 'OK', latest_balance: balance } const j = JSON.stringify(message) this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'content', content: j, pub: user.nostr_public_key }) this.SendEncryptedNotification(app, user, op) @@ -396,7 +397,7 @@ export default class { }) } - async UpdateBeacon(app: Application, content: { type: 'service', name: string, avatarUrl?: string, nextRelay?: string }) { + async UpdateBeacon(app: Application, content: Types.BeaconData) { if (!app.nostr_public_key) { getLogger({ appName: app.name })("cannot update beacon, public key not set") return @@ -435,8 +436,9 @@ export default class { async ResetNostr() { const apps = await this.storage.applicationStorage.GetApplications() const nextRelay = this.settings.getSettings().nostrRelaySettings.relays[0] + const fees = this.paymentManager.GetAllFees() for (const app of apps) { - await this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, nextRelay }) + await this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, nextRelay, fees }) } const defaultNames = ['wallet', 'wallet-test', this.settings.getSettings().serviceSettings.defaultAppName] @@ -453,7 +455,8 @@ export default class { apps: apps.map(a => ({ appId: a.app_id, name: a.name, privateKey: a.nostr_private_key || "", publicKey: a.nostr_public_key || "" })), relays: this.settings.getSettings().nostrRelaySettings.relays, maxEventContentLength: this.settings.getSettings().nostrRelaySettings.maxEventContentLength, - clients: [liquidityProviderInfo] + clients: [liquidityProviderInfo], + providerDestinationPub: this.settings.getSettings().liquiditySettings.liquidityProviderPub } this.nostrReset(s) } diff --git a/src/services/main/liquidityManager.ts b/src/services/main/liquidityManager.ts index 8637952c..e03b1e02 100644 --- a/src/services/main/liquidityManager.ts +++ b/src/services/main/liquidityManager.ts @@ -104,7 +104,7 @@ export class LiquidityManager { afterOutInvoicePaid = async () => { } shouldDrainProvider = async () => { - const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable() + const maxW = await this.liquidityProvider.GetMaxWithdrawable() const { remote } = await this.lnd.ChannelBalance() const drainable = Math.min(maxW, remote) if (drainable < 500) { @@ -173,7 +173,7 @@ export class LiquidityManager { if (pendingChannels.pendingOpenChannels.length > 0) { return { shouldOpen: false } } - const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable() + const maxW = await this.liquidityProvider.GetMaxWithdrawable() if (maxW < threshold) { return { shouldOpen: false } } diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 0e65be90..54b860aa 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -1,7 +1,7 @@ import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js' import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js' import * as Types from '../../../proto/autogenerated/ts/types.js' -import { getLogger } from '../helpers/logger.js' +import { ERROR, getLogger } from '../helpers/logger.js' import { Utils } from '../helpers/utilsWrapper.js' import { NostrEvent, NostrSend } from '../nostr/handler.js' import { InvoicePaidCb } from '../lnd/settings.js' @@ -9,7 +9,12 @@ 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 @@ -28,9 +33,11 @@ export class LiquidityProvider { queue: ((state: 'ready') => void)[] = [] utils: Utils pendingPayments: Record = {} - stateCache: Types.UserInfo | null = null - unreachableSince: number | null = null - reconnecting = false + feesCache: { networkFeeBps: number, networkFeeFixed: number, serviceFeeBps: number } | null = null + // unreachableSince: number | null = null + // reconnecting = false + lastSeenBeacon = 0 + latestReceivedBalance = 0 incrementProviderBalance: (balance: number) => Promise // make the sub process accept client constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise) { @@ -71,11 +78,12 @@ 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) - } - return this.ready && !this.getSettings().disableLiquidityProvider && !this.unreachableSince + /* 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 } AwaitProviderReady = async (): Promise<'inactive' | 'ready'> => { @@ -114,6 +122,7 @@ export class LiquidityProvider { try { await this.invoicePaidCb(res.operation.identifier, res.operation.amount, 'provider') this.incrementProviderBalance(res.operation.amount) + this.latestReceivedBalance = res.latest_balance } catch (err: any) { this.log("error processing incoming invoice", err.message) } @@ -126,51 +135,60 @@ export class LiquidityProvider { if (res.status === 'ERROR') { if (res.reason !== 'timeout') { this.log("error getting user info", res.reason) - if (!this.unreachableSince) this.unreachableSince = Date.now() } return res } - this.unreachableSince = null - this.stateCache = res + this.feesCache = { + networkFeeBps: res.network_max_fee_bps, + networkFeeFixed: res.network_max_fee_fixed, + serviceFeeBps: res.service_fee_bps + } + this.latestReceivedBalance = res.balance this.utils.stateBundler.AddBalancePoint('providerBalance', res.balance) this.utils.stateBundler.AddBalancePoint('providerMaxWithdrawable', res.max_withdrawable) return res } GetFees = () => { - if (!this.stateCache) { - throw new Error("user state not cached") - } - return { - serviceFeeBps: this.stateCache.service_fee_bps, - networkFeeBps: this.stateCache.network_max_fee_bps, - networkFeeFixed: this.stateCache.network_max_fee_fixed, - + if (!this.feesCache) { + throw new Error("fees not cached") } + return this.feesCache } - GetLatestMaxWithdrawable = async () => { + GetMaxWithdrawable = () => { + if (!this.IsReady() || !this.feesCache) { + return 0 + } + const { networkFeeBps, networkFeeFixed, serviceFeeBps } = this.feesCache + const totalBps = networkFeeBps + serviceFeeBps + const div = 1 + (totalBps / 10000) + 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 } - const res = await this.GetUserState() + return this.latestReceivedBalance + /* const res = await this.GetUserState() if (res.status === 'ERROR') { this.log("error getting user info", res.reason) return 0 } - return res.max_withdrawable - } - - GetLatestBalance = 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.balance + return res.balance */ } GetPendingBalance = async () => { @@ -270,6 +288,7 @@ export class LiquidityProvider { } */ const totalPaid = res.amount_paid + res.network_fee + res.service_fee this.incrementProviderBalance(-totalPaid).then(() => { delete this.pendingPayments[invoice] }) + this.latestReceivedBalance = res.latest_balance this.utils.stateBundler.AddTxPoint('paidAnInvoice', decodedAmount, { used: 'provider', from, timeDiscount: true }) return res } catch (err) { @@ -328,6 +347,26 @@ export class LiquidityProvider { this.log("configured to send to ", this.pubDestination) } } + // fees: { networkFeeBps: number, networkFeeFixed: number, serviceFeeBps: number } + onBeaconEvent = async (beaconData: { content: string, pub: string }) => { + if (beaconData.pub !== this.pubDestination) { + this.log(ERROR, "got beacon from invalid pub", beaconData.pub, this.pubDestination) + return + } + const beacon = JSON.parse(beaconData.content) as Types.BeaconData + const err = Types.BeaconDataValidate(beacon) + if (err) { + this.log(ERROR, "error validating beacon data", err.message) + return + } + if (beacon.type !== 'service') { + this.log(ERROR, "got beacon from invalid type", beacon.type) + return + } + if (beacon.fees) { + this.feesCache = beacon.fees + } + } onEvent = async (res: { requestId: string }, fromPub: string) => { if (fromPub !== this.pubDestination) { diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 752c1821..4019167b 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -36,6 +36,8 @@ interface UserOperationInfo { }; internal?: boolean; } + + export type PendingTx = { type: 'incoming', tx: AddressReceivingTransaction } | { type: 'outgoing', tx: UserTransactionPayment } const defaultLnurlPayMetadata = (text: string) => `[["text/plain", "${text}"]]` const defaultLnAddressMetadata = (text: string, id: string) => `[["text/plain", "${text}"],["text/identifier", "${id}"]]` @@ -232,22 +234,37 @@ export default class { } } - GetMaxPayableInvoice(balance: number, appUser: boolean): { max: number, serviceFeeBps: number, networkFeeBps: number, networkFeeFixed: number } { - const { outgoingAppInvoiceFee, outgoingAppUserInvoiceFee, outgoingAppUserInvoiceFeeBps } = this.settings.getSettings().serviceFeeSettings - const serviceFee = appUser ? outgoingAppUserInvoiceFee : outgoingAppInvoiceFee + GetAllFees = (): Types.CumulativeFees => { + const { outgoingAppUserInvoiceFeeBps } = this.settings.getSettings().serviceFeeSettings 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 } + return { networkFeeBps, networkFeeFixed: fees.networkFeeFixed, serviceFeeBps: outgoingAppUserInvoiceFeeBps } } - 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 } + const { feeFixedLimit, feeRateBps } = this.settings.getSettings().lndSettings + return { networkFeeBps: feeRateBps, networkFeeFixed: feeFixedLimit, serviceFeeBps: outgoingAppUserInvoiceFeeBps } + } + + GetMaxPayableInvoice(balance: number): Types.CumulativeFees & { max: number } { + const { networkFeeBps, networkFeeFixed, serviceFeeBps } = this.GetAllFees() + const totalBps = networkFeeBps + serviceFeeBps + 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))) @@ -317,6 +334,7 @@ export default class { operation_id: `${Types.UserOperationType.OUTGOING_INVOICE}-${paymentInfo.serialId}`, network_fee: paymentInfo.networkFee, service_fee: serviceFee, + latest_balance: user.balance_sats } } diff --git a/src/services/main/rugPullTracker.ts b/src/services/main/rugPullTracker.ts index ab5889a5..8feecb30 100644 --- a/src/services/main/rugPullTracker.ts +++ b/src/services/main/rugPullTracker.ts @@ -27,7 +27,7 @@ export class RugPullTracker { const providerTracker = await this.storage.liquidityStorage.GetTrackedProvider('lnPub', pubDst) const ready = this.liquidProvider.IsReady() if (ready) { - const balance = await this.liquidProvider.GetLatestBalance() + const balance = this.liquidProvider.GetLatestBalance() const pendingBalance = await this.liquidProvider.GetPendingBalance() const trackedBalance = balance + pendingBalance if (!providerTracker) { diff --git a/src/services/nostr/handler.ts b/src/services/nostr/handler.ts index e054d8de..06c83c08 100644 --- a/src/services/nostr/handler.ts +++ b/src/services/nostr/handler.ts @@ -2,7 +2,7 @@ import WebSocket from 'ws' Object.assign(global, { WebSocket: WebSocket }); import crypto from 'crypto' -import { SimplePool, Event, UnsignedEvent, finalizeEvent, Relay, nip44 } from 'nostr-tools' +import { SimplePool, Event, UnsignedEvent, finalizeEvent, Relay, nip44, Filter } from 'nostr-tools' import { ERROR, getLogger } from '../helpers/logger.js' import { nip19 } from 'nostr-tools' import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.js' @@ -26,6 +26,7 @@ export type NostrSettings = { relays: string[] clients: ClientInfo[] maxEventContentLength: number + providerDestinationPub: string } export type NostrEvent = { @@ -69,9 +70,14 @@ type ProcessMetricsResponse = { type: 'processMetrics' metrics: ProcessMetrics } +type BeaconResponse = { + type: 'beacon' + content: string + pub: string +} export type ChildProcessRequest = SettingsRequest | SendRequest | PingRequest -export type ChildProcessResponse = ReadyResponse | EventResponse | ProcessMetricsResponse | PingResponse +export type ChildProcessResponse = ReadyResponse | EventResponse | ProcessMetricsResponse | PingResponse | BeaconResponse const send = (message: ChildProcessResponse) => { if (process.send) { process.send(message, undefined, undefined, err => { @@ -218,18 +224,28 @@ export default class Handler { appIds: appIds, listeningForPubkeys: appIds }) - - return relay.subscribe([ + const subs: Filter[] = [ { since: Math.ceil(Date.now() / 1000), kinds: supportedKinds, '#p': appIds, } - ], { + ] + if (this.settings.providerDestinationPub) { + subs.push({ + kinds: [30078], '#d': ['Lightning.Pub'], + authors: [this.settings.providerDestinationPub] + }) + } + return relay.subscribe(subs, { oneose: () => { this.log("up to date with nostr events") }, onevent: async (e) => { + if (e.kind === 30078 && e.pubkey === this.settings.providerDestinationPub) { + send({ type: 'beacon', content: e.content, pub: e.pubkey }) + return + } if (!supportedKinds.includes(e.kind) || !e.pubkey) { return } diff --git a/src/services/nostr/index.ts b/src/services/nostr/index.ts index 50fdf61b..68773ee5 100644 --- a/src/services/nostr/index.ts +++ b/src/services/nostr/index.ts @@ -3,7 +3,7 @@ import { NostrSettings, NostrEvent, ChildProcessRequest, ChildProcessResponse, S import { Utils } from '../helpers/utilsWrapper.js' import { getLogger, ERROR } from '../helpers/logger.js' type EventCallback = (event: NostrEvent) => void - +type BeaconCallback = (beacon: { content: string, pub: string }) => void @@ -13,7 +13,7 @@ export default class NostrSubprocess { utils: Utils awaitingPongs: (() => void)[] = [] log = getLogger({}) - constructor(settings: NostrSettings, utils: Utils, eventCallback: EventCallback) { + constructor(settings: NostrSettings, utils: Utils, eventCallback: EventCallback, beaconCallback: BeaconCallback) { this.utils = utils this.childProcess = fork("./build/src/services/nostr/handler") this.childProcess.on("error", (error) => { @@ -43,6 +43,9 @@ export default class NostrSubprocess { this.awaitingPongs.forEach(resolve => resolve()) this.awaitingPongs = [] break + case 'beacon': + beaconCallback({ content: message.content, pub: message.pub }) + break default: console.error("unknown nostr event response", message) break;