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.
"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:
@ -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)
#### 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"
- [ ] Channel Automation
- [ ] Bootstarp Peering (Passive "LSP")
- [ ] Subscriptions / Notifications
- [ ] Automated Channels
- [ ] Bootstrap Peering (Passive "LSP")
- [ ] Event Notifications
- [ ] Submarine Swaps
- [ ] High-Availabilty / Clustering
Dashboard:
Dashboard Wireframe:
<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:
* The service defaults to port `8080`
* 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
* Commands for your specific OS may differ slightly, Ubuntu/Debian used for example
#### 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
```
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`
6) Create an Application Pool
- A default "wallet" application pool will be automatically created, if you wish to create other app pools:
A default "wallet" pool will be automatically created and keys generated automatically, if you wish to create something other:
* `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'`
```
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.
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

View file

@ -1,52 +1,85 @@
#LND
LND_ADDRESS=127.0.0.1:10009
LND_CERT_PATH=/root/.lnd/tls.cert
LND_MACAROON_PATH=/root/.lnd/data/chain/bitcoin/mainnet/admin.macaroon
# Example configuration for Lightning.Pub
# Copy this file as .env in the Pub folder and uncomment the desired settings to override defaults
# Alternatively, these settings can be passed as environment variables at startup
#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
DATABASE_FILE=db.sqlite
METRICS_DATABASE_FILE=metrics.sqlite
#DATABASE_FILE=db.sqlite
#METRICS_DATABASE_FILE=metrics.sqlite
#LOCAL
ADMIN_TOKEN=
PORT=8080
JWT_SECRET=bigsecrethere
#LOCALHOST
#ADMIN_TOKEN=
#PORT=8080
#JWT_SECRET=
#LIGHTNING
OUTBOUND_MAX_FEE_BPS=60
OUTBOUND_MAX_FEE_EXTRA_SATS=100
# Maximum amount in network fees passed to LND when it pays an external invoice
# 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
INCOMING_CHAIN_FEE_ROOT_BPS=0
INCOMING_INVOICE_FEE_ROOT_BPS=0
OUTGOING_CHAIN_FEE_ROOT_BPS=60 #applied to application debits
OUTGOING_INVOICE_FEE_ROOT_BPS=60 #applied to application debits
TX_FEE_INTERNAL_ROOT_BPS=60 #applied to inter-application txns
# Applied to either debits or credits and sent to an admin account
# BPS are basis points, 100 BPS = 1%
#INCOMING_CHAIN_FEE_ROOT_BPS=0
#INCOMING_INVOICE_FEE_ROOT_BPS=0
# 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
INCOMING_INVOICE_FEE_USER_BPS=0 #app default
OUTGOING_INVOICE_FEE_USER_BPS=60 #app default
TX_FEE_INTERNAL_USER_BPS=60 #intra-application tx default
# An extra fee applied at the app level and sent to the application owner
#INCOMING_INVOICE_FEE_USER_BPS=0
#OUTGOING_INVOICE_FEE_USER_BPS=0
#TX_FEE_INTERNAL_USER_BPS=0
#NOSTR
NOSTR_RELAYS=wss://strfry.shock.network
# Default relay may become rate-limited without a paid subscription
#NOSTR_RELAYS=wss://strfry.shock.network
#LNURL
#Note that a reachable https endpoint for the service to handle lnurl requests is required for lightning address bridges
SERVICE_URL=https://test.lightning.pub
# Optional
# 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
MOCK_LND=false
ALLOW_BALANCE_MIGRATION=false
MIGRATE_DB=false
#SUBSCRIPTION_SERVICES
# Opt-in to cloud relays for LNURL and Nostr
# A small monthly fee supports the developers
# Read more at https://docs.shock.network
#SUBSCRIBER=1
#DEV_OPTS
#MOCK_LND=false
#ALLOW_BALANCE_MIGRATION=false
#MIGRATE_DB=false
#METRICS
RECORD_PERFORMANCE=true
SKIP_SANITY_CHECK=false
DISABLE_EXTERNAL_PAYMENTS=false
#RECORD_PERFORMANCE=true
#SKIP_SANITY_CHECK=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_MAX_DIFF_SATS=10000
# Max difference between users balance and LND balance after each payment
WATCHDOG_MAX_UPDATE_DIFF_SATS=1000
#WATCHDOG SECURITY
# A last line of defense against 0-day drainage attacks
# This will monitor LND separately and terminate sends if a balance discrepency is detected
# This setting defaults to 0 meaning no discrepency will be tolerated
# 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",
"axios": "^0.28.0",
"bech32": "^2.0.0",
"bitcoin-core": "^4.2.0",
"chai": "^4.3.7",
"chai-string": "^1.5.0",
"copyfiles": "^2.4.1",

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 { AddressPaidCb, BalanceInfo, DecodedInvoice, HtlcCb, Invoice, InvoicePaidCb, LndSettings, NewBlockCb, NodeInfo, PaidInvoice } from './settings.js'
import LND from './lnd.js'
import MockLnd from './mock.js'
import { getLogger } from '../helpers/logger.js'
import { LndSettings } from './settings.js'
export const LoadLndSettingsFromEnv = (): LndSettings => {
const lndAddr = EnvMustBeNonEmptyString("LND_ADDRESS")
const lndCertPath = EnvMustBeNonEmptyString("LND_CERT_PATH")
@ -14,40 +9,3 @@ export const LoadLndSettingsFromEnv = (): LndSettings => {
const mockLnd = EnvCanBeBoolean("MOCK_LND")
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 { RouterClient } from '../../../proto/lnd/router.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 { AddInvoiceReq } from './addInvoiceReq.js';
import { PayInvoiceReq } from './payInvoiceReq.js';
@ -80,7 +80,22 @@ export default class {
this.SubscribeInvoicePaid()
this.SubscribeNewBlock()
this.SubscribeHtlcEvents()
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> {
@ -277,6 +292,7 @@ export default class {
stream.responses.onMessage(payment => {
switch (payment.status) {
case Payment_PaymentStatus.FAILED:
console.log(payment)
this.log("invoice payment failed", payment.failureReason)
rej(PaymentFailureReason[payment.failureReason])
return
@ -317,6 +333,16 @@ export default class {
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> {
const wRes = await this.lightning.walletBalance({}, DeadLineMetadata())
const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response
@ -332,9 +358,9 @@ export default class {
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 }[]> {
const { response } = await this.lightning.forwardingHistory({ indexOffset, numMaxEvents: 0, startTime: 0n, 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 }))
async GetForwardingHistory(indexOffset: number, startTime = 0): Promise<ForwardingHistoryResponse> {
const { response } = await this.lightning.forwardingHistory({ indexOffset, numMaxEvents: 0, startTime: BigInt(startTime), endTime: 0n, peerAliasLookup: false }, DeadLineMetadata())
return response
}
async GetAllPaidInvoices(max: number) {
@ -346,29 +372,37 @@ export default class {
return res.response
}
async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number): Promise<string> {
await this.Health()
async ConnectPeer(addr: { pubkey: string, host: string }) {
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 req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats)
const stream = this.lightning.openChannel(req, { abort: abortController.signal })
return new Promise((res, rej) => {
stream.responses.onMessage(message => {
console.log("message", message)
switch (message.update.oneofKind) {
case 'chanPending':
abortController.abort()
res(Buffer.from(message.pendingChanId).toString('base64'))
break
default:
abortController.abort()
rej("unexpected state response: " + message.update.oneofKind)
}
})
stream.responses.onError(error => {
console.log("error", 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 => ({
nodePubkey: Buffer.from(destination, 'hex'),
@ -9,22 +9,22 @@ export const OpenChannelReq = (destination: string, closeAddress: string, fundin
satPerVbyte: 0n, // TBD
private: false,
minConfs: 0, // TBD
baseFee: 0n, // TBD
feeRate: 0n, // TBD
baseFee: 1n, // TBD
feeRate: 1n, // TBD
targetConf: 0,
zeroConf: false,
maxLocalCsv: 0,
remoteCsvDelay: 0,
spendUnconfirmed: false,
minHtlcMsat: 0n,
remoteChanReserveSat: 0n,
remoteMaxHtlcs: 0,
remoteMaxValueInFlightMsat: 0n,
useBaseFee: false,
useFeeRate: false,
minHtlcMsat: 1n,
remoteChanReserveSat: 10000n,
remoteMaxHtlcs: 483,
remoteMaxValueInFlightMsat: 990000000n,
useBaseFee: true,
useFeeRate: true,
// Default stuff
commitmentType: 0,
commitmentType: CommitmentType.ANCHORS,
scidAlias: false,
nodePubkeyString: "",
satPerByte: 0n,

View file

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

View file

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

View file

@ -5,7 +5,7 @@ import ProductManager from './productManager.js'
import ApplicationManager from './applicationManager.js'
import PaymentManager, { PendingTx } from './paymentManager.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 { getLogger, PubLogger } from "../helpers/logger.js"
import AppUserManager from "./appUserManager.js"
@ -26,7 +26,7 @@ type UserOperationsSub = {
export default class {
storage: Storage
lnd: LightningHandler
lnd: LND
settings: MainSettings
userOperationsSub: UserOperationsSub | null = null
productManager: ProductManager
@ -41,7 +41,7 @@ export default class {
this.settings = settings
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.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)
}
Stop() {
this.lnd.Stop()
this.applicationManager.Stop()
this.paymentManager.Stop()
}
attachNostrSend(f: NostrSend) {
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 { MainSettings } from './settings.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 { getLogger } from '../helpers/logger.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 confInTwo = 100 * 1000 * 1000
export default class {
storage: Storage
settings: MainSettings
lnd: LightningHandler
lnd: LND
addressPaidCb: AddressPaidCb
invoicePaidCb: InvoicePaidCb
log = getLogger({ appName: "PaymentManager" })
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.settings = settings
this.lnd = lnd
@ -54,6 +55,9 @@ export default class {
this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb
}
Stop() {
this.watchDog.Stop()
}
getServiceFee(action: Types.UserOperationType, amount: number, appUser: boolean): number {
switch (action) {
@ -281,7 +285,21 @@ export default class {
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> {
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 key = await this.storage.paymentStorage.AddUserEphemeralKey(ctx.user_id, 'balanceCheck', app)
return {
@ -327,6 +345,9 @@ export default class {
}
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")
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
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> {
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 url = baseUrl ? baseUrl : `${this.settings.serviceUrl}/api/guest/lnurl_pay/handle`
const { remote } = await this.lnd.ChannelBalance()
@ -354,6 +378,9 @@ export default class {
}
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)
if (!key.linkedApplication) {
throw new Error("invalid lnurl request")

View file

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

View file

@ -22,6 +22,12 @@ export type MainSettings = {
skipSanityCheck: 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 => {
return {
watchDogSettings: LoadWatchdogSettingsFromEnv(),
@ -36,7 +42,7 @@ export const LoadMainSettingsFromEnv = (): MainSettings => {
outgoingAppUserInvoiceFee: EnvMustBeInteger("OUTGOING_INVOICE_FEE_USER_BPS") / 10000,
userToUserFee: EnvMustBeInteger("TX_FEE_INTERNAL_USER_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"),
recordPerformance: process.env.RECORD_PERFORMANCE === '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 settings = LoadMainSettingsFromEnv()
return {
@ -61,8 +67,18 @@ export const LoadTestSettingsFromEnv = (): MainSettings & { lndSettings: { other
lndAddr: EnvMustBeNonEmptyString("LND_THIRD_ADDR"),
lndCertPath: EnvMustBeNonEmptyString("LND_THIRD_CERT_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 FunctionQueue from "../helpers/functionQueue.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 Storage from '../storage/index.js'
export type WatchdogSettings = {
@ -12,20 +13,24 @@ export const LoadWatchdogSettingsFromEnv = (test = false): WatchdogSettings => {
}
}
export class Watchdog {
queue: FunctionQueue<void>
initialLndBalance: number;
initialUsersBalance: number;
lnd: LightningHandler;
startedAtUnix: number;
latestIndexOffset: number;
accumulatedHtlcFees: number;
lnd: LND;
settings: WatchdogSettings;
storage: Storage;
latestCheckStart = 0
log = getLogger({ appName: "watchdog" })
enabled = false
ready = false
interval: NodeJS.Timer;
constructor(settings: WatchdogSettings, lnd: LightningHandler, storage: Storage) {
constructor(settings: WatchdogSettings, lnd: LND, storage: Storage) {
this.lnd = lnd;
this.settings = settings;
this.storage = storage;
this.queue = new FunctionQueue("watchdog::queue", () => this.StartCheck())
}
Stop() {
@ -35,10 +40,13 @@ export class Watchdog {
}
Start = async () => {
this.startedAtUnix = Math.floor(Date.now() / 1000)
const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
this.initialLndBalance = await this.getTotalLndBalance(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(() => {
if (this.latestCheckStart + (1000 * 60) < Date.now()) {
@ -46,26 +54,30 @@ export class Watchdog {
this.PaymentRequested()
}
}, 1000 * 60)
this.ready = true
}
getTotalLndBalance = async (usersTotal: number) => {
const localLog = getLogger({ appName: "debugLndBalancev2" })
const { confirmedBalance, channelsBalance } = await this.lnd.GetBalance()
this.log(confirmedBalance, "sats in chain wallet")
localLog({ c: channelsBalance, u: usersTotal })
let totalBalance = confirmedBalance
channelsBalance.forEach(c => {
let totalBalanceInHtlcs = 0
c.htlcs.forEach(htlc => {
if (htlc.incoming) {
totalBalanceInHtlcs += htlc.amount
} else {
//totalBalanceInHtlcs -= htlc.amount
updateAccumulatedHtlcFees = async () => {
const fwEvents = await this.lnd.GetForwardingHistory(this.latestIndexOffset, this.startedAtUnix)
this.latestIndexOffset = fwEvents.lastOffsetIndex
fwEvents.forwardingEvents.forEach((event) => {
this.accumulatedHtlcFees += Number(event.fee)
})
}
})
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) => {
@ -119,16 +131,12 @@ export class Watchdog {
return false
}
PaymentRequested = async () => {
this.log("Payment requested, checking balance")
if (!this.enabled) {
this.log("WARNING! Watchdog not enabled, skipping balance check")
return
}
StartCheck = async () => {
this.latestCheckStart = Date.now()
await this.updateAccumulatedHtlcFees()
const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
const totalLndBalance = await this.getTotalLndBalance(totalUsersBalance)
const deltaLnd = totalLndBalance - this.initialLndBalance
const deltaLnd = totalLndBalance - (this.initialLndBalance + this.accumulatedHtlcFees)
const deltaUsers = totalUsersBalance - this.initialUsersBalance
const deny = this.checkBalanceUpdate(deltaLnd, deltaUsers)
if (deny) {
@ -139,6 +147,16 @@ export class Watchdog {
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 => {
if (deltaLnd < 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 { BalanceEvent } from '../storage/entity/BalanceEvent.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'
const maxEvents = 100_000
export default class Handler {
storage: Storage
lnd: LightningHandler
lnd: LND
htlcTracker: HtlcTracker
metrics: Types.UsageMetric[] = []
constructor(storage: Storage, lnd: LightningHandler) {
constructor(storage: Storage, lnd: LND) {
this.storage = storage
this.lnd = lnd
this.htlcTracker = new HtlcTracker(this.storage)
@ -40,7 +40,8 @@ export default class Handler {
async FetchLatestForwardingEvents() {
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 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 })

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 SanityChecker from '../services/main/sanityChecker.js'
import LND from '../services/lnd/lnd.js'
import { LightningHandler } from '../services/lnd/index.js'
chai.use(chaiString)
export const expect = chai.expect
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) => {
T.main.paymentManager.watchDog.Stop()
T.main.lnd.Stop()
T.main.Stop()
T.externalAccessToMainLnd.Stop()
T.externalAccessToOtherLnd.Stop()
T.externalAccessToThirdLnd.Stop()

View file

@ -1,16 +1,27 @@
import { globby } from 'globby'
import { setupNetwork } from './networkSetup.js'
import { Describe, SetupTest, teardown, TestBase } from './testBase.js'
type TestModule = {
ignore?: boolean
dev?: boolean
default: (T: TestBase) => Promise<void>
}
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 }[] = []
let devModule = -1
for (const file of files) {
@ -19,8 +30,7 @@ const start = async () => {
if (module.dev) {
console.log("dev module found", file)
if (devModule !== -1) {
console.error(redConsole, "there are multiple dev modules", resetConsole)
return
throw new Error("there are multiple dev modules")
}
devModule = modules.length - 1
}
@ -28,16 +38,15 @@ const start = async () => {
if (devModule !== -1) {
console.log("running dev module")
await runTestFile(modules[devModule].file, modules[devModule].module)
return
}
else {
} else {
console.log("running all tests")
for (const { file, module } of modules) {
await runTestFile(file, module)
}
}
console.log(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 {
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 redConsole = "\x1b[31m"
const resetConsole = "\x1b[0m"

View file

@ -1,7 +1,7 @@
import { defaultInvoiceExpiry } from '../services/storage/paymentStorage.js'
import { Describe, expect, expectThrowsAsync, runSanityCheck, safelySetUserBalance, SetupTest, TestBase } from './testBase.js'
export const ignore = false
export const dev = true
export const dev = false
export default async (T: TestBase) => {
await safelySetUserBalance(T, T.user1, 2000)
await testSuccessfulU2UPayment(T)

View file

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