diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..21968762 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = tab +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index 57154db7..4b4bf590 100644 --- a/.gitignore +++ b/.gitignore @@ -1,60 +1,43 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Typescript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -RTL.conf \ No newline at end of file +# See http://help.github.com/ignore-files/ for more about ignoring files. +# compiled output +/dist +/tmp +/out-tsc + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db + +RTL.conf +/logs +/cookies +RTL.log diff --git a/angular.json b/angular.json new file mode 100644 index 00000000..7668ef9f --- /dev/null +++ b/angular.json @@ -0,0 +1,125 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "RTLApp": { + "root": "", + "sourceRoot": "src", + "projectType": "application", + "prefix": "rtl", + "schematics": {}, + "targets": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "angular", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.app.json", + "assets": [ + "src/assets" + ], + "styles": [ + "src/app/shared/theme/styles/styles.scss" + ], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true + } + } + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "RTLApp:build" + }, + "configurations": { + "production": { + "browserTarget": "RTLApp:build:production" + } + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "RTLApp:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.spec.json", + "karmaConfig": "src/karma.conf.js", + "styles": [ + "src/app/shared/theme/styles/styles.scss" + ], + "scripts": [], + "assets": [ + "src/assets" + ] + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "src/tsconfig.app.json", + "src/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } + }, + "RTLApp-e2e": { + "root": "e2e/", + "projectType": "application", + "targets": { + "e2e": { + "builder": "@angular-devkit/build-angular:protractor", + "options": { + "protractorConfig": "e2e/protractor.conf.js", + "devServerTarget": "RTLApp:serve" + }, + "configurations": { + "production": { + "devServerTarget": "RTLApp:serve:production" + } + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": "e2e/tsconfig.e2e.json", + "exclude": [ + "**/node_modules/**" + ] + } + } + } + } + }, + "defaultProject": "RTLApp" +} \ No newline at end of file diff --git a/e2e/protractor.conf.js b/e2e/protractor.conf.js new file mode 100644 index 00000000..86776a39 --- /dev/null +++ b/e2e/protractor.conf.js @@ -0,0 +1,28 @@ +// Protractor configuration file, see link for more information +// https://github.com/angular/protractor/blob/master/lib/config.ts + +const { SpecReporter } = require('jasmine-spec-reporter'); + +exports.config = { + allScriptsTimeout: 11000, + specs: [ + './src/**/*.e2e-spec.ts' + ], + capabilities: { + 'browserName': 'chrome' + }, + directConnect: true, + baseUrl: 'http://localhost:4200/', + framework: 'jasmine', + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000, + print: function() {} + }, + onPrepare() { + require('ts-node').register({ + project: require('path').join(__dirname, './tsconfig.e2e.json') + }); + jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); + } +}; \ No newline at end of file diff --git a/e2e/src/app.e2e-spec.ts b/e2e/src/app.e2e-spec.ts new file mode 100644 index 00000000..1d80d64a --- /dev/null +++ b/e2e/src/app.e2e-spec.ts @@ -0,0 +1,14 @@ +import { AppPage } from './app.po'; + +describe('workspace-project App', () => { + let page: AppPage; + + beforeEach(() => { + page = new AppPage(); + }); + + it('should display welcome message', () => { + page.navigateTo(); + expect(page.getParagraphText()).toEqual('Welcome to RTLApp!'); + }); +}); diff --git a/e2e/src/app.po.ts b/e2e/src/app.po.ts new file mode 100644 index 00000000..82ea75ba --- /dev/null +++ b/e2e/src/app.po.ts @@ -0,0 +1,11 @@ +import { browser, by, element } from 'protractor'; + +export class AppPage { + navigateTo() { + return browser.get('/'); + } + + getParagraphText() { + return element(by.css('app-root h1')).getText(); + } +} diff --git a/e2e/tsconfig.e2e.json b/e2e/tsconfig.e2e.json new file mode 100644 index 00000000..a6dd6220 --- /dev/null +++ b/e2e/tsconfig.e2e.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "module": "commonjs", + "target": "es5", + "types": [ + "jasmine", + "jasminewd2", + "node" + ] + } +} \ No newline at end of file diff --git a/prebuild.js b/prebuild.js new file mode 100644 index 00000000..b05e3772 --- /dev/null +++ b/prebuild.js @@ -0,0 +1,12 @@ +const path = require('path'); +const fs = require('fs'); +const appVersion = require('./package.json').version; +const versionFilePath = path.join(__dirname + '/src/environments/version.ts'); +const versionStr = `export const VERSION = '${appVersion}';`; +fs.writeFile(versionFilePath, versionStr, { flat: 'w' }, function (err) { + if (err) { + return console.log(err); + } + console.log(`Updating application version ${appVersion}`); + console.log(`${'Writing version module to '}${versionFilePath}\n`); +}); diff --git a/product management/Channels Details.png b/product management/Channels Details.png new file mode 100644 index 00000000..b95029dd Binary files /dev/null and b/product management/Channels Details.png differ diff --git a/product management/Channels Lookup.png b/product management/Channels Lookup.png new file mode 100644 index 00000000..4dc94001 Binary files /dev/null and b/product management/Channels Lookup.png differ diff --git a/product management/Channels.png b/product management/Channels.png new file mode 100644 index 00000000..ace41cb5 Binary files /dev/null and b/product management/Channels.png differ diff --git a/product management/Home Page.png b/product management/Home Page.png new file mode 100644 index 00000000..91b8e6a7 Binary files /dev/null and b/product management/Home Page.png differ diff --git a/product management/Node Lookup.png b/product management/Node Lookup.png new file mode 100644 index 00000000..8c7b7e30 Binary files /dev/null and b/product management/Node Lookup.png differ diff --git a/product management/Peers.png b/product management/Peers.png new file mode 100644 index 00000000..00234fda Binary files /dev/null and b/product management/Peers.png differ diff --git a/product management/Start.png b/product management/Start.png new file mode 100644 index 00000000..d97d0a2c Binary files /dev/null and b/product management/Start.png differ diff --git a/product management/Unlock Wallet.png b/product management/Unlock Wallet.png new file mode 100644 index 00000000..a11ba3dd Binary files /dev/null and b/product management/Unlock Wallet.png differ diff --git a/product management/roadmap.md b/product management/roadmap.md new file mode 100644 index 00000000..d0b2ae4d --- /dev/null +++ b/product management/roadmap.md @@ -0,0 +1,44 @@ +# Product Roadmap for RTL Application + +## Version 0.0.1-alpha (Minimum Viable Product) + +Start +- Unlock Wallet + +Home Page +- Wallet Balance +- Channel Balance +- Channel Status +- Chain Sync Status +- Fee Report + +Peer Management +- Listing of Connected Peers +- Initiate Connection with peers with the public key + +Channel Management +- Status of Channels (Active, Inactive, Pending) +- Listing of Channels +- Open Channel with connected peers + +## Version 0.0.2 +Globalization - Allow for Language customization + +## Feature Backlog +Start +- Create Wallet + + Home Page +- Network Status + +Channel Management +- Channel Detail - Bi-Directional channel balance view +- Close Channel + +LN Wallet +- Generate pub key to recieve Bitcoin +- Send Bitcoin to an address + +Payments +- Decode payment request +- Send payment diff --git a/protractor.conf.js b/protractor.conf.js new file mode 100644 index 00000000..7ee3b5ee --- /dev/null +++ b/protractor.conf.js @@ -0,0 +1,28 @@ +// Protractor configuration file, see link for more information +// https://github.com/angular/protractor/blob/master/lib/config.ts + +const { SpecReporter } = require('jasmine-spec-reporter'); + +exports.config = { + allScriptsTimeout: 11000, + specs: [ + './e2e/**/*.e2e-spec.ts' + ], + capabilities: { + 'browserName': 'chrome' + }, + directConnect: true, + baseUrl: 'http://localhost:4200/', + framework: 'jasmine', + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000, + print: function() {} + }, + onPrepare() { + require('ts-node').register({ + project: 'e2e/tsconfig.e2e.json' + }); + jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); + } +}; diff --git a/src/app/app.component.html b/src/app/app.component.html new file mode 100644 index 00000000..50d2776a --- /dev/null +++ b/src/app/app.component.html @@ -0,0 +1,78 @@ +
+ + + + + +
+ +
+ +
+
+

Ride The Lightning (Beta)

+
+
+ +
+
+
+ vpn_key +
 {{information?.identity_pubkey}} + + file_copyCopied +
+
+ +
+ +
+
+
+ + +
+ +
+
+ settings +
+
+ + + +
+
+ +

Loading RTL...

+
+
diff --git a/src/app/app.component.scss b/src/app/app.component.scss new file mode 100644 index 00000000..3c613452 --- /dev/null +++ b/src/app/app.component.scss @@ -0,0 +1,4 @@ +.inline-spinner { + display: inline-flex !important; + top: 0px !important; +} diff --git a/src/app/app.component.ts b/src/app/app.component.ts new file mode 100644 index 00000000..d994d495 --- /dev/null +++ b/src/app/app.component.ts @@ -0,0 +1,166 @@ +import { Component, OnInit, AfterViewInit, OnDestroy, ViewChild, HostListener } from '@angular/core'; +import { Router } from '@angular/router'; +import { Subject } from 'rxjs'; +import { takeUntil, filter } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { Actions } from '@ngrx/effects'; +import { UserIdleService } from 'angular-user-idle'; + +import { LoggerService } from './shared/services/logger.service'; +import { Settings, Authentication } from './shared/models/RTLconfig'; +import { GetInfo } from './shared/models/lndModels'; + +import * as RTLActions from './shared/store/rtl.actions'; +import * as fromRTLReducer from './shared/store/rtl.reducers'; + +@Component({ + selector: 'rtl-app', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent implements OnInit, AfterViewInit, OnDestroy { + @ViewChild('sideNavigation') sideNavigation: any; + @ViewChild('settingSidenav') settingSidenav: any; + public information: GetInfo = {}; + public flgLoading: Array = [true]; // 0: Info + public flgCopied = false; + public settings: Settings; + public authSettings: Authentication; + public accessKey = ''; + public smallScreen = false; + unsubs: Array> = [new Subject(), new Subject(), new Subject()]; + + constructor(private logger: LoggerService, private store: Store, private actions$: Actions, + private userIdle: UserIdleService, private router: Router) {} + + ngOnInit() { + this.store.dispatch(new RTLActions.FetchSettings()); + this.accessKey = this.readAccessKey(); + this.store.select('rtlRoot') + .pipe(takeUntil(this.unsubs[0])) + .subscribe(rtlStore => { + this.settings = rtlStore.settings; + this.information = rtlStore.information; + this.authSettings = rtlStore.authSettings; + this.flgLoading[0] = (undefined !== this.information.identity_pubkey) ? false : true; + if (window.innerWidth <= 768) { + this.settings.menu = 'Vertical'; + this.settings.flgSidenavOpened = false; + this.settings.flgSidenavPinned = false; + } + if (window.innerWidth <= 414) { + this.smallScreen = true; + } + this.logger.info(this.settings); + if (!sessionStorage.getItem('token')) { + this.flgLoading[0] = false; + } + }); + if (sessionStorage.getItem('token')) { + this.store.dispatch(new RTLActions.FetchInfo()); + } + this.actions$ + .pipe( + takeUntil(this.unsubs[1]), + filter((action) => action.type === RTLActions.INIT_APP_DATA || action.type === RTLActions.SET_SETTINGS || action.type === RTLActions.SET_AUTH_SETTINGS) + ).subscribe((actionPayload: (RTLActions.InitAppData | RTLActions.SetSettings | RTLActions.SetAuthSettings)) => { + if (actionPayload.type === RTLActions.SET_AUTH_SETTINGS) { + if (!sessionStorage.getItem('token')) { + if (+actionPayload.payload.rtlSSO) { + this.store.dispatch(new RTLActions.Signin(window.btoa(this.accessKey))); + } else { + this.router.navigate([this.authSettings.logoutRedirectLink]); + } + } + } else if (actionPayload.type === RTLActions.INIT_APP_DATA) { + this.store.dispatch(new RTLActions.FetchInfo()); + } else if (actionPayload.type === RTLActions.SET_SETTINGS) { + if (this.settings.menu === 'Horizontal') { + this.settingSidenav.toggle(); // To dynamically update the width to 100% after side nav is closed + setTimeout(() => { this.settingSidenav.toggle(); }, 100); + } + } + }); + this.actions$ + .pipe( + takeUntil(this.unsubs[1]), + filter((action) => action.type === RTLActions.SET_INFO) + ).subscribe((infoData: RTLActions.SetInfo) => { + if (undefined !== infoData.payload.identity_pubkey) { + this.initializeRemainingData(); + } + }); + this.userIdle.startWatching(); + this.userIdle.onTimerStart().subscribe(count => {}); + this.userIdle.onTimeout().subscribe(() => { + if (sessionStorage.getItem('token')) { + this.logger.warn('Time limit exceeded for session inactivity! Logging out!'); + this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: { + type: 'WARN', + titleMessage: 'Time limit exceeded for session inactivity! Logging out!' + }})); + this.store.dispatch(new RTLActions.Signout()); + this.userIdle.resetTimer(); + } + }); + } + + private readAccessKey() { + const url = window.location.href; + return url.substring(url.lastIndexOf('/') + 1); + } + + initializeRemainingData() { + this.store.dispatch(new RTLActions.FetchPeers()); + this.store.dispatch(new RTLActions.FetchBalance('channels')); + this.store.dispatch(new RTLActions.FetchFees()); + this.store.dispatch(new RTLActions.FetchNetwork()); + this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'all', channelStatus: ''})); + this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'pending', channelStatus: ''})); + this.store.dispatch(new RTLActions.FetchInvoices()); + this.store.dispatch(new RTLActions.FetchPayments()); + } + + ngAfterViewInit() { + if (!this.settings.flgSidenavPinned) { + this.sideNavigation.close(); + this.settingSidenav.toggle(); + } + if (window.innerWidth <= 768) { + this.sideNavigation.close(); + this.settingSidenav.toggle(); + } + } + + @HostListener('window:resize') + public onWindowResize(): void { + if (window.innerWidth <= 768) { + this.settings.menu = 'Vertical'; + this.settings.flgSidenavOpened = false; + this.settings.flgSidenavPinned = false; + } + } + + sideNavToggle() { + this.sideNavigation.toggle(); + } + + onNavigationClicked(event: any) { + if (window.innerWidth <= 414) { + this.sideNavigation.close(); + } + } + + copiedText(payload) { + this.flgCopied = true; + setTimeout(() => {this.flgCopied = false; }, 5000); + this.logger.info('Copied Text: ' + payload); + } + + ngOnDestroy() { + this.unsubs.forEach(unsub => { + unsub.next(); + unsub.complete(); + }); + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts new file mode 100644 index 00000000..f4fdd850 --- /dev/null +++ b/src/app/app.module.ts @@ -0,0 +1,103 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; +import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; +import { StoreDevtoolsModule } from '@ngrx/store-devtools'; +import { UserIdleModule } from 'angular-user-idle'; + +import { OverlayContainer } from '@angular/cdk/overlay'; +import { NgxChartsModule } from '@swimlane/ngx-charts'; + +import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar'; +import { PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar'; +import { PerfectScrollbarConfigInterface } from 'ngx-perfect-scrollbar'; + +const DEFAULT_PERFECT_SCROLLBAR_CONFIG: PerfectScrollbarConfigInterface = { + suppressScrollX: false +}; + +import { environment } from '../environments/environment'; +import { routing } from './app.routing'; +import { SharedModule } from './shared/shared.module'; +import { ThemeOverlay } from './shared/theme/overlay-container/theme-overlay'; +import { AppComponent } from './app.component'; +import { HomeComponent } from './pages/home/home.component'; +import { PeersComponent } from './pages/peers/peers.component'; +import { SendReceiveTransComponent } from './pages/transactions/send-receive/send-receive-trans.component'; +import { InvoicesComponent } from './pages/invoices/invoices.component'; +import { ServerConfigComponent } from './pages/server-config/server-config.component'; +import { HelpComponent } from './pages/help/help.component'; +import { UnlockLNDComponent } from './pages/unlock-lnd/unlock-lnd.component'; +import { PaymentsComponent } from './pages/payments/payments.component'; +import { SideNavigationComponent } from './pages/navigation/side-navigation/side-navigation.component'; +import { TopMenuComponent } from './pages/navigation/top-menu/top-menu.component'; +import { HorizontalNavigationComponent } from './pages/navigation/horizontal-navigation/horizontal-navigation.component'; +import { ChannelManageComponent } from './pages/channels/channel-manage/channel-manage.component'; +import { ChannelPendingComponent } from './pages/channels/channel-pending/channel-pending.component'; +import { SigninComponent } from './pages/signin/signin.component'; + +import { RTLRootReducer } from './shared/store/rtl.reducers'; +import { RTLEffects } from './shared/store/rtl.effects'; + +import { LoggerService, ConsoleLoggerService } from './shared/services/logger.service'; +import { AuthGuard, LNDUnlockedGuard } from './shared/services/auth.guard'; +import { AuthInterceptor } from './shared/services/auth.interceptor'; +import { ChannelClosedComponent } from './pages/channels/channel-closed/channel-closed.component'; +import { ListTransactionsComponent } from './pages/transactions/list-transactions/list-transactions.component'; +import { LookupsComponent } from './pages/lookups/lookups.component'; +import { ForwardingHistoryComponent } from './pages/switch/forwarding-history.component'; +import { ChannelLookupComponent } from './pages/lookups/channel-lookup/channel-lookup.component'; +import { NodeLookupComponent } from './pages/lookups/node-lookup/node-lookup.component'; + +@NgModule({ + imports: [ + BrowserModule, + BrowserAnimationsModule, + FormsModule, + ReactiveFormsModule, + HttpClientModule, + PerfectScrollbarModule, + SharedModule, + NgxChartsModule, + routing, + UserIdleModule.forRoot({idle: 60 * 60, timeout: 1, ping: null}), + StoreModule.forRoot({rtlRoot: RTLRootReducer}), + EffectsModule.forRoot([RTLEffects]), + !environment.production ? StoreDevtoolsModule.instrument() : [] + ], + declarations: [ + AppComponent, + HomeComponent, + PeersComponent, + SendReceiveTransComponent, + InvoicesComponent, + ServerConfigComponent, + HelpComponent, + UnlockLNDComponent, + PaymentsComponent, + SideNavigationComponent, + TopMenuComponent, + HorizontalNavigationComponent, + ChannelManageComponent, + ChannelPendingComponent, + SigninComponent, + ChannelClosedComponent, + ListTransactionsComponent, + LookupsComponent, + ForwardingHistoryComponent, + ChannelLookupComponent, + NodeLookupComponent + ], + providers: [ + { provide: LoggerService, useClass: ConsoleLoggerService }, + { provide: PERFECT_SCROLLBAR_CONFIG, useValue: DEFAULT_PERFECT_SCROLLBAR_CONFIG }, + { provide: OverlayContainer, useClass: ThemeOverlay }, + { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, + AuthGuard, LNDUnlockedGuard + ], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/src/app/app.routing.ts b/src/app/app.routing.ts new file mode 100644 index 00000000..3918f710 --- /dev/null +++ b/src/app/app.routing.ts @@ -0,0 +1,46 @@ +import { Routes, RouterModule } from '@angular/router'; +import { ModuleWithProviders } from '@angular/core'; + +import { NotFoundComponent } from './shared/components/not-found/not-found.component'; + +import { HomeComponent } from './pages/home/home.component'; +import { UnlockLNDComponent } from './pages/unlock-lnd/unlock-lnd.component'; +import { ChannelClosedComponent } from './pages/channels/channel-closed/channel-closed.component'; +import { ChannelManageComponent } from './pages/channels/channel-manage/channel-manage.component'; +import { ChannelPendingComponent } from './pages/channels/channel-pending/channel-pending.component'; +import { PeersComponent } from './pages/peers/peers.component'; +import { SendReceiveTransComponent } from './pages/transactions/send-receive/send-receive-trans.component'; +import { ListTransactionsComponent } from './pages/transactions/list-transactions/list-transactions.component'; +import { PaymentsComponent } from './pages/payments/payments.component'; +import { ServerConfigComponent } from './pages/server-config/server-config.component'; +import { HelpComponent } from './pages/help/help.component'; +import { InvoicesComponent } from './pages/invoices/invoices.component'; +import { LookupsComponent } from './pages/lookups/lookups.component'; +import { SigninComponent } from './pages/signin/signin.component'; +import { ForwardingHistoryComponent } from './pages/switch/forwarding-history.component'; +import { SsoFailedComponent } from './shared/components/sso-failed/sso-failed.component'; + +import { AuthGuard, LNDUnlockedGuard } from './shared/services/auth.guard'; + +export const routes: Routes = [ + { path: '', redirectTo: '/home', pathMatch: 'full', canActivate: [AuthGuard, LNDUnlockedGuard] }, + { path: 'unlocklnd', component: UnlockLNDComponent, canActivate: [AuthGuard] }, + { path: 'home', component: HomeComponent, canActivate: [AuthGuard, LNDUnlockedGuard] }, + { path: 'peers', component: PeersComponent, canActivate: [AuthGuard, LNDUnlockedGuard] }, + { path: 'chnlclosed', component: ChannelClosedComponent, canActivate: [AuthGuard, LNDUnlockedGuard] }, + { path: 'chnlmanage', component: ChannelManageComponent, canActivate: [AuthGuard, LNDUnlockedGuard] }, + { path: 'chnlpending', component: ChannelPendingComponent, canActivate: [AuthGuard, LNDUnlockedGuard] }, + { path: 'transsendreceive', component: SendReceiveTransComponent, canActivate: [AuthGuard, LNDUnlockedGuard] }, + { path: 'translist', component: ListTransactionsComponent, canActivate: [AuthGuard, LNDUnlockedGuard] }, + { path: 'payments', component: PaymentsComponent, canActivate: [AuthGuard, LNDUnlockedGuard] }, + { path: 'invoices', component: InvoicesComponent, canActivate: [AuthGuard, LNDUnlockedGuard] }, + { path: 'switch', component: ForwardingHistoryComponent, canActivate: [AuthGuard, LNDUnlockedGuard] }, + { path: 'lookups', component: LookupsComponent, canActivate: [AuthGuard, LNDUnlockedGuard] }, + { path: 'sconfig', component: ServerConfigComponent, canActivate: [AuthGuard] }, + { path: 'login', component: SigninComponent }, + { path: 'help', component: HelpComponent }, + { path: 'ssoerror', component: SsoFailedComponent }, + { path: '**', component: NotFoundComponent } +]; + +export const routing: ModuleWithProviders = RouterModule.forRoot(routes, { enableTracing: true }); diff --git a/src/app/pages/channels/channel-closed/channel-closed.component.html b/src/app/pages/channels/channel-closed/channel-closed.component.html new file mode 100644 index 00000000..bf79c325 --- /dev/null +++ b/src/app/pages/channels/channel-closed/channel-closed.component.html @@ -0,0 +1,64 @@ +
+
+ + + +

Closed Channels

+
+
+ +
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Close Type {{channel.close_type || 'COOPERATIVE_CLOSE'}} Channel Point {{channel.channel_point | slice:0:10}}... ID {{channel.chan_id}} Closing Txn Hash +
{{channel.closing_tx_hash | slice:0:10}}...
Pub Key +
{{channel.remote_pubkey | slice:0:10}}...
Capacity {{channel.capacity | number}} Close Height {{channel.close_height | number}} Settled Balance {{channel.settled_balance | number}} Time Locked Balance {{channel.time_locked_balance | number}}
+
+
+
+
+
+ \ No newline at end of file diff --git a/src/app/pages/channels/channel-closed/channel-closed.component.scss b/src/app/pages/channels/channel-closed/channel-closed.component.scss new file mode 100644 index 00000000..801fe9ca --- /dev/null +++ b/src/app/pages/channels/channel-closed/channel-closed.component.scss @@ -0,0 +1,25 @@ +.mat-column-close_type { + flex: 0 0 16%; + min-width: 160px; +} + +.mat-column-chan_id { + flex: 0 0 17%; + min-width: 170px; +} + +table { + width:100%; +} + +.table-container { + height: 79vh; + overflow: auto; +} + +@media screen and (max-width: 414px) { + .table-container { + height: 68vh; + overflow: auto; + } +} diff --git a/src/app/pages/channels/channel-closed/channel-closed.component.spec.ts b/src/app/pages/channels/channel-closed/channel-closed.component.spec.ts new file mode 100644 index 00000000..325af666 --- /dev/null +++ b/src/app/pages/channels/channel-closed/channel-closed.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChannelClosedComponent } from './channel-closed.component'; + +describe('ChannelClosedComponent', () => { + let component: ChannelClosedComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ChannelClosedComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ChannelClosedComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/channels/channel-closed/channel-closed.component.ts b/src/app/pages/channels/channel-closed/channel-closed.component.ts new file mode 100644 index 00000000..099cde55 --- /dev/null +++ b/src/app/pages/channels/channel-closed/channel-closed.component.ts @@ -0,0 +1,104 @@ +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; + +import { MatTableDataSource, MatSort } from '@angular/material'; +import { ClosedChannel } from '../../../shared/models/lndModels'; +import { LoggerService } from '../../../shared/services/logger.service'; + +import { RTLEffects } from '../../../shared/store/rtl.effects'; +import * as RTLActions from '../../../shared/store/rtl.actions'; +import * as fromRTLReducer from '../../../shared/store/rtl.reducers'; + +@Component({ + selector: 'rtl-channel-closed', + templateUrl: './channel-closed.component.html', + styleUrls: ['./channel-closed.component.scss'] +}) +export class ChannelClosedComponent implements OnInit, OnDestroy { + @ViewChild(MatSort) sort: MatSort; + public displayedColumns = []; + public closedChannels: any; + public flgLoading: Array = [true]; + public selectedFilter = ''; + private unsub: Array> = [new Subject(), new Subject()]; + + constructor(private logger: LoggerService, private store: Store, private rtlEffects: RTLEffects) { + switch (true) { + case (window.innerWidth <= 415): + this.displayedColumns = ['close_type', 'chan_id', 'settled_balance']; + break; + case (window.innerWidth > 415 && window.innerWidth <= 730): + this.displayedColumns = ['close_type', 'channel_point', 'chan_id', 'settled_balance']; + break; + case (window.innerWidth > 730 && window.innerWidth <= 1024): + this.displayedColumns = ['close_type', 'channel_point', 'chan_id', 'capacity', 'close_height', 'settled_balance']; + break; + case (window.innerWidth > 1024 && window.innerWidth <= 1280): + this.displayedColumns = ['close_type', 'channel_point', 'chan_id', 'closing_tx_hash', 'remote_pubkey', 'capacity', + 'close_height', 'settled_balance', 'time_locked_balance']; + break; + default: + this.displayedColumns = ['close_type', 'channel_point', 'chan_id', 'closing_tx_hash', 'remote_pubkey', 'capacity', + 'close_height', 'settled_balance', 'time_locked_balance']; + break; + } + } + + ngOnInit() { + this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'closed', channelStatus: ''})); + this.store.select('rtlRoot') + .pipe(takeUntil(this.unsub[0])) + .subscribe((rtlStore: fromRTLReducer.State) => { + rtlStore.effectErrors.forEach(effectsErr => { + if (effectsErr.action === 'FetchChannels/closed') { + this.flgLoading[0] = 'error'; + } + }); + if (undefined !== rtlStore.closedChannels && rtlStore.closedChannels.length > 0) { + this.loadClosedChannelsTable(rtlStore.closedChannels); + } + if (this.flgLoading[0] !== 'error') { + this.flgLoading[0] = (undefined !== rtlStore.closedChannels) ? false : true; + } + this.logger.info(rtlStore); + }); + + } + + applyFilter(selFilter: string) { + this.selectedFilter = selFilter; + this.closedChannels.filter = selFilter; + } + + onClosedChannelClick(selRow: ClosedChannel, event: any) { + const selChannel = this.closedChannels.data.filter(closedChannel => { + return closedChannel.chan_id === selRow.chan_id; + })[0]; + const reorderedChannel = JSON.parse(JSON.stringify(selChannel, ['close_type', 'channel_point', 'chan_id', 'closing_tx_hash', 'remote_pubkey', 'capacity', + 'close_height', 'settled_balance', 'time_locked_balance'] , 2)); + this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: { + type: 'INFO', + message: JSON.stringify(reorderedChannel) + }})); + } + + loadClosedChannelsTable(closedChannels) { + this.closedChannels = new MatTableDataSource([...closedChannels]); + this.closedChannels.sort = this.sort; + this.logger.info(this.closedChannels); + } + + resetData() { + this.selectedFilter = ''; + } + + ngOnDestroy() { + this.unsub.forEach(completeSub => { + completeSub.next(); + completeSub.complete(); + }); + } + +} diff --git a/src/app/pages/channels/channel-manage/channel-manage.component.html b/src/app/pages/channels/channel-manage/channel-manage.component.html new file mode 100644 index 00000000..65a1a09a --- /dev/null +++ b/src/app/pages/channels/channel-manage/channel-manage.component.html @@ -0,0 +1,131 @@ +
+
+ + + +

Add Channel

+
+
+ +
+ + + + {{peer.alias}} + + + + + + (Wallet Bal: {{totalBalance}}, Remaining Bal: {{totalBalance - ((fundingAmount) ? fundingAmount : 0)}}) + +
+ Options +
+ +
+ + + + {{transType.name}} + + + +
+
+ + + + + + + + + +
+
+ Spend Unconfirmed Output +
+
+
+ +
+
+ +
+
+
+
+
+
+ + +
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Disconnect link_offeditedit Status {{channel.active}} ID {{channel.chan_id}} Pub Key +
{{channel.remote_pubkey | slice:0:10}}...
Alias {{channel.remote_alias}} Capacity {{channel.capacity | number}} Local Bal {{channel.local_balance | number}} Remote Bal {{channel.remote_balance | number}} {{information?.smaller_currency_unit}} Sent {{channel.total_satoshis_sent | number}} {{information?.smaller_currency_unit}} Recv {{channel.total_satoshis_received | number}} Fee {{channel.commit_fee | number}}
+
+
+
+
+
+ \ No newline at end of file diff --git a/src/app/pages/channels/channel-manage/channel-manage.component.scss b/src/app/pages/channels/channel-manage/channel-manage.component.scss new file mode 100644 index 00000000..ee209344 --- /dev/null +++ b/src/app/pages/channels/channel-manage/channel-manage.component.scss @@ -0,0 +1,50 @@ +.mat-column-close, .mat-column-update, .mat-column-active { + flex: 0 0 6%; + min-width: 50px; +} + +mat-cell.mat-column-close, .mat-column-update { + cursor: pointer; +} + +.mat-column-chan_id { + flex: 0 0 16%; + min-width: 160px; +} + +.mat-checkbox-inner-container:focus, .mat-checkbox-input:focus { + outline: none !important; +} + +.size-40 { + font-size: 40px; + margin-left: -30%; +} + +.mat-button-text { + font-size: 24px; + padding-bottom: 20px; +} + +.flex-ellipsis { + padding-right: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +table { + width:100%; +} + +.table-container { + height: 68vh; + overflow: auto; +} + +@media screen and (max-width: 414px) { + .table-container { + height: 31vh; + overflow: auto; + } +} diff --git a/src/app/pages/channels/channel-manage/channel-manage.component.spec.ts b/src/app/pages/channels/channel-manage/channel-manage.component.spec.ts new file mode 100644 index 00000000..8c1baf54 --- /dev/null +++ b/src/app/pages/channels/channel-manage/channel-manage.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChannelManageComponent } from './channel-manage.component'; + +describe('ChannelManageComponent', () => { + let component: ChannelManageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ChannelManageComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ChannelManageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/channels/channel-manage/channel-manage.component.ts b/src/app/pages/channels/channel-manage/channel-manage.component.ts new file mode 100644 index 00000000..4c6639a5 --- /dev/null +++ b/src/app/pages/channels/channel-manage/channel-manage.component.ts @@ -0,0 +1,251 @@ +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil, filter } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { Actions } from '@ngrx/effects'; + +import { MatTableDataSource, MatSort } from '@angular/material'; +import { Channel, Peer, GetInfo } from '../../../shared/models/lndModels'; +import { LoggerService } from '../../../shared/services/logger.service'; + +import { RTLEffects } from '../../../shared/store/rtl.effects'; +import * as RTLActions from '../../../shared/store/rtl.actions'; +import * as fromRTLReducer from '../../../shared/store/rtl.reducers'; + +@Component({ + selector: 'rtl-channel-manage', + templateUrl: './channel-manage.component.html', + styleUrls: ['./channel-manage.component.scss'] +}) +export class ChannelManageComponent implements OnInit, OnDestroy { + @ViewChild(MatSort) sort: MatSort; + public totalBalance = 0; + public selectedPeer = ''; + public fundingAmount: number; + public displayedColumns = []; + public channels: any; + public peers: Peer[] = []; + public information: GetInfo = {}; + public flgLoading: Array = [true]; + public selectedFilter = ''; + public statusFilters = ['Active', 'Inactive']; + public myChanPolicy: any = {}; + public selFilter = ''; + public flgSticky = true; + public transTypes = [{id: '0', name: 'Default Priority'}, {id: '1', name: 'Target Confirmation Blocks'}, {id: '2', name: 'Fee'}]; + public selTransType = '0'; + public transTypeValue = {blocks: '', fees: ''}; + public spendUnconfirmed = false; + public moreOptions = false; + private unsub: Array> = [new Subject(), new Subject(), new Subject(), new Subject()]; + + constructor(private logger: LoggerService, private store: Store, private rtlEffects: RTLEffects, private actions$: Actions) { + switch (true) { + case (window.innerWidth <= 415): + this.displayedColumns = ['close', 'update', 'active', 'chan_id', 'remote_alias']; + break; + case (window.innerWidth > 415 && window.innerWidth <= 730): + this.displayedColumns = ['close', 'update', 'active', 'chan_id', 'remote_alias', 'capacity']; + break; + case (window.innerWidth > 730 && window.innerWidth <= 1024): + this.displayedColumns = ['close', 'update', 'active', 'chan_id', 'remote_alias', 'capacity', 'local_balance', 'remote_balance']; + break; + case (window.innerWidth > 1024 && window.innerWidth <= 1280): + this.displayedColumns = ['close', 'update', 'active', 'chan_id', 'remote_alias', 'capacity', 'local_balance', 'remote_balance', 'total_satoshis_sent', + 'total_satoshis_received', 'commit_fee']; + break; + default: + this.displayedColumns = ['close', 'update', 'active', 'chan_id', 'remote_pubkey', 'remote_alias', 'capacity', 'local_balance', 'remote_balance', + 'total_satoshis_sent', 'total_satoshis_received', 'commit_fee']; + break; + } + } + + ngOnInit() { + this.store.select('rtlRoot') + .pipe(takeUntil(this.unsub[0])) + .subscribe((rtlStore: fromRTLReducer.State) => { + rtlStore.effectErrors.forEach(effectsErr => { + if (effectsErr.action === 'FetchChannels/all') { + this.flgLoading[0] = 'error'; + } + }); + this.information = rtlStore.information; + this.peers = rtlStore.peers; + this.peers.forEach(peer => { + if (undefined === peer.alias || peer.alias === '') { + peer.alias = peer.pub_key.substring(0, 15) + '...'; + } + }); + + this.totalBalance = +rtlStore.blockchainBalance.total_balance; + if (undefined !== rtlStore.allChannels && rtlStore.allChannels.length > 0) { + this.loadChannelsTable(rtlStore.allChannels); + } + if (this.flgLoading[0] !== 'error') { + this.flgLoading[0] = (undefined !== rtlStore.allChannels) ? false : true; + } + + this.logger.info(rtlStore); + }); + } + + onOpenChannel(form: any) { + this.store.dispatch(new RTLActions.OpenSpinner('Opening Channel...')); + let transTypeValue = '0'; + if (this.selTransType === '1') { + transTypeValue = this.transTypeValue.blocks; + } else if (this.selTransType === '2') { + transTypeValue = this.transTypeValue.fees; + } + this.store.dispatch(new RTLActions.SaveNewChannel({ + selectedPeerPubkey: this.selectedPeer, fundingAmount: this.fundingAmount, + transType: this.selTransType, transTypeValue: transTypeValue, spendUnconfirmed: this.spendUnconfirmed + })); + } + + onChannelUpdate(channelToUpdate: any) { + if (channelToUpdate === 'all') { + const titleMsg = 'Updated Values for ALL Channels'; + const confirmationMsg = {}; + this.store.dispatch(new RTLActions.OpenConfirmation({ width: '70%', data: { + type: 'CONFIRM', titleMessage: titleMsg, noBtnText: 'Cancel', yesBtnText: 'Update', message: JSON.stringify(confirmationMsg), flgShowInput: true, getInputs: [ + {placeholder: 'Base Fee msat', inputType: 'number', inputValue: 1000}, + {placeholder: 'Fee Rate mili msat', inputType: 'number', inputValue: 1, min: 1}, + {placeholder: 'Time Lock Delta', inputType: 'number', inputValue: 144} + ] + }})); + this.rtlEffects.closeConfirm + .pipe(takeUntil(this.unsub[2])) + .subscribe(confirmRes => { + if (confirmRes) { + const base_fee = confirmRes[0].inputValue; + const fee_rate = confirmRes[1].inputValue; + const time_lock_delta = confirmRes[2].inputValue; + this.store.dispatch(new RTLActions.OpenSpinner('Updating Channel Policy...')); + this.store.dispatch(new RTLActions.UpdateChannels({baseFeeMsat: base_fee, feeRate: fee_rate, timeLockDelta: time_lock_delta, chanPoint: 'all'})); + } + }); + } else { + this.myChanPolicy = {fee_base_msat: 0, fee_rate_milli_msat: 0, time_lock_delta: 0}; + this.store.dispatch(new RTLActions.OpenSpinner('Fetching Channel Policy...')); + this.store.dispatch(new RTLActions.ChannelLookup(channelToUpdate.chan_id.toString())); + this.rtlEffects.setLookup + .pipe(takeUntil(this.unsub[3])) + .subscribe(resLookup => { + this.logger.info(resLookup); + if (resLookup.node1_pub === this.information.identity_pubkey) { + this.myChanPolicy = resLookup.node1_policy; + } else if (resLookup.node2_pub === this.information.identity_pubkey) { + this.myChanPolicy = resLookup.node2_policy; + } else { + this.myChanPolicy = {fee_base_msat: 0, fee_rate_milli_msat: 0, time_lock_delta: 0}; + } + this.logger.info(this.myChanPolicy); + this.store.dispatch(new RTLActions.CloseSpinner()); + const titleMsg = 'Updated Values for Channel Point: ' + channelToUpdate.channel_point; + const confirmationMsg = {}; + this.store.dispatch(new RTLActions.OpenConfirmation({ width: '70%', data: { + type: 'CONFIRM', titleMessage: titleMsg, noBtnText: 'Cancel', yesBtnText: 'Update', message: JSON.stringify(confirmationMsg), flgShowInput: true, getInputs: [ + {placeholder: 'Base Fee msat', inputType: 'number', inputValue: (this.myChanPolicy.fee_base_msat === '') ? 0 : this.myChanPolicy.fee_base_msat}, + {placeholder: 'Fee Rate mili msat', inputType: 'number', inputValue: this.myChanPolicy.fee_rate_milli_msat, min: 1}, + {placeholder: 'Time Lock Delta', inputType: 'number', inputValue: this.myChanPolicy.time_lock_delta} + ] + }})); + }); + this.rtlEffects.closeConfirm + .pipe(takeUntil(this.unsub[2])) + .subscribe(confirmRes => { + if (confirmRes) { + const base_fee = confirmRes[0].inputValue; + const fee_rate = confirmRes[1].inputValue; + const time_lock_delta = confirmRes[2].inputValue; + this.store.dispatch(new RTLActions.OpenSpinner('Updating Channel Policy...')); + this.store.dispatch(new RTLActions.UpdateChannels({baseFeeMsat: base_fee, feeRate: fee_rate, timeLockDelta: time_lock_delta, chanPoint: channelToUpdate.channel_point})); + } + }); + } + this.applyFilter(); + } + + onChannelClose(channelToClose: Channel) { + this.store.dispatch(new RTLActions.OpenConfirmation({ + width: '70%', data: { type: 'CONFIRM', titleMessage: 'Closing channel: ' + channelToClose.chan_id, noBtnText: 'Cancel', yesBtnText: 'Disconnect' + }})); + this.rtlEffects.closeConfirm + .pipe(takeUntil(this.unsub[1])) + .subscribe(confirmRes => { + if (confirmRes) { + this.store.dispatch(new RTLActions.OpenSpinner('Closing Channel...')); + this.store.dispatch(new RTLActions.CloseChannel({channelPoint: channelToClose.channel_point, forcibly: true, channelStatus: channelToClose.active})); + } + }); + } + + applyFilter() { + this.selectedFilter = this.selFilter; + this.channels.filter = this.selFilter; + } + + onChannelClick(selRow: Channel, event: any) { + const flgCloseClicked = + event.target.className.includes('mat-column-close') + || event.target.className.includes('mat-column-update') + || event.target.className.includes('mat-icon'); + if (flgCloseClicked) { + return; + } + const selChannel = this.channels.data.filter(channel => { + return channel.chan_id === selRow.chan_id; + })[0]; + const reorderedChannel = JSON.parse(JSON.stringify(selChannel, [ + 'active', 'remote_pubkey', 'remote_alias', 'channel_point', 'chan_id', 'capacity', 'local_balance', 'remote_balance', 'commit_fee', 'commit_weight', + 'fee_per_kw', 'unsettled_balance', 'total_satoshis_sent', 'total_satoshis_received', 'num_updates', 'pending_htlcs', 'csv_delay', 'private' + ] , 2)); + this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: { + type: 'INFO', + message: JSON.stringify(reorderedChannel) + }})); + } + + loadChannelsTable(channels) { + channels.sort(function(a, b) { + return (a.active === b.active) ? 0 : ((b.active) ? 1 : -1); + }); + channels.forEach(channel => { + if (channel.active === true || channel.active === 'Active') { + channel.active = 'Active'; + } else { + channel.active = 'Inactive'; + } + }); + this.channels = new MatTableDataSource([...channels]); + this.channels.sort = this.sort; + this.logger.info(this.channels); + } + + resetData() { + this.selectedPeer = ''; + this.fundingAmount = 0; + this.moreOptions = false; + this.spendUnconfirmed = false; + this.selTransType = '0'; + this.transTypeValue = {blocks: '', fees: ''}; + } + + onMoreOptionsChange(event: any) { + if (!event.checked) { + this.spendUnconfirmed = false; + this.selTransType = '0'; + this.transTypeValue = {blocks: '', fees: ''}; + } + } + + ngOnDestroy() { + this.unsub.forEach(completeSub => { + completeSub.next(); + completeSub.complete(); + }); + } + +} diff --git a/src/app/pages/channels/channel-pending/channel-pending.component.html b/src/app/pages/channels/channel-pending/channel-pending.component.html new file mode 100644 index 00000000..59cf8f94 --- /dev/null +++ b/src/app/pages/channels/channel-pending/channel-pending.component.html @@ -0,0 +1,252 @@ +
+
+ + + +

Pending Channels

+ +
+
+ +
+

Total Limbo Balance: + {{pendingChannels.btc_total_limbo_balance | number}} {{information?.currency_unit}}

+ +

Total Limbo Balance: {{pendingChannels.total_limbo_balance | number}} + {{information?.smaller_currency_unit}}

+
+
+
+ + + + +

Pending Open Channels({{pendingOpenChannelsLength}})

+
+ +
+ + + Remote Node Pub + {{channel.channel.remote_node_pub}} + + + + Local Balance + {{channel.channel.local_balance | + number}} + + + + Commit Fee + {{channel.commit_fee | number}} + + + + + Remote Balance + {{channel.channel.remote_balance | + number}} + + + + Capacity + {{channel.channel.capacity | + number}} + + + + Commit Weight + {{channel.commit_weight | number}} + + + + + Fee Per KW + {{channel.fee_per_kw | number}} + + + + + Confirmation Height + {{channel.confirmation_height | + number}} + + + Channel Point + {{channel.channel.channel_point}} + + + + +
+ + + +

Pending Force Closing Channels({{pendingForceClosingChannelsLength}})

+
+ +
+ + + Remote Node Pub + {{channel.channel.remote_node_pub}} + + + + Recovered Balance + {{channel.recovered_balance | + number}} + + + + Limbo Balance + {{channel.limbo_balance | number}} + + + + + Block Till Maturity + {{channel.blocks_til_maturity | + number}} + + + + Maturity Height + {{channel.maturity_height | number}} + + + + + Local Balance + {{channel.channel.local_balance | + number}} + + + + Remote Balance + {{channel.channel.remote_balance | + number}} + + + + Capacity + {{channel.channel.capacity | + number}} + + + Transaction Id + +
{{channel.closing_txid}}
+
+
+ + Channel Point + {{channel.channel.channel_point}} + + + +
+
+ + + +

Pending Closing Channels({{pendingClosingChannelsLength}})

+
+ +
+ + + Remote Node Pub + {{channel.channel.remote_node_pub}} + + + + Local Balance + {{channel.channel.local_balance | + number}} + + + + Remote Balance + {{channel.channel.remote_balance | + number}} + + + + Capacity + {{channel.channel.capacity | + number}} + + + Transaction Id + +
{{channel.closing_txid}}
+
+
+ + Channel Point + {{channel.channel.channel_point}} + + + +
+
+ + + +

Waiting Close Channels({{pendingWaitClosingChannelsLength}})

+
+ +
+ + + Remote Node Pub + {{channel.channel.remote_node_pub}} + + + + Limbo Balance + {{channel.limbo_balance | number}} + + + + + Local Balance + {{channel.channel.local_balance | + number}} + + + + Remote Balance + {{channel.channel.remote_balance | + number}} + + + + Capacity + {{channel.channel.capacity | + number}} + + + Channel Point + {{channel.channel.channel_point}} + + + + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/src/app/pages/channels/channel-pending/channel-pending.component.scss b/src/app/pages/channels/channel-pending/channel-pending.component.scss new file mode 100644 index 00000000..4ddbcf5b --- /dev/null +++ b/src/app/pages/channels/channel-pending/channel-pending.component.scss @@ -0,0 +1,6 @@ +.flex-ellipsis { + padding-right: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/src/app/pages/channels/channel-pending/channel-pending.component.spec.ts b/src/app/pages/channels/channel-pending/channel-pending.component.spec.ts new file mode 100644 index 00000000..2b53efe0 --- /dev/null +++ b/src/app/pages/channels/channel-pending/channel-pending.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChannelPendingComponent } from './channel-pending.component'; + +describe('ChannelPendingComponent', () => { + let component: ChannelPendingComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ChannelPendingComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ChannelPendingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/channels/channel-pending/channel-pending.component.ts b/src/app/pages/channels/channel-pending/channel-pending.component.ts new file mode 100644 index 00000000..d4ee3e64 --- /dev/null +++ b/src/app/pages/channels/channel-pending/channel-pending.component.ts @@ -0,0 +1,241 @@ +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; + +import { MatTableDataSource, MatSort } from '@angular/material'; +import { Channel, GetInfo, PendingChannels } from '../../../shared/models/lndModels'; +import { Settings } from '../../../shared/models/RTLconfig'; +import { LoggerService } from '../../../shared/services/logger.service'; + +import { RTLEffects } from '../../../shared/store/rtl.effects'; +import * as RTLActions from '../../../shared/store/rtl.actions'; +import * as fromRTLReducer from '../../../shared/store/rtl.reducers'; + +@Component({ + selector: 'rtl-channel-pending', + templateUrl: './channel-pending.component.html', + styleUrls: ['./channel-pending.component.scss'] +}) +export class ChannelPendingComponent implements OnInit, OnDestroy { + @ViewChild(MatSort) sort: MatSort; + public selectedFilter = 0; + public settings: Settings; + public information: GetInfo = {}; + public pendingChannels: PendingChannels = {}; + public displayedClosingColumns = [ + 'closing_txid', + 'channel_point', 'remote_balance', 'local_balance', 'remote_node_pub', 'capacity' + ]; + public pendingClosingChannelsLength = 0; + public pendingClosingChannels: any; + public displayedForceClosingColumns = [ + 'closing_txid', 'limbo_balance', 'maturity_height', 'blocks_til_maturity', 'recovered_balance', + 'channel_point', 'remote_balance', 'local_balance', 'remote_node_pub', 'capacity' + ]; + public pendingForceClosingChannelsLength = 0; + public pendingForceClosingChannels: any; + public displayedOpenColumns = [ + 'commit_weight', 'confirmation_height', 'fee_per_kw', 'commit_fee', + 'channel_point', 'remote_balance', 'local_balance', 'remote_node_pub', 'capacity' + ]; + public pendingOpenChannelsLength = 0; + public pendingOpenChannels: any; + public displayedWaitClosingColumns = [ + 'limbo_balance', + 'channel_point', 'remote_balance', 'local_balance', 'remote_node_pub', 'capacity' + ]; + public pendingWaitClosingChannelsLength = 0; + public pendingWaitClosingChannels: any; + public flgLoading: Array = [true]; + private unsub: Array> = [new Subject(), new Subject()]; + + constructor(private logger: LoggerService, private store: Store, private rtlEffects: RTLEffects) { + switch (true) { + case (window.innerWidth <= 415): + this.displayedClosingColumns = ['remote_node_pub', 'local_balance', 'remote_balance']; + this.displayedForceClosingColumns = ['remote_node_pub', 'recovered_balance', 'limbo_balance']; + this.displayedOpenColumns = ['remote_node_pub', 'local_balance', 'commit_fee']; + this.displayedWaitClosingColumns = ['remote_node_pub', 'limbo_balance', 'local_balance']; + break; + case (window.innerWidth > 415 && window.innerWidth <= 730): + this.displayedClosingColumns = ['remote_node_pub', 'local_balance', 'remote_balance', 'capacity']; + this.displayedForceClosingColumns = ['remote_node_pub', 'recovered_balance', 'limbo_balance', 'blocks_til_maturity', 'maturity_height']; + this.displayedOpenColumns = ['remote_node_pub', 'local_balance', 'commit_fee', 'remote_balance']; + this.displayedWaitClosingColumns = ['remote_node_pub', 'limbo_balance', 'local_balance', 'remote_balance']; + break; + case (window.innerWidth > 730 && window.innerWidth <= 1024): + this.displayedClosingColumns = ['remote_node_pub', 'local_balance', 'remote_balance', 'capacity', 'closing_txid']; + this.displayedForceClosingColumns = ['remote_node_pub', 'recovered_balance', 'limbo_balance', 'blocks_til_maturity', 'maturity_height', 'local_balance']; + this.displayedOpenColumns = ['remote_node_pub', 'local_balance', 'commit_fee', 'remote_balance', 'capacity']; + this.displayedWaitClosingColumns = ['remote_node_pub', 'limbo_balance', 'local_balance', 'remote_balance', 'capacity', 'channel_point']; + break; + case (window.innerWidth > 1024 && window.innerWidth <= 1280): + this.displayedClosingColumns = ['remote_node_pub', 'local_balance', 'remote_balance', 'capacity', 'closing_txid', 'channel_point']; + this.displayedForceClosingColumns = [ + 'remote_node_pub', 'recovered_balance', 'limbo_balance', 'blocks_til_maturity', + 'maturity_height', 'local_balance', 'remote_balance', 'capacity', 'closing_txid', 'channel_point' + ]; + this.displayedOpenColumns = [ + 'remote_node_pub', 'local_balance', 'commit_fee', 'remote_balance', 'capacity', 'commit_weight', 'fee_per_kw', 'confirmation_height', 'channel_point' + ]; + this.displayedWaitClosingColumns = ['remote_node_pub', 'limbo_balance', 'local_balance', 'remote_balance', 'capacity', 'channel_point']; + break; + default: + this.displayedClosingColumns = ['remote_node_pub', 'local_balance', 'remote_balance', 'capacity', 'closing_txid', 'channel_point']; + this.displayedForceClosingColumns = [ + 'remote_node_pub', 'recovered_balance', 'limbo_balance', 'blocks_til_maturity', + 'maturity_height', 'local_balance', 'remote_balance', 'capacity', 'closing_txid', 'channel_point' + ]; + this.displayedOpenColumns = [ + 'remote_node_pub', 'local_balance', 'commit_fee', 'remote_balance', 'capacity', 'commit_weight', 'fee_per_kw', 'confirmation_height', 'channel_point' + ]; + this.displayedWaitClosingColumns = ['remote_node_pub', 'limbo_balance', 'local_balance', 'remote_balance', 'capacity', 'channel_point']; + break; + } + } + + ngOnInit() { + this.store.select('rtlRoot') + .pipe(takeUntil(this.unsub[0])) + .subscribe((rtlStore: fromRTLReducer.State) => { + rtlStore.effectErrors.forEach(effectsErr => { + if (effectsErr.action === 'FetchChannels/pending') { + this.flgLoading[0] = 'error'; + } + }); + + this.settings = rtlStore.settings; + this.information = rtlStore.information; + this.pendingChannels = rtlStore.pendingChannels; + if (undefined !== this.pendingChannels.total_limbo_balance) { + this.flgLoading[1] = false; + if (undefined !== this.pendingChannels.pending_closing_channels) { + this.loadClosingChannelsTable(this.pendingChannels.pending_closing_channels); + } + if (undefined !== this.pendingChannels.pending_force_closing_channels) { + this.loadForceClosingChannelsTable(this.pendingChannels.pending_force_closing_channels); + } + if (undefined !== this.pendingChannels.pending_open_channels) { + this.loadOpenChannelsTable(this.pendingChannels.pending_open_channels); + } + if (undefined !== this.pendingChannels.waiting_close_channels) { + this.loadWaitClosingChannelsTable(this.pendingChannels.waiting_close_channels); + } + } + if (this.flgLoading[0] !== 'error') { + this.flgLoading[0] = (undefined !== this.information.identity_pubkey) ? false : true; + } + this.logger.info(rtlStore); + }); + + } + + onOpenClick(selRow: any, event: any) { + const selChannel = this.pendingOpenChannels.data.filter(channel => { + return channel.channel.channel_point === selRow.channel.channel_point; + })[0]; + const fcChannelObj1 = JSON.parse(JSON.stringify(selChannel, ['commit_weight', 'confirmation_height', 'fee_per_kw', 'commit_fee'], 2)); + const fcChannelObj2 = JSON.parse(JSON.stringify(selChannel.channel, ['channel_point', 'remote_balance', 'local_balance', 'remote_node_pub', 'capacity'], 2)); + const reorderedChannel = {}; + Object.assign(reorderedChannel, fcChannelObj1, fcChannelObj2); + this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: { + type: 'INFO', + message: JSON.stringify(reorderedChannel) + }})); + } + + onForceClosingClick(selRow: any, event: any) { + const selChannel = this.pendingForceClosingChannels.data.filter(channel => { + return channel.channel.channel_point === selRow.channel.channel_point; + })[0]; + const fcChannelObj1 = JSON.parse(JSON.stringify(selChannel, ['closing_txid', 'limbo_balance', 'maturity_height', 'blocks_til_maturity', 'recovered_balance'], 2)); + const fcChannelObj2 = JSON.parse(JSON.stringify(selChannel.channel, ['channel_point', 'remote_balance', 'local_balance', 'remote_node_pub', 'capacity'], 2)); + const reorderedChannel = {}; + Object.assign(reorderedChannel, fcChannelObj1, fcChannelObj2); + this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: { + type: 'INFO', + message: JSON.stringify(reorderedChannel) + }})); + } + + onClosingClick(selRow: any, event: any) { + const selChannel = this.pendingClosingChannels.data.filter(channel => { + return channel.channel.channel_point === selRow.channel.channel_point; + })[0]; + const fcChannelObj1 = JSON.parse(JSON.stringify(selChannel, ['closing_txid'], 2)); + const fcChannelObj2 = JSON.parse(JSON.stringify(selChannel.channel, ['channel_point', 'remote_balance', 'local_balance', 'remote_node_pub', 'capacity'], 2)); + const reorderedChannel = {}; + Object.assign(reorderedChannel, fcChannelObj1, fcChannelObj2); + this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: { + type: 'INFO', + message: JSON.stringify(reorderedChannel) + }})); + } + + onWaitClosingClick(selRow: any, event: any) { + const selChannel = this.pendingWaitClosingChannels.data.filter(channel => { + return channel.channel.channel_point === selRow.channel.channel_point; + })[0]; + const fcChannelObj1 = JSON.parse(JSON.stringify(selChannel, ['limbo_balance'], 2)); + const fcChannelObj2 = JSON.parse(JSON.stringify(selChannel.channel, ['channel_point', 'remote_balance', 'local_balance', 'remote_node_pub', 'capacity'], 2)); + const reorderedChannel = {}; + Object.assign(reorderedChannel, fcChannelObj1, fcChannelObj2); + this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: { + type: 'INFO', + message: JSON.stringify(reorderedChannel) + }})); + } + + loadOpenChannelsTable(channels) { + channels.sort(function(a, b) { + return (a.active === b.active) ? 0 : ((b.active) ? -1 : 1); + }); + this.pendingOpenChannelsLength = (undefined !== channels.length) ? channels.length : 0; + this.pendingOpenChannels = new MatTableDataSource([...channels]); + this.pendingOpenChannels.sort = this.sort; + this.logger.info(this.pendingOpenChannels); + } + + loadForceClosingChannelsTable(channels) { + channels.sort(function(a, b) { + return (a.active === b.active) ? 0 : ((b.active) ? -1 : 1); + }); + this.pendingForceClosingChannelsLength = (undefined !== channels.length) ? channels.length : 0; + this.pendingForceClosingChannels = new MatTableDataSource([...channels]); + this.pendingForceClosingChannels.sort = this.sort; + this.logger.info(this.pendingForceClosingChannels); + } + + loadClosingChannelsTable(channels) { + channels.sort(function(a, b) { + return (a.active === b.active) ? 0 : ((b.active) ? -1 : 1); + }); + this.pendingClosingChannelsLength = (undefined !== channels.length) ? channels.length : 0; + this.pendingClosingChannels = new MatTableDataSource([...channels]); + this.pendingClosingChannels.sort = this.sort; + this.logger.info(this.pendingClosingChannels); + } + + loadWaitClosingChannelsTable(channels) { + channels.sort(function(a, b) { + return (a.active === b.active) ? 0 : ((b.active) ? -1 : 1); + }); + this.pendingWaitClosingChannelsLength = (undefined !== channels.length) ? channels.length : 0; + this.pendingWaitClosingChannels = new MatTableDataSource([...channels]); + this.pendingWaitClosingChannels.sort = this.sort; + this.logger.info(this.pendingWaitClosingChannels); + } + + applyFilter(selFilter: number) { + this.selectedFilter = selFilter; + } + + ngOnDestroy() { + this.unsub.forEach(completeSub => { + completeSub.next(); + completeSub.complete(); + }); + } + +} diff --git a/src/app/pages/help/help.component.html b/src/app/pages/help/help.component.html new file mode 100644 index 00000000..38191ad6 --- /dev/null +++ b/src/app/pages/help/help.component.html @@ -0,0 +1,20 @@ +
+
+ + + +

Help

+
+
+ + + + {{helpTopic.question}} + + {{helpTopic.answer}} + +
+
+
+
+
\ No newline at end of file diff --git a/src/app/pages/help/help.component.scss b/src/app/pages/help/help.component.scss new file mode 100644 index 00000000..f4bcac9e --- /dev/null +++ b/src/app/pages/help/help.component.scss @@ -0,0 +1,3 @@ +.mat-card-content { + margin-bottom: 4px; +} diff --git a/src/app/pages/help/help.component.ts b/src/app/pages/help/help.component.ts new file mode 100644 index 00000000..3ba67079 --- /dev/null +++ b/src/app/pages/help/help.component.ts @@ -0,0 +1,29 @@ +import { Component, OnInit } from '@angular/core'; + +export class HelpTopic { + question: string; + answer: string; + + constructor(ques: string, ans: string) { + this.question = ques; + this.answer = ans; + } +} + +@Component({ + selector: 'rtl-help', + templateUrl: './help.component.html', + styleUrls: ['./help.component.scss'] +}) +export class HelpComponent implements OnInit { + public helpTopics: Array = []; + + constructor() {} + + ngOnInit() { + // this.helpTopics.push(new HelpTopic('Set LND home directory?', + // 'Pass the directroy information while getting the server up with --lndir "local-lnd-path".
Example: node rtl --lndir C:\lnd\dir\path')); + this.helpTopics.push(new HelpTopic('Change theme?', 'Click on rotating setting icon on the right side of the screen and choose from the given options.')); + } + +} diff --git a/src/app/pages/home/home.component.html b/src/app/pages/home/home.component.html new file mode 100644 index 00000000..bbd82d00 --- /dev/null +++ b/src/app/pages/home/home.component.html @@ -0,0 +1,237 @@ +
+
+ + + +
Wallet Balance
+
+
+ + + account_balance_wallet + + +

{{BTCtotalBalance | number}} {{information?.currency_unit}}

+

{{totalBalance | number}} {{information?.smaller_currency_unit}}

+
+
+ + +
+
+
+ + + +
Peers
+
+
+ + + group + +

{{totalPeers | number}}

+ +

0

+
+
+ + +
+
+
+ + + +
Channel Balance
+
+
+ + + linear_scale + + +

{{BTCchannelBalance | number}} {{information?.currency_unit}}

+

{{channelBalance | number}} {{information?.smaller_currency_unit}}

+
+
+ + +
+
+
+ + + +
Chain Sync Status
+
+
+ + + sync + + check_circle + + cancel + + + + +
+
+
+
+
+
+ + + +
Fee Report
+
+
+ +
+ + Daily ({{information?.smaller_currency_unit}}) + {{fees?.day_fee_sum}} + + + + Weekly ({{information?.smaller_currency_unit}}) + {{fees?.week_fee_sum}} + + + + Monthly ({{information?.smaller_currency_unit}}) + {{fees?.month_fee_sum}} + + +
+ + +
+
+ + + +
Channel Status
+
+
+ +
+ + Active +

{{activeChannels}}

+ +
+ + Inactive +

{{inactiveChannels}}

+ +
+ + Pending +

{{pendingChannels}}

+ +
+
+ + +
+
+
+
+
+ + + +
Local-Remote Channel Capacity
+
+
+ +
+
+ + +
+
+ + +
+
+
+
+ + + +
Network Information
+
+
+ +
+ + Network Capacity ({{information?.currency_unit}}) + {{networkInfo?.btc_total_network_capacity | number}} + Network Capacity ({{information?.smaller_currency_unit}}) + {{networkInfo?.total_network_capacity | number}} + + + + Number of Nodes + {{networkInfo?.num_nodes | number}} + + + + Number of Channels + {{networkInfo?.num_channels | number}} + + + + Max Out Degree + {{networkInfo?.max_out_degree | number}} + + + + Avg Out Degree + {{networkInfo?.avg_out_degree | number:'1.0-2'}} + + + + Max Channel Size ({{information?.currency_unit}}) + Max Channel Size ({{information?.smaller_currency_unit}}) + {{networkInfo?.btc_max_channel_size | number}} + {{networkInfo?.max_channel_size | number}} + + + + Avg Channel Size ({{information?.currency_unit}}) + Avg Channel Size ({{information?.smaller_currency_unit}}) + {{networkInfo?.btc_avg_channel_size | number}} + {{networkInfo?.avg_channel_size | number:'1.0-2'}} + + + + Min Channel Size ({{information?.currency_unit}}) + Min Channel Size ({{information?.smaller_currency_unit}}) + {{networkInfo?.btc_min_channel_size | number}} + {{networkInfo?.min_channel_size | number}} + + +
+ + +
+
+
+
+

Sats

diff --git a/src/app/pages/home/home.component.scss b/src/app/pages/home/home.component.scss new file mode 100644 index 00000000..ca8052d8 --- /dev/null +++ b/src/app/pages/home/home.component.scss @@ -0,0 +1,12 @@ +.network-info-list .mat-list-item { + height: 44px; +} + +.mat-column-bytes_sent, .mat-column-bytes_recv, .mat-column-sat_sent, .mat-column-sat_recv, .mat-column-inbound, .mat-column-ping_time { + flex: 0 0 8%; + min-width: 80px; +} + +.card-chnl-balances { + min-height: 354px; +} diff --git a/src/app/pages/home/home.component.spec.ts b/src/app/pages/home/home.component.spec.ts new file mode 100644 index 00000000..490e81bd --- /dev/null +++ b/src/app/pages/home/home.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HomeComponent } from './home.component'; + +describe('HomeComponent', () => { + let component: HomeComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ HomeComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/home/home.component.ts b/src/app/pages/home/home.component.ts new file mode 100644 index 00000000..4fae5f12 --- /dev/null +++ b/src/app/pages/home/home.component.ts @@ -0,0 +1,148 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; + +import { LoggerService } from '../../shared/services/logger.service'; +import { GetInfo, NetworkInfo, Fees, Peer } from '../../shared/models/lndModels'; +import { Settings } from '../../shared/models/RTLconfig'; + +import * as fromRTLReducer from '../../shared/store/rtl.reducers'; + +@Component({ + selector: 'rtl-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'] +}) +export class HomeComponent implements OnInit, OnDestroy { + public settings: Settings; + public fees: Fees; + public information: GetInfo = {}; + public remainder = 0; + public totalPeers = -1; + public totalBalance = ''; + public channelBalance = ''; + public BTCtotalBalance = ''; + public BTCchannelBalance = ''; + public networkInfo: NetworkInfo = {}; + public flgLoading: Array = [true, true, true, true, true, true, true, true]; // 0: Info, 1: Fee, 2: Wallet, 3: Channel, 4: Network + private unsub: Array> = [new Subject(), new Subject(), new Subject()]; + public channels: any; + public position = 'below'; + public activeChannels = 0; + public inactiveChannels = 0; + public pendingChannels = 0; + public peers: Peer[] = []; + barPadding = 0; + maxBalanceValue = 0; + totalBalances = [...[{'name': 'Local Balance', 'value': 0}, {'name': 'Remote Balance', 'value': 0}]]; + flgTotalCalculated = false; + view = []; + yAxisLabel = 'Balance'; + colorScheme = {domain: ['#FFFFFF']}; + + constructor(private logger: LoggerService, private store: Store) { + switch (true) { + case (window.innerWidth <= 730): + this.view = [250, 352]; + break; + case (window.innerWidth > 415 && window.innerWidth <= 730): + this.view = [280, 352]; + break; + case (window.innerWidth > 730 && window.innerWidth <= 1024): + this.view = [300, 352]; + break; + case (window.innerWidth > 1024 && window.innerWidth <= 1280): + this.view = [350, 352]; + break; + default: + this.view = [300, 352]; + break; + } + Object.assign(this, this.totalBalances); + } + + ngOnInit() { + this.flgTotalCalculated = false; + this.store.select('rtlRoot') + .pipe(takeUntil(this.unsub[0])) + .subscribe((rtlStore: fromRTLReducer.State) => { + rtlStore.effectErrors.forEach(effectsErr => { + if (effectsErr.action === 'FetchInfo') { + this.flgLoading[0] = 'error'; + } + if (effectsErr.action === 'FetchFees') { + this.flgLoading[1] = 'error'; + } + if (effectsErr.action === 'FetchBalance/blockchain') { + this.flgLoading[2] = 'error'; + } + if (effectsErr.action === 'FetchBalance/channels') { + this.flgLoading[3] = 'error'; + } + if (effectsErr.action === 'FetchNetwork') { + this.flgLoading[4] = 'error'; + } + if (effectsErr.action === 'FetchChannels/all') { + this.flgLoading[5] = 'error'; + this.flgLoading[6] = 'error'; + } + }); + this.settings = rtlStore.settings; + this.information = rtlStore.information; + if (this.flgLoading[0] !== 'error') { + this.flgLoading[0] = (undefined !== this.information.identity_pubkey) ? false : true; + } + + this.fees = rtlStore.fees; + if (this.flgLoading[1] !== 'error') { + this.flgLoading[1] = (undefined !== this.fees.day_fee_sum) ? false : true; + } + + this.totalBalance = rtlStore.blockchainBalance.total_balance; + this.BTCtotalBalance = rtlStore.blockchainBalance.btc_total_balance; + if (this.flgLoading[2] !== 'error') { + this.flgLoading[2] = ('' !== this.totalBalance) ? false : true; + } + + this.channelBalance = rtlStore.channelBalance.balance; + this.BTCchannelBalance = rtlStore.channelBalance.btc_balance; + if (this.flgLoading[3] !== 'error') { + this.flgLoading[3] = ('' !== this.channelBalance) ? false : true; + } + + this.networkInfo = rtlStore.networkInfo; + if (this.flgLoading[4] !== 'error') { + this.flgLoading[4] = (undefined !== this.networkInfo.num_nodes) ? false : true; + } + + this.totalBalances = [...[{'name': 'Local Balance', 'value': +rtlStore.totalLocalBalance}, {'name': 'Remote Balance', 'value': +rtlStore.totalRemoteBalance}]]; + this.maxBalanceValue = (rtlStore.totalLocalBalance > rtlStore.totalRemoteBalance) ? rtlStore.totalLocalBalance : rtlStore.totalRemoteBalance; + if (rtlStore.totalLocalBalance >= 0 && rtlStore.totalRemoteBalance >= 0) { + this.flgTotalCalculated = true; + if (this.flgLoading[5] !== 'error') { + this.flgLoading[5] = false; + } + } + + this.activeChannels = rtlStore.numberOfActiveChannels; + this.inactiveChannels = rtlStore.numberOfInactiveChannels; + this.pendingChannels = (undefined !== rtlStore.pendingChannels.pending_open_channels) ? rtlStore.pendingChannels.pending_open_channels.length : 0; + if (rtlStore.totalLocalBalance >= 0 && rtlStore.totalRemoteBalance >= 0 && this.flgLoading[6] !== 'error') { + this.flgLoading[6] = false; + } + + this.totalPeers = rtlStore.peers.length; + + this.logger.info(rtlStore); + }); + } + + ngOnDestroy() { + this.unsub.forEach(completeSub => { + completeSub.next(); + completeSub.complete(); + }); + } + +} diff --git a/src/app/pages/invoices/invoices.component.html b/src/app/pages/invoices/invoices.component.html new file mode 100644 index 00000000..53477439 --- /dev/null +++ b/src/app/pages/invoices/invoices.component.html @@ -0,0 +1,77 @@ +
+
+ + + +

Invoices

+
+
+ +
+ + + + + + +
+ +
+
+ +
+
+
+
+
+
+ + +
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Creation Date {{invoice.creation_date_str}} Settle Date {{invoice.settle_date_str}} Memo {{invoice.memo}} Value ({{(settings?.satsToBTC) ? information?.currency_unit : information?.smaller_currency_unit}}) {{(settings?.satsToBTC) ? (invoice?.btc_value | number:'1.0-3') : (invoice?.value | number)}} Settled {{invoice.settled}} Expiry (Sec) {{invoice.expiry | number}} CLTV Expiry {{invoice.cltv_expiry | number}} Amount Paid ({{(settings?.satsToBTC) ? information?.currency_unit : information?.smaller_currency_unit}}) {{(settings?.satsToBTC) ? (invoice?.btc_amt_paid_sat | number:'1.0-3') : (invoice?.amt_paid_sat | number)}}
+
+
+
+
+
diff --git a/src/app/pages/invoices/invoices.component.scss b/src/app/pages/invoices/invoices.component.scss new file mode 100644 index 00000000..785c97c4 --- /dev/null +++ b/src/app/pages/invoices/invoices.component.scss @@ -0,0 +1,23 @@ +.mat-column-value { + padding-right: 1rem; +} + +.mat-column-settled { + padding-left: 1rem; +} + +table { + width:100%; +} + +.table-container { + height: 68vh; + overflow: auto; +} + +@media screen and (max-width: 414px) { + .table-container { + max-height: 31vh; + overflow: auto; + } +} diff --git a/src/app/pages/invoices/invoices.component.ts b/src/app/pages/invoices/invoices.component.ts new file mode 100644 index 00000000..ab20c975 --- /dev/null +++ b/src/app/pages/invoices/invoices.component.ts @@ -0,0 +1,141 @@ +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { formatDate } from '@angular/common'; +import { Subject } from 'rxjs'; +import { takeUntil, filter } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { Actions } from '@ngrx/effects'; + +import { MatTableDataSource, MatSort } from '@angular/material'; +import { Settings } from '../../shared/models/RTLconfig'; +import { GetInfo, Invoice } from '../../shared/models/lndModels'; +import { LoggerService } from '../../shared/services/logger.service'; + +import { newlyAddedRowAnimation } from '../../shared/animation/row-animation'; +import * as RTLActions from '../../shared/store/rtl.actions'; +import * as fromRTLReducer from '../../shared/store/rtl.reducers'; + +@Component({ + selector: 'rtl-invoices', + templateUrl: './invoices.component.html', + styleUrls: ['./invoices.component.scss'], + animations: [newlyAddedRowAnimation] +}) +export class InvoicesComponent implements OnInit, OnDestroy { + @ViewChild(MatSort) sort: MatSort; + public newlyAddedInvoiceMemo = ''; + public newlyAddedInvoiceValue = 0; + public flgAnimate = true; + public settings: Settings; + public memo = ''; + public invoiceValue: number; + public displayedColumns = []; + public invoicePaymentReq = ''; + public invoices: any; + public information: GetInfo = {}; + public flgLoading: Array = [true]; + private unSubs: Array> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject()]; + + constructor(private logger: LoggerService, private store: Store, private actions$: Actions) { + switch (true) { + case (window.innerWidth <= 415): + this.displayedColumns = ['creation_date', 'memo', 'value', 'settled']; + break; + case (window.innerWidth > 415 && window.innerWidth <= 730): + this.displayedColumns = ['creation_date', 'settle_date', 'memo', 'value', 'settled', 'amt_paid_sat']; + break; + case (window.innerWidth > 730 && window.innerWidth <= 1024): + this.displayedColumns = ['creation_date', 'settle_date', 'memo', 'value', 'settled', 'amt_paid_sat']; + break; + case (window.innerWidth > 1024 && window.innerWidth <= 1280): + this.displayedColumns = ['creation_date', 'settle_date', 'memo', 'value', 'settled', 'amt_paid_sat', 'expiry', 'cltv_expiry']; + break; + default: + this.displayedColumns = ['creation_date', 'settle_date', 'memo', 'value', 'settled', 'amt_paid_sat', 'expiry', 'cltv_expiry']; + break; + } + } + + ngOnInit() { + this.store.select('rtlRoot') + .pipe(takeUntil(this.unSubs[0])) + .subscribe((rtlStore: fromRTLReducer.State) => { + rtlStore.effectErrors.forEach(effectsErr => { + if (effectsErr.action === 'FetchInvoices') { + this.flgLoading[0] = 'error'; + } + }); + this.settings = rtlStore.settings; + this.information = rtlStore.information; + this.logger.info(rtlStore); + this.loadInvoicesTable(rtlStore.invoices); + if (this.flgLoading[0] !== 'error') { + this.flgLoading[0] = (undefined !== rtlStore.invoices[0]) ? false : true; + } + }); + + } + + onAddInvoice(form: any) { + this.flgAnimate = true; + this.newlyAddedInvoiceMemo = this.memo; + this.newlyAddedInvoiceValue = this.invoiceValue; + this.store.dispatch(new RTLActions.OpenSpinner('Adding Invoice...')); + this.store.dispatch(new RTLActions.SaveNewInvoice({memo: this.memo, invoiceValue: this.invoiceValue})); + this.actions$ + .pipe( + takeUntil(this.unSubs[1]), + filter((action) => action.type === RTLActions.ADD_INVOICE) + ).subscribe((newInvoiceAction: RTLActions.AddInvoice) => { + this.logger.info(newInvoiceAction.payload); + this.invoicePaymentReq = newInvoiceAction.payload.payment_request; + }); + + } + + onInvoiceClick(selRow: Invoice, event: any) { + const selInvoice = this.invoices.data.filter(invoice => { + return invoice.payment_request === selRow.payment_request; + })[0]; + const reorderedInvoice = JSON.parse(JSON.stringify(selInvoice, [ + 'creation_date_str', 'settle_date_str', 'memo', 'receipt', 'r_preimage', 'r_hash', 'value', 'settled', 'payment_request', + 'description_hash', 'expiry', 'fallback_addr', 'cltv_expiry', 'route_hints', 'private', 'add_index', 'settle_index', + 'amt_paid', 'amt_paid_sat', 'amt_paid_msat' + ] , 2)); + this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: { + type: 'INFO', + message: JSON.stringify(reorderedInvoice) + }})); + } + + loadInvoicesTable(invoices) { + this.invoices = new MatTableDataSource([...invoices]); + this.invoices.sort = this.sort; + this.invoices.data.forEach(invoice => { + if (undefined !== invoice.creation_date_str) { + invoice.creation_date_str = (invoice.creation_date_str === '') ? '' : formatDate(invoice.creation_date_str, 'MMM/dd/yy HH:mm:ss', 'en-US'); + } + if (undefined !== invoice.settle_date_str) { + invoice.settle_date_str = (invoice.settle_date_str === '') ? '' : formatDate(invoice.settle_date_str, 'MMM/dd/yy HH:mm:ss', 'en-US'); + } + }); + setTimeout(() => { this.flgAnimate = false; }, 5000); + this.logger.info(this.invoices); + } + + resetData() { + this.memo = ''; + this.invoiceValue = 0; + } + + applyFilter(selFilter: string) { + this.invoices.filter = selFilter; + } + + ngOnDestroy() { + this.unSubs.forEach(completeSub => { + completeSub.next(); + completeSub.complete(); + }); + } + +} diff --git a/src/app/pages/lookups/channel-lookup/channel-lookup.component.css b/src/app/pages/lookups/channel-lookup/channel-lookup.component.css new file mode 100644 index 00000000..ef68f255 --- /dev/null +++ b/src/app/pages/lookups/channel-lookup/channel-lookup.component.css @@ -0,0 +1,3 @@ +.mat-list-base .mat-list-item, .mat-list-base .mat-list-option { + height: 38px !important; +} \ No newline at end of file diff --git a/src/app/pages/lookups/channel-lookup/channel-lookup.component.html b/src/app/pages/lookups/channel-lookup/channel-lookup.component.html new file mode 100644 index 00000000..0ed05159 --- /dev/null +++ b/src/app/pages/lookups/channel-lookup/channel-lookup.component.html @@ -0,0 +1,109 @@ +
+ + + + Channel Id + {{lookupResult.channel_id}} + + + + Channel Point + {{lookupResult.chan_point}} + + + + Last Update + {{lookupResult.last_update_str}} + + + + Capacity (Sats) + {{lookupResult.capacity | number}} + + + + +
+
+
+ + + +
Node 1
+
Node 1 (Your Node)
+
+
+ + + {{lookupResult.node1_pub}} + + + + Time Lock Delta + {{lookupResult.node1_policy.time_lock_delta}} + + + + Min HTLC + {{lookupResult.node1_policy.min_htlc}} + + + + Fee Base Msat + {{lookupResult.node1_policy.fee_base_msat}} + + + + Fee Rate Milli Msat + {{lookupResult.node1_policy.fee_rate_milli_msat}} + + + + Disabled + {{lookupResult.node1_policy.disabled}} + + + +
+
+
+ + + +
Node 2
+
Node 2 (Your Node)
+
+
+ + + {{lookupResult.node2_pub}} + + + + Time Lock Delta + {{lookupResult.node2_policy.time_lock_delta}} + + + + Min HTLC + {{lookupResult.node2_policy.min_htlc}} + + + + Fee Base Msat + {{lookupResult.node2_policy.fee_base_msat}} + + + + Fee Rate Milli Msat + {{lookupResult.node2_policy.fee_rate_milli_msat}} + + + + Disabled + {{lookupResult.node2_policy.disabled}} + + + +
+
diff --git a/src/app/pages/lookups/channel-lookup/channel-lookup.component.spec.ts b/src/app/pages/lookups/channel-lookup/channel-lookup.component.spec.ts new file mode 100644 index 00000000..919141e4 --- /dev/null +++ b/src/app/pages/lookups/channel-lookup/channel-lookup.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChannelLookupComponent } from './channel-lookup.component'; + +describe('ChannelLookupComponent', () => { + let component: ChannelLookupComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ChannelLookupComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ChannelLookupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/lookups/channel-lookup/channel-lookup.component.ts b/src/app/pages/lookups/channel-lookup/channel-lookup.component.ts new file mode 100644 index 00000000..8e39ff24 --- /dev/null +++ b/src/app/pages/lookups/channel-lookup/channel-lookup.component.ts @@ -0,0 +1,40 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { formatDate } from '@angular/common'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; + +import { ChannelEdge } from '../../../shared/models/lndModels'; +import * as fromRTLReducer from '../../../shared/store/rtl.reducers'; + +@Component({ + selector: 'rtl-channel-lookup', + templateUrl: './channel-lookup.component.html', + styleUrls: ['./channel-lookup.component.css'] +}) +export class ChannelLookupComponent implements OnInit { + @Input() lookupResult: ChannelEdge; + public node1_match = false; + public node2_match = false; + private unSubs: Array> = [new Subject(), new Subject(), new Subject(), new Subject()]; + + constructor(private store: Store) { } + + ngOnInit() { + if (undefined !== this.lookupResult && undefined !== this.lookupResult.last_update_str) { + this.lookupResult.last_update_str = (this.lookupResult.last_update_str === '') ? + '' : formatDate(this.lookupResult.last_update_str, 'MMM/dd/yy HH:mm:ss', 'en-US'); + } + this.store.select('rtlRoot') + .pipe(takeUntil(this.unSubs[0])) + .subscribe((rtlStore: fromRTLReducer.State) => { + if (this.lookupResult.node1_pub === rtlStore.information.identity_pubkey) { + this.node1_match = true; + } + if (this.lookupResult.node2_pub === rtlStore.information.identity_pubkey) { + this.node2_match = true; + } + }); + } + +} diff --git a/src/app/pages/lookups/lookups.component.html b/src/app/pages/lookups/lookups.component.html new file mode 100644 index 00000000..501d586c --- /dev/null +++ b/src/app/pages/lookups/lookups.component.html @@ -0,0 +1,47 @@ +
+
+ + + +

Lookups

+
+
+ +
+ + + + {{lookupField.name}} + + + + + + +
+ +
+
+ +
+
+
+
+
+
+ + + +

{{selectedField.name}} Details

+
+
+ +
+ + +

Error! Unable to find details!

+
+
+
+
+
diff --git a/src/app/pages/lookups/lookups.component.scss b/src/app/pages/lookups/lookups.component.scss new file mode 100644 index 00000000..d45059cb --- /dev/null +++ b/src/app/pages/lookups/lookups.component.scss @@ -0,0 +1,14 @@ +.tree-invisible { + display: none; +} + +.lookup-tree ul, +.lookup-tree li { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +.pl-3 { + padding-left: 3rem; +} \ No newline at end of file diff --git a/src/app/pages/lookups/lookups.component.spec.ts b/src/app/pages/lookups/lookups.component.spec.ts new file mode 100644 index 00000000..428d1de1 --- /dev/null +++ b/src/app/pages/lookups/lookups.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LookupsComponent } from './lookups.component'; + +describe('LookupsComponent', () => { + let component: LookupsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LookupsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LookupsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/lookups/lookups.component.ts b/src/app/pages/lookups/lookups.component.ts new file mode 100644 index 00000000..79ae3cd9 --- /dev/null +++ b/src/app/pages/lookups/lookups.component.ts @@ -0,0 +1,94 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil, filter } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { Actions } from '@ngrx/effects'; + +import { LoggerService } from '../../shared/services/logger.service'; + +import * as RTLActions from '../../shared/store/rtl.actions'; +import * as fromRTLReducer from '../../shared/store/rtl.reducers'; + +@Component({ + selector: 'rtl-lookups', + templateUrl: './lookups.component.html', + styleUrls: ['./lookups.component.scss'] +}) +export class LookupsComponent implements OnInit, OnDestroy { + public lookupKey = ''; + public lookupValue = {}; + public flgSetLookupValue = false; + public temp: any; + public messageObj = []; + public selectedField = { id: '0', name: 'Node', placeholder: 'Pubkey'}; + public lookupFields = [ + { id: '0', name: 'Node', placeholder: 'Pubkey'}, + { id: '1', name: 'Channel', placeholder: 'Channel ID'} + ]; + public flgLoading: Array = [true]; + private unSubs: Array> = [new Subject()]; + + constructor(private logger: LoggerService, private store: Store, private actions$: Actions) {} + + ngOnInit() { + this.actions$ + .pipe( + takeUntil(this.unSubs[0]), + filter((action) => (action.type === RTLActions.SET_LOOKUP || action.type === RTLActions.EFFECT_ERROR)) + ).subscribe((resLookup: RTLActions.SetLookup) => { + if (resLookup.payload.action === 'Lookup') { + this.flgLoading[0] = 'error'; + } else { + this.flgLoading[0] = true; + this.lookupValue = JSON.parse(JSON.stringify(resLookup.payload)); + this.flgSetLookupValue = true; + this.logger.info(this.lookupValue); + } + }); + } + + onLookup() { + this.flgSetLookupValue = false; + this.lookupValue = {}; + this.store.dispatch(new RTLActions.OpenSpinner('Searching ' + this.selectedField.name + '...')); + switch (this.selectedField.id) { + case '0': + this.store.dispatch(new RTLActions.PeerLookup(this.lookupKey.trim())); + break; + case '1': + this.store.dispatch(new RTLActions.ChannelLookup(this.lookupKey.trim())); + break; + default: + break; + } + } + + onSelectChange(event: any) { + this.flgSetLookupValue = false; + this.lookupKey = ''; + this.lookupValue = {}; + } + + resetData() { + this.flgSetLookupValue = false; + this.lookupKey = ''; + this.selectedField = { id: '0', name: 'Node', placeholder: 'Pubkey'}; + this.lookupValue = {}; + this.flgLoading.forEach((flg, i) => { + this.flgLoading[i] = true; + }); + } + + clearLookupValue() { + this.lookupValue = {}; + this.flgSetLookupValue = false; + } + + ngOnDestroy() { + this.unSubs.forEach(completeSub => { + completeSub.next(); + completeSub.complete(); + }); + } + +} diff --git a/src/app/pages/lookups/node-lookup/node-lookup.component.css b/src/app/pages/lookups/node-lookup/node-lookup.component.css new file mode 100644 index 00000000..ddab909a --- /dev/null +++ b/src/app/pages/lookups/node-lookup/node-lookup.component.css @@ -0,0 +1,7 @@ +.mat-table { + width:99%; +} + +.mat-list-base .mat-list-item, .mat-list-base .mat-list-option { + height: 38px !important; +} diff --git a/src/app/pages/lookups/node-lookup/node-lookup.component.html b/src/app/pages/lookups/node-lookup/node-lookup.component.html new file mode 100644 index 00000000..719dbef2 --- /dev/null +++ b/src/app/pages/lookups/node-lookup/node-lookup.component.html @@ -0,0 +1,56 @@ +
+
+ + +
+ + Alias + {{lookupResult.node.alias}} + + + + Pub Key + {{lookupResult.node.pub_key}} + + + + Color + {{lookupResult.node?.color}} + + + + Last Update + {{lookupResult.node.last_update_str}} + + + + Total Capacity (Sats) + {{lookupResult.total_capacity | number}} + + + + Number of Channels + {{lookupResult.num_channels | number}} + + + + + Addresses + + + Network +
{{address?.network}}
+
+ + Address +
{{address?.addr}}
+
+ + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/src/app/pages/lookups/node-lookup/node-lookup.component.spec.ts b/src/app/pages/lookups/node-lookup/node-lookup.component.spec.ts new file mode 100644 index 00000000..64bc61bf --- /dev/null +++ b/src/app/pages/lookups/node-lookup/node-lookup.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NodeLookupComponent } from './node-lookup.component'; + +describe('NodeLookupComponent', () => { + let component: NodeLookupComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ NodeLookupComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NodeLookupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/lookups/node-lookup/node-lookup.component.ts b/src/app/pages/lookups/node-lookup/node-lookup.component.ts new file mode 100644 index 00000000..dfb8a241 --- /dev/null +++ b/src/app/pages/lookups/node-lookup/node-lookup.component.ts @@ -0,0 +1,24 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { formatDate } from '@angular/common'; + +import { GraphNode } from '../../../shared/models/lndModels'; + +@Component({ + selector: 'rtl-node-lookup', + templateUrl: './node-lookup.component.html', + styleUrls: ['./node-lookup.component.css'] +}) +export class NodeLookupComponent implements OnInit { + @Input() lookupResult: GraphNode; + public displayedColumns = ['network', 'addr']; + + constructor() { } + + ngOnInit() { + if (undefined !== this.lookupResult && undefined !== this.lookupResult.node && undefined !== this.lookupResult.node.last_update_str) { + this.lookupResult.node.last_update_str = (this.lookupResult.node.last_update_str === '') ? + '' : formatDate(this.lookupResult.node.last_update_str, 'MMM/dd/yy HH:mm:ss', 'en-US'); + } + } + +} diff --git a/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.html b/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.html new file mode 100644 index 00000000..816d2551 --- /dev/null +++ b/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.html @@ -0,0 +1,19 @@ +
+
+ +
+ + +
+ +
+
+
+
+
diff --git a/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.scss b/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.scss new file mode 100644 index 00000000..010435ed --- /dev/null +++ b/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.scss @@ -0,0 +1,19 @@ +.mat-menu-panel.child-menu { + min-width: 88px; + width:88px; + border-radius: 0; + margin-left: 30%; + margin-top: 6%; + .mat-menu-content { + .mat-menu-item { + padding: 0; + margin-top: -3px; + .mat-icon { + margin-right: 0; + } + button { + border-radius: 0; + } + } + } +} diff --git a/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.spec.ts b/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.spec.ts new file mode 100644 index 00000000..04191181 --- /dev/null +++ b/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HorizontalNavigationComponent } from './horizontal-navigation.component'; + +describe('HorizontalNavigationComponent', () => { + let component: HorizontalNavigationComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ HorizontalNavigationComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HorizontalNavigationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.ts b/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.ts new file mode 100644 index 00000000..c908cf98 --- /dev/null +++ b/src/app/pages/navigation/horizontal-navigation/horizontal-navigation.component.ts @@ -0,0 +1,69 @@ +import { Component, OnInit } from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil, filter } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { Actions } from '@ngrx/effects'; + +import { LoggerService } from '../../../shared/services/logger.service'; +import { MENU_DATA } from '../../../shared/models/navMenu'; + +import { RTLEffects } from '../../../shared/store/rtl.effects'; +import * as RTLActions from '../../../shared/store/rtl.actions'; +import * as fromRTLReducer from '../../../shared/store/rtl.reducers'; + +@Component({ + selector: 'rtl-horizontal-navigation', + templateUrl: './horizontal-navigation.component.html', + styleUrls: ['./horizontal-navigation.component.scss'] +}) +export class HorizontalNavigationComponent implements OnInit { + public menuNodes = []; + public logoutNode = []; + public showLogout = false; + public numPendingChannels = 0; + private unSubs = [new Subject(), new Subject(), new Subject()]; + + constructor(private logger: LoggerService, private store: Store, private actions$: Actions, private rtlEffects: RTLEffects) { + this.menuNodes = MENU_DATA.children; + } + + ngOnInit() { + this.store.select('rtlRoot') + .pipe(takeUntil(this.unSubs[0])) + .subscribe((rtlStore: fromRTLReducer.State) => { + this.numPendingChannels = rtlStore.numberOfPendingChannels; + }); + this.actions$ + .pipe( + takeUntil(this.unSubs[2]), + filter((action) => action.type === RTLActions.SIGNOUT || action.type === RTLActions.SIGNIN) + ).subscribe((action) => { + this.logger.warn(action); + if (action.type === RTLActions.SIGNIN) { + this.menuNodes.push({id: 100, parentId: 0, name: 'Logout', icon: 'eject'}); + } + if (action.type === RTLActions.SIGNOUT) { + this.menuNodes.pop(); + } + }); + if (sessionStorage.getItem('token')) { + this.menuNodes.push({id: 100, parentId: 0, name: 'Logout', icon: 'eject'}); + } + } + + onClick(node) { + if (node.name === 'Logout') { + this.store.dispatch(new RTLActions.OpenConfirmation({ + width: '70%', data: { type: 'CONFIRM', titleMessage: 'Logout from this device?', noBtnText: 'Cancel', yesBtnText: 'Logout' + }})); + this.rtlEffects.closeConfirm + .pipe(takeUntil(this.unSubs[1])) + .subscribe(confirmRes => { + if (confirmRes) { + this.showLogout = false; + this.store.dispatch(new RTLActions.Signout()); + } + }); + } + } +} diff --git a/src/app/pages/navigation/side-navigation/side-navigation.component.html b/src/app/pages/navigation/side-navigation/side-navigation.component.html new file mode 100644 index 00000000..2f549a82 --- /dev/null +++ b/src/app/pages/navigation/side-navigation/side-navigation.component.html @@ -0,0 +1,65 @@ + + + + + + + + +
+
+

Alias: {{information?.alias}}

+

Chain: {{informationChain.chain | titlecase}} [{{informationChain.network | titlecase}}]

+

LND Version: {{information?.version}}

+
+
+ + + + {{node.icon}} + {{node.name}} + + + +
+
+ {{node.icon}}{{node.name}} +
+ +
+
+
+ + + {{node.icon}} + {{node.name}} + + + + + + {{node.icon}} + {{node.name}} + + + +
+ {{node.icon}} + {{node.name}} +
+
+ +
+
+
+ + + {{node.icon}} + {{node.name}} + + diff --git a/src/app/pages/navigation/side-navigation/side-navigation.component.scss b/src/app/pages/navigation/side-navigation/side-navigation.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/pages/navigation/side-navigation/side-navigation.component.spec.ts b/src/app/pages/navigation/side-navigation/side-navigation.component.spec.ts new file mode 100644 index 00000000..660c9832 --- /dev/null +++ b/src/app/pages/navigation/side-navigation/side-navigation.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SideNavigationComponent } from './side-navigation.component'; + +describe('SideNavigationComponent', () => { + let component: SideNavigationComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SideNavigationComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SideNavigationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/navigation/side-navigation/side-navigation.component.ts b/src/app/pages/navigation/side-navigation/side-navigation.component.ts new file mode 100644 index 00000000..d05bb1be --- /dev/null +++ b/src/app/pages/navigation/side-navigation/side-navigation.component.ts @@ -0,0 +1,153 @@ +import { Component, OnInit, OnDestroy, Output, EventEmitter } from '@angular/core'; +import { Router } from '@angular/router'; +import { Subject, Observable, of } from 'rxjs'; +import { takeUntil, filter } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { Actions } from '@ngrx/effects'; +import { environment } from '../../../../environments/environment'; + +import { FlatTreeControl } from '@angular/cdk/tree'; +import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree'; + +import { Settings } from '../../../shared/models/RTLconfig'; +import { LoggerService } from '../../../shared/services/logger.service'; +import { GetInfo, GetInfoChain } from '../../../shared/models/lndModels'; +import { MenuNode, FlatMenuNode, MENU_DATA } from '../../../shared/models/navMenu'; + +import { RTLEffects } from '../../../shared/store/rtl.effects'; +import * as RTLActions from '../../../shared/store/rtl.actions'; +import * as fromRTLReducer from '../../../shared/store/rtl.reducers'; + +@Component({ + selector: 'rtl-side-navigation', + templateUrl: './side-navigation.component.html', + styleUrls: ['./side-navigation.component.scss'] +}) +export class SideNavigationComponent implements OnInit, OnDestroy { + @Output() ChildNavClicked = new EventEmitter(); + public version = ''; + public settings: Settings; + public information: GetInfo = {}; + public informationChain: GetInfoChain = {}; + public flgLoading = true; + public logoutNode = [{id: 100, parentId: 0, name: 'Logout', icon: 'eject'}]; + public showLogout = false; + public numPendingChannels = 0; + public smallScreen = false; + private unSubs = [new Subject(), new Subject(), new Subject()]; + treeControl: FlatTreeControl; + treeControlLogout: FlatTreeControl; + treeFlattener: MatTreeFlattener; + treeFlattenerLogout: MatTreeFlattener; + navMenus: MatTreeFlatDataSource; + navMenusLogout: MatTreeFlatDataSource; + + constructor(private logger: LoggerService, private store: Store, private actions$: Actions, private rtlEffects: RTLEffects, private router: Router) { + this.version = environment.VERSION; + if (MENU_DATA.children[MENU_DATA.children.length - 1].id === 100) { + MENU_DATA.children.pop(); + } + this.treeFlattener = new MatTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren); + this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable); + this.navMenus = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); + this.navMenus.data = MENU_DATA.children; + + this.treeFlattenerLogout = new MatTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren); + this.treeControlLogout = new FlatTreeControl(this.getLevel, this.isExpandable); + this.navMenusLogout = new MatTreeFlatDataSource(this.treeControlLogout, this.treeFlattenerLogout); + this.navMenusLogout.data = this.logoutNode; + } + + ngOnInit() { + this.store.select('rtlRoot') + .pipe(takeUntil(this.unSubs[0])) + .subscribe((rtlStore: fromRTLReducer.State) => { + this.settings = rtlStore.settings; + this.information = rtlStore.information; + this.numPendingChannels = rtlStore.numberOfPendingChannels; + + if (undefined !== this.information.identity_pubkey) { + if (undefined !== this.information.chains && typeof this.information.chains[0] === 'string') { + this.informationChain.chain = this.information.chains[0].toString(); + this.informationChain.network = (this.information.testnet) ? 'Testnet' : 'Mainnet'; + } else if (typeof this.information.chains[0] === 'object' && this.information.chains[0].hasOwnProperty('chain')) { + const getInfoChain = this.information.chains[0]; + this.informationChain.chain = getInfoChain.chain; + this.informationChain.network = getInfoChain.network; + } + } else { + this.informationChain.chain = ''; + this.informationChain.network = ''; + } + + this.flgLoading = (undefined !== this.information.identity_pubkey) ? false : true; + this.showLogout = (sessionStorage.getItem('token')) ? true : false; + if (!sessionStorage.getItem('token')) { + this.flgLoading = false; + } + if (window.innerWidth <= 414) { + this.smallScreen = true; + } + this.logger.info(rtlStore); + }); + this.actions$ + .pipe( + takeUntil(this.unSubs[2]), + filter((action) => action.type === RTLActions.SIGNOUT) + ).subscribe(() => { + this.showLogout = false; + }); + } + + private transformer(node: MenuNode, level: number) { return new FlatMenuNode(!!node.children, level, node.id, node.parentId, node.name, node.icon, node.link); } + + private getLevel(node: FlatMenuNode) { return node.level; } + + private isExpandable(node: FlatMenuNode) { return node.expandable; } + + private getChildren(node: MenuNode): Observable { return of(node.children); } + + hasChild(_: number, _nodeData: FlatMenuNode) { return _nodeData.expandable; } + + toggleTree(node: FlatMenuNode) { + this.treeControl.collapseAll(); + if (node.parentId === 0) { + this.treeControl.expandDescendants(node); + this.router.navigate([node.link]); + } else { + const parentNode = this.treeControl.dataNodes.filter(dataNode => { + return dataNode.id === node.parentId; + })[0]; + this.treeControl.expandDescendants(parentNode); + } + } + + onClick(node: MenuNode) { + if (node.name === 'Logout') { + this.store.dispatch(new RTLActions.OpenConfirmation({ + width: '70%', data: { type: 'CONFIRM', titleMessage: 'Logout from this device?', noBtnText: 'Cancel', yesBtnText: 'Logout' + }})); + this.rtlEffects.closeConfirm + .pipe(takeUntil(this.unSubs[1])) + .subscribe(confirmRes => { + if (confirmRes) { + this.showLogout = false; + this.store.dispatch(new RTLActions.Signout()); + } + }); + } + this.ChildNavClicked.emit(node); + } + + onChildNavClicked(node) { + this.ChildNavClicked.emit(node); + } + + ngOnDestroy() { + this.unSubs.forEach(completeSub => { + completeSub.next(); + completeSub.complete(); + }); + } + +} diff --git a/src/app/pages/navigation/top-menu/top-menu.component.html b/src/app/pages/navigation/top-menu/top-menu.component.html new file mode 100644 index 00000000..1946b13a --- /dev/null +++ b/src/app/pages/navigation/top-menu/top-menu.component.html @@ -0,0 +1,22 @@ + +

+ publish + Version: {{version}} +

+ + perm_data_setting + Node Config + + + help + Help + + + eject + Logout + +
+ + diff --git a/src/app/pages/navigation/top-menu/top-menu.component.scss b/src/app/pages/navigation/top-menu/top-menu.component.scss new file mode 100644 index 00000000..6a2bd51c --- /dev/null +++ b/src/app/pages/navigation/top-menu/top-menu.component.scss @@ -0,0 +1,26 @@ +.mat-menu-panel.top-menu{ + .mat-toolbar, .mat-toolbar-row{ + height: 100px !important; + padding: 0 16px !important; + } + .info-block{ + width: 230px; + p{ + font-size: 16px; + line-height: 22px; + text-align: center; + } + } + .mat-menu-item{ + height: 36px; + line-height: 36px; + } + .mat-menu-content { + p{ + cursor: default; + mat-icon, span, div { + cursor: default; + } + } + } +} diff --git a/src/app/pages/navigation/top-menu/top-menu.component.spec.ts b/src/app/pages/navigation/top-menu/top-menu.component.spec.ts new file mode 100644 index 00000000..999b9093 --- /dev/null +++ b/src/app/pages/navigation/top-menu/top-menu.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TopMenuComponent } from './top-menu.component'; + +describe('TopMenuComponent', () => { + let component: TopMenuComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ TopMenuComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TopMenuComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/navigation/top-menu/top-menu.component.ts b/src/app/pages/navigation/top-menu/top-menu.component.ts new file mode 100644 index 00000000..c74c01b0 --- /dev/null +++ b/src/app/pages/navigation/top-menu/top-menu.component.ts @@ -0,0 +1,93 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Subject } from 'rxjs/Subject'; +import { takeUntil, filter } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { Actions } from '@ngrx/effects'; + +import { Settings } from '../../../shared/models/RTLconfig'; +import { LoggerService } from '../../../shared/services/logger.service'; +import { GetInfo, GetInfoChain } from '../../../shared/models/lndModels'; +import { environment } from '../../../../environments/environment'; + +import { RTLEffects } from '../../../shared/store/rtl.effects'; +import * as fromRTLReducer from '../../../shared/store/rtl.reducers'; +import * as RTLActions from '../../../shared/store/rtl.actions'; + +@Component({ + selector: 'rtl-top-menu', + templateUrl: './top-menu.component.html', + styleUrls: ['./top-menu.component.scss'] +}) +export class TopMenuComponent implements OnInit, OnDestroy { + public settings: Settings; + public version = ''; + public information: GetInfo = {}; + public informationChain: GetInfoChain = {}; + public flgLoading = true; + public showLogout = false; + private unSubs = [new Subject(), new Subject(), new Subject()]; + + constructor(private logger: LoggerService, private store: Store, private rtlEffects: RTLEffects, private actions$: Actions) { + this.version = environment.VERSION; + } + + ngOnInit() { + this.store.select('rtlRoot') + .pipe(takeUntil(this.unSubs[0])) + .subscribe((rtlStore: fromRTLReducer.State) => { + this.settings = rtlStore.settings; + + this.information = rtlStore.information; + this.flgLoading = (undefined !== this.information.identity_pubkey) ? false : true; + + if (undefined !== this.information.identity_pubkey) { + if (undefined !== this.information.chains && typeof this.information.chains[0] === 'string') { + this.informationChain.chain = this.information.chains[0].toString(); + this.informationChain.network = (this.information.testnet) ? 'Testnet' : 'Mainnet'; + } else if (typeof this.information.chains[0] === 'object' && this.information.chains[0].hasOwnProperty('chain')) { + const getInfoChain = this.information.chains[0]; + this.informationChain.chain = getInfoChain.chain; + this.informationChain.network = getInfoChain.network; + } + } else { + this.informationChain.chain = ''; + this.informationChain.network = ''; + } + this.showLogout = (sessionStorage.getItem('token')) ? true : false; + + this.logger.info(rtlStore); + if (!sessionStorage.getItem('token')) { + this.flgLoading = false; + } + }); + this.actions$ + .pipe( + takeUntil(this.unSubs[2]), + filter((action) => action.type === RTLActions.SIGNOUT) + ).subscribe(() => { + this.showLogout = false; + }); + } + + onClick() { + this.store.dispatch(new RTLActions.OpenConfirmation({ + width: '70%', data: { type: 'CONFIRM', titleMessage: 'Logout from this device?', noBtnText: 'Cancel', yesBtnText: 'Logout' + }})); + this.rtlEffects.closeConfirm + .pipe(takeUntil(this.unSubs[1])) + .subscribe(confirmRes => { + if (confirmRes) { + this.showLogout = false; + this.store.dispatch(new RTLActions.Signout()); + } + }); + } + + ngOnDestroy() { + this.unSubs.forEach(completeSub => { + completeSub.next(); + completeSub.complete(); + }); + } + +} diff --git a/src/app/pages/payments/payments.component.html b/src/app/pages/payments/payments.component.html new file mode 100644 index 00000000..cf89b995 --- /dev/null +++ b/src/app/pages/payments/payments.component.html @@ -0,0 +1,81 @@ +
+
+ + + +

Verify and Send Payments

+
+
+ +
+
+ + + +
+
+ + +
+
+
+
+
+
+ + +
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Creation Date{{payment?.creation_date_str}}Payment Hash +
{{payment?.payment_hash | slice:0:10}}...
+
Fee{{payment?.fee | number}}Value{{payment?.value | number}}Payment Pre Image +
{{payment?.payment_preimage | slice:0:10}}...
+
Value MSat{{payment?.value_msat | number}}Value Sat{{payment?.value_sat | number}}Path{{payment?.path?.length || 0}} Hops
+
+
+
+
+
diff --git a/src/app/pages/payments/payments.component.scss b/src/app/pages/payments/payments.component.scss new file mode 100644 index 00000000..a59d5a78 --- /dev/null +++ b/src/app/pages/payments/payments.component.scss @@ -0,0 +1,43 @@ +.mat-column-path { + padding-left: 10px; +} + +.mat-expansion-panel-header { + padding: 0; +} + +.mat-accordion .mat-expansion-panel { + padding: 0 10px; +} + +.ml-minus-24px { + margin-left: -24px; +} + +.info-column { + flex: 1 1 34%; + box-sizing: border-box; + max-width: 34%; +} + +.info-value { + flex: 1 1 64%; + max-width: 64%; + word-break: break-word; +} + +table { + width:100%; +} + +.table-container { + height: 68vh; + overflow: auto; +} + +@media screen and (max-width: 414px) { + .table-container { + max-height: 46vh; + overflow: auto; + } +} diff --git a/src/app/pages/payments/payments.component.ts b/src/app/pages/payments/payments.component.ts new file mode 100644 index 00000000..e9c24bd2 --- /dev/null +++ b/src/app/pages/payments/payments.component.ts @@ -0,0 +1,192 @@ +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { formatDate } from '@angular/common'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; + +import { MatTableDataSource, MatSort } from '@angular/material'; +import { Settings } from '../../shared/models/RTLconfig'; +import { GetInfo, Payment, PayRequest } from '../../shared/models/lndModels'; +import { LoggerService } from '../../shared/services/logger.service'; + +import { newlyAddedRowAnimation } from '../../shared/animation/row-animation'; +import { RTLEffects } from '../../shared/store/rtl.effects'; +import * as RTLActions from '../../shared/store/rtl.actions'; +import * as fromRTLReducer from '../../shared/store/rtl.reducers'; + +@Component({ + selector: 'rtl-payments', + templateUrl: './payments.component.html', + styleUrls: ['./payments.component.scss'], + animations: [newlyAddedRowAnimation] +}) +export class PaymentsComponent implements OnInit, OnDestroy { + @ViewChild(MatSort) sort: MatSort; + @ViewChild('sendPaymentForm') form; + public newlyAddedPayment = ''; + public flgAnimate = true; + public settings: Settings; + public flgLoading: Array = [true]; + public information: GetInfo = {}; + public payments: any; + public paymentJSONArr: Payment[] = []; + public displayedColumns = []; + public paymentDecoded: PayRequest = {}; + public paymentRequest = ''; + private unsub: Array> = [new Subject(), new Subject(), new Subject(), new Subject()]; + + constructor(private logger: LoggerService, private store: Store, private rtlEffects: RTLEffects) { + switch (true) { + case (window.innerWidth <= 415): + this.displayedColumns = ['creation_date', 'fee', 'value']; + break; + case (window.innerWidth > 415 && window.innerWidth <= 730): + this.displayedColumns = ['creation_date', 'payment_hash', 'fee', 'value', 'payment_preimage']; + break; + case (window.innerWidth > 730 && window.innerWidth <= 1024): + this.displayedColumns = ['creation_date', 'payment_hash', 'fee', 'value', 'payment_preimage', 'path']; + break; + case (window.innerWidth > 1024 && window.innerWidth <= 1280): + this.displayedColumns = ['creation_date', 'payment_hash', 'fee', 'value', 'payment_preimage', 'value_msat', 'value_sat', 'path']; + break; + default: + this.displayedColumns = ['creation_date', 'payment_hash', 'fee', 'value', 'payment_preimage', 'value_msat', 'value_sat', 'path']; + break; + } + } + + ngOnInit() { + this.store.select('rtlRoot') + .pipe(takeUntil(this.unsub[0])) + .subscribe((rtlStore: fromRTLReducer.State) => { + rtlStore.effectErrors.forEach(effectsErr => { + if (effectsErr.action === 'FetchPayments') { + this.flgLoading[0] = 'error'; + } + }); + this.settings = rtlStore.settings; + this.information = rtlStore.information; + this.paymentJSONArr = (rtlStore.payments.length > 0) ? rtlStore.payments : []; + this.payments = (undefined === rtlStore.payments) ? new MatTableDataSource([]) : new MatTableDataSource([...this.paymentJSONArr]); + this.payments.data = this.paymentJSONArr; + this.payments.sort = this.sort; + this.payments.data.forEach(payment => { + payment.creation_date_str = (payment.creation_date_str === '') ? '' : formatDate(payment.creation_date_str, 'MMM/dd/yy HH:mm:ss', 'en-US'); + }); + setTimeout(() => { this.flgAnimate = false; }, 5000); + if (this.flgLoading[0] !== 'error') { + this.flgLoading[0] = (undefined !== this.paymentJSONArr[0]) ? false : true; + } + this.logger.info(rtlStore); + }); + + } + + onSendPayment() { + if (undefined !== this.paymentDecoded.timestamp_str) { + this.sendPayment(); + } else { + this.store.dispatch(new RTLActions.OpenSpinner('Decoding Payment...')); + this.store.dispatch(new RTLActions.DecodePayment(this.paymentRequest)); + this.rtlEffects.setDecodedPayment + .pipe(takeUntil(this.unsub[1])) + .subscribe(decodedPayment => { + this.paymentDecoded = decodedPayment; + if (undefined !== this.paymentDecoded.timestamp_str) { + this.paymentDecoded.timestamp_str = (this.paymentDecoded.timestamp_str === '') ? '' : + formatDate(this.paymentDecoded.timestamp_str, 'MMM/dd/yy HH:mm:ss', 'en-US'); + if (undefined === this.paymentDecoded.num_satoshis) { + this.paymentDecoded.num_satoshis = '0'; + } + this.sendPayment(); + } else { + this.resetData(); + } + }); + } + + } + + sendPayment() { + this.flgAnimate = true; + this.newlyAddedPayment = this.paymentDecoded.payment_hash; + if (undefined === this.paymentDecoded.num_satoshis || this.paymentDecoded.num_satoshis === '' || this.paymentDecoded.num_satoshis === '0') { + const titleMsg = 'This is an empty invoice. Enter the amount (Sats) to pay.'; + this.store.dispatch(new RTLActions.OpenConfirmation({ width: '70%', data: { + type: 'CONFIRM', titleMessage: titleMsg, message: JSON.stringify(this.paymentDecoded), noBtnText: 'Cancel', yesBtnText: 'Send', flgShowInput: true, getInputs: [ + {placeholder: 'Amount (Sats)', inputType: 'number', inputValue: ''} + ] + }})); + this.rtlEffects.closeConfirm + .pipe(takeUntil(this.unsub[2])) + .subscribe(confirmRes => { + if (confirmRes) { + this.paymentDecoded.num_satoshis = confirmRes[0].inputValue; + this.store.dispatch(new RTLActions.OpenSpinner('Sending Payment...')); + this.store.dispatch(new RTLActions.SendPayment([this.paymentRequest, this.paymentDecoded, true])); + this.resetData(); + } + }); + } else { + this.store.dispatch(new RTLActions.OpenConfirmation({ width: '70%', data: { + type: 'CONFIRM', titleMessage: 'Send Payment', noBtnText: 'Cancel', yesBtnText: 'Send', message: JSON.stringify(this.paymentDecoded) + }})); + this.rtlEffects.closeConfirm + .pipe(takeUntil(this.unsub[3])) + .subscribe(confirmRes => { + if (confirmRes) { + this.store.dispatch(new RTLActions.OpenSpinner('Sending Payment...')); + this.store.dispatch(new RTLActions.SendPayment([this.paymentRequest, this.paymentDecoded, false])); + this.resetData(); + } + }); + } + } + + onVerifyPayment() { + this.store.dispatch(new RTLActions.OpenSpinner('Decoding Payment...')); + this.store.dispatch(new RTLActions.DecodePayment(this.paymentRequest)); + this.rtlEffects.setDecodedPayment.subscribe(decodedPayment => { + this.paymentDecoded = decodedPayment; + if (undefined !== this.paymentDecoded.timestamp_str) { + this.paymentDecoded.timestamp_str = (this.paymentDecoded.timestamp_str === '') ? '' : + formatDate(this.paymentDecoded.timestamp_str, 'MMM/dd/yy HH:mm:ss', 'en-US'); + } else { + this.resetData(); + } + }); + } + + resetData() { + this.form.reset(); + } + + onPaymentClick(selRow: Payment, event: any) { + const flgExpansionClicked = event.target.className.includes('mat-expansion-panel-header') || event.target.className.includes('mat-expansion-indicator'); + if (flgExpansionClicked) { + return; + } + const selPayment = this.payments.data.filter(payment => { + return payment.payment_hash === selRow.payment_hash; + })[0]; + const reorderedPayment = JSON.parse(JSON.stringify(selPayment, [ + 'creation_date_str', 'payment_hash', 'fee', 'value_msat', 'value_sat', 'value', 'payment_preimage', 'path' + ] , 2)); + this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: { + type: 'INFO', + message: JSON.stringify(reorderedPayment) + }})); + } + + applyFilter(selFilter: string) { + this.payments.filter = selFilter; + } + + ngOnDestroy() { + this.unsub.forEach(completeSub => { + completeSub.next(); + completeSub.complete(); + }); + } + +} diff --git a/src/app/pages/peers/peers.component.html b/src/app/pages/peers/peers.component.html new file mode 100644 index 00000000..f29dacfd --- /dev/null +++ b/src/app/pages/peers/peers.component.html @@ -0,0 +1,87 @@ +
+
+ + + +

Add Peer

+
+
+ +
+ + + +
+ +
+
+ +
+
+
+
+
+
+ + +
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Detachlink_off Pub Key +
{{peer?.pub_key | slice:0:10}}...
+
Alias {{peer?.alias}} Address {{peer?.address}} Bytes Sent {{peer?.bytes_sent | number}} Bytes Recv {{peer?.bytes_recv | number}} {{information?.smaller_currency_unit}} Sent {{peer?.sat_sent | number}} {{information?.smaller_currency_unit}} Recv {{peer?.sat_recv | number}} Inbound {{peer?.inbound}} Ping {{peer?.ping_time | number}}
+
+
+
+
+
diff --git a/src/app/pages/peers/peers.component.scss b/src/app/pages/peers/peers.component.scss new file mode 100644 index 00000000..4d8df74b --- /dev/null +++ b/src/app/pages/peers/peers.component.scss @@ -0,0 +1,30 @@ +.mat-column-detach { + flex: 0 0 5%; + min-width: 50px; +} + +.mat-column-alias, .mat-column-address { + flex: 0 0 15%; + min-width: 100px; +} + +mat-cell.mat-column-detach { + cursor: pointer; +} + + +table { + width:100%; +} + +.table-container { + height: 68vh; + overflow: auto; +} + +@media screen and (max-width: 414px) { + .table-container { + height: 40vh; + overflow: auto; + } +} diff --git a/src/app/pages/peers/peers.component.spec.ts b/src/app/pages/peers/peers.component.spec.ts new file mode 100644 index 00000000..a5fe2db8 --- /dev/null +++ b/src/app/pages/peers/peers.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PeersComponent } from './peers.component'; + +describe('PeersComponent', () => { + let component: PeersComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ PeersComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PeersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/peers/peers.component.ts b/src/app/pages/peers/peers.component.ts new file mode 100644 index 00000000..7b23b2ac --- /dev/null +++ b/src/app/pages/peers/peers.component.ts @@ -0,0 +1,157 @@ +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil, filter, take } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { Actions } from '@ngrx/effects'; + +import { MatTableDataSource, MatSort } from '@angular/material'; +import { Peer, GetInfo } from '../../shared/models/lndModels'; +import { LoggerService } from '../../shared/services/logger.service'; + +import { newlyAddedRowAnimation } from '../../shared/animation/row-animation'; +import { RTLEffects } from '../../shared/store/rtl.effects'; +import * as RTLActions from '../../shared/store/rtl.actions'; +import * as fromRTLReducer from '../../shared/store/rtl.reducers'; + +@Component({ + selector: 'rtl-peers', + templateUrl: './peers.component.html', + styleUrls: ['./peers.component.scss'], + animations: [newlyAddedRowAnimation] +}) +export class PeersComponent implements OnInit, OnDestroy { + @ViewChild(MatSort) sort: MatSort; + public newlyAddedPeer = ''; + public flgAnimate = true; + public displayedColumns = []; + public peerAddress = ''; + public peers: any; + public information: GetInfo = {}; + public flgLoading: Array = [true]; // 0: peers + private unSubs: Array> = [new Subject(), new Subject(), new Subject(), new Subject()]; + + constructor(private logger: LoggerService, private store: Store, private rtlEffects: RTLEffects, private actions$: Actions) { + switch (true) { + case (window.innerWidth <= 415): + this.displayedColumns = ['detach', 'pub_key', 'alias']; + break; + case (window.innerWidth > 415 && window.innerWidth <= 730): + this.displayedColumns = ['detach', 'pub_key', 'alias', 'address', 'sat_sent', 'sat_recv']; + break; + case (window.innerWidth > 730 && window.innerWidth <= 1024): + this.displayedColumns = ['detach', 'pub_key', 'alias', 'address', 'sat_sent', 'sat_recv', 'inbound']; + break; + case (window.innerWidth > 1024 && window.innerWidth <= 1280): + this.displayedColumns = ['detach', 'pub_key', 'alias', 'address', 'sat_sent', 'sat_recv', 'inbound', 'ping_time']; + break; + default: + this.displayedColumns = ['detach', 'pub_key', 'alias', 'address', 'bytes_sent', 'bytes_recv', 'sat_sent', 'sat_recv', 'inbound', 'ping_time']; + break; + } + } + + ngOnInit() { + this.store.select('rtlRoot') + .pipe(takeUntil(this.unSubs[0])) + .subscribe((rtlStore: fromRTLReducer.State) => { + rtlStore.effectErrors.forEach(effectsErr => { + if (effectsErr.action === 'FetchPeers') { + this.flgLoading[0] = 'error'; + } + }); + this.information = rtlStore.information; + this.peers = new MatTableDataSource([]); + this.peers.data = []; + if (undefined !== rtlStore.peers) { + this.peers = new MatTableDataSource([...rtlStore.peers]); + this.peers.data = rtlStore.peers; + setTimeout(() => { this.flgAnimate = false; }, 5000); + } + this.peers.sort = this.sort; + if (this.flgLoading[0] !== 'error') { + this.flgLoading[0] = false; + } + this.logger.info(rtlStore); + }); + this.actions$ + .pipe( + takeUntil(this.unSubs[1]), + filter((action) => action.type === RTLActions.SET_PEERS) + ).subscribe((setPeers: RTLActions.SetPeers) => { + this.peerAddress = undefined; + }); + } + + onAddPeer(form: any) { + this.flgAnimate = true; + const pattern = '^([a-zA-Z0-9]){1,66}@(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]):[0-9]+$'; + const deviderIndex = this.peerAddress.search('@'); + let pubkey = ''; + let host = ''; + + if (new RegExp(pattern).test(this.peerAddress)) { + pubkey = this.peerAddress.substring(0, deviderIndex); + host = this.peerAddress.substring(deviderIndex + 1); + this.addPeerWithParams(pubkey, host); + } else { + pubkey = (deviderIndex > -1) ? this.peerAddress.substring(0, deviderIndex) : this.peerAddress; + this.store.dispatch(new RTLActions.OpenSpinner('Getting Node Address...')); + this.store.dispatch(new RTLActions.FetchGraphNode(pubkey)); + this.rtlEffects.setGraphNode + .pipe(take(1)) + .subscribe(graphNode => { + host = (undefined === graphNode.node.addresses || undefined === graphNode.node.addresses[0].addr) ? '' : graphNode.node.addresses[0].addr; + this.addPeerWithParams(pubkey, host); + }); + } + } + + addPeerWithParams(pubkey: string, host: string) { + this.newlyAddedPeer = pubkey; + this.store.dispatch(new RTLActions.OpenSpinner('Adding Peer...')); + this.store.dispatch(new RTLActions.SaveNewPeer({pubkey: pubkey, host: host, perm: false})); + } + + onPeerClick(selRow: Peer, event: any) { + const flgCloseClicked = event.target.className.includes('mat-column-detach') || event.target.className.includes('mat-icon'); + if (flgCloseClicked) { + return; + } + const selPeer = this.peers.data.filter(peer => { + return peer.pub_key === selRow.pub_key; + })[0]; + const reorderedPeer = JSON.parse(JSON.stringify(selPeer, [ + 'pub_key', 'alias', 'address', 'bytes_sent', 'bytes_recv', 'sat_sent', 'sat_recv', 'inbound', 'ping_time' + ] , 2)); + this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: { type: 'INFO', message: JSON.stringify(reorderedPeer)}})); + } + + resetData() { + this.peerAddress = ''; + } + + onPeerDetach(peerToDetach: Peer) { + const msg = 'Detach peer: ' + peerToDetach.pub_key; + const msg_type = 'CONFIRM'; + this.store.dispatch(new RTLActions.OpenConfirmation({ width: '70%', data: { type: msg_type, titleMessage: msg, noBtnText: 'Cancel', yesBtnText: 'Detach'}})); + this.rtlEffects.closeConfirm + .pipe(takeUntil(this.unSubs[3])) + .subscribe(confirmRes => { + if (confirmRes) { + this.store.dispatch(new RTLActions.OpenSpinner('Detaching Peer...')); + this.store.dispatch(new RTLActions.DetachPeer({pubkey: peerToDetach.pub_key})); + } + }); + } + + applyFilter(selFilter: string) { + this.peers.filter = selFilter; + } + + ngOnDestroy() { + this.unSubs.forEach(completeSub => { + completeSub.next(); + completeSub.complete(); + }); + } +} diff --git a/src/app/pages/server-config/server-config.component.html b/src/app/pages/server-config/server-config.component.html new file mode 100644 index 00000000..f412064d --- /dev/null +++ b/src/app/pages/server-config/server-config.component.html @@ -0,0 +1,37 @@ +
+
+ + + +

Show Configurations

+
+
+ +
+ + LND + BITCOIND + RTL + +
+ + +
+
+
+ + + +

{{conf}}

+
+ +

{{conf}}

+
+ +
+
+
+
+
+
+
diff --git a/src/app/pages/server-config/server-config.component.scss b/src/app/pages/server-config/server-config.component.scss new file mode 100644 index 00000000..15d29fee --- /dev/null +++ b/src/app/pages/server-config/server-config.component.scss @@ -0,0 +1,3 @@ +h4 { + word-break: break-word; +} diff --git a/src/app/pages/server-config/server-config.component.ts b/src/app/pages/server-config/server-config.component.ts new file mode 100644 index 00000000..ec82a59e --- /dev/null +++ b/src/app/pages/server-config/server-config.component.ts @@ -0,0 +1,72 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; + +import { RTLEffects } from '../../shared/store/rtl.effects'; +import * as RTLActions from '../../shared/store/rtl.actions'; +import * as fromRTLReducer from '../../shared/store/rtl.reducers'; +import { Authentication } from '../../shared/models/RTLconfig'; + +@Component({ + selector: 'rtl-server-config', + templateUrl: './server-config.component.html', + styleUrls: ['./server-config.component.scss'] +}) +export class ServerConfigComponent implements OnInit, OnDestroy { + public selectedNodeType = 'lnd'; + public authSettings: Authentication = {}; + public showLND = false; + public showBitcoind = false; + public configData = ''; + private unsubs: Array> = [new Subject(), new Subject()]; + + constructor(private store: Store, private rtlEffects: RTLEffects) {} + + ngOnInit() { + this.store.select('rtlRoot') + .pipe(takeUntil(this.unsubs[0])) + .subscribe((rtlStore: fromRTLReducer.State) => { + rtlStore.effectErrors.forEach(effectsErr => { + if (effectsErr.action === 'fetchConfig') { + this.resetData(); + } + }); + this.authSettings = rtlStore.authSettings; + if (undefined !== this.authSettings && this.authSettings.lndConfigPath !== '') { + this.showLND = true; + } + if (undefined !== this.authSettings && undefined !== this.authSettings.bitcoindConfigPath && this.authSettings.bitcoindConfigPath !== '') { + this.showBitcoind = true; + } + }); + } + + onSelectionChange(event) { + this.selectedNodeType = event.value; + this.configData = ''; + } + + onShowConfig() { + this.store.dispatch(new RTLActions.OpenSpinner('Opening Config File...')); + this.store.dispatch(new RTLActions.FetchConfig(this.selectedNodeType)); + this.rtlEffects.showLNDConfig + .pipe(takeUntil(this.unsubs[1])) + .subscribe((configFile: any) => { + this.configData = (configFile === '' || undefined === configFile) ? [] : configFile.split('\n'); + }); + } + + resetData() { + this.configData = ''; + this.selectedNodeType = 'lnd'; + } + + ngOnDestroy() { + this.unsubs.forEach(completeSub => { + completeSub.next(); + completeSub.complete(); + }); + } + +} diff --git a/src/app/pages/signin/signin.component.html b/src/app/pages/signin/signin.component.html new file mode 100644 index 00000000..6a141a34 --- /dev/null +++ b/src/app/pages/signin/signin.component.html @@ -0,0 +1,22 @@ +
+
+ + + +

Login to RTL

+
+
+ +
+ + + {{hintStr}} + + + +
+
+
+
+
+ \ No newline at end of file diff --git a/src/app/pages/signin/signin.component.scss b/src/app/pages/signin/signin.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/pages/signin/signin.component.ts b/src/app/pages/signin/signin.component.ts new file mode 100644 index 00000000..0a166d32 --- /dev/null +++ b/src/app/pages/signin/signin.component.ts @@ -0,0 +1,59 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; + +import { LoggerService } from '../../shared/services/logger.service'; +import * as fromRTLReducer from '../../shared/store/rtl.reducers'; +import * as RTLActions from '../../shared/store/rtl.actions'; + +@Component({ + selector: 'rtl-signin', + templateUrl: './signin.component.html', + styleUrls: ['./signin.component.scss'] +}) +export class SigninComponent implements OnInit, OnDestroy { + password = ''; + nodeAuthType = ''; + rtlSSO = 0; + rtlCookiePath = ''; + hintStr = ''; + accessKey = ''; + + private unsub: Array> = [new Subject(), new Subject(), new Subject()]; + + constructor(private logger: LoggerService, private store: Store) { } + + ngOnInit() { + this.store.select('rtlRoot') + .pipe(takeUntil(this.unsub[0])) + .subscribe((rtlStore: fromRTLReducer.State) => { + rtlStore.effectErrors.forEach(effectsErr => { + this.logger.error(effectsErr); + }); + this.nodeAuthType = rtlStore.authSettings.nodeAuthType; + this.logger.info(rtlStore); + if (this.nodeAuthType.toUpperCase() === 'DEFAULT') { + this.hintStr = 'Enter RPC password'; + } else { + this.hintStr = ''; // Do not remove, initial passowrd 'DEFAULT' is initilizing its value + } + }); + } + + onSignin() { + this.store.dispatch(new RTLActions.Signin(window.btoa(this.password))); + } + + resetData() { + this.password = ''; + } + + ngOnDestroy() { + this.unsub.forEach(completeSub => { + completeSub.next(); + completeSub.complete(); + }); + } + +} diff --git a/src/app/pages/switch/forwarding-history.component.html b/src/app/pages/switch/forwarding-history.component.html new file mode 100644 index 00000000..52b8e8ae --- /dev/null +++ b/src/app/pages/switch/forwarding-history.component.html @@ -0,0 +1,87 @@ +
+
+ + + +

Forwarding History

+ +
+
+ +
+
+ + + + + + + + + + +
+
+ + +
+
+
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Timestamp{{fhEvent.timestamp_str}}Chan Id In{{fhEvent.chan_id_in}}Alias In{{fhEvent.alias_in}}Chan Id Out{{fhEvent.chan_id_out}}Alias Out{{fhEvent.alias_out}}Amount + Out (Sats){{fhEvent.amt_out | number}}Amount + In (Sats){{fhEvent.amt_in | number}}Fee + (Sats){{fhEvent.fee | number}}
+
+
+
+
+
\ No newline at end of file diff --git a/src/app/pages/switch/forwarding-history.component.scss b/src/app/pages/switch/forwarding-history.component.scss new file mode 100644 index 00000000..bf2b3bd5 --- /dev/null +++ b/src/app/pages/switch/forwarding-history.component.scss @@ -0,0 +1,21 @@ +.mat-column-amt_in { + flex: 0 0 15%; + min-width: 120px; + padding-right: 20px; +} + +table { + width:100%; +} + +.table-container { + height: 72vh; + overflow: auto; +} + +@media screen and (max-width: 414px) { + .table-container { + height: 53vh; + overflow: auto; + } +} diff --git a/src/app/pages/switch/forwarding-history.component.spec.ts b/src/app/pages/switch/forwarding-history.component.spec.ts new file mode 100644 index 00000000..f5c03c97 --- /dev/null +++ b/src/app/pages/switch/forwarding-history.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ForwardingHistoryComponent } from './forwarding-history.component'; + +describe('ForwardingHistoryComponent', () => { + let component: ForwardingHistoryComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ForwardingHistoryComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ForwardingHistoryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/switch/forwarding-history.component.ts b/src/app/pages/switch/forwarding-history.component.ts new file mode 100644 index 00000000..ff0678dc --- /dev/null +++ b/src/app/pages/switch/forwarding-history.component.ts @@ -0,0 +1,123 @@ +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { formatDate } from '@angular/common'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; + +import { MatTableDataSource, MatSort } from '@angular/material'; +import { ForwardingEvent } from '../../shared/models/lndModels'; +import { LoggerService } from '../../shared/services/logger.service'; + +import * as RTLActions from '../../shared/store/rtl.actions'; +import * as fromRTLReducer from '../../shared/store/rtl.reducers'; + +@Component({ + selector: 'rtl-forwarding-history', + templateUrl: './forwarding-history.component.html', + styleUrls: ['./forwarding-history.component.scss'] +}) +export class ForwardingHistoryComponent implements OnInit, OnDestroy { + @ViewChild(MatSort) sort: MatSort; + public displayedColumns = []; + public forwardingHistoryEvents: any; + public lastOffsetIndex = 0; + public flgLoading: Array = [true]; + public today = new Date(Date.now()); + public yesterday = new Date(this.today.getFullYear(), this.today.getMonth(), this.today.getDate() - 1, this.today.getHours(), this.today.getMinutes(), this.today.getSeconds()); + public endDate = this.today; + public startDate = this.yesterday; + private unsub: Array> = [new Subject(), new Subject()]; + + constructor(private logger: LoggerService, private store: Store) { + switch (true) { + case (window.innerWidth <= 415): + this.displayedColumns = ['timestamp', 'amt_out', 'amt_in']; + break; + case (window.innerWidth > 415 && window.innerWidth <= 730): + this.displayedColumns = ['timestamp', 'amt_out', 'amt_in', 'fee']; + break; + case (window.innerWidth > 730 && window.innerWidth <= 1024): + this.displayedColumns = ['timestamp', 'chan_id_in', 'chan_id_out', 'amt_out', 'amt_in', 'fee']; + break; + case (window.innerWidth > 1024 && window.innerWidth <= 1280): + this.displayedColumns = ['timestamp', 'chan_id_in', 'chan_id_out', 'amt_out', 'amt_in', 'fee']; + break; + default: + this.displayedColumns = ['timestamp', 'chan_id_in', 'chan_id_out', 'amt_out', 'amt_in', 'fee']; + break; + } + } + + ngOnInit() { + this.store.dispatch(new RTLActions.GetForwardingHistory({})); + this.store.select('rtlRoot') + .pipe(takeUntil(this.unsub[0])) + .subscribe((rtlStore: fromRTLReducer.State) => { + rtlStore.effectErrors.forEach(effectsErr => { + if (effectsErr.action === 'GetForwardingHistory') { + this.flgLoading[0] = 'error'; + } + }); + if (undefined !== rtlStore.forwardingHistory && undefined !== rtlStore.forwardingHistory.forwarding_events) { + this.lastOffsetIndex = rtlStore.forwardingHistory.last_offset_index; + this.loadForwardingEventsTable(rtlStore.forwardingHistory.forwarding_events); + } + if (this.flgLoading[0] !== 'error') { + this.flgLoading[0] = (undefined !== rtlStore.forwardingHistory) ? false : true; + } + this.logger.info(rtlStore); + }); + + } + + onForwardingEventClick(selRow: ForwardingEvent, event: any) { + const selFEvent = this.forwardingHistoryEvents.data.filter(fhEvent => { + return fhEvent.chan_id_in === selRow.chan_id_in; + })[0]; + const reorderedFHEvent = JSON.parse(JSON.stringify(selFEvent, ['timestamp_str', 'chan_id_in', 'alias_in', 'chan_id_out', 'alias_out', 'amt_out', 'amt_in', 'fee'] , 2)); + this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: { + type: 'INFO', + message: JSON.stringify(reorderedFHEvent) + }})); + } + + loadForwardingEventsTable(forwardingEvents: ForwardingEvent[]) { + this.forwardingHistoryEvents = new MatTableDataSource([...forwardingEvents]); + this.forwardingHistoryEvents.sort = this.sort; + this.forwardingHistoryEvents.data.forEach(event => { + event.timestamp_str = (event.timestamp_str === '') ? '' : formatDate(event.timestamp_str, 'MMM/dd/yy HH:mm:ss', 'en-US'); + }); + + this.logger.info(this.forwardingHistoryEvents); + } + + onForwardingHistoryFetch() { + if (undefined === this.endDate || this.endDate == null) { + this.endDate = new Date(); + } + if (undefined === this.startDate || this.startDate == null) { + this.startDate = new Date(this.endDate.getFullYear(), this.endDate.getMonth(), this.endDate.getDate() - 1); + } + this.store.dispatch(new RTLActions.GetForwardingHistory({ + end_time: Math.round(this.endDate.getTime() / 1000).toString(), + start_time: Math.round(this.startDate.getTime() / 1000).toString() + })); + } + + resetData() { + this.endDate = new Date(); + this.startDate = new Date(this.endDate.getFullYear(), this.endDate.getMonth(), this.endDate.getDate() - 1); + if (undefined !== this.forwardingHistoryEvents) { + this.forwardingHistoryEvents.data = []; + } + } + + ngOnDestroy() { + this.resetData(); + this.unsub.forEach(completeSub => { + completeSub.next(); + completeSub.complete(); + }); + } + +} diff --git a/src/app/pages/transactions/list-transactions/list-transactions.component.html b/src/app/pages/transactions/list-transactions/list-transactions.component.html new file mode 100644 index 00000000..89d225d5 --- /dev/null +++ b/src/app/pages/transactions/list-transactions/list-transactions.component.html @@ -0,0 +1,57 @@ +
+
+ + + +

Transactions

+
+
+ +
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Destination Addresses{{trans?.dest_addresses?.length || 0}} Addr Timestamp {{trans.time_stamp_str}} Num Confirmations {{trans.num_confirmations | number}} Total Fees {{trans.total_fees | number}} Block Hash {{trans.block_hash | slice:0:10}}... Block Height {{trans.block_height | number}} Txn Hash
{{trans.tx_hash | slice:0:10}}...
Amount {{trans.amount | number}}
+
+
+
+
+
diff --git a/src/app/pages/transactions/list-transactions/list-transactions.component.scss b/src/app/pages/transactions/list-transactions/list-transactions.component.scss new file mode 100644 index 00000000..f02ab006 --- /dev/null +++ b/src/app/pages/transactions/list-transactions/list-transactions.component.scss @@ -0,0 +1,31 @@ +.mat-column-tx_hash, .mat-column-block_hash { + padding-left: 10px; +} + +.mat-expansion-panel-header { + padding: 0; +} + +.mat-accordion .mat-expansion-panel { + padding: 0 10px; +} + +.ml-minus-24px { + margin-left: -24px; +} + +table { + width:100%; +} + +.table-container { + height: 78vh; + overflow: auto; +} + +@media screen and (max-width: 414px) { + .table-container { + height: 68vh; + overflow: auto; + } +} diff --git a/src/app/pages/transactions/list-transactions/list-transactions.component.spec.ts b/src/app/pages/transactions/list-transactions/list-transactions.component.spec.ts new file mode 100644 index 00000000..9222f131 --- /dev/null +++ b/src/app/pages/transactions/list-transactions/list-transactions.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ListTransactionsComponent } from './list-transactions.component'; + +describe('ListTransactionsComponent', () => { + let component: ListTransactionsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ListTransactionsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ListTransactionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/transactions/list-transactions/list-transactions.component.ts b/src/app/pages/transactions/list-transactions/list-transactions.component.ts new file mode 100644 index 00000000..75ca8ea2 --- /dev/null +++ b/src/app/pages/transactions/list-transactions/list-transactions.component.ts @@ -0,0 +1,110 @@ +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { formatDate } from '@angular/common'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; + +import { MatTableDataSource, MatSort } from '@angular/material'; +import { Transaction } from '../../../shared/models/lndModels'; +import { LoggerService } from '../../../shared/services/logger.service'; + +import { RTLEffects } from '../../../shared/store/rtl.effects'; +import * as RTLActions from '../../../shared/store/rtl.actions'; +import * as fromRTLReducer from '../../../shared/store/rtl.reducers'; + +@Component({ + selector: 'rtl-list-transactions', + templateUrl: './list-transactions.component.html', + styleUrls: ['./list-transactions.component.scss'] +}) +export class ListTransactionsComponent implements OnInit, OnDestroy { + @ViewChild(MatSort) sort: MatSort; + public displayedColumns = []; + public listTransactions: any; + public flgLoading: Array = [true]; + private unsub: Array> = [new Subject(), new Subject()]; + + constructor(private logger: LoggerService, private store: Store, private rtlEffects: RTLEffects) { + switch (true) { + case (window.innerWidth <= 415): + this.displayedColumns = ['dest_addresses', 'total_fees', 'amount']; + break; + case (window.innerWidth > 415 && window.innerWidth <= 730): + this.displayedColumns = ['dest_addresses', 'time_stamp', 'total_fees', 'amount']; + break; + case (window.innerWidth > 730 && window.innerWidth <= 1024): + this.displayedColumns = ['dest_addresses', 'time_stamp', 'num_confirmations', 'total_fees', 'tx_hash', 'amount']; + break; + case (window.innerWidth > 1024 && window.innerWidth <= 1280): + this.displayedColumns = ['dest_addresses', 'time_stamp', 'num_confirmations', 'total_fees', 'tx_hash', 'amount']; + break; + default: + this.displayedColumns = ['dest_addresses', 'time_stamp', 'num_confirmations', 'total_fees', 'block_hash', 'block_height', 'tx_hash', 'amount']; + break; + } + } + + ngOnInit() { + this.store.dispatch(new RTLActions.FetchTransactions()); + this.store.select('rtlRoot') + .pipe(takeUntil(this.unsub[0])) + .subscribe((rtlStore: fromRTLReducer.State) => { + rtlStore.effectErrors.forEach(effectsErr => { + if (effectsErr.action === 'FetchTransactions') { + this.flgLoading[0] = 'error'; + } + }); + if (undefined !== rtlStore.transactions && rtlStore.transactions.length > 0) { + this.loadTransactionsTable(rtlStore.transactions); + } + if (this.flgLoading[0] !== 'error') { + this.flgLoading[0] = (undefined !== rtlStore.transactions) ? false : true; + } + this.logger.info(rtlStore); + }); + + } + + applyFilter(selFilter: string) { + this.listTransactions.filter = selFilter; + } + + onTransactionClick(selRow: Transaction, event: any) { + const flgExpansionClicked = event.target.className.includes('mat-expansion-panel-header') || event.target.className.includes('mat-expansion-indicator'); + if (flgExpansionClicked) { + return; + } + const selTransaction = this.listTransactions.data.filter(transaction => { + return transaction.tx_hash === selRow.tx_hash; + })[0]; + const reorderedTransactions = JSON.parse(JSON.stringify(selTransaction, [ + 'dest_addresses', 'time_stamp_str', 'num_confirmations', 'total_fees', 'block_hash', 'block_height', 'tx_hash', 'amount' + ] , 2)); + this.store.dispatch(new RTLActions.OpenAlert({ width: '75%', data: { + type: 'INFO', + message: JSON.stringify(reorderedTransactions) + }})); + } + + loadTransactionsTable(transactions) { + this.listTransactions = new MatTableDataSource([...transactions]); + this.listTransactions.sort = this.sort; + this.listTransactions.data.forEach(transaction => { + if (undefined !== transaction.time_stamp_str) { + transaction.time_stamp_str = (transaction.time_stamp_str === '') ? '' : formatDate(transaction.time_stamp_str, 'MMM/dd/yy HH:mm:ss', 'en-US'); + } + }); + this.logger.info(this.listTransactions); + } + + resetData() { + } + + ngOnDestroy() { + this.unsub.forEach(completeSub => { + completeSub.next(); + completeSub.complete(); + }); + } + +} diff --git a/src/app/pages/transactions/send-receive/send-receive-trans.component.html b/src/app/pages/transactions/send-receive/send-receive-trans.component.html new file mode 100644 index 00000000..33d753bf --- /dev/null +++ b/src/app/pages/transactions/send-receive/send-receive-trans.component.html @@ -0,0 +1,163 @@ +
+
Don't be #reckless. #craefulgang #craefulgang #craefulgang.
+
+
+
+ + + +
Total Balance
+ +
+
+ + + + + + + +

{{blockchainBalance?.btc_total_balance | number}} {{information?.currency_unit}}

+

{{blockchainBalance?.total_balance | number}} {{information?.smaller_currency_unit}}

+
+
+ + +
+
+
+ + + +
Confirmed Balance
+ +
+
+ + + + + + + +

{{blockchainBalance?.btc_confirmed_balance | number}} {{information?.currency_unit}}

+

{{blockchainBalance?.confirmed_balance | number}} {{information?.smaller_currency_unit}}

+
+
+ + +
+
+
+ + + +
Unconfirmed Balance
+ +
+
+ + + + + + + +

{{blockchainBalance?.btc_unconfirmed_balance | number}} {{information?.currency_unit}}

+

{{blockchainBalance?.unconfirmed_balance | number}} {{information?.smaller_currency_unit}}

+
+
+ + +
+
+
+ + + +

Receive Funds

+
+
+ +
+
+ + + + {{addressType.addressTp}} + + + +
+
+ +
+
+ +
+
+ + + +
+
+ +
+
+
+
+
+
+ + + +

Send Funds

+
+
+ +
+
+ + + +
+
+ + Sweep All + + + + +
+
+
+
+ + + + {{transType.name}} + + + +
+
+ + + + + + +
+
+ + +
+
+
+
+
+
+

Sats

diff --git a/src/app/pages/transactions/send-receive/send-receive-trans.component.scss b/src/app/pages/transactions/send-receive/send-receive-trans.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/pages/transactions/send-receive/send-receive-trans.component.ts b/src/app/pages/transactions/send-receive/send-receive-trans.component.ts new file mode 100644 index 00000000..d24db3ff --- /dev/null +++ b/src/app/pages/transactions/send-receive/send-receive-trans.component.ts @@ -0,0 +1,175 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil, take } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; + +import { Settings } from '../../../shared/models/RTLconfig'; +import { GetInfo, Balance, ChannelsTransaction, AddressType } from '../../../shared/models/lndModels'; +import { Authentication } from '../../../shared/models/RTLconfig'; +import { LoggerService } from '../../../shared/services/logger.service'; + +import { RTLEffects } from '../../../shared/store/rtl.effects'; +import * as RTLActions from '../../../shared/store/rtl.actions'; +import * as fromRTLReducer from '../../../shared/store/rtl.reducers'; + +@Component({ + selector: 'rtl-send-receive-trans', + templateUrl: './send-receive-trans.component.html', + styleUrls: ['./send-receive-trans.component.scss'] +}) +export class SendReceiveTransComponent implements OnInit, OnDestroy { + public settings: Settings; + public addressTypes = []; + public flgLoadingWallet: Boolean | 'error' = true; + public selectedAddress: AddressType = {}; + public blockchainBalance: Balance = {}; + public information: GetInfo = {}; + public authSettings: Authentication = {}; + public newAddress = ''; + public transaction: ChannelsTransaction = {}; + public transTypes = [{id: '1', name: 'Target Confirmation Blocks'}, {id: '2', name: 'Fee'}]; + public selTransType = '1'; + public flgCustomAmount = '1'; + private unsub: Array> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject()]; + + constructor(private logger: LoggerService, private store: Store, private rtlEffects: RTLEffects) {} + + ngOnInit() { + this.store.select('rtlRoot') + .pipe(takeUntil(this.unsub[0])) + .subscribe((rtlStore: fromRTLReducer.State) => { + rtlStore.effectErrors.forEach(effectsErr => { + if (effectsErr.action === 'FetchBalance/blockchain') { + this.flgLoadingWallet = 'error'; + } + }); + this.settings = rtlStore.settings; + this.information = rtlStore.information; + this.addressTypes = rtlStore.addressTypes; + this.authSettings = rtlStore.authSettings; + + this.blockchainBalance = rtlStore.blockchainBalance; + if (undefined === this.blockchainBalance.total_balance) { + this.blockchainBalance.total_balance = '0'; + } + if (undefined === this.blockchainBalance.confirmed_balance) { + this.blockchainBalance.confirmed_balance = '0'; + } + if (undefined === this.blockchainBalance.unconfirmed_balance) { + this.blockchainBalance.unconfirmed_balance = '0'; + } + if (this.flgLoadingWallet !== 'error') { + this.flgLoadingWallet = false; + } + + this.logger.info(rtlStore); + }); + + } + + onGenerateAddress() { + this.store.dispatch(new RTLActions.OpenSpinner('Getting New Address...')); + this.store.dispatch(new RTLActions.GetNewAddress(this.selectedAddress)); + this.rtlEffects.setNewAddress + .pipe(takeUntil(this.unsub[1])) + .subscribe(newAddress => { + this.newAddress = newAddress; + }); + } + + onSendFunds() { + const confirmationMsg = { + 'BTC Address': this.transaction.address, + }; + if (!+this.flgCustomAmount) { + confirmationMsg['Sweep All'] = 'True'; + this.transaction.sendAll = true; + } else { + confirmationMsg['Amount (' + this.information.smaller_currency_unit + ')'] = this.transaction.amount; + this.transaction.sendAll = false; + } + if (this.selTransType === '1') { + delete this.transaction.fees; + confirmationMsg['Target Confirmation Blocks'] = this.transaction.blocks; + } else { + delete this.transaction.blocks; + confirmationMsg['Fee (' + this.information.smaller_currency_unit + '/Byte)'] = this.transaction.fees; + } + this.store.dispatch(new RTLActions.OpenConfirmation({ width: '70%', data: + {type: 'CONFIRM', message: JSON.stringify(confirmationMsg), noBtnText: 'Cancel', yesBtnText: 'Send'} + })); + + this.rtlEffects.closeConfirm + .pipe(takeUntil(this.unsub[2])) + .subscribe(confirmRes => { + if (confirmRes) { + if (this.transaction.sendAll && !+this.authSettings.rtlSSO) { + this.store.dispatch(new RTLActions.OpenConfirmation({ width: '70%', data: + {type: 'CONFIRM', titleMessage: 'Enter Login Password', noBtnText: 'Cancel', yesBtnText: 'Authorize', flgShowInput: true, getInputs: [ + {placeholder: 'Enter Login Password', inputType: 'password', inputValue: ''} + ]} + })); + this.rtlEffects.closeConfirm + .pipe(takeUntil(this.unsub[3])) + .subscribe(pwdConfirmRes => { + if (pwdConfirmRes) { + const pwd = pwdConfirmRes[0].inputValue; + this.store.dispatch(new RTLActions.IsAuthorized(window.btoa(pwd))); + this.rtlEffects.isAuthorizedRes + .pipe(take(1)) + .subscribe(authRes => { + if (authRes !== 'ERROR') { + this.dispatchToSendFunds(); + } + }); + } + }); + } else { + this.dispatchToSendFunds(); + } + } + }); + } + + dispatchToSendFunds() { + this.store.dispatch(new RTLActions.OpenSpinner('Sending Funds...')); + this.store.dispatch(new RTLActions.SetChannelTransaction(this.transaction)); + this.transaction = {address: '', amount: 0, blocks: 0, fees: 0}; + } + + get invalidValues(): boolean { + return (undefined === this.transaction.address || this.transaction.address === '') + || (+this.flgCustomAmount && (undefined === this.transaction.amount || this.transaction.amount <= 0)) + || (this.selTransType === '1' && (undefined === this.transaction.blocks || this.transaction.blocks <= 0)) + || (this.selTransType === '2' && (undefined === this.transaction.fees || this.transaction.fees <= 0)); + } + + onCustomClicked() { + this.flgCustomAmount = '1'; + } + + onOptionChange(event) { + if (!+this.flgCustomAmount) { + delete this.transaction.amount; + } + } + + resetData() { + this.transaction.address = ''; + this.transaction.amount = 0; + this.transaction.blocks = 0; + this.transaction.fees = 0; + } + + resetReceiveData() { + this.selectedAddress = {}; + this.newAddress = ''; + } + + ngOnDestroy() { + this.unsub.forEach(completeSub => { + completeSub.next(); + completeSub.complete(); + }); + } +} diff --git a/src/app/pages/unlock-lnd/unlock-lnd.component.html b/src/app/pages/unlock-lnd/unlock-lnd.component.html new file mode 100644 index 00000000..e91c9aa0 --- /dev/null +++ b/src/app/pages/unlock-lnd/unlock-lnd.component.html @@ -0,0 +1,21 @@ +
+
+ + + +

Unlock LND Wallet

+
+
+ +
+ + + Enter LND Wallet Password + + + +
+
+
+
+
diff --git a/src/app/pages/unlock-lnd/unlock-lnd.component.scss b/src/app/pages/unlock-lnd/unlock-lnd.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/pages/unlock-lnd/unlock-lnd.component.ts b/src/app/pages/unlock-lnd/unlock-lnd.component.ts new file mode 100644 index 00000000..62536af4 --- /dev/null +++ b/src/app/pages/unlock-lnd/unlock-lnd.component.ts @@ -0,0 +1,41 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Router } from '@angular/router'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; + +import { LoggerService } from '../../shared/services/logger.service'; + +import * as RTLActions from '../../shared/store/rtl.actions'; +import * as fromRTLReducer from '../../shared/store/rtl.reducers'; + +@Component({ + selector: 'rtl-unlock-lnd', + templateUrl: './unlock-lnd.component.html', + styleUrls: ['./unlock-lnd.component.scss'] +}) +export class UnlockLNDComponent implements OnInit, OnDestroy { + walletPassword = ''; + private unsub = new Subject(); + + constructor(private store: Store) {} + + ngOnInit() { + this.walletPassword = ''; + } + + onOperateWallet(operation: string) { + this.store.dispatch(new RTLActions.OpenSpinner('Unlocking...')); + this.store.dispatch(new RTLActions.OperateWallet({operation: operation, pwd: this.walletPassword})); + } + + resetData() { + this.walletPassword = ''; + } + + ngOnDestroy() { + this.unsub.next(); + this.unsub.complete(); + } + +} diff --git a/src/app/shared/animation/row-animation.ts b/src/app/shared/animation/row-animation.ts new file mode 100644 index 00000000..6dba26f1 --- /dev/null +++ b/src/app/shared/animation/row-animation.ts @@ -0,0 +1,10 @@ +import { trigger, state, animate, transition, style } from '@angular/animations'; + +export const newlyAddedRowAnimation = [ + trigger('newlyAddedRowAnimation', [ + state('notAdded, void', style({ transform: 'translateX(0)' })), + state('added', style({ transform: 'translateX(1.5)', border: '1px solid' })), + transition('added <=> notAdded', animate('2000ms ease-out')), + transition('added <=> void', animate('2000ms ease-out')) + ]) +]; diff --git a/src/app/shared/components/alert-message/alert-message.component.html b/src/app/shared/components/alert-message/alert-message.component.html new file mode 100644 index 00000000..57480a6b --- /dev/null +++ b/src/app/shared/components/alert-message/alert-message.component.html @@ -0,0 +1,35 @@ +
+
+ +

{{data.type}}

+ close +
+ +
+

{{data.titleMessage | titlecase}}

+
+
+
{{obj[0] | titlecase}}
+
:
+
+
+ {{obj[1] | number:'1.0-3'}} +
+ + {{obj[1]}} + + file_copyCopied +
+
+ +
+
+
+
+ +
+ +
+
+
+
diff --git a/src/app/shared/components/alert-message/alert-message.component.scss b/src/app/shared/components/alert-message/alert-message.component.scss new file mode 100644 index 00000000..b35af4b1 --- /dev/null +++ b/src/app/shared/components/alert-message/alert-message.component.scss @@ -0,0 +1,40 @@ +.p-2 { + padding: 1rem; +} + +.pb-1 { + padding-bottom: 0.3rem; +} + +.pb-2 { + padding-bottom: 1rem; +} + +.mb-1 { + margin-bottom: 0.5rem; +} + +.wrap-text { + -ms-word-break: break-all; + word-break: break-all; + word-break: break-word; + -webkit-hyphens: auto; + -moz-hyphens: auto; + hyphens: auto; +} + +.mat-icon[type="button"] { + cursor: pointer; +} + +.new-line { + white-space: pre-wrap; +} + +.title-message { + font-size: 110%; +} + +.mt-minus-40px { + margin-top:-40px; +} diff --git a/src/app/shared/components/alert-message/alert-message.component.ts b/src/app/shared/components/alert-message/alert-message.component.ts new file mode 100644 index 00000000..7217cd03 --- /dev/null +++ b/src/app/shared/components/alert-message/alert-message.component.ts @@ -0,0 +1,106 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; + +import { LoggerService } from '../../../shared/services/logger.service'; +import { AlertData } from '../../../shared/models/alertData'; + +@Component({ + selector: 'rtl-alert-message', + templateUrl: './alert-message.component.html', + styleUrls: ['./alert-message.component.scss'] +}) +export class AlertMessageComponent implements OnInit { + public msgTypeBackground = 'bg-primary p-1'; + public msgTypeForeground = 'primary'; + public messageObj = []; + public flgCopied = false; + + constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: AlertData, private logger: LoggerService) { } + + ngOnInit() { + this.setStyleOnAlertType(); + this.convertJSONData(); + } + + setStyleOnAlertType() { + // INFO/WARN/ERROR/SUCCESS/CONFIRM + if (this.data.type === 'WARN') { + this.msgTypeBackground = 'bg-accent p-1'; + this.msgTypeForeground = 'accent'; + } + if (this.data.type === 'ERROR') { + this.msgTypeBackground = 'bg-warn p-1'; + this.msgTypeForeground = 'warn'; + if (undefined === this.data.message && undefined === this.data.titleMessage && this.messageObj.length <= 0 ) { + this.data.titleMessage = 'Please Check Server Connection'; + } + } + } + + convertJSONData() { + this.data.message = (undefined === this.data.message) ? '' : this.data.message.replace(/{/g, '').replace(/"/g, '').replace(/}/g, '').replace(/\n/g, ''); + // Start: For Payment Path + const arrayStartIdx = this.data.message.search('\\['); + const arrayEndIdx = this.data.message.search('\\]'); + if (arrayStartIdx > -1 && arrayEndIdx > -1) { + this.data.message = this.data.message.substring(0, arrayStartIdx).concat( + this.data.message.substring(arrayStartIdx + 1, arrayEndIdx).replace(/,/g, '\n'), + this.data.message.substring(arrayEndIdx + 1)); + } + // End: For Payment Path + this.messageObj = (this.data.message === '') ? [] : this.data.message.split(','); + this.messageObj.forEach((obj, idx) => { + this.messageObj[idx] = obj.split(':'); + this.messageObj[idx][0] = this.messageObj[idx][0].replace('_str', ''); + this.messageObj[idx][0] = this.messageObj[idx][0].replace(/_/g, ' '); // To replace Backend Data's '_' + // Start: To Merge Time Value Again with ':', example Payment Creation Time + if (this.messageObj[idx].length > 2) { + this.messageObj[idx].forEach((dataValue, j) => { + if (j === 0 || j === 1) { + return; + } else { + this.messageObj[idx][1] = this.messageObj[idx][1] + ':' + this.messageObj[idx][j]; + } + }); + } + // End: To Merge Time Value Again with ':', example Payment Creation Time + }); + } + + showCopyOption(key): boolean { + let flgFoundKey = false; + const showCopyOnKeys = ['payment request']; + showCopyOnKeys.filter((arrKey) => { + if (arrKey === key) { + flgFoundKey = true; + return true; + } + }); + return flgFoundKey; + } + + copiedText(payload) { + this.flgCopied = true; + setTimeout(() => {this.flgCopied = false; }, 5000); + this.logger.info('Copied Text: ' + payload); + } + + isNumber(value, key): boolean { + let flgFoundKey = false; + const notNumberKeys = ['chan id', 'creation date', 'chan id out', 'chan id in']; + notNumberKeys.filter((arrKey) => { + if (arrKey === key) { + flgFoundKey = true; + } + }); + if (!flgFoundKey) { + return new RegExp(/^[0-9]+$/).test(value); + } else { + return false; + } + } + + onClose() { + this.dialogRef.close(false); + } +} diff --git a/src/app/shared/components/confirmation-message/confirmation-message.component.html b/src/app/shared/components/confirmation-message/confirmation-message.component.html new file mode 100644 index 00000000..f1a1b94b --- /dev/null +++ b/src/app/shared/components/confirmation-message/confirmation-message.component.html @@ -0,0 +1,39 @@ +
+
+ +

{{data.type}}

+ close +
+ +
+

{{data.titleMessage | titlecase}}

+
+ + + +
+
+
+
{{obj[0] | titlecase}}
+
:
+
+
+ {{obj[1] | number:'1.0-3'}} +
+ + {{obj[1]}} + + file_copyCopied +
+
+
+
+ +
+ + + +
+
+
+
diff --git a/src/app/shared/components/confirmation-message/confirmation-message.component.scss b/src/app/shared/components/confirmation-message/confirmation-message.component.scss new file mode 100644 index 00000000..79c78813 --- /dev/null +++ b/src/app/shared/components/confirmation-message/confirmation-message.component.scss @@ -0,0 +1,16 @@ +.new-line { + white-space: pre-wrap; +} + +.wrap-text { + -ms-word-break: break-all; + word-break: break-all; + word-break: break-word; + -webkit-hyphens: auto; + -moz-hyphens: auto; + hyphens: auto; +} + +.title-message { + font-size: 110%; +} diff --git a/src/app/shared/components/confirmation-message/confirmation-message.component.ts b/src/app/shared/components/confirmation-message/confirmation-message.component.ts new file mode 100644 index 00000000..f6a3e5f1 --- /dev/null +++ b/src/app/shared/components/confirmation-message/confirmation-message.component.ts @@ -0,0 +1,107 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; +import { Store } from '@ngrx/store'; + +import { LoggerService } from '../../../shared/services/logger.service'; +import { AlertData, InputData } from '../../../shared/models/alertData'; + +import * as RTLActions from '../../../shared/store/rtl.actions'; +import * as fromRTLReducer from '../../../shared/store/rtl.reducers'; + +@Component({ + selector: 'rtl-confirmation-message', + templateUrl: './confirmation-message.component.html', + styleUrls: ['./confirmation-message.component.scss'] +}) +export class ConfirmationMessageComponent implements OnInit { + public msgTypeBackground = 'bg-primary p-1'; + public msgTypeForeground = 'primary'; + public noBtnText = 'No'; + public yesBtnText = 'Yes'; + public messageObj = []; + public flgCopied = false; + public flgShowInput = false; + public getInputs: Array = [{placeholder: '', inputType: 'text', inputValue: ''}]; + + constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: AlertData, private logger: LoggerService, + private store: Store) { } + + ngOnInit() { + this.flgShowInput = this.data.flgShowInput; + this.getInputs = this.data.getInputs; + this.noBtnText = (undefined !== this.data.noBtnText) ? this.data.noBtnText : 'No'; + this.yesBtnText = (undefined !== this.data.yesBtnText) ? this.data.yesBtnText : 'Yes'; + + // INFO/WARN/ERROR/SUCCESS/CONFIRM + if (this.data.type === 'WARN') { + this.msgTypeBackground = 'bg-accent p-1'; + } + if (this.data.type === 'ERROR') { + this.msgTypeBackground = 'bg-warn p-1'; + this.msgTypeForeground = 'warn'; + } + this.data.message = (undefined === this.data.message) ? '' : this.data.message.replace(/{/g, '').replace(/"/g, '').replace(/}/g, '').replace(/\n/g, ''); + // Start: For Payment Path + const arrayStartIdx = this.data.message.search('\\['); + const arrayEndIdx = this.data.message.search('\\]'); + if (arrayStartIdx > -1 && arrayEndIdx > -1) { + this.data.message = this.data.message.substring(0, arrayStartIdx).concat( + this.data.message.substring(arrayStartIdx + 1, arrayEndIdx).replace(/,/g, '\n'), + this.data.message.substring(arrayEndIdx + 1)); + } + // End: For Payment Path + this.messageObj = (this.data.message === '') ? [] : this.data.message.split(','); + this.messageObj.forEach((obj, idx) => { + this.messageObj[idx] = obj.split(':'); + this.messageObj[idx][0] = this.messageObj[idx][0].replace(/_/g, ' '); // To replace Backend Data's '_' + // Start: To Merge Time Value Again with ':', example Payment Creation Time + if (this.messageObj[idx].length > 2) { + this.messageObj[idx].forEach((dataValue, j) => { + if (j === 0 || j === 1) { + return; + } else { + this.messageObj[idx][1] = this.messageObj[idx][1] + ':' + this.messageObj[idx][j]; + } + }); + } + // End: To Merge Time Value Again with ':', example Payment Creation Time + }); + } + + showCopyOption(key): boolean { + let flgFoundKey = false; + const showCopyOnKeys = ['payment request']; + showCopyOnKeys.filter((arrKey) => { + if (arrKey === key) { + flgFoundKey = true; + return true; + } + }); + return flgFoundKey; + } + + copiedText(payload) { + this.flgCopied = true; + setTimeout(() => {this.flgCopied = false; }, 5000); + this.logger.info('Copied Text: ' + payload); + } + + isNumber(value, key): boolean { + let flgFoundKey = false; + const notNumberKeys = ['chan id', 'creation date']; + notNumberKeys.filter((arrKey) => { + if (arrKey === key) { + flgFoundKey = true; + } + }); + if (!flgFoundKey) { + return new RegExp(/^[0-9]+$/).test(value); + } else { + return false; + } + } + + onClose(dialogRes: any) { + this.store.dispatch(new RTLActions.CloseConfirmation(dialogRes)); + } +} diff --git a/src/app/shared/components/not-found/not-found.component.html b/src/app/shared/components/not-found/not-found.component.html new file mode 100644 index 00000000..c12b788e --- /dev/null +++ b/src/app/shared/components/not-found/not-found.component.html @@ -0,0 +1,16 @@ +
+ +
+ +

404

+
+ + +
This page does not exist!
+ +
+
+
+
diff --git a/src/app/shared/components/not-found/not-found.component.ts b/src/app/shared/components/not-found/not-found.component.ts new file mode 100644 index 00000000..834aa818 --- /dev/null +++ b/src/app/shared/components/not-found/not-found.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'rtl-not-found', + templateUrl: './not-found.component.html' +}) +export class NotFoundComponent { + + constructor(public router: Router) {} + + goHome(): void { + this.router.navigate(['/']); + } + +} diff --git a/src/app/shared/components/settings-nav/settings-nav.component.html b/src/app/shared/components/settings-nav/settings-nav.component.html new file mode 100644 index 00000000..19278ec9 --- /dev/null +++ b/src/app/shared/components/settings-nav/settings-nav.component.html @@ -0,0 +1,74 @@ + +

Settings

+
+
+ +
+

Currency Unit

+
+ {{currencyUnit}} + +
+ +
+

Menu

+ + {{menu}} + + +
+

Menu Type

+ + {{menuType}} + + +
+
+

Sidenav Options

+
+ Opened + +
+
+ Pinned + +
+
+ +
+

Skins

+
+
+
+
+
+
+
+
+
+
+ + +
+
diff --git a/src/app/shared/components/settings-nav/settings-nav.component.scss b/src/app/shared/components/settings-nav/settings-nav.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/shared/components/settings-nav/settings-nav.component.spec.ts b/src/app/shared/components/settings-nav/settings-nav.component.spec.ts new file mode 100644 index 00000000..05f43ea7 --- /dev/null +++ b/src/app/shared/components/settings-nav/settings-nav.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SettingsNavComponent } from './settings-nav.component'; + +describe('SettingsNavComponent', () => { + let component: SettingsNavComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SettingsNavComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SettingsNavComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/settings-nav/settings-nav.component.ts b/src/app/shared/components/settings-nav/settings-nav.component.ts new file mode 100644 index 00000000..97f9a4c0 --- /dev/null +++ b/src/app/shared/components/settings-nav/settings-nav.component.ts @@ -0,0 +1,80 @@ +import { Component, OnInit, OnDestroy, Output, EventEmitter } from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; + +import { Settings } from '../../models/RTLconfig'; +import { GetInfo } from '../../models/lndModels'; +import { LoggerService } from '../../services/logger.service'; + +import * as RTLActions from '../../store/rtl.actions'; +import * as fromRTLReducer from '../../store/rtl.reducers'; + +@Component({ + selector: 'rtl-settings-nav', + templateUrl: './settings-nav.component.html', + styleUrls: ['./settings-nav.component.scss'] +}) +export class SettingsNavComponent implements OnInit, OnDestroy { + public information: GetInfo = {}; + public settings: Settings; + public menus = ['Vertical', 'Horizontal']; + public menuTypes = ['Regular', 'Compact', 'Mini']; + public selectedMenu: string; + public selectedMenuType: string; + public currencyUnit = 'BTC'; + public showSettingOption = true; + unsubs: Array> = [new Subject(), new Subject()]; + @Output('done') done: EventEmitter = new EventEmitter(); + + constructor(private logger: LoggerService, private store: Store) {} + + ngOnInit() { + this.store.select('rtlRoot') + .pipe(takeUntil(this.unsubs[0])) + .subscribe((rtlStore: fromRTLReducer.State) => { + this.settings = rtlStore.settings; + this.selectedMenu = this.settings.menu; + this.selectedMenuType = this.settings.menuType; + if (window.innerWidth <= 768) { + this.settings.menu = 'Vertical'; + this.settings.flgSidenavOpened = false; + this.settings.flgSidenavPinned = false; + this.showSettingOption = false; + } + this.information = rtlStore.information; + this.currencyUnit = (undefined !== this.information && undefined !== this.information.currency_unit) ? this.information.currency_unit : 'BTC'; + this.logger.info(rtlStore); + }); + } + + public chooseMenu() { + this.settings.menu = this.selectedMenu; + } + + public chooseMenuType() { + this.settings.menuType = this.selectedMenuType; + } + + toggleSettings(toggleField: string) { + this.settings[toggleField] = !this.settings[toggleField]; + } + + changeTheme(newTheme: string) { + this.settings.theme = newTheme; + } + + onClose() { + this.logger.info(this.settings); + this.store.dispatch(new RTLActions.SaveSettings(this.settings)); + this.done.emit(); + } + + ngOnDestroy() { + this.unsubs.forEach(unsub => { + unsub.next(); + unsub.complete(); + }); + } + +} diff --git a/src/app/shared/components/spinner-dialog/spinner-dialog.component.html b/src/app/shared/components/spinner-dialog/spinner-dialog.component.html new file mode 100644 index 00000000..3d4e3539 --- /dev/null +++ b/src/app/shared/components/spinner-dialog/spinner-dialog.component.html @@ -0,0 +1,6 @@ +
+
+ +

{{data.titleMessage}}

+
+
diff --git a/src/app/shared/components/spinner-dialog/spinner-dialog.component.scss b/src/app/shared/components/spinner-dialog/spinner-dialog.component.scss new file mode 100644 index 00000000..8d712cca --- /dev/null +++ b/src/app/shared/components/spinner-dialog/spinner-dialog.component.scss @@ -0,0 +1,4 @@ +.spinner-container { + position: absolute; + left: 50%; +} diff --git a/src/app/shared/components/spinner-dialog/spinner-dialog.component.ts b/src/app/shared/components/spinner-dialog/spinner-dialog.component.ts new file mode 100644 index 00000000..162cc3dd --- /dev/null +++ b/src/app/shared/components/spinner-dialog/spinner-dialog.component.ts @@ -0,0 +1,16 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; + +@Component({ + selector: 'rtl-spinner-dialog', + templateUrl: './spinner-dialog.component.html', + styleUrls: ['./spinner-dialog.component.scss'] +}) +export class SpinnerDialogComponent implements OnInit { + + constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any) { } + + ngOnInit() { + } + +} diff --git a/src/app/shared/components/sso-failed/sso-failed.component.html b/src/app/shared/components/sso-failed/sso-failed.component.html new file mode 100644 index 00000000..7318507c --- /dev/null +++ b/src/app/shared/components/sso-failed/sso-failed.component.html @@ -0,0 +1,15 @@ +
+ +
+ +

401

+
+ + +
Single Sign On Failed!
+
+
+
+
diff --git a/src/app/shared/components/sso-failed/sso-failed.component.ts b/src/app/shared/components/sso-failed/sso-failed.component.ts new file mode 100644 index 00000000..c0653b4d --- /dev/null +++ b/src/app/shared/components/sso-failed/sso-failed.component.ts @@ -0,0 +1,14 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'rtl-sso-failed', + templateUrl: './sso-failed.component.html' +}) +export class SsoFailedComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/src/app/shared/directive/clipboard.directive.spec.ts b/src/app/shared/directive/clipboard.directive.spec.ts new file mode 100644 index 00000000..79021ddf --- /dev/null +++ b/src/app/shared/directive/clipboard.directive.spec.ts @@ -0,0 +1,8 @@ +import { ClipboardDirective } from './clipboard.directive'; + +describe('ClipboardDirective', () => { + it('should create an instance', () => { + const directive = new ClipboardDirective(); + expect(directive).toBeTruthy(); + }); +}); diff --git a/src/app/shared/directive/clipboard.directive.ts b/src/app/shared/directive/clipboard.directive.ts new file mode 100644 index 00000000..9a49de3e --- /dev/null +++ b/src/app/shared/directive/clipboard.directive.ts @@ -0,0 +1,28 @@ +import { Directive, Input, Output, EventEmitter, HostListener } from '@angular/core'; + +@Directive({ + selector: '[rtlClipboard]' +}) +export class ClipboardDirective { + @Input() payload: string; + + @Output('copied') + public copied: EventEmitter = new EventEmitter(); + + @HostListener('click', ['$event']) + public onClick(event: MouseEvent): void { + event.preventDefault(); + if (!this.payload) { + return; + } + const listener = (e: ClipboardEvent) => { + const clipboard = e.clipboardData || window['clipboardData']; + clipboard.setData('text', this.payload.toString()); + e.preventDefault(); + this.copied.emit(this.payload); + }; + document.addEventListener('copy', listener, false); + document.execCommand('copy'); + document.removeEventListener('copy', listener, false); + } +} diff --git a/src/app/shared/models/RTLconfig.ts b/src/app/shared/models/RTLconfig.ts new file mode 100644 index 00000000..663c0f17 --- /dev/null +++ b/src/app/shared/models/RTLconfig.ts @@ -0,0 +1,31 @@ +export class Settings { + constructor( + public flgSidenavOpened: boolean, + public flgSidenavPinned: boolean, + public menu: string, + public menuType: string, + public theme: string, + public satsToBTC: boolean + ) { } +} + +export class Authentication { + constructor( + public lndServerUrl?: string, + public macaroonPath?: string, + public nodeAuthType?: string, + public lndConfigPath?: string, + public bitcoindConfigPath?: string, + public rtlPass?: string, + public enableLogging?: string, + public rtlSSO?: number, + public logoutRedirectLink?: string + ) { } +} + +export class RTLConfiguration { + constructor( + public settings: Settings, + public authentication: Authentication + ) { } +} diff --git a/src/app/shared/models/alertData.ts b/src/app/shared/models/alertData.ts new file mode 100644 index 00000000..d37427c7 --- /dev/null +++ b/src/app/shared/models/alertData.ts @@ -0,0 +1,15 @@ +export interface InputData { + placeholder: string; + inputValue?: string; + inputType?: string; +} + +export interface AlertData { + type: string; // INFO/WARN/ERROR/SUCCESS/CONFIRM + titleMessage?: string; + message?: string; + noBtnText?: string; + yesBtnText?: string; + flgShowInput?: boolean; + getInputs?: Array; +} diff --git a/src/app/shared/models/errorPayload.ts b/src/app/shared/models/errorPayload.ts new file mode 100644 index 00000000..9abcb4b0 --- /dev/null +++ b/src/app/shared/models/errorPayload.ts @@ -0,0 +1,7 @@ +export interface ErrorPayload { + action?: string; + code?: string; + message?: string; + URL?: string; + filePath?: string; +} diff --git a/src/app/shared/models/lndModels.ts b/src/app/shared/models/lndModels.ts new file mode 100644 index 00000000..a1e48064 --- /dev/null +++ b/src/app/shared/models/lndModels.ts @@ -0,0 +1,340 @@ +export interface AddressType { + addressId?: string; + addressTp?: string; + addressDetails?: string; +} + +export interface Balance { + btc_balance?: string; // For Channels Balance + balance?: string; // For Channels Balance + btc_pending_open_balance?: string; // For Channels Balance + pending_open_balance?: string; // For Channels Balance + btc_total_balance?: string; // For Blockchain Balance + total_balance?: string; // For Blockchain Balance + btc_confirmed_balance?: string; // For Blockchain Balance + confirmed_balance?: string; // For Blockchain Balance + btc_unconfirmed_balance?: string; // For Blockchain Balance + unconfirmed_balance?: string; // For Blockchain Balance +} + +export interface ChannelFeeReport { + chan_point?: string; + base_fee_msat?: number; + fee_per_mil?: number; + fee_rate?: number; +} + +export interface Channel { + active?: boolean; + remote_pubkey?: string; + remote_alias?: string; + channel_point?: string; + chan_id?: number; + capacity?: number; + local_balance?: number; + remote_balance?: number; + commit_fee?: number; + commit_weight?: number; + fee_per_kw?: number; + unsettled_balance?: number; + total_satoshis_sent?: number; + total_satoshis_received?: number; + num_updates?: number; + pending_htlcs?: HTLC[]; + csv_delay?: number; + private?: boolean; +} + +export interface PendingChannels { + total_limbo_balance?: number; + btc_total_limbo_balance?: number; + pending_closing_channels?: Array; + pending_force_closing_channels?: Array; + pending_open_channels?: Array; + waiting_close_channels?: Array; +} + +export interface ClosedChannel { + time_locked_balance?: string; + closing_tx_hash?: string; + close_type?: any; + close_height?: number; + chain_hash?: string; + channel_point?: string; + chan_id?: string; + remote_pubkey?: string; + capacity?: string; + settled_balance?: string; +} + +export interface NetworkGraph { + nodes: LightningNode[]; + edges: ChannelEdge[]; +} + +export interface LightningNode { + last_update?: number; + pub_key?: string; + alias?: string; + addresses?: NodeAddress[]; + color?: string; +} + +export interface NodeAddress { + network?: string; + addr?: string; +} + +export interface ChannelEdge { + channel_id?: string; + chan_point?: string; + last_update?: number; + last_update_str?: string; + node1_pub?: string; + node2_pub?: string; + capacity?: string; + node1_policy?: RoutingPolicy; + node2_policy?: RoutingPolicy; +} + +export interface RoutingPolicy { + time_lock_delta?: number; + min_htlc?: string; + fee_base_msat?: string; + fee_rate_milli_msat?: string; + disabled?: boolean; +} + +export interface SigmaNode { + id: string; + label: string; + x: number; + y: number; + size: number; +} + +export interface SigmaEdge { + id: string; + source: string; + target: string; +} + +export interface FeeLimit { + percent?: number; + fixed?: number; +} + +export interface Fees { + channel_fees?: ChannelFeeReport[]; + day_fee_sum?: number; + week_fee_sum?: number; + month_fee_sum?: number; + btc_day_fee_sum?: number; + btc_week_fee_sum?: number; + btc_month_fee_sum?: number; +} + +export interface GetInfoChain { + chain?: string; + network?: string; +} + +export interface GetInfo { + identity_pubkey?: string; + alias?: string; + num_pending_channels?: number; + num_active_channels?: number; + num_peers?: number; + block_height?: number; + block_hash?: string; + synced_to_chain?: boolean; + testnet?: boolean; + chains?: GetInfoChain[] | string[]; + uris?: string[]; + best_header_timestamp?: number; + version?: string; + currency_unit?: string; + smaller_currency_unit?: string; +} + +export interface GraphNode { + node?: LightningNode; + num_channels?: number; + total_capacity?: string; +} + +export interface HopHint { + cltv_expiry_delta?: number; + node_id?: string; + chan_id?: number; + fee_proportional_millionths?: number; + fee_base_msat?: number; +} + +export interface HTLC { + incoming?: boolean; + amount?: number; + hash_lock?: string; + expiration_height?: number; +} + +export interface Invoice { + memo?: string; + receipt?: string; + r_preimage?: string; + r_hash?: string; + value?: string; + btc_value?: string; + settled?: boolean; + creation_date?: string; + creation_date_str?: string; + settle_date?: string; + settle_date_str?: string; + payment_request?: string; + description_hash?: string; + expiry?: string; + fallback_addr?: string; + cltv_expiry?: string; + route_hints?: RouteHint[]; + private?: boolean; + add_index?: string; + settle_index?: string; + amt_paid?: string; + amt_paid_sat?: string; + btc_amt_paid_sat?: string; + amt_paid_msat?: string; +} + +export interface ListInvoices { + invoices?: Invoice[]; + last_index_offset?: string; + first_index_offset?: string; +} + +export interface LightningNode { + last_update?: number; + last_update_str?: string; + pub_key?: string; + alias?: string; + addresses?: NodeAddress[]; + color?: string; +} + +export interface NetworkInfo { + num_nodes?: number; + btc_max_channel_size?: string; + max_channel_size?: string; + btc_avg_channel_size?: string; + avg_channel_size?: string; + graph_diameter?: number; + num_channels?: number; + max_out_degree?: number; + btc_total_network_capacity?: string; + total_network_capacity?: string; + avg_out_degree?: number; + btc_min_channel_size?: string; + min_channel_size?: string; +} + +export interface NodeAddress { + network?: string; + address?: string; +} + +export interface Payment { + creation_date?: number; + creation_date_str?: string; + payment_hash?: string; + path?: string[]; + fee?: number; + value_msat?: number; + value_sat?: number; + value?: number; + payment_preimage?: string; +} + +export interface PayRequest { + payment_hash?: string; + route_hints?: RouteHint[]; + timestamp?: number; + timestamp_str?: string; + fallback_addr?: string; + cltv_expiry?: number; + description_hash?: string; + destination?: string; + expiry?: number; + description?: string; + num_satoshis?: string; + btc_num_satoshis?: string; +} + +export interface Peer { + pub_key?: string; + alias?: string; + address?: string; // host + bytes_sent?: number; + bytes_recv?: number; + sat_sent?: string; + sat_recv?: string; + inbound?: boolean; + ping_time?: number; +} + +export interface RouteHint { + hop_hints?: HopHint[]; +} + +export interface SendPayment { + dest_string?: string; + dest?: string; + payment_hash_string?: string; + payment_request?: string; + fee_limit?: FeeLimit; + amt?: number; + payment_hash?: string; + final_cltv_delta?: number; +} + +export interface ChannelsTransaction { + address?: string; + amount?: number; + sendAll?: boolean; + blocks?: number; + fees?: number; +} + +export interface Transaction { + dest_addresses?: string[]; + time_stamp?: string; + time_stamp_str?: string; + num_confirmations?: number; + total_fees?: string; + block_hash?: string; + block_height?: number; + tx_hash?: string; + amount?: string; +} + +export interface SwitchReq { + num_max_events?: number; + index_offset?: number; + end_time?: string; + start_time?: string; +} + +export interface ForwardingEvent { + timestamp?: string; + timestamp_str?: string; + chan_id_out?: string; + alias_out?: string; + amt_out?: string; + amt_in?: string; + chan_id_in?: string; + alias_in?: string; + fee?: string; +} + +export interface SwitchRes { + last_offset_index?: number; + forwarding_events?: ForwardingEvent[]; +} diff --git a/src/app/shared/models/navMenu.ts b/src/app/shared/models/navMenu.ts new file mode 100644 index 00000000..c0564a35 --- /dev/null +++ b/src/app/shared/models/navMenu.ts @@ -0,0 +1,39 @@ +export const MENU_DATA: MenuNode = { + id: 0, + parentId: 0, + name: 'root', + icon: 'root', + link: 'root', + children: [ + {id: 1, parentId: 0, name: 'Home', icon: 'home', link: '/home'}, + {id: 2, parentId: 0, name: 'LND Wallet', icon: 'account_balance_wallet', link: '/transsendreceive', children: [ + {id: 21, parentId: 2, name: 'Send/Receive', icon: 'compare_arrows', link: '/transsendreceive'}, + {id: 22, parentId: 2, name: 'List Transactions', icon: 'list_alt', link: '/translist'}, + ]}, + {id: 3, parentId: 0, name: 'Peers', icon: 'group', link: '/peers'}, + {id: 4, parentId: 0, name: 'Channels', icon: 'settings_ethernet', link: '/chnlmanage', children: [ + {id: 41, parentId: 4, name: 'Management', icon: 'subtitles', link: '/chnlmanage'}, + {id: 42, parentId: 4, name: 'Pending', icon: 'watch', link: '/chnlpending'}, + {id: 43, parentId: 4, name: 'Closed', icon: 'watch_later', link: '/chnlclosed'} + ]}, + {id: 5, parentId: 0, name: 'Payments', icon: 'payment', link: '/payments'}, + {id: 6, parentId: 0, name: 'Invoices', icon: 'receipt', link: '/invoices'}, + {id: 7, parentId: 0, name: 'Forwarding History', icon: 'timeline', link: '/switch'}, + {id: 8, parentId: 0, name: 'Lookups', icon: 'search', link: '/lookups'}, + {id: 9, parentId: 0, name: 'Node Config', icon: 'perm_data_setting', link: '/sconfig'}, + {id: 10, parentId: 0, name: 'Help', icon: 'help', link: '/help'} + ] +}; + +export class MenuNode { + id: number; + parentId: number; + name?: string; + icon?: string; + link?: any; + children?: MenuNode[]; +} + +export class FlatMenuNode { + constructor(public expandable: boolean, public level: number, public id: number, public parentId: number, public name: string, public icon: string, public link: string) {} +} diff --git a/src/app/shared/services/auth.guard.ts b/src/app/shared/services/auth.guard.ts new file mode 100644 index 00000000..a3f12af5 --- /dev/null +++ b/src/app/shared/services/auth.guard.ts @@ -0,0 +1,29 @@ +import { CanActivate } from '@angular/router'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +@Injectable() +export class AuthGuard implements CanActivate { + constructor() {} + + canActivate(): boolean | Observable | Promise { + if (!sessionStorage.getItem('token')) { + return false; + } else { + return true; + } + } +} + +@Injectable() +export class LNDUnlockedGuard implements CanActivate { + constructor() {} + + canActivate(): boolean | Observable | Promise { + if (!sessionStorage.getItem('lndUnlocked')) { + return false; + } else { + return true; + } + } +} diff --git a/src/app/shared/services/auth.interceptor.ts b/src/app/shared/services/auth.interceptor.ts new file mode 100644 index 00000000..ad47aca3 --- /dev/null +++ b/src/app/shared/services/auth.interceptor.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; +import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + + constructor() {} + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + if (sessionStorage.getItem('token')) { + const cloned = req.clone({ + headers: req.headers.set('Authorization', 'Bearer ' + sessionStorage.getItem('token')) + }); + return next.handle(cloned); + } else { + return next.handle(req); + } + } + +} diff --git a/src/app/shared/services/logger.service.ts b/src/app/shared/services/logger.service.ts new file mode 100644 index 00000000..22ae50d9 --- /dev/null +++ b/src/app/shared/services/logger.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@angular/core'; +import { environment } from '../../../environments/environment'; +export let isDebugMode = environment.isDebugMode; +const noop = (): any => undefined; + +export abstract class Logger { + info: any; + warn: any; + error: any; +} + +@Injectable() +export class LoggerService implements Logger { + info: any; + warn: any; + error: any; + invokeConsoleMethod(type: string, args?: any): void {} +} + +@Injectable() +export class ConsoleLoggerService implements Logger { + get info() { + if (isDebugMode) { + return console.log.bind(console); + } else { + return noop; + } + } + + get warn() { + if (isDebugMode) { + return console.warn.bind(console); + } else { + return noop; + } + } + + get error() { + if (isDebugMode) { + return console.error.bind(console); + } else { + return noop; + } + } + + invokeConsoleMethod(type: string, args?: any): void { + const logFn: Function = (console)[type] || console.log || noop; + logFn.apply(console, [args]); + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts new file mode 100644 index 00000000..c2cca0b3 --- /dev/null +++ b/src/app/shared/shared.module.ts @@ -0,0 +1,105 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { + MatButtonModule, MatButtonToggleModule, MatCardModule, MatCheckboxModule, MatDialogModule, MatExpansionModule, MatGridListModule, MatDatepickerModule, + MatIconModule, MatInputModule, MatListModule, MatMenuModule, MatProgressBarModule, MatProgressSpinnerModule, MatRadioModule, MatTreeModule, MatNativeDateModule, + MatSelectModule, MatSidenavModule, MatSlideToggleModule, MatSortModule, MatTableModule, MatToolbarModule, MatTooltipModule, MAT_DIALOG_DEFAULT_OPTIONS, MatBadgeModule +} from '@angular/material'; +import { QRCodeModule } from 'angularx-qrcode'; +import { AlertMessageComponent } from './components/alert-message/alert-message.component'; +import { ConfirmationMessageComponent } from './components/confirmation-message/confirmation-message.component'; +import { SpinnerDialogComponent } from './components/spinner-dialog/spinner-dialog.component'; +import { NotFoundComponent } from './components/not-found/not-found.component'; +import { SettingsNavComponent } from './components/settings-nav/settings-nav.component'; +import { ClipboardDirective } from './directive/clipboard.directive'; +import { SsoFailedComponent } from './components/sso-failed/sso-failed.component'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + FlexLayoutModule, + MatButtonModule, + MatButtonToggleModule, + MatCardModule, + MatCheckboxModule, + MatDialogModule, + MatExpansionModule, + MatGridListModule, + MatDatepickerModule, + MatNativeDateModule, + MatIconModule, + MatInputModule, + MatListModule, + MatMenuModule, + MatProgressBarModule, + MatProgressSpinnerModule, + MatRadioModule, + MatTreeModule, + MatSelectModule, + MatSidenavModule, + MatSlideToggleModule, + MatSortModule, + MatTableModule, + MatToolbarModule, + MatTooltipModule, + MatBadgeModule, + QRCodeModule + ], + exports: [ + FlexLayoutModule, + MatButtonModule, + MatButtonToggleModule, + MatCardModule, + MatCheckboxModule, + MatDialogModule, + MatExpansionModule, + MatGridListModule, + MatDatepickerModule, + MatNativeDateModule, + MatIconModule, + MatInputModule, + MatListModule, + MatMenuModule, + MatProgressBarModule, + MatProgressSpinnerModule, + MatRadioModule, + MatTreeModule, + MatSelectModule, + MatSidenavModule, + MatSlideToggleModule, + MatSortModule, + MatTableModule, + MatToolbarModule, + MatTooltipModule, + MatBadgeModule, + AlertMessageComponent, + ConfirmationMessageComponent, + SpinnerDialogComponent, + NotFoundComponent, + SettingsNavComponent, + ClipboardDirective, + QRCodeModule + ], + declarations: [ + AlertMessageComponent, + ConfirmationMessageComponent, + SpinnerDialogComponent, + NotFoundComponent, + SettingsNavComponent, + ClipboardDirective, + SsoFailedComponent + ], + entryComponents: [ + AlertMessageComponent, + SpinnerDialogComponent, + ConfirmationMessageComponent + ], + providers: [ + { provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: { hasBackdrop: true, autoFocus: true, disableClose: true, role: 'dialog', width: '700px' } } + ] +}) +export class SharedModule { } diff --git a/src/app/shared/store/rtl.actions.ts b/src/app/shared/store/rtl.actions.ts new file mode 100644 index 00000000..129bfffa --- /dev/null +++ b/src/app/shared/store/rtl.actions.ts @@ -0,0 +1,405 @@ +import { Action } from '@ngrx/store'; +import { Settings, Authentication } from '../models/RTLconfig'; +import { ErrorPayload } from '../models/errorPayload'; +import { + GetInfo, Peer, Balance, NetworkInfo, Fees, Channel, Invoice, Payment, GraphNode, AddressType, + PayRequest, ChannelsTransaction, PendingChannels, ClosedChannel, Transaction, SwitchReq, SwitchRes +} from '../models/lndModels'; +import { MatDialogConfig } from '@angular/material'; + +export const CLEAR_EFFECT_ERROR = 'CLEAR_EFFECT_ERROR'; +export const EFFECT_ERROR = 'EFFECT_ERROR'; +export const OPEN_SPINNER = 'OPEN_SPINNER'; +export const CLOSE_SPINNER = 'CLOSE_SPINNER'; +export const OPEN_ALERT = 'OPEN_ALERT'; +export const CLOSE_ALERT = 'CLOSE_ALERT'; +export const OPEN_CONFIRMATION = 'OPEN_CONFIRMATION'; +export const CLOSE_CONFIRMATION = 'CLOSE_CONFIRMATION'; +export const FETCH_STORE = 'FETCH_STORE'; +export const SET_STORE = 'SET_STORE'; +export const FETCH_SETTINGS = 'FETCH_SETTINGS'; +export const SET_SETTINGS = 'SET_SETTINGS'; +export const SET_AUTH_SETTINGS = 'SET_AUTH_SETTINGS'; +export const SAVE_SETTINGS = 'SAVE_SETTINGS'; +export const FETCH_INFO = 'FETCH_INFO'; +export const SET_INFO = 'SET_INFO'; +export const FETCH_PEERS = 'FETCH_PEERS'; +export const SET_PEERS = 'SET_PEERS'; +export const SAVE_NEW_PEER = 'SAVE_NEW_PEER'; +export const ADD_PEER = 'ADD_PEER'; +export const DETACH_PEER = 'DETACH_PEER'; +export const REMOVE_PEER = 'REMOVE_PEER'; +export const SAVE_NEW_INVOICE = 'SAVE_NEW_INVOICE'; +export const ADD_INVOICE = 'ADD_INVOICE'; +export const FETCH_FEES = 'FETCH_FEES'; +export const SET_FEES = 'SET_FEES'; +export const FETCH_BALANCE = 'FETCH_BALANCE'; +export const SET_BALANCE = 'SET_BALANCE'; +export const FETCH_NETWORK = 'FETCH_NETWORK'; +export const SET_NETWORK = 'SET_NETWORK'; +export const FETCH_CHANNELS = 'FETCH_CHANNELS'; +export const SET_CHANNELS = 'SET_CHANNELS'; +export const UPDATE_CHANNELS = 'UPDATE_CHANNELS'; +export const SET_PENDING_CHANNELS = 'SET_PENDING_CHANNELS'; +export const SET_CLOSED_CHANNELS = 'SET_CLOSED_CHANNELS'; +export const SAVE_NEW_CHANNEL = 'SAVE_NEW_CHANNEL'; +export const CLOSE_CHANNEL = 'CLOSE_CHANNEL'; +export const REMOVE_CHANNEL = 'REMOVE_CHANNEL'; +export const FETCH_INVOICES = 'FETCH_INVOICES'; +export const SET_INVOICES = 'SET_INVOICES'; +export const FETCH_TRANSACTIONS = 'FETCH_TRANSACTIONS'; +export const SET_TRANSACTIONS = 'SET_TRANSACTIONS'; +export const FETCH_PAYMENTS = 'FETCH_PAYMENTS'; +export const SET_PAYMENTS = 'SET_PAYMENTS'; +export const DECODE_PAYMENT = 'DECODE_PAYMENT'; +export const SEND_PAYMENT = 'SEND_PAYMENT'; +export const SET_DECODED_PAYMENT = 'SET_DECODED_PAYMENT'; +export const FETCH_GRAPH_NODE = 'FETCH_GRAPH_NODE'; +export const SET_GRAPH_NODE = 'SET_GRAPH_NODE'; +export const GET_NEW_ADDRESS = 'GET_NEW_ADDRESS'; +export const SET_NEW_ADDRESS = 'SET_NEW_ADDRESS'; +export const SET_CHANNEL_TRANSACTION = 'SET_CHANNEL_TRANSACTION'; +export const OPERATE_WALLET = 'OPERATE_WALLET'; +export const FETCH_CONFIG = 'FETCH_CONFIG'; +export const SHOW_CONFIG = 'SHOW_CONFIG'; +export const IS_AUTHORIZED = 'IS_AUTHORIZED'; +export const IS_AUTHORIZED_RES = 'IS_AUTHORIZED_RES'; +export const SIGNIN = 'SIGNIN'; +export const SIGNOUT = 'SIGNOUT'; +export const INIT_APP_DATA = 'INIT_APP_DATA'; +export const PEER_LOOKUP = 'PEER_LOOKUP'; +export const CHANNEL_LOOKUP = 'CHANNEL_LOOKUP'; +export const INVOICE_LOOKUP = 'INVOICE_LOOKUP'; +export const SET_LOOKUP = 'SET_LOOKUP'; +export const GET_FORWARDING_HISTORY = 'GET_FORWARDING_HISTORY'; +export const SET_FORWARDING_HISTORY = 'SET_FORWARDING_HISTORY'; + +export class ClearEffectError implements Action { + readonly type = CLEAR_EFFECT_ERROR; + constructor(public payload: string) {} // payload = errorAction +} + +export class EffectError implements Action { + readonly type = EFFECT_ERROR; + constructor(public payload: ErrorPayload) {} +} + +export class OpenSpinner implements Action { + readonly type = OPEN_SPINNER; + constructor(public payload: string) {} // payload = titleMessage +} + +export class CloseSpinner implements Action { + readonly type = CLOSE_SPINNER; +} + +export class OpenAlert implements Action { + readonly type = OPEN_ALERT; + constructor(public payload: MatDialogConfig) {} +} + +export class CloseAlert implements Action { + readonly type = CLOSE_ALERT; +} + +export class OpenConfirmation implements Action { + readonly type = OPEN_CONFIRMATION; + constructor(public payload: MatDialogConfig) {} +} + +export class CloseConfirmation implements Action { + readonly type = CLOSE_CONFIRMATION; + constructor(public payload: boolean) {} +} + +export class FetchSettings implements Action { + readonly type = FETCH_SETTINGS; +} + +export class SetSettings implements Action { + readonly type = SET_SETTINGS; + constructor(public payload: Settings) {} +} + +export class SetAuthSettings implements Action { + readonly type = SET_AUTH_SETTINGS; + constructor(public payload: Authentication) {} +} + +export class SaveSettings implements Action { + readonly type = SAVE_SETTINGS; + constructor(public payload: Settings) {} +} + +export class FetchInfo implements Action { + readonly type = FETCH_INFO; +} + +export class SetInfo implements Action { + readonly type = SET_INFO; + constructor(public payload: GetInfo) {} +} + +export class FetchPeers implements Action { + readonly type = FETCH_PEERS; +} + +export class SetPeers implements Action { + readonly type = SET_PEERS; + constructor(public payload: Peer[]) {} +} + +export class SaveNewPeer implements Action { + readonly type = SAVE_NEW_PEER; + constructor(public payload: {pubkey: string, host: string, perm: boolean}) {} +} + +export class AddPeer implements Action { + readonly type = ADD_PEER; + constructor(public payload: Peer) {} +} + +export class DetachPeer implements Action { + readonly type = DETACH_PEER; + constructor(public payload: {pubkey: string}) {} +} + +export class RemovePeer implements Action { + readonly type = REMOVE_PEER; + constructor(public payload: {pubkey: string}) {} +} + +export class SaveNewInvoice implements Action { + readonly type = SAVE_NEW_INVOICE; + constructor(public payload: {memo: string, invoiceValue: number}) {} +} + +export class AddInvoice implements Action { + readonly type = ADD_INVOICE; + constructor(public payload: Invoice) {} +} + +export class FetchFees implements Action { + readonly type = FETCH_FEES; +} + +export class SetFees implements Action { + readonly type = SET_FEES; + constructor(public payload: Fees) {} +} + +export class FetchBalance implements Action { + readonly type = FETCH_BALANCE; + constructor(public payload: string) {} // payload = routeParam +} + +export class SetBalance implements Action { + readonly type = SET_BALANCE; + constructor(public payload: {target: string, balance: Balance}) {} +} + +export class FetchNetwork implements Action { + readonly type = FETCH_NETWORK; +} + +export class SetNetwork implements Action { + readonly type = SET_NETWORK; + constructor(public payload: NetworkInfo) {} +} + +export class FetchChannels implements Action { + readonly type = FETCH_CHANNELS; + constructor(public payload: {routeParam: string, channelStatus: string}) {} +} + +export class SetChannels implements Action { + readonly type = SET_CHANNELS; + constructor(public payload: Channel[]) {} +} + +export class UpdateChannels implements Action { + readonly type = UPDATE_CHANNELS; + constructor(public payload: any) {} +} + +export class SetPendingChannels implements Action { + readonly type = SET_PENDING_CHANNELS; + constructor(public payload: PendingChannels) {} +} + +export class SetClosedChannels implements Action { + readonly type = SET_CLOSED_CHANNELS; + constructor(public payload: ClosedChannel[]) {} +} + +export class SaveNewChannel implements Action { + readonly type = SAVE_NEW_CHANNEL; + constructor(public payload: {selectedPeerPubkey: string, fundingAmount: number, transType: string, transTypeValue: string, spendUnconfirmed: boolean}) {} +} + +export class CloseChannel implements Action { + readonly type = CLOSE_CHANNEL; + constructor(public payload: {channelPoint: string, forcibly: boolean, channelStatus: boolean}) {} +} + +export class RemoveChannel implements Action { + readonly type = REMOVE_CHANNEL; + constructor(public payload: {channelPoint: string}) {} +} + +export class FetchInvoices implements Action { + readonly type = FETCH_INVOICES; +} + +export class SetInvoices implements Action { + readonly type = SET_INVOICES; + constructor(public payload: Invoice[]) {} +} + +export class FetchTransactions implements Action { + readonly type = FETCH_TRANSACTIONS; +} + +export class SetTransactions implements Action { + readonly type = SET_TRANSACTIONS; + constructor(public payload: Transaction[]) {} +} + +export class FetchPayments implements Action { + readonly type = FETCH_PAYMENTS; +} + +export class SetPayments implements Action { + readonly type = SET_PAYMENTS; + constructor(public payload: Payment[]) {} +} + +export class DecodePayment implements Action { + readonly type = DECODE_PAYMENT; + constructor(public payload: string) {} // payload = routeParam +} + +export class SetDecodedPayment implements Action { + readonly type = SET_DECODED_PAYMENT; + constructor(public payload: PayRequest) {} +} + +export class SendPayment implements Action { + readonly type = SEND_PAYMENT; + constructor(public payload: [string, PayRequest, boolean]) {} // payload = [paymentReqStr, paymentDecoded, EmptyAmtInvoice] +} + +export class FetchGraphNode implements Action { + readonly type = FETCH_GRAPH_NODE; + constructor(public payload: string) {} // payload = pubkey +} + +export class SetGraphNode implements Action { + readonly type = SET_GRAPH_NODE; + constructor(public payload: GraphNode) {} +} + +export class GetNewAddress implements Action { + readonly type = GET_NEW_ADDRESS; + constructor(public payload: AddressType) {} +} + +export class SetNewAddress implements Action { + readonly type = SET_NEW_ADDRESS; + constructor(public payload: string) {} // payload = newAddress +} + +export class SetChannelTransaction implements Action { + readonly type = SET_CHANNEL_TRANSACTION; + constructor(public payload: ChannelsTransaction) {} +} + +export class OperateWallet implements Action { + readonly type = OPERATE_WALLET; + constructor(public payload: {operation: string, pwd: string}) {} +} + +export class FetchConfig implements Action { + readonly type = FETCH_CONFIG; + constructor(public payload: string) {} // payload = lnd/bitcoin node +} + +export class ShowConfig implements Action { + readonly type = SHOW_CONFIG; + constructor(public payload: any) {} // payload = Config File +} + +export class PeerLookup implements Action { + readonly type = PEER_LOOKUP; + constructor(public payload: string) {} // payload = pubkey +} + +export class ChannelLookup implements Action { + readonly type = CHANNEL_LOOKUP; + constructor(public payload: string) {} // payload = chanID +} + +export class InvoiceLookup implements Action { + readonly type = INVOICE_LOOKUP; + constructor(public payload: string) {} // payload = rHashStr +} + +export class SetLookup implements Action { + readonly type = SET_LOOKUP; + constructor(public payload: any) {} // payload = lookup Response (Peer/Channel/Invoice) +} + +export class GetForwardingHistory implements Action { + readonly type = GET_FORWARDING_HISTORY; + constructor(public payload: SwitchReq) {} +} + +export class SetForwardingHistory implements Action { + readonly type = SET_FORWARDING_HISTORY; + constructor(public payload: SwitchRes) {} +} + +export class IsAuthorized implements Action { + readonly type = IS_AUTHORIZED; + constructor(public payload: string) {} // payload = password +} + +export class IsAuthorizedRes implements Action { + readonly type = IS_AUTHORIZED_RES; + constructor(public payload: any) {} // payload = token/error +} + +export class Signin implements Action { + readonly type = SIGNIN; + constructor(public payload: string) {} // payload = password +} + +export class Signout implements Action { + readonly type = SIGNOUT; + constructor() {} +} + +export class InitAppData implements Action { + readonly type = INIT_APP_DATA; +} + +export type RTLActions = + ClearEffectError | EffectError | OpenSpinner | CloseSpinner | + FetchSettings | SetSettings | SaveSettings | SetAuthSettings | + OpenAlert | CloseAlert | OpenConfirmation | CloseConfirmation | + FetchInfo | SetInfo | + FetchPeers | SetPeers | AddPeer | DetachPeer | SaveNewPeer | RemovePeer | + AddInvoice | SaveNewInvoice | GetForwardingHistory | SetForwardingHistory | + FetchFees | SetFees | + FetchBalance | SetBalance | + FetchNetwork | SetNetwork | + FetchChannels | SetChannels | SetPendingChannels | SetClosedChannels | UpdateChannels | + SaveNewChannel | CloseChannel | RemoveChannel | + FetchTransactions | SetTransactions | + FetchInvoices | SetInvoices | + FetchPayments | SetPayments | SendPayment | + DecodePayment | SetDecodedPayment | + FetchGraphNode | SetGraphNode | + GetNewAddress | SetNewAddress | SetChannelTransaction | OperateWallet | + FetchConfig | ShowConfig | PeerLookup | ChannelLookup | InvoiceLookup | SetLookup | + IsAuthorized | IsAuthorizedRes | Signin | Signout | InitAppData; diff --git a/src/app/shared/store/rtl.effects.ts b/src/app/shared/store/rtl.effects.ts new file mode 100644 index 00000000..a224c6bd --- /dev/null +++ b/src/app/shared/store/rtl.effects.ts @@ -0,0 +1,1064 @@ +import { Injectable, OnDestroy } from '@angular/core'; +import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http'; +import { Router } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { Actions, Effect, ofType } from '@ngrx/effects'; +import { of, Subject } from 'rxjs'; +import { map, mergeMap, catchError, take, withLatestFrom } from 'rxjs/operators'; + +import { MatDialog } from '@angular/material'; + +import { environment } from '../../../environments/environment'; +import { LoggerService } from '../services/logger.service'; +import { Settings } from '../models/RTLconfig'; +import { GetInfo, Fees, Balance, NetworkInfo, Payment, Invoice, GraphNode, Transaction, SwitchReq } from '../models/lndModels'; + +import { SpinnerDialogComponent } from '../components/spinner-dialog/spinner-dialog.component'; +import { AlertMessageComponent } from '../components/alert-message/alert-message.component'; +import { ConfirmationMessageComponent } from '../components/confirmation-message/confirmation-message.component'; + +import * as RTLActions from './rtl.actions'; +import * as fromRTLReducer from './rtl.reducers'; + +@Injectable() +export class RTLEffects implements OnDestroy { + dialogRef: any; + private unSubs: Array> = [new Subject(), new Subject()]; + + constructor( + private actions$: Actions, + private httpClient: HttpClient, + private store: Store, + private logger: LoggerService, + public dialog: MatDialog, + private router: Router) { } + + @Effect({ dispatch: false }) + openSpinner = this.actions$.pipe( + ofType(RTLActions.OPEN_SPINNER), + map((action: RTLActions.OpenSpinner) => { + this.dialogRef = this.dialog.open(SpinnerDialogComponent, { data: { titleMessage: action.payload}}); + } + )); + + @Effect({ dispatch: false }) + closeSpinner = this.actions$.pipe( + ofType(RTLActions.CLOSE_SPINNER), + map((action: RTLActions.CloseSpinner) => { + this.dialogRef.close(); + } + )); + + @Effect({ dispatch: false }) + openAlert = this.actions$.pipe( + ofType(RTLActions.OPEN_ALERT), + map((action: RTLActions.OpenAlert) => { + this.dialogRef = this.dialog.open(AlertMessageComponent, action.payload); + } + )); + + @Effect({ dispatch: false }) + closeAlert = this.actions$.pipe( + ofType(RTLActions.CLOSE_ALERT), + map((action: RTLActions.CloseAlert) => { + this.dialogRef.close(); + } + )); + + @Effect({ dispatch: false }) + openConfirm = this.actions$.pipe( + ofType(RTLActions.OPEN_CONFIRMATION), + map((action: RTLActions.OpenConfirmation) => { + this.dialogRef = this.dialog.open(ConfirmationMessageComponent, action.payload); + }) + ); + + @Effect({ dispatch: false }) + closeConfirm = this.actions$.pipe( + ofType(RTLActions.CLOSE_CONFIRMATION), + take(1), + map((action: RTLActions.CloseConfirmation) => { + this.dialogRef.close(); + this.logger.info(action.payload); + return action.payload; + } + )); + + @Effect() + settingFetch = this.actions$.pipe( + ofType(RTLActions.FETCH_SETTINGS), + mergeMap((action: RTLActions.FetchSettings) => { + this.store.dispatch(new RTLActions.ClearEffectError('FetchSettings')); + return this.httpClient.get(environment.CONF_API + '/rtlconf'); + }), + map((rtlConfig: any) => { + this.logger.info(rtlConfig); + this.store.dispatch(new RTLActions.SetAuthSettings(rtlConfig.authSettings)); + return { + type: RTLActions.SET_SETTINGS, + payload: (undefined !== rtlConfig && undefined !== rtlConfig.settings) ? rtlConfig.settings : + {'flgSidenavOpened': true, 'flgSidenavPinned': true, 'menu': 'Vertical', 'menuType': 'Regular', 'theme': 'dark-blue', 'satsToBTC': false} + }; + }, + catchError((err) => { + this.logger.error(err); + this.store.dispatch(new RTLActions.EffectError({ action: 'FetchSettings', code: err.status, message: err.error.error })); + return of(); + }) + )); + + @Effect({ dispatch: false }) + settingSave = this.actions$.pipe( + ofType(RTLActions.SAVE_SETTINGS), + mergeMap((action: RTLActions.SaveSettings) => { + return this.httpClient.post(environment.CONF_API, { updatedSettings: action.payload }); + } + )); + + @Effect() + infoFetch = this.actions$.pipe( + ofType(RTLActions.FETCH_INFO), + withLatestFrom(this.store.select('rtlRoot')), + mergeMap(([action, store]: [RTLActions.FetchInfo, fromRTLReducer.State]) => { + this.store.dispatch(new RTLActions.ClearEffectError('FetchInfo')); + return this.httpClient.get(environment.GETINFO_API) + .pipe( + map((info) => { + this.logger.info(info); + if (undefined === info.identity_pubkey) { + sessionStorage.removeItem('lndUnlocked'); + this.logger.info('Redirecting to Unlock'); + this.router.navigate(['/unlocklnd']); + return of(); + } else { + sessionStorage.setItem('lndUnlocked', 'true'); + return { + type: RTLActions.SET_INFO, + payload: (undefined !== info) ? info : {} + }; + } + }), + catchError((err) => { + this.logger.error(err); + this.store.dispatch(new RTLActions.EffectError({ action: 'FetchInfo', code: err.status, message: err.error.error })); + if (+store.authSettings.rtlSSO) { + this.router.navigate(['/ssoerror']); + } else { + if (err.status === 401) { + this.logger.info('Redirecting to Signin'); + this.router.navigate([store.authSettings.logoutRedirectLink]); + return of(); + } else { + this.logger.info('Redirecting to Unlock'); + this.router.navigate(['/unlocklnd']); + return of(); + } + } + }) + ); + } + )); + + @Effect() + peersFetch = this.actions$.pipe( + ofType(RTLActions.FETCH_PEERS), + mergeMap((action: RTLActions.FetchPeers) => { + this.store.dispatch(new RTLActions.ClearEffectError('FetchPeers')); + return this.httpClient.get(environment.PEERS_API) + .pipe( + map((peers: any) => { + this.logger.info(peers); + return { + type: RTLActions.SET_PEERS, + payload: (undefined !== peers) ? peers : [] + }; + }), + catchError((err: any) => { + this.store.dispatch(new RTLActions.EffectError({ action: 'FetchPeers', code: err.status, message: err.error.error })); + this.logger.error(err); + return of(); + }) + ); + } + )); + + @Effect() + saveNewPeer = this.actions$.pipe( + ofType(RTLActions.SAVE_NEW_PEER), + mergeMap((action: RTLActions.SaveNewPeer) => { + return this.httpClient.post(environment.PEERS_API, {pubkey: action.payload.pubkey, host: action.payload.host, perm: action.payload.perm}) + .pipe( + map((postRes: any) => { + this.logger.info(postRes); + this.store.dispatch(new RTLActions.CloseSpinner()); + this.store.dispatch(new RTLActions.OpenAlert({ width: '70%', data: { type: 'SUCCESS', titleMessage: 'Peer Added Successfully!'}})); + return { + type: RTLActions.SET_PEERS, + payload: (undefined !== postRes && postRes.length > 0) ? postRes : [] + }; + }), + catchError((err: any) => { + this.store.dispatch(new RTLActions.CloseSpinner()); + this.logger.error(err); + return of( + { + type: RTLActions.OPEN_ALERT, + payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Add Peer Failed', + message: JSON.stringify({code: err.status, Message: err.error.error}) + }} + } + ); + }) + ); + } + )); + + @Effect() + detachPeer = this.actions$.pipe( + ofType(RTLActions.DETACH_PEER), + mergeMap((action: RTLActions.DetachPeer) => { + return this.httpClient.delete(environment.PEERS_API + '/' + action.payload.pubkey) + .pipe( + map((postRes: any) => { + this.logger.info(postRes); + this.store.dispatch(new RTLActions.CloseSpinner()); + this.store.dispatch(new RTLActions.OpenAlert({ width: '70%', data: {type: 'SUCCESS', titleMessage: 'Peer Detached Successfully!'}})); + return { + type: RTLActions.REMOVE_PEER, + payload: { pubkey: action.payload.pubkey } + }; + }), + catchError((err: any) => { + this.store.dispatch(new RTLActions.CloseSpinner()); + this.logger.error(err); + return of( + { + type: RTLActions.OPEN_ALERT, + payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Unable to Detach Peer. Try again later.', + message: JSON.stringify({code: err.status, Message: err.error.error})} + } + } + ); + }) + ); + } + )); + + @Effect() + saveNewInvoice = this.actions$.pipe( + ofType(RTLActions.SAVE_NEW_INVOICE), + mergeMap((action: RTLActions.SaveNewInvoice) => { + return this.httpClient.post(environment.INVOICES_API, {memo: action.payload.memo, amount: action.payload.invoiceValue}) + .pipe( + map((postRes: any) => { + postRes.memo = action.payload.memo; + postRes.value = action.payload.invoiceValue; + postRes.expiry = '3600'; + postRes.cltv_expiry = '144'; + postRes.creation_date = Math.round(new Date().getTime() / 1000).toString(); + postRes.creation_date_str = new Date(+postRes.creation_date * 1000).toUTCString(); + this.logger.info(postRes); + this.store.dispatch(new RTLActions.CloseSpinner()); + const msg = { payment_request: postRes.payment_request }; + this.store.dispatch(new RTLActions.OpenAlert({ width: '70%', + data: { type: 'SUCCESS', titleMessage: 'Invoice Added Successfully!', message: JSON.stringify(msg) }})); + return { + type: RTLActions.ADD_INVOICE, + payload: postRes + }; + }), + catchError((err: any) => { + this.store.dispatch(new RTLActions.CloseSpinner()); + this.logger.error(err); + return of( + { + type: RTLActions.OPEN_ALERT, + payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Add Invoice Failed', + message: JSON.stringify({code: err.status, Message: err.error.error}) + }} + } + ); + }) + ); + } + )); + + @Effect() + openNewChannel = this.actions$.pipe( + ofType(RTLActions.SAVE_NEW_CHANNEL), + mergeMap((action: RTLActions.SaveNewChannel) => { + return this.httpClient.post(environment.CHANNELS_API, { + node_pubkey: action.payload.selectedPeerPubkey, local_funding_amount: action.payload.fundingAmount, + trans_type: action.payload.transType, trans_type_value: action.payload.transTypeValue, spend_unconfirmed: action.payload.spendUnconfirmed + }) + .pipe( + map((postRes: any) => { + this.logger.info(postRes); + this.store.dispatch(new RTLActions.CloseSpinner()); + this.store.dispatch(new RTLActions.OpenAlert({ width: '70%', data: {type: 'SUCCESS', titleMessage: 'Channel Added Successfully!'}})); + this.store.dispatch(new RTLActions.FetchBalance('blockchain')); + this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'all', channelStatus: ''})); + return { + type: RTLActions.FETCH_CHANNELS, + payload: {routeParam: 'pending', channelStatus: ''} + }; + }), + catchError((err: any) => { + this.store.dispatch(new RTLActions.CloseSpinner()); + this.logger.error(err); + return of( + { + type: RTLActions.OPEN_ALERT, + payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Open Channel Failed', + message: JSON.stringify({code: err.status, Message: err.error.error}) + }} + } + ); + }) + ); + } + )); + + @Effect() + updateChannel = this.actions$.pipe( + ofType(RTLActions.UPDATE_CHANNELS), + mergeMap((action: RTLActions.UpdateChannels) => { + return this.httpClient.post(environment.CHANNELS_API + '/chanPolicy', + { baseFeeMsat: action.payload.baseFeeMsat, feeRate: action.payload.feeRate, timeLockDelta: action.payload.timeLockDelta, chanPoint: action.payload.chanPoint }) + .pipe( + map((postRes: any) => { + this.logger.info(postRes); + this.store.dispatch(new RTLActions.CloseSpinner()); + this.store.dispatch(new RTLActions.OpenAlert({ width: '70%', data: {type: 'SUCCESS', titleMessage: 'Channel Updated Successfully!'}})); + return { + type: RTLActions.FETCH_CHANNELS, + payload: {routeParam: 'all', channelStatus: ''} + }; + }), + catchError((err: any) => { + this.store.dispatch(new RTLActions.CloseSpinner()); + this.logger.error(err); + return of( + { + type: RTLActions.OPEN_ALERT, + payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Update Channel Failed', + message: JSON.stringify({code: err.status, Message: err.error.error}) + }} + } + ); + }) + ); + } + )); + + @Effect() + closeChannel = this.actions$.pipe( + ofType(RTLActions.CLOSE_CHANNEL), + mergeMap((action: RTLActions.CloseChannel) => { + return this.httpClient.delete(environment.CHANNELS_API + '/' + action.payload.channelPoint + '?force=' + action.payload.forcibly) + .pipe( + map((postRes: any) => { + this.logger.info(postRes); + this.store.dispatch(new RTLActions.CloseSpinner()); + this.store.dispatch(new RTLActions.OpenAlert({ width: '70%', data: {type: 'SUCCESS', titleMessage: 'Channel Closed Successfully!'}})); + this.store.dispatch(new RTLActions.FetchBalance('channels')); + this.store.dispatch(new RTLActions.FetchBalance('blockchain')); + this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'all', channelStatus: ''})); + if (action.payload.channelStatus) { + this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'closed', channelStatus: ''})); + } else { + this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'pending', channelStatus: ''})); + } + return { + type: RTLActions.REMOVE_CHANNEL, + payload: { channelPoint: action.payload.channelPoint } + }; + }), + catchError((err: any) => { + this.store.dispatch(new RTLActions.CloseSpinner()); + this.logger.error(err); + return of( + { + type: RTLActions.OPEN_ALERT, + payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Unable to Close Channel. Try again later.', + message: JSON.stringify({code: err.status, Message: err.error.error.message})}} + } + ); + }) + ); + } + )); + + @Effect() + fetchFees = this.actions$.pipe( + ofType(RTLActions.FETCH_FEES), + mergeMap((action: RTLActions.FetchFees) => { + this.store.dispatch(new RTLActions.ClearEffectError('FetchFees')); + return this.httpClient.get(environment.FEES_API); + }), + map((fees) => { + this.logger.info(fees); + return { + type: RTLActions.SET_FEES, + payload: (undefined !== fees) ? fees : {} + }; + }), + catchError((err: any) => { + this.logger.error(err); + this.store.dispatch(new RTLActions.EffectError({ action: 'FetchFees', code: err.status, message: err.error.error })); + return of(); + } + )); + + @Effect() + balanceFetch = this.actions$.pipe( + ofType(RTLActions.FETCH_BALANCE), + mergeMap((action: RTLActions.FetchBalance) => { + this.store.dispatch(new RTLActions.ClearEffectError('FetchBalance/' + action.payload)); + return this.httpClient.get(environment.BALANCE_API + '/' + action.payload) + .pipe( + map((res: any) => { + if (action.payload === 'channels') { + this.store.dispatch(new RTLActions.FetchBalance('blockchain')); + } + this.logger.info(res); + const emptyRes = (action.payload === 'channels') ? {balance: '', btc_balance: ''} : {total_balance: '', btc_total_balance: ''}; + return { + type: RTLActions.SET_BALANCE, + payload: (undefined !== res) ? { target: action.payload, balance: res } : { target: action.payload, balance: emptyRes } + }; + }), + catchError((err: any) => { + this.logger.error(err); + this.store.dispatch(new RTLActions.EffectError({ action: 'FetchBalance/' + action.payload, code: err.status, message: err.error.error })); + return of(); + } + )); + } + )); + + @Effect() + networkInfoFetch = this.actions$.pipe( + ofType(RTLActions.FETCH_NETWORK), + mergeMap((action: RTLActions.FetchNetwork) => { + this.store.dispatch(new RTLActions.ClearEffectError('FetchNetwork')); + return this.httpClient.get(environment.NETWORK_API + '/info'); + }), + map((networkInfo) => { + this.logger.info(networkInfo); + return { + type: RTLActions.SET_NETWORK, + payload: (undefined !== networkInfo) ? networkInfo : {} + }; + }), + catchError((err: any) => { + this.logger.error(err); + this.store.dispatch(new RTLActions.EffectError({ action: 'FetchNetwork', code: err.status, message: err.error.error })); + return of(); + } + )); + + @Effect() + channelsFetch = this.actions$.pipe( + ofType(RTLActions.FETCH_CHANNELS), + mergeMap((action: RTLActions.FetchChannels) => { + const options = + (undefined === action.payload.channelStatus || action.payload.channelStatus === '') ? {} : { params: new HttpParams().set(action.payload.channelStatus, 'true') }; + return this.httpClient.get(environment.CHANNELS_API + '/' + action.payload.routeParam, options) + .pipe( + map((channels: any) => { + this.logger.info(channels); + if (action.payload.routeParam === 'pending') { + return { + type: RTLActions.SET_PENDING_CHANNELS, + payload: (undefined !== channels) ? channels : {} + }; + } else if (action.payload.routeParam === 'closed') { + return { + type: RTLActions.SET_CLOSED_CHANNELS, + payload: (undefined !== channels && undefined !== channels.channels && channels.channels.length > 0) ? channels.channels : [] + }; + } else if (action.payload.routeParam === 'all') { + return { + type: RTLActions.SET_CHANNELS, + payload: (undefined !== channels && undefined !== channels.channels && channels.channels.length > 0) ? channels.channels : [] + }; + } + }, + catchError((err: any) => { + this.logger.error(err); + this.store.dispatch(new RTLActions.EffectError({ action: 'FetchChannels/' + action.payload.routeParam, code: err.status, message: err.error.error })); + return of(); + }) + )); + } + )); + + @Effect() + invoicesFetch = this.actions$.pipe( + ofType(RTLActions.FETCH_INVOICES), + mergeMap((action: RTLActions.FetchInvoices) => { + this.store.dispatch(new RTLActions.ClearEffectError('FetchInvoices')); + return this.httpClient.get(environment.INVOICES_API); + }), + map((res: any) => { + this.logger.info(res); + return { + type: RTLActions.SET_INVOICES, + payload: (undefined !== res && undefined !== res.invoices && res.invoices.length > 0) ? res.invoices : [] + }; + }), + catchError((err: any) => { + this.logger.error(err); + this.store.dispatch(new RTLActions.EffectError({ action: 'FetchInvoices', code: err.status, message: err.error.error })); + return of(); + } + )); + + @Effect() + transactionsFetch = this.actions$.pipe( + ofType(RTLActions.FETCH_TRANSACTIONS), + mergeMap((action: RTLActions.FetchTransactions) => { + this.store.dispatch(new RTLActions.ClearEffectError('FetchTransactions')); + return this.httpClient.get(environment.TRANSACTIONS_API); + }), + map((transactions) => { + this.logger.info(transactions); + return { + type: RTLActions.SET_TRANSACTIONS, + payload: (undefined !== transactions && transactions.length > 0) ? transactions : [] + }; + }), + catchError((err: any) => { + this.logger.error(err); + this.store.dispatch(new RTLActions.EffectError({ action: 'FetchTransactions', code: err.status, message: err.error.error })); + return of(); + } + )); + + @Effect() + paymentsFetch = this.actions$.pipe( + ofType(RTLActions.FETCH_PAYMENTS), + mergeMap((action: RTLActions.FetchPayments) => { + this.store.dispatch(new RTLActions.ClearEffectError('FetchPayments')); + return this.httpClient.get(environment.PAYMENTS_API); + }), + map((payments) => { + this.logger.info(payments); + return { + type: RTLActions.SET_PAYMENTS, + payload: (undefined !== payments) ? payments : [] + }; + }), + catchError((err: any) => { + this.logger.error(err); + this.store.dispatch(new RTLActions.EffectError({ action: 'FetchPayments', code: err.status, message: err.error.error })); + return of(); + } + )); + + @Effect() + decodePayment = this.actions$.pipe( + ofType(RTLActions.DECODE_PAYMENT), + mergeMap((action: RTLActions.DecodePayment) => { + return this.httpClient.get(environment.PAYREQUEST_API + '/' + action.payload) + .pipe( + map((decodedPayment) => { + this.logger.info(decodedPayment); + this.store.dispatch(new RTLActions.CloseSpinner()); + return { + type: RTLActions.SET_DECODED_PAYMENT, + payload: (undefined !== decodedPayment) ? decodedPayment : {} + }; + }), + catchError((err: any) => { + this.store.dispatch(new RTLActions.CloseSpinner()); + this.logger.error(err); + return of( + { + type: RTLActions.OPEN_ALERT, + payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Decode Payment Failed', + message: JSON.stringify({Code: err.status, Message: err.error.error, URL: environment.PAYREQUEST_API + '/' + action.payload})}} + } + ); + }) + ); + }) + ); + + @Effect({ dispatch: false }) + setDecodedPayment = this.actions$.pipe( + ofType(RTLActions.SET_DECODED_PAYMENT), + map((action: RTLActions.SetDecodedPayment) => { + this.logger.info(action.payload); + return action.payload; + }) + ); + + @Effect() + sendPayment = this.actions$.pipe( + ofType(RTLActions.SEND_PAYMENT), + withLatestFrom(this.store.select('rtlRoot')), + mergeMap(([action, store]: [RTLActions.SendPayment, fromRTLReducer.State]) => { + let queryHeaders = {}; + if (action.payload[2]) { + queryHeaders = {paymentDecoded: action.payload[1]}; + } else { + queryHeaders = {paymentReq: action.payload[0]}; + } + return this.httpClient.post(environment.CHANNELS_API + '/transactions', queryHeaders) + .pipe( + map((sendRes: any) => { + this.logger.info(sendRes); + this.store.dispatch(new RTLActions.CloseSpinner()); + if (sendRes.payment_error) { + this.logger.error('Error: ' + sendRes.payment_error); + return of({ + type: RTLActions.OPEN_ALERT, + payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Send Payment Failed', + message: JSON.stringify( + {code: sendRes.payment_error.status, Message: sendRes.payment_error.error.message, URL: environment.CHANNELS_API + '/transactions/' + action.payload[0]} + ) + }} + }); + } else { + const confirmationMsg = { 'Destination': action.payload[1].destination, 'Timestamp': action.payload[1].timestamp_str, 'Expiry': action.payload[1].expiry }; + confirmationMsg['Amount (' + ((undefined === store.information.smaller_currency_unit) ? + 'Sats' : store.information.smaller_currency_unit) + ')'] = action.payload[1].num_satoshis; + const msg = {}; + msg['Total Fee (' + ((undefined === store.information.smaller_currency_unit) ? 'Sats' : store.information.smaller_currency_unit) + ')'] = + (sendRes.payment_route.total_fees_msat / 1000); + Object.assign(msg, confirmationMsg); + this.store.dispatch(new RTLActions.OpenAlert({ width: '70%', + data: { type: 'SUCCESS', titleMessage: 'Payment Sent Successfully!', message: JSON.stringify(msg) }})); + this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'all', channelStatus: ''})); + this.store.dispatch(new RTLActions.FetchPayments()); + return { + type: RTLActions.SET_DECODED_PAYMENT, + payload: {} + }; + } + }), + catchError((err: any) => { + this.store.dispatch(new RTLActions.CloseSpinner()); + this.logger.error(err); + return of( + { + type: RTLActions.OPEN_ALERT, + payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Send Payment Failed', + message: JSON.stringify({code: err.status, Message: err.error.error, URL: environment.CHANNELS_API + '/transactions/' + action.payload[0]})}} + } + ); + }) + ); + }) + ); + + @Effect() + graphNodeFetch = this.actions$.pipe( + ofType(RTLActions.FETCH_GRAPH_NODE), + mergeMap((action: RTLActions.FetchGraphNode) => { + return this.httpClient.get(environment.NETWORK_API + '/node/' + action.payload) + .pipe(map((graphNode: any) => { + this.logger.info(graphNode); + this.store.dispatch(new RTLActions.CloseSpinner()); + return { + type: RTLActions.SET_GRAPH_NODE, + payload: (undefined !== graphNode) ? graphNode : {} + }; + }), + catchError((err: any) => { + this.store.dispatch(new RTLActions.CloseSpinner()); + this.logger.error(err); + return of( + { + type: RTLActions.OPEN_ALERT, + payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Get Node Address Failed', + message: JSON.stringify({Code: err.status, Message: err.error.error})}} + } + ); + })); + } + )); + + @Effect({ dispatch: false }) + setGraphNode = this.actions$.pipe( + ofType(RTLActions.SET_GRAPH_NODE), + map((action: RTLActions.SetGraphNode) => { + this.logger.info(action.payload); + return action.payload; + }) + ); + + @Effect() + getNewAddress = this.actions$.pipe( + ofType(RTLActions.GET_NEW_ADDRESS), + mergeMap((action: RTLActions.GetNewAddress) => { + return this.httpClient.get(environment.NEW_ADDRESS_API + '?type=' + action.payload.addressId) + .pipe(map((newAddress: any) => { + this.logger.info(newAddress); + this.store.dispatch(new RTLActions.CloseSpinner()); + return { + type: RTLActions.SET_NEW_ADDRESS, + payload: (undefined !== newAddress && undefined !== newAddress.address) ? newAddress.address : {} + }; + }), + catchError((err: any) => { + this.store.dispatch(new RTLActions.CloseSpinner()); + this.logger.error(err); + return of( + { + type: RTLActions.OPEN_ALERT, + payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Generate New Address Failed', + message: JSON.stringify({Code: err.status, Message: err.error.error, URL: environment.NEW_ADDRESS_API + '?type=' + action.payload.addressId})}} + } + ); + })); + }) + ); + + @Effect({ dispatch: false }) + setNewAddress = this.actions$.pipe( + ofType(RTLActions.SET_NEW_ADDRESS), + map((action: RTLActions.SetNewAddress) => { + this.logger.info(action.payload); + return action.payload; + }) + ); + + @Effect() + configFetch = this.actions$.pipe( + ofType(RTLActions.FETCH_CONFIG), + mergeMap((action: RTLActions.FetchConfig) => { + this.store.dispatch(new RTLActions.ClearEffectError('fetchConfig')); + return this.httpClient.get(environment.CONF_API + '/config/' + action.payload) + .pipe( + map((configFile: any) => { + this.store.dispatch(new RTLActions.CloseSpinner()); + return { + type: RTLActions.SHOW_CONFIG, + payload: configFile + }; + }), + catchError((err: any) => { + this.store.dispatch(new RTLActions.CloseSpinner()); + this.logger.error(err); + this.store.dispatch(new RTLActions.EffectError({ action: 'fetchConfig', code: err.status, message: err.error.error })); + return of( + { + type: RTLActions.OPEN_ALERT, + payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Fetch Config Failed!', + message: JSON.stringify({Code: err.status, Message: err.error.error})}} + } + ); + } + )); + }) + ); + + @Effect({ dispatch: false }) + showLNDConfig = this.actions$.pipe( + ofType(RTLActions.SHOW_CONFIG), + map((action: RTLActions.ShowConfig) => { + this.logger.info(action.payload); + return action.payload; + }) + ); + + @Effect() + SetChannelTransaction = this.actions$.pipe( + ofType(RTLActions.SET_CHANNEL_TRANSACTION), + mergeMap((action: RTLActions.SetChannelTransaction) => { + this.store.dispatch(new RTLActions.ClearEffectError('SetChannelTransaction')); + return this.httpClient.post(environment.TRANSACTIONS_API, + { amount: action.payload.amount, address: action.payload.address, sendAll: action.payload.sendAll, fees: action.payload.fees, blocks: action.payload.blocks } + ) + .pipe( + map((postRes: any) => { + this.logger.info(postRes); + this.store.dispatch(new RTLActions.CloseSpinner()); + return { + type: RTLActions.OPEN_ALERT, + payload: { data: {type: 'SUCCESS', titleMessage: 'Fund Sent Successfully!'} } + }; + }), + catchError((err: any) => { + this.store.dispatch(new RTLActions.CloseSpinner()); + this.store.dispatch(new RTLActions.EffectError({ action: 'SetChannelTransaction', code: err.status, message: err.error.error })); + this.logger.error(err); + return of( + { + type: RTLActions.OPEN_ALERT, + payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Sending Fund Failed', + message: JSON.stringify({Code: err.status, Message: err.error.error})}} + } + ); + })); + }) + ); + + @Effect() + fetchForwardingHistory = this.actions$.pipe( + ofType(RTLActions.GET_FORWARDING_HISTORY), + mergeMap((action: RTLActions.GetForwardingHistory) => { + this.store.dispatch(new RTLActions.ClearEffectError('GetForwardingHistory')); + const queryHeaders: SwitchReq = { + num_max_events: action.payload.num_max_events, index_offset: action.payload.index_offset, end_time: action.payload.end_time , start_time: action.payload.start_time + }; + return this.httpClient.post(environment.SWITCH_API, queryHeaders) + .pipe( + map((fhRes: any) => { + this.logger.info(fhRes); + return { + type: RTLActions.SET_FORWARDING_HISTORY, + payload: fhRes + }; + }), + catchError((err: any) => { + this.store.dispatch(new RTLActions.EffectError({ action: 'GetForwardingHistory', code: err.status, message: err.error.error })); + this.logger.error(err); + return of( + { + type: RTLActions.OPEN_ALERT, + payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Get Forwarding History Failed', + message: JSON.stringify({code: err.status, Message: err.error.error, URL: environment.SWITCH_API})}} + } + ); + }) + ); + }) + ); + + @Effect({ dispatch : false }) + operateWallet = this.actions$.pipe( + ofType(RTLActions.OPERATE_WALLET), + mergeMap((action: RTLActions.OperateWallet) => { + return this.httpClient.post(environment.WALLET_API + '/' + action.payload.operation, { wallet_password: action.payload.pwd }) + .pipe( + map((postRes) => { + this.logger.info(postRes); + this.store.dispatch(new RTLActions.CloseSpinner()); + this.store.dispatch(new RTLActions.OpenSpinner('Initializing Node...')); + this.logger.info('Successfully Unlocked!'); + sessionStorage.setItem('lndUnlocked', 'true'); + setTimeout(() => { + this.store.dispatch(new RTLActions.CloseSpinner()); + this.logger.info('Successfully Initialized!'); + this.store.dispatch(new RTLActions.InitAppData()); + this.router.navigate(['/']); + }, 1000 * 90); + return of({}); + }), + catchError((err) => { + this.store.dispatch(new RTLActions.CloseSpinner()); + this.store.dispatch(new RTLActions.OpenAlert({ width: '70%', data: {type: 'ERROR', titleMessage: err.error.error}})); + this.logger.error(err.error.error); + return of(); + }) + ); + } + )); + + @Effect() + isAuthorized = this.actions$.pipe( + ofType(RTLActions.IS_AUTHORIZED), + withLatestFrom(this.store.select('rtlRoot')), + mergeMap(([action, store]: [RTLActions.IsAuthorized, fromRTLReducer.State]) => { + this.store.dispatch(new RTLActions.ClearEffectError('IsAuthorized')); + return this.httpClient.post(environment.AUTHENTICATE_API, { password: action.payload }) + .pipe( + map((postRes: any) => { + this.logger.info(postRes); + this.logger.info('Successfully Authorized!'); + return { + type: RTLActions.IS_AUTHORIZED_RES, + payload: postRes + }; + }), + catchError((err) => { + this.store.dispatch(new RTLActions.OpenAlert({ width: '70%', data: {type: 'ERROR', titleMessage: 'Authorization Failed', + message: JSON.stringify({Code: err.status, Message: err.error.error})}})); + this.store.dispatch(new RTLActions.EffectError({ action: 'IsAuthorized', code: err.status, message: err.error.message })); + this.logger.error(err.error); + return of({ + type: RTLActions.IS_AUTHORIZED_RES, + payload: 'ERROR' + }); + }) + ); + })); + + @Effect({ dispatch: false }) + isAuthorizedRes = this.actions$.pipe( + ofType(RTLActions.IS_AUTHORIZED_RES), + map((action: RTLActions.IsAuthorizedRes) => { + return action.payload; + }) + ); + + @Effect({ dispatch: false }) + authSignin = this.actions$.pipe( + ofType(RTLActions.SIGNIN), + withLatestFrom(this.store.select('rtlRoot')), + mergeMap(([action, store]: [RTLActions.Signin, fromRTLReducer.State]) => { + this.store.dispatch(new RTLActions.ClearEffectError('Signin')); + return this.httpClient.post(environment.AUTHENTICATE_API, { password: action.payload }) + .pipe( + map((postRes: any) => { + this.logger.info(postRes); + this.logger.info('Successfully Authorized!'); + this.SetToken(postRes.token); + this.router.navigate(['/']); + }), + catchError((err) => { + this.store.dispatch(new RTLActions.OpenAlert({ width: '70%', data: {type: 'ERROR', message: JSON.stringify(err.error)}})); + this.store.dispatch(new RTLActions.EffectError({ action: 'Signin', code: err.status, message: err.error.message })); + this.logger.error(err.error); + this.logger.info('Redirecting to Signin Error Page'); + if (+store.authSettings.rtlSSO) { + this.router.navigate(['/ssoerror']); + } else { + this.router.navigate([store.authSettings.logoutRedirectLink]); + } + return of(); + }) + ); + })); + + @Effect({ dispatch: false }) + signOut = this.actions$.pipe( + ofType(RTLActions.SIGNOUT), + withLatestFrom(this.store.select('rtlRoot')), + mergeMap(([action, store]: [RTLActions.Signout, fromRTLReducer.State]) => { + if (+store.authSettings.rtlSSO) { + window.location.href = store.authSettings.logoutRedirectLink; + } else { + this.router.navigate([store.authSettings.logoutRedirectLink]); + } + sessionStorage.removeItem('lndUnlocked'); + sessionStorage.removeItem('token'); + this.logger.warn('LOGGED OUT'); + return of(); + })); + + @Effect() + peerLookup = this.actions$.pipe( + ofType(RTLActions.PEER_LOOKUP), + mergeMap((action: RTLActions.PeerLookup) => { + this.store.dispatch(new RTLActions.ClearEffectError('Lookup')); + return this.httpClient.get(environment.NETWORK_API + '/node/' + action.payload) + .pipe( + map((resPeer) => { + this.logger.info(resPeer); + this.store.dispatch(new RTLActions.CloseSpinner()); + return { + type: RTLActions.SET_LOOKUP, + payload: resPeer + }; + }), + catchError((err: any) => { + this.store.dispatch(new RTLActions.CloseSpinner()); + this.store.dispatch(new RTLActions.EffectError({ action: 'Lookup', code: err.status, message: err.error.message })); + this.logger.error(err); + return of( + { + type: RTLActions.OPEN_ALERT, + payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Peer Lookup Failed', + message: JSON.stringify({Code: err.status, Message: err.error.error, URL: environment.NETWORK_API + '/node/' + action.payload})}} + } + ); + }) + ); + }) + ); + + @Effect() + channelLookup = this.actions$.pipe( + ofType(RTLActions.CHANNEL_LOOKUP), + mergeMap((action: RTLActions.ChannelLookup) => { + this.store.dispatch(new RTLActions.ClearEffectError('Lookup')); + return this.httpClient.get(environment.NETWORK_API + '/edge/' + action.payload) + .pipe( + map((resChannel) => { + this.logger.info(resChannel); + this.store.dispatch(new RTLActions.CloseSpinner()); + return { + type: RTLActions.SET_LOOKUP, + payload: resChannel + }; + }), + catchError((err: any) => { + this.store.dispatch(new RTLActions.CloseSpinner()); + this.store.dispatch(new RTLActions.EffectError({ action: 'Lookup', code: err.status, message: err.error.message })); + this.logger.error(err); + return of( + { + type: RTLActions.OPEN_ALERT, + payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Channel Lookup Failed', + message: JSON.stringify({Code: err.status, Message: err.error.error, URL: environment.NETWORK_API + '/edge/' + action.payload})}} + } + ); + }) + ); + }) + ); + + @Effect() + invoiceLookup = this.actions$.pipe( + ofType(RTLActions.INVOICE_LOOKUP), + mergeMap((action: RTLActions.InvoiceLookup) => { + this.store.dispatch(new RTLActions.ClearEffectError('Lookup')); + return this.httpClient.get(environment.INVOICES_API + '/' + action.payload) + .pipe( + map((resInvoice) => { + this.logger.info(resInvoice); + this.store.dispatch(new RTLActions.CloseSpinner()); + return { + type: RTLActions.SET_LOOKUP, + payload: resInvoice + }; + }), + catchError((err: any) => { + this.store.dispatch(new RTLActions.CloseSpinner()); + this.store.dispatch(new RTLActions.EffectError({ action: 'Lookup', code: err.status, message: err.error.message })); + this.logger.error(err); + return of( + { + type: RTLActions.OPEN_ALERT, + payload: { width: '70%', data: {type: 'ERROR', titleMessage: 'Invoice Lookup Failed', + message: JSON.stringify({Code: err.status, Message: err.error.error, URL: environment.INVOICES_API + '/' + action.payload})}} + } + ); + }) + ); + }) + ); + + @Effect({ dispatch: false }) + setLookup = this.actions$.pipe( + ofType(RTLActions.SET_LOOKUP), + map((action: RTLActions.SetLookup) => { + this.logger.info(action.payload); + return action.payload; + }) + ); + + SetToken(token: string) { + if (token) { + sessionStorage.setItem('lndUnlocked', 'true'); + sessionStorage.setItem('token', token); + this.store.dispatch(new RTLActions.InitAppData()); + } else { + sessionStorage.removeItem('lndUnlocked'); + sessionStorage.removeItem('token'); + } + } + + ngOnDestroy() { + this.unSubs.forEach(completeSub => { + completeSub.next(); + completeSub.complete(); + }); + } + +} diff --git a/src/app/shared/store/rtl.reducers.ts b/src/app/shared/store/rtl.reducers.ts new file mode 100644 index 00000000..3b0b2692 --- /dev/null +++ b/src/app/shared/store/rtl.reducers.ts @@ -0,0 +1,265 @@ +import * as RTLActions from './rtl.actions'; + +import { ErrorPayload } from '../models/errorPayload'; +import { Settings, Authentication } from '../models/RTLconfig'; +import { + GetInfo, GetInfoChain, Peer, AddressType, Fees, NetworkInfo, Balance, Channel, Payment, Invoice, PendingChannels, ClosedChannel, Transaction, SwitchRes +} from '../models/lndModels'; + +export interface State { + effectErrors: ErrorPayload[]; + settings: Settings; + authSettings: Authentication; + information: GetInfo; + peers: Peer[]; + fees: Fees; + networkInfo: NetworkInfo; + channelBalance: Balance; + blockchainBalance: Balance; + allChannels: Channel[]; + closedChannels: ClosedChannel[]; + pendingChannels: PendingChannels; + numberOfActiveChannels: number; + numberOfInactiveChannels: number; + numberOfPendingChannels: number; + totalLocalBalance: number; + totalRemoteBalance: number; + transactions: Transaction[]; + payments: Payment[]; + invoices: Invoice[]; + forwardingHistory: SwitchRes; + addressTypes: AddressType[]; +} + +const initialState: State = { + effectErrors: [], + settings: {flgSidenavOpened: true, flgSidenavPinned: true, menu: 'Vertical', menuType: 'Regular', theme: 'dark-blue', satsToBTC: false}, + authSettings: {nodeAuthType: 'CUSTOM', lndConfigPath: '', bitcoindConfigPath: '', rtlSSO: 0, logoutRedirectLink: '/login' }, + information: {}, + peers: [], + fees: {}, + networkInfo: {}, + channelBalance: {balance: '', btc_balance: ''}, + blockchainBalance: { total_balance: '', btc_total_balance: ''}, + allChannels: [], + closedChannels: [], + pendingChannels: {}, + numberOfActiveChannels: 0, + numberOfInactiveChannels: 0, + numberOfPendingChannels: -1, + totalLocalBalance: -1, + totalRemoteBalance: -1, + transactions: [], + payments: [], + invoices: [], + forwardingHistory: {}, + addressTypes: [ + { addressId: '0', addressTp: 'p2wkh', addressDetails: 'Pay to witness key hash'}, + { addressId: '1', addressTp: 'np2wkh', addressDetails: 'Pay to nested witness key hash (default)'} + ] +}; + +export function RTLRootReducer(state = initialState, action: RTLActions.RTLActions) { + switch (action.type) { + case RTLActions.CLEAR_EFFECT_ERROR: + const clearedEffectErrors = [...state.effectErrors]; + const removeEffectIdx = state.effectErrors.findIndex(err => { + return err.action === action.payload; + }); + if (removeEffectIdx > -1) { + clearedEffectErrors.splice(removeEffectIdx, 1); + } + return { + ...state, + effectErrors: clearedEffectErrors + }; + case RTLActions.EFFECT_ERROR: + return { + ...state, + effectErrors: [...state.effectErrors, action.payload] + }; + case RTLActions.SET_SETTINGS: + return { + ...state, + settings: action.payload + }; + case RTLActions.SET_AUTH_SETTINGS: + return { + ...state, + authSettings: action.payload + }; + case RTLActions.SET_INFO: + if (undefined !== action.payload.chains) { + if (typeof action.payload.chains[0] === 'string') { + action.payload.smaller_currency_unit = (action.payload.chains[0].toString().toLowerCase().indexOf('bitcoin') < 0) ? 'Litoshis' : 'Sats'; + action.payload.currency_unit = (action.payload.chains[0].toString().toLowerCase().indexOf('bitcoin') < 0) ? 'LTC' : 'BTC'; + } else if (typeof action.payload.chains[0] === 'object' && action.payload.chains[0].hasOwnProperty('chain')) { + const getInfoChain = action.payload.chains[0]; + action.payload.smaller_currency_unit = (getInfoChain.chain.toLowerCase().indexOf('bitcoin') < 0) ? 'Litoshis' : 'Sats'; + action.payload.currency_unit = (getInfoChain.chain.toLowerCase().indexOf('bitcoin') < 0) ? 'LTC' : 'BTC'; + } + action.payload.version = (undefined === action.payload.version) ? '' : action.payload.version.split(' ')[0]; + } else { + action.payload.smaller_currency_unit = 'Sats'; + action.payload.currency_unit = 'BTC'; + action.payload.version = (undefined === action.payload.version) ? '' : action.payload.version.split(' ')[0]; + } + return { + ...state, + information: action.payload + }; + case RTLActions.SET_PEERS: + return { + ...state, + peers: action.payload + }; + case RTLActions.ADD_PEER: + return { + ...state, + peers: [...state.peers, action.payload] + }; + case RTLActions.REMOVE_PEER: + const modifiedPeers = [...state.peers]; + const removePeerIdx = state.peers.findIndex(peer => { + return peer.pub_key === action.payload.pubkey; + }); + if (removePeerIdx > -1) { + modifiedPeers.splice(removePeerIdx, 1); + } + return { + ...state, + peers: modifiedPeers + }; + case RTLActions.ADD_INVOICE: + return { + ...state, + invoices: [action.payload, ...state.invoices] + }; + case RTLActions.SET_FEES: + return { + ...state, + fees: action.payload + }; + case RTLActions.SET_CLOSED_CHANNELS: + return { + ...state, + closedChannels: action.payload, + }; + case RTLActions.SET_PENDING_CHANNELS: + let pendingChannels = -1; + if (action.payload) { + pendingChannels = 0; + if (action.payload.pending_closing_channels) { + pendingChannels = pendingChannels + action.payload.pending_closing_channels.length; + } + if (action.payload.pending_force_closing_channels) { + pendingChannels = pendingChannels + action.payload.pending_force_closing_channels.length; + } + if (action.payload.pending_open_channels) { + pendingChannels = pendingChannels + action.payload.pending_open_channels.length; + } + if (action.payload.waiting_close_channels) { + pendingChannels = pendingChannels + action.payload.waiting_close_channels.length; + } + } + return { + ...state, + pendingChannels: action.payload, + numberOfPendingChannels: pendingChannels, + }; + case RTLActions.SET_CHANNELS: + let localBal = 0, remoteBal = 0, activeChannels = 0, inactiveChannels = 0; + if (action.payload) { + action.payload.filter(channel => { + if (undefined !== channel.local_balance) { + localBal = +localBal + +channel.local_balance; + } + if (undefined !== channel.remote_balance) { + remoteBal = +remoteBal + +channel.remote_balance; + } + if (channel.active === true) { + activeChannels = activeChannels + 1; + } else { + inactiveChannels = inactiveChannels + 1; + } + }); + } + return { + ...state, + allChannels: action.payload, + numberOfActiveChannels: activeChannels, + numberOfInactiveChannels: inactiveChannels, + totalLocalBalance: localBal, + totalRemoteBalance: remoteBal + }; + case RTLActions.REMOVE_CHANNEL: + const modifiedChannels = [...state.allChannels]; + const removeChannelIdx = state.allChannels.findIndex(channel => { + return channel.channel_point === action.payload.channelPoint; + }); + if (removeChannelIdx > -1) { + modifiedChannels.splice(removeChannelIdx, 1); + } + return { + ...state, + allChannels: modifiedChannels + }; + case RTLActions.SET_BALANCE: + if (action.payload.target === 'channels') { + return { + ...state, + channelBalance: action.payload.balance + }; + } else { + return { + ...state, + blockchainBalance: action.payload.balance + }; + } + case RTLActions.SET_NETWORK: + return { + ...state, + networkInfo: action.payload + }; + case RTLActions.SET_INVOICES: + return { + ...state, + invoices: action.payload + }; + case RTLActions.SET_TRANSACTIONS: + return { + ...state, + transactions: action.payload + }; + case RTLActions.SET_PAYMENTS: + return { + ...state, + payments: action.payload + }; + case RTLActions.SET_FORWARDING_HISTORY: + if (action.payload.forwarding_events) { + const storedChannels = [...state.allChannels]; + action.payload.forwarding_events.forEach(event => { + if (storedChannels) { + for (let idx = 0; idx < storedChannels.length; idx++) { + if (storedChannels[idx].chan_id.toString() === event.chan_id_in) { + event.alias_in = storedChannels[idx].remote_alias; + if (event.alias_out) { return; } + } + if (storedChannels[idx].chan_id.toString() === event.chan_id_out) { + event.alias_out = storedChannels[idx].remote_alias; + if (event.alias_in) { return; } + } + } + } + }); + } + return { + ...state, + forwardingHistory: action.payload + }; + default: + return state; + } + +} diff --git a/src/app/shared/theme/overlay-container/theme-overlay.ts b/src/app/shared/theme/overlay-container/theme-overlay.ts new file mode 100644 index 00000000..f264e406 --- /dev/null +++ b/src/app/shared/theme/overlay-container/theme-overlay.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@angular/core'; +import { OverlayContainer } from '@angular/cdk/overlay'; + +@Injectable() +export class ThemeOverlay extends OverlayContainer { + _createContainer(): void { + const container = document.createElement('div'); + container.classList.add('cdk-overlay-container'); + document.getElementById('rtl-container').appendChild(container); + this._containerElement = container; + } +} diff --git a/src/app/shared/theme/skins/bluegray-amber.scss b/src/app/shared/theme/skins/bluegray-amber.scss new file mode 100644 index 00000000..5f483866 --- /dev/null +++ b/src/app/shared/theme/skins/bluegray-amber.scss @@ -0,0 +1,50 @@ +$custom-dark-theme-background: ( + status-bar: map_get($mat-blue-gray, 800), + app-bar: map_get($mat-blue-gray, 700), + background: map_get($mat-blue-gray, 100), + hover: rgba(map_get($mat-grey, 900), 0.04), + card: map_get($mat-blue-gray, 200), + dialog: map_get($mat-blue-gray, 200), + disabled-button: rgba(map_get($mat-grey, 900), 0.12), + raised-button: map-get($mat-blue-gray, 600), + focused-button: $light-focused, + selected-button: map_get($mat-grey, 800), + selected-disabled-button: map_get($mat-grey, 600), + disabled-button-toggle: map_get($mat-grey, 900), + unselected-chip: map_get($mat-grey, 700), + disabled-list-option: map_get($mat-grey, 900) +); + +$custom-dark-theme-foreground: ( + base: black, + divider: $dark-dividers, + dividers: $dark-dividers, + disabled: $dark-disabled-text, + disabled-button: rgba(black, 0.26), + disabled-text: $dark-disabled-text, + hint-text: $dark-disabled-text, + secondary-text: $dark-secondary-text, + icon: rgba(black, 0.54), + icons: rgba(black, 0.54), + text: rgba(black, 0.87), + slider-min: rgba(black, 0.87), + slider-off: rgba(black, 0.26), + slider-off-active: rgba(black, 0.38), +); + +@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) { + @return ( + primary: $primary, + accent: $accent, + warn: $warn, + is-dark: false, + foreground: $custom-dark-theme-foreground, + background: $custom-dark-theme-background + ); +} + +$amber-bluegray-primary: mat-palette($mat-amber, 500, 100, 700); +$amber-bluegray-accent: mat-palette($mat-purple); +$amber-bluegray-warn: mat-palette($mat-red, A700); + +$amber-bluegray-theme: create-custom-theme($amber-bluegray-primary, $amber-bluegray-accent, $amber-bluegray-warn); diff --git a/src/app/shared/theme/skins/bluegray-deeppurple.scss b/src/app/shared/theme/skins/bluegray-deeppurple.scss new file mode 100644 index 00000000..b9c7521f --- /dev/null +++ b/src/app/shared/theme/skins/bluegray-deeppurple.scss @@ -0,0 +1,50 @@ +$custom-dark-theme-background: ( + status-bar: map_get($mat-blue-gray, 800), + app-bar: map_get($mat-blue-gray, 700), + background: map_get($mat-blue-gray, 100), + hover: rgba(map_get($mat-grey, 900), 0.04), + card: map_get($mat-blue-gray, 200), + dialog: map_get($mat-blue-gray, 200), + disabled-button: rgba(map_get($mat-grey, 900), 0.12), + raised-button: map-get($mat-blue-gray, 600), + focused-button: $light-focused, + selected-button: map_get($mat-grey, 800), + selected-disabled-button: map_get($mat-grey, 600), + disabled-button-toggle: map_get($mat-grey, 900), + unselected-chip: map_get($mat-grey, 700), + disabled-list-option: map_get($mat-grey, 900) +); + +$custom-dark-theme-foreground: ( + base: black, + divider: $dark-dividers, + dividers: $dark-dividers, + disabled: $dark-disabled-text, + disabled-button: rgba(black, 0.26), + disabled-text: $dark-disabled-text, + hint-text: $dark-disabled-text, + secondary-text: $dark-secondary-text, + icon: rgba(black, 0.54), + icons: rgba(black, 0.54), + text: rgba(black, 0.87), + slider-min: rgba(black, 0.87), + slider-off: rgba(black, 0.26), + slider-off-active: rgba(black, 0.38), +); + +@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) { + @return ( + primary: $primary, + accent: $accent, + warn: $warn, + is-dark: false, + foreground: $custom-dark-theme-foreground, + background: $custom-dark-theme-background + ); +} + +$deeppurple-bluegray-primary: mat-palette($mat-deep-purple, 600, 100, 900); +$deeppurple-bluegray-accent: mat-palette($mat-pink); +$deeppurple-bluegray-warn: mat-palette($mat-red, A700); + +$deeppurple-bluegray-theme: create-custom-theme($deeppurple-bluegray-primary, $deeppurple-bluegray-accent, $deeppurple-bluegray-warn); diff --git a/src/app/shared/theme/skins/bluegray-lightgreen.scss b/src/app/shared/theme/skins/bluegray-lightgreen.scss new file mode 100644 index 00000000..5209daf2 --- /dev/null +++ b/src/app/shared/theme/skins/bluegray-lightgreen.scss @@ -0,0 +1,50 @@ +$custom-dark-theme-background: ( + status-bar: map_get($mat-blue-gray, 800), + app-bar: map_get($mat-blue-gray, 700), + background: map_get($mat-blue-gray, 100), + hover: rgba(map_get($mat-grey, 900), 0.04), + card: map_get($mat-blue-gray, 200), + dialog: map_get($mat-blue-gray, 200), + disabled-button: rgba(map_get($mat-grey, 900), 0.12), + raised-button: map-get($mat-blue-gray, 600), + focused-button: $light-focused, + selected-button: map_get($mat-grey, 800), + selected-disabled-button: map_get($mat-grey, 600), + disabled-button-toggle: map_get($mat-grey, 900), + unselected-chip: map_get($mat-grey, 700), + disabled-list-option: map_get($mat-grey, 900) +); + +$custom-dark-theme-foreground: ( + base: black, + divider: $dark-dividers, + dividers: $dark-dividers, + disabled: $dark-disabled-text, + disabled-button: rgba(black, 0.26), + disabled-text: $dark-disabled-text, + hint-text: $dark-disabled-text, + secondary-text: $dark-secondary-text, + icon: rgba(black, 0.54), + icons: rgba(black, 0.54), + text: rgba(black, 0.87), + slider-min: rgba(black, 0.87), + slider-off: rgba(black, 0.26), + slider-off-active: rgba(black, 0.38), +); + +@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) { + @return ( + primary: $primary, + accent: $accent, + warn: $warn, + is-dark: false, + foreground: $custom-dark-theme-foreground, + background: $custom-dark-theme-background + ); +} + +$lightgreen-bluegray-primary: mat-palette($mat-light-green, 700, 100, 900); +$lightgreen-bluegray-accent: mat-palette($mat-deep-orange); +$lightgreen-bluegray-warn: mat-palette($mat-red, A700); + +$lightgreen-bluegray-theme: create-custom-theme($lightgreen-bluegray-primary, $lightgreen-bluegray-accent, $lightgreen-bluegray-warn); diff --git a/src/app/shared/theme/skins/dark-blue.scss b/src/app/shared/theme/skins/dark-blue.scss new file mode 100644 index 00000000..1e7e5405 --- /dev/null +++ b/src/app/shared/theme/skins/dark-blue.scss @@ -0,0 +1,5 @@ +$blue-dark-primary: mat-palette($mat-light-blue, 700, 300, 900); +$blue-dark-accent: mat-palette($mat-amber, A100, 900, A700); +$blue-dark-warn: mat-palette($mat-red, A200); + +$blue-dark-theme: mat-dark-theme($blue-dark-primary, $blue-dark-accent, $blue-dark-warn); \ No newline at end of file diff --git a/src/app/shared/theme/skins/dark-green.scss b/src/app/shared/theme/skins/dark-green.scss new file mode 100644 index 00000000..e6e02e57 --- /dev/null +++ b/src/app/shared/theme/skins/dark-green.scss @@ -0,0 +1,5 @@ +$green-dark-primary: mat-palette($mat-green, 700, 600, 900); +$green-dark-accent: mat-palette($mat-amber, A100, 900, A700); +$green-dark-warn: mat-palette($mat-red, A200); + +$green-dark-theme: mat-dark-theme($green-dark-primary, $green-dark-accent, $green-dark-warn); \ No newline at end of file diff --git a/src/app/shared/theme/skins/dark-pink.scss b/src/app/shared/theme/skins/dark-pink.scss new file mode 100644 index 00000000..83e7c513 --- /dev/null +++ b/src/app/shared/theme/skins/dark-pink.scss @@ -0,0 +1,5 @@ +$pink-dark-primary: mat-palette($mat-pink, 700, 400, 900); +$pink-dark-accent: mat-palette($mat-blue-grey, 700, 500, 900); +$pink-dark-warn: mat-palette($mat-red, A200); + +$pink-dark-theme: mat-dark-theme($pink-dark-primary, $pink-dark-accent, $pink-dark-warn); \ No newline at end of file diff --git a/src/app/shared/theme/skins/gray-blue.scss b/src/app/shared/theme/skins/gray-blue.scss new file mode 100644 index 00000000..a8c9aeb3 --- /dev/null +++ b/src/app/shared/theme/skins/gray-blue.scss @@ -0,0 +1,50 @@ +$custom-dark-theme-background: ( + status-bar: map_get($mat-gray, 800), + app-bar: map_get($mat-gray, 700), + background: map_get($mat-gray, 100), + hover: rgba(map_get($mat-grey, 900), 0.04), + card: map_get($mat-gray, 200), + dialog: map_get($mat-gray, 200), + disabled-button: rgba(map_get($mat-grey, 900), 0.12), + raised-button: map-get($mat-gray, 600), + focused-button: $light-focused, + selected-button: map_get($mat-grey, 800), + selected-disabled-button: map_get($mat-grey, 600), + disabled-button-toggle: map_get($mat-grey, 900), + unselected-chip: map_get($mat-grey, 700), + disabled-list-option: map_get($mat-grey, 900) +); + +$custom-dark-theme-foreground: ( + base: black, + divider: $dark-dividers, + dividers: $dark-dividers, + disabled: $dark-disabled-text, + disabled-button: rgba(black, 0.26), + disabled-text: $dark-disabled-text, + hint-text: $dark-disabled-text, + secondary-text: $dark-secondary-text, + icon: rgba(black, 0.54), + icons: rgba(black, 0.54), + text: rgba(black, 0.87), + slider-min: rgba(black, 0.87), + slider-off: rgba(black, 0.26), + slider-off-active: rgba(black, 0.38), +); + +@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) { + @return ( + primary: $primary, + accent: $accent, + warn: $warn, + is-dark: false, + foreground: $custom-dark-theme-foreground, + background: $custom-dark-theme-background + ); +} + +$blue-gray-primary: mat-palette($mat-cyan, 500, 200, 900); +$blue-gray-accent: mat-palette($mat-deep-orange); +$blue-gray-warn: mat-palette($mat-red, A700); + +$blue-gray-theme: create-custom-theme($blue-gray-primary, $blue-gray-accent, $blue-gray-warn); diff --git a/src/app/shared/theme/skins/gray-lime.scss b/src/app/shared/theme/skins/gray-lime.scss new file mode 100644 index 00000000..eeba6cd4 --- /dev/null +++ b/src/app/shared/theme/skins/gray-lime.scss @@ -0,0 +1,50 @@ +$custom-dark-theme-background: ( + status-bar: map_get($mat-gray, 800), + app-bar: map_get($mat-gray, 700), + background: map_get($mat-gray, 100), + hover: rgba(map_get($mat-grey, 900), 0.04), + card: map_get($mat-gray, 200), + dialog: map_get($mat-gray, 200), + disabled-button: rgba(map_get($mat-grey, 900), 0.12), + raised-button: map-get($mat-gray, 600), + focused-button: $light-focused, + selected-button: map_get($mat-grey, 800), + selected-disabled-button: map_get($mat-grey, 600), + disabled-button-toggle: map_get($mat-grey, 900), + unselected-chip: map_get($mat-grey, 700), + disabled-list-option: map_get($mat-grey, 900) +); + +$custom-dark-theme-foreground: ( + base: black, + divider: $dark-dividers, + dividers: $dark-dividers, + disabled: $dark-disabled-text, + disabled-button: rgba(black, 0.26), + disabled-text: $dark-disabled-text, + hint-text: $dark-disabled-text, + secondary-text: $dark-secondary-text, + icon: rgba(black, 0.54), + icons: rgba(black, 0.54), + text: rgba(black, 0.87), + slider-min: rgba(black, 0.87), + slider-off: rgba(black, 0.26), + slider-off-active: rgba(black, 0.38), +); + +@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) { + @return ( + primary: $primary, + accent: $accent, + warn: $warn, + is-dark: false, + foreground: $custom-dark-theme-foreground, + background: $custom-dark-theme-background + ); +} + +$lime-gray-primary: mat-palette($mat-lime, 300, 100, 700); +$lime-gray-accent: mat-palette($mat-purple); +$lime-gray-warn: mat-palette($mat-red, A700); + +$lime-gray-theme: create-custom-theme($lime-gray-primary, $lime-gray-accent, $lime-gray-warn); diff --git a/src/app/shared/theme/skins/gray-purple.scss b/src/app/shared/theme/skins/gray-purple.scss new file mode 100644 index 00000000..a66143d8 --- /dev/null +++ b/src/app/shared/theme/skins/gray-purple.scss @@ -0,0 +1,50 @@ +$custom-dark-theme-background: ( + status-bar: map_get($mat-gray, 800), + app-bar: map_get($mat-gray, 700), + background: map_get($mat-gray, 100), + hover: rgba(map_get($mat-grey, 900), 0.04), + card: map_get($mat-gray, 200), + dialog: map_get($mat-gray, 200), + disabled-button: rgba(map_get($mat-grey, 900), 0.12), + raised-button: map-get($mat-gray, 600), + focused-button: $light-focused, + selected-button: map_get($mat-grey, 800), + selected-disabled-button: map_get($mat-grey, 600), + disabled-button-toggle: map_get($mat-grey, 900), + unselected-chip: map_get($mat-grey, 700), + disabled-list-option: map_get($mat-grey, 900) +); + +$custom-dark-theme-foreground: ( + base: black, + divider: $dark-dividers, + dividers: $dark-dividers, + disabled: $dark-disabled-text, + disabled-button: rgba(black, 0.26), + disabled-text: $dark-disabled-text, + hint-text: $dark-disabled-text, + secondary-text: $dark-secondary-text, + icon: rgba(black, 0.54), + icons: rgba(black, 0.54), + text: rgba(black, 0.87), + slider-min: rgba(black, 0.87), + slider-off: rgba(black, 0.26), + slider-off-active: rgba(black, 0.38), +); + +@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) { + @return ( + primary: $primary, + accent: $accent, + warn: $warn, + is-dark: false, + foreground: $custom-dark-theme-foreground, + background: $custom-dark-theme-background + ); +} + +$purple-gray-primary: mat-palette($mat-purple, 700, 100, 900); +$purple-gray-accent: mat-palette($mat-amber); +$purple-gray-warn: mat-palette($mat-red, A700); + +$purple-gray-theme: create-custom-theme($purple-gray-primary, $purple-gray-accent, $purple-gray-warn); diff --git a/src/app/shared/theme/skins/light-blue.scss b/src/app/shared/theme/skins/light-blue.scss new file mode 100644 index 00000000..3834dae3 --- /dev/null +++ b/src/app/shared/theme/skins/light-blue.scss @@ -0,0 +1,50 @@ +$custom-light-theme-background: ( + status-bar: map_get($mat-grey, 300), + app-bar: map_get($mat-grey, 100), + background:map_get($mat-gray, 100), + hover: rgba(black, 0.04), + card: map_get($mat-gray, 100), + dialog: map_get($mat-gray, 100), + disabled-button: rgba(black, 0.12), + raised-button: map_get($mat-grey, 50), + focused-button: $dark-focused, + selected-button: map_get($mat-grey, 300), + selected-disabled-button: map_get($mat-grey, 400), + disabled-button-toggle: map_get($mat-grey, 200), + unselected-chip: map_get($mat-grey, 300), + disabled-list-option: map_get($mat-grey, 200), +); + +$custom-light-theme-foreground: ( + base: black, + divider: $dark-dividers, + dividers: $dark-dividers, + disabled: $dark-disabled-text, + disabled-button: rgba(black, 0.26), + disabled-text: $dark-disabled-text, + hint-text: $dark-disabled-text, + secondary-text: $dark-secondary-text, + icon: rgba(black, 0.54), + icons: rgba(black, 0.54), + text: rgba(black, 0.87), + slider-min: rgba(black, 0.87), + slider-off: rgba(black, 0.26), + slider-off-active: rgba(black, 0.38), +); + +@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) { + @return ( + primary: $primary, + accent: $accent, + warn: $warn, + is-dark: false, + foreground: $custom-light-theme-foreground, + background: $custom-light-theme-background + ); +} + +$blue-light-primary: mat-palette($mat-indigo, 800, 300, 900); +$blue-light-accent: mat-palette($mat-pink, A200, A100, A400); +$blue-light-warn: mat-palette($mat-red, 500); + +$blue-light-theme: create-custom-theme($blue-light-primary, $blue-light-accent, $blue-light-warn); diff --git a/src/app/shared/theme/skins/light-red.scss b/src/app/shared/theme/skins/light-red.scss new file mode 100644 index 00000000..ee2a47e1 --- /dev/null +++ b/src/app/shared/theme/skins/light-red.scss @@ -0,0 +1,50 @@ +$custom-light-theme-background: ( + status-bar: map_get($mat-grey, 300), + app-bar: map_get($mat-grey, 100), + background:map_get($mat-gray, 100), + hover: rgba(black, 0.04), + card: map_get($mat-gray, 100), + dialog: map_get($mat-gray, 100), + disabled-button: rgba(black, 0.12), + raised-button: map_get($mat-grey, 50), + focused-button: $dark-focused, + selected-button: map_get($mat-grey, 300), + selected-disabled-button: map_get($mat-grey, 400), + disabled-button-toggle: map_get($mat-grey, 200), + unselected-chip: map_get($mat-grey, 300), + disabled-list-option: map_get($mat-grey, 200), +); + +$custom-light-theme-foreground: ( + base: black, + divider: $dark-dividers, + dividers: $dark-dividers, + disabled: $dark-disabled-text, + disabled-button: rgba(black, 0.26), + disabled-text: $dark-disabled-text, + hint-text: $dark-disabled-text, + secondary-text: $dark-secondary-text, + icon: rgba(black, 0.54), + icons: rgba(black, 0.54), + text: rgba(black, 0.87), + slider-min: rgba(black, 0.87), + slider-off: rgba(black, 0.26), + slider-off-active: rgba(black, 0.38), +); + +@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) { + @return ( + primary: $primary, + accent: $accent, + warn: $warn, + is-dark: false, + foreground: $custom-light-theme-foreground, + background: $custom-light-theme-background + ); +} + +$red-light-primary: mat-palette($mat-red, 800, 300, 900); +$red-light-accent: mat-palette($mat-green, 600, 300, 900); +$red-light-warn: mat-palette($mat-deep-orange, A400); + +$red-light-theme: create-custom-theme($red-light-primary, $red-light-accent, $red-light-warn); \ No newline at end of file diff --git a/src/app/shared/theme/skins/light-teal.scss b/src/app/shared/theme/skins/light-teal.scss new file mode 100644 index 00000000..30c9a6a6 --- /dev/null +++ b/src/app/shared/theme/skins/light-teal.scss @@ -0,0 +1,50 @@ +$custom-light-theme-background: ( + status-bar: map_get($mat-grey, 300), + app-bar: map_get($mat-grey, 100), + background:map_get($mat-gray, 100), + hover: rgba(black, 0.04), + card: map_get($mat-gray, 100), + dialog: map_get($mat-gray, 100), + disabled-button: rgba(black, 0.12), + raised-button: map_get($mat-grey, 50), + focused-button: $dark-focused, + selected-button: map_get($mat-grey, 300), + selected-disabled-button: map_get($mat-grey, 400), + disabled-button-toggle: map_get($mat-grey, 200), + unselected-chip: map_get($mat-grey, 300), + disabled-list-option: map_get($mat-grey, 200), +); + +$custom-light-theme-foreground: ( + base: black, + divider: $dark-dividers, + dividers: $dark-dividers, + disabled: $dark-disabled-text, + disabled-button: rgba(black, 0.26), + disabled-text: $dark-disabled-text, + hint-text: $dark-disabled-text, + secondary-text: $dark-secondary-text, + icon: rgba(black, 0.54), + icons: rgba(black, 0.54), + text: rgba(black, 0.87), + slider-min: rgba(black, 0.87), + slider-off: rgba(black, 0.26), + slider-off-active: rgba(black, 0.38), +); + +@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) { + @return ( + primary: $primary, + accent: $accent, + warn: $warn, + is-dark: false, + foreground: $custom-light-theme-foreground, + background: $custom-light-theme-background + ); +} + +$teal-light-primary: mat-palette($mat-teal, 800, 300, 900); +$teal-light-accent: mat-palette($mat-amber, A200, A100, A700); +$teal-light-warn: mat-palette($mat-red, A200); + +$teal-light-theme: create-custom-theme($teal-light-primary, $teal-light-accent, $teal-light-warn); \ No newline at end of file diff --git a/src/app/shared/theme/skins/self-blue.scss b/src/app/shared/theme/skins/self-blue.scss new file mode 100644 index 00000000..4d236ad7 --- /dev/null +++ b/src/app/shared/theme/skins/self-blue.scss @@ -0,0 +1,83 @@ +$custom-blue: ( + 50 : #f3f8fc, + 100 : #e2eef8, + 200 : #cfe2f4, + 300 : #bcd6ef, + 400 : #adceeb, + 500 : #9fc5e8, + 600 : #97bfe5, + 700 : #8db8e2, + 800 : #83b0de, + 900 : #72a3d8, + A100 : #ffffff, + A200 : #ffffff, + A400 : #eef6ff, + A700 : #d4e9ff, + contrast: ( + 50 : #ECEFF1, + 100 : #CFD8DC, + 200 : #37474F, + 300 : #37474F, + 400 : #37474F, + 500 : #607D8B, + 600 : #546E7A, + 700 : #455A64, + 800 : #37474F, + 900 : #263238, + A100 : #000000, + A200 : #000000, + A400 : #000000, + A700 : #000000, + ) +); + +$custom-light-theme-background: ( + status-bar: map_get($mat-blue-gray, 300), + app-bar: map_get($mat-blue-gray, 300), + background: map_get($custom-blue, 50), + hover: rgba(black, 0.04), + card: map_get($custom-blue, 100), + dialog: map_get($custom-blue, 100), + disabled-button: rgba(black, 0.12), + raised-button: map_get($mat-blue-gray, 100), + focused-button: $dark-focused, + selected-button: map_get($mat-blue-gray, 300), + selected-disabled-button: map_get($mat-blue-gray, 400), + disabled-button-toggle: map_get($mat-blue-gray, 200), + unselected-chip: map_get($mat-blue-gray, 300), + disabled-list-option: map_get($mat-blue-gray, 200), +); + +$custom-light-theme-foreground: ( + base: map_get($mat-blue-gray, 900), + divider: $dark-dividers, + dividers: $dark-dividers, + disabled: $dark-disabled-text, + disabled-button: rgba(map_get($mat-blue-gray, 900), 0.26), + disabled-text: $dark-disabled-text, + hint-text: $dark-disabled-text, + secondary-text: $dark-secondary-text, + icon: rgba(map_get($mat-blue-gray, 900), 0.54), + icons: rgba(map_get($mat-blue-gray, 900), 0.54), + text: rgba(map_get($mat-blue-gray, 900), 0.87), + slider-min: rgba(map_get($mat-blue-gray, 900), 0.87), + slider-off: rgba(map_get($mat-blue-gray, 900), 0.26), + slider-off-active: rgba(map_get($mat-blue-gray, 900), 0.38), +); + +@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) { + @return ( + primary: $primary, + accent: $accent, + warn: $warn, + is-dark: false, + foreground: $custom-light-theme-foreground, + background: $custom-light-theme-background + ); +} + +$blue-primary: mat-palette($custom-blue, 200, 50, 800); +$blue-accent: mat-palette($mat-brown, 600, 300, 900); +$blue-warn: mat-palette($mat-red, 500); + +$blue-theme: create-custom-theme($blue-primary, $blue-accent, $blue-warn); \ No newline at end of file diff --git a/src/app/shared/theme/skins/self-brown.scss b/src/app/shared/theme/skins/self-brown.scss new file mode 100644 index 00000000..45718a78 --- /dev/null +++ b/src/app/shared/theme/skins/self-brown.scss @@ -0,0 +1,83 @@ +$custom-brown: ( + 50 : #fcf7f3, + 100 : #f8ece2, + 200 : #f4dfcf, + 300 : #efd2bc, + 400 : #ebc9ad, + 500 : #e8bf9f, + 600 : #e5b997, + 700 : #e2b18d, + 800 : #dea983, + 900 : #d89b72, + A100 : #ffffff, + A200 : #ffffff, + A400 : #fff5ee, + A700 : #ffe5d4, + contrast: ( + 50 : #ECEFF1, + 100 : #CFD8DC, + 200 : #37474F, + 300 : #37474F, + 400 : #37474F, + 500 : #607D8B, + 600 : #546E7A, + 700 : #455A64, + 800 : #37474F, + 900 : #263238, + A100 : #000000, + A200 : #000000, + A400 : #000000, + A700 : #000000, + ) +); + +$custom-light-theme-background: ( + status-bar: map_get($mat-blue-gray, 300), + app-bar: map_get($mat-blue-gray, 300), + background: map_get($custom-brown, 50), + hover: rgba(black, 0.04), + card: map_get($custom-brown, 100), + dialog: map_get($custom-brown, 100), + disabled-button: rgba(black, 0.12), + raised-button: map_get($mat-blue-gray, 100), + focused-button: $dark-focused, + selected-button: map_get($mat-blue-gray, 300), + selected-disabled-button: map_get($mat-blue-gray, 400), + disabled-button-toggle: map_get($mat-blue-gray, 200), + unselected-chip: map_get($mat-blue-gray, 300), + disabled-list-option: map_get($mat-blue-gray, 200), +); + +$custom-light-theme-foreground: ( + base: map_get($mat-blue-gray, 900), + divider: $dark-dividers, + dividers: $dark-dividers, + disabled: $dark-disabled-text, + disabled-button: rgba(map_get($mat-blue-gray, 900), 0.26), + disabled-text: $dark-disabled-text, + hint-text: $dark-disabled-text, + secondary-text: $dark-secondary-text, + icon: rgba(map_get($mat-blue-gray, 900), 0.54), + icons: rgba(map_get($mat-blue-gray, 900), 0.54), + text: rgba(map_get($mat-blue-gray, 900), 0.87), + slider-min: rgba(map_get($mat-blue-gray, 900), 0.87), + slider-off: rgba(map_get($mat-blue-gray, 900), 0.26), + slider-off-active: rgba(map_get($mat-blue-gray, 900), 0.38), +); + +@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) { + @return ( + primary: $primary, + accent: $accent, + warn: $warn, + is-dark: false, + foreground: $custom-light-theme-foreground, + background: $custom-light-theme-background + ); +} + +$brown-primary: mat-palette($custom-brown, 200, 50, 800); +$brown-accent: mat-palette($mat-brown, 600, 300, 900); +$brown-warn: mat-palette($mat-red, 500); + +$brown-theme: create-custom-theme($brown-primary, $brown-accent, $brown-warn); \ No newline at end of file diff --git a/src/app/shared/theme/skins/self-gray.scss b/src/app/shared/theme/skins/self-gray.scss new file mode 100644 index 00000000..723ed183 --- /dev/null +++ b/src/app/shared/theme/skins/self-gray.scss @@ -0,0 +1,50 @@ +$custom-dark-theme-background: ( + status-bar: map_get($mat-gray, 900), + app-bar: map_get($mat-gray, 900), + background: map_get($mat-gray, 50), + hover: rgba(map_get($mat-grey, 900), 0.04), + card: map_get($mat-gray, 100), + dialog: map_get($mat-gray, 100), + disabled-button: rgba(map_get($mat-grey, 900), 0.12), + raised-button: map-get($mat-gray, 600), + focused-button: $light-focused, + selected-button: map_get($mat-grey, 800), + selected-disabled-button: map_get($mat-grey, 600), + disabled-button-toggle: map_get($mat-grey, 900), + unselected-chip: map_get($mat-grey, 700), + disabled-list-option: map_get($mat-grey, 900) +); + +$custom-dark-theme-foreground: ( + base: black, + divider: $dark-dividers, + dividers: $dark-dividers, + disabled: $dark-disabled-text, + disabled-button: rgba(black, 0.26), + disabled-text: $dark-disabled-text, + hint-text: $dark-disabled-text, + secondary-text: $dark-secondary-text, + icon: rgba(black, 0.54), + icons: rgba(black, 0.54), + text: rgba(black, 0.87), + slider-min: rgba(black, 0.87), + slider-off: rgba(black, 0.26), + slider-off-active: rgba(black, 0.38), +); + +@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) { + @return ( + primary: $primary, + accent: $accent, + warn: $warn, + is-dark: false, + foreground: $custom-dark-theme-foreground, + background: $custom-dark-theme-background + ); +} + +$gray-primary: mat-palette($mat-gray, 500, 200, 900); +$gray-accent: mat-palette($mat-indigo); +$gray-warn: mat-palette($mat-red, 900); + +$gray-theme: create-custom-theme($gray-primary, $gray-accent, $gray-warn); \ No newline at end of file diff --git a/src/app/shared/theme/skins/self-green.scss b/src/app/shared/theme/skins/self-green.scss new file mode 100644 index 00000000..44c1e29d --- /dev/null +++ b/src/app/shared/theme/skins/self-green.scss @@ -0,0 +1,83 @@ +$custom-green: ( + 50 : #f6faf5, + 100 : #e9f3e5, + 200 : #dbebd4, + 300 : #cce3c2, + 400 : #c1ddb5, + 500 : #b6d7a8, + 600 : #afd3a0, + 700 : #a6cd97, + 800 : #9ec78d, + 900 : #8ebe7d, + A100 : #ffffff, + A200 : #ffffff, + A400 : #e7ffde, + A700 : #d4ffc4, + contrast: ( + 50 : #ECEFF1, + 100 : #CFD8DC, + 200 : #37474F, + 300 : #37474F, + 400 : #37474F, + 500 : #607D8B, + 600 : #546E7A, + 700 : #455A64, + 800 : #37474F, + 900 : #263238, + A100 : #000000, + A200 : #000000, + A400 : #000000, + A700 : #000000, + ) +); + +$custom-light-theme-background: ( + status-bar: map_get($mat-blue-gray, 300), + app-bar: map_get($mat-blue-gray, 300), + background: map_get($custom-green, 50), + hover: rgba(black, 0.04), + card: map_get($custom-green, 100), + dialog: map_get($custom-green, 100), + disabled-button: rgba(black, 0.12), + raised-button: map_get($mat-blue-gray, 100), + focused-button: $dark-focused, + selected-button: map_get($mat-blue-gray, 300), + selected-disabled-button: map_get($mat-blue-gray, 400), + disabled-button-toggle: map_get($mat-blue-gray, 200), + unselected-chip: map_get($mat-blue-gray, 300), + disabled-list-option: map_get($mat-blue-gray, 200), +); + +$custom-light-theme-foreground: ( + base: map_get($mat-blue-gray, 900), + divider: $dark-dividers, + dividers: $dark-dividers, + disabled: $dark-disabled-text, + disabled-button: rgba(map_get($mat-blue-gray, 900), 0.26), + disabled-text: $dark-disabled-text, + hint-text: $dark-disabled-text, + secondary-text: $dark-secondary-text, + icon: rgba(map_get($mat-blue-gray, 900), 0.54), + icons: rgba(map_get($mat-blue-gray, 900), 0.54), + text: rgba(map_get($mat-blue-gray, 900), 0.87), + slider-min: rgba(map_get($mat-blue-gray, 900), 0.87), + slider-off: rgba(map_get($mat-blue-gray, 900), 0.26), + slider-off-active: rgba(map_get($mat-blue-gray, 900), 0.38), +); + +@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) { + @return ( + primary: $primary, + accent: $accent, + warn: $warn, + is-dark: false, + foreground: $custom-light-theme-foreground, + background: $custom-light-theme-background + ); +} + +$green-primary: mat-palette($custom-green, 200, 50, 800); +$green-accent: mat-palette($mat-brown, 600, 300, 900); +$green-warn: mat-palette($mat-red, 500); + +$green-theme: create-custom-theme($green-primary, $green-accent, $green-warn); \ No newline at end of file diff --git a/src/app/shared/theme/skins/self-pink.scss b/src/app/shared/theme/skins/self-pink.scss new file mode 100644 index 00000000..b213deef --- /dev/null +++ b/src/app/shared/theme/skins/self-pink.scss @@ -0,0 +1,83 @@ +$custom-pink: ( + 50 : #fcf4f5, + 100 : #f8e3e6, + 200 : #f4d1d6, + 300 : #efbec5, + 400 : #ebb0b8, + 500 : #e8a2ac, + 600 : #e59aa5, + 700 : #e2909b, + 800 : #de8692, + 900 : #d87582, + A100 : #ffffff, + A200 : #ffffff, + A400 : #fff1f2, + A700 : #ffd7dc, + contrast: ( + 50 : #ECEFF1, + 100 : #CFD8DC, + 200 : #37474F, + 300 : #90A4AE, + 400 : #78909C, + 500 : #607D8B, + 600 : #546E7A, + 700 : #455A64, + 800 : #37474F, + 900 : #263238, + A100 : #000000, + A200 : #000000, + A400 : #000000, + A700 : #000000, + ) +); + +$custom-light-theme-background: ( + status-bar: map_get($mat-blue-gray, 300), + app-bar: map_get($mat-blue-gray, 300), + background: map_get($custom-pink, 50), + hover: rgba(black, 0.04), + card: map_get($custom-pink, 100), + dialog: map_get($custom-pink, 100), + disabled-button: rgba(black, 0.12), + raised-button: map_get($mat-blue-gray, 100), + focused-button: $dark-focused, + selected-button: map_get($mat-blue-gray, 300), + selected-disabled-button: map_get($mat-blue-gray, 400), + disabled-button-toggle: map_get($mat-blue-gray, 200), + unselected-chip: map_get($mat-blue-gray, 300), + disabled-list-option: map_get($mat-blue-gray, 200), +); + +$custom-light-theme-foreground: ( + base: map_get($mat-blue-gray, 900), + divider: $dark-dividers, + dividers: $dark-dividers, + disabled: $dark-disabled-text, + disabled-button: rgba(map_get($mat-blue-gray, 900), 0.26), + disabled-text: $dark-disabled-text, + hint-text: $dark-disabled-text, + secondary-text: $dark-secondary-text, + icon: rgba(map_get($mat-blue-gray, 900), 0.54), + icons: rgba(map_get($mat-blue-gray, 900), 0.54), + text: rgba(map_get($mat-blue-gray, 900), 0.87), + slider-min: rgba(map_get($mat-blue-gray, 900), 0.87), + slider-off: rgba(map_get($mat-blue-gray, 900), 0.26), + slider-off-active: rgba(map_get($mat-blue-gray, 900), 0.38), +); + +@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) { + @return ( + primary: $primary, + accent: $accent, + warn: $warn, + is-dark: false, + foreground: $custom-light-theme-foreground, + background: $custom-light-theme-background + ); +} + +$pink-primary: mat-palette($custom-pink, 200, 50, 800); +$pink-accent: mat-palette($mat-blue-gray, 600, 300, 900); +$pink-warn: mat-palette($mat-red, 700); + +$pink-theme: create-custom-theme($pink-primary, $pink-accent, $pink-warn); \ No newline at end of file diff --git a/src/app/shared/theme/skins/self-yellow.scss b/src/app/shared/theme/skins/self-yellow.scss new file mode 100644 index 00000000..342f70b2 --- /dev/null +++ b/src/app/shared/theme/skins/self-yellow.scss @@ -0,0 +1,83 @@ +$custom-yellow: ( + 50 : #fffcf3, + 100 : #fff7e0, + 200 : #fff2cc, + 300 : #ffedb8, + 400 : #ffe9a8, + 500 : #ffe599, + 600 : #ffe291, + 700 : #ffde86, + 800 : #ffda7c, + 900 : #ffd36b, + A100 : #ffffff, + A200 : #ffffff, + A400 : #ffffff, + A700 : #fffbf3, + contrast: ( + 50 : #ECEFF1, + 100 : #CFD8DC, + 200 : #37474F, + 300 : #37474F, + 400 : #37474F, + 500 : #607D8B, + 600 : #546E7A, + 700 : #455A64, + 800 : #37474F, + 900 : #263238, + A100 : #000000, + A200 : #000000, + A400 : #000000, + A700 : #000000, + ) +); + +$custom-light-theme-background: ( + status-bar: map_get($mat-blue-gray, 300), + app-bar: map_get($mat-blue-gray, 300), + background: map_get($custom-yellow, 50), + hover: rgba(black, 0.04), + card: map_get($custom-yellow, 100), + dialog: map_get($custom-yellow, 100), + disabled-button: rgba(black, 0.12), + raised-button: map_get($mat-blue-gray, 100), + focused-button: $dark-focused, + selected-button: map_get($mat-blue-gray, 300), + selected-disabled-button: map_get($mat-blue-gray, 400), + disabled-button-toggle: map_get($mat-blue-gray, 200), + unselected-chip: map_get($mat-blue-gray, 300), + disabled-list-option: map_get($mat-blue-gray, 200), +); + +$custom-light-theme-foreground: ( + base: map_get($mat-blue-gray, 900), + divider: $dark-dividers, + dividers: $dark-dividers, + disabled: $dark-disabled-text, + disabled-button: rgba(map_get($mat-blue-gray, 900), 0.26), + disabled-text: $dark-disabled-text, + hint-text: $dark-disabled-text, + secondary-text: $dark-secondary-text, + icon: rgba(map_get($mat-blue-gray, 900), 0.54), + icons: rgba(map_get($mat-blue-gray, 900), 0.54), + text: rgba(map_get($mat-blue-gray, 900), 0.87), + slider-min: rgba(map_get($mat-blue-gray, 900), 0.87), + slider-off: rgba(map_get($mat-blue-gray, 900), 0.26), + slider-off-active: rgba(map_get($mat-blue-gray, 900), 0.38), +); + +@function create-custom-theme($primary, $accent, $warn: mat-palette($mat-red)) { + @return ( + primary: $primary, + accent: $accent, + warn: $warn, + is-dark: false, + foreground: $custom-light-theme-foreground, + background: $custom-light-theme-background + ); +} + +$yellow-primary: mat-palette($custom-yellow, 200, 50, 800); +$yellow-accent: mat-palette($mat-brown, 600, 300, 900); +$yellow-warn: mat-palette($mat-red, 500); + +$yellow-theme: create-custom-theme($yellow-primary, $yellow-accent, $yellow-warn); \ No newline at end of file diff --git a/src/app/shared/theme/styles/change-theme.scss b/src/app/shared/theme/styles/change-theme.scss new file mode 100644 index 00000000..7cb23e85 --- /dev/null +++ b/src/app/shared/theme/styles/change-theme.scss @@ -0,0 +1,63 @@ +@import "constants"; + +@mixin change-theme($theme) { + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + $foreground: map-get($theme, foreground); + $background: map-get($theme, background); + $testColor: mat-palette($mat-red, A200); + + .bg-primary { + @include _mat-toolbar-color($primary); + } + + .bg-accent { + @include _mat-toolbar-color($accent); + } + + .bg-warn { + @include _mat-toolbar-color($warn); + } + + .bg-test { + @include _mat-toolbar-color($testColor); + } + + .foreground.mat-progress-spinner circle, .foreground.mat-spinner circle { + stroke: mat-color($foreground, text); + } + + .mat-toolbar-row, + .mat-toolbar-single-row { + height: $toolbar-height; + } + + .lnd-info{ + border-bottom: 1px solid mat-color($foreground, divider); + } + .horizontal-nav { + position: fixed; + top:74px; + z-index: 9999; + } + a { + color: mat-color($foreground, text); + } + + .active-link { + background: mat-color($primary); + } + + .h-active-link { + background: mat-color($primary, lighter) !important; + } + + .ngx-charts { + fill: mat-color($foreground, text); + .bar { + fill: mat-color($primary) !important; + cursor: default; + } + } +} diff --git a/src/app/shared/theme/styles/constants.scss b/src/app/shared/theme/styles/constants.scss new file mode 100644 index 00000000..1f69edae --- /dev/null +++ b/src/app/shared/theme/styles/constants.scss @@ -0,0 +1,11 @@ +$font-family: 'Roboto', sans-serif; +$font-size: 14px; +$toolbar-height: 46px; +$regular-sidenav-width: 250px; +$compact-sidenav-width: 185px; +$mini-sidenav-width: 100px; +$sidenav-info-height: 100px; +$settings-nav-width: 150px; +$gap: 8px; +$icon-size: 36px; +$pubkey-info-height: 15px; diff --git a/src/app/shared/theme/styles/perfect-scrollbar.scss b/src/app/shared/theme/styles/perfect-scrollbar.scss new file mode 100644 index 00000000..468b4e96 --- /dev/null +++ b/src/app/shared/theme/styles/perfect-scrollbar.scss @@ -0,0 +1,100 @@ +.ps { + overflow: hidden !important; + overflow-anchor: none; + -ms-overflow-style: none; + touch-action: auto; + -ms-touch-action: auto; + } + + .ps__rail-x { + display: none; + opacity: 0; + transition: background-color .2s linear, opacity .2s linear; + -webkit-transition: background-color .2s linear, opacity .2s linear; + height: 15px; + bottom: 0px; + margin-top: -15px; + position: relative; + } + + .ps__rail-y { + display: none; + opacity: 0; + transition: background-color .2s linear, opacity .2s linear; + -webkit-transition: background-color .2s linear, opacity .2s linear; + width: 0px; + right: 0; + position: relative; + } + + .ps--active-x > .ps__rail-x, + .ps--active-y > .ps__rail-y { + display: block; + background-color: transparent; + } + + .ps:hover > .ps__rail-x, + .ps:hover > .ps__rail-y, + .ps--focus > .ps__rail-x, + .ps--focus > .ps__rail-y, + .ps--scrolling-x > .ps__rail-x, + .ps--scrolling-y > .ps__rail-y { + opacity: 0.6; + } + + .ps .ps__rail-x:hover, + .ps .ps__rail-y:hover, + .ps .ps__rail-x:focus, + .ps .ps__rail-y:focus, + .ps .ps__rail-x.ps--clicking, + .ps .ps__rail-y.ps--clicking { + background-color: transparent; + opacity: 0.9; + } + + .ps__thumb-x { + background-color: #aaa; + border-radius: 6px; + transition: background-color .2s linear, height .2s ease-in-out; + -webkit-transition: background-color .2s linear, height .2s ease-in-out; + height: 4px; + bottom: 2px; + position: absolute; + } + + .ps__thumb-y { + background-color: #aaa; + border-radius: 6px; + transition: background-color .2s linear, width .2s ease-in-out; + -webkit-transition: background-color .2s linear, width .2s ease-in-out; + width: 4px; + right: 2px; + position: absolute; + } + + .ps__rail-x:hover > .ps__thumb-x, + .ps__rail-x:focus > .ps__thumb-x, + .ps__rail-x.ps--clicking .ps__thumb-x { + background-color: #999; + height: 6px; + } + + .ps__rail-y:hover > .ps__thumb-y, + .ps__rail-y:focus > .ps__thumb-y, + .ps__rail-y.ps--clicking .ps__thumb-y { + background-color: #999; + width: 6px; + } + + @supports (-ms-overflow-style: none) { + .ps { + overflow: auto !important; + } + } + + @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + .ps { + overflow: auto !important; + } + } + \ No newline at end of file diff --git a/src/app/shared/theme/styles/root.scss b/src/app/shared/theme/styles/root.scss new file mode 100644 index 00000000..9a89a5f6 --- /dev/null +++ b/src/app/shared/theme/styles/root.scss @@ -0,0 +1,782 @@ +@import "constants"; + +html, body { + width: 100%; + height: 99%; + font-family: $font-family !important; + font-size: $font-size !important; + line-height: 1.5; + overflow-x: hidden; +} + +.rtl-container{ + position:absolute; + width: 100%; + height: 100%; + top:0; + left:0; + right:0; + bottom:0; + .mat-menu-panel .mat-menu-content { + padding-top: 0; + padding-bottom: 0; + } + .top-toolbar { + left: $regular-sidenav-width; + width: calc(100% - #{$regular-sidenav-width}) !important; + } + .mat-sidenav-content { + margin-left: $regular-sidenav-width !important; + } + &.horizontal { + .top-toolbar, .pubkey-info-top { + left: 0; + width: 100% !important; + } + .mat-sidenav-content { + top: calc(#{$toolbar-height} + #{$gap} + #{$pubkey-info-height} + #{$toolbar-height}); + margin-left: 0 !important; + } + } + &.compact { + .sidenav{ + width: $compact-sidenav-width; + } + .mat-tree-node { + height: 76px; + padding: 0; + } + .top-toolbar, .pubkey-info-top { + left: $compact-sidenav-width; + width: calc(100% - #{$compact-sidenav-width}) !important; + } + .mat-sidenav-content { + margin-left: $compact-sidenav-width !important; + } + } + &.mini { + .sidenav { + width: $mini-sidenav-width; + } + .mat-tree-node { + padding: 0 8px 12px 8px; + .mat-icon { + font-size: $icon-size; + } + } + .top-toolbar, .pubkey-info-top { + left: $mini-sidenav-width; + width: calc(100% - #{$mini-sidenav-width}) !important; + } + .mat-sidenav-content { + margin-left: $mini-sidenav-width !important; + } + } +} + +.mat-sidenav-container .mat-sidenav-content { + top: calc(#{$toolbar-height} + #{$gap} + #{$pubkey-info-height}); + height: 100vh; +} + +.sidenav{ + width: $regular-sidenav-width; + height: 100%; + overflow: hidden !important; + overflow-anchor: none; + -ms-overflow-style: none; + touch-action: auto; + -ms-touch-action: auto; +} + +.font-9px { + font-size: 9px !important; +} + +.sticky { + position: fixed; + top: 0; + z-index: 9999; +} + +.horizontal-menu { + padding: 0; + z-index: 999; + position: fixed; + top: 0; + height: $toolbar-height; + overflow: visible; +} + +.top-bar { + position: relative; + top: 0; + bottom:0; + left:0; + right:0; + height: 0; +} + +.pubkey-info-top { + flex-wrap: wrap; + top: $toolbar-height; + margin-top: 1px; + min-height: $pubkey-info-height; + cursor: pointer; + display: flex; + left: $regular-sidenav-width; + width: calc(100% - #{$regular-sidenav-width}) !important; + align-content: center; +} + +.inner-sidenav-content { + position: relative; + // top: calc(#{$toolbar-height} + #{$gap} + #{$pubkey-info-height}); + top: 0; + bottom:0; + left:0; + right:0; + padding: 4px; + min-height: calc(100% - (#{$toolbar-height} + #{$gap}*4)); + max-height: 90vh; +} + +.top-50 { + top: 50px; +} + +*{ + margin: 0; + padding: 0; +} + +.rtl-spinner{ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + position: fixed; + background: #fff; + z-index: 999999; + visibility: visible; + opacity: 1; + h4{ + margin-top: 10px; + } +} + +.mat-expansion-panel-header, .mat-menu-item, .mat-list .mat-list-item, .mat-nav-list .mat-list-item, .mat-selection-list .mat-list-item { + font-size: $font-size !important; +} + +.mat-raised-button { + width: 100%; + margin-top: 5px; + max-height: 36px; +} + +.padding-gap { + padding: $gap !important; +} + +.padding-gap-x { + padding: 0 $gap 0 $gap !important; +} + +.mat-raised-button { + margin-top: 5px !important; + max-height: 36px; +} + +.logo { + font-size: $font-size * 2; + font-weight: 700; + letter-spacing: 1px; +} + +.mat-card { + padding: 12px 24px !important; + overflow: hidden; + border-radius: 2px !important; +} + +.mat-toolbar-row, .mat-toolbar-single-row { + height: $toolbar-height; +} + +.mat-card-actions{ + display: block; + margin-bottom: 16px; + padding-left: 6px; + padding-right: 6px; +} + +.mat-card-content, .mat-card-subtitle, .mat-card-title { + display: block; + margin-bottom: 16px; +} + +.mat-card-header-text { + margin: 0 !important; + line-height: 1; +} + +.mat-form-field-wrapper { + width: 100%; + margin: 0 15px 0 0; +} + +.mat-select { + margin: 0 15px 0 0; +} + +.green { + color: #388e3c !important; +} + +.red { + color: #c62828 !important; +} + +.yellow { + color: #ffd740 !important; +} + +.mat-dialog-container { + padding: 0 !important; +} + +.mt-1px { + margin-top: 1px !important; +} + +.mt-1 { + margin-top: 0.55rem !important; +} + +.mb-1 { + margin-bottom: 0.55rem !important; +} + +.ml-1 { + margin-left: 0.55rem !important; +} + +.mr-1 { + margin-right: 0.55rem !important; +} + +.mx-1 { + margin: 0 0.55rem !important; +} + +.my-1 { + margin: 0.55rem 0 !important; +} + +.m-1 { + margin: 0.55rem !important; +} + +.mt-2 { + margin-top: 1rem !important; +} + +.mt-minus-1 { + margin-top: -0.5rem !important; +} + +.mb-2 { + margin-bottom: 1rem !important; +} + +.ml-2 { + margin-left: 1rem !important; +} + +.mr-2 { + margin-right: 1rem !important; +} + +.ml-4 { + margin-left: 2rem !important; +} + +.ml-8 { + margin-left: 4rem !important; +} + +.mr-4 { + margin-right: 2rem !important; +} + +.mx-2 { + margin: 0 1rem !important; +} + +.my-2 { + margin: 1rem 0 !important; +} + +.m-2 { + margin: 1rem !important; +} + +.pt-1 { + padding-top: 0.55rem !important; +} + +.pb-1 { + padding-bottom: 0.55rem !important; +} + +.pl-1 { + padding-left: 0.55rem !important; +} + +.pr-1 { + padding-right: 0.55rem !important; +} + +.pr-4px { + padding-right: 4px !important; +} + +.p-0 { + padding: 0 !important; +} + +.pl-0 { + padding-left: 0 !important; +} + +.px-1 { + padding: 0 0.55rem !important; +} + +.py-1 { + padding: 0.55rem 0 !important; +} + +.p-1 { + padding: 0.55rem !important; +} + +.pt-2 { + padding-top: 1rem !important; +} + +.pb-2 { + padding-bottom: 1rem !important; +} + +.pl-2 { + padding-left: 1rem !important; +} + +.pt-4 { + padding-top: 2rem !important; +} + +.pl-4 { + padding-left: 2rem !important; +} + +.pr-2 { + padding-right: 1rem !important; +} + +.pr-5 { + padding-right: 2.5rem !important; +} + +.px-2 { + padding: 0 1rem !important; +} + +.py-2 { + padding: 1rem 0 !important; +} + +.p-2 { + padding: 1rem !important; +} + +.m-1px { + margin: 1px !important; +} + +.overflow-x-auto { + overflow-x: auto; +} + +.overflow-y-auto { + overflow-y: auto; +} + +.overflow-auto { + overflow: auto; +} + +.mat-footer-row, .mat-header-row, .mat-row { + border-bottom-width: 0px; +} + +.mat-cell, .mat-header-cell, .mat-footer-cell { + border-bottom-width: 1px; + border-bottom-style: solid; + border-bottom-color: rgba(0, 0, 0, 0.12); +} + +.flex-ellipsis { + padding-right: 30px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.mat-list, .mat-list .mat-list-item .mat-list-item-content, .mat-nav-list, .mat-selection-list { + padding: 0 !important; +} + +.inline-spinner { + display: inline-flex !important; + top: 5px !important; +} + +.top-minus-5px { + position: relative; + top:-5px; +} + +.top-minus-15px { + position: relative; + top:-15px; +} + +.top-minus-25px { + position: relative; + top:-25px; + margin-bottom: -25px !important; +} + +.top-minus-30px { + position: relative; + top:-30px; +} + +.cursor-pointer { + cursor: pointer !important; +} + +.cursor-default { + cursor: default !important; +} + +.cursor-not-allowed { + cursor: not-allowed !important; +} + +.font-60-percent { + font-size: 60%; +} + +.inline-flex { + display: inline-flex !important; +} + +.error-border { + border: 1px solid red; + box-shadow: 0 3px 1px -2px rgba(255,0,0,.2), 0 2px 2px 0 rgba(255,0,0,.14), 0 1px 5px 0 rgba(255,0,0,.12) !important; +} + +.settings{ + position: fixed; + width: $settings-nav-width; + .container{ + padding: 6px 14px; + h4{ + border-bottom: 1px solid #ccc; + margin: 12px 0 6px 0; + } + .skin{ + width:32px; + height: 0px; + padding: 0; + overflow: hidden; + cursor: pointer; + border: 16px solid; + &.light-blue{ + border-top-color: #3F51B5; + border-bottom-color: #3F51B5; + border-left-color: #ffffff; + border-right-color: #ffffff; + } + &.light-teal{ + border-left-color: #ffffff; + border-right-color: #ffffff; + border-top-color: #009688; + border-bottom-color: #009688; + } + &.light-red{ + border-left-color: #ffffff; + border-right-color: #ffffff; + border-top-color: #F44336; + border-bottom-color: #F44336; + } + &.dark-blue{ + border-left-color: #262626; + border-right-color: #262626; + border-top-color: #0277bd; + border-bottom-color: #0277bd; + } + &.dark-green{ + border-left-color: #262626; + border-right-color: #262626; + border-top-color: #388E3C; + border-bottom-color: #388E3C; + } + &.dark-pink{ + border-left-color: #262626; + border-right-color: #262626; + border-top-color: #D81B60; + border-bottom-color: #D81B60; + } + &.gray-blue{ + border-left-color: #EEEEEE; + border-right-color: #EEEEEE; + border-top-color: #00BCD4; + border-bottom-color: #00BCD4; + } + &.gray-lime{ + border-left-color: #EEEEEE; + border-right-color: #EEEEEE; + border-top-color: #DCE775; + border-bottom-color: #DCE775; + } + &.gray-purple{ + border-left-color: #EEEEEE; + border-right-color: #EEEEEE; + border-top-color: #512DA8; + border-bottom-color: #512DA8; + } + &.bluegray-amber{ + border-left-color: #CFD8DC; + border-right-color: #CFD8DC; + border-top-color: #FFA000; + border-bottom-color: #FFA000; + } + &.bluegray-deeppurple{ + border-left-color: #CFD8DC; + border-right-color: #CFD8DC; + border-top-color: #5E35B1; + border-bottom-color: #5E35B1; + } + &.bluegray-lightgreen{ + border-left-color: #CFD8DC; + border-right-color: #CFD8DC; + border-top-color: #689F38; + border-bottom-color: #689F38; + } + &.self-gray{ + border-left-color: #FAFAFA; + border-right-color: #FAFAFA; + border-top-color: #9e9e9e; + border-bottom-color: #9e9e9e; + } + &.self-green{ + border-left-color: #dbebd4; + border-right-color: #dbebd4; + border-top-color: #9ec78d; + border-bottom-color: #9ec78d; + } + &.self-yellow{ + border-left-color: #fff2cc; + border-right-color: #fff2cc; + border-top-color: #ffda7c; + border-bottom-color: #ffda7c; + } + &.self-blue{ + border-left-color: #cfe2f4; + border-right-color: #cfe2f4; + border-top-color: #83b0de; + border-bottom-color: #83b0de; + } + &.self-brown{ + border-left-color: #f4dfcf; + border-right-color: #f4dfcf; + border-top-color: #dea983; + border-bottom-color: #dea983; + } + &.self-pink{ + border-left-color: #f4d1d6; + border-right-color: #f4d1d6; + border-top-color: #de8692; + border-bottom-color: #de8692; + } + } + } + + .mat-radio-group{ + display: inline-flex; + flex-direction: column; + .mat-radio-button{ + margin: 2px 0; + } + } + + .mat-slide-toggle{ + padding: 0px 14px; + } + +} + +.op-image{ + box-shadow: 0 0 2px #ccc; + border: 2px solid; + border-color: transparent; + cursor: pointer; + transition: 0.2s; +} + +.settings-icon{ + position: fixed; + top: 30%; + right: 0; + width: 42px; + height: 42px; + opacity: 0.6; + cursor: pointer; + z-index: 999999; +} + +.test-banner { + padding-top: 2px; + background-color: #FC7783; + text-transform: uppercase; + border-radius: 2px; +} + +.icon-large { + font-size: 70px; + margin-left: -100%; +} + +.icon-small { + height: 20px !important; + width: 20px !important; + font-size: 20px !important; +} + +.icon-smaller { + height: 10px !important; + width: 10px !important; + font-size: 10px !important; +} + +.copy-icon { + position: relative; + top: 5px; +} + +.copy-icon-smaller { + position: relative; + top: 2px; +} + +.top-5px { + position: relative; + top: 5px; +} + +.animate-settings { + -webkit-animation: animate-settings 10s linear infinite; + -moz-animation: animate-settings 10s linear infinite; + animation: animate-settings 10s linear infinite; +} +@keyframes animate-settings { + 100% {transform: rotate(360deg)} +} +@-moz-keyframes animate-settings { + 100% {-moz-transform: rotate(360deg)} +} +@-webkit-keyframes animate-settings { + 100% {-webkit-transform: rotate(360deg)} +} + +.size-30 { + font-size: 30px; +} + +.mt-minus-5 { + position: relative; + margin-top: -5px; +} + +.custom-card { + padding: 0px 0px 8px 0px !important; +} + +.not-found-box { + min-width: 30%; +} + +.w-100 { + width: 100% !important; +} + +.w-84 { + width: 84% !important; +} + +.h-100 { + height: 100% !important; +} + +a { + outline: none; + text-decoration: none; +} + +.mat-tree { + width: 100%; + margin-top: 8px; +} + +.mat-tree-node { + min-height: 42px !important; + padding: 0 12px 0 12px; + cursor: pointer; +} + +.mat-tree-node:focus, .mat-tree-node:active { + outline: none; +} + +.lnd-info { + height: $sidenav-info-height; +} + +.horizontal-button { + height: $toolbar-height; +} + +.mat-icon-36 { + width: $icon-size !important; + height: $icon-size !important; + font-size: $icon-size; +} + +.flex-wrap { + flex-wrap: wrap !important; +} + +.word-break { + word-break: break-all !important; +} + +.qr-border { + border: 2px solid white; +} diff --git a/src/app/shared/theme/styles/styles.scss b/src/app/shared/theme/styles/styles.scss new file mode 100644 index 00000000..abe37aeb --- /dev/null +++ b/src/app/shared/theme/styles/styles.scss @@ -0,0 +1,6 @@ +@import url('https://fonts.googleapis.com/css?family=Roboto:100,100i,300,300i,400,400i,500,500i,700,700i,900,900i'); +@import url('https://fonts.googleapis.com/icon?family=Material+Icons'); + +@import "/root"; +@import "/perfect-scrollbar"; +@import "/theme"; diff --git a/src/app/shared/theme/styles/theme.scss b/src/app/shared/theme/styles/theme.scss new file mode 100644 index 00000000..95217bba --- /dev/null +++ b/src/app/shared/theme/styles/theme.scss @@ -0,0 +1,96 @@ +@import '~@angular/material/theming'; +@include mat-core(); +@import 'change-theme'; + +.rtl-container{ + &.light-blue{ + @import "../skins/light-blue"; + @include angular-material-theme($blue-light-theme); + @include change-theme($blue-light-theme); + } + &.light-teal{ + @import "../skins/light-teal"; + @include angular-material-theme($teal-light-theme); + @include change-theme($teal-light-theme); + } + &.light-red{ + @import "../skins/light-red"; + @include angular-material-theme($red-light-theme); + @include change-theme($red-light-theme); + } + &.dark-blue{ + @import "../skins/dark-blue"; + @include angular-material-theme($blue-dark-theme); + @include change-theme($blue-dark-theme); + } + &.dark-green{ + @import "../skins/dark-green"; + @include angular-material-theme($green-dark-theme); + @include change-theme($green-dark-theme); + } + &.dark-pink{ + @import "../skins/dark-pink"; + @include angular-material-theme($pink-dark-theme); + @include change-theme($pink-dark-theme); + } + &.gray-blue{ + @import "../skins/gray-blue"; + @include angular-material-theme($blue-gray-theme); + @include change-theme($blue-gray-theme); + } + &.gray-lime{ + @import "../skins/gray-lime"; + @include angular-material-theme($lime-gray-theme); + @include change-theme($lime-gray-theme); + } + &.gray-purple{ + @import "../skins/gray-purple"; + @include angular-material-theme($purple-gray-theme); + @include change-theme($purple-gray-theme); + } + &.bluegray-amber{ + @import "../skins/bluegray-amber"; + @include angular-material-theme($amber-bluegray-theme); + @include change-theme($amber-bluegray-theme); + } + &.bluegray-deeppurple{ + @import "../skins/bluegray-deeppurple"; + @include angular-material-theme($deeppurple-bluegray-theme); + @include change-theme($deeppurple-bluegray-theme); + } + &.bluegray-lightgreen{ + @import "../skins/bluegray-lightgreen"; + @include angular-material-theme($lightgreen-bluegray-theme); + @include change-theme($lightgreen-bluegray-theme); + } + &.self-gray{ + @import "../skins/self-gray"; + @include angular-material-theme($gray-theme); + @include change-theme($gray-theme); + } + &.self-green{ + @import "../skins/self-green"; + @include angular-material-theme($green-theme); + @include change-theme($green-theme); + } + &.self-yellow{ + @import "../skins/self-yellow"; + @include angular-material-theme($yellow-theme); + @include change-theme($yellow-theme); + } + &.self-blue{ + @import "../skins/self-blue"; + @include angular-material-theme($blue-theme); + @include change-theme($blue-theme); + } + &.self-brown{ + @import "../skins/self-brown"; + @include angular-material-theme($brown-theme); + @include change-theme($brown-theme); + } + &.self-pink{ + @import "../skins/self-pink"; + @include angular-material-theme($pink-theme); + @include change-theme($pink-theme); + } +} diff --git a/src/assets/.gitkeep b/src/assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/assets/images/RTL1.jpg b/src/assets/images/RTL1.jpg new file mode 100644 index 00000000..09d4a96e Binary files /dev/null and b/src/assets/images/RTL1.jpg differ diff --git a/src/assets/images/RTL2.jpg b/src/assets/images/RTL2.jpg new file mode 100644 index 00000000..2f3a5634 Binary files /dev/null and b/src/assets/images/RTL2.jpg differ diff --git a/src/assets/images/favicon.ico b/src/assets/images/favicon.ico new file mode 100644 index 00000000..c5c9ce90 Binary files /dev/null and b/src/assets/images/favicon.ico differ diff --git a/src/browserslist b/src/browserslist new file mode 100644 index 00000000..37371cb0 --- /dev/null +++ b/src/browserslist @@ -0,0 +1,11 @@ +# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries +# +# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed + +> 0.5% +last 2 versions +Firefox ESR +not dead +not IE 9-11 \ No newline at end of file diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts new file mode 100644 index 00000000..2ad8a3d0 --- /dev/null +++ b/src/environments/environment.prod.ts @@ -0,0 +1,24 @@ +import { VERSION } from './version'; + +export const API_URL = './api'; + +export const environment = { + production: true, + isDebugMode: false, + AUTHENTICATE_API: API_URL + '/authenticate', + BALANCE_API: API_URL + '/balance', + FEES_API: API_URL + '/fees', + PEERS_API: API_URL + '/peers', + CHANNELS_API: API_URL + '/channels', + GETINFO_API: API_URL + '/getinfo', + WALLET_API: API_URL + '/wallet', + NETWORK_API: API_URL + '/network', + NEW_ADDRESS_API : API_URL + '/newaddress', + TRANSACTIONS_API : API_URL + '/transactions', + CONF_API: API_URL + '/conf', + PAYREQUEST_API: API_URL + '/payreq', + PAYMENTS_API: API_URL + '/payments', + INVOICES_API: API_URL + '/invoices', + SWITCH_API: API_URL + '/switch', + VERSION: VERSION +}; diff --git a/src/environments/environment.ts b/src/environments/environment.ts new file mode 100644 index 00000000..2024cbe1 --- /dev/null +++ b/src/environments/environment.ts @@ -0,0 +1,24 @@ +import { VERSION } from './version'; + +export const API_URL = 'http://localhost:3000/rtl/api'; + +export const environment = { + production: false, + isDebugMode: true, + AUTHENTICATE_API: API_URL + '/authenticate', + BALANCE_API: API_URL + '/balance', + FEES_API: API_URL + '/fees', + PEERS_API: API_URL + '/peers', + CHANNELS_API: API_URL + '/channels', + GETINFO_API: API_URL + '/getinfo', + WALLET_API: API_URL + '/wallet', + NETWORK_API: API_URL + '/network', + NEW_ADDRESS_API : API_URL + '/newaddress', + TRANSACTIONS_API : API_URL + '/transactions', + CONF_API: API_URL + '/conf', + PAYREQUEST_API: API_URL + '/payreq', + PAYMENTS_API: API_URL + '/payments', + INVOICES_API: API_URL + '/invoices', + SWITCH_API: API_URL + '/switch', + VERSION: VERSION +}; diff --git a/src/environments/version.ts b/src/environments/version.ts new file mode 100644 index 00000000..716fbd1b --- /dev/null +++ b/src/environments/version.ts @@ -0,0 +1 @@ +export const VERSION = '0.2.16-beta'; \ No newline at end of file diff --git a/src/index.html b/src/index.html new file mode 100644 index 00000000..7999ed6e --- /dev/null +++ b/src/index.html @@ -0,0 +1,13 @@ + + + + + RTL + + + + + + + + diff --git a/src/karma.conf.js b/src/karma.conf.js new file mode 100644 index 00000000..b6e00421 --- /dev/null +++ b/src/karma.conf.js @@ -0,0 +1,31 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '../coverage'), + reports: ['html', 'lcovonly'], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false + }); +}; \ No newline at end of file diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 00000000..e44f3d92 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,12 @@ +import 'hammerjs'; +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule).catch(err => console.log(err)); diff --git a/src/polyfills.ts b/src/polyfills.ts new file mode 100644 index 00000000..480c13eb --- /dev/null +++ b/src/polyfills.ts @@ -0,0 +1,80 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE9, IE10 and IE11 requires all of the following polyfills. **/ +import 'core-js/es6/symbol'; +import 'core-js/es6/object'; +import 'core-js/es6/function'; +import 'core-js/es6/parse-int'; +import 'core-js/es6/parse-float'; +import 'core-js/es6/number'; +import 'core-js/es6/math'; +import 'core-js/es6/string'; +import 'core-js/es6/date'; +import 'core-js/es6/array'; +import 'core-js/es6/regexp'; +import 'core-js/es6/map'; +import 'core-js/es6/weak-map'; +import 'core-js/es6/set'; + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** IE10 and IE11 requires the following for the Reflect API. */ +// import 'core-js/es6/reflect'; + + +/** Evergreen browsers require these. **/ +// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. + + + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + **/ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + */ + + // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + + /* + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + */ +// (window as any).__Zone_enable_cross_context_check = true; + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/src/test.ts b/src/test.ts new file mode 100644 index 00000000..16317897 --- /dev/null +++ b/src/test.ts @@ -0,0 +1,20 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/zone-testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/src/tsconfig.app.json b/src/tsconfig.app.json new file mode 100644 index 00000000..190fd300 --- /dev/null +++ b/src/tsconfig.app.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "types": [] + }, + "exclude": [ + "test.ts", + "**/*.spec.ts" + ] +} diff --git a/src/tsconfig.spec.json b/src/tsconfig.spec.json new file mode 100644 index 00000000..de773363 --- /dev/null +++ b/src/tsconfig.spec.json @@ -0,0 +1,18 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "test.ts", + "polyfills.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/src/tslint.json b/src/tslint.json new file mode 100644 index 00000000..9fceb7c0 --- /dev/null +++ b/src/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "rtl", + "camelCase" + ], + "component-selector": [ + true, + "element", + "rtl", + "kebab-case" + ] + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..3d5861df --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "importHelpers": true, + "outDir": "./angular", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2017", + "dom" + ] + } +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 00000000..03baed77 --- /dev/null +++ b/tslint.json @@ -0,0 +1,141 @@ +{ + "rulesDirectory": [ + "node_modules/codelyzer" + ], + "rules": { + "arrow-return-shorthand": true, + "callable-types": true, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "eofline": true, + "forin": true, + "import-blacklist": [ + true + ], + "import-spacing": true, + "indent": [ + true, + "spaces" + ], + "interface-over-type-literal": true, + "label-position": true, + "max-line-length": [ + true, + 180 + ], + "member-access": false, + "member-ordering": [ + true, + { + "order": [ + "static-field", + "instance-field", + "static-method", + "instance-method" + ] + } + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-super": true, + "no-empty": false, + "no-empty-interface": true, + "no-eval": true, + "no-inferrable-types": [ + true, + "ignore-params" + ], + "no-misused-new": true, + "no-non-null-assertion": true, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-string-throw": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unnecessary-initializer": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "prefer-const": true, + "quotemark": [ + true, + "single" + ], + "radix": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "typeof-compare": true, + "unified-signatures": true, + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + "directive-selector": [ + true, + "attribute", + "rtl", + "camelCase" + ], + "component-selector": [ + true, + "element", + "rtl", + "kebab-case" + ], + "use-input-property-decorator": true, + "use-output-property-decorator": true, + "use-host-property-decorator": true, + "no-input-rename": true, + "no-output-rename": true, + "use-life-cycle-interface": true, + "use-pipe-transform-interface": true, + "component-class-suffix": true, + "directive-class-suffix": true, + "no-access-missing-member": true, + "templates-use-public": true, + "invoke-injectable": true + } +}