From 72683a3e8821019adfd584c26579ebd660cf3c70 Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Thu, 23 May 2024 21:22:43 +0200 Subject: [PATCH] liquidity provider tested --- src/services/lnd/liquidityProvider.ts | 25 ++++---- src/services/lnd/lnd.ts | 2 +- src/services/lnd/lsp.ts | 29 +++++++++ src/services/main/init.ts | 2 +- src/services/main/settings.ts | 5 +- src/tests/liquidityProvider.spec.ts | 62 +++++++++++++------ src/tests/networkSetup.ts | 4 +- src/tests/setupBootstrapped.ts | 89 +++++++++++++++++++++++++++ src/tests/testBase.ts | 10 +-- 9 files changed, 186 insertions(+), 42 deletions(-) create mode 100644 src/services/lnd/lsp.ts create mode 100644 src/tests/setupBootstrapped.ts diff --git a/src/services/lnd/liquidityProvider.ts b/src/services/lnd/liquidityProvider.ts index 18af9d68..28de9c36 100644 --- a/src/services/lnd/liquidityProvider.ts +++ b/src/services/lnd/liquidityProvider.ts @@ -21,6 +21,7 @@ export class LiquidityProvider { pubDestination: string latestMaxWithdrawable: number | null = null invoicePaidCb: InvoicePaidCb + connecting = false // make the sub process accept client constructor(pubDestination: string, invoicePaidCb: InvoicePaidCb) { if (!pubDestination) { @@ -44,11 +45,13 @@ export class LiquidityProvider { Connect = async () => { await new Promise(res => setTimeout(res, 2000)) this.log("ready") - this.CheckUSerState() + await this.CheckUserState() if (this.latestMaxWithdrawable === null) { return } + this.log("subbing to user operations") this.client.GetLiveUserOperations(res => { + console.log("got user operation", res) if (res.status === 'ERROR') { this.log("error getting user operations", res.reason) return @@ -61,7 +64,7 @@ export class LiquidityProvider { }) } - CheckUSerState = async () => { + CheckUserState = async () => { const res = await this.client.GetUserInfo() if (res.status === 'ERROR') { this.log("error getting user info", res) @@ -69,6 +72,7 @@ export class LiquidityProvider { } this.latestMaxWithdrawable = res.max_withdrawable this.log("latest provider balance:", res.max_withdrawable) + return res } CanProviderHandle = (req: LiquidityRequest) => { @@ -88,7 +92,7 @@ export class LiquidityProvider { throw new Error(res.reason) } this.log("new invoice", res.invoice) - this.CheckUSerState() + this.CheckUserState() return res.invoice } @@ -99,7 +103,7 @@ export class LiquidityProvider { throw new Error(res.reason) } this.log("paid invoice", res) - this.CheckUSerState() + this.CheckUserState() return res } @@ -123,18 +127,17 @@ export class LiquidityProvider { } } - - onEvent = async (res: { requestId: string }, fromPub: string) => { if (fromPub !== this.pubDestination) { + this.log("got event from invalid pub", fromPub, this.pubDestination) return false } if (this.clientCbs[res.requestId]) { const cb = this.clientCbs[res.requestId] cb.f(res) if (cb.type === 'single') { - const deleteOk = (delete this.clientCbs[res.requestId]) - console.log(this.getSingleSubs(), "single subs left", deleteOk) + delete this.clientCbs[res.requestId] + this.log(this.getSingleSubs(), "single subs left") } return true } @@ -160,7 +163,7 @@ export class LiquidityProvider { //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 => { this.clientCbs[reqId] = { startedAtMillis: Date.now(), @@ -187,7 +190,7 @@ export class LiquidityProvider { type: 'stream', f: (response: any) => { cb(response) }, } - console.log("sub for", reqId, "was already registered, overriding") + this.log("sub for", reqId, "was already registered, overriding") return } this.nostrSend({ type: 'client', clientId: this.clientId }, { @@ -195,7 +198,7 @@ export class LiquidityProvider { pub: to, content: JSON.stringify(message) }) - console.log("subbing to stream", reqId) + this.log("subbing to stream", reqId) this.clientCbs[reqId] = { startedAtMillis: Date.now(), type: 'stream', diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index 31ef23a9..4834dfe3 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -230,7 +230,7 @@ export default class { }, { abort: this.abortController.signal }) stream.responses.onMessage(invoice => { if (invoice.state === Invoice_InvoiceState.SETTLED) { - this.log("An invoice was paid for", Number(invoice.amtPaidSat), "sats") + this.log("An invoice was paid for", Number(invoice.amtPaidSat), "sats", invoice.paymentRequest) this.latestKnownSettleIndex = Number(invoice.settleIndex) this.invoicePaidCb(invoice.paymentRequest, Number(invoice.amtPaidSat), false) } diff --git a/src/services/lnd/lsp.ts b/src/services/lnd/lsp.ts new file mode 100644 index 00000000..87f6a99c --- /dev/null +++ b/src/services/lnd/lsp.ts @@ -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 + } +} \ No newline at end of file diff --git a/src/services/main/init.ts b/src/services/main/init.ts index 11875a40..488a34cc 100644 --- a/src/services/main/init.ts +++ b/src/services/main/init.ts @@ -54,7 +54,7 @@ export const initMainHandler = async (log: PubLogger, mainSettings: MainSettings if (stop) { return } - return { mainHandler, apps, liquidityProviderInfo } + return { mainHandler, apps, liquidityProviderInfo, liquidityProviderApp } } const processArgs = async (mainHandler: Main) => { diff --git a/src/services/main/settings.ts b/src/services/main/settings.ts index 054858d9..d692c54f 100644 --- a/src/services/main/settings.ts +++ b/src/services/main/settings.ts @@ -75,14 +75,15 @@ export const LoadTestSettingsFromEnv = (): TestSettings => { lndAddr: EnvMustBeNonEmptyString("LND_FOURTH_ADDR"), lndCertPath: EnvMustBeNonEmptyString("LND_FOURTH_CERT_PATH"), lndMacaroonPath: EnvMustBeNonEmptyString("LND_FOURTH_MACAROON_PATH") - } + }, + liquidityProviderPub: "" }, skipSanityCheck: true, bitcoinCoreSettings: { port: EnvMustBeInteger("BITCOIN_CORE_PORT"), user: EnvMustBeNonEmptyString("BITCOIN_CORE_USER"), pass: EnvMustBeNonEmptyString("BITCOIN_CORE_PASS") - } + }, } } diff --git a/src/tests/liquidityProvider.spec.ts b/src/tests/liquidityProvider.spec.ts index af20eb4c..13268f83 100644 --- a/src/tests/liquidityProvider.spec.ts +++ b/src/tests/liquidityProvider.spec.ts @@ -1,31 +1,53 @@ -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' +import { disableLoggers } from '../services/helpers/logger.js' +import { runSanityCheck, safelySetUserBalance, TestBase, TestUserData } from './testBase.js' +import { initBootstrappedInstance } from './setupBootstrapped.js' +import Main from '../services/main/index.js' +import { AppData } from '../services/main/init.js' export const ignore = false -export const dev = true +export const dev = false export default async (T: TestBase) => { + disableLoggers([], ["EventsLogManager", "watchdog", "htlcTracker", "debugHtlcs", "debugLndBalancev3", "metrics", "mainForTest", "main"]) 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" }) + T.d("starting liquidityProvider tests...") + const { bootstrapped, bootstrappedUser } = await initBootstrappedInstance(T) + await testInboundPaymentFromProvider(T, bootstrapped, bootstrappedUser) + await testOutboundPaymentFromProvider(T, bootstrapped, bootstrappedUser) 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 +const testInboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bUser: TestUserData) => { + T.d("starting testInboundPaymentFromProvider") + const invoiceRes = await bootstrapped.appUserManager.NewInvoice({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier }, { amountSats: 2000, memo: "liquidityTest" }) - bootstrapped.liquidProvider.attachNostrSend((identifier, data, r) => { - console.log(identifier, data) - }) - bootstrapped.liquidProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey }) - return bootstrapped + await T.externalAccessToOtherLnd.PayInvoice(invoiceRes.invoice, 0, 100) + const userBalance = await bootstrapped.appUserManager.GetUserInfo({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier }) + T.expect(userBalance.balance).to.equal(2000) + + const providerBalance = await bootstrapped.liquidProvider.CheckUserState() + if (!providerBalance) { + throw new Error("provider balance not found") + } + T.expect(providerBalance.balance).to.equal(2000) + T.d("testInboundPaymentFromProvider done") +} + +const testOutboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bootstrappedUser: TestUserData) => { + T.d("starting testOutboundPaymentFromProvider") + + const invoice = await T.externalAccessToOtherLnd.NewInvoice(1000, "", 60 * 60) + const ctx = { app_id: bootstrappedUser.appId, user_id: bootstrappedUser.userId, app_user_id: bootstrappedUser.appUserIdentifier } + const res = await bootstrapped.appUserManager.PayInvoice(ctx, { invoice: invoice.payRequest, amount: 0 }) + + const userBalance = await bootstrapped.appUserManager.GetUserInfo(ctx) + T.expect(userBalance.balance).to.equal(986) // 2000 - (1000 + 6(x2) + 2) + + const providerBalance = await bootstrapped.liquidProvider.CheckUserState() + if (!providerBalance) { + throw new Error("provider balance not found") + } + T.expect(providerBalance.balance).to.equal(992) // 2000 - (1000 + 6 +2) + T.d("testOutboundPaymentFromProvider done") } \ No newline at end of file diff --git a/src/tests/networkSetup.ts b/src/tests/networkSetup.ts index 90ddc7a5..45ea6ef7 100644 --- a/src/tests/networkSetup.ts +++ b/src/tests/networkSetup.ts @@ -8,8 +8,8 @@ export const setupNetwork = async () => { const core = new BitcoinCoreWrapper(settings) await core.InitAddress() await core.Mine(1) - const alice = new LND(settings.lndSettings, new LiquidityProvider(""), () => { }, () => { }, () => { }, () => { }) - const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, new LiquidityProvider(""), () => { }, () => { }, () => { }, () => { }) + const alice = new LND(settings.lndSettings, new LiquidityProvider("", () => { }), () => { }, () => { }, () => { }, () => { }) + const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, new LiquidityProvider("", () => { }), () => { }, () => { }, () => { }, () => { }) await tryUntil(async i => { const peers = await alice.ListPeers() if (peers.peers.length > 0) { diff --git a/src/tests/setupBootstrapped.ts b/src/tests/setupBootstrapped.ts new file mode 100644 index 00000000..2abc0990 --- /dev/null +++ b/src/tests/setupBootstrapped.ts @@ -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(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) + } +} \ No newline at end of file diff --git a/src/tests/testBase.ts b/src/tests/testBase.ts index 248a9117..a130703f 100644 --- a/src/tests/testBase.ts +++ b/src/tests/testBase.ts @@ -9,7 +9,7 @@ import chaiString from 'chai-string' import { defaultInvoiceExpiry } from '../services/storage/paymentStorage.js' import SanityChecker from '../services/main/sanityChecker.js' import LND from '../services/lnd/lnd.js' -import { resetDisabledLoggers } from '../services/helpers/logger.js' +import { getLogger, resetDisabledLoggers } from '../services/helpers/logger.js' import { LiquidityProvider } from '../services/lnd/liquidityProvider.js' chai.use(chaiString) export const expect = chai.expect @@ -34,7 +34,7 @@ export type TestBase = { export const SetupTest = async (d: Describe): Promise => { const settings = LoadTestSettingsFromEnv() - const initialized = await initMainHandler(console.log, settings) + const initialized = await initMainHandler(getLogger({ component: "mainForTest" }), settings) if (!initialized) { throw new Error("failed to initialize main handler") } @@ -46,15 +46,15 @@ export const SetupTest = async (d: Describe): Promise => { 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() 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() 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()