├── tests ├── utils │ ├── storage.json │ ├── global-teardown.ts │ ├── spawn_lightning_cluster.ts │ ├── global-setup.ts │ ├── spawn_lightning_server.ts │ ├── global_spawn_lightning.ts │ ├── spawn_lightning.ts │ └── constants.ts ├── server │ ├── certValidityDays.test.ts │ ├── price.test.ts │ ├── reconnect.test.ts │ ├── find.test.ts │ ├── closed.test.ts │ ├── chainfees.test.ts │ ├── utxos.test.ts │ ├── forwards.test.ts │ ├── chainDeposit.test.ts │ ├── call.test.ts │ ├── chartChainFees.test.ts │ ├── lnurl.test.ts │ ├── chartPaymentsReceived.test.ts │ ├── balance.test.ts │ ├── peers.test.ts │ ├── cleanFailedPayments.test.ts │ ├── graph.test.ts │ ├── tags.test.ts │ └── fees.test.ts └── client │ ├── fees.test.ts │ ├── reconnect.test.ts │ ├── graph.test.ts │ ├── find.test.ts │ ├── closed.test.ts │ ├── open.test.ts │ ├── utxos.test.ts │ ├── call.test.ts │ ├── certValidityDays.test.ts │ ├── chainDeposit.test.ts │ ├── forwards.test.ts │ ├── invoice.test.ts │ ├── chartChainFees.test.ts │ ├── createChannelGroup.test.ts │ ├── joinChannelGroup.test.ts │ ├── probe.test.ts │ ├── chartPaymentsReceived.test.ts │ ├── chartFeesEarned.test.ts │ ├── quicktools.test.ts │ ├── balance.test.ts │ ├── send.test.ts │ └── cleanFailedPayments.test.ts ├── app-stores └── umbrel │ ├── .bosgui │ └── .gitkeep │ ├── exports.sh │ ├── docker-compose.yml │ └── umbrel-app.yml ├── storage.json ├── .DS_Store ├── next-env.d.ts ├── .dockerignore ├── src ├── client │ ├── public │ │ ├── startup.mov │ │ └── startup.mp4 │ ├── .vscode │ │ └── settings.json │ ├── .eslintrc.json │ ├── app │ │ ├── Dashboard │ │ │ └── page.tsx │ │ ├── Commands │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── pages │ │ ├── index.tsx │ │ ├── _app.tsx │ │ ├── 404.tsx │ │ └── 500.tsx │ ├── standard_components │ │ ├── lndboss │ │ │ ├── index.ts │ │ │ ├── RawApiList.tsx │ │ │ └── TagsList.tsx │ │ └── app-components │ │ │ ├── quicktools │ │ │ └── index.ts │ │ │ ├── StandardSwitch.tsx │ │ │ ├── ContainerStyle.tsx │ │ │ ├── CenterFlexBox.tsx │ │ │ ├── StandardRouterLink.tsx │ │ │ ├── SubmitButton.tsx │ │ │ ├── CopyText.tsx │ │ │ ├── StartFlexBox.tsx │ │ │ ├── Startup.tsx │ │ │ ├── BasicDatePicker.tsx │ │ │ ├── StandardButtonLink.tsx │ │ │ ├── ProgressBar.tsx │ │ │ ├── index.ts │ │ │ ├── RouteGuard.tsx │ │ │ ├── ReactCron.tsx │ │ │ ├── BasicTable.tsx │ │ │ └── ResponsiveGrid.tsx │ ├── next-env.d.ts │ ├── hooks │ │ ├── useLoading.ts │ │ └── usePasswordValidation.ts │ ├── dashboard │ │ ├── Title.tsx │ │ ├── PendingChart.tsx │ │ └── RoutingFeeChart.tsx │ ├── next.config.js │ ├── output │ │ ├── JoinGroupChannelOutput.tsx │ │ ├── CreateGroupChannelOutput.tsx │ │ ├── FeesOutput.tsx │ │ ├── CallOutput.tsx │ │ ├── FindOutput.tsx │ │ ├── ReconnectOutput.tsx │ │ ├── CertValidityDaysOutput.tsx │ │ ├── GraphOutput.tsx │ │ ├── EncryptOutput.tsx │ │ ├── DecryptOutput.tsx │ │ ├── ChainDepositOutput.tsx │ │ ├── PriceOutput.tsx │ │ ├── ChainfeesOutput.tsx │ │ ├── InvoiceOutput.tsx │ │ ├── CleanFailedPaymentsOutput.tsx │ │ └── OpenOutput.tsx │ ├── utils │ │ ├── validations │ │ │ └── validate_join_channel_group_command.ts │ │ ├── fetch_peers_and_tags.ts │ │ ├── fee_strategies.ts │ │ ├── jwt.ts │ │ ├── cookie.ts │ │ └── constants.ts │ ├── tsconfig.json │ └── register_charts.ts ├── server │ ├── commands │ │ ├── grpc_utils │ │ │ ├── index.ts │ │ │ ├── icons.ts │ │ │ └── format_tokens.ts │ │ ├── lnurl │ │ │ ├── index.ts │ │ │ ├── der_encode_signature.ts │ │ │ └── sign_auth_challenge.ts │ │ ├── cleanFailedPayments │ │ │ └── clean_failed_payments_command.ts │ │ ├── decrypt │ │ │ └── decrypt_command.ts │ │ ├── encrypt │ │ │ └── encrypt_command.ts │ │ ├── joinGroupChannel │ │ │ ├── join_channel_group_command.ts │ │ │ └── spawn_process.ts │ │ ├── rebalance │ │ │ ├── encode_rebalance_params.ts │ │ │ ├── encode_trigger.ts │ │ │ ├── read_rebalance_file.ts │ │ │ └── get_triggers.ts │ │ ├── chainfees │ │ │ └── chainfees_command.ts │ │ ├── createChannelGroup │ │ │ ├── create_channel_group_command.ts │ │ │ └── spawn_process.ts │ │ ├── fees │ │ │ ├── read_fees_file.ts │ │ │ └── fees_command.ts │ │ ├── reconnect │ │ │ └── reconnect_command.ts │ │ ├── price │ │ │ └── price_command.ts │ │ ├── chartChainFees │ │ │ └── chart_chain_fees_command.ts │ │ ├── chartPaymentsReceived │ │ │ └── chart_payments_received_command.ts │ │ ├── certValidityDays │ │ │ └── cert_validity_days_command.ts │ │ ├── chartFeesEarned │ │ │ └── chart_fees_earned_command.ts │ │ ├── utxos │ │ │ └── utxos_command.ts │ │ ├── accounting │ │ │ └── accounting_command.ts │ │ └── invoice │ │ │ └── invoice_command.ts │ ├── external_services_utils │ │ └── index.ts │ ├── authentication │ │ ├── index.ts │ │ └── getAccountInfo.ts │ ├── settings │ │ ├── settings.json │ │ ├── index.ts │ │ ├── check_amboss_health_setting.ts │ │ ├── check_scheduled_rebalance_setting.ts │ │ └── get_settings.file.ts │ ├── modules │ │ ├── auth │ │ │ ├── local-auth.guard.ts │ │ │ ├── jwt.strategy.ts │ │ │ ├── jwt-auth.guard.ts │ │ │ ├── auth.module.ts │ │ │ └── local.strategy.ts │ │ ├── grpc │ │ │ ├── grpc.module.ts │ │ │ └── grpc.controller.ts │ │ ├── users │ │ │ └── users.module.ts │ │ ├── cron │ │ │ └── cron.module.ts │ │ ├── commands │ │ │ └── commands.module.ts │ │ ├── fees │ │ │ ├── fees.module.ts │ │ │ └── fees.controller.ts │ │ ├── lnd │ │ │ ├── lnd.module.ts │ │ │ └── lnd.service.ts │ │ ├── boslogger │ │ │ ├── boslogger.module.ts │ │ │ └── boslogger.service.ts │ │ ├── socket │ │ │ ├── socket.module.ts │ │ │ └── socket.gateway.ts │ │ ├── view │ │ │ ├── view.module.ts │ │ │ ├── view.controller.ts │ │ │ └── view.service.ts │ │ ├── external-services │ │ │ ├── external-services.module.ts │ │ │ └── external-services.service.ts │ │ ├── rebalance │ │ │ ├── rebalance.module.ts │ │ │ └── rebalance.controller.ts │ │ └── credentials │ │ │ ├── credentials.module.ts │ │ │ ├── credentials.controller.ts │ │ │ └── credentials.service.ts │ ├── lnd │ │ ├── index.ts │ │ ├── check_connection.ts │ │ └── authenticated_lnd.ts │ ├── app.controller.ts │ ├── main.ts │ └── utils │ │ └── constants.ts └── shared │ └── cast.helper.ts ├── nest-cli.json ├── .npmignore ├── .prettierrc ├── tsconfig.build.json ├── .prettierignore ├── docker ├── docker-compose-dev.yaml ├── docker-compose.yaml ├── docker-compose-umbrel.yaml └── dev.Dockerfile ├── tsconfig.paths.json ├── .editorconfig ├── scripts └── nest.sh ├── .vscode └── settings.json ├── tsconfig.json ├── .gitignore ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── on-push-dockerhub.yml │ ├── on-tag-dockerhub.yml │ ├── on-tag-dockerhub-root.yml │ ├── on-tag-github.ignore │ └── on-push-github.ignore ├── License ├── .env.example ├── arm64.Dockerfile └── .eslintrc.js /tests/utils/storage.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app-stores/umbrel/.bosgui/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage.json: -------------------------------------------------------------------------------- 1 | { 2 | "cookies": [], 3 | "origins": [] 4 | } 5 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niteshbalusu11/lndboss/HEAD/.DS_Store -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | dist 4 | tests 5 | playwright-report 6 | .env 7 | github 8 | -------------------------------------------------------------------------------- /app-stores/umbrel/exports.sh: -------------------------------------------------------------------------------- 1 | export APP_LNDBOSS_IP="10.21.21.47" 2 | export APP_LNDBOSS_PORT="8055" 3 | -------------------------------------------------------------------------------- /src/client/public/startup.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niteshbalusu11/lndboss/HEAD/src/client/public/startup.mov -------------------------------------------------------------------------------- /src/client/public/startup.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niteshbalusu11/lndboss/HEAD/src/client/public/startup.mp4 -------------------------------------------------------------------------------- /src/server/commands/grpc_utils/index.ts: -------------------------------------------------------------------------------- 1 | import getPending from './get_pending'; 2 | 3 | export { getPending }; 4 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "entryFile": "server/main" 5 | } 6 | -------------------------------------------------------------------------------- /src/server/external_services_utils/index.ts: -------------------------------------------------------------------------------- 1 | import ambossHealthCheck from './amboss_health_check'; 2 | 3 | export { ambossHealthCheck }; 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tests 3 | .env 4 | dev.Dockerfile 5 | Dockerfile 6 | docker-compose-dev.yaml 7 | docker-compose-umbrel.yaml 8 | .dockerignore 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "tabWidth": 2, 5 | "printWidth": 120, 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "tests", "test", "dist", "**/*spec.ts", "**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/client/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "../../node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | docker-compose.yaml 2 | yarn.lock 3 | package.json 4 | node_modules 5 | dist 6 | Dockerfile 7 | yarn-error.log 8 | LICENSE 9 | README.md 10 | -------------------------------------------------------------------------------- /src/server/commands/lnurl/index.ts: -------------------------------------------------------------------------------- 1 | import lnurlCommand from './lnurl_command'; 2 | import parseUrl from './parse_url'; 3 | 4 | export { lnurlCommand, parseUrl }; 5 | -------------------------------------------------------------------------------- /src/server/authentication/index.ts: -------------------------------------------------------------------------------- 1 | import getAccountInfo from './getAccountInfo'; 2 | import register from './register'; 3 | 4 | export { getAccountInfo, register }; 5 | -------------------------------------------------------------------------------- /src/client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next", 3 | "rules": { 4 | "react-hooks/rules-of-hooks": "off", 5 | "react-hooks/exhaustive-deps": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/server/settings/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ambossHealthCheck": { 3 | "is_enabled": true 4 | }, 5 | "scheduledRebalancing": { 6 | "is_enabled": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/client/app/Dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import MainDashboard from '~client/dashboard/MainDashboard'; 4 | 5 | export default function Dashboard() { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /docker/docker-compose-dev.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | lndboss: 3 | build: . 4 | volumes: 5 | - ~/.bosgui:/home/node/.bosgui 6 | - /Users/nitesh/Library/Application Support/Lnd:/home/node/.lnd 7 | ports: 8 | - '8055:8055' 9 | -------------------------------------------------------------------------------- /tsconfig.paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src", 4 | "paths": { 5 | "~client/*": ["client/*"], 6 | "~server/*": ["server/*"], 7 | "~shared/*": ["shared/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /scripts/nest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd src/server/modules 4 | 5 | while getopts "n:" arg; do 6 | case $arg in 7 | n) Arg=$OPTARG && nest g controller $Arg --no-spec && nest g service $Arg --no-spec && nest g module $Arg --no-spec;; 8 | esac 9 | done 10 | -------------------------------------------------------------------------------- /src/client/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Startup } from '../standard_components/app-components'; 3 | 4 | /* 5 | App starts here 6 | */ 7 | 8 | const Home = () => { 9 | return ; 10 | }; 11 | 12 | export default Home; 13 | -------------------------------------------------------------------------------- /src/client/standard_components/lndboss/index.ts: -------------------------------------------------------------------------------- 1 | import PeersAndTagsList from './PeersAndTagsList'; 2 | import PeersList from './PeersList'; 3 | import RawApiList from './RawApiList'; 4 | import TagsList from './TagsList'; 5 | 6 | export { PeersAndTagsList, PeersList, RawApiList, TagsList }; 7 | -------------------------------------------------------------------------------- /tests/utils/global-teardown.ts: -------------------------------------------------------------------------------- 1 | import { killGlobalContainer } from './global_spawn_lightning'; 2 | 3 | async function globalTeardown() { 4 | await killGlobalContainer(); 5 | console.log('======================Stopped Global Container======================'); 6 | } 7 | 8 | export default globalTeardown; 9 | -------------------------------------------------------------------------------- /src/client/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /src/server/modules/auth/local-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { AuthGuard } from '@nestjs/passport'; 2 | import { Injectable } from '@nestjs/common'; 3 | 4 | // Add a local auth guard to check if the user is logged in before returning a JWT 5 | 6 | @Injectable() 7 | export class LocalAuthGuard extends AuthGuard('local') {} 8 | -------------------------------------------------------------------------------- /src/server/modules/grpc/grpc.module.ts: -------------------------------------------------------------------------------- 1 | import { GrpcController } from './grpc.controller'; 2 | import { GrpcService } from './grpc.service'; 3 | import { Module } from '@nestjs/common'; 4 | 5 | @Module({ 6 | controllers: [GrpcController], 7 | providers: [GrpcService], 8 | }) 9 | export class GrpcModule {} 10 | -------------------------------------------------------------------------------- /src/server/modules/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UsersService } from './users.service'; 3 | 4 | // Users Module: Module for the users service 5 | 6 | @Module({ 7 | providers: [UsersService], 8 | exports: [UsersService], 9 | }) 10 | export class UsersModule {} 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": false, 3 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": false, 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "editor.formatOnSave": true, 6 | "git.enableCommitSigning": true 7 | } 8 | -------------------------------------------------------------------------------- /src/server/modules/cron/cron.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { CronService } from './cron.service'; 4 | 5 | // Global module for cron service 6 | 7 | @Global() 8 | @Module({ 9 | providers: [CronService], 10 | exports: [CronService], 11 | }) 12 | export class CronModule {} 13 | -------------------------------------------------------------------------------- /docker/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | lndboss: 3 | image: niteshbalusu/lndboss:latest 4 | volumes: 5 | - ~/.bosgui:/home/node/.bosgui 6 | - /path/to/your/lnd/directory:/home/node/.lnd 7 | ports: 8 | - "8055:8055" 9 | networks: 10 | default: 11 | name: 1_default 12 | external: true 13 | -------------------------------------------------------------------------------- /src/client/standard_components/app-components/quicktools/index.ts: -------------------------------------------------------------------------------- 1 | import CreateChainAddress from './CreateChainAddress'; 2 | import CreateInvoice from './CreateInvoice'; 3 | import PayInvoice from './PayInvoice'; 4 | import SendOnchain from './SendOnchain'; 5 | 6 | export { CreateChainAddress, CreateInvoice, PayInvoice, SendOnchain }; 7 | -------------------------------------------------------------------------------- /src/client/app/Commands/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ContainerStyle } from '../../standard_components/app-components'; 4 | import React from 'react'; 5 | 6 | /* 7 | Renders all commands on home page 8 | */ 9 | 10 | const Commands = () => { 11 | return ; 12 | }; 13 | 14 | export default Commands; 15 | -------------------------------------------------------------------------------- /src/client/app/layout.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: 'Next.js', 3 | description: 'Generated by Next.js', 4 | } 5 | 6 | export default function RootLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode 10 | }) { 11 | return ( 12 | 13 | {children} 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/server/modules/commands/commands.module.ts: -------------------------------------------------------------------------------- 1 | import { CommandsController } from './commands.controller'; 2 | import { CommandsService } from './commands.service'; 3 | import { Module } from '@nestjs/common'; 4 | 5 | @Module({ 6 | controllers: [CommandsController], 7 | providers: [CommandsService], 8 | }) 9 | export class CommandsModule {} 10 | -------------------------------------------------------------------------------- /src/server/modules/fees/fees.module.ts: -------------------------------------------------------------------------------- 1 | import { FeesController } from './fees.controller'; 2 | import { FeesService } from './fees.service'; 3 | import { Module } from '@nestjs/common'; 4 | 5 | // Module for fees command 6 | 7 | @Module({ 8 | controllers: [FeesController], 9 | providers: [FeesService], 10 | }) 11 | export class FeesModule {} 12 | -------------------------------------------------------------------------------- /src/server/modules/lnd/lnd.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { LndService } from './lnd.service'; 4 | 5 | // Lnd module: Global Module for the Authenticated LND API Object service 6 | 7 | @Global() 8 | @Module({ 9 | providers: [LndService], 10 | exports: [LndService], 11 | }) 12 | export class LndModule {} 13 | -------------------------------------------------------------------------------- /src/server/modules/boslogger/boslogger.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { BosloggerService } from './boslogger.service'; 4 | 5 | // Global module for logger service 6 | 7 | @Global() 8 | @Module({ 9 | providers: [BosloggerService], 10 | exports: [BosloggerService], 11 | }) 12 | export class BosloggerModule {} 13 | -------------------------------------------------------------------------------- /src/server/modules/socket/socket.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { SocketGateway } from './socket.gateway'; 4 | 5 | // Module for the NestJS Websockets 6 | 7 | @Global() 8 | @Module({ 9 | controllers: [], 10 | providers: [SocketGateway], 11 | exports: [SocketGateway], 12 | }) 13 | export class SocketModule {} 14 | -------------------------------------------------------------------------------- /src/server/modules/view/view.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ViewController } from '~server/modules/view/view.controller'; 3 | import { ViewService } from '~server/modules/view/view.service'; 4 | 5 | @Module({ 6 | imports: [], 7 | providers: [ViewService], 8 | controllers: [ViewController], 9 | }) 10 | export class ViewModule {} 11 | -------------------------------------------------------------------------------- /src/server/settings/index.ts: -------------------------------------------------------------------------------- 1 | import checkAmbossHealthSetting from './check_amboss_health_setting'; 2 | import checkScheduledRebalanceSetting from './check_scheduled_rebalance_setting'; 3 | import getSettingsFile from './get_settings.file'; 4 | import writeSettingsFile from './write_settings_file'; 5 | 6 | export { checkAmbossHealthSetting, checkScheduledRebalanceSetting, getSettingsFile, writeSettingsFile }; 7 | -------------------------------------------------------------------------------- /tests/utils/spawn_lightning_cluster.ts: -------------------------------------------------------------------------------- 1 | import { spawnLightningCluster } from 'ln-docker-daemons'; 2 | 3 | const spawnCluster = async (size: number) => { 4 | const { nodes } = await spawnLightningCluster({ size }); 5 | 6 | console.log('============================Lightning Cluster Spawned==============================='); 7 | 8 | return nodes; 9 | }; 10 | 11 | export default spawnCluster; 12 | -------------------------------------------------------------------------------- /docker/docker-compose-umbrel.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | lndboss: 3 | image: niteshbalusu/lndboss:latest 4 | volumes: 5 | - ~/.bosgui:/home/node/.bosgui 6 | - ~/umbrel/app-data/lightning/data/lnd:/home/node/.lnd 7 | ports: 8 | - '8055:8055' 9 | extra_hosts: 10 | - 'localhost:10.21.21.9' 11 | networks: 12 | default: 13 | external: true 14 | name: umbrel_main_network 15 | -------------------------------------------------------------------------------- /src/server/modules/external-services/external-services.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { ExternalServicesService } from './external-services.service'; 4 | 5 | // Global module for external services 6 | 7 | @Global() 8 | @Module({ 9 | providers: [ExternalServicesService], 10 | exports: [ExternalServicesService], 11 | }) 12 | export class ExternalServicesModule {} 13 | -------------------------------------------------------------------------------- /src/server/modules/rebalance/rebalance.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RebalanceController } from './rebalance.controller'; 3 | import { RebalanceService } from './rebalance.service'; 4 | 5 | // Rebalance module: Module for the rebalance command 6 | 7 | @Module({ 8 | controllers: [RebalanceController], 9 | providers: [RebalanceService], 10 | }) 11 | export class RebalanceModule {} 12 | -------------------------------------------------------------------------------- /src/client/hooks/useLoading.ts: -------------------------------------------------------------------------------- 1 | import Notiflix from 'notiflix'; 2 | 3 | // Adds a loading indicator to pages when waiting for a response from the server 4 | 5 | type Args = { 6 | isLoading: boolean; 7 | }; 8 | 9 | export const useLoading = ({ isLoading }: Args) => { 10 | if (!!isLoading) { 11 | Notiflix.Loading.dots('Loading...'); 12 | } 13 | 14 | if (!isLoading) { 15 | Notiflix.Loading.remove(); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/server/commands/grpc_utils/icons.ts: -------------------------------------------------------------------------------- 1 | const icons = { 2 | balanced_open: '⚖️', 3 | block: '⏹', 4 | bot: '🤖', 5 | chain: '⛓', 6 | closing: '⏳', 7 | disconnected: '😵', 8 | earn: '💰', 9 | forwarding: '💸', 10 | info: 'ℹ️', 11 | liquidity: '🌊', 12 | opening: '⏳', 13 | probe: '👽', 14 | rebalance: '☯️', 15 | receive: '💵', 16 | spent: '⚡️', 17 | warning: '⚠️', 18 | }; 19 | 20 | export default icons; 21 | -------------------------------------------------------------------------------- /src/server/modules/credentials/credentials.module.ts: -------------------------------------------------------------------------------- 1 | import { CredentialsController } from './credentials.controller'; 2 | import { CredentialsService } from './credentials.service'; 3 | import { Module } from '@nestjs/common'; 4 | 5 | // Credentials module: Module for the credentials service 6 | 7 | @Module({ 8 | providers: [CredentialsService], 9 | controllers: [CredentialsController], 10 | }) 11 | export class CredentialsModule {} 12 | -------------------------------------------------------------------------------- /src/client/dashboard/Title.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Typography from '@mui/material/Typography'; 3 | 4 | // Renders the title of the dashboard. 5 | 6 | interface TitleProps { 7 | children?: React.ReactNode; 8 | } 9 | 10 | const Title = (props: TitleProps) => { 11 | return ( 12 | 13 | {props.children} 14 | 15 | ); 16 | }; 17 | 18 | export default Title; 19 | -------------------------------------------------------------------------------- /src/client/next.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const path = require('path'); 3 | const dotenv = require('dotenv'); 4 | const { homedir } = require('os'); 5 | 6 | dotenv.config({ path: path.resolve(process.cwd(), '.env') }); 7 | dotenv.config({ path: path.join(homedir(), '.bosgui', '.env') }); 8 | 9 | module.exports = { 10 | reactStrictMode: true, 11 | swcMinify: true, 12 | experimental: { 13 | appDir: true, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /tests/utils/global-setup.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { join } from 'path'; 3 | import { startGlobalContainer } from './global_spawn_lightning'; 4 | 5 | dotenv.config(); 6 | 7 | // Alternatively, read from "../my.env" file. 8 | dotenv.config({ path: join(__dirname, '../../.env') }); 9 | 10 | async function globalSetup() { 11 | await startGlobalContainer(); 12 | console.log('======================Started Global Container======================'); 13 | } 14 | 15 | export default globalSetup; 16 | -------------------------------------------------------------------------------- /src/client/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { NextRouter } from 'next/router'; 2 | import { RouteGuard } from '~client/standard_components/app-components'; 3 | 4 | // First page that gets rendered before every page. 5 | 6 | type Props = { 7 | Component: React.ComponentType; 8 | pageProps: any; 9 | router: NextRouter; 10 | }; 11 | 12 | const App = ({ Component, pageProps, router }: Props) => { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /src/client/output/JoinGroupChannelOutput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* 4 | Renders the output of the join-group-channel command. 5 | */ 6 | 7 | type Args = { 8 | data: string | undefined; 9 | }; 10 | const JoinGroupChannelOutput = ({ data }: Args) => { 11 | return ( 12 |
13 | {!!data &&
{data}
} 14 |
15 | ); 16 | }; 17 | 18 | export default JoinGroupChannelOutput; 19 | 20 | const styles = { 21 | div: { 22 | width: '1100px', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.paths.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "declaration": true, 6 | "removeComments": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "target": "es2017", 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "incremental": true, 14 | "jsx": "preserve", 15 | "resolveJsonModule": true 16 | }, 17 | "include": ["./src/server/**/*.ts", "./src/shared/**/*.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /src/client/output/CreateGroupChannelOutput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* 4 | Renders the output of the create-group-channel command. 5 | */ 6 | 7 | type Args = { 8 | data: string | undefined; 9 | }; 10 | const CreateGroupChannelOutput = ({ data }: Args) => { 11 | return ( 12 |
13 | {!!data &&
{data}
} 14 |
15 | ); 16 | }; 17 | 18 | export default CreateGroupChannelOutput; 19 | 20 | const styles = { 21 | div: { 22 | width: '1100px', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/client/output/FeesOutput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StandardTableOutput } from '~client/standard_components/app-components'; 3 | 4 | // Renders the output of the bos fees command 5 | 6 | type Props = { 7 | data: { 8 | rows: string[][]; 9 | }; 10 | }; 11 | 12 | const FeesOutput = ({ data }: Props) => { 13 | return ( 14 |
15 | {!!data ? :

No Output to display

} 16 |
17 | ); 18 | }; 19 | 20 | export default FeesOutput; 21 | -------------------------------------------------------------------------------- /src/server/lnd/index.ts: -------------------------------------------------------------------------------- 1 | import authenticatedLnd from './authenticated_lnd'; 2 | import checkConnection from './check_connection'; 3 | import getLnds from './get_lnds'; 4 | import getSavedCredentials from './get_saved_credentials'; 5 | import getSavedNodes from './get_saved_nodes'; 6 | import lndCredentials from './lnd_credentials'; 7 | import putSavedCredentials from './put_saved_credentials'; 8 | 9 | export { 10 | authenticatedLnd, 11 | checkConnection, 12 | getLnds, 13 | getSavedCredentials, 14 | getSavedNodes, 15 | lndCredentials, 16 | putSavedCredentials, 17 | }; 18 | -------------------------------------------------------------------------------- /src/server/modules/view/view.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Req, Res } from '@nestjs/common'; 2 | import { Request, Response } from 'express'; 3 | import { ViewService } from '~server/modules/view/view.service'; 4 | import { Public } from '../../utils/constants'; 5 | 6 | @Public() 7 | @Controller('/') 8 | export class ViewController { 9 | constructor(private viewService: ViewService) {} 10 | 11 | @Get('*') 12 | static(@Req() req: Request, @Res() res: Response) { 13 | const handle = this.viewService.getNextServer().getRequestHandler(); 14 | handle(req, res); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/client/output/CallOutput.tsx: -------------------------------------------------------------------------------- 1 | import * as YAML from 'json-to-pretty-yaml'; 2 | 3 | import React from 'react'; 4 | 5 | // Renders output of bos call command 6 | 7 | const styles = { 8 | pre: { 9 | fontWeight: 'bold', 10 | }, 11 | }; 12 | 13 | type Args = { 14 | data: any[]; 15 | }; 16 | 17 | const CallOutput = ({ data }: Args) => { 18 | const output = YAML.stringify(data); 19 | return ( 20 |
21 | {Object.keys(data).length ?
{output}
:

No data found

} 22 |
23 | ); 24 | }; 25 | 26 | export default CallOutput; 27 | -------------------------------------------------------------------------------- /src/client/output/FindOutput.tsx: -------------------------------------------------------------------------------- 1 | import * as YAML from 'json-to-pretty-yaml'; 2 | 3 | import React from 'react'; 4 | 5 | // Renders output of bos find command 6 | 7 | const styles = { 8 | pre: { 9 | fontWeight: 'bold', 10 | }, 11 | }; 12 | 13 | type Args = { 14 | data: any[]; 15 | }; 16 | 17 | const FindOutput = ({ data }: Args) => { 18 | const output = YAML.stringify(data); 19 | return ( 20 |
21 | {Object.keys(data).length ?
{output}
:

No data found

} 22 |
23 | ); 24 | }; 25 | 26 | export default FindOutput; 27 | -------------------------------------------------------------------------------- /src/server/modules/credentials/credentials.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from '@nestjs/common'; 2 | import { credentialsDto } from '~shared/commands.dto'; 3 | import { CredentialsService } from './credentials.service'; 4 | 5 | // Credentials Controller: Handles routes to the credentials service 6 | 7 | @Controller('api/credentials') 8 | export class CredentialsController { 9 | constructor(private credentialsService: CredentialsService) {} 10 | 11 | @Post() 12 | async credentials(@Body() args: credentialsDto) { 13 | return await this.credentialsService.post(args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/client/hooks/usePasswordValidation.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | // Hook for validatating password strength 4 | 5 | export const usePasswordValidation = ({ firstPassword = '', secondPassword = '', requiredLength }) => { 6 | const [validLength, setValidLength] = useState(false); 7 | const [match, setMatch] = useState(false); 8 | 9 | useEffect(() => { 10 | setValidLength(firstPassword.length >= requiredLength); 11 | setMatch(!!firstPassword && firstPassword === secondPassword); 12 | }, [firstPassword, secondPassword, requiredLength]); 13 | 14 | return [validLength, match]; 15 | }; 16 | -------------------------------------------------------------------------------- /src/client/output/ReconnectOutput.tsx: -------------------------------------------------------------------------------- 1 | import * as YAML from 'json-to-pretty-yaml'; 2 | 3 | import React from 'react'; 4 | 5 | // Renders output of bos reconnect command 6 | 7 | const styles = { 8 | pre: { 9 | fontWeight: 'bold', 10 | }, 11 | }; 12 | 13 | type Args = { 14 | data: any[]; 15 | }; 16 | 17 | const ReconnectOutput = ({ data }: Args) => { 18 | const output = YAML.stringify(data); 19 | return ( 20 |
21 | {Object.keys(data).length ?
{output}
:

No data found

} 22 |
23 | ); 24 | }; 25 | 26 | export default ReconnectOutput; 27 | -------------------------------------------------------------------------------- /src/client/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { StandardHomeButtonLink, StartFlexBox } from '../standard_components/app-components'; 2 | 3 | import { CssBaseline } from '@mui/material'; 4 | import React from 'react'; 5 | 6 | // Standard 404 page not found page to be used for all 404 errors 7 | 8 | const styles = { 9 | h1: { 10 | marginTop: '100px', 11 | }, 12 | }; 13 | 14 | export default function Custom404() { 15 | return ( 16 | 17 | 18 | 19 |

