provider: wire beacon + return balance + use cached balance
This commit is contained in:
parent
c8ede119d6
commit
8e4a8b2a2a
17 changed files with 307 additions and 92 deletions
|
|
@ -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_
|
||||
|
|
|
|||
|
|
@ -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,6 +375,7 @@ type LiveManageRequest struct {
|
|||
Request_id string `json:"request_id"`
|
||||
}
|
||||
type LiveUserOperation struct {
|
||||
Latest_balance int64 `json:"latest_balance"`
|
||||
Operation *UserOperation `json:"operation"`
|
||||
}
|
||||
type LndChannels struct {
|
||||
|
|
@ -560,6 +573,7 @@ type PayInvoiceRequest struct {
|
|||
}
|
||||
type PayInvoiceResponse struct {
|
||||
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"`
|
||||
|
|
|
|||
|
|
@ -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`)
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
11
src/index.ts
11
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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
import {
|
||||
debitAccessRulesToDebitRules, newNdebitResponse, debitRulesToDebitAccessRules,
|
||||
nofferErrors, AuthRequiredRes, HandleNdebitRes, expirationRuleName,
|
||||
frequencyRuleName,IntervalTypeToSeconds,unitToIntervalType } from "./debitTypes.js";
|
||||
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,
|
||||
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<T> = { 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<string, number> = {}
|
||||
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<void>
|
||||
// make the sub process accept client
|
||||
constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise<any>) {
|
||||
|
|
@ -71,11 +78,12 @@ export class LiquidityProvider {
|
|||
}
|
||||
|
||||
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) {
|
||||
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'> => {
|
||||
|
|
@ -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,30 +135,38 @@ 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
|
||||
}
|
||||
|
|
@ -159,18 +176,19 @@ export class LiquidityProvider {
|
|||
return 0
|
||||
}
|
||||
return res.max_withdrawable
|
||||
}
|
||||
} */
|
||||
|
||||
GetLatestBalance = async () => {
|
||||
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.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) {
|
||||
|
|
|
|||
|
|
@ -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,10 +234,25 @@ 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 networkFeeBps = fees.networkFeeBps + fees.serviceFeeBps
|
||||
return { networkFeeBps, networkFeeFixed: fees.networkFeeFixed, serviceFeeBps: outgoingAppUserInvoiceFeeBps }
|
||||
}
|
||||
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
|
||||
|
|
@ -247,7 +264,7 @@ export default class {
|
|||
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 }
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue