liquidity provider tests

This commit is contained in:
boufni95 2024-05-23 16:58:22 +02:00
parent bbd00b3450
commit f31551a2b5
7 changed files with 125 additions and 18 deletions

View file

@ -8,5 +8,5 @@ export const LoadLndSettingsFromEnv = (): LndSettings => {
const feeFixedLimit = EnvCanBeInteger("OUTBOUND_MAX_FEE_EXTRA_SATS", 100)
const mockLnd = EnvCanBeBoolean("MOCK_LND")
const liquidityProviderPub = process.env.LIQUIDITY_PROVIDER_PUB || ""
return { mainNode: { lndAddr, lndCertPath, lndMacaroonPath }, feeRateLimit, feeFixedLimit, mockLnd, liquidityProviderPub }
return { mainNode: { lndAddr, lndCertPath, lndMacaroonPath }, feeRateLimit, feeFixedLimit, mockLnd, liquidityProviderPub, useOnlyLiquidityProvider: false }
}

View file

@ -5,7 +5,9 @@ import { decodeNprofile } from '../../custom-nip19.js'
import { getLogger } from '../helpers/logger.js'
import { NostrEvent, NostrSend } from '../nostr/handler.js'
import { relayInit } from '../nostr/tools/relay.js'
import { InvoicePaidCb } from './settings.js'
export type LiquidityRequest = { action: 'spend' | 'receive', amount: number }
export type nostrCallback<T> = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void }
export class LiquidityProvider {
@ -17,13 +19,15 @@ export class LiquidityProvider {
nostrSend: NostrSend | null = null
ready = false
pubDestination: string
latestMaxWithdrawable: number | null = null
invoicePaidCb: InvoicePaidCb
// make the sub process accept client
constructor(pubDestination: string) {
constructor(pubDestination: string, invoicePaidCb: InvoicePaidCb) {
if (!pubDestination) {
this.log("No pub provider to liquidity provider, will not be initialized")
}
this.pubDestination = pubDestination
this.invoicePaidCb = invoicePaidCb
this.client = newNostrClient({
pubDestination: this.pubDestination,
retrieveNostrUserAuth: async () => this.myPub,
@ -32,20 +36,71 @@ export class LiquidityProvider {
const interval = setInterval(() => {
if (this.ready) {
clearInterval(interval)
this.CheckUSerState()
this.Connect()
}
}, 1000)
}
CheckUSerState = async () => {
Connect = async () => {
await new Promise(res => setTimeout(res, 2000))
this.log("ready")
this.CheckUSerState()
if (this.latestMaxWithdrawable === null) {
return
}
this.client.GetLiveUserOperations(res => {
if (res.status === 'ERROR') {
this.log("error getting user operations", res.reason)
return
}
this.log("got user operation", res.operation)
if (res.operation.type === Types.UserOperationType.INCOMING_INVOICE) {
this.log("invoice was paid", res.operation.identifier)
this.invoicePaidCb(res.operation.identifier, res.operation.amount, false)
}
})
}
CheckUSerState = async () => {
const res = await this.client.GetUserInfo()
if (res.status === 'ERROR') {
this.log("error getting user info", res)
return
}
this.log("got user info", res)
this.latestMaxWithdrawable = res.max_withdrawable
this.log("latest provider balance:", res.max_withdrawable)
}
CanProviderHandle = (req: LiquidityRequest) => {
if (this.latestMaxWithdrawable === null) {
return false
}
if (req.action === 'spend') {
return this.latestMaxWithdrawable > req.amount
}
return true
}
AddInvoice = async (amount: number, memo: string) => {
const res = await this.client.NewInvoice({ amountSats: amount, memo })
if (res.status === 'ERROR') {
this.log("error creating invoice", res.reason)
throw new Error(res.reason)
}
this.log("new invoice", res.invoice)
this.CheckUSerState()
return res.invoice
}
PayInvoice = async (invoice: string) => {
const res = await this.client.PayInvoice({ invoice, amount: 0 })
if (res.status === 'ERROR') {
this.log("error paying invoice", res.reason)
throw new Error(res.reason)
}
this.log("paid invoice", res)
this.CheckUSerState()
return res
}
setNostrInfo = ({ clientId, myPub }: { myPub: string, clientId: string }) => {
@ -68,9 +123,7 @@ export class LiquidityProvider {
}
}
IsReady = () => {
return this.ready
}
onEvent = async (res: { requestId: string }, fromPub: string) => {
if (fromPub !== this.pubDestination) {

View file

@ -16,7 +16,7 @@ import { SendCoinsReq } from './sendCoinsReq.js';
import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo } from './settings.js';
import { getLogger } from '../helpers/logger.js';
import { HtlcEvent_EventType } from '../../../proto/lnd/router.js';
import { LiquidityProvider } from './liquidityProvider.js';
import { LiquidityProvider, LiquidityRequest } from './liquidityProvider.js';
const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
const deadLndRetrySeconds = 5
export default class {
@ -36,7 +36,6 @@ export default class {
log = getLogger({ component: 'lndManager' })
outgoingOpsLocked = false
liquidProvider: LiquidityProvider
useLiquidityProvider = false
constructor(settings: LndSettings, liquidProvider: LiquidityProvider, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb) {
this.settings = settings
this.addressPaidCb = addressPaidCb
@ -64,7 +63,6 @@ export default class {
this.router = new RouterClient(transport)
this.chainNotifier = new ChainNotifierClient(transport)
this.liquidProvider = liquidProvider
this.useLiquidityProvider = !!settings.liquidityProviderPub
}
LockOutgoingOperations(): void {
@ -80,6 +78,21 @@ export default class {
Stop() {
this.abortController.abort()
}
async ShouldUseLiquidityProvider(req: LiquidityRequest): Promise<boolean> {
if (this.settings.useOnlyLiquidityProvider) {
return true
}
if (!this.liquidProvider.CanProviderHandle(req)) {
return false
}
const channels = await this.ListChannels()
if (channels.channels.length === 0) {
this.log("no channels, will use liquidity provider")
return true
}
return false
}
async Warmup() {
this.SubscribeAddressPaid()
this.SubscribeInvoicePaid()
@ -256,6 +269,11 @@ export default class {
async NewInvoice(value: number, memo: string, expiry: number): Promise<Invoice> {
this.log("generating new invoice for", value, "sats")
await this.Health()
const shouldUseLiquidityProvider = await this.ShouldUseLiquidityProvider({ action: 'receive', amount: value })
if (shouldUseLiquidityProvider) {
const invoice = await this.liquidProvider.AddInvoice(value, memo)
return { payRequest: invoice }
}
const res = await this.lightning.addInvoice(AddInvoiceReq(value, expiry, false, memo), DeadLineMetadata())
this.log("new invoice", res.response.paymentRequest)
return { payRequest: res.response.paymentRequest }
@ -286,6 +304,11 @@ export default class {
}
await this.Health()
this.log("paying invoice", invoice, "for", amount, "sats")
const shouldUseLiquidityProvider = await this.ShouldUseLiquidityProvider({ action: 'spend', amount })
if (shouldUseLiquidityProvider) {
const res = await this.liquidProvider.PayInvoice(invoice)
return { feeSat: res.network_fee + res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage }
}
const abortController = new AbortController()
const req = PayInvoiceReq(invoice, amount, feeLimit)
const stream = this.router.sendPaymentV2(req, { abort: abortController.signal })

View file

@ -10,6 +10,7 @@ export type LndSettings = {
feeFixedLimit: number
mockLnd: boolean
liquidityProviderPub: string
useOnlyLiquidityProvider: boolean
otherNode?: NodeSettings
thirdNode?: NodeSettings

View file

@ -38,11 +38,10 @@ export default class {
metricsManager: MetricsManager
liquidProvider: LiquidityProvider
nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") }
constructor(settings: MainSettings, storage: Storage, liquidProvider: LiquidityProvider) {
constructor(settings: MainSettings, storage: Storage) {
this.settings = settings
this.storage = storage
this.liquidProvider = liquidProvider
this.liquidProvider = new LiquidityProvider(settings.lndSettings.liquidityProviderPub, this.invoicePaidCb)
this.lnd = new LND(settings.lndSettings, this.liquidProvider, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb)
this.metricsManager = new MetricsManager(this.storage, this.lnd)

View file

@ -17,8 +17,8 @@ export const initMainHandler = async (log: PubLogger, mainSettings: MainSettings
if (manualMigration) {
return
}
const liquidityProvider = new LiquidityProvider(mainSettings.lndSettings.liquidityProviderPub)
const mainHandler = new Main(mainSettings, storageManager, liquidityProvider)
const mainHandler = new Main(mainSettings, storageManager)
await mainHandler.lnd.Warmup()
if (!mainSettings.skipSanityCheck) {
const sanityChecker = new SanityChecker(storageManager, mainHandler.lnd)
@ -49,7 +49,7 @@ export const initMainHandler = async (log: PubLogger, mainSettings: MainSettings
publicKey: liquidityProviderApp.publicKey,
name: "liquidity_provider", clientId: `client_${liquidityProviderApp.appId}`
}
liquidityProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey })
mainHandler.liquidProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey })
const stop = await processArgs(mainHandler)
if (stop) {
return

View file

@ -0,0 +1,31 @@
import { initMainHandler } from '../services/main/init.js'
import { LoadTestSettingsFromEnv } from '../services/main/settings.js'
import { defaultInvoiceExpiry } from '../services/storage/paymentStorage.js'
import { runSanityCheck, safelySetUserBalance, TestBase } from './testBase.js'
export const ignore = false
export const dev = true
export default async (T: TestBase) => {
await safelySetUserBalance(T, T.user1, 2000)
const bootstrapped = await initBootstrappedInstance(T)
bootstrapped.appUserManager.NewInvoice({ app_id: T.user1.appId, user_id: T.user1.userId, app_user_id: T.user1.appUserIdentifier }, { amountSats: 2000, memo: "liquidityTest" })
await runSanityCheck(T)
}
const initBootstrappedInstance = async (T: TestBase) => {
const settings = LoadTestSettingsFromEnv()
settings.lndSettings.useOnlyLiquidityProvider = true
const initialized = await initMainHandler(console.log, settings)
if (!initialized) {
throw new Error("failed to initialize bootstrapped main handler")
}
const { mainHandler: bootstrapped, liquidityProviderInfo } = initialized
bootstrapped.liquidProvider.attachNostrSend((identifier, data, r) => {
console.log(identifier, data)
})
bootstrapped.liquidProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey })
return bootstrapped
}