404 - Page Not Found

20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/client/pages/500.tsx: -------------------------------------------------------------------------------- 1 | import { StandardHomeButtonLink, StartFlexBox } from '../standard_components/app-components'; 2 | 3 | import { CssBaseline } from '@mui/material'; 4 | import React from 'react'; 5 | 6 | // Standard 500 error page to be used for all 500 errors 7 | 8 | const styles = { 9 | h1: { 10 | marginTop: '100px', 11 | }, 12 | }; 13 | 14 | export default function Custom500() { 15 | return ( 16 | 17 | 18 | 19 |

500 - Server-side error occurred

20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/client/output/CertValidityDaysOutput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Render the output of the CertValidityDays command. 4 | 5 | const styles = { 6 | div: { 7 | marginTop: '100px', 8 | marginLeft: '10px', 9 | }, 10 | text: { 11 | fontSize: '15px', 12 | fontWeight: 'bold', 13 | }, 14 | }; 15 | 16 | type Data = { 17 | data: string; 18 | }; 19 | 20 | const CertValidityDaysOutput = ({ data }: Data) => { 21 | return ( 22 |
23 |

Remaining number of days of certificate validity: {data}

24 |
25 | ); 26 | }; 27 | 28 | export default CertValidityDaysOutput; 29 | -------------------------------------------------------------------------------- /src/client/standard_components/app-components/StandardSwitch.tsx: -------------------------------------------------------------------------------- 1 | import { Switch, alpha, styled } from '@mui/material'; 2 | import { blue, green } from '@mui/material/colors'; 3 | 4 | /* 5 | Renders the standard ios style switch used in command forms. 6 | */ 7 | 8 | const StandardSwitch = styled(Switch)(({ theme }) => ({ 9 | '& .MuiSwitch-switchBase.Mui-checked': { 10 | color: blue[600], 11 | '&:hover': { 12 | backgroundColor: alpha(green[500], theme.palette.action.hoverOpacity), 13 | }, 14 | }, 15 | '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { 16 | backgroundColor: green[500], 17 | }, 18 | })); 19 | 20 | export default StandardSwitch; 21 | -------------------------------------------------------------------------------- /src/client/standard_components/app-components/ContainerStyle.tsx: -------------------------------------------------------------------------------- 1 | import { PositionedMenu, ResponsiveGrid } from './index'; 2 | 3 | import CenterFlexBox from './CenterFlexBox'; 4 | import CssBaseline from '@mui/material/CssBaseline'; 5 | import React from 'react'; 6 | import commands from '../../commands'; 7 | 8 | /* 9 | Renders the login button and the commands grid on the home page. 10 | */ 11 | 12 | const ContainerStyle = () => { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default ContainerStyle; 24 | -------------------------------------------------------------------------------- /src/client/standard_components/app-components/CenterFlexBox.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@mui/material'; 2 | import React from 'react'; 3 | 4 | /* 5 | children: Renders the children passed into the center flex box. 6 | */ 7 | 8 | type Props = { 9 | children: React.PropsWithChildren<{ unknown: any }>['children']; 10 | }; 11 | 12 | const CenterFlexBox = ({ children }: Props) => { 13 | return ( 14 | 22 | {children} 23 | 24 | ); 25 | }; 26 | 27 | export default CenterFlexBox; 28 | -------------------------------------------------------------------------------- /tests/server/certValidityDays.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import { certValidityDaysCommand } from '../../src/server/commands/'; 4 | 5 | test.describe('Test CertValidityDays command on the node.js side', async () => { 6 | test.beforeAll(async () => { 7 | // Do nothing 8 | }); 9 | 10 | test('run ChainDeposit command', async () => { 11 | const args = { 12 | below: 1000, 13 | node: 'testnode1', 14 | }; 15 | 16 | const { result } = await certValidityDaysCommand({ below: args.below, node: args.node }); 17 | console.log('certValidityDays----', result); 18 | 19 | expect(result).toBeTruthy(); 20 | }); 21 | 22 | test.afterAll(async () => { 23 | // Do nothing 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/standard_components/app-components/StandardRouterLink.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import React from 'react'; 3 | 4 | /* 5 | { 6 | label: 7 | destination: 8 | } 9 | Returns the standard link 10 | */ 11 | 12 | const styles = { 13 | link: { 14 | fontSize: '20px', 15 | margin: '0px', 16 | cursor: 'pointer', 17 | color: 'white', 18 | }, 19 | }; 20 | 21 | type Props = { 22 | label: string; 23 | destination: string; 24 | }; 25 | 26 | const StandardRouterLink = ({ label, destination }: Props) => { 27 | return ( 28 | 29 | {label} 30 | 31 | ); 32 | }; 33 | export default StandardRouterLink; 34 | -------------------------------------------------------------------------------- /src/client/standard_components/app-components/SubmitButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonProps, styled } from '@mui/material'; 2 | 3 | import React from 'react'; 4 | import { grey } from '@mui/material/colors'; 5 | 6 | /* 7 | Renders the standard submit button used in command forms. 8 | */ 9 | 10 | const ColorButton = styled(Button)(({ theme }) => ({ 11 | color: theme.palette.getContrastText(grey[900]), 12 | backgroundColor: grey[900], 13 | '&:hover': { 14 | backgroundColor: grey[800], 15 | }, 16 | marginTop: '30px', 17 | fontWeight: 'bold', 18 | width: '250px', 19 | })); 20 | 21 | const SubmitButton = (props: ButtonProps) => { 22 | return {props.children}; 23 | }; 24 | 25 | export default SubmitButton; 26 | -------------------------------------------------------------------------------- /src/server/modules/fees/fees.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from '@nestjs/common'; 2 | import { feesDto, feesStrategiesDto } from '~shared/commands.dto'; 3 | import { FeesService } from './fees.service'; 4 | 5 | // Fees controller: Defines routes for fees command 6 | 7 | @Controller() 8 | export class FeesController { 9 | constructor(private feeService: FeesService) {} 10 | 11 | @Post('api/fees') 12 | async fees(@Body() args: feesDto) { 13 | return this.feeService.feesCommand(args); 14 | } 15 | 16 | @Post('api/fees/save-strategies') 17 | async save(@Body() args: feesStrategiesDto) { 18 | return this.feeService.save(args); 19 | } 20 | 21 | @Post('api/fees/getfile') 22 | async getFile() { 23 | return this.feeService.readFeesFile(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app-stores/umbrel/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | app_proxy: 5 | environment: 6 | APP_HOST: $APP_LNDBOSS_IP 7 | APP_PORT: $APP_LNDBOSS_PORT 8 | web: 9 | image: niteshbalusu/lndboss:v1.15.2@sha256:c6fecdb6a4a0c1960d6ae22eba9ad73abaed45224d3c3cfd3d6f1d04f52a1c36 10 | restart: on-failure 11 | stop_grace_period: 1m 12 | volumes: 13 | - ${APP_LIGHTNING_NODE_DATA_DIR}:/home/node/.lnd:ro 14 | - ${APP_DATA_DIR}/.bosgui:/home/node/.bosgui 15 | environment: 16 | BOS_DATA_PATH: '/home/node/.bosgui' 17 | NODE_ENV: 'production' 18 | PORT: $APP_LNDBOSS_PORT 19 | BOS_DEFAULT_LND_SOCKET: $APP_LIGHTNING_NODE_IP:$APP_LIGHTNING_NODE_GRPC_PORT 20 | networks: 21 | default: 22 | ipv4_address: $APP_LNDBOSS_IP 23 | -------------------------------------------------------------------------------- /src/client/output/GraphOutput.tsx: -------------------------------------------------------------------------------- 1 | import * as YAML from 'json-to-pretty-yaml'; 2 | 3 | import React from 'react'; 4 | import { StandardTableOutput } from '~client/standard_components/app-components'; 5 | 6 | // Renders the output of the bos graph command 7 | 8 | type Props = { 9 | data: string[][]; 10 | summary: object; 11 | }; 12 | 13 | const styles = { 14 | pre: { 15 | fontWeight: 'bold', 16 | }, 17 | }; 18 | const GraphOutput = ({ data, summary }: Props) => { 19 | const output = YAML.stringify(summary); 20 | 21 | return ( 22 |
23 |
{output}
24 | {!!data ? :

No Output to display

} 25 |
26 | ); 27 | }; 28 | 29 | export default GraphOutput; 30 | -------------------------------------------------------------------------------- /src/client/standard_components/app-components/CopyText.tsx: -------------------------------------------------------------------------------- 1 | import ContentCopyIcon from '@mui/icons-material/ContentCopy'; 2 | import { CopyToClipboard } from 'react-copy-to-clipboard'; 3 | import IconButton from '@mui/material/IconButton'; 4 | import React from 'react'; 5 | import { useNotify } from '~client/hooks/useNotify'; 6 | 7 | // Renders the button to copy the text to the clipboard 8 | 9 | type Args = { 10 | text: string; 11 | }; 12 | const CopyText = ({ text }: Args) => { 13 | return ( 14 | useNotify({ type: 'success', message: 'Copied to clipboard' })}> 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default CopyText; 23 | -------------------------------------------------------------------------------- /tests/server/price.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import { getPrices } from '@alexbosworth/fiat'; 4 | import request from 'balanceofsatoshis/commands/simple_request'; 5 | 6 | test.describe('Test Price command on the node.js side', async () => { 7 | test.beforeAll(async () => { 8 | // Do nothing 9 | }); 10 | 11 | test('run Price command', async () => { 12 | const args = { 13 | file: false, 14 | from: 'coinbase', 15 | }; 16 | const result = await getPrices({ 17 | request, 18 | symbols: ['USD', 'AUD'], 19 | from: args.from, 20 | }); 21 | 22 | console.log('price----', result); 23 | expect(result.tickers).toBeTruthy(); 24 | }); 25 | 26 | test.afterAll(async () => { 27 | // Do nothing 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/server/reconnect.test.ts: -------------------------------------------------------------------------------- 1 | import { SpawnLightningServerType, spawnLightningServer } from '../utils/spawn_lightning_server'; 2 | import { expect, test } from '@playwright/test'; 3 | 4 | import { reconnectCommand } from '../../src/server/commands/'; 5 | 6 | test.describe('Test Reconnect command on the node.js side', async () => { 7 | let lightning: SpawnLightningServerType; 8 | 9 | test.beforeAll(async () => { 10 | lightning = await spawnLightningServer(); 11 | }); 12 | 13 | test('run Reconnect command', async () => { 14 | const { result } = await reconnectCommand({ lnd: lightning.lnd }); 15 | 16 | console.log('reconnect----', result); 17 | 18 | expect(result).toBeTruthy(); 19 | }); 20 | 21 | test.afterAll(async () => { 22 | await lightning.kill({}); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /app-stores/umbrel/umbrel-app.yml: -------------------------------------------------------------------------------- 1 | manifestVersion: 1 2 | id: lndboss 3 | category: Lightning Node Management 4 | name: LndBoss 5 | version: '1.15.2' 6 | tagline: A GUI for BalanceOfSatoshis 7 | description: LndBoss is a GUI for BalanceOfSatoshis. 8 | It is a tool that makes it easy to run your favorite 9 | bos commands and helps manage your lightning node. 10 | You can schedule jobs to automatically rebalance channels, 11 | integration with amboss to post updates and much more. 12 | developer: Nitesh Balusu 13 | website: https://github.com/niteshbalusu11 14 | dependencies: 15 | - lightning 16 | repo: https://github.com/niteshbalusu11/lndboss 17 | support: https://t.me/lndboss 18 | port: 8055 19 | gallery: 20 | - 1.jpg 21 | - 2.jpg 22 | - 3.jpg 23 | path: '' 24 | defaultUsername: '' 25 | defaultPassword: '' 26 | -------------------------------------------------------------------------------- /tests/server/find.test.ts: -------------------------------------------------------------------------------- 1 | import { SpawnLightningServerType, spawnLightningServer } from '../utils/spawn_lightning_server'; 2 | import { expect, test } from '@playwright/test'; 3 | 4 | import { findCommand } from '../../src/server/commands/'; 5 | 6 | test.describe('Test Find command on the node.js side', async () => { 7 | let lightning: SpawnLightningServerType; 8 | 9 | test.beforeAll(async () => { 10 | lightning = await spawnLightningServer(); 11 | }); 12 | 13 | test('run Find command', async () => { 14 | const args = { 15 | query: 'alice', 16 | }; 17 | const { result } = await findCommand({ args, lnd: lightning.lnd }); 18 | console.log('find----', result); 19 | expect(result).toBeTruthy(); 20 | }); 21 | 22 | test.afterAll(async () => { 23 | await lightning.kill({}); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/client/utils/validations/validate_join_channel_group_command.ts: -------------------------------------------------------------------------------- 1 | const isCode = n => !!n && n.length === 98; 2 | const isNumber = (n: number) => !isNaN(n); 3 | 4 | /** Validate join group channel body 5 | { 6 | code: 7 | max_rate: 8 | } 9 | 10 | @returns boolean 11 | */ 12 | 13 | type Args = { 14 | code: string; 15 | max_rate: number; 16 | }; 17 | const validateJoinGroupChannelCommand = (args: Args) => { 18 | if (!args.code || !isCode(args.code)) { 19 | throw new Error('Expected Valid Invite Code To Join Group Channel'); 20 | } 21 | 22 | if (!args.max_rate || !isNumber(args.max_rate)) { 23 | throw new Error('Expected Numeric Max Fee Rate To Join Group Channel'); 24 | } 25 | 26 | return true; 27 | }; 28 | 29 | export default validateJoinGroupChannelCommand; 30 | -------------------------------------------------------------------------------- /src/server/commands/cleanFailedPayments/clean_failed_payments_command.ts: -------------------------------------------------------------------------------- 1 | import { cleanFailedPayments } from 'balanceofsatoshis/wallets'; 2 | 3 | /** Clean out failed payments from the wallet 4 | 5 | { 6 | is_dry_run: 7 | lnd: 8 | logger: 9 | } 10 | 11 | @returns via Promise 12 | { 13 | [total_failed_payments_found]: 14 | [total_failed_payments_deleted]: 15 | } 16 | */ 17 | 18 | const cleanFailedPaymentsCommand = async ({ args, lnd, logger }) => { 19 | const result = await cleanFailedPayments({ 20 | lnd, 21 | logger, 22 | is_dry_run: args.is_dry_run, 23 | }); 24 | 25 | return { result }; 26 | }; 27 | 28 | export default cleanFailedPaymentsCommand; 29 | -------------------------------------------------------------------------------- /tests/server/closed.test.ts: -------------------------------------------------------------------------------- 1 | import { SpawnLightningServerType, spawnLightningServer } from '../utils/spawn_lightning_server'; 2 | import { expect, test } from '@playwright/test'; 3 | 4 | import { closedCommand } from '../../src/server/commands'; 5 | 6 | test.describe('Test Closed command on the node.js side', async () => { 7 | let lightning: SpawnLightningServerType; 8 | 9 | test.beforeAll(async () => { 10 | lightning = await spawnLightningServer(); 11 | }); 12 | 13 | test('run closed command', async () => { 14 | const args = { 15 | limit: 1, 16 | }; 17 | const { result } = await closedCommand({ args, lnd: lightning.lnd }); 18 | console.log('closed----', result); 19 | expect(result.closes).toBeTruthy(); 20 | }); 21 | 22 | test.afterAll(async () => { 23 | await lightning.kill({}); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | .next 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | /test-results/ 21 | /playwright-report/ 22 | /playwright/.cache/ 23 | 24 | # IDEs and editors 25 | /.idea 26 | .project 27 | .classpath 28 | .c9/ 29 | *.launch 30 | .settings/ 31 | *.sublime-workspace 32 | 33 | # IDE - VSCode 34 | .vscode/* 35 | !.vscode/settings.json 36 | !.vscode/tasks.json 37 | !.vscode/launch.json 38 | !.vscode/extensions.json 39 | /test-results/ 40 | /playwright-report/ 41 | /playwright/.cache/ 42 | 43 | # Env files 44 | .env 45 | .env.local 46 | 47 | # Docker 48 | lndboss.tar 49 | lndboss.tar.gz 50 | /test-results/ 51 | /playwright-report/ 52 | /playwright/.cache/ 53 | -------------------------------------------------------------------------------- /src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.paths.json", 3 | "plugins": [{ "name": "next" }], 4 | "compilerOptions": { 5 | "plugins": [{ "name": "next" }], 6 | "target": "es5", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "strictNullChecks": false 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.js", "shared/**/*.*", ".next/types/**/*.ts"], 23 | "exclude": ["node_modules", ".next", "server/**/*.*"] 24 | } 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Pull Request Checklist 2 | 3 | ## Description 4 | 5 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 6 | 7 | Fixes # (issue) 8 | 9 | ## Type of change 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | - [ ] This change bumps dependencies 16 | 17 | ## Checklist if applicable: 18 | 19 | - [ ] Appropriate comments have been added along with code? 20 | - [ ] Test case has been added/updated? 21 | - [ ] ReadMe has been updated? 22 | - [ ] Ran test cases? 23 | -------------------------------------------------------------------------------- /src/server/modules/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | 3 | import { Injectable } from '@nestjs/common'; 4 | import { PassportStrategy } from '@nestjs/passport'; 5 | import { jwtConstants } from '../../utils/constants'; 6 | 7 | type Payload = { 8 | username: string; 9 | sub: string; 10 | iat: number; 11 | exp: number; 12 | }; 13 | 14 | // Local strategy for JWT, validate the user's credentials 15 | 16 | @Injectable() 17 | export class JwtStrategy extends PassportStrategy(Strategy) { 18 | constructor() { 19 | super({ 20 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 21 | ignoreExpiration: false, 22 | secretOrKey: jwtConstants.secret, 23 | }); 24 | } 25 | 26 | async validate(payload: Payload) { 27 | return { userId: payload.sub, username: payload.username }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/shared/cast.helper.ts: -------------------------------------------------------------------------------- 1 | export function trim(value: string): string { 2 | return value.trim(); 3 | } 4 | 5 | export function toDate(value: string): Date { 6 | return new Date(value); 7 | } 8 | 9 | export function toBoolean(value: string): boolean { 10 | value = value.toLowerCase(); 11 | 12 | return !!(value === 'true' || value === '1'); 13 | } 14 | 15 | export function toNumber(value: string): number { 16 | const newValue: number = Number.parseInt(value, 10); 17 | 18 | return newValue; 19 | } 20 | 21 | export function toStringArray(value: string | string[] | undefined): string[] { 22 | const { isArray } = Array; 23 | const isString = n => typeof n === 'string'; 24 | if (!value) { 25 | return []; 26 | } 27 | 28 | if (!!isArray(value)) { 29 | return value; 30 | } 31 | 32 | if (!!isString(value)) { 33 | return [value]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/utils/spawn_lightning_server.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticatedLnd, getIdentity } from 'lightning'; 2 | 3 | import { spawnLightningCluster } from 'ln-docker-daemons'; 4 | 5 | export type SpawnLightningServerType = { 6 | lnd: AuthenticatedLnd; 7 | kill: ({}) => Promise; 8 | }; 9 | 10 | const spawnLightningServer = async (): Promise => { 11 | // Launch a lightning node 12 | const { nodes } = await spawnLightningCluster({}); 13 | const [{ lnd, generate, kill }] = nodes; 14 | 15 | await generate({ count: 5 }); 16 | 17 | const publicKey = (await getIdentity({ lnd })).public_key; 18 | 19 | if (!!publicKey) { 20 | console.log('============================Lightning Server Spawned==============================='); 21 | } 22 | 23 | // Stop the image 24 | return { lnd, kill }; 25 | }; 26 | 27 | export { spawnLightningServer }; 28 | -------------------------------------------------------------------------------- /src/server/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, UseGuards, Body } from '@nestjs/common'; 2 | import { LocalAuthGuard } from '~server/modules/auth/local-auth.guard'; 3 | import { AuthService } from '~server/modules/auth/auth.service'; 4 | import { authenticationDto } from '~shared/commands.dto'; 5 | import { Public } from './utils/constants'; 6 | 7 | // App Controller: Handles routes to the auth service 8 | 9 | @Controller() 10 | export class AppController { 11 | constructor(private authService: AuthService) {} 12 | 13 | @Public() 14 | @UseGuards(LocalAuthGuard) 15 | @Post('api/auth/login') 16 | async login(@Body() body: authenticationDto) { 17 | return this.authService.login(body); 18 | } 19 | 20 | @Public() 21 | @Post('api/auth/register') 22 | async register(@Body() body: authenticationDto) { 23 | return this.authService.registerUser(body); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/server/modules/view/view.service.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-duplicates */ 2 | import { Injectable, OnModuleInit } from '@nestjs/common'; 3 | 4 | import { ConfigService } from '@nestjs/config'; 5 | import { NextServer } from 'next/dist/server/next'; 6 | import createServer from 'next'; 7 | 8 | @Injectable() 9 | export class ViewService implements OnModuleInit { 10 | private server: NextServer; 11 | 12 | constructor(private configService: ConfigService) {} 13 | 14 | async onModuleInit(): Promise { 15 | try { 16 | this.server = createServer({ 17 | dev: this.configService.get('NODE_ENV') !== 'production', 18 | dir: './src/client', 19 | }); 20 | await this.server.prepare(); 21 | } catch (error) { 22 | console.error(error); 23 | } 24 | } 25 | 26 | getNextServer(): NextServer { 27 | return this.server; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/client/standard_components/app-components/StartFlexBox.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@mui/material'; 2 | import React from 'react'; 3 | 4 | /* 5 | children: Renders the children passed into the start flex box. 6 | This flexbox is used for command forms that align to the left. 7 | */ 8 | 9 | type Props = { 10 | children: React.PropsWithChildren<{ unknown }>['children']; 11 | }; 12 | 13 | const StartFlexBox = ({ children }: Props) => { 14 | return ( 15 | 26 | {children} 27 | 28 | ); 29 | }; 30 | 31 | export default StartFlexBox; 32 | -------------------------------------------------------------------------------- /src/server/modules/auth/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, Injectable } from '@nestjs/common'; 2 | 3 | import { AuthGuard } from '@nestjs/passport'; 4 | import { IS_PUBLIC_KEY } from '../../utils/constants'; 5 | import { Reflector } from '@nestjs/core'; 6 | 7 | /* 8 | Add a local auth guard to check if the user is logged in before returning a JWT 9 | Can Activate is used to set @Public() on certain routes 10 | */ 11 | 12 | @Injectable() 13 | export class JwtAuthGuard extends AuthGuard('jwt') { 14 | constructor(private reflector: Reflector) { 15 | super(); 16 | } 17 | 18 | canActivate(context: ExecutionContext) { 19 | const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ 20 | context.getHandler(), 21 | context.getClass(), 22 | ]); 23 | if (isPublic) { 24 | return true; 25 | } 26 | return super.canActivate(context); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/server/commands/decrypt/decrypt_command.ts: -------------------------------------------------------------------------------- 1 | import * as types from '~shared/types'; 2 | 3 | import { AuthenticatedLnd } from 'lightning'; 4 | import { decryptWithNode } from 'balanceofsatoshis/encryption'; 5 | 6 | /** Decrypt data from node 7 | 8 | { 9 | encrypted: 10 | lnd: 11 | } 12 | 13 | @returns via Promise 14 | { 15 | message: 16 | with_alias: 17 | with_public_key: 18 | } 19 | */ 20 | 21 | type Args = { 22 | args: types.commandDecrypt; 23 | lnd: AuthenticatedLnd; 24 | }; 25 | const decryptCommand = async ({ args, lnd }: Args): Promise<{ result: any }> => { 26 | const result = await decryptWithNode({ 27 | lnd, 28 | encrypted: args.encrypted, 29 | }); 30 | 31 | return { result }; 32 | }; 33 | 34 | export default decryptCommand; 35 | -------------------------------------------------------------------------------- /src/server/modules/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { isProduction, jwtConstants } from '../../utils/constants'; 2 | 3 | import { AuthService } from './auth.service'; 4 | import { JwtModule } from '@nestjs/jwt'; 5 | import { JwtStrategy } from './jwt.strategy'; 6 | import { LocalStrategy } from './local.strategy'; 7 | import { Module } from '@nestjs/common'; 8 | import { PassportModule } from '@nestjs/passport'; 9 | import { UsersModule } from '../users/users.module'; 10 | 11 | // AuthModule: Module for the authentication service 12 | 13 | @Module({ 14 | imports: [ 15 | UsersModule, 16 | PassportModule, 17 | JwtModule.register({ 18 | secret: jwtConstants.secret, 19 | signOptions: { expiresIn: !!isProduction ? process.env.SESSION_DURATION || '40m' : '1h' }, 20 | }), 21 | ], 22 | providers: [AuthService, LocalStrategy, JwtStrategy], 23 | exports: [AuthService], 24 | }) 25 | export class AuthModule {} 26 | -------------------------------------------------------------------------------- /src/server/commands/encrypt/encrypt_command.ts: -------------------------------------------------------------------------------- 1 | import * as types from '~shared/types'; 2 | 3 | import { AuthenticatedLnd } from 'lightning'; 4 | import { encryptToNode } from 'balanceofsatoshis/encryption'; 5 | 6 | /** Encrypt data to a node 7 | 8 | { 9 | lnd: 10 | message: 11 | [to]: 12 | } 13 | 14 | @returns via Promise 15 | { 16 | encrypted: 17 | to: 18 | } 19 | */ 20 | type Args = { 21 | args: types.commandEncrypt; 22 | lnd: AuthenticatedLnd; 23 | }; 24 | 25 | const encryptCommand = async ({ args, lnd }: Args): Promise<{ result: any }> => { 26 | const result = await encryptToNode({ 27 | lnd, 28 | message: args.message, 29 | to: args.to, 30 | }); 31 | 32 | return { result }; 33 | }; 34 | 35 | export default encryptCommand; 36 | -------------------------------------------------------------------------------- /src/server/modules/auth/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, Injectable } from '@nestjs/common'; 2 | 3 | import { AuthService } from './auth.service'; 4 | import { PassportStrategy } from '@nestjs/passport'; 5 | import { Strategy } from 'passport-local'; 6 | 7 | /* 8 | Implementation of the local strategy, validate the user's credentials 9 | Call the auth service to validate the user's credentials 10 | If the user is valid, return the user's JWT 11 | */ 12 | 13 | @Injectable() 14 | export class LocalStrategy extends PassportStrategy(Strategy) { 15 | constructor(private authService: AuthService) { 16 | super(); 17 | } 18 | 19 | async validate(username: string, password: string): Promise { 20 | const user = await this.authService.validateUser(username, password); 21 | if (!user) { 22 | throw new HttpException('InvalidUsernameOrPassword', 401); 23 | } 24 | return user; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/server/chainfees.test.ts: -------------------------------------------------------------------------------- 1 | import { SpawnLightningServerType, spawnLightningServer } from '../utils/spawn_lightning_server'; 2 | import { expect, test } from '@playwright/test'; 3 | 4 | import { chainfeesCommand } from '../../src/server/commands/'; 5 | 6 | test.describe('Test Chainfees command on the node.js side', async () => { 7 | let lightning: SpawnLightningServerType; 8 | 9 | test.beforeAll(async () => { 10 | lightning = await spawnLightningServer(); 11 | }); 12 | 13 | test('run Chainfees command', async () => { 14 | const args = { 15 | blocks: 6, 16 | }; 17 | const { result } = await chainfeesCommand({ args, lnd: lightning.lnd }); 18 | console.log('chain fees----', result); 19 | expect(result.current_block_hash).toBeTruthy(); 20 | expect(result.fee_by_block_target).toBeTruthy(); 21 | }); 22 | 23 | test.afterAll(async () => { 24 | await lightning.kill({}); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/server/utxos.test.ts: -------------------------------------------------------------------------------- 1 | import { SpawnLightningServerType, spawnLightningServer } from '../utils/spawn_lightning_server'; 2 | import { expect, test } from '@playwright/test'; 3 | 4 | import { utxosCommand } from '../../src/server/commands'; 5 | 6 | test.describe('Test Utxos command on the node.js side', async () => { 7 | let lightning: SpawnLightningServerType; 8 | 9 | test.beforeAll(async () => { 10 | lightning = await spawnLightningServer(); 11 | }); 12 | 13 | test('run forwards command', async () => { 14 | const args = { 15 | count_below: 0, 16 | is_confirmed: false, 17 | is_count: false, 18 | min_tokens: 0, 19 | }; 20 | 21 | const { result } = await utxosCommand({ args, lnd: lightning.lnd }); 22 | console.log('utxos----', result); 23 | expect(result.utxos).toBeTruthy(); 24 | }); 25 | 26 | test.afterAll(async () => { 27 | await lightning.kill({}); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/server/modules/credentials/credentials.service.ts: -------------------------------------------------------------------------------- 1 | import { checkConnection, putSavedCredentials } from '~server/lnd'; 2 | 3 | import { Injectable } from '@nestjs/common'; 4 | import { credentialsDto } from '~shared/commands.dto'; 5 | 6 | /** Credentials Service: Handles routes to the credentials service 7 | { 8 | cert: 9 | is_default: 10 | macaroon: 11 | node: 12 | socket: 13 | } 14 | @returns via Promise 15 | { 16 | connection: 17 | error: 18 | result: 19 | */ 20 | 21 | @Injectable() 22 | export class CredentialsService { 23 | async post(args: credentialsDto) { 24 | const { result } = await putSavedCredentials(args); 25 | const connection = await checkConnection({ node: args.node }); 26 | return { connection, result }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/server/commands/grpc_utils/format_tokens.ts: -------------------------------------------------------------------------------- 1 | const fullTokensType = 'full'; 2 | const isString = (n: any) => typeof n === 'string'; 3 | const tokensAsBigUnit = (tokens: number) => (tokens / 1e8).toFixed(8); 4 | 5 | /** Format tokens for display 6 | { 7 | [none]: 8 | tokens: 9 | } 10 | @returns 11 | { 12 | display: 13 | } 14 | */ 15 | 16 | type Args = { 17 | none?: string; 18 | tokens: number; 19 | }; 20 | const formatTokens = ({ none, tokens }: Args) => { 21 | if (isString(none) && !tokens) { 22 | return { display: none }; 23 | } 24 | 25 | // Exit early for tokens environment displays the value with no leading zero 26 | if (process.env.PREFERRED_TOKENS_TYPE === fullTokensType) { 27 | return { display: tokens.toLocaleString() }; 28 | } 29 | 30 | return { display: tokensAsBigUnit(tokens) }; 31 | }; 32 | 33 | export default formatTokens; 34 | -------------------------------------------------------------------------------- /tests/server/forwards.test.ts: -------------------------------------------------------------------------------- 1 | import { SpawnLightningServerType, spawnLightningServer } from '../utils/spawn_lightning_server'; 2 | import { expect, test } from '@playwright/test'; 3 | 4 | import { forwardsCommand } from '../../src/server/commands'; 5 | 6 | test.describe('Test Forwards command on the node.js side', async () => { 7 | let lightning: SpawnLightningServerType; 8 | 9 | test.beforeAll(async () => { 10 | lightning = await spawnLightningServer(); 11 | }); 12 | 13 | test('run forwards command', async () => { 14 | const args = { 15 | days: 4, 16 | from: 'alice', 17 | sort: 'earned_in', 18 | tags: [], 19 | to: 'bob', 20 | }; 21 | const { result } = await forwardsCommand({ args, lnd: lightning.lnd }); 22 | console.log('forwards----', result); 23 | expect(result.peers).toBeTruthy(); 24 | }); 25 | 26 | test.afterAll(async () => { 27 | await lightning.kill({}); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/client/output/EncryptOutput.tsx: -------------------------------------------------------------------------------- 1 | import { CopyText } from '~client/standard_components/app-components'; 2 | import React from 'react'; 3 | 4 | const substring = n => n.slice(0, 20) + '......' + n.slice(-20); 5 | 6 | /* 7 | Renders the output of the Encrypt command. 8 | */ 9 | 10 | type Args = { 11 | data: { 12 | encrypted: string; 13 | to: string; 14 | }; 15 | }; 16 | const EncryptOutput = ({ data }: Args) => { 17 | return ( 18 |
19 |

{`Encrypted: ${substring(data.encrypted)}`}

20 | 21 |

{`To: ${data.to}`}

22 |
23 | ); 24 | }; 25 | 26 | export default EncryptOutput; 27 | 28 | const styles = { 29 | div: { 30 | marginTop: '100px', 31 | marginLeft: '10px', 32 | }, 33 | text: { 34 | fontSize: '15px', 35 | fontWeight: 'bold', 36 | display: 'inline-block', 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /tests/server/chainDeposit.test.ts: -------------------------------------------------------------------------------- 1 | import { SpawnLightningServerType, spawnLightningServer } from '../utils/spawn_lightning_server'; 2 | import { expect, test } from '@playwright/test'; 3 | 4 | import { chainDepositCommand } from '../../src/server/commands/'; 5 | 6 | test.describe('Test ChainDeposit command on the node.js side', async () => { 7 | let lightning: SpawnLightningServerType; 8 | 9 | test.beforeAll(async () => { 10 | lightning = await spawnLightningServer(); 11 | }); 12 | 13 | test('run ChainDeposit command', async () => { 14 | const args = { 15 | amount: 1000, 16 | format: 'p2wpkh', 17 | }; 18 | const { result } = await chainDepositCommand({ args, lnd: lightning.lnd }); 19 | console.log('chain deposit----', result); 20 | expect(result.address).toBeTruthy(); 21 | expect(result.url).toBeTruthy(); 22 | }); 23 | 24 | test.afterAll(async () => { 25 | await lightning.kill({}); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/server/authentication/getAccountInfo.ts: -------------------------------------------------------------------------------- 1 | import { auto } from 'async'; 2 | import { homedir } from 'os'; 3 | import { join } from 'path'; 4 | import { readFile } from 'fs'; 5 | 6 | const auth = 'auth.json'; 7 | const home = '.bosgui'; 8 | 9 | /** Read account info from auth.json 10 | 11 | @returns via Promise 12 | { 13 | result: 14 | } 15 | */ 16 | 17 | type Tasks = { 18 | readFile: any; 19 | }; 20 | 21 | const getAccountInfo = async (): Promise => { 22 | const result = await auto({ 23 | readFile: (cbk: any) => { 24 | const path = join(...[homedir(), home, auth]); 25 | 26 | return readFile(path, (err: any, data: any) => { 27 | // Ignore errors, the file may not exist 28 | if (!!err) { 29 | return cbk(); 30 | } 31 | 32 | return cbk(null, data); 33 | }); 34 | }, 35 | }); 36 | 37 | return result.readFile; 38 | }; 39 | 40 | export default getAccountInfo; 41 | -------------------------------------------------------------------------------- /tests/client/fees.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import { setCookie } from '../utils/setAccessToken'; 4 | import { testConstants } from '../utils/constants'; 5 | 6 | test.describe('Test the Fees command client page', async () => { 7 | test.beforeEach(async ({ page }) => { 8 | await setCookie({ page }); 9 | }); 10 | 11 | test('test the Fees command page and input values', async ({ page }) => { 12 | await page.goto(testConstants.commandsPage); 13 | await page.click('#Fees'); 14 | await expect(page).toHaveTitle('Fees'); 15 | 16 | await page.type('#node', 'testnode1'); 17 | 18 | await page.click('text=run command'); 19 | await page.waitForTimeout(1000); 20 | 21 | await expect(page.locator('#feesOutput')).toBeVisible(); 22 | await page.click('text=home'); 23 | }); 24 | 25 | test.afterEach(async ({ page }) => { 26 | await page.context().clearCookies(); 27 | await page.close(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/utils/global_spawn_lightning.ts: -------------------------------------------------------------------------------- 1 | import { SpawnLightningType, spawnLightning } from './spawn_lightning'; 2 | 3 | import { putSavedCredentials } from '../../src/server/lnd'; 4 | 5 | let lightning: SpawnLightningType; 6 | 7 | const startGlobalContainer = async () => { 8 | lightning = await spawnLightning(); 9 | const node = 'testnode1'; 10 | 11 | const { result } = await putSavedCredentials({ 12 | auth_type: 'credentials', 13 | cert: lightning.cert, 14 | macaroon: lightning.macaroon, 15 | socket: lightning.socket, 16 | node, 17 | is_default: false, 18 | }); 19 | 20 | if (!result) { 21 | throw new Error('Failed to put saved credentials'); 22 | } 23 | 24 | return lightning; 25 | }; 26 | 27 | const returnGlobalLightning = async () => { 28 | return lightning; 29 | }; 30 | 31 | const killGlobalContainer = async () => { 32 | await lightning.kill({}); 33 | }; 34 | 35 | export { killGlobalContainer, returnGlobalLightning, startGlobalContainer }; 36 | -------------------------------------------------------------------------------- /src/server/modules/lnd/lnd.service.ts: -------------------------------------------------------------------------------- 1 | import { authenticatedLnd, getLnds } from '~server/lnd'; 2 | 3 | import { Injectable } from '@nestjs/common'; 4 | 5 | /** 6 | Authenticated LND 7 | { 8 | [node]: 9 | } 10 | @returns via Promise 11 | { 12 | lnd: 13 | } 14 | 15 | Authenticated LNDs 16 | { 17 | [nodes]: 18 | } 19 | @returns via Promise 20 | { 21 | lnds: 22 | } 23 | */ 24 | 25 | @Injectable() 26 | export class LndService { 27 | // Get a single authenticated LND for a node 28 | static async authenticatedLnd(args: { node: string }) { 29 | const { lnd } = await authenticatedLnd({ node: args.node }); 30 | 31 | return lnd; 32 | } 33 | 34 | // Get multiple authenticated LNDs for a node array 35 | static async getLnds(args: { nodes?: string[] }) { 36 | const { lnds } = await getLnds({ nodes: args.nodes }); 37 | 38 | return lnds; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/server/lnd/check_connection.ts: -------------------------------------------------------------------------------- 1 | import authenticatedLnd from './authenticated_lnd'; 2 | import getSavedCredentials from './get_saved_credentials'; 3 | import { verifyAccess } from 'lightning'; 4 | 5 | const stringify = (data: any) => JSON.stringify(data); 6 | 7 | /** Check if lnd is connected 8 | 9 | @returns via Promise 10 | { 11 | result: 12 | [error]: 13 | } 14 | */ 15 | 16 | type Args = { 17 | node: string; 18 | }; 19 | 20 | const checkConnection = async ({ node }: Args): Promise<{ [key: string]: string | boolean }> => { 21 | try { 22 | const permissions = ['info:read']; 23 | const { lnd } = await authenticatedLnd({ node }); 24 | const { macaroon } = await getSavedCredentials({ node }); 25 | const hasAccess = (await verifyAccess({ lnd, macaroon, permissions })).is_valid; 26 | 27 | return { hasAccess }; 28 | } catch (error) { 29 | return { error: stringify(error) }; 30 | } 31 | }; 32 | 33 | export default checkConnection; 34 | -------------------------------------------------------------------------------- /tests/client/reconnect.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import { setCookie } from '../utils/setAccessToken'; 4 | import { testConstants } from '../utils/constants'; 5 | 6 | test.describe('Test the Reconnect command client page', async () => { 7 | test.beforeEach(async ({ page }) => { 8 | await setCookie({ page }); 9 | }); 10 | 11 | test('test the Reconnect command page and input values', async ({ page }) => { 12 | await page.goto(testConstants.commandsPage); 13 | await page.click('#Reconnect'); 14 | await expect(page).toHaveTitle('Reconnect'); 15 | 16 | await page.type('#node', 'testnode1'); 17 | 18 | await page.click('text=run command'); 19 | await page.waitForTimeout(1000); 20 | 21 | await expect(page.locator('#reconnectOutput')).toBeVisible(); 22 | await page.click('text=home'); 23 | }); 24 | 25 | test.afterEach(async ({ page }) => { 26 | await page.context().clearCookies(); 27 | await page.close(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/client/standard_components/lndboss/RawApiList.tsx: -------------------------------------------------------------------------------- 1 | import { Autocomplete, TextField } from '@mui/material'; 2 | 3 | import React from 'react'; 4 | import { rawApi } from '~shared/raw_api'; 5 | 6 | // Renders the raw api list for call command 7 | 8 | const styles = { 9 | textField: { 10 | width: '600px', 11 | }, 12 | }; 13 | 14 | type Args = { 15 | setMethod: (method: string) => void; 16 | }; 17 | 18 | const RawApiList = ({ setMethod }: Args) => { 19 | const methods = rawApi.calls.map(call => call.method); 20 | 21 | return ( 22 | <> 23 | } 28 | onChange={(_event: any, newValue: any) => { 29 | setMethod(newValue || ''); 30 | }} 31 | style={styles.textField} 32 | /> 33 | 34 | ); 35 | }; 36 | 37 | export default RawApiList; 38 | -------------------------------------------------------------------------------- /tests/server/call.test.ts: -------------------------------------------------------------------------------- 1 | import { SpawnLightningServerType, spawnLightningServer } from '../utils/spawn_lightning_server'; 2 | import { expect, test } from '@playwright/test'; 3 | 4 | import { callCommand } from '../../src/server/commands/'; 5 | 6 | test.describe('Test Call command on the node.js side', async () => { 7 | let lightning: SpawnLightningServerType; 8 | 9 | test.beforeAll(async () => { 10 | lightning = await spawnLightningServer(); 11 | }); 12 | 13 | test('run call command', async () => { 14 | const args = { 15 | method: 'createInvoice', 16 | postArgs: { 17 | cltv_delta: 144, 18 | description: 'testdescription', 19 | is_including_private_channels: true, 20 | mtokens: '1000', 21 | }, 22 | }; 23 | const result = await callCommand({ args, lnd: lightning.lnd }); 24 | 25 | console.log('call----', result.call); 26 | expect(result).toBeTruthy(); 27 | }); 28 | 29 | test.afterAll(async () => { 30 | await lightning.kill({}); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/client/utils/fetch_peers_and_tags.ts: -------------------------------------------------------------------------------- 1 | import * as types from '~shared/types'; 2 | 3 | import { axiosGetNoLoading } from './axios'; 4 | 5 | const fetchPeersAndTags = async ({}) => { 6 | const list = []; 7 | const tagsQuery: types.commandTags = { 8 | icon: '', 9 | add: '', 10 | id: '', 11 | is_avoided: false, 12 | remove: '', 13 | tag: '', 14 | }; 15 | 16 | const [responsePeers, responseTags] = await Promise.all([ 17 | axiosGetNoLoading({ path: 'grpc/get-peers-all-nodes', query: {} }), 18 | axiosGetNoLoading({ path: 'tags', query: tagsQuery }), 19 | ]); 20 | 21 | if (!!responsePeers) { 22 | responsePeers.forEach(peers => { 23 | peers.result.forEach(p => { 24 | list.push(`${p.alias}\n${p.public_key}\nNode: ${peers.node}`); 25 | }); 26 | }); 27 | 28 | if (!!responseTags) { 29 | responseTags.forEach(tag => { 30 | list.push(`Tag: \n${tag.alias}`); 31 | }); 32 | } 33 | 34 | return { list }; 35 | } 36 | }; 37 | 38 | export default fetchPeersAndTags; 39 | -------------------------------------------------------------------------------- /src/client/output/DecryptOutput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* 4 | Renders the output of the Decrypt command. 5 | */ 6 | 7 | type Args = { 8 | data: { 9 | message: string; 10 | with_alias: string; 11 | with_public_key: string; 12 | }; 13 | }; 14 | const DecryptOutput = ({ data }: Args) => { 15 | return ( 16 |
17 |
18 | Result 19 |
20 | 21 |
{JSON.stringify(data, null, 2)}
22 |
23 | ); 24 | }; 25 | 26 | export default DecryptOutput; 27 | 28 | const styles = { 29 | headerStyle: { 30 | backgroundColor: '#193549', 31 | padding: '5px 10px', 32 | fontFamily: 'monospace', 33 | color: '#ffc600', 34 | }, 35 | preStyle: { 36 | display: 'block', 37 | padding: '10px 30px', 38 | margin: '0', 39 | overflow: 'scroll', 40 | }, 41 | style: { 42 | backgroundColor: '#1f4662', 43 | color: '#fff', 44 | fontSize: '12px', 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /src/client/output/ChainDepositOutput.tsx: -------------------------------------------------------------------------------- 1 | import { CopyText } from '~client/standard_components/app-components'; 2 | import QRCode from 'qrcode.react'; 3 | import React from 'react'; 4 | 5 | /* 6 | Renders the output of the ChainDeposit command. 7 | */ 8 | 9 | const styles = { 10 | div: { 11 | marginTop: '100px', 12 | marginLeft: '10px', 13 | }, 14 | qr: { 15 | height: '250px', 16 | width: '250px', 17 | padding: '5px', 18 | }, 19 | text: { 20 | fontSize: '15px', 21 | fontWeight: 'bold', 22 | display: 'inline-block', 23 | }, 24 | }; 25 | 26 | type Data = { 27 | data: { 28 | address: string; 29 | url: string; 30 | }; 31 | }; 32 | 33 | const ChainDepositOutput = ({ data }: Data) => { 34 | return ( 35 |
36 | 37 |

{data.address}

38 | 39 |
40 | ); 41 | }; 42 | 43 | export default ChainDepositOutput; 44 | -------------------------------------------------------------------------------- /src/server/commands/joinGroupChannel/join_channel_group_command.ts: -------------------------------------------------------------------------------- 1 | import * as types from '~shared/types'; 2 | 3 | import { AuthenticatedLnd } from 'lightning'; 4 | import { Logger } from 'winston'; 5 | import { joinGroupChannel } from 'paid-services'; 6 | 7 | /** Join a channel group 8 | { 9 | code: 10 | lnd: 11 | logger: 12 | max_rate: 13 | } 14 | @returns via cbk or Promise 15 | { 16 | transaction_id: 17 | } 18 | */ 19 | 20 | type Args = { 21 | args: types.commandJoinChannelGroup; 22 | lnd: AuthenticatedLnd; 23 | logger: Logger; 24 | }; 25 | const joinChannelGroupCommand = async ({ args, lnd, logger }: Args) => { 26 | const result = await joinGroupChannel({ 27 | lnd, 28 | logger, 29 | code: args.code, 30 | max_rate: args.max_rate, 31 | }); 32 | 33 | return { result }; 34 | }; 35 | 36 | export default joinChannelGroupCommand; 37 | -------------------------------------------------------------------------------- /src/server/modules/boslogger/boslogger.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; 3 | import { Logger } from 'winston'; 4 | 5 | const stringify = (n: object) => JSON.stringify(n, null, 2); 6 | 7 | /** 8 | Bos Logger Service - Logs Serverside logs to the console 9 | { 10 | message: , 11 | type: , 12 | } 13 | */ 14 | 15 | @Injectable() 16 | export class BosloggerService { 17 | constructor(@Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: Logger) {} 18 | 19 | log({ message, type }: { message: any; type: string }) { 20 | if (type === 'error') { 21 | this.logger.error(message); 22 | } 23 | 24 | if (type === 'warn') { 25 | this.logger.warn(message); 26 | } 27 | 28 | if (type === 'info') { 29 | this.logger.log(message, { level: 'info' }); 30 | } 31 | 32 | if (type === 'json') { 33 | this.logger.log(stringify(message), { level: 'info' }); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/on-push-dockerhub.yml: -------------------------------------------------------------------------------- 1 | name: Build on push 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - '**/*.md' 9 | - '**/*.yml' 10 | - '**/*.yaml' 11 | - 'README.md' 12 | - 'CHANGELOG.md' 13 | 14 | jobs: 15 | build: 16 | name: Build image 17 | runs-on: ubuntu-22.04 18 | 19 | steps: 20 | - name: Checkout project 21 | uses: actions/checkout@v3 22 | 23 | - name: Login to DockerHub 24 | uses: docker/login-action@v2 25 | with: 26 | username: ${{ secrets.DOCKER_USER }} 27 | password: ${{ secrets.DOCKER_PASSWORD }} 28 | 29 | - name: Set up QEMU 30 | uses: docker/setup-qemu-action@v2 31 | id: qemu 32 | 33 | - name: Setup Docker buildx action 34 | uses: docker/setup-buildx-action@v2 35 | id: buildx 36 | 37 | - name: Run Docker buildx 38 | run: | 39 | docker buildx build \ 40 | --platform linux/amd64,linux/arm64 \ 41 | --no-cache -t niteshbalusu/lndboss:latest --push . 42 | -------------------------------------------------------------------------------- /src/server/modules/external-services/external-services.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleInit } from '@nestjs/common'; 2 | 3 | import { BosloggerService } from '../boslogger/boslogger.service'; 4 | import { CronService } from '../cron/cron.service'; 5 | import { ambossHealthCheck } from '~server/external_services_utils'; 6 | 7 | /** 8 | @onModuleInit 9 | { 10 | Check if AMBOSS_HEALTH_CHECK ENV variable is set 11 | Ping amboss health check 12 | } 13 | 14 | @pingAmbossHealthCheck 15 | { 16 | logger: , 17 | } 18 | */ 19 | 20 | @Injectable() 21 | export class ExternalServicesService implements OnModuleInit { 22 | constructor(private logger: BosloggerService, private cronService: CronService) {} 23 | async onModuleInit(): Promise { 24 | this.pingAmbossHealthCheck({ logger: this.logger }); 25 | } 26 | 27 | async pingAmbossHealthCheck({ logger }) { 28 | try { 29 | await ambossHealthCheck({ logger }); 30 | } catch (error) { 31 | logger.log({ type: 'error', message: JSON.stringify(error) }); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/client/standard_components/app-components/Startup.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | import { CssBaseline } from '@mui/material'; 4 | import Router from 'next/router'; 5 | import { clientConstants } from '../../utils/constants'; 6 | import { useRouter } from 'next/navigation'; 7 | 8 | /* 9 | Render bos startup video for 3 seconds and redirect to the commands page. 10 | */ 11 | 12 | const styles: any = { 13 | video: { 14 | position: 'fixed', 15 | right: 0, 16 | bottom: 0, 17 | minWidth: '100%', 18 | minHeight: '100%', 19 | }, 20 | }; 21 | 22 | const Startup = () => { 23 | const router = useRouter(); 24 | useEffect(() => { 25 | const id = setTimeout(() => { 26 | router.push(clientConstants.loginUrl); 27 | }, 3000); 28 | 29 | return () => clearTimeout(id); 30 | }, []); 31 | return ( 32 | 33 | 36 | 37 | ); 38 | }; 39 | 40 | export default Startup; 41 | -------------------------------------------------------------------------------- /src/server/commands/rebalance/encode_rebalance_params.ts: -------------------------------------------------------------------------------- 1 | import { encodeTlvStream } from 'bolt01'; 2 | 3 | const isString = (n: any) => typeof n === 'string'; 4 | const typeRebalanceId = '7'; 5 | const typeRebalanceData = '8'; 6 | const stringToHex = (n: string) => Buffer.from(n, 'hex'); 7 | 8 | /** Encode the follow node params 9 | 10 | [0]: 11 | 1: 12 | 13 | { 14 | id: 15 | } 16 | 17 | @throws 18 | 19 | 20 | @returns 21 | { 22 | encoded: 23 | } 24 | */ 25 | 26 | type Args = { 27 | data: string; 28 | id: string; 29 | }; 30 | 31 | const encodeRebalanceParams = ({ data, id }: Args) => { 32 | if (!isString(id) || !isString(data)) { 33 | throw new Error('ExpectedRebalanceDataAndIdToEncodeFollowParams'); 34 | } 35 | 36 | return encodeTlvStream({ 37 | records: [ 38 | { type: typeRebalanceId, value: stringToHex(id) }, 39 | { type: typeRebalanceData, value: stringToHex(data) }, 40 | ], 41 | }); 42 | }; 43 | 44 | export default encodeRebalanceParams; 45 | -------------------------------------------------------------------------------- /tests/client/graph.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import commands from '../../src/client/commands'; 4 | import { setCookie } from '../utils/setAccessToken'; 5 | import { testConstants } from '../utils/constants'; 6 | 7 | const GraphCommand = commands.find(n => n.value === 'Graph'); 8 | 9 | test.describe('Test the Graph command client page', async () => { 10 | test.beforeEach(async ({ page }) => { 11 | await setCookie({ page }); 12 | }); 13 | 14 | test('test the Graph command page and input values', async ({ page }) => { 15 | await page.goto(testConstants.commandsPage); 16 | await page.click('#Graph'); 17 | await expect(page).toHaveTitle('Graph'); 18 | 19 | await page.type(`#${GraphCommand?.args?.alias_or_pubkey}`, 'alice'); 20 | await page.type('#node', 'testnode1'); 21 | 22 | await page.click('text=run command'); 23 | await page.waitForTimeout(1000); 24 | 25 | await page.click('text=home'); 26 | }); 27 | 28 | test.afterEach(async ({ page }) => { 29 | await page.context().clearCookies(); 30 | await page.close(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/server/commands/chainfees/chainfees_command.ts: -------------------------------------------------------------------------------- 1 | import * as types from '~shared/types'; 2 | 3 | import { AuthenticatedLnd } from 'lightning'; 4 | import { getChainFees } from 'balanceofsatoshis/chain'; 5 | import { httpLogger } from '~server/utils/global_functions'; 6 | 7 | /** Get chain fees 8 | 9 | Requires that the lnd is built with walletrpc 10 | 11 | { 12 | [blocks]: 13 | lnd: 14 | } 15 | 16 | @returns via Promise 17 | { 18 | current_block_hash: 19 | fee_by_block_target: { 20 | $number: 21 | } 22 | } 23 | */ 24 | 25 | type Args = { 26 | args: types.commandChainfees; 27 | lnd: AuthenticatedLnd; 28 | }; 29 | const chainfeesCommand = async ({ args, lnd }: Args): Promise<{ result: any }> => { 30 | try { 31 | const result = await getChainFees({ 32 | lnd, 33 | blocks: args.blocks, 34 | }); 35 | 36 | return { result }; 37 | } catch (error) { 38 | httpLogger({ error }); 39 | } 40 | }; 41 | 42 | export default chainfeesCommand; 43 | -------------------------------------------------------------------------------- /src/server/modules/rebalance/rebalance.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post, Query } from '@nestjs/common'; 2 | import { deleteRebalanceDto, rebalanceDto, rebalanceScheduleDto } from '~shared/commands.dto'; 3 | import { RebalanceService } from './rebalance.service'; 4 | 5 | // Rebalance controller: Defines routes for rebalance command 6 | 7 | @Controller() 8 | export class RebalanceController { 9 | constructor(private rebalanceService: RebalanceService) {} 10 | @Post('api/rebalance/schedule') 11 | async scheduleRebalance(@Body() args: rebalanceScheduleDto) { 12 | return this.rebalanceService.scheduleRebalance(args); 13 | } 14 | 15 | @Get('api/rebalance') 16 | async rebalance(@Query() args: rebalanceDto) { 17 | return this.rebalanceService.rebalance(args); 18 | } 19 | 20 | @Get('api/rebalance/getrebalances') 21 | async getRebalances() { 22 | return this.rebalanceService.getRebalances(); 23 | } 24 | 25 | @Get('api/rebalance/deleterebalance') 26 | async deleteRebalance(@Query() args: deleteRebalanceDto) { 27 | return this.rebalanceService.deleteRebalance(args); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nitesh Balusu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/server/commands/createChannelGroup/create_channel_group_command.ts: -------------------------------------------------------------------------------- 1 | import * as types from '~shared/types'; 2 | 3 | import { AuthenticatedLnd } from 'lightning'; 4 | import { Logger } from 'winston'; 5 | import { createGroupChannel } from 'paid-services'; 6 | 7 | /** Create a channel group 8 | { 9 | capacity: 10 | count: 11 | lnd: 12 | logger: 13 | rate: 14 | } 15 | @returns via Promise 16 | { 17 | transaction_id: 18 | } 19 | */ 20 | 21 | type Args = { 22 | args: types.commandCreateChannelGroup; 23 | lnd: AuthenticatedLnd; 24 | logger: Logger; 25 | }; 26 | const createChannelGroupCommand = async ({ args, lnd, logger }: Args) => { 27 | const result = await createGroupChannel({ 28 | lnd, 29 | logger, 30 | capacity: args.capacity, 31 | count: args.count, 32 | members: args.members || [], 33 | rate: args.rate, 34 | }); 35 | return { result }; 36 | }; 37 | 38 | export default createChannelGroupCommand; 39 | -------------------------------------------------------------------------------- /src/client/standard_components/app-components/BasicDatePicker.tsx: -------------------------------------------------------------------------------- 1 | import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; 2 | import { DatePicker } from '@mui/x-date-pickers/DatePicker'; 3 | import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; 4 | import React from 'react'; 5 | import TextField from '@mui/material/TextField'; 6 | import moment from 'moment'; 7 | 8 | // Renders a date picker 9 | 10 | type Args = { 11 | id: string; 12 | label: string; 13 | setValue: (value: string) => void; 14 | value: string; 15 | }; 16 | const BasicDatePicker = ({ id, label, setValue, value }: Args) => { 17 | return ( 18 | 19 | { 24 | !!newValue ? setValue(moment(newValue).format('YYYY-MM-DD')) : setValue(null); 25 | }} 26 | renderInput={params => } 27 | inputFormat="yyyy-MM-dd" 28 | /> 29 | 30 | ); 31 | }; 32 | 33 | export default BasicDatePicker; 34 | -------------------------------------------------------------------------------- /tests/client/find.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import commands from '../../src/client/commands'; 4 | import { setCookie } from '../utils/setAccessToken'; 5 | import { testConstants } from '../utils/constants'; 6 | 7 | const FindCommand = commands.find(n => n.value === 'Find'); 8 | 9 | test.describe('Test the Find command client page', async () => { 10 | test.beforeEach(async ({ page }) => { 11 | await setCookie({ page }); 12 | }); 13 | 14 | test('test the Find command page and input values', async ({ page }) => { 15 | await page.goto(testConstants.commandsPage); 16 | await page.click('text=Find'); 17 | await expect(page).toHaveTitle('Find'); 18 | 19 | await page.type(`#${FindCommand?.args?.query}`, 'alice'); 20 | await page.type('#node', 'testnode1'); 21 | 22 | await page.click('text=run command'); 23 | await page.waitForTimeout(1000); 24 | 25 | await expect(page.locator('#findoutput')).toBeVisible(); 26 | await page.click('text=home'); 27 | }); 28 | 29 | test.afterEach(async ({ page }) => { 30 | await page.context().clearCookies(); 31 | await page.close(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/server/commands/fees/read_fees_file.ts: -------------------------------------------------------------------------------- 1 | import { auto } from 'async'; 2 | import { homedir } from 'os'; 3 | import { join } from 'path'; 4 | import { readFile } from 'fs'; 5 | 6 | const feesFile = 'fees.json'; 7 | const home = '.bosgui'; 8 | const { parse } = JSON; 9 | 10 | /** Read the fees.json file 11 | 12 | @returns via Promise 13 | { 14 | data: 15 | } 16 | */ 17 | type Tasks = { 18 | readFile: { 19 | data: any; 20 | }; 21 | }; 22 | const readFeesFile = async ({}) => { 23 | return ( 24 | await auto({ 25 | readFile: (cbk: any) => { 26 | const filePath = join(...[homedir(), home, feesFile]); 27 | readFile(filePath, (err, res) => { 28 | if (!!err || !res) { 29 | return cbk(null, false); 30 | } 31 | 32 | try { 33 | parse(res.toString()); 34 | } catch (err) { 35 | return cbk([400, 'ExpectedValidFeesJsonFileToScheduleFeesCommand']); 36 | } 37 | 38 | return cbk(null, { data: parse(res.toString()) }); 39 | }); 40 | }, 41 | }) 42 | ).readFile; 43 | }; 44 | 45 | export default readFeesFile; 46 | -------------------------------------------------------------------------------- /tests/client/closed.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import commands from '../../src/client/commands'; 4 | import { setCookie } from '../utils/setAccessToken'; 5 | import { testConstants } from '../utils/constants'; 6 | 7 | const ClosedCommand = commands.find(n => n.value === 'Closed'); 8 | 9 | test.describe('Test the Closed command client page', async () => { 10 | test.beforeEach(async ({ page }) => { 11 | await setCookie({ page }); 12 | }); 13 | 14 | test('test the Forwards command page and input values', async ({ page }) => { 15 | await page.goto(testConstants.commandsPage); 16 | await page.click('text=Closed'); 17 | await expect(page).toHaveTitle('Closed'); 18 | 19 | await page.type(`#${ClosedCommand?.flags?.limit}`, '5'); 20 | await page.type('#node', 'testnode1'); 21 | 22 | await page.click('text=run command'); 23 | await page.waitForTimeout(1000); 24 | 25 | await expect(page.locator('#closedoutput')).toBeVisible(); 26 | await page.click('text=home'); 27 | }); 28 | 29 | test.afterEach(async ({ page }) => { 30 | await page.context().clearCookies(); 31 | await page.close(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/client/register_charts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArcElement, 3 | BarController, 4 | BarElement, 5 | BubbleController, 6 | CategoryScale, 7 | Chart, 8 | Decimation, 9 | DoughnutController, 10 | Filler, 11 | Legend, 12 | LineController, 13 | LineElement, 14 | LinearScale, 15 | LogarithmicScale, 16 | PieController, 17 | PointElement, 18 | PolarAreaController, 19 | RadarController, 20 | RadialLinearScale, 21 | ScatterController, 22 | TimeScale, 23 | TimeSeriesScale, 24 | Title, 25 | Tooltip, 26 | } from 'chart.js'; 27 | 28 | const resgisterCharts = () => { 29 | Chart.register( 30 | ArcElement, 31 | LineElement, 32 | BarElement, 33 | PointElement, 34 | BarController, 35 | BubbleController, 36 | DoughnutController, 37 | LineController, 38 | PieController, 39 | PolarAreaController, 40 | RadarController, 41 | ScatterController, 42 | CategoryScale, 43 | LinearScale, 44 | LogarithmicScale, 45 | RadialLinearScale, 46 | TimeScale, 47 | TimeSeriesScale, 48 | Decimation, 49 | Filler, 50 | Legend, 51 | Title, 52 | Tooltip 53 | ); 54 | }; 55 | 56 | export default resgisterCharts; 57 | -------------------------------------------------------------------------------- /src/client/dashboard/PendingChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | import { BasicTable } from '~client/standard_components/app-components'; 4 | import { axiosPost } from '~client/utils/axios'; 5 | import resgisterCharts from '~client/register_charts'; 6 | import { selectedSavedNode } from '~client/utils/constants'; 7 | import { useLoading } from '~client/hooks/useLoading'; 8 | 9 | // Calls NestJs Server and returns pending payments and channels 10 | 11 | const PendingChart = () => { 12 | const [data, setData] = useState(undefined); 13 | 14 | useEffect(() => { 15 | const fetchData = async () => { 16 | const postBody = { 17 | node: selectedSavedNode(), 18 | }; 19 | 20 | useLoading({ isLoading: true }); 21 | const result = await axiosPost({ path: 'grpc/get-pending', postBody }); 22 | 23 | if (!!result) { 24 | setData(result); 25 | } 26 | 27 | useLoading({ isLoading: false }); 28 | }; 29 | 30 | fetchData(); 31 | }, []); 32 | 33 | resgisterCharts(); 34 | return !!data && !!data.length ? : null; 35 | }; 36 | 37 | export default PendingChart; 38 | -------------------------------------------------------------------------------- /src/client/output/PriceOutput.tsx: -------------------------------------------------------------------------------- 1 | import * as YAML from 'json-to-pretty-yaml'; 2 | 3 | import React from 'react'; 4 | 5 | const stringify = (obj: any) => JSON.stringify(obj, null, 2); 6 | 7 | // Renders the output of bos price command 8 | 9 | const styles = { 10 | pre: { 11 | fontWeight: 'bold', 12 | }, 13 | }; 14 | 15 | type Args = { 16 | data: object; 17 | file: boolean; 18 | }; 19 | 20 | const ManageOutput = ({ data, file }: Args) => { 21 | if (!data || !Object.keys(data).length) { 22 | return

No data found

; 23 | } 24 | 25 | if (!!file) { 26 | return ( 27 | 32 | Results are ready, click here to download 33 | 34 | ); 35 | } 36 | 37 | const output = YAML.stringify(data); 38 | 39 | return
{output}
; 40 | }; 41 | 42 | const PriceOutput = ({ data, file }: Args) => { 43 | return ( 44 |
45 | 46 |
47 | ); 48 | }; 49 | 50 | export default PriceOutput; 51 | -------------------------------------------------------------------------------- /tests/client/open.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import { setCookie } from '../utils/setAccessToken'; 4 | import { testConstants } from '../utils/constants'; 5 | 6 | test.describe('Test the Open command client page', async () => { 7 | test.beforeEach(async ({ page }) => { 8 | await setCookie({ page }); 9 | }); 10 | 11 | test('test the Open page and input values', async ({ page }) => { 12 | await page.goto(testConstants.commandsPage); 13 | await page.click('text=Open'); 14 | await expect(page).toHaveTitle('Open'); 15 | 16 | await page.type(`#pubkey-0`, '034f94cccfb1ce5c31e0a367d4ee556f66a865b54b389f15781cacd6ed6611976d'); 17 | await page.type(`#amount-0`, '100000'); 18 | await page.type(`#address-0`, 'bcrt1qtdgrtstez46e8umx49tq22sh7terccyxjmwt2w'); 19 | await page.type(`#give-0`, '100000'); 20 | await page.type('#node', 'testnode1'); 21 | 22 | await page.click('text=Validate and Open Channels'); 23 | await page.waitForTimeout(1000); 24 | 25 | await page.click('text=home'); 26 | }); 27 | 28 | test.afterEach(async ({ page }) => { 29 | await page.context().clearCookies(); 30 | await page.close(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/client/utils/fee_strategies.ts: -------------------------------------------------------------------------------- 1 | export const configs = { 2 | defaultConfig: { 3 | configs: [ 4 | { 5 | name: 'Default', 6 | config: { 7 | strategy: 'static', 8 | fee_ppm: 200, 9 | id: ['xxx', 'yyy'], 10 | }, 11 | }, 12 | ], 13 | }, 14 | activityConfig: { 15 | configs: [ 16 | { 17 | name: 'Default', 18 | config: { 19 | strategy: 'static', 20 | fee_ppm: 200, 21 | id: ['xxx', 'yyy'], 22 | }, 23 | }, 24 | { 25 | name: 'low-out-flow', 26 | config: { 27 | activity_period: '15d', 28 | strategy: 'static', 29 | fee_ppm: 100, 30 | }, 31 | }, 32 | { 33 | name: 'very-low-out-flow', 34 | config: { 35 | activity_period: '7d', 36 | strategy: 'static', 37 | fee_ppm: 100, 38 | }, 39 | }, 40 | ], 41 | }, 42 | staticConfig: { 43 | configs: [ 44 | { 45 | name: 'Default', 46 | config: { 47 | strategy: 'static', 48 | fee_ppm: 200, 49 | id: ['xxx', 'yyy'], 50 | }, 51 | }, 52 | ], 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /src/client/output/ChainfeesOutput.tsx: -------------------------------------------------------------------------------- 1 | import * as YAML from 'json-to-pretty-yaml'; 2 | 3 | import React from 'react'; 4 | 5 | const stringify = (obj: any) => JSON.stringify(obj, null, 2); 6 | 7 | // Renders the output of bos chainfees command 8 | 9 | const styles = { 10 | pre: { 11 | fontWeight: 'bold', 12 | }, 13 | }; 14 | 15 | type Args = { 16 | data: object; 17 | file: boolean; 18 | }; 19 | 20 | const ManageOutput = ({ data, file }: Args) => { 21 | if (!data || !Object.keys(data).length) { 22 | return

No data found

; 23 | } 24 | 25 | if (!!file) { 26 | return ( 27 | 32 | Results are ready, click here to download 33 | 34 | ); 35 | } 36 | 37 | const output = YAML.stringify(data); 38 | 39 | return
{output}
; 40 | }; 41 | 42 | const ChainfeesOutput = ({ data, file }: Args) => { 43 | return ( 44 |
45 | 46 |
47 | ); 48 | }; 49 | 50 | export default ChainfeesOutput; 51 | -------------------------------------------------------------------------------- /src/server/commands/fees/fees_command.ts: -------------------------------------------------------------------------------- 1 | import * as types from '~shared/types'; 2 | 3 | import { AuthenticatedLnd } from 'lightning'; 4 | import { Logger } from 'winston'; 5 | import { adjustFees } from 'balanceofsatoshis/routing'; 6 | import { readFile } from 'fs'; 7 | 8 | /** View and adjust routing fees 9 | 10 | { 11 | [cltv_delta]: 12 | [fee_rate]: 13 | lnd: 14 | logger: 15 | to: [] 16 | } 17 | 18 | @returns via cbk or Promise 19 | { 20 | rows: [[]] 21 | } 22 | */ 23 | type Args = { 24 | args: types.commandFees; 25 | lnd: AuthenticatedLnd; 26 | logger: Logger; 27 | }; 28 | const feesCommand = async ({ args, lnd, logger }: Args) => { 29 | const toArray = !!args.to ? args.to.filter((n: string) => !!n) : []; 30 | 31 | const result = await adjustFees({ 32 | lnd, 33 | logger, 34 | cltv_delta: args.cltv_delta || undefined, 35 | fee_rate: args.fee_rate, 36 | fs: { getFile: readFile }, 37 | to: toArray, 38 | }); 39 | 40 | return { result }; 41 | }; 42 | 43 | export default feesCommand; 44 | -------------------------------------------------------------------------------- /tests/utils/spawn_lightning.ts: -------------------------------------------------------------------------------- 1 | import { spawnLightningDocker } from 'ln-docker-daemons'; 2 | const address = 'bcrt1qkznf8grqj8ed9xrt8mtxmj5da63cwyk3tl0wh3'; 3 | const ports = { 4 | chain_p2p_port: 2345, 5 | chain_rpc_port: 3456, 6 | chain_zmq_block_port: 3345, 7 | chain_zmq_tx_port: 4445, 8 | lightning_p2p_port: 5445, 9 | lightning_rpc_port: 6445, 10 | lightning_tower_port: 8011, 11 | }; 12 | 13 | export type SpawnLightningType = { 14 | cert: string; 15 | kill: ({}) => Promise; 16 | macaroon: string; 17 | socket: string; 18 | }; 19 | 20 | const spawnLightning = async (): Promise => { 21 | const { cert, kill, macaroon, socket }: SpawnLightningType = await spawnLightningDocker({ 22 | chain_p2p_port: ports.chain_p2p_port, 23 | chain_rpc_port: ports.chain_rpc_port, 24 | chain_zmq_block_port: ports.chain_zmq_block_port, 25 | chain_zmq_tx_port: ports.chain_zmq_tx_port, 26 | generate_address: address, 27 | lightning_p2p_port: ports.lightning_p2p_port, 28 | lightning_rpc_port: ports.lightning_rpc_port, 29 | lightning_tower_port: ports.lightning_tower_port, 30 | }); 31 | 32 | return { cert, kill, macaroon, socket }; 33 | }; 34 | 35 | export { spawnLightning }; 36 | -------------------------------------------------------------------------------- /.github/workflows/on-tag-dockerhub.yml: -------------------------------------------------------------------------------- 1 | name: Build on tag 2 | 3 | on: 4 | push: 5 | tags: 6 | - v[0-9]+.[0-9]+.[0-9]+ 7 | - v[0-9]+.[0-9]+.[0-9]+-* 8 | 9 | jobs: 10 | build: 11 | name: Build image 12 | runs-on: ubuntu-22.04 13 | 14 | steps: 15 | - name: Checkout project 16 | uses: actions/checkout@v3 17 | 18 | - name: Login to DockerHub 19 | uses: docker/login-action@v2 20 | with: 21 | username: ${{ secrets.DOCKER_USER }} 22 | password: ${{ secrets.DOCKER_PASSWORD }} 23 | 24 | - name: Set up QEMU 25 | uses: docker/setup-qemu-action@v2 26 | id: qemu 27 | 28 | - name: Setup Docker buildx action 29 | uses: docker/setup-buildx-action@v2 30 | id: buildx 31 | 32 | - name: Set env variables 33 | run: | 34 | echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV 35 | IMAGE_NAME="${GITHUB_REPOSITORY#*/}" 36 | echo "IMAGE_NAME=${IMAGE_NAME//docker-/}" >> $GITHUB_ENV 37 | 38 | - name: Run Docker buildx 39 | run: | 40 | docker buildx build \ 41 | --platform linux/amd64,linux/arm64 \ 42 | --no-cache -t niteshbalusu/lndboss:$TAG --push . 43 | -------------------------------------------------------------------------------- /src/server/modules/grpc/grpc.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post, Query } from '@nestjs/common'; 2 | import { getPendingDto, grpcDto } from '~shared/commands.dto'; 3 | import { GrpcService } from './grpc.service'; 4 | 5 | @Controller() 6 | export class GrpcController { 7 | constructor(private grpcService: GrpcService) {} 8 | 9 | @Get('api/grpc/get-channel-balance') 10 | async getChannelBalance(@Query() args: grpcDto) { 11 | return this.grpcService.getChannelBalance(args); 12 | } 13 | 14 | @Get('api/grpc/get-peers') 15 | async getPeers(@Query() args: grpcDto) { 16 | return this.grpcService.getPeers(args); 17 | } 18 | 19 | @Get('api/grpc/get-peers-all-nodes') 20 | async getPeersAllNodes() { 21 | return this.grpcService.getPeersAllNodes(); 22 | } 23 | 24 | @Post('api/grpc/get-pending') 25 | async getPending(@Body() args: getPendingDto) { 26 | return this.grpcService.getPending(args); 27 | } 28 | 29 | @Get('api/grpc/get-saved-nodes') 30 | async getSavedNodes() { 31 | return this.grpcService.getSavedNodes(); 32 | } 33 | 34 | @Get('api/grpc/get-wallet-info') 35 | async getWalletInfo(@Query() args: grpcDto) { 36 | return this.grpcService.getWalletInfo(args); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/client/utxos.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import commands from '../../src/client/commands'; 4 | import { setCookie } from '../utils/setAccessToken'; 5 | import { testConstants } from '../utils/constants'; 6 | 7 | const UtxosCommand = commands.find(n => n.value === 'Utxos'); 8 | 9 | test.describe('Test the Utxos command client page', async () => { 10 | test.beforeEach(async ({ page }) => { 11 | await setCookie({ page }); 12 | }); 13 | 14 | test('test the Utxos command page and input values', async ({ page }) => { 15 | await page.goto(testConstants.commandsPage); 16 | await page.click('text=Utxos'); 17 | await expect(page).toHaveTitle('Utxos'); 18 | 19 | await page.type(`#${UtxosCommand?.flags?.count_below}`, '0'); 20 | await page.type(`#${UtxosCommand?.flags?.size}`, '0'); 21 | 22 | await page.type('#node', 'testnode1'); 23 | 24 | await page.click('text=run command'); 25 | await page.waitForTimeout(1000); 26 | 27 | await expect(page.locator('#utxosoutput')).toBeVisible(); 28 | await page.click('text=home'); 29 | }); 30 | 31 | test.afterEach(async ({ page }) => { 32 | await page.context().clearCookies(); 33 | await page.close(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/server/commands/reconnect/reconnect_command.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticatedLnd } from 'lightning'; 2 | import { httpLogger } from '~server/utils/global_functions'; 3 | import { reconnect } from 'balanceofsatoshis/network'; 4 | 5 | /** Get channel peers that are disconnected and attempt to reconnect 6 | 7 | This method will also disconnect peers that are connected, but have inactive 8 | channels. 9 | 10 | { 11 | lnd: 12 | } 13 | 14 | @returns via Promise 15 | { 16 | offline: [{ 17 | alias: 18 | public_key: 22 | public_key: => { 38 | try { 39 | const result = await reconnect({ lnd }); 40 | 41 | return { result }; 42 | } catch (error) { 43 | httpLogger({ error }); 44 | } 45 | }; 46 | 47 | export default reconnectCommand; 48 | -------------------------------------------------------------------------------- /src/client/utils/jwt.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import jwt_decode from 'jwt-decode'; 3 | 4 | /** Decode the JWT token 5 | { 6 | token: , 7 | } 8 | @returns 9 | { 10 | boolean: 11 | } 12 | */ 13 | 14 | type Decoded = { 15 | exp: number; 16 | iat: number; 17 | username: string; 18 | sub: string; 19 | }; 20 | 21 | export const isJwtValid = ({ token }: { token: string }) => { 22 | try { 23 | const decoded: Decoded = jwt_decode(token); 24 | 25 | if (!decoded || !decoded.exp) { 26 | return false; 27 | } 28 | const { exp } = decoded; 29 | const now = new Date().getTime(); 30 | 31 | return now < exp * 1000; 32 | } catch (error) { 33 | return false; 34 | } 35 | }; 36 | 37 | /** Decode the JWT token 38 | { 39 | token: , 40 | } 41 | @returns 42 | decoded: 43 | */ 44 | 45 | export const jwtDecode = ({ token }: { token: string }) => { 46 | try { 47 | const decoded: Decoded = jwt_decode(token); 48 | 49 | if (!decoded || !decoded.exp) { 50 | return false; 51 | } 52 | 53 | return decoded.exp; 54 | } catch (error) { 55 | return false; 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /.github/workflows/on-tag-dockerhub-root.yml: -------------------------------------------------------------------------------- 1 | name: Build on tag 2 | 3 | on: 4 | push: 5 | tags: 6 | - v[0-9]+.[0-9]+.[0-9]+ 7 | - v[0-9]+.[0-9]+.[0-9]+-* 8 | 9 | jobs: 10 | build: 11 | name: Build image 12 | runs-on: ubuntu-22.04 13 | 14 | steps: 15 | - name: Checkout project 16 | uses: actions/checkout@v3 17 | 18 | - name: Set env variables 19 | run: | 20 | echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV 21 | IMAGE_NAME="${GITHUB_REPOSITORY#*/}" 22 | echo "IMAGE_NAME=${IMAGE_NAME//docker-/}" >> $GITHUB_ENV 23 | 24 | - name: Login to DockerHub 25 | uses: docker/login-action@v2 26 | with: 27 | username: ${{ secrets.DOCKER_USER }} 28 | password: ${{ secrets.DOCKER_PASSWORD }} 29 | 30 | - name: Set up QEMU 31 | uses: docker/setup-qemu-action@v2 32 | id: qemu 33 | 34 | - name: Setup Docker buildx action 35 | uses: docker/setup-buildx-action@v2 36 | id: buildx 37 | 38 | - name: Run Docker buildx 39 | run: | 40 | docker buildx build \ 41 | --file arm64.Dockerfile --platform linux/amd64,linux/arm64 \ 42 | --no-cache -t niteshbalusu/lndboss:root --push . 43 | -------------------------------------------------------------------------------- /src/server/modules/socket/socket.gateway.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OnGatewayConnection, 3 | OnGatewayDisconnect, 4 | OnGatewayInit, 5 | SubscribeMessage, 6 | WebSocketGateway, 7 | WebSocketServer, 8 | } from '@nestjs/websockets'; 9 | import { Server, Socket } from 'socket.io'; 10 | 11 | import { BosloggerService } from '../boslogger/boslogger.service'; 12 | 13 | @WebSocketGateway({ 14 | cors: { 15 | origin: '*', 16 | }, 17 | }) 18 | export class SocketGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { 19 | constructor(private logger: BosloggerService) {} 20 | 21 | @WebSocketServer() server: Server; 22 | 23 | @SubscribeMessage('msgToServer') 24 | handleMessage(client: Socket, payload: string): void { 25 | this.logger.log({ message: `Message Received: ${client.id}`, type: 'info' }); 26 | this.server.emit('msgToClient', payload); 27 | } 28 | 29 | afterInit() { 30 | this.logger.log({ message: 'Socket Gateway Initialized', type: 'info' }); 31 | } 32 | 33 | handleDisconnect(client: Socket) { 34 | this.logger.log({ message: `Client disconnected: ${client.id}`, type: 'warn' }); 35 | } 36 | 37 | handleConnection(client: Socket) { 38 | this.logger.log({ message: `Client connected: ${client.id}`, type: 'info' }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/client/output/InvoiceOutput.tsx: -------------------------------------------------------------------------------- 1 | import { CopyText } from '~client/standard_components/app-components'; 2 | import QRCode from 'qrcode.react'; 3 | import React from 'react'; 4 | 5 | const substring = n => n.slice(0, 20) + '......' + n.slice(-20); 6 | 7 | /* 8 | Renders the output of the Invoice command. 9 | */ 10 | 11 | type Args = { 12 | data: { 13 | request: string; 14 | tokens: number; 15 | }; 16 | }; 17 | const InvoiceOutput = ({ data }: Args) => { 18 | return ( 19 |
20 | 21 |

22 | {substring(data.request)} 23 |

24 | 25 |
26 |

{`Tokens: ${data.tokens}`}

27 |
28 |
29 | ); 30 | }; 31 | 32 | export default InvoiceOutput; 33 | 34 | const styles = { 35 | div: { 36 | marginTop: '30px', 37 | marginLeft: '10px', 38 | }, 39 | qr: { 40 | height: '350px', 41 | width: '350px', 42 | padding: '5px', 43 | }, 44 | text: { 45 | fontSize: '15px', 46 | fontWeight: 'bold', 47 | display: 'inline-block', 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/client/standard_components/app-components/StandardButtonLink.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonProps, styled } from '@mui/material'; 2 | 3 | import Link from 'next/link'; 4 | import React from 'react'; 5 | import { purple } from '@mui/material/colors'; 6 | 7 | /* Renders the standard link button 8 | { 9 | destination:
27 | 28 | 29 | {title} 30 | 31 | 32 | 33 | {data.map(row => ( 34 | 35 | 36 | {row.data} 37 | 38 | 39 | ))} 40 | 41 |
42 | 43 | ); 44 | }; 45 | 46 | export default BasicTable; 47 | -------------------------------------------------------------------------------- /tests/server/chartPaymentsReceived.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import { chartPaymentsReceivedCommand } from '../../src/server/commands/'; 4 | import spawnCluster from '../utils/spawn_lightning_cluster'; 5 | 6 | test.describe('Test ChartPaymentsReceived command on the node.js side', async () => { 7 | let lightning: any[]; 8 | 9 | test.beforeAll(async () => { 10 | lightning = await spawnCluster(2); 11 | }); 12 | 13 | test('run ChartPaymentsReceived command', async () => { 14 | const args = { 15 | days: 0, 16 | nodes: [], 17 | }; 18 | 19 | const lnds = lightning.map(({ lnd }) => lnd); 20 | const { result } = await chartPaymentsReceivedCommand({ args, lnd: lnds }); 21 | 22 | console.log('ChartPaymentsReceived----', result); 23 | 24 | expect(result).toBeTruthy(); 25 | }); 26 | 27 | test('run ChartPaymentsReceived command: dates', async () => { 28 | const args = { 29 | is_count: true, 30 | days: 0, 31 | end_date: '2021-08-01', 32 | for: '', 33 | nodes: [], 34 | start_date: '2021-07-01', 35 | }; 36 | 37 | const lnds = lightning.map(({ lnd }) => lnd); 38 | const { result } = await chartPaymentsReceivedCommand({ args, lnd: lnds }); 39 | 40 | console.log('ChartPaymentsReceived----', result); 41 | 42 | expect(result).toBeTruthy(); 43 | }); 44 | 45 | test.afterAll(async () => { 46 | await Promise.all(lightning.map(({ kill }) => kill({}))); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /.github/workflows/on-push-github.ignore: -------------------------------------------------------------------------------- 1 | name: Build on push 2 | 3 | permissions: 4 | packages: write 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | paths-ignore: 11 | - '**/*.md' 12 | - '**/*.yml' 13 | - '**/*.yaml' 14 | - 'README.md' 15 | - 'CHANGELOG.md' 16 | 17 | jobs: 18 | build: 19 | name: Build image 20 | runs-on: ubuntu-22.04 21 | 22 | steps: 23 | - name: Checkout project 24 | uses: actions/checkout@v3 25 | 26 | - name: Set env variables 27 | run: | 28 | echo "BRANCH=$(echo ${GITHUB_REF#refs/heads/} | sed 's/\//-/g')" >> $GITHUB_ENV 29 | IMAGE_NAME="${GITHUB_REPOSITORY#*/}" 30 | echo "IMAGE_NAME=${IMAGE_NAME//docker-/}" >> $GITHUB_ENV 31 | 32 | - name: Login to GitHub Container Registry 33 | uses: docker/login-action@v2 34 | with: 35 | registry: ghcr.io 36 | username: ${{ github.repository_owner }} 37 | password: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Set up QEMU 40 | uses: docker/setup-qemu-action@v2 41 | id: qemu 42 | 43 | - name: Setup Docker buildx action 44 | uses: docker/setup-buildx-action@v2 45 | id: buildx 46 | 47 | - name: Run Docker buildx 48 | run: | 49 | docker buildx build \ 50 | --platform linux/amd64,linux/arm64 \ 51 | --tag ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:$BRANCH \ 52 | --output "type=registry" ./ 53 | -------------------------------------------------------------------------------- /src/server/commands/rebalance/get_triggers.ts: -------------------------------------------------------------------------------- 1 | import { auto } from 'async'; 2 | import decodeTrigger from './decode_trigger'; 3 | import readRebalanceFile from './read_rebalance_file'; 4 | 5 | const defaultRebalances = { rebalances: [] }; 6 | const { parse } = JSON; 7 | 8 | /** Get registered triggers 9 | 10 | {} 11 | 12 | @returns via cbk or Promise 13 | { 14 | triggers: [{ 15 | [connectivity]: { 16 | id: 17 | } 18 | [follow]: { 19 | id: 20 | } 21 | id: 22 | }] 23 | } 24 | */ 25 | const getTriggers = async ({}) => { 26 | return auto({ 27 | // Check arguments 28 | validate: (cbk: any) => { 29 | return cbk(); 30 | }, 31 | 32 | // Get the past triggers 33 | getTriggers: [ 34 | 'validate', 35 | async () => { 36 | const triggers = []; 37 | const data = await readRebalanceFile({}); 38 | if (!data) { 39 | return defaultRebalances; 40 | } 41 | 42 | const parsedData = parse(data); 43 | 44 | parsedData.rebalances.forEach(n => { 45 | const { result } = decodeTrigger({ encoded: n.rebalance }); 46 | triggers.push({ 47 | id: n.id, 48 | rebalance_data: parse(result.rebalance_data), 49 | }); 50 | }); 51 | 52 | return triggers; 53 | }, 54 | ], 55 | }); 56 | }; 57 | 58 | export default getTriggers; 59 | -------------------------------------------------------------------------------- /tests/client/createChannelGroup.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import commands from '../../src/client/commands'; 4 | import { setCookie } from '../utils/setAccessToken'; 5 | import { testConstants } from '../utils/constants'; 6 | 7 | const CreateChannelGroupCommand = commands.find(n => n.value === 'CreateChannelGroup'); 8 | 9 | test.describe('Test the Create Group Channel command client page', async () => { 10 | test.beforeEach(async ({ page }) => { 11 | await setCookie({ page }); 12 | }); 13 | 14 | test('test the Create Channel Group command page and input values', async ({ page }) => { 15 | await page.goto(testConstants.commandsPage); 16 | await page.click('text=Create Channel Group'); 17 | await expect(page).toHaveTitle('Create Channel Group'); 18 | 19 | await page.type(`#${CreateChannelGroupCommand?.flags?.capacity}`, '100000'); 20 | await page.type(`#${CreateChannelGroupCommand?.flags?.fee_rate}`, '2'); 21 | await page.type(`#${CreateChannelGroupCommand?.flags?.size}`, '2'); 22 | 23 | await page.type('#node', 'testnode1'); 24 | 25 | await page.click('text=run command'); 26 | await page.waitForTimeout(1000); 27 | 28 | // await expect(page.locator('#creategroupchanneloutput')).toBeVisible(); // Need docker daemons to be able to generate coins 29 | await page.click('text=home'); 30 | }); 31 | 32 | test.afterEach(async ({ page }) => { 33 | await page.context().clearCookies(); 34 | await page.close(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/client/standard_components/lndboss/TagsList.tsx: -------------------------------------------------------------------------------- 1 | import * as types from '~shared/types'; 2 | 3 | import { Autocomplete, TextField } from '@mui/material'; 4 | import React, { useEffect, useState } from 'react'; 5 | 6 | import { axiosGet } from '~client/utils/axios'; 7 | 8 | type Args = { 9 | id: string; 10 | label: string; 11 | placeholder: string; 12 | setTag: (peer: string) => void; 13 | }; 14 | 15 | const styles = { 16 | textField: { 17 | width: '400px', 18 | }, 19 | }; 20 | 21 | const TagsList = ({ id, label, placeholder, setTag }: Args) => { 22 | const [tags, setTags] = useState([]); 23 | 24 | const query: types.commandTags = { 25 | icon: '', 26 | add: '', 27 | id: '', 28 | is_avoided: false, 29 | remove: '', 30 | tag: '', 31 | }; 32 | 33 | useEffect(() => { 34 | const fetchData = async () => { 35 | const result = await axiosGet({ path: 'tags', query }); 36 | 37 | if (!!result) { 38 | setTags(result); 39 | } 40 | }; 41 | 42 | fetchData(); 43 | }, []); 44 | 45 | return ( 46 | <> 47 | tag.alias)} 51 | renderInput={params => } 52 | onChange={(_event: any, newValue: any) => { 53 | setTag(!!newValue ? newValue : ''); 54 | }} 55 | style={styles.textField} 56 | /> 57 | 58 | ); 59 | }; 60 | 61 | export default TagsList; 62 | -------------------------------------------------------------------------------- /tests/server/balance.test.ts: -------------------------------------------------------------------------------- 1 | import { SpawnLightningServerType, spawnLightningServer } from '../utils/spawn_lightning_server'; 2 | import { expect, test } from '@playwright/test'; 3 | 4 | import { balanceCommand } from '../../src/server/commands/'; 5 | 6 | test.describe('Test Balance command on the node.js side', async () => { 7 | let lightning: SpawnLightningServerType; 8 | 9 | test.beforeAll(async () => { 10 | lightning = await spawnLightningServer(); 11 | }); 12 | 13 | test('run balance command', async () => { 14 | const args = { 15 | above: 0, 16 | below: 0, 17 | is_confirmed: true, 18 | is_offchain_only: false, 19 | is_onchain_only: false, 20 | is_detailed: false, 21 | node: '', 22 | }; 23 | const { result } = await balanceCommand({ args, lnd: lightning.lnd, lnds: [lightning.lnd] }); 24 | 25 | console.log('balance----', result); 26 | expect(result).toBeTruthy(); 27 | }); 28 | 29 | test('run balance command detailed', async () => { 30 | const args = { 31 | above: 0, 32 | below: 0, 33 | is_confirmed: true, 34 | is_offchain_only: false, 35 | is_onchain_only: false, 36 | is_detailed: true, 37 | node: '', 38 | }; 39 | const { result } = await balanceCommand({ args, lnd: lightning.lnd, lnds: [lightning.lnd] }); 40 | 41 | console.log('balance detailed----', result); 42 | 43 | expect(result).toBeTruthy(); 44 | }); 45 | 46 | test.afterAll(async () => { 47 | await lightning.kill({}); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/client/standard_components/app-components/ResponsiveGrid.tsx: -------------------------------------------------------------------------------- 1 | import Grid from '@mui/material/Grid'; 2 | import Paper from '@mui/material/Paper'; 3 | import React from 'react'; 4 | import StandardRouterLink from './StandardRouterLink'; 5 | import { experimentalStyled as styled } from '@mui/material/styles'; 6 | 7 | /* 8 | Renders the grid of commands on the home page. 9 | */ 10 | 11 | type Props = { 12 | gridArray: { 13 | name: string; 14 | value: string; 15 | description: string; 16 | }[]; 17 | }; 18 | 19 | const Item = styled(Paper)(({ theme }) => ({ 20 | backgroundColor: '#042b57', 21 | ...theme.typography.body2, 22 | padding: theme.spacing(2), 23 | textAlign: 'center', 24 | color: 'white', 25 | height: '130px', 26 | borderRadius: '30px', 27 | marginRight: '10px', 28 | })); 29 | 30 | const ResponsiveGrid = ({ gridArray }: Props) => { 31 | return ( 32 | 42 | {gridArray.map(grid => ( 43 | 44 | 45 | 46 |

{grid.description}

47 |
48 |
49 | ))} 50 |
51 | ); 52 | }; 53 | 54 | export default ResponsiveGrid; 55 | -------------------------------------------------------------------------------- /tests/client/joinChannelGroup.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import commands from '../../src/client/commands'; 4 | import { setCookie } from '../utils/setAccessToken'; 5 | import { testConstants } from '../utils/constants'; 6 | 7 | const joinChannelGroupCommand = commands.find(n => n.value === 'JoinChannelGroup'); 8 | 9 | test.describe('Test the Join Channel Group command client page', async () => { 10 | test.beforeEach(async ({ page }) => { 11 | await setCookie({ page }); 12 | }); 13 | 14 | test('test the Join Channel Group command page and input values', async ({ page }) => { 15 | test.slow(); 16 | await page.goto(testConstants.commandsPage); 17 | await page.click('text=Join Channel Group'); 18 | await expect(page).toHaveTitle('Join Channel Group'); 19 | 20 | await page.type( 21 | `#${joinChannelGroupCommand?.args?.code}`, 22 | '023d8b17ae417518ef86d5471dfd5010b47cfb896812924f1ac8102ff8b76fa3fe47c004c6c6202060d5be81fe7ae90652' 23 | ); 24 | await page.type(`#${joinChannelGroupCommand?.flags?.max_fee_rate}`, '2'); 25 | 26 | await page.type('#node', 'testnode1'); 27 | 28 | await page.click('text=run command'); 29 | await page.waitForTimeout(1000); 30 | 31 | // await expect(page.locator('#Joingroupchanneloutput')).toBeVisible(); // Need docker daemons to be able to generate coins 32 | await page.click('text=home'); 33 | }); 34 | 35 | test.afterEach(async ({ page }) => { 36 | await page.context().clearCookies(); 37 | await page.close(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/server/commands/chartFeesEarned/chart_fees_earned_command.ts: -------------------------------------------------------------------------------- 1 | import * as types from '~shared/types'; 2 | 3 | import { AuthenticatedLnd } from 'lightning'; 4 | import { getFeesChart } from 'balanceofsatoshis/routing'; 5 | import { httpLogger } from '~server/utils/global_functions'; 6 | import { readFile } from 'fs'; 7 | 8 | /** Get data for fees chart 9 | 10 | { 11 | [days]: 12 | [end_date]: 13 | [is_count]: 14 | lnds: [] 15 | [start_date]: 16 | [via]: 17 | } 18 | 19 | @returns via Promise 20 | { 21 | data: [] 22 | description: 23 | title: 24 | } 25 | */ 26 | 27 | type Args = { 28 | args: types.commandChartFeesEarned; 29 | lnd: AuthenticatedLnd[]; 30 | }; 31 | const chartFeesEarnedCommand = async ({ args, lnd }: Args): Promise<{ result: any }> => { 32 | try { 33 | const result = await getFeesChart({ 34 | days: args.days, 35 | end_date: args.end_date || undefined, 36 | fs: { getFile: readFile }, 37 | is_count: args.is_count, 38 | is_forwarded: args.is_forwarded, 39 | lnds: lnd, 40 | start_date: args.start_date || undefined, 41 | via: args.via || undefined, 42 | }); 43 | 44 | return { result }; 45 | } catch (error) { 46 | httpLogger({ error }); 47 | } 48 | }; 49 | 50 | export default chartFeesEarnedCommand; 51 | -------------------------------------------------------------------------------- /tests/client/probe.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import commands from '../../src/client/commands'; 4 | import { setCookie } from '../utils/setAccessToken'; 5 | import { testConstants } from '../utils/constants'; 6 | 7 | const ProbeCommand = commands.find(n => n.value === 'Probe'); 8 | 9 | test.describe('Test the Probe command client page', async () => { 10 | test.beforeEach(async ({ page }) => { 11 | await setCookie({ page }); 12 | }); 13 | 14 | test('test the Probe command page and input values', async ({ page }) => { 15 | await page.goto(testConstants.commandsPage); 16 | await page.click('text=Probe'); 17 | await expect(page).toHaveTitle('Probe Command'); 18 | await page.type(`#avoid-0`, 'ban'); 19 | await page.type(`#${ProbeCommand?.args?.to}`, 'pubkey'); 20 | await page.type(`#${ProbeCommand?.flags?.in}`, 'carol'); 21 | await page.type(`#${ProbeCommand?.args?.amount}`, '50000'); 22 | await page.type(`#${ProbeCommand?.flags?.max_fee}`, '100'); 23 | 24 | await page.type('#node', 'alice'); 25 | 26 | await page.click('text=run command'); 27 | const popup = await page.waitForEvent('popup'); 28 | 29 | await expect(popup).toHaveTitle('Probe Result'); 30 | await popup.waitForTimeout(1000); 31 | await expect(popup.locator('#probeResultTitle')).toBeVisible(); 32 | 33 | await popup.close(); 34 | 35 | await page.bringToFront(); 36 | await page.click('text=home'); 37 | }); 38 | 39 | test.afterEach(async ({ page }) => { 40 | await page.context().clearCookies(); 41 | await page.close(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/server/peers.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { setupChannel, spawnLightningCluster } from 'ln-docker-daemons'; 3 | 4 | import { AuthenticatedLnd } from 'lightning'; 5 | import { peersCommand } from '../../src/server/commands'; 6 | 7 | test.describe('Test Peers command on the node.js side', async () => { 8 | type LightningCluster = { 9 | lnd: AuthenticatedLnd; 10 | kill: ({}) => Promise; 11 | nodes: any[]; 12 | }; 13 | let lightning: LightningCluster; 14 | let [alice, bob]: any[] = []; 15 | 16 | test.beforeAll(async () => { 17 | lightning = await spawnLightningCluster({ size: 2 }); 18 | [alice, bob] = lightning.nodes; 19 | 20 | await setupChannel({ generate: alice.generate, lnd: alice.lnd, to: bob }); 21 | }); 22 | 23 | test('run peers command with complete', async () => { 24 | const args = { 25 | is_active: true, 26 | }; 27 | 28 | const { result } = await peersCommand({ 29 | lnd: alice.lnd, 30 | args, 31 | }); 32 | 33 | console.log('peers----', result); 34 | expect(result.peers).toBeTruthy(); 35 | expect(result.rows).toBeTruthy(); 36 | }); 37 | 38 | test('run peers command with table', async () => { 39 | const args = { 40 | is_active: true, 41 | is_table: false, 42 | }; 43 | 44 | const { result } = await peersCommand({ 45 | lnd: alice.lnd, 46 | args, 47 | }); 48 | 49 | console.log('peers----', result); 50 | expect(result.peers).toBeTruthy(); 51 | }); 52 | 53 | test.afterAll(async () => { 54 | await lightning.kill({}); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/server/commands/utxos/utxos_command.ts: -------------------------------------------------------------------------------- 1 | import * as types from '~shared/types'; 2 | 3 | import { AuthenticatedLnd } from 'lightning'; 4 | import { getUtxos } from 'balanceofsatoshis/chain'; 5 | 6 | /** Get UTXOs 7 | 8 | { 9 | [count_below]: 10 | [is_count]: 11 | [is_confirmed]: 12 | lnd: 13 | [min_tokens]: 14 | } 15 | 16 | // Non-count response 17 | @returns via Promise 18 | { 19 | utxos: [{ 20 | address: 21 | amount: 22 | [confirmations]: 23 | outpoint: 24 | [is_unconfirmed]: 25 | [locked]: 26 | [lock_expires_at]: 27 | [related_description]: 28 | [related_channels]: [] 29 | }] 30 | } 31 | 32 | // Count response 33 | @returns via Promise 34 | 35 | */ 36 | type Args = { 37 | args: types.commandUtxos; 38 | lnd: AuthenticatedLnd; 39 | }; 40 | const utxosCommand = async ({ args, lnd }: Args) => { 41 | const result = await getUtxos({ 42 | lnd, 43 | count_below: args.count_below, 44 | is_confirmed: args.is_confirmed, 45 | is_count: args.is_count, 46 | min_tokens: args.min_tokens, 47 | }); 48 | 49 | return { result }; 50 | }; 51 | 52 | export default utxosCommand; 53 | -------------------------------------------------------------------------------- /arm64.Dockerfile: -------------------------------------------------------------------------------- 1 | # --------------- 2 | # Install Dependencies 3 | # --------------- 4 | FROM node:16-alpine as build 5 | 6 | WORKDIR /lndboss 7 | 8 | COPY package.json yarn.lock ./ 9 | RUN yarn install --network-timeout 1000000 10 | 11 | # --------------- 12 | # Build App 13 | # --------------- 14 | 15 | COPY . . 16 | ENV NEXT_TELEMETRY_DISABLED=1 17 | RUN yarn build:prod 18 | 19 | # --------------- 20 | # Install Production Dependencies 21 | # --------------- 22 | 23 | FROM node:16-alpine as deps 24 | 25 | WORKDIR /lndboss 26 | 27 | COPY package.json yarn.lock ./ 28 | 29 | RUN yarn install --production --network-timeout 1000000 30 | 31 | # --------------- 32 | # Release App 33 | # --------------- 34 | FROM node:16-alpine as final 35 | 36 | WORKDIR /lndboss 37 | 38 | # Set environment to production 39 | ARG NODE_ENV="production" 40 | ENV NODE_ENV=${NODE_ENV} 41 | ENV NEXT_TELEMETRY_DISABLED=1 42 | 43 | # Copy files from build 44 | COPY --from=build /lndboss/package.json ./ 45 | COPY --from=deps /lndboss/node_modules/ ./node_modules 46 | 47 | # Copy NestJS files from build 48 | COPY --from=build /lndboss/nest-cli.json ./ 49 | COPY --from=build /lndboss/dist/ ./dist 50 | 51 | # Copy NextJS files from build 52 | COPY --from=build /lndboss/next-env.d.ts ./ 53 | COPY --from=build /lndboss/src/client/.next ./src/client/.next 54 | COPY --from=build /lndboss/src/client/public ./src/client/public 55 | COPY --from=build /lndboss/src/client/next-env.d.ts ./src/client/next-env.d.ts 56 | COPY --from=build /lndboss/src/client/next.config.js ./src/client/next.config.js 57 | 58 | # Expose the port the app runs on 59 | EXPOSE 8055 60 | 61 | # Start the app 62 | CMD [ "yarn", "start:prod" ] 63 | -------------------------------------------------------------------------------- /tests/client/chartPaymentsReceived.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import commands from '../../src/client/commands'; 4 | import { setCookie } from '../utils/setAccessToken'; 5 | import { testConstants } from '../utils/constants'; 6 | 7 | const ChartPaymentsReceivedCommand = commands.find(n => n.value === 'ChartPaymentsReceived'); 8 | 9 | test.describe('Test the ChartPaymentsReceived command client page', async () => { 10 | test.beforeEach(async ({ page }) => { 11 | await setCookie({ page }); 12 | }); 13 | 14 | test('test the ChartPaymentsReceived command page and input values', async ({ page }) => { 15 | await page.goto(testConstants.commandsPage); 16 | await page.click('text=Chart Payments Received'); 17 | await expect(page).toHaveTitle('Chart Payments Received'); 18 | await page.type(`#${ChartPaymentsReceivedCommand?.flags?.days}`, '10'); 19 | await page.type(`#${ChartPaymentsReceivedCommand?.flags?.for}`, 'alice'); 20 | await page.check(`#${ChartPaymentsReceivedCommand?.flags?.count}`); 21 | await page.type('#node-0', 'testnode1'); 22 | 23 | await page.click('text=run command'); 24 | const popup = await page.waitForEvent('popup'); 25 | 26 | await expect(popup).toHaveTitle('Chart Payments Received Result'); 27 | await popup.waitForTimeout(1000); 28 | await expect(popup.locator('#ChartPaymentsReceivedOutput')).toBeVisible(); 29 | 30 | await popup.close(); 31 | 32 | await page.bringToFront(); 33 | await page.click('text=home'); 34 | }); 35 | 36 | test.afterEach(async ({ page }) => { 37 | await page.context().clearCookies(); 38 | await page.close(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/client/chartFeesEarned.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import commands from '../../src/client/commands'; 4 | import { setCookie } from '../utils/setAccessToken'; 5 | import { testConstants } from '../utils/constants'; 6 | 7 | const ChartFeesEarnedCommand = commands.find(n => n.value === 'ChartFeesEarned'); 8 | 9 | test.describe('Test the ChartFeesEarned command client page', async () => { 10 | test.beforeEach(async ({ page }) => { 11 | await setCookie({ page }); 12 | }); 13 | 14 | test('test the ChartFeesEarned command page and input values', async ({ page }) => { 15 | await page.goto(testConstants.commandsPage); 16 | await page.click('text=Chart Fees Earned'); 17 | await expect(page).toHaveTitle('Chart Fees Earned'); 18 | 19 | await page.type(`#${ChartFeesEarnedCommand?.args?.via}`, 'outpeers'); 20 | await page.type(`#${ChartFeesEarnedCommand?.flags?.days}`, '10'); 21 | await page.check(`#${ChartFeesEarnedCommand?.flags?.count}`); 22 | await page.check(`#${ChartFeesEarnedCommand?.flags?.forwarded}`); 23 | await page.type('#node-0', 'testnode1'); 24 | 25 | await page.click('text=run command'); 26 | const popup = await page.waitForEvent('popup'); 27 | 28 | await expect(popup).toHaveTitle('Chart Fees Earned Result'); 29 | await popup.waitForTimeout(1000); 30 | await expect(popup.locator('#ChartFeesEarnedOutput')).toBeVisible(); 31 | await popup.close(); 32 | 33 | await page.bringToFront(); 34 | await page.click('text=home'); 35 | }); 36 | 37 | test.afterEach(async ({ page }) => { 38 | await page.context().clearCookies(); 39 | await page.close(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/server/cleanFailedPayments.test.ts: -------------------------------------------------------------------------------- 1 | import { Logger, createLogger, format, transports } from 'winston'; 2 | import { SpawnLightningServerType, spawnLightningServer } from '../utils/spawn_lightning_server'; 3 | import { expect, test } from '@playwright/test'; 4 | 5 | import { cleanFailedPaymentsCommand } from '../../src/server/commands/'; 6 | 7 | test.describe('Test CleanFailedPayments command on the node.js side', async () => { 8 | let lightning: SpawnLightningServerType; 9 | let logger: Logger; 10 | 11 | test.beforeAll(async () => { 12 | lightning = await spawnLightningServer(); 13 | logger = createLogger({ 14 | level: 'info', 15 | format: format.json(), 16 | defaultMeta: { service: 'cleanfailedpayments' }, 17 | transports: [ 18 | new transports.Console({ 19 | format: format.combine(format.prettyPrint()), 20 | }), 21 | ], 22 | }); 23 | }); 24 | 25 | test('run CleanFailedPayments command (dryrun)', async () => { 26 | const args = { 27 | is_dry_run: true, 28 | }; 29 | const { result } = await cleanFailedPaymentsCommand({ args, logger, lnd: lightning.lnd }); 30 | console.log('clean failed payments----', result); 31 | expect(result).toBeDefined(); 32 | }); 33 | 34 | test('run CleanFailedPayments command', async () => { 35 | const args = { 36 | is_dry_run: false, 37 | }; 38 | const { result } = await cleanFailedPaymentsCommand({ args, logger, lnd: lightning.lnd }); 39 | console.log('clean failed payments----', result); 40 | expect(result).toBeDefined(); 41 | }); 42 | 43 | test.afterAll(async () => { 44 | await lightning.kill({}); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/client/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const clientConstants = { 2 | authenticatePage: '/Authenticate', 3 | commandsPage: '/Commands', 4 | dashboardPage: '/Dashboard', 5 | feeStrategyPath: '/schedulers/FeesScheduler', 6 | homeButtonLabel: 'Home', 7 | loginUrl: '/auth/Login', 8 | quickTools: '/preferences/QuickTools', 9 | publicPaths: ['/', '/auth/Login', '/auth/Register'], 10 | rebalanceSchedulerUrl: '/schedulers/RebalanceScheduler', 11 | rebalanceUrl: '/commands/Rebalance', 12 | registerUrl: '/auth/Register', 13 | userPreferencesUrl: '/preferences/UserPreferences', 14 | }; 15 | 16 | export const defaultChartQueryDays = 7; 17 | 18 | export const selectedSavedNode = () => { 19 | if (!!localStorage.getItem('SELECTED_SAVED_NODE') && localStorage.getItem('SELECTED_SAVED_NODE') !== 'undefined') { 20 | return localStorage.getItem('SELECTED_SAVED_NODE') as string; 21 | } 22 | 23 | return ''; 24 | }; 25 | 26 | // Parse Ansi escape sequences 27 | export const removeStyling = o => 28 | JSON.parse( 29 | JSON.stringify(o, (k, v) => 30 | typeof v === 'string' 31 | ? v.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '') 32 | : v === undefined 33 | ? undefined 34 | : v 35 | ) 36 | ); 37 | 38 | // Donation LNURL 39 | export const donationLnurl = 40 | 'LNURL1DP68GURN8GHJ7MRW9E6XJURN9UH8WETVDSKKKMN0WAHZ7MRWW4EXCUP0X9UR2VRYX4NRWCFNVVMKYCMXXCURQGXD649'; 41 | 42 | // Donation Lightning Address 43 | export const donationLnaddress = '0x49f4f513b6752c95@ln.tips'; 44 | 45 | // Convert sats to btc 46 | export const tokensAsBigTokens = (tokens: number) => (!!tokens ? (tokens / 1e8).toFixed(8) : 0); 47 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable comma-dangle */ 2 | module.exports = { 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | }, 7 | extends: ['standard-with-typescript', 'prettier'], 8 | overrides: [], 9 | parserOptions: { 10 | ecmaVersion: 'latest', 11 | sourceType: 'module', 12 | project: './tsconfig.json', 13 | }, 14 | ignorePatterns: ['src/client', 'src/shared', 'tests', 'node_modules', 'dist', 'playwright.config.ts'], 15 | rules: { 16 | 'comma-dangle': 'off', 17 | 'no-control-regex': 'off', 18 | 'no-empty-pattern': 'off', 19 | '@typescript-eslint/explicit-function-return-type': 'off', 20 | '@typescript-eslint/strict-boolean-expressions': 'off', 21 | '@typescript-eslint/restrict-template-expressions': 'off', 22 | '@typescript-eslint/no-misused-promises': 'off', 23 | '@typescript-eslint/prefer-nullish-coalescing': 'off', 24 | '@typescript-eslint/no-floating-promises': 'off', 25 | '@typescript-eslint/return-await': 'off', 26 | '@typescript-eslint/prefer-optional-chain': 'off', 27 | '@typescript-eslint/restrict-plus-operands': 'off', 28 | '@typescript-eslint/no-confusing-void-expression': 'off', 29 | '@typescript-eslint/naming-convention': 'off', 30 | 'no-extra-boolean-cast': 'off', 31 | '@typescript-eslint/consistent-type-definitions': 'off', 32 | '@typescript-eslint/consistent-type-imports': 'off', 33 | '@typescript-eslint/prefer-readonly': 'off', 34 | '@typescript-eslint/array-type': 'off', 35 | '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off', 36 | '@typescript-eslint/promise-function-async': 'off', 37 | '@typescript-eslint/consistent-indexed-object-style': 'off', 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /tests/client/quicktools.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import { setCookie } from '../utils/setAccessToken'; 4 | import { testConstants } from '../utils/constants'; 5 | 6 | test.describe('Test the Quick Tools client page', async () => { 7 | test.beforeEach(async ({ page }) => { 8 | await setCookie({ page }); 9 | }); 10 | 11 | test('test the Create Invoice quick tools page and input values', async ({ page }) => { 12 | await page.goto(testConstants.quickToolsPage); 13 | await expect(page).toHaveTitle('Quick Tools'); 14 | await page.click('text=Create Invoice'); 15 | 16 | await page.type(`#amount`, '1000'); 17 | await page.type('#node', 'testnode1'); 18 | 19 | await page.click('#createinvoice'); 20 | await page.waitForTimeout(1000); 21 | 22 | await expect(page.locator('#createinvoiceoutput')).toBeVisible(); 23 | await expect(page.locator('#invoice')).toBeVisible(); 24 | }); 25 | 26 | test('test the Create Chain Address quick tools page and input values', async ({ page }) => { 27 | await page.goto(testConstants.quickToolsPage); 28 | await expect(page).toHaveTitle('Quick Tools'); 29 | await page.click('text=Create OnChain Address'); 30 | 31 | await page.type(`#amount`, '1000'); 32 | await page.type('#node', 'testnode1'); 33 | 34 | await page.click('#createchainaddress'); 35 | await page.waitForTimeout(1000); 36 | 37 | await expect(page.locator('#createchainaddressoutput')).toBeVisible(); 38 | await expect(page.locator('#chainaddress')).toBeVisible(); 39 | }); 40 | 41 | test.afterEach(async ({ page }) => { 42 | await page.context().clearCookies(); 43 | await page.close(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/server/settings/get_settings.file.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFile } from 'fs'; 2 | 3 | import { auto } from 'async'; 4 | import { homedir } from 'os'; 5 | import { join } from 'path'; 6 | 7 | const home = '.bosgui'; 8 | const { parse } = JSON; 9 | const settingsFile = 'settings.json'; 10 | 11 | /** Write the settings file 12 | @returns via Promise 13 | data: 14 | */ 15 | type Tasks = { 16 | checkFile: boolean; 17 | readFile: string; 18 | }; 19 | const getSettingsFile = async () => { 20 | return ( 21 | await auto({ 22 | // Check if the settings file exists 23 | checkFile: [ 24 | (cbk: any) => { 25 | const filePath = join(...[homedir(), home, settingsFile]); 26 | 27 | return cbk(null, existsSync(filePath)); 28 | }, 29 | ], 30 | 31 | // Read the settings file 32 | readFile: [ 33 | 'checkFile', 34 | ({ checkFile }, cbk: any) => { 35 | // Exit early if the settings file doesn't exist 36 | if (!checkFile) { 37 | return cbk(); 38 | } 39 | 40 | const filePath = join(...[homedir(), home, settingsFile]); 41 | 42 | readFile(filePath, (err, data) => { 43 | if (!!err) { 44 | return cbk([500, 'UnexpectedErrorReadingSettingsFile', err]); 45 | } 46 | 47 | try { 48 | parse(data.toString()); 49 | } catch (err) { 50 | return cbk([400, 'ExpectedValidJsonSettingsFile', { err }]); 51 | } 52 | 53 | return cbk(null, data.toString()); 54 | }); 55 | }, 56 | ], 57 | }) 58 | ).readFile; 59 | }; 60 | 61 | export default getSettingsFile; 62 | -------------------------------------------------------------------------------- /docker/dev.Dockerfile: -------------------------------------------------------------------------------- 1 | # --------------- 2 | # Install Dependencies 3 | # --------------- 4 | FROM node:16-buster-slim as deps 5 | 6 | WORKDIR /lndboss 7 | 8 | COPY package.json yarn.lock ./ 9 | RUN yarn install --network-timeout 1000000 10 | 11 | # --------------- 12 | # Build App 13 | # --------------- 14 | FROM deps as build 15 | 16 | WORKDIR /lndboss 17 | 18 | RUN apt-get update && apt-get install -y jq 19 | 20 | COPY . . 21 | RUN yarn build 22 | RUN yarn remove $(cat package.json | jq -r '.devDependencies | keys | join(" ")') 23 | 24 | # --------------- 25 | # Release App 26 | # --------------- 27 | FROM node:16-buster-slim as final 28 | 29 | WORKDIR /lndboss 30 | 31 | # Set environment to production 32 | ARG NODE_ENV="production" 33 | ENV NODE_ENV=${NODE_ENV} 34 | 35 | # Create a new user and group 36 | ARG USER_ID=1000 37 | ARG GROUP_ID=1000 38 | ENV USER_ID=$USER_ID 39 | ENV GROUP_ID=$GROUP_ID 40 | 41 | # Copy files from build 42 | COPY --from=build /lndboss/package.json ./ 43 | COPY --from=build /lndboss/node_modules/ ./node_modules 44 | COPY --from=build /lndboss/nest-cli.json ./ 45 | COPY --from=build /lndboss/next-env.d.ts ./ 46 | COPY --from=build /lndboss/src ./src 47 | COPY --from=build /lndboss/dist/ ./dist 48 | 49 | # Change ownership of files to use the new user 50 | RUN chown -R $USER_ID:$GROUP_ID /lndboss/ 51 | 52 | # Switch to the new user 53 | USER $USER_ID:$GROUP_ID 54 | 55 | # Create required directories 56 | # UID / GID 1000 is default for user `node` in the `node:latest` image, this 57 | # way the process will run as a non-root user 58 | RUN mkdir /home/node/.bosgui 59 | RUN mkdir /home/node/.lnd 60 | RUN touch .env 61 | 62 | # Expose the port the app runs on 63 | EXPOSE 8055 64 | 65 | # Start the app 66 | CMD [ "yarn", "start:prod" ] 67 | -------------------------------------------------------------------------------- /src/server/commands/lnurl/sign_auth_challenge.ts: -------------------------------------------------------------------------------- 1 | import { createHash, createHmac } from 'crypto'; 2 | 3 | import derEncodeSignature from './der_encode_signature'; 4 | 5 | const bufferAsHex = buffer => buffer.toString('hex'); 6 | const { from } = Buffer; 7 | const hexAsBuffer = hex => Buffer.from(hex, 'hex'); 8 | const hmacSha256 = (pk, url) => createHmac('sha256', pk).update(url).digest(); 9 | const sha256 = n => createHash('sha256').update(n).digest(); 10 | const utf8AsBuffer = utf8 => Buffer.from(utf8, 'utf8'); 11 | 12 | /** Sign an authentication challenge for LNURL Auth 13 | 14 | { 15 | ecp: 16 | hostname: 17 | k1: 18 | seed: 19 | } 20 | 21 | @returns 22 | { 23 | public_key: 24 | signature: 25 | } 26 | */ 27 | const signAuthChallenge = ({ ecp, hostname, k1, seed }) => { 28 | // LUD-13: LN wallet defines hashingKey as sha256(signature) 29 | const hashingKey = sha256(utf8AsBuffer(seed)); 30 | 31 | // LUD-13: linkingPrivKey is defined as hmacSha256(hashingKey, domain) 32 | const linkingPrivKey = hmacSha256(hashingKey, utf8AsBuffer(hostname)); 33 | 34 | // Instantiate the key pair from this derived private key 35 | const linkingKey = ecp.fromPrivateKey(linkingPrivKey); 36 | 37 | // Using the host-specific linking key, sign the challenge k1 value 38 | const signature = bufferAsHex(from(linkingKey.sign(hexAsBuffer(k1)))); 39 | 40 | return { 41 | public_key: bufferAsHex(linkingKey.publicKey), 42 | signature: derEncodeSignature({ signature }).encoded, 43 | }; 44 | }; 45 | 46 | export default signAuthChallenge; 47 | -------------------------------------------------------------------------------- /tests/server/graph.test.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticatedLnd, GetWalletInfoResult, getWalletInfo } from 'lightning'; 2 | import { Logger, createLogger, format, transports } from 'winston'; 3 | import { expect, test } from '@playwright/test'; 4 | import { setupChannel, spawnLightningCluster } from 'ln-docker-daemons'; 5 | 6 | import { graphCommand } from '../../src/server/commands'; 7 | 8 | test.describe('Test Graph command on the node.js side', async () => { 9 | type LightningCluster = { 10 | lnd: AuthenticatedLnd; 11 | kill: ({}) => Promise; 12 | nodes: any[]; 13 | }; 14 | let logger: Logger; 15 | let [alice, bob]: any[] = []; 16 | let lightning: LightningCluster; 17 | let bobWallet: GetWalletInfoResult; 18 | 19 | test.beforeAll(async () => { 20 | logger = createLogger({ 21 | level: 'info', 22 | format: format.json(), 23 | defaultMeta: { service: 'graph' }, 24 | transports: [ 25 | new transports.Console({ 26 | format: format.combine(format.prettyPrint()), 27 | }), 28 | ], 29 | }); 30 | 31 | lightning = await spawnLightningCluster({ size: 2 }); 32 | [alice, bob] = lightning.nodes; 33 | 34 | await setupChannel({ generate: alice.generate, lnd: alice.lnd, to: bob }); 35 | 36 | bobWallet = await getWalletInfo({ lnd: bob.lnd }); 37 | }); 38 | 39 | test('run graph command', async () => { 40 | const args = { 41 | filters: [], 42 | node: '', 43 | query: bobWallet.public_key, 44 | sort: '', 45 | }; 46 | const { result } = await graphCommand({ args, lnd: alice.lnd, logger }); 47 | console.log('graph----', result); 48 | expect(result).toBeTruthy(); 49 | }); 50 | 51 | test.afterAll(async () => { 52 | await lightning.kill({}); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/server/commands/accounting/accounting_command.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'balanceofsatoshis/commands/simple_request'; 2 | import * as types from '~shared/types'; 3 | 4 | import { AuthenticatedLnd } from 'lightning'; 5 | import { getAccountingReport } from 'balanceofsatoshis/balances'; 6 | import { httpLogger } from '~server/utils/global_functions'; 7 | 8 | /** Get an accounting report 9 | 10 | { 11 | category: 12 | [currency]: 13 | [date]: 14 | [fiat]: 15 | [is_csv]: 16 | [is_fiat_disabled]: 17 | lnd: 18 | [month]: 19 | [node]: 20 | [rate_provider]: 21 | request: 22 | [year]: 23 | } 24 | 25 | @returns via Promise 26 | { 27 | [rows]: [[]] 28 | [rows_summary]: [[]] 29 | } 30 | */ 31 | 32 | type Args = { 33 | args: types.commandAccounting; 34 | lnd: AuthenticatedLnd; 35 | }; 36 | 37 | const accountingCommand = async ({ args, lnd }: Args): Promise<{ result: any }> => { 38 | try { 39 | const result = await getAccountingReport({ 40 | lnd, 41 | request, 42 | category: args.category, 43 | date: args.date, 44 | is_csv: args.is_csv, 45 | is_fiat_disabled: args.is_fiat_disabled, 46 | month: args.month, 47 | node: args.node, 48 | rate_provider: args.rate_provider, 49 | year: Number(args.year), 50 | }); 51 | 52 | return { result }; 53 | } catch (error) { 54 | httpLogger({ error }); 55 | } 56 | }; 57 | 58 | export default accountingCommand; 59 | -------------------------------------------------------------------------------- /tests/client/balance.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import commands from '../../src/client/commands'; 4 | import { setCookie } from '../utils/setAccessToken'; 5 | import { testConstants } from '../utils/constants'; 6 | 7 | const BalanceCommand = commands.find(n => n.value === 'Balance'); 8 | 9 | test.describe('Test the Balance command client page', async () => { 10 | test.beforeEach(async ({ page }) => { 11 | await setCookie({ page }); 12 | }); 13 | 14 | test('Test the Balance command page and input values', async ({ page }) => { 15 | await page.goto(testConstants.commandsPage); 16 | await page.click('text=Balance'); 17 | await expect(page).toHaveTitle('Balance'); 18 | await page.type(`#${BalanceCommand?.flags?.above}`, '1000'); 19 | await page.type(`#${BalanceCommand?.flags?.below}`, '1000'); 20 | await page.check(`#${BalanceCommand?.flags?.confirmed}`); 21 | await page.check(`#${BalanceCommand?.flags?.detailed}`); 22 | await page.check(`#${BalanceCommand?.flags?.offchain}`); 23 | await page.check(`#${BalanceCommand?.flags?.onchain}`); 24 | await page.type('#node', 'testnode1'); 25 | await page.click('text=run command'); 26 | await page.waitForTimeout(1000); 27 | await expect(page.locator('text=OnchainBalance')).toBeVisible(); 28 | await expect(page.locator('text=OffchainBalance')).toBeVisible(); 29 | await expect(page.locator('text=ClosingBalance')).toBeVisible(); 30 | await expect(page.locator('text=ConflictedPending')).toBeVisible(); 31 | await expect(page.locator('text=InvalidPending')).toBeVisible(); 32 | await page.click('text=home'); 33 | }); 34 | 35 | test.afterEach(async ({ page }) => { 36 | await page.context().clearCookies(); 37 | await page.close(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/server/tags.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import { tagsCommand } from '../../src/server/commands/'; 4 | 5 | type Args = { 6 | add: string[]; 7 | id?: string; 8 | tag?: string; 9 | remove: string[]; 10 | is_avoided?: boolean; 11 | icon?: string; 12 | }; 13 | 14 | test.describe('Test Tags command on the node.js side', async () => { 15 | test.beforeAll(async () => { 16 | // Nothing to do 17 | }); 18 | 19 | test('run tags command, add a tag', async () => { 20 | const args: Args = { 21 | add: [ 22 | '034f10e9540d56502bd29e03b3d1536b798e723adcfbd43932f2313d282eb15d6f', 23 | '021b0ea06c90e7e4ea85daff1a83f7a1b97646da652829178ad1bd5f309af632eb', 24 | ], 25 | remove: [], 26 | tag: 'testtag', 27 | icon: '❤️', 28 | is_avoided: true, 29 | }; 30 | const { result } = await tagsCommand({ args }); 31 | console.log(result); 32 | expect(result).toBeTruthy(); 33 | }); 34 | 35 | test('run tags command, print all tags', async () => { 36 | const args = { 37 | add: [], 38 | remove: [], 39 | }; 40 | const { result } = await tagsCommand({ args }); 41 | console.log(result); 42 | 43 | expect(result).toBeTruthy(); 44 | }); 45 | 46 | test('run tags command, remove tag', async () => { 47 | const args = { 48 | add: [], 49 | remove: [ 50 | '034f10e9540d56502bd29e03b3d1536b798e723adcfbd43932f2313d282eb15d6f', 51 | '021b0ea06c90e7e4ea85daff1a83f7a1b97646da652829178ad1bd5f309af632eb', 52 | ], 53 | tag: 'testtag', 54 | }; 55 | const { result } = await tagsCommand({ args }); 56 | console.log(result); 57 | 58 | expect(result).toBeTruthy(); 59 | }); 60 | 61 | test.afterAll(async () => { 62 | // Nothing to do 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /tests/client/send.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import commands from '../../src/client/commands'; 4 | import { setCookie } from '../utils/setAccessToken'; 5 | import { testConstants } from '../utils/constants'; 6 | 7 | const SendCommand = commands.find(n => n.value === 'Send'); 8 | 9 | test.describe('Test the Send command client page', async () => { 10 | test.beforeEach(async ({ page }) => { 11 | await setCookie({ page }); 12 | }); 13 | 14 | test('test the Send command page and input values', async ({ page }) => { 15 | await page.goto(testConstants.commandsPage); 16 | await page.click('#Send'); 17 | await expect(page).toHaveTitle('Send'); 18 | await page.type(`#avoid-0`, 'ban'); 19 | await page.type(`#${SendCommand?.args?.destination}`, 'pubkey'); 20 | await page.type(`#${SendCommand?.flags?.in_through}`, 'carol'); 21 | await page.type(`#${SendCommand?.flags?.out_through}`, 'bob'); 22 | await page.type(`#${SendCommand?.flags?.amount}`, '50000'); 23 | await page.type(`#${SendCommand?.flags?.max_fee}`, '1000'); 24 | await page.type(`#${SendCommand?.flags?.max_fee_rate}`, '1000'); 25 | await page.type(`#${SendCommand?.flags?.message}`, 'test message'); 26 | 27 | await page.type('#node', 'alice'); 28 | 29 | await page.click('text=run command'); 30 | const popup = await page.waitForEvent('popup'); 31 | 32 | await expect(popup).toHaveTitle('Send Result'); 33 | await popup.waitForTimeout(1000); 34 | await expect(popup.locator('#sendResultTitle')).toBeVisible(); 35 | 36 | await popup.close(); 37 | 38 | await page.bringToFront(); 39 | await page.click('text=home'); 40 | }); 41 | 42 | test.afterEach(async ({ page }) => { 43 | await page.context().clearCookies(); 44 | await page.close(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/server/fees.test.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticatedLnd, GetWalletInfoResult, getWalletInfo } from 'lightning'; 2 | import { Logger, createLogger, format, transports } from 'winston'; 3 | import { expect, test } from '@playwright/test'; 4 | import { setupChannel, spawnLightningCluster } from 'ln-docker-daemons'; 5 | 6 | import { feesCommand } from '../../src/server/commands'; 7 | 8 | test.describe('Test Fees command on the node.js side', async () => { 9 | type LightningCluster = { 10 | lnd: AuthenticatedLnd; 11 | kill: ({}) => Promise; 12 | nodes: any[]; 13 | }; 14 | let lightning: LightningCluster; 15 | let logger: Logger; 16 | let bobWallet: GetWalletInfoResult; 17 | let [alice, bob]: any[] = []; 18 | 19 | test.beforeAll(async () => { 20 | logger = createLogger({ 21 | level: 'info', 22 | format: format.combine(format.prettyPrint()), 23 | defaultMeta: { service: 'fees' }, 24 | transports: [ 25 | new transports.Console({ 26 | format: format.combine(format.prettyPrint()), 27 | }), 28 | ], 29 | }); 30 | 31 | lightning = await spawnLightningCluster({ size: 2 }); 32 | [alice, bob] = lightning.nodes; 33 | 34 | await setupChannel({ generate: alice.generate, lnd: alice.lnd, to: bob }); 35 | 36 | bobWallet = await getWalletInfo({ lnd: bob.lnd }); 37 | }); 38 | 39 | test('run fees command', async () => { 40 | const args = { 41 | cltv_delta: 40, 42 | fees: '100', 43 | to: [bobWallet.public_key], 44 | }; 45 | 46 | const result = await feesCommand({ 47 | logger, 48 | lnd: alice.lnd, 49 | args, 50 | }); 51 | 52 | console.log('fees----', result); 53 | expect(result).toBeTruthy(); 54 | }); 55 | 56 | test.afterAll(async () => { 57 | await lightning.kill({}); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/server/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | 3 | import { Logger, SetMetadata } from '@nestjs/common'; 4 | 5 | import { CronExpression } from '@nestjs/schedule'; 6 | import { homedir } from 'os'; 7 | import { join } from 'path'; 8 | import { randomBytes } from 'crypto'; 9 | 10 | dotenv.config({ path: join(__dirname, '../../../.env') }); 11 | dotenv.config({ path: join(homedir(), '.bosgui', '.env') }); 12 | 13 | // Check if the environment is production 14 | export const isProduction = process.env.NODE_ENV === 'production'; 15 | 16 | Logger.log(`isProduction: ${isProduction}`); 17 | 18 | // These constants are for setting @Public() decorator 19 | export const IS_PUBLIC_KEY = 'isPublic'; 20 | export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); 21 | // These constants are for setting @Private() decorator 22 | 23 | // JWT secret key 24 | export const jwtConstants = { 25 | secret: !!isProduction ? randomBytes(64).toString('hex') : process.env.JWT_SECRET_DEV, 26 | }; 27 | 28 | // Encryption key 29 | export const encryptionKey = 30 | !!process.env.ENCRYPTION_KEY && process.env.ENCRYPTION_KEY !== '' ? process.env.ENCRYPTION_KEY : undefined; 31 | 32 | // Encryption algorithm 33 | export const algorithm = 'aes-256-cbc'; 34 | 35 | // Amboss API URL 36 | export const ambossUrl = 'https://api.amboss.space/graphql'; 37 | 38 | // Amboss Health Check Cron Schedule 39 | export const autoFeesCronSchedule = process.env.AUTO_FEES_SCHEDULE || CronExpression.EVERY_12_HOURS; 40 | 41 | // Parse Ansi escape sequences 42 | export const removeStyling = o => 43 | JSON.parse( 44 | JSON.stringify(o, (k, v) => 45 | typeof v === 'string' 46 | ? v.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '') 47 | : v === undefined 48 | ? null 49 | : v 50 | ) 51 | ); 52 | -------------------------------------------------------------------------------- /tests/client/cleanFailedPayments.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | import commands from '../../src/client/commands'; 4 | import { setCookie } from '../utils/setAccessToken'; 5 | import { testConstants } from '../utils/constants'; 6 | 7 | const CleanFailedPaymentsCommand = commands.find(n => n.value === 'CleanFailedPayments'); 8 | 9 | test.describe('Test the CleanFailedPayments command client page', async () => { 10 | test.beforeEach(async ({ page }) => { 11 | await setCookie({ page }); 12 | }); 13 | 14 | test('test the CleanFailedPayments (dryrun) command page and input values', async ({ page }) => { 15 | await page.goto(testConstants.commandsPage); 16 | await page.click('text=Clean Failed Payments'); 17 | await expect(page).toHaveTitle('Clean Failed Payments'); 18 | await page.check(`#${CleanFailedPaymentsCommand?.flags?.dryrun}`); 19 | 20 | await page.type('#node', 'testnode1'); 21 | 22 | await page.click('text=run command'); 23 | await page.waitForTimeout(1000); 24 | 25 | await expect(page.locator('#paymentsfound')).toBeVisible(); 26 | 27 | await page.click('text=home'); 28 | }); 29 | 30 | test('test the CleanFailedPayments command page and input values', async ({ page }) => { 31 | await page.goto(testConstants.commandsPage); 32 | await page.click('text=Clean Failed Payments'); 33 | await expect(page).toHaveTitle('Clean Failed Payments'); 34 | 35 | await page.type('#node', 'testnode1'); 36 | 37 | await page.click('text=run command'); 38 | await page.waitForTimeout(1000); 39 | 40 | await expect(page.locator('#paymentsdeleted')).toBeVisible(); 41 | 42 | await page.click('text=home'); 43 | }); 44 | 45 | test.afterEach(async ({ page }) => { 46 | await page.context().clearCookies(); 47 | await page.close(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/server/commands/invoice/invoice_command.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'balanceofsatoshis/commands/simple_request'; 2 | import * as types from '~shared/types'; 3 | 4 | import { AuthenticatedLnd } from 'lightning'; 5 | import { Logger } from 'winston'; 6 | import { createInvoice } from 'balanceofsatoshis/offchain'; 7 | import { interrogate } from 'balanceofsatoshis/commands'; 8 | 9 | /** Create an invoice for a requested amount 10 | { 11 | amount: 12 | [description]: 13 | [expires_in]: 14 | [is_hinting]: 15 | [is_selecting_hops]: 16 | [is_virtual]: 17 | lnd: 18 | [rate_provider]: 19 | } 20 | @returns via Promise 21 | { 22 | [is_settled]: 23 | [request]: 24 | [tokens]: 25 | } 26 | */ 27 | 28 | type Args = { 29 | args: types.commandInvoice; 30 | lnd: AuthenticatedLnd; 31 | logger: Logger; 32 | }; 33 | const invoiceCommand = async ({ args, lnd, logger }: Args): Promise<{ result: any }> => { 34 | const result = await createInvoice({ 35 | lnd, 36 | logger, 37 | request, 38 | amount: args.amount, 39 | ask: await interrogate({}), 40 | description: args.description, 41 | expires_in: args.expires_in, 42 | is_hinting: args.is_hinting || undefined, 43 | is_selecting_hops: args.is_selecting_hops || undefined, 44 | is_virtual: args.is_selecting_hops || undefined, 45 | rate_provider: args.rate_provider || undefined, 46 | virtual_fee_rate: args.virtual_fee_rate, 47 | }); 48 | 49 | return { result }; 50 | }; 51 | 52 | export default invoiceCommand; 53 | --------------------------------------------------------------------------------