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
|
||||
# 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
|
||||
|
||||
#LND_CONNECTION
|
||||
# Defaults typical for straight Linux
|
||||
# Containers, Mac and Windows may need more detailed paths
|
||||
#LND_ADDRESS=127.0.0.1:10009
|
||||
#LND_CERT_PATH=~/.lnd/tls.cert
|
||||
#LND_MACAROON_PATH=~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon
|
||||
|
||||
LIQUIDITY_PROVIDER_PUB=
|
||||
|
||||
#DB
|
||||
#DATABASE_FILE=db.sqlite
|
||||
#METRICS_DATABASE_FILE=metrics.sqlite
|
||||
#LOGS_DIR=logs
|
||||
|
||||
#LOCALHOST
|
||||
#ADMIN_TOKEN=
|
||||
#PORT=1776
|
||||
#JWT_SECRET=
|
||||
|
||||
#LIGHTNING
|
||||
# Maximum amount in network fees passed to LND when it pays an external invoice
|
||||
# BPS are basis points, 100 BPS = 1%
|
||||
#OUTBOUND_MAX_FEE_BPS=60
|
||||
#OUTBOUND_MAX_FEE_EXTRA_SATS=100
|
||||
# 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
|
||||
#BOOTSTRAP=1
|
||||
|
||||
#ROOT_FEES
|
||||
# Applied to either debits or credits and sent to an admin account
|
||||
# BPS are basis points, 100 BPS = 1%
|
||||
#INCOMING_CHAIN_FEE_ROOT_BPS=0
|
||||
#INCOMING_INVOICE_FEE_ROOT_BPS=0
|
||||
# Chain spends are currently unstable and thus disabled, do not use until further notice
|
||||
#OUTGOING_CHAIN_FEE_ROOT_BPS=60
|
||||
# Outgoing Invoice Fee must be >= Lightning Outbound Max Fee so admins don't incur losses on spends
|
||||
#OUTGOING_INVOICE_FEE_ROOT_BPS=60
|
||||
# Internal user fees bugged, do not use until further notice
|
||||
#TX_FEE_INTERNAL_ROOT_BPS=0 #applied to inter-application txns
|
||||
|
||||
#APP_FEES
|
||||
# An extra fee applied at the app level and sent to the application owner
|
||||
#INCOMING_INVOICE_FEE_USER_BPS=0
|
||||
#OUTGOING_INVOICE_FEE_USER_BPS=0
|
||||
#TX_FEE_INTERNAL_USER_BPS=0
|
||||
|
||||
#NOSTR
|
||||
# Default relay may become rate-limited without a paid subscription
|
||||
#NOSTR_RELAYS=wss://strfry.shock.network
|
||||
|
||||
#LNURL
|
||||
# Optional
|
||||
# If undefined, LNURLs (including Lightning Address) will be disabled
|
||||
# To enable, add a reachable https endpoint for requests (or purchase a subscription)
|
||||
# You also need an SSL reverse proxy from the domain to this local host
|
||||
# Read more at https://docs.shock.network
|
||||
#SERVICE_URL=https://yourdomainhere.xyz
|
||||
|
||||
#SUBSCRIPTION_SERVICES
|
||||
# Opt-in to cloud relays for LNURL and Nostr
|
||||
# A small monthly fee supports the developers
|
||||
# Read more at https://docs.shock.network
|
||||
#SUBSCRIBER=1
|
||||
|
||||
#DEV_OPTS
|
||||
#MOCK_LND=false
|
||||
#ALLOW_BALANCE_MIGRATION=false
|
||||
#MIGRATE_DB=false
|
||||
#LOG_LEVEL=DEBUG
|
||||
|
||||
#METRICS
|
||||
#RECORD_PERFORMANCE=true
|
||||
#SKIP_SANITY_CHECK=false
|
||||
# A read-only token that can be used with dashboard to view reports
|
||||
#METRICS_TOKEN=
|
||||
# Disable outbound payments aka honeypot mode
|
||||
#DISABLE_EXTERNAL_PAYMENTS=false
|
||||
|
||||
#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
|
||||
# Example configuration for Lightning.Pub
|
||||
# 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
|
||||
|
||||
#LND_CONNECTION
|
||||
# Defaults typical for straight Linux
|
||||
# Containers, Mac and Windows may need more detailed paths
|
||||
#LND_ADDRESS=127.0.0.1:10009
|
||||
#LND_CERT_PATH=~/.lnd/tls.cert
|
||||
#LND_MACAROON_PATH=~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon
|
||||
|
||||
LIQUIDITY_PROVIDER_PUB=
|
||||
|
||||
#DB
|
||||
#DATABASE_FILE=db.sqlite
|
||||
#METRICS_DATABASE_FILE=metrics.sqlite
|
||||
#LOGS_DIR=logs
|
||||
|
||||
#LOCALHOST
|
||||
#ADMIN_TOKEN=
|
||||
#PORT=1776
|
||||
#JWT_SECRET=
|
||||
|
||||
#LIGHTNING
|
||||
# Maximum amount in network fees passed to LND when it pays an external invoice
|
||||
# BPS are basis points, 100 BPS = 1%
|
||||
#OUTBOUND_MAX_FEE_BPS=60
|
||||
#OUTBOUND_MAX_FEE_EXTRA_SATS=100
|
||||
# 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
|
||||
#BOOTSTRAP=1
|
||||
|
||||
#LSP
|
||||
OLYMPUS_LSP_URL=https://lsps1.lnolymp.us/api/v1
|
||||
VOLTAGE_LSP_URL=https://lsp.voltageapi.com/api/v1
|
||||
FLASHSATS_LSP_URL=https://lsp.flashsats.xyz/lsp/channel
|
||||
LSP_CHANNEL_THRESHOLD=1000000
|
||||
LSP_MAX_FEE_BPS=100
|
||||
|
||||
#ROOT_FEES
|
||||
# Applied to either debits or credits and sent to an admin account
|
||||
# BPS are basis points, 100 BPS = 1%
|
||||
#INCOMING_CHAIN_FEE_ROOT_BPS=0
|
||||
#INCOMING_INVOICE_FEE_ROOT_BPS=0
|
||||
# Chain spends are currently unstable and thus disabled, do not use until further notice
|
||||
#OUTGOING_CHAIN_FEE_ROOT_BPS=60
|
||||
# Outgoing Invoice Fee must be >= Lightning Outbound Max Fee so admins don't incur losses on spends
|
||||
#OUTGOING_INVOICE_FEE_ROOT_BPS=60
|
||||
# Internal user fees bugged, do not use until further notice
|
||||
#TX_FEE_INTERNAL_ROOT_BPS=0 #applied to inter-application txns
|
||||
|
||||
#APP_FEES
|
||||
# An extra fee applied at the app level and sent to the application owner
|
||||
#INCOMING_INVOICE_FEE_USER_BPS=0
|
||||
#OUTGOING_INVOICE_FEE_USER_BPS=0
|
||||
#TX_FEE_INTERNAL_USER_BPS=0
|
||||
|
||||
#NOSTR
|
||||
# Default relay may become rate-limited without a paid subscription
|
||||
#NOSTR_RELAYS=wss://strfry.shock.network
|
||||
|
||||
#LNURL
|
||||
# Optional
|
||||
# If undefined, LNURLs (including Lightning Address) will be disabled
|
||||
# To enable, add a reachable https endpoint for requests (or purchase a subscription)
|
||||
# You also need an SSL reverse proxy from the domain to this local host
|
||||
# Read more at https://docs.shock.network
|
||||
#SERVICE_URL=https://yourdomainhere.xyz
|
||||
|
||||
#SUBSCRIPTION_SERVICES
|
||||
# Opt-in to cloud relays for LNURL and Nostr
|
||||
# A small monthly fee supports the developers
|
||||
# Read more at https://docs.shock.network
|
||||
#SUBSCRIBER=1
|
||||
|
||||
#DEV_OPTS
|
||||
#MOCK_LND=false
|
||||
#ALLOW_BALANCE_MIGRATION=false
|
||||
#MIGRATE_DB=false
|
||||
#LOG_LEVEL=DEBUG
|
||||
|
||||
#METRICS
|
||||
#RECORD_PERFORMANCE=true
|
||||
#SKIP_SANITY_CHECK=false
|
||||
# A read-only token that can be used with dashboard to view reports
|
||||
#METRICS_TOKEN=
|
||||
# Disable outbound payments aka honeypot mode
|
||||
#DISABLE_EXTERNAL_PAYMENTS=false
|
||||
|
||||
#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 { ChannelRouting } from "./build/src/services/storage/entity/ChannelRouting.js"
|
||||
|
||||
|
||||
|
||||
export default new DataSource({
|
||||
type: "sqlite",
|
||||
database: "metrics.sqlite",
|
||||
entities: [ChannelRouting],
|
||||
import { DataSource } from "typeorm"
|
||||
import { LspOrder } from "./build/src/services/storage/entity/LspOrder.js"
|
||||
|
||||
|
||||
|
||||
export default new DataSource({
|
||||
type: "sqlite",
|
||||
database: "db.sqlite",
|
||||
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 feeFixedLimit = EnvCanBeInteger("OUTBOUND_MAX_FEE_EXTRA_SATS", 100)
|
||||
const mockLnd = EnvCanBeBoolean("MOCK_LND")
|
||||
const liquidityProviderPub = process.env.LIQUIDITY_PROVIDER_PUB || ""
|
||||
return { mainNode: { lndAddr, lndCertPath, lndMacaroonPath }, feeRateLimit, feeFixedLimit, mockLnd, liquidityProviderPub, useOnlyLiquidityProvider: false }
|
||||
return { mainNode: { lndAddr, lndCertPath, lndMacaroonPath }, feeRateLimit, feeFixedLimit, mockLnd }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,226 +1,252 @@
|
|||
import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js'
|
||||
import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js'
|
||||
import * as Types from '../../../proto/autogenerated/ts/types.js'
|
||||
import { decodeNprofile } from '../../custom-nip19.js'
|
||||
import { getLogger } from '../helpers/logger.js'
|
||||
import { NostrEvent, NostrSend } from '../nostr/handler.js'
|
||||
import { relayInit } from '../nostr/tools/relay.js'
|
||||
import { InvoicePaidCb } from './settings.js'
|
||||
|
||||
export type LiquidityRequest = { action: 'spend' | 'receive', amount: number }
|
||||
|
||||
export type nostrCallback<T> = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void }
|
||||
export class LiquidityProvider {
|
||||
client: ReturnType<typeof newNostrClient>
|
||||
clientCbs: Record<string, nostrCallback<any>> = {}
|
||||
clientId: string = ""
|
||||
myPub: string = ""
|
||||
log = getLogger({ component: 'liquidityProvider' })
|
||||
nostrSend: NostrSend | null = null
|
||||
ready = false
|
||||
pubDestination: string
|
||||
latestMaxWithdrawable: number | null = null
|
||||
invoicePaidCb: InvoicePaidCb
|
||||
connecting = false
|
||||
readyInterval: NodeJS.Timeout
|
||||
// make the sub process accept client
|
||||
constructor(pubDestination: string, invoicePaidCb: InvoicePaidCb) {
|
||||
if (!pubDestination) {
|
||||
this.log("No pub provider to liquidity provider, will not be initialized")
|
||||
}
|
||||
this.pubDestination = pubDestination
|
||||
this.invoicePaidCb = invoicePaidCb
|
||||
this.client = newNostrClient({
|
||||
pubDestination: this.pubDestination,
|
||||
retrieveNostrUserAuth: async () => this.myPub,
|
||||
}, this.clientSend, this.clientSub)
|
||||
|
||||
this.readyInterval = setInterval(() => {
|
||||
if (this.ready) {
|
||||
clearInterval(this.readyInterval)
|
||||
this.Connect()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
Stop = () => {
|
||||
clearInterval(this.readyInterval)
|
||||
}
|
||||
|
||||
Connect = async () => {
|
||||
await new Promise(res => setTimeout(res, 2000))
|
||||
this.log("ready")
|
||||
await this.CheckUserState()
|
||||
if (this.latestMaxWithdrawable === null) {
|
||||
return
|
||||
}
|
||||
this.log("subbing to user operations")
|
||||
this.client.GetLiveUserOperations(res => {
|
||||
console.log("got user operation", res)
|
||||
if (res.status === 'ERROR') {
|
||||
this.log("error getting user operations", res.reason)
|
||||
return
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
this.latestMaxWithdrawable = res.max_withdrawable
|
||||
this.log("latest provider balance:", res.max_withdrawable)
|
||||
return res
|
||||
}
|
||||
|
||||
CanProviderHandle = (req: LiquidityRequest) => {
|
||||
if (this.latestMaxWithdrawable === null) {
|
||||
return false
|
||||
}
|
||||
if (req.action === 'spend') {
|
||||
return this.latestMaxWithdrawable > req.amount
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
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)
|
||||
throw new Error(res.reason)
|
||||
}
|
||||
this.log("new invoice", res.invoice)
|
||||
this.CheckUserState()
|
||||
return res.invoice
|
||||
}
|
||||
|
||||
PayInvoice = async (invoice: string) => {
|
||||
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)
|
||||
}
|
||||
this.log("paid invoice", res)
|
||||
this.CheckUserState()
|
||||
return res
|
||||
}
|
||||
|
||||
setNostrInfo = ({ clientId, myPub }: { myPub: string, clientId: string }) => {
|
||||
this.clientId = clientId
|
||||
this.myPub = myPub
|
||||
this.setSetIfReady()
|
||||
}
|
||||
|
||||
|
||||
|
||||
attachNostrSend(f: NostrSend) {
|
||||
this.nostrSend = f
|
||||
this.setSetIfReady()
|
||||
}
|
||||
|
||||
setSetIfReady = () => {
|
||||
if (this.nostrSend && !!this.pubDestination && !!this.clientId && !!this.myPub) {
|
||||
this.ready = true
|
||||
this.log("ready to send to ", this.pubDestination)
|
||||
}
|
||||
}
|
||||
|
||||
onEvent = async (res: { requestId: string }, fromPub: string) => {
|
||||
if (fromPub !== this.pubDestination) {
|
||||
this.log("got event from invalid pub", fromPub, this.pubDestination)
|
||||
return false
|
||||
}
|
||||
if (this.clientCbs[res.requestId]) {
|
||||
const cb = this.clientCbs[res.requestId]
|
||||
cb.f(res)
|
||||
if (cb.type === 'single') {
|
||||
delete this.clientCbs[res.requestId]
|
||||
this.log(this.getSingleSubs(), "single subs left")
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
clientSend = (to: string, message: NostrRequest): Promise<any> => {
|
||||
if (!this.ready || !this.nostrSend) {
|
||||
throw new Error("liquidity provider not initialized")
|
||||
}
|
||||
if (!message.requestId) {
|
||||
message.requestId = makeId(16)
|
||||
}
|
||||
const reqId = message.requestId
|
||||
if (this.clientCbs[reqId]) {
|
||||
throw new Error("request was already sent")
|
||||
}
|
||||
this.nostrSend({ type: 'client', clientId: this.clientId }, {
|
||||
type: 'content',
|
||||
pub: to,
|
||||
content: JSON.stringify(message)
|
||||
})
|
||||
|
||||
//this.nostrSend(this.relays, to, JSON.stringify(message), this.settings)
|
||||
|
||||
this.log("subbing to single send", reqId, message.rpcName || 'no rpc name')
|
||||
return new Promise(res => {
|
||||
this.clientCbs[reqId] = {
|
||||
startedAtMillis: Date.now(),
|
||||
type: 'single',
|
||||
f: (response: any) => { res(response) },
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
clientSub = (to: string, message: NostrRequest, cb: (res: any) => void): void => {
|
||||
if (!this.ready || !this.nostrSend) {
|
||||
throw new Error("liquidity provider not initialized")
|
||||
}
|
||||
if (!message.requestId) {
|
||||
message.requestId = message.rpcName
|
||||
}
|
||||
const reqId = message.requestId
|
||||
if (!reqId) {
|
||||
throw new Error("invalid sub")
|
||||
}
|
||||
if (this.clientCbs[reqId]) {
|
||||
this.clientCbs[reqId] = {
|
||||
startedAtMillis: Date.now(),
|
||||
type: 'stream',
|
||||
f: (response: any) => { cb(response) },
|
||||
}
|
||||
this.log("sub for", reqId, "was already registered, overriding")
|
||||
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;
|
||||
import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js'
|
||||
import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js'
|
||||
import * as Types from '../../../proto/autogenerated/ts/types.js'
|
||||
import { decodeNprofile } from '../../custom-nip19.js'
|
||||
import { getLogger } from '../helpers/logger.js'
|
||||
import { NostrEvent, NostrSend } from '../nostr/handler.js'
|
||||
import { relayInit } from '../nostr/tools/relay.js'
|
||||
import { InvoicePaidCb } from './settings.js'
|
||||
|
||||
export type LiquidityRequest = { action: 'spend' | 'receive', amount: number }
|
||||
|
||||
export type nostrCallback<T> = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void }
|
||||
export class LiquidityProvider {
|
||||
client: ReturnType<typeof newNostrClient>
|
||||
clientCbs: Record<string, nostrCallback<any>> = {}
|
||||
clientId: string = ""
|
||||
myPub: string = ""
|
||||
log = getLogger({ component: 'liquidityProvider' })
|
||||
nostrSend: NostrSend | null = null
|
||||
ready = false
|
||||
pubDestination: string
|
||||
latestMaxWithdrawable: number | null = null
|
||||
latestBalance: number | null = null
|
||||
invoicePaidCb: InvoicePaidCb
|
||||
connecting = false
|
||||
readyInterval: NodeJS.Timeout
|
||||
// make the sub process accept client
|
||||
constructor(pubDestination: string, invoicePaidCb: InvoicePaidCb) {
|
||||
if (!pubDestination) {
|
||||
this.log("No pub provider to liquidity provider, will not be initialized")
|
||||
return
|
||||
}
|
||||
this.log("connecting to liquidity provider", pubDestination)
|
||||
this.pubDestination = pubDestination
|
||||
this.invoicePaidCb = invoicePaidCb
|
||||
this.client = newNostrClient({
|
||||
pubDestination: this.pubDestination,
|
||||
retrieveNostrUserAuth: async () => this.myPub,
|
||||
}, this.clientSend, this.clientSub)
|
||||
|
||||
this.readyInterval = setInterval(() => {
|
||||
if (this.ready) {
|
||||
clearInterval(this.readyInterval)
|
||||
this.Connect()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
Stop = () => {
|
||||
clearInterval(this.readyInterval)
|
||||
}
|
||||
|
||||
Connect = async () => {
|
||||
await new Promise(res => setTimeout(res, 2000))
|
||||
this.log("ready")
|
||||
await this.CheckUserState()
|
||||
if (this.latestMaxWithdrawable === null) {
|
||||
return
|
||||
}
|
||||
this.log("subbing to user operations")
|
||||
this.client.GetLiveUserOperations(res => {
|
||||
console.log("got user operation", res)
|
||||
if (res.status === 'ERROR') {
|
||||
this.log("error getting user operations", res.reason)
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
GetLatestMaxWithdrawable = async (fetch = false) => {
|
||||
if (this.latestMaxWithdrawable === null) {
|
||||
this.log("liquidity provider is not ready yet")
|
||||
return 0
|
||||
}
|
||||
if (fetch) {
|
||||
await this.CheckUserState()
|
||||
}
|
||||
return this.latestMaxWithdrawable || 0
|
||||
}
|
||||
|
||||
GetLatestBalance = async (fetch = false) => {
|
||||
if (this.latestMaxWithdrawable === null) {
|
||||
this.log("liquidity provider is not ready yet")
|
||||
return 0
|
||||
}
|
||||
if (fetch) {
|
||||
await this.CheckUserState()
|
||||
}
|
||||
return this.latestBalance || 0
|
||||
}
|
||||
|
||||
CheckUserState = async () => {
|
||||
const res = await this.client.GetUserInfo()
|
||||
if (res.status === 'ERROR') {
|
||||
this.log("error getting user info", res)
|
||||
return
|
||||
}
|
||||
this.latestMaxWithdrawable = res.max_withdrawable
|
||||
this.latestBalance = res.balance
|
||||
this.log("latest provider balance:", res.balance, "latest max withdrawable:", res.max_withdrawable)
|
||||
return res
|
||||
}
|
||||
|
||||
CanProviderHandle = (req: LiquidityRequest) => {
|
||||
if (this.latestMaxWithdrawable === null) {
|
||||
return false
|
||||
}
|
||||
if (req.action === 'spend') {
|
||||
return this.latestMaxWithdrawable > req.amount
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
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)
|
||||
throw new Error(res.reason)
|
||||
}
|
||||
this.log("new invoice", res.invoice)
|
||||
this.CheckUserState()
|
||||
return res.invoice
|
||||
}
|
||||
|
||||
PayInvoice = async (invoice: string) => {
|
||||
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)
|
||||
}
|
||||
this.log("paid invoice", res)
|
||||
this.CheckUserState()
|
||||
return res
|
||||
}
|
||||
|
||||
setNostrInfo = ({ clientId, myPub }: { myPub: string, clientId: string }) => {
|
||||
this.clientId = clientId
|
||||
this.myPub = myPub
|
||||
this.setSetIfReady()
|
||||
}
|
||||
|
||||
|
||||
|
||||
attachNostrSend(f: NostrSend) {
|
||||
this.nostrSend = f
|
||||
this.setSetIfReady()
|
||||
}
|
||||
|
||||
setSetIfReady = () => {
|
||||
if (this.nostrSend && !!this.pubDestination && !!this.clientId && !!this.myPub) {
|
||||
this.ready = true
|
||||
this.log("ready to send to ", this.pubDestination)
|
||||
}
|
||||
}
|
||||
|
||||
onEvent = async (res: { requestId: string }, fromPub: string) => {
|
||||
if (fromPub !== this.pubDestination) {
|
||||
this.log("got event from invalid pub", fromPub, this.pubDestination)
|
||||
return false
|
||||
}
|
||||
if (this.clientCbs[res.requestId]) {
|
||||
const cb = this.clientCbs[res.requestId]
|
||||
cb.f(res)
|
||||
if (cb.type === 'single') {
|
||||
delete this.clientCbs[res.requestId]
|
||||
this.log(this.getSingleSubs(), "single subs left")
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
clientSend = (to: string, message: NostrRequest): Promise<any> => {
|
||||
if (!this.ready || !this.nostrSend) {
|
||||
throw new Error("liquidity provider not initialized")
|
||||
}
|
||||
if (!message.requestId) {
|
||||
message.requestId = makeId(16)
|
||||
}
|
||||
const reqId = message.requestId
|
||||
if (this.clientCbs[reqId]) {
|
||||
throw new Error("request was already sent")
|
||||
}
|
||||
this.nostrSend({ type: 'client', clientId: this.clientId }, {
|
||||
type: 'content',
|
||||
pub: to,
|
||||
content: JSON.stringify(message)
|
||||
})
|
||||
|
||||
//this.nostrSend(this.relays, to, JSON.stringify(message), this.settings)
|
||||
|
||||
this.log("subbing to single send", reqId, message.rpcName || 'no rpc name')
|
||||
return new Promise(res => {
|
||||
this.clientCbs[reqId] = {
|
||||
startedAtMillis: Date.now(),
|
||||
type: 'single',
|
||||
f: (response: any) => { res(response) },
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
clientSub = (to: string, message: NostrRequest, cb: (res: any) => void): void => {
|
||||
if (!this.ready || !this.nostrSend) {
|
||||
throw new Error("liquidity provider not initialized")
|
||||
}
|
||||
if (!message.requestId) {
|
||||
message.requestId = message.rpcName
|
||||
}
|
||||
const reqId = message.requestId
|
||||
if (!reqId) {
|
||||
throw new Error("invalid sub")
|
||||
}
|
||||
if (this.clientCbs[reqId]) {
|
||||
this.clientCbs[reqId] = {
|
||||
startedAtMillis: Date.now(),
|
||||
type: 'stream',
|
||||
f: (response: any) => { cb(response) },
|
||||
}
|
||||
this.log("sub for", reqId, "was already registered, overriding")
|
||||
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');
|
||||
import crypto from 'crypto'
|
||||
import { credentials, Metadata } from '@grpc/grpc-js'
|
||||
import { GrpcTransport } from "@protobuf-ts/grpc-transport";
|
||||
import fs from 'fs'
|
||||
import * as Types from '../../../proto/autogenerated/ts/types.js'
|
||||
import { LightningClient } from '../../../proto/lnd/lightning.client.js'
|
||||
import { InvoicesClient } from '../../../proto/lnd/invoices.client.js'
|
||||
import { RouterClient } from '../../../proto/lnd/router.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 { OpenChannelReq } from './openChannelReq.js';
|
||||
import { AddInvoiceReq } from './addInvoiceReq.js';
|
||||
import { PayInvoiceReq } from './payInvoiceReq.js';
|
||||
import { SendCoinsReq } from './sendCoinsReq.js';
|
||||
import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo } from './settings.js';
|
||||
import { getLogger } from '../helpers/logger.js';
|
||||
import { HtlcEvent_EventType } from '../../../proto/lnd/router.js';
|
||||
import { LiquidityProvider, LiquidityRequest } from './liquidityProvider.js';
|
||||
const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
|
||||
const deadLndRetrySeconds = 5
|
||||
export default class {
|
||||
lightning: LightningClient
|
||||
invoices: InvoicesClient
|
||||
router: RouterClient
|
||||
chainNotifier: ChainNotifierClient
|
||||
settings: LndSettings
|
||||
ready = false
|
||||
latestKnownBlockHeigh = 0
|
||||
latestKnownSettleIndex = 0
|
||||
abortController = new AbortController()
|
||||
addressPaidCb: AddressPaidCb
|
||||
invoicePaidCb: InvoicePaidCb
|
||||
newBlockCb: NewBlockCb
|
||||
htlcCb: HtlcCb
|
||||
log = getLogger({ component: 'lndManager' })
|
||||
outgoingOpsLocked = false
|
||||
liquidProvider: LiquidityProvider
|
||||
constructor(settings: LndSettings, liquidProvider: LiquidityProvider, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb) {
|
||||
this.settings = settings
|
||||
this.addressPaidCb = addressPaidCb
|
||||
this.invoicePaidCb = invoicePaidCb
|
||||
this.newBlockCb = newBlockCb
|
||||
this.htlcCb = htlcCb
|
||||
const { lndAddr, lndCertPath, lndMacaroonPath } = this.settings.mainNode
|
||||
const lndCert = fs.readFileSync(lndCertPath);
|
||||
const macaroon = fs.readFileSync(lndMacaroonPath).toString('hex');
|
||||
const sslCreds = credentials.createSsl(lndCert);
|
||||
const macaroonCreds = credentials.createFromMetadataGenerator(
|
||||
function (args: any, callback: any) {
|
||||
let metadata = new Metadata();
|
||||
metadata.add('macaroon', macaroon);
|
||||
callback(null, metadata);
|
||||
},
|
||||
);
|
||||
const creds = credentials.combineChannelCredentials(
|
||||
sslCreds,
|
||||
macaroonCreds,
|
||||
);
|
||||
const transport = new GrpcTransport({ host: lndAddr, channelCredentials: creds })
|
||||
this.lightning = new LightningClient(transport)
|
||||
this.invoices = new InvoicesClient(transport)
|
||||
this.router = new RouterClient(transport)
|
||||
this.chainNotifier = new ChainNotifierClient(transport)
|
||||
this.liquidProvider = liquidProvider
|
||||
}
|
||||
|
||||
LockOutgoingOperations(): void {
|
||||
this.outgoingOpsLocked = true
|
||||
}
|
||||
UnlockOutgoingOperations(): void {
|
||||
this.outgoingOpsLocked = false
|
||||
}
|
||||
|
||||
SetMockInvoiceAsPaid(invoice: string, amount: number): Promise<void> {
|
||||
throw new Error("SetMockInvoiceAsPaid only available in mock mode")
|
||||
}
|
||||
Stop() {
|
||||
this.abortController.abort()
|
||||
this.liquidProvider.Stop()
|
||||
}
|
||||
|
||||
async ShouldUseLiquidityProvider(req: LiquidityRequest): Promise<boolean> {
|
||||
if (this.settings.useOnlyLiquidityProvider) {
|
||||
return true
|
||||
}
|
||||
if (!this.liquidProvider.CanProviderHandle(req)) {
|
||||
return false
|
||||
}
|
||||
const channels = await this.ListChannels()
|
||||
if (channels.channels.length === 0) {
|
||||
this.log("no channels, will use liquidity provider")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
async Warmup() {
|
||||
this.SubscribeAddressPaid()
|
||||
this.SubscribeInvoicePaid()
|
||||
this.SubscribeNewBlock()
|
||||
this.SubscribeHtlcEvents()
|
||||
const now = Date.now()
|
||||
return new Promise<void>((res, rej) => {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
await this.GetInfo()
|
||||
clearInterval(interval)
|
||||
this.ready = true
|
||||
res()
|
||||
} catch (err) {
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
async GetInfo(): Promise<NodeInfo> {
|
||||
const res = await this.lightning.getInfo({}, DeadLineMetadata())
|
||||
return res.response
|
||||
}
|
||||
async ListPendingChannels(): Promise<PendingChannelsResponse> {
|
||||
const res = await this.lightning.pendingChannels({}, DeadLineMetadata())
|
||||
return res.response
|
||||
}
|
||||
async ListChannels(): Promise<ListChannelsResponse> {
|
||||
const res = await this.lightning.listChannels({
|
||||
activeOnly: false, inactiveOnly: false, privateOnly: false, publicOnly: false, peer: Buffer.alloc(0)
|
||||
}, DeadLineMetadata())
|
||||
return res.response
|
||||
}
|
||||
async ListClosedChannels(): Promise<ClosedChannelsResponse> {
|
||||
const res = await this.lightning.closedChannels({
|
||||
abandoned: true,
|
||||
breach: true,
|
||||
cooperative: true,
|
||||
fundingCanceled: true,
|
||||
localForce: true,
|
||||
remoteForce: true
|
||||
}, DeadLineMetadata())
|
||||
return res.response
|
||||
}
|
||||
|
||||
async Health(): Promise<void> {
|
||||
if (!this.ready) {
|
||||
throw new Error("not ready")
|
||||
}
|
||||
const info = await this.GetInfo()
|
||||
if (!info.syncedToChain || !info.syncedToGraph) {
|
||||
throw new Error("not synced")
|
||||
}
|
||||
}
|
||||
|
||||
RestartStreams() {
|
||||
if (!this.ready) {
|
||||
return
|
||||
}
|
||||
this.log("LND is dead, will try to reconnect in", deadLndRetrySeconds, "seconds")
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
await this.Health()
|
||||
this.log("LND is back online")
|
||||
clearInterval(interval)
|
||||
this.Warmup()
|
||||
} catch (err) {
|
||||
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 => {
|
||||
this.htlcCb(htlc)
|
||||
})
|
||||
stream.responses.onError(error => {
|
||||
this.log("Error with subscribeHtlcEvents stream")
|
||||
})
|
||||
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 })
|
||||
stream.responses.onMessage(block => {
|
||||
this.newBlockCb(block.height)
|
||||
})
|
||||
stream.responses.onError(error => {
|
||||
this.log("Error with onchain tx stream")
|
||||
})
|
||||
stream.responses.onComplete(() => {
|
||||
this.log("onchain tx stream closed")
|
||||
})
|
||||
}
|
||||
|
||||
SubscribeAddressPaid(): void {
|
||||
const stream = this.lightning.subscribeTransactions({
|
||||
account: "",
|
||||
endHeight: 0,
|
||||
startHeight: this.latestKnownBlockHeigh,
|
||||
}, { abort: this.abortController.signal })
|
||||
stream.responses.onMessage(tx => {
|
||||
if (tx.blockHeight > this.latestKnownBlockHeigh) {
|
||||
this.latestKnownBlockHeigh = tx.blockHeight
|
||||
}
|
||||
if (tx.numConfirmations === 0) { // only process pending transactions, confirmed transaction are processed by the newBlock CB
|
||||
tx.outputDetails.forEach(output => {
|
||||
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.onComplete(() => {
|
||||
this.log("onchain tx stream closed")
|
||||
})
|
||||
}
|
||||
|
||||
SubscribeInvoicePaid(): void {
|
||||
const stream = this.lightning.subscribeInvoices({
|
||||
settleIndex: BigInt(this.latestKnownSettleIndex),
|
||||
addIndex: 0n,
|
||||
}, { abort: this.abortController.signal })
|
||||
stream.responses.onMessage(invoice => {
|
||||
if (invoice.state === Invoice_InvoiceState.SETTLED) {
|
||||
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.onComplete(() => {
|
||||
this.log("invoice stream closed")
|
||||
this.RestartStreams()
|
||||
})
|
||||
}
|
||||
|
||||
async NewAddress(addressType: Types.AddressType): Promise<NewAddressResponse> {
|
||||
this.log("generating new address")
|
||||
await this.Health()
|
||||
let lndAddressType: AddressType
|
||||
switch (addressType) {
|
||||
case Types.AddressType.NESTED_PUBKEY_HASH:
|
||||
lndAddressType = AddressType.NESTED_PUBKEY_HASH
|
||||
break;
|
||||
case Types.AddressType.WITNESS_PUBKEY_HASH:
|
||||
lndAddressType = AddressType.WITNESS_PUBKEY_HASH
|
||||
break;
|
||||
case Types.AddressType.TAPROOT_PUBKEY:
|
||||
lndAddressType = AddressType.TAPROOT_PUBKEY
|
||||
break;
|
||||
default:
|
||||
throw new Error("unknown address type " + addressType)
|
||||
}
|
||||
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()
|
||||
const shouldUseLiquidityProvider = await this.ShouldUseLiquidityProvider({ action: 'receive', amount: value })
|
||||
if (shouldUseLiquidityProvider) {
|
||||
const invoice = await this.liquidProvider.AddInvoice(value, memo)
|
||||
return { payRequest: invoice }
|
||||
}
|
||||
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 }
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
async ChannelBalance(): Promise<{ local: number, remote: number }> {
|
||||
const res = await this.lightning.channelBalance({})
|
||||
const r = res.response
|
||||
return { local: r.localBalance ? Number(r.localBalance.sat) : 0, remote: r.remoteBalance ? Number(r.remoteBalance.sat) : 0 }
|
||||
}
|
||||
async PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<PaidInvoice> {
|
||||
if (this.outgoingOpsLocked) {
|
||||
this.log("outgoing ops locked, rejecting payment request")
|
||||
throw new Error("lnd node is currently out of sync")
|
||||
}
|
||||
await this.Health()
|
||||
this.log("paying invoice", invoice, "for", amount, "sats")
|
||||
const shouldUseLiquidityProvider = await this.ShouldUseLiquidityProvider({ action: 'spend', amount })
|
||||
if (shouldUseLiquidityProvider) {
|
||||
const res = await this.liquidProvider.PayInvoice(invoice)
|
||||
return { feeSat: res.network_fee + res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage }
|
||||
}
|
||||
const abortController = new AbortController()
|
||||
const req = PayInvoiceReq(invoice, amount, feeLimit)
|
||||
const stream = this.router.sendPaymentV2(req, { abort: abortController.signal })
|
||||
return new Promise((res, rej) => {
|
||||
stream.responses.onError(error => {
|
||||
this.log("invoice payment failed", error)
|
||||
rej(error)
|
||||
})
|
||||
stream.responses.onMessage(payment => {
|
||||
switch (payment.status) {
|
||||
case Payment_PaymentStatus.FAILED:
|
||||
console.log(payment)
|
||||
this.log("invoice payment failed", payment.failureReason)
|
||||
rej(PaymentFailureReason[payment.failureReason])
|
||||
return
|
||||
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({
|
||||
addrToAmount: { [address]: BigInt(amount) },
|
||||
minConfs: 1,
|
||||
spendUnconfirmed: false,
|
||||
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")
|
||||
throw new Error("lnd node is currently out of sync")
|
||||
}
|
||||
await this.Health()
|
||||
this.log("sending chain TX for", amount, "sats", "to", address)
|
||||
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())
|
||||
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 GetBalance(): Promise<BalanceInfo> {
|
||||
const wRes = await this.lightning.walletBalance({}, DeadLineMetadata())
|
||||
const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response
|
||||
const { response } = await this.lightning.listChannels({
|
||||
activeOnly: false, inactiveOnly: false, privateOnly: false, publicOnly: false, peer: Buffer.alloc(0)
|
||||
}, DeadLineMetadata())
|
||||
const channelsBalance = response.channels.map(c => ({
|
||||
channelId: c.chanId,
|
||||
localBalanceSats: Number(c.localBalance),
|
||||
remoteBalanceSats: Number(c.remoteBalance),
|
||||
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 GetAllPaidInvoices(max: number) {
|
||||
const res = await this.lightning.listInvoices({ indexOffset: 0n, numMaxInvoices: BigInt(max), pendingOnly: false, reversed: true }, DeadLineMetadata())
|
||||
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,
|
||||
perm: true,
|
||||
timeout: 0n
|
||||
}, 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)
|
||||
const stream = this.lightning.openChannel(req, { abort: abortController.signal })
|
||||
return new Promise((res, rej) => {
|
||||
stream.responses.onMessage(message => {
|
||||
console.log("message", message)
|
||||
switch (message.update.oneofKind) {
|
||||
case 'chanPending':
|
||||
res(Buffer.from(message.pendingChanId).toString('base64'))
|
||||
break
|
||||
}
|
||||
})
|
||||
stream.responses.onError(error => {
|
||||
console.log("error", error)
|
||||
rej(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
//const grpc = require('@grpc/grpc-js');
|
||||
import crypto from 'crypto'
|
||||
import { credentials, Metadata } from '@grpc/grpc-js'
|
||||
import { GrpcTransport } from "@protobuf-ts/grpc-transport";
|
||||
import fs from 'fs'
|
||||
import * as Types from '../../../proto/autogenerated/ts/types.js'
|
||||
import { LightningClient } from '../../../proto/lnd/lightning.client.js'
|
||||
import { InvoicesClient } from '../../../proto/lnd/invoices.client.js'
|
||||
import { RouterClient } from '../../../proto/lnd/router.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 { OpenChannelReq } from './openChannelReq.js';
|
||||
import { AddInvoiceReq } from './addInvoiceReq.js';
|
||||
import { PayInvoiceReq } from './payInvoiceReq.js';
|
||||
import { SendCoinsReq } from './sendCoinsReq.js';
|
||||
import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo } from './settings.js';
|
||||
import { getLogger } from '../helpers/logger.js';
|
||||
import { HtlcEvent_EventType } from '../../../proto/lnd/router.js';
|
||||
import { LiquidityProvider, LiquidityRequest } from './liquidityProvider.js';
|
||||
const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
|
||||
const deadLndRetrySeconds = 5
|
||||
export default class {
|
||||
lightning: LightningClient
|
||||
invoices: InvoicesClient
|
||||
router: RouterClient
|
||||
chainNotifier: ChainNotifierClient
|
||||
settings: LndSettings
|
||||
ready = false
|
||||
latestKnownBlockHeigh = 0
|
||||
latestKnownSettleIndex = 0
|
||||
abortController = new AbortController()
|
||||
addressPaidCb: AddressPaidCb
|
||||
invoicePaidCb: InvoicePaidCb
|
||||
newBlockCb: NewBlockCb
|
||||
htlcCb: HtlcCb
|
||||
log = getLogger({ component: 'lndManager' })
|
||||
outgoingOpsLocked = false
|
||||
liquidProvider: LiquidityProvider
|
||||
useOnlyLiquidityProvider = false
|
||||
constructor(settings: LndSettings, provider: { liquidProvider: LiquidityProvider, useOnly?: boolean }, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb) {
|
||||
this.settings = settings
|
||||
this.addressPaidCb = addressPaidCb
|
||||
this.invoicePaidCb = invoicePaidCb
|
||||
this.newBlockCb = newBlockCb
|
||||
this.htlcCb = htlcCb
|
||||
const { lndAddr, lndCertPath, lndMacaroonPath } = this.settings.mainNode
|
||||
const lndCert = fs.readFileSync(lndCertPath);
|
||||
const macaroon = fs.readFileSync(lndMacaroonPath).toString('hex');
|
||||
const sslCreds = credentials.createSsl(lndCert);
|
||||
const macaroonCreds = credentials.createFromMetadataGenerator(
|
||||
function (args: any, callback: any) {
|
||||
let metadata = new Metadata();
|
||||
metadata.add('macaroon', macaroon);
|
||||
callback(null, metadata);
|
||||
},
|
||||
);
|
||||
const creds = credentials.combineChannelCredentials(
|
||||
sslCreds,
|
||||
macaroonCreds,
|
||||
);
|
||||
const transport = new GrpcTransport({ host: lndAddr, channelCredentials: creds })
|
||||
this.lightning = new LightningClient(transport)
|
||||
this.invoices = new InvoicesClient(transport)
|
||||
this.router = new RouterClient(transport)
|
||||
this.chainNotifier = new ChainNotifierClient(transport)
|
||||
this.liquidProvider = provider.liquidProvider
|
||||
this.useOnlyLiquidityProvider = !!provider.useOnly
|
||||
}
|
||||
|
||||
LockOutgoingOperations(): void {
|
||||
this.outgoingOpsLocked = true
|
||||
}
|
||||
UnlockOutgoingOperations(): void {
|
||||
this.outgoingOpsLocked = false
|
||||
}
|
||||
|
||||
SetMockInvoiceAsPaid(invoice: string, amount: number): Promise<void> {
|
||||
throw new Error("SetMockInvoiceAsPaid only available in mock mode")
|
||||
}
|
||||
Stop() {
|
||||
this.abortController.abort()
|
||||
this.liquidProvider.Stop()
|
||||
}
|
||||
|
||||
async ShouldUseLiquidityProvider(req: LiquidityRequest): Promise<boolean> {
|
||||
if (this.useOnlyLiquidityProvider) {
|
||||
return true
|
||||
}
|
||||
if (!this.liquidProvider.CanProviderHandle(req)) {
|
||||
return false
|
||||
}
|
||||
const channels = await this.ListChannels()
|
||||
if (channels.channels.length === 0) {
|
||||
this.log("no channels, will use liquidity provider")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
async Warmup() {
|
||||
this.SubscribeAddressPaid()
|
||||
this.SubscribeInvoicePaid()
|
||||
this.SubscribeNewBlock()
|
||||
this.SubscribeHtlcEvents()
|
||||
const now = Date.now()
|
||||
return new Promise<void>((res, rej) => {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
await this.GetInfo()
|
||||
clearInterval(interval)
|
||||
this.ready = true
|
||||
res()
|
||||
} catch (err) {
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
async GetInfo(): Promise<NodeInfo> {
|
||||
const res = await this.lightning.getInfo({}, DeadLineMetadata())
|
||||
return res.response
|
||||
}
|
||||
async ListPendingChannels(): Promise<PendingChannelsResponse> {
|
||||
const res = await this.lightning.pendingChannels({}, DeadLineMetadata())
|
||||
return res.response
|
||||
}
|
||||
async ListChannels(): Promise<ListChannelsResponse> {
|
||||
const res = await this.lightning.listChannels({
|
||||
activeOnly: false, inactiveOnly: false, privateOnly: false, publicOnly: false, peer: Buffer.alloc(0)
|
||||
}, DeadLineMetadata())
|
||||
return res.response
|
||||
}
|
||||
async ListClosedChannels(): Promise<ClosedChannelsResponse> {
|
||||
const res = await this.lightning.closedChannels({
|
||||
abandoned: true,
|
||||
breach: true,
|
||||
cooperative: true,
|
||||
fundingCanceled: true,
|
||||
localForce: true,
|
||||
remoteForce: true
|
||||
}, DeadLineMetadata())
|
||||
return res.response
|
||||
}
|
||||
|
||||
async Health(): Promise<void> {
|
||||
if (!this.ready) {
|
||||
throw new Error("not ready")
|
||||
}
|
||||
const info = await this.GetInfo()
|
||||
if (!info.syncedToChain || !info.syncedToGraph) {
|
||||
throw new Error("not synced")
|
||||
}
|
||||
}
|
||||
|
||||
RestartStreams() {
|
||||
if (!this.ready) {
|
||||
return
|
||||
}
|
||||
this.log("LND is dead, will try to reconnect in", deadLndRetrySeconds, "seconds")
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
await this.Health()
|
||||
this.log("LND is back online")
|
||||
clearInterval(interval)
|
||||
this.Warmup()
|
||||
} catch (err) {
|
||||
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 => {
|
||||
this.htlcCb(htlc)
|
||||
})
|
||||
stream.responses.onError(error => {
|
||||
this.log("Error with subscribeHtlcEvents stream")
|
||||
})
|
||||
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 })
|
||||
stream.responses.onMessage(block => {
|
||||
this.newBlockCb(block.height)
|
||||
})
|
||||
stream.responses.onError(error => {
|
||||
this.log("Error with onchain tx stream")
|
||||
})
|
||||
stream.responses.onComplete(() => {
|
||||
this.log("onchain tx stream closed")
|
||||
})
|
||||
}
|
||||
|
||||
SubscribeAddressPaid(): void {
|
||||
const stream = this.lightning.subscribeTransactions({
|
||||
account: "",
|
||||
endHeight: 0,
|
||||
startHeight: this.latestKnownBlockHeigh,
|
||||
}, { abort: this.abortController.signal })
|
||||
stream.responses.onMessage(tx => {
|
||||
if (tx.blockHeight > this.latestKnownBlockHeigh) {
|
||||
this.latestKnownBlockHeigh = tx.blockHeight
|
||||
}
|
||||
if (tx.numConfirmations === 0) { // only process pending transactions, confirmed transaction are processed by the newBlock CB
|
||||
tx.outputDetails.forEach(output => {
|
||||
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.onComplete(() => {
|
||||
this.log("onchain tx stream closed")
|
||||
})
|
||||
}
|
||||
|
||||
SubscribeInvoicePaid(): void {
|
||||
const stream = this.lightning.subscribeInvoices({
|
||||
settleIndex: BigInt(this.latestKnownSettleIndex),
|
||||
addIndex: 0n,
|
||||
}, { abort: this.abortController.signal })
|
||||
stream.responses.onMessage(invoice => {
|
||||
if (invoice.state === Invoice_InvoiceState.SETTLED) {
|
||||
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.onComplete(() => {
|
||||
this.log("invoice stream closed")
|
||||
this.RestartStreams()
|
||||
})
|
||||
}
|
||||
|
||||
async NewAddress(addressType: Types.AddressType): Promise<NewAddressResponse> {
|
||||
this.log("generating new address")
|
||||
await this.Health()
|
||||
let lndAddressType: AddressType
|
||||
switch (addressType) {
|
||||
case Types.AddressType.NESTED_PUBKEY_HASH:
|
||||
lndAddressType = AddressType.NESTED_PUBKEY_HASH
|
||||
break;
|
||||
case Types.AddressType.WITNESS_PUBKEY_HASH:
|
||||
lndAddressType = AddressType.WITNESS_PUBKEY_HASH
|
||||
break;
|
||||
case Types.AddressType.TAPROOT_PUBKEY:
|
||||
lndAddressType = AddressType.TAPROOT_PUBKEY
|
||||
break;
|
||||
default:
|
||||
throw new Error("unknown address type " + addressType)
|
||||
}
|
||||
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, useProvider = false): Promise<Invoice> {
|
||||
this.log("generating new invoice for", value, "sats")
|
||||
await this.Health()
|
||||
const shouldUseLiquidityProvider = await this.ShouldUseLiquidityProvider({ action: 'receive', amount: value })
|
||||
if (shouldUseLiquidityProvider || useProvider) {
|
||||
const invoice = await this.liquidProvider.AddInvoice(value, memo)
|
||||
return { payRequest: invoice }
|
||||
}
|
||||
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 }
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
async ChannelBalance(): Promise<{ local: number, remote: number }> {
|
||||
const res = await this.lightning.channelBalance({})
|
||||
const r = res.response
|
||||
return { local: r.localBalance ? Number(r.localBalance.sat) : 0, remote: r.remoteBalance ? Number(r.remoteBalance.sat) : 0 }
|
||||
}
|
||||
async PayInvoice(invoice: string, amount: number, feeLimit: number, useProvider = false): Promise<PaidInvoice> {
|
||||
if (this.outgoingOpsLocked) {
|
||||
this.log("outgoing ops locked, rejecting payment request")
|
||||
throw new Error("lnd node is currently out of sync")
|
||||
}
|
||||
await this.Health()
|
||||
this.log("paying invoice", invoice, "for", amount, "sats")
|
||||
const shouldUseLiquidityProvider = await this.ShouldUseLiquidityProvider({ action: 'spend', amount })
|
||||
if (shouldUseLiquidityProvider || useProvider) {
|
||||
const res = await this.liquidProvider.PayInvoice(invoice)
|
||||
return { feeSat: res.network_fee + res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage }
|
||||
}
|
||||
const abortController = new AbortController()
|
||||
const req = PayInvoiceReq(invoice, amount, feeLimit)
|
||||
const stream = this.router.sendPaymentV2(req, { abort: abortController.signal })
|
||||
return new Promise((res, rej) => {
|
||||
stream.responses.onError(error => {
|
||||
this.log("invoice payment failed", error)
|
||||
rej(error)
|
||||
})
|
||||
stream.responses.onMessage(payment => {
|
||||
switch (payment.status) {
|
||||
case Payment_PaymentStatus.FAILED:
|
||||
console.log(payment)
|
||||
this.log("invoice payment failed", payment.failureReason)
|
||||
rej(PaymentFailureReason[payment.failureReason])
|
||||
return
|
||||
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({
|
||||
addrToAmount: { [address]: BigInt(amount) },
|
||||
minConfs: 1,
|
||||
spendUnconfirmed: false,
|
||||
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")
|
||||
throw new Error("lnd node is currently out of sync")
|
||||
}
|
||||
await this.Health()
|
||||
this.log("sending chain TX for", amount, "sats", "to", address)
|
||||
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())
|
||||
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 GetBalance(): Promise<BalanceInfo> {
|
||||
const wRes = await this.lightning.walletBalance({}, DeadLineMetadata())
|
||||
const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response
|
||||
const { response } = await this.lightning.listChannels({
|
||||
activeOnly: false, inactiveOnly: false, privateOnly: false, publicOnly: false, peer: Buffer.alloc(0)
|
||||
}, DeadLineMetadata())
|
||||
const channelsBalance = response.channels.map(c => ({
|
||||
channelId: c.chanId,
|
||||
localBalanceSats: Number(c.localBalance),
|
||||
remoteBalanceSats: Number(c.remoteBalance),
|
||||
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 GetAllPaidInvoices(max: number) {
|
||||
const res = await this.lightning.listInvoices({ indexOffset: 0n, numMaxInvoices: BigInt(max), pendingOnly: false, reversed: true }, DeadLineMetadata())
|
||||
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,
|
||||
perm: true,
|
||||
timeout: 0n
|
||||
}, 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)
|
||||
const stream = this.lightning.openChannel(req, { abort: abortController.signal })
|
||||
return new Promise((res, rej) => {
|
||||
stream.responses.onMessage(message => {
|
||||
console.log("message", message)
|
||||
switch (message.update.oneofKind) {
|
||||
case 'chanPending':
|
||||
res(Buffer.from(message.pendingChanId).toString('base64'))
|
||||
break
|
||||
}
|
||||
})
|
||||
stream.responses.onError(error => {
|
||||
console.log("error", error)
|
||||
rej(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,331 @@
|
|||
import fetch from "node-fetch"
|
||||
|
||||
export class LSP {
|
||||
serviceUrl: string
|
||||
constructor(serviceUrl: string) {
|
||||
this.serviceUrl = serviceUrl
|
||||
}
|
||||
|
||||
getInfo = async () => {
|
||||
const res = await fetch(`${this.serviceUrl}/getinfo`)
|
||||
const json = await res.json() as { options: {}, uris: string[] }
|
||||
}
|
||||
|
||||
createOrder = async (req: { public_key: string }) => {
|
||||
const res = await fetch(`${this.serviceUrl}/create_order`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(req),
|
||||
headers: { "Content-Type": "application/json" }
|
||||
})
|
||||
const json = await res.json() as {}
|
||||
return json
|
||||
}
|
||||
|
||||
getOrder = async (orderId: string) => {
|
||||
const res = await fetch(`${this.serviceUrl}/get_order&order_id=${orderId}`)
|
||||
const json = await res.json() as {}
|
||||
return json
|
||||
}
|
||||
import fetch from "node-fetch"
|
||||
import { LiquidityProvider } from "./liquidityProvider.js"
|
||||
import { getLogger, PubLogger } from '../helpers/logger.js'
|
||||
import LND from "./lnd.js"
|
||||
import { AddressType } from "../../../proto/autogenerated/ts/types.js"
|
||||
import { EnvCanBeInteger } from "../helpers/envParser.js"
|
||||
export type LSPSettings = {
|
||||
olympusServiceUrl: string
|
||||
voltageServiceUrl: string
|
||||
flashsatsServiceUrl: string
|
||||
channelThreshold: number
|
||||
maxRelativeFee: number
|
||||
}
|
||||
|
||||
export const LoadLSPSettingsFromEnv = (): LSPSettings => {
|
||||
const olympusServiceUrl = process.env.OLYMPUS_LSP_URL || "https://lsps1.lnolymp.us/api/v1"
|
||||
const voltageServiceUrl = process.env.VOLTAGE_LSP_URL || "https://lsp.voltageapi.com/api/v1"
|
||||
const flashsatsServiceUrl = process.env.FLASHSATS_LSP_URL || "https://lsp.flashsats.xyz/lsp/channel"
|
||||
const channelThreshold = EnvCanBeInteger("LSP_CHANNEL_THRESHOLD", 1000000)
|
||||
const maxRelativeFee = EnvCanBeInteger("LSP_MAX_FEE_BPS", 100) / 10000
|
||||
return { olympusServiceUrl, voltageServiceUrl, channelThreshold, maxRelativeFee, flashsatsServiceUrl }
|
||||
|
||||
}
|
||||
type OlympusOrder = {
|
||||
"lsp_balance_sat": string,
|
||||
"client_balance_sat": string,
|
||||
"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"
|
||||
export type NodeSettings = {
|
||||
lndAddr: string
|
||||
lndCertPath: string
|
||||
lndMacaroonPath: string
|
||||
}
|
||||
export type LndSettings = {
|
||||
mainNode: NodeSettings
|
||||
feeRateLimit: number
|
||||
feeFixedLimit: number
|
||||
mockLnd: boolean
|
||||
liquidityProviderPub: string
|
||||
useOnlyLiquidityProvider: boolean
|
||||
|
||||
otherNode?: NodeSettings
|
||||
thirdNode?: NodeSettings
|
||||
}
|
||||
type TxOutput = {
|
||||
hash: string
|
||||
index: number
|
||||
}
|
||||
export type ChannelBalance = {
|
||||
channelId: string;
|
||||
localBalanceSats: number;
|
||||
remoteBalanceSats: number;
|
||||
htlcs: { incoming: boolean, amount: number }[]
|
||||
}
|
||||
export type BalanceInfo = {
|
||||
confirmedBalance: number;
|
||||
unconfirmedBalance: number;
|
||||
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 NewBlockCb = (height: number) => void
|
||||
export type HtlcCb = (event: HtlcEvent) => void
|
||||
|
||||
export type NodeInfo = {
|
||||
alias: string
|
||||
syncedToChain: boolean
|
||||
syncedToGraph: boolean
|
||||
blockHeight: number
|
||||
blockHash: string
|
||||
identityPubkey: string
|
||||
uris: string[]
|
||||
}
|
||||
export type Invoice = {
|
||||
payRequest: string
|
||||
}
|
||||
export type DecodedInvoice = {
|
||||
numSatoshis: number
|
||||
paymentHash: string
|
||||
}
|
||||
export type PaidInvoice = {
|
||||
feeSat: number
|
||||
valueSat: number
|
||||
paymentPreimage: string
|
||||
import { HtlcEvent } from "../../../proto/lnd/router"
|
||||
export type NodeSettings = {
|
||||
lndAddr: string
|
||||
lndCertPath: string
|
||||
lndMacaroonPath: string
|
||||
}
|
||||
export type LndSettings = {
|
||||
mainNode: NodeSettings
|
||||
feeRateLimit: number
|
||||
feeFixedLimit: number
|
||||
mockLnd: boolean
|
||||
|
||||
otherNode?: NodeSettings
|
||||
thirdNode?: NodeSettings
|
||||
}
|
||||
type TxOutput = {
|
||||
hash: string
|
||||
index: number
|
||||
}
|
||||
export type ChannelBalance = {
|
||||
channelId: string;
|
||||
localBalanceSats: number;
|
||||
remoteBalanceSats: number;
|
||||
htlcs: { incoming: boolean, amount: number }[]
|
||||
}
|
||||
export type BalanceInfo = {
|
||||
confirmedBalance: number;
|
||||
unconfirmedBalance: number;
|
||||
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 NewBlockCb = (height: number) => void
|
||||
export type HtlcCb = (event: HtlcEvent) => void
|
||||
|
||||
export type NodeInfo = {
|
||||
alias: string
|
||||
syncedToChain: boolean
|
||||
syncedToGraph: boolean
|
||||
blockHeight: number
|
||||
blockHash: string
|
||||
identityPubkey: string
|
||||
uris: string[]
|
||||
}
|
||||
export type Invoice = {
|
||||
payRequest: string
|
||||
}
|
||||
export type DecodedInvoice = {
|
||||
numSatoshis: number
|
||||
paymentHash: string
|
||||
}
|
||||
export type PaidInvoice = {
|
||||
feeSat: number
|
||||
valueSat: number
|
||||
paymentPreimage: string
|
||||
}
|
||||
|
|
@ -1,253 +1,259 @@
|
|||
import fetch from "node-fetch"
|
||||
import Storage, { LoadStorageSettingsFromEnv } from '../storage/index.js'
|
||||
import * as Types from '../../../proto/autogenerated/ts/types.js'
|
||||
import ProductManager from './productManager.js'
|
||||
import ApplicationManager from './applicationManager.js'
|
||||
import PaymentManager, { PendingTx } from './paymentManager.js'
|
||||
import { MainSettings } from './settings.js'
|
||||
import LND from "../lnd/lnd.js"
|
||||
import { AddressPaidCb, HtlcCb, InvoicePaidCb, NewBlockCb } from "../lnd/settings.js"
|
||||
import { ERROR, getLogger, PubLogger } from "../helpers/logger.js"
|
||||
import AppUserManager from "./appUserManager.js"
|
||||
import { Application } from '../storage/entity/Application.js'
|
||||
import { UserReceivingInvoice } from '../storage/entity/UserReceivingInvoice.js'
|
||||
import { UnsignedEvent } from '../nostr/tools/event.js'
|
||||
import { NostrSend } from '../nostr/handler.js'
|
||||
import MetricsManager from '../metrics/index.js'
|
||||
import { LoggedEvent } from '../storage/eventsLog.js'
|
||||
import { LiquidityProvider } from "../lnd/liquidityProvider.js"
|
||||
|
||||
type UserOperationsSub = {
|
||||
id: string
|
||||
newIncomingInvoice: (operation: Types.UserOperation) => void
|
||||
newOutgoingInvoice: (operation: Types.UserOperation) => void
|
||||
newIncomingTx: (operation: Types.UserOperation) => void
|
||||
newOutgoingTx: (operation: Types.UserOperation) => void
|
||||
}
|
||||
const appTag = "Lightning.Pub"
|
||||
export default class {
|
||||
storage: Storage
|
||||
lnd: LND
|
||||
settings: MainSettings
|
||||
userOperationsSub: UserOperationsSub | null = null
|
||||
productManager: ProductManager
|
||||
applicationManager: ApplicationManager
|
||||
appUserManager: AppUserManager
|
||||
paymentManager: PaymentManager
|
||||
paymentSubs: Record<string, ((op: Types.UserOperation) => void) | null> = {}
|
||||
metricsManager: MetricsManager
|
||||
liquidProvider: LiquidityProvider
|
||||
nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") }
|
||||
constructor(settings: MainSettings, storage: Storage) {
|
||||
this.settings = settings
|
||||
this.storage = storage
|
||||
this.liquidProvider = new LiquidityProvider(settings.lndSettings.liquidityProviderPub, this.invoicePaidCb)
|
||||
this.lnd = new LND(settings.lndSettings, this.liquidProvider, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb)
|
||||
this.metricsManager = new MetricsManager(this.storage, this.lnd)
|
||||
|
||||
this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings, this.addressPaidCb, this.invoicePaidCb)
|
||||
this.productManager = new ProductManager(this.storage, this.paymentManager, this.settings)
|
||||
this.applicationManager = new ApplicationManager(this.storage, this.settings, this.paymentManager)
|
||||
this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager)
|
||||
|
||||
}
|
||||
Stop() {
|
||||
this.lnd.Stop()
|
||||
this.applicationManager.Stop()
|
||||
this.paymentManager.Stop()
|
||||
}
|
||||
|
||||
StartBeacons() {
|
||||
this.applicationManager.StartAppsServiceBeacon(app => {
|
||||
this.UpdateBeacon(app, { type: 'service', name: app.name })
|
||||
})
|
||||
}
|
||||
|
||||
attachNostrSend(f: NostrSend) {
|
||||
this.nostrSend = f
|
||||
this.liquidProvider.attachNostrSend(f)
|
||||
}
|
||||
|
||||
htlcCb: HtlcCb = (e) => {
|
||||
this.metricsManager.HtlcCb(e)
|
||||
}
|
||||
|
||||
newBlockCb: NewBlockCb = (height) => {
|
||||
this.NewBlockHandler(height)
|
||||
}
|
||||
|
||||
NewBlockHandler = async (height: number) => {
|
||||
let confirmed: (PendingTx & { confs: number; })[]
|
||||
let log = getLogger({})
|
||||
|
||||
try {
|
||||
const balanceEvents = await this.paymentManager.GetLndBalance()
|
||||
await this.metricsManager.NewBlockCb(height, balanceEvents)
|
||||
confirmed = await this.paymentManager.CheckNewlyConfirmedTxs(height)
|
||||
} catch (err: any) {
|
||||
log(ERROR, "failed to check transactions after new block", err.message || err)
|
||||
return
|
||||
}
|
||||
await Promise.all(confirmed.map(async c => {
|
||||
if (c.type === 'outgoing') {
|
||||
await this.storage.paymentStorage.UpdateUserTransactionPayment(c.tx.serial_id, { confs: c.confs })
|
||||
const { linkedApplication, user, address, paid_amount: amount, service_fees: serviceFee, serial_id: serialId, chain_fees } = c.tx;
|
||||
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 }
|
||||
this.sendOperationToNostr(linkedApplication!, user.user_id, op)
|
||||
} else {
|
||||
this.storage.StartTransaction(async tx => {
|
||||
const { user_address: userAddress, paid_amount: amount, service_fee: serviceFee, serial_id: serialId, tx_hash } = c.tx
|
||||
if (!userAddress.linkedApplication) {
|
||||
log(ERROR, "an address was paid, that has no linked application")
|
||||
return
|
||||
}
|
||||
const updateResult = await this.storage.paymentStorage.UpdateAddressReceivingTransaction(serialId, { confs: c.confs }, tx)
|
||||
if (!updateResult.affected) {
|
||||
throw new Error("unable to flag chain transaction as paid")
|
||||
}
|
||||
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 })
|
||||
await this.storage.userStorage.IncrementUserBalance(userAddress.user.user_id, amount - serviceFee, addressData, tx)
|
||||
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({})
|
||||
if (!userAddress.linkedApplication) {
|
||||
log(ERROR, "an address was paid, that has no linked application")
|
||||
return
|
||||
}
|
||||
log = getLogger({ appName: userAddress.linkedApplication.name })
|
||||
const isAppUserPayment = userAddress.user.user_id !== userAddress.linkedApplication.owner.user_id
|
||||
let fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_TX, amount, isAppUserPayment)
|
||||
if (userAddress.linkedApplication && userAddress.linkedApplication.owner.user_id === userAddress.user.user_id) {
|
||||
fee = 0
|
||||
}
|
||||
try {
|
||||
// This call will fail if the transaction is already registered
|
||||
const addedTx = await this.storage.paymentStorage.AddAddressReceivingTransaction(userAddress, txOutput.hash, txOutput.index, amount, fee, internal, blockHeight, 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)
|
||||
if (fee > 0) {
|
||||
await this.storage.userStorage.IncrementUserBalance(userAddress.linkedApplication.owner.user_id, fee, 'fees', tx)
|
||||
}
|
||||
|
||||
}
|
||||
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 {
|
||||
log(ERROR, "cannot process address paid transaction, already registered")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
invoicePaidCb: InvoicePaidCb = (paymentRequest, amount, internal) => {
|
||||
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 }
|
||||
if (userInvoice.paid_at_unix > 0 && !internal && userInvoice.paidByLnd) { log("invoice already paid by lnd"); return }
|
||||
if (!userInvoice.linkedApplication) {
|
||||
log(ERROR, "an invoice was paid, that has no linked application")
|
||||
return
|
||||
}
|
||||
log = getLogger({ appName: userInvoice.linkedApplication.name })
|
||||
const isAppUserPayment = userInvoice.user.user_id !== userInvoice.linkedApplication.owner.user_id
|
||||
let fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_INVOICE, amount, isAppUserPayment)
|
||||
if (userInvoice.linkedApplication && userInvoice.linkedApplication.owner.user_id === userInvoice.user.user_id) {
|
||||
fee = 0
|
||||
}
|
||||
try {
|
||||
await this.storage.paymentStorage.FlagInvoiceAsPaid(userInvoice, amount, fee, internal, tx)
|
||||
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 })
|
||||
await this.storage.userStorage.IncrementUserBalance(userInvoice.user.user_id, amount - fee, userInvoice.invoice, tx)
|
||||
if (fee > 0) {
|
||||
await this.storage.userStorage.IncrementUserBalance(userInvoice.linkedApplication.owner.user_id, fee, 'fees', tx)
|
||||
}
|
||||
await this.triggerPaidCallback(log, userInvoice.callbackUrl)
|
||||
const operationId = `${Types.UserOperationType.INCOMING_INVOICE}-${userInvoice.serial_id}`
|
||||
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")
|
||||
} catch (err: any) {
|
||||
log(ERROR, "cannot process paid invoice", err.message || "")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async triggerPaidCallback(log: PubLogger, url: string) {
|
||||
if (!url) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await fetch(url + "&ok=true")
|
||||
} catch (err: any) {
|
||||
log(ERROR, "error sending paid callback for invoice", err.message || "")
|
||||
}
|
||||
}
|
||||
|
||||
async sendOperationToNostr(app: Application, userId: string, op: Types.UserOperation) {
|
||||
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")
|
||||
return
|
||||
}
|
||||
const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' }
|
||||
this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'content', content: JSON.stringify(message), pub: user.nostr_public_key })
|
||||
}
|
||||
|
||||
async UpdateBeacon(app: Application, content: { type: 'service', name: string }) {
|
||||
if (!app.nostr_public_key) {
|
||||
getLogger({ appName: app.name })("cannot update beacon, public key not set")
|
||||
return
|
||||
}
|
||||
const tags = [["d", appTag]]
|
||||
const event: UnsignedEvent = {
|
||||
content: JSON.stringify(content),
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 30078,
|
||||
pubkey: app.nostr_public_key,
|
||||
tags,
|
||||
}
|
||||
this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'event', event })
|
||||
}
|
||||
|
||||
async createZapReceipt(log: PubLogger, invoice: UserReceivingInvoice) {
|
||||
const zapInfo = invoice.zap_info
|
||||
if (!zapInfo || !invoice.linkedApplication || !invoice.linkedApplication.nostr_public_key) {
|
||||
log(ERROR, "no zap info linked to payment")
|
||||
return
|
||||
}
|
||||
const tags = [["p", zapInfo.pub], ["bolt11", invoice.invoice], ["description", zapInfo.description]]
|
||||
if (zapInfo.eventId) {
|
||||
tags.push(["e", zapInfo.eventId])
|
||||
}
|
||||
const event: UnsignedEvent = {
|
||||
content: "",
|
||||
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 })
|
||||
}
|
||||
}
|
||||
import fetch from "node-fetch"
|
||||
import Storage, { LoadStorageSettingsFromEnv } from '../storage/index.js'
|
||||
import * as Types from '../../../proto/autogenerated/ts/types.js'
|
||||
import ProductManager from './productManager.js'
|
||||
import ApplicationManager from './applicationManager.js'
|
||||
import PaymentManager, { PendingTx } from './paymentManager.js'
|
||||
import { MainSettings } from './settings.js'
|
||||
import LND from "../lnd/lnd.js"
|
||||
import { AddressPaidCb, HtlcCb, InvoicePaidCb, NewBlockCb } from "../lnd/settings.js"
|
||||
import { ERROR, getLogger, PubLogger } from "../helpers/logger.js"
|
||||
import AppUserManager from "./appUserManager.js"
|
||||
import { Application } from '../storage/entity/Application.js'
|
||||
import { UserReceivingInvoice } from '../storage/entity/UserReceivingInvoice.js'
|
||||
import { UnsignedEvent } from '../nostr/tools/event.js'
|
||||
import { NostrSend } from '../nostr/handler.js'
|
||||
import MetricsManager from '../metrics/index.js'
|
||||
import { LoggedEvent } from '../storage/eventsLog.js'
|
||||
import { LiquidityProvider } from "../lnd/liquidityProvider.js"
|
||||
import { LiquidityManager } from "./liquidityManager.js"
|
||||
|
||||
type UserOperationsSub = {
|
||||
id: string
|
||||
newIncomingInvoice: (operation: Types.UserOperation) => void
|
||||
newOutgoingInvoice: (operation: Types.UserOperation) => void
|
||||
newIncomingTx: (operation: Types.UserOperation) => void
|
||||
newOutgoingTx: (operation: Types.UserOperation) => void
|
||||
}
|
||||
const appTag = "Lightning.Pub"
|
||||
export default class {
|
||||
storage: Storage
|
||||
lnd: LND
|
||||
settings: MainSettings
|
||||
userOperationsSub: UserOperationsSub | null = null
|
||||
productManager: ProductManager
|
||||
applicationManager: ApplicationManager
|
||||
appUserManager: AppUserManager
|
||||
paymentManager: PaymentManager
|
||||
paymentSubs: Record<string, ((op: Types.UserOperation) => void) | null> = {}
|
||||
metricsManager: MetricsManager
|
||||
liquidProvider: LiquidityProvider
|
||||
liquidityManager: LiquidityManager
|
||||
nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") }
|
||||
constructor(settings: MainSettings, storage: Storage) {
|
||||
this.settings = settings
|
||||
this.storage = storage
|
||||
this.liquidProvider = new LiquidityProvider(settings.liquiditySettings.liquidityProviderPub, this.invoicePaidCb)
|
||||
const provider = { liquidProvider: this.liquidProvider, useOnly: settings.liquiditySettings.useOnlyLiquidityProvider }
|
||||
this.lnd = new LND(settings.lndSettings, provider, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb)
|
||||
this.liquidityManager = new LiquidityManager(this.settings.liquiditySettings, this.storage, this.liquidProvider, this.lnd)
|
||||
this.metricsManager = new MetricsManager(this.storage, this.lnd)
|
||||
|
||||
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)
|
||||
this.applicationManager = new ApplicationManager(this.storage, this.settings, this.paymentManager)
|
||||
this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager)
|
||||
}
|
||||
|
||||
Stop() {
|
||||
this.lnd.Stop()
|
||||
this.applicationManager.Stop()
|
||||
this.paymentManager.Stop()
|
||||
}
|
||||
|
||||
StartBeacons() {
|
||||
this.applicationManager.StartAppsServiceBeacon(app => {
|
||||
this.UpdateBeacon(app, { type: 'service', name: app.name })
|
||||
})
|
||||
}
|
||||
|
||||
attachNostrSend(f: NostrSend) {
|
||||
this.nostrSend = f
|
||||
this.liquidProvider.attachNostrSend(f)
|
||||
}
|
||||
|
||||
htlcCb: HtlcCb = (e) => {
|
||||
this.metricsManager.HtlcCb(e)
|
||||
}
|
||||
|
||||
newBlockCb: NewBlockCb = (height) => {
|
||||
this.NewBlockHandler(height)
|
||||
}
|
||||
|
||||
NewBlockHandler = async (height: number) => {
|
||||
let confirmed: (PendingTx & { confs: number; })[]
|
||||
let log = getLogger({})
|
||||
|
||||
try {
|
||||
const balanceEvents = await this.paymentManager.GetLndBalance()
|
||||
await this.metricsManager.NewBlockCb(height, balanceEvents)
|
||||
confirmed = await this.paymentManager.CheckNewlyConfirmedTxs(height)
|
||||
this.liquidityManager.onNewBlock()
|
||||
} catch (err: any) {
|
||||
log(ERROR, "failed to check transactions after new block", err.message || err)
|
||||
return
|
||||
}
|
||||
await Promise.all(confirmed.map(async c => {
|
||||
if (c.type === 'outgoing') {
|
||||
await this.storage.paymentStorage.UpdateUserTransactionPayment(c.tx.serial_id, { confs: c.confs })
|
||||
const { linkedApplication, user, address, paid_amount: amount, service_fees: serviceFee, serial_id: serialId, chain_fees } = c.tx;
|
||||
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 }
|
||||
this.sendOperationToNostr(linkedApplication!, user.user_id, op)
|
||||
} else {
|
||||
this.storage.StartTransaction(async tx => {
|
||||
const { user_address: userAddress, paid_amount: amount, service_fee: serviceFee, serial_id: serialId, tx_hash } = c.tx
|
||||
if (!userAddress.linkedApplication) {
|
||||
log(ERROR, "an address was paid, that has no linked application")
|
||||
return
|
||||
}
|
||||
const updateResult = await this.storage.paymentStorage.UpdateAddressReceivingTransaction(serialId, { confs: c.confs }, tx)
|
||||
if (!updateResult.affected) {
|
||||
throw new Error("unable to flag chain transaction as paid")
|
||||
}
|
||||
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 })
|
||||
await this.storage.userStorage.IncrementUserBalance(userAddress.user.user_id, amount - serviceFee, addressData, tx)
|
||||
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({})
|
||||
if (!userAddress.linkedApplication) {
|
||||
log(ERROR, "an address was paid, that has no linked application")
|
||||
return
|
||||
}
|
||||
log = getLogger({ appName: userAddress.linkedApplication.name })
|
||||
const isAppUserPayment = userAddress.user.user_id !== userAddress.linkedApplication.owner.user_id
|
||||
let fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_TX, amount, isAppUserPayment)
|
||||
if (userAddress.linkedApplication && userAddress.linkedApplication.owner.user_id === userAddress.user.user_id) {
|
||||
fee = 0
|
||||
}
|
||||
try {
|
||||
// This call will fail if the transaction is already registered
|
||||
const addedTx = await this.storage.paymentStorage.AddAddressReceivingTransaction(userAddress, txOutput.hash, txOutput.index, amount, fee, internal, blockHeight, 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)
|
||||
if (fee > 0) {
|
||||
await this.storage.userStorage.IncrementUserBalance(userAddress.linkedApplication.owner.user_id, fee, 'fees', tx)
|
||||
}
|
||||
|
||||
}
|
||||
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 {
|
||||
log(ERROR, "cannot process address paid transaction, already registered")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
invoicePaidCb: InvoicePaidCb = (paymentRequest, amount, internal) => {
|
||||
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 }
|
||||
if (userInvoice.paid_at_unix > 0 && !internal && userInvoice.paidByLnd) { log("invoice already paid by lnd"); return }
|
||||
if (!userInvoice.linkedApplication) {
|
||||
log(ERROR, "an invoice was paid, that has no linked application")
|
||||
return
|
||||
}
|
||||
log = getLogger({ appName: userInvoice.linkedApplication.name })
|
||||
const isAppUserPayment = userInvoice.user.user_id !== userInvoice.linkedApplication.owner.user_id
|
||||
let fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_INVOICE, amount, isAppUserPayment)
|
||||
if (userInvoice.linkedApplication && userInvoice.linkedApplication.owner.user_id === userInvoice.user.user_id) {
|
||||
fee = 0
|
||||
}
|
||||
try {
|
||||
await this.storage.paymentStorage.FlagInvoiceAsPaid(userInvoice, amount, fee, internal, tx)
|
||||
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 })
|
||||
await this.storage.userStorage.IncrementUserBalance(userInvoice.user.user_id, amount - fee, userInvoice.invoice, tx)
|
||||
if (fee > 0) {
|
||||
await this.storage.userStorage.IncrementUserBalance(userInvoice.linkedApplication.owner.user_id, fee, 'fees', tx)
|
||||
}
|
||||
await this.triggerPaidCallback(log, userInvoice.callbackUrl)
|
||||
const operationId = `${Types.UserOperationType.INCOMING_INVOICE}-${userInvoice.serial_id}`
|
||||
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()
|
||||
} catch (err: any) {
|
||||
log(ERROR, "cannot process paid invoice", err.message || "")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async triggerPaidCallback(log: PubLogger, url: string) {
|
||||
if (!url) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await fetch(url + "&ok=true")
|
||||
} catch (err: any) {
|
||||
log(ERROR, "error sending paid callback for invoice", err.message || "")
|
||||
}
|
||||
}
|
||||
|
||||
async sendOperationToNostr(app: Application, userId: string, op: Types.UserOperation) {
|
||||
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")
|
||||
return
|
||||
}
|
||||
const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' }
|
||||
this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'content', content: JSON.stringify(message), pub: user.nostr_public_key })
|
||||
}
|
||||
|
||||
async UpdateBeacon(app: Application, content: { type: 'service', name: string }) {
|
||||
if (!app.nostr_public_key) {
|
||||
getLogger({ appName: app.name })("cannot update beacon, public key not set")
|
||||
return
|
||||
}
|
||||
const tags = [["d", appTag]]
|
||||
const event: UnsignedEvent = {
|
||||
content: JSON.stringify(content),
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 30078,
|
||||
pubkey: app.nostr_public_key,
|
||||
tags,
|
||||
}
|
||||
this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'event', event })
|
||||
}
|
||||
|
||||
async createZapReceipt(log: PubLogger, invoice: UserReceivingInvoice) {
|
||||
const zapInfo = invoice.zap_info
|
||||
if (!zapInfo || !invoice.linkedApplication || !invoice.linkedApplication.nostr_public_key) {
|
||||
log(ERROR, "no zap info linked to payment")
|
||||
return
|
||||
}
|
||||
const tags = [["p", zapInfo.pub], ["bolt11", invoice.invoice], ["description", zapInfo.description]]
|
||||
if (zapInfo.eventId) {
|
||||
tags.push(["e", zapInfo.eventId])
|
||||
}
|
||||
const event: UnsignedEvent = {
|
||||
content: "",
|
||||
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 { LndSettings, NodeSettings } from '../lnd/settings.js'
|
||||
import { LoadWatchdogSettingsFromEnv, WatchdogSettings } from './watchdog.js'
|
||||
import { LoadLndSettingsFromEnv } from '../lnd/index.js'
|
||||
import { EnvCanBeInteger, EnvMustBeInteger, EnvMustBeNonEmptyString } from '../helpers/envParser.js'
|
||||
import { getLogger } from '../helpers/logger.js'
|
||||
import fs from 'fs'
|
||||
import crypto from 'crypto';
|
||||
export type MainSettings = {
|
||||
storageSettings: StorageSettings,
|
||||
lndSettings: LndSettings,
|
||||
watchDogSettings: WatchdogSettings,
|
||||
jwtSecret: string
|
||||
incomingTxFee: number
|
||||
outgoingTxFee: number
|
||||
incomingAppInvoiceFee: number
|
||||
incomingAppUserInvoiceFee: number
|
||||
outgoingAppInvoiceFee: number
|
||||
outgoingAppUserInvoiceFee: number
|
||||
userToUserFee: number
|
||||
appToUserFee: number
|
||||
serviceUrl: string
|
||||
servicePort: number
|
||||
recordPerformance: boolean
|
||||
skipSanityCheck: boolean
|
||||
disableExternalPayments: boolean
|
||||
}
|
||||
export type BitcoinCoreSettings = {
|
||||
port: number
|
||||
user: string
|
||||
pass: string
|
||||
}
|
||||
export type TestSettings = MainSettings & { lndSettings: { otherNode: NodeSettings, thirdNode: NodeSettings, fourthNode: NodeSettings }, bitcoinCoreSettings: BitcoinCoreSettings }
|
||||
export const LoadMainSettingsFromEnv = (): MainSettings => {
|
||||
return {
|
||||
watchDogSettings: LoadWatchdogSettingsFromEnv(),
|
||||
lndSettings: LoadLndSettingsFromEnv(),
|
||||
storageSettings: LoadStorageSettingsFromEnv(),
|
||||
jwtSecret: loadJwtSecret(),
|
||||
incomingTxFee: EnvCanBeInteger("INCOMING_CHAIN_FEE_ROOT_BPS", 0) / 10000,
|
||||
outgoingTxFee: EnvCanBeInteger("OUTGOING_CHAIN_FEE_ROOT_BPS", 60) / 10000,
|
||||
incomingAppInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_ROOT_BPS", 0) / 10000,
|
||||
outgoingAppInvoiceFee: EnvCanBeInteger("OUTGOING_INVOICE_FEE_ROOT_BPS", 60) / 10000,
|
||||
incomingAppUserInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_USER_BPS", 0) / 10000,
|
||||
outgoingAppUserInvoiceFee: EnvCanBeInteger("OUTGOING_INVOICE_FEE_USER_BPS", 0) / 10000,
|
||||
userToUserFee: EnvCanBeInteger("TX_FEE_INTERNAL_USER_BPS", 0) / 10000,
|
||||
appToUserFee: EnvCanBeInteger("TX_FEE_INTERNAL_ROOT_BPS", 0) / 10000,
|
||||
serviceUrl: process.env.SERVICE_URL || `http://localhost:${EnvCanBeInteger("PORT", 1776)}`,
|
||||
servicePort: EnvCanBeInteger("PORT", 1776),
|
||||
recordPerformance: process.env.RECORD_PERFORMANCE === 'true' || false,
|
||||
skipSanityCheck: process.env.SKIP_SANITY_CHECK === 'true' || false,
|
||||
disableExternalPayments: process.env.DISABLE_EXTERNAL_PAYMENTS === 'true' || false,
|
||||
}
|
||||
}
|
||||
|
||||
export const LoadTestSettingsFromEnv = (): TestSettings => {
|
||||
const eventLogPath = `logs/eventLogV2Test${Date.now()}.csv`
|
||||
const settings = LoadMainSettingsFromEnv()
|
||||
return {
|
||||
...settings,
|
||||
storageSettings: { dbSettings: { ...settings.storageSettings.dbSettings, databaseFile: ":memory:", metricsDatabaseFile: ":memory:" }, eventLogPath },
|
||||
lndSettings: {
|
||||
...settings.lndSettings,
|
||||
otherNode: {
|
||||
lndAddr: EnvMustBeNonEmptyString("LND_OTHER_ADDR"),
|
||||
lndCertPath: EnvMustBeNonEmptyString("LND_OTHER_CERT_PATH"),
|
||||
lndMacaroonPath: EnvMustBeNonEmptyString("LND_OTHER_MACAROON_PATH")
|
||||
},
|
||||
thirdNode: {
|
||||
lndAddr: EnvMustBeNonEmptyString("LND_THIRD_ADDR"),
|
||||
lndCertPath: EnvMustBeNonEmptyString("LND_THIRD_CERT_PATH"),
|
||||
lndMacaroonPath: EnvMustBeNonEmptyString("LND_THIRD_MACAROON_PATH")
|
||||
},
|
||||
fourthNode: {
|
||||
lndAddr: EnvMustBeNonEmptyString("LND_FOURTH_ADDR"),
|
||||
lndCertPath: EnvMustBeNonEmptyString("LND_FOURTH_CERT_PATH"),
|
||||
lndMacaroonPath: EnvMustBeNonEmptyString("LND_FOURTH_MACAROON_PATH")
|
||||
},
|
||||
liquidityProviderPub: ""
|
||||
},
|
||||
skipSanityCheck: true,
|
||||
bitcoinCoreSettings: {
|
||||
port: EnvMustBeInteger("BITCOIN_CORE_PORT"),
|
||||
user: EnvMustBeNonEmptyString("BITCOIN_CORE_USER"),
|
||||
pass: EnvMustBeNonEmptyString("BITCOIN_CORE_PASS")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const loadJwtSecret = (): string => {
|
||||
const secret = process.env["JWT_SECRET"]
|
||||
const log = getLogger({})
|
||||
if (secret) {
|
||||
return secret
|
||||
}
|
||||
log("JWT_SECRET not set in env, checking .jwt_secret file")
|
||||
try {
|
||||
const fileContent = fs.readFileSync(".jwt_secret", "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(".jwt_secret", secret)
|
||||
return secret
|
||||
}
|
||||
import { LoadStorageSettingsFromEnv, StorageSettings } from '../storage/index.js'
|
||||
import { LndSettings, NodeSettings } from '../lnd/settings.js'
|
||||
import { LoadWatchdogSettingsFromEnv, WatchdogSettings } from './watchdog.js'
|
||||
import { LoadLndSettingsFromEnv } from '../lnd/index.js'
|
||||
import { EnvCanBeInteger, EnvMustBeInteger, EnvMustBeNonEmptyString } from '../helpers/envParser.js'
|
||||
import { getLogger } from '../helpers/logger.js'
|
||||
import fs from 'fs'
|
||||
import crypto from 'crypto';
|
||||
import { LiquiditySettings, LoadLiquiditySettingsFromEnv } from './liquidityManager.js'
|
||||
export type MainSettings = {
|
||||
storageSettings: StorageSettings,
|
||||
lndSettings: LndSettings,
|
||||
watchDogSettings: WatchdogSettings,
|
||||
liquiditySettings: LiquiditySettings,
|
||||
jwtSecret: string
|
||||
incomingTxFee: number
|
||||
outgoingTxFee: number
|
||||
incomingAppInvoiceFee: number
|
||||
incomingAppUserInvoiceFee: number
|
||||
outgoingAppInvoiceFee: number
|
||||
outgoingAppUserInvoiceFee: number
|
||||
userToUserFee: number
|
||||
appToUserFee: number
|
||||
serviceUrl: string
|
||||
servicePort: number
|
||||
recordPerformance: boolean
|
||||
skipSanityCheck: boolean
|
||||
disableExternalPayments: boolean
|
||||
}
|
||||
export type BitcoinCoreSettings = {
|
||||
port: number
|
||||
user: string
|
||||
pass: string
|
||||
}
|
||||
export type TestSettings = MainSettings & { lndSettings: { otherNode: NodeSettings, thirdNode: NodeSettings, fourthNode: NodeSettings }, bitcoinCoreSettings: BitcoinCoreSettings }
|
||||
export const LoadMainSettingsFromEnv = (): MainSettings => {
|
||||
const storageSettings = LoadStorageSettingsFromEnv()
|
||||
return {
|
||||
watchDogSettings: LoadWatchdogSettingsFromEnv(),
|
||||
lndSettings: LoadLndSettingsFromEnv(),
|
||||
storageSettings: storageSettings,
|
||||
liquiditySettings: LoadLiquiditySettingsFromEnv(),
|
||||
jwtSecret: loadJwtSecret(storageSettings.dataDir),
|
||||
incomingTxFee: EnvCanBeInteger("INCOMING_CHAIN_FEE_ROOT_BPS", 0) / 10000,
|
||||
outgoingTxFee: EnvCanBeInteger("OUTGOING_CHAIN_FEE_ROOT_BPS", 60) / 10000,
|
||||
incomingAppInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_ROOT_BPS", 0) / 10000,
|
||||
outgoingAppInvoiceFee: EnvCanBeInteger("OUTGOING_INVOICE_FEE_ROOT_BPS", 60) / 10000,
|
||||
incomingAppUserInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_USER_BPS", 0) / 10000,
|
||||
outgoingAppUserInvoiceFee: EnvCanBeInteger("OUTGOING_INVOICE_FEE_USER_BPS", 0) / 10000,
|
||||
userToUserFee: EnvCanBeInteger("TX_FEE_INTERNAL_USER_BPS", 0) / 10000,
|
||||
appToUserFee: EnvCanBeInteger("TX_FEE_INTERNAL_ROOT_BPS", 0) / 10000,
|
||||
serviceUrl: process.env.SERVICE_URL || `http://localhost:${EnvCanBeInteger("PORT", 1776)}`,
|
||||
servicePort: EnvCanBeInteger("PORT", 1776),
|
||||
recordPerformance: process.env.RECORD_PERFORMANCE === 'true' || false,
|
||||
skipSanityCheck: process.env.SKIP_SANITY_CHECK === 'true' || false,
|
||||
disableExternalPayments: process.env.DISABLE_EXTERNAL_PAYMENTS === 'true' || false,
|
||||
}
|
||||
}
|
||||
|
||||
export const LoadTestSettingsFromEnv = (): TestSettings => {
|
||||
const eventLogPath = `logs/eventLogV2Test${Date.now()}.csv`
|
||||
const settings = LoadMainSettingsFromEnv()
|
||||
return {
|
||||
...settings,
|
||||
storageSettings: { dbSettings: { ...settings.storageSettings.dbSettings, databaseFile: ":memory:", metricsDatabaseFile: ":memory:" }, eventLogPath, dataDir: "data" },
|
||||
lndSettings: {
|
||||
...settings.lndSettings,
|
||||
otherNode: {
|
||||
lndAddr: EnvMustBeNonEmptyString("LND_OTHER_ADDR"),
|
||||
lndCertPath: EnvMustBeNonEmptyString("LND_OTHER_CERT_PATH"),
|
||||
lndMacaroonPath: EnvMustBeNonEmptyString("LND_OTHER_MACAROON_PATH")
|
||||
},
|
||||
thirdNode: {
|
||||
lndAddr: EnvMustBeNonEmptyString("LND_THIRD_ADDR"),
|
||||
lndCertPath: EnvMustBeNonEmptyString("LND_THIRD_CERT_PATH"),
|
||||
lndMacaroonPath: EnvMustBeNonEmptyString("LND_THIRD_MACAROON_PATH")
|
||||
},
|
||||
fourthNode: {
|
||||
lndAddr: EnvMustBeNonEmptyString("LND_FOURTH_ADDR"),
|
||||
lndCertPath: EnvMustBeNonEmptyString("LND_FOURTH_CERT_PATH"),
|
||||
lndMacaroonPath: EnvMustBeNonEmptyString("LND_FOURTH_MACAROON_PATH")
|
||||
},
|
||||
},
|
||||
liquiditySettings: {
|
||||
...settings.liquiditySettings,
|
||||
liquidityProviderPub: "",
|
||||
},
|
||||
skipSanityCheck: true,
|
||||
bitcoinCoreSettings: {
|
||||
port: EnvMustBeInteger("BITCOIN_CORE_PORT"),
|
||||
user: EnvMustBeNonEmptyString("BITCOIN_CORE_USER"),
|
||||
pass: EnvMustBeNonEmptyString("BITCOIN_CORE_PASS")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const loadJwtSecret = (dataDir: string): string => {
|
||||
const secret = process.env["JWT_SECRET"]
|
||||
const log = getLogger({})
|
||||
if (secret) {
|
||||
return secret
|
||||
}
|
||||
log("JWT_SECRET not set in env, checking .jwt_secret file")
|
||||
const secretPath = 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 FunctionQueue from "../helpers/functionQueue.js";
|
||||
import { getLogger } from "../helpers/logger.js";
|
||||
import LND from "../lnd/lnd.js";
|
||||
import { ChannelBalance } from "../lnd/settings.js";
|
||||
import Storage from '../storage/index.js'
|
||||
export type WatchdogSettings = {
|
||||
maxDiffSats: number
|
||||
}
|
||||
export const LoadWatchdogSettingsFromEnv = (test = false): WatchdogSettings => {
|
||||
return {
|
||||
maxDiffSats: EnvCanBeInteger("WATCHDOG_MAX_DIFF_SATS")
|
||||
}
|
||||
}
|
||||
export class Watchdog {
|
||||
queue: FunctionQueue<void>
|
||||
initialLndBalance: number;
|
||||
initialUsersBalance: number;
|
||||
startedAtUnix: number;
|
||||
latestIndexOffset: number;
|
||||
accumulatedHtlcFees: number;
|
||||
lnd: LND;
|
||||
settings: WatchdogSettings;
|
||||
storage: Storage;
|
||||
latestCheckStart = 0
|
||||
log = getLogger({ component: "watchdog" })
|
||||
ready = false
|
||||
interval: NodeJS.Timer;
|
||||
constructor(settings: WatchdogSettings, lnd: LND, storage: Storage) {
|
||||
this.lnd = lnd;
|
||||
this.settings = settings;
|
||||
this.storage = storage;
|
||||
this.queue = new FunctionQueue("watchdog_queue", () => this.StartCheck())
|
||||
}
|
||||
|
||||
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)
|
||||
this.initialUsersBalance = totalUsersBalance
|
||||
const fwEvents = await this.lnd.GetForwardingHistory(0, this.startedAtUnix)
|
||||
this.latestIndexOffset = fwEvents.lastOffsetIndex
|
||||
this.accumulatedHtlcFees = 0
|
||||
|
||||
this.interval = setInterval(() => {
|
||||
if (this.latestCheckStart + (1000 * 60) < Date.now()) {
|
||||
this.log("No balance check was made in the last minute, checking now")
|
||||
this.PaymentRequested()
|
||||
}
|
||||
}, 1000 * 60)
|
||||
|
||||
this.ready = true
|
||||
}
|
||||
|
||||
updateAccumulatedHtlcFees = async () => {
|
||||
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()
|
||||
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)
|
||||
return Number(walletBalance.confirmedBalance) + totalLightningBalance
|
||||
}
|
||||
|
||||
checkBalanceUpdate = (deltaLnd: number, deltaUsers: number) => {
|
||||
this.log("LND balance update:", deltaLnd, "sats since app startup")
|
||||
this.log("Users balance update:", deltaUsers, "sats since app startup")
|
||||
|
||||
const result = this.checkDeltas(deltaLnd, deltaUsers)
|
||||
switch (result.type) {
|
||||
case 'mismatch':
|
||||
if (deltaLnd < 0) {
|
||||
this.log("WARNING! LND balance decreased while users balance increased creating a difference of", result.absoluteDiff, "sats")
|
||||
if (result.absoluteDiff > this.settings.maxDiffSats) {
|
||||
this.log("Difference is too big for an update, locking outgoing operations")
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
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")
|
||||
return false
|
||||
}
|
||||
break
|
||||
case 'negative':
|
||||
if (Math.abs(deltaLnd) > Math.abs(deltaUsers)) {
|
||||
this.log("WARNING! LND balance decreased more than users balance with a difference of", result.absoluteDiff, "sats")
|
||||
if (result.absoluteDiff > this.settings.maxDiffSats) {
|
||||
this.log("Difference is too big for an update, locking outgoing operations")
|
||||
return true
|
||||
}
|
||||
} else if (deltaLnd === deltaUsers) {
|
||||
this.log("LND and users balance went both DOWN consistently")
|
||||
return false
|
||||
} 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")
|
||||
return false
|
||||
}
|
||||
break
|
||||
case 'positive':
|
||||
if (deltaLnd < deltaUsers) {
|
||||
this.log("WARNING! LND balance increased less than users balance with a difference of", result.absoluteDiff, "sats")
|
||||
if (result.absoluteDiff > this.settings.maxDiffSats) {
|
||||
this.log("Difference is too big for an update, locking outgoing operations")
|
||||
return true
|
||||
}
|
||||
} else if (deltaLnd === deltaUsers) {
|
||||
this.log("LND and users balance went both UP consistently")
|
||||
return false
|
||||
} 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")
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
StartCheck = async () => {
|
||||
this.latestCheckStart = Date.now()
|
||||
await this.updateAccumulatedHtlcFees()
|
||||
const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
|
||||
const totalLndBalance = await this.getTotalLndBalance(totalUsersBalance)
|
||||
const deltaLnd = totalLndBalance - (this.initialLndBalance + this.accumulatedHtlcFees)
|
||||
const deltaUsers = totalUsersBalance - this.initialUsersBalance
|
||||
const deny = this.checkBalanceUpdate(deltaLnd, deltaUsers)
|
||||
if (deny) {
|
||||
this.log("Balance mismatch detected in absolute update, locking outgoing operations")
|
||||
this.lnd.LockOutgoingOperations()
|
||||
return
|
||||
}
|
||||
this.lnd.UnlockOutgoingOperations()
|
||||
}
|
||||
|
||||
PaymentRequested = async () => {
|
||||
this.log("Payment requested, checking balance")
|
||||
if (!this.ready) {
|
||||
throw new Error("Watchdog not ready")
|
||||
}
|
||||
return new Promise<void>((res, rej) => {
|
||||
this.queue.Run({ res, rej })
|
||||
})
|
||||
}
|
||||
|
||||
checkDeltas = (deltaLnd: number, deltaUsers: number): DeltaCheckResult => {
|
||||
if (deltaLnd < 0) {
|
||||
if (deltaUsers < 0) {
|
||||
const diff = Math.abs(deltaLnd - deltaUsers)
|
||||
return { type: 'negative', absoluteDiff: diff, relativeDiff: diff / Math.max(deltaLnd, deltaUsers) }
|
||||
} else {
|
||||
const diff = Math.abs(deltaLnd) + deltaUsers
|
||||
return { type: 'mismatch', absoluteDiff: diff }
|
||||
}
|
||||
} else {
|
||||
if (deltaUsers < 0) {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
import { EnvCanBeInteger } from "../helpers/envParser.js";
|
||||
import FunctionQueue from "../helpers/functionQueue.js";
|
||||
import { getLogger } from "../helpers/logger.js";
|
||||
import { LiquidityProvider } from "../lnd/liquidityProvider.js";
|
||||
import LND from "../lnd/lnd.js";
|
||||
import { ChannelBalance } from "../lnd/settings.js";
|
||||
import Storage from '../storage/index.js'
|
||||
export type WatchdogSettings = {
|
||||
maxDiffSats: number
|
||||
}
|
||||
export const LoadWatchdogSettingsFromEnv = (test = false): WatchdogSettings => {
|
||||
return {
|
||||
maxDiffSats: EnvCanBeInteger("WATCHDOG_MAX_DIFF_SATS")
|
||||
}
|
||||
}
|
||||
export class Watchdog {
|
||||
queue: FunctionQueue<void>
|
||||
initialLndBalance: number;
|
||||
initialUsersBalance: number;
|
||||
startedAtUnix: number;
|
||||
latestIndexOffset: number;
|
||||
accumulatedHtlcFees: number;
|
||||
lnd: LND;
|
||||
liquidProvider: LiquidityProvider;
|
||||
settings: WatchdogSettings;
|
||||
storage: Storage;
|
||||
latestCheckStart = 0
|
||||
log = getLogger({ component: "watchdog" })
|
||||
ready = false
|
||||
interval: NodeJS.Timer;
|
||||
constructor(settings: WatchdogSettings, lnd: LND, storage: Storage) {
|
||||
this.lnd = lnd;
|
||||
this.settings = settings;
|
||||
this.storage = storage;
|
||||
this.liquidProvider = lnd.liquidProvider
|
||||
this.queue = new FunctionQueue("watchdog_queue", () => this.StartCheck())
|
||||
}
|
||||
|
||||
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)
|
||||
this.initialUsersBalance = totalUsersBalance
|
||||
const fwEvents = await this.lnd.GetForwardingHistory(0, this.startedAtUnix)
|
||||
this.latestIndexOffset = fwEvents.lastOffsetIndex
|
||||
this.accumulatedHtlcFees = 0
|
||||
|
||||
this.interval = setInterval(() => {
|
||||
if (this.latestCheckStart + (1000 * 60) < Date.now()) {
|
||||
this.log("No balance check was made in the last minute, checking now")
|
||||
this.PaymentRequested()
|
||||
}
|
||||
}, 1000 * 60)
|
||||
|
||||
this.ready = true
|
||||
}
|
||||
|
||||
updateAccumulatedHtlcFees = async () => {
|
||||
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()
|
||||
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)
|
||||
const providerBalance = await this.liquidProvider.GetLatestBalance()
|
||||
return Number(walletBalance.confirmedBalance) + totalLightningBalance + providerBalance
|
||||
}
|
||||
|
||||
checkBalanceUpdate = (deltaLnd: number, deltaUsers: number) => {
|
||||
this.log("LND balance update:", deltaLnd, "sats since app startup")
|
||||
this.log("Users balance update:", deltaUsers, "sats since app startup")
|
||||
|
||||
const result = this.checkDeltas(deltaLnd, deltaUsers)
|
||||
switch (result.type) {
|
||||
case 'mismatch':
|
||||
if (deltaLnd < 0) {
|
||||
this.log("WARNING! LND balance decreased while users balance increased creating a difference of", result.absoluteDiff, "sats")
|
||||
if (result.absoluteDiff > this.settings.maxDiffSats) {
|
||||
this.log("Difference is too big for an update, locking outgoing operations")
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
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")
|
||||
return false
|
||||
}
|
||||
break
|
||||
case 'negative':
|
||||
if (Math.abs(deltaLnd) > Math.abs(deltaUsers)) {
|
||||
this.log("WARNING! LND balance decreased more than users balance with a difference of", result.absoluteDiff, "sats")
|
||||
if (result.absoluteDiff > this.settings.maxDiffSats) {
|
||||
this.log("Difference is too big for an update, locking outgoing operations")
|
||||
return true
|
||||
}
|
||||
} else if (deltaLnd === deltaUsers) {
|
||||
this.log("LND and users balance went both DOWN consistently")
|
||||
return false
|
||||
} 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")
|
||||
return false
|
||||
}
|
||||
break
|
||||
case 'positive':
|
||||
if (deltaLnd < deltaUsers) {
|
||||
this.log("WARNING! LND balance increased less than users balance with a difference of", result.absoluteDiff, "sats")
|
||||
if (result.absoluteDiff > this.settings.maxDiffSats) {
|
||||
this.log("Difference is too big for an update, locking outgoing operations")
|
||||
return true
|
||||
}
|
||||
} else if (deltaLnd === deltaUsers) {
|
||||
this.log("LND and users balance went both UP consistently")
|
||||
return false
|
||||
} 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")
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
StartCheck = async () => {
|
||||
this.latestCheckStart = Date.now()
|
||||
await this.updateAccumulatedHtlcFees()
|
||||
const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
|
||||
const totalLndBalance = await this.getTotalLndBalance(totalUsersBalance)
|
||||
const deltaLnd = totalLndBalance - (this.initialLndBalance + this.accumulatedHtlcFees)
|
||||
const deltaUsers = totalUsersBalance - this.initialUsersBalance
|
||||
const deny = this.checkBalanceUpdate(deltaLnd, deltaUsers)
|
||||
if (deny) {
|
||||
this.log("Balance mismatch detected in absolute update, locking outgoing operations")
|
||||
this.lnd.LockOutgoingOperations()
|
||||
return
|
||||
}
|
||||
this.lnd.UnlockOutgoingOperations()
|
||||
}
|
||||
|
||||
PaymentRequested = async () => {
|
||||
this.log("Payment requested, checking balance")
|
||||
if (!this.ready) {
|
||||
throw new Error("Watchdog not ready")
|
||||
}
|
||||
return new Promise<void>((res, rej) => {
|
||||
this.queue.Run({ res, rej })
|
||||
})
|
||||
}
|
||||
|
||||
checkDeltas = (deltaLnd: number, deltaUsers: number): DeltaCheckResult => {
|
||||
if (deltaLnd < 0) {
|
||||
if (deltaUsers < 0) {
|
||||
const diff = Math.abs(deltaLnd - deltaUsers)
|
||||
return { type: 'negative', absoluteDiff: diff, relativeDiff: diff / Math.max(deltaLnd, deltaUsers) }
|
||||
} else {
|
||||
const diff = Math.abs(deltaLnd) + deltaUsers
|
||||
return { type: 'mismatch', absoluteDiff: diff }
|
||||
}
|
||||
} else {
|
||||
if (deltaUsers < 0) {
|
||||
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 }
|
||||
|
|
@ -1,84 +1,85 @@
|
|||
import "reflect-metadata"
|
||||
import { DataSource, Migration } from "typeorm"
|
||||
import { AddressReceivingTransaction } from "./entity/AddressReceivingTransaction.js"
|
||||
import { User } from "./entity/User.js"
|
||||
import { UserReceivingAddress } from "./entity/UserReceivingAddress.js"
|
||||
import { UserReceivingInvoice } from "./entity/UserReceivingInvoice.js"
|
||||
import { UserInvoicePayment } from "./entity/UserInvoicePayment.js"
|
||||
import { EnvMustBeNonEmptyString } from "../helpers/envParser.js"
|
||||
import { UserTransactionPayment } from "./entity/UserTransactionPayment.js"
|
||||
import { UserBasicAuth } from "./entity/UserBasicAuth.js"
|
||||
import { UserEphemeralKey } from "./entity/UserEphemeralKey.js"
|
||||
import { Product } from "./entity/Product.js"
|
||||
import { UserToUserPayment } from "./entity/UserToUserPayment.js"
|
||||
import { Application } from "./entity/Application.js"
|
||||
import { ApplicationUser } from "./entity/ApplicationUser.js"
|
||||
import { BalanceEvent } from "./entity/BalanceEvent.js"
|
||||
import { ChannelBalanceEvent } from "./entity/ChannelsBalanceEvent.js"
|
||||
import { getLogger } from "../helpers/logger.js"
|
||||
import { ChannelRouting } from "./entity/ChannelRouting.js"
|
||||
|
||||
|
||||
export type DbSettings = {
|
||||
databaseFile: string
|
||||
migrate: boolean
|
||||
metricsDatabaseFile: string
|
||||
}
|
||||
export const LoadDbSettingsFromEnv = (): DbSettings => {
|
||||
return {
|
||||
databaseFile: process.env.DATABASE_FILE || "db.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({
|
||||
type: "sqlite",
|
||||
database: settings.metricsDatabaseFile,
|
||||
entities: [BalanceEvent, ChannelBalanceEvent, ChannelRouting],
|
||||
migrations: metricsMigrations
|
||||
}).initialize();
|
||||
const log = getLogger({});
|
||||
const pendingMigrations = await source.showMigrations()
|
||||
if (pendingMigrations) {
|
||||
log("Migrations found, migrating...")
|
||||
const executedMigrations = await source.runMigrations({ transaction: 'all' })
|
||||
return { source, executedMigrations }
|
||||
}
|
||||
return { source, executedMigrations: [] }
|
||||
|
||||
}
|
||||
|
||||
export default async (settings: DbSettings, migrations: Function[]): Promise<{ source: DataSource, executedMigrations: Migration[] }> => {
|
||||
const source = await new DataSource({
|
||||
type: "sqlite",
|
||||
database: settings.databaseFile,
|
||||
// logging: true,
|
||||
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
|
||||
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment],
|
||||
//synchronize: true,
|
||||
migrations
|
||||
}).initialize()
|
||||
const log = getLogger({})
|
||||
const pendingMigrations = await source.showMigrations()
|
||||
if (pendingMigrations) {
|
||||
log("migrations found, migrating...")
|
||||
const executedMigrations = await source.runMigrations({ transaction: 'all' })
|
||||
return { source, executedMigrations }
|
||||
}
|
||||
return { source, executedMigrations: [] }
|
||||
}
|
||||
|
||||
export const runFakeMigration = async (databaseFile: string, migrations: Function[]) => {
|
||||
const source = await new DataSource({
|
||||
type: "sqlite",
|
||||
database: databaseFile,
|
||||
// logging: true,
|
||||
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
|
||||
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment],
|
||||
//synchronize: true,
|
||||
migrations
|
||||
}).initialize()
|
||||
return source.runMigrations({ fake: true })
|
||||
import "reflect-metadata"
|
||||
import { DataSource, Migration } from "typeorm"
|
||||
import { AddressReceivingTransaction } from "./entity/AddressReceivingTransaction.js"
|
||||
import { User } from "./entity/User.js"
|
||||
import { UserReceivingAddress } from "./entity/UserReceivingAddress.js"
|
||||
import { UserReceivingInvoice } from "./entity/UserReceivingInvoice.js"
|
||||
import { UserInvoicePayment } from "./entity/UserInvoicePayment.js"
|
||||
import { EnvMustBeNonEmptyString } from "../helpers/envParser.js"
|
||||
import { UserTransactionPayment } from "./entity/UserTransactionPayment.js"
|
||||
import { UserBasicAuth } from "./entity/UserBasicAuth.js"
|
||||
import { UserEphemeralKey } from "./entity/UserEphemeralKey.js"
|
||||
import { Product } from "./entity/Product.js"
|
||||
import { UserToUserPayment } from "./entity/UserToUserPayment.js"
|
||||
import { Application } from "./entity/Application.js"
|
||||
import { ApplicationUser } from "./entity/ApplicationUser.js"
|
||||
import { BalanceEvent } from "./entity/BalanceEvent.js"
|
||||
import { ChannelBalanceEvent } from "./entity/ChannelsBalanceEvent.js"
|
||||
import { getLogger } from "../helpers/logger.js"
|
||||
import { ChannelRouting } from "./entity/ChannelRouting.js"
|
||||
import { LspOrder } from "./entity/LspOrder.js"
|
||||
|
||||
|
||||
export type DbSettings = {
|
||||
databaseFile: string
|
||||
migrate: boolean
|
||||
metricsDatabaseFile: string
|
||||
}
|
||||
export const LoadDbSettingsFromEnv = (): DbSettings => {
|
||||
return {
|
||||
databaseFile: process.env.DATABASE_FILE || "db.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({
|
||||
type: "sqlite",
|
||||
database: settings.metricsDatabaseFile,
|
||||
entities: [BalanceEvent, ChannelBalanceEvent, ChannelRouting],
|
||||
migrations: metricsMigrations
|
||||
}).initialize();
|
||||
const log = getLogger({});
|
||||
const pendingMigrations = await source.showMigrations()
|
||||
if (pendingMigrations) {
|
||||
log("Migrations found, migrating...")
|
||||
const executedMigrations = await source.runMigrations({ transaction: 'all' })
|
||||
return { source, executedMigrations }
|
||||
}
|
||||
return { source, executedMigrations: [] }
|
||||
|
||||
}
|
||||
|
||||
export default async (settings: DbSettings, migrations: Function[]): Promise<{ source: DataSource, executedMigrations: Migration[] }> => {
|
||||
const source = await new DataSource({
|
||||
type: "sqlite",
|
||||
database: settings.databaseFile,
|
||||
// logging: true,
|
||||
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
|
||||
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder],
|
||||
//synchronize: true,
|
||||
migrations
|
||||
}).initialize()
|
||||
const log = getLogger({})
|
||||
const pendingMigrations = await source.showMigrations()
|
||||
if (pendingMigrations) {
|
||||
log("migrations found, migrating...")
|
||||
const executedMigrations = await source.runMigrations({ transaction: 'all' })
|
||||
return { source, executedMigrations }
|
||||
}
|
||||
return { source, executedMigrations: [] }
|
||||
}
|
||||
|
||||
export const runFakeMigration = async (databaseFile: string, migrations: Function[]) => {
|
||||
const source = await new DataSource({
|
||||
type: "sqlite",
|
||||
database: databaseFile,
|
||||
// logging: true,
|
||||
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
|
||||
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment],
|
||||
//synchronize: true,
|
||||
migrations
|
||||
}).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 NewDB, { DbSettings, LoadDbSettingsFromEnv } from "./db.js"
|
||||
import ProductStorage from './productStorage.js'
|
||||
import ApplicationStorage from './applicationStorage.js'
|
||||
import UserStorage from "./userStorage.js";
|
||||
import PaymentStorage from "./paymentStorage.js";
|
||||
import MetricsStorage from "./metricsStorage.js";
|
||||
import TransactionsQueue, { TX } from "./transactionsQueue.js";
|
||||
import EventsLogManager from "./eventsLog.js";
|
||||
export type StorageSettings = {
|
||||
dbSettings: DbSettings
|
||||
eventLogPath: string
|
||||
}
|
||||
export const LoadStorageSettingsFromEnv = (): StorageSettings => {
|
||||
return { dbSettings: LoadDbSettingsFromEnv(), eventLogPath: "logs/eventLogV2.csv" }
|
||||
}
|
||||
export default class {
|
||||
DB: DataSource | EntityManager
|
||||
settings: StorageSettings
|
||||
txQueue: TransactionsQueue
|
||||
productStorage: ProductStorage
|
||||
applicationStorage: ApplicationStorage
|
||||
userStorage: UserStorage
|
||||
paymentStorage: PaymentStorage
|
||||
metricsStorage: MetricsStorage
|
||||
eventsLog: EventsLogManager
|
||||
constructor(settings: StorageSettings) {
|
||||
this.settings = settings
|
||||
this.eventsLog = new EventsLogManager(settings.eventLogPath)
|
||||
}
|
||||
async Connect(migrations: Function[], metricsMigrations: Function[]) {
|
||||
const { source, executedMigrations } = await NewDB(this.settings.dbSettings, migrations)
|
||||
this.DB = source
|
||||
this.txQueue = new TransactionsQueue("main", this.DB)
|
||||
this.userStorage = new UserStorage(this.DB, this.txQueue, this.eventsLog)
|
||||
this.productStorage = new ProductStorage(this.DB, this.txQueue)
|
||||
this.applicationStorage = new ApplicationStorage(this.DB, this.userStorage, this.txQueue)
|
||||
this.paymentStorage = new PaymentStorage(this.DB, this.userStorage, this.txQueue)
|
||||
this.metricsStorage = new MetricsStorage(this.settings)
|
||||
const executedMetricsMigrations = await this.metricsStorage.Connect(metricsMigrations)
|
||||
return { executedMigrations, executedMetricsMigrations };
|
||||
}
|
||||
|
||||
StartTransaction<T>(exec: TX<T>, description?: string) {
|
||||
return this.txQueue.PushToQueue({ exec, dbTx: true, description })
|
||||
}
|
||||
import { DataSource, EntityManager } from "typeorm"
|
||||
import NewDB, { DbSettings, LoadDbSettingsFromEnv } from "./db.js"
|
||||
import ProductStorage from './productStorage.js'
|
||||
import ApplicationStorage from './applicationStorage.js'
|
||||
import UserStorage from "./userStorage.js";
|
||||
import PaymentStorage from "./paymentStorage.js";
|
||||
import MetricsStorage from "./metricsStorage.js";
|
||||
import TransactionsQueue, { TX } from "./transactionsQueue.js";
|
||||
import EventsLogManager from "./eventsLog.js";
|
||||
import { LiquidityStorage } from "./liquidityStorage.js";
|
||||
export type StorageSettings = {
|
||||
dbSettings: DbSettings
|
||||
eventLogPath: string
|
||||
dataDir: string
|
||||
}
|
||||
export const LoadStorageSettingsFromEnv = (): StorageSettings => {
|
||||
return { dbSettings: LoadDbSettingsFromEnv(), eventLogPath: "logs/eventLogV2.csv", dataDir: process.env.DATA_DIR || "" }
|
||||
}
|
||||
export default class {
|
||||
DB: DataSource | EntityManager
|
||||
settings: StorageSettings
|
||||
txQueue: TransactionsQueue
|
||||
productStorage: ProductStorage
|
||||
applicationStorage: ApplicationStorage
|
||||
userStorage: UserStorage
|
||||
paymentStorage: PaymentStorage
|
||||
metricsStorage: MetricsStorage
|
||||
liquidityStorage: LiquidityStorage
|
||||
eventsLog: EventsLogManager
|
||||
constructor(settings: StorageSettings) {
|
||||
this.settings = settings
|
||||
this.eventsLog = new EventsLogManager(settings.eventLogPath)
|
||||
}
|
||||
async Connect(migrations: Function[], metricsMigrations: Function[]) {
|
||||
const { source, executedMigrations } = await NewDB(this.settings.dbSettings, migrations)
|
||||
this.DB = source
|
||||
this.txQueue = new TransactionsQueue("main", this.DB)
|
||||
this.userStorage = new UserStorage(this.DB, this.txQueue, this.eventsLog)
|
||||
this.productStorage = new ProductStorage(this.DB, this.txQueue)
|
||||
this.applicationStorage = new ApplicationStorage(this.DB, this.userStorage, this.txQueue)
|
||||
this.paymentStorage = new PaymentStorage(this.DB, this.userStorage, this.txQueue)
|
||||
this.metricsStorage = new MetricsStorage(this.settings)
|
||||
this.liquidityStorage = new LiquidityStorage(this.DB, this.txQueue)
|
||||
const executedMetricsMigrations = await this.metricsStorage.Connect(metricsMigrations)
|
||||
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 { DbSettings, runFakeMigration } from '../db.js'
|
||||
import Storage, { StorageSettings } from '../index.js'
|
||||
import { Initial1703170309875 } from './1703170309875-initial.js'
|
||||
import { LndMetrics1703170330183 } from './1703170330183-lnd_metrics.js'
|
||||
import { ChannelRouting1709316653538 } from './1709316653538-channel_routing.js'
|
||||
const allMigrations = [Initial1703170309875]
|
||||
const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538]
|
||||
export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => {
|
||||
if (arg === 'fake_initial_migration') {
|
||||
runFakeMigration(settings.databaseFile, [Initial1703170309875])
|
||||
return true
|
||||
}
|
||||
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)
|
||||
if (migrations.length > 0) {
|
||||
log(executedMigrations.length, "of", migrations.length, "migrations were executed correctly")
|
||||
log(executedMigrations)
|
||||
log("-------------------")
|
||||
|
||||
} if (metricsMigrations.length > 0) {
|
||||
log(executedMetricsMigrations.length, "of", metricsMigrations.length, "metrics migrations were executed correctly")
|
||||
log(executedMetricsMigrations)
|
||||
}
|
||||
import { PubLogger } from '../../helpers/logger.js'
|
||||
import { DbSettings, runFakeMigration } from '../db.js'
|
||||
import Storage, { StorageSettings } from '../index.js'
|
||||
import { Initial1703170309875 } from './1703170309875-initial.js'
|
||||
import { LndMetrics1703170330183 } from './1703170330183-lnd_metrics.js'
|
||||
import { ChannelRouting1709316653538 } from './1709316653538-channel_routing.js'
|
||||
import { LspOrder1718387847693 } from './1718387847693-lsp_order.js'
|
||||
const allMigrations = [Initial1703170309875, LspOrder1718387847693]
|
||||
const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538]
|
||||
export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => {
|
||||
if (arg === 'fake_initial_migration') {
|
||||
runFakeMigration(settings.databaseFile, [Initial1703170309875])
|
||||
return true
|
||||
}
|
||||
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)
|
||||
if (migrations.length > 0) {
|
||||
log(executedMigrations.length, "of", migrations.length, "migrations were executed correctly")
|
||||
log(executedMigrations)
|
||||
log("-------------------")
|
||||
|
||||
} if (metricsMigrations.length > 0) {
|
||||
log(executedMetricsMigrations.length, "of", metricsMigrations.length, "metrics migrations were executed correctly")
|
||||
log(executedMetricsMigrations)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,108 +1,108 @@
|
|||
version: '3.3'
|
||||
services:
|
||||
backend1:
|
||||
environment:
|
||||
USERID: ${USERID:-1000}
|
||||
GROUPID: ${GROUPID:-1000}
|
||||
stop_grace_period: 5m
|
||||
image: polarlightning/bitcoind:26.0
|
||||
container_name: polar-n2-backend1
|
||||
hostname: backend1
|
||||
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
|
||||
volumes:
|
||||
- ./volumes/bitcoind/backend1:/home/bitcoin/.bitcoin
|
||||
expose:
|
||||
- '18443'
|
||||
- '18444'
|
||||
- '28334'
|
||||
- '28335'
|
||||
ports:
|
||||
- '18443:18443'
|
||||
- '19444:18444'
|
||||
- '28334:28334'
|
||||
- '29335:28335'
|
||||
alice:
|
||||
environment:
|
||||
USERID: ${USERID:-1000}
|
||||
GROUPID: ${GROUPID:-1000}
|
||||
stop_grace_period: 2m
|
||||
image: polarlightning/lnd:0.17.3-beta
|
||||
container_name: polar-n2-alice
|
||||
hostname: alice
|
||||
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
|
||||
restart: always
|
||||
volumes:
|
||||
- ./volumes/lnd/alice:/home/lnd/.lnd
|
||||
expose:
|
||||
- '8080'
|
||||
- '10009'
|
||||
- '9735'
|
||||
ports:
|
||||
# - '8081:8080'
|
||||
- '10001:10009'
|
||||
- '9735:9735'
|
||||
bob:
|
||||
environment:
|
||||
USERID: ${USERID:-1000}
|
||||
GROUPID: ${GROUPID:-1000}
|
||||
stop_grace_period: 2m
|
||||
image: polarlightning/lnd:0.17.3-beta
|
||||
container_name: polar-n2-bob
|
||||
hostname: bob
|
||||
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
|
||||
restart: always
|
||||
volumes:
|
||||
- ./volumes/lnd/bob:/home/lnd/.lnd
|
||||
expose:
|
||||
- '8080'
|
||||
- '10009'
|
||||
- '9735'
|
||||
ports:
|
||||
# - '8082:8080'
|
||||
- '10002:10009'
|
||||
- '9736:9735'
|
||||
carol:
|
||||
environment:
|
||||
USERID: ${USERID:-1000}
|
||||
GROUPID: ${GROUPID:-1000}
|
||||
stop_grace_period: 2m
|
||||
image: polarlightning/lnd:0.17.3-beta
|
||||
container_name: polar-n2-carol
|
||||
hostname: carol
|
||||
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
|
||||
restart: always
|
||||
volumes:
|
||||
- ./volumes/lnd/carol:/home/lnd/.lnd
|
||||
expose:
|
||||
- '8080'
|
||||
- '10009'
|
||||
- '9735'
|
||||
ports:
|
||||
# - '8083:8080'
|
||||
- '10003:10009'
|
||||
- '9737:9735'
|
||||
dave:
|
||||
environment:
|
||||
USERID: ${USERID:-1000}
|
||||
GROUPID: ${GROUPID:-1000}
|
||||
stop_grace_period: 2m
|
||||
image: polarlightning/lnd:0.17.3-beta
|
||||
container_name: polar-n2-dave
|
||||
hostname: dave
|
||||
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
|
||||
restart: always
|
||||
volumes:
|
||||
- ./volumes/lnd/dave:/home/lnd/.lnd
|
||||
expose:
|
||||
- '8080'
|
||||
- '10009'
|
||||
- '9735'
|
||||
ports:
|
||||
# - '8084:8080'
|
||||
- '10004:10009'
|
||||
- '9738:9735'
|
||||
version: '3.3'
|
||||
services:
|
||||
backend1:
|
||||
environment:
|
||||
USERID: ${USERID:-1000}
|
||||
GROUPID: ${GROUPID:-1000}
|
||||
stop_grace_period: 5m
|
||||
image: polarlightning/bitcoind:26.0
|
||||
container_name: polar-n2-backend1
|
||||
hostname: backend1
|
||||
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
|
||||
volumes:
|
||||
- ./volumes/bitcoind/backend1:/home/bitcoin/.bitcoin
|
||||
expose:
|
||||
- '18443'
|
||||
- '18444'
|
||||
- '28334'
|
||||
- '28335'
|
||||
ports:
|
||||
- '18443:18443'
|
||||
- '19444:18444'
|
||||
- '28334:28334'
|
||||
- '29335:28335'
|
||||
alice:
|
||||
environment:
|
||||
USERID: ${USERID:-1000}
|
||||
GROUPID: ${GROUPID:-1000}
|
||||
stop_grace_period: 2m
|
||||
image: polarlightning/lnd:0.18.0-beta
|
||||
container_name: polar-n2-alice
|
||||
hostname: alice
|
||||
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
|
||||
restart: always
|
||||
volumes:
|
||||
- ./volumes/lnd/alice:/home/lnd/.lnd
|
||||
expose:
|
||||
- '8080'
|
||||
- '10009'
|
||||
- '9735'
|
||||
ports:
|
||||
# - '8081:8080'
|
||||
- '10001:10009'
|
||||
- '9735:9735'
|
||||
bob:
|
||||
environment:
|
||||
USERID: ${USERID:-1000}
|
||||
GROUPID: ${GROUPID:-1000}
|
||||
stop_grace_period: 2m
|
||||
image: polarlightning/lnd:0.18.0-beta
|
||||
container_name: polar-n2-bob
|
||||
hostname: bob
|
||||
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
|
||||
restart: always
|
||||
volumes:
|
||||
- ./volumes/lnd/bob:/home/lnd/.lnd
|
||||
expose:
|
||||
- '8080'
|
||||
- '10009'
|
||||
- '9735'
|
||||
ports:
|
||||
# - '8082:8080'
|
||||
- '10002:10009'
|
||||
- '9736:9735'
|
||||
carol:
|
||||
environment:
|
||||
USERID: ${USERID:-1000}
|
||||
GROUPID: ${GROUPID:-1000}
|
||||
stop_grace_period: 2m
|
||||
image: polarlightning/lnd:0.18.0-beta
|
||||
container_name: polar-n2-carol
|
||||
hostname: carol
|
||||
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
|
||||
restart: always
|
||||
volumes:
|
||||
- ./volumes/lnd/carol:/home/lnd/.lnd
|
||||
expose:
|
||||
- '8080'
|
||||
- '10009'
|
||||
- '9735'
|
||||
ports:
|
||||
# - '8083:8080'
|
||||
- '10003:10009'
|
||||
- '9737:9735'
|
||||
dave:
|
||||
environment:
|
||||
USERID: ${USERID:-1000}
|
||||
GROUPID: ${GROUPID:-1000}
|
||||
stop_grace_period: 2m
|
||||
image: polarlightning/lnd:0.18.0-beta
|
||||
container_name: polar-n2-dave
|
||||
hostname: dave
|
||||
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
|
||||
restart: always
|
||||
volumes:
|
||||
- ./volumes/lnd/dave:/home/lnd/.lnd
|
||||
expose:
|
||||
- '8080'
|
||||
- '10009'
|
||||
- '9735'
|
||||
ports:
|
||||
# - '8084:8080'
|
||||
- '10004:10009'
|
||||
- '9738:9735'
|
||||
|
|
|
|||
|
|
@ -1,54 +1,56 @@
|
|||
import { disableLoggers } from '../services/helpers/logger.js'
|
||||
import { runSanityCheck, safelySetUserBalance, TestBase, TestUserData } from './testBase.js'
|
||||
import { initBootstrappedInstance } from './setupBootstrapped.js'
|
||||
import Main from '../services/main/index.js'
|
||||
import { AppData } from '../services/main/init.js'
|
||||
export const ignore = false
|
||||
export const dev = false
|
||||
|
||||
|
||||
|
||||
export default async (T: TestBase) => {
|
||||
disableLoggers([], ["EventsLogManager", "watchdog", "htlcTracker", "debugHtlcs", "debugLndBalancev3", "metrics", "mainForTest", "main"])
|
||||
await safelySetUserBalance(T, T.user1, 2000)
|
||||
T.d("starting liquidityProvider tests...")
|
||||
const { bootstrapped, bootstrappedUser, stop } = await initBootstrappedInstance(T)
|
||||
await testInboundPaymentFromProvider(T, bootstrapped, bootstrappedUser)
|
||||
await testOutboundPaymentFromProvider(T, bootstrapped, bootstrappedUser)
|
||||
stop()
|
||||
await runSanityCheck(T)
|
||||
}
|
||||
|
||||
const testInboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bUser: TestUserData) => {
|
||||
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" })
|
||||
|
||||
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 })
|
||||
T.expect(userBalance.balance).to.equal(2000)
|
||||
|
||||
const providerBalance = await bootstrapped.liquidProvider.CheckUserState()
|
||||
if (!providerBalance) {
|
||||
throw new Error("provider balance not found")
|
||||
}
|
||||
T.expect(providerBalance.balance).to.equal(2000)
|
||||
T.d("testInboundPaymentFromProvider done")
|
||||
}
|
||||
|
||||
const testOutboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bootstrappedUser: TestUserData) => {
|
||||
T.d("starting testOutboundPaymentFromProvider")
|
||||
|
||||
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 res = await bootstrapped.appUserManager.PayInvoice(ctx, { invoice: invoice.payRequest, amount: 0 })
|
||||
|
||||
const userBalance = await bootstrapped.appUserManager.GetUserInfo(ctx)
|
||||
T.expect(userBalance.balance).to.equal(986) // 2000 - (1000 + 6(x2) + 2)
|
||||
|
||||
const providerBalance = await bootstrapped.liquidProvider.CheckUserState()
|
||||
if (!providerBalance) {
|
||||
throw new Error("provider balance not found")
|
||||
}
|
||||
T.expect(providerBalance.balance).to.equal(992) // 2000 - (1000 + 6 +2)
|
||||
T.d("testOutboundPaymentFromProvider done")
|
||||
import { disableLoggers } from '../services/helpers/logger.js'
|
||||
import { runSanityCheck, safelySetUserBalance, TestBase, TestUserData } from './testBase.js'
|
||||
import { initBootstrappedInstance } from './setupBootstrapped.js'
|
||||
import Main from '../services/main/index.js'
|
||||
import { AppData } from '../services/main/init.js'
|
||||
export const ignore = false
|
||||
export const dev = false
|
||||
|
||||
|
||||
|
||||
export default async (T: TestBase) => {
|
||||
disableLoggers([], ["EventsLogManager", "watchdog", "htlcTracker", "debugHtlcs", "debugLndBalancev3", "metrics", "mainForTest", "main"])
|
||||
await safelySetUserBalance(T, T.user1, 2000)
|
||||
T.d("starting liquidityProvider tests...")
|
||||
const { bootstrapped, bootstrappedUser, stop } = await initBootstrappedInstance(T)
|
||||
await testInboundPaymentFromProvider(T, bootstrapped, bootstrappedUser)
|
||||
await testOutboundPaymentFromProvider(T, bootstrapped, bootstrappedUser)
|
||||
stop()
|
||||
await runSanityCheck(T)
|
||||
}
|
||||
|
||||
const testInboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bUser: TestUserData) => {
|
||||
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" })
|
||||
|
||||
await T.externalAccessToOtherLnd.PayInvoice(invoiceRes.invoice, 0, 100)
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
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)
|
||||
T.d("user balance is 2000")
|
||||
const providerBalance = await bootstrapped.liquidProvider.CheckUserState()
|
||||
if (!providerBalance) {
|
||||
throw new Error("provider balance not found")
|
||||
}
|
||||
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 invoice = await T.externalAccessToOtherLnd.NewInvoice(1000, "", 60 * 60)
|
||||
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 userBalance = await bootstrapped.appUserManager.GetUserInfo(ctx)
|
||||
T.expect(userBalance.balance).to.equal(986) // 2000 - (1000 + 6(x2) + 2)
|
||||
|
||||
const providerBalance = await bootstrapped.liquidProvider.CheckUserState()
|
||||
if (!providerBalance) {
|
||||
throw new Error("provider balance not found")
|
||||
}
|
||||
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 { BitcoinCoreWrapper } from "./bitcoinCore.js"
|
||||
import LND from '../services/lnd/lnd.js'
|
||||
import { LiquidityProvider } from "../services/lnd/liquidityProvider.js"
|
||||
|
||||
export const setupNetwork = async () => {
|
||||
const settings = LoadTestSettingsFromEnv()
|
||||
const core = new BitcoinCoreWrapper(settings)
|
||||
await core.InitAddress()
|
||||
await core.Mine(1)
|
||||
const alice = new LND(settings.lndSettings, new LiquidityProvider("", () => { }), () => { }, () => { }, () => { }, () => { })
|
||||
const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, new LiquidityProvider("", () => { }), () => { }, () => { }, () => { }, () => { })
|
||||
await tryUntil<void>(async i => {
|
||||
const peers = await alice.ListPeers()
|
||||
if (peers.peers.length > 0) {
|
||||
return
|
||||
}
|
||||
await alice.ConnectPeer({ pubkey: '0232842d81b2423df97aa8a264f8c0811610a736af65afe2e145279f285625c1e4', host: "carol:9735" })
|
||||
await alice.ConnectPeer({ pubkey: '027c50fde118af534ff27e59da722422d2f3e06505c31e94c1b40c112c48a83b1c', host: "dave:9735" })
|
||||
}, 15, 2000)
|
||||
await tryUntil<void>(async i => {
|
||||
const peers = await bob.ListPeers()
|
||||
if (peers.peers.length > 0) {
|
||||
return
|
||||
}
|
||||
await bob.ConnectPeer({ pubkey: '0232842d81b2423df97aa8a264f8c0811610a736af65afe2e145279f285625c1e4', host: "carol:9735" })
|
||||
}, 15, 2000)
|
||||
|
||||
await tryUntil<void>(async i => {
|
||||
const info = await alice.GetInfo()
|
||||
if (!info.syncedToChain) {
|
||||
throw new Error("alice not synced to chain")
|
||||
}
|
||||
if (!info.syncedToGraph) {
|
||||
//await lnd.ConnectPeer({})
|
||||
throw new Error("alice not synced to graph")
|
||||
}
|
||||
}, 15, 2000)
|
||||
|
||||
await tryUntil<void>(async i => {
|
||||
const info = await bob.GetInfo()
|
||||
if (!info.syncedToChain) {
|
||||
throw new Error("bob not synced to chain")
|
||||
}
|
||||
if (!info.syncedToGraph) {
|
||||
//await lnd.ConnectPeer({})
|
||||
throw new Error("bob not synced to graph")
|
||||
}
|
||||
}, 15, 2000)
|
||||
|
||||
alice.Stop()
|
||||
bob.Stop()
|
||||
}
|
||||
|
||||
const tryUntil = async <T>(fn: (attempt: number) => Promise<T>, maxTries: number, interval: number) => {
|
||||
for (let i = 0; i < maxTries; i++) {
|
||||
try {
|
||||
return await fn(i)
|
||||
} catch (e) {
|
||||
console.log("tryUntil error", e)
|
||||
await new Promise(resolve => setTimeout(resolve, interval))
|
||||
}
|
||||
}
|
||||
throw new Error("tryUntil failed")
|
||||
import { LoadTestSettingsFromEnv } from "../services/main/settings.js"
|
||||
import { BitcoinCoreWrapper } from "./bitcoinCore.js"
|
||||
import LND from '../services/lnd/lnd.js'
|
||||
import { LiquidityProvider } from "../services/lnd/liquidityProvider.js"
|
||||
|
||||
export const setupNetwork = async () => {
|
||||
const settings = LoadTestSettingsFromEnv()
|
||||
const core = new BitcoinCoreWrapper(settings)
|
||||
await core.InitAddress()
|
||||
await core.Mine(1)
|
||||
const alice = new LND(settings.lndSettings, { liquidProvider: new LiquidityProvider("", () => { }) }, () => { }, () => { }, () => { }, () => { })
|
||||
const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, { liquidProvider: new LiquidityProvider("", () => { }) }, () => { }, () => { }, () => { }, () => { })
|
||||
await tryUntil<void>(async i => {
|
||||
const peers = await alice.ListPeers()
|
||||
if (peers.peers.length > 0) {
|
||||
return
|
||||
}
|
||||
await alice.ConnectPeer({ pubkey: '0232842d81b2423df97aa8a264f8c0811610a736af65afe2e145279f285625c1e4', host: "carol:9735" })
|
||||
await alice.ConnectPeer({ pubkey: '027c50fde118af534ff27e59da722422d2f3e06505c31e94c1b40c112c48a83b1c', host: "dave:9735" })
|
||||
}, 15, 2000)
|
||||
await tryUntil<void>(async i => {
|
||||
const peers = await bob.ListPeers()
|
||||
if (peers.peers.length > 0) {
|
||||
return
|
||||
}
|
||||
await bob.ConnectPeer({ pubkey: '0232842d81b2423df97aa8a264f8c0811610a736af65afe2e145279f285625c1e4', host: "carol:9735" })
|
||||
}, 15, 2000)
|
||||
|
||||
await tryUntil<void>(async i => {
|
||||
const info = await alice.GetInfo()
|
||||
if (!info.syncedToChain) {
|
||||
throw new Error("alice not synced to chain")
|
||||
}
|
||||
if (!info.syncedToGraph) {
|
||||
//await lnd.ConnectPeer({})
|
||||
throw new Error("alice not synced to graph")
|
||||
}
|
||||
}, 15, 2000)
|
||||
|
||||
await tryUntil<void>(async i => {
|
||||
const info = await bob.GetInfo()
|
||||
if (!info.syncedToChain) {
|
||||
throw new Error("bob not synced to chain")
|
||||
}
|
||||
if (!info.syncedToGraph) {
|
||||
//await lnd.ConnectPeer({})
|
||||
throw new Error("bob not synced to graph")
|
||||
}
|
||||
}, 15, 2000)
|
||||
|
||||
alice.Stop()
|
||||
bob.Stop()
|
||||
}
|
||||
|
||||
const tryUntil = async <T>(fn: (attempt: number) => Promise<T>, maxTries: number, interval: number) => {
|
||||
for (let i = 0; i < maxTries; i++) {
|
||||
try {
|
||||
return await fn(i)
|
||||
} catch (e) {
|
||||
console.log("tryUntil error", e)
|
||||
await new Promise(resolve => setTimeout(resolve, interval))
|
||||
}
|
||||
}
|
||||
throw new Error("tryUntil failed")
|
||||
}
|
||||
|
|
@ -1,92 +1,92 @@
|
|||
import { getLogger } from '../services/helpers/logger.js'
|
||||
import { initMainHandler } from '../services/main/init.js'
|
||||
import { LoadTestSettingsFromEnv } from '../services/main/settings.js'
|
||||
import { SendData } from '../services/nostr/handler.js'
|
||||
import { TestBase, TestUserData } from './testBase.js'
|
||||
import * as Types from '../../proto/autogenerated/ts/types.js'
|
||||
|
||||
export const initBootstrappedInstance = async (T: TestBase) => {
|
||||
const settings = LoadTestSettingsFromEnv()
|
||||
settings.lndSettings.useOnlyLiquidityProvider = true
|
||||
settings.lndSettings.liquidityProviderPub = T.app.publicKey
|
||||
settings.lndSettings.mainNode = settings.lndSettings.thirdNode
|
||||
const initialized = await initMainHandler(getLogger({ component: "bootstrapped" }), settings)
|
||||
if (!initialized) {
|
||||
throw new Error("failed to initialize bootstrapped main handler")
|
||||
}
|
||||
const { mainHandler: bootstrapped, liquidityProviderInfo, liquidityProviderApp } = initialized
|
||||
T.main.attachNostrSend(async (_, data, r) => {
|
||||
if (data.type === 'event') {
|
||||
throw new Error("unsupported event type")
|
||||
}
|
||||
if (data.pub !== liquidityProviderInfo.publicKey) {
|
||||
throw new Error("invalid pub " + data.pub + " expected " + liquidityProviderInfo.publicKey)
|
||||
}
|
||||
const j = JSON.parse(data.content) as { requestId: string }
|
||||
console.log("sending new operation to provider")
|
||||
bootstrapped.liquidProvider.onEvent(j, T.app.publicKey)
|
||||
})
|
||||
bootstrapped.liquidProvider.attachNostrSend(async (_, data, r) => {
|
||||
const res = await handleSend(T, data)
|
||||
if (data.type === 'event') {
|
||||
throw new Error("unsupported event type")
|
||||
}
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
bootstrapped.liquidProvider.onEvent(res, data.pub)
|
||||
})
|
||||
bootstrapped.liquidProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey })
|
||||
await new Promise<void>(res => {
|
||||
const interval = setInterval(() => {
|
||||
if (bootstrapped.liquidProvider.CanProviderHandle({ action: 'receive', amount: 2000 })) {
|
||||
clearInterval(interval)
|
||||
res()
|
||||
} else {
|
||||
console.log("waiting for provider to be able to handle the request")
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
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 }
|
||||
return {
|
||||
bootstrapped, liquidityProviderInfo, liquidityProviderApp, bootstrappedUser, stop: () => {
|
||||
bootstrapped.Stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
type TransportRequest = { requestId: string, authIdentifier: string } & (
|
||||
{ rpcName: 'GetUserInfo' } |
|
||||
{ rpcName: 'NewInvoice', body: Types.NewInvoiceRequest } |
|
||||
{ rpcName: 'PayInvoice', body: Types.PayInvoiceRequest } |
|
||||
{ rpcName: 'GetLiveUserOperations' } |
|
||||
{ rpcName: "" }
|
||||
)
|
||||
const handleSend = async (T: TestBase, data: SendData) => {
|
||||
if (data.type === 'event') {
|
||||
throw new Error("unsupported event type")
|
||||
}
|
||||
if (data.pub !== T.app.publicKey) {
|
||||
throw new Error("invalid pub")
|
||||
}
|
||||
const j = JSON.parse(data.content) as TransportRequest
|
||||
const app = await T.main.storage.applicationStorage.GetApplication(T.app.appId)
|
||||
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 }
|
||||
switch (j.rpcName) {
|
||||
case 'GetUserInfo':
|
||||
const infoRes = await T.main.appUserManager.GetUserInfo(userCtx)
|
||||
return { ...infoRes, status: "OK", requestId: j.requestId }
|
||||
case 'NewInvoice':
|
||||
const genInvoiceRes = await T.main.appUserManager.NewInvoice(userCtx, j.body)
|
||||
return { ...genInvoiceRes, status: "OK", requestId: j.requestId }
|
||||
case 'PayInvoice':
|
||||
const payRes = await T.main.appUserManager.PayInvoice(userCtx, j.body)
|
||||
return { ...payRes, status: "OK", requestId: j.requestId }
|
||||
case 'GetLiveUserOperations':
|
||||
return
|
||||
default:
|
||||
console.log(data)
|
||||
throw new Error("unsupported rpcName " + j.rpcName)
|
||||
}
|
||||
import { getLogger } from '../services/helpers/logger.js'
|
||||
import { initMainHandler } from '../services/main/init.js'
|
||||
import { LoadTestSettingsFromEnv } from '../services/main/settings.js'
|
||||
import { SendData } from '../services/nostr/handler.js'
|
||||
import { TestBase, TestUserData } from './testBase.js'
|
||||
import * as Types from '../../proto/autogenerated/ts/types.js'
|
||||
|
||||
export const initBootstrappedInstance = async (T: TestBase) => {
|
||||
const settings = LoadTestSettingsFromEnv()
|
||||
settings.liquiditySettings.useOnlyLiquidityProvider = true
|
||||
settings.liquiditySettings.liquidityProviderPub = T.app.publicKey
|
||||
settings.lndSettings.mainNode = settings.lndSettings.thirdNode
|
||||
const initialized = await initMainHandler(getLogger({ component: "bootstrapped" }), settings)
|
||||
if (!initialized) {
|
||||
throw new Error("failed to initialize bootstrapped main handler")
|
||||
}
|
||||
const { mainHandler: bootstrapped, liquidityProviderInfo, liquidityProviderApp } = initialized
|
||||
T.main.attachNostrSend(async (_, data, r) => {
|
||||
if (data.type === 'event') {
|
||||
throw new Error("unsupported event type")
|
||||
}
|
||||
if (data.pub !== liquidityProviderInfo.publicKey) {
|
||||
throw new Error("invalid pub " + data.pub + " expected " + liquidityProviderInfo.publicKey)
|
||||
}
|
||||
const j = JSON.parse(data.content) as { requestId: string }
|
||||
console.log("sending new operation to provider")
|
||||
bootstrapped.liquidProvider.onEvent(j, T.app.publicKey)
|
||||
})
|
||||
bootstrapped.liquidProvider.attachNostrSend(async (_, data, r) => {
|
||||
const res = await handleSend(T, data)
|
||||
if (data.type === 'event') {
|
||||
throw new Error("unsupported event type")
|
||||
}
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
bootstrapped.liquidProvider.onEvent(res, data.pub)
|
||||
})
|
||||
bootstrapped.liquidProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey })
|
||||
await new Promise<void>(res => {
|
||||
const interval = setInterval(() => {
|
||||
if (bootstrapped.liquidProvider.CanProviderHandle({ action: 'receive', amount: 2000 })) {
|
||||
clearInterval(interval)
|
||||
res()
|
||||
} else {
|
||||
console.log("waiting for provider to be able to handle the request")
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
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 }
|
||||
return {
|
||||
bootstrapped, liquidityProviderInfo, liquidityProviderApp, bootstrappedUser, stop: () => {
|
||||
bootstrapped.Stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
type TransportRequest = { requestId: string, authIdentifier: string } & (
|
||||
{ rpcName: 'GetUserInfo' } |
|
||||
{ rpcName: 'NewInvoice', body: Types.NewInvoiceRequest } |
|
||||
{ rpcName: 'PayInvoice', body: Types.PayInvoiceRequest } |
|
||||
{ rpcName: 'GetLiveUserOperations' } |
|
||||
{ rpcName: "" }
|
||||
)
|
||||
const handleSend = async (T: TestBase, data: SendData) => {
|
||||
if (data.type === 'event') {
|
||||
throw new Error("unsupported event type")
|
||||
}
|
||||
if (data.pub !== T.app.publicKey) {
|
||||
throw new Error("invalid pub")
|
||||
}
|
||||
const j = JSON.parse(data.content) as TransportRequest
|
||||
const app = await T.main.storage.applicationStorage.GetApplication(T.app.appId)
|
||||
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 }
|
||||
switch (j.rpcName) {
|
||||
case 'GetUserInfo':
|
||||
const infoRes = await T.main.appUserManager.GetUserInfo(userCtx)
|
||||
return { ...infoRes, status: "OK", requestId: j.requestId }
|
||||
case 'NewInvoice':
|
||||
const genInvoiceRes = await T.main.appUserManager.NewInvoice(userCtx, j.body)
|
||||
return { ...genInvoiceRes, status: "OK", requestId: j.requestId }
|
||||
case 'PayInvoice':
|
||||
const payRes = await T.main.appUserManager.PayInvoice(userCtx, j.body)
|
||||
return { ...payRes, status: "OK", requestId: j.requestId }
|
||||
case 'GetLiveUserOperations':
|
||||
return
|
||||
default:
|
||||
console.log(data)
|
||||
throw new Error("unsupported rpcName " + j.rpcName)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,105 +1,105 @@
|
|||
import 'dotenv/config' // TODO - test env
|
||||
import chai from 'chai'
|
||||
import { AppData, initMainHandler } from '../services/main/init.js'
|
||||
import Main from '../services/main/index.js'
|
||||
import Storage from '../services/storage/index.js'
|
||||
import { User } from '../services/storage/entity/User.js'
|
||||
import { LoadMainSettingsFromEnv, LoadTestSettingsFromEnv, MainSettings } from '../services/main/settings.js'
|
||||
import chaiString from 'chai-string'
|
||||
import { defaultInvoiceExpiry } from '../services/storage/paymentStorage.js'
|
||||
import SanityChecker from '../services/main/sanityChecker.js'
|
||||
import LND from '../services/lnd/lnd.js'
|
||||
import { getLogger, resetDisabledLoggers } from '../services/helpers/logger.js'
|
||||
import { LiquidityProvider } from '../services/lnd/liquidityProvider.js'
|
||||
chai.use(chaiString)
|
||||
export const expect = chai.expect
|
||||
export type Describe = (message: string, failure?: boolean) => void
|
||||
export type TestUserData = {
|
||||
userId: string;
|
||||
appUserIdentifier: string;
|
||||
appId: string;
|
||||
|
||||
}
|
||||
export type TestBase = {
|
||||
expect: Chai.ExpectStatic;
|
||||
main: Main
|
||||
app: AppData
|
||||
user1: TestUserData
|
||||
user2: TestUserData
|
||||
externalAccessToMainLnd: LND
|
||||
externalAccessToOtherLnd: LND
|
||||
externalAccessToThirdLnd: LND
|
||||
d: Describe
|
||||
}
|
||||
|
||||
export const SetupTest = async (d: Describe): Promise<TestBase> => {
|
||||
const settings = LoadTestSettingsFromEnv()
|
||||
const initialized = await initMainHandler(getLogger({ component: "mainForTest" }), settings)
|
||||
if (!initialized) {
|
||||
throw new Error("failed to initialize main handler")
|
||||
}
|
||||
const main = initialized.mainHandler
|
||||
const app = initialized.apps[0]
|
||||
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 user1 = { userId: u1.info.userId, appUserIdentifier: u1.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, () => { }, () => { })
|
||||
await externalAccessToMainLnd.Warmup()
|
||||
|
||||
const otherLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }
|
||||
const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider("", () => { }), console.log, console.log, () => { }, () => { })
|
||||
await externalAccessToOtherLnd.Warmup()
|
||||
|
||||
const thirdLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.thirdNode }
|
||||
const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider("", () => { }), console.log, console.log, () => { }, () => { })
|
||||
await externalAccessToThirdLnd.Warmup()
|
||||
|
||||
|
||||
return {
|
||||
expect, main, app,
|
||||
user1, user2,
|
||||
externalAccessToMainLnd, externalAccessToOtherLnd, externalAccessToThirdLnd,
|
||||
d
|
||||
}
|
||||
}
|
||||
|
||||
export const teardown = async (T: TestBase) => {
|
||||
T.main.Stop()
|
||||
T.externalAccessToMainLnd.Stop()
|
||||
T.externalAccessToOtherLnd.Stop()
|
||||
T.externalAccessToThirdLnd.Stop()
|
||||
resetDisabledLoggers()
|
||||
console.log("teardown")
|
||||
}
|
||||
|
||||
export const safelySetUserBalance = async (T: TestBase, user: TestUserData, amount: number) => {
|
||||
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 })
|
||||
await T.externalAccessToOtherLnd.PayInvoice(invoice.invoice, 0, 100)
|
||||
const u = await T.main.storage.userStorage.GetUser(user.userId)
|
||||
expect(u.balance_sats).to.be.equal(amount)
|
||||
T.d(`user ${user.appUserIdentifier} balance is now ${amount}`)
|
||||
}
|
||||
|
||||
export const runSanityCheck = async (T: TestBase) => {
|
||||
const sanityChecker = new SanityChecker(T.main.storage, T.main.lnd)
|
||||
await sanityChecker.VerifyEventsLog()
|
||||
}
|
||||
|
||||
export const expectThrowsAsync = async (promise: Promise<any>, errorMessage?: string) => {
|
||||
let error: Error | null = null
|
||||
try {
|
||||
await promise
|
||||
}
|
||||
catch (err: any) {
|
||||
error = err as Error
|
||||
}
|
||||
expect(error).to.be.an('Error')
|
||||
console.log(error!.message)
|
||||
if (errorMessage) {
|
||||
expect(error!.message).to.equal(errorMessage)
|
||||
}
|
||||
import 'dotenv/config' // TODO - test env
|
||||
import chai from 'chai'
|
||||
import { AppData, initMainHandler } from '../services/main/init.js'
|
||||
import Main from '../services/main/index.js'
|
||||
import Storage from '../services/storage/index.js'
|
||||
import { User } from '../services/storage/entity/User.js'
|
||||
import { LoadMainSettingsFromEnv, LoadTestSettingsFromEnv, MainSettings } from '../services/main/settings.js'
|
||||
import chaiString from 'chai-string'
|
||||
import { defaultInvoiceExpiry } from '../services/storage/paymentStorage.js'
|
||||
import SanityChecker from '../services/main/sanityChecker.js'
|
||||
import LND from '../services/lnd/lnd.js'
|
||||
import { getLogger, resetDisabledLoggers } from '../services/helpers/logger.js'
|
||||
import { LiquidityProvider } from '../services/lnd/liquidityProvider.js'
|
||||
chai.use(chaiString)
|
||||
export const expect = chai.expect
|
||||
export type Describe = (message: string, failure?: boolean) => void
|
||||
export type TestUserData = {
|
||||
userId: string;
|
||||
appUserIdentifier: string;
|
||||
appId: string;
|
||||
|
||||
}
|
||||
export type TestBase = {
|
||||
expect: Chai.ExpectStatic;
|
||||
main: Main
|
||||
app: AppData
|
||||
user1: TestUserData
|
||||
user2: TestUserData
|
||||
externalAccessToMainLnd: LND
|
||||
externalAccessToOtherLnd: LND
|
||||
externalAccessToThirdLnd: LND
|
||||
d: Describe
|
||||
}
|
||||
|
||||
export const SetupTest = async (d: Describe): Promise<TestBase> => {
|
||||
const settings = LoadTestSettingsFromEnv()
|
||||
const initialized = await initMainHandler(getLogger({ component: "mainForTest" }), settings)
|
||||
if (!initialized) {
|
||||
throw new Error("failed to initialize main handler")
|
||||
}
|
||||
const main = initialized.mainHandler
|
||||
const app = initialized.apps[0]
|
||||
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 user1 = { userId: u1.info.userId, appUserIdentifier: u1.identifier, appId: app.appId }
|
||||
const user2 = { userId: u2.info.userId, appUserIdentifier: u2.identifier, appId: app.appId }
|
||||
|
||||
|
||||
const externalAccessToMainLnd = new LND(settings.lndSettings, { liquidProvider: new LiquidityProvider("", () => { }) }, console.log, console.log, () => { }, () => { })
|
||||
await externalAccessToMainLnd.Warmup()
|
||||
|
||||
const otherLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }
|
||||
const externalAccessToOtherLnd = new LND(otherLndSetting, { liquidProvider: new LiquidityProvider("", () => { }) }, console.log, console.log, () => { }, () => { })
|
||||
await externalAccessToOtherLnd.Warmup()
|
||||
|
||||
const thirdLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.thirdNode }
|
||||
const externalAccessToThirdLnd = new LND(thirdLndSetting, { liquidProvider: new LiquidityProvider("", () => { }) }, console.log, console.log, () => { }, () => { })
|
||||
await externalAccessToThirdLnd.Warmup()
|
||||
|
||||
|
||||
return {
|
||||
expect, main, app,
|
||||
user1, user2,
|
||||
externalAccessToMainLnd, externalAccessToOtherLnd, externalAccessToThirdLnd,
|
||||
d
|
||||
}
|
||||
}
|
||||
|
||||
export const teardown = async (T: TestBase) => {
|
||||
T.main.Stop()
|
||||
T.externalAccessToMainLnd.Stop()
|
||||
T.externalAccessToOtherLnd.Stop()
|
||||
T.externalAccessToThirdLnd.Stop()
|
||||
resetDisabledLoggers()
|
||||
console.log("teardown")
|
||||
}
|
||||
|
||||
export const safelySetUserBalance = async (T: TestBase, user: TestUserData, amount: number) => {
|
||||
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 })
|
||||
await T.externalAccessToOtherLnd.PayInvoice(invoice.invoice, 0, 100)
|
||||
const u = await T.main.storage.userStorage.GetUser(user.userId)
|
||||
expect(u.balance_sats).to.be.equal(amount)
|
||||
T.d(`user ${user.appUserIdentifier} balance is now ${amount}`)
|
||||
}
|
||||
|
||||
export const runSanityCheck = async (T: TestBase) => {
|
||||
const sanityChecker = new SanityChecker(T.main.storage, T.main.lnd)
|
||||
await sanityChecker.VerifyEventsLog()
|
||||
}
|
||||
|
||||
export const expectThrowsAsync = async (promise: Promise<any>, errorMessage?: string) => {
|
||||
let error: Error | null = null
|
||||
try {
|
||||
await promise
|
||||
}
|
||||
catch (err: any) {
|
||||
error = err as Error
|
||||
}
|
||||
expect(error).to.be.an('Error')
|
||||
console.log(error!.message)
|
||||
if (errorMessage) {
|
||||
expect(error!.message).to.equal(errorMessage)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue