Merge branch 'UX'

pull/260/head
Shahana Farooqui 4 years ago
commit 14151476cc

BIN
.DS_Store vendored

Binary file not shown.

@ -3,8 +3,8 @@ root = true
[*]
charset = utf-8
indent_style = tab
indent_size = 1
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

@ -1,7 +1,7 @@
## Ride The Lightning (RTL)
![](screenshots/RTL_Home.png)
<a href="https://snyk.io/test/github/ShahanaFarooqui/RTL"><img src="https://snyk.io/test/github/ShahanaFarooqui/RTL/badge.svg" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/ShahanaFarooqui/RTL" style="max-width:100%;"></a>
<a href="https://snyk.io/test/github/Ride-The-Lightning/RTL"><img src="https://snyk.io/test/github/Ride-The-Lightning/RTL/badge.svg" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/Ride-The-Lightning/RTL" style="max-width:100%;"></a>
[![license](https://img.shields.io/github/license/DAVFoundation/captain-n3m0.svg?style=flat-square)](https://github.com/DAVFoundation/captain-n3m0/blob/master/LICENSE)
### Stable Release: v0.5.4
@ -73,7 +73,7 @@ $ npm install
### <a name="prep"></a>Prep for Execution
RTL requires its own config file `RTL.conf`, to start the server and provide user authentication on the app.
*Advanced users can refer to [this page](docs/Multi-LND-Node-setup.md), for config settings required to manage multiple LND nodes*
*Advanced users can refer to [this page](docs/Multi-Node-setup.md), for config settings required to manage multiple nodes*
* Rename `sample-RTL.conf` file to `RTL.conf`.
* Locate the complete path of the readable macroon file (admin.macroon) on your node and the lnd.conf file.
@ -93,16 +93,15 @@ rtlCookiePath=C:\RTL\cookies\auth.cookie
logoutRedirectLink=/login
[Settings]
flgSidenavOpened=true
flgSidenavPinned=true
menu=Vertical
menuType=Regular
theme=dark-blue
satsToBTC=false
lndServerUrl=https://192.168.0.0:8080/v1
bitcoindConfigPath=
enableLogging=false
userPersona=OPERATOR
themeMode=DAY
themeColor=PURPLE
channelBackupPath=C:\Users\shaha\backup\node-0
bitcoindConfigPath=C:/Bitcoin/bitcoin.conf
enableLogging=true
port=3000
lndServerUrl=https://192.168.1.16:8080/v1
currencyUnit=USD
```
For details on all the configuration options refer to [this page](./docs/Application_configurations).

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

@ -0,0 +1 @@
<svg id="Layer_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" fill="#FFFFFF" xmlns="http://www.w3.org/2000/svg"><path d="m219.141 64.449c-3.875-1.623-8.677-.058-10.567 4.525-2.15 5.263 1.736 11.028 7.403 11.028 8.594 0 11.327-12.134 3.164-15.553z"/><path d="m434.971 179.113c1.365-94.939-77.738-179.113-178.991-179.113-102.004 0-180.37 85.092-178.992 179.111.06 4.406-2.873-.208 125.447 141.932-29.634 18.107-49.455 50.76-49.455 87.957 0 56.794 46.206 103 103 103s103-46.206 103-103c0-48.459-33.641-89.202-78.794-100.12l38.877-128.664c26.445-25.864 68.254-27.721 96.973-1.032l-103.793 114.97c-4.668 5.17-.918 13.361 5.936 13.361 2.185 0 4.361-.89 5.94-2.639 117.669-130.34 110.792-121.404 110.852-125.763zm-339.01.113c29.148-26.658 70.325-24.908 96.938 1l38.874 128.655c-4.995 1.208-9.848 2.78-14.531 4.688zm247.019 229.774c0 47.972-39.028 87-87 87s-87-39.028-87-87 39.028-87 87-87 87 39.028 87 87zm-95.262-102.668-38.671-127.982c27.723-24.935 67.878-23.912 93.874-.031l-38.68 128.012c-5.504-.438-10.931-.445-16.523.001zm64.32-141.331c-28.257-24.554-70.626-29.081-104.779-5.534-.159-18.163 1.899-36.181 6.169-53.749 1.044-4.293-1.591-8.62-5.884-9.663-4.294-1.044-8.619 1.591-9.663 5.884-4.499 18.508-6.713 37.475-6.632 56.594-30.242-19.978-67.736-19.347-97.188 1.637 8.696-75.247 68.846-135.043 144.25-143.209-7.39 9.761-14.017 20.063-19.757 30.807-2.082 3.897-.611 8.744 3.286 10.826 3.881 2.074 8.735.627 10.826-3.286 6.59-12.335 14.43-24.05 23.329-34.956 26.91 32.962 43.324 72.622 47.599 114.993.416 4.123 3.893 7.198 7.95 7.198 4.757 0 8.444-4.101 7.969-8.803-4.452-44.127-20.963-83.933-45.842-116.775 75.383 8.175 135.514 67.949 144.223 143.173-33.431-23.835-75.878-20.567-105.856 4.863z"/><path d="m253.98 373v-3c0-4.418-3.582-8-8-8s-8 3.582-8 8v3h-5c-4.418 0-8 3.582-8 8s3.582 8 8 8h1v40h-1c-4.418 0-8 3.582-8 8s3.582 8 8 8h5v3c0 4.418 3.582 8 8 8s8-3.582 8-8v-3h4v3c0 4.418 3.582 8 8 8s8-3.582 8-8v-4.933c13.278-5.979 17.193-22.9 7.958-34.067 9.242-11.175 5.308-28.094-7.958-34.067v-4.933c0-4.418-3.582-8-8-8s-8 3.582-8 8v3zm17 50c0 3.308-2.691 6-6 6h-15v-12h15c3.308 0 6 2.692 6 6zm-21-22v-12h15c3.309 0 6 2.692 6 6s-2.691 6-6 6z"/><path d="m182.98 409c0 40.252 32.748 73 73 73s73-32.748 73-73-32.748-73-73-73-73 32.748-73 73zm130 0c0 31.43-25.57 57-57 57s-57-25.57-57-57 25.57-57 57-57 57 25.57 57 57z"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@ -0,0 +1 @@
<svg height="512" viewBox="0 0 64 64" width="512" fill="#FFFFFF" xmlns="http://www.w3.org/2000/svg"><g id="Business-cryptocurrency-digital-money-3" data-name="Business-cryptocurrency-digital-money"><path d="m61 6h-58a1 1 0 0 0 -1 1v50a1 1 0 0 0 1 1h58a1 1 0 0 0 1-1v-50a1 1 0 0 0 -1-1zm-1 50h-56v-40h56zm0-42h-56v-6h56z"/><circle cx="8" cy="11" r="2"/><circle cx="14" cy="11" r="2"/><circle cx="20" cy="11" r="2"/><path d="m56 50v-23a1 1 0 0 0 -1-1h-6a1 1 0 0 0 -1 1v23h-4v-15a1 1 0 0 0 -1-1h-6a1 1 0 0 0 -1 1v15h-4v-9a1 1 0 0 0 -1-1h-6a1 1 0 0 0 -1 1v9h-6v-10.05a11 11 0 1 0 -2 0v11.05a1 1 0 0 0 1 1h41v-2zm-48-21a9 9 0 1 1 9 9 9.014 9.014 0 0 1 -9-9zm22 21h-4v-8h4zm12 0h-4v-14h4zm12 0h-4v-22h4z"/><path d="m22 27a3.01 3.01 0 0 0 -2-2.82v-1.18h-2v1h-2v-1h-2v1h-2v2h1v6h-1v2h2v1h2v-1h2v1h2v-1.18a3.01 3.01 0 0 0 2-2.82 2.974 2.974 0 0 0 -.78-2 2.974 2.974 0 0 0 .78-2zm-3 5h-4v-2h4a1 1 0 0 1 0 2zm0-4h-4v-2h4a1 1 0 0 1 0 2z"/><path d="m6 43h2v2h-2z"/><path d="m10 43h4v2h-4z"/><path d="m6 47h8v2h-8z"/></g></svg>

After

Width:  |  Height:  |  Size: 1013 B

@ -0,0 +1,2 @@
<svg height="512" viewBox="0 0 64 64" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m50 2h-32a5.006 5.006 0 0 0 -5 5v45h-4v5a5.006 5.006 0 0 0 5 5h28a5.006 5.006 0 0 0 5-5v-31h7a1 1 0 0 0 1-1v-18a5.006 5.006 0 0 0 -5-5zm-36 58a3 3 0 0 1 -3-3v-3h26v3a4.98 4.98 0 0 0 1 3zm31-53v50a3 3 0 0 1 -6 0v-5h-24v-45a3 3 0 0 1 3-3h28.026a4.948 4.948 0 0 0 -1.026 3zm8 17h-6v-17a3 3 0 0 1 6 0z"/><path d="m18 25h24v2h-24z"/><path d="m18 29h24v2h-24z"/><path d="m18 33h24v2h-24z"/><path d="m18 37h24v2h-24z"/><path d="m27 22h2v-2h2v2h2v-2a2.987 2.987 0 0 0 2.22-5 2.987 2.987 0 0 0 -2.22-5v-2h-2v2h-2v-2h-2v2h-2v2h2v6h-2v2h2zm2-10h4a1 1 0 0 1 0 2h-4zm0 4h4a1 1 0 0 1 0 2h-4z"/><path d="m18 41h24v2h-24z"/><path d="m18 11h2v-3h3v-2h-5z"/><path d="m41 11h2v-5h-5v2h3z"/><path d="m41 21h2v2h-2z"/><path d="m38 17h2v2h-2z"/><path d="m18 45h14v2h-14z"/></svg>
<div>Icons made by <a href="https://www.flaticon.com/authors/smalllikeart" title="smalllikeart">smalllikeart</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a></div>

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -0,0 +1 @@
<?xml version="1.0" encoding="iso-8859-1"?><!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"><g><g><path d="M400.388,175.787c-1.707-3.413-4.267-5.12-7.68-5.12H292.015L391.855,12.8c1.707-2.56,1.707-5.973,0-8.533 S387.588,0,384.175,0H247.642c-3.413,0-5.973,1.707-7.68,4.267l-128,256c-1.707,2.56-1.707,5.973,0,8.533 c1.707,2.56,5.12,4.267,7.68,4.267h87.893l-95.573,227.84c-1.707,3.413,0,7.68,3.413,10.24c0.853,0.853,2.56,0.853,4.267,0.853 c2.56,0,5.12-0.853,6.827-2.56l273.067-324.267C401.242,182.613,402.095,179.2,400.388,175.787z M149.508,454.827l78.507-187.733 c0.853-2.56,0.853-5.12-0.853-7.68c-1.707-1.707-4.267-3.413-6.827-3.413h-87.04L252.762,17.067h116.053L268.122,174.933 c-1.707,2.56-1.707,5.973,0,8.533s4.267,4.267,7.68,4.267h98.987L149.508,454.827z"/></g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1 @@
<svg height="512" viewBox="0 0 64 64" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m30 12a18 18 0 1 0 18 18 18.021 18.021 0 0 0 -18-18zm0 34a16 16 0 1 1 16-16 16.019 16.019 0 0 1 -16 16z"/><path d="m60.9 53.833-12.221-12.228a22.029 22.029 0 1 0 -7.079 7.074l12.233 12.221a5 5 0 0 0 7.067-7.067zm-50.9-23.833a20 20 0 1 1 20 20 20.023 20.023 0 0 1 -20-20zm49.483 29.483a3.067 3.067 0 0 1 -4.236 0l-11.967-11.958a22.2 22.2 0 0 0 4.245-4.245l11.958 11.967a3 3 0 0 1 0 4.236z"/><path d="m29 2h2v4h-2z"/><path d="m22.271 2.886h2v4h-2z" transform="matrix(.966 -.259 .259 .966 -.472 6.191)"/><path d="m16 5.483h2v4h-2z" transform="matrix(.866 -.5 .5 .866 -1.464 9.503)"/><path d="m10.615 9.615h2v4h-2z" transform="matrix(.707 -.707 .707 .707 -4.811 11.615)"/><path d="m6.483 15h2v4h-2z" transform="matrix(.5 -.866 .866 .5 -10.981 14.98)"/><path d="m3.886 21.271h2v4h-2z" transform="matrix(.259 -.966 .966 .259 -18.856 21.965)"/><path d="m2 29h4v2h-4z"/><path d="m2.886 35.729h4v2h-4z" transform="matrix(.966 -.259 .259 .966 -9.343 2.517)"/><path d="m5.483 42h4v2h-4z" transform="matrix(.866 -.5 .5 .866 -20.498 9.503)"/><path d="m9.615 47.385h4v2h-4z" transform="matrix(.707 -.707 .707 .707 -30.811 22.385)"/><path d="m15 51.516h4v2h-4z" transform="matrix(.5 -.866 .866 .5 -36.982 40.986)"/><path d="m21.271 54.114h4v2h-4z" transform="matrix(.259 -.966 .966 .259 -35.988 63.327)"/><path d="m29 54h2v4h-2z"/><path d="m35.73 53.114h2v4h-2z" transform="matrix(.966 -.259 .259 .966 -13.012 11.383)"/><path d="m54.114 34.729h2v4h-2z" transform="matrix(.259 -.966 .966 .259 5.367 80.454)"/><path d="m54 29h4v2h-4z"/><path d="m53.114 22.271h4v2h-4z" transform="matrix(.966 -.259 .259 .966 -4.146 15.064)"/><path d="m50.516 16h4.001v2h-4.001z" transform="matrix(.866 -.5 .5 .866 -1.465 28.531)"/><path d="m46.385 10.615h4v2h-4z" transform="matrix(.707 -.707 .707 .707 5.958 37.615)"/><path d="m41 6.483h4v2h-4z" transform="matrix(.5 -.866 .866 .5 15.022 40.983)"/><path d="m34.729 3.886h4v2h-4z" transform="matrix(.259 -.966 .966 .259 22.504 39.099)"/><path d="m30 19a11 11 0 1 0 11 11 11.013 11.013 0 0 0 -11-11zm0 20a9 9 0 1 1 9-9 9.011 9.011 0 0 1 -9 9z"/><path d="m32 25v-2h-2v2h-2v-2h-2v2a1 1 0 0 0 -1 1v8a1 1 0 0 0 1 1v2h2v-2h2v2h2v-2a2.987 2.987 0 0 0 2.22-5 2.987 2.987 0 0 0 -2.22-5zm1 3a1 1 0 0 1 -1 1h-5v-2h5a1 1 0 0 1 1 1zm-1 5h-5v-2h5a1 1 0 0 1 0 2z"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 479.504 479.504" style="enable-background:new 0 0 479.504 479.504;" xml:space="preserve">
<g>
<g>
<path d="M392.984,144.622v0.096c-52.497-85.63-164.47-112.489-250.1-59.993c-28.375,17.396-51.433,42.245-66.66,71.841
l-1.424-3.304c-36.8,15.888-61.248,36.2-70.76,58.752l14.712,6.216c6.688-15.864,24.12-31.312,49.6-44.248
c-14.202,36.39-16.272,76.394-5.904,114.056c-27.832-14.584-44.36-32.288-46.568-50.4L0,239.558
c3.136,25.84,25.032,48.968,63.32,66.896l2.864-6.264c33.456,94.761,137.397,144.458,232.158,111.002
c94.761-33.456,144.458-137.397,111.002-232.158c-1.507-4.267-3.172-8.477-4.992-12.62c24.232,10.696,42.352,23.8,51.672,37.776
l13.312-8.872C455.872,175.126,429.472,157.598,392.984,144.622z M351.072,118.83l-6.872,4.584l-26.248-5.256l-17.544-5.848
c-1.442-0.493-2.996-0.555-4.472-0.176l-21.736,5.432l-7.04-2.344l5.664-11.328h19.056c1.241,0.001,2.465-0.286,3.576-0.84
l19.8-9.904C328.278,100.088,340.322,108.724,351.072,118.83z M172.008,87.614l13.432,8.952c0.942,0.625,2.005,1.045,3.12,1.232
l26.768,4.464l-1.776,5.32l-14.208,4.744c-1.809,0.602-3.344,1.83-4.328,3.464l-16.968,28.288l-26.904,16.144l-40.4,5.768
c-3.939,0.565-6.865,3.94-6.864,7.92v18c-0.002,2.12,0.839,4.155,2.336,5.656l9.664,9.656v5.744L94.6,198.774l-8.56-25.664
C102.963,134.841,133.646,104.327,172.008,87.614z M77.456,197.91v0.04l2.832,8.488c0.562,1.688,1.671,3.141,3.152,4.128
l27.88,18.584l-5.104,5.104c-1.497,1.501-2.338,3.536-2.336,5.656v18c-0.001,1.241,0.286,2.465,0.84,3.576l12,24
c1.108,2.21,3.175,3.784,5.6,4.264l23.576,4.712v20.296c-1.808-0.328-3.68-0.592-5.448-0.944
c-16.601-3.152-32.944-7.532-48.896-13.104l-3.576,10C71.072,275.584,67.339,235.557,77.456,197.91z M237.88,405.91
c-61.496-0.06-117.926-34.092-146.672-88.456c15.106,5.027,30.526,9.057,46.16,12.064c2.768,0.544,5.672,0.984,8.512,1.48v16.912
c-0.001,1.016,0.192,2.024,0.568,2.968l12,30c0.874,2.184,2.665,3.872,4.896,4.616l18,6c2.435,0.814,5.114,0.409,7.2-1.088
c2.09-1.504,3.329-3.921,3.328-6.496v-26l20.8-15.6c1.377-1.111,2.336-2.657,2.72-4.384c3.88,0.112,7.744,0.264,11.656,0.296
h5.336c2.952,0,5.92,0,8.896-0.08c13.768-0.224,27.504-1.048,40.992-2.288c4.629,11.857,16.072,19.647,28.8,19.608h0.48
c16.03-0.316,29.173-12.807,30.304-28.8c4.72-1.04,9.44-2.096,13.992-3.272v0.456c-0.002,2.12,0.839,4.155,2.336,5.656l11.2,11.2
C338.103,381.739,289.48,405.849,237.88,405.91z M296.04,324.702c-0.147-8.296,6.46-15.141,14.756-15.287
c0.012,0,0.024,0,0.036-0.001h0.256c8.311,0.001,15.047,6.739,15.046,15.05c-0.001,8.311-6.739,15.047-15.05,15.046
C302.868,339.509,296.171,332.918,296.04,324.702z M378.68,327.406l-6.8-6.808v-1.664c4.88-1.496,9.6-3.096,14.24-4.744
C383.84,318.71,381.352,323.118,378.68,327.406z M394.752,293.726c-7.232,3.008-14.856,5.803-22.872,8.384v-2.2
c-0.001-1.242-0.292-2.466-0.848-3.576l-23.152-46.312V227.91c-0.001-2.676-1.34-5.174-3.568-6.656l-18-12
c-1.881-1.256-4.206-1.657-6.4-1.104l-24,6l3.88,15.52l20.568-5.144l11.496,7.664v19.72c-0.001,1.241,0.286,2.465,0.84,3.576
l23.16,46.312v5.032c-5.493,1.488-11.133,2.872-16.92,4.152c-1.565-3.279-3.693-6.257-6.288-8.8
c-5.945-5.686-13.879-8.818-22.104-8.728c-15.223,0.208-28.017,11.491-30.128,26.568c-12.968,1.192-26.176,1.976-39.408,2.192
c-2.896,0.048-5.776,0.08-8.64,0.08h-5.152c-3.832,0-7.592-0.176-11.36-0.288v-0.712l21.656-21.656l-11.32-11.312l-24,24
c-1.497,1.501-2.338,3.536-2.336,5.656v3.344c-0.272,0-0.552,0-0.8-0.048l-0.76,11.832l-19.2,14.4c-2.014,1.511-3.2,3.882-3.2,6.4
v18.896l-3.952-1.312l-10.048-25.12v-12.88c3.936,0.544,7.856,1.12,11.864,1.6l1.792-15.944
c-4.608-0.512-9.136-1.152-13.648-1.792V287.91c0.001-3.802-2.675-7.08-6.4-7.84l-26.168-5.24l-9.4-18.808v-12.8l19.808-19.808
l7.032,14.072c1.493,2.984,4.682,4.729,8,4.376l33.88-3.592l5.296,5.296l11.312-11.312l-8-8c-1.708-1.706-4.095-2.55-6.496-2.296
l-32.16,3.416l-9.52-19.04c-1.98-3.95-6.787-5.546-10.737-3.566c-0.763,0.382-1.459,0.884-2.063,1.486l-4.336,4.344v-4.688
c0-2.122-0.844-4.156-2.344-5.656l-9.656-9.656v-7.752l35.128-5.016c1.056-0.151,2.071-0.513,2.984-1.064l30-18
c1.125-0.673,2.067-1.613,2.744-2.736l16.48-27.464l15.2-5.072c2.391-0.79,4.266-2.665,5.056-5.056l6-18
c1.386-4.195-0.891-8.72-5.086-10.106c-0.387-0.128-0.784-0.226-1.186-0.294l-34.304-5.72l-2.2-1.464
c15.287-4.575,31.155-6.914,47.112-6.944c19.986,0.034,39.8,3.704,58.472,10.832l-6.336,3.2H267.88
c-3.032-0.001-5.805,1.712-7.16,4.424l-12,24c-1.973,3.953-0.367,8.758,3.586,10.73c0.335,0.167,0.682,0.311,1.038,0.43l18,6
c1.443,0.486,2.995,0.547,4.472,0.176l21.736-5.432l15.792,5.256c0.313,0.111,0.634,0.197,0.96,0.256l30,6
c0.527,0.106,1.063,0.16,1.6,0.16c1.578-0.001,3.12-0.469,4.432-1.344l12.088-8.056c5.627,6.38,10.754,13.184,15.336,20.352
l-26.504,8.8l-30.624-5.104l-10.096-15.08c-1.484-2.225-3.982-3.56-6.656-3.56h-36c-2.124-0.002-4.162,0.842-5.664,2.344l-18,18
l11.336,11.272l15.656-15.656h28.4l9.6,14.4c1.228,1.838,3.163,3.087,5.344,3.448l36,6c1.288,0.22,2.61,0.115,3.848-0.304
l31.416-10.472C406.28,204.868,409.509,251.396,394.752,293.726z"/>
</g>
</g>
<g>
<g>
<path d="M479.136,220.974l-15.888,1.872c0.163,1.416,0.249,2.839,0.256,4.264c0,19.152-15.936,38.72-44.856,55.104l7.88,13.92
c34.656-19.624,52.976-43.488,52.976-69.024v-0.864C479.47,224.484,479.347,222.725,479.136,220.974z"/>
</g>
</g>
<g>
<g>
<path d="M278.248,238.196c-1.062-3.07-2.742-5.89-4.936-8.285c8.869-9.683,8.209-24.723-1.475-33.592
c-2.396-2.194-5.215-3.874-8.285-4.936v-9.472h-16v8h-16v-8h-16v8h-8v16h8v48h-8v16h8v8h16v-8h16v8h16v-9.472
C275.961,264.145,282.541,250.605,278.248,238.196z M255.552,253.91h-24v-16h24c4.418,0,8,3.582,8,8S259.97,253.91,255.552,253.91
z M255.552,221.91h-24v-16h24c4.418,0,8,3.582,8,8S259.97,221.91,255.552,221.91z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>
<div>Icons made by <a href="https://www.flaticon.com/authors/turkkub" title="turkkub">turkkub</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a></div>

After

Width:  |  Height:  |  Size: 6.2 KiB

@ -0,0 +1 @@
<svg height="512" viewBox="0 0 64 64" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m31 16a16 16 0 1 0 16 16 16.021 16.021 0 0 0 -16-16zm0 30a14 14 0 1 1 14-14 14.015 14.015 0 0 1 -14 14z"/><path d="m31 13a19 19 0 1 0 19 19 19.017 19.017 0 0 0 -19-19zm0 36a17 17 0 1 1 17-17 17.024 17.024 0 0 1 -17 17z"/><path d="m36.97 32a4.985 4.985 0 0 0 -2.97-9v-2h-2v2h-3v-2h-2v2h-3v2h2v14h-2v2h3v2h2v-2h3v2h2v-2a4.985 4.985 0 0 0 2.97-9zm-8.97-7h6a3 3 0 0 1 0 6h-6zm6 14h-6v-6h6a3 3 0 0 1 0 6z"/><path d="m8 14a6 6 0 1 0 -6-6 6.006 6.006 0 0 0 6 6zm0-10a4 4 0 1 1 -4 4 4 4 0 0 1 4-4z"/><path d="m56 14a6 6 0 1 0 -6-6 6.006 6.006 0 0 0 6 6zm0-10a4 4 0 1 1 -4 4 4 4 0 0 1 4-4z"/><path d="m8 50a6 6 0 1 0 6 6 6.006 6.006 0 0 0 -6-6zm0 10a4 4 0 1 1 4-4 4 4 0 0 1 -4 4z"/><path d="m56 50a6 6 0 1 0 6 6 6.006 6.006 0 0 0 -6-6zm0 10a4 4 0 1 1 4-4 4 4 0 0 1 -4 4z"/><path d="m44.964 14.5h7.071v2h-7.071z" transform="matrix(.707 -.707 .707 .707 3.245 38.835)"/><path d="m47.5 44.964h2v7.071h-2z" transform="matrix(.707 -.707 .707 .707 -20.089 48.5)"/><path d="m12.172 48h5.657v2h-5.657z" transform="matrix(.707 -.707 .707 .707 -30.255 24.958)"/><path d="m14 12.172h2v5.657h-2z" transform="matrix(.707 -.707 .707 .707 -6.213 15)"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -0,0 +1 @@
<?xml version="1.0" encoding="iso-8859-1"?><!-- Generator: Adobe Illustrator 22.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 60 60" style="enable-background:new 0 0 60 60;" xml:space="preserve" width="512" height="512"><g id="_x32_5-Bitcoin_Settings"> <path d="M34,60h-8c-0.552,0-1-0.448-1-1v-3.5c0-1.151-0.735-2.171-1.786-2.48c-1.62-0.477-3.194-1.128-4.679-1.938 c-0.971-0.529-2.216-0.331-3.028,0.482l-2.477,2.477c-0.39,0.391-1.024,0.391-1.414,0l-5.657-5.657 c-0.391-0.391-0.391-1.023,0-1.414l2.477-2.477c0.812-0.813,1.011-2.058,0.482-3.028c-0.809-1.485-1.461-3.059-1.937-4.679 C6.671,35.734,5.651,35,4.5,35H1c-0.552,0-1-0.448-1-1v-8c0-0.552,0.448-1,1-1h3.5c1.151,0,2.171-0.734,2.48-1.786 c0.477-1.62,1.128-3.194,1.937-4.679c0.529-0.97,0.33-2.215-0.482-3.028l-2.477-2.477c-0.391-0.391-0.391-1.023,0-1.414 l5.657-5.657c0.39-0.391,1.024-0.391,1.414,0l2.477,2.477c0.813,0.812,2.058,1.01,3.028,0.482c1.485-0.809,3.059-1.461,4.679-1.938 C24.265,6.671,25,5.651,25,4.5V1c0-0.552,0.448-1,1-1h8c0.552,0,1,0.448,1,1v3.5c0,1.151,0.735,2.171,1.786,2.48 c1.62,0.477,3.194,1.128,4.679,1.938c0.97,0.527,2.215,0.33,3.028-0.482l2.477-2.477c0.39-0.391,1.024-0.391,1.414,0l5.657,5.657 c0.391,0.391,0.391,1.023,0,1.414l-2.477,2.477c-0.813,0.813-1.011,2.058-0.482,3.028c0.809,1.485,1.461,3.059,1.937,4.679 C53.329,24.266,54.349,25,55.5,25H59c0.552,0,1,0.448,1,1v8c0,0.552-0.448,1-1,1h-3.5c-1.151,0-2.171,0.734-2.48,1.786 c-0.477,1.62-1.128,3.194-1.937,4.679c-0.529,0.97-0.33,2.215,0.482,3.028l2.477,2.477c0.391,0.391,0.391,1.023,0,1.414 l-5.657,5.657c-0.39,0.391-1.024,0.391-1.414,0l-2.477-2.477c-0.813-0.813-2.058-1.01-3.028-0.482 c-1.485,0.809-3.059,1.461-4.679,1.938C35.735,53.329,35,54.349,35,55.5V59C35,59.552,34.552,60,34,60z M27,58h6v-2.5 c0-2.032,1.325-3.841,3.221-4.399c1.485-0.437,2.927-1.033,4.287-1.774c1.743-0.95,3.964-0.611,5.399,0.824l1.77,1.77l4.243-4.243 l-1.77-1.77c-1.435-1.435-1.774-3.656-0.825-5.399c0.741-1.36,1.338-2.803,1.775-4.287C51.659,34.325,53.468,33,55.5,33H58v-6h-2.5 c-2.032,0-3.841-1.325-4.399-3.222c-0.437-1.484-1.034-2.926-1.775-4.287c-0.95-1.744-0.611-3.964,0.825-5.399l1.77-1.77 L47.678,8.08l-1.77,1.77c-1.435,1.436-3.655,1.775-5.399,0.824c-1.36-0.741-2.802-1.338-4.287-1.774C34.325,8.341,33,6.532,33,4.5 V2h-6v2.5c0,2.032-1.325,3.841-3.221,4.399c-1.485,0.437-2.927,1.033-4.287,1.774c-1.743,0.95-3.964,0.611-5.399-0.824l-1.77-1.77 L8.08,12.322l1.77,1.77c1.436,1.436,1.775,3.656,0.825,5.399c-0.741,1.36-1.338,2.803-1.775,4.287C8.341,25.675,6.532,27,4.5,27H2 v6h2.5c2.032,0,3.841,1.325,4.399,3.222c0.437,1.484,1.034,2.926,1.775,4.287c0.95,1.744,0.611,3.964-0.825,5.399l-1.77,1.77 l4.243,4.243l1.77-1.77c1.436-1.435,3.656-1.773,5.399-0.824c1.36,0.741,2.802,1.338,4.287,1.774C25.675,51.659,27,53.468,27,55.5 V58z"/> <path d="M30,48c-9.925,0-18-8.075-18-18s8.075-18,18-18s18,8.075,18,18S39.925,48,30,48z M30,14c-8.823,0-16,7.178-16,16 s7.177,16,16,16s16-7.178,16-16S38.823,14,30,14z"/> <path d="M17,31c-0.552,0-1-0.448-1-1c0-7.72,6.28-14,14-14c0.552,0,1,0.448,1,1s-0.448,1-1,1c-6.617,0-12,5.383-12,12 C18,30.552,17.552,31,17,31z"/> <path d="M30,44c-0.552,0-1-0.448-1-1s0.448-1,1-1c6.617,0,12-5.383,12-12c0-0.552,0.448-1,1-1s1,0.448,1,1C44,37.72,37.72,44,30,44 z"/> <path d="M32,31h-6c-0.552,0-1-0.448-1-1v-6c0-0.552,0.448-1,1-1h6c2.206,0,4,1.794,4,4S34.206,31,32,31z M27,29h5 c1.103,0,2-0.897,2-2s-0.897-2-2-2h-5V29z"/> <path d="M32,37h-6c-0.552,0-1-0.448-1-1v-6c0-0.552,0.448-1,1-1h6c2.206,0,4,1.794,4,4S34.206,37,32,37z M27,35h5 c1.103,0,2-0.897,2-2s-0.897-2-2-2h-5V35z"/> <path d="M28,25c-0.552,0-1-0.448-1-1v-3c0-0.552,0.448-1,1-1s1,0.448,1,1v3C29,24.552,28.552,25,28,25z"/> <path d="M32,25c-0.552,0-1-0.448-1-1v-3c0-0.552,0.448-1,1-1s1,0.448,1,1v3C33,24.552,32.552,25,32,25z"/> <path d="M28,40c-0.552,0-1-0.448-1-1v-3c0-0.552,0.448-1,1-1s1,0.448,1,1v3C29,39.552,28.552,40,28,40z"/> <path d="M32,40c-0.552,0-1-0.448-1-1v-3c0-0.552,0.448-1,1-1s1,0.448,1,1v3C33,39.552,32.552,40,32,40z"/> <path d="M26,25h-2c-0.552,0-1-0.448-1-1s0.448-1,1-1h2c0.552,0,1,0.448,1,1S26.552,25,26,25z"/> <path d="M26,37h-2c-0.552,0-1-0.448-1-1s0.448-1,1-1h2c0.552,0,1,0.448,1,1S26.552,37,26,37z"/></g></svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -0,0 +1 @@
<svg height="512" viewBox="0 0 64 64" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m32 24a16 16 0 1 0 16 16 16.021 16.021 0 0 0 -16-16zm0 30a14 14 0 1 1 14-14 14.015 14.015 0 0 1 -14 14z"/><path d="m60 22.03v-2.03a5 5 0 0 0 -5-5h-2.96a8.962 8.962 0 0 0 -11.65-4.24 8.986 8.986 0 0 0 -16.41-.82 8.979 8.979 0 0 0 -12.47 5.06h-4.51a5 5 0 0 0 -5 5v34a5 5 0 0 0 5 5h50a5 5 0 0 0 5-5v-28a4.952 4.952 0 0 0 -2-3.97zm-7.23-5.03h2.23a3.009 3.009 0 0 1 3 3v1.1a5.468 5.468 0 0 0 -1-.1h-4.23a8.811 8.811 0 0 0 0-4zm-11.87-4.26a6.967 6.967 0 0 1 9.59 3.63 7.033 7.033 0 0 1 .51 2.63 7.114 7.114 0 0 1 -.29 2h-13.07a8.96 8.96 0 0 0 3.36-7 8.8 8.8 0 0 0 -.1-1.26zm-8.9-5.74a7 7 0 1 1 -7 7 7.008 7.008 0 0 1 7-7zm-18.78 9.25a7 7 0 0 1 6.78-5.25 6.856 6.856 0 0 1 3.27.84 8.931 8.931 0 0 0 3.09 9.16h-12.67a6.939 6.939 0 0 1 -.47-4.75zm-9.22 3.75a3.009 3.009 0 0 1 3-3h4.06c-.03.33-.06.67-.06 1a8.981 8.981 0 0 0 .53 3h-7.53zm56 24h-7a2.006 2.006 0 0 1 -2-2v-6a2.006 2.006 0 0 1 2-2h7zm0-12h-7a4 4 0 0 0 -4 4v6a4 4 0 0 0 4 4h7v8a3.009 3.009 0 0 1 -3 3h-50a3.009 3.009 0 0 1 -3-3v-31h53a3.009 3.009 0 0 1 3 3z"/><path d="m37.97 40a4.985 4.985 0 0 0 -2.97-9v-2h-2v2h-3v-2h-2v2h-3v2h2v14h-2v2h3v2h2v-2h3v2h2v-2a4.985 4.985 0 0 0 2.97-9zm-8.97-7h6a3 3 0 0 1 0 6h-6zm6 14h-6v-6h6a3 3 0 0 1 0 6z"/><path d="m56 42a3 3 0 1 0 -3-3 3 3 0 0 0 3 3zm0-4a1 1 0 1 1 -1 1 1 1 0 0 1 1-1z"/><path d="m6 27v26a3 3 0 0 0 3 3h3v-2h-3a1 1 0 0 1 -1-1v-26a1 1 0 0 1 1-1h3v-2h-3a3 3 0 0 0 -3 3z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.5 KiB

@ -0,0 +1 @@
<?xml version="1.0" encoding="iso-8859-1"?><svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"><g> <g> <path d="M482,274.58V255c0-24.485-11.797-46.264-30.002-59.961c0-0.013,0.002-0.026,0.002-0.039v-30c0-41.355-33.645-75-75-75h-15 V45c0-24.813-20.187-45-45-45H135c-24.813,0-45,20.187-45,45v45H60c-33.084,0-60,26.916-60,60v287c0,41.355,33.645,75,75,75h332 c41.355,0,75-33.645,75-75v-17.58c17.459-6.192,30-22.865,30-42.42v-60C512,297.445,499.459,280.772,482,274.58z M362,120h15 c24.813,0,45,20.187,45,45v15c-6.569,0-52.366,0-60,0V120z M240,30h77c8.271,0,15,6.729,15,15v135h-92V30z M180,30h30v150h-30V30z M120,45c0-8.271,6.729-15,15-15h15v150h-30V45z M60,120h30v60H60c-16.542,0-30-13.458-30-30S43.458,120,60,120z M452,437 c0,24.813-20.187,45-45,45H75c-24.813,0-45-20.187-45-45V201.928C38.833,207.051,49.075,210,60,210c17.464,0,342.315,0,347,0 c24.813,0,45,20.187,45,45v17h-45c-41.355,0-75,33.645-75,75c0,41.355,33.645,75,75,75h45V437z M482,377c0,8.271-6.729,15-15,15 h-60c-24.813,0-45-20.187-45-45s20.187-45,45-45h60c8.271,0,15,6.729,15,15V377z"/> </g></g><g> <g> <circle cx="407" cy="347" r="15"/> </g></g><g> <g> <path d="M300,317c0-24.813-20.187-45-45-45h-15v-15c0-8.284-6.716-15-15-15s-15,6.716-15,15v15h-45c-8.284,0-15,6.716-15,15 s6.716,15,15,15h15v90h-15c-8.284,0-15,6.716-15,15s6.716,15,15,15h45v15c0,8.284,6.716,15,15,15s15-6.716,15-15v-15h15 c24.813,0,45-20.187,45-45c0-11.517-4.354-22.032-11.495-30C295.646,339.032,300,328.517,300,317z M255,392h-45v-30h45 c8.271,0,15,6.729,15,15S263.271,392,255,392z M255,332h-45v-30h45c8.271,0,15,6.729,15,15S263.271,332,255,332z"/> </g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 499.312 499.312" fill="#FFFFFF" style="enable-background:new 0 0 499.312 499.312;" xml:space="preserve">
<g>
<g>
<path d="M499.312,35.312L464,0l-83.28,83.28C356.6,57.904,323.088,43.312,288,43.312c-35.296,0-68.32,14.376-92.424,39.624
C182.616,63.872,160.752,51.312,136,51.312H72c-39.704,0-72,32.304-72,72v96c0,13.232,10.768,24,24,24h40v68.28l19.448,12.96
l-22.76,22.76l24,24l-24,24l24,24L64,440v13.528l27.472,45.784h25.064L144,453.528V243.312h38.232
c23.92,35.12,63.096,56,105.768,56c70.576,0,128-57.424,128-128c0-15.36-2.752-30.352-8.064-44.624L499.312,35.312z M128,449.096
l-20.528,34.216h-6.936L80,449.096v-2.472l27.312-27.312l-24-24l24-24l-24-24l25.24-25.24L80,303.032v-59.72h48V449.096z
M192,219.312c0,4.408-3.584,8-8,8H24c-4.416,0-8-3.592-8-8v-96c0-30.88,25.128-56,56-56h64c30.872,0,56,25.12,56,56V219.312z
M464,22.624l12.688,12.688L288,224l-44.688-44.688L256,166.624l32,32L464,22.624z M288,283.312
c-35.648,0-68.512-16.64-89.632-44.896c5.816-4.384,9.632-11.28,9.632-19.104v-96c0-8.848-1.68-17.296-4.616-25.136
C224.72,73.496,255.24,59.312,288,59.312c30.832,0,60.296,12.872,81.424,35.264L288,176l-32-32l-35.312,35.312L288,246.624
L395.304,139.32c3.08,10.344,4.696,21.056,4.696,31.992C400,233.072,349.76,283.312,288,283.312z"/>
</g>
</g>
<g>
<g>
<path d="M128,99.312H80c-17.648,0-32,14.352-32,32s14.352,32,32,32h48c17.648,0,32-14.352,32-32S145.648,99.312,128,99.312z
M128,147.312H80c-8.824,0-16-7.176-16-16c0-8.824,7.176-16,16-16h48c8.824,0,16,7.176,16,16S136.824,147.312,128,147.312z"/>
</g>
</g>
<g>
<g>
<rect x="160" y="195.312" width="16" height="16"/>
</g>
</g>
<g>
<g>
<rect x="64" y="195.312" width="80" height="16"/>
</g>
</g>
<g>
<g>
<rect x="32" y="195.312" width="16" height="16"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@ -0,0 +1,2 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="512" height="512" fill="#FFFFFF"><g id="outline"><path d="M43,17A11,11,0,1,0,32,28,11.013,11.013,0,0,0,43,17ZM23,17a9,9,0,1,1,9,9A9.01,9.01,0,0,1,23,17Z"/><path d="M33,24h2V22a2.987,2.987,0,0,0,2.22-5A2.987,2.987,0,0,0,35,12V10H33v2H31V10H29v2H27v2h2v6H27v2h2v2h2V22h2Zm3-9a1,1,0,0,1-1,1H31V14h4A1,1,0,0,1,36,15Zm-5,3h4a1,1,0,0,1,0,2H31Z"/><path d="M26,52v4a1,1,0,0,0,.293.707l5,5a1,1,0,0,0,1.414,0l5-5A1,1,0,0,0,38,56V34h2a1,1,0,0,0,.707-1.707L38.74,30.326A14.815,14.815,0,0,0,47,17a15,15,0,0,0-30,0,14.815,14.815,0,0,0,8.26,13.326l-1.967,1.967A1,1,0,0,0,24,34h3v.586l-1.707,1.707A1,1,0,0,0,25,37v3a1,1,0,0,0,.629.929l3.348,1.339L26.4,44.2a1,1,0,0,0-.4.8v3a1,1,0,0,0,.445.832L28.2,50l-1.752,1.168A1,1,0,0,0,26,52Zm2,.535,2.555-1.7a1,1,0,0,0,0-1.664L28,47.465V45.5l3.6-2.7a1,1,0,0,0-.229-1.729L27,39.323V37.414l1.707-1.707A1,1,0,0,0,29,35V33a1,1,0,0,0-1-1H26.414l1.293-1.293a1,1,0,0,0-.35-1.641A12.86,12.86,0,0,1,19,17a13,13,0,0,1,26,0,12.86,12.86,0,0,1-8.357,12.066,1,1,0,0,0-.35,1.641L37.586,32H37a1,1,0,0,0-1,1V55.586l-3,3V45H31V58.586l-3-3Z"/><rect x="31" y="31" width="2" height="8"/><path d="M17,26a1,1,0,0,0-2,0,3,3,0,0,1-3,3,1,1,0,0,0,0,2,3,3,0,0,1,3,3,1,1,0,0,0,2,0,3,3,0,0,1,3-3,1,1,0,0,0,0-2A3,3,0,0,1,17,26Zm-1,5.031A4.988,4.988,0,0,0,14.969,30,4.988,4.988,0,0,0,16,28.969,4.988,4.988,0,0,0,17.031,30,4.988,4.988,0,0,0,16,31.031Z"/><path d="M41,39a3,3,0,0,1,3,3,1,1,0,0,0,2,0,3,3,0,0,1,3-3,1,1,0,0,0,0-2,3,3,0,0,1-3-3,1,1,0,0,0-2,0,3,3,0,0,1-3,3,1,1,0,0,0,0,2Zm4-2.031A4.988,4.988,0,0,0,46.031,38,4.988,4.988,0,0,0,45,39.031,4.988,4.988,0,0,0,43.969,38,4.988,4.988,0,0,0,45,36.969Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>RTL</title>
<base href="/rtl/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="apple-touch-icon" sizes="180x180" href="assets/images/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="assets/images/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="assets/images/favicon/favicon-16x16.png">
<link rel="manifest" href="assets/images/favicon/site.webmanifest">
<link rel="stylesheet" href="styles.68c5c110c36114003e31.css"></head>
<body>
<rtl-app></rtl-app>
<script src="runtime.49783ae5fbe9bffd3b65.js" defer></script><script src="polyfills-es5.b8e32dec482ae69710a2.js" nomodule defer></script><script src="polyfills.ebf9033c33aa4a5af12a.js" defer></script><script src="main.0c6a645ab1638ee36ab2.js" defer></script></body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
!function(e){function r(r){for(var n,a,i=r[0],c=r[1],l=r[2],p=0,s=[];p<i.length;p++)a=i[p],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={0:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+""+({}[e]||e)+"."+{1:"454a80537ee5d2127464",6:"acee564c946fe91034bb",7:"eb1dae2453682026ccc5"}[e]+".js"}(e);var c=new Error;u=function(r){i.onerror=i.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,(function(r){return e[r]}).bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="",a.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var l=0;l<i.length;l++)r(i[l]);var f=c;t()}([]);

File diff suppressed because one or more lines are too long

@ -4,10 +4,12 @@ const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const common = require("./common");
const app = express();
const baseHref = "/rtl/";
const apiRoot = baseHref + "api/";
const apiLNDRoot = baseHref + "api/lnd/";
const apiCLRoot = baseHref + "api/cl/";
const apiLoopRoot = baseHref + "api/loop";
const authenticateRoutes = require("./routes/authenticate");
const RTLConfRoutes = require("./routes/RTLConf");
@ -36,6 +38,8 @@ const paymentsCLRoutes = require("./routes/c-lightning/payments");
const peersCLRoutes = require("./routes/c-lightning/peers");
const networkCLRoutes = require("./routes/c-lightning/network");
const loopRoutes = require('./routes/loopd/loop');
app.use(cookieParser(common.secret_key));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
@ -83,6 +87,8 @@ app.use(apiCLRoot + "payments", paymentsCLRoutes);
app.use(apiCLRoot + "peers", peersCLRoutes);
app.use(apiCLRoot + "network", networkCLRoutes);
app.use(apiLoopRoot, loopRoutes);
app.use((req, res, next) => {
res.sendFile(path.join(__dirname, "angular", "index.html"));
});

@ -9,6 +9,7 @@ common.node_auth_type = 'DEFAULT';
common.rtl_pass = '';
common.rtl_sso = 0;
common.port = 3000;
common.currency_unit = 'USD';
common.rtl_cookie_path = '';
common.logout_redirect_link = '/login';
common.cookie = '';
@ -25,6 +26,9 @@ common.getOptions = () => {
};
common.updateSelectedNodeOptions = () => {
if (!common.selectedNode) {
common.selectedNode = {};
}
common.selectedNode.options = {
url: '',
rejectUnauthorized: false,
@ -89,19 +93,19 @@ common.convertToBTC = (num) => {
};
common.convertTimestampToDate = (num) => {
return new Date(+num * 1000).toUTCString();
return new Date(+num * 1000).toUTCString().substring(5, 22).replace(' ', '/').replace(' ', '/').toUpperCase();
};
common.sortAscByKey = (array, key) => {
return array.sort(function (a, b) {
var x = a[key]; var y = b[key];
var x = +a[key]; var y = +b[key];
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
});
}
common.sortDescByKey = (array, key) => {
const temp = array.sort(function (a, b) {
var x = a[key]; var y = b[key];
var x = +a[key]; var y = +b[key];
return ((x > y) ? -1 : ((x < y) ? 1 : 0));
});
return temp;

@ -22,44 +22,59 @@ connect.setDefaultConfig = () => {
case 'win32':
macaroonPath = homeDir + '\\AppData\\Local\\Lnd\\data\\chain\\bitcoin\\mainnet';
configPath = homeDir + '\\AppData\\Local\\Lnd\\lnd.conf';
channelBackupPath = homeDir + '\\backup\\node-1';
break;
case 'darwin':
macaroonPath = homeDir + '/Library/Application Support/Lnd/data/chain/bitcoin/mainnet';
configPath = homeDir + '/Library/Application Support/Lnd/lnd.conf';
channelBackupPath = homeDir + '/backup/node-1';
break;
case 'linux':
macaroonPath = homeDir + '/.lnd/data/chain/bitcoin/mainnet';
configPath = homeDir + '/.lnd/lnd.conf';
channelBackupPath = homeDir + '/backup/node-1';
break;
default:
macaroonPath = '';
configPath = '';
channelBackupPath = '';
break;
}
return {
Authentication: {
macaroonPath: macaroonPath,
nodeAuthType: 'DEFAULT',
configPath: configPath,
rtlPass: ''
},
Settings: {
flgSidenavOpened: true,
flgSidenavPinned: true,
menu: 'Vertical',
menuType: 'Regular',
theme: 'dark-blue',
satsToBTC: false,
channelBackupPath: homeDir + common.path_separator + 'backup' + common.path_separator + 'node-0',
lnServerUrl: 'https://localhost:8080/v1',
enableLogging: false,
port: 3000
},
port: "3000",
defaultNodeIndex: 1,
multiPass: "password",
SSO: {
rtlSSO: 0,
rtlCookiePath: '',
logoutRedirectLink: '/login'
}
rtlCookiePath: "",
logoutRedirectLink: ""
},
nodes: [
{
index: 1,
lnNode: "LND Testnet",
lnImplementation: "LND",
Authentication: {
macaroonPath: macaroonPath,
configPath: configPath,
},
Settings: {
userPersona: 'OPERATOR',
flgSidenavOpened: true,
flgSidenavPinned: true,
menu: "VERTICAL",
menuType: "REGULAR",
fontSize: "MEDIUM",
themeMode: "DAY",
themeColor: "PURPLE",
satsToBTC: false,
channelBackupPath: channelBackupPath,
enableLogging: "true",
lndServerUrl: "https://localhost:8080/v1",
currencyUnit: "USD"
}
}
]
};
}
@ -257,6 +272,14 @@ connect.validateSingleNodeConfig = (config) => {
}
}
if (undefined !== process.env.CURRENCY_UNIT) {
common.nodes[0].currency_unit = process.env.CURRENCY_UNIT;
} else if (undefined !== config.Settings.currencyUnit) {
common.nodes[0].currency_unit = config.Settings.currencyUnit;
} else {
common.nodes[0].currency_unit = 'USD';
}
if (undefined !== process.env.PORT) {
common.port = connect.normalizePort(process.env.PORT);
} else if (undefined !== config.Settings.port) {
@ -305,6 +328,8 @@ connect.validateMultiNodeConfig = (config) => {
common.nodes[idx].index = node.index;
common.nodes[idx].ln_node = node.lnNode;
common.nodes[idx].ln_implementation = node.lnImplementation;
common.nodes[idx].currency_unit = node.Settings.currencyUnit ? node.Settings.currencyUnit : 'USD';
if (undefined !== node.Authentication && undefined !== node.Authentication.lndConfigPath) {
common.nodes[idx].config_path = node.Authentication.lndConfigPath;
} else if (undefined !== node.Authentication && undefined !== node.Authentication.configPath) {
@ -445,32 +470,21 @@ connect.logEnvVariables = () => {
logger.info({fileName: 'Config Setup Variable', msg: 'DEFAULT_NODE_INDEX: ' + common.selectedNode.index});
logger.info({fileName: 'Config Setup Variable', msg: 'NODE_SETUP: MULTI', node});
logger.info({fileName: 'Config Setup Variable', msg: 'RTL_SSO: ' + common.rtl_sso, node});
logger.info({fileName: 'Config Setup Variable', msg: 'RTL_COOKIE_PATH: ' + common.rtl_cookie_path, node});
logger.info({fileName: 'Config Setup Variable', msg: 'LOGOUT_REDIRECT_LINK: ' + common.logout_redirect_link + '\r\n', node});
logger.info({fileName: 'Config Setup Variable', msg: 'INDEX: ' + node.index, node});
logger.info({fileName: 'Config Setup Variable', msg: 'LN NODE: ' + node.ln_node, node});
logger.info({fileName: 'Config Setup Variable', msg: 'LN IMPLEMENTATION: ' + node.ln_implementation, node});
logger.info({fileName: 'Config Setup Variable', msg: 'PORT: ' + common.port, node});
logger.info({fileName: 'Config Setup Variable', msg: 'MACAROON_PATH: ' + node.macaroon_path, node});
logger.info({fileName: 'Config Setup Variable', msg: 'CURRENCY_UNIT: ' + common.currency_unit, node});
logger.info({fileName: 'Config Setup Variable', msg: 'LND_SERVER_URL: ' + node.ln_server_url, node});
logger.info({fileName: 'Config Setup Variable', msg: 'RTL_CONFIG_PATH: ' + node.rtl_conf_file_path, node});
logger.info({fileName: 'Config Setup Variable', msg: 'CONFIG_PATH: ' + node.config_path, node});
logger.info({fileName: 'Config Setup Variable', msg: 'BITCOIND_CONFIG_PATH: ' + node.bitcoind_config_path, node});
logger.info({fileName: 'Config Setup Variable', msg: 'CHANNEL_BACKUP_PATH: ' + node.channel_backup_path, node});
});
} else {
if (!common.nodes[0].enable_logging) { return; }
logger.info({fileName: 'Config Setup Variable', msg: 'NODE_SETUP: SINGLE'});
logger.info({fileName: 'Config Setup Variable', msg: 'PORT: ' + common.port});
logger.info({fileName: 'Config Setup Variable', msg: 'CURRENCY_UNIT: ' + common.currency_unit});
logger.info({fileName: 'Config Setup Variable', msg: 'LND_SERVER_URL: ' + common.nodes[0].ln_server_url});
logger.info({fileName: 'Config Setup Variable', msg: 'MACAROON_PATH: ' + common.nodes[0].macaroon_path});
logger.info({fileName: 'Config Setup Variable', msg: 'NODE_AUTH_TYPE: ' + common.node_auth_type});
logger.info({fileName: 'Config Setup Variable', msg: 'CONFIG_PATH: ' + common.nodes[0].config_path});
logger.info({fileName: 'Config Setup Variable', msg: 'RTL_CONFIG_PATH: ' + common.rtl_conf_file_path});
logger.info({fileName: 'Config Setup Variable', msg: 'BITCOIND_CONFIG_PATH: ' + common.nodes[0].bitcoind_config_path});
logger.info({fileName: 'Config Setup Variable', msg: 'CHANNEL_BACKUP_PATH: ' + common.nodes[0].channel_backup_path});
logger.info({fileName: 'Config Setup Variable', msg: 'RTL_SSO: ' + common.rtl_sso});
logger.info({fileName: 'Config Setup Variable', msg: 'RTL_COOKIE_PATH: ' + common.rtl_cookie_path});
logger.info({fileName: 'Config Setup Variable', msg: 'LOGOUT_REDIRECT_LINK: ' + common.logout_redirect_link});
}
}

@ -2,11 +2,13 @@ var ini = require('ini');
var fs = require('fs');
var logger = require('./logger');
var common = require('../common');
var request = require('request-promise');
var options = {};
exports.updateSelectedNode = (req, res, next) => {
const selNodeIndex = req.body.selNodeIndex;
common.selectedNode = common.findNode(selNodeIndex);
const responseVal = common.selectedNode.ln_node ? common.selectedNode.ln_node : common.selectedNode.ln_server_url;
const responseVal = !common.selectedNode ? '' : (common.selectedNode.ln_node ? common.selectedNode.ln_node : common.selectedNode.ln_server_url);
logger.info({fileName: 'RTLConf', msg: 'Selected Node Updated To: ' + JSON.stringify(responseVal)});
res.status(200).json({status: 'Selected Node Updated To: ' + JSON.stringify(responseVal) + '!'});
};
@ -31,7 +33,15 @@ exports.getRTLConfig = (req, res, next) => {
bitcoindConfigPath: common.nodes[0].bitcoind_config_path
};
jsonConfig.Settings.channelBackupPath = (undefined !== jsonConfig.Settings.channelBackupPath) ? jsonConfig.Settings.channelBackupPath : common.nodes[0].channel_backup_path;
res.status(200).json({ selectedNodeIndex: common.selectedNode.index, sso: sso, nodes: [{
jsonConfig.Settings.flgSidenavOpened = (undefined !== jsonConfig.Settings.flgSidenavOpened) ? jsonConfig.Settings.flgSidenavOpened : true;
jsonConfig.Settings.flgSidenavPinned = (undefined !== jsonConfig.Settings.flgSidenavPinned) ? jsonConfig.Settings.flgSidenavPinned : true;
jsonConfig.Settings.menu = (undefined !== jsonConfig.Settings.menu) ? jsonConfig.Settings.menu : 'VERTICAL';
jsonConfig.Settings.menuType = (undefined !== jsonConfig.Settings.menuType) ? jsonConfig.Settings.menuType : 'REGULAR';
jsonConfig.Settings.fontSize = (undefined !== jsonConfig.Settings.fontSize) ? jsonConfig.Settings.fontSize : 'MEDIUM';
jsonConfig.Settings.themeMode = (undefined !== jsonConfig.Settings.themeMode) ? jsonConfig.Settings.themeMode : 'DAY';
jsonConfig.Settings.themeColor = (undefined !== jsonConfig.Settings.themeColor) ? jsonConfig.Settings.themeColor : 'PURPLE';
jsonConfig.Settings.satsToBTC = (undefined !== jsonConfig.Settings.satsToBTC) ? jsonConfig.Settings.satsToBTC : false;
res.status(200).json({ defaultNodeIndex: 0, selectedNodeIndex: common.selectedNode.index, sso: sso, nodes: [{
index: common.nodes[0].index,
lnNode: 'SingleNode',
lnImplementation: '',
@ -46,7 +56,7 @@ exports.getRTLConfig = (req, res, next) => {
if (err) {
if (err.code === 'ENOENT') {
logger.error({fileName: 'RTLConf', lineNum: 46, msg: 'Multi Node Config does not exist!'});
res.status(200).json({ selectedNodeIndex: {}, sso: {}, nodes: [] });
res.status(200).json({ defaultNodeIndex: 0, selectedNodeIndex: 0, sso: {}, nodes: [] });
} else {
logger.error({fileName: 'RTLConf', lineNum: 49, msg: 'Getting Multi Node Config Failed!'});
res.status(500).json({
@ -69,11 +79,18 @@ exports.getRTLConfig = (req, res, next) => {
} else {
authentication.configPath = '';
}
if(node.Settings.bitcoindConfigPath) {
if(node.Settings.bitcoindConfigPath) {
authentication.bitcoindConfigPath = node.Settings.bitcoindConfigPath;
}
node.Settings.channelBackupPath = (undefined !== node.Settings.channelBackupPath) ? node.Settings.channelBackupPath : common.nodes[i].channel_backup_path;
node.Settings.flgSidenavOpened = (undefined !== node.Settings.flgSidenavOpened) ? node.Settings.flgSidenavOpened : true;
node.Settings.flgSidenavPinned = (undefined !== node.Settings.flgSidenavPinned) ? node.Settings.flgSidenavPinned : true;
node.Settings.menu = (undefined !== node.Settings.menu) ? node.Settings.menu : 'VERTICAL';
node.Settings.menuType = (undefined !== node.Settings.menuType) ? node.Settings.menuType : 'REGULAR';
node.Settings.fontSize = (undefined !== node.Settings.fontSize) ? node.Settings.fontSize : 'MEDIUM';
node.Settings.themeMode = (undefined !== node.Settings.themeMode) ? node.Settings.themeMode : 'DAY';
node.Settings.themeColor = (undefined !== node.Settings.themeColor) ? node.Settings.themeColor : 'PURPLE';
node.Settings.satsToBTC = (undefined !== node.Settings.satsToBTC) ? node.Settings.satsToBTC : false;
nodesArr.push({
index: node.index,
lnNode: node.lnNode,
@ -82,7 +99,7 @@ exports.getRTLConfig = (req, res, next) => {
authentication: authentication})
});
}
res.status(200).json({ selectedNodeIndex: common.selectedNode.index, sso: sso, nodes: nodesArr });
res.status(200).json({ defaultNodeIndex: multiNodeConfig.defaultNodeIndex, selectedNodeIndex: common.selectedNode.index, sso: sso, nodes: nodesArr });
}
});
}
@ -95,53 +112,79 @@ exports.updateUISettings = (req, res, next) => {
var config = JSON.parse(fs.readFileSync(RTLConfFile, 'utf-8'));
config.nodes.find(node => {
if(node.index == common.selectedNode.index) {
node.Settings.flgSidenavOpened = req.body.updatedSettings.flgSidenavOpened;
node.Settings.flgSidenavPinned = req.body.updatedSettings.flgSidenavPinned;
node.Settings.menu = req.body.updatedSettings.menu;
node.Settings.menuType = req.body.updatedSettings.menuType;
node.Settings.theme = req.body.updatedSettings.theme;
node.Settings.satsToBTC = req.body.updatedSettings.satsToBTC;
node.Settings.userPersona = req.body.updatedSettings.userPersona;
node.Settings.themeMode = req.body.updatedSettings.themeMode;
node.Settings.themeColor = req.body.updatedSettings.themeColor;
node.Settings.currencyUnit = req.body.updatedSettings.currencyUnit;
node.Settings.flgSidenavOpened = true; // req.body.updatedSettings.flgSidenavOpened;
node.Settings.flgSidenavPinned = true; // req.body.updatedSettings.flgSidenavPinned;
node.Settings.menu = 'VERTICAL'; // req.body.updatedSettings.menu;
node.Settings.menuType = 'REGULAR'; // req.body.updatedSettings.menuType;
node.Settings.fontSize = 'MEDIUM'; // req.body.updatedSettings.fontSize;
node.Settings.satsToBTC = false; // req.body.updatedSettings.satsToBTC;
}
});
try {
fs.writeFileSync(RTLConfFile, JSON.stringify(config, null, 2), 'utf-8');
logger.info({fileName: 'RTLConf', msg: 'Updating UI Settings Succesful!'});
res.status(201).json({message: 'UI Settings Updated Successfully'});
logger.info({fileName: 'RTLConf', msg: 'Updating Application Node Settings Succesful!'});
res.status(201).json({message: 'Application Node Settings Updated Successfully'});
}
catch (err) {
logger.error({fileName: 'Conf', lineNum: 102, msg: 'Updating UI Settings Failed!'});
logger.error({fileName: 'Conf', lineNum: 102, msg: 'Updating Application Node Settings Failed!'});
res.status(500).json({
message: "Updating UI Settings Failed!",
error: 'Updating UI Settings Failed!'
message: "Updating Application Node Settings Failed!",
error: 'Updating Application Node Settings Failed!'
});
}
} else {
RTLConfFile = common.rtl_conf_file_path + '/RTL.conf';
var config = ini.parse(fs.readFileSync(RTLConfFile, 'utf-8'));
const settingsTemp = config.Settings;
settingsTemp.flgSidenavOpened = req.body.updatedSettings.flgSidenavOpened;
settingsTemp.flgSidenavPinned = req.body.updatedSettings.flgSidenavPinned;
settingsTemp.menu = req.body.updatedSettings.menu;
settingsTemp.menuType = req.body.updatedSettings.menuType;
settingsTemp.theme = req.body.updatedSettings.theme;
settingsTemp.satsToBTC = req.body.updatedSettings.satsToBTC;
settingsTemp.userPersona = req.body.updatedSettings.userPersona;
settingsTemp.themeMode = req.body.updatedSettings.themeMode;
settingsTemp.themeColor = req.body.updatedSettings.themeColor;
settingsTemp.currencyUnit = req.body.updatedSettings.currencyUnit;
settingsTemp.flgSidenavOpened = true; // req.body.updatedSettings.flgSidenavOpened;
settingsTemp.flgSidenavPinned = true; // req.body.updatedSettings.flgSidenavPinned;
settingsTemp.menu = 'VERTICAL'; // req.body.updatedSettings.menu;
settingsTemp.menuType = 'REGULAR'; // req.body.updatedSettings.menuType;
settingsTemp.fontSize = 'MEDIUM'; // req.body.updatedSettings.fontSize;
settingsTemp.satsToBTC = false; // req.body.updatedSettings.satsToBTC;
delete config.Settings;
fs.writeFileSync(RTLConfFile, ini.stringify(config));
fs.appendFile(RTLConfFile, ini.stringify(settingsTemp, { section: 'Settings' }), function(err) {
if (err) {
logger.error({fileName: 'Conf', lineNum: 122, msg:'Updating UI Settings Failed!'});
logger.error({fileName: 'Conf', lineNum: 122, msg:'Updating Application Node Settings Failed!'});
res.status(500).json({
message: "Updating UI Settings Failed!",
error: 'Updating UI Settings Failed!'
message: "Updating Application Node Settings Failed!",
error: 'Updating Application Node Settings Failed!'
});
} else {
logger.info({fileName: 'RTLConf', msg: 'Updating UI Settings Succesful!'});
res.status(201).json({message: 'UI Settings Updated Successfully'});
logger.info({fileName: 'RTLConf', msg: 'Updating Application Node Settings Succesful!'});
res.status(201).json({message: 'Application Node Settings Updated Successfully'});
}
});
}
};
exports.updateDefaultNode = (req, res, next) => {
RTLConfFile = common.rtl_conf_file_path + '/RTL-Multi-Node-Conf.json';
var config = JSON.parse(fs.readFileSync(RTLConfFile, 'utf-8'));
config.defaultNodeIndex = req.body.defaultNodeIndex;
try {
fs.writeFileSync(RTLConfFile, JSON.stringify(config, null, 2), 'utf-8');
logger.info({fileName: 'RTLConf', msg: 'Updating Default Node Succesful!'});
res.status(201).json({message: 'Default Node Updated Successfully'});
}
catch (err) {
logger.error({fileName: 'Conf', lineNum: 102, msg: 'Updating Default Node Failed!'});
res.status(500).json({
message: "Updating Default Node Failed!",
error: 'Updating Default Node Failed!'
});
}
};
exports.getConfig = (req, res, next) => {
let confFile = '';
let JSONFormat = false;
@ -193,3 +236,25 @@ exports.getConfig = (req, res, next) => {
}
});
};
exports.getCurrencyRates = (req, res, next) => {
options.url = 'https://blockchain.info/ticker';
request(options).then((body) => {
if(undefined === body || body.error) {
res.status(500).json({
message: "Fetching Rates Failed!",
error: (undefined === body) ? 'Error From External Server!' : body.error
});
} else {
res.status(200).json(body);
body = JSON.parse(body);
}
})
.catch(function (err) {
logger.error({fileName: 'Conf', lineNum: 241, msg: 'Fetching Rates Failed! ' + JSON.stringify(err)});
return res.status(500).json({
message: "Fetching Rates Failed!",
error: err.error
});
});
};

@ -8,6 +8,12 @@ exports.listChannels = (req, res, next) => {
options.url = common.getSelLNServerUrl() + '/channel/listChannels';
request(options).then(function (body) {
logger.info({fileName: 'Channels', msg: 'List Channels: ' + JSON.stringify(body)});
body.map(channel => {
local = (channel.msatoshi_to_us) ? channel.msatoshi_to_us : 0;
remote = (channel.msatoshi_to_them) ? channel.msatoshi_to_them : 0;
total = channel.msatoshi_total ? channel.msatoshi_total : 0;
channel.balancedness = (total === 0) ? 1 : (1 - Math.abs((local-remote)/total)).toFixed(3);
})
res.status(200).json(body);
})
.catch(function (err) {
@ -23,7 +29,7 @@ exports.openChannel = (req, res, next) => {
options = common.getOptions();
options.url = common.getSelLNServerUrl() + '/channel/openChannel';
options.body = req.body;
logger.info({fileName: 'Channels', msg: 'Open Channel Options: ' + JSON.stringify(options)});
logger.info({fileName: 'Channels', msg: 'Open Channel Options: ' + JSON.stringify(options.body)});
request.post(options).then((body) => {
logger.info({fileName: 'Channels', msg: 'Open Channel Response: ' + JSON.stringify(body)});
if(undefined === body || body.error) {
@ -48,7 +54,7 @@ exports.setChannelFee = (req, res, next) => {
options = common.getOptions();
options.url = common.getSelLNServerUrl() + '/channel/setChannelFee';
options.body = req.body;
logger.info({fileName: 'Channels', msg: 'Update Channel Policy Options: ' + JSON.stringify(options)});
logger.info({fileName: 'Channels', msg: 'Update Channel Policy Options: ' + JSON.stringify(options.body)});
request.post(options).then((body) => {
logger.info({fileName: 'Channels', msg: 'Update Channel Policy: ' + JSON.stringify(body)});
if(undefined === body || body.error) {

@ -26,6 +26,28 @@ exports.getInfo = (req, res, next) => {
} else {
body.currency_unit = 'BTC';
body.smaller_currency_unit = 'Sats';
body.lnImplementation = 'C-Lightning';
let chainObj = { chain: '', network: '' };
if (body.network === 'testnet') {
chainObj.chain = 'Bitcoin';
chainObj.network = 'Testnet';
} else if (body.network === 'bitcoin') {
chainObj.chain = 'Bitcoin';
chainObj.network = 'Mainnet';
} else if (body.network === 'litecoin') {
chainObj.chain = 'Litecoin';
chainObj.network = 'Mainnet';
} else if (body.network === 'litecoin-testnet') {
chainObj.chain = 'Litecoin';
chainObj.network = 'Testnet';
}
body.chains = [chainObj];
body.uris = [];
if (body.address && body.address.length>0) {
body.address.forEach(addr => {
body.uris.push(body.id + '@' + addr.address + ':' + addr.port);
});
}
res.status(200).json(body);
}
})

@ -29,7 +29,9 @@ exports.listNode = (req, res, next) => {
options.url = common.getSelLNServerUrl() + '/network/listNode/' + req.params.id;
request(options).then(function (body) {
logger.info({fileName: 'Network', msg: 'Node Lookup: ' + JSON.stringify(body)});
body.last_timestamp_str = (body.last_timestamp) ? common.convertTimestampToDate(body.last_timestamp) : '';
body.forEach(node => {
node.last_timestamp_str = (node.last_timestamp) ? common.convertTimestampToDate(node.last_timestamp) : '';
});
res.status(200).json(body);
})
.catch((err) => {

@ -21,7 +21,7 @@ exports.onChainWithdraw = (req, res, next) => {
options = common.getOptions();
options.url = common.getSelLNServerUrl() + '/withdraw';
options.body = req.body;
logger.info({fileName: 'OnChain', msg: 'OnChain Withdraw Options: ' + JSON.stringify(options)});
logger.info({fileName: 'OnChain', msg: 'OnChain Withdraw Options: ' + JSON.stringify(options.body)});
request.post(options).then((body) => {
logger.info({fileName: 'OnChain', msg: 'OnChain Withdraw Response: ' + JSON.stringify(body)});
if(undefined === body || body.error) {

@ -3,71 +3,104 @@ var common = require('../../common');
var logger = require('../logger');
var options = {};
getAliasForChannel = (channel, channelType) => {
getAliasForChannel = (channel) => {
return new Promise(function(resolve, reject) {
if (undefined === channelType || channelType === 'all') {
options.url = common.getSelLNServerUrl() + '/graph/node/' + channel.remote_pubkey;
} else {
options.url = common.getSelLNServerUrl() + '/graph/node/' + channel.channel.remote_node_pub;
}
options.url = common.getSelLNServerUrl() + '/graph/node/' + channel.remote_pubkey;
request(options).then(function(aliasBody) {
logger.info({fileName: 'Channels', msg: 'Alias: ' + JSON.stringify(aliasBody.node.alias)});
if (undefined === channelType || channelType === 'all') {
channel.remote_alias = aliasBody.node.alias;
resolve(aliasBody.node.alias);
} else {
channel.channel.remote_alias = aliasBody.node.alias;
resolve(aliasBody.node.alias);
}
channel.remote_alias = aliasBody.node.alias;
resolve(aliasBody.node.alias);
})
.catch(err => resolve(''));
});
}
exports.getChannels = (req, res, next) => {
exports.getAllChannels = (req, res, next) => {
options = common.getOptions();
if (undefined === req.params.channelType || req.params.channelType === 'all') {
options.url = common.getSelLNServerUrl() + '/channels';
} else {
options.url = common.getSelLNServerUrl() + '/channels/' + req.params.channelType; // active_only, inactive_only, public_only, private_only, Not Implemented in Frontend yet
}
options.url = common.getSelLNServerUrl() + '/channels';
options.qs = req.query;
let local = 0;
let remote = 0;
let total = 0;
request(options).then(function (body) {
let channels = [];
if (undefined === req.params.channelType || req.params.channelType === 'all') {
channels = (undefined === body.channels) ? [] : body.channels;
if(body.channels) {
Promise.all(
channels.map(channel => {
return getAliasForChannel(channel, req.params.channelType);
body.channels.map(channel => {
local = (channel.local_balance) ? +channel.local_balance : 0;
remote = (channel.remote_balance) ? +channel.remote_balance : 0;
total = local + remote;
channel.balancedness = (total === 0) ? 1 : (1 - Math.abs((local-remote)/total)).toFixed(3);
return getAliasForChannel(channel);
})
)
.then(function(values) {
logger.info({fileName: 'Channels', msg: 'Channels with Alias: ' + JSON.stringify(body)});
body.channels = common.sortDescByKey(body.channels, 'balancedness');
logger.info({fileName: 'Channels', msg: 'All Channels with Alias: ' + JSON.stringify(body)});
res.status(200).json(body);
}).catch(err => {
console.error(err.error);
logger.error({fileName: 'Channels', lineNum: 49, msg: 'Get All Channel Alias: ' + JSON.stringify(err)});
res.status(500).json({
message: 'Fetching Channels Alias Failed!',
error: err.error
});
});
} else {
if (undefined === body.total_limbo_balance) {
body.total_limbo_balance = 0;
body.btc_total_limbo_balance = 0;
} else {
body.btc_total_limbo_balance = common.convertToBTC(body.total_limbo_balance);
}
if (req.params.channelType === 'closed' && body.channels && body.channels.length > 0) {
body.channels.forEach(channel => {
channel.close_type = (undefined === channel.close_type) ? 'COOPERATIVE_CLOSE' : channel.close_type;
});
body.channels = common.sortDescByKey(body.channels, 'close_type');
}
logger.info({fileName: 'Channels', msg: 'Pending/Closed Channels: ' + JSON.stringify(body)});
body.channels = [];
res.status(200).json(body);
}
})
.catch(function (err) {
logger.error({fileName: 'Channels', lineNum: 68, msg: 'Get Channel: ' + JSON.stringify(err)});
logger.error({fileName: 'Channels', lineNum: 68, msg: 'Get All Channel: ' + JSON.stringify(err)});
return res.status(500).json({
message: 'Fetching All Channels Failed!',
error: err.error
});
});
};
exports.getPendingChannels = (req, res, next) => {
options = common.getOptions();
options.url = common.getSelLNServerUrl() + '/channels/pending';
options.qs = req.query;
request(options).then(function (body) {
let channels = [];
if (undefined === body.total_limbo_balance) {
body.total_limbo_balance = 0;
body.btc_total_limbo_balance = 0;
} else {
body.btc_total_limbo_balance = common.convertToBTC(body.total_limbo_balance);
}
logger.info({fileName: 'Channels', msg: 'Pending Channels: ' + JSON.stringify(body)});
res.status(200).json(body);
})
.catch(function (err) {
logger.error({fileName: 'Channels', lineNum: 68, msg: 'Get Pending Channel: ' + JSON.stringify(err)});
return res.status(500).json({
message: 'Fetching Channels Failed!',
message: 'Fetching Pending Channels Failed!',
error: err.error
});
});
};
exports.getClosedChannels = (req, res, next) => {
options = common.getOptions();
options.url = common.getSelLNServerUrl() + '/channels/closed';
options.qs = req.query;
request(options).then(function (body) {
let channels = [];
if (body.channels && body.channels.length > 0) {
body.channels.forEach(channel => {
channel.close_type = (undefined === channel.close_type) ? 'COOPERATIVE_CLOSE' : channel.close_type;
});
body.channels = common.sortDescByKey(body.channels, 'close_type');
}
logger.info({fileName: 'Channels', msg: 'Closed Channels: ' + JSON.stringify(body)});
res.status(200).json(body);
})
.catch(function (err) {
logger.error({fileName: 'Channels', lineNum: 68, msg: 'Get Closed Channel: ' + JSON.stringify(err)});
return res.status(500).json({
message: 'Fetching Closed Channels Failed!',
error: err.error
});
});
@ -112,17 +145,25 @@ exports.postTransactions = (req, res, next) => {
options = common.getOptions();
options.url = common.getSelLNServerUrl() + '/channels/transactions';
if(req.body.paymentReq) {
options.form = JSON.stringify({
options.form = {
payment_request: req.body.paymentReq
});
};
} else if(req.body.paymentDecoded) {
options.form = JSON.stringify({
options.form = {
payment_hash_string: req.body.paymentDecoded.payment_hash,
final_cltv_delta: parseInt(req.body.paymentDecoded.cltv_expiry),
amt: req.body.paymentDecoded.num_satoshis,
dest_string: req.body.paymentDecoded.destination
});
};
}
if(req.body.feeLimit) {
options.form.fee_limit = req.body.feeLimit;
}
if(req.body.outgoingChannel) {
options.form.outgoing_chan_id = req.body.outgoingChannel;
}
options.form = JSON.stringify(options.form);
logger.info({fileName: 'Channels', msg: 'Send Payment Options: ' + options.form});
request.post(options).then((body) => {
logger.info({fileName: 'Channels', msg: 'Send Payment Response: ' + JSON.stringify(body)});
if(undefined === body || body.error) {
@ -195,7 +236,7 @@ exports.postChanPolicy = (req, res, next) => {
chan_point: {funding_txid_str: txid_str, output_index: parseInt(output_idx)}
});
}
logger.info({fileName: 'Channels', msg: 'Update Channel Policy Options: ' + JSON.stringify(options)});
logger.info({fileName: 'Channels', msg: 'Update Channel Policy Options: ' + JSON.stringify(options.form)});
request.post(options).then((body) => {
logger.info({fileName: 'Channels', msg: 'Update Channel Policy: ' + JSON.stringify(body)});
if(undefined === body || body.error) {

@ -32,11 +32,13 @@ exports.getBackup = (req, res, next) => {
let message = '';
if (req.params.channelPoint === 'ALL') {
channel_backup_file = common.selectedNode.channel_backup_path + common.path_separator + 'channel-all.bak';
message = 'All Channels Backup Successful at: ' + channel_backup_file + ' !';
message = 'All Channels Backup Successful.';
// message = 'All Channels Backup Successful at: ' + channel_backup_file + ' !';
options.url = common.getSelLNServerUrl() + '/channels/backup';
} else {
channel_backup_file = common.selectedNode.channel_backup_path + common.path_separator + 'channel-' + req.params.channelPoint.replace(':', '-') + '.bak';
message = 'Channel Backup Successful at: ' + channel_backup_file + ' !';
message = 'Channel Backup Successful.';
// message = 'Channel Backup Successful at: ' + channel_backup_file + ' !';
let channelpoint = req.params.channelPoint.replace(':', '/');
options.url = common.getSelLNServerUrl() + '/channels/backup/' + channelpoint;
let exists = fs.existsSync(channel_backup_file);
@ -78,7 +80,7 @@ exports.postBackupVerify = (req, res, next) => {
let message = '';
let verify_backup = '';
if (req.params.channelPoint === 'ALL') {
message = 'All Channels Verify Successful!';
message = 'All Channels Verify Successful.';
channel_verify_file = common.selectedNode.channel_backup_path + common.path_separator + 'channel-all.bak';
let exists = fs.existsSync(channel_verify_file);
if (exists) {
@ -95,7 +97,8 @@ exports.postBackupVerify = (req, res, next) => {
res.status(404).json({ message: 'Channels backup to verify does not Exist!' });
}
} else {
message = 'Channel ' + req.params.channelPoint + ' Verify Successful!';
message = 'Channel Verify Successful.';
// message = 'Channel ' + req.params.channelPoint + ' Verify Successful!';
channel_verify_file = common.selectedNode.channel_backup_path + common.path_separator + 'channel-' + req.params.channelPoint.replace(':', '-') + '.bak';
let exists = fs.existsSync(channel_verify_file);
if (exists) {
@ -128,7 +131,7 @@ exports.postRestore = (req, res, next) => {
let message = '';
let restore_backup = '';
if (req.params.channelPoint === 'ALL') {
message = 'All Channels Restore Successful!';
message = 'All Channels Restore Successful.';
channel_restore_file = common.selectedNode.channel_backup_path + common.path_separator + 'restore' + common.path_separator + 'channel-all.bak';
let exists = fs.existsSync(channel_restore_file);
if (exists) {
@ -144,7 +147,8 @@ exports.postRestore = (req, res, next) => {
res.status(404).json({ message: 'Channels backup to restore does not Exist!' });
}
} else {
message = 'Channel ' + req.params.channelPoint + ' Restore Successful!';
message = 'Channel Restore Successful.';
// message = 'Channel ' + req.params.channelPoint + ' Restore Successful!';
channel_restore_file = common.selectedNode.channel_backup_path + common.path_separator + 'restore' + common.path_separator + 'channel-' + req.params.channelPoint.replace(':', '-') + '.bak';
let exists = fs.existsSync(channel_restore_file);
if (exists) {

@ -48,11 +48,11 @@ exports.getFees = (req, res, next) => {
return res.status(200).json(body);
})
.catch(err => {
logger.error({fileName: 'Fees', lineNum: 54, msg: 'Fetch Fee Error: ' + JSON.stringify(err)});
return res.status(500).json({
message: "Fetching fee failed!",
error: err.error
});
logger.error({fileName: 'Fees', lineNum: 54, msg: 'Fetch Forwarding Events Error: ' + JSON.stringify(err)});
body.daily_tx_count = 0;
body.weekly_tx_count = 0;
body.monthly_tx_count = 0;
return res.status(200).json(body);
});
}
})

@ -157,3 +157,44 @@ exports.getQueryRoutes = (req, res, next) => {
});
});
};
exports.getRemoteFeePolicy = (req, res, next) => {
options = common.getOptions();
options.url = common.getSelLNServerUrl() + '/graph/edge/' + req.params.chanid;
request(options).then((body) => {
logger.info({fileName: 'Graph', msg: 'Edge Info Received: ' + JSON.stringify(body)});
if(undefined === body || body.error) {
res.status(500).json({
message: "Fetching Edge Info Failed!",
error: (undefined === body) ? 'Error From Server!' : body.error
});
}
if (undefined !== body) {
body.last_update_str = (undefined === body.last_update) ? '' : common.convertTimestampToDate(body.last_update);
}
remoteNodeFee = {};
if(body.node1_pub === req.params.localPubkey){
remoteNodeFee = {
time_lock_delta: body.node2_policy.time_lock_delta,
fee_base_msat: body.node2_policy.fee_base_msat,
fee_rate_milli_msat: body.node2_policy.fee_rate_milli_msat
};
}
else if(body.node2_pub === req.params.localPubkey) {
remoteNodeFee = {
time_lock_delta: body.node1_policy.time_lock_delta,
fee_base_msat: body.node1_policy.fee_base_msat,
fee_rate_milli_msat: body.node1_policy.fee_rate_milli_msat
};
}
res.status(200).json(remoteNodeFee);
})
.catch((err) => {
return res.status(500).json({
message: "Fetching Edge Info Failed!",
error: err.error
});
});
};

@ -9,7 +9,7 @@ exports.decodePayment = (req, res, next) => {
request(options).then((body) => {
const body_str = (undefined === body) ? '' : JSON.stringify(body);
const search_idx = (undefined === body) ? -1 : body_str.search('Not Found');
logger.info({fileName: 'PayReq', msg: 'Payment Decodd Received: ' + body_str});
logger.info({fileName: 'PayReq', msg: 'Payment Decode Received: ' + body_str});
if(undefined === body || search_idx > -1 || body.error) {
res.status(500).json({
message: "Payment Request Decode Failed!",

@ -15,7 +15,6 @@ exports.getAllForwardingEvents = (start, end, offset, max_events) => {
options.form = JSON.stringify(options.form);
logger.info({fileName: 'Switch', msg: 'Forwarding History Params: ' + options.form});
request.post(options).then((body) => {
logger.info({fileName: 'Switch', msg: 'Forwarding History Received: ' + JSON.stringify(body)});
if(undefined === body || body.error) {
logger.error({fileName: 'Switch', lineNum: 31, msg: 'Forwarding History Error: ' + JSON.stringify((undefined === body) ? 'Error From Server!' : body.error)});
res.status(500).json({
@ -23,13 +22,14 @@ exports.getAllForwardingEvents = (start, end, offset, max_events) => {
error: (undefined === body) ? 'Error From Server!' : body.error
});
} else {
if (undefined !== body.forwarding_events && body.forwarding_events.length > 0) {
logger.info({fileName: 'Switch', msg: 'Forwarding History Received: ' + JSON.stringify(body)});
if (body.forwarding_events && body.forwarding_events.length > 0) {
body.forwarding_events.forEach(event => {
event.timestamp_str = (undefined === event.timestamp) ? '' : common.convertTimestampToDate(event.timestamp);
});
body.forwarding_events = common.sortDescByKey(body.forwarding_events, 'timestamp');
}
logger.info({fileName: 'Switch', msg: 'Forwarding History Before Resolve: ' + JSON.stringify(body)});
logger.info({fileName: 'Switch', msg: 'Forwarding History before Resolve: ' + JSON.stringify(body)});
resolve(body);
}
})

@ -6,7 +6,7 @@ exports.info = (msgJSON, selNode = common.selectedNode) => {
if (msgJSON.fileName !== 'Config Setup Variable') {
console.log('Console: ' + msgStr);
}
if(selNode.enable_logging) {
if(selNode && selNode.enable_logging) {
fs.appendFile(selNode.log_file, msgStr, function(err) {
if (err) {
return ({ error: 'Updating Log Failed!' });
@ -20,7 +20,7 @@ exports.info = (msgJSON, selNode = common.selectedNode) => {
exports.error = (msgJSON, selNode = common.selectedNode) => {
const msgStr = '\r\nERROR: ' + msgJSON.fileName + '(' + msgJSON.lineNum + ') => ' + msgJSON.msg;
console.error('Console: ' + msgStr);
if(selNode.enable_logging) {
if(selNode && selNode.enable_logging) {
fs.appendFile(selNode.log_file, msgStr, function(err) {
if (err) {
return ({ error: 'Updating Log Failed!' });

@ -0,0 +1,438 @@
syntax = "proto3";
import "google/api/annotations.proto";
package looprpc;
/**
SwapClient is a service that handles the client side process of onchain/offchain
swaps. The service is designed for a single client.
*/
service SwapClient {
/** loop: `out`
LoopOut initiates an loop out swap with the given parameters. The call
returns after the swap has been set up with the swap server. From that
point onwards, progress can be tracked via the SwapStatus stream that is
returned from Monitor().
*/
rpc LoopOut (LoopOutRequest) returns (SwapResponse) {
option (google.api.http) = {
post: "/v1/loop/out"
body: "*"
};
}
/**
LoopIn initiates a loop in swap with the given parameters. The call
returns after the swap has been set up with the swap server. From that
point onwards, progress can be tracked via the SwapStatus stream
that is returned from Monitor().
*/
rpc LoopIn (LoopInRequest) returns (SwapResponse) {
option (google.api.http) = {
post: "/v1/loop/in"
body: "*"
};
}
/** loop: `monitor`
Monitor will return a stream of swap updates for currently active swaps.
TODO: add MonitorSync version for REST clients.
*/
rpc Monitor (MonitorRequest) returns (stream SwapStatus);
/** loop: `terms`
LoopOutTerms returns the terms that the server enforces for a loop out swap.
*/
rpc LoopOutTerms (TermsRequest) returns (TermsResponse) {
option (google.api.http) = {
get: "/v1/loop/out/terms"
};
}
/** loop: `quote`
LoopOutQuote returns a quote for a loop out swap with the provided
parameters.
*/
rpc LoopOutQuote (QuoteRequest) returns (QuoteResponse) {
option (google.api.http) = {
get: "/v1/loop/out/quote/{amt}"
};
}
/**
GetTerms returns the terms that the server enforces for swaps.
*/
rpc GetLoopInTerms (TermsRequest) returns (TermsResponse) {
option (google.api.http) = {
get: "/v1/loop/in/terms"
};
}
/**
GetQuote returns a quote for a swap with the provided parameters.
*/
rpc GetLoopInQuote (QuoteRequest) returns (QuoteResponse) {
option (google.api.http) = {
get: "/v1/loop/in/quote/{amt}"
};
}
/**
GetLsatTokens returns all LSAT tokens the daemon ever paid for.
*/
rpc GetLsatTokens (TokensRequest) returns (TokensResponse) {
option (google.api.http) = {
get: "/v1/lsat/tokens"
};
}
}
message LoopOutRequest {
/**
Requested swap amount in sat. This does not include the swap and miner fee.
*/
int64 amt = 1;
/**
Base58 encoded destination address for the swap.
*/
string dest = 2;
/**
Maximum off-chain fee in msat that may be paid for payment to the server.
This limit is applied during path finding. Typically this value is taken
from the response of the GetQuote call.
*/
int64 max_swap_routing_fee = 3;
/**
Maximum off-chain fee in msat that may be paid for payment to the server.
This limit is applied during path finding. Typically this value is taken
from the response of the GetQuote call.
*/
int64 max_prepay_routing_fee = 4;
/**
Maximum we are willing to pay the server for the swap. This value is not
disclosed in the swap initiation call, but if the server asks for a
higher fee, we abort the swap. Typically this value is taken from the
response of the GetQuote call. It includes the prepay amount.
*/
int64 max_swap_fee = 5;
/**
Maximum amount of the swap fee that may be charged as a prepayment.
*/
int64 max_prepay_amt = 6;
/**
Maximum in on-chain fees that we are willing to spent. If we want to
sweep the on-chain htlc and the fee estimate turns out higher than this
value, we cancel the swap. If the fee estimate is lower, we publish the
sweep tx.
If the sweep tx is not confirmed, we are forced to ratchet up fees until it
is swept. Possibly even exceeding max_miner_fee if we get close to the htlc
timeout. Because the initial publication revealed the preimage, we have no
other choice. The server may already have pulled the off-chain htlc. Only
when the fee becomes higher than the swap amount, we can only wait for fees
to come down and hope - if we are past the timeout - that the server is not
publishing the revocation.
max_miner_fee is typically taken from the response of the GetQuote call.
*/
int64 max_miner_fee = 7;
/**
The channel to loop out, the channel to loop out is selected based on the
lowest routing fee for the swap payment to the server.
*/
uint64 loop_out_channel = 8;
/**
The number of blocks from the on-chain HTLC's confirmation height that it
should be swept within.
*/
int32 sweep_conf_target = 9;
/**
The latest time (in unix seconds) we allow the server to wait before
publishing the HTLC on chain. Setting this to a larger value will give the
server the opportunity to batch multiple swaps together, and wait for
low-fee periods before publishing the HTLC, potentially resulting in a
lower total swap fee.
*/
uint64 swap_publication_deadline = 10;
}
message LoopInRequest {
/**
Requested swap amount in sat. This does not include the swap and miner
fee.
*/
int64 amt = 1;
/**
Maximum we are willing to pay the server for the swap. This value is not
disclosed in the swap initiation call, but if the server asks for a
higher fee, we abort the swap. Typically this value is taken from the
response of the GetQuote call.
*/
int64 max_swap_fee = 2;
/**
Maximum in on-chain fees that we are willing to spent. If we want to
publish the on-chain htlc and the fee estimate turns out higher than this
value, we cancel the swap.
max_miner_fee is typically taken from the response of the GetQuote call.
*/
int64 max_miner_fee = 3;
/**
The channel to loop in. If zero, the channel to loop in is selected based
on the lowest routing fee for the swap payment from the server.
Note: NOT YET IMPLEMENTED
*/
uint64 loop_in_channel = 4;
/**
If external_htlc is true, we expect the htlc to be published by an external
actor.
*/
bool external_htlc = 5;
}
message SwapResponse {
/**
Swap identifier to track status in the update stream that is returned from
the Start() call. Currently this is the hash that locks the htlcs.
*/
string id = 1;
/**
The address of the on-chain htlc.
*/
string htlc_address = 2;
}
message MonitorRequest {
}
message SwapStatus {
/**
Requested swap amount in sat. This does not include the swap and miner
fee.
*/
int64 amt = 1;
/**
Swap identifier to track status in the update stream that is returned from
the Start() call. Currently this is the hash that locks the htlcs.
*/
string id = 2;
/**
Swap type
*/
SwapType type = 3;
/**
State the swap is currently in, see State enum.
*/
SwapState state = 4;
/**
Initiation time of the swap.
*/
int64 initiation_time = 5;
/**
Initiation time of the swap.
*/
int64 last_update_time = 6;
/**
Htlc address.
*/
string htlc_address = 7;
/// Swap server cost
int64 cost_server = 8;
// On-chain transaction cost
int64 cost_onchain = 9;
// Off-chain routing fees
int64 cost_offchain = 10;
}
enum SwapType {
// LOOP_OUT indicates an loop out swap (off-chain to on-chain)
LOOP_OUT = 0;
// LOOP_IN indicates a loop in swap (on-chain to off-chain)
LOOP_IN = 1;
}
enum SwapState {
/**
INITIATED is the initial state of a swap. At that point, the initiation
call to the server has been made and the payment process has been started
for the swap and prepayment invoices.
*/
INITIATED = 0;
/**
PREIMAGE_REVEALED is reached when the sweep tx publication is first
attempted. From that point on, we should consider the preimage to no
longer be secret and we need to do all we can to get the sweep confirmed.
This state will mostly coalesce with StateHtlcConfirmed, except in the
case where we wait for fees to come down before we sweep.
*/
PREIMAGE_REVEALED = 1;
/**
HTLC_PUBLISHED is reached when the htlc tx has been published in a loop in
swap.
*/
HTLC_PUBLISHED = 2;
/**
SUCCESS is the final swap state that is reached when the sweep tx has
the required confirmation depth.
*/
SUCCESS = 3;
/**
FAILED is the final swap state for a failed swap with or without loss of
the swap amount.
*/
FAILED = 4;
/**
INVOICE_SETTLED is reached when the swap invoice in a loop in swap has been
paid, but we are still waiting for the htlc spend to confirm.
*/
INVOICE_SETTLED = 5;
}
message TermsRequest {
}
message TermsResponse {
reserved 1, 2, 3, 4, 7;
/**
Minimum swap amount (sat)
*/
int64 min_swap_amount = 5;
/**
Maximum swap amount (sat)
*/
int64 max_swap_amount = 6;
}
message QuoteRequest {
/**
The amount to swap in satoshis.
*/
int64 amt = 1;
/**
The confirmation target that should be used either for the sweep of the
on-chain HTLC broadcast by the swap server in the case of a Loop Out, or for
the confirmation of the on-chain HTLC broadcast by the swap client in the
case of a Loop In.
*/
int32 conf_target = 2;
/**
If external_htlc is true, we expect the htlc to be published by an external
actor.
*/
bool external_htlc = 3;
}
message QuoteResponse {
/**
The fee that the swap server is charging for the swap.
*/
int64 swap_fee = 1;
/**
The part of the swap fee that is requested as a prepayment.
*/
int64 prepay_amt = 2;
/**
An estimate of the on-chain fee that needs to be paid to sweep the HTLC.
*/
int64 miner_fee = 3;
/**
The node pubkey where the swap payment needs to be paid
to. This can be used to test connectivity before initiating the swap.
*/
bytes swap_payment_dest = 4;
/**
On-chain cltv expiry delta
*/
int32 cltv_delta = 5;
}
message TokensRequest {
}
message TokensResponse {
/**
List of all tokens the daemon knows of, including old/expired tokens.
*/
repeated LsatToken tokens = 1;
}
message LsatToken {
/**
The base macaroon that was baked by the auth server.
*/
bytes base_macaroon = 1;
/**
The payment hash of the payment that was paid to obtain the token.
*/
bytes payment_hash = 2;
/**
The preimage of the payment hash, knowledge of this is proof that the
payment has been paid. If the preimage is set to all zeros, this means the
payment is still pending and the token is not yet fully valid.
*/
bytes payment_preimage = 3;
/**
The amount of millisatoshis that was paid to get the token.
*/
int64 amount_paid_msat = 4;
/**
The amount of millisatoshis paid in routing fee to pay for the token.
*/
int64 routing_fee_paid_msat = 5;
/**
The creation time of the token as UNIX timestamp in seconds.
*/
int64 time_created = 6;
/**
Indicates whether the token is expired or still valid.
*/
bool expired = 7;
/**
Identifying attribute of this token in the store. Currently represents the
file name of the token where it's stored on the file system.
*/
string storage_name = 8;
}

@ -0,0 +1,25 @@
const grpc = require('grpc');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync('./controllers/loopd/client.proto', {keepCase: true, longs: String, enums: String, defaults: true, oneofs: true});
const looprpc = grpc.loadPackageDefinition(packageDefinition).looprpc;
const swapClient = new looprpc.SwapClient('localhost:11010', grpc.credentials.createInsecure());
exports.loopIn = (req, res, next) => {
swapClient.loopIn(req.body, function(error, response) {
if (error) {
res.status(500).json({message: 'Loop In Failed!', error: error});
} else {
res.status(200).json(response);
}
})
};
exports.loopOut = (req, res, next) => {
swapClient.loopOut(req.body, function(error, response) {
if (error) {
res.status(500).json({message: 'Loop Out Failed!', error: error});
} else {
res.status(200).json(response);
}
})
};

@ -14,17 +14,11 @@ rtlPass=<>
[Settings]
;Set by RTL
flgSidenavOpened=true
userPersona=OPERATOR
;Set by RTL
flgSidenavPinned=true
themeMode=DAY
;Set by RTL
menu=Vertical
;Set by RTL
menuType=Regular
;Set by RTL
theme=dark-blue
;Set by RTL
satsToBTC=false
themeColor=PURPLE
;Full path of the bitcoin.conf file including the file name
bitcoindConfigPath=<>
;parameter to turn RTL logging off/on. Allowed values - true, false
@ -36,6 +30,8 @@ port=3000
lndServerUrl=https://localhost:8080/v1
;Channel backup folder
channelBackupPath=<>
;Set by RTL
currencyUnit=USD
[SSO]
;Single Sign On control
@ -66,3 +62,4 @@ RTL_SSO (1 - single sign on via an external cookie, 0 - stand alone RTL authenti
RTL_COOKIE_PATH (Full path of the cookie file including the file name)
LOGOUT_REDIRECT_LINK (URL to re-direct to after logout/timeout from RTL)
CHANNEL_BACKUP_PATH (folder location for saving the channel backup files)
CURRENCY_UNIT (Fiat currency unit for fiat conversion, Default 'USD')

@ -28,7 +28,7 @@ Follow the below steps to install and setup RTL to run on c-lightning.
#### First time setup
* Fetch sources from RTL git repository, by executing the below on the command prompt:
`$ git clone https://github.com/ShahanaFarooqui/RTL.git`
`$ git clone https://github.com/Ride-The-Lightning/RTL.git`
* Change directory to RTL folder:
`$ cd RTL`
@ -75,14 +75,12 @@ Ensure that the follow values are correct per your config:
"configPath": "<Optional - Config file path for c-lightning>"
},
"Settings": {
"flgSidenavOpened": false,
"flgSidenavPinned": true,
"menu": "Vertical",
"menuType": "Regular",
"theme": "dark-pink",
"satsToBTC": false,
"userPersona": "OPERATOR",
"themeMode": "DAY",
"themeColor": "PURPLE",
"bitcoindConfigPath": "",
"enableLogging": "true",
"enableLogging": true,
"currencyUnit": "USD",
"lnServerUrl": "https://<cl-rest api server ip address>:3001/v1"
}
}

