├── .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 |
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 |
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 |
--------------------------------------------------------------------------------