Merge branch 'master' into fix-lnd-crash

This commit is contained in:
boufni95 2025-10-31 19:51:41 +00:00
commit 97d960a441
43 changed files with 947 additions and 621 deletions

View file

@ -20,6 +20,7 @@ import { UserOffer } from "./build/src/services/storage/entity/UserOffer.js"
import { ManagementGrant } from "./build/src/services/storage/entity/ManagementGrant.js" import { ManagementGrant } from "./build/src/services/storage/entity/ManagementGrant.js"
import { AppUserDevice } from "./build/src/services/storage/entity/AppUserDevice.js" import { AppUserDevice } from "./build/src/services/storage/entity/AppUserDevice.js"
import { UserAccess } from "./build/src/services/storage/entity/UserAccess.js" import { UserAccess } from "./build/src/services/storage/entity/UserAccess.js"
import { AdminSettings } from "./build/src/services/storage/entity/AdminSettings.js"
import { Initial1703170309875 } from './build/src/services/storage/migrations/1703170309875-initial.js' import { Initial1703170309875 } from './build/src/services/storage/migrations/1703170309875-initial.js'
import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js' import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js'
@ -37,6 +38,9 @@ import { InvoiceCallbackUrls1752425992291 } from './build/src/services/storage/m
import { OldSomethingLeftover1753106599604 } from './build/src/services/storage/migrations/1753106599604-old_something_leftover.js' import { OldSomethingLeftover1753106599604 } from './build/src/services/storage/migrations/1753106599604-old_something_leftover.js'
import { UserReceivingInvoiceIdx1753109184611 } from './build/src/services/storage/migrations/1753109184611-user_receiving_invoice_idx.js' import { UserReceivingInvoiceIdx1753109184611 } from './build/src/services/storage/migrations/1753109184611-user_receiving_invoice_idx.js'
import { AppUserDevice1753285173175 } from './build/src/services/storage/migrations/1753285173175-app_user_device.js' import { AppUserDevice1753285173175 } from './build/src/services/storage/migrations/1753285173175-app_user_device.js'
import { UserAccess1759426050669 } from './build/src/services/storage/migrations/1759426050669-user_access.js'
import { AddBlindToUserOffer1760000000000 } from './build/src/services/storage/migrations/1760000000000-add_blind_to_user_offer.js'
import { ApplicationAvatarUrl1761000001000 } from './build/src/services/storage/migrations/1761000001000-application_avatar_url.js'
export default new DataSource({ export default new DataSource({
type: "better-sqlite3", type: "better-sqlite3",
@ -45,10 +49,10 @@ export default new DataSource({
migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, CreateInviteTokenTable1721751414878, migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, CreateInviteTokenTable1721751414878,
PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043,
UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611,
AppUserDevice1753285173175], AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000],
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo, UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo,
TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice, UserAccess], TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice, UserAccess, AdminSettings],
// synchronize: true, // synchronize: true,
}) })
//npx typeorm migration:generate ./src/services/storage/migrations/user_access -d ./datasource.js //npx typeorm migration:generate ./src/services/storage/migrations/admin_settings -d ./datasource.js

View file

@ -15,6 +15,7 @@
# The developer is used by default or you may specify your own # The developer is used by default or you may specify your own
# To disable this feature entirely overwrite the env with "null" # To disable this feature entirely overwrite the env with "null"
#LIQUIDITY_PROVIDER_PUB=null #LIQUIDITY_PROVIDER_PUB=null
#DISABLE_LIQUIDITY_PROVIDER=false
#DB #DB
#DATABASE_FILE=db.sqlite #DATABASE_FILE=db.sqlite

View file

@ -14,7 +14,7 @@ const serverOptions = (mainHandler: Main): ServerOptions => {
UserAuthGuard: async (authHeader) => { return mainHandler.appUserManager.DecodeUserToken(stripBearer(authHeader)) }, UserAuthGuard: async (authHeader) => { return mainHandler.appUserManager.DecodeUserToken(stripBearer(authHeader)) },
GuestWithPubAuthGuard: async (_) => { throw new Error("Nostr only route") }, GuestWithPubAuthGuard: async (_) => { throw new Error("Nostr only route") },
GuestAuthGuard: async (_) => ({}), GuestAuthGuard: async (_) => ({}),
metricsCallback: metrics => mainHandler.settings.recordPerformance ? mainHandler.metricsManager.AddMetrics(metrics) : null, metricsCallback: metrics => mainHandler.settings.getSettings().serviceSettings.recordPerformance ? mainHandler.metricsManager.AddMetrics(metrics) : null,
allowCors: true, allowCors: true,
logMethod: true, logMethod: true,
logBody: true logBody: true

View file

@ -4,16 +4,17 @@ import GetServerMethods from './services/serverMethods/index.js'
import serverOptions from './auth.js'; import serverOptions from './auth.js';
import nostrMiddleware from './nostrMiddleware.js' import nostrMiddleware from './nostrMiddleware.js'
import { getLogger } from './services/helpers/logger.js'; import { getLogger } from './services/helpers/logger.js';
import { initMainHandler } from './services/main/init.js'; import { initMainHandler, initSettings } from './services/main/init.js';
import { LoadMainSettingsFromEnv } from './services/main/settings.js';
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
import { LoadStorageSettingsFromEnv } from './services/storage/index.js';
//@ts-ignore //@ts-ignore
const { nprofileEncode } = nip19 const { nprofileEncode } = nip19
const start = async () => { const start = async () => {
const log = getLogger({}) const log = getLogger({})
const mainSettings = LoadMainSettingsFromEnv() const storageSettings = LoadStorageSettingsFromEnv()
const keepOn = await initMainHandler(log, mainSettings) const settingsManager = await initSettings(log, storageSettings)
const keepOn = await initMainHandler(log, settingsManager)
if (!keepOn) { if (!keepOn) {
log("manual process ended") log("manual process ended")
return return
@ -21,7 +22,7 @@ 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)
const nostrSettings = mainSettings.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] },
@ -36,6 +37,6 @@ const start = async () => {
} }
adminManager.setAppNprofile(appNprofile) adminManager.setAppNprofile(appNprofile)
const Server = NewServer(serverMethods, serverOptions(mainHandler)) const Server = NewServer(serverMethods, serverOptions(mainHandler))
Server.Listen(mainSettings.servicePort) Server.Listen(settingsManager.getSettings().serviceSettings.servicePort)
} }
start() start()

View file