@ -1,15 +1,15 @@
### Directions for setting up RTL to connect with and manage multiple LND nodes
### Directions for setting up RTL to connect with and manage multiple nodes
Caution: This feature is for advanced users, running multiple LND nodes.
Caution: This feature is for advanced users, running multiple nodes.
A single server instance of RTL can now be used to connect with multiple LND nodes on the same network. Multi-Node configuration requires the following:
1. Update lnd.conf for the LND nodes to enable remote connections and restart LND
2. Configure 'RTL-Multi-Node-Conf.json' with individual entries for each LND node
A single server instance of RTL can now be used to connect with multiple nodes on the same network. Multi-Node configuration requires the following:
1. In case of LND node implementation, update lnd.conf for the node to enable remote connections and restart LND
2. Configure 'RTL-Multi-Node-Conf.json' with individual entries for each node
3. Restart RTL
4. Run RTL and switch LND nodes live via settings
4. Run RTL and switch nodes live via dropdown on the menubar
#### 1. Update lnd.conf
This step is required to configure the nodes, which will be remotely connected with RTL.
This step is only required to configure the nodes, which will be remotely connected with RTL.
1. A static IP address must be assigned to the device running LND
2. `admin.macaroon` for this node must be transferred to the device on which you need to run RTL
3. Add this setting your lnd.conf file under the [Application Options] section: `restlisten=<ip address of the device running LND>:8080`
@ -19,19 +19,20 @@ This step is required to configure the nodes, which will be remotely connected w
1. Rename the `sample-RTL-Multi-Node-Conf.json` on the root RTL location to `RTL-Multi-Node-Conf.json`
2. Set `multiPass` to the preferred password. This password will be used to authenticate the user for RTL. Once authenticated, the user will be able to access all the nodes configured in the json file
3. Set the `port` to the preferred port number over which to run RTL
4. `SSO` section can be used for single-sign-on from applications like BTCPayserver. If using RTL as a stand-alone app to connect with the nodes, keep the `rtlSSO=0` and ignore the rest of `SSO` section.
5. `nodes` section is a json array, with each element of the array representing the specific parameters for the LND node to connect with. `index` must be a number and start with 1. This number must be unique for each node in the array. For each element, two items need to be configured for each unique node on the network (`macaroonPath` and `lndServerUrl`).
6. `macaroonPath` should be set to the local path of the folder containing `admin.macaroon` file for each node. Each node must have a different folder for the `admin.macaroon` on the RTL server.
7. `lndServerUrl` must be set to the service url for LND REST APIs for each node, with the unique ip address of the node hosting lnd e.g. https://192.168.0.1:8080/v1. In this case the ip address of the node hosting lnd is '192.168.0.1'
8. `lndConfigPath` and `bitcoindConfigPath` are optional parameters which can be set only if the RTL is running locally on the same node. Else it can be set to "" or removed from the conf file all together.
4. Set the `defaultNodeIndex` to configure the default start up node at server restart
5. `SSO` section can be used for single-sign-on from applications like BTCPayserver. If using RTL as a stand-alone app to connect with the nodes, keep the `rtlSSO=0` and ignore the rest of `SSO` section.
6. `nodes` section is a json array, with each element of the array representing the specific parameters for the LND node to connect with. `index` must be a number and start with 1. This number must be unique for each node in the array. For each element, two items need to be configured for each node on the network (`macaroonPath` and `lndServerUrl`/`lnServerUrl`).
7. `macaroonPath` should be set to the local path of the folder containing `admin.macaroon` file for each node. Each node must have a different folder for the `admin.macaroon` on the RTL server.
8. `lndServerUrl` must be set to the service url for LND REST APIs for each node, with the unique ip address of the node hosting lnd e.g. https://192.168.0.1:8080/v1. In this case the ip address of the node hosting lnd is '192.168.0.1'
OR
`lnServerUrl` must be set to the service url for C Lightining REST APIs for each node, with the unique ip address of the node hosting clightning e.g. https://192.168.0.2:3001/v1. In this case the ip address of the node hosting clightning is '192.168.0.2'
9. `lndConfigPath`(for LND)/`configPath`(for CLT) and `bitcoindConfigPath` are optional parameters which can be set only if the RTL is running locally on the same node. Else it can be set to "" or removed from the conf file all together.
#### 3. Restart RTL
#### 4. Run RTL and switch LND nodes in the UI
The application should be accessed, with the ip and port combination, as earlier. The user needs to enter the password set with the `multiPass` variable to authenticate. Once authenticated, the application will load the node with index value 1 by default. The other nodes configured with the file, can be accessed via the settings menu. Settings menu is opened by the gear icon on the right side of the app.
Upon opening up the settings menu, the list of available nodes can be found under `Switch Nodes` label. Choose the preferred nodes from the drop-down to switch.
#### 4. Run RTL and switch nodes in the UI
The application should be accessed, with the ip and port combination, as earlier. The user needs to enter the password set with the `multiPass` variable to authenticate. Once authenticated, the application will load the node with index configured with `defaultNodeIndex`. The other nodes configured with the file, can be accessed via the dropdown in the side menu.
Thats all for now.
The application is currently designed for a simple setup to access and manage multiple nodes.
More features like an advanced multi-node dashboard can be developed in the future, depending on the interest from the community.
More features like an advanced multi-node dashboard can be developed in the future, depending upon the interest from the community.

