Merge branch 'master' into fix-lnd-crash

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

View file

@ -20,6 +20,7 @@ import { UserOffer } from "./build/src/services/storage/entity/UserOffer.js"
import { ManagementGrant } from "./build/src/services/storage/entity/ManagementGrant.js"
import { 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
//npx typeorm migration:generate ./src/services/storage/migrations/admin_settings -d ./datasource.js

View file

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

View file

@ -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

View file

@ -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()

View file

@ -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()

View file

@ -5,8 +5,9 @@ import * as Types from '../proto/autogenerated/ts/types.js'
import NewNostrTransport, { NostrRequest } from '../proto/autogenerated/ts/nostr_transport.js';
import { 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<void> } => {
type ExportedCalls = { Stop: () => void, Send: NostrSend, Ping: () => Promise<void>, 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)
}
}

View file

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

View file

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

View file

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

View file

@ -13,23 +13,31 @@ import { OpenChannelReq } from './openChannelReq.js';
import { AddInvoiceReq } from './addInvoiceReq.js';
import { 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 = 20
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
@ -44,8 +52,8 @@ export default class {
liquidProvider: LiquidityProvider
utils: Utils
unlockLnd: () => Promise<void>
constructor(settings: LndSettings, liquidProvider: LiquidityProvider, unlockLnd: () => Promise<any>, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb, channelEventCb: ChannelEventCb) {
this.settings = settings
constructor(getSettings: () => { lndSettings: LndSettings, lndNodeSettings: LndNodeSettings }, liquidProvider: LiquidityProvider, unlockLnd: () => Promise<any>, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb, channelEventCb: ChannelEventCb) {
this.getSettings = getSettings
this.utils = utils
this.unlockLnd = unlockLnd
this.addressPaidCb = addressPaidCb
@ -53,7 +61,7 @@ export default class {
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);
@ -335,11 +343,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 }> {

View file

@ -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<OrderResponse | null> => {
if (!this.settings.flashsatsServiceUrl) {
const s = this.settings.getSettings().lspSettings
const flashsatsServiceUrl = s.flashsatsServiceUrl
const maxRelativeFee = s.maxRelativeFee
if (!flashsatsServiceUrl) {
this.log("no flashsats service url provided")
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<OrderResponse | null> => {
if (!this.settings.olympusServiceUrl) {
const s = this.settings.getSettings().lspSettings
const olympusServiceUrl = s.olympusServiceUrl
const maxRelativeFee = s.maxRelativeFee
if (!olympusServiceUrl) {
this.log("no olympus service url provided")
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
}

View file

@ -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
@ -63,3 +47,6 @@ export type PaidInvoice = {
paymentPreimage: string
providerDst?: string
}

View file

@ -1,16 +1,11 @@
import fs, { watchFile } from "fs";
import crypto from 'crypto'
import { ERROR, getLogger } from "../helpers/logger.js";
import { MainSettings, 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 +18,10 @@ export class AdminManager {
appNprofile: string
lnd: LND
nostrConnected: boolean = false
constructor(mainSettings: MainSettings, storage: Storage) {
private nostrReset: () => Promise<void> = async () => { this.log("nostr reset not initialized yet") }
constructor(settings: SettingsManager, storage: Storage) {
this.storage = storage
this.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 +35,14 @@ export class AdminManager {
this.start()
}
attachNostrReset(f: () => Promise<void>) {
this.nostrReset = f
}
async ResetNostr() {
await this.nostrReset()
}
setLND = (lnd: LND) => {
this.lnd = lnd
}
@ -250,3 +254,7 @@ export class AdminManager {
}
}
}
const getDataPath = (dataDir: string, dataPath: string) => {
return dataDir !== "" ? `${dataDir}/${dataPath}` : dataPath
}

View file

@ -2,23 +2,23 @@ import jwt from 'jsonwebtoken'
import Storage from '../storage/index.js'
import * 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)

View file

@ -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<string, NsecLinkingData>();
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,7 +150,7 @@ 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 })
@ -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
},
}
}

View file

@ -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<void>) | null = null
constructor(settings: MainSettings, storage: Storage, adminManager: AdminManager, utils: Utils, unlocker: Unlocker) {
nostrReset: (settings: NostrSettings) => void = () => { getLogger({})("nostr reset not initialized yet") }
constructor(settings: SettingsManager, storage: Storage, adminManager: AdminManager, utils: Utils, unlocker: Unlocker) {
this.settings = settings
this.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.unlocker.Unlock(), 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.unlocker.Unlock(), this.utils, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb, this.channelEventCb)
this.liquidityManager = new LiquidityManager(this.settings, this.storage, this.utils, this.liquidityProvider, this.lnd, this.rugPullTracker)
this.metricsManager = new MetricsManager(this.storage, this.lnd)
this.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)
}
}

View file

@ -1,55 +1,56 @@
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)
export const initSettings = async (log: PubLogger, storageSettings: StorageSettings): Promise<SettingsManager> => {
const utils = new Utils({ dataDir: storageSettings.dataDir, allowResetMetricsStorages: storageSettings.allowResetMetricsStorages })
const storageManager = new Storage(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)
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(mainSettings, storageManager)
let reloadedSettings = mainSettings
const adminManager = new AdminManager(settingsManager, storageManager)
let wizard: Wizard | null = null
if (mainSettings.wizard) {
wizard = new Wizard(mainSettings, storageManager, adminManager)
const reload = await wizard.Configure()
if (reload) {
reloadedSettings = LoadMainSettingsFromEnv()
}
if (settingsManager.getSettings().serviceSettings.wizard) {
wizard = new Wizard(settingsManager, storageManager, adminManager)
await wizard.Configure()
}
const mainHandler = new Main(reloadedSettings, storageManager, adminManager, utils, unlocker)
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 => {

View file

@ -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 }
}

View file

@ -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<T> = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void }
export class LiquidityProvider {
getSettings: () => LiquiditySettings
client: ReturnType<typeof newNostrClient>
clientCbs: Record<string, nostrCallback<any>> = {}
clientId: string = ""
@ -28,12 +30,19 @@ export class LiquidityProvider {
pendingPayments: Record<string, number> = {}
incrementProviderBalance: (balance: number) => Promise<void>
// make the sub process accept client
constructor(pubDestination: string, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise<any>) {
constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise<any>) {
this.utils = utils
this.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 },

View file

@ -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<T> = { 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<string, { request: NmanageRequest, event: NostrEvent }> = {}
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,
}

View file

@ -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<string, ShockPush> = {}
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
}

View file

@ -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] }))
}
@ -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 }