@ -4,16 +4,19 @@ import GetServerMethods from './services/serverMethods/index.js'
import serverOptions from './auth.js'; import serverOptions from './auth.js';
import nostrMiddleware from './nostrMiddleware.js' import nostrMiddleware from './nostrMiddleware.js'
import { getLogger } from './services/helpers/logger.js'; import { getLogger } from './services/helpers/logger.js';
import { initMainHandler } from './services/main/init.js'; import { initMainHandler, initSettings } from './services/main/init.js';
import { LoadMainSettingsFromEnv } from './services/main/settings.js';
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
import { LoadStorageSettingsFromEnv } from './services/storage/index.js';
//@ts-ignore //@ts-ignore
const { nprofileEncode } = nip19 const { nprofileEncode } = nip19
const start = async () => { const start = async () => {
const log = getLogger({}) const log = getLogger({})
const mainSettings = LoadMainSettingsFromEnv() //const mainSettings = LoadMainSettingsFromEnv()
const keepOn = await initMainHandler(log, mainSettings) const storageSettings = LoadStorageSettingsFromEnv()
const settingsManager = await initSettings(log, storageSettings)
const keepOn = await initMainHandler(log, settingsManager)
if (!keepOn) { if (!keepOn) {
log("manual process ended") log("manual process ended")
return return
@ -22,22 +25,25 @@ 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 { Send, Stop, Ping } = nostrMiddleware(serverMethods, mainHandler, const relays = mainHandler.settings.getSettings().nostrRelaySettings.relays
{ ...mainSettings.nostrRelaySettings, apps, clients: [liquidityProviderInfo] }, const maxEventContentLength = mainHandler.settings.getSettings().nostrRelaySettings.maxEventContentLength
const { Send, Stop, Ping, Reset } = nostrMiddleware(serverMethods, mainHandler,
{ relays, maxEventContentLength, apps, clients: [liquidityProviderInfo] },
(e, p) => mainHandler.liquidityProvider.onEvent(e, p) (e, p) => mainHandler.liquidityProvider.onEvent(e, p)
) )
exitHandler(() => { Stop(); mainHandler.Stop() }) exitHandler(() => { Stop(); mainHandler.Stop() })
log("starting server") log("starting server")
mainHandler.attachNostrSend(Send) mainHandler.attachNostrSend(Send)
mainHandler.attachNostrProcessPing(Ping) mainHandler.attachNostrProcessPing(Ping)
mainHandler.attachNostrReset(Reset)
mainHandler.StartBeacons() mainHandler.StartBeacons()
const appNprofile = nprofileEncode({ pubkey: liquidityProviderInfo.publicKey, relays: mainSettings.nostrRelaySettings.relays }) const appNprofile = nprofileEncode({ pubkey: liquidityProviderInfo.publicKey, relays })
if (wizard) { if (wizard) {
wizard.AddConnectInfo(appNprofile, mainSettings.nostrRelaySettings.relays) wizard.AddConnectInfo(appNprofile, relays)
} }
adminManager.setAppNprofile(appNprofile) adminManager.setAppNprofile(appNprofile)
const Server = NewServer(serverMethods, serverOptions(mainHandler)) const Server = NewServer(serverMethods, serverOptions(mainHandler))
Server.Listen(mainSettings.servicePort) Server.Listen(mainHandler.settings.getSettings().serviceSettings.servicePort)
} }
start() start()

View file

@ -5,8 +5,9 @@ import * as Types from '../proto/autogenerated/ts/types.js'
import NewNostrTransport, { NostrRequest } from '../proto/autogenerated/ts/nostr_transport.js'; import NewNostrTransport, { NostrRequest } from '../proto/autogenerated/ts/nostr_transport.js';
import { ERROR, getLogger } from "./services/helpers/logger.js"; import { ERROR, getLogger } from "./services/helpers/logger.js";
import { NdebitData, NofferData, NmanageRequest } from "@shocknet/clink-sdk"; import { NdebitData, NofferData, NmanageRequest } from "@shocknet/clink-sdk";
type ExportedCalls = { Stop: () => void, Send: NostrSend, Ping: () => Promise<void>, Reset: (settings: NostrSettings) => void }
export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings, onClientEvent: (e: { requestId: string }, fromPub: string) => void): { Stop: () => void, Send: NostrSend, Ping: () => Promise<void> } => { type ClientEventCallback = (e: { requestId: string }, fromPub: string) => void
export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings, onClientEvent: ClientEventCallback): ExportedCalls => {
const log = getLogger({}) const log = getLogger({})
const nostrTransport = NewNostrTransport(serverMethods, { const nostrTransport = NewNostrTransport(serverMethods, {
NostrUserAuthGuard: async (appId, pub) => { NostrUserAuthGuard: async (appId, pub) => {
@ -29,7 +30,7 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
log("operator access from", pub) log("operator access from", pub)
return { operator_id: pub, app_id: appId || "" } return { operator_id: pub, app_id: appId || "" }
}, },
metricsCallback: metrics => mainHandler.settings.recordPerformance ? mainHandler.metricsManager.AddMetrics(metrics) : null, metricsCallback: metrics => mainHandler.settings.getSettings().serviceSettings.recordPerformance ? mainHandler.metricsManager.AddMetrics(metrics) : null,
NostrGuestWithPubAuthGuard: async (appId, pub) => { NostrGuestWithPubAuthGuard: async (appId, pub) => {
if (!pub || !appId) { if (!pub || !appId) {
throw new Error("Unknown error occured") throw new Error("Unknown error occured")
@ -83,7 +84,12 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
// Mark nostr connected/ready after initial subscription tick // Mark nostr connected/ready after initial subscription tick
mainHandler.adminManager.setNostrConnected(true) mainHandler.adminManager.setNostrConnected(true)
return { Stop: () => { mainHandler.adminManager.setNostrConnected(false); return nostr.Stop }, Send: (...args) => nostr.Send(...args), Ping: () => nostr.Ping() } return {
Stop: () => { mainHandler.adminManager.setNostrConnected(false); return nostr.Stop },
Send: (...args) => nostr.Send(...args),
Ping: () => nostr.Ping(),
Reset: (settings: NostrSettings) => nostr.Reset(settings)
}
} }

View file

@ -26,3 +26,32 @@ export const EnvCanBeBoolean = (name: string): boolean => {
if (!env) return false if (!env) return false
return env.toLowerCase() === 'true' return env.toLowerCase() === 'true'
} }
export const IntOrUndefinedEnv = (v: string | undefined): number | undefined => {
if (!v) return undefined
const num = +v
if (isNaN(num) || !Number.isInteger(num)) return undefined
return num
}
export type EnvCacher = (key: string, value: string) => void
export const chooseEnv = (key: string, dbEnv: Record<string, string | undefined>, defaultValue: string, addToDb?: EnvCacher): string => {
const fromProcess = process.env[key]
if (fromProcess) {
if (fromProcess !== dbEnv[key] && addToDb) addToDb(key, fromProcess)
return fromProcess
}
return dbEnv[key] || defaultValue
}
export const chooseEnvInt = (key: string, dbEnv: Record<string, string | undefined>, defaultValue: number, addToDb?: EnvCacher): number => {
const v = IntOrUndefinedEnv(chooseEnv(key, dbEnv, defaultValue.toString(), addToDb))
if (v === undefined) return defaultValue
return v
}
export const chooseEnvBool = (key: string, dbEnv: Record<string, string | undefined>, defaultValue: boolean, addToDb?: EnvCacher): boolean => {
const v = chooseEnv(key, dbEnv, defaultValue.toString(), addToDb)
return v.toLowerCase() === 'true'
}

View file

@ -1,4 +1,3 @@
import { MainSettings } from "../main/settings.js";
import { StateBundler } from "../storage/tlv/stateBundler.js"; import { StateBundler } from "../storage/tlv/stateBundler.js";
import { TlvStorageFactory } from "../storage/tlv/tlvFilesStorageFactory.js"; import { TlvStorageFactory } from "../storage/tlv/tlvFilesStorageFactory.js";
import { NostrSend } from "../nostr/handler.js"; import { NostrSend } from "../nostr/handler.js";

View file

@ -1,26 +0,0 @@
import { EnvMustBeNonEmptyString, EnvMustBeInteger, EnvCanBeBoolean, EnvCanBeInteger } from '../helpers/envParser.js'
import { LndSettings } from './settings.js'
import os from 'os'
import path from 'path'
const resolveHome = (filepath: string) => {
let homeDir;
if (process.env.SUDO_USER) {
homeDir = path.join('/home', process.env.SUDO_USER);
} else {
homeDir = os.homedir();
}
return path.join(homeDir, filepath);
}
export const LoadLndSettingsFromEnv = (): LndSettings => {
const lndAddr = process.env.LND_ADDRESS || "127.0.0.1:10009"
const lndCertPath = process.env.LND_CERT_PATH || resolveHome("/.lnd/tls.cert")
const lndMacaroonPath = process.env.LND_MACAROON_PATH || resolveHome("/.lnd/data/chain/bitcoin/mainnet/admin.macaroon")
const lndLogDir = process.env.LND_LOG_DIR || resolveHome("/.lnd/logs/bitcoin/mainnet/lnd.log")
const feeRateBps = EnvCanBeInteger("OUTBOUND_MAX_FEE_BPS", 60)
const feeRateLimit = feeRateBps / 10000
const feeFixedLimit = EnvCanBeInteger("OUTBOUND_MAX_FEE_EXTRA_SATS", 100)
const mockLnd = EnvCanBeBoolean("MOCK_LND")
return { mainNode: { lndAddr, lndCertPath, lndMacaroonPath }, lndLogDir, feeRateLimit, feeFixedLimit, feeRateBps, mockLnd }
}

View file

@ -13,23 +13,31 @@ import { OpenChannelReq } from './openChannelReq.js';
import { AddInvoiceReq } from './addInvoiceReq.js'; import { AddInvoiceReq } from './addInvoiceReq.js';
import { PayInvoiceReq } from './payInvoiceReq.js'; import { PayInvoiceReq } from './payInvoiceReq.js';
import { SendCoinsReq } from './sendCoinsReq.js'; import { SendCoinsReq } from './sendCoinsReq.js';
import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo, ChannelEventCb } from './settings.js'; import { AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo, ChannelEventCb } from './settings.js';
import { ERROR, getLogger } from '../helpers/logger.js'; import { ERROR, getLogger } from '../helpers/logger.js';
import { HtlcEvent_EventType } from '../../../proto/lnd/router.js'; import { HtlcEvent_EventType } from '../../../proto/lnd/router.js';
import { LiquidityProvider, LiquidityRequest } from '../main/liquidityProvider.js'; import { LiquidityProvider, LiquidityRequest } from '../main/liquidityProvider.js';
import { Utils } from '../helpers/utilsWrapper.js'; import { Utils } from '../helpers/utilsWrapper.js';
import { TxPointSettings } from '../storage/tlv/stateBundler.js'; import { TxPointSettings } from '../storage/tlv/stateBundler.js';
import { WalletKitClient } from '../../../proto/lnd/walletkit.client.js'; import { WalletKitClient } from '../../../proto/lnd/walletkit.client.js';
import SettingsManager from '../main/settingsManager.js';
import { LndNodeSettings, LndSettings } from '../main/settings.js';
const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline }) const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
const deadLndRetrySeconds = 20 const deadLndRetrySeconds = 20
type TxActionOptions = { useProvider: boolean, from: 'user' | 'system' } type TxActionOptions = { useProvider: boolean, from: 'user' | 'system' }
type NodeSettingsOverride = {
lndAddr: string
lndCertPath: string
lndMacaroonPath: string
}
export default class { export default class {
lightning: LightningClient lightning: LightningClient
invoices: InvoicesClient invoices: InvoicesClient
router: RouterClient router: RouterClient
chainNotifier: ChainNotifierClient chainNotifier: ChainNotifierClient
walletKit: WalletKitClient walletKit: WalletKitClient
settings: LndSettings getSettings: () => { lndSettings: LndSettings, lndNodeSettings: LndNodeSettings }
ready = false ready = false
latestKnownBlockHeigh = 0 latestKnownBlockHeigh = 0
latestKnownSettleIndex = 0 latestKnownSettleIndex = 0
@ -44,8 +52,8 @@ export default class {
liquidProvider: LiquidityProvider liquidProvider: LiquidityProvider
utils: Utils utils: Utils
unlockLnd: () => Promise<void> unlockLnd: () => Promise<void>
constructor(settings: LndSettings, liquidProvider: LiquidityProvider, unlockLnd: () => Promise<any>, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb, channelEventCb: ChannelEventCb) { constructor(getSettings: () => { lndSettings: LndSettings, lndNodeSettings: LndNodeSettings }, liquidProvider: LiquidityProvider, unlockLnd: () => Promise<any>, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb, channelEventCb: ChannelEventCb) {
this.settings = settings this.getSettings = getSettings
this.utils = utils this.utils = utils
this.unlockLnd = unlockLnd this.unlockLnd = unlockLnd
this.addressPaidCb = addressPaidCb this.addressPaidCb = addressPaidCb
@ -53,7 +61,7 @@ export default class {
this.newBlockCb = newBlockCb this.newBlockCb = newBlockCb
this.htlcCb = htlcCb this.htlcCb = htlcCb
this.channelEventCb = channelEventCb this.channelEventCb = channelEventCb
const { lndAddr, lndCertPath, lndMacaroonPath } = this.settings.mainNode const { lndAddr, lndCertPath, lndMacaroonPath } = this.getSettings().lndNodeSettings
const lndCert = fs.readFileSync(lndCertPath); const lndCert = fs.readFileSync(lndCertPath);
const macaroon = fs.readFileSync(lndMacaroonPath).toString('hex'); const macaroon = fs.readFileSync(lndMacaroonPath).toString('hex');
const sslCreds = credentials.createSsl(lndCert); const sslCreds = credentials.createSsl(lndCert);
@ -335,11 +343,11 @@ export default class {
} }
GetFeeLimitAmount(amount: number): number { GetFeeLimitAmount(amount: number): number {
return Math.ceil(amount * this.settings.feeRateLimit + this.settings.feeFixedLimit); return Math.ceil(amount * this.getSettings().lndSettings.feeRateLimit + this.getSettings().lndSettings.feeFixedLimit);
} }
GetMaxWithinLimit(amount: number): number { GetMaxWithinLimit(amount: number): number {
return Math.max(0, Math.floor(amount * (1 - this.settings.feeRateLimit) - this.settings.feeFixedLimit)) return Math.max(0, Math.floor(amount * (1 - this.getSettings().lndSettings.feeRateLimit) - this.getSettings().lndSettings.feeFixedLimit))
} }
async ChannelBalance(): Promise<{ local: number, remote: number }> { async ChannelBalance(): Promise<{ local: number, remote: number }> {

View file

@ -3,24 +3,8 @@ import { LiquidityProvider } from "../main/liquidityProvider.js"
import { getLogger, PubLogger } from '../helpers/logger.js' import { getLogger, PubLogger } from '../helpers/logger.js'
import LND from "./lnd.js" import LND from "./lnd.js"
import { AddressType } from "../../../proto/autogenerated/ts/types.js" import { AddressType } from "../../../proto/autogenerated/ts/types.js"
import { EnvCanBeInteger } from "../helpers/envParser.js" import SettingsManager from "../main/settingsManager.js"
export type LSPSettings = {
olympusServiceUrl: string
voltageServiceUrl: string
flashsatsServiceUrl: string
channelThreshold: number
maxRelativeFee: number
}
export const LoadLSPSettingsFromEnv = (): LSPSettings => {
const olympusServiceUrl = process.env.OLYMPUS_LSP_URL || "https://lsps1.lnolymp.us/api/v1"
const voltageServiceUrl = process.env.VOLTAGE_LSP_URL || "https://lsp.voltageapi.com/api/v1"
const flashsatsServiceUrl = process.env.FLASHSATS_LSP_URL || "https://lsp.flashsats.xyz/lsp/channel"
const channelThreshold = EnvCanBeInteger("LSP_CHANNEL_THRESHOLD", 1000000)
const maxRelativeFee = EnvCanBeInteger("LSP_MAX_FEE_BPS", 100) / 10000
return { olympusServiceUrl, voltageServiceUrl, channelThreshold, maxRelativeFee, flashsatsServiceUrl }
}
type OlympusOrder = { type OlympusOrder = {
"lsp_balance_sat": string, "lsp_balance_sat": string,
"client_balance_sat": string, "client_balance_sat": string,
@ -50,11 +34,11 @@ type OrderResponse = {
} }
class LSP { class LSP {
settings: LSPSettings settings: SettingsManager
liquidityProvider: LiquidityProvider liquidityProvider: LiquidityProvider
lnd: LND lnd: LND
log: PubLogger log: PubLogger
constructor(serviceName: string, settings: LSPSettings, lnd: LND, liquidityProvider: LiquidityProvider) { constructor(serviceName: string, settings: SettingsManager, lnd: LND, liquidityProvider: LiquidityProvider) {
this.settings = settings this.settings = settings
this.lnd = lnd this.lnd = lnd
this.liquidityProvider = liquidityProvider this.liquidityProvider = liquidityProvider
@ -71,12 +55,15 @@ class LSP {
} }
export class FlashsatsLSP extends LSP { export class FlashsatsLSP extends LSP {
constructor(settings: LSPSettings, lnd: LND, liquidityProvider: LiquidityProvider) { constructor(settings: SettingsManager, lnd: LND, liquidityProvider: LiquidityProvider) {
super("FlashsatsLSP", settings, lnd, liquidityProvider) super("FlashsatsLSP", settings, lnd, liquidityProvider)
} }
requestChannel = async (maxSpendable: number): Promise<OrderResponse | null> => { requestChannel = async (maxSpendable: number): Promise<OrderResponse | null> => {
if (!this.settings.flashsatsServiceUrl) { const s = this.settings.getSettings().lspSettings
const flashsatsServiceUrl = s.flashsatsServiceUrl
const maxRelativeFee = s.maxRelativeFee
if (!flashsatsServiceUrl) {
this.log("no flashsats service url provided") this.log("no flashsats service url provided")
return null return null
} }
@ -91,7 +78,7 @@ export class FlashsatsLSP extends LSP {
this.log("no uri found for this node,uri is required to use flashsats") this.log("no uri found for this node,uri is required to use flashsats")
return null return null
} }
const channelSize = Math.floor(maxSpendable * (1 - this.settings.maxRelativeFee)) * 2 const channelSize = Math.floor(maxSpendable * (1 - maxRelativeFee)) * 2
const lspBalance = channelSize.toString() const lspBalance = channelSize.toString()
const chanExpiryBlocks = serviceInfo.options.max_channel_expiry_blocks const chanExpiryBlocks = serviceInfo.options.max_channel_expiry_blocks
const order = await this.createOrder({ nodeUri: myUri, lspBalance, clientBalance: "0", chanExpiryBlocks }) const order = await this.createOrder({ nodeUri: myUri, lspBalance, clientBalance: "0", chanExpiryBlocks })
@ -109,8 +96,8 @@ export class FlashsatsLSP extends LSP {
return null return null
} }
const relativeFee = +order.payment.fee_total_sat / channelSize const relativeFee = +order.payment.fee_total_sat / channelSize
if (relativeFee > this.settings.maxRelativeFee) { if (relativeFee > maxRelativeFee) {
this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee) this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", maxRelativeFee)
return null return null
} }
const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice, decoded.numSatoshis, 'system') const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice, decoded.numSatoshis, 'system')
@ -120,7 +107,7 @@ export class FlashsatsLSP extends LSP {
} }
getInfo = async () => { getInfo = async () => {
const res = await fetch(`${this.settings.flashsatsServiceUrl}/info`) const res = await fetch(`${this.settings.getSettings().lspSettings.flashsatsServiceUrl}/info`)
const json = await res.json() as { options: { min_initial_client_balance_sat: string, max_channel_expiry_blocks: number } } const json = await res.json() as { options: { min_initial_client_balance_sat: string, max_channel_expiry_blocks: number } }
return json return json
} }
@ -134,7 +121,7 @@ export class FlashsatsLSP extends LSP {
confirms_within_blocks: 6, confirms_within_blocks: 6,
token: "flashsats" token: "flashsats"
} }
const res = await fetch(`${this.settings.flashsatsServiceUrl}/channel`, { const res = await fetch(`${this.settings.getSettings().lspSettings.flashsatsServiceUrl}/channel`, {
method: "POST", method: "POST",
body: JSON.stringify(req), body: JSON.stringify(req),
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" }
@ -145,12 +132,15 @@ export class FlashsatsLSP extends LSP {
} }
export class OlympusLSP extends LSP { export class OlympusLSP extends LSP {
constructor(settings: LSPSettings, lnd: LND, liquidityProvider: LiquidityProvider) { constructor(settings: SettingsManager, lnd: LND, liquidityProvider: LiquidityProvider) {
super("OlympusLSP", settings, lnd, liquidityProvider) super("OlympusLSP", settings, lnd, liquidityProvider)
} }
requestChannel = async (maxSpendable: number): Promise<OrderResponse | null> => { requestChannel = async (maxSpendable: number): Promise<OrderResponse | null> => {
if (!this.settings.olympusServiceUrl) { const s = this.settings.getSettings().lspSettings
const olympusServiceUrl = s.olympusServiceUrl
const maxRelativeFee = s.maxRelativeFee
if (!olympusServiceUrl) {
this.log("no olympus service url provided") this.log("no olympus service url provided")
return null return null
} }
@ -164,7 +154,7 @@ export class OlympusLSP extends LSP {
const lndInfo = await this.lnd.GetInfo() const lndInfo = await this.lnd.GetInfo()
const myPub = lndInfo.identityPubkey const myPub = lndInfo.identityPubkey
const refundAddr = await this.lnd.NewAddress(AddressType.WITNESS_PUBKEY_HASH, { useProvider: false, from: 'system' }) const refundAddr = await this.lnd.NewAddress(AddressType.WITNESS_PUBKEY_HASH, { useProvider: false, from: 'system' })
const channelSize = Math.floor(maxSpendable * (1 - this.settings.maxRelativeFee)) * 2 const channelSize = Math.floor(maxSpendable * (1 - maxRelativeFee)) * 2
const lspBalance = channelSize.toString() const lspBalance = channelSize.toString()
const chanExpiryBlocks = serviceInfo.max_channel_expiry_blocks const chanExpiryBlocks = serviceInfo.max_channel_expiry_blocks
const order = await this.createOrder({ pubKey: myPub, refundAddr: refundAddr.address, lspBalance, clientBalance: "0", chanExpiryBlocks }) const order = await this.createOrder({ pubKey: myPub, refundAddr: refundAddr.address, lspBalance, clientBalance: "0", chanExpiryBlocks })
@ -182,8 +172,8 @@ export class OlympusLSP extends LSP {
return null return null
} }
const relativeFee = +order.payment.bolt11.fee_total_sat / channelSize const relativeFee = +order.payment.bolt11.fee_total_sat / channelSize
if (relativeFee > this.settings.maxRelativeFee) { if (relativeFee > maxRelativeFee) {
this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee) this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", maxRelativeFee)
return null return null
} }
const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11.invoice, decoded.numSatoshis, 'system') const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11.invoice, decoded.numSatoshis, 'system')
@ -193,7 +183,7 @@ export class OlympusLSP extends LSP {
} }
getInfo = async () => { getInfo = async () => {
const res = await fetch(`${this.settings.olympusServiceUrl}/get_info`) const res = await fetch(`${this.settings.getSettings().lspSettings.olympusServiceUrl}/get_info`)
const json = await res.json() as { min_initial_client_balance_sat: string, max_channel_expiry_blocks: number, uris: string[] } const json = await res.json() as { min_initial_client_balance_sat: string, max_channel_expiry_blocks: number, uris: string[] }
return json return json
} }
@ -209,7 +199,7 @@ export class OlympusLSP extends LSP {
funding_confirms_within_blocks: 6, funding_confirms_within_blocks: 6,
required_channel_confirmations: 0 required_channel_confirmations: 0
} }
const res = await fetch(`${this.settings.olympusServiceUrl}/create_order`, { const res = await fetch(`${this.settings.getSettings().lspSettings.olympusServiceUrl}/create_order`, {
method: "POST", method: "POST",
body: JSON.stringify(req), body: JSON.stringify(req),
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" }
@ -219,7 +209,7 @@ export class OlympusLSP extends LSP {
} }
getOrder = async (orderId: string) => { getOrder = async (orderId: string) => {
const res = await fetch(`${this.settings.olympusServiceUrl}/get_order&order_id=${orderId}`) const res = await fetch(`${this.settings.getSettings().lspSettings.olympusServiceUrl}/get_order&order_id=${orderId}`)
const json = await res.json() as {} const json = await res.json() as {}
return json return json
} }

View file

@ -1,21 +1,5 @@
import { Channel, ChannelEventUpdate } from "../../../proto/lnd/lightning" import { Channel, ChannelEventUpdate } from "../../../proto/lnd/lightning"
import { HtlcEvent } from "../../../proto/lnd/router" import { HtlcEvent } from "../../../proto/lnd/router"
export type NodeSettings = {
lndAddr: string
lndCertPath: string
lndMacaroonPath: string
}
export type LndSettings = {
mainNode: NodeSettings
lndLogDir: string
feeRateLimit: number
feeFixedLimit: number
feeRateBps: number
mockLnd: boolean
otherNode?: NodeSettings
thirdNode?: NodeSettings
}
type TxOutput = { type TxOutput = {
hash: string hash: string
@ -63,3 +47,6 @@ export type PaidInvoice = {
paymentPreimage: string paymentPreimage: string
providerDst?: string providerDst?: string
} }

View file

@ -1,16 +1,11 @@
import fs, { watchFile } from "fs"; import fs, { watchFile } from "fs";
import crypto from 'crypto' import crypto from 'crypto'
import { ERROR, getLogger } from "../helpers/logger.js"; import { ERROR, getLogger } from "../helpers/logger.js";
import { MainSettings, getDataPath } from "./settings.js";
import Storage from "../storage/index.js"; import Storage from "../storage/index.js";
import * as Types from '../../../proto/autogenerated/ts/types.js' import * as Types from '../../../proto/autogenerated/ts/types.js'
import LND from "../lnd/lnd.js"; import LND from "../lnd/lnd.js";
import SettingsManager from "./settingsManager.js";
export class AdminManager { export class AdminManager {
storage: Storage storage: Storage
log = getLogger({ component: "adminManager" }) log = getLogger({ component: "adminManager" })
adminNpub = "" adminNpub = ""
@ -23,9 +18,10 @@ export class AdminManager {
appNprofile: string appNprofile: string
lnd: LND lnd: LND
nostrConnected: boolean = false nostrConnected: boolean = false
constructor(mainSettings: MainSettings, storage: Storage) { private nostrReset: () => Promise<void> = async () => { this.log("nostr reset not initialized yet") }
constructor(settings: SettingsManager, storage: Storage) {
this.storage = storage this.storage = storage
this.dataDir = mainSettings.storageSettings.dataDir this.dataDir = settings.getStorageSettings().dataDir
this.adminNpubPath = getDataPath(this.dataDir, 'admin.npub') this.adminNpubPath = getDataPath(this.dataDir, 'admin.npub')
this.adminEnrollTokenPath = getDataPath(this.dataDir, 'admin.enroll') this.adminEnrollTokenPath = getDataPath(this.dataDir, 'admin.enroll')
this.adminConnectPath = getDataPath(this.dataDir, 'admin.connect') this.adminConnectPath = getDataPath(this.dataDir, 'admin.connect')
@ -39,6 +35,14 @@ export class AdminManager {
this.start() this.start()
} }
attachNostrReset(f: () => Promise<void>) {
this.nostrReset = f
}
async ResetNostr() {
await this.nostrReset()
}
setLND = (lnd: LND) => { setLND = (lnd: LND) => {
this.lnd = lnd this.lnd = lnd
} }
@ -250,3 +254,7 @@ export class AdminManager {
} }
} }
} }
const getDataPath = (dataDir: string, dataPath: string) => {
return dataDir !== "" ? `${dataDir}/${dataPath}` : dataPath
}

View file

@ -2,23 +2,23 @@ import jwt from 'jsonwebtoken'
import Storage from '../storage/index.js' import Storage from '../storage/index.js'
import * as Types from '../../../proto/autogenerated/ts/types.js' import * as Types from '../../../proto/autogenerated/ts/types.js'
import { MainSettings } from './settings.js'
import ApplicationManager from './applicationManager.js' import ApplicationManager from './applicationManager.js'
import { OfferPriceType, ndebitEncode, nmanageEncode, nofferEncode } from '@shocknet/clink-sdk' import { OfferPriceType, ndebitEncode, nmanageEncode, nofferEncode } from '@shocknet/clink-sdk'
import { getLogger } from '../helpers/logger.js' import { getLogger } from '../helpers/logger.js'
import SettingsManager from './settingsManager.js'
export default class { export default class {
storage: Storage storage: Storage
settings: MainSettings settings: SettingsManager
applicationManager: ApplicationManager applicationManager: ApplicationManager
log = getLogger({ component: 'AppUserManager' }) log = getLogger({ component: 'AppUserManager' })
constructor(storage: Storage, settings: MainSettings, applicationManager: ApplicationManager) { constructor(storage: Storage, settings: SettingsManager, applicationManager: ApplicationManager) {
this.storage = storage this.storage = storage
this.settings = settings this.settings = settings
this.applicationManager = applicationManager this.applicationManager = applicationManager
} }
SignUserToken(userId: string, appId: string, userIdentifier: string): string { SignUserToken(userId: string, appId: string, userIdentifier: string): string {
return jwt.sign({ user_id: userId, app_id: appId, app_user_id: userIdentifier }, this.settings.jwtSecret); return jwt.sign({ user_id: userId, app_id: appId, app_user_id: userIdentifier }, this.settings.getStorageSettings().jwtSecret);
} }
DecodeUserToken(token?: string): { user_id: string, app_id: string, app_user_id: string } { DecodeUserToken(token?: string): { user_id: string, app_id: string, app_user_id: string } {
@ -28,7 +28,7 @@ export default class {
t = token.substring("Bearer ".length) t = token.substring("Bearer ".length)
} }
if (!t) throw new Error("no user token provided") if (!t) throw new Error("no user token provided")
const decoded = jwt.verify(token, this.settings.jwtSecret) as { user_id: string, app_id: string, app_user_id: string } const decoded = jwt.verify(token, this.settings.getStorageSettings().jwtSecret) as { user_id: string, app_id: string, app_user_id: string }
if (!decoded.user_id || !decoded.app_id || !decoded.app_user_id) { if (!decoded.user_id || !decoded.app_id || !decoded.app_user_id) {
throw new Error("the provided token is not a valid app user token token") throw new Error("the provided token is not a valid app user token token")
} }
@ -37,11 +37,11 @@ export default class {
} }
GetHttpCreds(ctx: Types.UserContext): Types.HttpCreds { GetHttpCreds(ctx: Types.UserContext): Types.HttpCreds {
if (!this.settings.allowHttpUpgrade) { if (!this.settings.getSettings().serviceSettings.allowHttpUpgrade) {
throw new Error("http upgrade not allowed") throw new Error("http upgrade not allowed")
} }
return { return {
url: this.settings.serviceUrl, url: this.settings.getSettings().serviceSettings.serviceUrl,
token: this.SignUserToken(ctx.user_id, ctx.app_id, ctx.app_user_id) token: this.SignUserToken(ctx.user_id, ctx.app_id, ctx.app_user_id)
} }
} }
@ -68,20 +68,20 @@ export default class {
if (!appUser) { if (!appUser) {
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.nostrRelaySettings const nostrSettings = this.settings.getSettings().nostrRelaySettings
return { return {
userId: ctx.user_id, userId: ctx.user_id,
balance: user.balance_sats, balance: user.balance_sats,
max_withdrawable: this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats, true), max_withdrawable: this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats, true),
user_identifier: appUser.identifier, user_identifier: appUser.identifier,
network_max_fee_bps: this.settings.lndSettings.feeRateBps, network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps,
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit, network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit,
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps, service_fee_bps: this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps,
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: appUser.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }), noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: appUser.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }),
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }), ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }),
nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }), nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }),
callback_url: appUser.callback_url, callback_url: appUser.callback_url,
bridge_url: this.settings.bridgeUrl bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl
} }
} }
@ -124,24 +124,24 @@ export default class {
async CleanupInactiveUsers() { async CleanupInactiveUsers() {
this.log("Cleaning up inactive users") this.log("Cleaning up inactive users")
const inactiveUsers = await this.storage.userStorage.GetInactiveUsers(365) const inactiveUsers = await this.storage.userStorage.GetInactiveUsers(365)
const toDelete:{userId: string, appUserIds: string[]}[] = [] const toDelete: { userId: string, appUserIds: string[] }[] = []
for (const u of inactiveUsers) { for (const u of inactiveUsers) {
const user = await this.storage.userStorage.GetUser(u.user_id) const user = await this.storage.userStorage.GetUser(u.user_id)
if (user.balance_sats > 10_000) { if (user.balance_sats > 10_000) {
continue continue
} }
const appUsers = await this.storage.applicationStorage.GetAllAppUsersFromUser(u.user_id) const appUsers = await this.storage.applicationStorage.GetAllAppUsersFromUser(u.user_id)
toDelete.push({userId: u.user_id, appUserIds: appUsers.map(a => a.identifier)}) toDelete.push({ userId: u.user_id, appUserIds: appUsers.map(a => a.identifier) })
} }
this.log("Found",toDelete.length, "inactive users to delete") this.log("Found", toDelete.length, "inactive users to delete")
// await this.RemoveUsers(toDelete) // await this.RemoveUsers(toDelete)
} }
async CleanupNeverActiveUsers() { async CleanupNeverActiveUsers() {
this.log("Cleaning up never active users") this.log("Cleaning up never active users")
const inactiveUsers = await this.storage.userStorage.GetInactiveUsers(30) const inactiveUsers = await this.storage.userStorage.GetInactiveUsers(30)
const toDelete:{userId: string, appUserIds: string[]}[] = [] const toDelete: { userId: string, appUserIds: string[] }[] = []
for (const u of inactiveUsers) { for (const u of inactiveUsers) {
const user = await this.storage.userStorage.GetUser(u.user_id) const user = await this.storage.userStorage.GetUser(u.user_id)
if (user.balance_sats > 0) { if (user.balance_sats > 0) {
@ -160,18 +160,18 @@ export default class {
continue continue
} }
const appUsers = await this.storage.applicationStorage.GetAllAppUsersFromUser(u.user_id) const appUsers = await this.storage.applicationStorage.GetAllAppUsersFromUser(u.user_id)
toDelete.push({userId: u.user_id, appUserIds: appUsers.map(a => a.identifier)}) toDelete.push({ userId: u.user_id, appUserIds: appUsers.map(a => a.identifier) })
} }
this.log("Found",toDelete.length, "never active users to delete") this.log("Found", toDelete.length, "never active users to delete")
// await this.RemoveUsers(toDelete) TODO: activate deletion // await this.RemoveUsers(toDelete) TODO: activate deletion
} }
async RemoveUsers(toDelete: { userId: string, appUserIds: string[] }[]) { async RemoveUsers(toDelete: { userId: string, appUserIds: string[] }[]) {
this.log("Deleting",toDelete.length, "inactive users") this.log("Deleting", toDelete.length, "inactive users")
for (let i = 0; i < toDelete.length; i++) { for (let i = 0; i < toDelete.length; i++) {
const {userId,appUserIds} = toDelete[i] const { userId, appUserIds } = toDelete[i]
this.log("Deleting user", userId, "progress", i+1, "/", toDelete.length) this.log("Deleting user", userId, "progress", i + 1, "/", toDelete.length)
await this.storage.StartTransaction(async tx => { await this.storage.StartTransaction(async tx => {
for (const appUserId of appUserIds) { for (const appUserId of appUserIds) {
await this.storage.managementStorage.removeUserGrants(appUserId, tx) await this.storage.managementStorage.removeUserGrants(appUserId, tx)

View file

@ -1,7 +1,6 @@
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import Storage from '../storage/index.js' import Storage from '../storage/index.js'
import * as Types from '../../../proto/autogenerated/ts/types.js' import * as Types from '../../../proto/autogenerated/ts/types.js'
import { MainSettings } from './settings.js'
import PaymentManager from './paymentManager.js' import PaymentManager from './paymentManager.js'
import { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorage.js' import { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorage.js'
import { ApplicationUser } from '../storage/entity/ApplicationUser.js' import { ApplicationUser } from '../storage/entity/ApplicationUser.js'
@ -10,6 +9,7 @@ import crypto from 'crypto'
import { Application } from '../storage/entity/Application.js' import { Application } from '../storage/entity/Application.js'
import { ZapInfo } from '../storage/entity/UserReceivingInvoice.js' import { ZapInfo } from '../storage/entity/UserReceivingInvoice.js'
import { nofferEncode, ndebitEncode, OfferPriceType, nmanageEncode } from '@shocknet/clink-sdk' import { nofferEncode, ndebitEncode, OfferPriceType, nmanageEncode } from '@shocknet/clink-sdk'
import SettingsManager from './settingsManager.js'
const TOKEN_EXPIRY_TIME = 2 * 60 * 1000 // 2 minutes, in milliseconds const TOKEN_EXPIRY_TIME = 2 * 60 * 1000 // 2 minutes, in milliseconds
type NsecLinkingData = { type NsecLinkingData = {
@ -19,13 +19,13 @@ type NsecLinkingData = {
export default class { export default class {
storage: Storage storage: Storage
settings: MainSettings settings: SettingsManager
paymentManager: PaymentManager paymentManager: PaymentManager
nPubLinkingTokens = new Map<string, NsecLinkingData>(); nPubLinkingTokens = new Map<string, NsecLinkingData>();
linkingTokenInterval: NodeJS.Timeout | null = null linkingTokenInterval: NodeJS.Timeout | null = null
serviceBeaconInterval: NodeJS.Timeout | null = null serviceBeaconInterval: NodeJS.Timeout | null = null
log: PubLogger log: PubLogger
constructor(storage: Storage, settings: MainSettings, paymentManager: PaymentManager) { constructor(storage: Storage, settings: SettingsManager, paymentManager: PaymentManager) {
this.log = getLogger({ component: "ApplicationManager" }) this.log = getLogger({ component: "ApplicationManager" })
this.storage = storage this.storage = storage
this.settings = settings this.settings = settings
@ -69,7 +69,7 @@ export default class {
} }
} }
SignAppToken(appId: string): string { SignAppToken(appId: string): string {
return jwt.sign({ appId }, this.settings.jwtSecret); return jwt.sign({ appId }, this.settings.getStorageSettings().jwtSecret);
} }
DecodeAppToken(token?: string): string { DecodeAppToken(token?: string): string {
if (!token) throw new Error("empty app token provided") if (!token) throw new Error("empty app token provided")
@ -78,7 +78,7 @@ export default class {
t = token.substring("Bearer ".length) t = token.substring("Bearer ".length)
} }
if (!t) throw new Error("no app token provided") if (!t) throw new Error("no app token provided")
const decoded = jwt.verify(token, this.settings.jwtSecret) as { appId?: string } const decoded = jwt.verify(token, this.settings.getStorageSettings().jwtSecret) as { appId?: string }
if (!decoded.appId) { if (!decoded.appId) {
throw new Error("the provided token is not an app token") throw new Error("the provided token is not an app token")
} }
@ -150,7 +150,7 @@ export default class {
u = user u = user
if (created) log(u.identifier, u.user.user_id, "user created") if (created) log(u.identifier, u.user.user_id, "user created")
} }
const nostrSettings = this.settings.nostrRelaySettings const nostrSettings = this.settings.getSettings().nostrRelaySettings
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 })
@ -162,14 +162,14 @@ export default class {
balance: u.user.balance_sats, balance: u.user.balance_sats,
max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true), max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true),
user_identifier: u.identifier, user_identifier: u.identifier,
network_max_fee_bps: this.settings.lndSettings.feeRateBps, network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps,
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit, network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit,
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps, service_fee_bps: this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps,
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: u.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }), noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: u.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }),
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }), ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }),
nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }), nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }),
callback_url: u.callback_url, callback_url: u.callback_url,
bridge_url: this.settings.bridgeUrl bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl
}, },
max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true) max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true)
@ -212,20 +212,20 @@ 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 max = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true) const max = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true)
const nostrSettings = this.settings.nostrRelaySettings const nostrSettings = this.settings.getSettings().nostrRelaySettings
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,
max_withdrawable: this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true), max_withdrawable: this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true),
user_identifier: user.identifier, user_identifier: user.identifier,
network_max_fee_bps: this.settings.lndSettings.feeRateBps, network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps,
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit, network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit,
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps, service_fee_bps: this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps,
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: user.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }), noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: user.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }),
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }), ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }),
nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }), nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }),
callback_url: user.callback_url, callback_url: user.callback_url,
bridge_url: this.settings.bridgeUrl bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl
}, },
} }
} }