1814
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -28,18 +28,21 @@
"@angular/platform-browser-dynamic": "~8.1.2",
"@angular/router": "~8.1.2",
"@fortawesome/angular-fontawesome": "^0.5.0",
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"@ngrx/effects": "^8.4.0",
"@ngrx/router-store": "^8.4.0",
"@ngrx/store": "^8.4.0",
"@ngrx/store-devtools": "^8.4.0",
"@swimlane/ngx-charts": "^12.0.1",
"@fortawesome/fontawesome-svg-core": "^1.2.26",
"@fortawesome/free-regular-svg-icons": "^5.12.0",
"@fortawesome/free-solid-svg-icons": "^5.12.0",
"@grpc/proto-loader": "^0.5.3",
"@ngrx/effects": "^8.6.0",
"@ngrx/router-store": "^8.6.0",
"@ngrx/store": "^8.6.0",
"@ngrx/store-devtools": "^8.6.0",
"@swimlane/ngx-charts": "^12.1.0",
"angular-user-idle": "^2.2.1",
"angularx-qrcode": "^1.5.3",
"cookie-parser": "^1.4.4",
"core-js": "^2.6.10",
"core-js": "^2.6.11",
"express": "^4.16.4",
"grpc": "^1.24.2",
"hammerjs": "^2.0.8",
"ini": "^1.3.5",
"jsonwebtoken": "^8.4.0",
@ -48,11 +51,13 @@
"node-sass": "^4.13.0",
"nodemon": "^1.19.4",
"optimist": "^0.6.1",
"request-promise": "^4.2.2",
"request-promise": "^4.2.5",
"roboto-fontface": "^0.10.0",
"rxjs": "^6.5.3",
"rxjs-compat": "^6.5.3",
"sha256": "^0.2.0",
"socket.io": "^2.3.0",
"socket.io-client": "^2.3.0",
"tslib": "^1.9.0",
"upper-case": "^1.1.3",
"zone.js": "~0.9.1"
@ -65,7 +70,7 @@
"@types/jasmine": "~3.3.8",
"@types/jasminewd2": "^2.0.8",
"@types/node": "~8.9.4",
"codelyzer": "^5.2.0",
"codelyzer": "^5.2.1",
"jasmine-core": "~3.4.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.1.0",

