Merge branch 'master' into umbrel-works

This commit is contained in:
Justin (shocknet) 2024-06-30 14:52:54 -04:00
commit 8d5a9069ae
24 changed files with 12438 additions and 11898 deletions

View file

@ -1,89 +1,96 @@
# Example configuration for Lightning.Pub # Example configuration for Lightning.Pub
# Copy this file as .env in the Pub folder and uncomment the desired settings to override defaults # Copy this file as .env in the Pub folder and uncomment the desired settings to override defaults
# Alternatively, these settings can be passed as environment variables at startup # Alternatively, these settings can be passed as environment variables at startup
#LND_CONNECTION #LND_CONNECTION
# Defaults typical for straight Linux # Defaults typical for straight Linux
# Containers, Mac and Windows may need more detailed paths # Containers, Mac and Windows may need more detailed paths
#LND_ADDRESS=127.0.0.1:10009 #LND_ADDRESS=127.0.0.1:10009
#LND_CERT_PATH=~/.lnd/tls.cert #LND_CERT_PATH=~/.lnd/tls.cert
#LND_MACAROON_PATH=~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon #LND_MACAROON_PATH=~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon
LIQUIDITY_PROVIDER_PUB= LIQUIDITY_PROVIDER_PUB=
#DB #DB
#DATABASE_FILE=db.sqlite #DATABASE_FILE=db.sqlite
#METRICS_DATABASE_FILE=metrics.sqlite #METRICS_DATABASE_FILE=metrics.sqlite
#LOGS_DIR=logs #LOGS_DIR=logs
#LOCALHOST #LOCALHOST
#ADMIN_TOKEN= #ADMIN_TOKEN=
#PORT=1776 #PORT=1776
#JWT_SECRET= #JWT_SECRET=
#LIGHTNING #LIGHTNING
# Maximum amount in network fees passed to LND when it pays an external invoice # Maximum amount in network fees passed to LND when it pays an external invoice
# BPS are basis points, 100 BPS = 1% # BPS are basis points, 100 BPS = 1%
#OUTBOUND_MAX_FEE_BPS=60 #OUTBOUND_MAX_FEE_BPS=60
#OUTBOUND_MAX_FEE_EXTRA_SATS=100 #OUTBOUND_MAX_FEE_EXTRA_SATS=100
# If the back-end doesn't have adequate channel capacity, buy one from an LSP # If the back-end doesn't have adequate channel capacity, buy one from an LSP
# Will execute when it costs less than 1% of balance and uses a trusted peer # Will execute when it costs less than 1% of balance and uses a trusted peer
#BOOTSTRAP=1 #BOOTSTRAP=1
#ROOT_FEES #LSP
# Applied to either debits or credits and sent to an admin account OLYMPUS_LSP_URL=https://lsps1.lnolymp.us/api/v1
# BPS are basis points, 100 BPS = 1% VOLTAGE_LSP_URL=https://lsp.voltageapi.com/api/v1
#INCOMING_CHAIN_FEE_ROOT_BPS=0 FLASHSATS_LSP_URL=https://lsp.flashsats.xyz/lsp/channel
#INCOMING_INVOICE_FEE_ROOT_BPS=0 LSP_CHANNEL_THRESHOLD=1000000
# Chain spends are currently unstable and thus disabled, do not use until further notice LSP_MAX_FEE_BPS=100
#OUTGOING_CHAIN_FEE_ROOT_BPS=60
# Outgoing Invoice Fee must be >= Lightning Outbound Max Fee so admins don't incur losses on spends #ROOT_FEES
#OUTGOING_INVOICE_FEE_ROOT_BPS=60 # Applied to either debits or credits and sent to an admin account
# Internal user fees bugged, do not use until further notice # BPS are basis points, 100 BPS = 1%
#TX_FEE_INTERNAL_ROOT_BPS=0 #applied to inter-application txns #INCOMING_CHAIN_FEE_ROOT_BPS=0
#INCOMING_INVOICE_FEE_ROOT_BPS=0
#APP_FEES # Chain spends are currently unstable and thus disabled, do not use until further notice
# An extra fee applied at the app level and sent to the application owner #OUTGOING_CHAIN_FEE_ROOT_BPS=60
#INCOMING_INVOICE_FEE_USER_BPS=0 # Outgoing Invoice Fee must be >= Lightning Outbound Max Fee so admins don't incur losses on spends
#OUTGOING_INVOICE_FEE_USER_BPS=0 #OUTGOING_INVOICE_FEE_ROOT_BPS=60
#TX_FEE_INTERNAL_USER_BPS=0 # Internal user fees bugged, do not use until further notice
#TX_FEE_INTERNAL_ROOT_BPS=0 #applied to inter-application txns
#NOSTR
# Default relay may become rate-limited without a paid subscription #APP_FEES
#NOSTR_RELAYS=wss://strfry.shock.network # An extra fee applied at the app level and sent to the application owner
#INCOMING_INVOICE_FEE_USER_BPS=0
#LNURL #OUTGOING_INVOICE_FEE_USER_BPS=0
# Optional #TX_FEE_INTERNAL_USER_BPS=0
# If undefined, LNURLs (including Lightning Address) will be disabled
# To enable, add a reachable https endpoint for requests (or purchase a subscription) #NOSTR
# You also need an SSL reverse proxy from the domain to this local host # Default relay may become rate-limited without a paid subscription
# Read more at https://docs.shock.network #NOSTR_RELAYS=wss://strfry.shock.network
#SERVICE_URL=https://yourdomainhere.xyz
#LNURL
#SUBSCRIPTION_SERVICES # Optional
# Opt-in to cloud relays for LNURL and Nostr # If undefined, LNURLs (including Lightning Address) will be disabled
# A small monthly fee supports the developers # To enable, add a reachable https endpoint for requests (or purchase a subscription)
# Read more at https://docs.shock.network # You also need an SSL reverse proxy from the domain to this local host
#SUBSCRIBER=1 # Read more at https://docs.shock.network
#SERVICE_URL=https://yourdomainhere.xyz
#DEV_OPTS
#MOCK_LND=false #SUBSCRIPTION_SERVICES
#ALLOW_BALANCE_MIGRATION=false # Opt-in to cloud relays for LNURL and Nostr
#MIGRATE_DB=false # A small monthly fee supports the developers
#LOG_LEVEL=DEBUG # Read more at https://docs.shock.network
#SUBSCRIBER=1
#METRICS
#RECORD_PERFORMANCE=true #DEV_OPTS
#SKIP_SANITY_CHECK=false #MOCK_LND=false
# A read-only token that can be used with dashboard to view reports #ALLOW_BALANCE_MIGRATION=false
#METRICS_TOKEN= #MIGRATE_DB=false
# Disable outbound payments aka honeypot mode #LOG_LEVEL=DEBUG
#DISABLE_EXTERNAL_PAYMENTS=false
#METRICS
#WATCHDOG SECURITY #RECORD_PERFORMANCE=true
# A last line of defense against 0-day drainage attacks #SKIP_SANITY_CHECK=false
# This will monitor LND separately and terminate sends if a balance discrepency is detected # A read-only token that can be used with dashboard to view reports
# This setting defaults to 0 meaning no discrepency will be tolerated #METRICS_TOKEN=
# Increase this values to add a spending buffer for non-Pub services sharing LND # Disable outbound payments aka honeypot mode
# Max difference between users balance and LND balance at Pub startup #DISABLE_EXTERNAL_PAYMENTS=false
#WATCHDOG_MAX_DIFF_SATS=0
#WATCHDOG SECURITY
# A last line of defense against 0-day drainage attacks
# This will monitor LND separately and terminate sends if a balance discrepency is detected
# This setting defaults to 0 meaning no discrepency will be tolerated
# Increase this values to add a spending buffer for non-Pub services sharing LND
# Max difference between users balance and LND balance at Pub startup
#WATCHDOG_MAX_DIFF_SATS=0

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],
}); });

18636
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,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 liquidityProviderPub = process.env.LIQUIDITY_PROVIDER_PUB || "" return { mainNode: { lndAddr, lndCertPath, lndMacaroonPath }, feeRateLimit, feeFixedLimit, mockLnd }
return { mainNode: { lndAddr, lndCertPath, lndMacaroonPath }, feeRateLimit, feeFixedLimit, mockLnd, liquidityProviderPub, useOnlyLiquidityProvider: false }
} }

View file

@ -1,226 +1,252 @@
import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js' import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js'
import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js' import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js'
import * as Types from '../../../proto/autogenerated/ts/types.js' import * as Types from '../../../proto/autogenerated/ts/types.js'
import { decodeNprofile } from '../../custom-nip19.js' import { decodeNprofile } from '../../custom-nip19.js'
import { getLogger } from '../helpers/logger.js' import { getLogger } from '../helpers/logger.js'
import { NostrEvent, NostrSend } from '../nostr/handler.js' import { NostrEvent, NostrSend } from '../nostr/handler.js'
import { relayInit } from '../nostr/tools/relay.js' import { relayInit } from '../nostr/tools/relay.js'
import { InvoicePaidCb } from './settings.js' import { InvoicePaidCb } from './settings.js'
export type LiquidityRequest = { action: 'spend' | 'receive', amount: number } export type LiquidityRequest = { action: 'spend' | 'receive', amount: number }
export type nostrCallback<T> = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void } export type nostrCallback<T> = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void }
export class LiquidityProvider { export class LiquidityProvider {
client: ReturnType<typeof newNostrClient> client: ReturnType<typeof newNostrClient>
clientCbs: Record<string, nostrCallback<any>> = {} clientCbs: Record<string, nostrCallback<any>> = {}
clientId: string = "" clientId: string = ""
myPub: string = "" myPub: string = ""
log = getLogger({ component: 'liquidityProvider' }) log = getLogger({ component: 'liquidityProvider' })
nostrSend: NostrSend | null = null nostrSend: NostrSend | null = null
ready = false ready = false
pubDestination: string pubDestination: string
latestMaxWithdrawable: number | null = null latestMaxWithdrawable: number | null = null
invoicePaidCb: InvoicePaidCb latestBalance: number | null = null
connecting = false invoicePaidCb: InvoicePaidCb
readyInterval: NodeJS.Timeout connecting = false
// make the sub process accept client readyInterval: NodeJS.Timeout
constructor(pubDestination: string, invoicePaidCb: InvoicePaidCb) { // make the sub process accept client
if (!pubDestination) { constructor(pubDestination: string, invoicePaidCb: InvoicePaidCb) {
this.log("No pub provider to liquidity provider, will not be initialized") if (!pubDestination) {
} this.log("No pub provider to liquidity provider, will not be initialized")
this.pubDestination = pubDestination return
this.invoicePaidCb = invoicePaidCb }
this.client = newNostrClient({ this.log("connecting to liquidity provider", pubDestination)
pubDestination: this.pubDestination, this.pubDestination = pubDestination
retrieveNostrUserAuth: async () => this.myPub, this.invoicePaidCb = invoicePaidCb
}, this.clientSend, this.clientSub) this.client = newNostrClient({
pubDestination: this.pubDestination,
this.readyInterval = setInterval(() => { retrieveNostrUserAuth: async () => this.myPub,
if (this.ready) { }, this.clientSend, this.clientSub)
clearInterval(this.readyInterval)
this.Connect() this.readyInterval = setInterval(() => {
} if (this.ready) {
}, 1000) clearInterval(this.readyInterval)
} this.Connect()
}
Stop = () => { }, 1000)
clearInterval(this.readyInterval) }
}
Stop = () => {
Connect = async () => { clearInterval(this.readyInterval)
await new Promise(res => setTimeout(res, 2000)) }
this.log("ready")
await this.CheckUserState() Connect = async () => {
if (this.latestMaxWithdrawable === null) { await new Promise(res => setTimeout(res, 2000))
return this.log("ready")
} await this.CheckUserState()
this.log("subbing to user operations") if (this.latestMaxWithdrawable === null) {
this.client.GetLiveUserOperations(res => { return
console.log("got user operation", res) }
if (res.status === 'ERROR') { this.log("subbing to user operations")
this.log("error getting user operations", res.reason) this.client.GetLiveUserOperations(res => {
return console.log("got user operation", res)
} if (res.status === 'ERROR') {
this.log("got user operation", res.operation) this.log("error getting user operations", res.reason)
if (res.operation.type === Types.UserOperationType.INCOMING_INVOICE) { return
this.log("invoice was paid", res.operation.identifier) }
this.invoicePaidCb(res.operation.identifier, res.operation.amount, false) this.log("got user operation", res.operation)
} if (res.operation.type === Types.UserOperationType.INCOMING_INVOICE) {
}) this.log("invoice was paid", res.operation.identifier)
} this.invoicePaidCb(res.operation.identifier, res.operation.amount, false)
}
CheckUserState = async () => { })
const res = await this.client.GetUserInfo() }
if (res.status === 'ERROR') {
this.log("error getting user info", res) GetLatestMaxWithdrawable = async (fetch = false) => {
return if (this.latestMaxWithdrawable === null) {
} this.log("liquidity provider is not ready yet")
this.latestMaxWithdrawable = res.max_withdrawable return 0
this.log("latest provider balance:", res.max_withdrawable) }
return res if (fetch) {
} await this.CheckUserState()
}
CanProviderHandle = (req: LiquidityRequest) => { return this.latestMaxWithdrawable || 0
if (this.latestMaxWithdrawable === null) { }
return false
} GetLatestBalance = async (fetch = false) => {
if (req.action === 'spend') { if (this.latestMaxWithdrawable === null) {
return this.latestMaxWithdrawable > req.amount this.log("liquidity provider is not ready yet")
} return 0
return true }
} if (fetch) {
await this.CheckUserState()
AddInvoice = async (amount: number, memo: string) => { }
const res = await this.client.NewInvoice({ amountSats: amount, memo }) return this.latestBalance || 0
if (res.status === 'ERROR') { }
this.log("error creating invoice", res.reason)
throw new Error(res.reason) CheckUserState = async () => {
} const res = await this.client.GetUserInfo()
this.log("new invoice", res.invoice) if (res.status === 'ERROR') {
this.CheckUserState() this.log("error getting user info", res)
return res.invoice return
} }
this.latestMaxWithdrawable = res.max_withdrawable
PayInvoice = async (invoice: string) => { this.latestBalance = res.balance
const res = await this.client.PayInvoice({ invoice, amount: 0 }) this.log("latest provider balance:", res.balance, "latest max withdrawable:", res.max_withdrawable)
if (res.status === 'ERROR') { return res
this.log("error paying invoice", res.reason) }
throw new Error(res.reason)
} CanProviderHandle = (req: LiquidityRequest) => {
this.log("paid invoice", res) if (this.latestMaxWithdrawable === null) {
this.CheckUserState() return false
return res }
} if (req.action === 'spend') {
return this.latestMaxWithdrawable > req.amount
setNostrInfo = ({ clientId, myPub }: { myPub: string, clientId: string }) => { }
this.clientId = clientId return true
this.myPub = myPub }
this.setSetIfReady()
} AddInvoice = async (amount: number, memo: string) => {
const res = await this.client.NewInvoice({ amountSats: amount, memo })
if (res.status === 'ERROR') {
this.log("error creating invoice", res.reason)
attachNostrSend(f: NostrSend) { throw new Error(res.reason)
this.nostrSend = f }
this.setSetIfReady() this.log("new invoice", res.invoice)
} this.CheckUserState()
return res.invoice
setSetIfReady = () => { }
if (this.nostrSend && !!this.pubDestination && !!this.clientId && !!this.myPub) {
this.ready = true PayInvoice = async (invoice: string) => {
this.log("ready to send to ", this.pubDestination) const res = await this.client.PayInvoice({ invoice, amount: 0 })
} if (res.status === 'ERROR') {
} this.log("error paying invoice", res.reason)
throw new Error(res.reason)
onEvent = async (res: { requestId: string }, fromPub: string) => { }
if (fromPub !== this.pubDestination) { this.log("paid invoice", res)
this.log("got event from invalid pub", fromPub, this.pubDestination) this.CheckUserState()
return false return res
} }
if (this.clientCbs[res.requestId]) {
const cb = this.clientCbs[res.requestId] setNostrInfo = ({ clientId, myPub }: { myPub: string, clientId: string }) => {
cb.f(res) this.clientId = clientId
if (cb.type === 'single') { this.myPub = myPub
delete this.clientCbs[res.requestId] this.setSetIfReady()
this.log(this.getSingleSubs(), "single subs left") }
}
return true
}
return false attachNostrSend(f: NostrSend) {
} this.nostrSend = f
this.setSetIfReady()
clientSend = (to: string, message: NostrRequest): Promise<any> => { }
if (!this.ready || !this.nostrSend) {
throw new Error("liquidity provider not initialized") setSetIfReady = () => {
} if (this.nostrSend && !!this.pubDestination && !!this.clientId && !!this.myPub) {
if (!message.requestId) { this.ready = true
message.requestId = makeId(16) this.log("ready to send to ", this.pubDestination)
} }
const reqId = message.requestId }
if (this.clientCbs[reqId]) {
throw new Error("request was already sent") onEvent = async (res: { requestId: string }, fromPub: string) => {
} if (fromPub !== this.pubDestination) {
this.nostrSend({ type: 'client', clientId: this.clientId }, { this.log("got event from invalid pub", fromPub, this.pubDestination)
type: 'content', return false
pub: to, }
content: JSON.stringify(message) if (this.clientCbs[res.requestId]) {
}) const cb = this.clientCbs[res.requestId]
cb.f(res)
//this.nostrSend(this.relays, to, JSON.stringify(message), this.settings) if (cb.type === 'single') {
delete this.clientCbs[res.requestId]
this.log("subbing to single send", reqId, message.rpcName || 'no rpc name') this.log(this.getSingleSubs(), "single subs left")
return new Promise(res => { }
this.clientCbs[reqId] = { return true
startedAtMillis: Date.now(), }
type: 'single', return false
f: (response: any) => { res(response) }, }
}
}) clientSend = (to: string, message: NostrRequest): Promise<any> => {
} if (!this.ready || !this.nostrSend) {
throw new Error("liquidity provider not initialized")
clientSub = (to: string, message: NostrRequest, cb: (res: any) => void): void => { }
if (!this.ready || !this.nostrSend) { if (!message.requestId) {
throw new Error("liquidity provider not initialized") message.requestId = makeId(16)
} }
if (!message.requestId) { const reqId = message.requestId
message.requestId = message.rpcName if (this.clientCbs[reqId]) {
} throw new Error("request was already sent")
const reqId = message.requestId }
if (!reqId) { this.nostrSend({ type: 'client', clientId: this.clientId }, {
throw new Error("invalid sub") type: 'content',
} pub: to,
if (this.clientCbs[reqId]) { content: JSON.stringify(message)
this.clientCbs[reqId] = { })
startedAtMillis: Date.now(),
type: 'stream', //this.nostrSend(this.relays, to, JSON.stringify(message), this.settings)
f: (response: any) => { cb(response) },
} this.log("subbing to single send", reqId, message.rpcName || 'no rpc name')
this.log("sub for", reqId, "was already registered, overriding") return new Promise(res => {
return this.clientCbs[reqId] = {
} startedAtMillis: Date.now(),
this.nostrSend({ type: 'client', clientId: this.clientId }, { type: 'single',
type: 'content', f: (response: any) => { res(response) },
pub: to, }
content: JSON.stringify(message) })
}) }
this.log("subbing to stream", reqId)
this.clientCbs[reqId] = { clientSub = (to: string, message: NostrRequest, cb: (res: any) => void): void => {
startedAtMillis: Date.now(), if (!this.ready || !this.nostrSend) {
type: 'stream', throw new Error("liquidity provider not initialized")
f: (response: any) => { cb(response) } }
} if (!message.requestId) {
} message.requestId = message.rpcName
getSingleSubs = () => { }
return Object.entries(this.clientCbs).filter(([_, cb]) => cb.type === 'single') const reqId = message.requestId
} if (!reqId) {
} throw new Error("invalid sub")
}
export const makeId = (length: number) => { if (this.clientCbs[reqId]) {
let result = ''; this.clientCbs[reqId] = {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; startedAtMillis: Date.now(),
const charactersLength = characters.length; type: 'stream',
for (let i = 0; i < length; i++) { f: (response: any) => { cb(response) },
result += characters.charAt(Math.floor(Math.random() * charactersLength)); }
} this.log("sub for", reqId, "was already registered, overriding")
return result; return
}
this.nostrSend({ type: 'client', clientId: this.clientId }, {
type: 'content',
pub: to,
content: JSON.stringify(message)
})
this.log("subbing to stream", reqId)
this.clientCbs[reqId] = {
startedAtMillis: Date.now(),
type: 'stream',
f: (response: any) => { cb(response) }
}
}
getSingleSubs = () => {
return Object.entries(this.clientCbs).filter(([_, cb]) => cb.type === 'single')
}
}
export const makeId = (length: number) => {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
} }

