Merging Angular pre-build code

Merging Angular pre-build code
pull/98/head
ShahanaFarooqui 5 years ago
parent 3cad7ad605
commit 0d3ff90977

@ -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

103
.gitignore vendored

@ -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
# 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

@ -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"
}

@ -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 } }));
}
};

@ -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!');
});
});

@ -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();
}
}

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

@ -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`);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

@ -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

@ -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 } }));
}
};

@ -0,0 +1,78 @@
<div fxLayout="column" id="rtl-container" class="rtl-container" [ngClass]="settings.theme" [class.horizontal]="settings.menu === 'Horizontal'" [class.compact]="settings.menuType === 'Compact'" [class.mini]="settings.menuType === 'Mini'">
<mat-sidenav-container>
<mat-sidenav perfectScrollbar *ngIf="settings.menu === 'Vertical'" [opened]="settings.flgSidenavOpened" [mode]="(settings.flgSidenavPinned) ? 'side' : 'over'"
#sideNavigation class="sidenav mat-elevation-z6">
<rtl-side-navigation (ChildNavClicked)="onNavigationClicked($event)"></rtl-side-navigation>
</mat-sidenav>
<mat-sidenav-content perfectScrollbar class="overflow-auto">
<div class="top-bar">
<mat-toolbar fxLayout="row" fxLayoutAlign="space-between center" color="primary" class="padding-gap-x sticky top-toolbar">
<div fxLayoutAlign="center center">
<button *ngIf="settings.menu === 'Vertical'" mat-icon-button (click)="sideNavToggle(sideNavigation)">
<mat-icon>menu</mat-icon>
</button>
</div>
<div>
<h2>Ride The Lightning <span class="font-60-percent">(Beta)</span></h2>
</div>
<div>
<rtl-top-menu></rtl-top-menu>
</div>
</mat-toolbar>
<div fxLayout="row" fxLayoutAlign="center center" class="bg-primary pubkey-info-top sticky" rtlClipboard [payload]="information?.identity_pubkey" (copied)="copiedText($event)">
<mat-icon [ngClass]="{'icon-smaller': smallScreen}">vpn_key</mat-icon>
<div [ngClass]="{'word-break font-9px': smallScreen, 'word-break': !smallScreen}">&nbsp;{{information?.identity_pubkey}}
<mat-spinner [diameter]="20" *ngIf="flgLoading[0]" class="inline-spinner foreground"></mat-spinner>
<mat-icon [ngClass]="{'icon-smaller cursor-pointer copy-icon-smaller': smallScreen, 'icon-small cursor-pointer copy-icon': !smallScreen}">file_copy</mat-icon><span [hidden]="!flgCopied">Copied</span>
</div>
</div>
<mat-toolbar color="primary" *ngIf="settings.menu === 'Horizontal'" class="padding-gap-x horizontal-nav sticky">
<div fxLayout="row" fxFlex="100" fxLayoutAlign="center center" class="h-100">
<rtl-horizontal-navigation></rtl-horizontal-navigation>
</div>
</mat-toolbar>
</div>
<!-- <div fxLayout="column" class="top-bar">
<mat-toolbar fxLayout="row" fxLayoutAlign="space-between center" color="primary" class="padding-gap-x sticky top-toolbar">
<div fxLayoutAlign="center center">
<button *ngIf="settings.menu === 'Vertical'" mat-icon-button (click)="sideNavToggle(sideNavigation)">
<mat-icon>menu</mat-icon>
</button>
</div>
<div>
<h2>Ride The Lightning <span class="font-60-percent">(Beta)</span></h2>
</div>
<div>
<rtl-top-menu></rtl-top-menu>
</div>
</mat-toolbar>
<div fxLayout="row" fxLayoutAlign="space-between center" class="bg-primary pubkey-info-top sticky" rtlClipboard [payload]="information?.identity_pubkey" (copied)="copiedText($event)">
<mat-icon [ngClass]="{'icon-smaller': smallScreen}">vpn_key</mat-icon>
<div [ngClass]="{'word-break font-9px': smallScreen, 'word-break': !smallScreen}">&nbsp;{{information?.identity_pubkey}}
<mat-spinner [diameter]="20" *ngIf="flgLoading[0]" class="inline-spinner foreground"></mat-spinner>
<mat-icon [ngClass]="{'icon-smaller cursor-pointer copy-icon-smaller': smallScreen, 'icon-small cursor-pointer copy-icon': !smallScreen}">file_copy</mat-icon><span [hidden]="!flgCopied">Copied</span>
</div>
</div>
<mat-toolbar color="primary" *ngIf="settings.menu === 'Horizontal'" class="padding-gap-x horizontal-nav sticky">
<div fxLayout="row" fxFlex="100" fxLayoutAlign="center center" class="h-100">
<rtl-horizontal-navigation></rtl-horizontal-navigation>
</div>
</mat-toolbar>
</div> -->
<div [ngClass]="{'mt-minus-1': smallScreen, 'inner-sidenav-content': true}">
<router-outlet></router-outlet>
</div>
<div fxLayout="row" fxLayoutAlign="center center" class="bg-primary settings-icon" (click)="settingSidenav.toggle()">
<mat-icon class="animate-settings">settings</mat-icon>
</div>
</mat-sidenav-content>
<mat-sidenav #settingSidenav position="end" class="settings mat-elevation-z6" mode="side">
<rtl-settings-nav (done)="settingSidenav.toggle()"></rtl-settings-nav>
</mat-sidenav>
</mat-sidenav-container>
<div class="rtl-spinner" *ngIf="undefined === settings.theme">
<mat-spinner color="accent"></mat-spinner>
<h4>Loading RTL...</h4>
</div>
</div>

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

@ -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<Boolean | 'error'> = [true]; // 0: Info
public flgCopied = false;
public settings: Settings;
public authSettings: Authentication;
public accessKey = '';
public smallScreen = false;
unsubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.State>, 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();
});
}
}

@ -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 {}

@ -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 });

@ -0,0 +1,64 @@
<div fxLayout="column">
<div class="padding-gap">
<mat-card>
<mat-card-header>
<mat-card-subtitle>
<h2>Closed Channels</h2>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content class="table-card-content">
<div fxLayout="row" fxLayoutAlign="start start">
<mat-form-field fxFlex="30">
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
</mat-form-field>
</div>
<div perfectScrollbar class="table-container mat-elevation-z8">
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
<table mat-table #table [dataSource]="closedChannels" matSort [ngClass]="{'mat-elevation-z8 overflow-auto error-border': flgLoading[0]==='error','mat-elevation-z8 overflow-auto': true}">
<ng-container matColumnDef="close_type">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Close Type </th>
<td mat-cell *matCellDef="let channel"> {{channel.close_type || 'COOPERATIVE_CLOSE'}} </td>
</ng-container>
<ng-container matColumnDef="channel_point">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Channel Point </th>
<td mat-cell *matCellDef="let channel"> {{channel.channel_point | slice:0:10}}...</td>
</ng-container>
<ng-container matColumnDef="chan_id">
<th mat-header-cell *matHeaderCellDef mat-sort-header> ID </th>
<td mat-cell *matCellDef="let channel"> {{channel.chan_id}} </td>
</ng-container>
<ng-container matColumnDef="closing_tx_hash">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Closing Txn Hash </th>
<td mat-cell *matCellDef="let channel">
<div>{{channel.closing_tx_hash | slice:0:10}}...</div></td>
</ng-container>
<ng-container matColumnDef="remote_pubkey">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Pub Key </th>
<td mat-cell *matCellDef="let channel">
<div>{{channel.remote_pubkey | slice:0:10}}...</div></td>
</ng-container>
<ng-container matColumnDef="capacity">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Capacity </th>
<td mat-cell *matCellDef="let channel"><span fxLayoutAlign="end center"> {{channel.capacity | number}} </span></td>
</ng-container>
<ng-container matColumnDef="close_height">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Close Height </th>
<td mat-cell *matCellDef="let channel"><span fxLayoutAlign="end center"> {{channel.close_height | number}} </span></td>
</ng-container>
<ng-container matColumnDef="settled_balance">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Settled Balance </th>
<td mat-cell *matCellDef="let channel"><span fxLayoutAlign="end center"> {{channel.settled_balance | number}} </span></td>
</ng-container>
<ng-container matColumnDef="time_locked_balance">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Time Locked Balance </th>
<td mat-cell *matCellDef="let channel"><span fxLayoutAlign="end center"> {{channel.time_locked_balance | number}} </span></td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" (click)="onClosedChannelClick(row, $event)"></tr>
</table>
</div>
</mat-card-content>
</mat-card>
</div>
</div>

@ -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;
}
}

@ -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<ChannelClosedComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ChannelClosedComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ChannelClosedComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<Boolean | 'error'> = [true];
public selectedFilter = '';
private unsub: Array<Subject<void>> = [new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.State>, 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<ClosedChannel>([...closedChannels]);
this.closedChannels.sort = this.sort;
this.logger.info(this.closedChannels);
}
resetData() {
this.selectedFilter = '';
}
ngOnDestroy() {
this.unsub.forEach(completeSub => {
completeSub.next();
completeSub.complete();
});
}
}

@ -0,0 +1,131 @@
<div fxLayout="column">
<div class="padding-gap">
<mat-card>
<mat-card-header>
<mat-card-subtitle>
<h2>Add Channel</h2>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<form fxLayout="column" fxLayout.gt-sm="row wrap" (ngSubmit)="openChannelForm.form.valid && onOpenChannel(openChannelForm)" #openChannelForm="ngForm">
<mat-form-field fxFlex="40" fxLayoutAlign="start end">
<mat-select [(ngModel)]="selectedPeer" placeholder="Alias" name="peer_alias" tabindex="1" required name="selPeer" #selPeer="ngModel">
<mat-option *ngFor="let peer of peers" [value]="peer.pub_key">
{{peer.alias}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="25" fxLayoutAlign="start end">
<input matInput [(ngModel)]="fundingAmount" placeholder="Amount ({{information?.smaller_currency_unit}})" type="number" step="1000" min="1" tabindex="2" required name="amount" #amount="ngModel">
<mat-hint>(Wallet Bal: {{totalBalance}}, Remaining Bal: {{totalBalance - ((fundingAmount) ? fundingAmount : 0)}})</mat-hint>
</mat-form-field>
<div fxFlex="15" tabindex="3" fxLayoutAlign="start center">
<mat-checkbox [(ngModel)]="moreOptions" name="moreOptions" (change)="onMoreOptionsChange($event)">Options</mat-checkbox>
</div>
<span *ngIf="moreOptions" fxLayout="column" fxLayout.gt-sm="row wrap" fxFlex="77">
<div fxFlex="30" tabindex="4" fxLayoutAlign="start start">
<mat-form-field fxFlex="99">
<mat-select [(value)]="selTransType">
<mat-option *ngFor="let transType of transTypes" [value]="transType.id">
{{transType.name}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxFlex="30" fxLayoutAlign="start start">
<mat-form-field fxFlex="90" fxFlex.lt-sm="100" *ngIf="selTransType=='0'">
<input matInput placeholder="Channel Opening Priority" disabled>
</mat-form-field>
<mat-form-field fxFlex="90" fxFlex.lt-sm="100" *ngIf="selTransType=='1'">
<input matInput [(ngModel)]="transTypeValue.blocks" placeholder="Target Confirmation Blocks" type="number" name="blocks" step="1" min="0" required tabindex="5" #blocks="ngModel">
</mat-form-field>
<mat-form-field fxFlex="90" fxFlex.lt-sm="100" *ngIf="selTransType=='2'">
<input matInput [(ngModel)]="transTypeValue.fees" placeholder="Fee ({{information?.smaller_currency_unit}}/Byte)" type="number" name="fees" step="1" min="0" required tabindex="6" #fees="ngModel">
</mat-form-field>
</div>
<div fxFleax="40" fxLayoutAlign="start center">
<mat-checkbox tabindex="7" [(ngModel)]="spendUnconfirmed" name="spendUnconfirmed">Spend Unconfirmed Output</mat-checkbox>
</div>
</span>
<div fxFlex="10" fxLayoutAlign="end start">
<button fxFlex="90" fxLayoutAlign="center center" mat-raised-button color="primary" [disabled]="selectedPeer === '' || fundingAmount == null || (totalBalance - ((fundingAmount) ? fundingAmount : 0) < 0)" type="submit" tabindex="8">
<p *ngIf="(selectedPeer === '' || fundingAmount == null) && (selPeer.touched || selPeer.dirty) && (amount.touched || amount.dirty); else openText">Invalid Values</p>
<ng-template #openText><p>Open</p></ng-template>
</button>
</div>
<div fxFlex="10" fxLayoutAlign="end start">
<button fxFlex="90" fxLayoutAlign="center center" mat-raised-button color="accent" tabindex="9" type="reset" (click)="resetData()">Clear</button>
</div>
</form>
</mat-card-content>
</mat-card>
</div>
<div class="padding-gap">
<mat-card>
<mat-card-content fxFlex="100" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start start">
<mat-form-field fxFlex="30">
<input matInput (keyup)="applyFilter()" [(ngModel)]="selFilter" name="filter" placeholder="Filter">
</mat-form-field>
</div>
<div perfectScrollbar class="table-container mat-elevation-z8">
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
<table mat-table #table [dataSource]="channels" matSort [ngClass]="{'mat-elevation-z8 overflow-auto error-border': flgLoading[0]==='error','mat-elevation-z8 overflow-auto': true}">
<ng-container matColumnDef="close">
<th mat-header-cell *matHeaderCellDef> Disconnect </th>
<td mat-cell *matCellDef="let channel"><mat-icon color="accent" (click)="onChannelClose(channel)">link_off</mat-icon></td>
</ng-container>
<ng-container matColumnDef="update">
<th mat-header-cell *matHeaderCellDef><mat-icon color="accent" (click)="onChannelUpdate('all')">edit</mat-icon></th>
<td mat-cell *matCellDef="let channel"><mat-icon color="accent" (click)="onChannelUpdate(channel)">edit</mat-icon></td>
</ng-container>
<ng-container matColumnDef="active">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Status </th>
<td mat-cell *matCellDef="let channel"> {{channel.active}} </td>
</ng-container>
<ng-container matColumnDef="chan_id">
<th mat-header-cell *matHeaderCellDef mat-sort-header> ID </th>
<td mat-cell *matCellDef="let channel"> {{channel.chan_id}} </td>
</ng-container>
<ng-container matColumnDef="remote_pubkey">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Pub Key </th>
<td mat-cell *matCellDef="let channel">
<div>{{channel.remote_pubkey | slice:0:10}}...</div></td>
</ng-container>
<ng-container matColumnDef="remote_alias">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Alias </th>
<td mat-cell *matCellDef="let channel">{{channel.remote_alias}}</td>
</ng-container>
<ng-container matColumnDef="capacity">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Capacity </th>
<td mat-cell *matCellDef="let channel"><span fxLayoutAlign="end center"> {{channel.capacity | number}} </span></td>
</ng-container>
<ng-container matColumnDef="local_balance">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Local Bal </th>
<td mat-cell *matCellDef="let channel"><span fxLayoutAlign="end center"> {{channel.local_balance | number}} </span></td>
</ng-container>
<ng-container matColumnDef="remote_balance">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Remote Bal </th>
<td mat-cell *matCellDef="let channel"><span fxLayoutAlign="end center"> {{channel.remote_balance | number}} </span></td>
</ng-container>
<ng-container matColumnDef="total_satoshis_sent">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> {{information?.smaller_currency_unit}} Sent </th>
<td mat-cell *matCellDef="let channel"><span fxLayoutAlign="end center"> {{channel.total_satoshis_sent | number}} </span></td>
</ng-container>
<ng-container matColumnDef="total_satoshis_received">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> {{information?.smaller_currency_unit}} Recv </th>
<td mat-cell *matCellDef="let channel"><span fxLayoutAlign="end center"> {{channel.total_satoshis_received | number}} </span></td>
</ng-container>
<ng-container matColumnDef="commit_fee">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Fee </th>
<td mat-cell *matCellDef="let channel"><span fxLayoutAlign="end center"> {{channel.commit_fee | number}} </span></td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: flgSticky;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" (click)="onChannelClick(row, $event)"></tr>
</table>
</div>
</mat-card-content>
</mat-card>
</div>
</div>

@ -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;
}
}

@ -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<ChannelManageComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ChannelManageComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ChannelManageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<Boolean | 'error'> = [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<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.State>, 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<Channel>([...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();
});
}
}

@ -0,0 +1,252 @@
<div fxLayout="column">
<div fxFlex="100" class="padding-gap">
<mat-card>
<mat-card-header>
<mat-card-subtitle>
<h2>Pending Channels</h2>
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div class="padding-gap">
<h3 *ngIf="settings?.satsToBTC; else smallerUnit">Total Limbo Balance:
{{pendingChannels.btc_total_limbo_balance | number}} {{information?.currency_unit}}</h3>
<ng-template #smallerUnit>
<h3>Total Limbo Balance: {{pendingChannels.total_limbo_balance | number}}
{{information?.smaller_currency_unit}}</h3>
</ng-template>
</div>
<div class="">
<mat-accordion>
<mat-expansion-panel displayMode="flat">
<mat-expansion-panel-header class="pl-1 pr-1">
<mat-panel-title>
<h3>Pending Open Channels({{pendingOpenChannelsLength}})</h3>
</mat-panel-title>
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
</mat-expansion-panel-header>
<mat-table #table perfectScrollbar [dataSource]="pendingOpenChannels" matSort
[ngClass]="{'mat-elevation-z8 overflow-auto error-border': flgLoading[0]==='error','mat-elevation-z8 overflow-auto': true}">
<ng-container matColumnDef="remote_node_pub">
<mat-header-cell class="pl-2" *matHeaderCellDef mat-sort-header> Remote Node Pub </mat-header-cell>
<mat-cell class="pl-2" *matCellDef="let channel">{{channel.channel.remote_node_pub}}</mat-cell>
</ng-container>
<ng-container matColumnDef="local_balance">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Local Balance </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.channel.local_balance |
number}}</mat-cell>
</ng-container>
<ng-container matColumnDef="commit_fee">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Commit Fee </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.commit_fee | number}}
</mat-cell>
</ng-container>
<ng-container matColumnDef="remote_balance">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Remote Balance </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.channel.remote_balance |
number}}</mat-cell>
</ng-container>
<ng-container matColumnDef="capacity">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Capacity </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.channel.capacity |
number}}</mat-cell>
</ng-container>
<ng-container matColumnDef="commit_weight">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Commit Weight </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.commit_weight | number}}
</mat-cell>
</ng-container>
<ng-container matColumnDef="fee_per_kw">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Fee Per KW </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.fee_per_kw | number}}
</mat-cell>
</ng-container>
<ng-container matColumnDef="confirmation_height">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Confirmation Height </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.confirmation_height |
number}}</mat-cell>
</ng-container>
<ng-container matColumnDef="channel_point">
<mat-header-cell class="pl-2" *matHeaderCellDef mat-sort-header> Channel Point </mat-header-cell>
<mat-cell class="pl-2" *matCellDef="let channel">{{channel.channel.channel_point}}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedOpenColumns"></mat-header-row>
<mat-row fxLayoutAlign="stretch stretch" *matRowDef="let row; columns: displayedOpenColumns;"
(click)="onOpenClick(row, $event)"></mat-row>
</mat-table>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header class="pl-1 pr-1">
<mat-panel-title>
<h3>Pending Force Closing Channels({{pendingForceClosingChannelsLength}})</h3>
</mat-panel-title>
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
</mat-expansion-panel-header>
<mat-table #table perfectScrollbar [dataSource]="pendingForceClosingChannels" matSort
[ngClass]="{'mat-elevation-z8 overflow-auto error-border': flgLoading[0]==='error','mat-elevation-z8 overflow-auto': true}">
<ng-container matColumnDef="remote_node_pub">
<mat-header-cell class="pl-2" *matHeaderCellDef mat-sort-header> Remote Node Pub </mat-header-cell>
<mat-cell class="pl-2" *matCellDef="let channel">{{channel.channel.remote_node_pub}}</mat-cell>
</ng-container>
<ng-container matColumnDef="recovered_balance">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Recovered Balance </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.recovered_balance |
number}}</mat-cell>
</ng-container>
<ng-container matColumnDef="limbo_balance">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Limbo Balance </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.limbo_balance | number}}
</mat-cell>
</ng-container>
<ng-container matColumnDef="blocks_til_maturity">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Block Till Maturity </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.blocks_til_maturity |
number}}</mat-cell>
</ng-container>
<ng-container matColumnDef="maturity_height">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Maturity Height </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.maturity_height | number}}
</mat-cell>
</ng-container>
<ng-container matColumnDef="local_balance">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Local Balance </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.channel.local_balance |
number}}</mat-cell>
</ng-container>
<ng-container matColumnDef="remote_balance">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Remote Balance </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.channel.remote_balance |
number}}</mat-cell>
</ng-container>
<ng-container matColumnDef="capacity">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Capacity </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.channel.capacity |
number}}</mat-cell>
</ng-container>
<ng-container matColumnDef="closing_txid">
<mat-header-cell *matHeaderCellDef mat-sort-header> Transaction Id </mat-header-cell>
<mat-cell *matCellDef="let channel">
<div class="flex-ellipsis">{{channel.closing_txid}}</div>
</mat-cell>
</ng-container>
<ng-container matColumnDef="channel_point">
<mat-header-cell class="pl-2" *matHeaderCellDef mat-sort-header> Channel Point </mat-header-cell>
<mat-cell class="pl-2" *matCellDef="let channel">{{channel.channel.channel_point}}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedForceClosingColumns"></mat-header-row>
<mat-row fxLayoutAlign="stretch stretch" *matRowDef="let row; columns: displayedForceClosingColumns;"
(click)="onForceClosingClick(row, $event)"></mat-row>
</mat-table>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header class="pl-1 pr-1">
<mat-panel-title>
<h3>Pending Closing Channels({{pendingClosingChannelsLength}})</h3>
</mat-panel-title>
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
</mat-expansion-panel-header>
<mat-table #table perfectScrollbar [dataSource]="pendingClosingChannels" matSort
[ngClass]="{'mat-elevation-z8 overflow-auto error-border': flgLoading[0]==='error','mat-elevation-z8 overflow-auto': true}">
<ng-container matColumnDef="remote_node_pub">
<mat-header-cell class="pl-2" *matHeaderCellDef mat-sort-header> Remote Node Pub </mat-header-cell>
<mat-cell class="pl-2" *matCellDef="let channel">{{channel.channel.remote_node_pub}}</mat-cell>
</ng-container>
<ng-container matColumnDef="local_balance">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Local Balance </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.channel.local_balance |
number}}</mat-cell>
</ng-container>
<ng-container matColumnDef="remote_balance">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Remote Balance </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.channel.remote_balance |
number}}</mat-cell>
</ng-container>
<ng-container matColumnDef="capacity">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Capacity </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.channel.capacity |
number}}</mat-cell>
</ng-container>
<ng-container matColumnDef="closing_txid">
<mat-header-cell *matHeaderCellDef mat-sort-header> Transaction Id </mat-header-cell>
<mat-cell *matCellDef="let channel">
<div class="flex-ellipsis">{{channel.closing_txid}}</div>
</mat-cell>
</ng-container>
<ng-container matColumnDef="channel_point">
<mat-header-cell class="pl-2" *matHeaderCellDef mat-sort-header> Channel Point </mat-header-cell>
<mat-cell class="pl-2" *matCellDef="let channel">{{channel.channel.channel_point}}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedClosingColumns"></mat-header-row>
<mat-row fxLayoutAlign="stretch stretch" *matRowDef="let row; columns: displayedClosingColumns;"
(click)="onClosingClick(row, $event)"></mat-row>
</mat-table>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header class="pl-1 pr-1">
<mat-panel-title>
<h3>Waiting Close Channels({{pendingWaitClosingChannelsLength}})</h3>
</mat-panel-title>
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
</mat-expansion-panel-header>
<mat-table #table perfectScrollbar [dataSource]="pendingWaitClosingChannels" matSort
[ngClass]="{'mat-elevation-z8 overflow-auto error-border': flgLoading[0]==='error','mat-elevation-z8 overflow-auto': true}">
<ng-container matColumnDef="remote_node_pub">
<mat-header-cell class="pl-2" *matHeaderCellDef mat-sort-header> Remote Node Pub </mat-header-cell>
<mat-cell class="pl-2" *matCellDef="let channel">{{channel.channel.remote_node_pub}}</mat-cell>
</ng-container>
<ng-container matColumnDef="limbo_balance">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Limbo Balance </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.limbo_balance | number}}
</mat-cell>
</ng-container>
<ng-container matColumnDef="local_balance">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Local Balance </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.channel.local_balance |
number}}</mat-cell>
</ng-container>
<ng-container matColumnDef="remote_balance">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Remote Balance </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.channel.remote_balance |
number}}</mat-cell>
</ng-container>
<ng-container matColumnDef="capacity">
<mat-header-cell fxLayoutAlign="end center" *matHeaderCellDef mat-sort-header arrowPosition="before">
Capacity </mat-header-cell>
<mat-cell fxLayoutAlign="end center" *matCellDef="let channel">{{channel.channel.capacity |
number}}</mat-cell>
</ng-container>
<ng-container matColumnDef="channel_point">
<mat-header-cell class="pl-2" *matHeaderCellDef mat-sort-header> Channel Point </mat-header-cell>
<mat-cell class="pl-2" *matCellDef="let channel">{{channel.channel.channel_point}}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedWaitClosingColumns"></mat-header-row>
<mat-row fxLayoutAlign="stretch stretch" *matRowDef="let row; columns: displayedWaitClosingColumns;"
(click)="onWaitClosingClick(row, $event)"></mat-row>
</mat-table>
</mat-expansion-panel>
</mat-accordion>
</div>
</mat-card-content>
</mat-card>
</div>
</div>

@ -0,0 +1,6 @@
.flex-ellipsis {
padding-right: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

@ -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<ChannelPendingComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ChannelPendingComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ChannelPendingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<Boolean | 'error'> = [true];
private unsub: Array<Subject<void>> = [new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.State>, 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<Channel>([...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<Channel>([...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<Channel>([...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<Channel>([...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();
});
}
}

@ -0,0 +1,20 @@
<div fxLayout="column">
<div class="padding-gap">
<mat-card>
<mat-card-header>
<mat-card-subtitle>
<h2>Help</h2>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content *ngFor="let helpTopic of helpTopics">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>{{helpTopic.question}}</mat-panel-title>
</mat-expansion-panel-header>
<mat-panel-description>{{helpTopic.answer}}</mat-panel-description>
</mat-expansion-panel>
<div class="divider"></div>
</mat-card-content>
</mat-card>
</div>
</div>

@ -0,0 +1,3 @@
.mat-card-content {
margin-bottom: 4px;
}

@ -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<HelpTopic> = [];
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".<br>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.'));
}
}

@ -0,0 +1,237 @@
<div fxLayout="column" fxLayout.gt-sm="row wrap">
<div fxFlex="25" class="padding-gap">
<mat-card [ngClass]="{'custom-card error-border': flgLoading[2]==='error','custom-card': true}">
<mat-card-header class="bg-primary" fxLayoutAlign="center end">
<mat-card-title class="m-0 pt-2">
<h5>Wallet Balance</h5>
</mat-card-title>
</mat-card-header>
<mat-card-content fxLayout="column" fxLayoutAlign="center center">
<mat-card-content class="mt-1">
<mat-icon class="icon-large">account_balance_wallet</mat-icon>
</mat-card-content>
<span *ngIf="information?.currency_unit; else withoutData">
<h3 *ngIf="settings?.satsToBTC; else smallerUnit1">{{BTCtotalBalance | number}} {{information?.currency_unit}}</h3>
<ng-template #smallerUnit1><h3>{{totalBalance | number}} {{information?.smaller_currency_unit}}</h3></ng-template>
</span>
</mat-card-content>
<mat-progress-bar class="mt-minus-5" *ngIf="flgLoading[2]===true" mode="indeterminate"></mat-progress-bar>
<mat-divider></mat-divider>
</mat-card>
</div>
<div fxFlex="25" class="padding-gap">
<mat-card [ngClass]="{'custom-card error-border': flgLoading[0]==='error','custom-card': true}">
<mat-card-header class="bg-primary" fxLayoutAlign="center center">
<mat-card-title class="m-0 pt-2">
<h5>Peers</h5>
</mat-card-title>
</mat-card-header>
<mat-card-content fxLayout="column" fxLayoutAlign="center center">
<mat-card-content class="mt-1">
<mat-icon class="icon-large">group</mat-icon>
</mat-card-content>
<h3 *ngIf="information.num_peers; else zeroPeers">{{totalPeers | number}}</h3>
<ng-template #zeroPeers>
<h3>0</h3>
</ng-template>
</mat-card-content>
<mat-progress-bar class="mt-minus-5" *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
<mat-divider></mat-divider>
</mat-card>
</div>
<div fxFlex="25" class="padding-gap">
<mat-card [ngClass]="{'custom-card error-border': flgLoading[3]==='error','custom-card': true}">
<mat-card-header class="bg-primary" fxLayoutAlign="center center">
<mat-card-title class="m-0 pt-2">
<h5>Channel Balance</h5>
</mat-card-title>
</mat-card-header>
<mat-card-content fxLayout="column" fxLayoutAlign="center center">
<mat-card-content class="mt-1">
<mat-icon class="icon-large">linear_scale</mat-icon>
</mat-card-content>
<span *ngIf="information?.currency_unit; else withoutData">
<h3 *ngIf="settings?.satsToBTC; else smallerUnit2">{{BTCchannelBalance | number}} {{information?.currency_unit}}</h3>
<ng-template #smallerUnit2><h3>{{channelBalance | number}} {{information?.smaller_currency_unit}}</h3></ng-template>
</span>
</mat-card-content>
<mat-progress-bar class="mt-minus-5" *ngIf="flgLoading[3]===true || flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
<mat-divider></mat-divider>
</mat-card>
</div>
<div fxFlex="25" class="padding-gap">
<mat-card [ngClass]="{'custom-card error-border': flgLoading[0]==='error','custom-card': true}">
<mat-card-header class="bg-primary" fxLayoutAlign="center center">
<mat-card-title class="m-0 pt-2">
<h5>Chain Sync Status</h5>
</mat-card-title>
</mat-card-header>
<mat-card-content fxLayout="column" fxLayoutAlign="center center">
<mat-card-content class="mt-1">
<mat-icon class="icon-large">sync</mat-icon>
</mat-card-content>
<mat-icon *ngIf="information?.synced_to_chain; else notSynced" class="size-30 green sync-to-chain">check_circle</mat-icon>
<ng-template #notSynced>
<mat-icon class="size-30 red">cancel</mat-icon>
</ng-template>
</mat-card-content>
<mat-progress-bar class="mt-minus-5" *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
<mat-divider></mat-divider>
</mat-card>
</div>
</div>
<div fxLayout="column" fxLayout.gt-sm="row wrap">
<div fxFlex="25" class="padding-gap">
<div fxLayout="column">
<mat-card fxFlex="100" [ngClass]="{'custom-card error-border': flgLoading[1]==='error','custom-card': true}">
<mat-card-header class="bg-primary" fxLayoutAlign="center center">
<mat-card-title class="m-0 pt-2">
<h5>Fee Report</h5>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div fxLayout="column" class="pl-4">
<mat-list fxFlex="100" fxLayoutAlign="start start">
<mat-list-item fxFlex="65" fxLayoutAlign="start start">Daily ({{information?.smaller_currency_unit}})</mat-list-item>
<mat-list-item fxFlex="25" fxLayoutAlign="end start">{{fees?.day_fee_sum}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxFlex="100" fxLayoutAlign="start start">
<mat-list-item fxFlex="65" fxLayoutAlign="start start">Weekly ({{information?.smaller_currency_unit}})</mat-list-item>
<mat-list-item fxFlex="25" fxLayoutAlign="end start">{{fees?.week_fee_sum}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxFlex="100" fxLayoutAlign="start start">
<mat-list-item fxFlex="65" fxLayoutAlign="start start">Monthly ({{information?.smaller_currency_unit}})</mat-list-item>
<mat-list-item fxFlex="25" fxLayoutAlign="end start">{{fees?.month_fee_sum}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
</div>
<mat-progress-bar *ngIf="flgLoading[1]===true" mode="indeterminate"></mat-progress-bar>
<mat-divider></mat-divider>
</mat-card-content>
</mat-card>
<mat-card fxFlex="100" [ngClass]="{'mt-2 custom-card error-border': flgLoading[5]==='error','mt-2 custom-card': true}">
<mat-card-header class="bg-primary" fxLayoutAlign="center center">
<mat-card-title class="m-0 pt-2">
<h5>Channel Status</h5>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div fxLayout="column" class="pl-4">
<mat-list fxFlex="100" fxLayoutAlign="start start">
<mat-list-item fxFlex="65" fxLayoutAlign="start start">Active</mat-list-item>
<mat-list-item fxFlex="25" fxLayoutAlign="end start"><p class="mat-button-text pt-2">{{activeChannels}}</p></mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxFlex="100" fxLayoutAlign="start start">
<mat-list-item fxFlex="65" fxLayoutAlign="start start">Inactive</mat-list-item>
<mat-list-item fxFlex="25" fxLayoutAlign="end start"><p class="mat-button-text pt-2">{{inactiveChannels}}</p></mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxFlex="100" fxLayoutAlign="start start">
<mat-list-item fxFlex="65" fxLayoutAlign="start start">Pending</mat-list-item>
<mat-list-item fxFlex="25" fxLayoutAlign="end start"><p class="mat-button-text pt-2">{{pendingChannels}}</p></mat-list-item>
<mat-divider></mat-divider>
</mat-list>
</div>
<mat-progress-bar *ngIf="flgLoading[6]===true" mode="indeterminate" class="mt-minus-5"></mat-progress-bar>
<mat-divider></mat-divider>
</mat-card-content>
</mat-card>
</div>
</div>
<div fxFlex="40" class="padding-gap">
<mat-card [ngClass]="{'custom-card error-border': flgLoading[5]==='error','custom-card': true}">
<mat-card-header class="bg-primary" fxLayoutAlign="center center">
<mat-card-title class="m-0 pt-2">
<h5>Local-Remote Channel Capacity</h5>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div fxLayout="row" class="card-chnl-balances">
<div fxFlex="100" fxLayoutAlign="center center" *ngIf="flgTotalCalculated">
<ngx-charts-bar-vertical
[view]="view"
[scheme]="colorScheme"
[results]="totalBalances"
[yAxisLabel]="yAxisLabel"
[yScaleMax]="maxBalanceValue"
xAxis="false"
yAxis="true"
showYAxis="true"
showDataLabel="true"
tooltipDisabled="true">
</ngx-charts-bar-vertical>
</div>
</div>
<mat-progress-bar *ngIf="flgLoading[5]===true" mode="indeterminate" class="mt-minus-5"></mat-progress-bar>
<mat-divider></mat-divider>
</mat-card-content>
</mat-card>
</div>
<div fxFlex="35" class="padding-gap">
<mat-card [ngClass]="{'custom-card error-border': flgLoading[5]==='error','custom-card': true}">
<mat-card-header class="bg-primary" fxLayoutAlign="center center">
<mat-card-title class="m-0 pt-2">
<h5>Network Information</h5>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div fxLayout="column" class="pl-4 network-info-list">
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="65" fxLayoutAlign="start start" *ngIf="settings?.satsToBTC; else smallerUnit6">Network Capacity ({{information?.currency_unit}})</mat-list-item>
<mat-list-item fxFlex="25" fxLayoutAlign="end start" *ngIf="settings?.satsToBTC; else smallerData6">{{networkInfo?.btc_total_network_capacity | number}}</mat-list-item>
<ng-template #smallerUnit6><mat-list-item fxFlex="65" fxLayoutAlign="start start">Network Capacity ({{information?.smaller_currency_unit}})</mat-list-item></ng-template>
<ng-template #smallerData6><mat-list-item fxFlex="25" fxLayoutAlign="end start">{{networkInfo?.total_network_capacity | number}}</mat-list-item></ng-template>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="65" fxLayoutAlign="start start">Number of Nodes</mat-list-item>
<mat-list-item fxFlex="25" fxLayoutAlign="end start">{{networkInfo?.num_nodes | number}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="65" fxLayoutAlign="start start">Number of Channels</mat-list-item>
<mat-list-item fxFlex="25" fxLayoutAlign="end start">{{networkInfo?.num_channels | number}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="65" fxLayoutAlign="start start">Max Out Degree</mat-list-item>
<mat-list-item fxFlex="25" fxLayoutAlign="end start">{{networkInfo?.max_out_degree | number}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="65" fxLayoutAlign="start start">Avg Out Degree</mat-list-item>
<mat-list-item fxFlex="25" fxLayoutAlign="end start">{{networkInfo?.avg_out_degree | number:'1.0-2'}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="65" fxLayoutAlign="start start" *ngIf="settings?.satsToBTC; else smallerUnit7">Max Channel Size ({{information?.currency_unit}})</mat-list-item>
<ng-template #smallerUnit7><mat-list-item fxFlex="65" fxLayoutAlign="start start">Max Channel Size ({{information?.smaller_currency_unit}})</mat-list-item></ng-template>
<mat-list-item fxFlex="25" fxLayoutAlign="end start" *ngIf="settings?.satsToBTC; else smallerData7">{{networkInfo?.btc_max_channel_size | number}}</mat-list-item>
<ng-template #smallerData7><mat-list-item fxFlex="25" fxLayoutAlign="end start">{{networkInfo?.max_channel_size | number}}</mat-list-item></ng-template>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="65" fxLayoutAlign="start start" *ngIf="settings?.satsToBTC; else smallerUnit8">Avg Channel Size ({{information?.currency_unit}})</mat-list-item>
<ng-template #smallerUnit8><mat-list-item fxFlex="65" fxLayoutAlign="start start">Avg Channel Size ({{information?.smaller_currency_unit}})</mat-list-item></ng-template>
<mat-list-item fxFlex="25" fxLayoutAlign="end start" *ngIf="settings?.satsToBTC; else smallerData8">{{networkInfo?.btc_avg_channel_size | number}}</mat-list-item>
<ng-template #smallerData8><mat-list-item fxFlex="25" fxLayoutAlign="end start">{{networkInfo?.avg_channel_size | number:'1.0-2'}}</mat-list-item></ng-template>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="65" fxLayoutAlign="start start" *ngIf="settings?.satsToBTC; else smallerUnit9">Min Channel Size ({{information?.currency_unit}})</mat-list-item>
<ng-template #smallerUnit9><mat-list-item fxFlex="65" fxLayoutAlign="start start">Min Channel Size ({{information?.smaller_currency_unit}})</mat-list-item></ng-template>
<mat-list-item fxFlex="25" fxLayoutAlign="end start" *ngIf="settings?.satsToBTC; else smallerData9">{{networkInfo?.btc_min_channel_size | number}}</mat-list-item>
<ng-template #smallerData9><mat-list-item fxFlex="25" fxLayoutAlign="end start">{{networkInfo?.min_channel_size | number}}</mat-list-item></ng-template>
<mat-divider></mat-divider>
</mat-list>
</div>
<mat-progress-bar *ngIf="flgLoading[4]===true" mode="indeterminate"></mat-progress-bar>
<mat-divider></mat-divider>
</mat-card-content>
</mat-card>
</div>
</div>
<ng-template #withoutData><h3>Sats</h3></ng-template>

@ -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;
}

@ -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<HomeComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HomeComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<Boolean | 'error'> = [true, true, true, true, true, true, true, true]; // 0: Info, 1: Fee, 2: Wallet, 3: Channel, 4: Network
private unsub: Array<Subject<void>> = [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<fromRTLReducer.State>) {
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();
});
}
}

@ -0,0 +1,77 @@
<div fxLayout="column">
<div class="padding-gap">
<mat-card>
<mat-card-header>
<mat-card-subtitle>
<h2>Invoices</h2>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<form fxLayout="column" fxLayout.gt-sm="row wrap" (ngSubmit)="addInvoiceForm.form.valid && onAddInvoice(addInvoiceForm)" #addInvoiceForm="ngForm">
<mat-form-field fxFlex="60" fxLayoutAlign="start end">
<input matInput [(ngModel)]="memo" placeholder="Memo" tabindex="1" name="memo">
</mat-form-field>
<mat-form-field fxFlex="20" fxLayoutAlign="start end">
<input matInput [(ngModel)]="invoiceValue" placeholder="Invoice Value ({{information?.smaller_currency_unit}})" type="number" step="100" min="1" tabindex="2" name="invoiceValue">
</mat-form-field>
<div fxFlex="10" fxLayoutAlign="start start">
<button fxFlex="90" fxLayoutAlign="center center" mat-raised-button color="primary" type="submit" tabindex="3">Add</button>
</div>
<div fxFlex="10" fxLayoutAlign="start start">
<button fxFlex="90" fxLayoutAlign="center center" mat-raised-button color="accent" tabindex="4" type="reset" (click)="resetData()">Clear</button>
</div>
</form>
</mat-card-content>
</mat-card>
</div>
<div class="padding-gap">
<mat-card>
<mat-card-content class="table-card-content">
<div fxLayout="row" fxLayoutAlign="start start">
<mat-form-field fxFlex="30">
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
</mat-form-field>
</div>
<div perfectScrollbar class="table-container mat-elevation-z8">
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
<table mat-table #table [dataSource]="invoices" matSort [ngClass]="{'mat-elevation-z8 overflow-auto error-border': flgLoading[0]==='error','mat-elevation-z8 overflow-auto': true}">
<ng-container matColumnDef="creation_date">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Creation Date </th>
<td mat-cell *matCellDef="let invoice">{{invoice.creation_date_str}}</td>
</ng-container>
<ng-container matColumnDef="settle_date">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Settle Date </th>
<td mat-cell *matCellDef="let invoice">{{invoice.settle_date_str}}</td>
</ng-container>
<ng-container matColumnDef="memo">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Memo </th>
<td mat-cell *matCellDef="let invoice">{{invoice.memo}}</td>
</ng-container>
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Value ({{(settings?.satsToBTC) ? information?.currency_unit : information?.smaller_currency_unit}}) </th>
<td mat-cell *matCellDef="let invoice"><span fxLayoutAlign="end center"> {{(settings?.satsToBTC) ? (invoice?.btc_value | number:'1.0-3') : (invoice?.value | number)}} </span></td>
</ng-container>
<ng-container matColumnDef="settled">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Settled </th>
<td mat-cell *matCellDef="let invoice"> {{invoice.settled}} </td>
</ng-container>
<ng-container matColumnDef="expiry">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Expiry (Sec)</th>
<td mat-cell *matCellDef="let invoice"><span fxLayoutAlign="end center"> {{invoice.expiry | number}} </span></td>
</ng-container>
<ng-container matColumnDef="cltv_expiry">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> CLTV Expiry </th>
<td mat-cell *matCellDef="let invoice"><span fxLayoutAlign="end center"> {{invoice.cltv_expiry | number}} </span></td>
</ng-container>
<ng-container matColumnDef="amt_paid_sat">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Amount Paid ({{(settings?.satsToBTC) ? information?.currency_unit : information?.smaller_currency_unit}})</th>
<td mat-cell *matCellDef="let invoice"><span fxLayoutAlign="end center"> {{(settings?.satsToBTC) ? (invoice?.btc_amt_paid_sat | number:'1.0-3') : (invoice?.amt_paid_sat | number)}} </span></td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [@newlyAddedRowAnimation]="(row.memo === newlyAddedInvoiceMemo && row.value === newlyAddedInvoiceValue && flgAnimate) ? 'added' : 'notAdded'" (click)="onInvoiceClick(row, $event)"></tr>
</table>
</div>
</mat-card-content>
</mat-card>
</div>
</div>

@ -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;
}
}

@ -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<Boolean | 'error'> = [true];
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.State>, 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<Invoice>([...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();
});
}
}

@ -0,0 +1,3 @@
.mat-list-base .mat-list-item, .mat-list-base .mat-list-option {
height: 38px !important;
}

@ -0,0 +1,109 @@
<div fxLayout="column" fxLayout.gt-sm="row wrap">
<mat-card fxFlex="100" fxLayoutAlign="start start">
<mat-card-content fxFlex="100" *ngIf="lookupResult">
<mat-list fxLayoutAlign="space-between start">
<mat-list-item fxFlex="30" fxLayoutAlign="start start">Channel Id</mat-list-item>
<mat-list-item fxFlex="68" fxLayoutAlign="start start">{{lookupResult.channel_id}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="space-between start">
<mat-list-item fxFlex="30" fxLayoutAlign="start start">Channel Point</mat-list-item>
<mat-list-item fxFlex="68" fxLayoutAlign="start start" class="word-break">{{lookupResult.chan_point}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="space-between start">
<mat-list-item fxFlex="30" fxLayoutAlign="start start">Last Update</mat-list-item>
<mat-list-item fxFlex="68" fxLayoutAlign="start start">{{lookupResult.last_update_str}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="space-between start">
<mat-list-item fxFlex="30" fxLayoutAlign="start start">Capacity (Sats)</mat-list-item>
<mat-list-item fxFlex="68" fxLayoutAlign="start start">{{lookupResult.capacity | number}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
</mat-card-content>
</mat-card>
</div>
<div fxLayout="column" fxLayoutAlign="space-between start" fxLayout.gt-sm="row wrap" class="mt-2">
<div fxFlex="48">
<mat-card class="custom-card mat-elevation-z12">
<mat-card-header class="bg-primary" fxLayoutAlign="center center">
<mat-card-title class="m-0 pt-2">
<h5 *ngIf="!node1_match">Node 1</h5>
<h5 *ngIf="node1_match">Node 1 (Your Node)</h5>
</mat-card-title>
</mat-card-header>
<mat-card-content class="px-2">
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="100" fxLayoutAlign="start start" class="word-break">{{lookupResult.node1_pub}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="50" fxLayoutAlign="start start">Time Lock Delta</mat-list-item>
<mat-list-item fxFlex="50" fxLayoutAlign="start start">{{lookupResult.node1_policy.time_lock_delta}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="50" fxLayoutAlign="start start">Min HTLC</mat-list-item>
<mat-list-item fxFlex="50" fxLayoutAlign="start start">{{lookupResult.node1_policy.min_htlc}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="50" fxLayoutAlign="start start">Fee Base Msat</mat-list-item>
<mat-list-item fxFlex="50" fxLayoutAlign="start start">{{lookupResult.node1_policy.fee_base_msat}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="50" fxLayoutAlign="start start">Fee Rate Milli Msat</mat-list-item>
<mat-list-item fxFlex="50" fxLayoutAlign="start start">{{lookupResult.node1_policy.fee_rate_milli_msat}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="50" fxLayoutAlign="start start">Disabled</mat-list-item>
<mat-list-item fxFlex="50" fxLayoutAlign="start start">{{lookupResult.node1_policy.disabled}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
</mat-card-content>
</mat-card>
</div>
<div fxFlex="48">
<mat-card class="custom-card mat-elevation-z12">
<mat-card-header class="bg-primary" fxLayoutAlign="center center">
<mat-card-title class="m-0 pt-2">
<h5 *ngIf="!node2_match">Node 2</h5>
<h5 *ngIf="node2_match">Node 2 (Your Node)</h5>
</mat-card-title>
</mat-card-header>
<mat-card-content class="px-2">
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="100" fxLayoutAlign="start start" class="word-break">{{lookupResult.node2_pub}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="50" fxLayoutAlign="start start">Time Lock Delta</mat-list-item>
<mat-list-item fxFlex="50" fxLayoutAlign="start start">{{lookupResult.node2_policy.time_lock_delta}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="50" fxLayoutAlign="start start">Min HTLC</mat-list-item>
<mat-list-item fxFlex="50" fxLayoutAlign="start start">{{lookupResult.node2_policy.min_htlc}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="50" fxLayoutAlign="start start">Fee Base Msat</mat-list-item>
<mat-list-item fxFlex="50" fxLayoutAlign="start start">{{lookupResult.node2_policy.fee_base_msat}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="50" fxLayoutAlign="start start">Fee Rate Milli Msat</mat-list-item>
<mat-list-item fxFlex="50" fxLayoutAlign="start start">{{lookupResult.node2_policy.fee_rate_milli_msat}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="50" fxLayoutAlign="start start">Disabled</mat-list-item>
<mat-list-item fxFlex="50" fxLayoutAlign="start start">{{lookupResult.node2_policy.disabled}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
</mat-card-content>
</mat-card>
</div>

@ -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<ChannelLookupComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ChannelLookupComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ChannelLookupComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private store: Store<fromRTLReducer.State>) { }
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;
}
});
}
}

@ -0,0 +1,47 @@
<div fxLayout="column">
<div class="padding-gap">
<mat-card>
<mat-card-header>
<mat-card-subtitle>
<h2>Lookups</h2>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<form fxLayout="column" fxLayout.gt-sm="row wrap" #form="ngForm">
<mat-form-field fxFlex="20" fxLayoutAlign="start end">
<mat-select [(ngModel)]="selectedField" placeholder="Lookup Field" (selectionChange)="onSelectChange($event)" tabindex="1" required name="lookupField">
<mat-option *ngFor="let lookupField of lookupFields" [value]="lookupField">
{{lookupField.name}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="50" fxLayoutAlign="start end">
<input matInput name="lookupKey" [placeholder]="selectedField?.placeholder || 'Lookup Key'" (change)="clearLookupValue()" [(ngModel)]="lookupKey" tabindex="2" required>
</mat-form-field>
<div fxFlex="12" fxLayoutAlign="start start">
<button fxFlex="90" fxLayoutAlign="center center" mat-raised-button color="primary" tabindex="3" type="submit" (click)="onLookup()" [disabled]="!form.valid">Lookup</button>
</div>
<div fxFlex="12" fxLayoutAlign="start start">
<button fxFlex="90" fxLayoutAlign="center center" mat-raised-button color="accent" tabindex="4" type="reset" (click)="resetData()">Clear</button>
</div>
</form>
</mat-card-content>
</mat-card>
</div>
<div class="padding-gap" *ngIf="lookupValue && flgSetLookupValue">
<mat-card [ngClass]="{'error-border': flgLoading[0]==='error'}">
<mat-card-header>
<mat-card-subtitle>
<h2>{{selectedField.name}} Details</h2>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div [ngSwitch]="selectedField.id">
<span *ngSwitchCase="0"><rtl-node-lookup [lookupResult]="lookupValue"></rtl-node-lookup></span>
<span *ngSwitchCase="1"><rtl-channel-lookup [lookupResult]="lookupValue"></rtl-channel-lookup></span>
<span *ngSwitchDefault><h3>Error! Unable to find details!</h3></span>
</div>
</mat-card-content>
</mat-card>
</div>
</div>

@ -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;
}

@ -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<LookupsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LookupsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LookupsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<Boolean | 'error'> = [true];
private unSubs: Array<Subject<void>> = [new Subject()];
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.State>, 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();
});
}
}

@ -0,0 +1,7 @@
.mat-table {
width:99%;
}
.mat-list-base .mat-list-item, .mat-list-base .mat-list-option {
height: 38px !important;
}

@ -0,0 +1,56 @@
<div fxLayout="column">
<div class="padding-gap">
<mat-card>
<mat-card-content *ngIf="lookupResult">
<div fxLayout="column">
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="50" fxLayoutAlign="start start">Alias</mat-list-item>
<mat-list-item fxFlex="40" fxLayoutAlign="start start">{{lookupResult.node.alias}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="50" fxLayoutAlign="start start">Pub Key</mat-list-item>
<mat-list-item fxFlex="40" fxLayoutAlign="start start">{{lookupResult.node.pub_key}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="50" fxLayoutAlign="start start">Color</mat-list-item>
<mat-list-item fxFlex="40" fxLayoutAlign="start start"><span [ngStyle]="{'background-color': lookupResult.node?.color}">{{lookupResult.node?.color}}</span></mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="50" fxLayoutAlign="start start">Last Update</mat-list-item>
<mat-list-item fxFlex="40" fxLayoutAlign="start start">{{lookupResult.node.last_update_str}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="50" fxLayoutAlign="start start">Total Capacity (Sats)</mat-list-item>
<mat-list-item fxFlex="40" fxLayoutAlign="start start">{{lookupResult.total_capacity | number}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayoutAlign="start start">
<mat-list-item fxFlex="50" fxLayoutAlign="start start">Number of Channels</mat-list-item>
<mat-list-item fxFlex="40" fxLayoutAlign="start start">{{lookupResult.num_channels | number}}</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<mat-list fxLayout="column" fxLayoutAlign="start start">
<mat-divider></mat-divider>
<mat-list-item fxFlex="100" fxLayoutAlign="start start">Addresses</mat-list-item>
<mat-table [dataSource]="lookupResult.node.addresses" matSort class="mat-elevation-z8 overflow-auto">
<ng-container matColumnDef="network">
<mat-header-cell *matHeaderCellDef mat-sort-header>Network</mat-header-cell>
<mat-cell *matCellDef="let address"><div>{{address?.network}}</div></mat-cell>
</ng-container>
<ng-container matColumnDef="addr">
<mat-header-cell *matHeaderCellDef mat-sort-header>Address</mat-header-cell>
<mat-cell *matCellDef="let address"><div>{{address?.addr}}</div></mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns;"></mat-header-row>
<mat-row fxLayoutAlign="stretch stretch" *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
</mat-list>
</div>
</mat-card-content>
</mat-card>
</div>
</div>

@ -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<NodeLookupComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ NodeLookupComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(NodeLookupComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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');
}
}
}

@ -0,0 +1,19 @@
<div fxLayout="row" fxLayoutAlign="start center">
<div *ngFor="let menuNode of menuNodes">
<button mat-button *ngIf="undefined === menuNode.children" class="horizontal-button" routerLinkActive="h-active-link" [routerLinkActiveOptions]="{exact: true}" routerLink="{{menuNode.link}}" matTooltip="{{menuNode.name}}" matTooltipPosition="above" (click)="onClick(menuNode)">
<mat-icon class="mat-icon-36">{{menuNode.icon}}</mat-icon>
</button>
<div *ngIf="undefined !== menuNode.children" fxLayoutAlign="center center">
<button mat-button class="horizontal-button" matTooltip="{{menuNode.name}}" matTooltipPosition="above">
<mat-icon [matMenuTriggerFor]="childMenu" class="mat-icon-36">{{menuNode.icon}}</mat-icon>
</button>
<mat-menu #childMenu="matMenu" xPosition="after" overlapTrigger="false" class="child-menu">
<div *ngFor="let childNode of menuNode.children">
<button mat-button class="horizontal-button bg-primary p-0" fxFlex="100" [routerLinkActive]="'h-active-link'" routerLink="{{childNode.link}}" [routerLinkActiveOptions]="{exact: true}">
<mat-icon matTooltip="{{childNode.name}}" matTooltipPosition="after" class="mat-icon-36">{{childNode.icon}}<span *ngIf="childNode.name === 'Pending'" [matBadgeHidden]="numPendingChannels<1" matBadge="{{numPendingChannels}}" matBadgeOverlap="false" matBadgeColor="accent"></span></mat-icon>
</button>
</div>
</mat-menu>
</div>
</div>
</div>

@ -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;
}
}
}
}

@ -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<HorizontalNavigationComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HorizontalNavigationComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HorizontalNavigationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<fromRTLReducer.State>, 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());
}
});
}
}
}

@ -0,0 +1,65 @@
<mat-toolbar color="primary" [fxLayoutAlign] = "settings.menuType === 'Mini' ? 'center center' : 'space-between center'">
<a *ngIf="settings.menuType === 'Mini'" routerLink="/home" class="logo padding-gap-x mat-elevation-z6">R</a>
<a *ngIf="settings.menuType !== 'Mini'" routerLink="/home" class="logo">RTL</a>
<svg *ngIf="settings.menuType !== 'Mini' && !smallScreen" style="width:24px;height:24px;cursor:pointer;" viewBox="0 0 24 24"
(click)="settings.flgSidenavPinned = !settings.flgSidenavPinned">
<path fill="currentColor" *ngIf="!settings.flgSidenavPinned" d="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z" />
<path fill="currentColor" *ngIf="settings.flgSidenavPinned" d="M2,5.27L3.28,4L20,20.72L18.73,22L12.8,16.07V22H11.2V16H6V14L8,12V11.27L2,5.27M16,12L18,14V16H17.82L8,6.18V4H7V2H17V4H16V12Z" />
</svg>
</mat-toolbar>
<div fxLayout="row" fxLayoutAlign="start center" class="lnd-info pl-2" *ngIf="settings.menuType !== 'Mini'">
<div fxLayout="column">
<p class="name">Alias: <mat-spinner [diameter]="20" *ngIf="flgLoading" class="inline-spinner"></mat-spinner>{{information?.alias}}</p>
<p>Chain: <mat-spinner [diameter]="20" *ngIf="flgLoading" class="inline-spinner"></mat-spinner>{{informationChain.chain | titlecase}}<span> [{{informationChain.network | titlecase}}]</span></p>
<p class="name">LND Version: <mat-spinner [diameter]="20" *ngIf="flgLoading" class="inline-spinner"></mat-spinner>{{information?.version}}</p>
</div>
</div>
<mat-tree [dataSource]="navMenus" [treeControl]="treeControl" *ngIf="settings.menuType !== 'Compact'">
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle (click)="onChildNavClicked(node)" matTreeNodePadding [matTreeNodePaddingIndent]="settings.menuType === 'Mini' ? 28 : 40" routerLinkActive="active-link" routerLink="{{node.link}}">
<mat-icon class="mr-1" matTooltip="{{node.name}}" matTooltipPosition="right" [matTooltipDisabled]="settings.menuType !== 'Mini'">{{node.icon}}<span *ngIf="node.name === 'Pending' && settings.menuType === 'Mini'" [matBadgeHidden]="numPendingChannels<1" matBadge="{{numPendingChannels}}" matBadgeOverlap="false" matBadgeColor="accent"></span></mat-icon>
<span *ngIf="settings.menuType !== 'Mini'">{{node.name}}<span *ngIf="node.name === 'Pending'" [matBadgeHidden]="numPendingChannels<1" matBadge="{{numPendingChannels}}" matBadgeOverlap="false" matBadgeColor="accent"></span></span>
</mat-tree-node>
<mat-tree-node *matTreeNodeDef="let node;when: hasChild" matTreeNodePadding>
<div fxLayout="row" fxFlex="100" fxLayoutAlign="start center" matTreeNodeToggle (click)="toggleTree(node)">
<div fxFlex="89" fxLayoutAlign="start center">
<mat-icon class="mr-1" matTooltip="{{node.name}}" matTooltipPosition="right" [matTooltipDisabled]="settings.menuType !== 'Mini'">{{node.icon}}</mat-icon><span *ngIf="settings.menuType !== 'Mini'">{{node.name}}</span>
</div>
<button fxFlex="11" fxLayoutAlign="end center" mat-icon-button matTreeNodeToggle [attr.aria-label]="'toggle ' + node.name" fxLayoutAlign="end center">
<mat-icon class="mat-icon-rtl-mirror"> {{treeControl.isExpanded(node) ? 'arrow_drop_up' : 'arrow_drop_down'}}</mat-icon>
</button>
</div>
</mat-tree-node>
</mat-tree>
<mat-tree [dataSource]="navMenusLogout" [treeControl]="treeControlLogout" *ngIf="settings.menuType !== 'Compact' && showLogout" class="mt-minus-1">
<mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding [matTreeNodePaddingIndent]="settings.menuType === 'Mini' ? 28 : 40" (click)="onClick(node)">
<mat-icon class="mr-1" matTooltip="{{node.name}}" matTooltipPosition="right" [matTooltipDisabled]="settings.menuType !== 'Mini'">{{node.icon}}</mat-icon>
<span *ngIf="settings.menuType !== 'Mini'">{{node.name}}</span>
</mat-tree-node>
</mat-tree>
<mat-tree [dataSource]="navMenus" [treeControl]="treeControl" *ngIf="settings.menuType === 'Compact'">
<mat-tree-node fxLayout="column" fxLayoutAlign="center center" *matTreeNodeDef="let node" matTreeNodeToggle (click)="onChildNavClicked(node)" matTreeNodePadding matTreeNodePaddingIndent="60" routerLinkActive="active-link" routerLink="{{node.link}}">
<mat-icon class="mat-icon-36">{{node.icon}}</mat-icon>
<span>{{node.name}}<span *ngIf="node.name === 'Pending'" [matBadgeHidden]="numPendingChannels<1" matBadge="{{numPendingChannels}}" matBadgeOverlap="false" matBadgeColor="accent"></span></span>
</mat-tree-node>
<mat-tree-node fxLayout="row" *matTreeNodeDef="let node;when: hasChild" matTreeNodePadding>
<div class="ml-8" fxLayout="column" fxLayoutAlign="center center" matTreeNodeToggle (click)="toggleTree(node)">
<mat-icon class="mat-icon-36">{{node.icon}}</mat-icon>
<span>{{node.name}}</span>
</div>
<div fxLayout="column" fxLayoutAlign="center center" matTreeNodeToggle (click)="toggleTree(node)">
<button mat-icon-button [attr.aria-label]="'toggle ' + node.name" matTreeNodeToggle>
<mat-icon class="mat-icon-rtl-mirror"> {{treeControl.isExpanded(node) ? 'arrow_drop_up' : 'arrow_drop_down'}}</mat-icon>
</button>
</div>
</mat-tree-node>
</mat-tree>
<mat-tree [dataSource]="navMenusLogout" [treeControl]="treeControlLogout" *ngIf="settings.menuType === 'Compact' && showLogout" class="mt-minus-1">
<mat-tree-node fxLayout="column" fxLayoutAlign="center center" *matTreeNodeDef="let node" matTreeNodePadding matTreeNodePaddingIndent="60" (click)="onClick(node)">
<mat-icon class="mat-icon-36">{{node.icon}}</mat-icon>
<span>{{node.name}}</span>
</mat-tree-node>
</mat-tree>

@ -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<SideNavigationComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SideNavigationComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SideNavigationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<any>();
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<FlatMenuNode>;
treeControlLogout: FlatTreeControl<FlatMenuNode>;
treeFlattener: MatTreeFlattener<MenuNode, FlatMenuNode>;
treeFlattenerLogout: MatTreeFlattener<MenuNode, FlatMenuNode>;
navMenus: MatTreeFlatDataSource<MenuNode, FlatMenuNode>;
navMenusLogout: MatTreeFlatDataSource<MenuNode, FlatMenuNode>;
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.State>, 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<FlatMenuNode>(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<FlatMenuNode>(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 = <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<MenuNode[]> { 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();
});
}
}

@ -0,0 +1,22 @@
<mat-menu #topMenu="matMenu" [overlapTrigger]="false" class="top-menu">
<p mat-menu-item>
<mat-icon>publish</mat-icon>
<span>Version: {{version}}</span>
</p>
<a mat-menu-item routerLink="/sconfig">
<mat-icon>perm_data_setting</mat-icon>
<span routerLink="/sconfig">Node Config</span>
</a>
<a mat-menu-item routerLink="/help">
<mat-icon>help</mat-icon>
<span routerLink="/help">Help</span>
</a>
<a *ngIf="showLogout" mat-menu-item (click)="onClick()">
<mat-icon>eject</mat-icon>
<span>Logout</span>
</a>
</mat-menu>
<button mat-icon-button [matMenuTriggerFor]="topMenu">
<mat-icon>account_circle</mat-icon>
</button>

@ -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;
}
}
}
}

@ -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<TopMenuComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ TopMenuComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TopMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<fromRTLReducer.State>, 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 = <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();
});
}
}

@ -0,0 +1,81 @@
<div fxLayout="column">
<div class="padding-gap">
<mat-card>
<mat-card-header>
<mat-card-subtitle>
<h2>Verify and Send Payments</h2>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<form fxLayout="column" fxLayoutAlign="space-between stretch" fxLayout.gt-md="row wrap" #sendPaymentForm="ngForm">
<div fxFlex="69" fxLayoutAlign="space-between stretch">
<mat-form-field class="w-100">
<input matInput placeholder="Payment Request" name="paymentRequest" [(ngModel)]="paymentRequest" tabindex="1" required #paymentReq="ngModel">
</mat-form-field>
</div>
<div fxFlex="30" fxLayoutAlign="space-between stretch">
<button fxFlex="48" fxLayoutAlign="center center" mat-raised-button color="primary" [disabled]="paymentReq.invalid" (click)="onSendPayment()" tabindex="2">
<p *ngIf="paymentReq.invalid && (paymentReq.dirty || paymentReq.touched); else sendText">Invalid Req</p>
<ng-template #sendText><p>Send Payment</p></ng-template>
</button>
<button fxFlex="48" mat-raised-button color="accent" type="reset" tabindex="3" type="reset" (click)="resetData()">Clear</button>
</div>
</form>
</mat-card-content>
</mat-card>
</div>
<div class="padding-gap">
<mat-card>
<mat-card-content class="table-card-content">
<div fxLayout="row" fxLayoutAlign="start start">
<mat-form-field fxFlex="30">
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
</mat-form-field>
</div>
<div perfectScrollbar class="table-container mat-elevation-z8">
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
<table mat-table #table [dataSource]="payments" matSort [ngClass]="{'mat-elevation-z8 overflow-auto error-border': flgLoading[0]==='error','mat-elevation-z8 overflow-auto': true}">
<ng-container matColumnDef="creation_date">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Creation Date</th>
<td mat-cell *matCellDef="let payment">{{payment?.creation_date_str}}</td>
</ng-container>
<ng-container matColumnDef="payment_hash">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Payment Hash</th>
<td mat-cell *matCellDef="let payment">
<div>{{payment?.payment_hash | slice:0:10}}...</div>
</td>
</ng-container>
<ng-container matColumnDef="fee">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Fee</th>
<td mat-cell *matCellDef="let payment"><span fxLayoutAlign="end center">{{payment?.fee | number}}</span></td>
</ng-container>
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Value</th>
<td mat-cell *matCellDef="let payment"><span fxLayoutAlign="end center">{{payment?.value | number}}</span></td>
</ng-container>
<ng-container matColumnDef="payment_preimage">
<th mat-header-cell class="pl-4" *matHeaderCellDef mat-sort-header>Payment Pre Image</th>
<td mat-cell class="pl-4" *matCellDef="let payment">
<div>{{payment?.payment_preimage | slice:0:10}}...</div>
</td>
</ng-container>
<ng-container matColumnDef="value_msat">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Value MSat</th>
<td mat-cell *matCellDef="let payment"><span fxLayoutAlign="end center">{{payment?.value_msat | number}}</span></td>
</ng-container>
<ng-container matColumnDef="value_sat">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Value Sat</th>
<td mat-cell *matCellDef="let payment"><span fxLayoutAlign="end center">{{payment?.value_sat | number}}</span></td>
</ng-container>
<ng-container matColumnDef="path">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Path</th>
<td mat-cell *matCellDef="let payment">{{payment?.path?.length || 0}} Hops</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [@newlyAddedRowAnimation]="(row.payment_hash === newlyAddedPayment && flgAnimate) ? 'added' : 'notAdded'" (click)="onPaymentClick(row, $event)"></tr>
</table>
</div>
</mat-card-content>
</mat-card>
</div>
</div>

@ -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;
}
}

@ -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<Boolean | 'error'> = [true];
public information: GetInfo = {};
public payments: any;
public paymentJSONArr: Payment[] = [];
public displayedColumns = [];
public paymentDecoded: PayRequest = {};
public paymentRequest = '';
private unsub: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.State>, 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<Payment>([...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();
});
}
}

@ -0,0 +1,87 @@
<div fxLayout="column">
<div class="padding-gap">
<mat-card>
<mat-card-header>
<mat-card-subtitle>
<h2>Add Peer</h2>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<form fxLayout="column" fxLayout.gt-sm="row wrap" (ngSubmit)="addPeerForm.form.valid && onAddPeer(addPeerForm)" #addPeerForm="ngForm">
<mat-form-field fxFlex="70" fxLayoutAlign="start end">
<input matInput placeholder="Lightning Address (pubkey OR pubkey@ip:port)" name="peerAddress" [(ngModel)]="peerAddress" tabindex="1" required #peerAdd="ngModel">
</mat-form-field>
<div fxFlex="15" fxLayoutAlign="start start">
<button fxFlex="90" fxLayoutAlign="center center" mat-raised-button color="primary" [disabled]="peerAdd.invalid" type="submit" tabindex="2">
<p *ngIf="peerAdd.invalid && (peerAdd.dirty || peerAdd.touched); else addText">Invalid Address</p>
<ng-template #addText><p>Add</p></ng-template>
</button>
</div>
<div fxFlex="15" fxLayoutAlign="start start">
<button fxFlex="90" fxLayoutAlign="center center" mat-raised-button color="accent" tabindex="2" type="reset" (click)="resetData()">Clear</button>
</div>
</form>
</mat-card-content>
</mat-card>
</div>
<div class="padding-gap">
<mat-card>
<mat-card-content class="table-card-content">
<div fxLayout="row" fxLayoutAlign="start start">
<mat-form-field fxFlex="30">
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
</mat-form-field>
</div>
<div perfectScrollbar class="table-container mat-elevation-z8">
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
<table mat-table #table [dataSource]="peers" matSort [ngClass]="{'mat-elevation-z8 overflow-x-auto error-border': flgLoading[0]==='error','mat-elevation-z8 overflow-x-auto': true}">
<ng-container matColumnDef="detach">
<th mat-header-cell *matHeaderCellDef>Detach</th>
<td mat-cell *matCellDef="let peer"><mat-icon color="accent" (click)="onPeerDetach(peer)">link_off</mat-icon></td>
</ng-container>
<ng-container matColumnDef="pub_key">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Pub Key </th>
<td mat-cell *matCellDef="let peer">
<div> {{peer?.pub_key | slice:0:10}}... </div>
</td>
</ng-container>
<ng-container matColumnDef="alias">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Alias </th>
<td mat-cell *matCellDef="let peer"> {{peer?.alias}} </td>
</ng-container>
<ng-container matColumnDef="address">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Address </th>
<td mat-cell *matCellDef="let peer"> {{peer?.address}} </td>
</ng-container>
<ng-container matColumnDef="bytes_sent">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Bytes Sent </th>
<td mat-cell *matCellDef="let peer"><span fxLayoutAlign="end center"> {{peer?.bytes_sent | number}} </span></td>
</ng-container>
<ng-container matColumnDef="bytes_recv">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Bytes Recv </th>
<td mat-cell *matCellDef="let peer"><span fxLayoutAlign="end center"> {{peer?.bytes_recv | number}} </span></td>
</ng-container>
<ng-container matColumnDef="sat_sent">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> {{information?.smaller_currency_unit}} Sent </th>
<td mat-cell *matCellDef="let peer"><span fxLayoutAlign="end center"> {{peer?.sat_sent | number}} </span></td>
</ng-container>
<ng-container matColumnDef="sat_recv">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> {{information?.smaller_currency_unit}} Recv </th>
<td mat-cell *matCellDef="let peer"><span fxLayoutAlign="end center"> {{peer?.sat_recv | number}} </span></td>
</ng-container>
<ng-container matColumnDef="inbound">
<th mat-header-cell class="pl-4" *matHeaderCellDef mat-sort-header> Inbound </th>
<td mat-cell class="pl-4" *matCellDef="let peer"> {{peer?.inbound}} </td>
</ng-container>
<ng-container matColumnDef="ping_time">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Ping </th>
<td mat-cell *matCellDef="let peer"><span fxLayoutAlign="end center"> {{peer?.ping_time | number}} </span></td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [@newlyAddedRowAnimation]="(row.pub_key === newlyAddedPeer && flgAnimate) ? 'added' : 'notAdded'" (click)="onPeerClick(row, $event)"></tr>
</table>
</div>
</mat-card-content>
</mat-card>
</div>
</div>

@ -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;
}
}

@ -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<PeersComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ PeersComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PeersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<Boolean | 'error'> = [true]; // 0: peers
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.State>, 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<Peer>([...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();
});
}
}

@ -0,0 +1,37 @@
<div fxLayout="column">
<div class="padding-gap">
<mat-card>
<mat-card-header>
<mat-card-subtitle>
<h2>Show Configurations</h2>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<form fxLayout="column" fxLayoutAlign="space-between stretch" fxLayout.gt-md="row wrap">
<mat-radio-group fxFlex="20" fxLayoutAlign="start" (change)="onSelectionChange($event)" class="mt-1 mb-1">
<mat-radio-button class="pr-5" value="lnd" *ngIf="showLND" [checked]="selectedNodeType=='lnd'">LND</mat-radio-button>
<mat-radio-button class="pr-5" value="bitcoind" *ngIf="showBitcoind" [checked]="selectedNodeType=='bitcoind'">BITCOIND</mat-radio-button>
<mat-radio-button class="pr-5" value="rtl" [checked]="selectedNodeType=='rtl'">RTL</mat-radio-button>
</mat-radio-group>
<div fxFlex="30" fxLayoutAlign="space-between stretch">
<button fxFlex="50" fxLayoutAlign="center center" mat-raised-button color="primary" (click)="onShowConfig()" tabindex="2">Show Config</button>
<button fxFlex="50" fxLayoutAlign="center center" mat-raised-button color="accent" tabindex="3" type="reset" class="ml-2" (click)="resetData()">Clear</button>
</div>
</form>
<div *ngIf="configData !== ''">
<mat-list>
<mat-list-item *ngFor="let conf of configData; index as i;">
<mat-card-subtitle class="my-1">
<h2 *ngIf="conf.indexOf('[') >= 0">{{conf}}</h2>
</mat-card-subtitle>
<mat-card-subtitle class="m-0">
<h4 *ngIf="conf.indexOf('[') < 0" class="ml-4">{{conf}}</h4>
</mat-card-subtitle>
<mat-divider [inset]="true" *ngIf="conf.indexOf('[') < 0"></mat-divider>
</mat-list-item>
</mat-list>
</div>
</mat-card-content>
</mat-card>
</div>
</div>

@ -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<Subject<void>> = [new Subject(), new Subject()];
constructor(private store: Store<fromRTLReducer.State>, 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();
});
}
}

@ -0,0 +1,22 @@
<div fxLayout="column">
<div class="padding-gap">
<mat-card>
<mat-card-header>
<mat-card-subtitle>
<h2>Login to RTL</h2>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<form (ngSubmit)="onSignin()" #signinForm="ngForm">
<mat-form-field fxFlex="50" fxLayoutAlign="start">
<input matInput placeholder="Password" type="password" id="password" name="password" [(ngModel)]="password" tabindex="1" required>
<mat-hint>{{hintStr}}</mat-hint>
</mat-form-field>
<button fxFlex="10" class="mr-2" fxLayoutAlign="center center" mat-raised-button color="primary" tabindex="2" type="submit" [disabled]="!password">Login</button>
<button fxFlex="10" fxLayoutAlign="center center" mat-raised-button color="accent" tabindex="3" type="reset" (click)="resetData()">Clear</button>
</form>
</mat-card-content>
</mat-card>
</div>
</div>

@ -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<Subject<void>> = [new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.State>) { }
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();
});
}
}

@ -0,0 +1,87 @@
<div fxLayout="column">
<div fxFlex="100" class="padding-gap">
<mat-card>
<mat-card-header>
<mat-card-subtitle>
<h2>Forwarding History</h2>
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<form fxLayout="column" fxLayoutAlign="space-between stretch" fxLayout.gt-md="row wrap"
(ngSubmit)="fhForm.form.valid && onForwardingHistoryFetch()" #fhForm="ngForm" class="padding-gap">
<div fxFlex="69" fxLayoutAlign="space-between stretch">
<mat-form-field fxFlex="49" fxLayoutAlign="start">
<input matInput [matDatepicker]="startDatepicker" placeholder="Start Date" [max]="yesterday"
name="startDate" [(ngModel)]="startDate" tabindex="1" #strtDate="ngModel">
<mat-datepicker-toggle matSuffix [for]="startDatepicker"></mat-datepicker-toggle>
<mat-datepicker #startDatepicker [startAt]="startDate"></mat-datepicker>
</mat-form-field>
<mat-form-field fxFlex="49" fxLayoutAlign="start">
<input matInput [matDatepicker]="endDatepicker" [max]="today" placeholder="End Date" name="endDate"
[(ngModel)]="endDate" tabindex="2" #enDate="ngModel">
<mat-datepicker-toggle matSuffix [for]="endDatepicker"></mat-datepicker-toggle>
<mat-datepicker #endDatepicker [startAt]="endDate"></mat-datepicker>
</mat-form-field>
</div>
<div fxFlex="30" fxLayoutAlign="space-between stretch">
<button fxFlex="50" fxLayoutAlign="center center" mat-raised-button color="primary"
[disabled]="fhForm.invalid" type="submit" tabindex="3">Fetch</button>
<button fxFlex="50" fxLayoutAlign="center center" mat-raised-button color="accent" class="ml-2" tabindex="4"
type="reset" (click)="resetData()">Clear</button>
</div>
</form>
</mat-card-content>
</mat-card>
</div>
<div class="padding-gap">
<mat-card>
<mat-card-content class="table-card-content">
<div perfectScrollbar class="table-container mat-elevation-z8">
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
<table mat-table #table [dataSource]="forwardingHistoryEvents" matSort
[ngClass]="{'mat-elevation-z8 overflow-auto error-border': flgLoading[0]==='error','mat-elevation-z8 overflow-auto': true}">
<ng-container matColumnDef="timestamp">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Timestamp</th>
<td mat-cell *matCellDef="let fhEvent">{{fhEvent.timestamp_str}}</td>
</ng-container>
<ng-container matColumnDef="chan_id_in">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Chan Id In</th>
<td mat-cell *matCellDef="let fhEvent">{{fhEvent.chan_id_in}}</td>
</ng-container>
<ng-container matColumnDef="alias_in">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Alias In</th>
<td mat-cell *matCellDef="let fhEvent">{{fhEvent.alias_in}}</td>
</ng-container>
<ng-container matColumnDef="chan_id_out">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Chan Id Out</th>
<td mat-cell *matCellDef="let fhEvent">{{fhEvent.chan_id_out}}</td>
</ng-container>
<ng-container matColumnDef="alias_out">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Alias Out</th>
<td mat-cell *matCellDef="let fhEvent">{{fhEvent.alias_out}}</td>
</ng-container>
<ng-container matColumnDef="amt_out">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Amount
Out (Sats)</th>
<td mat-cell *matCellDef="let fhEvent"><span fxLayoutAlign="end center">{{fhEvent.amt_out | number}}</span></td>
</ng-container>
<ng-container matColumnDef="amt_in">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Amount
In (Sats)</th>
<td mat-cell *matCellDef="let fhEvent"><span fxLayoutAlign="end center">{{fhEvent.amt_in | number}}</span></td>
</ng-container>
<ng-container matColumnDef="fee">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before">Fee
(Sats)</th>
<td mat-cell *matCellDef="let fhEvent"><span fxLayoutAlign="end center">{{fhEvent.fee | number}}</span></td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"
(click)="onForwardingEventClick(row, $event)"></tr>
</table>
</div>
</mat-card-content>
</mat-card>
</div>
</div>

@ -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;
}
}

@ -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<ForwardingHistoryComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ForwardingHistoryComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ForwardingHistoryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<Boolean | 'error'> = [true];
public today = new Date(Date.now());
public yesterday = new Date(this.today.getFullYear(), this.today.getMonth(), this.today.getDate() - 1, this.today.getHours(), this.today.getMinutes(), this.today.getSeconds());
public endDate = this.today;
public startDate = this.yesterday;
private unsub: Array<Subject<void>> = [new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.State>) {
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<ForwardingEvent>([...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();
});
}
}

@ -0,0 +1,57 @@
<div fxLayout="column">
<div class="padding-gap">
<mat-card>
<mat-card-header>
<mat-card-subtitle>
<h2>Transactions</h2>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content class="table-card-content">
<div fxLayout="row" fxLayoutAlign="start start">
<mat-form-field fxFlex="30">
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
</mat-form-field>
</div>
<div perfectScrollbar class="table-container mat-elevation-z8">
<mat-progress-bar *ngIf="flgLoading[0]===true" mode="indeterminate"></mat-progress-bar>
<table mat-table #table [dataSource]="listTransactions" matSort [ngClass]="{'mat-elevation-z8 overflow-auto error-border': flgLoading[0]==='error','mat-elevation-z8 overflow-auto': true}">
<ng-container matColumnDef="dest_addresses">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Destination Addresses</th>
<td mat-cell *matCellDef="let trans">{{trans?.dest_addresses?.length || 0}} Addr</td>
</ng-container>
<ng-container matColumnDef="time_stamp">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Timestamp </th>
<td mat-cell *matCellDef="let trans"> {{trans.time_stamp_str}}</td>
</ng-container>
<ng-container matColumnDef="num_confirmations">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Num Confirmations </th>
<td mat-cell *matCellDef="let trans"><span fxLayoutAlign="end center"> {{trans.num_confirmations | number}} </span></td>
</ng-container>
<ng-container matColumnDef="total_fees">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Total Fees </th>
<td mat-cell *matCellDef="let trans"><span fxLayoutAlign="end center"> {{trans.total_fees | number}} </span></td>
</ng-container>
<ng-container matColumnDef="block_hash">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Block Hash </th>
<td mat-cell *matCellDef="let trans"> {{trans.block_hash | slice:0:10}}...</td>
</ng-container>
<ng-container matColumnDef="block_height">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Block Height </th>
<td mat-cell *matCellDef="let trans"><span fxLayoutAlign="end center"> {{trans.block_height | number}} </span></td>
</ng-container>
<ng-container matColumnDef="tx_hash">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Txn Hash </th>
<td mat-cell *matCellDef="let trans"><div>{{trans.tx_hash | slice:0:10}}...</div></td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef mat-sort-header arrowPosition="before"> Amount </th>
<td mat-cell *matCellDef="let trans"><span fxLayoutAlign="end center"> {{trans.amount | number}} </span></td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" (click)="onTransactionClick(row, $event)"></tr>
</table>
</div>
</mat-card-content>
</mat-card>
</div>
</div>

@ -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;
}
}

@ -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<ListTransactionsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ListTransactionsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ListTransactionsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<Boolean | 'error'> = [true];
private unsub: Array<Subject<void>> = [new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.State>, 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<Transaction>([...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();
});
}
}

@ -0,0 +1,163 @@
<div fxLayout="column" fxLayoutAlign="center center" class="test-banner mx-1">
<h5>Don't be #reckless. #craefulgang #craefulgang #craefulgang.</h5>
</div>
<div fxLayout="column" fxLayout.gt-sm="row wrap">
<div fxFlex="34" class="padding-gap">
<mat-card [ngClass]="{'custom-card error-border': flgLoadingWallet==='error','custom-card': true}">
<mat-card-header class="bg-primary p-1" fxLayoutAlign="center center">
<mat-card-title class="m-0 pt-1">
<h5>Total Balance</h5>
<mat-progress-bar *ngIf="flgLoadingWallet===true" mode="indeterminate"></mat-progress-bar>
</mat-card-title>
</mat-card-header>
<mat-card-content fxLayout="column" fxLayoutAlign="center center">
<mat-card-content class="mt-1">
<svg style="width:70px;height:70px" viewBox="0 0 24 24">
<path fill="currentColor" d="M10,2H14A2,2 0 0,1 16,4V6H20A2,2 0 0,1 22,8V19A2,2 0 0,1 20,21H4C2.89,21 2,20.1 2,19V8C2,6.89 2.89,6 4,6H8V4C8,2.89 8.89,2 10,2M14,6V4H10V6H14Z" />
</svg>
</mat-card-content>
<span *ngIf="information?.currency_unit; else withoutData">
<h3 *ngIf="settings?.satsToBTC; else smallerUnit1">{{blockchainBalance?.btc_total_balance | number}} {{information?.currency_unit}}</h3>
<ng-template #smallerUnit1><h3>{{blockchainBalance?.total_balance | number}} {{information?.smaller_currency_unit}}</h3></ng-template>
</span>
</mat-card-content>
<mat-progress-bar class="mt-minus-5" *ngIf="flgLoadingWallet===true" mode="indeterminate"></mat-progress-bar>
<mat-divider></mat-divider>
</mat-card>
</div>
<div fxFlex="33" class="padding-gap">
<mat-card [ngClass]="{'custom-card error-border': flgLoadingWallet==='error','custom-card': true}">
<mat-card-header class="bg-primary p-1" fxLayoutAlign="center center">
<mat-card-title class="m-0 pt-1">
<h5>Confirmed Balance</h5>
<mat-progress-bar *ngIf="flgLoadingWallet===true" mode="indeterminate"></mat-progress-bar>
</mat-card-title>
</mat-card-header>
<mat-card-content fxLayout="column" fxLayoutAlign="center center">
<mat-card-content class="mt-1">
<svg style="width:70px;height:70px" viewBox="0 0 24 24">
<path fill="currentColor" d="M10,2H14A2,2 0 0,1 16,4V6H20A2,2 0 0,1 22,8V19A2,2 0 0,1 20,21H4A2,2 0 0,1 2,19V8A2,2 0 0,1 4,6H8V4A2,2 0 0,1 10,2M14,6V4H10V6H14M10.5,17.5L17.09,10.91L15.68,9.5L10.5,14.67L8.41,12.59L7,14L10.5,17.5Z" />
</svg>
</mat-card-content>
<span *ngIf="information?.currency_unit; else withoutData">
<h3 *ngIf="settings?.satsToBTC; else smallerUnit2">{{blockchainBalance?.btc_confirmed_balance | number}} {{information?.currency_unit}}</h3>
<ng-template #smallerUnit2><h3>{{blockchainBalance?.confirmed_balance | number}} {{information?.smaller_currency_unit}}</h3></ng-template>
</span>
</mat-card-content>
<mat-progress-bar class="mt-minus-5" *ngIf="flgLoadingWallet===true" mode="indeterminate"></mat-progress-bar>
<mat-divider></mat-divider>
</mat-card>
</div>
<div fxFlex="33" class="padding-gap">
<mat-card [ngClass]="{'custom-card error-border': flgLoadingWallet==='error','custom-card': true}">
<mat-card-header class="bg-primary p-1" fxLayoutAlign="center center">
<mat-card-title class="m-0 pt-1">
<h5>Unconfirmed Balance</h5>
<mat-progress-bar *ngIf="flgLoadingWallet===true" mode="indeterminate"></mat-progress-bar>
</mat-card-title>
</mat-card-header>
<mat-card-content fxLayout="column" fxLayoutAlign="center center">
<mat-card-content class="mt-1">
<svg style="width:70px;height:70px" viewBox="0 0 24 24">
<path fill="currentColor" d="M14,2A2,2 0 0,1 16,4V6H20A2,2 0 0,1 22,8L10.85,19C10.85,20.1 10.85,19.5 10.85,21H4C2.89,21 2,20.1 2,19V8C2,6.89 2.89,6 4,6H8V4C8,2.89 8.89,2 10,2H14M14,6V4H10V6H14M21.04,12.13C20.9,12.13 20.76,12.19 20.65,12.3L19.65,13.3L21.7,15.35L22.7,14.35C22.92,14.14 22.92,13.79 22.7,13.58L21.42,12.3C21.31,12.19 21.18,12.13 21.04,12.13M19.07,13.88L13,19.94V22H15.06L21.12,15.93L19.07,13.88Z" />
</svg>
</mat-card-content>
<span *ngIf="information?.currency_unit; else withoutData">
<h3 *ngIf="settings?.satsToBTC; else smallerUnit3">{{blockchainBalance?.btc_unconfirmed_balance | number}} {{information?.currency_unit}}</h3>
<ng-template #smallerUnit3><h3>{{blockchainBalance?.unconfirmed_balance | number}} {{information?.smaller_currency_unit}}</h3></ng-template>
</span>
</mat-card-content>
<mat-progress-bar class="mt-minus-5" *ngIf="flgLoadingWallet===true" mode="indeterminate"></mat-progress-bar>
<mat-divider></mat-divider>
</mat-card>
</div>
<div fxFlex="100" class="padding-gap">
<mat-card>
<mat-card-header>
<mat-card-subtitle>
<h2>Receive Funds</h2>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content class="top-minus-25px">
<div fxLayout="column" fxLayout.gt-sm="row wrap">
<div fxFlex="26" fxLayoutAlign="start end">
<mat-form-field fxFlex="99">
<mat-select [(ngModel)]="selectedAddress" placeholder="Address Type" name="address_type" tabindex="1">
<mat-option *ngFor="let addressType of addressTypes" [value]="addressType">
{{addressType.addressTp}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxFlex="15" fxLayoutAlign="start end">
<button fxFlex="99" fxLayoutAlign="center center" mat-raised-button color="primary" [disabled]="undefined === selectedAddress.addressId" (click)="onGenerateAddress()" tabindex="2" class="top-minus-15px">Generate Address</button>
</div>
<div fxFlex="7" fxLayoutAlign="start end">
<button fxFlex="99" fxLayoutAlign="center center" mat-raised-button color="accent" tabindex="3" (click)="resetReceiveData()" class="top-minus-15px">Clear</button>
</div>
<div fxFlex="34" fxLayoutAlign="start end">
<mat-form-field fxFlex="100">
<input matInput [value]="newAddress" readonly>
</mat-form-field>
</div>
<div fxFlex="14" fxLayoutAlign="center center">
<qrcode [qrdata]="newAddress" [size]="120" [level]="'L'" [allowEmptyString]="true" [ngStyle]="{'visibility': (newAddress === '') ? 'hidden' : 'visible'}" class="top-minus-5px qr-border"></qrcode>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
<div fxFlex="100" class="padding-gap">
<mat-card>
<mat-card-header>
<mat-card-subtitle>
<h2>Send Funds</h2>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div fxLayout="column" fxLayout.gt-sm="row wrap">
<div fxFlex="62" fxLayoutAlign="start end">
<mat-form-field fxFlex="99">
<input matInput [(ngModel)]="transaction.address" placeholder="{{information?.currency_unit}} Address" tabindex="4" name="address" #address="ngModel">
</mat-form-field>
</div>
<div fxFleax="38" fxLayoutAlign="start center">
<mat-radio-group fxFlex="100" fxLayoutAlign="space-between center" (change)="onOptionChange($event)" [(ngModel)]="flgCustomAmount">
<mat-radio-button fxFlex="35" value="0">Sweep All</mat-radio-button>
<mat-radio-button fxFlex="60" value="1">
<mat-form-field fxFlex="70"><input matInput [(ngModel)]="transaction.amount" (click)="onCustomClicked()" placeholder="Amount ({{information?.smaller_currency_unit}})" name="amount" type="number" step="100" min="0" tabindex="5" #amount="ngModel"></mat-form-field>
</mat-radio-button>
</mat-radio-group>
</div>
</div>
<div fxLayout="column" fxLayout.gt-sm="row wrap">
<div fxFlex="30" fxLayoutAlign="start start">
<mat-form-field fxFlex="99">
<mat-select [(value)]="selTransType">
<mat-option *ngFor="let transType of transTypes" [value]="transType.id">
{{transType.name}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxFlex="30" fxLayoutAlign="start start">
<mat-form-field fxFlex="99" *ngIf="selTransType=='1'">
<input matInput [(ngModel)]="transaction.blocks" placeholder="Target Confirmation Blocks" type="number" name="blocks" step="1" min="0" required tabindex="6" #blocks="ngModel">
</mat-form-field>
<mat-form-field fxFlex="99" *ngIf="selTransType=='2'">
<input matInput [(ngModel)]="transaction.fees" placeholder="Fee ({{information?.smaller_currency_unit}}/Byte)" type="number" name="fees" step="1" min="0" required tabindex="7" #fees="ngModel">
</mat-form-field>
</div>
<div fxFlex="40" fxLayoutAlign="space-between start">
<button fxFlex="48" fxLayoutAlign="center center" mat-raised-button color="primary" [disabled]="invalidValues" type="submit" tabindex="8" (click)="onSendFunds()">
<p *ngIf="invalidValues && (address.touched || address.dirty) && (amount.touched || amount.dirty); else sendText">Invalid Values</p>
<ng-template #sendText><p>Send</p></ng-template>
</button>
<button fxFlex="48" fxLayoutAlign="center center" mat-raised-button color="accent" tabindex="9" type="reset" (click)="resetData()">Clear</button>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
<ng-template #withoutData><h3>Sats</h3></ng-template>

@ -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<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.State>, 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();
});
}
}

@ -0,0 +1,21 @@
<div fxLayout="column">
<div class="padding-gap">
<mat-card>
<mat-card-header>
<mat-card-subtitle>
<h2>Unlock LND Wallet</h2>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<form fxLayout="row" fxFlex="70" fxLayoutAlign="space-between">
<mat-form-field fxFlex="65" fxLayoutAlign="start">
<input matInput type="password" placeholder="Password" name="walletPassword" [(ngModel)]="walletPassword" tabindex="1" required>
<mat-hint>Enter LND Wallet Password</mat-hint>
</mat-form-field>
<button mat-raised-button fxFlex="15" color="primary" [disabled]="walletPassword == ''" (click)="onOperateWallet('unlock')" tabindex="2">Unlock Wallet</button>
<button fxFlex="15" fxLayoutAlign="center center" mat-raised-button color="accent" tabindex="3" type="reset" (click)="resetData()">Clear</button>
</form>
</mat-card-content>
</mat-card>
</div>
</div>

@ -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<fromRTLReducer.State>) {}
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();
}
}

@ -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'))
])
];

@ -0,0 +1,35 @@
<div fxLayout="row">
<div class="w-100">
<mat-card-header [ngClass]="msgTypeBackground" fxLayoutAlign="end">
<h2 fxFlex="91">{{data.type}}</h2>
<mat-icon fxFlex="7" fxLayoutAlign="end" type="button" (click)="onClose()" class="cursor-pointer">close</mat-icon>
</mat-card-header>
<mat-card-content>
<div class="pb-2 p-2 wrap-text new-line">
<p *ngIf="data.titleMessage" fxLayoutAlign="start center" class="title-message pb-1 pl-1">{{data.titleMessage | titlecase}}</p>
<div *ngIf="messageObj?.length>0">
<div *ngFor="let obj of messageObj" fxLayout="row" fxLayoutAlign="center flex-start">
<div fxFlex="20">{{obj[0] | titlecase}}</div>
<div fxFlex="2">:</div>
<div [fxFlex]="showCopyOption(obj[0]) ? 60 : 75" class="pr-2">
<div *ngIf="isNumber(obj[1], obj[0]);else notNumberTemplate">
<span>{{obj[1] | number:'1.0-3'}}</span>
</div>
<ng-template #notNumberTemplate>
<span>{{obj[1]}}</span>
</ng-template>
<mat-icon *ngIf="showCopyOption(obj[0])" class="icon-small cursor-pointer pl-1 top-5px" rtlClipboard [payload]="obj[1]" (copied)="copiedText($event)">file_copy</mat-icon><span *ngIf="showCopyOption(obj[0])" [hidden]="!flgCopied">Copied</span>
</div>
<div *ngIf="showCopyOption(obj[0])" fxFlex="15" fxLayoutAlign="center center">
<qrcode [qrdata]="obj[1]" [size]="120" [level]="'L'" [allowEmptyString]="true" [ngStyle]="{'visibility': (obj[1] === '') ? 'hidden' : 'visible'}" class="mt-minus-40px qr-border"></qrcode>
</div>
</div>
</div>
</div>
<mat-divider class="pb-1"></mat-divider>
<div fxLayoutAlign="center">
<button mat-raised-button [color]="msgTypeForeground" fxFlex="30" class="mb-1" type="button" [mat-dialog-close]="false" default>OK</button>
</div>
</mat-card-content>
</div>
</div>

@ -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;
}

@ -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<AlertMessageComponent>, @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);
}
}

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

Loading…
Cancel
Save