Merge branch 'master' into about-widths
This commit is contained in:
		
						commit
						0fd672a741
					
				| @ -25,7 +25,8 @@ | |||||||
|     "AUTOMATIC_BLOCK_REINDEXING": false, |     "AUTOMATIC_BLOCK_REINDEXING": false, | ||||||
|     "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json", |     "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json", | ||||||
|     "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", |     "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", | ||||||
|     "ADVANCED_TRANSACTION_SELECTION": false, |     "ADVANCED_GBT_AUDIT": false, | ||||||
|  |     "ADVANCED_GBT_MEMPOOL": false, | ||||||
|     "TRANSACTION_INDEXING": false |     "TRANSACTION_INDEXING": false | ||||||
|   }, |   }, | ||||||
|   "CORE_RPC": { |   "CORE_RPC": { | ||||||
|  | |||||||
| @ -26,7 +26,8 @@ | |||||||
|     "INDEXING_BLOCKS_AMOUNT": 14, |     "INDEXING_BLOCKS_AMOUNT": 14, | ||||||
|     "POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__", |     "POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__", | ||||||
|     "POOLS_JSON_URL": "__POOLS_JSON_URL__", |     "POOLS_JSON_URL": "__POOLS_JSON_URL__", | ||||||
|     "ADVANCED_TRANSACTION_SELECTION": "__ADVANCED_TRANSACTION_SELECTION__", |     "ADVANCED_GBT_AUDIT": "__ADVANCED_GBT_AUDIT__", | ||||||
|  |     "ADVANCED_GBT_MEMPOOL": "__ADVANCED_GBT_MEMPOOL__", | ||||||
|     "TRANSACTION_INDEXING": "__TRANSACTION_INDEXING__" |     "TRANSACTION_INDEXING": "__TRANSACTION_INDEXING__" | ||||||
|   }, |   }, | ||||||
|   "CORE_RPC": { |   "CORE_RPC": { | ||||||
|  | |||||||
| @ -38,7 +38,8 @@ describe('Mempool Backend Config', () => { | |||||||
|         STDOUT_LOG_MIN_PRIORITY: 'debug', |         STDOUT_LOG_MIN_PRIORITY: 'debug', | ||||||
|         POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', |         POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', | ||||||
|         POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json', |         POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json', | ||||||
|         ADVANCED_TRANSACTION_SELECTION: false, |         ADVANCED_GBT_AUDIT: false, | ||||||
|  |         ADVANCED_GBT_MEMPOOL: false, | ||||||
|         TRANSACTION_INDEXING: false, |         TRANSACTION_INDEXING: false, | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ import logger from '../logger'; | |||||||
| import { Common } from './common'; | import { Common } from './common'; | ||||||
| 
 | 
 | ||||||
| class DatabaseMigration { | class DatabaseMigration { | ||||||
|   private static currentVersion = 47; |   private static currentVersion = 48; | ||||||
|   private queryTimeout = 900_000; |   private queryTimeout = 900_000; | ||||||
|   private statisticsAddedIndexed = false; |   private statisticsAddedIndexed = false; | ||||||
|   private uniqueLogs: string[] = []; |   private uniqueLogs: string[] = []; | ||||||
| @ -379,7 +379,20 @@ class DatabaseMigration { | |||||||
|       await this.$executeQuery(this.getCreateCPFPTableQuery(), await this.$checkIfTableExists('cpfp_clusters')); |       await this.$executeQuery(this.getCreateCPFPTableQuery(), await this.$checkIfTableExists('cpfp_clusters')); | ||||||
|       await this.$executeQuery(this.getCreateTransactionsTableQuery(), await this.$checkIfTableExists('transactions')); |       await this.$executeQuery(this.getCreateTransactionsTableQuery(), await this.$checkIfTableExists('transactions')); | ||||||
|     } |     } | ||||||
| } | 
 | ||||||
|  |     if (databaseSchemaVersion < 48 && isBitcoin === true) { | ||||||
|  |       await this.$executeQuery('ALTER TABLE `channels` ADD source_checked tinyint(1) DEFAULT 0'); | ||||||
|  |       await this.$executeQuery('ALTER TABLE `channels` ADD closing_fee bigint(20) unsigned DEFAULT 0'); | ||||||
|  |       await this.$executeQuery('ALTER TABLE `channels` ADD node1_funding_balance bigint(20) unsigned DEFAULT 0'); | ||||||
|  |       await this.$executeQuery('ALTER TABLE `channels` ADD node2_funding_balance bigint(20) unsigned DEFAULT 0'); | ||||||
|  |       await this.$executeQuery('ALTER TABLE `channels` ADD node1_closing_balance bigint(20) unsigned DEFAULT 0'); | ||||||
|  |       await this.$executeQuery('ALTER TABLE `channels` ADD node2_closing_balance bigint(20) unsigned DEFAULT 0'); | ||||||
|  |       await this.$executeQuery('ALTER TABLE `channels` ADD funding_ratio float unsigned DEFAULT NULL'); | ||||||
|  |       await this.$executeQuery('ALTER TABLE `channels` ADD closed_by varchar(66) DEFAULT NULL'); | ||||||
|  |       await this.$executeQuery('ALTER TABLE `channels` ADD single_funded tinyint(1) DEFAULT 0'); | ||||||
|  |       await this.$executeQuery('ALTER TABLE `channels` ADD outputs JSON DEFAULT "[]"'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Special case here for the `statistics` table - It appeared that somehow some dbs already had the `added` field indexed |    * Special case here for the `statistics` table - It appeared that somehow some dbs already had the `added` field indexed | ||||||
|  | |||||||
| @ -128,6 +128,21 @@ class ChannelsApi { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public async $getChannelsWithoutSourceChecked(): Promise<any[]> { | ||||||
|  |     try { | ||||||
|  |       const query = ` | ||||||
|  |         SELECT channels.* | ||||||
|  |         FROM channels | ||||||
|  |         WHERE channels.source_checked != 1 | ||||||
|  |       `;
 | ||||||
|  |       const [rows]: any = await DB.query(query); | ||||||
|  |       return rows; | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err('$getUnresolvedClosedChannels error: ' + (e instanceof Error ? e.message : e)); | ||||||
|  |       throw e; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   public async $getChannelsWithoutCreatedDate(): Promise<any[]> { |   public async $getChannelsWithoutCreatedDate(): Promise<any[]> { | ||||||
|     try { |     try { | ||||||
|       const query = `SELECT * FROM channels WHERE created IS NULL`; |       const query = `SELECT * FROM channels WHERE created IS NULL`; | ||||||
| @ -257,6 +272,108 @@ class ChannelsApi { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public async $getChannelByClosingId(transactionId: string): Promise<any> { | ||||||
|  |     try { | ||||||
|  |       const query = ` | ||||||
|  |         SELECT | ||||||
|  |           channels.* | ||||||
|  |         FROM channels | ||||||
|  |         WHERE channels.closing_transaction_id = ? | ||||||
|  |       `;
 | ||||||
|  |       const [rows]: any = await DB.query(query, [transactionId]); | ||||||
|  |       if (rows.length > 0) { | ||||||
|  |         rows[0].outputs = JSON.parse(rows[0].outputs); | ||||||
|  |         return rows[0]; | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err('$getChannelByClosingId error: ' + (e instanceof Error ? e.message : e)); | ||||||
|  |       // don't throw - this data isn't essential
 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public async $getChannelsByOpeningId(transactionId: string): Promise<any> { | ||||||
|  |     try { | ||||||
|  |       const query = ` | ||||||
|  |         SELECT | ||||||
|  |           channels.* | ||||||
|  |         FROM channels | ||||||
|  |         WHERE channels.transaction_id = ? | ||||||
|  |       `;
 | ||||||
|  |       const [rows]: any = await DB.query(query, [transactionId]); | ||||||
|  |       if (rows.length > 0) { | ||||||
|  |         return rows.map(row => { | ||||||
|  |           row.outputs = JSON.parse(row.outputs); | ||||||
|  |           return row; | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err('$getChannelsByOpeningId error: ' + (e instanceof Error ? e.message : e)); | ||||||
|  |       // don't throw - this data isn't essential
 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public async $updateClosingInfo(channelInfo: { id: string, node1_closing_balance: number, node2_closing_balance: number, closed_by: string | null, closing_fee: number, outputs: ILightningApi.ForensicOutput[]}): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       const query = ` | ||||||
|  |         UPDATE channels SET | ||||||
|  |           node1_closing_balance = ?, | ||||||
|  |           node2_closing_balance = ?, | ||||||
|  |           closed_by = ?, | ||||||
|  |           closing_fee = ?, | ||||||
|  |           outputs = ? | ||||||
|  |         WHERE channels.id = ? | ||||||
|  |       `;
 | ||||||
|  |       await DB.query<ResultSetHeader>(query, [ | ||||||
|  |         channelInfo.node1_closing_balance || 0, | ||||||
|  |         channelInfo.node2_closing_balance || 0, | ||||||
|  |         channelInfo.closed_by, | ||||||
|  |         channelInfo.closing_fee || 0, | ||||||
|  |         JSON.stringify(channelInfo.outputs), | ||||||
|  |         channelInfo.id, | ||||||
|  |       ]); | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err('$updateClosingInfo error: ' + (e instanceof Error ? e.message : e)); | ||||||
|  |       // don't throw - this data isn't essential
 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public async $updateOpeningInfo(channelInfo: { id: string, node1_funding_balance: number, node2_funding_balance: number, funding_ratio: number, single_funded: boolean | void }): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       const query = ` | ||||||
|  |         UPDATE channels SET | ||||||
|  |           node1_funding_balance = ?, | ||||||
|  |           node2_funding_balance = ?, | ||||||
|  |           funding_ratio = ?, | ||||||
|  |           single_funded = ? | ||||||
|  |         WHERE channels.id = ? | ||||||
|  |       `;
 | ||||||
|  |       await DB.query<ResultSetHeader>(query, [ | ||||||
|  |         channelInfo.node1_funding_balance || 0, | ||||||
|  |         channelInfo.node2_funding_balance || 0, | ||||||
|  |         channelInfo.funding_ratio, | ||||||
|  |         channelInfo.single_funded ? 1 : 0, | ||||||
|  |         channelInfo.id, | ||||||
|  |       ]); | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err('$updateOpeningInfo error: ' + (e instanceof Error ? e.message : e)); | ||||||
|  |       // don't throw - this data isn't essential
 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public async $markChannelSourceChecked(id: string): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       const query = ` | ||||||
|  |         UPDATE channels | ||||||
|  |         SET source_checked = 1 | ||||||
|  |         WHERE id = ? | ||||||
|  |       `;
 | ||||||
|  |       await DB.query<ResultSetHeader>(query, [id]); | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err('$markChannelSourceChecked error: ' + (e instanceof Error ? e.message : e)); | ||||||
|  |       // don't throw - this data isn't essential
 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   public async $getChannelsForNode(public_key: string, index: number, length: number, status: string): Promise<any[]> { |   public async $getChannelsForNode(public_key: string, index: number, length: number, status: string): Promise<any[]> { | ||||||
|     try { |     try { | ||||||
|       let channelStatusFilter; |       let channelStatusFilter; | ||||||
| @ -385,11 +502,15 @@ class ChannelsApi { | |||||||
|       'transaction_id': channel.transaction_id, |       'transaction_id': channel.transaction_id, | ||||||
|       'transaction_vout': channel.transaction_vout, |       'transaction_vout': channel.transaction_vout, | ||||||
|       'closing_transaction_id': channel.closing_transaction_id, |       'closing_transaction_id': channel.closing_transaction_id, | ||||||
|  |       'closing_fee': channel.closing_fee, | ||||||
|       'closing_reason': channel.closing_reason, |       'closing_reason': channel.closing_reason, | ||||||
|       'closing_date': channel.closing_date, |       'closing_date': channel.closing_date, | ||||||
|       'updated_at': channel.updated_at, |       'updated_at': channel.updated_at, | ||||||
|       'created': channel.created, |       'created': channel.created, | ||||||
|       'status': channel.status, |       'status': channel.status, | ||||||
|  |       'funding_ratio': channel.funding_ratio, | ||||||
|  |       'closed_by': channel.closed_by, | ||||||
|  |       'single_funded': !!channel.single_funded, | ||||||
|       'node_left': { |       'node_left': { | ||||||
|         'alias': channel.alias_left, |         'alias': channel.alias_left, | ||||||
|         'public_key': channel.node1_public_key, |         'public_key': channel.node1_public_key, | ||||||
| @ -404,6 +525,9 @@ class ChannelsApi { | |||||||
|         'updated_at': channel.node1_updated_at, |         'updated_at': channel.node1_updated_at, | ||||||
|         'longitude': channel.node1_longitude, |         'longitude': channel.node1_longitude, | ||||||
|         'latitude': channel.node1_latitude, |         'latitude': channel.node1_latitude, | ||||||
|  |         'funding_balance': channel.node1_funding_balance, | ||||||
|  |         'closing_balance': channel.node1_closing_balance, | ||||||
|  |         'initiated_close': channel.closed_by === channel.node1_public_key ? true : undefined, | ||||||
|       }, |       }, | ||||||
|       'node_right': { |       'node_right': { | ||||||
|         'alias': channel.alias_right, |         'alias': channel.alias_right, | ||||||
| @ -419,6 +543,9 @@ class ChannelsApi { | |||||||
|         'updated_at': channel.node2_updated_at, |         'updated_at': channel.node2_updated_at, | ||||||
|         'longitude': channel.node2_longitude, |         'longitude': channel.node2_longitude, | ||||||
|         'latitude': channel.node2_latitude, |         'latitude': channel.node2_latitude, | ||||||
|  |         'funding_balance': channel.node2_funding_balance, | ||||||
|  |         'closing_balance': channel.node2_closing_balance, | ||||||
|  |         'initiated_close': channel.closed_by === channel.node2_public_key ? true : undefined, | ||||||
|       }, |       }, | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -83,4 +83,10 @@ export namespace ILightningApi { | |||||||
|     is_required: boolean; |     is_required: boolean; | ||||||
|     is_known: boolean; |     is_known: boolean; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   export interface ForensicOutput { | ||||||
|  |     node?: 1 | 2; | ||||||
|  |     type: number; | ||||||
|  |     value: number; | ||||||
|  |   } | ||||||
| } | } | ||||||
| @ -250,12 +250,12 @@ class WebsocketHandler { | |||||||
|       throw new Error('WebSocket.Server is not set'); |       throw new Error('WebSocket.Server is not set'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (config.MEMPOOL.ADVANCED_TRANSACTION_SELECTION) { |     if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) { | ||||||
|       await mempoolBlocks.makeBlockTemplates(newMempool, 8, null, true); |       await mempoolBlocks.makeBlockTemplates(newMempool, 8, null, true); | ||||||
|     } |     } else { | ||||||
|     else { |  | ||||||
|       mempoolBlocks.updateMempoolBlocks(newMempool); |       mempoolBlocks.updateMempoolBlocks(newMempool); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     const mBlocks = mempoolBlocks.getMempoolBlocks(); |     const mBlocks = mempoolBlocks.getMempoolBlocks(); | ||||||
|     const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); |     const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); | ||||||
|     const mempoolInfo = memPool.getMempoolInfo(); |     const mempoolInfo = memPool.getMempoolInfo(); | ||||||
| @ -417,9 +417,8 @@ class WebsocketHandler { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const _memPool = memPool.getMempool(); |     const _memPool = memPool.getMempool(); | ||||||
|     let matchRate; |  | ||||||
| 
 | 
 | ||||||
|     if (config.MEMPOOL.ADVANCED_TRANSACTION_SELECTION) { |     if (config.MEMPOOL.ADVANCED_GBT_AUDIT) { | ||||||
|       await mempoolBlocks.makeBlockTemplates(_memPool, 2); |       await mempoolBlocks.makeBlockTemplates(_memPool, 2); | ||||||
|     } else { |     } else { | ||||||
|       mempoolBlocks.updateMempoolBlocks(_memPool); |       mempoolBlocks.updateMempoolBlocks(_memPool); | ||||||
| @ -429,7 +428,7 @@ class WebsocketHandler { | |||||||
|       const projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions(); |       const projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions(); | ||||||
| 
 | 
 | ||||||
|       const { censored, added, fresh, score } = Audit.auditBlock(transactions, projectedBlocks, _memPool); |       const { censored, added, fresh, score } = Audit.auditBlock(transactions, projectedBlocks, _memPool); | ||||||
|       matchRate = Math.round(score * 100 * 100) / 100; |       const matchRate = Math.round(score * 100 * 100) / 100; | ||||||
| 
 | 
 | ||||||
|       const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => { |       const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => { | ||||||
|         return { |         return { | ||||||
| @ -468,7 +467,7 @@ class WebsocketHandler { | |||||||
|       delete _memPool[txId]; |       delete _memPool[txId]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (config.MEMPOOL.ADVANCED_TRANSACTION_SELECTION) { |     if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) { | ||||||
|       await mempoolBlocks.makeBlockTemplates(_memPool, 2); |       await mempoolBlocks.makeBlockTemplates(_memPool, 2); | ||||||
|     } else { |     } else { | ||||||
|       mempoolBlocks.updateMempoolBlocks(_memPool); |       mempoolBlocks.updateMempoolBlocks(_memPool); | ||||||
|  | |||||||
| @ -29,7 +29,8 @@ interface IConfig { | |||||||
|     AUTOMATIC_BLOCK_REINDEXING: boolean; |     AUTOMATIC_BLOCK_REINDEXING: boolean; | ||||||
|     POOLS_JSON_URL: string, |     POOLS_JSON_URL: string, | ||||||
|     POOLS_JSON_TREE_URL: string, |     POOLS_JSON_TREE_URL: string, | ||||||
|     ADVANCED_TRANSACTION_SELECTION: boolean; |     ADVANCED_GBT_AUDIT: boolean; | ||||||
|  |     ADVANCED_GBT_MEMPOOL: boolean; | ||||||
|     TRANSACTION_INDEXING: boolean; |     TRANSACTION_INDEXING: boolean; | ||||||
|   }; |   }; | ||||||
|   ESPLORA: { |   ESPLORA: { | ||||||
| @ -148,7 +149,8 @@ const defaults: IConfig = { | |||||||
|     'AUTOMATIC_BLOCK_REINDEXING': false, |     'AUTOMATIC_BLOCK_REINDEXING': false, | ||||||
|     'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json', |     'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json', | ||||||
|     'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', |     'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', | ||||||
|     'ADVANCED_TRANSACTION_SELECTION': false, |     'ADVANCED_GBT_AUDIT': false, | ||||||
|  |     'ADVANCED_GBT_MEMPOOL': false, | ||||||
|     'TRANSACTION_INDEXING': false, |     'TRANSACTION_INDEXING': false, | ||||||
|   }, |   }, | ||||||
|   'ESPLORA': { |   'ESPLORA': { | ||||||
|  | |||||||
| @ -5,13 +5,16 @@ import bitcoinApi from '../../api/bitcoin/bitcoin-api-factory'; | |||||||
| import config from '../../config'; | import config from '../../config'; | ||||||
| import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface'; | import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface'; | ||||||
| import { Common } from '../../api/common'; | import { Common } from '../../api/common'; | ||||||
|  | import { ILightningApi } from '../../api/lightning/lightning-api.interface'; | ||||||
| 
 | 
 | ||||||
| const throttleDelay = 20; //ms
 | const throttleDelay = 20; //ms
 | ||||||
|  | const tempCacheSize = 10000; | ||||||
| 
 | 
 | ||||||
| class ForensicsService { | class ForensicsService { | ||||||
|   loggerTimer = 0; |   loggerTimer = 0; | ||||||
|   closedChannelsScanBlock = 0; |   closedChannelsScanBlock = 0; | ||||||
|   txCache: { [txid: string]: IEsploraApi.Transaction } = {}; |   txCache: { [txid: string]: IEsploraApi.Transaction } = {}; | ||||||
|  |   tempCached: string[] = []; | ||||||
| 
 | 
 | ||||||
|   constructor() {} |   constructor() {} | ||||||
| 
 | 
 | ||||||
| @ -29,6 +32,7 @@ class ForensicsService { | |||||||
| 
 | 
 | ||||||
|       if (config.MEMPOOL.BACKEND === 'esplora') { |       if (config.MEMPOOL.BACKEND === 'esplora') { | ||||||
|         await this.$runClosedChannelsForensics(false); |         await this.$runClosedChannelsForensics(false); | ||||||
|  |         await this.$runOpenedChannelsForensics(); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
| @ -95,17 +99,10 @@ class ForensicsService { | |||||||
|           const lightningScriptReasons: number[] = []; |           const lightningScriptReasons: number[] = []; | ||||||
|           for (const outspend of outspends) { |           for (const outspend of outspends) { | ||||||
|             if (outspend.spent && outspend.txid) { |             if (outspend.spent && outspend.txid) { | ||||||
|               let spendingTx: IEsploraApi.Transaction | undefined = this.txCache[outspend.txid]; |               let spendingTx = await this.fetchTransaction(outspend.txid); | ||||||
|               if (!spendingTx) { |               if (!spendingTx) { | ||||||
|                 try { |  | ||||||
|                   spendingTx = await bitcoinApi.$getRawTransaction(outspend.txid); |  | ||||||
|                   await Common.sleep$(throttleDelay); |  | ||||||
|                   this.txCache[outspend.txid] = spendingTx; |  | ||||||
|                 } catch (e) { |  | ||||||
|                   logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + outspend.txid}. Reason ${e instanceof Error ? e.message : e}`); |  | ||||||
|                 continue; |                 continue; | ||||||
|               } |               } | ||||||
|               } |  | ||||||
|               cached.push(spendingTx.txid); |               cached.push(spendingTx.txid); | ||||||
|               const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]); |               const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]); | ||||||
|               lightningScriptReasons.push(lightningScript); |               lightningScriptReasons.push(lightningScript); | ||||||
| @ -124,17 +121,10 @@ class ForensicsService { | |||||||
|               We can detect a commitment transaction (force close) by reading Sequence and Locktime |               We can detect a commitment transaction (force close) by reading Sequence and Locktime | ||||||
|               https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
 |               https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
 | ||||||
|             */ |             */ | ||||||
|             let closingTx: IEsploraApi.Transaction | undefined = this.txCache[channel.closing_transaction_id]; |             let closingTx = await this.fetchTransaction(channel.closing_transaction_id, true); | ||||||
|             if (!closingTx) { |             if (!closingTx) { | ||||||
|               try { |  | ||||||
|                 closingTx = await bitcoinApi.$getRawTransaction(channel.closing_transaction_id); |  | ||||||
|                 await Common.sleep$(throttleDelay); |  | ||||||
|                 this.txCache[channel.closing_transaction_id] = closingTx; |  | ||||||
|               } catch (e) { |  | ||||||
|                 logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id}. Reason ${e instanceof Error ? e.message : e}`); |  | ||||||
|               continue; |               continue; | ||||||
|             } |             } | ||||||
|             } |  | ||||||
|             cached.push(closingTx.txid); |             cached.push(closingTx.txid); | ||||||
|             const sequenceHex: string = closingTx.vin[0].sequence.toString(16); |             const sequenceHex: string = closingTx.vin[0].sequence.toString(16); | ||||||
|             const locktimeHex: string = closingTx.locktime.toString(16); |             const locktimeHex: string = closingTx.locktime.toString(16); | ||||||
| @ -174,7 +164,7 @@ class ForensicsService { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private findLightningScript(vin: IEsploraApi.Vin): number { |   private findLightningScript(vin: IEsploraApi.Vin): number { | ||||||
|     const topElement = vin.witness[vin.witness.length - 2]; |     const topElement = vin.witness?.length > 2 ? vin.witness[vin.witness.length - 2] : null; | ||||||
|       if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(vin.inner_witnessscript_asm)) { |       if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(vin.inner_witnessscript_asm)) { | ||||||
|         // https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
 |         // https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
 | ||||||
|         if (topElement === '01') { |         if (topElement === '01') { | ||||||
| @ -193,7 +183,7 @@ class ForensicsService { | |||||||
|       ) { |       ) { | ||||||
|         // https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
 |         // https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
 | ||||||
|         // https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs
 |         // https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs
 | ||||||
|         if (topElement.length === 66) { |         if (topElement?.length === 66) { | ||||||
|           // top element is a public key
 |           // top element is a public key
 | ||||||
|           // 'Revoked Lightning HTLC'; Penalty force closed
 |           // 'Revoked Lightning HTLC'; Penalty force closed
 | ||||||
|           return 4; |           return 4; | ||||||
| @ -220,6 +210,249 @@ class ForensicsService { | |||||||
|       } |       } | ||||||
|       return 1; |       return 1; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   // If a channel open tx spends funds from a another channel transaction,
 | ||||||
|  |   // we can attribute that output to a specific counterparty
 | ||||||
|  |   private async $runOpenedChannelsForensics(): Promise<void> { | ||||||
|  |     const runTimer = Date.now(); | ||||||
|  |     let progress = 0; | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       logger.info(`Started running open channel forensics...`); | ||||||
|  |       const channels = await channelsApi.$getChannelsWithoutSourceChecked(); | ||||||
|  | 
 | ||||||
|  |       for (const openChannel of channels) { | ||||||
|  |         let openTx = await this.fetchTransaction(openChannel.transaction_id, true); | ||||||
|  |         if (!openTx) { | ||||||
|  |           continue; | ||||||
|  |         } | ||||||
|  |         for (const input of openTx.vin) { | ||||||
|  |           const closeChannel = await channelsApi.$getChannelByClosingId(input.txid); | ||||||
|  |           if (closeChannel) { | ||||||
|  |             // this input directly spends a channel close output
 | ||||||
|  |             await this.$attributeChannelBalances(closeChannel, openChannel, input); | ||||||
|  |           } else { | ||||||
|  |             const prevOpenChannels = await channelsApi.$getChannelsByOpeningId(input.txid); | ||||||
|  |             if (prevOpenChannels?.length) { | ||||||
|  |               // this input spends a channel open change output
 | ||||||
|  |               for (const prevOpenChannel of prevOpenChannels) { | ||||||
|  |                 await this.$attributeChannelBalances(prevOpenChannel, openChannel, input, null, null, true); | ||||||
|  |               } | ||||||
|  |             } else { | ||||||
|  |               // check if this input spends any swept channel close outputs
 | ||||||
|  |               await this.$attributeSweptChannelCloses(openChannel, input); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         // calculate how much of the total input value is attributable to the channel open output
 | ||||||
|  |         openChannel.funding_ratio = openTx.vout[openChannel.transaction_vout].value / ((openTx.vout.reduce((sum, v) => sum + v.value, 0) || 1) + openTx.fee); | ||||||
|  |         // save changes to the opening channel, and mark it as checked
 | ||||||
|  |         if (openTx?.vin?.length === 1) { | ||||||
|  |           openChannel.single_funded = true; | ||||||
|  |         } | ||||||
|  |         if (openChannel.node1_funding_balance || openChannel.node2_funding_balance || openChannel.node1_closing_balance || openChannel.node2_closing_balance || openChannel.closed_by) { | ||||||
|  |           await channelsApi.$updateOpeningInfo(openChannel); | ||||||
|  |         } | ||||||
|  |         await channelsApi.$markChannelSourceChecked(openChannel.id); | ||||||
|  | 
 | ||||||
|  |         ++progress; | ||||||
|  |         const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); | ||||||
|  |         if (elapsedSeconds > 10) { | ||||||
|  |           logger.info(`Updating opened channel forensics ${progress}/${channels?.length}`); | ||||||
|  |           this.loggerTimer = new Date().getTime() / 1000; | ||||||
|  |           this.truncateTempCache(); | ||||||
|  |         } | ||||||
|  |         if (Date.now() - runTimer > (config.LIGHTNING.FORENSICS_INTERVAL * 1000)) { | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       logger.info(`Open channels forensics scan complete.`); | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err('$runOpenedChannelsForensics() error: ' + (e instanceof Error ? e.message : e)); | ||||||
|  |     } finally { | ||||||
|  |       this.clearTempCache(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Check if a channel open tx input spends the result of a swept channel close output
 | ||||||
|  |   private async $attributeSweptChannelCloses(openChannel: ILightningApi.Channel, input: IEsploraApi.Vin): Promise<void> { | ||||||
|  |     let sweepTx = await this.fetchTransaction(input.txid, true); | ||||||
|  |     if (!sweepTx) { | ||||||
|  |       logger.err(`couldn't find input transaction for channel forensics ${openChannel.channel_id} ${input.txid}`); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     const openContribution = sweepTx.vout[input.vout].value; | ||||||
|  |     for (const sweepInput of sweepTx.vin) { | ||||||
|  |       const lnScriptType = this.findLightningScript(sweepInput); | ||||||
|  |       if (lnScriptType > 1) { | ||||||
|  |         const closeChannel = await channelsApi.$getChannelByClosingId(sweepInput.txid); | ||||||
|  |         if (closeChannel) { | ||||||
|  |           const initiator = (lnScriptType === 2 || lnScriptType === 4) ? 'remote' : (lnScriptType === 3 ? 'local' : null); | ||||||
|  |           await this.$attributeChannelBalances(closeChannel, openChannel, sweepInput, openContribution, initiator); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private async $attributeChannelBalances( | ||||||
|  |     prevChannel, openChannel, input: IEsploraApi.Vin, openContribution: number | null = null, | ||||||
|  |     initiator: 'remote' | 'local' | null = null, linkedOpenings: boolean = false | ||||||
|  |   ): Promise<void> { | ||||||
|  |     // figure out which node controls the input/output
 | ||||||
|  |     let openSide; | ||||||
|  |     let prevLocal; | ||||||
|  |     let prevRemote; | ||||||
|  |     let matched = false; | ||||||
|  |     let ambiguous = false; // if counterparties are the same in both channels, we can't tell them apart
 | ||||||
|  |     if (openChannel.node1_public_key === prevChannel.node1_public_key) { | ||||||
|  |       openSide = 1; | ||||||
|  |       prevLocal = 1; | ||||||
|  |       prevRemote = 2; | ||||||
|  |       matched = true; | ||||||
|  |     } else if (openChannel.node1_public_key === prevChannel.node2_public_key) { | ||||||
|  |       openSide = 1; | ||||||
|  |       prevLocal = 2; | ||||||
|  |       prevRemote = 1; | ||||||
|  |       matched = true; | ||||||
|  |     } | ||||||
|  |     if (openChannel.node2_public_key === prevChannel.node1_public_key) { | ||||||
|  |       openSide = 2; | ||||||
|  |       prevLocal = 1; | ||||||
|  |       prevRemote = 2; | ||||||
|  |       if (matched) { | ||||||
|  |         ambiguous = true; | ||||||
|  |       } | ||||||
|  |       matched = true; | ||||||
|  |     } else if (openChannel.node2_public_key === prevChannel.node2_public_key) { | ||||||
|  |       openSide = 2; | ||||||
|  |       prevLocal = 2; | ||||||
|  |       prevRemote = 1; | ||||||
|  |       if (matched) { | ||||||
|  |         ambiguous = true; | ||||||
|  |       } | ||||||
|  |       matched = true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (matched && !ambiguous) { | ||||||
|  |       // fetch closing channel transaction and perform forensics on the outputs
 | ||||||
|  |       let prevChannelTx = await this.fetchTransaction(input.txid, true); | ||||||
|  |       let outspends: IEsploraApi.Outspend[] | undefined; | ||||||
|  |       try { | ||||||
|  |         outspends = await bitcoinApi.$getOutspends(input.txid); | ||||||
|  |         await Common.sleep$(throttleDelay); | ||||||
|  |       } catch (e) { | ||||||
|  |         logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + input.txid + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`); | ||||||
|  |       } | ||||||
|  |       if (!outspends || !prevChannelTx) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       if (!linkedOpenings) { | ||||||
|  |         if (!prevChannel.outputs || !prevChannel.outputs.length) { | ||||||
|  |           prevChannel.outputs = prevChannelTx.vout.map(vout => { | ||||||
|  |             return { | ||||||
|  |               type: 0, | ||||||
|  |               value: vout.value, | ||||||
|  |             }; | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |         for (let i = 0; i < outspends?.length; i++) { | ||||||
|  |           const outspend = outspends[i]; | ||||||
|  |           const output = prevChannel.outputs[i]; | ||||||
|  |           if (outspend.spent && outspend.txid) { | ||||||
|  |             try { | ||||||
|  |               const spendingTx = await this.fetchTransaction(outspend.txid, true); | ||||||
|  |               if (spendingTx) { | ||||||
|  |                 output.type = this.findLightningScript(spendingTx.vin[outspend.vin || 0]); | ||||||
|  |               } | ||||||
|  |             } catch (e) { | ||||||
|  |               logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + outspend.txid}. Reason ${e instanceof Error ? e.message : e}`); | ||||||
|  |             } | ||||||
|  |           } else { | ||||||
|  |             output.type = 0; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // attribute outputs to each counterparty, and sum up total known balances
 | ||||||
|  |         prevChannel.outputs[input.vout].node = prevLocal; | ||||||
|  |         const isPenalty = prevChannel.outputs.filter((out) => out.type === 2 || out.type === 4)?.length > 0; | ||||||
|  |         const normalOutput = [1,3].includes(prevChannel.outputs[input.vout].type); | ||||||
|  |         const mutualClose = ((prevChannel.status === 2 || prevChannel.status === 'closed') && prevChannel.closing_reason === 1); | ||||||
|  |         let localClosingBalance = 0; | ||||||
|  |         let remoteClosingBalance = 0; | ||||||
|  |         for (const output of prevChannel.outputs) { | ||||||
|  |           if (isPenalty) { | ||||||
|  |             // penalty close, so local node takes everything
 | ||||||
|  |             localClosingBalance += output.value; | ||||||
|  |           } else if (output.node) { | ||||||
|  |             // this output determinstically linked to one of the counterparties
 | ||||||
|  |             if (output.node === prevLocal) { | ||||||
|  |               localClosingBalance += output.value; | ||||||
|  |             } else { | ||||||
|  |               remoteClosingBalance += output.value; | ||||||
|  |             } | ||||||
|  |           } else if (normalOutput && (output.type === 1 || output.type === 3 || (mutualClose && prevChannel.outputs.length === 2))) { | ||||||
|  |             // local node had one main output, therefore remote node takes the other
 | ||||||
|  |             remoteClosingBalance += output.value; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         prevChannel[`node${prevLocal}_closing_balance`] = localClosingBalance; | ||||||
|  |         prevChannel[`node${prevRemote}_closing_balance`] = remoteClosingBalance; | ||||||
|  |         prevChannel.closing_fee = prevChannelTx.fee; | ||||||
|  | 
 | ||||||
|  |         if (initiator && !linkedOpenings) { | ||||||
|  |           const initiatorSide = initiator === 'remote' ? prevRemote : prevLocal; | ||||||
|  |           prevChannel.closed_by = prevChannel[`node${initiatorSide}_public_key`]; | ||||||
|  |         } | ||||||
|  |    | ||||||
|  |         // save changes to the closing channel
 | ||||||
|  |         await channelsApi.$updateClosingInfo(prevChannel); | ||||||
|  |       } else { | ||||||
|  |         if (prevChannelTx.vin.length <= 1) { | ||||||
|  |           prevChannel[`node${prevLocal}_funding_balance`] = prevChannel.capacity; | ||||||
|  |           prevChannel.single_funded = true; | ||||||
|  |           prevChannel.funding_ratio = 1; | ||||||
|  |           // save changes to the closing channel
 | ||||||
|  |           await channelsApi.$updateOpeningInfo(prevChannel); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       openChannel[`node${openSide}_funding_balance`] = openChannel[`node${openSide}_funding_balance`] + (openContribution || prevChannelTx?.vout[input.vout]?.value || 0); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async fetchTransaction(txid: string, temp: boolean = false): Promise<IEsploraApi.Transaction | null> { | ||||||
|  |     let tx = this.txCache[txid]; | ||||||
|  |     if (!tx) { | ||||||
|  |       try { | ||||||
|  |         tx = await bitcoinApi.$getRawTransaction(txid); | ||||||
|  |         this.txCache[txid] = tx; | ||||||
|  |         if (temp) { | ||||||
|  |           this.tempCached.push(txid); | ||||||
|  |         } | ||||||
|  |         await Common.sleep$(throttleDelay); | ||||||
|  |       } catch (e) { | ||||||
|  |         logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + txid + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`); | ||||||
|  |         return null; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return tx; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   clearTempCache(): void { | ||||||
|  |     for (const txid of this.tempCached) { | ||||||
|  |       delete this.txCache[txid]; | ||||||
|  |     } | ||||||
|  |     this.tempCached = []; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   truncateTempCache(): void { | ||||||
|  |     if (this.tempCached.length > tempCacheSize) { | ||||||
|  |       const removed = this.tempCached.splice(0, this.tempCached.length - tempCacheSize); | ||||||
|  |       for (const txid of removed) { | ||||||
|  |         delete this.txCache[txid]; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default new ForensicsService(); | export default new ForensicsService(); | ||||||
|  | |||||||
| @ -31,6 +31,7 @@ class NetworkSyncService { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async $runTasks(): Promise<void> { |   private async $runTasks(): Promise<void> { | ||||||
|  |     const taskStartTime = Date.now(); | ||||||
|     try { |     try { | ||||||
|       logger.info(`Updating nodes and channels`); |       logger.info(`Updating nodes and channels`); | ||||||
| 
 | 
 | ||||||
| @ -57,7 +58,7 @@ class NetworkSyncService { | |||||||
|       logger.err('$runTasks() error: ' + (e instanceof Error ? e.message : e)); |       logger.err('$runTasks() error: ' + (e instanceof Error ? e.message : e)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     setTimeout(() => { this.$runTasks(); }, 1000 * config.LIGHTNING.GRAPH_REFRESH_INTERVAL); |     setTimeout(() => { this.$runTasks(); }, Math.max(1, (1000 * config.LIGHTNING.GRAPH_REFRESH_INTERVAL) - (Date.now() - taskStartTime))); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|  | |||||||
| @ -30,7 +30,6 @@ export class BisqMasterPageComponent implements OnInit { | |||||||
|     this.connectionState$ = this.stateService.connectionState$; |     this.connectionState$ = this.stateService.connectionState$; | ||||||
|     this.urlLanguage = this.languageService.getLanguageForUrl(); |     this.urlLanguage = this.languageService.getLanguageForUrl(); | ||||||
|     this.navigationService.subnetPaths.subscribe((paths) => { |     this.navigationService.subnetPaths.subscribe((paths) => { | ||||||
|       console.log('network paths updated...'); |  | ||||||
|       this.networkPaths = paths; |       this.networkPaths = paths; | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -33,7 +33,6 @@ export class LiquidMasterPageComponent implements OnInit { | |||||||
|     this.network$ = merge(of(''), this.stateService.networkChanged$); |     this.network$ = merge(of(''), this.stateService.networkChanged$); | ||||||
|     this.urlLanguage = this.languageService.getLanguageForUrl(); |     this.urlLanguage = this.languageService.getLanguageForUrl(); | ||||||
|     this.navigationService.subnetPaths.subscribe((paths) => { |     this.navigationService.subnetPaths.subscribe((paths) => { | ||||||
|       console.log('network paths updated...'); |  | ||||||
|       this.networkPaths = paths; |       this.networkPaths = paths; | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -35,7 +35,6 @@ export class MasterPageComponent implements OnInit { | |||||||
|     this.urlLanguage = this.languageService.getLanguageForUrl(); |     this.urlLanguage = this.languageService.getLanguageForUrl(); | ||||||
|     this.subdomain = this.enterpriseService.getSubdomain(); |     this.subdomain = this.enterpriseService.getSubdomain(); | ||||||
|     this.navigationService.subnetPaths.subscribe((paths) => { |     this.navigationService.subnetPaths.subscribe((paths) => { | ||||||
|       console.log('network paths updated...'); |  | ||||||
|       this.networkPaths = paths; |       this.networkPaths = paths; | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -217,8 +217,8 @@ export interface IChannel { | |||||||
|   updated_at: string; |   updated_at: string; | ||||||
|   created: string; |   created: string; | ||||||
|   status: number; |   status: number; | ||||||
|   node_left: Node, |   node_left: INode, | ||||||
|   node_right: Node, |   node_right: INode, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -236,4 +236,6 @@ export interface INode { | |||||||
|   updated_at: string; |   updated_at: string; | ||||||
|   longitude: number; |   longitude: number; | ||||||
|   latitude: number; |   latitude: number; | ||||||
|  |   funding_balance?: number; | ||||||
|  |   closing_balance?: number; | ||||||
| } | } | ||||||
|  | |||||||
| @ -0,0 +1,19 @@ | |||||||
|  | <div class="box"> | ||||||
|  |   <table class="table table-borderless table-striped"> | ||||||
|  |     <tbody> | ||||||
|  |       <tr></tr> | ||||||
|  |       <tr> | ||||||
|  |         <td i18n="lightning.starting-balance|Channel starting balance">Starting balance</td> | ||||||
|  |         <td *ngIf="showStartingBalance && minStartingBalance === maxStartingBalance"><app-sats [satoshis]="minStartingBalance"></app-sats></td> | ||||||
|  |         <td *ngIf="showStartingBalance && minStartingBalance !== maxStartingBalance">{{ minStartingBalance | number : '1.0-0' }} - {{ maxStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></td> | ||||||
|  |         <td *ngIf="!showStartingBalance">?</td> | ||||||
|  |       </tr> | ||||||
|  |       <tr *ngIf="channel.status === 2"> | ||||||
|  |         <td i18n="lightning.closing-balance|Channel closing balance">Closing balance</td> | ||||||
|  |         <td *ngIf="showClosingBalance && minClosingBalance === maxClosingBalance"><app-sats [satoshis]="minClosingBalance"></app-sats></td> | ||||||
|  |         <td *ngIf="showClosingBalance && minClosingBalance !== maxClosingBalance">{{ minClosingBalance | number : '1.0-0' }} - {{ maxClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></td> | ||||||
|  |         <td *ngIf="!showClosingBalance">?</td> | ||||||
|  |       </tr> | ||||||
|  |     </tbody> | ||||||
|  |   </table> | ||||||
|  | </div> | ||||||
| @ -0,0 +1,9 @@ | |||||||
|  | .box { | ||||||
|  |   margin-top: 20px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @media (max-width: 768px) { | ||||||
|  |   .box { | ||||||
|  |     margin-bottom: 20px; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,25 @@ | |||||||
|  | import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||||
|  | 
 | ||||||
|  | import { ChannelCloseBoxComponent } from './channel-close-box.component'; | ||||||
|  | 
 | ||||||
|  | describe('ChannelCloseBoxComponent', () => { | ||||||
|  |   let component: ChannelCloseBoxComponent; | ||||||
|  |   let fixture: ComponentFixture<ChannelCloseBoxComponent>; | ||||||
|  | 
 | ||||||
|  |   beforeEach(async () => { | ||||||
|  |     await TestBed.configureTestingModule({ | ||||||
|  |       declarations: [ ChannelCloseBoxComponent ] | ||||||
|  |     }) | ||||||
|  |     .compileComponents(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   beforeEach(() => { | ||||||
|  |     fixture = TestBed.createComponent(ChannelCloseBoxComponent); | ||||||
|  |     component = fixture.componentInstance; | ||||||
|  |     fixture.detectChanges(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should create', () => { | ||||||
|  |     expect(component).toBeTruthy(); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @ -0,0 +1,58 @@ | |||||||
|  | import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-channel-close-box', | ||||||
|  |   templateUrl: './channel-close-box.component.html', | ||||||
|  |   styleUrls: ['./channel-close-box.component.scss'], | ||||||
|  |   changeDetection: ChangeDetectionStrategy.OnPush, | ||||||
|  | }) | ||||||
|  | export class ChannelCloseBoxComponent implements OnChanges { | ||||||
|  |   @Input() channel: any; | ||||||
|  |   @Input() local: any; | ||||||
|  |   @Input() remote: any; | ||||||
|  | 
 | ||||||
|  |   showStartingBalance: boolean = false; | ||||||
|  |   showClosingBalance: boolean = false; | ||||||
|  |   minStartingBalance: number; | ||||||
|  |   maxStartingBalance: number; | ||||||
|  |   minClosingBalance: number; | ||||||
|  |   maxClosingBalance: number; | ||||||
|  | 
 | ||||||
|  |   constructor() { } | ||||||
|  | 
 | ||||||
|  |   ngOnChanges(changes: SimpleChanges): void { | ||||||
|  |     if (this.channel && this.local && this.remote) { | ||||||
|  |       this.showStartingBalance = (this.local.funding_balance || this.remote.funding_balance) && this.channel.funding_ratio; | ||||||
|  |       this.showClosingBalance = this.local.closing_balance || this.remote.closing_balance; | ||||||
|  | 
 | ||||||
|  |       if (this.channel.single_funded) { | ||||||
|  |         if (this.local.funding_balance) { | ||||||
|  |           this.minStartingBalance = this.channel.capacity; | ||||||
|  |           this.maxStartingBalance = this.channel.capacity; | ||||||
|  |         } else if (this.remote.funding_balance) { | ||||||
|  |           this.minStartingBalance = 0; | ||||||
|  |           this.maxStartingBalance = 0; | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         this.minStartingBalance = clampRound(0, this.channel.capacity, this.local.funding_balance * this.channel.funding_ratio); | ||||||
|  |         this.maxStartingBalance = clampRound(0, this.channel.capacity, this.channel.capacity - (this.remote.funding_balance * this.channel.funding_ratio)); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const closingCapacity = this.channel.capacity - this.channel.closing_fee; | ||||||
|  |       this.minClosingBalance = clampRound(0, closingCapacity, this.local.closing_balance); | ||||||
|  |       this.maxClosingBalance = clampRound(0, closingCapacity, closingCapacity - this.remote.closing_balance); | ||||||
|  | 
 | ||||||
|  |       // margin of error to account for 2 x 330 sat anchor outputs
 | ||||||
|  |       if (Math.abs(this.minClosingBalance - this.maxClosingBalance) <= 660) { | ||||||
|  |         this.maxClosingBalance = this.minClosingBalance; | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       this.showStartingBalance = false; | ||||||
|  |       this.showClosingBalance = false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function clampRound(min: number, max: number, value: number): number { | ||||||
|  |   return Math.max(0, Math.min(max, Math.round(value))); | ||||||
|  | } | ||||||
| @ -48,6 +48,15 @@ | |||||||
|                 <td i18n="lightning.capacity">Capacity</td> |                 <td i18n="lightning.capacity">Capacity</td> | ||||||
|                 <td><app-sats [satoshis]="channel.capacity"></app-sats><app-fiat [value]="channel.capacity" digitsInfo="1.0-0"></app-fiat></td> |                 <td><app-sats [satoshis]="channel.capacity"></app-sats><app-fiat [value]="channel.capacity" digitsInfo="1.0-0"></app-fiat></td> | ||||||
|               </tr> |               </tr> | ||||||
|  |               <tr *ngIf="channel.closed_by"> | ||||||
|  |                 <td i18n="lightning.closed_by">Closed by</td> | ||||||
|  |                 <td> | ||||||
|  |                   <a [routerLink]="['/lightning/node' | relativeUrl, channel.closed_by]" > | ||||||
|  |                     <ng-container *ngIf="channel.closed_by === channel.node_left.public_key">{{ channel.node_left.alias }}</ng-container> | ||||||
|  |                     <ng-container *ngIf="channel.closed_by === channel.node_right.public_key">{{ channel.node_right.alias }}</ng-container> | ||||||
|  |                   </a> | ||||||
|  |                 </td> | ||||||
|  |               </tr> | ||||||
|             </tbody> |             </tbody> | ||||||
|           </table> |           </table> | ||||||
|         </div> |         </div> | ||||||
| @ -59,9 +68,11 @@ | |||||||
|   <div class="row row-cols-1 row-cols-md-2"> |   <div class="row row-cols-1 row-cols-md-2"> | ||||||
|     <div class="col"> |     <div class="col"> | ||||||
|       <app-channel-box [channel]="channel.node_left"></app-channel-box> |       <app-channel-box [channel]="channel.node_left"></app-channel-box> | ||||||
|  |       <app-channel-close-box *ngIf="showCloseBoxes(channel)" [channel]="channel" [local]="channel.node_left" [remote]="channel.node_right"></app-channel-close-box> | ||||||
|     </div> |     </div> | ||||||
|     <div class="col"> |     <div class="col"> | ||||||
|       <app-channel-box [channel]="channel.node_right"></app-channel-box> |       <app-channel-box [channel]="channel.node_right"></app-channel-box> | ||||||
|  |       <app-channel-close-box *ngIf="showCloseBoxes(channel)" [channel]="channel" [local]="channel.node_right" [remote]="channel.node_left"></app-channel-close-box> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -78,4 +78,9 @@ export class ChannelComponent implements OnInit { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   showCloseBoxes(channel: IChannel): boolean { | ||||||
|  |     return !!(channel.node_left.funding_balance || channel.node_left.closing_balance  | ||||||
|  |       || channel.node_right.funding_balance || channel.node_right.closing_balance); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ import { ChannelsListComponent } from './channels-list/channels-list.component'; | |||||||
| import { ChannelComponent } from './channel/channel.component'; | import { ChannelComponent } from './channel/channel.component'; | ||||||
| import { LightningWrapperComponent } from './lightning-wrapper/lightning-wrapper.component'; | import { LightningWrapperComponent } from './lightning-wrapper/lightning-wrapper.component'; | ||||||
| import { ChannelBoxComponent } from './channel/channel-box/channel-box.component'; | import { ChannelBoxComponent } from './channel/channel-box/channel-box.component'; | ||||||
|  | import { ChannelCloseBoxComponent } from './channel/channel-close-box/channel-close-box.component'; | ||||||
| import { ClosingTypeComponent } from './channel/closing-type/closing-type.component'; | import { ClosingTypeComponent } from './channel/closing-type/closing-type.component'; | ||||||
| import { LightningStatisticsChartComponent } from './statistics-chart/lightning-statistics-chart.component'; | import { LightningStatisticsChartComponent } from './statistics-chart/lightning-statistics-chart.component'; | ||||||
| import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component'; | import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component'; | ||||||
| @ -45,6 +46,7 @@ import { GroupComponent } from './group/group.component'; | |||||||
|     ChannelComponent, |     ChannelComponent, | ||||||
|     LightningWrapperComponent, |     LightningWrapperComponent, | ||||||
|     ChannelBoxComponent, |     ChannelBoxComponent, | ||||||
|  |     ChannelCloseBoxComponent, | ||||||
|     ClosingTypeComponent, |     ClosingTypeComponent, | ||||||
|     LightningStatisticsChartComponent, |     LightningStatisticsChartComponent, | ||||||
|     NodesNetworksChartComponent, |     NodesNetworksChartComponent, | ||||||
| @ -81,6 +83,7 @@ import { GroupComponent } from './group/group.component'; | |||||||
|     ChannelComponent, |     ChannelComponent, | ||||||
|     LightningWrapperComponent, |     LightningWrapperComponent, | ||||||
|     ChannelBoxComponent, |     ChannelBoxComponent, | ||||||
|  |     ChannelCloseBoxComponent, | ||||||
|     ClosingTypeComponent, |     ClosingTypeComponent, | ||||||
|     LightningStatisticsChartComponent, |     LightningStatisticsChartComponent, | ||||||
|     NodesNetworksChartComponent, |     NodesNetworksChartComponent, | ||||||
|  | |||||||
| @ -10,6 +10,8 @@ | |||||||
|     "POLL_RATE_MS": 1000, |     "POLL_RATE_MS": 1000, | ||||||
|     "INDEXING_BLOCKS_AMOUNT": -1, |     "INDEXING_BLOCKS_AMOUNT": -1, | ||||||
|     "BLOCKS_SUMMARIES_INDEXING": true, |     "BLOCKS_SUMMARIES_INDEXING": true, | ||||||
|  |     "ADVANCED_GBT_AUDIT": true, | ||||||
|  |     "ADVANCED_GBT_MEMPOOL": false, | ||||||
|     "USE_SECOND_NODE_FOR_MINFEE": true |     "USE_SECOND_NODE_FOR_MINFEE": true | ||||||
|   }, |   }, | ||||||
|   "SYSLOG" : { |   "SYSLOG" : { | ||||||
|  | |||||||
| @ -7,6 +7,8 @@ | |||||||
|     "SPAWN_CLUSTER_PROCS": 0, |     "SPAWN_CLUSTER_PROCS": 0, | ||||||
|     "API_URL_PREFIX": "/api/v1/", |     "API_URL_PREFIX": "/api/v1/", | ||||||
|     "INDEXING_BLOCKS_AMOUNT": -1, |     "INDEXING_BLOCKS_AMOUNT": -1, | ||||||
|  |     "ADVANCED_GBT_AUDIT": true, | ||||||
|  |     "ADVANCED_GBT_MEMPOOL": false, | ||||||
|     "POLL_RATE_MS": 1000 |     "POLL_RATE_MS": 1000 | ||||||
|   }, |   }, | ||||||
|   "SYSLOG" : { |   "SYSLOG" : { | ||||||
|  | |||||||
| @ -7,6 +7,8 @@ | |||||||
|     "SPAWN_CLUSTER_PROCS": 0, |     "SPAWN_CLUSTER_PROCS": 0, | ||||||
|     "API_URL_PREFIX": "/api/v1/", |     "API_URL_PREFIX": "/api/v1/", | ||||||
|     "INDEXING_BLOCKS_AMOUNT": -1, |     "INDEXING_BLOCKS_AMOUNT": -1, | ||||||
|  |     "ADVANCED_GBT_AUDIT": true, | ||||||
|  |     "ADVANCED_GBT_MEMPOOL": false, | ||||||
|     "POLL_RATE_MS": 1000 |     "POLL_RATE_MS": 1000 | ||||||
|   }, |   }, | ||||||
|   "SYSLOG" : { |   "SYSLOG" : { | ||||||
|  | |||||||
| @ -35,3 +35,5 @@ gzip_types application/javascript application/json application/ld+json applicati | |||||||
| # limit request body size | # limit request body size | ||||||
| client_max_body_size 10m; | client_max_body_size 10m; | ||||||
| 
 | 
 | ||||||
|  | # need to bump this up for about page sponsor images lol | ||||||
|  | http2_max_concurrent_streams 256; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user