@ -7,5 +7,6 @@ router.get("/rtlconf", RTLConfController.getRTLConfig);
router.post("/", authCheck, RTLConfController.updateUISettings);
router.get("/config/:nodeType", authCheck, RTLConfController.getConfig);
router.post("/updateSelNode", RTLConfController.updateSelectedNode);
router.post("/updateDefaultNode", RTLConfController.updateDefaultNode);
router.get("/rates", RTLConfController.getCurrencyRates);
module.exports = router;

@ -3,9 +3,10 @@ const express = require("express");
const router = express.Router();
const authCheck = require("../authCheck");
router.get("/", authCheck, ChannelsController.getChannels);
router.get("/", authCheck, ChannelsController.getAllChannels);
router.get("/pending", authCheck, ChannelsController.getPendingChannels);
router.get("/closed", authCheck, ChannelsController.getClosedChannels);
router.post("/", authCheck, ChannelsController.postChannel);
router.get("/:channelType", authCheck, ChannelsController.getChannels);
router.post("/transactions", authCheck, ChannelsController.postTransactions);
router.delete("/:channelPoint", authCheck, ChannelsController.closeChannel);
router.post("/chanPolicy", authCheck, ChannelsController.postChanPolicy);

@ -7,6 +7,7 @@ router.get("/", authCheck, graphController.getDescribeGraph);
router.get("/info", authCheck, graphController.getGraphInfo);
router.get("/node/:pubKey", authCheck, graphController.getGraphNode);
router.get("/edge/:chanid", authCheck, graphController.getGraphEdge);
router.get("/edge/:chanid/:localPubkey", authCheck, graphController.getRemoteFeePolicy);
router.get("/routes/:destPubkey/:amount", authCheck, graphController.getQueryRoutes);
module.exports = router;

