liquidity provider tests
This commit is contained in:
parent
bbd00b3450
commit
f31551a2b5
7 changed files with 125 additions and 18 deletions
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export type LndSettings = {
|
|||
feeFixedLimit: number
|
||||
mockLnd: boolean
|
||||
liquidityProviderPub: string
|
||||
useOnlyLiquidityProvider: boolean
|
||||
|
||||
otherNode?: NodeSettings
|
||||
thirdNode?: NodeSettings
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
31
src/tests/liquidityProvider.spec.ts
Normal file
31
src/tests/liquidityProvider.spec.ts
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue