liquidity provider tested

This commit is contained in:
hatim boufnichel 2024-05-23 21:22:43 +02:00
parent f31551a2b5
commit 72683a3e88
9 changed files with 186 additions and 42 deletions

View file

@ -21,6 +21,7 @@ export class LiquidityProvider {
pubDestination: string pubDestination: string
latestMaxWithdrawable: number | null = null latestMaxWithdrawable: number | null = null
invoicePaidCb: InvoicePaidCb invoicePaidCb: InvoicePaidCb
connecting = false
// make the sub process accept client // make the sub process accept client
constructor(pubDestination: string, invoicePaidCb: InvoicePaidCb) { constructor(pubDestination: string, invoicePaidCb: InvoicePaidCb) {
if (!pubDestination) { if (!pubDestination) {
@ -44,11 +45,13 @@ export class LiquidityProvider {
Connect = async () => { Connect = async () => {
await new Promise(res => setTimeout(res, 2000)) await new Promise(res => setTimeout(res, 2000))
this.log("ready") this.log("ready")
this.CheckUSerState() await this.CheckUserState()
if (this.latestMaxWithdrawable === null) { if (this.latestMaxWithdrawable === null) {
return return
} }
this.log("subbing to user operations")
this.client.GetLiveUserOperations(res => { this.client.GetLiveUserOperations(res => {
console.log("got user operation", res)
if (res.status === 'ERROR') { if (res.status === 'ERROR') {
this.log("error getting user operations", res.reason) this.log("error getting user operations", res.reason)
return return
@ -61,7 +64,7 @@ export class LiquidityProvider {
}) })
} }
CheckUSerState = async () => { 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)
@ -69,6 +72,7 @@ export class LiquidityProvider {
} }
this.latestMaxWithdrawable = res.max_withdrawable this.latestMaxWithdrawable = res.max_withdrawable
this.log("latest provider balance:", res.max_withdrawable) this.log("latest provider balance:", res.max_withdrawable)
return res
} }
CanProviderHandle = (req: LiquidityRequest) => { CanProviderHandle = (req: LiquidityRequest) => {
@ -88,7 +92,7 @@ export class LiquidityProvider {
throw new Error(res.reason) throw new Error(res.reason)
} }
this.log("new invoice", res.invoice) this.log("new invoice", res.invoice)
this.CheckUSerState() this.CheckUserState()
return res.invoice return res.invoice
} }
@ -99,7 +103,7 @@ export class LiquidityProvider {
throw new Error(res.reason) throw new Error(res.reason)
} }
this.log("paid invoice", res) this.log("paid invoice", res)
this.CheckUSerState() this.CheckUserState()
return res return res
} }
@ -123,18 +127,17 @@ export class LiquidityProvider {
} }
} }
onEvent = async (res: { requestId: string }, fromPub: string) => { onEvent = async (res: { requestId: string }, fromPub: string) => {
if (fromPub !== this.pubDestination) { if (fromPub !== this.pubDestination) {
this.log("got event from invalid pub", fromPub, this.pubDestination)
return false return false
} }
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') {
const deleteOk = (delete this.clientCbs[res.requestId]) delete this.clientCbs[res.requestId]
console.log(this.getSingleSubs(), "single subs left", deleteOk) this.log(this.getSingleSubs(), "single subs left")
} }
return true return true
} }
@ -160,7 +163,7 @@ export class LiquidityProvider {
//this.nostrSend(this.relays, to, JSON.stringify(message), this.settings) //this.nostrSend(this.relays, to, JSON.stringify(message), this.settings)
console.log("subbing to single send", reqId, message.rpcName) 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(),
@ -187,7 +190,7 @@ export class LiquidityProvider {
type: 'stream', type: 'stream',
f: (response: any) => { cb(response) }, f: (response: any) => { cb(response) },
} }
console.log("sub for", reqId, "was already registered, overriding") this.log("sub for", reqId, "was already registered, overriding")
return return
} }
this.nostrSend({ type: 'client', clientId: this.clientId }, { this.nostrSend({ type: 'client', clientId: this.clientId }, {
@ -195,7 +198,7 @@ export class LiquidityProvider {
pub: to, pub: to,
content: JSON.stringify(message) content: JSON.stringify(message)
}) })
console.log("subbing to stream", reqId) this.log("subbing to stream", reqId)
this.clientCbs[reqId] = { this.clientCbs[reqId] = {
startedAtMillis: Date.now(), startedAtMillis: Date.now(),
type: 'stream', type: 'stream',

View file

@ -230,7 +230,7 @@ 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") 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), false)
} }

29
src/services/lnd/lsp.ts Normal file
View file

@ -0,0 +1,29 @@
import fetch from "node-fetch"
export class LSP {
serviceUrl: string
constructor(serviceUrl: string) {
this.serviceUrl = serviceUrl
}
getInfo = async () => {
const res = await fetch(`${this.serviceUrl}/getinfo`)
const json = await res.json() as { options: {}, uris: string[] }
}
createOrder = async (req: { public_key: string }) => {
const res = await fetch(`${this.serviceUrl}/create_order`, {
method: "POST",
body: JSON.stringify(req),
headers: { "Content-Type": "application/json" }
})
const json = await res.json() as {}
return json
}
getOrder = async (orderId: string) => {
const res = await fetch(`${this.serviceUrl}/get_order&order_id=${orderId}`)
const json = await res.json() as {}
return json
}
}

View file

@ -54,7 +54,7 @@ export const initMainHandler = async (log: PubLogger, mainSettings: MainSettings
if (stop) { if (stop) {
return return
} }
return { mainHandler, apps, liquidityProviderInfo } return { mainHandler, apps, liquidityProviderInfo, liquidityProviderApp }
} }
const processArgs = async (mainHandler: Main) => { const processArgs = async (mainHandler: Main) => {

View file

@ -75,14 +75,15 @@ export const LoadTestSettingsFromEnv = (): TestSettings => {
lndAddr: EnvMustBeNonEmptyString("LND_FOURTH_ADDR"), lndAddr: EnvMustBeNonEmptyString("LND_FOURTH_ADDR"),
lndCertPath: EnvMustBeNonEmptyString("LND_FOURTH_CERT_PATH"), lndCertPath: EnvMustBeNonEmptyString("LND_FOURTH_CERT_PATH"),
lndMacaroonPath: EnvMustBeNonEmptyString("LND_FOURTH_MACAROON_PATH") lndMacaroonPath: EnvMustBeNonEmptyString("LND_FOURTH_MACAROON_PATH")
} },
liquidityProviderPub: ""
}, },
skipSanityCheck: true, skipSanityCheck: true,
bitcoinCoreSettings: { bitcoinCoreSettings: {
port: EnvMustBeInteger("BITCOIN_CORE_PORT"), port: EnvMustBeInteger("BITCOIN_CORE_PORT"),
user: EnvMustBeNonEmptyString("BITCOIN_CORE_USER"), user: EnvMustBeNonEmptyString("BITCOIN_CORE_USER"),
pass: EnvMustBeNonEmptyString("BITCOIN_CORE_PASS") pass: EnvMustBeNonEmptyString("BITCOIN_CORE_PASS")
} },
} }
} }

