├── .nvmrc ├── .github ├── FUNDING.yml ├── workflows │ ├── release-drafter.yml │ └── nodejs.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── release-drafter.yml ├── .vscode └── settings.json ├── .husky └── pre-commit ├── nest-cli.json ├── .eslintignore ├── src ├── client │ ├── public │ │ ├── favicon.ico │ │ ├── assets │ │ │ ├── amboss_icon.png │ │ │ └── amboss_logo.png │ │ └── static │ │ │ └── thunderstorm.webp │ ├── src │ │ ├── graphql │ │ │ ├── fragmentTypes.json │ │ │ ├── mutations │ │ │ │ ├── logout.ts │ │ │ │ ├── pushBackup.ts │ │ │ │ ├── loginAmboss.ts │ │ │ │ ├── createAddress.ts │ │ │ │ ├── getAuthToken.ts │ │ │ │ ├── removePeer.ts │ │ │ │ ├── toggleConfig.ts │ │ │ │ ├── removeTwofaSecret.ts │ │ │ │ ├── purchaseLiquidity.ts │ │ │ │ ├── updateMultipleFees.ts │ │ │ │ ├── updateTwofaSecret.ts │ │ │ │ ├── createBaseInvoice.ts │ │ │ │ ├── getSessionToken.ts │ │ │ │ ├── keysend.ts │ │ │ │ ├── openChannel.ts │ │ │ │ ├── createMacaroon.ts │ │ │ │ ├── pay.ts │ │ │ │ ├── addPeer.ts │ │ │ │ ├── createThunderPoints.ts │ │ │ │ ├── sendMessage.ts │ │ │ │ ├── closeChannel.ts │ │ │ │ ├── createInvoice.ts │ │ │ │ ├── sendToAddress.ts │ │ │ │ ├── claimBoltzTransaction.ts │ │ │ │ ├── lnMarkets.ts │ │ │ │ ├── updateFees.ts │ │ │ │ ├── createBoltzReverseSwap.ts │ │ │ │ ├── bosRebalance.ts │ │ │ │ └── lnUrl.ts │ │ │ └── queries │ │ │ │ ├── getBackups.ts │ │ │ │ ├── getBitcoinPrice.ts │ │ │ │ ├── getLnMarketsUrl.ts │ │ │ │ ├── getLatestVersion.ts │ │ │ │ ├── getBaseCanConnect.ts │ │ │ │ ├── getLiquidityPerUsd.ts │ │ │ │ ├── getLnMarketsStatus.ts │ │ │ │ ├── signMessage.ts │ │ │ │ ├── recoverFunds.ts │ │ │ │ ├── verifyBackup.ts │ │ │ │ ├── verifyBackups.ts │ │ │ │ ├── getBasePoints.ts │ │ │ │ ├── getTwofaSecret.ts │ │ │ │ ├── getInvoiceStatusChange.ts │ │ │ │ ├── getBoltzInfo.ts │ │ │ │ ├── getAmbossLoginToken.ts │ │ │ │ ├── verifyMessage.ts │ │ │ │ ├── getBaseNodes.ts │ │ │ │ ├── getAccount.ts │ │ │ │ ├── getBitcoinFees.ts │ │ │ │ ├── getServerAccounts.ts │ │ │ │ ├── getNode.ts │ │ │ │ ├── getUtxos.ts │ │ │ │ ├── getLnMarketsUserInfo.ts │ │ │ │ ├── getConfigState.ts │ │ │ │ ├── getChannelReport.ts │ │ │ │ ├── getChainTransactions.ts │ │ │ │ ├── getLightningAddressInfo.ts │ │ │ │ ├── getBoltzSwapStatus.ts │ │ │ │ ├── getAmbossUser.ts │ │ │ │ ├── getNetworkInfo.ts │ │ │ │ ├── getNodeBalances.ts │ │ │ │ ├── getMessages.ts │ │ │ │ ├── getVolumeHealth.ts │ │ │ │ ├── getNodeSocialInfo.ts │ │ │ │ ├── getWalletInfo.ts │ │ │ │ ├── getAccountingReport.ts │ │ │ │ ├── getTimeHealth.ts │ │ │ │ ├── getPeers.ts │ │ │ │ ├── getNodeInfo.ts │ │ │ │ ├── decodeRequest.ts │ │ │ │ ├── getPendingChannels.ts │ │ │ │ ├── getFeeHealth.ts │ │ │ │ ├── getClosedChannels.ts │ │ │ │ ├── getPayments.ts │ │ │ │ ├── getChannel.ts │ │ │ │ └── getInvoices.ts │ │ ├── utils │ │ │ ├── appConstants.ts │ │ │ ├── cookies.ts │ │ │ ├── basePath.tsx │ │ │ ├── gridConstants.ts │ │ │ ├── number.ts │ │ │ ├── version.ts │ │ │ ├── url.ts │ │ │ ├── ssr.ts │ │ │ └── chat.ts │ │ ├── components │ │ │ ├── chart │ │ │ │ └── common.ts │ │ │ ├── spacer │ │ │ │ └── Spacer.tsx │ │ │ ├── emoji │ │ │ │ └── Emoji.tsx │ │ │ ├── notification │ │ │ │ └── Beta.tsx │ │ │ ├── bitcoinInfo │ │ │ │ ├── BitcoinFees.ts │ │ │ │ └── BitcoinPrice.ts │ │ │ ├── viewSwitch │ │ │ │ └── ViewSwitch.tsx │ │ │ ├── table │ │ │ │ └── DebouncedInput.tsx │ │ │ ├── loadingBar │ │ │ │ └── LoadingBar.tsx │ │ │ ├── satoshi │ │ │ │ └── Satoshi.tsx │ │ │ ├── burgerMenu │ │ │ │ └── BurgerMenu.tsx │ │ │ ├── checkbox │ │ │ │ └── Checkbox.tsx │ │ │ ├── slider │ │ │ │ └── index.tsx │ │ │ ├── section │ │ │ │ └── Section.tsx │ │ │ ├── version │ │ │ │ └── Version.tsx │ │ │ └── modal │ │ │ │ └── ReactModal.tsx │ │ ├── layouts │ │ │ └── Layout.styled.ts │ │ ├── hooks │ │ │ ├── UseAmbossUser.tsx │ │ │ ├── UseAccount.tsx │ │ │ ├── UseBaseConnect.tsx │ │ │ ├── UseInterval.tsx │ │ │ ├── UseNodeDetails.tsx │ │ │ ├── UseNodeBalances.tsx │ │ │ ├── UseMutationWithReset.tsx │ │ │ ├── UseChannelInfo.ts │ │ │ ├── UseBitcoinFees.tsx │ │ │ ├── UseElementSize.tsx │ │ │ ├── UseEventListener.tsx │ │ │ ├── UseCheckAuthToken.tsx │ │ │ └── UseSocket.tsx │ │ ├── views │ │ │ ├── dashboard │ │ │ │ └── widgets │ │ │ │ │ ├── lightning │ │ │ │ │ ├── channels.tsx │ │ │ │ │ ├── forwards.tsx │ │ │ │ │ └── info.tsx │ │ │ │ │ ├── util │ │ │ │ │ ├── Sign.tsx │ │ │ │ │ └── DonateWidget.tsx │ │ │ │ │ ├── external │ │ │ │ │ └── mempool.tsx │ │ │ │ │ └── link │ │ │ │ │ └── index.tsx │ │ │ ├── swap │ │ │ │ ├── SwapExpire.tsx │ │ │ │ └── types.ts │ │ │ ├── homepage │ │ │ │ ├── Top.tsx │ │ │ │ └── HomePage.styled.ts │ │ │ ├── home │ │ │ │ ├── reports │ │ │ │ │ ├── forwardReport │ │ │ │ │ │ └── helpers.ts │ │ │ │ │ └── mempool │ │ │ │ │ │ └── index.tsx │ │ │ │ ├── account │ │ │ │ │ └── createInvoice │ │ │ │ │ │ ├── InvoiceStatus.tsx │ │ │ │ │ │ └── Timer.tsx │ │ │ │ └── quickActions │ │ │ │ │ ├── decode │ │ │ │ │ └── Decode.tsx │ │ │ │ │ ├── donate │ │ │ │ │ └── DonateCard.tsx │ │ │ │ │ └── lightningAddress │ │ │ │ │ └── Addresses.tsx │ │ │ ├── lnmarkets │ │ │ │ └── GoToLnMarkets.tsx │ │ │ ├── tools │ │ │ │ ├── messages │ │ │ │ │ └── Messages.tsx │ │ │ │ ├── Tools.styled.tsx │ │ │ │ └── backups │ │ │ │ │ ├── Backups.tsx │ │ │ │ │ └── DownloadBackups.tsx │ │ │ ├── stats │ │ │ │ ├── Wrapper.tsx │ │ │ │ └── styles.tsx │ │ │ ├── channels │ │ │ │ └── channels │ │ │ │ │ ├── helpers.ts │ │ │ │ │ └── ChannelDetails.tsx │ │ │ ├── settings │ │ │ │ ├── Dashboard.tsx │ │ │ │ └── WidgetRow.tsx │ │ │ ├── amboss │ │ │ │ └── Billboard.tsx │ │ │ ├── chat │ │ │ │ └── helpers │ │ │ │ │ └── chatHelpers.ts │ │ │ └── balance │ │ │ │ └── Balance.styled.tsx │ │ ├── context │ │ │ ├── ContextProvider.tsx │ │ │ ├── DashContext.tsx │ │ │ └── BaseContext.tsx │ │ └── styles │ │ │ └── GlobalStyle.ts │ ├── .prettierrc │ ├── @types │ │ └── index.d.ts │ ├── next-env.d.ts │ ├── pages │ │ ├── _error.tsx │ │ ├── swap.tsx │ │ ├── sso.tsx │ │ ├── login.tsx │ │ ├── settings │ │ │ ├── dashboard.tsx │ │ │ └── index.tsx │ │ ├── dashboard.tsx │ │ ├── tools.tsx │ │ ├── chain.tsx │ │ ├── rebalance.tsx │ │ ├── stats.tsx │ │ ├── _document.tsx │ │ ├── index.tsx │ │ └── amboss │ │ │ └── index.tsx │ ├── tsconfig.json │ ├── .eslintrc.js │ └── next.config.js └── server │ ├── modules │ ├── accounts │ │ ├── accounts.types.ts │ │ └── accounts.module.ts │ ├── api │ │ ├── main │ │ │ ├── main.module.ts │ │ │ └── main.resolver.ts │ │ ├── auth │ │ │ ├── auth.types.ts │ │ │ └── auth.module.ts │ │ ├── base │ │ │ ├── base.module.ts │ │ │ └── base.types.ts │ │ ├── tools │ │ │ └── tools.module.ts │ │ ├── wallet │ │ │ ├── wallet.module.ts │ │ │ ├── wallet.resolver.ts │ │ │ └── wallet.types.ts │ │ ├── github │ │ │ ├── github.module.ts │ │ │ └── github.resolver.ts │ │ ├── bitcoin │ │ │ ├── bitcoin.module.ts │ │ │ └── bitcoin.types.ts │ │ ├── chat │ │ │ ├── chat.module.ts │ │ │ └── chat.types.ts │ │ ├── edge │ │ │ ├── edge.module.ts │ │ │ └── edge.types.ts │ │ ├── peer │ │ │ ├── peer.module.ts │ │ │ └── peer.types.ts │ │ ├── account │ │ │ ├── account.module.ts │ │ │ └── account.types.ts │ │ ├── chain │ │ │ ├── chain.module.ts │ │ │ └── chain.types.ts │ │ ├── health │ │ │ └── health.module.ts │ │ ├── network │ │ │ ├── network.module.ts │ │ │ ├── network.types.ts │ │ │ └── network.resolver.ts │ │ ├── invoices │ │ │ └── invoices.module.ts │ │ ├── macaroon │ │ │ ├── macaroon.module.ts │ │ │ ├── macaroon.resolver.ts │ │ │ └── macaroon.types.ts │ │ ├── transactions │ │ │ └── transactions.module.ts │ │ ├── bos │ │ │ ├── bos.module.ts │ │ │ └── bos.types.ts │ │ ├── channels │ │ │ ├── channels.module.ts │ │ │ └── channels.helpers.ts │ │ ├── lnurl │ │ │ └── lnurl.module.ts │ │ ├── lnmarkets │ │ │ ├── lnmarkets.types.ts │ │ │ └── lnmarkets.module.ts │ │ ├── userConfig │ │ │ ├── userConfig.module.ts │ │ │ └── userConfig.types.ts │ │ ├── amboss │ │ │ ├── amboss.module.ts │ │ │ └── amboss.helpers.ts │ │ ├── node │ │ │ └── node.module.ts │ │ ├── boltz │ │ │ └── boltz.module.ts │ │ └── forwards │ │ │ └── forwards.module.ts │ ├── node │ │ ├── lnd │ │ │ ├── lnd.module.ts │ │ │ └── lnd.helpers.ts │ │ └── node.module.ts │ ├── fetch │ │ └── fetch.module.ts │ ├── files │ │ └── files.module.ts │ ├── security │ │ ├── security.types.ts │ │ ├── guards │ │ │ ├── graphql.guard.ts │ │ │ ├── throttler.guard.ts │ │ │ └── roles.guard.ts │ │ ├── jwt.strategy.ts │ │ ├── security.decorators.ts │ │ └── security.module.ts │ ├── view │ │ ├── view.module.ts │ │ ├── view.controller.ts │ │ └── view.service.ts │ ├── mempool │ │ ├── mempool.module.ts │ │ └── mempool.service.ts │ ├── blockstream │ │ ├── blockstream.module.ts │ │ └── blockstream.service.ts │ ├── dataloader │ │ ├── dataloader.module.ts │ │ └── dataloader.service.ts │ ├── ws │ │ ├── ws.module.ts │ │ └── ws.service.ts │ ├── sub │ │ └── sub.module.ts │ └── auth │ │ ├── auth.module.ts │ │ └── auth.service.ts │ ├── utils │ ├── appConstants.ts │ ├── async.ts │ ├── request.ts │ ├── network.ts │ ├── env.ts │ ├── string.ts │ └── crypto.ts │ └── main.ts ├── tsconfig.build.json ├── README.md ├── .prettierrc ├── .dockerignore ├── tsconfig.json ├── flake.nix ├── .versionrc.js ├── .eslintrc.js ├── codegen.yml ├── .gitignore ├── scripts └── updateToLatest.sh ├── LICENSE └── Dockerfile /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.18.2 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [apotdevin] 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint-staged 5 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src/server" 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | src/client/.next 4 | 5 | **/*.generated.tsx 6 | src/graphql/types.ts -------------------------------------------------------------------------------- /src/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apotdevin/thunderhub/HEAD/src/client/public/favicon.ico -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/client/public/assets/amboss_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apotdevin/thunderhub/HEAD/src/client/public/assets/amboss_icon.png -------------------------------------------------------------------------------- /src/client/public/assets/amboss_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apotdevin/thunderhub/HEAD/src/client/public/assets/amboss_logo.png -------------------------------------------------------------------------------- /src/client/public/static/thunderstorm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apotdevin/thunderhub/HEAD/src/client/public/static/thunderstorm.webp -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **ThunderHub - Lightning Node Manager** 2 | 3 | **Documentation has moved!** Find the documentation [here](http://docs.thunderhub.io/). 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "tabWidth": 2, 5 | "printWidth": 80, 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/fragmentTypes.json: -------------------------------------------------------------------------------- 1 | { 2 | "possibleTypes": { 3 | "LnUrlRequest": ["ChannelRequest", "PayRequest", "WithdrawRequest"] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "tabWidth": 2, 5 | "printWidth": 80, 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /src/client/@types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | declare module '*.jpg'; 3 | declare module '*.jpeg'; 4 | declare module '*.svg'; 5 | declare module '*.gif'; 6 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/logout.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const LOGOUT = gql` 4 | mutation Logout { 5 | logout 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/server/modules/accounts/accounts.types.ts: -------------------------------------------------------------------------------- 1 | import { ParsedAccount } from '../files/files.types'; 2 | 3 | export type EnrichedAccount = { 4 | lnd: any; 5 | } & ParsedAccount; 6 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getBackups.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_BACKUPS = gql` 4 | query GetBackups { 5 | getBackups 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/pushBackup.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const PUSH_BACKUP = gql` 4 | mutation PushBackup { 5 | pushBackup 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/loginAmboss.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const LOGIN_AMBOSS = gql` 4 | mutation LoginAmboss { 5 | loginAmboss 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getBitcoinPrice.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_BITCOIN_PRICE = gql` 4 | query GetBitcoinPrice { 5 | getBitcoinPrice 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getLnMarketsUrl.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_LN_MARKETS_URL = gql` 4 | query GetLnMarketsUrl { 5 | getLnMarketsUrl 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getLatestVersion.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_LATEST_VERSION = gql` 4 | query GetLatestVersion { 5 | getLatestVersion 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getBaseCanConnect.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_BASE_CAN_CONNECT = gql` 4 | query GetBaseCanConnect { 5 | getBaseCanConnect 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getLiquidityPerUsd.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GetLiquidityPerUsd = gql` 4 | query GetLiquidityPerUsd { 5 | getLiquidityPerUsd 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getLnMarketsStatus.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_LN_MARKETS_STATUS = gql` 4 | query GetLnMarketsStatus { 5 | getLnMarketsStatus 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/utils/appConstants.ts: -------------------------------------------------------------------------------- 1 | export const appConstants = { 2 | cookieName: 'Thub-Auth', 3 | lnMarketsAuth: 'LnMarkets-Auth', 4 | tokenCookieName: 'Tbase-Auth', 5 | ambossCookieName: 'Amboss-Auth', 6 | }; 7 | -------------------------------------------------------------------------------- /src/server/utils/appConstants.ts: -------------------------------------------------------------------------------- 1 | export const appConstants = { 2 | cookieName: 'Thub-Auth', 3 | lnMarketsAuth: 'LnMarkets-Auth', 4 | tokenCookieName: 'Tbase-Auth', 5 | ambossCookieName: 'Amboss-Auth', 6 | }; 7 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/signMessage.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const SIGN_MESSAGE = gql` 4 | query SignMessage($message: String!) { 5 | signMessage(message: $message) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/createAddress.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const CREATE_ADDRESS = gql` 4 | mutation CreateAddress($type: String) { 5 | createAddress(type: $type) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/recoverFunds.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const RECOVER_FUNDS = gql` 4 | query RecoverFunds($backup: String!) { 5 | recoverFunds(backup: $backup) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/verifyBackup.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const VERIFY_BACKUP = gql` 4 | query VerifyBackup($backup: String!) { 5 | verifyBackup(backup: $backup) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/verifyBackups.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const VERIFY_BACKUPS = gql` 4 | query VerifyBackups($backup: String!) { 5 | verifyBackups(backup: $backup) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/server/modules/api/main/main.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MainResolver } from './main.resolver'; 3 | 4 | @Module({ 5 | providers: [MainResolver], 6 | }) 7 | export class MainModule {} 8 | -------------------------------------------------------------------------------- /src/server/modules/node/lnd/lnd.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { LndService } from './lnd.service'; 3 | 4 | @Module({ providers: [LndService], exports: [LndService] }) 5 | export class LndModule {} 6 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/getAuthToken.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_AUTH_TOKEN = gql` 4 | mutation GetAuthToken($cookie: String) { 5 | getAuthToken(cookie: $cookie) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/removePeer.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const REMOVE_PEER = gql` 4 | mutation RemovePeer($publicKey: String!) { 5 | removePeer(publicKey: $publicKey) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/toggleConfig.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const TOGGLE_CONFIG = gql` 4 | mutation ToggleConfig($field: ConfigFields!) { 5 | toggleConfig(field: $field) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/server/modules/api/auth/auth.types.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | 3 | @ObjectType() 4 | export class TwofaResult { 5 | @Field() 6 | url: string; 7 | @Field() 8 | secret: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/client/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /src/client/src/components/chart/common.ts: -------------------------------------------------------------------------------- 1 | export const COMMON_CHART_STYLES = { 2 | tooltip: { 3 | backgroundColor: 'black', 4 | borderColor: 'black', 5 | textStyle: { 6 | color: 'white', 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/removeTwofaSecret.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const REMOVE_TWOFA_SECRET = gql` 4 | mutation RemoveTwofaSecret($token: String!) { 5 | removeTwofaSecret(token: $token) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/server/modules/api/main/main.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Query, Resolver } from '@nestjs/graphql'; 2 | 3 | @Resolver() 4 | export class MainResolver { 5 | @Query(() => String) 6 | async getHello() { 7 | return 'Hello'; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getBasePoints.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_BASE_POINTS = gql` 4 | query GetBasePoints { 5 | getBasePoints { 6 | alias 7 | amount 8 | } 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getTwofaSecret.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_TWOFA_SECRET = gql` 4 | query GetTwofaSecret { 5 | getTwofaSecret { 6 | secret 7 | url 8 | } 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /src/client/src/utils/cookies.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage } from 'http'; 2 | import cookie from 'cookie'; 3 | 4 | export const parseCookies = (req: IncomingMessage) => { 5 | return cookie.parse(req ? req.headers.cookie || '' : document?.cookie); 6 | }; 7 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getInvoiceStatusChange.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_INVOICE_STATUS_CHANGE = gql` 4 | query GetInvoiceStatusChange($id: String!) { 5 | getInvoiceStatusChange(id: $id) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/utils/basePath.tsx: -------------------------------------------------------------------------------- 1 | import getConfig from 'next/config'; 2 | 3 | const { publicRuntimeConfig } = getConfig(); 4 | const { basePath } = publicRuntimeConfig; 5 | 6 | export const appendBasePath = (url: string): string => `${basePath}${url}`; 7 | -------------------------------------------------------------------------------- /src/server/modules/fetch/fetch.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FetchService } from './fetch.service'; 3 | 4 | @Module({ 5 | providers: [FetchService], 6 | exports: [FetchService], 7 | }) 8 | export class FetchModule {} 9 | -------------------------------------------------------------------------------- /src/server/modules/files/files.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FilesService } from './files.service'; 3 | 4 | @Module({ 5 | providers: [FilesService], 6 | exports: [FilesService], 7 | }) 8 | export class FilesModule {} 9 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getBoltzInfo.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_BOLTZ_INFO = gql` 4 | query GetBoltzInfo { 5 | getBoltzInfo { 6 | max 7 | min 8 | feePercent 9 | } 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/purchaseLiquidity.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const PurchaseLiquidity = gql` 4 | mutation PurchaseLiquidity($amount_cents: String!) { 5 | purchaseLiquidity(amount_cents: $amount_cents) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getAmbossLoginToken.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_AMBOSS_LOGIN_TOKEN = gql` 4 | query GetAmbossLoginToken($redirect_url: String) { 5 | getAmbossLoginToken(redirect_url: $redirect_url) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/verifyMessage.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const VERIFY_MESSAGE = gql` 4 | query VerifyMessage($message: String!, $signature: String!) { 5 | verifyMessage(message: $message, signature: $signature) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/updateMultipleFees.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const UPDATE_MULTIPLE_FEES = gql` 4 | mutation UpdateMultipleFees($channels: [UpdateRoutingFeesParams!]!) { 5 | updateMultipleFees(channels: $channels) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/updateTwofaSecret.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const UPDATE_TWOFA_SECRET = gql` 4 | mutation UpdateTwofaSecret($secret: String!, $token: String!) { 5 | updateTwofaSecret(secret: $secret, token: $token) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getBaseNodes.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_BASE_NODES = gql` 4 | query GetBaseNodes { 5 | getBaseNodes { 6 | _id 7 | name 8 | public_key 9 | socket 10 | } 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /src/client/src/layouts/Layout.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const PageWrapper = styled.div` 4 | position: relative; 5 | min-height: 100vh; 6 | `; 7 | 8 | export const HeaderBodyWrapper = styled.div` 9 | padding-bottom: 120px; 10 | `; 11 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getAccount.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_ACCOUNT = gql` 4 | query GetAccount { 5 | getAccount { 6 | name 7 | id 8 | loggedIn 9 | type 10 | twofaEnabled 11 | } 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getBitcoinFees.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_BITCOIN_FEES = gql` 4 | query GetBitcoinFees { 5 | getBitcoinFees { 6 | fast 7 | halfHour 8 | hour 9 | minimum 10 | } 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getServerAccounts.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_SERVER_ACCOUNTS = gql` 4 | query GetServerAccounts { 5 | getServerAccounts { 6 | name 7 | id 8 | loggedIn 9 | type 10 | } 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /src/server/modules/api/base/base.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FetchModule } from '../../fetch/fetch.module'; 3 | import { BaseResolver } from './base.resolver'; 4 | 5 | @Module({ imports: [FetchModule], providers: [BaseResolver] }) 6 | export class BaseModule {} 7 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/createBaseInvoice.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const CREATE_BASE_INVOICE = gql` 4 | mutation CreateBaseInvoice($amount: Float!) { 5 | createBaseInvoice(amount: $amount) { 6 | request 7 | id 8 | } 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/getSessionToken.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_SESSION_TOKEN = gql` 4 | mutation GetSessionToken($id: String!, $password: String!, $token: String) { 5 | getSessionToken(id: $id, password: $password, token: $token) 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/keysend.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const KEY_SEND = gql` 4 | mutation Keysend($destination: String!, $tokens: Float!) { 5 | keysend(destination: $destination, tokens: $tokens) { 6 | is_confirmed 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /src/client/src/utils/gridConstants.ts: -------------------------------------------------------------------------------- 1 | export const defaultGrid = { 2 | breakpoints: { 3 | lg: 1200, 4 | md: 996, 5 | sm: 768, 6 | xs: 480, 7 | xxs: 0, 8 | }, 9 | columns: { lg: 24, md: 16, sm: 12, xs: 4, xxs: 2 }, 10 | margin: [4, 4] as [number, number], 11 | }; 12 | -------------------------------------------------------------------------------- /src/server/modules/api/tools/tools.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NodeModule } from '../../node/node.module'; 3 | import { ToolsResolver } from './tools.resolver'; 4 | 5 | @Module({ imports: [NodeModule], providers: [ToolsResolver] }) 6 | export class ToolsModule {} 7 | -------------------------------------------------------------------------------- /src/server/modules/api/wallet/wallet.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NodeModule } from '../../node/node.module'; 3 | import { WalletResolver } from './wallet.resolver'; 4 | 5 | @Module({ imports: [NodeModule], providers: [WalletResolver] }) 6 | export class WalletModule {} 7 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/openChannel.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const OPEN_CHANNEL = gql` 4 | mutation OpenChannel($input: OpenChannelParams!) { 5 | openChannel(input: $input) { 6 | transactionId 7 | transactionOutputIndex 8 | } 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /src/server/modules/api/github/github.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FetchModule } from '../../fetch/fetch.module'; 3 | import { GithubResolver } from './github.resolver'; 4 | 5 | @Module({ imports: [FetchModule], providers: [GithubResolver] }) 6 | export class GithubModule {} 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .cache 4 | *.md 5 | !README*.md 6 | /node_modules 7 | /.next 8 | src/client/.next 9 | /dist 10 | /docs 11 | /.github 12 | .vscode 13 | CHANGELOG.md 14 | 15 | # all env files 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/createMacaroon.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const CREATE_MACAROON = gql` 4 | mutation CreateMacaroon($permissions: NetworkInfoInput!) { 5 | createMacaroon(permissions: $permissions) { 6 | base 7 | hex 8 | } 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /src/server/modules/api/bitcoin/bitcoin.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FetchModule } from '../../fetch/fetch.module'; 3 | import { BitcoinResolver } from './bitcoin.resolver'; 4 | 5 | @Module({ imports: [FetchModule], providers: [BitcoinResolver] }) 6 | export class BitcoinModule {} 7 | -------------------------------------------------------------------------------- /src/server/modules/api/bitcoin/bitcoin.types.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | 3 | @ObjectType() 4 | export class BitcoinFee { 5 | @Field() 6 | fast: number; 7 | @Field() 8 | halfHour: number; 9 | @Field() 10 | hour: number; 11 | @Field() 12 | minimum: number; 13 | } 14 | -------------------------------------------------------------------------------- /src/server/modules/api/chat/chat.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NodeModule } from '../../node/node.module'; 3 | import { ChatResolver } from './chat.resolver'; 4 | 5 | @Module({ 6 | imports: [NodeModule], 7 | providers: [ChatResolver], 8 | }) 9 | export class ChatModule {} 10 | -------------------------------------------------------------------------------- /src/server/modules/api/edge/edge.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NodeModule } from '../../node/node.module'; 3 | import { EdgeResolver } from './edge.resolver'; 4 | 5 | @Module({ 6 | imports: [NodeModule], 7 | providers: [EdgeResolver], 8 | }) 9 | export class EdgeModule {} 10 | -------------------------------------------------------------------------------- /src/server/modules/api/peer/peer.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NodeModule } from '../../node/node.module'; 3 | import { PeerResolver } from './peer.resolver'; 4 | 5 | @Module({ 6 | imports: [NodeModule], 7 | providers: [PeerResolver], 8 | }) 9 | export class PeerModule {} 10 | -------------------------------------------------------------------------------- /src/server/modules/security/security.types.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | 3 | export type JwtObjectType = { 4 | iat: number; 5 | exp: number; 6 | iss: string; 7 | sub: string; 8 | }; 9 | 10 | @ObjectType() 11 | export class UserId { 12 | @Field() 13 | id: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/server/modules/api/account/account.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AccountsModule } from '../../accounts/accounts.module'; 3 | import { AccountResolver } from './account.resolver'; 4 | 5 | @Module({ imports: [AccountsModule], providers: [AccountResolver] }) 6 | export class AccountModule {} 7 | -------------------------------------------------------------------------------- /src/server/modules/api/chain/chain.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NodeModule } from '../../node/node.module'; 3 | import { ChainResolver } from './chain.resolver'; 4 | 5 | @Module({ 6 | imports: [NodeModule], 7 | providers: [ChainResolver], 8 | }) 9 | export class ChainModule {} 10 | -------------------------------------------------------------------------------- /src/server/modules/api/health/health.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NodeModule } from '../../node/node.module'; 3 | import { HealthResolver } from './health.resolver'; 4 | 5 | @Module({ 6 | imports: [NodeModule], 7 | providers: [HealthResolver], 8 | }) 9 | export class HealthModule {} 10 | -------------------------------------------------------------------------------- /src/server/modules/api/network/network.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NodeModule } from '../../node/node.module'; 3 | import { NetworkResolver } from './network.resolver'; 4 | 5 | @Module({ 6 | imports: [NodeModule], 7 | providers: [NetworkResolver], 8 | }) 9 | export class NetworkModule {} 10 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getNode.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_NODE = gql` 4 | query GetNode($publicKey: String!, $withoutChannels: Boolean) { 5 | getNode(publicKey: $publicKey, withoutChannels: $withoutChannels) { 6 | node { 7 | alias 8 | } 9 | } 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /src/server/modules/api/invoices/invoices.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NodeModule } from '../../node/node.module'; 3 | import { InvoicesResolver } from './invoices.resolver'; 4 | 5 | @Module({ 6 | imports: [NodeModule], 7 | providers: [InvoicesResolver], 8 | }) 9 | export class InvoicesModule {} 10 | -------------------------------------------------------------------------------- /src/server/modules/api/macaroon/macaroon.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NodeModule } from '../../node/node.module'; 3 | import { MacaroonResolver } from './macaroon.resolver'; 4 | 5 | @Module({ 6 | imports: [NodeModule], 7 | providers: [MacaroonResolver], 8 | }) 9 | export class MacaroonModule {} 10 | -------------------------------------------------------------------------------- /src/server/modules/view/view.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ViewController } from './view.controller'; 4 | import { ViewService } from './view.service'; 5 | 6 | @Module({ 7 | imports: [], 8 | providers: [ViewService], 9 | controllers: [ViewController], 10 | }) 11 | export class ViewModule {} 12 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/pay.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const PAY = gql` 4 | mutation Pay( 5 | $max_fee: Float! 6 | $max_paths: Float! 7 | $out: [String!] 8 | $request: String! 9 | ) { 10 | pay(max_fee: $max_fee, max_paths: $max_paths, out: $out, request: $request) 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /src/client/src/utils/number.ts: -------------------------------------------------------------------------------- 1 | import numeral from 'numeral'; 2 | 3 | export const numberWithCommas = ( 4 | x: number | string | undefined | null, 5 | format = '0,0' 6 | ): string => { 7 | const normalized = Number(x); 8 | 9 | if (!normalized) { 10 | return '-'; 11 | } 12 | 13 | return numeral(normalized).format(format); 14 | }; 15 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getUtxos.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_UTXOS = gql` 4 | query GetUtxos { 5 | getUtxos { 6 | address 7 | address_format 8 | confirmation_count 9 | output_script 10 | tokens 11 | transaction_id 12 | transaction_vout 13 | } 14 | } 15 | `; 16 | -------------------------------------------------------------------------------- /src/server/modules/api/account/account.types.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | 3 | @ObjectType() 4 | export class ServerAccount { 5 | @Field() 6 | name: string; 7 | @Field() 8 | id: string; 9 | @Field() 10 | loggedIn: boolean; 11 | @Field() 12 | type: string; 13 | @Field() 14 | twofaEnabled: boolean; 15 | } 16 | -------------------------------------------------------------------------------- /src/server/modules/api/transactions/transactions.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NodeModule } from '../../node/node.module'; 3 | import { TransactionsResolver } from './transactions.resolver'; 4 | 5 | @Module({ 6 | imports: [NodeModule], 7 | providers: [TransactionsResolver], 8 | }) 9 | export class TransactionsModule {} 10 | -------------------------------------------------------------------------------- /src/server/modules/mempool/mempool.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MempoolService } from './mempool.service'; 3 | import { FetchModule } from '../fetch/fetch.module'; 4 | 5 | @Module({ 6 | imports: [FetchModule], 7 | providers: [MempoolService], 8 | exports: [MempoolService], 9 | }) 10 | export class MempoolModule {} 11 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getLnMarketsUserInfo.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_LN_MARKETS_USER_INFO = gql` 4 | query GetLnMarketsUserInfo { 5 | getLnMarketsUserInfo { 6 | uid 7 | balance 8 | account_type 9 | username 10 | linkingpublickey 11 | last_ip 12 | } 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /src/server/modules/accounts/accounts.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FilesModule } from '../files/files.module'; 3 | import { AccountsService } from './accounts.service'; 4 | 5 | @Module({ 6 | imports: [FilesModule], 7 | providers: [AccountsService], 8 | exports: [AccountsService], 9 | }) 10 | export class AccountsModule {} 11 | -------------------------------------------------------------------------------- /src/client/pages/_error.tsx: -------------------------------------------------------------------------------- 1 | import Error from 'next/error'; 2 | 3 | function Page({ statusCode }: any) { 4 | return ; 5 | } 6 | 7 | Page.getInitialProps = ({ res, err }: any) => { 8 | const statusCode = res ? res.statusCode : err ? err.statusCode : 404; 9 | return { statusCode }; 10 | }; 11 | 12 | export default Page; 13 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getConfigState.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_CONFIG_STATE = gql` 4 | query GetConfigState { 5 | getConfigState { 6 | backup_state 7 | healthcheck_ping_state 8 | onchain_push_enabled 9 | channels_push_enabled 10 | private_channels_push_enabled 11 | } 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /src/client/src/components/spacer/Spacer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | import { mediaWidths } from '../../../src/styles/Themes'; 4 | 5 | const StyledSpacer = styled.div` 6 | height: 32px; 7 | 8 | @media (${mediaWidths.mobile}) { 9 | height: 0; 10 | } 11 | `; 12 | 13 | export const Spacer = () => ; 14 | -------------------------------------------------------------------------------- /src/server/modules/api/bos/bos.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AccountsModule } from '../../accounts/accounts.module'; 3 | import { WsModule } from '../../ws/ws.module'; 4 | import { BosResolver } from './bos.resolver'; 5 | 6 | @Module({ 7 | imports: [WsModule, AccountsModule], 8 | providers: [BosResolver], 9 | }) 10 | export class BosModule {} 11 | -------------------------------------------------------------------------------- /src/server/modules/blockstream/blockstream.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BlockstreamService } from './blockstream.service'; 3 | import { FetchModule } from '../fetch/fetch.module'; 4 | 5 | @Module({ 6 | imports: [FetchModule], 7 | providers: [BlockstreamService], 8 | exports: [BlockstreamService], 9 | }) 10 | export class BlockstreamModule {} 11 | -------------------------------------------------------------------------------- /src/server/modules/dataloader/dataloader.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { DataloaderService } from './dataloader.service'; 3 | import { AmbossModule } from '../api/amboss/amboss.module'; 4 | 5 | @Module({ 6 | imports: [AmbossModule], 7 | providers: [DataloaderService], 8 | exports: [DataloaderService], 9 | }) 10 | export class DataloaderModule {} 11 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getChannelReport.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_LIQUID_REPORT = gql` 4 | query GetLiquidReport { 5 | getChannelReport { 6 | local 7 | remote 8 | maxIn 9 | maxOut 10 | commit 11 | totalPendingHtlc 12 | outgoingPendingHtlc 13 | incomingPendingHtlc 14 | } 15 | } 16 | `; 17 | -------------------------------------------------------------------------------- /src/server/modules/ws/ws.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthenticationModule } from '../auth/auth.module'; 3 | import { WsGateway } from './ws.gateway'; 4 | import { WsService } from './ws.service'; 5 | 6 | @Module({ 7 | imports: [AuthenticationModule], 8 | providers: [WsGateway, WsService], 9 | exports: [WsService], 10 | }) 11 | export class WsModule {} 12 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getChainTransactions.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_CHAIN_TRANSACTIONS = gql` 4 | query GetChainTransactions { 5 | getChainTransactions { 6 | block_id 7 | confirmation_count 8 | confirmation_height 9 | created_at 10 | fee 11 | id 12 | output_addresses 13 | tokens 14 | } 15 | } 16 | `; 17 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getLightningAddressInfo.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_LIGHTNING_ADDRESS_INFO = gql` 4 | query GetLightningAddressInfo($address: String!) { 5 | getLightningAddressInfo(address: $address) { 6 | callback 7 | maxSendable 8 | minSendable 9 | metadata 10 | commentAllowed 11 | tag 12 | } 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /src/client/src/hooks/UseAmbossUser.tsx: -------------------------------------------------------------------------------- 1 | import { useGetAmbossUserQuery } from '../../src/graphql/queries/__generated__/getAmbossUser.generated'; 2 | 3 | export const useAmbossUser = () => { 4 | const { data, loading } = useGetAmbossUserQuery(); 5 | 6 | if (loading || !data?.getAmbossUser) { 7 | return { user: null, loading }; 8 | } 9 | 10 | return { user: data.getAmbossUser, loading }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/server/modules/node/node.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AccountsModule } from '../accounts/accounts.module'; 3 | import { LndModule } from './lnd/lnd.module'; 4 | import { NodeService } from './node.service'; 5 | 6 | @Module({ 7 | imports: [LndModule, AccountsModule], 8 | providers: [NodeService], 9 | exports: [NodeService], 10 | }) 11 | export class NodeModule {} 12 | -------------------------------------------------------------------------------- /src/client/src/components/emoji/Emoji.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface EmojiProps { 4 | symbol: string; 5 | label?: string; 6 | } 7 | 8 | export const Emoji = ({ label, symbol }: EmojiProps) => ( 9 | 15 | {symbol} 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/addPeer.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const ADD_PEER = gql` 4 | mutation AddPeer( 5 | $url: String 6 | $publicKey: String 7 | $socket: String 8 | $isTemporary: Boolean 9 | ) { 10 | addPeer( 11 | url: $url 12 | publicKey: $publicKey 13 | socket: $socket 14 | isTemporary: $isTemporary 15 | ) 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getBoltzSwapStatus.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_BOLTZ_SWAP_STATUS = gql` 4 | query GetBoltzSwapStatus($ids: [String!]!) { 5 | getBoltzSwapStatus(ids: $ids) { 6 | id 7 | boltz { 8 | status 9 | transaction { 10 | id 11 | hex 12 | eta 13 | } 14 | } 15 | } 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /src/server/utils/async.ts: -------------------------------------------------------------------------------- 1 | export const to = async (promise: Promise) => { 2 | return promise 3 | .then(data => data) 4 | .catch(err => { 5 | throw new Error(err); 6 | }); 7 | }; 8 | 9 | export const toWithError = async (promise: Promise) => { 10 | return promise 11 | .then(data => [data, undefined] as [T, undefined]) 12 | .catch(err => [undefined, err] as [undefined, Error]); 13 | }; 14 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getAmbossUser.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_AMBOSS_USER = gql` 4 | query GetAmbossUser { 5 | getAmbossUser { 6 | subscription { 7 | end_date 8 | subscribed 9 | upgradable 10 | } 11 | backups { 12 | last_update 13 | last_update_size 14 | total_size_saved 15 | } 16 | } 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getNetworkInfo.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_NETWORK_INFO = gql` 4 | query GetNetworkInfo { 5 | getNetworkInfo { 6 | averageChannelSize 7 | channelCount 8 | maxChannelSize 9 | medianChannelSize 10 | minChannelSize 11 | nodeCount 12 | notRecentlyUpdatedPolicyCount 13 | totalCapacity 14 | } 15 | } 16 | `; 17 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getNodeBalances.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_NODE_BALANCES = gql` 4 | query GetNodeBalances { 5 | getNodeBalances { 6 | onchain { 7 | confirmed 8 | pending 9 | closing 10 | } 11 | lightning { 12 | confirmed 13 | active 14 | commit 15 | pending 16 | } 17 | } 18 | } 19 | `; 20 | -------------------------------------------------------------------------------- /src/client/src/hooks/UseAccount.tsx: -------------------------------------------------------------------------------- 1 | import { ServerAccount } from '../../src/graphql/types'; 2 | import { useGetAccountQuery } from '../../src/graphql/queries/__generated__/getAccount.generated'; 3 | 4 | export const useAccount = (): ServerAccount | null => { 5 | const { data, loading } = useGetAccountQuery({ ssr: false }); 6 | 7 | if (loading || !data?.getAccount) { 8 | return null; 9 | } 10 | 11 | return data.getAccount; 12 | }; 13 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/createThunderPoints.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const CREATE_THUNDER_POINTS = gql` 4 | mutation CreateThunderPoints( 5 | $id: String! 6 | $alias: String! 7 | $uris: [String!]! 8 | $public_key: String! 9 | ) { 10 | createThunderPoints( 11 | id: $id 12 | alias: $alias 13 | uris: $uris 14 | public_key: $public_key 15 | ) 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getMessages.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_MESSAGES = gql` 4 | query GetMessages($initialize: Boolean) { 5 | getMessages(initialize: $initialize) { 6 | token 7 | messages { 8 | date 9 | contentType 10 | alias 11 | message 12 | id 13 | sender 14 | verified 15 | tokens 16 | } 17 | } 18 | } 19 | `; 20 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getVolumeHealth.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_VOLUME_HEALTH = gql` 4 | query GetVolumeHealth { 5 | getVolumeHealth { 6 | score 7 | channels { 8 | id 9 | score 10 | volumeNormalized 11 | averageVolumeNormalized 12 | partner { 13 | node { 14 | alias 15 | } 16 | } 17 | } 18 | } 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /src/client/src/views/dashboard/widgets/lightning/channels.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { ChannelTable } from '../../../channels/channels/ChannelTable'; 3 | 4 | const S = { 5 | wrapper: styled.div` 6 | height: 100%; 7 | width: 100%; 8 | overflow: auto; 9 | `, 10 | }; 11 | 12 | export const ChannelListWidget = () => { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getNodeSocialInfo.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_NODE_SOCIAL_INFO = gql` 4 | query GetNodeSocialInfo($pubkey: String!) { 5 | getNodeSocialInfo(pubkey: $pubkey) { 6 | socials { 7 | info { 8 | private 9 | telegram 10 | twitter 11 | twitter_verified 12 | website 13 | email 14 | } 15 | } 16 | } 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /src/server/modules/api/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AccountsModule } from '../../accounts/accounts.module'; 3 | import { FilesModule } from '../../files/files.module'; 4 | import { NodeModule } from '../../node/node.module'; 5 | import { AuthResolver } from './auth.resolver'; 6 | 7 | @Module({ 8 | imports: [AccountsModule, FilesModule, NodeModule], 9 | providers: [AuthResolver], 10 | }) 11 | export class AuthModule {} 12 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getWalletInfo.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_WALLET_INFO = gql` 4 | query GetWalletInfo { 5 | getWalletInfo { 6 | build_tags 7 | commit_hash 8 | is_autopilotrpc_enabled 9 | is_chainrpc_enabled 10 | is_invoicesrpc_enabled 11 | is_signrpc_enabled 12 | is_walletrpc_enabled 13 | is_watchtowerrpc_enabled 14 | is_wtclientrpc_enabled 15 | } 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /src/server/modules/api/channels/channels.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NodeModule } from '../../node/node.module'; 3 | import { ChannelResolver } from './channel.resolver'; 4 | import { ChannelsResolver } from './channels.resolver'; 5 | import { FetchModule } from '../../fetch/fetch.module'; 6 | 7 | @Module({ 8 | imports: [NodeModule, FetchModule], 9 | providers: [ChannelsResolver, ChannelResolver], 10 | }) 11 | export class ChannelsModule {} 12 | -------------------------------------------------------------------------------- /src/server/modules/ws/ws.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Server } from 'socket.io'; 3 | 4 | @Injectable() 5 | export class WsService { 6 | private socket: Server = null; 7 | 8 | init(socket: Server) { 9 | this.socket = socket; 10 | } 11 | 12 | emit(account: string, event: string, payload: any) { 13 | if (!this.socket || !account || !event || !payload) return; 14 | this.socket.in(account).emit(event, payload); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/server/modules/api/lnurl/lnurl.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FetchModule } from '../../fetch/fetch.module'; 3 | import { NodeModule } from '../../node/node.module'; 4 | import { LnUrlResolver } from './lnurl.resolver'; 5 | import { LnUrlService } from './lnurl.service'; 6 | 7 | @Module({ 8 | imports: [NodeModule, FetchModule], 9 | providers: [LnUrlResolver, LnUrlService], 10 | exports: [LnUrlService], 11 | }) 12 | export class LnUrlModule {} 13 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - master 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "master" 14 | - uses: release-drafter/release-drafter@v5 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/sendMessage.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const SEND_MESSAGE = gql` 4 | mutation SendMessage( 5 | $publicKey: String! 6 | $message: String! 7 | $messageType: String 8 | $tokens: Float 9 | $maxFee: Float 10 | ) { 11 | sendMessage( 12 | publicKey: $publicKey 13 | message: $message 14 | messageType: $messageType 15 | tokens: $tokens 16 | maxFee: $maxFee 17 | ) 18 | } 19 | `; 20 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getAccountingReport.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_ACCOUNTING_REPORT = gql` 4 | query GetAccountingReport( 5 | $category: String 6 | $currency: String 7 | $fiat: String 8 | $month: String 9 | $year: String 10 | ) { 11 | getAccountingReport( 12 | category: $category 13 | currency: $currency 14 | fiat: $fiat 15 | month: $month 16 | year: $year 17 | ) 18 | } 19 | `; 20 | -------------------------------------------------------------------------------- /src/client/src/hooks/UseBaseConnect.tsx: -------------------------------------------------------------------------------- 1 | import { useGetBaseCanConnectQuery } from '../../src/graphql/queries/__generated__/getBaseCanConnect.generated'; 2 | 3 | export const useBaseConnect = () => { 4 | const { loading, error, data } = useGetBaseCanConnectQuery({ 5 | ssr: false, 6 | fetchPolicy: 'cache-first', 7 | }); 8 | 9 | if (loading || !data?.getBaseCanConnect || error) 10 | return { connected: false, loading }; 11 | 12 | return { connected: true, loading }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/server/modules/api/edge/edge.types.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | 3 | @ObjectType() 4 | export class ChannelReport { 5 | @Field() 6 | local: number; 7 | @Field() 8 | remote: number; 9 | @Field() 10 | maxIn: number; 11 | @Field() 12 | maxOut: number; 13 | @Field() 14 | commit: number; 15 | @Field() 16 | totalPendingHtlc: number; 17 | @Field() 18 | outgoingPendingHtlc: number; 19 | @Field() 20 | incomingPendingHtlc: number; 21 | } 22 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getTimeHealth.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_TIME_HEALTH = gql` 4 | query GetTimeHealth { 5 | getTimeHealth { 6 | score 7 | channels { 8 | id 9 | score 10 | significant 11 | monitoredTime 12 | monitoredUptime 13 | monitoredDowntime 14 | partner { 15 | node { 16 | alias 17 | } 18 | } 19 | } 20 | } 21 | } 22 | `; 23 | -------------------------------------------------------------------------------- /src/server/utils/request.ts: -------------------------------------------------------------------------------- 1 | import { Ipware } from '@fullerstack/nax-ipware'; 2 | const ipware = new Ipware(); 3 | 4 | export const getIp = (req: any) => { 5 | const ip_info = ipware.getClientIP(req); 6 | return ip_info.ip; 7 | }; 8 | 9 | export const getAuthToken = (req: Request) => { 10 | const authHeader = req.headers['authorization'] || ''; 11 | if (authHeader.startsWith('Bearer ')) { 12 | return authHeader.substring(7, authHeader.length); 13 | } else { 14 | return ''; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "esModuleInterop": true 16 | }, 17 | "include": ["src/server"] 18 | } 19 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/closeChannel.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const CLOSE_CHANNEL = gql` 4 | mutation CloseChannel( 5 | $id: String! 6 | $forceClose: Boolean 7 | $target: Float 8 | $tokens: Float 9 | ) { 10 | closeChannel( 11 | id: $id 12 | forceClose: $forceClose 13 | targetConfirmations: $target 14 | tokensPerVByte: $tokens 15 | ) { 16 | transactionId 17 | transactionOutputIndex 18 | } 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getPeers.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_PEERS = gql` 4 | query GetPeers { 5 | getPeers { 6 | bytes_received 7 | bytes_sent 8 | is_inbound 9 | is_sync_peer 10 | ping_time 11 | public_key 12 | socket 13 | tokens_received 14 | tokens_sent 15 | partner_node_info { 16 | node { 17 | alias 18 | public_key 19 | } 20 | } 21 | } 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /src/client/src/components/notification/Beta.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { chartColors, mediaWidths } from '../../../src/styles/Themes'; 3 | 4 | export const BetaNotification = styled.div` 5 | width: 100%; 6 | text-align: center; 7 | background-color: ${chartColors.orange}; 8 | border-radius: 4px; 9 | color: black; 10 | margin-bottom: 16px; 11 | padding: 4px 0; 12 | 13 | @media (${mediaWidths.mobile}) { 14 | margin-top: 8px; 15 | margin-bottom: 8px; 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getNodeInfo.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_NODE_INFO = gql` 4 | query GetNodeInfo { 5 | getNodeInfo { 6 | alias 7 | public_key 8 | uris 9 | chains 10 | color 11 | is_synced_to_chain 12 | current_block_height 13 | latest_block_height 14 | peers_count 15 | version 16 | active_channels_count 17 | closed_channels_count 18 | pending_channels_count 19 | } 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /src/client/src/hooks/UseInterval.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export const useInterval = (callback: () => void, delay: number) => { 4 | const savedCallback = useRef(callback); 5 | 6 | useEffect(() => { 7 | savedCallback.current = callback; 8 | }, [callback]); 9 | 10 | useEffect(() => { 11 | const tick = () => { 12 | savedCallback.current(); 13 | }; 14 | const id = setInterval(tick, delay); 15 | return () => clearInterval(id); 16 | }, [delay]); 17 | }; 18 | -------------------------------------------------------------------------------- /src/server/modules/api/lnmarkets/lnmarkets.types.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | 3 | @ObjectType() 4 | export class LnMarketsUserInfo { 5 | @Field({ nullable: true }) 6 | uid: string; 7 | @Field({ nullable: true }) 8 | balance: string; 9 | @Field({ nullable: true }) 10 | account_type: string; 11 | @Field({ nullable: true }) 12 | username: string; 13 | @Field({ nullable: true }) 14 | linkingpublickey: string; 15 | @Field({ nullable: true }) 16 | last_ip: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/createInvoice.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const CREATE_INVOICE = gql` 4 | mutation CreateInvoice( 5 | $amount: Float! 6 | $description: String 7 | $secondsUntil: Float 8 | $includePrivate: Boolean 9 | ) { 10 | createInvoice( 11 | amount: $amount 12 | description: $description 13 | secondsUntil: $secondsUntil 14 | includePrivate: $includePrivate 15 | ) { 16 | request 17 | id 18 | } 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /src/client/pages/swap.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GridWrapper } from '../src/components/gridWrapper/GridWrapper'; 3 | import { NextPageContext } from 'next'; 4 | import { getProps } from '../src/utils/ssr'; 5 | import { SwapView } from '../src/views/swap'; 6 | 7 | const Wrapped = () => ( 8 | 9 | 10 | 11 | ); 12 | 13 | export default Wrapped; 14 | 15 | export async function getServerSideProps(context: NextPageContext) { 16 | return await getProps(context); 17 | } 18 | -------------------------------------------------------------------------------- /src/server/modules/api/userConfig/userConfig.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FilesModule } from '../../files/files.module'; 3 | import { 4 | UserConfigResolver, 5 | UserConfigStateResolver, 6 | } from './userConfig.resolver'; 7 | import { UserConfigService } from './userConfig.service'; 8 | 9 | @Module({ 10 | imports: [FilesModule], 11 | providers: [UserConfigService, UserConfigResolver, UserConfigStateResolver], 12 | exports: [UserConfigService], 13 | }) 14 | export class UserConfigModule {} 15 | -------------------------------------------------------------------------------- /src/server/modules/api/network/network.types.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | 3 | @ObjectType() 4 | export class NetworkInfo { 5 | @Field() 6 | averageChannelSize: number; 7 | @Field() 8 | channelCount: number; 9 | @Field() 10 | maxChannelSize: number; 11 | @Field() 12 | medianChannelSize: number; 13 | @Field() 14 | minChannelSize: number; 15 | @Field() 16 | nodeCount: number; 17 | @Field() 18 | notRecentlyUpdatedPolicyCount: number; 19 | @Field() 20 | totalCapacity: number; 21 | } 22 | -------------------------------------------------------------------------------- /src/client/src/hooks/UseNodeDetails.tsx: -------------------------------------------------------------------------------- 1 | import { useGetNodeQuery } from '../graphql/queries/__generated__/getNode.generated'; 2 | 3 | export const useNodeDetails = (pubkey: string) => { 4 | const { data, loading, error } = useGetNodeQuery({ 5 | variables: { publicKey: pubkey }, 6 | skip: !pubkey, 7 | }); 8 | 9 | if (loading) { 10 | return { alias: '' }; 11 | } 12 | 13 | if (!data?.getNode.node?.alias || error) { 14 | return { alias: 'Unknown' }; 15 | } 16 | 17 | return { alias: data.getNode.node.alias }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/server/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER)); 8 | app.setGlobalPrefix(process.env.BASE_PATH || ''); 9 | 10 | await app.listen(process.env.PORT || 3000, process.env.HOST); 11 | console.log(`Application is running on: ${await app.getUrl()}`); 12 | } 13 | bootstrap(); 14 | -------------------------------------------------------------------------------- /src/server/modules/api/lnmarkets/lnmarkets.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FetchModule } from '../../fetch/fetch.module'; 3 | import { NodeModule } from '../../node/node.module'; 4 | import { LnUrlModule } from '../lnurl/lnurl.module'; 5 | import { LnMarketsResolver } from './lnmarkets.resolver'; 6 | import { LnMarketsService } from './lnmarkets.service'; 7 | 8 | @Module({ 9 | imports: [LnUrlModule, NodeModule, FetchModule], 10 | providers: [LnMarketsService, LnMarketsResolver], 11 | }) 12 | export class LnMarketsModule {} 13 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/sendToAddress.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const PAY_ADDRESS = gql` 4 | mutation PayAddress( 5 | $address: String! 6 | $tokens: Float 7 | $fee: Float 8 | $target: Float 9 | $sendAll: Boolean 10 | ) { 11 | sendToAddress( 12 | address: $address 13 | tokens: $tokens 14 | fee: $fee 15 | target: $target 16 | sendAll: $sendAll 17 | ) { 18 | confirmationCount 19 | id 20 | isConfirmed 21 | isOutgoing 22 | tokens 23 | } 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /src/client/pages/sso.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useCheckAuthToken } from '../src/hooks/UseCheckAuthToken'; 3 | import { NextPageContext } from 'next'; 4 | import { getProps } from '../src/utils/ssr'; 5 | import { LoadingCard } from '../src/components/loading/LoadingCard'; 6 | 7 | const Wrapped = () => { 8 | useCheckAuthToken(); 9 | 10 | return ; 11 | }; 12 | 13 | export default Wrapped; 14 | 15 | export async function getServerSideProps(context: NextPageContext) { 16 | return await getProps(context, true); 17 | } 18 | -------------------------------------------------------------------------------- /src/client/src/views/swap/SwapExpire.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { formatDistanceToNowStrict } from 'date-fns'; 3 | 4 | export const useSwapExpire = (date?: string) => { 5 | const [, setCount] = useState(0); 6 | 7 | useEffect(() => { 8 | const myInterval = setInterval(() => { 9 | setCount(p => p + 1); 10 | }, 1000); 11 | return () => { 12 | clearInterval(myInterval); 13 | }; 14 | }); 15 | 16 | if (!date) return ''; 17 | return `(Expires in ${formatDistanceToNowStrict(new Date(date), { 18 | unit: 'second', 19 | })})`; 20 | }; 21 | -------------------------------------------------------------------------------- /src/server/modules/api/wallet/wallet.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Query, Resolver } from '@nestjs/graphql'; 2 | import { NodeService } from '../../node/node.service'; 3 | import { CurrentUser } from '../../security/security.decorators'; 4 | import { UserId } from '../../security/security.types'; 5 | import { Wallet } from './wallet.types'; 6 | 7 | @Resolver() 8 | export class WalletResolver { 9 | constructor(private nodeService: NodeService) {} 10 | 11 | @Query(() => Wallet) 12 | async getWalletInfo(@CurrentUser() { id }: UserId) { 13 | return this.nodeService.getWalletVersion(id); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/server/modules/api/base/base.types.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | 3 | @ObjectType() 4 | export class BaseNode { 5 | @Field({ nullable: true }) 6 | _id: string; 7 | @Field({ nullable: true }) 8 | name: string; 9 | @Field() 10 | public_key: string; 11 | @Field() 12 | socket: string; 13 | } 14 | 15 | @ObjectType() 16 | export class BasePoints { 17 | @Field() 18 | alias: string; 19 | @Field() 20 | amount: number; 21 | } 22 | 23 | @ObjectType() 24 | export class BaseInvoice { 25 | @Field() 26 | id: string; 27 | @Field() 28 | request: string; 29 | } 30 | -------------------------------------------------------------------------------- /src/client/src/utils/version.ts: -------------------------------------------------------------------------------- 1 | export const getVersion = ( 2 | version: string 3 | ): { 4 | mayor: number; 5 | minor: number; 6 | revision: number; 7 | version: string; 8 | versionWithPatch: string; 9 | } => { 10 | const versionNumber = version.split(' '); 11 | const onlyVersion = versionNumber[0].split('-'); 12 | const numbers = onlyVersion[0].split('.'); 13 | 14 | return { 15 | mayor: Number(numbers[0]) || 0, 16 | minor: Number(numbers[1]) || 0, 17 | revision: Number(numbers[2]) || 0, 18 | version: onlyVersion[0] || '', 19 | versionWithPatch: versionNumber[0] || '', 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/client/src/hooks/UseNodeBalances.tsx: -------------------------------------------------------------------------------- 1 | import { useGetNodeBalancesQuery } from '../../src/graphql/queries/__generated__/getNodeBalances.generated'; 2 | 3 | const initialState = { 4 | onchain: { confirmed: '0', pending: '0', closing: '0' }, 5 | lightning: { 6 | confirmed: '0', 7 | active: '0', 8 | commit: '0', 9 | pending: '0', 10 | }, 11 | }; 12 | 13 | export const useNodeBalances = () => { 14 | const { data, loading, error } = useGetNodeBalancesQuery(); 15 | 16 | if (!data?.getNodeBalances || loading || error) { 17 | return initialState; 18 | } 19 | 20 | return data.getNodeBalances; 21 | }; 22 | -------------------------------------------------------------------------------- /src/server/modules/api/wallet/wallet.types.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | 3 | @ObjectType() 4 | export class Wallet { 5 | @Field(() => [String]) 6 | build_tags: string[]; 7 | @Field() 8 | commit_hash: string; 9 | @Field() 10 | is_autopilotrpc_enabled: boolean; 11 | @Field() 12 | is_chainrpc_enabled: boolean; 13 | @Field() 14 | is_invoicesrpc_enabled: boolean; 15 | @Field() 16 | is_signrpc_enabled: boolean; 17 | @Field() 18 | is_walletrpc_enabled: boolean; 19 | @Field() 20 | is_watchtowerrpc_enabled: boolean; 21 | @Field() 22 | is_wtclientrpc_enabled: boolean; 23 | } 24 | -------------------------------------------------------------------------------- /src/server/modules/view/view.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Res, Req } from '@nestjs/common'; 2 | import { SkipThrottle } from '@nestjs/throttler'; 3 | import { Request, Response } from 'express'; 4 | import { Public } from '../security/security.decorators'; 5 | import { ViewService } from './view.service'; 6 | 7 | @Public() 8 | @SkipThrottle() 9 | @Controller('/') 10 | export class ViewController { 11 | constructor(private viewService: ViewService) {} 12 | 13 | @Get(['/', '*']) 14 | public async showHome(@Req() req: Request, @Res() res: Response) { 15 | await this.viewService.handler(req, res); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/client/src/hooks/UseMutationWithReset.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const useMutationResultWithReset = ( 4 | data: T | undefined | null 5 | ): [T | undefined | null, () => void] => { 6 | const current = React.useRef(data); 7 | const latest = React.useRef(data); 8 | const [, setState] = React.useState(0); 9 | const clearCurrentData = () => { 10 | current.current = undefined; 11 | setState(state => state + 1); 12 | }; 13 | 14 | if (data !== latest.current) { 15 | current.current = data; 16 | latest.current = data; 17 | } 18 | 19 | return [current.current, clearCurrentData]; 20 | }; 21 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/claimBoltzTransaction.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const CLAIM_BOLTZ_TRANSACTION = gql` 4 | mutation ClaimBoltzTransaction( 5 | $id: String! 6 | $redeem: String! 7 | $lockupAddress: String! 8 | $preimage: String! 9 | $privateKey: String! 10 | $destination: String! 11 | $fee: Float! 12 | ) { 13 | claimBoltzTransaction( 14 | id: $id 15 | redeem: $redeem 16 | lockupAddress: $lockupAddress 17 | preimage: $preimage 18 | privateKey: $privateKey 19 | destination: $destination 20 | fee: $fee 21 | ) 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /src/server/modules/api/peer/peer.types.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | import { Node } from '../node/node.types'; 3 | 4 | @ObjectType() 5 | export class Peer { 6 | @Field() 7 | bytes_received: number; 8 | @Field() 9 | bytes_sent: number; 10 | @Field() 11 | is_inbound: boolean; 12 | @Field({ nullable: true }) 13 | is_sync_peer: boolean; 14 | @Field() 15 | ping_time: number; 16 | @Field() 17 | public_key: string; 18 | @Field() 19 | socket: string; 20 | @Field() 21 | tokens_received: number; 22 | @Field() 23 | tokens_sent: number; 24 | @Field(() => Node) 25 | partner_node_info: Node; 26 | } 27 | -------------------------------------------------------------------------------- /src/server/modules/sub/sub.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AccountsModule } from '../accounts/accounts.module'; 3 | import { AmbossModule } from '../api/amboss/amboss.module'; 4 | import { UserConfigModule } from '../api/userConfig/userConfig.module'; 5 | import { NodeModule } from '../node/node.module'; 6 | import { WsModule } from '../ws/ws.module'; 7 | import { SubService } from './sub.service'; 8 | 9 | @Module({ 10 | imports: [ 11 | UserConfigModule, 12 | NodeModule, 13 | AccountsModule, 14 | WsModule, 15 | AmbossModule, 16 | ], 17 | providers: [SubService], 18 | }) 19 | export class SubModule {} 20 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | }; 6 | outputs = { self, nixpkgs, flake-utils, fedimint }: 7 | flake-utils.lib.eachDefaultSystem (system: 8 | let pkgs = import nixpkgs { inherit system; }; 9 | in { 10 | devShells = fmLib.devShells // { 11 | default = fmLib.devShells.default.overrideAttrs (prev: { 12 | nativeBuildInputs = [ pkgs.nodejs ] ++ prev.nativeBuildInputs; 13 | shellHook = '' 14 | npm install 15 | ''; 16 | }); 17 | }; 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/decodeRequest.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const DECODE_REQUEST = gql` 4 | query DecodeRequest($request: String!) { 5 | decodeRequest(request: $request) { 6 | chain_address 7 | cltv_delta 8 | description 9 | description_hash 10 | destination 11 | destination_node { 12 | node { 13 | alias 14 | } 15 | } 16 | expires_at 17 | id 18 | routes { 19 | base_fee_mtokens 20 | channel 21 | cltv_delta 22 | fee_rate 23 | public_key 24 | } 25 | tokens 26 | } 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /src/client/src/views/homepage/Top.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { inverseTextColor } from '../../styles/Themes'; 3 | import { Section } from '../../components/section/Section'; 4 | import { Headline, HomeTitle, HomeText, FullWidth } from './HomePage.styled'; 5 | 6 | export const TopSection = () => ( 7 |
8 | 9 | Control the Lightning 10 | 11 | 12 | Monitor and manage your node from any browser and any device. 13 | 14 | 15 | 16 |
17 | ); 18 | -------------------------------------------------------------------------------- /src/server/modules/api/amboss/amboss.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AccountsModule } from '../../accounts/accounts.module'; 3 | import { FetchModule } from '../../fetch/fetch.module'; 4 | import { NodeModule } from '../../node/node.module'; 5 | import { UserConfigModule } from '../userConfig/userConfig.module'; 6 | import { AmbossResolver } from './amboss.resolver'; 7 | import { AmbossService } from './amboss.service'; 8 | 9 | @Module({ 10 | imports: [UserConfigModule, AccountsModule, NodeModule, FetchModule], 11 | providers: [AmbossResolver, AmbossService], 12 | exports: [AmbossService], 13 | }) 14 | export class AmbossModule {} 15 | -------------------------------------------------------------------------------- /src/server/modules/api/userConfig/userConfig.types.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | 3 | export enum ConfigFields { 4 | BACKUPS = 'BACKUPS', 5 | HEALTHCHECKS = 'HEALTHCHECKS', 6 | ONCHAIN_PUSH = 'ONCHAIN_PUSH', 7 | CHANNELS_PUSH = 'CHANNELS_PUSH', 8 | PRIVATE_CHANNELS_PUSH = 'PRIVATE_CHANNELS_PUSH', 9 | } 10 | 11 | @ObjectType() 12 | export class ConfigState { 13 | @Field() 14 | backup_state: boolean; 15 | @Field() 16 | healthcheck_ping_state: boolean; 17 | @Field() 18 | onchain_push_enabled: boolean; 19 | @Field() 20 | channels_push_enabled: boolean; 21 | @Field() 22 | private_channels_push_enabled: boolean; 23 | } 24 | -------------------------------------------------------------------------------- /src/server/modules/api/channels/channels.helpers.ts: -------------------------------------------------------------------------------- 1 | export const getChannelAge = (id: string, currentHeight: number): number => { 2 | const info = getChannelIdInfo(id); 3 | if (!info) return 0; 4 | return currentHeight - info.blockHeight; 5 | }; 6 | 7 | export const getChannelIdInfo = ( 8 | id: string 9 | ): { blockHeight: number; transaction: number; output: number } | null => { 10 | const format = /^\d*x\d*x\d*$/; 11 | 12 | if (!format.test(id)) return null; 13 | 14 | const separate = id.split('x'); 15 | 16 | return { 17 | blockHeight: Number(separate[0]), 18 | transaction: Number(separate[1]), 19 | output: Number(separate[2]), 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/lnMarkets.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const LN_MARKETS_LOGIN = gql` 4 | mutation LnMarketsLogin { 5 | lnMarketsLogin { 6 | status 7 | message 8 | } 9 | } 10 | `; 11 | 12 | export const LN_MARKETS_WITHDRAW = gql` 13 | mutation LnMarketsWithdraw($amount: Float!) { 14 | lnMarketsWithdraw(amount: $amount) 15 | } 16 | `; 17 | 18 | export const LN_MARKETS_DEPOSIT = gql` 19 | mutation LnMarketsDeposit($amount: Float!) { 20 | lnMarketsDeposit(amount: $amount) 21 | } 22 | `; 23 | 24 | export const LN_MARKETS_LOGOUT = gql` 25 | mutation LnMarketsLogout { 26 | lnMarketsLogout 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /src/client/src/views/home/reports/forwardReport/helpers.ts: -------------------------------------------------------------------------------- 1 | import { GetClosedChannelsQuery } from '../../../../graphql/queries/__generated__/getClosedChannels.generated'; 2 | 3 | export const getAliasFromClosedChannels = ( 4 | channelId: string, 5 | channels: GetClosedChannelsQuery['getClosedChannels'] 6 | ): { alias: string; closed: boolean } => { 7 | if (!channels) return { alias: 'Unknown', closed: false }; 8 | 9 | const channel = channels.find(c => c?.id === channelId); 10 | 11 | if (channel?.partner_node_info.node?.alias) { 12 | return { alias: channel.partner_node_info.node.alias, closed: true }; 13 | } 14 | 15 | return { alias: 'Unknown', closed: false }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/server/modules/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { AccountsModule } from '../accounts/accounts.module'; 5 | import { AuthenticationService } from './auth.service'; 6 | 7 | @Module({ 8 | imports: [ 9 | AccountsModule, 10 | JwtModule.registerAsync({ 11 | inject: [ConfigService], 12 | useFactory: (config: ConfigService) => ({ 13 | secret: config.get('jwtSecret'), 14 | }), 15 | }), 16 | ], 17 | providers: [AuthenticationService], 18 | exports: [AuthenticationService], 19 | }) 20 | export class AuthenticationModule {} 21 | -------------------------------------------------------------------------------- /src/client/src/views/lnmarkets/GoToLnMarkets.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { ColorButton } from '../../components/buttons/colorButton/ColorButton'; 3 | import { useGetLnMarketsUrlLazyQuery } from '../../graphql/queries/__generated__/getLnMarketsUrl.generated'; 4 | 5 | export const GoToLnMarkets = () => { 6 | const [getUrl, { data, loading }] = useGetLnMarketsUrlLazyQuery(); 7 | 8 | useEffect(() => { 9 | if (loading || !data?.getLnMarketsUrl) return; 10 | window.open(data.getLnMarketsUrl, '_blank'); 11 | }, [loading, data]); 12 | 13 | return ( 14 | 15 | Go To LnMarkets 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/client/src/views/tools/messages/Messages.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { 4 | CardWithTitle, 5 | SubTitle, 6 | Card, 7 | } from '../../../components/generic/Styled'; 8 | import { SignMessageCard } from './SignMessage'; 9 | import { VerifyMessage } from './VerifyMessage'; 10 | 11 | export const NoWrap = styled.div` 12 | margin-right: 16px; 13 | white-space: nowrap; 14 | `; 15 | 16 | export const MessagesView = () => { 17 | return ( 18 | 19 | Messages 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true 19 | }, 20 | "exclude": ["node_modules", ".next"], 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.js"] 22 | } 23 | -------------------------------------------------------------------------------- /src/server/modules/api/chat/chat.types.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | 3 | @ObjectType() 4 | class Message { 5 | @Field() 6 | date: string; 7 | @Field() 8 | id: string; 9 | @Field() 10 | verified: boolean; 11 | @Field({ nullable: true }) 12 | contentType: string; 13 | @Field({ nullable: true }) 14 | sender: string; 15 | @Field({ nullable: true }) 16 | alias: string; 17 | @Field({ nullable: true }) 18 | message: string; 19 | @Field({ nullable: true }) 20 | tokens: number; 21 | } 22 | 23 | @ObjectType() 24 | export class GetMessages { 25 | @Field({ nullable: true }) 26 | token: string; 27 | @Field(() => [Message]) 28 | messages: Message[]; 29 | } 30 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/updateFees.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const UPDATE_FEES = gql` 4 | mutation UpdateFees( 5 | $transaction_id: String 6 | $transaction_vout: Float 7 | $base_fee_tokens: Float 8 | $fee_rate: Float 9 | $cltv_delta: Float 10 | $max_htlc_mtokens: String 11 | $min_htlc_mtokens: String 12 | ) { 13 | updateFees( 14 | transaction_id: $transaction_id 15 | transaction_vout: $transaction_vout 16 | base_fee_tokens: $base_fee_tokens 17 | fee_rate: $fee_rate 18 | cltv_delta: $cltv_delta 19 | max_htlc_mtokens: $max_htlc_mtokens 20 | min_htlc_mtokens: $min_htlc_mtokens 21 | ) 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /src/client/src/views/home/account/createInvoice/InvoiceStatus.tsx: -------------------------------------------------------------------------------- 1 | import { useGetInvoiceStatusChangeQuery } from '../../../../graphql/queries/__generated__/getInvoiceStatusChange.generated'; 2 | import { useEffect } from 'react'; 3 | type InvoiceProps = { 4 | id: string; 5 | callback: (state: string) => void; 6 | }; 7 | 8 | export const InvoiceStatus: React.FC = ({ id, callback }) => { 9 | const { data, loading } = useGetInvoiceStatusChangeQuery({ 10 | variables: { id }, 11 | }); 12 | 13 | useEffect(() => { 14 | if (!loading && data?.getInvoiceStatusChange) { 15 | callback(data.getInvoiceStatusChange); 16 | } 17 | }, [loading, data, callback]); 18 | 19 | return null; 20 | }; 21 | -------------------------------------------------------------------------------- /src/server/modules/api/node/node.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NodeModule as NodeServiceModule } from '../../node/node.module'; 3 | import { 4 | BalancesResolver, 5 | LightningBalanceResolver, 6 | NodeFieldResolver, 7 | NodeInfoResolver, 8 | NodeResolver, 9 | OnChainBalanceResolver, 10 | } from './node.resolver'; 11 | import { FetchModule } from '../../fetch/fetch.module'; 12 | 13 | @Module({ 14 | imports: [NodeServiceModule, FetchModule], 15 | providers: [ 16 | NodeResolver, 17 | BalancesResolver, 18 | OnChainBalanceResolver, 19 | LightningBalanceResolver, 20 | NodeFieldResolver, 21 | NodeInfoResolver, 22 | ], 23 | }) 24 | export class NodeModule {} 25 | -------------------------------------------------------------------------------- /src/client/src/components/bitcoinInfo/BitcoinFees.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useGetBitcoinFeesQuery } from '../../../src/graphql/queries/__generated__/getBitcoinFees.generated'; 3 | import { useConfigState } from '../../context/ConfigContext'; 4 | 5 | export const BitcoinFees: React.FC = () => { 6 | const { fetchFees } = useConfigState(); 7 | 8 | const { stopPolling, error } = useGetBitcoinFeesQuery({ 9 | ssr: false, 10 | skip: !fetchFees, 11 | fetchPolicy: 'network-only', 12 | pollInterval: 60000, 13 | }); 14 | 15 | useEffect(() => { 16 | if (error || !fetchFees) { 17 | stopPolling(); 18 | } 19 | }, [error, stopPolling, fetchFees]); 20 | 21 | return null; 22 | }; 23 | -------------------------------------------------------------------------------- /.versionrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: [ 3 | { type: 'feat', section: 'Features', hidden: false }, 4 | { type: 'chore', section: 'Improvements', hidden: false }, 5 | { type: 'fix', section: 'Bug Fixes', hidden: false }, 6 | { type: 'refactor', section: 'Refactoring', hidden: false }, 7 | { type: 'perf', section: 'Performance', hidden: false }, 8 | { type: 'revert', section: 'Reverts', hidden: false }, 9 | { type: 'docs', section: 'Docs', hidden: false }, 10 | { type: 'style', section: 'Styling', hidden: false }, 11 | { type: 'test', section: 'Tests', hidden: false }, 12 | { type: 'build', section: 'Build', hidden: false }, 13 | { type: 'ci', section: 'CI', hidden: false }, 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /src/client/src/utils/url.ts: -------------------------------------------------------------------------------- 1 | import { bech32 } from 'bech32'; 2 | 3 | export const getUrlParam = ( 4 | params: string | string[] | undefined 5 | ): string | null => { 6 | if (!params) { 7 | return null; 8 | } 9 | const typeOfQuery = typeof params; 10 | if (typeOfQuery === 'string') { 11 | return params as string; 12 | } 13 | if (typeOfQuery === 'object') { 14 | return params[0]; 15 | } 16 | 17 | return null; 18 | }; 19 | 20 | export const decodeLnUrl = (url: string): string => { 21 | const cleanUrl = url.toLowerCase().replace('lightning:', ''); 22 | const { words } = bech32.decode(cleanUrl, 500); 23 | const bytes = bech32.fromWords(words); 24 | return new String(Buffer.from(bytes)).toString(); 25 | }; 26 | -------------------------------------------------------------------------------- /src/server/utils/network.ts: -------------------------------------------------------------------------------- 1 | import { reversedBytes } from './string'; 2 | 3 | const chains = { 4 | btc: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f', 5 | btcregtest: 6 | '0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206', 7 | btctestnet: 8 | '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943', 9 | btctestnet4: 10 | '00000000da84f2bafbbc53dee25a72ae507ff4914b867c565be350b0da8bf043', 11 | }; 12 | 13 | export const getNetwork = (chain: string) => { 14 | if (!chain) { 15 | return undefined; 16 | } 17 | 18 | const network = Object.keys(chains).find(network => { 19 | return chain === reversedBytes(chains[network]); 20 | }); 21 | 22 | return network; 23 | }; 24 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getPendingChannels.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_PENDING_CHANNELS = gql` 4 | query GetPendingChannels { 5 | getPendingChannels { 6 | close_transaction_id 7 | is_active 8 | is_closing 9 | is_opening 10 | is_timelocked 11 | local_balance 12 | local_reserve 13 | timelock_blocks 14 | timelock_expiration 15 | partner_public_key 16 | received 17 | remote_balance 18 | remote_reserve 19 | sent 20 | transaction_fee 21 | transaction_id 22 | transaction_vout 23 | partner_node_info { 24 | node { 25 | alias 26 | } 27 | } 28 | } 29 | } 30 | `; 31 | -------------------------------------------------------------------------------- /src/server/modules/api/boltz/boltz.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FetchModule } from '../../fetch/fetch.module'; 3 | import { NodeModule } from '../../node/node.module'; 4 | import { 5 | BoltzResolver, 6 | CreateBoltzReverseSwapTypeResolver, 7 | } from './boltz.resolver'; 8 | import { BoltzService } from './boltz.service'; 9 | import { MempoolModule } from '../../mempool/mempool.module'; 10 | import { BlockstreamModule } from '../../blockstream/blockstream.module'; 11 | 12 | @Module({ 13 | imports: [NodeModule, FetchModule, MempoolModule, BlockstreamModule], 14 | providers: [BoltzService, CreateBoltzReverseSwapTypeResolver, BoltzResolver], 15 | exports: [BoltzService], 16 | }) 17 | export class BoltzModule {} 18 | -------------------------------------------------------------------------------- /src/client/src/context/ContextProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { PriceProvider } from './PriceContext'; 3 | import { ChatProvider } from './ChatContext'; 4 | import { RebalanceProvider } from './RebalanceContext'; 5 | import { DashProvider } from './DashContext'; 6 | import { NotificationProvider } from './NotificationContext'; 7 | 8 | export const ContextProvider: React.FC<{ children?: ReactNode }> = ({ 9 | children, 10 | }) => ( 11 | 12 | 13 | 14 | 15 | {children} 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getFeeHealth.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_FEE_HEALTH = gql` 4 | query GetFeeHealth { 5 | getFeeHealth { 6 | score 7 | channels { 8 | id 9 | partnerSide { 10 | score 11 | rate 12 | base 13 | rateScore 14 | baseScore 15 | rateOver 16 | baseOver 17 | } 18 | mySide { 19 | score 20 | rate 21 | base 22 | rateScore 23 | baseScore 24 | rateOver 25 | baseOver 26 | } 27 | partner { 28 | node { 29 | alias 30 | } 31 | } 32 | } 33 | } 34 | } 35 | `; 36 | -------------------------------------------------------------------------------- /src/client/src/views/dashboard/widgets/lightning/forwards.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { ForwardsList } from '../../../forwards'; 3 | 4 | const S = { 5 | wrapper: styled.div` 6 | width: 100%; 7 | height: 100%; 8 | `, 9 | table: styled.div` 10 | width: 100%; 11 | height: calc(100% - 40px); 12 | overflow: auto; 13 | `, 14 | title: styled.h4` 15 | font-weight: 900; 16 | width: 100%; 17 | text-align: center; 18 | margin: 8px 0; 19 | `, 20 | }; 21 | 22 | export const ForwardListWidget = () => { 23 | return ( 24 | 25 | Forwards 26 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/client/src/views/dashboard/widgets/util/Sign.tsx: -------------------------------------------------------------------------------- 1 | import { ColorButton } from '../../../../components/buttons/colorButton/ColorButton'; 2 | import { useDashDispatch } from '../../../../context/DashContext'; 3 | import styled from 'styled-components'; 4 | 5 | const S = { 6 | wrapper: styled.div` 7 | height: 100%; 8 | width: 100%; 9 | `, 10 | }; 11 | 12 | export const SignWidget = () => { 13 | const dispatch = useDashDispatch(); 14 | 15 | return ( 16 | 17 | 20 | dispatch({ type: 'openModal', modalType: 'signMessage' }) 21 | } 22 | > 23 | Sign Message 24 | 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/client/src/hooks/UseChannelInfo.ts: -------------------------------------------------------------------------------- 1 | import { useGetChannelQuery } from '../graphql/queries/__generated__/getChannel.generated'; 2 | 3 | export const useChannelInfo = (id: string) => { 4 | const { data, loading, error } = useGetChannelQuery({ 5 | variables: { id }, 6 | skip: !id, 7 | }); 8 | 9 | if (loading) { 10 | return { peer: { alias: '-', pubkey: '' } }; 11 | } 12 | 13 | if (!data?.getChannel.partner_node_policies?.node?.node?.alias || error) { 14 | return { peer: { alias: 'Unknown', pubkey: '' } }; 15 | } 16 | 17 | return { 18 | peer: { 19 | alias: data.getChannel.partner_node_policies.node.node.alias, 20 | pubkey: data.getChannel.partner_node_policies.node.node.public_key, 21 | }, 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getClosedChannels.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_CLOSED_CHANNELS = gql` 4 | query GetClosedChannels { 5 | getClosedChannels { 6 | capacity 7 | close_confirm_height 8 | close_transaction_id 9 | closed_for_blocks 10 | final_local_balance 11 | final_time_locked_balance 12 | id 13 | is_breach_close 14 | is_cooperative_close 15 | is_funding_cancel 16 | is_local_force_close 17 | is_remote_force_close 18 | partner_public_key 19 | transaction_id 20 | transaction_vout 21 | channel_age 22 | partner_node_info { 23 | node { 24 | alias 25 | } 26 | } 27 | } 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Ideas and feature requests 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Provide examples** 17 | If applicable provide examples, wireframes, sketches or images to better explain your idea. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/client/src/views/tools/Tools.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { ResponsiveLine } from '../../components/generic/Styled'; 3 | 4 | export const NoWrap = styled.div` 5 | margin-right: 16px; 6 | white-space: nowrap; 7 | `; 8 | 9 | export const WrapRequest = styled.div` 10 | overflow-wrap: break-word; 11 | word-wrap: break-word; 12 | -ms-word-break: break-all; 13 | word-break: break-word; 14 | margin: 24px; 15 | font-size: 14px; 16 | `; 17 | 18 | export const Column = styled.div` 19 | width: 100%; 20 | height: 100%; 21 | display: flex; 22 | flex-direction: column; 23 | justify-content: center; 24 | align-items: center; 25 | `; 26 | 27 | export const ToolsResponsiveLine = styled(ResponsiveLine)` 28 | margin-bottom: 8px; 29 | `; 30 | -------------------------------------------------------------------------------- /src/client/src/views/stats/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Card, SubTitle } from '../../components/generic/Styled'; 3 | import { ChevronDown, ChevronUp } from 'react-feather'; 4 | import { StatHeaderLine } from './styles'; 5 | 6 | type StatWrapperProps = { 7 | title: string; 8 | children?: React.ReactNode; 9 | }; 10 | 11 | export const StatWrapper: React.FC = ({ 12 | children, 13 | title, 14 | }) => { 15 | const [open, openSet] = React.useState(false); 16 | 17 | return ( 18 | 19 | openSet(p => !p)}> 20 | {title} 21 | {open ? : } 22 | 23 | {open && children} 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/server/modules/blockstream/blockstream.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { FetchService } from '../fetch/fetch.service'; 3 | import { ConfigService } from '@nestjs/config'; 4 | 5 | @Injectable() 6 | export class BlockstreamService { 7 | constructor( 8 | private fetchService: FetchService, 9 | private config: ConfigService 10 | ) {} 11 | 12 | async broadcastTransaction(transactionHex: string): Promise { 13 | const response = await this.fetchService.fetchWithProxy( 14 | this.config.get('urls.blockstream') + `/api/tx`, 15 | { 16 | method: 'POST', 17 | body: transactionHex, 18 | headers: { 'Content-Type': 'text/plain' }, 19 | } 20 | ); 21 | return (await response.text()) as string; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | 'prettier', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | '@typescript-eslint/no-unused-vars': 'error', 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: 'http://localhost:3000/graphql' 3 | documents: 'src/client/src/graphql/**/*.ts' 4 | hooks: 5 | afterAllFileWrite: 6 | - prettier --write 7 | - eslint --fix 8 | generates: 9 | src/client/src/graphql/fragmentTypes.json: 10 | plugins: 11 | - fragment-matcher 12 | src/client/src/graphql/types.ts: 13 | plugins: 14 | - typescript 15 | src/client/src/graphql/: 16 | preset: near-operation-file 17 | presetConfig: 18 | baseTypesPath: types.ts 19 | extension: .generated.tsx 20 | folder: __generated__ 21 | config: 22 | withComponent: false 23 | withHOC: false 24 | withHooks: true 25 | reactApolloVersion: 3 26 | plugins: 27 | - 'typescript-operations' 28 | - 'typescript-react-apollo' 29 | -------------------------------------------------------------------------------- /src/client/src/components/viewSwitch/ViewSwitch.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import styled from 'styled-components'; 3 | import { mediaWidths } from '../../styles/Themes'; 4 | 5 | const HideMobile = styled.div` 6 | @media (${mediaWidths.mobile}) { 7 | display: none; 8 | } 9 | `; 10 | 11 | const HideDesktop = styled.div` 12 | display: none; 13 | @media (${mediaWidths.mobile}) { 14 | display: unset; 15 | } 16 | `; 17 | 18 | interface ViewSwitchProps { 19 | hideMobile?: boolean; 20 | children?: ReactNode; 21 | } 22 | 23 | export const ViewSwitch: React.FC = ({ 24 | hideMobile, 25 | children, 26 | }) => { 27 | return hideMobile ? ( 28 | {children} 29 | ) : ( 30 | {children} 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/client/src/views/channels/channels/helpers.ts: -------------------------------------------------------------------------------- 1 | export const defaultHiddenColumns = [ 2 | 'channelPrivateLogo', 3 | 'channelOpenerLogo', 4 | 'channel_age', 5 | 'channel_age_duplicate', 6 | 'past_states', 7 | 'local_balance', 8 | 'remote_balance', 9 | 'balancePercentText', 10 | 'pending_total_amount', 11 | 'pending_total_tokens', 12 | 'pending_incoming_amount', 13 | 'pending_incoming_tokens', 14 | 'pending_outgoing_amount', 15 | 'pending_outgoing_tokens', 16 | 'time_offline', 17 | 'percentOnlineText', 18 | 'time_online', 19 | 'sent', 20 | 'received', 21 | 'activityPercentText', 22 | 'myBase', 23 | 'partnerBase', 24 | 'myMaxHtlc', 25 | 'myMinHtlc', 26 | 'partnerMaxHtlc', 27 | 'partnerMinHtlc', 28 | 'proportionalBars', 29 | 'activityBars', 30 | 'viewAction', 31 | ]; 32 | -------------------------------------------------------------------------------- /src/client/src/views/tools/backups/Backups.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | CardWithTitle, 4 | CardTitle, 5 | SubTitle, 6 | Card, 7 | } from '../../../components/generic/Styled'; 8 | import { DownloadBackups } from './DownloadBackups'; 9 | import { VerifyBackups } from './VerifyBackups'; 10 | import { RecoverFunds } from './RecoverFunds'; 11 | import { VerifyBackup } from './VerifyBackup'; 12 | 13 | export const BackupsView = () => { 14 | return ( 15 | <> 16 | 17 | 18 | Backups 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /.next 5 | src/client/.next 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | .env.local 39 | .env.development 40 | # Elastic Beanstalk Files 41 | .elasticbeanstalk/* 42 | !.elasticbeanstalk/*.cfg.yml 43 | !.elasticbeanstalk/*.global.yml 44 | 45 | # config files 46 | config.yaml 47 | 48 | # For Dev 49 | local_data 50 | docker-compose.dev.yml 51 | -------------------------------------------------------------------------------- /src/client/pages/login.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Spacer } from '../src/components/spacer/Spacer'; 3 | import { ThunderStorm } from '../src/views/homepage/HomePage.styled'; 4 | import { appendBasePath } from '../src/utils/basePath'; 5 | import { NextPageContext } from 'next'; 6 | import { getProps } from '../src/utils/ssr'; 7 | import { TopSection } from '../src/views/homepage/Top'; 8 | import { Accounts } from '../src/views/homepage/Accounts'; 9 | 10 | const ContextApp = () => ( 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | 19 | export default ContextApp; 20 | 21 | export async function getServerSideProps(context: NextPageContext) { 22 | return await getProps(context, true); 23 | } 24 | -------------------------------------------------------------------------------- /src/client/src/views/settings/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { SettingsLine } from '../../../pages/settings'; 3 | import { ColorButton } from '../../components/buttons/colorButton/ColorButton'; 4 | import { 5 | Card, 6 | CardWithTitle, 7 | Sub4Title, 8 | SubTitle, 9 | } from '../../components/generic/Styled'; 10 | 11 | export const DashboardSettings = () => { 12 | const { push } = useRouter(); 13 | 14 | return ( 15 | 16 | Dashboard 17 | 18 | 19 | Widgets 20 | push('/settings/dashboard')}> 21 | Change 22 | 23 | 24 | 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report a problem 3 | about: File a technical problem or report a bug 4 | --- 5 | 6 | **Describe the problem/bug** 7 | A clear and concise description of what the bug is. 8 | 9 | **Your environment** 10 | * Version of ThunderHub: 11 | * Deployment method: 12 | * Other relevant environment details: 13 | 14 | **To Reproduce** 15 | Steps to reproduce the behavior: 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Actual behavior** 25 | Tell us what happens instead 26 | 27 | **Screenshots/Links** 28 | If applicable, add screenshots or links to help explain your problem. 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | categories: 4 | - title: '🚀 Features' 5 | labels: 6 | - 'feat' 7 | - 'feature' 8 | - 'enhancement' 9 | - title: '💪 Improvements' 10 | labels: 11 | - 'chore' 12 | - 'refactor' 13 | - 'perf' 14 | - title: '📃 Docs' 15 | label: 'docs' 16 | - title: '🐛 Bug Fixes' 17 | labels: 18 | - 'fix' 19 | - 'bugfix' 20 | - 'bug' 21 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 22 | version-resolver: 23 | major: 24 | labels: 25 | - 'major' 26 | minor: 27 | labels: 28 | - 'minor' 29 | patch: 30 | labels: 31 | - 'patch' 32 | default: patch 33 | template: | 34 | ## Changes 35 | 36 | $CHANGES 37 | 38 | ## Contributors 39 | 40 | $CONTRIBUTORS 41 | -------------------------------------------------------------------------------- /src/client/pages/settings/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GridWrapper } from '../../src/components/gridWrapper/GridWrapper'; 3 | import { NextPageContext } from 'next'; 4 | import { getProps } from '../../src/utils/ssr'; 5 | import dynamic from 'next/dynamic'; 6 | import { LoadingCard } from '../../src/components/loading/LoadingCard'; 7 | 8 | const LoadingComp = () => ; 9 | 10 | const Dashboard = dynamic(() => import('../../src/views/settings/DashPanel'), { 11 | ssr: false, 12 | loading: LoadingComp, 13 | }); 14 | 15 | const Wrapped = () => ( 16 | 17 | 18 | 19 | ); 20 | 21 | export default Wrapped; 22 | 23 | export async function getServerSideProps(context: NextPageContext) { 24 | return await getProps(context); 25 | } 26 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getPayments.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_PAYMENTS = gql` 4 | query GetPayments($token: String) { 5 | getPayments(token: $token) { 6 | next 7 | payments { 8 | created_at 9 | destination 10 | destination_node { 11 | node { 12 | alias 13 | public_key 14 | } 15 | } 16 | fee 17 | fee_mtokens 18 | hops { 19 | node { 20 | alias 21 | public_key 22 | } 23 | } 24 | id 25 | index 26 | is_confirmed 27 | is_outgoing 28 | mtokens 29 | request 30 | safe_fee 31 | safe_tokens 32 | secret 33 | tokens 34 | type 35 | date 36 | } 37 | } 38 | } 39 | `; 40 | -------------------------------------------------------------------------------- /scripts/updateToLatest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # fetch latest master 3 | echo "Checking for changes upstream ..." 4 | git fetch 5 | UPSTREAM=${1:-'@{u}'} 6 | LOCAL=$(git rev-parse @) 7 | REMOTE=$(git rev-parse "$UPSTREAM") 8 | 9 | if [ $LOCAL = $REMOTE ]; then 10 | TAG=$(git tag | sort -V | tail -1) 11 | echo "You are up-to-date on version" $TAG 12 | else 13 | echo "Reseting repository..." 14 | git reset --hard 15 | 16 | echo "Pulling latest changes..." 17 | git pull -p 18 | 19 | # install deps 20 | echo "Installing dependencies..." 21 | npm install --quiet 22 | 23 | # build nextjs 24 | echo "Building application..." 25 | npm run build 26 | 27 | # remove useless deps 28 | echo "Removing unneccesary modules..." 29 | npm prune --production 30 | 31 | TAG=$(git tag | sort -V | tail -1) 32 | echo "Updated to version" $TAG 33 | fi -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/createBoltzReverseSwap.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_BOLTZ_INFO = gql` 4 | mutation CreateBoltzReverseSwap($amount: Float!, $address: String) { 5 | createBoltzReverseSwap(amount: $amount, address: $address) { 6 | id 7 | invoice 8 | redeemScript 9 | onchainAmount 10 | timeoutBlockHeight 11 | lockupAddress 12 | minerFeeInvoice 13 | receivingAddress 14 | preimage 15 | preimageHash 16 | privateKey 17 | publicKey 18 | decodedInvoice { 19 | description 20 | destination 21 | expires_at 22 | id 23 | safe_tokens 24 | tokens 25 | destination_node { 26 | node { 27 | alias 28 | public_key 29 | } 30 | } 31 | } 32 | } 33 | } 34 | `; 35 | -------------------------------------------------------------------------------- /src/client/src/views/swap/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BoltzSwapStatus, 3 | CreateBoltzReverseSwapType, 4 | DecodeInvoice, 5 | } from '../../graphql/types'; 6 | 7 | export type CreateBoltzReverseSwap = Pick< 8 | CreateBoltzReverseSwapType, 9 | | 'id' 10 | | 'invoice' 11 | | 'redeemScript' 12 | | 'onchainAmount' 13 | | 'timeoutBlockHeight' 14 | | 'lockupAddress' 15 | | 'minerFeeInvoice' 16 | | 'receivingAddress' 17 | | 'preimage' 18 | | 'privateKey' 19 | > & { 20 | decodedInvoice?: Pick< 21 | DecodeInvoice, 22 | | 'description' 23 | | 'destination' 24 | | 'expires_at' 25 | | 'id' 26 | | 'safe_tokens' 27 | | 'tokens' 28 | | 'destination_node' 29 | > | null; 30 | } & { claimTransaction?: string }; 31 | 32 | export type EnrichedSwap = { 33 | boltz?: Pick | null; 34 | } & CreateBoltzReverseSwap; 35 | -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getChannel.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_CHANNEL = gql` 4 | query GetChannel($id: String!) { 5 | getChannel(id: $id) { 6 | partner_node_policies { 7 | node { 8 | node { 9 | alias 10 | public_key 11 | } 12 | } 13 | } 14 | } 15 | } 16 | `; 17 | 18 | export const GET_CHANNEL_INFO = gql` 19 | query GetChannelInfo($id: String!) { 20 | getChannel(id: $id) { 21 | transaction_id 22 | transaction_vout 23 | node_policies { 24 | base_fee_mtokens 25 | max_htlc_mtokens 26 | min_htlc_mtokens 27 | fee_rate 28 | cltv_delta 29 | } 30 | partner_node_policies { 31 | node { 32 | node { 33 | alias 34 | } 35 | } 36 | } 37 | } 38 | } 39 | `; 40 | -------------------------------------------------------------------------------- /src/server/modules/api/forwards/forwards.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NodeModule } from '../../node/node.module'; 3 | import { 4 | AggregatedChannelForwardsResolver, 5 | AggregatedChannelSideForwardsResolver, 6 | AggregatedRouteForwardsResolver, 7 | AggregatedSideStatsResolver, 8 | BaseNodeInfoResolver, 9 | ChannelInfoResolver, 10 | ForwardResolver, 11 | ForwardsResolver, 12 | GetForwardsResolver, 13 | } from './forwards.resolver'; 14 | 15 | @Module({ 16 | imports: [NodeModule], 17 | providers: [ 18 | ForwardsResolver, 19 | ChannelInfoResolver, 20 | BaseNodeInfoResolver, 21 | GetForwardsResolver, 22 | AggregatedChannelSideForwardsResolver, 23 | AggregatedRouteForwardsResolver, 24 | ForwardResolver, 25 | AggregatedChannelForwardsResolver, 26 | AggregatedSideStatsResolver, 27 | ], 28 | }) 29 | export class ForwardsModule {} 30 | -------------------------------------------------------------------------------- /src/server/utils/env.ts: -------------------------------------------------------------------------------- 1 | import { YamlEnvs } from '../config/configuration'; 2 | import { 3 | AccountType, 4 | UnresolvedAccountType, 5 | } from '../modules/files/files.types'; 6 | 7 | export const resolveEnvVarsInAccount = ( 8 | account: UnresolvedAccountType, 9 | yamlEnvs: YamlEnvs 10 | ): AccountType => { 11 | const regex = /(?<=\{)(.*?)(?=\})/; 12 | 13 | const resolved = Object.fromEntries( 14 | Object.entries(account).map(([k, v]) => { 15 | if (typeof v !== 'string') { 16 | return [k, v]; 17 | } 18 | 19 | const match: string | boolean = 20 | yamlEnvs[v.toString().match(regex)?.[0] || ''] || v; 21 | 22 | if (match === 'true') { 23 | return [k, true]; 24 | } 25 | 26 | if (match === 'false') { 27 | return [k, false]; 28 | } 29 | 30 | return [k, match]; 31 | }) 32 | ); 33 | 34 | return resolved; 35 | }; 36 | -------------------------------------------------------------------------------- /src/server/modules/view/view.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleInit } from '@nestjs/common'; 2 | import createServer from 'next'; 3 | import { NextServer } from 'next/dist/server/next'; 4 | import { Request, Response } from 'express'; 5 | import { ConfigService } from '@nestjs/config'; 6 | 7 | @Injectable() 8 | export class ViewService implements OnModuleInit { 9 | private server: NextServer; 10 | 11 | constructor(private configService: ConfigService) {} 12 | 13 | async onModuleInit(): Promise { 14 | try { 15 | this.server = createServer({ 16 | dev: !this.configService.get('isProduction'), 17 | dir: './src/client', 18 | }); 19 | await this.server.prepare(); 20 | } catch (error) { 21 | console.error(error); 22 | } 23 | } 24 | 25 | handler(req: Request, res: Response) { 26 | return this.server.getRequestHandler()(req, res); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/server/modules/node/lnd/lnd.helpers.ts: -------------------------------------------------------------------------------- 1 | export const to = async (promise: Promise) => { 2 | return promise 3 | .then(data => data) 4 | .catch(err => { 5 | throw new Error(getErrorMsg(err)); 6 | }); 7 | }; 8 | 9 | export const getErrorMsg = (error: any[] | string): string => { 10 | if (!error) return 'Unknown Error'; 11 | if (typeof error === 'string') return error; 12 | 13 | if (error[2]) { 14 | const errorTitle = error[1] || ''; 15 | const errorObject = error[2]?.err; 16 | 17 | let errorString = ''; 18 | if (typeof errorObject === 'string') { 19 | errorString = `${errorTitle}. ${errorObject}`; 20 | } else { 21 | errorString = `${errorTitle}. ${errorObject?.details || ''}`; 22 | } 23 | 24 | return errorString; 25 | } 26 | 27 | if (error[1] && typeof error[1] === 'string') { 28 | return error[1]; 29 | } 30 | 31 | console.log('Unknown Error:', error); 32 | return 'Unknown Error'; 33 | }; 34 | -------------------------------------------------------------------------------- /src/client/src/hooks/UseBitcoinFees.tsx: -------------------------------------------------------------------------------- 1 | import { useGetBitcoinFeesQuery } from '../../src/graphql/queries/__generated__/getBitcoinFees.generated'; 2 | 3 | type State = { 4 | dontShow: boolean; 5 | fast: number; 6 | halfHour: number; 7 | hour: number; 8 | minimum: number; 9 | }; 10 | 11 | const initialState: State = { 12 | dontShow: true, 13 | fast: 0, 14 | halfHour: 0, 15 | hour: 0, 16 | minimum: 0, 17 | }; 18 | 19 | export const useBitcoinFees = (dontFetch?: boolean): State => { 20 | const { loading, data, error } = useGetBitcoinFeesQuery({ 21 | fetchPolicy: 'cache-first', 22 | skip: dontFetch, 23 | }); 24 | 25 | if (!data?.getBitcoinFees || loading || error) { 26 | return initialState; 27 | } 28 | 29 | const { fast, halfHour, hour, minimum } = data.getBitcoinFees; 30 | return { 31 | fast: fast || 0, 32 | halfHour: halfHour || 0, 33 | hour: hour || 0, 34 | dontShow: false, 35 | minimum: minimum || 0, 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/server/modules/dataloader/dataloader.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import DataLoader from 'dataloader'; 3 | import { AmbossService } from '../api/amboss/amboss.service'; 4 | import { EdgeInfo, NodeAlias } from '../api/amboss/amboss.types'; 5 | 6 | export type DataloaderTypes = { 7 | nodesLoader: DataLoader; 8 | edgesLoader: DataLoader; 9 | }; 10 | 11 | @Injectable() 12 | export class DataloaderService { 13 | constructor(private ambossService: AmbossService) {} 14 | 15 | createLoaders(): DataloaderTypes { 16 | const nodesLoader = new DataLoader( 17 | async (pubkeys: string[]) => this.ambossService.getNodeAliasBatch(pubkeys) 18 | ); 19 | 20 | const edgesLoader = new DataLoader( 21 | async (ids: string[]) => this.ambossService.getEdgeInfoBatch(ids) 22 | ); 23 | 24 | return { 25 | nodesLoader, 26 | edgesLoader, 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/server/utils/string.ts: -------------------------------------------------------------------------------- 1 | export const shorten = (text: string): string => { 2 | if (!text) return ''; 3 | const amount = 6; 4 | const beginning = text.slice(0, amount); 5 | const end = text.slice(text.length - amount); 6 | 7 | return `${beginning}...${end}`; 8 | }; 9 | 10 | export const reversedBytes = hex => 11 | Buffer.from(hex, 'hex').reverse().toString('hex'); 12 | 13 | const ansiRegex = ({ onlyFirst = false } = {}) => { 14 | const pattern = [ 15 | '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', 16 | '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))', 17 | ].join('|'); 18 | 19 | return new RegExp(pattern, onlyFirst ? undefined : 'g'); 20 | }; 21 | 22 | export const stripAnsi = string => { 23 | if (typeof string !== 'string') { 24 | throw new TypeError(`Expected a \`string\`, got \`${typeof string}\``); 25 | } 26 | 27 | return string.replace(ansiRegex(), ''); 28 | }; 29 | -------------------------------------------------------------------------------- /src/client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: './tsconfig.json', 5 | sourceType: 'module', 6 | tsconfigRootDir: __dirname, 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'next/core-web-vitals', 11 | 'plugin:@typescript-eslint/recommended', 12 | 'plugin:prettier/recommended', 13 | 'prettier', 14 | ], 15 | root: true, 16 | env: { 17 | browser: true, 18 | node: true, 19 | jest: true, 20 | }, 21 | ignorePatterns: ['.eslintrc.js'], 22 | rules: { 23 | '@typescript-eslint/interface-name-prefix': 'off', 24 | '@typescript-eslint/explicit-function-return-type': 'off', 25 | '@typescript-eslint/explicit-module-boundary-types': 'off', 26 | '@typescript-eslint/no-explicit-any': 'off', 27 | '@typescript-eslint/no-unused-vars': 'error', 28 | // '@next/next/no-html-link-for-pages': ['error', '/src/client/pages/'], 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/client/pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { NextPageContext } from 'next'; 2 | import { getProps } from '../src/utils/ssr'; 3 | import dynamic from 'next/dynamic'; 4 | import { LoadingCard } from '../src/components/loading/LoadingCard'; 5 | import { SimpleWrapper } from '../src/components/gridWrapper/GridWrapper'; 6 | import styled from 'styled-components'; 7 | 8 | const S = { 9 | wrapper: styled.div` 10 | position: relative; 11 | `, 12 | }; 13 | 14 | const LoadingComp = () => ; 15 | 16 | const Dashboard = dynamic(() => import('../src/views/dashboard'), { 17 | ssr: false, 18 | loading: LoadingComp, 19 | }); 20 | 21 | const Wrapped = () => { 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default Wrapped; 32 | 33 | export async function getServerSideProps(context: NextPageContext) { 34 | return await getProps(context); 35 | } 36 | -------------------------------------------------------------------------------- /src/client/pages/tools.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GridWrapper } from '../src/components/gridWrapper/GridWrapper'; 3 | import { Bakery } from '../src/views/tools/bakery/Bakery'; 4 | import { Accounting } from '../src/views/tools/accounting/Accounting'; 5 | import { NextPageContext } from 'next'; 6 | import { getProps } from '../src/utils/ssr'; 7 | import { BackupsView } from '../src/views/tools/backups/Backups'; 8 | import { MessagesView } from '../src/views/tools/messages/Messages'; 9 | import { WalletVersion } from '../src/views/tools/WalletVersion'; 10 | 11 | const ToolsView = () => ( 12 | <> 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | 21 | const Wrapped = () => ( 22 | 23 | 24 | 25 | ); 26 | 27 | export default Wrapped; 28 | 29 | export async function getServerSideProps(context: NextPageContext) { 30 | return await getProps(context); 31 | } 32 | -------------------------------------------------------------------------------- /src/server/modules/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; 4 | import { Logger } from 'winston'; 5 | import { AccountsService } from '../accounts/accounts.service'; 6 | 7 | @Injectable() 8 | export class AuthenticationService { 9 | constructor( 10 | private readonly jwtService: JwtService, 11 | private accountsService: AccountsService, 12 | @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger 13 | ) {} 14 | 15 | public async getUserFromAuthToken(token: string) { 16 | try { 17 | const payload = this.jwtService.verify(token); 18 | 19 | if (payload.sub) { 20 | const account = this.accountsService.getAccount(payload.sub); 21 | 22 | if (account) { 23 | return payload.sub; 24 | } 25 | } 26 | } catch (error) { 27 | this.logger.error('Invalid token for authentication'); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/client/src/hooks/UseElementSize.tsx: -------------------------------------------------------------------------------- 1 | import { RefObject, useState, useEffect, useCallback } from 'react'; 2 | 3 | import useEventListener from './UseEventListener'; 4 | 5 | interface Size { 6 | width: number; 7 | height: number; 8 | } 9 | 10 | function useElementSize( 11 | elementRef: RefObject 12 | ): Size { 13 | const [size, setSize] = useState({ 14 | width: 0, 15 | height: 0, 16 | }); 17 | 18 | // Prevent too many rendering using useCallback 19 | const updateSize = useCallback(() => { 20 | const node = elementRef?.current; 21 | if (node) { 22 | setSize({ 23 | width: node.offsetWidth || 0, 24 | height: node.offsetHeight || 0, 25 | }); 26 | } 27 | }, [elementRef]); 28 | 29 | // Initial size on mount 30 | useEffect(() => { 31 | updateSize(); 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | }, []); 34 | 35 | useEventListener('resize', updateSize); 36 | 37 | return size; 38 | } 39 | 40 | export default useElementSize; 41 | -------------------------------------------------------------------------------- /src/client/src/styles/GlobalStyle.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | import { backgroundColor, textColor } from './Themes'; 3 | import { Noto_Sans } from 'next/font/google'; 4 | 5 | const notoSans = Noto_Sans({ 6 | weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'], 7 | subsets: ['latin'], 8 | }); 9 | 10 | export const GlobalStyles = createGlobalStyle` 11 | html, body { 12 | margin: 0; 13 | padding: 0; 14 | } 15 | * { 16 | font-variant-numeric: tabular-nums; 17 | font-family: ${notoSans.style.fontFamily}, sans-serif; 18 | } 19 | *, *::after, *::before { 20 | box-sizing: border-box; 21 | } 22 | body { 23 | background: ${backgroundColor}; 24 | color: ${textColor}; 25 | font-variant-numeric: tabular-nums; 26 | font-family: ${notoSans.style.fontFamily}, sans-serif; 27 | text-rendering: optimizeLegibility; 28 | -webkit-font-smoothing: antialiased; 29 | -moz-osx-font-smoothing: grayscale; 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /src/client/src/views/dashboard/widgets/util/DonateWidget.tsx: -------------------------------------------------------------------------------- 1 | import { Heart } from 'react-feather'; 2 | import { ColorButton } from '../../../../components/buttons/colorButton/ColorButton'; 3 | import { useDashDispatch } from '../../../../context/DashContext'; 4 | import styled from 'styled-components'; 5 | 6 | const S = { 7 | wrapper: styled.div` 8 | height: 100%; 9 | width: 100%; 10 | `, 11 | title: styled.div` 12 | font-size: 14px; 13 | margin-left: 4px; 14 | `, 15 | row: styled.div` 16 | display: flex; 17 | justify-content: space-around; 18 | align-items: center; 19 | `, 20 | }; 21 | 22 | export const DonateWidget = () => { 23 | const dispatch = useDashDispatch(); 24 | 25 | return ( 26 | 27 | dispatch({ type: 'openModal', modalType: 'donate' })} 30 | > 31 | 32 | 33 | Donate 34 | 35 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/server/modules/api/network/network.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Query, Resolver } from '@nestjs/graphql'; 2 | import { NodeService } from '../../node/node.service'; 3 | import { CurrentUser } from '../../security/security.decorators'; 4 | import { UserId } from '../../security/security.types'; 5 | import { NetworkInfo } from './network.types'; 6 | 7 | @Resolver() 8 | export class NetworkResolver { 9 | constructor(private nodeService: NodeService) {} 10 | 11 | @Query(() => NetworkInfo) 12 | async getNetworkInfo(@CurrentUser() { id }: UserId) { 13 | const info = await this.nodeService.getNetworkInfo(id); 14 | 15 | return { 16 | averageChannelSize: info.average_channel_size, 17 | channelCount: info.channel_count, 18 | maxChannelSize: info.max_channel_size, 19 | medianChannelSize: info.median_channel_size, 20 | minChannelSize: info.min_channel_size, 21 | nodeCount: info.node_count, 22 | notRecentlyUpdatedPolicyCount: info.not_recently_updated_policy_count, 23 | totalCapacity: info.total_capacity, 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/server/modules/security/guards/graphql.guard.ts: -------------------------------------------------------------------------------- 1 | import { ContextType, ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { GqlExecutionContext } from '@nestjs/graphql'; 4 | import { IS_PUBLIC_KEY } from '../security.decorators'; 5 | import { Reflector } from '@nestjs/core'; 6 | 7 | @Injectable() 8 | export class GqlAuthGuard extends AuthGuard('jwt') { 9 | constructor(private reflector: Reflector) { 10 | super(); 11 | } 12 | 13 | canActivate(context: ExecutionContext) { 14 | const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ 15 | context.getHandler(), 16 | context.getClass(), 17 | ]); 18 | if (isPublic) { 19 | return true; 20 | } 21 | return super.canActivate(context); 22 | } 23 | 24 | getRequest(context: ExecutionContext) { 25 | if (context.getType() === 'graphql') { 26 | return GqlExecutionContext.create(context).getContext().req; 27 | } 28 | return context.switchToHttp().getRequest(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/server/modules/security/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable, Inject } from '@nestjs/common'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import { JwtObjectType, UserId } from './security.types'; 6 | import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; 7 | import { Logger } from 'winston'; 8 | 9 | @Injectable() 10 | export class JwtStrategy extends PassportStrategy(Strategy) { 11 | constructor( 12 | private configService: ConfigService, 13 | @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger 14 | ) { 15 | super({ 16 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 17 | secretOrKey: configService.get('jwtSecret'), 18 | }); 19 | } 20 | 21 | async validate(payload: JwtObjectType): Promise { 22 | if (!payload?.sub) { 23 | throw new Error('Unauthorized token'); 24 | } 25 | 26 | const id: UserId = { 27 | id: payload.sub || '', 28 | }; 29 | 30 | return id; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/server/modules/security/security.decorators.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createParamDecorator, 3 | SetMetadata, 4 | ExecutionContext, 5 | } from '@nestjs/common'; 6 | import { GqlExecutionContext } from '@nestjs/graphql'; 7 | import { getIp } from 'src/server/utils/request'; 8 | 9 | export enum Role { 10 | Owner = 'owner', 11 | Admin = 'admin', 12 | Premium = 'premium', 13 | } 14 | 15 | export const ROLES_KEY = 'roles'; 16 | export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles); 17 | 18 | export const IS_PUBLIC_KEY = 'isPublic'; 19 | export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); 20 | 21 | export const CurrentUser = createParamDecorator( 22 | (_: unknown, context: ExecutionContext) => { 23 | const ctx = GqlExecutionContext.create(context); 24 | return ctx.getContext().req.user; 25 | } 26 | ); 27 | 28 | export const CurrentIp = createParamDecorator( 29 | (_: unknown, context: ExecutionContext) => { 30 | const ctx = GqlExecutionContext.create(context); 31 | const req = ctx.getContext().req; 32 | return getIp(req); 33 | } 34 | ); 35 | -------------------------------------------------------------------------------- /src/server/modules/security/guards/throttler.guard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { GqlExecutionContext } from '@nestjs/graphql'; 3 | import { 4 | ThrottlerException, 5 | ThrottlerGuard, 6 | ThrottlerOptions, 7 | } from '@nestjs/throttler'; 8 | import { getIp } from 'src/server/utils/request'; 9 | 10 | @Injectable() 11 | export class GqlThrottlerGuard extends ThrottlerGuard { 12 | async handleRequest( 13 | context: ExecutionContext, 14 | limit: number, 15 | ttl: number, 16 | throttler: ThrottlerOptions 17 | ): Promise { 18 | const gqlCtx = GqlExecutionContext.create(context); 19 | const { req, connection } = gqlCtx.getContext(); 20 | 21 | const request = connection?.context?.req ? connection.context.req : req; 22 | const ip = getIp(request); 23 | const key = this.generateKey(context, ip, throttler.name); 24 | const { totalHits } = await this.storageService.increment(key, ttl); 25 | 26 | if (totalHits >= limit) { 27 | throw new ThrottlerException(); 28 | } 29 | 30 | return true; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/client/src/components/table/DebouncedInput.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Input } from '../input'; 3 | 4 | // A debounced input react component 5 | export function DebouncedInput({ 6 | value: initialValue, 7 | onChange, 8 | debounce = 500, 9 | placeholder, 10 | count, 11 | }: { 12 | value: string | number; 13 | onChange: (value: string | number) => void; 14 | count: number; 15 | debounce?: number; 16 | placeholder?: string; 17 | } & Omit, 'onChange'>) { 18 | const [value, setValue] = useState(initialValue); 19 | 20 | useEffect(() => { 21 | setValue(initialValue); 22 | }, [initialValue]); 23 | 24 | useEffect(() => { 25 | const timeout = setTimeout(() => { 26 | onChange(value); 27 | }, debounce); 28 | 29 | return () => clearTimeout(timeout); 30 | }, [value]); 31 | 32 | return ( 33 | setValue(e.target.value)} 37 | placeholder={`Search ${count} ${placeholder || ''}`} 38 | /> 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/client/src/views/dashboard/widgets/external/mempool.tsx: -------------------------------------------------------------------------------- 1 | import Table from '../../../../components/table'; 2 | import { useBitcoinFees } from '../../../../hooks/UseBitcoinFees'; 3 | import styled from 'styled-components'; 4 | 5 | const S = { 6 | wrapper: styled.div` 7 | width: 100%; 8 | overflow: auto; 9 | `, 10 | }; 11 | 12 | export const MempoolWidget = () => { 13 | const { fast, halfHour, hour, minimum, dontShow } = useBitcoinFees(); 14 | 15 | if (dontShow) { 16 | return null; 17 | } 18 | 19 | const columns = [ 20 | { header: 'Fastest', accessorKey: 'fast' }, 21 | { header: 'Half Hour', accessorKey: 'halfHour' }, 22 | { header: 'Hour', accessorKey: 'hour' }, 23 | { header: 'Minimum', accessorKey: 'minimum' }, 24 | ]; 25 | 26 | const data = [ 27 | { 28 | fast: `${fast} sat/vB`, 29 | halfHour: `${halfHour} sat/vB`, 30 | hour: `${hour} sat/vB`, 31 | minimum: `${minimum} sat/vB`, 32 | }, 33 | ]; 34 | 35 | return ( 36 | 37 | 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/server/modules/api/github/github.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Query, Resolver } from '@nestjs/graphql'; 2 | import { toWithError } from 'src/server/utils/async'; 3 | import { FetchService } from '../../fetch/fetch.service'; 4 | import { Inject } from '@nestjs/common'; 5 | import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; 6 | import { Logger } from 'winston'; 7 | import { ConfigService } from '@nestjs/config'; 8 | 9 | @Resolver() 10 | export class GithubResolver { 11 | constructor( 12 | private configService: ConfigService, 13 | private fetchService: FetchService, 14 | @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger 15 | ) {} 16 | 17 | @Query(() => String) 18 | async getLatestVersion() { 19 | const [response, error] = await toWithError( 20 | this.fetchService.fetchWithProxy(this.configService.get('urls.github')) 21 | ); 22 | 23 | if (error || !response) { 24 | this.logger.debug('Unable to get latest github version'); 25 | throw new Error('NoGithubVersion'); 26 | } 27 | 28 | const json = await response.json(); 29 | 30 | return json.tag_name; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Anthony Potdevin 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. -------------------------------------------------------------------------------- /src/client/src/graphql/queries/getInvoices.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const GET_INVOICES = gql` 4 | query GetInvoices($token: String) { 5 | getInvoices(token: $token) { 6 | next 7 | invoices { 8 | chain_address 9 | confirmed_at 10 | created_at 11 | description 12 | description_hash 13 | expires_at 14 | id 15 | is_canceled 16 | is_confirmed 17 | is_held 18 | is_private 19 | is_push 20 | received 21 | received_mtokens 22 | request 23 | secret 24 | tokens 25 | type 26 | date 27 | payments { 28 | canceled_at 29 | confirmed_at 30 | created_at 31 | created_height 32 | is_canceled 33 | is_confirmed 34 | is_held 35 | mtokens 36 | pending_index 37 | timeout 38 | tokens 39 | total_mtokens 40 | in_channel 41 | messages { 42 | message 43 | } 44 | } 45 | } 46 | } 47 | } 48 | `; 49 | -------------------------------------------------------------------------------- /src/client/src/components/loadingBar/LoadingBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { progressBackground } from '../../styles/Themes'; 4 | 5 | const Progress = styled.div` 6 | width: 100%; 7 | background: ${progressBackground}; 8 | `; 9 | 10 | interface ProgressBar { 11 | percent: number; 12 | barColor?: string; 13 | } 14 | 15 | const ProgressBar = styled.div` 16 | height: 10px; 17 | background-color: ${({ barColor }) => (barColor ? barColor : 'blue')}; 18 | width: ${({ percent }: ProgressBar) => `${percent}%`}; 19 | `; 20 | 21 | const getColor = (percent: number) => { 22 | switch (true) { 23 | case percent < 20: 24 | return '#ff4d4f'; 25 | case percent < 40: 26 | return '#ff7a45'; 27 | case percent < 60: 28 | return '#ffa940'; 29 | case percent < 80: 30 | return '#bae637'; 31 | case percent <= 100: 32 | return '#73d13d'; 33 | default: 34 | return ''; 35 | } 36 | }; 37 | 38 | export const LoadingBar = ({ percent }: { percent: number }) => ( 39 | 40 | 41 | 42 | ); 43 | -------------------------------------------------------------------------------- /src/client/src/views/amboss/Billboard.tsx: -------------------------------------------------------------------------------- 1 | import { ChatInput } from '../chat/ChatInput'; 2 | import { 3 | Card, 4 | CardWithTitle, 5 | SubTitle, 6 | Separation, 7 | } from '../../components/generic/Styled'; 8 | import { Text } from '../../components/typography/Styled'; 9 | import { Link } from '../../components/link/Link'; 10 | 11 | export const Billboard = () => { 12 | return ( 13 | 14 | Keysend Billboard 15 | 16 | 17 | Keysend{' '} 18 | 22 | Amboss 23 | {' '} 24 | a message and it will appear on their home page! Messages are sorted 25 | by amount of sats sent and how recent it was. 26 | 27 | 28 | 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/server/modules/api/amboss/amboss.helpers.ts: -------------------------------------------------------------------------------- 1 | import { GetChannelsResult } from 'lightning'; 2 | import { EdgeInfo, NodeAlias } from './amboss.types'; 3 | 4 | export const mapNodeResult = ( 5 | pubkeys: string[], 6 | nodes: NodeAlias[] 7 | ): (NodeAlias | null)[] => { 8 | return pubkeys.map(pk => nodes.find(node => node.pub_key === pk) || null); 9 | }; 10 | 11 | export const mapEdgeResult = ( 12 | ids: string[], 13 | edges: EdgeInfo[] 14 | ): (EdgeInfo | null)[] => { 15 | return ids.map( 16 | pk => edges.find(edge => edge.short_channel_id === pk) || null 17 | ); 18 | }; 19 | 20 | export const getMappedChannelInfo = ( 21 | info: GetChannelsResult 22 | ): { 23 | chan_id: string; 24 | balance: string; 25 | capacity: string; 26 | }[] => { 27 | if (!info?.channels?.length) return []; 28 | return info.channels.map(c => { 29 | const heldAmount = c.pending_payments.reduce((p, pp) => { 30 | if (!pp) return p; 31 | if (!pp.is_outgoing) return p; 32 | return p + pp.tokens; 33 | }, 0); 34 | 35 | return { 36 | chan_id: c.id, 37 | balance: (c.local_balance + heldAmount).toString(), 38 | capacity: c.capacity + '', 39 | }; 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /src/server/modules/security/security.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { PassportModule } from '@nestjs/passport'; 4 | import { APP_GUARD } from '@nestjs/core'; 5 | import { JwtStrategy } from './jwt.strategy'; 6 | import { RolesGuard } from './guards/roles.guard'; 7 | import { GqlAuthGuard } from './guards/graphql.guard'; 8 | import { GqlThrottlerGuard as ThrottlerGuard } from './guards/throttler.guard'; 9 | import { ThrottlerModule } from '@nestjs/throttler'; 10 | 11 | @Module({ 12 | imports: [ 13 | PassportModule.register({ defaultStrategy: 'jwt', session: true }), 14 | ThrottlerModule.forRootAsync({ 15 | inject: [ConfigService], 16 | useFactory: (config: ConfigService) => [ 17 | { 18 | ttl: config.get('throttler.ttl'), 19 | limit: config.get('throttler.limit') * 1000, 20 | }, 21 | ], 22 | }), 23 | ], 24 | 25 | providers: [ 26 | JwtStrategy, 27 | { provide: APP_GUARD, useClass: GqlAuthGuard }, 28 | { provide: APP_GUARD, useClass: RolesGuard }, 29 | { provide: APP_GUARD, useClass: ThrottlerGuard }, 30 | ], 31 | }) 32 | export class AuthenticationModule {} 33 | -------------------------------------------------------------------------------- /src/client/pages/chain.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GridWrapper } from '../src/components/gridWrapper/GridWrapper'; 3 | import { NextPageContext } from 'next'; 4 | import { getProps } from '../src/utils/ssr'; 5 | import { ChainTransactions } from '../src/views/chain/transactions/ChainTransactions'; 6 | import { ChainUtxos } from '../src/views/chain/utxos/ChainUtxos'; 7 | import { 8 | Card, 9 | CardWithTitle, 10 | SubTitle, 11 | } from '../src/components/generic/Styled'; 12 | 13 | const ChainView = () => { 14 | return ( 15 | <> 16 | 17 | Chain Utxos 18 | 19 | 20 | 21 | 22 | 23 | Chain Transactions 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | const Wrapped = () => ( 33 | 34 | 35 | 36 | ); 37 | 38 | export default Wrapped; 39 | 40 | export async function getServerSideProps(context: NextPageContext) { 41 | return await getProps(context); 42 | } 43 | -------------------------------------------------------------------------------- /src/client/src/components/satoshi/Satoshi.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | 3 | export const SatoshiSymbol = forwardRef( 4 | ({ color = 'currentColor', size = 16, ...rest }, ref) => { 5 | return ( 6 | 14 | 15 | 16 | 23 | 30 | 37 | 38 | ); 39 | } 40 | ); 41 | 42 | SatoshiSymbol.displayName = 'AmbossLogo'; 43 | -------------------------------------------------------------------------------- /src/client/src/views/stats/styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { DarkSubTitle } from '../../components/generic/Styled'; 3 | import { chartColors, mediaWidths } from '../../styles/Themes'; 4 | 5 | export const ScoreLine = styled.div` 6 | display: flex; 7 | justify-content: space-between; 8 | width: 160px; 9 | 10 | @media (${mediaWidths.mobile}) { 11 | margin-top: 8px; 12 | width: 100%; 13 | } 14 | `; 15 | 16 | type StatHeaderProps = { 17 | isOpen?: boolean; 18 | }; 19 | 20 | export const StatHeaderLine = styled.div` 21 | cursor: pointer; 22 | display: flex; 23 | padding: 8px 0 16px; 24 | margin-bottom: ${({ isOpen }) => (isOpen ? 0 : '-8px')}; 25 | justify-content: space-between; 26 | align-items: center; 27 | `; 28 | 29 | export const StatsTitle = styled.div` 30 | font-size: 24px; 31 | width: 100%; 32 | text-align: center; 33 | `; 34 | 35 | type WarningProps = { 36 | warningColor?: string; 37 | }; 38 | 39 | export const WarningText = styled(DarkSubTitle)` 40 | width: 100%; 41 | text-align: center; 42 | color: ${({ warningColor }) => 43 | warningColor ? warningColor : chartColors.orange}; 44 | `; 45 | 46 | export const Clickable = styled.div` 47 | cursor: pointer; 48 | `; 49 | -------------------------------------------------------------------------------- /src/client/src/views/settings/WidgetRow.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { 3 | MultiButton, 4 | SingleButton, 5 | } from '../../components/buttons/multiButton/MultiButton'; 6 | import { DarkSubTitle } from '../../components/generic/Styled'; 7 | import styled from 'styled-components'; 8 | import { NormalizedWidgets } from './DashPanel'; 9 | 10 | const S = { 11 | line: styled.div` 12 | margin-bottom: 8px; 13 | display: flex; 14 | justify-content: space-between; 15 | align-items: center; 16 | `, 17 | }; 18 | 19 | type WidgetRowParams = { 20 | widget: NormalizedWidgets; 21 | handleAdd: (id: number) => void; 22 | handleDelete: (id: number) => void; 23 | }; 24 | 25 | export const WidgetRow: FC = ({ 26 | widget, 27 | handleAdd, 28 | handleDelete, 29 | }) => ( 30 | 31 | {widget.name} 32 | 33 | handleAdd(widget.id)} 36 | > 37 | Show 38 | 39 | handleDelete(widget.id)} 42 | > 43 | Hide 44 | 45 | 46 | 47 | ); 48 | -------------------------------------------------------------------------------- /src/server/modules/api/macaroon/macaroon.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver } from '@nestjs/graphql'; 2 | import { NodeService } from '../../node/node.service'; 3 | import { UserId } from '../../security/security.types'; 4 | import { Inject } from '@nestjs/common'; 5 | import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; 6 | import { Logger } from 'winston'; 7 | import { CreateMacaroon, NetworkInfoInput } from './macaroon.types'; 8 | import { CurrentUser } from '../../security/security.decorators'; 9 | 10 | @Resolver() 11 | export class MacaroonResolver { 12 | constructor( 13 | private nodeService: NodeService, 14 | @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger 15 | ) {} 16 | 17 | @Mutation(() => CreateMacaroon) 18 | async createMacaroon( 19 | @Args('permissions') permissions: NetworkInfoInput, 20 | @CurrentUser() { id }: UserId 21 | ) { 22 | const { macaroon, permissions: permissionList } = 23 | await this.nodeService.grantAccess(id, permissions); 24 | 25 | this.logger.debug('Macaroon created with the following permissions', { 26 | permissions: permissionList.join(', '), 27 | }); 28 | 29 | const hex = Buffer.from(macaroon, 'base64').toString('hex'); 30 | 31 | return { base: macaroon, hex }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/client/src/views/chat/helpers/chatHelpers.ts: -------------------------------------------------------------------------------- 1 | const formatMessage = (message: string, type: string): [string, number] => { 2 | const newMessage = message.replace(type, '').trim(); 3 | const split = newMessage.split(' ').filter(t => t); 4 | 5 | let amount = 0; 6 | 7 | if (split.length > 0) { 8 | amount = Number(split[0]); 9 | } else { 10 | return ['', 0]; 11 | } 12 | 13 | if (isNaN(amount)) { 14 | return ['', 0]; 15 | } 16 | 17 | return [newMessage.replace(`${amount}`, '').trim(), amount]; 18 | }; 19 | 20 | export const handleMessage = ( 21 | message: string 22 | ): [string, string, number, boolean] => { 23 | if (message.indexOf('/pay') === 0) { 24 | const [finalMessage, amount] = formatMessage(message, '/pay'); 25 | 26 | if (finalMessage === '' && amount === 0) { 27 | return ['', '', 0, false]; 28 | } 29 | 30 | return [finalMessage || 'payment', 'payment', amount, true]; 31 | } 32 | if (message.indexOf('/request') === 0) { 33 | const [finalMessage, amount] = formatMessage(message, '/request'); 34 | 35 | if (finalMessage === '' && amount === 0) { 36 | return ['', '', 0, false]; 37 | } 38 | return [finalMessage || 'paymentrequest', 'paymentrequest', amount, true]; 39 | } 40 | return [message, '', 0, true]; 41 | }; 42 | -------------------------------------------------------------------------------- /src/client/pages/rebalance.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GridWrapper } from '../src/components/gridWrapper/GridWrapper'; 3 | import { 4 | CardWithTitle, 5 | SingleLine, 6 | SubTitle, 7 | } from '../src/components/generic/Styled'; 8 | import { AdvancedBalance } from '../src/views/balance/AdvancedBalance'; 9 | import { NextPageContext } from 'next'; 10 | import { getProps } from '../src/utils/ssr'; 11 | import { HelpCircle } from 'react-feather'; 12 | import styled from 'styled-components'; 13 | import { chartColors } from '../src/styles/Themes'; 14 | 15 | const Button = styled.a` 16 | cursor: pointer; 17 | `; 18 | 19 | const BalanceView = () => ( 20 | 21 | 22 | Rebalance 23 | 29 | 30 | 31 | 32 | ); 33 | 34 | const Wrapped = () => ( 35 | 36 | 37 | 38 | ); 39 | 40 | export default Wrapped; 41 | 42 | export async function getServerSideProps(context: NextPageContext) { 43 | return await getProps(context); 44 | } 45 | -------------------------------------------------------------------------------- /src/client/src/hooks/UseEventListener.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect, RefObject } from 'react'; 2 | 3 | function useEventListener( 4 | eventName: string, 5 | handler: (event: Event) => void, 6 | element?: RefObject 7 | ) { 8 | // Create a ref that stores handler 9 | const savedHandler = useRef<(event: Event) => void>(); 10 | 11 | useEffect(() => { 12 | // Define the listening target 13 | const targetElement: T | Window = element?.current || window; 14 | if (!(targetElement && targetElement.addEventListener)) { 15 | return; 16 | } 17 | 18 | // Update saved handler if necessary 19 | if (savedHandler.current !== handler) { 20 | savedHandler.current = handler; 21 | } 22 | 23 | // Create event listener that calls handler function stored in ref 24 | const eventListener = (event: Event) => { 25 | // eslint-disable-next-line no-extra-boolean-cast 26 | if (!!savedHandler?.current) { 27 | savedHandler.current(event); 28 | } 29 | }; 30 | 31 | targetElement.addEventListener(eventName, eventListener); 32 | 33 | return () => { 34 | targetElement.removeEventListener(eventName, eventListener); 35 | }; 36 | }, [eventName, element, handler]); 37 | } 38 | 39 | export default useEventListener; 40 | -------------------------------------------------------------------------------- /src/client/src/views/dashboard/widgets/lightning/info.tsx: -------------------------------------------------------------------------------- 1 | import { useGetLiquidReportQuery } from '../../../../graphql/queries/__generated__/getChannelReport.generated'; 2 | import { useNodeInfo } from '../../../../hooks/UseNodeInfo'; 3 | import styled from 'styled-components'; 4 | 5 | const S = { 6 | wrapper: styled.div` 7 | overflow: auto; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | width: 100%; 13 | height: 100%; 14 | `, 15 | title: styled.h2` 16 | margin: 0; 17 | `, 18 | }; 19 | 20 | export const AliasWidget = () => { 21 | const { alias } = useNodeInfo(); 22 | 23 | return ( 24 | 25 | {alias} 26 | 27 | ); 28 | }; 29 | 30 | export const BalanceWidget = () => { 31 | const { data } = useGetLiquidReportQuery({ errorPolicy: 'ignore' }); 32 | 33 | if (!data?.getChannelReport) { 34 | return ( 35 | 36 | - 37 | 38 | ); 39 | } 40 | 41 | const { local, remote } = data.getChannelReport; 42 | 43 | const balance = Math.round(((local || 0) / (remote || 1)) * 100); 44 | 45 | return ( 46 | 47 | {`${balance}%`} 48 | 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/client/pages/stats.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { GridWrapper } from '../src/components/gridWrapper/GridWrapper'; 4 | import { VolumeStats } from '../src/views/stats/FlowStats'; 5 | import { TimeStats } from '../src/views/stats/TimeStats'; 6 | import { FeeStats } from '../src/views/stats/FeeStats'; 7 | import { StatResume } from '../src/views/stats/StatResume'; 8 | import { StatsProvider } from '../src/views/stats/context'; 9 | import { NextPageContext } from 'next'; 10 | import { getProps } from '../src/utils/ssr'; 11 | import { SingleLine } from '../src/components/generic/Styled'; 12 | 13 | export const ButtonRow = styled.div` 14 | width: auto; 15 | display: flex; 16 | `; 17 | 18 | export const SettingsLine = styled(SingleLine)` 19 | margin: 8px 0; 20 | `; 21 | 22 | const StatsView = () => { 23 | return ( 24 | <> 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | const Wrapped = () => ( 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | 41 | export default Wrapped; 42 | 43 | export async function getServerSideProps(context: NextPageContext) { 44 | return await getProps(context); 45 | } 46 | -------------------------------------------------------------------------------- /src/client/src/views/home/account/createInvoice/Timer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { DarkSubTitle } from '../../../../components/generic/Styled'; 3 | import styled from 'styled-components'; 4 | 5 | const Wrapper = styled(DarkSubTitle)` 6 | width: 100%; 7 | text-align: center; 8 | `; 9 | 10 | type TimerProps = { 11 | initialMinute: number; 12 | initialSeconds: number; 13 | }; 14 | 15 | export const Timer: React.FC = ({ 16 | initialMinute, 17 | initialSeconds, 18 | }) => { 19 | const [minutes, setMinutes] = useState(initialMinute); 20 | const [seconds, setSeconds] = useState(initialSeconds); 21 | 22 | useEffect(() => { 23 | const myInterval = setInterval(() => { 24 | if (seconds > 0) { 25 | setSeconds(seconds - 1); 26 | } 27 | if (seconds === 0) { 28 | if (minutes === 0) { 29 | clearInterval(myInterval); 30 | } else { 31 | setMinutes(minutes - 1); 32 | setSeconds(59); 33 | } 34 | } 35 | }, 1000); 36 | return () => { 37 | clearInterval(myInterval); 38 | }; 39 | }); 40 | 41 | return minutes === 0 && seconds === 0 ? null : ( 42 | 43 | {`Will disappear in ${minutes}:${seconds < 10 ? `0${seconds}` : seconds}`} 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/client/src/utils/ssr.ts: -------------------------------------------------------------------------------- 1 | import { NextPageContext } from 'next'; 2 | import { parseCookies } from '../utils/cookies'; 3 | import { appConstants } from './appConstants'; 4 | 5 | const cookieProps = ( 6 | context: NextPageContext, 7 | noAuth?: boolean 8 | ): { 9 | theme: string; 10 | authenticated: boolean; 11 | hasToken: boolean; 12 | authToken: string; 13 | } => { 14 | if (!context?.req) 15 | return { 16 | theme: 'dark', 17 | authenticated: false, 18 | hasToken: false, 19 | authToken: '', 20 | }; 21 | 22 | const cookies = parseCookies(context.req); 23 | 24 | const hasToken = !!cookies[appConstants.tokenCookieName]; 25 | 26 | if (!cookies[appConstants.cookieName] && !noAuth) { 27 | return { theme: 'dark', authenticated: false, hasToken, authToken: '' }; 28 | } 29 | 30 | return { 31 | theme: cookies?.theme ? cookies.theme : 'dark', 32 | authenticated: true, 33 | hasToken, 34 | authToken: cookies[appConstants.cookieName] || '', 35 | }; 36 | }; 37 | 38 | export const getProps = async (context: NextPageContext, noAuth?: boolean) => { 39 | const { theme, authenticated, hasToken, authToken } = cookieProps( 40 | context, 41 | noAuth 42 | ); 43 | 44 | return { 45 | props: { initialConfig: { theme }, hasToken, authenticated, authToken }, 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /src/server/utils/crypto.ts: -------------------------------------------------------------------------------- 1 | import { createHash, randomBytes } from 'crypto'; 2 | import AES from 'crypto-js/aes'; 3 | import Utf8 from 'crypto-js/enc-utf8'; 4 | import bcrypt from 'bcryptjs'; 5 | import { PRE_PASS_STRING } from '../modules/files/files.service'; 6 | 7 | export const getPreimageAndHash = () => { 8 | const preimage = randomBytes(32); 9 | const preimageHash = getSHA256Hash(preimage); 10 | 11 | return { preimage, hash: preimageHash }; 12 | }; 13 | 14 | export const getSHA256Hash = ( 15 | str: string | Buffer, 16 | encoding: 'hex' | 'base64' = 'hex' 17 | ) => createHash('sha256').update(str).digest().toString(encoding); 18 | 19 | export const decodeMacaroon = (macaroon: string, password: string) => { 20 | try { 21 | return AES.decrypt(macaroon, password).toString(Utf8); 22 | } catch (error: any) { 23 | console.log(`Error decoding macaroon with password: ${password}`); 24 | throw new Error('WrongPasswordForLogin'); 25 | } 26 | }; 27 | 28 | export const hashPassword = (password: string): string => 29 | `${PRE_PASS_STRING}${bcrypt.hashSync(password, 12)}`; 30 | 31 | export const isCorrectPassword = ( 32 | password: string, 33 | correctPassword: string 34 | ): boolean => { 35 | const cleanPassword = correctPassword.replace(PRE_PASS_STRING, ''); 36 | return bcrypt.compareSync(password, cleanPassword); 37 | }; 38 | -------------------------------------------------------------------------------- /src/client/src/views/balance/Balance.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { SubCard, SingleLine } from '../../components/generic/Styled'; 3 | import { mediaWidths, themeColors } from '../../styles/Themes'; 4 | 5 | export const FullWidthSubCard = styled(SubCard)` 6 | width: 100%; 7 | align-self: stretch; 8 | `; 9 | 10 | export const WithSpaceSubCard = styled(FullWidthSubCard)` 11 | margin-right: 12px; 12 | 13 | @media (${mediaWidths.mobile}) { 14 | margin-right: 0; 15 | } 16 | `; 17 | 18 | export const RebalanceTitle = styled.div` 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | width: 100%; 23 | margin-bottom: 16px; 24 | `; 25 | 26 | export const RebalanceTag = styled.div` 27 | padding: 2px 8px; 28 | border: 1px solid ${themeColors.blue2}; 29 | border-radius: 4px; 30 | margin-right: 8px; 31 | font-size: 14px; 32 | 33 | @media (${mediaWidths.mobile}) { 34 | max-width: 80px; 35 | overflow: hidden; 36 | white-space: nowrap; 37 | text-overflow: ellipsis; 38 | } 39 | `; 40 | 41 | export const RebalanceLine = styled(SingleLine)` 42 | margin-bottom: 8px; 43 | `; 44 | 45 | export const RebalanceWrapLine = styled(SingleLine)` 46 | flex-wrap: wrap; 47 | `; 48 | 49 | export const RebalanceSubTitle = styled.div` 50 | white-space: nowrap; 51 | font-size: 14px; 52 | `; 53 | -------------------------------------------------------------------------------- /src/server/modules/api/macaroon/macaroon.types.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType, ObjectType } from '@nestjs/graphql'; 2 | 3 | @InputType() 4 | export class NetworkInfoInput { 5 | @Field() 6 | is_ok_to_adjust_peers: boolean; 7 | @Field() 8 | is_ok_to_create_chain_addresses: boolean; 9 | @Field() 10 | is_ok_to_create_invoices: boolean; 11 | @Field() 12 | is_ok_to_create_macaroons: boolean; 13 | @Field() 14 | is_ok_to_derive_keys: boolean; 15 | @Field() 16 | is_ok_to_get_access_ids: boolean; 17 | @Field() 18 | is_ok_to_get_chain_transactions: boolean; 19 | @Field() 20 | is_ok_to_get_invoices: boolean; 21 | @Field() 22 | is_ok_to_get_wallet_info: boolean; 23 | @Field() 24 | is_ok_to_get_payments: boolean; 25 | @Field() 26 | is_ok_to_get_peers: boolean; 27 | @Field() 28 | is_ok_to_pay: boolean; 29 | @Field() 30 | is_ok_to_revoke_access_ids: boolean; 31 | @Field() 32 | is_ok_to_send_to_chain_addresses: boolean; 33 | @Field() 34 | is_ok_to_sign_bytes: boolean; 35 | @Field() 36 | is_ok_to_sign_messages: boolean; 37 | @Field() 38 | is_ok_to_stop_daemon: boolean; 39 | @Field() 40 | is_ok_to_verify_bytes_signatures: boolean; 41 | @Field() 42 | is_ok_to_verify_messages: boolean; 43 | } 44 | 45 | @ObjectType() 46 | export class CreateMacaroon { 47 | @Field() 48 | base: string; 49 | @Field() 50 | hex: string; 51 | } 52 | -------------------------------------------------------------------------------- /src/client/src/components/bitcoinInfo/BitcoinPrice.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useGetBitcoinPriceQuery } from '../../../src/graphql/queries/__generated__/getBitcoinPrice.generated'; 3 | import { usePriceDispatch } from '../../context/PriceContext'; 4 | import { useConfigState } from '../../context/ConfigContext'; 5 | 6 | export const BitcoinPrice: React.FC = () => { 7 | const { fetchPrices } = useConfigState(); 8 | const setPrices = usePriceDispatch(); 9 | const { loading, data, stopPolling } = useGetBitcoinPriceQuery({ 10 | ssr: false, 11 | skip: !fetchPrices, 12 | fetchPolicy: 'network-only', 13 | onError: () => { 14 | setPrices({ type: 'dontShow' }); 15 | stopPolling(); 16 | }, 17 | pollInterval: 60000, 18 | }); 19 | 20 | useEffect(() => { 21 | if (!fetchPrices) { 22 | setPrices({ type: 'dontShow' }); 23 | } 24 | }, [fetchPrices, setPrices]); 25 | 26 | useEffect(() => { 27 | if (!loading && data && data.getBitcoinPrice && fetchPrices) { 28 | try { 29 | const prices = JSON.parse(data.getBitcoinPrice); 30 | setPrices({ type: 'fetched', state: { prices } }); 31 | } catch (error: any) { 32 | setPrices({ type: 'dontShow' }); 33 | stopPolling(); 34 | } 35 | } 36 | }, [data, loading, setPrices, stopPolling, fetchPrices]); 37 | 38 | return null; 39 | }; 40 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/bosRebalance.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const BOS_REBALANCE = gql` 4 | mutation BosRebalance( 5 | $avoid: [String!] 6 | $in_through: String 7 | $max_fee: Float 8 | $max_fee_rate: Float 9 | $max_rebalance: Float 10 | $timeout_minutes: Float 11 | $node: String 12 | $out_through: String 13 | $out_inbound: Float 14 | ) { 15 | bosRebalance( 16 | avoid: $avoid 17 | in_through: $in_through 18 | max_fee: $max_fee 19 | max_fee_rate: $max_fee_rate 20 | max_rebalance: $max_rebalance 21 | timeout_minutes: $timeout_minutes 22 | node: $node 23 | out_through: $out_through 24 | out_inbound: $out_inbound 25 | ) { 26 | increase { 27 | increased_inbound_on 28 | liquidity_inbound 29 | liquidity_inbound_opening 30 | liquidity_inbound_pending 31 | liquidity_outbound 32 | liquidity_outbound_opening 33 | liquidity_outbound_pending 34 | } 35 | decrease { 36 | decreased_inbound_on 37 | liquidity_inbound 38 | liquidity_inbound_opening 39 | liquidity_inbound_pending 40 | liquidity_outbound 41 | liquidity_outbound_opening 42 | liquidity_outbound_pending 43 | } 44 | result { 45 | rebalanced 46 | rebalance_fees_spent 47 | } 48 | } 49 | } 50 | `; 51 | -------------------------------------------------------------------------------- /src/client/next.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const path = require('path'); 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires 4 | const dotenv = require('dotenv'); 5 | 6 | dotenv.config({ path: path.resolve(process.cwd(), '.env.local') }); 7 | dotenv.config({ path: path.resolve(process.cwd(), '.env') }); 8 | 9 | module.exports = { 10 | reactStrictMode: true, 11 | poweredByHeader: false, 12 | basePath: process.env.BASE_PATH || '', 13 | transpilePackages: ['echarts', 'zrender'], 14 | compiler: { 15 | styledComponents: true, 16 | }, 17 | publicRuntimeConfig: { 18 | mempoolUrl: process.env.MEMPOOL_URL || 'https://mempool.space', 19 | disable2FA: process.env.DISABLE_TWOFA === 'true', 20 | apiUrl: `${process.env.BASE_PATH || ''}/graphql`, 21 | basePath: process.env.BASE_PATH || '', 22 | npmVersion: process.env.npm_package_version || '0.0.0', 23 | defaultTheme: process.env.THEME || 'dark', 24 | defaultCurrency: process.env.CURRENCY || 'sat', 25 | fetchPrices: process.env.FETCH_PRICES === 'false' ? false : true, 26 | fetchFees: process.env.FETCH_FEES === 'false' ? false : true, 27 | disableLinks: process.env.DISABLE_LINKS === 'true', 28 | disableLnMarkets: process.env.DISABLE_LNMARKETS === 'true', 29 | noVersionCheck: process.env.NO_VERSION_CHECK === 'true', 30 | logoutUrl: process.env.LOGOUT_URL || '', 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/client/src/components/burgerMenu/BurgerMenu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | import { burgerColor } from '../../styles/Themes'; 4 | import { NodeInfo } from '../../layouts/navigation/nodeInfo/NodeInfo'; 5 | import { SideSettings } from '../../layouts/navigation/sideSettings/SideSettings'; 6 | import { Navigation } from '../../layouts/navigation/Navigation'; 7 | import { LogoutWrapper } from '../logoutButton'; 8 | import { ColorButton } from '../buttons/colorButton/ColorButton'; 9 | 10 | type StyledProps = { 11 | open: boolean; 12 | }; 13 | 14 | const StyledBurger = styled.div` 15 | padding: 16px 16px 0; 16 | background-color: ${burgerColor}; 17 | box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.1); 18 | ${({ open }) => 19 | open && 20 | css` 21 | margin-bottom: 16px; 22 | `} 23 | `; 24 | 25 | interface BurgerProps { 26 | open: boolean; 27 | setOpen: (state: boolean) => void; 28 | } 29 | 30 | export const BurgerMenu = ({ open, setOpen }: BurgerProps) => { 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | Logout 39 | 40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/server/modules/api/chain/chain.types.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | 3 | @ObjectType() 4 | export class Utxo { 5 | @Field() 6 | address: string; 7 | @Field() 8 | address_format: string; 9 | @Field() 10 | confirmation_count: number; 11 | @Field() 12 | output_script: string; 13 | @Field() 14 | tokens: number; 15 | @Field() 16 | transaction_id: string; 17 | @Field() 18 | transaction_vout: number; 19 | } 20 | 21 | @ObjectType() 22 | export class ChainTransaction { 23 | @Field({ nullable: true }) 24 | block_id?: string; 25 | @Field({ nullable: true }) 26 | confirmation_count?: number; 27 | @Field({ nullable: true }) 28 | confirmation_height?: number; 29 | @Field() 30 | created_at: string; 31 | @Field({ nullable: true }) 32 | description?: string; 33 | @Field({ nullable: true }) 34 | fee?: number; 35 | @Field() 36 | id: string; 37 | @Field() 38 | is_confirmed: boolean; 39 | @Field() 40 | is_outgoing: boolean; 41 | @Field(() => [String]) 42 | output_addresses: string[]; 43 | @Field() 44 | tokens: number; 45 | @Field({ nullable: true }) 46 | transaction?: string; 47 | } 48 | 49 | @ObjectType() 50 | export class ChainAddressSend { 51 | @Field() 52 | confirmationCount: number; 53 | @Field() 54 | id: string; 55 | @Field() 56 | isConfirmed: boolean; 57 | @Field() 58 | isOutgoing: boolean; 59 | @Field({ nullable: true }) 60 | tokens: number; 61 | } 62 | -------------------------------------------------------------------------------- /src/client/src/views/home/quickActions/decode/Decode.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | Card, 4 | Sub4Title, 5 | ResponsiveLine, 6 | } from '../../../../components/generic/Styled'; 7 | import { ColorButton } from '../../../../components/buttons/colorButton/ColorButton'; 8 | import { Input } from '../../../../components/input'; 9 | import { Decoded } from './Decoded'; 10 | 11 | export const DecodeCard = () => { 12 | const [request, setRequest] = useState(''); 13 | const [show, setShow] = useState(false); 14 | 15 | return ( 16 | 17 | {!show && ( 18 | 19 | Request: 20 | setRequest(e.target.value)} 26 | /> 27 | { 34 | setShow(true); 35 | }} 36 | > 37 | Decode 38 | 39 | 40 | )} 41 | {show && } 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/client/src/views/home/quickActions/donate/DonateCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Heart } from 'react-feather'; 3 | import styled from 'styled-components'; 4 | import { 5 | chartColors, 6 | cardColor, 7 | cardBorderColor, 8 | unSelectedNavButton, 9 | mediaWidths, 10 | } from '../../../../styles/Themes'; 11 | 12 | const QuickTitle = styled.div` 13 | font-size: 12px; 14 | color: ${unSelectedNavButton}; 15 | margin-top: 10px; 16 | `; 17 | 18 | const QuickCard = styled.div` 19 | background: ${cardColor}; 20 | box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.1); 21 | border-radius: 4px; 22 | border: 1px solid ${cardBorderColor}; 23 | height: 100px; 24 | width: 100px; 25 | display: flex; 26 | flex-direction: column; 27 | justify-content: center; 28 | align-items: center; 29 | padding: 10px; 30 | cursor: pointer; 31 | color: #69c0ff; 32 | 33 | @media (${mediaWidths.mobile}) { 34 | padding: 4px; 35 | height: 80px; 36 | width: 80px; 37 | } 38 | 39 | &:hover { 40 | background-color: ${chartColors.green}; 41 | color: white; 42 | 43 | & ${QuickTitle} { 44 | color: white; 45 | } 46 | } 47 | `; 48 | 49 | type SupportCardProps = { 50 | callback: () => void; 51 | }; 52 | 53 | export const SupportCard = ({ callback }: SupportCardProps) => { 54 | return ( 55 | 56 | 57 | Donate 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /src/client/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Document, { 3 | DocumentContext, 4 | Html, 5 | Head, 6 | Main, 7 | NextScript, 8 | } from 'next/document'; 9 | import { ServerStyleSheet } from 'styled-components'; 10 | 11 | export default class MyDocument extends Document { 12 | static async getInitialProps(ctx: DocumentContext) { 13 | const sheet = new ServerStyleSheet(); 14 | const originalRenderPage = ctx.renderPage; 15 | 16 | try { 17 | ctx.renderPage = () => 18 | originalRenderPage({ 19 | // eslint-disable-next-line react/display-name 20 | enhanceApp: App => props => sheet.collectStyles(), 21 | }); 22 | 23 | const initialProps = await Document.getInitialProps(ctx); 24 | return { 25 | ...initialProps, 26 | styles: ( 27 | <> 28 | {initialProps.styles} 29 | {sheet.getStyleElement()} 30 | 31 | ), 32 | }; 33 | } finally { 34 | sheet.seal(); 35 | } 36 | } 37 | 38 | render() { 39 | return ( 40 | 41 | 42 | 47 | 48 | 49 |
50 | 51 | 52 | 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/client/src/utils/chat.ts: -------------------------------------------------------------------------------- 1 | import { sortBy, groupBy } from 'lodash'; 2 | import { Message } from '../graphql/types'; 3 | 4 | export const separateBySender = (chats: Message[]) => { 5 | return groupBy(chats, 'sender'); 6 | }; 7 | 8 | export const getSenders = ( 9 | bySender: ReturnType 10 | ): Message[] => { 11 | const senders: Message[] = []; 12 | for (const key in bySender) { 13 | if (Object.prototype.hasOwnProperty.call(bySender, key)) { 14 | const messages = bySender[key]; 15 | const sorted: Message[] = sortBy(messages, 'date').reverse(); 16 | 17 | if (sorted.length > 0) { 18 | const chat = sorted[0]; 19 | if (chat?.sender) { 20 | senders.push(chat); 21 | } 22 | } 23 | } 24 | } 25 | return senders; 26 | }; 27 | 28 | export const getSubMessage = ( 29 | contentType: string | null, 30 | message: string | null, 31 | tokens: number | null, 32 | isSent: boolean 33 | ): string => { 34 | if (!contentType) return ''; 35 | if (!message && !tokens) return ''; 36 | switch (contentType) { 37 | case 'payment': 38 | if (isSent) { 39 | return `Sent ${tokens} sats`; 40 | } 41 | return `Received ${tokens} sats`; 42 | case 'paymentrequest': 43 | if (isSent) { 44 | return `You requested ${tokens} sats`; 45 | } 46 | return `Requested ${tokens} sats from you`; 47 | default: 48 | if (message) return message; 49 | return ''; 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /src/client/src/views/home/reports/mempool/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardWithTitle, 4 | SubTitle, 5 | } from '../../../../components/generic/Styled'; 6 | import Table from '../../../../components/table'; 7 | import { useBitcoinFees } from '../../../../hooks/UseBitcoinFees'; 8 | 9 | export const MempoolReport = () => { 10 | const { fast, halfHour, hour, minimum, dontShow } = useBitcoinFees(); 11 | 12 | if (dontShow) { 13 | return null; 14 | } 15 | 16 | const columns = [ 17 | { 18 | header: 'Fastest', 19 | accessorKey: 'fast', 20 | cell: ({ cell }: any) => cell.renderValue(), 21 | }, 22 | { 23 | header: 'Half Hour', 24 | accessorKey: 'halfHour', 25 | cell: ({ cell }: any) => cell.renderValue(), 26 | }, 27 | { 28 | header: 'Hour', 29 | accessorKey: 'hour', 30 | cell: ({ cell }: any) => cell.renderValue(), 31 | }, 32 | { 33 | header: 'Minimum', 34 | accessorKey: 'minimum', 35 | cell: ({ cell }: any) => cell.renderValue(), 36 | }, 37 | ]; 38 | 39 | const data = [ 40 | { 41 | fast: `${fast} sat/vB`, 42 | halfHour: `${halfHour} sat/vB`, 43 | hour: `${hour} sat/vB`, 44 | minimum: `${minimum} sat/vB`, 45 | }, 46 | ]; 47 | 48 | return ( 49 | 50 | Mempool Fees 51 | 52 |
53 | 54 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/client/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GridWrapper } from '../src/components/gridWrapper/GridWrapper'; 3 | import { Version } from '../src/components/version/Version'; 4 | import { NextPageContext } from 'next'; 5 | import { getProps } from '../src/utils/ssr'; 6 | import { MempoolReport } from '../src/views/home/reports/mempool'; 7 | import { LiquidityGraph } from '../src/views/home/reports/liquidReport/LiquidityGraph'; 8 | import { AccountButtons } from '../src/views/home/account/AccountButtons'; 9 | import { AccountInfo } from '../src/views/home/account/AccountInfo'; 10 | import { QuickActions } from '../src/views/home/quickActions/QuickActions'; 11 | import { FlowBox } from '../src/views/home/reports/flow'; 12 | import { ForwardBox } from '../src/views/home/reports/forwardReport'; 13 | import { ConnectCard } from '../src/views/home/connect/Connect'; 14 | import { Liquidity } from '../src/views/home/liquidity/Liquidity'; 15 | 16 | const HomeView = () => ( 17 | <> 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | 31 | const Wrapped = () => ( 32 | 33 | 34 | 35 | ); 36 | 37 | export default Wrapped; 38 | 39 | export async function getServerSideProps(context: NextPageContext) { 40 | return await getProps(context); 41 | } 42 | -------------------------------------------------------------------------------- /src/client/src/components/checkbox/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import styled from 'styled-components'; 3 | import { 4 | colorButtonBackground, 5 | buttonBorderColor, 6 | themeColors, 7 | } from '../../styles/Themes'; 8 | 9 | const StyledContainer = styled.div` 10 | display: flex; 11 | justify-content: flex-start; 12 | align-items: center; 13 | padding-right: 32px; 14 | cursor: pointer; 15 | `; 16 | 17 | const FixedWidth = styled.div` 18 | height: 18px; 19 | width: 18px; 20 | margin: 0px; 21 | margin-right: 8px; 22 | `; 23 | 24 | const StyledCheckbox = styled.div<{ checked: boolean }>` 25 | height: 16px; 26 | width: 16px; 27 | margin: 0; 28 | border: 1px solid ${buttonBorderColor}; 29 | border-radius: 4px; 30 | outline: none; 31 | transition-duration: 0.3s; 32 | background-color: ${colorButtonBackground}; 33 | box-sizing: border-box; 34 | border-radius: 50%; 35 | 36 | ${({ checked }) => checked && `background-color: ${themeColors.blue2}`} 37 | `; 38 | 39 | type CheckboxProps = { 40 | checked: boolean; 41 | onChange: (state: boolean) => void; 42 | children?: ReactNode; 43 | }; 44 | 45 | export const Checkbox: React.FC = ({ 46 | children, 47 | checked, 48 | onChange, 49 | }) => { 50 | return ( 51 | onChange(!checked)}> 52 | 53 | 54 | 55 | {children} 56 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/client/pages/amboss/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GridWrapper } from '../../src/components/gridWrapper/GridWrapper'; 3 | import { SingleLine } from '../../src/components/generic/Styled'; 4 | import { NextPageContext } from 'next'; 5 | import { getProps } from '../../src/utils/ssr'; 6 | import { AmbossLoginButton } from '../../src/views/amboss/LoginButton'; 7 | import { Backups } from '../../src/views/amboss/Backups'; 8 | import { SectionTitle, Text } from '../../src/components/typography/Styled'; 9 | import { Healthchecks } from '../../src/views/amboss/Healthchecks'; 10 | import { Balances } from '../../src/views/amboss/Balances'; 11 | import { Billboard } from '../../src/views/amboss/Billboard'; 12 | 13 | const AmbossView = () => ( 14 | <> 15 | 16 | 17 | AMBOSS 18 | 19 | 20 | 21 | 22 | Amboss offers different integration options that can help you monitor your 23 | node, store backups and get historical graphs about your balances. 24 | 25 | 26 | ); 27 | 28 | const Wrapped = () => ( 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | 38 | export default Wrapped; 39 | 40 | export async function getServerSideProps(context: NextPageContext) { 41 | return await getProps(context); 42 | } 43 | -------------------------------------------------------------------------------- /src/server/modules/mempool/mempool.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { FetchService } from '../fetch/fetch.service'; 3 | import { ConfigService } from '@nestjs/config'; 4 | 5 | @Injectable() 6 | export class MempoolService { 7 | constructor( 8 | private fetchService: FetchService, 9 | private config: ConfigService 10 | ) {} 11 | 12 | async getAddressTransactions(address: string): Promise< 13 | { 14 | txid: string; 15 | vin: { txid: string; prevout: { scriptpubkey_address: string } }[]; 16 | vout: { scriptpubkey_address: string }[]; 17 | }[] 18 | > { 19 | const response = await this.fetchService.fetchWithProxy( 20 | this.config.get('urls.mempool') + `/api/address/${address}/txs` 21 | ); 22 | return (await response.json()) as any; 23 | } 24 | 25 | async getTransactionHex(transactionId: string): Promise { 26 | const response = await this.fetchService.fetchWithProxy( 27 | this.config.get('urls.mempool') + `/api/tx/${transactionId}/hex` 28 | ); 29 | return (await response.text()) as string; 30 | } 31 | 32 | async broadcastTransaction(transactionHex: string): Promise { 33 | const response = await this.fetchService.fetchWithProxy( 34 | this.config.get('urls.mempool') + `/api/tx`, 35 | { 36 | method: 'POST', 37 | body: transactionHex, 38 | headers: { 'Content-Type': 'text/plain' }, 39 | } 40 | ); 41 | return (await response.text()) as string; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/client/src/components/slider/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactSlider from 'react-slider'; 2 | import { 3 | sliderBackgroundColor, 4 | sliderThumbColor, 5 | themeColors, 6 | } from '../../../src/styles/Themes'; 7 | import styled from 'styled-components'; 8 | 9 | const StyledSlider = styled(ReactSlider)` 10 | max-width: 440px; 11 | width: 100%; 12 | height: 38px; 13 | display: flex; 14 | align-items: center; 15 | outline: none; 16 | `; 17 | 18 | const StyledThumb = styled.div` 19 | height: 24px; 20 | width: 24px; 21 | background-color: ${sliderThumbColor}; 22 | color: #fff; 23 | border-radius: 50%; 24 | cursor: grab; 25 | `; 26 | 27 | const Thumb = (props: any) => ; 28 | 29 | const StyledTrack = styled.div<{ index: number }>` 30 | height: 8px; 31 | background: ${({ index }) => 32 | index === 1 ? sliderBackgroundColor : themeColors.blue2}; 33 | border-radius: 8px; 34 | `; 35 | 36 | const Track = (props: any, state: any) => ( 37 | 38 | ); 39 | 40 | type SliderProps = { 41 | value: number; 42 | max: number; 43 | min: number; 44 | onChange: (value: number) => void; 45 | }; 46 | 47 | export const Slider = ({ value, max, min, onChange }: SliderProps) => { 48 | return ( 49 | value && typeof value === 'number' && onChange(value)} 56 | /> 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/client/src/views/channels/channels/ChannelDetails.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { DarkSubTitle } from '../../../components/generic/Styled'; 3 | import { LoadingCard } from '../../../components/loading/LoadingCard'; 4 | import { ChangeDetails } from '../../../components/modal/changeDetails/ChangeDetails'; 5 | import { useGetChannelInfoQuery } from '../../../graphql/queries/__generated__/getChannel.generated'; 6 | 7 | export const ChannelDetails: FC<{ id?: string; name?: string }> = ({ 8 | id = '', 9 | name = '', 10 | }) => { 11 | const { data, loading, error } = useGetChannelInfoQuery({ 12 | variables: { id }, 13 | skip: !id, 14 | }); 15 | 16 | if (loading) { 17 | return ; 18 | } 19 | 20 | if (!data?.getChannel || error) { 21 | return ( 22 | 23 | Error getting channel information. Try refreshing the page. 24 | 25 | ); 26 | } 27 | 28 | const { transaction_id, transaction_vout, node_policies } = data.getChannel; 29 | 30 | return ( 31 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/server/modules/security/guards/roles.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; 2 | import { Reflector } from '@nestjs/core'; 3 | import { GqlExecutionContext } from '@nestjs/graphql'; 4 | import { IS_PUBLIC_KEY, Role, ROLES_KEY } from '../security.decorators'; 5 | import { UserId } from '../security.types'; 6 | 7 | @Injectable() 8 | export class RolesGuard implements CanActivate { 9 | constructor(private reflector: Reflector) {} 10 | 11 | async canActivate(ctx: ExecutionContext): Promise { 12 | const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ 13 | ctx.getHandler(), 14 | ctx.getClass(), 15 | ]); 16 | 17 | // Early return if it's a public endpoint 18 | if (isPublic) return true; 19 | 20 | const decorator = this.reflector.getAllAndOverride(ROLES_KEY, [ 21 | ctx.getHandler(), 22 | ctx.getClass(), 23 | ]); 24 | 25 | // If the endpoint does not need a role 26 | if (!decorator?.length) { 27 | return true; 28 | } 29 | 30 | const gqlCtx = GqlExecutionContext.create(ctx); 31 | const { req } = gqlCtx.getContext(); 32 | 33 | const user: UserId | undefined = req?.user; 34 | 35 | // If the endpoint needs a role but no user is found in the request 36 | if (!user) { 37 | return false; 38 | } 39 | 40 | return true; 41 | } 42 | 43 | getRequest(context: ExecutionContext) { 44 | const ctx = GqlExecutionContext.create(context); 45 | return ctx.getContext().req; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/client/src/hooks/UseCheckAuthToken.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useRouter } from 'next/router'; 3 | import { getUrlParam } from '../utils/url'; 4 | import { toast } from 'react-toastify'; 5 | import { getErrorContent } from '../utils/error'; 6 | import getConfig from 'next/config'; 7 | import { useGetAuthTokenMutation } from '../graphql/mutations/__generated__/getAuthToken.generated'; 8 | 9 | const { publicRuntimeConfig } = getConfig(); 10 | const { logoutUrl, basePath } = publicRuntimeConfig; 11 | 12 | export const useCheckAuthToken = () => { 13 | const { query } = useRouter(); 14 | 15 | const cookieParam = getUrlParam(query?.token); 16 | 17 | const [getToken, { data }] = useGetAuthTokenMutation({ 18 | variables: { cookie: cookieParam }, 19 | refetchQueries: ['GetNodeInfo'], 20 | onError: error => { 21 | toast.error(getErrorContent(error)); 22 | window.location.href = logoutUrl || `${basePath}/login`; 23 | }, 24 | }); 25 | 26 | React.useEffect(() => { 27 | if (cookieParam) { 28 | getToken(); 29 | } else { 30 | window.location.href = logoutUrl || `${basePath}/login`; 31 | } 32 | }, [cookieParam, getToken]); 33 | 34 | React.useEffect(() => { 35 | if (!cookieParam || !data) return; 36 | if (data.getAuthToken) { 37 | window.location.href = `${basePath}/`; 38 | } 39 | if (!data.getAuthToken) { 40 | toast.warning('Unable to SSO. Check your logs.'); 41 | window.location.href = logoutUrl || `${basePath}/login`; 42 | } 43 | }, [data, cookieParam]); 44 | }; 45 | -------------------------------------------------------------------------------- /src/client/src/views/home/quickActions/lightningAddress/Addresses.tsx: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from '../../../../hooks/UseLocalStorage'; 2 | import { Separation, Sub4Title } from '../../../../components/generic/Styled'; 3 | import styled from 'styled-components'; 4 | import { cardBorderColor, subCardColor } from '../../../../styles/Themes'; 5 | import { FC } from 'react'; 6 | 7 | const S = { 8 | wrapper: styled.div` 9 | display: flex; 10 | flex-wrap: wrap; 11 | `, 12 | address: styled.button` 13 | font-size: 14px; 14 | padding: 4px 8px; 15 | margin: 2px; 16 | border: 1px solid ${cardBorderColor}; 17 | background-color: ${subCardColor}; 18 | border-radius: 4px; 19 | cursor: pointer; 20 | color: inherit; 21 | 22 | :hover { 23 | background-color: ${cardBorderColor}; 24 | } 25 | `, 26 | }; 27 | 28 | type AddressProps = { 29 | handleClick: (address: string) => void; 30 | }; 31 | 32 | export const PreviousAddresses: FC = ({ handleClick }) => { 33 | const [savedAddresses] = useLocalStorage( 34 | 'saved_lightning_address', 35 | [] 36 | ); 37 | 38 | if (!savedAddresses.length) { 39 | return null; 40 | } 41 | 42 | return ( 43 | <> 44 | 45 | Previously Used Addresses: 46 | 47 | {savedAddresses.map((a, index) => ( 48 | handleClick(a)} key={`${index}${a}`}> 49 | {a} 50 | 51 | ))} 52 | 53 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /src/client/src/views/tools/backups/DownloadBackups.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { toast } from 'react-toastify'; 3 | import { useGetBackupsLazyQuery } from '../../../graphql/queries/__generated__/getBackups.generated'; 4 | import { format } from 'date-fns'; 5 | import { useNodeInfo } from '../../../hooks/UseNodeInfo'; 6 | import { DarkSubTitle, SingleLine } from '../../../components/generic/Styled'; 7 | import { saveToPc } from '../../../utils/helpers'; 8 | import { getErrorContent } from '../../../utils/error'; 9 | import { ColorButton } from '../../../components/buttons/colorButton/ColorButton'; 10 | 11 | export const DownloadBackups = () => { 12 | const [getBackups, { data, loading }] = useGetBackupsLazyQuery({ 13 | onError: error => toast.error(getErrorContent(error)), 14 | }); 15 | 16 | const { publicKey } = useNodeInfo(); 17 | 18 | useEffect(() => { 19 | if (loading || !data?.getBackups) return; 20 | 21 | const date = format(new Date(), 'ddMMyyyyhhmmss'); 22 | saveToPc(data.getBackups, `ChannelBackup-${publicKey}-${date}`); 23 | localStorage.setItem(`lastBackup-${publicKey}`, new Date().toString()); 24 | toast.success('Downloaded'); 25 | }, [data, loading, publicKey]); 26 | 27 | return ( 28 | 29 | Backup All Channels 30 | getBackups()} 34 | loading={loading} 35 | > 36 | Download 37 | 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/client/src/views/dashboard/widgets/link/index.tsx: -------------------------------------------------------------------------------- 1 | import { ColorButton } from '../../../../components/buttons/colorButton/ColorButton'; 2 | import { Link } from '../../../../components/link/Link'; 3 | import styled from 'styled-components'; 4 | 5 | const S = { 6 | wrapper: styled.div` 7 | width: 100%; 8 | overflow: hidden; 9 | `, 10 | }; 11 | 12 | export const DashSettingsLink = () => { 13 | return ( 14 | 15 | 16 | Dash Settings 17 | 18 | 19 | ); 20 | }; 21 | 22 | export const ForwardsViewLink = () => { 23 | return ( 24 | 25 | 26 | Forwards 27 | 28 | 29 | ); 30 | }; 31 | 32 | export const TransactionsViewLink = () => { 33 | return ( 34 | 35 | 36 | Transactions 37 | 38 | 39 | ); 40 | }; 41 | 42 | export const ChannelViewLink = () => { 43 | return ( 44 | 45 | 46 | Channels 47 | 48 | 49 | ); 50 | }; 51 | 52 | export const RebalanceViewLink = () => { 53 | return ( 54 | 55 | 56 | Rebalance 57 | 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /src/server/modules/api/bos/bos.types.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql'; 2 | 3 | export type RebalanceResponseType = { rebalance: [any, any, any] }; 4 | 5 | @ObjectType() 6 | class BosIncrease { 7 | @Field() 8 | increased_inbound_on: string; 9 | @Field() 10 | liquidity_inbound: string; 11 | @Field({ nullable: true }) 12 | liquidity_inbound_opening: string; 13 | @Field({ nullable: true }) 14 | liquidity_inbound_pending: string; 15 | @Field() 16 | liquidity_outbound: string; 17 | @Field({ nullable: true }) 18 | liquidity_outbound_opening: string; 19 | @Field({ nullable: true }) 20 | liquidity_outbound_pending: string; 21 | } 22 | 23 | @ObjectType() 24 | class BosDecrease { 25 | @Field() 26 | decreased_inbound_on: string; 27 | @Field() 28 | liquidity_inbound: string; 29 | @Field({ nullable: true }) 30 | liquidity_inbound_opening: string; 31 | @Field({ nullable: true }) 32 | liquidity_inbound_pending: string; 33 | @Field() 34 | liquidity_outbound: string; 35 | @Field({ nullable: true }) 36 | liquidity_outbound_opening: string; 37 | @Field({ nullable: true }) 38 | liquidity_outbound_pending: string; 39 | } 40 | 41 | @ObjectType() 42 | class BosResult { 43 | @Field() 44 | rebalanced: string; 45 | @Field() 46 | rebalance_fees_spent: string; 47 | } 48 | 49 | @ObjectType() 50 | export class BosRebalanceResult { 51 | @Field(() => BosIncrease, { nullable: true }) 52 | increase: BosIncrease; 53 | @Field(() => BosDecrease, { nullable: true }) 54 | decrease: BosDecrease; 55 | @Field(() => BosResult, { nullable: true }) 56 | result: BosResult; 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Test, lint and build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [opened, reopened, synchronize] 9 | 10 | jobs: 11 | test_lint_build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout Code 15 | uses: actions/checkout@v2 16 | 17 | - name: Cache node modules 18 | id: cache-npm 19 | uses: actions/cache@v3 20 | env: 21 | cache-name: cache-node-modules 22 | with: 23 | path: ~/.npm 24 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 25 | restore-keys: | 26 | ${{ runner.os }}-build-${{ env.cache-name }}- 27 | ${{ runner.os }}-build- 28 | ${{ runner.os }}- 29 | 30 | - if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} 31 | name: List the state of node modules 32 | continue-on-error: true 33 | run: npm list 34 | 35 | - name: Install modules 36 | run: npm ci 37 | 38 | - name: Run eslint 39 | run: npm run lint:check 40 | 41 | - name: Run tests 42 | run: npm run test 43 | 44 | - name: Setup Docker Buildx Driver 45 | id: docker_driver_setup 46 | uses: docker/setup-buildx-action@v1 47 | 48 | - name: Run docker build 49 | id: docker_build 50 | uses: docker/build-push-action@v2 51 | with: 52 | context: ./ 53 | file: ./Dockerfile 54 | push: false 55 | cache-from: type=gha 56 | cache-to: type=gha,mode=max 57 | -------------------------------------------------------------------------------- /src/client/src/components/section/Section.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | import { ThemeSet } from 'styled-theming'; 4 | import { backgroundColor, mediaWidths } from '../../styles/Themes'; 5 | 6 | interface FullWidthProps { 7 | padding?: string; 8 | withColor?: boolean; 9 | sectionColor?: string | ThemeSet; 10 | textColor?: string | ThemeSet; 11 | } 12 | 13 | const FullWidth = styled.div` 14 | width: 100%; 15 | ${({ padding }) => 16 | padding && 17 | css` 18 | padding: ${padding}; 19 | `} 20 | ${({ textColor }) => 21 | textColor && 22 | css` 23 | color: ${textColor}; 24 | `} 25 | background-color: ${({ sectionColor }) => 26 | sectionColor ? sectionColor : backgroundColor}; 27 | 28 | @media (${mediaWidths.mobile}) { 29 | padding: 16px 0; 30 | } 31 | `; 32 | 33 | const FixedWidth = styled.div` 34 | max-width: 1000px; 35 | margin: 0 auto 0; 36 | 37 | @media (max-width: 1035px) { 38 | padding: 0 16px; 39 | } 40 | `; 41 | 42 | type SectionProps = { 43 | fixedWidth?: boolean; 44 | color?: string | ThemeSet; 45 | textColor?: string | ThemeSet; 46 | padding?: string; 47 | children?: ReactNode; 48 | }; 49 | 50 | export const Section: React.FC = ({ 51 | fixedWidth = false, 52 | children, 53 | color, 54 | textColor, 55 | padding, 56 | }) => { 57 | const Fixed = fixedWidth ? FixedWidth : React.Fragment; 58 | 59 | return ( 60 | 61 | {children} 62 | 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /src/client/src/context/DashContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, createContext, useContext, useReducer } from 'react'; 2 | 3 | type State = { 4 | modalType: string; 5 | }; 6 | 7 | type ActionType = { 8 | type: 'openModal'; 9 | modalType: string; 10 | }; 11 | 12 | type Dispatch = (action: ActionType) => void; 13 | 14 | export const StateContext = createContext(undefined); 15 | export const DispatchContext = createContext(undefined); 16 | 17 | const stateReducer = (state: State, action: ActionType): State => { 18 | switch (action.type) { 19 | case 'openModal': 20 | return { ...state, modalType: action.modalType }; 21 | default: 22 | return state; 23 | } 24 | }; 25 | 26 | const DashProvider: React.FC<{ children?: ReactNode }> = ({ children }) => { 27 | const [state, dispatch] = useReducer(stateReducer, { 28 | modalType: '', 29 | }); 30 | 31 | return ( 32 | 33 | {children} 34 | 35 | ); 36 | }; 37 | 38 | const useDashState = () => { 39 | const context = useContext(StateContext); 40 | if (context === undefined) { 41 | throw new Error('useDashState must be used within a DashProvider'); 42 | } 43 | return context; 44 | }; 45 | 46 | const useDashDispatch = () => { 47 | const context = useContext(DispatchContext); 48 | if (context === undefined) { 49 | throw new Error('useDashDispatch must be used within a DashProvider'); 50 | } 51 | return context; 52 | }; 53 | 54 | export { DashProvider, useDashState, useDashDispatch }; 55 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # --------------- 2 | # Install Dependencies 3 | # --------------- 4 | FROM node:18.18.2-alpine as deps 5 | 6 | WORKDIR /app 7 | 8 | # Install dependencies neccesary for node-gyp on node alpine 9 | RUN apk add --update --no-cache \ 10 | libc6-compat \ 11 | python3 \ 12 | make \ 13 | g++ 14 | 15 | # Install app dependencies 16 | COPY package.json package-lock.json ./ 17 | RUN npm ci 18 | 19 | # --------------- 20 | # Build App 21 | # --------------- 22 | FROM deps as build 23 | 24 | WORKDIR /app 25 | 26 | # Set env variables 27 | ARG BASE_PATH="" 28 | ENV BASE_PATH=${BASE_PATH} 29 | ARG NODE_ENV="production" 30 | ENV NODE_ENV=${NODE_ENV} 31 | ENV NEXT_TELEMETRY_DISABLED=1 32 | 33 | # Build the NestJS and NextJS application 34 | COPY . . 35 | RUN npm run build:nest 36 | RUN npm run build:next 37 | 38 | # Remove non production necessary modules 39 | RUN npm prune --production 40 | 41 | # --------------- 42 | # Release App 43 | # --------------- 44 | FROM node:18.18.2-alpine as final 45 | 46 | WORKDIR /app 47 | 48 | # Set env variables 49 | ARG BASE_PATH="" 50 | ENV BASE_PATH=${BASE_PATH} 51 | ARG NODE_ENV="production" 52 | ENV NODE_ENV=${NODE_ENV} 53 | ENV NEXT_TELEMETRY_DISABLED=1 54 | 55 | COPY --from=build /app/package.json ./ 56 | COPY --from=build /app/node_modules/ ./node_modules 57 | 58 | # Copy NextJS files 59 | COPY --from=build /app/src/client/public ./src/client/public 60 | COPY --from=build /app/src/client/next.config.js ./src/client/ 61 | COPY --from=build /app/src/client/.next/ ./src/client/.next 62 | 63 | # Copy NestJS files 64 | COPY --from=build /app/dist/ ./dist 65 | 66 | EXPOSE 3000 67 | 68 | CMD [ "npm", "run", "start:prod" ] 69 | -------------------------------------------------------------------------------- /src/client/src/components/version/Version.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useGetLatestVersionQuery } from '../../../src/graphql/queries/__generated__/getLatestVersion.generated'; 3 | import getConfig from 'next/config'; 4 | import styled from 'styled-components'; 5 | import { Link } from '../link/Link'; 6 | 7 | const VersionBox = styled.div` 8 | width: 100%; 9 | text-align: center; 10 | font-size: 14px; 11 | opacity: 0.3; 12 | cursor: pointer; 13 | 14 | &:hover { 15 | opacity: 1; 16 | color: white; 17 | } 18 | `; 19 | 20 | const { publicRuntimeConfig } = getConfig(); 21 | const { npmVersion, noVersionCheck } = publicRuntimeConfig; 22 | 23 | export const Version = () => { 24 | const { data, loading, error } = useGetLatestVersionQuery({ 25 | skip: noVersionCheck, 26 | }); 27 | 28 | if (noVersionCheck) { 29 | return null; 30 | } 31 | 32 | if (error || !data || loading || !data?.getLatestVersion) { 33 | return null; 34 | } 35 | 36 | const githubVersion = data.getLatestVersion.replace('v', ''); 37 | const version = githubVersion.split('.'); 38 | const localVersion = npmVersion.split('.').map(Number); 39 | 40 | const newVersionAvailable = 41 | version[0] > localVersion[0] || 42 | version[1] > localVersion[1] || 43 | version[2] > localVersion[2]; 44 | 45 | if (!newVersionAvailable) { 46 | return null; 47 | } 48 | 49 | return ( 50 | 54 | {`Version ${githubVersion} is available. You are on version ${npmVersion}`} 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /src/client/src/views/homepage/HomePage.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { fontColors, mediaWidths, headerColor } from '../../styles/Themes'; 3 | 4 | export const Headline = styled.div` 5 | padding: 16px 0; 6 | width: 100%; 7 | 8 | @media (${mediaWidths.mobile}) { 9 | padding: 0; 10 | } 11 | `; 12 | 13 | export const HomeTitle = styled.h1<{ textColor?: string }>` 14 | width: 100%; 15 | text-align: center; 16 | color: ${({ textColor }) => (textColor ? textColor : fontColors.white)}; 17 | font-size: 56px; 18 | margin: 0; 19 | font-weight: 900; 20 | 21 | @media (${mediaWidths.mobile}) { 22 | font-size: 24px; 23 | } 24 | `; 25 | 26 | export const HomeText = styled.p` 27 | color: ${fontColors.white}; 28 | text-align: center; 29 | font-size: 20px; 30 | 31 | @media (${mediaWidths.mobile}) { 32 | font-size: 14px; 33 | margin: 0 32px; 34 | } 35 | `; 36 | 37 | export const FullWidth = styled.div` 38 | display: flex; 39 | justify-content: center; 40 | width: 100%; 41 | margin-top: 8px; 42 | `; 43 | 44 | export const ConnectTitle = styled.div<{ changeColor?: boolean | null }>` 45 | width: 100%; 46 | font-size: 18px; 47 | ${({ changeColor }) => changeColor && `color: ${fontColors.white};`} 48 | padding-bottom: 8px; 49 | `; 50 | 51 | export const LockPadding = styled.span` 52 | margin-left: 4px; 53 | `; 54 | 55 | export const ThunderStorm = styled.img` 56 | height: 320px; 57 | width: 100%; 58 | top: 0px; 59 | object-fit: cover; 60 | position: absolute; 61 | z-index: -1; 62 | background-color: ${headerColor}; 63 | 64 | @media (${mediaWidths.mobile}) { 65 | font-size: 15px; 66 | } 67 | `; 68 | -------------------------------------------------------------------------------- /src/client/src/components/modal/ReactModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { css } from 'styled-components'; 3 | import ReactModal from 'styled-react-modal'; 4 | import { cardColor, mediaWidths, themeColors } from '../../styles/Themes'; 5 | 6 | interface ModalProps { 7 | children: ReactNode; 8 | isOpen: boolean; 9 | noMinWidth?: boolean; 10 | closeCallback: () => void; 11 | } 12 | 13 | const generalCSS = css` 14 | position: absolute; 15 | top: 50%; 16 | left: 50%; 17 | transform: translateY(-50%) translateX(-50%); 18 | background-color: ${cardColor}; 19 | padding: 20px; 20 | border-radius: 5px; 21 | outline: none; 22 | max-height: 80%; 23 | overflow-y: auto; 24 | border: 1px solid ${themeColors.grey8}; 25 | 26 | @media (${mediaWidths.mobile}) { 27 | /* top: 100%; */ 28 | border-radius: 0px; 29 | /* transform: translateY(-100%) translateX(-50%); */ 30 | width: 100%; 31 | min-width: 325px; 32 | max-height: 100%; 33 | } 34 | `; 35 | 36 | const StyleModal = ReactModal.styled` 37 | ${generalCSS} 38 | min-width: 578px; 39 | `; 40 | 41 | const StyleModalSmall = ReactModal.styled` 42 | ${generalCSS} 43 | background-color: ${themeColors.white}; 44 | `; 45 | 46 | const Modal = ({ 47 | children, 48 | isOpen, 49 | noMinWidth = false, 50 | closeCallback, 51 | }: ModalProps) => { 52 | const Styled = noMinWidth ? StyleModalSmall : StyleModal; 53 | 54 | return ( 55 | 60 | {children} 61 | 62 | ); 63 | }; 64 | 65 | export default Modal; 66 | -------------------------------------------------------------------------------- /src/client/src/context/BaseContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, createContext, useContext, useReducer } from 'react'; 2 | 3 | type State = { 4 | hasToken: boolean; 5 | }; 6 | 7 | type ActionType = { 8 | type: 'change'; 9 | hasToken: boolean; 10 | }; 11 | 12 | type Dispatch = (action: ActionType) => void; 13 | 14 | export const StateContext = createContext(undefined); 15 | export const DispatchContext = createContext(undefined); 16 | 17 | const stateReducer = (state: State, action: ActionType): State => { 18 | switch (action.type) { 19 | case 'change': 20 | return { hasToken: action.hasToken }; 21 | default: 22 | return state; 23 | } 24 | }; 25 | 26 | const BaseProvider: React.FC<{ 27 | initialHasToken: boolean; 28 | children?: ReactNode; 29 | }> = ({ children, initialHasToken = false }) => { 30 | const [state, dispatch] = useReducer(stateReducer, { 31 | hasToken: initialHasToken, 32 | }); 33 | 34 | return ( 35 | 36 | {children} 37 | 38 | ); 39 | }; 40 | 41 | const useBaseState = () => { 42 | const context = useContext(StateContext); 43 | if (context === undefined) { 44 | throw new Error('useBaseState must be used within a BaseProvider'); 45 | } 46 | return context; 47 | }; 48 | 49 | const useBaseDispatch = () => { 50 | const context = useContext(DispatchContext); 51 | if (context === undefined) { 52 | throw new Error('useBaseDispatch must be used within a BaseProvider'); 53 | } 54 | return context; 55 | }; 56 | 57 | export { BaseProvider, useBaseState, useBaseDispatch }; 58 | -------------------------------------------------------------------------------- /src/client/src/graphql/mutations/lnUrl.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | 3 | export const FETCH_LN_URL = gql` 4 | mutation FetchLnUrl($url: String!) { 5 | fetchLnUrl(url: $url) { 6 | ... on WithdrawRequest { 7 | callback 8 | k1 9 | maxWithdrawable 10 | defaultDescription 11 | minWithdrawable 12 | tag 13 | } 14 | ... on PayRequest { 15 | callback 16 | maxSendable 17 | minSendable 18 | metadata 19 | commentAllowed 20 | tag 21 | } 22 | ... on ChannelRequest { 23 | tag 24 | k1 25 | callback 26 | uri 27 | } 28 | } 29 | } 30 | `; 31 | 32 | export const AUTH_LN_URL = gql` 33 | mutation AuthLnUrl($url: String!) { 34 | lnUrlAuth(url: $url) { 35 | status 36 | message 37 | } 38 | } 39 | `; 40 | 41 | export const PAY_LN_URL = gql` 42 | mutation PayLnUrl($callback: String!, $amount: Float!, $comment: String) { 43 | lnUrlPay(callback: $callback, amount: $amount, comment: $comment) { 44 | tag 45 | description 46 | url 47 | message 48 | ciphertext 49 | iv 50 | } 51 | } 52 | `; 53 | 54 | export const WITHDRAW_LN_URL = gql` 55 | mutation WithdrawLnUrl( 56 | $callback: String! 57 | $amount: Float! 58 | $k1: String! 59 | $description: String 60 | ) { 61 | lnUrlWithdraw( 62 | callback: $callback 63 | amount: $amount 64 | k1: $k1 65 | description: $description 66 | ) 67 | } 68 | `; 69 | 70 | export const CHANNEL_LN_URL = gql` 71 | mutation ChannelLnUrl($callback: String!, $k1: String!, $uri: String!) { 72 | lnUrlChannel(callback: $callback, k1: $k1, uri: $uri) 73 | } 74 | `; 75 | -------------------------------------------------------------------------------- /src/client/src/hooks/UseSocket.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useRef } from 'react'; 2 | import { Socket } from 'socket.io-client'; 3 | import { SocketContext } from '../context/SocketContext'; 4 | 5 | export const useSocket = (disabled?: boolean) => { 6 | const socket = useRef(undefined); 7 | 8 | const context = useContext(SocketContext); 9 | 10 | if (context === undefined) { 11 | throw new Error('useSocket must be used within a SocketProvider'); 12 | } 13 | 14 | const { getStatus, createConnection, getError } = context; 15 | 16 | const status = getStatus(); 17 | const error = getError(); 18 | 19 | useEffect(() => { 20 | if (disabled) return; 21 | const { socket: _socket, cleanup } = createConnection(); 22 | socket.current = _socket; 23 | return () => { 24 | cleanup(); 25 | }; 26 | }, [createConnection, disabled]); 27 | 28 | return { 29 | socket: socket.current, 30 | status, 31 | error, 32 | }; 33 | }; 34 | 35 | export const useSocketEvent = ( 36 | socket: Socket | undefined, 37 | event: string, 38 | cbk?: (data: any) => void 39 | ) => { 40 | const context = useContext(SocketContext); 41 | 42 | if (context === undefined) { 43 | throw new Error('useSocketEvent must be used within a SocketProvider'); 44 | } 45 | 46 | const { registerSharedListener, getLastMessage } = context; 47 | const lastMessage = getLastMessage(event); 48 | const sendMessage = (message: any) => socket?.emit(event, message); 49 | 50 | useEffect(() => { 51 | registerSharedListener(event); 52 | }, [event, registerSharedListener]); 53 | 54 | useEffect(() => { 55 | if (!lastMessage) return; 56 | cbk?.(lastMessage); 57 | }, [lastMessage, cbk]); 58 | 59 | return { lastMessage, sendMessage }; 60 | }; 61 | -------------------------------------------------------------------------------- /src/client/pages/settings/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { GridWrapper } from '../../src/components/gridWrapper/GridWrapper'; 4 | import { NextPageContext } from 'next'; 5 | import { getProps } from '../../src/utils/ssr'; 6 | import { DashboardSettings } from '../../src/views/settings/Dashboard'; 7 | import { SingleLine } from '../../src/components/generic/Styled'; 8 | import { InterfaceSettings } from '../../src/views/settings/Interface'; 9 | import { DangerView } from '../../src/views/settings/Danger'; 10 | import { ChatSettings } from '../../src/views/settings/Chat'; 11 | import { PrivacySettings } from '../../src/views/settings/Privacy'; 12 | import { Security } from '../../src/views/settings/Security'; 13 | import { NetworkInfo } from '../../src/views/home/networkInfo/NetworkInfo'; 14 | import { NotificationSettings } from '../../src/views/settings/Notifications'; 15 | import { AmbossSettings } from '../../src/views/settings/Amboss'; 16 | 17 | export const ButtonRow = styled.div` 18 | width: auto; 19 | display: flex; 20 | `; 21 | 22 | export const SettingsLine = styled(SingleLine)` 23 | margin: 8px 0; 24 | `; 25 | 26 | const SettingsView = () => { 27 | return ( 28 | <> 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | const Wrapped = () => ( 43 | 44 | 45 | 46 | ); 47 | 48 | export default Wrapped; 49 | 50 | export async function getServerSideProps(context: NextPageContext) { 51 | return await getProps(context); 52 | } 53 | --------------------------------------------------------------------------------