View file

@ -5,7 +5,6 @@ import * as Types from '../../../proto/autogenerated/ts/types.js'
import ProductManager from './productManager.js' import ProductManager from './productManager.js'
import ApplicationManager from './applicationManager.js' import ApplicationManager from './applicationManager.js'
import PaymentManager, { PendingTx } from './paymentManager.js' import PaymentManager, { PendingTx } from './paymentManager.js'
import { MainSettings } from './settings.js'
import LND from "../lnd/lnd.js" import LND from "../lnd/lnd.js"
import { AddressPaidCb, ChannelEventCb, HtlcCb, InvoicePaidCb, NewBlockCb } from "../lnd/settings.js" import { AddressPaidCb, ChannelEventCb, HtlcCb, InvoicePaidCb, NewBlockCb } from "../lnd/settings.js"
import { ERROR, getLogger, PubLogger } from "../helpers/logger.js" import { ERROR, getLogger, PubLogger } from "../helpers/logger.js"
@ -31,7 +30,8 @@ import { ManagementManager } from "./managementManager.js"
import { Agent } from "https" import { Agent } from "https"
import { NotificationsManager } from "./notificationsManager.js" import { NotificationsManager } from "./notificationsManager.js"
import { ApplicationUser } from '../storage/entity/ApplicationUser.js' import { ApplicationUser } from '../storage/entity/ApplicationUser.js'
import SettingsManager from './settingsManager.js'
import { NostrSettings } from '../nostr/handler.js'
type UserOperationsSub = { type UserOperationsSub = {
id: string id: string
newIncomingInvoice: (operation: Types.UserOperation) => void newIncomingInvoice: (operation: Types.UserOperation) => void
@ -44,7 +44,7 @@ const appTag = "Lightning.Pub"
export default class { export default class {
storage: Storage storage: Storage
lnd: LND lnd: LND
settings: MainSettings settings: SettingsManager
userOperationsSub: UserOperationsSub | null = null userOperationsSub: UserOperationsSub | null = null
adminManager: AdminManager adminManager: AdminManager
productManager: ProductManager productManager: ProductManager
@ -65,17 +65,22 @@ export default class {
//webRTC: webRTC //webRTC: webRTC
nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") } nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") }
nostrProcessPing: (() => Promise<void>) | null = null nostrProcessPing: (() => Promise<void>) | null = null
constructor(settings: MainSettings, storage: Storage, adminManager: AdminManager, utils: Utils, unlocker: Unlocker) { nostrReset: (settings: NostrSettings) => void = () => { getLogger({})("nostr reset not initialized yet") }
constructor(settings: SettingsManager, storage: Storage, adminManager: AdminManager, utils: Utils, unlocker: Unlocker) {
this.settings = settings this.settings = settings
this.storage = storage this.storage = storage
this.adminManager = adminManager this.adminManager = adminManager
this.utils = utils this.utils = utils
this.unlocker = unlocker this.unlocker = unlocker
const updateProviderBalance = (b: number) => this.storage.liquidityStorage.IncrementTrackedProviderBalance('lnPub', settings.liquiditySettings.liquidityProviderPub, b) const updateProviderBalance = (b: number) => this.storage.liquidityStorage.IncrementTrackedProviderBalance('lnPub', settings.getSettings().liquiditySettings.liquidityProviderPub, b)
this.liquidityProvider = new LiquidityProvider(settings.liquiditySettings.liquidityProviderPub, this.utils, this.invoicePaidCb, updateProviderBalance) this.liquidityProvider = new LiquidityProvider(() => this.settings.getSettings().liquiditySettings, this.utils, this.invoicePaidCb, updateProviderBalance)
this.rugPullTracker = new RugPullTracker(this.storage, this.liquidityProvider) this.rugPullTracker = new RugPullTracker(this.storage, this.liquidityProvider)
this.lnd = new LND(settings.lndSettings, this.liquidityProvider, () => this.unlocker.Unlock(), this.utils, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb, this.channelEventCb) const lndGetSettings = () => ({
this.liquidityManager = new LiquidityManager(this.settings.liquiditySettings, this.storage, this.utils, this.liquidityProvider, this.lnd, this.rugPullTracker) lndSettings: settings.getSettings().lndSettings,
lndNodeSettings: settings.getSettings().lndNodeSettings
})
this.lnd = new LND(lndGetSettings, this.liquidityProvider, () => this.unlocker.Unlock(), this.utils, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb, this.channelEventCb)
this.liquidityManager = new LiquidityManager(this.settings, this.storage, this.utils, this.liquidityProvider, this.lnd, this.rugPullTracker)
this.metricsManager = new MetricsManager(this.storage, this.lnd) this.metricsManager = new MetricsManager(this.storage, this.lnd)
this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings, this.liquidityManager, this.utils, this.addressPaidCb, this.invoicePaidCb) this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings, this.liquidityManager, this.utils, this.addressPaidCb, this.invoicePaidCb)
@ -85,7 +90,7 @@ export default class {
this.debitManager = new DebitManager(this.storage, this.lnd, this.applicationManager) this.debitManager = new DebitManager(this.storage, this.lnd, this.applicationManager)
this.offerManager = new OfferManager(this.storage, this.settings, this.lnd, this.applicationManager, this.productManager, this.liquidityManager) this.offerManager = new OfferManager(this.storage, this.settings, this.lnd, this.applicationManager, this.productManager, this.liquidityManager)
this.managementManager = new ManagementManager(this.storage, this.settings) this.managementManager = new ManagementManager(this.storage, this.settings)
this.notificationsManager = new NotificationsManager(this.settings.shockPushBaseUrl) this.notificationsManager = new NotificationsManager(this.settings)
//this.webRTC = new webRTC(this.storage, this.utils) //this.webRTC = new webRTC(this.storage, this.utils)
} }
@ -99,7 +104,7 @@ export default class {
StartBeacons() { StartBeacons() {
this.applicationManager.StartAppsServiceBeacon(app => { this.applicationManager.StartAppsServiceBeacon(app => {
this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: (app as any).avatar_url }) this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url })
}) })
} }
@ -117,6 +122,11 @@ export default class {
this.nostrProcessPing = f this.nostrProcessPing = f
} }
attachNostrReset(f: (settings: NostrSettings) => void) {
this.nostrReset = f
this.adminManager.attachNostrReset(() => this.ResetNostr())
}
async pingSubProcesses() { async pingSubProcesses() {
if (!this.nostrProcessPing) { if (!this.nostrProcessPing) {
throw new Error("nostr process ping not initialized") throw new Error("nostr process ping not initialized")
@ -386,7 +396,7 @@ export default class {
}) })
} }
async UpdateBeacon(app: Application, content: { type: 'service', name: string, avatarUrl?: string }) { async UpdateBeacon(app: Application, content: { type: 'service', name: string, avatarUrl?: string, nextRelay?: string }) {
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
@ -421,6 +431,32 @@ export default class {
log({ unsigned: event }) log({ unsigned: event })
this.nostrSend({ type: 'app', appId: invoice.linkedApplication.app_id }, { type: 'event', event }, zapInfo.relays || undefined) this.nostrSend({ type: 'app', appId: invoice.linkedApplication.app_id }, { type: 'event', event }, zapInfo.relays || undefined)
} }
async ResetNostr() {
const apps = await this.storage.applicationStorage.GetApplications()
const nextRelay = this.settings.getSettings().nostrRelaySettings.relays[0]
for (const app of apps) {
await this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, nextRelay })
}
const defaultNames = ['wallet', 'wallet-test', this.settings.getSettings().serviceSettings.defaultAppName]
const liquidityProviderApp = apps.find(app => defaultNames.includes(app.name))
if (!liquidityProviderApp) {
throw new Error("wallet app not initialized correctly")
}
const liquidityProviderInfo = {
privateKey: liquidityProviderApp.nostr_private_key || "",
publicKey: liquidityProviderApp.nostr_public_key || "",
name: "liquidity_provider", clientId: `client_${liquidityProviderApp.app_id}`
}
const s: NostrSettings = {
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]
}
this.nostrReset(s)
}
} }

View file