View file

@ -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<Types.LnurlLinkResponse> {
@ -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,7 +635,7 @@ export default class {
}
return {
// We fetch in ascending order
toIndex: { ts: operations.at(-1)!.paid_at_unix, id: operations.at(-1)!.serial_id } ,
toIndex: { ts: operations.at(-1)!.paid_at_unix, id: operations.at(-1)!.serial_id },
fromIndex: { ts: operations[0].paid_at_unix, id: operations[0]!.serial_id },
operations: operations.map((o: UserOperationInfo): Types.UserOperation => {
let identifier = "";
@ -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() {

View file

@ -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

View file

@ -1,23 +1,8 @@
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
export type ServiceFeeSettings = {
incomingTxFee: number
outgoingTxFee: number
incomingAppInvoiceFee: number
@ -27,19 +12,57 @@ export type MainSettings = {
outgoingAppUserInvoiceFeeBps: number
userToUserFee: number
appToUserFee: number
serviceUrl: string
}
export const LoadServiceFeeSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): ServiceFeeSettings => {
const outgoingAppUserInvoiceFeeBps = chooseEnvInt("OUTGOING_INVOICE_FEE_USER_BPS", dbEnv, 0, addToDb)
return {
incomingTxFee: chooseEnvInt("INCOMING_CHAIN_FEE_ROOT_BPS", dbEnv, 0, addToDb) / 10000,
outgoingTxFee: chooseEnvInt("OUTGOING_CHAIN_FEE_ROOT_BPS", dbEnv, 60, addToDb) / 10000,
incomingAppInvoiceFee: chooseEnvInt("INCOMING_INVOICE_FEE_ROOT_BPS", dbEnv, 0, addToDb) / 10000,
outgoingAppInvoiceFee: chooseEnvInt("OUTGOING_INVOICE_FEE_ROOT_BPS", dbEnv, 60, addToDb) / 10000,
incomingAppUserInvoiceFee: chooseEnvInt("INCOMING_INVOICE_FEE_USER_BPS", dbEnv, 0, addToDb) / 10000,
outgoingAppUserInvoiceFeeBps,
outgoingAppUserInvoiceFee: outgoingAppUserInvoiceFeeBps / 10000,
userToUserFee: chooseEnvInt("TX_FEE_INTERNAL_USER_BPS", dbEnv, 0, addToDb) / 10000,
appToUserFee: chooseEnvInt("TX_FEE_INTERNAL_ROOT_BPS", dbEnv, 0, addToDb) / 10000,
}
}
export type ServiceSettings = {
servicePort: number
recordPerformance: boolean
skipSanityCheck: boolean
disableExternalPayments: boolean
wizard: boolean
bridgeUrl: string,
shockPushBaseUrl: string
serviceUrl: string
disableExternalPayments: boolean
defaultAppName: string
pushBackupsToNostr: boolean
lnurlMetaText: string,
bridgeUrl: string,
allowResetMetricsStorages: boolean
allowHttpUpgrade: boolean
shockPushBaseUrl: string
}
export const LoadServiceSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): ServiceSettings => {
const port = chooseEnvInt("PORT", dbEnv, 1776, addToDb)
return {
serviceUrl: chooseEnv("SERVICE_URL", dbEnv, `http://localhost:${port}`, addToDb),
servicePort: port,
recordPerformance: chooseEnvBool("RECORD_PERFORMANCE", dbEnv, false, addToDb),
skipSanityCheck: chooseEnvBool("SKIP_SANITY_CHECK", dbEnv, false, addToDb),
disableExternalPayments: chooseEnvBool("DISABLE_EXTERNAL_PAYMENTS", dbEnv, false, addToDb),
wizard: chooseEnvBool("WIZARD", dbEnv, false, addToDb),
defaultAppName: chooseEnv("DEFAULT_APP_NAME", dbEnv, "wallet", addToDb),
pushBackupsToNostr: chooseEnvBool("PUSH_BACKUPS_TO_NOSTR", dbEnv, false, addToDb),
lnurlMetaText: chooseEnv("LNURL_META_TEXT", dbEnv, "LNURL via Lightning.pub", addToDb),
bridgeUrl: chooseEnv("BRIDGE_URL", dbEnv, "https://shockwallet.app", addToDb),
allowHttpUpgrade: chooseEnvBool("ALLOW_HTTP_UPGRADE", dbEnv, false, addToDb),
shockPushBaseUrl: chooseEnv("SHOCK_PUSH_URL", dbEnv, "", addToDb),
}
}
export type BitcoinCoreSettings = {
@ -48,109 +71,136 @@ 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<string, string | undefined>, 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<string, string | undefined>, 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 settings = LoadMainSettingsFromEnv()
export const LoadNostrRelaySettingsFromEnv = (dbEnv: Record<string, string | undefined>, 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<string, string | undefined>, addToDb?: EnvCacher): WatchdogSettings => {
return {
maxDiffSats: chooseEnvInt("WATCHDOG_MAX_DIFF_SATS", dbEnv, 0, addToDb)
}
}
export type LSPSettings = {
olympusServiceUrl: string // hot setting
voltageServiceUrl: string // unused?
flashsatsServiceUrl: string // hot setting
channelThreshold: number // hot setting
maxRelativeFee: number // hot setting
}
export const LoadLSPSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): LSPSettings => {
const olympusServiceUrl = chooseEnv("OLYMPUS_LSP_URL", dbEnv, "https://lsps1.lnolymp.us/api/v1", addToDb)
const voltageServiceUrl = chooseEnv("VOLTAGE_LSP_URL", dbEnv, "https://lsp.voltageapi.com/api/v1", addToDb)
const flashsatsServiceUrl = chooseEnv("FLASHSATS_LSP_URL", dbEnv, "https://lsp.flashsats.xyz/lsp/channel", addToDb)
const channelThreshold = chooseEnvInt("LSP_CHANNEL_THRESHOLD", dbEnv, 1000000, addToDb)
const maxRelativeFee = chooseEnvInt("LSP_MAX_FEE_BPS", dbEnv, 100, addToDb) / 10000
return { olympusServiceUrl, voltageServiceUrl, channelThreshold, maxRelativeFee, flashsatsServiceUrl }
}
export type LiquiditySettings = {
liquidityProviderPub: string // cold setting
useOnlyLiquidityProvider: boolean // hot setting
disableLiquidityProvider: boolean // hot setting
}
export const LoadLiquiditySettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): LiquiditySettings => {
//const liquidityProviderPub = process.env.LIQUIDITY_PROVIDER_PUB === "null" ? "" : (process.env.LIQUIDITY_PROVIDER_PUB || "76ed45f00cea7bac59d8d0b7d204848f5319d7b96c140ffb6fcbaaab0a13d44e")
const liquidityProviderPub = chooseEnv("LIQUIDITY_PROVIDER_PUB", dbEnv, "76ed45f00cea7bac59d8d0b7d204848f5319d7b96c140ffb6fcbaaab0a13d44e", addToDb)
const disableLiquidityProvider = chooseEnvBool("DISABLE_LIQUIDITY_PROVIDER", dbEnv, false, addToDb) || liquidityProviderPub === "null"
return { liquidityProviderPub, useOnlyLiquidityProvider: false, disableLiquidityProvider }
}
export const LoadSecondLndSettingsFromEnv = (): LndNodeSettings => {
return {
...settings,
storageSettings: GetTestStorageSettings(settings.storageSettings),
lndSettings: {
...settings.lndSettings,
otherNode: {
lndAddr: EnvMustBeNonEmptyString("LND_OTHER_ADDR"),
lndCertPath: EnvMustBeNonEmptyString("LND_OTHER_CERT_PATH"),
lndMacaroonPath: EnvMustBeNonEmptyString("LND_OTHER_MACAROON_PATH")
},
thirdNode: {
}
}
export const LoadThirdLndSettingsFromEnv = (): LndNodeSettings => {
return {
lndAddr: EnvMustBeNonEmptyString("LND_THIRD_ADDR"),
lndCertPath: EnvMustBeNonEmptyString("LND_THIRD_CERT_PATH"),
lndMacaroonPath: EnvMustBeNonEmptyString("LND_THIRD_MACAROON_PATH")
},
fourthNode: {
}
}
export const LoadFourthLndSettingsFromEnv = (): LndNodeSettings => {
return {
lndAddr: EnvMustBeNonEmptyString("LND_FOURTH_ADDR"),
lndCertPath: EnvMustBeNonEmptyString("LND_FOURTH_CERT_PATH"),
lndMacaroonPath: EnvMustBeNonEmptyString("LND_FOURTH_MACAROON_PATH")
},
},
liquiditySettings: {
...settings.liquiditySettings,
liquidityProviderPub: "",
},
skipSanityCheck: true,
bitcoinCoreSettings: {
}
}
export const LoadBitcoinCoreSettingsFromEnv = (): BitcoinCoreSettings => {
return {
port: EnvMustBeInteger("BITCOIN_CORE_PORT"),
user: EnvMustBeNonEmptyString("BITCOIN_CORE_USER"),
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
}

View file

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

View file

@ -4,23 +4,23 @@ import { GrpcTransport } from "@protobuf-ts/grpc-transport";
import { credentials, Metadata } from '@grpc/grpc-js'
import { 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<void> => {
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 | null = null
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) {

View file

@ -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<void>
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) {

View file

@ -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'

View file

@ -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<string, AppInfo> = {}
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,78 +156,60 @@ 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<void>( async (res) => {
const relay = await this.GetRelay()
if (!relay) {
return new Promise<void>(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.onclose = null
relay.close()
this.sub?.close()
if (this.relay) {
this.relay.onclose = null
this.relay.close()
this.relay = null
}
this.sub = null
res()
})
})
}
async GetRelay(): Promise<Relay|null> {
async GetRelay(): Promise<Relay | null> {
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")
}
} catch (err:any) {
log("failed to connect to relay, will try again in 2 seconds", err.message || err)
setTimeout(() => {
this.Connect()
}, 2000)
return
}
log("connected, subbing...")
relay.onclose = (() => {
log("relay disconnected, will try to reconnect in 2 seconds")
relay.close()
setTimeout(() => {
this.Connect()
}, 2000)
})
this.Subscribe(relay)
} */
Subscribe(relay: Relay) {
const appIds = Object.keys(this.apps)
this.log("🔍 [NOSTR SUBSCRIPTION] Setting up subscription", {

View file

@ -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<void>((resolve) => {

View file

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

View file

@ -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)

View file

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

View file

@ -12,16 +12,58 @@ import DebitStorage from "./debitStorage.js"
import OfferStorage from "./offerStorage.js"
import { 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()
}

View file

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

View file

@ -26,12 +26,14 @@ import { UserReceivingInvoiceIdx1753109184611 } from './1753109184611-user_recei
import { UserAccess1759426050669 } from './1759426050669-user_access.js'
import { 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]
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<boolean> => {

View file

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

View file

@ -1,10 +1,7 @@
import fs from 'fs'
import path from 'path';
import { config as loadEnvFile } from 'dotenv'
import { getLogger } from "../helpers/logger.js"
import 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<WizardTypes.ServiceStateResponse> => {
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 || ''
}
@ -99,7 +95,7 @@ export class Wizard {
WizardState = async (): Promise<WizardTypes.StateResponse> => {
return {
config_sent: this.pendingConfig !== null,
config_sent: false,
admin_linked: this.adminManager.GetAdminNpub() !== "",
}
}
@ -148,7 +144,7 @@ export class Wizard {
}
Configure = async (): Promise<boolean> => {
if (this.IsInitialized() || this.pendingConfig !== null) {
if (this.IsInitialized()) {
return false
}
return new Promise((res) => {
@ -165,78 +161,43 @@ export class Wizard {
const pendingConfig = { sourceName: req.source_name, relayUrl: req.relay_url, automateLiquidity: req.automate_liquidity, pushBackupsToNostr: req.push_backups_to_nostr }
// 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 })
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)
}
} catch (e) {
this.log(`Error updating app info: ${(e as Error).message}`)
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[] = []
updateDefaultApp = async (currentName: string, avatarUrl?: string): Promise<void> => {
const newName = this.settings.getSettings().serviceSettings.defaultAppName
try {
envFileContent = fs.readFileSync('.env', 'utf-8').split('\n')
} catch (err: any) {
if (err.code !== 'ENOENT') {
throw err
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}`)
}
const toMerge: string[] = []
const sourceNameIndex = envFileContent.findIndex(line => line.startsWith('DEFAULT_APP_NAME'))
if (sourceNameIndex === -1) {
toMerge.push(`DEFAULT_APP_NAME=${pendingConfig.sourceName}`)
} else {
envFileContent[sourceNameIndex] = `DEFAULT_APP_NAME=${pendingConfig.sourceName}`
}
const relayUrlIndex = envFileContent.findIndex(line => line.startsWith('RELAY_URL'))
if (relayUrlIndex === -1) {
toMerge.push(`RELAY_URL=${pendingConfig.relayUrl}`)
} else {
envFileContent[relayUrlIndex] = `RELAY_URL=${pendingConfig.relayUrl}`
}
const automateLiquidityIndex = envFileContent.findIndex(line => line.startsWith('LIQUIDITY_PROVIDER_PUB'))
if (pendingConfig.automateLiquidity) {
if (automateLiquidityIndex !== -1) {
envFileContent.splice(automateLiquidityIndex, 1)
}
} else {
if (automateLiquidityIndex === -1) {
toMerge.push(`LIQUIDITY_PROVIDER_PUB=null`)
} else {
envFileContent[automateLiquidityIndex] = `LIQUIDITY_PROVIDER_PUB=null`
}
}
const pushBackupsToNostrIndex = envFileContent.findIndex(line => line.startsWith('PUSH_BACKUPS_TO_NOSTR'))
if (pushBackupsToNostrIndex === -1) {
toMerge.push(`PUSH_BACKUPS_TO_NOSTR=${pendingConfig.pushBackupsToNostr ? 'true' : 'false'}`)
} else {
envFileContent[pushBackupsToNostrIndex] = `PUSH_BACKUPS_TO_NOSTR=${pendingConfig.pushBackupsToNostr ? 'true' : 'false'}`
}
const merged = [...envFileContent, ...toMerge].join('\n')
fs.writeFileSync('.env', merged)
loadEnvFile()
}
}

View file

@ -1,16 +1,16 @@
// @ts-ignore
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,
})

View file

@ -1,21 +1,31 @@
import { LoadTestSettingsFromEnv } from "../services/main/settings.js"
import {
LiquiditySettings, LoadBitcoinCoreSettingsFromEnv, LoadLndNodeSettingsFromEnv,
LoadLndSettingsFromEnv, LoadSecondLndSettingsFromEnv
} from "../services/main/settings.js"
import { GetTestStorageSettings } from "../services/storage/index.js"
import { BitcoinCoreWrapper } from "./bitcoinCore.js"
import 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<void>
}
export const setupNetwork = async (): Promise<ChainTools> => {
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 () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { })
const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, new LiquidityProvider("", setupUtils, async () => { }, 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 () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { })
const bob = new LND(() => ({ lndSettings, lndNodeSettings: secondLndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { })
await tryUntil<void>(async i => {
const peers = await alice.ListPeers()
if (peers.peers.length > 0) {

View file

@ -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")
}

View file

@ -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<StorageTestBase> => {
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<TestBase> => {
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<Te
const user1 = { userId: u1.info.userId, appUserIdentifier: u1.identifier, appId: app.appId }
const user2 = { userId: u2.info.userId, appUserIdentifier: u2.identifier, appId: app.appId }
const extermnalUtils = new Utils({ dataDir: settings.storageSettings.dataDir, allowResetMetricsStorages: settings.allowResetMetricsStorages })
const extermnalUtils = new Utils({ dataDir: storageSettings.dataDir, allowResetMetricsStorages: storageSettings.allowResetMetricsStorages })
/* const externalAccessToMainLnd = new LND(settings.lndSettings, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { })
await externalAccessToMainLnd.Warmup() */
const otherLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }
const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider("", extermnalUtils, async () => { }, 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 () => { }), 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 () => { }), async () => { }, extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { })
const thirdLndNodeSettings = LoadThirdLndSettingsFromEnv()
const thirdLndSetting = () => ({ lndSettings, lndNodeSettings: thirdLndNodeSettings })
const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider(() => liquiditySettings, extermnalUtils, async () => { }, async () => { }), async () => { }, extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { })
await externalAccessToThirdLnd.Warmup()