logging, drain logic, watchdog, rugpull

This commit is contained in:
boufni95 2024-07-12 22:31:58 +02:00
parent 32dbd20a76
commit 3a8fdf89f5
46 changed files with 10441 additions and 9936 deletions

View file

@ -13,16 +13,19 @@ import { UserToUserPayment } from "./build/src/services/storage/entity/UserToUse
import { UserTransactionPayment } from "./build/src/services/storage/entity/UserTransactionPayment.js" import { UserTransactionPayment } from "./build/src/services/storage/entity/UserTransactionPayment.js"
import { LspOrder } from "./build/src/services/storage/entity/LspOrder.js" import { LspOrder } from "./build/src/services/storage/entity/LspOrder.js"
import { LndNodeInfo } from "./build/src/services/storage/entity/LndNodeInfo.js" import { LndNodeInfo } from "./build/src/services/storage/entity/LndNodeInfo.js"
import { TrackedProvider } from "./build/src/services/storage/entity/TrackedProvider.js"
import { Initial1703170309875 } from './build/src/services/storage/migrations/1703170309875-initial.js' import { Initial1703170309875 } from './build/src/services/storage/migrations/1703170309875-initial.js'
import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js' import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js'
import { LndNodeInfo1720187506189 } from './build/src/services/storage/migrations/1720187506189-lnd_node_info.js'
import { LiquidityProvider1719335699480 } from './build/src/services/storage/migrations/1719335699480-liquidity_provider.js' import { LiquidityProvider1719335699480 } from './build/src/services/storage/migrations/1719335699480-liquidity_provider.js'
export default new DataSource({ export default new DataSource({
type: "sqlite", type: "sqlite",
database: "db.sqlite", database: "db.sqlite",
// logging: true, // logging: true,
migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480], migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189],
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo], UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo, TrackedProvider],
// synchronize: true, // synchronize: true,
}) })
//npx typeorm migration:generate ./src/services/storage/migrations/lnd_node_info -d ./datasource.js //npx typeorm migration:generate ./src/services/storage/migrations/lnd_node_info -d ./datasource.js

View file

