provider: wire beacon + return balance + use cached balance

This commit is contained in:
boufni95 2025-11-21 16:45:29 +00:00
parent c8ede119d6
commit 8e4a8b2a2a
17 changed files with 307 additions and 92 deletions

View file

@ -1104,6 +1104,13 @@ The nostr server will send back a message response, and inside the body there wi
- __nostr_pub__: _string_ - __nostr_pub__: _string_
- __user_identifier__: _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 ### BundleData
- __available_chunks__: ARRAY of: _number_ - __available_chunks__: ARRAY of: _number_
- __base_64_data__: ARRAY of: _string_ - __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 ### CreateOneTimeInviteLinkResponse
- __invitation_link__: _string_ - __invitation_link__: _string_
### CumulativeFees
- __networkFeeBps__: _number_
- __networkFeeFixed__: _number_
- __serviceFeeBps__: _number_
### DebitAuthorization ### DebitAuthorization
- __authorized__: _boolean_ - __authorized__: _boolean_
- __debit_id__: _string_ - __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_ - __request_id__: _string_
### LiveUserOperation ### LiveUserOperation
- __latest_balance__: _number_
- __operation__: _[UserOperation](#UserOperation)_ - __operation__: _[UserOperation](#UserOperation)_
### LndChannels ### LndChannels
@ -1487,6 +1500,7 @@ The nostr server will send back a message response, and inside the body there wi
### PayInvoiceResponse ### PayInvoiceResponse
- __amount_paid__: _number_ - __amount_paid__: _number_
- __latest_balance__: _number_
- __network_fee__: _number_ - __network_fee__: _number_
- __operation_id__: _string_ - __operation_id__: _string_
- __preimage__: _string_ - __preimage__: _string_

View file

@ -177,6 +177,13 @@ type BannedAppUser struct {
Nostr_pub string `json:"nostr_pub"` Nostr_pub string `json:"nostr_pub"`
User_identifier string `json:"user_identifier"` 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 { type BundleData struct {
Available_chunks []int64 `json:"available_chunks"` Available_chunks []int64 `json:"available_chunks"`
Base_64_data []string `json:"base_64_data"` Base_64_data []string `json:"base_64_data"`
@ -222,6 +229,11 @@ type CreateOneTimeInviteLinkRequest struct {
type CreateOneTimeInviteLinkResponse struct { type CreateOneTimeInviteLinkResponse struct {
Invitation_link string `json:"invitation_link"` Invitation_link string `json:"invitation_link"`
} }
type CumulativeFees struct {
Networkfeebps int64 `json:"networkFeeBps"`
Networkfeefixed int64 `json:"networkFeeFixed"`
Servicefeebps int64 `json:"serviceFeeBps"`
}
type DebitAuthorization struct { type DebitAuthorization struct {
Authorized bool `json:"authorized"` Authorized bool `json:"authorized"`
Debit_id string `json:"debit_id"` Debit_id string `json:"debit_id"`
@ -363,7 +375,8 @@ type LiveManageRequest struct {
Request_id string `json:"request_id"` Request_id string `json:"request_id"`
} }
type LiveUserOperation struct { type LiveUserOperation struct {
Operation *UserOperation `json:"operation"` Latest_balance int64 `json:"latest_balance"`
Operation *UserOperation `json:"operation"`
} }
type LndChannels struct { type LndChannels struct {
Open_channels []OpenChannel `json:"open_channels"` Open_channels []OpenChannel `json:"open_channels"`
@ -559,11 +572,12 @@ type PayInvoiceRequest struct {
Invoice string `json:"invoice"` Invoice string `json:"invoice"`
} }
type PayInvoiceResponse struct { type PayInvoiceResponse struct {
Amount_paid int64 `json:"amount_paid"` Amount_paid int64 `json:"amount_paid"`
Network_fee int64 `json:"network_fee"` Latest_balance int64 `json:"latest_balance"`
Operation_id string `json:"operation_id"` Network_fee int64 `json:"network_fee"`
Preimage string `json:"preimage"` Operation_id string `json:"operation_id"`
Service_fee int64 `json:"service_fee"` Preimage string `json:"preimage"`
Service_fee int64 `json:"service_fee"`
} }
type PayerData struct { type PayerData struct {
Data map[string]string `json:"data"` Data map[string]string `json:"data"`

View file

@ -983,6 +983,48 @@ export const BannedAppUserValidate = (o?: BannedAppUser, opts: BannedAppUserOpti
return null 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 = { export type BundleData = {
available_chunks: number[] available_chunks: number[]
base_64_data: string[] base_64_data: string[]
@ -1256,6 +1298,34 @@ export const CreateOneTimeInviteLinkResponseValidate = (o?: CreateOneTimeInviteL
return null 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 = { export type DebitAuthorization = {
authorized: boolean authorized: boolean
debit_id: string debit_id: string
@ -2112,17 +2182,22 @@ export const LiveManageRequestValidate = (o?: LiveManageRequest, opts: LiveManag
} }
export type LiveUserOperation = { export type LiveUserOperation = {
latest_balance: number
operation: UserOperation operation: UserOperation
} }
export const LiveUserOperationOptionalFields: [] = [] export const LiveUserOperationOptionalFields: [] = []
export type LiveUserOperationOptions = OptionsBaseMessage & { export type LiveUserOperationOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: [] checkOptionalsAreSet?: []
latest_balance_CustomCheck?: (v: number) => boolean
operation_Options?: UserOperationOptions operation_Options?: UserOperationOptions
} }
export const LiveUserOperationValidate = (o?: LiveUserOperation, opts: LiveUserOperationOptions = {}, path: string = 'LiveUserOperation::root.'): Error | null => { 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 (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 !== '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`) const operationErr = UserOperationValidate(o.operation, opts.operation_Options, `${path}.operation`)
if (operationErr !== null) return operationErr if (operationErr !== null) return operationErr
@ -3287,6 +3362,7 @@ export const PayInvoiceRequestValidate = (o?: PayInvoiceRequest, opts: PayInvoic
export type PayInvoiceResponse = { export type PayInvoiceResponse = {
amount_paid: number amount_paid: number
latest_balance: number
network_fee: number network_fee: number
operation_id: string operation_id: string
preimage: string preimage: string
@ -3296,6 +3372,7 @@ export const PayInvoiceResponseOptionalFields: [] = []
export type PayInvoiceResponseOptions = OptionsBaseMessage & { export type PayInvoiceResponseOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: [] checkOptionalsAreSet?: []
amount_paid_CustomCheck?: (v: number) => boolean amount_paid_CustomCheck?: (v: number) => boolean
latest_balance_CustomCheck?: (v: number) => boolean
network_fee_CustomCheck?: (v: number) => boolean network_fee_CustomCheck?: (v: number) => boolean
operation_id_CustomCheck?: (v: string) => boolean operation_id_CustomCheck?: (v: string) => boolean
preimage_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 (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 (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 (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`) if (opts.network_fee_CustomCheck && !opts.network_fee_CustomCheck(o.network_fee)) return new Error(`${path}.network_fee: custom check failed`)

View file

@ -476,6 +476,7 @@ message PayInvoiceResponse{
string operation_id = 3; string operation_id = 3;
int64 service_fee = 4; int64 service_fee = 4;
int64 network_fee = 5; int64 network_fee = 5;
int64 latest_balance = 6;
} }
message InvoicePaymentStream { message InvoicePaymentStream {
@ -613,6 +614,7 @@ message GetProductBuyLinkResponse {
message LiveUserOperation { message LiveUserOperation {
UserOperation operation = 1; UserOperation operation = 1;
int64 latest_balance = 2;
} }
message MigrationUpdate { message MigrationUpdate {
optional ClosureMigration closure = 1; optional ClosureMigration closure = 1;
@ -833,3 +835,18 @@ message MessagingToken {
string device_id = 1; string device_id = 1;
string firebase_messaging_token = 2; 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;
}

View file

@ -25,7 +25,10 @@ const start = async () => {
const nostrSettings = settingsManager.getSettings().nostrRelaySettings const nostrSettings = settingsManager.getSettings().nostrRelaySettings
log("initializing nostr middleware") log("initializing nostr middleware")
const { Send } = nostrMiddleware(serverMethods, mainHandler, 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) (e, p) => mainHandler.liquidityProvider.onEvent(e, p)
) )
log("starting server") log("starting server")

View file

@ -25,10 +25,13 @@ const start = async () => {
const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn
const serverMethods = GetServerMethods(mainHandler) const serverMethods = GetServerMethods(mainHandler)
log("initializing nostr middleware") log("initializing nostr middleware")
const relays = mainHandler.settings.getSettings().nostrRelaySettings.relays const relays = settingsManager.getSettings().nostrRelaySettings.relays
const maxEventContentLength = mainHandler.settings.getSettings().nostrRelaySettings.maxEventContentLength const maxEventContentLength = settingsManager.getSettings().nostrRelaySettings.maxEventContentLength
const { Send, Stop, Ping, Reset } = nostrMiddleware(serverMethods, mainHandler, 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) (e, p) => mainHandler.liquidityProvider.onEvent(e, p)
) )
exitHandler(() => { Stop(); mainHandler.Stop() }) exitHandler(() => { Stop(); mainHandler.Stop() })
@ -43,7 +46,7 @@ const start = async () => {
} }
adminManager.setAppNprofile(appNprofile) adminManager.setAppNprofile(appNprofile)
const Server = NewServer(serverMethods, serverOptions(mainHandler)) const Server = NewServer(serverMethods, serverOptions(mainHandler))
Server.Listen(mainHandler.settings.getSettings().serviceSettings.servicePort) Server.Listen(settingsManager.getSettings().serviceSettings.servicePort)
} }
start() start()

View file

@ -79,7 +79,7 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
nostrTransport({ ...j, appId: event.appId }, res => { 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 }) }) nostr.Send({ type: 'app', appId: event.appId }, { type: 'content', pub: event.pub, content: JSON.stringify({ ...res, requestId: j.requestId }) })
}, event.startAtNano, event.startAtMs) }, event.startAtNano, event.startAtMs)
}) }, beacon => mainHandler.liquidityProvider.onBeaconEvent(beacon))
// Mark nostr connected/ready after initial subscription tick // Mark nostr connected/ready after initial subscription tick
mainHandler.adminManager.setNostrConnected(true) mainHandler.adminManager.setNostrConnected(true)

View file

@ -69,7 +69,7 @@ export default class {
throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing
} }
const nostrSettings = this.settings.getSettings().nostrRelaySettings 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 { return {
userId: ctx.user_id, userId: ctx.user_id,
balance: user.balance_sats, balance: user.balance_sats,

View file

@ -47,12 +47,13 @@ export default class {
}, 60 * 1000); // 1 minute }, 60 * 1000); // 1 minute
} }
async StartAppsServiceBeacon(publishBeacon: (app: Application) => void) { async StartAppsServiceBeacon(publishBeacon: (app: Application, fees: Types.CumulativeFees) => void) {
this.serviceBeaconInterval = setInterval(async () => { this.serviceBeaconInterval = setInterval(async () => {
try { try {
const fees = this.paymentManager.GetAllFees()
const apps = await this.storage.applicationStorage.GetApplications() const apps = await this.storage.applicationStorage.GetApplications()
apps.forEach(app => { apps.forEach(app => {
publishBeacon(app) publishBeacon(app, fees)
}) })
} catch (e) { } catch (e) {
this.log("error in beacon", (e as any).message) 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] }) 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 }) 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 { return {
identifier: u.identifier, identifier: u.identifier,
info: { info: {
@ -212,7 +213,7 @@ export default class {
const app = await this.storage.applicationStorage.GetApplication(appId) const app = await this.storage.applicationStorage.GetApplication(appId)
const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier)
const nostrSettings = this.settings.getSettings().nostrRelaySettings 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 { return {
max_withdrawable: max, identifier: req.user_identifier, info: { max_withdrawable: max, identifier: req.user_identifier, info: {
userId: user.user.user_id, balance: user.user.balance_sats, userId: user.user.user_id, balance: user.user.balance_sats,

View file

@ -9,9 +9,11 @@ import { ApplicationUser } from '../storage/entity/ApplicationUser.js';
import { NostrEvent, NostrSend, SendData, SendInitiator } from '../nostr/handler.js'; import { NostrEvent, NostrSend, SendData, SendInitiator } from '../nostr/handler.js';
import { UnsignedEvent } from 'nostr-tools'; import { UnsignedEvent } from 'nostr-tools';
import { Ndebit, NdebitData, NdebitFailure, NdebitSuccess, RecurringDebitTimeUnit } from "@shocknet/clink-sdk"; import { Ndebit, NdebitData, NdebitFailure, NdebitSuccess, RecurringDebitTimeUnit } from "@shocknet/clink-sdk";
import { debitAccessRulesToDebitRules, newNdebitResponse,debitRulesToDebitAccessRules, import {
debitAccessRulesToDebitRules, newNdebitResponse, debitRulesToDebitAccessRules,
nofferErrors, AuthRequiredRes, HandleNdebitRes, expirationRuleName, nofferErrors, AuthRequiredRes, HandleNdebitRes, expirationRuleName,
frequencyRuleName,IntervalTypeToSeconds,unitToIntervalType } from "./debitTypes.js"; frequencyRuleName, IntervalTypeToSeconds, unitToIntervalType
} from "./debitTypes.js";
export class DebitManager { 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 }) this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: req.npub, id: req.request_id, appId: ctx.app_id })
return return
case Types.DebitResponse_response_type.INVOICE: 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 return
case Types.DebitResponse_response_type.AUTHORIZE: case Types.DebitResponse_response_type.AUTHORIZE:
await this.handleAuthorization(ctx, req.response.authorize, { npub: req.npub, request_id: req.request_id }) 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 { try {
this.logger("🔍 [DEBIT REQUEST] Paying single invoice") this.logger("🔍 [DEBIT REQUEST] Paying single invoice")
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) 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", { this.logger("🔍 [DEBIT REQUEST] Handling authorization", {
npub, npub,
request_id, request_id,
@ -144,7 +146,7 @@ export class DebitManager {
pointerdata pointerdata
}) })
const res = await this.payNdebitInvoice(event, 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') { if (res.status === 'fail' || res.status === 'authOk') {
const e = newNdebitResponse(JSON.stringify(res.debitRes), event) const e = newNdebitResponse(JSON.stringify(res.debitRes), event)
this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) 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) 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) { if (!res.appUser.nostr_public_key) {
this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: event.pub, id: event.id, appId: event.appId }) this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: event.pub, id: event.id, appId: event.appId })
return return
@ -169,7 +171,9 @@ export class DebitManager {
} }
notifyPaymentSuccess = (appUser: ApplicationUser, debitRes: NdebitSuccess, op: Types.UserOperation, event: { pub: string, id: string, appId: string }) => { 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 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 }) this.nostrSend({ type: 'app', appId: event.appId }, { type: 'content', content: JSON.stringify(message), pub: appUser.nostr_public_key })
} }

View file

@ -103,8 +103,8 @@ export default class {
} }
StartBeacons() { StartBeacons() {
this.applicationManager.StartAppsServiceBeacon(app => { this.applicationManager.StartAppsServiceBeacon((app, fees) => {
this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url }) 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") getLogger({ appName: app.name })("cannot notify user, not a nostr user")
return return
} }
const balance = user.user.balance_sats
const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' } const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } =
{ operation: op, requestId: "GetLiveUserOperations", status: 'OK', latest_balance: balance }
const j = JSON.stringify(message) const j = JSON.stringify(message)
this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'content', content: j, pub: user.nostr_public_key }) this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'content', content: j, pub: user.nostr_public_key })
this.SendEncryptedNotification(app, user, op) 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) { if (!app.nostr_public_key) {
getLogger({ appName: app.name })("cannot update beacon, public key not set") getLogger({ appName: app.name })("cannot update beacon, public key not set")
return return
@ -435,8 +436,9 @@ export default class {
async ResetNostr() { async ResetNostr() {
const apps = await this.storage.applicationStorage.GetApplications() const apps = await this.storage.applicationStorage.GetApplications()
const nextRelay = this.settings.getSettings().nostrRelaySettings.relays[0] const nextRelay = this.settings.getSettings().nostrRelaySettings.relays[0]
const fees = this.paymentManager.GetAllFees()
for (const app of apps) { 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] 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 || "" })), 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, relays: this.settings.getSettings().nostrRelaySettings.relays,
maxEventContentLength: this.settings.getSettings().nostrRelaySettings.maxEventContentLength, maxEventContentLength: this.settings.getSettings().nostrRelaySettings.maxEventContentLength,
clients: [liquidityProviderInfo] clients: [liquidityProviderInfo],
providerDestinationPub: this.settings.getSettings().liquiditySettings.liquidityProviderPub
} }
this.nostrReset(s) this.nostrReset(s)
} }

View file

@ -104,7 +104,7 @@ export class LiquidityManager {
afterOutInvoicePaid = async () => { } afterOutInvoicePaid = async () => { }
shouldDrainProvider = async () => { shouldDrainProvider = async () => {
const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable() const maxW = await this.liquidityProvider.GetMaxWithdrawable()
const { remote } = await this.lnd.ChannelBalance() const { remote } = await this.lnd.ChannelBalance()
const drainable = Math.min(maxW, remote) const drainable = Math.min(maxW, remote)
if (drainable < 500) { if (drainable < 500) {
@ -173,7 +173,7 @@ export class LiquidityManager {
if (pendingChannels.pendingOpenChannels.length > 0) { if (pendingChannels.pendingOpenChannels.length > 0) {
return { shouldOpen: false } return { shouldOpen: false }
} }
const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable() const maxW = await this.liquidityProvider.GetMaxWithdrawable()
if (maxW < threshold) { if (maxW < threshold) {
return { shouldOpen: false } return { shouldOpen: false }
} }

View file

@ -1,7 +1,7 @@
import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js' import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js'
import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js' import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js'
import * as Types from '../../../proto/autogenerated/ts/types.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 { Utils } from '../helpers/utilsWrapper.js'
import { NostrEvent, NostrSend } from '../nostr/handler.js' import { NostrEvent, NostrSend } from '../nostr/handler.js'
import { InvoicePaidCb } from '../lnd/settings.js' import { InvoicePaidCb } from '../lnd/settings.js'
@ -9,7 +9,12 @@ import Storage from '../storage/index.js'
import SettingsManager from './settingsManager.js' import SettingsManager from './settingsManager.js'
import { LiquiditySettings } from './settings.js' import { LiquiditySettings } from './settings.js'
export type LiquidityRequest = { action: 'spend' | 'receive', amount: number } 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<T> = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void } export type nostrCallback<T> = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void }
export class LiquidityProvider { export class LiquidityProvider {
getSettings: () => LiquiditySettings getSettings: () => LiquiditySettings
@ -28,9 +33,11 @@ export class LiquidityProvider {
queue: ((state: 'ready') => void)[] = [] queue: ((state: 'ready') => void)[] = []
utils: Utils utils: Utils
pendingPayments: Record<string, number> = {} pendingPayments: Record<string, number> = {}
stateCache: Types.UserInfo | null = null feesCache: { networkFeeBps: number, networkFeeFixed: number, serviceFeeBps: number } | null = null
unreachableSince: number | null = null // unreachableSince: number | null = null
reconnecting = false // reconnecting = false
lastSeenBeacon = 0
latestReceivedBalance = 0
incrementProviderBalance: (balance: number) => Promise<void> incrementProviderBalance: (balance: number) => Promise<void>
// make the sub process accept client // make the sub process accept client
constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise<any>) { constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise<any>) {
@ -71,11 +78,12 @@ export class LiquidityProvider {
} }
IsReady = () => { IsReady = () => {
const elapsed = this.unreachableSince ? Date.now() - this.unreachableSince : 0 /* const elapsed = this.unreachableSince ? Date.now() - this.unreachableSince : 0
if (!this.reconnecting && elapsed > 1000 * 60 * 5) { if (!this.reconnecting && elapsed > 1000 * 60 * 5) {
this.GetUserState().then(() => this.reconnecting = false) this.GetUserState().then(() => this.reconnecting = false)
} } */
return this.ready && !this.getSettings().disableLiquidityProvider && !this.unreachableSince const seenInPast2Minutes = Date.now() - this.lastSeenBeacon < 1000 * 60 * 2
return this.ready && !this.getSettings().disableLiquidityProvider && seenInPast2Minutes
} }
AwaitProviderReady = async (): Promise<'inactive' | 'ready'> => { AwaitProviderReady = async (): Promise<'inactive' | 'ready'> => {
@ -114,6 +122,7 @@ export class LiquidityProvider {
try { try {
await this.invoicePaidCb(res.operation.identifier, res.operation.amount, 'provider') await this.invoicePaidCb(res.operation.identifier, res.operation.amount, 'provider')
this.incrementProviderBalance(res.operation.amount) this.incrementProviderBalance(res.operation.amount)
this.latestReceivedBalance = res.latest_balance
} catch (err: any) { } catch (err: any) {
this.log("error processing incoming invoice", err.message) this.log("error processing incoming invoice", err.message)
} }
@ -126,51 +135,60 @@ export class LiquidityProvider {
if (res.status === 'ERROR') { if (res.status === 'ERROR') {
if (res.reason !== 'timeout') { if (res.reason !== 'timeout') {
this.log("error getting user info", res.reason) this.log("error getting user info", res.reason)
if (!this.unreachableSince) this.unreachableSince = Date.now()
} }
return res return res
} }
this.unreachableSince = null this.feesCache = {
this.stateCache = res 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('providerBalance', res.balance)
this.utils.stateBundler.AddBalancePoint('providerMaxWithdrawable', res.max_withdrawable) this.utils.stateBundler.AddBalancePoint('providerMaxWithdrawable', res.max_withdrawable)
return res return res
} }
GetFees = () => { GetFees = () => {
if (!this.stateCache) { if (!this.feesCache) {
throw new Error("user state not cached") throw new Error("fees not cached")
}
return {
serviceFeeBps: this.stateCache.service_fee_bps,
networkFeeBps: this.stateCache.network_max_fee_bps,
networkFeeFixed: this.stateCache.network_max_fee_fixed,
} }
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()) { if (!this.IsReady()) {
return 0 return 0
} }
const res = await this.GetUserState() return this.latestReceivedBalance
/* const res = await this.GetUserState()
if (res.status === 'ERROR') { if (res.status === 'ERROR') {
this.log("error getting user info", res.reason) this.log("error getting user info", res.reason)
return 0 return 0
} }
return res.max_withdrawable return res.balance */
}
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
} }
GetPendingBalance = async () => { GetPendingBalance = async () => {
@ -270,6 +288,7 @@ export class LiquidityProvider {
} */ } */
const totalPaid = res.amount_paid + res.network_fee + res.service_fee const totalPaid = res.amount_paid + res.network_fee + res.service_fee
this.incrementProviderBalance(-totalPaid).then(() => { delete this.pendingPayments[invoice] }) this.incrementProviderBalance(-totalPaid).then(() => { delete this.pendingPayments[invoice] })
this.latestReceivedBalance = res.latest_balance
this.utils.stateBundler.AddTxPoint('paidAnInvoice', decodedAmount, { used: 'provider', from, timeDiscount: true }) this.utils.stateBundler.AddTxPoint('paidAnInvoice', decodedAmount, { used: 'provider', from, timeDiscount: true })
return res return res
} catch (err) { } catch (err) {
@ -328,6 +347,26 @@ export class LiquidityProvider {
this.log("configured to send to ", this.pubDestination) 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) => { onEvent = async (res: { requestId: string }, fromPub: string) => {
if (fromPub !== this.pubDestination) { if (fromPub !== this.pubDestination) {

View file

@ -36,6 +36,8 @@ interface UserOperationInfo {
}; };
internal?: boolean; internal?: boolean;
} }
export type PendingTx = { type: 'incoming', tx: AddressReceivingTransaction } | { type: 'outgoing', tx: UserTransactionPayment } export type PendingTx = { type: 'incoming', tx: AddressReceivingTransaction } | { type: 'outgoing', tx: UserTransactionPayment }
const defaultLnurlPayMetadata = (text: string) => `[["text/plain", "${text}"]]` const defaultLnurlPayMetadata = (text: string) => `[["text/plain", "${text}"]]`
const defaultLnAddressMetadata = (text: string, id: string) => `[["text/plain", "${text}"],["text/identifier", "${id}"]]` 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 } { GetAllFees = (): Types.CumulativeFees => {
const { outgoingAppInvoiceFee, outgoingAppUserInvoiceFee, outgoingAppUserInvoiceFeeBps } = this.settings.getSettings().serviceFeeSettings const { outgoingAppUserInvoiceFeeBps } = this.settings.getSettings().serviceFeeSettings
const serviceFee = appUser ? outgoingAppUserInvoiceFee : outgoingAppInvoiceFee
if (this.lnd.liquidProvider.IsReady()) { if (this.lnd.liquidProvider.IsReady()) {
const fees = this.lnd.liquidProvider.GetFees() 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 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 { feeFixedLimit, feeRateBps } = this.settings.getSettings().lndSettings
const div = 1 + serviceFee + feeRateLimit return { networkFeeBps: feeRateBps, networkFeeFixed: feeFixedLimit, serviceFeeBps: outgoingAppUserInvoiceFeeBps }
const max = Math.floor((balance - feeFixedLimit) / div) }
return { max, serviceFeeBps: outgoingAppUserInvoiceFeeBps, networkFeeBps: feeRateBps, networkFeeFixed: feeFixedLimit }
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 /* let maxWithinServiceFee = 0
if (appUser) { if (appUser) {
maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee))) 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}`, operation_id: `${Types.UserOperationType.OUTGOING_INVOICE}-${paymentInfo.serialId}`,
network_fee: paymentInfo.networkFee, network_fee: paymentInfo.networkFee,
service_fee: serviceFee, service_fee: serviceFee,
latest_balance: user.balance_sats
} }
} }

View file

@ -27,7 +27,7 @@ export class RugPullTracker {
const providerTracker = await this.storage.liquidityStorage.GetTrackedProvider('lnPub', pubDst) const providerTracker = await this.storage.liquidityStorage.GetTrackedProvider('lnPub', pubDst)
const ready = this.liquidProvider.IsReady() const ready = this.liquidProvider.IsReady()
if (ready) { if (ready) {
const balance = await this.liquidProvider.GetLatestBalance() const balance = this.liquidProvider.GetLatestBalance()
const pendingBalance = await this.liquidProvider.GetPendingBalance() const pendingBalance = await this.liquidProvider.GetPendingBalance()
const trackedBalance = balance + pendingBalance const trackedBalance = balance + pendingBalance
if (!providerTracker) { if (!providerTracker) {

View file

@ -2,7 +2,7 @@
import WebSocket from 'ws' import WebSocket from 'ws'
Object.assign(global, { WebSocket: WebSocket }); Object.assign(global, { WebSocket: WebSocket });
import crypto from 'crypto' 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 { ERROR, getLogger } from '../helpers/logger.js'
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.js' import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.js'
@ -26,6 +26,7 @@ export type NostrSettings = {
relays: string[] relays: string[]
clients: ClientInfo[] clients: ClientInfo[]
maxEventContentLength: number maxEventContentLength: number
providerDestinationPub: string
} }
export type NostrEvent = { export type NostrEvent = {
@ -69,9 +70,14 @@ type ProcessMetricsResponse = {
type: 'processMetrics' type: 'processMetrics'
metrics: ProcessMetrics metrics: ProcessMetrics
} }
type BeaconResponse = {
type: 'beacon'
content: string
pub: string
}
export type ChildProcessRequest = SettingsRequest | SendRequest | PingRequest 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) => { const send = (message: ChildProcessResponse) => {
if (process.send) { if (process.send) {
process.send(message, undefined, undefined, err => { process.send(message, undefined, undefined, err => {
@ -218,18 +224,28 @@ export default class Handler {
appIds: appIds, appIds: appIds,
listeningForPubkeys: appIds listeningForPubkeys: appIds
}) })
const subs: Filter[] = [
return relay.subscribe([
{ {
since: Math.ceil(Date.now() / 1000), since: Math.ceil(Date.now() / 1000),
kinds: supportedKinds, kinds: supportedKinds,
'#p': appIds, '#p': appIds,
} }
], { ]
if (this.settings.providerDestinationPub) {
subs.push({
kinds: [30078], '#d': ['Lightning.Pub'],
authors: [this.settings.providerDestinationPub]
})
}
return relay.subscribe(subs, {
oneose: () => { oneose: () => {
this.log("up to date with nostr events") this.log("up to date with nostr events")
}, },
onevent: async (e) => { 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) { if (!supportedKinds.includes(e.kind) || !e.pubkey) {
return return
} }

View file

@ -3,7 +3,7 @@ import { NostrSettings, NostrEvent, ChildProcessRequest, ChildProcessResponse, S
import { Utils } from '../helpers/utilsWrapper.js' import { Utils } from '../helpers/utilsWrapper.js'
import { getLogger, ERROR } from '../helpers/logger.js' import { getLogger, ERROR } from '../helpers/logger.js'
type EventCallback = (event: NostrEvent) => void type EventCallback = (event: NostrEvent) => void
type BeaconCallback = (beacon: { content: string, pub: string }) => void
@ -13,7 +13,7 @@ export default class NostrSubprocess {
utils: Utils utils: Utils
awaitingPongs: (() => void)[] = [] awaitingPongs: (() => void)[] = []
log = getLogger({}) log = getLogger({})
constructor(settings: NostrSettings, utils: Utils, eventCallback: EventCallback) { constructor(settings: NostrSettings, utils: Utils, eventCallback: EventCallback, beaconCallback: BeaconCallback) {
this.utils = utils this.utils = utils
this.childProcess = fork("./build/src/services/nostr/handler") this.childProcess = fork("./build/src/services/nostr/handler")
this.childProcess.on("error", (error) => { this.childProcess.on("error", (error) => {
@ -43,6 +43,9 @@ export default class NostrSubprocess {
this.awaitingPongs.forEach(resolve => resolve()) this.awaitingPongs.forEach(resolve => resolve())
this.awaitingPongs = [] this.awaitingPongs = []
break break
case 'beacon':
beaconCallback({ content: message.content, pub: message.pub })
break
default: default:
console.error("unknown nostr event response", message) console.error("unknown nostr event response", message)
break; break;