diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..21968762
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,13 @@
+# Editor configuration, see http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = tab
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false
diff --git a/.gitignore b/.gitignore
index 57154db7..4b4bf590 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,60 +1,43 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-
-# Runtime data
-pids
-*.pid
-*.seed
-*.pid.lock
-
-# Directory for instrumented libs generated by jscoverage/JSCover
-lib-cov
-
-# Coverage directory used by tools like istanbul
-coverage
-
-# nyc test coverage
-.nyc_output
-
-# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
-.grunt
-
-# Bower dependency directory (https://bower.io/)
-bower_components
-
-# node-waf configuration
-.lock-wscript
-
-# Compiled binary addons (http://nodejs.org/api/addons.html)
-build/Release
-
-# Dependency directories
-node_modules/
-jspm_packages/
-
-# Typescript v1 declaration files
-typings/
-
-# Optional npm cache directory
-.npm
-
-# Optional eslint cache
-.eslintcache
-
-# Optional REPL history
-.node_repl_history
-
-# Output of 'npm pack'
-*.tgz
-
-# Yarn Integrity file
-.yarn-integrity
-
-# dotenv environment variables file
-.env
-
-RTL.conf
\ No newline at end of file
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+# compiled output
+/dist
+/tmp
+/out-tsc
+
+# dependencies
+/node_modules
+
+# IDEs and editors
+/.idea
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# IDE - VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# misc
+/.sass-cache
+/connect.lock
+/coverage
+/libpeerconnection.log
+npm-debug.log
+yarn-error.log
+testem.log
+/typings
+
+# System Files
+.DS_Store
+Thumbs.db
+
+RTL.conf
+/logs
+/cookies
+RTL.log
diff --git a/angular.json b/angular.json
new file mode 100644
index 00000000..7668ef9f
--- /dev/null
+++ b/angular.json
@@ -0,0 +1,125 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "RTLApp": {
+ "root": "",
+ "sourceRoot": "src",
+ "projectType": "application",
+ "prefix": "rtl",
+ "schematics": {},
+ "targets": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:browser",
+ "options": {
+ "outputPath": "angular",
+ "index": "src/index.html",
+ "main": "src/main.ts",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "src/tsconfig.app.json",
+ "assets": [
+ "src/assets"
+ ],
+ "styles": [
+ "src/app/shared/theme/styles/styles.scss"
+ ],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.prod.ts"
+ }
+ ],
+ "optimization": true,
+ "outputHashing": "all",
+ "sourceMap": false,
+ "extractCss": true,
+ "namedChunks": false,
+ "aot": true,
+ "extractLicenses": true,
+ "vendorChunk": false,
+ "buildOptimizer": true
+ }
+ }
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "options": {
+ "browserTarget": "RTLApp:build"
+ },
+ "configurations": {
+ "production": {
+ "browserTarget": "RTLApp:build:production"
+ }
+ }
+ },
+ "extract-i18n": {
+ "builder": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "browserTarget": "RTLApp:build"
+ }
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "main": "src/test.ts",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "src/tsconfig.spec.json",
+ "karmaConfig": "src/karma.conf.js",
+ "styles": [
+ "src/app/shared/theme/styles/styles.scss"
+ ],
+ "scripts": [],
+ "assets": [
+ "src/assets"
+ ]
+ }
+ },
+ "lint": {
+ "builder": "@angular-devkit/build-angular:tslint",
+ "options": {
+ "tsConfig": [
+ "src/tsconfig.app.json",
+ "src/tsconfig.spec.json"
+ ],
+ "exclude": [
+ "**/node_modules/**"
+ ]
+ }
+ }
+ }
+ },
+ "RTLApp-e2e": {
+ "root": "e2e/",
+ "projectType": "application",
+ "targets": {
+ "e2e": {
+ "builder": "@angular-devkit/build-angular:protractor",
+ "options": {
+ "protractorConfig": "e2e/protractor.conf.js",
+ "devServerTarget": "RTLApp:serve"
+ },
+ "configurations": {
+ "production": {
+ "devServerTarget": "RTLApp:serve:production"
+ }
+ }
+ },
+ "lint": {
+ "builder": "@angular-devkit/build-angular:tslint",
+ "options": {
+ "tsConfig": "e2e/tsconfig.e2e.json",
+ "exclude": [
+ "**/node_modules/**"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "defaultProject": "RTLApp"
+}
\ No newline at end of file
diff --git a/e2e/protractor.conf.js b/e2e/protractor.conf.js
new file mode 100644
index 00000000..86776a39
--- /dev/null
+++ b/e2e/protractor.conf.js
@@ -0,0 +1,28 @@
+// Protractor configuration file, see link for more information
+// https://github.com/angular/protractor/blob/master/lib/config.ts
+
+const { SpecReporter } = require('jasmine-spec-reporter');
+
+exports.config = {
+ allScriptsTimeout: 11000,
+ specs: [
+ './src/**/*.e2e-spec.ts'
+ ],
+ capabilities: {
+ 'browserName': 'chrome'
+ },
+ directConnect: true,
+ baseUrl: 'http://localhost:4200/',
+ framework: 'jasmine',
+ jasmineNodeOpts: {
+ showColors: true,
+ defaultTimeoutInterval: 30000,
+ print: function() {}
+ },
+ onPrepare() {
+ require('ts-node').register({
+ project: require('path').join(__dirname, './tsconfig.e2e.json')
+ });
+ jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
+ }
+};
\ No newline at end of file
diff --git a/e2e/src/app.e2e-spec.ts b/e2e/src/app.e2e-spec.ts
new file mode 100644
index 00000000..1d80d64a
--- /dev/null
+++ b/e2e/src/app.e2e-spec.ts
@@ -0,0 +1,14 @@
+import { AppPage } from './app.po';
+
+describe('workspace-project App', () => {
+ let page: AppPage;
+
+ beforeEach(() => {
+ page = new AppPage();
+ });
+
+ it('should display welcome message', () => {
+ page.navigateTo();
+ expect(page.getParagraphText()).toEqual('Welcome to RTLApp!');
+ });
+});
diff --git a/e2e/src/app.po.ts b/e2e/src/app.po.ts
new file mode 100644
index 00000000..82ea75ba
--- /dev/null
+++ b/e2e/src/app.po.ts
@@ -0,0 +1,11 @@
+import { browser, by, element } from 'protractor';
+
+export class AppPage {
+ navigateTo() {
+ return browser.get('/');
+ }
+
+ getParagraphText() {
+ return element(by.css('app-root h1')).getText();
+ }
+}
diff --git a/e2e/tsconfig.e2e.json b/e2e/tsconfig.e2e.json
new file mode 100644
index 00000000..a6dd6220
--- /dev/null
+++ b/e2e/tsconfig.e2e.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/app",
+ "module": "commonjs",
+ "target": "es5",
+ "types": [
+ "jasmine",
+ "jasminewd2",
+ "node"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/prebuild.js b/prebuild.js
new file mode 100644
index 00000000..b05e3772
--- /dev/null
+++ b/prebuild.js
@@ -0,0 +1,12 @@
+const path = require('path');
+const fs = require('fs');
+const appVersion = require('./package.json').version;
+const versionFilePath = path.join(__dirname + '/src/environments/version.ts');
+const versionStr = `export const VERSION = '${appVersion}';`;
+fs.writeFile(versionFilePath, versionStr, { flat: 'w' }, function (err) {
+ if (err) {
+ return console.log(err);
+ }
+ console.log(`Updating application version ${appVersion}`);
+ console.log(`${'Writing version module to '}${versionFilePath}\n`);
+});
diff --git a/product management/Channels Details.png b/product management/Channels Details.png
new file mode 100644
index 00000000..b95029dd
Binary files /dev/null and b/product management/Channels Details.png differ
diff --git a/product management/Channels Lookup.png b/product management/Channels Lookup.png
new file mode 100644
index 00000000..4dc94001
Binary files /dev/null and b/product management/Channels Lookup.png differ
diff --git a/product management/Channels.png b/product management/Channels.png
new file mode 100644
index 00000000..ace41cb5
Binary files /dev/null and b/product management/Channels.png differ
diff --git a/product management/Home Page.png b/product management/Home Page.png
new file mode 100644
index 00000000..91b8e6a7
Binary files /dev/null and b/product management/Home Page.png differ
diff --git a/product management/Node Lookup.png b/product management/Node Lookup.png
new file mode 100644
index 00000000..8c7b7e30
Binary files /dev/null and b/product management/Node Lookup.png differ
diff --git a/product management/Peers.png b/product management/Peers.png
new file mode 100644
index 00000000..00234fda
Binary files /dev/null and b/product management/Peers.png differ
diff --git a/product management/Start.png b/product management/Start.png
new file mode 100644
index 00000000..d97d0a2c
Binary files /dev/null and b/product management/Start.png differ
diff --git a/product management/Unlock Wallet.png b/product management/Unlock Wallet.png
new file mode 100644
index 00000000..a11ba3dd
Binary files /dev/null and b/product management/Unlock Wallet.png differ
diff --git a/product management/roadmap.md b/product management/roadmap.md
new file mode 100644
index 00000000..d0b2ae4d
--- /dev/null
+++ b/product management/roadmap.md
@@ -0,0 +1,44 @@
+# Product Roadmap for RTL Application
+
+## Version 0.0.1-alpha (Minimum Viable Product)
+
+Start
+- Unlock Wallet
+
+Home Page
+- Wallet Balance
+- Channel Balance
+- Channel Status
+- Chain Sync Status
+- Fee Report
+
+Peer Management
+- Listing of Connected Peers
+- Initiate Connection with peers with the public key
+
+Channel Management
+- Status of Channels (Active, Inactive, Pending)
+- Listing of Channels
+- Open Channel with connected peers
+
+## Version 0.0.2
+Globalization - Allow for Language customization
+
+## Feature Backlog
+Start
+- Create Wallet
+
+ Home Page
+- Network Status
+
+Channel Management
+- Channel Detail - Bi-Directional channel balance view
+- Close Channel
+
+LN Wallet
+- Generate pub key to recieve Bitcoin
+- Send Bitcoin to an address
+
+Payments
+- Decode payment request
+- Send payment
diff --git a/protractor.conf.js b/protractor.conf.js
new file mode 100644
index 00000000..7ee3b5ee
--- /dev/null
+++ b/protractor.conf.js
@@ -0,0 +1,28 @@
+// Protractor configuration file, see link for more information
+// https://github.com/angular/protractor/blob/master/lib/config.ts
+
+const { SpecReporter } = require('jasmine-spec-reporter');
+
+exports.config = {
+ allScriptsTimeout: 11000,
+ specs: [
+ './e2e/**/*.e2e-spec.ts'
+ ],
+ capabilities: {
+ 'browserName': 'chrome'
+ },
+ directConnect: true,
+ baseUrl: 'http://localhost:4200/',
+ framework: 'jasmine',
+ jasmineNodeOpts: {
+ showColors: true,
+ defaultTimeoutInterval: 30000,
+ print: function() {}
+ },
+ onPrepare() {
+ require('ts-node').register({
+ project: 'e2e/tsconfig.e2e.json'
+ });
+ jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
+ }
+};
diff --git a/src/app/app.component.html b/src/app/app.component.html
new file mode 100644
index 00000000..50d2776a
--- /dev/null
+++ b/src/app/app.component.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
Ride The Lightning (Beta)
+
+
+
+
+
+
+
vpn_key
+
{{information?.identity_pubkey}}
+
+ file_copyCopied
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ settings
+
+
+
+
+
+
+
+
+
Loading RTL...
+
+
diff --git a/src/app/app.component.scss b/src/app/app.component.scss
new file mode 100644
index 00000000..3c613452
--- /dev/null
+++ b/src/app/app.component.scss
@@ -0,0 +1,4 @@
+.inline-spinner {
+ display: inline-flex !important;
+ top: 0px !important;
+}
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
new file mode 100644
index 00000000..d994d495
--- /dev/null
+++ b/src/app/app.component.ts
@@ -0,0 +1,166 @@
+import { Component, OnInit, AfterViewInit, OnDestroy, ViewChild, HostListener } from '@angular/core';
+import { Router } from '@angular/router';
+import { Subject } from 'rxjs';
+import { takeUntil, filter } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+import { Actions } from '@ngrx/effects';
+import { UserIdleService } from 'angular-user-idle';
+
+import { LoggerService } from './shared/services/logger.service';
+import { Settings, Authentication } from './shared/models/RTLconfig';
+import { GetInfo } from './shared/models/lndModels';
+
+import * as RTLActions from './shared/store/rtl.actions';
+import * as fromRTLReducer from './shared/store/rtl.reducers';
+
+@Component({
+ selector: 'rtl-app',
+ templateUrl: './app.component.html',
+ styleUrls: ['./app.component.scss']
+})
+export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
+ @ViewChild('sideNavigation') sideNavigation: any;
+ @ViewChild('settingSidenav') settingSidenav: any;
+ public information: GetInfo = {};
+ public flgLoading: Array = [true]; // 0: Info
+ public flgCopied = false;
+ public settings: Settings;
+ public authSettings: Authentication;
+ public accessKey = '';
+ public smallScreen = false;
+ unsubs: Array> = [new Subject(), new Subject(), new Subject()];
+
+ constructor(private logger: LoggerService, private store: Store, private actions$: Actions,
+ private userIdle: UserIdleService, private router: Router) {}
+
+ ngOnInit() {
+ this.store.dispatch(new RTLActions.FetchSettings());
+ this.accessKey = this.readAccessKey();
+ this.store.select('rtlRoot')
+ .pipe(takeUntil(this.unsubs[0]))
+ .subscribe(rtlStore => {
+ this.settings = rtlStore.settings;
+ this.information = rtlStore.information;
+ this.authSettings = rtlStore.authSettings;
+ 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 (!sessionStorage.getItem('token')) {
+ this.flgLoading[0] = false;
+ }
+ });
+ if (sessionStorage.getItem('token')) {
+ this.store.dispatch(new RTLActions.FetchInfo());
+ }
+ this.actions$
+ .pipe(
+ takeUntil(this.unsubs[1]),
+ filter((action) => action.type === RTLActions.INIT_APP_DATA || action.type === RTLActions.SET_SETTINGS || action.type === RTLActions.SET_AUTH_SETTINGS)
+ ).subscribe((actionPayload: (RTLActions.InitAppData | RTLActions.SetSettings | RTLActions.SetAuthSettings)) => {
+ if (actionPayload.type === RTLActions.SET_AUTH_SETTINGS) {
+ if (!sessionStorage.getItem('token')) {
+ if (+actionPayload.payload.rtlSSO) {
+ this.store.dispatch(new RTLActions.Signin(window.btoa(this.accessKey)));
+ } else {
+ this.router.navigate([this.authSettings.logoutRedirectLink]);
+ }
+ }
+ } else if (actionPayload.type === RTLActions.INIT_APP_DATA) {
+ this.store.dispatch(new RTLActions.FetchInfo());
+ } else if (actionPayload.type === RTLActions.SET_SETTINGS) {
+ if (this.settings.menu === 'Horizontal') {
+ this.settingSidenav.toggle(); // To dynamically update the width to 100% after side nav is closed
+ setTimeout(() => { this.settingSidenav.toggle(); }, 100);
+ }
+ }
+ });
+ this.actions$
+ .pipe(
+ takeUntil(this.unsubs[1]),
+ filter((action) => action.type === RTLActions.SET_INFO)
+ ).subscribe((infoData: RTLActions.SetInfo) => {
+ if (undefined !== infoData.payload.identity_pubkey) {
+ this.initializeRemainingData();
+ }
+ });
+ this.userIdle.startWatching();
+ this.userIdle.onTimerStart().subscribe(count => {});
+ this.userIdle.onTimeout().subscribe(() => {
+ if (sessionStorage.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.store.dispatch(new RTLActions.Signout());
+ this.userIdle.resetTimer();
+ }
+ });
+ }
+
+ private readAccessKey() {
+ const url = window.location.href;
+ return url.substring(url.lastIndexOf('/') + 1);
+ }
+
+ initializeRemainingData() {
+ this.store.dispatch(new RTLActions.FetchPeers());
+ this.store.dispatch(new RTLActions.FetchBalance('channels'));
+ this.store.dispatch(new RTLActions.FetchFees());
+ this.store.dispatch(new RTLActions.FetchNetwork());
+ this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'all', channelStatus: ''}));
+ this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'pending', channelStatus: ''}));
+ this.store.dispatch(new RTLActions.FetchInvoices());
+ this.store.dispatch(new RTLActions.FetchPayments());
+ }
+
+ ngAfterViewInit() {
+ if (!this.settings.flgSidenavPinned) {
+ this.sideNavigation.close();
+ this.settingSidenav.toggle();
+ }
+ if (window.innerWidth <= 768) {
+ 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.sideNavigation.toggle();
+ }
+
+ onNavigationClicked(event: any) {
+ if (window.innerWidth <= 414) {
+ this.sideNavigation.close();
+ }
+ }
+
+ copiedText(payload) {
+ this.flgCopied = true;
+ setTimeout(() => {this.flgCopied = false; }, 5000);
+ this.logger.info('Copied Text: ' + payload);
+ }
+
+ ngOnDestroy() {
+ this.unsubs.forEach(unsub => {
+ unsub.next();
+ unsub.complete();
+ });
+ }
+}
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
new file mode 100644
index 00000000..f4fdd850
--- /dev/null
+++ b/src/app/app.module.ts
@@ -0,0 +1,103 @@
+import { BrowserModule } from '@angular/platform-browser';
+import { NgModule } from '@angular/core';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
+import { StoreModule } from '@ngrx/store';
+import { EffectsModule } from '@ngrx/effects';
+import { StoreDevtoolsModule } from '@ngrx/store-devtools';
+import { UserIdleModule } from 'angular-user-idle';
+
+import { OverlayContainer } from '@angular/cdk/overlay';
+import { NgxChartsModule } from '@swimlane/ngx-charts';
+
+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 { environment } from '../environments/environment';
+import { routing } from './app.routing';
+import { SharedModule } from './shared/shared.module';
+import { ThemeOverlay } from './shared/theme/overlay-container/theme-overlay';
+import { AppComponent } from './app.component';
+import { HomeComponent } from './pages/home/home.component';
+import { PeersComponent } from './pages/peers/peers.component';
+import { SendReceiveTransComponent } from './pages/transactions/send-receive/send-receive-trans.component';
+import { InvoicesComponent } from './pages/invoices/invoices.component';
+import { ServerConfigComponent } from './pages/server-config/server-config.component';
+import { HelpComponent } from './pages/help/help.component';
+import { UnlockLNDComponent } from './pages/unlock-lnd/unlock-lnd.component';
+import { PaymentsComponent } from './pages/payments/payments.component';
+import { SideNavigationComponent } from './pages/navigation/side-navigation/side-navigation.component';
+import { TopMenuComponent } from './pages/navigation/top-menu/top-menu.component';
+import { HorizontalNavigationComponent } from './pages/navigation/horizontal-navigation/horizontal-navigation.component';
+import { ChannelManageComponent } from './pages/channels/channel-manage/channel-manage.component';
+import { ChannelPendingComponent } from './pages/channels/channel-pending/channel-pending.component';
+import { SigninComponent } from './pages/signin/signin.component';
+
+import { RTLRootReducer } from './shared/store/rtl.reducers';
+import { RTLEffects } from './shared/store/rtl.effects';
+
+import { LoggerService, ConsoleLoggerService } from './shared/services/logger.service';
+import { AuthGuard, LNDUnlockedGuard } from './shared/services/auth.guard';
+import { AuthInterceptor } from './shared/services/auth.interceptor';
+import { ChannelClosedComponent } from './pages/channels/channel-closed/channel-closed.component';
+import { ListTransactionsComponent } from './pages/transactions/list-transactions/list-transactions.component';
+import { LookupsComponent } from './pages/lookups/lookups.component';
+import { ForwardingHistoryComponent } from './pages/switch/forwarding-history.component';
+import { ChannelLookupComponent } from './pages/lookups/channel-lookup/channel-lookup.component';
+import { NodeLookupComponent } from './pages/lookups/node-lookup/node-lookup.component';
+
+@NgModule({
+ imports: [
+ BrowserModule,
+ BrowserAnimationsModule,
+ FormsModule,
+ ReactiveFormsModule,
+ HttpClientModule,
+ PerfectScrollbarModule,
+ SharedModule,
+ NgxChartsModule,
+ routing,
+ UserIdleModule.forRoot({idle: 60 * 60, timeout: 1, ping: null}),
+ StoreModule.forRoot({rtlRoot: RTLRootReducer}),
+ EffectsModule.forRoot([RTLEffects]),
+ !environment.production ? StoreDevtoolsModule.instrument() : []
+ ],
+ declarations: [
+ AppComponent,
+ HomeComponent,
+ PeersComponent,
+ SendReceiveTransComponent,
+ InvoicesComponent,
+ ServerConfigComponent,
+ HelpComponent,
+ UnlockLNDComponent,
+ PaymentsComponent,
+ SideNavigationComponent,
+ TopMenuComponent,
+ HorizontalNavigationComponent,
+ ChannelManageComponent,
+ ChannelPendingComponent,
+ SigninComponent,
+ ChannelClosedComponent,
+ ListTransactionsComponent,
+ LookupsComponent,
+ ForwardingHistoryComponent,
+ ChannelLookupComponent,
+ NodeLookupComponent
+ ],
+ providers: [
+ { provide: LoggerService, useClass: ConsoleLoggerService },
+ { provide: PERFECT_SCROLLBAR_CONFIG, useValue: DEFAULT_PERFECT_SCROLLBAR_CONFIG },
+ { provide: OverlayContainer, useClass: ThemeOverlay },
+ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
+ AuthGuard, LNDUnlockedGuard
+ ],
+ bootstrap: [AppComponent]
+})
+export class AppModule {}
diff --git a/src/app/app.routing.ts b/src/app/app.routing.ts
new file mode 100644
index 00000000..3918f710
--- /dev/null
+++ b/src/app/app.routing.ts
@@ -0,0 +1,46 @@
+import { Routes, RouterModule } from '@angular/router';
+import { ModuleWithProviders } from '@angular/core';
+
+import { NotFoundComponent } from './shared/components/not-found/not-found.component';
+
+import { HomeComponent } from './pages/home/home.component';
+import { UnlockLNDComponent } from './pages/unlock-lnd/unlock-lnd.component';
+import { ChannelClosedComponent } from './pages/channels/channel-closed/channel-closed.component';
+import { ChannelManageComponent } from './pages/channels/channel-manage/channel-manage.component';
+import { ChannelPendingComponent } from './pages/channels/channel-pending/channel-pending.component';
+import { PeersComponent } from './pages/peers/peers.component';
+import { SendReceiveTransComponent } from './pages/transactions/send-receive/send-receive-trans.component';
+import { ListTransactionsComponent } from './pages/transactions/list-transactions/list-transactions.component';
+import { PaymentsComponent } from './pages/payments/payments.component';
+import { ServerConfigComponent } from './pages/server-config/server-config.component';
+import { HelpComponent } from './pages/help/help.component';
+import { InvoicesComponent } from './pages/invoices/invoices.component';
+import { LookupsComponent } from './pages/lookups/lookups.component';
+import { SigninComponent } from './pages/signin/signin.component';
+import { ForwardingHistoryComponent } from './pages/switch/forwarding-history.component';
+import { SsoFailedComponent } from './shared/components/sso-failed/sso-failed.component';
+
+import { AuthGuard, LNDUnlockedGuard } from './shared/services/auth.guard';
+
+export const routes: Routes = [
+ { path: '', redirectTo: '/home', pathMatch: 'full', canActivate: [AuthGuard, LNDUnlockedGuard] },
+ { path: 'unlocklnd', component: UnlockLNDComponent, canActivate: [AuthGuard] },
+ { path: 'home', component: HomeComponent, canActivate: [AuthGuard, LNDUnlockedGuard] },
+ { path: 'peers', component: PeersComponent, canActivate: [AuthGuard, LNDUnlockedGuard] },
+ { path: 'chnlclosed', component: ChannelClosedComponent, canActivate: [AuthGuard, LNDUnlockedGuard] },
+ { path: 'chnlmanage', component: ChannelManageComponent, canActivate: [AuthGuard, LNDUnlockedGuard] },
+ { path: 'chnlpending', component: ChannelPendingComponent, canActivate: [AuthGuard, LNDUnlockedGuard] },
+ { path: 'transsendreceive', component: SendReceiveTransComponent, canActivate: [AuthGuard, LNDUnlockedGuard] },
+ { path: 'translist', component: ListTransactionsComponent, canActivate: [AuthGuard, LNDUnlockedGuard] },
+ { path: 'payments', component: PaymentsComponent, canActivate: [AuthGuard, LNDUnlockedGuard] },
+ { path: 'invoices', component: InvoicesComponent, canActivate: [AuthGuard, LNDUnlockedGuard] },
+ { path: 'switch', component: ForwardingHistoryComponent, canActivate: [AuthGuard, LNDUnlockedGuard] },
+ { path: 'lookups', component: LookupsComponent, canActivate: [AuthGuard, LNDUnlockedGuard] },
+ { path: 'sconfig', component: ServerConfigComponent, canActivate: [AuthGuard] },
+ { path: 'login', component: SigninComponent },
+ { path: 'help', component: HelpComponent },
+ { path: 'ssoerror', component: SsoFailedComponent },
+ { path: '**', component: NotFoundComponent }
+];
+
+export const routing: ModuleWithProviders = RouterModule.forRoot(routes, { enableTracing: true });
diff --git a/src/app/pages/channels/channel-closed/channel-closed.component.html b/src/app/pages/channels/channel-closed/channel-closed.component.html
new file mode 100644
index 00000000..bf79c325
--- /dev/null
+++ b/src/app/pages/channels/channel-closed/channel-closed.component.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+ Closed Channels
+
+
+
+
+
+
+
+
+
+
+
+
+ Close Type |
+ {{channel.close_type || 'COOPERATIVE_CLOSE'}} |
+
+
+ Channel Point |
+ {{channel.channel_point | slice:0:10}}... |
+
+
+ ID |
+ {{channel.chan_id}} |
+
+
+ Closing Txn Hash |
+
+ {{channel.closing_tx_hash | slice:0:10}}... |
+
+
+ Pub Key |
+
+ {{channel.remote_pubkey | slice:0:10}}... |
+
+
+ Capacity |
+ {{channel.capacity | number}} |
+
+
+ Close Height |
+ {{channel.close_height | number}} |
+
+
+ Settled Balance |
+ {{channel.settled_balance | number}} |
+
+
+ Time Locked Balance |
+ {{channel.time_locked_balance | number}} |
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/pages/channels/channel-closed/channel-closed.component.scss b/src/app/pages/channels/channel-closed/channel-closed.component.scss
new file mode 100644
index 00000000..801fe9ca
--- /dev/null
+++ b/src/app/pages/channels/channel-closed/channel-closed.component.scss
@@ -0,0 +1,25 @@
+.mat-column-close_type {
+ flex: 0 0 16%;
+ min-width: 160px;
+}
+
+.mat-column-chan_id {
+ flex: 0 0 17%;
+ min-width: 170px;
+}
+
+table {
+ width:100%;
+}
+
+.table-container {
+ height: 79vh;
+ overflow: auto;
+}
+
+@media screen and (max-width: 414px) {
+ .table-container {
+ height: 68vh;
+ overflow: auto;
+ }
+}
diff --git a/src/app/pages/channels/channel-closed/channel-closed.component.spec.ts b/src/app/pages/channels/channel-closed/channel-closed.component.spec.ts
new file mode 100644
index 00000000..325af666
--- /dev/null
+++ b/src/app/pages/channels/channel-closed/channel-closed.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ChannelClosedComponent } from './channel-closed.component';
+
+describe('ChannelClosedComponent', () => {
+ let component: ChannelClosedComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ChannelClosedComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ChannelClosedComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/channels/channel-closed/channel-closed.component.ts b/src/app/pages/channels/channel-closed/channel-closed.component.ts
new file mode 100644
index 00000000..099cde55
--- /dev/null
+++ b/src/app/pages/channels/channel-closed/channel-closed.component.ts
@@ -0,0 +1,104 @@
+import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+
+import { MatTableDataSource, MatSort } from '@angular/material';
+import { ClosedChannel } from '../../../shared/models/lndModels';
+import { LoggerService } from '../../../shared/services/logger.service';
+
+import { RTLEffects } from '../../../shared/store/rtl.effects';
+import * as RTLActions from '../../../shared/store/rtl.actions';
+import * as fromRTLReducer from '../../../shared/store/rtl.reducers';
+
+@Component({
+ selector: 'rtl-channel-closed',
+ templateUrl: './channel-closed.component.html',
+ styleUrls: ['./channel-closed.component.scss']
+})
+export class ChannelClosedComponent implements OnInit, OnDestroy {
+ @ViewChild(MatSort) sort: MatSort;
+ public displayedColumns = [];
+ public closedChannels: any;
+ public flgLoading: Array = [true];
+ public selectedFilter = '';
+ private unsub: Array> = [new Subject(), new Subject()];
+
+ constructor(private logger: LoggerService, private store: Store, private rtlEffects: RTLEffects) {
+ switch (true) {
+ case (window.innerWidth <= 415):
+ this.displayedColumns = ['close_type', 'chan_id', 'settled_balance'];
+ break;
+ case (window.innerWidth > 415 && window.innerWidth <= 730):
+ this.displayedColumns = ['close_type', 'channel_point', 'chan_id', 'settled_balance'];
+ break;
+ case (window.innerWidth > 730 && window.innerWidth <= 1024):
+ this.displayedColumns = ['close_type', 'channel_point', 'chan_id', 'capacity', 'close_height', 'settled_balance'];
+ break;
+ case (window.innerWidth > 1024 && window.innerWidth <= 1280):
+ this.displayedColumns = ['close_type', 'channel_point', 'chan_id', 'closing_tx_hash', 'remote_pubkey', 'capacity',
+ 'close_height', 'settled_balance', 'time_locked_balance'];
+ break;
+ default:
+ this.displayedColumns = ['close_type', 'channel_point', 'chan_id', 'closing_tx_hash', 'remote_pubkey', 'capacity',
+ 'close_height', 'settled_balance', 'time_locked_balance'];
+ break;
+ }
+ }
+
+ ngOnInit() {
+ this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'closed', channelStatus: ''}));
+ this.store.select('rtlRoot')
+ .pipe(takeUntil(this.unsub[0]))
+ .subscribe((rtlStore: fromRTLReducer.State) => {
+ rtlStore.effectErrors.forEach(effectsErr => {
+ if (effectsErr.action === 'FetchChannels/closed') {
+ this.flgLoading[0] = 'error';
+ }
+ });
+ if (undefined !== rtlStore.closedChannels && rtlStore.closedChannels.length > 0) {
+ this.loadClosedChannelsTable(rtlStore.closedChannels);
+ }
+ if (this.flgLoading[0] !== 'error') {
+ this.flgLoading[0] = (undefined !== rtlStore.closedChannels) ? false : true;
+ }
+ this.logger.info(rtlStore);
+ });
+
+ }
+
+ applyFilter(selFilter: string) {
+ this.selectedFilter = selFilter;
+ this.closedChannels.filter = selFilter;
+ }
+
+ onClosedChannelClick(selRow: ClosedChannel, event: any) {
+ const selChannel = this.closedChannels.data.filter(closedChannel => {
+ return closedChannel.chan_id === selRow.chan_id;
+ })[0];
+ const reorderedChannel = JSON.parse(JSON.stringify(selChannel, ['close_type', 'channel_point', 'chan_id', 'closing_tx_hash', 'remote_pubkey', 'capacity',
+ 'close_height', 'settled_balance', 'time_locked_balance'] , 2));
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: {
+ type: 'INFO',
+ message: JSON.stringify(reorderedChannel)
+ }}));
+ }
+
+ loadClosedChannelsTable(closedChannels) {
+ this.closedChannels = new MatTableDataSource([...closedChannels]);
+ this.closedChannels.sort = this.sort;
+ this.logger.info(this.closedChannels);
+ }
+
+ resetData() {
+ this.selectedFilter = '';
+ }
+
+ ngOnDestroy() {
+ this.unsub.forEach(completeSub => {
+ completeSub.next();
+ completeSub.complete();
+ });
+ }
+
+}
diff --git a/src/app/pages/channels/channel-manage/channel-manage.component.html b/src/app/pages/channels/channel-manage/channel-manage.component.html
new file mode 100644
index 00000000..65a1a09a
--- /dev/null
+++ b/src/app/pages/channels/channel-manage/channel-manage.component.html
@@ -0,0 +1,131 @@
+
+
+
+
+
+ Add Channel
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Disconnect |
+ link_off |
+
+
+ edit |
+ edit |
+
+
+ Status |
+ {{channel.active}} |
+
+
+ ID |
+ {{channel.chan_id}} |
+
+
+ Pub Key |
+
+ {{channel.remote_pubkey | slice:0:10}}... |
+
+
+ Alias |
+ {{channel.remote_alias}} |
+
+
+ Capacity |
+ {{channel.capacity | number}} |
+
+
+ Local Bal |
+ {{channel.local_balance | number}} |
+
+
+ Remote Bal |
+ {{channel.remote_balance | number}} |
+
+
+ {{information?.smaller_currency_unit}} Sent |
+ {{channel.total_satoshis_sent | number}} |
+
+
+ {{information?.smaller_currency_unit}} Recv |
+ {{channel.total_satoshis_received | number}} |
+
+
+ Fee |
+ {{channel.commit_fee | number}} |
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/pages/channels/channel-manage/channel-manage.component.scss b/src/app/pages/channels/channel-manage/channel-manage.component.scss
new file mode 100644
index 00000000..ee209344
--- /dev/null
+++ b/src/app/pages/channels/channel-manage/channel-manage.component.scss
@@ -0,0 +1,50 @@
+.mat-column-close, .mat-column-update, .mat-column-active {
+ flex: 0 0 6%;
+ min-width: 50px;
+}
+
+mat-cell.mat-column-close, .mat-column-update {
+ cursor: pointer;
+}
+
+.mat-column-chan_id {
+ flex: 0 0 16%;
+ min-width: 160px;
+}
+
+.mat-checkbox-inner-container:focus, .mat-checkbox-input:focus {
+ outline: none !important;
+}
+
+.size-40 {
+ font-size: 40px;
+ margin-left: -30%;
+}
+
+.mat-button-text {
+ font-size: 24px;
+ padding-bottom: 20px;
+}
+
+.flex-ellipsis {
+ padding-right: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+table {
+ width:100%;
+}
+
+.table-container {
+ height: 68vh;
+ overflow: auto;
+}
+
+@media screen and (max-width: 414px) {
+ .table-container {
+ height: 31vh;
+ overflow: auto;
+ }
+}
diff --git a/src/app/pages/channels/channel-manage/channel-manage.component.spec.ts b/src/app/pages/channels/channel-manage/channel-manage.component.spec.ts
new file mode 100644
index 00000000..8c1baf54
--- /dev/null
+++ b/src/app/pages/channels/channel-manage/channel-manage.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ChannelManageComponent } from './channel-manage.component';
+
+describe('ChannelManageComponent', () => {
+ let component: ChannelManageComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ChannelManageComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ChannelManageComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/channels/channel-manage/channel-manage.component.ts b/src/app/pages/channels/channel-manage/channel-manage.component.ts
new file mode 100644
index 00000000..4c6639a5
--- /dev/null
+++ b/src/app/pages/channels/channel-manage/channel-manage.component.ts
@@ -0,0 +1,251 @@
+import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
+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 { Channel, Peer, GetInfo } from '../../../shared/models/lndModels';
+import { LoggerService } from '../../../shared/services/logger.service';
+
+import { RTLEffects } from '../../../shared/store/rtl.effects';
+import * as RTLActions from '../../../shared/store/rtl.actions';
+import * as fromRTLReducer from '../../../shared/store/rtl.reducers';
+
+@Component({
+ selector: 'rtl-channel-manage',
+ templateUrl: './channel-manage.component.html',
+ styleUrls: ['./channel-manage.component.scss']
+})
+export class ChannelManageComponent implements OnInit, OnDestroy {
+ @ViewChild(MatSort) sort: MatSort;
+ public totalBalance = 0;
+ public selectedPeer = '';
+ public fundingAmount: number;
+ public displayedColumns = [];
+ public channels: any;
+ public peers: Peer[] = [];
+ public information: GetInfo = {};
+ public flgLoading: Array = [true];
+ public selectedFilter = '';
+ public statusFilters = ['Active', 'Inactive'];
+ public myChanPolicy: any = {};
+ public selFilter = '';
+ public flgSticky = true;
+ public transTypes = [{id: '0', name: 'Default Priority'}, {id: '1', name: 'Target Confirmation Blocks'}, {id: '2', name: 'Fee'}];
+ public selTransType = '0';
+ public transTypeValue = {blocks: '', fees: ''};
+ public spendUnconfirmed = false;
+ public moreOptions = false;
+ private unsub: Array> = [new Subject(), new Subject(), new Subject(), new Subject()];
+
+ constructor(private logger: LoggerService, private store: Store, private rtlEffects: RTLEffects, private actions$: Actions) {
+ switch (true) {
+ case (window.innerWidth <= 415):
+ this.displayedColumns = ['close', 'update', 'active', 'chan_id', 'remote_alias'];
+ break;
+ case (window.innerWidth > 415 && window.innerWidth <= 730):
+ this.displayedColumns = ['close', 'update', 'active', 'chan_id', 'remote_alias', 'capacity'];
+ break;
+ case (window.innerWidth > 730 && window.innerWidth <= 1024):
+ this.displayedColumns = ['close', 'update', 'active', 'chan_id', 'remote_alias', 'capacity', 'local_balance', 'remote_balance'];
+ break;
+ case (window.innerWidth > 1024 && window.innerWidth <= 1280):
+ this.displayedColumns = ['close', 'update', 'active', 'chan_id', 'remote_alias', 'capacity', 'local_balance', 'remote_balance', 'total_satoshis_sent',
+ 'total_satoshis_received', 'commit_fee'];
+ break;
+ default:
+ this.displayedColumns = ['close', 'update', 'active', 'chan_id', 'remote_pubkey', 'remote_alias', 'capacity', 'local_balance', 'remote_balance',
+ 'total_satoshis_sent', 'total_satoshis_received', 'commit_fee'];
+ break;
+ }
+ }
+
+ ngOnInit() {
+ this.store.select('rtlRoot')
+ .pipe(takeUntil(this.unsub[0]))
+ .subscribe((rtlStore: fromRTLReducer.State) => {
+ rtlStore.effectErrors.forEach(effectsErr => {
+ if (effectsErr.action === 'FetchChannels/all') {
+ this.flgLoading[0] = 'error';
+ }
+ });
+ this.information = rtlStore.information;
+ this.peers = rtlStore.peers;
+ this.peers.forEach(peer => {
+ if (undefined === peer.alias || peer.alias === '') {
+ peer.alias = peer.pub_key.substring(0, 15) + '...';
+ }
+ });
+
+ this.totalBalance = +rtlStore.blockchainBalance.total_balance;
+ if (undefined !== rtlStore.allChannels && rtlStore.allChannels.length > 0) {
+ this.loadChannelsTable(rtlStore.allChannels);
+ }
+ if (this.flgLoading[0] !== 'error') {
+ this.flgLoading[0] = (undefined !== rtlStore.allChannels) ? false : true;
+ }
+
+ this.logger.info(rtlStore);
+ });
+ }
+
+ onOpenChannel(form: any) {
+ this.store.dispatch(new RTLActions.OpenSpinner('Opening Channel...'));
+ let transTypeValue = '0';
+ if (this.selTransType === '1') {
+ transTypeValue = this.transTypeValue.blocks;
+ } else if (this.selTransType === '2') {
+ transTypeValue = this.transTypeValue.fees;
+ }
+ this.store.dispatch(new RTLActions.SaveNewChannel({
+ selectedPeerPubkey: this.selectedPeer, fundingAmount: this.fundingAmount,
+ transType: this.selTransType, transTypeValue: transTypeValue, spendUnconfirmed: this.spendUnconfirmed
+ }));
+ }
+
+ onChannelUpdate(channelToUpdate: any) {
+ 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},
+ {placeholder: 'Time Lock Delta', inputType: 'number', inputValue: 144}
+ ]
+ }}));
+ this.rtlEffects.closeConfirm
+ .pipe(takeUntil(this.unsub[2]))
+ .subscribe(confirmRes => {
+ if (confirmRes) {
+ const base_fee = confirmRes[0].inputValue;
+ const fee_rate = confirmRes[1].inputValue;
+ const time_lock_delta = confirmRes[2].inputValue;
+ this.store.dispatch(new RTLActions.OpenSpinner('Updating Channel Policy...'));
+ this.store.dispatch(new RTLActions.UpdateChannels({baseFeeMsat: base_fee, feeRate: fee_rate, timeLockDelta: time_lock_delta, chanPoint: 'all'}));
+ }
+ });
+ } else {
+ this.myChanPolicy = {fee_base_msat: 0, fee_rate_milli_msat: 0, time_lock_delta: 0};
+ this.store.dispatch(new RTLActions.OpenSpinner('Fetching Channel Policy...'));
+ this.store.dispatch(new RTLActions.ChannelLookup(channelToUpdate.chan_id.toString()));
+ this.rtlEffects.setLookup
+ .pipe(takeUntil(this.unsub[3]))
+ .subscribe(resLookup => {
+ this.logger.info(resLookup);
+ if (resLookup.node1_pub === this.information.identity_pubkey) {
+ this.myChanPolicy = resLookup.node1_policy;
+ } else if (resLookup.node2_pub === this.information.identity_pubkey) {
+ this.myChanPolicy = resLookup.node2_policy;
+ } else {
+ this.myChanPolicy = {fee_base_msat: 0, fee_rate_milli_msat: 0, time_lock_delta: 0};
+ }
+ this.logger.info(this.myChanPolicy);
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ const titleMsg = 'Updated Values for Channel Point: ' + channelToUpdate.channel_point;
+ 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},
+ {placeholder: 'Time Lock Delta', inputType: 'number', inputValue: this.myChanPolicy.time_lock_delta}
+ ]
+ }}));
+ });
+ this.rtlEffects.closeConfirm
+ .pipe(takeUntil(this.unsub[2]))
+ .subscribe(confirmRes => {
+ if (confirmRes) {
+ const base_fee = confirmRes[0].inputValue;
+ const fee_rate = confirmRes[1].inputValue;
+ const time_lock_delta = confirmRes[2].inputValue;
+ this.store.dispatch(new RTLActions.OpenSpinner('Updating Channel Policy...'));
+ this.store.dispatch(new RTLActions.UpdateChannels({baseFeeMsat: base_fee, feeRate: fee_rate, timeLockDelta: time_lock_delta, chanPoint: channelToUpdate.channel_point}));
+ }
+ });
+ }
+ this.applyFilter();
+ }
+
+ onChannelClose(channelToClose: Channel) {
+ this.store.dispatch(new RTLActions.OpenConfirmation({
+ width: '70%', data: { type: 'CONFIRM', titleMessage: 'Closing channel: ' + channelToClose.chan_id, noBtnText: 'Cancel', yesBtnText: 'Disconnect'
+ }}));
+ 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.CloseChannel({channelPoint: channelToClose.channel_point, forcibly: true, channelStatus: channelToClose.active}));
+ }
+ });
+ }
+
+ applyFilter() {
+ this.selectedFilter = this.selFilter;
+ this.channels.filter = this.selFilter;
+ }
+
+ onChannelClick(selRow: Channel, 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.chan_id === selRow.chan_id;
+ })[0];
+ const reorderedChannel = JSON.parse(JSON.stringify(selChannel, [
+ 'active', 'remote_pubkey', 'remote_alias', 'channel_point', 'chan_id', 'capacity', 'local_balance', 'remote_balance', 'commit_fee', 'commit_weight',
+ 'fee_per_kw', 'unsettled_balance', 'total_satoshis_sent', 'total_satoshis_received', 'num_updates', 'pending_htlcs', 'csv_delay', 'private'
+ ] , 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.active === b.active) ? 0 : ((b.active) ? 1 : -1);
+ });
+ channels.forEach(channel => {
+ if (channel.active === true || channel.active === 'Active') {
+ channel.active = 'Active';
+ } else {
+ channel.active = 'Inactive';
+ }
+ });
+ this.channels = new MatTableDataSource([...channels]);
+ this.channels.sort = this.sort;
+ this.logger.info(this.channels);
+ }
+
+ resetData() {
+ this.selectedPeer = '';
+ this.fundingAmount = 0;
+ this.moreOptions = false;
+ this.spendUnconfirmed = false;
+ this.selTransType = '0';
+ this.transTypeValue = {blocks: '', fees: ''};
+ }
+
+ onMoreOptionsChange(event: any) {
+ if (!event.checked) {
+ this.spendUnconfirmed = false;
+ this.selTransType = '0';
+ this.transTypeValue = {blocks: '', fees: ''};
+ }
+ }
+
+ ngOnDestroy() {
+ this.unsub.forEach(completeSub => {
+ completeSub.next();
+ completeSub.complete();
+ });
+ }
+
+}
diff --git a/src/app/pages/channels/channel-pending/channel-pending.component.html b/src/app/pages/channels/channel-pending/channel-pending.component.html
new file mode 100644
index 00000000..59cf8f94
--- /dev/null
+++ b/src/app/pages/channels/channel-pending/channel-pending.component.html
@@ -0,0 +1,252 @@
+
+
+
+
+
+ Pending Channels
+
+
+
+
+
+
Total Limbo Balance:
+ {{pendingChannels.btc_total_limbo_balance | number}} {{information?.currency_unit}}
+
+ Total Limbo Balance: {{pendingChannels.total_limbo_balance | number}}
+ {{information?.smaller_currency_unit}}
+
+
+
+
+
+
+
+ Pending Open Channels({{pendingOpenChannelsLength}})
+
+
+
+
+
+ Remote Node Pub
+ {{channel.channel.remote_node_pub}}
+
+
+
+ Local Balance
+ {{channel.channel.local_balance |
+ number}}
+
+
+
+ Commit Fee
+ {{channel.commit_fee | number}}
+
+
+
+
+ Remote Balance
+ {{channel.channel.remote_balance |
+ number}}
+
+
+
+ Capacity
+ {{channel.channel.capacity |
+ number}}
+
+
+
+ Commit Weight
+ {{channel.commit_weight | number}}
+
+
+
+
+ Fee Per KW
+ {{channel.fee_per_kw | number}}
+
+
+
+
+ Confirmation Height
+ {{channel.confirmation_height |
+ number}}
+
+
+ Channel Point
+ {{channel.channel.channel_point}}
+
+
+
+
+
+
+
+
+ Pending Force Closing Channels({{pendingForceClosingChannelsLength}})
+
+
+
+
+
+ Remote Node Pub
+ {{channel.channel.remote_node_pub}}
+
+
+
+ Recovered Balance
+ {{channel.recovered_balance |
+ number}}
+
+
+
+ Limbo Balance
+ {{channel.limbo_balance | number}}
+
+
+
+
+ Block Till Maturity
+ {{channel.blocks_til_maturity |
+ number}}
+
+
+
+ Maturity Height
+ {{channel.maturity_height | number}}
+
+
+
+
+ Local Balance
+ {{channel.channel.local_balance |
+ number}}
+
+
+
+ Remote Balance
+ {{channel.channel.remote_balance |
+ number}}
+
+
+
+ Capacity
+ {{channel.channel.capacity |
+ number}}
+
+
+ Transaction Id
+
+ {{channel.closing_txid}}
+
+
+
+ Channel Point
+ {{channel.channel.channel_point}}
+
+
+
+
+
+
+
+
+ Pending Closing Channels({{pendingClosingChannelsLength}})
+
+
+
+
+
+ Remote Node Pub
+ {{channel.channel.remote_node_pub}}
+
+
+
+ Local Balance
+ {{channel.channel.local_balance |
+ number}}
+
+
+
+ Remote Balance
+ {{channel.channel.remote_balance |
+ number}}
+
+
+
+ Capacity
+ {{channel.channel.capacity |
+ number}}
+
+
+ Transaction Id
+
+ {{channel.closing_txid}}
+
+
+
+ Channel Point
+ {{channel.channel.channel_point}}
+
+
+
+
+
+
+
+
+ Waiting Close Channels({{pendingWaitClosingChannelsLength}})
+
+
+
+
+
+ Remote Node Pub
+ {{channel.channel.remote_node_pub}}
+
+
+
+ Limbo Balance
+ {{channel.limbo_balance | number}}
+
+
+
+
+ Local Balance
+ {{channel.channel.local_balance |
+ number}}
+
+
+
+ Remote Balance
+ {{channel.channel.remote_balance |
+ number}}
+
+
+
+ Capacity
+ {{channel.channel.capacity |
+ number}}
+
+
+ Channel Point
+ {{channel.channel.channel_point}}
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/pages/channels/channel-pending/channel-pending.component.scss b/src/app/pages/channels/channel-pending/channel-pending.component.scss
new file mode 100644
index 00000000..4ddbcf5b
--- /dev/null
+++ b/src/app/pages/channels/channel-pending/channel-pending.component.scss
@@ -0,0 +1,6 @@
+.flex-ellipsis {
+ padding-right: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/src/app/pages/channels/channel-pending/channel-pending.component.spec.ts b/src/app/pages/channels/channel-pending/channel-pending.component.spec.ts
new file mode 100644
index 00000000..2b53efe0
--- /dev/null
+++ b/src/app/pages/channels/channel-pending/channel-pending.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ChannelPendingComponent } from './channel-pending.component';
+
+describe('ChannelPendingComponent', () => {
+ let component: ChannelPendingComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ChannelPendingComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ChannelPendingComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/channels/channel-pending/channel-pending.component.ts b/src/app/pages/channels/channel-pending/channel-pending.component.ts
new file mode 100644
index 00000000..d4ee3e64
--- /dev/null
+++ b/src/app/pages/channels/channel-pending/channel-pending.component.ts
@@ -0,0 +1,241 @@
+import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+
+import { MatTableDataSource, MatSort } from '@angular/material';
+import { Channel, GetInfo, PendingChannels } from '../../../shared/models/lndModels';
+import { Settings } from '../../../shared/models/RTLconfig';
+import { LoggerService } from '../../../shared/services/logger.service';
+
+import { RTLEffects } from '../../../shared/store/rtl.effects';
+import * as RTLActions from '../../../shared/store/rtl.actions';
+import * as fromRTLReducer from '../../../shared/store/rtl.reducers';
+
+@Component({
+ selector: 'rtl-channel-pending',
+ templateUrl: './channel-pending.component.html',
+ styleUrls: ['./channel-pending.component.scss']
+})
+export class ChannelPendingComponent implements OnInit, OnDestroy {
+ @ViewChild(MatSort) sort: MatSort;
+ public selectedFilter = 0;
+ public settings: Settings;
+ public information: GetInfo = {};
+ public pendingChannels: PendingChannels = {};
+ public displayedClosingColumns = [
+ 'closing_txid',
+ 'channel_point', 'remote_balance', 'local_balance', 'remote_node_pub', 'capacity'
+ ];
+ public pendingClosingChannelsLength = 0;
+ public pendingClosingChannels: any;
+ public displayedForceClosingColumns = [
+ 'closing_txid', 'limbo_balance', 'maturity_height', 'blocks_til_maturity', 'recovered_balance',
+ 'channel_point', 'remote_balance', 'local_balance', 'remote_node_pub', 'capacity'
+ ];
+ public pendingForceClosingChannelsLength = 0;
+ public pendingForceClosingChannels: any;
+ public displayedOpenColumns = [
+ 'commit_weight', 'confirmation_height', 'fee_per_kw', 'commit_fee',
+ 'channel_point', 'remote_balance', 'local_balance', 'remote_node_pub', 'capacity'
+ ];
+ public pendingOpenChannelsLength = 0;
+ public pendingOpenChannels: any;
+ public displayedWaitClosingColumns = [
+ 'limbo_balance',
+ 'channel_point', 'remote_balance', 'local_balance', 'remote_node_pub', 'capacity'
+ ];
+ public pendingWaitClosingChannelsLength = 0;
+ public pendingWaitClosingChannels: any;
+ public flgLoading: Array = [true];
+ private unsub: Array> = [new Subject(), new Subject()];
+
+ constructor(private logger: LoggerService, private store: Store, private rtlEffects: RTLEffects) {
+ switch (true) {
+ case (window.innerWidth <= 415):
+ this.displayedClosingColumns = ['remote_node_pub', 'local_balance', 'remote_balance'];
+ this.displayedForceClosingColumns = ['remote_node_pub', 'recovered_balance', 'limbo_balance'];
+ this.displayedOpenColumns = ['remote_node_pub', 'local_balance', 'commit_fee'];
+ this.displayedWaitClosingColumns = ['remote_node_pub', 'limbo_balance', 'local_balance'];
+ break;
+ case (window.innerWidth > 415 && window.innerWidth <= 730):
+ this.displayedClosingColumns = ['remote_node_pub', 'local_balance', 'remote_balance', 'capacity'];
+ this.displayedForceClosingColumns = ['remote_node_pub', 'recovered_balance', 'limbo_balance', 'blocks_til_maturity', 'maturity_height'];
+ this.displayedOpenColumns = ['remote_node_pub', 'local_balance', 'commit_fee', 'remote_balance'];
+ this.displayedWaitClosingColumns = ['remote_node_pub', 'limbo_balance', 'local_balance', 'remote_balance'];
+ break;
+ case (window.innerWidth > 730 && window.innerWidth <= 1024):
+ this.displayedClosingColumns = ['remote_node_pub', 'local_balance', 'remote_balance', 'capacity', 'closing_txid'];
+ this.displayedForceClosingColumns = ['remote_node_pub', 'recovered_balance', 'limbo_balance', 'blocks_til_maturity', 'maturity_height', 'local_balance'];
+ this.displayedOpenColumns = ['remote_node_pub', 'local_balance', 'commit_fee', 'remote_balance', 'capacity'];
+ this.displayedWaitClosingColumns = ['remote_node_pub', 'limbo_balance', 'local_balance', 'remote_balance', 'capacity', 'channel_point'];
+ break;
+ case (window.innerWidth > 1024 && window.innerWidth <= 1280):
+ this.displayedClosingColumns = ['remote_node_pub', 'local_balance', 'remote_balance', 'capacity', 'closing_txid', 'channel_point'];
+ this.displayedForceClosingColumns = [
+ 'remote_node_pub', 'recovered_balance', 'limbo_balance', 'blocks_til_maturity',
+ 'maturity_height', 'local_balance', 'remote_balance', 'capacity', 'closing_txid', 'channel_point'
+ ];
+ this.displayedOpenColumns = [
+ 'remote_node_pub', 'local_balance', 'commit_fee', 'remote_balance', 'capacity', 'commit_weight', 'fee_per_kw', 'confirmation_height', 'channel_point'
+ ];
+ this.displayedWaitClosingColumns = ['remote_node_pub', 'limbo_balance', 'local_balance', 'remote_balance', 'capacity', 'channel_point'];
+ break;
+ default:
+ this.displayedClosingColumns = ['remote_node_pub', 'local_balance', 'remote_balance', 'capacity', 'closing_txid', 'channel_point'];
+ this.displayedForceClosingColumns = [
+ 'remote_node_pub', 'recovered_balance', 'limbo_balance', 'blocks_til_maturity',
+ 'maturity_height', 'local_balance', 'remote_balance', 'capacity', 'closing_txid', 'channel_point'
+ ];
+ this.displayedOpenColumns = [
+ 'remote_node_pub', 'local_balance', 'commit_fee', 'remote_balance', 'capacity', 'commit_weight', 'fee_per_kw', 'confirmation_height', 'channel_point'
+ ];
+ this.displayedWaitClosingColumns = ['remote_node_pub', 'limbo_balance', 'local_balance', 'remote_balance', 'capacity', 'channel_point'];
+ break;
+ }
+ }
+
+ ngOnInit() {
+ this.store.select('rtlRoot')
+ .pipe(takeUntil(this.unsub[0]))
+ .subscribe((rtlStore: fromRTLReducer.State) => {
+ rtlStore.effectErrors.forEach(effectsErr => {
+ if (effectsErr.action === 'FetchChannels/pending') {
+ this.flgLoading[0] = 'error';
+ }
+ });
+
+ this.settings = rtlStore.settings;
+ this.information = rtlStore.information;
+ this.pendingChannels = rtlStore.pendingChannels;
+ if (undefined !== this.pendingChannels.total_limbo_balance) {
+ this.flgLoading[1] = false;
+ if (undefined !== this.pendingChannels.pending_closing_channels) {
+ this.loadClosingChannelsTable(this.pendingChannels.pending_closing_channels);
+ }
+ if (undefined !== this.pendingChannels.pending_force_closing_channels) {
+ this.loadForceClosingChannelsTable(this.pendingChannels.pending_force_closing_channels);
+ }
+ if (undefined !== this.pendingChannels.pending_open_channels) {
+ this.loadOpenChannelsTable(this.pendingChannels.pending_open_channels);
+ }
+ if (undefined !== this.pendingChannels.waiting_close_channels) {
+ this.loadWaitClosingChannelsTable(this.pendingChannels.waiting_close_channels);
+ }
+ }
+ if (this.flgLoading[0] !== 'error') {
+ this.flgLoading[0] = (undefined !== this.information.identity_pubkey) ? false : true;
+ }
+ this.logger.info(rtlStore);
+ });
+
+ }
+
+ onOpenClick(selRow: any, event: any) {
+ const selChannel = this.pendingOpenChannels.data.filter(channel => {
+ return channel.channel.channel_point === selRow.channel.channel_point;
+ })[0];
+ const fcChannelObj1 = JSON.parse(JSON.stringify(selChannel, ['commit_weight', 'confirmation_height', 'fee_per_kw', 'commit_fee'], 2));
+ const fcChannelObj2 = JSON.parse(JSON.stringify(selChannel.channel, ['channel_point', 'remote_balance', 'local_balance', 'remote_node_pub', 'capacity'], 2));
+ const reorderedChannel = {};
+ Object.assign(reorderedChannel, fcChannelObj1, fcChannelObj2);
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: {
+ type: 'INFO',
+ message: JSON.stringify(reorderedChannel)
+ }}));
+ }
+
+ onForceClosingClick(selRow: any, event: any) {
+ const selChannel = this.pendingForceClosingChannels.data.filter(channel => {
+ return channel.channel.channel_point === selRow.channel.channel_point;
+ })[0];
+ const fcChannelObj1 = JSON.parse(JSON.stringify(selChannel, ['closing_txid', 'limbo_balance', 'maturity_height', 'blocks_til_maturity', 'recovered_balance'], 2));
+ const fcChannelObj2 = JSON.parse(JSON.stringify(selChannel.channel, ['channel_point', 'remote_balance', 'local_balance', 'remote_node_pub', 'capacity'], 2));
+ const reorderedChannel = {};
+ Object.assign(reorderedChannel, fcChannelObj1, fcChannelObj2);
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: {
+ type: 'INFO',
+ message: JSON.stringify(reorderedChannel)
+ }}));
+ }
+
+ onClosingClick(selRow: any, event: any) {
+ const selChannel = this.pendingClosingChannels.data.filter(channel => {
+ return channel.channel.channel_point === selRow.channel.channel_point;
+ })[0];
+ const fcChannelObj1 = JSON.parse(JSON.stringify(selChannel, ['closing_txid'], 2));
+ const fcChannelObj2 = JSON.parse(JSON.stringify(selChannel.channel, ['channel_point', 'remote_balance', 'local_balance', 'remote_node_pub', 'capacity'], 2));
+ const reorderedChannel = {};
+ Object.assign(reorderedChannel, fcChannelObj1, fcChannelObj2);
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: {
+ type: 'INFO',
+ message: JSON.stringify(reorderedChannel)
+ }}));
+ }
+
+ onWaitClosingClick(selRow: any, event: any) {
+ const selChannel = this.pendingWaitClosingChannels.data.filter(channel => {
+ return channel.channel.channel_point === selRow.channel.channel_point;
+ })[0];
+ const fcChannelObj1 = JSON.parse(JSON.stringify(selChannel, ['limbo_balance'], 2));
+ const fcChannelObj2 = JSON.parse(JSON.stringify(selChannel.channel, ['channel_point', 'remote_balance', 'local_balance', 'remote_node_pub', 'capacity'], 2));
+ const reorderedChannel = {};
+ Object.assign(reorderedChannel, fcChannelObj1, fcChannelObj2);
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: {
+ type: 'INFO',
+ message: JSON.stringify(reorderedChannel)
+ }}));
+ }
+
+ loadOpenChannelsTable(channels) {
+ channels.sort(function(a, b) {
+ return (a.active === b.active) ? 0 : ((b.active) ? -1 : 1);
+ });
+ this.pendingOpenChannelsLength = (undefined !== channels.length) ? channels.length : 0;
+ this.pendingOpenChannels = new MatTableDataSource([...channels]);
+ this.pendingOpenChannels.sort = this.sort;
+ this.logger.info(this.pendingOpenChannels);
+ }
+
+ loadForceClosingChannelsTable(channels) {
+ channels.sort(function(a, b) {
+ return (a.active === b.active) ? 0 : ((b.active) ? -1 : 1);
+ });
+ this.pendingForceClosingChannelsLength = (undefined !== channels.length) ? channels.length : 0;
+ this.pendingForceClosingChannels = new MatTableDataSource([...channels]);
+ this.pendingForceClosingChannels.sort = this.sort;
+ this.logger.info(this.pendingForceClosingChannels);
+ }
+
+ loadClosingChannelsTable(channels) {
+ channels.sort(function(a, b) {
+ return (a.active === b.active) ? 0 : ((b.active) ? -1 : 1);
+ });
+ this.pendingClosingChannelsLength = (undefined !== channels.length) ? channels.length : 0;
+ this.pendingClosingChannels = new MatTableDataSource([...channels]);
+ this.pendingClosingChannels.sort = this.sort;
+ this.logger.info(this.pendingClosingChannels);
+ }
+
+ loadWaitClosingChannelsTable(channels) {
+ channels.sort(function(a, b) {
+ return (a.active === b.active) ? 0 : ((b.active) ? -1 : 1);
+ });
+ this.pendingWaitClosingChannelsLength = (undefined !== channels.length) ? channels.length : 0;
+ this.pendingWaitClosingChannels = new MatTableDataSource([...channels]);
+ this.pendingWaitClosingChannels.sort = this.sort;
+ this.logger.info(this.pendingWaitClosingChannels);
+ }
+
+ applyFilter(selFilter: number) {
+ this.selectedFilter = selFilter;
+ }
+
+ ngOnDestroy() {
+ this.unsub.forEach(completeSub => {
+ completeSub.next();
+ completeSub.complete();
+ });
+ }
+
+}
diff --git a/src/app/pages/help/help.component.html b/src/app/pages/help/help.component.html
new file mode 100644
index 00000000..38191ad6
--- /dev/null
+++ b/src/app/pages/help/help.component.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+ Help
+
+
+
+
+
+ {{helpTopic.question}}
+
+ {{helpTopic.answer}}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/pages/help/help.component.scss b/src/app/pages/help/help.component.scss
new file mode 100644
index 00000000..f4bcac9e
--- /dev/null
+++ b/src/app/pages/help/help.component.scss
@@ -0,0 +1,3 @@
+.mat-card-content {
+ margin-bottom: 4px;
+}
diff --git a/src/app/pages/help/help.component.ts b/src/app/pages/help/help.component.ts
new file mode 100644
index 00000000..3ba67079
--- /dev/null
+++ b/src/app/pages/help/help.component.ts
@@ -0,0 +1,29 @@
+import { Component, OnInit } from '@angular/core';
+
+export class HelpTopic {
+ question: string;
+ answer: string;
+
+ constructor(ques: string, ans: string) {
+ this.question = ques;
+ this.answer = ans;
+ }
+}
+
+@Component({
+ selector: 'rtl-help',
+ templateUrl: './help.component.html',
+ styleUrls: ['./help.component.scss']
+})
+export class HelpComponent implements OnInit {
+ public helpTopics: Array = [];
+
+ constructor() {}
+
+ ngOnInit() {
+ // this.helpTopics.push(new HelpTopic('Set LND home directory?',
+ // 'Pass the directroy information while getting the server up with --lndir "local-lnd-path".
Example: node rtl --lndir C:\lnd\dir\path'));
+ this.helpTopics.push(new HelpTopic('Change theme?', 'Click on rotating setting icon on the right side of the screen and choose from the given options.'));
+ }
+
+}
diff --git a/src/app/pages/home/home.component.html b/src/app/pages/home/home.component.html
new file mode 100644
index 00000000..bbd82d00
--- /dev/null
+++ b/src/app/pages/home/home.component.html
@@ -0,0 +1,237 @@
+
+
+
+
+
+ Wallet Balance
+
+
+
+
+ account_balance_wallet
+
+
+ {{BTCtotalBalance | number}} {{information?.currency_unit}}
+ {{totalBalance | number}} {{information?.smaller_currency_unit}}
+
+
+
+
+
+
+
+
+
+
+ Peers
+
+
+
+
+ group
+
+ {{totalPeers | number}}
+
+ 0
+
+
+
+
+
+
+
+
+
+
+ Channel Balance
+
+
+
+
+ linear_scale
+
+
+ {{BTCchannelBalance | number}} {{information?.currency_unit}}
+ {{channelBalance | number}} {{information?.smaller_currency_unit}}
+
+
+
+
+
+
+
+
+
+
+ Chain Sync Status
+
+
+
+
+ sync
+
+ check_circle
+
+ cancel
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Fee Report
+
+
+
+
+
+ Daily ({{information?.smaller_currency_unit}})
+ {{fees?.day_fee_sum}}
+
+
+
+ Weekly ({{information?.smaller_currency_unit}})
+ {{fees?.week_fee_sum}}
+
+
+
+ Monthly ({{information?.smaller_currency_unit}})
+ {{fees?.month_fee_sum}}
+
+
+
+
+
+
+
+
+
+
+ Channel Status
+
+
+
+
+
+ Active
+ {{activeChannels}}
+
+
+
+ Inactive
+ {{inactiveChannels}}
+
+
+
+ Pending
+ {{pendingChannels}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Local-Remote Channel Capacity
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Network Information
+
+
+
+
+
+ Network Capacity ({{information?.currency_unit}})
+ {{networkInfo?.btc_total_network_capacity | number}}
+ Network Capacity ({{information?.smaller_currency_unit}})
+ {{networkInfo?.total_network_capacity | number}}
+
+
+
+ Number of Nodes
+ {{networkInfo?.num_nodes | number}}
+
+
+
+ Number of Channels
+ {{networkInfo?.num_channels | number}}
+
+
+
+ Max Out Degree
+ {{networkInfo?.max_out_degree | number}}
+
+
+
+ Avg Out Degree
+ {{networkInfo?.avg_out_degree | number:'1.0-2'}}
+
+
+
+ Max Channel Size ({{information?.currency_unit}})
+ Max Channel Size ({{information?.smaller_currency_unit}})
+ {{networkInfo?.btc_max_channel_size | number}}
+ {{networkInfo?.max_channel_size | number}}
+
+
+
+ Avg Channel Size ({{information?.currency_unit}})
+ Avg Channel Size ({{information?.smaller_currency_unit}})
+ {{networkInfo?.btc_avg_channel_size | number}}
+ {{networkInfo?.avg_channel_size | number:'1.0-2'}}
+
+
+
+ Min Channel Size ({{information?.currency_unit}})
+ Min Channel Size ({{information?.smaller_currency_unit}})
+ {{networkInfo?.btc_min_channel_size | number}}
+ {{networkInfo?.min_channel_size | number}}
+
+
+
+
+
+
+
+
+
+Sats
diff --git a/src/app/pages/home/home.component.scss b/src/app/pages/home/home.component.scss
new file mode 100644
index 00000000..ca8052d8
--- /dev/null
+++ b/src/app/pages/home/home.component.scss
@@ -0,0 +1,12 @@
+.network-info-list .mat-list-item {
+ height: 44px;
+}
+
+.mat-column-bytes_sent, .mat-column-bytes_recv, .mat-column-sat_sent, .mat-column-sat_recv, .mat-column-inbound, .mat-column-ping_time {
+ flex: 0 0 8%;
+ min-width: 80px;
+}
+
+.card-chnl-balances {
+ min-height: 354px;
+}
diff --git a/src/app/pages/home/home.component.spec.ts b/src/app/pages/home/home.component.spec.ts
new file mode 100644
index 00000000..490e81bd
--- /dev/null
+++ b/src/app/pages/home/home.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HomeComponent } from './home.component';
+
+describe('HomeComponent', () => {
+ let component: HomeComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ HomeComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(HomeComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/home/home.component.ts b/src/app/pages/home/home.component.ts
new file mode 100644
index 00000000..4fae5f12
--- /dev/null
+++ b/src/app/pages/home/home.component.ts
@@ -0,0 +1,148 @@
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+
+import { LoggerService } from '../../shared/services/logger.service';
+import { GetInfo, NetworkInfo, Fees, Peer } from '../../shared/models/lndModels';
+import { Settings } from '../../shared/models/RTLconfig';
+
+import * as fromRTLReducer from '../../shared/store/rtl.reducers';
+
+@Component({
+ selector: 'rtl-home',
+ templateUrl: './home.component.html',
+ styleUrls: ['./home.component.scss']
+})
+export class HomeComponent implements OnInit, OnDestroy {
+ public settings: Settings;
+ public fees: Fees;
+ public information: GetInfo = {};
+ public remainder = 0;
+ public totalPeers = -1;
+ public totalBalance = '';
+ public channelBalance = '';
+ public BTCtotalBalance = '';
+ public BTCchannelBalance = '';
+ public networkInfo: NetworkInfo = {};
+ public flgLoading: Array = [true, true, true, true, true, true, true, true]; // 0: Info, 1: Fee, 2: Wallet, 3: Channel, 4: Network
+ private unsub: Array> = [new Subject(), new Subject(), new Subject()];
+ public channels: any;
+ public position = 'below';
+ public activeChannels = 0;
+ public inactiveChannels = 0;
+ public pendingChannels = 0;
+ public peers: Peer[] = [];
+ barPadding = 0;
+ maxBalanceValue = 0;
+ totalBalances = [...[{'name': 'Local Balance', 'value': 0}, {'name': 'Remote Balance', 'value': 0}]];
+ flgTotalCalculated = false;
+ view = [];
+ yAxisLabel = 'Balance';
+ colorScheme = {domain: ['#FFFFFF']};
+
+ constructor(private logger: LoggerService, private store: Store) {
+ switch (true) {
+ case (window.innerWidth <= 730):
+ this.view = [250, 352];
+ break;
+ case (window.innerWidth > 415 && window.innerWidth <= 730):
+ this.view = [280, 352];
+ break;
+ case (window.innerWidth > 730 && window.innerWidth <= 1024):
+ this.view = [300, 352];
+ break;
+ case (window.innerWidth > 1024 && window.innerWidth <= 1280):
+ this.view = [350, 352];
+ break;
+ default:
+ this.view = [300, 352];
+ break;
+ }
+ Object.assign(this, this.totalBalances);
+ }
+
+ ngOnInit() {
+ this.flgTotalCalculated = false;
+ this.store.select('rtlRoot')
+ .pipe(takeUntil(this.unsub[0]))
+ .subscribe((rtlStore: fromRTLReducer.State) => {
+ rtlStore.effectErrors.forEach(effectsErr => {
+ if (effectsErr.action === 'FetchInfo') {
+ this.flgLoading[0] = 'error';
+ }
+ if (effectsErr.action === 'FetchFees') {
+ this.flgLoading[1] = 'error';
+ }
+ if (effectsErr.action === 'FetchBalance/blockchain') {
+ this.flgLoading[2] = 'error';
+ }
+ if (effectsErr.action === 'FetchBalance/channels') {
+ this.flgLoading[3] = 'error';
+ }
+ if (effectsErr.action === 'FetchNetwork') {
+ this.flgLoading[4] = 'error';
+ }
+ if (effectsErr.action === 'FetchChannels/all') {
+ this.flgLoading[5] = 'error';
+ this.flgLoading[6] = 'error';
+ }
+ });
+ this.settings = rtlStore.settings;
+ this.information = rtlStore.information;
+ if (this.flgLoading[0] !== 'error') {
+ this.flgLoading[0] = (undefined !== this.information.identity_pubkey) ? false : true;
+ }
+
+ this.fees = rtlStore.fees;
+ if (this.flgLoading[1] !== 'error') {
+ this.flgLoading[1] = (undefined !== this.fees.day_fee_sum) ? false : true;
+ }
+
+ this.totalBalance = rtlStore.blockchainBalance.total_balance;
+ this.BTCtotalBalance = rtlStore.blockchainBalance.btc_total_balance;
+ if (this.flgLoading[2] !== 'error') {
+ this.flgLoading[2] = ('' !== this.totalBalance) ? false : true;
+ }
+
+ this.channelBalance = rtlStore.channelBalance.balance;
+ this.BTCchannelBalance = rtlStore.channelBalance.btc_balance;
+ if (this.flgLoading[3] !== 'error') {
+ this.flgLoading[3] = ('' !== this.channelBalance) ? false : true;
+ }
+
+ this.networkInfo = rtlStore.networkInfo;
+ if (this.flgLoading[4] !== 'error') {
+ this.flgLoading[4] = (undefined !== this.networkInfo.num_nodes) ? false : true;
+ }
+
+ this.totalBalances = [...[{'name': 'Local Balance', 'value': +rtlStore.totalLocalBalance}, {'name': 'Remote Balance', 'value': +rtlStore.totalRemoteBalance}]];
+ this.maxBalanceValue = (rtlStore.totalLocalBalance > rtlStore.totalRemoteBalance) ? rtlStore.totalLocalBalance : rtlStore.totalRemoteBalance;
+ if (rtlStore.totalLocalBalance >= 0 && rtlStore.totalRemoteBalance >= 0) {
+ this.flgTotalCalculated = true;
+ if (this.flgLoading[5] !== 'error') {
+ this.flgLoading[5] = false;
+ }
+ }
+
+ this.activeChannels = rtlStore.numberOfActiveChannels;
+ this.inactiveChannels = rtlStore.numberOfInactiveChannels;
+ this.pendingChannels = (undefined !== rtlStore.pendingChannels.pending_open_channels) ? rtlStore.pendingChannels.pending_open_channels.length : 0;
+ if (rtlStore.totalLocalBalance >= 0 && rtlStore.totalRemoteBalance >= 0 && this.flgLoading[6] !== 'error') {
+ this.flgLoading[6] = false;
+ }
+
+ this.totalPeers = rtlStore.peers.length;
+
+ this.logger.info(rtlStore);
+ });
+ }
+
+ ngOnDestroy() {
+ this.unsub.forEach(completeSub => {
+ completeSub.next();
+ completeSub.complete();
+ });
+ }
+
+}
diff --git a/src/app/pages/invoices/invoices.component.html b/src/app/pages/invoices/invoices.component.html
new file mode 100644
index 00000000..53477439
--- /dev/null
+++ b/src/app/pages/invoices/invoices.component.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+ Invoices
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Creation Date |
+ {{invoice.creation_date_str}} |
+
+
+ Settle Date |
+ {{invoice.settle_date_str}} |
+
+
+ Memo |
+ {{invoice.memo}} |
+
+
+ Value ({{(settings?.satsToBTC) ? information?.currency_unit : information?.smaller_currency_unit}}) |
+ {{(settings?.satsToBTC) ? (invoice?.btc_value | number:'1.0-3') : (invoice?.value | number)}} |
+
+
+ Settled |
+ {{invoice.settled}} |
+
+
+ Expiry (Sec) |
+ {{invoice.expiry | number}} |
+
+
+ CLTV Expiry |
+ {{invoice.cltv_expiry | number}} |
+
+
+ Amount Paid ({{(settings?.satsToBTC) ? information?.currency_unit : information?.smaller_currency_unit}}) |
+ {{(settings?.satsToBTC) ? (invoice?.btc_amt_paid_sat | number:'1.0-3') : (invoice?.amt_paid_sat | number)}} |
+
+
+
+
+
+
+
+
+
diff --git a/src/app/pages/invoices/invoices.component.scss b/src/app/pages/invoices/invoices.component.scss
new file mode 100644
index 00000000..785c97c4
--- /dev/null
+++ b/src/app/pages/invoices/invoices.component.scss
@@ -0,0 +1,23 @@
+.mat-column-value {
+ padding-right: 1rem;
+}
+
+.mat-column-settled {
+ padding-left: 1rem;
+}
+
+table {
+ width:100%;
+}
+
+.table-container {
+ height: 68vh;
+ overflow: auto;
+}
+
+@media screen and (max-width: 414px) {
+ .table-container {
+ max-height: 31vh;
+ overflow: auto;
+ }
+}
diff --git a/src/app/pages/invoices/invoices.component.ts b/src/app/pages/invoices/invoices.component.ts
new file mode 100644
index 00000000..ab20c975
--- /dev/null
+++ b/src/app/pages/invoices/invoices.component.ts
@@ -0,0 +1,141 @@
+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 { Settings } from '../../shared/models/RTLconfig';
+import { GetInfo, Invoice } from '../../shared/models/lndModels';
+import { LoggerService } from '../../shared/services/logger.service';
+
+import { newlyAddedRowAnimation } from '../../shared/animation/row-animation';
+import * as RTLActions from '../../shared/store/rtl.actions';
+import * as fromRTLReducer from '../../shared/store/rtl.reducers';
+
+@Component({
+ selector: 'rtl-invoices',
+ templateUrl: './invoices.component.html',
+ styleUrls: ['./invoices.component.scss'],
+ animations: [newlyAddedRowAnimation]
+})
+export class InvoicesComponent implements OnInit, OnDestroy {
+ @ViewChild(MatSort) sort: MatSort;
+ public newlyAddedInvoiceMemo = '';
+ public newlyAddedInvoiceValue = 0;
+ public flgAnimate = true;
+ public settings: Settings;
+ public memo = '';
+ public invoiceValue: number;
+ public displayedColumns = [];
+ public invoicePaymentReq = '';
+ public invoices: any;
+ public information: GetInfo = {};
+ public flgLoading: Array = [true];
+ private unSubs: Array> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
+
+ constructor(private logger: LoggerService, private store: Store, private actions$: Actions) {
+ switch (true) {
+ case (window.innerWidth <= 415):
+ this.displayedColumns = ['creation_date', 'memo', 'value', 'settled'];
+ break;
+ case (window.innerWidth > 415 && window.innerWidth <= 730):
+ this.displayedColumns = ['creation_date', 'settle_date', 'memo', 'value', 'settled', 'amt_paid_sat'];
+ break;
+ case (window.innerWidth > 730 && window.innerWidth <= 1024):
+ this.displayedColumns = ['creation_date', 'settle_date', 'memo', 'value', 'settled', 'amt_paid_sat'];
+ break;
+ case (window.innerWidth > 1024 && window.innerWidth <= 1280):
+ this.displayedColumns = ['creation_date', 'settle_date', 'memo', 'value', 'settled', 'amt_paid_sat', 'expiry', 'cltv_expiry'];
+ break;
+ default:
+ this.displayedColumns = ['creation_date', 'settle_date', 'memo', 'value', 'settled', 'amt_paid_sat', 'expiry', 'cltv_expiry'];
+ break;
+ }
+ }
+
+ ngOnInit() {
+ this.store.select('rtlRoot')
+ .pipe(takeUntil(this.unSubs[0]))
+ .subscribe((rtlStore: fromRTLReducer.State) => {
+ rtlStore.effectErrors.forEach(effectsErr => {
+ if (effectsErr.action === 'FetchInvoices') {
+ this.flgLoading[0] = 'error';
+ }
+ });
+ this.settings = rtlStore.settings;
+ this.information = rtlStore.information;
+ this.logger.info(rtlStore);
+ this.loadInvoicesTable(rtlStore.invoices);
+ if (this.flgLoading[0] !== 'error') {
+ this.flgLoading[0] = (undefined !== rtlStore.invoices[0]) ? false : true;
+ }
+ });
+
+ }
+
+ onAddInvoice(form: any) {
+ this.flgAnimate = true;
+ this.newlyAddedInvoiceMemo = this.memo;
+ this.newlyAddedInvoiceValue = this.invoiceValue;
+ this.store.dispatch(new RTLActions.OpenSpinner('Adding Invoice...'));
+ this.store.dispatch(new RTLActions.SaveNewInvoice({memo: this.memo, invoiceValue: this.invoiceValue}));
+ this.actions$
+ .pipe(
+ takeUntil(this.unSubs[1]),
+ filter((action) => action.type === RTLActions.ADD_INVOICE)
+ ).subscribe((newInvoiceAction: RTLActions.AddInvoice) => {
+ this.logger.info(newInvoiceAction.payload);
+ this.invoicePaymentReq = newInvoiceAction.payload.payment_request;
+ });
+
+ }
+
+ onInvoiceClick(selRow: Invoice, event: any) {
+ const selInvoice = this.invoices.data.filter(invoice => {
+ return invoice.payment_request === selRow.payment_request;
+ })[0];
+ const reorderedInvoice = JSON.parse(JSON.stringify(selInvoice, [
+ 'creation_date_str', 'settle_date_str', 'memo', 'receipt', 'r_preimage', 'r_hash', 'value', 'settled', 'payment_request',
+ 'description_hash', 'expiry', 'fallback_addr', 'cltv_expiry', 'route_hints', 'private', 'add_index', 'settle_index',
+ 'amt_paid', 'amt_paid_sat', 'amt_paid_msat'
+ ] , 2));
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: {
+ type: 'INFO',
+ message: JSON.stringify(reorderedInvoice)
+ }}));
+ }
+
+ loadInvoicesTable(invoices) {
+ this.invoices = new MatTableDataSource([...invoices]);
+ this.invoices.sort = this.sort;
+ this.invoices.data.forEach(invoice => {
+ if (undefined !== invoice.creation_date_str) {
+ invoice.creation_date_str = (invoice.creation_date_str === '') ? '' : formatDate(invoice.creation_date_str, 'MMM/dd/yy HH:mm:ss', 'en-US');
+ }
+ if (undefined !== invoice.settle_date_str) {
+ invoice.settle_date_str = (invoice.settle_date_str === '') ? '' : formatDate(invoice.settle_date_str, 'MMM/dd/yy HH:mm:ss', 'en-US');
+ }
+ });
+ setTimeout(() => { this.flgAnimate = false; }, 5000);
+ this.logger.info(this.invoices);
+ }
+
+ resetData() {
+ this.memo = '';
+ this.invoiceValue = 0;
+ }
+
+ applyFilter(selFilter: string) {
+ this.invoices.filter = selFilter;
+ }
+
+ ngOnDestroy() {
+ this.unSubs.forEach(completeSub => {
+ completeSub.next();
+ completeSub.complete();
+ });
+ }
+
+}
diff --git a/src/app/pages/lookups/channel-lookup/channel-lookup.component.css b/src/app/pages/lookups/channel-lookup/channel-lookup.component.css
new file mode 100644
index 00000000..ef68f255
--- /dev/null
+++ b/src/app/pages/lookups/channel-lookup/channel-lookup.component.css
@@ -0,0 +1,3 @@
+.mat-list-base .mat-list-item, .mat-list-base .mat-list-option {
+ height: 38px !important;
+}
\ No newline at end of file
diff --git a/src/app/pages/lookups/channel-lookup/channel-lookup.component.html b/src/app/pages/lookups/channel-lookup/channel-lookup.component.html
new file mode 100644
index 00000000..0ed05159
--- /dev/null
+++ b/src/app/pages/lookups/channel-lookup/channel-lookup.component.html
@@ -0,0 +1,109 @@
+
+
+
+
+ Channel Id
+ {{lookupResult.channel_id}}
+
+
+
+ Channel Point
+ {{lookupResult.chan_point}}
+
+
+
+ Last Update
+ {{lookupResult.last_update_str}}
+
+
+
+ Capacity (Sats)
+ {{lookupResult.capacity | number}}
+
+
+
+
+
+
+
+
+
+
+ Node 1
+ Node 1 (Your Node)
+
+
+
+
+ {{lookupResult.node1_pub}}
+
+
+
+ Time Lock Delta
+ {{lookupResult.node1_policy.time_lock_delta}}
+
+
+
+ Min HTLC
+ {{lookupResult.node1_policy.min_htlc}}
+
+
+
+ Fee Base Msat
+ {{lookupResult.node1_policy.fee_base_msat}}
+
+
+
+ Fee Rate Milli Msat
+ {{lookupResult.node1_policy.fee_rate_milli_msat}}
+
+
+
+ Disabled
+ {{lookupResult.node1_policy.disabled}}
+
+
+
+
+
+
+
+
+
+ Node 2
+ Node 2 (Your Node)
+
+
+
+
+ {{lookupResult.node2_pub}}
+
+
+
+ Time Lock Delta
+ {{lookupResult.node2_policy.time_lock_delta}}
+
+
+
+ Min HTLC
+ {{lookupResult.node2_policy.min_htlc}}
+
+
+
+ Fee Base Msat
+ {{lookupResult.node2_policy.fee_base_msat}}
+
+
+
+ Fee Rate Milli Msat
+ {{lookupResult.node2_policy.fee_rate_milli_msat}}
+
+
+
+ Disabled
+ {{lookupResult.node2_policy.disabled}}
+
+
+
+
+
diff --git a/src/app/pages/lookups/channel-lookup/channel-lookup.component.spec.ts b/src/app/pages/lookups/channel-lookup/channel-lookup.component.spec.ts
new file mode 100644
index 00000000..919141e4
--- /dev/null
+++ b/src/app/pages/lookups/channel-lookup/channel-lookup.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ChannelLookupComponent } from './channel-lookup.component';
+
+describe('ChannelLookupComponent', () => {
+ let component: ChannelLookupComponent;
+ let fixture: ComponentFixture
;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ChannelLookupComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ChannelLookupComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/lookups/channel-lookup/channel-lookup.component.ts b/src/app/pages/lookups/channel-lookup/channel-lookup.component.ts
new file mode 100644
index 00000000..8e39ff24
--- /dev/null
+++ b/src/app/pages/lookups/channel-lookup/channel-lookup.component.ts
@@ -0,0 +1,40 @@
+import { Component, OnInit, Input } from '@angular/core';
+import { formatDate } from '@angular/common';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+
+import { ChannelEdge } from '../../../shared/models/lndModels';
+import * as fromRTLReducer from '../../../shared/store/rtl.reducers';
+
+@Component({
+ selector: 'rtl-channel-lookup',
+ templateUrl: './channel-lookup.component.html',
+ styleUrls: ['./channel-lookup.component.css']
+})
+export class ChannelLookupComponent implements OnInit {
+ @Input() lookupResult: ChannelEdge;
+ public node1_match = false;
+ public node2_match = false;
+ private unSubs: Array> = [new Subject(), new Subject(), new Subject(), new Subject()];
+
+ constructor(private store: Store) { }
+
+ ngOnInit() {
+ if (undefined !== this.lookupResult && undefined !== this.lookupResult.last_update_str) {
+ this.lookupResult.last_update_str = (this.lookupResult.last_update_str === '') ?
+ '' : formatDate(this.lookupResult.last_update_str, 'MMM/dd/yy HH:mm:ss', 'en-US');
+ }
+ this.store.select('rtlRoot')
+ .pipe(takeUntil(this.unSubs[0]))
+ .subscribe((rtlStore: fromRTLReducer.State) => {
+ if (this.lookupResult.node1_pub === rtlStore.information.identity_pubkey) {
+ this.node1_match = true;
+ }
+ if (this.lookupResult.node2_pub === rtlStore.information.identity_pubkey) {
+ this.node2_match = true;
+ }
+ });
+ }
+
+}
diff --git a/src/app/pages/lookups/lookups.component.html b/src/app/pages/lookups/lookups.component.html
new file mode 100644
index 00000000..501d586c
--- /dev/null
+++ b/src/app/pages/lookups/lookups.component.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+ Lookups
+
+
+
+
+
+
+
+
+
+
+
+ {{selectedField.name}} Details
+
+
+
+
+
+
+ Error! Unable to find details!
+
+
+
+
+
diff --git a/src/app/pages/lookups/lookups.component.scss b/src/app/pages/lookups/lookups.component.scss
new file mode 100644
index 00000000..d45059cb
--- /dev/null
+++ b/src/app/pages/lookups/lookups.component.scss
@@ -0,0 +1,14 @@
+.tree-invisible {
+ display: none;
+}
+
+.lookup-tree ul,
+.lookup-tree li {
+ margin-top: 0;
+ margin-bottom: 0;
+ list-style-type: none;
+}
+
+.pl-3 {
+ padding-left: 3rem;
+}
\ No newline at end of file
diff --git a/src/app/pages/lookups/lookups.component.spec.ts b/src/app/pages/lookups/lookups.component.spec.ts
new file mode 100644
index 00000000..428d1de1
--- /dev/null
+++ b/src/app/pages/lookups/lookups.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LookupsComponent } from './lookups.component';
+
+describe('LookupsComponent', () => {
+ let component: LookupsComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ LookupsComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LookupsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/lookups/lookups.component.ts b/src/app/pages/lookups/lookups.component.ts
new file mode 100644
index 00000000..79ae3cd9
--- /dev/null
+++ b/src/app/pages/lookups/lookups.component.ts
@@ -0,0 +1,94 @@
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { Subject } from 'rxjs';
+import { takeUntil, filter } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+import { Actions } from '@ngrx/effects';
+
+import { LoggerService } from '../../shared/services/logger.service';
+
+import * as RTLActions from '../../shared/store/rtl.actions';
+import * as fromRTLReducer from '../../shared/store/rtl.reducers';
+
+@Component({
+ selector: 'rtl-lookups',
+ templateUrl: './lookups.component.html',
+ styleUrls: ['./lookups.component.scss']
+})
+export class LookupsComponent implements OnInit, OnDestroy {
+ public lookupKey = '';
+ public lookupValue = {};
+ public flgSetLookupValue = false;
+ public temp: any;
+ public messageObj = [];
+ public selectedField = { id: '0', name: 'Node', placeholder: 'Pubkey'};
+ public lookupFields = [
+ { id: '0', name: 'Node', placeholder: 'Pubkey'},
+ { id: '1', name: 'Channel', placeholder: 'Channel ID'}
+ ];
+ public flgLoading: Array = [true];
+ private unSubs: Array> = [new Subject()];
+
+ constructor(private logger: LoggerService, private store: Store, private actions$: Actions) {}
+
+ ngOnInit() {
+ this.actions$
+ .pipe(
+ takeUntil(this.unSubs[0]),
+ filter((action) => (action.type === RTLActions.SET_LOOKUP || action.type === RTLActions.EFFECT_ERROR))
+ ).subscribe((resLookup: RTLActions.SetLookup) => {
+ if (resLookup.payload.action === 'Lookup') {
+ this.flgLoading[0] = 'error';
+ } else {
+ this.flgLoading[0] = true;
+ this.lookupValue = JSON.parse(JSON.stringify(resLookup.payload));
+ this.flgSetLookupValue = true;
+ this.logger.info(this.lookupValue);
+ }
+ });
+ }
+
+ onLookup() {
+ this.flgSetLookupValue = false;
+ this.lookupValue = {};
+ this.store.dispatch(new RTLActions.OpenSpinner('Searching ' + this.selectedField.name + '...'));
+ switch (this.selectedField.id) {
+ case '0':
+ this.store.dispatch(new RTLActions.PeerLookup(this.lookupKey.trim()));
+ break;
+ case '1':
+ this.store.dispatch(new RTLActions.ChannelLookup(this.lookupKey.trim()));
+ break;
+ default:
+ break;
+ }
+ }
+
+ onSelectChange(event: any) {
+ this.flgSetLookupValue = false;
+ this.lookupKey = '';
+ this.lookupValue = {};
+ }
+
+ resetData() {
+ this.flgSetLookupValue = false;
+ this.lookupKey = '';
+ this.selectedField = { id: '0', name: 'Node', placeholder: 'Pubkey'};
+ this.lookupValue = {};
+ this.flgLoading.forEach((flg, i) => {
+ this.flgLoading[i] = true;
+ });
+ }
+
+ clearLookupValue() {
+ this.lookupValue = {};
+ this.flgSetLookupValue = false;
+ }
+
+ ngOnDestroy() {
+ this.unSubs.forEach(completeSub => {
+ completeSub.next();
+ completeSub.complete();
+ });
+ }
+
+}
diff --git a/src/app/pages/lookups/node-lookup/node-lookup.component.css b/src/app/pages/lookups/node-lookup/node-lookup.component.css
new file mode 100644
index 00000000..ddab909a
--- /dev/null
+++ b/src/app/pages/lookups/node-lookup/node-lookup.component.css
@@ -0,0 +1,7 @@
+.mat-table {
+ width:99%;
+}
+
+.mat-list-base .mat-list-item, .mat-list-base .mat-list-option {
+ height: 38px !important;
+}
diff --git a/src/app/pages/lookups/node-lookup/node-lookup.component.html b/src/app/pages/lookups/node-lookup/node-lookup.component.html
new file mode 100644
index 00000000..719dbef2
--- /dev/null
+++ b/src/app/pages/lookups/node-lookup/node-lookup.component.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+ Alias
+ {{lookupResult.node.alias}}
+
+
+
+ Pub Key
+ {{lookupResult.node.pub_key}}
+
+
+
+ Color
+ {{lookupResult.node?.color}}
+
+
+
+ Last Update
+ {{lookupResult.node.last_update_str}}
+
+
+
+ Total Capacity (Sats)
+ {{lookupResult.total_capacity | number}}
+
+
+
+ Number of Channels
+ {{lookupResult.num_channels | number}}
+
+
+
+
+ Addresses
+
+
+ Network
+ {{address?.network}}
+
+
+ Address
+ {{address?.addr}}
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/pages/lookups/node-lookup/node-lookup.component.spec.ts b/src/app/pages/lookups/node-lookup/node-lookup.component.spec.ts
new file mode 100644
index 00000000..64bc61bf
--- /dev/null
+++ b/src/app/pages/lookups/node-lookup/node-lookup.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NodeLookupComponent } from './node-lookup.component';
+
+describe('NodeLookupComponent', () => {
+ let component: NodeLookupComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ NodeLookupComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NodeLookupComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/lookups/node-lookup/node-lookup.component.ts b/src/app/pages/lookups/node-lookup/node-lookup.component.ts
new file mode 100644
index 00000000..dfb8a241
--- /dev/null
+++ b/src/app/pages/lookups/node-lookup/node-lookup.component.ts
@@ -0,0 +1,24 @@
+import { Component, OnInit, Input } from '@angular/core';
+import { formatDate } from '@angular/common';
+
+import { GraphNode } from '../../../shared/models/lndModels';
+
+@Component({
+ selector: 'rtl-node-lookup',
+ templateUrl: './node-lookup.component.html',
+ styleUrls: ['./node-lookup.component.css']
+})
+export class NodeLookupComponent implements OnInit {
+ @Input() lookupResult: GraphNode;
+ public displayedColumns = ['network', 'addr'];
+
+ constructor() { }
+
+ ngOnInit() {
+ if (undefined !== this.lookupResult && undefined !== this.lookupResult.node && undefined !== this.lookupResult.node.last_update_str) {
+ this.lookupResult.node.last_update_str = (this.lookupResult.node.last_update_str === '') ?
+ '' : formatDate(this.lookupResult.node.last_update_str, 'MMM/dd/yy HH:mm:ss', 'en-US');
+ }
+ }
+
+}
diff --git a/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.html b/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.html
new file mode 100644
index 00000000..816d2551
--- /dev/null
+++ b/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.scss b/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.scss
new file mode 100644
index 00000000..010435ed
--- /dev/null
+++ b/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.scss
@@ -0,0 +1,19 @@
+.mat-menu-panel.child-menu {
+ min-width: 88px;
+ width:88px;
+ border-radius: 0;
+ margin-left: 30%;
+ margin-top: 6%;
+ .mat-menu-content {
+ .mat-menu-item {
+ padding: 0;
+ margin-top: -3px;
+ .mat-icon {
+ margin-right: 0;
+ }
+ button {
+ border-radius: 0;
+ }
+ }
+ }
+}
diff --git a/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.spec.ts b/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.spec.ts
new file mode 100644
index 00000000..04191181
--- /dev/null
+++ b/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HorizontalNavigationComponent } from './horizontal-navigation.component';
+
+describe('HorizontalNavigationComponent', () => {
+ let component: HorizontalNavigationComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ HorizontalNavigationComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(HorizontalNavigationComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.ts b/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.ts
new file mode 100644
index 00000000..c908cf98
--- /dev/null
+++ b/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.ts
@@ -0,0 +1,69 @@
+import { Component, OnInit } from '@angular/core';
+import { Subject } from 'rxjs';
+import { takeUntil, filter } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+import { Actions } from '@ngrx/effects';
+
+import { LoggerService } from '../../../shared/services/logger.service';
+import { MENU_DATA } from '../../../shared/models/navMenu';
+
+import { RTLEffects } from '../../../shared/store/rtl.effects';
+import * as RTLActions from '../../../shared/store/rtl.actions';
+import * as fromRTLReducer from '../../../shared/store/rtl.reducers';
+
+@Component({
+ selector: 'rtl-horizontal-navigation',
+ templateUrl: './horizontal-navigation.component.html',
+ styleUrls: ['./horizontal-navigation.component.scss']
+})
+export class HorizontalNavigationComponent implements OnInit {
+ public menuNodes = [];
+ public logoutNode = [];
+ public showLogout = false;
+ public numPendingChannels = 0;
+ private unSubs = [new Subject(), new Subject(), new Subject()];
+
+ constructor(private logger: LoggerService, private store: Store, private actions$: Actions, private rtlEffects: RTLEffects) {
+ this.menuNodes = MENU_DATA.children;
+ }
+
+ ngOnInit() {
+ this.store.select('rtlRoot')
+ .pipe(takeUntil(this.unSubs[0]))
+ .subscribe((rtlStore: fromRTLReducer.State) => {
+ this.numPendingChannels = rtlStore.numberOfPendingChannels;
+ });
+ this.actions$
+ .pipe(
+ takeUntil(this.unSubs[2]),
+ filter((action) => action.type === RTLActions.SIGNOUT || action.type === RTLActions.SIGNIN)
+ ).subscribe((action) => {
+ this.logger.warn(action);
+ if (action.type === RTLActions.SIGNIN) {
+ this.menuNodes.push({id: 100, parentId: 0, name: 'Logout', icon: 'eject'});
+ }
+ if (action.type === RTLActions.SIGNOUT) {
+ this.menuNodes.pop();
+ }
+ });
+ if (sessionStorage.getItem('token')) {
+ this.menuNodes.push({id: 100, parentId: 0, name: 'Logout', icon: 'eject'});
+ }
+ }
+
+ onClick(node) {
+ if (node.name === 'Logout') {
+ this.store.dispatch(new RTLActions.OpenConfirmation({
+ width: '70%', data: { type: 'CONFIRM', titleMessage: 'Logout from this device?', noBtnText: 'Cancel', yesBtnText: 'Logout'
+ }}));
+ this.rtlEffects.closeConfirm
+ .pipe(takeUntil(this.unSubs[1]))
+ .subscribe(confirmRes => {
+ if (confirmRes) {
+ this.showLogout = false;
+ this.store.dispatch(new RTLActions.Signout());
+ }
+ });
+ }
+ }
+}
diff --git a/src/app/pages/navigation/side-navigation/side-navigation.component.html b/src/app/pages/navigation/side-navigation/side-navigation.component.html
new file mode 100644
index 00000000..2f549a82
--- /dev/null
+++ b/src/app/pages/navigation/side-navigation/side-navigation.component.html
@@ -0,0 +1,65 @@
+
+ R
+ RTL
+
+
+
+
+
Alias: {{information?.alias}}
+
Chain: {{informationChain.chain | titlecase}} [{{informationChain.network | titlecase}}]
+
LND Version: {{information?.version}}
+
+
+
+
+
+ {{node.icon}}
+ {{node.name}}
+
+
+
+
+
+ {{node.icon}}{{node.name}}
+
+
+
+
+
+
+
+ {{node.icon}}
+ {{node.name}}
+
+
+
+
+
+ {{node.icon}}
+ {{node.name}}
+
+
+
+
+ {{node.icon}}
+ {{node.name}}
+
+
+
+
+
+
+
+
+ {{node.icon}}
+ {{node.name}}
+
+
diff --git a/src/app/pages/navigation/side-navigation/side-navigation.component.scss b/src/app/pages/navigation/side-navigation/side-navigation.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/pages/navigation/side-navigation/side-navigation.component.spec.ts b/src/app/pages/navigation/side-navigation/side-navigation.component.spec.ts
new file mode 100644
index 00000000..660c9832
--- /dev/null
+++ b/src/app/pages/navigation/side-navigation/side-navigation.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SideNavigationComponent } from './side-navigation.component';
+
+describe('SideNavigationComponent', () => {
+ let component: SideNavigationComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ SideNavigationComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SideNavigationComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/navigation/side-navigation/side-navigation.component.ts b/src/app/pages/navigation/side-navigation/side-navigation.component.ts
new file mode 100644
index 00000000..d05bb1be
--- /dev/null
+++ b/src/app/pages/navigation/side-navigation/side-navigation.component.ts
@@ -0,0 +1,153 @@
+import { Component, OnInit, OnDestroy, Output, EventEmitter } from '@angular/core';
+import { Router } from '@angular/router';
+import { Subject, Observable, of } from 'rxjs';
+import { takeUntil, filter } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+import { Actions } from '@ngrx/effects';
+import { environment } from '../../../../environments/environment';
+
+import { FlatTreeControl } from '@angular/cdk/tree';
+import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree';
+
+import { Settings } from '../../../shared/models/RTLconfig';
+import { LoggerService } from '../../../shared/services/logger.service';
+import { GetInfo, GetInfoChain } from '../../../shared/models/lndModels';
+import { MenuNode, FlatMenuNode, MENU_DATA } from '../../../shared/models/navMenu';
+
+import { RTLEffects } from '../../../shared/store/rtl.effects';
+import * as RTLActions from '../../../shared/store/rtl.actions';
+import * as fromRTLReducer from '../../../shared/store/rtl.reducers';
+
+@Component({
+ selector: 'rtl-side-navigation',
+ templateUrl: './side-navigation.component.html',
+ styleUrls: ['./side-navigation.component.scss']
+})
+export class SideNavigationComponent implements OnInit, OnDestroy {
+ @Output() ChildNavClicked = new EventEmitter();
+ public version = '';
+ public settings: Settings;
+ public information: GetInfo = {};
+ public informationChain: GetInfoChain = {};
+ public flgLoading = true;
+ public logoutNode = [{id: 100, parentId: 0, name: 'Logout', icon: 'eject'}];
+ public showLogout = false;
+ public numPendingChannels = 0;
+ public smallScreen = false;
+ private unSubs = [new Subject(), new Subject(), new Subject()];
+ treeControl: FlatTreeControl;
+ treeControlLogout: FlatTreeControl;
+ treeFlattener: MatTreeFlattener;
+ treeFlattenerLogout: MatTreeFlattener;
+ navMenus: MatTreeFlatDataSource;
+ navMenusLogout: MatTreeFlatDataSource;
+
+ constructor(private logger: LoggerService, private store: Store, private actions$: Actions, private rtlEffects: RTLEffects, private router: Router) {
+ this.version = environment.VERSION;
+ if (MENU_DATA.children[MENU_DATA.children.length - 1].id === 100) {
+ MENU_DATA.children.pop();
+ }
+ this.treeFlattener = new MatTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren);
+ this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable);
+ this.navMenus = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
+ this.navMenus.data = MENU_DATA.children;
+
+ this.treeFlattenerLogout = new MatTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren);
+ this.treeControlLogout = new FlatTreeControl(this.getLevel, this.isExpandable);
+ this.navMenusLogout = new MatTreeFlatDataSource(this.treeControlLogout, this.treeFlattenerLogout);
+ this.navMenusLogout.data = this.logoutNode;
+ }
+
+ ngOnInit() {
+ this.store.select('rtlRoot')
+ .pipe(takeUntil(this.unSubs[0]))
+ .subscribe((rtlStore: fromRTLReducer.State) => {
+ this.settings = rtlStore.settings;
+ this.information = rtlStore.information;
+ this.numPendingChannels = rtlStore.numberOfPendingChannels;
+
+ if (undefined !== this.information.identity_pubkey) {
+ if (undefined !== this.information.chains && typeof this.information.chains[0] === 'string') {
+ this.informationChain.chain = this.information.chains[0].toString();
+ this.informationChain.network = (this.information.testnet) ? 'Testnet' : 'Mainnet';
+ } else if (typeof this.information.chains[0] === 'object' && this.information.chains[0].hasOwnProperty('chain')) {
+ const getInfoChain = this.information.chains[0];
+ this.informationChain.chain = getInfoChain.chain;
+ this.informationChain.network = getInfoChain.network;
+ }
+ } else {
+ this.informationChain.chain = '';
+ this.informationChain.network = '';
+ }
+
+ this.flgLoading = (undefined !== this.information.identity_pubkey) ? false : true;
+ this.showLogout = (sessionStorage.getItem('token')) ? true : false;
+ if (!sessionStorage.getItem('token')) {
+ this.flgLoading = false;
+ }
+ if (window.innerWidth <= 414) {
+ this.smallScreen = true;
+ }
+ this.logger.info(rtlStore);
+ });
+ this.actions$
+ .pipe(
+ takeUntil(this.unSubs[2]),
+ filter((action) => action.type === RTLActions.SIGNOUT)
+ ).subscribe(() => {
+ this.showLogout = false;
+ });
+ }
+
+ private transformer(node: MenuNode, level: number) { return new FlatMenuNode(!!node.children, level, node.id, node.parentId, node.name, node.icon, node.link); }
+
+ private getLevel(node: FlatMenuNode) { return node.level; }
+
+ private isExpandable(node: FlatMenuNode) { return node.expandable; }
+
+ private getChildren(node: MenuNode): Observable { return of(node.children); }
+
+ hasChild(_: number, _nodeData: FlatMenuNode) { return _nodeData.expandable; }
+
+ toggleTree(node: FlatMenuNode) {
+ this.treeControl.collapseAll();
+ if (node.parentId === 0) {
+ this.treeControl.expandDescendants(node);
+ this.router.navigate([node.link]);
+ } else {
+ const parentNode = this.treeControl.dataNodes.filter(dataNode => {
+ return dataNode.id === node.parentId;
+ })[0];
+ this.treeControl.expandDescendants(parentNode);
+ }
+ }
+
+ onClick(node: MenuNode) {
+ if (node.name === 'Logout') {
+ this.store.dispatch(new RTLActions.OpenConfirmation({
+ width: '70%', data: { type: 'CONFIRM', titleMessage: 'Logout from this device?', noBtnText: 'Cancel', yesBtnText: 'Logout'
+ }}));
+ this.rtlEffects.closeConfirm
+ .pipe(takeUntil(this.unSubs[1]))
+ .subscribe(confirmRes => {
+ if (confirmRes) {
+ this.showLogout = false;
+ this.store.dispatch(new RTLActions.Signout());
+ }
+ });
+ }
+ this.ChildNavClicked.emit(node);
+ }
+
+ onChildNavClicked(node) {
+ this.ChildNavClicked.emit(node);
+ }
+
+ ngOnDestroy() {
+ this.unSubs.forEach(completeSub => {
+ completeSub.next();
+ completeSub.complete();
+ });
+ }
+
+}
diff --git a/src/app/pages/navigation/top-menu/top-menu.component.html b/src/app/pages/navigation/top-menu/top-menu.component.html
new file mode 100644
index 00000000..1946b13a
--- /dev/null
+++ b/src/app/pages/navigation/top-menu/top-menu.component.html
@@ -0,0 +1,22 @@
+
+
+
diff --git a/src/app/pages/navigation/top-menu/top-menu.component.scss b/src/app/pages/navigation/top-menu/top-menu.component.scss
new file mode 100644
index 00000000..6a2bd51c
--- /dev/null
+++ b/src/app/pages/navigation/top-menu/top-menu.component.scss
@@ -0,0 +1,26 @@
+.mat-menu-panel.top-menu{
+ .mat-toolbar, .mat-toolbar-row{
+ height: 100px !important;
+ padding: 0 16px !important;
+ }
+ .info-block{
+ width: 230px;
+ p{
+ font-size: 16px;
+ line-height: 22px;
+ text-align: center;
+ }
+ }
+ .mat-menu-item{
+ height: 36px;
+ line-height: 36px;
+ }
+ .mat-menu-content {
+ p{
+ cursor: default;
+ mat-icon, span, div {
+ cursor: default;
+ }
+ }
+ }
+}
diff --git a/src/app/pages/navigation/top-menu/top-menu.component.spec.ts b/src/app/pages/navigation/top-menu/top-menu.component.spec.ts
new file mode 100644
index 00000000..999b9093
--- /dev/null
+++ b/src/app/pages/navigation/top-menu/top-menu.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TopMenuComponent } from './top-menu.component';
+
+describe('TopMenuComponent', () => {
+ let component: TopMenuComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ TopMenuComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TopMenuComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/navigation/top-menu/top-menu.component.ts b/src/app/pages/navigation/top-menu/top-menu.component.ts
new file mode 100644
index 00000000..c74c01b0
--- /dev/null
+++ b/src/app/pages/navigation/top-menu/top-menu.component.ts
@@ -0,0 +1,93 @@
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { Subject } from 'rxjs/Subject';
+import { takeUntil, filter } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+import { Actions } from '@ngrx/effects';
+
+import { Settings } from '../../../shared/models/RTLconfig';
+import { LoggerService } from '../../../shared/services/logger.service';
+import { GetInfo, GetInfoChain } from '../../../shared/models/lndModels';
+import { environment } from '../../../../environments/environment';
+
+import { RTLEffects } from '../../../shared/store/rtl.effects';
+import * as fromRTLReducer from '../../../shared/store/rtl.reducers';
+import * as RTLActions from '../../../shared/store/rtl.actions';
+
+@Component({
+ selector: 'rtl-top-menu',
+ templateUrl: './top-menu.component.html',
+ styleUrls: ['./top-menu.component.scss']
+})
+export class TopMenuComponent implements OnInit, OnDestroy {
+ public settings: Settings;
+ public version = '';
+ public information: GetInfo = {};
+ public informationChain: GetInfoChain = {};
+ public flgLoading = true;
+ public showLogout = false;
+ private unSubs = [new Subject(), new Subject(), new Subject()];
+
+ constructor(private logger: LoggerService, private store: Store, private rtlEffects: RTLEffects, private actions$: Actions) {
+ this.version = environment.VERSION;
+ }
+
+ ngOnInit() {
+ this.store.select('rtlRoot')
+ .pipe(takeUntil(this.unSubs[0]))
+ .subscribe((rtlStore: fromRTLReducer.State) => {
+ this.settings = rtlStore.settings;
+
+ this.information = rtlStore.information;
+ this.flgLoading = (undefined !== this.information.identity_pubkey) ? false : true;
+
+ if (undefined !== this.information.identity_pubkey) {
+ if (undefined !== this.information.chains && typeof this.information.chains[0] === 'string') {
+ this.informationChain.chain = this.information.chains[0].toString();
+ this.informationChain.network = (this.information.testnet) ? 'Testnet' : 'Mainnet';
+ } else if (typeof this.information.chains[0] === 'object' && this.information.chains[0].hasOwnProperty('chain')) {
+ const getInfoChain = this.information.chains[0];
+ this.informationChain.chain = getInfoChain.chain;
+ this.informationChain.network = getInfoChain.network;
+ }
+ } else {
+ this.informationChain.chain = '';
+ this.informationChain.network = '';
+ }
+ this.showLogout = (sessionStorage.getItem('token')) ? true : false;
+
+ this.logger.info(rtlStore);
+ if (!sessionStorage.getItem('token')) {
+ this.flgLoading = false;
+ }
+ });
+ this.actions$
+ .pipe(
+ takeUntil(this.unSubs[2]),
+ filter((action) => action.type === RTLActions.SIGNOUT)
+ ).subscribe(() => {
+ this.showLogout = false;
+ });
+ }
+
+ onClick() {
+ this.store.dispatch(new RTLActions.OpenConfirmation({
+ width: '70%', data: { type: 'CONFIRM', titleMessage: 'Logout from this device?', noBtnText: 'Cancel', yesBtnText: 'Logout'
+ }}));
+ this.rtlEffects.closeConfirm
+ .pipe(takeUntil(this.unSubs[1]))
+ .subscribe(confirmRes => {
+ if (confirmRes) {
+ this.showLogout = false;
+ this.store.dispatch(new RTLActions.Signout());
+ }
+ });
+ }
+
+ ngOnDestroy() {
+ this.unSubs.forEach(completeSub => {
+ completeSub.next();
+ completeSub.complete();
+ });
+ }
+
+}
diff --git a/src/app/pages/payments/payments.component.html b/src/app/pages/payments/payments.component.html
new file mode 100644
index 00000000..cf89b995
--- /dev/null
+++ b/src/app/pages/payments/payments.component.html
@@ -0,0 +1,81 @@
+
+
+
+
+
+ Verify and Send Payments
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Creation Date |
+ {{payment?.creation_date_str}} |
+
+
+ Payment Hash |
+
+ {{payment?.payment_hash | slice:0:10}}...
+ |
+
+
+ Fee |
+ {{payment?.fee | number}} |
+
+
+ Value |
+ {{payment?.value | number}} |
+
+
+ Payment Pre Image |
+
+ {{payment?.payment_preimage | slice:0:10}}...
+ |
+
+
+ Value MSat |
+ {{payment?.value_msat | number}} |
+
+
+ Value Sat |
+ {{payment?.value_sat | number}} |
+
+
+ Path |
+ {{payment?.path?.length || 0}} Hops |
+
+
+
+
+
+
+
+
+
diff --git a/src/app/pages/payments/payments.component.scss b/src/app/pages/payments/payments.component.scss
new file mode 100644
index 00000000..a59d5a78
--- /dev/null
+++ b/src/app/pages/payments/payments.component.scss
@@ -0,0 +1,43 @@
+.mat-column-path {
+ padding-left: 10px;
+}
+
+.mat-expansion-panel-header {
+ padding: 0;
+}
+
+.mat-accordion .mat-expansion-panel {
+ padding: 0 10px;
+}
+
+.ml-minus-24px {
+ margin-left: -24px;
+}
+
+.info-column {
+ flex: 1 1 34%;
+ box-sizing: border-box;
+ max-width: 34%;
+}
+
+.info-value {
+ flex: 1 1 64%;
+ max-width: 64%;
+ word-break: break-word;
+}
+
+table {
+ width:100%;
+}
+
+.table-container {
+ height: 68vh;
+ overflow: auto;
+}
+
+@media screen and (max-width: 414px) {
+ .table-container {
+ max-height: 46vh;
+ overflow: auto;
+ }
+}
diff --git a/src/app/pages/payments/payments.component.ts b/src/app/pages/payments/payments.component.ts
new file mode 100644
index 00000000..e9c24bd2
--- /dev/null
+++ b/src/app/pages/payments/payments.component.ts
@@ -0,0 +1,192 @@
+import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
+import { formatDate } from '@angular/common';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+
+import { MatTableDataSource, MatSort } from '@angular/material';
+import { Settings } from '../../shared/models/RTLconfig';
+import { GetInfo, Payment, PayRequest } from '../../shared/models/lndModels';
+import { LoggerService } from '../../shared/services/logger.service';
+
+import { newlyAddedRowAnimation } from '../../shared/animation/row-animation';
+import { RTLEffects } from '../../shared/store/rtl.effects';
+import * as RTLActions from '../../shared/store/rtl.actions';
+import * as fromRTLReducer from '../../shared/store/rtl.reducers';
+
+@Component({
+ selector: 'rtl-payments',
+ templateUrl: './payments.component.html',
+ styleUrls: ['./payments.component.scss'],
+ animations: [newlyAddedRowAnimation]
+})
+export class PaymentsComponent implements OnInit, OnDestroy {
+ @ViewChild(MatSort) sort: MatSort;
+ @ViewChild('sendPaymentForm') form;
+ public newlyAddedPayment = '';
+ public flgAnimate = true;
+ public settings: Settings;
+ public flgLoading: Array = [true];
+ public information: GetInfo = {};
+ public payments: any;
+ public paymentJSONArr: Payment[] = [];
+ public displayedColumns = [];
+ public paymentDecoded: PayRequest = {};
+ public paymentRequest = '';
+ private unsub: Array> = [new Subject(), new Subject(), new Subject(), new Subject()];
+
+ constructor(private logger: LoggerService, private store: Store, private rtlEffects: RTLEffects) {
+ switch (true) {
+ case (window.innerWidth <= 415):
+ this.displayedColumns = ['creation_date', 'fee', 'value'];
+ break;
+ case (window.innerWidth > 415 && window.innerWidth <= 730):
+ this.displayedColumns = ['creation_date', 'payment_hash', 'fee', 'value', 'payment_preimage'];
+ break;
+ case (window.innerWidth > 730 && window.innerWidth <= 1024):
+ this.displayedColumns = ['creation_date', 'payment_hash', 'fee', 'value', 'payment_preimage', 'path'];
+ break;
+ case (window.innerWidth > 1024 && window.innerWidth <= 1280):
+ this.displayedColumns = ['creation_date', 'payment_hash', 'fee', 'value', 'payment_preimage', 'value_msat', 'value_sat', 'path'];
+ break;
+ default:
+ this.displayedColumns = ['creation_date', 'payment_hash', 'fee', 'value', 'payment_preimage', 'value_msat', 'value_sat', 'path'];
+ break;
+ }
+ }
+
+ ngOnInit() {
+ this.store.select('rtlRoot')
+ .pipe(takeUntil(this.unsub[0]))
+ .subscribe((rtlStore: fromRTLReducer.State) => {
+ rtlStore.effectErrors.forEach(effectsErr => {
+ if (effectsErr.action === 'FetchPayments') {
+ this.flgLoading[0] = 'error';
+ }
+ });
+ this.settings = rtlStore.settings;
+ this.information = rtlStore.information;
+ this.paymentJSONArr = (rtlStore.payments.length > 0) ? rtlStore.payments : [];
+ this.payments = (undefined === rtlStore.payments) ? new MatTableDataSource([]) : new MatTableDataSource([...this.paymentJSONArr]);
+ this.payments.data = this.paymentJSONArr;
+ this.payments.sort = this.sort;
+ this.payments.data.forEach(payment => {
+ payment.creation_date_str = (payment.creation_date_str === '') ? '' : formatDate(payment.creation_date_str, 'MMM/dd/yy HH:mm:ss', 'en-US');
+ });
+ setTimeout(() => { this.flgAnimate = false; }, 5000);
+ if (this.flgLoading[0] !== 'error') {
+ this.flgLoading[0] = (undefined !== this.paymentJSONArr[0]) ? false : true;
+ }
+ this.logger.info(rtlStore);
+ });
+
+ }
+
+ onSendPayment() {
+ if (undefined !== this.paymentDecoded.timestamp_str) {
+ this.sendPayment();
+ } else {
+ this.store.dispatch(new RTLActions.OpenSpinner('Decoding Payment...'));
+ this.store.dispatch(new RTLActions.DecodePayment(this.paymentRequest));
+ this.rtlEffects.setDecodedPayment
+ .pipe(takeUntil(this.unsub[1]))
+ .subscribe(decodedPayment => {
+ this.paymentDecoded = decodedPayment;
+ if (undefined !== this.paymentDecoded.timestamp_str) {
+ this.paymentDecoded.timestamp_str = (this.paymentDecoded.timestamp_str === '') ? '' :
+ formatDate(this.paymentDecoded.timestamp_str, 'MMM/dd/yy HH:mm:ss', 'en-US');
+ if (undefined === this.paymentDecoded.num_satoshis) {
+ this.paymentDecoded.num_satoshis = '0';
+ }
+ this.sendPayment();
+ } else {
+ this.resetData();
+ }
+ });
+ }
+
+ }
+
+ sendPayment() {
+ this.flgAnimate = true;
+ this.newlyAddedPayment = this.paymentDecoded.payment_hash;
+ if (undefined === this.paymentDecoded.num_satoshis || this.paymentDecoded.num_satoshis === '' || this.paymentDecoded.num_satoshis === '0') {
+ const titleMsg = 'This is an empty invoice. Enter the amount (Sats) to pay.';
+ this.store.dispatch(new RTLActions.OpenConfirmation({ width: '70%', data: {
+ type: 'CONFIRM', titleMessage: titleMsg, message: JSON.stringify(this.paymentDecoded), noBtnText: 'Cancel', yesBtnText: 'Send', flgShowInput: true, getInputs: [
+ {placeholder: 'Amount (Sats)', inputType: 'number', inputValue: ''}
+ ]
+ }}));
+ this.rtlEffects.closeConfirm
+ .pipe(takeUntil(this.unsub[2]))
+ .subscribe(confirmRes => {
+ if (confirmRes) {
+ this.paymentDecoded.num_satoshis = confirmRes[0].inputValue;
+ this.store.dispatch(new RTLActions.OpenSpinner('Sending Payment...'));
+ this.store.dispatch(new RTLActions.SendPayment([this.paymentRequest, this.paymentDecoded, true]));
+ this.resetData();
+ }
+ });
+ } else {
+ this.store.dispatch(new RTLActions.OpenConfirmation({ width: '70%', data: {
+ type: 'CONFIRM', titleMessage: 'Send Payment', noBtnText: 'Cancel', yesBtnText: 'Send', message: JSON.stringify(this.paymentDecoded)
+ }}));
+ this.rtlEffects.closeConfirm
+ .pipe(takeUntil(this.unsub[3]))
+ .subscribe(confirmRes => {
+ if (confirmRes) {
+ this.store.dispatch(new RTLActions.OpenSpinner('Sending Payment...'));
+ this.store.dispatch(new RTLActions.SendPayment([this.paymentRequest, this.paymentDecoded, false]));
+ this.resetData();
+ }
+ });
+ }
+ }
+
+ onVerifyPayment() {
+ this.store.dispatch(new RTLActions.OpenSpinner('Decoding Payment...'));
+ this.store.dispatch(new RTLActions.DecodePayment(this.paymentRequest));
+ this.rtlEffects.setDecodedPayment.subscribe(decodedPayment => {
+ this.paymentDecoded = decodedPayment;
+ if (undefined !== this.paymentDecoded.timestamp_str) {
+ this.paymentDecoded.timestamp_str = (this.paymentDecoded.timestamp_str === '') ? '' :
+ formatDate(this.paymentDecoded.timestamp_str, 'MMM/dd/yy HH:mm:ss', 'en-US');
+ } else {
+ this.resetData();
+ }
+ });
+ }
+
+ resetData() {
+ this.form.reset();
+ }
+
+ onPaymentClick(selRow: Payment, event: any) {
+ const flgExpansionClicked = event.target.className.includes('mat-expansion-panel-header') || event.target.className.includes('mat-expansion-indicator');
+ if (flgExpansionClicked) {
+ return;
+ }
+ const selPayment = this.payments.data.filter(payment => {
+ return payment.payment_hash === selRow.payment_hash;
+ })[0];
+ const reorderedPayment = JSON.parse(JSON.stringify(selPayment, [
+ 'creation_date_str', 'payment_hash', 'fee', 'value_msat', 'value_sat', 'value', 'payment_preimage', 'path'
+ ] , 2));
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: {
+ type: 'INFO',
+ message: JSON.stringify(reorderedPayment)
+ }}));
+ }
+
+ applyFilter(selFilter: string) {
+ this.payments.filter = selFilter;
+ }
+
+ ngOnDestroy() {
+ this.unsub.forEach(completeSub => {
+ completeSub.next();
+ completeSub.complete();
+ });
+ }
+
+}
diff --git a/src/app/pages/peers/peers.component.html b/src/app/pages/peers/peers.component.html
new file mode 100644
index 00000000..f29dacfd
--- /dev/null
+++ b/src/app/pages/peers/peers.component.html
@@ -0,0 +1,87 @@
+
+
+
+
+
+ Add Peer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Detach |
+ link_off |
+
+
+ Pub Key |
+
+ {{peer?.pub_key | slice:0:10}}...
+ |
+
+
+ Alias |
+ {{peer?.alias}} |
+
+
+ Address |
+ {{peer?.address}} |
+
+
+ Bytes Sent |
+ {{peer?.bytes_sent | number}} |
+
+
+ Bytes Recv |
+ {{peer?.bytes_recv | number}} |
+
+
+ {{information?.smaller_currency_unit}} Sent |
+ {{peer?.sat_sent | number}} |
+
+
+ {{information?.smaller_currency_unit}} Recv |
+ {{peer?.sat_recv | number}} |
+
+
+ Inbound |
+ {{peer?.inbound}} |
+
+
+ Ping |
+ {{peer?.ping_time | number}} |
+
+
+
+
+
+
+
+
+
diff --git a/src/app/pages/peers/peers.component.scss b/src/app/pages/peers/peers.component.scss
new file mode 100644
index 00000000..4d8df74b
--- /dev/null
+++ b/src/app/pages/peers/peers.component.scss
@@ -0,0 +1,30 @@
+.mat-column-detach {
+ flex: 0 0 5%;
+ min-width: 50px;
+}
+
+.mat-column-alias, .mat-column-address {
+ flex: 0 0 15%;
+ min-width: 100px;
+}
+
+mat-cell.mat-column-detach {
+ cursor: pointer;
+}
+
+
+table {
+ width:100%;
+}
+
+.table-container {
+ height: 68vh;
+ overflow: auto;
+}
+
+@media screen and (max-width: 414px) {
+ .table-container {
+ height: 40vh;
+ overflow: auto;
+ }
+}
diff --git a/src/app/pages/peers/peers.component.spec.ts b/src/app/pages/peers/peers.component.spec.ts
new file mode 100644
index 00000000..a5fe2db8
--- /dev/null
+++ b/src/app/pages/peers/peers.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PeersComponent } from './peers.component';
+
+describe('PeersComponent', () => {
+ let component: PeersComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ PeersComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PeersComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/peers/peers.component.ts b/src/app/pages/peers/peers.component.ts
new file mode 100644
index 00000000..7b23b2ac
--- /dev/null
+++ b/src/app/pages/peers/peers.component.ts
@@ -0,0 +1,157 @@
+import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
+import { Subject } from 'rxjs';
+import { takeUntil, filter, take } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+import { Actions } from '@ngrx/effects';
+
+import { MatTableDataSource, MatSort } from '@angular/material';
+import { Peer, GetInfo } from '../../shared/models/lndModels';
+import { LoggerService } from '../../shared/services/logger.service';
+
+import { newlyAddedRowAnimation } from '../../shared/animation/row-animation';
+import { RTLEffects } from '../../shared/store/rtl.effects';
+import * as RTLActions from '../../shared/store/rtl.actions';
+import * as fromRTLReducer from '../../shared/store/rtl.reducers';
+
+@Component({
+ selector: 'rtl-peers',
+ templateUrl: './peers.component.html',
+ styleUrls: ['./peers.component.scss'],
+ animations: [newlyAddedRowAnimation]
+})
+export class PeersComponent implements OnInit, OnDestroy {
+ @ViewChild(MatSort) sort: MatSort;
+ public newlyAddedPeer = '';
+ public flgAnimate = true;
+ public displayedColumns = [];
+ public peerAddress = '';
+ public peers: any;
+ public information: GetInfo = {};
+ public flgLoading: Array = [true]; // 0: peers
+ private unSubs: Array> = [new Subject(), new Subject(), new Subject(), new Subject()];
+
+ constructor(private logger: LoggerService, private store: Store, private rtlEffects: RTLEffects, private actions$: Actions) {
+ switch (true) {
+ case (window.innerWidth <= 415):
+ this.displayedColumns = ['detach', 'pub_key', 'alias'];
+ break;
+ case (window.innerWidth > 415 && window.innerWidth <= 730):
+ this.displayedColumns = ['detach', 'pub_key', 'alias', 'address', 'sat_sent', 'sat_recv'];
+ break;
+ case (window.innerWidth > 730 && window.innerWidth <= 1024):
+ this.displayedColumns = ['detach', 'pub_key', 'alias', 'address', 'sat_sent', 'sat_recv', 'inbound'];
+ break;
+ case (window.innerWidth > 1024 && window.innerWidth <= 1280):
+ this.displayedColumns = ['detach', 'pub_key', 'alias', 'address', 'sat_sent', 'sat_recv', 'inbound', 'ping_time'];
+ break;
+ default:
+ this.displayedColumns = ['detach', 'pub_key', 'alias', 'address', 'bytes_sent', 'bytes_recv', 'sat_sent', 'sat_recv', 'inbound', 'ping_time'];
+ break;
+ }
+ }
+
+ ngOnInit() {
+ this.store.select('rtlRoot')
+ .pipe(takeUntil(this.unSubs[0]))
+ .subscribe((rtlStore: fromRTLReducer.State) => {
+ rtlStore.effectErrors.forEach(effectsErr => {
+ if (effectsErr.action === 'FetchPeers') {
+ this.flgLoading[0] = 'error';
+ }
+ });
+ this.information = rtlStore.information;
+ this.peers = new MatTableDataSource([]);
+ this.peers.data = [];
+ if (undefined !== rtlStore.peers) {
+ this.peers = new MatTableDataSource([...rtlStore.peers]);
+ this.peers.data = rtlStore.peers;
+ setTimeout(() => { this.flgAnimate = false; }, 5000);
+ }
+ this.peers.sort = this.sort;
+ if (this.flgLoading[0] !== 'error') {
+ this.flgLoading[0] = false;
+ }
+ this.logger.info(rtlStore);
+ });
+ this.actions$
+ .pipe(
+ takeUntil(this.unSubs[1]),
+ filter((action) => action.type === RTLActions.SET_PEERS)
+ ).subscribe((setPeers: RTLActions.SetPeers) => {
+ this.peerAddress = undefined;
+ });
+ }
+
+ onAddPeer(form: any) {
+ this.flgAnimate = true;
+ const pattern = '^([a-zA-Z0-9]){1,66}@(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]):[0-9]+$';
+ const deviderIndex = this.peerAddress.search('@');
+ let pubkey = '';
+ let host = '';
+
+ if (new RegExp(pattern).test(this.peerAddress)) {
+ pubkey = this.peerAddress.substring(0, deviderIndex);
+ host = this.peerAddress.substring(deviderIndex + 1);
+ this.addPeerWithParams(pubkey, host);
+ } else {
+ pubkey = (deviderIndex > -1) ? this.peerAddress.substring(0, deviderIndex) : this.peerAddress;
+ this.store.dispatch(new RTLActions.OpenSpinner('Getting Node Address...'));
+ this.store.dispatch(new RTLActions.FetchGraphNode(pubkey));
+ this.rtlEffects.setGraphNode
+ .pipe(take(1))
+ .subscribe(graphNode => {
+ host = (undefined === graphNode.node.addresses || undefined === graphNode.node.addresses[0].addr) ? '' : graphNode.node.addresses[0].addr;
+ this.addPeerWithParams(pubkey, host);
+ });
+ }
+ }
+
+ addPeerWithParams(pubkey: string, host: string) {
+ this.newlyAddedPeer = pubkey;
+ this.store.dispatch(new RTLActions.OpenSpinner('Adding Peer...'));
+ this.store.dispatch(new RTLActions.SaveNewPeer({pubkey: pubkey, host: host, perm: false}));
+ }
+
+ onPeerClick(selRow: Peer, event: any) {
+ const flgCloseClicked = event.target.className.includes('mat-column-detach') || event.target.className.includes('mat-icon');
+ if (flgCloseClicked) {
+ return;
+ }
+ const selPeer = this.peers.data.filter(peer => {
+ return peer.pub_key === selRow.pub_key;
+ })[0];
+ const reorderedPeer = JSON.parse(JSON.stringify(selPeer, [
+ 'pub_key', 'alias', 'address', 'bytes_sent', 'bytes_recv', 'sat_sent', 'sat_recv', 'inbound', 'ping_time'
+ ] , 2));
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: { type: 'INFO', message: JSON.stringify(reorderedPeer)}}));
+ }
+
+ resetData() {
+ this.peerAddress = '';
+ }
+
+ onPeerDetach(peerToDetach: Peer) {
+ const msg = 'Detach peer: ' + peerToDetach.pub_key;
+ const msg_type = 'CONFIRM';
+ this.store.dispatch(new RTLActions.OpenConfirmation({ width: '70%', data: { type: msg_type, titleMessage: msg, noBtnText: 'Cancel', yesBtnText: 'Detach'}}));
+ this.rtlEffects.closeConfirm
+ .pipe(takeUntil(this.unSubs[3]))
+ .subscribe(confirmRes => {
+ if (confirmRes) {
+ this.store.dispatch(new RTLActions.OpenSpinner('Detaching Peer...'));
+ this.store.dispatch(new RTLActions.DetachPeer({pubkey: peerToDetach.pub_key}));
+ }
+ });
+ }
+
+ applyFilter(selFilter: string) {
+ this.peers.filter = selFilter;
+ }
+
+ ngOnDestroy() {
+ this.unSubs.forEach(completeSub => {
+ completeSub.next();
+ completeSub.complete();
+ });
+ }
+}
diff --git a/src/app/pages/server-config/server-config.component.html b/src/app/pages/server-config/server-config.component.html
new file mode 100644
index 00000000..f412064d
--- /dev/null
+++ b/src/app/pages/server-config/server-config.component.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+ Show Configurations
+
+
+
+
+
+
+
+
+ = 0">{{conf}}
+
+
+ {{conf}}
+
+
+
+
+
+
+
+
+
diff --git a/src/app/pages/server-config/server-config.component.scss b/src/app/pages/server-config/server-config.component.scss
new file mode 100644
index 00000000..15d29fee
--- /dev/null
+++ b/src/app/pages/server-config/server-config.component.scss
@@ -0,0 +1,3 @@
+h4 {
+ word-break: break-word;
+}
diff --git a/src/app/pages/server-config/server-config.component.ts b/src/app/pages/server-config/server-config.component.ts
new file mode 100644
index 00000000..ec82a59e
--- /dev/null
+++ b/src/app/pages/server-config/server-config.component.ts
@@ -0,0 +1,72 @@
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+
+import { RTLEffects } from '../../shared/store/rtl.effects';
+import * as RTLActions from '../../shared/store/rtl.actions';
+import * as fromRTLReducer from '../../shared/store/rtl.reducers';
+import { Authentication } from '../../shared/models/RTLconfig';
+
+@Component({
+ selector: 'rtl-server-config',
+ templateUrl: './server-config.component.html',
+ styleUrls: ['./server-config.component.scss']
+})
+export class ServerConfigComponent implements OnInit, OnDestroy {
+ public selectedNodeType = 'lnd';
+ public authSettings: Authentication = {};
+ public showLND = false;
+ public showBitcoind = false;
+ public configData = '';
+ private unsubs: Array> = [new Subject(), new Subject()];
+
+ constructor(private store: Store, private rtlEffects: RTLEffects) {}
+
+ ngOnInit() {
+ this.store.select('rtlRoot')
+ .pipe(takeUntil(this.unsubs[0]))
+ .subscribe((rtlStore: fromRTLReducer.State) => {
+ rtlStore.effectErrors.forEach(effectsErr => {
+ if (effectsErr.action === 'fetchConfig') {
+ this.resetData();
+ }
+ });
+ this.authSettings = rtlStore.authSettings;
+ if (undefined !== this.authSettings && this.authSettings.lndConfigPath !== '') {
+ this.showLND = true;
+ }
+ if (undefined !== this.authSettings && undefined !== this.authSettings.bitcoindConfigPath && this.authSettings.bitcoindConfigPath !== '') {
+ this.showBitcoind = true;
+ }
+ });
+ }
+
+ onSelectionChange(event) {
+ this.selectedNodeType = event.value;
+ this.configData = '';
+ }
+
+ onShowConfig() {
+ this.store.dispatch(new RTLActions.OpenSpinner('Opening Config File...'));
+ this.store.dispatch(new RTLActions.FetchConfig(this.selectedNodeType));
+ this.rtlEffects.showLNDConfig
+ .pipe(takeUntil(this.unsubs[1]))
+ .subscribe((configFile: any) => {
+ this.configData = (configFile === '' || undefined === configFile) ? [] : configFile.split('\n');
+ });
+ }
+
+ resetData() {
+ this.configData = '';
+ this.selectedNodeType = 'lnd';
+ }
+
+ ngOnDestroy() {
+ this.unsubs.forEach(completeSub => {
+ completeSub.next();
+ completeSub.complete();
+ });
+ }
+
+}
diff --git a/src/app/pages/signin/signin.component.html b/src/app/pages/signin/signin.component.html
new file mode 100644
index 00000000..6a141a34
--- /dev/null
+++ b/src/app/pages/signin/signin.component.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+ Login to RTL
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/pages/signin/signin.component.scss b/src/app/pages/signin/signin.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/pages/signin/signin.component.ts b/src/app/pages/signin/signin.component.ts
new file mode 100644
index 00000000..0a166d32
--- /dev/null
+++ b/src/app/pages/signin/signin.component.ts
@@ -0,0 +1,59 @@
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+
+import { LoggerService } from '../../shared/services/logger.service';
+import * as fromRTLReducer from '../../shared/store/rtl.reducers';
+import * as RTLActions from '../../shared/store/rtl.actions';
+
+@Component({
+ selector: 'rtl-signin',
+ templateUrl: './signin.component.html',
+ styleUrls: ['./signin.component.scss']
+})
+export class SigninComponent implements OnInit, OnDestroy {
+ password = '';
+ nodeAuthType = '';
+ rtlSSO = 0;
+ rtlCookiePath = '';
+ hintStr = '';
+ accessKey = '';
+
+ private unsub: Array> = [new Subject(), new Subject(), new Subject()];
+
+ constructor(private logger: LoggerService, private store: Store) { }
+
+ ngOnInit() {
+ this.store.select('rtlRoot')
+ .pipe(takeUntil(this.unsub[0]))
+ .subscribe((rtlStore: fromRTLReducer.State) => {
+ rtlStore.effectErrors.forEach(effectsErr => {
+ this.logger.error(effectsErr);
+ });
+ this.nodeAuthType = rtlStore.authSettings.nodeAuthType;
+ this.logger.info(rtlStore);
+ if (this.nodeAuthType.toUpperCase() === 'DEFAULT') {
+ this.hintStr = 'Enter RPC password';
+ } else {
+ this.hintStr = ''; // Do not remove, initial passowrd 'DEFAULT' is initilizing its value
+ }
+ });
+ }
+
+ onSignin() {
+ this.store.dispatch(new RTLActions.Signin(window.btoa(this.password)));
+ }
+
+ resetData() {
+ this.password = '';
+ }
+
+ ngOnDestroy() {
+ this.unsub.forEach(completeSub => {
+ completeSub.next();
+ completeSub.complete();
+ });
+ }
+
+}
diff --git a/src/app/pages/switch/forwarding-history.component.html b/src/app/pages/switch/forwarding-history.component.html
new file mode 100644
index 00000000..52b8e8ae
--- /dev/null
+++ b/src/app/pages/switch/forwarding-history.component.html
@@ -0,0 +1,87 @@
+
+
+
+
+
+ Forwarding History
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Timestamp |
+ {{fhEvent.timestamp_str}} |
+
+
+ Chan Id In |
+ {{fhEvent.chan_id_in}} |
+
+
+ Alias In |
+ {{fhEvent.alias_in}} |
+
+
+ Chan Id Out |
+ {{fhEvent.chan_id_out}} |
+
+
+ Alias Out |
+ {{fhEvent.alias_out}} |
+
+
+ Amount
+ Out (Sats) |
+ {{fhEvent.amt_out | number}} |
+
+
+ Amount
+ In (Sats) |
+ {{fhEvent.amt_in | number}} |
+
+
+ Fee
+ (Sats) |
+ {{fhEvent.fee | number}} |
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/pages/switch/forwarding-history.component.scss b/src/app/pages/switch/forwarding-history.component.scss
new file mode 100644
index 00000000..bf2b3bd5
--- /dev/null
+++ b/src/app/pages/switch/forwarding-history.component.scss
@@ -0,0 +1,21 @@
+.mat-column-amt_in {
+ flex: 0 0 15%;
+ min-width: 120px;
+ padding-right: 20px;
+}
+
+table {
+ width:100%;
+}
+
+.table-container {
+ height: 72vh;
+ overflow: auto;
+}
+
+@media screen and (max-width: 414px) {
+ .table-container {
+ height: 53vh;
+ overflow: auto;
+ }
+}
diff --git a/src/app/pages/switch/forwarding-history.component.spec.ts b/src/app/pages/switch/forwarding-history.component.spec.ts
new file mode 100644
index 00000000..f5c03c97
--- /dev/null
+++ b/src/app/pages/switch/forwarding-history.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ForwardingHistoryComponent } from './forwarding-history.component';
+
+describe('ForwardingHistoryComponent', () => {
+ let component: ForwardingHistoryComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ForwardingHistoryComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ForwardingHistoryComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/switch/forwarding-history.component.ts b/src/app/pages/switch/forwarding-history.component.ts
new file mode 100644
index 00000000..ff0678dc
--- /dev/null
+++ b/src/app/pages/switch/forwarding-history.component.ts
@@ -0,0 +1,123 @@
+import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
+import { formatDate } from '@angular/common';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+
+import { MatTableDataSource, MatSort } from '@angular/material';
+import { ForwardingEvent } from '../../shared/models/lndModels';
+import { LoggerService } from '../../shared/services/logger.service';
+
+import * as RTLActions from '../../shared/store/rtl.actions';
+import * as fromRTLReducer from '../../shared/store/rtl.reducers';
+
+@Component({
+ selector: 'rtl-forwarding-history',
+ templateUrl: './forwarding-history.component.html',
+ styleUrls: ['./forwarding-history.component.scss']
+})
+export class ForwardingHistoryComponent implements OnInit, OnDestroy {
+ @ViewChild(MatSort) sort: MatSort;
+ public displayedColumns = [];
+ public forwardingHistoryEvents: any;
+ public lastOffsetIndex = 0;
+ public flgLoading: Array = [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;
+ private unsub: Array> = [new Subject(), new Subject()];
+
+ constructor(private logger: LoggerService, private store: Store) {
+ switch (true) {
+ case (window.innerWidth <= 415):
+ this.displayedColumns = ['timestamp', 'amt_out', 'amt_in'];
+ break;
+ case (window.innerWidth > 415 && window.innerWidth <= 730):
+ this.displayedColumns = ['timestamp', 'amt_out', 'amt_in', 'fee'];
+ break;
+ case (window.innerWidth > 730 && window.innerWidth <= 1024):
+ this.displayedColumns = ['timestamp', 'chan_id_in', 'chan_id_out', 'amt_out', 'amt_in', 'fee'];
+ break;
+ case (window.innerWidth > 1024 && window.innerWidth <= 1280):
+ this.displayedColumns = ['timestamp', 'chan_id_in', 'chan_id_out', 'amt_out', 'amt_in', 'fee'];
+ break;
+ default:
+ this.displayedColumns = ['timestamp', 'chan_id_in', 'chan_id_out', 'amt_out', 'amt_in', 'fee'];
+ break;
+ }
+ }
+
+ ngOnInit() {
+ this.store.dispatch(new RTLActions.GetForwardingHistory({}));
+ this.store.select('rtlRoot')
+ .pipe(takeUntil(this.unsub[0]))
+ .subscribe((rtlStore: fromRTLReducer.State) => {
+ rtlStore.effectErrors.forEach(effectsErr => {
+ if (effectsErr.action === 'GetForwardingHistory') {
+ this.flgLoading[0] = 'error';
+ }
+ });
+ if (undefined !== rtlStore.forwardingHistory && undefined !== rtlStore.forwardingHistory.forwarding_events) {
+ this.lastOffsetIndex = rtlStore.forwardingHistory.last_offset_index;
+ this.loadForwardingEventsTable(rtlStore.forwardingHistory.forwarding_events);
+ }
+ if (this.flgLoading[0] !== 'error') {
+ this.flgLoading[0] = (undefined !== rtlStore.forwardingHistory) ? false : true;
+ }
+ this.logger.info(rtlStore);
+ });
+
+ }
+
+ onForwardingEventClick(selRow: ForwardingEvent, event: any) {
+ const selFEvent = this.forwardingHistoryEvents.data.filter(fhEvent => {
+ return fhEvent.chan_id_in === selRow.chan_id_in;
+ })[0];
+ const reorderedFHEvent = JSON.parse(JSON.stringify(selFEvent, ['timestamp_str', 'chan_id_in', 'alias_in', 'chan_id_out', 'alias_out', 'amt_out', 'amt_in', 'fee'] , 2));
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: {
+ type: 'INFO',
+ message: JSON.stringify(reorderedFHEvent)
+ }}));
+ }
+
+ loadForwardingEventsTable(forwardingEvents: ForwardingEvent[]) {
+ this.forwardingHistoryEvents = new MatTableDataSource([...forwardingEvents]);
+ this.forwardingHistoryEvents.sort = this.sort;
+ this.forwardingHistoryEvents.data.forEach(event => {
+ event.timestamp_str = (event.timestamp_str === '') ? '' : formatDate(event.timestamp_str, 'MMM/dd/yy HH:mm:ss', 'en-US');
+ });
+
+ this.logger.info(this.forwardingHistoryEvents);
+ }
+
+ onForwardingHistoryFetch() {
+ 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.GetForwardingHistory({
+ 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 = [];
+ }
+ }
+
+ ngOnDestroy() {
+ this.resetData();
+ this.unsub.forEach(completeSub => {
+ completeSub.next();
+ completeSub.complete();
+ });
+ }
+
+}
diff --git a/src/app/pages/transactions/list-transactions/list-transactions.component.html b/src/app/pages/transactions/list-transactions/list-transactions.component.html
new file mode 100644
index 00000000..89d225d5
--- /dev/null
+++ b/src/app/pages/transactions/list-transactions/list-transactions.component.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+ Transactions
+
+
+
+
+
+
+
+
+
+
+
+
+ Destination Addresses |
+ {{trans?.dest_addresses?.length || 0}} Addr |
+
+
+ Timestamp |
+ {{trans.time_stamp_str}} |
+
+
+ Num Confirmations |
+ {{trans.num_confirmations | number}} |
+
+
+ Total Fees |
+ {{trans.total_fees | number}} |
+
+
+ Block Hash |
+ {{trans.block_hash | slice:0:10}}... |
+
+
+ Block Height |
+ {{trans.block_height | number}} |
+
+
+ Txn Hash |
+ {{trans.tx_hash | slice:0:10}}... |
+
+
+ Amount |
+ {{trans.amount | number}} |
+
+
+
+
+
+
+
+
+
diff --git a/src/app/pages/transactions/list-transactions/list-transactions.component.scss b/src/app/pages/transactions/list-transactions/list-transactions.component.scss
new file mode 100644
index 00000000..f02ab006
--- /dev/null
+++ b/src/app/pages/transactions/list-transactions/list-transactions.component.scss
@@ -0,0 +1,31 @@
+.mat-column-tx_hash, .mat-column-block_hash {
+ padding-left: 10px;
+}
+
+.mat-expansion-panel-header {
+ padding: 0;
+}
+
+.mat-accordion .mat-expansion-panel {
+ padding: 0 10px;
+}
+
+.ml-minus-24px {
+ margin-left: -24px;
+}
+
+table {
+ width:100%;
+}
+
+.table-container {
+ height: 78vh;
+ overflow: auto;
+}
+
+@media screen and (max-width: 414px) {
+ .table-container {
+ height: 68vh;
+ overflow: auto;
+ }
+}
diff --git a/src/app/pages/transactions/list-transactions/list-transactions.component.spec.ts b/src/app/pages/transactions/list-transactions/list-transactions.component.spec.ts
new file mode 100644
index 00000000..9222f131
--- /dev/null
+++ b/src/app/pages/transactions/list-transactions/list-transactions.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ListTransactionsComponent } from './list-transactions.component';
+
+describe('ListTransactionsComponent', () => {
+ let component: ListTransactionsComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ ListTransactionsComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ListTransactionsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/transactions/list-transactions/list-transactions.component.ts b/src/app/pages/transactions/list-transactions/list-transactions.component.ts
new file mode 100644
index 00000000..75ca8ea2
--- /dev/null
+++ b/src/app/pages/transactions/list-transactions/list-transactions.component.ts
@@ -0,0 +1,110 @@
+import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
+import { formatDate } from '@angular/common';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+
+import { MatTableDataSource, MatSort } from '@angular/material';
+import { Transaction } from '../../../shared/models/lndModels';
+import { LoggerService } from '../../../shared/services/logger.service';
+
+import { RTLEffects } from '../../../shared/store/rtl.effects';
+import * as RTLActions from '../../../shared/store/rtl.actions';
+import * as fromRTLReducer from '../../../shared/store/rtl.reducers';
+
+@Component({
+ selector: 'rtl-list-transactions',
+ templateUrl: './list-transactions.component.html',
+ styleUrls: ['./list-transactions.component.scss']
+})
+export class ListTransactionsComponent implements OnInit, OnDestroy {
+ @ViewChild(MatSort) sort: MatSort;
+ public displayedColumns = [];
+ public listTransactions: any;
+ public flgLoading: Array = [true];
+ private unsub: Array> = [new Subject(), new Subject()];
+
+ constructor(private logger: LoggerService, private store: Store, private rtlEffects: RTLEffects) {
+ switch (true) {
+ case (window.innerWidth <= 415):
+ this.displayedColumns = ['dest_addresses', 'total_fees', 'amount'];
+ break;
+ case (window.innerWidth > 415 && window.innerWidth <= 730):
+ this.displayedColumns = ['dest_addresses', 'time_stamp', 'total_fees', 'amount'];
+ break;
+ case (window.innerWidth > 730 && window.innerWidth <= 1024):
+ this.displayedColumns = ['dest_addresses', 'time_stamp', 'num_confirmations', 'total_fees', 'tx_hash', 'amount'];
+ break;
+ case (window.innerWidth > 1024 && window.innerWidth <= 1280):
+ this.displayedColumns = ['dest_addresses', 'time_stamp', 'num_confirmations', 'total_fees', 'tx_hash', 'amount'];
+ break;
+ default:
+ this.displayedColumns = ['dest_addresses', 'time_stamp', 'num_confirmations', 'total_fees', 'block_hash', 'block_height', 'tx_hash', 'amount'];
+ break;
+ }
+ }
+
+ ngOnInit() {
+ this.store.dispatch(new RTLActions.FetchTransactions());
+ this.store.select('rtlRoot')
+ .pipe(takeUntil(this.unsub[0]))
+ .subscribe((rtlStore: fromRTLReducer.State) => {
+ rtlStore.effectErrors.forEach(effectsErr => {
+ if (effectsErr.action === 'FetchTransactions') {
+ this.flgLoading[0] = 'error';
+ }
+ });
+ if (undefined !== rtlStore.transactions && rtlStore.transactions.length > 0) {
+ this.loadTransactionsTable(rtlStore.transactions);
+ }
+ if (this.flgLoading[0] !== 'error') {
+ this.flgLoading[0] = (undefined !== rtlStore.transactions) ? false : true;
+ }
+ this.logger.info(rtlStore);
+ });
+
+ }
+
+ applyFilter(selFilter: string) {
+ this.listTransactions.filter = selFilter;
+ }
+
+ onTransactionClick(selRow: Transaction, event: any) {
+ const flgExpansionClicked = event.target.className.includes('mat-expansion-panel-header') || event.target.className.includes('mat-expansion-indicator');
+ if (flgExpansionClicked) {
+ return;
+ }
+ const selTransaction = this.listTransactions.data.filter(transaction => {
+ return transaction.tx_hash === selRow.tx_hash;
+ })[0];
+ const reorderedTransactions = JSON.parse(JSON.stringify(selTransaction, [
+ 'dest_addresses', 'time_stamp_str', 'num_confirmations', 'total_fees', 'block_hash', 'block_height', 'tx_hash', 'amount'
+ ] , 2));
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: {
+ type: 'INFO',
+ message: JSON.stringify(reorderedTransactions)
+ }}));
+ }
+
+ loadTransactionsTable(transactions) {
+ this.listTransactions = new MatTableDataSource([...transactions]);
+ this.listTransactions.sort = this.sort;
+ this.listTransactions.data.forEach(transaction => {
+ if (undefined !== transaction.time_stamp_str) {
+ transaction.time_stamp_str = (transaction.time_stamp_str === '') ? '' : formatDate(transaction.time_stamp_str, 'MMM/dd/yy HH:mm:ss', 'en-US');
+ }
+ });
+ this.logger.info(this.listTransactions);
+ }
+
+ resetData() {
+ }
+
+ ngOnDestroy() {
+ this.unsub.forEach(completeSub => {
+ completeSub.next();
+ completeSub.complete();
+ });
+ }
+
+}
diff --git a/src/app/pages/transactions/send-receive/send-receive-trans.component.html b/src/app/pages/transactions/send-receive/send-receive-trans.component.html
new file mode 100644
index 00000000..33d753bf
--- /dev/null
+++ b/src/app/pages/transactions/send-receive/send-receive-trans.component.html
@@ -0,0 +1,163 @@
+
+
Don't be #reckless. #craefulgang #craefulgang #craefulgang.
+
+
+
+
+
+
+ Total Balance
+
+
+
+
+
+
+
+
+ {{blockchainBalance?.btc_total_balance | number}} {{information?.currency_unit}}
+ {{blockchainBalance?.total_balance | number}} {{information?.smaller_currency_unit}}
+
+
+
+
+
+
+
+
+
+
+ Confirmed Balance
+
+
+
+
+
+
+
+
+ {{blockchainBalance?.btc_confirmed_balance | number}} {{information?.currency_unit}}
+ {{blockchainBalance?.confirmed_balance | number}} {{information?.smaller_currency_unit}}
+
+
+
+
+
+
+
+
+
+
+ Unconfirmed Balance
+
+
+
+
+
+
+
+
+ {{blockchainBalance?.btc_unconfirmed_balance | number}} {{information?.currency_unit}}
+ {{blockchainBalance?.unconfirmed_balance | number}} {{information?.smaller_currency_unit}}
+
+
+
+
+
+
+
+
+
+
+ Receive Funds
+
+
+
+
+
+
+
+
+ {{addressType.addressTp}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Send Funds
+
+
+
+
+
+
+
+
+
+ {{transType.name}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Sats
diff --git a/src/app/pages/transactions/send-receive/send-receive-trans.component.scss b/src/app/pages/transactions/send-receive/send-receive-trans.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/pages/transactions/send-receive/send-receive-trans.component.ts b/src/app/pages/transactions/send-receive/send-receive-trans.component.ts
new file mode 100644
index 00000000..d24db3ff
--- /dev/null
+++ b/src/app/pages/transactions/send-receive/send-receive-trans.component.ts
@@ -0,0 +1,175 @@
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { Subject } from 'rxjs';
+import { takeUntil, take } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+
+import { Settings } from '../../../shared/models/RTLconfig';
+import { GetInfo, Balance, ChannelsTransaction, AddressType } from '../../../shared/models/lndModels';
+import { Authentication } from '../../../shared/models/RTLconfig';
+import { LoggerService } from '../../../shared/services/logger.service';
+
+import { RTLEffects } from '../../../shared/store/rtl.effects';
+import * as RTLActions from '../../../shared/store/rtl.actions';
+import * as fromRTLReducer from '../../../shared/store/rtl.reducers';
+
+@Component({
+ selector: 'rtl-send-receive-trans',
+ templateUrl: './send-receive-trans.component.html',
+ styleUrls: ['./send-receive-trans.component.scss']
+})
+export class SendReceiveTransComponent implements OnInit, OnDestroy {
+ public settings: Settings;
+ public addressTypes = [];
+ public flgLoadingWallet: Boolean | 'error' = true;
+ public selectedAddress: AddressType = {};
+ public blockchainBalance: Balance = {};
+ public information: GetInfo = {};
+ public authSettings: Authentication = {};
+ public newAddress = '';
+ public transaction: ChannelsTransaction = {};
+ public transTypes = [{id: '1', name: 'Target Confirmation Blocks'}, {id: '2', name: 'Fee'}];
+ public selTransType = '1';
+ public flgCustomAmount = '1';
+ private unsub: Array> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
+
+ constructor(private logger: LoggerService, private store: Store, private rtlEffects: RTLEffects) {}
+
+ ngOnInit() {
+ this.store.select('rtlRoot')
+ .pipe(takeUntil(this.unsub[0]))
+ .subscribe((rtlStore: fromRTLReducer.State) => {
+ rtlStore.effectErrors.forEach(effectsErr => {
+ if (effectsErr.action === 'FetchBalance/blockchain') {
+ this.flgLoadingWallet = 'error';
+ }
+ });
+ this.settings = rtlStore.settings;
+ this.information = rtlStore.information;
+ this.addressTypes = rtlStore.addressTypes;
+ this.authSettings = rtlStore.authSettings;
+
+ this.blockchainBalance = rtlStore.blockchainBalance;
+ if (undefined === this.blockchainBalance.total_balance) {
+ this.blockchainBalance.total_balance = '0';
+ }
+ if (undefined === this.blockchainBalance.confirmed_balance) {
+ this.blockchainBalance.confirmed_balance = '0';
+ }
+ if (undefined === this.blockchainBalance.unconfirmed_balance) {
+ this.blockchainBalance.unconfirmed_balance = '0';
+ }
+ if (this.flgLoadingWallet !== 'error') {
+ this.flgLoadingWallet = false;
+ }
+
+ this.logger.info(rtlStore);
+ });
+
+ }
+
+ onGenerateAddress() {
+ this.store.dispatch(new RTLActions.OpenSpinner('Getting New Address...'));
+ this.store.dispatch(new RTLActions.GetNewAddress(this.selectedAddress));
+ this.rtlEffects.setNewAddress
+ .pipe(takeUntil(this.unsub[1]))
+ .subscribe(newAddress => {
+ this.newAddress = newAddress;
+ });
+ }
+
+ onSendFunds() {
+ const confirmationMsg = {
+ 'BTC Address': this.transaction.address,
+ };
+ if (!+this.flgCustomAmount) {
+ confirmationMsg['Sweep All'] = 'True';
+ this.transaction.sendAll = true;
+ } else {
+ confirmationMsg['Amount (' + this.information.smaller_currency_unit + ')'] = this.transaction.amount;
+ this.transaction.sendAll = false;
+ }
+ if (this.selTransType === '1') {
+ delete this.transaction.fees;
+ confirmationMsg['Target Confirmation Blocks'] = this.transaction.blocks;
+ } else {
+ delete this.transaction.blocks;
+ confirmationMsg['Fee (' + this.information.smaller_currency_unit + '/Byte)'] = this.transaction.fees;
+ }
+ this.store.dispatch(new RTLActions.OpenConfirmation({ width: '70%', data:
+ {type: 'CONFIRM', message: JSON.stringify(confirmationMsg), noBtnText: 'Cancel', yesBtnText: 'Send'}
+ }));
+
+ this.rtlEffects.closeConfirm
+ .pipe(takeUntil(this.unsub[2]))
+ .subscribe(confirmRes => {
+ if (confirmRes) {
+ if (this.transaction.sendAll && !+this.authSettings.rtlSSO) {
+ this.store.dispatch(new RTLActions.OpenConfirmation({ width: '70%', data:
+ {type: 'CONFIRM', titleMessage: 'Enter Login Password', noBtnText: 'Cancel', yesBtnText: 'Authorize', flgShowInput: true, getInputs: [
+ {placeholder: 'Enter Login Password', inputType: 'password', inputValue: ''}
+ ]}
+ }));
+ this.rtlEffects.closeConfirm
+ .pipe(takeUntil(this.unsub[3]))
+ .subscribe(pwdConfirmRes => {
+ if (pwdConfirmRes) {
+ const pwd = pwdConfirmRes[0].inputValue;
+ this.store.dispatch(new RTLActions.IsAuthorized(window.btoa(pwd)));
+ this.rtlEffects.isAuthorizedRes
+ .pipe(take(1))
+ .subscribe(authRes => {
+ if (authRes !== 'ERROR') {
+ this.dispatchToSendFunds();
+ }
+ });
+ }
+ });
+ } else {
+ this.dispatchToSendFunds();
+ }
+ }
+ });
+ }
+
+ dispatchToSendFunds() {
+ this.store.dispatch(new RTLActions.OpenSpinner('Sending Funds...'));
+ this.store.dispatch(new RTLActions.SetChannelTransaction(this.transaction));
+ this.transaction = {address: '', amount: 0, blocks: 0, fees: 0};
+ }
+
+ get invalidValues(): boolean {
+ return (undefined === this.transaction.address || this.transaction.address === '')
+ || (+this.flgCustomAmount && (undefined === this.transaction.amount || this.transaction.amount <= 0))
+ || (this.selTransType === '1' && (undefined === this.transaction.blocks || this.transaction.blocks <= 0))
+ || (this.selTransType === '2' && (undefined === this.transaction.fees || this.transaction.fees <= 0));
+ }
+
+ onCustomClicked() {
+ this.flgCustomAmount = '1';
+ }
+
+ onOptionChange(event) {
+ if (!+this.flgCustomAmount) {
+ delete this.transaction.amount;
+ }
+ }
+
+ resetData() {
+ this.transaction.address = '';
+ this.transaction.amount = 0;
+ this.transaction.blocks = 0;
+ this.transaction.fees = 0;
+ }
+
+ resetReceiveData() {
+ this.selectedAddress = {};
+ this.newAddress = '';
+ }
+
+ ngOnDestroy() {
+ this.unsub.forEach(completeSub => {
+ completeSub.next();
+ completeSub.complete();
+ });
+ }
+}
diff --git a/src/app/pages/unlock-lnd/unlock-lnd.component.html b/src/app/pages/unlock-lnd/unlock-lnd.component.html
new file mode 100644
index 00000000..e91c9aa0
--- /dev/null
+++ b/src/app/pages/unlock-lnd/unlock-lnd.component.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+ Unlock LND Wallet
+
+
+
+
+
+
+
+
diff --git a/src/app/pages/unlock-lnd/unlock-lnd.component.scss b/src/app/pages/unlock-lnd/unlock-lnd.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/pages/unlock-lnd/unlock-lnd.component.ts b/src/app/pages/unlock-lnd/unlock-lnd.component.ts
new file mode 100644
index 00000000..62536af4
--- /dev/null
+++ b/src/app/pages/unlock-lnd/unlock-lnd.component.ts
@@ -0,0 +1,41 @@
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { Router } from '@angular/router';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+
+import { LoggerService } from '../../shared/services/logger.service';
+
+import * as RTLActions from '../../shared/store/rtl.actions';
+import * as fromRTLReducer from '../../shared/store/rtl.reducers';
+
+@Component({
+ selector: 'rtl-unlock-lnd',
+ templateUrl: './unlock-lnd.component.html',
+ styleUrls: ['./unlock-lnd.component.scss']
+})
+export class UnlockLNDComponent implements OnInit, OnDestroy {
+ walletPassword = '';
+ private unsub = new Subject();
+
+ constructor(private store: Store) {}
+
+ ngOnInit() {
+ this.walletPassword = '';
+ }
+
+ onOperateWallet(operation: string) {
+ this.store.dispatch(new RTLActions.OpenSpinner('Unlocking...'));
+ this.store.dispatch(new RTLActions.OperateWallet({operation: operation, pwd: this.walletPassword}));
+ }
+
+ resetData() {
+ this.walletPassword = '';
+ }
+
+ ngOnDestroy() {
+ this.unsub.next();
+ this.unsub.complete();
+ }
+
+}
diff --git a/src/app/shared/animation/row-animation.ts b/src/app/shared/animation/row-animation.ts
new file mode 100644
index 00000000..6dba26f1
--- /dev/null
+++ b/src/app/shared/animation/row-animation.ts
@@ -0,0 +1,10 @@
+import { trigger, state, animate, transition, style } from '@angular/animations';
+
+export const newlyAddedRowAnimation = [
+ trigger('newlyAddedRowAnimation', [
+ state('notAdded, void', style({ transform: 'translateX(0)' })),
+ state('added', style({ transform: 'translateX(1.5)', border: '1px solid' })),
+ transition('added <=> notAdded', animate('2000ms ease-out')),
+ transition('added <=> void', animate('2000ms ease-out'))
+ ])
+];
diff --git a/src/app/shared/components/alert-message/alert-message.component.html b/src/app/shared/components/alert-message/alert-message.component.html
new file mode 100644
index 00000000..57480a6b
--- /dev/null
+++ b/src/app/shared/components/alert-message/alert-message.component.html
@@ -0,0 +1,35 @@
+
+
+
+ {{data.type}}
+ close
+
+
+
+
{{data.titleMessage | titlecase}}
+
0">
+
+
{{obj[0] | titlecase}}
+
:
+
+
+ {{obj[1] | number:'1.0-3'}}
+
+
+ {{obj[1]}}
+
+
file_copyCopied
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/shared/components/alert-message/alert-message.component.scss b/src/app/shared/components/alert-message/alert-message.component.scss
new file mode 100644
index 00000000..b35af4b1
--- /dev/null
+++ b/src/app/shared/components/alert-message/alert-message.component.scss
@@ -0,0 +1,40 @@
+.p-2 {
+ padding: 1rem;
+}
+
+.pb-1 {
+ padding-bottom: 0.3rem;
+}
+
+.pb-2 {
+ padding-bottom: 1rem;
+}
+
+.mb-1 {
+ margin-bottom: 0.5rem;
+}
+
+.wrap-text {
+ -ms-word-break: break-all;
+ word-break: break-all;
+ word-break: break-word;
+ -webkit-hyphens: auto;
+ -moz-hyphens: auto;
+ hyphens: auto;
+}
+
+.mat-icon[type="button"] {
+ cursor: pointer;
+}
+
+.new-line {
+ white-space: pre-wrap;
+}
+
+.title-message {
+ font-size: 110%;
+}
+
+.mt-minus-40px {
+ margin-top:-40px;
+}
diff --git a/src/app/shared/components/alert-message/alert-message.component.ts b/src/app/shared/components/alert-message/alert-message.component.ts
new file mode 100644
index 00000000..7217cd03
--- /dev/null
+++ b/src/app/shared/components/alert-message/alert-message.component.ts
@@ -0,0 +1,106 @@
+import { Component, OnInit, Inject } from '@angular/core';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
+
+import { LoggerService } from '../../../shared/services/logger.service';
+import { AlertData } from '../../../shared/models/alertData';
+
+@Component({
+ selector: 'rtl-alert-message',
+ templateUrl: './alert-message.component.html',
+ styleUrls: ['./alert-message.component.scss']
+})
+export class AlertMessageComponent implements OnInit {
+ public msgTypeBackground = 'bg-primary p-1';
+ public msgTypeForeground = 'primary';
+ public messageObj = [];
+ public flgCopied = false;
+
+ constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: AlertData, private logger: LoggerService) { }
+
+ ngOnInit() {
+ this.setStyleOnAlertType();
+ this.convertJSONData();
+ }
+
+ setStyleOnAlertType() {
+ // INFO/WARN/ERROR/SUCCESS/CONFIRM
+ if (this.data.type === 'WARN') {
+ this.msgTypeBackground = 'bg-accent p-1';
+ this.msgTypeForeground = 'accent';
+ }
+ if (this.data.type === 'ERROR') {
+ this.msgTypeBackground = 'bg-warn p-1';
+ this.msgTypeForeground = 'warn';
+ if (undefined === this.data.message && undefined === this.data.titleMessage && this.messageObj.length <= 0 ) {
+ this.data.titleMessage = 'Please Check Server Connection';
+ }
+ }
+ }
+
+ convertJSONData() {
+ this.data.message = (undefined === this.data.message) ? '' : this.data.message.replace(/{/g, '').replace(/"/g, '').replace(/}/g, '').replace(/\n/g, '');
+ // Start: For Payment Path
+ const arrayStartIdx = this.data.message.search('\\[');
+ const arrayEndIdx = this.data.message.search('\\]');
+ if (arrayStartIdx > -1 && arrayEndIdx > -1) {
+ this.data.message = this.data.message.substring(0, arrayStartIdx).concat(
+ this.data.message.substring(arrayStartIdx + 1, arrayEndIdx).replace(/,/g, '\n'),
+ this.data.message.substring(arrayEndIdx + 1));
+ }
+ // End: For Payment Path
+ this.messageObj = (this.data.message === '') ? [] : this.data.message.split(',');
+ this.messageObj.forEach((obj, idx) => {
+ this.messageObj[idx] = obj.split(':');
+ this.messageObj[idx][0] = this.messageObj[idx][0].replace('_str', '');
+ this.messageObj[idx][0] = this.messageObj[idx][0].replace(/_/g, ' '); // To replace Backend Data's '_'
+ // Start: To Merge Time Value Again with ':', example Payment Creation Time
+ if (this.messageObj[idx].length > 2) {
+ this.messageObj[idx].forEach((dataValue, j) => {
+ if (j === 0 || j === 1) {
+ return;
+ } else {
+ this.messageObj[idx][1] = this.messageObj[idx][1] + ':' + this.messageObj[idx][j];
+ }
+ });
+ }
+ // End: To Merge Time Value Again with ':', example Payment Creation Time
+ });
+ }
+
+ showCopyOption(key): boolean {
+ let flgFoundKey = false;
+ const showCopyOnKeys = ['payment request'];
+ showCopyOnKeys.filter((arrKey) => {
+ if (arrKey === key) {
+ flgFoundKey = true;
+ return true;
+ }
+ });
+ return flgFoundKey;
+ }
+
+ copiedText(payload) {
+ this.flgCopied = true;
+ setTimeout(() => {this.flgCopied = false; }, 5000);
+ this.logger.info('Copied Text: ' + payload);
+ }
+
+ isNumber(value, key): boolean {
+ let flgFoundKey = false;
+ const notNumberKeys = ['chan id', 'creation date', 'chan id out', 'chan id in'];
+ notNumberKeys.filter((arrKey) => {
+ if (arrKey === key) {
+ flgFoundKey = true;
+ }
+ });
+ if (!flgFoundKey) {
+ return new RegExp(/^[0-9]+$/).test(value);
+ } else {
+ return false;
+ }
+ }
+
+ onClose() {
+ this.dialogRef.close(false);
+ }
+}
diff --git a/src/app/shared/components/confirmation-message/confirmation-message.component.html b/src/app/shared/components/confirmation-message/confirmation-message.component.html
new file mode 100644
index 00000000..f1a1b94b
--- /dev/null
+++ b/src/app/shared/components/confirmation-message/confirmation-message.component.html
@@ -0,0 +1,39 @@
+
+
+
+ {{data.type}}
+ close
+
+
+
+
{{data.titleMessage | titlecase}}
+
+
+
+
+
+
0">
+
+
{{obj[0] | titlecase}}
+
:
+
+
+ {{obj[1] | number:'1.0-3'}}
+
+
+ {{obj[1]}}
+
+
file_copyCopied
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/shared/components/confirmation-message/confirmation-message.component.scss b/src/app/shared/components/confirmation-message/confirmation-message.component.scss
new file mode 100644
index 00000000..79c78813
--- /dev/null
+++ b/src/app/shared/components/confirmation-message/confirmation-message.component.scss
@@ -0,0 +1,16 @@
+.new-line {
+ white-space: pre-wrap;
+}
+
+.wrap-text {
+ -ms-word-break: break-all;
+ word-break: break-all;
+ word-break: break-word;
+ -webkit-hyphens: auto;
+ -moz-hyphens: auto;
+ hyphens: auto;
+}
+
+.title-message {
+ font-size: 110%;
+}
diff --git a/src/app/shared/components/confirmation-message/confirmation-message.component.ts b/src/app/shared/components/confirmation-message/confirmation-message.component.ts
new file mode 100644
index 00000000..f6a3e5f1
--- /dev/null
+++ b/src/app/shared/components/confirmation-message/confirmation-message.component.ts
@@ -0,0 +1,107 @@
+import { Component, OnInit, Inject } from '@angular/core';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
+import { Store } from '@ngrx/store';
+
+import { LoggerService } from '../../../shared/services/logger.service';
+import { AlertData, InputData } from '../../../shared/models/alertData';
+
+import * as RTLActions from '../../../shared/store/rtl.actions';
+import * as fromRTLReducer from '../../../shared/store/rtl.reducers';
+
+@Component({
+ selector: 'rtl-confirmation-message',
+ templateUrl: './confirmation-message.component.html',
+ styleUrls: ['./confirmation-message.component.scss']
+})
+export class ConfirmationMessageComponent implements OnInit {
+ public msgTypeBackground = 'bg-primary p-1';
+ public msgTypeForeground = 'primary';
+ public noBtnText = 'No';
+ public yesBtnText = 'Yes';
+ public messageObj = [];
+ public flgCopied = false;
+ public flgShowInput = false;
+ public getInputs: Array = [{placeholder: '', inputType: 'text', inputValue: ''}];
+
+ constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: AlertData, private logger: LoggerService,
+ private store: Store) { }
+
+ ngOnInit() {
+ this.flgShowInput = this.data.flgShowInput;
+ this.getInputs = this.data.getInputs;
+ this.noBtnText = (undefined !== this.data.noBtnText) ? this.data.noBtnText : 'No';
+ this.yesBtnText = (undefined !== this.data.yesBtnText) ? this.data.yesBtnText : 'Yes';
+
+ // INFO/WARN/ERROR/SUCCESS/CONFIRM
+ if (this.data.type === 'WARN') {
+ this.msgTypeBackground = 'bg-accent p-1';
+ }
+ if (this.data.type === 'ERROR') {
+ this.msgTypeBackground = 'bg-warn p-1';
+ this.msgTypeForeground = 'warn';
+ }
+ this.data.message = (undefined === this.data.message) ? '' : this.data.message.replace(/{/g, '').replace(/"/g, '').replace(/}/g, '').replace(/\n/g, '');
+ // Start: For Payment Path
+ const arrayStartIdx = this.data.message.search('\\[');
+ const arrayEndIdx = this.data.message.search('\\]');
+ if (arrayStartIdx > -1 && arrayEndIdx > -1) {
+ this.data.message = this.data.message.substring(0, arrayStartIdx).concat(
+ this.data.message.substring(arrayStartIdx + 1, arrayEndIdx).replace(/,/g, '\n'),
+ this.data.message.substring(arrayEndIdx + 1));
+ }
+ // End: For Payment Path
+ this.messageObj = (this.data.message === '') ? [] : this.data.message.split(',');
+ this.messageObj.forEach((obj, idx) => {
+ this.messageObj[idx] = obj.split(':');
+ this.messageObj[idx][0] = this.messageObj[idx][0].replace(/_/g, ' '); // To replace Backend Data's '_'
+ // Start: To Merge Time Value Again with ':', example Payment Creation Time
+ if (this.messageObj[idx].length > 2) {
+ this.messageObj[idx].forEach((dataValue, j) => {
+ if (j === 0 || j === 1) {
+ return;
+ } else {
+ this.messageObj[idx][1] = this.messageObj[idx][1] + ':' + this.messageObj[idx][j];
+ }
+ });
+ }
+ // End: To Merge Time Value Again with ':', example Payment Creation Time
+ });
+ }
+
+ showCopyOption(key): boolean {
+ let flgFoundKey = false;
+ const showCopyOnKeys = ['payment request'];
+ showCopyOnKeys.filter((arrKey) => {
+ if (arrKey === key) {
+ flgFoundKey = true;
+ return true;
+ }
+ });
+ return flgFoundKey;
+ }
+
+ copiedText(payload) {
+ this.flgCopied = true;
+ setTimeout(() => {this.flgCopied = false; }, 5000);
+ this.logger.info('Copied Text: ' + payload);
+ }
+
+ isNumber(value, key): boolean {
+ let flgFoundKey = false;
+ const notNumberKeys = ['chan id', 'creation date'];
+ notNumberKeys.filter((arrKey) => {
+ if (arrKey === key) {
+ flgFoundKey = true;
+ }
+ });
+ if (!flgFoundKey) {
+ return new RegExp(/^[0-9]+$/).test(value);
+ } else {
+ return false;
+ }
+ }
+
+ onClose(dialogRes: any) {
+ this.store.dispatch(new RTLActions.CloseConfirmation(dialogRes));
+ }
+}
diff --git a/src/app/shared/components/not-found/not-found.component.html b/src/app/shared/components/not-found/not-found.component.html
new file mode 100644
index 00000000..c12b788e
--- /dev/null
+++ b/src/app/shared/components/not-found/not-found.component.html
@@ -0,0 +1,16 @@
+
+
+
+
+
404
+
+
+
+ This page does not exist!
+
+
+
+
+
diff --git a/src/app/shared/components/not-found/not-found.component.ts b/src/app/shared/components/not-found/not-found.component.ts
new file mode 100644
index 00000000..834aa818
--- /dev/null
+++ b/src/app/shared/components/not-found/not-found.component.ts
@@ -0,0 +1,16 @@
+import { Component } from '@angular/core';
+import { Router } from '@angular/router';
+
+@Component({
+ selector: 'rtl-not-found',
+ templateUrl: './not-found.component.html'
+})
+export class NotFoundComponent {
+
+ constructor(public router: Router) {}
+
+ goHome(): void {
+ this.router.navigate(['/']);
+ }
+
+}
diff --git a/src/app/shared/components/settings-nav/settings-nav.component.html b/src/app/shared/components/settings-nav/settings-nav.component.html
new file mode 100644
index 00000000..19278ec9
--- /dev/null
+++ b/src/app/shared/components/settings-nav/settings-nav.component.html
@@ -0,0 +1,74 @@
+
+ Settings
+
+
+
+
+
Currency Unit
+
+ {{currencyUnit}}
+
+
+
+
+
Menu
+
+ {{menu}}
+
+
+
+
Menu Type
+
+ {{menuType}}
+
+
+
+
+
Sidenav Options
+
+ Opened
+
+
+
+ Pinned
+
+
+
+
+
+
Skins
+
+
+
+
+
+
diff --git a/src/app/shared/components/settings-nav/settings-nav.component.scss b/src/app/shared/components/settings-nav/settings-nav.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/shared/components/settings-nav/settings-nav.component.spec.ts b/src/app/shared/components/settings-nav/settings-nav.component.spec.ts
new file mode 100644
index 00000000..05f43ea7
--- /dev/null
+++ b/src/app/shared/components/settings-nav/settings-nav.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SettingsNavComponent } from './settings-nav.component';
+
+describe('SettingsNavComponent', () => {
+ let component: SettingsNavComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ SettingsNavComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SettingsNavComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/shared/components/settings-nav/settings-nav.component.ts b/src/app/shared/components/settings-nav/settings-nav.component.ts
new file mode 100644
index 00000000..97f9a4c0
--- /dev/null
+++ b/src/app/shared/components/settings-nav/settings-nav.component.ts
@@ -0,0 +1,80 @@
+import { Component, OnInit, OnDestroy, Output, EventEmitter } from '@angular/core';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+
+import { Settings } from '../../models/RTLconfig';
+import { GetInfo } from '../../models/lndModels';
+import { LoggerService } from '../../services/logger.service';
+
+import * as RTLActions from '../../store/rtl.actions';
+import * as fromRTLReducer from '../../store/rtl.reducers';
+
+@Component({
+ selector: 'rtl-settings-nav',
+ templateUrl: './settings-nav.component.html',
+ styleUrls: ['./settings-nav.component.scss']
+})
+export class SettingsNavComponent implements OnInit, OnDestroy {
+ public information: GetInfo = {};
+ public settings: Settings;
+ public menus = ['Vertical', 'Horizontal'];
+ public menuTypes = ['Regular', 'Compact', 'Mini'];
+ public selectedMenu: string;
+ public selectedMenuType: string;
+ public currencyUnit = 'BTC';
+ public showSettingOption = true;
+ unsubs: Array> = [new Subject(), new Subject()];
+ @Output('done') done: EventEmitter = new EventEmitter();
+
+ constructor(private logger: LoggerService, private store: Store) {}
+
+ ngOnInit() {
+ this.store.select('rtlRoot')
+ .pipe(takeUntil(this.unsubs[0]))
+ .subscribe((rtlStore: fromRTLReducer.State) => {
+ this.settings = rtlStore.settings;
+ this.selectedMenu = this.settings.menu;
+ this.selectedMenuType = this.settings.menuType;
+ if (window.innerWidth <= 768) {
+ this.settings.menu = 'Vertical';
+ this.settings.flgSidenavOpened = false;
+ this.settings.flgSidenavPinned = false;
+ this.showSettingOption = false;
+ }
+ this.information = rtlStore.information;
+ this.currencyUnit = (undefined !== this.information && undefined !== this.information.currency_unit) ? this.information.currency_unit : 'BTC';
+ this.logger.info(rtlStore);
+ });
+ }
+
+ public chooseMenu() {
+ this.settings.menu = this.selectedMenu;
+ }
+
+ public chooseMenuType() {
+ this.settings.menuType = this.selectedMenuType;
+ }
+
+ toggleSettings(toggleField: string) {
+ this.settings[toggleField] = !this.settings[toggleField];
+ }
+
+ changeTheme(newTheme: string) {
+ this.settings.theme = newTheme;
+ }
+
+ onClose() {
+ this.logger.info(this.settings);
+ this.store.dispatch(new RTLActions.SaveSettings(this.settings));
+ this.done.emit();
+ }
+
+ ngOnDestroy() {
+ this.unsubs.forEach(unsub => {
+ unsub.next();
+ unsub.complete();
+ });
+ }
+
+}
diff --git a/src/app/shared/components/spinner-dialog/spinner-dialog.component.html b/src/app/shared/components/spinner-dialog/spinner-dialog.component.html
new file mode 100644
index 00000000..3d4e3539
--- /dev/null
+++ b/src/app/shared/components/spinner-dialog/spinner-dialog.component.html
@@ -0,0 +1,6 @@
+
+
+
+
{{data.titleMessage}}
+
+
diff --git a/src/app/shared/components/spinner-dialog/spinner-dialog.component.scss b/src/app/shared/components/spinner-dialog/spinner-dialog.component.scss
new file mode 100644
index 00000000..8d712cca
--- /dev/null
+++ b/src/app/shared/components/spinner-dialog/spinner-dialog.component.scss
@@ -0,0 +1,4 @@
+.spinner-container {
+ position: absolute;
+ left: 50%;
+}
diff --git a/src/app/shared/components/spinner-dialog/spinner-dialog.component.ts b/src/app/shared/components/spinner-dialog/spinner-dialog.component.ts
new file mode 100644
index 00000000..162cc3dd
--- /dev/null
+++ b/src/app/shared/components/spinner-dialog/spinner-dialog.component.ts
@@ -0,0 +1,16 @@
+import { Component, OnInit, Inject } from '@angular/core';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
+
+@Component({
+ selector: 'rtl-spinner-dialog',
+ templateUrl: './spinner-dialog.component.html',
+ styleUrls: ['./spinner-dialog.component.scss']
+})
+export class SpinnerDialogComponent implements OnInit {
+
+ constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any) { }
+
+ ngOnInit() {
+ }
+
+}
diff --git a/src/app/shared/components/sso-failed/sso-failed.component.html b/src/app/shared/components/sso-failed/sso-failed.component.html
new file mode 100644
index 00000000..7318507c
--- /dev/null
+++ b/src/app/shared/components/sso-failed/sso-failed.component.html
@@ -0,0 +1,15 @@
+
+
+
+
+
401
+
+
+
+ Single Sign On Failed!
+
+
+
+
diff --git a/src/app/shared/components/sso-failed/sso-failed.component.ts b/src/app/shared/components/sso-failed/sso-failed.component.ts
new file mode 100644
index 00000000..c0653b4d
--- /dev/null
+++ b/src/app/shared/components/sso-failed/sso-failed.component.ts
@@ -0,0 +1,14 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'rtl-sso-failed',
+ templateUrl: './sso-failed.component.html'
+})
+export class SsoFailedComponent implements OnInit {
+
+ constructor() { }
+
+ ngOnInit() {
+ }
+
+}
diff --git a/src/app/shared/directive/clipboard.directive.spec.ts b/src/app/shared/directive/clipboard.directive.spec.ts
new file mode 100644
index 00000000..79021ddf
--- /dev/null
+++ b/src/app/shared/directive/clipboard.directive.spec.ts
@@ -0,0 +1,8 @@
+import { ClipboardDirective } from './clipboard.directive';
+
+describe('ClipboardDirective', () => {
+ it('should create an instance', () => {
+ const directive = new ClipboardDirective();
+ expect(directive).toBeTruthy();
+ });
+});
diff --git a/src/app/shared/directive/clipboard.directive.ts b/src/app/shared/directive/clipboard.directive.ts
new file mode 100644
index 00000000..9a49de3e
--- /dev/null
+++ b/src/app/shared/directive/clipboard.directive.ts
@@ -0,0 +1,28 @@
+import { Directive, Input, Output, EventEmitter, HostListener } from '@angular/core';
+
+@Directive({
+ selector: '[rtlClipboard]'
+})
+export class ClipboardDirective {
+ @Input() payload: string;
+
+ @Output('copied')
+ public copied: EventEmitter = new EventEmitter();
+
+ @HostListener('click', ['$event'])
+ public onClick(event: MouseEvent): void {
+ event.preventDefault();
+ if (!this.payload) {
+ return;
+ }
+ const listener = (e: ClipboardEvent) => {
+ const clipboard = e.clipboardData || window['clipboardData'];
+ clipboard.setData('text', this.payload.toString());
+ e.preventDefault();
+ this.copied.emit(this.payload);
+ };
+ document.addEventListener('copy', listener, false);
+ document.execCommand('copy');
+ document.removeEventListener('copy', listener, false);
+ }
+}
diff --git a/src/app/shared/models/RTLconfig.ts b/src/app/shared/models/RTLconfig.ts
new file mode 100644
index 00000000..663c0f17
--- /dev/null
+++ b/src/app/shared/models/RTLconfig.ts
@@ -0,0 +1,31 @@
+export class Settings {
+ constructor(
+ public flgSidenavOpened: boolean,
+ public flgSidenavPinned: boolean,
+ public menu: string,
+ public menuType: string,
+ public theme: string,
+ public satsToBTC: boolean
+ ) { }
+}
+
+export class Authentication {
+ constructor(
+ public lndServerUrl?: string,
+ public macaroonPath?: string,
+ public nodeAuthType?: string,
+ public lndConfigPath?: string,
+ public bitcoindConfigPath?: string,
+ public rtlPass?: string,
+ public enableLogging?: string,
+ public rtlSSO?: number,
+ public logoutRedirectLink?: string
+ ) { }
+}
+
+export class RTLConfiguration {
+ constructor(
+ public settings: Settings,
+ public authentication: Authentication
+ ) { }
+}
diff --git a/src/app/shared/models/alertData.ts b/src/app/shared/models/alertData.ts
new file mode 100644
index 00000000..d37427c7
--- /dev/null
+++ b/src/app/shared/models/alertData.ts
@@ -0,0 +1,15 @@
+export interface InputData {
+ placeholder: string;
+ inputValue?: string;
+ inputType?: string;
+}
+
+export interface AlertData {
+ type: string; // INFO/WARN/ERROR/SUCCESS/CONFIRM
+ titleMessage?: string;
+ message?: string;
+ noBtnText?: string;
+ yesBtnText?: string;
+ flgShowInput?: boolean;
+ getInputs?: Array;
+}
diff --git a/src/app/shared/models/errorPayload.ts b/src/app/shared/models/errorPayload.ts
new file mode 100644
index 00000000..9abcb4b0
--- /dev/null
+++ b/src/app/shared/models/errorPayload.ts
@@ -0,0 +1,7 @@
+export interface ErrorPayload {
+ action?: string;
+ code?: string;
+ message?: string;
+ URL?: string;
+ filePath?: string;
+}
diff --git a/src/app/shared/models/lndModels.ts b/src/app/shared/models/lndModels.ts
new file mode 100644
index 00000000..a1e48064
--- /dev/null
+++ b/src/app/shared/models/lndModels.ts
@@ -0,0 +1,340 @@
+export interface AddressType {
+ addressId?: string;
+ addressTp?: string;
+ addressDetails?: string;
+}
+
+export interface Balance {
+ btc_balance?: string; // For Channels Balance
+ balance?: string; // For Channels Balance
+ btc_pending_open_balance?: string; // For Channels Balance
+ pending_open_balance?: string; // For Channels Balance
+ btc_total_balance?: string; // For Blockchain Balance
+ total_balance?: string; // For Blockchain Balance
+ btc_confirmed_balance?: string; // For Blockchain Balance
+ confirmed_balance?: string; // For Blockchain Balance
+ btc_unconfirmed_balance?: string; // For Blockchain Balance
+ unconfirmed_balance?: string; // For Blockchain Balance
+}
+
+export interface ChannelFeeReport {
+ chan_point?: string;
+ base_fee_msat?: number;
+ fee_per_mil?: number;
+ fee_rate?: number;
+}
+
+export interface Channel {
+ active?: boolean;
+ remote_pubkey?: string;
+ remote_alias?: string;
+ channel_point?: string;
+ chan_id?: number;
+ capacity?: number;
+ local_balance?: number;
+ remote_balance?: number;
+ commit_fee?: number;
+ commit_weight?: number;
+ fee_per_kw?: number;
+ unsettled_balance?: number;
+ total_satoshis_sent?: number;
+ total_satoshis_received?: number;
+ num_updates?: number;
+ pending_htlcs?: HTLC[];
+ csv_delay?: number;
+ private?: boolean;
+}
+
+export interface PendingChannels {
+ total_limbo_balance?: number;
+ btc_total_limbo_balance?: number;
+ pending_closing_channels?: Array;
+ pending_force_closing_channels?: Array;
+ pending_open_channels?: Array;
+ waiting_close_channels?: Array;
+}
+
+export interface ClosedChannel {
+ time_locked_balance?: string;
+ closing_tx_hash?: string;
+ close_type?: any;
+ close_height?: number;
+ chain_hash?: string;
+ channel_point?: string;
+ chan_id?: string;
+ remote_pubkey?: string;
+ capacity?: string;
+ settled_balance?: string;
+}
+
+export interface NetworkGraph {
+ nodes: LightningNode[];
+ edges: ChannelEdge[];
+}
+
+export interface LightningNode {
+ last_update?: number;
+ pub_key?: string;
+ alias?: string;
+ addresses?: NodeAddress[];
+ color?: string;
+}
+
+export interface NodeAddress {
+ network?: string;
+ addr?: string;
+}
+
+export interface ChannelEdge {
+ channel_id?: string;
+ chan_point?: string;
+ last_update?: number;
+ last_update_str?: string;
+ node1_pub?: string;
+ node2_pub?: string;
+ capacity?: string;
+ node1_policy?: RoutingPolicy;
+ node2_policy?: RoutingPolicy;
+}
+
+export interface RoutingPolicy {
+ time_lock_delta?: number;
+ min_htlc?: string;
+ fee_base_msat?: string;
+ fee_rate_milli_msat?: string;
+ disabled?: boolean;
+}
+
+export interface SigmaNode {
+ id: string;
+ label: string;
+ x: number;
+ y: number;
+ size: number;
+}
+
+export interface SigmaEdge {
+ id: string;
+ source: string;
+ target: string;
+}
+
+export interface FeeLimit {
+ percent?: number;
+ fixed?: number;
+}
+
+export interface Fees {
+ channel_fees?: ChannelFeeReport[];
+ day_fee_sum?: number;
+ week_fee_sum?: number;
+ month_fee_sum?: number;
+ btc_day_fee_sum?: number;
+ btc_week_fee_sum?: number;
+ btc_month_fee_sum?: number;
+}
+
+export interface GetInfoChain {
+ chain?: string;
+ network?: string;
+}
+
+export interface GetInfo {
+ identity_pubkey?: string;
+ alias?: string;
+ num_pending_channels?: number;
+ num_active_channels?: number;
+ num_peers?: number;
+ block_height?: number;
+ block_hash?: string;
+ synced_to_chain?: boolean;
+ testnet?: boolean;
+ chains?: GetInfoChain[] | string[];
+ uris?: string[];
+ best_header_timestamp?: number;
+ version?: string;
+ currency_unit?: string;
+ smaller_currency_unit?: string;
+}
+
+export interface GraphNode {
+ node?: LightningNode;
+ num_channels?: number;
+ total_capacity?: string;
+}
+
+export interface HopHint {
+ cltv_expiry_delta?: number;
+ node_id?: string;
+ chan_id?: number;
+ fee_proportional_millionths?: number;
+ fee_base_msat?: number;
+}
+
+export interface HTLC {
+ incoming?: boolean;
+ amount?: number;
+ hash_lock?: string;
+ expiration_height?: number;
+}
+
+export interface Invoice {
+ memo?: string;
+ receipt?: string;
+ r_preimage?: string;
+ r_hash?: string;
+ value?: string;
+ btc_value?: string;
+ settled?: boolean;
+ creation_date?: string;
+ creation_date_str?: string;
+ settle_date?: string;
+ settle_date_str?: string;
+ payment_request?: string;
+ description_hash?: string;
+ expiry?: string;
+ fallback_addr?: string;
+ cltv_expiry?: string;
+ route_hints?: RouteHint[];
+ private?: boolean;
+ add_index?: string;
+ settle_index?: string;
+ amt_paid?: string;
+ amt_paid_sat?: string;
+ btc_amt_paid_sat?: string;
+ amt_paid_msat?: string;
+}
+
+export interface ListInvoices {
+ invoices?: Invoice[];
+ last_index_offset?: string;
+ first_index_offset?: string;
+}
+
+export interface LightningNode {
+ last_update?: number;
+ last_update_str?: string;
+ pub_key?: string;
+ alias?: string;
+ addresses?: NodeAddress[];
+ color?: string;
+}
+
+export interface NetworkInfo {
+ num_nodes?: number;
+ btc_max_channel_size?: string;
+ max_channel_size?: string;
+ btc_avg_channel_size?: string;
+ avg_channel_size?: string;
+ graph_diameter?: number;
+ num_channels?: number;
+ max_out_degree?: number;
+ btc_total_network_capacity?: string;
+ total_network_capacity?: string;
+ avg_out_degree?: number;
+ btc_min_channel_size?: string;
+ min_channel_size?: string;
+}
+
+export interface NodeAddress {
+ network?: string;
+ address?: string;
+}
+
+export interface Payment {
+ creation_date?: number;
+ creation_date_str?: string;
+ payment_hash?: string;
+ path?: string[];
+ fee?: number;
+ value_msat?: number;
+ value_sat?: number;
+ value?: number;
+ payment_preimage?: string;
+}
+
+export interface PayRequest {
+ payment_hash?: string;
+ route_hints?: RouteHint[];
+ timestamp?: number;
+ timestamp_str?: string;
+ fallback_addr?: string;
+ cltv_expiry?: number;
+ description_hash?: string;
+ destination?: string;
+ expiry?: number;
+ description?: string;
+ num_satoshis?: string;
+ btc_num_satoshis?: string;
+}
+
+export interface Peer {
+ pub_key?: string;
+ alias?: string;
+ address?: string; // host
+ bytes_sent?: number;
+ bytes_recv?: number;
+ sat_sent?: string;
+ sat_recv?: string;
+ inbound?: boolean;
+ ping_time?: number;
+}
+
+export interface RouteHint {
+ hop_hints?: HopHint[];
+}
+
+export interface SendPayment {
+ dest_string?: string;
+ dest?: string;
+ payment_hash_string?: string;
+ payment_request?: string;
+ fee_limit?: FeeLimit;
+ amt?: number;
+ payment_hash?: string;
+ final_cltv_delta?: number;
+}
+
+export interface ChannelsTransaction {
+ address?: string;
+ amount?: number;
+ sendAll?: boolean;
+ blocks?: number;
+ fees?: number;
+}
+
+export interface Transaction {
+ dest_addresses?: string[];
+ time_stamp?: string;
+ time_stamp_str?: string;
+ num_confirmations?: number;
+ total_fees?: string;
+ block_hash?: string;
+ block_height?: number;
+ tx_hash?: string;
+ amount?: string;
+}
+
+export interface SwitchReq {
+ num_max_events?: number;
+ index_offset?: number;
+ end_time?: string;
+ start_time?: string;
+}
+
+export interface ForwardingEvent {
+ timestamp?: string;
+ timestamp_str?: string;
+ chan_id_out?: string;
+ alias_out?: string;
+ amt_out?: string;
+ amt_in?: string;
+ chan_id_in?: string;
+ alias_in?: string;
+ fee?: string;
+}
+
+export interface SwitchRes {
+ last_offset_index?: number;
+ forwarding_events?: ForwardingEvent[];
+}
diff --git a/src/app/shared/models/navMenu.ts b/src/app/shared/models/navMenu.ts
new file mode 100644
index 00000000..c0564a35
--- /dev/null
+++ b/src/app/shared/models/navMenu.ts
@@ -0,0 +1,39 @@
+export const MENU_DATA: MenuNode = {
+ id: 0,
+ parentId: 0,
+ name: 'root',
+ icon: 'root',
+ link: 'root',
+ children: [
+ {id: 1, parentId: 0, name: 'Home', icon: 'home', link: '/home'},
+ {id: 2, parentId: 0, name: 'LND Wallet', icon: 'account_balance_wallet', link: '/transsendreceive', children: [
+ {id: 21, parentId: 2, name: 'Send/Receive', icon: 'compare_arrows', link: '/transsendreceive'},
+ {id: 22, parentId: 2, name: 'List Transactions', icon: 'list_alt', link: '/translist'},
+ ]},
+ {id: 3, parentId: 0, name: 'Peers', icon: 'group', link: '/peers'},
+ {id: 4, parentId: 0, name: 'Channels', icon: 'settings_ethernet', link: '/chnlmanage', children: [
+ {id: 41, parentId: 4, name: 'Management', icon: 'subtitles', link: '/chnlmanage'},
+ {id: 42, parentId: 4, name: 'Pending', icon: 'watch', link: '/chnlpending'},
+ {id: 43, parentId: 4, name: 'Closed', icon: 'watch_later', link: '/chnlclosed'}
+ ]},
+ {id: 5, parentId: 0, name: 'Payments', icon: 'payment', link: '/payments'},
+ {id: 6, parentId: 0, name: 'Invoices', icon: 'receipt', link: '/invoices'},
+ {id: 7, parentId: 0, name: 'Forwarding History', icon: 'timeline', link: '/switch'},
+ {id: 8, parentId: 0, name: 'Lookups', icon: 'search', link: '/lookups'},
+ {id: 9, parentId: 0, name: 'Node Config', icon: 'perm_data_setting', link: '/sconfig'},
+ {id: 10, parentId: 0, name: 'Help', icon: 'help', link: '/help'}
+ ]
+};
+
+export class MenuNode {
+ id: number;
+ parentId: number;
+ name?: string;
+ icon?: string;
+ link?: any;
+ children?: MenuNode[];
+}
+
+export class FlatMenuNode {
+ constructor(public expandable: boolean, public level: number, public id: number, public parentId: number, public name: string, public icon: string, public link: string) {}
+}
diff --git a/src/app/shared/services/auth.guard.ts b/src/app/shared/services/auth.guard.ts
new file mode 100644
index 00000000..a3f12af5
--- /dev/null
+++ b/src/app/shared/services/auth.guard.ts
@@ -0,0 +1,29 @@
+import { CanActivate } from '@angular/router';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+
+@Injectable()
+export class AuthGuard implements CanActivate {
+ constructor() {}
+
+ canActivate(): boolean | Observable | Promise {
+ if (!sessionStorage.getItem('token')) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+}
+
+@Injectable()
+export class LNDUnlockedGuard implements CanActivate {
+ constructor() {}
+
+ canActivate(): boolean | Observable | Promise {
+ if (!sessionStorage.getItem('lndUnlocked')) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+}
diff --git a/src/app/shared/services/auth.interceptor.ts b/src/app/shared/services/auth.interceptor.ts
new file mode 100644
index 00000000..ad47aca3
--- /dev/null
+++ b/src/app/shared/services/auth.interceptor.ts
@@ -0,0 +1,21 @@
+import { Injectable } from '@angular/core';
+import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
+import { Observable } from 'rxjs';
+
+@Injectable()
+export class AuthInterceptor implements HttpInterceptor {
+
+ constructor() {}
+
+ intercept(req: HttpRequest, next: HttpHandler): Observable> {
+ if (sessionStorage.getItem('token')) {
+ const cloned = req.clone({
+ headers: req.headers.set('Authorization', 'Bearer ' + sessionStorage.getItem('token'))
+ });
+ return next.handle(cloned);
+ } else {
+ return next.handle(req);
+ }
+ }
+
+}
diff --git a/src/app/shared/services/logger.service.ts b/src/app/shared/services/logger.service.ts
new file mode 100644
index 00000000..22ae50d9
--- /dev/null
+++ b/src/app/shared/services/logger.service.ts
@@ -0,0 +1,50 @@
+import { Injectable } from '@angular/core';
+import { environment } from '../../../environments/environment';
+export let isDebugMode = environment.isDebugMode;
+const noop = (): any => undefined;
+
+export abstract class Logger {
+ info: any;
+ warn: any;
+ error: any;
+}
+
+@Injectable()
+export class LoggerService implements Logger {
+ info: any;
+ warn: any;
+ error: any;
+ invokeConsoleMethod(type: string, args?: any): void {}
+}
+
+@Injectable()
+export class ConsoleLoggerService implements Logger {
+ get info() {
+ if (isDebugMode) {
+ return console.log.bind(console);
+ } else {
+ return noop;
+ }
+ }
+
+ get warn() {
+ if (isDebugMode) {
+ return console.warn.bind(console);
+ } else {
+ return noop;
+ }
+ }
+
+ get error() {
+ if (isDebugMode) {
+ return console.error.bind(console);
+ } else {
+ return noop;
+ }
+ }
+
+ invokeConsoleMethod(type: string, args?: any): void {
+ const logFn: Function = (console)[type] || console.log || noop;
+ logFn.apply(console, [args]);
+ }
+}
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
new file mode 100644
index 00000000..c2cca0b3
--- /dev/null
+++ b/src/app/shared/shared.module.ts
@@ -0,0 +1,105 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { FlexLayoutModule } from '@angular/flex-layout';
+import {
+ MatButtonModule, MatButtonToggleModule, MatCardModule, MatCheckboxModule, MatDialogModule, MatExpansionModule, MatGridListModule, MatDatepickerModule,
+ MatIconModule, MatInputModule, MatListModule, MatMenuModule, MatProgressBarModule, MatProgressSpinnerModule, MatRadioModule, MatTreeModule, MatNativeDateModule,
+ MatSelectModule, MatSidenavModule, MatSlideToggleModule, MatSortModule, MatTableModule, MatToolbarModule, MatTooltipModule, MAT_DIALOG_DEFAULT_OPTIONS, MatBadgeModule
+} from '@angular/material';
+import { QRCodeModule } from 'angularx-qrcode';
+import { AlertMessageComponent } from './components/alert-message/alert-message.component';
+import { ConfirmationMessageComponent } from './components/confirmation-message/confirmation-message.component';
+import { SpinnerDialogComponent } from './components/spinner-dialog/spinner-dialog.component';
+import { NotFoundComponent } from './components/not-found/not-found.component';
+import { SettingsNavComponent } from './components/settings-nav/settings-nav.component';
+import { ClipboardDirective } from './directive/clipboard.directive';
+import { SsoFailedComponent } from './components/sso-failed/sso-failed.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ FlexLayoutModule,
+ MatButtonModule,
+ MatButtonToggleModule,
+ MatCardModule,
+ MatCheckboxModule,
+ MatDialogModule,
+ MatExpansionModule,
+ MatGridListModule,
+ MatDatepickerModule,
+ MatNativeDateModule,
+ MatIconModule,
+ MatInputModule,
+ MatListModule,
+ MatMenuModule,
+ MatProgressBarModule,
+ MatProgressSpinnerModule,
+ MatRadioModule,
+ MatTreeModule,
+ MatSelectModule,
+ MatSidenavModule,
+ MatSlideToggleModule,
+ MatSortModule,
+ MatTableModule,
+ MatToolbarModule,
+ MatTooltipModule,
+ MatBadgeModule,
+ QRCodeModule
+ ],
+ exports: [
+ FlexLayoutModule,
+ MatButtonModule,
+ MatButtonToggleModule,
+ MatCardModule,
+ MatCheckboxModule,
+ MatDialogModule,
+ MatExpansionModule,
+ MatGridListModule,
+ MatDatepickerModule,
+ MatNativeDateModule,
+ MatIconModule,
+ MatInputModule,
+ MatListModule,
+ MatMenuModule,
+ MatProgressBarModule,
+ MatProgressSpinnerModule,
+ MatRadioModule,
+ MatTreeModule,
+ MatSelectModule,
+ MatSidenavModule,
+ MatSlideToggleModule,
+ MatSortModule,
+ MatTableModule,
+ MatToolbarModule,
+ MatTooltipModule,
+ MatBadgeModule,
+ AlertMessageComponent,
+ ConfirmationMessageComponent,
+ SpinnerDialogComponent,
+ NotFoundComponent,
+ SettingsNavComponent,
+ ClipboardDirective,
+ QRCodeModule
+ ],
+ declarations: [
+ AlertMessageComponent,
+ ConfirmationMessageComponent,
+ SpinnerDialogComponent,
+ NotFoundComponent,
+ SettingsNavComponent,
+ ClipboardDirective,
+ SsoFailedComponent
+ ],
+ entryComponents: [
+ AlertMessageComponent,
+ SpinnerDialogComponent,
+ ConfirmationMessageComponent
+ ],
+ providers: [
+ { provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: { hasBackdrop: true, autoFocus: true, disableClose: true, role: 'dialog', width: '700px' } }
+ ]
+})
+export class SharedModule { }
diff --git a/src/app/shared/store/rtl.actions.ts b/src/app/shared/store/rtl.actions.ts
new file mode 100644
index 00000000..129bfffa
--- /dev/null
+++ b/src/app/shared/store/rtl.actions.ts
@@ -0,0 +1,405 @@
+import { Action } from '@ngrx/store';
+import { Settings, Authentication } from '../models/RTLconfig';
+import { ErrorPayload } from '../models/errorPayload';
+import {
+ GetInfo, Peer, Balance, NetworkInfo, Fees, Channel, Invoice, Payment, GraphNode, AddressType,
+ PayRequest, ChannelsTransaction, PendingChannels, ClosedChannel, Transaction, SwitchReq, SwitchRes
+} from '../models/lndModels';
+import { MatDialogConfig } from '@angular/material';
+
+export const CLEAR_EFFECT_ERROR = 'CLEAR_EFFECT_ERROR';
+export const EFFECT_ERROR = 'EFFECT_ERROR';
+export const OPEN_SPINNER = 'OPEN_SPINNER';
+export const CLOSE_SPINNER = 'CLOSE_SPINNER';
+export const OPEN_ALERT = 'OPEN_ALERT';
+export const CLOSE_ALERT = 'CLOSE_ALERT';
+export const OPEN_CONFIRMATION = 'OPEN_CONFIRMATION';
+export const CLOSE_CONFIRMATION = 'CLOSE_CONFIRMATION';
+export const FETCH_STORE = 'FETCH_STORE';
+export const SET_STORE = 'SET_STORE';
+export const FETCH_SETTINGS = 'FETCH_SETTINGS';
+export const SET_SETTINGS = 'SET_SETTINGS';
+export const SET_AUTH_SETTINGS = 'SET_AUTH_SETTINGS';
+export const SAVE_SETTINGS = 'SAVE_SETTINGS';
+export const FETCH_INFO = 'FETCH_INFO';
+export const SET_INFO = 'SET_INFO';
+export const FETCH_PEERS = 'FETCH_PEERS';
+export const SET_PEERS = 'SET_PEERS';
+export const SAVE_NEW_PEER = 'SAVE_NEW_PEER';
+export const ADD_PEER = 'ADD_PEER';
+export const DETACH_PEER = 'DETACH_PEER';
+export const REMOVE_PEER = 'REMOVE_PEER';
+export const SAVE_NEW_INVOICE = 'SAVE_NEW_INVOICE';
+export const ADD_INVOICE = 'ADD_INVOICE';
+export const FETCH_FEES = 'FETCH_FEES';
+export const SET_FEES = 'SET_FEES';
+export const FETCH_BALANCE = 'FETCH_BALANCE';
+export const SET_BALANCE = 'SET_BALANCE';
+export const FETCH_NETWORK = 'FETCH_NETWORK';
+export const SET_NETWORK = 'SET_NETWORK';
+export const FETCH_CHANNELS = 'FETCH_CHANNELS';
+export const SET_CHANNELS = 'SET_CHANNELS';
+export const UPDATE_CHANNELS = 'UPDATE_CHANNELS';
+export const SET_PENDING_CHANNELS = 'SET_PENDING_CHANNELS';
+export const SET_CLOSED_CHANNELS = 'SET_CLOSED_CHANNELS';
+export const SAVE_NEW_CHANNEL = 'SAVE_NEW_CHANNEL';
+export const CLOSE_CHANNEL = 'CLOSE_CHANNEL';
+export const REMOVE_CHANNEL = 'REMOVE_CHANNEL';
+export const FETCH_INVOICES = 'FETCH_INVOICES';
+export const SET_INVOICES = 'SET_INVOICES';
+export const FETCH_TRANSACTIONS = 'FETCH_TRANSACTIONS';
+export const SET_TRANSACTIONS = 'SET_TRANSACTIONS';
+export const FETCH_PAYMENTS = 'FETCH_PAYMENTS';
+export const SET_PAYMENTS = 'SET_PAYMENTS';
+export const DECODE_PAYMENT = 'DECODE_PAYMENT';
+export const SEND_PAYMENT = 'SEND_PAYMENT';
+export const SET_DECODED_PAYMENT = 'SET_DECODED_PAYMENT';
+export const FETCH_GRAPH_NODE = 'FETCH_GRAPH_NODE';
+export const SET_GRAPH_NODE = 'SET_GRAPH_NODE';
+export const GET_NEW_ADDRESS = 'GET_NEW_ADDRESS';
+export const SET_NEW_ADDRESS = 'SET_NEW_ADDRESS';
+export const SET_CHANNEL_TRANSACTION = 'SET_CHANNEL_TRANSACTION';
+export const OPERATE_WALLET = 'OPERATE_WALLET';
+export const FETCH_CONFIG = 'FETCH_CONFIG';
+export const SHOW_CONFIG = 'SHOW_CONFIG';
+export const IS_AUTHORIZED = 'IS_AUTHORIZED';
+export const IS_AUTHORIZED_RES = 'IS_AUTHORIZED_RES';
+export const SIGNIN = 'SIGNIN';
+export const SIGNOUT = 'SIGNOUT';
+export const INIT_APP_DATA = 'INIT_APP_DATA';
+export const PEER_LOOKUP = 'PEER_LOOKUP';
+export const CHANNEL_LOOKUP = 'CHANNEL_LOOKUP';
+export const INVOICE_LOOKUP = 'INVOICE_LOOKUP';
+export const SET_LOOKUP = 'SET_LOOKUP';
+export const GET_FORWARDING_HISTORY = 'GET_FORWARDING_HISTORY';
+export const SET_FORWARDING_HISTORY = 'SET_FORWARDING_HISTORY';
+
+export class ClearEffectError implements Action {
+ readonly type = CLEAR_EFFECT_ERROR;
+ constructor(public payload: string) {} // payload = errorAction
+}
+
+export class EffectError implements Action {
+ readonly type = EFFECT_ERROR;
+ constructor(public payload: ErrorPayload) {}
+}
+
+export class OpenSpinner implements Action {
+ readonly type = OPEN_SPINNER;
+ constructor(public payload: string) {} // payload = titleMessage
+}
+
+export class CloseSpinner implements Action {
+ readonly type = CLOSE_SPINNER;
+}
+
+export class OpenAlert implements Action {
+ readonly type = OPEN_ALERT;
+ constructor(public payload: MatDialogConfig) {}
+}
+
+export class CloseAlert implements Action {
+ readonly type = CLOSE_ALERT;
+}
+
+export class OpenConfirmation implements Action {
+ readonly type = OPEN_CONFIRMATION;
+ constructor(public payload: MatDialogConfig) {}
+}
+
+export class CloseConfirmation implements Action {
+ readonly type = CLOSE_CONFIRMATION;
+ constructor(public payload: boolean) {}
+}
+
+export class FetchSettings implements Action {
+ readonly type = FETCH_SETTINGS;
+}
+
+export class SetSettings implements Action {
+ readonly type = SET_SETTINGS;
+ constructor(public payload: Settings) {}
+}
+
+export class SetAuthSettings implements Action {
+ readonly type = SET_AUTH_SETTINGS;
+ constructor(public payload: Authentication) {}
+}
+
+export class SaveSettings implements Action {
+ readonly type = SAVE_SETTINGS;
+ constructor(public payload: Settings) {}
+}
+
+export class FetchInfo implements Action {
+ readonly type = FETCH_INFO;
+}
+
+export class SetInfo implements Action {
+ readonly type = SET_INFO;
+ constructor(public payload: GetInfo) {}
+}
+
+export class FetchPeers implements Action {
+ readonly type = FETCH_PEERS;
+}
+
+export class SetPeers implements Action {
+ readonly type = SET_PEERS;
+ constructor(public payload: Peer[]) {}
+}
+
+export class SaveNewPeer implements Action {
+ readonly type = SAVE_NEW_PEER;
+ constructor(public payload: {pubkey: string, host: string, perm: boolean}) {}
+}
+
+export class AddPeer implements Action {
+ readonly type = ADD_PEER;
+ constructor(public payload: Peer) {}
+}
+
+export class DetachPeer implements Action {
+ readonly type = DETACH_PEER;
+ constructor(public payload: {pubkey: string}) {}
+}
+
+export class RemovePeer implements Action {
+ readonly type = REMOVE_PEER;
+ constructor(public payload: {pubkey: string}) {}
+}
+
+export class SaveNewInvoice implements Action {
+ readonly type = SAVE_NEW_INVOICE;
+ constructor(public payload: {memo: string, invoiceValue: number}) {}
+}
+
+export class AddInvoice implements Action {
+ readonly type = ADD_INVOICE;
+ constructor(public payload: Invoice) {}
+}
+
+export class FetchFees implements Action {
+ readonly type = FETCH_FEES;
+}
+
+export class SetFees implements Action {
+ readonly type = SET_FEES;
+ constructor(public payload: Fees) {}
+}
+
+export class FetchBalance implements Action {
+ readonly type = FETCH_BALANCE;
+ constructor(public payload: string) {} // payload = routeParam
+}
+
+export class SetBalance implements Action {
+ readonly type = SET_BALANCE;
+ constructor(public payload: {target: string, balance: Balance}) {}
+}
+
+export class FetchNetwork implements Action {
+ readonly type = FETCH_NETWORK;
+}
+
+export class SetNetwork implements Action {
+ readonly type = SET_NETWORK;
+ constructor(public payload: NetworkInfo) {}
+}
+
+export class FetchChannels implements Action {
+ readonly type = FETCH_CHANNELS;
+ constructor(public payload: {routeParam: string, channelStatus: string}) {}
+}
+
+export class SetChannels implements Action {
+ readonly type = SET_CHANNELS;
+ constructor(public payload: Channel[]) {}
+}
+
+export class UpdateChannels implements Action {
+ readonly type = UPDATE_CHANNELS;
+ constructor(public payload: any) {}
+}
+
+export class SetPendingChannels implements Action {
+ readonly type = SET_PENDING_CHANNELS;
+ constructor(public payload: PendingChannels) {}
+}
+
+export class SetClosedChannels implements Action {
+ readonly type = SET_CLOSED_CHANNELS;
+ constructor(public payload: ClosedChannel[]) {}
+}
+
+export class SaveNewChannel implements Action {
+ readonly type = SAVE_NEW_CHANNEL;
+ constructor(public payload: {selectedPeerPubkey: string, fundingAmount: number, transType: string, transTypeValue: string, spendUnconfirmed: boolean}) {}
+}
+
+export class CloseChannel implements Action {
+ readonly type = CLOSE_CHANNEL;
+ constructor(public payload: {channelPoint: string, forcibly: boolean, channelStatus: boolean}) {}
+}
+
+export class RemoveChannel implements Action {
+ readonly type = REMOVE_CHANNEL;
+ constructor(public payload: {channelPoint: string}) {}
+}
+
+export class FetchInvoices implements Action {
+ readonly type = FETCH_INVOICES;
+}
+
+export class SetInvoices implements Action {
+ readonly type = SET_INVOICES;
+ constructor(public payload: Invoice[]) {}
+}
+
+export class FetchTransactions implements Action {
+ readonly type = FETCH_TRANSACTIONS;
+}
+
+export class SetTransactions implements Action {
+ readonly type = SET_TRANSACTIONS;
+ constructor(public payload: Transaction[]) {}
+}
+
+export class FetchPayments implements Action {
+ readonly type = FETCH_PAYMENTS;
+}
+
+export class SetPayments implements Action {
+ readonly type = SET_PAYMENTS;
+ constructor(public payload: Payment[]) {}
+}
+
+export class DecodePayment implements Action {
+ readonly type = DECODE_PAYMENT;
+ constructor(public payload: string) {} // payload = routeParam
+}
+
+export class SetDecodedPayment implements Action {
+ readonly type = SET_DECODED_PAYMENT;
+ constructor(public payload: PayRequest) {}
+}
+
+export class SendPayment implements Action {
+ readonly type = SEND_PAYMENT;
+ constructor(public payload: [string, PayRequest, boolean]) {} // payload = [paymentReqStr, paymentDecoded, EmptyAmtInvoice]
+}
+
+export class FetchGraphNode implements Action {
+ readonly type = FETCH_GRAPH_NODE;
+ constructor(public payload: string) {} // payload = pubkey
+}
+
+export class SetGraphNode implements Action {
+ readonly type = SET_GRAPH_NODE;
+ constructor(public payload: GraphNode) {}
+}
+
+export class GetNewAddress implements Action {
+ readonly type = GET_NEW_ADDRESS;
+ constructor(public payload: AddressType) {}
+}
+
+export class SetNewAddress implements Action {
+ readonly type = SET_NEW_ADDRESS;
+ constructor(public payload: string) {} // payload = newAddress
+}
+
+export class SetChannelTransaction implements Action {
+ readonly type = SET_CHANNEL_TRANSACTION;
+ constructor(public payload: ChannelsTransaction) {}
+}
+
+export class OperateWallet implements Action {
+ readonly type = OPERATE_WALLET;
+ constructor(public payload: {operation: string, pwd: string}) {}
+}
+
+export class FetchConfig implements Action {
+ readonly type = FETCH_CONFIG;
+ constructor(public payload: string) {} // payload = lnd/bitcoin node
+}
+
+export class ShowConfig implements Action {
+ readonly type = SHOW_CONFIG;
+ constructor(public payload: any) {} // payload = Config File
+}
+
+export class PeerLookup implements Action {
+ readonly type = PEER_LOOKUP;
+ constructor(public payload: string) {} // payload = pubkey
+}
+
+export class ChannelLookup implements Action {
+ readonly type = CHANNEL_LOOKUP;
+ constructor(public payload: string) {} // payload = chanID
+}
+
+export class InvoiceLookup implements Action {
+ readonly type = INVOICE_LOOKUP;
+ constructor(public payload: string) {} // payload = rHashStr
+}
+
+export class SetLookup implements Action {
+ readonly type = SET_LOOKUP;
+ constructor(public payload: any) {} // payload = lookup Response (Peer/Channel/Invoice)
+}
+
+export class GetForwardingHistory implements Action {
+ readonly type = GET_FORWARDING_HISTORY;
+ constructor(public payload: SwitchReq) {}
+}
+
+export class SetForwardingHistory implements Action {
+ readonly type = SET_FORWARDING_HISTORY;
+ constructor(public payload: SwitchRes) {}
+}
+
+export class IsAuthorized implements Action {
+ readonly type = IS_AUTHORIZED;
+ constructor(public payload: string) {} // payload = password
+}
+
+export class IsAuthorizedRes implements Action {
+ readonly type = IS_AUTHORIZED_RES;
+ constructor(public payload: any) {} // payload = token/error
+}
+
+export class Signin implements Action {
+ readonly type = SIGNIN;
+ constructor(public payload: string) {} // payload = password
+}
+
+export class Signout implements Action {
+ readonly type = SIGNOUT;
+ constructor() {}
+}
+
+export class InitAppData implements Action {
+ readonly type = INIT_APP_DATA;
+}
+
+export type RTLActions =
+ ClearEffectError | EffectError | OpenSpinner | CloseSpinner |
+ FetchSettings | SetSettings | SaveSettings | SetAuthSettings |
+ OpenAlert | CloseAlert | OpenConfirmation | CloseConfirmation |
+ FetchInfo | SetInfo |
+ FetchPeers | SetPeers | AddPeer | DetachPeer | SaveNewPeer | RemovePeer |
+ AddInvoice | SaveNewInvoice | GetForwardingHistory | SetForwardingHistory |
+ FetchFees | SetFees |
+ FetchBalance | SetBalance |
+ FetchNetwork | SetNetwork |
+ FetchChannels | SetChannels | SetPendingChannels | SetClosedChannels | UpdateChannels |
+ SaveNewChannel | CloseChannel | RemoveChannel |
+ FetchTransactions | SetTransactions |
+ FetchInvoices | SetInvoices |
+ FetchPayments | SetPayments | SendPayment |
+ DecodePayment | SetDecodedPayment |
+ FetchGraphNode | SetGraphNode |
+ GetNewAddress | SetNewAddress | SetChannelTransaction | OperateWallet |
+ FetchConfig | ShowConfig | PeerLookup | ChannelLookup | InvoiceLookup | SetLookup |
+ IsAuthorized | IsAuthorizedRes | Signin | Signout | InitAppData;
diff --git a/src/app/shared/store/rtl.effects.ts b/src/app/shared/store/rtl.effects.ts
new file mode 100644
index 00000000..a224c6bd
--- /dev/null
+++ b/src/app/shared/store/rtl.effects.ts
@@ -0,0 +1,1064 @@
+import { Injectable, OnDestroy } from '@angular/core';
+import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
+import { Router } from '@angular/router';
+import { Store } from '@ngrx/store';
+import { Actions, Effect, ofType } from '@ngrx/effects';
+import { of, Subject } from 'rxjs';
+import { map, mergeMap, catchError, take, withLatestFrom } from 'rxjs/operators';
+
+import { MatDialog } from '@angular/material';
+
+import { environment } from '../../../environments/environment';
+import { LoggerService } from '../services/logger.service';
+import { Settings } from '../models/RTLconfig';
+import { GetInfo, Fees, Balance, NetworkInfo, Payment, Invoice, GraphNode, Transaction, SwitchReq } from '../models/lndModels';
+
+import { SpinnerDialogComponent } from '../components/spinner-dialog/spinner-dialog.component';
+import { AlertMessageComponent } from '../components/alert-message/alert-message.component';
+import { ConfirmationMessageComponent } from '../components/confirmation-message/confirmation-message.component';
+
+import * as RTLActions from './rtl.actions';
+import * as fromRTLReducer from './rtl.reducers';
+
+@Injectable()
+export class RTLEffects implements OnDestroy {
+ dialogRef: any;
+ private unSubs: Array> = [new Subject(), new Subject()];
+
+ constructor(
+ private actions$: Actions,
+ private httpClient: HttpClient,
+ private store: Store,
+ private logger: LoggerService,
+ public dialog: MatDialog,
+ private router: Router) { }
+
+ @Effect({ dispatch: false })
+ openSpinner = this.actions$.pipe(
+ ofType(RTLActions.OPEN_SPINNER),
+ map((action: RTLActions.OpenSpinner) => {
+ this.dialogRef = this.dialog.open(SpinnerDialogComponent, { data: { titleMessage: action.payload}});
+ }
+ ));
+
+ @Effect({ dispatch: false })
+ closeSpinner = this.actions$.pipe(
+ ofType(RTLActions.CLOSE_SPINNER),
+ map((action: RTLActions.CloseSpinner) => {
+ this.dialogRef.close();
+ }
+ ));
+
+ @Effect({ dispatch: false })
+ openAlert = this.actions$.pipe(
+ ofType(RTLActions.OPEN_ALERT),
+ map((action: RTLActions.OpenAlert) => {
+ this.dialogRef = this.dialog.open(AlertMessageComponent, action.payload);
+ }
+ ));
+
+ @Effect({ dispatch: false })
+ closeAlert = this.actions$.pipe(
+ ofType(RTLActions.CLOSE_ALERT),
+ map((action: RTLActions.CloseAlert) => {
+ this.dialogRef.close();
+ }
+ ));
+
+ @Effect({ dispatch: false })
+ openConfirm = this.actions$.pipe(
+ ofType(RTLActions.OPEN_CONFIRMATION),
+ map((action: RTLActions.OpenConfirmation) => {
+ this.dialogRef = this.dialog.open(ConfirmationMessageComponent, action.payload);
+ })
+ );
+
+ @Effect({ dispatch: false })
+ closeConfirm = this.actions$.pipe(
+ ofType(RTLActions.CLOSE_CONFIRMATION),
+ take(1),
+ map((action: RTLActions.CloseConfirmation) => {
+ this.dialogRef.close();
+ this.logger.info(action.payload);
+ return action.payload;
+ }
+ ));
+
+ @Effect()
+ settingFetch = this.actions$.pipe(
+ ofType(RTLActions.FETCH_SETTINGS),
+ mergeMap((action: RTLActions.FetchSettings) => {
+ this.store.dispatch(new RTLActions.ClearEffectError('FetchSettings'));
+ return this.httpClient.get(environment.CONF_API + '/rtlconf');
+ }),
+ map((rtlConfig: any) => {
+ this.logger.info(rtlConfig);
+ this.store.dispatch(new RTLActions.SetAuthSettings(rtlConfig.authSettings));
+ return {
+ type: RTLActions.SET_SETTINGS,
+ payload: (undefined !== rtlConfig && undefined !== rtlConfig.settings) ? rtlConfig.settings :
+ {'flgSidenavOpened': true, 'flgSidenavPinned': true, 'menu': 'Vertical', 'menuType': 'Regular', 'theme': 'dark-blue', 'satsToBTC': false}
+ };
+ },
+ catchError((err) => {
+ this.logger.error(err);
+ this.store.dispatch(new RTLActions.EffectError({ action: 'FetchSettings', code: err.status, message: err.error.error }));
+ return of();
+ })
+ ));
+
+ @Effect({ dispatch: false })
+ settingSave = this.actions$.pipe(
+ ofType(RTLActions.SAVE_SETTINGS),
+ mergeMap((action: RTLActions.SaveSettings) => {
+ return this.httpClient.post(environment.CONF_API, { updatedSettings: action.payload });
+ }
+ ));
+
+ @Effect()
+ infoFetch = this.actions$.pipe(
+ ofType(RTLActions.FETCH_INFO),
+ withLatestFrom(this.store.select('rtlRoot')),
+ mergeMap(([action, store]: [RTLActions.FetchInfo, fromRTLReducer.State]) => {
+ this.store.dispatch(new RTLActions.ClearEffectError('FetchInfo'));
+ return this.httpClient.get(environment.GETINFO_API)
+ .pipe(
+ map((info) => {
+ this.logger.info(info);
+ if (undefined === info.identity_pubkey) {
+ sessionStorage.removeItem('lndUnlocked');
+ this.logger.info('Redirecting to Unlock');
+ this.router.navigate(['/unlocklnd']);
+ return of();
+ } else {
+ sessionStorage.setItem('lndUnlocked', 'true');
+ return {
+ type: RTLActions.SET_INFO,
+ payload: (undefined !== info) ? info : {}
+ };
+ }
+ }),
+ catchError((err) => {
+ this.logger.error(err);
+ this.store.dispatch(new RTLActions.EffectError({ action: 'FetchInfo', code: err.status, message: err.error.error }));
+ if (+store.authSettings.rtlSSO) {
+ this.router.navigate(['/ssoerror']);
+ } else {
+ if (err.status === 401) {
+ this.logger.info('Redirecting to Signin');
+ this.router.navigate([store.authSettings.logoutRedirectLink]);
+ return of();
+ } else {
+ this.logger.info('Redirecting to Unlock');
+ this.router.navigate(['/unlocklnd']);
+ return of();
+ }
+ }
+ })
+ );
+ }
+ ));
+
+ @Effect()
+ peersFetch = this.actions$.pipe(
+ ofType(RTLActions.FETCH_PEERS),
+ mergeMap((action: RTLActions.FetchPeers) => {
+ this.store.dispatch(new RTLActions.ClearEffectError('FetchPeers'));
+ return this.httpClient.get(environment.PEERS_API)
+ .pipe(
+ map((peers: any) => {
+ this.logger.info(peers);
+ return {
+ type: RTLActions.SET_PEERS,
+ payload: (undefined !== peers) ? peers : []
+ };
+ }),
+ catchError((err: any) => {
+ this.store.dispatch(new RTLActions.EffectError({ action: 'FetchPeers', code: err.status, message: err.error.error }));
+ this.logger.error(err);
+ return of();
+ })
+ );
+ }
+ ));
+
+ @Effect()
+ saveNewPeer = this.actions$.pipe(
+ ofType(RTLActions.SAVE_NEW_PEER),
+ mergeMap((action: RTLActions.SaveNewPeer) => {
+ return this.httpClient.post(environment.PEERS_API, {pubkey: action.payload.pubkey, host: action.payload.host, perm: action.payload.perm})
+ .pipe(
+ map((postRes: any) => {
+ this.logger.info(postRes);
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '70%', data: { type: 'SUCCESS', titleMessage: 'Peer Added Successfully!'}}));
+ return {
+ type: RTLActions.SET_PEERS,
+ payload: (undefined !== postRes && postRes.length > 0) ? postRes : []
+ };
+ }),
+ catchError((err: any) => {
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.logger.error(err);
+ return of(
+ {
+ type: RTLActions.OPEN_ALERT,
+ payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Add Peer Failed',
+ message: JSON.stringify({code: err.status, Message: err.error.error})
+ }}
+ }
+ );
+ })
+ );
+ }
+ ));
+
+ @Effect()
+ detachPeer = this.actions$.pipe(
+ ofType(RTLActions.DETACH_PEER),
+ mergeMap((action: RTLActions.DetachPeer) => {
+ return this.httpClient.delete(environment.PEERS_API + '/' + action.payload.pubkey)
+ .pipe(
+ map((postRes: any) => {
+ this.logger.info(postRes);
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '70%', data: {type: 'SUCCESS', titleMessage: 'Peer Detached Successfully!'}}));
+ return {
+ type: RTLActions.REMOVE_PEER,
+ payload: { pubkey: action.payload.pubkey }
+ };
+ }),
+ catchError((err: any) => {
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.logger.error(err);
+ return of(
+ {
+ type: RTLActions.OPEN_ALERT,
+ payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Unable to Detach Peer. Try again later.',
+ message: JSON.stringify({code: err.status, Message: err.error.error})}
+ }
+ }
+ );
+ })
+ );
+ }
+ ));
+
+ @Effect()
+ saveNewInvoice = this.actions$.pipe(
+ ofType(RTLActions.SAVE_NEW_INVOICE),
+ mergeMap((action: RTLActions.SaveNewInvoice) => {
+ return this.httpClient.post(environment.INVOICES_API, {memo: action.payload.memo, amount: action.payload.invoiceValue})
+ .pipe(
+ map((postRes: any) => {
+ postRes.memo = action.payload.memo;
+ postRes.value = action.payload.invoiceValue;
+ postRes.expiry = '3600';
+ postRes.cltv_expiry = '144';
+ postRes.creation_date = Math.round(new Date().getTime() / 1000).toString();
+ postRes.creation_date_str = new Date(+postRes.creation_date * 1000).toUTCString();
+ this.logger.info(postRes);
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ const msg = { payment_request: postRes.payment_request };
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '70%',
+ data: { type: 'SUCCESS', titleMessage: 'Invoice Added Successfully!', message: JSON.stringify(msg) }}));
+ return {
+ type: RTLActions.ADD_INVOICE,
+ payload: postRes
+ };
+ }),
+ catchError((err: any) => {
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.logger.error(err);
+ return of(
+ {
+ type: RTLActions.OPEN_ALERT,
+ payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Add Invoice Failed',
+ message: JSON.stringify({code: err.status, Message: err.error.error})
+ }}
+ }
+ );
+ })
+ );
+ }
+ ));
+
+ @Effect()
+ openNewChannel = this.actions$.pipe(
+ ofType(RTLActions.SAVE_NEW_CHANNEL),
+ mergeMap((action: RTLActions.SaveNewChannel) => {
+ return this.httpClient.post(environment.CHANNELS_API, {
+ node_pubkey: action.payload.selectedPeerPubkey, local_funding_amount: action.payload.fundingAmount,
+ trans_type: action.payload.transType, trans_type_value: action.payload.transTypeValue, spend_unconfirmed: action.payload.spendUnconfirmed
+ })
+ .pipe(
+ map((postRes: any) => {
+ this.logger.info(postRes);
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '70%', data: {type: 'SUCCESS', titleMessage: 'Channel Added Successfully!'}}));
+ this.store.dispatch(new RTLActions.FetchBalance('blockchain'));
+ this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'all', channelStatus: ''}));
+ return {
+ type: RTLActions.FETCH_CHANNELS,
+ payload: {routeParam: 'pending', channelStatus: ''}
+ };
+ }),
+ catchError((err: any) => {
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.logger.error(err);
+ return of(
+ {
+ type: RTLActions.OPEN_ALERT,
+ payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Open Channel Failed',
+ message: JSON.stringify({code: err.status, Message: err.error.error})
+ }}
+ }
+ );
+ })
+ );
+ }
+ ));
+
+ @Effect()
+ updateChannel = this.actions$.pipe(
+ ofType(RTLActions.UPDATE_CHANNELS),
+ mergeMap((action: RTLActions.UpdateChannels) => {
+ return this.httpClient.post(environment.CHANNELS_API + '/chanPolicy',
+ { baseFeeMsat: action.payload.baseFeeMsat, feeRate: action.payload.feeRate, timeLockDelta: action.payload.timeLockDelta, chanPoint: action.payload.chanPoint })
+ .pipe(
+ map((postRes: any) => {
+ this.logger.info(postRes);
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '70%', data: {type: 'SUCCESS', titleMessage: 'Channel Updated Successfully!'}}));
+ return {
+ type: RTLActions.FETCH_CHANNELS,
+ payload: {routeParam: 'all', channelStatus: ''}
+ };
+ }),
+ catchError((err: any) => {
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.logger.error(err);
+ return of(
+ {
+ type: RTLActions.OPEN_ALERT,
+ payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Update Channel Failed',
+ message: JSON.stringify({code: err.status, Message: err.error.error})
+ }}
+ }
+ );
+ })
+ );
+ }
+ ));
+
+ @Effect()
+ closeChannel = this.actions$.pipe(
+ ofType(RTLActions.CLOSE_CHANNEL),
+ mergeMap((action: RTLActions.CloseChannel) => {
+ return this.httpClient.delete(environment.CHANNELS_API + '/' + action.payload.channelPoint + '?force=' + action.payload.forcibly)
+ .pipe(
+ map((postRes: any) => {
+ this.logger.info(postRes);
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '70%', data: {type: 'SUCCESS', titleMessage: 'Channel Closed Successfully!'}}));
+ this.store.dispatch(new RTLActions.FetchBalance('channels'));
+ this.store.dispatch(new RTLActions.FetchBalance('blockchain'));
+ this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'all', channelStatus: ''}));
+ if (action.payload.channelStatus) {
+ this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'closed', channelStatus: ''}));
+ } else {
+ this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'pending', channelStatus: ''}));
+ }
+ return {
+ type: RTLActions.REMOVE_CHANNEL,
+ payload: { channelPoint: action.payload.channelPoint }
+ };
+ }),
+ catchError((err: any) => {
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.logger.error(err);
+ return of(
+ {
+ type: RTLActions.OPEN_ALERT,
+ payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Unable to Close Channel. Try again later.',
+ message: JSON.stringify({code: err.status, Message: err.error.error.message})}}
+ }
+ );
+ })
+ );
+ }
+ ));
+
+ @Effect()
+ fetchFees = this.actions$.pipe(
+ ofType(RTLActions.FETCH_FEES),
+ mergeMap((action: RTLActions.FetchFees) => {
+ this.store.dispatch(new RTLActions.ClearEffectError('FetchFees'));
+ return this.httpClient.get(environment.FEES_API);
+ }),
+ map((fees) => {
+ this.logger.info(fees);
+ return {
+ type: RTLActions.SET_FEES,
+ payload: (undefined !== fees) ? fees : {}
+ };
+ }),
+ catchError((err: any) => {
+ this.logger.error(err);
+ this.store.dispatch(new RTLActions.EffectError({ action: 'FetchFees', code: err.status, message: err.error.error }));
+ return of();
+ }
+ ));
+
+ @Effect()
+ balanceFetch = this.actions$.pipe(
+ ofType(RTLActions.FETCH_BALANCE),
+ mergeMap((action: RTLActions.FetchBalance) => {
+ this.store.dispatch(new RTLActions.ClearEffectError('FetchBalance/' + action.payload));
+ return this.httpClient.get(environment.BALANCE_API + '/' + action.payload)
+ .pipe(
+ map((res: any) => {
+ if (action.payload === 'channels') {
+ this.store.dispatch(new RTLActions.FetchBalance('blockchain'));
+ }
+ this.logger.info(res);
+ const emptyRes = (action.payload === 'channels') ? {balance: '', btc_balance: ''} : {total_balance: '', btc_total_balance: ''};
+ return {
+ type: RTLActions.SET_BALANCE,
+ payload: (undefined !== res) ? { target: action.payload, balance: res } : { target: action.payload, balance: emptyRes }
+ };
+ }),
+ catchError((err: any) => {
+ this.logger.error(err);
+ this.store.dispatch(new RTLActions.EffectError({ action: 'FetchBalance/' + action.payload, code: err.status, message: err.error.error }));
+ return of();
+ }
+ ));
+ }
+ ));
+
+ @Effect()
+ networkInfoFetch = this.actions$.pipe(
+ ofType(RTLActions.FETCH_NETWORK),
+ mergeMap((action: RTLActions.FetchNetwork) => {
+ this.store.dispatch(new RTLActions.ClearEffectError('FetchNetwork'));
+ return this.httpClient.get(environment.NETWORK_API + '/info');
+ }),
+ map((networkInfo) => {
+ this.logger.info(networkInfo);
+ return {
+ type: RTLActions.SET_NETWORK,
+ payload: (undefined !== networkInfo) ? networkInfo : {}
+ };
+ }),
+ catchError((err: any) => {
+ this.logger.error(err);
+ this.store.dispatch(new RTLActions.EffectError({ action: 'FetchNetwork', code: err.status, message: err.error.error }));
+ return of();
+ }
+ ));
+
+ @Effect()
+ channelsFetch = this.actions$.pipe(
+ ofType(RTLActions.FETCH_CHANNELS),
+ mergeMap((action: RTLActions.FetchChannels) => {
+ const options =
+ (undefined === action.payload.channelStatus || action.payload.channelStatus === '') ? {} : { params: new HttpParams().set(action.payload.channelStatus, 'true') };
+ return this.httpClient.get(environment.CHANNELS_API + '/' + action.payload.routeParam, options)
+ .pipe(
+ map((channels: any) => {
+ this.logger.info(channels);
+ if (action.payload.routeParam === 'pending') {
+ return {
+ type: RTLActions.SET_PENDING_CHANNELS,
+ payload: (undefined !== channels) ? channels : {}
+ };
+ } else if (action.payload.routeParam === 'closed') {
+ return {
+ type: RTLActions.SET_CLOSED_CHANNELS,
+ payload: (undefined !== channels && undefined !== channels.channels && channels.channels.length > 0) ? channels.channels : []
+ };
+ } else if (action.payload.routeParam === 'all') {
+ return {
+ type: RTLActions.SET_CHANNELS,
+ payload: (undefined !== channels && undefined !== channels.channels && channels.channels.length > 0) ? channels.channels : []
+ };
+ }
+ },
+ catchError((err: any) => {
+ this.logger.error(err);
+ this.store.dispatch(new RTLActions.EffectError({ action: 'FetchChannels/' + action.payload.routeParam, code: err.status, message: err.error.error }));
+ return of();
+ })
+ ));
+ }
+ ));
+
+ @Effect()
+ invoicesFetch = this.actions$.pipe(
+ ofType(RTLActions.FETCH_INVOICES),
+ mergeMap((action: RTLActions.FetchInvoices) => {
+ this.store.dispatch(new RTLActions.ClearEffectError('FetchInvoices'));
+ return this.httpClient.get(environment.INVOICES_API);
+ }),
+ map((res: any) => {
+ this.logger.info(res);
+ return {
+ type: RTLActions.SET_INVOICES,
+ payload: (undefined !== res && undefined !== res.invoices && res.invoices.length > 0) ? res.invoices : []
+ };
+ }),
+ catchError((err: any) => {
+ this.logger.error(err);
+ this.store.dispatch(new RTLActions.EffectError({ action: 'FetchInvoices', code: err.status, message: err.error.error }));
+ return of();
+ }
+ ));
+
+ @Effect()
+ transactionsFetch = this.actions$.pipe(
+ ofType(RTLActions.FETCH_TRANSACTIONS),
+ mergeMap((action: RTLActions.FetchTransactions) => {
+ this.store.dispatch(new RTLActions.ClearEffectError('FetchTransactions'));
+ return this.httpClient.get(environment.TRANSACTIONS_API);
+ }),
+ map((transactions) => {
+ this.logger.info(transactions);
+ return {
+ type: RTLActions.SET_TRANSACTIONS,
+ payload: (undefined !== transactions && transactions.length > 0) ? transactions : []
+ };
+ }),
+ catchError((err: any) => {
+ this.logger.error(err);
+ this.store.dispatch(new RTLActions.EffectError({ action: 'FetchTransactions', code: err.status, message: err.error.error }));
+ return of();
+ }
+ ));
+
+ @Effect()
+ paymentsFetch = this.actions$.pipe(
+ ofType(RTLActions.FETCH_PAYMENTS),
+ mergeMap((action: RTLActions.FetchPayments) => {
+ this.store.dispatch(new RTLActions.ClearEffectError('FetchPayments'));
+ return this.httpClient.get(environment.PAYMENTS_API);
+ }),
+ map((payments) => {
+ this.logger.info(payments);
+ return {
+ type: RTLActions.SET_PAYMENTS,
+ payload: (undefined !== payments) ? payments : []
+ };
+ }),
+ catchError((err: any) => {
+ this.logger.error(err);
+ this.store.dispatch(new RTLActions.EffectError({ action: 'FetchPayments', code: err.status, message: err.error.error }));
+ return of();
+ }
+ ));
+
+ @Effect()
+ decodePayment = this.actions$.pipe(
+ ofType(RTLActions.DECODE_PAYMENT),
+ mergeMap((action: RTLActions.DecodePayment) => {
+ return this.httpClient.get(environment.PAYREQUEST_API + '/' + action.payload)
+ .pipe(
+ map((decodedPayment) => {
+ this.logger.info(decodedPayment);
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ return {
+ type: RTLActions.SET_DECODED_PAYMENT,
+ payload: (undefined !== decodedPayment) ? decodedPayment : {}
+ };
+ }),
+ catchError((err: any) => {
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.logger.error(err);
+ return of(
+ {
+ type: RTLActions.OPEN_ALERT,
+ payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Decode Payment Failed',
+ message: JSON.stringify({Code: err.status, Message: err.error.error, URL: environment.PAYREQUEST_API + '/' + action.payload})}}
+ }
+ );
+ })
+ );
+ })
+ );
+
+ @Effect({ dispatch: false })
+ setDecodedPayment = this.actions$.pipe(
+ ofType(RTLActions.SET_DECODED_PAYMENT),
+ map((action: RTLActions.SetDecodedPayment) => {
+ this.logger.info(action.payload);
+ return action.payload;
+ })
+ );
+
+ @Effect()
+ sendPayment = this.actions$.pipe(
+ ofType(RTLActions.SEND_PAYMENT),
+ withLatestFrom(this.store.select('rtlRoot')),
+ mergeMap(([action, store]: [RTLActions.SendPayment, fromRTLReducer.State]) => {
+ let queryHeaders = {};
+ if (action.payload[2]) {
+ queryHeaders = {paymentDecoded: action.payload[1]};
+ } else {
+ queryHeaders = {paymentReq: action.payload[0]};
+ }
+ return this.httpClient.post(environment.CHANNELS_API + '/transactions', queryHeaders)
+ .pipe(
+ map((sendRes: any) => {
+ this.logger.info(sendRes);
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ if (sendRes.payment_error) {
+ this.logger.error('Error: ' + sendRes.payment_error);
+ return of({
+ type: RTLActions.OPEN_ALERT,
+ payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Send Payment Failed',
+ message: JSON.stringify(
+ {code: sendRes.payment_error.status, Message: sendRes.payment_error.error.message, URL: environment.CHANNELS_API + '/transactions/' + action.payload[0]}
+ )
+ }}
+ });
+ } else {
+ const confirmationMsg = { 'Destination': action.payload[1].destination, 'Timestamp': action.payload[1].timestamp_str, 'Expiry': action.payload[1].expiry };
+ confirmationMsg['Amount (' + ((undefined === store.information.smaller_currency_unit) ?
+ 'Sats' : store.information.smaller_currency_unit) + ')'] = action.payload[1].num_satoshis;
+ const msg = {};
+ msg['Total Fee (' + ((undefined === store.information.smaller_currency_unit) ? 'Sats' : store.information.smaller_currency_unit) + ')'] =
+ (sendRes.payment_route.total_fees_msat / 1000);
+ Object.assign(msg, confirmationMsg);
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '70%',
+ data: { type: 'SUCCESS', titleMessage: 'Payment Sent Successfully!', message: JSON.stringify(msg) }}));
+ this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'all', channelStatus: ''}));
+ this.store.dispatch(new RTLActions.FetchPayments());
+ return {
+ type: RTLActions.SET_DECODED_PAYMENT,
+ payload: {}
+ };
+ }
+ }),
+ catchError((err: any) => {
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.logger.error(err);
+ return of(
+ {
+ type: RTLActions.OPEN_ALERT,
+ payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Send Payment Failed',
+ message: JSON.stringify({code: err.status, Message: err.error.error, URL: environment.CHANNELS_API + '/transactions/' + action.payload[0]})}}
+ }
+ );
+ })
+ );
+ })
+ );
+
+ @Effect()
+ graphNodeFetch = this.actions$.pipe(
+ ofType(RTLActions.FETCH_GRAPH_NODE),
+ mergeMap((action: RTLActions.FetchGraphNode) => {
+ return this.httpClient.get(environment.NETWORK_API + '/node/' + action.payload)
+ .pipe(map((graphNode: any) => {
+ this.logger.info(graphNode);
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ return {
+ type: RTLActions.SET_GRAPH_NODE,
+ payload: (undefined !== graphNode) ? graphNode : {}
+ };
+ }),
+ catchError((err: any) => {
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.logger.error(err);
+ return of(
+ {
+ type: RTLActions.OPEN_ALERT,
+ payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Get Node Address Failed',
+ message: JSON.stringify({Code: err.status, Message: err.error.error})}}
+ }
+ );
+ }));
+ }
+ ));
+
+ @Effect({ dispatch: false })
+ setGraphNode = this.actions$.pipe(
+ ofType(RTLActions.SET_GRAPH_NODE),
+ map((action: RTLActions.SetGraphNode) => {
+ this.logger.info(action.payload);
+ return action.payload;
+ })
+ );
+
+ @Effect()
+ getNewAddress = this.actions$.pipe(
+ ofType(RTLActions.GET_NEW_ADDRESS),
+ mergeMap((action: RTLActions.GetNewAddress) => {
+ return this.httpClient.get(environment.NEW_ADDRESS_API + '?type=' + action.payload.addressId)
+ .pipe(map((newAddress: any) => {
+ this.logger.info(newAddress);
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ return {
+ type: RTLActions.SET_NEW_ADDRESS,
+ payload: (undefined !== newAddress && undefined !== newAddress.address) ? newAddress.address : {}
+ };
+ }),
+ catchError((err: any) => {
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.logger.error(err);
+ return of(
+ {
+ type: RTLActions.OPEN_ALERT,
+ payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Generate New Address Failed',
+ message: JSON.stringify({Code: err.status, Message: err.error.error, URL: environment.NEW_ADDRESS_API + '?type=' + action.payload.addressId})}}
+ }
+ );
+ }));
+ })
+ );
+
+ @Effect({ dispatch: false })
+ setNewAddress = this.actions$.pipe(
+ ofType(RTLActions.SET_NEW_ADDRESS),
+ map((action: RTLActions.SetNewAddress) => {
+ this.logger.info(action.payload);
+ return action.payload;
+ })
+ );
+
+ @Effect()
+ configFetch = this.actions$.pipe(
+ ofType(RTLActions.FETCH_CONFIG),
+ mergeMap((action: RTLActions.FetchConfig) => {
+ this.store.dispatch(new RTLActions.ClearEffectError('fetchConfig'));
+ return this.httpClient.get(environment.CONF_API + '/config/' + action.payload)
+ .pipe(
+ map((configFile: any) => {
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ return {
+ type: RTLActions.SHOW_CONFIG,
+ payload: configFile
+ };
+ }),
+ catchError((err: any) => {
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.logger.error(err);
+ this.store.dispatch(new RTLActions.EffectError({ action: 'fetchConfig', code: err.status, message: err.error.error }));
+ return of(
+ {
+ type: RTLActions.OPEN_ALERT,
+ payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Fetch Config Failed!',
+ message: JSON.stringify({Code: err.status, Message: err.error.error})}}
+ }
+ );
+ }
+ ));
+ })
+ );
+
+ @Effect({ dispatch: false })
+ showLNDConfig = this.actions$.pipe(
+ ofType(RTLActions.SHOW_CONFIG),
+ map((action: RTLActions.ShowConfig) => {
+ this.logger.info(action.payload);
+ return action.payload;
+ })
+ );
+
+ @Effect()
+ SetChannelTransaction = this.actions$.pipe(
+ ofType(RTLActions.SET_CHANNEL_TRANSACTION),
+ mergeMap((action: RTLActions.SetChannelTransaction) => {
+ this.store.dispatch(new RTLActions.ClearEffectError('SetChannelTransaction'));
+ return this.httpClient.post(environment.TRANSACTIONS_API,
+ { amount: action.payload.amount, address: action.payload.address, sendAll: action.payload.sendAll, fees: action.payload.fees, blocks: action.payload.blocks }
+ )
+ .pipe(
+ map((postRes: any) => {
+ this.logger.info(postRes);
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ return {
+ type: RTLActions.OPEN_ALERT,
+ payload: { data: {type: 'SUCCESS', titleMessage: 'Fund Sent Successfully!'} }
+ };
+ }),
+ catchError((err: any) => {
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.store.dispatch(new RTLActions.EffectError({ action: 'SetChannelTransaction', code: err.status, message: err.error.error }));
+ this.logger.error(err);
+ return of(
+ {
+ type: RTLActions.OPEN_ALERT,
+ payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Sending Fund Failed',
+ message: JSON.stringify({Code: err.status, Message: err.error.error})}}
+ }
+ );
+ }));
+ })
+ );
+
+ @Effect()
+ fetchForwardingHistory = this.actions$.pipe(
+ ofType(RTLActions.GET_FORWARDING_HISTORY),
+ mergeMap((action: RTLActions.GetForwardingHistory) => {
+ this.store.dispatch(new RTLActions.ClearEffectError('GetForwardingHistory'));
+ const queryHeaders: SwitchReq = {
+ num_max_events: action.payload.num_max_events, index_offset: action.payload.index_offset, end_time: action.payload.end_time , start_time: action.payload.start_time
+ };
+ return this.httpClient.post(environment.SWITCH_API, queryHeaders)
+ .pipe(
+ map((fhRes: any) => {
+ this.logger.info(fhRes);
+ return {
+ type: RTLActions.SET_FORWARDING_HISTORY,
+ payload: fhRes
+ };
+ }),
+ catchError((err: any) => {
+ this.store.dispatch(new RTLActions.EffectError({ action: 'GetForwardingHistory', code: err.status, message: err.error.error }));
+ this.logger.error(err);
+ return of(
+ {
+ type: RTLActions.OPEN_ALERT,
+ payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Get Forwarding History Failed',
+ message: JSON.stringify({code: err.status, Message: err.error.error, URL: environment.SWITCH_API})}}
+ }
+ );
+ })
+ );
+ })
+ );
+
+ @Effect({ dispatch : false })
+ operateWallet = this.actions$.pipe(
+ ofType(RTLActions.OPERATE_WALLET),
+ mergeMap((action: RTLActions.OperateWallet) => {
+ return this.httpClient.post(environment.WALLET_API + '/' + action.payload.operation, { wallet_password: action.payload.pwd })
+ .pipe(
+ map((postRes) => {
+ this.logger.info(postRes);
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.store.dispatch(new RTLActions.OpenSpinner('Initializing Node...'));
+ this.logger.info('Successfully Unlocked!');
+ sessionStorage.setItem('lndUnlocked', 'true');
+ setTimeout(() => {
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.logger.info('Successfully Initialized!');
+ this.store.dispatch(new RTLActions.InitAppData());
+ this.router.navigate(['/']);
+ }, 1000 * 90);
+ return of({});
+ }),
+ catchError((err) => {
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '70%', data: {type: 'ERROR', titleMessage: err.error.error}}));
+ this.logger.error(err.error.error);
+ return of();
+ })
+ );
+ }
+ ));
+
+ @Effect()
+ isAuthorized = this.actions$.pipe(
+ ofType(RTLActions.IS_AUTHORIZED),
+ withLatestFrom(this.store.select('rtlRoot')),
+ mergeMap(([action, store]: [RTLActions.IsAuthorized, fromRTLReducer.State]) => {
+ this.store.dispatch(new RTLActions.ClearEffectError('IsAuthorized'));
+ return this.httpClient.post(environment.AUTHENTICATE_API, { password: action.payload })
+ .pipe(
+ map((postRes: any) => {
+ this.logger.info(postRes);
+ this.logger.info('Successfully Authorized!');
+ return {
+ type: RTLActions.IS_AUTHORIZED_RES,
+ payload: postRes
+ };
+ }),
+ catchError((err) => {
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '70%', data: {type: 'ERROR', titleMessage: 'Authorization Failed',
+ message: JSON.stringify({Code: err.status, Message: err.error.error})}}));
+ this.store.dispatch(new RTLActions.EffectError({ action: 'IsAuthorized', code: err.status, message: err.error.message }));
+ this.logger.error(err.error);
+ return of({
+ type: RTLActions.IS_AUTHORIZED_RES,
+ payload: 'ERROR'
+ });
+ })
+ );
+ }));
+
+ @Effect({ dispatch: false })
+ isAuthorizedRes = this.actions$.pipe(
+ ofType(RTLActions.IS_AUTHORIZED_RES),
+ map((action: RTLActions.IsAuthorizedRes) => {
+ return action.payload;
+ })
+ );
+
+ @Effect({ dispatch: false })
+ authSignin = this.actions$.pipe(
+ ofType(RTLActions.SIGNIN),
+ withLatestFrom(this.store.select('rtlRoot')),
+ mergeMap(([action, store]: [RTLActions.Signin, fromRTLReducer.State]) => {
+ this.store.dispatch(new RTLActions.ClearEffectError('Signin'));
+ return this.httpClient.post(environment.AUTHENTICATE_API, { password: action.payload })
+ .pipe(
+ map((postRes: any) => {
+ this.logger.info(postRes);
+ this.logger.info('Successfully Authorized!');
+ this.SetToken(postRes.token);
+ this.router.navigate(['/']);
+ }),
+ catchError((err) => {
+ this.store.dispatch(new RTLActions.OpenAlert({ width: '70%', data: {type: 'ERROR', message: JSON.stringify(err.error)}}));
+ this.store.dispatch(new RTLActions.EffectError({ action: 'Signin', code: err.status, message: err.error.message }));
+ this.logger.error(err.error);
+ this.logger.info('Redirecting to Signin Error Page');
+ if (+store.authSettings.rtlSSO) {
+ this.router.navigate(['/ssoerror']);
+ } else {
+ this.router.navigate([store.authSettings.logoutRedirectLink]);
+ }
+ return of();
+ })
+ );
+ }));
+
+ @Effect({ dispatch: false })
+ signOut = this.actions$.pipe(
+ ofType(RTLActions.SIGNOUT),
+ withLatestFrom(this.store.select('rtlRoot')),
+ mergeMap(([action, store]: [RTLActions.Signout, fromRTLReducer.State]) => {
+ if (+store.authSettings.rtlSSO) {
+ window.location.href = store.authSettings.logoutRedirectLink;
+ } else {
+ this.router.navigate([store.authSettings.logoutRedirectLink]);
+ }
+ sessionStorage.removeItem('lndUnlocked');
+ sessionStorage.removeItem('token');
+ this.logger.warn('LOGGED OUT');
+ return of();
+ }));
+
+ @Effect()
+ peerLookup = this.actions$.pipe(
+ ofType(RTLActions.PEER_LOOKUP),
+ mergeMap((action: RTLActions.PeerLookup) => {
+ this.store.dispatch(new RTLActions.ClearEffectError('Lookup'));
+ return this.httpClient.get(environment.NETWORK_API + '/node/' + action.payload)
+ .pipe(
+ map((resPeer) => {
+ this.logger.info(resPeer);
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ return {
+ type: RTLActions.SET_LOOKUP,
+ payload: resPeer
+ };
+ }),
+ catchError((err: any) => {
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.store.dispatch(new RTLActions.EffectError({ action: 'Lookup', code: err.status, message: err.error.message }));
+ this.logger.error(err);
+ return of(
+ {
+ type: RTLActions.OPEN_ALERT,
+ payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Peer Lookup Failed',
+ message: JSON.stringify({Code: err.status, Message: err.error.error, URL: environment.NETWORK_API + '/node/' + action.payload})}}
+ }
+ );
+ })
+ );
+ })
+ );
+
+ @Effect()
+ channelLookup = this.actions$.pipe(
+ ofType(RTLActions.CHANNEL_LOOKUP),
+ mergeMap((action: RTLActions.ChannelLookup) => {
+ this.store.dispatch(new RTLActions.ClearEffectError('Lookup'));
+ return this.httpClient.get(environment.NETWORK_API + '/edge/' + action.payload)
+ .pipe(
+ map((resChannel) => {
+ this.logger.info(resChannel);
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ return {
+ type: RTLActions.SET_LOOKUP,
+ payload: resChannel
+ };
+ }),
+ catchError((err: any) => {
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.store.dispatch(new RTLActions.EffectError({ action: 'Lookup', code: err.status, message: err.error.message }));
+ this.logger.error(err);
+ return of(
+ {
+ type: RTLActions.OPEN_ALERT,
+ payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Channel Lookup Failed',
+ message: JSON.stringify({Code: err.status, Message: err.error.error, URL: environment.NETWORK_API + '/edge/' + action.payload})}}
+ }
+ );
+ })
+ );
+ })
+ );
+
+ @Effect()
+ invoiceLookup = this.actions$.pipe(
+ ofType(RTLActions.INVOICE_LOOKUP),
+ mergeMap((action: RTLActions.InvoiceLookup) => {
+ this.store.dispatch(new RTLActions.ClearEffectError('Lookup'));
+ return this.httpClient.get(environment.INVOICES_API + '/' + action.payload)
+ .pipe(
+ map((resInvoice) => {
+ this.logger.info(resInvoice);
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ return {
+ type: RTLActions.SET_LOOKUP,
+ payload: resInvoice
+ };
+ }),
+ catchError((err: any) => {
+ this.store.dispatch(new RTLActions.CloseSpinner());
+ this.store.dispatch(new RTLActions.EffectError({ action: 'Lookup', code: err.status, message: err.error.message }));
+ this.logger.error(err);
+ return of(
+ {
+ type: RTLActions.OPEN_ALERT,
+ payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Invoice Lookup Failed',
+ message: JSON.stringify({Code: err.status, Message: err.error.error, URL: environment.INVOICES_API + '/' + action.payload})}}
+ }
+ );
+ })
+ );
+ })
+ );
+
+ @Effect({ dispatch: false })
+ setLookup = this.actions$.pipe(
+ ofType(RTLActions.SET_LOOKUP),
+ map((action: RTLActions.SetLookup) => {
+ this.logger.info(action.payload);
+ return action.payload;
+ })
+ );
+
+ SetToken(token: string) {
+ if (token) {
+ sessionStorage.setItem('lndUnlocked', 'true');
+ sessionStorage.setItem('token', token);
+ this.store.dispatch(new RTLActions.InitAppData());
+ } else {
+ sessionStorage.removeItem('lndUnlocked');
+ sessionStorage.removeItem('token');
+ }
+ }
+
+ ngOnDestroy() {
+ this.unSubs.forEach(completeSub => {
+ completeSub.next();
+ completeSub.complete();
+ });
+ }
+
+}
diff --git a/src/app/shared/store/rtl.reducers.ts b/src/app/shared/store/rtl.reducers.ts
new file mode 100644
index 00000000..3b0b2692
--- /dev/null
+++ b/src/app/shared/store/rtl.reducers.ts
@@ -0,0 +1,265 @@
+import * as RTLActions from './rtl.actions';
+
+import { ErrorPayload } from '../models/errorPayload';
+import { Settings, Authentication } from '../models/RTLconfig';
+import {
+ GetInfo, GetInfoChain, Peer, AddressType, Fees, NetworkInfo, Balance, Channel, Payment, Invoice, PendingChannels, ClosedChannel, Transaction, SwitchRes
+} from '../models/lndModels';
+
+export interface State {
+ effectErrors: ErrorPayload[];
+ settings: Settings;
+ authSettings: Authentication;
+ information: GetInfo;
+ peers: Peer[];
+ fees: Fees;
+ networkInfo: NetworkInfo;
+ channelBalance: Balance;
+ blockchainBalance: Balance;
+ allChannels: Channel[];
+ closedChannels: ClosedChannel[];
+ pendingChannels: PendingChannels;
+ numberOfActiveChannels: number;
+ numberOfInactiveChannels: number;
+ numberOfPendingChannels: number;
+ totalLocalBalance: number;
+ totalRemoteBalance: number;
+ transactions: Transaction[];
+ payments: Payment[];
+ invoices: Invoice[];
+ forwardingHistory: SwitchRes;
+ addressTypes: AddressType[];
+}
+
+const initialState: State = {
+ effectErrors: [],
+ settings: {flgSidenavOpened: true, flgSidenavPinned: true, menu: 'Vertical', menuType: 'Regular', theme: 'dark-blue', satsToBTC: false},
+ authSettings: {nodeAuthType: 'CUSTOM', lndConfigPath: '', bitcoindConfigPath: '', rtlSSO: 0, logoutRedirectLink: '/login' },
+ information: {},
+ peers: [],
+ fees: {},
+ networkInfo: {},
+ channelBalance: {balance: '', btc_balance: ''},
+ blockchainBalance: { total_balance: '', btc_total_balance: ''},
+ allChannels: [],
+ closedChannels: [],
+ pendingChannels: {},
+ numberOfActiveChannels: 0,
+ numberOfInactiveChannels: 0,
+ numberOfPendingChannels: -1,
+ totalLocalBalance: -1,
+ totalRemoteBalance: -1,
+ transactions: [],
+ payments: [],
+ invoices: [],
+ forwardingHistory: {},
+ addressTypes: [
+ { addressId: '0', addressTp: 'p2wkh', addressDetails: 'Pay to witness key hash'},
+ { addressId: '1', addressTp: 'np2wkh', addressDetails: 'Pay to nested witness key hash (default)'}
+ ]
+};
+
+export function RTLRootReducer(state = initialState, action: RTLActions.RTLActions) {
+ switch (action.type) {
+ case RTLActions.CLEAR_EFFECT_ERROR:
+ const clearedEffectErrors = [...state.effectErrors];
+ const removeEffectIdx = state.effectErrors.findIndex(err => {
+ return err.action === action.payload;
+ });
+ if (removeEffectIdx > -1) {
+ clearedEffectErrors.splice(removeEffectIdx, 1);
+ }
+ return {
+ ...state,
+ effectErrors: clearedEffectErrors
+ };
+ case RTLActions.EFFECT_ERROR:
+ return {
+ ...state,
+ effectErrors: [...state.effectErrors, action.payload]
+ };
+ case RTLActions.SET_SETTINGS:
+ return {
+ ...state,
+ settings: action.payload
+ };
+ case RTLActions.SET_AUTH_SETTINGS:
+ return {
+ ...state,
+ authSettings: action.payload
+ };
+ case RTLActions.SET_INFO:
+ if (undefined !== action.payload.chains) {
+ if (typeof action.payload.chains[0] === 'string') {
+ action.payload.smaller_currency_unit = (action.payload.chains[0].toString().toLowerCase().indexOf('bitcoin') < 0) ? 'Litoshis' : 'Sats';
+ action.payload.currency_unit = (action.payload.chains[0].toString().toLowerCase().indexOf('bitcoin') < 0) ? 'LTC' : 'BTC';
+ } else if (typeof action.payload.chains[0] === 'object' && action.payload.chains[0].hasOwnProperty('chain')) {
+ const getInfoChain = action.payload.chains[0];
+ action.payload.smaller_currency_unit = (getInfoChain.chain.toLowerCase().indexOf('bitcoin') < 0) ? 'Litoshis' : 'Sats';
+ action.payload.currency_unit = (getInfoChain.chain.toLowerCase().indexOf('bitcoin') < 0) ? 'LTC' : 'BTC';
+ }
+ action.payload.version = (undefined === action.payload.version) ? '' : action.payload.version.split(' ')[0];
+ } else {
+ action.payload.smaller_currency_unit = 'Sats';
+ action.payload.currency_unit = 'BTC';
+ action.payload.version = (undefined === action.payload.version) ? '' : action.payload.version.split(' ')[0];
+ }
+ return {
+ ...state,
+ information: action.payload
+ };
+ case RTLActions.SET_PEERS:
+ return {
+ ...state,
+ peers: action.payload
+ };
+ case RTLActions.ADD_PEER:
+ return {
+ ...state,
+ peers: [...state.peers, action.payload]
+ };
+ case RTLActions.REMOVE_PEER:
+ const modifiedPeers = [...state.peers];
+ const removePeerIdx = state.peers.findIndex(peer => {
+ return peer.pub_key === action.payload.pubkey;
+ });
+ if (removePeerIdx > -1) {
+ modifiedPeers.splice(removePeerIdx, 1);
+ }
+ return {
+ ...state,
+ peers: modifiedPeers
+ };
+ case RTLActions.ADD_INVOICE:
+ return {
+ ...state,
+ invoices: [action.payload, ...state.invoices]
+ };
+ case RTLActions.SET_FEES:
+ return {
+ ...state,
+ fees: action.payload
+ };
+ case RTLActions.SET_CLOSED_CHANNELS:
+ return {
+ ...state,
+ closedChannels: action.payload,
+ };
+ case RTLActions.SET_PENDING_CHANNELS:
+ let pendingChannels = -1;
+ if (action.payload) {
+ pendingChannels = 0;
+ if (action.payload.pending_closing_channels) {
+ pendingChannels = pendingChannels + action.payload.pending_closing_channels.length;
+ }
+ if (action.payload.pending_force_closing_channels) {
+ pendingChannels = pendingChannels + action.payload.pending_force_closing_channels.length;
+ }
+ if (action.payload.pending_open_channels) {
+ pendingChannels = pendingChannels + action.payload.pending_open_channels.length;
+ }
+ if (action.payload.waiting_close_channels) {
+ pendingChannels = pendingChannels + action.payload.waiting_close_channels.length;
+ }
+ }
+ return {
+ ...state,
+ pendingChannels: action.payload,
+ numberOfPendingChannels: pendingChannels,
+ };
+ case RTLActions.SET_CHANNELS:
+ let localBal = 0, remoteBal = 0, activeChannels = 0, inactiveChannels = 0;
+ if (action.payload) {
+ action.payload.filter(channel => {
+ if (undefined !== channel.local_balance) {
+ localBal = +localBal + +channel.local_balance;
+ }
+ if (undefined !== channel.remote_balance) {
+ remoteBal = +remoteBal + +channel.remote_balance;
+ }
+ if (channel.active === true) {
+ activeChannels = activeChannels + 1;
+ } else {
+ inactiveChannels = inactiveChannels + 1;
+ }
+ });
+ }
+ return {
+ ...state,
+ allChannels: action.payload,
+ numberOfActiveChannels: activeChannels,
+ numberOfInactiveChannels: inactiveChannels,
+ totalLocalBalance: localBal,
+ totalRemoteBalance: remoteBal
+ };
+ case RTLActions.REMOVE_CHANNEL:
+ const modifiedChannels = [...state.allChannels];
+ const removeChannelIdx = state.allChannels.findIndex(channel => {
+ return channel.channel_point === action.payload.channelPoint;
+ });
+ if (removeChannelIdx > -1) {
+ modifiedChannels.splice(removeChannelIdx, 1);
+ }
+ return {
+ ...state,
+ allChannels: modifiedChannels
+ };
+ case RTLActions.SET_BALANCE:
+ if (action.payload.target === 'channels') {
+ return {
+ ...state,
+ channelBalance: action.payload.balance
+ };
+ } else {
+ return {
+ ...state,
+ blockchainBalance: action.payload.balance
+ };
+ }
+ case RTLActions.SET_NETWORK:
+ return {
+ ...state,
+ networkInfo: action.payload
+ };
+ case RTLActions.SET_INVOICES:
+ return {
+ ...state,
+ invoices: action.payload
+ };
+ case RTLActions.SET_TRANSACTIONS:
+ return {
+ ...state,
+ transactions: action.payload
+ };
+ case RTLActions.SET_PAYMENTS:
+ return {
+ ...state,
+ payments: action.payload
+ };
+ case RTLActions.SET_FORWARDING_HISTORY:
+ if (action.payload.forwarding_events) {
+ const storedChannels = [...state.allChannels];
+ action.payload.forwarding_events.forEach(event => {
+ if (storedChannels) {
+ for (let idx = 0; idx < storedChannels.length; idx++) {
+ if (storedChannels[idx].chan_id.toString() === event.chan_id_in) {
+ event.alias_in = storedChannels[idx].remote_alias;
+ if (event.alias_out) { return; }
+ }
+ if (storedChannels[idx].chan_id.toString() === event.chan_id_out) {
+ event.alias_out = storedChannels[idx].remote_alias;
+ if (event.alias_in) { return; }
+ }
+ }
+ }
+ });
+ }
+ return {
+ ...state,
+ forwardingHistory: action.payload
+ };
+ default:
+ return state;
+ }
+
+}
diff --git a/src/app/shared/theme/overlay-container/theme-overlay.ts b/src/app/shared/theme/overlay-container/theme-overlay.ts
new file mode 100644
index 00000000..f264e406
--- /dev/null
+++ b/src/app/shared/theme/overlay-container/theme-overlay.ts
@@ -0,0 +1,12 @@
+import { Injectable } from '@angular/core';
+import { OverlayContainer } from '@angular/cdk/overlay';
+
+@Injectable()
+export class ThemeOverlay extends OverlayContainer {
+ _createContainer(): void {
+ const container = document.createElement('div');
+ container.classList.add('cdk-overlay-container');
+ document.getElementById('rtl-container').appendChild(container);
+ this._containerElement = container;
+ }
+}
diff --git a/src/app/shared/theme/skins/bluegray-amber.scss b/src/app/shared/theme/skins/bluegray-amber.scss
new file mode 100644
index 00000000..5f483866
--- /dev/null
+++ b/src/app/shared/theme/skins/bluegray-amber.scss
@@ -0,0 +1,50 @@
+$custom-dark-theme-background: (
+ status-bar: map_get($mat-blue-gray, 800),
+ app-bar: map_get($mat-blue-gray, 700),
+ background: map_get($mat-blue-gray, 100),
+ hover: rgba(map_get($mat-grey, 900), 0.04),
+ card: map_get($mat-blue-gray, 200),
+ dialog: map_get($mat-blue-gray, 200),
+ disabled-button: rgba(map_get($mat-grey, 900), 0.12),
+ raised-button: map-get($mat-blue-gray, 600),
+ focused-button: $light-focused,
+ selected-button: map_get($mat-grey, 800),
+ selected-disabled-button: map_get($mat-grey, 600),
+ disabled-button-toggle: map_get($mat-grey, 900),
+ unselected-chip: map_get($mat-grey, 700),
+ disabled-list-option: map_get($mat-grey, 900)
+);
+
+$custom-dark-theme-foreground: (
+ base: black,
+ divider: $dark-dividers,
+ dividers: $dark-dividers,
+ disabled: $dark-disabled-text,
+ disabled-button: rgba(black, 0.26),
+ disabled-text: $dark-disabled-text,
+ hint-text: $dark-disabled-text,
+ secondary-text: $dark-secondary-text,
+ icon: rgba(black, 0.54),
+ icons: rgba(black, 0.54),
+ text: rgba(black, 0.87),
+ slider-min: rgba(black, 0.87),
+ slider-off: rgba(black, 0.26),
+ slider-off-active: rgba(black, 0.38),
+);
+
+@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) {
+ @return (
+ primary: $primary,
+ accent: $accent,
+ warn: $warn,
+ is-dark: false,
+ foreground: $custom-dark-theme-foreground,
+ background: $custom-dark-theme-background
+ );
+}
+
+$amber-bluegray-primary: mat-palette($mat-amber, 500, 100, 700);
+$amber-bluegray-accent: mat-palette($mat-purple);
+$amber-bluegray-warn: mat-palette($mat-red, A700);
+
+$amber-bluegray-theme: create-custom-theme($amber-bluegray-primary, $amber-bluegray-accent, $amber-bluegray-warn);
diff --git a/src/app/shared/theme/skins/bluegray-deeppurple.scss b/src/app/shared/theme/skins/bluegray-deeppurple.scss
new file mode 100644
index 00000000..b9c7521f
--- /dev/null
+++ b/src/app/shared/theme/skins/bluegray-deeppurple.scss
@@ -0,0 +1,50 @@
+$custom-dark-theme-background: (
+ status-bar: map_get($mat-blue-gray, 800),
+ app-bar: map_get($mat-blue-gray, 700),
+ background: map_get($mat-blue-gray, 100),
+ hover: rgba(map_get($mat-grey, 900), 0.04),
+ card: map_get($mat-blue-gray, 200),
+ dialog: map_get($mat-blue-gray, 200),
+ disabled-button: rgba(map_get($mat-grey, 900), 0.12),
+ raised-button: map-get($mat-blue-gray, 600),
+ focused-button: $light-focused,
+ selected-button: map_get($mat-grey, 800),
+ selected-disabled-button: map_get($mat-grey, 600),
+ disabled-button-toggle: map_get($mat-grey, 900),
+ unselected-chip: map_get($mat-grey, 700),
+ disabled-list-option: map_get($mat-grey, 900)
+);
+
+$custom-dark-theme-foreground: (
+ base: black,
+ divider: $dark-dividers,
+ dividers: $dark-dividers,
+ disabled: $dark-disabled-text,
+ disabled-button: rgba(black, 0.26),
+ disabled-text: $dark-disabled-text,
+ hint-text: $dark-disabled-text,
+ secondary-text: $dark-secondary-text,
+ icon: rgba(black, 0.54),
+ icons: rgba(black, 0.54),
+ text: rgba(black, 0.87),
+ slider-min: rgba(black, 0.87),
+ slider-off: rgba(black, 0.26),
+ slider-off-active: rgba(black, 0.38),
+);
+
+@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) {
+ @return (
+ primary: $primary,
+ accent: $accent,
+ warn: $warn,
+ is-dark: false,
+ foreground: $custom-dark-theme-foreground,
+ background: $custom-dark-theme-background
+ );
+}
+
+$deeppurple-bluegray-primary: mat-palette($mat-deep-purple, 600, 100, 900);
+$deeppurple-bluegray-accent: mat-palette($mat-pink);
+$deeppurple-bluegray-warn: mat-palette($mat-red, A700);
+
+$deeppurple-bluegray-theme: create-custom-theme($deeppurple-bluegray-primary, $deeppurple-bluegray-accent, $deeppurple-bluegray-warn);
diff --git a/src/app/shared/theme/skins/bluegray-lightgreen.scss b/src/app/shared/theme/skins/bluegray-lightgreen.scss
new file mode 100644
index 00000000..5209daf2
--- /dev/null
+++ b/src/app/shared/theme/skins/bluegray-lightgreen.scss
@@ -0,0 +1,50 @@
+$custom-dark-theme-background: (
+ status-bar: map_get($mat-blue-gray, 800),
+ app-bar: map_get($mat-blue-gray, 700),
+ background: map_get($mat-blue-gray, 100),
+ hover: rgba(map_get($mat-grey, 900), 0.04),
+ card: map_get($mat-blue-gray, 200),
+ dialog: map_get($mat-blue-gray, 200),
+ disabled-button: rgba(map_get($mat-grey, 900), 0.12),
+ raised-button: map-get($mat-blue-gray, 600),
+ focused-button: $light-focused,
+ selected-button: map_get($mat-grey, 800),
+ selected-disabled-button: map_get($mat-grey, 600),
+ disabled-button-toggle: map_get($mat-grey, 900),
+ unselected-chip: map_get($mat-grey, 700),
+ disabled-list-option: map_get($mat-grey, 900)
+);
+
+$custom-dark-theme-foreground: (
+ base: black,
+ divider: $dark-dividers,
+ dividers: $dark-dividers,
+ disabled: $dark-disabled-text,
+ disabled-button: rgba(black, 0.26),
+ disabled-text: $dark-disabled-text,
+ hint-text: $dark-disabled-text,
+ secondary-text: $dark-secondary-text,
+ icon: rgba(black, 0.54),
+ icons: rgba(black, 0.54),
+ text: rgba(black, 0.87),
+ slider-min: rgba(black, 0.87),
+ slider-off: rgba(black, 0.26),
+ slider-off-active: rgba(black, 0.38),
+);
+
+@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) {
+ @return (
+ primary: $primary,
+ accent: $accent,
+ warn: $warn,
+ is-dark: false,
+ foreground: $custom-dark-theme-foreground,
+ background: $custom-dark-theme-background
+ );
+}
+
+$lightgreen-bluegray-primary: mat-palette($mat-light-green, 700, 100, 900);
+$lightgreen-bluegray-accent: mat-palette($mat-deep-orange);
+$lightgreen-bluegray-warn: mat-palette($mat-red, A700);
+
+$lightgreen-bluegray-theme: create-custom-theme($lightgreen-bluegray-primary, $lightgreen-bluegray-accent, $lightgreen-bluegray-warn);
diff --git a/src/app/shared/theme/skins/dark-blue.scss b/src/app/shared/theme/skins/dark-blue.scss
new file mode 100644
index 00000000..1e7e5405
--- /dev/null
+++ b/src/app/shared/theme/skins/dark-blue.scss
@@ -0,0 +1,5 @@
+$blue-dark-primary: mat-palette($mat-light-blue, 700, 300, 900);
+$blue-dark-accent: mat-palette($mat-amber, A100, 900, A700);
+$blue-dark-warn: mat-palette($mat-red, A200);
+
+$blue-dark-theme: mat-dark-theme($blue-dark-primary, $blue-dark-accent, $blue-dark-warn);
\ No newline at end of file
diff --git a/src/app/shared/theme/skins/dark-green.scss b/src/app/shared/theme/skins/dark-green.scss
new file mode 100644
index 00000000..e6e02e57
--- /dev/null
+++ b/src/app/shared/theme/skins/dark-green.scss
@@ -0,0 +1,5 @@
+$green-dark-primary: mat-palette($mat-green, 700, 600, 900);
+$green-dark-accent: mat-palette($mat-amber, A100, 900, A700);
+$green-dark-warn: mat-palette($mat-red, A200);
+
+$green-dark-theme: mat-dark-theme($green-dark-primary, $green-dark-accent, $green-dark-warn);
\ No newline at end of file
diff --git a/src/app/shared/theme/skins/dark-pink.scss b/src/app/shared/theme/skins/dark-pink.scss
new file mode 100644
index 00000000..83e7c513
--- /dev/null
+++ b/src/app/shared/theme/skins/dark-pink.scss
@@ -0,0 +1,5 @@
+$pink-dark-primary: mat-palette($mat-pink, 700, 400, 900);
+$pink-dark-accent: mat-palette($mat-blue-grey, 700, 500, 900);
+$pink-dark-warn: mat-palette($mat-red, A200);
+
+$pink-dark-theme: mat-dark-theme($pink-dark-primary, $pink-dark-accent, $pink-dark-warn);
\ No newline at end of file
diff --git a/src/app/shared/theme/skins/gray-blue.scss b/src/app/shared/theme/skins/gray-blue.scss
new file mode 100644
index 00000000..a8c9aeb3
--- /dev/null
+++ b/src/app/shared/theme/skins/gray-blue.scss
@@ -0,0 +1,50 @@
+$custom-dark-theme-background: (
+ status-bar: map_get($mat-gray, 800),
+ app-bar: map_get($mat-gray, 700),
+ background: map_get($mat-gray, 100),
+ hover: rgba(map_get($mat-grey, 900), 0.04),
+ card: map_get($mat-gray, 200),
+ dialog: map_get($mat-gray, 200),
+ disabled-button: rgba(map_get($mat-grey, 900), 0.12),
+ raised-button: map-get($mat-gray, 600),
+ focused-button: $light-focused,
+ selected-button: map_get($mat-grey, 800),
+ selected-disabled-button: map_get($mat-grey, 600),
+ disabled-button-toggle: map_get($mat-grey, 900),
+ unselected-chip: map_get($mat-grey, 700),
+ disabled-list-option: map_get($mat-grey, 900)
+);
+
+$custom-dark-theme-foreground: (
+ base: black,
+ divider: $dark-dividers,
+ dividers: $dark-dividers,
+ disabled: $dark-disabled-text,
+ disabled-button: rgba(black, 0.26),
+ disabled-text: $dark-disabled-text,
+ hint-text: $dark-disabled-text,
+ secondary-text: $dark-secondary-text,
+ icon: rgba(black, 0.54),
+ icons: rgba(black, 0.54),
+ text: rgba(black, 0.87),
+ slider-min: rgba(black, 0.87),
+ slider-off: rgba(black, 0.26),
+ slider-off-active: rgba(black, 0.38),
+);
+
+@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) {
+ @return (
+ primary: $primary,
+ accent: $accent,
+ warn: $warn,
+ is-dark: false,
+ foreground: $custom-dark-theme-foreground,
+ background: $custom-dark-theme-background
+ );
+}
+
+$blue-gray-primary: mat-palette($mat-cyan, 500, 200, 900);
+$blue-gray-accent: mat-palette($mat-deep-orange);
+$blue-gray-warn: mat-palette($mat-red, A700);
+
+$blue-gray-theme: create-custom-theme($blue-gray-primary, $blue-gray-accent, $blue-gray-warn);
diff --git a/src/app/shared/theme/skins/gray-lime.scss b/src/app/shared/theme/skins/gray-lime.scss
new file mode 100644
index 00000000..eeba6cd4
--- /dev/null
+++ b/src/app/shared/theme/skins/gray-lime.scss
@@ -0,0 +1,50 @@
+$custom-dark-theme-background: (
+ status-bar: map_get($mat-gray, 800),
+ app-bar: map_get($mat-gray, 700),
+ background: map_get($mat-gray, 100),
+ hover: rgba(map_get($mat-grey, 900), 0.04),
+ card: map_get($mat-gray, 200),
+ dialog: map_get($mat-gray, 200),
+ disabled-button: rgba(map_get($mat-grey, 900), 0.12),
+ raised-button: map-get($mat-gray, 600),
+ focused-button: $light-focused,
+ selected-button: map_get($mat-grey, 800),
+ selected-disabled-button: map_get($mat-grey, 600),
+ disabled-button-toggle: map_get($mat-grey, 900),
+ unselected-chip: map_get($mat-grey, 700),
+ disabled-list-option: map_get($mat-grey, 900)
+);
+
+$custom-dark-theme-foreground: (
+ base: black,
+ divider: $dark-dividers,
+ dividers: $dark-dividers,
+ disabled: $dark-disabled-text,
+ disabled-button: rgba(black, 0.26),
+ disabled-text: $dark-disabled-text,
+ hint-text: $dark-disabled-text,
+ secondary-text: $dark-secondary-text,
+ icon: rgba(black, 0.54),
+ icons: rgba(black, 0.54),
+ text: rgba(black, 0.87),
+ slider-min: rgba(black, 0.87),
+ slider-off: rgba(black, 0.26),
+ slider-off-active: rgba(black, 0.38),
+);
+
+@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) {
+ @return (
+ primary: $primary,
+ accent: $accent,
+ warn: $warn,
+ is-dark: false,
+ foreground: $custom-dark-theme-foreground,
+ background: $custom-dark-theme-background
+ );
+}
+
+$lime-gray-primary: mat-palette($mat-lime, 300, 100, 700);
+$lime-gray-accent: mat-palette($mat-purple);
+$lime-gray-warn: mat-palette($mat-red, A700);
+
+$lime-gray-theme: create-custom-theme($lime-gray-primary, $lime-gray-accent, $lime-gray-warn);
diff --git a/src/app/shared/theme/skins/gray-purple.scss b/src/app/shared/theme/skins/gray-purple.scss
new file mode 100644
index 00000000..a66143d8
--- /dev/null
+++ b/src/app/shared/theme/skins/gray-purple.scss
@@ -0,0 +1,50 @@
+$custom-dark-theme-background: (
+ status-bar: map_get($mat-gray, 800),
+ app-bar: map_get($mat-gray, 700),
+ background: map_get($mat-gray, 100),
+ hover: rgba(map_get($mat-grey, 900), 0.04),
+ card: map_get($mat-gray, 200),
+ dialog: map_get($mat-gray, 200),
+ disabled-button: rgba(map_get($mat-grey, 900), 0.12),
+ raised-button: map-get($mat-gray, 600),
+ focused-button: $light-focused,
+ selected-button: map_get($mat-grey, 800),
+ selected-disabled-button: map_get($mat-grey, 600),
+ disabled-button-toggle: map_get($mat-grey, 900),
+ unselected-chip: map_get($mat-grey, 700),
+ disabled-list-option: map_get($mat-grey, 900)
+);
+
+$custom-dark-theme-foreground: (
+ base: black,
+ divider: $dark-dividers,
+ dividers: $dark-dividers,
+ disabled: $dark-disabled-text,
+ disabled-button: rgba(black, 0.26),
+ disabled-text: $dark-disabled-text,
+ hint-text: $dark-disabled-text,
+ secondary-text: $dark-secondary-text,
+ icon: rgba(black, 0.54),
+ icons: rgba(black, 0.54),
+ text: rgba(black, 0.87),
+ slider-min: rgba(black, 0.87),
+ slider-off: rgba(black, 0.26),
+ slider-off-active: rgba(black, 0.38),
+);
+
+@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) {
+ @return (
+ primary: $primary,
+ accent: $accent,
+ warn: $warn,
+ is-dark: false,
+ foreground: $custom-dark-theme-foreground,
+ background: $custom-dark-theme-background
+ );
+}
+
+$purple-gray-primary: mat-palette($mat-purple, 700, 100, 900);
+$purple-gray-accent: mat-palette($mat-amber);
+$purple-gray-warn: mat-palette($mat-red, A700);
+
+$purple-gray-theme: create-custom-theme($purple-gray-primary, $purple-gray-accent, $purple-gray-warn);
diff --git a/src/app/shared/theme/skins/light-blue.scss b/src/app/shared/theme/skins/light-blue.scss
new file mode 100644
index 00000000..3834dae3
--- /dev/null
+++ b/src/app/shared/theme/skins/light-blue.scss
@@ -0,0 +1,50 @@
+$custom-light-theme-background: (
+ status-bar: map_get($mat-grey, 300),
+ app-bar: map_get($mat-grey, 100),
+ background:map_get($mat-gray, 100),
+ hover: rgba(black, 0.04),
+ card: map_get($mat-gray, 100),
+ dialog: map_get($mat-gray, 100),
+ disabled-button: rgba(black, 0.12),
+ raised-button: map_get($mat-grey, 50),
+ focused-button: $dark-focused,
+ selected-button: map_get($mat-grey, 300),
+ selected-disabled-button: map_get($mat-grey, 400),
+ disabled-button-toggle: map_get($mat-grey, 200),
+ unselected-chip: map_get($mat-grey, 300),
+ disabled-list-option: map_get($mat-grey, 200),
+);
+
+$custom-light-theme-foreground: (
+ base: black,
+ divider: $dark-dividers,
+ dividers: $dark-dividers,
+ disabled: $dark-disabled-text,
+ disabled-button: rgba(black, 0.26),
+ disabled-text: $dark-disabled-text,
+ hint-text: $dark-disabled-text,
+ secondary-text: $dark-secondary-text,
+ icon: rgba(black, 0.54),
+ icons: rgba(black, 0.54),
+ text: rgba(black, 0.87),
+ slider-min: rgba(black, 0.87),
+ slider-off: rgba(black, 0.26),
+ slider-off-active: rgba(black, 0.38),
+);
+
+@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) {
+ @return (
+ primary: $primary,
+ accent: $accent,
+ warn: $warn,
+ is-dark: false,
+ foreground: $custom-light-theme-foreground,
+ background: $custom-light-theme-background
+ );
+}
+
+$blue-light-primary: mat-palette($mat-indigo, 800, 300, 900);
+$blue-light-accent: mat-palette($mat-pink, A200, A100, A400);
+$blue-light-warn: mat-palette($mat-red, 500);
+
+$blue-light-theme: create-custom-theme($blue-light-primary, $blue-light-accent, $blue-light-warn);
diff --git a/src/app/shared/theme/skins/light-red.scss b/src/app/shared/theme/skins/light-red.scss
new file mode 100644
index 00000000..ee2a47e1
--- /dev/null
+++ b/src/app/shared/theme/skins/light-red.scss
@@ -0,0 +1,50 @@
+$custom-light-theme-background: (
+ status-bar: map_get($mat-grey, 300),
+ app-bar: map_get($mat-grey, 100),
+ background:map_get($mat-gray, 100),
+ hover: rgba(black, 0.04),
+ card: map_get($mat-gray, 100),
+ dialog: map_get($mat-gray, 100),
+ disabled-button: rgba(black, 0.12),
+ raised-button: map_get($mat-grey, 50),
+ focused-button: $dark-focused,
+ selected-button: map_get($mat-grey, 300),
+ selected-disabled-button: map_get($mat-grey, 400),
+ disabled-button-toggle: map_get($mat-grey, 200),
+ unselected-chip: map_get($mat-grey, 300),
+ disabled-list-option: map_get($mat-grey, 200),
+);
+
+$custom-light-theme-foreground: (
+ base: black,
+ divider: $dark-dividers,
+ dividers: $dark-dividers,
+ disabled: $dark-disabled-text,
+ disabled-button: rgba(black, 0.26),
+ disabled-text: $dark-disabled-text,
+ hint-text: $dark-disabled-text,
+ secondary-text: $dark-secondary-text,
+ icon: rgba(black, 0.54),
+ icons: rgba(black, 0.54),
+ text: rgba(black, 0.87),
+ slider-min: rgba(black, 0.87),
+ slider-off: rgba(black, 0.26),
+ slider-off-active: rgba(black, 0.38),
+);
+
+@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) {
+ @return (
+ primary: $primary,
+ accent: $accent,
+ warn: $warn,
+ is-dark: false,
+ foreground: $custom-light-theme-foreground,
+ background: $custom-light-theme-background
+ );
+}
+
+$red-light-primary: mat-palette($mat-red, 800, 300, 900);
+$red-light-accent: mat-palette($mat-green, 600, 300, 900);
+$red-light-warn: mat-palette($mat-deep-orange, A400);
+
+$red-light-theme: create-custom-theme($red-light-primary, $red-light-accent, $red-light-warn);
\ No newline at end of file
diff --git a/src/app/shared/theme/skins/light-teal.scss b/src/app/shared/theme/skins/light-teal.scss
new file mode 100644
index 00000000..30c9a6a6
--- /dev/null
+++ b/src/app/shared/theme/skins/light-teal.scss
@@ -0,0 +1,50 @@
+$custom-light-theme-background: (
+ status-bar: map_get($mat-grey, 300),
+ app-bar: map_get($mat-grey, 100),
+ background:map_get($mat-gray, 100),
+ hover: rgba(black, 0.04),
+ card: map_get($mat-gray, 100),
+ dialog: map_get($mat-gray, 100),
+ disabled-button: rgba(black, 0.12),
+ raised-button: map_get($mat-grey, 50),
+ focused-button: $dark-focused,
+ selected-button: map_get($mat-grey, 300),
+ selected-disabled-button: map_get($mat-grey, 400),
+ disabled-button-toggle: map_get($mat-grey, 200),
+ unselected-chip: map_get($mat-grey, 300),
+ disabled-list-option: map_get($mat-grey, 200),
+);
+
+$custom-light-theme-foreground: (
+ base: black,
+ divider: $dark-dividers,
+ dividers: $dark-dividers,
+ disabled: $dark-disabled-text,
+ disabled-button: rgba(black, 0.26),
+ disabled-text: $dark-disabled-text,
+ hint-text: $dark-disabled-text,
+ secondary-text: $dark-secondary-text,
+ icon: rgba(black, 0.54),
+ icons: rgba(black, 0.54),
+ text: rgba(black, 0.87),
+ slider-min: rgba(black, 0.87),
+ slider-off: rgba(black, 0.26),
+ slider-off-active: rgba(black, 0.38),
+);
+
+@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) {
+ @return (
+ primary: $primary,
+ accent: $accent,
+ warn: $warn,
+ is-dark: false,
+ foreground: $custom-light-theme-foreground,
+ background: $custom-light-theme-background
+ );
+}
+
+$teal-light-primary: mat-palette($mat-teal, 800, 300, 900);
+$teal-light-accent: mat-palette($mat-amber, A200, A100, A700);
+$teal-light-warn: mat-palette($mat-red, A200);
+
+$teal-light-theme: create-custom-theme($teal-light-primary, $teal-light-accent, $teal-light-warn);
\ No newline at end of file
diff --git a/src/app/shared/theme/skins/self-blue.scss b/src/app/shared/theme/skins/self-blue.scss
new file mode 100644
index 00000000..4d236ad7
--- /dev/null
+++ b/src/app/shared/theme/skins/self-blue.scss
@@ -0,0 +1,83 @@
+$custom-blue: (
+ 50 : #f3f8fc,
+ 100 : #e2eef8,
+ 200 : #cfe2f4,
+ 300 : #bcd6ef,
+ 400 : #adceeb,
+ 500 : #9fc5e8,
+ 600 : #97bfe5,
+ 700 : #8db8e2,
+ 800 : #83b0de,
+ 900 : #72a3d8,
+ A100 : #ffffff,
+ A200 : #ffffff,
+ A400 : #eef6ff,
+ A700 : #d4e9ff,
+ contrast: (
+ 50 : #ECEFF1,
+ 100 : #CFD8DC,
+ 200 : #37474F,
+ 300 : #37474F,
+ 400 : #37474F,
+ 500 : #607D8B,
+ 600 : #546E7A,
+ 700 : #455A64,
+ 800 : #37474F,
+ 900 : #263238,
+ A100 : #000000,
+ A200 : #000000,
+ A400 : #000000,
+ A700 : #000000,
+ )
+);
+
+$custom-light-theme-background: (
+ status-bar: map_get($mat-blue-gray, 300),
+ app-bar: map_get($mat-blue-gray, 300),
+ background: map_get($custom-blue, 50),
+ hover: rgba(black, 0.04),
+ card: map_get($custom-blue, 100),
+ dialog: map_get($custom-blue, 100),
+ disabled-button: rgba(black, 0.12),
+ raised-button: map_get($mat-blue-gray, 100),
+ focused-button: $dark-focused,
+ selected-button: map_get($mat-blue-gray, 300),
+ selected-disabled-button: map_get($mat-blue-gray, 400),
+ disabled-button-toggle: map_get($mat-blue-gray, 200),
+ unselected-chip: map_get($mat-blue-gray, 300),
+ disabled-list-option: map_get($mat-blue-gray, 200),
+);
+
+$custom-light-theme-foreground: (
+ base: map_get($mat-blue-gray, 900),
+ divider: $dark-dividers,
+ dividers: $dark-dividers,
+ disabled: $dark-disabled-text,
+ disabled-button: rgba(map_get($mat-blue-gray, 900), 0.26),
+ disabled-text: $dark-disabled-text,
+ hint-text: $dark-disabled-text,
+ secondary-text: $dark-secondary-text,
+ icon: rgba(map_get($mat-blue-gray, 900), 0.54),
+ icons: rgba(map_get($mat-blue-gray, 900), 0.54),
+ text: rgba(map_get($mat-blue-gray, 900), 0.87),
+ slider-min: rgba(map_get($mat-blue-gray, 900), 0.87),
+ slider-off: rgba(map_get($mat-blue-gray, 900), 0.26),
+ slider-off-active: rgba(map_get($mat-blue-gray, 900), 0.38),
+);
+
+@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) {
+ @return (
+ primary: $primary,
+ accent: $accent,
+ warn: $warn,
+ is-dark: false,
+ foreground: $custom-light-theme-foreground,
+ background: $custom-light-theme-background
+ );
+}
+
+$blue-primary: mat-palette($custom-blue, 200, 50, 800);
+$blue-accent: mat-palette($mat-brown, 600, 300, 900);
+$blue-warn: mat-palette($mat-red, 500);
+
+$blue-theme: create-custom-theme($blue-primary, $blue-accent, $blue-warn);
\ No newline at end of file
diff --git a/src/app/shared/theme/skins/self-brown.scss b/src/app/shared/theme/skins/self-brown.scss
new file mode 100644
index 00000000..45718a78
--- /dev/null
+++ b/src/app/shared/theme/skins/self-brown.scss
@@ -0,0 +1,83 @@
+$custom-brown: (
+ 50 : #fcf7f3,
+ 100 : #f8ece2,
+ 200 : #f4dfcf,
+ 300 : #efd2bc,
+ 400 : #ebc9ad,
+ 500 : #e8bf9f,
+ 600 : #e5b997,
+ 700 : #e2b18d,
+ 800 : #dea983,
+ 900 : #d89b72,
+ A100 : #ffffff,
+ A200 : #ffffff,
+ A400 : #fff5ee,
+ A700 : #ffe5d4,
+ contrast: (
+ 50 : #ECEFF1,
+ 100 : #CFD8DC,
+ 200 : #37474F,
+ 300 : #37474F,
+ 400 : #37474F,
+ 500 : #607D8B,
+ 600 : #546E7A,
+ 700 : #455A64,
+ 800 : #37474F,
+ 900 : #263238,
+ A100 : #000000,
+ A200 : #000000,
+ A400 : #000000,
+ A700 : #000000,
+ )
+);
+
+$custom-light-theme-background: (
+ status-bar: map_get($mat-blue-gray, 300),
+ app-bar: map_get($mat-blue-gray, 300),
+ background: map_get($custom-brown, 50),
+ hover: rgba(black, 0.04),
+ card: map_get($custom-brown, 100),
+ dialog: map_get($custom-brown, 100),
+ disabled-button: rgba(black, 0.12),
+ raised-button: map_get($mat-blue-gray, 100),
+ focused-button: $dark-focused,
+ selected-button: map_get($mat-blue-gray, 300),
+ selected-disabled-button: map_get($mat-blue-gray, 400),
+ disabled-button-toggle: map_get($mat-blue-gray, 200),
+ unselected-chip: map_get($mat-blue-gray, 300),
+ disabled-list-option: map_get($mat-blue-gray, 200),
+);
+
+$custom-light-theme-foreground: (
+ base: map_get($mat-blue-gray, 900),
+ divider: $dark-dividers,
+ dividers: $dark-dividers,
+ disabled: $dark-disabled-text,
+ disabled-button: rgba(map_get($mat-blue-gray, 900), 0.26),
+ disabled-text: $dark-disabled-text,
+ hint-text: $dark-disabled-text,
+ secondary-text: $dark-secondary-text,
+ icon: rgba(map_get($mat-blue-gray, 900), 0.54),
+ icons: rgba(map_get($mat-blue-gray, 900), 0.54),
+ text: rgba(map_get($mat-blue-gray, 900), 0.87),
+ slider-min: rgba(map_get($mat-blue-gray, 900), 0.87),
+ slider-off: rgba(map_get($mat-blue-gray, 900), 0.26),
+ slider-off-active: rgba(map_get($mat-blue-gray, 900), 0.38),
+);
+
+@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) {
+ @return (
+ primary: $primary,
+ accent: $accent,
+ warn: $warn,
+ is-dark: false,
+ foreground: $custom-light-theme-foreground,
+ background: $custom-light-theme-background
+ );
+}
+
+$brown-primary: mat-palette($custom-brown, 200, 50, 800);
+$brown-accent: mat-palette($mat-brown, 600, 300, 900);
+$brown-warn: mat-palette($mat-red, 500);
+
+$brown-theme: create-custom-theme($brown-primary, $brown-accent, $brown-warn);
\ No newline at end of file
diff --git a/src/app/shared/theme/skins/self-gray.scss b/src/app/shared/theme/skins/self-gray.scss
new file mode 100644
index 00000000..723ed183
--- /dev/null
+++ b/src/app/shared/theme/skins/self-gray.scss
@@ -0,0 +1,50 @@
+$custom-dark-theme-background: (
+ status-bar: map_get($mat-gray, 900),
+ app-bar: map_get($mat-gray, 900),
+ background: map_get($mat-gray, 50),
+ hover: rgba(map_get($mat-grey, 900), 0.04),
+ card: map_get($mat-gray, 100),
+ dialog: map_get($mat-gray, 100),
+ disabled-button: rgba(map_get($mat-grey, 900), 0.12),
+ raised-button: map-get($mat-gray, 600),
+ focused-button: $light-focused,
+ selected-button: map_get($mat-grey, 800),
+ selected-disabled-button: map_get($mat-grey, 600),
+ disabled-button-toggle: map_get($mat-grey, 900),
+ unselected-chip: map_get($mat-grey, 700),
+ disabled-list-option: map_get($mat-grey, 900)
+);
+
+$custom-dark-theme-foreground: (
+ base: black,
+ divider: $dark-dividers,
+ dividers: $dark-dividers,
+ disabled: $dark-disabled-text,
+ disabled-button: rgba(black, 0.26),
+ disabled-text: $dark-disabled-text,
+ hint-text: $dark-disabled-text,
+ secondary-text: $dark-secondary-text,
+ icon: rgba(black, 0.54),
+ icons: rgba(black, 0.54),
+ text: rgba(black, 0.87),
+ slider-min: rgba(black, 0.87),
+ slider-off: rgba(black, 0.26),
+ slider-off-active: rgba(black, 0.38),
+);
+
+@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) {
+ @return (
+ primary: $primary,
+ accent: $accent,
+ warn: $warn,
+ is-dark: false,
+ foreground: $custom-dark-theme-foreground,
+ background: $custom-dark-theme-background
+ );
+}
+
+$gray-primary: mat-palette($mat-gray, 500, 200, 900);
+$gray-accent: mat-palette($mat-indigo);
+$gray-warn: mat-palette($mat-red, 900);
+
+$gray-theme: create-custom-theme($gray-primary, $gray-accent, $gray-warn);
\ No newline at end of file
diff --git a/src/app/shared/theme/skins/self-green.scss b/src/app/shared/theme/skins/self-green.scss
new file mode 100644
index 00000000..44c1e29d
--- /dev/null
+++ b/src/app/shared/theme/skins/self-green.scss
@@ -0,0 +1,83 @@
+$custom-green: (
+ 50 : #f6faf5,
+ 100 : #e9f3e5,
+ 200 : #dbebd4,
+ 300 : #cce3c2,
+ 400 : #c1ddb5,
+ 500 : #b6d7a8,
+ 600 : #afd3a0,
+ 700 : #a6cd97,
+ 800 : #9ec78d,
+ 900 : #8ebe7d,
+ A100 : #ffffff,
+ A200 : #ffffff,
+ A400 : #e7ffde,
+ A700 : #d4ffc4,
+ contrast: (
+ 50 : #ECEFF1,
+ 100 : #CFD8DC,
+ 200 : #37474F,
+ 300 : #37474F,
+ 400 : #37474F,
+ 500 : #607D8B,
+ 600 : #546E7A,
+ 700 : #455A64,
+ 800 : #37474F,
+ 900 : #263238,
+ A100 : #000000,
+ A200 : #000000,
+ A400 : #000000,
+ A700 : #000000,
+ )
+);
+
+$custom-light-theme-background: (
+ status-bar: map_get($mat-blue-gray, 300),
+ app-bar: map_get($mat-blue-gray, 300),
+ background: map_get($custom-green, 50),
+ hover: rgba(black, 0.04),
+ card: map_get($custom-green, 100),
+ dialog: map_get($custom-green, 100),
+ disabled-button: rgba(black, 0.12),
+ raised-button: map_get($mat-blue-gray, 100),
+ focused-button: $dark-focused,
+ selected-button: map_get($mat-blue-gray, 300),
+ selected-disabled-button: map_get($mat-blue-gray, 400),
+ disabled-button-toggle: map_get($mat-blue-gray, 200),
+ unselected-chip: map_get($mat-blue-gray, 300),
+ disabled-list-option: map_get($mat-blue-gray, 200),
+);
+
+$custom-light-theme-foreground: (
+ base: map_get($mat-blue-gray, 900),
+ divider: $dark-dividers,
+ dividers: $dark-dividers,
+ disabled: $dark-disabled-text,
+ disabled-button: rgba(map_get($mat-blue-gray, 900), 0.26),
+ disabled-text: $dark-disabled-text,
+ hint-text: $dark-disabled-text,
+ secondary-text: $dark-secondary-text,
+ icon: rgba(map_get($mat-blue-gray, 900), 0.54),
+ icons: rgba(map_get($mat-blue-gray, 900), 0.54),
+ text: rgba(map_get($mat-blue-gray, 900), 0.87),
+ slider-min: rgba(map_get($mat-blue-gray, 900), 0.87),
+ slider-off: rgba(map_get($mat-blue-gray, 900), 0.26),
+ slider-off-active: rgba(map_get($mat-blue-gray, 900), 0.38),
+);
+
+@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) {
+ @return (
+ primary: $primary,
+ accent: $accent,
+ warn: $warn,
+ is-dark: false,
+ foreground: $custom-light-theme-foreground,
+ background: $custom-light-theme-background
+ );
+}
+
+$green-primary: mat-palette($custom-green, 200, 50, 800);
+$green-accent: mat-palette($mat-brown, 600, 300, 900);
+$green-warn: mat-palette($mat-red, 500);
+
+$green-theme: create-custom-theme($green-primary, $green-accent, $green-warn);
\ No newline at end of file
diff --git a/src/app/shared/theme/skins/self-pink.scss b/src/app/shared/theme/skins/self-pink.scss
new file mode 100644
index 00000000..b213deef
--- /dev/null
+++ b/src/app/shared/theme/skins/self-pink.scss
@@ -0,0 +1,83 @@
+$custom-pink: (
+ 50 : #fcf4f5,
+ 100 : #f8e3e6,
+ 200 : #f4d1d6,
+ 300 : #efbec5,
+ 400 : #ebb0b8,
+ 500 : #e8a2ac,
+ 600 : #e59aa5,
+ 700 : #e2909b,
+ 800 : #de8692,
+ 900 : #d87582,
+ A100 : #ffffff,
+ A200 : #ffffff,
+ A400 : #fff1f2,
+ A700 : #ffd7dc,
+ contrast: (
+ 50 : #ECEFF1,
+ 100 : #CFD8DC,
+ 200 : #37474F,
+ 300 : #90A4AE,
+ 400 : #78909C,
+ 500 : #607D8B,
+ 600 : #546E7A,
+ 700 : #455A64,
+ 800 : #37474F,
+ 900 : #263238,
+ A100 : #000000,
+ A200 : #000000,
+ A400 : #000000,
+ A700 : #000000,
+ )
+);
+
+$custom-light-theme-background: (
+ status-bar: map_get($mat-blue-gray, 300),
+ app-bar: map_get($mat-blue-gray, 300),
+ background: map_get($custom-pink, 50),
+ hover: rgba(black, 0.04),
+ card: map_get($custom-pink, 100),
+ dialog: map_get($custom-pink, 100),
+ disabled-button: rgba(black, 0.12),
+ raised-button: map_get($mat-blue-gray, 100),
+ focused-button: $dark-focused,
+ selected-button: map_get($mat-blue-gray, 300),
+ selected-disabled-button: map_get($mat-blue-gray, 400),
+ disabled-button-toggle: map_get($mat-blue-gray, 200),
+ unselected-chip: map_get($mat-blue-gray, 300),
+ disabled-list-option: map_get($mat-blue-gray, 200),
+);
+
+$custom-light-theme-foreground: (
+ base: map_get($mat-blue-gray, 900),
+ divider: $dark-dividers,
+ dividers: $dark-dividers,
+ disabled: $dark-disabled-text,
+ disabled-button: rgba(map_get($mat-blue-gray, 900), 0.26),
+ disabled-text: $dark-disabled-text,
+ hint-text: $dark-disabled-text,
+ secondary-text: $dark-secondary-text,
+ icon: rgba(map_get($mat-blue-gray, 900), 0.54),
+ icons: rgba(map_get($mat-blue-gray, 900), 0.54),
+ text: rgba(map_get($mat-blue-gray, 900), 0.87),
+ slider-min: rgba(map_get($mat-blue-gray, 900), 0.87),
+ slider-off: rgba(map_get($mat-blue-gray, 900), 0.26),
+ slider-off-active: rgba(map_get($mat-blue-gray, 900), 0.38),
+);
+
+@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) {
+ @return (
+ primary: $primary,
+ accent: $accent,
+ warn: $warn,
+ is-dark: false,
+ foreground: $custom-light-theme-foreground,
+ background: $custom-light-theme-background
+ );
+}
+
+$pink-primary: mat-palette($custom-pink, 200, 50, 800);
+$pink-accent: mat-palette($mat-blue-gray, 600, 300, 900);
+$pink-warn: mat-palette($mat-red, 700);
+
+$pink-theme: create-custom-theme($pink-primary, $pink-accent, $pink-warn);
\ No newline at end of file
diff --git a/src/app/shared/theme/skins/self-yellow.scss b/src/app/shared/theme/skins/self-yellow.scss
new file mode 100644
index 00000000..342f70b2
--- /dev/null
+++ b/src/app/shared/theme/skins/self-yellow.scss
@@ -0,0 +1,83 @@
+$custom-yellow: (
+ 50 : #fffcf3,
+ 100 : #fff7e0,
+ 200 : #fff2cc,
+ 300 : #ffedb8,
+ 400 : #ffe9a8,
+ 500 : #ffe599,
+ 600 : #ffe291,
+ 700 : #ffde86,
+ 800 : #ffda7c,
+ 900 : #ffd36b,
+ A100 : #ffffff,
+ A200 : #ffffff,
+ A400 : #ffffff,
+ A700 : #fffbf3,
+ contrast: (
+ 50 : #ECEFF1,
+ 100 : #CFD8DC,
+ 200 : #37474F,
+ 300 : #37474F,
+ 400 : #37474F,
+ 500 : #607D8B,
+ 600 : #546E7A,
+ 700 : #455A64,
+ 800 : #37474F,
+ 900 : #263238,
+ A100 : #000000,
+ A200 : #000000,
+ A400 : #000000,
+ A700 : #000000,
+ )
+);
+
+$custom-light-theme-background: (
+ status-bar: map_get($mat-blue-gray, 300),
+ app-bar: map_get($mat-blue-gray, 300),
+ background: map_get($custom-yellow, 50),
+ hover: rgba(black, 0.04),
+ card: map_get($custom-yellow, 100),
+ dialog: map_get($custom-yellow, 100),
+ disabled-button: rgba(black, 0.12),
+ raised-button: map_get($mat-blue-gray, 100),
+ focused-button: $dark-focused,
+ selected-button: map_get($mat-blue-gray, 300),
+ selected-disabled-button: map_get($mat-blue-gray, 400),
+ disabled-button-toggle: map_get($mat-blue-gray, 200),
+ unselected-chip: map_get($mat-blue-gray, 300),
+ disabled-list-option: map_get($mat-blue-gray, 200),
+);
+
+$custom-light-theme-foreground: (
+ base: map_get($mat-blue-gray, 900),
+ divider: $dark-dividers,
+ dividers: $dark-dividers,
+ disabled: $dark-disabled-text,
+ disabled-button: rgba(map_get($mat-blue-gray, 900), 0.26),
+ disabled-text: $dark-disabled-text,
+ hint-text: $dark-disabled-text,
+ secondary-text: $dark-secondary-text,
+ icon: rgba(map_get($mat-blue-gray, 900), 0.54),
+ icons: rgba(map_get($mat-blue-gray, 900), 0.54),
+ text: rgba(map_get($mat-blue-gray, 900), 0.87),
+ slider-min: rgba(map_get($mat-blue-gray, 900), 0.87),
+ slider-off: rgba(map_get($mat-blue-gray, 900), 0.26),
+ slider-off-active: rgba(map_get($mat-blue-gray, 900), 0.38),
+);
+
+@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) {
+ @return (
+ primary: $primary,
+ accent: $accent,
+ warn: $warn,
+ is-dark: false,
+ foreground: $custom-light-theme-foreground,
+ background: $custom-light-theme-background
+ );
+}
+
+$yellow-primary: mat-palette($custom-yellow, 200, 50, 800);
+$yellow-accent: mat-palette($mat-brown, 600, 300, 900);
+$yellow-warn: mat-palette($mat-red, 500);
+
+$yellow-theme: create-custom-theme($yellow-primary, $yellow-accent, $yellow-warn);
\ No newline at end of file
diff --git a/src/app/shared/theme/styles/change-theme.scss b/src/app/shared/theme/styles/change-theme.scss
new file mode 100644
index 00000000..7cb23e85
--- /dev/null
+++ b/src/app/shared/theme/styles/change-theme.scss
@@ -0,0 +1,63 @@
+@import "constants";
+
+@mixin change-theme($theme) {
+ $primary: map-get($theme, primary);
+ $accent: map-get($theme, accent);
+ $warn: map-get($theme, warn);
+ $foreground: map-get($theme, foreground);
+ $background: map-get($theme, background);
+ $testColor: mat-palette($mat-red, A200);
+
+ .bg-primary {
+ @include _mat-toolbar-color($primary);
+ }
+
+ .bg-accent {
+ @include _mat-toolbar-color($accent);
+ }
+
+ .bg-warn {
+ @include _mat-toolbar-color($warn);
+ }
+
+ .bg-test {
+ @include _mat-toolbar-color($testColor);
+ }
+
+ .foreground.mat-progress-spinner circle, .foreground.mat-spinner circle {
+ stroke: mat-color($foreground, text);
+ }
+
+ .mat-toolbar-row,
+ .mat-toolbar-single-row {
+ height: $toolbar-height;
+ }
+
+ .lnd-info{
+ border-bottom: 1px solid mat-color($foreground, divider);
+ }
+ .horizontal-nav {
+ position: fixed;
+ top:74px;
+ z-index: 9999;
+ }
+ a {
+ color: mat-color($foreground, text);
+ }
+
+ .active-link {
+ background: mat-color($primary);
+ }
+
+ .h-active-link {
+ background: mat-color($primary, lighter) !important;
+ }
+
+ .ngx-charts {
+ fill: mat-color($foreground, text);
+ .bar {
+ fill: mat-color($primary) !important;
+ cursor: default;
+ }
+ }
+}
diff --git a/src/app/shared/theme/styles/constants.scss b/src/app/shared/theme/styles/constants.scss
new file mode 100644
index 00000000..1f69edae
--- /dev/null
+++ b/src/app/shared/theme/styles/constants.scss
@@ -0,0 +1,11 @@
+$font-family: 'Roboto', sans-serif;
+$font-size: 14px;
+$toolbar-height: 46px;
+$regular-sidenav-width: 250px;
+$compact-sidenav-width: 185px;
+$mini-sidenav-width: 100px;
+$sidenav-info-height: 100px;
+$settings-nav-width: 150px;
+$gap: 8px;
+$icon-size: 36px;
+$pubkey-info-height: 15px;
diff --git a/src/app/shared/theme/styles/perfect-scrollbar.scss b/src/app/shared/theme/styles/perfect-scrollbar.scss
new file mode 100644
index 00000000..468b4e96
--- /dev/null
+++ b/src/app/shared/theme/styles/perfect-scrollbar.scss
@@ -0,0 +1,100 @@
+.ps {
+ overflow: hidden !important;
+ overflow-anchor: none;
+ -ms-overflow-style: none;
+ touch-action: auto;
+ -ms-touch-action: auto;
+ }
+
+ .ps__rail-x {
+ display: none;
+ opacity: 0;
+ transition: background-color .2s linear, opacity .2s linear;
+ -webkit-transition: background-color .2s linear, opacity .2s linear;
+ height: 15px;
+ bottom: 0px;
+ margin-top: -15px;
+ position: relative;
+ }
+
+ .ps__rail-y {
+ display: none;
+ opacity: 0;
+ transition: background-color .2s linear, opacity .2s linear;
+ -webkit-transition: background-color .2s linear, opacity .2s linear;
+ width: 0px;
+ right: 0;
+ position: relative;
+ }
+
+ .ps--active-x > .ps__rail-x,
+ .ps--active-y > .ps__rail-y {
+ display: block;
+ background-color: transparent;
+ }
+
+ .ps:hover > .ps__rail-x,
+ .ps:hover > .ps__rail-y,
+ .ps--focus > .ps__rail-x,
+ .ps--focus > .ps__rail-y,
+ .ps--scrolling-x > .ps__rail-x,
+ .ps--scrolling-y > .ps__rail-y {
+ opacity: 0.6;
+ }
+
+ .ps .ps__rail-x:hover,
+ .ps .ps__rail-y:hover,
+ .ps .ps__rail-x:focus,
+ .ps .ps__rail-y:focus,
+ .ps .ps__rail-x.ps--clicking,
+ .ps .ps__rail-y.ps--clicking {
+ background-color: transparent;
+ opacity: 0.9;
+ }
+
+ .ps__thumb-x {
+ background-color: #aaa;
+ border-radius: 6px;
+ transition: background-color .2s linear, height .2s ease-in-out;
+ -webkit-transition: background-color .2s linear, height .2s ease-in-out;
+ height: 4px;
+ bottom: 2px;
+ position: absolute;
+ }
+
+ .ps__thumb-y {
+ background-color: #aaa;
+ border-radius: 6px;
+ transition: background-color .2s linear, width .2s ease-in-out;
+ -webkit-transition: background-color .2s linear, width .2s ease-in-out;
+ width: 4px;
+ right: 2px;
+ position: absolute;
+ }
+
+ .ps__rail-x:hover > .ps__thumb-x,
+ .ps__rail-x:focus > .ps__thumb-x,
+ .ps__rail-x.ps--clicking .ps__thumb-x {
+ background-color: #999;
+ height: 6px;
+ }
+
+ .ps__rail-y:hover > .ps__thumb-y,
+ .ps__rail-y:focus > .ps__thumb-y,
+ .ps__rail-y.ps--clicking .ps__thumb-y {
+ background-color: #999;
+ width: 6px;
+ }
+
+ @supports (-ms-overflow-style: none) {
+ .ps {
+ overflow: auto !important;
+ }
+ }
+
+ @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
+ .ps {
+ overflow: auto !important;
+ }
+ }
+
\ No newline at end of file
diff --git a/src/app/shared/theme/styles/root.scss b/src/app/shared/theme/styles/root.scss
new file mode 100644
index 00000000..9a89a5f6
--- /dev/null
+++ b/src/app/shared/theme/styles/root.scss
@@ -0,0 +1,782 @@
+@import "constants";
+
+html, body {
+ width: 100%;
+ height: 99%;
+ font-family: $font-family !important;
+ font-size: $font-size !important;
+ line-height: 1.5;
+ overflow-x: hidden;
+}
+
+.rtl-container{
+ position:absolute;
+ width: 100%;
+ height: 100%;
+ top:0;
+ left:0;
+ right:0;
+ bottom:0;
+ .mat-menu-panel .mat-menu-content {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+ .top-toolbar {
+ left: $regular-sidenav-width;
+ width: calc(100% - #{$regular-sidenav-width}) !important;
+ }
+ .mat-sidenav-content {
+ margin-left: $regular-sidenav-width !important;
+ }
+ &.horizontal {
+ .top-toolbar, .pubkey-info-top {
+ left: 0;
+ width: 100% !important;
+ }
+ .mat-sidenav-content {
+ top: calc(#{$toolbar-height} + #{$gap} + #{$pubkey-info-height} + #{$toolbar-height});
+ margin-left: 0 !important;
+ }
+ }
+ &.compact {
+ .sidenav{
+ width: $compact-sidenav-width;
+ }
+ .mat-tree-node {
+ height: 76px;
+ padding: 0;
+ }
+ .top-toolbar, .pubkey-info-top {
+ left: $compact-sidenav-width;
+ width: calc(100% - #{$compact-sidenav-width}) !important;
+ }
+ .mat-sidenav-content {
+ margin-left: $compact-sidenav-width !important;
+ }
+ }
+ &.mini {
+ .sidenav {
+ width: $mini-sidenav-width;
+ }
+ .mat-tree-node {
+ padding: 0 8px 12px 8px;
+ .mat-icon {
+ font-size: $icon-size;
+ }
+ }
+ .top-toolbar, .pubkey-info-top {
+ left: $mini-sidenav-width;
+ width: calc(100% - #{$mini-sidenav-width}) !important;
+ }
+ .mat-sidenav-content {
+ margin-left: $mini-sidenav-width !important;
+ }
+ }
+}
+
+.mat-sidenav-container .mat-sidenav-content {
+ top: calc(#{$toolbar-height} + #{$gap} + #{$pubkey-info-height});
+ height: 100vh;
+}
+
+.sidenav{
+ width: $regular-sidenav-width;
+ height: 100%;
+ overflow: hidden !important;
+ overflow-anchor: none;
+ -ms-overflow-style: none;
+ touch-action: auto;
+ -ms-touch-action: auto;
+}
+
+.font-9px {
+ font-size: 9px !important;
+}
+
+.sticky {
+ position: fixed;
+ top: 0;
+ z-index: 9999;
+}
+
+.horizontal-menu {
+ padding: 0;
+ z-index: 999;
+ position: fixed;
+ top: 0;
+ height: $toolbar-height;
+ overflow: visible;
+}
+
+.top-bar {
+ position: relative;
+ top: 0;
+ bottom:0;
+ left:0;
+ right:0;
+ height: 0;
+}
+
+.pubkey-info-top {
+ flex-wrap: wrap;
+ top: $toolbar-height;
+ margin-top: 1px;
+ min-height: $pubkey-info-height;
+ cursor: pointer;
+ display: flex;
+ left: $regular-sidenav-width;
+ width: calc(100% - #{$regular-sidenav-width}) !important;
+ align-content: center;
+}
+
+.inner-sidenav-content {
+ position: relative;
+ // top: calc(#{$toolbar-height} + #{$gap} + #{$pubkey-info-height});
+ top: 0;
+ bottom:0;
+ left:0;
+ right:0;
+ padding: 4px;
+ min-height: calc(100% - (#{$toolbar-height} + #{$gap}*4));
+ max-height: 90vh;
+}
+
+.top-50 {
+ top: 50px;
+}
+
+*{
+ margin: 0;
+ padding: 0;
+}
+
+.rtl-spinner{
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+ position: fixed;
+ background: #fff;
+ z-index: 999999;
+ visibility: visible;
+ opacity: 1;
+ h4{
+ margin-top: 10px;
+ }
+}
+
+.mat-expansion-panel-header, .mat-menu-item, .mat-list .mat-list-item, .mat-nav-list .mat-list-item, .mat-selection-list .mat-list-item {
+ font-size: $font-size !important;
+}
+
+.mat-raised-button {
+ width: 100%;
+ margin-top: 5px;
+ max-height: 36px;
+}
+
+.padding-gap {
+ padding: $gap !important;
+}
+
+.padding-gap-x {
+ padding: 0 $gap 0 $gap !important;
+}
+
+.mat-raised-button {
+ margin-top: 5px !important;
+ max-height: 36px;
+}
+
+.logo {
+ font-size: $font-size * 2;
+ font-weight: 700;
+ letter-spacing: 1px;
+}
+
+.mat-card {
+ padding: 12px 24px !important;
+ overflow: hidden;
+ border-radius: 2px !important;
+}
+
+.mat-toolbar-row, .mat-toolbar-single-row {
+ height: $toolbar-height;
+}
+
+.mat-card-actions{
+ display: block;
+ margin-bottom: 16px;
+ padding-left: 6px;
+ padding-right: 6px;
+}
+
+.mat-card-content, .mat-card-subtitle, .mat-card-title {
+ display: block;
+ margin-bottom: 16px;
+}
+
+.mat-card-header-text {
+ margin: 0 !important;
+ line-height: 1;
+}
+
+.mat-form-field-wrapper {
+ width: 100%;
+ margin: 0 15px 0 0;
+}
+
+.mat-select {
+ margin: 0 15px 0 0;
+}
+
+.green {
+ color: #388e3c !important;
+}
+
+.red {
+ color: #c62828 !important;
+}
+
+.yellow {
+ color: #ffd740 !important;
+}
+
+.mat-dialog-container {
+ padding: 0 !important;
+}
+
+.mt-1px {
+ margin-top: 1px !important;
+}
+
+.mt-1 {
+ margin-top: 0.55rem !important;
+}
+
+.mb-1 {
+ margin-bottom: 0.55rem !important;
+}
+
+.ml-1 {
+ margin-left: 0.55rem !important;
+}
+
+.mr-1 {
+ margin-right: 0.55rem !important;
+}
+
+.mx-1 {
+ margin: 0 0.55rem !important;
+}
+
+.my-1 {
+ margin: 0.55rem 0 !important;
+}
+
+.m-1 {
+ margin: 0.55rem !important;
+}
+
+.mt-2 {
+ margin-top: 1rem !important;
+}
+
+.mt-minus-1 {
+ margin-top: -0.5rem !important;
+}
+
+.mb-2 {
+ margin-bottom: 1rem !important;
+}
+
+.ml-2 {
+ margin-left: 1rem !important;
+}
+
+.mr-2 {
+ margin-right: 1rem !important;
+}
+
+.ml-4 {
+ margin-left: 2rem !important;
+}
+
+.ml-8 {
+ margin-left: 4rem !important;
+}
+
+.mr-4 {
+ margin-right: 2rem !important;
+}
+
+.mx-2 {
+ margin: 0 1rem !important;
+}
+
+.my-2 {
+ margin: 1rem 0 !important;
+}
+
+.m-2 {
+ margin: 1rem !important;
+}
+
+.pt-1 {
+ padding-top: 0.55rem !important;
+}
+
+.pb-1 {
+ padding-bottom: 0.55rem !important;
+}
+
+.pl-1 {
+ padding-left: 0.55rem !important;
+}
+
+.pr-1 {
+ padding-right: 0.55rem !important;
+}
+
+.pr-4px {
+ padding-right: 4px !important;
+}
+
+.p-0 {
+ padding: 0 !important;
+}
+
+.pl-0 {
+ padding-left: 0 !important;
+}
+
+.px-1 {
+ padding: 0 0.55rem !important;
+}
+
+.py-1 {
+ padding: 0.55rem 0 !important;
+}
+
+.p-1 {
+ padding: 0.55rem !important;
+}
+
+.pt-2 {
+ padding-top: 1rem !important;
+}
+
+.pb-2 {
+ padding-bottom: 1rem !important;
+}
+
+.pl-2 {
+ padding-left: 1rem !important;
+}
+
+.pt-4 {
+ padding-top: 2rem !important;
+}
+
+.pl-4 {
+ padding-left: 2rem !important;
+}
+
+.pr-2 {
+ padding-right: 1rem !important;
+}
+
+.pr-5 {
+ padding-right: 2.5rem !important;
+}
+
+.px-2 {
+ padding: 0 1rem !important;
+}
+
+.py-2 {
+ padding: 1rem 0 !important;
+}
+
+.p-2 {
+ padding: 1rem !important;
+}
+
+.m-1px {
+ margin: 1px !important;
+}
+
+.overflow-x-auto {
+ overflow-x: auto;
+}
+
+.overflow-y-auto {
+ overflow-y: auto;
+}
+
+.overflow-auto {
+ overflow: auto;
+}
+
+.mat-footer-row, .mat-header-row, .mat-row {
+ border-bottom-width: 0px;
+}
+
+.mat-cell, .mat-header-cell, .mat-footer-cell {
+ border-bottom-width: 1px;
+ border-bottom-style: solid;
+ border-bottom-color: rgba(0, 0, 0, 0.12);
+}
+
+.flex-ellipsis {
+ padding-right: 30px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.mat-list, .mat-list .mat-list-item .mat-list-item-content, .mat-nav-list, .mat-selection-list {
+ padding: 0 !important;
+}
+
+.inline-spinner {
+ display: inline-flex !important;
+ top: 5px !important;
+}
+
+.top-minus-5px {
+ position: relative;
+ top:-5px;
+}
+
+.top-minus-15px {
+ position: relative;
+ top:-15px;
+}
+
+.top-minus-25px {
+ position: relative;
+ top:-25px;
+ margin-bottom: -25px !important;
+}
+
+.top-minus-30px {
+ position: relative;
+ top:-30px;
+}
+
+.cursor-pointer {
+ cursor: pointer !important;
+}
+
+.cursor-default {
+ cursor: default !important;
+}
+
+.cursor-not-allowed {
+ cursor: not-allowed !important;
+}
+
+.font-60-percent {
+ font-size: 60%;
+}
+
+.inline-flex {
+ display: inline-flex !important;
+}
+
+.error-border {
+ border: 1px solid red;
+ box-shadow: 0 3px 1px -2px rgba(255,0,0,.2), 0 2px 2px 0 rgba(255,0,0,.14), 0 1px 5px 0 rgba(255,0,0,.12) !important;
+}
+
+.settings{
+ position: fixed;
+ width: $settings-nav-width;
+ .container{
+ padding: 6px 14px;
+ h4{
+ border-bottom: 1px solid #ccc;
+ margin: 12px 0 6px 0;
+ }
+ .skin{
+ width:32px;
+ height: 0px;
+ padding: 0;
+ overflow: hidden;
+ cursor: pointer;
+ border: 16px solid;
+ &.light-blue{
+ border-top-color: #3F51B5;
+ border-bottom-color: #3F51B5;
+ border-left-color: #ffffff;
+ border-right-color: #ffffff;
+ }
+ &.light-teal{
+ border-left-color: #ffffff;
+ border-right-color: #ffffff;
+ border-top-color: #009688;
+ border-bottom-color: #009688;
+ }
+ &.light-red{
+ border-left-color: #ffffff;
+ border-right-color: #ffffff;
+ border-top-color: #F44336;
+ border-bottom-color: #F44336;
+ }
+ &.dark-blue{
+ border-left-color: #262626;
+ border-right-color: #262626;
+ border-top-color: #0277bd;
+ border-bottom-color: #0277bd;
+ }
+ &.dark-green{
+ border-left-color: #262626;
+ border-right-color: #262626;
+ border-top-color: #388E3C;
+ border-bottom-color: #388E3C;
+ }
+ &.dark-pink{
+ border-left-color: #262626;
+ border-right-color: #262626;
+ border-top-color: #D81B60;
+ border-bottom-color: #D81B60;
+ }
+ &.gray-blue{
+ border-left-color: #EEEEEE;
+ border-right-color: #EEEEEE;
+ border-top-color: #00BCD4;
+ border-bottom-color: #00BCD4;
+ }
+ &.gray-lime{
+ border-left-color: #EEEEEE;
+ border-right-color: #EEEEEE;
+ border-top-color: #DCE775;
+ border-bottom-color: #DCE775;
+ }
+ &.gray-purple{
+ border-left-color: #EEEEEE;
+ border-right-color: #EEEEEE;
+ border-top-color: #512DA8;
+ border-bottom-color: #512DA8;
+ }
+ &.bluegray-amber{
+ border-left-color: #CFD8DC;
+ border-right-color: #CFD8DC;
+ border-top-color: #FFA000;
+ border-bottom-color: #FFA000;
+ }
+ &.bluegray-deeppurple{
+ border-left-color: #CFD8DC;
+ border-right-color: #CFD8DC;
+ border-top-color: #5E35B1;
+ border-bottom-color: #5E35B1;
+ }
+ &.bluegray-lightgreen{
+ border-left-color: #CFD8DC;
+ border-right-color: #CFD8DC;
+ border-top-color: #689F38;
+ border-bottom-color: #689F38;
+ }
+ &.self-gray{
+ border-left-color: #FAFAFA;
+ border-right-color: #FAFAFA;
+ border-top-color: #9e9e9e;
+ border-bottom-color: #9e9e9e;
+ }
+ &.self-green{
+ border-left-color: #dbebd4;
+ border-right-color: #dbebd4;
+ border-top-color: #9ec78d;
+ border-bottom-color: #9ec78d;
+ }
+ &.self-yellow{
+ border-left-color: #fff2cc;
+ border-right-color: #fff2cc;
+ border-top-color: #ffda7c;
+ border-bottom-color: #ffda7c;
+ }
+ &.self-blue{
+ border-left-color: #cfe2f4;
+ border-right-color: #cfe2f4;
+ border-top-color: #83b0de;
+ border-bottom-color: #83b0de;
+ }
+ &.self-brown{
+ border-left-color: #f4dfcf;
+ border-right-color: #f4dfcf;
+ border-top-color: #dea983;
+ border-bottom-color: #dea983;
+ }
+ &.self-pink{
+ border-left-color: #f4d1d6;
+ border-right-color: #f4d1d6;
+ border-top-color: #de8692;
+ border-bottom-color: #de8692;
+ }
+ }
+ }
+
+ .mat-radio-group{
+ display: inline-flex;
+ flex-direction: column;
+ .mat-radio-button{
+ margin: 2px 0;
+ }
+ }
+
+ .mat-slide-toggle{
+ padding: 0px 14px;
+ }
+
+}
+
+.op-image{
+ box-shadow: 0 0 2px #ccc;
+ border: 2px solid;
+ border-color: transparent;
+ cursor: pointer;
+ transition: 0.2s;
+}
+
+.settings-icon{
+ position: fixed;
+ top: 30%;
+ right: 0;
+ width: 42px;
+ height: 42px;
+ opacity: 0.6;
+ cursor: pointer;
+ z-index: 999999;
+}
+
+.test-banner {
+ padding-top: 2px;
+ background-color: #FC7783;
+ text-transform: uppercase;
+ border-radius: 2px;
+}
+
+.icon-large {
+ font-size: 70px;
+ margin-left: -100%;
+}
+
+.icon-small {
+ height: 20px !important;
+ width: 20px !important;
+ font-size: 20px !important;
+}
+
+.icon-smaller {
+ height: 10px !important;
+ width: 10px !important;
+ font-size: 10px !important;
+}
+
+.copy-icon {
+ position: relative;
+ top: 5px;
+}
+
+.copy-icon-smaller {
+ position: relative;
+ top: 2px;
+}
+
+.top-5px {
+ position: relative;
+ top: 5px;
+}
+
+.animate-settings {
+ -webkit-animation: animate-settings 10s linear infinite;
+ -moz-animation: animate-settings 10s linear infinite;
+ animation: animate-settings 10s linear infinite;
+}
+@keyframes animate-settings {
+ 100% {transform: rotate(360deg)}
+}
+@-moz-keyframes animate-settings {
+ 100% {-moz-transform: rotate(360deg)}
+}
+@-webkit-keyframes animate-settings {
+ 100% {-webkit-transform: rotate(360deg)}
+}
+
+.size-30 {
+ font-size: 30px;
+}
+
+.mt-minus-5 {
+ position: relative;
+ margin-top: -5px;
+}
+
+.custom-card {
+ padding: 0px 0px 8px 0px !important;
+}
+
+.not-found-box {
+ min-width: 30%;
+}
+
+.w-100 {
+ width: 100% !important;
+}
+
+.w-84 {
+ width: 84% !important;
+}
+
+.h-100 {
+ height: 100% !important;
+}
+
+a {
+ outline: none;
+ text-decoration: none;
+}
+
+.mat-tree {
+ width: 100%;
+ margin-top: 8px;
+}
+
+.mat-tree-node {
+ min-height: 42px !important;
+ padding: 0 12px 0 12px;
+ cursor: pointer;
+}
+
+.mat-tree-node:focus, .mat-tree-node:active {
+ outline: none;
+}
+
+.lnd-info {
+ height: $sidenav-info-height;
+}
+
+.horizontal-button {
+ height: $toolbar-height;
+}
+
+.mat-icon-36 {
+ width: $icon-size !important;
+ height: $icon-size !important;
+ font-size: $icon-size;
+}
+
+.flex-wrap {
+ flex-wrap: wrap !important;
+}
+
+.word-break {
+ word-break: break-all !important;
+}
+
+.qr-border {
+ border: 2px solid white;
+}
diff --git a/src/app/shared/theme/styles/styles.scss b/src/app/shared/theme/styles/styles.scss
new file mode 100644
index 00000000..abe37aeb
--- /dev/null
+++ b/src/app/shared/theme/styles/styles.scss
@@ -0,0 +1,6 @@
+@import url('https://fonts.googleapis.com/css?family=Roboto:100,100i,300,300i,400,400i,500,500i,700,700i,900,900i');
+@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
+
+@import "/root";
+@import "/perfect-scrollbar";
+@import "/theme";
diff --git a/src/app/shared/theme/styles/theme.scss b/src/app/shared/theme/styles/theme.scss
new file mode 100644
index 00000000..95217bba
--- /dev/null
+++ b/src/app/shared/theme/styles/theme.scss
@@ -0,0 +1,96 @@
+@import '~@angular/material/theming';
+@include mat-core();
+@import 'change-theme';
+
+.rtl-container{
+ &.light-blue{
+ @import "../skins/light-blue";
+ @include angular-material-theme($blue-light-theme);
+ @include change-theme($blue-light-theme);
+ }
+ &.light-teal{
+ @import "../skins/light-teal";
+ @include angular-material-theme($teal-light-theme);
+ @include change-theme($teal-light-theme);
+ }
+ &.light-red{
+ @import "../skins/light-red";
+ @include angular-material-theme($red-light-theme);
+ @include change-theme($red-light-theme);
+ }
+ &.dark-blue{
+ @import "../skins/dark-blue";
+ @include angular-material-theme($blue-dark-theme);
+ @include change-theme($blue-dark-theme);
+ }
+ &.dark-green{
+ @import "../skins/dark-green";
+ @include angular-material-theme($green-dark-theme);
+ @include change-theme($green-dark-theme);
+ }
+ &.dark-pink{
+ @import "../skins/dark-pink";
+ @include angular-material-theme($pink-dark-theme);
+ @include change-theme($pink-dark-theme);
+ }
+ &.gray-blue{
+ @import "../skins/gray-blue";
+ @include angular-material-theme($blue-gray-theme);
+ @include change-theme($blue-gray-theme);
+ }
+ &.gray-lime{
+ @import "../skins/gray-lime";
+ @include angular-material-theme($lime-gray-theme);
+ @include change-theme($lime-gray-theme);
+ }
+ &.gray-purple{
+ @import "../skins/gray-purple";
+ @include angular-material-theme($purple-gray-theme);
+ @include change-theme($purple-gray-theme);
+ }
+ &.bluegray-amber{
+ @import "../skins/bluegray-amber";
+ @include angular-material-theme($amber-bluegray-theme);
+ @include change-theme($amber-bluegray-theme);
+ }
+ &.bluegray-deeppurple{
+ @import "../skins/bluegray-deeppurple";
+ @include angular-material-theme($deeppurple-bluegray-theme);
+ @include change-theme($deeppurple-bluegray-theme);
+ }
+ &.bluegray-lightgreen{
+ @import "../skins/bluegray-lightgreen";
+ @include angular-material-theme($lightgreen-bluegray-theme);
+ @include change-theme($lightgreen-bluegray-theme);
+ }
+ &.self-gray{
+ @import "../skins/self-gray";
+ @include angular-material-theme($gray-theme);
+ @include change-theme($gray-theme);
+ }
+ &.self-green{
+ @import "../skins/self-green";
+ @include angular-material-theme($green-theme);
+ @include change-theme($green-theme);
+ }
+ &.self-yellow{
+ @import "../skins/self-yellow";
+ @include angular-material-theme($yellow-theme);
+ @include change-theme($yellow-theme);
+ }
+ &.self-blue{
+ @import "../skins/self-blue";
+ @include angular-material-theme($blue-theme);
+ @include change-theme($blue-theme);
+ }
+ &.self-brown{
+ @import "../skins/self-brown";
+ @include angular-material-theme($brown-theme);
+ @include change-theme($brown-theme);
+ }
+ &.self-pink{
+ @import "../skins/self-pink";
+ @include angular-material-theme($pink-theme);
+ @include change-theme($pink-theme);
+ }
+}
diff --git a/src/assets/.gitkeep b/src/assets/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/src/assets/images/RTL1.jpg b/src/assets/images/RTL1.jpg
new file mode 100644
index 00000000..09d4a96e
Binary files /dev/null and b/src/assets/images/RTL1.jpg differ
diff --git a/src/assets/images/RTL2.jpg b/src/assets/images/RTL2.jpg
new file mode 100644
index 00000000..2f3a5634
Binary files /dev/null and b/src/assets/images/RTL2.jpg differ
diff --git a/src/assets/images/favicon.ico b/src/assets/images/favicon.ico
new file mode 100644
index 00000000..c5c9ce90
Binary files /dev/null and b/src/assets/images/favicon.ico differ
diff --git a/src/browserslist b/src/browserslist
new file mode 100644
index 00000000..37371cb0
--- /dev/null
+++ b/src/browserslist
@@ -0,0 +1,11 @@
+# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
+# For additional information regarding the format and rule options, please see:
+# https://github.com/browserslist/browserslist#queries
+#
+# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed
+
+> 0.5%
+last 2 versions
+Firefox ESR
+not dead
+not IE 9-11
\ No newline at end of file
diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts
new file mode 100644
index 00000000..2ad8a3d0
--- /dev/null
+++ b/src/environments/environment.prod.ts
@@ -0,0 +1,24 @@
+import { VERSION } from './version';
+
+export const API_URL = './api';
+
+export const environment = {
+ production: true,
+ isDebugMode: false,
+ AUTHENTICATE_API: API_URL + '/authenticate',
+ BALANCE_API: API_URL + '/balance',
+ FEES_API: API_URL + '/fees',
+ PEERS_API: API_URL + '/peers',
+ CHANNELS_API: API_URL + '/channels',
+ GETINFO_API: API_URL + '/getinfo',
+ WALLET_API: API_URL + '/wallet',
+ NETWORK_API: API_URL + '/network',
+ NEW_ADDRESS_API : API_URL + '/newaddress',
+ TRANSACTIONS_API : API_URL + '/transactions',
+ CONF_API: API_URL + '/conf',
+ PAYREQUEST_API: API_URL + '/payreq',
+ PAYMENTS_API: API_URL + '/payments',
+ INVOICES_API: API_URL + '/invoices',
+ SWITCH_API: API_URL + '/switch',
+ VERSION: VERSION
+};
diff --git a/src/environments/environment.ts b/src/environments/environment.ts
new file mode 100644
index 00000000..2024cbe1
--- /dev/null
+++ b/src/environments/environment.ts
@@ -0,0 +1,24 @@
+import { VERSION } from './version';
+
+export const API_URL = 'http://localhost:3000/rtl/api';
+
+export const environment = {
+ production: false,
+ isDebugMode: true,
+ AUTHENTICATE_API: API_URL + '/authenticate',
+ BALANCE_API: API_URL + '/balance',
+ FEES_API: API_URL + '/fees',
+ PEERS_API: API_URL + '/peers',
+ CHANNELS_API: API_URL + '/channels',
+ GETINFO_API: API_URL + '/getinfo',
+ WALLET_API: API_URL + '/wallet',
+ NETWORK_API: API_URL + '/network',
+ NEW_ADDRESS_API : API_URL + '/newaddress',
+ TRANSACTIONS_API : API_URL + '/transactions',
+ CONF_API: API_URL + '/conf',
+ PAYREQUEST_API: API_URL + '/payreq',
+ PAYMENTS_API: API_URL + '/payments',
+ INVOICES_API: API_URL + '/invoices',
+ SWITCH_API: API_URL + '/switch',
+ VERSION: VERSION
+};
diff --git a/src/environments/version.ts b/src/environments/version.ts
new file mode 100644
index 00000000..716fbd1b
--- /dev/null
+++ b/src/environments/version.ts
@@ -0,0 +1 @@
+export const VERSION = '0.2.16-beta';
\ No newline at end of file
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 00000000..7999ed6e
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ RTL
+
+
+
+
+
+
+
+
diff --git a/src/karma.conf.js b/src/karma.conf.js
new file mode 100644
index 00000000..b6e00421
--- /dev/null
+++ b/src/karma.conf.js
@@ -0,0 +1,31 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+ config.set({
+ basePath: '',
+ frameworks: ['jasmine', '@angular-devkit/build-angular'],
+ plugins: [
+ require('karma-jasmine'),
+ require('karma-chrome-launcher'),
+ require('karma-jasmine-html-reporter'),
+ require('karma-coverage-istanbul-reporter'),
+ require('@angular-devkit/build-angular/plugins/karma')
+ ],
+ client: {
+ clearContext: false // leave Jasmine Spec Runner output visible in browser
+ },
+ coverageIstanbulReporter: {
+ dir: require('path').join(__dirname, '../coverage'),
+ reports: ['html', 'lcovonly'],
+ fixWebpackSourcePaths: true
+ },
+ reporters: ['progress', 'kjhtml'],
+ port: 9876,
+ colors: true,
+ logLevel: config.LOG_INFO,
+ autoWatch: true,
+ browsers: ['Chrome'],
+ singleRun: false
+ });
+};
\ No newline at end of file
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 00000000..e44f3d92
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,12 @@
+import 'hammerjs';
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+import { environment } from './environments/environment';
+
+if (environment.production) {
+ enableProdMode();
+}
+
+platformBrowserDynamic().bootstrapModule(AppModule).catch(err => console.log(err));
diff --git a/src/polyfills.ts b/src/polyfills.ts
new file mode 100644
index 00000000..480c13eb
--- /dev/null
+++ b/src/polyfills.ts
@@ -0,0 +1,80 @@
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ * file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
+ * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/** IE9, IE10 and IE11 requires all of the following polyfills. **/
+import 'core-js/es6/symbol';
+import 'core-js/es6/object';
+import 'core-js/es6/function';
+import 'core-js/es6/parse-int';
+import 'core-js/es6/parse-float';
+import 'core-js/es6/number';
+import 'core-js/es6/math';
+import 'core-js/es6/string';
+import 'core-js/es6/date';
+import 'core-js/es6/array';
+import 'core-js/es6/regexp';
+import 'core-js/es6/map';
+import 'core-js/es6/weak-map';
+import 'core-js/es6/set';
+
+/** IE10 and IE11 requires the following for NgClass support on SVG elements */
+// import 'classlist.js'; // Run `npm install --save classlist.js`.
+
+/** IE10 and IE11 requires the following for the Reflect API. */
+// import 'core-js/es6/reflect';
+
+
+/** Evergreen browsers require these. **/
+// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
+
+
+
+/**
+ * Web Animations `@angular/platform-browser/animations`
+ * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
+ * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
+ **/
+// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
+
+/**
+ * By default, zone.js will patch all possible macroTask and DomEvents
+ * user can disable parts of macroTask/DomEvents patch by setting following flags
+ */
+
+ // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
+ // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
+ // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
+
+ /*
+ * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
+ * with the following flag, it will bypass `zone.js` patch for IE/Edge
+ */
+// (window as any).__Zone_enable_cross_context_check = true;
+
+/***************************************************************************************************
+ * Zone JS is required by default for Angular itself.
+ */
+import 'zone.js/dist/zone'; // Included with Angular CLI.
+
+
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
diff --git a/src/test.ts b/src/test.ts
new file mode 100644
index 00000000..16317897
--- /dev/null
+++ b/src/test.ts
@@ -0,0 +1,20 @@
+// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+
+import 'zone.js/dist/zone-testing';
+import { getTestBed } from '@angular/core/testing';
+import {
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting
+} from '@angular/platform-browser-dynamic/testing';
+
+declare const require: any;
+
+// First, initialize the Angular testing environment.
+getTestBed().initTestEnvironment(
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting()
+);
+// Then we find all the tests.
+const context = require.context('./', true, /\.spec\.ts$/);
+// And load the modules.
+context.keys().map(context);
diff --git a/src/tsconfig.app.json b/src/tsconfig.app.json
new file mode 100644
index 00000000..190fd300
--- /dev/null
+++ b/src/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/app",
+ "types": []
+ },
+ "exclude": [
+ "test.ts",
+ "**/*.spec.ts"
+ ]
+}
diff --git a/src/tsconfig.spec.json b/src/tsconfig.spec.json
new file mode 100644
index 00000000..de773363
--- /dev/null
+++ b/src/tsconfig.spec.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/spec",
+ "types": [
+ "jasmine",
+ "node"
+ ]
+ },
+ "files": [
+ "test.ts",
+ "polyfills.ts"
+ ],
+ "include": [
+ "**/*.spec.ts",
+ "**/*.d.ts"
+ ]
+}
diff --git a/src/tslint.json b/src/tslint.json
new file mode 100644
index 00000000..9fceb7c0
--- /dev/null
+++ b/src/tslint.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../tslint.json",
+ "rules": {
+ "directive-selector": [
+ true,
+ "attribute",
+ "rtl",
+ "camelCase"
+ ],
+ "component-selector": [
+ true,
+ "element",
+ "rtl",
+ "kebab-case"
+ ]
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..3d5861df
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "importHelpers": true,
+ "outDir": "./angular",
+ "sourceMap": true,
+ "declaration": false,
+ "moduleResolution": "node",
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "target": "es5",
+ "typeRoots": [
+ "node_modules/@types"
+ ],
+ "lib": [
+ "es2017",
+ "dom"
+ ]
+ }
+}
diff --git a/tslint.json b/tslint.json
new file mode 100644
index 00000000..03baed77
--- /dev/null
+++ b/tslint.json
@@ -0,0 +1,141 @@
+{
+ "rulesDirectory": [
+ "node_modules/codelyzer"
+ ],
+ "rules": {
+ "arrow-return-shorthand": true,
+ "callable-types": true,
+ "class-name": true,
+ "comment-format": [
+ true,
+ "check-space"
+ ],
+ "curly": true,
+ "eofline": true,
+ "forin": true,
+ "import-blacklist": [
+ true
+ ],
+ "import-spacing": true,
+ "indent": [
+ true,
+ "spaces"
+ ],
+ "interface-over-type-literal": true,
+ "label-position": true,
+ "max-line-length": [
+ true,
+ 180
+ ],
+ "member-access": false,
+ "member-ordering": [
+ true,
+ {
+ "order": [
+ "static-field",
+ "instance-field",
+ "static-method",
+ "instance-method"
+ ]
+ }
+ ],
+ "no-arg": true,
+ "no-bitwise": true,
+ "no-console": [
+ true,
+ "debug",
+ "info",
+ "time",
+ "timeEnd",
+ "trace"
+ ],
+ "no-construct": true,
+ "no-debugger": true,
+ "no-duplicate-super": true,
+ "no-empty": false,
+ "no-empty-interface": true,
+ "no-eval": true,
+ "no-inferrable-types": [
+ true,
+ "ignore-params"
+ ],
+ "no-misused-new": true,
+ "no-non-null-assertion": true,
+ "no-shadowed-variable": true,
+ "no-string-literal": false,
+ "no-string-throw": true,
+ "no-switch-case-fall-through": true,
+ "no-trailing-whitespace": true,
+ "no-unnecessary-initializer": true,
+ "no-unused-expression": true,
+ "no-use-before-declare": true,
+ "no-var-keyword": true,
+ "object-literal-sort-keys": false,
+ "one-line": [
+ true,
+ "check-open-brace",
+ "check-catch",
+ "check-else",
+ "check-whitespace"
+ ],
+ "prefer-const": true,
+ "quotemark": [
+ true,
+ "single"
+ ],
+ "radix": true,
+ "semicolon": [
+ true,
+ "always"
+ ],
+ "triple-equals": [
+ true,
+ "allow-null-check"
+ ],
+ "typedef-whitespace": [
+ true,
+ {
+ "call-signature": "nospace",
+ "index-signature": "nospace",
+ "parameter": "nospace",
+ "property-declaration": "nospace",
+ "variable-declaration": "nospace"
+ }
+ ],
+ "typeof-compare": true,
+ "unified-signatures": true,
+ "variable-name": false,
+ "whitespace": [
+ true,
+ "check-branch",
+ "check-decl",
+ "check-operator",
+ "check-separator",
+ "check-type"
+ ],
+ "directive-selector": [
+ true,
+ "attribute",
+ "rtl",
+ "camelCase"
+ ],
+ "component-selector": [
+ true,
+ "element",
+ "rtl",
+ "kebab-case"
+ ],
+ "use-input-property-decorator": true,
+ "use-output-property-decorator": true,
+ "use-host-property-decorator": true,
+ "no-input-rename": true,
+ "no-output-rename": true,
+ "use-life-cycle-interface": true,
+ "use-pipe-transform-interface": true,
+ "component-class-suffix": true,
+ "directive-class-suffix": true,
+ "no-access-missing-member": true,
+ "templates-use-public": true,
+ "invoke-injectable": true
+ }
+}