View file

@ -1,31 +1,53 @@
import { initMainHandler } from '../services/main/init.js' import { disableLoggers } from '../services/helpers/logger.js'
import { LoadTestSettingsFromEnv } from '../services/main/settings.js' import { runSanityCheck, safelySetUserBalance, TestBase, TestUserData } from './testBase.js'
import { defaultInvoiceExpiry } from '../services/storage/paymentStorage.js' import { initBootstrappedInstance } from './setupBootstrapped.js'
import { runSanityCheck, safelySetUserBalance, TestBase } from './testBase.js' import Main from '../services/main/index.js'
import { AppData } from '../services/main/init.js'
export const ignore = false export const ignore = false
export const dev = true export const dev = false
export default async (T: TestBase) => { export default async (T: TestBase) => {
disableLoggers([], ["EventsLogManager", "watchdog", "htlcTracker", "debugHtlcs", "debugLndBalancev3", "metrics", "mainForTest", "main"])
await safelySetUserBalance(T, T.user1, 2000) await safelySetUserBalance(T, T.user1, 2000)
const bootstrapped = await initBootstrappedInstance(T) T.d("starting liquidityProvider tests...")
bootstrapped.appUserManager.NewInvoice({ app_id: T.user1.appId, user_id: T.user1.userId, app_user_id: T.user1.appUserIdentifier }, { amountSats: 2000, memo: "liquidityTest" }) const { bootstrapped, bootstrappedUser } = await initBootstrappedInstance(T)
await testInboundPaymentFromProvider(T, bootstrapped, bootstrappedUser)
await testOutboundPaymentFromProvider(T, bootstrapped, bootstrappedUser)
await runSanityCheck(T) await runSanityCheck(T)
} }
const initBootstrappedInstance = async (T: TestBase) => { const testInboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bUser: TestUserData) => {
const settings = LoadTestSettingsFromEnv() T.d("starting testInboundPaymentFromProvider")
settings.lndSettings.useOnlyLiquidityProvider = true const invoiceRes = await bootstrapped.appUserManager.NewInvoice({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier }, { amountSats: 2000, memo: "liquidityTest" })
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) => { await T.externalAccessToOtherLnd.PayInvoice(invoiceRes.invoice, 0, 100)
console.log(identifier, data) 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)
bootstrapped.liquidProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey })
return bootstrapped const providerBalance = await bootstrapped.liquidProvider.CheckUserState()
if (!providerBalance) {
throw new Error("provider balance not found")
}
T.expect(providerBalance.balance).to.equal(2000)
T.d("testInboundPaymentFromProvider done")
}
const testOutboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bootstrappedUser: TestUserData) => {
T.d("starting testOutboundPaymentFromProvider")
const invoice = await T.externalAccessToOtherLnd.NewInvoice(1000, "", 60 * 60)
const ctx = { app_id: bootstrappedUser.appId, user_id: bootstrappedUser.userId, app_user_id: bootstrappedUser.appUserIdentifier }
const res = await bootstrapped.appUserManager.PayInvoice(ctx, { invoice: invoice.payRequest, amount: 0 })
const userBalance = await bootstrapped.appUserManager.GetUserInfo(ctx)
T.expect(userBalance.balance).to.equal(986) // 2000 - (1000 + 6(x2) + 2)
const providerBalance = await bootstrapped.liquidProvider.CheckUserState()
if (!providerBalance) {
throw new Error("provider balance not found")
}
T.expect(providerBalance.balance).to.equal(992) // 2000 - (1000 + 6 +2)
T.d("testOutboundPaymentFromProvider done")
} }