View file

@ -1,437 +1,439 @@
//const grpc = require('@grpc/grpc-js'); //const grpc = require('@grpc/grpc-js');
import crypto from 'crypto' import crypto from 'crypto'
import { credentials, Metadata } from '@grpc/grpc-js' import { credentials, Metadata } from '@grpc/grpc-js'
import { GrpcTransport } from "@protobuf-ts/grpc-transport"; import { GrpcTransport } from "@protobuf-ts/grpc-transport";
import fs from 'fs' import fs from 'fs'
import * as Types from '../../../proto/autogenerated/ts/types.js' import * as Types from '../../../proto/autogenerated/ts/types.js'
import { LightningClient } from '../../../proto/lnd/lightning.client.js' import { LightningClient } from '../../../proto/lnd/lightning.client.js'
import { InvoicesClient } from '../../../proto/lnd/invoices.client.js' import { InvoicesClient } from '../../../proto/lnd/invoices.client.js'
import { RouterClient } from '../../../proto/lnd/router.client.js' import { RouterClient } from '../../../proto/lnd/router.client.js'
import { ChainNotifierClient } from '../../../proto/lnd/chainnotifier.client.js' import { ChainNotifierClient } from '../../../proto/lnd/chainnotifier.client.js'
import { GetInfoResponse, AddressType, NewAddressResponse, AddInvoiceResponse, Invoice_InvoiceState, PayReq, Payment_PaymentStatus, Payment, PaymentFailureReason, SendCoinsResponse, EstimateFeeResponse, ChannelBalanceResponse, TransactionDetails, ListChannelsResponse, ClosedChannelsResponse, PendingChannelsResponse, ForwardingHistoryResponse } from '../../../proto/lnd/lightning.js' import { GetInfoResponse, AddressType, NewAddressResponse, AddInvoiceResponse, Invoice_InvoiceState, PayReq, Payment_PaymentStatus, Payment, PaymentFailureReason, SendCoinsResponse, EstimateFeeResponse, ChannelBalanceResponse, TransactionDetails, ListChannelsResponse, ClosedChannelsResponse, PendingChannelsResponse, ForwardingHistoryResponse } from '../../../proto/lnd/lightning.js'
import { OpenChannelReq } from './openChannelReq.js'; import { OpenChannelReq } from './openChannelReq.js';
import { AddInvoiceReq } from './addInvoiceReq.js'; import { AddInvoiceReq } from './addInvoiceReq.js';
import { PayInvoiceReq } from './payInvoiceReq.js'; import { PayInvoiceReq } from './payInvoiceReq.js';
import { SendCoinsReq } from './sendCoinsReq.js'; import { SendCoinsReq } from './sendCoinsReq.js';
import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo } from './settings.js'; import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo } from './settings.js';
import { getLogger } from '../helpers/logger.js'; import { getLogger } from '../helpers/logger.js';
import { HtlcEvent_EventType } from '../../../proto/lnd/router.js'; import { HtlcEvent_EventType } from '../../../proto/lnd/router.js';
import { LiquidityProvider, LiquidityRequest } from './liquidityProvider.js'; import { LiquidityProvider, LiquidityRequest } from './liquidityProvider.js';
const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline }) const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
const deadLndRetrySeconds = 5 const deadLndRetrySeconds = 5
export default class { export default class {
lightning: LightningClient lightning: LightningClient
invoices: InvoicesClient invoices: InvoicesClient
router: RouterClient router: RouterClient
chainNotifier: ChainNotifierClient chainNotifier: ChainNotifierClient
settings: LndSettings settings: LndSettings
ready = false ready = false
latestKnownBlockHeigh = 0 latestKnownBlockHeigh = 0
latestKnownSettleIndex = 0 latestKnownSettleIndex = 0
abortController = new AbortController() abortController = new AbortController()
addressPaidCb: AddressPaidCb addressPaidCb: AddressPaidCb
invoicePaidCb: InvoicePaidCb invoicePaidCb: InvoicePaidCb
newBlockCb: NewBlockCb newBlockCb: NewBlockCb
htlcCb: HtlcCb htlcCb: HtlcCb
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
this.settings = settings constructor(settings: LndSettings, provider: { liquidProvider: LiquidityProvider, useOnly?: boolean }, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb) {
this.addressPaidCb = addressPaidCb this.settings = settings
this.invoicePaidCb = invoicePaidCb this.addressPaidCb = addressPaidCb
this.newBlockCb = newBlockCb this.invoicePaidCb = invoicePaidCb
this.htlcCb = htlcCb this.newBlockCb = newBlockCb
const { lndAddr, lndCertPath, lndMacaroonPath } = this.settings.mainNode this.htlcCb = htlcCb
const lndCert = fs.readFileSync(lndCertPath); const { lndAddr, lndCertPath, lndMacaroonPath } = this.settings.mainNode
const macaroon = fs.readFileSync(lndMacaroonPath).toString('hex'); const lndCert = fs.readFileSync(lndCertPath);
const sslCreds = credentials.createSsl(lndCert); const macaroon = fs.readFileSync(lndMacaroonPath).toString('hex');
const macaroonCreds = credentials.createFromMetadataGenerator( const sslCreds = credentials.createSsl(lndCert);
function (args: any, callback: any) { const macaroonCreds = credentials.createFromMetadataGenerator(
let metadata = new Metadata(); function (args: any, callback: any) {
metadata.add('macaroon', macaroon); let metadata = new Metadata();
callback(null, metadata); metadata.add('macaroon', macaroon);
}, callback(null, metadata);
); },
const creds = credentials.combineChannelCredentials( );
sslCreds, const creds = credentials.combineChannelCredentials(
macaroonCreds, sslCreds,
); macaroonCreds,
const transport = new GrpcTransport({ host: lndAddr, channelCredentials: creds }) );
this.lightning = new LightningClient(transport) const transport = new GrpcTransport({ host: lndAddr, channelCredentials: creds })
this.invoices = new InvoicesClient(transport) this.lightning = new LightningClient(transport)
this.router = new RouterClient(transport) this.invoices = new InvoicesClient(transport)
this.chainNotifier = new ChainNotifierClient(transport) this.router = new RouterClient(transport)
this.liquidProvider = liquidProvider this.chainNotifier = new ChainNotifierClient(transport)
} this.liquidProvider = provider.liquidProvider
this.useOnlyLiquidityProvider = !!provider.useOnly
LockOutgoingOperations(): void { }
this.outgoingOpsLocked = true
} LockOutgoingOperations(): void {
UnlockOutgoingOperations(): void { this.outgoingOpsLocked = true
this.outgoingOpsLocked = false }
} UnlockOutgoingOperations(): void {
this.outgoingOpsLocked = false
SetMockInvoiceAsPaid(invoice: string, amount: number): Promise<void> { }
throw new Error("SetMockInvoiceAsPaid only available in mock mode")
} SetMockInvoiceAsPaid(invoice: string, amount: number): Promise<void> {
Stop() { throw new Error("SetMockInvoiceAsPaid only available in mock mode")
this.abortController.abort() }
this.liquidProvider.Stop() Stop() {
} this.abortController.abort()
this.liquidProvider.Stop()
async ShouldUseLiquidityProvider(req: LiquidityRequest): Promise<boolean> { }
if (this.settings.useOnlyLiquidityProvider) {
return true async ShouldUseLiquidityProvider(req: LiquidityRequest): Promise<boolean> {
} if (this.useOnlyLiquidityProvider) {
if (!this.liquidProvider.CanProviderHandle(req)) { return true
return false }
} if (!this.liquidProvider.CanProviderHandle(req)) {
const channels = await this.ListChannels() return false
if (channels.channels.length === 0) { }
this.log("no channels, will use liquidity provider") const channels = await this.ListChannels()
return true if (channels.channels.length === 0) {
} this.log("no channels, will use liquidity provider")
return false return true
} }
async Warmup() { return false
this.SubscribeAddressPaid() }
this.SubscribeInvoicePaid() async Warmup() {
this.SubscribeNewBlock() this.SubscribeAddressPaid()
this.SubscribeHtlcEvents() this.SubscribeInvoicePaid()
const now = Date.now() this.SubscribeNewBlock()
return new Promise<void>((res, rej) => { this.SubscribeHtlcEvents()
const interval = setInterval(async () => { const now = Date.now()
try { return new Promise<void>((res, rej) => {
await this.GetInfo() const interval = setInterval(async () => {
clearInterval(interval) try {
this.ready = true await this.GetInfo()
res() clearInterval(interval)
} catch (err) { this.ready = true
this.log("LND is not ready yet, will try again in 1 second") res()
if (Date.now() - now > 1000 * 60) { } catch (err) {
rej(new Error("LND not ready after 1 minute")) this.log("LND is not ready yet, will try again in 1 second")
} if (Date.now() - now > 1000 * 60) {
} rej(new Error("LND not ready after 1 minute"))
}, 1000) }
}) }
} }, 1000)
})
async GetInfo(): Promise<NodeInfo> { }
const res = await this.lightning.getInfo({}, DeadLineMetadata())
return res.response async GetInfo(): Promise<NodeInfo> {
} const res = await this.lightning.getInfo({}, DeadLineMetadata())
async ListPendingChannels(): Promise<PendingChannelsResponse> { return res.response
const res = await this.lightning.pendingChannels({}, DeadLineMetadata()) }
return res.response async ListPendingChannels(): Promise<PendingChannelsResponse> {
} const res = await this.lightning.pendingChannels({}, DeadLineMetadata())
async ListChannels(): Promise<ListChannelsResponse> { return res.response
const res = await this.lightning.listChannels({ }
activeOnly: false, inactiveOnly: false, privateOnly: false, publicOnly: false, peer: Buffer.alloc(0) async ListChannels(): Promise<ListChannelsResponse> {
}, DeadLineMetadata()) const res = await this.lightning.listChannels({
return res.response activeOnly: false, inactiveOnly: false, privateOnly: false, publicOnly: false, peer: Buffer.alloc(0)
} }, DeadLineMetadata())
async ListClosedChannels(): Promise<ClosedChannelsResponse> { return res.response
const res = await this.lightning.closedChannels({ }
abandoned: true, async ListClosedChannels(): Promise<ClosedChannelsResponse> {
breach: true, const res = await this.lightning.closedChannels({
cooperative: true, abandoned: true,
fundingCanceled: true, breach: true,
localForce: true, cooperative: true,
remoteForce: true fundingCanceled: true,
}, DeadLineMetadata()) localForce: true,
return res.response remoteForce: true
} }, DeadLineMetadata())
return res.response
async Health(): Promise<void> { }
if (!this.ready) {
throw new Error("not ready") async Health(): Promise<void> {
} if (!this.ready) {
const info = await this.GetInfo() throw new Error("not ready")
if (!info.syncedToChain || !info.syncedToGraph) { }
throw new Error("not synced") const info = await this.GetInfo()
} if (!info.syncedToChain || !info.syncedToGraph) {
} throw new Error("not synced")
}
RestartStreams() { }
if (!this.ready) {
return RestartStreams() {
} if (!this.ready) {
this.log("LND is dead, will try to reconnect in", deadLndRetrySeconds, "seconds") return
const interval = setInterval(async () => { }
try { this.log("LND is dead, will try to reconnect in", deadLndRetrySeconds, "seconds")
await this.Health() const interval = setInterval(async () => {
this.log("LND is back online") try {
clearInterval(interval) await this.Health()
this.Warmup() this.log("LND is back online")
} catch (err) { clearInterval(interval)
this.log("LND still dead, will try again in", deadLndRetrySeconds, "seconds") this.Warmup()
} } catch (err) {
}, deadLndRetrySeconds * 1000) this.log("LND still dead, will try again in", deadLndRetrySeconds, "seconds")
} }
}, deadLndRetrySeconds * 1000)
async SubscribeHtlcEvents() { }
const stream = this.router.subscribeHtlcEvents({}, { abort: this.abortController.signal })
stream.responses.onMessage(htlc => { async SubscribeHtlcEvents() {
this.htlcCb(htlc) const stream = this.router.subscribeHtlcEvents({}, { abort: this.abortController.signal })
}) stream.responses.onMessage(htlc => {
stream.responses.onError(error => { this.htlcCb(htlc)
this.log("Error with subscribeHtlcEvents stream") })
}) stream.responses.onError(error => {
stream.responses.onComplete(() => { this.log("Error with subscribeHtlcEvents stream")
this.log("subscribeHtlcEvents stream closed") })
}) stream.responses.onComplete(() => {
} this.log("subscribeHtlcEvents stream closed")
})
async SubscribeNewBlock() { }
const { blockHeight } = await this.GetInfo()
const stream = this.chainNotifier.registerBlockEpochNtfn({ height: blockHeight, hash: Buffer.alloc(0) }, { abort: this.abortController.signal }) async SubscribeNewBlock() {
stream.responses.onMessage(block => { const { blockHeight } = await this.GetInfo()
this.newBlockCb(block.height) const stream = this.chainNotifier.registerBlockEpochNtfn({ height: blockHeight, hash: Buffer.alloc(0) }, { abort: this.abortController.signal })
}) stream.responses.onMessage(block => {
stream.responses.onError(error => { this.newBlockCb(block.height)
this.log("Error with onchain tx stream") })
}) stream.responses.onError(error => {
stream.responses.onComplete(() => { this.log("Error with onchain tx stream")
this.log("onchain tx stream closed") })
}) stream.responses.onComplete(() => {
} this.log("onchain tx stream closed")
})
SubscribeAddressPaid(): void { }
const stream = this.lightning.subscribeTransactions({
account: "", SubscribeAddressPaid(): void {
endHeight: 0, const stream = this.lightning.subscribeTransactions({
startHeight: this.latestKnownBlockHeigh, account: "",
}, { abort: this.abortController.signal }) endHeight: 0,
stream.responses.onMessage(tx => { startHeight: this.latestKnownBlockHeigh,
if (tx.blockHeight > this.latestKnownBlockHeigh) { }, { abort: this.abortController.signal })
this.latestKnownBlockHeigh = tx.blockHeight stream.responses.onMessage(tx => {
} if (tx.blockHeight > this.latestKnownBlockHeigh) {
if (tx.numConfirmations === 0) { // only process pending transactions, confirmed transaction are processed by the newBlock CB this.latestKnownBlockHeigh = tx.blockHeight
tx.outputDetails.forEach(output => { }
if (output.isOurAddress) { if (tx.numConfirmations === 0) { // only process pending transactions, confirmed transaction are processed by the newBlock CB
this.log("received chan TX", Number(output.amount), "sats", "for", output.address) tx.outputDetails.forEach(output => {
this.addressPaidCb({ hash: tx.txHash, index: Number(output.outputIndex) }, output.address, Number(output.amount), false) if (output.isOurAddress) {
} this.log("received chan TX", Number(output.amount), "sats", "for", output.address)
}) this.addressPaidCb({ hash: tx.txHash, index: Number(output.outputIndex) }, output.address, Number(output.amount), false)
} }
}) })
stream.responses.onError(error => { }
this.log("Error with onchain tx stream") })
}) stream.responses.onError(error => {
stream.responses.onComplete(() => { this.log("Error with onchain tx stream")
this.log("onchain tx stream closed") })
}) stream.responses.onComplete(() => {
} this.log("onchain tx stream closed")
})
SubscribeInvoicePaid(): void { }
const stream = this.lightning.subscribeInvoices({
settleIndex: BigInt(this.latestKnownSettleIndex), SubscribeInvoicePaid(): void {
addIndex: 0n, const stream = this.lightning.subscribeInvoices({
}, { abort: this.abortController.signal }) settleIndex: BigInt(this.latestKnownSettleIndex),
stream.responses.onMessage(invoice => { addIndex: 0n,
if (invoice.state === Invoice_InvoiceState.SETTLED) { }, { abort: this.abortController.signal })
this.log("An invoice was paid for", Number(invoice.amtPaidSat), "sats", invoice.paymentRequest) stream.responses.onMessage(invoice => {
this.latestKnownSettleIndex = Number(invoice.settleIndex) if (invoice.state === Invoice_InvoiceState.SETTLED) {
this.invoicePaidCb(invoice.paymentRequest, Number(invoice.amtPaidSat), false) this.log("An invoice was paid for", Number(invoice.amtPaidSat), "sats", invoice.paymentRequest)
} this.latestKnownSettleIndex = Number(invoice.settleIndex)
}) this.invoicePaidCb(invoice.paymentRequest, Number(invoice.amtPaidSat), false)
stream.responses.onError(error => { }
this.log("Error with invoice stream") })
}) stream.responses.onError(error => {
stream.responses.onComplete(() => { this.log("Error with invoice stream")
this.log("invoice stream closed") })
this.RestartStreams() stream.responses.onComplete(() => {
}) this.log("invoice stream closed")
} this.RestartStreams()
})
async NewAddress(addressType: Types.AddressType): Promise<NewAddressResponse> { }
this.log("generating new address")
await this.Health() async NewAddress(addressType: Types.AddressType): Promise<NewAddressResponse> {
let lndAddressType: AddressType this.log("generating new address")
switch (addressType) { await this.Health()
case Types.AddressType.NESTED_PUBKEY_HASH: let lndAddressType: AddressType
lndAddressType = AddressType.NESTED_PUBKEY_HASH switch (addressType) {
break; case Types.AddressType.NESTED_PUBKEY_HASH:
case Types.AddressType.WITNESS_PUBKEY_HASH: lndAddressType = AddressType.NESTED_PUBKEY_HASH
lndAddressType = AddressType.WITNESS_PUBKEY_HASH break;
break; case Types.AddressType.WITNESS_PUBKEY_HASH:
case Types.AddressType.TAPROOT_PUBKEY: lndAddressType = AddressType.WITNESS_PUBKEY_HASH
lndAddressType = AddressType.TAPROOT_PUBKEY break;
break; case Types.AddressType.TAPROOT_PUBKEY:
default: lndAddressType = AddressType.TAPROOT_PUBKEY
throw new Error("unknown address type " + addressType) break;
} default:
const res = await this.lightning.newAddress({ account: "", type: lndAddressType }, DeadLineMetadata()) throw new Error("unknown address type " + addressType)
this.log("new address", res.response.address) }
return res.response const res = await this.lightning.newAddress({ account: "", type: lndAddressType }, DeadLineMetadata())
} this.log("new address", res.response.address)
return res.response
async NewInvoice(value: number, memo: string, expiry: number): Promise<Invoice> { }
this.log("generating new invoice for", value, "sats")
await this.Health() async NewInvoice(value: number, memo: string, expiry: number, useProvider = false): Promise<Invoice> {
const shouldUseLiquidityProvider = await this.ShouldUseLiquidityProvider({ action: 'receive', amount: value }) this.log("generating new invoice for", value, "sats")
if (shouldUseLiquidityProvider) { await this.Health()
const invoice = await this.liquidProvider.AddInvoice(value, memo) const shouldUseLiquidityProvider = await this.ShouldUseLiquidityProvider({ action: 'receive', amount: value })
return { payRequest: invoice } if (shouldUseLiquidityProvider || useProvider) {
} const invoice = await this.liquidProvider.AddInvoice(value, memo)
const res = await this.lightning.addInvoice(AddInvoiceReq(value, expiry, false, memo), DeadLineMetadata()) return { payRequest: invoice }
this.log("new invoice", res.response.paymentRequest) }
return { payRequest: res.response.paymentRequest } const res = await this.lightning.addInvoice(AddInvoiceReq(value, expiry, false, memo), DeadLineMetadata())
} this.log("new invoice", res.response.paymentRequest)
return { payRequest: res.response.paymentRequest }
async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> { }
const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata())
return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash } async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> {
} const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata())
return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash }
GetFeeLimitAmount(amount: number): number { }
return Math.ceil(amount * this.settings.feeRateLimit + this.settings.feeFixedLimit);
} GetFeeLimitAmount(amount: number): number {
return Math.ceil(amount * this.settings.feeRateLimit + this.settings.feeFixedLimit);
GetMaxWithinLimit(amount: number): number { }
return Math.max(0, Math.floor(amount * (1 - this.settings.feeRateLimit) - this.settings.feeFixedLimit))
} GetMaxWithinLimit(amount: number): number {
return Math.max(0, Math.floor(amount * (1 - this.settings.feeRateLimit) - this.settings.feeFixedLimit))
async ChannelBalance(): Promise<{ local: number, remote: number }> { }
const res = await this.lightning.channelBalance({})
const r = res.response async ChannelBalance(): Promise<{ local: number, remote: number }> {
return { local: r.localBalance ? Number(r.localBalance.sat) : 0, remote: r.remoteBalance ? Number(r.remoteBalance.sat) : 0 } const res = await this.lightning.channelBalance({})
} const r = res.response
async PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<PaidInvoice> { return { local: r.localBalance ? Number(r.localBalance.sat) : 0, remote: r.remoteBalance ? Number(r.remoteBalance.sat) : 0 }
if (this.outgoingOpsLocked) { }
this.log("outgoing ops locked, rejecting payment request") async PayInvoice(invoice: string, amount: number, feeLimit: number, useProvider = false): Promise<PaidInvoice> {
throw new Error("lnd node is currently out of sync") if (this.outgoingOpsLocked) {
} this.log("outgoing ops locked, rejecting payment request")
await this.Health() throw new Error("lnd node is currently out of sync")
this.log("paying invoice", invoice, "for", amount, "sats") }
const shouldUseLiquidityProvider = await this.ShouldUseLiquidityProvider({ action: 'spend', amount }) await this.Health()
if (shouldUseLiquidityProvider) { this.log("paying invoice", invoice, "for", amount, "sats")
const res = await this.liquidProvider.PayInvoice(invoice) const shouldUseLiquidityProvider = await this.ShouldUseLiquidityProvider({ action: 'spend', amount })
return { feeSat: res.network_fee + res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage } if (shouldUseLiquidityProvider || useProvider) {
} const res = await this.liquidProvider.PayInvoice(invoice)
const abortController = new AbortController() return { feeSat: res.network_fee + res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage }
const req = PayInvoiceReq(invoice, amount, feeLimit) }
const stream = this.router.sendPaymentV2(req, { abort: abortController.signal }) const abortController = new AbortController()
return new Promise((res, rej) => { const req = PayInvoiceReq(invoice, amount, feeLimit)
stream.responses.onError(error => { const stream = this.router.sendPaymentV2(req, { abort: abortController.signal })
this.log("invoice payment failed", error) return new Promise((res, rej) => {
rej(error) stream.responses.onError(error => {
}) this.log("invoice payment failed", error)
stream.responses.onMessage(payment => { rej(error)
switch (payment.status) { })
case Payment_PaymentStatus.FAILED: stream.responses.onMessage(payment => {
console.log(payment) switch (payment.status) {
this.log("invoice payment failed", payment.failureReason) case Payment_PaymentStatus.FAILED:
rej(PaymentFailureReason[payment.failureReason]) console.log(payment)
return this.log("invoice payment failed", payment.failureReason)
case Payment_PaymentStatus.SUCCEEDED: rej(PaymentFailureReason[payment.failureReason])
this.log("invoice payment succeded", Number(payment.valueSat)) return
res({ feeSat: Math.ceil(Number(payment.feeMsat) / 1000), valueSat: Number(payment.valueSat), paymentPreimage: payment.paymentPreimage }) case Payment_PaymentStatus.SUCCEEDED:
} this.log("invoice payment succeded", Number(payment.valueSat))
}) res({ feeSat: Math.ceil(Number(payment.feeMsat) / 1000), valueSat: Number(payment.valueSat), paymentPreimage: payment.paymentPreimage })
}) }
} })
})
async EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse> { }
await this.Health()
const res = await this.lightning.estimateFee({ async EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse> {
addrToAmount: { [address]: BigInt(amount) }, await this.Health()
minConfs: 1, const res = await this.lightning.estimateFee({
spendUnconfirmed: false, addrToAmount: { [address]: BigInt(amount) },
targetConf: targetConf minConfs: 1,
}) spendUnconfirmed: false,
return res.response targetConf: targetConf
} })
return res.response
async PayAddress(address: string, amount: number, satPerVByte: number, label = ""): Promise<SendCoinsResponse> { }
if (this.outgoingOpsLocked) {
this.log("outgoing ops locked, rejecting payment request") async PayAddress(address: string, amount: number, satPerVByte: number, label = ""): Promise<SendCoinsResponse> {
throw new Error("lnd node is currently out of sync") if (this.outgoingOpsLocked) {
} this.log("outgoing ops locked, rejecting payment request")
await this.Health() throw new Error("lnd node is currently out of sync")
this.log("sending chain TX for", amount, "sats", "to", address) }
const res = await this.lightning.sendCoins(SendCoinsReq(address, amount, satPerVByte, label), DeadLineMetadata()) await this.Health()
this.log("sent chain TX for", amount, "sats", "to", address) this.log("sending chain TX for", amount, "sats", "to", address)
return res.response const res = await this.lightning.sendCoins(SendCoinsReq(address, amount, satPerVByte, label), DeadLineMetadata())
} this.log("sent chain TX for", amount, "sats", "to", address)
return res.response
async GetTransactions(startHeight: number): Promise<TransactionDetails> { }
await this.Health()
const res = await this.lightning.getTransactions({ startHeight, endHeight: 0, account: "" }, DeadLineMetadata()) async GetTransactions(startHeight: number): Promise<TransactionDetails> {
return res.response await this.Health()
} const res = await this.lightning.getTransactions({ startHeight, endHeight: 0, account: "" }, DeadLineMetadata())
return res.response
async GetChannelBalance() { }
const res = await this.lightning.channelBalance({}, DeadLineMetadata())
return res.response async GetChannelBalance() {
} const res = await this.lightning.channelBalance({}, DeadLineMetadata())
return res.response
async GetWalletBalance() { }
const res = await this.lightning.walletBalance({}, DeadLineMetadata())
return res.response async GetWalletBalance() {
} const res = await this.lightning.walletBalance({}, DeadLineMetadata())
return res.response
async GetBalance(): Promise<BalanceInfo> { }
const wRes = await this.lightning.walletBalance({}, DeadLineMetadata())
const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response async GetBalance(): Promise<BalanceInfo> {
const { response } = await this.lightning.listChannels({ const wRes = await this.lightning.walletBalance({}, DeadLineMetadata())
activeOnly: false, inactiveOnly: false, privateOnly: false, publicOnly: false, peer: Buffer.alloc(0) const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response
}, DeadLineMetadata()) const { response } = await this.lightning.listChannels({
const channelsBalance = response.channels.map(c => ({ activeOnly: false, inactiveOnly: false, privateOnly: false, publicOnly: false, peer: Buffer.alloc(0)
channelId: c.chanId, }, DeadLineMetadata())
localBalanceSats: Number(c.localBalance), const channelsBalance = response.channels.map(c => ({
remoteBalanceSats: Number(c.remoteBalance), channelId: c.chanId,
htlcs: c.pendingHtlcs.map(htlc => ({ incoming: htlc.incoming, amount: Number(htlc.amount), index: Number(htlc.htlcIndex), fwIndex: Number(htlc.forwardingHtlcIndex) })) localBalanceSats: Number(c.localBalance),
})) remoteBalanceSats: Number(c.remoteBalance),
return { confirmedBalance: Number(confirmedBalance), unconfirmedBalance: Number(unconfirmedBalance), totalBalance: Number(totalBalance), channelsBalance } htlcs: c.pendingHtlcs.map(htlc => ({ incoming: htlc.incoming, amount: Number(htlc.amount), index: Number(htlc.htlcIndex), fwIndex: Number(htlc.forwardingHtlcIndex) }))
} }))
return { confirmedBalance: Number(confirmedBalance), unconfirmedBalance: Number(unconfirmedBalance), totalBalance: Number(totalBalance), channelsBalance }
async GetForwardingHistory(indexOffset: number, startTime = 0): Promise<ForwardingHistoryResponse> { }
const { response } = await this.lightning.forwardingHistory({ indexOffset, numMaxEvents: 0, startTime: BigInt(startTime), endTime: 0n, peerAliasLookup: false }, DeadLineMetadata())
return response async GetForwardingHistory(indexOffset: number, startTime = 0): Promise<ForwardingHistoryResponse> {
} const { response } = await this.lightning.forwardingHistory({ indexOffset, numMaxEvents: 0, startTime: BigInt(startTime), endTime: 0n, peerAliasLookup: false }, DeadLineMetadata())
return response
async GetAllPaidInvoices(max: number) { }
const res = await this.lightning.listInvoices({ indexOffset: 0n, numMaxInvoices: BigInt(max), pendingOnly: false, reversed: true }, DeadLineMetadata())
return res.response async GetAllPaidInvoices(max: number) {
} const res = await this.lightning.listInvoices({ indexOffset: 0n, numMaxInvoices: BigInt(max), pendingOnly: false, reversed: true }, DeadLineMetadata())
async GetAllPayments(max: number) { return res.response
const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset: 0n, maxPayments: BigInt(max), reversed: true }) }
return res.response async GetAllPayments(max: number) {
} const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset: 0n, maxPayments: BigInt(max), reversed: true })
return res.response
async ConnectPeer(addr: { pubkey: string, host: string }) { }
const res = await this.lightning.connectPeer({
addr, async ConnectPeer(addr: { pubkey: string, host: string }) {
perm: true, const res = await this.lightning.connectPeer({
timeout: 0n addr,
}, DeadLineMetadata()) perm: true,
return res.response timeout: 0n
} }, DeadLineMetadata())
return res.response
async ListPeers() { }
const res = await this.lightning.listPeers({ latestError: true }, DeadLineMetadata())
return res.response async ListPeers() {
} const res = await this.lightning.listPeers({ latestError: true }, DeadLineMetadata())
return res.response
async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number) { }
const abortController = new AbortController()
const req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats) async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number) {
const stream = this.lightning.openChannel(req, { abort: abortController.signal }) const abortController = new AbortController()
return new Promise((res, rej) => { const req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats)
stream.responses.onMessage(message => { const stream = this.lightning.openChannel(req, { abort: abortController.signal })
console.log("message", message) return new Promise((res, rej) => {
switch (message.update.oneofKind) { stream.responses.onMessage(message => {
case 'chanPending': console.log("message", message)
res(Buffer.from(message.pendingChanId).toString('base64')) switch (message.update.oneofKind) {
break case 'chanPending':
} res(Buffer.from(message.pendingChanId).toString('base64'))
}) break
stream.responses.onError(error => { }
console.log("error", error) })
rej(error) stream.responses.onError(error => {
}) console.log("error", error)
}) rej(error)
} })
} })
}
}