@ -0,0 +1,9 @@
const LoopController = require("../../controllers/loopd/loop");
const express = require("express");
const router = express.Router();
const authCheck = require("../authCheck");
router.get("/loopin", authCheck, LoopController.loopIn);
router.get("/loopout", authCheck, LoopController.loopOut);
module.exports = router;

@ -3,6 +3,11 @@ const common = require("./common");
const debug = require("debug")("node-angular");
const http = require("http");
var connect = require('./connect').setServerConfiguration(); //Do NOT Remove
const grpc = require('grpc');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync('./controllers/loopd/client.proto', {keepCase: true, longs: String, enums: String, defaults: true, oneofs: true});
const looprpc = grpc.loadPackageDefinition(packageDefinition).looprpc;
const swapClient = new looprpc.SwapClient('localhost:11010', grpc.credentials.createInsecure());
const onError = error => {
if (error.syscall !== "listen") {
@ -35,6 +40,40 @@ const onListening = () => {
};
const server = http.createServer(app);
const io = require('socket.io')(server);
const loopMonitor = io.of('/loopMonitor').on('connection', (socket) => {
let i = 1;
let call = null;
socket.on('start', function() {
console.log('Application started subscription');
console.log(call);
var request = {};
call = swapClient.monitor(request);
call.on('data', function(response) {
console.log('Monitor sent message as: ' + JSON.stringify(response));
socket.emit('message', { message: response });
});
call.on('status', function(status) {
console.log('Monitor status: ' + JSON.stringify(status));
socket.emit('status', { message: status });
});
call.on('end', function() {
console.log('Monitor stopped streaming');
socket.emit('end');
});
interval = setInterval(() => {
socket.emit('message', { message: 'Message ' + i });
i++;
}, 15000);
});
socket.on('end', function() {
call = null;
clearInterval(interval);
console.log('Socket stopped subscription');
});
});
server.on("error", onError);
server.on("listening", onListening);
server.listen(common.port);

@ -17,36 +17,32 @@
"lndConfigPath": "<Optional:Path of the lnd.conf if present locally or empty>"
},
"Settings": {
"flgSidenavOpened": "true",
"flgSidenavPinned": "true",
"menu": "Vertical",
"menuType": "Regular",
"theme": "dark-blue",
"satsToBTC": "false",
"userPersona": "OPERATOR",
"themeMode": "DAY",
"themeColor": "PURPLE",
"channelBackupPath": "C:\\RTL\\backup\\node-1",
"bitcoindConfigPath": "<Optional: path of bitcoind.conf path if available locally>",
"enableLogging": "true",
"enableLogging": true,
"currencyUnit": "USD",
"lndServerUrl": "<Service url for LND REST APIs for node # 1 e.g. https://192.168.0.1:8080/v1"
}
},
{
"index": 2,
"lnNode": "LND Mainnet",
"lnImplementation": "LND",
"lnNode": "C Lighting Testnet",
"lnImplementation": "CLT",
"Authentication": {
"macaroonPath": "<Complete path of the folder containing admin.macaroon for the node # 2>"
},
"Settings": {
"flgSidenavOpened": "true",
"flgSidenavPinned": "true",
"menu": "Vertical",
"menuType": "Regular",
"theme": "light-teal",
"satsToBTC": "false",
"userPersona": "MERCHANT",
"themeMode": "NIGHT",
"themeColor": "TEAL",
"channelBackupPath": "C:\\RTL\\backup\\node-2",
"bitcoindConfigPath": "",
"enableLogging": "true",
"lndServerUrl": "<Service url for LND REST APIs for node # 2 e.g. https://192.168.0.6:8080/v1"
"enableLogging": true,
"currencyUnit": "GBP",
"lnServerUrl": "<Service url for C Lightning REST APIs for node # 2 e.g. https://192.168.0.2:3001/v1"
}
}
]

@ -5,17 +5,15 @@ lndConfigPath=
rtlPass=
[Settings]
flgSidenavOpened=true
flgSidenavPinned=true
menu=Vertical
menuType=Regular
theme=dark-blue
satsToBTC=false
userPersona=OPERATOR
themeMode=DAY
themeColor=PURPLE
channelBackupPath=C:\RTL\backup
bitcoindConfigPath=
enableLogging=true
port=3000
lndServerUrl=https://localhost:8080/v1
currencyUnit=USD
[SSO]
rtlSSO=0

