This commit is contained in:
hatim boufnichel 2024-06-14 20:14:41 +02:00
parent c49627a6e8
commit 0db4c3f92f
17 changed files with 170 additions and 83 deletions

View file

@ -1,10 +1,10 @@
import { DataSource } from "typeorm" import { DataSource } from "typeorm"
import { ChannelRouting } from "./build/src/services/storage/entity/ChannelRouting.js" import { LspOrder } from "./build/src/services/storage/entity/LspOrder.js"
export default new DataSource({ export default new DataSource({
type: "sqlite", type: "sqlite",
database: "metrics.sqlite", database: "db.sqlite",
entities: [ChannelRouting], entities: [LspOrder],
}); });

View file

@ -1,5 +1,4 @@
import { EnvMustBeNonEmptyString, EnvMustBeInteger, EnvCanBeBoolean, EnvCanBeInteger } from '../helpers/envParser.js' import { EnvMustBeNonEmptyString, EnvMustBeInteger, EnvCanBeBoolean, EnvCanBeInteger } from '../helpers/envParser.js'
import { LoadLiquiditySettingsFromEnv } from './liquidityManager.js'
import { LndSettings } from './settings.js' import { LndSettings } from './settings.js'
export const LoadLndSettingsFromEnv = (): LndSettings => { export const LoadLndSettingsFromEnv = (): LndSettings => {
const lndAddr = process.env.LND_ADDRESS || "127.0.0.1:10009" const lndAddr = process.env.LND_ADDRESS || "127.0.0.1:10009"
@ -8,6 +7,5 @@ export const LoadLndSettingsFromEnv = (): LndSettings => {
const feeRateLimit = EnvCanBeInteger("OUTBOUND_MAX_FEE_BPS", 60) / 10000 const feeRateLimit = EnvCanBeInteger("OUTBOUND_MAX_FEE_BPS", 60) / 10000
const feeFixedLimit = EnvCanBeInteger("OUTBOUND_MAX_FEE_EXTRA_SATS", 100) const feeFixedLimit = EnvCanBeInteger("OUTBOUND_MAX_FEE_EXTRA_SATS", 100)
const mockLnd = EnvCanBeBoolean("MOCK_LND") const mockLnd = EnvCanBeBoolean("MOCK_LND")
const liquiditySettings = LoadLiquiditySettingsFromEnv() return { mainNode: { lndAddr, lndCertPath, lndMacaroonPath }, feeRateLimit, feeFixedLimit, mockLnd }
return { mainNode: { lndAddr, lndCertPath, lndMacaroonPath }, feeRateLimit, feeFixedLimit, mockLnd, liquiditySettings }
} }

View file

@ -36,7 +36,8 @@ export default class {
log = getLogger({ component: 'lndManager' }) log = getLogger({ component: 'lndManager' })
outgoingOpsLocked = false outgoingOpsLocked = false
liquidProvider: LiquidityProvider liquidProvider: LiquidityProvider
constructor(settings: LndSettings, liquidProvider: LiquidityProvider, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb) { useOnlyLiquidityProvider = false
constructor(settings: LndSettings, provider: { liquidProvider: LiquidityProvider, useOnly?: boolean }, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb) {
this.settings = settings this.settings = settings
this.addressPaidCb = addressPaidCb this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb this.invoicePaidCb = invoicePaidCb
@ -62,7 +63,8 @@ export default class {
this.invoices = new InvoicesClient(transport) this.invoices = new InvoicesClient(transport)
this.router = new RouterClient(transport) this.router = new RouterClient(transport)
this.chainNotifier = new ChainNotifierClient(transport) this.chainNotifier = new ChainNotifierClient(transport)
this.liquidProvider = liquidProvider this.liquidProvider = provider.liquidProvider
this.useOnlyLiquidityProvider = !!provider.useOnly
} }
LockOutgoingOperations(): void { LockOutgoingOperations(): void {
@ -81,7 +83,7 @@ export default class {
} }
async ShouldUseLiquidityProvider(req: LiquidityRequest): Promise<boolean> { async ShouldUseLiquidityProvider(req: LiquidityRequest): Promise<boolean> {
if (this.settings.liquiditySettings.useOnlyLiquidityProvider) { if (this.useOnlyLiquidityProvider) {
return true return true
} }
if (!this.liquidProvider.CanProviderHandle(req)) { if (!this.liquidProvider.CanProviderHandle(req)) {

View file

@ -42,6 +42,13 @@ type FlashsatsOrder = {
"token": string "token": string
} }
type OrderResponse = {
orderId: string
invoice: string
totalSats: number
fees: number
}
class LSP { class LSP {
settings: LSPSettings settings: LSPSettings
liquidityProvider: LiquidityProvider liquidityProvider: LiquidityProvider
@ -71,7 +78,7 @@ class LSP {
} }
const userState = await this.liquidityProvider.CheckUserState() const userState = await this.liquidityProvider.CheckUserState()
if (!userState || userState.max_withdrawable < this.settings.channelThreshold) { if (!userState || userState.max_withdrawable < this.settings.channelThreshold) {
this.log("user balance too low to trigger channel request") this.log("balance of", userState?.max_withdrawable || 0, "is lower than channel threshold of", this.settings.channelThreshold)
return { shouldOpen: false } return { shouldOpen: false }
} }
return { shouldOpen: true, maxSpendable: userState.max_withdrawable } return { shouldOpen: true, maxSpendable: userState.max_withdrawable }
@ -91,50 +98,50 @@ export class FlashsatsLSP extends LSP {
super("FlashsatsLSP", settings, lnd, liquidityProvider) super("FlashsatsLSP", settings, lnd, liquidityProvider)
} }
openChannelIfReady = async (): Promise<boolean> => { openChannelIfReady = async (): Promise<OrderResponse | null> => {
const shouldOpen = await this.shouldOpenChannel() const shouldOpen = await this.shouldOpenChannel()
if (!shouldOpen.shouldOpen) { if (!shouldOpen.shouldOpen) {
return false return null
} }
if (!this.settings.flashsatsServiceUrl) { if (!this.settings.flashsatsServiceUrl) {
this.log("no flashsats service url provided") this.log("no flashsats service url provided")
return false return null
} }
const serviceInfo = await this.getInfo() const serviceInfo = await this.getInfo()
if (+serviceInfo.options.min_initial_client_balance_sat > shouldOpen.maxSpendable) { if (+serviceInfo.options.min_initial_client_balance_sat > shouldOpen.maxSpendable) {
this.log("user balance too low for service minimum") this.log("balance of", shouldOpen.maxSpendable, "is lower than service minimum of", serviceInfo.options.min_initial_client_balance_sat)
return false return null
} }
const lndInfo = await this.lnd.GetInfo() const lndInfo = await this.lnd.GetInfo()
const myUri = lndInfo.uris.length > 0 ? lndInfo.uris[0] : "" const myUri = lndInfo.uris.length > 0 ? lndInfo.uris[0] : ""
if (!myUri) { if (!myUri) {
this.log("no uri found for this node,uri is required to use flashsats") this.log("no uri found for this node,uri is required to use flashsats")
return false return null
} }
const lspBalance = (this.settings.channelThreshold * 2).toString() const lspBalance = (this.settings.channelThreshold * 2).toString()
const chanExpiryBlocks = serviceInfo.options.max_channel_expiry_blocks const chanExpiryBlocks = serviceInfo.options.max_channel_expiry_blocks
const order = await this.createOrder({ nodeUri: myUri, lspBalance, clientBalance: "0", chanExpiryBlocks }) const order = await this.createOrder({ nodeUri: myUri, lspBalance, clientBalance: "0", chanExpiryBlocks })
if (order.payment.state !== 'EXPECT_PAYMENT') { if (order.payment.state !== 'EXPECT_PAYMENT') {
this.log("order not in expect payment state") this.log("order not in expect payment state")
return false return null
} }
const decoded = await this.lnd.DecodeInvoice(order.payment.bolt11_invoice) const decoded = await this.lnd.DecodeInvoice(order.payment.bolt11_invoice)
if (decoded.numSatoshis !== +order.payment.order_total_sat) { if (decoded.numSatoshis !== +order.payment.order_total_sat) {
this.log("invoice amount does not match order total") this.log("invoice of amount", decoded.numSatoshis, "does not match order total of", order.payment.order_total_sat)
return false return null
} }
if (decoded.numSatoshis > shouldOpen.maxSpendable) { if (decoded.numSatoshis > shouldOpen.maxSpendable) {
this.log("invoice amount exceeds user balance") this.log("invoice of amount", decoded.numSatoshis, "exceeds user balance of", shouldOpen.maxSpendable)
return false return null
} }
const relativeFee = +order.payment.fee_total_sat / this.settings.channelThreshold const relativeFee = +order.payment.fee_total_sat / this.settings.channelThreshold
if (relativeFee > this.settings.maxRelativeFee) { if (relativeFee > this.settings.maxRelativeFee) {
this.log("invoice fee exceeds max fee percent") this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee)
return false return null
} }
await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice) const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice)
this.log("paid invoice to open channel") this.log("paid", res.amount_paid, "to open channel")
return true return { orderId: order.order_id, invoice: order.payment.bolt11_invoice, totalSats: +order.payment.order_total_sat, fees: +order.payment.fee_total_sat }
} }
getInfo = async () => { getInfo = async () => {
@ -167,20 +174,19 @@ export class OlympusLSP extends LSP {
super("OlympusLSP", settings, lnd, liquidityProvider) super("OlympusLSP", settings, lnd, liquidityProvider)
} }
openChannelIfReady = async (): Promise<boolean> => { openChannelIfReady = async (): Promise<OrderResponse | null> => {
this.log("checking if channel should be opened")
const shouldOpen = await this.shouldOpenChannel() const shouldOpen = await this.shouldOpenChannel()
if (!shouldOpen.shouldOpen) { if (!shouldOpen.shouldOpen) {
return false return null
} }
if (!this.settings.olympusServiceUrl) { if (!this.settings.olympusServiceUrl) {
this.log("no olympus service url provided") this.log("no olympus service url provided")
return false return null
} }
const serviceInfo = await this.getInfo() const serviceInfo = await this.getInfo()
if (+serviceInfo.options.min_initial_client_balance_sat > shouldOpen.maxSpendable) { if (+serviceInfo.options.min_initial_client_balance_sat > shouldOpen.maxSpendable) {
this.log("user balance too low for service minimum") this.log("balance of", shouldOpen.maxSpendable, "is lower than service minimum of", serviceInfo.options.min_initial_client_balance_sat)
return false return null
} }
const [servicePub, host] = serviceInfo.uris[0].split('@') const [servicePub, host] = serviceInfo.uris[0].split('@')
await this.addPeer(servicePub, host) await this.addPeer(servicePub, host)
@ -192,25 +198,25 @@ export class OlympusLSP extends LSP {
const order = await this.createOrder({ pubKey: myPub, refundAddr: refundAddr.address, lspBalance, clientBalance: "0", chanExpiryBlocks }) const order = await this.createOrder({ pubKey: myPub, refundAddr: refundAddr.address, lspBalance, clientBalance: "0", chanExpiryBlocks })
if (order.payment.state !== 'EXPECT_PAYMENT') { if (order.payment.state !== 'EXPECT_PAYMENT') {
this.log("order not in expect payment state") this.log("order not in expect payment state")
return false return null
} }
const decoded = await this.lnd.DecodeInvoice(order.payment.bolt11_invoice) const decoded = await this.lnd.DecodeInvoice(order.payment.bolt11_invoice)
if (decoded.numSatoshis !== +order.payment.order_total_sat) { if (decoded.numSatoshis !== +order.payment.order_total_sat) {
this.log("invoice amount does not match order total") this.log("invoice of amount", decoded.numSatoshis, "does not match order total of", order.payment.order_total_sat)
return false return null
} }
if (decoded.numSatoshis > shouldOpen.maxSpendable) { if (decoded.numSatoshis > shouldOpen.maxSpendable) {
this.log("invoice amount exceeds user balance") this.log("invoice of amount", decoded.numSatoshis, "exceeds user balance of", shouldOpen.maxSpendable)
return false return null
} }
const relativeFee = +order.payment.fee_total_sat / this.settings.channelThreshold const relativeFee = +order.payment.fee_total_sat / this.settings.channelThreshold
if (relativeFee > this.settings.maxRelativeFee) { if (relativeFee > this.settings.maxRelativeFee) {
this.log("invoice fee exceeds max fee percent") this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee)
return false return null
} }
await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice) const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice)
this.log("paid invoice to open channel") this.log("paid", res.amount_paid, "to open channel")
return true return { orderId: order.order_id, invoice: order.payment.bolt11_invoice, totalSats: +order.payment.order_total_sat, fees: +order.payment.fee_total_sat }
} }
getInfo = async () => { getInfo = async () => {
@ -267,15 +273,15 @@ export class VoltageLSP extends LSP {
return json return json
} }
openChannelIfReady = async (): Promise<boolean> => { openChannelIfReady = async (): Promise<OrderResponse | null> => {
const shouldOpen = await this.shouldOpenChannel() const shouldOpen = await this.shouldOpenChannel()
if (!shouldOpen.shouldOpen) { if (!shouldOpen.shouldOpen) {
return false return null
} }
if (!this.settings.voltageServiceUrl) { if (!this.settings.voltageServiceUrl) {
this.log("no voltage service url provided") this.log("no voltage service url provided")
return false return null
} }
const lndInfo = await this.lnd.GetInfo() const lndInfo = await this.lnd.GetInfo()
@ -286,15 +292,15 @@ export class VoltageLSP extends LSP {
const relativeFee = feeSats / this.settings.channelThreshold const relativeFee = feeSats / this.settings.channelThreshold
if (relativeFee > this.settings.maxRelativeFee) { if (relativeFee > this.settings.maxRelativeFee) {
this.log("fee percent exceeds max fee percent") this.log("relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee)
return false return null
} }
const info = await this.getInfo() const info = await this.getInfo()
const ipv4 = info.connection_methods.find(c => c.type === 'ipv4') const ipv4 = info.connection_methods.find(c => c.type === 'ipv4')
if (!ipv4) { if (!ipv4) {
this.log("no ipv4 address found") this.log("no ipv4 address found")
return false return null
} }
await this.addPeer(info.pubkey, `${ipv4.address}:${ipv4.port}`) await this.addPeer(info.pubkey, `${ipv4.address}:${ipv4.port}`)
@ -302,13 +308,13 @@ export class VoltageLSP extends LSP {
const res = await this.proposal(invoice.payRequest, fee.id) const res = await this.proposal(invoice.payRequest, fee.id)
const decoded = await this.lnd.DecodeInvoice(res.jit_bolt11) const decoded = await this.lnd.DecodeInvoice(res.jit_bolt11)
if (decoded.numSatoshis !== this.settings.channelThreshold + feeSats) { if (decoded.numSatoshis !== this.settings.channelThreshold + feeSats) {
this.log("invoice amount does not math requested amount") this.log("invoice of amount", decoded.numSatoshis, "does not match expected amount of", this.settings.channelThreshold + feeSats)
return false return null
} }
await this.liquidityProvider.PayInvoice(res.jit_bolt11) const invoiceRes = await this.liquidityProvider.PayInvoice(res.jit_bolt11)
this.log("paid invoice to open channel") this.log("paid", invoiceRes.amount_paid, "to open channel")
return true return { orderId: fee.id, invoice: res.jit_bolt11, totalSats: decoded.numSatoshis, fees: feeSats }
} }
proposal = async (bolt11: string, feeId: string) => { proposal = async (bolt11: string, feeId: string) => {

View file

@ -1,5 +1,4 @@
import { HtlcEvent } from "../../../proto/lnd/router" import { HtlcEvent } from "../../../proto/lnd/router"
import { LiquiditySettings } from "./liquidityManager"
export type NodeSettings = { export type NodeSettings = {
lndAddr: string lndAddr: string
lndCertPath: string lndCertPath: string
@ -13,8 +12,6 @@ export type LndSettings = {
otherNode?: NodeSettings otherNode?: NodeSettings
thirdNode?: NodeSettings thirdNode?: NodeSettings
liquiditySettings: LiquiditySettings
} }
type TxOutput = { type TxOutput = {
hash: string hash: string

View file

@ -16,7 +16,7 @@ import { NostrSend } from '../nostr/handler.js'
import MetricsManager from '../metrics/index.js' import MetricsManager from '../metrics/index.js'
import { LoggedEvent } from '../storage/eventsLog.js' import { LoggedEvent } from '../storage/eventsLog.js'
import { LiquidityProvider } from "../lnd/liquidityProvider.js" import { LiquidityProvider } from "../lnd/liquidityProvider.js"
import { LiquidityManager } from "../lnd/liquidityManager.js" import { LiquidityManager } from "./liquidityManager.js"
type UserOperationsSub = { type UserOperationsSub = {
id: string id: string
@ -43,9 +43,10 @@ export default class {
constructor(settings: MainSettings, storage: Storage) { constructor(settings: MainSettings, storage: Storage) {
this.settings = settings this.settings = settings
this.storage = storage this.storage = storage
this.liquidProvider = new LiquidityProvider(settings.lndSettings.liquiditySettings.liquidityProviderPub, this.invoicePaidCb) this.liquidProvider = new LiquidityProvider(settings.liquiditySettings.liquidityProviderPub, this.invoicePaidCb)
this.lnd = new LND(settings.lndSettings, this.liquidProvider, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb) const provider = { liquidProvider: this.liquidProvider, useOnly: settings.liquiditySettings.useOnlyLiquidityProvider }
this.liquidityManager = new LiquidityManager(this.settings.lndSettings.liquiditySettings, this.liquidProvider, this.lnd) this.lnd = new LND(settings.lndSettings, provider, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb)
this.liquidityManager = new LiquidityManager(this.settings.liquiditySettings, this.liquidProvider, this.lnd)
this.metricsManager = new MetricsManager(this.storage, this.lnd) this.metricsManager = new MetricsManager(this.storage, this.lnd)
this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings, this.addressPaidCb, this.invoicePaidCb) this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings, this.addressPaidCb, this.invoicePaidCb)

View file

@ -1,7 +1,8 @@
import { getLogger } from "../helpers/logger.js" import { getLogger } from "../helpers/logger.js"
import { LiquidityProvider } from "./liquidityProvider.js" import { LiquidityProvider } from "../lnd/liquidityProvider.js"
import LND from "./lnd.js" import LND from "../lnd/lnd.js"
import { FlashsatsLSP, LoadLSPSettingsFromEnv, LSPSettings, OlympusLSP, VoltageLSP } from "./lsp.js" import { FlashsatsLSP, LoadLSPSettingsFromEnv, LSPSettings, OlympusLSP, VoltageLSP } from "../lnd/lsp.js"
import Storage from '../storage/index.js'
export type LiquiditySettings = { export type LiquiditySettings = {
lspSettings: LSPSettings lspSettings: LSPSettings
liquidityProviderPub: string liquidityProviderPub: string
@ -14,6 +15,7 @@ export const LoadLiquiditySettingsFromEnv = (): LiquiditySettings => {
} }
export class LiquidityManager { export class LiquidityManager {
settings: LiquiditySettings settings: LiquiditySettings
storage: Storage
liquidityProvider: LiquidityProvider liquidityProvider: LiquidityProvider
lnd: LND lnd: LND
olympusLSP: OlympusLSP olympusLSP: OlympusLSP
@ -31,19 +33,26 @@ export class LiquidityManager {
} }
beforeInvoiceCreation = async () => { } beforeInvoiceCreation = async () => { }
afterInInvoicePaid = async () => { afterInInvoicePaid = async () => {
const existingOrder = await this.storage.liquidityStorage.GetLatestLspOrder()
if (existingOrder) {
return
}
if (this.channelRequested) { if (this.channelRequested) {
return return
} }
this.log("checking if channel should be requested")
const olympusOk = await this.olympusLSP.openChannelIfReady() const olympusOk = await this.olympusLSP.openChannelIfReady()
if (olympusOk) { if (olympusOk) {
this.log("requested channel from olympus") this.log("requested channel from olympus")
this.channelRequested = true this.channelRequested = true
await this.storage.liquidityStorage.SaveLspOrder({ service_name: 'olympus', invoice: olympusOk.invoice, total_paid: olympusOk.totalSats, order_id: olympusOk.orderId, fees: olympusOk.fees })
return return
} }
const voltageOk = await this.voltageLSP.openChannelIfReady() const voltageOk = await this.voltageLSP.openChannelIfReady()
if (voltageOk) { if (voltageOk) {
this.log("requested channel from voltage") this.log("requested channel from voltage")
this.channelRequested = true this.channelRequested = true
await this.storage.liquidityStorage.SaveLspOrder({ service_name: 'voltage', invoice: voltageOk.invoice, total_paid: voltageOk.totalSats, order_id: voltageOk.orderId, fees: voltageOk.fees })
return return
} }
@ -51,6 +60,7 @@ export class LiquidityManager {
if (flashsatsOk) { if (flashsatsOk) {
this.log("requested channel from flashsats") this.log("requested channel from flashsats")
this.channelRequested = true this.channelRequested = true
await this.storage.liquidityStorage.SaveLspOrder({ service_name: 'flashsats', invoice: flashsatsOk.invoice, total_paid: flashsatsOk.totalSats, order_id: flashsatsOk.orderId, fees: flashsatsOk.fees })
return return
} }
this.log("no channel requested") this.log("no channel requested")

View file

@ -6,10 +6,12 @@ import { EnvCanBeInteger, EnvMustBeInteger, EnvMustBeNonEmptyString } from '../h
import { getLogger } from '../helpers/logger.js' import { getLogger } from '../helpers/logger.js'
import fs from 'fs' import fs from 'fs'
import crypto from 'crypto'; import crypto from 'crypto';
import { LiquiditySettings, LoadLiquiditySettingsFromEnv } from './liquidityManager.js'
export type MainSettings = { export type MainSettings = {
storageSettings: StorageSettings, storageSettings: StorageSettings,
lndSettings: LndSettings, lndSettings: LndSettings,
watchDogSettings: WatchdogSettings, watchDogSettings: WatchdogSettings,
liquiditySettings: LiquiditySettings,
jwtSecret: string jwtSecret: string
incomingTxFee: number incomingTxFee: number
outgoingTxFee: number outgoingTxFee: number
@ -32,11 +34,13 @@ export type BitcoinCoreSettings = {
} }
export type TestSettings = MainSettings & { lndSettings: { otherNode: NodeSettings, thirdNode: NodeSettings, fourthNode: NodeSettings }, bitcoinCoreSettings: BitcoinCoreSettings } export type TestSettings = MainSettings & { lndSettings: { otherNode: NodeSettings, thirdNode: NodeSettings, fourthNode: NodeSettings }, bitcoinCoreSettings: BitcoinCoreSettings }
export const LoadMainSettingsFromEnv = (): MainSettings => { export const LoadMainSettingsFromEnv = (): MainSettings => {
const storageSettings = LoadStorageSettingsFromEnv()
return { return {
watchDogSettings: LoadWatchdogSettingsFromEnv(), watchDogSettings: LoadWatchdogSettingsFromEnv(),
lndSettings: LoadLndSettingsFromEnv(), lndSettings: LoadLndSettingsFromEnv(),
storageSettings: LoadStorageSettingsFromEnv(), storageSettings: storageSettings,
jwtSecret: loadJwtSecret(), liquiditySettings: LoadLiquiditySettingsFromEnv(),
jwtSecret: loadJwtSecret(storageSettings.dataDir),
incomingTxFee: EnvCanBeInteger("INCOMING_CHAIN_FEE_ROOT_BPS", 0) / 10000, incomingTxFee: EnvCanBeInteger("INCOMING_CHAIN_FEE_ROOT_BPS", 0) / 10000,
outgoingTxFee: EnvCanBeInteger("OUTGOING_CHAIN_FEE_ROOT_BPS", 60) / 10000, outgoingTxFee: EnvCanBeInteger("OUTGOING_CHAIN_FEE_ROOT_BPS", 60) / 10000,
incomingAppInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_ROOT_BPS", 0) / 10000, incomingAppInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_ROOT_BPS", 0) / 10000,
@ -58,7 +62,7 @@ export const LoadTestSettingsFromEnv = (): TestSettings => {
const settings = LoadMainSettingsFromEnv() const settings = LoadMainSettingsFromEnv()
return { return {
...settings, ...settings,
storageSettings: { dbSettings: { ...settings.storageSettings.dbSettings, databaseFile: ":memory:", metricsDatabaseFile: ":memory:" }, eventLogPath }, storageSettings: { dbSettings: { ...settings.storageSettings.dbSettings, databaseFile: ":memory:", metricsDatabaseFile: ":memory:" }, eventLogPath, dataDir: "data" },
lndSettings: { lndSettings: {
...settings.lndSettings, ...settings.lndSettings,
otherNode: { otherNode: {
@ -76,10 +80,10 @@ export const LoadTestSettingsFromEnv = (): TestSettings => {
lndCertPath: EnvMustBeNonEmptyString("LND_FOURTH_CERT_PATH"), lndCertPath: EnvMustBeNonEmptyString("LND_FOURTH_CERT_PATH"),
lndMacaroonPath: EnvMustBeNonEmptyString("LND_FOURTH_MACAROON_PATH") lndMacaroonPath: EnvMustBeNonEmptyString("LND_FOURTH_MACAROON_PATH")
}, },
liquiditySettings: { },
...settings.lndSettings.liquiditySettings, liquiditySettings: {
liquidityProviderPub: "", ...settings.liquiditySettings,
} liquidityProviderPub: "",
}, },
skipSanityCheck: true, skipSanityCheck: true,
bitcoinCoreSettings: { bitcoinCoreSettings: {
@ -90,20 +94,21 @@ export const LoadTestSettingsFromEnv = (): TestSettings => {
} }
} }
export const loadJwtSecret = (): string => { export const loadJwtSecret = (dataDir: string): string => {
const secret = process.env["JWT_SECRET"] const secret = process.env["JWT_SECRET"]
const log = getLogger({}) const log = getLogger({})
if (secret) { if (secret) {
return secret return secret
} }
log("JWT_SECRET not set in env, checking .jwt_secret file") log("JWT_SECRET not set in env, checking .jwt_secret file")
const secretPath = dataDir !== "" ? `${dataDir}/.jwt_secret` : ".jwt_secret"
try { try {
const fileContent = fs.readFileSync(".jwt_secret", "utf-8") const fileContent = fs.readFileSync(secretPath, "utf-8")
return fileContent.trim() return fileContent.trim()
} catch (e) { } catch (e) {
log(".jwt_secret file not found, generating random secret") log(".jwt_secret file not found, generating random secret")
const secret = crypto.randomBytes(32).toString('hex') const secret = crypto.randomBytes(32).toString('hex')
fs.writeFileSync(".jwt_secret", secret) fs.writeFileSync(secretPath, secret)
return secret return secret
} }
} }

View file

@ -17,6 +17,7 @@ import { BalanceEvent } from "./entity/BalanceEvent.js"
import { ChannelBalanceEvent } from "./entity/ChannelsBalanceEvent.js" import { ChannelBalanceEvent } from "./entity/ChannelsBalanceEvent.js"
import { getLogger } from "../helpers/logger.js" import { getLogger } from "../helpers/logger.js"
import { ChannelRouting } from "./entity/ChannelRouting.js" import { ChannelRouting } from "./entity/ChannelRouting.js"
import { LspOrder } from "./entity/LspOrder.js"
export type DbSettings = { export type DbSettings = {
@ -56,7 +57,7 @@ export default async (settings: DbSettings, migrations: Function[]): Promise<{ s
database: settings.databaseFile, database: settings.databaseFile,
// logging: true, // logging: true,
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment], UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder],
//synchronize: true, //synchronize: true,
migrations migrations
}).initialize() }).initialize()

View file

@ -0,0 +1,28 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from "typeorm"
@Entity()
export class LspOrder {
@PrimaryGeneratedColumn()
serial_id: number
@Column()
service_name: string
@Column()
invoice: string
@Column()
order_id: string
@Column()
total_paid: number
@Column()
fees: number
@CreateDateColumn()
created_at: Date
@UpdateDateColumn()
updated_at: Date
}

View file

@ -7,12 +7,14 @@ import PaymentStorage from "./paymentStorage.js";
import MetricsStorage from "./metricsStorage.js"; import MetricsStorage from "./metricsStorage.js";
import TransactionsQueue, { TX } from "./transactionsQueue.js"; import TransactionsQueue, { TX } from "./transactionsQueue.js";
import EventsLogManager from "./eventsLog.js"; import EventsLogManager from "./eventsLog.js";
import { LiquidityStorage } from "./liquidityStorage.js";
export type StorageSettings = { export type StorageSettings = {
dbSettings: DbSettings dbSettings: DbSettings
eventLogPath: string eventLogPath: string
dataDir: string
} }
export const LoadStorageSettingsFromEnv = (): StorageSettings => { export const LoadStorageSettingsFromEnv = (): StorageSettings => {
return { dbSettings: LoadDbSettingsFromEnv(), eventLogPath: "logs/eventLogV2.csv" } return { dbSettings: LoadDbSettingsFromEnv(), eventLogPath: "logs/eventLogV2.csv", dataDir: process.env.DATA_DIR || "" }
} }
export default class { export default class {
DB: DataSource | EntityManager DB: DataSource | EntityManager
@ -23,6 +25,7 @@ export default class {
userStorage: UserStorage userStorage: UserStorage
paymentStorage: PaymentStorage paymentStorage: PaymentStorage
metricsStorage: MetricsStorage metricsStorage: MetricsStorage
liquidityStorage: LiquidityStorage
eventsLog: EventsLogManager eventsLog: EventsLogManager
constructor(settings: StorageSettings) { constructor(settings: StorageSettings) {
this.settings = settings this.settings = settings
@ -37,6 +40,7 @@ export default class {
this.applicationStorage = new ApplicationStorage(this.DB, this.userStorage, this.txQueue) this.applicationStorage = new ApplicationStorage(this.DB, this.userStorage, this.txQueue)
this.paymentStorage = new PaymentStorage(this.DB, this.userStorage, this.txQueue) this.paymentStorage = new PaymentStorage(this.DB, this.userStorage, this.txQueue)
this.metricsStorage = new MetricsStorage(this.settings) this.metricsStorage = new MetricsStorage(this.settings)
this.liquidityStorage = new LiquidityStorage(this.DB, this.txQueue)
const executedMetricsMigrations = await this.metricsStorage.Connect(metricsMigrations) const executedMetricsMigrations = await this.metricsStorage.Connect(metricsMigrations)
return { executedMigrations, executedMetricsMigrations }; return { executedMigrations, executedMetricsMigrations };
} }

View file

@ -0,0 +1,20 @@
import { DataSource, EntityManager } from "typeorm"
import { LspOrder } from "./entity/LspOrder.js";
import TransactionsQueue, { TX } from "./transactionsQueue.js";
export class LiquidityStorage {
DB: DataSource | EntityManager
txQueue: TransactionsQueue
constructor(DB: DataSource | EntityManager, txQueue: TransactionsQueue) {
this.DB = DB
this.txQueue = txQueue
}
GetLatestLspOrder() {
return this.DB.getRepository(LspOrder).findOne({ order: { serial_id: "DESC" } })
}
SaveLspOrder(order: Partial<LspOrder>) {
const entry = this.DB.getRepository(LspOrder).create(order)
return this.txQueue.PushToQueue<LspOrder>({ exec: async db => db.getRepository(LspOrder).save(entry), dbTx: false })
}
}

View file

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class LspOrder1718387847693 implements MigrationInterface {
name = 'LspOrder1718387847693'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "lsp_order" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "service_name" varchar NOT NULL, "invoice" varchar NOT NULL, "order_id" varchar NOT NULL, "total_paid" integer NOT NULL, "fees" integer NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')))`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "lsp_order"`);
}
}

View file

@ -4,7 +4,8 @@ import Storage, { StorageSettings } from '../index.js'
import { Initial1703170309875 } from './1703170309875-initial.js' import { Initial1703170309875 } from './1703170309875-initial.js'
import { LndMetrics1703170330183 } from './1703170330183-lnd_metrics.js' import { LndMetrics1703170330183 } from './1703170330183-lnd_metrics.js'
import { ChannelRouting1709316653538 } from './1709316653538-channel_routing.js' import { ChannelRouting1709316653538 } from './1709316653538-channel_routing.js'
const allMigrations = [Initial1703170309875] import { LspOrder1718387847693 } from './1718387847693-lsp_order.js'
const allMigrations = [Initial1703170309875, LspOrder1718387847693]
const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538] const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538]
export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => { export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => {
if (arg === 'fake_initial_migration') { if (arg === 'fake_initial_migration') {

View file

@ -8,8 +8,8 @@ export const setupNetwork = async () => {
const core = new BitcoinCoreWrapper(settings) const core = new BitcoinCoreWrapper(settings)
await core.InitAddress() await core.InitAddress()
await core.Mine(1) await core.Mine(1)
const alice = new LND(settings.lndSettings, new LiquidityProvider("", () => { }), () => { }, () => { }, () => { }, () => { }) const alice = new LND(settings.lndSettings, { liquidProvider: new LiquidityProvider("", () => { }) }, () => { }, () => { }, () => { }, () => { })
const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, new LiquidityProvider("", () => { }), () => { }, () => { }, () => { }, () => { }) const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, { liquidProvider: new LiquidityProvider("", () => { }) }, () => { }, () => { }, () => { }, () => { })
await tryUntil<void>(async i => { await tryUntil<void>(async i => {
const peers = await alice.ListPeers() const peers = await alice.ListPeers()
if (peers.peers.length > 0) { if (peers.peers.length > 0) {

View file

@ -7,8 +7,8 @@ import * as Types from '../../proto/autogenerated/ts/types.js'
export const initBootstrappedInstance = async (T: TestBase) => { export const initBootstrappedInstance = async (T: TestBase) => {
const settings = LoadTestSettingsFromEnv() const settings = LoadTestSettingsFromEnv()
settings.lndSettings.liquiditySettings.useOnlyLiquidityProvider = true settings.liquiditySettings.useOnlyLiquidityProvider = true
settings.lndSettings.liquiditySettings.liquidityProviderPub = T.app.publicKey settings.liquiditySettings.liquidityProviderPub = T.app.publicKey
settings.lndSettings.mainNode = settings.lndSettings.thirdNode settings.lndSettings.mainNode = settings.lndSettings.thirdNode
const initialized = await initMainHandler(getLogger({ component: "bootstrapped" }), settings) const initialized = await initMainHandler(getLogger({ component: "bootstrapped" }), settings)
if (!initialized) { if (!initialized) {

View file

@ -46,15 +46,15 @@ export const SetupTest = async (d: Describe): Promise<TestBase> => {
const user2 = { userId: u2.info.userId, appUserIdentifier: u2.identifier, appId: app.appId } const user2 = { userId: u2.info.userId, appUserIdentifier: u2.identifier, appId: app.appId }
const externalAccessToMainLnd = new LND(settings.lndSettings, new LiquidityProvider("", () => { }), console.log, console.log, () => { }, () => { }) const externalAccessToMainLnd = new LND(settings.lndSettings, { liquidProvider: new LiquidityProvider("", () => { }) }, console.log, console.log, () => { }, () => { })
await externalAccessToMainLnd.Warmup() await externalAccessToMainLnd.Warmup()
const otherLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.otherNode } const otherLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }
const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider("", () => { }), console.log, console.log, () => { }, () => { }) const externalAccessToOtherLnd = new LND(otherLndSetting, { liquidProvider: new LiquidityProvider("", () => { }) }, console.log, console.log, () => { }, () => { })
await externalAccessToOtherLnd.Warmup() await externalAccessToOtherLnd.Warmup()
const thirdLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.thirdNode } const thirdLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.thirdNode }
const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider("", () => { }), console.log, console.log, () => { }, () => { }) const externalAccessToThirdLnd = new LND(thirdLndSetting, { liquidProvider: new LiquidityProvider("", () => { }) }, console.log, console.log, () => { }, () => { })
await externalAccessToThirdLnd.Warmup() await externalAccessToThirdLnd.Warmup()