Merge remote-tracking branch 'origin/master' into add-webview

This commit is contained in:
polarDefender 2024-05-06 03:06:22 -07:00
commit 374d0d60ed
28 changed files with 1661 additions and 345 deletions

55
.github/workflows/test.yaml vendored Normal file
View file

@ -0,0 +1,55 @@
name: Docker Compose Actions Workflow
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: unzip the file
run: unzip src/tests/regtestNetwork.zip
- name: list files
run: ls -la
- name: Build the stack
run: docker-compose --project-directory ./ -f src/tests/docker-compose.yml up -d
- name: Copy alice cert file
run: docker cp polar-n2-alice:/home/lnd/.lnd/tls.cert alice-tls.cert
- name: Copy bob cert file
run: docker cp polar-n2-bob:/home/lnd/.lnd/tls.cert bob-tls.cert
- name: Copy carol cert file
run: docker cp polar-n2-carol:/home/lnd/.lnd/tls.cert carol-tls.cert
- name: Copy dave cert file
run: docker cp polar-n2-dave:/home/lnd/.lnd/tls.cert dave-tls.cert
- name: Copy alice macaroon file
run: docker cp polar-n2-alice:/home/lnd/.lnd/data/chain/bitcoin/regtest/admin.macaroon alice-admin.macaroon
- name: Copy bob macaroon file
run: docker cp polar-n2-bob:/home/lnd/.lnd/data/chain/bitcoin/regtest/admin.macaroon bob-admin.macaroon
- name: Copy carol macaroon file
run: docker cp polar-n2-carol:/home/lnd/.lnd/data/chain/bitcoin/regtest/admin.macaroon carol-admin.macaroon
- name: Copy dave macaroon file
run: docker cp polar-n2-dave:/home/lnd/.lnd/data/chain/bitcoin/regtest/admin.macaroon dave-admin.macaroon
- name: copy env file
run: cp src/tests/.env.test .env
- name: List files
run: ls -la
- name: Cache node modules
id: cache-npm
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }}
name: List the state of node modules
continue-on-error: true
run: npm list
- name: Install dependencies
run: npm install
- name: Run tests
run: npm test

View file

@ -8,11 +8,15 @@
### Don't just run a Lightning Node, run a Lightning Pub. ### Don't just run a Lightning Node, run a Lightning Pub.
"Pub" is a `nostr` native account system that makes connecting your node to apps and websites super easy. "Pub" is a [Nostr](https://nostr.info)-native account system designed to make running Lightning infrastructure for your friends/family/customers easier than previously thought possible.
Using Nostr relays as transport for encrypted RPCs, Pub eliminates the complexity of WebServer and SSL configurations. It may come as a surprise that the biggest hurdle to more Uncle Jim nodes hasn't been with Bitcoin/Lightning node management itself, since even in bad patterns like mobile nodes, that is easily automated.
By solving the networking and programability hurdles, Pub enables node-runners and Uncle Jim's to bring their Friends, Family and Customers into Bitcoin's permissionless circular economy. All while keeping the Lightning Network decentralized, and custodial scaling free of fiat shitcoin rails and large banks. It's the legacy baggage of traditional web infrastructure, things like IP4, reverse proxies, DNS, Firewalls and SSL certificates, all of which require a personal configuration that is a hurdle for most. The slow and unreliable nature of things like Tor have proven to be dead-ends, and Bolt12 being a re-implementation of Tor is destined for the same fate.
Pub solves these network challenges with a Full RPC that is Nostr-native. Being Nostr-native eliminates the complexity of legacy server configuration by using completely commoditized and trustless Nostr relays. Additionally, some optional services are integrated for backward compatibility with LNURL and Lightning Address.
By solving the networking and programability hurdles, Pub's provide a 3rd Lightning Layer that enables node-runners and Uncle Jims to more easily bring their personal network into Bitcoin's permissionless economy. In doing so, Pub can keep the Lightning Network decentralized, with custodial scaling free of fiat rails, large banks, and other forms of high-time-preference shitcoinery.
#### Features: #### Features:
@ -24,59 +28,69 @@ By solving the networking and programability hurdles, Pub enables node-runners a
![Accounts](https://github.com/shocknet/Lightning.Pub/raw/master/accounting_layers.png) ![Accounts](https://github.com/shocknet/Lightning.Pub/raw/master/accounting_layers.png)
#### Planned #### Planned
- [ ] Management Dashboard is being integrated into [ShockWallet](https://github.com/shocknet/wallet2) - [ ] Management Dashboard is actively being integrated into [ShockWallet](https://github.com/shocknet/wallet2)
- [ ] Nostr native "offers" - [ ] Nostr native "offers"
- [ ] Channel Automation - [ ] Automated Channels
- [ ] Bootstarp Peering (Passive "LSP") - [ ] Bootstrap Peering (Passive "LSP")
- [ ] Subscriptions / Notifications - [ ] Event Notifications
- [ ] Submarine Swaps - [ ] Submarine Swaps
- [ ] High-Availabilty / Clustering - [ ] High-Availabilty / Clustering
Dashboard: Dashboard Wireframe:
<img src="https://shockwallet.b-cdn.net/pub_home_ss.png" alt="Pub Dashboard" width="240"> <img src="https://shockwallet.b-cdn.net/pub_home_ss.png" alt="Pub Dashboard" width="240">
#### ShockWallet and Lightning.Pub are free software. If you would like to see continued development, please show your [support](https://github.com/sponsors/shocknet) :) > [!IMPORTANT]
> ShockWallet and Lightning.Pub are free software. If you would like to see continued development, please show your [**support**](https://github.com/sponsors/shocknet) 😊
> [!WARNING]
> While this software has been used in a high-profile production environment for over a year, it should still be considered bleeding edge. Special care has been taken to mitigate the risk of drainage attacks, which is a common risk to all Lightning API's. An integrated Watchdog service will terminate spends if it detects a discrepency between LND and the database, for this reason **IT IS NOT RECOMMENDED TO USE PUB ALONGSIDE OTHER ACCOUNT SYSTEMS**. While we give the utmost care and attention to security, **the internet is an adversarial environment and SECURITY/RELIABILITY ARE NOT GUARANTEED- USE AT YOUR OWN RISK**.
> **WARNING:** While this software has been used in production for many months, it is still bleeding edge and security or reliabilty is not guaranteed. ## Umbrel Installation
## Manual Installation Coming Soon
## Desktop Installation
Coming Soon
## Manual CLI Installation
#### Notes: #### Notes:
* The service defaults to port `8080`
* Use of a reverse proxy is only required if you wish to serve LNURLs * Use of a reverse proxy is only required if you wish to serve LNURLs
* The service defaults to port `8080`
* Requires [Node.js](https://nodejs.org) >=18.x * Requires [Node.js](https://nodejs.org) >=18.x
* Commands for your specific OS may differ slightly, Ubuntu/Debian used for example
#### Steps: #### Steps:
1) Run [LND](https://github.com/lightningnetwork/lnd/releases) - *Example mainnet startup*: 1) Run [LND](https://github.com/lightningnetwork/lnd/releases) if you aren't already
*Example mainnet startup*:
``` ```
./lnd --bitcoin.active --bitcoin.mainnet --bitcoin.node=neutrino --neutrino.connect=neutrino.shock.network --routing.assumechanvalid --accept-keysend --allow-circular-route --feeurl=https://nodes.lightning.computer/fees/v1/btc-fee-estimates.json ./lnd --bitcoin.active --bitcoin.mainnet --bitcoin.node=neutrino --neutrino.addpeer=neutrino.shock.network --feeurl=https://nodes.lightning.computer/fees/v1/btc-fee-estimates.json
``` ```
2) Download and Install Lightning.Pub 2) Download and Install Lightning.Pub
```
git clone https://github.com/shocknet/Lightning.Pub
cd Lightning.Pub && npm i
```
3) `cp env.example .env` * `git clone https://github.com/shocknet/Lightning.Pub`
4) Add values to env file * `cd Lightning.Pub && npm i`
3) Configure values to env file as desired
* `cp env.example .env && nano .env`
5) `npm start` 5) `npm start`
6) Create an Application Pool - A default "wallet" application pool will be automatically created, if you wish to create other app pools:
* `curl -XPOST -H 'Authorization: Bearer defined_in_ADMIN_TOKEN_env' -H "Content-type: application/json" -d '{"name":"ExampleApplicationPoolName"}' 'http://localhost:8080/api/admin/app/add'`
A default "wallet" pool will be automatically created and keys generated automatically, if you wish to create something other: 5) Connect with [wallet2](https://github.com/shocknet/wallet2) using the wallet nprofile that gets logged at startup.
> [!NOTE]
``` > Connecting with wallet will create an account on the node, it will not show or have access to the full LND balance
curl -XPOST -H 'Authorization: Bearer defined_in_constants.ts' -H "Content-type: application/json" -d '{"name":"ExampleApplicationPoolName"}' 'http://localhost:8080/api/admin/app/add'
```
7) Connect with [wallet2](https://github.com/shocknet/wallet2) using the npub response in step 6 or the the wallet application nprofile logged at startup.

View file

@ -1,52 +1,85 @@
#LND # Example configuration for Lightning.Pub
LND_ADDRESS=127.0.0.1:10009 # Copy this file as .env in the Pub folder and uncomment the desired settings to override defaults
LND_CERT_PATH=/root/.lnd/tls.cert # Alternatively, these settings can be passed as environment variables at startup
LND_MACAROON_PATH=/root/.lnd/data/chain/bitcoin/mainnet/admin.macaroon
#LND_CONNECTION
# Defaults typical for straight Linux
# Containers, Mac and Windows may need more detailed paths
#LND_ADDRESS=127.0.0.1:10009
#LND_CERT_PATH=~/.lnd/tls.cert
#LND_MACAROON_PATH=~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon
#DB #DB
DATABASE_FILE=db.sqlite #DATABASE_FILE=db.sqlite
METRICS_DATABASE_FILE=metrics.sqlite #METRICS_DATABASE_FILE=metrics.sqlite
#LOCAL #LOCALHOST
ADMIN_TOKEN= #ADMIN_TOKEN=
PORT=8080 #PORT=8080
JWT_SECRET=bigsecrethere #JWT_SECRET=
#LIGHTNING #LIGHTNING
OUTBOUND_MAX_FEE_BPS=60 # Maximum amount in network fees passed to LND when it pays an external invoice
OUTBOUND_MAX_FEE_EXTRA_SATS=100 # BPS are basis points, 100 BPS = 1%
#OUTBOUND_MAX_FEE_BPS=60
#OUTBOUND_MAX_FEE_EXTRA_SATS=100
# If the back-end doesn't have adequate channel capacity, buy one from an LSP
# Will execute when it costs less than 1% of balance and uses a trusted peer
#BOOTSTRAP=1
#ROOT_FEES #ROOT_FEES
INCOMING_CHAIN_FEE_ROOT_BPS=0 # Applied to either debits or credits and sent to an admin account
INCOMING_INVOICE_FEE_ROOT_BPS=0 # BPS are basis points, 100 BPS = 1%
OUTGOING_CHAIN_FEE_ROOT_BPS=60 #applied to application debits #INCOMING_CHAIN_FEE_ROOT_BPS=0
OUTGOING_INVOICE_FEE_ROOT_BPS=60 #applied to application debits #INCOMING_INVOICE_FEE_ROOT_BPS=0
TX_FEE_INTERNAL_ROOT_BPS=60 #applied to inter-application txns # Chain spends are currently unstable and thus disabled, do not use until further notice
#OUTGOING_CHAIN_FEE_ROOT_BPS=60
# Outgoing Invoice Fee must be >= Lightning Outbound Max Fee so admins don't incur losses on spends
#OUTGOING_INVOICE_FEE_ROOT_BPS=60
# Internal user fees bugged, do not use until further notice
#TX_FEE_INTERNAL_ROOT_BPS=0 #applied to inter-application txns
#APP_FEES #APP_FEES
INCOMING_INVOICE_FEE_USER_BPS=0 #app default # An extra fee applied at the app level and sent to the application owner
OUTGOING_INVOICE_FEE_USER_BPS=60 #app default #INCOMING_INVOICE_FEE_USER_BPS=0
TX_FEE_INTERNAL_USER_BPS=60 #intra-application tx default #OUTGOING_INVOICE_FEE_USER_BPS=0
#TX_FEE_INTERNAL_USER_BPS=0
#NOSTR #NOSTR
NOSTR_RELAYS=wss://strfry.shock.network # Default relay may become rate-limited without a paid subscription
#NOSTR_RELAYS=wss://strfry.shock.network
#LNURL #LNURL
#Note that a reachable https endpoint for the service to handle lnurl requests is required for lightning address bridges # Optional
SERVICE_URL=https://test.lightning.pub # If undefined, LNURLs (including Lightning Address) will be disabled
# To enable, add a reachable https endpoint for requests (or purchase a subscription)
# You also need an SSL reverse proxy from the domain to this local host
# Read more at https://docs.shock.network
#SERVICE_URL=https://yourdomainhere.xyz
#DEV #SUBSCRIPTION_SERVICES
MOCK_LND=false # Opt-in to cloud relays for LNURL and Nostr
ALLOW_BALANCE_MIGRATION=false # A small monthly fee supports the developers
MIGRATE_DB=false # Read more at https://docs.shock.network
#SUBSCRIBER=1
#DEV_OPTS
#MOCK_LND=false
#ALLOW_BALANCE_MIGRATION=false
#MIGRATE_DB=false
#METRICS #METRICS
RECORD_PERFORMANCE=true #RECORD_PERFORMANCE=true
SKIP_SANITY_CHECK=false #SKIP_SANITY_CHECK=false
DISABLE_EXTERNAL_PAYMENTS=false # A read-only token that can be used with dashboard to view reports
#METRICS_TOKEN=
# Disable outbound payments aka honeypot mode
#DISABLE_EXTERNAL_PAYMENTS=false
# Max difference between users balance and LND balance since beginning of app execution #WATCHDOG SECURITY
WATCHDOG_MAX_DIFF_SATS=10000 # A last line of defense against 0-day drainage attacks
# This will monitor LND separately and terminate sends if a balance discrepency is detected
# Max difference between users balance and LND balance after each payment # This setting defaults to 0 meaning no discrepency will be tolerated
WATCHDOG_MAX_UPDATE_DIFF_SATS=1000 # Increase this values to add a spending buffer for non-Pub services sharing LND
# Max difference between users balance and LND balance at Pub startup
#WATCHDOG_MAX_DIFF_SATS=0

994
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -35,6 +35,7 @@
"@types/secp256k1": "^4.0.3", "@types/secp256k1": "^4.0.3",
"axios": "^0.28.0", "axios": "^0.28.0",
"bech32": "^2.0.0", "bech32": "^2.0.0",
"bitcoin-core": "^4.2.0",
"chai": "^4.3.7", "chai": "^4.3.7",
"chai-string": "^1.5.0", "chai-string": "^1.5.0",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
@ -77,4 +78,4 @@
"ts-node": "10.7.0", "ts-node": "10.7.0",
"typescript": "4.5.2" "typescript": "4.5.2"
} }
} }

View file

@ -0,0 +1,34 @@
import { PubLogger, getLogger } from "../helpers/logger.js"
type Item<T> = { res: (v: T) => void, rej: (message: string) => void }
export default class FunctionQueue<T> {
log: PubLogger
queue: Item<T>[] = []
running: boolean = false
f: () => Promise<T>
constructor(name: string, f: () => Promise<T>) {
this.log = getLogger({ appName: name })
this.f = f
}
Run = (item: Item<T>) => {
this.queue.push(item)
if (!this.running) {
this.execF()
}
}
execF = async () => {
this.running = true
try {
const res = await this.f()
this.queue.forEach(q => q.res(res))
} catch (err) {
this.queue.forEach(q => q.rej((err as any).message))
}
this.queue = []
this.running = false
}
}

View file

@ -1,10 +1,5 @@
import * as Types from '../../../proto/autogenerated/ts/types.js'
import { GetInfoResponse, NewAddressResponse, AddInvoiceResponse, PayReq, Payment, SendCoinsResponse, EstimateFeeResponse, TransactionDetails, ClosedChannelsResponse, ListChannelsResponse, PendingChannelsResponse, ListInvoiceResponse, ListPaymentsResponse } from '../../../proto/lnd/lightning.js'
import { EnvMustBeNonEmptyString, EnvMustBeInteger, EnvCanBeBoolean } from '../helpers/envParser.js' import { EnvMustBeNonEmptyString, EnvMustBeInteger, EnvCanBeBoolean } from '../helpers/envParser.js'
import { AddressPaidCb, BalanceInfo, DecodedInvoice, HtlcCb, Invoice, InvoicePaidCb, LndSettings, NewBlockCb, NodeInfo, PaidInvoice } from './settings.js' import { LndSettings } from './settings.js'
import LND from './lnd.js'
import MockLnd from './mock.js'
import { getLogger } from '../helpers/logger.js'
export const LoadLndSettingsFromEnv = (): LndSettings => { export const LoadLndSettingsFromEnv = (): LndSettings => {
const lndAddr = EnvMustBeNonEmptyString("LND_ADDRESS") const lndAddr = EnvMustBeNonEmptyString("LND_ADDRESS")
const lndCertPath = EnvMustBeNonEmptyString("LND_CERT_PATH") const lndCertPath = EnvMustBeNonEmptyString("LND_CERT_PATH")
@ -14,40 +9,3 @@ export const LoadLndSettingsFromEnv = (): LndSettings => {
const mockLnd = EnvCanBeBoolean("MOCK_LND") const mockLnd = EnvCanBeBoolean("MOCK_LND")
return { mainNode: { lndAddr, lndCertPath, lndMacaroonPath }, feeRateLimit, feeFixedLimit, mockLnd } return { mainNode: { lndAddr, lndCertPath, lndMacaroonPath }, feeRateLimit, feeFixedLimit, mockLnd }
} }
export interface LightningHandler {
Stop(): void
Warmup(): Promise<void>
GetInfo(): Promise<NodeInfo>
Health(): Promise<void>
NewAddress(addressType: Types.AddressType): Promise<NewAddressResponse>
NewInvoice(value: number, memo: string, expiry: number): Promise<Invoice>
DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice>
GetFeeLimitAmount(amount: number): number
GetMaxWithinLimit(amount: number): number
PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<PaidInvoice>
EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse>
PayAddress(address: string, amount: number, satPerVByte: number, label?: string): Promise<SendCoinsResponse>
OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number): Promise<string>
SetMockInvoiceAsPaid(invoice: string, amount: number): Promise<void>
ChannelBalance(): Promise<{ local: number, remote: number }>
GetTransactions(startHeight: number): Promise<TransactionDetails>
GetBalance(): Promise<BalanceInfo>
ListClosedChannels(): Promise<ClosedChannelsResponse>
ListChannels(): Promise<ListChannelsResponse>
ListPendingChannels(): Promise<PendingChannelsResponse>
GetForwardingHistory(indexOffset: number): Promise<{ fee: number, chanIdIn: string, chanIdOut: string, timestampNs: number, offset: number }[]>
GetAllPaidInvoices(max: number): Promise<ListInvoiceResponse>
GetAllPayments(max: number): Promise<ListPaymentsResponse>
LockOutgoingOperations(): void
UnlockOutgoingOperations(): void
}
export default (settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb): LightningHandler => {
if (settings.mockLnd) {
getLogger({})("registering mock lnd handler")
return new MockLnd(settings, addressPaidCb, invoicePaidCb, newBlockCb)
} else {
getLogger({})("registering prod lnd handler")
return new LND(settings, addressPaidCb, invoicePaidCb, newBlockCb, htlcCb)
}
}

View file

@ -8,7 +8,7 @@ import { LightningClient } from '../../../proto/lnd/lightning.client.js'
import { InvoicesClient } from '../../../proto/lnd/invoices.client.js' import { InvoicesClient } from '../../../proto/lnd/invoices.client.js'
import { RouterClient } from '../../../proto/lnd/router.client.js' import { RouterClient } from '../../../proto/lnd/router.client.js'
import { ChainNotifierClient } from '../../../proto/lnd/chainnotifier.client.js' import { ChainNotifierClient } from '../../../proto/lnd/chainnotifier.client.js'
import { GetInfoResponse, AddressType, NewAddressResponse, AddInvoiceResponse, Invoice_InvoiceState, PayReq, Payment_PaymentStatus, Payment, PaymentFailureReason, SendCoinsResponse, EstimateFeeResponse, ChannelBalanceResponse, TransactionDetails, ListChannelsResponse, ClosedChannelsResponse, PendingChannelsResponse } from '../../../proto/lnd/lightning.js' import { GetInfoResponse, AddressType, NewAddressResponse, AddInvoiceResponse, Invoice_InvoiceState, PayReq, Payment_PaymentStatus, Payment, PaymentFailureReason, SendCoinsResponse, EstimateFeeResponse, ChannelBalanceResponse, TransactionDetails, ListChannelsResponse, ClosedChannelsResponse, PendingChannelsResponse, ForwardingHistoryResponse } from '../../../proto/lnd/lightning.js'
import { OpenChannelReq } from './openChannelReq.js'; import { OpenChannelReq } from './openChannelReq.js';
import { AddInvoiceReq } from './addInvoiceReq.js'; import { AddInvoiceReq } from './addInvoiceReq.js';
import { PayInvoiceReq } from './payInvoiceReq.js'; import { PayInvoiceReq } from './payInvoiceReq.js';
@ -80,7 +80,22 @@ export default class {
this.SubscribeInvoicePaid() this.SubscribeInvoicePaid()
this.SubscribeNewBlock() this.SubscribeNewBlock()
this.SubscribeHtlcEvents() this.SubscribeHtlcEvents()
this.ready = true const now = Date.now()
return new Promise<void>((res, rej) => {
const interval = setInterval(async () => {
try {
await this.GetInfo()
clearInterval(interval)
this.ready = true
res()
} catch (err) {
this.log("LND is not ready yet, will try again in 1 second")
if (Date.now() - now > 1000 * 60) {
rej(new Error("LND not ready after 1 minute"))
}
}
}, 1000)
})
} }
async GetInfo(): Promise<NodeInfo> { async GetInfo(): Promise<NodeInfo> {
@ -277,6 +292,7 @@ export default class {
stream.responses.onMessage(payment => { stream.responses.onMessage(payment => {
switch (payment.status) { switch (payment.status) {
case Payment_PaymentStatus.FAILED: case Payment_PaymentStatus.FAILED:
console.log(payment)
this.log("invoice payment failed", payment.failureReason) this.log("invoice payment failed", payment.failureReason)
rej(PaymentFailureReason[payment.failureReason]) rej(PaymentFailureReason[payment.failureReason])
return return
@ -317,6 +333,16 @@ export default class {
return res.response return res.response
} }
async GetChannelBalance() {
const res = await this.lightning.channelBalance({}, DeadLineMetadata())
return res.response
}
async GetWalletBalance() {
const res = await this.lightning.walletBalance({}, DeadLineMetadata())
return res.response
}
async GetBalance(): Promise<BalanceInfo> { async GetBalance(): Promise<BalanceInfo> {
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
@ -332,9 +358,9 @@ export default class {
return { confirmedBalance: Number(confirmedBalance), unconfirmedBalance: Number(unconfirmedBalance), totalBalance: Number(totalBalance), channelsBalance } return { confirmedBalance: Number(confirmedBalance), unconfirmedBalance: Number(unconfirmedBalance), totalBalance: Number(totalBalance), channelsBalance }
} }
async GetForwardingHistory(indexOffset: number): Promise<{ fee: number, chanIdIn: string, chanIdOut: string, timestampNs: number, offset: number }[]> { async GetForwardingHistory(indexOffset: number, startTime = 0): Promise<ForwardingHistoryResponse> {
const { response } = await this.lightning.forwardingHistory({ indexOffset, numMaxEvents: 0, startTime: 0n, endTime: 0n, peerAliasLookup: false }, DeadLineMetadata()) const { response } = await this.lightning.forwardingHistory({ indexOffset, numMaxEvents: 0, startTime: BigInt(startTime), endTime: 0n, peerAliasLookup: false }, DeadLineMetadata())
return response.forwardingEvents.map(e => ({ fee: Number(e.fee), chanIdIn: e.chanIdIn, chanIdOut: e.chanIdOut, timestampNs: Number(e.timestampNs), offset: response.lastOffsetIndex })) return response
} }
async GetAllPaidInvoices(max: number) { async GetAllPaidInvoices(max: number) {
@ -346,29 +372,37 @@ export default class {
return res.response return res.response
} }
async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number): Promise<string> { async ConnectPeer(addr: { pubkey: string, host: string }) {
await this.Health() const res = await this.lightning.connectPeer({
addr,
perm: true,
timeout: 0n
}, DeadLineMetadata())
return res.response
}
async ListPeers() {
const res = await this.lightning.listPeers({ latestError: true }, DeadLineMetadata())
return res.response
}
async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number) {
const abortController = new AbortController() const abortController = new AbortController()
const req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats) const req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats)
const stream = this.lightning.openChannel(req, { abort: abortController.signal }) const stream = this.lightning.openChannel(req, { abort: abortController.signal })
return new Promise((res, rej) => { return new Promise((res, rej) => {
stream.responses.onMessage(message => { stream.responses.onMessage(message => {
console.log("message", message)
switch (message.update.oneofKind) { switch (message.update.oneofKind) {
case 'chanPending': case 'chanPending':
abortController.abort()
res(Buffer.from(message.pendingChanId).toString('base64')) res(Buffer.from(message.pendingChanId).toString('base64'))
break break
default:
abortController.abort()
rej("unexpected state response: " + message.update.oneofKind)
} }
}) })
stream.responses.onError(error => { stream.responses.onError(error => {
console.log("error", error)
rej(error) rej(error)
}) })
}) })
} }
} }

View file

@ -1,142 +0,0 @@
//const grpc = require('@grpc/grpc-js');
import { credentials, Metadata } from '@grpc/grpc-js'
import { GrpcTransport } from "@protobuf-ts/grpc-transport";
import fs from 'fs'
import crypto from 'crypto'
import * as Types from '../../../proto/autogenerated/ts/types.js'
import { LightningClient } from '../../../proto/lnd/lightning.client.js'
import { InvoicesClient } from '../../../proto/lnd/invoices.client.js'
import { RouterClient } from '../../../proto/lnd/router.client.js'
import { GetInfoResponse, AddressType, NewAddressResponse, AddInvoiceResponse, Invoice_InvoiceState, PayReq, Payment_PaymentStatus, Payment, PaymentFailureReason, SendCoinsResponse, EstimateFeeResponse, TransactionDetails, ClosedChannelsResponse, ListChannelsResponse, PendingChannelsResponse, ListInvoiceResponse, ListPaymentsResponse } from '../../../proto/lnd/lightning.js'
import { OpenChannelReq } from './openChannelReq.js';
import { AddInvoiceReq } from './addInvoiceReq.js';
import { PayInvoiceReq } from './payInvoiceReq.js';
import { SendCoinsReq } from './sendCoinsReq.js';
import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, BalanceInfo } from './settings.js';
import { getLogger } from '../helpers/logger.js';
export default class {
invoicesAwaiting: Record<string /* invoice */, { value: number, memo: string, expiryUnix: number }> = {}
settings: LndSettings
abortController = new AbortController()
addressPaidCb: AddressPaidCb
invoicePaidCb: InvoicePaidCb
constructor(settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb) {
this.settings = settings
this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb
}
async SetMockInvoiceAsPaid(invoice: string, amount: number): Promise<void> {
const decoded = await this.DecodeInvoice(invoice)
if (decoded.numSatoshis && amount) {
throw new Error("non zero amount provided to pay invoice but invoice has value already")
}
this.invoicePaidCb(invoice, decoded.numSatoshis || amount, false)
delete this.invoicesAwaiting[invoice]
}
Stop() { }
async Warmup() { }
async ListClosedChannels(): Promise<ClosedChannelsResponse> { throw new Error("ListClosedChannels disabled in mock mode") }
async ListChannels(): Promise<ListChannelsResponse> { throw new Error("ListChannels disabled in mock mode") }
async ListPendingChannels(): Promise<PendingChannelsResponse> { throw new Error("ListPendingChannels disabled in mock mode") }
async GetForwardingHistory(indexOffset: number): Promise<{ fee: number, chanIdIn: string, chanIdOut: string, timestampNs: number, offset: number }[]> { throw new Error("GetForwardingHistory disabled in mock mode") }
async GetInfo(): Promise<NodeInfo> {
return { alias: "mock", syncedToChain: true, syncedToGraph: true, blockHeight: 1, blockHash: "" }
}
async Health(): Promise<void> { }
async NewAddress(addressType: Types.AddressType): Promise<NewAddressResponse> {
throw new Error("NewAddress disabled in mock mode")
}
async NewInvoice(value: number, memo: string, expiry: number): Promise<Invoice> {
const mockInvoice = "lnbcrtmockin" + crypto.randomBytes(32).toString('hex')
this.invoicesAwaiting[mockInvoice] = { value, memo, expiryUnix: expiry + Date.now() / 1000 }
return { payRequest: mockInvoice }
}
async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> {
if (paymentRequest.startsWith('lnbcrtmockout')) {
const amt = this.decodeOutboundInvoice(paymentRequest)
return { numSatoshis: amt, paymentHash: paymentRequest }
}
const i = this.invoicesAwaiting[paymentRequest]
if (!i) {
throw new Error("invoice not found")
}
return { numSatoshis: i.value, paymentHash: paymentRequest }
}
GetFeeLimitAmount(amount: number): number {
return Math.ceil(amount * this.settings.feeRateLimit + this.settings.feeFixedLimit);
}
GetMaxWithinLimit(amount: number): number {
return Math.max(0, Math.floor(amount * (1 - this.settings.feeRateLimit) - this.settings.feeFixedLimit))
}
decodeOutboundInvoice(invoice: string): number {
if (!invoice.startsWith('lnbcrtmockout')) {
throw new Error("invalid mock invoice provided for payment")
}
const amt = invoice.substring('lnbcrtmockout'.length).split("__")[0]
if (isNaN(+amt)) {
throw new Error("invalid mock invoice provided for payment")
}
return +amt
}
async PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<PaidInvoice> {
const log = getLogger({})
log('payng', invoice)
await new Promise(res => setTimeout(res, 200))
const amt = this.decodeOutboundInvoice(invoice)
log('paid', invoice)
return { feeSat: 1, paymentPreimage: "all_good", valueSat: amt || amount }
}
async ChannelBalance(): Promise<{ local: number, remote: number }> {
return { local: 100 * 1000 * 1000, remote: 100 * 1000 * 1000 }
}
async EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse> {
throw new Error("EstimateChainFees disabled in mock mode")
}
async PayAddress(address: string, amount: number, satPerVByte: number, label = ""): Promise<SendCoinsResponse> {
throw new Error("PayAddress disabled in mock mode")
}
async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number): Promise<string> {
throw new Error("OpenChannel disabled in mock mode")
}
async GetTransactions(startHeight: number): Promise<TransactionDetails> {
throw new Error("GetTransactions disabled in mock mode")
}
GetBalance(): Promise<BalanceInfo> {
throw new Error("GetBalance disabled in mock mode")
}
async GetAllPaidInvoices(max: number): Promise<ListInvoiceResponse> {
throw new Error("not implemented")
}
async GetAllPayments(max: number): Promise<ListPaymentsResponse> {
throw new Error("not implemented")
}
LockOutgoingOperations() {
throw new Error("not implemented")
}
UnlockOutgoingOperations() {
throw new Error("not implemented")
}
}

View file

@ -1,4 +1,4 @@
import { OpenChannelRequest } from "../../../proto/lnd/lightning"; import { CommitmentType, OpenChannelRequest } from "../../../proto/lnd/lightning.js";
export const OpenChannelReq = (destination: string, closeAddress: string, fundingAmount: number, pushSats: number): OpenChannelRequest => ({ export const OpenChannelReq = (destination: string, closeAddress: string, fundingAmount: number, pushSats: number): OpenChannelRequest => ({
nodePubkey: Buffer.from(destination, 'hex'), nodePubkey: Buffer.from(destination, 'hex'),
@ -9,22 +9,22 @@ export const OpenChannelReq = (destination: string, closeAddress: string, fundin
satPerVbyte: 0n, // TBD satPerVbyte: 0n, // TBD
private: false, private: false,
minConfs: 0, // TBD minConfs: 0, // TBD
baseFee: 0n, // TBD baseFee: 1n, // TBD
feeRate: 0n, // TBD feeRate: 1n, // TBD
targetConf: 0, targetConf: 0,
zeroConf: false, zeroConf: false,
maxLocalCsv: 0, maxLocalCsv: 0,
remoteCsvDelay: 0, remoteCsvDelay: 0,
spendUnconfirmed: false, spendUnconfirmed: false,
minHtlcMsat: 0n, minHtlcMsat: 1n,
remoteChanReserveSat: 0n, remoteChanReserveSat: 10000n,
remoteMaxHtlcs: 0, remoteMaxHtlcs: 483,
remoteMaxValueInFlightMsat: 0n, remoteMaxValueInFlightMsat: 990000000n,
useBaseFee: false, useBaseFee: true,
useFeeRate: false, useFeeRate: true,
// Default stuff // Default stuff
commitmentType: 0, commitmentType: CommitmentType.ANCHORS,
scidAlias: false, scidAlias: false,
nodePubkeyString: "", nodePubkeyString: "",
satPerByte: 0n, satPerByte: 0n,

View file

@ -41,6 +41,8 @@ export type NodeInfo = {
syncedToGraph: boolean syncedToGraph: boolean
blockHeight: number blockHeight: number
blockHash: string blockHash: string
identityPubkey: string
uris: string[]
} }
export type Invoice = { export type Invoice = {
payRequest: string payRequest: string

View file

@ -19,11 +19,12 @@ export default class {
settings: MainSettings settings: MainSettings
paymentManager: PaymentManager paymentManager: PaymentManager
nPubLinkingTokens = new Map<string, NsecLinkingData>(); nPubLinkingTokens = new Map<string, NsecLinkingData>();
linkingTokenInterval: NodeJS.Timeout
constructor(storage: Storage, settings: MainSettings, paymentManager: PaymentManager) { constructor(storage: Storage, settings: MainSettings, paymentManager: PaymentManager) {
this.storage = storage this.storage = storage
this.settings = settings this.settings = settings
this.paymentManager = paymentManager this.paymentManager = paymentManager
setInterval(() => { this.linkingTokenInterval = setInterval(() => {
const now = Date.now(); const now = Date.now();
for (let [token, data] of this.nPubLinkingTokens) { for (let [token, data] of this.nPubLinkingTokens) {
if (data.expiry <= now) { if (data.expiry <= now) {
@ -35,6 +36,9 @@ export default class {
} }
}, 60 * 1000); // 1 minute }, 60 * 1000); // 1 minute
} }
Stop() {
clearInterval(this.linkingTokenInterval)
}
SignAppToken(appId: string): string { SignAppToken(appId: string): string {
return jwt.sign({ appId }, this.settings.jwtSecret); return jwt.sign({ appId }, this.settings.jwtSecret);
} }

View file

@ -5,7 +5,7 @@ import ProductManager from './productManager.js'
import ApplicationManager from './applicationManager.js' import ApplicationManager from './applicationManager.js'
import PaymentManager, { PendingTx } from './paymentManager.js' import PaymentManager, { PendingTx } from './paymentManager.js'
import { MainSettings } from './settings.js' import { MainSettings } from './settings.js'
import NewLightningHandler, { LightningHandler } from "../lnd/index.js" import LND from "../lnd/lnd.js"
import { AddressPaidCb, HtlcCb, InvoicePaidCb, NewBlockCb } from "../lnd/settings.js" import { AddressPaidCb, HtlcCb, InvoicePaidCb, NewBlockCb } from "../lnd/settings.js"
import { getLogger, PubLogger } from "../helpers/logger.js" import { getLogger, PubLogger } from "../helpers/logger.js"
import AppUserManager from "./appUserManager.js" import AppUserManager from "./appUserManager.js"
@ -26,7 +26,7 @@ type UserOperationsSub = {
export default class { export default class {
storage: Storage storage: Storage
lnd: LightningHandler lnd: LND
settings: MainSettings settings: MainSettings
userOperationsSub: UserOperationsSub | null = null userOperationsSub: UserOperationsSub | null = null
productManager: ProductManager productManager: ProductManager
@ -41,7 +41,7 @@ export default class {
this.settings = settings this.settings = settings
this.storage = storage this.storage = storage
this.lnd = NewLightningHandler(settings.lndSettings, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb) this.lnd = new LND(settings.lndSettings, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb)
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.addressPaidCb, this.invoicePaidCb) this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings, this.addressPaidCb, this.invoicePaidCb)
@ -50,6 +50,11 @@ export default class {
this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager) this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager)
} }
Stop() {
this.lnd.Stop()
this.applicationManager.Stop()
this.paymentManager.Stop()
}
attachNostrSend(f: NostrSend) { attachNostrSend(f: NostrSend) {
this.nostrSend = f this.nostrSend = f

View file

@ -4,7 +4,7 @@ import Storage from '../storage/index.js'
import * as Types from '../../../proto/autogenerated/ts/types.js' import * as Types from '../../../proto/autogenerated/ts/types.js'
import { MainSettings } from './settings.js' import { MainSettings } from './settings.js'
import { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorage.js' import { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorage.js'
import { LightningHandler } from '../lnd/index.js' import LND from '../lnd/lnd.js'
import { Application } from '../storage/entity/Application.js' import { Application } from '../storage/entity/Application.js'
import { getLogger } from '../helpers/logger.js' import { getLogger } from '../helpers/logger.js'
import { UserReceivingAddress } from '../storage/entity/UserReceivingAddress.js' import { UserReceivingAddress } from '../storage/entity/UserReceivingAddress.js'
@ -39,14 +39,15 @@ const defaultLnurlPayMetadata = `[["text/plain", "lnurl pay to Lightning.pub"]]`
const confInOne = 1000 * 1000 const confInOne = 1000 * 1000
const confInTwo = 100 * 1000 * 1000 const confInTwo = 100 * 1000 * 1000
export default class { export default class {
storage: Storage storage: Storage
settings: MainSettings settings: MainSettings
lnd: LightningHandler lnd: LND
addressPaidCb: AddressPaidCb addressPaidCb: AddressPaidCb
invoicePaidCb: InvoicePaidCb invoicePaidCb: InvoicePaidCb
log = getLogger({ appName: "PaymentManager" }) log = getLogger({ appName: "PaymentManager" })
watchDog: Watchdog watchDog: Watchdog
constructor(storage: Storage, lnd: LightningHandler, settings: MainSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) { constructor(storage: Storage, lnd: LND, settings: MainSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
this.storage = storage this.storage = storage
this.settings = settings this.settings = settings
this.lnd = lnd this.lnd = lnd
@ -54,6 +55,9 @@ export default class {
this.addressPaidCb = addressPaidCb this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb this.invoicePaidCb = invoicePaidCb
} }
Stop() {
this.watchDog.Stop()
}
getServiceFee(action: Types.UserOperationType, amount: number, appUser: boolean): number { getServiceFee(action: Types.UserOperationType, amount: number, appUser: boolean): number {
switch (action) { switch (action) {
@ -281,7 +285,21 @@ export default class {
return `${this.settings.serviceUrl}/api/guest/lnurl_withdraw/info?k1=${k1}` return `${this.settings.serviceUrl}/api/guest/lnurl_withdraw/info?k1=${k1}`
} }
isDefaultServiceUrl(): boolean {
if (
this.settings.serviceUrl.includes("localhost")
||
this.settings.serviceUrl.includes("127.0.0.1")
) {
return true
}
return false;
}
async GetLnurlWithdrawLink(ctx: Types.UserContext): Promise<Types.LnurlLinkResponse> { async GetLnurlWithdrawLink(ctx: Types.UserContext): Promise<Types.LnurlLinkResponse> {
if(this.isDefaultServiceUrl()) {
throw new Error("Lnurl not enabled. Make sure to set SERVICE_URL env variable")
}
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, 'balanceCheck', app) const key = await this.storage.paymentStorage.AddUserEphemeralKey(ctx.user_id, 'balanceCheck', app)
return { return {
@ -327,6 +345,9 @@ export default class {
} }
async GetLnurlPayLink(ctx: Types.UserContext): Promise<Types.LnurlLinkResponse> { async GetLnurlPayLink(ctx: Types.UserContext): Promise<Types.LnurlLinkResponse> {
if(this.isDefaultServiceUrl()) {
throw new Error("Lnurl not enabled. Make sure to set SERVICE_URL env variable")
}
getLogger({})("getting lnurl pay link") 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)
@ -339,6 +360,9 @@ export default class {
} }
async GetLnurlPayInfoFromUser(userId: string, linkedApplication: Application, baseUrl?: string): Promise<Types.LnurlPayInfoResponse> { async GetLnurlPayInfoFromUser(userId: string, linkedApplication: Application, baseUrl?: string): Promise<Types.LnurlPayInfoResponse> {
if(this.isDefaultServiceUrl()) {
throw new Error("Lnurl not enabled. Make sure to set SERVICE_URL env variable")
}
const payK1 = await this.storage.paymentStorage.AddUserEphemeralKey(userId, 'pay', linkedApplication) const payK1 = await this.storage.paymentStorage.AddUserEphemeralKey(userId, 'pay', linkedApplication)
const url = baseUrl ? baseUrl : `${this.settings.serviceUrl}/api/guest/lnurl_pay/handle` const url = baseUrl ? baseUrl : `${this.settings.serviceUrl}/api/guest/lnurl_pay/handle`
const { remote } = await this.lnd.ChannelBalance() const { remote } = await this.lnd.ChannelBalance()
@ -354,6 +378,9 @@ export default class {
} }
async GetLnurlPayInfo(payInfoK1: string): Promise<Types.LnurlPayInfoResponse> { async GetLnurlPayInfo(payInfoK1: string): Promise<Types.LnurlPayInfoResponse> {
if(this.isDefaultServiceUrl()) {
throw new Error("Lnurl not enabled. Make sure to set SERVICE_URL env variable")
}
const key = await this.storage.paymentStorage.UseUserEphemeralKey(payInfoK1, 'pay', true) const key = await this.storage.paymentStorage.UseUserEphemeralKey(payInfoK1, 'pay', true)
if (!key.linkedApplication) { if (!key.linkedApplication) {
throw new Error("invalid lnurl request") throw new Error("invalid lnurl request")

View file

@ -1,5 +1,5 @@
import Storage from '../storage/index.js' import Storage from '../storage/index.js'
import { LightningHandler } from "../lnd/index.js" import LND from "../lnd/lnd.js"
import { LoggedEvent } from '../storage/eventsLog.js' import { LoggedEvent } from '../storage/eventsLog.js'
import { Invoice, Payment } from '../../../proto/lnd/lightning'; import { Invoice, Payment } from '../../../proto/lnd/lightning';
import { getLogger } from '../helpers/logger.js'; import { getLogger } from '../helpers/logger.js';
@ -12,7 +12,7 @@ type Reason = UniqueDecrementReasons | UniqueIncrementReasons | CommonReasons
const incrementTwiceAllowed = ['fees', 'ban'] const incrementTwiceAllowed = ['fees', 'ban']
export default class SanityChecker { export default class SanityChecker {
storage: Storage storage: Storage
lnd: LightningHandler lnd: LND
events: LoggedEvent[] = [] events: LoggedEvent[] = []
invoices: Invoice[] = [] invoices: Invoice[] = []
@ -22,7 +22,7 @@ export default class SanityChecker {
decrementEvents: Record<string, { userId: string, refund: number, failure: boolean }> = {} decrementEvents: Record<string, { userId: string, refund: number, failure: boolean }> = {}
log = getLogger({ appName: "SanityChecker" }) log = getLogger({ appName: "SanityChecker" })
users: Record<string, { ts: number, updatedBalance: number }> = {} users: Record<string, { ts: number, updatedBalance: number }> = {}
constructor(storage: Storage, lnd: LightningHandler) { constructor(storage: Storage, lnd: LND) {
this.storage = storage this.storage = storage
this.lnd = lnd this.lnd = lnd
} }

View file

@ -22,6 +22,12 @@ export type MainSettings = {
skipSanityCheck: boolean skipSanityCheck: boolean
disableExternalPayments: boolean disableExternalPayments: boolean
} }
export type BitcoinCoreSettings = {
port: number
user: string
pass: string
}
export type TestSettings = MainSettings & { lndSettings: { otherNode: NodeSettings, thirdNode: NodeSettings, fourthNode: NodeSettings }, bitcoinCoreSettings: BitcoinCoreSettings }
export const LoadMainSettingsFromEnv = (): MainSettings => { export const LoadMainSettingsFromEnv = (): MainSettings => {
return { return {
watchDogSettings: LoadWatchdogSettingsFromEnv(), watchDogSettings: LoadWatchdogSettingsFromEnv(),
@ -36,7 +42,7 @@ export const LoadMainSettingsFromEnv = (): MainSettings => {
outgoingAppUserInvoiceFee: EnvMustBeInteger("OUTGOING_INVOICE_FEE_USER_BPS") / 10000, outgoingAppUserInvoiceFee: EnvMustBeInteger("OUTGOING_INVOICE_FEE_USER_BPS") / 10000,
userToUserFee: EnvMustBeInteger("TX_FEE_INTERNAL_USER_BPS") / 10000, userToUserFee: EnvMustBeInteger("TX_FEE_INTERNAL_USER_BPS") / 10000,
appToUserFee: EnvMustBeInteger("TX_FEE_INTERNAL_ROOT_BPS") / 10000, appToUserFee: EnvMustBeInteger("TX_FEE_INTERNAL_ROOT_BPS") / 10000,
serviceUrl: EnvMustBeNonEmptyString("SERVICE_URL"), serviceUrl: process.env.SERVICE_URL || `http://localhost:${EnvMustBeInteger("PORT")}`,
servicePort: EnvMustBeInteger("PORT"), servicePort: EnvMustBeInteger("PORT"),
recordPerformance: process.env.RECORD_PERFORMANCE === 'true' || false, recordPerformance: process.env.RECORD_PERFORMANCE === 'true' || false,
skipSanityCheck: process.env.SKIP_SANITY_CHECK === 'true' || false, skipSanityCheck: process.env.SKIP_SANITY_CHECK === 'true' || false,
@ -44,7 +50,7 @@ export const LoadMainSettingsFromEnv = (): MainSettings => {
} }
} }
export const LoadTestSettingsFromEnv = (): MainSettings & { lndSettings: { otherNode: NodeSettings, thirdNode: NodeSettings } } => { export const LoadTestSettingsFromEnv = (): TestSettings => {
const eventLogPath = `logs/eventLogV2Test${Date.now()}.csv` const eventLogPath = `logs/eventLogV2Test${Date.now()}.csv`
const settings = LoadMainSettingsFromEnv() const settings = LoadMainSettingsFromEnv()
return { return {
@ -61,8 +67,18 @@ export const LoadTestSettingsFromEnv = (): MainSettings & { lndSettings: { other
lndAddr: EnvMustBeNonEmptyString("LND_THIRD_ADDR"), lndAddr: EnvMustBeNonEmptyString("LND_THIRD_ADDR"),
lndCertPath: EnvMustBeNonEmptyString("LND_THIRD_CERT_PATH"), lndCertPath: EnvMustBeNonEmptyString("LND_THIRD_CERT_PATH"),
lndMacaroonPath: EnvMustBeNonEmptyString("LND_THIRD_MACAROON_PATH") lndMacaroonPath: EnvMustBeNonEmptyString("LND_THIRD_MACAROON_PATH")
},
fourthNode: {
lndAddr: EnvMustBeNonEmptyString("LND_FOURTH_ADDR"),
lndCertPath: EnvMustBeNonEmptyString("LND_FOURTH_CERT_PATH"),
lndMacaroonPath: EnvMustBeNonEmptyString("LND_FOURTH_MACAROON_PATH")
} }
}, },
skipSanityCheck: true skipSanityCheck: true,
bitcoinCoreSettings: {
port: EnvMustBeInteger("BITCOIN_CORE_PORT"),
user: EnvMustBeNonEmptyString("BITCOIN_CORE_USER"),
pass: EnvMustBeNonEmptyString("BITCOIN_CORE_PASS")
}
} }
} }

View file

@ -1,6 +1,7 @@
import { EnvCanBeInteger } from "../helpers/envParser.js"; import { EnvCanBeInteger } from "../helpers/envParser.js";
import FunctionQueue from "../helpers/functionQueue.js";
import { getLogger } from "../helpers/logger.js"; import { getLogger } from "../helpers/logger.js";
import { LightningHandler } from "../lnd/index.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'
export type WatchdogSettings = { export type WatchdogSettings = {
@ -12,20 +13,24 @@ export const LoadWatchdogSettingsFromEnv = (test = false): WatchdogSettings => {
} }
} }
export class Watchdog { export class Watchdog {
queue: FunctionQueue<void>
initialLndBalance: number; initialLndBalance: number;
initialUsersBalance: number; initialUsersBalance: number;
lnd: LightningHandler; startedAtUnix: number;
latestIndexOffset: number;
accumulatedHtlcFees: number;
lnd: LND;
settings: WatchdogSettings; settings: WatchdogSettings;
storage: Storage; storage: Storage;
latestCheckStart = 0 latestCheckStart = 0
log = getLogger({ appName: "watchdog" }) log = getLogger({ appName: "watchdog" })
enabled = false ready = false
interval: NodeJS.Timer; interval: NodeJS.Timer;
constructor(settings: WatchdogSettings, lnd: LightningHandler, storage: Storage) { constructor(settings: WatchdogSettings, lnd: LND, storage: Storage) {
this.lnd = lnd; this.lnd = lnd;
this.settings = settings; this.settings = settings;
this.storage = storage; this.storage = storage;
this.queue = new FunctionQueue("watchdog::queue", () => this.StartCheck())
} }
Stop() { Stop() {
@ -35,10 +40,13 @@ export class Watchdog {
} }
Start = async () => { Start = async () => {
this.startedAtUnix = Math.floor(Date.now() / 1000)
const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance() const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
this.initialLndBalance = await this.getTotalLndBalance(totalUsersBalance) this.initialLndBalance = await this.getTotalLndBalance(totalUsersBalance)
this.initialUsersBalance = totalUsersBalance this.initialUsersBalance = totalUsersBalance
this.enabled = true const fwEvents = await this.lnd.GetForwardingHistory(0, this.startedAtUnix)
this.latestIndexOffset = fwEvents.lastOffsetIndex
this.accumulatedHtlcFees = 0
this.interval = setInterval(() => { this.interval = setInterval(() => {
if (this.latestCheckStart + (1000 * 60) < Date.now()) { if (this.latestCheckStart + (1000 * 60) < Date.now()) {
@ -46,26 +54,30 @@ export class Watchdog {
this.PaymentRequested() this.PaymentRequested()
} }
}, 1000 * 60) }, 1000 * 60)
this.ready = true
} }
getTotalLndBalance = async (usersTotal: number) => { updateAccumulatedHtlcFees = async () => {
const localLog = getLogger({ appName: "debugLndBalancev2" }) const fwEvents = await this.lnd.GetForwardingHistory(this.latestIndexOffset, this.startedAtUnix)
const { confirmedBalance, channelsBalance } = await this.lnd.GetBalance() this.latestIndexOffset = fwEvents.lastOffsetIndex
this.log(confirmedBalance, "sats in chain wallet") fwEvents.forwardingEvents.forEach((event) => {
localLog({ c: channelsBalance, u: usersTotal }) this.accumulatedHtlcFees += Number(event.fee)
let totalBalance = confirmedBalance
channelsBalance.forEach(c => {
let totalBalanceInHtlcs = 0
c.htlcs.forEach(htlc => {
if (htlc.incoming) {
totalBalanceInHtlcs += htlc.amount
} else {
//totalBalanceInHtlcs -= htlc.amount
}
})
totalBalance += c.localBalanceSats + totalBalanceInHtlcs
}) })
return totalBalance
}
getTotalLndBalance = async (usersTotal: number) => {
const walletBalance = await this.lnd.GetWalletBalance()
this.log(Number(walletBalance.confirmedBalance), "sats in chain wallet")
const channelsBalance = await this.lnd.GetChannelBalance()
getLogger({ appName: "debugLndBalancev3" })({ w: walletBalance, c: channelsBalance, u: usersTotal, f: this.accumulatedHtlcFees })
const localChannelsBalance = Number(channelsBalance.localBalance?.sat || 0)
const unsettledLocalBalance = Number(channelsBalance.unsettledLocalBalance?.sat || 0)
return Number(walletBalance.confirmedBalance) + localChannelsBalance + unsettledLocalBalance
} }
checkBalanceUpdate = (deltaLnd: number, deltaUsers: number) => { checkBalanceUpdate = (deltaLnd: number, deltaUsers: number) => {
@ -119,16 +131,12 @@ export class Watchdog {
return false return false
} }
PaymentRequested = async () => { StartCheck = async () => {
this.log("Payment requested, checking balance")
if (!this.enabled) {
this.log("WARNING! Watchdog not enabled, skipping balance check")
return
}
this.latestCheckStart = Date.now() this.latestCheckStart = Date.now()
await this.updateAccumulatedHtlcFees()
const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance() const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
const totalLndBalance = await this.getTotalLndBalance(totalUsersBalance) const totalLndBalance = await this.getTotalLndBalance(totalUsersBalance)
const deltaLnd = totalLndBalance - this.initialLndBalance 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 = this.checkBalanceUpdate(deltaLnd, deltaUsers)
if (deny) { if (deny) {
@ -139,6 +147,16 @@ export class Watchdog {
this.lnd.UnlockOutgoingOperations() this.lnd.UnlockOutgoingOperations()
} }
PaymentRequested = async () => {
this.log("Payment requested, checking balance")
if (!this.ready) {
throw new Error("Watchdog not ready")
}
return new Promise<void>((res, rej) => {
this.queue.Run({ res, rej })
})
}
checkDeltas = (deltaLnd: number, deltaUsers: number): DeltaCheckResult => { checkDeltas = (deltaLnd: number, deltaUsers: number): DeltaCheckResult => {
if (deltaLnd < 0) { if (deltaLnd < 0) {
if (deltaUsers < 0) { if (deltaUsers < 0) {

View file

@ -5,15 +5,15 @@ import { HtlcEvent, HtlcEvent_EventType } from '../../../proto/lnd/router.js'
import { BalanceInfo } from '../lnd/settings.js' import { BalanceInfo } from '../lnd/settings.js'
import { BalanceEvent } from '../storage/entity/BalanceEvent.js' import { BalanceEvent } from '../storage/entity/BalanceEvent.js'
import { ChannelBalanceEvent } from '../storage/entity/ChannelsBalanceEvent.js' import { ChannelBalanceEvent } from '../storage/entity/ChannelsBalanceEvent.js'
import { LightningHandler } from '../lnd/index.js' import LND from '../lnd/lnd.js'
import HtlcTracker from './htlcTracker.js' import HtlcTracker from './htlcTracker.js'
const maxEvents = 100_000 const maxEvents = 100_000
export default class Handler { export default class Handler {
storage: Storage storage: Storage
lnd: LightningHandler lnd: LND
htlcTracker: HtlcTracker htlcTracker: HtlcTracker
metrics: Types.UsageMetric[] = [] metrics: Types.UsageMetric[] = []
constructor(storage: Storage, lnd: LightningHandler) { constructor(storage: Storage, lnd: LND) {
this.storage = storage this.storage = storage
this.lnd = lnd this.lnd = lnd
this.htlcTracker = new HtlcTracker(this.storage) this.htlcTracker = new HtlcTracker(this.storage)
@ -40,7 +40,8 @@ export default class Handler {
async FetchLatestForwardingEvents() { async FetchLatestForwardingEvents() {
const latestIndex = await this.storage.metricsStorage.GetLatestForwardingIndexOffset() const latestIndex = await this.storage.metricsStorage.GetLatestForwardingIndexOffset()
const forwards = await this.lnd.GetForwardingHistory(latestIndex) const res = await this.lnd.GetForwardingHistory(latestIndex)
const forwards = res.forwardingEvents.map(e => ({ fee: Number(e.fee), chanIdIn: e.chanIdIn, chanIdOut: e.chanIdOut, timestampNs: e.timestampNs.toString(), offset: res.lastOffsetIndex }))
await Promise.all(forwards.map(async f => { await Promise.all(forwards.map(async f => {
await this.storage.metricsStorage.IncrementChannelRouting(f.chanIdIn, { forward_fee_as_input: f.fee, latest_index_offset: f.offset }) await this.storage.metricsStorage.IncrementChannelRouting(f.chanIdIn, { forward_fee_as_input: f.fee, latest_index_offset: f.offset })
await this.storage.metricsStorage.IncrementChannelRouting(f.chanIdOut, { forward_fee_as_output: f.fee, latest_index_offset: f.offset }) await this.storage.metricsStorage.IncrementChannelRouting(f.chanIdOut, { forward_fee_as_output: f.fee, latest_index_offset: f.offset })

40
src/tests/.env.test Normal file
View file

@ -0,0 +1,40 @@
LND_ADDRESS=127.0.0.1:10001 #alice
LND_CERT_PATH=alice-tls.cert #alice
LND_MACAROON_PATH=alice-admin.macaroon
DATABASE_FILE=db.sqlite
JWT_SECRET=bigsecrethere
ALLOW_BALANCE_MIGRATION=true
OUTBOUND_MAX_FEE_BPS=60
OUTBOUND_MAX_FEE_EXTRA_SATS=100
INCOMING_CHAIN_FEE_ROOT_BPS=0
OUTGOING_CHAIN_FEE_ROOT_BPS=60 #this is applied only to withdrawls from application wallets
INCOMING_INVOICE_FEE_ROOT_BPS=0
OUTGOING_INVOICE_FEE_ROOT_BPS=60 #this is applied only to withdrawals from application wallets
INCOMING_INVOICE_FEE_USER_BPS=0 #defined by app this is just default
OUTGOING_INVOICE_FEE_USER_BPS=60 #defined by app this is just default
TX_FEE_INTERNAL_ROOT_BPS=60 #this is applied only to withdrawls from application wallets
TX_FEE_INTERNAL_USER_BPS=60 #defined by app this is just default
NOSTR_RELAYS=wss://strfry.shock.network
SERVICE_URL=http://localhost:8080
ADMIN_TOKEN=thisisadmin
PORT=8080
METRICS_DATABASE_FILE=metrics.sqlite
WATCHDOG_MAX_DIFF_BPS=100
WATCHDOG_MAX_DIFF_SATS=10000
# dave <--> alice <--> carol <--> bob
LND_OTHER_ADDR=127.0.0.1:10002
LND_OTHER_CERT_PATH=bob-tls.cert
LND_OTHER_MACAROON_PATH=bob-admin.macaroon
LND_THIRD_ADDR=127.0.0.1:10003
LND_THIRD_CERT_PATH=carol-tls.cert
LND_THIRD_MACAROON_PATH=carol-admin.macaroon
LND_FOURTH_ADDR=127.0.0.1:10004
LND_FOURTH_CERT_PATH=dave-tls.cert
LND_FOURTH_MACAROON_PATH=dave-admin.macaroon
BITCOIN_CORE_PORT=18443
BITCOIN_CORE_USER=polaruser
BITCOIN_CORE_PASS=polarpass

17
src/tests/DockerFile Normal file
View file

@ -0,0 +1,17 @@
FROM node:10-alpine
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
WORKDIR /home/node/app
COPY package*.json ./
USER node
RUN npm install
COPY env.example .env
COPY --chown=node:node . .
CMD [ "npm", "test" ]

39
src/tests/bitcoinCore.ts Normal file
View file

@ -0,0 +1,39 @@
// @ts-ignore
import BitcoinCore from 'bitcoin-core';
import { TestSettings } from '../services/main/settings';
export class BitcoinCoreWrapper {
core: BitcoinCore
addr: { address: string }
constructor(settings: TestSettings) {
this.core = new BitcoinCore({
//network: 'regtest',
host: '127.0.0.1',
port: `${settings.bitcoinCoreSettings.port}`,
username: settings.bitcoinCoreSettings.user,
password: settings.bitcoinCoreSettings.pass,
// use a long timeout due to the time it takes to mine a lot of blocks
timeout: 5 * 60 * 1000,
})
}
InitAddress = async () => {
this.addr = await this.core.getNewAddress()
}
Init = async () => {
const wallet = await this.core.createWallet('');
console.log({ wallet })
await this.InitAddress()
console.log({ addr: this.addr })
await this.Mine(101)
const info = await this.core.getWalletInfo();
console.log({ info })
}
Mine = async (blocks: number) => {
await this.core.generateToAddress(blocks, this.addr)
}
SendToAddress = async (address: string, amount: number) => {
const tx = await this.core.sendToAddress(address, amount)
console.log({ tx })
}
}

View file

@ -0,0 +1,108 @@
version: '3.3'
services:
backend1:
environment:
USERID: ${USERID:-1000}
GROUPID: ${GROUPID:-1000}
stop_grace_period: 5m
image: polarlightning/bitcoind:26.0
container_name: polar-n2-backend1
hostname: backend1
command: >-
bitcoind -server=1 -regtest=1 -rpcauth=polaruser:5e5e98c21f5c814568f8b55d83b23c1c$$066b03f92df30b11de8e4b1b1cd5b1b4281aa25205bd57df9be82caf97a05526 -debug=1 -zmqpubrawblock=tcp://0.0.0.0:28334 -zmqpubrawtx=tcp://0.0.0.0:28335 -zmqpubhashblock=tcp://0.0.0.0:28336 -txindex=1 -dnsseed=0 -upnp=0 -rpcbind=0.0.0.0 -rpcallowip=0.0.0.0/0 -rpcport=18443 -rest -listen=1 -listenonion=0 -fallbackfee=0.0002 -blockfilterindex=1 -peerblockfilters=1
volumes:
- ./volumes/bitcoind/backend1:/home/bitcoin/.bitcoin
expose:
- '18443'
- '18444'
- '28334'
- '28335'
ports:
- '18443:18443'
- '19444:18444'
- '28334:28334'
- '29335:28335'
alice:
environment:
USERID: ${USERID:-1000}
GROUPID: ${GROUPID:-1000}
stop_grace_period: 2m
image: polarlightning/lnd:0.17.3-beta
container_name: polar-n2-alice
hostname: alice
command: >-
lnd --noseedbackup --trickledelay=5000 --alias=alice --externalip=alice --tlsextradomain=alice --tlsextradomain=polar-n2-alice --tlsextradomain=host.docker.internal --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind --bitcoind.rpchost=polar-n2-backend1 --bitcoind.rpcuser=polaruser --bitcoind.rpcpass=polarpass --bitcoind.zmqpubrawblock=tcp://polar-n2-backend1:28334 --bitcoind.zmqpubrawtx=tcp://polar-n2-backend1:28335
restart: always
volumes:
- ./volumes/lnd/alice:/home/lnd/.lnd
expose:
- '8080'
- '10009'
- '9735'
ports:
# - '8081:8080'
- '10001:10009'
- '9735:9735'
bob:
environment:
USERID: ${USERID:-1000}
GROUPID: ${GROUPID:-1000}
stop_grace_period: 2m
image: polarlightning/lnd:0.17.3-beta
container_name: polar-n2-bob
hostname: bob
command: >-
lnd --noseedbackup --trickledelay=5000 --alias=bob --externalip=bob --tlsextradomain=bob --tlsextradomain=polar-n2-bob --tlsextradomain=host.docker.internal --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind --bitcoind.rpchost=polar-n2-backend1 --bitcoind.rpcuser=polaruser --bitcoind.rpcpass=polarpass --bitcoind.zmqpubrawblock=tcp://polar-n2-backend1:28334 --bitcoind.zmqpubrawtx=tcp://polar-n2-backend1:28335
restart: always
volumes:
- ./volumes/lnd/bob:/home/lnd/.lnd
expose:
- '8080'
- '10009'
- '9735'
ports:
# - '8082:8080'
- '10002:10009'
- '9736:9735'
carol:
environment:
USERID: ${USERID:-1000}
GROUPID: ${GROUPID:-1000}
stop_grace_period: 2m
image: polarlightning/lnd:0.17.3-beta
container_name: polar-n2-carol
hostname: carol
command: >-
lnd --noseedbackup --trickledelay=5000 --alias=carol --externalip=carol --tlsextradomain=carol --tlsextradomain=polar-n2-carol --tlsextradomain=host.docker.internal --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind --bitcoind.rpchost=polar-n2-backend1 --bitcoind.rpcuser=polaruser --bitcoind.rpcpass=polarpass --bitcoind.zmqpubrawblock=tcp://polar-n2-backend1:28334 --bitcoind.zmqpubrawtx=tcp://polar-n2-backend1:28335
restart: always
volumes:
- ./volumes/lnd/carol:/home/lnd/.lnd
expose:
- '8080'
- '10009'
- '9735'
ports:
# - '8083:8080'
- '10003:10009'
- '9737:9735'
dave:
environment:
USERID: ${USERID:-1000}
GROUPID: ${GROUPID:-1000}
stop_grace_period: 2m
image: polarlightning/lnd:0.17.3-beta
container_name: polar-n2-dave
hostname: dave
command: >-
lnd --noseedbackup --trickledelay=5000 --alias=dave --externalip=dave --tlsextradomain=dave --tlsextradomain=polar-n2-dave --tlsextradomain=host.docker.internal --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind --bitcoind.rpchost=polar-n2-backend1 --bitcoind.rpcuser=polaruser --bitcoind.rpcpass=polarpass --bitcoind.zmqpubrawblock=tcp://polar-n2-backend1:28334 --bitcoind.zmqpubrawtx=tcp://polar-n2-backend1:28335
restart: always
volumes:
- ./volumes/lnd/dave:/home/lnd/.lnd
expose:
- '8080'
- '10009'
- '9735'
ports:
# - '8084:8080'
- '10004:10009'
- '9738:9735'

61
src/tests/networkSetup.ts Normal file
View file

@ -0,0 +1,61 @@
import { LoadTestSettingsFromEnv } from "../services/main/settings.js"
import { BitcoinCoreWrapper } from "./bitcoinCore.js"
import LND from '../services/lnd/lnd.js'
export const setupNetwork = async () => {
const settings = LoadTestSettingsFromEnv()
const core = new BitcoinCoreWrapper(settings)
await core.InitAddress()
await core.Mine(1)
const alice = new LND(settings.lndSettings, () => { }, () => { }, () => { }, () => { })
const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, () => { }, () => { }, () => { }, () => { })
await tryUntil<void>(async i => {
const peers = await alice.ListPeers()
if (peers.peers.length > 0) {
return
}
await alice.ConnectPeer({ pubkey: '0232842d81b2423df97aa8a264f8c0811610a736af65afe2e145279f285625c1e4', host: "carol:9735" })
await alice.ConnectPeer({ pubkey: '027c50fde118af534ff27e59da722422d2f3e06505c31e94c1b40c112c48a83b1c', host: "dave:9735" })
}, 15, 2000)
await tryUntil<void>(async i => {
const peers = await bob.ListPeers()
if (peers.peers.length > 0) {
return
}
await bob.ConnectPeer({ pubkey: '0232842d81b2423df97aa8a264f8c0811610a736af65afe2e145279f285625c1e4', host: "carol:9735" })
}, 15, 2000)
await tryUntil<void>(async i => {
const info = await alice.GetInfo()
if (!info.syncedToChain) {
throw new Error("alice not synced to chain")
}
if (!info.syncedToGraph) {
//await lnd.ConnectPeer({})
throw new Error("alice not synced to graph")
}
}, 15, 2000)
await tryUntil<void>(async i => {
const info = await bob.GetInfo()
if (!info.syncedToChain) {
throw new Error("bob not synced to chain")
}
if (!info.syncedToGraph) {
//await lnd.ConnectPeer({})
throw new Error("bob not synced to graph")
}
}, 15, 2000)
}
const tryUntil = async <T>(fn: (attempt: number) => Promise<T>, maxTries: number, interval: number) => {
for (let i = 0; i < maxTries; i++) {
try {
return await fn(i)
} catch (e) {
console.log("tryUntil error", e)
await new Promise(resolve => setTimeout(resolve, interval))
}
}
throw new Error("tryUntil failed")
}

Binary file not shown.

View file

@ -9,7 +9,6 @@ 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 { LightningHandler } from '../services/lnd/index.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
@ -66,8 +65,7 @@ export const SetupTest = async (d: Describe): Promise<TestBase> => {
} }
export const teardown = async (T: TestBase) => { export const teardown = async (T: TestBase) => {
T.main.paymentManager.watchDog.Stop() T.main.Stop()
T.main.lnd.Stop()
T.externalAccessToMainLnd.Stop() T.externalAccessToMainLnd.Stop()
T.externalAccessToOtherLnd.Stop() T.externalAccessToOtherLnd.Stop()
T.externalAccessToThirdLnd.Stop() T.externalAccessToThirdLnd.Stop()

View file

@ -1,16 +1,27 @@
import { globby } from 'globby' import { globby } from 'globby'
import { setupNetwork } from './networkSetup.js'
import { Describe, SetupTest, teardown, TestBase } from './testBase.js' import { Describe, SetupTest, teardown, TestBase } from './testBase.js'
type TestModule = { type TestModule = {
ignore?: boolean ignore?: boolean
dev?: boolean dev?: boolean
default: (T: TestBase) => Promise<void> default: (T: TestBase) => Promise<void>
} }
let failures = 0 let failures = 0
const start = async () => { const getDescribe = (fileName: string): Describe => {
return (message, failure) => {
if (failure) {
failures++
console.error(redConsole, fileName, ": FAILURE ", message, resetConsole)
} else {
console.log(greenConsole, fileName, ":", message, resetConsole)
}
}
}
const files = await globby("**/*.spec.js") const start = async () => {
await setupNetwork()
const files = await globby(["**/*.spec.js", "!**/node_modules/**"])
const modules: { file: string, module: TestModule }[] = [] const modules: { file: string, module: TestModule }[] = []
let devModule = -1 let devModule = -1
for (const file of files) { for (const file of files) {
@ -19,8 +30,7 @@ const start = async () => {
if (module.dev) { if (module.dev) {
console.log("dev module found", file) console.log("dev module found", file)
if (devModule !== -1) { if (devModule !== -1) {
console.error(redConsole, "there are multiple dev modules", resetConsole) throw new Error("there are multiple dev modules")
return
} }
devModule = modules.length - 1 devModule = modules.length - 1
} }
@ -28,16 +38,15 @@ const start = async () => {
if (devModule !== -1) { if (devModule !== -1) {
console.log("running dev module") console.log("running dev module")
await runTestFile(modules[devModule].file, modules[devModule].module) await runTestFile(modules[devModule].file, modules[devModule].module)
return } else {
}
else {
console.log("running all tests") console.log("running all tests")
for (const { file, module } of modules) { for (const { file, module } of modules) {
await runTestFile(file, module) await runTestFile(file, module)
} }
} }
console.log(failures)
if (failures) { if (failures) {
console.error(redConsole, "there have been", `${failures}`, "failures in all tests", resetConsole) throw new Error("there have been " + failures + " failures in all tests")
} else { } else {
console.log(greenConsole, "there have been 0 failures in all tests", resetConsole) console.log(greenConsole, "there have been 0 failures in all tests", resetConsole)
} }
@ -69,16 +78,7 @@ const runTestFile = async (fileName: string, mod: TestModule) => {
} }
} }
const getDescribe = (fileName: string): Describe => {
return (message, failure) => {
if (failure) {
failures++
console.error(redConsole, fileName, ": FAILURE ", message, resetConsole)
} else {
console.log(greenConsole, fileName, ":", message, resetConsole)
}
}
}
const greenConsole = "\x1b[32m" const greenConsole = "\x1b[32m"
const redConsole = "\x1b[31m" const redConsole = "\x1b[31m"
const resetConsole = "\x1b[0m" const resetConsole = "\x1b[0m"

View file

@ -1,7 +1,7 @@
import { defaultInvoiceExpiry } from '../services/storage/paymentStorage.js' import { defaultInvoiceExpiry } from '../services/storage/paymentStorage.js'
import { Describe, expect, expectThrowsAsync, runSanityCheck, safelySetUserBalance, SetupTest, TestBase } from './testBase.js' import { Describe, expect, expectThrowsAsync, runSanityCheck, safelySetUserBalance, SetupTest, TestBase } from './testBase.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) => {
await safelySetUserBalance(T, T.user1, 2000) await safelySetUserBalance(T, T.user1, 2000)
await testSuccessfulU2UPayment(T) await testSuccessfulU2UPayment(T)

View file

@ -1 +0,0 @@
{"include":["**/*.spec.ts"]}