View file

@ -1,29 +1,331 @@
import fetch from "node-fetch" import fetch from "node-fetch"
import { LiquidityProvider } from "./liquidityProvider.js"
export class LSP { import { getLogger, PubLogger } from '../helpers/logger.js'
serviceUrl: string import LND from "./lnd.js"
constructor(serviceUrl: string) { import { AddressType } from "../../../proto/autogenerated/ts/types.js"
this.serviceUrl = serviceUrl import { EnvCanBeInteger } from "../helpers/envParser.js"
} export type LSPSettings = {
olympusServiceUrl: string
getInfo = async () => { voltageServiceUrl: string
const res = await fetch(`${this.serviceUrl}/getinfo`) flashsatsServiceUrl: string
const json = await res.json() as { options: {}, uris: string[] } channelThreshold: number
} maxRelativeFee: number
}
createOrder = async (req: { public_key: string }) => {
const res = await fetch(`${this.serviceUrl}/create_order`, { export const LoadLSPSettingsFromEnv = (): LSPSettings => {
method: "POST", const olympusServiceUrl = process.env.OLYMPUS_LSP_URL || "https://lsps1.lnolymp.us/api/v1"
body: JSON.stringify(req), const voltageServiceUrl = process.env.VOLTAGE_LSP_URL || "https://lsp.voltageapi.com/api/v1"
headers: { "Content-Type": "application/json" } const flashsatsServiceUrl = process.env.FLASHSATS_LSP_URL || "https://lsp.flashsats.xyz/lsp/channel"
}) const channelThreshold = EnvCanBeInteger("LSP_CHANNEL_THRESHOLD", 1000000)
const json = await res.json() as {} const maxRelativeFee = EnvCanBeInteger("LSP_MAX_FEE_BPS", 100) / 10000
return json return { olympusServiceUrl, voltageServiceUrl, channelThreshold, maxRelativeFee, flashsatsServiceUrl }
}
}
getOrder = async (orderId: string) => { type OlympusOrder = {
const res = await fetch(`${this.serviceUrl}/get_order&order_id=${orderId}`) "lsp_balance_sat": string,
const json = await res.json() as {} "client_balance_sat": string,
return json "required_channel_confirmations": number,
} "funding_confirms_within_blocks": number,
"channel_expiry_blocks": number,
"refund_onchain_address": string,
"announce_channel": boolean,
"public_key": string
}
type FlashsatsOrder = {
"node_connection_info": string,
"lsp_balance_sat": number,
"client_balance_sat": number,
"confirms_within_blocks": number,
"channel_expiry_blocks": number,
"announce_channel": boolean,
"token": string
}
type OrderResponse = {
orderId: string
invoice: string
totalSats: number
fees: number
}
class LSP {
settings: LSPSettings
liquidityProvider: LiquidityProvider
lnd: LND
log: PubLogger
constructor(serviceName: string, settings: LSPSettings, lnd: LND, liquidityProvider: LiquidityProvider) {
this.settings = settings
this.lnd = lnd
this.liquidityProvider = liquidityProvider
this.log = getLogger({ component: serviceName })
}
shouldOpenChannel = async (): Promise<{ shouldOpen: false } | { shouldOpen: true, maxSpendable: number }> => {
if (this.settings.channelThreshold === 0) {
this.log("channel threshold is 0")
return { shouldOpen: false }
}
const channels = await this.lnd.ListChannels()
if (channels.channels.length > 0) {
this.log("this node already has open channels")
return { shouldOpen: false }
}
const pendingChannels = await this.lnd.ListPendingChannels()
if (pendingChannels.pendingOpenChannels.length > 0) {
this.log("this node already has pending channels")
return { shouldOpen: false }
}
const userState = await this.liquidityProvider.CheckUserState()
if (!userState || userState.max_withdrawable < this.settings.channelThreshold) {
this.log("balance of", userState?.max_withdrawable || 0, "is lower than channel threshold of", this.settings.channelThreshold)
return { shouldOpen: false }
}
return { shouldOpen: true, maxSpendable: userState.max_withdrawable }
}
addPeer = async (pubKey: string, host: string) => {
const { peers } = await this.lnd.ListPeers()
if (!peers.find(p => p.pubKey === pubKey)) {
await this.lnd.ConnectPeer({ host, pubkey: pubKey })
}
}
}
export class FlashsatsLSP extends LSP {
constructor(settings: LSPSettings, lnd: LND, liquidityProvider: LiquidityProvider) {
super("FlashsatsLSP", settings, lnd, liquidityProvider)
}
openChannelIfReady = async (): Promise<OrderResponse | null> => {
const shouldOpen = await this.shouldOpenChannel()
if (!shouldOpen.shouldOpen) {
return null
}
if (!this.settings.flashsatsServiceUrl) {
this.log("no flashsats service url provided")
return null
}
const serviceInfo = await this.getInfo()
if (+serviceInfo.options.min_initial_client_balance_sat > shouldOpen.maxSpendable) {
this.log("balance of", shouldOpen.maxSpendable, "is lower than service minimum of", serviceInfo.options.min_initial_client_balance_sat)
return null
}
const lndInfo = await this.lnd.GetInfo()
const myUri = lndInfo.uris.length > 0 ? lndInfo.uris[0] : ""
if (!myUri) {
this.log("no uri found for this node,uri is required to use flashsats")
return null
}
const lspBalance = (this.settings.channelThreshold * 2).toString()
const chanExpiryBlocks = serviceInfo.options.max_channel_expiry_blocks
const order = await this.createOrder({ nodeUri: myUri, lspBalance, clientBalance: "0", chanExpiryBlocks })
if (order.payment.state !== 'EXPECT_PAYMENT') {
this.log("order not in expect payment state")
return null
}
const decoded = await this.lnd.DecodeInvoice(order.payment.bolt11_invoice)
if (decoded.numSatoshis !== +order.payment.order_total_sat) {
this.log("invoice of amount", decoded.numSatoshis, "does not match order total of", order.payment.order_total_sat)
return null
}
if (decoded.numSatoshis > shouldOpen.maxSpendable) {
this.log("invoice of amount", decoded.numSatoshis, "exceeds user balance of", shouldOpen.maxSpendable)
return null
}
const relativeFee = +order.payment.fee_total_sat / this.settings.channelThreshold
if (relativeFee > this.settings.maxRelativeFee) {
this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee)
return null
}
const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice)
this.log("paid", res.amount_paid, "to open channel")
return { orderId: order.order_id, invoice: order.payment.bolt11_invoice, totalSats: +order.payment.order_total_sat, fees: +order.payment.fee_total_sat }
}
getInfo = async () => {
const res = await fetch(`${this.settings.flashsatsServiceUrl}/info`)
const json = await res.json() as { options: { min_initial_client_balance_sat: string, max_channel_expiry_blocks: number } }
return json
}
createOrder = async (orderInfo: { nodeUri: string, lspBalance: string, clientBalance: string, chanExpiryBlocks: number }) => {
const req: FlashsatsOrder = {
node_connection_info: orderInfo.nodeUri,
announce_channel: true,
channel_expiry_blocks: orderInfo.chanExpiryBlocks,
client_balance_sat: +orderInfo.clientBalance,
lsp_balance_sat: +orderInfo.lspBalance,
confirms_within_blocks: 6,
token: "flashsats"
}
const res = await fetch(`${this.settings.flashsatsServiceUrl}/channel`, {
method: "POST",
body: JSON.stringify(req),
headers: { "Content-Type": "application/json" }
})
const json = await res.json() as { order_id: string, payment: { state: 'EXPECT_PAYMENT', bolt11_invoice: string, fee_total_sat: string, order_total_sat: string } }
return json
}
}
export class OlympusLSP extends LSP {
constructor(settings: LSPSettings, lnd: LND, liquidityProvider: LiquidityProvider) {
super("OlympusLSP", settings, lnd, liquidityProvider)
}
openChannelIfReady = async (): Promise<OrderResponse | null> => {
const shouldOpen = await this.shouldOpenChannel()
if (!shouldOpen.shouldOpen) {
return null
}
if (!this.settings.olympusServiceUrl) {
this.log("no olympus service url provided")
return null
}
const serviceInfo = await this.getInfo()
if (+serviceInfo.options.min_initial_client_balance_sat > shouldOpen.maxSpendable) {
this.log("balance of", shouldOpen.maxSpendable, "is lower than service minimum of", serviceInfo.options.min_initial_client_balance_sat)
return null
}
const [servicePub, host] = serviceInfo.uris[0].split('@')
await this.addPeer(servicePub, host)
const lndInfo = await this.lnd.GetInfo()
const myPub = lndInfo.identityPubkey
const refundAddr = await this.lnd.NewAddress(AddressType.WITNESS_PUBKEY_HASH)
const lspBalance = (this.settings.channelThreshold * 2).toString()
const chanExpiryBlocks = serviceInfo.options.max_channel_expiry_blocks
const order = await this.createOrder({ pubKey: myPub, refundAddr: refundAddr.address, lspBalance, clientBalance: "0", chanExpiryBlocks })
if (order.payment.state !== 'EXPECT_PAYMENT') {
this.log("order not in expect payment state")
return null
}
const decoded = await this.lnd.DecodeInvoice(order.payment.bolt11_invoice)
if (decoded.numSatoshis !== +order.payment.order_total_sat) {
this.log("invoice of amount", decoded.numSatoshis, "does not match order total of", order.payment.order_total_sat)
return null
}
if (decoded.numSatoshis > shouldOpen.maxSpendable) {
this.log("invoice of amount", decoded.numSatoshis, "exceeds user balance of", shouldOpen.maxSpendable)
return null
}
const relativeFee = +order.payment.fee_total_sat / this.settings.channelThreshold
if (relativeFee > this.settings.maxRelativeFee) {
this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee)
return null
}
const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice)
this.log("paid", res.amount_paid, "to open channel")
return { orderId: order.order_id, invoice: order.payment.bolt11_invoice, totalSats: +order.payment.order_total_sat, fees: +order.payment.fee_total_sat }
}
getInfo = async () => {
const res = await fetch(`${this.settings.olympusServiceUrl}/get_info`)
const json = await res.json() as { options: { min_initial_client_balance_sat: string, max_channel_expiry_blocks: number }, uris: string[] }
return json
}
createOrder = async (orderInfo: { pubKey: string, refundAddr: string, lspBalance: string, clientBalance: string, chanExpiryBlocks: number }) => {
const req: OlympusOrder = {
public_key: orderInfo.pubKey,
announce_channel: true,
refund_onchain_address: orderInfo.refundAddr,
lsp_balance_sat: orderInfo.lspBalance,
client_balance_sat: orderInfo.clientBalance,
channel_expiry_blocks: orderInfo.chanExpiryBlocks,
funding_confirms_within_blocks: 6,
required_channel_confirmations: 0
}
const res = await fetch(`${this.settings.olympusServiceUrl}/create_order`, {
method: "POST",
body: JSON.stringify(req),
headers: { "Content-Type": "application/json" }
})
const json = await res.json() as { order_id: string, payment: { state: 'EXPECT_PAYMENT', bolt11_invoice: string, fee_total_sat: string, order_total_sat: string } }
return json
}
getOrder = async (orderId: string) => {
const res = await fetch(`${this.settings.olympusServiceUrl}/get_order&order_id=${orderId}`)
const json = await res.json() as {}
return json
}
}
export class VoltageLSP extends LSP {
constructor(settings: LSPSettings, lnd: LND, liquidityProvider: LiquidityProvider) {
super("VoltageLSP", settings, lnd, liquidityProvider)
}
getInfo = async () => {
const res = await fetch(`${this.settings.voltageServiceUrl}/info`)
const json = await res.json() as { connection_methods: { address: string, port: string, type: string }[], pubkey: string }
return json
}
getFees = async (amtMsat: number, pubkey: string) => {
const res = await fetch(`${this.settings.voltageServiceUrl}/fee`, {
method: "POST",
body: JSON.stringify({ amount_msat: amtMsat, pubkey }),
headers: { "Content-Type": "application/json" }
})
const resText = await res.text()
this.log("fee response", resText)
const json = JSON.parse(resText) as { fee_amount_msat: number, id: string }
return json
}
openChannelIfReady = async (): Promise<OrderResponse | null> => {
const shouldOpen = await this.shouldOpenChannel()
if (!shouldOpen.shouldOpen) {
return null
}
if (!this.settings.voltageServiceUrl) {
this.log("no voltage service url provided")
return null
}
const lndInfo = await this.lnd.GetInfo()
const myPub = lndInfo.identityPubkey
const amtMsats = this.settings.channelThreshold * 1000
const fee = await this.getFees(amtMsats, myPub)
const feeSats = fee.fee_amount_msat / 1000
const relativeFee = feeSats / this.settings.channelThreshold
if (relativeFee > this.settings.maxRelativeFee) {
this.log("relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee)
return null
}
const info = await this.getInfo()
const ipv4 = info.connection_methods.find(c => c.type === 'ipv4')
if (!ipv4) {
this.log("no ipv4 address found")
return null
}
await this.addPeer(info.pubkey, `${ipv4.address}:${ipv4.port}`)
const invoice = await this.lnd.NewInvoice(this.settings.channelThreshold, "open channel", 60 * 60)
const res = await this.proposal(invoice.payRequest, fee.id)
const decoded = await this.lnd.DecodeInvoice(res.jit_bolt11)
if (decoded.numSatoshis !== this.settings.channelThreshold + feeSats) {
this.log("invoice of amount", decoded.numSatoshis, "does not match expected amount of", this.settings.channelThreshold + feeSats)
return null
}
const invoiceRes = await this.liquidityProvider.PayInvoice(res.jit_bolt11)
this.log("paid", invoiceRes.amount_paid, "to open channel")
return { orderId: fee.id, invoice: res.jit_bolt11, totalSats: decoded.numSatoshis, fees: feeSats }
}
proposal = async (bolt11: string, feeId: string) => {
const res = await fetch(`${this.settings.voltageServiceUrl}/proposal`, {
method: "POST",
body: JSON.stringify({ bolt11, fee_id: feeId }),
headers: { "Content-Type": "application/json" }
})
const json = await res.json() as { jit_bolt11: string }
return json
}
} }

