Merge branch 'master' into umbrel-works
This commit is contained in:
commit
8d5a9069ae
24 changed files with 12438 additions and 11898 deletions
185
env.example
185
env.example
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
18636
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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 }
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
})
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
106
src/services/main/liquidityManager.ts
Normal file
106
src/services/main/liquidityManager.ts
Normal 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
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
28
src/services/storage/entity/LspOrder.ts
Normal file
28
src/services/storage/entity/LspOrder.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
20
src/services/storage/liquidityStorage.ts
Normal file
20
src/services/storage/liquidityStorage.ts
Normal 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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/services/storage/migrations/1718387847693-lsp_order.ts
Normal file
14
src/services/storage/migrations/1718387847693-lsp_order.ts
Normal 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"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue