Merge remote-tracking branch 'origin/master' into add-webview
This commit is contained in:
commit
374d0d60ed
28 changed files with 1661 additions and 345 deletions
55
.github/workflows/test.yaml
vendored
Normal file
55
.github/workflows/test.yaml
vendored
Normal 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
|
||||
68
README.md
68
README.md
|
|
@ -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
|
|||

|
||||
|
||||
#### 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
|
||||
|
||||
|
||||
|
|
|
|||
103
env.example
103
env.example
|
|
@ -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
994
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
34
src/services/helpers/functionQueue.ts
Normal file
34
src/services/helpers/functionQueue.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
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> {
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ export type NodeInfo = {
|
|||
syncedToGraph: boolean
|
||||
blockHeight: number
|
||||
blockHash: string
|
||||
identityPubkey: string
|
||||
uris: string[]
|
||||
}
|
||||
export type Invoice = {
|
||||
payRequest: string
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
totalBalance += c.localBalanceSats + totalBalanceInHtlcs
|
||||
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)
|
||||
})
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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
40
src/tests/.env.test
Normal 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
17
src/tests/DockerFile
Normal 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
39
src/tests/bitcoinCore.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
108
src/tests/docker-compose.yml
Normal file
108
src/tests/docker-compose.yml
Normal 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
61
src/tests/networkSetup.ts
Normal 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")
|
||||
}
|
||||
BIN
src/tests/regtestNetwork.zip
Normal file
BIN
src/tests/regtestNetwork.zip
Normal file
Binary file not shown.
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
{"include":["**/*.spec.ts"]}
|
||||
Loading…
Add table
Add a link
Reference in a new issue