View file

@ -1,60 +1,58 @@
import { HtlcEvent } from "../../../proto/lnd/router" import { HtlcEvent } from "../../../proto/lnd/router"
export type NodeSettings = { export type NodeSettings = {
lndAddr: string lndAddr: string
lndCertPath: string lndCertPath: string
lndMacaroonPath: string lndMacaroonPath: string
} }
export type LndSettings = { export type LndSettings = {
mainNode: NodeSettings mainNode: NodeSettings
feeRateLimit: number feeRateLimit: number
feeFixedLimit: number feeFixedLimit: number
mockLnd: boolean mockLnd: boolean
liquidityProviderPub: string
useOnlyLiquidityProvider: boolean otherNode?: NodeSettings
thirdNode?: NodeSettings
otherNode?: NodeSettings }
thirdNode?: NodeSettings type TxOutput = {
} hash: string
type TxOutput = { index: number
hash: string }
index: number export type ChannelBalance = {
} channelId: string;
export type ChannelBalance = { localBalanceSats: number;
channelId: string; remoteBalanceSats: number;
localBalanceSats: number; htlcs: { incoming: boolean, amount: number }[]
remoteBalanceSats: number; }
htlcs: { incoming: boolean, amount: number }[] export type BalanceInfo = {
} confirmedBalance: number;
export type BalanceInfo = { unconfirmedBalance: number;
confirmedBalance: number; totalBalance: number;
unconfirmedBalance: number; channelsBalance: ChannelBalance[];
totalBalance: number; }
channelsBalance: ChannelBalance[];
} export type AddressPaidCb = (txOutput: TxOutput, address: string, amount: number, internal: boolean) => void
export type InvoicePaidCb = (paymentRequest: string, amount: number, internal: boolean) => void
export type AddressPaidCb = (txOutput: TxOutput, address: string, amount: number, internal: boolean) => void export type NewBlockCb = (height: number) => void
export type InvoicePaidCb = (paymentRequest: string, amount: number, internal: boolean) => void export type HtlcCb = (event: HtlcEvent) => void
export type NewBlockCb = (height: number) => void
export type HtlcCb = (event: HtlcEvent) => void export type NodeInfo = {
alias: string
export type NodeInfo = { syncedToChain: boolean
alias: string syncedToGraph: boolean
syncedToChain: boolean blockHeight: number
syncedToGraph: boolean blockHash: string
blockHeight: number identityPubkey: string
blockHash: string uris: string[]
identityPubkey: string }
uris: string[] export type Invoice = {
} payRequest: string
export type Invoice = { }
payRequest: string export type DecodedInvoice = {
} numSatoshis: number
export type DecodedInvoice = { paymentHash: string
numSatoshis: number }
paymentHash: string export type PaidInvoice = {
} feeSat: number
export type PaidInvoice = { valueSat: number
feeSat: number paymentPreimage: string
valueSat: number
paymentPreimage: string
} }

View file

@ -1,253 +1,259 @@
import fetch from "node-fetch" import fetch from "node-fetch"
import Storage, { LoadStorageSettingsFromEnv } from '../storage/index.js' import Storage, { LoadStorageSettingsFromEnv } from '../storage/index.js'
import * as Types from '../../../proto/autogenerated/ts/types.js' import * as Types from '../../../proto/autogenerated/ts/types.js'
import ProductManager from './productManager.js' import ProductManager from './productManager.js'
import ApplicationManager from './applicationManager.js' import ApplicationManager from './applicationManager.js'
import PaymentManager, { PendingTx } from './paymentManager.js' import PaymentManager, { PendingTx } from './paymentManager.js'
import { MainSettings } from './settings.js' import { MainSettings } from './settings.js'
import LND from "../lnd/lnd.js" import LND from "../lnd/lnd.js"
import { AddressPaidCb, HtlcCb, InvoicePaidCb, NewBlockCb } from "../lnd/settings.js" import { AddressPaidCb, HtlcCb, InvoicePaidCb, NewBlockCb } from "../lnd/settings.js"
import { ERROR, getLogger, PubLogger } from "../helpers/logger.js" import { ERROR, getLogger, PubLogger } from "../helpers/logger.js"
import AppUserManager from "./appUserManager.js" import AppUserManager from "./appUserManager.js"
import { Application } from '../storage/entity/Application.js' import { Application } from '../storage/entity/Application.js'
import { UserReceivingInvoice } from '../storage/entity/UserReceivingInvoice.js' import { UserReceivingInvoice } from '../storage/entity/UserReceivingInvoice.js'
import { UnsignedEvent } from '../nostr/tools/event.js' import { UnsignedEvent } from '../nostr/tools/event.js'
import { NostrSend } from '../nostr/handler.js' 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 "./liquidityManager.js"
type UserOperationsSub = {
id: string type UserOperationsSub = {
newIncomingInvoice: (operation: Types.UserOperation) => void id: string
newOutgoingInvoice: (operation: Types.UserOperation) => void newIncomingInvoice: (operation: Types.UserOperation) => void
newIncomingTx: (operation: Types.UserOperation) => void newOutgoingInvoice: (operation: Types.UserOperation) => void
newOutgoingTx: (operation: Types.UserOperation) => void newIncomingTx: (operation: Types.UserOperation) => void
} newOutgoingTx: (operation: Types.UserOperation) => void
const appTag = "Lightning.Pub" }
export default class { const appTag = "Lightning.Pub"
storage: Storage export default class {
lnd: LND storage: Storage
settings: MainSettings lnd: LND
userOperationsSub: UserOperationsSub | null = null settings: MainSettings
productManager: ProductManager userOperationsSub: UserOperationsSub | null = null
applicationManager: ApplicationManager productManager: ProductManager
appUserManager: AppUserManager applicationManager: ApplicationManager
paymentManager: PaymentManager appUserManager: AppUserManager
paymentSubs: Record<string, ((op: Types.UserOperation) => void) | null> = {} paymentManager: PaymentManager
metricsManager: MetricsManager paymentSubs: Record<string, ((op: Types.UserOperation) => void) | null> = {}
liquidProvider: LiquidityProvider metricsManager: MetricsManager
nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") } liquidProvider: LiquidityProvider
constructor(settings: MainSettings, storage: Storage) { liquidityManager: LiquidityManager
this.settings = settings nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") }
this.storage = storage constructor(settings: MainSettings, storage: Storage) {
this.liquidProvider = new LiquidityProvider(settings.lndSettings.liquidityProviderPub, this.invoicePaidCb) this.settings = settings
this.lnd = new LND(settings.lndSettings, this.liquidProvider, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb) this.storage = storage
this.metricsManager = new MetricsManager(this.storage, this.lnd) this.liquidProvider = new LiquidityProvider(settings.liquiditySettings.liquidityProviderPub, this.invoicePaidCb)
const provider = { liquidProvider: this.liquidProvider, useOnly: settings.liquiditySettings.useOnlyLiquidityProvider }
this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings, this.addressPaidCb, this.invoicePaidCb) this.lnd = new LND(settings.lndSettings, provider, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb)
this.productManager = new ProductManager(this.storage, this.paymentManager, this.settings) this.liquidityManager = new LiquidityManager(this.settings.liquiditySettings, this.storage, this.liquidProvider, this.lnd)
this.applicationManager = new ApplicationManager(this.storage, this.settings, this.paymentManager) this.metricsManager = new MetricsManager(this.storage, this.lnd)
this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager)
this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings, this.liquidityManager, this.addressPaidCb, this.invoicePaidCb)
} this.productManager = new ProductManager(this.storage, this.paymentManager, this.settings)
Stop() { this.applicationManager = new ApplicationManager(this.storage, this.settings, this.paymentManager)
this.lnd.Stop() this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager)
this.applicationManager.Stop() }
this.paymentManager.Stop()
} Stop() {
this.lnd.Stop()
StartBeacons() { this.applicationManager.Stop()
this.applicationManager.StartAppsServiceBeacon(app => { this.paymentManager.Stop()
this.UpdateBeacon(app, { type: 'service', name: app.name }) }
})
} StartBeacons() {
this.applicationManager.StartAppsServiceBeacon(app => {
attachNostrSend(f: NostrSend) { this.UpdateBeacon(app, { type: 'service', name: app.name })
this.nostrSend = f })
this.liquidProvider.attachNostrSend(f) }
}
attachNostrSend(f: NostrSend) {
htlcCb: HtlcCb = (e) => { this.nostrSend = f
this.metricsManager.HtlcCb(e) this.liquidProvider.attachNostrSend(f)
} }
newBlockCb: NewBlockCb = (height) => { htlcCb: HtlcCb = (e) => {
this.NewBlockHandler(height) this.metricsManager.HtlcCb(e)
} }
NewBlockHandler = async (height: number) => { newBlockCb: NewBlockCb = (height) => {
let confirmed: (PendingTx & { confs: number; })[] this.NewBlockHandler(height)
let log = getLogger({}) }
try { NewBlockHandler = async (height: number) => {
const balanceEvents = await this.paymentManager.GetLndBalance() let confirmed: (PendingTx & { confs: number; })[]
await this.metricsManager.NewBlockCb(height, balanceEvents) let log = getLogger({})
confirmed = await this.paymentManager.CheckNewlyConfirmedTxs(height)
} catch (err: any) { try {
log(ERROR, "failed to check transactions after new block", err.message || err) const balanceEvents = await this.paymentManager.GetLndBalance()
return await this.metricsManager.NewBlockCb(height, balanceEvents)
} confirmed = await this.paymentManager.CheckNewlyConfirmedTxs(height)
await Promise.all(confirmed.map(async c => { this.liquidityManager.onNewBlock()
if (c.type === 'outgoing') { } catch (err: any) {
await this.storage.paymentStorage.UpdateUserTransactionPayment(c.tx.serial_id, { confs: c.confs }) log(ERROR, "failed to check transactions after new block", err.message || err)
const { linkedApplication, user, address, paid_amount: amount, service_fees: serviceFee, serial_id: serialId, chain_fees } = c.tx; return
const operationId = `${Types.UserOperationType.OUTGOING_TX}-${serialId}` }
const op = { amount, paidAtUnix: Date.now() / 1000, inbound: false, type: Types.UserOperationType.OUTGOING_TX, identifier: address, operationId, network_fee: chain_fees, service_fee: serviceFee, confirmed: true, tx_hash: c.tx.tx_hash, internal: c.tx.internal } await Promise.all(confirmed.map(async c => {
this.sendOperationToNostr(linkedApplication!, user.user_id, op) if (c.type === 'outgoing') {
} else { await this.storage.paymentStorage.UpdateUserTransactionPayment(c.tx.serial_id, { confs: c.confs })
this.storage.StartTransaction(async tx => { const { linkedApplication, user, address, paid_amount: amount, service_fees: serviceFee, serial_id: serialId, chain_fees } = c.tx;
const { user_address: userAddress, paid_amount: amount, service_fee: serviceFee, serial_id: serialId, tx_hash } = c.tx const operationId = `${Types.UserOperationType.OUTGOING_TX}-${serialId}`
if (!userAddress.linkedApplication) { const op = { amount, paidAtUnix: Date.now() / 1000, inbound: false, type: Types.UserOperationType.OUTGOING_TX, identifier: address, operationId, network_fee: chain_fees, service_fee: serviceFee, confirmed: true, tx_hash: c.tx.tx_hash, internal: c.tx.internal }
log(ERROR, "an address was paid, that has no linked application") this.sendOperationToNostr(linkedApplication!, user.user_id, op)
return } else {
} this.storage.StartTransaction(async tx => {
const updateResult = await this.storage.paymentStorage.UpdateAddressReceivingTransaction(serialId, { confs: c.confs }, tx) const { user_address: userAddress, paid_amount: amount, service_fee: serviceFee, serial_id: serialId, tx_hash } = c.tx
if (!updateResult.affected) { if (!userAddress.linkedApplication) {
throw new Error("unable to flag chain transaction as paid") log(ERROR, "an address was paid, that has no linked application")
} return
const addressData = `${userAddress.address}:${tx_hash}` }
this.storage.eventsLog.LogEvent({ type: 'address_paid', userId: userAddress.user.user_id, appId: userAddress.linkedApplication.app_id, appUserId: "", balance: userAddress.user.balance_sats, data: addressData, amount }) const updateResult = await this.storage.paymentStorage.UpdateAddressReceivingTransaction(serialId, { confs: c.confs }, tx)
await this.storage.userStorage.IncrementUserBalance(userAddress.user.user_id, amount - serviceFee, addressData, tx) if (!updateResult.affected) {
if (serviceFee > 0) { throw new Error("unable to flag chain transaction as paid")
await this.storage.userStorage.IncrementUserBalance(userAddress.linkedApplication.owner.user_id, serviceFee, 'fees', tx) }
} const addressData = `${userAddress.address}:${tx_hash}`
const operationId = `${Types.UserOperationType.INCOMING_TX}-${serialId}` this.storage.eventsLog.LogEvent({ type: 'address_paid', userId: userAddress.user.user_id, appId: userAddress.linkedApplication.app_id, appUserId: "", balance: userAddress.user.balance_sats, data: addressData, amount })
const op = { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_TX, identifier: userAddress.address, operationId, network_fee: 0, service_fee: serviceFee, confirmed: true, tx_hash: c.tx.tx_hash, internal: c.tx.internal } await this.storage.userStorage.IncrementUserBalance(userAddress.user.user_id, amount - serviceFee, addressData, tx)
this.sendOperationToNostr(userAddress.linkedApplication!, userAddress.user.user_id, op) if (serviceFee > 0) {
}) await this.storage.userStorage.IncrementUserBalance(userAddress.linkedApplication.owner.user_id, serviceFee, 'fees', tx)
} }
})) const operationId = `${Types.UserOperationType.INCOMING_TX}-${serialId}`
} const op = { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_TX, identifier: userAddress.address, operationId, network_fee: 0, service_fee: serviceFee, confirmed: true, tx_hash: c.tx.tx_hash, internal: c.tx.internal }
this.sendOperationToNostr(userAddress.linkedApplication!, userAddress.user.user_id, op)
addressPaidCb: AddressPaidCb = (txOutput, address, amount, internal) => { })
this.storage.StartTransaction(async tx => { }
const { blockHeight } = await this.lnd.GetInfo() }))
const userAddress = await this.storage.paymentStorage.GetAddressOwner(address, tx) }
if (!userAddress) { return }
let log = getLogger({}) addressPaidCb: AddressPaidCb = (txOutput, address, amount, internal) => {
if (!userAddress.linkedApplication) { this.storage.StartTransaction(async tx => {
log(ERROR, "an address was paid, that has no linked application") const { blockHeight } = await this.lnd.GetInfo()
return const userAddress = await this.storage.paymentStorage.GetAddressOwner(address, tx)
} if (!userAddress) { return }
log = getLogger({ appName: userAddress.linkedApplication.name }) let log = getLogger({})
const isAppUserPayment = userAddress.user.user_id !== userAddress.linkedApplication.owner.user_id if (!userAddress.linkedApplication) {
let fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_TX, amount, isAppUserPayment) log(ERROR, "an address was paid, that has no linked application")
if (userAddress.linkedApplication && userAddress.linkedApplication.owner.user_id === userAddress.user.user_id) { return
fee = 0 }
} log = getLogger({ appName: userAddress.linkedApplication.name })
try { const isAppUserPayment = userAddress.user.user_id !== userAddress.linkedApplication.owner.user_id
// This call will fail if the transaction is already registered let fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_TX, amount, isAppUserPayment)
const addedTx = await this.storage.paymentStorage.AddAddressReceivingTransaction(userAddress, txOutput.hash, txOutput.index, amount, fee, internal, blockHeight, tx) if (userAddress.linkedApplication && userAddress.linkedApplication.owner.user_id === userAddress.user.user_id) {
if (internal) { fee = 0
const addressData = `${address}:${txOutput.hash}` }
this.storage.eventsLog.LogEvent({ type: 'address_paid', userId: userAddress.user.user_id, appId: userAddress.linkedApplication.app_id, appUserId: "", balance: userAddress.user.balance_sats, data: addressData, amount }) try {
await this.storage.userStorage.IncrementUserBalance(userAddress.user.user_id, addedTx.paid_amount - fee, addressData, tx) // This call will fail if the transaction is already registered
if (fee > 0) { const addedTx = await this.storage.paymentStorage.AddAddressReceivingTransaction(userAddress, txOutput.hash, txOutput.index, amount, fee, internal, blockHeight, tx)
await this.storage.userStorage.IncrementUserBalance(userAddress.linkedApplication.owner.user_id, fee, 'fees', tx) if (internal) {
} const addressData = `${address}:${txOutput.hash}`
this.storage.eventsLog.LogEvent({ type: 'address_paid', userId: userAddress.user.user_id, appId: userAddress.linkedApplication.app_id, appUserId: "", balance: userAddress.user.balance_sats, data: addressData, amount })
} await this.storage.userStorage.IncrementUserBalance(userAddress.user.user_id, addedTx.paid_amount - fee, addressData, tx)
const operationId = `${Types.UserOperationType.INCOMING_TX}-${addedTx.serial_id}` if (fee > 0) {
const op = { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_TX, identifier: userAddress.address, operationId, network_fee: 0, service_fee: fee, confirmed: internal, tx_hash: txOutput.hash, internal: false } await this.storage.userStorage.IncrementUserBalance(userAddress.linkedApplication.owner.user_id, fee, 'fees', tx)
this.sendOperationToNostr(userAddress.linkedApplication, userAddress.user.user_id, op) }
} catch {
log(ERROR, "cannot process address paid transaction, already registered") }
} const operationId = `${Types.UserOperationType.INCOMING_TX}-${addedTx.serial_id}`
}) const op = { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_TX, identifier: userAddress.address, operationId, network_fee: 0, service_fee: fee, confirmed: internal, tx_hash: txOutput.hash, internal: false }
} this.sendOperationToNostr(userAddress.linkedApplication, userAddress.user.user_id, op)
} catch {
invoicePaidCb: InvoicePaidCb = (paymentRequest, amount, internal) => { log(ERROR, "cannot process address paid transaction, already registered")
this.storage.StartTransaction(async tx => { }
let log = getLogger({}) })
const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(paymentRequest, tx) }
if (!userInvoice) { return }
if (userInvoice.paid_at_unix > 0 && internal) { log("cannot pay internally, invoice already paid"); return } invoicePaidCb: InvoicePaidCb = (paymentRequest, amount, internal) => {
if (userInvoice.paid_at_unix > 0 && !internal && userInvoice.paidByLnd) { log("invoice already paid by lnd"); return } this.storage.StartTransaction(async tx => {
if (!userInvoice.linkedApplication) { let log = getLogger({})
log(ERROR, "an invoice was paid, that has no linked application") const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(paymentRequest, tx)
return if (!userInvoice) { return }
} if (userInvoice.paid_at_unix > 0 && internal) { log("cannot pay internally, invoice already paid"); return }
log = getLogger({ appName: userInvoice.linkedApplication.name }) if (userInvoice.paid_at_unix > 0 && !internal && userInvoice.paidByLnd) { log("invoice already paid by lnd"); return }
const isAppUserPayment = userInvoice.user.user_id !== userInvoice.linkedApplication.owner.user_id if (!userInvoice.linkedApplication) {
let fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_INVOICE, amount, isAppUserPayment) log(ERROR, "an invoice was paid, that has no linked application")
if (userInvoice.linkedApplication && userInvoice.linkedApplication.owner.user_id === userInvoice.user.user_id) { return
fee = 0 }
} log = getLogger({ appName: userInvoice.linkedApplication.name })
try { const isAppUserPayment = userInvoice.user.user_id !== userInvoice.linkedApplication.owner.user_id
await this.storage.paymentStorage.FlagInvoiceAsPaid(userInvoice, amount, fee, internal, tx) let fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_INVOICE, amount, isAppUserPayment)
this.storage.eventsLog.LogEvent({ type: 'invoice_paid', userId: userInvoice.user.user_id, appId: userInvoice.linkedApplication.app_id, appUserId: "", balance: userInvoice.user.balance_sats, data: paymentRequest, amount }) if (userInvoice.linkedApplication && userInvoice.linkedApplication.owner.user_id === userInvoice.user.user_id) {
await this.storage.userStorage.IncrementUserBalance(userInvoice.user.user_id, amount - fee, userInvoice.invoice, tx) fee = 0
if (fee > 0) { }
await this.storage.userStorage.IncrementUserBalance(userInvoice.linkedApplication.owner.user_id, fee, 'fees', tx) try {
} await this.storage.paymentStorage.FlagInvoiceAsPaid(userInvoice, amount, fee, internal, tx)
await this.triggerPaidCallback(log, userInvoice.callbackUrl) this.storage.eventsLog.LogEvent({ type: 'invoice_paid', userId: userInvoice.user.user_id, appId: userInvoice.linkedApplication.app_id, appUserId: "", balance: userInvoice.user.balance_sats, data: paymentRequest, amount })
const operationId = `${Types.UserOperationType.INCOMING_INVOICE}-${userInvoice.serial_id}` await this.storage.userStorage.IncrementUserBalance(userInvoice.user.user_id, amount - fee, userInvoice.invoice, tx)
const op = { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_INVOICE, identifier: userInvoice.invoice, operationId, network_fee: 0, service_fee: fee, confirmed: true, tx_hash: "", internal } if (fee > 0) {
this.sendOperationToNostr(userInvoice.linkedApplication, userInvoice.user.user_id, op) await this.storage.userStorage.IncrementUserBalance(userInvoice.linkedApplication.owner.user_id, fee, 'fees', tx)
this.createZapReceipt(log, userInvoice) }
log("paid invoice processed successfully") await this.triggerPaidCallback(log, userInvoice.callbackUrl)
} catch (err: any) { const operationId = `${Types.UserOperationType.INCOMING_INVOICE}-${userInvoice.serial_id}`
log(ERROR, "cannot process paid invoice", err.message || "") const op = { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_INVOICE, identifier: userInvoice.invoice, operationId, network_fee: 0, service_fee: fee, confirmed: true, tx_hash: "", internal }
} this.sendOperationToNostr(userInvoice.linkedApplication, userInvoice.user.user_id, op)
}) this.createZapReceipt(log, userInvoice)
} log("paid invoice processed successfully")
this.liquidityManager.afterInInvoicePaid()
async triggerPaidCallback(log: PubLogger, url: string) { } catch (err: any) {
if (!url) { log(ERROR, "cannot process paid invoice", err.message || "")
return }
} })
try { }
await fetch(url + "&ok=true")
} catch (err: any) { async triggerPaidCallback(log: PubLogger, url: string) {
log(ERROR, "error sending paid callback for invoice", err.message || "") if (!url) {
} return
} }
try {
async sendOperationToNostr(app: Application, userId: string, op: Types.UserOperation) { await fetch(url + "&ok=true")
const user = await this.storage.applicationStorage.GetAppUserFromUser(app, userId) } catch (err: any) {
if (!user || !user.nostr_public_key) { log(ERROR, "error sending paid callback for invoice", err.message || "")
getLogger({ appName: app.name })("cannot notify user, not a nostr user") }
return }
}
const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' } async sendOperationToNostr(app: Application, userId: string, op: Types.UserOperation) {
this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'content', content: JSON.stringify(message), pub: user.nostr_public_key }) const user = await this.storage.applicationStorage.GetAppUserFromUser(app, userId)
} if (!user || !user.nostr_public_key) {
getLogger({ appName: app.name })("cannot notify user, not a nostr user")
async UpdateBeacon(app: Application, content: { type: 'service', name: string }) { return
if (!app.nostr_public_key) { }
getLogger({ appName: app.name })("cannot update beacon, public key not set") const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' }
return this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'content', content: JSON.stringify(message), pub: user.nostr_public_key })
} }
const tags = [["d", appTag]]
const event: UnsignedEvent = { async UpdateBeacon(app: Application, content: { type: 'service', name: string }) {
content: JSON.stringify(content), if (!app.nostr_public_key) {
created_at: Math.floor(Date.now() / 1000), getLogger({ appName: app.name })("cannot update beacon, public key not set")
kind: 30078, return
pubkey: app.nostr_public_key, }
tags, const tags = [["d", appTag]]
} const event: UnsignedEvent = {
this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'event', event }) content: JSON.stringify(content),
} created_at: Math.floor(Date.now() / 1000),
kind: 30078,
async createZapReceipt(log: PubLogger, invoice: UserReceivingInvoice) { pubkey: app.nostr_public_key,
const zapInfo = invoice.zap_info tags,
if (!zapInfo || !invoice.linkedApplication || !invoice.linkedApplication.nostr_public_key) { }
log(ERROR, "no zap info linked to payment") this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'event', event })
return }
}
const tags = [["p", zapInfo.pub], ["bolt11", invoice.invoice], ["description", zapInfo.description]] async createZapReceipt(log: PubLogger, invoice: UserReceivingInvoice) {
if (zapInfo.eventId) { const zapInfo = invoice.zap_info
tags.push(["e", zapInfo.eventId]) if (!zapInfo || !invoice.linkedApplication || !invoice.linkedApplication.nostr_public_key) {
} log(ERROR, "no zap info linked to payment")
const event: UnsignedEvent = { return
content: "", }
created_at: invoice.paid_at_unix, const tags = [["p", zapInfo.pub], ["bolt11", invoice.invoice], ["description", zapInfo.description]]
kind: 9735, if (zapInfo.eventId) {
pubkey: invoice.linkedApplication.nostr_public_key, tags.push(["e", zapInfo.eventId])
tags, }
} const event: UnsignedEvent = {
log({ unsigned: event }) content: "",
this.nostrSend({ type: 'app', appId: invoice.linkedApplication.app_id }, { type: 'event', event }) created_at: invoice.paid_at_unix,
} kind: 9735,
} pubkey: invoice.linkedApplication.nostr_public_key,
tags,
}
log({ unsigned: event })
this.nostrSend({ type: 'app', appId: invoice.linkedApplication.app_id }, { type: 'event', event })
}
}