@ -1,48 +1,51 @@
<div fxLayout="column" id="rtl-container" class="rtl-container" [ngClass]="settings.theme" [class.horizontal]="settings.menu === 'Horizontal'" [class.compact]="settings.menuType === 'Compact'" [class.mini]="settings.menuType === 'Mini'">
<!-- <div fxLayout="column" id="rtl-container" class="rtl-container" [ngClass]="[settings.themeColor, settings.themeMode]" [class.horizontal]="settings.menu === 'HORIZONTAL'" [class.compact]="settings.menuType === 'COMPACT'" [class.mini]="settings.menuType === 'MINI'" [style.fontSize.px]="getFontSize()"> -->
<div fxLayout="column" id="rtl-container" class="rtl-container" [ngClass]="[settings.themeColor | lowercase, settings.themeMode | lowercase, settings.fontSize | lowercase]" [class.horizontal]="settings.menu === 'HORIZONTAL'" [class.compact]="settings.menuType === 'COMPACT'" [class.mini]="settings.menuType === 'MINI'">
<mat-toolbar fxLayout="row" fxLayoutAlign="space-between center" class="padding-gap-x bg-primary rtl-top-toolbar" *ngIf="settings.menu === 'VERTICAL'">
<div>
<button *ngIf="settings.menu === 'VERTICAL'" class="top-toolbar-icon" mat-icon-button (click)="sideNavToggle()">
<mat-icon class="mr-5px">menu</mat-icon>
</button>
<button *ngIf="settings.fontSize === 'SMALL' && settings.menu === 'VERTICAL' && !smallScreen" mat-icon-button (click)="settings.flgSidenavPinned = !settings.flgSidenavPinned">
<svg class="top-toolbar-icon icon-pinned" viewBox="0 0 42 42">
<path fill="currentColor" *ngIf="!settings.flgSidenavPinned" d="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z" />
<path fill="currentColor" *ngIf="settings.flgSidenavPinned" d="M2,5.27L3.28,4L20,20.72L18.73,22L12.8,16.07V22H11.2V16H6V14L8,12V11.27L2,5.27M16,12L18,14V16H17.82L8,6.18V4H7V2H17V4H16V12Z" />
</svg>
</button>
<button *ngIf="settings.fontSize === 'MEDIUM' && settings.menu === 'VERTICAL' && !smallScreen" mat-icon-button (click)="settings.flgSidenavPinned = !settings.flgSidenavPinned">
<svg class="top-toolbar-icon icon-pinned" viewBox="0 0 32 32">
<path fill="currentColor" *ngIf="!settings.flgSidenavPinned" d="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z" />
<path fill="currentColor" *ngIf="settings.flgSidenavPinned" d="M2,5.27L3.28,4L20,20.72L18.73,22L12.8,16.07V22H11.2V16H6V14L8,12V11.27L2,5.27M16,12L18,14V16H17.82L8,6.18V4H7V2H17V4H16V12Z" />
</svg>
</button>
<button *ngIf="settings.fontSize === 'LARGE' && settings.menu === 'VERTICAL' && !smallScreen" mat-icon-button (click)="settings.flgSidenavPinned = !settings.flgSidenavPinned">
<svg class="top-toolbar-icon icon-pinned" viewBox="0 0 24 24">
<path fill="currentColor" *ngIf="!settings.flgSidenavPinned" d="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z" />
<path fill="currentColor" *ngIf="settings.flgSidenavPinned" d="M2,5.27L3.28,4L20,20.72L18.73,22L12.8,16.07V22H11.2V16H6V14L8,12V11.27L2,5.27M16,12L18,14V16H17.82L8,6.18V4H7V2H17V4H16V12Z" />
</svg>
</button>
</div>
<div>
<span *ngIf="xSmallScreen">RTL</span>
<span *ngIf="!xSmallScreen">Ride The Lightning</span>
</div>
<div>
<rtl-top-menu></rtl-top-menu>
</div>
</mat-toolbar>
<mat-toolbar color="primary" *ngIf="settings.menu === 'HORIZONTAL'" class="padding-gap-x horizontal-nav">
<rtl-horizontal-navigation fxLayout="row" fxFlex="100" fxLayoutAlign="start center" class="h-100"></rtl-horizontal-navigation>
</mat-toolbar>
<mat-sidenav-container>
<mat-sidenav perfectScrollbar *ngIf="settings.menu === 'Vertical'" [opened]="settings.flgSidenavOpened" [mode]="(settings.flgSidenavPinned) ? 'side' : 'over'"
#sideNavigation class="sidenav mat-elevation-z6 overflow-auto">
<rtl-side-navigation (ChildNavClicked)="onNavigationClicked($event)"></rtl-side-navigation>
<mat-sidenav perfectScrollbar *ngIf="settings.menu === 'VERTICAL'" [opened]="settings.flgSidenavOpened" [mode]="(settings.flgSidenavPinned && !smallScreen) ? 'side' : 'over'" #sideNavigation class="sidenav mat-elevation-z6">
<rtl-side-navigation (ChildNavClicked)="onNavigationClicked($event)" fxFlex="100"></rtl-side-navigation>
</mat-sidenav>
<mat-sidenav-content perfectScrollbar class="overflow-auto">
<mat-toolbar fxLayout="row" fxLayoutAlign="space-between center" color="primary" class="padding-gap-x">
<div fxLayoutAlign="center center">
<button *ngIf="settings.menu === 'Vertical'" mat-icon-button (click)="sideNavToggle()">
<mat-icon>menu</mat-icon>
</button>
</div>
<div>
<h2>Ride The Lightning <span class="font-60-percent">(Beta)</span></h2>
</div>
<div fxLayoutAlign="space-between center">
<rtl-top-menu></rtl-top-menu>
</div>
</mat-toolbar>
<div matTooltip="Copy public key" matTooltipPosition="above" fxLayout="row" fxLayoutAlign="center center" class="bg-primary flex-wrap pubkey-info-top" rtlClipboard [payload]="information?.identity_pubkey" (copied)="copiedText($event)">
<mat-icon [ngClass]="{'icon-smaller': smallScreen}">vpn_key</mat-icon>
<div [ngClass]="{'word-break font-9px': smallScreen, 'word-break': !smallScreen}">&nbsp;{{information?.identity_pubkey}}
<mat-spinner [diameter]="20" *ngIf="flgLoading[0]" class="inline-spinner foreground"></mat-spinner>
<fa-icon [icon]="faCopy"></fa-icon>
<span [hidden]="!flgCopied">Copied</span>
</div>
</div>
<mat-toolbar color="primary" *ngIf="settings.menu === 'Horizontal'" class="padding-gap-x horizontal-nav">
<div fxLayout="row" fxFlex="100" fxLayoutAlign="center center" class="h-100">
<rtl-horizontal-navigation></rtl-horizontal-navigation>
</div>
</mat-toolbar>
<mat-sidenav-content perfectScrollbar>
<div [ngClass]="{'mt-minus-1': smallScreen, 'inner-sidenav-content': true}">
<router-outlet></router-outlet>
</div>
<div fxLayout="row" fxLayoutAlign="center center" class="bg-primary settings-icon" (click)="settingSidenav.toggle()">
<mat-icon class="animate-settings">settings</mat-icon>
</div>
</mat-sidenav-content>
<mat-sidenav #settingSidenav position="end" class="settings mat-elevation-z6" mode="side">
<rtl-settings-nav (done)="settingSidenav.toggle()"></rtl-settings-nav>
</mat-sidenav>
</mat-sidenav-content>>
</mat-sidenav-container>
<div class="rtl-spinner" *ngIf="undefined === settings.theme">
<div class="rtl-spinner" *ngIf="undefined === settings.themeColor">
<mat-spinner color="accent"></mat-spinner>
<h4>Loading RTL...</h4>
</div>

@ -1,4 +1,4 @@
.inline-spinner {
display: inline-flex !important;
top: 0px !important;
top: 0 !important;
}