View file

@ -8,8 +8,8 @@ export const setupNetwork = async () => {
const core = new BitcoinCoreWrapper(settings) const core = new BitcoinCoreWrapper(settings)
await core.InitAddress() await core.InitAddress()
await core.Mine(1) await core.Mine(1)
const alice = new LND(settings.lndSettings, new LiquidityProvider(""), () => { }, () => { }, () => { }, () => { }) const alice = new LND(settings.lndSettings, new LiquidityProvider("", () => { }), () => { }, () => { }, () => { }, () => { })
const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, new LiquidityProvider(""), () => { }, () => { }, () => { }, () => { }) const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, new LiquidityProvider("", () => { }), () => { }, () => { }, () => { }, () => { })
await tryUntil<void>(async i => { await tryUntil<void>(async i => {
const peers = await alice.ListPeers() const peers = await alice.ListPeers()
if (peers.peers.length > 0) { if (peers.peers.length > 0) {

View file

@ -0,0 +1,89 @@
import { getLogger } from '../services/helpers/logger.js'
import { initMainHandler } from '../services/main/init.js'
import { LoadTestSettingsFromEnv } from '../services/main/settings.js'
import { SendData } from '../services/nostr/handler.js'
import { TestBase, TestUserData } from './testBase.js'
import * as Types from '../../proto/autogenerated/ts/types.js'
export const initBootstrappedInstance = async (T: TestBase) => {
const requests = {}
const settings = LoadTestSettingsFromEnv()
settings.lndSettings.useOnlyLiquidityProvider = true
settings.lndSettings.liquidityProviderPub = T.app.publicKey
settings.lndSettings.mainNode = settings.lndSettings.thirdNode
const initialized = await initMainHandler(getLogger({ component: "bootstrapped" }), settings)
if (!initialized) {
throw new Error("failed to initialize bootstrapped main handler")
}
const { mainHandler: bootstrapped, liquidityProviderInfo, liquidityProviderApp, apps } = initialized
T.main.attachNostrSend(async (initiator, data, r) => {
if (data.type === 'event') {
throw new Error("unsupported event type")
}
if (data.pub !== liquidityProviderInfo.publicKey) {
throw new Error("invalid pub " + data.pub + " expected " + liquidityProviderInfo.publicKey)
}
const j = JSON.parse(data.content) as { requestId: string }
console.log("sending new operation to provider")
bootstrapped.liquidProvider.onEvent(j, T.app.publicKey)
})
bootstrapped.liquidProvider.attachNostrSend(async (initiator, data, r) => {
const res = await handleSend(T, data)
if (data.type === 'event') {
throw new Error("unsupported event type")
}
if (!res) {
return
}
bootstrapped.liquidProvider.onEvent(res, data.pub)
})
bootstrapped.liquidProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey })
await new Promise<void>(res => {
const interval = setInterval(() => {
if (bootstrapped.liquidProvider.CanProviderHandle({ action: 'receive', amount: 2000 })) {
clearInterval(interval)
res()
} else {
console.log("waiting for provider to be able to handle the request")
}
}, 500)
})
const bUser = await bootstrapped.applicationManager.AddAppUser(liquidityProviderApp.appId, { identifier: "user1_bootstrapped", balance: 0, fail_if_exists: true })
const bootstrappedUser: TestUserData = { userId: bUser.info.userId, appUserIdentifier: bUser.identifier, appId: liquidityProviderApp.appId }
return { bootstrapped, liquidityProviderInfo, liquidityProviderApp, bootstrappedUser }
}
type TransportRequest = { requestId: string, authIdentifier: string } & (
{ rpcName: 'GetUserInfo' } |
{ rpcName: 'NewInvoice', body: Types.NewInvoiceRequest } |
{ rpcName: 'PayInvoice', body: Types.PayInvoiceRequest } |
{ rpcName: 'GetLiveUserOperations' } |
{ rpcName: "" }
)
const handleSend = async (T: TestBase, data: SendData) => {
if (data.type === 'event') {
throw new Error("unsupported event type")
}
if (data.pub !== T.app.publicKey) {
throw new Error("invalid pub")
}
const j = JSON.parse(data.content) as TransportRequest
const app = await T.main.storage.applicationStorage.GetApplication(T.app.appId)
const nostrUser = await T.main.storage.applicationStorage.GetOrCreateNostrAppUser(app, j.authIdentifier)
const userCtx = { app_id: app.app_id, user_id: nostrUser.user.user_id, app_user_id: nostrUser.identifier }
switch (j.rpcName) {
case 'GetUserInfo':
const infoRes = await T.main.appUserManager.GetUserInfo(userCtx)
return { ...infoRes, status: "OK", requestId: j.requestId }
case 'NewInvoice':
const genInvoiceRes = await T.main.appUserManager.NewInvoice(userCtx, j.body)
return { ...genInvoiceRes, status: "OK", requestId: j.requestId }
case 'PayInvoice':
const payRes = await T.main.appUserManager.PayInvoice(userCtx, j.body)
return { ...payRes, status: "OK", requestId: j.requestId }
case 'GetLiveUserOperations':
return
default:
console.log(data)
throw new Error("unsupported rpcName " + j.rpcName)
}
}

View file

@ -9,7 +9,7 @@ import chaiString from 'chai-string'
import { defaultInvoiceExpiry } from '../services/storage/paymentStorage.js' import { defaultInvoiceExpiry } from '../services/storage/paymentStorage.js'
import SanityChecker from '../services/main/sanityChecker.js' import SanityChecker from '../services/main/sanityChecker.js'
import LND from '../services/lnd/lnd.js' import LND from '../services/lnd/lnd.js'
import { resetDisabledLoggers } from '../services/helpers/logger.js' import { getLogger, resetDisabledLoggers } from '../services/helpers/logger.js'
import { LiquidityProvider } from '../services/lnd/liquidityProvider.js' import { LiquidityProvider } from '../services/lnd/liquidityProvider.js'
chai.use(chaiString) chai.use(chaiString)
export const expect = chai.expect export const expect = chai.expect
@ -34,7 +34,7 @@ export type TestBase = {
export const SetupTest = async (d: Describe): Promise<TestBase> => { export const SetupTest = async (d: Describe): Promise<TestBase> => {
const settings = LoadTestSettingsFromEnv() const settings = LoadTestSettingsFromEnv()
const initialized = await initMainHandler(console.log, settings) const initialized = await initMainHandler(getLogger({ component: "mainForTest" }), settings)
if (!initialized) { if (!initialized) {
throw new Error("failed to initialize main handler") throw new Error("failed to initialize main handler")
} }
@ -46,15 +46,15 @@ export const SetupTest = async (d: Describe): Promise<TestBase> => {
const user2 = { userId: u2.info.userId, appUserIdentifier: u2.identifier, appId: app.appId } const user2 = { userId: u2.info.userId, appUserIdentifier: u2.identifier, appId: app.appId }
const externalAccessToMainLnd = new LND(settings.lndSettings, new LiquidityProvider(""), console.log, console.log, () => { }, () => { }) const externalAccessToMainLnd = new LND(settings.lndSettings, new LiquidityProvider("", () => { }), console.log, console.log, () => { }, () => { })
await externalAccessToMainLnd.Warmup() await externalAccessToMainLnd.Warmup()
const otherLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.otherNode } const otherLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }
const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider(""), console.log, console.log, () => { }, () => { }) const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider("", () => { }), console.log, console.log, () => { }, () => { })
await externalAccessToOtherLnd.Warmup() await externalAccessToOtherLnd.Warmup()
const thirdLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.thirdNode } const thirdLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.thirdNode }
const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider(""), console.log, console.log, () => { }, () => { }) const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider("", () => { }), console.log, console.log, () => { }, () => { })
await externalAccessToThirdLnd.Warmup() await externalAccessToThirdLnd.Warmup()