├── .gitignore ├── src ├── app │ ├── app.component.html │ ├── providers.ts │ ├── app-routing.module.ts │ ├── app.component.ts │ └── app.module.ts ├── favicon.ico ├── environments │ ├── environment.ts │ └── environment.prod.ts ├── assets │ └── images │ │ ├── sbl.png │ │ ├── cumulative.svg │ │ ├── divide.svg │ │ ├── invoices.svg │ │ ├── lightning-fees.svg │ │ ├── alert.svg │ │ ├── rebalance.svg │ │ ├── close.svg │ │ ├── sbl.svg │ │ ├── sats.svg │ │ ├── chart.svg │ │ ├── keysends.svg │ │ ├── bottle.svg │ │ ├── github.svg │ │ ├── sats-routed.svg │ │ ├── average.svg │ │ ├── payments.svg │ │ ├── settings.svg │ │ ├── ruler.svg │ │ ├── profit.svg │ │ ├── alert-sbl.svg │ │ ├── private.svg │ │ ├── payment.svg │ │ ├── twitter.svg │ │ ├── chain-fees.svg │ │ ├── weekly.svg │ │ ├── daily.svg │ │ ├── count.svg │ │ ├── amboss.svg │ │ ├── monthly.svg │ │ ├── telegram.svg │ │ └── forwards.svg ├── bos-data │ ├── chain-fees.ts │ ├── forwards.ts │ ├── invoices.ts │ └── payments.ts ├── pages │ ├── chart │ │ ├── chart.scss │ │ ├── chart.html │ │ └── chart.ts │ ├── cold-sats │ │ ├── cold-sats.scss │ │ ├── cold-sats.html │ │ └── cold-sats.ts │ ├── keysends-exclude-list │ │ ├── keysends-exclude-list.scss │ │ ├── keysends-exclude-list.html │ │ └── keysends-exclude-list.ts │ ├── payments-exclude-list │ │ ├── payments-exclude-list.scss │ │ ├── payments-exclude-list.html │ │ └── payments-exclude-list.ts │ └── add-data │ │ ├── add-data.scss │ │ ├── add-data.ts │ │ └── add-data.html ├── main.ts ├── index.html ├── menu │ ├── menu.scss │ ├── menu.html │ ├── menu.ts │ └── menu-items.ts ├── styles.scss ├── polyfills.ts └── providers │ ├── data.ts │ └── csv-parser.ts ├── tsconfig.app.json ├── tsconfig.spec.json ├── tsconfig.json ├── karma.conf.js ├── package.json ├── angular.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .angular/cache 3 | .DS_Store 4 | dist/ 5 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cold-sats/ln-charts/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/assets/images/sbl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cold-sats/ln-charts/HEAD/src/assets/images/sbl.png -------------------------------------------------------------------------------- /src/app/providers.ts: -------------------------------------------------------------------------------- 1 | import { Data } from 'src/providers/data'; 2 | import { CSVParser } from 'src/providers/csv-parser'; 3 | 4 | export const providers = [ 5 | Data, 6 | CSVParser 7 | ] 8 | -------------------------------------------------------------------------------- /src/bos-data/chain-fees.ts: -------------------------------------------------------------------------------- 1 | //When running locally you can paste data below instead of pasting in the UI 2 | //If you commit code be sure to remove your data here 3 | 4 | export const chainFees = ` 5 | ` 6 | -------------------------------------------------------------------------------- /src/bos-data/forwards.ts: -------------------------------------------------------------------------------- 1 | //When running locally you can paste data below instead of pasting in the UI 2 | //If you commit code be sure to remove your data here 3 | 4 | export const forwards = ` 5 | ` 6 | -------------------------------------------------------------------------------- /src/bos-data/invoices.ts: -------------------------------------------------------------------------------- 1 | //When running locally you can paste data below instead of pasting in the UI 2 | //If you commit code be sure to remove your data here 3 | 4 | export const invoices = ` 5 | ` 6 | -------------------------------------------------------------------------------- /src/bos-data/payments.ts: -------------------------------------------------------------------------------- 1 | //When running locally you can paste data below instead of pasting in the UI 2 | //If you commit code be sure to remove your data here 3 | 4 | export const payments = ` 5 | ` 6 | -------------------------------------------------------------------------------- /src/pages/chart/chart.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep .ngx-charts .bar { 2 | fill: white; 3 | } 4 | 5 | .chart { 6 | fill: white; 7 | } 8 | 9 | .chart-container { 10 | float: left; 11 | margin-top:55px; 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/cold-sats/cold-sats.scss: -------------------------------------------------------------------------------- 1 | .social-media-container { 2 | margin: 20px 0px 0px -15px; 3 | } 4 | 5 | .social-media { 6 | width: 40px; 7 | margin-left: 15px; 8 | } 9 | 10 | .text-container { 11 | max-width:490px; 12 | margin-bottom: 20px; 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/images/cumulative.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/assets/images/divide.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/pages/chart/chart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 13 | 14 | 15 |
16 | -------------------------------------------------------------------------------- /src/assets/images/invoices.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ln-charts 5 | 6 | 7 | 8 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/pages/chart/chart.ts: -------------------------------------------------------------------------------- 1 | import { Component, NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { NgxChartsModule } from '@swimlane/ngx-charts'; 4 | 5 | import { Data } from 'src/providers/data'; 6 | 7 | @Component({ 8 | selector: 'chart', 9 | templateUrl: './chart.html', 10 | styleUrls: ['./chart.scss'] 11 | }) 12 | 13 | export class ChartPage { 14 | 15 | colorScheme: any = { 16 | domain: ['#5AA454', '#A10A28', '#C7B42C', '#AAAAAA'] 17 | }; 18 | 19 | constructor( 20 | public data: Data, 21 | ) {} 22 | 23 | getContainerHeight() { 24 | return window.innerHeight - 80; 25 | } 26 | 27 | getContainerWidth() { 28 | return window.innerWidth - 166; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/assets/images/lightning-fees.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/images/alert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/keysends-exclude-list/keysends-exclude-list.scss: -------------------------------------------------------------------------------- 1 | .keysend-textarea { 2 | margin: 20px 0px 5px 0px; 3 | width: 330px; 4 | resize: none 5 | } 6 | 7 | .title-container { 8 | width: 330px; 9 | } 10 | 11 | .button-helper-container { 12 | width: 330px; 13 | text-align: center; 14 | } 15 | 16 | textarea:focus { 17 | outline: 1px solid #EBEBEB; 18 | resize: none; 19 | } 20 | 21 | .delete-icon { 22 | margin-bottom: -2px; 23 | padding-left: 5px; 24 | height: 15px; 25 | height: 20px; 26 | margin: 0 5px -5px 13px; 27 | } 28 | 29 | .delete-icon:hover { 30 | opacity: 70%; 31 | cursor: pointer; 32 | } 33 | 34 | .add-item-button { 35 | width: 331px; 36 | height: 40px; 37 | opacity: 50%; 38 | cursor: pointer; 39 | font: 300 16px Avenir, serif; 40 | margin-bottom: 5px; 41 | } 42 | 43 | .add-item-button:hover { 44 | opacity:40%; 45 | } 46 | -------------------------------------------------------------------------------- /src/pages/payments-exclude-list/payments-exclude-list.scss: -------------------------------------------------------------------------------- 1 | .payments-textarea { 2 | margin: 20px 0px 5px 0px; 3 | width: 330px; 4 | resize: none 5 | } 6 | 7 | .title-container { 8 | width: 330px; 9 | } 10 | 11 | .button-helper-container { 12 | width:330px; 13 | text-align: center; 14 | } 15 | 16 | textarea:focus { 17 | outline: 1px solid #EBEBEB; 18 | resize: none; 19 | } 20 | 21 | .delete-icon { 22 | margin-bottom: -2px; 23 | padding-left: 5px; 24 | height: 15px; 25 | height: 20px; 26 | margin: 0 5px -5px 13px; 27 | } 28 | 29 | .delete-icon:hover { 30 | opacity: 70%; 31 | cursor: pointer; 32 | } 33 | 34 | .add-item-button { 35 | width: 331px; 36 | height: 40px; 37 | opacity: 50%; 38 | cursor: pointer; 39 | font: 300 16px Avenir, serif; 40 | margin-bottom: 5px; 41 | } 42 | 43 | .add-item-button:hover { 44 | opacity:40%; 45 | } 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "sourceMap": true, 13 | "declaration": false, 14 | "downlevelIteration": true, 15 | "experimentalDecorators": true, 16 | "moduleResolution": "node", 17 | "importHelpers": true, 18 | "target": "es2020", 19 | "module": "es2020", 20 | "lib": [ 21 | "es2020", 22 | "dom" 23 | ] 24 | }, 25 | "angularCompilerOptions": { 26 | "enableI18nLegacyMessageIdFormat": false, 27 | "strictInjectionParameters": true, 28 | "strictInputAccessModifiers": true, 29 | "strictTemplates": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { AddDataPage } from 'src/pages/add-data/add-data'; 5 | import { ChartPage } from 'src/pages/chart/chart'; 6 | import { KeysendsExcludeListPage } from 'src/pages/keysends-exclude-list/keysends-exclude-list'; 7 | import { PaymentsExcludeListPage } from 'src/pages/payments-exclude-list/payments-exclude-list'; 8 | import { ColdSatsPage } from 'src/pages/cold-sats/cold-sats'; 9 | 10 | const routes: Routes = [ 11 | { path: 'add-data', component: AddDataPage }, 12 | { path: '', component: ChartPage }, 13 | { path: 'keysends-exclude-list', component: KeysendsExcludeListPage }, 14 | { path: 'payments-exclude-list', component: PaymentsExcludeListPage }, 15 | { path: 'cold-sats', component: ColdSatsPage } 16 | ]; 17 | 18 | @NgModule({ 19 | imports: [RouterModule.forRoot(routes)], 20 | exports: [RouterModule] 21 | }) 22 | 23 | export class AppRoutingModule {} 24 | -------------------------------------------------------------------------------- /src/pages/keysends-exclude-list/keysends-exclude-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | Keysends Exclude List 7 |
8 | 9 |
10 | Exclude the following from Keysends Chart: 11 |
12 | 13 |
14 | {{item}} 15 | 16 |
17 | 18 |
19 | No Keysends Added 20 |
21 | 22 |
23 | 24 | 27 | 28 |
29 | Paste the text from the "Amount" column of the bos invoices report. 30 |
31 | 32 |
33 | -------------------------------------------------------------------------------- /src/menu/menu.scss: -------------------------------------------------------------------------------- 1 | .menu-container { 2 | float: left; 3 | margin: 10px -5px 0 -40px; 4 | } 5 | 6 | .logo-container { 7 | display: flex; 8 | align-items: left; 9 | justify-content: left; 10 | margin-bottom: 20px; 11 | } 12 | 13 | .logo-title-container { 14 | line-height: 1.2em; 15 | } 16 | 17 | .menu-header-container { 18 | margin: 15px 0px 1px 12px; 19 | opacity: 30% 20 | } 21 | 22 | .menu-button-container { 23 | padding: 10px 15px 8px 0px; 24 | cursor: pointer; 25 | } 26 | 27 | .menu-button-container:hover { 28 | background-color: #1B1B1B; 29 | cursor: pointer; 30 | } 31 | 32 | .logo { 33 | width: 40px; 34 | margin: 0px 10px 0 12px; 35 | } 36 | 37 | .logo:hover { 38 | cursor: pointer; 39 | opacity: 60% 40 | } 41 | 42 | .menu-text { 43 | font: 14px Avenir, serif; 44 | color: white; 45 | } 46 | 47 | .frequency-icon { 48 | height: 17px; 49 | margin: 0 5px -3px 13px; 50 | cursor: pointer; 51 | } 52 | 53 | .no-data { 54 | opacity: 40%; 55 | } 56 | 57 | .link-selected { 58 | opacity: 70% 59 | } 60 | 61 | .menu-button-selected { 62 | background-color: #1B1B1B; 63 | } 64 | -------------------------------------------------------------------------------- /src/pages/payments-exclude-list/payments-exclude-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | Payments Exclude List 7 |
8 | 9 |
10 | Exclude the following from Payments Chart: 11 |
12 | 13 |
14 | {{item}} 15 | 16 |
17 | 18 |
19 | No Payments Added 20 |
21 | 22 |
23 | 24 | 27 | 28 |
29 | Paste the text from the "Notes" column of the bos payments report. 30 |
31 | 32 |
33 | -------------------------------------------------------------------------------- /src/assets/images/rebalance.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/images/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/pages/keysends-exclude-list/keysends-exclude-list.ts: -------------------------------------------------------------------------------- 1 | import { Component, NgModule, OnInit } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { Storage } from '@ionic/storage'; 4 | 5 | import { Data } from 'src/providers/data'; 6 | 7 | @Component({ 8 | selector: 'keysends-exclude-list', 9 | templateUrl: './keysends-exclude-list.html', 10 | styleUrls: ['./keysends-exclude-list.scss'] 11 | }) 12 | 13 | export class KeysendsExcludeListPage { 14 | 15 | constructor( 16 | public data: Data, 17 | public storage: Storage 18 | ) {} 19 | 20 | async addToKeysendsExcludeList() { 21 | const data = (document.getElementById('keysendsExcludeTextArea')).value; 22 | this.data.keysendsExcludeList.push(data); 23 | await this.storage.set('keysendsExcludeList', this.data.keysendsExcludeList); 24 | (document.getElementById('keysendsExcludeTextArea')).value = ''; 25 | this.data.loadData(); 26 | } 27 | 28 | async removeFromKeysendsExcludeList(item) { 29 | const index = this.data.keysendsExcludeList.indexOf(item); 30 | this.data.keysendsExcludeList.splice(index, 1); 31 | await this.storage.set('keysendsExcludeList', this.data.keysendsExcludeList); 32 | this.data.loadData(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/assets/images/sbl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/images/sats.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/pages/payments-exclude-list/payments-exclude-list.ts: -------------------------------------------------------------------------------- 1 | import { Component, NgModule, OnInit } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { Storage } from '@ionic/storage'; 4 | 5 | import { Data } from 'src/providers/data'; 6 | 7 | @Component({ 8 | selector: 'payments-exclude-list', 9 | templateUrl: './payments-exclude-list.html', 10 | styleUrls: ['./payments-exclude-list.scss'] 11 | }) 12 | 13 | export class PaymentsExcludeListPage { 14 | 15 | constructor( 16 | public data: Data, 17 | public storage: Storage 18 | ) {} 19 | 20 | async addToPaymentsExcludeList() { 21 | const data = (document.getElementById('paymentsExcludeTextArea')).value; 22 | const formattedData = '\"' + data + '\"'; 23 | this.data.paymentsExcludeList.push(formattedData); 24 | await this.storage.set('paymentsExcludeList', this.data.paymentsExcludeList); 25 | (document.getElementById('paymentsExcludeTextArea')).value = ''; 26 | this.data.loadData(); 27 | } 28 | 29 | async removeFromPaymentsExcludeList(item) { 30 | const index = this.data.paymentsExcludeList.indexOf(item); 31 | this.data.paymentsExcludeList.splice(index, 1); 32 | await this.storage.set('paymentsExcludeList', this.data.paymentsExcludeList); 33 | this.data.loadData(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/assets/images/chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, NgModule, OnInit } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { Router, NavigationEnd } from '@angular/router'; 4 | import { filter } from 'rxjs/operators'; 5 | 6 | import { Data } from 'src/providers/data'; 7 | 8 | declare let gtag: Function; 9 | 10 | @Component({ 11 | selector: 'app-root', 12 | templateUrl: './app.component.html' 13 | }) 14 | 15 | export class AppComponent implements OnInit { 16 | 17 | loaded: boolean; 18 | 19 | constructor( 20 | public data: Data, 21 | private router: Router 22 | ) {} 23 | 24 | async ngOnInit() { 25 | this.loaded = false; 26 | try { 27 | this.setUpAnalytics(); 28 | await this.data.loadData(); 29 | if (this.router.url == '/') { 30 | const path = this.data.hasData ? [''] : ['add-data']; 31 | this.router.navigate(path); 32 | } 33 | console.log(this.data); 34 | } catch(err) { 35 | console.log(err) 36 | } 37 | this.loaded = true; 38 | } 39 | 40 | setUpAnalytics() { 41 | this.router.events.pipe(filter(event => event instanceof NavigationEnd)) 42 | .subscribe((event: NavigationEnd) => { 43 | gtag('config', 'G-20MYM71Z9F', 44 | { 45 | page_path: event.urlAfterRedirects 46 | } 47 | ); 48 | }); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/pages/cold-sats/cold-sats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | Cold Sats 7 |
8 | 9 |
10 | My name is Steven Ellis and I run the Lightning Node 11 | Cold Sats.
12 | I built ln-charts for myself but hope others can enjoy. Feel free to contribute more charts, improvements, fixes, etc. 13 |
14 | 15 |
16 | If you enjoy ln-charts and want to donate please keysend:
17 | bos send 020a3dce2dab038955eb435a8342e4fe897304015314485d3738d5f41eccb47859 --amount 1000 --message "Thanks for ln-charts!" 18 | 19 | Copied. Thank you! 20 | 21 |
22 | 23 | 29 | 30 |
31 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | .app-background { 2 | background-color: black; 3 | } 4 | 5 | .sub-section-container { 6 | float: left; 7 | margin: 10px 0px 20px 20px; 8 | } 9 | 10 | .link { 11 | font: 12px Avenir, serif; 12 | color: white; 13 | cursor: pointer; 14 | } 15 | 16 | .link:hover { 17 | cursor: pointer; 18 | opacity: 70% 19 | } 20 | 21 | .header { 22 | font: 17px Avenir, serif; 23 | color: white; 24 | } 25 | 26 | .title { 27 | font: 500 20px Avenir, serif; 28 | color: white; 29 | } 30 | 31 | .small-text { 32 | font: 12px Avenir, serif; 33 | color: white; 34 | } 35 | 36 | .body { 37 | font: 300 16px Avenir, serif; 38 | color: white; 39 | } 40 | 41 | .title-body-container { 42 | margin-bottom: 15px; 43 | line-height: 1.8em; 44 | } 45 | 46 | .icon { 47 | height: 20px; 48 | margin: 0 5px -5px 13px; 49 | } 50 | 51 | .title-link { 52 | font: 500 20px Avenir, serif; 53 | color: white; 54 | } 55 | 56 | .title-link:hover { 57 | opacity: 50%; 58 | cursor: pointer; 59 | } 60 | 61 | .big-link { 62 | font: 300 16px Avenir, serif; 63 | color: white; 64 | } 65 | 66 | .big-link:hover { 67 | opacity: 50%; 68 | cursor: pointer; 69 | } 70 | 71 | .small-link { 72 | font: 300 12px Avenir, serif; 73 | color: white; 74 | } 75 | 76 | .small-link:hover { 77 | opacity: 50%; 78 | cursor: pointer; 79 | } 80 | 81 | .margin-left { 82 | margin-left: 15px; 83 | } 84 | 85 | .margin-bottom { 86 | margin-bottom: 15px; 87 | } 88 | 89 | .extra-margin-bottom { 90 | margin-bottom: 25px; 91 | } 92 | -------------------------------------------------------------------------------- /src/assets/images/keysends.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/ln-charts'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /src/assets/images/bottle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | 5 | import { AppRoutingModule } from 'src/app/app-routing.module'; 6 | import { AppComponent } from 'src/app/app.component'; 7 | import { providers } from 'src/app/providers'; 8 | 9 | import { NgxChartsModule } from '@swimlane/ngx-charts'; 10 | 11 | import { IonicStorageModule } from '@ionic/storage'; 12 | 13 | import { MenuComponent } from 'src/menu/menu'; 14 | 15 | import { AddDataPage } from 'src/pages/add-data/add-data'; 16 | import { ChartPage } from 'src/pages/chart/chart'; 17 | import { KeysendsExcludeListPage } from 'src/pages/keysends-exclude-list/keysends-exclude-list'; 18 | import { PaymentsExcludeListPage } from 'src/pages/payments-exclude-list/payments-exclude-list'; 19 | import { ColdSatsPage } from 'src/pages/cold-sats/cold-sats'; 20 | 21 | @NgModule({ 22 | declarations: [ 23 | AppComponent, 24 | MenuComponent, 25 | AddDataPage, 26 | ChartPage, 27 | KeysendsExcludeListPage, 28 | PaymentsExcludeListPage, 29 | ColdSatsPage 30 | ], 31 | imports: [ 32 | AppRoutingModule, 33 | BrowserModule, 34 | BrowserAnimationsModule, 35 | NgxChartsModule, 36 | IonicStorageModule.forRoot() 37 | ], 38 | exports: [ 39 | MenuComponent, 40 | AddDataPage, 41 | ChartPage, 42 | KeysendsExcludeListPage, 43 | PaymentsExcludeListPage, 44 | ColdSatsPage 45 | ], 46 | providers: [ 47 | ...providers 48 | ], 49 | bootstrap: [AppComponent] 50 | }) 51 | 52 | export class AppModule { } 53 | -------------------------------------------------------------------------------- /src/assets/images/sats-routed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ln-charts", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^14.0.0", 14 | "@angular/cdk": "^14.0.1", 15 | "@angular/common": "^14.0.0", 16 | "@angular/compiler": "^14.0.0", 17 | "@angular/core": "^14.0.0", 18 | "@angular/flex-layout": "^13.0.0-beta.38", 19 | "@angular/forms": "^14.0.0", 20 | "@angular/platform-browser": "^14.0.0", 21 | "@angular/platform-browser-dynamic": "^14.0.0", 22 | "@angular/router": "^14.0.0", 23 | "@ionic/storage": "^2.1.3", 24 | "@material/button": "^14.0.0", 25 | "@material/dom": "^14.0.0", 26 | "@material/list": "^14.0.0", 27 | "@swimlane/ngx-charts": "^20.1.0", 28 | "luxon": "^2.4.0", 29 | "material-components-web": "^14.0.0", 30 | "rxjs": "~7.5.0", 31 | "tslib": "^2.3.0", 32 | "zone.js": "~0.11.4" 33 | }, 34 | "devDependencies": { 35 | "@angular-devkit/build-angular": "^14.0.1", 36 | "@angular/cli": "^14.0.1", 37 | "@angular/compiler-cli": "^14.0.0", 38 | "@types/d3-scale": "^4.0.2", 39 | "@types/d3-selection": "^3.0.2", 40 | "@types/jasmine": "~4.0.0", 41 | "@types/luxon": "^2.3.2", 42 | "angular-cli-ghpages": "^1.0.0", 43 | "jasmine-core": "~4.1.0", 44 | "karma": "~6.3.0", 45 | "karma-chrome-launcher": "~3.1.0", 46 | "karma-coverage": "~2.2.0", 47 | "karma-jasmine": "~5.0.0", 48 | "karma-jasmine-html-reporter": "~1.7.0", 49 | "typescript": "^4.6.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/assets/images/average.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/images/payments.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/pages/cold-sats/cold-sats.ts: -------------------------------------------------------------------------------- 1 | import { Component, NgModule, OnInit } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { Storage } from '@ionic/storage'; 4 | 5 | import { Data } from 'src/providers/data'; 6 | 7 | @Component({ 8 | selector: 'cold-sats', 9 | templateUrl: './cold-sats.html', 10 | styleUrls: ['./cold-sats.scss'] 11 | }) 12 | 13 | export class ColdSatsPage { 14 | 15 | showCopied: boolean; 16 | 17 | constructor( 18 | public data: Data, 19 | public storage: Storage 20 | ) {} 21 | 22 | addToKeysendsExcludeList() { 23 | const data = (document.getElementById('keysendsExcludeTextArea')).value; 24 | this.data.keysendsExcludeList.push(data); 25 | this.storage.set('keysendsExcludeList', this.data.keysendsExcludeList); 26 | } 27 | 28 | removeFromKeysendsExcludeList(item) { 29 | const index = this.data.keysendsExcludeList.indexOf(item); 30 | this.data.keysendsExcludeList.splice(index, 1); 31 | this.storage.set('keysendsExcludeList', this.data.keysendsExcludeList); 32 | } 33 | 34 | copyKeysendCommand() { 35 | const command = 'bos send 020a3dce2dab038955eb435a8342e4fe897304015314485d3738d5f41eccb47859 --amount 1000 --message "Thanks for ln-charts!"'; 36 | this.copyToClipBoard(command); 37 | this.showCopied = true; 38 | setTimeout(() => this.showCopied = false, 2000); 39 | } 40 | 41 | copyToClipBoard(text) { 42 | const textArea = document.createElement('textarea'); 43 | textArea.value = text; 44 | document.body.appendChild(textArea); 45 | textArea.focus(); 46 | textArea.select(); 47 | document.execCommand('copy'); 48 | document.body.removeChild(textArea); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/assets/images/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ruler.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/images/profit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/images/alert-sbl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/images/private.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/images/payment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/images/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/images/chain-fees.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/images/weekly.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/pages/add-data/add-data.scss: -------------------------------------------------------------------------------- 1 | .add-data-textarea { 2 | margin-bottom: 5px; 3 | width:515px; 4 | resize: none; 5 | } 6 | 7 | textarea:focus { 8 | outline: 1px solid #EBEBEB; 9 | resize: none; 10 | } 11 | 12 | .save-data-button { 13 | width:516px; 14 | height: 40px; 15 | opacity: 50%; 16 | cursor: pointer; 17 | font: 300 16px Avenir, serif; 18 | } 19 | 20 | .save-data-button:hover { 21 | opacity:40%; 22 | } 23 | 24 | .error-container { 25 | padding-left: 150px; 26 | margin-top: -5px; 27 | margin: 10px 0px 8px 0px; 28 | } 29 | 30 | .success-container { 31 | padding-left: 215px; 32 | margin-top: -5px; 33 | margin: 10px 0px 8px 0px; 34 | } 35 | 36 | .title-alert-container { 37 | margin: 0px 0px 5px -5px; 38 | width: 500px; 39 | text-align: center; 40 | } 41 | 42 | .alert-container { 43 | margin: 0px 0px 20px 5px; 44 | width: 500px; 45 | text-align: center; 46 | } 47 | 48 | .saved-data-container { 49 | padding-bottom:5px; 50 | } 51 | 52 | .error-text { 53 | color: red; 54 | font: 300 16px Avenir, serif; 55 | } 56 | 57 | .success-text { 58 | color: #41cab7; 59 | font: 300 16px Avenir, serif; 60 | } 61 | 62 | .data-container { 63 | margin-left: -10px; 64 | } 65 | 66 | img { 67 | max-width: 100%; 68 | padding-top: 10px; 69 | } 70 | 71 | .forwards-icon { 72 | height: 20px; 73 | margin: 0 9px -5px 16px; 74 | } 75 | 76 | .keysends-icon { 77 | height: 20px; 78 | margin: 0 13px -5px 14px; 79 | } 80 | 81 | .chainfees-icon { 82 | height: 20px; 83 | margin: 0 12px -5px 13px; 84 | } 85 | 86 | .payments-icon { 87 | height: 20px; 88 | margin: 0 12px -5px 13px; 89 | } 90 | 91 | .wiggle-animation { 92 | animation: wiggle 1.5s infinite; 93 | animation-direction: alternate-reverse; 94 | } 95 | 96 | @keyframes wiggle { 97 | 10%, 90% { 98 | transform: translate(-1px, 0); 99 | } 100 | 101 | 20%, 80% { 102 | transform: translate(2px, 0); 103 | } 104 | 105 | 30%, 50%, 70% { 106 | transform: translate(-4px, 0); 107 | } 108 | 109 | 40%, 60% { 110 | transform: translate(4px, 0); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/assets/images/daily.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/images/count.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/amboss.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/images/monthly.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /src/assets/images/telegram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/images/forwards.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/menu/menu.html: -------------------------------------------------------------------------------- 1 | 70 | -------------------------------------------------------------------------------- /src/pages/add-data/add-data.ts: -------------------------------------------------------------------------------- 1 | import { Component, NgModule, OnInit } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { Storage } from '@ionic/storage'; 4 | import { Router } from '@angular/router'; 5 | 6 | import { Data } from 'src/providers/data'; 7 | import { CSVParser } from 'src/providers/csv-parser'; 8 | 9 | @Component({ 10 | selector: 'add-data', 11 | templateUrl: './add-data.html', 12 | styleUrls: ['./add-data.scss'] 13 | }) 14 | 15 | export class AddDataPage { 16 | 17 | isConfirmingClearData: boolean; 18 | showInvalidDataError: boolean; 19 | showSuccessAlert: boolean; 20 | showWiggleAnimation = { 21 | forwards: false, 22 | chainFees: false, 23 | payments: false, 24 | invoices: false 25 | }; 26 | 27 | constructor( 28 | public data: Data, 29 | private parser: CSVParser, 30 | private router: Router, 31 | private storage: Storage 32 | ) {} 33 | 34 | async saveData() { 35 | this.showInvalidDataError = false; 36 | this.showSuccessAlert = false; 37 | try { 38 | const data = (document.getElementById('bosTextArea')).value; 39 | await this.data.parseDataForChart(data); 40 | (document.getElementById('bosTextArea')).value = ''; 41 | this.selectMenuItems(); 42 | this.showWiggleAnimation[this.data.lastAddedChartType] = true; 43 | this.showSuccessAlert = true; 44 | setTimeout(() => { 45 | this.showWiggleAnimation[this.data.lastAddedChartType] = false; 46 | this.showSuccessAlert = false; 47 | }, 1500); 48 | } catch(err) { 49 | this.showInvalidDataError = true; 50 | setTimeout(() => this.showInvalidDataError = false, 3000) 51 | } 52 | } 53 | 54 | selectMenuItems() { 55 | this.data.menuItems.map((item) => { 56 | if (this.data[item.dataName]?.daily) { 57 | this.data.hasData = true; 58 | item.hasData = true; 59 | } 60 | }); 61 | if (this.data.forwards) { 62 | this.data.profit = this.parser.parseProfit(this.data.forwards, this.data.keysends, this.data.chainFees, this.data.rebalanceFees, this.data.lightningFees, this.data.payments); 63 | this.data.menuItems.map((item) => { 64 | if (item.title == 'Profit') { 65 | item.hasData = true; 66 | } 67 | }); 68 | } 69 | } 70 | 71 | confirmClearData() { 72 | this.showWiggleAnimation = { 73 | forwards: true, 74 | chainFees: true, 75 | payments: true, 76 | invoices: true 77 | } 78 | setTimeout(() => this.showWiggleAnimation = { 79 | forwards: false, 80 | chainFees: false, 81 | payments: false, 82 | invoices: false 83 | }, 1500) 84 | this.data.clearData(); 85 | this.data.clearStorage(); 86 | this.isConfirmingClearData = false; 87 | } 88 | 89 | exportTxt(fileName, data) { 90 | var link = document.createElement('a'); 91 | link.download = `${fileName}.txt`; 92 | var blob = new Blob([data], {type: 'text/plain'}); 93 | link.href = window.URL.createObjectURL(blob); 94 | link.click(); 95 | } 96 | 97 | goToPaymentsExcludeListPage() { 98 | this.router.navigate(['payments-exclude-list']); 99 | } 100 | 101 | goToKeysendsExcludeListPage() { 102 | this.router.navigate(['keysends-exclude-list']); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/menu/menu.ts: -------------------------------------------------------------------------------- 1 | import { Component, NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { Router } from '@angular/router'; 4 | import { Storage } from '@ionic/storage'; 5 | 6 | import { Data } from 'src/providers/data'; 7 | 8 | @Component({ 9 | selector: 'menu', 10 | templateUrl: './menu.html', 11 | styleUrls: ['./menu.scss'] 12 | }) 13 | 14 | export class MenuComponent { 15 | 16 | constructor( 17 | public data: Data, 18 | public router: Router, 19 | private storage: Storage 20 | ) {} 21 | 22 | async selectChart(chart) { 23 | if (!chart.hasData) { 24 | return; 25 | } 26 | this.data.selectedChartName = chart.title; 27 | if (!this.data.selectedFrequency) this.data.selectedFrequency = 'weekly'; 28 | if (!this.data.selectedFilter || !this.chartHasSelectedFilter()) this.data.selectedFilter = 'sats'; 29 | this.data.selectedChart = this.data[chart.dataName][this.data.selectedFrequency || 'weekly'][this.data.selectedFilter || 'count']; 30 | this.data.menuItems.map((chart) => chart.isSelected = false); 31 | chart.isSelected = true; 32 | this.router.navigate(['']); 33 | } 34 | 35 | chartHasSelectedFilter() { 36 | let hasFilter = false; 37 | this.data.filterMenuItems[this.data.selectedChartName].map((item) => { 38 | if (item.filter == this.data.selectedFilter) { 39 | hasFilter = true; 40 | } 41 | }); 42 | return hasFilter; 43 | } 44 | 45 | async selectFrequency(frequency) { 46 | if (!this.data.hasData) { 47 | return; 48 | } 49 | this.data.selectedFrequency = frequency; 50 | if (!this.hasChartSelected()) this.data.selectDefaultChartInMenu(false, true); 51 | const selectedChart = this.getSelectedChart(this.data.selectedChartName); 52 | this.data.selectedChart = selectedChart[frequency][this.data.selectedFilter || 'count']; 53 | this.router.navigate(['']); 54 | } 55 | 56 | async selectFilter(filter) { 57 | if (!this.data.hasData) { 58 | return; 59 | } 60 | this.data.selectedFilter = filter; 61 | if (!this.hasChartSelected()) this.data.selectDefaultChartInMenu(true, false); 62 | const selectedChart = this.getSelectedChart(this.data.selectedChartName); 63 | this.data.selectedChart = selectedChart[this.data.selectedFrequency || 'weekly'][filter]; 64 | this.router.navigate(['']); 65 | } 66 | 67 | hasChartSelected() { 68 | let hasChartSelected = false; 69 | this.data.menuItems.map((item) => { 70 | if (item.isSelected) { 71 | hasChartSelected = true; 72 | } 73 | }); 74 | return hasChartSelected; 75 | } 76 | 77 | getSelectedChart(title) { 78 | const types = { 79 | 'Forwards': this.data.forwards, 80 | 'Chain Fees': this.data.chainFees, 81 | 'Rebalance Fees': this.data.rebalanceFees, 82 | 'Payments': this.data.payments, 83 | 'Lightning Fees': this.data.lightningFees, 84 | 'Keysends': this.data.keysends, 85 | 'Profit': this.data.profit 86 | } 87 | return types[title]; 88 | } 89 | 90 | goToAddDataPage() { 91 | this.data.menuItems.map((item) => item.isSelected = false); 92 | this.data.selectedFrequency = ''; 93 | this.data.selectedFilter = ''; 94 | this.router.navigate(['add-data']); 95 | } 96 | 97 | goToColdSatsPage() { 98 | this.data.menuItems.map((item) => item.isSelected = false); 99 | this.data.selectedFrequency = ''; 100 | this.data.selectedFilter = ''; 101 | this.router.navigate(['cold-sats']); 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ln-charts": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/ln-charts", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "inlineStyleLanguage": "scss", 26 | "assets": [ 27 | "src/favicon.ico", 28 | "src/assets" 29 | ], 30 | "styles": [ 31 | "src/styles.scss" 32 | ], 33 | "scripts": [] 34 | }, 35 | "configurations": { 36 | "production": { 37 | "budgets": [ 38 | { 39 | "type": "initial", 40 | "maximumWarning": "500kb", 41 | "maximumError": "1mb" 42 | }, 43 | { 44 | "type": "anyComponentStyle", 45 | "maximumWarning": "2kb", 46 | "maximumError": "1mb" 47 | } 48 | ], 49 | "fileReplacements": [ 50 | { 51 | "replace": "src/environments/environment.ts", 52 | "with": "src/environments/environment.prod.ts" 53 | } 54 | ], 55 | "outputHashing": "all" 56 | }, 57 | "development": { 58 | "buildOptimizer": false, 59 | "optimization": false, 60 | "vendorChunk": true, 61 | "extractLicenses": false, 62 | "sourceMap": true, 63 | "namedChunks": true 64 | } 65 | }, 66 | "defaultConfiguration": "production" 67 | }, 68 | "serve": { 69 | "builder": "@angular-devkit/build-angular:dev-server", 70 | "configurations": { 71 | "production": { 72 | "browserTarget": "ln-charts:build:production" 73 | }, 74 | "development": { 75 | "browserTarget": "ln-charts:build:development" 76 | } 77 | }, 78 | "defaultConfiguration": "development" 79 | }, 80 | "extract-i18n": { 81 | "builder": "@angular-devkit/build-angular:extract-i18n", 82 | "options": { 83 | "browserTarget": "ln-charts:build" 84 | } 85 | }, 86 | "test": { 87 | "builder": "@angular-devkit/build-angular:karma", 88 | "options": { 89 | "main": "src/test.ts", 90 | "polyfills": "src/polyfills.ts", 91 | "tsConfig": "tsconfig.spec.json", 92 | "karmaConfig": "karma.conf.js", 93 | "inlineStyleLanguage": "scss", 94 | "assets": [ 95 | "src/favicon.ico", 96 | "src/assets" 97 | ], 98 | "styles": [ 99 | "src/styles.scss" 100 | ], 101 | "scripts": [] 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/pages/add-data/add-data.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 | Step 1: Run 8 | bos 9 | commands on your Lightning node 10 |
11 | 12 | bos accounting forwards/payments/chain-fees/invoices --month x --year y --csv
13 |
14 |
15 | 16 |
17 | 18 | Step 2: Copy and paste the entire ouput from bos below 19 |
20 | 21 | No need to clarify which report. Paste one report type at a time. 22 | 23 |
24 | 25 |
26 | 27 | 30 | 31 |
32 | Invalid data. See Github for help. 33 |
34 | 35 |
36 | Data Saved! 37 |
38 | 39 |
40 | 41 | Only you can see this data 42 |
43 | 44 |
45 | ln-charts uses 46 | Ionic Storage 47 | to save this data on your device. It will persist until you clear data below. View 48 | GitHub 49 | to learn more. 50 |
51 | 52 |
53 | Saved Data 54 | Clear Data 55 | 56 | 57 | Are you sure? 58 | 59 | 60 | Yes 61 | 62 | 63 | No 64 | 65 | 66 |
67 | 68 |
69 | 70 | Forwards: {{data?.rawForwardsLength.toLocaleString('en', {useGrouping:true})}} 71 | Export 72 |
73 | 74 |
75 | 76 | Chain Fees: {{data?.rawChainFeesLength.toLocaleString('en', {useGrouping:true})}} 77 | Export 78 |
79 | 80 |
81 | 82 | Payments: {{data?.rawPaymentsLength.toLocaleString('en', {useGrouping:true})}} 83 | Export 84 | Payments Exclude List 85 |
86 | 87 |
88 | 89 | Invoices: {{data?.rawInvoicesLength.toLocaleString('en', {useGrouping:true})}} 90 | Export 91 | Keysends Exclude List 92 |
93 | 94 |
95 | -------------------------------------------------------------------------------- /src/menu/menu-items.ts: -------------------------------------------------------------------------------- 1 | export const menuItems = [ 2 | { 3 | title: 'Profit', 4 | dataName: 'profit', 5 | icon: 'assets/images/profit.svg', 6 | isSelected: false, 7 | hasData: false 8 | }, 9 | { 10 | title: 'Forwards', 11 | dataName: 'forwards', 12 | icon: 'assets/images/forwards.svg', 13 | isSelected: false, 14 | hasData: false 15 | }, 16 | { 17 | title: 'Keysends', 18 | dataName: 'keysends', 19 | icon: 'assets/images/keysends.svg', 20 | isSelected: false, 21 | hasData: false 22 | }, 23 | { 24 | title: 'Payments', 25 | dataName: 'payments', 26 | icon: 'assets/images/payments.svg', 27 | isSelected: false, 28 | hasData: false 29 | }, 30 | { 31 | title: 'Chain Fees', 32 | dataName: 'chainFees', 33 | icon: 'assets/images/chain-fees.svg', 34 | isSelected: false, 35 | hasData: false 36 | }, 37 | { 38 | title: 'Rebalance Fees', 39 | dataName: 'rebalanceFees', 40 | icon: 'assets/images/rebalance.svg', 41 | isSelected: false, 42 | hasData: false 43 | }, 44 | { 45 | title: 'Lightning Fees', 46 | dataName: 'lightningFees', 47 | icon: 'assets/images/lightning-fees.svg', 48 | isSelected: false, 49 | hasData: false 50 | } 51 | ]; 52 | 53 | export const filterMenuItems = { 54 | 'Profit': [ 55 | { 56 | title: 'Net', 57 | filter: 'sats', 58 | icon: 'assets/images/sats.svg' 59 | }, 60 | { 61 | title: 'Cumulative', 62 | filter: 'cumulative', 63 | icon: 'assets/images/cumulative.svg' 64 | } 65 | ], 66 | 'Forwards': [ 67 | { 68 | title: 'Sats Earned', 69 | filter: 'sats', 70 | icon: 'assets/images/sats.svg' 71 | }, 72 | { 73 | title: 'Count', 74 | filter: 'count', 75 | icon: 'assets/images/count.svg' 76 | }, 77 | { 78 | title: 'Avg. Size', 79 | filter: 'routeSize', 80 | icon: 'assets/images/ruler.svg' 81 | }, 82 | { 83 | title: 'Avg. PPM', 84 | filter: 'avgPPM', 85 | icon: 'assets/images/divide.svg' 86 | }, 87 | { 88 | title: 'Avg. Earning', 89 | filter: 'average', 90 | icon: 'assets/images/average.svg' 91 | }, 92 | { 93 | title: 'Sats Routed', 94 | filter: 'amountRouted', 95 | icon: 'assets/images/sats-routed.svg' 96 | }, 97 | ], 98 | 'Keysends': [ 99 | { 100 | title: 'Sats Earned', 101 | filter: 'sats', 102 | icon: 'assets/images/sats.svg' 103 | }, 104 | { 105 | title: 'Count', 106 | filter: 'count', 107 | icon: 'assets/images/count.svg' 108 | }, 109 | { 110 | title: 'Avg. Size', 111 | filter: 'average', 112 | icon: 'assets/images/ruler.svg' 113 | } 114 | ], 115 | 'Payments': [ 116 | { 117 | title: 'Sats Sent', 118 | filter: 'sats', 119 | icon: 'assets/images/sats.svg' 120 | }, 121 | { 122 | title: 'Count', 123 | filter: 'count', 124 | icon: 'assets/images/count.svg' 125 | }, 126 | { 127 | title: 'Avg. Size', 128 | filter: 'average', 129 | icon: 'assets/images/ruler.svg' 130 | } 131 | ], 132 | 'Chain Fees': [ 133 | { 134 | title: 'Sats Spent', 135 | filter: 'sats', 136 | icon: 'assets/images/sats.svg' 137 | }, 138 | { 139 | title: 'Count', 140 | filter: 'count', 141 | icon: 'assets/images/count.svg' 142 | }, 143 | { 144 | title: 'Avg. Size', 145 | filter: 'average', 146 | icon: 'assets/images/ruler.svg' 147 | } 148 | ], 149 | 'Rebalance Fees': [ 150 | { 151 | title: 'Sats Spent', 152 | filter: 'sats', 153 | icon: 'assets/images/sats.svg' 154 | }, 155 | { 156 | title: 'Count', 157 | filter: 'count', 158 | icon: 'assets/images/count.svg' 159 | }, 160 | { 161 | title: 'Avg. Size', 162 | filter: 'average', 163 | icon: 'assets/images/ruler.svg' 164 | } 165 | ], 166 | 'Lightning Fees': [ 167 | { 168 | title: 'Sats Spent', 169 | filter: 'sats', 170 | icon: 'assets/images/sats.svg' 171 | }, 172 | { 173 | title: 'Count', 174 | filter: 'count', 175 | icon: 'assets/images/count.svg' 176 | }, 177 | { 178 | title: 'Avg. Size', 179 | filter: 'average', 180 | icon: 'assets/images/ruler.svg' 181 | } 182 | ] 183 | } 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | image 2 | 3 | # ln-charts 4 | 5 | ln-charts parses the output of bos accounting commands into various charts for your Lightning Node. It runs on Angular, JS, HTML, CSS, ngx-charts, Ionic Storage and Luxon. 6 | 7 | You must have [bos](https://github.com/alexbosworth/balanceofsatoshis) which runs on [lnd](https://github.com/lightningnetwork/lnd) to use this version of ln-charts. 8 | 9 | You can run ln-charts locally or access at https://cold-sats.github.io/ln-charts/. 10 | 11 | ## Data Storage 12 | 13 | ln-charts stores your data locally in your browser using [Ionic Storage](https://ionicframework.com/docs/angular/storage). Only you can see the data you upload. It will persist in your browser until you remove it using the "Clear Data" button in the UI. 14 | 15 | ## How to Build Charts 16 | 17 | **Step 1**: Run bos commands on your Lightning node. 18 | 19 | ``` 20 | bos accounting forwards --month x --year -y --csv 21 | bos accounting chain-fees --month x --year -y --csv 22 | bos accounting payments --month x --year -y --csv 23 | bos accounting invoices --month x --year -y --csv 24 | ``` 25 | 26 | You can add the `--disable-fiat` flag if you don't want bos to calculate the fiat values for your reports. ln-charts does not use this fiat value and this will speed things up. 27 | 28 | **Step 2**: Copy and paste the output into ln-charts interface. 29 | 30 | Highlight the beginning of the text, scroll a little so scrollbar appears, grab the scrollbar and drag to the bottom (or scroll with the mouse wheel). Then hold shift, tap the end of the text to highlight everything, and copy. 31 | 32 | Only paste one report type at a time into ln-charts. You can include the header at the beginning or not. It can take up to a minute to paste large amounts of text. Example report: 33 | 34 | ``` 35 | "Amount","Asset","Date & Time","Fiat Amount","From ID","Network ID","Notes","To ID","Transaction ID","Type" 36 | -378,"BTC","2022-04-04T17:38:45.000Z",-0.1755226480744611,"","","Channel close: 0:closechannel:shortchanid-794957902071791618 [Chain Fee]","","329f9f7767e4383546ef2942749554f0c1e230f8544db2992261e53d2ec8f365:fee","fee:network" 37 | -292,"BTC","2022-04-04T19:52:57.000Z",-0.13558892390937208,"","","0:openchannel:shortchanid-803136069653233665 [Chain Fee]","","d3e69380718d54bfea8bcf25780fec7e9d2406a2617f044e6c5b51d06fa48a3c:fee","fee:network" 38 | ``` 39 | 40 | ## Parsing 41 | 42 | ln-charts automatically determines the type of report entered and parses it into multiple charts: 43 | 44 | **Forwards** get parsed into: 45 | - Forwards - sats earned, count, avg. earning, avg. ppm, avg. earning, sats routed 46 | 47 | **Chain Fees** get parsed into: 48 | - Chain Fees - sats spent, count, avg. size 49 | 50 | **Payments** get parsed into: 51 | - Payments - sats sent, count, avg. size 52 | - Rebalance Fees - sats sent, count, avg. size 53 | - Lightning Fees (doesn't include rebalance fees) - sats sent, count, avg. size 54 | 55 | **Invoices** get parsed into: 56 | - Keysends - sats received, count, avg. size 57 | 58 | All charts are then summed into a profit chart: 59 | - Profit = forward earnings + keysends - chain fees - rebalance fees - lightning fees - payments 60 | 61 | ## Exclude Lists 62 | 63 | ln-charts has an exclude list for payments and keysends. Use it to filter out events you don't want to see in charts. 64 | 65 | **Payments Exclude List** 66 | 67 | Paste the text from the "Notes" column of the bos payments report. Example: ```Wallet of Satoshi``` 68 | 69 | **Keysends Exclude List** 70 | 71 | Paste the text from the "Amount" column of the bos invoices report. Example: ```2000000``` 72 | 73 | **How to Determine What to Exclude** 74 | 1. Paste the bos accounting reports into Google Sheets 75 | 2. Select the column -> Data -> Split text to columns 76 | 3. Select all columns -> Data -> Create filter 77 | 78 | Now you can filter the columns and browse much easier. Browse invoices report for keysends (noted as [Push Payment]). Then scan payments report for payments with unique notes. 79 | 80 | ## Running Locally 81 | 82 | Use Angular CLI to run ln-charts locally on your computer. The data you save will persist between sessions. 83 | 84 | Clone the repo and navigate to it: 85 | ``` 86 | git clone https://github.com/cold-sats/ln-charts 87 | cd ln-charts 88 | ``` 89 | 90 | Install and start npm (if you don't already have npm): 91 | ``` 92 | npm install 93 | npm start 94 | ``` 95 | 96 | Install Angular CLI globally: 97 | ``` 98 | npm install -g @angular/cli 99 | ``` 100 | 101 | Run on http://localhost:4200/#/: 102 | ``` 103 | ng serve --open 104 | ``` 105 | 106 | When running locally you have the option to save your data in the `bos-data` directory instead of inputting it into the UI and saving in cache. Only use one type of data storage at a time (either project files or inputting into UI). 107 | 108 | When running locally you can also save keysends / payments exclude list items in the `csv-parser` provider. 109 | 110 | ln-charts uses Google Analytics to track usage. If you prefer to not allow this, pull the `no-google-analytics` branch which does not include that. 111 | 112 | ## Backing Up Your Data 113 | 114 | I recommend maintaining a local backup of all of your bos accounting data (if you don't run ln-charts locally and save your data in the project folders). You can export stored data as a .txt using the "Export" button shown next to each report in the UI. 115 | 116 | ## Contributing 117 | 118 | Contributions are welcome! Let me know if you find bugs or have ideas. Create an issue or PR. 119 | 120 | If you enjoy ln-charts and want to donate please keysend: 121 | ``` 122 | bos send 020a3dce2dab038955eb435a8342e4fe897304015314485d3738d5f41eccb47859 --amount 1000 --message "Thanks for ln-charts!" 123 | ``` 124 | -------------------------------------------------------------------------------- /src/providers/data.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Storage } from '@ionic/storage'; 3 | 4 | import { CSVParser } from 'src/providers/csv-parser'; 5 | 6 | import { menuItems, filterMenuItems } from 'src/menu/menu-items'; 7 | 8 | import { chainFees } from 'src/bos-data/chain-fees'; 9 | import { forwards } from 'src/bos-data/forwards'; 10 | import { invoices } from 'src/bos-data/invoices'; 11 | import { payments } from 'src/bos-data/payments'; 12 | 13 | interface chartModel { 14 | name: string; 15 | value: number; 16 | } 17 | 18 | @Injectable() 19 | export class Data { 20 | 21 | loaded: boolean; 22 | lastAddedChartType: string; 23 | hasData: boolean; 24 | 25 | //Exclude lists (saved in storage) 26 | paymentsExcludeList = []; 27 | keysendsExcludeList = []; 28 | 29 | //Raw bos csv data (saved in storage) 30 | rawForwards: string; 31 | rawPayments: string; 32 | rawChainFees: string; 33 | rawInvoices: string; 34 | 35 | //Raw bos csv data (saved in project files) 36 | rawForwardsFile = forwards; 37 | rawPaymentsFile = payments; 38 | rawChainFeesFile = chainFees; 39 | rawInvoicesFile = invoices; 40 | 41 | //Length of raw bos data 42 | rawForwardsLength = 0; 43 | rawPaymentsLength = 0; 44 | rawChainFeesLength = 0; 45 | rawInvoicesLength = 0; 46 | 47 | //Formatted arrays for ngx charts 48 | profit: { 49 | daily: { 50 | sats: chartModel[]; 51 | cumulative: chartModel[]; 52 | }; 53 | weekly: { 54 | sats: chartModel[]; 55 | cumulative: chartModel[]; 56 | }; 57 | monthly: { 58 | sats: chartModel[]; 59 | cumulative: chartModel[]; 60 | }; 61 | }; 62 | chainFees: { 63 | daily: { 64 | average: chartModel[]; 65 | count: chartModel[]; 66 | sats: chartModel[]; 67 | }; 68 | weekly: { 69 | average: chartModel[]; 70 | count: chartModel[]; 71 | sats: chartModel[]; 72 | }; 73 | monthly: { 74 | average: chartModel[]; 75 | count: chartModel[]; 76 | sats: chartModel[]; 77 | }; 78 | }; 79 | forwards: { 80 | daily: { 81 | amountRouted: chartModel[]; 82 | average: chartModel[]; 83 | avgPPM: chartModel[]; 84 | count: chartModel[]; 85 | routeSize: chartModel[]; 86 | sats: chartModel[]; 87 | }; 88 | weekly: { 89 | amountRouted: chartModel[]; 90 | average: chartModel[]; 91 | avgPPM: chartModel[]; 92 | count: chartModel[]; 93 | routeSize: chartModel[]; 94 | sats: chartModel[]; 95 | }; 96 | monthly: { 97 | amountRouted: chartModel[]; 98 | average: chartModel[]; 99 | avgPPM: chartModel[]; 100 | count: chartModel[]; 101 | routeSize: chartModel[]; 102 | sats: chartModel[]; 103 | }; 104 | }; 105 | rebalanceFees: { 106 | daily: { 107 | average: chartModel[]; 108 | count: chartModel[]; 109 | sats: chartModel[]; 110 | }; 111 | weekly: { 112 | average: chartModel[]; 113 | count: chartModel[]; 114 | sats: chartModel[]; 115 | }; 116 | monthly: { 117 | average: chartModel[]; 118 | count: chartModel[]; 119 | sats: chartModel[]; 120 | }; 121 | }; 122 | payments: { 123 | daily: { 124 | average: chartModel[]; 125 | count: chartModel[]; 126 | sats: chartModel[]; 127 | }; 128 | weekly: { 129 | average: chartModel[]; 130 | count: chartModel[]; 131 | sats: chartModel[]; 132 | }; 133 | monthly: { 134 | average: chartModel[]; 135 | count: chartModel[]; 136 | sats: chartModel[]; 137 | }; 138 | }; 139 | lightningFees: { 140 | daily: { 141 | average: chartModel[]; 142 | count: chartModel[]; 143 | sats: chartModel[]; 144 | }; 145 | weekly: { 146 | average: chartModel[]; 147 | count: chartModel[]; 148 | sats: chartModel[]; 149 | }; 150 | monthly: { 151 | average: chartModel[]; 152 | count: chartModel[]; 153 | sats: chartModel[]; 154 | }; 155 | }; 156 | keysends: { 157 | daily: { 158 | average: chartModel[]; 159 | count: chartModel[]; 160 | sats: chartModel[]; 161 | }; 162 | weekly: { 163 | average: chartModel[]; 164 | count: chartModel[]; 165 | sats: chartModel[]; 166 | }; 167 | monthly: { 168 | average: chartModel[]; 169 | count: chartModel[]; 170 | sats: chartModel[]; 171 | }; 172 | }; 173 | 174 | //Menu items 175 | menuItems = menuItems; 176 | filterMenuItems = filterMenuItems; 177 | 178 | //Current Menu Selections 179 | selectedChart: any; 180 | selectedChartName: string; 181 | selectedFrequency: string; 182 | selectedFilter: string; 183 | 184 | constructor( 185 | private parser: CSVParser, 186 | private storage: Storage 187 | ) {} 188 | 189 | async loadData() { 190 | this.clearData(); 191 | this.loaded = false; 192 | this.paymentsExcludeList = await this.storage.get('paymentsExcludeList') || []; 193 | this.keysendsExcludeList = await this.storage.get('keysendsExcludeList') || []; 194 | await this.parseRawData('rawForwards'); 195 | await this.parseRawData('rawPayments'); 196 | await this.parseRawData('rawChainFees'); 197 | await this.parseRawData('rawInvoices'); 198 | if (this.rawForwards) { 199 | this.profit = this.parser.parseProfit(this.forwards, this.keysends, this.chainFees, this.rebalanceFees, this.lightningFees, this.payments); 200 | } 201 | this.populateMenuWithData(); 202 | if (this.hasData && !this.selectedChart) { 203 | this.selectDefaultChartInMenu(true, true); 204 | } 205 | this.loaded = true; 206 | } 207 | 208 | async parseRawData(rawDataName) { 209 | const data = await this.storage.get(rawDataName); 210 | if (data) { 211 | this.parseDataForChart(data); 212 | } else if (this[rawDataName+'File'].length > 1) { 213 | this.parseDataForChart(this[rawDataName+'File']); 214 | } 215 | } 216 | 217 | parseDataForChart(data) { 218 | const rawArray = this.parser.parseRawDataIntoArray(data); 219 | const type = this.getReportType(rawArray); 220 | switch (type) { 221 | case 'forwards': 222 | this.forwards = this.parser.parseRawArrayIntoChartArray('forwards', rawArray, this.paymentsExcludeList, this.keysendsExcludeList); 223 | return this.saveRawData('forwards', 'rawForwards', rawArray, data); 224 | case 'invoices': 225 | this.keysends = this.parser.parseRawArrayIntoChartArray('keysends', rawArray, this.paymentsExcludeList, this.keysendsExcludeList); 226 | return this.saveRawData('invoices', 'rawInvoices', rawArray, data); 227 | case 'chainFees': 228 | this.chainFees = this.parser.parseRawArrayIntoChartArray('chain-fees', rawArray, this.paymentsExcludeList, this.keysendsExcludeList); 229 | return this.saveRawData('chainFees', 'rawChainFees', rawArray, data); 230 | case 'payments': 231 | this.rebalanceFees = this.parser.parseRawArrayIntoChartArray('rebalance-fees', rawArray, this.paymentsExcludeList, this.keysendsExcludeList); 232 | this.lightningFees = this.parser.parseRawArrayIntoChartArray('lightning-fees', rawArray, this.paymentsExcludeList, this.keysendsExcludeList); 233 | this.payments = this.parser.parseRawArrayIntoChartArray('payments', rawArray, this.paymentsExcludeList, this.keysendsExcludeList); 234 | return this.saveRawData('payments', 'rawPayments', rawArray, data); 235 | } 236 | } 237 | 238 | getReportType(array) { 239 | if (array[0][8] == `""`) { 240 | return 'forwards'; 241 | } else if (array[0][9] == '"income"') { 242 | return 'invoices'; 243 | } 244 | let isOnChainFees = true; 245 | array.map((item) => { 246 | if (!item[9]?.includes('"fee:network"') && item[9] !== undefined) { 247 | isOnChainFees = false; 248 | } 249 | }); 250 | if (isOnChainFees) { 251 | return 'chainFees' 252 | } else { 253 | return 'payments' 254 | } 255 | } 256 | 257 | saveRawData(lastAddedType, type, array, csv) { 258 | if (!this[type]) { 259 | this[type] = csv; 260 | } else { 261 | this[type] += ` 262 | ${csv}`; 263 | } 264 | this.storage.set(type, this[type]); 265 | this[type + 'Length'] += array.length; 266 | this.lastAddedChartType = lastAddedType; 267 | } 268 | 269 | populateMenuWithData() { 270 | this.menuItems.map((item) => { 271 | if (this[item.dataName]) { 272 | this.hasData = true; 273 | item.hasData = true; 274 | } else { 275 | item.hasData = false; 276 | } 277 | }); 278 | if (this.forwards) { 279 | this.menuItems.map((item) => { 280 | if (item.title == 'Profit') { 281 | item.hasData = true; 282 | } 283 | }); 284 | } 285 | } 286 | 287 | selectDefaultChartInMenu(shouldSelectFrequency, shouldSelectFilter) { 288 | let didSelectItem = false; 289 | this.menuItems.map((item) => { 290 | if (item.hasData && !didSelectItem) { 291 | this.selectedChartName = item.title; 292 | this.selectedChart = this[item.dataName].weekly.sats; 293 | item.isSelected = true; 294 | didSelectItem = true; 295 | } 296 | }); 297 | if (shouldSelectFrequency) this.selectedFrequency = 'weekly'; 298 | if (shouldSelectFilter) this.selectedFilter = 'sats'; 299 | } 300 | 301 | clearData() { 302 | this.profit = null; 303 | this.chainFees = null; 304 | this.forwards = null; 305 | this.rebalanceFees = null; 306 | this.payments = null; 307 | this.lightningFees = null; 308 | this.keysends = null; 309 | this.rawForwards = null; 310 | this.rawPayments = null; 311 | this.rawChainFees = null; 312 | this.rawInvoices = null; 313 | this.rawForwardsLength = 0; 314 | this.rawInvoicesLength = 0; 315 | this.rawChainFeesLength = 0; 316 | this.rawPaymentsLength = 0; 317 | this.menuItems.map((item) => item.hasData = false); 318 | this.hasData = false; 319 | } 320 | 321 | clearStorage() { 322 | this.storage.clear(); 323 | } 324 | 325 | } 326 | -------------------------------------------------------------------------------- /src/providers/csv-parser.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { DateTime } from 'luxon'; 3 | 4 | @Injectable() 5 | export class CSVParser { 6 | 7 | //This parser builds a day, week, and month array for each chart type 8 | dayChart = []; 9 | weekChart = []; 10 | monthChart = []; 11 | 12 | //By default payments disclude rebalance fees and routing fees (which are built as separate charts) 13 | defaultPaymentsExcludeList = [ 14 | '\"Circular payment routing fee\"', 15 | '\"Routing fee\"' 16 | ]; 17 | 18 | defaultKeysendsExcludeList = [ 19 | ]; 20 | 21 | parseRawDataIntoArray(data, delimiter = ',', omitFirstRow = false) { 22 | let arrays = data 23 | .slice(omitFirstRow ? data.indexOf('\n') + 1 : 0) 24 | .split('\n') 25 | .map((v) => v.split(delimiter)); 26 | let array = []; 27 | arrays.map((item) => { 28 | if (!item[0].includes('"Amount"') && item.length > 1) { 29 | array.push(item); 30 | } 31 | }); 32 | return array; 33 | } 34 | 35 | parseRawArrayIntoChartArray(subsetToParse, rawArray, paymentsExcludeList, keysendsExcludeList) { 36 | this.parseByDayWeekMonth(rawArray, subsetToParse, paymentsExcludeList, keysendsExcludeList); 37 | this.addMissingEmptyDays(); 38 | this.addMissingEmptyWeeks(); 39 | this.addMissingEmptyMonths(); 40 | const chartArray = this.parseFinalData(subsetToParse); 41 | this.clearProviderData(); 42 | return chartArray; 43 | } 44 | 45 | parseByDayWeekMonth(rawArray, subsetToParse, paymentsExcludeList, keysendsExcludeList) { 46 | let array = []; 47 | rawArray.map((row) => { 48 | if (this.isCorrectSubsetToParse(row, subsetToParse, paymentsExcludeList, keysendsExcludeList)) { 49 | array.push({ 50 | jsDate: this.getJsDate(row[2]), 51 | luxonDate: this.getLuxonDate(row[2]), 52 | day: this.getDay(this.getLuxonDate(row[2])), 53 | amount: parseInt(row[0]), 54 | routeSize: parseInt(row[6].replace('"', '')) 55 | }); 56 | } 57 | }); 58 | array = array.sort((a, b) => a.jsDate - b.jsDate); 59 | array.map((row) => { 60 | this.parseIntoDayChart(row.luxonDate, row.day, row.amount, row.routeSize); 61 | this.parseIntoWeekChart(row.luxonDate, row.amount, row.routeSize); 62 | this.parseIntoMonthChart(row.luxonDate, row.amount, row.routeSize); 63 | }); 64 | } 65 | 66 | isCorrectSubsetToParse(row, subsetToParse, paymentsExcludeList, keysendsExcludeList) { 67 | if (subsetToParse.includes('rebalance-fees')) { 68 | return row[6] == '\"Circular payment routing fee\"'; 69 | } else if (subsetToParse.includes('lightning-fees')) { 70 | return row[6] == '\"Routing fee\"'; 71 | } else if (subsetToParse.includes('payments')) { 72 | return !this.isInPaymentsExcludeList(row[6], paymentsExcludeList); 73 | } else if (subsetToParse.includes('keysends')) { 74 | return row[6] == '\"[Push Payment]\"' && !this.isInKeysendsExcludeList(row[0], keysendsExcludeList);; 75 | } else { 76 | return true; 77 | } 78 | } 79 | 80 | isInPaymentsExcludeList(item, paymentsExcludeList) { 81 | const isInExcludeList = this.defaultPaymentsExcludeList.includes(item) || paymentsExcludeList?.includes(item); 82 | const isSelfPayment = item.indexOf('[To Self]') !== -1; 83 | return isInExcludeList || isSelfPayment; 84 | } 85 | 86 | isInKeysendsExcludeList(item, keysendsExcludeList) { 87 | return this.defaultKeysendsExcludeList.includes(item) || keysendsExcludeList?.includes(item); 88 | } 89 | 90 | parseIntoDayChart(luxonDate, day, amount, routeSize) { 91 | const dayAlreadyAdded = this.dayChart.find(item => item.name == day); 92 | if (dayAlreadyAdded) { 93 | this.dayChart[this.dayChart.length - 1].amounts.push(amount); 94 | this.dayChart[this.dayChart.length - 1].routeSize.push(routeSize); 95 | } else { 96 | this.dayChart.push({ 97 | luxonDate: luxonDate, 98 | name: day, 99 | amounts: [ amount ], 100 | routeSize: [ routeSize ] 101 | }); 102 | } 103 | } 104 | 105 | parseIntoWeekChart(luxonDate, amount, routeSize) { 106 | const weekAlreadyAdded = this.weekChart.find(item => item.luxonDate.weekNumber.toString() + item.luxonDate.year == luxonDate.weekNumber.toString() + luxonDate.year); 107 | if (weekAlreadyAdded) { 108 | this.weekChart[this.weekChart.length - 1].amounts.push(amount); 109 | this.weekChart[this.weekChart.length - 1].routeSize.push(routeSize); 110 | } else { 111 | this.weekChart.push({ 112 | luxonDate: luxonDate, 113 | name: `Week ${luxonDate.weekNumber} ${luxonDate.year}`, 114 | amounts: [ amount ], 115 | routeSize: [ routeSize ], 116 | weekNumber: luxonDate.weekNumber 117 | }); 118 | } 119 | } 120 | 121 | parseIntoMonthChart(luxonDate, amount, routeSize) { 122 | const monthAlreadyAdded = this.monthChart.find(item => item.luxonDate.monthLong + item.luxonDate.year == luxonDate.monthLong + luxonDate.year); 123 | if (monthAlreadyAdded) { 124 | this.monthChart[this.monthChart.length - 1].amounts.push(amount); 125 | this.monthChart[this.monthChart.length - 1].routeSize.push(routeSize); 126 | } else { 127 | this.monthChart.push({ 128 | luxonDate: luxonDate, 129 | name: `${luxonDate.monthLong} ${luxonDate.year}`, 130 | amounts: [ amount ], 131 | routeSize: [ routeSize ], 132 | monthNumber: luxonDate.month 133 | }); 134 | } 135 | } 136 | 137 | addMissingEmptyDays() { 138 | let array = []; 139 | this.dayChart.map((day, index) => { 140 | array.push(day); 141 | if (this.dayChart[index+1]) { 142 | const nextDay = day.luxonDate.plus({ days: 1 }); 143 | const isMissingDays = this.getDay(nextDay) !== this.getDay(this.dayChart[index+1].luxonDate); 144 | if (isMissingDays) { 145 | const differenceBetweenNextDay = day.luxonDate.diff(this.dayChart[index+1].luxonDate, 'days').days; 146 | const numberOfMissingDays = Math.ceil(differenceBetweenNextDay) * -1; 147 | for (let i = 0; i < numberOfMissingDays - 1; i++) { 148 | const date = day.luxonDate.plus({ days: (i+1) }); 149 | const missingDay = { 150 | name: this.getDay(date), 151 | amounts: [] 152 | }; 153 | array.push(missingDay); 154 | } 155 | } 156 | } 157 | }); 158 | this.dayChart = array; 159 | } 160 | 161 | addMissingEmptyWeeks() { 162 | const array: any = []; 163 | this.weekChart.map((week, i) => { 164 | array.push(week); 165 | if (this.weekChart[i+1]) { 166 | const isMissingWeek = week.weekNumber + 1 !== this.weekChart[i+1].weekNumber; 167 | const isEndOfYear = week.weekNumber > this.weekChart[i+1].weekNumber && this.weekChart[i+1].weekNumber !== 1; 168 | if (isMissingWeek || isEndOfYear) { 169 | const numberOfMissingWeeks = isEndOfYear ? 170 | 52 - week.weekNumber + this.weekChart[i+1].weekNumber - 1 : 171 | this.weekChart[i+1].weekNumber - week.weekNumber - 1; 172 | for (let i = 0; i < numberOfMissingWeeks; i++) { 173 | const luxonDate = week.luxonDate.plus({ weeks: (i+1) }); 174 | const missingWeek = { 175 | name: `Week ${luxonDate.weekNumber} ${luxonDate.year}`, 176 | amounts: [], 177 | luxonDate: luxonDate //used for profit parsing 178 | }; 179 | array.push(missingWeek); 180 | } 181 | } 182 | } 183 | }); 184 | this.weekChart = array; 185 | } 186 | 187 | addMissingEmptyMonths() { 188 | const array: any = []; 189 | this.monthChart.map((month, i) => { 190 | array.push(month); 191 | if (this.monthChart[i+1]) { 192 | const isMissingMonth = month.monthNumber + 1 !== this.monthChart[i+1].monthNumber; 193 | const isEndOfYear = month.monthNumber > this.monthChart[i+1].monthNumber && this.monthChart[i+1].monthNumber !== 2; 194 | if (isMissingMonth || isEndOfYear) { 195 | const numberOfMissingMonths = isEndOfYear ? 196 | 12 - month.monthNumber + this.monthChart[i+1].monthNumber - 1 : 197 | this.monthChart[i+1].monthNumber - month.monthNumber - 1; 198 | for (let i = 0; i < numberOfMissingMonths; i++) { 199 | const luxonDate = month.luxonDate.plus({ months: (i+1) }); 200 | const missingMonth = { 201 | name: `${luxonDate.monthLong} ${luxonDate.year}`, 202 | amounts: [] 203 | }; 204 | array.push(missingMonth); 205 | } 206 | } 207 | } 208 | }); 209 | this.monthChart = array; 210 | } 211 | 212 | parseFinalData(subsetToParse): any { 213 | let data = { 214 | daily: { 215 | sats: this.dayChart.map((day) => { 216 | return { 217 | name: day.name, 218 | value: this.getChartValue(day, 'sats') 219 | } 220 | }), 221 | count: this.dayChart.map((day) => { 222 | return { 223 | name: day.name, 224 | value: this.getChartValue(day, 'count') 225 | } 226 | }), 227 | average: this.dayChart.map((day) => { 228 | return { 229 | name: day.name, 230 | value: this.getChartValue(day, 'average') 231 | } 232 | }) 233 | }, 234 | weekly: { 235 | sats: this.weekChart.map((week) => { 236 | return { 237 | name: week.name, 238 | value: this.getChartValue(week, 'sats'), 239 | luxonDate: week.luxonDate //used for profit parsing 240 | } 241 | }), 242 | count: this.weekChart.map((week) => { 243 | return { 244 | name: week.name, 245 | value: this.getChartValue(week, 'count') 246 | } 247 | }), 248 | average: this.weekChart.map((week) => { 249 | return { 250 | name: week.name, 251 | value: this.getChartValue(week, 'average') 252 | } 253 | }) 254 | }, 255 | monthly: { 256 | sats: this.monthChart.map((month) => { 257 | return { 258 | name: month.name, 259 | value: this.getChartValue(month, 'sats') 260 | } 261 | }), 262 | count: this.monthChart.map((month) => { 263 | return { 264 | name: month.name, 265 | value: this.getChartValue(month, 'count') 266 | } 267 | }), 268 | average: this.monthChart.map((month) => { 269 | return { 270 | name: month.name, 271 | value: this.getChartValue(month, 'average') 272 | } 273 | }) 274 | } 275 | }; 276 | if (subsetToParse == 'forwards') { 277 | data = this.addMoreForwardsFilters(data); 278 | } 279 | return data; 280 | } 281 | 282 | getChartValue(array, subsetToParse) { 283 | if (subsetToParse.includes('sats')) { 284 | return Math.abs(array.amounts.reduce((a, b) => a + b, 0)); 285 | } else if (subsetToParse.includes('count')) { 286 | return array.amounts.length; 287 | } else if (subsetToParse.includes('average')) { 288 | let total = 0; 289 | array.amounts.map((item) => total += item); 290 | return Math.round(Math.abs(total / array.amounts.length)) || 0; 291 | } 292 | } 293 | 294 | addMoreForwardsFilters(chartArray) { 295 | chartArray.daily['routeSize'] = this.dayChart.map((day) => { 296 | let total = 0; 297 | day?.routeSize?.map((routeSize) => total += routeSize); 298 | const avgRouteSize = Math.round(Math.abs(total / day?.routeSize?.length)); 299 | return { 300 | name: day.name, 301 | value: avgRouteSize || 0 302 | } 303 | }); 304 | chartArray.weekly['routeSize'] = this.weekChart.map((week) => { 305 | let total = 0; 306 | week?.routeSize?.map((routeSize) => total += routeSize); 307 | const avgRouteSize = Math.round(Math.abs(total / week?.routeSize?.length)); 308 | return { 309 | name: week.name, 310 | value: avgRouteSize || 0 311 | } 312 | }); 313 | chartArray.monthly['routeSize'] = this.monthChart.map((month) => { 314 | let total = 0; 315 | month?.routeSize?.map((routeSize) => total += routeSize); 316 | const avgRouteSize = Math.round(Math.abs(total / month?.routeSize?.length)); 317 | return { 318 | name: month.name, 319 | value: avgRouteSize || 0 320 | } 321 | }); 322 | chartArray.daily['amountRouted'] = this.dayChart.map((day) => { 323 | let total = 0; 324 | day?.routeSize?.map((routeSize) => total += routeSize); 325 | return { 326 | name: day.name, 327 | value: total 328 | } 329 | }); 330 | chartArray.weekly['amountRouted'] = this.weekChart.map((week) => { 331 | let total = 0; 332 | week?.routeSize?.map((routeSize) => total += routeSize); 333 | return { 334 | name: week.name, 335 | value: total 336 | } 337 | }); 338 | chartArray.monthly['amountRouted'] = this.monthChart.map((month) => { 339 | let total = 0; 340 | month?.routeSize?.map((routeSize) => total += routeSize); 341 | return { 342 | name: month.name, 343 | value: total 344 | } 345 | }); 346 | chartArray.daily['avgPPM'] = this.dayChart.map((day) => { 347 | let totalRouted = 0; 348 | day?.routeSize?.map((routeSize) => totalRouted += routeSize); 349 | let totalEarned = 0; 350 | day.amounts.map((earned) => totalEarned += earned); 351 | const avgPPM = Math.round((totalEarned / totalRouted)*1000000); 352 | return { 353 | name: day.name, 354 | value: avgPPM || 0 355 | } 356 | }); 357 | chartArray.weekly['avgPPM'] = this.weekChart.map((week) => { 358 | let totalRouted = 0; 359 | week?.routeSize?.map((routeSize) => totalRouted += routeSize); 360 | let totalEarned = 0; 361 | week.amounts.map((earned) => totalEarned += earned); 362 | const avgPPM = Math.round((totalEarned / totalRouted)*1000000); 363 | return { 364 | name: week.name, 365 | value: avgPPM || 0 366 | } 367 | }); 368 | chartArray.monthly['avgPPM'] = this.monthChart.map((month) => { 369 | let totalRouted = 0; 370 | month?.routeSize?.map((routeSize) => totalRouted += routeSize); 371 | let totalEarned = 0; 372 | month.amounts.map((earned) => totalEarned += earned); 373 | const avgPPM = Math.round((totalEarned / totalRouted)*1000000); 374 | return { 375 | name: month.name, 376 | value: avgPPM || 0 377 | } 378 | }); 379 | return chartArray; 380 | } 381 | 382 | parseProfit(forwards, keysends = null, chainFees = null, rebalanceFees = null, lightningFees = null, payments = null) { 383 | let profit = { 384 | daily: { 385 | sats: [], 386 | cumulative: [] 387 | }, 388 | weekly: { 389 | sats: [], 390 | cumulative: [] 391 | }, 392 | monthly: { 393 | sats: [], 394 | cumulative: [] 395 | } 396 | }; 397 | const allDays = this.getAllRange('daily', forwards, keysends, chainFees, rebalanceFees, lightningFees, payments); 398 | let dailyCumulativeTotal = 0; 399 | allDays.map((day, i) => { 400 | const dayForwards = forwards?.daily.sats.find((forward) => forward.name == day); 401 | const dayKeysends = keysends?.daily.sats.find((keysend) => keysend.name == day); 402 | const dayChainFees = chainFees?.daily.sats.find((chainFee) => chainFee.name == day); 403 | const dayRebalanceFees = rebalanceFees?.daily.sats.find((rebalanceFee) => rebalanceFee.name == day); 404 | const dayLightningFees = lightningFees?.daily.sats.find((lightningFee) => lightningFee.name == day); 405 | const dayPayments = payments?.daily.sats.find((payment) => payment.name == day); 406 | const amount = (dayForwards?.value || 0) + (dayKeysends?.value || 0) - (dayChainFees?.value || 0) - (dayRebalanceFees?.value || 0) - (dayLightningFees?.value || 0) - (dayPayments?.value || 0); 407 | dailyCumulativeTotal += amount; 408 | profit.daily.sats.push({ 409 | name: day, 410 | value: amount 411 | }); 412 | profit.daily.cumulative.push({ 413 | name: day, 414 | value: dailyCumulativeTotal 415 | }); 416 | }); 417 | const allWeeks = this.getAllRange('weekly', forwards, keysends, chainFees, rebalanceFees, lightningFees, payments); 418 | let weeklyCumulativeTotal = 0; 419 | allWeeks.map((week, i) => { 420 | const weekForwards = forwards?.weekly.sats.find((forwards) => forwards.name == week); 421 | const weekKeysends = keysends?.weekly.sats.find((keysend) => keysend.name == week); 422 | const weekChainFees = chainFees?.weekly.sats.find((chainFee) => chainFee.name == week); 423 | const weekRebalanceFees = rebalanceFees?.weekly.sats.find((rebalanceFee) => rebalanceFee.name == week); 424 | const weekLightningFees = lightningFees?.weekly.sats.find((lightningFee) => lightningFee.name == week); 425 | const weekPayments = payments?.weekly.sats.find((payment) => payment.name == week); 426 | const amount = (weekForwards?.value || 0) + (weekKeysends?.value || 0) - (weekChainFees?.value || 0) - (weekRebalanceFees?.value || 0) - (weekLightningFees?.value || 0) - (weekPayments?.value || 0); 427 | weeklyCumulativeTotal += amount; 428 | profit.weekly.sats.push({ 429 | name: week, 430 | value: amount 431 | }); 432 | profit.weekly.cumulative.push({ 433 | name: week, 434 | value: weeklyCumulativeTotal 435 | }); 436 | }); 437 | const allMonths = this.getAllRange('monthly', forwards, keysends, chainFees, rebalanceFees, lightningFees, payments); 438 | let monthlyCumulativeTotal = 0; 439 | allMonths.map((month, i) => { 440 | const monthForwards = forwards?.monthly.sats.find((forwards) => forwards.name == month); 441 | const monthKeysends = keysends?.monthly.sats.find((keysend) => keysend.name == month); 442 | const monthChainFees = chainFees?.monthly.sats.find((chainFee) => chainFee.name == month); 443 | const monthRebalanceFees = rebalanceFees?.monthly.sats.find((rebalanceFee) => rebalanceFee.name == month); 444 | const monthLightningFees = lightningFees?.monthly.sats.find((lightningFee) => lightningFee.name == month); 445 | const monthPayments = payments?.monthly.sats.find((payment) => payment.name == month); 446 | const amount = (monthForwards?.value || 0) + (monthKeysends?.value || 0) - (monthChainFees?.value || 0) - (monthRebalanceFees?.value || 0) - (monthLightningFees?.value || 0) - (monthPayments?.value || 0) 447 | monthlyCumulativeTotal += amount; 448 | profit.monthly.sats.push({ 449 | name: month, 450 | value: amount 451 | }); 452 | profit.monthly.cumulative.push({ 453 | name: month, 454 | value: monthlyCumulativeTotal 455 | }); 456 | }); 457 | return profit; 458 | } 459 | 460 | getAllRange(range, forwards, keysends, chainFees, rebalanceFees, lightningFees, payments) { 461 | let array = []; 462 | forwards[range].sats.map((item) => { 463 | array.push({ 464 | name: item.name, 465 | luxonDate: item.luxonDate 466 | }); 467 | }); 468 | if (keysends) { 469 | keysends[range].sats.map((item) => { 470 | const alreadyAdded = array.some(arrayItem => arrayItem.name == item.name); 471 | if (!alreadyAdded) { 472 | array.push({ 473 | name: item.name, 474 | luxonDate: item.luxonDate 475 | }); 476 | } 477 | }); 478 | } 479 | if (chainFees) { 480 | chainFees[range].sats.map((item) => { 481 | const alreadyAdded = array.some(arrayItem => arrayItem.name == item.name); 482 | if (!alreadyAdded) { 483 | array.push({ 484 | name: item.name, 485 | luxonDate: item.luxonDate 486 | }); 487 | } 488 | }); 489 | } 490 | if (rebalanceFees) { 491 | rebalanceFees[range].sats.map((item) => { 492 | const alreadyAdded = array.some(arrayItem => arrayItem.name == item.name); 493 | if (!alreadyAdded) { 494 | array.push({ 495 | name: item.name, 496 | luxonDate: item.luxonDate 497 | }); 498 | } 499 | }); 500 | } 501 | if (lightningFees) { 502 | lightningFees[range].sats.map((item) => { 503 | const alreadyAdded = array.some(arrayItem => arrayItem.name == item.name); 504 | if (!alreadyAdded) { 505 | array.push({ 506 | name: item.name, 507 | luxonDate: item.luxonDate 508 | }); 509 | } 510 | }); 511 | } 512 | if (payments) { 513 | payments[range].sats.map((item) => { 514 | const alreadyAdded = array.some(arrayItem => arrayItem.name == item.name); 515 | if (!alreadyAdded) { 516 | array.push({ 517 | name: item.name, 518 | luxonDate: item.luxonDate 519 | }); 520 | } 521 | }); 522 | } 523 | let returnArray = []; 524 | if (range == 'weekly') { 525 | array.sort((a, b) => a.luxonDate - b.luxonDate); 526 | array.map((item) => returnArray.push(item.name)); 527 | } else { 528 | array.sort((a, b) => new Date(a.name).valueOf() - new Date(b.name).valueOf()); 529 | array.map((item) => returnArray.push(item.name)); 530 | } 531 | return returnArray; 532 | } 533 | 534 | clearProviderData() { 535 | this.dayChart = []; 536 | this.weekChart = []; 537 | this.monthChart = []; 538 | } 539 | 540 | getMonthName(monthNumber) { 541 | const date = new Date(); 542 | date.setMonth(monthNumber - 1); 543 | const monthName = date.toLocaleString("default", { month: "long", timeZone: 'UTC' }); 544 | return monthName; 545 | } 546 | 547 | getLuxonDate(rawDate) { 548 | const date = rawDate.replaceAll('"', ''); 549 | return DateTime.fromISO(date, { zone: 'UTC' }); 550 | } 551 | 552 | getJsDate(rawDate) { 553 | const date = rawDate.replaceAll('"', ''); 554 | return new Date(date); 555 | } 556 | 557 | getDay(luxonDate) { 558 | const formattedDate = luxonDate.toLocaleString(DateTime.DATETIME_SHORT); 559 | const dayArray = formattedDate.split(','); 560 | return dayArray[0]; 561 | } 562 | 563 | } 564 | --------------------------------------------------------------------------------