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,16 +99,9 @@ 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 {
|
continue;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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]);
|
||||||
@ -124,16 +121,9 @@ 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 {
|
continue;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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);
|
||||||
@ -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