View file

@ -0,0 +1,106 @@
import { getLogger } from "../helpers/logger.js"
import { LiquidityProvider } from "../lnd/liquidityProvider.js"
import LND from "../lnd/lnd.js"
import { FlashsatsLSP, LoadLSPSettingsFromEnv, LSPSettings, OlympusLSP, VoltageLSP } from "../lnd/lsp.js"
import Storage from '../storage/index.js'
import { defaultInvoiceExpiry } from "../storage/paymentStorage.js"
export type LiquiditySettings = {
lspSettings: LSPSettings
liquidityProviderPub: string
useOnlyLiquidityProvider: boolean
}
export const LoadLiquiditySettingsFromEnv = (): LiquiditySettings => {
const lspSettings = LoadLSPSettingsFromEnv()
const liquidityProviderPub = process.env.LIQUIDITY_PROVIDER_PUB || ""
return { lspSettings, liquidityProviderPub, useOnlyLiquidityProvider: false }
}
export class LiquidityManager {
settings: LiquiditySettings
storage: Storage
liquidityProvider: LiquidityProvider
lnd: LND
olympusLSP: OlympusLSP
voltageLSP: VoltageLSP
flashsatsLSP: FlashsatsLSP
log = getLogger({ component: "liquidityManager" })
channelRequested = false
channelRequesting = false
constructor(settings: LiquiditySettings, storage: Storage, liquidityProvider: LiquidityProvider, lnd: LND) {
this.settings = settings
this.storage = storage
this.liquidityProvider = liquidityProvider
this.lnd = lnd
this.olympusLSP = new OlympusLSP(settings.lspSettings, lnd, liquidityProvider)
this.voltageLSP = new VoltageLSP(settings.lspSettings, lnd, liquidityProvider)
this.flashsatsLSP = new FlashsatsLSP(settings.lspSettings, lnd, liquidityProvider)
}
onNewBlock = async () => {
const balance = await this.liquidityProvider.GetLatestMaxWithdrawable()
const { remote } = await this.lnd.ChannelBalance()
if (remote > balance) {
this.log("draining provider balance to channel")
const invoice = await this.lnd.NewInvoice(balance, "liqudity provider drain", defaultInvoiceExpiry)
const res = await this.liquidityProvider.PayInvoice(invoice.payRequest)
this.log("drained provider balance to channel", res.amount_paid)
}
}
beforeInvoiceCreation = async (amount: number): Promise<'lnd' | 'provider'> => {
const { remote } = await this.lnd.ChannelBalance()
if (remote > amount) {
this.log("channel has enough balance for invoice")
return 'lnd'
}
this.log("channel does not have enough balance for invoice,suggesting provider")
return 'provider'
}
afterInInvoicePaid = async () => {
const existingOrder = await this.storage.liquidityStorage.GetLatestLspOrder()
if (existingOrder) {
return
}
if (this.channelRequested || this.channelRequesting) {
return
}
this.channelRequesting = true
this.log("checking if channel should be requested")
const olympusOk = await this.olympusLSP.openChannelIfReady()
if (olympusOk) {
this.log("requested channel from olympus")
this.channelRequested = true
this.channelRequesting = false
await this.storage.liquidityStorage.SaveLspOrder({ service_name: 'olympus', invoice: olympusOk.invoice, total_paid: olympusOk.totalSats, order_id: olympusOk.orderId, fees: olympusOk.fees })
return
}
const voltageOk = await this.voltageLSP.openChannelIfReady()
if (voltageOk) {
this.log("requested channel from voltage")
this.channelRequested = true
this.channelRequesting = false
await this.storage.liquidityStorage.SaveLspOrder({ service_name: 'voltage', invoice: voltageOk.invoice, total_paid: voltageOk.totalSats, order_id: voltageOk.orderId, fees: voltageOk.fees })
return
}
const flashsatsOk = await this.flashsatsLSP.openChannelIfReady()
if (flashsatsOk) {
this.log("requested channel from flashsats")
this.channelRequested = true
this.channelRequesting = false
await this.storage.liquidityStorage.SaveLspOrder({ service_name: 'flashsats', invoice: flashsatsOk.invoice, total_paid: flashsatsOk.totalSats, order_id: flashsatsOk.orderId, fees: flashsatsOk.fees })
return
}
this.channelRequesting = false
this.log("no channel requested")
}
beforeOutInvoicePayment = async (amount: number): Promise<'lnd' | 'provider'> => {
const balance = await this.liquidityProvider.GetLatestMaxWithdrawable(true)
if (balance > amount) {
this.log("provider has enough balance for payment")
return 'provider'
}
this.log("provider does not have enough balance for payment, suggesting lnd")
return 'lnd'
}
afterOutInvoicePaid = async () => { }
}

File diff suppressed because it is too large Load diff

View file

