From dd69d7d0cccad8a87ad0e91186c7a392e33542be Mon Sep 17 00:00:00 2001 From: boufni95 Date: Tue, 28 Oct 2025 20:36:10 +0000 Subject: [PATCH] dynamic settings --- datasource.js | 10 +- src/auth.ts | 2 +- src/e2e.ts | 13 +- src/index.ts | 24 +- src/nostrMiddleware.ts | 14 +- src/services/helpers/envParser.ts | 37 ++- src/services/helpers/utilsWrapper.ts | 1 - src/services/lnd/index.ts | 26 -- src/services/lnd/lnd.ts | 22 +- src/services/lnd/lsp.ts | 58 ++-- src/services/lnd/settings.ts | 21 +- src/services/main/adminManager.ts | 21 +- src/services/main/appUserManager.ts | 44 +-- src/services/main/applicationManager.ts | 34 +- src/services/main/index.ts | 58 +++- src/services/main/init.ts | 65 ++-- src/services/main/liquidityManager.ts | 29 +- src/services/main/liquidityProvider.ts | 41 ++- src/services/main/managementManager.ts | 8 +- src/services/main/notificationsManager.ts | 11 +- src/services/main/offerManager.ts | 24 +- src/services/main/paymentManager.ts | 62 ++-- src/services/main/productManager.ts | 6 +- src/services/main/settings.ts | 299 ++++++++++++------ src/services/main/settingsManager.ts | 179 +++++++++++ src/services/main/unlocker.ts | 22 +- src/services/main/watchdog.ts | 26 +- src/services/metrics/index.ts | 1 - src/services/nostr/handler.ts | 144 ++++----- src/services/nostr/index.ts | 8 +- src/services/serverMethods/index.ts | 8 +- src/services/storage/db/db.ts | 13 +- src/services/storage/entity/AdminSettings.ts | 19 ++ src/services/storage/index.ts | 52 ++- .../1761683639419-admin_settings.ts | 13 + src/services/storage/migrations/runner.ts | 6 +- src/services/storage/settingsStorage.ts | 34 ++ src/services/wizard/index.ts | 173 +++++----- src/tests/bitcoinCore.ts | 10 +- src/tests/networkSetup.ts | 22 +- src/tests/setupBootstrapped.ts | 20 +- src/tests/testBase.ts | 34 +- 42 files changed, 1103 insertions(+), 611 deletions(-) delete mode 100644 src/services/lnd/index.ts create mode 100644 src/services/main/settingsManager.ts create mode 100644 src/services/storage/entity/AdminSettings.ts create mode 100644 src/services/storage/migrations/1761683639419-admin_settings.ts create mode 100644 src/services/storage/settingsStorage.ts diff --git a/datasource.js b/datasource.js index 1fbf411a..7d4baa01 100644 --- a/datasource.js +++ b/datasource.js @@ -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 { AppUserDevice } from "./build/src/services/storage/entity/AppUserDevice.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 { 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 { 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 { 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({ type: "better-sqlite3", @@ -45,10 +49,10 @@ export default new DataSource({ migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, - AppUserDevice1753285173175], + AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000], entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, 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, }) -//npx typeorm migration:generate ./src/services/storage/migrations/user_access -d ./datasource.js \ No newline at end of file +//npx typeorm migration:generate ./src/services/storage/migrations/admin_settings -d ./datasource.js \ No newline at end of file diff --git a/src/auth.ts b/src/auth.ts index e1534b34..cca17bbd 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -14,7 +14,7 @@ const serverOptions = (mainHandler: Main): ServerOptions => { UserAuthGuard: async (authHeader) => { return mainHandler.appUserManager.DecodeUserToken(stripBearer(authHeader)) }, GuestWithPubAuthGuard: async (_) => { throw new Error("Nostr only route") }, 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, logMethod: true, logBody: true diff --git a/src/e2e.ts b/src/e2e.ts index bcb16e3d..d46e2d59 100644 --- a/src/e2e.ts +++ b/src/e2e.ts @@ -4,16 +4,17 @@ import GetServerMethods from './services/serverMethods/index.js' import serverOptions from './auth.js'; import nostrMiddleware from './nostrMiddleware.js' import { getLogger } from './services/helpers/logger.js'; -import { initMainHandler } from './services/main/init.js'; -import { LoadMainSettingsFromEnv } from './services/main/settings.js'; +import { initMainHandler, initSettings } from './services/main/init.js'; import { nip19 } from 'nostr-tools' +import { LoadStorageSettingsFromEnv } from './services/storage/index.js'; //@ts-ignore const { nprofileEncode } = nip19 const start = async () => { const log = getLogger({}) - 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) { log("manual process ended") return @@ -21,7 +22,7 @@ const start = async () => { const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn const serverMethods = GetServerMethods(mainHandler) - const nostrSettings = mainSettings.nostrRelaySettings + const nostrSettings = settingsManager.getSettings().nostrRelaySettings log("initializing nostr middleware") const { Send } = nostrMiddleware(serverMethods, mainHandler, { ...nostrSettings, apps, clients: [liquidityProviderInfo] }, @@ -36,6 +37,6 @@ const start = async () => { } adminManager.setAppNprofile(appNprofile) const Server = NewServer(serverMethods, serverOptions(mainHandler)) - Server.Listen(mainSettings.servicePort) + Server.Listen(settingsManager.getSettings().serviceSettings.servicePort) } start() diff --git a/src/index.ts b/src/index.ts index 1a56397d..aa4c3c0b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,16 +4,19 @@ import GetServerMethods from './services/serverMethods/index.js' import serverOptions from './auth.js'; import nostrMiddleware from './nostrMiddleware.js' import { getLogger } from './services/helpers/logger.js'; -import { initMainHandler } from './services/main/init.js'; -import { LoadMainSettingsFromEnv } from './services/main/settings.js'; +import { initMainHandler, initSettings } from './services/main/init.js'; import { nip19 } from 'nostr-tools' +import { LoadStorageSettingsFromEnv } from './services/storage/index.js'; //@ts-ignore const { nprofileEncode } = nip19 + const start = async () => { const log = getLogger({}) - const mainSettings = LoadMainSettingsFromEnv() - const keepOn = await initMainHandler(log, mainSettings) + //const mainSettings = LoadMainSettingsFromEnv() + const storageSettings = LoadStorageSettingsFromEnv() + const settingsManager = await initSettings(log, storageSettings) + const keepOn = await initMainHandler(log, settingsManager) if (!keepOn) { log("manual process ended") return @@ -22,22 +25,25 @@ const start = async () => { const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn const serverMethods = GetServerMethods(mainHandler) log("initializing nostr middleware") - const { Send, Stop, Ping } = nostrMiddleware(serverMethods, mainHandler, - { ...mainSettings.nostrRelaySettings, apps, clients: [liquidityProviderInfo] }, + const relays = mainHandler.settings.getSettings().nostrRelaySettings.relays + 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) ) exitHandler(() => { Stop(); mainHandler.Stop() }) log("starting server") mainHandler.attachNostrSend(Send) mainHandler.attachNostrProcessPing(Ping) + mainHandler.attachNostrReset(Reset) mainHandler.StartBeacons() - const appNprofile = nprofileEncode({ pubkey: liquidityProviderInfo.publicKey, relays: mainSettings.nostrRelaySettings.relays }) + const appNprofile = nprofileEncode({ pubkey: liquidityProviderInfo.publicKey, relays }) if (wizard) { - wizard.AddConnectInfo(appNprofile, mainSettings.nostrRelaySettings.relays) + wizard.AddConnectInfo(appNprofile, relays) } adminManager.setAppNprofile(appNprofile) const Server = NewServer(serverMethods, serverOptions(mainHandler)) - Server.Listen(mainSettings.servicePort) + Server.Listen(mainHandler.settings.getSettings().serviceSettings.servicePort) } start() diff --git a/src/nostrMiddleware.ts b/src/nostrMiddleware.ts index c17e2d01..cc1db630 100644 --- a/src/nostrMiddleware.ts +++ b/src/nostrMiddleware.ts @@ -5,8 +5,9 @@ import * as Types from '../proto/autogenerated/ts/types.js' import NewNostrTransport, { NostrRequest } from '../proto/autogenerated/ts/nostr_transport.js'; import { ERROR, getLogger } from "./services/helpers/logger.js"; import { NdebitData, NofferData, NmanageRequest } from "@shocknet/clink-sdk"; - -export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings, onClientEvent: (e: { requestId: string }, fromPub: string) => void): { Stop: () => void, Send: NostrSend, Ping: () => Promise } => { +type ExportedCalls = { Stop: () => void, Send: NostrSend, Ping: () => Promise, Reset: (settings: NostrSettings) => 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 nostrTransport = NewNostrTransport(serverMethods, { NostrUserAuthGuard: async (appId, pub) => { @@ -29,7 +30,7 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett log("operator access from", pub) 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) => { if (!pub || !appId) { 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 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) + } } diff --git a/src/services/helpers/envParser.ts b/src/services/helpers/envParser.ts index 12705ea5..d0838f75 100644 --- a/src/services/helpers/envParser.ts +++ b/src/services/helpers/envParser.ts @@ -25,4 +25,39 @@ export const EnvCanBeBoolean = (name: string): boolean => { const env = process.env[name] if (!env) return false return env.toLowerCase() === 'true' -} \ No newline at end of file +} + +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, 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, 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, defaultValue: boolean, addToDb?: EnvCacher): boolean => { + const v = chooseEnv(key, dbEnv, defaultValue.toString(), addToDb) + return v.toLowerCase() === 'true' +} + +export type StringSetting = { t: 'string', v?: string, defaultValue: string } +export type NumberSetting = { t: 'number', v?: number, defaultValue: number } +export type BooleanSetting = { t: 'boolean', v?: boolean, defaultValue: boolean } +export type EnvSetting = StringSetting | NumberSetting | BooleanSetting +export type SettingsJson = Record> \ No newline at end of file diff --git a/src/services/helpers/utilsWrapper.ts b/src/services/helpers/utilsWrapper.ts index d14b5e64..d7d159f3 100644 --- a/src/services/helpers/utilsWrapper.ts +++ b/src/services/helpers/utilsWrapper.ts @@ -1,4 +1,3 @@ -import { MainSettings } from "../main/settings.js"; import { StateBundler } from "../storage/tlv/stateBundler.js"; import { TlvStorageFactory } from "../storage/tlv/tlvFilesStorageFactory.js"; import { NostrSend } from "../nostr/handler.js"; diff --git a/src/services/lnd/index.ts b/src/services/lnd/index.ts deleted file mode 100644 index 8b1f2d00..00000000 --- a/src/services/lnd/index.ts +++ /dev/null @@ -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 } -} diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index 254ccb7b..24982e73 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -13,23 +13,31 @@ import { OpenChannelReq } from './openChannelReq.js'; import { AddInvoiceReq } from './addInvoiceReq.js'; import { PayInvoiceReq } from './payInvoiceReq.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 { HtlcEvent_EventType } from '../../../proto/lnd/router.js'; import { LiquidityProvider, LiquidityRequest } from '../main/liquidityProvider.js'; import { Utils } from '../helpers/utilsWrapper.js'; import { TxPointSettings } from '../storage/tlv/stateBundler.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 deadLndRetrySeconds = 5 type TxActionOptions = { useProvider: boolean, from: 'user' | 'system' } +type NodeSettingsOverride = { + lndAddr: string + lndCertPath: string + lndMacaroonPath: string +} export default class { lightning: LightningClient invoices: InvoicesClient router: RouterClient chainNotifier: ChainNotifierClient walletKit: WalletKitClient - settings: LndSettings + getSettings: () => { lndSettings: LndSettings, lndNodeSettings: LndNodeSettings } ready = false latestKnownBlockHeigh = 0 latestKnownSettleIndex = 0 @@ -43,15 +51,15 @@ export default class { outgoingOpsLocked = false liquidProvider: LiquidityProvider utils: Utils - constructor(settings: LndSettings, liquidProvider: LiquidityProvider, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb, channelEventCb: ChannelEventCb) { - this.settings = settings + constructor(getSettings: () => { lndSettings: LndSettings, lndNodeSettings: LndNodeSettings }, liquidProvider: LiquidityProvider, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb, channelEventCb: ChannelEventCb, nodeToUse?: "other" | "third" | "fourth") { + this.getSettings = getSettings this.utils = utils this.addressPaidCb = addressPaidCb this.invoicePaidCb = invoicePaidCb this.newBlockCb = newBlockCb this.htlcCb = htlcCb this.channelEventCb = channelEventCb - const { lndAddr, lndCertPath, lndMacaroonPath } = this.settings.mainNode + const { lndAddr, lndCertPath, lndMacaroonPath } = this.getSettings().lndNodeSettings const lndCert = fs.readFileSync(lndCertPath); const macaroon = fs.readFileSync(lndMacaroonPath).toString('hex'); const sslCreds = credentials.createSsl(lndCert); @@ -311,11 +319,11 @@ export default class { } 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 { - 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 }> { diff --git a/src/services/lnd/lsp.ts b/src/services/lnd/lsp.ts index 820bd1b6..e734c62c 100644 --- a/src/services/lnd/lsp.ts +++ b/src/services/lnd/lsp.ts @@ -3,24 +3,8 @@ import { LiquidityProvider } from "../main/liquidityProvider.js" import { getLogger, PubLogger } from '../helpers/logger.js' import LND from "./lnd.js" import { AddressType } from "../../../proto/autogenerated/ts/types.js" -import { EnvCanBeInteger } from "../helpers/envParser.js" -export type LSPSettings = { - olympusServiceUrl: string - voltageServiceUrl: string - flashsatsServiceUrl: string - channelThreshold: number - maxRelativeFee: number -} +import SettingsManager from "../main/settingsManager.js" -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 = { "lsp_balance_sat": string, "client_balance_sat": string, @@ -50,11 +34,11 @@ type OrderResponse = { } class LSP { - settings: LSPSettings + settings: SettingsManager liquidityProvider: LiquidityProvider lnd: LND log: PubLogger - constructor(serviceName: string, settings: LSPSettings, lnd: LND, liquidityProvider: LiquidityProvider) { + constructor(serviceName: string, settings: SettingsManager, lnd: LND, liquidityProvider: LiquidityProvider) { this.settings = settings this.lnd = lnd this.liquidityProvider = liquidityProvider @@ -71,12 +55,15 @@ class 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) } requestChannel = async (maxSpendable: number): Promise => { - 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") 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") 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 chanExpiryBlocks = serviceInfo.options.max_channel_expiry_blocks const order = await this.createOrder({ nodeUri: myUri, lspBalance, clientBalance: "0", chanExpiryBlocks }) @@ -109,8 +96,8 @@ export class FlashsatsLSP extends LSP { return null } const relativeFee = +order.payment.fee_total_sat / channelSize - if (relativeFee > this.settings.maxRelativeFee) { - this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee) + if (relativeFee > maxRelativeFee) { + this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", maxRelativeFee) return null } const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice, decoded.numSatoshis, 'system') @@ -120,7 +107,7 @@ export class FlashsatsLSP extends LSP { } 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 } } return json } @@ -134,7 +121,7 @@ export class FlashsatsLSP extends LSP { confirms_within_blocks: 6, token: "flashsats" } - const res = await fetch(`${this.settings.flashsatsServiceUrl}/channel`, { + const res = await fetch(`${this.settings.getSettings().lspSettings.flashsatsServiceUrl}/channel`, { method: "POST", body: JSON.stringify(req), headers: { "Content-Type": "application/json" } @@ -145,12 +132,15 @@ export class FlashsatsLSP 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) } requestChannel = async (maxSpendable: number): Promise => { - 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") return null } @@ -164,7 +154,7 @@ export class OlympusLSP extends LSP { const lndInfo = await this.lnd.GetInfo() const myPub = lndInfo.identityPubkey 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 chanExpiryBlocks = serviceInfo.max_channel_expiry_blocks 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 } const relativeFee = +order.payment.bolt11.fee_total_sat / channelSize - if (relativeFee > this.settings.maxRelativeFee) { - this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee) + if (relativeFee > maxRelativeFee) { + this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", maxRelativeFee) return null } const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11.invoice, decoded.numSatoshis, 'system') @@ -193,7 +183,7 @@ export class OlympusLSP extends LSP { } 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[] } return json } @@ -209,7 +199,7 @@ export class OlympusLSP extends LSP { funding_confirms_within_blocks: 6, 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", body: JSON.stringify(req), headers: { "Content-Type": "application/json" } @@ -219,7 +209,7 @@ export class OlympusLSP extends LSP { } 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 {} return json } diff --git a/src/services/lnd/settings.ts b/src/services/lnd/settings.ts index 1ced39ee..43a125a5 100644 --- a/src/services/lnd/settings.ts +++ b/src/services/lnd/settings.ts @@ -1,21 +1,5 @@ import { Channel, ChannelEventUpdate } from "../../../proto/lnd/lightning" 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 = { hash: string @@ -62,4 +46,7 @@ export type PaidInvoice = { valueSat: number paymentPreimage: string providerDst?: string -} \ No newline at end of file +} + + + diff --git a/src/services/main/adminManager.ts b/src/services/main/adminManager.ts index c3927f87..97a41bc3 100644 --- a/src/services/main/adminManager.ts +++ b/src/services/main/adminManager.ts @@ -1,16 +1,12 @@ import fs, { watchFile } from "fs"; import crypto from 'crypto' import { ERROR, getLogger } from "../helpers/logger.js"; -import { MainSettings, getDataPath } from "./settings.js"; +import { getDataPath } from "./settings.js"; import Storage from "../storage/index.js"; import * as Types from '../../../proto/autogenerated/ts/types.js' import LND from "../lnd/lnd.js"; +import SettingsManager from "./settingsManager.js"; export class AdminManager { - - - - - storage: Storage log = getLogger({ component: "adminManager" }) adminNpub = "" @@ -23,9 +19,10 @@ export class AdminManager { appNprofile: string lnd: LND nostrConnected: boolean = false - constructor(mainSettings: MainSettings, storage: Storage) { + private nostrReset: () => Promise = async () => { this.log("nostr reset not initialized yet") } + constructor(settings: SettingsManager, storage: Storage) { this.storage = storage - this.dataDir = mainSettings.storageSettings.dataDir + this.dataDir = settings.getStorageSettings().dataDir this.adminNpubPath = getDataPath(this.dataDir, 'admin.npub') this.adminEnrollTokenPath = getDataPath(this.dataDir, 'admin.enroll') this.adminConnectPath = getDataPath(this.dataDir, 'admin.connect') @@ -39,6 +36,14 @@ export class AdminManager { this.start() } + attachNostrReset(f: () => Promise) { + this.nostrReset = f + } + + async ResetNostr() { + await this.nostrReset() + } + setLND = (lnd: LND) => { this.lnd = lnd } diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index 77b67653..5f78570e 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -2,23 +2,23 @@ import jwt from 'jsonwebtoken' import Storage from '../storage/index.js' import * as Types from '../../../proto/autogenerated/ts/types.js' -import { MainSettings } from './settings.js' import ApplicationManager from './applicationManager.js' import { OfferPriceType, ndebitEncode, nmanageEncode, nofferEncode } from '@shocknet/clink-sdk' import { getLogger } from '../helpers/logger.js' +import SettingsManager from './settingsManager.js' export default class { storage: Storage - settings: MainSettings + settings: SettingsManager applicationManager: ApplicationManager log = getLogger({ component: 'AppUserManager' }) - constructor(storage: Storage, settings: MainSettings, applicationManager: ApplicationManager) { + constructor(storage: Storage, settings: SettingsManager, applicationManager: ApplicationManager) { this.storage = storage this.settings = settings this.applicationManager = applicationManager } 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 } { @@ -28,7 +28,7 @@ export default class { t = token.substring("Bearer ".length) } 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) { 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 { - if (!this.settings.allowHttpUpgrade) { + if (!this.settings.getSettings().serviceSettings.allowHttpUpgrade) { throw new Error("http upgrade not allowed") } return { - url: this.settings.serviceUrl, + url: this.settings.getSettings().serviceSettings.serviceUrl, token: this.SignUserToken(ctx.user_id, ctx.app_id, ctx.app_user_id) } } @@ -68,20 +68,20 @@ export default class { if (!appUser) { 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 { userId: ctx.user_id, balance: user.balance_sats, max_withdrawable: this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats, true), user_identifier: appUser.identifier, - network_max_fee_bps: this.settings.lndSettings.feeRateBps, - network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit, - service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps, + network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps, + network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit, + 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] }), 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] }), 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() { this.log("Cleaning up inactive users") 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) { const user = await this.storage.userStorage.GetUser(u.user_id) if (user.balance_sats > 10_000) { continue } 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) } async CleanupNeverActiveUsers() { this.log("Cleaning up never active users") 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) { const user = await this.storage.userStorage.GetUser(u.user_id) if (user.balance_sats > 0) { @@ -160,18 +160,18 @@ export default class { continue } 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 } 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++) { - const {userId,appUserIds} = toDelete[i] - this.log("Deleting user", userId, "progress", i+1, "/", toDelete.length) + const { userId, appUserIds } = toDelete[i] + this.log("Deleting user", userId, "progress", i + 1, "/", toDelete.length) await this.storage.StartTransaction(async tx => { for (const appUserId of appUserIds) { await this.storage.managementStorage.removeUserGrants(appUserId, tx) diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index fd084c27..76ae76db 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -1,7 +1,6 @@ import jwt from 'jsonwebtoken' import Storage from '../storage/index.js' import * as Types from '../../../proto/autogenerated/ts/types.js' -import { MainSettings } from './settings.js' import PaymentManager from './paymentManager.js' import { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorage.js' import { ApplicationUser } from '../storage/entity/ApplicationUser.js' @@ -10,6 +9,7 @@ import crypto from 'crypto' import { Application } from '../storage/entity/Application.js' import { ZapInfo } from '../storage/entity/UserReceivingInvoice.js' import { nofferEncode, ndebitEncode, OfferPriceType, nmanageEncode } from '@shocknet/clink-sdk' +import SettingsManager from './settingsManager.js' const TOKEN_EXPIRY_TIME = 2 * 60 * 1000 // 2 minutes, in milliseconds type NsecLinkingData = { @@ -19,13 +19,13 @@ type NsecLinkingData = { export default class { storage: Storage - settings: MainSettings + settings: SettingsManager paymentManager: PaymentManager nPubLinkingTokens = new Map(); linkingTokenInterval: NodeJS.Timeout | null = null serviceBeaconInterval: NodeJS.Timeout | null = null log: PubLogger - constructor(storage: Storage, settings: MainSettings, paymentManager: PaymentManager) { + constructor(storage: Storage, settings: SettingsManager, paymentManager: PaymentManager) { this.log = getLogger({ component: "ApplicationManager" }) this.storage = storage this.settings = settings @@ -69,7 +69,7 @@ export default class { } } SignAppToken(appId: string): string { - return jwt.sign({ appId }, this.settings.jwtSecret); + return jwt.sign({ appId }, this.settings.getStorageSettings().jwtSecret); } DecodeAppToken(token?: string): string { if (!token) throw new Error("empty app token provided") @@ -78,7 +78,7 @@ export default class { t = token.substring("Bearer ".length) } 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) { throw new Error("the provided token is not an app token") } @@ -150,11 +150,11 @@ export default class { u = user 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] }) log("🔗 [DEBUG] Generated ndebit for user", { userId: u.user.user_id, ndebit: ndebitString }) - + return { identifier: u.identifier, info: { @@ -162,14 +162,14 @@ export default class { balance: u.user.balance_sats, max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true), user_identifier: u.identifier, - network_max_fee_bps: this.settings.lndSettings.feeRateBps, - network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit, - service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps, + network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps, + network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit, + 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] }), 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] }), 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) @@ -212,20 +212,20 @@ export default class { const app = await this.storage.applicationStorage.GetApplication(appId) const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) const max = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true) - const nostrSettings = this.settings.nostrRelaySettings + const nostrSettings = this.settings.getSettings().nostrRelaySettings return { max_withdrawable: max, identifier: req.user_identifier, info: { userId: user.user.user_id, balance: user.user.balance_sats, max_withdrawable: this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true), user_identifier: user.identifier, - network_max_fee_bps: this.settings.lndSettings.feeRateBps, - network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit, - service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps, + network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps, + network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit, + 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] }), 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] }), callback_url: user.callback_url, - bridge_url: this.settings.bridgeUrl + bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl }, } } diff --git a/src/services/main/index.ts b/src/services/main/index.ts index 71c64592..756e9aa4 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -5,7 +5,6 @@ import * as Types from '../../../proto/autogenerated/ts/types.js' import ProductManager from './productManager.js' import ApplicationManager from './applicationManager.js' import PaymentManager, { PendingTx } from './paymentManager.js' -import { MainSettings } from './settings.js' import LND from "../lnd/lnd.js" import { AddressPaidCb, ChannelEventCb, HtlcCb, InvoicePaidCb, NewBlockCb } from "../lnd/settings.js" import { ERROR, getLogger, PubLogger } from "../helpers/logger.js" @@ -31,7 +30,8 @@ import { ManagementManager } from "./managementManager.js" import { Agent } from "https" import { NotificationsManager } from "./notificationsManager.js" import { ApplicationUser } from '../storage/entity/ApplicationUser.js' - +import SettingsManager from './settingsManager.js' +import { NostrSettings } from '../nostr/handler.js' type UserOperationsSub = { id: string newIncomingInvoice: (operation: Types.UserOperation) => void @@ -44,7 +44,7 @@ const appTag = "Lightning.Pub" export default class { storage: Storage lnd: LND - settings: MainSettings + settings: SettingsManager userOperationsSub: UserOperationsSub | null = null adminManager: AdminManager productManager: ProductManager @@ -65,17 +65,22 @@ export default class { //webRTC: webRTC nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") } nostrProcessPing: (() => Promise) | 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.storage = storage this.adminManager = adminManager this.utils = utils this.unlocker = unlocker - const updateProviderBalance = (b: number) => this.storage.liquidityStorage.IncrementTrackedProviderBalance('lnPub', settings.liquiditySettings.liquidityProviderPub, b) - this.liquidityProvider = new LiquidityProvider(settings.liquiditySettings.liquidityProviderPub, this.utils, this.invoicePaidCb, updateProviderBalance) + const updateProviderBalance = (b: number) => this.storage.liquidityStorage.IncrementTrackedProviderBalance('lnPub', settings.getSettings().liquiditySettings.liquidityProviderPub, b) + this.liquidityProvider = new LiquidityProvider(() => this.settings.getSettings().liquiditySettings, this.utils, this.invoicePaidCb, updateProviderBalance) this.rugPullTracker = new RugPullTracker(this.storage, this.liquidityProvider) - this.lnd = new LND(settings.lndSettings, this.liquidityProvider, this.utils, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb, this.channelEventCb) - this.liquidityManager = new LiquidityManager(this.settings.liquiditySettings, this.storage, this.utils, this.liquidityProvider, this.lnd, this.rugPullTracker) + const lndGetSettings = () => ({ + lndSettings: settings.getSettings().lndSettings, + lndNodeSettings: settings.getSettings().lndNodeSettings + }) + this.lnd = new LND(lndGetSettings, this.liquidityProvider, 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.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.offerManager = new OfferManager(this.storage, this.settings, this.lnd, this.applicationManager, this.productManager, this.liquidityManager) 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) } @@ -99,7 +104,7 @@ export default class { StartBeacons() { 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 } + attachNostrReset(f: (settings: NostrSettings) => void) { + this.nostrReset = f + this.adminManager.attachNostrReset(() => this.ResetNostr()) + } + async pingSubProcesses() { if (!this.nostrProcessPing) { 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) { getLogger({ appName: app.name })("cannot update beacon, public key not set") return @@ -421,6 +431,32 @@ export default class { log({ unsigned: event }) 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) + } } diff --git a/src/services/main/init.ts b/src/services/main/init.ts index 0d0cb245..5bb34df3 100644 --- a/src/services/main/init.ts +++ b/src/services/main/init.ts @@ -1,55 +1,62 @@ import { PubLogger, getLogger } from "../helpers/logger.js" import { LiquidityProvider } from "./liquidityProvider.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 Main from "./index.js" import SanityChecker from "./sanityChecker.js" -import { LoadMainSettingsFromEnv, MainSettings } from "./settings.js" import { Utils } from "../helpers/utilsWrapper.js" import { Wizard } from "../wizard/index.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 = { privateKey: string; publicKey: string; appId: string; name: string; } -export const initMainHandler = async (log: PubLogger, mainSettings: MainSettings) => { - const utils = new Utils({ dataDir: mainSettings.storageSettings.dataDir, allowResetMetricsStorages: mainSettings.allowResetMetricsStorages }) - const storageManager = new Storage(mainSettings.storageSettings, utils) - await storageManager.Connect(log) - /* const manualMigration = await TypeOrmMigrationRunner(log, storageManager, mainSettings.storageSettings.dbSettings, process.argv[2]) - if (manualMigration) { - return - } */ - const unlocker = new Unlocker(mainSettings, storageManager) - await unlocker.Unlock() - const adminManager = new AdminManager(mainSettings, storageManager) - let reloadedSettings = mainSettings - let wizard: Wizard | null = null - if (mainSettings.wizard) { - wizard = new Wizard(mainSettings, storageManager, adminManager) - const reload = await wizard.Configure() - if (reload) { - reloadedSettings = LoadMainSettingsFromEnv() - } - } - const mainHandler = new Main(reloadedSettings, storageManager, adminManager, utils, unlocker) +export const initSettings = async (log: PubLogger, storageSettings: StorageSettings): Promise => { + const utils = new Utils({ dataDir: storageSettings.dataDir, allowResetMetricsStorages: storageSettings.allowResetMetricsStorages }) + const storageManager = new Storage(storageSettings, utils) + await storageManager.Connect(log) + const settingsManager = new SettingsManager(storageManager) + await settingsManager.InitSettings() + return settingsManager +} +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() + const adminManager = new AdminManager(settingsManager, storageManager) + const wizard = new Wizard(settingsManager, storageManager, adminManager) + await wizard.Configure() + /* let reloadedSettings = mainSettings + let wizard: Wizard | null = null + if (mainSettings.wizard) { + wizard = new Wizard(settingsManager, storageManager, adminManager) + const reload = await wizard.Configure() + if (reload) { + reloadedSettings = LoadMainSettingsFromEnv() + } + } */ + + const mainHandler = new Main(settingsManager, storageManager, adminManager, utils, unlocker) adminManager.setLND(mainHandler.lnd) await mainHandler.lnd.Warmup() - if (!reloadedSettings.skipSanityCheck) { + if (!settingsManager.getSettings().serviceSettings.skipSanityCheck) { const sanityChecker = new SanityChecker(storageManager, mainHandler.lnd) await sanityChecker.VerifyEventsLog() } + const defaultAppName = settingsManager.getSettings().serviceSettings.defaultAppName 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)) if (!existingWalletApp) { 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) } const apps: AppData[] = await Promise.all(appsData.map(app => { @@ -57,7 +64,7 @@ export const initMainHandler = async (log: PubLogger, mainSettings: MainSettings return mainHandler.storage.applicationStorage.GenerateApplicationKeys(app); } // -- else { - return { privateKey: app.nostr_private_key, publicKey: app.nostr_public_key, appId: app.app_id, name: app.name } + return { privateKey: app.nostr_private_key, publicKey: app.nostr_public_key, appId: app.app_id, name: app.name, avatarUrl: app.avatar_url } } })) const liquidityProviderApp = apps.find(app => defaultNames.includes(app.name)) @@ -79,7 +86,7 @@ export const initMainHandler = async (log: PubLogger, mainSettings: MainSettings await mainHandler.appUserManager.CleanupInactiveUsers() await mainHandler.appUserManager.CleanupNeverActiveUsers() await mainHandler.paymentManager.watchDog.Start() - return { mainHandler, apps, liquidityProviderInfo, liquidityProviderApp, wizard, adminManager } + return { mainHandler, apps, liquidityProviderInfo, liquidityProviderApp, wizard, adminManager, settingsManager } } const processArgs = async (mainHandler: Main) => { diff --git a/src/services/main/liquidityManager.ts b/src/services/main/liquidityManager.ts index be4172d8..ac72975e 100644 --- a/src/services/main/liquidityManager.ts +++ b/src/services/main/liquidityManager.ts @@ -2,22 +2,14 @@ import { getLogger } from "../helpers/logger.js" import { Utils } from "../helpers/utilsWrapper.js" import { LiquidityProvider } from "./liquidityProvider.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 { defaultInvoiceExpiry } from "../storage/paymentStorage.js" import { RugPullTracker } from "./rugPullTracker.js" -export type LiquiditySettings = { - 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 } -} +import SettingsManager from "./settingsManager.js" + export class LiquidityManager { - settings: LiquiditySettings + settings: SettingsManager storage: Storage liquidityProvider: LiquidityProvider rugPullTracker: RugPullTracker @@ -32,16 +24,16 @@ export class LiquidityManager { utils: Utils latestDrain: ({ success: true, amt: number } | { success: false, amt: number, attempt: number, at: Date }) = { success: true, amt: 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.storage = storage this.liquidityProvider = liquidityProvider this.lnd = lnd this.rugPullTracker = rugPullTracker 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.flashsatsLSP = new FlashsatsLSP(settings.lspSettings, lnd, liquidityProvider) + this.flashsatsLSP = new FlashsatsLSP(settings, lnd, liquidityProvider) } GetPaidFees = () => { @@ -58,7 +50,8 @@ export class LiquidityManager { } beforeInvoiceCreation = async (amount: number): Promise<'lnd' | 'provider'> => { - if (this.settings.useOnlyLiquidityProvider) { + + if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { return 'provider' } @@ -86,7 +79,7 @@ export class LiquidityManager { } beforeOutInvoicePayment = async (amount: number): Promise<'lnd' | 'provider'> => { - if (this.settings.useOnlyLiquidityProvider) { + if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { return 'provider' } 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 }> => { - const threshold = this.settings.lspSettings.channelThreshold + const threshold = this.settings.getSettings().lspSettings.channelThreshold if (threshold === 0) { return { shouldOpen: false } } diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 725aa61e..10f9bfad 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -6,11 +6,13 @@ import { Utils } from '../helpers/utilsWrapper.js' import { NostrEvent, NostrSend } from '../nostr/handler.js' import { InvoicePaidCb } from '../lnd/settings.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 nostrCallback = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void } export class LiquidityProvider { - + getSettings: () => LiquiditySettings client: ReturnType clientCbs: Record> = {} clientId: string = "" @@ -28,12 +30,19 @@ export class LiquidityProvider { pendingPayments: Record = {} incrementProviderBalance: (balance: number) => Promise // make the sub process accept client - constructor(pubDestination: string, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise) { + constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise) { this.utils = utils + this.getSettings = getSettings + const pubDestination = getSettings().liquidityProviderPub + const disableLiquidityProvider = getSettings().disableLiquidityProvider if (!pubDestination) { this.log("No pub provider to liquidity provider, will not be initialized") return } + if (disableLiquidityProvider) { + this.log("Liquidity provider is disabled, will not be initialized") + return + } this.log("connecting to liquidity provider:", pubDestination) this.pubDestination = pubDestination this.invoicePaidCb = invoicePaidCb @@ -59,14 +68,14 @@ export class LiquidityProvider { } IsReady = () => { - return this.ready + return this.ready && !this.getSettings().disableLiquidityProvider } AwaitProviderReady = async (): Promise<'inactive' | 'ready'> => { - if (!this.pubDestination) { + if (!this.pubDestination || this.getSettings().disableLiquidityProvider) { return 'inactive' } - if (this.ready) { + if (this.IsReady()) { return 'ready' } return new Promise<'ready'>(res => { @@ -119,7 +128,7 @@ export class LiquidityProvider { } GetLatestMaxWithdrawable = async () => { - if (!this.ready) { + if (!this.IsReady()) { return 0 } const res = await this.GetUserState() @@ -131,7 +140,7 @@ export class LiquidityProvider { } GetLatestBalance = async () => { - if (!this.ready) { + if (!this.IsReady()) { return 0 } const res = await this.GetUserState() @@ -155,7 +164,7 @@ export class LiquidityProvider { } CanProviderHandle = async (req: LiquidityRequest) => { - if (!this.ready) { + if (!this.IsReady()) { return false } const maxW = await this.GetLatestMaxWithdrawable() @@ -167,8 +176,8 @@ export class LiquidityProvider { AddInvoice = async (amount: number, memo: string, from: 'user' | 'system', expiry: number) => { try { - if (!this.ready) { - throw new Error("liquidity provider is not ready yet") + if (!this.IsReady()) { + throw new Error("liquidity provider is not ready yet or disabled") } const res = await this.client.NewInvoice({ amountSats: amount, memo, expiry }) if (res.status === 'ERROR') { @@ -186,8 +195,8 @@ export class LiquidityProvider { PayInvoice = async (invoice: string, decodedAmount: number, from: 'user' | 'system') => { try { - if (!this.ready) { - throw new Error("liquidity provider is not ready yet") + if (!this.IsReady()) { + throw new Error("liquidity provider is not ready yet or disabled") } const userInfo = await this.GetUserState() if (userInfo.status === 'ERROR') { @@ -211,8 +220,8 @@ export class LiquidityProvider { } GetPaymentState = async (invoice: string) => { - if (!this.ready) { - throw new Error("liquidity provider is not ready yet") + if (!this.IsReady()) { + throw new Error("liquidity provider is not ready yet or disabled") } const res = await this.client.GetPaymentState({ invoice }) if (res.status === 'ERROR') { @@ -223,8 +232,8 @@ export class LiquidityProvider { } GetOperations = async () => { - if (!this.ready) { - throw new Error("liquidity provider is not ready yet") + if (!this.IsReady()) { + throw new Error("liquidity provider is not ready yet or disabled") } const res = await this.client.GetUserOperations({ latestIncomingInvoice: { ts: 0, id: 0 }, latestOutgoingInvoice: { ts: 0, id: 0 }, diff --git a/src/services/main/managementManager.ts b/src/services/main/managementManager.ts index 6c643620..2255a8e3 100644 --- a/src/services/main/managementManager.ts +++ b/src/services/main/managementManager.ts @@ -6,19 +6,19 @@ import { NostrEvent, NostrSend } from "../nostr/handler.js"; import Storage from "../storage/index.js"; import { OfferManager } from "./offerManager.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 { UnsignedEvent } from "nostr-tools"; import { getLogger, PubLogger, ERROR } from "../helpers/logger.js"; +import SettingsManager from "./settingsManager.js"; type Result = { state: 'success', result: T } | { state: 'error', err: NmanageFailure } | { state: 'authRequired' } export class ManagementManager { private nostrSend: NostrSend; private storage: Storage; - private settings: MainSettings; + private settings: SettingsManager; private awaitingRequests: Record = {} private logger: PubLogger - constructor(storage: Storage, settings: MainSettings) { + constructor(storage: Storage, settings: SettingsManager) { this.storage = storage; this.settings = settings; this.logger = getLogger({ component: 'ManagementManager' }) @@ -141,7 +141,7 @@ export class ManagementManager { const pointer: OfferPointer = { offer: offer.offer_id, pubkey: appPub, - relay: this.settings.nostrRelaySettings.relays[0], + relay: this.settings.getSettings().nostrRelaySettings.relays[0], priceType: offer.price_sats > 0 ? OfferPriceType.Fixed : OfferPriceType.Spontaneous, price: offer.price_sats, } diff --git a/src/services/main/notificationsManager.ts b/src/services/main/notificationsManager.ts index 55b3ddff..4e3602b0 100644 --- a/src/services/main/notificationsManager.ts +++ b/src/services/main/notificationsManager.ts @@ -1,12 +1,13 @@ import { PushPair, ShockPush } from "../ShockPush/index.js" import { getLogger, PubLogger } from "../helpers/logger.js" +import SettingsManager from "./settingsManager.js" export class NotificationsManager { - private shockPushBaseUrl: string + private settings: SettingsManager private clients: Record = {} private logger: PubLogger - constructor(shockPushBaseUrl: string) { - this.shockPushBaseUrl = shockPushBaseUrl + constructor(settings: SettingsManager) { + this.settings = settings this.logger = getLogger({ component: 'notificationsManager' }) } @@ -15,13 +16,13 @@ export class NotificationsManager { if (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 return newClient } 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") return } diff --git a/src/services/main/offerManager.ts b/src/services/main/offerManager.ts index 213a3192..bbc3b64d 100644 --- a/src/services/main/offerManager.ts +++ b/src/services/main/offerManager.ts @@ -9,7 +9,7 @@ import { UnsignedEvent } from 'nostr-tools'; import { UserOffer } from '../storage/entity/UserOffer.js'; import { LiquidityManager } from "./liquidityManager.js" 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 offerStr = offer.offer_id @@ -34,14 +34,14 @@ export class OfferManager { _nostrSend: NostrSend | null = null - settings: MainSettings + settings: SettingsManager applicationManager: ApplicationManager productManager: ProductManager storage: Storage lnd: LND liquidityManager: LiquidityManager 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.settings = settings this.lnd = lnd @@ -112,7 +112,7 @@ export class OfferManager { if (!offer) { 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] }) } @@ -130,7 +130,7 @@ export class OfferManager { if (toAppend) { offers.push(toAppend) } - const nostrSettings = this.settings.nostrRelaySettings + const nostrSettings = this.settings.getSettings().nostrRelaySettings return { offers: offers.map(o => mapToOfferConfig(ctx.app_user_id, o, { pubkey: app.npub, relay: nostrSettings.relays[0] })) } @@ -163,9 +163,9 @@ export class OfferManager { amount: offerReq.amount_sats, payerData: offerReq.payer_data }) - + const offerInvoice = await this.getNofferInvoice(offerReq, event.appId) - + if (!offerInvoice.success) { const code = offerInvoice.code this.logger("❌ [OFFER REJECTED] Offer request failed", { @@ -179,17 +179,17 @@ export class OfferManager { this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) return } - + this.logger("✅ [OFFER SUCCESS] Generated invoice for offer request", { fromPub: event.pub, eventId: event.id, invoice: offerInvoice.invoice.substring(0, 50) + "...", offer: offerReq.offer }) - + const e = newNofferResponse(JSON.stringify({ bolt11: offerInvoice.invoice }), event) this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) - + this.logger("📤 [OFFER RESPONSE] Sent offer response", { toPub: event.pub, eventId: event.id, @@ -205,7 +205,7 @@ export class OfferManager { } const res = await this.applicationManager.AddAppUserInvoice(appId, { 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' }) 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 }> { const { amount_sats: amount, offer } = offerReq 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) { return this.HandleDefaultUserOffer(offerReq, appId, remote, { memo: offerReq.description, expiry }) diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 2058570d..4dcd0b6e 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -2,7 +2,6 @@ import { bech32 } from 'bech32' import crypto from 'crypto' import Storage from '../storage/index.js' import * as Types from '../../../proto/autogenerated/ts/types.js' -import { MainSettings } from './settings.js' import { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorage.js' import LND from '../lnd/lnd.js' import { Application } from '../storage/entity/Application.js' @@ -17,6 +16,7 @@ import { Watchdog } from './watchdog.js' import { LiquidityManager } from './liquidityManager.js' import { Utils } from '../helpers/utilsWrapper.js' import { UserInvoicePayment } from '../storage/entity/UserInvoicePayment.js' +import SettingsManager from './settingsManager.js' interface UserOperationInfo { serial_id: number paid_amount: number @@ -43,7 +43,7 @@ const confInOne = 1000 * 1000 const confInTwo = 100 * 1000 * 1000 export default class { storage: Storage - settings: MainSettings + settings: SettingsManager lnd: LND addressPaidCb: AddressPaidCb invoicePaidCb: InvoicePaidCb @@ -51,13 +51,13 @@ export default class { watchDog: Watchdog liquidityManager: LiquidityManager 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.settings = settings this.lnd = lnd this.liquidityManager = liquidityManager 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.invoicePaidCb = invoicePaidCb } @@ -163,38 +163,38 @@ export default class { getServiceFee(action: Types.UserOperationType, amount: number, appUser: boolean): number { switch (action) { 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: - return Math.ceil(this.settings.outgoingTxFee * amount) + return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingTxFee * amount) case Types.UserOperationType.INCOMING_INVOICE: 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: 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: 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: throw new Error("Unknown service action type") } } 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") } await this.lnd.SetMockInvoiceAsPaid(req.invoice, req.amount) } 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") } getLogger({})("setting mock balance...") @@ -235,9 +235,9 @@ export default class { GetMaxPayableInvoice(balance: number, appUser: boolean): number { let maxWithinServiceFee = 0 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 { - 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) } @@ -293,7 +293,7 @@ export default class { } 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") } const existingPendingPayment = await this.storage.paymentStorage.GetPaymentOwner(invoice) @@ -412,14 +412,14 @@ export default class { } 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 { 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 } @@ -471,7 +471,7 @@ export default class { } 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 { @@ -493,7 +493,7 @@ export default class { } const { baseUrl, metadata } = opts 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() let maxSendable = remote * 1000 if (remote === 0 && (await this.liquidityManager.liquidityProvider.IsReady())) { @@ -504,7 +504,7 @@ export default class { callback: `${url}?k1=${payK1.key}`, maxSendable: maxSendable, minSendable: 10000, - metadata: metadata ? metadata : defaultLnurlPayMetadata(this.settings.lnurlMetaText), + metadata: metadata ? metadata : defaultLnurlPayMetadata(this.settings.getSettings().serviceSettings.lnurlMetaText), allowsNostr: !!linkedApplication.nostr_public_key, nostrPubkey: linkedApplication.nostr_public_key || "" } @@ -525,10 +525,10 @@ export default class { } return { 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, minSendable: 10000, - metadata: defaultLnurlPayMetadata(this.settings.lnurlMetaText), + metadata: defaultLnurlPayMetadata(this.settings.getSettings().serviceSettings.lnurlMetaText), allowsNostr: !!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, { 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 }) return { pr: invoice.invoice, @@ -620,7 +620,9 @@ export default class { if (!linkedUser) { 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 { @@ -633,8 +635,8 @@ export default class { } return { // We fetch in ascending order - 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 }, + 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 }, operations: operations.map((o: UserOperationInfo): Types.UserOperation => { let identifier = ""; if (o.invoice) { @@ -762,7 +764,7 @@ export default class { async CleanupOldUnpaidInvoices() { this.log("Cleaning up old unpaid invoices") 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() { diff --git a/src/services/main/productManager.ts b/src/services/main/productManager.ts index ed8c225a..3363b326 100644 --- a/src/services/main/productManager.ts +++ b/src/services/main/productManager.ts @@ -2,17 +2,17 @@ import Storage from '../storage/index.js' import * as Types from '../../../proto/autogenerated/ts/types.js' -import { MainSettings } from './settings.js' import PaymentManager from './paymentManager.js' import { defaultInvoiceExpiry } from '../storage/paymentStorage.js' import { nofferEncode, OfferPriceType } from '@shocknet/clink-sdk' +import SettingsManager from './settingsManager.js' export default class { storage: Storage - settings: MainSettings + settings: SettingsManager paymentManager: PaymentManager - constructor(storage: Storage, paymentManager: PaymentManager, settings: MainSettings) { + constructor(storage: Storage, paymentManager: PaymentManager, settings: SettingsManager) { this.storage = storage this.settings = settings this.paymentManager = paymentManager diff --git a/src/services/main/settings.ts b/src/services/main/settings.ts index 18150d52..0886f861 100644 --- a/src/services/main/settings.ts +++ b/src/services/main/settings.ts @@ -1,45 +1,68 @@ -import { LoadStorageSettingsFromEnv, StorageSettings } from '../storage/index.js' -import { LndSettings, NodeSettings } from '../lnd/settings.js' -import { LoadWatchdogSettingsFromEnv, WatchdogSettings } from './watchdog.js' -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' +import { EnvCacher, EnvMustBeNonEmptyString, EnvMustBeInteger, chooseEnv, chooseEnvBool, chooseEnvInt } from '../helpers/envParser.js' +import os from 'os' +import path from 'path' -export type MainSettings = { - storageSettings: StorageSettings, - lndSettings: LndSettings, - watchDogSettings: WatchdogSettings, - liquiditySettings: LiquiditySettings, - nostrRelaySettings: NostrRelaySettings, - jwtSecret: string - walletPasswordPath: string - walletSecretPath: string - incomingTxFee: number - outgoingTxFee: number - incomingAppInvoiceFee: number - incomingAppUserInvoiceFee: number - outgoingAppInvoiceFee: number - outgoingAppUserInvoiceFee: number - outgoingAppUserInvoiceFeeBps: number - userToUserFee: number - appToUserFee: number - serviceUrl: string - servicePort: number - recordPerformance: boolean - skipSanityCheck: boolean - disableExternalPayments: boolean - wizard: boolean - defaultAppName: string - pushBackupsToNostr: boolean - lnurlMetaText: string, - bridgeUrl: string, - allowResetMetricsStorages: boolean - allowHttpUpgrade: boolean - shockPushBaseUrl: string +export type ServiceFeeSettings = { + incomingTxFee: number // Hot + outgoingTxFee: number // Hot + incomingAppInvoiceFee: number // Hot + incomingAppUserInvoiceFee: number // Hot + outgoingAppInvoiceFee: number // Hot + outgoingAppUserInvoiceFee: number // Hot + outgoingAppUserInvoiceFeeBps: number // Hot + userToUserFee: number // Hot + appToUserFee: number // Hot +} + +export const LoadServiceFeeSettingsFromEnv = (dbEnv: Record, 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 // Cold + recordPerformance: boolean // Cold + skipSanityCheck: boolean // Cold + wizard: boolean // Cold + bridgeUrl: string, // Cold + shockPushBaseUrl: string // Cold + + serviceUrl: string // Hot + disableExternalPayments: boolean // Hot + defaultAppName: string // Hot + pushBackupsToNostr: boolean // Hot + lnurlMetaText: string, // Hot + allowHttpUpgrade: boolean // Hot + + +} + +export const LoadServiceSettingsFromEnv = (dbEnv: Record, 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 = { @@ -48,54 +71,145 @@ export type BitcoinCoreSettings = { pass: string } -export type TestSettings = MainSettings & { lndSettings: { otherNode: NodeSettings, thirdNode: NodeSettings, fourthNode: NodeSettings }, bitcoinCoreSettings: BitcoinCoreSettings } -export const LoadMainSettingsFromEnv = (): MainSettings => { - const storageSettings = LoadStorageSettingsFromEnv() - const outgoingAppUserInvoiceFeeBps = EnvCanBeInteger("OUTGOING_INVOICE_FEE_USER_BPS", 0) - const nostrRelaySettings = LoadNosrtRelaySettingsFromEnv() +export type LndNodeSettings = { + lndAddr: string // cold setting + lndCertPath: string // cold setting + lndMacaroonPath: string // cold setting +} +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, addToDb?: EnvCacher): LndNodeSettings => { return { - watchDogSettings: LoadWatchdogSettingsFromEnv(), - lndSettings: LoadLndSettingsFromEnv(), - storageSettings: storageSettings, - 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 || "" + lndAddr: chooseEnv('LND_ADDRESS', dbEnv, "127.0.0.1:10009", addToDb), + lndCertPath: chooseEnv('LND_CERT_PATH', dbEnv, resolveHome("/.lnd/tls.cert"), addToDb), + lndMacaroonPath: chooseEnv('LND_MACAROON_PATH', dbEnv, resolveHome("/.lnd/data/chain/bitcoin/mainnet/admin.macaroon"), addToDb), } } -export const GetTestStorageSettings = (s?: StorageSettings): StorageSettings => { - const eventLogPath = `logs/eventLogV3Test${Date.now()}.csv` - if (s) { - return { dbSettings: { ...s.dbSettings, databaseFile: ":memory:", metricsDatabaseFile: ":memory:" }, eventLogPath, dataDir: "test-data" } +export const LoadLndSettingsFromEnv = (dbEnv: Record, addToDb?: EnvCacher): LndSettings => { + const feeRateBps: number = chooseEnvInt('OUTBOUND_MAX_FEE_BPS', dbEnv, 60, addToDb) + return { + lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, resolveHome("/.lnd/logs/bitcoin/mainnet/lnd.log"), addToDb), + feeRateBps: feeRateBps, + feeRateLimit: feeRateBps / 10000, + feeFixedLimit: chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 100, addToDb), + mockLnd: false } - return { dbSettings: { databaseFile: ":memory:", metricsDatabaseFile: ":memory:", migrate: true }, eventLogPath, dataDir: "test-data" } } -export const LoadTestSettingsFromEnv = (): TestSettings => { +export type NostrRelaySettings = { + relays: string[], + maxEventContentLength: number +} + +const getEnvOrDefault = (name: string, defaultValue: string): string => { + return process.env[name] || defaultValue; +} + +export const LoadNosrtRelaySettingsFromEnv = (dbEnv: Record, addToDb?: EnvCacher): NostrRelaySettings => { + const relaysEnv = chooseEnv("NOSTR_RELAYS", dbEnv, "wss://relay.lightning.pub", addToDb); + const maxEventContentLength = chooseEnvInt("NOSTR_MAX_EVENT_CONTENT_LENGTH", dbEnv, 40000, addToDb) + return { + relays: relaysEnv.split(' '), + maxEventContentLength + } +} + +export type WatchdogSettings = { + maxDiffSats: number // hot setting +} +export const LoadWatchdogSettingsFromEnv = (dbEnv: Record, 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, 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, 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") + } +} + +/* export const LoadTestSettingsFromEnv = (): TestSettings => { const settings = LoadMainSettingsFromEnv() return { @@ -130,26 +244,9 @@ export const LoadTestSettingsFromEnv = (): TestSettings => { pass: EnvMustBeNonEmptyString("BITCOIN_CORE_PASS") }, } -} +} */ + -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 const getDataPath = (dataDir: string, dataPath: string) => { return dataDir !== "" ? `${dataDir}/${dataPath}` : dataPath diff --git a/src/services/main/settingsManager.ts b/src/services/main/settingsManager.ts new file mode 100644 index 00000000..13dd3614 --- /dev/null +++ b/src/services/main/settingsManager.ts @@ -0,0 +1,179 @@ +import Storage, { StorageSettings } from "../storage/index.js" +import { EnvCacher, EnvSetting, SettingsJson, StringSetting } from "../helpers/envParser.js" +import { getLogger, PubLogger } from "../helpers/logger.js" +import { + BitcoinCoreSettings, LiquiditySettings, LndNodeSettings, LndSettings, LoadBitcoinCoreSettingsFromEnv, + LoadFourthLndSettingsFromEnv, LoadLiquiditySettingsFromEnv, LoadSecondLndSettingsFromEnv, LoadThirdLndSettingsFromEnv, + LoadLSPSettingsFromEnv, LSPSettings, ServiceFeeSettings, ServiceSettings, LoadServiceFeeSettingsFromEnv, + LoadNosrtRelaySettingsFromEnv, LoadServiceSettingsFromEnv, LoadWatchdogSettingsFromEnv +} from "./settings.js" +import { LoadLndNodeSettingsFromEnv, LoadLndSettingsFromEnv, NostrRelaySettings, WatchdogSettings } from "./settings.js" +export default class SettingsManager { + storage: Storage + private settings: FullSettings | null = null + //private testSettings: TestSettings | null = null + + log: PubLogger + constructor(storage: Storage) { + this.storage = storage + this.log = getLogger({ component: "SettingsManager" }) + } + + loadEnvs(dbEnv: Record, addToDb?: EnvCacher): FullSettings { + return { + lndNodeSettings: LoadLndNodeSettingsFromEnv(dbEnv, addToDb), + lndSettings: LoadLndSettingsFromEnv(dbEnv, addToDb), + liquiditySettings: LoadLiquiditySettingsFromEnv(dbEnv, addToDb), + lspSettings: LoadLSPSettingsFromEnv(dbEnv, addToDb), + nostrRelaySettings: LoadNosrtRelaySettingsFromEnv(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 InitTestSettings(): Promise { + await this.InitSettings() + await this.updateSkipSanityCheck(true) + await this.updateDisableLiquidityProvider(true) + this.testSettings = { + secondLndSettings: LoadSecondLndSettingsFromEnv(), + thirdLndSettings: LoadThirdLndSettingsFromEnv(), + fourthLndSettings: LoadFourthLndSettingsFromEnv(), + bitcoinCoreSettings: LoadBitcoinCoreSettingsFromEnv(), + } + } */ + + async InitSettings(): Promise { + const dbSettings = await this.storage.settingsStorage.getAllDbEnvs() + const toAdd: Record = {} + const addToDb = (key: string, value: string) => { + toAdd[key] = value + } + this.settings = this.loadEnvs(dbSettings, addToDb) + 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 + } + + /* getTestSettings(): TestSettings { + if (!this.testSettings) { + throw new Error("Test settings not initialized") + } + return this.testSettings + } */ + + async updateDefaultAppName(name: string): Promise { + 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 { + 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 { + 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 { + 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 { + 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, // Hot + nostrRelaySettings: NostrRelaySettings, // Hot + serviceFeeSettings: ServiceFeeSettings, // Hot + serviceSettings: ServiceSettings, // Hot + lspSettings: LSPSettings +} + +/* type TestSettings = { + secondLndSettings: LndNodeSettings + thirdLndSettings: LndNodeSettings + fourthLndSettings: LndNodeSettings + bitcoinCoreSettings: BitcoinCoreSettings +} */ \ No newline at end of file diff --git a/src/services/main/unlocker.ts b/src/services/main/unlocker.ts index b104ffd3..68bb7468 100644 --- a/src/services/main/unlocker.ts +++ b/src/services/main/unlocker.ts @@ -4,23 +4,23 @@ import { GrpcTransport } from "@protobuf-ts/grpc-transport"; import { credentials, Metadata } from '@grpc/grpc-js' import { getLogger } from '../helpers/logger.js'; import { WalletUnlockerClient } from '../../../proto/lnd/walletunlocker.client.js'; -import { MainSettings } from '../main/settings.js'; import { InitWalletReq } from '../lnd/initWalletReq.js'; import Storage from '../storage/index.js' import { LightningClient } from '../../../proto/lnd/lightning.client.js'; import * as Types from '../../../proto/autogenerated/ts/types.js' +import SettingsManager from './settingsManager.js'; const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline }) type EncryptedData = { iv: string, encrypted: string } type Seed = { plaintextSeed: string[], encryptedSeed: EncryptedData } export class Unlocker { - settings: MainSettings + settings: SettingsManager storage: Storage abortController = new AbortController() subbedToBackups = false nodePub: string | null = null log = getLogger({ component: "unlocker" }) - constructor(settings: MainSettings, storage: Storage) { + constructor(settings: SettingsManager, storage: Storage) { this.settings = settings this.storage = storage } @@ -30,8 +30,8 @@ export class Unlocker { } getCreds = () => { - const macroonPath = this.settings.lndSettings.mainNode.lndMacaroonPath - const certPath = this.settings.lndSettings.mainNode.lndCertPath + const macroonPath = this.settings.getSettings().lndNodeSettings.lndMacaroonPath + const certPath = this.settings.getSettings().lndNodeSettings.lndCertPath let macaroon = "" let lndCert: Buffer try { @@ -96,8 +96,8 @@ export class Unlocker { } private waitForLndSync = async (timeoutSeconds: number): Promise => { - const lndLogPath = this.settings.lndSettings.lndLogDir; - if (this.settings.lndSettings.mockLnd) { + const lndLogPath = this.settings.getSettings().lndSettings.lndLogDir; + if (this.settings.getSettings().lndSettings.mockLnd) { this.log("MOCK_LND set, skipping header sync wait."); return; } @@ -284,7 +284,7 @@ export class Unlocker { } GetWalletSecret = (create: boolean) => { - const path = this.settings.walletSecretPath + const path = this.settings.getStorageSettings().walletSecretPath let secret = "" try { secret = fs.readFileSync(path, 'utf-8') @@ -300,7 +300,7 @@ export class Unlocker { } GetWalletPassword = () => { - const path = this.settings.walletPasswordPath + const path = this.settings.getStorageSettings().walletPasswordPath let password = Buffer.alloc(0) try { password = fs.readFileSync(path) @@ -339,14 +339,14 @@ export class Unlocker { } GetUnlockerClient = (cert: Buffer) => { - const host = this.settings.lndSettings.mainNode.lndAddr + const host = this.settings.getSettings().lndNodeSettings.lndAddr const channelCredentials = credentials.createSsl(cert) const transport = new GrpcTransport({ host, channelCredentials }) const client = new WalletUnlockerClient(transport) return client } 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 macaroonCreds = credentials.createFromMetadataGenerator( function (args: any, callback: any) { diff --git a/src/services/main/watchdog.ts b/src/services/main/watchdog.ts index 07e59662..e84c929a 100644 --- a/src/services/main/watchdog.ts +++ b/src/services/main/watchdog.ts @@ -1,4 +1,3 @@ -import { EnvCanBeInteger } from "../helpers/envParser.js"; import FunctionQueue from "../helpers/functionQueue.js"; import { getLogger } from "../helpers/logger.js"; import { Utils } from "../helpers/utilsWrapper.js"; @@ -8,14 +7,8 @@ import { ChannelBalance } from "../lnd/settings.js"; import Storage from '../storage/index.js' import { LiquidityManager } from "./liquidityManager.js"; import { RugPullTracker } from "./rugPullTracker.js"; -export type WatchdogSettings = { - maxDiffSats: number -} -export const LoadWatchdogSettingsFromEnv = (test = false): WatchdogSettings => { - return { - maxDiffSats: EnvCanBeInteger("WATCHDOG_MAX_DIFF_SATS") - } -} +import SettingsManager from "./settingsManager.js"; + export class Watchdog { queue: FunctionQueue initialLndBalance: number; @@ -27,7 +20,7 @@ export class Watchdog { lnd: LND; liquidProvider: LiquidityProvider; liquidityManager: LiquidityManager; - settings: WatchdogSettings; + settings: SettingsManager; storage: Storage; rugPullTracker: RugPullTracker utils: Utils @@ -36,7 +29,7 @@ export class Watchdog { ready = false interval: NodeJS.Timer; 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.settings = settings; this.storage = storage; @@ -114,7 +107,7 @@ export class Watchdog { switch (result.type) { case 'mismatch': if (deltaLnd < 0) { - if (result.absoluteDiff > this.settings.maxDiffSats) { + if (result.absoluteDiff > this.settings.getSettings().watchDogSettings.maxDiffSats) { await this.updateDisruption(true, result.absoluteDiff, lndWithDeltaUsers) return true } @@ -126,7 +119,7 @@ export class Watchdog { break case 'negative': 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) return true } @@ -142,7 +135,7 @@ export class Watchdog { case 'positive': if (deltaLnd < deltaUsers) { 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) return true } @@ -160,12 +153,13 @@ export class Watchdog { updateDisruption = async (isDisrupted: boolean, absoluteDiff: number, lndWithDeltaUsers: number) => { const tracker = await this.getTracker() this.storage.liquidityStorage.UpdateTrackedProviderBalance('lnd', this.lndPubKey, lndWithDeltaUsers) + const maxDiffSats = this.settings.getSettings().watchDogSettings.maxDiffSats if (isDisrupted) { if (tracker.latest_distruption_at_unix === 0) { 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 { - 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 { if (tracker.latest_distruption_at_unix !== 0) { diff --git a/src/services/metrics/index.ts b/src/services/metrics/index.ts index 64d46960..9774d4c1 100644 --- a/src/services/metrics/index.ts +++ b/src/services/metrics/index.ts @@ -9,7 +9,6 @@ import { BalanceEvent } from '../storage/entity/BalanceEvent.js' import { ChannelBalanceEvent } from '../storage/entity/ChannelsBalanceEvent.js' import LND from '../lnd/lnd.js' import HtlcTracker from './htlcTracker.js' -import { MainSettings } from '../main/settings.js' import { getLogger } from '../helpers/logger.js' import { encodeTLV, usageMetricsToTlv } from '../helpers/tlv.js' import { ChannelCloseSummary_ClosureType } from '../../../proto/lnd/lightning.js' diff --git a/src/services/nostr/handler.ts b/src/services/nostr/handler.ts index 357838ec..c576a3c5 100644 --- a/src/services/nostr/handler.ts +++ b/src/services/nostr/handler.ts @@ -7,7 +7,7 @@ import { ERROR, getLogger } from '../helpers/logger.js' import { nip19 } from 'nostr-tools' import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.js' 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 { v2 } = nip44 const { encrypt: encryptV2, decrypt: decryptV2, utils } = v2 @@ -28,24 +28,6 @@ export type NostrSettings = { 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 = { id: string pub: string @@ -104,7 +86,7 @@ let subProcessHandler: Handler | undefined process.on("message", (message: ChildProcessRequest) => { switch (message.type) { case 'settings': - initSubprocessHandler(message.settings) + handleNostrSettings(message.settings) break case 'send': sendToNostr(message.initiator, message.data, message.relays) @@ -117,18 +99,14 @@ process.on("message", (message: ChildProcessRequest) => { break } }) -const initSubprocessHandler = (settings: NostrSettings) => { +const handleNostrSettings = (settings: NostrSettings) => { 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 } - subProcessHandler = new Handler(settings, event => { - send({ - type: 'event', - event: event - }) - }) - + initNostrHandler(settings) new ProcessMetricsCollector((metrics) => { send({ 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) => { if (!subProcessHandler) { getLogger({ component: "nostrMiddleware" })(ERROR, "nostr was not initialized") @@ -152,13 +138,16 @@ export default class Handler { apps: Record = {} eventCallback: (event: NostrEvent) => void log = getLogger({ component: "nostrMiddleware" }) + relay: Relay | null = null + sub: Subscription | null = null + stopped = false constructor(settings: NostrSettings, eventCallback: (event: NostrEvent) => void) { this.settings = settings this.log("connecting to relays:", settings.relays) this.settings.apps.forEach(app => { 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.apps[app.publicKey] = app }) @@ -167,76 +156,87 @@ export default class Handler { async ConnectLoop() { let failures = 0 - while (true) { + while (!this.stopped) { await this.ConnectPromise() const pow = Math.pow(2, failures) const delay = Math.min(pow, 900) 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++ } + this.log("nostr handler stopped") + } + + Stop() { + this.stopped = true + this.sub?.close() + this.relay?.close() + this.relay = null + this.sub = null } async ConnectPromise() { - return new Promise( async (res) => { - const relay = await this.GetRelay() - if (!relay) { + return new Promise(async (res) => { + this.relay = await this.GetRelay() + if (!this.relay) { res() return } - const sub =this.Subscribe(relay) - relay.onclose = (() => { + this.sub = this.Subscribe(this.relay) + this.relay.onclose = (() => { this.log("relay disconnected") - sub.close() - relay.close() + this.sub?.close() + this.relay?.close() + this.relay = null + this.sub = null res() }) }) } - async GetRelay(): Promise { + async GetRelay(): Promise { try { const relay = await Relay.connect(this.settings.relays[0]) if (!relay.connected) { throw new Error("failed to connect to relay") } return relay - } catch (err:any) { + } catch (err: any) { this.log("failed to connect to relay", err.message || err) 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") + /* 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 } - } 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) - - } */ + + 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) { const appIds = Object.keys(this.apps) @@ -246,7 +246,7 @@ export default class Handler { appIds: appIds, listeningForPubkeys: appIds }) - + return relay.subscribe([ { since: Math.ceil(Date.now() / 1000), @@ -308,7 +308,7 @@ export default class Handler { this.log(ERROR, "failed to send event", e.message || e) throw e } - + } async handleSend(data: SendData, keys: { name: string, privateKey: string, publicKey: string }): Promise { diff --git a/src/services/nostr/index.ts b/src/services/nostr/index.ts index 07fe6872..50fdf61b 100644 --- a/src/services/nostr/index.ts +++ b/src/services/nostr/index.ts @@ -1,8 +1,7 @@ import { ChildProcess, fork } from 'child_process' -import { EnvCanBeInteger, EnvMustBeNonEmptyString } from "../helpers/envParser.js" import { NostrSettings, NostrEvent, ChildProcessRequest, ChildProcessResponse, SendData, SendInitiator } from "./handler.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 @@ -10,7 +9,6 @@ type EventCallback = (event: NostrEvent) => void export default class NostrSubprocess { - settings: NostrSettings childProcess: ChildProcess utils: Utils awaitingPongs: (() => void)[] = [] @@ -55,6 +53,10 @@ export default class NostrSubprocess { this.childProcess.send(message) } + Reset(settings: NostrSettings) { + this.sendToChildProcess({ type: 'settings', settings }) + } + Ping() { this.sendToChildProcess({ type: 'ping' }) return new Promise((resolve) => { diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index 5dba3078..c9f92932 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -1,6 +1,4 @@ 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' export default (mainHandler: Main): Types.ServerMethods => { return { @@ -350,9 +348,9 @@ export default (mainHandler: Main): Types.ServerMethods => { if (err != null) throw new Error(err.message) return mainHandler.adminManager.GetInviteTokenState(ctx, req); }, -/* AuthorizeDebit: async ({ ctx, req }) => { - return mainHandler.debitManager.AuthorizeDebit(ctx, req) - }, */ + /* AuthorizeDebit: async ({ ctx, req }) => { + return mainHandler.debitManager.AuthorizeDebit(ctx, req) + }, */ GetDebitAuthorizations: async ({ ctx }) => { return mainHandler.debitManager.GetDebitAuthorizations(ctx) }, diff --git a/src/services/storage/db/db.ts b/src/services/storage/db/db.ts index 881f9124..98ab32bd 100644 --- a/src/services/storage/db/db.ts +++ b/src/services/storage/db/db.ts @@ -5,7 +5,6 @@ import { User } from "../entity/User.js" import { UserReceivingAddress } from "../entity/UserReceivingAddress.js" import { UserReceivingInvoice } from "../entity/UserReceivingInvoice.js" import { UserInvoicePayment } from "../entity/UserInvoicePayment.js" -import { EnvMustBeNonEmptyString } from "../../helpers/envParser.js" import { UserTransactionPayment } from "../entity/UserTransactionPayment.js" import { UserBasicAuth } from "../entity/UserBasicAuth.js" import { UserEphemeralKey } from "../entity/UserEphemeralKey.js" @@ -29,6 +28,7 @@ import { ChannelEvent } from "../entity/ChannelEvent.js" import { AppUserDevice } from "../entity/AppUserDevice.js" import * as fs from 'fs' import { UserAccess } from "../entity/UserAccess.js" +import { AdminSettings } from "../entity/AdminSettings.js" export type DbSettings = { @@ -73,7 +73,8 @@ export const MainDbEntities = { 'Product': Product, 'ManagementGrant': ManagementGrant, 'AppUserDevice': AppUserDevice, - 'UserAccess': UserAccess + 'UserAccess': UserAccess, + 'AdminSettings': AdminSettings } export type MainDbNames = keyof typeof MainDbEntities export const MainDbEntitiesNames = Object.keys(MainDbEntities) @@ -95,12 +96,12 @@ export const newMetricsDb = async (settings: DbSettings, metricsMigrations: Func entities: Object.values(MetricsDbEntities), migrations: metricsMigrations }).initialize(); - + // Secure the DB file permissions if (fs.existsSync(settings.metricsDatabaseFile)) { fs.chmodSync(settings.metricsDatabaseFile, 0o600); } - + const log = getLogger({}); const pendingMigrations = await source.showMigrations() if (pendingMigrations) { @@ -121,12 +122,12 @@ export default async (settings: DbSettings, migrations: Function[]): Promise<{ s //synchronize: true, migrations }).initialize() - + // Secure the DB file permissions if (fs.existsSync(settings.databaseFile)) { fs.chmodSync(settings.databaseFile, 0o600); } - + const log = getLogger({}) const pendingMigrations = await source.showMigrations() if (pendingMigrations) { diff --git a/src/services/storage/entity/AdminSettings.ts b/src/services/storage/entity/AdminSettings.ts new file mode 100644 index 00000000..3b5b01b5 --- /dev/null +++ b/src/services/storage/entity/AdminSettings.ts @@ -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 +} diff --git a/src/services/storage/index.ts b/src/services/storage/index.ts index 0bdcc1a2..33bb9784 100644 --- a/src/services/storage/index.ts +++ b/src/services/storage/index.ts @@ -12,16 +12,58 @@ import DebitStorage from "./debitStorage.js" import OfferStorage from "./offerStorage.js" import { ManagementStorage } from "./managementStorage.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 { Utils } from '../helpers/utilsWrapper.js'; +import SettingsStorage from "./settingsStorage.js"; +import crypto from 'crypto'; export type StorageSettings = { dbSettings: DbSettings eventLogPath: 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 => { - 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 { //DB: DataSource | EntityManager @@ -39,6 +81,7 @@ export default class { offerStorage: OfferStorage managementStorage: ManagementStorage eventsLog: EventsLogManager + settingsStorage: SettingsStorage utils: Utils constructor(settings: StorageSettings, utils: Utils) { this.settings = settings @@ -51,6 +94,7 @@ export default class { //const { source, executedMigrations } = await NewDB(this.settings.dbSettings, allMigrations) //this.DB = source //this.txQueue = new TransactionsQueue("main", this.DB) + this.settingsStorage = new SettingsStorage(this.dbs) this.userStorage = new UserStorage(this.dbs, this.eventsLog) this.productStorage = new ProductStorage(this.dbs) this.applicationStorage = new ApplicationStorage(this.dbs, this.userStorage) @@ -74,6 +118,10 @@ export default class { } */ } + getStorageSettings(): StorageSettings { + return this.settings + } + Stop() { this.dbs.disconnect() } diff --git a/src/services/storage/migrations/1761683639419-admin_settings.ts b/src/services/storage/migrations/1761683639419-admin_settings.ts new file mode 100644 index 00000000..2a2e9990 --- /dev/null +++ b/src/services/storage/migrations/1761683639419-admin_settings.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AdminSettings1761683639419 implements MigrationInterface { + name = 'AdminSettings1761683639419' + + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`DROP TABLE "admin_settings"`); + } +} diff --git a/src/services/storage/migrations/runner.ts b/src/services/storage/migrations/runner.ts index 37f89b0a..c110859c 100644 --- a/src/services/storage/migrations/runner.ts +++ b/src/services/storage/migrations/runner.ts @@ -26,12 +26,14 @@ import { UserReceivingInvoiceIdx1753109184611 } from './1753109184611-user_recei import { UserAccess1759426050669 } from './1759426050669-user_access.js' import { AddBlindToUserOffer1760000000000 } from './1760000000000-add_blind_to_user_offer.js' import { ApplicationAvatarUrl1761000001000 } from './1761000001000-application_avatar_url.js' +import { AdminSettings1761683639419 } from './1761683639419-admin_settings.js' export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, - DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513, - InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000] + DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513, + InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175, + UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419] export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411] /* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise => { diff --git a/src/services/storage/settingsStorage.ts b/src/services/storage/settingsStorage.ts new file mode 100644 index 00000000..3b4075e5 --- /dev/null +++ b/src/services/storage/settingsStorage.ts @@ -0,0 +1,34 @@ +import { EnvSetting, SettingsJson } from "../helpers/envParser.js"; +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> { + const settings = await this.dbs.Find('AdminSettings', {}); + const envs: Record = {}; + for (const setting of settings) { + envs[setting.env_name] = setting.env_value; + } + return envs; + } + + async getDbEnv(envName: string): Promise { + const setting = await this.dbs.FindOne('AdminSettings', { where: { env_name: envName } }); + if (!setting) return undefined; + return setting.env_value; + } + + async setDbEnvIFNeeded(envName: string, envValue: string): Promise { + const setting = await this.dbs.FindOne('AdminSettings', { where: { env_name: envName } }); + if (!setting) { + await this.dbs.CreateAndSave('AdminSettings', { env_name: envName, env_value: envValue }); + } else if (setting.env_value !== envValue) { + setting.env_value = envValue; + await this.dbs.Update('AdminSettings', setting.serial_id, setting); + } + } +} \ No newline at end of file diff --git a/src/services/wizard/index.ts b/src/services/wizard/index.ts index f28095c3..8613941c 100644 --- a/src/services/wizard/index.ts +++ b/src/services/wizard/index.ts @@ -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 NewWizardServer from "../../../proto/wizard_service/autogenerated/ts/express_server.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 { Unlocker } from "../main/unlocker.js" import { AdminManager } from '../main/adminManager.js'; @@ -17,16 +14,15 @@ export type WizardSettings = { const defaultProviderPub = "" export class Wizard { log = getLogger({ component: "wizard" }) - settings: MainSettings + settings: SettingsManager adminManager: AdminManager storage: Storage configQueue: { res: (reload: boolean) => void }[] = [] - pendingConfig: WizardSettings | null = null awaitingNprofile: { res: (nprofile: string) => void }[] = [] nprofile = "" relays: string[] = [] - constructor(mainSettings: MainSettings, storage: Storage, adminManager: AdminManager) { - this.settings = mainSettings + constructor(settings: SettingsManager, storage: Storage, adminManager: AdminManager) { + this.settings = settings this.adminManager = adminManager this.storage = storage this.log('Starting wizard...') @@ -36,16 +32,16 @@ export class Wizard { GetAdminConnectInfo: async () => { return this.GetAdminConnectInfo() }, GetServiceState: async () => { return this.GetServiceState() } }, { GuestAuthGuard: async () => "", metricsCallback: () => { }, staticFiles: 'static' }) - wizardServer.Listen(mainSettings.servicePort + 1) + wizardServer.Listen(settings.getSettings().serviceSettings.servicePort + 1) } GetServiceState = async (): Promise => { try { const apps = await this.storage.applicationStorage.GetApplications() 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 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 let lndState: WizardTypes.LndState = WizardTypes.LndState.OFFLINE let watchdogOk = false @@ -60,17 +56,17 @@ export class Wizard { } return { admin_npub: this.adminManager.GetAdminNpub(), - http_url: this.settings.serviceUrl, + http_url: this.settings.getSettings().serviceSettings.serviceUrl, lnd_state: lndState, nprofile: this.nprofile, provider_name: defaultApp?.name || appNamesList, relay_connected: this.adminManager.GetNostrConnected(), relays: this.relays, watchdog_ok: watchdogOk, - source_name: defaultApp?.name || this.settings.defaultAppName || appNamesList, + source_name: defaultApp?.name || this.settings.getSettings().serviceSettings.defaultAppName || appNamesList, relay_url: relayUrl, - automate_liquidity: this.settings.liquiditySettings.liquidityProviderPub !== 'null', - push_backups_to_nostr: this.settings.pushBackupsToNostr, + automate_liquidity: this.settings.getSettings().liquiditySettings.liquidityProviderPub !== 'null', + push_backups_to_nostr: this.settings.getSettings().serviceSettings.pushBackupsToNostr, avatar_url: defaultApp?.avatar_url || '', app_id: defaultApp?.app_id || '' } @@ -96,10 +92,10 @@ export class Wizard { } } - + WizardState = async (): Promise => { return { - config_sent: this.pendingConfig !== null, + config_sent: false, admin_linked: this.adminManager.GetAdminNpub() !== "", } } @@ -148,7 +144,7 @@ export class Wizard { } Configure = async (): Promise => { - if (this.IsInitialized() || this.pendingConfig !== null) { + if (this.IsInitialized()) { return false } return new Promise((res) => { @@ -165,78 +161,99 @@ export class Wizard { 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) - try { - const appsList = await this.storage.applicationStorage.GetApplications() - const defaultNames = ['wallet', 'wallet-test', this.settings.defaultAppName] - const existingDefaultApp = appsList.find(app => defaultNames.includes(app.name)) || appsList[0] - if (existingDefaultApp) { - await this.storage.applicationStorage.UpdateApplication(existingDefaultApp, { name: req.source_name, avatar_url: (req as any).avatar_url || existingDefaultApp.avatar_url }) - } - } catch (e) { - this.log(`Error updating app info: ${(e as Error).message}`) + await this.settings.updateDisableLiquidityProvider(pendingConfig.automateLiquidity) + await this.settings.updatePushBackupsToNostr(pendingConfig.pushBackupsToNostr) + const oldAppName = this.settings.getSettings().serviceSettings.defaultAppName + const nameUpdated = await this.settings.updateDefaultAppName(pendingConfig.sourceName) + if (nameUpdated) { + await this.updateDefaultApp(oldAppName, req.avatar_url) + } + const relayUpdated = await this.settings.updateRelayUrl(pendingConfig.relayUrl) + if (relayUpdated && this.IsInitialized()) { + await this.adminManager.ResetNostr() } // If already initialized, treat as idempotent update for env and exit if (this.IsInitialized()) { - this.updateEnvFile(pendingConfig) + this.log("reloaded wizard config") + if (nameUpdated) this.log("name updated") + if (relayUpdated) this.log("relay updated") return } // 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 = [] return } - updateEnvFile = (pendingConfig: WizardSettings) => { - let envFileContent: string[] = [] - try { - envFileContent = fs.readFileSync('.env', 'utf-8').split('\n') - } catch (err: any) { - if (err.code !== 'ENOENT') { - throw err - } - } - - 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() + async updateConfigs(pendingConfig: WizardSettings): Promise { + await this.settings.updateDefaultAppName(pendingConfig.sourceName) + await this.settings.updateRelayUrl(pendingConfig.relayUrl) + await this.settings.updateDisableLiquidityProvider(pendingConfig.automateLiquidity) + await this.settings.updatePushBackupsToNostr(pendingConfig.pushBackupsToNostr) } + + updateDefaultApp = async (currentName: string, avatarUrl?: string): Promise => { + const newName = this.settings.getSettings().serviceSettings.defaultAppName + try { + const appsList = await this.storage.applicationStorage.GetApplications() + const defaultNames = ['wallet', 'wallet-test', currentName] + const existingDefaultApp = appsList.find(app => defaultNames.includes(app.name)) || appsList[0] + 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}`) + } + } + /* + updateEnvFile = (pendingConfig: WizardSettings) => { + let envFileContent: string[] = [] + try { + envFileContent = fs.readFileSync('.env', 'utf-8').split('\n') + } catch (err: any) { + if (err.code !== 'ENOENT') { + throw err + } + } + + 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() + } */ } \ No newline at end of file diff --git a/src/tests/bitcoinCore.ts b/src/tests/bitcoinCore.ts index 58b5d490..a853831b 100644 --- a/src/tests/bitcoinCore.ts +++ b/src/tests/bitcoinCore.ts @@ -1,16 +1,16 @@ // @ts-ignore import BitcoinCore from 'bitcoin-core'; -import { TestSettings } from '../services/main/settings'; +import { BitcoinCoreSettings } from '../services/main/settings'; export class BitcoinCoreWrapper { core: BitcoinCore addr: { address: string } - constructor(settings: TestSettings) { + constructor(settings: BitcoinCoreSettings) { this.core = new BitcoinCore({ //network: 'regtest', host: '127.0.0.1', - port: `${settings.bitcoinCoreSettings.port}`, - username: settings.bitcoinCoreSettings.user, - password: settings.bitcoinCoreSettings.pass, + port: `${settings.port}`, + username: settings.user, + password: settings.pass, // use a long timeout due to the time it takes to mine a lot of blocks timeout: 5 * 60 * 1000, }) diff --git a/src/tests/networkSetup.ts b/src/tests/networkSetup.ts index 5944a1fc..8331531d 100644 --- a/src/tests/networkSetup.ts +++ b/src/tests/networkSetup.ts @@ -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 LND from '../services/lnd/lnd.js' import { LiquidityProvider } from "../services/main/liquidityProvider.js" import { Utils } from "../services/helpers/utilsWrapper.js" +import { LoadStorageSettingsFromEnv } from "../services/storage/index.js" export type ChainTools = { mine: (amount: number) => Promise } export const setupNetwork = async (): Promise => { - const settings = LoadTestSettingsFromEnv() - const core = new BitcoinCoreWrapper(settings) + const storageSettings = GetTestStorageSettings(LoadStorageSettingsFromEnv()) + 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.Mine(1) - const setupUtils = new Utils({ dataDir: settings.storageSettings.dataDir, allowResetMetricsStorages: settings.allowResetMetricsStorages }) - const alice = new LND(settings.lndSettings, new LiquidityProvider("", setupUtils, async () => { }, async () => { }), setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) - const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, new LiquidityProvider("", setupUtils, async () => { }, async () => { }), setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) + const lndSettings = LoadLndSettingsFromEnv({}) + const lndNodeSettings = LoadLndNodeSettingsFromEnv({}) + const secondLndNodeSettings = LoadSecondLndSettingsFromEnv() + const liquiditySettings: LiquiditySettings = { disableLiquidityProvider: true, liquidityProviderPub: "", useOnlyLiquidityProvider: false } + const alice = new LND(() => ({ lndSettings, lndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) + const bob = new LND(() => ({ lndSettings, lndNodeSettings: secondLndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) await tryUntil(async i => { const peers = await alice.ListPeers() if (peers.peers.length > 0) { diff --git a/src/tests/setupBootstrapped.ts b/src/tests/setupBootstrapped.ts index 0b5b1d24..dfcf16c7 100644 --- a/src/tests/setupBootstrapped.ts +++ b/src/tests/setupBootstrapped.ts @@ -1,16 +1,22 @@ import { getLogger } from '../services/helpers/logger.js' -import { initMainHandler } from '../services/main/init.js' -import { LoadTestSettingsFromEnv } from '../services/main/settings.js' +import { initMainHandler, initSettings } from '../services/main/init.js' import { SendData } from '../services/nostr/handler.js' import { TestBase, TestUserData } from './testBase.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) => { - const settings = LoadTestSettingsFromEnv() - settings.liquiditySettings.useOnlyLiquidityProvider = true - settings.liquiditySettings.liquidityProviderPub = T.app.publicKey - settings.lndSettings.mainNode = settings.lndSettings.thirdNode - const initialized = await initMainHandler(getLogger({ component: "bootstrapped" }), settings) + const storageSettings = GetTestStorageSettings(LoadStorageSettingsFromEnv()) + const settingsManager = await initSettings(getLogger({ component: "bootstrapped" }), storageSettings) + const thirdNodeSettings = LoadThirdLndSettingsFromEnv() + settingsManager.OverrideTestSettings(s => { + 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) { throw new Error("failed to initialize bootstrapped main handler") } diff --git a/src/tests/testBase.ts b/src/tests/testBase.ts index 95454a95..235e8f86 100644 --- a/src/tests/testBase.ts +++ b/src/tests/testBase.ts @@ -1,10 +1,9 @@ import 'dotenv/config' // TODO - test env 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 Storage from '../services/storage/index.js' +import Storage, { GetTestStorageSettings, LoadStorageSettingsFromEnv } from '../services/storage/index.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 { defaultInvoiceExpiry } from '../services/storage/paymentStorage.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 { TlvStorageFactory } from '../services/storage/tlv/tlvFilesStorageFactory.js' import { ChainTools } from './networkSetup.js' +import { LiquiditySettings, LoadLndSettingsFromEnv, LoadSecondLndSettingsFromEnv, LoadThirdLndSettingsFromEnv } from '../services/main/settings.js' chai.use(chaiString) export const expect = chai.expect export type Describe = (message: string, failure?: boolean) => void @@ -45,7 +45,7 @@ export type StorageTestBase = { } export const setupStorageTest = async (d: Describe): Promise => { - const settings = GetTestStorageSettings() + const settings = GetTestStorageSettings(LoadStorageSettingsFromEnv()) const utils = new Utils({ dataDir: settings.dataDir, allowResetMetricsStorages: true }) const storageManager = new Storage(settings, utils) await storageManager.Connect(console.log) @@ -61,8 +61,15 @@ export const teardownStorageTest = async (T: StorageTestBase) => { } export const SetupTest = async (d: Describe, chainTools: ChainTools): Promise => { - const settings = LoadTestSettingsFromEnv() - const initialized = await initMainHandler(getLogger({ component: "mainForTest" }), settings) + const storageSettings = GetTestStorageSettings(LoadStorageSettingsFromEnv()) + 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) { throw new Error("failed to initialize main handler") } @@ -73,16 +80,19 @@ export const SetupTest = async (d: Describe, chainTools: ChainTools): Promise { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { }) await externalAccessToMainLnd.Warmup() */ - - const otherLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.otherNode } - const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) + const liquiditySettings: LiquiditySettings = { disableLiquidityProvider: true, liquidityProviderPub: "", useOnlyLiquidityProvider: false } + const lndSettings = LoadLndSettingsFromEnv({}) + const secondLndNodeSettings = LoadSecondLndSettingsFromEnv() + const otherLndSetting = () => ({ lndSettings, lndNodeSettings: secondLndNodeSettings }) + const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider(() => liquiditySettings, extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) await externalAccessToOtherLnd.Warmup() - const thirdLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.thirdNode } - const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) + const thirdLndNodeSettings = LoadThirdLndSettingsFromEnv() + const thirdLndSetting = () => ({ lndSettings, lndNodeSettings: thirdLndNodeSettings }) + const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider(() => liquiditySettings, extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) await externalAccessToThirdLnd.Warmup()