move linked channel scan into forensics task, add backend throttling
This commit is contained in:
		
							parent
							
								
									5e1f54e862
								
							
						
					
					
						commit
						609f68eb24
					
				@ -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,248 @@ 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.$getChannelForensicsByClosingId(input.txid);
 | 
				
			||||||
 | 
					          if (closeChannel) {
 | 
				
			||||||
 | 
					            // this input directly spends a channel close output
 | 
				
			||||||
 | 
					            await this.$attributeChannelBalances(closeChannel, openChannel, input);
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            const prevOpenChannels = await channelsApi.$getChannelForensicsByOpeningId(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.$getChannelForensicsByClosingId(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 = 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);
 | 
				
			||||||
 | 
					        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)) {
 | 
				
			||||||
 | 
					            // 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();
 | 
				
			||||||
 | 
				
			|||||||
@ -305,256 +305,6 @@ class NetworkSyncService {
 | 
				
			|||||||
      logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e));
 | 
					      logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  private findLightningScript(vin: IEsploraApi.Vin): number {
 | 
					 | 
				
			||||||
    const topElement = vin.witness ? vin.witness[vin.witness.length - 2] : '';
 | 
					 | 
				
			||||||
      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
 | 
					 | 
				
			||||||
        if (topElement === '01') {
 | 
					 | 
				
			||||||
          // top element is '01' to get in the revocation path
 | 
					 | 
				
			||||||
          // 'Revoked Lightning Force Close';
 | 
					 | 
				
			||||||
          // Penalty force closed
 | 
					 | 
				
			||||||
          return 2;
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          // top element is '', this is a delayed to_local output
 | 
					 | 
				
			||||||
          // 'Lightning Force Close';
 | 
					 | 
				
			||||||
          return 3;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } else if (
 | 
					 | 
				
			||||||
        /^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(vin.inner_witnessscript_asm) ||
 | 
					 | 
				
			||||||
        /^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_IF OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_DROP OP_PUSHBYTES_3 \w{6} OP_CLTV OP_DROP OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(vin.inner_witnessscript_asm)
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        // 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
 | 
					 | 
				
			||||||
        if (topElement?.length === 66) {
 | 
					 | 
				
			||||||
          // top element is a public key
 | 
					 | 
				
			||||||
          // 'Revoked Lightning HTLC'; Penalty force closed
 | 
					 | 
				
			||||||
          return 4;
 | 
					 | 
				
			||||||
        } else if (topElement) {
 | 
					 | 
				
			||||||
          // top element is a preimage
 | 
					 | 
				
			||||||
          // 'Lightning HTLC';
 | 
					 | 
				
			||||||
          return 5;
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          // top element is '' to get in the expiry of the script
 | 
					 | 
				
			||||||
          // 'Expired Lightning HTLC';
 | 
					 | 
				
			||||||
          return 6;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } else if (/^OP_PUSHBYTES_33 \w{66} OP_CHECKSIG OP_IFDUP OP_NOTIF OP_PUSHNUM_16 OP_CSV OP_ENDIF$/.test(vin.inner_witnessscript_asm)) {
 | 
					 | 
				
			||||||
        // https://github.com/lightning/bolts/blob/master/03-transactions.md#to_local_anchor-and-to_remote_anchor-output-option_anchors
 | 
					 | 
				
			||||||
        if (topElement) {
 | 
					 | 
				
			||||||
          // top element is a signature
 | 
					 | 
				
			||||||
          // 'Lightning Anchor';
 | 
					 | 
				
			||||||
          return 7;
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          // top element is '', it has been swept after 16 blocks
 | 
					 | 
				
			||||||
          // 'Swept Lightning Anchor';
 | 
					 | 
				
			||||||
          return 8;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      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) {
 | 
					 | 
				
			||||||
        const openTx = await bitcoinApi.$getRawTransaction(openChannel.transaction_id);
 | 
					 | 
				
			||||||
        for (const input of openTx.vin) {
 | 
					 | 
				
			||||||
          const closeChannel = await channelsApi.$getChannelForensicsByClosingId(input.txid);
 | 
					 | 
				
			||||||
          if (closeChannel) {
 | 
					 | 
				
			||||||
            // this input directly spends a channel close output
 | 
					 | 
				
			||||||
            await this.$attributeChannelBalances(closeChannel, openChannel, input);
 | 
					 | 
				
			||||||
          } else {
 | 
					 | 
				
			||||||
            const prevOpenChannels = await channelsApi.$getChannelForensicsByOpeningId(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;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (Date.now() - runTimer > (config.LIGHTNING.GRAPH_REFRESH_INTERVAL * 1000)) {
 | 
					 | 
				
			||||||
          break;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      logger.info(`Open channels forensics scan complete.`);
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      logger.err('$runOpenedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 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> {
 | 
					 | 
				
			||||||
    const sweepTx = await bitcoinApi.$getRawTransaction(input.txid);
 | 
					 | 
				
			||||||
    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.$getChannelForensicsByClosingId(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: IEsploraApi.Transaction | undefined;
 | 
					 | 
				
			||||||
      let outspends: IEsploraApi.Outspend[] | undefined;
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        prevChannelTx = await bitcoinApi.$getRawTransaction(input.txid);
 | 
					 | 
				
			||||||
        outspends = await bitcoinApi.$getOutspends(input.txid);
 | 
					 | 
				
			||||||
      } 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 = 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 bitcoinApi.$getRawTransaction(outspend.txid);
 | 
					 | 
				
			||||||
              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);
 | 
					 | 
				
			||||||
        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)) {
 | 
					 | 
				
			||||||
            // 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);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default new NetworkSyncService();
 | 
					export default new NetworkSyncService();
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user