@ -1,106 +1,114 @@
import { LoadStorageSettingsFromEnv, StorageSettings } from '../storage/index.js' import { LoadStorageSettingsFromEnv, StorageSettings } from '../storage/index.js'
import { LndSettings, NodeSettings } from '../lnd/settings.js' import { LndSettings, NodeSettings } from '../lnd/settings.js'
import { LoadWatchdogSettingsFromEnv, WatchdogSettings } from './watchdog.js' import { LoadWatchdogSettingsFromEnv, WatchdogSettings } from './watchdog.js'
import { LoadLndSettingsFromEnv } from '../lnd/index.js' import { LoadLndSettingsFromEnv } from '../lnd/index.js'
import { EnvCanBeInteger, EnvMustBeInteger, EnvMustBeNonEmptyString } from '../helpers/envParser.js' import { EnvCanBeInteger, EnvMustBeInteger, EnvMustBeNonEmptyString } from '../helpers/envParser.js'
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';
export type MainSettings = { import { LiquiditySettings, LoadLiquiditySettingsFromEnv } from './liquidityManager.js'
storageSettings: StorageSettings, export type MainSettings = {
lndSettings: LndSettings, storageSettings: StorageSettings,
watchDogSettings: WatchdogSettings, lndSettings: LndSettings,
jwtSecret: string watchDogSettings: WatchdogSettings,
incomingTxFee: number liquiditySettings: LiquiditySettings,
outgoingTxFee: number jwtSecret: string
incomingAppInvoiceFee: number incomingTxFee: number
incomingAppUserInvoiceFee: number outgoingTxFee: number
outgoingAppInvoiceFee: number incomingAppInvoiceFee: number
outgoingAppUserInvoiceFee: number incomingAppUserInvoiceFee: number
userToUserFee: number outgoingAppInvoiceFee: number
appToUserFee: number outgoingAppUserInvoiceFee: number
serviceUrl: string userToUserFee: number
servicePort: number appToUserFee: number
recordPerformance: boolean serviceUrl: string
skipSanityCheck: boolean servicePort: number
disableExternalPayments: boolean recordPerformance: boolean
} skipSanityCheck: boolean
export type BitcoinCoreSettings = { disableExternalPayments: boolean
port: number }
user: string export type BitcoinCoreSettings = {
pass: string port: number
} user: string
export type TestSettings = MainSettings & { lndSettings: { otherNode: NodeSettings, thirdNode: NodeSettings, fourthNode: NodeSettings }, bitcoinCoreSettings: BitcoinCoreSettings } pass: string
export const LoadMainSettingsFromEnv = (): MainSettings => { }
return { export type TestSettings = MainSettings & { lndSettings: { otherNode: NodeSettings, thirdNode: NodeSettings, fourthNode: NodeSettings }, bitcoinCoreSettings: BitcoinCoreSettings }
watchDogSettings: LoadWatchdogSettingsFromEnv(), export const LoadMainSettingsFromEnv = (): MainSettings => {
lndSettings: LoadLndSettingsFromEnv(), const storageSettings = LoadStorageSettingsFromEnv()
storageSettings: LoadStorageSettingsFromEnv(), return {
jwtSecret: loadJwtSecret(), watchDogSettings: LoadWatchdogSettingsFromEnv(),
incomingTxFee: EnvCanBeInteger("INCOMING_CHAIN_FEE_ROOT_BPS", 0) / 10000, lndSettings: LoadLndSettingsFromEnv(),
outgoingTxFee: EnvCanBeInteger("OUTGOING_CHAIN_FEE_ROOT_BPS", 60) / 10000, storageSettings: storageSettings,
incomingAppInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_ROOT_BPS", 0) / 10000, liquiditySettings: LoadLiquiditySettingsFromEnv(),
outgoingAppInvoiceFee: EnvCanBeInteger("OUTGOING_INVOICE_FEE_ROOT_BPS", 60) / 10000, jwtSecret: loadJwtSecret(storageSettings.dataDir),
incomingAppUserInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_USER_BPS", 0) / 10000, incomingTxFee: EnvCanBeInteger("INCOMING_CHAIN_FEE_ROOT_BPS", 0) / 10000,
outgoingAppUserInvoiceFee: EnvCanBeInteger("OUTGOING_INVOICE_FEE_USER_BPS", 0) / 10000, outgoingTxFee: EnvCanBeInteger("OUTGOING_CHAIN_FEE_ROOT_BPS", 60) / 10000,
userToUserFee: EnvCanBeInteger("TX_FEE_INTERNAL_USER_BPS", 0) / 10000, incomingAppInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_ROOT_BPS", 0) / 10000,
appToUserFee: EnvCanBeInteger("TX_FEE_INTERNAL_ROOT_BPS", 0) / 10000, outgoingAppInvoiceFee: EnvCanBeInteger("OUTGOING_INVOICE_FEE_ROOT_BPS", 60) / 10000,
serviceUrl: process.env.SERVICE_URL || `http://localhost:${EnvCanBeInteger("PORT", 1776)}`, incomingAppUserInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_USER_BPS", 0) / 10000,
servicePort: EnvCanBeInteger("PORT", 1776), outgoingAppUserInvoiceFee: EnvCanBeInteger("OUTGOING_INVOICE_FEE_USER_BPS", 0) / 10000,
recordPerformance: process.env.RECORD_PERFORMANCE === 'true' || false, userToUserFee: EnvCanBeInteger("TX_FEE_INTERNAL_USER_BPS", 0) / 10000,
skipSanityCheck: process.env.SKIP_SANITY_CHECK === 'true' || false, appToUserFee: EnvCanBeInteger("TX_FEE_INTERNAL_ROOT_BPS", 0) / 10000,
disableExternalPayments: process.env.DISABLE_EXTERNAL_PAYMENTS === 'true' || false, 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,
export const LoadTestSettingsFromEnv = (): TestSettings => { disableExternalPayments: process.env.DISABLE_EXTERNAL_PAYMENTS === 'true' || false,
const eventLogPath = `logs/eventLogV2Test${Date.now()}.csv` }
const settings = LoadMainSettingsFromEnv() }
return {
...settings, export const LoadTestSettingsFromEnv = (): TestSettings => {
storageSettings: { dbSettings: { ...settings.storageSettings.dbSettings, databaseFile: ":memory:", metricsDatabaseFile: ":memory:" }, eventLogPath }, const eventLogPath = `logs/eventLogV2Test${Date.now()}.csv`
lndSettings: { const settings = LoadMainSettingsFromEnv()
...settings.lndSettings, return {
otherNode: { ...settings,
lndAddr: EnvMustBeNonEmptyString("LND_OTHER_ADDR"), storageSettings: { dbSettings: { ...settings.storageSettings.dbSettings, databaseFile: ":memory:", metricsDatabaseFile: ":memory:" }, eventLogPath, dataDir: "data" },
lndCertPath: EnvMustBeNonEmptyString("LND_OTHER_CERT_PATH"), lndSettings: {
lndMacaroonPath: EnvMustBeNonEmptyString("LND_OTHER_MACAROON_PATH") ...settings.lndSettings,
}, otherNode: {
thirdNode: { lndAddr: EnvMustBeNonEmptyString("LND_OTHER_ADDR"),
lndAddr: EnvMustBeNonEmptyString("LND_THIRD_ADDR"), lndCertPath: EnvMustBeNonEmptyString("LND_OTHER_CERT_PATH"),
lndCertPath: EnvMustBeNonEmptyString("LND_THIRD_CERT_PATH"), lndMacaroonPath: EnvMustBeNonEmptyString("LND_OTHER_MACAROON_PATH")
lndMacaroonPath: EnvMustBeNonEmptyString("LND_THIRD_MACAROON_PATH") },
}, thirdNode: {
fourthNode: { lndAddr: EnvMustBeNonEmptyString("LND_THIRD_ADDR"),
lndAddr: EnvMustBeNonEmptyString("LND_FOURTH_ADDR"), lndCertPath: EnvMustBeNonEmptyString("LND_THIRD_CERT_PATH"),
lndCertPath: EnvMustBeNonEmptyString("LND_FOURTH_CERT_PATH"), lndMacaroonPath: EnvMustBeNonEmptyString("LND_THIRD_MACAROON_PATH")
lndMacaroonPath: EnvMustBeNonEmptyString("LND_FOURTH_MACAROON_PATH") },
}, fourthNode: {
liquidityProviderPub: "" lndAddr: EnvMustBeNonEmptyString("LND_FOURTH_ADDR"),
}, lndCertPath: EnvMustBeNonEmptyString("LND_FOURTH_CERT_PATH"),
skipSanityCheck: true, lndMacaroonPath: EnvMustBeNonEmptyString("LND_FOURTH_MACAROON_PATH")
bitcoinCoreSettings: { },
port: EnvMustBeInteger("BITCOIN_CORE_PORT"), },
user: EnvMustBeNonEmptyString("BITCOIN_CORE_USER"), liquiditySettings: {
pass: EnvMustBeNonEmptyString("BITCOIN_CORE_PASS") ...settings.liquiditySettings,
}, liquidityProviderPub: "",
} },
} skipSanityCheck: true,
bitcoinCoreSettings: {
export const loadJwtSecret = (): string => { port: EnvMustBeInteger("BITCOIN_CORE_PORT"),
const secret = process.env["JWT_SECRET"] user: EnvMustBeNonEmptyString("BITCOIN_CORE_USER"),
const log = getLogger({}) pass: EnvMustBeNonEmptyString("BITCOIN_CORE_PASS")
if (secret) { },
return secret }
} }
log("JWT_SECRET not set in env, checking .jwt_secret file")
try { export const loadJwtSecret = (dataDir: string): string => {
const fileContent = fs.readFileSync(".jwt_secret", "utf-8") const secret = process.env["JWT_SECRET"]
return fileContent.trim() const log = getLogger({})
} catch (e) { if (secret) {
log(".jwt_secret file not found, generating random secret") return secret
const secret = crypto.randomBytes(32).toString('hex') }
fs.writeFileSync(".jwt_secret", secret) log("JWT_SECRET not set in env, checking .jwt_secret file")
return secret const secretPath = dataDir !== "" ? `${dataDir}/.jwt_secret` : ".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
}
} }

View file

@ -1,179 +1,183 @@
import { EnvCanBeInteger } from "../helpers/envParser.js"; import { EnvCanBeInteger } from "../helpers/envParser.js";
import FunctionQueue from "../helpers/functionQueue.js"; import FunctionQueue from "../helpers/functionQueue.js";
import { getLogger } from "../helpers/logger.js"; import { getLogger } from "../helpers/logger.js";
import LND from "../lnd/lnd.js"; import { LiquidityProvider } from "../lnd/liquidityProvider.js";
import { ChannelBalance } from "../lnd/settings.js"; import LND from "../lnd/lnd.js";
import Storage from '../storage/index.js' import { ChannelBalance } from "../lnd/settings.js";
export type WatchdogSettings = { import Storage from '../storage/index.js'
maxDiffSats: number export type WatchdogSettings = {
} maxDiffSats: number
export const LoadWatchdogSettingsFromEnv = (test = false): WatchdogSettings => { }
return { export const LoadWatchdogSettingsFromEnv = (test = false): WatchdogSettings => {
maxDiffSats: EnvCanBeInteger("WATCHDOG_MAX_DIFF_SATS") return {
} maxDiffSats: EnvCanBeInteger("WATCHDOG_MAX_DIFF_SATS")
} }
export class Watchdog { }
queue: FunctionQueue<void> export class Watchdog {
initialLndBalance: number; queue: FunctionQueue<void>
initialUsersBalance: number; initialLndBalance: number;
startedAtUnix: number; initialUsersBalance: number;
latestIndexOffset: number; startedAtUnix: number;
accumulatedHtlcFees: number; latestIndexOffset: number;
lnd: LND; accumulatedHtlcFees: number;
settings: WatchdogSettings; lnd: LND;
storage: Storage; liquidProvider: LiquidityProvider;
latestCheckStart = 0 settings: WatchdogSettings;
log = getLogger({ component: "watchdog" }) storage: Storage;
ready = false latestCheckStart = 0
interval: NodeJS.Timer; log = getLogger({ component: "watchdog" })
constructor(settings: WatchdogSettings, lnd: LND, storage: Storage) { ready = false
this.lnd = lnd; interval: NodeJS.Timer;
this.settings = settings; constructor(settings: WatchdogSettings, lnd: LND, storage: Storage) {
this.storage = storage; this.lnd = lnd;
this.queue = new FunctionQueue("watchdog_queue", () => this.StartCheck()) this.settings = settings;
} this.storage = storage;
this.liquidProvider = lnd.liquidProvider
Stop() { this.queue = new FunctionQueue("watchdog_queue", () => this.StartCheck())
if (this.interval) { }
clearInterval(this.interval)
} Stop() {
} if (this.interval) {
clearInterval(this.interval)
Start = async () => { }
this.startedAtUnix = Math.floor(Date.now() / 1000) }
const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
this.initialLndBalance = await this.getTotalLndBalance(totalUsersBalance) Start = async () => {
this.initialUsersBalance = totalUsersBalance this.startedAtUnix = Math.floor(Date.now() / 1000)
const fwEvents = await this.lnd.GetForwardingHistory(0, this.startedAtUnix) const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
this.latestIndexOffset = fwEvents.lastOffsetIndex this.initialLndBalance = await this.getTotalLndBalance(totalUsersBalance)
this.accumulatedHtlcFees = 0 this.initialUsersBalance = totalUsersBalance
const fwEvents = await this.lnd.GetForwardingHistory(0, this.startedAtUnix)
this.interval = setInterval(() => { this.latestIndexOffset = fwEvents.lastOffsetIndex
if (this.latestCheckStart + (1000 * 60) < Date.now()) { this.accumulatedHtlcFees = 0
this.log("No balance check was made in the last minute, checking now")
this.PaymentRequested() this.interval = setInterval(() => {
} if (this.latestCheckStart + (1000 * 60) < Date.now()) {
}, 1000 * 60) this.log("No balance check was made in the last minute, checking now")
this.PaymentRequested()
this.ready = true }
} }, 1000 * 60)
updateAccumulatedHtlcFees = async () => { this.ready = true
const fwEvents = await this.lnd.GetForwardingHistory(this.latestIndexOffset, this.startedAtUnix) }
this.latestIndexOffset = fwEvents.lastOffsetIndex
fwEvents.forwardingEvents.forEach((event) => { updateAccumulatedHtlcFees = async () => {
this.accumulatedHtlcFees += Number(event.fee) const fwEvents = await this.lnd.GetForwardingHistory(this.latestIndexOffset, this.startedAtUnix)
}) this.latestIndexOffset = fwEvents.lastOffsetIndex
fwEvents.forwardingEvents.forEach((event) => {
} this.accumulatedHtlcFees += Number(event.fee)
})
}
getTotalLndBalance = async (usersTotal: number) => {
const walletBalance = await this.lnd.GetWalletBalance()
this.log(Number(walletBalance.confirmedBalance), "sats in chain wallet")
const channelsBalance = await this.lnd.GetChannelBalance() getTotalLndBalance = async (usersTotal: number) => {
getLogger({ component: "debugLndBalancev3" })({ w: walletBalance, c: channelsBalance, u: usersTotal, f: this.accumulatedHtlcFees }) const walletBalance = await this.lnd.GetWalletBalance()
const totalLightningBalanceMsats = (channelsBalance.localBalance?.msat || 0n) + (channelsBalance.unsettledLocalBalance?.msat || 0n) this.log(Number(walletBalance.confirmedBalance), "sats in chain wallet")
const totalLightningBalance = Math.ceil(Number(totalLightningBalanceMsats) / 1000) const channelsBalance = await this.lnd.GetChannelBalance()
return Number(walletBalance.confirmedBalance) + totalLightningBalance getLogger({ component: "debugLndBalancev3" })({ w: walletBalance, c: channelsBalance, u: usersTotal, f: this.accumulatedHtlcFees })
} const totalLightningBalanceMsats = (channelsBalance.localBalance?.msat || 0n) + (channelsBalance.unsettledLocalBalance?.msat || 0n)
const totalLightningBalance = Math.ceil(Number(totalLightningBalanceMsats) / 1000)
checkBalanceUpdate = (deltaLnd: number, deltaUsers: number) => { const providerBalance = await this.liquidProvider.GetLatestBalance()
this.log("LND balance update:", deltaLnd, "sats since app startup") return Number(walletBalance.confirmedBalance) + totalLightningBalance + providerBalance
this.log("Users balance update:", deltaUsers, "sats since app startup") }
const result = this.checkDeltas(deltaLnd, deltaUsers) checkBalanceUpdate = (deltaLnd: number, deltaUsers: number) => {
switch (result.type) { this.log("LND balance update:", deltaLnd, "sats since app startup")
case 'mismatch': this.log("Users balance update:", deltaUsers, "sats since app startup")
if (deltaLnd < 0) {
this.log("WARNING! LND balance decreased while users balance increased creating a difference of", result.absoluteDiff, "sats") const result = this.checkDeltas(deltaLnd, deltaUsers)
if (result.absoluteDiff > this.settings.maxDiffSats) { switch (result.type) {
this.log("Difference is too big for an update, locking outgoing operations") case 'mismatch':
return true if (deltaLnd < 0) {
} this.log("WARNING! LND balance decreased while users balance increased creating a difference of", result.absoluteDiff, "sats")
} else { if (result.absoluteDiff > this.settings.maxDiffSats) {
this.log("LND balance increased while users balance decreased creating a difference of", result.absoluteDiff, "sats, could be caused by data loss, or liquidity injection") this.log("Difference is too big for an update, locking outgoing operations")
return false return true
} }
break } else {
case 'negative': this.log("LND balance increased while users balance decreased creating a difference of", result.absoluteDiff, "sats, could be caused by data loss, or liquidity injection")
if (Math.abs(deltaLnd) > Math.abs(deltaUsers)) { return false
this.log("WARNING! LND balance decreased more than users balance with a difference of", result.absoluteDiff, "sats") }
if (result.absoluteDiff > this.settings.maxDiffSats) { break
this.log("Difference is too big for an update, locking outgoing operations") case 'negative':
return true if (Math.abs(deltaLnd) > Math.abs(deltaUsers)) {
} this.log("WARNING! LND balance decreased more than users balance with a difference of", result.absoluteDiff, "sats")
} else if (deltaLnd === deltaUsers) { if (result.absoluteDiff > this.settings.maxDiffSats) {
this.log("LND and users balance went both DOWN consistently") this.log("Difference is too big for an update, locking outgoing operations")
return false return true
} else { }
this.log("LND balance decreased less than users balance with a difference of", result.absoluteDiff, "sats, could be caused by data loss, or liquidity injection") } else if (deltaLnd === deltaUsers) {
return false this.log("LND and users balance went both DOWN consistently")
} return false
break } else {
case 'positive': this.log("LND balance decreased less than users balance with a difference of", result.absoluteDiff, "sats, could be caused by data loss, or liquidity injection")
if (deltaLnd < deltaUsers) { return false
this.log("WARNING! LND balance increased less than users balance with a difference of", result.absoluteDiff, "sats") }
if (result.absoluteDiff > this.settings.maxDiffSats) { break
this.log("Difference is too big for an update, locking outgoing operations") case 'positive':
return true if (deltaLnd < deltaUsers) {
} this.log("WARNING! LND balance increased less than users balance with a difference of", result.absoluteDiff, "sats")
} else if (deltaLnd === deltaUsers) { if (result.absoluteDiff > this.settings.maxDiffSats) {
this.log("LND and users balance went both UP consistently") this.log("Difference is too big for an update, locking outgoing operations")
return false return true
} else { }
this.log("LND balance increased more than users balance with a difference of", result.absoluteDiff, "sats, could be caused by data loss, or liquidity injection") } else if (deltaLnd === deltaUsers) {
return false this.log("LND and users balance went both UP consistently")
} return false
} } else {
return false this.log("LND balance increased more than users balance with a difference of", result.absoluteDiff, "sats, could be caused by data loss, or liquidity injection")
} return false
}
StartCheck = async () => { }
this.latestCheckStart = Date.now() return false
await this.updateAccumulatedHtlcFees() }
const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
const totalLndBalance = await this.getTotalLndBalance(totalUsersBalance) StartCheck = async () => {
const deltaLnd = totalLndBalance - (this.initialLndBalance + this.accumulatedHtlcFees) this.latestCheckStart = Date.now()
const deltaUsers = totalUsersBalance - this.initialUsersBalance await this.updateAccumulatedHtlcFees()
const deny = this.checkBalanceUpdate(deltaLnd, deltaUsers) const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
if (deny) { const totalLndBalance = await this.getTotalLndBalance(totalUsersBalance)
this.log("Balance mismatch detected in absolute update, locking outgoing operations") const deltaLnd = totalLndBalance - (this.initialLndBalance + this.accumulatedHtlcFees)
this.lnd.LockOutgoingOperations() const deltaUsers = totalUsersBalance - this.initialUsersBalance
return const deny = this.checkBalanceUpdate(deltaLnd, deltaUsers)
} if (deny) {
this.lnd.UnlockOutgoingOperations() this.log("Balance mismatch detected in absolute update, locking outgoing operations")
} this.lnd.LockOutgoingOperations()
return
PaymentRequested = async () => { }
this.log("Payment requested, checking balance") this.lnd.UnlockOutgoingOperations()
if (!this.ready) { }
throw new Error("Watchdog not ready")
} PaymentRequested = async () => {
return new Promise<void>((res, rej) => { this.log("Payment requested, checking balance")
this.queue.Run({ res, rej }) if (!this.ready) {
}) throw new Error("Watchdog not ready")
} }
return new Promise<void>((res, rej) => {
checkDeltas = (deltaLnd: number, deltaUsers: number): DeltaCheckResult => { this.queue.Run({ res, rej })
if (deltaLnd < 0) { })
if (deltaUsers < 0) { }
const diff = Math.abs(deltaLnd - deltaUsers)
return { type: 'negative', absoluteDiff: diff, relativeDiff: diff / Math.max(deltaLnd, deltaUsers) } checkDeltas = (deltaLnd: number, deltaUsers: number): DeltaCheckResult => {
} else { if (deltaLnd < 0) {
const diff = Math.abs(deltaLnd) + deltaUsers if (deltaUsers < 0) {
return { type: 'mismatch', absoluteDiff: diff } const diff = Math.abs(deltaLnd - deltaUsers)
} return { type: 'negative', absoluteDiff: diff, relativeDiff: diff / Math.max(deltaLnd, deltaUsers) }
} else { } else {
if (deltaUsers < 0) { const diff = Math.abs(deltaLnd) + deltaUsers
const diff = deltaLnd + Math.abs(deltaUsers) return { type: 'mismatch', absoluteDiff: diff }
return { type: 'mismatch', absoluteDiff: diff } }
} else { } else {
const diff = Math.abs(deltaLnd - deltaUsers) if (deltaUsers < 0) {
return { type: 'positive', absoluteDiff: diff, relativeDiff: diff / Math.max(deltaLnd, deltaUsers) } const diff = deltaLnd + Math.abs(deltaUsers)
} return { type: 'mismatch', absoluteDiff: diff }
} } else {
} const diff = Math.abs(deltaLnd - deltaUsers)
} return { type: 'positive', absoluteDiff: diff, relativeDiff: diff / Math.max(deltaLnd, deltaUsers) }
}
}
}
}
type DeltaCheckResult = { type: 'negative' | 'positive', absoluteDiff: number, relativeDiff: number } | { type: 'mismatch', absoluteDiff: number } type DeltaCheckResult = { type: 'negative' | 'positive', absoluteDiff: number, relativeDiff: number } | { type: 'mismatch', absoluteDiff: number }

View file

@ -1,84 +1,85 @@
import "reflect-metadata" import "reflect-metadata"
import { DataSource, Migration } from "typeorm" import { DataSource, Migration } from "typeorm"
import { AddressReceivingTransaction } from "./entity/AddressReceivingTransaction.js" import { AddressReceivingTransaction } from "./entity/AddressReceivingTransaction.js"
import { User } from "./entity/User.js" import { User } from "./entity/User.js"
import { UserReceivingAddress } from "./entity/UserReceivingAddress.js" import { UserReceivingAddress } from "./entity/UserReceivingAddress.js"
import { UserReceivingInvoice } from "./entity/UserReceivingInvoice.js" import { UserReceivingInvoice } from "./entity/UserReceivingInvoice.js"
import { UserInvoicePayment } from "./entity/UserInvoicePayment.js" import { UserInvoicePayment } from "./entity/UserInvoicePayment.js"
import { EnvMustBeNonEmptyString } from "../helpers/envParser.js" import { EnvMustBeNonEmptyString } from "../helpers/envParser.js"
import { UserTransactionPayment } from "./entity/UserTransactionPayment.js" import { UserTransactionPayment } from "./entity/UserTransactionPayment.js"
import { UserBasicAuth } from "./entity/UserBasicAuth.js" import { UserBasicAuth } from "./entity/UserBasicAuth.js"
import { UserEphemeralKey } from "./entity/UserEphemeralKey.js" import { UserEphemeralKey } from "./entity/UserEphemeralKey.js"
import { Product } from "./entity/Product.js" import { Product } from "./entity/Product.js"
import { UserToUserPayment } from "./entity/UserToUserPayment.js" import { UserToUserPayment } from "./entity/UserToUserPayment.js"
import { Application } from "./entity/Application.js" import { Application } from "./entity/Application.js"
import { ApplicationUser } from "./entity/ApplicationUser.js" import { ApplicationUser } from "./entity/ApplicationUser.js"
import { BalanceEvent } from "./entity/BalanceEvent.js" 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 = {
databaseFile: string export type DbSettings = {
migrate: boolean databaseFile: string
metricsDatabaseFile: string migrate: boolean
} metricsDatabaseFile: string
export const LoadDbSettingsFromEnv = (): DbSettings => { }
return { export const LoadDbSettingsFromEnv = (): DbSettings => {
databaseFile: process.env.DATABASE_FILE || "db.sqlite", return {
migrate: process.env.MIGRATE_DB === 'true' || false, databaseFile: process.env.DATABASE_FILE || "db.sqlite",
metricsDatabaseFile: process.env.METRICS_DATABASE_FILE || "metrics.sqlite" migrate: process.env.MIGRATE_DB === 'true' || false,
} metricsDatabaseFile: process.env.METRICS_DATABASE_FILE || "metrics.sqlite"
} }
}
export const newMetricsDb = async (settings: DbSettings, metricsMigrations: Function[]): Promise<{ source: DataSource, executedMigrations: Migration[] }> => {
const source = await new DataSource({ export const newMetricsDb = async (settings: DbSettings, metricsMigrations: Function[]): Promise<{ source: DataSource, executedMigrations: Migration[] }> => {
type: "sqlite", const source = await new DataSource({
database: settings.metricsDatabaseFile, type: "sqlite",
entities: [BalanceEvent, ChannelBalanceEvent, ChannelRouting], database: settings.metricsDatabaseFile,
migrations: metricsMigrations entities: [BalanceEvent, ChannelBalanceEvent, ChannelRouting],
}).initialize(); migrations: metricsMigrations
const log = getLogger({}); }).initialize();
const pendingMigrations = await source.showMigrations() const log = getLogger({});
if (pendingMigrations) { const pendingMigrations = await source.showMigrations()
log("Migrations found, migrating...") if (pendingMigrations) {
const executedMigrations = await source.runMigrations({ transaction: 'all' }) log("Migrations found, migrating...")
return { source, executedMigrations } const executedMigrations = await source.runMigrations({ transaction: 'all' })
} return { source, executedMigrations }
return { source, executedMigrations: [] } }
return { source, executedMigrations: [] }
}
}
export default async (settings: DbSettings, migrations: Function[]): Promise<{ source: DataSource, executedMigrations: Migration[] }> => {
const source = await new DataSource({ export default async (settings: DbSettings, migrations: Function[]): Promise<{ source: DataSource, executedMigrations: Migration[] }> => {
type: "sqlite", const source = await new DataSource({
database: settings.databaseFile, type: "sqlite",
// logging: true, database: settings.databaseFile,
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, // logging: true,
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment], entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
//synchronize: true, UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder],
migrations //synchronize: true,
}).initialize() migrations
const log = getLogger({}) }).initialize()
const pendingMigrations = await source.showMigrations() const log = getLogger({})
if (pendingMigrations) { const pendingMigrations = await source.showMigrations()
log("migrations found, migrating...") if (pendingMigrations) {
const executedMigrations = await source.runMigrations({ transaction: 'all' }) log("migrations found, migrating...")
return { source, executedMigrations } const executedMigrations = await source.runMigrations({ transaction: 'all' })
} return { source, executedMigrations }
return { source, executedMigrations: [] } }
} return { source, executedMigrations: [] }
}
export const runFakeMigration = async (databaseFile: string, migrations: Function[]) => {
const source = await new DataSource({ export const runFakeMigration = async (databaseFile: string, migrations: Function[]) => {
type: "sqlite", const source = await new DataSource({
database: databaseFile, type: "sqlite",
// logging: true, database: databaseFile,
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, // logging: true,
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment], entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
//synchronize: true, UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment],
migrations //synchronize: true,
}).initialize() migrations
return source.runMigrations({ fake: true }) }).initialize()
return source.runMigrations({ fake: true })
} }

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

