├── .github ├── docs │ └── Contributing.md ├── images │ ├── Dashboard.png │ ├── start9-badge-light.svg │ └── umbrel-badge-light.svg └── workflows │ ├── build.yml │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .prettierrc.json ├── Dockerfile ├── LICENSE ├── README.md ├── apps ├── backend │ ├── dist │ │ ├── controllers │ │ │ ├── auth.js │ │ │ ├── lightning.js │ │ │ └── shared.js │ │ ├── models │ │ │ ├── errors.js │ │ │ └── showrunes.type.js │ │ ├── routes │ │ │ └── v1 │ │ │ │ ├── auth.js │ │ │ │ ├── lightning.js │ │ │ │ └── shared.js │ │ ├── server.js │ │ ├── service │ │ │ ├── grpc.service.js │ │ │ └── lightning.service.js │ │ └── shared │ │ │ ├── consts.js │ │ │ ├── error-handler.js │ │ │ ├── logger.js │ │ │ ├── routes.config.js │ │ │ └── utils.js │ ├── eslint.config.js │ ├── package.json │ ├── proto │ │ ├── node.proto │ │ └── primitives.proto │ ├── source │ │ ├── controllers │ │ │ ├── auth.ts │ │ │ ├── lightning.ts │ │ │ └── shared.ts │ │ ├── models │ │ │ ├── errors.ts │ │ │ └── showrunes.type.ts │ │ ├── routes │ │ │ └── v1 │ │ │ │ ├── auth.ts │ │ │ │ ├── lightning.ts │ │ │ │ └── shared.ts │ │ ├── server.ts │ │ ├── service │ │ │ ├── grpc.service.ts │ │ │ └── lightning.service.ts │ │ └── shared │ │ │ ├── consts.ts │ │ │ ├── error-handler.ts │ │ │ ├── logger.ts │ │ │ ├── routes.config.ts │ │ │ └── utils.ts │ └── tsconfig.json └── frontend │ ├── build │ ├── asset-manifest.json │ ├── fonts │ │ ├── Inter-Bold.ttf │ │ ├── Inter-Medium.ttf │ │ ├── Inter-Regular.ttf │ │ ├── Inter-SemiBold.ttf │ │ ├── Inter-Thin.ttf │ │ ├── OFL.txt │ │ └── README.txt │ ├── images │ │ ├── cln-favicon.ico │ │ ├── cln-logo-dark.png │ │ ├── cln-logo-dark.svg │ │ ├── cln-logo-light.png │ │ └── cln-logo-light.svg │ ├── index.html │ └── static │ │ ├── css │ │ ├── 648.1c4becff.chunk.css │ │ ├── 648.1c4becff.chunk.css.map │ │ ├── 687.19b1d4d6.chunk.css │ │ ├── 687.19b1d4d6.chunk.css.map │ │ ├── 78.93e26be7.chunk.css │ │ ├── 78.93e26be7.chunk.css.map │ │ ├── 880.70c3ebe7.chunk.css │ │ ├── 880.70c3ebe7.chunk.css.map │ │ ├── main.94b1eb52.css │ │ └── main.94b1eb52.css.map │ │ ├── js │ │ ├── 213.6d084fd7.chunk.js │ │ ├── 213.6d084fd7.chunk.js.LICENSE.txt │ │ ├── 213.6d084fd7.chunk.js.map │ │ ├── 400.868d8ca3.chunk.js │ │ ├── 400.868d8ca3.chunk.js.map │ │ ├── 408.da49cbb1.chunk.js │ │ ├── 408.da49cbb1.chunk.js.map │ │ ├── 648.6b3a5a0d.chunk.js │ │ ├── 648.6b3a5a0d.chunk.js.map │ │ ├── 687.c6039462.chunk.js │ │ ├── 687.c6039462.chunk.js.map │ │ ├── 768.59f5d13d.chunk.js │ │ ├── 768.59f5d13d.chunk.js.map │ │ ├── 78.c368a490.chunk.js │ │ ├── 78.c368a490.chunk.js.map │ │ ├── 813.cdbe58bb.chunk.js │ │ ├── 813.cdbe58bb.chunk.js.LICENSE.txt │ │ ├── 813.cdbe58bb.chunk.js.map │ │ ├── 880.cd8c8d95.chunk.js │ │ ├── 880.cd8c8d95.chunk.js.map │ │ ├── main.07a60e9b.js │ │ ├── main.07a60e9b.js.LICENSE.txt │ │ └── main.07a60e9b.js.map │ │ └── media │ │ ├── Inter-Bold.88fa7ae373b07b41ecce.ttf │ │ ├── Inter-Medium.6dcbc9bed1ec438907ee.ttf │ │ ├── Inter-SemiBold.4d56bb21f2399db8ad48.ttf │ │ └── Inter-Thin.f341ca512063c66296d1.ttf │ ├── eslint.config.js │ ├── package.json │ ├── public │ ├── fonts │ │ ├── Inter-Bold.ttf │ │ ├── Inter-Medium.ttf │ │ ├── Inter-Regular.ttf │ │ ├── Inter-SemiBold.ttf │ │ ├── Inter-Thin.ttf │ │ ├── OFL.txt │ │ └── README.txt │ ├── images │ │ ├── cln-favicon.ico │ │ ├── cln-logo-dark.png │ │ ├── cln-logo-dark.svg │ │ ├── cln-logo-light.png │ │ └── cln-logo-light.svg │ └── index.html │ ├── src │ ├── components │ │ ├── App │ │ │ ├── App.scss │ │ │ ├── App.test.tsx │ │ │ └── App.tsx │ │ ├── bookkeeper │ │ │ ├── AccountEvents │ │ │ │ ├── AccountEventsGraph │ │ │ │ │ ├── AccountEventsGraph.scss │ │ │ │ │ ├── AccountEventsGraph.test.tsx │ │ │ │ │ └── AccountEventsGraph.tsx │ │ │ │ ├── AccountEventsRoot.scss │ │ │ │ ├── AccountEventsRoot.test.tsx │ │ │ │ ├── AccountEventsRoot.tsx │ │ │ │ └── AccountEventsTable │ │ │ │ │ ├── AccountEventsTable.scss │ │ │ │ │ ├── AccountEventsTable.test.tsx │ │ │ │ │ └── AccountEventsTable.tsx │ │ │ ├── BkprHome │ │ │ │ ├── AccountEventsInfo │ │ │ │ │ ├── AccountEventsInfo.scss │ │ │ │ │ └── AccountEventsInfo.tsx │ │ │ │ ├── BkprHome.scss │ │ │ │ ├── BkprHome.test.tsx │ │ │ │ ├── BkprHome.tsx │ │ │ │ ├── SatsFlowInfo │ │ │ │ │ ├── SatsFlowInfo.scss │ │ │ │ │ └── SatsFlowInfo.tsx │ │ │ │ └── VolumeInfo │ │ │ │ │ ├── VolumeInfo.scss │ │ │ │ │ └── VolumeInfo.tsx │ │ │ ├── SatsFlow │ │ │ │ ├── SatsFlowGraph │ │ │ │ │ ├── SatsFlowGraph.scss │ │ │ │ │ ├── SatsFlowGraph.test.tsx │ │ │ │ │ └── SatsFlowGraph.tsx │ │ │ │ ├── SatsFlowRoot.scss │ │ │ │ ├── SatsFlowRoot.test.tsx │ │ │ │ └── SatsFlowRoot.tsx │ │ │ └── Volume │ │ │ │ ├── VolumeGraph │ │ │ │ ├── VolumeGraph.scss │ │ │ │ └── VolumeGraph.tsx │ │ │ │ ├── VolumeRoot.scss │ │ │ │ ├── VolumeRoot.test.tsx │ │ │ │ └── VolumeRoot.tsx │ │ ├── cln │ │ │ ├── BTCCard │ │ │ │ ├── BTCCard.scss │ │ │ │ ├── BTCCard.test.tsx │ │ │ │ └── BTCCard.tsx │ │ │ ├── BTCDeposit │ │ │ │ ├── BTCDeposit.scss │ │ │ │ ├── BTCDeposit.test.tsx │ │ │ │ └── BTCDeposit.tsx │ │ │ ├── BTCTransaction │ │ │ │ ├── BTCTransaction.scss │ │ │ │ ├── BTCTransaction.test.tsx │ │ │ │ └── BTCTransaction.tsx │ │ │ ├── BTCTransactionsList │ │ │ │ ├── BTCTransactionsList.scss │ │ │ │ ├── BTCTransactionsList.test.tsx │ │ │ │ └── BTCTransactionsList.tsx │ │ │ ├── BTCWallet │ │ │ │ ├── BTCWallet.scss │ │ │ │ ├── BTCWallet.test.tsx │ │ │ │ └── BTCWallet.tsx │ │ │ ├── BTCWithdraw │ │ │ │ ├── BTCWithdraw.scss │ │ │ │ ├── BTCWithdraw.test.tsx │ │ │ │ └── BTCWithdraw.tsx │ │ │ ├── CLNCard │ │ │ │ ├── CLNCard.scss │ │ │ │ ├── CLNCard.test.tsx │ │ │ │ └── CLNCard.tsx │ │ │ ├── CLNHome │ │ │ │ ├── CLNHome.scss │ │ │ │ ├── CLNHome.test.tsx │ │ │ │ └── CLNHome.tsx │ │ │ ├── CLNOffer │ │ │ │ ├── CLNOffer.scss │ │ │ │ ├── CLNOffer.test.tsx │ │ │ │ └── CLNOffer.tsx │ │ │ ├── CLNOffersList │ │ │ │ ├── CLNOffersList.scss │ │ │ │ ├── CLNOffersList.test.tsx │ │ │ │ └── CLNOffersList.tsx │ │ │ ├── CLNReceive │ │ │ │ ├── CLNReceive.scss │ │ │ │ ├── CLNReceive.test.tsx │ │ │ │ └── CLNReceive.tsx │ │ │ ├── CLNSend │ │ │ │ ├── CLNSend.scss │ │ │ │ ├── CLNSend.test.tsx │ │ │ │ └── CLNSend.tsx │ │ │ ├── CLNTransaction │ │ │ │ ├── CLNTransaction.scss │ │ │ │ ├── CLNTransaction.test.tsx │ │ │ │ └── CLNTransaction.tsx │ │ │ ├── CLNTransactionsList │ │ │ │ ├── CLNTransactionsList.scss │ │ │ │ ├── CLNTransactionsList.test.tsx │ │ │ │ └── CLNTransactionsList.tsx │ │ │ ├── CLNWallet │ │ │ │ ├── CLNWallet.scss │ │ │ │ ├── CLNWallet.test.tsx │ │ │ │ └── CLNWallet.tsx │ │ │ ├── ChannelDetails │ │ │ │ ├── ChannelDetails.scss │ │ │ │ ├── ChannelDetails.test.tsx │ │ │ │ └── ChannelDetails.tsx │ │ │ ├── ChannelOpen │ │ │ │ ├── ChannelOpen.scss │ │ │ │ ├── ChannelOpen.test.tsx │ │ │ │ └── ChannelOpen.tsx │ │ │ ├── Channels │ │ │ │ ├── Channels.scss │ │ │ │ ├── Channels.test.tsx │ │ │ │ └── Channels.tsx │ │ │ ├── ChannelsCard │ │ │ │ ├── ChannelsCard.scss │ │ │ │ ├── ChannelsCard.test.tsx │ │ │ │ └── ChannelsCard.tsx │ │ │ └── Overview │ │ │ │ ├── Overview.scss │ │ │ │ ├── Overview.test.tsx │ │ │ │ └── Overview.tsx │ │ ├── modals │ │ │ ├── ConnectWallet │ │ │ │ ├── ConnectWallet.scss │ │ │ │ ├── ConnectWallet.test.tsx │ │ │ │ └── ConnectWallet.tsx │ │ │ ├── Login │ │ │ │ ├── Login.scss │ │ │ │ ├── Login.test.tsx │ │ │ │ └── Login.tsx │ │ │ ├── Logout │ │ │ │ ├── Logout.scss │ │ │ │ ├── Logout.test.tsx │ │ │ │ └── Logout.tsx │ │ │ ├── NodeInfo │ │ │ │ ├── NodeInfo.scss │ │ │ │ ├── NodeInfo.test.tsx │ │ │ │ └── NodeInfo.tsx │ │ │ ├── SQLTerminal │ │ │ │ ├── SQLTerminal.scss │ │ │ │ ├── SQLTerminal.test.tsx │ │ │ │ └── SQLTerminal.tsx │ │ │ └── SetPassword │ │ │ │ ├── SetPassword.scss │ │ │ │ ├── SetPassword.test.tsx │ │ │ │ └── SetPassword.tsx │ │ ├── shared │ │ │ ├── CurrencyBox │ │ │ │ ├── CurrencyBox.scss │ │ │ │ ├── CurrencyBox.test.tsx │ │ │ │ └── CurrencyBox.tsx │ │ │ ├── DataFilterOptions │ │ │ │ ├── DataFilterOptions.scss │ │ │ │ ├── DataFilterOptions.test.tsx │ │ │ │ └── DataFilterOptions.tsx │ │ │ ├── DateBox │ │ │ │ ├── DateBox.scss │ │ │ │ ├── DateBox.test.tsx │ │ │ │ └── DateBox.tsx │ │ │ ├── DatepickerInput │ │ │ │ ├── DatepickerInput.scss │ │ │ │ └── DatepickerInput.tsx │ │ │ ├── FeerateRange │ │ │ │ ├── FeerateRange.scss │ │ │ │ ├── FeerateRange.test.tsx │ │ │ │ └── FeerateRange.tsx │ │ │ ├── FiatBox │ │ │ │ ├── FiatBox.scss │ │ │ │ ├── FiatBox.test.tsx │ │ │ │ └── FiatBox.tsx │ │ │ ├── FiatSelection │ │ │ │ ├── FiatSelection.scss │ │ │ │ ├── FiatSelection.test.tsx │ │ │ │ └── FiatSelection.tsx │ │ │ ├── InvalidInputMessage │ │ │ │ ├── InvalidInputMessage.scss │ │ │ │ ├── InvalidInputMessage.test.tsx │ │ │ │ └── InvalidInputMessage.tsx │ │ │ ├── QRCode │ │ │ │ ├── QRCode.scss │ │ │ │ ├── QRCode.test.tsx │ │ │ │ └── QRCode.tsx │ │ │ ├── StatusAlert │ │ │ │ ├── StatusAlert.scss │ │ │ │ ├── StatusAlert.test.tsx │ │ │ │ └── StatusAlert.tsx │ │ │ ├── ToastMessage │ │ │ │ ├── ToastMessage.scss │ │ │ │ ├── ToastMessage.test.tsx │ │ │ │ └── ToastMessage.tsx │ │ │ └── ToggleSwitch │ │ │ │ ├── ToggleSwitch.scss │ │ │ │ ├── ToggleSwitch.test.tsx │ │ │ │ └── ToggleSwitch.tsx │ │ └── ui │ │ │ ├── Header │ │ │ ├── Header.scss │ │ │ ├── Header.test.tsx │ │ │ └── Header.tsx │ │ │ ├── Loading │ │ │ └── Loading.tsx │ │ │ ├── Menu │ │ │ ├── Menu.scss │ │ │ ├── Menu.test.tsx │ │ │ └── Menu.tsx │ │ │ ├── RouteTransition │ │ │ ├── RouteTransition.test.tsx │ │ │ └── RouteTransition.tsx │ │ │ └── Settings │ │ │ ├── Settings.scss │ │ │ ├── Settings.test.tsx │ │ │ └── Settings.tsx │ ├── hooks │ │ ├── use-breakpoint.ts │ │ ├── use-injectreducer.ts │ │ └── use-input.ts │ ├── index.tsx │ ├── routes │ │ ├── dataLoader.tsx │ │ ├── router.config.tsx │ │ └── routerReduxSync.tsx │ ├── services │ │ ├── data-transform.service.ts │ │ ├── http.service.ts │ │ └── logger.service.ts │ ├── setupTests.ts │ ├── store │ │ ├── appStore.tsx │ │ ├── bkprSelectors.tsx │ │ ├── bkprSlice.tsx │ │ ├── clnSelectors.tsx │ │ ├── clnSlice.tsx │ │ ├── rootSelectors.tsx │ │ ├── rootSlice.tsx │ │ └── store.type.ts │ ├── styles │ │ ├── bootstrap-custom.scss │ │ ├── constants.scss │ │ ├── fonts.scss │ │ ├── mode-dark.scss │ │ ├── mode-light.scss │ │ └── shared.scss │ ├── svgs │ │ ├── AccountEvents.tsx │ │ ├── Action.tsx │ │ ├── Add.tsx │ │ ├── Address.tsx │ │ ├── Amount.tsx │ │ ├── Balance.tsx │ │ ├── BitcoinWallet.tsx │ │ ├── CLNLogo.tsx │ │ ├── Capacity.tsx │ │ ├── Channels.tsx │ │ ├── ChevronDown.tsx │ │ ├── Close.tsx │ │ ├── Copy.tsx │ │ ├── Currency.tsx │ │ ├── DayMode.tsx │ │ ├── Deposit.tsx │ │ ├── Description.tsx │ │ ├── Hide.tsx │ │ ├── IncomingArrow.tsx │ │ ├── Information.tsx │ │ ├── LightningWallet.tsx │ │ ├── Logout.tsx │ │ ├── NightMode.tsx │ │ ├── NoBTCTransactionDark.tsx │ │ ├── NoBTCTransactionLight.tsx │ │ ├── NoCLNTransactionDark.tsx │ │ ├── NoCLNTransactionLight.tsx │ │ ├── NoChannelDark.tsx │ │ ├── NoChannelLight.tsx │ │ ├── OpenLink.tsx │ │ ├── OutgoingArrow.tsx │ │ ├── Password.tsx │ │ ├── Peers.tsx │ │ ├── QuestionMark.tsx │ │ ├── Reserved.tsx │ │ ├── SQL.tsx │ │ ├── SatsFlow.tsx │ │ ├── Settings.tsx │ │ ├── Show.tsx │ │ ├── UnReserved.tsx │ │ ├── VolumeChart.tsx │ │ └── Withdraw.tsx │ ├── types │ │ ├── bookkeeper.type.ts │ │ ├── cln.type.ts │ │ └── root.type.ts │ └── utilities │ │ ├── bookkeeper-sql.ts │ │ ├── constants.ts │ │ ├── data-formatters.test.tsx │ │ ├── data-formatters.ts │ │ └── test-utilities │ │ ├── mockData.tsx │ │ ├── mockService.ts │ │ └── mockStore.tsx │ └── tsconfig.json ├── docker-compose.yml ├── entrypoint.sh ├── env.sh ├── package-lock.json └── package.json /.github/docs/Contributing.md: -------------------------------------------------------------------------------- 1 | Development 2 | ----------- 3 | * Clone the project: git clone https://github.com/ElementsProject/cln-application.git 4 | * Change directory: cd cln-application 5 | * Install dependencies: Assuming that nodejs (v14 & above) and npm are already installed, run `npm install`. 6 | * Setup environment variables: Assuming that bitcoind and core-lightning are already running, adjust environment variables listed in `./env.sh` file and execute the script with `'. env.sh'` to setup required environment variables to connect to the node. 7 | * Setup Commando auth: Update `LIGHTNING_PUBKEY` and `LIGHTNING_RUNE` variables in `.commando` for successful backend authentication and connection via commando. Or run `entrypoint.sh` with correct lightningd path to do the same. 8 | * Run backend server: Get backend server up by running `npm run backend:serve`. 9 | * Watch backend server: Watch backend server for realtime changes with `npm run backend:watch`. 10 | * React frontend server: React development server is set to serve on port 4300. Run `npm run frontend:dev` script to get it working. 11 | 12 | 13 | Releasing and packaging on Github 14 | ---------------------------------- 15 | * Merge the `Release-` branch into `main` branch. 16 | * Set VERSION env `VERSION=v`. 17 | * Tag the commit with `git tag -a -s ${VERSION} -m ${VERSION} && git push --tags`. 18 | * Go to repo's `Releases` page and draft a new release from above tag. 19 | * Prepare release notes with the help of milestone, issues and PRs. Add them on the release page. 20 | * Signing the release: 21 | ** `mkdir -p ./release & git archive --format zip --output ./release/cln-application-${VERSION}.zip main` 22 | ** `cd release` 23 | ** `sha256sum cln* > SHA256SUMS` 24 | ** `gpg -sb --armor -o SHA256SUMS.asc SHA256SUMS` 25 | * Verify the release with `gpg --verify SHA256SUMS.asc`. 26 | * Upload `cln-application-${VERSION}.zip`, `SHA256SUMS` and `SHA256SUMS.asc` files on release assets. 27 | * Go to repo's `Actions` tab and confirm that actions have been triggered for `Artifact` and `Build and publish Github image`. 28 | * Confirm that both actions finished successfully and the latest package is available at `https://github.com/orgs/ElementsProject/packages?repo_name=cln-application`. 29 | -------------------------------------------------------------------------------- /.github/images/Dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/.github/images/Dashboard.png -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - name: Checkout source code 12 | uses: actions/checkout@v3 13 | 14 | - name: Setup Node 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: 18.x 18 | 19 | - name: Get version from package.json 20 | id: package-version 21 | run: | 22 | echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV 23 | echo "Project Version: $VERSION" 24 | 25 | - name: Cache node_modules 26 | uses: actions/cache@v3 27 | id: cache-npm-packages 28 | with: 29 | path: node_modules 30 | key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }} 31 | 32 | - name: Install NPM dependencies 33 | if: steps.cache-npm-packages.outputs.cache-hit != 'true' 34 | run: npm clean-install 35 | 36 | - name: Cache build frontend 37 | uses: actions/cache@v3 38 | id: cache-build-frontend 39 | with: 40 | path: apps/frontend 41 | key: ${{ runner.os }}-frontend-${{ github.sha }} 42 | 43 | - name: Run build production application 44 | run: npm run frontend:build 45 | 46 | - name: Cache build backend 47 | uses: actions/cache@v3 48 | id: cache-build-backend 49 | with: 50 | path: apps/backend 51 | key: ${{ runner.os }}-backend-${{ github.sha }} 52 | 53 | - name: Run build backend server 54 | run: npm run backend:build 55 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | eslint: 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - uses: actions/checkout@v3 12 | 13 | - name: Use Node.js 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: 18.x 17 | 18 | - name: Get version from package.json 19 | id: package-version 20 | run: | 21 | echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV 22 | echo "Project Version: $VERSION" 23 | 24 | - name: Cache node_modules 25 | uses: actions/cache@v3 26 | id: cache-npm-packages 27 | with: 28 | path: node_modules 29 | key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }} 30 | 31 | - name: Install NPM dependencies 32 | if: steps.cache-npm-packages.outputs.cache-hit != 'true' 33 | run: npm clean-install 34 | 35 | - name: Check lint errors 36 | run: npm run lint 37 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - name: Checkout source code 12 | uses: actions/checkout@v3 13 | 14 | - name: Setup Node 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: 18.x 18 | 19 | - name: Get version from package.json 20 | id: package-version 21 | run: | 22 | echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV 23 | echo "Project Version: $VERSION" 24 | 25 | - name: Cache node_modules 26 | uses: actions/cache@v3 27 | id: cache-npm-packages 28 | with: 29 | path: node_modules 30 | key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }} 31 | 32 | - name: Install NPM dependencies 33 | if: steps.cache-npm-packages.outputs.cache-hit != 'true' 34 | run: npm clean-install 35 | 36 | - name: Run frontend unit tests 37 | run: npm run frontend:test 38 | 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | data 4 | application-cln.log 5 | .commando-env 6 | .commando 7 | release 8 | env-local.sh 9 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "endOfLine": "lf", 5 | "bracketSameLine": false, 6 | "jsxSingleQuote": false, 7 | "printWidth": 100, 8 | "proseWrap": "preserve", 9 | "quoteProps": "as-needed", 10 | "semi": true, 11 | "singleQuote": true, 12 | "tabWidth": 2, 13 | "trailingComma": "all", 14 | "useTabs": false 15 | } 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build Stage 2 | FROM node:18-buster AS cln-app-builder 3 | 4 | # Install system dependencies 5 | RUN apt-get update && apt-get install -y \ 6 | build-essential \ 7 | libcairo2-dev \ 8 | libpango1.0-dev \ 9 | libjpeg-dev \ 10 | libgif-dev \ 11 | librsvg2-dev 12 | 13 | # Create app directory 14 | WORKDIR /app 15 | 16 | # Copy project files and folders 17 | COPY apps/backend ./apps/backend 18 | COPY apps/frontend ./apps/frontend 19 | COPY package.json ./ 20 | COPY package-lock.json ./ 21 | 22 | # Install dependencies 23 | RUN npm install 24 | 25 | # Build assets 26 | RUN npm run build 27 | 28 | # Prune development dependencies 29 | RUN npm prune --omit=dev 30 | 31 | # Final image 32 | FROM node:18-buster-slim AS cln-app-final 33 | 34 | # Install jq for JSON parsing in entrypoint.sh 35 | RUN apt-get update && apt-get install -y jq socat 36 | 37 | # Copy built code from build stages to '/app/frontend' directory 38 | COPY --from=cln-app-builder /app/apps/frontend/build /app/apps/frontend/build 39 | COPY --from=cln-app-builder /app/apps/frontend/public /app/apps/frontend/public 40 | COPY --from=cln-app-builder /app/apps/frontend/package.json /app/apps/frontend/package.json 41 | 42 | # Copy built code from build stages to '/app/backend' directory 43 | COPY --from=cln-app-builder /app/apps/backend/dist /app/apps/backend/dist 44 | COPY --from=cln-app-builder /app/apps/backend/package.json /app/apps/backend/package.json 45 | 46 | # Copy built code from build stages to '/app' directory 47 | COPY --from=cln-app-builder /app/package.json /app/package.json 48 | COPY --from=cln-app-builder /app/node_modules /app/node_modules 49 | 50 | # Change directory to '/app' 51 | WORKDIR /app 52 | 53 | COPY entrypoint.sh entrypoint.sh 54 | 55 | ENTRYPOINT ["bash", "./entrypoint.sh"] 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ElementsProject 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/backend/dist/controllers/lightning.js: -------------------------------------------------------------------------------- 1 | import handleError from '../shared/error-handler.js'; 2 | import { CLNService } from '../service/lightning.service.js'; 3 | import { logger } from '../shared/logger.js'; 4 | import { AppConnect, APP_CONSTANTS } from '../shared/consts.js'; 5 | const clnService = CLNService; 6 | class LightningController { 7 | callMethod(req, res, next) { 8 | try { 9 | logger.info('Calling method: ' + req.body.method); 10 | clnService 11 | .call(req.body.method, req.body.params) 12 | .then((commandRes) => { 13 | logger.info('Controller received response for ' + 14 | req.body.method + 15 | ': ' + 16 | JSON.stringify(commandRes)); 17 | if (APP_CONSTANTS.APP_CONNECT == AppConnect.COMMANDO && 18 | req.body.method && 19 | req.body.method === 'listpeers') { 20 | // Filter out ln message pubkey from peers list 21 | const lnmPubkey = clnService.getLNMsgPubkey(); 22 | commandRes.peers = commandRes.peers.filter((peer) => peer.id !== lnmPubkey); 23 | res.status(200).json(commandRes); 24 | } 25 | else { 26 | res.status(200).json(commandRes); 27 | } 28 | }) 29 | .catch((err) => { 30 | logger.error('Controller caught lightning error from ' + 31 | req.body.method + 32 | ': ' + 33 | JSON.stringify(err)); 34 | return handleError(err, req, res, next); 35 | }); 36 | } 37 | catch (error) { 38 | return handleError(error, req, res, next); 39 | } 40 | } 41 | } 42 | export default new LightningController(); 43 | -------------------------------------------------------------------------------- /apps/backend/dist/models/errors.js: -------------------------------------------------------------------------------- 1 | import { HttpStatusCode } from '../shared/consts.js'; 2 | export class BaseError extends Error { 3 | code; 4 | message; 5 | constructor(code, message) { 6 | super(message); 7 | Object.setPrototypeOf(this, new.target.prototype); 8 | this.code = code; 9 | this.message = message; 10 | Error.captureStackTrace(this); 11 | } 12 | } 13 | export class APIError extends BaseError { 14 | constructor(code = HttpStatusCode.INTERNAL_SERVER, message = 'Unknown API Server Error') { 15 | super(code, message); 16 | } 17 | } 18 | export class BitcoindError extends BaseError { 19 | constructor(code = HttpStatusCode.BITCOIN_SERVER, message = 'Unknown Bitcoin API Error') { 20 | super(code, message); 21 | } 22 | } 23 | export class LightningError extends BaseError { 24 | constructor(code = HttpStatusCode.LIGHTNING_SERVER, message = 'Unknown Core Lightning API Error') { 25 | super(code, message); 26 | } 27 | } 28 | export class ValidationError extends BaseError { 29 | constructor(code = HttpStatusCode.INVALID_DATA, message = 'Unknown Validation Error') { 30 | super(code, message); 31 | } 32 | } 33 | export class AuthError extends BaseError { 34 | constructor(code = HttpStatusCode.UNAUTHORIZED, message = 'Unknown Authentication Error') { 35 | super(code, message); 36 | } 37 | } 38 | export class GRPCError extends BaseError { 39 | constructor(code = HttpStatusCode.GRPC_UNKNOWN, message = 'Unknown gRPC Error') { 40 | super(code, message); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /apps/backend/dist/models/showrunes.type.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /apps/backend/dist/routes/v1/auth.js: -------------------------------------------------------------------------------- 1 | import { CommonRoutesConfig } from '../../shared/routes.config.js'; 2 | import AuthController from '../../controllers/auth.js'; 3 | import { API_VERSION } from '../../shared/consts.js'; 4 | const AUTH_ROUTE = '/auth'; 5 | export class AuthRoutes extends CommonRoutesConfig { 6 | constructor(app) { 7 | super(app, 'Auth Routes'); 8 | } 9 | configureRoutes() { 10 | this.app.route(API_VERSION + AUTH_ROUTE + '/logout/').get(AuthController.userLogout); 11 | this.app.route(API_VERSION + AUTH_ROUTE + '/login/').post(AuthController.userLogin); 12 | this.app.route(API_VERSION + AUTH_ROUTE + '/reset/').post(AuthController.resetPassword); 13 | this.app 14 | .route(API_VERSION + AUTH_ROUTE + '/isauthenticated/') 15 | .post(AuthController.isUserAuthenticated); 16 | return this.app; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/backend/dist/routes/v1/lightning.js: -------------------------------------------------------------------------------- 1 | import { CommonRoutesConfig } from '../../shared/routes.config.js'; 2 | import AuthController from '../../controllers/auth.js'; 3 | import LightningController from '../../controllers/lightning.js'; 4 | import { API_VERSION } from '../../shared/consts.js'; 5 | const LIGHTNING_ROOT_ROUTE = '/cln'; 6 | export class LightningRoutes extends CommonRoutesConfig { 7 | constructor(app) { 8 | super(app, 'Lightning Routes'); 9 | } 10 | configureRoutes() { 11 | this.app 12 | .route(API_VERSION + LIGHTNING_ROOT_ROUTE + '/call') 13 | .post(AuthController.isUserAuthenticated, LightningController.callMethod); 14 | return this.app; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/backend/dist/routes/v1/shared.js: -------------------------------------------------------------------------------- 1 | import { CommonRoutesConfig } from '../../shared/routes.config.js'; 2 | import AuthController from '../../controllers/auth.js'; 3 | import SharedController from '../../controllers/shared.js'; 4 | import { API_VERSION } from '../../shared/consts.js'; 5 | const SHARED_ROUTE = '/shared'; 6 | export class SharedRoutes extends CommonRoutesConfig { 7 | constructor(app) { 8 | super(app, 'Shared Routes'); 9 | } 10 | configureRoutes() { 11 | this.app.route(API_VERSION + SHARED_ROUTE + '/csrf/').get((req, res, next) => { 12 | res.send({ 13 | csrfToken: req.csrfToken && typeof req.csrfToken === 'function' ? req.csrfToken() : 'not-set', 14 | }); 15 | }); 16 | this.app 17 | .route(API_VERSION + SHARED_ROUTE + '/config/') 18 | .get(SharedController.getApplicationSettings); 19 | this.app 20 | .route(API_VERSION + SHARED_ROUTE + '/config/') 21 | .post(AuthController.isUserAuthenticated, SharedController.setApplicationSettings); 22 | this.app 23 | .route(API_VERSION + SHARED_ROUTE + '/connectwallet/') 24 | .get(AuthController.isUserAuthenticated, SharedController.getWalletConnectSettings); 25 | this.app 26 | .route(API_VERSION + SHARED_ROUTE + '/rate/:fiatCurrency') 27 | .get(SharedController.getFiatRate); 28 | this.app 29 | .route(API_VERSION + SHARED_ROUTE + '/saveinvoicerune/') 30 | .post(AuthController.isUserAuthenticated, SharedController.saveInvoiceRune); 31 | return this.app; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/backend/dist/shared/error-handler.js: -------------------------------------------------------------------------------- 1 | import { HttpStatusCode } from './consts.js'; 2 | import { logger } from './logger.js'; 3 | function handleError(error, req, res, next) { 4 | const route = req.url || ''; 5 | const message = error.message 6 | ? error.message 7 | : typeof error === 'object' 8 | ? JSON.stringify(error) 9 | : typeof error === 'string' 10 | ? error 11 | : 'Unknown Error!'; 12 | logger.error(message, route, error.stack); 13 | return res.status(error.code || HttpStatusCode.INTERNAL_SERVER).json(message); 14 | } 15 | export default handleError; 16 | -------------------------------------------------------------------------------- /apps/backend/dist/shared/logger.js: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | import { Environment, APP_CONSTANTS } from './consts.js'; 3 | export const logConfiguration = { 4 | transports: [ 5 | new winston.transports.Console({ 6 | level: APP_CONSTANTS.APP_MODE === Environment.PRODUCTION 7 | ? "warn" /* LogLevel.WARN */ 8 | : APP_CONSTANTS.APP_MODE === Environment.TESTING 9 | ? "debug" /* LogLevel.DEBUG */ 10 | : "info" /* LogLevel.INFO */, 11 | format: winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), winston.format.timestamp(), winston.format.align(), winston.format.json(), winston.format.colorize({ all: true })), 12 | }), 13 | new winston.transports.File({ 14 | filename: APP_CONSTANTS.APP_LOG_FILE, 15 | level: APP_CONSTANTS.APP_MODE === Environment.PRODUCTION 16 | ? "warn" /* LogLevel.WARN */ 17 | : APP_CONSTANTS.APP_MODE === Environment.TESTING 18 | ? "debug" /* LogLevel.DEBUG */ 19 | : "info" /* LogLevel.INFO */, 20 | format: winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), winston.format.timestamp(), winston.format.align(), winston.format.json(), winston.format.colorize({ all: true })), 21 | }), 22 | ], 23 | }; 24 | export const expressLogConfiguration = { 25 | ...logConfiguration, 26 | meta: APP_CONSTANTS.APP_MODE !== Environment.PRODUCTION, 27 | message: 'HTTP {{res.statusCode}} {{req.method}} {{res.responseTime}}ms {{req.url}}', 28 | expressFormat: false, 29 | colorize: true, 30 | }; 31 | export const logger = winston.createLogger(logConfiguration); 32 | -------------------------------------------------------------------------------- /apps/backend/dist/shared/routes.config.js: -------------------------------------------------------------------------------- 1 | export class CommonRoutesConfig { 2 | app; 3 | name; 4 | constructor(app, name) { 5 | this.app = app; 6 | this.name = name; 7 | this.configureRoutes(); 8 | } 9 | getName() { 10 | return this.name; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/backend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import tseslint from 'typescript-eslint'; 2 | import globals from 'globals'; 3 | 4 | export default [ 5 | ...tseslint.configs.recommended, 6 | { 7 | files: ['**/*.{js,ts}'], 8 | languageOptions: { 9 | globals: { 10 | ...globals.node, 11 | }, 12 | parserOptions: { 13 | sourceType: 'module', 14 | project: './tsconfig.json', 15 | }, 16 | }, 17 | rules: { 18 | '@typescript-eslint/explicit-function-return-type': 'off', 19 | '@typescript-eslint/no-explicit-any': 'off', 20 | '@typescript-eslint/no-unused-vars': 'off', 21 | }, 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /apps/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cln-application-backend", 3 | "version": "0.0.7", 4 | "description": "Core lightning application backend", 5 | "private": true, 6 | "license": "MIT", 7 | "type": "module", 8 | "scripts": { 9 | "build": "rm -rf dist/ && prettier --write source/ && tsc --project tsconfig.json", 10 | "serve": "node ./dist/server.js", 11 | "start": "tsc --project tsconfig.json --watch & prettier --write source/ & nodemon ./dist/server.js", 12 | "watch": "tsc --project tsconfig.json --watch & prettier --write source/", 13 | "lint": "eslint source --ext .js,.ts" 14 | }, 15 | "dependencies": { 16 | "axios": "^1.7.7", 17 | "cookie-parser": "^1.4.6", 18 | "cors": "^2.8.5", 19 | "csurf": "^1.11.0", 20 | "express": "^4.18.2", 21 | "express-winston": "^4.2.0", 22 | "jsonwebtoken": "^9.0.2", 23 | "lnmessage": "^0.2.6", 24 | "protobufjs": "^7.4.0", 25 | "ts-node": "^10.9.1", 26 | "winston": "^3.11.0" 27 | }, 28 | "devDependencies": { 29 | "@eslint/js": "^9.22.0", 30 | "@types/cookie-parser": "^1.4.6", 31 | "@types/cors": "^2.8.17", 32 | "@types/csurf": "^1.11.5", 33 | "@types/express": "^4.17.21", 34 | "@types/jsonwebtoken": "^9.0.5", 35 | "@types/morgan": "^1.9.9", 36 | "@types/node": "^20.9.4", 37 | "@typescript-eslint/eslint-plugin": "^8.33.0", 38 | "@typescript-eslint/parser": "^8.33.0", 39 | "eslint": "^9.27.0", 40 | "eslint-plugin-node-dependencies": "^0.12.0", 41 | "eslint-plugin-react": "^7.35.2", 42 | "globals": "^16.0.0", 43 | "nodemon": "^3.0.1", 44 | "prettier": "^3.1.0", 45 | "typescript": "^5.8.3", 46 | "typescript-eslint": "^8.26.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /apps/backend/proto/primitives.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package cln; 3 | 4 | message Amount { 5 | uint64 msat = 1; 6 | } 7 | 8 | message AmountOrAll { 9 | oneof value { 10 | Amount amount = 1; 11 | bool all = 2; 12 | } 13 | } 14 | 15 | message AmountOrAny { 16 | oneof value { 17 | Amount amount = 1; 18 | bool any = 2; 19 | } 20 | } 21 | 22 | enum ChannelSide { 23 | LOCAL = 0; 24 | REMOTE = 1; 25 | } 26 | 27 | enum ChannelState { 28 | Openingd = 0; 29 | ChanneldAwaitingLockin = 1; 30 | ChanneldNormal = 2; 31 | ChanneldShuttingDown = 3; 32 | ClosingdSigexchange = 4; 33 | ClosingdComplete = 5; 34 | AwaitingUnilateral = 6; 35 | FundingSpendSeen = 7; 36 | Onchain = 8; 37 | DualopendOpenInit = 9; 38 | DualopendAwaitingLockin = 10; 39 | } 40 | 41 | enum HtlcState { 42 | SentAddHtlc = 0; 43 | SentAddCommit = 1; 44 | RcvdAddRevocation = 2; 45 | RcvdAddAckCommit = 3; 46 | SentAddAckRevocation = 4; 47 | RcvdAddAckRevocation = 5; 48 | RcvdRemoveHtlc = 6; 49 | RcvdRemoveCommit = 7; 50 | SentRemoveRevocation = 8; 51 | SentRemoveAckCommit = 9; 52 | RcvdRemoveAckRevocation = 10; 53 | RCVD_ADD_HTLC = 11; 54 | RCVD_ADD_COMMIT = 12; 55 | SENT_ADD_REVOCATION = 13; 56 | SENT_ADD_ACK_COMMIT = 14; 57 | SENT_REMOVE_HTLC = 15; 58 | SENT_REMOVE_COMMIT = 16; 59 | RCVD_REMOVE_REVOCATION = 17; 60 | RCVD_REMOVE_ACK_COMMIT = 18; 61 | SENT_REMOVE_ACK_REVOCATION = 19; 62 | } 63 | 64 | message ChannelStateChangeCause {} 65 | 66 | message Outpoint { 67 | bytes txid = 1; 68 | uint32 outnum = 2; 69 | } 70 | 71 | message Feerate { 72 | oneof style { 73 | bool slow = 1; 74 | bool normal = 2; 75 | bool urgent = 3; 76 | uint32 perkb = 4; 77 | uint32 perkw = 5; 78 | } 79 | } 80 | 81 | message OutputDesc { 82 | string address = 1; 83 | Amount amount = 2; 84 | } 85 | 86 | message RouteHop { 87 | bytes id = 1; 88 | string short_channel_id = 2; 89 | Amount feebase = 3; 90 | uint32 feeprop = 4; 91 | uint32 expirydelta = 5; 92 | } 93 | message Routehint { 94 | repeated RouteHop hops = 1; 95 | } 96 | message RoutehintList { 97 | repeated Routehint hints = 2; 98 | } 99 | 100 | 101 | message TlvEntry { 102 | uint64 type = 1; 103 | bytes value = 2; 104 | } 105 | message TlvStream { 106 | repeated TlvEntry entries = 1; 107 | } -------------------------------------------------------------------------------- /apps/backend/source/controllers/lightning.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import handleError from '../shared/error-handler.js'; 3 | import { CLNService, LightningService } from '../service/lightning.service.js'; 4 | import { logger } from '../shared/logger.js'; 5 | import { AppConnect, APP_CONSTANTS } from '../shared/consts.js'; 6 | 7 | const clnService: LightningService = CLNService; 8 | 9 | class LightningController { 10 | callMethod(req: Request, res: Response, next: NextFunction) { 11 | try { 12 | logger.info('Calling method: ' + req.body.method); 13 | clnService 14 | .call(req.body.method, req.body.params) 15 | .then((commandRes: any) => { 16 | logger.info( 17 | 'Controller received response for ' + 18 | req.body.method + 19 | ': ' + 20 | JSON.stringify(commandRes), 21 | ); 22 | if ( 23 | APP_CONSTANTS.APP_CONNECT == AppConnect.COMMANDO && 24 | req.body.method && 25 | req.body.method === 'listpeers' 26 | ) { 27 | // Filter out ln message pubkey from peers list 28 | const lnmPubkey = clnService.getLNMsgPubkey(); 29 | commandRes.peers = commandRes.peers.filter((peer: any) => peer.id !== lnmPubkey); 30 | res.status(200).json(commandRes); 31 | } else { 32 | res.status(200).json(commandRes); 33 | } 34 | }) 35 | .catch((err: any) => { 36 | logger.error( 37 | 'Controller caught lightning error from ' + 38 | req.body.method + 39 | ': ' + 40 | JSON.stringify(err), 41 | ); 42 | return handleError(err, req, res, next); 43 | }); 44 | } catch (error: any) { 45 | return handleError(error, req, res, next); 46 | } 47 | } 48 | } 49 | 50 | export default new LightningController(); 51 | -------------------------------------------------------------------------------- /apps/backend/source/models/errors.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatusCode } from '../shared/consts.js'; 2 | 3 | export class BaseError extends Error { 4 | public readonly code: HttpStatusCode; 5 | public readonly message: string; 6 | 7 | constructor(code: HttpStatusCode, message: string) { 8 | super(message); 9 | Object.setPrototypeOf(this, new.target.prototype); 10 | 11 | this.code = code; 12 | this.message = message; 13 | Error.captureStackTrace(this); 14 | } 15 | } 16 | 17 | export class APIError extends BaseError { 18 | constructor( 19 | code: HttpStatusCode = HttpStatusCode.INTERNAL_SERVER, 20 | message: string = 'Unknown API Server Error', 21 | ) { 22 | super(code, message); 23 | } 24 | } 25 | 26 | export class BitcoindError extends BaseError { 27 | constructor( 28 | code: HttpStatusCode = HttpStatusCode.BITCOIN_SERVER, 29 | message: string = 'Unknown Bitcoin API Error', 30 | ) { 31 | super(code, message); 32 | } 33 | } 34 | 35 | export class LightningError extends BaseError { 36 | constructor( 37 | code: HttpStatusCode = HttpStatusCode.LIGHTNING_SERVER, 38 | message: string = 'Unknown Core Lightning API Error', 39 | ) { 40 | super(code, message); 41 | } 42 | } 43 | 44 | export class ValidationError extends BaseError { 45 | constructor( 46 | code: HttpStatusCode = HttpStatusCode.INVALID_DATA, 47 | message: string = 'Unknown Validation Error', 48 | ) { 49 | super(code, message); 50 | } 51 | } 52 | 53 | export class AuthError extends BaseError { 54 | constructor( 55 | code: HttpStatusCode = HttpStatusCode.UNAUTHORIZED, 56 | message: string = 'Unknown Authentication Error', 57 | ) { 58 | super(code, message); 59 | } 60 | } 61 | 62 | export class GRPCError extends BaseError { 63 | constructor( 64 | code: HttpStatusCode = HttpStatusCode.GRPC_UNKNOWN, 65 | message: string = 'Unknown gRPC Error', 66 | ) { 67 | super(code, message); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /apps/backend/source/models/showrunes.type.ts: -------------------------------------------------------------------------------- 1 | export type ShowRunes = { 2 | runes: Rune[]; 3 | isLoading: boolean; 4 | error?: any; 5 | }; 6 | 7 | export type Rune = { 8 | rune: string; 9 | unique_id: string; 10 | restrictions: Restriction[]; 11 | restrictions_as_english: string; 12 | stored?: boolean; 13 | blacklisted?: boolean; 14 | last_used?: number; 15 | our_rune?: boolean; 16 | }; 17 | 18 | export type Restriction = { 19 | alternatives: Alternative[]; 20 | english: string; 21 | }; 22 | 23 | export type Alternative = { 24 | fieldname: string; 25 | value: string; 26 | condition: string; 27 | english: string; 28 | }; 29 | -------------------------------------------------------------------------------- /apps/backend/source/routes/v1/auth.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { CommonRoutesConfig } from '../../shared/routes.config.js'; 3 | import AuthController from '../../controllers/auth.js'; 4 | import { API_VERSION } from '../../shared/consts.js'; 5 | 6 | const AUTH_ROUTE = '/auth'; 7 | 8 | export class AuthRoutes extends CommonRoutesConfig { 9 | constructor(app: express.Application) { 10 | super(app, 'Auth Routes'); 11 | } 12 | 13 | configureRoutes() { 14 | this.app.route(API_VERSION + AUTH_ROUTE + '/logout/').get(AuthController.userLogout); 15 | this.app.route(API_VERSION + AUTH_ROUTE + '/login/').post(AuthController.userLogin); 16 | this.app.route(API_VERSION + AUTH_ROUTE + '/reset/').post(AuthController.resetPassword); 17 | this.app 18 | .route(API_VERSION + AUTH_ROUTE + '/isauthenticated/') 19 | .post(AuthController.isUserAuthenticated); 20 | return this.app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/backend/source/routes/v1/lightning.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { CommonRoutesConfig } from '../../shared/routes.config.js'; 3 | import AuthController from '../../controllers/auth.js'; 4 | import LightningController from '../../controllers/lightning.js'; 5 | import { API_VERSION } from '../../shared/consts.js'; 6 | 7 | const LIGHTNING_ROOT_ROUTE = '/cln'; 8 | 9 | export class LightningRoutes extends CommonRoutesConfig { 10 | constructor(app: express.Application) { 11 | super(app, 'Lightning Routes'); 12 | } 13 | 14 | configureRoutes() { 15 | this.app 16 | .route(API_VERSION + LIGHTNING_ROOT_ROUTE + '/call') 17 | .post(AuthController.isUserAuthenticated, LightningController.callMethod); 18 | return this.app; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/backend/source/routes/v1/shared.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { CommonRoutesConfig } from '../../shared/routes.config.js'; 3 | import AuthController from '../../controllers/auth.js'; 4 | import SharedController from '../../controllers/shared.js'; 5 | import { API_VERSION } from '../../shared/consts.js'; 6 | 7 | const SHARED_ROUTE = '/shared'; 8 | 9 | export class SharedRoutes extends CommonRoutesConfig { 10 | constructor(app: express.Application) { 11 | super(app, 'Shared Routes'); 12 | } 13 | 14 | configureRoutes() { 15 | this.app.route(API_VERSION + SHARED_ROUTE + '/csrf/').get((req, res, next) => { 16 | res.send({ 17 | csrfToken: 18 | req.csrfToken && typeof req.csrfToken === 'function' ? req.csrfToken() : 'not-set', 19 | }); 20 | }); 21 | this.app 22 | .route(API_VERSION + SHARED_ROUTE + '/config/') 23 | .get(SharedController.getApplicationSettings); 24 | this.app 25 | .route(API_VERSION + SHARED_ROUTE + '/config/') 26 | .post(AuthController.isUserAuthenticated, SharedController.setApplicationSettings); 27 | this.app 28 | .route(API_VERSION + SHARED_ROUTE + '/connectwallet/') 29 | .get(AuthController.isUserAuthenticated, SharedController.getWalletConnectSettings); 30 | this.app 31 | .route(API_VERSION + SHARED_ROUTE + '/rate/:fiatCurrency') 32 | .get(SharedController.getFiatRate); 33 | this.app 34 | .route(API_VERSION + SHARED_ROUTE + '/saveinvoicerune/') 35 | .post(AuthController.isUserAuthenticated, SharedController.saveInvoiceRune); 36 | return this.app; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /apps/backend/source/shared/error-handler.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { 3 | APIError, 4 | BaseError, 5 | BitcoindError, 6 | GRPCError, 7 | LightningError, 8 | ValidationError, 9 | } from '../models/errors.js'; 10 | import { HttpStatusCode } from './consts.js'; 11 | import { logger } from './logger.js'; 12 | 13 | function handleError( 14 | error: BaseError | APIError | BitcoindError | LightningError | ValidationError | GRPCError, 15 | req: Request, 16 | res: Response, 17 | next?: NextFunction, 18 | ) { 19 | const route = req.url || ''; 20 | const message = error.message 21 | ? error.message 22 | : typeof error === 'object' 23 | ? JSON.stringify(error) 24 | : typeof error === 'string' 25 | ? error 26 | : 'Unknown Error!'; 27 | logger.error(message, route, error.stack); 28 | return res.status(error.code || HttpStatusCode.INTERNAL_SERVER).json(message); 29 | } 30 | 31 | export default handleError; 32 | -------------------------------------------------------------------------------- /apps/backend/source/shared/logger.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | import { Environment, APP_CONSTANTS } from './consts.js'; 3 | 4 | export const enum LogLevel { 5 | ERROR = 'error', 6 | WARN = 'warn', 7 | INFO = 'info', 8 | VERBOSE = 'verbose', 9 | DEBUG = 'debug', 10 | } 11 | 12 | export const logConfiguration = { 13 | transports: [ 14 | new winston.transports.Console({ 15 | level: 16 | APP_CONSTANTS.APP_MODE === Environment.PRODUCTION 17 | ? LogLevel.WARN 18 | : APP_CONSTANTS.APP_MODE === Environment.TESTING 19 | ? LogLevel.DEBUG 20 | : LogLevel.INFO, 21 | format: winston.format.combine( 22 | winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), 23 | winston.format.timestamp(), 24 | winston.format.align(), 25 | winston.format.json(), 26 | winston.format.colorize({ all: true }), 27 | ), 28 | }), 29 | new winston.transports.File({ 30 | filename: APP_CONSTANTS.APP_LOG_FILE, 31 | level: 32 | APP_CONSTANTS.APP_MODE === Environment.PRODUCTION 33 | ? LogLevel.WARN 34 | : APP_CONSTANTS.APP_MODE === Environment.TESTING 35 | ? LogLevel.DEBUG 36 | : LogLevel.INFO, 37 | format: winston.format.combine( 38 | winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), 39 | winston.format.timestamp(), 40 | winston.format.align(), 41 | winston.format.json(), 42 | winston.format.colorize({ all: true }), 43 | ), 44 | }), 45 | ], 46 | }; 47 | 48 | export const expressLogConfiguration = { 49 | ...logConfiguration, 50 | meta: APP_CONSTANTS.APP_MODE !== Environment.PRODUCTION, 51 | message: 'HTTP {{res.statusCode}} {{req.method}} {{res.responseTime}}ms {{req.url}}', 52 | expressFormat: false, 53 | colorize: true, 54 | }; 55 | 56 | export const logger = winston.createLogger(logConfiguration); 57 | -------------------------------------------------------------------------------- /apps/backend/source/shared/routes.config.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | export abstract class CommonRoutesConfig { 4 | app: express.Application; 5 | name: string; 6 | 7 | constructor(app: express.Application, name: string) { 8 | this.app = app; 9 | this.name = name; 10 | this.configureRoutes(); 11 | } 12 | 13 | getName() { 14 | return this.name; 15 | } 16 | 17 | abstract configureRoutes(): express.Application; 18 | } 19 | -------------------------------------------------------------------------------- /apps/backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "ES2022", 5 | "module": "ES2022", 6 | "moduleResolution": "node", 7 | "outDir": "./dist", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "inlineSourceMap": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "rootDir": "./source", 13 | "skipLibCheck": true, 14 | "sourceMap": false, 15 | "lib": ["ES2022"] 16 | }, 17 | "files": ["./source/server.ts"], 18 | "include": ["./source/**/*.d.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /apps/frontend/build/fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/build/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /apps/frontend/build/fonts/Inter-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/build/fonts/Inter-Medium.ttf -------------------------------------------------------------------------------- /apps/frontend/build/fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/build/fonts/Inter-Regular.ttf -------------------------------------------------------------------------------- /apps/frontend/build/fonts/Inter-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/build/fonts/Inter-SemiBold.ttf -------------------------------------------------------------------------------- /apps/frontend/build/fonts/Inter-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/build/fonts/Inter-Thin.ttf -------------------------------------------------------------------------------- /apps/frontend/build/fonts/README.txt: -------------------------------------------------------------------------------- 1 | Inter Variable Font 2 | =================== 3 | 4 | This download contains Inter as both a variable font and static fonts. 5 | 6 | Inter is a variable font with these axes: 7 | slnt 8 | wght 9 | 10 | This means all the styles are contained in a single file: 11 | Inter-VariableFont_slnt,wght.ttf 12 | 13 | If your app fully supports variable fonts, you can now pick intermediate styles 14 | that aren’t available as static fonts. Not all apps support variable fonts, and 15 | in those cases you can use the static font files for Inter: 16 | static/Inter-Thin.ttf 17 | static/Inter-ExtraLight.ttf 18 | static/Inter-Light.ttf 19 | static/Inter-Regular.ttf 20 | static/Inter-Medium.ttf 21 | static/Inter-SemiBold.ttf 22 | static/Inter-Bold.ttf 23 | static/Inter-ExtraBold.ttf 24 | static/Inter-Black.ttf 25 | 26 | Get started 27 | ----------- 28 | 29 | 1. Install the font files you want to use 30 | 31 | 2. Use your app's font picker to view the font family and all the 32 | available styles 33 | 34 | Learn more about variable fonts 35 | ------------------------------- 36 | 37 | https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts 38 | https://variablefonts.typenetwork.com 39 | https://medium.com/variable-fonts 40 | 41 | In desktop apps 42 | 43 | https://theblog.adobe.com/can-variable-fonts-illustrator-cc 44 | https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts 45 | 46 | Online 47 | 48 | https://developers.google.com/fonts/docs/getting_started 49 | https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide 50 | https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts 51 | 52 | Installing fonts 53 | 54 | MacOS: https://support.apple.com/en-us/HT201749 55 | Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux 56 | Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows 57 | 58 | Android Apps 59 | 60 | https://developers.google.com/fonts/docs/android 61 | https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts 62 | 63 | License 64 | ------- 65 | Please read the full license text (OFL.txt) to understand the permissions, 66 | restrictions and requirements for usage, redistribution, and modification. 67 | 68 | You can use them in your products & projects – print or digital, 69 | commercial or otherwise. 70 | 71 | This isn't legal advice, please consider consulting a lawyer and see the full 72 | license for all details. 73 | -------------------------------------------------------------------------------- /apps/frontend/build/images/cln-favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/build/images/cln-favicon.ico -------------------------------------------------------------------------------- /apps/frontend/build/images/cln-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/build/images/cln-logo-dark.png -------------------------------------------------------------------------------- /apps/frontend/build/images/cln-logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/frontend/build/images/cln-logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/build/images/cln-logo-light.png -------------------------------------------------------------------------------- /apps/frontend/build/images/cln-logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/frontend/build/index.html: -------------------------------------------------------------------------------- 1 | Core Lightning
-------------------------------------------------------------------------------- /apps/frontend/build/static/js/213.6d084fd7.chunk.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license qrcode.react 3 | * Copyright (c) Paul O'Shannessy 4 | * SPDX-License-Identifier: ISC 5 | */ 6 | -------------------------------------------------------------------------------- /apps/frontend/build/static/js/813.cdbe58bb.chunk.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | * Copyright 2024 Fonticons, Inc. 5 | */ 6 | -------------------------------------------------------------------------------- /apps/frontend/build/static/media/Inter-Bold.88fa7ae373b07b41ecce.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/build/static/media/Inter-Bold.88fa7ae373b07b41ecce.ttf -------------------------------------------------------------------------------- /apps/frontend/build/static/media/Inter-Medium.6dcbc9bed1ec438907ee.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/build/static/media/Inter-Medium.6dcbc9bed1ec438907ee.ttf -------------------------------------------------------------------------------- /apps/frontend/build/static/media/Inter-SemiBold.4d56bb21f2399db8ad48.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/build/static/media/Inter-SemiBold.4d56bb21f2399db8ad48.ttf -------------------------------------------------------------------------------- /apps/frontend/build/static/media/Inter-Thin.f341ca512063c66296d1.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/build/static/media/Inter-Thin.f341ca512063c66296d1.ttf -------------------------------------------------------------------------------- /apps/frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports */ 2 | const tsParser = require('@typescript-eslint/parser'); 3 | const tsPlugin = require('@typescript-eslint/eslint-plugin'); 4 | const react = require('eslint-plugin-react'); 5 | const reactHooks = require('eslint-plugin-react-hooks'); 6 | const globals = require('globals'); 7 | 8 | module.exports = [ 9 | { 10 | files: ['**/*.{js,jsx,ts,tsx}'], 11 | languageOptions: { 12 | parser: tsParser, 13 | globals: { 14 | ...globals.browser, 15 | ...globals.node, 16 | }, 17 | parserOptions: { 18 | ecmaFeatures: { 19 | jsx: true, 20 | }, 21 | ecmaVersion: 'latest', 22 | sourceType: 'module', 23 | }, 24 | }, 25 | plugins: { 26 | '@typescript-eslint': tsPlugin, 27 | react, 28 | 'react-hooks': reactHooks, 29 | }, 30 | settings: { 31 | react: { 32 | version: 'detect', 33 | }, 34 | }, 35 | rules: { 36 | ...tsPlugin.configs.recommended.rules, 37 | ...react.configs.recommended.rules, 38 | 'react/react-in-jsx-scope': 'off', 39 | 'react/prop-types': 'off', 40 | 'react/display-name': 'off', 41 | 'react-hooks/rules-of-hooks': 'error', 42 | '@typescript-eslint/no-explicit-any': 'off', 43 | '@typescript-eslint/no-unused-vars': 'off', 44 | }, 45 | }, 46 | ]; 47 | -------------------------------------------------------------------------------- /apps/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cln-application-frontend", 3 | "version": "0.0.7", 4 | "description": "Core lightning application frontend", 5 | "private": true, 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "PORT=4300 react-scripts start", 9 | "build": "react-scripts build", 10 | "test": "react-scripts test", 11 | "lint": "eslint src --ext .js,.jsx,.ts,.tsx" 12 | }, 13 | "dependencies": { 14 | "@fortawesome/fontawesome-svg-core": "^6.4.2", 15 | "@fortawesome/free-solid-svg-icons": "^6.4.2", 16 | "@fortawesome/react-fontawesome": "^0.2.0", 17 | "@reduxjs/toolkit": "^2.7.0", 18 | "axios": "^1.6.7", 19 | "bootstrap": "^5.3.2", 20 | "copy-to-clipboard": "^3.3.3", 21 | "crypto-js": "^4.2.0", 22 | "framer-motion": "^10.16.5", 23 | "moment": "^2.30.1", 24 | "node-sass": "^9.0.0", 25 | "qrcode.react": "^3.1.0", 26 | "react": "^18.2.0", 27 | "react-bootstrap": "^2.10.1", 28 | "react-datepicker": "^8.1.0", 29 | "react-dom": "^18.2.0", 30 | "react-perfect-scrollbar": "^1.5.8", 31 | "react-redux": "^9.2.0", 32 | "react-router-dom": "^6.30.0", 33 | "recharts": "^2.15.1", 34 | "redux-thunk": "^3.1.0" 35 | }, 36 | "devDependencies": { 37 | "@testing-library/jest-dom": "^6.4.2", 38 | "@testing-library/react": "^14.2.1", 39 | "@testing-library/user-event": "^14.5.1", 40 | "@types/jest": "^29.5.12", 41 | "@types/node": "^20.9.4", 42 | "@types/react": "^18.2.61", 43 | "@types/react-dom": "^18.2.19", 44 | "@types/redux-mock-store": "^1.5.0", 45 | "axios-mock-adapter": "^1.22.0", 46 | "canvas": "^2.11.2", 47 | "react-scripts": "^5.0.1", 48 | "redux-mock-store": "^1.5.5", 49 | "@typescript-eslint/eslint-plugin": "^8.33.0", 50 | "@typescript-eslint/parser": "^8.33.0", 51 | "eslint": "^9.27.0", 52 | "eslint-plugin-jsx-a11y": "^6.10.2", 53 | "eslint-plugin-react": "^7.37.5", 54 | "eslint-plugin-react-hooks": "^5.2.0", 55 | "ts-jest": "^29.1.2", 56 | "typescript": "^5.8.2" 57 | }, 58 | "browserslist": { 59 | "production": [ 60 | ">0.2%", 61 | "not dead", 62 | "not op_mini all" 63 | ], 64 | "development": [ 65 | "last 1 chrome version", 66 | "last 1 firefox version", 67 | "last 1 safari version" 68 | ] 69 | }, 70 | "jest": { 71 | "transform": { 72 | "^.+\\.[t|j]sx?$": "ts-jest" 73 | }, 74 | "transformIgnorePatterns": [ 75 | "/node_modules/(?!axios|react-router-dom)/" 76 | ] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /apps/frontend/public/fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/public/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /apps/frontend/public/fonts/Inter-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/public/fonts/Inter-Medium.ttf -------------------------------------------------------------------------------- /apps/frontend/public/fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/public/fonts/Inter-Regular.ttf -------------------------------------------------------------------------------- /apps/frontend/public/fonts/Inter-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/public/fonts/Inter-SemiBold.ttf -------------------------------------------------------------------------------- /apps/frontend/public/fonts/Inter-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/public/fonts/Inter-Thin.ttf -------------------------------------------------------------------------------- /apps/frontend/public/fonts/README.txt: -------------------------------------------------------------------------------- 1 | Inter Variable Font 2 | =================== 3 | 4 | This download contains Inter as both a variable font and static fonts. 5 | 6 | Inter is a variable font with these axes: 7 | slnt 8 | wght 9 | 10 | This means all the styles are contained in a single file: 11 | Inter-VariableFont_slnt,wght.ttf 12 | 13 | If your app fully supports variable fonts, you can now pick intermediate styles 14 | that aren’t available as static fonts. Not all apps support variable fonts, and 15 | in those cases you can use the static font files for Inter: 16 | static/Inter-Thin.ttf 17 | static/Inter-ExtraLight.ttf 18 | static/Inter-Light.ttf 19 | static/Inter-Regular.ttf 20 | static/Inter-Medium.ttf 21 | static/Inter-SemiBold.ttf 22 | static/Inter-Bold.ttf 23 | static/Inter-ExtraBold.ttf 24 | static/Inter-Black.ttf 25 | 26 | Get started 27 | ----------- 28 | 29 | 1. Install the font files you want to use 30 | 31 | 2. Use your app's font picker to view the font family and all the 32 | available styles 33 | 34 | Learn more about variable fonts 35 | ------------------------------- 36 | 37 | https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts 38 | https://variablefonts.typenetwork.com 39 | https://medium.com/variable-fonts 40 | 41 | In desktop apps 42 | 43 | https://theblog.adobe.com/can-variable-fonts-illustrator-cc 44 | https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts 45 | 46 | Online 47 | 48 | https://developers.google.com/fonts/docs/getting_started 49 | https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide 50 | https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts 51 | 52 | Installing fonts 53 | 54 | MacOS: https://support.apple.com/en-us/HT201749 55 | Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux 56 | Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows 57 | 58 | Android Apps 59 | 60 | https://developers.google.com/fonts/docs/android 61 | https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts 62 | 63 | License 64 | ------- 65 | Please read the full license text (OFL.txt) to understand the permissions, 66 | restrictions and requirements for usage, redistribution, and modification. 67 | 68 | You can use them in your products & projects – print or digital, 69 | commercial or otherwise. 70 | 71 | This isn't legal advice, please consider consulting a lawyer and see the full 72 | license for all details. 73 | -------------------------------------------------------------------------------- /apps/frontend/public/images/cln-favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/public/images/cln-favicon.ico -------------------------------------------------------------------------------- /apps/frontend/public/images/cln-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/public/images/cln-logo-dark.png -------------------------------------------------------------------------------- /apps/frontend/public/images/cln-logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/frontend/public/images/cln-logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/public/images/cln-logo-light.png -------------------------------------------------------------------------------- /apps/frontend/public/images/cln-logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Core Lightning 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /apps/frontend/src/components/App/App.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/bootstrap-custom'; 2 | @import '../../styles/constants'; 3 | @import '../../styles/shared'; 4 | 5 | .list-scroll-container { 6 | max-height: 48vh; 7 | } 8 | -------------------------------------------------------------------------------- /apps/frontend/src/components/App/App.tsx: -------------------------------------------------------------------------------- 1 | import './App.scss'; 2 | import { Container } from 'react-bootstrap'; 3 | 4 | import useBreakpoint from '../../hooks/use-breakpoint'; 5 | import ToastMessage from '../shared/ToastMessage/ToastMessage'; 6 | import NodeInfo from '../modals/NodeInfo/NodeInfo'; 7 | import ConnectWallet from '../modals/ConnectWallet/ConnectWallet'; 8 | import LoginComponent from '../modals/Login/Login'; 9 | import LogoutComponent from '../modals/Logout/Logout'; 10 | import SetPasswordComponent from '../modals/SetPassword/SetPassword'; 11 | import RouteTransition from '../ui/RouteTransition/RouteTransition'; 12 | import { useSelector } from 'react-redux'; 13 | import { selectAppMode, selectIsAuthenticated, selectIsDarkMode } from '../../store/rootSelectors'; 14 | import SQLTerminal from '../modals/SQLTerminal/SQLTerminal'; 15 | 16 | export const App = () => { 17 | const currentScreenSize = useBreakpoint(); 18 | const isAuthenticated = useSelector(selectIsAuthenticated); 19 | const appMode = useSelector(selectAppMode); 20 | const isDarkMode = useSelector(selectIsDarkMode); 21 | const containerClassName = isAuthenticated ? 'py-4' : 'py-4 blurred-container'; 22 | const bodyHTML = document.getElementsByTagName('body')[0]; 23 | const htmlAttributes = bodyHTML.attributes; 24 | const theme = document.createAttribute('data-bs-theme'); 25 | theme.value = appMode.toLowerCase() || 'dark'; 26 | bodyHTML.style.backgroundColor = isDarkMode ? '#0C0C0F' : '#EBEFF9'; 27 | const screensize = document.createAttribute('data-screensize'); 28 | screensize.value = currentScreenSize; 29 | htmlAttributes.setNamedItem(theme); 30 | htmlAttributes.setNamedItem(screensize); 31 | 32 | return ( 33 | <> 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default App; 49 | -------------------------------------------------------------------------------- /apps/frontend/src/components/bookkeeper/AccountEvents/AccountEventsGraph/AccountEventsGraph.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/constants.scss'; 2 | 3 | .bkpr-tooltip { 4 | border: 1px solid $light-dark; 5 | color: $dark; 6 | background-color: $white; 7 | border-radius: 4px; 8 | box-shadow: 0px 4px 8px 0px rgba($gray-400, 0.16); 9 | } 10 | 11 | .account-events-graph { 12 | width: 100%; 13 | height: 100%; 14 | } 15 | 16 | @include color-mode(dark) { 17 | .bkpr-tooltip { 18 | border: 1px solid $card-bg-dark; 19 | color: $light-dark; 20 | background-color: $tooltip-bg-dark; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/frontend/src/components/bookkeeper/AccountEvents/AccountEventsRoot.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .account-events-container { 4 | height: 85vh; 5 | max-height: 85vh; 6 | } 7 | 8 | .account-events-graph-container { 9 | height: 40vh; 10 | max-height: 40vh; 11 | } 12 | 13 | .account-events-table-container { 14 | height: 25vh; 15 | max-height: 25vh; 16 | } 17 | -------------------------------------------------------------------------------- /apps/frontend/src/components/bookkeeper/AccountEvents/AccountEventsRoot.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react'; 2 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 3 | import { spyOnBKPRGetAccountEvents, spyOnBKPRGetSatsFlow, spyOnBKPRGetVolume } from '../../../utilities/test-utilities/mockService'; 4 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 5 | import AccountEventsRoot from './AccountEventsRoot'; 6 | 7 | describe('Account Events component', () => { 8 | beforeEach(() => { 9 | global.ResizeObserver = jest.fn().mockImplementation(() => ({ 10 | observe: jest.fn(), 11 | unobserve: jest.fn(), 12 | disconnect: jest.fn(), 13 | })); 14 | }); 15 | 16 | it('should be in the document', async () => { 17 | spyOnBKPRGetAccountEvents(); 18 | spyOnBKPRGetSatsFlow(); 19 | spyOnBKPRGetVolume(); 20 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/bookkeeper/accountevents'] }); 21 | expect(screen.getByTestId('account-events-container')).not.toBeEmptyDOMElement(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /apps/frontend/src/components/bookkeeper/BkprHome/AccountEventsInfo/AccountEventsInfo.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/constants.scss'; 2 | -------------------------------------------------------------------------------- /apps/frontend/src/components/bookkeeper/BkprHome/BkprHome.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | [data-screensize='SM'], 4 | [data-screensize='XS'] { 5 | & .cards-container { 6 | height: 77vh; 7 | } 8 | } 9 | 10 | .bookkeeper-dashboard-container { 11 | padding: 0 1rem; 12 | } 13 | 14 | .positive { 15 | color: $success; 16 | line-break: anywhere; 17 | } 18 | 19 | .negative { 20 | color: $warning; 21 | line-break: anywhere; 22 | } 23 | 24 | .primary { 25 | color: $primary; 26 | line-break: anywhere; 27 | } 28 | -------------------------------------------------------------------------------- /apps/frontend/src/components/bookkeeper/BkprHome/BkprHome.tsx: -------------------------------------------------------------------------------- 1 | import './BkprHome.scss'; 2 | import { useLocation } from 'react-router-dom'; 3 | import { Row, Col } from 'react-bootstrap'; 4 | import RouteTransition from '../../ui/RouteTransition/RouteTransition'; 5 | import SatsFlowInfo from './SatsFlowInfo/SatsFlowInfo'; 6 | import VolumeInfo from './VolumeInfo/VolumeInfo'; 7 | import Overview from '../../cln/Overview/Overview'; 8 | import Header from '../../ui/Header/Header'; 9 | import AccountEventsInfo from './AccountEventsInfo/AccountEventsInfo'; 10 | import { useSelector } from 'react-redux'; 11 | import { useInjectReducer } from '../../../hooks/use-injectreducer'; 12 | import bkprReducer from '../../../store/bkprSlice'; 13 | import { selectNodeInfo } from '../../../store/rootSelectors'; 14 | 15 | const Bookkeeper = () => { 16 | useInjectReducer('bkpr', bkprReducer); 17 | const nodeInfo = useSelector(selectNodeInfo); 18 | const location = useLocation(); 19 | 20 | if (nodeInfo.error) { 21 | return ( 22 | 23 | 24 | {nodeInfo.error} 25 | 26 | 27 | ); 28 | } 29 | 30 | return ( 31 |
32 |
33 | 34 | {location.pathname === '/bookkeeper' && ( 35 | <> 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | )} 52 |
53 | ); 54 | }; 55 | 56 | export default Bookkeeper; 57 | -------------------------------------------------------------------------------- /apps/frontend/src/components/bookkeeper/BkprHome/SatsFlowInfo/SatsFlowInfo.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/constants.scss'; 2 | 3 | [data-screensize='SM'], 4 | [data-screensize='XS'] { 5 | & .sats-flow-info { 6 | max-height: 42vh; 7 | min-height: 42vh; 8 | } 9 | & .card-body { 10 | flex-direction: column !important; 11 | } 12 | } 13 | 14 | .sats-flow-info { 15 | max-height: 32vh; 16 | min-height: 32vh; 17 | margin-bottom: 1.5rem; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /apps/frontend/src/components/bookkeeper/BkprHome/VolumeInfo/VolumeInfo.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/constants.scss'; 2 | 3 | [data-screensize='SM'], 4 | [data-screensize='XS'] { 5 | & .volume-info { 6 | max-height: 42vh; 7 | min-height: 42vh; 8 | } 9 | } 10 | 11 | .volume-info { 12 | max-height: 32vh; 13 | min-height: 32vh; 14 | } 15 | 16 | .tooltip { 17 | & .tooltip-inner { 18 | max-width: 30vw; 19 | width: auto; 20 | white-space: pre-line; 21 | text-align: left; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/frontend/src/components/bookkeeper/SatsFlow/SatsFlowGraph/SatsFlowGraph.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/constants.scss'; 2 | 3 | .bkpr-tooltip { 4 | border: 1px solid $light-dark; 5 | color: $dark; 6 | background-color: $white; 7 | border-radius: 4px; 8 | box-shadow: 0px 4px 8px 0px rgba($gray-400, 0.16); 9 | } 10 | 11 | .sats-flow-lagend-bullet { 12 | width: 12px; 13 | height: 12px; 14 | margin-right: 6px; 15 | border-radius: 2px; 16 | } 17 | 18 | .sats-flow-graph { 19 | width: 100%; 20 | height: 100%; 21 | } 22 | 23 | .series-net-inflow.recharts-layer { 24 | & path.recharts-curve.recharts-line-curve { 25 | stroke-width: 1; 26 | stroke: var(--bs-body-color); 27 | } 28 | & .recharts-line-dots circle.recharts-dot.recharts-line-dot { 29 | r: 2; 30 | stroke: var(--bs-body-color); 31 | } 32 | } 33 | 34 | @include color-mode(dark) { 35 | .bkpr-tooltip { 36 | border: 1px solid $card-bg-dark; 37 | color: $light-dark; 38 | background-color: $tooltip-bg-dark; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/frontend/src/components/bookkeeper/SatsFlow/SatsFlowGraph/SatsFlowGraph.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react'; 2 | import { mockAppStore, mockBKPRSatsFlowEvents } from '../../../../utilities/test-utilities/mockData'; 3 | import { renderWithProviders } from '../../../../utilities/test-utilities/mockStore'; 4 | import SatsFlowGraph from './SatsFlowGraph'; 5 | 6 | describe('Sats Flow Graph component ', () => { 7 | beforeEach(() => { 8 | class ResizeObserverMock { 9 | private callback: ResizeObserverCallback; 10 | constructor(callback: ResizeObserverCallback) { 11 | this.callback = callback; 12 | } 13 | observe = (target: Element) => { 14 | // Simulate dimensions 15 | this.callback( 16 | [ 17 | { 18 | target, 19 | contentRect: { 20 | width: 500, 21 | height: 400, 22 | top: 0, 23 | left: 0, 24 | bottom: 400, 25 | right: 500, 26 | x: 0, 27 | y: 0, 28 | toJSON: () => {}, 29 | }, 30 | } as ResizeObserverEntry, 31 | ], 32 | this 33 | ); 34 | }; 35 | unobserve = jest.fn(); 36 | disconnect = jest.fn(); 37 | } 38 | global.ResizeObserver = ResizeObserverMock as unknown as typeof ResizeObserver; 39 | Object.defineProperty(HTMLElement.prototype, 'clientWidth', { configurable: true, value: 500 }); 40 | Object.defineProperty(HTMLElement.prototype, 'clientHeight', { configurable: true, value: 400 }); 41 | }); 42 | 43 | it('should be in the document', async () => { 44 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/bookkeeper/satsflow'] }); 45 | expect(screen.getByTestId('sats-flow-graph')).not.toBeEmptyDOMElement(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /apps/frontend/src/components/bookkeeper/SatsFlow/SatsFlowRoot.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .satsflow-container { 4 | height: 80vh; 5 | max-height: 80vh; 6 | } 7 | 8 | .sats-flow-graph-container { 9 | height: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /apps/frontend/src/components/bookkeeper/SatsFlow/SatsFlowRoot.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react'; 2 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 3 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 4 | import SatsFlowRoot from './SatsFlowRoot'; 5 | 6 | describe('Sats Flow component ', () => { 7 | beforeEach(() => { 8 | global.ResizeObserver = jest.fn().mockImplementation(() => ({ 9 | observe: jest.fn(), 10 | unobserve: jest.fn(), 11 | disconnect: jest.fn(), 12 | })); 13 | }); 14 | 15 | it('should be in the document', async () => { 16 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/bookkeeper/satsflow'] }); 17 | expect(screen.getByTestId('satsflow-container')).not.toBeEmptyDOMElement(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /apps/frontend/src/components/bookkeeper/Volume/VolumeGraph/VolumeGraph.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/constants.scss'; 2 | 3 | .volume-graph { 4 | width: 100%; 5 | height: 100%; 6 | } 7 | 8 | .recharts-wrapper { 9 | opacity: 0.85; 10 | & .recharts-sector { 11 | stroke-width: 4px; 12 | stroke: $white; 13 | } 14 | } 15 | 16 | .bkpr-tooltip { 17 | border: 1px solid $light-dark; 18 | color: $dark; 19 | background-color: $white; 20 | border-radius: 4px; 21 | box-shadow: 0px 4px 8px 0px rgba($gray-400, 0.16); 22 | } 23 | 24 | .volume-graph-legend { 25 | padding-top: 2rem; 26 | width: 100%; 27 | justify-content: center; 28 | & .badge:empty { 29 | display: flex; 30 | } 31 | & .legend-color { 32 | width: 14px; 33 | height: 14px; 34 | } 35 | & .legend-label { 36 | max-width: 5rem; 37 | } 38 | } 39 | 40 | @include color-mode(dark) { 41 | .recharts-wrapper { 42 | opacity: 1; 43 | & .recharts-sector { 44 | stroke: $card-bg-dark; 45 | } 46 | } 47 | .bkpr-tooltip { 48 | border: 1px solid $card-bg-dark; 49 | color: $light-dark; 50 | background-color: $tooltip-bg-dark; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /apps/frontend/src/components/bookkeeper/Volume/VolumeRoot.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .volume-container { 4 | height: 80vh; 5 | max-height: 80vh; 6 | } 7 | 8 | .volume-graph-container { 9 | height: 100%; 10 | } -------------------------------------------------------------------------------- /apps/frontend/src/components/bookkeeper/Volume/VolumeRoot.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react'; 2 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 3 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 4 | import VolumeRoot from './VolumeRoot'; 5 | 6 | describe('Volume component ', () => { 7 | beforeEach(() => { 8 | global.ResizeObserver = jest.fn().mockImplementation(() => ({ 9 | observe: jest.fn(), 10 | unobserve: jest.fn(), 11 | disconnect: jest.fn(), 12 | })); 13 | }); 14 | 15 | it('should be in the document', async () => { 16 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/bookkeeper/volume'] }); 17 | expect(screen.getByTestId('volume-container')).not.toBeEmptyDOMElement(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /apps/frontend/src/components/bookkeeper/Volume/VolumeRoot.tsx: -------------------------------------------------------------------------------- 1 | import './VolumeRoot.scss'; 2 | import { Card, Row, Col } from 'react-bootstrap'; 3 | import VolumeGraph from './VolumeGraph/VolumeGraph'; 4 | import { CloseSVG } from '../../../svgs/Close'; 5 | import { useNavigate } from 'react-router-dom'; 6 | 7 | const VolumeRoot = props => { 8 | const navigate = useNavigate(); 9 | 10 | return ( 11 |
12 | 13 | 14 | 15 | Volume Chart 16 | 17 | { 20 | navigate('..'); 21 | }} 22 | > 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | ); 36 | }; 37 | 38 | export default VolumeRoot; 39 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/BTCCard/BTCCard.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/src/components/cln/BTCCard/BTCCard.scss -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/BTCCard/BTCCard.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react'; 2 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 3 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 4 | import BTCCard from './BTCCard'; 5 | 6 | describe('BTCCard component ', () => { 7 | it('should be in the document', async () => { 8 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/cln'] }); 9 | expect(screen.getByTestId('btc-card')).not.toBeEmptyDOMElement(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/BTCCard/BTCCard.tsx: -------------------------------------------------------------------------------- 1 | import './BTCCard.scss'; 2 | import { useState } from 'react'; 3 | import { motion, AnimatePresence } from 'framer-motion'; 4 | import { Card } from 'react-bootstrap'; 5 | 6 | import BTCWallet from '../BTCWallet/BTCWallet'; 7 | import BTCDeposit from '../BTCDeposit/BTCDeposit'; 8 | import BTCWithdraw from '../BTCWithdraw/BTCWithdraw'; 9 | import { TRANSITION_DURATION } from '../../../utilities/constants'; 10 | 11 | const BTCCard = () => { 12 | const [selBTCCard, setSelBTCCard] = useState('wallet'); 13 | 14 | return ( 15 | 16 | 17 | 25 | {selBTCCard === 'wallet' ? ( 26 | setSelBTCCard(action)} /> 27 | ) : selBTCCard === 'deposit' ? ( 28 | setSelBTCCard('wallet')} /> 29 | ) : ( 30 | setSelBTCCard('wallet')} /> 31 | )} 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default BTCCard; 39 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/BTCDeposit/BTCDeposit.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/src/components/cln/BTCDeposit/BTCDeposit.scss -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/BTCDeposit/BTCDeposit.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, screen, waitFor } from '@testing-library/react'; 2 | import { mockAppStore, mockNewAddr } from '../../../utilities/test-utilities/mockData'; 3 | import { spyOnBTCDeposit } from '../../../utilities/test-utilities/mockService'; 4 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 5 | import BTCDeposit from './BTCDeposit'; 6 | 7 | describe('BTCDeposit component ', () => { 8 | it('should show deposit card when clicking deposit action from BTC card', async () => { 9 | spyOnBTCDeposit(); 10 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/cln'] }); 11 | 12 | // Initial state 13 | expect(screen.getByTestId('btc-wallet-balance-card')).toBeInTheDocument(); 14 | 15 | // Click the deposit button 16 | const depositButton = screen.getByTestId('deposit-button'); 17 | fireEvent.click(depositButton); 18 | await waitFor(() => { 19 | expect(screen.getByTestId('btc-deposit')).toBeInTheDocument(); 20 | expect(screen.getByTestId('qr-code-component')).toBeInTheDocument(); 21 | expect(screen.getByTestId('qrcode-copy').getAttribute('placeholder')).toBe(mockNewAddr.bech32); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/BTCTransaction/BTCTransaction.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .btc-transaction-placeholder { 4 | transform-origin: top left; 5 | & .btc-transaction-detail { 6 | align-items: center; 7 | margin: 0.5rem; 8 | } 9 | & .btc-transaction-copy { 10 | display: inline-flex; 11 | align-items: flex-start; 12 | padding: 0; 13 | cursor: pointer; 14 | &:hover { 15 | svg path { 16 | stroke: darken($primary, 10%); 17 | } 18 | } 19 | } 20 | & .btc-transaction-open { 21 | display: inline-flex; 22 | align-items: flex-start; 23 | padding: 0; 24 | cursor: pointer; 25 | &:hover { 26 | svg path { 27 | fill: darken($primary, 10%); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/BTCTransaction/BTCTransaction.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, screen, waitFor } from '@testing-library/react'; 2 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 3 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 4 | import BTCTransaction from './BTCTransaction'; 5 | 6 | describe('BTCTransaction component ', () => { 7 | it('should be in the document', async () => { 8 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/cln'] }); 9 | 10 | // Initial state 11 | expect(screen.getByTestId('btc-transactions-list')).toBeInTheDocument(); 12 | 13 | // Click to expand 14 | const expandDiv = screen.getByTestId('btc-transaction-header'); 15 | fireEvent.click(expandDiv); 16 | await waitFor(() => { 17 | expect(screen.getByTestId('withdraw-header')).toBeInTheDocument(); 18 | expect(screen.getByTestId('withdraw-amount')).toBeInTheDocument(); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/BTCTransactionsList/BTCTransactionsList.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .btc-transactions-list { 4 | cursor: pointer; 5 | display: flex; 6 | flex-direction: column; 7 | height: 100%; 8 | padding-right: 0.5rem; 9 | transition: all $transition-time ease; 10 | & .btc-transaction-header { 11 | display: flex; 12 | flex-direction: column; 13 | border: none; 14 | margin-top: 0.5rem; 15 | border-radius: 0.675rem; 16 | padding: 0.5rem 1rem 0.5rem 0.125rem; 17 | transition: all $transition-time ease; 18 | &.expanded { 19 | border-bottom: 2px dashed rgba($light, 0.2); 20 | border-bottom-left-radius: 0; 21 | border-bottom-right-radius: 0; 22 | } 23 | &:hover { 24 | background-color: $body-bg-light !important; 25 | } 26 | } 27 | & .btc-transaction-details { 28 | transition: background-color $theme-transition ease; 29 | background-color: $body-bg-light; 30 | border-bottom-left-radius: 0.675rem; 31 | border-bottom-right-radius: 0.675rem; 32 | } 33 | } 34 | 35 | @include color-mode(dark) { 36 | .btc-transactions-list { 37 | & .btc-transaction-header { 38 | &:hover { 39 | background-color: $body-bg-dark !important; 40 | } 41 | } 42 | & .btc-transaction-details { 43 | background-color: $body-bg-dark; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/BTCTransactionsList/BTCTransactionsList.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react'; 2 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 3 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 4 | import BTCTransactionsList from './BTCTransactionsList'; 5 | 6 | describe('BTCTransactionsList component ', () => { 7 | it('should be in the document', async () => { 8 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/cln'] }); 9 | expect(screen.getByTestId('btc-transactions-list')).toBeInTheDocument(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/BTCWallet/BTCWallet.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .btc-transactions-tabs { 4 | border-bottom: 0.5px solid $border-color; 5 | padding: 0 2rem 0.25rem 0.25rem; 6 | } 7 | 8 | @include color-mode(dark) { 9 | .btc-transactions-tabs { 10 | border-bottom: 0.5px solid $border-color-dark; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/BTCWithdraw/BTCWithdraw.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/src/components/cln/BTCWithdraw/BTCWithdraw.scss -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/BTCWithdraw/BTCWithdraw.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, screen, waitFor } from '@testing-library/react'; 2 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 3 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 4 | import BTCWithdraw from './BTCWithdraw'; 5 | 6 | describe('BTCWithdraw component ', () => { 7 | it('should show withdraw card when clicking withdraw action from BTC card', async () => { 8 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/cln'] }); 9 | 10 | // Initial state 11 | expect(screen.getByTestId('btc-wallet-balance-card')).toBeInTheDocument(); 12 | 13 | // Click the withdraw button 14 | const withdrawButton = screen.getByTestId('withdraw-button'); 15 | fireEvent.click(withdrawButton); 16 | await waitFor(() => { 17 | expect(screen.getByTestId('btc-withdraw')).toBeInTheDocument(); 18 | expect(screen.getByTestId('button-withdraw')).toBeInTheDocument(); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/CLNCard/CLNCard.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/src/components/cln/CLNCard/CLNCard.scss -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/CLNCard/CLNCard.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react'; 2 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 3 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 4 | import CLNCard from './CLNCard'; 5 | 6 | describe('CLNCard component ', () => { 7 | it('should be in the document', async () => { 8 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/cln'] }); 9 | expect(screen.getByTestId('cln-card')).not.toBeEmptyDOMElement(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/CLNCard/CLNCard.tsx: -------------------------------------------------------------------------------- 1 | import './CLNCard.scss'; 2 | import { useState } from 'react'; 3 | import { motion, AnimatePresence } from 'framer-motion'; 4 | import { Card } from 'react-bootstrap'; 5 | 6 | import CLNWallet from '../CLNWallet/CLNWallet'; 7 | import CLNReceive from '../CLNReceive/CLNReceive'; 8 | import CLNSend from '../CLNSend/CLNSend'; 9 | import { TRANSITION_DURATION } from '../../../utilities/constants'; 10 | 11 | const CLNCard = () => { 12 | const [selCLNCard, setSelCLNCard] = useState('wallet'); 13 | 14 | return ( 15 | 16 | 17 | 25 | {selCLNCard === 'wallet' ? ( 26 | setSelCLNCard(action)} /> 27 | ) : selCLNCard === 'receive' ? ( 28 | setSelCLNCard('wallet')} /> 29 | ) : ( 30 | setSelCLNCard('wallet')} /> 31 | )} 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default CLNCard; 39 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/CLNHome/CLNHome.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/src/components/cln/CLNHome/CLNHome.scss -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/CLNHome/CLNHome.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react'; 2 | import { mockAppStore, mockBKPRStoreData, mockCLNStoreData, mockNodeInfo, mockRootStoreData } from '../../../utilities/test-utilities/mockData'; 3 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 4 | import CLNHome from './CLNHome'; 5 | 6 | describe('CLNHome Component', () => { 7 | it('should render error message when nodeInfo has an error', async () => { 8 | const errorMessage = 'Error loading node info'; 9 | const customMockStore = { 10 | root: { 11 | ...mockRootStoreData, 12 | nodeInfo: { 13 | ...mockNodeInfo, 14 | error: errorMessage 15 | } 16 | }, 17 | cln: mockCLNStoreData, 18 | bkpr: mockBKPRStoreData 19 | }; 20 | await renderWithProviders(, { preloadedState: customMockStore, initialRoute: ['/cln'] }); 21 | expect(screen.getByText(errorMessage)).toBeInTheDocument(); 22 | }); 23 | 24 | it('should render the full page', async () => { 25 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/cln'] }); 26 | expect(screen.getByTestId('header')).toBeInTheDocument(); 27 | expect(screen.getByTestId('cln-container')).toBeInTheDocument(); 28 | 29 | expect(screen.getByTestId('overview-total-balance-col')).toBeInTheDocument(); 30 | expect(screen.getByTestId('btc-card')).toBeInTheDocument(); 31 | expect(screen.getByTestId('cln-card')).toBeInTheDocument(); 32 | expect(screen.getByTestId('channels-card')).toBeInTheDocument(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/CLNHome/CLNHome.tsx: -------------------------------------------------------------------------------- 1 | import './CLNHome.scss'; 2 | import { Row, Col } from 'react-bootstrap'; 3 | import Overview from '../Overview/Overview'; 4 | import BTCCard from '../BTCCard/BTCCard'; 5 | import CLNCard from '../CLNCard/CLNCard'; 6 | import ChannelsCard from '../ChannelsCard/ChannelsCard'; 7 | import Header from '../../ui/Header/Header'; 8 | import { useSelector } from 'react-redux'; 9 | import { useInjectReducer } from '../../../hooks/use-injectreducer'; 10 | import clnReducer from '../../../store/clnSlice'; 11 | import { selectNodeInfo } from '../../../store/rootSelectors'; 12 | 13 | function CLNHome() { 14 | useInjectReducer('cln', clnReducer); 15 | const nodeInfo = useSelector(selectNodeInfo); 16 | 17 | if (nodeInfo.error) { 18 | return ( 19 | 20 | 21 | {nodeInfo.error} 22 | 23 | 24 | ); 25 | } 26 | 27 | return ( 28 | <> 29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 | ); 50 | } 51 | 52 | export default CLNHome; 53 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/CLNOffer/CLNOffer.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .cln-offer-placeholder { 4 | transform-origin: top left; 5 | & .cln-offer-detail { 6 | align-items: center; 7 | margin: 0.5rem; 8 | } 9 | & .cln-offer-copy { 10 | display: inline-flex; 11 | align-items: flex-start; 12 | padding: 0; 13 | cursor: pointer; 14 | &:hover { 15 | svg path { 16 | stroke: darken($primary, 10%); 17 | } 18 | } 19 | } 20 | & .cln-offer-open { 21 | display: inline-flex; 22 | align-items: flex-start; 23 | padding: 0; 24 | cursor: pointer; 25 | &:hover { 26 | svg path { 27 | fill: darken($primary, 10%); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/CLNOffer/CLNOffer.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, screen, waitFor } from '@testing-library/react'; 2 | import { act } from 'react'; 3 | import { mockAppStore, mockOffer1 } from '../../../utilities/test-utilities/mockData'; 4 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 5 | import CLNOffer from './CLNOffer'; 6 | 7 | describe('CLNOffer component ', () => { 8 | it('should be in the document', async () => { 9 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/cln'] }); 10 | 11 | // Initial state 12 | expect(screen.getByTestId('cln-offers-list')).toBeInTheDocument(); 13 | 14 | // Click to expand 15 | const expandDiv = await screen.getByTestId('cln-offer-header'); 16 | fireEvent.click(expandDiv); 17 | 18 | await act(async () => { 19 | let safety = 0; 20 | while (jest.getTimerCount() > 0 && safety++ < 100) { 21 | jest.runOnlyPendingTimers(); 22 | await Promise.resolve(); 23 | } 24 | }); 25 | 26 | await waitFor(() => { 27 | expect(screen.getByTestId('cln-offer-detail')).toBeInTheDocument(); 28 | expect(screen.getByText(mockOffer1.bolt12)).toBeInTheDocument(); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/CLNOffer/CLNOffer.tsx: -------------------------------------------------------------------------------- 1 | import './CLNOffer.scss'; 2 | import { motion } from 'framer-motion'; 3 | import { Row, Col } from 'react-bootstrap'; 4 | 5 | import { CopySVG } from '../../../svgs/Copy'; 6 | import { TRANSITION_DURATION } from '../../../utilities/constants'; 7 | import { copyTextToClipboard } from '../../../utilities/data-formatters'; 8 | import logger from '../../../services/logger.service'; 9 | import { setShowToast } from '../../../store/rootSlice'; 10 | import { useDispatch } from 'react-redux'; 11 | 12 | const OfferDetail = ({ offer, copyHandler }) => { 13 | return ( 14 | <> 15 | {offer.bolt12 ? ( 16 | 17 | 18 | Bolt 12 19 | 20 | 21 | {offer.bolt12} 22 | 23 | 24 | 25 | 26 | 27 | ) : ( 28 | <> 29 | )} 30 | 31 | ); 32 | }; 33 | 34 | const CLNOffer = (props) => { 35 | const dispatch = useDispatch(); 36 | 37 | const copyHandler = (event) => { 38 | let textToCopy = ''; 39 | switch (event.target.id) { 40 | case 'Bolt12': 41 | textToCopy = props.offer.bolt12; 42 | break; 43 | default: 44 | textToCopy = props.offer.bolt12; 45 | break; 46 | } 47 | copyTextToClipboard(textToCopy).then((response) => { 48 | dispatch(setShowToast({show: true, message: (event.target.id + ' Copied Successfully!'), bg: 'success'})); 49 | }).catch((err) => { 50 | logger.error(err); 51 | }); 52 | } 53 | 54 | return ( 55 | 61 | 62 | 63 | ); 64 | }; 65 | 66 | export default CLNOffer; 67 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/CLNOffersList/CLNOffersList.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .cln-offers-list { 4 | cursor: pointer; 5 | display: flex; 6 | flex-direction: column; 7 | height: 100%; 8 | padding-right: 0.5rem; 9 | transition: all $transition-time ease; 10 | & .cln-offer-header { 11 | display: flex; 12 | flex-direction: column; 13 | border: none; 14 | margin-top: 0.5rem; 15 | border-radius: 0.675rem; 16 | padding: 0.5rem 1rem 0.5rem 0.125rem; 17 | transition: all $transition-time ease; 18 | &.expanded { 19 | border-bottom: 2px dashed rgba($light, 0.2); 20 | border-bottom-left-radius: 0; 21 | border-bottom-right-radius: 0; 22 | } 23 | &:hover { 24 | background-color: $body-bg-light !important; 25 | } 26 | } 27 | & .cln-offer-details { 28 | transition: background-color $theme-transition ease; 29 | background-color: $body-bg-light; 30 | border-bottom-left-radius: 0.675rem; 31 | border-bottom-right-radius: 0.675rem; 32 | } 33 | } 34 | 35 | @include color-mode(dark) { 36 | .cln-offers-list { 37 | & .cln-offer-header { 38 | &:hover { 39 | background-color: $body-bg-dark !important; 40 | } 41 | } 42 | & .cln-offer-details { 43 | background-color: $body-bg-dark; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/CLNReceive/CLNReceive.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/src/components/cln/CLNReceive/CLNReceive.scss -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/CLNReceive/CLNReceive.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, screen, waitFor } from '@testing-library/react'; 2 | import { act } from 'react'; 3 | import { APP_ANIMATION_DURATION } from '../../../utilities/constants'; 4 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 5 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 6 | import CLNReceive from './CLNReceive'; 7 | 8 | describe('CLNReceive component ', () => { 9 | it('should show receive card when clicking receive action from wallet', async () => { 10 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/cln'] }); 11 | await act(async () => jest.advanceTimersByTime(APP_ANIMATION_DURATION * 1000)); 12 | 13 | // Initial state 14 | expect(screen.getByTestId('cln-wallet-balance-card')).toBeInTheDocument(); 15 | 16 | // Click the receive button 17 | const receiveButton = screen.getByTestId('receive-button'); 18 | fireEvent.click(receiveButton); 19 | await waitFor(() => { 20 | expect(screen.getByTestId('cln-receive')).toBeInTheDocument(); 21 | expect(screen.getByTestId('button-generate')).toBeInTheDocument(); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/CLNSend/CLNSend.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/src/components/cln/CLNSend/CLNSend.scss -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/CLNTransaction/CLNTransaction.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .cln-transaction-placeholder { 4 | transform-origin: top left; 5 | & .cln-transaction-detail { 6 | align-items: center; 7 | margin: 0.5rem; 8 | } 9 | & .cln-transaction-copy { 10 | display: inline-flex; 11 | align-items: flex-start; 12 | padding: 0; 13 | cursor: pointer; 14 | &:hover { 15 | svg path { 16 | stroke: darken($primary, 10%); 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/CLNTransaction/CLNTransaction.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, screen, waitFor } from '@testing-library/react'; 2 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 3 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 4 | import CLNTransaction from './CLNTransaction'; 5 | 6 | describe('CLNTransaction component ', () => { 7 | it('should be in the document', async () => { 8 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/cln'] }); 9 | 10 | // Initial state 11 | expect(screen.getByTestId('cln-transactions-list')).toBeInTheDocument(); 12 | 13 | // Click to expand 14 | const expandDiv = screen.getByTestId('cln-transaction-header'); 15 | fireEvent.click(expandDiv); 16 | await waitFor(() => { 17 | expect(screen.getByTestId('invoice-header')).toBeInTheDocument(); 18 | expect(screen.queryByTestId('preimage')).not.toBeInTheDocument(); 19 | expect(screen.queryByTestId('valid-till')).not.toBeInTheDocument(); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/CLNTransactionsList/CLNTransactionsList.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .cln-transactions-list { 4 | cursor: pointer; 5 | display: flex; 6 | flex-direction: column; 7 | height: 100%; 8 | padding-right: 0.5rem; 9 | transition: all $transition-time ease; 10 | & .cln-transaction-header { 11 | display: flex; 12 | flex-direction: column; 13 | border: none; 14 | margin-top: 0.5rem; 15 | border-radius: 0.675rem; 16 | padding: 0.5rem 1rem 0.5rem 0.125rem; 17 | transition: all $transition-time ease; 18 | &.expanded { 19 | border-bottom: 2px dashed rgba($light, 0.2); 20 | border-bottom-left-radius: 0; 21 | border-bottom-right-radius: 0; 22 | } 23 | &:hover { 24 | background-color: $body-bg-light !important; 25 | } 26 | } 27 | & .cln-transaction-details { 28 | transition: background-color $theme-transition ease; 29 | background-color: $body-bg-light; 30 | border-bottom-left-radius: 0.675rem; 31 | border-bottom-right-radius: 0.675rem; 32 | } 33 | } 34 | 35 | @include color-mode(dark) { 36 | .cln-transactions-list { 37 | & .cln-transaction-header { 38 | &:hover { 39 | background-color: $body-bg-dark !important; 40 | } 41 | } 42 | & .cln-transaction-details { 43 | background-color: $body-bg-dark; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/CLNWallet/CLNWallet.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .nav.cln-transactions-tabs { 4 | border: none; 5 | border-bottom: 0.5px solid $border-color; 6 | border-left: none; 7 | border-right: none; 8 | border-top: none; 9 | & .nav-item .nav-link { 10 | padding: 0 1rem 0.25rem 0.25rem; 11 | color: $light; 12 | & span { 13 | padding: 0.25rem 0.25rem 0.25rem 0.25rem; 14 | } 15 | &.active, 16 | &:hover, 17 | &:focus { 18 | background-color: transparent; 19 | & span { 20 | border-bottom: 2px solid $primary; 21 | } 22 | } 23 | } 24 | } 25 | 26 | @include color-mode(dark) { 27 | .nav.cln-transactions-tabs { 28 | border-bottom: 0.5px solid $border-color-dark; 29 | & .nav-item .nav-link { 30 | color: $light-dark; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/ChannelDetails/ChannelDetails.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .channel-scroll-container { 4 | max-height: 53vh; 5 | & .progress { 6 | height: 6px; 7 | } 8 | & .btn-sm-svg.btn-svg-copy { 9 | display: inline-flex; 10 | align-items: flex-start; 11 | padding: 0; 12 | cursor: pointer; 13 | &:hover { 14 | svg path { 15 | stroke: darken($primary, 10%); 16 | } 17 | } 18 | } 19 | & .btn-sm-svg.btn-svg-open { 20 | display: inline-flex; 21 | align-items: flex-start; 22 | padding: 0; 23 | cursor: pointer; 24 | &:hover { 25 | svg path { 26 | fill: darken($primary, 10%); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/ChannelDetails/ChannelDetails.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, screen, waitFor } from '@testing-library/react'; 2 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 3 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 4 | import ChannelDetails from './ChannelDetails'; 5 | 6 | describe('ChannelDetails component', () => { 7 | it('should be in the document', async () => { 8 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/cln'] }); 9 | 10 | // Channels list rendered 11 | expect(screen.getByTestId('channels')).toBeInTheDocument(); 12 | expect(screen.queryByTestId('channel-details')).not.toBeInTheDocument(); 13 | 14 | // Click an first channel 15 | const channelItems = screen.getAllByTestId('list-item-channel'); 16 | fireEvent.click(channelItems[0]); 17 | 18 | await waitFor(() => { 19 | expect(screen.getByTestId('channel-details')).toBeInTheDocument(); 20 | expect(screen.queryByTestId('channels')).not.toBeInTheDocument(); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/ChannelOpen/ChannelOpen.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/src/components/cln/ChannelOpen/ChannelOpen.scss -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/ChannelOpen/ChannelOpen.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, screen, waitFor } from '@testing-library/react'; 2 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 3 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 4 | import ChannelOpen from './ChannelOpen'; 5 | 6 | describe('ChannelOpen component ', () => { 7 | it('should be in the document', async () => { 8 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/cln'] }); 9 | 10 | // Channels list rendered 11 | expect(screen.getByTestId('channels')).toBeInTheDocument(); 12 | expect(screen.queryByTestId('channel-open-card')).not.toBeInTheDocument(); 13 | 14 | // Click open channel 15 | const openChannelBtn = screen.getByTestId('button-open-channel'); 16 | await fireEvent.click(openChannelBtn); 17 | await waitFor(() => { 18 | expect(screen.getByTestId('channel-open-card')).toBeInTheDocument(); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/Channels/Channels.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .channels-scroll-container { 4 | overflow: hidden; 5 | & .list-channels { 6 | transition: background-color $theme-transition ease; 7 | & .list-item-channel { 8 | border-radius: 0.675rem; 9 | margin-bottom: 0.5rem; 10 | padding: 0.5rem; 11 | display: flex; 12 | justify-content: space-between; 13 | align-items: flex-start; 14 | transition: background-color $theme-transition ease; 15 | &.newly-opened { 16 | background-color: $body-bg-light; 17 | } 18 | &:hover { 19 | cursor: pointer; 20 | background-color: $body-bg-light; 21 | } 22 | & .progress { 23 | height: 6px; 24 | } 25 | } 26 | } 27 | } 28 | 29 | @include color-mode(dark) { 30 | .channels-scroll-container { 31 | & .list-channels { 32 | transition: background-color $theme-transition ease; 33 | & .list-item-channel { 34 | transition: background-color $theme-transition ease; 35 | &.newly-opened { 36 | background-color: $body-bg-dark; 37 | } 38 | &:hover { 39 | background-color: $body-bg-dark; 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/ChannelsCard/ChannelsCard.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/src/components/cln/ChannelsCard/ChannelsCard.scss -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/ChannelsCard/ChannelsCard.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react'; 2 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 3 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 4 | import ChannelsCard from './ChannelsCard'; 5 | 6 | describe('ChannelsCard component ', () => { 7 | it('should be in the document', async () => { 8 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/cln'] }); 9 | expect(screen.getByTestId('channels-card')).not.toBeEmptyDOMElement(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/ChannelsCard/ChannelsCard.tsx: -------------------------------------------------------------------------------- 1 | import './ChannelsCard.scss'; 2 | import { useState } from 'react'; 3 | import { motion, AnimatePresence } from 'framer-motion'; 4 | import { Card } from 'react-bootstrap'; 5 | 6 | import Channels from '../Channels/Channels'; 7 | import ChannelOpen from '../ChannelOpen/ChannelOpen'; 8 | import ChannelDetails from '../ChannelDetails/ChannelDetails'; 9 | import { CLEAR_STATUS_ALERT_DELAY, TRANSITION_DURATION } from '../../../utilities/constants'; 10 | import { PeerChannel } from '../../../types/root.type'; 11 | 12 | const ChannelsCard = () => { 13 | const [selChannelCard, setSelChannelCard] = useState('channels'); 14 | const [selChannel, setSelChannel] = useState(null); 15 | const [newlyOpenedChannelId, setNewlyOpenedChannelId] = useState(''); 16 | 17 | const onCloseHandler = channelId => { 18 | setNewlyOpenedChannelId(channelId); 19 | setSelChannelCard('channels'); 20 | setTimeout(() => { 21 | setNewlyOpenedChannelId(''); 22 | }, CLEAR_STATUS_ALERT_DELAY); 23 | }; 24 | 25 | return ( 26 | 27 | 28 | 36 | {selChannelCard === 'open' ? ( 37 | 38 | ) : selChannelCard === 'details' ? ( 39 | setSelChannelCard('channels')} selChannel={selChannel} /> 40 | ) : ( 41 | setSelChannelCard('open')} 44 | onChannelClick={channel => { 45 | setSelChannel(channel); 46 | setSelChannelCard('details'); 47 | }} 48 | /> 49 | )} 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | export default ChannelsCard; 57 | -------------------------------------------------------------------------------- /apps/frontend/src/components/cln/Overview/Overview.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/src/components/cln/Overview/Overview.scss -------------------------------------------------------------------------------- /apps/frontend/src/components/modals/ConnectWallet/ConnectWallet.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .modal-content { 4 | & .modal-header, 5 | & .modal-footer { 6 | border: none; 7 | } 8 | } 9 | 10 | .dropdown.dropdown-network { 11 | & .dropdown-toggle.btn.btn-secondary { 12 | box-shadow: 0px 1px 2px rgba($dark-blue, 0.05); 13 | background-image: none; 14 | padding: 0.675rem; 15 | border-radius: 0.375rem !important; 16 | } 17 | & .dropdown-menu { 18 | font-size: 0.875rem; 19 | width: 100%; 20 | } 21 | } 22 | 23 | @include color-mode(light) { 24 | .dropdown.dropdown-network { 25 | & .dropdown-toggle.btn.btn-secondary { 26 | color: $dark; 27 | background-color: $body-bg; 28 | border-color: $border-color; 29 | } 30 | & .dropdown-menu { 31 | & .dropdown-item { 32 | color: $dark; 33 | &.active, 34 | &:active, 35 | &:focus, 36 | &:hover { 37 | color: $primary; 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | @include color-mode(dark) { 45 | .dropdown.dropdown-network { 46 | & .dropdown-toggle.btn.btn-secondary { 47 | color: $white; 48 | background-color: $form-ctrl-bg-dark; 49 | border-color: $border-color-dark; 50 | } 51 | & .dropdown-menu { 52 | & .dropdown-item { 53 | color: $light-dark; 54 | &.active, 55 | &:active, 56 | &:focus, 57 | &:hover { 58 | color: $primary; 59 | } 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /apps/frontend/src/components/modals/Login/Login.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/src/components/modals/Login/Login.scss -------------------------------------------------------------------------------- /apps/frontend/src/components/modals/Logout/Logout.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .modal-content { 4 | border: none; 5 | & .modal-body { 6 | & .modal-box { 7 | box-shadow: 0px 8px 16px 0px rgba($dark, 0.2); 8 | border-radius: 0.5rem; 9 | border: 1px solid $primary; 10 | } 11 | 12 | & .message-type-box { 13 | background-color: $primary; 14 | border-top-left-radius: 0.35rem; 15 | border-bottom-left-radius: 0.35rem; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/frontend/src/components/modals/Logout/Logout.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, fireEvent, waitFor } from '@testing-library/react'; 2 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 3 | import LogoutComponent from './Logout'; 4 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 5 | import { RootService } from '../../../services/http.service'; 6 | import { spyOnUserLogout } from '../../../utilities/test-utilities/mockService'; 7 | 8 | describe('LogoutComponent', () => { 9 | beforeEach(() => { 10 | mockAppStore.root.showModals.logoutModal = true; 11 | jest.clearAllMocks(); 12 | }); 13 | 14 | it('renders the logout modal', async () => { 15 | await renderWithProviders(, { preloadedState: mockAppStore }); 16 | expect(screen.getByTestId('logout-modal')).toBeInTheDocument(); 17 | expect(screen.getByText(/Logout\?/i)).toBeInTheDocument(); 18 | }); 19 | 20 | it('calls userLogout and clears stores on Yes click', async () => { 21 | spyOnUserLogout(); 22 | await renderWithProviders(, { preloadedState: mockAppStore }); 23 | fireEvent.click(screen.getByText('Yes')); 24 | await waitFor(() => { 25 | expect(RootService.userLogout).toHaveBeenCalled(); 26 | }); 27 | }); 28 | 29 | it('does not call userLogout on No click', async () => { 30 | spyOnUserLogout(); 31 | await renderWithProviders(, { preloadedState: mockAppStore }); 32 | fireEvent.click(screen.getByText('No')); 33 | await waitFor(() => { 34 | expect(RootService.userLogout).not.toHaveBeenCalled(); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /apps/frontend/src/components/modals/NodeInfo/NodeInfo.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .fa-circle-xmark { 4 | & path { 5 | fill: $light; 6 | } 7 | } 8 | 9 | .modal-content { 10 | & .modal-header, 11 | & .modal-footer { 12 | border: none; 13 | } 14 | } 15 | 16 | @include color-mode(dark) { 17 | .fa-circle-xmark { 18 | & path { 19 | fill: $white; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/frontend/src/components/modals/NodeInfo/NodeInfo.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react'; 2 | import NodeInfo from './NodeInfo'; 3 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 4 | import { defaultRootState } from '../../../store/rootSelectors'; 5 | import { mockShowModals } from '../../../utilities/test-utilities/mockData'; 6 | import { defaultCLNState } from '../../../store/clnSelectors'; 7 | import { defaultBKPRState } from '../../../store/bkprSelectors'; 8 | 9 | describe('NodeInfo component ', () => { 10 | let customMockStore; 11 | beforeEach(() => { 12 | customMockStore = { 13 | root: { 14 | ...defaultRootState, 15 | showModals: { 16 | ...mockShowModals, 17 | nodeInfoModal: true, 18 | }, 19 | }, 20 | cln: defaultCLNState, 21 | bkpr: defaultBKPRState 22 | }; 23 | }); 24 | 25 | it('should be in the document', async () => { 26 | await renderWithProviders(, { preloadedState: customMockStore }); 27 | expect(screen.getByTestId('node-info-modal')).toBeInTheDocument(); 28 | }); 29 | 30 | it('if AppContext config says hide, hide this modal', async () => { 31 | customMockStore.root.showModals.nodeInfoModal = false; 32 | await renderWithProviders(, { preloadedState: customMockStore }); 33 | expect(screen.queryByTestId('node-info-modal')).not.toBeInTheDocument(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /apps/frontend/src/components/modals/SQLTerminal/SQLTerminal.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .terminal-container { 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | 8 | .terminal-input { 9 | resize: none; 10 | white-space: pre-wrap; 11 | height: 8rem; 12 | overflow: hidden; 13 | } 14 | 15 | .terminal-output { 16 | height: 47vh; 17 | overflow: hidden; 18 | width: 100%; 19 | resize: none; 20 | font-size: $font-size-base; 21 | white-space: pre-wrap; 22 | padding: 1rem 0 1rem 2rem; 23 | margin-bottom: 1.5rem; 24 | background-color: $body-bg-light; 25 | border-radius: $border-radius; 26 | border: $input-border-width solid $input-border-color; 27 | box-shadow: 0px 1px 2px rgba($dark-blue, 0.05); 28 | } 29 | 30 | .btn-copy-output { 31 | padding: 1rem; 32 | border: none; 33 | cursor: pointer; 34 | position: absolute; 35 | right: 0; 36 | background: transparent; 37 | z-index: 1; 38 | border-radius: $border-radius; 39 | &:hover, 40 | &:focus-visible { 41 | outline: none; 42 | svg path { 43 | stroke: darken($primary, 10%); 44 | &.svg-add { 45 | stroke: none; 46 | fill: darken($primary, 10%); 47 | } 48 | } 49 | } 50 | } 51 | 52 | @include color-mode(dark) { 53 | .terminal-output { 54 | background-color: $body-bg-dark; 55 | border: $input-border-width solid $input-border-color; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /apps/frontend/src/components/modals/SetPassword/SetPassword.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/src/components/modals/SetPassword/SetPassword.scss -------------------------------------------------------------------------------- /apps/frontend/src/components/modals/SetPassword/SetPassword.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react'; 2 | import SetPasswordComponent from './SetPassword'; 3 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 4 | import { defaultRootState } from '../../../store/rootSelectors'; 5 | import { mockShowModals } from '../../../utilities/test-utilities/mockData'; 6 | import { defaultCLNState } from '../../../store/clnSelectors'; 7 | import { defaultBKPRState } from '../../../store/bkprSelectors'; 8 | 9 | describe('Password component ', () => { 10 | let customMockStore; 11 | beforeEach(() => { 12 | customMockStore = { 13 | root: { 14 | ...defaultRootState, 15 | showModals: { 16 | ...mockShowModals, 17 | setPasswordModal: true, 18 | }, 19 | }, 20 | cln: defaultCLNState, 21 | bkpr: defaultBKPRState 22 | }; 23 | }); 24 | 25 | it('should be in the document', async () => { 26 | await renderWithProviders(, { preloadedState: customMockStore }); 27 | expect(screen.getByTestId('set-password-modal')).toBeInTheDocument(); 28 | }); 29 | 30 | it('if AppContext config says hide, hide this modal', async () => { 31 | customMockStore.root.showModals.setPasswordModal = false; 32 | await renderWithProviders(, { preloadedState: customMockStore }); 33 | expect(screen.queryByTestId('set-password-modal')).not.toBeInTheDocument(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/CurrencyBox/CurrencyBox.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/src/components/shared/CurrencyBox/CurrencyBox.scss -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/DataFilterOptions/DataFilterOptions.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .time-granularity-group { 4 | & .dropdown-toggle { 5 | & .form-control.form-control-left { 6 | width: 5rem; 7 | } 8 | &::after { 9 | display: none; 10 | } 11 | } 12 | & .dropdown-menu { 13 | width: 7rem; 14 | font-size: $font-size-base; 15 | & .dropdown-item:hover { 16 | color: $primary; 17 | background-color: transparent; 18 | background-image: none; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/DateBox/DateBox.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/src/components/shared/DateBox/DateBox.scss -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/DateBox/DateBox.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render } from '@testing-library/react'; 2 | import DateBox from './DateBox'; 3 | import { mockAppStore, mockInvoice } from '../../../utilities/test-utilities/mockData'; 4 | import { convertIntoDateFormat } from '../../../utilities/data-formatters'; 5 | import { createMockStore } from '../../../utilities/test-utilities/mockStore'; 6 | import { Provider } from 'react-redux'; 7 | import { APP_ANIMATION_DURATION } from '../../../utilities/constants'; 8 | import { act } from 'react'; 9 | 10 | describe('DateBox component ', () => { 11 | it('format date', async () => { 12 | const store = createMockStore('/', mockAppStore); 13 | render( 14 | 15 | 16 | 17 | ); 18 | await act(async () => jest.advanceTimersByTime(APP_ANIMATION_DURATION * 1000)); 19 | const overlayTrigger = screen.getByTestId('overlay-trigger'); 20 | expect(overlayTrigger).toHaveTextContent(convertIntoDateFormat(mockInvoice.expires_at)); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/DateBox/DateBox.tsx: -------------------------------------------------------------------------------- 1 | import './DateBox.scss'; 2 | import { OverlayTrigger, Tooltip } from 'react-bootstrap'; 3 | import { convertIntoDateFormat } from '../../../utilities/data-formatters'; 4 | 5 | const DateBox = props => { 6 | return ( 7 | {props.dataType} : <> 12 | } 13 | > 14 |
15 | {convertIntoDateFormat(props.dataValue)} 16 |
17 |
18 | ); 19 | }; 20 | 21 | export default DateBox; 22 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/DatepickerInput/DatepickerInput.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .react-datepicker-wrapper { 4 | width: 6.5rem; 5 | } 6 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/DatepickerInput/DatepickerInput.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import './DatepickerInput.scss'; 3 | import DatePicker from 'react-datepicker'; 4 | import { InputGroup } from 'react-bootstrap'; 5 | import { ChevronDown } from '../../../svgs/ChevronDown'; 6 | 7 | const CLNDatePicker = (props) => { 8 | const datePickerRef = useRef(null); 9 | 10 | const openDetapicker = () => { 11 | if (datePickerRef.current) { 12 | datePickerRef.current.setOpen(true); 13 | } 14 | }; 15 | 16 | return ( 17 | 18 | 28 | 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default CLNDatePicker; 39 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/FeerateRange/FeerateRange.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .slider-container { 4 | width: 100%; 5 | & .slider-pic { 6 | -webkit-appearance: none; 7 | appearance: none; 8 | width: 100%; 9 | height: 0.375rem; 10 | border-radius: 0.25rem; 11 | background: linear-gradient(to right, $success, $warning, $danger); 12 | outline: none; 13 | opacity: 0.8; 14 | transition: opacity $theme-transition ease; 15 | cursor: pointer; 16 | &:hover { 17 | opacity: 1; 18 | } 19 | &::-moz-range-track { 20 | background: linear-gradient(to right, $success, $warning, $danger); 21 | } 22 | &::-webkit-slider-range-thumb { 23 | -webkit-appearance: none; 24 | appearance: none; 25 | background: transparent; 26 | width: 2.5rem; 27 | height: 2.5rem; 28 | cursor: pointer; 29 | } 30 | &::-webkit-slider-runnable-track { 31 | background-color: transparent; 32 | } 33 | } 34 | } 35 | 36 | .tooltip.bs-tooltip-top.feerate-tooltip { 37 | &.feerate-tooltip-Slow { 38 | left: -6.7vw !important; 39 | } 40 | &.feerate-tooltip-Urgent { 41 | left: 6.7vw !important; 42 | } 43 | & .tooltip-inner { 44 | margin-bottom: 0.5rem; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/FeerateRange/FeerateRange.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render } from '@testing-library/react'; 2 | import { act } from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import { APP_ANIMATION_DURATION } from '../../../utilities/constants'; 5 | import { createMockStore } from '../../../utilities/test-utilities/mockStore'; 6 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 7 | import FeerateRange from './FeerateRange'; 8 | 9 | describe('FeerateRange component ', () => { 10 | it('should be in the document', async () => { 11 | const store = createMockStore('/', mockAppStore); 12 | render( 13 | 14 | 15 | 16 | ); 17 | await act(async () => jest.advanceTimersByTime(APP_ANIMATION_DURATION * 1000)); 18 | expect(screen.getByTestId('fee-rate-container')).toBeInTheDocument(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/FiatBox/FiatBox.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .fiat-box-span { 4 | & .svg-currency { 5 | fill: $light; 6 | } 7 | & .svg-inline--fa { 8 | display: inline-block; 9 | height: 0.6rem; 10 | vertical-align: -1px; 11 | &.fa-lg { 12 | height: 0.75rem; 13 | } 14 | } 15 | } 16 | 17 | @include color-mode(dark) { 18 | .fiat-box-span { 19 | & .svg-currency { 20 | fill: $light-dark; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/FiatBox/FiatBox.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render } from '@testing-library/react'; 2 | import { act } from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import { APP_ANIMATION_DURATION } from '../../../utilities/constants'; 5 | import { createMockStore } from '../../../utilities/test-utilities/mockStore'; 6 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 7 | import FiatBox from './FiatBox'; 8 | 9 | 10 | describe('FiatBox component ', () => { 11 | it('should be in the document', async () => { 12 | const store = createMockStore('/', mockAppStore); 13 | render( 14 | 15 | 16 | 17 | ); 18 | await act(async () => jest.advanceTimersByTime(APP_ANIMATION_DURATION * 1000)); 19 | expect(screen.getByTestId('fiat-box')).toBeInTheDocument(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/FiatBox/FiatBox.tsx: -------------------------------------------------------------------------------- 1 | import './FiatBox.scss'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | 4 | import { formatFiatValue } from '../../../utilities/data-formatters'; 5 | import { FIAT_CURRENCIES, Units } from '../../../utilities/constants'; 6 | import { CurrencySVG } from '../../../svgs/Currency'; 7 | import { useSelector } from 'react-redux'; 8 | import { selectFiatUnit } from '../../../store/rootSelectors'; 9 | 10 | const FiatBox = props => { 11 | const fiatUnit = useSelector(selectFiatUnit); 12 | const fiatSymbol = FIAT_CURRENCIES.find((fiat => fiat.currency === fiatUnit))?.symbol; 13 | 14 | return ( 15 | 19 | {props.symbol || (fiatSymbol?.prefix.startsWith('fa') && fiatSymbol.iconName) ? ( 20 | 21 | ) : ( 22 | 23 | )} 24 | 25 | {formatFiatValue(+props.value || 0, +props.rate, props.fromUnit || Units.SATS)} 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default FiatBox; 32 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/FiatSelection/FiatSelection.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .fiat-dropdown.dropdown { 4 | & .svg-curr-symbol { 5 | margin-top: 3px; 6 | } 7 | & .dropdown-menu { 8 | & .dropdown-item { 9 | & .svg-currency { 10 | fill: $dark; 11 | } 12 | &:hover { 13 | & .svg-currency { 14 | fill: $primary; 15 | } 16 | color: $primary; 17 | } 18 | } 19 | & .fiat-dropdown-scroller { 20 | max-height: 200px; 21 | height: 200px; 22 | } 23 | } 24 | & button.dropdown-toggle { 25 | width: 5rem; 26 | border-radius: 0.5rem; 27 | &::after { 28 | color: $dark; 29 | display: inline-block; 30 | margin-left: 0.255em; 31 | content: ''; 32 | border-top: 0.3em solid; 33 | border-right: 0.3em solid transparent; 34 | border-bottom: 0; 35 | border-left: 0.3em solid transparent; 36 | } 37 | & .svg-currency { 38 | fill: $dark; 39 | } 40 | & .dropdown-toggle-text { 41 | width: 2.2rem; 42 | font-size: 12px; 43 | display: inline-flex; 44 | } 45 | &:hover, 46 | &.btn:first-child:active, 47 | &.btn.show { 48 | background-color: transparent; 49 | border-color: $gray-400; 50 | box-shadow: none; 51 | } 52 | } 53 | } 54 | 55 | @include color-mode(dark) { 56 | .fiat-dropdown.dropdown { 57 | & .dropdown-menu { 58 | & .dropdown-item { 59 | & .svg-currency { 60 | fill: $white; 61 | } 62 | &:hover { 63 | & .svg-currency { 64 | fill: $primary; 65 | } 66 | color: $primary; 67 | } 68 | } 69 | } 70 | & button.dropdown-toggle { 71 | &::after { 72 | color: $white; 73 | } 74 | & .svg-currency { 75 | fill: $white; 76 | } 77 | & .dropdown-toggle-text { 78 | color: $white; 79 | } 80 | &:hover, 81 | &.btn:first-child:active, 82 | &.btn.show { 83 | border-color: $gray-200; 84 | color: $primary; 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/FiatSelection/FiatSelection.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render } from '@testing-library/react'; 2 | import { act } from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import { APP_ANIMATION_DURATION } from '../../../utilities/constants'; 5 | import { createMockStore } from '../../../utilities/test-utilities/mockStore'; 6 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 7 | import FiatSelection from './FiatSelection'; 8 | 9 | describe('FiatSelection component ', () => { 10 | it('should be in the document', async () => { 11 | const store = createMockStore('/', mockAppStore); 12 | render( 13 | 14 | 15 | 16 | ); 17 | await act(async () => jest.advanceTimersByTime(APP_ANIMATION_DURATION * 1000)); 18 | expect(screen.getByTestId('fiat-selection')).toBeInTheDocument(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/InvalidInputMessage/InvalidInputMessage.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/src/components/shared/InvalidInputMessage/InvalidInputMessage.scss -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/InvalidInputMessage/InvalidInputMessage.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render } from '@testing-library/react'; 2 | import { act } from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import { APP_ANIMATION_DURATION } from '../../../utilities/constants'; 5 | import { createMockStore } from '../../../utilities/test-utilities/mockStore'; 6 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 7 | import InvalidInputMessage from './InvalidInputMessage'; 8 | 9 | describe('InvalidInputMessage component ', () => { 10 | it('should be in the document', async () => { 11 | const store = createMockStore('/', mockAppStore); 12 | render( 13 | 14 | 15 | 16 | ); 17 | await act(async () => jest.advanceTimersByTime(APP_ANIMATION_DURATION * 1000)); 18 | expect(screen.getByText('my message!')).toBeInTheDocument(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/InvalidInputMessage/InvalidInputMessage.tsx: -------------------------------------------------------------------------------- 1 | import './InvalidInputMessage.scss'; 2 | import { motion } from 'framer-motion'; 3 | import { STAGERRED_SPRING_VARIANTS_2 } from '../../../utilities/constants'; 4 | import { InformationSVG } from '../../../svgs/Information'; 5 | 6 | const InvalidInputMessage = props => { 7 | return ( 8 | 16 | 17 | {props.message} 18 | 19 | ); 20 | }; 21 | 22 | export default InvalidInputMessage; 23 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/QRCode/QRCode.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElementsProject/cln-application/b76cdba2f7fe2e797f2bce42889409c4b0488b20/apps/frontend/src/components/shared/QRCode/QRCode.scss -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/QRCode/QRCode.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render } from '@testing-library/react'; 2 | import { act } from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import { APP_ANIMATION_DURATION } from '../../../utilities/constants'; 5 | import { createMockStore } from '../../../utilities/test-utilities/mockStore'; 6 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 7 | import QRCodeComponent from './QRCode'; 8 | 9 | describe('QRCodeComponent component ', () => { 10 | it('should be in the document', async () => { 11 | const store = createMockStore('/', mockAppStore); 12 | render( 13 | 14 | 15 | 16 | ); 17 | await act(async () => jest.advanceTimersByTime(APP_ANIMATION_DURATION * 1000)); 18 | expect(screen.getByTestId('qr-code-component')).toBeInTheDocument(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/StatusAlert/StatusAlert.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .alert { 4 | padding: 0.75rem; 5 | &.alert-danger, 6 | &.alert-success, 7 | &.alert-warning { 8 | transition: all $transition-time ease; 9 | } 10 | &.alert-danger .btn-sm-svg.btn-svg-copy { 11 | & svg { 12 | & path { 13 | stroke: $danger; 14 | } 15 | &:hover { 16 | & path { 17 | stroke: darken($danger, 25%); 18 | } 19 | } 20 | } 21 | } 22 | &.alert-success .btn-sm-svg.btn-svg-copy { 23 | & svg { 24 | & path { 25 | stroke: $success; 26 | } 27 | &:hover { 28 | & path { 29 | stroke: darken($success, 25%); 30 | } 31 | } 32 | } 33 | } 34 | & .text-status { 35 | max-height: 8rem; 36 | overflow: hidden; 37 | } 38 | & .btn-sm-svg.btn-svg-copy { 39 | display: inline-flex; 40 | align-items: flex-start; 41 | padding: 0; 42 | cursor: pointer; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/StatusAlert/StatusAlert.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render } from '@testing-library/react'; 2 | import { act } from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import { APP_ANIMATION_DURATION } from '../../../utilities/constants'; 5 | import { createMockStore } from '../../../utilities/test-utilities/mockStore'; 6 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 7 | import StatusAlert from './StatusAlert'; 8 | import { CallStatus } from '../../../utilities/constants'; 9 | 10 | describe('StatusAlert component ', () => { 11 | it('pending shows spinner, message is capitalized', async () => { 12 | const store = createMockStore('/', mockAppStore); 13 | render( 14 | 15 | 16 | 17 | ); 18 | await act(async () => jest.advanceTimersByTime(APP_ANIMATION_DURATION * 1000)); 19 | expect(screen.getByTestId('status-pending-spinner')).toBeInTheDocument(); 20 | expect(screen.getByText('Loading...')).toBeInTheDocument(); 21 | }); 22 | 23 | it('CallStatus.None shows nothing', async () => { 24 | const store = createMockStore('/', mockAppStore); 25 | render( 26 | 27 | 28 | 29 | ); 30 | await act(async () => jest.advanceTimersByTime(APP_ANIMATION_DURATION * 1000)); 31 | expect(screen.queryByTestId('status-pending-spinner')).not.toBeInTheDocument(); 32 | expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/ToastMessage/ToastMessage.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .toast-container { 4 | & .toast { 5 | box-shadow: 0px 8px 16px 0px rgba($dark, 0.2); 6 | border-radius: 0.5rem; 7 | border: 1px solid $light; 8 | &.opaque { 9 | background-color: rgba($body-bg, 1); 10 | } 11 | & .message-type-box { 12 | background-color: $light; 13 | border-top-left-radius: 0.35rem; 14 | border-bottom-left-radius: 0.35rem; 15 | } 16 | 17 | span.btn-toast-close { 18 | padding: 0.5rem; 19 | cursor: pointer; 20 | & svg { 21 | width: 0.75rem; 22 | height: 0.75rem; 23 | & path { 24 | transition: all $transition-time ease; 25 | } 26 | } 27 | &:hover { 28 | & svg path { 29 | stroke: darken($light, 15%); 30 | } 31 | } 32 | } 33 | 34 | &[data-bg='success'] { 35 | border: 1px solid $success; 36 | 37 | & .message-type-box { 38 | background-color: $success; 39 | } 40 | } 41 | 42 | &[data-bg='danger'] { 43 | border: 1px solid $danger; 44 | 45 | & .message-type-box { 46 | background-color: $danger; 47 | } 48 | } 49 | 50 | &[data-bg='warning'] { 51 | border: 1px solid $warning; 52 | 53 | & .message-type-box { 54 | background-color: $warning; 55 | } 56 | } 57 | 58 | &[data-bg='primary'] { 59 | border: 1px solid $primary; 60 | 61 | & .message-type-box { 62 | background-color: $primary; 63 | } 64 | } 65 | } 66 | } 67 | 68 | @include color-mode(dark) { 69 | & .toast { 70 | box-shadow: 0px 8px 16px 0px rgba($light, 0.2); 71 | &.opaque { 72 | background-color: rgba($body-bg-dark, 1); 73 | } 74 | span.btn-toast-close { 75 | &:hover { 76 | & svg path { 77 | stroke: lighten($light-dark, 15%); 78 | } 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/ToastMessage/ToastMessage.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render } from '@testing-library/react'; 2 | import { act } from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import { createMockStore } from '../../../utilities/test-utilities/mockStore'; 5 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 6 | import ToastMessage from './ToastMessage'; 7 | import { APP_ANIMATION_DURATION } from '../../../utilities/constants'; 8 | 9 | describe('ToastMessage component ', () => { 10 | it('should be in the document', async () => { 11 | const defaultToastProps = { 12 | showOnComponent: true, 13 | show: true, 14 | message: 'my message', 15 | delay: 0, 16 | onConfirmResponse: jest.fn() 17 | }; 18 | const store = createMockStore('/', mockAppStore); 19 | render( 20 | 21 | 22 | 23 | ); 24 | await act(async () => jest.advanceTimersByTime(APP_ANIMATION_DURATION * 1000)); 25 | expect(screen.getByTestId('toast-body')).toBeInTheDocument(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/ToggleSwitch/ToggleSwitch.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .toggle { 4 | color: $dark; 5 | font-weight: 600; 6 | border: 1px solid $gray-300; 7 | border-radius: 0.5rem; 8 | height: 2rem; 9 | width: 6.5rem; 10 | box-sizing: border-box; 11 | position: relative; 12 | cursor: pointer; 13 | background: $white; 14 | display: flex; 15 | justify-content: flex-start; 16 | 17 | & .toggle-bg-text { 18 | height: 100%; 19 | } 20 | 21 | & .toggle-switch { 22 | margin-top: -1px; 23 | position: absolute; 24 | height: 105%; 25 | width: calc(55%); 26 | color: $white; 27 | font-weight: 600; 28 | background: $primary; 29 | &.toggle-left { 30 | border-color: $primary; 31 | border-top-left-radius: 0.5rem; 32 | border-bottom-left-radius: 0.5rem; 33 | } 34 | &.toggle-right { 35 | border-color: $primary; 36 | border-top-right-radius: 0.5rem; 37 | border-bottom-right-radius: 0.5rem; 38 | } 39 | &:hover { 40 | background-color: lighten($primary, 5%); 41 | border-color: lighten($primary, 5%); 42 | } 43 | } 44 | &[data-isswitchon='true'] { 45 | justify-content: flex-end; 46 | } 47 | } 48 | 49 | @include color-mode(dark) { 50 | .toggle { 51 | color: $white; 52 | background: lighten($card-bg-dark, 10%); 53 | 54 | & .toggle-switch { 55 | color: $card-bg-dark; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/ToggleSwitch/ToggleSwitch.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render } from '@testing-library/react'; 2 | import { act } from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import { APP_ANIMATION_DURATION } from '../../../utilities/constants'; 5 | import { createMockStore } from '../../../utilities/test-utilities/mockStore'; 6 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 7 | import ToggleSwitch from './ToggleSwitch'; 8 | import { Units } from '../../../utilities/constants'; 9 | 10 | describe('ToggleSwitch component ', () => { 11 | it('should be in the document', async () => { 12 | const store = createMockStore('/', mockAppStore); 13 | render( 14 | 15 | 21 | 22 | ); 23 | await act(async () => jest.advanceTimersByTime(APP_ANIMATION_DURATION * 1000)); 24 | expect(screen.getByTestId('toggle-switch')).toBeInTheDocument(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /apps/frontend/src/components/shared/ToggleSwitch/ToggleSwitch.tsx: -------------------------------------------------------------------------------- 1 | import './ToggleSwitch.scss'; 2 | import { useState } from 'react'; 3 | import { motion } from 'framer-motion'; 4 | 5 | import { SPRING_VARIANTS } from '../../../utilities/constants'; 6 | import { RootService } from '../../../services/http.service'; 7 | import { setConfig } from '../../../store/rootSlice'; 8 | import { useDispatch, useSelector } from 'react-redux'; 9 | import { selectAppConfig } from '../../../store/rootSelectors'; 10 | 11 | const ToggleSwitch = props => { 12 | const dispatch = useDispatch(); 13 | const [isSwitchOn, setIsSwitchOn] = useState(props.selValue === props.values[1]); 14 | const appConfig = useSelector(selectAppConfig); 15 | 16 | const changeValueHandler = async(event) => { 17 | setIsSwitchOn((prevValue) => !prevValue); 18 | const currValue = isSwitchOn ? 0 : 1; 19 | const updatedConfig = { 20 | ...appConfig, 21 | uiConfig: { 22 | ...appConfig.uiConfig, 23 | unit: props.values[currValue] 24 | } 25 | }; 26 | await RootService.updateConfig(updatedConfig); 27 | dispatch(setConfig(updatedConfig)); 28 | }; 29 | 30 | return ( 31 |
37 |
38 | {props.values[0]} 39 | {props.values[1]} 40 |
41 | 49 | {props.selValue} 50 | 51 |
52 | ); 53 | }; 54 | 55 | export default ToggleSwitch; 56 | -------------------------------------------------------------------------------- /apps/frontend/src/components/ui/Header/Header.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react'; 2 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 3 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 4 | import Header from './Header'; 5 | 6 | describe('Header component ', () => { 7 | beforeEach(() => {}); 8 | 9 | it('should be in the document', async () => { 10 | await renderWithProviders(
, { preloadedState: mockAppStore, initialRoute: ['/cln'] }) 11 | expect(screen.getByTestId('header')).toBeInTheDocument(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /apps/frontend/src/components/ui/Loading/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Col, Row, Spinner } from "react-bootstrap"; 2 | 3 | export const Loading = () => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 |
Loading...
11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /apps/frontend/src/components/ui/Menu/Menu.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .menu-dropdown.dropdown { 4 | & .dropdown-toggle.btn-menu { 5 | background-color: $primary; 6 | width: 8rem; 7 | padding: 1.25rem 1.375rem; 8 | &::after { 9 | display: none; 10 | } 11 | } 12 | & .dropdown-menu { 13 | & a.dropdown-item { 14 | &.active, 15 | &:active, 16 | &:focus, 17 | &:hover { 18 | color: $primary; 19 | } 20 | } 21 | & div.dropdown-item { 22 | color: $dark; 23 | } 24 | } 25 | &:focus-visible { 26 | outline: none; 27 | } 28 | } 29 | 30 | @include color-mode(light) { 31 | .menu-dropdown.dropdown { 32 | & .dropdown-toggle.btn-menu { 33 | color: $white; 34 | & svg path { 35 | fill: $white; 36 | } 37 | } 38 | & > .dropdown-menu { 39 | border: none; 40 | } 41 | } 42 | .btn-compact { 43 | background-color: $primary; 44 | & svg > path { 45 | fill: $dark; 46 | } 47 | } 48 | } 49 | 50 | @include color-mode(dark) { 51 | .menu-dropdown.dropdown { 52 | & .dropdown-toggle.btn-menu { 53 | color: $card-bg-dark; 54 | & svg path { 55 | fill: $card-bg-dark; 56 | } 57 | } 58 | & .dropdown-menu { 59 | & div.dropdown-item { 60 | color: $white; 61 | } 62 | } 63 | } 64 | .btn-compact { 65 | background-color: $primary; 66 | & svg > path { 67 | fill: $dark; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /apps/frontend/src/components/ui/Menu/Menu.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react'; 2 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 3 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 4 | import Menu from './Menu'; 5 | 6 | describe('Menu component ', () => { 7 | beforeEach(() => {}); 8 | 9 | it('should be in the document', async () => { 10 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/cln'] }) 11 | expect(screen.getByTestId('menu')).toBeInTheDocument(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /apps/frontend/src/components/ui/Menu/Menu.tsx: -------------------------------------------------------------------------------- 1 | import './Menu.scss'; 2 | import { Link, useLocation } from 'react-router-dom'; 3 | import Dropdown from 'react-bootstrap/Dropdown'; 4 | import { useSelector } from 'react-redux'; 5 | import { selectIsAuthenticated, selectNodeInfo } from '../../../store/rootSelectors'; 6 | 7 | const Menu = props => { 8 | const isAuthenticated = useSelector(selectIsAuthenticated); 9 | const nodeInfo = useSelector(selectNodeInfo); 10 | const location = useLocation(); 11 | 12 | return ( 13 | 22 | 36 | {location.pathname.includes('bookkeeper') ? 'Dashboard' : 'Bookkeeper'} 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default Menu; 43 | -------------------------------------------------------------------------------- /apps/frontend/src/components/ui/RouteTransition/RouteTransition.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { AnimatePresence, motion } from 'framer-motion'; 3 | import { Outlet, useLocation } from 'react-router-dom'; 4 | import { TRANSITION_DURATION } from '../../../utilities/constants'; 5 | 6 | const RouteTransition = () => { 7 | const location = useLocation(); 8 | 9 | useEffect(() => { 10 | window.scrollTo({ top: 0, behavior: 'smooth' }); 11 | }, [location.pathname]); 12 | 13 | return ( 14 | 15 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default RouteTransition; 31 | -------------------------------------------------------------------------------- /apps/frontend/src/components/ui/Settings/Settings.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/constants.scss'; 2 | 3 | .settings-menu.dropdown { 4 | & .dropdown-toggle.btn-settings-menu { 5 | margin-left: 0.5rem; 6 | padding: 1.25rem 1.375rem; 7 | &::after { 8 | display: none; 9 | } 10 | } 11 | & .dropdown-menu { 12 | & a.dropdown-item { 13 | &.active, 14 | &:active, 15 | &:focus, 16 | &:hover { 17 | color: $primary; 18 | } 19 | } 20 | & div.dropdown-item { 21 | color: $dark; 22 | } 23 | } 24 | } 25 | 26 | @include color-mode(light) { 27 | .settings-menu.dropdown { 28 | margin-left: 0.5rem; 29 | & .dropdown-toggle.btn-settings-menu { 30 | color: $white; 31 | & svg path { 32 | fill: $white; 33 | } 34 | } 35 | & > .dropdown-menu { 36 | border: none; 37 | } 38 | } 39 | .btn-compact { 40 | background-color: $primary; 41 | & svg > path { 42 | fill: $dark; 43 | } 44 | } 45 | } 46 | 47 | @include color-mode(dark) { 48 | .settings-menu.dropdown { 49 | & .dropdown-toggle.btn-settings-menu { 50 | color: $card-bg-dark; 51 | & svg path { 52 | fill: $card-bg-dark; 53 | } 54 | } 55 | & .dropdown-menu { 56 | & div.dropdown-item { 57 | color: $white; 58 | } 59 | } 60 | } 61 | .btn-compact { 62 | background-color: $primary; 63 | & svg > path { 64 | fill: $dark; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /apps/frontend/src/components/ui/Settings/Settings.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react'; 2 | import { renderWithProviders } from '../../../utilities/test-utilities/mockStore'; 3 | import { mockAppStore } from '../../../utilities/test-utilities/mockData'; 4 | import Settings from './Settings'; 5 | 6 | describe('Settings component ', () => { 7 | beforeEach(() => {}); 8 | 9 | it('should be in the document', async () => { 10 | await renderWithProviders(, { preloadedState: mockAppStore, initialRoute: ['/cln'] }) 11 | expect(screen.getByTestId('settings')).toBeInTheDocument(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /apps/frontend/src/hooks/use-breakpoint.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Junaid Atari 3 | * @link https://gist.github.com/blacksmoke26/65f35ee824674e00d858047e852bd270 4 | * 5 | * Modified by AgainPsychoX to use TypeScript and `use-debounce` package. 6 | * Modified by Shahana to remove `use-debounce` package and use enums. 7 | */ 8 | 9 | import { useState, useEffect } from 'react'; 10 | import { Breakpoints } from '../utilities/constants'; 11 | 12 | const resolveBreakpoint = (width: number): Breakpoints => { 13 | if (width < 576) return Breakpoints.XS; 14 | if (width < 768) return Breakpoints.SM; 15 | if (width < 992) return Breakpoints.MD; 16 | if (width < 1200) return Breakpoints.LG; 17 | if (width < 1440) return Breakpoints.XL; 18 | return Breakpoints.XXL; 19 | }; 20 | 21 | const useBreakpoint = () => { 22 | const [size, setSize] = useState(() => resolveBreakpoint(window.innerWidth)); 23 | 24 | useEffect(() => { 25 | const update = () => { 26 | return setTimeout(() => { 27 | return setSize(resolveBreakpoint(window.innerWidth)); 28 | }, 200); 29 | }; 30 | 31 | window.addEventListener('resize', update); 32 | return () => window.removeEventListener('resize', update); 33 | }, []); 34 | 35 | return size; 36 | }; 37 | 38 | export default useBreakpoint; 39 | -------------------------------------------------------------------------------- /apps/frontend/src/hooks/use-injectreducer.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { appStore } from '../store/appStore'; 3 | import { StoreWithManager } from '../store/store.type'; 4 | 5 | const injectedReducers: Record = {}; 6 | 7 | export function useInjectReducer( 8 | key: Key, 9 | reducer: any 10 | ) { 11 | useEffect(() => { 12 | const store = appStore as StoreWithManager; 13 | if (injectedReducers[key]) return; 14 | 15 | store.reducerManager.add(key, reducer); 16 | store.replaceReducer(store.reducerManager.reduce); 17 | injectedReducers[key] = true; 18 | }, [key, reducer]); 19 | } 20 | -------------------------------------------------------------------------------- /apps/frontend/src/hooks/use-input.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { InputType } from '../utilities/constants'; 3 | 4 | const useInput = (validateValue, inputType: InputType = InputType.ORIGINAL) => { 5 | const [enteredValue, setEnteredValue] = useState(''); 6 | const [isTouched, setIsTouched] = useState(false); 7 | 8 | const normalizeValue = (value: string) => { 9 | switch (inputType) { 10 | case 'lowercase': 11 | return value.toLowerCase(); 12 | case 'uppercase': 13 | return value.toUpperCase(); 14 | default: 15 | return value; 16 | } 17 | }; 18 | 19 | const valueIsValid = validateValue(enteredValue); 20 | const hasError = !valueIsValid && isTouched; 21 | 22 | const valueChangeHandler = (event) => { 23 | event.target.value = normalizeValue(event.target.value); 24 | setEnteredValue(event.target.value); 25 | }; 26 | 27 | const inputBlurHandler = (event) => { 28 | setIsTouched(true); 29 | }; 30 | 31 | const reset = () => { 32 | setEnteredValue(''); 33 | setIsTouched(false); 34 | }; 35 | 36 | return { 37 | value: enteredValue, 38 | isValid: valueIsValid, 39 | hasError, 40 | valueChangeHandler, 41 | inputBlurHandler, 42 | reset 43 | }; 44 | }; 45 | 46 | export default useInput; 47 | -------------------------------------------------------------------------------- /apps/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | import { Provider } from 'react-redux'; 3 | import { RouterProvider } from 'react-router-dom'; 4 | import { createRootRouter } from './routes/router.config'; 5 | import { HttpService, RootService } from './services/http.service'; 6 | import { appStore } from './store/appStore'; 7 | import { defaultRootState } from './store/rootSelectors'; 8 | import { setAuthStatus, setConfig, setFiatConfig, setShowModals } from './store/rootSlice'; 9 | 10 | export async function initializeAuth() { 11 | try { 12 | await HttpService.setCSRFToken(); 13 | const data = await RootService.fetchAuthData(); 14 | return data; 15 | } catch (error) { 16 | return {config: defaultRootState.appConfig, authStatus: defaultRootState.authStatus, fiatConfig: defaultRootState.fiatConfig}; 17 | } 18 | } 19 | 20 | async function bootstrapApp() { 21 | const { config, authStatus, fiatConfig } = await initializeAuth(); 22 | 23 | if (!authStatus.isAuthenticated) { 24 | if (authStatus.isValidPassword) { 25 | appStore.dispatch(setShowModals({ ...defaultRootState.showModals, loginModal: true })); 26 | } else { 27 | appStore.dispatch(setShowModals({ ...defaultRootState.showModals, setPasswordModal: true })); 28 | } 29 | } 30 | 31 | appStore.dispatch(setAuthStatus(authStatus)); 32 | appStore.dispatch(setConfig(config)); 33 | appStore.dispatch(setFiatConfig(fiatConfig)); 34 | 35 | const rootRouter = createRootRouter(); 36 | const root = ReactDOM.createRoot(document.getElementById('root')!); 37 | root.render( 38 | 39 | 40 | 41 | ); 42 | } 43 | 44 | bootstrapApp(); -------------------------------------------------------------------------------- /apps/frontend/src/routes/dataLoader.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderFunctionArgs } from "react-router-dom"; 2 | import { BookkeeperService, CLNService, RootService } from "../services/http.service"; 3 | import { appStore } from "../store/appStore"; 4 | import { AppState } from "../store/store.type"; 5 | 6 | export async function rootLoader({ request }: LoaderFunctionArgs) { 7 | const state = appStore.getState() as AppState; 8 | if (state.root.authStatus.isAuthenticated) { 9 | const [connectwalletData, rootData] = await Promise.all([ 10 | RootService.getConnectWallet(), 11 | RootService.fetchRootData() 12 | ]); 13 | return { ...rootData, connectWallet: connectwalletData }; 14 | } 15 | return null 16 | } 17 | 18 | export async function clnLoader({ request }: LoaderFunctionArgs) { 19 | const state = appStore.getState() as AppState; 20 | if (state.root.authStatus.isAuthenticated) { 21 | const clnData = await CLNService.fetchCLNData(); 22 | return clnData; 23 | } 24 | return null; 25 | } 26 | 27 | export async function bkprLoader({ request }: LoaderFunctionArgs) { 28 | const state = appStore.getState() as AppState; 29 | if (state.root.authStatus.isAuthenticated) { 30 | const bkprData = await BookkeeperService.fetchBKPRData(); 31 | return bkprData; 32 | } 33 | return null; 34 | } 35 | -------------------------------------------------------------------------------- /apps/frontend/src/services/logger.service.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel, LOG_LEVEL } from '../utilities/constants'; 2 | 3 | export interface LogFn { 4 | (message?: any, ...optionalParams: any[]): void; 5 | } 6 | 7 | export interface Logger { 8 | info: LogFn; 9 | warn: LogFn; 10 | error: LogFn; 11 | } 12 | 13 | const NO_OP: LogFn = (message?: any, ...optionalParams: any[]) => {}; 14 | 15 | export class ConsoleLogger implements Logger { 16 | readonly info: LogFn; 17 | readonly warn: LogFn; 18 | readonly error: LogFn; 19 | 20 | constructor(options?: { level? : LogLevel }) { 21 | const { level } = options || {}; 22 | 23 | this.error = console.error.bind(console); 24 | 25 | if (level === 'error') { 26 | this.warn = NO_OP; 27 | this.info = NO_OP; 28 | 29 | return; 30 | } 31 | 32 | this.warn = console.warn.bind(console); 33 | 34 | if (level === 'warn') { 35 | this.info = NO_OP; 36 | 37 | return; 38 | } 39 | 40 | this.info = console.log.bind(console); 41 | } 42 | } 43 | 44 | const logger = new ConsoleLogger({ level: LOG_LEVEL }); 45 | 46 | export default logger; 47 | -------------------------------------------------------------------------------- /apps/frontend/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { spyOnBKPRGetAccountEvents, spyOnBKPRGetSatsFlow, spyOnBKPRGetVolume, spyOnGetInfo, spyOnListChannels, spyOnListFunds, spyOnListNodes, spyOnListPeers } from './utilities/test-utilities/mockService'; 3 | 4 | let mockedLocation = { 5 | pathname: '/', 6 | search: '', 7 | hash: '', 8 | state: null, 9 | key: 'default', 10 | }; 11 | 12 | jest.mock('react-router-dom', () => { 13 | const actual = jest.requireActual('react-router-dom'); 14 | return { 15 | ...actual, 16 | useNavigate: () => jest.fn(), 17 | useLocation: () => mockedLocation, 18 | }; 19 | }); 20 | 21 | export const setMockedLocation = (location: Partial) => { 22 | mockedLocation = { ...mockedLocation, ...location }; 23 | }; 24 | 25 | beforeEach(() => { 26 | jest.useFakeTimers(); 27 | spyOnGetInfo(); 28 | spyOnListNodes(); 29 | spyOnListChannels(); 30 | spyOnListPeers(); 31 | spyOnListFunds(); 32 | spyOnBKPRGetAccountEvents(); 33 | spyOnBKPRGetSatsFlow(); 34 | spyOnBKPRGetVolume(); 35 | const originalConsoleWarning = console.warn; 36 | jest.spyOn(console, 'warn').mockImplementation((msg, ...args) => { 37 | if ( 38 | typeof msg === 'string' && 39 | msg.includes('React Router Future Flag Warning: React Router will begin wrapping state updates in `React.startTransition` in v7') 40 | ) { 41 | return; 42 | } 43 | originalConsoleWarning(msg, ...args); 44 | }); 45 | }); 46 | 47 | afterEach(() => { 48 | jest.useRealTimers(); 49 | jest.restoreAllMocks(); 50 | }); 51 | 52 | beforeAll(() => { 53 | window.scrollTo = jest.fn(); 54 | }); 55 | 56 | afterAll(() => { 57 | jest.restoreAllMocks(); 58 | }); 59 | -------------------------------------------------------------------------------- /apps/frontend/src/store/appStore.tsx: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import rootReducer from './rootSlice'; 3 | import { StoreWithManager } from './store.type'; 4 | import { combineReducers } from "@reduxjs/toolkit"; 5 | 6 | export function createReducerManager(initialReducers: any) { 7 | const reducers = { ...initialReducers }; 8 | let combinedReducer = combineReducers(reducers); 9 | 10 | return { 11 | getReducerMap: () => reducers, 12 | reduce: (state: any, action: any) => combinedReducer(state, action), 13 | add: (key: string, reducer: any) => { 14 | if (!key || reducers[key]) return; 15 | reducers[key] = reducer; 16 | combinedReducer = combineReducers(reducers); 17 | }, 18 | remove: (key: string) => { 19 | if (!key || !reducers[key]) return; 20 | delete reducers[key]; 21 | combinedReducer = combineReducers(reducers); 22 | }, 23 | }; 24 | } 25 | 26 | const reducerManager = createReducerManager({ root: rootReducer }); 27 | 28 | export const appStore = configureStore({ 29 | reducer: reducerManager.reduce, 30 | }) as StoreWithManager; 31 | 32 | appStore.reducerManager = reducerManager; 33 | -------------------------------------------------------------------------------- /apps/frontend/src/store/clnSelectors.tsx: -------------------------------------------------------------------------------- 1 | import { createSelector } from '@reduxjs/toolkit'; 2 | import { CLNState } from '../types/cln.type'; 3 | 4 | export const defaultCLNState: CLNState = { 5 | listInvoices: { isLoading: true, invoices: [] }, 6 | listPayments: { isLoading: true, payments: [] }, 7 | listOffers: { isLoading: true, offers: [] }, 8 | listLightningTransactions: { isLoading: true, clnTransactions: [] }, 9 | listBitcoinTransactions: { isLoading: true, btcTransactions: [] }, 10 | feeRate: { isLoading: true }, 11 | }; 12 | 13 | const selectCLNState = (state: { cln: CLNState }) => state.cln || defaultCLNState; 14 | 15 | export const selectListInvoices = createSelector( 16 | selectCLNState, 17 | (cln) => cln.listInvoices 18 | ); 19 | 20 | export const selectListPayments = createSelector( 21 | selectCLNState, 22 | (cln) => cln.listPayments 23 | ); 24 | 25 | export const selectListOffers = createSelector( 26 | selectCLNState, 27 | (cln) => cln.listOffers 28 | ); 29 | 30 | export const selectListLightningTransactions = createSelector( 31 | selectCLNState, 32 | (cln) => cln.listLightningTransactions 33 | ); 34 | 35 | export const selectListBitcoinTransactions = createSelector( 36 | selectCLNState, 37 | (cln) => cln.listBitcoinTransactions 38 | ); 39 | 40 | export const selectFeeRate = createSelector( 41 | selectCLNState, 42 | (cln) => cln.feeRate 43 | ); 44 | 45 | export const selectInvoiceByHash = (paymentHash: string) => createSelector( 46 | selectListInvoices, 47 | (data) => data.invoices?.find(inv => inv.payment_hash === paymentHash) 48 | ); 49 | 50 | export const selectPaymentByHash = (paymentHash: string) => createSelector( 51 | selectListPayments, 52 | (data) => data.payments?.find(pay => pay.payment_hash === paymentHash) 53 | ); 54 | 55 | export const selectCurrentFeeRate = createSelector( 56 | selectFeeRate, 57 | (feeRate) => feeRate.onchain_fee_estimates?.opening_channel_satoshis || 0 58 | ); 59 | -------------------------------------------------------------------------------- /apps/frontend/src/store/store.type.ts: -------------------------------------------------------------------------------- 1 | import { EnhancedStore } from "@reduxjs/toolkit"; 2 | import { BKPRState } from "../types/bookkeeper.type"; 3 | import { CLNState } from "../types/cln.type"; 4 | import { RootState } from "../types/root.type"; 5 | 6 | export type ReducerManager = { 7 | getReducerMap: () => Record; 8 | reduce: (state: any, action: any) => any; 9 | add: (key: string, reducer: any) => void; 10 | remove: (key: string) => void; 11 | }; 12 | 13 | export interface StoreWithManager extends EnhancedStore { 14 | reducerManager: ReducerManager; 15 | getActions: () => any[]; 16 | clearActions: () => any; 17 | } 18 | 19 | export type AppState = { 20 | root: RootState; 21 | cln?: CLNState; 22 | bkpr?: BKPRState; 23 | }; 24 | -------------------------------------------------------------------------------- /apps/frontend/src/styles/bootstrap-custom.scss: -------------------------------------------------------------------------------- 1 | @import 'constants'; 2 | 3 | $theme-colors: ( 4 | primary: $primary, 5 | secondary: $secondary, 6 | success: $success, 7 | warning: $warning, 8 | danger: $danger, 9 | dark: $dark, 10 | light: $light, 11 | ); 12 | 13 | @import '~bootstrap'; 14 | -------------------------------------------------------------------------------- /apps/frontend/src/styles/constants.scss: -------------------------------------------------------------------------------- 1 | @import './fonts.scss'; 2 | @import '~bootstrap/scss/functions'; 3 | @import '~bootstrap/scss/variables'; 4 | @import '~bootstrap/scss/mixins'; 5 | @import 'react-perfect-scrollbar/dist/css/styles.css'; 6 | 7 | $transition-time: 300ms; 8 | $theme-transition: 500ms; 9 | $color-mode-type: data; 10 | $enable-shadows: true; 11 | $enable-gradients: true; 12 | $enable-responsive-font-sizes: true; 13 | 14 | $font-family-base: -apple-system, BlinkMacSystemFont, 'Inter', 'DM Sans', 'Helvetica Neue', 15 | 'Segoe UI', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 16 | 'Segoe UI Symbol', 'Noto Color Emoji'; 17 | 18 | $font-size-base: 14px; 19 | $font-weight-base: 500; 20 | $btn-font-size: 14px; 21 | 22 | $primary: #e1ba2d; 23 | $primary-darker: #cca103; 24 | $secondary: $gray-100; 25 | $success: #33db95; 26 | $warning: #fe8e02; 27 | $danger: #dc3545; 28 | $blue: #1b2559; 29 | $darker-blue: #141c44; 30 | $light: #9f9f9f; 31 | $dark: #3a4247; 32 | $light-dark: #b7bbc2; 33 | $card-bg-dark: #2a2a2c; 34 | $text-dark: #131314; 35 | $form-ctrl-bg-dark: #303032; 36 | $dark-blue: #101828; 37 | $border-color-dark: #495057; 38 | $border-color: #dee2e6; 39 | $tooltip-bg-dark: #1b1b1d; 40 | 41 | $body-bg-light: #ebeff9; 42 | $body-bg: $white; 43 | $body-color: $dark; 44 | $body-tertiary-bg: $white; 45 | $body-bg-dark: #0c0c0f; 46 | $body-color-dark: $white; 47 | 48 | $border-radius: 1.25rem; 49 | $btn-link-color: $primary; 50 | $btn-padding-x: 0.625rem; 51 | 52 | $form-check-input-checked-bg-color: $primary; 53 | $form-check-input-checked-border-color: $primary; 54 | $form-check-input-focus-border: lighten($primary, 10%); 55 | $form-check-input-focus-box-shadow: 0 0 0 0.25rem rgba(lighten($primary, 10%), 0.25); 56 | 57 | $card-border-width: 0.5px; 58 | $card-border-color: rgba($light, 0.1); 59 | $card-border-radius: $border-radius; 60 | $card-cap-bg: transparent; 61 | 62 | $dropdown-min-width: 5rem; 63 | $dropdown-box-shadow: none; 64 | $dropdown-border-radius: 0.5rem; 65 | 66 | $input-color: $dark; 67 | $input-disabled-bg: $gray-200; 68 | $modal-backdrop-opacity: 0.2; 69 | 70 | $form-range-track-height: 0.25rem; 71 | $form-range-track-bg: $success; 72 | $form-range-thumb-bg: $white; 73 | $form-range-thumb-border: 0.5px solid $gray-300; 74 | $form-range-thumb-focus-box-shadow: 75 | 0 0 0 1px $body-bg, 76 | $primary; 77 | $form-range-thumb-active-bg: tint-color($primary, 70%); 78 | 79 | $toast-max-width: 24rem; 80 | -------------------------------------------------------------------------------- /apps/frontend/src/styles/fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Inter'; 3 | font-style: italic; 4 | font-weight: 400; 5 | font-display: swap; 6 | src: url('../../public/fonts/Inter-Thin.ttf') format('truetype'); 7 | } 8 | @font-face { 9 | font-family: 'Inter'; 10 | font-style: normal; 11 | font-weight: 500; 12 | font-display: swap; 13 | src: url('../../public/fonts/Inter-Medium.ttf') format('truetype'); 14 | } 15 | @font-face { 16 | font-family: 'Inter'; 17 | font-style: normal; 18 | font-weight: 600; 19 | font-display: swap; 20 | src: url('../../public/fonts/Inter-SemiBold.ttf') format('truetype'); 21 | } 22 | @font-face { 23 | font-family: 'Inter'; 24 | font-style: normal; 25 | font-weight: 700; 26 | font-display: swap; 27 | src: url('../../public/fonts/Inter-Bold.ttf') format('truetype'); 28 | } 29 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/AccountEvents.tsx: -------------------------------------------------------------------------------- 1 | export const AccountEventsSVG = props => { 2 | return ( 3 | 12 | 13 | 20 | 27 | 34 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/Action.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const ActionSVG = props => { 4 | return ( 5 | 13 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/Add.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { OverlayTrigger, Tooltip } from 'react-bootstrap'; 3 | 4 | export const AddSVG = props => { 5 | return ( 6 | {props.tooltipText || ''} : <>)} 10 | > 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/Address.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const AddressSVG = props => { 4 | return ( 5 | 13 | 17 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/Balance.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { motion } from 'framer-motion'; 4 | import { ANIMATION_FINAL_STATE, ANIMATION_INITIAL_STATE, ANIMATION_TRANSITION, OPACITY_VARIANTS } from '../utilities/constants'; 5 | 6 | export const BalanceSVG = props => { 7 | return ( 8 | 17 | 27 | 28 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/BitcoinWallet.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { motion } from 'framer-motion'; 4 | import { ANIMATION_FINAL_STATE, ANIMATION_INITIAL_STATE, ANIMATION_TRANSITION, OPACITY_VARIANTS } from '../utilities/constants'; 5 | 6 | export const BitcoinWalletSVG = props => { 7 | return ( 8 | 17 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/CLNLogo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const CLNLogoSVG = props => { 4 | return ( 5 | 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/Capacity.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { motion } from 'framer-motion'; 4 | import { ANIMATION_FINAL_STATE, ANIMATION_INITIAL_STATE, ANIMATION_TRANSITION, STAGERRED_SPRING_VARIANTS_1 } from '../utilities/constants'; 5 | 6 | export const CapacitySVG = props => { 7 | return ( 8 | 18 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/Channels.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { motion } from 'framer-motion'; 4 | import { STAGERRED_SPRING_VARIANTS_1 } from '../utilities/constants'; 5 | 6 | export const ChannelsSVG = props => { 7 | return ( 8 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/ChevronDown.tsx: -------------------------------------------------------------------------------- 1 | export const ChevronDown = (props) => { 2 | return ( 3 | 11 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/Close.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const CloseSVG = props => { 4 | return ( 5 | 13 | 19 | 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/Copy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { OverlayTrigger, Tooltip } from 'react-bootstrap'; 3 | 4 | export const CopySVG = props => { 5 | return ( 6 | {'Copy ' + (props.id || '')} : <>)} 10 | > 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/Deposit.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const DepositSVG = props => { 4 | return ( 5 | 13 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/Description.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const DescriptionSVG = props => { 4 | return ( 5 | 13 | 17 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/Hide.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const HideSVG = props => { 4 | return ( 5 | // Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. 6 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/IncomingArrow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { OverlayTrigger, Tooltip } from 'react-bootstrap'; 3 | import { titleCase } from '../utilities/data-formatters'; 4 | 5 | export const IncomingArrowSVG = props => { 6 | return ( 7 | {titleCase(props.txStatus)} : (props.txStatus?.toLowerCase() === 'deposit') ? <> : {props.txStatus?.toLowerCase() === 'paid' ? 'Received' : titleCase(props.txStatus)}} 11 | > 12 | 20 | 21 | 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/Information.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const InformationSVG = props => { 4 | return ( 5 | 13 | 19 | 25 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/LightningWallet.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { motion } from 'framer-motion'; 4 | import { ANIMATION_FINAL_STATE, ANIMATION_INITIAL_STATE, ANIMATION_TRANSITION, OPACITY_VARIANTS } from '../utilities/constants'; 5 | 6 | export const LightningWalletSVG = props => { 7 | return ( 8 | 17 | 24 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/NightMode.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const NightModeSVG = props => { 4 | return ( 5 | 13 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/OpenLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { OverlayTrigger, Tooltip } from 'react-bootstrap'; 3 | 4 | export const OpenLinkSVG = props => { 5 | return ( 6 | {'Open with Blockstream Explorer'}} 10 | > 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/OutgoingArrow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { OverlayTrigger, Tooltip } from 'react-bootstrap'; 3 | import { titleCase } from '../utilities/data-formatters'; 4 | 5 | export const OutgoingArrowSVG = props => { 6 | return ( 7 | : {props.txStatus?.toLowerCase() === 'complete' ? 'Paid' : titleCase(props.txStatus)}} 11 | > 12 | 20 | 21 | 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/Password.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const PasswordSVG = props => { 4 | return ( 5 | // Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. 6 | 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/QuestionMark.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const QuestionMarkSVG = props => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/Reserved.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { OverlayTrigger, Tooltip } from 'react-bootstrap'; 3 | 4 | // Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. 5 | export const ReservedSVG = props => { 6 | return ( 7 | Reserved} 11 | > 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/SQL.tsx: -------------------------------------------------------------------------------- 1 | export const SQLSVG = props => { 2 | return ( 3 | 11 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/SatsFlow.tsx: -------------------------------------------------------------------------------- 1 | export const SatsFlowSVG = props => { 2 | return ( 3 | 12 | 13 | 20 | 27 | 34 | 41 | 48 | 55 | 62 | 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/Show.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const ShowSVG = props => { 4 | return ( 5 | // Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. 6 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/UnReserved.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { OverlayTrigger, Tooltip } from 'react-bootstrap'; 3 | 4 | // Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. 5 | export const UnReservedSVG = props => { 6 | return ( 7 | Unreserved} 11 | > 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/VolumeChart.tsx: -------------------------------------------------------------------------------- 1 | export const VolumeChartSVG = props => { 2 | return ( 3 | 12 | 13 | 20 | 27 | 34 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /apps/frontend/src/svgs/Withdraw.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const WithdrawSVG = props => { 4 | return ( 5 | 13 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /apps/frontend/src/utilities/bookkeeper-sql.ts: -------------------------------------------------------------------------------- 1 | export const AccountEventsSQL = 2 | 'SELECT peerchannels.short_channel_id, ' + 3 | 'nodes.alias, ' + 4 | 'bkpr_accountevents.credit_msat, ' + 5 | 'bkpr_accountevents.debit_msat, ' + 6 | 'bkpr_accountevents.account, ' + 7 | 'bkpr_accountevents.timestamp ' + 8 | 'FROM bkpr_accountevents ' + 9 | 'LEFT JOIN peerchannels ON upper(bkpr_accountevents.account)=hex(peerchannels.channel_id) ' + 10 | 'LEFT JOIN nodes ON peerchannels.peer_id=nodes.nodeid ' + 11 | "WHERE bkpr_accountevents.type != 'onchain_fee' " + 12 | "AND bkpr_accountevents.account != 'external';"; 13 | 14 | export const SatsFlowSQL = (startTimestamp: number, endTimestamp: number): string => 15 | 'SELECT account, ' + 16 | 'tag, ' + 17 | 'credit_msat, ' + 18 | 'debit_msat, ' + 19 | 'currency, ' + 20 | 'timestamp, ' + 21 | 'description, ' + 22 | 'outpoint, ' + 23 | 'txid, ' + 24 | 'payment_id ' + 25 | 'FROM bkpr_income ' + 26 | 'WHERE bkpr_income.timestamp BETWEEN ' + 27 | startTimestamp + 28 | ' AND ' + 29 | endTimestamp + 30 | ';'; 31 | 32 | export const VolumeSQL = 33 | 'SELECT in_channel, ' + 34 | '(SELECT peer_id FROM peerchannels WHERE peerchannels.short_channel_id=in_channel) AS in_channel_peerid, ' + 35 | '(SELECT nodes.alias FROM nodes WHERE nodes.nodeid=(SELECT peer_id FROM peerchannels WHERE peerchannels.short_channel_id=in_channel)) AS in_channel_peer_alias, ' + 36 | 'SUM(in_msat), ' + 37 | 'out_channel, ' + 38 | '(SELECT peer_id FROM peerchannels WHERE peerchannels.short_channel_id=out_channel) AS out_channel_peerid, ' + 39 | '(SELECT nodes.alias FROM nodes WHERE nodes.nodeid=(SELECT peer_id FROM peerchannels WHERE peerchannels.short_channel_id=out_channel)) AS out_channel_peer_alias, ' + 40 | 'SUM(out_msat), ' + 41 | 'SUM(fee_msat) ' + 42 | 'FROM forwards ' + 43 | "WHERE forwards.status='settled' " + 44 | 'GROUP BY in_channel, out_channel;'; 45 | -------------------------------------------------------------------------------- /apps/frontend/src/utilities/data-formatters.test.tsx: -------------------------------------------------------------------------------- 1 | import { isCompatibleVersion } from './data-formatters'; 2 | 3 | describe('isCompatibleVersion function', () => { 4 | it('should return true for compatible versions', async () => { 5 | expect(isCompatibleVersion('v23.05', '23.05')).toBe(true); 6 | expect(isCompatibleVersion('v23.08', '23.05')).toBe(true); 7 | expect(isCompatibleVersion('v24.02rc1', '23.05')).toBe(true); 8 | expect(isCompatibleVersion('v23.05rc1', '23.05')).toBe(true); 9 | expect(isCompatibleVersion('v23.05rc4-11-g1e96146', '23.05')).toBe(true); 10 | expect(isCompatibleVersion('v23.05-1-gf165dc0-modded', '23.05')).toBe(true); 11 | expect(isCompatibleVersion('v23.11-2', '23.05')).toBe(true); 12 | }); 13 | 14 | it('should return false for incompatible versions', async () => { 15 | expect(isCompatibleVersion('v23.02', '23.05')).toBe(false); 16 | expect(isCompatibleVersion('v22.08', '23.05')).toBe(false); 17 | expect(isCompatibleVersion('v23.02rc1', '23.05')).toBe(false); 18 | expect(isCompatibleVersion('v23.05.1', '23.05')).toBe(false); 19 | }); 20 | 21 | it('should handle empty or invalid input', async () => { 22 | expect(isCompatibleVersion('', '23.05')).toBe(false); 23 | expect(isCompatibleVersion('23.05', '')).toBe(false); 24 | expect(isCompatibleVersion('', '')).toBe(false); 25 | expect(isCompatibleVersion('invalidVersion', '23.05')).toBe(false); 26 | expect(isCompatibleVersion('23.05', 'invalidVersion')).toBe(false); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /apps/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "ESNext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "noImplicitAny": false, 18 | "sourceMap": false, 19 | "jsx": "react-jsx" 20 | }, 21 | "include": ["src"] 22 | } 23 | -------------------------------------------------------------------------------- /env.sh: -------------------------------------------------------------------------------- 1 | SETUP=${1:-local} 2 | 3 | export APP_BITCOIN_NETWORK="regtest" 4 | export BITCOIN_NETWORK="$APP_BITCOIN_NETWORK" 5 | if [[ "$APP_BITCOIN_NETWORK" == "mainnet" ]]; then 6 | export BITCOIN_NETWORK="bitcoin" 7 | fi 8 | 9 | export APP_PORT=2103 10 | export HIDDEN_SERVICE_URL="http://oqaer4kd7ufryngx6dsztovs4pnlmaouwmtkofjsd2m7pkq7qd.onion" 11 | export APP_MODE="testing" 12 | export APP_PROTOCOL="http" 13 | 14 | if [[ "$SETUP" == "docker" ]]; then 15 | export DEVICE_DOMAIN_NAME="docker.local" 16 | export LOCAL_HOST="http://""$DEVICE_DOMAIN_NAME" 17 | export BITCOIN_NODE_IP="170.21.22.2" 18 | export LIGHTNING_IP="170.21.22.3" 19 | export APP_IP="170.21.22.5" 20 | export APP_CONFIG_DIR="/data/app" 21 | export APP_CORE_LIGHTNING_REST_CERT_DIR="/c-lightning-rest/certs" 22 | export LIGHTNING_WEBSOCKET_PORT=2106 23 | export APP_BITCOIN_RPC_USER="user" 24 | export APP_BITCOIN_RPC_PASS="password" 25 | export LIGHTNING_GRPC_PORT=2105 26 | export LIGHTNING_REST_PORT=2104 27 | export SINGLE_SIGN_ON=true 28 | export LIGHTNING_PATH="/data/.lightning" 29 | export COMMANDO_CONFIG="/data/.lightning/.commando-env" 30 | echo "Docker Environment Variables Set" 31 | else 32 | export DEVICE_DOMAIN_NAME="local.local" 33 | export LOCAL_HOST="http://""$DEVICE_DOMAIN_NAME" 34 | export BITCOIN_NODE_IP="localhost" 35 | export LIGHTNING_IP="localhost" 36 | export APP_IP="127.0.0.1" 37 | export APP_CONFIG_DIR="$PWD/data/app" 38 | export APP_CORE_LIGHTNING_REST_CERT_DIR="$PWD/data/c-lightning-rest/certs" 39 | export LIGHTNING_WEBSOCKET_PORT=5001 40 | export LIGHTNING_GRPC_PORT=5002 41 | export LIGHTNING_REST_PORT=3001 42 | export SINGLE_SIGN_ON=false 43 | export LIGHTNING_PATH="/home/.lightning/l1-regtest" 44 | export COMMANDO_CONFIG="$PWD/.commando" 45 | echo "Local Environment Variables Set" 46 | fi 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cln-application", 3 | "version": "0.0.7", 4 | "description": "Core lightning application", 5 | "private": true, 6 | "license": "MIT", 7 | "scripts": { 8 | "frontend:dev": "npm run start -w cln-application-frontend", 9 | "frontend:build": "npm run build -w cln-application-frontend", 10 | "frontend:test": "npm run test -w cln-application-frontend", 11 | "frontend:lint": "npm run lint -w cln-application-frontend", 12 | "backend:build": "npm run build -w cln-application-backend", 13 | "backend:serve": "npm run serve -w cln-application-backend", 14 | "backend:watch": "npm run watch -w cln-application-backend", 15 | "backend:lint": "npm run lint -w cln-application-backend", 16 | "dev": "npm run backend:serve & npm run frontend:dev", 17 | "build": "npm run backend:build & npm run frontend:build", 18 | "start": "npm run serve -w cln-application-backend", 19 | "lint": "npm run backend:lint && npm run frontend:lint" 20 | }, 21 | "keywords": [ 22 | "cln application", 23 | "core lightning" 24 | ], 25 | "workspaces": [ 26 | "apps/*" 27 | ] 28 | } 29 | --------------------------------------------------------------------------------