@ -1,8 +1,8 @@
import { Component, OnInit, AfterViewInit, OnDestroy, ViewChild, HostListener } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { Component, OnInit, AfterViewInit, OnDestroy, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout';
import { Subject } from 'rxjs';
import { takeUntil, filter } from 'rxjs/operators';
import { faCopy } from '@fortawesome/free-solid-svg-icons';
import { Store } from '@ngrx/store';
import { Actions } from '@ngrx/effects';
@ -10,7 +10,9 @@ import { UserIdleService } from 'angular-user-idle';
import * as sha256 from 'sha256';
import { LoggerService } from './shared/services/logger.service';
import { CommonService } from './shared/services/common.service';
import { SessionService } from './shared/services/session.service';
import { AlertTypeEnum, ScreenSizeEnum, NODE_SETTINGS } from './shared/services/consts-enums-functions';
import { RTLConfiguration, Settings, LightningNode, GetInfoRoot } from './shared/models/RTLconfig';
import * as RTLActions from './store/rtl.actions';
@ -23,8 +25,6 @@ import * as fromRTLReducer from './store/rtl.reducers';
})
export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('sideNavigation', { static: false }) sideNavigation: any;
@ViewChild('settingSidenav', { static: true }) settingSidenav: any;
faCopy = faCopy;
public selNode: LightningNode;
public settings: Settings;
public information: GetInfoRoot = {};
@ -32,38 +32,52 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
public flgCopied = false;
public appConfig: RTLConfiguration;
public accessKey = '';
public xSmallScreen = false;
public smallScreen = false;
unsubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject()];
unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.RTLState>, private actions$: Actions,
private userIdle: UserIdleService, private router: Router, private sessionService: SessionService) {}
constructor(private logger: LoggerService, private commonService: CommonService, private store: Store<fromRTLReducer.RTLState>, private actions$: Actions,
private userIdle: UserIdleService, private router: Router, private sessionService: SessionService, private breakpointObserver: BreakpointObserver) {}
ngOnInit() {
this.breakpointObserver.observe([Breakpoints.XSmall, Breakpoints.TabletPortrait, Breakpoints.Small, Breakpoints.Medium])
.pipe(takeUntil(this.unSubs[5]))
.subscribe((matches) => {
if(matches.breakpoints[Breakpoints.XSmall]) {
this.commonService.setScreenSize(ScreenSizeEnum.XS);
this.xSmallScreen = true;
this.smallScreen = true;
} else if(matches.breakpoints[Breakpoints.TabletPortrait]) {
this.commonService.setScreenSize(ScreenSizeEnum.SM);
this.xSmallScreen = false;
this.smallScreen = true;
} else if(matches.breakpoints[Breakpoints.Small] || matches.breakpoints[Breakpoints.Medium]) {
this.commonService.setScreenSize(ScreenSizeEnum.MD);
this.xSmallScreen = false;
this.smallScreen = false;
} else {
this.commonService.setScreenSize(ScreenSizeEnum.LG);
this.xSmallScreen = false;
this.smallScreen = false;
}
});
this.store.dispatch(new RTLActions.FetchRTLConfig());
this.accessKey = this.readAccessKey();
this.store.select('root')
.pipe(takeUntil(this.unsubs[0]))
.pipe(takeUntil(this.unSubs[0]))
.subscribe(rtlStore => {
this.selNode = rtlStore.selNode;
this.settings = this.selNode.settings;
this.appConfig = rtlStore.appConfig;
this.information = rtlStore.nodeData;
this.flgLoading[0] = (undefined !== this.information.identity_pubkey) ? false : true;
if (window.innerWidth <= 768) {
this.settings.menu = 'Vertical';
this.settings.flgSidenavOpened = false;
this.settings.flgSidenavPinned = false;
}
if (window.innerWidth <= 414) {
this.smallScreen = true;
}
this.logger.info(this.settings);
if (!this.sessionService.getItem('token')) {
this.flgLoading[0] = false;
}
});
this.actions$.pipe(takeUntil(this.unsubs[1]),
this.actions$.pipe(takeUntil(this.unSubs[1]),
filter((action) => action.type === RTLActions.SET_RTL_CONFIG))
.subscribe((action: (RTLActions.SetRTLConfig)) => {
if (action.type === RTLActions.SET_RTL_CONFIG) {
@ -74,28 +88,39 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
this.router.navigate([this.appConfig.sso.logoutRedirectLink]);
}
}
if (
this.settings.menu === 'Horizontal' ||
this.settings.menuType === 'Compact' ||
this.settings.menuType === 'Mini') {
this.settingSidenav.toggle(); // To dynamically update the width to 100% after side nav is closed
setTimeout(() => { this.settingSidenav.toggle(); }, 100);
// START: Workaround to add adjust container width initially
this.sideNavigation.toggle();
setTimeout(() => { this.sideNavigation.toggle(); }, 500);
if (this.settings.menuType === 'COMPACT' || this.settings.menuType === 'MINI') {
this.sideNavigation.toggle(); // To dynamically update the width to 100% after side nav is closed
setTimeout(() => { this.sideNavigation.toggle(); }, 100);
}
// END: Workaround to add left margin to container initially
}
});
this.userIdle.startWatching();
this.userIdle.onTimerStart().subscribe(count => {});
this.userIdle.onTimeout().subscribe(() => {
this.userIdle.onTimerStart().pipe(takeUntil(this.unSubs[2])).subscribe(count => {});
this.userIdle.onTimeout().pipe(takeUntil(this.unSubs[3])).subscribe(() => {
if (this.sessionService.getItem('token')) {
this.logger.warn('Time limit exceeded for session inactivity! Logging out!');
this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: {
type: 'WARN',
titleMessage: 'Time limit exceeded for session inactivity! Logging out!'
this.logger.warn('Time limit exceeded for session inactivity.');
this.store.dispatch(new RTLActions.OpenAlert({ data: {
type: AlertTypeEnum.WARNING,
alertTitle: 'Logging out',
titleMessage: 'Time limit exceeded for session inactivity.'
}}));
this.store.dispatch(new RTLActions.Signout());
this.userIdle.resetTimer();
}
});
this.commonService.containerWidthChanged.pipe(takeUntil(this.unSubs[4]))
.subscribe((fieldType: string) => {
if(fieldType !== 'menuType') {
this.sideNavToggle();
} else {
this.sideNavigation.toggle(); // To dynamically update the width to 100% after side nav is closed
setTimeout(() => { this.sideNavigation.toggle(); }, 0);
}
});
}
private readAccessKey() {
@ -104,31 +129,18 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
}
ngAfterViewInit() {
if (!this.settings.flgSidenavPinned) {
this.sideNavigation.close();
this.settingSidenav.toggle();
}
if (window.innerWidth <= 768) {
if ((this.settings.menuType !== 'REGULAR' || !this.settings.flgSidenavPinned) || (this.smallScreen)) {
this.sideNavigation.close();
this.settingSidenav.toggle();
}
}
@HostListener('window:resize')
public onWindowResize(): void {
if (window.innerWidth <= 768) {
this.settings.menu = 'Vertical';
this.settings.flgSidenavOpened = false;
this.settings.flgSidenavPinned = false;
}
}
sideNavToggle() {
this.settings.flgSidenavOpened = !this.settings.flgSidenavOpened;
this.sideNavigation.toggle();
}
onNavigationClicked(event: any) {
if (window.innerWidth <= 414) {
if (this.smallScreen) {
this.sideNavigation.close();
}
}
@ -139,8 +151,13 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
this.logger.info('Copied Text: ' + payload);
}
getFontSize() {
return (this.settings.fontSize === NODE_SETTINGS.fontSize[0].class) ? 14 :
(this.settings.fontSize === NODE_SETTINGS.fontSize[2].class) ? 18 : 16;
}
ngOnDestroy() {
this.unsubs.forEach(unsub => {
this.unSubs.forEach(unsub => {
unsub.next();
unsub.complete();
});

@ -9,14 +9,6 @@ import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { UserIdleModule } from 'angular-user-idle';
import { OverlayContainer } from '@angular/cdk/overlay';
import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar';
import { PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar';
import { PerfectScrollbarConfigInterface } from 'ngx-perfect-scrollbar';
const DEFAULT_PERFECT_SCROLLBAR_CONFIG: PerfectScrollbarConfigInterface = {
suppressScrollX: false
};
import { routing } from './app.routing';
import { SharedModule } from './shared/shared.module';
import { ThemeOverlay } from './shared/theme/overlay-container/theme-overlay';
@ -24,6 +16,7 @@ import { AppComponent } from './app.component';
import { environment } from '../environments/environment';
import { SessionService } from './shared/services/session.service';
import { CommonService } from './shared/services/common.service';
import { LoggerService, ConsoleLoggerService } from './shared/services/logger.service';
import { AuthGuard } from './shared/services/auth.guard';
import { AuthInterceptor } from './shared/services/auth.interceptor';
@ -32,18 +25,29 @@ import { RTLReducer } from './store/rtl.reducers';
import { RTLEffects } from './store/rtl.effects';
import { LNDEffects } from './lnd/store/lnd.effects';
import { CLEffects } from './clightning/store/cl.effects';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatCardModule } from '@angular/material/card';
import { MatMenuModule } from '@angular/material/menu';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { LayoutModule } from '@angular/cdk/layout';
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule,
SharedModule,
PerfectScrollbarModule,
routing,
UserIdleModule.forRoot({idle: 60 * 60, timeout: 1, ping: null}),
StoreModule.forRoot(RTLReducer),
EffectsModule.forRoot([RTLEffects, LNDEffects, CLEffects]),
!environment.production ? StoreDevtoolsModule.instrument() : []
!environment.production ? StoreDevtoolsModule.instrument() : [],
MatGridListModule,
MatCardModule,
MatMenuModule,
MatIconModule,
MatButtonModule,
LayoutModule
],
declarations: [
AppComponent
@ -51,9 +55,8 @@ import { CLEffects } from './clightning/store/cl.effects';
providers: [
{ provide: LoggerService, useClass: ConsoleLoggerService },
{ provide: OverlayContainer, useClass: ThemeOverlay },
{ provide: PERFECT_SCROLLBAR_CONFIG, useValue: DEFAULT_PERFECT_SCROLLBAR_CONFIG },
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
AuthGuard, SessionService
CommonService, AuthGuard, SessionService
],
bootstrap: [AppComponent]
})

@ -1,8 +1,8 @@
import { Routes, RouterModule } from '@angular/router';
import { ModuleWithProviders } from '@angular/core';
import { SettingsComponent } from './shared/components/settings/settings.component';
import { NotFoundComponent } from './shared/components/not-found/not-found.component';
import { ServerConfigComponent } from './shared/components/server-config/server-config.component';
import { HelpComponent } from './shared/components/help/help.component';
import { SigninComponent } from './shared/components/signin/signin.component';
import { ErrorComponent } from './shared/components/error/error.component';
@ -11,7 +11,7 @@ import { AuthGuard } from './shared/services/auth.guard';
export const routes: Routes = [
{ path: 'lnd', loadChildren: () => import('./lnd/lnd.module').then(childModule => childModule.LNDModule), canActivate: [AuthGuard] },
{ path: 'cl', loadChildren: () => import('./clightning/cl.module').then(childModule => childModule.CLModule), canActivate: [AuthGuard] },
{ path: 'sconfig', component: ServerConfigComponent, canActivate: [AuthGuard] },
{ path: 'settings', component: SettingsComponent, canActivate: [AuthGuard] },
{ path: 'help', component: HelpComponent },
{ path: 'login', component: SigninComponent },
{ path: 'error', component: ErrorComponent },

@ -1,136 +0,0 @@
<div fxLayout="column">
<div class="padding-gap">
<mat-card [ngClass]="{'flip': redirectedWithPeer}">
<mat-card-header>
<mat-card-subtitle>
<h2>Open Channel</h2>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<form fxLayout="column" fxLayout.gt-sm="row wrap"
(ngSubmit)="openChannelForm.form.valid && onOpenChannel(openChannelForm)" #openChannelForm="ngForm">
<mat-form-field fxFlex="40" fxLayoutAlign="start end">
<mat-select [(ngModel)]="selectedPeer" placeholder="Alias" name="peer_alias" tabindex="1" required
name="selPeer" #selPeer="ngModel">
<mat-option *ngFor="let peer of peers" [value]="peer.id">
{{peer.alias}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="25" fxLayoutAlign="start end">
<input matInput [(ngModel)]="fundingAmount" placeholder="Amount (mSats)" type="number" step="1000" min="1"
tabindex="2" required name="amount" #amount="ngModel">
<mat-hint>(Wallet Bal: {{totalBalance}}, Remaining Bal:
{{totalBalance - ((fundingAmount) ? fundingAmount : 0)}})</mat-hint>
</mat-form-field>
<div fxFlex="15" tabindex="3" fxLayoutAlign="start center" class="chkbox-options">
<mat-checkbox [(ngModel)]="moreOptions" name="moreOptions" (change)="onMoreOptionsChange($event)">Options
</mat-checkbox>
</div>
<span *ngIf="moreOptions" fxLayout="column" fxLayout.gt-sm="row wrap" fxFlex="80"
fxLayoutAlign.gt-sm="start center">
<mat-form-field fxFlex="25" fxLayoutAlign="start end">
<mat-select tabindex="4" placeholder="Fee Rate" [(value)]="selFeeRate">
<mat-option *ngFor="let feeRateType of feeRateTypes" [value]="feeRateType.feeRateId">
{{feeRateType.feeRateType}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox fxFlex="25" fxFlex.lt-lg="35" tabindex="5" [(ngModel)]="flgMinConf" name="flgMinConf">
<mat-form-field fxFlex="100">
<input matInput [(ngModel)]="minConfValue" placeholder="Min Confirmation Blocks" type="number"
name="blocks" step="1" min="0" tabindex="6" #blocks="ngModel" [required]="flgMinConf"
[disabled]="!flgMinConf">
</mat-form-field>
</mat-checkbox>
</span>
<div fxFlex="10" fxLayoutAlign="end start">
<button fxFlex="90" fxLayoutAlign="center center" mat-raised-button color="primary"
[disabled]="selectedPeer === '' || fundingAmount == null || (totalBalance - ((fundingAmount) ? fundingAmount : 0) < 0) || !openChannelForm.valid"
type="submit" tabindex="7">
<p
*ngIf="(selectedPeer === '' || fundingAmount == null) && (selPeer.touched || selPeer.dirty) && (amount.touched || amount.dirty); else openText">
Invalid Values</p>
<ng-template #openText>
<p>Open</p>
</ng-template>
</button>
</div>
<div fxFlex="10" fxLayoutAlign="end start">
<button fxFlex="90" fxLayoutAlign="center center" mat-raised-button color="accent" tabindex="9" type="reset"
(click)="resetData()">Clear</button>
</div>
</form>
</mat-card-content>
</mat-card>
</div>
<div class="padding-gap">
<mat-card>
<mat-card-content fxFlex="100" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start start">
<mat-form-field fxFlex="30">
<input matInput (keyup)="applyFilter()" [(ngModel)]="selFilter" name="filter" placeholder="Filter">
</mat-form-field>
</div>
<div perfectScrollbar class="table-container mat-elevation-z8">
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
<table mat-table #table [dataSource]="channels" matSort
[ngClass]="{'mat-elevation-z8 overflow-auto error-border': flgLoading[0]==='error','mat-elevation-z8 overflow-auto': true}">
<ng-container matColumnDef="close">
<th mat-header-cell *matHeaderCellDef> Close Channel </th>
<td mat-cell *matCellDef="let channel">
<mat-icon color="accent" (click)="onChannelClose(channel)">link_off</mat-icon>
</td>
</ng-container>
<ng-container matColumnDef="update">
<th mat-header-cell *matHeaderCellDef>
<mat-icon color="accent" (click)="onChannelUpdate('all')">edit</mat-icon>
</th>
<td mat-cell *matCellDef="let channel">
<mat-icon color="accent" (click)="onChannelUpdate(channel)">edit</mat-icon>
</td>
</ng-container>
<ng-container matColumnDef="short_channel_id">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Short Channel ID </th>
<td mat-cell *matCellDef="let channel"> {{channel?.short_channel_id}}</td>
</ng-container>
<ng-container matColumnDef="alias">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Alias </th>
<td mat-cell *matCellDef="let channel">{{channel?.alias}}</td>
</ng-container>
<ng-container matColumnDef="connected">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Connected </th>
<td mat-cell *matCellDef="let channel"> {{(channel?.connected) ? 'Connected' : 'Disconnected'}} </td>
</ng-container>
<ng-container matColumnDef="private">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Private </th>
<td mat-cell *matCellDef="let channel"> {{(channel?.private ? 'Private' : 'Public')}} </td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef mat-sort-header> State </th>
<td mat-cell *matCellDef="let channel"> {{channel?.state}}</td>
</ng-container>
<ng-container matColumnDef="msatoshi_to_us">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> mSatoshi To Us </th>
<td mat-cell *matCellDef="let channel"><span fxLayoutAlign="end center">
{{channel?.msatoshi_to_us | number}} </span></td>
</ng-container>
<ng-container matColumnDef="msatoshi_total">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> mSatoshi Total </th>
<td mat-cell *matCellDef="let channel"><span fxLayoutAlign="end center">
{{channel?.msatoshi_total | number}} </span></td>
</ng-container>
<ng-container matColumnDef="spendable_msatoshi">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Spendable Satoshi </th>
<td mat-cell *matCellDef="let channel"><span fxLayoutAlign="end center">
{{channel?.spendable_msatoshi | number}} </span></td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" (click)="onChannelClick(row, $event)"
[ngClass]="{'row-disabled': row.state === 'ONCHAIN'}"></tr>
</table>
</div>
</mat-card-content>
</mat-card>
</div>
</div>

@ -1,271 +0,0 @@
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { MatTableDataSource, MatSort } from '@angular/material';
import { ChannelCL, PeerCL, GetInfoCL, ChannelEdgeCL } from '../../shared/models/clModels';
import { LoggerService } from '../../shared/services/logger.service';
import { CLEffects } from '../store/cl.effects';
import { RTLEffects } from '../../store/rtl.effects';
import * as RTLActions from '../../store/rtl.actions';
import * as fromRTLReducer from '../../store/rtl.reducers';
@Component({
selector: 'rtl-cl-channels',
templateUrl: './channels.component.html',
styleUrls: ['./channels.component.scss']
})
export class CLChannelsComponent implements OnInit, OnDestroy {
@ViewChild(MatSort, { static: true }) sort: MatSort;
public totalBalance = 0;
public selectedPeer = '';
public fundingAmount: number;
public displayedColumns = [];
public channels: any;
public peers: PeerCL[] = [];
public information: GetInfoCL = {};
public flgLoading: Array<Boolean | 'error'> = [true];
public selectedFilter = '';
public myChanPolicy: any = {};
public selFilter = '';
public feeRateTypes = [];
public selFeeRate = '';
public isPrivate = false;
public flgMinConf = false;
public minConfValue = null;
public moreOptions = false;
public flgSticky = false;
public redirectedWithPeer = false;
private unsub: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.RTLState>, private rtlEffects: RTLEffects, private clEffects: CLEffects, private activatedRoute: ActivatedRoute) {
switch (true) {
case (window.innerWidth <= 415):
this.displayedColumns = ['close', 'update', 'short_channel_id', 'state', 'msatoshi_total'];
break;
case (window.innerWidth > 415 && window.innerWidth <= 730):
this.displayedColumns = ['close', 'update', 'short_channel_id', 'state', 'msatoshi_to_us', 'msatoshi_total', 'spendable_msatoshi'];
break;
case (window.innerWidth > 730 && window.innerWidth <= 1024):
this.displayedColumns = [
'close', 'update', 'short_channel_id', 'alias', 'connected', 'private', 'state', 'msatoshi_to_us', 'msatoshi_total', 'spendable_msatoshi'
];
break;
case (window.innerWidth > 1024 && window.innerWidth <= 1280):
this.flgSticky = true;
this.displayedColumns = [
'close', 'update', 'short_channel_id', 'alias', 'connected', 'private', 'state', 'msatoshi_to_us', 'msatoshi_total', 'spendable_msatoshi'
];
break;
default:
this.flgSticky = true;
this.displayedColumns = [
'close', 'update', 'short_channel_id', 'alias', 'connected', 'private', 'state', 'msatoshi_to_us', 'msatoshi_total', 'spendable_msatoshi'
];
break;
}
}
ngOnInit() {
this.store.dispatch(new RTLActions.FetchChannelsCL());
this.store.select('cl')
.pipe(takeUntil(this.unsub[0]))
.subscribe((rtlStore) => {
rtlStore.effectErrorsCl.forEach(effectsErr => {
if (effectsErr.action === 'FetchChannelsCL') {
this.flgLoading[0] = 'error';
}
});
this.information = rtlStore.information;
this.feeRateTypes = rtlStore.feeRateTypes;
this.peers = rtlStore.peers;
this.peers.forEach(peer => {
if (undefined === peer.alias || peer.alias === '') {
peer.alias = peer.id.substring(0, 15) + '...';
}
});
this.totalBalance = +rtlStore.balance.totalBalance;
if (undefined !== rtlStore.allChannels) {
this.loadChannelsTable(rtlStore.allChannels);
}
if (this.flgLoading[0] !== 'error') {
this.flgLoading[0] = (undefined !== rtlStore.allChannels) ? false : true;
}
this.logger.info(rtlStore);
});
this.activatedRoute.paramMap.subscribe(() => {
this.selectedPeer = window.history.state.peer;
this.redirectedWithPeer = (window.history.state.peer) ? true : false;
});
}
onOpenChannel(form: any) {
this.store.dispatch(new RTLActions.OpenSpinner('Opening Channel...'));
this.store.dispatch(new RTLActions.SaveNewChannelCL({
peerId: this.selectedPeer, satoshis: this.fundingAmount, announce: !this.isPrivate, feeRate: this.selFeeRate, minconf: this.flgMinConf ? this.minConfValue : null
}));
}
onChannelUpdate(channelToUpdate: ChannelCL | 'all') {
if(channelToUpdate !== 'all' && channelToUpdate.state === 'ONCHAIN') {
return;
}
if (channelToUpdate === 'all') {
const titleMsg = 'Updated Values for ALL Channels';
const confirmationMsg = {};
this.store.dispatch(new RTLActions.OpenConfirmation({ width: '70%', data: {
type: 'CONFIRM', titleMessage: titleMsg, noBtnText: 'Cancel', yesBtnText: 'Update', message: JSON.stringify(confirmationMsg), flgShowInput: true, getInputs: [
{placeholder: 'Base Fee msat', inputType: 'number', inputValue: 1000},
{placeholder: 'Fee Rate mili msat', inputType: 'number', inputValue: 1, min: 1}
]
}}));
this.rtlEffects.closeConfirm
.pipe(takeUntil(this.unsub[2]))
.subscribe(confirmRes => {
if (confirmRes) {
const base_fee = confirmRes[0].inputValue;
const fee_rate = confirmRes[1].inputValue;
this.store.dispatch(new RTLActions.OpenSpinner('Updating Channel Policy...'));
this.store.dispatch(new RTLActions.UpdateChannelsCL({baseFeeMsat: base_fee, feeRate: fee_rate, channelId: 'all'}));
}
});
} else {
this.myChanPolicy = {fee_base_msat: 0, fee_rate_milli_msat: 0};
this.store.dispatch(new RTLActions.OpenSpinner('Fetching Channel Policy...'));
this.store.dispatch(new RTLActions.ChannelLookupCL(channelToUpdate.short_channel_id));
this.clEffects.setLookupCL
.pipe(takeUntil(this.unsub[3]))
.subscribe((resLookup: ChannelEdgeCL[]) => {
this.logger.info(resLookup);
if (resLookup.length > 0 && resLookup[0].source === this.information.id) {
this.myChanPolicy = {fee_base_msat: resLookup[0].base_fee_millisatoshi, fee_rate_milli_msat: resLookup[0].fee_per_millionth};
} else if (resLookup.length > 1 && resLookup[1].source === this.information.id) {
this.myChanPolicy = {fee_base_msat: resLookup[1].base_fee_millisatoshi, fee_rate_milli_msat: resLookup[1].fee_per_millionth};
} else {
this.myChanPolicy = {fee_base_msat: 0, fee_rate_milli_msat: 0};
}
this.logger.info(this.myChanPolicy);
this.store.dispatch(new RTLActions.CloseSpinner());
const titleMsg = 'Updated Values for Channel: ' + channelToUpdate.channel_id;
const confirmationMsg = {};
this.store.dispatch(new RTLActions.OpenConfirmation({ width: '70%', data: {
type: 'CONFIRM', titleMessage: titleMsg, noBtnText: 'Cancel', yesBtnText: 'Update', message: JSON.stringify(confirmationMsg), flgShowInput: true, getInputs: [
{placeholder: 'Base Fee msat', inputType: 'number', inputValue: (this.myChanPolicy.fee_base_msat === '') ? 0 : this.myChanPolicy.fee_base_msat},
{placeholder: 'Fee Rate mili msat', inputType: 'number', inputValue: this.myChanPolicy.fee_rate_milli_msat, min: 1}
]
}}));
});
this.rtlEffects.closeConfirm
.pipe(takeUntil(this.unsub[2]))
.subscribe(confirmRes => {
if (confirmRes) {
const base_fee = confirmRes[0].inputValue;
const fee_rate = confirmRes[1].inputValue;
this.store.dispatch(new RTLActions.OpenSpinner('Updating Channel Policy...'));
this.store.dispatch(new RTLActions.UpdateChannelsCL({baseFeeMsat: base_fee, feeRate: fee_rate, channelId: channelToUpdate.channel_id}));
}
});
}
this.applyFilter();
}
onChannelClose(channelToClose: ChannelCL) {
if(channelToClose.state === 'ONCHAIN') {
return;
}
if (channelToClose.state === 'AWAITING_UNILATERAL') {
this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: {
type: 'WARN',
titleMessage: 'Channel can not be closed when it is in AWAITING UNILATERAL state.'
}}));
} else {
this.store.dispatch(new RTLActions.OpenConfirmation({
width: '70%', data: { type: 'CONFIRM', titleMessage: 'Closing channel: ' + channelToClose.channel_id, noBtnText: 'Cancel', yesBtnText: 'Close Channel'
}}));
this.rtlEffects.closeConfirm
.pipe(takeUntil(this.unsub[1]))
.subscribe(confirmRes => {
if (confirmRes) {
this.store.dispatch(new RTLActions.OpenSpinner('Closing Channel...'));
this.store.dispatch(new RTLActions.CloseChannelCL({channelId: channelToClose.channel_id}));
}
});
}
}
applyFilter() {
this.selectedFilter = this.selFilter;
this.channels.filter = this.selFilter;
}
onChannelClick(selRow: ChannelCL, event: any) {
const flgCloseClicked =
event.target.className.includes('mat-column-close')
|| event.target.className.includes('mat-column-update')
|| event.target.className.includes('mat-icon');
if (flgCloseClicked) {
return;
}
const selChannel = this.channels.data.filter(channel => {
return channel.channel_id === selRow.channel_id;
})[0];
const reorderedChannel = JSON.parse(JSON.stringify(selChannel, [
'channel_id', 'short_channel_id', 'id', 'alias', 'connected', 'private', 'state', 'funding_txid', 'msatoshi_to_us', 'msatoshi_total', 'their_channel_reserve_satoshis', 'our_channel_reserve_satoshis', 'spendable_msatoshi'
] , 2));
this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: {
type: 'INFO',
message: JSON.stringify(reorderedChannel)
}}));
}
loadChannelsTable(channels) {
channels.sort(function(a, b) {
return (a && a.active === b.active) ? 0 : ((b.active) ? 1 : -1);
});
this.channels = new MatTableDataSource<ChannelCL>([...channels]);
this.channels.sort = this.sort;
this.channels.filterPredicate = (channel: ChannelCL, fltr: string) => {
const newChannel = ((channel.connected) ? 'connected' : 'disconnected') + (channel.channel_id ? channel.channel_id : '') +
(channel.short_channel_id ? channel.short_channel_id : '') + (channel.id ? channel.id : '') + (channel.alias ? channel.alias : '') +
(channel.private ? 'private' : 'public') + (channel.state ? channel.state.toLowerCase() : '') +
(channel.funding_txid ? channel.funding_txid : '') + (channel.msatoshi_to_us ? channel.msatoshi_to_us : '') +
(channel.msatoshi_total ? channel.msatoshi_total : '') + (channel.their_channel_reserve_satoshis ? channel.their_channel_reserve_satoshis : '') +
(channel.our_channel_reserve_satoshis ? channel.our_channel_reserve_satoshis : '') + (channel.spendable_msatoshi ? channel.spendable_msatoshi : '');
return newChannel.includes(fltr.toLowerCase());
};
this.logger.info(this.channels);
}
resetData() {
this.selectedPeer = '';
this.fundingAmount = 0;
this.moreOptions = false;
this.flgMinConf = false;
this.isPrivate = false;
this.selFeeRate = '';
this.minConfValue = null;
this.redirectedWithPeer = false;
}
onMoreOptionsChange(event: any) {
if (!event.checked) {
this.flgMinConf = false;
this.isPrivate = false;
this.selFeeRate = '';
this.minConfValue = null;
}
}
ngOnDestroy() {
this.unsub.forEach(completeSub => {
completeSub.next();
completeSub.complete();
});
}
}

@ -1 +1,2 @@
<router-outlet></router-outlet>
<mat-progress-bar *ngIf="loading" color="primary" mode="indeterminate"></mat-progress-bar>
<router-outlet *ngIf="!loading"></router-outlet>

@ -1,4 +1,5 @@
import { Component } from '@angular/core';
import { Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router } from '@angular/router';
@Component({
selector: 'rtl-cl-root',
@ -6,5 +7,25 @@ import { Component } from '@angular/core';
styleUrls: ['./cl-root.component.scss']
})
export class CLRootComponent {
constructor() {}
loading = false;
constructor(private router: Router) {
this.router.events.subscribe((event: Event) => {
switch (true) {
case event instanceof NavigationStart: {
this.loading = true;
break;
}
case event instanceof NavigationEnd:
case event instanceof NavigationCancel:
case event instanceof NavigationError: {
this.loading = false;
break;
}
default: {
break;
}
}
});
}
}

@ -6,20 +6,34 @@ import { SharedModule } from '../shared/shared.module';
import { CLRootComponent } from './cl-root.component';
import { CLHomeComponent } from './home/home.component';
import { CLFeeRatesComponent } from './home/fee-rates/fee-rates.component';
import { CLChannelsComponent } from './channels/channels.component';
import { CLInvoicesComponent } from './invoices/invoices.component';
import { CLPeersChannelsComponent } from './peers-channels/peers-channels.component';
import { CLChannelsTablesComponent } from './peers-channels/channels/channels-tables/channels-tables.component';
import { CLPeersComponent } from './peers-channels/peers/peers.component';
import { CLLightningInvoicesComponent } from './transactions/invoices/lightning-invoices.component';
import { CLOnChainSendComponent } from './on-chain/on-chain-send/on-chain-send.component';
import { CLOnChainReceiveComponent } from './on-chain/on-chain-receive/on-chain-receive.component';
import { CLOnChainComponent } from './on-chain/on-chain.component';
import { CLLightningPaymentsComponent } from './transactions/payments/lightning-payments.component';
import { CLChannelManageComponent } from './peers-channels/channels/channel-manage/channel-manage.component';
import { CLTransactionsComponent } from './transactions/transactions.component';
import { CLLookupsComponent } from './lookups/lookups.component';
import { CLRoutingComponent } from './routing/routing.component';
import { CLForwardingHistoryComponent } from './routing/forwarding-history/forwarding-history.component';
import { CLFailedTransactionsComponent } from './routing/failed-transactions/failed-transactions.component';
import { CLChannelLookupComponent } from './lookups/channel-lookup/channel-lookup.component';
import { CLNodeLookupComponent } from './lookups/node-lookup/node-lookup.component';
import { CLOnChainComponent } from './on-chain/on-chain.component';
import { CLQueryRoutesComponent } from './payments/query-routes/query-routes.component';
import { CLPaymentsComponent } from './payments/send-receive/payments.component';
import { CLPeersComponent } from './peers/peers.component';
import { CLForwardingHistoryComponent } from './forwarding-history/forwarding-history.component';
import { CLQueryRoutesComponent } from './transactions/query-routes/query-routes.component';
import { CLChannelOpenTableComponent } from './peers-channels/channels/channels-tables/channel-open-table/channel-open-table.component';
import { CLChannelPendingTableComponent } from './peers-channels/channels/channels-tables/channel-pending-table/channel-pending-table.component';
import { CLNodeInfoComponent } from './home/node-info/node-info.component';
import { CLBalancesInfoComponent } from './home/balances-info/balances-info.component';
import { CLFeeInfoComponent } from './home/fee-info/fee-info.component';
import { CLChannelStatusInfoComponent } from './home/channel-status-info/channel-status-info.component';
import { CLChannelCapacityInfoComponent } from './home/channel-capacity-info/channel-capacity-info.component';
import { CLChannelLiquidityInfoComponent } from './home/channel-liquidity-info/channel-liquidity-info.component';
import { CLNetworkInfoComponent } from './network-info/network-info.component';
import { CLFeeRatesComponent } from './network-info/fee-rates/fee-rates.component';
import { CommonService } from '../shared/services/common.service';
import { LoggerService, ConsoleLoggerService } from '../shared/services/logger.service';
import { CLUnlockedGuard } from '../shared/services/auth.guard';
@NgModule({
@ -31,22 +45,36 @@ import { CLUnlockedGuard } from '../shared/services/auth.guard';
declarations: [
CLRootComponent,
CLHomeComponent,
CLChannelsComponent,
CLInvoicesComponent,
CLPeersComponent,
CLPeersChannelsComponent,
CLLightningInvoicesComponent,
CLLightningPaymentsComponent,
CLChannelManageComponent,
CLTransactionsComponent,
CLLookupsComponent,
CLRoutingComponent,
CLForwardingHistoryComponent,
CLFailedTransactionsComponent,
CLChannelLookupComponent,
CLNodeLookupComponent,
CLOnChainComponent,
CLQueryRoutesComponent,
CLPaymentsComponent,
CLPeersComponent,
CLForwardingHistoryComponent,
CLOnChainSendComponent,
CLOnChainReceiveComponent,
CLOnChainComponent,
CLChannelsTablesComponent,
CLChannelOpenTableComponent,
CLChannelPendingTableComponent,
CLNodeInfoComponent,
CLBalancesInfoComponent,
CLFeeInfoComponent,
CLChannelStatusInfoComponent,
CLChannelCapacityInfoComponent,
CLChannelLiquidityInfoComponent,
CLNetworkInfoComponent,
CLFeeRatesComponent
],
providers: [
{ provide: LoggerService, useClass: ConsoleLoggerService },
CLUnlockedGuard,
CommonService
CLUnlockedGuard
],
bootstrap: [CLRootComponent]
})

@ -3,15 +3,12 @@ import { ModuleWithProviders } from '@angular/core';
import { CLRootComponent } from './cl-root.component';
import { CLHomeComponent } from './home/home.component';
import { CLChannelsComponent } from './channels/channels.component';
import { CLInvoicesComponent } from './invoices/invoices.component';
import { CLLookupsComponent } from './lookups/lookups.component';
import { CLOnChainComponent } from './on-chain/on-chain.component';
import { CLQueryRoutesComponent } from './payments/query-routes/query-routes.component';
import { CLPaymentsComponent } from './payments/send-receive/payments.component';
import { CLPeersComponent } from './peers/peers.component';
import { CLForwardingHistoryComponent } from './forwarding-history/forwarding-history.component';
import { CLPeersChannelsComponent } from '../clightning/peers-channels/peers-channels.component';
import { CLTransactionsComponent } from '../clightning/transactions/transactions.component';
import { CLRoutingComponent } from '../clightning/routing/routing.component';
import { CLLookupsComponent } from './lookups/lookups.component';
import { CLNetworkInfoComponent } from './network-info/network-info.component';
import { CLUnlockedGuard } from '../shared/services/auth.guard';
import { NotFoundComponent } from '../shared/components/not-found/not-found.component';
@ -19,23 +16,16 @@ export const ClRoutes: Routes = [
{ path: '', component: CLRootComponent,
children: [
{ path: 'home', component: CLHomeComponent, canActivate: [CLUnlockedGuard] },
{ path: 'peers', component: CLPeersComponent, canActivate: [CLUnlockedGuard] },
{ path: 'chnlmanage', component: CLChannelsComponent, canActivate: [CLUnlockedGuard] },
{ path: 'onchain', component: CLOnChainComponent, canActivate: [CLUnlockedGuard] },
{ path: 'paymentsend', component: CLPaymentsComponent, canActivate: [CLUnlockedGuard] },
{ path: 'queryroutes', component: CLQueryRoutesComponent, canActivate: [CLUnlockedGuard] },
{ path: 'invoices', component: CLInvoicesComponent, canActivate: [CLUnlockedGuard] },
{ path: 'forwardinghistory', component: CLForwardingHistoryComponent, canActivate: [CLUnlockedGuard] },
{ path: 'peerschannels', component: CLPeersChannelsComponent, canActivate: [CLUnlockedGuard] },
{ path: 'transactions', component: CLTransactionsComponent, canActivate: [CLUnlockedGuard] },
{ path: 'routing', component: CLRoutingComponent, canActivate: [CLUnlockedGuard] },
{ path: 'lookups', component: CLLookupsComponent, canActivate: [CLUnlockedGuard] },
{ path: 'unlocklnd', redirectTo: 'home' },
{ path: 'chnlclosed', redirectTo: 'chnlmanage' },
{ path: 'chnlpending', redirectTo: 'chnlmanage' },
{ path: 'chnlbackup', redirectTo: 'chnlmanage' },
{ path: 'transsendreceive', redirectTo: 'onchain' },
{ path: 'translist', redirectTo: 'onchain' },
{ path: 'switch', redirectTo: 'forwardinghistory' },
{ path: 'routingpeers', redirectTo: 'home' },
{ path: '**', component: NotFoundComponent }
{ path: 'rates', component: CLNetworkInfoComponent, canActivate: [CLUnlockedGuard] },
{ path: '**', component: NotFoundComponent },
{ path: 'network', redirectTo: 'rates' },
{ path: 'wallet', redirectTo: 'home' },
{ path: 'backup', redirectTo: 'home' }
]}
];

@ -1,95 +0,0 @@
<div fxLayout="column">
<!-- <div fxFlex="100" class="padding-gap">
<mat-card>
<mat-card-header>
<mat-card-subtitle>
<h2>Forwarding History</h2>
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<form fxLayout="column" fxLayoutAlign="space-between stretch" fxLayout.gt-md="row wrap"
(ngSubmit)="fhForm.form.valid && onForwardingHistoryFetch()" #fhForm="ngForm" class="padding-gap">
<div fxFlex="69" fxLayoutAlign="space-between stretch">
<mat-form-field fxFlex="49" fxLayoutAlign="start">
<input matInput [matDatepicker]="startDatepicker" placeholder="Start Date" [max]="yesterday"
name="startDate" [(ngModel)]="startDate" tabindex="1" #strtDate="ngModel">
<mat-datepicker-toggle matSuffix [for]="startDatepicker"></mat-datepicker-toggle>
<mat-datepicker #startDatepicker [startAt]="startDate"></mat-datepicker>
</mat-form-field>
<mat-form-field fxFlex="49" fxLayoutAlign="start">
<input matInput [matDatepicker]="endDatepicker" [max]="today" placeholder="End Date" name="endDate"
[(ngModel)]="endDate" tabindex="2" #enDate="ngModel">
<mat-datepicker-toggle matSuffix [for]="endDatepicker"></mat-datepicker-toggle>
<mat-datepicker #endDatepicker [startAt]="endDate"></mat-datepicker>
</mat-form-field>
</div>
<div fxFlex="30" fxLayoutAlign="space-between stretch">
<button fxFlex="50" fxLayoutAlign="center center" mat-raised-button color="primary"
[disabled]="fhForm.invalid" type="submit" tabindex="3">Fetch</button>
<button fxFlex="50" fxLayoutAlign="center center" mat-raised-button color="accent" class="ml-2" tabindex="4"
type="reset" (click)="resetData()">Clear</button>
</div>
</form>
</mat-card-content>
</mat-card>
</div> -->
<div class="padding-gap">
<mat-card>
<mat-card-content class="table-card-content">
<div fxLayout="row" fxLayoutAlign="start start">
<mat-form-field fxFlex="30">
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
</mat-form-field>
</div>
<div perfectScrollbar class="table-container mat-elevation-z8">
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
<table mat-table #table [dataSource]="forwardingHistoryEvents" matSort
[ngClass]="{'mat-elevation-z8 overflow-auto error-border': flgLoading[0]==='error','mat-elevation-z8 overflow-auto': true}">
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Status</th>
<td mat-cell *matCellDef="let fhEvent">{{fhEvent?.status}}</td>
</ng-container>
<ng-container matColumnDef="received_time_str">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Received Time</th>
<td mat-cell *matCellDef="let fhEvent">{{fhEvent?.received_time_str}}</td>
</ng-container>
<ng-container matColumnDef="resolved_time_str">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Resolved Time</th>
<td mat-cell *matCellDef="let fhEvent">{{fhEvent?.resolved_time_str}}</td>
</ng-container>
<ng-container matColumnDef="in_channel">
<th mat-header-cell *matHeaderCellDef mat-sort-header>In Channel</th>
<td mat-cell *matCellDef="let fhEvent">{{fhEvent?.in_channel}}</td>
</ng-container>
<ng-container matColumnDef="out_channel">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Out Channel</th>
<td mat-cell *matCellDef="let fhEvent">{{fhEvent?.out_channel}}</td>
</ng-container>
<ng-container matColumnDef="in_msatoshi">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">In mSatoshi</th>
<td mat-cell *matCellDef="let fhEvent"><span fxLayoutAlign="end center">{{fhEvent?.in_msatoshi | number}}</span></td>
</ng-container>
<ng-container matColumnDef="out_msatoshi">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Out mSatoshi</th>
<td mat-cell *matCellDef="let fhEvent"><span fxLayoutAlign="end center">{{fhEvent?.out_msatoshi | number}}</span></td>
</ng-container>
<ng-container matColumnDef="fee">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Fee mSat</th>
<td mat-cell *matCellDef="let fhEvent"><span fxLayoutAlign="end center">{{fhEvent?.fee | number}}</span></td>
</ng-container>
<ng-container matColumnDef="payment_hash">
<th mat-header-cell class="pl-4" *matHeaderCellDef mat-sort-header>Payment Hash</th>
<td mat-cell class="pl-4" *matCellDef="let fhEvent">
<div>{{fhEvent?.payment_hash | slice:0:10}}...</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"
(click)="onForwardingEventClick(row, $event)"></tr>
</table>
</div>
</mat-card-content>
</mat-card>
</div>
</div>

@ -1,136 +0,0 @@
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { formatDate } from '@angular/common';
import { Subject } from 'rxjs';
import { takeUntil, filter } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { Actions } from '@ngrx/effects';
import { MatTableDataSource, MatSort } from '@angular/material';
import { ForwardingEventCL } from '../../shared/models/clModels';
import { LoggerService } from '../../shared/services/logger.service';
import * as RTLActions from '../../store/rtl.actions';
import * as fromRTLReducer from '../../store/rtl.reducers';
@Component({
selector: 'rtl-cl-forwarding-history',
templateUrl: './forwarding-history.component.html',
styleUrls: ['./forwarding-history.component.scss']
})
export class CLForwardingHistoryComponent implements OnInit, OnDestroy {
@ViewChild(MatSort, { static: true }) sort: MatSort;
public displayedColumns = [];
public forwardingHistoryEvents: any;
public lastOffsetIndex = 0;
public flgLoading: Array<Boolean | 'error'> = [true];
public today = new Date(Date.now());
public yesterday = new Date(this.today.getFullYear(), this.today.getMonth(), this.today.getDate() - 1, this.today.getHours(), this.today.getMinutes(), this.today.getSeconds());
public endDate = this.today;
public startDate = this.yesterday;
public flgSticky = false;
private unsub: Array<Subject<void>> = [new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.RTLState>, private actions$: Actions) {
switch (true) {
case (window.innerWidth <= 415):
this.displayedColumns = ['status', 'in_msatoshi', 'out_msatoshi'];
break;
case (window.innerWidth > 415 && window.innerWidth <= 730):
this.displayedColumns = ['status', 'in_msatoshi', 'out_msatoshi', 'fee'];
break;
case (window.innerWidth > 730 && window.innerWidth <= 1024):
this.displayedColumns = ['status', 'received_time_str', 'resolved_time_str', 'in_channel', 'out_channel', 'in_msatoshi', 'out_msatoshi', 'fee', 'payment_hash'];
break;
case (window.innerWidth > 1024 && window.innerWidth <= 1280):
this.flgSticky = true;
this.displayedColumns = ['status', 'received_time_str', 'resolved_time_str', 'in_channel', 'out_channel', 'in_msatoshi', 'out_msatoshi', 'fee', 'payment_hash'];
break;
default:
this.flgSticky = true;
this.displayedColumns = ['status', 'received_time_str', 'resolved_time_str', 'in_channel', 'out_channel', 'in_msatoshi', 'out_msatoshi', 'fee', 'payment_hash'];
break;
}
}
ngOnInit() {
this.onForwardingHistoryFetchCL();
this.store.select('cl')
.pipe(takeUntil(this.unsub[0]))
.subscribe((rtlStore) => {
rtlStore.effectErrorsCl.forEach(effectsErr => {
if (effectsErr.action === 'GetForwardingHistoryCL') {
this.flgLoading[0] = 'error';
}
});
if (undefined !== rtlStore.forwardingHistory.forwarding_events && rtlStore.forwardingHistory.forwarding_events.length > 0) {
this.lastOffsetIndex = rtlStore.forwardingHistory.last_offset_index;
this.loadForwardingEventsTable(rtlStore.forwardingHistory.forwarding_events);
} else {
// To reset table after other Forwarding history calls
this.lastOffsetIndex = 0;
this.loadForwardingEventsTable([]);
}
if (this.flgLoading[0] !== 'error') {
this.flgLoading[0] = (undefined !== rtlStore.forwardingHistory.forwarding_events) ? false : true;
}
this.logger.info(rtlStore);
});
}
onForwardingEventClick(selRow: ForwardingEventCL, event: any) {
const selFEvent = this.forwardingHistoryEvents.data.filter(fhEvent => {
return (fhEvent.received_time === selRow.received_time && fhEvent.in_channel === selRow.in_channel);
})[0];
const reorderedFHEvent = JSON.parse(JSON.stringify(selFEvent, [
'status', 'received_time_str', 'resolved_time_str', 'in_channel', 'out_channel', 'in_msatoshi', 'in_msat', 'out_msatoshi', 'out_msat', 'fee', 'fee_msat', 'payment_hash'
] , 2));
this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: {
type: 'INFO',
message: JSON.stringify(reorderedFHEvent)
}}));
}
loadForwardingEventsTable(forwardingEvents: ForwardingEventCL[]) {
this.forwardingHistoryEvents = new MatTableDataSource<ForwardingEventCL>([...forwardingEvents]);
this.forwardingHistoryEvents.sort = this.sort;
this.forwardingHistoryEvents.data.forEach(event => {
event.received_time_str = (event.received_time_str === '') ? '' : formatDate(event.received_time_str, 'MMM/dd/yy HH:mm:ss', 'en-US');
event.resolved_time_str = (event.resolved_time_str === '') ? '' : formatDate(event.resolved_time_str, 'MMM/dd/yy HH:mm:ss', 'en-US');
});
this.logger.info(this.forwardingHistoryEvents);
}
onForwardingHistoryFetchCL() {
if (undefined === this.endDate || this.endDate == null) {
this.endDate = new Date();
}
if (undefined === this.startDate || this.startDate == null) {
this.startDate = new Date(this.endDate.getFullYear(), this.endDate.getMonth(), this.endDate.getDate() - 1);
}
this.store.dispatch(new RTLActions.GetForwardingHistoryCL(
// { end_time: Math.round(this.endDate.getTime() / 1000).toString(), start_time: Math.round(this.startDate.getTime() / 1000).toString() }
));
}
resetData() {
// this.endDate = new Date();
// this.startDate = new Date(this.endDate.getFullYear(), this.endDate.getMonth(), this.endDate.getDate() - 1);
if (undefined !== this.forwardingHistoryEvents) {
this.forwardingHistoryEvents.data = [];
}
}
applyFilter(selFilter: string) {
this.forwardingHistoryEvents.filter = selFilter;
}
ngOnDestroy() {
this.resetData();
this.unsub.forEach(completeSub => {
completeSub.next();
completeSub.complete();
});
}
}