@ -1,55 +1,56 @@
import { PubLogger, getLogger } from "../helpers/logger.js" import { PubLogger, getLogger } from "../helpers/logger.js"
import { LiquidityProvider } from "./liquidityProvider.js" import { LiquidityProvider } from "./liquidityProvider.js"
import { Unlocker } from "./unlocker.js" import { Unlocker } from "./unlocker.js"
import Storage from "../storage/index.js" import Storage, { StorageSettings } from "../storage/index.js"
/* import { TypeOrmMigrationRunner } from "../storage/migrations/runner.js" */ /* import { TypeOrmMigrationRunner } from "../storage/migrations/runner.js" */
import Main from "./index.js" import Main from "./index.js"
import SanityChecker from "./sanityChecker.js" import SanityChecker from "./sanityChecker.js"
import { LoadMainSettingsFromEnv, MainSettings } from "./settings.js"
import { Utils } from "../helpers/utilsWrapper.js" import { Utils } from "../helpers/utilsWrapper.js"
import { Wizard } from "../wizard/index.js" import { Wizard } from "../wizard/index.js"
import { AdminManager } from "./adminManager.js" import { AdminManager } from "./adminManager.js"
import { TlvStorageFactory } from "../storage/tlv/tlvFilesStorageFactory.js" import SettingsManager from "./settingsManager.js"
import { LoadStorageSettingsFromEnv } from "../storage/index.js"
export type AppData = { export type AppData = {
privateKey: string; privateKey: string;
publicKey: string; publicKey: string;
appId: string; appId: string;
name: string; name: string;
} }
export const initMainHandler = async (log: PubLogger, mainSettings: MainSettings) => {
const utils = new Utils({ dataDir: mainSettings.storageSettings.dataDir, allowResetMetricsStorages: mainSettings.allowResetMetricsStorages }) export const initSettings = async (log: PubLogger, storageSettings: StorageSettings): Promise<SettingsManager> => {
const storageManager = new Storage(mainSettings.storageSettings, utils) const utils = new Utils({ dataDir: storageSettings.dataDir, allowResetMetricsStorages: storageSettings.allowResetMetricsStorages })
const storageManager = new Storage(storageSettings, utils)
await storageManager.Connect(log) await storageManager.Connect(log)
/* const manualMigration = await TypeOrmMigrationRunner(log, storageManager, mainSettings.storageSettings.dbSettings, process.argv[2]) const settingsManager = new SettingsManager(storageManager)
if (manualMigration) { await settingsManager.InitSettings()
return return settingsManager
} */ }
const unlocker = new Unlocker(mainSettings, storageManager) export const initMainHandler = async (log: PubLogger, settingsManager: SettingsManager) => {
const storageManager = settingsManager.storage
const utils = storageManager.utils
const unlocker = new Unlocker(settingsManager, storageManager)
await unlocker.Unlock() await unlocker.Unlock()
const adminManager = new AdminManager(mainSettings, storageManager) const adminManager = new AdminManager(settingsManager, storageManager)
let reloadedSettings = mainSettings
let wizard: Wizard | null = null let wizard: Wizard | null = null
if (mainSettings.wizard) { if (settingsManager.getSettings().serviceSettings.wizard) {
wizard = new Wizard(mainSettings, storageManager, adminManager) wizard = new Wizard(settingsManager, storageManager, adminManager)
const reload = await wizard.Configure() await wizard.Configure()
if (reload) {
reloadedSettings = LoadMainSettingsFromEnv()
}
} }
const mainHandler = new Main(reloadedSettings, storageManager, adminManager, utils, unlocker) const mainHandler = new Main(settingsManager, storageManager, adminManager, utils, unlocker)
adminManager.setLND(mainHandler.lnd) adminManager.setLND(mainHandler.lnd)
await mainHandler.lnd.Warmup() await mainHandler.lnd.Warmup()
if (!reloadedSettings.skipSanityCheck) { if (!settingsManager.getSettings().serviceSettings.skipSanityCheck) {
const sanityChecker = new SanityChecker(storageManager, mainHandler.lnd) const sanityChecker = new SanityChecker(storageManager, mainHandler.lnd)
await sanityChecker.VerifyEventsLog() await sanityChecker.VerifyEventsLog()
} }
const defaultAppName = settingsManager.getSettings().serviceSettings.defaultAppName
const appsData = await mainHandler.storage.applicationStorage.GetApplications() const appsData = await mainHandler.storage.applicationStorage.GetApplications()
const defaultNames = ['wallet', 'wallet-test', reloadedSettings.defaultAppName] const defaultNames = ['wallet', 'wallet-test', defaultAppName]
const existingWalletApp = await appsData.find(app => defaultNames.includes(app.name)) const existingWalletApp = await appsData.find(app => defaultNames.includes(app.name))
if (!existingWalletApp) { if (!existingWalletApp) {
log("no default wallet app found, creating one...") log("no default wallet app found, creating one...")
const newWalletApp = await mainHandler.storage.applicationStorage.AddApplication(reloadedSettings.defaultAppName, true) const newWalletApp = await mainHandler.storage.applicationStorage.AddApplication(defaultAppName, true)
appsData.push(newWalletApp) appsData.push(newWalletApp)
} }
const apps: AppData[] = await Promise.all(appsData.map(app => { const apps: AppData[] = await Promise.all(appsData.map(app => {

View file

@ -2,22 +2,14 @@ import { getLogger } from "../helpers/logger.js"
import { Utils } from "../helpers/utilsWrapper.js" import { Utils } from "../helpers/utilsWrapper.js"
import { LiquidityProvider } from "./liquidityProvider.js" import { LiquidityProvider } from "./liquidityProvider.js"
import LND from "../lnd/lnd.js" import LND from "../lnd/lnd.js"
import { FlashsatsLSP, LoadLSPSettingsFromEnv, LSPSettings, OlympusLSP, /* VoltageLSP */ } from "../lnd/lsp.js" import { FlashsatsLSP, OlympusLSP, /* VoltageLSP */ } from "../lnd/lsp.js"
import Storage from '../storage/index.js' import Storage from '../storage/index.js'
import { defaultInvoiceExpiry } from "../storage/paymentStorage.js" import { defaultInvoiceExpiry } from "../storage/paymentStorage.js"
import { RugPullTracker } from "./rugPullTracker.js" import { RugPullTracker } from "./rugPullTracker.js"
export type LiquiditySettings = { import SettingsManager from "./settingsManager.js"
lspSettings: LSPSettings
liquidityProviderPub: string
useOnlyLiquidityProvider: boolean
}
export const LoadLiquiditySettingsFromEnv = (): LiquiditySettings => {
const lspSettings = LoadLSPSettingsFromEnv()
const liquidityProviderPub = process.env.LIQUIDITY_PROVIDER_PUB === "null" ? "" : (process.env.LIQUIDITY_PROVIDER_PUB || "76ed45f00cea7bac59d8d0b7d204848f5319d7b96c140ffb6fcbaaab0a13d44e")
return { lspSettings, liquidityProviderPub, useOnlyLiquidityProvider: false }
}
export class LiquidityManager { export class LiquidityManager {
settings: LiquiditySettings settings: SettingsManager
storage: Storage storage: Storage
liquidityProvider: LiquidityProvider liquidityProvider: LiquidityProvider
rugPullTracker: RugPullTracker rugPullTracker: RugPullTracker
@ -32,16 +24,16 @@ export class LiquidityManager {
utils: Utils utils: Utils
latestDrain: ({ success: true, amt: number } | { success: false, amt: number, attempt: number, at: Date }) = { success: true, amt: 0 } latestDrain: ({ success: true, amt: number } | { success: false, amt: number, attempt: number, at: Date }) = { success: true, amt: 0 }
drainsSkipped = 0 drainsSkipped = 0
constructor(settings: LiquiditySettings, storage: Storage, utils: Utils, liquidityProvider: LiquidityProvider, lnd: LND, rugPullTracker: RugPullTracker) { constructor(settings: SettingsManager, storage: Storage, utils: Utils, liquidityProvider: LiquidityProvider, lnd: LND, rugPullTracker: RugPullTracker) {
this.settings = settings this.settings = settings
this.storage = storage this.storage = storage
this.liquidityProvider = liquidityProvider this.liquidityProvider = liquidityProvider
this.lnd = lnd this.lnd = lnd
this.rugPullTracker = rugPullTracker this.rugPullTracker = rugPullTracker
this.utils = utils this.utils = utils
this.olympusLSP = new OlympusLSP(settings.lspSettings, lnd, liquidityProvider) this.olympusLSP = new OlympusLSP(settings, lnd, liquidityProvider)
/* this.voltageLSP = new VoltageLSP(settings.lspSettings, lnd, liquidityProvider) */ /* this.voltageLSP = new VoltageLSP(settings.lspSettings, lnd, liquidityProvider) */
this.flashsatsLSP = new FlashsatsLSP(settings.lspSettings, lnd, liquidityProvider) this.flashsatsLSP = new FlashsatsLSP(settings, lnd, liquidityProvider)
} }
GetPaidFees = () => { GetPaidFees = () => {
@ -58,7 +50,8 @@ export class LiquidityManager {
} }
beforeInvoiceCreation = async (amount: number): Promise<'lnd' | 'provider'> => { beforeInvoiceCreation = async (amount: number): Promise<'lnd' | 'provider'> => {
if (this.settings.useOnlyLiquidityProvider) {
if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) {
return 'provider' return 'provider'
} }
@ -86,7 +79,7 @@ export class LiquidityManager {
} }
beforeOutInvoicePayment = async (amount: number): Promise<'lnd' | 'provider'> => { beforeOutInvoicePayment = async (amount: number): Promise<'lnd' | 'provider'> => {
if (this.settings.useOnlyLiquidityProvider) { if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) {
return 'provider' return 'provider'
} }
const canHandle = await this.liquidityProvider.CanProviderHandle({ action: 'spend', amount }) const canHandle = await this.liquidityProvider.CanProviderHandle({ action: 'spend', amount })
@ -155,7 +148,7 @@ export class LiquidityManager {
shouldOpenChannel = async (): Promise<{ shouldOpen: false } | { shouldOpen: true, maxSpendable: number }> => { shouldOpenChannel = async (): Promise<{ shouldOpen: false } | { shouldOpen: true, maxSpendable: number }> => {
const threshold = this.settings.lspSettings.channelThreshold const threshold = this.settings.getSettings().lspSettings.channelThreshold
if (threshold === 0) { if (threshold === 0) {
return { shouldOpen: false } return { shouldOpen: false }
} }

View file

@ -6,11 +6,13 @@ 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'
import Storage from '../storage/index.js' 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 LiquidityRequest = { action: 'spend' | 'receive', amount: number }
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
client: ReturnType<typeof newNostrClient> client: ReturnType<typeof newNostrClient>
clientCbs: Record<string, nostrCallback<any>> = {} clientCbs: Record<string, nostrCallback<any>> = {}
clientId: string = "" clientId: string = ""
@ -28,12 +30,19 @@ export class LiquidityProvider {
pendingPayments: Record<string, number> = {} pendingPayments: Record<string, number> = {}
incrementProviderBalance: (balance: number) => Promise<void> incrementProviderBalance: (balance: number) => Promise<void>
// make the sub process accept client // make the sub process accept client
constructor(pubDestination: string, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise<any>) { constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise<any>) {
this.utils = utils this.utils = utils
this.getSettings = getSettings
const pubDestination = getSettings().liquidityProviderPub
const disableLiquidityProvider = getSettings().disableLiquidityProvider
if (!pubDestination) { if (!pubDestination) {
this.log("No pub provider to liquidity provider, will not be initialized") this.log("No pub provider to liquidity provider, will not be initialized")
return return
} }
if (disableLiquidityProvider) {
this.log("Liquidity provider is disabled, will not be initialized")
return
}
this.log("connecting to liquidity provider:", pubDestination) this.log("connecting to liquidity provider:", pubDestination)
this.pubDestination = pubDestination this.pubDestination = pubDestination
this.invoicePaidCb = invoicePaidCb this.invoicePaidCb = invoicePaidCb
@ -59,14 +68,14 @@ export class LiquidityProvider {
} }
IsReady = () => { IsReady = () => {
return this.ready return this.ready && !this.getSettings().disableLiquidityProvider
} }
AwaitProviderReady = async (): Promise<'inactive' | 'ready'> => { AwaitProviderReady = async (): Promise<'inactive' | 'ready'> => {
if (!this.pubDestination) { if (!this.pubDestination || this.getSettings().disableLiquidityProvider) {
return 'inactive' return 'inactive'
} }
if (this.ready) { if (this.IsReady()) {
return 'ready' return 'ready'
} }
return new Promise<'ready'>(res => { return new Promise<'ready'>(res => {
@ -119,7 +128,7 @@ export class LiquidityProvider {
} }
GetLatestMaxWithdrawable = async () => { GetLatestMaxWithdrawable = async () => {
if (!this.ready) { if (!this.IsReady()) {
return 0 return 0
} }
const res = await this.GetUserState() const res = await this.GetUserState()
@ -131,7 +140,7 @@ export class LiquidityProvider {
} }
GetLatestBalance = async () => { GetLatestBalance = async () => {
if (!this.ready) { if (!this.IsReady()) {
return 0 return 0
} }
const res = await this.GetUserState() const res = await this.GetUserState()
@ -155,7 +164,7 @@ export class LiquidityProvider {
} }
CanProviderHandle = async (req: LiquidityRequest) => { CanProviderHandle = async (req: LiquidityRequest) => {
if (!this.ready) { if (!this.IsReady()) {
return false return false
} }
const maxW = await this.GetLatestMaxWithdrawable() const maxW = await this.GetLatestMaxWithdrawable()
@ -167,8 +176,8 @@ export class LiquidityProvider {
AddInvoice = async (amount: number, memo: string, from: 'user' | 'system', expiry: number) => { AddInvoice = async (amount: number, memo: string, from: 'user' | 'system', expiry: number) => {
try { try {
if (!this.ready) { if (!this.IsReady()) {
throw new Error("liquidity provider is not ready yet") throw new Error("liquidity provider is not ready yet or disabled")
} }
const res = await this.client.NewInvoice({ amountSats: amount, memo, expiry }) const res = await this.client.NewInvoice({ amountSats: amount, memo, expiry })
if (res.status === 'ERROR') { if (res.status === 'ERROR') {
@ -186,8 +195,8 @@ export class LiquidityProvider {
PayInvoice = async (invoice: string, decodedAmount: number, from: 'user' | 'system') => { PayInvoice = async (invoice: string, decodedAmount: number, from: 'user' | 'system') => {
try { try {
if (!this.ready) { if (!this.IsReady()) {
throw new Error("liquidity provider is not ready yet") throw new Error("liquidity provider is not ready yet or disabled")
} }
const userInfo = await this.GetUserState() const userInfo = await this.GetUserState()
if (userInfo.status === 'ERROR') { if (userInfo.status === 'ERROR') {
@ -211,8 +220,8 @@ export class LiquidityProvider {
} }
GetPaymentState = async (invoice: string) => { GetPaymentState = async (invoice: string) => {
if (!this.ready) { if (!this.IsReady()) {
throw new Error("liquidity provider is not ready yet") throw new Error("liquidity provider is not ready yet or disabled")
} }
const res = await this.client.GetPaymentState({ invoice }) const res = await this.client.GetPaymentState({ invoice })
if (res.status === 'ERROR') { if (res.status === 'ERROR') {
@ -223,8 +232,8 @@ export class LiquidityProvider {
} }
GetOperations = async () => { GetOperations = async () => {
if (!this.ready) { if (!this.IsReady()) {
throw new Error("liquidity provider is not ready yet") throw new Error("liquidity provider is not ready yet or disabled")
} }
const res = await this.client.GetUserOperations({ const res = await this.client.GetUserOperations({
latestIncomingInvoice: { ts: 0, id: 0 }, latestOutgoingInvoice: { ts: 0, id: 0 }, latestIncomingInvoice: { ts: 0, id: 0 }, latestOutgoingInvoice: { ts: 0, id: 0 },

View file

@ -6,19 +6,19 @@ import { NostrEvent, NostrSend } from "../nostr/handler.js";
import Storage from "../storage/index.js"; import Storage from "../storage/index.js";
import { OfferManager } from "./offerManager.js"; import { OfferManager } from "./offerManager.js";
import * as Types from "../../../proto/autogenerated/ts/types.js"; import * as Types from "../../../proto/autogenerated/ts/types.js";
import { MainSettings } from "./settings.js";
import { nofferEncode, OfferPointer, OfferPriceType, NmanageRequest, NmanageResponse, NmanageCreateOffer, NmanageUpdateOffer, NmanageDeleteOffer, NmanageGetOffer, NmanageListOffers, OfferData, OfferFields, NmanageFailure } from "@shocknet/clink-sdk"; import { nofferEncode, OfferPointer, OfferPriceType, NmanageRequest, NmanageResponse, NmanageCreateOffer, NmanageUpdateOffer, NmanageDeleteOffer, NmanageGetOffer, NmanageListOffers, OfferData, OfferFields, NmanageFailure } from "@shocknet/clink-sdk";
import { UnsignedEvent } from "nostr-tools"; import { UnsignedEvent } from "nostr-tools";
import { getLogger, PubLogger, ERROR } from "../helpers/logger.js"; import { getLogger, PubLogger, ERROR } from "../helpers/logger.js";
import SettingsManager from "./settingsManager.js";
type Result<T> = { state: 'success', result: T } | { state: 'error', err: NmanageFailure } | { state: 'authRequired' } type Result<T> = { state: 'success', result: T } | { state: 'error', err: NmanageFailure } | { state: 'authRequired' }
export class ManagementManager { export class ManagementManager {
private nostrSend: NostrSend; private nostrSend: NostrSend;
private storage: Storage; private storage: Storage;
private settings: MainSettings; private settings: SettingsManager;
private awaitingRequests: Record<string, { request: NmanageRequest, event: NostrEvent }> = {} private awaitingRequests: Record<string, { request: NmanageRequest, event: NostrEvent }> = {}
private logger: PubLogger private logger: PubLogger
constructor(storage: Storage, settings: MainSettings) { constructor(storage: Storage, settings: SettingsManager) {
this.storage = storage; this.storage = storage;
this.settings = settings; this.settings = settings;
this.logger = getLogger({ component: 'ManagementManager' }) this.logger = getLogger({ component: 'ManagementManager' })
@ -141,7 +141,7 @@ export class ManagementManager {
const pointer: OfferPointer = { const pointer: OfferPointer = {
offer: offer.offer_id, offer: offer.offer_id,
pubkey: appPub, pubkey: appPub,
relay: this.settings.nostrRelaySettings.relays[0], relay: this.settings.getSettings().nostrRelaySettings.relays[0],
priceType: offer.price_sats > 0 ? OfferPriceType.Fixed : OfferPriceType.Spontaneous, priceType: offer.price_sats > 0 ? OfferPriceType.Fixed : OfferPriceType.Spontaneous,
price: offer.price_sats, price: offer.price_sats,
} }

View file

@ -1,12 +1,13 @@
import { PushPair, ShockPush } from "../ShockPush/index.js" import { PushPair, ShockPush } from "../ShockPush/index.js"
import { getLogger, PubLogger } from "../helpers/logger.js" import { getLogger, PubLogger } from "../helpers/logger.js"
import SettingsManager from "./settingsManager.js"
export class NotificationsManager { export class NotificationsManager {
private shockPushBaseUrl: string private settings: SettingsManager
private clients: Record<string, ShockPush> = {} private clients: Record<string, ShockPush> = {}
private logger: PubLogger private logger: PubLogger
constructor(shockPushBaseUrl: string) { constructor(settings: SettingsManager) {
this.shockPushBaseUrl = shockPushBaseUrl this.settings = settings
this.logger = getLogger({ component: 'notificationsManager' }) this.logger = getLogger({ component: 'notificationsManager' })
} }
@ -15,13 +16,13 @@ export class NotificationsManager {
if (client) { if (client) {
return client return client
} }
const newClient = new ShockPush(this.shockPushBaseUrl, pair) const newClient = new ShockPush(this.settings.getSettings().serviceSettings.shockPushBaseUrl, pair)
this.clients[pair.pubkey] = newClient this.clients[pair.pubkey] = newClient
return newClient return newClient
} }
SendNotification = async (message: string, messagingTokens: string[], pair: PushPair) => { SendNotification = async (message: string, messagingTokens: string[], pair: PushPair) => {
if (!this.shockPushBaseUrl) { if (!this.settings.getSettings().serviceSettings.shockPushBaseUrl) {
this.logger("ShockPush is not configured, skipping notification") this.logger("ShockPush is not configured, skipping notification")
return return
} }

View file

@ -9,7 +9,7 @@ import { UnsignedEvent } from 'nostr-tools';
import { UserOffer } from '../storage/entity/UserOffer.js'; import { UserOffer } from '../storage/entity/UserOffer.js';
import { LiquidityManager } from "./liquidityManager.js" import { LiquidityManager } from "./liquidityManager.js"
import { NofferData, OfferPriceType, nofferEncode } from '@shocknet/clink-sdk'; import { NofferData, OfferPriceType, nofferEncode } from '@shocknet/clink-sdk';
import { MainSettings } from "./settings.js"; import SettingsManager from "./settingsManager.js";
const mapToOfferConfig = (appUserId: string, offer: UserOffer, { pubkey, relay }: { pubkey: string, relay: string }): Types.OfferConfig => { const mapToOfferConfig = (appUserId: string, offer: UserOffer, { pubkey, relay }: { pubkey: string, relay: string }): Types.OfferConfig => {
const offerStr = offer.offer_id const offerStr = offer.offer_id
@ -34,14 +34,14 @@ export class OfferManager {
_nostrSend: NostrSend | null = null _nostrSend: NostrSend | null = null
settings: MainSettings settings: SettingsManager
applicationManager: ApplicationManager applicationManager: ApplicationManager
productManager: ProductManager productManager: ProductManager
storage: Storage storage: Storage
lnd: LND lnd: LND
liquidityManager: LiquidityManager liquidityManager: LiquidityManager
logger = getLogger({ component: 'OfferManager' }) logger = getLogger({ component: 'OfferManager' })
constructor(storage: Storage, settings: MainSettings, lnd: LND, applicationManager: ApplicationManager, productManager: ProductManager, liquidityManager: LiquidityManager) { constructor(storage: Storage, settings: SettingsManager, lnd: LND, applicationManager: ApplicationManager, productManager: ProductManager, liquidityManager: LiquidityManager) {
this.storage = storage this.storage = storage
this.settings = settings this.settings = settings
this.lnd = lnd this.lnd = lnd
@ -112,7 +112,7 @@ export class OfferManager {
if (!offer) { if (!offer) {
throw new Error("Offer not found") throw new Error("Offer not found")
} }
const nostrSettings = this.settings.nostrRelaySettings const nostrSettings = this.settings.getSettings().nostrRelaySettings
return mapToOfferConfig(ctx.app_user_id, offer, { pubkey: app.npub, relay: nostrSettings.relays[0] }) return mapToOfferConfig(ctx.app_user_id, offer, { pubkey: app.npub, relay: nostrSettings.relays[0] })
} }
@ -130,7 +130,7 @@ export class OfferManager {
if (toAppend) { if (toAppend) {
offers.push(toAppend) offers.push(toAppend)
} }
const nostrSettings = this.settings.nostrRelaySettings const nostrSettings = this.settings.getSettings().nostrRelaySettings
return { return {
offers: offers.map(o => mapToOfferConfig(ctx.app_user_id, o, { pubkey: app.npub, relay: nostrSettings.relays[0] })) offers: offers.map(o => mapToOfferConfig(ctx.app_user_id, o, { pubkey: app.npub, relay: nostrSettings.relays[0] }))
} }
@ -205,7 +205,7 @@ export class OfferManager {
} }
const res = await this.applicationManager.AddAppUserInvoice(appId, { const res = await this.applicationManager.AddAppUserInvoice(appId, {
http_callback_url: "", payer_identifier: offer, receiver_identifier: offer, http_callback_url: "", payer_identifier: offer, receiver_identifier: offer,
invoice_req: { amountSats: amount, memo: memo ||"Default CLINK Offer", zap: offerReq.zap, expiry }, invoice_req: { amountSats: amount, memo: memo || "Default CLINK Offer", zap: offerReq.zap, expiry },
offer_string: 'offer' offer_string: 'offer'
}) })
return { success: true, invoice: res.invoice } return { success: true, invoice: res.invoice }
@ -214,7 +214,7 @@ export class OfferManager {
async HandleUserOffer(offerReq: NofferData, appId: string, remote: number): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { async HandleUserOffer(offerReq: NofferData, appId: string, remote: number): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> {
const { amount_sats: amount, offer } = offerReq const { amount_sats: amount, offer } = offerReq
const userOffer = await this.storage.offerStorage.GetOffer(offer) const userOffer = await this.storage.offerStorage.GetOffer(offer)
const expiry = offerReq.expires_in_seconds ? offerReq.expires_in_seconds : undefined const expiry = offerReq.expires_in_seconds ? offerReq.expires_in_seconds : undefined
if (!userOffer) { if (!userOffer) {
return this.HandleDefaultUserOffer(offerReq, appId, remote, { memo: offerReq.description, expiry }) return this.HandleDefaultUserOffer(offerReq, appId, remote, { memo: offerReq.description, expiry })

View file

@ -2,7 +2,6 @@ import { bech32 } from 'bech32'
import crypto from 'crypto' import crypto from 'crypto'
import Storage from '../storage/index.js' import Storage from '../storage/index.js'
import * as Types from '../../../proto/autogenerated/ts/types.js' import * as Types from '../../../proto/autogenerated/ts/types.js'
import { MainSettings } from './settings.js'
import { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorage.js' import { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorage.js'
import LND from '../lnd/lnd.js' import LND from '../lnd/lnd.js'
import { Application } from '../storage/entity/Application.js' import { Application } from '../storage/entity/Application.js'
@ -17,6 +16,7 @@ import { Watchdog } from './watchdog.js'
import { LiquidityManager } from './liquidityManager.js' import { LiquidityManager } from './liquidityManager.js'
import { Utils } from '../helpers/utilsWrapper.js' import { Utils } from '../helpers/utilsWrapper.js'
import { UserInvoicePayment } from '../storage/entity/UserInvoicePayment.js' import { UserInvoicePayment } from '../storage/entity/UserInvoicePayment.js'
import SettingsManager from './settingsManager.js'
interface UserOperationInfo { interface UserOperationInfo {
serial_id: number serial_id: number
paid_amount: number paid_amount: number
@ -43,7 +43,7 @@ const confInOne = 1000 * 1000
const confInTwo = 100 * 1000 * 1000 const confInTwo = 100 * 1000 * 1000
export default class { export default class {
storage: Storage storage: Storage
settings: MainSettings settings: SettingsManager
lnd: LND lnd: LND
addressPaidCb: AddressPaidCb addressPaidCb: AddressPaidCb
invoicePaidCb: InvoicePaidCb invoicePaidCb: InvoicePaidCb
@ -51,13 +51,13 @@ export default class {
watchDog: Watchdog watchDog: Watchdog
liquidityManager: LiquidityManager liquidityManager: LiquidityManager
utils: Utils utils: Utils
constructor(storage: Storage, lnd: LND, settings: MainSettings, liquidityManager: LiquidityManager, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) { constructor(storage: Storage, lnd: LND, settings: SettingsManager, liquidityManager: LiquidityManager, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
this.storage = storage this.storage = storage
this.settings = settings this.settings = settings
this.lnd = lnd this.lnd = lnd
this.liquidityManager = liquidityManager this.liquidityManager = liquidityManager
this.utils = utils this.utils = utils
this.watchDog = new Watchdog(settings.watchDogSettings, this.liquidityManager, this.lnd, this.storage, this.utils, this.liquidityManager.rugPullTracker) this.watchDog = new Watchdog(settings, this.liquidityManager, this.lnd, this.storage, this.utils, this.liquidityManager.rugPullTracker)
this.addressPaidCb = addressPaidCb this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb this.invoicePaidCb = invoicePaidCb
} }
@ -163,38 +163,38 @@ export default class {
getServiceFee(action: Types.UserOperationType, amount: number, appUser: boolean): number { getServiceFee(action: Types.UserOperationType, amount: number, appUser: boolean): number {
switch (action) { switch (action) {
case Types.UserOperationType.INCOMING_TX: case Types.UserOperationType.INCOMING_TX:
return Math.ceil(this.settings.incomingTxFee * amount) return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingTxFee * amount)
case Types.UserOperationType.OUTGOING_TX: case Types.UserOperationType.OUTGOING_TX:
return Math.ceil(this.settings.outgoingTxFee * amount) return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingTxFee * amount)
case Types.UserOperationType.INCOMING_INVOICE: case Types.UserOperationType.INCOMING_INVOICE:
if (appUser) { if (appUser) {
return Math.ceil(this.settings.incomingAppUserInvoiceFee * amount) return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppUserInvoiceFee * amount)
} }
return Math.ceil(this.settings.incomingAppInvoiceFee * amount) return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppInvoiceFee * amount)
case Types.UserOperationType.OUTGOING_INVOICE: case Types.UserOperationType.OUTGOING_INVOICE:
if (appUser) { if (appUser) {
return Math.ceil(this.settings.outgoingAppUserInvoiceFee * amount) return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee * amount)
} }
return Math.ceil(this.settings.outgoingAppInvoiceFee * amount) return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingAppInvoiceFee * amount)
case Types.UserOperationType.OUTGOING_USER_TO_USER || Types.UserOperationType.INCOMING_USER_TO_USER: case Types.UserOperationType.OUTGOING_USER_TO_USER || Types.UserOperationType.INCOMING_USER_TO_USER:
if (appUser) { if (appUser) {
return Math.ceil(this.settings.userToUserFee * amount) return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount)
} }
return Math.ceil(this.settings.appToUserFee * amount) return Math.ceil(this.settings.getSettings().serviceFeeSettings.appToUserFee * amount)
default: default:
throw new Error("Unknown service action type") throw new Error("Unknown service action type")
} }
} }
async SetMockInvoiceAsPaid(req: Types.SetMockInvoiceAsPaidRequest) { async SetMockInvoiceAsPaid(req: Types.SetMockInvoiceAsPaidRequest) {
if (!this.settings.lndSettings.mockLnd) { if (!this.settings.getSettings().lndSettings.mockLnd) {
throw new Error("mock disabled, cannot set invoice as paid") throw new Error("mock disabled, cannot set invoice as paid")
} }
await this.lnd.SetMockInvoiceAsPaid(req.invoice, req.amount) await this.lnd.SetMockInvoiceAsPaid(req.invoice, req.amount)
} }
async SetMockUserBalance(userId: string, balance: number) { async SetMockUserBalance(userId: string, balance: number) {
if (!this.settings.lndSettings.mockLnd) { if (!this.settings.getSettings().lndSettings.mockLnd) {
throw new Error("mock disabled, cannot set invoice as paid") throw new Error("mock disabled, cannot set invoice as paid")
} }
getLogger({})("setting mock balance...") getLogger({})("setting mock balance...")
@ -235,9 +235,9 @@ export default class {
GetMaxPayableInvoice(balance: number, appUser: boolean): number { GetMaxPayableInvoice(balance: number, appUser: boolean): number {
let maxWithinServiceFee = 0 let maxWithinServiceFee = 0
if (appUser) { if (appUser) {
maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.outgoingAppUserInvoiceFee))) maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee)))
} else { } else {
maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.outgoingAppInvoiceFee))) maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.getSettings().serviceFeeSettings.outgoingAppInvoiceFee)))
} }
return this.lnd.GetMaxWithinLimit(maxWithinServiceFee) return this.lnd.GetMaxWithinLimit(maxWithinServiceFee)
} }
@ -293,7 +293,7 @@ export default class {
} }
async PayExternalInvoice(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, amountForLnd: number }, linkedApplication: Application, debitNpub?: string) { async PayExternalInvoice(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, amountForLnd: number }, linkedApplication: Application, debitNpub?: string) {
if (this.settings.disableExternalPayments) { if (this.settings.getSettings().serviceSettings.disableExternalPayments) {
throw new Error("something went wrong sending payment, please try again later") throw new Error("something went wrong sending payment, please try again later")
} }
const existingPendingPayment = await this.storage.paymentStorage.GetPaymentOwner(invoice) const existingPendingPayment = await this.storage.paymentStorage.GetPaymentOwner(invoice)
@ -412,14 +412,14 @@ export default class {
} }
balanceCheckUrl(k1: string): string { balanceCheckUrl(k1: string): string {
return `${this.settings.serviceUrl}/api/guest/lnurl_withdraw/info?k1=${k1}` return `${this.settings.getSettings().serviceSettings.serviceUrl}/api/guest/lnurl_withdraw/info?k1=${k1}`
} }
isDefaultServiceUrl(): boolean { isDefaultServiceUrl(): boolean {
if ( if (
this.settings.serviceUrl.includes("localhost") this.settings.getSettings().serviceSettings.serviceUrl.includes("localhost")
|| ||
this.settings.serviceUrl.includes("127.0.0.1") this.settings.getSettings().serviceSettings.serviceUrl.includes("127.0.0.1")
) { ) {
return true return true
} }
@ -471,7 +471,7 @@ export default class {
} }
lnurlPayUrl(k1: string): string { lnurlPayUrl(k1: string): string {
return `${this.settings.serviceUrl}/api/guest/lnurl_pay/info?k1=${k1}` return `${this.settings.getSettings().serviceSettings.serviceUrl}/api/guest/lnurl_pay/info?k1=${k1}`
} }
async GetLnurlPayLink(ctx: Types.UserContext): Promise<Types.LnurlLinkResponse> { async GetLnurlPayLink(ctx: Types.UserContext): Promise<Types.LnurlLinkResponse> {
@ -493,7 +493,7 @@ export default class {
} }
const { baseUrl, metadata } = opts const { baseUrl, metadata } = opts
const payK1 = await this.storage.paymentStorage.AddUserEphemeralKey(userId, 'pay', linkedApplication) const payK1 = await this.storage.paymentStorage.AddUserEphemeralKey(userId, 'pay', linkedApplication)
const url = baseUrl ? baseUrl : `${this.settings.serviceUrl}/api/guest/lnurl_pay/handle` const url = baseUrl ? baseUrl : `${this.settings.getSettings().serviceSettings.serviceUrl}/api/guest/lnurl_pay/handle`
const { remote } = await this.lnd.ChannelBalance() const { remote } = await this.lnd.ChannelBalance()
let maxSendable = remote * 1000 let maxSendable = remote * 1000
if (remote === 0 && (await this.liquidityManager.liquidityProvider.IsReady())) { if (remote === 0 && (await this.liquidityManager.liquidityProvider.IsReady())) {
@ -504,7 +504,7 @@ export default class {
callback: `${url}?k1=${payK1.key}`, callback: `${url}?k1=${payK1.key}`,
maxSendable: maxSendable, maxSendable: maxSendable,
minSendable: 10000, minSendable: 10000,
metadata: metadata ? metadata : defaultLnurlPayMetadata(this.settings.lnurlMetaText), metadata: metadata ? metadata : defaultLnurlPayMetadata(this.settings.getSettings().serviceSettings.lnurlMetaText),
allowsNostr: !!linkedApplication.nostr_public_key, allowsNostr: !!linkedApplication.nostr_public_key,
nostrPubkey: linkedApplication.nostr_public_key || "" nostrPubkey: linkedApplication.nostr_public_key || ""
} }
@ -525,10 +525,10 @@ export default class {
} }
return { return {
tag: 'payRequest', tag: 'payRequest',
callback: `${this.settings.serviceUrl}/api/guest/lnurl_pay/handle?k1=${payInfoK1}`, callback: `${this.settings.getSettings().serviceSettings.serviceUrl}/api/guest/lnurl_pay/handle?k1=${payInfoK1}`,
maxSendable: maxSendable, maxSendable: maxSendable,
minSendable: 10000, minSendable: 10000,
metadata: defaultLnurlPayMetadata(this.settings.lnurlMetaText), metadata: defaultLnurlPayMetadata(this.settings.getSettings().serviceSettings.lnurlMetaText),
allowsNostr: !!key.linkedApplication.nostr_public_key, allowsNostr: !!key.linkedApplication.nostr_public_key,
nostrPubkey: key.linkedApplication.nostr_public_key || "" nostrPubkey: key.linkedApplication.nostr_public_key || ""
} }
@ -607,7 +607,7 @@ export default class {
} }
const invoice = await this.NewInvoice(key.user.user_id, { const invoice = await this.NewInvoice(key.user.user_id, {
amountSats: sats, amountSats: sats,
memo: zapInfo ? zapInfo.description : defaultLnurlPayMetadata(this.settings.lnurlMetaText) memo: zapInfo ? zapInfo.description : defaultLnurlPayMetadata(this.settings.getSettings().serviceSettings.lnurlMetaText)
}, { expiry: defaultInvoiceExpiry, linkedApplication: key.linkedApplication, zapInfo }) }, { expiry: defaultInvoiceExpiry, linkedApplication: key.linkedApplication, zapInfo })
return { return {
pr: invoice.invoice, pr: invoice.invoice,
@ -620,7 +620,9 @@ export default class {
if (!linkedUser) { if (!linkedUser) {
throw new Error("this address is not linked to any user") throw new Error("this address is not linked to any user")
} }
return this.GetLnurlPayInfoFromUser(linkedUser.user.user_id, linkedUser.application, { metadata: defaultLnAddressMetadata(this.settings.lnurlMetaText, addressName) }) return this.GetLnurlPayInfoFromUser(linkedUser.user.user_id, linkedUser.application, {
metadata: defaultLnAddressMetadata(this.settings.getSettings().serviceSettings.lnurlMetaText, addressName)
})
} }
mapOperations(operations: UserOperationInfo[], type: Types.UserOperationType, inbound: boolean): Types.UserOperations { mapOperations(operations: UserOperationInfo[], type: Types.UserOperationType, inbound: boolean): Types.UserOperations {
@ -633,8 +635,8 @@ export default class {
} }
return { return {
// We fetch in ascending order // We fetch in ascending order
toIndex: { ts: operations.at(-1)!.paid_at_unix, id: operations.at(-1)!.serial_id } , toIndex: { ts: operations.at(-1)!.paid_at_unix, id: operations.at(-1)!.serial_id },
fromIndex: { ts: operations[0].paid_at_unix, id: operations[0]!.serial_id }, fromIndex: { ts: operations[0].paid_at_unix, id: operations[0]!.serial_id },
operations: operations.map((o: UserOperationInfo): Types.UserOperation => { operations: operations.map((o: UserOperationInfo): Types.UserOperation => {
let identifier = ""; let identifier = "";
if (o.invoice) { if (o.invoice) {
@ -762,7 +764,7 @@ export default class {
async CleanupOldUnpaidInvoices() { async CleanupOldUnpaidInvoices() {
this.log("Cleaning up old unpaid invoices") this.log("Cleaning up old unpaid invoices")
const affected = await this.storage.paymentStorage.RemoveOldUnpaidInvoices() const affected = await this.storage.paymentStorage.RemoveOldUnpaidInvoices()
this.log("Cleaned up",affected, "old unpaid invoices") this.log("Cleaned up", affected, "old unpaid invoices")
} }
async GetLndBalance() { async GetLndBalance() {

View file

@ -2,17 +2,17 @@ import Storage from '../storage/index.js'
import * as Types from '../../../proto/autogenerated/ts/types.js' import * as Types from '../../../proto/autogenerated/ts/types.js'
import { MainSettings } from './settings.js'
import PaymentManager from './paymentManager.js' import PaymentManager from './paymentManager.js'
import { defaultInvoiceExpiry } from '../storage/paymentStorage.js' import { defaultInvoiceExpiry } from '../storage/paymentStorage.js'
import { nofferEncode, OfferPriceType } from '@shocknet/clink-sdk' import { nofferEncode, OfferPriceType } from '@shocknet/clink-sdk'
import SettingsManager from './settingsManager.js'
export default class { export default class {
storage: Storage storage: Storage
settings: MainSettings settings: SettingsManager
paymentManager: PaymentManager paymentManager: PaymentManager
constructor(storage: Storage, paymentManager: PaymentManager, settings: MainSettings) { constructor(storage: Storage, paymentManager: PaymentManager, settings: SettingsManager) {
this.storage = storage this.storage = storage
this.settings = settings this.settings = settings
this.paymentManager = paymentManager this.paymentManager = paymentManager

View file

@ -1,23 +1,8 @@
import { LoadStorageSettingsFromEnv, StorageSettings } from '../storage/index.js' import { EnvCacher, EnvMustBeNonEmptyString, EnvMustBeInteger, chooseEnv, chooseEnvBool, chooseEnvInt } from '../helpers/envParser.js'
import { LndSettings, NodeSettings } from '../lnd/settings.js' import os from 'os'
import { LoadWatchdogSettingsFromEnv, WatchdogSettings } from './watchdog.js' import path from 'path'
import { LoadLndSettingsFromEnv } from '../lnd/index.js'
import { EnvCanBeInteger, EnvMustBeInteger, EnvMustBeNonEmptyString } from '../helpers/envParser.js'
import { getLogger } from '../helpers/logger.js'
import fs from 'fs'
import crypto from 'crypto';
import { LiquiditySettings, LoadLiquiditySettingsFromEnv } from './liquidityManager.js'
import { LoadNosrtRelaySettingsFromEnv, NostrRelaySettings } from '../nostr/handler.js'
export type MainSettings = { export type ServiceFeeSettings = {
storageSettings: StorageSettings,
lndSettings: LndSettings,
watchDogSettings: WatchdogSettings,
liquiditySettings: LiquiditySettings,
nostrRelaySettings: NostrRelaySettings,
jwtSecret: string
walletPasswordPath: string
walletSecretPath: string
incomingTxFee: number incomingTxFee: number
outgoingTxFee: number outgoingTxFee: number
incomingAppInvoiceFee: number incomingAppInvoiceFee: number
@ -27,19 +12,57 @@ export type MainSettings = {
outgoingAppUserInvoiceFeeBps: number outgoingAppUserInvoiceFeeBps: number
userToUserFee: number userToUserFee: number
appToUserFee: number appToUserFee: number
serviceUrl: string }
export const LoadServiceFeeSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): ServiceFeeSettings => {
const outgoingAppUserInvoiceFeeBps = chooseEnvInt("OUTGOING_INVOICE_FEE_USER_BPS", dbEnv, 0, addToDb)
return {
incomingTxFee: chooseEnvInt("INCOMING_CHAIN_FEE_ROOT_BPS", dbEnv, 0, addToDb) / 10000,
outgoingTxFee: chooseEnvInt("OUTGOING_CHAIN_FEE_ROOT_BPS", dbEnv, 60, addToDb) / 10000,
incomingAppInvoiceFee: chooseEnvInt("INCOMING_INVOICE_FEE_ROOT_BPS", dbEnv, 0, addToDb) / 10000,
outgoingAppInvoiceFee: chooseEnvInt("OUTGOING_INVOICE_FEE_ROOT_BPS", dbEnv, 60, addToDb) / 10000,
incomingAppUserInvoiceFee: chooseEnvInt("INCOMING_INVOICE_FEE_USER_BPS", dbEnv, 0, addToDb) / 10000,
outgoingAppUserInvoiceFeeBps,
outgoingAppUserInvoiceFee: outgoingAppUserInvoiceFeeBps / 10000,
userToUserFee: chooseEnvInt("TX_FEE_INTERNAL_USER_BPS", dbEnv, 0, addToDb) / 10000,
appToUserFee: chooseEnvInt("TX_FEE_INTERNAL_ROOT_BPS", dbEnv, 0, addToDb) / 10000,
}
}
export type ServiceSettings = {
servicePort: number servicePort: number
recordPerformance: boolean recordPerformance: boolean
skipSanityCheck: boolean skipSanityCheck: boolean
disableExternalPayments: boolean
wizard: boolean wizard: boolean
bridgeUrl: string,
shockPushBaseUrl: string
serviceUrl: string
disableExternalPayments: boolean
defaultAppName: string defaultAppName: string
pushBackupsToNostr: boolean pushBackupsToNostr: boolean
lnurlMetaText: string, lnurlMetaText: string,
bridgeUrl: string,
allowResetMetricsStorages: boolean
allowHttpUpgrade: boolean allowHttpUpgrade: boolean
shockPushBaseUrl: string
}
export const LoadServiceSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): ServiceSettings => {
const port = chooseEnvInt("PORT", dbEnv, 1776, addToDb)
return {
serviceUrl: chooseEnv("SERVICE_URL", dbEnv, `http://localhost:${port}`, addToDb),
servicePort: port,
recordPerformance: chooseEnvBool("RECORD_PERFORMANCE", dbEnv, false, addToDb),
skipSanityCheck: chooseEnvBool("SKIP_SANITY_CHECK", dbEnv, false, addToDb),
disableExternalPayments: chooseEnvBool("DISABLE_EXTERNAL_PAYMENTS", dbEnv, false, addToDb),
wizard: chooseEnvBool("WIZARD", dbEnv, false, addToDb),
defaultAppName: chooseEnv("DEFAULT_APP_NAME", dbEnv, "wallet", addToDb),
pushBackupsToNostr: chooseEnvBool("PUSH_BACKUPS_TO_NOSTR", dbEnv, false, addToDb),
lnurlMetaText: chooseEnv("LNURL_META_TEXT", dbEnv, "LNURL via Lightning.pub", addToDb),
bridgeUrl: chooseEnv("BRIDGE_URL", dbEnv, "https://shockwallet.app", addToDb),
allowHttpUpgrade: chooseEnvBool("ALLOW_HTTP_UPGRADE", dbEnv, false, addToDb),
shockPushBaseUrl: chooseEnv("SHOCK_PUSH_URL", dbEnv, "", addToDb),
}
} }
export type BitcoinCoreSettings = { export type BitcoinCoreSettings = {
@ -48,109 +71,136 @@ export type BitcoinCoreSettings = {
pass: string pass: string
} }
export type TestSettings = MainSettings & { lndSettings: { otherNode: NodeSettings, thirdNode: NodeSettings, fourthNode: NodeSettings }, bitcoinCoreSettings: BitcoinCoreSettings } export type LndNodeSettings = {
export const LoadMainSettingsFromEnv = (): MainSettings => { lndAddr: string // cold setting
const storageSettings = LoadStorageSettingsFromEnv() lndCertPath: string // cold setting
const outgoingAppUserInvoiceFeeBps = EnvCanBeInteger("OUTGOING_INVOICE_FEE_USER_BPS", 0) lndMacaroonPath: string // cold setting
const nostrRelaySettings = LoadNosrtRelaySettingsFromEnv() }
export type LndSettings = {
lndLogDir: string
feeRateLimit: number
feeFixedLimit: number
feeRateBps: number
mockLnd: boolean
}
const resolveHome = (filepath: string) => {
let homeDir;
if (process.env.SUDO_USER) {
homeDir = path.join('/home', process.env.SUDO_USER);
} else {
homeDir = os.homedir();
}
return path.join(homeDir, filepath);
}
export const LoadLndNodeSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): LndNodeSettings => {
return { return {
watchDogSettings: LoadWatchdogSettingsFromEnv(), lndAddr: chooseEnv('LND_ADDRESS', dbEnv, "127.0.0.1:10009", addToDb),
lndSettings: LoadLndSettingsFromEnv(), lndCertPath: chooseEnv('LND_CERT_PATH', dbEnv, resolveHome("/.lnd/tls.cert"), addToDb),
storageSettings: storageSettings, lndMacaroonPath: chooseEnv('LND_MACAROON_PATH', dbEnv, resolveHome("/.lnd/data/chain/bitcoin/mainnet/admin.macaroon"), addToDb),
liquiditySettings: LoadLiquiditySettingsFromEnv(),
nostrRelaySettings: nostrRelaySettings,
jwtSecret: loadJwtSecret(storageSettings.dataDir),
walletSecretPath: process.env.WALLET_SECRET_PATH || getDataPath(storageSettings.dataDir, ".wallet_secret"),
walletPasswordPath: process.env.WALLET_PASSWORD_PATH || getDataPath(storageSettings.dataDir, ".wallet_password"),
incomingTxFee: EnvCanBeInteger("INCOMING_CHAIN_FEE_ROOT_BPS", 0) / 10000,
outgoingTxFee: EnvCanBeInteger("OUTGOING_CHAIN_FEE_ROOT_BPS", 60) / 10000,
incomingAppInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_ROOT_BPS", 0) / 10000,
outgoingAppInvoiceFee: EnvCanBeInteger("OUTGOING_INVOICE_FEE_ROOT_BPS", 60) / 10000,
incomingAppUserInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_USER_BPS", 0) / 10000,
outgoingAppUserInvoiceFeeBps,
outgoingAppUserInvoiceFee: outgoingAppUserInvoiceFeeBps / 10000,
userToUserFee: EnvCanBeInteger("TX_FEE_INTERNAL_USER_BPS", 0) / 10000,
appToUserFee: EnvCanBeInteger("TX_FEE_INTERNAL_ROOT_BPS", 0) / 10000,
serviceUrl: process.env.SERVICE_URL || `http://localhost:${EnvCanBeInteger("PORT", 1776)}`,
servicePort: EnvCanBeInteger("PORT", 1776),
recordPerformance: process.env.RECORD_PERFORMANCE === 'true' || false,
skipSanityCheck: process.env.SKIP_SANITY_CHECK === 'true' || false,
disableExternalPayments: process.env.DISABLE_EXTERNAL_PAYMENTS === 'true' || false,
wizard: process.env.WIZARD === 'true' || false,
defaultAppName: process.env.DEFAULT_APP_NAME || "wallet",
pushBackupsToNostr: process.env.PUSH_BACKUPS_TO_NOSTR === 'true' || false,
lnurlMetaText: process.env.LNURL_META_TEXT || "LNURL via Lightning.pub",
bridgeUrl: process.env.BRIDGE_URL || "https://shockwallet.app",
allowResetMetricsStorages: process.env.ALLOW_RESET_METRICS_STORAGES === 'true' || false,
allowHttpUpgrade: process.env.ALLOW_HTTP_UPGRADE === 'true' || false,
shockPushBaseUrl: process.env.SHOCK_PUSH_URL || ""
} }
} }
export const GetTestStorageSettings = (s?: StorageSettings): StorageSettings => { export const LoadLndSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): LndSettings => {
const eventLogPath = `logs/eventLogV3Test${Date.now()}.csv` const feeRateBps: number = chooseEnvInt('OUTBOUND_MAX_FEE_BPS', dbEnv, 60, addToDb)
if (s) {
return { dbSettings: { ...s.dbSettings, databaseFile: ":memory:", metricsDatabaseFile: ":memory:" }, eventLogPath, dataDir: "test-data" }
}
return { dbSettings: { databaseFile: ":memory:", metricsDatabaseFile: ":memory:", migrate: true }, eventLogPath, dataDir: "test-data" }
}
export const LoadTestSettingsFromEnv = (): TestSettings => {
const settings = LoadMainSettingsFromEnv()
return { return {
...settings, lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, resolveHome("/.lnd/logs/bitcoin/mainnet/lnd.log"), addToDb),
storageSettings: GetTestStorageSettings(settings.storageSettings), feeRateBps: feeRateBps,
lndSettings: { feeRateLimit: feeRateBps / 10000,
...settings.lndSettings, feeFixedLimit: chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 100, addToDb),
otherNode: { mockLnd: false
lndAddr: EnvMustBeNonEmptyString("LND_OTHER_ADDR"),
lndCertPath: EnvMustBeNonEmptyString("LND_OTHER_CERT_PATH"),
lndMacaroonPath: EnvMustBeNonEmptyString("LND_OTHER_MACAROON_PATH")
},
thirdNode: {
lndAddr: EnvMustBeNonEmptyString("LND_THIRD_ADDR"),
lndCertPath: EnvMustBeNonEmptyString("LND_THIRD_CERT_PATH"),
lndMacaroonPath: EnvMustBeNonEmptyString("LND_THIRD_MACAROON_PATH")
},
fourthNode: {
lndAddr: EnvMustBeNonEmptyString("LND_FOURTH_ADDR"),
lndCertPath: EnvMustBeNonEmptyString("LND_FOURTH_CERT_PATH"),
lndMacaroonPath: EnvMustBeNonEmptyString("LND_FOURTH_MACAROON_PATH")
},
},
liquiditySettings: {
...settings.liquiditySettings,
liquidityProviderPub: "",
},
skipSanityCheck: true,
bitcoinCoreSettings: {
port: EnvMustBeInteger("BITCOIN_CORE_PORT"),
user: EnvMustBeNonEmptyString("BITCOIN_CORE_USER"),
pass: EnvMustBeNonEmptyString("BITCOIN_CORE_PASS")
},
} }
} }
export const loadJwtSecret = (dataDir: string): string => { export type NostrRelaySettings = {
const secret = process.env["JWT_SECRET"] relays: string[],
const log = getLogger({}) maxEventContentLength: number
if (secret) { }
return secret
} export const LoadNostrRelaySettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): NostrRelaySettings => {
log("JWT_SECRET not set in env, checking .jwt_secret file") const relaysEnv = chooseEnv("NOSTR_RELAYS", dbEnv, "wss://relay.lightning.pub", addToDb);
const secretPath = getDataPath(dataDir, ".jwt_secret") const maxEventContentLength = chooseEnvInt("NOSTR_MAX_EVENT_CONTENT_LENGTH", dbEnv, 40000, addToDb)
try { return {
const fileContent = fs.readFileSync(secretPath, "utf-8") relays: relaysEnv.split(' '),
return fileContent.trim() maxEventContentLength
} catch (e) {
log(".jwt_secret file not found, generating random secret")
const secret = crypto.randomBytes(32).toString('hex')
fs.writeFileSync(secretPath, secret)
return secret
} }
} }
export const getDataPath = (dataDir: string, dataPath: string) => { export type WatchdogSettings = {
return dataDir !== "" ? `${dataDir}/${dataPath}` : dataPath maxDiffSats: number // hot setting
}
export const LoadWatchdogSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): WatchdogSettings => {
return {
maxDiffSats: chooseEnvInt("WATCHDOG_MAX_DIFF_SATS", dbEnv, 0, addToDb)
}
}
export type LSPSettings = {
olympusServiceUrl: string // hot setting
voltageServiceUrl: string // unused?
flashsatsServiceUrl: string // hot setting
channelThreshold: number // hot setting
maxRelativeFee: number // hot setting
}
export const LoadLSPSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): LSPSettings => {
const olympusServiceUrl = chooseEnv("OLYMPUS_LSP_URL", dbEnv, "https://lsps1.lnolymp.us/api/v1", addToDb)
const voltageServiceUrl = chooseEnv("VOLTAGE_LSP_URL", dbEnv, "https://lsp.voltageapi.com/api/v1", addToDb)
const flashsatsServiceUrl = chooseEnv("FLASHSATS_LSP_URL", dbEnv, "https://lsp.flashsats.xyz/lsp/channel", addToDb)
const channelThreshold = chooseEnvInt("LSP_CHANNEL_THRESHOLD", dbEnv, 1000000, addToDb)
const maxRelativeFee = chooseEnvInt("LSP_MAX_FEE_BPS", dbEnv, 100, addToDb) / 10000
return { olympusServiceUrl, voltageServiceUrl, channelThreshold, maxRelativeFee, flashsatsServiceUrl }
}
export type LiquiditySettings = {
liquidityProviderPub: string // cold setting
useOnlyLiquidityProvider: boolean // hot setting
disableLiquidityProvider: boolean // hot setting
}
export const LoadLiquiditySettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): LiquiditySettings => {
//const liquidityProviderPub = process.env.LIQUIDITY_PROVIDER_PUB === "null" ? "" : (process.env.LIQUIDITY_PROVIDER_PUB || "76ed45f00cea7bac59d8d0b7d204848f5319d7b96c140ffb6fcbaaab0a13d44e")
const liquidityProviderPub = chooseEnv("LIQUIDITY_PROVIDER_PUB", dbEnv, "76ed45f00cea7bac59d8d0b7d204848f5319d7b96c140ffb6fcbaaab0a13d44e", addToDb)
const disableLiquidityProvider = chooseEnvBool("DISABLE_LIQUIDITY_PROVIDER", dbEnv, false, addToDb) || liquidityProviderPub === "null"
return { liquidityProviderPub, useOnlyLiquidityProvider: false, disableLiquidityProvider }
}
export const LoadSecondLndSettingsFromEnv = (): LndNodeSettings => {
return {
lndAddr: EnvMustBeNonEmptyString("LND_OTHER_ADDR"),
lndCertPath: EnvMustBeNonEmptyString("LND_OTHER_CERT_PATH"),
lndMacaroonPath: EnvMustBeNonEmptyString("LND_OTHER_MACAROON_PATH")
}
}
export const LoadThirdLndSettingsFromEnv = (): LndNodeSettings => {
return {
lndAddr: EnvMustBeNonEmptyString("LND_THIRD_ADDR"),
lndCertPath: EnvMustBeNonEmptyString("LND_THIRD_CERT_PATH"),
lndMacaroonPath: EnvMustBeNonEmptyString("LND_THIRD_MACAROON_PATH")
}
}
export const LoadFourthLndSettingsFromEnv = (): LndNodeSettings => {
return {
lndAddr: EnvMustBeNonEmptyString("LND_FOURTH_ADDR"),
lndCertPath: EnvMustBeNonEmptyString("LND_FOURTH_CERT_PATH"),
lndMacaroonPath: EnvMustBeNonEmptyString("LND_FOURTH_MACAROON_PATH")
}
}
export const LoadBitcoinCoreSettingsFromEnv = (): BitcoinCoreSettings => {
return {
port: EnvMustBeInteger("BITCOIN_CORE_PORT"),
user: EnvMustBeNonEmptyString("BITCOIN_CORE_USER"),
pass: EnvMustBeNonEmptyString("BITCOIN_CORE_PASS")
}
} }

View file

@ -0,0 +1,152 @@
import Storage, { StorageSettings } from "../storage/index.js"
import { EnvCacher } from "../helpers/envParser.js"
import { getLogger, PubLogger } from "../helpers/logger.js"
import {
LiquiditySettings, LndNodeSettings, LndSettings, LoadLiquiditySettingsFromEnv,
LoadLSPSettingsFromEnv, LSPSettings, ServiceFeeSettings, ServiceSettings, LoadServiceFeeSettingsFromEnv,
LoadNostrRelaySettingsFromEnv, LoadServiceSettingsFromEnv, LoadWatchdogSettingsFromEnv,
LoadLndNodeSettingsFromEnv, LoadLndSettingsFromEnv, NostrRelaySettings, WatchdogSettings
} from "./settings.js"
export default class SettingsManager {
storage: Storage
private settings: FullSettings | null = null
log: PubLogger
constructor(storage: Storage) {
this.storage = storage
this.log = getLogger({ component: "SettingsManager" })
}
loadEnvs(dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): FullSettings {
return {
lndNodeSettings: LoadLndNodeSettingsFromEnv(dbEnv, addToDb),
lndSettings: LoadLndSettingsFromEnv(dbEnv, addToDb),
liquiditySettings: LoadLiquiditySettingsFromEnv(dbEnv, addToDb),
lspSettings: LoadLSPSettingsFromEnv(dbEnv, addToDb),
nostrRelaySettings: LoadNostrRelaySettingsFromEnv(dbEnv, addToDb),
serviceFeeSettings: LoadServiceFeeSettingsFromEnv(dbEnv, addToDb),
serviceSettings: LoadServiceSettingsFromEnv(dbEnv, addToDb),
watchDogSettings: LoadWatchdogSettingsFromEnv(dbEnv, addToDb),
}
}
OverrideTestSettings(f: (s: FullSettings) => FullSettings) {
if (!this.settings) {
throw new Error("Settings not initialized")
}
this.settings = f(this.settings)
}
async InitSettings(): Promise<FullSettings> {
const dbSettings = await this.storage.settingsStorage.getAllDbEnvs()
const toAdd: Record<string, string> = {}
const addToDb = (key: string, value: string) => {
toAdd[key] = value
}
this.settings = this.loadEnvs(dbSettings, addToDb)
this.log("adding", Object.keys(toAdd).length, "settings to db")
for (const key in toAdd) {
await this.storage.settingsStorage.setDbEnvIFNeeded(key, toAdd[key])
}
return this.settings
}
getStorageSettings(): StorageSettings {
return this.storage.getStorageSettings()
}
getSettings(): FullSettings {
if (!this.settings) {
throw new Error("Settings not initialized")
}
return this.settings
}
async updateDefaultAppName(name: string): Promise<boolean> {
if (!this.settings) {
throw new Error("Settings not initialized")
}
if (name === this.settings.serviceSettings.defaultAppName) {
return false
}
if (!!process.env.DEFAULT_APP_NAME) {
return false
}
await this.storage.settingsStorage.setDbEnvIFNeeded("DEFAULT_APP_NAME", name)
this.settings.serviceSettings.defaultAppName = name
return true
}
async updateRelayUrl(url: string): Promise<boolean> {
if (!this.settings) {
throw new Error("Settings not initialized")
}
if (url === this.settings.nostrRelaySettings.relays[0]) {
return false
}
if (!!process.env.RELAY_URL) {
return false
}
await this.storage.settingsStorage.setDbEnvIFNeeded("NOSTR_RELAYS", url)
this.settings.nostrRelaySettings.relays = [url]
return true
}
async updateDisableLiquidityProvider(disable: boolean): Promise<boolean> {
if (!this.settings) {
throw new Error("Settings not initialized")
}
if (disable === this.settings.liquiditySettings.disableLiquidityProvider) {
return false
}
if (!!process.env.DISABLE_LIQUIDITY_PROVIDER) {
return false
}
await this.storage.settingsStorage.setDbEnvIFNeeded("DISABLE_LIQUIDITY_PROVIDER", disable ? "true" : "false")
this.settings.liquiditySettings.disableLiquidityProvider = disable
return true
}
async updatePushBackupsToNostr(push: boolean): Promise<boolean> {
if (!this.settings) {
throw new Error("Settings not initialized")
}
if (push === this.settings.serviceSettings.pushBackupsToNostr) {
return false
}
if (!!process.env.PUSH_BACKUPS_TO_NOSTR) {
return false
}
await this.storage.settingsStorage.setDbEnvIFNeeded("PUSH_BACKUPS_TO_NOSTR", push ? "true" : "false")
this.settings.serviceSettings.pushBackupsToNostr = push
return true
}
async updateSkipSanityCheck(skip: boolean): Promise<boolean> {
if (!this.settings) {
throw new Error("Settings not initialized")
}
if (skip === this.settings.serviceSettings.skipSanityCheck) {
return false
}
if (!!process.env.SKIP_SANITY_CHECK) {
return false
}
await this.storage.settingsStorage.setDbEnvIFNeeded("SKIP_SANITY_CHECK", skip ? "true" : "false")
this.settings.serviceSettings.skipSanityCheck = skip
return true
}
}
type FullSettings = {
lndNodeSettings: LndNodeSettings
lndSettings: LndSettings
liquiditySettings: LiquiditySettings
watchDogSettings: WatchdogSettings,
nostrRelaySettings: NostrRelaySettings,
serviceFeeSettings: ServiceFeeSettings,
serviceSettings: ServiceSettings,
lspSettings: LSPSettings
}

View file

@ -4,23 +4,23 @@ import { GrpcTransport } from "@protobuf-ts/grpc-transport";
import { credentials, Metadata } from '@grpc/grpc-js' import { credentials, Metadata } from '@grpc/grpc-js'
import { getLogger } from '../helpers/logger.js'; import { getLogger } from '../helpers/logger.js';
import { WalletUnlockerClient } from '../../../proto/lnd/walletunlocker.client.js'; import { WalletUnlockerClient } from '../../../proto/lnd/walletunlocker.client.js';
import { MainSettings } from '../main/settings.js';
import { InitWalletReq } from '../lnd/initWalletReq.js'; import { InitWalletReq } from '../lnd/initWalletReq.js';
import Storage from '../storage/index.js' import Storage from '../storage/index.js'
import { LightningClient } from '../../../proto/lnd/lightning.client.js'; import { LightningClient } from '../../../proto/lnd/lightning.client.js';
import * as Types from '../../../proto/autogenerated/ts/types.js' import * as Types from '../../../proto/autogenerated/ts/types.js'
import SettingsManager from './settingsManager.js';
const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline }) const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
type EncryptedData = { iv: string, encrypted: string } type EncryptedData = { iv: string, encrypted: string }
type Seed = { plaintextSeed: string[], encryptedSeed: EncryptedData } type Seed = { plaintextSeed: string[], encryptedSeed: EncryptedData }
export class Unlocker { export class Unlocker {
settings: MainSettings settings: SettingsManager
storage: Storage storage: Storage
abortController = new AbortController() abortController = new AbortController()
subbedToBackups = false subbedToBackups = false
nodePub: string | null = null nodePub: string | null = null
log = getLogger({ component: "unlocker" }) log = getLogger({ component: "unlocker" })
constructor(settings: MainSettings, storage: Storage) { constructor(settings: SettingsManager, storage: Storage) {
this.settings = settings this.settings = settings
this.storage = storage this.storage = storage
} }
@ -30,8 +30,8 @@ export class Unlocker {
} }
getCreds = () => { getCreds = () => {
const macroonPath = this.settings.lndSettings.mainNode.lndMacaroonPath const macroonPath = this.settings.getSettings().lndNodeSettings.lndMacaroonPath
const certPath = this.settings.lndSettings.mainNode.lndCertPath const certPath = this.settings.getSettings().lndNodeSettings.lndCertPath
let macaroon = "" let macaroon = ""
let lndCert: Buffer let lndCert: Buffer
try { try {
@ -96,8 +96,8 @@ export class Unlocker {
} }
private waitForLndSync = async (timeoutSeconds: number): Promise<void> => { private waitForLndSync = async (timeoutSeconds: number): Promise<void> => {
const lndLogPath = this.settings.lndSettings.lndLogDir; const lndLogPath = this.settings.getSettings().lndSettings.lndLogDir;
if (this.settings.lndSettings.mockLnd) { if (this.settings.getSettings().lndSettings.mockLnd) {
this.log("MOCK_LND set, skipping header sync wait."); this.log("MOCK_LND set, skipping header sync wait.");
return; return;
} }
@ -284,7 +284,7 @@ export class Unlocker {
} }
GetWalletSecret = (create: boolean) => { GetWalletSecret = (create: boolean) => {
const path = this.settings.walletSecretPath const path = this.settings.getStorageSettings().walletSecretPath
let secret = "" let secret = ""
try { try {
secret = fs.readFileSync(path, 'utf-8') secret = fs.readFileSync(path, 'utf-8')
@ -300,7 +300,7 @@ export class Unlocker {
} }
GetWalletPassword = () => { GetWalletPassword = () => {
const path = this.settings.walletPasswordPath const path = this.settings.getStorageSettings().walletPasswordPath
let password: Buffer | null = null let password: Buffer | null = null
try { try {
password = fs.readFileSync(path) password = fs.readFileSync(path)
@ -339,14 +339,14 @@ export class Unlocker {
} }
GetUnlockerClient = (cert: Buffer) => { GetUnlockerClient = (cert: Buffer) => {
const host = this.settings.lndSettings.mainNode.lndAddr const host = this.settings.getSettings().lndNodeSettings.lndAddr
const channelCredentials = credentials.createSsl(cert) const channelCredentials = credentials.createSsl(cert)
const transport = new GrpcTransport({ host, channelCredentials }) const transport = new GrpcTransport({ host, channelCredentials })
const client = new WalletUnlockerClient(transport) const client = new WalletUnlockerClient(transport)
return client return client
} }
GetLightningClient = (cert: Buffer, macaroon: string) => { GetLightningClient = (cert: Buffer, macaroon: string) => {
const host = this.settings.lndSettings.mainNode.lndAddr const host = this.settings.getSettings().lndNodeSettings.lndAddr
const sslCreds = credentials.createSsl(cert) const sslCreds = credentials.createSsl(cert)
const macaroonCreds = credentials.createFromMetadataGenerator( const macaroonCreds = credentials.createFromMetadataGenerator(
function (args: any, callback: any) { function (args: any, callback: any) {

View file

@ -1,4 +1,3 @@
import { EnvCanBeInteger } from "../helpers/envParser.js";
import FunctionQueue from "../helpers/functionQueue.js"; import FunctionQueue from "../helpers/functionQueue.js";
import { getLogger } from "../helpers/logger.js"; import { getLogger } from "../helpers/logger.js";
import { Utils } from "../helpers/utilsWrapper.js"; import { Utils } from "../helpers/utilsWrapper.js";
@ -8,14 +7,8 @@ import { ChannelBalance } from "../lnd/settings.js";
import Storage from '../storage/index.js' import Storage from '../storage/index.js'
import { LiquidityManager } from "./liquidityManager.js"; import { LiquidityManager } from "./liquidityManager.js";
import { RugPullTracker } from "./rugPullTracker.js"; import { RugPullTracker } from "./rugPullTracker.js";
export type WatchdogSettings = { import SettingsManager from "./settingsManager.js";
maxDiffSats: number
}
export const LoadWatchdogSettingsFromEnv = (test = false): WatchdogSettings => {
return {
maxDiffSats: EnvCanBeInteger("WATCHDOG_MAX_DIFF_SATS")
}
}
export class Watchdog { export class Watchdog {
queue: FunctionQueue<void> queue: FunctionQueue<void>
initialLndBalance: number; initialLndBalance: number;
@ -27,7 +20,7 @@ export class Watchdog {
lnd: LND; lnd: LND;
liquidProvider: LiquidityProvider; liquidProvider: LiquidityProvider;
liquidityManager: LiquidityManager; liquidityManager: LiquidityManager;
settings: WatchdogSettings; settings: SettingsManager;
storage: Storage; storage: Storage;
rugPullTracker: RugPullTracker rugPullTracker: RugPullTracker
utils: Utils utils: Utils
@ -36,7 +29,7 @@ export class Watchdog {
ready = false ready = false
interval: NodeJS.Timer; interval: NodeJS.Timer;
lndPubKey: string; lndPubKey: string;
constructor(settings: WatchdogSettings, liquidityManager: LiquidityManager, lnd: LND, storage: Storage, utils: Utils, rugPullTracker: RugPullTracker) { constructor(settings: SettingsManager, liquidityManager: LiquidityManager, lnd: LND, storage: Storage, utils: Utils, rugPullTracker: RugPullTracker) {
this.lnd = lnd; this.lnd = lnd;
this.settings = settings; this.settings = settings;
this.storage = storage; this.storage = storage;
@ -114,7 +107,7 @@ export class Watchdog {
switch (result.type) { switch (result.type) {
case 'mismatch': case 'mismatch':
if (deltaLnd < 0) { if (deltaLnd < 0) {
if (result.absoluteDiff > this.settings.maxDiffSats) { if (result.absoluteDiff > this.settings.getSettings().watchDogSettings.maxDiffSats) {
await this.updateDisruption(true, result.absoluteDiff, lndWithDeltaUsers) await this.updateDisruption(true, result.absoluteDiff, lndWithDeltaUsers)
return true return true
} }
@ -126,7 +119,7 @@ export class Watchdog {
break break
case 'negative': case 'negative':
if (Math.abs(deltaLnd) > Math.abs(deltaUsers)) { if (Math.abs(deltaLnd) > Math.abs(deltaUsers)) {
if (result.absoluteDiff > this.settings.maxDiffSats) { if (result.absoluteDiff > this.settings.getSettings().watchDogSettings.maxDiffSats) {
await this.updateDisruption(true, result.absoluteDiff, lndWithDeltaUsers) await this.updateDisruption(true, result.absoluteDiff, lndWithDeltaUsers)
return true return true
} }
@ -142,7 +135,7 @@ export class Watchdog {
case 'positive': case 'positive':
if (deltaLnd < deltaUsers) { if (deltaLnd < deltaUsers) {
this.log("WARNING! LND balance increased less than users balance with a difference of", result.absoluteDiff, "sats") this.log("WARNING! LND balance increased less than users balance with a difference of", result.absoluteDiff, "sats")
if (result.absoluteDiff > this.settings.maxDiffSats) { if (result.absoluteDiff > this.settings.getSettings().watchDogSettings.maxDiffSats) {
await this.updateDisruption(true, result.absoluteDiff, lndWithDeltaUsers) await this.updateDisruption(true, result.absoluteDiff, lndWithDeltaUsers)
return true return true
} }
@ -160,12 +153,13 @@ export class Watchdog {
updateDisruption = async (isDisrupted: boolean, absoluteDiff: number, lndWithDeltaUsers: number) => { updateDisruption = async (isDisrupted: boolean, absoluteDiff: number, lndWithDeltaUsers: number) => {
const tracker = await this.getTracker() const tracker = await this.getTracker()
this.storage.liquidityStorage.UpdateTrackedProviderBalance('lnd', this.lndPubKey, lndWithDeltaUsers) this.storage.liquidityStorage.UpdateTrackedProviderBalance('lnd', this.lndPubKey, lndWithDeltaUsers)
const maxDiffSats = this.settings.getSettings().watchDogSettings.maxDiffSats
if (isDisrupted) { if (isDisrupted) {
if (tracker.latest_distruption_at_unix === 0) { if (tracker.latest_distruption_at_unix === 0) {
await this.storage.liquidityStorage.UpdateTrackedProviderDisruption('lnd', this.lndPubKey, Math.floor(Date.now() / 1000)) await this.storage.liquidityStorage.UpdateTrackedProviderDisruption('lnd', this.lndPubKey, Math.floor(Date.now() / 1000))
this.log("detected lnd loss of", absoluteDiff, "sats,", absoluteDiff - this.settings.maxDiffSats, "above the max allowed") this.log("detected lnd loss of", absoluteDiff, "sats,", absoluteDiff - maxDiffSats, "above the max allowed")
} else { } else {
this.log("ongoing lnd loss of", absoluteDiff, "sats,", absoluteDiff - this.settings.maxDiffSats, "above the max allowed") this.log("ongoing lnd loss of", absoluteDiff, "sats,", absoluteDiff - maxDiffSats, "above the max allowed")
} }
} else { } else {
if (tracker.latest_distruption_at_unix !== 0) { if (tracker.latest_distruption_at_unix !== 0) {

View file

@ -9,7 +9,6 @@ import { BalanceEvent } from '../storage/entity/BalanceEvent.js'
import { ChannelBalanceEvent } from '../storage/entity/ChannelsBalanceEvent.js' import { ChannelBalanceEvent } from '../storage/entity/ChannelsBalanceEvent.js'
import LND from '../lnd/lnd.js' import LND from '../lnd/lnd.js'
import HtlcTracker from './htlcTracker.js' import HtlcTracker from './htlcTracker.js'
import { MainSettings } from '../main/settings.js'
import { getLogger } from '../helpers/logger.js' import { getLogger } from '../helpers/logger.js'
import { encodeTLV, usageMetricsToTlv } from '../helpers/tlv.js' import { encodeTLV, usageMetricsToTlv } from '../helpers/tlv.js'
import { ChannelCloseSummary_ClosureType } from '../../../proto/lnd/lightning.js' import { ChannelCloseSummary_ClosureType } from '../../../proto/lnd/lightning.js'

View file

@ -7,7 +7,7 @@ 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'
import { ProcessMetrics, ProcessMetricsCollector } from '../storage/tlv/processMetricsCollector.js' import { ProcessMetrics, ProcessMetricsCollector } from '../storage/tlv/processMetricsCollector.js'
import { EnvCanBeInteger, } from '../helpers/envParser.js' import { Subscription } from 'nostr-tools/lib/types/abstract-relay.js';
const { nprofileEncode } = nip19 const { nprofileEncode } = nip19
const { v2 } = nip44 const { v2 } = nip44
const { encrypt: encryptV2, decrypt: decryptV2, utils } = v2 const { encrypt: encryptV2, decrypt: decryptV2, utils } = v2
@ -28,24 +28,6 @@ export type NostrSettings = {
maxEventContentLength: number maxEventContentLength: number
} }
export type NostrRelaySettings = {
relays: string[],
maxEventContentLength: number
}
const getEnvOrDefault = (name: string, defaultValue: string): string => {
return process.env[name] || defaultValue;
}
export const LoadNosrtRelaySettingsFromEnv = (test = false): NostrRelaySettings => {
const relaysEnv = getEnvOrDefault("NOSTR_RELAYS", "wss://relay.lightning.pub");
const maxEventContentLength = EnvCanBeInteger("NOSTR_MAX_EVENT_CONTENT_LENGTH", 40000)
return {
relays: relaysEnv.split(' '),
maxEventContentLength
}
}
export type NostrEvent = { export type NostrEvent = {
id: string id: string
pub: string pub: string
@ -104,7 +86,7 @@ let subProcessHandler: Handler | undefined
process.on("message", (message: ChildProcessRequest) => { process.on("message", (message: ChildProcessRequest) => {
switch (message.type) { switch (message.type) {
case 'settings': case 'settings':
initSubprocessHandler(message.settings) handleNostrSettings(message.settings)
break break
case 'send': case 'send':
sendToNostr(message.initiator, message.data, message.relays) sendToNostr(message.initiator, message.data, message.relays)
@ -117,18 +99,14 @@ process.on("message", (message: ChildProcessRequest) => {
break break
} }
}) })
const initSubprocessHandler = (settings: NostrSettings) => { const handleNostrSettings = (settings: NostrSettings) => {
if (subProcessHandler) { if (subProcessHandler) {
getLogger({ component: "nostrMiddleware" })(ERROR, "nostr settings ignored since handler already exists") getLogger({ component: "nostrMiddleware" })("got new nostr setting, resetting nostr handler")
subProcessHandler.Stop()
initNostrHandler(settings)
return return
} }
subProcessHandler = new Handler(settings, event => { initNostrHandler(settings)
send({
type: 'event',
event: event
})
})
new ProcessMetricsCollector((metrics) => { new ProcessMetricsCollector((metrics) => {
send({ send({
type: 'processMetrics', type: 'processMetrics',
@ -136,6 +114,14 @@ const initSubprocessHandler = (settings: NostrSettings) => {
}) })
}) })
} }
const initNostrHandler = (settings: NostrSettings) => {
subProcessHandler = new Handler(settings, event => {
send({
type: 'event',
event: event
})
})
}
const sendToNostr: NostrSend = (initiator, data, relays) => { const sendToNostr: NostrSend = (initiator, data, relays) => {
if (!subProcessHandler) { if (!subProcessHandler) {
getLogger({ component: "nostrMiddleware" })(ERROR, "nostr was not initialized") getLogger({ component: "nostrMiddleware" })(ERROR, "nostr was not initialized")
@ -152,13 +138,16 @@ export default class Handler {
apps: Record<string, AppInfo> = {} apps: Record<string, AppInfo> = {}
eventCallback: (event: NostrEvent) => void eventCallback: (event: NostrEvent) => void
log = getLogger({ component: "nostrMiddleware" }) log = getLogger({ component: "nostrMiddleware" })
relay: Relay | null = null
sub: Subscription | null = null
stopped = false
constructor(settings: NostrSettings, eventCallback: (event: NostrEvent) => void) { constructor(settings: NostrSettings, eventCallback: (event: NostrEvent) => void) {
this.settings = settings this.settings = settings
this.log("connecting to relays:", settings.relays) this.log("connecting to relays:", settings.relays)
this.settings.apps.forEach(app => { this.settings.apps.forEach(app => {
this.log("appId:", app.appId, "pubkey:", app.publicKey, "nprofile:", nprofileEncode({ pubkey: app.publicKey, relays: settings.relays })) this.log("appId:", app.appId, "pubkey:", app.publicKey, "nprofile:", nprofileEncode({ pubkey: app.publicKey, relays: settings.relays }))
}) })
this.eventCallback = eventCallback this.eventCallback = (e) => { if (!this.stopped) eventCallback(e) }
this.settings.apps.forEach(app => { this.settings.apps.forEach(app => {
this.apps[app.publicKey] = app this.apps[app.publicKey] = app
}) })
@ -167,78 +156,60 @@ export default class Handler {
async ConnectLoop() { async ConnectLoop() {
let failures = 0 let failures = 0
while (true) { while (!this.stopped) {
await this.ConnectPromise() await this.ConnectPromise()
const pow = Math.pow(2, failures) const pow = Math.pow(2, failures)
const delay = Math.min(pow, 900) const delay = Math.min(pow, 900)
this.log("relay connection failed, will try again in", delay, "seconds (failures:", failures, ")") this.log("relay connection failed, will try again in", delay, "seconds (failures:", failures, ")")
await new Promise(resolve => setTimeout(resolve, delay*1000)) await new Promise(resolve => setTimeout(resolve, delay * 1000))
failures++ failures++
} }
this.log("nostr handler stopped")
}
Stop() {
this.stopped = true
this.sub?.close()
this.relay?.close()
this.relay = null
this.sub = null
} }
async ConnectPromise() { async ConnectPromise() {
return new Promise<void>( async (res) => { return new Promise<void>(async (res) => {
const relay = await this.GetRelay() this.relay = await this.GetRelay()
if (!relay) { if (!this.relay) {
res() res()
return return
} }
const sub = this.Subscribe(relay) this.sub = this.Subscribe(this.relay)
relay.onclose = (() => { this.relay.onclose = (() => {
this.log("relay disconnected") this.log("relay disconnected")
sub.close() this.sub?.close()
relay.onclose = null if (this.relay) {
relay.close() this.relay.onclose = null
this.relay.close()
this.relay = null
}
this.sub = null
res() res()
}) })
}) })
} }
async GetRelay(): Promise<Relay|null> { async GetRelay(): Promise<Relay | null> {
try { try {
const relay = await Relay.connect(this.settings.relays[0]) const relay = await Relay.connect(this.settings.relays[0])
if (!relay.connected) { if (!relay.connected) {
throw new Error("failed to connect to relay") throw new Error("failed to connect to relay")
} }
return relay return relay
} catch (err:any) { } catch (err: any) {
this.log("failed to connect to relay", err.message || err) this.log("failed to connect to relay", err.message || err)
return null return null
} }
} }
/* async Connect() {
const log = getLogger({})
log("conneting to relay...", this.settings.relays[0])
let relay: Relay | null = null
//const relay = relayInit(this.settings.relays[0]) // TODO: create multiple conns for multiple relays
try {
relay = await Relay.connect(this.settings.relays[0])
if (!relay.connected) {
throw new Error("failed to connect to relay")
}
} catch (err:any) {
log("failed to connect to relay, will try again in 2 seconds", err.message || err)
setTimeout(() => {
this.Connect()
}, 2000)
return
}
log("connected, subbing...")
relay.onclose = (() => {
log("relay disconnected, will try to reconnect in 2 seconds")
relay.close()
setTimeout(() => {
this.Connect()
}, 2000)
})
this.Subscribe(relay)
} */
Subscribe(relay: Relay) { Subscribe(relay: Relay) {
const appIds = Object.keys(this.apps) const appIds = Object.keys(this.apps)
this.log("🔍 [NOSTR SUBSCRIPTION] Setting up subscription", { this.log("🔍 [NOSTR SUBSCRIPTION] Setting up subscription", {

View file

@ -1,8 +1,7 @@
import { ChildProcess, fork } from 'child_process' import { ChildProcess, fork } from 'child_process'
import { EnvCanBeInteger, EnvMustBeNonEmptyString } from "../helpers/envParser.js"
import { NostrSettings, NostrEvent, ChildProcessRequest, ChildProcessResponse, SendData, SendInitiator } from "./handler.js" import { NostrSettings, NostrEvent, ChildProcessRequest, ChildProcessResponse, SendData, SendInitiator } from "./handler.js"
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
@ -10,7 +9,6 @@ type EventCallback = (event: NostrEvent) => void
export default class NostrSubprocess { export default class NostrSubprocess {
settings: NostrSettings
childProcess: ChildProcess childProcess: ChildProcess
utils: Utils utils: Utils
awaitingPongs: (() => void)[] = [] awaitingPongs: (() => void)[] = []
@ -55,6 +53,10 @@ export default class NostrSubprocess {
this.childProcess.send(message) this.childProcess.send(message)
} }
Reset(settings: NostrSettings) {
this.sendToChildProcess({ type: 'settings', settings })
}
Ping() { Ping() {
this.sendToChildProcess({ type: 'ping' }) this.sendToChildProcess({ type: 'ping' })
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {

View file

@ -1,6 +1,4 @@
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 main from '../main/index.js'
import Main from '../main/index.js' import Main from '../main/index.js'
export default (mainHandler: Main): Types.ServerMethods => { export default (mainHandler: Main): Types.ServerMethods => {
return { return {
@ -350,9 +348,9 @@ export default (mainHandler: Main): Types.ServerMethods => {
if (err != null) throw new Error(err.message) if (err != null) throw new Error(err.message)
return mainHandler.adminManager.GetInviteTokenState(ctx, req); return mainHandler.adminManager.GetInviteTokenState(ctx, req);
}, },
/* AuthorizeDebit: async ({ ctx, req }) => { /* AuthorizeDebit: async ({ ctx, req }) => {
return mainHandler.debitManager.AuthorizeDebit(ctx, req) return mainHandler.debitManager.AuthorizeDebit(ctx, req)
}, */ }, */
GetDebitAuthorizations: async ({ ctx }) => { GetDebitAuthorizations: async ({ ctx }) => {
return mainHandler.debitManager.GetDebitAuthorizations(ctx) return mainHandler.debitManager.GetDebitAuthorizations(ctx)
}, },

View file

@ -5,7 +5,6 @@ import { User } from "../entity/User.js"
import { UserReceivingAddress } from "../entity/UserReceivingAddress.js" import { UserReceivingAddress } from "../entity/UserReceivingAddress.js"
import { UserReceivingInvoice } from "../entity/UserReceivingInvoice.js" import { UserReceivingInvoice } from "../entity/UserReceivingInvoice.js"
import { UserInvoicePayment } from "../entity/UserInvoicePayment.js" import { UserInvoicePayment } from "../entity/UserInvoicePayment.js"
import { EnvMustBeNonEmptyString } from "../../helpers/envParser.js"
import { UserTransactionPayment } from "../entity/UserTransactionPayment.js" import { UserTransactionPayment } from "../entity/UserTransactionPayment.js"
import { UserBasicAuth } from "../entity/UserBasicAuth.js" import { UserBasicAuth } from "../entity/UserBasicAuth.js"
import { UserEphemeralKey } from "../entity/UserEphemeralKey.js" import { UserEphemeralKey } from "../entity/UserEphemeralKey.js"
@ -29,6 +28,7 @@ import { ChannelEvent } from "../entity/ChannelEvent.js"
import { AppUserDevice } from "../entity/AppUserDevice.js" import { AppUserDevice } from "../entity/AppUserDevice.js"
import * as fs from 'fs' import * as fs from 'fs'
import { UserAccess } from "../entity/UserAccess.js" import { UserAccess } from "../entity/UserAccess.js"
import { AdminSettings } from "../entity/AdminSettings.js"
export type DbSettings = { export type DbSettings = {
@ -73,7 +73,8 @@ export const MainDbEntities = {
'Product': Product, 'Product': Product,
'ManagementGrant': ManagementGrant, 'ManagementGrant': ManagementGrant,
'AppUserDevice': AppUserDevice, 'AppUserDevice': AppUserDevice,
'UserAccess': UserAccess 'UserAccess': UserAccess,
'AdminSettings': AdminSettings
} }
export type MainDbNames = keyof typeof MainDbEntities export type MainDbNames = keyof typeof MainDbEntities
export const MainDbEntitiesNames = Object.keys(MainDbEntities) export const MainDbEntitiesNames = Object.keys(MainDbEntities)

View file

@ -0,0 +1,19 @@
import { Entity, PrimaryGeneratedColumn, Column, Index, Check, CreateDateColumn, UpdateDateColumn } from "typeorm"
@Entity()
export class AdminSettings {
@PrimaryGeneratedColumn()
serial_id: number
@Column({ unique: true })
env_name: string
@Column()
env_value: string
@CreateDateColumn()
created_at: Date
@UpdateDateColumn()
updated_at: Date
}

View file

@ -12,16 +12,58 @@ import DebitStorage from "./debitStorage.js"
import OfferStorage from "./offerStorage.js" import OfferStorage from "./offerStorage.js"
import { ManagementStorage } from "./managementStorage.js"; import { ManagementStorage } from "./managementStorage.js";
import { StorageInterface, TX } from "./db/storageInterface.js"; import { StorageInterface, TX } from "./db/storageInterface.js";
import { PubLogger } from "../helpers/logger.js" import { getLogger, PubLogger } from "../helpers/logger.js"
import { TlvStorageFactory } from './tlv/tlvFilesStorageFactory.js'; import { TlvStorageFactory } from './tlv/tlvFilesStorageFactory.js';
import { Utils } from '../helpers/utilsWrapper.js'; import { Utils } from '../helpers/utilsWrapper.js';
import SettingsStorage from "./settingsStorage.js";
import crypto from 'crypto';
export type StorageSettings = { export type StorageSettings = {
dbSettings: DbSettings dbSettings: DbSettings
eventLogPath: string eventLogPath: string
dataDir: string dataDir: string
allowResetMetricsStorages: boolean
walletPasswordPath: string
walletSecretPath: string
jwtSecret: string // Secret
}
const getDataPath = (dataDir: string, dataPath: string) => {
return dataDir !== "" ? `${dataDir}/${dataPath}` : dataPath
} }
export const LoadStorageSettingsFromEnv = (): StorageSettings => { export const LoadStorageSettingsFromEnv = (): StorageSettings => {
return { dbSettings: LoadDbSettingsFromEnv(), eventLogPath: "logs/eventLogV3.csv", dataDir: process.env.DATA_DIR || "" } const dataDir = process.env.DATA_DIR || ""
return {
dbSettings: LoadDbSettingsFromEnv(), eventLogPath: "logs/eventLogV3.csv", dataDir,
allowResetMetricsStorages: process.env.ALLOW_RESET_METRICS_STORAGES === 'true' || false,
walletSecretPath: process.env.WALLET_SECRET_PATH || getDataPath(dataDir, ".wallet_secret"),
walletPasswordPath: process.env.WALLET_PASSWORD_PATH || getDataPath(dataDir, ".wallet_password"),
jwtSecret: loadJwtSecret(dataDir)
}
}
export const GetTestStorageSettings = (s: StorageSettings): StorageSettings => {
const eventLogPath = `logs/eventLogV3Test${Date.now()}.csv`
return {
...s,
dbSettings: { ...s.dbSettings, databaseFile: ":memory:", metricsDatabaseFile: ":memory:" },
eventLogPath, dataDir: "test-data"
}
}
export const loadJwtSecret = (dataDir: string): string => {
const secret = process.env["JWT_SECRET"]
const log = getLogger({})
if (secret) {
return secret
}
log("JWT_SECRET not set in env, checking .jwt_secret file")
const secretPath = getDataPath(dataDir, ".jwt_secret")
try {
const fileContent = fs.readFileSync(secretPath, "utf-8")
return fileContent.trim()
} catch (e) {
log(".jwt_secret file not found, generating random secret")
const secret = crypto.randomBytes(32).toString('hex')
fs.writeFileSync(secretPath, secret)
return secret
}
} }
export default class { export default class {
//DB: DataSource | EntityManager //DB: DataSource | EntityManager
@ -39,6 +81,7 @@ export default class {
offerStorage: OfferStorage offerStorage: OfferStorage
managementStorage: ManagementStorage managementStorage: ManagementStorage
eventsLog: EventsLogManager eventsLog: EventsLogManager
settingsStorage: SettingsStorage
utils: Utils utils: Utils
constructor(settings: StorageSettings, utils: Utils) { constructor(settings: StorageSettings, utils: Utils) {
this.settings = settings this.settings = settings
@ -51,6 +94,7 @@ export default class {
//const { source, executedMigrations } = await NewDB(this.settings.dbSettings, allMigrations) //const { source, executedMigrations } = await NewDB(this.settings.dbSettings, allMigrations)
//this.DB = source //this.DB = source
//this.txQueue = new TransactionsQueue("main", this.DB) //this.txQueue = new TransactionsQueue("main", this.DB)
this.settingsStorage = new SettingsStorage(this.dbs)
this.userStorage = new UserStorage(this.dbs, this.eventsLog) this.userStorage = new UserStorage(this.dbs, this.eventsLog)
this.productStorage = new ProductStorage(this.dbs) this.productStorage = new ProductStorage(this.dbs)
this.applicationStorage = new ApplicationStorage(this.dbs, this.userStorage) this.applicationStorage = new ApplicationStorage(this.dbs, this.userStorage)
@ -74,6 +118,10 @@ export default class {
} */ } */
} }
getStorageSettings(): StorageSettings {
return this.settings
}
Stop() { Stop() {
this.dbs.disconnect() this.dbs.disconnect()
} }

View file

@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AdminSettings1761683639419 implements MigrationInterface {
name = 'AdminSettings1761683639419'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "admin_settings" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "env_name" varchar NOT NULL, "env_value" varchar NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "UQ_d8a6092ee66a2e65a9d278cf041" UNIQUE ("env_name"))`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "admin_settings"`);
}
}

View file

@ -26,12 +26,14 @@ import { UserReceivingInvoiceIdx1753109184611 } from './1753109184611-user_recei
import { UserAccess1759426050669 } from './1759426050669-user_access.js' import { UserAccess1759426050669 } from './1759426050669-user_access.js'
import { AddBlindToUserOffer1760000000000 } from './1760000000000-add_blind_to_user_offer.js' import { AddBlindToUserOffer1760000000000 } from './1760000000000-add_blind_to_user_offer.js'
import { ApplicationAvatarUrl1761000001000 } from './1761000001000-application_avatar_url.js' import { ApplicationAvatarUrl1761000001000 } from './1761000001000-application_avatar_url.js'
import { AdminSettings1761683639419 } from './1761683639419-admin_settings.js'
export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189,
TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264,
DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513, DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513,
InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000] InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175,
UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419]
export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411] export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411]
/* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => { /* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => {

View file

@ -0,0 +1,35 @@
import { StorageInterface } from "./db/storageInterface.js";
import { AdminSettings } from "./entity/AdminSettings.js";
export default class SettingsStorage {
dbs: StorageInterface
constructor(dbs: StorageInterface) {
this.dbs = dbs
}
async getAllDbEnvs(): Promise<Record<string, string>> {
const settings = await this.dbs.Find<AdminSettings>('AdminSettings', {});
const envs: Record<string, string> = {};
for (const setting of settings) {
envs[setting.env_name] = setting.env_value;
}
return envs;
}
async getDbEnv(envName: string): Promise<string | undefined> {
const setting = await this.dbs.FindOne<AdminSettings>('AdminSettings', { where: { env_name: envName } });
if (!setting) return undefined;
return setting.env_value;
}
async setDbEnvIFNeeded(envName: string, envValue: string): Promise<void> {
await this.dbs.Tx(async tx => {
const setting = await this.dbs.FindOne<AdminSettings>('AdminSettings', { where: { env_name: envName } }, tx);
if (!setting) {
await this.dbs.CreateAndSave<AdminSettings>('AdminSettings', { env_name: envName, env_value: envValue }, tx);
} else if (setting.env_value !== envValue) {
setting.env_value = envValue;
await this.dbs.Update<AdminSettings>('AdminSettings', setting.serial_id, setting, tx);
}
})
}
}

View file

@ -1,10 +1,7 @@
import fs from 'fs'
import path from 'path';
import { config as loadEnvFile } from 'dotenv'
import { getLogger } from "../helpers/logger.js" import { getLogger } from "../helpers/logger.js"
import NewWizardServer from "../../../proto/wizard_service/autogenerated/ts/express_server.js" import NewWizardServer from "../../../proto/wizard_service/autogenerated/ts/express_server.js"
import * as WizardTypes from "../../../proto/wizard_service/autogenerated/ts/types.js" import * as WizardTypes from "../../../proto/wizard_service/autogenerated/ts/types.js"
import { MainSettings } from "../main/settings.js" import SettingsManager from "../main/settingsManager.js"
import Storage from '../storage/index.js' import Storage from '../storage/index.js'
import { Unlocker } from "../main/unlocker.js" import { Unlocker } from "../main/unlocker.js"
import { AdminManager } from '../main/adminManager.js'; import { AdminManager } from '../main/adminManager.js';
@ -17,16 +14,15 @@ export type WizardSettings = {
const defaultProviderPub = "" const defaultProviderPub = ""
export class Wizard { export class Wizard {
log = getLogger({ component: "wizard" }) log = getLogger({ component: "wizard" })
settings: MainSettings settings: SettingsManager
adminManager: AdminManager adminManager: AdminManager
storage: Storage storage: Storage
configQueue: { res: (reload: boolean) => void }[] = [] configQueue: { res: (reload: boolean) => void }[] = []
pendingConfig: WizardSettings | null = null
awaitingNprofile: { res: (nprofile: string) => void }[] = [] awaitingNprofile: { res: (nprofile: string) => void }[] = []
nprofile = "" nprofile = ""
relays: string[] = [] relays: string[] = []
constructor(mainSettings: MainSettings, storage: Storage, adminManager: AdminManager) { constructor(settings: SettingsManager, storage: Storage, adminManager: AdminManager) {
this.settings = mainSettings this.settings = settings
this.adminManager = adminManager this.adminManager = adminManager
this.storage = storage this.storage = storage
this.log('Starting wizard...') this.log('Starting wizard...')
@ -36,16 +32,16 @@ export class Wizard {
GetAdminConnectInfo: async () => { return this.GetAdminConnectInfo() }, GetAdminConnectInfo: async () => { return this.GetAdminConnectInfo() },
GetServiceState: async () => { return this.GetServiceState() } GetServiceState: async () => { return this.GetServiceState() }
}, { GuestAuthGuard: async () => "", metricsCallback: () => { }, staticFiles: 'static' }) }, { GuestAuthGuard: async () => "", metricsCallback: () => { }, staticFiles: 'static' })
wizardServer.Listen(mainSettings.servicePort + 1) wizardServer.Listen(settings.getSettings().serviceSettings.servicePort + 1)
} }
GetServiceState = async (): Promise<WizardTypes.ServiceStateResponse> => { GetServiceState = async (): Promise<WizardTypes.ServiceStateResponse> => {
try { try {
const apps = await this.storage.applicationStorage.GetApplications() const apps = await this.storage.applicationStorage.GetApplications()
const appNamesList = apps.map(app => app.name).join(', ') const appNamesList = apps.map(app => app.name).join(', ')
const relays = this.settings.nostrRelaySettings ? this.settings.nostrRelaySettings.relays : []; const relays = this.settings.getSettings().nostrRelaySettings.relays
const relayUrl = (relays && relays.length > 0) ? relays[0] : ''; const relayUrl = (relays && relays.length > 0) ? relays[0] : '';
const defaultApp = apps.find(a => a.name === this.settings.defaultAppName) || apps[0] const defaultApp = apps.find(a => a.name === this.settings.getSettings().serviceSettings.defaultAppName) || apps[0]
// Determine LND state and watchdog // Determine LND state and watchdog
let lndState: WizardTypes.LndState = WizardTypes.LndState.OFFLINE let lndState: WizardTypes.LndState = WizardTypes.LndState.OFFLINE
let watchdogOk = false let watchdogOk = false
@ -60,17 +56,17 @@ export class Wizard {
} }
return { return {
admin_npub: this.adminManager.GetAdminNpub(), admin_npub: this.adminManager.GetAdminNpub(),
http_url: this.settings.serviceUrl, http_url: this.settings.getSettings().serviceSettings.serviceUrl,
lnd_state: lndState, lnd_state: lndState,
nprofile: this.nprofile, nprofile: this.nprofile,
provider_name: defaultApp?.name || appNamesList, provider_name: defaultApp?.name || appNamesList,
relay_connected: this.adminManager.GetNostrConnected(), relay_connected: this.adminManager.GetNostrConnected(),
relays: this.relays, relays: this.relays,
watchdog_ok: watchdogOk, watchdog_ok: watchdogOk,
source_name: defaultApp?.name || this.settings.defaultAppName || appNamesList, source_name: defaultApp?.name || this.settings.getSettings().serviceSettings.defaultAppName || appNamesList,
relay_url: relayUrl, relay_url: relayUrl,
automate_liquidity: this.settings.liquiditySettings.liquidityProviderPub !== 'null', automate_liquidity: this.settings.getSettings().liquiditySettings.liquidityProviderPub !== 'null',
push_backups_to_nostr: this.settings.pushBackupsToNostr, push_backups_to_nostr: this.settings.getSettings().serviceSettings.pushBackupsToNostr,
avatar_url: defaultApp?.avatar_url || '', avatar_url: defaultApp?.avatar_url || '',
app_id: defaultApp?.app_id || '' app_id: defaultApp?.app_id || ''
} }
@ -99,7 +95,7 @@ export class Wizard {
WizardState = async (): Promise<WizardTypes.StateResponse> => { WizardState = async (): Promise<WizardTypes.StateResponse> => {
return { return {
config_sent: this.pendingConfig !== null, config_sent: false,
admin_linked: this.adminManager.GetAdminNpub() !== "", admin_linked: this.adminManager.GetAdminNpub() !== "",
} }
} }
@ -148,7 +144,7 @@ export class Wizard {
} }
Configure = async (): Promise<boolean> => { Configure = async (): Promise<boolean> => {
if (this.IsInitialized() || this.pendingConfig !== null) { if (this.IsInitialized()) {
return false return false
} }
return new Promise((res) => { return new Promise((res) => {
@ -165,78 +161,43 @@ export class Wizard {
const pendingConfig = { sourceName: req.source_name, relayUrl: req.relay_url, automateLiquidity: req.automate_liquidity, pushBackupsToNostr: req.push_backups_to_nostr } const pendingConfig = { sourceName: req.source_name, relayUrl: req.relay_url, automateLiquidity: req.automate_liquidity, pushBackupsToNostr: req.push_backups_to_nostr }
// Persist app name/avatar to DB regardless (idempotent behavior) // Persist app name/avatar to DB regardless (idempotent behavior)
try { await this.settings.updateDisableLiquidityProvider(pendingConfig.automateLiquidity)
const appsList = await this.storage.applicationStorage.GetApplications() await this.settings.updatePushBackupsToNostr(pendingConfig.pushBackupsToNostr)
const defaultNames = ['wallet', 'wallet-test', this.settings.defaultAppName] const oldAppName = this.settings.getSettings().serviceSettings.defaultAppName
const existingDefaultApp = appsList.find(app => defaultNames.includes(app.name)) || appsList[0] const nameUpdated = await this.settings.updateDefaultAppName(pendingConfig.sourceName)
if (existingDefaultApp) { if (nameUpdated) {
await this.storage.applicationStorage.UpdateApplication(existingDefaultApp, { name: req.source_name, avatar_url: (req as any).avatar_url || existingDefaultApp.avatar_url }) await this.updateDefaultApp(oldAppName, req.avatar_url)
} }
} catch (e) { const relayUpdated = await this.settings.updateRelayUrl(pendingConfig.relayUrl)
this.log(`Error updating app info: ${(e as Error).message}`) if (relayUpdated && this.IsInitialized()) {
await this.adminManager.ResetNostr()
} }
// If already initialized, treat as idempotent update for env and exit // If already initialized, treat as idempotent update for env and exit
if (this.IsInitialized()) { if (this.IsInitialized()) {
this.updateEnvFile(pendingConfig) this.log("reloaded wizard config")
if (nameUpdated) this.log("name updated")
if (relayUpdated) this.log("relay updated")
return return
} }
// First-time configuration flow // First-time configuration flow
if (this.pendingConfig !== null) {
throw new Error("already initializing")
}
this.updateEnvFile(pendingConfig)
this.configQueue.forEach(q => q.res(true)) this.configQueue.forEach(q => q.res(true))
this.configQueue = [] this.configQueue = []
return return
} }
updateEnvFile = (pendingConfig: WizardSettings) => { updateDefaultApp = async (currentName: string, avatarUrl?: string): Promise<void> => {
let envFileContent: string[] = [] const newName = this.settings.getSettings().serviceSettings.defaultAppName
try { try {
envFileContent = fs.readFileSync('.env', 'utf-8').split('\n') const appsList = await this.storage.applicationStorage.GetApplications()
} catch (err: any) { const defaultNames = ['wallet', 'wallet-test', currentName]
if (err.code !== 'ENOENT') { const existingDefaultApp = appsList.find(app => defaultNames.includes(app.name)) || appsList[0]
throw err if (existingDefaultApp) {
await this.storage.applicationStorage.UpdateApplication(existingDefaultApp, { name: newName, avatar_url: avatarUrl || existingDefaultApp.avatar_url })
} }
} catch (e) {
this.log(`Error updating app info: ${(e as Error).message}`)
} }
const toMerge: string[] = []
const sourceNameIndex = envFileContent.findIndex(line => line.startsWith('DEFAULT_APP_NAME'))
if (sourceNameIndex === -1) {
toMerge.push(`DEFAULT_APP_NAME=${pendingConfig.sourceName}`)
} else {
envFileContent[sourceNameIndex] = `DEFAULT_APP_NAME=${pendingConfig.sourceName}`
}
const relayUrlIndex = envFileContent.findIndex(line => line.startsWith('RELAY_URL'))
if (relayUrlIndex === -1) {
toMerge.push(`RELAY_URL=${pendingConfig.relayUrl}`)
} else {
envFileContent[relayUrlIndex] = `RELAY_URL=${pendingConfig.relayUrl}`
}
const automateLiquidityIndex = envFileContent.findIndex(line => line.startsWith('LIQUIDITY_PROVIDER_PUB'))
if (pendingConfig.automateLiquidity) {
if (automateLiquidityIndex !== -1) {
envFileContent.splice(automateLiquidityIndex, 1)
}
} else {
if (automateLiquidityIndex === -1) {
toMerge.push(`LIQUIDITY_PROVIDER_PUB=null`)
} else {
envFileContent[automateLiquidityIndex] = `LIQUIDITY_PROVIDER_PUB=null`
}
}
const pushBackupsToNostrIndex = envFileContent.findIndex(line => line.startsWith('PUSH_BACKUPS_TO_NOSTR'))
if (pushBackupsToNostrIndex === -1) {
toMerge.push(`PUSH_BACKUPS_TO_NOSTR=${pendingConfig.pushBackupsToNostr ? 'true' : 'false'}`)
} else {
envFileContent[pushBackupsToNostrIndex] = `PUSH_BACKUPS_TO_NOSTR=${pendingConfig.pushBackupsToNostr ? 'true' : 'false'}`
}
const merged = [...envFileContent, ...toMerge].join('\n')
fs.writeFileSync('.env', merged)
loadEnvFile()
} }
} }

View file

@ -1,16 +1,16 @@
// @ts-ignore // @ts-ignore
import BitcoinCore from 'bitcoin-core'; import BitcoinCore from 'bitcoin-core';
import { TestSettings } from '../services/main/settings'; import { BitcoinCoreSettings } from '../services/main/settings';
export class BitcoinCoreWrapper { export class BitcoinCoreWrapper {
core: BitcoinCore core: BitcoinCore
addr: { address: string } addr: { address: string }
constructor(settings: TestSettings) { constructor(settings: BitcoinCoreSettings) {
this.core = new BitcoinCore({ this.core = new BitcoinCore({
//network: 'regtest', //network: 'regtest',
host: '127.0.0.1', host: '127.0.0.1',
port: `${settings.bitcoinCoreSettings.port}`, port: `${settings.port}`,
username: settings.bitcoinCoreSettings.user, username: settings.user,
password: settings.bitcoinCoreSettings.pass, password: settings.pass,
// use a long timeout due to the time it takes to mine a lot of blocks // use a long timeout due to the time it takes to mine a lot of blocks
timeout: 5 * 60 * 1000, timeout: 5 * 60 * 1000,
}) })

View file

@ -1,21 +1,31 @@
import { LoadTestSettingsFromEnv } from "../services/main/settings.js" import {
LiquiditySettings, LoadBitcoinCoreSettingsFromEnv, LoadLndNodeSettingsFromEnv,
LoadLndSettingsFromEnv, LoadSecondLndSettingsFromEnv
} from "../services/main/settings.js"
import { GetTestStorageSettings } from "../services/storage/index.js"
import { BitcoinCoreWrapper } from "./bitcoinCore.js" import { BitcoinCoreWrapper } from "./bitcoinCore.js"
import LND from '../services/lnd/lnd.js' import LND from '../services/lnd/lnd.js'
import { LiquidityProvider } from "../services/main/liquidityProvider.js" import { LiquidityProvider } from "../services/main/liquidityProvider.js"
import { Utils } from "../services/helpers/utilsWrapper.js" import { Utils } from "../services/helpers/utilsWrapper.js"
import { LoadStorageSettingsFromEnv } from "../services/storage/index.js"
export type ChainTools = { export type ChainTools = {
mine: (amount: number) => Promise<void> mine: (amount: number) => Promise<void>
} }
export const setupNetwork = async (): Promise<ChainTools> => { export const setupNetwork = async (): Promise<ChainTools> => {
const settings = LoadTestSettingsFromEnv() const storageSettings = GetTestStorageSettings(LoadStorageSettingsFromEnv())
const core = new BitcoinCoreWrapper(settings) const setupUtils = new Utils({ dataDir: storageSettings.dataDir, allowResetMetricsStorages: storageSettings.allowResetMetricsStorages })
//const settingsManager = new SettingsManager(storageSettings)
const core = new BitcoinCoreWrapper(LoadBitcoinCoreSettingsFromEnv())
await core.InitAddress() await core.InitAddress()
await core.Mine(1) await core.Mine(1)
const setupUtils = new Utils({ dataDir: settings.storageSettings.dataDir, allowResetMetricsStorages: settings.allowResetMetricsStorages }) const lndSettings = LoadLndSettingsFromEnv({})
const alice = new LND(settings.lndSettings, new LiquidityProvider("", setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) const lndNodeSettings = LoadLndNodeSettingsFromEnv({})
const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, new LiquidityProvider("", setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) const secondLndNodeSettings = LoadSecondLndSettingsFromEnv()
const liquiditySettings: LiquiditySettings = { disableLiquidityProvider: true, liquidityProviderPub: "", useOnlyLiquidityProvider: false }
const alice = new LND(() => ({ lndSettings, lndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { })
const bob = new LND(() => ({ lndSettings, lndNodeSettings: secondLndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { })
await tryUntil<void>(async i => { await tryUntil<void>(async i => {
const peers = await alice.ListPeers() const peers = await alice.ListPeers()
if (peers.peers.length > 0) { if (peers.peers.length > 0) {

View file

@ -1,16 +1,22 @@
import { getLogger } from '../services/helpers/logger.js' import { getLogger } from '../services/helpers/logger.js'
import { initMainHandler } from '../services/main/init.js' import { initMainHandler, initSettings } from '../services/main/init.js'
import { LoadTestSettingsFromEnv } from '../services/main/settings.js'
import { SendData } from '../services/nostr/handler.js' import { SendData } from '../services/nostr/handler.js'
import { TestBase, TestUserData } from './testBase.js' import { TestBase, TestUserData } from './testBase.js'
import * as Types from '../../proto/autogenerated/ts/types.js' import * as Types from '../../proto/autogenerated/ts/types.js'
import { GetTestStorageSettings, LoadStorageSettingsFromEnv } from '../services/storage/index.js'
import { LoadThirdLndSettingsFromEnv } from '../services/main/settings.js'
export const initBootstrappedInstance = async (T: TestBase) => { export const initBootstrappedInstance = async (T: TestBase) => {
const settings = LoadTestSettingsFromEnv() const storageSettings = GetTestStorageSettings(LoadStorageSettingsFromEnv())
settings.liquiditySettings.useOnlyLiquidityProvider = true const settingsManager = await initSettings(getLogger({ component: "bootstrapped" }), storageSettings)
settings.liquiditySettings.liquidityProviderPub = T.app.publicKey const thirdNodeSettings = LoadThirdLndSettingsFromEnv()
settings.lndSettings.mainNode = settings.lndSettings.thirdNode settingsManager.OverrideTestSettings(s => {
const initialized = await initMainHandler(getLogger({ component: "bootstrapped" }), settings) s.liquiditySettings.useOnlyLiquidityProvider = true
s.liquiditySettings.liquidityProviderPub = T.app.publicKey
s.lndNodeSettings = thirdNodeSettings
return s
})
const initialized = await initMainHandler(getLogger({ component: "bootstrapped" }), settingsManager)
if (!initialized) { if (!initialized) {
throw new Error("failed to initialize bootstrapped main handler") throw new Error("failed to initialize bootstrapped main handler")
} }

View file

@ -1,10 +1,9 @@
import 'dotenv/config' // TODO - test env import 'dotenv/config' // TODO - test env
import chai from 'chai' import chai from 'chai'
import { AppData, initMainHandler } from '../services/main/init.js' import { AppData, initMainHandler, initSettings } from '../services/main/init.js'
import Main from '../services/main/index.js' import Main from '../services/main/index.js'
import Storage from '../services/storage/index.js' import Storage, { GetTestStorageSettings, LoadStorageSettingsFromEnv } from '../services/storage/index.js'
import { User } from '../services/storage/entity/User.js' import { User } from '../services/storage/entity/User.js'
import { GetTestStorageSettings, LoadMainSettingsFromEnv, LoadTestSettingsFromEnv, MainSettings } from '../services/main/settings.js'
import chaiString from 'chai-string' import chaiString from 'chai-string'
import { defaultInvoiceExpiry } from '../services/storage/paymentStorage.js' import { defaultInvoiceExpiry } from '../services/storage/paymentStorage.js'
import SanityChecker from '../services/main/sanityChecker.js' import SanityChecker from '../services/main/sanityChecker.js'
@ -15,6 +14,7 @@ import { Utils } from '../services/helpers/utilsWrapper.js'
import { AdminManager } from '../services/main/adminManager.js' import { AdminManager } from '../services/main/adminManager.js'
import { TlvStorageFactory } from '../services/storage/tlv/tlvFilesStorageFactory.js' import { TlvStorageFactory } from '../services/storage/tlv/tlvFilesStorageFactory.js'
import { ChainTools } from './networkSetup.js' import { ChainTools } from './networkSetup.js'
import { LiquiditySettings, LoadLndSettingsFromEnv, LoadSecondLndSettingsFromEnv, LoadThirdLndSettingsFromEnv } from '../services/main/settings.js'
chai.use(chaiString) chai.use(chaiString)
export const expect = chai.expect export const expect = chai.expect
export type Describe = (message: string, failure?: boolean) => void export type Describe = (message: string, failure?: boolean) => void
@ -45,7 +45,7 @@ export type StorageTestBase = {
} }
export const setupStorageTest = async (d: Describe): Promise<StorageTestBase> => { export const setupStorageTest = async (d: Describe): Promise<StorageTestBase> => {
const settings = GetTestStorageSettings() const settings = GetTestStorageSettings(LoadStorageSettingsFromEnv())
const utils = new Utils({ dataDir: settings.dataDir, allowResetMetricsStorages: true }) const utils = new Utils({ dataDir: settings.dataDir, allowResetMetricsStorages: true })
const storageManager = new Storage(settings, utils) const storageManager = new Storage(settings, utils)
await storageManager.Connect(console.log) await storageManager.Connect(console.log)
@ -61,8 +61,15 @@ export const teardownStorageTest = async (T: StorageTestBase) => {
} }
export const SetupTest = async (d: Describe, chainTools: ChainTools): Promise<TestBase> => { export const SetupTest = async (d: Describe, chainTools: ChainTools): Promise<TestBase> => {
const settings = LoadTestSettingsFromEnv() const storageSettings = GetTestStorageSettings(LoadStorageSettingsFromEnv())
const initialized = await initMainHandler(getLogger({ component: "mainForTest" }), settings) const settingsManager = await initSettings(getLogger({ component: "mainForTest" }), storageSettings)
settingsManager.OverrideTestSettings(s => {
s.liquiditySettings.disableLiquidityProvider = true
s.liquiditySettings.liquidityProviderPub = ""
s.liquiditySettings.useOnlyLiquidityProvider = false
return s
})
const initialized = await initMainHandler(getLogger({ component: "mainForTest" }), settingsManager)
if (!initialized) { if (!initialized) {
throw new Error("failed to initialize main handler") throw new Error("failed to initialize main handler")
} }
@ -73,16 +80,19 @@ export const SetupTest = async (d: Describe, chainTools: ChainTools): Promise<Te
const user1 = { userId: u1.info.userId, appUserIdentifier: u1.identifier, appId: app.appId } const user1 = { userId: u1.info.userId, appUserIdentifier: u1.identifier, appId: app.appId }
const user2 = { userId: u2.info.userId, appUserIdentifier: u2.identifier, appId: app.appId } const user2 = { userId: u2.info.userId, appUserIdentifier: u2.identifier, appId: app.appId }
const extermnalUtils = new Utils({ dataDir: settings.storageSettings.dataDir, allowResetMetricsStorages: settings.allowResetMetricsStorages }) const extermnalUtils = new Utils({ dataDir: storageSettings.dataDir, allowResetMetricsStorages: storageSettings.allowResetMetricsStorages })
/* const externalAccessToMainLnd = new LND(settings.lndSettings, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { }) /* const externalAccessToMainLnd = new LND(settings.lndSettings, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { })
await externalAccessToMainLnd.Warmup() */ await externalAccessToMainLnd.Warmup() */
const liquiditySettings: LiquiditySettings = { disableLiquidityProvider: true, liquidityProviderPub: "", useOnlyLiquidityProvider: false }
const otherLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.otherNode } const lndSettings = LoadLndSettingsFromEnv({})
const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), async () => { }, extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) const secondLndNodeSettings = LoadSecondLndSettingsFromEnv()
const otherLndSetting = () => ({ lndSettings, lndNodeSettings: secondLndNodeSettings })
const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider(() => liquiditySettings, extermnalUtils, async () => { }, async () => { }), async () => { }, extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { })
await externalAccessToOtherLnd.Warmup() await externalAccessToOtherLnd.Warmup()
const thirdLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.thirdNode } const thirdLndNodeSettings = LoadThirdLndSettingsFromEnv()
const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), async () => { }, extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) const thirdLndSetting = () => ({ lndSettings, lndNodeSettings: thirdLndNodeSettings })
const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider(() => liquiditySettings, extermnalUtils, async () => { }, async () => { }), async () => { }, extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { })
await externalAccessToThirdLnd.Warmup() await externalAccessToThirdLnd.Warmup()