@ -1,47 +1,51 @@
import { DataSource, EntityManager } from "typeorm" import { DataSource, EntityManager } from "typeorm"
import NewDB, { DbSettings, LoadDbSettingsFromEnv } from "./db.js" import NewDB, { DbSettings, LoadDbSettingsFromEnv } from "./db.js"
import ProductStorage from './productStorage.js' import ProductStorage from './productStorage.js'
import ApplicationStorage from './applicationStorage.js' import ApplicationStorage from './applicationStorage.js'
import UserStorage from "./userStorage.js"; import UserStorage from "./userStorage.js";
import PaymentStorage from "./paymentStorage.js"; 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";
export type StorageSettings = { import { LiquidityStorage } from "./liquidityStorage.js";
dbSettings: DbSettings export type StorageSettings = {
eventLogPath: string dbSettings: DbSettings
} eventLogPath: string
export const LoadStorageSettingsFromEnv = (): StorageSettings => { dataDir: string
return { dbSettings: LoadDbSettingsFromEnv(), eventLogPath: "logs/eventLogV2.csv" } }
} export const LoadStorageSettingsFromEnv = (): StorageSettings => {
export default class { return { dbSettings: LoadDbSettingsFromEnv(), eventLogPath: "logs/eventLogV2.csv", dataDir: process.env.DATA_DIR || "" }
DB: DataSource | EntityManager }
settings: StorageSettings export default class {
txQueue: TransactionsQueue DB: DataSource | EntityManager
productStorage: ProductStorage settings: StorageSettings
applicationStorage: ApplicationStorage txQueue: TransactionsQueue
userStorage: UserStorage productStorage: ProductStorage
paymentStorage: PaymentStorage applicationStorage: ApplicationStorage
metricsStorage: MetricsStorage userStorage: UserStorage
eventsLog: EventsLogManager paymentStorage: PaymentStorage
constructor(settings: StorageSettings) { metricsStorage: MetricsStorage
this.settings = settings liquidityStorage: LiquidityStorage
this.eventsLog = new EventsLogManager(settings.eventLogPath) eventsLog: EventsLogManager
} constructor(settings: StorageSettings) {
async Connect(migrations: Function[], metricsMigrations: Function[]) { this.settings = settings
const { source, executedMigrations } = await NewDB(this.settings.dbSettings, migrations) this.eventsLog = new EventsLogManager(settings.eventLogPath)
this.DB = source }
this.txQueue = new TransactionsQueue("main", this.DB) async Connect(migrations: Function[], metricsMigrations: Function[]) {
this.userStorage = new UserStorage(this.DB, this.txQueue, this.eventsLog) const { source, executedMigrations } = await NewDB(this.settings.dbSettings, migrations)
this.productStorage = new ProductStorage(this.DB, this.txQueue) this.DB = source
this.applicationStorage = new ApplicationStorage(this.DB, this.userStorage, this.txQueue) this.txQueue = new TransactionsQueue("main", this.DB)
this.paymentStorage = new PaymentStorage(this.DB, this.userStorage, this.txQueue) this.userStorage = new UserStorage(this.DB, this.txQueue, this.eventsLog)
this.metricsStorage = new MetricsStorage(this.settings) this.productStorage = new ProductStorage(this.DB, this.txQueue)
const executedMetricsMigrations = await this.metricsStorage.Connect(metricsMigrations) this.applicationStorage = new ApplicationStorage(this.DB, this.userStorage, this.txQueue)
return { executedMigrations, executedMetricsMigrations }; this.paymentStorage = new PaymentStorage(this.DB, this.userStorage, this.txQueue)
} this.metricsStorage = new MetricsStorage(this.settings)
this.liquidityStorage = new LiquidityStorage(this.DB, this.txQueue)
StartTransaction<T>(exec: TX<T>, description?: string) { const executedMetricsMigrations = await this.metricsStorage.Connect(metricsMigrations)
return this.txQueue.PushToQueue({ exec, dbTx: true, description }) return { executedMigrations, executedMetricsMigrations };
} }
StartTransaction<T>(exec: TX<T>, description?: string) {
return this.txQueue.PushToQueue({ exec, dbTx: true, description })
}
} }

View file

@ -0,0 +1,20 @@
import { DataSource, EntityManager, MoreThan } 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({ where: { serial_id: MoreThan(0) }, 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

@ -1,29 +1,30 @@
import { PubLogger } from '../../helpers/logger.js' import { PubLogger } from '../../helpers/logger.js'
import { DbSettings, runFakeMigration } from '../db.js' import { DbSettings, runFakeMigration } from '../db.js'
import Storage, { StorageSettings } from '../index.js' 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 allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538] const allMigrations = [Initial1703170309875, LspOrder1718387847693]
export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => { const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538]
if (arg === 'fake_initial_migration') { export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => {
runFakeMigration(settings.databaseFile, [Initial1703170309875]) if (arg === 'fake_initial_migration') {
return true runFakeMigration(settings.databaseFile, [Initial1703170309875])
} return true
await connectAndMigrate(log, storageManager, allMigrations, allMetricsMigrations) }
return false await connectAndMigrate(log, storageManager, allMigrations, allMetricsMigrations)
} return false
}
const connectAndMigrate = async (log: PubLogger, storageManager: Storage, migrations: Function[], metricsMigrations: Function[]) => {
const { executedMigrations, executedMetricsMigrations } = await storageManager.Connect(migrations, metricsMigrations) const connectAndMigrate = async (log: PubLogger, storageManager: Storage, migrations: Function[], metricsMigrations: Function[]) => {
if (migrations.length > 0) { const { executedMigrations, executedMetricsMigrations } = await storageManager.Connect(migrations, metricsMigrations)
log(executedMigrations.length, "of", migrations.length, "migrations were executed correctly") if (migrations.length > 0) {
log(executedMigrations) log(executedMigrations.length, "of", migrations.length, "migrations were executed correctly")
log("-------------------") log(executedMigrations)
log("-------------------")
} if (metricsMigrations.length > 0) {
log(executedMetricsMigrations.length, "of", metricsMigrations.length, "metrics migrations were executed correctly") } if (metricsMigrations.length > 0) {
log(executedMetricsMigrations) log(executedMetricsMigrations.length, "of", metricsMigrations.length, "metrics migrations were executed correctly")
} log(executedMetricsMigrations)
}
} }

View file

@ -1,108 +1,108 @@
version: '3.3' version: '3.3'
services: services:
backend1: backend1:
environment: environment:
USERID: ${USERID:-1000} USERID: ${USERID:-1000}
GROUPID: ${GROUPID:-1000} GROUPID: ${GROUPID:-1000}
stop_grace_period: 5m stop_grace_period: 5m
image: polarlightning/bitcoind:26.0 image: polarlightning/bitcoind:26.0
container_name: polar-n2-backend1 container_name: polar-n2-backend1
hostname: backend1 hostname: backend1
command: >- command: >-
bitcoind -server=1 -regtest=1 -rpcauth=polaruser:5e5e98c21f5c814568f8b55d83b23c1c$$066b03f92df30b11de8e4b1b1cd5b1b4281aa25205bd57df9be82caf97a05526 -debug=1 -zmqpubrawblock=tcp://0.0.0.0:28334 -zmqpubrawtx=tcp://0.0.0.0:28335 -zmqpubhashblock=tcp://0.0.0.0:28336 -txindex=1 -dnsseed=0 -upnp=0 -rpcbind=0.0.0.0 -rpcallowip=0.0.0.0/0 -rpcport=18443 -rest -listen=1 -listenonion=0 -fallbackfee=0.0002 -blockfilterindex=1 -peerblockfilters=1 bitcoind -server=1 -regtest=1 -rpcauth=polaruser:5e5e98c21f5c814568f8b55d83b23c1c$$066b03f92df30b11de8e4b1b1cd5b1b4281aa25205bd57df9be82caf97a05526 -debug=1 -zmqpubrawblock=tcp://0.0.0.0:28334 -zmqpubrawtx=tcp://0.0.0.0:28335 -zmqpubhashblock=tcp://0.0.0.0:28336 -txindex=1 -dnsseed=0 -upnp=0 -rpcbind=0.0.0.0 -rpcallowip=0.0.0.0/0 -rpcport=18443 -rest -listen=1 -listenonion=0 -fallbackfee=0.0002 -blockfilterindex=1 -peerblockfilters=1
volumes: volumes:
- ./volumes/bitcoind/backend1:/home/bitcoin/.bitcoin - ./volumes/bitcoind/backend1:/home/bitcoin/.bitcoin
expose: expose:
- '18443' - '18443'
- '18444' - '18444'
- '28334' - '28334'
- '28335' - '28335'
ports: ports:
- '18443:18443' - '18443:18443'
- '19444:18444' - '19444:18444'
- '28334:28334' - '28334:28334'
- '29335:28335' - '29335:28335'
alice: alice:
environment: environment:
USERID: ${USERID:-1000} USERID: ${USERID:-1000}
GROUPID: ${GROUPID:-1000} GROUPID: ${GROUPID:-1000}
stop_grace_period: 2m stop_grace_period: 2m
image: polarlightning/lnd:0.17.3-beta image: polarlightning/lnd:0.18.0-beta
container_name: polar-n2-alice container_name: polar-n2-alice
hostname: alice hostname: alice
command: >- command: >-
lnd --noseedbackup --trickledelay=5000 --alias=alice --externalip=alice --tlsextradomain=alice --tlsextradomain=polar-n2-alice --tlsextradomain=host.docker.internal --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind --bitcoind.rpchost=polar-n2-backend1 --bitcoind.rpcuser=polaruser --bitcoind.rpcpass=polarpass --bitcoind.zmqpubrawblock=tcp://polar-n2-backend1:28334 --bitcoind.zmqpubrawtx=tcp://polar-n2-backend1:28335 lnd --noseedbackup --trickledelay=5000 --alias=alice --externalip=alice --tlsextradomain=alice --tlsextradomain=polar-n2-alice --tlsextradomain=host.docker.internal --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind --bitcoind.rpchost=polar-n2-backend1 --bitcoind.rpcuser=polaruser --bitcoind.rpcpass=polarpass --bitcoind.zmqpubrawblock=tcp://polar-n2-backend1:28334 --bitcoind.zmqpubrawtx=tcp://polar-n2-backend1:28335
restart: always restart: always
volumes: volumes:
- ./volumes/lnd/alice:/home/lnd/.lnd - ./volumes/lnd/alice:/home/lnd/.lnd
expose: expose:
- '8080' - '8080'
- '10009' - '10009'
- '9735' - '9735'
ports: ports:
# - '8081:8080' # - '8081:8080'
- '10001:10009' - '10001:10009'
- '9735:9735' - '9735:9735'
bob: bob:
environment: environment:
USERID: ${USERID:-1000} USERID: ${USERID:-1000}
GROUPID: ${GROUPID:-1000} GROUPID: ${GROUPID:-1000}
stop_grace_period: 2m stop_grace_period: 2m
image: polarlightning/lnd:0.17.3-beta image: polarlightning/lnd:0.18.0-beta
container_name: polar-n2-bob container_name: polar-n2-bob
hostname: bob hostname: bob
command: >- command: >-
lnd --noseedbackup --trickledelay=5000 --alias=bob --externalip=bob --tlsextradomain=bob --tlsextradomain=polar-n2-bob --tlsextradomain=host.docker.internal --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind --bitcoind.rpchost=polar-n2-backend1 --bitcoind.rpcuser=polaruser --bitcoind.rpcpass=polarpass --bitcoind.zmqpubrawblock=tcp://polar-n2-backend1:28334 --bitcoind.zmqpubrawtx=tcp://polar-n2-backend1:28335 lnd --noseedbackup --trickledelay=5000 --alias=bob --externalip=bob --tlsextradomain=bob --tlsextradomain=polar-n2-bob --tlsextradomain=host.docker.internal --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind --bitcoind.rpchost=polar-n2-backend1 --bitcoind.rpcuser=polaruser --bitcoind.rpcpass=polarpass --bitcoind.zmqpubrawblock=tcp://polar-n2-backend1:28334 --bitcoind.zmqpubrawtx=tcp://polar-n2-backend1:28335
restart: always restart: always
volumes: volumes:
- ./volumes/lnd/bob:/home/lnd/.lnd - ./volumes/lnd/bob:/home/lnd/.lnd
expose: expose:
- '8080' - '8080'
- '10009' - '10009'
- '9735' - '9735'
ports: ports:
# - '8082:8080' # - '8082:8080'
- '10002:10009' - '10002:10009'
- '9736:9735' - '9736:9735'
carol: carol:
environment: environment:
USERID: ${USERID:-1000} USERID: ${USERID:-1000}
GROUPID: ${GROUPID:-1000} GROUPID: ${GROUPID:-1000}
stop_grace_period: 2m stop_grace_period: 2m
image: polarlightning/lnd:0.17.3-beta image: polarlightning/lnd:0.18.0-beta
container_name: polar-n2-carol container_name: polar-n2-carol
hostname: carol hostname: carol
command: >- command: >-
lnd --noseedbackup --trickledelay=5000 --alias=carol --externalip=carol --tlsextradomain=carol --tlsextradomain=polar-n2-carol --tlsextradomain=host.docker.internal --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind --bitcoind.rpchost=polar-n2-backend1 --bitcoind.rpcuser=polaruser --bitcoind.rpcpass=polarpass --bitcoind.zmqpubrawblock=tcp://polar-n2-backend1:28334 --bitcoind.zmqpubrawtx=tcp://polar-n2-backend1:28335 lnd --noseedbackup --trickledelay=5000 --alias=carol --externalip=carol --tlsextradomain=carol --tlsextradomain=polar-n2-carol --tlsextradomain=host.docker.internal --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind --bitcoind.rpchost=polar-n2-backend1 --bitcoind.rpcuser=polaruser --bitcoind.rpcpass=polarpass --bitcoind.zmqpubrawblock=tcp://polar-n2-backend1:28334 --bitcoind.zmqpubrawtx=tcp://polar-n2-backend1:28335
restart: always restart: always
volumes: volumes:
- ./volumes/lnd/carol:/home/lnd/.lnd - ./volumes/lnd/carol:/home/lnd/.lnd
expose: expose:
- '8080' - '8080'
- '10009' - '10009'
- '9735' - '9735'
ports: ports:
# - '8083:8080' # - '8083:8080'
- '10003:10009' - '10003:10009'
- '9737:9735' - '9737:9735'
dave: dave:
environment: environment:
USERID: ${USERID:-1000} USERID: ${USERID:-1000}
GROUPID: ${GROUPID:-1000} GROUPID: ${GROUPID:-1000}
stop_grace_period: 2m stop_grace_period: 2m
image: polarlightning/lnd:0.17.3-beta image: polarlightning/lnd:0.18.0-beta
container_name: polar-n2-dave container_name: polar-n2-dave
hostname: dave hostname: dave
command: >- command: >-
lnd --noseedbackup --trickledelay=5000 --alias=dave --externalip=dave --tlsextradomain=dave --tlsextradomain=polar-n2-dave --tlsextradomain=host.docker.internal --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind --bitcoind.rpchost=polar-n2-backend1 --bitcoind.rpcuser=polaruser --bitcoind.rpcpass=polarpass --bitcoind.zmqpubrawblock=tcp://polar-n2-backend1:28334 --bitcoind.zmqpubrawtx=tcp://polar-n2-backend1:28335 lnd --noseedbackup --trickledelay=5000 --alias=dave --externalip=dave --tlsextradomain=dave --tlsextradomain=polar-n2-dave --tlsextradomain=host.docker.internal --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind --bitcoind.rpchost=polar-n2-backend1 --bitcoind.rpcuser=polaruser --bitcoind.rpcpass=polarpass --bitcoind.zmqpubrawblock=tcp://polar-n2-backend1:28334 --bitcoind.zmqpubrawtx=tcp://polar-n2-backend1:28335
restart: always restart: always
volumes: volumes:
- ./volumes/lnd/dave:/home/lnd/.lnd - ./volumes/lnd/dave:/home/lnd/.lnd
expose: expose:
- '8080' - '8080'
- '10009' - '10009'
- '9735' - '9735'
ports: ports:
# - '8084:8080' # - '8084:8080'
- '10004:10009' - '10004:10009'
- '9738:9735' - '9738:9735'

View file

@ -1,54 +1,56 @@
import { disableLoggers } from '../services/helpers/logger.js' import { disableLoggers } from '../services/helpers/logger.js'
import { runSanityCheck, safelySetUserBalance, TestBase, TestUserData } from './testBase.js' import { runSanityCheck, safelySetUserBalance, TestBase, TestUserData } from './testBase.js'
import { initBootstrappedInstance } from './setupBootstrapped.js' import { initBootstrappedInstance } from './setupBootstrapped.js'
import Main from '../services/main/index.js' import Main from '../services/main/index.js'
import { AppData } from '../services/main/init.js' import { AppData } from '../services/main/init.js'
export const ignore = false export const ignore = false
export const dev = false export const dev = false
export default async (T: TestBase) => { export default async (T: TestBase) => {
disableLoggers([], ["EventsLogManager", "watchdog", "htlcTracker", "debugHtlcs", "debugLndBalancev3", "metrics", "mainForTest", "main"]) disableLoggers([], ["EventsLogManager", "watchdog", "htlcTracker", "debugHtlcs", "debugLndBalancev3", "metrics", "mainForTest", "main"])
await safelySetUserBalance(T, T.user1, 2000) await safelySetUserBalance(T, T.user1, 2000)
T.d("starting liquidityProvider tests...") T.d("starting liquidityProvider tests...")
const { bootstrapped, bootstrappedUser, stop } = await initBootstrappedInstance(T) const { bootstrapped, bootstrappedUser, stop } = await initBootstrappedInstance(T)
await testInboundPaymentFromProvider(T, bootstrapped, bootstrappedUser) await testInboundPaymentFromProvider(T, bootstrapped, bootstrappedUser)
await testOutboundPaymentFromProvider(T, bootstrapped, bootstrappedUser) await testOutboundPaymentFromProvider(T, bootstrapped, bootstrappedUser)
stop() stop()
await runSanityCheck(T) await runSanityCheck(T)
} }
const testInboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bUser: TestUserData) => { const testInboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bUser: TestUserData) => {
T.d("starting testInboundPaymentFromProvider") T.d("starting testInboundPaymentFromProvider")
const invoiceRes = await bootstrapped.appUserManager.NewInvoice({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier }, { amountSats: 2000, memo: "liquidityTest" }) const invoiceRes = await bootstrapped.appUserManager.NewInvoice({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier }, { amountSats: 2000, memo: "liquidityTest" })
await T.externalAccessToOtherLnd.PayInvoice(invoiceRes.invoice, 0, 100) await T.externalAccessToOtherLnd.PayInvoice(invoiceRes.invoice, 0, 100)
const userBalance = await bootstrapped.appUserManager.GetUserInfo({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier }) await new Promise((resolve) => setTimeout(resolve, 200))
T.expect(userBalance.balance).to.equal(2000) const userBalance = await bootstrapped.appUserManager.GetUserInfo({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier })
T.expect(userBalance.balance).to.equal(2000)
const providerBalance = await bootstrapped.liquidProvider.CheckUserState() T.d("user balance is 2000")
if (!providerBalance) { const providerBalance = await bootstrapped.liquidProvider.CheckUserState()
throw new Error("provider balance not found") if (!providerBalance) {
} throw new Error("provider balance not found")
T.expect(providerBalance.balance).to.equal(2000) }
T.d("testInboundPaymentFromProvider done") T.expect(providerBalance.balance).to.equal(2000)
} T.d("provider balance is 2000")
T.d("testInboundPaymentFromProvider done")
const testOutboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bootstrappedUser: TestUserData) => { }
T.d("starting testOutboundPaymentFromProvider")
const testOutboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bootstrappedUser: TestUserData) => {
const invoice = await T.externalAccessToOtherLnd.NewInvoice(1000, "", 60 * 60) T.d("starting testOutboundPaymentFromProvider")
const ctx = { app_id: bootstrappedUser.appId, user_id: bootstrappedUser.userId, app_user_id: bootstrappedUser.appUserIdentifier }
const res = await bootstrapped.appUserManager.PayInvoice(ctx, { invoice: invoice.payRequest, amount: 0 }) const invoice = await T.externalAccessToOtherLnd.NewInvoice(1000, "", 60 * 60)
const ctx = { app_id: bootstrappedUser.appId, user_id: bootstrappedUser.userId, app_user_id: bootstrappedUser.appUserIdentifier }
const userBalance = await bootstrapped.appUserManager.GetUserInfo(ctx) const res = await bootstrapped.appUserManager.PayInvoice(ctx, { invoice: invoice.payRequest, amount: 0 })
T.expect(userBalance.balance).to.equal(986) // 2000 - (1000 + 6(x2) + 2)
const userBalance = await bootstrapped.appUserManager.GetUserInfo(ctx)
const providerBalance = await bootstrapped.liquidProvider.CheckUserState() T.expect(userBalance.balance).to.equal(986) // 2000 - (1000 + 6(x2) + 2)
if (!providerBalance) {
throw new Error("provider balance not found") const providerBalance = await bootstrapped.liquidProvider.CheckUserState()
} if (!providerBalance) {
T.expect(providerBalance.balance).to.equal(992) // 2000 - (1000 + 6 +2) throw new Error("provider balance not found")
T.d("testOutboundPaymentFromProvider done") }
T.expect(providerBalance.balance).to.equal(992) // 2000 - (1000 + 6 +2)
T.d("testOutboundPaymentFromProvider done")
} }