@ -0,0 +1,16 @@
<div fxLayout="column" fxFlex="100" fxLayoutAlign="space-between stretch">
<div>
<h4 fxLayoutAlign="start" class="dashboard-info-title">Lightning</h4>
<div class="overflow-wrap dashboard-info-value">{{balances.lightning | number}} Sats</div>
<mat-progress-bar class="dashboard-progress-bar" mode="determinate" value="{{balances.lightning/balances.total*100}}"></mat-progress-bar>
</div>
<div>
<h4 fxLayoutAlign="start" class="dashboard-info-title">On-chain</h4>
<div class="overflow-wrap dashboard-info-value">{{balances.onchain | number}} Sats</div>
<mat-progress-bar class="dashboard-progress-bar" mode="determinate" value="{{balances.onchain/balances.total*100}}"></mat-progress-bar>
</div>
<div>
<h4 fxLayoutAlign="start" class="dashboard-info-title">Total</h4>
<div class="overflow-wrap dashboard-info-value">{{balances.total | number}} Sats</div>
</div>
</div>

@ -1,20 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChannelClosedComponent } from './channel-closed.component';
import { CLBalancesInfoComponent } from './balances-info.component';
describe('ChannelClosedComponent', () => {
let component: ChannelClosedComponent;
let fixture: ComponentFixture<ChannelClosedComponent>;
describe('CLBalancesInfoComponent', () => {
let component: CLBalancesInfoComponent;
let fixture: ComponentFixture<CLBalancesInfoComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ChannelClosedComponent ]
declarations: [ CLBalancesInfoComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ChannelClosedComponent);
fixture = TestBed.createComponent(CLBalancesInfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

@ -0,0 +1,13 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'rtl-cl-balances-info',
templateUrl: './balances-info.component.html',
styleUrls: ['./balances-info.component.scss']
})
export class CLBalancesInfoComponent {
@Input() balances = { onchain: 0, lightning: 0, total: 0 };
constructor() {}
}

@ -0,0 +1,37 @@
<div fxLayout="column" fxLayoutAlign="space-between stretch" fxFlex="100">
<div fxLayout="column" fxFlex="9" fxLayoutAlign="end start">
<span class="dashboard-capacity-header this-channel-capacity">Total Capacity</span>
<div fxLayout="row" fxLayoutAlign="space-between start" class="w-100">
<mat-hint fxFlex="40" fxLayoutAlign="start center" class="font-size-90"><strong class="font-weight-900 mr-5px">Local:</strong>{{channelBalances.localBalance || 0 | number:'1.0-0'}} Sats</mat-hint>
<mat-hint fxFlex="20" fxLayoutAlign="center center" class="font-size-90">
<fa-icon [icon]="faBalanceScale" class="mr-3px" matTooltip="Balance Score"></fa-icon>
({{channelBalances.balancedness || 0 | number}})
</mat-hint>
<mat-hint fxFlex="40" fxLayoutAlign="end center" class="font-size-90"><strong class="font-weight-900 mr-5px">Remote:</strong>{{channelBalances.remoteBalance || 0 | number:'1.0-0'}} Sats</mat-hint>
</div>
<mat-progress-bar class="dashboard-progress-bar this-channel-bar" mode="determinate" color="accent" value="{{channelBalances.localBalance && channelBalances.localBalance > 0 ? ((+channelBalances.localBalance/((+channelBalances.localBalance)+(+channelBalances.remoteBalance)))*100) : 0}}"></mat-progress-bar>
</div>
<div fxLayout="column" fxFlex="3" fxLayoutAlign="end stretch"><mat-divider class="dashboard-divider"></mat-divider></div>
<div fxLayout="column" fxFlex.gt-sm="88" fxFlex="84" fxLayoutAlign="start start" class="channels-capacity-scroll" perfectScrollbar>
<div fxLayout="column" fxFlex="100" class="w-100" *ngIf="allChannels && allChannels.length > 0; else noChannelBlock">
<div *ngFor="let channel of allChannels" class="mt-2">
<span class="dashboard-capacity-header" matTooltip="{{channel.alias || channel.id}}" matTooltipDisabled="{{(channel.alias || channel.id).length < 26}}">{{(channel.alias || channel.id) | slice:0:24}}{{(channel.alias || channel.id).length > 25 ? '...' : ''}}</span>
<div fxLayout="row" fxLayoutAlign="space-between start" class="w-100">
<mat-hint fxFlex="40" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Local:</strong>{{channel.msatoshi_to_us/1000 || 0 | number:'1.0-0'}} Sats</mat-hint>
<mat-hint fxFlex="20" fxLayoutAlign="center center" class="font-size-90 color-primary">
<fa-icon [icon]="faBalanceScale" class="color-primary mr-3px" matTooltip="Balance Score"></fa-icon>
({{channel.balancedness || 0 | number}})
</mat-hint>
<mat-hint fxFlex="40" fxLayoutAlign="end center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Remote:</strong>{{channel.msatoshi_to_them/1000 || 0 | number:'1.0-0'}} Sats</mat-hint>
</div>
<mat-progress-bar class="dashboard-progress-bar" mode="determinate" value="{{channel.msatoshi_to_us && channel.msatoshi_to_us > 0 ? ((+channel.msatoshi_to_us/((+channel.msatoshi_to_us)+(+channel.msatoshi_to_them)))*100) : 0}}"></mat-progress-bar>
</div>
</div>
</div>
</div>
<ng-template #noChannelBlock>
<div fxLayout="row" fxFlex="10" fxLayoutAlign="space-between center" class="w-100 mt-1">
No channels available.
<button fxFlex="33" fxLayoutAlign="center center" mat-stroked-button color="primary" (click)="goToChannels()" tabindex="1">Open Channel</button>
</div>
</ng-template>

@ -0,0 +1,5 @@
.channels-capacity-scroll {
width: 100%;
height: 100%;
overflow-y: hidden;
}

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CLChannelCapacityInfoComponent } from './channel-capacity-info.component';
describe('CLChannelCapacityInfoComponent', () => {
let component: CLChannelCapacityInfoComponent;
let fixture: ComponentFixture<CLChannelCapacityInfoComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CLChannelCapacityInfoComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CLChannelCapacityInfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,25 @@
import { Component, Input } from '@angular/core';
import { Router } from '@angular/router';
import { faBalanceScale, faDumbbell } from '@fortawesome/free-solid-svg-icons';
import { ChannelCL } from '../../../shared/models/clModels';
@Component({
selector: 'rtl-cl-channel-capacity-info',
templateUrl: './channel-capacity-info.component.html',
styleUrls: ['./channel-capacity-info.component.scss']
})
export class CLChannelCapacityInfoComponent {
public faBalanceScale = faBalanceScale;
public faDumbbell = faDumbbell;
@Input() channelBalances: {localBalance: number, remoteBalance: number, balancedness: string};
@Input() allChannels: ChannelCL[];
@Input() sortBy: string = 'Balance Score';
constructor(private router: Router) {}
goToChannels() {
this.router.navigateByUrl('/lnd/peerschannels');
}
}

@ -0,0 +1,27 @@
<div fxLayout="column" fxLayoutAlign="space-between stretch" fxFlex="100">
<div fxLayout="column" fxFlex="9" fxLayoutAlign="end start">
<span class="dashboard-capacity-header this-channel-capacity">Total Capacity</span>
<mat-hint class="font-size-90">{{totalLiquidity | number:'1.0-0'}} Sats</mat-hint>
<mat-progress-bar class="dashboard-progress-bar this-channel-bar" mode="determinate" color="accent" value="100"></mat-progress-bar>
</div>
<div fxLayout="column" fxFlex="3" fxLayoutAlign="end stretch"><mat-divider class="dashboard-divider"></mat-divider></div>
<div fxLayout="column" fxFlex.gt-sm="88" fxFlex="84" fxLayoutAlign="start start" class="channels-capacity-scroll" perfectScrollbar>
<div fxLayout="column" fxFlex="100" class="w-100" *ngIf="allChannels && allChannels.length > 0; else noChannelBlock">
<div *ngFor="let channel of allChannels" class="mt-2">
<span class="dashboard-capacity-header" matTooltip="{{channel.alias || channel.id}}" matTooltipDisabled="{{(channel.alias || channel.id).length < 26}}">{{(channel.alias || channel.id) | slice:0:24}}{{(channel.alias || channel.id).length > 25 ? '...' : ''}}</span>
<div fxLayout="row" fxLayoutAlign="space-between start" class="w-100">
<mat-hint *ngIf="direction === 'In'" fxFlex="100" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Capacity: </strong>{{channel.msatoshi_to_them/1000 || 0 | number:'1.0-0'}} Sats</mat-hint>
<mat-hint *ngIf="direction === 'Out'" fxFlex="100" fxLayoutAlign="start center" class="font-size-90 color-primary"><strong class="font-weight-900 mr-5px">Capacity: </strong>{{channel.msatoshi_to_us/1000 || 0 | number:'1.0-0'}} Sats</mat-hint>
</div>
<mat-progress-bar *ngIf="direction === 'In'" class="dashboard-progress-bar" mode="determinate" value="{{(totalLiquidity > 0) ? ((+channel.msatoshi_to_them/1000 || 0)/(totalLiquidity) * 100) : 0}}"></mat-progress-bar>
<mat-progress-bar *ngIf="direction === 'Out'" class="dashboard-progress-bar" mode="determinate" value="{{(totalLiquidity > 0) ? ((+channel.msatoshi_to_us/1000 || 0)/(totalLiquidity) * 100) : 0}}"></mat-progress-bar>
</div>
</div>
</div>
</div>
<ng-template #noChannelBlock>
<div fxLayout="row" fxFlex="10" fxLayoutAlign="space-between center" class="w-100 mt-1">
No channels available.
<button fxFlex="33" fxLayoutAlign="center center" mat-stroked-button color="primary" (click)="goToChannels()" tabindex="1">Open Channel</button>
</div>
</ng-template>

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CLChannelLiquidityInfoComponent } from './channel-liquidity-info.component';
describe('CLChannelLiquidityInfoComponent', () => {
let component: CLChannelLiquidityInfoComponent;
let fixture: ComponentFixture<CLChannelLiquidityInfoComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CLChannelLiquidityInfoComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CLChannelLiquidityInfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,22 @@
import { Component, Input } from '@angular/core';
import { Router } from '@angular/router';
import { ChannelCL } from '../../../shared/models/clModels';
@Component({
selector: 'rtl-cl-channel-liquidity-info',
templateUrl: './channel-liquidity-info.component.html',
styleUrls: ['./channel-liquidity-info.component.scss']
})
export class CLChannelLiquidityInfoComponent {
@Input() direction: string;
@Input() totalLiquidity: number;
@Input() allChannels: ChannelCL[];
constructor(private router: Router) {}
goToChannels() {
this.router.navigateByUrl('/lnd/peerschannels');
}
}

@ -0,0 +1,28 @@
<div fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch">
<div>
<h4 fxLayoutAlign="start" class="dashboard-info-title">Active</h4>
<div class="overflow-wrap dashboard-info-value"><span class="dot tiny-dot green"></span>{{(channelsStatus.active.channels || 0) | number}}</div>
</div>
<div>
<h4 fxLayoutAlign="start" class="dashboard-info-title">Pending</h4>
<div class="overflow-wrap dashboard-info-value"><span class="dot tiny-dot yellow"></span>{{(channelsStatus.pending.channels || 0) | number}}</div>
</div>
<div>
<h4 fxLayoutAlign="start" class="dashboard-info-title">Inactive</h4>
<div class="overflow-wrap dashboard-info-value"><span class="dot tiny-dot grey"></span>{{(channelsStatus.inactive.channels || 0) | number}}</div>
</div>
</div>
<div fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch">
<div>
<h4 fxLayoutAlign="start" class="dashboard-info-title">Capacity</h4>
<div class="overflow-wrap dashboard-info-value">{{(channelsStatus.active.capacity || 0) | number}} Sats</div>
</div>
<div>
<h4 fxLayoutAlign="start" class="dashboard-info-title">Capacity</h4>
<div class="overflow-wrap dashboard-info-value">{{(channelsStatus.pending.capacity || 0) | number}} Sats</div>
</div>
<div>
<h4 fxLayoutAlign="start" class="dashboard-info-title">Capacity</h4>
<div class="overflow-wrap dashboard-info-value">{{(channelsStatus.inactive.capacity || 0) | number}} Sats</div>
</div>
</div>

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CLChannelStatusInfoComponent } from './channel-status-info.component';
describe('CLChannelStatusInfoComponent', () => {
let component: CLChannelStatusInfoComponent;
let fixture: ComponentFixture<CLChannelStatusInfoComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CLChannelStatusInfoComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CLChannelStatusInfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,16 @@
import { Component, OnChanges, Input } from '@angular/core';
import { ChannelsStatusCL } from '../../../shared/models/clModels';
@Component({
selector: 'rtl-cl-channel-status-info',
templateUrl: './channel-status-info.component.html',
styleUrls: ['./channel-status-info.component.scss']
})
export class CLChannelStatusInfoComponent implements OnChanges {
@Input() channelsStatus: ChannelsStatusCL = {};
constructor() {}
ngOnChanges() {}
}

@ -0,0 +1,12 @@
<div fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch">
<div>
<h4 fxLayoutAlign="start" class="dashboard-info-title">Total</h4>
<div class="overflow-wrap dashboard-info-value">{{fees?.feeCollected/1000 | number}} Sats</div>
</div>
</div>
<div fxLayout="column" fxFlex="50" fxLayoutAlign="space-between stretch">
<div>
<h4 fxLayoutAlign="start" class="dashboard-info-title">Transactions</h4>
<div class="overflow-wrap dashboard-info-value">{{fees?.totalTxCount | number}}</div>
</div>
</div>

@ -1,20 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CLInvoicesComponent } from './invoices.component';
import { CLFeeInfoComponent } from './fee-info.component';
describe('CLInvoicesComponent', () => {
let component: CLInvoicesComponent;
let fixture: ComponentFixture<CLInvoicesComponent>;
describe('CLFeeInfoComponent', () => {
let component: CLFeeInfoComponent;
let fixture: ComponentFixture<CLFeeInfoComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CLInvoicesComponent ]
declarations: [ CLFeeInfoComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CLInvoicesComponent);
fixture = TestBed.createComponent(CLFeeInfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

@ -0,0 +1,13 @@
import { Component, Input } from '@angular/core';
import { FeesCL } from '../../../shared/models/clModels';
@Component({
selector: 'rtl-cl-fee-info',
templateUrl: './fee-info.component.html',
styleUrls: ['./fee-info.component.scss']
})
export class CLFeeInfoComponent {
@Input() fees: FeesCL;
totalFees = [{'name': 'Total', 'value': 0}];
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save