├── .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 |
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 |
--------------------------------------------------------------------------------