View file

@ -1,65 +1,65 @@
import { LoadTestSettingsFromEnv } from "../services/main/settings.js" import { LoadTestSettingsFromEnv } from "../services/main/settings.js"
import { BitcoinCoreWrapper } from "./bitcoinCore.js" import { BitcoinCoreWrapper } from "./bitcoinCore.js"
import LND from '../services/lnd/lnd.js' import LND from '../services/lnd/lnd.js'
import { LiquidityProvider } from "../services/lnd/liquidityProvider.js" import { LiquidityProvider } from "../services/lnd/liquidityProvider.js"
export const setupNetwork = async () => { export const setupNetwork = async () => {
const settings = LoadTestSettingsFromEnv() const settings = LoadTestSettingsFromEnv()
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) {
return return
} }
await alice.ConnectPeer({ pubkey: '0232842d81b2423df97aa8a264f8c0811610a736af65afe2e145279f285625c1e4', host: "carol:9735" }) await alice.ConnectPeer({ pubkey: '0232842d81b2423df97aa8a264f8c0811610a736af65afe2e145279f285625c1e4', host: "carol:9735" })
await alice.ConnectPeer({ pubkey: '027c50fde118af534ff27e59da722422d2f3e06505c31e94c1b40c112c48a83b1c', host: "dave:9735" }) await alice.ConnectPeer({ pubkey: '027c50fde118af534ff27e59da722422d2f3e06505c31e94c1b40c112c48a83b1c', host: "dave:9735" })
}, 15, 2000) }, 15, 2000)
await tryUntil<void>(async i => { await tryUntil<void>(async i => {
const peers = await bob.ListPeers() const peers = await bob.ListPeers()
if (peers.peers.length > 0) { if (peers.peers.length > 0) {
return return
} }
await bob.ConnectPeer({ pubkey: '0232842d81b2423df97aa8a264f8c0811610a736af65afe2e145279f285625c1e4', host: "carol:9735" }) await bob.ConnectPeer({ pubkey: '0232842d81b2423df97aa8a264f8c0811610a736af65afe2e145279f285625c1e4', host: "carol:9735" })
}, 15, 2000) }, 15, 2000)
await tryUntil<void>(async i => { await tryUntil<void>(async i => {
const info = await alice.GetInfo() const info = await alice.GetInfo()
if (!info.syncedToChain) { if (!info.syncedToChain) {
throw new Error("alice not synced to chain") throw new Error("alice not synced to chain")
} }
if (!info.syncedToGraph) { if (!info.syncedToGraph) {
//await lnd.ConnectPeer({}) //await lnd.ConnectPeer({})
throw new Error("alice not synced to graph") throw new Error("alice not synced to graph")
} }
}, 15, 2000) }, 15, 2000)
await tryUntil<void>(async i => { await tryUntil<void>(async i => {
const info = await bob.GetInfo() const info = await bob.GetInfo()
if (!info.syncedToChain) { if (!info.syncedToChain) {
throw new Error("bob not synced to chain") throw new Error("bob not synced to chain")
} }
if (!info.syncedToGraph) { if (!info.syncedToGraph) {
//await lnd.ConnectPeer({}) //await lnd.ConnectPeer({})
throw new Error("bob not synced to graph") throw new Error("bob not synced to graph")
} }
}, 15, 2000) }, 15, 2000)
alice.Stop() alice.Stop()
bob.Stop() bob.Stop()
} }
const tryUntil = async <T>(fn: (attempt: number) => Promise<T>, maxTries: number, interval: number) => { const tryUntil = async <T>(fn: (attempt: number) => Promise<T>, maxTries: number, interval: number) => {
for (let i = 0; i < maxTries; i++) { for (let i = 0; i < maxTries; i++) {
try { try {
return await fn(i) return await fn(i)
} catch (e) { } catch (e) {
console.log("tryUntil error", e) console.log("tryUntil error", e)
await new Promise(resolve => setTimeout(resolve, interval)) await new Promise(resolve => setTimeout(resolve, interval))
} }
} }
throw new Error("tryUntil failed") throw new Error("tryUntil failed")
} }

View file

@ -1,92 +1,92 @@
import { getLogger } from '../services/helpers/logger.js' import { getLogger } from '../services/helpers/logger.js'
import { initMainHandler } from '../services/main/init.js' import { initMainHandler } from '../services/main/init.js'
import { LoadTestSettingsFromEnv } from '../services/main/settings.js' import { LoadTestSettingsFromEnv } from '../services/main/settings.js'
import { SendData } from '../services/nostr/handler.js' import { SendData } from '../services/nostr/handler.js'
import { TestBase, TestUserData } from './testBase.js' import { TestBase, TestUserData } from './testBase.js'
import * as Types from '../../proto/autogenerated/ts/types.js' import * as Types from '../../proto/autogenerated/ts/types.js'
export const initBootstrappedInstance = async (T: TestBase) => { export const initBootstrappedInstance = async (T: TestBase) => {
const settings = LoadTestSettingsFromEnv() const settings = LoadTestSettingsFromEnv()
settings.lndSettings.useOnlyLiquidityProvider = true settings.liquiditySettings.useOnlyLiquidityProvider = true
settings.lndSettings.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) {
throw new Error("failed to initialize bootstrapped main handler") throw new Error("failed to initialize bootstrapped main handler")
} }
const { mainHandler: bootstrapped, liquidityProviderInfo, liquidityProviderApp } = initialized const { mainHandler: bootstrapped, liquidityProviderInfo, liquidityProviderApp } = initialized
T.main.attachNostrSend(async (_, data, r) => { T.main.attachNostrSend(async (_, data, r) => {
if (data.type === 'event') { if (data.type === 'event') {
throw new Error("unsupported event type") throw new Error("unsupported event type")
} }
if (data.pub !== liquidityProviderInfo.publicKey) { if (data.pub !== liquidityProviderInfo.publicKey) {
throw new Error("invalid pub " + data.pub + " expected " + liquidityProviderInfo.publicKey) throw new Error("invalid pub " + data.pub + " expected " + liquidityProviderInfo.publicKey)
} }
const j = JSON.parse(data.content) as { requestId: string } const j = JSON.parse(data.content) as { requestId: string }
console.log("sending new operation to provider") console.log("sending new operation to provider")
bootstrapped.liquidProvider.onEvent(j, T.app.publicKey) bootstrapped.liquidProvider.onEvent(j, T.app.publicKey)
}) })
bootstrapped.liquidProvider.attachNostrSend(async (_, data, r) => { bootstrapped.liquidProvider.attachNostrSend(async (_, data, r) => {
const res = await handleSend(T, data) const res = await handleSend(T, data)
if (data.type === 'event') { if (data.type === 'event') {
throw new Error("unsupported event type") throw new Error("unsupported event type")
} }
if (!res) { if (!res) {
return return
} }
bootstrapped.liquidProvider.onEvent(res, data.pub) bootstrapped.liquidProvider.onEvent(res, data.pub)
}) })
bootstrapped.liquidProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey }) bootstrapped.liquidProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey })
await new Promise<void>(res => { await new Promise<void>(res => {
const interval = setInterval(() => { const interval = setInterval(() => {
if (bootstrapped.liquidProvider.CanProviderHandle({ action: 'receive', amount: 2000 })) { if (bootstrapped.liquidProvider.CanProviderHandle({ action: 'receive', amount: 2000 })) {
clearInterval(interval) clearInterval(interval)
res() res()
} else { } else {
console.log("waiting for provider to be able to handle the request") console.log("waiting for provider to be able to handle the request")
} }
}, 500) }, 500)
}) })
const bUser = await bootstrapped.applicationManager.AddAppUser(liquidityProviderApp.appId, { identifier: "user1_bootstrapped", balance: 0, fail_if_exists: true }) const bUser = await bootstrapped.applicationManager.AddAppUser(liquidityProviderApp.appId, { identifier: "user1_bootstrapped", balance: 0, fail_if_exists: true })
const bootstrappedUser: TestUserData = { userId: bUser.info.userId, appUserIdentifier: bUser.identifier, appId: liquidityProviderApp.appId } const bootstrappedUser: TestUserData = { userId: bUser.info.userId, appUserIdentifier: bUser.identifier, appId: liquidityProviderApp.appId }
return { return {
bootstrapped, liquidityProviderInfo, liquidityProviderApp, bootstrappedUser, stop: () => { bootstrapped, liquidityProviderInfo, liquidityProviderApp, bootstrappedUser, stop: () => {
bootstrapped.Stop() bootstrapped.Stop()
} }
} }
} }
type TransportRequest = { requestId: string, authIdentifier: string } & ( type TransportRequest = { requestId: string, authIdentifier: string } & (
{ rpcName: 'GetUserInfo' } | { rpcName: 'GetUserInfo' } |
{ rpcName: 'NewInvoice', body: Types.NewInvoiceRequest } | { rpcName: 'NewInvoice', body: Types.NewInvoiceRequest } |
{ rpcName: 'PayInvoice', body: Types.PayInvoiceRequest } | { rpcName: 'PayInvoice', body: Types.PayInvoiceRequest } |
{ rpcName: 'GetLiveUserOperations' } | { rpcName: 'GetLiveUserOperations' } |
{ rpcName: "" } { rpcName: "" }
) )
const handleSend = async (T: TestBase, data: SendData) => { const handleSend = async (T: TestBase, data: SendData) => {
if (data.type === 'event') { if (data.type === 'event') {
throw new Error("unsupported event type") throw new Error("unsupported event type")
} }
if (data.pub !== T.app.publicKey) { if (data.pub !== T.app.publicKey) {
throw new Error("invalid pub") throw new Error("invalid pub")
} }
const j = JSON.parse(data.content) as TransportRequest const j = JSON.parse(data.content) as TransportRequest
const app = await T.main.storage.applicationStorage.GetApplication(T.app.appId) const app = await T.main.storage.applicationStorage.GetApplication(T.app.appId)
const nostrUser = await T.main.storage.applicationStorage.GetOrCreateNostrAppUser(app, j.authIdentifier) const nostrUser = await T.main.storage.applicationStorage.GetOrCreateNostrAppUser(app, j.authIdentifier)
const userCtx = { app_id: app.app_id, user_id: nostrUser.user.user_id, app_user_id: nostrUser.identifier } const userCtx = { app_id: app.app_id, user_id: nostrUser.user.user_id, app_user_id: nostrUser.identifier }
switch (j.rpcName) { switch (j.rpcName) {
case 'GetUserInfo': case 'GetUserInfo':
const infoRes = await T.main.appUserManager.GetUserInfo(userCtx) const infoRes = await T.main.appUserManager.GetUserInfo(userCtx)
return { ...infoRes, status: "OK", requestId: j.requestId } return { ...infoRes, status: "OK", requestId: j.requestId }
case 'NewInvoice': case 'NewInvoice':
const genInvoiceRes = await T.main.appUserManager.NewInvoice(userCtx, j.body) const genInvoiceRes = await T.main.appUserManager.NewInvoice(userCtx, j.body)
return { ...genInvoiceRes, status: "OK", requestId: j.requestId } return { ...genInvoiceRes, status: "OK", requestId: j.requestId }
case 'PayInvoice': case 'PayInvoice':
const payRes = await T.main.appUserManager.PayInvoice(userCtx, j.body) const payRes = await T.main.appUserManager.PayInvoice(userCtx, j.body)
return { ...payRes, status: "OK", requestId: j.requestId } return { ...payRes, status: "OK", requestId: j.requestId }
case 'GetLiveUserOperations': case 'GetLiveUserOperations':
return return
default: default:
console.log(data) console.log(data)
throw new Error("unsupported rpcName " + j.rpcName) throw new Error("unsupported rpcName " + j.rpcName)
} }
} }

View file

@ -1,105 +1,105 @@
import 'dotenv/config' // TODO - test env import 'dotenv/config' // TODO - test env
import chai from 'chai' import chai from 'chai'
import { AppData, initMainHandler } from '../services/main/init.js' import { AppData, initMainHandler } from '../services/main/init.js'
import Main from '../services/main/index.js' import Main from '../services/main/index.js'
import Storage from '../services/storage/index.js' import Storage from '../services/storage/index.js'
import { User } from '../services/storage/entity/User.js' import { User } from '../services/storage/entity/User.js'
import { LoadMainSettingsFromEnv, LoadTestSettingsFromEnv, MainSettings } from '../services/main/settings.js' import { LoadMainSettingsFromEnv, LoadTestSettingsFromEnv, MainSettings } from '../services/main/settings.js'
import chaiString from 'chai-string' import chaiString from 'chai-string'
import { defaultInvoiceExpiry } from '../services/storage/paymentStorage.js' import { defaultInvoiceExpiry } from '../services/storage/paymentStorage.js'
import SanityChecker from '../services/main/sanityChecker.js' import SanityChecker from '../services/main/sanityChecker.js'
import LND from '../services/lnd/lnd.js' import LND from '../services/lnd/lnd.js'
import { getLogger, resetDisabledLoggers } from '../services/helpers/logger.js' import { getLogger, resetDisabledLoggers } from '../services/helpers/logger.js'
import { LiquidityProvider } from '../services/lnd/liquidityProvider.js' import { LiquidityProvider } from '../services/lnd/liquidityProvider.js'
chai.use(chaiString) chai.use(chaiString)
export const expect = chai.expect export const expect = chai.expect
export type Describe = (message: string, failure?: boolean) => void export type Describe = (message: string, failure?: boolean) => void
export type TestUserData = { export type TestUserData = {
userId: string; userId: string;
appUserIdentifier: string; appUserIdentifier: string;
appId: string; appId: string;
} }
export type TestBase = { export type TestBase = {
expect: Chai.ExpectStatic; expect: Chai.ExpectStatic;
main: Main main: Main
app: AppData app: AppData
user1: TestUserData user1: TestUserData
user2: TestUserData user2: TestUserData
externalAccessToMainLnd: LND externalAccessToMainLnd: LND
externalAccessToOtherLnd: LND externalAccessToOtherLnd: LND
externalAccessToThirdLnd: LND externalAccessToThirdLnd: LND
d: Describe d: Describe
} }
export const SetupTest = async (d: Describe): Promise<TestBase> => { export const SetupTest = async (d: Describe): Promise<TestBase> => {
const settings = LoadTestSettingsFromEnv() const settings = LoadTestSettingsFromEnv()
const initialized = await initMainHandler(getLogger({ component: "mainForTest" }), settings) const initialized = await initMainHandler(getLogger({ component: "mainForTest" }), settings)
if (!initialized) { if (!initialized) {
throw new Error("failed to initialize main handler") throw new Error("failed to initialize main handler")
} }
const main = initialized.mainHandler const main = initialized.mainHandler
const app = initialized.apps[0] const app = initialized.apps[0]
const u1 = await main.applicationManager.AddAppUser(app.appId, { identifier: "user1", balance: 0, fail_if_exists: true }) const u1 = await main.applicationManager.AddAppUser(app.appId, { identifier: "user1", balance: 0, fail_if_exists: true })
const u2 = await main.applicationManager.AddAppUser(app.appId, { identifier: "user2", balance: 0, fail_if_exists: true }) const u2 = await main.applicationManager.AddAppUser(app.appId, { identifier: "user2", balance: 0, fail_if_exists: true })
const user1 = { userId: u1.info.userId, appUserIdentifier: u1.identifier, appId: app.appId } const user1 = { userId: u1.info.userId, appUserIdentifier: u1.identifier, appId: app.appId }
const user2 = { userId: u2.info.userId, appUserIdentifier: u2.identifier, appId: app.appId } const user2 = { userId: u2.info.userId, appUserIdentifier: u2.identifier, appId: app.appId }
const 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()
return { return {
expect, main, app, expect, main, app,
user1, user2, user1, user2,
externalAccessToMainLnd, externalAccessToOtherLnd, externalAccessToThirdLnd, externalAccessToMainLnd, externalAccessToOtherLnd, externalAccessToThirdLnd,
d d
} }
} }
export const teardown = async (T: TestBase) => { export const teardown = async (T: TestBase) => {
T.main.Stop() T.main.Stop()
T.externalAccessToMainLnd.Stop() T.externalAccessToMainLnd.Stop()
T.externalAccessToOtherLnd.Stop() T.externalAccessToOtherLnd.Stop()
T.externalAccessToThirdLnd.Stop() T.externalAccessToThirdLnd.Stop()
resetDisabledLoggers() resetDisabledLoggers()
console.log("teardown") console.log("teardown")
} }
export const safelySetUserBalance = async (T: TestBase, user: TestUserData, amount: number) => { export const safelySetUserBalance = async (T: TestBase, user: TestUserData, amount: number) => {
const app = await T.main.storage.applicationStorage.GetApplication(user.appId) const app = await T.main.storage.applicationStorage.GetApplication(user.appId)
const invoice = await T.main.paymentManager.NewInvoice(user.userId, { amountSats: amount, memo: "test" }, { linkedApplication: app, expiry: defaultInvoiceExpiry }) const invoice = await T.main.paymentManager.NewInvoice(user.userId, { amountSats: amount, memo: "test" }, { linkedApplication: app, expiry: defaultInvoiceExpiry })
await T.externalAccessToOtherLnd.PayInvoice(invoice.invoice, 0, 100) await T.externalAccessToOtherLnd.PayInvoice(invoice.invoice, 0, 100)
const u = await T.main.storage.userStorage.GetUser(user.userId) const u = await T.main.storage.userStorage.GetUser(user.userId)
expect(u.balance_sats).to.be.equal(amount) expect(u.balance_sats).to.be.equal(amount)
T.d(`user ${user.appUserIdentifier} balance is now ${amount}`) T.d(`user ${user.appUserIdentifier} balance is now ${amount}`)
} }
export const runSanityCheck = async (T: TestBase) => { export const runSanityCheck = async (T: TestBase) => {
const sanityChecker = new SanityChecker(T.main.storage, T.main.lnd) const sanityChecker = new SanityChecker(T.main.storage, T.main.lnd)
await sanityChecker.VerifyEventsLog() await sanityChecker.VerifyEventsLog()
} }
export const expectThrowsAsync = async (promise: Promise<any>, errorMessage?: string) => { export const expectThrowsAsync = async (promise: Promise<any>, errorMessage?: string) => {
let error: Error | null = null let error: Error | null = null
try { try {
await promise await promise
} }
catch (err: any) { catch (err: any) {
error = err as Error error = err as Error
} }
expect(error).to.be.an('Error') expect(error).to.be.an('Error')
console.log(error!.message) console.log(error!.message)
if (errorMessage) { if (errorMessage) {
expect(error!.message).to.equal(errorMessage) expect(error!.message).to.equal(errorMessage)
} }
} }