@ -1,2 +1,4 @@
create lnd classes: `npx protoc -I ./others --ts_out=./lnd others/*` create lnd classes: `npx protoc -I ./others --ts_out=./lnd others/*`
create server classes: `npx protoc -I ./service --pub_out=. service/*` create server classes: `npx protoc -I ./service --pub_out=. service/*`
export PATH=$PATH:~/Lightning.Pub/proto

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,282 +1,282 @@
// This file was autogenerated from a .proto file, DO NOT EDIT! // This file was autogenerated from a .proto file, DO NOT EDIT!
import { NostrRequest } from './nostr_transport.js' import { NostrRequest } from './nostr_transport.js'
import * as Types from './types.js' import * as Types from './types.js'
export type ResultError = { status: 'ERROR', reason: string } export type ResultError = { status: 'ERROR', reason: string }
export type NostrClientParams = { export type NostrClientParams = {
pubDestination: string pubDestination: string
retrieveNostrUserAuth: () => Promise<string | null> retrieveNostrUserAuth: () => Promise<string | null>
checkResult?: true checkResult?: true
} }
export default (params: NostrClientParams, send: (to:string, message: NostrRequest) => Promise<any>, subscribe: (to:string, message: NostrRequest, cb:(res:any)=> void) => void) => ({ export default (params: NostrClientParams, send: (to:string, message: NostrRequest) => Promise<any>, subscribe: (to:string, message: NostrRequest, cb:(res:any)=> void) => void) => ({
LinkNPubThroughToken: async (request: Types.LinkNPubThroughTokenRequest): Promise<ResultError | ({ status: 'OK' })> => { LinkNPubThroughToken: async (request: Types.LinkNPubThroughTokenRequest): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
nostrRequest.body = request nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'LinkNPubThroughToken',authIdentifier:auth, ...nostrRequest }) const data = await send(params.pubDestination, {rpcName:'LinkNPubThroughToken',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
return data return data
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
UserHealth: async (): Promise<ResultError | ({ status: 'OK' })> => { UserHealth: async (): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
const data = await send(params.pubDestination, {rpcName:'UserHealth',authIdentifier:auth, ...nostrRequest }) const data = await send(params.pubDestination, {rpcName:'UserHealth',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
return data return data
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
GetUserInfo: async (): Promise<ResultError | ({ status: 'OK' }& Types.UserInfo)> => { GetUserInfo: async (): Promise<ResultError | ({ status: 'OK' }& Types.UserInfo)> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
const data = await send(params.pubDestination, {rpcName:'GetUserInfo',authIdentifier:auth, ...nostrRequest }) const data = await send(params.pubDestination, {rpcName:'GetUserInfo',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return { status: 'OK', ...result } if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.UserInfoValidate(result) const error = Types.UserInfoValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
AddProduct: async (request: Types.AddProductRequest): Promise<ResultError | ({ status: 'OK' }& Types.Product)> => { AddProduct: async (request: Types.AddProductRequest): Promise<ResultError | ({ status: 'OK' }& Types.Product)> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
nostrRequest.body = request nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'AddProduct',authIdentifier:auth, ...nostrRequest }) const data = await send(params.pubDestination, {rpcName:'AddProduct',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return { status: 'OK', ...result } if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.ProductValidate(result) const error = Types.ProductValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
NewProductInvoice: async (query: Types.NewProductInvoice_Query): Promise<ResultError | ({ status: 'OK' }& Types.NewInvoiceResponse)> => { NewProductInvoice: async (query: Types.NewProductInvoice_Query): Promise<ResultError | ({ status: 'OK' }& Types.NewInvoiceResponse)> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
nostrRequest.query = query nostrRequest.query = query
const data = await send(params.pubDestination, {rpcName:'NewProductInvoice',authIdentifier:auth, ...nostrRequest }) const data = await send(params.pubDestination, {rpcName:'NewProductInvoice',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return { status: 'OK', ...result } if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.NewInvoiceResponseValidate(result) const error = Types.NewInvoiceResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
GetUserOperations: async (request: Types.GetUserOperationsRequest): Promise<ResultError | ({ status: 'OK' }& Types.GetUserOperationsResponse)> => { GetUserOperations: async (request: Types.GetUserOperationsRequest): Promise<ResultError | ({ status: 'OK' }& Types.GetUserOperationsResponse)> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
nostrRequest.body = request nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'GetUserOperations',authIdentifier:auth, ...nostrRequest }) const data = await send(params.pubDestination, {rpcName:'GetUserOperations',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return { status: 'OK', ...result } if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.GetUserOperationsResponseValidate(result) const error = Types.GetUserOperationsResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
NewAddress: async (request: Types.NewAddressRequest): Promise<ResultError | ({ status: 'OK' }& Types.NewAddressResponse)> => { NewAddress: async (request: Types.NewAddressRequest): Promise<ResultError | ({ status: 'OK' }& Types.NewAddressResponse)> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
nostrRequest.body = request nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'NewAddress',authIdentifier:auth, ...nostrRequest }) const data = await send(params.pubDestination, {rpcName:'NewAddress',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return { status: 'OK', ...result } if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.NewAddressResponseValidate(result) const error = Types.NewAddressResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
PayAddress: async (request: Types.PayAddressRequest): Promise<ResultError | ({ status: 'OK' }& Types.PayAddressResponse)> => { PayAddress: async (request: Types.PayAddressRequest): Promise<ResultError | ({ status: 'OK' }& Types.PayAddressResponse)> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
nostrRequest.body = request nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'PayAddress',authIdentifier:auth, ...nostrRequest }) const data = await send(params.pubDestination, {rpcName:'PayAddress',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return { status: 'OK', ...result } if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.PayAddressResponseValidate(result) const error = Types.PayAddressResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
NewInvoice: async (request: Types.NewInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.NewInvoiceResponse)> => { NewInvoice: async (request: Types.NewInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.NewInvoiceResponse)> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
nostrRequest.body = request nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'NewInvoice',authIdentifier:auth, ...nostrRequest }) const data = await send(params.pubDestination, {rpcName:'NewInvoice',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return { status: 'OK', ...result } if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.NewInvoiceResponseValidate(result) const error = Types.NewInvoiceResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
DecodeInvoice: async (request: Types.DecodeInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.DecodeInvoiceResponse)> => { DecodeInvoice: async (request: Types.DecodeInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.DecodeInvoiceResponse)> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
nostrRequest.body = request nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'DecodeInvoice',authIdentifier:auth, ...nostrRequest }) const data = await send(params.pubDestination, {rpcName:'DecodeInvoice',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return { status: 'OK', ...result } if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.DecodeInvoiceResponseValidate(result) const error = Types.DecodeInvoiceResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
PayInvoice: async (request: Types.PayInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.PayInvoiceResponse)> => { PayInvoice: async (request: Types.PayInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.PayInvoiceResponse)> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
nostrRequest.body = request nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'PayInvoice',authIdentifier:auth, ...nostrRequest }) const data = await send(params.pubDestination, {rpcName:'PayInvoice',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return { status: 'OK', ...result } if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.PayInvoiceResponseValidate(result) const error = Types.PayInvoiceResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
OpenChannel: async (request: Types.OpenChannelRequest): Promise<ResultError | ({ status: 'OK' }& Types.OpenChannelResponse)> => { OpenChannel: async (request: Types.OpenChannelRequest): Promise<ResultError | ({ status: 'OK' }& Types.OpenChannelResponse)> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
nostrRequest.body = request nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'OpenChannel',authIdentifier:auth, ...nostrRequest }) const data = await send(params.pubDestination, {rpcName:'OpenChannel',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return { status: 'OK', ...result } if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.OpenChannelResponseValidate(result) const error = Types.OpenChannelResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
GetLnurlWithdrawLink: async (): Promise<ResultError | ({ status: 'OK' }& Types.LnurlLinkResponse)> => { GetLnurlWithdrawLink: async (): Promise<ResultError | ({ status: 'OK' }& Types.LnurlLinkResponse)> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
const data = await send(params.pubDestination, {rpcName:'GetLnurlWithdrawLink',authIdentifier:auth, ...nostrRequest }) const data = await send(params.pubDestination, {rpcName:'GetLnurlWithdrawLink',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return { status: 'OK', ...result } if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.LnurlLinkResponseValidate(result) const error = Types.LnurlLinkResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
GetLnurlPayLink: async (): Promise<ResultError | ({ status: 'OK' }& Types.LnurlLinkResponse)> => { GetLnurlPayLink: async (): Promise<ResultError | ({ status: 'OK' }& Types.LnurlLinkResponse)> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
const data = await send(params.pubDestination, {rpcName:'GetLnurlPayLink',authIdentifier:auth, ...nostrRequest }) const data = await send(params.pubDestination, {rpcName:'GetLnurlPayLink',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return { status: 'OK', ...result } if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.LnurlLinkResponseValidate(result) const error = Types.LnurlLinkResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
GetLNURLChannelLink: async (): Promise<ResultError | ({ status: 'OK' }& Types.LnurlLinkResponse)> => { GetLNURLChannelLink: async (): Promise<ResultError | ({ status: 'OK' }& Types.LnurlLinkResponse)> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
const data = await send(params.pubDestination, {rpcName:'GetLNURLChannelLink',authIdentifier:auth, ...nostrRequest }) const data = await send(params.pubDestination, {rpcName:'GetLNURLChannelLink',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return { status: 'OK', ...result } if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.LnurlLinkResponseValidate(result) const error = Types.LnurlLinkResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
GetLiveUserOperations: async (cb: (res:ResultError | ({ status: 'OK' }& Types.LiveUserOperation)) => void): Promise<void> => { GetLiveUserOperations: async (cb: (res:ResultError | ({ status: 'OK' }& Types.LiveUserOperation)) => void): Promise<void> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
subscribe(params.pubDestination, {rpcName:'GetLiveUserOperations',authIdentifier:auth, ...nostrRequest }, (data) => { subscribe(params.pubDestination, {rpcName:'GetLiveUserOperations',authIdentifier:auth, ...nostrRequest }, (data) => {
if (data.status === 'ERROR' && typeof data.reason === 'string') return cb(data) if (data.status === 'ERROR' && typeof data.reason === 'string') return cb(data)
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return cb({ status: 'OK', ...result }) if(!params.checkResult) return cb({ status: 'OK', ...result })
const error = Types.LiveUserOperationValidate(result) const error = Types.LiveUserOperationValidate(result)
if (error === null) { return cb({ status: 'OK', ...result }) } else return cb({ status: 'ERROR', reason: error.message }) if (error === null) { return cb({ status: 'OK', ...result }) } else return cb({ status: 'ERROR', reason: error.message })
} }
return cb({ status: 'ERROR', reason: 'invalid response' }) return cb({ status: 'ERROR', reason: 'invalid response' })
}) })
}, },
GetMigrationUpdate: async (cb: (res:ResultError | ({ status: 'OK' }& Types.MigrationUpdate)) => void): Promise<void> => { GetMigrationUpdate: async (cb: (res:ResultError | ({ status: 'OK' }& Types.MigrationUpdate)) => void): Promise<void> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
subscribe(params.pubDestination, {rpcName:'GetMigrationUpdate',authIdentifier:auth, ...nostrRequest }, (data) => { subscribe(params.pubDestination, {rpcName:'GetMigrationUpdate',authIdentifier:auth, ...nostrRequest }, (data) => {
if (data.status === 'ERROR' && typeof data.reason === 'string') return cb(data) if (data.status === 'ERROR' && typeof data.reason === 'string') return cb(data)
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return cb({ status: 'OK', ...result }) if(!params.checkResult) return cb({ status: 'OK', ...result })
const error = Types.MigrationUpdateValidate(result) const error = Types.MigrationUpdateValidate(result)
if (error === null) { return cb({ status: 'OK', ...result }) } else return cb({ status: 'ERROR', reason: error.message }) if (error === null) { return cb({ status: 'OK', ...result }) } else return cb({ status: 'ERROR', reason: error.message })
} }
return cb({ status: 'ERROR', reason: 'invalid response' }) return cb({ status: 'ERROR', reason: 'invalid response' })
}) })
}, },
GetHttpCreds: async (cb: (res:ResultError | ({ status: 'OK' }& Types.HttpCreds)) => void): Promise<void> => { GetHttpCreds: async (cb: (res:ResultError | ({ status: 'OK' }& Types.HttpCreds)) => void): Promise<void> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {} const nostrRequest: NostrRequest = {}
subscribe(params.pubDestination, {rpcName:'GetHttpCreds',authIdentifier:auth, ...nostrRequest }, (data) => { subscribe(params.pubDestination, {rpcName:'GetHttpCreds',authIdentifier:auth, ...nostrRequest }, (data) => {
if (data.status === 'ERROR' && typeof data.reason === 'string') return cb(data) if (data.status === 'ERROR' && typeof data.reason === 'string') return cb(data)
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return cb({ status: 'OK', ...result }) if(!params.checkResult) return cb({ status: 'OK', ...result })
const error = Types.HttpCredsValidate(result) const error = Types.HttpCredsValidate(result)
if (error === null) { return cb({ status: 'OK', ...result }) } else return cb({ status: 'ERROR', reason: error.message }) if (error === null) { return cb({ status: 'OK', ...result }) } else return cb({ status: 'ERROR', reason: error.message })
} }
return cb({ status: 'ERROR', reason: 'invalid response' }) return cb({ status: 'ERROR', reason: 'invalid response' })
}) })
}, },
BatchUser: async (requests:Types.UserMethodInputs[]): Promise<ResultError | ({ status: 'OK', responses:(Types.UserMethodOutputs)[] })> => { BatchUser: async (requests:Types.UserMethodInputs[]): Promise<ResultError | ({ status: 'OK', responses:(Types.UserMethodOutputs)[] })> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {body:{requests}} const nostrRequest: NostrRequest = {body:{requests}}
const data = await send(params.pubDestination, {rpcName:'BatchUser',authIdentifier:auth, ...nostrRequest }) const data = await send(params.pubDestination, {rpcName:'BatchUser',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
return data return data
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
} }
}) })

View file

@ -1,499 +1,493 @@
// This file was autogenerated from a .proto file, DO NOT EDIT! // This file was autogenerated from a .proto file, DO NOT EDIT!
import * as Types from './types.js' import * as Types from './types.js'
export type Logger = { log: (v: any) => void, error: (v: any) => void } export type Logger = { log: (v: any) => void, error: (v: any) => void }
type NostrResponse = (message: object) => void type NostrResponse = (message: object) => void
export type NostrRequest = { export type NostrRequest = {
rpcName?: string rpcName?: string
params?: Record<string, string> params?: Record<string, string>
query?: Record<string, string> query?: Record<string, string>
body?: any body?: any
authIdentifier?: string authIdentifier?: string
requestId?: string requestId?: string
appId?: string appId?: string
} }
export type NostrOptions = { export type NostrOptions = {
logger?: Logger logger?: Logger
throwErrors?: true throwErrors?: true
metricsCallback: (metrics: Types.RequestMetric[]) => void metricsCallback: (metrics: Types.RequestMetric[]) => void
NostrUserAuthGuard: (appId?: string, identifier?: string) => Promise<Types.UserContext> NostrUserAuthGuard: (appId?:string, identifier?: string) => Promise<Types.UserContext>
} }
const logErrorAndReturnResponse = (error: Error, response: string, res: NostrResponse, logger: Logger, metric: Types.RequestMetric, metricsCallback: (metrics: Types.RequestMetric[]) => void) => { const logErrorAndReturnResponse = (error: Error, response: string, res: NostrResponse, logger: Logger, metric: Types.RequestMetric, metricsCallback: (metrics: Types.RequestMetric[]) => void) => {
logger.error(error.message || error); metricsCallback([{ ...metric, error: response }]); res({ status: 'ERROR', reason: response }) logger.error(error.message || error); metricsCallback([{ ...metric, error: response }]); res({ status: 'ERROR', reason: response })
} }
export default (methods: Types.ServerMethods, opts: NostrOptions) => { export default (methods: Types.ServerMethods, opts: NostrOptions) => {
const logger = opts.logger || { log: console.log, error: console.error } const logger = opts.logger || { log: console.log, error: console.error }
return async (req: NostrRequest, res: NostrResponse, startString: string, startMs: number) => { return async (req: NostrRequest, res: NostrResponse, startString: string, startMs: number) => {
const startTime = BigInt(startString) const startTime = BigInt(startString)
const info: Types.RequestInfo = { rpcName: req.rpcName || 'unkown', batch: false, nostr: true, batchSize: 0 } const info: Types.RequestInfo = { rpcName: req.rpcName || 'unkown', batch: false, nostr: true, batchSize: 0 }
const stats: Types.RequestStats = { startMs, start: startTime, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } const stats: Types.RequestStats = { startMs, start: startTime, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {} let authCtx: Types.AuthContext = {}
switch (req.rpcName) { switch (req.rpcName) {
case 'LinkNPubThroughToken': case 'LinkNPubThroughToken':
try { try {
if (!methods.LinkNPubThroughToken) throw new Error('method: LinkNPubThroughToken is not implemented') if (!methods.LinkNPubThroughToken) throw new Error('method: LinkNPubThroughToken is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
authCtx = authContext authCtx = authContext
const request = req.body const request = req.body
const error = Types.LinkNPubThroughTokenRequestValidate(request) const error = Types.LinkNPubThroughTokenRequestValidate(request)
stats.validate = process.hrtime.bigint() stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback)
await methods.LinkNPubThroughToken({ rpcName: 'LinkNPubThroughToken', ctx: authContext, req: request }) await methods.LinkNPubThroughToken({rpcName:'LinkNPubThroughToken', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint() stats.handle = process.hrtime.bigint()
res({ status: 'OK' }) res({status: 'OK'})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'UserHealth': case 'UserHealth':
try { try {
if (!methods.UserHealth) throw new Error('method: UserHealth is not implemented') if (!methods.UserHealth) throw new Error('method: UserHealth is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
authCtx = authContext authCtx = authContext
stats.validate = stats.guard stats.validate = stats.guard
await methods.UserHealth({ rpcName: 'UserHealth', ctx: authContext }) await methods.UserHealth({rpcName:'UserHealth', ctx:authContext })
stats.handle = process.hrtime.bigint() stats.handle = process.hrtime.bigint()
res({ status: 'OK' }) res({status: 'OK'})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'GetUserInfo': case 'GetUserInfo':
try { try {
if (!methods.GetUserInfo) throw new Error('method: GetUserInfo is not implemented') if (!methods.GetUserInfo) throw new Error('method: GetUserInfo is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
authCtx = authContext authCtx = authContext
stats.validate = stats.guard stats.validate = stats.guard
const response = await methods.GetUserInfo({ rpcName: 'GetUserInfo', ctx: authContext }) const response = await methods.GetUserInfo({rpcName:'GetUserInfo', ctx:authContext })
stats.handle = process.hrtime.bigint() stats.handle = process.hrtime.bigint()
res({ status: 'OK', ...response }) res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'AddProduct': case 'AddProduct':
try { try {
if (!methods.AddProduct) throw new Error('method: AddProduct is not implemented') if (!methods.AddProduct) throw new Error('method: AddProduct is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
authCtx = authContext authCtx = authContext
const request = req.body const request = req.body
const error = Types.AddProductRequestValidate(request) const error = Types.AddProductRequestValidate(request)
stats.validate = process.hrtime.bigint() stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback)
const response = await methods.AddProduct({ rpcName: 'AddProduct', ctx: authContext, req: request }) const response = await methods.AddProduct({rpcName:'AddProduct', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint() stats.handle = process.hrtime.bigint()
res({ status: 'OK', ...response }) res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'NewProductInvoice': case 'NewProductInvoice':
try { try {
if (!methods.NewProductInvoice) throw new Error('method: NewProductInvoice is not implemented') if (!methods.NewProductInvoice) throw new Error('method: NewProductInvoice is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
authCtx = authContext authCtx = authContext
stats.validate = stats.guard stats.validate = stats.guard
const response = await methods.NewProductInvoice({ rpcName: 'NewProductInvoice', ctx: authContext, query: req.query || {} }) const response = await methods.NewProductInvoice({rpcName:'NewProductInvoice', ctx:authContext ,query: req.query||{}})
stats.handle = process.hrtime.bigint() stats.handle = process.hrtime.bigint()
res({ status: 'OK', ...response }) res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'GetUserOperations': case 'GetUserOperations':
try { try {
if (!methods.GetUserOperations) throw new Error('method: GetUserOperations is not implemented') if (!methods.GetUserOperations) throw new Error('method: GetUserOperations is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
authCtx = authContext authCtx = authContext
const request = req.body const request = req.body
const error = Types.GetUserOperationsRequestValidate(request) const error = Types.GetUserOperationsRequestValidate(request)
stats.validate = process.hrtime.bigint() stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback)
const response = await methods.GetUserOperations({ rpcName: 'GetUserOperations', ctx: authContext, req: request }) const response = await methods.GetUserOperations({rpcName:'GetUserOperations', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint() stats.handle = process.hrtime.bigint()
res({ status: 'OK', ...response }) res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'NewAddress': case 'NewAddress':
try { try {
if (!methods.NewAddress) throw new Error('method: NewAddress is not implemented') if (!methods.NewAddress) throw new Error('method: NewAddress is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
authCtx = authContext authCtx = authContext
const request = req.body const request = req.body
const error = Types.NewAddressRequestValidate(request) const error = Types.NewAddressRequestValidate(request)
stats.validate = process.hrtime.bigint() stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback)
const response = await methods.NewAddress({ rpcName: 'NewAddress', ctx: authContext, req: request }) const response = await methods.NewAddress({rpcName:'NewAddress', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint() stats.handle = process.hrtime.bigint()
res({ status: 'OK', ...response }) res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'PayAddress': case 'PayAddress':
try { try {
if (!methods.PayAddress) throw new Error('method: PayAddress is not implemented') if (!methods.PayAddress) throw new Error('method: PayAddress is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
authCtx = authContext authCtx = authContext
const request = req.body const request = req.body
const error = Types.PayAddressRequestValidate(request) const error = Types.PayAddressRequestValidate(request)
stats.validate = process.hrtime.bigint() stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback)
const response = await methods.PayAddress({ rpcName: 'PayAddress', ctx: authContext, req: request }) const response = await methods.PayAddress({rpcName:'PayAddress', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint() stats.handle = process.hrtime.bigint()
res({ status: 'OK', ...response }) res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'NewInvoice': case 'NewInvoice':
try { try {
if (!methods.NewInvoice) throw new Error('method: NewInvoice is not implemented') if (!methods.NewInvoice) throw new Error('method: NewInvoice is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
authCtx = authContext authCtx = authContext
const request = req.body const request = req.body
const error = Types.NewInvoiceRequestValidate(request) const error = Types.NewInvoiceRequestValidate(request)
stats.validate = process.hrtime.bigint() stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback)
const response = await methods.NewInvoice({ rpcName: 'NewInvoice', ctx: authContext, req: request }) const response = await methods.NewInvoice({rpcName:'NewInvoice', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint() stats.handle = process.hrtime.bigint()
res({ status: 'OK', ...response }) res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'DecodeInvoice': case 'DecodeInvoice':
try { try {
if (!methods.DecodeInvoice) throw new Error('method: DecodeInvoice is not implemented') if (!methods.DecodeInvoice) throw new Error('method: DecodeInvoice is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
authCtx = authContext authCtx = authContext
const request = req.body const request = req.body
const error = Types.DecodeInvoiceRequestValidate(request) const error = Types.DecodeInvoiceRequestValidate(request)
stats.validate = process.hrtime.bigint() stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback)
const response = await methods.DecodeInvoice({ rpcName: 'DecodeInvoice', ctx: authContext, req: request }) const response = await methods.DecodeInvoice({rpcName:'DecodeInvoice', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint() stats.handle = process.hrtime.bigint()
res({ status: 'OK', ...response }) res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'PayInvoice': case 'PayInvoice':
try { try {
if (!methods.PayInvoice) throw new Error('method: PayInvoice is not implemented') if (!methods.PayInvoice) throw new Error('method: PayInvoice is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
authCtx = authContext authCtx = authContext
const request = req.body const request = req.body
const error = Types.PayInvoiceRequestValidate(request) const error = Types.PayInvoiceRequestValidate(request)
stats.validate = process.hrtime.bigint() stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback)
const response = await methods.PayInvoice({ rpcName: 'PayInvoice', ctx: authContext, req: request }) const response = await methods.PayInvoice({rpcName:'PayInvoice', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint() stats.handle = process.hrtime.bigint()
res({ status: 'OK', ...response }) res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'OpenChannel': case 'OpenChannel':
try { try {
if (!methods.OpenChannel) throw new Error('method: OpenChannel is not implemented') if (!methods.OpenChannel) throw new Error('method: OpenChannel is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
authCtx = authContext authCtx = authContext
const request = req.body const request = req.body
const error = Types.OpenChannelRequestValidate(request) const error = Types.OpenChannelRequestValidate(request)
stats.validate = process.hrtime.bigint() stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback)
const response = await methods.OpenChannel({ rpcName: 'OpenChannel', ctx: authContext, req: request }) const response = await methods.OpenChannel({rpcName:'OpenChannel', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint() stats.handle = process.hrtime.bigint()
res({ status: 'OK', ...response }) res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'GetLnurlWithdrawLink': case 'GetLnurlWithdrawLink':
try { try {
if (!methods.GetLnurlWithdrawLink) throw new Error('method: GetLnurlWithdrawLink is not implemented') if (!methods.GetLnurlWithdrawLink) throw new Error('method: GetLnurlWithdrawLink is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
authCtx = authContext authCtx = authContext
stats.validate = stats.guard stats.validate = stats.guard
const response = await methods.GetLnurlWithdrawLink({ rpcName: 'GetLnurlWithdrawLink', ctx: authContext }) const response = await methods.GetLnurlWithdrawLink({rpcName:'GetLnurlWithdrawLink', ctx:authContext })
stats.handle = process.hrtime.bigint() stats.handle = process.hrtime.bigint()
res({ status: 'OK', ...response }) res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'GetLnurlPayLink': case 'GetLnurlPayLink':
try { try {
if (!methods.GetLnurlPayLink) throw new Error('method: GetLnurlPayLink is not implemented') if (!methods.GetLnurlPayLink) throw new Error('method: GetLnurlPayLink is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
authCtx = authContext authCtx = authContext
stats.validate = stats.guard stats.validate = stats.guard
const response = await methods.GetLnurlPayLink({ rpcName: 'GetLnurlPayLink', ctx: authContext }) const response = await methods.GetLnurlPayLink({rpcName:'GetLnurlPayLink', ctx:authContext })
stats.handle = process.hrtime.bigint() stats.handle = process.hrtime.bigint()
res({ status: 'OK', ...response }) res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'GetLNURLChannelLink': case 'GetLNURLChannelLink':
try { try {
if (!methods.GetLNURLChannelLink) throw new Error('method: GetLNURLChannelLink is not implemented') if (!methods.GetLNURLChannelLink) throw new Error('method: GetLNURLChannelLink is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
authCtx = authContext authCtx = authContext
stats.validate = stats.guard stats.validate = stats.guard
const response = await methods.GetLNURLChannelLink({ rpcName: 'GetLNURLChannelLink', ctx: authContext }) const response = await methods.GetLNURLChannelLink({rpcName:'GetLNURLChannelLink', ctx:authContext })
stats.handle = process.hrtime.bigint() stats.handle = process.hrtime.bigint()
res({ status: 'OK', ...response }) res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'GetLiveUserOperations': case 'GetLiveUserOperations':
try { try {
if (!methods.GetLiveUserOperations) throw new Error('method: GetLiveUserOperations is not implemented') if (!methods.GetLiveUserOperations) throw new Error('method: GetLiveUserOperations is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
authCtx = authContext authCtx = authContext
stats.validate = stats.guard stats.validate = stats.guard
methods.GetLiveUserOperations({ methods.GetLiveUserOperations({rpcName:'GetLiveUserOperations', ctx:authContext ,cb: (response, err) => {
rpcName: 'GetLiveUserOperations', ctx: authContext, cb: (response, err) => { stats.handle = process.hrtime.bigint()
stats.handle = process.hrtime.bigint() if (err) { logErrorAndReturnResponse(err, err.message, res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback)} else { res({status: 'OK', ...response});opts.metricsCallback([{ ...info, ...stats, ...authContext }])}
if (err) { logErrorAndReturnResponse(err, err.message, res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback) } else { res({ status: 'OK', ...response }); opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } }})
} }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
}) break
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } case 'GetMigrationUpdate':
break try {
case 'GetMigrationUpdate': if (!methods.GetMigrationUpdate) throw new Error('method: GetMigrationUpdate is not implemented')
try { const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
if (!methods.GetMigrationUpdate) throw new Error('method: GetMigrationUpdate is not implemented') stats.guard = process.hrtime.bigint()
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) authCtx = authContext
stats.guard = process.hrtime.bigint() stats.validate = stats.guard
authCtx = authContext methods.GetMigrationUpdate({rpcName:'GetMigrationUpdate', ctx:authContext ,cb: (response, err) => {
stats.validate = stats.guard stats.handle = process.hrtime.bigint()
methods.GetMigrationUpdate({ if (err) { logErrorAndReturnResponse(err, err.message, res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback)} else { res({status: 'OK', ...response});opts.metricsCallback([{ ...info, ...stats, ...authContext }])}
rpcName: 'GetMigrationUpdate', ctx: authContext, cb: (response, err) => { }})
stats.handle = process.hrtime.bigint() }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
if (err) { logErrorAndReturnResponse(err, err.message, res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback) } else { res({ status: 'OK', ...response }); opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } break
} case 'GetHttpCreds':
}) try {
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } if (!methods.GetHttpCreds) throw new Error('method: GetHttpCreds is not implemented')
break const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
case 'GetHttpCreds': stats.guard = process.hrtime.bigint()
try { authCtx = authContext
if (!methods.GetHttpCreds) throw new Error('method: GetHttpCreds is not implemented') stats.validate = stats.guard
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) methods.GetHttpCreds({rpcName:'GetHttpCreds', ctx:authContext ,cb: (response, err) => {
stats.guard = process.hrtime.bigint() stats.handle = process.hrtime.bigint()
authCtx = authContext if (err) { logErrorAndReturnResponse(err, err.message, res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback)} else { res({status: 'OK', ...response});opts.metricsCallback([{ ...info, ...stats, ...authContext }])}
stats.validate = stats.guard }})
methods.GetHttpCreds({ }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
rpcName: 'GetHttpCreds', ctx: authContext, cb: (response, err) => { break
stats.handle = process.hrtime.bigint() case 'BatchUser':
if (err) { logErrorAndReturnResponse(err, err.message, res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback) } else { res({ status: 'OK', ...response }); opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } try {
} info.batch = true
}) const requests = req.body.requests as Types.UserMethodInputs[]
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } if (!Array.isArray(requests))throw new Error('invalid body, is not an array')
break info.batchSize = requests.length
case 'BatchUser': if (requests.length > 10) throw new Error('too many requests in the batch')
try { const ctx = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
info.batch = true stats.guard = process.hrtime.bigint()
const requests = req.body.requests as Types.UserMethodInputs[] authCtx = ctx
if (!Array.isArray(requests)) throw new Error('invalid body, is not an array') stats.validate = stats.guard
info.batchSize = requests.length const responses = []
if (requests.length > 10) throw new Error('too many requests in the batch') const callsMetrics: Types.RequestMetric[] = []
const ctx = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) for (let i = 0; i < requests.length; i++) {
stats.guard = process.hrtime.bigint() const operation = requests[i]
authCtx = ctx const opInfo: Types.RequestInfo = { rpcName: operation.rpcName, batch: true, nostr: true, batchSize: 0 }
stats.validate = stats.guard const opStats: Types.RequestStats = { startMs, start: startTime, parse: stats.parse, guard: stats.guard, validate: 0n, handle: 0n }
const responses = [] try {
const callsMetrics: Types.RequestMetric[] = [] switch(operation.rpcName) {
for (let i = 0; i < requests.length; i++) { case 'LinkNPubThroughToken':
const operation = requests[i] if (!methods.LinkNPubThroughToken) {
const opInfo: Types.RequestInfo = { rpcName: operation.rpcName, batch: true, nostr: true, batchSize: 0 } throw new Error('method not defined: LinkNPubThroughToken')
const opStats: Types.RequestStats = { startMs, start: startTime, parse: stats.parse, guard: stats.guard, validate: 0n, handle: 0n } } else {
try { const error = Types.LinkNPubThroughTokenRequestValidate(operation.req)
switch (operation.rpcName) { opStats.validate = process.hrtime.bigint()
case 'LinkNPubThroughToken': if (error !== null) throw error
if (!methods.LinkNPubThroughToken) { await methods.LinkNPubThroughToken({...operation, ctx}); responses.push({ status: 'OK' })
throw new Error('method not defined: LinkNPubThroughToken') opStats.handle = process.hrtime.bigint()
} else { callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
const error = Types.LinkNPubThroughTokenRequestValidate(operation.req) }
opStats.validate = process.hrtime.bigint() break
if (error !== null) throw error case 'UserHealth':
await methods.LinkNPubThroughToken({ ...operation, ctx }); responses.push({ status: 'OK' }) if (!methods.UserHealth) {
opStats.handle = process.hrtime.bigint() throw new Error('method not defined: UserHealth')
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } else {
} opStats.validate = opStats.guard
break await methods.UserHealth({...operation, ctx}); responses.push({ status: 'OK' })
case 'UserHealth': opStats.handle = process.hrtime.bigint()
if (!methods.UserHealth) { callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
throw new Error('method not defined: UserHealth') }
} else { break
opStats.validate = opStats.guard case 'GetUserInfo':
await methods.UserHealth({ ...operation, ctx }); responses.push({ status: 'OK' }) if (!methods.GetUserInfo) {
opStats.handle = process.hrtime.bigint() throw new Error('method not defined: GetUserInfo')
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } else {
} opStats.validate = opStats.guard
break const res = await methods.GetUserInfo({...operation, ctx}); responses.push({ status: 'OK', ...res })
case 'GetUserInfo': opStats.handle = process.hrtime.bigint()
if (!methods.GetUserInfo) { callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
throw new Error('method not defined: GetUserInfo') }
} else { break
opStats.validate = opStats.guard case 'AddProduct':
const res = await methods.GetUserInfo({ ...operation, ctx }); responses.push({ status: 'OK', ...res }) if (!methods.AddProduct) {
opStats.handle = process.hrtime.bigint() throw new Error('method not defined: AddProduct')
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } else {
} const error = Types.AddProductRequestValidate(operation.req)
break opStats.validate = process.hrtime.bigint()
case 'AddProduct': if (error !== null) throw error
if (!methods.AddProduct) { const res = await methods.AddProduct({...operation, ctx}); responses.push({ status: 'OK', ...res })
throw new Error('method not defined: AddProduct') opStats.handle = process.hrtime.bigint()
} else { callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
const error = Types.AddProductRequestValidate(operation.req) }
opStats.validate = process.hrtime.bigint() break
if (error !== null) throw error case 'NewProductInvoice':
const res = await methods.AddProduct({ ...operation, ctx }); responses.push({ status: 'OK', ...res }) if (!methods.NewProductInvoice) {
opStats.handle = process.hrtime.bigint() throw new Error('method not defined: NewProductInvoice')
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } else {
} opStats.validate = opStats.guard
break const res = await methods.NewProductInvoice({...operation, ctx}); responses.push({ status: 'OK', ...res })
case 'NewProductInvoice': opStats.handle = process.hrtime.bigint()
if (!methods.NewProductInvoice) { callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
throw new Error('method not defined: NewProductInvoice') }
} else { break
opStats.validate = opStats.guard case 'GetUserOperations':
const res = await methods.NewProductInvoice({ ...operation, ctx }); responses.push({ status: 'OK', ...res }) if (!methods.GetUserOperations) {
opStats.handle = process.hrtime.bigint() throw new Error('method not defined: GetUserOperations')
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } else {
} const error = Types.GetUserOperationsRequestValidate(operation.req)
break opStats.validate = process.hrtime.bigint()
case 'GetUserOperations': if (error !== null) throw error
if (!methods.GetUserOperations) { const res = await methods.GetUserOperations({...operation, ctx}); responses.push({ status: 'OK', ...res })
throw new Error('method not defined: GetUserOperations') opStats.handle = process.hrtime.bigint()
} else { callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
const error = Types.GetUserOperationsRequestValidate(operation.req) }
opStats.validate = process.hrtime.bigint() break
if (error !== null) throw error case 'NewAddress':
const res = await methods.GetUserOperations({ ...operation, ctx }); responses.push({ status: 'OK', ...res }) if (!methods.NewAddress) {
opStats.handle = process.hrtime.bigint() throw new Error('method not defined: NewAddress')
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } else {
} const error = Types.NewAddressRequestValidate(operation.req)
break opStats.validate = process.hrtime.bigint()
case 'NewAddress': if (error !== null) throw error
if (!methods.NewAddress) { const res = await methods.NewAddress({...operation, ctx}); responses.push({ status: 'OK', ...res })
throw new Error('method not defined: NewAddress') opStats.handle = process.hrtime.bigint()
} else { callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
const error = Types.NewAddressRequestValidate(operation.req) }
opStats.validate = process.hrtime.bigint() break
if (error !== null) throw error case 'PayAddress':
const res = await methods.NewAddress({ ...operation, ctx }); responses.push({ status: 'OK', ...res }) if (!methods.PayAddress) {
opStats.handle = process.hrtime.bigint() throw new Error('method not defined: PayAddress')
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } else {
} const error = Types.PayAddressRequestValidate(operation.req)
break opStats.validate = process.hrtime.bigint()
case 'PayAddress': if (error !== null) throw error
if (!methods.PayAddress) { const res = await methods.PayAddress({...operation, ctx}); responses.push({ status: 'OK', ...res })
throw new Error('method not defined: PayAddress') opStats.handle = process.hrtime.bigint()
} else { callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
const error = Types.PayAddressRequestValidate(operation.req) }
opStats.validate = process.hrtime.bigint() break
if (error !== null) throw error case 'NewInvoice':
const res = await methods.PayAddress({ ...operation, ctx }); responses.push({ status: 'OK', ...res }) if (!methods.NewInvoice) {
opStats.handle = process.hrtime.bigint() throw new Error('method not defined: NewInvoice')
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } else {
} const error = Types.NewInvoiceRequestValidate(operation.req)
break opStats.validate = process.hrtime.bigint()
case 'NewInvoice': if (error !== null) throw error
if (!methods.NewInvoice) { const res = await methods.NewInvoice({...operation, ctx}); responses.push({ status: 'OK', ...res })
throw new Error('method not defined: NewInvoice') opStats.handle = process.hrtime.bigint()
} else { callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
const error = Types.NewInvoiceRequestValidate(operation.req) }
opStats.validate = process.hrtime.bigint() break
if (error !== null) throw error case 'DecodeInvoice':
const res = await methods.NewInvoice({ ...operation, ctx }); responses.push({ status: 'OK', ...res }) if (!methods.DecodeInvoice) {
opStats.handle = process.hrtime.bigint() throw new Error('method not defined: DecodeInvoice')
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } else {
} const error = Types.DecodeInvoiceRequestValidate(operation.req)
break opStats.validate = process.hrtime.bigint()
case 'DecodeInvoice': if (error !== null) throw error
if (!methods.DecodeInvoice) { const res = await methods.DecodeInvoice({...operation, ctx}); responses.push({ status: 'OK', ...res })
throw new Error('method not defined: DecodeInvoice') opStats.handle = process.hrtime.bigint()
} else { callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
const error = Types.DecodeInvoiceRequestValidate(operation.req) }
opStats.validate = process.hrtime.bigint() break
if (error !== null) throw error case 'PayInvoice':
const res = await methods.DecodeInvoice({ ...operation, ctx }); responses.push({ status: 'OK', ...res }) if (!methods.PayInvoice) {
opStats.handle = process.hrtime.bigint() throw new Error('method not defined: PayInvoice')
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } else {
} const error = Types.PayInvoiceRequestValidate(operation.req)
break opStats.validate = process.hrtime.bigint()
case 'PayInvoice': if (error !== null) throw error
if (!methods.PayInvoice) { const res = await methods.PayInvoice({...operation, ctx}); responses.push({ status: 'OK', ...res })
throw new Error('method not defined: PayInvoice') opStats.handle = process.hrtime.bigint()
} else { callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
const error = Types.PayInvoiceRequestValidate(operation.req) }
opStats.validate = process.hrtime.bigint() break
if (error !== null) throw error case 'OpenChannel':
const res = await methods.PayInvoice({ ...operation, ctx }); responses.push({ status: 'OK', ...res }) if (!methods.OpenChannel) {
opStats.handle = process.hrtime.bigint() throw new Error('method not defined: OpenChannel')
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } else {
} const error = Types.OpenChannelRequestValidate(operation.req)
break opStats.validate = process.hrtime.bigint()
case 'OpenChannel': if (error !== null) throw error
if (!methods.OpenChannel) { const res = await methods.OpenChannel({...operation, ctx}); responses.push({ status: 'OK', ...res })
throw new Error('method not defined: OpenChannel') opStats.handle = process.hrtime.bigint()
} else { callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
const error = Types.OpenChannelRequestValidate(operation.req) }
opStats.validate = process.hrtime.bigint() break
if (error !== null) throw error case 'GetLnurlWithdrawLink':
const res = await methods.OpenChannel({ ...operation, ctx }); responses.push({ status: 'OK', ...res }) if (!methods.GetLnurlWithdrawLink) {
opStats.handle = process.hrtime.bigint() throw new Error('method not defined: GetLnurlWithdrawLink')
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } else {
} opStats.validate = opStats.guard
break const res = await methods.GetLnurlWithdrawLink({...operation, ctx}); responses.push({ status: 'OK', ...res })
case 'GetLnurlWithdrawLink': opStats.handle = process.hrtime.bigint()
if (!methods.GetLnurlWithdrawLink) { callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
throw new Error('method not defined: GetLnurlWithdrawLink') }
} else { break
opStats.validate = opStats.guard case 'GetLnurlPayLink':
const res = await methods.GetLnurlWithdrawLink({ ...operation, ctx }); responses.push({ status: 'OK', ...res }) if (!methods.GetLnurlPayLink) {
opStats.handle = process.hrtime.bigint() throw new Error('method not defined: GetLnurlPayLink')
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } else {
} opStats.validate = opStats.guard
break const res = await methods.GetLnurlPayLink({...operation, ctx}); responses.push({ status: 'OK', ...res })
case 'GetLnurlPayLink': opStats.handle = process.hrtime.bigint()
if (!methods.GetLnurlPayLink) { callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
throw new Error('method not defined: GetLnurlPayLink') }
} else { break
opStats.validate = opStats.guard case 'GetLNURLChannelLink':
const res = await methods.GetLnurlPayLink({ ...operation, ctx }); responses.push({ status: 'OK', ...res }) if (!methods.GetLNURLChannelLink) {
opStats.handle = process.hrtime.bigint() throw new Error('method not defined: GetLNURLChannelLink')
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } else {
} opStats.validate = opStats.guard
break const res = await methods.GetLNURLChannelLink({...operation, ctx}); responses.push({ status: 'OK', ...res })
case 'GetLNURLChannelLink': opStats.handle = process.hrtime.bigint()
if (!methods.GetLNURLChannelLink) { callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
throw new Error('method not defined: GetLNURLChannelLink') }
} else { break
opStats.validate = opStats.guard default:
const res = await methods.GetLNURLChannelLink({ ...operation, ctx }); responses.push({ status: 'OK', ...res }) throw new Error('unkown rpcName')
opStats.handle = process.hrtime.bigint() }
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } catch(ex) {const e = ex as any; logger.error(e.message || e); callsMetrics.push({ ...opInfo, ...opStats, ...ctx, error: e.message }); responses.push({ status: 'ERROR', reason: e.message || e })}
} }
break stats.handle = process.hrtime.bigint()
default: res({ status: 'OK', responses })
throw new Error('unkown rpcName') opts.metricsCallback([{ ...info, ...stats, ...ctx }, ...callsMetrics])
} }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
} catch (ex) { const e = ex as any; logger.error(e.message || e); callsMetrics.push({ ...opInfo, ...opStats, ...ctx, error: e.message }); responses.push({ status: 'ERROR', reason: e.message || e }) } break
} default: logger.error('unknown rpc call name from nostr event:'+req.rpcName)
stats.handle = process.hrtime.bigint() }
res({ status: 'OK', responses }) }
opts.metricsCallback([{ ...info, ...stats, ...ctx }, ...callsMetrics]) }
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break
default: logger.error('unknown rpc call name from nostr event:' + req.rpcName)
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -349,6 +349,9 @@ message UserInfo{
int64 balance = 2; int64 balance = 2;
int64 max_withdrawable = 3; int64 max_withdrawable = 3;
string user_identifier = 4; string user_identifier = 4;
int64 service_fee_bps = 5;
int64 network_max_fee_bps = 6;
int64 network_max_fee_fixed = 7;
} }
message GetUserOperationsRequest{ message GetUserOperationsRequest{

View file

@ -22,7 +22,7 @@ const start = async () => {
log("initializing nostr middleware") log("initializing nostr middleware")
const { Send } = nostrMiddleware(serverMethods, mainHandler, const { Send } = nostrMiddleware(serverMethods, mainHandler,
{ ...nostrSettings, apps, clients: [liquidityProviderInfo] }, { ...nostrSettings, apps, clients: [liquidityProviderInfo] },
(e, p) => mainHandler.liquidProvider.onEvent(e, p) (e, p) => mainHandler.liquidityProvider.onEvent(e, p)
) )
log("starting server") log("starting server")
mainHandler.attachNostrSend(Send) mainHandler.attachNostrSend(Send)

View file

@ -20,7 +20,7 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
let j: NostrRequest let j: NostrRequest
try { try {
j = JSON.parse(event.content) j = JSON.parse(event.content)
log("nostr event", j.rpcName || 'no rpc name') //log("nostr event", j.rpcName || 'no rpc name')
} catch { } catch {
log(ERROR, "invalid json event received", event.content) log(ERROR, "invalid json event received", event.content)
return return

View file

@ -0,0 +1,11 @@
import { MainSettings } from "../main/settings.js";
import { StateBundler } from "../storage/stateBundler.js";
export class Utils {
stateBundler: StateBundler
settings: MainSettings
constructor(settings: MainSettings) {
this.settings = settings
this.stateBundler = new StateBundler()
}
}

View file

@ -11,11 +11,12 @@ const resolveHome = (filepath: string) => {
} }
export const LoadLndSettingsFromEnv = (): LndSettings => { export const LoadLndSettingsFromEnv = (): LndSettings => {
const lndAddr = process.env.LND_ADDRESS || "127.0.0.1:10009" const lndAddr = process.env.LND_ADDRESS || "127.0.0.1:10009"
const lndCertPath = process.env.LND_CERT_PATH || resolveHome("~/.lnd/tls.cert") const lndCertPath = process.env.LND_CERT_PATH || resolveHome("~/.lnd/tls.cert")
const lndMacaroonPath = process.env.LND_MACAROON_PATH || resolveHome("~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon") const lndMacaroonPath = process.env.LND_MACAROON_PATH || resolveHome("~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon")
const feeRateLimit = EnvCanBeInteger("OUTBOUND_MAX_FEE_BPS", 60) / 10000 const feeRateBps = EnvCanBeInteger("OUTBOUND_MAX_FEE_BPS", 60)
const feeFixedLimit = EnvCanBeInteger("OUTBOUND_MAX_FEE_EXTRA_SATS", 100) const feeRateLimit = feeRateBps / 10000
const mockLnd = EnvCanBeBoolean("MOCK_LND") const feeFixedLimit = EnvCanBeInteger("OUTBOUND_MAX_FEE_EXTRA_SATS", 100)
return { mainNode: { lndAddr, lndCertPath, lndMacaroonPath }, feeRateLimit, feeFixedLimit, mockLnd } const mockLnd = EnvCanBeBoolean("MOCK_LND")
return { mainNode: { lndAddr, lndCertPath, lndMacaroonPath }, feeRateLimit, feeFixedLimit, feeRateBps, mockLnd }
} }

View file

@ -16,9 +16,12 @@ import { SendCoinsReq } from './sendCoinsReq.js';
import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo } from './settings.js'; import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo } from './settings.js';
import { getLogger } from '../helpers/logger.js'; import { getLogger } from '../helpers/logger.js';
import { HtlcEvent_EventType } from '../../../proto/lnd/router.js'; import { HtlcEvent_EventType } from '../../../proto/lnd/router.js';
import { LiquidityProvider, LiquidityRequest } from './liquidityProvider.js'; import { LiquidityProvider, LiquidityRequest } from '../main/liquidityProvider.js';
import { Utils } from '../helpers/utilsWrapper.js';
import { TxPointSettings } from '../storage/stateBundler.js';
const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline }) const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
const deadLndRetrySeconds = 5 const deadLndRetrySeconds = 5
type TxActionOptions = { useProvider: boolean, from: 'user' | 'system' }
export default class { export default class {
lightning: LightningClient lightning: LightningClient
invoices: InvoicesClient invoices: InvoicesClient
@ -36,8 +39,10 @@ export default class {
log = getLogger({ component: 'lndManager' }) log = getLogger({ component: 'lndManager' })
outgoingOpsLocked = false outgoingOpsLocked = false
liquidProvider: LiquidityProvider liquidProvider: LiquidityProvider
constructor(settings: LndSettings, liquidProvider: LiquidityProvider, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb) { utils: Utils
constructor(settings: LndSettings, liquidProvider: LiquidityProvider, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb) {
this.settings = settings this.settings = settings
this.utils = utils
this.addressPaidCb = addressPaidCb this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb this.invoicePaidCb = invoicePaidCb
this.newBlockCb = newBlockCb this.newBlockCb = newBlockCb
@ -196,8 +201,7 @@ export default class {
if (tx.numConfirmations === 0) { // only process pending transactions, confirmed transaction are processed by the newBlock CB if (tx.numConfirmations === 0) { // only process pending transactions, confirmed transaction are processed by the newBlock CB
tx.outputDetails.forEach(output => { tx.outputDetails.forEach(output => {
if (output.isOurAddress) { 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), 'lnd')
this.addressPaidCb({ hash: tx.txHash, index: Number(output.outputIndex) }, output.address, Number(output.amount), false)
} }
}) })
} }
@ -217,9 +221,8 @@ export default class {
}, { abort: this.abortController.signal }) }, { abort: this.abortController.signal })
stream.responses.onMessage(invoice => { stream.responses.onMessage(invoice => {
if (invoice.state === Invoice_InvoiceState.SETTLED) { 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.latestKnownSettleIndex = Number(invoice.settleIndex)
this.invoicePaidCb(invoice.paymentRequest, Number(invoice.amtPaidSat), false) this.invoicePaidCb(invoice.paymentRequest, Number(invoice.amtPaidSat), 'lnd')
} }
}) })
stream.responses.onError(error => { stream.responses.onError(error => {
@ -231,8 +234,8 @@ export default class {
}) })
} }
async NewAddress(addressType: Types.AddressType): Promise<NewAddressResponse> { async NewAddress(addressType: Types.AddressType, { useProvider, from }: TxActionOptions): Promise<NewAddressResponse> {
this.log("generating new address")
await this.Health() await this.Health()
let lndAddressType: AddressType let lndAddressType: AddressType
switch (addressType) { switch (addressType) {
@ -248,22 +251,34 @@ export default class {
default: default:
throw new Error("unknown address type " + addressType) throw new Error("unknown address type " + addressType)
} }
const res = await this.lightning.newAddress({ account: "", type: lndAddressType }, DeadLineMetadata()) if (useProvider) {
this.log("new address", res.response.address) throw new Error("provider payments not support chain payments yet")
return res.response }
try {
const res = await this.lightning.newAddress({ account: "", type: lndAddressType }, DeadLineMetadata())
this.utils.stateBundler.AddTxPoint('addedAddress', 1, { from, used: 'lnd' })
return res.response
} catch (err) {
this.utils.stateBundler.AddTxPointFailed('addedAddress', 1, { from, used: 'lnd' })
throw err
}
} }
async NewInvoice(value: number, memo: string, expiry: number, useProvider = false): Promise<Invoice> { async NewInvoice(value: number, memo: string, expiry: number, { useProvider, from }: TxActionOptions): Promise<Invoice> {
this.log("generating new invoice for", value, "sats")
await this.Health() await this.Health()
if (useProvider) { if (useProvider) {
const invoice = await this.liquidProvider.AddInvoice(value, memo) const invoice = await this.liquidProvider.AddInvoice(value, memo, from)
const providerDst = this.liquidProvider.GetProviderDestination() const providerDst = this.liquidProvider.GetProviderDestination()
return { payRequest: invoice, providerDst } return { payRequest: invoice, providerDst }
} }
const res = await this.lightning.addInvoice(AddInvoiceReq(value, expiry, true, memo), DeadLineMetadata()) try {
this.log("new invoice", res.response.paymentRequest) const res = await this.lightning.addInvoice(AddInvoiceReq(value, expiry, true, memo), DeadLineMetadata())
return { payRequest: res.response.paymentRequest } this.utils.stateBundler.AddTxPoint('addedInvoice', value, { from, used: 'lnd' })
return { payRequest: res.response.paymentRequest }
} catch (err) {
this.utils.stateBundler.AddTxPointFailed('addedInvoice', value, { from, used: 'lnd' })
throw err
}
} }
async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> { async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> {
@ -284,42 +299,45 @@ export default class {
const r = res.response const r = res.response
return { local: r.localBalance ? Number(r.localBalance.sat) : 0, remote: r.remoteBalance ? Number(r.remoteBalance.sat) : 0 } 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> { async PayInvoice(invoice: string, amount: number, feeLimit: number, decodedAmount: number, { useProvider, from }: TxActionOptions): Promise<PaidInvoice> {
if (this.outgoingOpsLocked) { if (this.outgoingOpsLocked) {
this.log("outgoing ops locked, rejecting payment request") this.log("outgoing ops locked, rejecting payment request")
throw new Error("lnd node is currently out of sync") throw new Error("lnd node is currently out of sync")
} }
await this.Health() await this.Health()
this.log("paying invoice", invoice, "for", amount, "sats with", useProvider ? 'provider' : 'lnd')
if (useProvider) { if (useProvider) {
const res = await this.liquidProvider.PayInvoice(invoice) const res = await this.liquidProvider.PayInvoice(invoice, decodedAmount, from)
const providerDst = this.liquidProvider.GetProviderDestination() const providerDst = this.liquidProvider.GetProviderDestination()
return { feeSat: res.network_fee + res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage, providerDst } return { feeSat: res.network_fee + res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage, providerDst }
} }
const abortController = new AbortController() try {
const req = PayInvoiceReq(invoice, amount, feeLimit) const abortController = new AbortController()
const stream = this.router.sendPaymentV2(req, { abort: abortController.signal }) const req = PayInvoiceReq(invoice, amount, feeLimit)
return new Promise((res, rej) => { const stream = this.router.sendPaymentV2(req, { abort: abortController.signal })
stream.responses.onError(error => { return new Promise((res, rej) => {
this.log("invoice payment failed", error) stream.responses.onError(error => {
rej(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.utils.stateBundler.AddTxPoint('paidAnInvoice', Number(payment.valueSat), { from, used: 'lnd', timeDiscount: true })
res({ feeSat: Math.ceil(Number(payment.feeMsat) / 1000), valueSat: Number(payment.valueSat), paymentPreimage: payment.paymentPreimage })
return
}
})
}) })
stream.responses.onMessage(payment => { } catch (err) {
switch (payment.status) { this.utils.stateBundler.AddTxPointFailed('paidAnInvoice', decodedAmount, { from, used: 'lnd' })
case Payment_PaymentStatus.FAILED: throw err
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 })
return
default:
this.log("inflight payment update index", Number(payment.paymentIndex), Payment_PaymentStatus[payment.status])
}
})
})
} }
async EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse> { async EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse> {
@ -333,16 +351,23 @@ export default class {
return res.response return res.response
} }
async PayAddress(address: string, amount: number, satPerVByte: number, label = ""): Promise<SendCoinsResponse> { async PayAddress(address: string, amount: number, satPerVByte: number, label = "", { useProvider, from }: TxActionOptions): Promise<SendCoinsResponse> {
if (this.outgoingOpsLocked) { if (this.outgoingOpsLocked) {
this.log("outgoing ops locked, rejecting payment request") this.log("outgoing ops locked, rejecting payment request")
throw new Error("lnd node is currently out of sync") throw new Error("lnd node is currently out of sync")
} }
await this.Health() if (useProvider) {
this.log("sending chain TX for", amount, "sats", "to", address) throw new Error("provider payments not support chain payments yet")
const res = await this.lightning.sendCoins(SendCoinsReq(address, amount, satPerVByte, label), DeadLineMetadata()) }
this.log("sent chain TX for", amount, "sats", "to", address) try {
return res.response await this.Health()
const res = await this.lightning.sendCoins(SendCoinsReq(address, amount, satPerVByte, label), DeadLineMetadata())
this.utils.stateBundler.AddTxPoint('paidAnAddress', amount, { from, used: 'lnd', timeDiscount: true })
return res.response
} catch (err) {
this.utils.stateBundler.AddTxPointFailed('paidAnAddress', amount, { from, used: 'lnd' })
throw err
}
} }
async GetTransactions(startHeight: number): Promise<TransactionDetails> { async GetTransactions(startHeight: number): Promise<TransactionDetails> {
@ -361,7 +386,20 @@ export default class {
return res.response return res.response
} }
async GetBalance(): Promise<BalanceInfo> { async GetTotalBalace() {
const walletBalance = await this.GetWalletBalance()
const confirmedWalletBalance = Number(walletBalance.confirmedBalance)
this.utils.stateBundler.AddBalancePoint('walletBalance', confirmedWalletBalance)
const channelsBalance = await this.GetChannelBalance()
const totalLightningBalanceMsats = (channelsBalance.localBalance?.msat || 0n) + (channelsBalance.unsettledLocalBalance?.msat || 0n)
const totalLightningBalance = Math.ceil(Number(totalLightningBalanceMsats) / 1000)
this.utils.stateBundler.AddBalancePoint('channelBalance', totalLightningBalance)
const totalLndBalance = confirmedWalletBalance + totalLightningBalance
this.utils.stateBundler.AddBalancePoint('totalLndBalance', totalLndBalance)
return totalLndBalance
}
async GetBalance(): Promise<BalanceInfo> { // TODO: remove this
const wRes = await this.lightning.walletBalance({}, DeadLineMetadata()) const wRes = await this.lightning.walletBalance({}, DeadLineMetadata())
const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response
const { response } = await this.lightning.listChannels({ const { response } = await this.lightning.listChannels({

View file

@ -1,5 +1,5 @@
import fetch from "node-fetch" import fetch from "node-fetch"
import { LiquidityProvider } from "./liquidityProvider.js" import { LiquidityProvider } from "../main/liquidityProvider.js"
import { getLogger, PubLogger } from '../helpers/logger.js' import { getLogger, PubLogger } from '../helpers/logger.js'
import LND from "./lnd.js" import LND from "./lnd.js"
import { AddressType } from "../../../proto/autogenerated/ts/types.js" import { AddressType } from "../../../proto/autogenerated/ts/types.js"
@ -113,7 +113,7 @@ export class FlashsatsLSP extends LSP {
this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee) this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee)
return null return null
} }
const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice) const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice, decoded.numSatoshis, 'system')
const fees = +order.payment.fee_total_sat + res.network_fee + res.service_fee const fees = +order.payment.fee_total_sat + res.network_fee + res.service_fee
this.log("paid", res.amount_paid, "to open channel, and a fee of", fees) this.log("paid", res.amount_paid, "to open channel, and a fee of", fees)
return { orderId: order.order_id, invoice: order.payment.bolt11_invoice, totalSats: +order.payment.order_total_sat, fees } return { orderId: order.order_id, invoice: order.payment.bolt11_invoice, totalSats: +order.payment.order_total_sat, fees }
@ -163,7 +163,7 @@ export class OlympusLSP extends LSP {
await this.addPeer(servicePub, host) await this.addPeer(servicePub, host)
const lndInfo = await this.lnd.GetInfo() const lndInfo = await this.lnd.GetInfo()
const myPub = lndInfo.identityPubkey const myPub = lndInfo.identityPubkey
const refundAddr = await this.lnd.NewAddress(AddressType.WITNESS_PUBKEY_HASH) const refundAddr = await this.lnd.NewAddress(AddressType.WITNESS_PUBKEY_HASH, { useProvider: false, from: 'system' })
const channelSize = Math.floor(maxSpendable * (1 - this.settings.maxRelativeFee)) * 2 const channelSize = Math.floor(maxSpendable * (1 - this.settings.maxRelativeFee)) * 2
const lspBalance = channelSize.toString() const lspBalance = channelSize.toString()
const chanExpiryBlocks = serviceInfo.max_channel_expiry_blocks const chanExpiryBlocks = serviceInfo.max_channel_expiry_blocks
@ -186,7 +186,7 @@ export class OlympusLSP extends LSP {
this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee) this.log("invoice relative fee of", relativeFee, "exceeds max relative fee of", this.settings.maxRelativeFee)
return null return null
} }
const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11.invoice) const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11.invoice, decoded.numSatoshis, 'system')
const fees = +order.payment.bolt11.fee_total_sat + res.network_fee + res.service_fee const fees = +order.payment.bolt11.fee_total_sat + res.network_fee + res.service_fee
this.log("paid", res.amount_paid, "to open channel, and a fee of", fees) this.log("paid", res.amount_paid, "to open channel, and a fee of", fees)
return { orderId: order.order_id, invoice: order.payment.bolt11.invoice, totalSats: +order.payment.bolt11.order_total_sat, fees } return { orderId: order.order_id, invoice: order.payment.bolt11.invoice, totalSats: +order.payment.bolt11.order_total_sat, fees }
@ -275,7 +275,7 @@ export class VoltageLSP extends LSP {
} }
await this.addPeer(info.pubkey, `${ipv4.address}:${ipv4.port}`) await this.addPeer(info.pubkey, `${ipv4.address}:${ipv4.port}`)
const invoice = await this.lnd.NewInvoice(amtSats, "open channel", 60 * 60) const invoice = await this.lnd.NewInvoice(amtSats, "open channel", 60 * 60, { from: 'system', useProvider: false })
const proposalRes = await this.proposal(invoice.payRequest, fee.id) const proposalRes = await this.proposal(invoice.payRequest, fee.id)
this.log("proposal res", proposalRes, fee.id) this.log("proposal res", proposalRes, fee.id)
const decoded = await this.lnd.DecodeInvoice(proposalRes.jit_bolt11) const decoded = await this.lnd.DecodeInvoice(proposalRes.jit_bolt11)
@ -287,7 +287,7 @@ export class VoltageLSP extends LSP {
this.log("invoice of amount", decoded.numSatoshis, "exceeds user balance of", maxSpendable) this.log("invoice of amount", decoded.numSatoshis, "exceeds user balance of", maxSpendable)
return null return null
} }
const res = await this.liquidityProvider.PayInvoice(proposalRes.jit_bolt11) const res = await this.liquidityProvider.PayInvoice(proposalRes.jit_bolt11, decoded.numSatoshis, 'system')
const fees = feeSats + res.network_fee + res.service_fee const fees = feeSats + res.network_fee + res.service_fee
this.log("paid", res.amount_paid, "to open channel, and a fee of", fees) this.log("paid", res.amount_paid, "to open channel, and a fee of", fees)
return { orderId: fee.id, invoice: proposalRes.jit_bolt11, totalSats: decoded.numSatoshis, fees } return { orderId: fee.id, invoice: proposalRes.jit_bolt11, totalSats: decoded.numSatoshis, fees }

View file

@ -8,6 +8,7 @@ export type LndSettings = {
mainNode: NodeSettings mainNode: NodeSettings
feeRateLimit: number feeRateLimit: number
feeFixedLimit: number feeFixedLimit: number
feeRateBps: number
mockLnd: boolean mockLnd: boolean
otherNode?: NodeSettings otherNode?: NodeSettings
@ -30,8 +31,8 @@ export type BalanceInfo = {
channelsBalance: ChannelBalance[]; channelsBalance: ChannelBalance[];
} }
export type AddressPaidCb = (txOutput: TxOutput, address: string, amount: number, internal: boolean) => void export type AddressPaidCb = (txOutput: TxOutput, address: string, amount: number, used: 'lnd' | 'provider' | 'internal') => Promise<void>
export type InvoicePaidCb = (paymentRequest: string, amount: number, internal: boolean) => void export type InvoicePaidCb = (paymentRequest: string, amount: number, used: 'lnd' | 'provider' | 'internal') => Promise<void>
export type NewBlockCb = (height: number) => void export type NewBlockCb = (height: number) => void
export type HtlcCb = (event: HtlcEvent) => void export type HtlcCb = (event: HtlcEvent) => void

View file

@ -56,7 +56,10 @@ export default class {
userId: ctx.user_id, userId: ctx.user_id,
balance: user.balance_sats, balance: user.balance_sats,
max_withdrawable: this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats, true), max_withdrawable: this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats, true),
user_identifier: appUser.identifier user_identifier: appUser.identifier,
network_max_fee_bps: this.settings.lndSettings.feeRateBps,
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit,
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps
} }
} }

View file

@ -154,7 +154,11 @@ export default class {
userId: u.user.user_id, userId: u.user.user_id,
balance: u.user.balance_sats, balance: u.user.balance_sats,
max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true), max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true),
user_identifier: u.identifier user_identifier: u.identifier,
network_max_fee_bps: this.settings.lndSettings.feeRateBps,
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit,
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps
}, },
max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true) max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true)
} }
@ -191,7 +195,10 @@ export default class {
max_withdrawable: max, identifier: req.user_identifier, info: { max_withdrawable: max, identifier: req.user_identifier, info: {
userId: user.user.user_id, balance: user.user.balance_sats, userId: user.user.user_id, balance: user.user.balance_sats,
max_withdrawable: this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true), max_withdrawable: this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true),
user_identifier: user.identifier user_identifier: user.identifier,
network_max_fee_bps: this.settings.lndSettings.feeRateBps,
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit,
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps
} }
} }
} }

View file

@ -15,8 +15,10 @@ import { UnsignedEvent } from '../nostr/tools/event.js'
import { NostrSend } from '../nostr/handler.js' import { NostrSend } from '../nostr/handler.js'
import MetricsManager from '../metrics/index.js' import MetricsManager from '../metrics/index.js'
import { LoggedEvent } from '../storage/eventsLog.js' import { LoggedEvent } from '../storage/eventsLog.js'
import { LiquidityProvider } from "../lnd/liquidityProvider.js" import { LiquidityProvider } from "./liquidityProvider.js"
import { LiquidityManager } from "./liquidityManager.js" import { LiquidityManager } from "./liquidityManager.js"
import { Utils } from "../helpers/utilsWrapper.js"
import { RugPullTracker } from "./rugPullTracker.js"
type UserOperationsSub = { type UserOperationsSub = {
id: string id: string
@ -37,18 +39,22 @@ export default class {
paymentManager: PaymentManager paymentManager: PaymentManager
paymentSubs: Record<string, ((op: Types.UserOperation) => void) | null> = {} paymentSubs: Record<string, ((op: Types.UserOperation) => void) | null> = {}
metricsManager: MetricsManager metricsManager: MetricsManager
liquidProvider: LiquidityProvider liquidityProvider: LiquidityProvider
liquidityManager: LiquidityManager liquidityManager: LiquidityManager
utils: Utils
rugPullTracker: RugPullTracker
nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") } nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") }
constructor(settings: MainSettings, storage: Storage) { constructor(settings: MainSettings, storage: Storage, utils: Utils) {
this.settings = settings this.settings = settings
this.storage = storage this.storage = storage
this.liquidProvider = new LiquidityProvider(settings.liquiditySettings.liquidityProviderPub, this.invoicePaidCb) this.utils = utils
this.lnd = new LND(settings.lndSettings, this.liquidProvider, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb) this.liquidityProvider = new LiquidityProvider(settings.liquiditySettings.liquidityProviderPub, this.utils, this.invoicePaidCb)
this.liquidityManager = new LiquidityManager(this.settings.liquiditySettings, this.storage, this.liquidProvider, this.lnd) this.rugPullTracker = new RugPullTracker(this.storage, this.liquidityProvider)
this.lnd = new LND(settings.lndSettings, this.liquidityProvider, this.utils, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb)
this.liquidityManager = new LiquidityManager(this.settings.liquiditySettings, this.storage, this.utils, this.liquidityProvider, this.lnd, this.rugPullTracker)
this.metricsManager = new MetricsManager(this.storage, this.lnd) this.metricsManager = new MetricsManager(this.storage, this.lnd)
this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings, this.liquidityManager, this.addressPaidCb, this.invoicePaidCb) this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings, this.liquidityManager, this.utils, this.addressPaidCb, this.invoicePaidCb)
this.productManager = new ProductManager(this.storage, this.paymentManager, this.settings) this.productManager = new ProductManager(this.storage, this.paymentManager, this.settings)
this.applicationManager = new ApplicationManager(this.storage, this.settings, this.paymentManager) this.applicationManager = new ApplicationManager(this.storage, this.settings, this.paymentManager)
this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager) this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager)
@ -68,7 +74,7 @@ export default class {
attachNostrSend(f: NostrSend) { attachNostrSend(f: NostrSend) {
this.nostrSend = f this.nostrSend = f
this.liquidProvider.attachNostrSend(f) this.liquidityProvider.attachNostrSend(f)
} }
htlcCb: HtlcCb = (e) => { htlcCb: HtlcCb = (e) => {
@ -124,11 +130,12 @@ export default class {
})) }))
} }
addressPaidCb: AddressPaidCb = (txOutput, address, amount, internal) => { addressPaidCb: AddressPaidCb = (txOutput, address, amount, used) => {
this.storage.StartTransaction(async tx => { return this.storage.StartTransaction(async tx => {
const { blockHeight } = await this.lnd.GetInfo() const { blockHeight } = await this.lnd.GetInfo()
const userAddress = await this.storage.paymentStorage.GetAddressOwner(address, tx) const userAddress = await this.storage.paymentStorage.GetAddressOwner(address, tx)
if (!userAddress) { return } if (!userAddress) { return }
const internal = used === 'internal'
let log = getLogger({}) let log = getLogger({})
if (!userAddress.linkedApplication) { if (!userAddress.linkedApplication) {
log(ERROR, "an address was paid, that has no linked application") log(ERROR, "an address was paid, that has no linked application")
@ -155,17 +162,20 @@ export default class {
const operationId = `${Types.UserOperationType.INCOMING_TX}-${addedTx.serial_id}` 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 } 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) this.sendOperationToNostr(userAddress.linkedApplication, userAddress.user.user_id, op)
} catch { this.utils.stateBundler.AddTxPoint('addressWasPaid', amount, { used, from: 'system', timeDiscount: true })
} catch (err: any) {
this.utils.stateBundler.AddTxPointFailed('addressWasPaid', amount, { used, from: 'system' })
log(ERROR, "cannot process address paid transaction, already registered") log(ERROR, "cannot process address paid transaction, already registered")
} }
}) })
} }
invoicePaidCb: InvoicePaidCb = (paymentRequest, amount, internal) => { invoicePaidCb: InvoicePaidCb = (paymentRequest, amount, used) => {
this.storage.StartTransaction(async tx => { return this.storage.StartTransaction(async tx => {
let log = getLogger({}) let log = getLogger({})
const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(paymentRequest, tx) const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(paymentRequest, tx)
if (!userInvoice) { return } if (!userInvoice) { return }
const internal = used === 'internal'
if (userInvoice.paid_at_unix > 0 && internal) { log("cannot pay internally, invoice already paid"); 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.paid_at_unix > 0 && !internal && userInvoice.paidByLnd) { log("invoice already paid by lnd"); return }
if (!userInvoice.linkedApplication) { if (!userInvoice.linkedApplication) {
@ -190,9 +200,10 @@ export default class {
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 } 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.sendOperationToNostr(userInvoice.linkedApplication, userInvoice.user.user_id, op)
this.createZapReceipt(log, userInvoice) this.createZapReceipt(log, userInvoice)
log("paid invoice processed successfully")
this.liquidityManager.afterInInvoicePaid() this.liquidityManager.afterInInvoicePaid()
this.utils.stateBundler.AddTxPoint('invoiceWasPaid', amount, { used, from: 'system', timeDiscount: true })
} catch (err: any) { } catch (err: any) {
this.utils.stateBundler.AddTxPointFailed('invoiceWasPaid', amount, { used, from: 'system' })
log(ERROR, "cannot process paid invoice", err.message || "") log(ERROR, "cannot process paid invoice", err.message || "")
} }
}) })

View file

@ -1,11 +1,12 @@
import { PubLogger, getLogger } from "../helpers/logger.js" import { PubLogger, getLogger } from "../helpers/logger.js"
import { LiquidityProvider } from "../lnd/liquidityProvider.js" import { LiquidityProvider } from "./liquidityProvider.js"
import { Unlocker } from "./unlocker.js" import { Unlocker } from "./unlocker.js"
import Storage from "../storage/index.js" import Storage from "../storage/index.js"
import { TypeOrmMigrationRunner } from "../storage/migrations/runner.js" import { TypeOrmMigrationRunner } from "../storage/migrations/runner.js"
import Main from "./index.js" import Main from "./index.js"
import SanityChecker from "./sanityChecker.js" import SanityChecker from "./sanityChecker.js"
import { MainSettings } from "./settings.js" import { MainSettings } from "./settings.js"
import { Utils } from "../helpers/utilsWrapper.js"
export type AppData = { export type AppData = {
privateKey: string; privateKey: string;
publicKey: string; publicKey: string;
@ -13,6 +14,7 @@ export type AppData = {
name: string; name: string;
} }
export const initMainHandler = async (log: PubLogger, mainSettings: MainSettings) => { export const initMainHandler = async (log: PubLogger, mainSettings: MainSettings) => {
const utils = new Utils(mainSettings)
const storageManager = new Storage(mainSettings.storageSettings) const storageManager = new Storage(mainSettings.storageSettings)
const manualMigration = await TypeOrmMigrationRunner(log, storageManager, mainSettings.storageSettings.dbSettings, process.argv[2]) const manualMigration = await TypeOrmMigrationRunner(log, storageManager, mainSettings.storageSettings.dbSettings, process.argv[2])
if (manualMigration) { if (manualMigration) {
@ -21,7 +23,7 @@ export const initMainHandler = async (log: PubLogger, mainSettings: MainSettings
const unlocker = new Unlocker(mainSettings, storageManager) const unlocker = new Unlocker(mainSettings, storageManager)
await unlocker.Unlock() await unlocker.Unlock()
const mainHandler = new Main(mainSettings, storageManager) const mainHandler = new Main(mainSettings, storageManager, utils)
await mainHandler.lnd.Warmup() await mainHandler.lnd.Warmup()
if (!mainSettings.skipSanityCheck) { if (!mainSettings.skipSanityCheck) {
const sanityChecker = new SanityChecker(storageManager, mainHandler.lnd) const sanityChecker = new SanityChecker(storageManager, mainHandler.lnd)
@ -51,7 +53,7 @@ export const initMainHandler = async (log: PubLogger, mainSettings: MainSettings
publicKey: liquidityProviderApp.publicKey, publicKey: liquidityProviderApp.publicKey,
name: "liquidity_provider", clientId: `client_${liquidityProviderApp.appId}` name: "liquidity_provider", clientId: `client_${liquidityProviderApp.appId}`
} }
mainHandler.liquidProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey }) mainHandler.liquidityProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey })
const stop = await processArgs(mainHandler) const stop = await processArgs(mainHandler)
if (stop) { if (stop) {
return return

View file

@ -1,9 +1,11 @@
import { getLogger } from "../helpers/logger.js" import { getLogger } from "../helpers/logger.js"
import { LiquidityProvider } from "../lnd/liquidityProvider.js" import { Utils } from "../helpers/utilsWrapper.js"
import { LiquidityProvider } from "./liquidityProvider.js"
import LND from "../lnd/lnd.js" import LND from "../lnd/lnd.js"
import { FlashsatsLSP, LoadLSPSettingsFromEnv, LSPSettings, OlympusLSP, VoltageLSP } from "../lnd/lsp.js" import { FlashsatsLSP, LoadLSPSettingsFromEnv, LSPSettings, OlympusLSP, VoltageLSP } from "../lnd/lsp.js"
import Storage from '../storage/index.js' import Storage from '../storage/index.js'
import { defaultInvoiceExpiry } from "../storage/paymentStorage.js" import { defaultInvoiceExpiry } from "../storage/paymentStorage.js"
import { RugPullTracker } from "./rugPullTracker.js"
export type LiquiditySettings = { export type LiquiditySettings = {
lspSettings: LSPSettings lspSettings: LSPSettings
liquidityProviderPub: string liquidityProviderPub: string
@ -18,6 +20,7 @@ export class LiquidityManager {
settings: LiquiditySettings settings: LiquiditySettings
storage: Storage storage: Storage
liquidityProvider: LiquidityProvider liquidityProvider: LiquidityProvider
rugPullTracker: RugPullTracker
lnd: LND lnd: LND
olympusLSP: OlympusLSP olympusLSP: OlympusLSP
voltageLSP: VoltageLSP voltageLSP: VoltageLSP
@ -26,31 +29,28 @@ export class LiquidityManager {
channelRequested = false channelRequested = false
channelRequesting = false channelRequesting = false
feesPaid = 0 feesPaid = 0
constructor(settings: LiquiditySettings, storage: Storage, liquidityProvider: LiquidityProvider, lnd: LND) { utils: Utils
latestDrain: ({ success: true, amt: number } | { success: false, amt: number, attempt: number, at: Date }) = { success: true, amt: 0 }
drainsSkipped = 0
constructor(settings: LiquiditySettings, storage: Storage, utils: Utils, liquidityProvider: LiquidityProvider, lnd: LND, rugPullTracker: RugPullTracker) {
this.settings = settings this.settings = settings
this.storage = storage this.storage = storage
this.liquidityProvider = liquidityProvider this.liquidityProvider = liquidityProvider
this.lnd = lnd this.lnd = lnd
this.rugPullTracker = rugPullTracker
this.utils = utils
this.olympusLSP = new OlympusLSP(settings.lspSettings, lnd, liquidityProvider) this.olympusLSP = new OlympusLSP(settings.lspSettings, lnd, liquidityProvider)
this.voltageLSP = new VoltageLSP(settings.lspSettings, lnd, liquidityProvider) this.voltageLSP = new VoltageLSP(settings.lspSettings, lnd, liquidityProvider)
this.flashsatsLSP = new FlashsatsLSP(settings.lspSettings, lnd, liquidityProvider) this.flashsatsLSP = new FlashsatsLSP(settings.lspSettings, lnd, liquidityProvider)
} }
GetPaidFees = () => { GetPaidFees = () => {
this.utils.stateBundler.AddBalancePoint('feesPaidForLiquidity', this.feesPaid)
return this.feesPaid return this.feesPaid
} }
onNewBlock = async () => { onNewBlock = async () => {
const balance = await this.liquidityProvider.GetLatestMaxWithdrawable() await this.shouldDrainProvider()
const { remote } = await this.lnd.ChannelBalance()
if (remote > balance && balance > 0) {
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)
const fees = res.network_fee + res.service_fee
this.log("drained provider balance to channel", res.amount_paid, "fees paid:", fees)
this.feesPaid += fees
}
} }
beforeInvoiceCreation = async (amount: number): Promise<'lnd' | 'provider'> => { beforeInvoiceCreation = async (amount: number): Promise<'lnd' | 'provider'> => {
@ -58,17 +58,18 @@ export class LiquidityManager {
return 'provider' return 'provider'
} }
if (this.rugPullTracker.HasProviderRugPulled()) {
return 'lnd'
}
const { remote } = await this.lnd.ChannelBalance() const { remote } = await this.lnd.ChannelBalance()
if (remote > amount) { if (remote > amount) {
this.log("channel has enough balance for invoice")
return 'lnd' return 'lnd'
} }
const providerCanHandle = this.liquidityProvider.CanProviderHandle({ action: 'receive', amount }) const providerCanHandle = this.liquidityProvider.CanProviderHandle({ action: 'receive', amount })
if (!providerCanHandle) { if (!providerCanHandle) {
return 'lnd' return 'lnd'
} }
this.log("channel does not have enough balance for invoice,suggesting provider")
return 'provider' return 'provider'
} }
@ -80,6 +81,75 @@ export class LiquidityManager {
} }
} }
beforeOutInvoicePayment = async (amount: number): Promise<'lnd' | 'provider'> => {
if (this.settings.useOnlyLiquidityProvider) {
return 'provider'
}
const canHandle = await this.liquidityProvider.CanProviderHandle({ action: 'spend', amount })
if (canHandle) {
return 'provider'
}
return 'lnd'
}
afterOutInvoicePaid = async () => { }
shouldDrainProvider = async () => {
const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable()
const { remote } = await this.lnd.ChannelBalance()
const drainable = Math.min(maxW, remote)
if (drainable < 500) {
return
}
if (this.latestDrain.success) {
if (this.latestDrain.amt === 0) {
await this.drainProvider(drainable)
} else {
await this.drainProvider(Math.min(drainable, this.latestDrain.amt * 2))
}
} else if (this.latestDrain.attempt * 10 < this.drainsSkipped) {
const drain = Math.min(drainable, Math.ceil(this.latestDrain.amt / 2))
this.drainsSkipped = 0
if (drain < 500) {
this.log("drain attempt went below 500 sats, will start again")
this.updateLatestDrain(true, 0)
} else {
await this.drainProvider(drain)
}
} else {
this.drainsSkipped += 1
}
}
drainProvider = async (amt: number) => {
try {
const invoice = await this.lnd.NewInvoice(amt, "liqudity provider drain", defaultInvoiceExpiry, { from: 'system', useProvider: false })
const res = await this.liquidityProvider.PayInvoice(invoice.payRequest, amt, 'system')
const fees = res.network_fee + res.service_fee
this.feesPaid += fees
this.updateLatestDrain(true, amt)
} catch (err: any) {
this.log("error draining provider balance", err.message || err)
this.updateLatestDrain(false, amt)
}
}
updateLatestDrain = (success: boolean, amt: number) => {
if (this.latestDrain.success) {
if (success) {
this.latestDrain = { success: true, amt }
} else {
this.latestDrain = { success: false, amt, attempt: 1, at: new Date() }
}
} else {
if (success) {
this.latestDrain = { success: true, amt }
} else {
this.latestDrain = { success: false, amt, attempt: this.latestDrain.attempt + 1, at: new Date() }
}
}
}
shouldOpenChannel = async (): Promise<{ shouldOpen: false } | { shouldOpen: true, maxSpendable: number }> => { shouldOpenChannel = async (): Promise<{ shouldOpen: false } | { shouldOpen: true, maxSpendable: number }> => {
const threshold = this.settings.lspSettings.channelThreshold const threshold = this.settings.lspSettings.channelThreshold
if (threshold === 0) { if (threshold === 0) {
@ -96,12 +166,12 @@ export class LiquidityManager {
this.log("pending open channels detected, liquidiity might be on the way") this.log("pending open channels detected, liquidiity might be on the way")
return { shouldOpen: false } return { shouldOpen: false }
} }
const userState = await this.liquidityProvider.CheckUserState() const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable()
if (!userState || userState.max_withdrawable < threshold) { if (maxW < threshold) {
this.log("balance of", userState?.max_withdrawable || 0, "is lower than channel threshold of", threshold) this.log("max withdrawable of", maxW, "is lower than channel threshold of", threshold)
return { shouldOpen: false } return { shouldOpen: false }
} }
return { shouldOpen: true, maxSpendable: userState.max_withdrawable } return { shouldOpen: true, maxSpendable: maxW }
} }
orderChannelIfNeeded = async () => { orderChannelIfNeeded = async () => {
@ -150,19 +220,4 @@ export class LiquidityManager {
this.channelRequesting = false this.channelRequesting = false
this.log("no channel requested") this.log("no channel requested")
} }
beforeOutInvoicePayment = async (amount: number): Promise<'lnd' | 'provider'> => {
if (this.settings.useOnlyLiquidityProvider) {
return '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 () => { }
} }

View file

@ -3,9 +3,11 @@ import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js
import * as Types from '../../../proto/autogenerated/ts/types.js' import * as Types from '../../../proto/autogenerated/ts/types.js'
import { decodeNprofile } from '../../custom-nip19.js' import { decodeNprofile } from '../../custom-nip19.js'
import { getLogger } from '../helpers/logger.js' import { getLogger } from '../helpers/logger.js'
import { Utils } from '../helpers/utilsWrapper.js'
import { NostrEvent, NostrSend } from '../nostr/handler.js' import { NostrEvent, NostrSend } from '../nostr/handler.js'
import { relayInit } from '../nostr/tools/relay.js' import { relayInit } from '../nostr/tools/relay.js'
import { InvoicePaidCb } from './settings.js' import { InvoicePaidCb } from '../lnd/settings.js'
import Storage from '../storage/index.js'
export type LiquidityRequest = { action: 'spend' | 'receive', amount: number } export type LiquidityRequest = { action: 'spend' | 'receive', amount: number }
export type nostrCallback<T> = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void } export type nostrCallback<T> = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void }
@ -17,16 +19,18 @@ export class LiquidityProvider {
myPub: string = "" myPub: string = ""
log = getLogger({ component: 'liquidityProvider' }) log = getLogger({ component: 'liquidityProvider' })
nostrSend: NostrSend | null = null nostrSend: NostrSend | null = null
ready = false configured = false
pubDestination: string pubDestination: string
latestMaxWithdrawable: number | null = null ready: boolean
latestBalance: number | null = null
invoicePaidCb: InvoicePaidCb invoicePaidCb: InvoicePaidCb
connecting = false connecting = false
readyInterval: NodeJS.Timeout configuredInterval: NodeJS.Timeout
queue: ((state: 'ready') => void)[] = [] queue: ((state: 'ready') => void)[] = []
utils: Utils
pendingPayments: Record<string, number> = {}
// make the sub process accept client // make the sub process accept client
constructor(pubDestination: string, invoicePaidCb: InvoicePaidCb) { constructor(pubDestination: string, utils: Utils, invoicePaidCb: InvoicePaidCb) {
this.utils = utils
if (!pubDestination) { if (!pubDestination) {
this.log("No pub provider to liquidity provider, will not be initialized") this.log("No pub provider to liquidity provider, will not be initialized")
return return
@ -39,9 +43,9 @@ export class LiquidityProvider {
retrieveNostrUserAuth: async () => this.myPub, retrieveNostrUserAuth: async () => this.myPub,
}, this.clientSend, this.clientSub) }, this.clientSend, this.clientSub)
this.readyInterval = setInterval(() => { this.configuredInterval = setInterval(() => {
if (this.ready) { if (this.configured) {
clearInterval(this.readyInterval) clearInterval(this.configuredInterval)
this.Connect() this.Connect()
} }
}, 1000) }, 1000)
@ -51,11 +55,15 @@ export class LiquidityProvider {
return this.pubDestination return this.pubDestination
} }
IsReady = () => {
return this.ready
}
AwaitProviderReady = async (): Promise<'inactive' | 'ready'> => { AwaitProviderReady = async (): Promise<'inactive' | 'ready'> => {
if (!this.pubDestination) { if (!this.pubDestination) {
return 'inactive' return 'inactive'
} }
if (this.latestMaxWithdrawable !== null) { if (this.ready) {
return 'ready' return 'ready'
} }
return new Promise<'ready'>(res => { return new Promise<'ready'>(res => {
@ -64,16 +72,17 @@ export class LiquidityProvider {
} }
Stop = () => { Stop = () => {
clearInterval(this.readyInterval) clearInterval(this.configuredInterval)
} }
Connect = async () => { Connect = async () => {
await new Promise(res => setTimeout(res, 2000)) await new Promise(res => setTimeout(res, 2000))
this.log("ready") this.log("ready")
await this.CheckUserState() const res = await this.GetUserState()
if (this.latestMaxWithdrawable === null) { if (res.status === 'ERROR') {
return return
} }
this.ready = true
this.queue.forEach(q => q('ready')) this.queue.forEach(q => q('ready'))
this.log("subbing to user operations") this.log("subbing to user operations")
this.client.GetLiveUserOperations(res => { this.client.GetLiveUserOperations(res => {
@ -82,94 +91,117 @@ export class LiquidityProvider {
this.log("error getting user operations", res.reason) this.log("error getting user operations", res.reason)
return return
} }
this.log("got user operation", res.operation) //this.log("got user operation", res.operation)
if (res.operation.type === Types.UserOperationType.INCOMING_INVOICE) { if (res.operation.type === Types.UserOperationType.INCOMING_INVOICE) {
this.log("invoice was paid", res.operation.identifier) this.invoicePaidCb(res.operation.identifier, res.operation.amount, 'provider')
this.invoicePaidCb(res.operation.identifier, res.operation.amount, false)
} }
}) })
} }
GetLatestMaxWithdrawable = async (fetch = false) => { GetUserState = async () => {
if (!this.pubDestination) {
return 0
}
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.pubDestination) {
return 0
}
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() const res = await this.client.GetUserInfo()
if (res.status === 'ERROR') { if (res.status === 'ERROR') {
this.log("error getting user info", res) this.log("error getting user info", res)
return return res
} }
this.latestMaxWithdrawable = res.max_withdrawable this.utils.stateBundler.AddBalancePoint('providerBalance', res.balance)
this.latestBalance = res.balance this.utils.stateBundler.AddBalancePoint('providerMaxWithdrawable', res.max_withdrawable)
this.log("latest provider balance:", res.balance, "latest max withdrawable:", res.max_withdrawable)
return res return res
} }
CanProviderHandle = (req: LiquidityRequest) => { GetLatestMaxWithdrawable = async () => {
if (this.latestMaxWithdrawable === null) { if (!this.ready) {
return 0
}
const res = await this.GetUserState()
if (res.status === 'ERROR') {
this.log("error getting user info", res.reason)
return 0
}
return res.max_withdrawable
}
GetLatestBalance = async () => {
if (!this.ready) {
return 0
}
const res = await this.GetUserState()
if (res.status === 'ERROR') {
this.log("error getting user info", res.reason)
return 0
}
return res.balance
}
GetPendingBalance = async () => {
return Object.values(this.pendingPayments).reduce((a, b) => a + b, 0)
}
CalculateExpectedFeeLimit = (amount: number, info: Types.UserInfo) => {
const serviceFeeRate = info.service_fee_bps / 10000
const serviceFee = Math.ceil(serviceFeeRate * amount)
const networkMaxFeeRate = info.network_max_fee_bps / 10000
const networkFeeLimit = Math.ceil(amount * networkMaxFeeRate + info.network_max_fee_fixed)
return serviceFee + networkFeeLimit
}
CanProviderHandle = async (req: LiquidityRequest) => {
if (!this.ready) {
return false return false
} }
const maxW = await this.GetLatestMaxWithdrawable()
if (req.action === 'spend') { if (req.action === 'spend') {
return this.latestMaxWithdrawable > req.amount return maxW > req.amount
} }
return true return true
} }
AddInvoice = async (amount: number, memo: string) => { AddInvoice = async (amount: number, memo: string, from: 'user' | 'system') => {
if (this.latestMaxWithdrawable === null) { try {
throw new Error("liquidity provider is not ready yet") if (!this.ready) {
throw new Error("liquidity provider is not ready yet")
}
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.utils.stateBundler.AddTxPoint('addedInvoice', amount, { used: 'provider', from })
return res.invoice
} catch (err) {
this.utils.stateBundler.AddTxPointFailed('addedInvoice', amount, { used: 'provider', from })
throw err
} }
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) => { PayInvoice = async (invoice: string, decodedAmount: number, from: 'user' | 'system') => {
if (this.latestMaxWithdrawable === null) { try {
throw new Error("liquidity provider is not ready yet") if (!this.ready) {
throw new Error("liquidity provider is not ready yet")
}
const userInfo = await this.GetUserState()
if (userInfo.status === 'ERROR') {
throw new Error(userInfo.reason)
}
this.pendingPayments[invoice] = decodedAmount + this.CalculateExpectedFeeLimit(decodedAmount, userInfo)
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)
}
delete this.pendingPayments[invoice]
this.utils.stateBundler.AddTxPoint('paidAnInvoice', decodedAmount, { used: 'provider', from, timeDiscount: true })
return res
} catch (err) {
delete this.pendingPayments[invoice]
this.utils.stateBundler.AddTxPointFailed('paidAnInvoice', decodedAmount, { used: 'provider', from })
throw err
} }
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
} }
GetOperations = async () => { GetOperations = async () => {
if (this.latestMaxWithdrawable === null) { if (!this.ready) {
throw new Error("liquidity provider is not ready yet") throw new Error("liquidity provider is not ready yet")
} }
const res = await this.client.GetUserOperations({ const res = await this.client.GetUserOperations({
@ -188,7 +220,7 @@ export class LiquidityProvider {
this.log("setting nostr info") this.log("setting nostr info")
this.clientId = clientId this.clientId = clientId
this.myPub = myPub this.myPub = myPub
this.setSetIfReady() this.setSetIfConfigured()
} }
@ -196,13 +228,13 @@ export class LiquidityProvider {
attachNostrSend(f: NostrSend) { attachNostrSend(f: NostrSend) {
this.log("attaching nostrSend action") this.log("attaching nostrSend action")
this.nostrSend = f this.nostrSend = f
this.setSetIfReady() this.setSetIfConfigured()
} }
setSetIfReady = () => { setSetIfConfigured = () => {
if (this.nostrSend && !!this.pubDestination && !!this.clientId && !!this.myPub) { if (this.nostrSend && !!this.pubDestination && !!this.clientId && !!this.myPub) {
this.ready = true this.configured = true
this.log("ready to send to ", this.pubDestination) this.log("configured to send to ", this.pubDestination)
} }
} }
@ -213,10 +245,11 @@ export class LiquidityProvider {
} }
if (this.clientCbs[res.requestId]) { if (this.clientCbs[res.requestId]) {
const cb = this.clientCbs[res.requestId] const cb = this.clientCbs[res.requestId]
cb.f(res) cb.f(res)
if (cb.type === 'single') { if (cb.type === 'single') {
delete this.clientCbs[res.requestId] delete this.clientCbs[res.requestId]
this.log(this.getSingleSubs(), "single subs left") this.utils.stateBundler.AddMaxPoint('maxProviderRespTime', Date.now() - cb.startedAtMillis)
} }
return true return true
} }
@ -224,7 +257,7 @@ export class LiquidityProvider {
} }
clientSend = (to: string, message: NostrRequest): Promise<any> => { clientSend = (to: string, message: NostrRequest): Promise<any> => {
if (!this.ready || !this.nostrSend) { if (!this.configured || !this.nostrSend) {
throw new Error("liquidity provider not initialized") throw new Error("liquidity provider not initialized")
} }
if (!message.requestId) { if (!message.requestId) {
@ -242,7 +275,7 @@ export class LiquidityProvider {
//this.nostrSend(this.relays, to, JSON.stringify(message), this.settings) //this.nostrSend(this.relays, to, JSON.stringify(message), this.settings)
this.log("subbing to single send", reqId, message.rpcName || 'no rpc name') // this.log("subbing to single send", reqId, message.rpcName || 'no rpc name')
return new Promise(res => { return new Promise(res => {
this.clientCbs[reqId] = { this.clientCbs[reqId] = {
startedAtMillis: Date.now(), startedAtMillis: Date.now(),
@ -253,7 +286,7 @@ export class LiquidityProvider {
} }
clientSub = (to: string, message: NostrRequest, cb: (res: any) => void): void => { clientSub = (to: string, message: NostrRequest, cb: (res: any) => void): void => {
if (!this.ready || !this.nostrSend) { if (!this.configured || !this.nostrSend) {
throw new Error("liquidity provider not initialized") throw new Error("liquidity provider not initialized")
} }
if (!message.requestId) { if (!message.requestId) {

View file

@ -15,8 +15,9 @@ import { Event, verifiedSymbol, verifySignature } from '../nostr/tools/event.js'
import { AddressReceivingTransaction } from '../storage/entity/AddressReceivingTransaction.js' import { AddressReceivingTransaction } from '../storage/entity/AddressReceivingTransaction.js'
import { UserTransactionPayment } from '../storage/entity/UserTransactionPayment.js' import { UserTransactionPayment } from '../storage/entity/UserTransactionPayment.js'
import { Watchdog } from './watchdog.js' import { Watchdog } from './watchdog.js'
import { LiquidityProvider } from '../lnd/liquidityProvider.js' import { LiquidityProvider } from './liquidityProvider.js'
import { LiquidityManager } from './liquidityManager.js' import { LiquidityManager } from './liquidityManager.js'
import { Utils } from '../helpers/utilsWrapper.js'
interface UserOperationInfo { interface UserOperationInfo {
serial_id: number serial_id: number
paid_amount: number paid_amount: number
@ -49,12 +50,14 @@ export default class {
log = getLogger({ component: "PaymentManager" }) log = getLogger({ component: "PaymentManager" })
watchDog: Watchdog watchDog: Watchdog
liquidityManager: LiquidityManager liquidityManager: LiquidityManager
constructor(storage: Storage, lnd: LND, settings: MainSettings, liquidityManager: LiquidityManager, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) { utils: Utils
constructor(storage: Storage, lnd: LND, settings: MainSettings, liquidityManager: LiquidityManager, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
this.storage = storage this.storage = storage
this.settings = settings this.settings = settings
this.lnd = lnd this.lnd = lnd
this.liquidityManager = liquidityManager this.liquidityManager = liquidityManager
this.watchDog = new Watchdog(settings.watchDogSettings, this.liquidityManager, lnd, storage) this.utils = utils
this.watchDog = new Watchdog(settings.watchDogSettings, this.liquidityManager, this.lnd, this.storage, this.utils, this.liquidityManager.rugPullTracker)
this.addressPaidCb = addressPaidCb this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb this.invoicePaidCb = invoicePaidCb
} }
@ -113,7 +116,7 @@ export default class {
if (existingAddress) { if (existingAddress) {
return { address: existingAddress.address } return { address: existingAddress.address }
} }
const res = await this.lnd.NewAddress(req.addressType) const res = await this.lnd.NewAddress(req.addressType, { useProvider: false, from: 'user' })
const userAddress = await this.storage.paymentStorage.AddUserAddress(user, res.address, { linkedApplication: app }) const userAddress = await this.storage.paymentStorage.AddUserAddress(user, res.address, { linkedApplication: app })
this.storage.eventsLog.LogEvent({ type: 'new_address', userId: user.user_id, appUserId: "", appId: app.app_id, balance: user.balance_sats, data: res.address, amount: 0 }) this.storage.eventsLog.LogEvent({ type: 'new_address', userId: user.user_id, appUserId: "", appId: app.app_id, balance: user.balance_sats, data: res.address, amount: 0 })
return { address: userAddress.address } return { address: userAddress.address }
@ -125,7 +128,7 @@ export default class {
throw new Error("user is banned, cannot generate invoice") throw new Error("user is banned, cannot generate invoice")
} }
const use = await this.liquidityManager.beforeInvoiceCreation(req.amountSats) const use = await this.liquidityManager.beforeInvoiceCreation(req.amountSats)
const res = await this.lnd.NewInvoice(req.amountSats, req.memo, options.expiry, use === 'provider') const res = await this.lnd.NewInvoice(req.amountSats, req.memo, options.expiry, { useProvider: use === 'provider', from: 'user' })
const userInvoice = await this.storage.paymentStorage.AddUserInvoice(user, res.payRequest, options, res.providerDst) const userInvoice = await this.storage.paymentStorage.AddUserInvoice(user, res.payRequest, options, res.providerDst)
const appId = options.linkedApplication ? options.linkedApplication.app_id : "" const appId = options.linkedApplication ? options.linkedApplication.app_id : ""
this.storage.eventsLog.LogEvent({ type: 'new_invoice', userId: user.user_id, appUserId: "", appId, balance: user.balance_sats, data: userInvoice.invoice, amount: req.amountSats }) this.storage.eventsLog.LogEvent({ type: 'new_invoice', userId: user.user_id, appUserId: "", appId, balance: user.balance_sats, data: userInvoice.invoice, amount: req.amountSats })
@ -151,7 +154,6 @@ export default class {
} }
async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application): Promise<Types.PayInvoiceResponse> { async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application): Promise<Types.PayInvoiceResponse> {
this.log("paying invoice", req.invoice, "for user", userId, "with amount", req.amount)
await this.watchDog.PaymentRequested() await this.watchDog.PaymentRequested()
const maybeBanned = await this.storage.userStorage.GetUser(userId) const maybeBanned = await this.storage.userStorage.GetUser(userId)
if (maybeBanned.locked) { if (maybeBanned.locked) {
@ -201,14 +203,12 @@ export default class {
} }
const { amountForLnd, payAmount, serviceFee } = amounts const { amountForLnd, payAmount, serviceFee } = amounts
const totalAmountToDecrement = payAmount + serviceFee const totalAmountToDecrement = payAmount + serviceFee
this.log("paying external invoice", invoice)
const routingFeeLimit = this.lnd.GetFeeLimitAmount(payAmount) const routingFeeLimit = this.lnd.GetFeeLimitAmount(payAmount)
await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, invoice) await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, invoice)
const pendingPayment = await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, payAmount, linkedApplication) const pendingPayment = await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, payAmount, linkedApplication)
const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount) const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount)
try { try {
const payment = await this.lnd.PayInvoice(invoice, amountForLnd, routingFeeLimit, use === 'provider') const payment = await this.lnd.PayInvoice(invoice, amountForLnd, routingFeeLimit, payAmount, { useProvider: use === 'provider', from: 'user' })
if (routingFeeLimit - payment.feeSat > 0) { if (routingFeeLimit - payment.feeSat > 0) {
this.log("refund routing fee", routingFeeLimit, payment.feeSat, "sats") this.log("refund routing fee", routingFeeLimit, payment.feeSat, "sats")
await this.storage.userStorage.IncrementUserBalance(userId, routingFeeLimit - payment.feeSat, "routing_fee_refund:" + invoice) await this.storage.userStorage.IncrementUserBalance(userId, routingFeeLimit - payment.feeSat, "routing_fee_refund:" + invoice)
@ -225,16 +225,24 @@ export default class {
} }
async PayInternalInvoice(userId: string, internalInvoice: UserReceivingInvoice, amounts: { payAmount: number, serviceFee: number }, linkedApplication: Application) { async PayInternalInvoice(userId: string, internalInvoice: UserReceivingInvoice, amounts: { payAmount: number, serviceFee: number }, linkedApplication: Application) {
this.log("paying internal invoice", internalInvoice.invoice)
if (internalInvoice.paid_at_unix > 0) { if (internalInvoice.paid_at_unix > 0) {
throw new Error("this invoice was already paid") throw new Error("this invoice was already paid")
} }
const { payAmount, serviceFee } = amounts const { payAmount, serviceFee } = amounts
const totalAmountToDecrement = payAmount + serviceFee const totalAmountToDecrement = payAmount + serviceFee
await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement, internalInvoice.invoice) await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement, internalInvoice.invoice)
this.invoicePaidCb(internalInvoice.invoice, payAmount, true) try {
const newPayment = await this.storage.paymentStorage.AddInternalPayment(userId, internalInvoice.invoice, payAmount, serviceFee, linkedApplication) await this.invoicePaidCb(internalInvoice.invoice, payAmount, 'internal')
return { preimage: "", amtPaid: payAmount, networkFee: 0, serialId: newPayment.serial_id } const newPayment = await this.storage.paymentStorage.AddInternalPayment(userId, internalInvoice.invoice, payAmount, serviceFee, linkedApplication)
this.utils.stateBundler.AddTxPoint('paidAnInvoice', payAmount, { used: 'internal', from: 'user' })
return { preimage: "", amtPaid: payAmount, networkFee: 0, serialId: newPayment.serial_id }
} catch (err) {
await this.storage.userStorage.IncrementUserBalance(userId, totalAmountToDecrement, "internal_payment_refund:" + internalInvoice.invoice)
this.utils.stateBundler.AddTxPointFailed('paidAnInvoice', payAmount, { used: 'internal', from: 'user' })
throw err
}
} }
@ -262,7 +270,7 @@ export default class {
// WARNING, before re-enabling this, make sure to add the tx_hash to the DecrementUserBalance "reason"!! // WARNING, before re-enabling this, make sure to add the tx_hash to the DecrementUserBalance "reason"!!
this.storage.userStorage.DecrementUserBalance(ctx.user_id, total + serviceFee, req.address) this.storage.userStorage.DecrementUserBalance(ctx.user_id, total + serviceFee, req.address)
try { try {
const payment = await this.lnd.PayAddress(req.address, req.amoutSats, req.satsPerVByte) const payment = await this.lnd.PayAddress(req.address, req.amoutSats, req.satsPerVByte, "", { useProvider: false, from: 'user' })
txId = payment.txid txId = payment.txid
} catch (err) { } catch (err) {
// WARNING, before re-enabling this, make sure to add the tx_hash to the IncrementUserBalance "reason"!! // WARNING, before re-enabling this, make sure to add the tx_hash to the IncrementUserBalance "reason"!!
@ -274,7 +282,7 @@ export default class {
txId = crypto.randomBytes(32).toString("hex") txId = crypto.randomBytes(32).toString("hex")
const addressData = `${req.address}:${txId}` const addressData = `${req.address}:${txId}`
await this.storage.userStorage.DecrementUserBalance(ctx.user_id, req.amoutSats + serviceFee, addressData) await this.storage.userStorage.DecrementUserBalance(ctx.user_id, req.amoutSats + serviceFee, addressData)
this.addressPaidCb({ hash: txId, index: 0 }, req.address, req.amoutSats, true) this.addressPaidCb({ hash: txId, index: 0 }, req.address, req.amoutSats, 'internal')
} }
if (isAppUserPayment && serviceFee > 0) { if (isAppUserPayment && serviceFee > 0) {
@ -360,11 +368,9 @@ export default class {
if (this.isDefaultServiceUrl()) { if (this.isDefaultServiceUrl()) {
throw new Error("Lnurl not enabled. Make sure to set SERVICE_URL env variable") throw new Error("Lnurl not enabled. Make sure to set SERVICE_URL env variable")
} }
getLogger({})("getting lnurl pay link")
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const key = await this.storage.paymentStorage.AddUserEphemeralKey(ctx.user_id, 'pay', app) const key = await this.storage.paymentStorage.AddUserEphemeralKey(ctx.user_id, 'pay', app)
const lnurl = this.encodeLnurl(this.lnurlPayUrl(key.key)) const lnurl = this.encodeLnurl(this.lnurlPayUrl(key.key))
getLogger({})("got lnurl pay link: ", lnurl)
return { return {
lnurl, lnurl,
k1: key.key k1: key.key

View file

@ -0,0 +1,80 @@
import FunctionQueue from "../helpers/functionQueue.js";
import { getLogger } from "../helpers/logger.js";
import { Utils } from "../helpers/utilsWrapper.js";
import { LiquidityProvider } from "./liquidityProvider.js";
import { TrackedProvider } from "../storage/entity/TrackedProvider.js";
import Storage from "../storage/index.js";
export class RugPullTracker {
liquidProvider: LiquidityProvider
storage: Storage
log = getLogger({ component: "rugPullTracker" })
rugPulled = false
constructor(storage: Storage, liquidProvider: LiquidityProvider) {
this.liquidProvider = liquidProvider
this.storage = storage
}
HasProviderRugPulled = () => {
return this.rugPulled
}
CheckProviderBalance = async (): Promise<{ balance: number, prevBalance?: number }> => {
const pubDst = this.liquidProvider.GetProviderDestination()
if (!pubDst) {
return { balance: 0 }
}
const fetchedBalance = await this.liquidProvider.GetLatestBalance()
const pendingBalance = await this.liquidProvider.GetPendingBalance()
const providerTracker = await this.storage.liquidityStorage.GetTrackedProvider('lnPub', pubDst)
const balance = this.liquidProvider.IsReady() ? fetchedBalance : providerTracker?.latest_balance || 0
const trackedBalance = balance + pendingBalance
if (!providerTracker) {
this.log("starting to track provider", this.liquidProvider.GetProviderDestination())
await this.storage.liquidityStorage.CreateTrackedProvider('lnPub', pubDst, trackedBalance)
return { balance: trackedBalance }
}
if (providerTracker.latest_balance !== trackedBalance) {
return this.handleBalanceMismatch(pubDst, trackedBalance, providerTracker)
}
this.rugPulled = false
return { balance: trackedBalance }
}
handleBalanceMismatch = async (pubDst: string, trackedBalance: number, providerTracker: TrackedProvider) => {
const diff = trackedBalance - providerTracker.latest_balance
if (diff < 0) {
getLogger({ component: 'rugPull' })(pubDst, "provider balance changed from", providerTracker.latest_balance, "to", trackedBalance, "losing", diff)
this.rugPulled = true
if (providerTracker.latest_distruption_at_unix === 0) {
await this.storage.liquidityStorage.UpdateTrackedProviderDisruption('lnPub', pubDst, Math.floor(Date.now() / 1000))
}
} else {
getLogger({ component: 'rugPush' })(pubDst, "provider balance changed from", providerTracker.latest_balance, "to", trackedBalance, "gaining", diff)
this.rugPulled = false
if (providerTracker.latest_distruption_at_unix !== 0) {
await this.storage.liquidityStorage.UpdateTrackedProviderDisruption('lnPub', pubDst, 0)
}
}
return { balance: trackedBalance, prevBalance: providerTracker.latest_balance }
}
updateDisruption = async (pubDst: string, trackedBalance: number, providerTracker: TrackedProvider, diff: number) => {
if (diff < 0) {
if (providerTracker.latest_distruption_at_unix === 0) {
await this.storage.liquidityStorage.UpdateTrackedProviderDisruption('lnPub', pubDst, Math.floor(Date.now() / 1000))
getLogger({ component: 'rugPull' })("detected rugpull from: ", pubDst, "provider balance changed from", providerTracker.latest_balance, "to", trackedBalance, "losing", diff)
} else {
getLogger({ component: 'rugPull' })("ongoing rugpull from: ", pubDst, "provider balance changed from", providerTracker.latest_balance, "to", trackedBalance, "losing", diff)
}
} else {
if (providerTracker.latest_distruption_at_unix !== 0) {
await this.storage.liquidityStorage.UpdateTrackedProviderDisruption('lnPub', pubDst, 0)
getLogger({ component: 'rugPull' })("rugpull from: ", pubDst, "cleared after: ", (Date.now() / 1000) - providerTracker.latest_distruption_at_unix, "seconds")
}
if (diff > 0) {
this.log("detected excees from: ", pubDst, "provider balance changed from", providerTracker.latest_balance, "to", trackedBalance, "gaining", diff)
}
}
}
}

View file

@ -21,6 +21,7 @@ export type MainSettings = {
incomingAppUserInvoiceFee: number incomingAppUserInvoiceFee: number
outgoingAppInvoiceFee: number outgoingAppInvoiceFee: number
outgoingAppUserInvoiceFee: number outgoingAppUserInvoiceFee: number
outgoingAppUserInvoiceFeeBps: number
userToUserFee: number userToUserFee: number
appToUserFee: number appToUserFee: number
serviceUrl: string serviceUrl: string
@ -39,6 +40,7 @@ export type BitcoinCoreSettings = {
export type TestSettings = MainSettings & { lndSettings: { otherNode: NodeSettings, thirdNode: NodeSettings, fourthNode: NodeSettings }, bitcoinCoreSettings: BitcoinCoreSettings } export type TestSettings = MainSettings & { lndSettings: { otherNode: NodeSettings, thirdNode: NodeSettings, fourthNode: NodeSettings }, bitcoinCoreSettings: BitcoinCoreSettings }
export const LoadMainSettingsFromEnv = (): MainSettings => { export const LoadMainSettingsFromEnv = (): MainSettings => {
const storageSettings = LoadStorageSettingsFromEnv() const storageSettings = LoadStorageSettingsFromEnv()
const outgoingAppUserInvoiceFeeBps = EnvCanBeInteger("OUTGOING_INVOICE_FEE_USER_BPS", 0)
return { return {
watchDogSettings: LoadWatchdogSettingsFromEnv(), watchDogSettings: LoadWatchdogSettingsFromEnv(),
lndSettings: LoadLndSettingsFromEnv(), lndSettings: LoadLndSettingsFromEnv(),
@ -52,7 +54,8 @@ export const LoadMainSettingsFromEnv = (): MainSettings => {
incomingAppInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_ROOT_BPS", 0) / 10000, incomingAppInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_ROOT_BPS", 0) / 10000,
outgoingAppInvoiceFee: EnvCanBeInteger("OUTGOING_INVOICE_FEE_ROOT_BPS", 60) / 10000, outgoingAppInvoiceFee: EnvCanBeInteger("OUTGOING_INVOICE_FEE_ROOT_BPS", 60) / 10000,
incomingAppUserInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_USER_BPS", 0) / 10000, incomingAppUserInvoiceFee: EnvCanBeInteger("INCOMING_INVOICE_FEE_USER_BPS", 0) / 10000,
outgoingAppUserInvoiceFee: EnvCanBeInteger("OUTGOING_INVOICE_FEE_USER_BPS", 0) / 10000, outgoingAppUserInvoiceFeeBps,
outgoingAppUserInvoiceFee: outgoingAppUserInvoiceFeeBps / 10000,
userToUserFee: EnvCanBeInteger("TX_FEE_INTERNAL_USER_BPS", 0) / 10000, userToUserFee: EnvCanBeInteger("TX_FEE_INTERNAL_USER_BPS", 0) / 10000,
appToUserFee: EnvCanBeInteger("TX_FEE_INTERNAL_ROOT_BPS", 0) / 10000, appToUserFee: EnvCanBeInteger("TX_FEE_INTERNAL_ROOT_BPS", 0) / 10000,
serviceUrl: process.env.SERVICE_URL || `http://localhost:${EnvCanBeInteger("PORT", 1776)}`, serviceUrl: process.env.SERVICE_URL || `http://localhost:${EnvCanBeInteger("PORT", 1776)}`,

View file

@ -60,7 +60,7 @@ export class Unlocker {
await unlocker.unlockWallet({ walletPassword, recoveryWindow: 0, statelessInit: false, channelBackups: undefined }, DeadLineMetadata()) await unlocker.unlockWallet({ walletPassword, recoveryWindow: 0, statelessInit: false, channelBackups: undefined }, DeadLineMetadata())
const infoAfter = await this.GetLndInfo(ln) const infoAfter = await this.GetLndInfo(ln)
if (!infoAfter.ok) { if (!infoAfter.ok) {
throw new Error("failed to init lnd wallet " + infoAfter.failure) throw new Error("failed to unlock lnd wallet " + infoAfter.failure)
} }
this.log("unlocked wallet with pub:", infoAfter.pub) this.log("unlocked wallet with pub:", infoAfter.pub)
return { ln, pub: infoAfter.pub } return { ln, pub: infoAfter.pub }
@ -74,8 +74,6 @@ export class Unlocker {
aezeedPassphrase: Buffer.alloc(0), aezeedPassphrase: Buffer.alloc(0),
seedEntropy: entropy seedEntropy: entropy
}, DeadLineMetadata()) }, DeadLineMetadata())
console.log(seedRes.response.cipherSeedMnemonic)
console.log(seedRes.response.encipheredSeed)
this.log("seed created, encrypting and saving...") this.log("seed created, encrypting and saving...")
const { encryptedData } = this.EncryptWalletSeed(seedRes.response.cipherSeedMnemonic) const { encryptedData } = this.EncryptWalletSeed(seedRes.response.cipherSeedMnemonic)
const walletPw = this.GetWalletPassword() const walletPw = this.GetWalletPassword()

View file

@ -1,11 +1,13 @@
import { EnvCanBeInteger } from "../helpers/envParser.js"; import { EnvCanBeInteger } from "../helpers/envParser.js";
import FunctionQueue from "../helpers/functionQueue.js"; import FunctionQueue from "../helpers/functionQueue.js";
import { getLogger } from "../helpers/logger.js"; import { getLogger } from "../helpers/logger.js";
import { LiquidityProvider } from "../lnd/liquidityProvider.js"; import { Utils } from "../helpers/utilsWrapper.js";
import { LiquidityProvider } from "./liquidityProvider.js";
import LND from "../lnd/lnd.js"; import LND from "../lnd/lnd.js";
import { ChannelBalance } from "../lnd/settings.js"; import { ChannelBalance } from "../lnd/settings.js";
import Storage from '../storage/index.js' import Storage from '../storage/index.js'
import { LiquidityManager } from "./liquidityManager.js"; import { LiquidityManager } from "./liquidityManager.js";
import { RugPullTracker } from "./rugPullTracker.js";
export type WatchdogSettings = { export type WatchdogSettings = {
maxDiffSats: number maxDiffSats: number
} }
@ -26,16 +28,21 @@ export class Watchdog {
liquidityManager: LiquidityManager; liquidityManager: LiquidityManager;
settings: WatchdogSettings; settings: WatchdogSettings;
storage: Storage; storage: Storage;
rugPullTracker: RugPullTracker
utils: Utils
latestCheckStart = 0 latestCheckStart = 0
log = getLogger({ component: "watchdog" }) log = getLogger({ component: "watchdog" })
ready = false ready = false
interval: NodeJS.Timer; interval: NodeJS.Timer;
constructor(settings: WatchdogSettings, liquidityManager: LiquidityManager, lnd: LND, storage: Storage) { lndPubKey: string;
constructor(settings: WatchdogSettings, liquidityManager: LiquidityManager, lnd: LND, storage: Storage, utils: Utils, rugPullTracker: RugPullTracker) {
this.lnd = lnd; this.lnd = lnd;
this.settings = settings; this.settings = settings;
this.storage = storage; this.storage = storage;
this.liquidProvider = lnd.liquidProvider this.liquidProvider = lnd.liquidProvider
this.liquidityManager = liquidityManager this.liquidityManager = liquidityManager
this.utils = utils
this.rugPullTracker = rugPullTracker
this.queue = new FunctionQueue("watchdog_queue", () => this.StartCheck()) this.queue = new FunctionQueue("watchdog_queue", () => this.StartCheck())
} }
@ -45,33 +52,22 @@ export class Watchdog {
} }
} }
Start = async () => { Start = async () => {
const result = await Promise.race([
this.liquidProvider.AwaitProviderReady(),
new Promise<'failed'>((res, rej) => {
setTimeout(() => {
this.log("Provider did not become ready in time, starting without it")
res('failed')
}, 3 * 60 * 1000)
})
])
let providerBalance = 0
if (result === 'ready') {
providerBalance = await this.liquidProvider.GetLatestBalance()
}
try { try {
await this.StartWatching(providerBalance) await this.StartWatching()
} catch (err: any) { } catch (err: any) {
this.log("Failed to start watchdog", err.message || err) this.log("Failed to start watchdog", err.message || err)
throw err throw err
} }
} }
StartWatching = async (providerBalance: number) => { StartWatching = async () => {
this.log("Starting watchdog") this.log("Starting watchdog")
this.startedAtUnix = Math.floor(Date.now() / 1000) this.startedAtUnix = Math.floor(Date.now() / 1000)
const info = await this.lnd.GetInfo()
this.lndPubKey = info.identityPubkey
await this.getTracker()
const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance() const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
this.initialLndBalance = await this.getTotalLndBalance(totalUsersBalance, providerBalance) this.utils.stateBundler.AddBalancePoint('usersBalance', totalUsersBalance)
this.initialLndBalance = await this.getAggregatedExternalBalance()
this.initialUsersBalance = totalUsersBalance this.initialUsersBalance = totalUsersBalance
const fwEvents = await this.lnd.GetForwardingHistory(0, this.startedAtUnix) const fwEvents = await this.lnd.GetForwardingHistory(0, this.startedAtUnix)
this.latestIndexOffset = fwEvents.lastOffsetIndex this.latestIndexOffset = fwEvents.lastOffsetIndex
@ -79,7 +75,6 @@ export class Watchdog {
this.interval = setInterval(() => { this.interval = setInterval(() => {
if (this.latestCheckStart + (1000 * 60) < Date.now()) { if (this.latestCheckStart + (1000 * 60) < Date.now()) {
this.log("No balance check was made in the last minute, checking now")
this.PaymentRequested() this.PaymentRequested()
} }
}, 1000 * 60) }, 1000 * 60)
@ -96,49 +91,42 @@ export class Watchdog {
} }
getAggregatedExternalBalance = async () => {
const totalLndBalance = await this.lnd.GetTotalBalace()
getTotalLndBalance = async (usersTotal: number, providerBalance: 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 feesPaidForLiquidity = this.liquidityManager.GetPaidFees() const feesPaidForLiquidity = this.liquidityManager.GetPaidFees()
return Number(walletBalance.confirmedBalance) + totalLightningBalance + providerBalance + feesPaidForLiquidity const pb = await this.rugPullTracker.CheckProviderBalance()
const providerBalance = pb.prevBalance || pb.balance
return totalLndBalance + providerBalance + feesPaidForLiquidity
} }
checkBalanceUpdate = (deltaLnd: number, deltaUsers: number) => { checkBalanceUpdate = async (deltaLnd: number, deltaUsers: number) => {
this.log("LND balance update:", deltaLnd, "sats since app startup") this.utils.stateBundler.AddBalancePoint('deltaLnd', deltaLnd)
this.log("Users balance update:", deltaUsers, "sats since app startup") this.utils.stateBundler.AddBalancePoint('deltaUsers', deltaUsers)
const result = this.checkDeltas(deltaLnd, deltaUsers) const result = this.checkDeltas(deltaLnd, deltaUsers)
switch (result.type) { switch (result.type) {
case 'mismatch': case 'mismatch':
if (deltaLnd < 0) { 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) { if (result.absoluteDiff > this.settings.maxDiffSats) {
this.log("Difference is too big for an update, locking outgoing operations") await this.updateDisruption(true, result.absoluteDiff)
return true return true
} }
} else { } 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") this.updateDisruption(false, result.absoluteDiff)
return false return false
} }
break break
case 'negative': case 'negative':
if (Math.abs(deltaLnd) > Math.abs(deltaUsers)) { 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) { if (result.absoluteDiff > this.settings.maxDiffSats) {
this.log("Difference is too big for an update, locking outgoing operations") await this.updateDisruption(true, result.absoluteDiff)
return true return true
} }
} else if (deltaLnd === deltaUsers) { } else if (deltaLnd === deltaUsers) {
this.log("LND and users balance went both DOWN consistently") await this.updateDisruption(false, 0)
return false return false
} else { } 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") await this.updateDisruption(false, result.absoluteDiff)
return false return false
} }
break break
@ -146,29 +134,49 @@ export class Watchdog {
if (deltaLnd < deltaUsers) { if (deltaLnd < deltaUsers) {
this.log("WARNING! LND balance increased less than users balance with a difference of", result.absoluteDiff, "sats") this.log("WARNING! LND balance increased less than users balance with a difference of", result.absoluteDiff, "sats")
if (result.absoluteDiff > this.settings.maxDiffSats) { if (result.absoluteDiff > this.settings.maxDiffSats) {
this.log("Difference is too big for an update, locking outgoing operations") await this.updateDisruption(true, result.absoluteDiff)
return true return true
} }
} else if (deltaLnd === deltaUsers) { } else if (deltaLnd === deltaUsers) {
this.log("LND and users balance went both UP consistently") await this.updateDisruption(false, 0)
return false return false
} else { } 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") await this.updateDisruption(false, result.absoluteDiff)
return false return false
} }
} }
return false return false
} }
updateDisruption = async (isDisrupted: boolean, absoluteDiff: number) => {
const tracker = await this.getTracker()
if (isDisrupted) {
if (tracker.latest_distruption_at_unix === 0) {
await this.storage.liquidityStorage.UpdateTrackedProviderDisruption('lnd', this.lndPubKey, Math.floor(Date.now() / 1000))
getLogger({ component: 'bark' })("detected lnd loss of", absoluteDiff, "sats,", absoluteDiff - this.settings.maxDiffSats, "above the max allowed")
} else {
getLogger({ component: 'bark' })("ongoing lnd loss of", absoluteDiff, "sats,", absoluteDiff - this.settings.maxDiffSats, "above the max allowed")
}
} else {
if (tracker.latest_distruption_at_unix !== 0) {
await this.storage.liquidityStorage.UpdateTrackedProviderDisruption('lnd', this.lndPubKey, 0)
getLogger({ component: 'bark' })("loss cleared after: ", (Date.now() / 1000) - tracker.latest_distruption_at_unix, "seconds")
} else if (absoluteDiff > 0) {
this.log("lnd balance increased more than users balance by", absoluteDiff)
}
}
}
StartCheck = async () => { StartCheck = async () => {
this.latestCheckStart = Date.now() this.latestCheckStart = Date.now()
await this.updateAccumulatedHtlcFees() await this.updateAccumulatedHtlcFees()
const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance() const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
const providerBalance = await this.liquidProvider.GetLatestBalance() this.utils.stateBundler.AddBalancePoint('usersBalance', totalUsersBalance)
const totalLndBalance = await this.getTotalLndBalance(totalUsersBalance, providerBalance) const totalLndBalance = await this.getAggregatedExternalBalance()
this.utils.stateBundler.AddBalancePoint('accumulatedHtlcFees', this.accumulatedHtlcFees)
const deltaLnd = totalLndBalance - (this.initialLndBalance + this.accumulatedHtlcFees) const deltaLnd = totalLndBalance - (this.initialLndBalance + this.accumulatedHtlcFees)
const deltaUsers = totalUsersBalance - this.initialUsersBalance const deltaUsers = totalUsersBalance - this.initialUsersBalance
const deny = this.checkBalanceUpdate(deltaLnd, deltaUsers) const deny = await this.checkBalanceUpdate(deltaLnd, deltaUsers)
if (deny) { if (deny) {
this.log("Balance mismatch detected in absolute update, locking outgoing operations") this.log("Balance mismatch detected in absolute update, locking outgoing operations")
this.lnd.LockOutgoingOperations() this.lnd.LockOutgoingOperations()
@ -178,7 +186,6 @@ export class Watchdog {
} }
PaymentRequested = async () => { PaymentRequested = async () => {
this.log("Payment requested, checking balance")
if (!this.ready) { if (!this.ready) {
throw new Error("Watchdog not ready") throw new Error("Watchdog not ready")
} }
@ -206,5 +213,13 @@ export class Watchdog {
} }
} }
} }
getTracker = async () => {
const tracker = await this.storage.liquidityStorage.GetTrackedProvider('lnd', this.lndPubKey)
if (!tracker) {
return this.storage.liquidityStorage.CreateTrackedProvider('lnd', this.lndPubKey, 0)
}
return tracker
}
} }
type DeltaCheckResult = { type: 'negative' | 'positive', absoluteDiff: number, relativeDiff: number } | { type: 'mismatch', absoluteDiff: number } type DeltaCheckResult = { type: 'negative' | 'positive', absoluteDiff: number, relativeDiff: number } | { type: 'mismatch', absoluteDiff: number }

View file

@ -18,10 +18,9 @@ export default class HtlcTracker {
} }
log = getLogger({ component: 'htlcTracker' }) log = getLogger({ component: 'htlcTracker' })
onHtlcEvent = async (htlc: HtlcEvent) => { onHtlcEvent = async (htlc: HtlcEvent) => {
getLogger({ component: 'debugHtlcs' })(htlc) //getLogger({ component: 'debugHtlcs' })(htlc)
const htlcEvent = htlc.event const htlcEvent = htlc.event
if (htlcEvent.oneofKind === 'subscribedEvent') { if (htlcEvent.oneofKind === 'subscribedEvent') {
this.log("htlc subscribed")
return return
} }
const outgoingHtlcId = Number(htlc.outgoingHtlcId) const outgoingHtlcId = Number(htlc.outgoingHtlcId)
@ -45,12 +44,11 @@ export default class HtlcTracker {
case 'settleEvent': case 'settleEvent':
return this.handleSuccess(info) return this.handleSuccess(info)
default: default:
this.log("unknown htlc event type") //this.log("unknown htlc event type")
} }
} }
handleForward = (fwe: ForwardEvent, { eventType, outgoingHtlcId, incomingHtlcId }: EventInfo) => { handleForward = (fwe: ForwardEvent, { eventType, outgoingHtlcId, incomingHtlcId }: EventInfo) => {
this.log("new forward event, currently tracked htlcs: (s,r,f)", this.pendingSendHtlcs.size, this.pendingReceiveHtlcs.size, this.pendingForwardHtlcs.size)
const { info } = fwe const { info } = fwe
const incomingAmtMsat = info ? Number(info.incomingAmtMsat) : 0 const incomingAmtMsat = info ? Number(info.incomingAmtMsat) : 0
const outgoingAmtMsat = info ? Number(info.outgoingAmtMsat) : 0 const outgoingAmtMsat = info ? Number(info.outgoingAmtMsat) : 0
@ -60,8 +58,6 @@ export default class HtlcTracker {
this.pendingReceiveHtlcs.set(incomingHtlcId, incomingAmtMsat - outgoingAmtMsat) this.pendingReceiveHtlcs.set(incomingHtlcId, incomingAmtMsat - outgoingAmtMsat)
} else if (eventType === HtlcEvent_EventType.FORWARD) { } else if (eventType === HtlcEvent_EventType.FORWARD) {
this.pendingForwardHtlcs.set(outgoingHtlcId, outgoingAmtMsat - incomingAmtMsat) this.pendingForwardHtlcs.set(outgoingHtlcId, outgoingAmtMsat - incomingAmtMsat)
} else {
this.log("unknown htlc event type for forward event")
} }
} }
@ -90,7 +86,6 @@ export default class HtlcTracker {
return this.incrementReceiveFailures(incomingChannelId) return this.incrementReceiveFailures(incomingChannelId)
} }
} }
this.log("unknown htlc event type for failure event", eventType)
} }
handleSuccess = ({ eventType, outgoingHtlcId, incomingHtlcId }: EventInfo) => { handleSuccess = ({ eventType, outgoingHtlcId, incomingHtlcId }: EventInfo) => {
@ -104,8 +99,6 @@ export default class HtlcTracker {
if (this.deleteMapEntry(outgoingHtlcId, this.pendingSendHtlcs) !== null) return if (this.deleteMapEntry(outgoingHtlcId, this.pendingSendHtlcs) !== null) return
if (this.deleteMapEntry(incomingHtlcId, this.pendingReceiveHtlcs) !== null) return if (this.deleteMapEntry(incomingHtlcId, this.pendingReceiveHtlcs) !== null) return
if (this.deleteMapEntry(outgoingHtlcId, this.pendingForwardHtlcs) !== null) return if (this.deleteMapEntry(outgoingHtlcId, this.pendingForwardHtlcs) !== null) return
} else {
this.log("unknown htlc event type for success event", eventType)
} }
} }

View file

@ -50,7 +50,7 @@ const send = (message: ChildProcessResponse) => {
process.send(message, undefined, undefined, err => { process.send(message, undefined, undefined, err => {
if (err) { if (err) {
getLogger({ component: "nostrMiddleware" })(ERROR, "failed to send message to parent process", err, "message:", message) getLogger({ component: "nostrMiddleware" })(ERROR, "failed to send message to parent process", err, "message:", message)
throw new Error("failed to send message to parent process") process.exit(1)
} }
}) })
} }

View file

@ -19,6 +19,7 @@ import { ChannelRouting } from "./entity/ChannelRouting.js"
import { LspOrder } from "./entity/LspOrder.js" import { LspOrder } from "./entity/LspOrder.js"
import { Product } from "./entity/Product.js" import { Product } from "./entity/Product.js"
import { LndNodeInfo } from "./entity/LndNodeInfo.js" import { LndNodeInfo } from "./entity/LndNodeInfo.js"
import { TrackedProvider } from "./entity/TrackedProvider.js"
export type DbSettings = { export type DbSettings = {
@ -58,7 +59,7 @@ export default async (settings: DbSettings, migrations: Function[]): Promise<{ s
database: settings.databaseFile, database: settings.databaseFile,
// logging: true, // logging: true,
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo], UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo, TrackedProvider],
//synchronize: true, //synchronize: true,
migrations migrations
}).initialize() }).initialize()

View file

@ -0,0 +1,26 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from "typeorm"
@Entity()
@Index("tracked_provider_unique", ["provider_type", "provider_pubkey"], { unique: true })
export class TrackedProvider {
@PrimaryGeneratedColumn()
serial_id: number
@Column()
provider_type: 'lnd' | 'lnPub'
@Column()
provider_pubkey: string
@Column()
latest_balance: number
@Column({ default: 0 })
latest_distruption_at_unix: number
@CreateDateColumn()
created_at: Date
@UpdateDateColumn()
updated_at: Date
}

View file

@ -42,7 +42,7 @@ export default class EventsLogManager {
LogEvent = (e: Omit<LoggedEvent, 'timestampMs'>) => { LogEvent = (e: Omit<LoggedEvent, 'timestampMs'>) => {
this.log(e.type, "->", e.userId, "->", e.appId, "->", e.appUserId, "->", e.balance, "->", e.data, "->", e.amount) //this.log(e.type, "->", e.userId, "->", e.appId, "->", e.appUserId, "->", e.balance, "->", e.data, "->", e.amount)
this.write([Date.now(), e.userId, e.appUserId, e.appId, e.balance, e.type, e.data, e.amount]) this.write([Date.now(), e.userId, e.appUserId, e.appId, e.balance, e.type, e.data, e.amount])
} }

View file

@ -8,6 +8,7 @@ import MetricsStorage from "./metricsStorage.js";
import TransactionsQueue, { TX } from "./transactionsQueue.js"; import TransactionsQueue, { TX } from "./transactionsQueue.js";
import EventsLogManager from "./eventsLog.js"; import EventsLogManager from "./eventsLog.js";
import { LiquidityStorage } from "./liquidityStorage.js"; import { LiquidityStorage } from "./liquidityStorage.js";
import { StateBundler } from "./stateBundler.js";
export type StorageSettings = { export type StorageSettings = {
dbSettings: DbSettings dbSettings: DbSettings
eventLogPath: string eventLogPath: string
@ -27,6 +28,7 @@ export default class {
metricsStorage: MetricsStorage metricsStorage: MetricsStorage
liquidityStorage: LiquidityStorage liquidityStorage: LiquidityStorage
eventsLog: EventsLogManager eventsLog: EventsLogManager
stateBundler: StateBundler
constructor(settings: StorageSettings) { constructor(settings: StorageSettings) {
this.settings = settings this.settings = settings
this.eventsLog = new EventsLogManager(settings.eventLogPath) this.eventsLog = new EventsLogManager(settings.eventLogPath)

View file

@ -2,6 +2,7 @@ import { DataSource, EntityManager, MoreThan } from "typeorm"
import { LspOrder } from "./entity/LspOrder.js"; import { LspOrder } from "./entity/LspOrder.js";
import TransactionsQueue, { TX } from "./transactionsQueue.js"; import TransactionsQueue, { TX } from "./transactionsQueue.js";
import { LndNodeInfo } from "./entity/LndNodeInfo.js"; import { LndNodeInfo } from "./entity/LndNodeInfo.js";
import { TrackedProvider } from "./entity/TrackedProvider.js";
export class LiquidityStorage { export class LiquidityStorage {
DB: DataSource | EntityManager DB: DataSource | EntityManager
txQueue: TransactionsQueue txQueue: TransactionsQueue
@ -37,4 +38,18 @@ export class LiquidityStorage {
const entry = this.DB.getRepository(LndNodeInfo).create({ pubkey, backup }) const entry = this.DB.getRepository(LndNodeInfo).create({ pubkey, backup })
await this.txQueue.PushToQueue<LndNodeInfo>({ exec: async db => db.getRepository(LndNodeInfo).save(entry), dbTx: false }) await this.txQueue.PushToQueue<LndNodeInfo>({ exec: async db => db.getRepository(LndNodeInfo).save(entry), dbTx: false })
} }
async GetTrackedProvider(providerType: 'lnd' | 'lnPub', pub: string) {
return this.DB.getRepository(TrackedProvider).findOne({ where: { provider_pubkey: pub, provider_type: providerType } })
}
async CreateTrackedProvider(providerType: 'lnd' | 'lnPub', pub: string, latestBalance = 0) {
const entry = this.DB.getRepository(TrackedProvider).create({ provider_pubkey: pub, provider_type: providerType, latest_balance: latestBalance })
return this.txQueue.PushToQueue<TrackedProvider>({ exec: async db => db.getRepository(TrackedProvider).save(entry), dbTx: false })
}
async UpdateTrackedProviderBalance(providerType: 'lnd' | 'lnPub', pub: string, latestBalance: number) {
return this.DB.getRepository(TrackedProvider).update({ provider_pubkey: pub, provider_type: providerType }, { latest_balance: latestBalance })
}
async UpdateTrackedProviderDisruption(providerType: 'lnd' | 'lnPub', pub: string, latestDisruptionAtUnix: number) {
return this.DB.getRepository(TrackedProvider).update({ provider_pubkey: pub, provider_type: providerType }, { latest_distruption_at_unix: latestDisruptionAtUnix })
}
} }

View file

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class TrackedProvider1720814323679 implements MigrationInterface {
name = 'TrackedProvider1720814323679'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "tracked_provider" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "provider_type" varchar NOT NULL, "provider_pubkey" varchar NOT NULL, "latest_balance" integer NOT NULL, "latest_distruption_at_unix" integer NOT NULL DEFAULT (0), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')))`);
await queryRunner.query(`CREATE UNIQUE INDEX "tracked_provider_unique" ON "tracked_provider" ("provider_type", "provider_pubkey") `);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "tracked_provider_unique"`);
await queryRunner.query(`DROP TABLE "tracked_provider"`);
}
}

View file

@ -7,7 +7,8 @@ import { ChannelRouting1709316653538 } from './1709316653538-channel_routing.js'
import { LspOrder1718387847693 } from './1718387847693-lsp_order.js' import { LspOrder1718387847693 } from './1718387847693-lsp_order.js'
import { LiquidityProvider1719335699480 } from './1719335699480-liquidity_provider.js' import { LiquidityProvider1719335699480 } from './1719335699480-liquidity_provider.js'
import { LndNodeInfo1720187506189 } from './1720187506189-lnd_node_info.js' import { LndNodeInfo1720187506189 } from './1720187506189-lnd_node_info.js'
const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189] import { TrackedProvider1720814323679 } from './1720814323679-tracked_provider.js'
const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, TrackedProvider1720814323679]
const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538] const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538]
export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => { export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => {
if (arg === 'fake_initial_migration') { if (arg === 'fake_initial_migration') {

View file

@ -0,0 +1,142 @@
import { getLogger } from "../helpers/logger.js"
const transactionStatePointTypes = ['addedInvoice', 'invoiceWasPaid', 'paidAnInvoice', 'addedAddress', 'addressWasPaid', 'paidAnAddress', 'user2user'] as const
const balanceStatePointTypes = ['providerBalance', 'providerMaxWithdrawable', 'walletBalance', 'channelBalance', 'usersBalance', 'feesPaidForLiquidity', 'totalLndBalance', 'accumulatedHtlcFees', 'deltaUsers', 'deltaLnd'] as const
const maxStatePointTypes = ['maxProviderRespTime'] as const
export type TransactionStatePointType = typeof transactionStatePointTypes[number]
export type BalanceStatePointType = typeof balanceStatePointTypes[number]
export type MaxStatePointType = typeof maxStatePointTypes[number]
/*export type TransactionStatePoint = {
type: typeof TransactionStatePointTypes[number]
with: 'lnd' | 'internal' | 'provider'
by: 'user' | 'system'
amount: number
success: boolean
networkFee?: number
serviceFee?: number
liquidtyFee?: number
}*/
type StateBundle = Record<string, number>
export type TxPointSettings = {
used: 'lnd' | 'internal' | 'provider' | 'unknown'
from: 'user' | 'system'
meta?: string[]
timeDiscount?: true
}
export class StateBundler {
sinceStart: StateBundle = {}
lastReport: StateBundle = {}
sinceLatestReport: StateBundle = {}
reportPeriod = 1000 * 60 * 60 * 12 //12h
satsPer1SecondDiscount = 1
totalSatsForDiscount = 0
latestReport = Date.now()
reportLog = getLogger({ component: 'stateBundlerReport' })
constructor() {
process.on('exit', () => {
this.Report()
});
// catch ctrl+c event and exit normally
process.on('SIGINT', () => {
console.log('Ctrl-C...');
process.exit(2);
});
//catch uncaught exceptions, trace, then exit normally
process.on('uncaughtException', (e) => {
console.log('Uncaught Exception...');
console.log(e.stack);
process.exit(99);
});
}
increment = (key: string, value: number) => {
this.sinceStart[key] = (this.sinceStart[key] || 0) + value
this.sinceLatestReport[key] = (this.sinceLatestReport[key] || 0) + value
this.triggerReportCheck()
}
set = (key: string, value: number) => {
this.sinceStart[key] = value
this.sinceLatestReport[key] = value
this.triggerReportCheck()
}
max = (key: string, value: number) => {
this.sinceStart[key] = Math.max(this.sinceStart[key] || 0, value)
this.sinceLatestReport[key] = Math.max(this.sinceLatestReport[key] || 0, value)
this.triggerReportCheck()
}
AddTxPoint = (actionName: TransactionStatePointType, v: number, settings: TxPointSettings) => {
const { used, from, timeDiscount } = settings
const meta = settings.meta || []
const key = `${actionName}_${from}_${used}_${meta.join('_')}`
this.increment(key, v)
if (timeDiscount) {
this.totalSatsForDiscount += v
}
this.smallLogEvent(actionName, from)
}
AddTxPointFailed = (actionName: TransactionStatePointType, v: number, settings: TxPointSettings) => {
const { used, from } = settings
const meta = settings.meta || []
const key = `${actionName}_${from}_${used}_${meta.join('_')}_failed`
this.increment(key, v)
}
AddBalancePoint = (actionName: BalanceStatePointType, v: number, meta = []) => {
const key = `${actionName}_${meta.join('_')}`
this.set(key, v)
}
AddMaxPoint = (actionName: MaxStatePointType, v: number, meta = []) => {
const key = `${actionName}_${meta.join('_')}`
this.max(key, v)
}
triggerReportCheck = () => {
const discountSeconds = Math.floor(this.totalSatsForDiscount / this.satsPer1SecondDiscount)
const totalElapsed = Date.now() - this.latestReport
const elapsedWithDiscount = totalElapsed + discountSeconds * 1000
if (elapsedWithDiscount > this.reportPeriod) {
this.Report()
}
}
smallLogEvent(event: TransactionStatePointType, from: 'user' | 'system') {
const char = from === 'user' ? 'U' : 'S'
switch (event) {
case 'addedAddress':
case 'addedInvoice':
process.stdout.write(`${char}+,`)
return
case 'addressWasPaid':
case 'invoiceWasPaid':
process.stdout.write(`${char}>,`)
return
case 'paidAnAddress':
case 'paidAnInvoice':
process.stdout.write(`${char}<,`)
return
case 'user2user':
process.stdout.write(`UU`)
}
}
Report = () => {
this.totalSatsForDiscount = 0
this.latestReport = Date.now()
this.reportLog("+++++ since last report:")
Object.entries(this.sinceLatestReport).forEach(([key, value]) => {
this.reportLog(key, value)
})
this.reportLog("+++++ since start:")
Object.entries(this.sinceStart).forEach(([key, value]) => {
this.reportLog(key, value)
})
this.lastReport = { ...this.sinceLatestReport }
this.sinceLatestReport = {}
}
}

View file

@ -13,7 +13,7 @@ export default async (T: TestBase) => {
const testSuccessfulExternalPayment = async (T: TestBase) => { const testSuccessfulExternalPayment = async (T: TestBase) => {
T.d("starting testSuccessfulExternalPayment") T.d("starting testSuccessfulExternalPayment")
const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId) const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId)
const invoice = await T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry) const invoice = await T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry, { from: 'system', useProvider: false })
expect(invoice.payRequest).to.startWith("lnbcrt5u") expect(invoice.payRequest).to.startWith("lnbcrt5u")
T.d("generated 500 sats invoice for external node") T.d("generated 500 sats invoice for external node")
@ -32,7 +32,7 @@ const testSuccessfulExternalPayment = async (T: TestBase) => {
const testFailedExternalPayment = async (T: TestBase) => { const testFailedExternalPayment = async (T: TestBase) => {
T.d("starting testFailedExternalPayment") T.d("starting testFailedExternalPayment")
const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId) const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId)
const invoice = await T.externalAccessToOtherLnd.NewInvoice(1500, "test", defaultInvoiceExpiry) const invoice = await T.externalAccessToOtherLnd.NewInvoice(1500, "test", defaultInvoiceExpiry, { from: 'system', useProvider: false })
expect(invoice.payRequest).to.startWith("lnbcrt15u") expect(invoice.payRequest).to.startWith("lnbcrt15u")
T.d("generated 1500 sats invoice for external node") T.d("generated 1500 sats invoice for external node")

View file

@ -23,16 +23,13 @@ const testInboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, b
T.d("starting testInboundPaymentFromProvider") T.d("starting testInboundPaymentFromProvider")
const invoiceRes = await bootstrapped.appUserManager.NewInvoice({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier }, { amountSats: 2000, memo: "liquidityTest" }) const invoiceRes = await bootstrapped.appUserManager.NewInvoice({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier }, { amountSats: 2000, memo: "liquidityTest" })
await T.externalAccessToOtherLnd.PayInvoice(invoiceRes.invoice, 0, 100) await T.externalAccessToOtherLnd.PayInvoice(invoiceRes.invoice, 0, 100, 2000, { from: 'system', useProvider: false })
await new Promise((resolve) => setTimeout(resolve, 200)) 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 }) 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.expect(userBalance.balance).to.equal(2000)
T.d("user balance is 2000") T.d("user balance is 2000")
const providerBalance = await bootstrapped.liquidProvider.CheckUserState() const providerBalance = await bootstrapped.liquidityProvider.GetLatestBalance()
if (!providerBalance) { T.expect(providerBalance).to.equal(2000)
throw new Error("provider balance not found")
}
T.expect(providerBalance.balance).to.equal(2000)
T.d("provider balance is 2000") T.d("provider balance is 2000")
T.d("testInboundPaymentFromProvider done") T.d("testInboundPaymentFromProvider done")
} }
@ -40,17 +37,14 @@ const testInboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, b
const testOutboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bootstrappedUser: TestUserData) => { const testOutboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bootstrappedUser: TestUserData) => {
T.d("starting testOutboundPaymentFromProvider") T.d("starting testOutboundPaymentFromProvider")
const invoice = await T.externalAccessToOtherLnd.NewInvoice(1000, "", 60 * 60) const invoice = await T.externalAccessToOtherLnd.NewInvoice(1000, "", 60 * 60, { from: 'system', useProvider: false })
const ctx = { app_id: bootstrappedUser.appId, user_id: bootstrappedUser.userId, app_user_id: bootstrappedUser.appUserIdentifier } 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 res = await bootstrapped.appUserManager.PayInvoice(ctx, { invoice: invoice.payRequest, amount: 0 })
const userBalance = await bootstrapped.appUserManager.GetUserInfo(ctx) const userBalance = await bootstrapped.appUserManager.GetUserInfo(ctx)
T.expect(userBalance.balance).to.equal(986) // 2000 - (1000 + 6(x2) + 2) T.expect(userBalance.balance).to.equal(986) // 2000 - (1000 + 6(x2) + 2)
const providerBalance = await bootstrapped.liquidProvider.CheckUserState() const providerBalance = await bootstrapped.liquidityProvider.GetLatestBalance()
if (!providerBalance) { T.expect(providerBalance).to.equal(992) // 2000 - (1000 + 6 +2)
throw new Error("provider balance not found")
}
T.expect(providerBalance.balance).to.equal(992) // 2000 - (1000 + 6 +2)
T.d("testOutboundPaymentFromProvider done") T.d("testOutboundPaymentFromProvider done")
} }

View file

@ -1,15 +1,17 @@
import { LoadTestSettingsFromEnv } from "../services/main/settings.js" import { LoadTestSettingsFromEnv } from "../services/main/settings.js"
import { BitcoinCoreWrapper } from "./bitcoinCore.js" import { BitcoinCoreWrapper } from "./bitcoinCore.js"
import LND from '../services/lnd/lnd.js' import LND from '../services/lnd/lnd.js'
import { LiquidityProvider } from "../services/lnd/liquidityProvider.js" import { LiquidityProvider } from "../services/main/liquidityProvider.js"
import { Utils } from "../services/helpers/utilsWrapper.js"
export const setupNetwork = async () => { export const setupNetwork = async () => {
const settings = LoadTestSettingsFromEnv() const settings = LoadTestSettingsFromEnv()
const core = new BitcoinCoreWrapper(settings) const core = new BitcoinCoreWrapper(settings)
await core.InitAddress() await core.InitAddress()
await core.Mine(1) await core.Mine(1)
const alice = new LND(settings.lndSettings, new LiquidityProvider("", () => { }), () => { }, () => { }, () => { }, () => { }) const setupUtils = new Utils(settings)
const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, new LiquidityProvider("", () => { }), () => { }, () => { }, () => { }, () => { }) const alice = new LND(settings.lndSettings, new LiquidityProvider("", setupUtils, async () => { }), setupUtils, async () => { }, async () => { }, () => { }, () => { })
const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, new LiquidityProvider("", setupUtils, async () => { }), setupUtils, async () => { }, async () => { }, () => { }, () => { })
await tryUntil<void>(async i => { await tryUntil<void>(async i => {
const peers = await alice.ListPeers() const peers = await alice.ListPeers()
if (peers.peers.length > 0) { if (peers.peers.length > 0) {

View file

@ -24,9 +24,9 @@ export const initBootstrappedInstance = async (T: TestBase) => {
} }
const j = JSON.parse(data.content) as { requestId: string } const j = JSON.parse(data.content) as { requestId: string }
console.log("sending new operation to provider") console.log("sending new operation to provider")
bootstrapped.liquidProvider.onEvent(j, T.app.publicKey) bootstrapped.liquidityProvider.onEvent(j, T.app.publicKey)
}) })
bootstrapped.liquidProvider.attachNostrSend(async (_, data, r) => { bootstrapped.liquidityProvider.attachNostrSend(async (_, data, r) => {
const res = await handleSend(T, data) const res = await handleSend(T, data)
if (data.type === 'event') { if (data.type === 'event') {
throw new Error("unsupported event type") throw new Error("unsupported event type")
@ -34,12 +34,13 @@ export const initBootstrappedInstance = async (T: TestBase) => {
if (!res) { if (!res) {
return return
} }
bootstrapped.liquidProvider.onEvent(res, data.pub) bootstrapped.liquidityProvider.onEvent(res, data.pub)
}) })
bootstrapped.liquidProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey }) bootstrapped.liquidityProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey })
await new Promise<void>(res => { await new Promise<void>(res => {
const interval = setInterval(() => { const interval = setInterval(async () => {
if (bootstrapped.liquidProvider.CanProviderHandle({ action: 'receive', amount: 2000 })) { const canHandle = await bootstrapped.liquidityProvider.CanProviderHandle({ action: 'receive', amount: 2000 })
if (canHandle) {
clearInterval(interval) clearInterval(interval)
res() res()
} else { } else {

View file

@ -14,7 +14,7 @@ export default async (T: TestBase) => {
const testSpamExternalPayment = async (T: TestBase) => { const testSpamExternalPayment = async (T: TestBase) => {
T.d("starting testSpamExternalPayment") T.d("starting testSpamExternalPayment")
const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId) const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId)
const invoices = await Promise.all(new Array(10).fill(0).map(() => T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry))) const invoices = await Promise.all(new Array(10).fill(0).map(() => T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry, { from: 'system', useProvider: false })))
T.d("generated 10 500 sats invoices for external node") T.d("generated 10 500 sats invoices for external node")
const res = await Promise.all(invoices.map(async (invoice, i) => { const res = await Promise.all(invoices.map(async (invoice, i) => {
try { try {

View file

@ -15,7 +15,7 @@ export default async (T: TestBase) => {
const testSpamExternalPayment = async (T: TestBase) => { const testSpamExternalPayment = async (T: TestBase) => {
T.d("starting testSpamExternalPayment") T.d("starting testSpamExternalPayment")
const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId) const application = await T.main.storage.applicationStorage.GetApplication(T.app.appId)
const invoicesForExternal = await Promise.all(new Array(5).fill(0).map(() => T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry))) const invoicesForExternal = await Promise.all(new Array(5).fill(0).map(() => T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry, { from: 'system', useProvider: false })))
const invoicesForUser2 = await Promise.all(new Array(5).fill(0).map(() => T.main.paymentManager.NewInvoice(T.user2.userId, { amountSats: 500, memo: "test" }, { linkedApplication: application, expiry: defaultInvoiceExpiry }))) const invoicesForUser2 = await Promise.all(new Array(5).fill(0).map(() => T.main.paymentManager.NewInvoice(T.user2.userId, { amountSats: 500, memo: "test" }, { linkedApplication: application, expiry: defaultInvoiceExpiry })))
const invoices = invoicesForExternal.map(i => i.payRequest).concat(invoicesForUser2.map(i => i.invoice)) const invoices = invoicesForExternal.map(i => i.payRequest).concat(invoicesForUser2.map(i => i.invoice))
T.d("generated 10 500 sats mixed invoices between external node and user 2") T.d("generated 10 500 sats mixed invoices between external node and user 2")

View file

@ -10,7 +10,8 @@ import { defaultInvoiceExpiry } from '../services/storage/paymentStorage.js'
import SanityChecker from '../services/main/sanityChecker.js' import SanityChecker from '../services/main/sanityChecker.js'
import LND from '../services/lnd/lnd.js' import LND from '../services/lnd/lnd.js'
import { getLogger, resetDisabledLoggers } from '../services/helpers/logger.js' import { getLogger, resetDisabledLoggers } from '../services/helpers/logger.js'
import { LiquidityProvider } from '../services/lnd/liquidityProvider.js' import { LiquidityProvider } from '../services/main/liquidityProvider.js'
import { Utils } from '../services/helpers/utilsWrapper.js'
chai.use(chaiString) chai.use(chaiString)
export const expect = chai.expect export const expect = chai.expect
export type Describe = (message: string, failure?: boolean) => void export type Describe = (message: string, failure?: boolean) => void
@ -45,16 +46,16 @@ export const SetupTest = async (d: Describe): Promise<TestBase> => {
const user1 = { userId: u1.info.userId, appUserIdentifier: u1.identifier, appId: app.appId } const user1 = { userId: u1.info.userId, appUserIdentifier: u1.identifier, appId: app.appId }
const user2 = { userId: u2.info.userId, appUserIdentifier: u2.identifier, appId: app.appId } const user2 = { userId: u2.info.userId, appUserIdentifier: u2.identifier, appId: app.appId }
const extermnalUtils = new Utils(settings)
const externalAccessToMainLnd = new LND(settings.lndSettings, new LiquidityProvider("", () => { }), console.log, console.log, () => { }, () => { }) const externalAccessToMainLnd = new LND(settings.lndSettings, new LiquidityProvider("", extermnalUtils, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { })
await externalAccessToMainLnd.Warmup() await externalAccessToMainLnd.Warmup()
const otherLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.otherNode } const otherLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }
const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider("", () => { }), console.log, console.log, () => { }, () => { }) const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider("", extermnalUtils, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { })
await externalAccessToOtherLnd.Warmup() await externalAccessToOtherLnd.Warmup()
const thirdLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.thirdNode } const thirdLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.thirdNode }
const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider("", () => { }), console.log, console.log, () => { }, () => { }) const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider("", extermnalUtils, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { })
await externalAccessToThirdLnd.Warmup() await externalAccessToThirdLnd.Warmup()
@ -78,7 +79,7 @@ export const teardown = async (T: TestBase) => {
export const safelySetUserBalance = async (T: TestBase, user: TestUserData, amount: number) => { export const safelySetUserBalance = async (T: TestBase, user: TestUserData, amount: number) => {
const app = await T.main.storage.applicationStorage.GetApplication(user.appId) const app = await T.main.storage.applicationStorage.GetApplication(user.appId)
const invoice = await T.main.paymentManager.NewInvoice(user.userId, { amountSats: amount, memo: "test" }, { linkedApplication: app, expiry: defaultInvoiceExpiry }) const invoice = await T.main.paymentManager.NewInvoice(user.userId, { amountSats: amount, memo: "test" }, { linkedApplication: app, expiry: defaultInvoiceExpiry })
await T.externalAccessToOtherLnd.PayInvoice(invoice.invoice, 0, 100) await T.externalAccessToOtherLnd.PayInvoice(invoice.invoice, 0, 100, amount, { from: 'system', useProvider: false })
const u = await T.main.storage.userStorage.GetUser(user.userId) const u = await T.main.storage.userStorage.GetUser(user.userId)
expect(u.balance_sats).to.be.equal(amount) expect(u.balance_sats).to.be.equal(amount)
T.d(`user ${user.appUserIdentifier} balance is now ${amount}`) T.d(`user ${user.appUserIdentifier} balance is now ${amount}`)

View file

@ -12,7 +12,7 @@ export default async (T: TestBase) => {
const testSuccessfulUserPaymentToExternalNode = async (T: TestBase) => { const testSuccessfulUserPaymentToExternalNode = async (T: TestBase) => {
T.d("starting testSuccessfulUserPaymentToExternalNode") T.d("starting testSuccessfulUserPaymentToExternalNode")
const invoice = await T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry) const invoice = await T.externalAccessToOtherLnd.NewInvoice(500, "test", defaultInvoiceExpiry, { from: 'system', useProvider: false })
const payment = await T.main.appUserManager.PayInvoice({ app_id: T.user1.appId, user_id: T.user1.userId, app_user_id: T.user1.appUserIdentifier }, { invoice: invoice.payRequest, amount: 0 }) const payment = await T.main.appUserManager.PayInvoice({ app_id: T.user1.appId, user_id: T.user1.userId, app_user_id: T.user1.appUserIdentifier }, { invoice: invoice.payRequest, amount: 0 })
T.d("paid 500 sats invoice from user1 to external node") T.d("paid 500 sats invoice from user1 to external node")
} }