Closed channels forensics
This commit is contained in:
		
							parent
							
								
									fee6d3c4e9
								
							
						
					
					
						commit
						8ef5e7425b
					
				@ -29,7 +29,7 @@
 | 
			
		||||
                <td><app-time-since [dateString]="channel.updated_at"></app-time-since></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td i18n="address.total-sent">Transaction ID</td>
 | 
			
		||||
                <td i18n="address.total-sent">Opening transaction</td>
 | 
			
		||||
                <td>
 | 
			
		||||
                  <a [routerLink]="['/tx' | relativeUrl, channel.transaction_id + ':' + channel.transaction_vout]" >
 | 
			
		||||
                    <span>{{ channel.transaction_id | shortenString : 10 }}</span>
 | 
			
		||||
@ -37,6 +37,23 @@
 | 
			
		||||
                  <app-clipboard [text]="channel.transaction_id"></app-clipboard>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <ng-template [ngIf]="channel.closing_transaction_id">
 | 
			
		||||
                <tr *ngIf="channel.closing_transaction_id">
 | 
			
		||||
                  <td i18n="address.total-sent">Closing transaction</td>
 | 
			
		||||
                  <td>
 | 
			
		||||
                    <a [routerLink]="['/tx' | relativeUrl, channel.closing_transaction_id]" >
 | 
			
		||||
                      <span>{{ channel.closing_transaction_id | shortenString : 10 }}</span>
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <app-clipboard [text]="channel.closing_transaction_id"></app-clipboard>
 | 
			
		||||
                  </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td i18n="address.total-sent">Closing type</td>
 | 
			
		||||
                  <td>
 | 
			
		||||
                    <app-closing-type [type]="channel.closing_reason"></app-closing-type>
 | 
			
		||||
                  </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
              </ng-template>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
        </div>
 | 
			
		||||
@ -69,3 +86,11 @@
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<br>
 | 
			
		||||
 | 
			
		||||
<ng-template [ngIf]="error">
 | 
			
		||||
  <div class="text-center">
 | 
			
		||||
    <span i18n="error.general-loading-data">Error loading data.</span>
 | 
			
		||||
    <br><br>
 | 
			
		||||
    <i>{{ error.status }}: {{ error.error }}</i>
 | 
			
		||||
  </div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute, ParamMap } from '@angular/router';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { switchMap } from 'rxjs/operators';
 | 
			
		||||
import { Observable, of } from 'rxjs';
 | 
			
		||||
import { catchError, switchMap } from 'rxjs/operators';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { LightningApiService } from '../lightning-api.service';
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@ import { LightningApiService } from '../lightning-api.service';
 | 
			
		||||
})
 | 
			
		||||
export class ChannelComponent implements OnInit {
 | 
			
		||||
  channel$: Observable<any>;
 | 
			
		||||
  error: any = null;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private lightningApiService: LightningApiService,
 | 
			
		||||
@ -24,8 +25,16 @@ export class ChannelComponent implements OnInit {
 | 
			
		||||
    this.channel$ = this.activatedRoute.paramMap
 | 
			
		||||
      .pipe(
 | 
			
		||||
        switchMap((params: ParamMap) => {
 | 
			
		||||
          this.error = null;
 | 
			
		||||
          this.seoService.setTitle(`Channel: ${params.get('short_id')}`);
 | 
			
		||||
          return this.lightningApiService.getChannel$(params.get('short_id'));
 | 
			
		||||
          return this.lightningApiService.getChannel$(params.get('short_id'))
 | 
			
		||||
            .pipe(
 | 
			
		||||
              catchError((err) => {
 | 
			
		||||
                this.error = err;
 | 
			
		||||
                console.log(this.error);
 | 
			
		||||
                return of(null);
 | 
			
		||||
              })
 | 
			
		||||
            );
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1 @@
 | 
			
		||||
<span class="badge badge-pill badge-{{ label.class }}" >{{ label.label }}</span>
 | 
			
		||||
@ -0,0 +1,37 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-closing-type',
 | 
			
		||||
  templateUrl: './closing-type.component.html',
 | 
			
		||||
  styleUrls: ['./closing-type.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class ClosingTypeComponent implements OnChanges {
 | 
			
		||||
  @Input() type = 0;
 | 
			
		||||
  label: { label: string; class: string };
 | 
			
		||||
 | 
			
		||||
  ngOnChanges() {
 | 
			
		||||
    this.label = this.getLabelFromType(this.type);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getLabelFromType(type: number): { label: string; class: string } {
 | 
			
		||||
    switch (type) {
 | 
			
		||||
      case 1: return { 
 | 
			
		||||
        label: 'Mutually closed',
 | 
			
		||||
        class: 'success',
 | 
			
		||||
      };
 | 
			
		||||
      case 2: return {
 | 
			
		||||
        label: 'Force closed',
 | 
			
		||||
        class: 'warning',
 | 
			
		||||
      };
 | 
			
		||||
      case 3: return {
 | 
			
		||||
        label: 'Force closed with penalty',
 | 
			
		||||
        class: 'danger',
 | 
			
		||||
      };
 | 
			
		||||
      default: return {
 | 
			
		||||
        label: 'Unknown',
 | 
			
		||||
        class: 'secondary',
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -52,7 +52,12 @@
 | 
			
		||||
  <td class="d-none d-md-table-cell">
 | 
			
		||||
    <span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span>
 | 
			
		||||
    <span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span>
 | 
			
		||||
    <span class="badge rounded-pill badge-danger" *ngIf="channel.status === 2">Closed</span>
 | 
			
		||||
    <ng-template [ngIf]="channel.status === 2">
 | 
			
		||||
      <span class="badge rounded-pill badge-secondary" *ngIf="!channel.closing_reason; else closingReason">Closed</span>
 | 
			
		||||
      <ng-template #closingReason>
 | 
			
		||||
        <app-closing-type [type]="channel.closing_reason"></app-closing-type>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
  </td>
 | 
			
		||||
  <td class="capacity text-left d-none d-md-table-cell">
 | 
			
		||||
    {{ node.fee_rate }} <span class="symbol">ppm ({{ node.fee_rate / 10000 | number }}%)</span>
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ import { ChannelsListComponent } from './channels-list/channels-list.component';
 | 
			
		||||
import { ChannelComponent } from './channel/channel.component';
 | 
			
		||||
import { LightningWrapperComponent } from './lightning-wrapper/lightning-wrapper.component';
 | 
			
		||||
import { ChannelBoxComponent } from './channel/channel-box/channel-box.component';
 | 
			
		||||
import { ClosingTypeComponent } from './channel/closing-type/closing-type.component';
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
    LightningDashboardComponent,
 | 
			
		||||
@ -22,6 +23,7 @@ import { ChannelBoxComponent } from './channel/channel-box/channel-box.component
 | 
			
		||||
    ChannelComponent,
 | 
			
		||||
    LightningWrapperComponent,
 | 
			
		||||
    ChannelBoxComponent,
 | 
			
		||||
    ClosingTypeComponent,
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    CommonModule,
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,9 @@
 | 
			
		||||
    "API_URL_PREFIX": "/api/v1/",
 | 
			
		||||
    "STDOUT_LOG_MIN_PRIORITY": "debug"
 | 
			
		||||
  },
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "REST_API_URL": ""
 | 
			
		||||
  },
 | 
			
		||||
  "SYSLOG": {
 | 
			
		||||
    "ENABLED": false,
 | 
			
		||||
    "HOST": "127.0.0.1",
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,25 @@
 | 
			
		||||
import { IEsploraApi } from './esplora-api.interface';
 | 
			
		||||
 | 
			
		||||
export interface AbstractBitcoinApi {
 | 
			
		||||
  $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
 | 
			
		||||
  $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
 | 
			
		||||
  $getBlockHeightTip(): Promise<number>;
 | 
			
		||||
  $getTxIdsForBlock(hash: string): Promise<string[]>;
 | 
			
		||||
  $getBlockHash(height: number): Promise<string>;
 | 
			
		||||
  $getBlockHeader(hash: string): Promise<string>;
 | 
			
		||||
  $getBlock(hash: string): Promise<IEsploraApi.Block>;
 | 
			
		||||
  $getAddress(address: string): Promise<IEsploraApi.Address>;
 | 
			
		||||
  $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
 | 
			
		||||
  $getAddressPrefix(prefix: string): string[];
 | 
			
		||||
  $sendRawTransaction(rawTransaction: string): Promise<string>;
 | 
			
		||||
  $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
 | 
			
		||||
  $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
 | 
			
		||||
  $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
 | 
			
		||||
}
 | 
			
		||||
export interface BitcoinRpcCredentials {
 | 
			
		||||
  host: string;
 | 
			
		||||
  port: number;
 | 
			
		||||
  user: string;
 | 
			
		||||
  pass: string;
 | 
			
		||||
  timeout: number;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								lightning-backend/src/api/bitcoin/bitcoin-api-factory.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								lightning-backend/src/api/bitcoin/bitcoin-api-factory.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
import EsploraApi from './esplora-api';
 | 
			
		||||
import BitcoinApi from './bitcoin-api';
 | 
			
		||||
import bitcoinClient from './bitcoin-client';
 | 
			
		||||
 | 
			
		||||
function bitcoinApiFactory(): AbstractBitcoinApi {
 | 
			
		||||
  if (config.ESPLORA.REST_API_URL) {
 | 
			
		||||
    return new EsploraApi();
 | 
			
		||||
  } else {
 | 
			
		||||
    return new BitcoinApi(bitcoinClient);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default bitcoinApiFactory();
 | 
			
		||||
							
								
								
									
										175
									
								
								lightning-backend/src/api/bitcoin/bitcoin-api.interface.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								lightning-backend/src/api/bitcoin/bitcoin-api.interface.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,175 @@
 | 
			
		||||
export namespace IBitcoinApi {
 | 
			
		||||
  export interface MempoolInfo {
 | 
			
		||||
    loaded: boolean;                 //  (boolean) True if the mempool is fully loaded
 | 
			
		||||
    size: number;                    //  (numeric) Current tx count
 | 
			
		||||
    bytes: number;                   //  (numeric) Sum of all virtual transaction sizes as defined in BIP 141.
 | 
			
		||||
    usage: number;                   //  (numeric) Total memory usage for the mempool
 | 
			
		||||
    total_fee: number;               //  (numeric) Total fees of transactions in the mempool
 | 
			
		||||
    maxmempool: number;              //  (numeric) Maximum memory usage for the mempool
 | 
			
		||||
    mempoolminfee: number;           //  (numeric) Minimum fee rate in BTC/kB for tx to be accepted.
 | 
			
		||||
    minrelaytxfee: number;           //  (numeric) Current minimum relay fee for transactions
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface RawMempool { [txId: string]: MempoolEntry; }
 | 
			
		||||
 | 
			
		||||
  export interface MempoolEntry {
 | 
			
		||||
    vsize: number;                   //  (numeric) virtual transaction size as defined in BIP 141.
 | 
			
		||||
    weight: number;                  //  (numeric) transaction weight as defined in BIP 141.
 | 
			
		||||
    time: number;                    //  (numeric) local time transaction entered pool in seconds since 1 Jan 1970 GMT
 | 
			
		||||
    height: number;                  //  (numeric) block height when transaction entered pool
 | 
			
		||||
    descendantcount: number;         //  (numeric) number of in-mempool descendant transactions (including this one)
 | 
			
		||||
    descendantsize: number;          //  (numeric) virtual transaction size of in-mempool descendants (including this one)
 | 
			
		||||
    ancestorcount: number;           //  (numeric) number of in-mempool ancestor transactions (including this one)
 | 
			
		||||
    ancestorsize: number;            //  (numeric) virtual transaction size of in-mempool ancestors (including this one)
 | 
			
		||||
    wtxid: string;                   //  (string) hash of serialized transactionumber; including witness data
 | 
			
		||||
    fees: {
 | 
			
		||||
      base: number;                  //  (numeric) transaction fee in BTC
 | 
			
		||||
      modified: number;              //  (numeric) transaction fee with fee deltas used for mining priority in BTC
 | 
			
		||||
      ancestor: number;              //  (numeric) modified fees (see above) of in-mempool ancestors (including this one) in BTC
 | 
			
		||||
      descendant: number;            //  (numeric) modified fees (see above) of in-mempool descendants (including this one) in BTC
 | 
			
		||||
    };
 | 
			
		||||
    depends: string[];               //  (string) parent transaction id
 | 
			
		||||
    spentby: string[];               //  (array) unconfirmed transactions spending outputs from this transaction
 | 
			
		||||
    'bip125-replaceable': boolean;   //  (boolean) Whether this transaction could be replaced due to BIP125 (replace-by-fee)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Block {
 | 
			
		||||
    hash: string;                    //  (string) the block hash (same as provided)
 | 
			
		||||
    confirmations: number;           //  (numeric) The number of confirmations, or -1 if the block is not on the main chain
 | 
			
		||||
    size: number;                    //  (numeric) The block size
 | 
			
		||||
    strippedsize: number;            //  (numeric) The block size excluding witness data
 | 
			
		||||
    weight: number;                  //  (numeric) The block weight as defined in BIP 141
 | 
			
		||||
    height: number;                  //  (numeric) The block height or index
 | 
			
		||||
    version: number;                 //  (numeric) The block version
 | 
			
		||||
    versionHex: string;              //  (string) The block version formatted in hexadecimal
 | 
			
		||||
    merkleroot: string;              //  (string) The merkle root
 | 
			
		||||
    tx: Transaction[];
 | 
			
		||||
    time: number;                    //  (numeric) The block time expressed in UNIX epoch time
 | 
			
		||||
    mediantime: number;              //  (numeric) The median block time expressed in UNIX epoch time
 | 
			
		||||
    nonce: number;                   //  (numeric) The nonce
 | 
			
		||||
    bits: string;                    //  (string) The bits
 | 
			
		||||
    difficulty: number;              //  (numeric) The difficulty
 | 
			
		||||
    chainwork: string;               //  (string) Expected number of hashes required to produce the chain up to this block (in hex)
 | 
			
		||||
    nTx: number;                     //  (numeric) The number of transactions in the block
 | 
			
		||||
    previousblockhash: string;       //  (string) The hash of the previous block
 | 
			
		||||
    nextblockhash: string;           //  (string) The hash of the next block
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Transaction {
 | 
			
		||||
    in_active_chain: boolean;        //  (boolean) Whether specified block is in the active chain or not
 | 
			
		||||
    hex: string;                     //  (string) The serialized, hex-encoded data for 'txid'
 | 
			
		||||
    txid: string;                    //  (string) The transaction id (same as provided)
 | 
			
		||||
    hash: string;                    //  (string) The transaction hash (differs from txid for witness transactions)
 | 
			
		||||
    size: number;                    //  (numeric) The serialized transaction size
 | 
			
		||||
    vsize: number;                   //  (numeric) The virtual transaction size (differs from size for witness transactions)
 | 
			
		||||
    weight: number;                  //  (numeric) The transaction's weight (between vsize*4-3 and vsize*4)
 | 
			
		||||
    version: number;                 //  (numeric) The version
 | 
			
		||||
    locktime: number;                //  (numeric) The lock time
 | 
			
		||||
    vin: Vin[];
 | 
			
		||||
    vout: Vout[];
 | 
			
		||||
    blockhash: string;               //  (string) the block hash
 | 
			
		||||
    confirmations: number;           //  (numeric) The confirmations
 | 
			
		||||
    blocktime: number;               //  (numeric) The block time expressed in UNIX epoch time
 | 
			
		||||
    time: number;                    //  (numeric) Same as blocktime
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface VerboseBlock extends Block {
 | 
			
		||||
    tx: VerboseTransaction[];        // The transactions in the format of the getrawtransaction RPC. Different from verbosity = 1 "tx" result
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface VerboseTransaction extends Transaction {
 | 
			
		||||
    fee?: number;                   //  (numeric) The transaction fee in BTC, omitted if block undo data is not available
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Vin {
 | 
			
		||||
    txid?: string;                   //  (string) The transaction id
 | 
			
		||||
    vout?: number;                   //  (string)
 | 
			
		||||
    scriptSig?: {                    //  (json object) The script
 | 
			
		||||
      asm: string;                   //  (string) asm
 | 
			
		||||
      hex: string;                   //  (string) hex
 | 
			
		||||
    };
 | 
			
		||||
    sequence: number;                //  (numeric) The script sequence number
 | 
			
		||||
    txinwitness?: string[];          //  (string) hex-encoded witness data
 | 
			
		||||
    coinbase?: string;
 | 
			
		||||
    is_pegin?: boolean;              //  (boolean) Elements peg-in
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Vout {
 | 
			
		||||
    value: number;                   //  (numeric) The value in BTC
 | 
			
		||||
    n: number;                       //  (numeric) index
 | 
			
		||||
    asset?: string;                  //  (string) Elements asset id
 | 
			
		||||
    scriptPubKey: {                  //  (json object)
 | 
			
		||||
      asm: string;                   //  (string) the asm
 | 
			
		||||
      hex: string;                   //  (string) the hex
 | 
			
		||||
      reqSigs?: number;              //  (numeric) The required sigs
 | 
			
		||||
      type: string;                  //  (string) The type, eg 'pubkeyhash'
 | 
			
		||||
      address?: string;              //  (string) bitcoin address
 | 
			
		||||
      addresses?: string[];           //  (string) bitcoin addresses
 | 
			
		||||
      pegout_chain?: string;         //  (string) Elements peg-out chain
 | 
			
		||||
      pegout_addresses?: string[];   //  (string) Elements peg-out addresses
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface AddressInformation {
 | 
			
		||||
    isvalid: boolean;                //  (boolean) If the address is valid or not. If not, this is the only property returned.
 | 
			
		||||
    isvalid_parent?: boolean;        //  (boolean) Elements only
 | 
			
		||||
    address: string;                 //  (string) The bitcoin address validated
 | 
			
		||||
    scriptPubKey: string;            //  (string) The hex-encoded scriptPubKey generated by the address
 | 
			
		||||
    isscript: boolean;               //  (boolean) If the key is a script
 | 
			
		||||
    iswitness: boolean;              //  (boolean) If the address is a witness
 | 
			
		||||
    witness_version?: number;        //  (numeric, optional) The version number of the witness program
 | 
			
		||||
    witness_program: string;         //  (string, optional) The hex value of the witness program
 | 
			
		||||
    confidential_key?: string;       //  (string) Elements only
 | 
			
		||||
    unconfidential?: string;         //  (string) Elements only
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface ChainTips {
 | 
			
		||||
    height: number;                  //  (numeric) height of the chain tip
 | 
			
		||||
    hash: string;                    //  (string) block hash of the tip
 | 
			
		||||
    branchlen: number;               //  (numeric) zero for main chain, otherwise length of branch connecting the tip to the main chain
 | 
			
		||||
    status: 'invalid' | 'headers-only' | 'valid-headers' | 'valid-fork' | 'active';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface BlockchainInfo {
 | 
			
		||||
    chain: number;                   // (string) current network name as defined in BIP70 (main, test, regtest)
 | 
			
		||||
    blocks: number;                  // (numeric) the current number of blocks processed in the server
 | 
			
		||||
    headers: number;                 // (numeric) the current number of headers we have validated
 | 
			
		||||
    bestblockhash: string,           // (string) the hash of the currently best block
 | 
			
		||||
    difficulty: number;              // (numeric) the current difficulty
 | 
			
		||||
    mediantime: number;              // (numeric) median time for the current best block
 | 
			
		||||
    verificationprogress: number;    // (numeric) estimate of verification progress [0..1]
 | 
			
		||||
    initialblockdownload: boolean;   // (bool) (debug information) estimate of whether this node is in Initial Block Download mode.
 | 
			
		||||
    chainwork: string                // (string) total amount of work in active chain, in hexadecimal
 | 
			
		||||
    size_on_disk: number;            // (numeric) the estimated size of the block and undo files on disk
 | 
			
		||||
    pruned: number;                  // (boolean) if the blocks are subject to pruning
 | 
			
		||||
    pruneheight: number;             // (numeric) lowest-height complete block stored (only present if pruning is enabled)
 | 
			
		||||
    automatic_pruning: number;       // (boolean) whether automatic pruning is enabled (only present if pruning is enabled)
 | 
			
		||||
    prune_target_size: number;       // (numeric) the target size used by pruning (only present if automatic pruning is enabled)
 | 
			
		||||
    softforks: SoftFork[];           // (array) status of softforks in progress
 | 
			
		||||
    bip9_softforks: { [name: string]: Bip9SoftForks[] } // (object) status of BIP9 softforks in progress
 | 
			
		||||
    warnings: string;                // (string) any network and blockchain warnings.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface SoftFork {
 | 
			
		||||
    id: string;                      // (string) name of softfork
 | 
			
		||||
    version: number;                 // (numeric) block version
 | 
			
		||||
    reject: {                        // (object) progress toward rejecting pre-softfork blocks
 | 
			
		||||
      status: boolean;               // (boolean) true if threshold reached
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
  interface Bip9SoftForks {
 | 
			
		||||
    status: number;                  // (string) one of defined, started, locked_in, active, failed
 | 
			
		||||
    bit: number;                     // (numeric) the bit (0-28) in the block version field used to signal this softfork (only for started status)
 | 
			
		||||
    startTime: number;               // (numeric) the minimum median time past of a block at which the bit gains its meaning
 | 
			
		||||
    timeout: number;                 // (numeric) the median time past of a block at which the deployment is considered failed if not yet locked in
 | 
			
		||||
    since: number;                   // (numeric) height of the first block to which the status applies
 | 
			
		||||
    statistics: {                    // (object) numeric statistics about BIP9 signalling for a softfork (only for started status)
 | 
			
		||||
      period: number;                // (numeric) the length in blocks of the BIP9 signalling period 
 | 
			
		||||
      threshold: number;             // (numeric) the number of blocks with the version bit set required to activate the feature 
 | 
			
		||||
      elapsed: number;               // (numeric) the number of blocks elapsed since the beginning of the current period 
 | 
			
		||||
      count: number;                 // (numeric) the number of blocks with the version bit set in the current period 
 | 
			
		||||
      possible: boolean;             // (boolean) returns false if there are not enough blocks left in this period to pass activation threshold 
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										313
									
								
								lightning-backend/src/api/bitcoin/bitcoin-api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								lightning-backend/src/api/bitcoin/bitcoin-api.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,313 @@
 | 
			
		||||
import * as bitcoinjs from 'bitcoinjs-lib';
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
import { IBitcoinApi } from './bitcoin-api.interface';
 | 
			
		||||
import { IEsploraApi } from './esplora-api.interface';
 | 
			
		||||
 | 
			
		||||
class BitcoinApi implements AbstractBitcoinApi {
 | 
			
		||||
  protected bitcoindClient: any;
 | 
			
		||||
 | 
			
		||||
  constructor(bitcoinClient: any) {
 | 
			
		||||
    this.bitcoindClient = bitcoinClient;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddressPrefix(prefix: string): string[] {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlock(hash: string): Promise<IEsploraApi.Block> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawTransaction(txId: string, skipConversion = false, addPrevout = false, lazyPrevouts = false): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    return this.bitcoindClient.getRawTransaction(txId, true)
 | 
			
		||||
      .then((transaction: IBitcoinApi.Transaction) => {
 | 
			
		||||
        if (skipConversion) {
 | 
			
		||||
          transaction.vout.forEach((vout) => {
 | 
			
		||||
            vout.value = Math.round(vout.value * 100000000);
 | 
			
		||||
          });
 | 
			
		||||
          return transaction;
 | 
			
		||||
        }
 | 
			
		||||
        return this.$convertTransaction(transaction, addPrevout, lazyPrevouts);
 | 
			
		||||
      })
 | 
			
		||||
      .catch((e: Error) => {
 | 
			
		||||
        throw e;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHeightTip(): Promise<number> {
 | 
			
		||||
    return this.bitcoindClient.getChainTips()
 | 
			
		||||
      .then((result: IBitcoinApi.ChainTips[]) => {
 | 
			
		||||
        return result.find(tip => tip.status === 'active')!.height;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getTxIdsForBlock(hash: string): Promise<string[]> {
 | 
			
		||||
    return this.bitcoindClient.getBlock(hash, 1)
 | 
			
		||||
      .then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawBlock(hash: string): Promise<string> {
 | 
			
		||||
    return this.bitcoindClient.getBlock(hash, 0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHash(height: number): Promise<string> {
 | 
			
		||||
    return this.bitcoindClient.getBlockHash(height);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHeader(hash: string): Promise<string> {
 | 
			
		||||
    return this.bitcoindClient.getBlockHeader(hash, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddress(address: string): Promise<IEsploraApi.Address> {
 | 
			
		||||
    throw new Error('Method getAddress not supported by the Bitcoin RPC API.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
 | 
			
		||||
    throw new Error('Method getAddressTransactions not supported by the Bitcoin RPC API.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
 | 
			
		||||
    return this.bitcoindClient.getRawMemPool();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $sendRawTransaction(rawTransaction: string): Promise<string> {
 | 
			
		||||
    return this.bitcoindClient.sendRawTransaction(rawTransaction);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
 | 
			
		||||
    const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
 | 
			
		||||
    return {
 | 
			
		||||
      spent: txOut === null,
 | 
			
		||||
      status: {
 | 
			
		||||
        confirmed: true,
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
 | 
			
		||||
    const outSpends: IEsploraApi.Outspend[] = [];
 | 
			
		||||
    const tx = await this.$getRawTransaction(txId, true, false);
 | 
			
		||||
    for (let i = 0; i < tx.vout.length; i++) {
 | 
			
		||||
      if (tx.status && tx.status.block_height === 0) {
 | 
			
		||||
        outSpends.push({
 | 
			
		||||
          spent: false
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        const txOut = await this.bitcoindClient.getTxOut(txId, i);
 | 
			
		||||
        outSpends.push({
 | 
			
		||||
          spent: txOut === null,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return outSpends;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
 | 
			
		||||
    const outspends: IEsploraApi.Outspend[][] = [];
 | 
			
		||||
    for (const tx of txId) {
 | 
			
		||||
      const outspend = await this.$getOutspends(tx);
 | 
			
		||||
      outspends.push(outspend);
 | 
			
		||||
    }
 | 
			
		||||
    return outspends;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getEstimatedHashrate(blockHeight: number): Promise<number> {
 | 
			
		||||
    // 120 is the default block span in Core
 | 
			
		||||
    return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean, lazyPrevouts = false): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    let esploraTransaction: IEsploraApi.Transaction = {
 | 
			
		||||
      txid: transaction.txid,
 | 
			
		||||
      version: transaction.version,
 | 
			
		||||
      locktime: transaction.locktime,
 | 
			
		||||
      size: transaction.size,
 | 
			
		||||
      weight: transaction.weight,
 | 
			
		||||
      fee: 0,
 | 
			
		||||
      vin: [],
 | 
			
		||||
      vout: [],
 | 
			
		||||
      status: { confirmed: false },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    esploraTransaction.vout = transaction.vout.map((vout) => {
 | 
			
		||||
      return {
 | 
			
		||||
        value: Math.round(vout.value * 100000000),
 | 
			
		||||
        scriptpubkey: vout.scriptPubKey.hex,
 | 
			
		||||
        scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.address ? vout.scriptPubKey.address
 | 
			
		||||
          : vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '',
 | 
			
		||||
        scriptpubkey_asm: vout.scriptPubKey.asm ? this.convertScriptSigAsm(vout.scriptPubKey.hex) : '',
 | 
			
		||||
        scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type),
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    esploraTransaction.vin = transaction.vin.map((vin) => {
 | 
			
		||||
      return {
 | 
			
		||||
        is_coinbase: !!vin.coinbase,
 | 
			
		||||
        prevout: null,
 | 
			
		||||
        scriptsig: vin.scriptSig && vin.scriptSig.hex || vin.coinbase || '',
 | 
			
		||||
        scriptsig_asm: vin.scriptSig && this.convertScriptSigAsm(vin.scriptSig.hex) || '',
 | 
			
		||||
        sequence: vin.sequence,
 | 
			
		||||
        txid: vin.txid || '',
 | 
			
		||||
        vout: vin.vout || 0,
 | 
			
		||||
        witness: vin.txinwitness,
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (transaction.confirmations) {
 | 
			
		||||
      esploraTransaction.status = {
 | 
			
		||||
        confirmed: true,
 | 
			
		||||
        block_height: -1,
 | 
			
		||||
        block_hash: transaction.blockhash,
 | 
			
		||||
        block_time: transaction.blocktime,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (addPrevout) {
 | 
			
		||||
      esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, false, lazyPrevouts);
 | 
			
		||||
    } else if (!transaction.confirmations) {
 | 
			
		||||
      // esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return esploraTransaction;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private translateScriptPubKeyType(outputType: string): string {
 | 
			
		||||
    const map = {
 | 
			
		||||
      'pubkey': 'p2pk',
 | 
			
		||||
      'pubkeyhash': 'p2pkh',
 | 
			
		||||
      'scripthash': 'p2sh',
 | 
			
		||||
      'witness_v0_keyhash': 'v0_p2wpkh',
 | 
			
		||||
      'witness_v0_scripthash': 'v0_p2wsh',
 | 
			
		||||
      'witness_v1_taproot': 'v1_p2tr',
 | 
			
		||||
      'nonstandard': 'nonstandard',
 | 
			
		||||
      'multisig': 'multisig',
 | 
			
		||||
      'nulldata': 'op_return'
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (map[outputType]) {
 | 
			
		||||
      return map[outputType];
 | 
			
		||||
    } else {
 | 
			
		||||
      return 'unknown';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction, addPrevout: boolean, lazyPrevouts: boolean): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    if (transaction.vin[0].is_coinbase) {
 | 
			
		||||
      transaction.fee = 0;
 | 
			
		||||
      return transaction;
 | 
			
		||||
    }
 | 
			
		||||
    let totalIn = 0;
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < transaction.vin.length; i++) {
 | 
			
		||||
      if (lazyPrevouts && i > 12) {
 | 
			
		||||
        transaction.vin[i].lazy = true;
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      const innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false);
 | 
			
		||||
      transaction.vin[i].prevout = innerTx.vout[transaction.vin[i].vout];
 | 
			
		||||
      this.addInnerScriptsToVin(transaction.vin[i]);
 | 
			
		||||
      totalIn += innerTx.vout[transaction.vin[i].vout].value;
 | 
			
		||||
    }
 | 
			
		||||
    if (lazyPrevouts && transaction.vin.length > 12) {
 | 
			
		||||
      transaction.fee = -1;
 | 
			
		||||
    } else {
 | 
			
		||||
      const totalOut = transaction.vout.reduce((p, output) => p + output.value, 0);
 | 
			
		||||
      transaction.fee = parseFloat((totalIn - totalOut).toFixed(8));
 | 
			
		||||
    }
 | 
			
		||||
    return transaction;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private convertScriptSigAsm(hex: string): string {
 | 
			
		||||
    const buf = Buffer.from(hex, 'hex');
 | 
			
		||||
 | 
			
		||||
    const b: string[] = [];
 | 
			
		||||
 | 
			
		||||
    let i = 0;
 | 
			
		||||
    while (i < buf.length) {
 | 
			
		||||
      const op = buf[i];
 | 
			
		||||
      if (op >= 0x01 && op <= 0x4e) {
 | 
			
		||||
        i++;
 | 
			
		||||
        let push: number;
 | 
			
		||||
        if (op === 0x4c) {
 | 
			
		||||
          push = buf.readUInt8(i);
 | 
			
		||||
          b.push('OP_PUSHDATA1');
 | 
			
		||||
          i += 1;
 | 
			
		||||
        } else if (op === 0x4d) {
 | 
			
		||||
          push = buf.readUInt16LE(i);
 | 
			
		||||
          b.push('OP_PUSHDATA2');
 | 
			
		||||
          i += 2;
 | 
			
		||||
        } else if (op === 0x4e) {
 | 
			
		||||
          push = buf.readUInt32LE(i);
 | 
			
		||||
          b.push('OP_PUSHDATA4');
 | 
			
		||||
          i += 4;
 | 
			
		||||
        } else {
 | 
			
		||||
          push = op;
 | 
			
		||||
          b.push('OP_PUSHBYTES_' + push);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const data = buf.slice(i, i + push);
 | 
			
		||||
        if (data.length !== push) {
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        b.push(data.toString('hex'));
 | 
			
		||||
        i += data.length;
 | 
			
		||||
      } else {
 | 
			
		||||
        if (op === 0x00) {
 | 
			
		||||
          b.push('OP_0');
 | 
			
		||||
        } else if (op === 0x4f) {
 | 
			
		||||
          b.push('OP_PUSHNUM_NEG1');
 | 
			
		||||
        } else if (op === 0xb1) {
 | 
			
		||||
          b.push('OP_CLTV');
 | 
			
		||||
        } else if (op === 0xb2) {
 | 
			
		||||
          b.push('OP_CSV');
 | 
			
		||||
        } else if (op === 0xba) {
 | 
			
		||||
          b.push('OP_CHECKSIGADD');
 | 
			
		||||
        } else {
 | 
			
		||||
          const opcode = bitcoinjs.script.toASM([ op ]);
 | 
			
		||||
          if (opcode && op < 0xfd) {
 | 
			
		||||
            if (/^OP_(\d+)$/.test(opcode)) {
 | 
			
		||||
              b.push(opcode.replace(/^OP_(\d+)$/, 'OP_PUSHNUM_$1'));
 | 
			
		||||
            } else {
 | 
			
		||||
              b.push(opcode);
 | 
			
		||||
            }
 | 
			
		||||
          } else {
 | 
			
		||||
            b.push('OP_RETURN_' + op);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        i += 1;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return b.join(' ');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private addInnerScriptsToVin(vin: IEsploraApi.Vin): void {
 | 
			
		||||
    if (!vin.prevout) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (vin.prevout.scriptpubkey_type === 'p2sh') {
 | 
			
		||||
      const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0];
 | 
			
		||||
      vin.inner_redeemscript_asm = this.convertScriptSigAsm(redeemScript);
 | 
			
		||||
      if (vin.witness && vin.witness.length > 2) {
 | 
			
		||||
        const witnessScript = vin.witness[vin.witness.length - 1];
 | 
			
		||||
        vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) {
 | 
			
		||||
      const witnessScript = vin.witness[vin.witness.length - 1];
 | 
			
		||||
      vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness && vin.witness.length > 1) {
 | 
			
		||||
      const witnessScript = vin.witness[vin.witness.length - 2];
 | 
			
		||||
      vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default BitcoinApi;
 | 
			
		||||
							
								
								
									
										172
									
								
								lightning-backend/src/api/bitcoin/esplora-api.interface.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								lightning-backend/src/api/bitcoin/esplora-api.interface.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,172 @@
 | 
			
		||||
export namespace IEsploraApi {
 | 
			
		||||
  export interface Transaction {
 | 
			
		||||
    txid: string;
 | 
			
		||||
    version: number;
 | 
			
		||||
    locktime: number;
 | 
			
		||||
    size: number;
 | 
			
		||||
    weight: number;
 | 
			
		||||
    fee: number;
 | 
			
		||||
    vin: Vin[];
 | 
			
		||||
    vout: Vout[];
 | 
			
		||||
    status: Status;
 | 
			
		||||
    hex?: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Recent {
 | 
			
		||||
    txid: string;
 | 
			
		||||
    fee: number;
 | 
			
		||||
    vsize: number;
 | 
			
		||||
    value: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Vin {
 | 
			
		||||
    txid: string;
 | 
			
		||||
    vout: number;
 | 
			
		||||
    is_coinbase: boolean;
 | 
			
		||||
    scriptsig: string;
 | 
			
		||||
    scriptsig_asm: string;
 | 
			
		||||
    inner_redeemscript_asm: string;
 | 
			
		||||
    inner_witnessscript_asm: string;
 | 
			
		||||
    sequence: any;
 | 
			
		||||
    witness: string[];
 | 
			
		||||
    prevout: Vout | null;
 | 
			
		||||
    // Elements
 | 
			
		||||
    is_pegin?: boolean;
 | 
			
		||||
    issuance?: Issuance;
 | 
			
		||||
    // Custom
 | 
			
		||||
    lazy?: boolean;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface Issuance {
 | 
			
		||||
    asset_id: string;
 | 
			
		||||
    is_reissuance: string;
 | 
			
		||||
    asset_blinding_nonce: string;
 | 
			
		||||
    asset_entropy: string;
 | 
			
		||||
    contract_hash: string;
 | 
			
		||||
    assetamount?: number;
 | 
			
		||||
    assetamountcommitment?: string;
 | 
			
		||||
    tokenamount?: number;
 | 
			
		||||
    tokenamountcommitment?: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Vout {
 | 
			
		||||
    scriptpubkey: string;
 | 
			
		||||
    scriptpubkey_asm: string;
 | 
			
		||||
    scriptpubkey_type: string;
 | 
			
		||||
    scriptpubkey_address: string;
 | 
			
		||||
    value: number;
 | 
			
		||||
    // Elements
 | 
			
		||||
    valuecommitment?: number;
 | 
			
		||||
    asset?: string;
 | 
			
		||||
    pegout?: Pegout;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface Pegout {
 | 
			
		||||
    genesis_hash: string;
 | 
			
		||||
    scriptpubkey: string;
 | 
			
		||||
    scriptpubkey_asm: string;
 | 
			
		||||
    scriptpubkey_address: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Status {
 | 
			
		||||
    confirmed: boolean;
 | 
			
		||||
    block_height?: number;
 | 
			
		||||
    block_hash?: string;
 | 
			
		||||
    block_time?: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Block {
 | 
			
		||||
    id: string;
 | 
			
		||||
    height: number;
 | 
			
		||||
    version: number;
 | 
			
		||||
    timestamp: number;
 | 
			
		||||
    bits: number;
 | 
			
		||||
    nonce: number;
 | 
			
		||||
    difficulty: number;
 | 
			
		||||
    merkle_root: string;
 | 
			
		||||
    tx_count: number;
 | 
			
		||||
    size: number;
 | 
			
		||||
    weight: number;
 | 
			
		||||
    previousblockhash: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Address {
 | 
			
		||||
    address: string;
 | 
			
		||||
    chain_stats: ChainStats;
 | 
			
		||||
    mempool_stats: MempoolStats;
 | 
			
		||||
    electrum?: boolean;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface ChainStats {
 | 
			
		||||
    funded_txo_count: number;
 | 
			
		||||
    funded_txo_sum: number;
 | 
			
		||||
    spent_txo_count: number;
 | 
			
		||||
    spent_txo_sum: number;
 | 
			
		||||
    tx_count: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface MempoolStats {
 | 
			
		||||
    funded_txo_count: number;
 | 
			
		||||
    funded_txo_sum: number;
 | 
			
		||||
    spent_txo_count: number;
 | 
			
		||||
    spent_txo_sum: number;
 | 
			
		||||
    tx_count: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Outspend {
 | 
			
		||||
    spent: boolean;
 | 
			
		||||
    txid?: string;
 | 
			
		||||
    vin?: number;
 | 
			
		||||
    status?: Status;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Asset {
 | 
			
		||||
    asset_id: string;
 | 
			
		||||
    issuance_txin: IssuanceTxin;
 | 
			
		||||
    issuance_prevout: IssuancePrevout;
 | 
			
		||||
    reissuance_token: string;
 | 
			
		||||
    contract_hash: string;
 | 
			
		||||
    status: Status;
 | 
			
		||||
    chain_stats: AssetStats;
 | 
			
		||||
    mempool_stats: AssetStats;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface AssetExtended extends Asset {
 | 
			
		||||
    name: string;
 | 
			
		||||
    ticker: string;
 | 
			
		||||
    precision: number;
 | 
			
		||||
    entity: Entity;
 | 
			
		||||
    version: number;
 | 
			
		||||
    issuer_pubkey: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Entity {
 | 
			
		||||
    domain: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface IssuanceTxin {
 | 
			
		||||
    txid: string;
 | 
			
		||||
    vin: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface IssuancePrevout {
 | 
			
		||||
    txid: string;
 | 
			
		||||
    vout: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface AssetStats {
 | 
			
		||||
    tx_count: number;
 | 
			
		||||
    issuance_count: number;
 | 
			
		||||
    issued_amount: number;
 | 
			
		||||
    burned_amount: number;
 | 
			
		||||
    has_blinded_issuances: boolean;
 | 
			
		||||
    reissuance_tokens: number;
 | 
			
		||||
    burned_reissuance_tokens: number;
 | 
			
		||||
    peg_in_count: number;
 | 
			
		||||
    peg_in_amount: number;
 | 
			
		||||
    peg_out_count: number;
 | 
			
		||||
    peg_out_amount: number;
 | 
			
		||||
    burn_count: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										84
									
								
								lightning-backend/src/api/bitcoin/esplora-api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								lightning-backend/src/api/bitcoin/esplora-api.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import axios, { AxiosRequestConfig } from 'axios';
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
import { IEsploraApi } from './esplora-api.interface';
 | 
			
		||||
 | 
			
		||||
class ElectrsApi implements AbstractBitcoinApi {
 | 
			
		||||
  axiosConfig: AxiosRequestConfig = {
 | 
			
		||||
    timeout: 10000,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
 | 
			
		||||
    return axios.get<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids', this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    return axios.get<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHeightTip(): Promise<number> {
 | 
			
		||||
    return axios.get<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getTxIdsForBlock(hash: string): Promise<string[]> {
 | 
			
		||||
    return axios.get<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHash(height: number): Promise<string> {
 | 
			
		||||
    return axios.get<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height, this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHeader(hash: string): Promise<string> {
 | 
			
		||||
    return axios.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header', this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlock(hash: string): Promise<IEsploraApi.Block> {
 | 
			
		||||
    return axios.get<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddress(address: string): Promise<IEsploraApi.Address> {
 | 
			
		||||
    throw new Error('Method getAddress not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
 | 
			
		||||
    throw new Error('Method getAddressTransactions not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddressPrefix(prefix: string): string[] {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $sendRawTransaction(rawTransaction: string): Promise<string> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
 | 
			
		||||
    return axios.get<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout, this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
 | 
			
		||||
    return axios.get<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
 | 
			
		||||
    const outspends: IEsploraApi.Outspend[][] = [];
 | 
			
		||||
    for (const tx of txId) {
 | 
			
		||||
      const outspend = await this.$getOutspends(tx);
 | 
			
		||||
      outspends.push(outspend);
 | 
			
		||||
    }
 | 
			
		||||
    return outspends;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ElectrsApi;
 | 
			
		||||
@ -36,6 +36,17 @@ class ChannelsApi {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getClosedChannelsWithoutReason(): Promise<any[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const query = `SELECT * FROM channels WHERE status = 2 AND closing_reason IS NULL`;
 | 
			
		||||
      const [rows]: any = await DB.query(query);
 | 
			
		||||
      return rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$getClosedChannelsWithoutReason error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getChannelsWithoutCreatedDate(): Promise<any[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const query = `SELECT * FROM channels WHERE created IS NULL`;
 | 
			
		||||
@ -115,6 +126,8 @@ class ChannelsApi {
 | 
			
		||||
      'capacity': channel.capacity,
 | 
			
		||||
      'transaction_id': channel.transaction_id,
 | 
			
		||||
      'transaction_vout': channel.transaction_vout,
 | 
			
		||||
      'closing_transaction_id': channel.closing_transaction_id,
 | 
			
		||||
      'closing_reason': channel.closing_reason,
 | 
			
		||||
      'updated_at': channel.updated_at,
 | 
			
		||||
      'created': channel.created,
 | 
			
		||||
      'status': channel.status,
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,9 @@ interface IConfig {
 | 
			
		||||
    API_URL_PREFIX: string;
 | 
			
		||||
    STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
 | 
			
		||||
  };
 | 
			
		||||
  ESPLORA: {
 | 
			
		||||
    REST_API_URL: string;
 | 
			
		||||
  };
 | 
			
		||||
  SYSLOG: {
 | 
			
		||||
    ENABLED: boolean;
 | 
			
		||||
    HOST: string;
 | 
			
		||||
@ -43,6 +46,9 @@ const defaults: IConfig = {
 | 
			
		||||
    'API_URL_PREFIX': '/api/v1/',
 | 
			
		||||
    'STDOUT_LOG_MIN_PRIORITY': 'debug',
 | 
			
		||||
  },
 | 
			
		||||
  'ESPLORA': {
 | 
			
		||||
    'REST_API_URL': 'http://127.0.0.1:3000',
 | 
			
		||||
  },
 | 
			
		||||
  'SYSLOG': {
 | 
			
		||||
    'ENABLED': true,
 | 
			
		||||
    'HOST': '127.0.0.1',
 | 
			
		||||
@ -72,6 +78,7 @@ const defaults: IConfig = {
 | 
			
		||||
 | 
			
		||||
class Config implements IConfig {
 | 
			
		||||
  MEMPOOL: IConfig['MEMPOOL'];
 | 
			
		||||
  ESPLORA: IConfig['ESPLORA'];
 | 
			
		||||
  SYSLOG: IConfig['SYSLOG'];
 | 
			
		||||
  LN_NODE_AUTH: IConfig['LN_NODE_AUTH'];
 | 
			
		||||
  CORE_RPC: IConfig['CORE_RPC'];
 | 
			
		||||
@ -80,6 +87,7 @@ class Config implements IConfig {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    const configs = this.merge(configFile, defaults);
 | 
			
		||||
    this.MEMPOOL = configs.MEMPOOL;
 | 
			
		||||
    this.ESPLORA = configs.ESPLORA;
 | 
			
		||||
    this.SYSLOG = configs.SYSLOG;
 | 
			
		||||
    this.LN_NODE_AUTH = configs.LN_NODE_AUTH;
 | 
			
		||||
    this.CORE_RPC = configs.CORE_RPC;
 | 
			
		||||
 | 
			
		||||
@ -213,6 +213,8 @@ class DatabaseMigration {
 | 
			
		||||
      updated_at datetime DEFAULT NULL,
 | 
			
		||||
      created datetime DEFAULT NULL,
 | 
			
		||||
      status int(11) NOT NULL DEFAULT 0,
 | 
			
		||||
      closing_transaction_id varchar(64) DEFAULT NULL,
 | 
			
		||||
      closing_reason int(11) DEFAULT NULL,
 | 
			
		||||
      node1_public_key varchar(66) NOT NULL,
 | 
			
		||||
      node1_base_fee_mtokens bigint(20) unsigned DEFAULT NULL,
 | 
			
		||||
      node1_cltv_delta int(11) DEFAULT NULL,
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,10 @@ import lightningApi from '../api/lightning/lightning-api-factory';
 | 
			
		||||
import { ILightningApi } from '../api/lightning/lightning-api.interface';
 | 
			
		||||
import channelsApi from '../api/explorer/channels.api';
 | 
			
		||||
import bitcoinClient from '../api/bitcoin/bitcoin-client';
 | 
			
		||||
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import { IEsploraApi } from '../api/bitcoin/esplora-api.interface';
 | 
			
		||||
import e from 'express';
 | 
			
		||||
 | 
			
		||||
class NodeSyncService {
 | 
			
		||||
  constructor() {}
 | 
			
		||||
@ -38,15 +42,18 @@ class NodeSyncService {
 | 
			
		||||
      await this.$findInactiveNodesAndChannels();
 | 
			
		||||
      logger.debug(`Inactive channels scan complete`);
 | 
			
		||||
 | 
			
		||||
      await this.$scanForClosedChannels();
 | 
			
		||||
      logger.debug(`Closed channels scan complete`);
 | 
			
		||||
 | 
			
		||||
      await this.$lookUpCreationDateFromChain();
 | 
			
		||||
      logger.debug(`Channel creation dates scan complete`);
 | 
			
		||||
 | 
			
		||||
      await this.$updateNodeFirstSeen();
 | 
			
		||||
      logger.debug(`Node first seen dates scan complete`);
 | 
			
		||||
 | 
			
		||||
      await this.$scanForClosedChannels();
 | 
			
		||||
      logger.debug(`Closed channels scan complete`);
 | 
			
		||||
 | 
			
		||||
      await this.$runClosedChannelsForensics();
 | 
			
		||||
      logger.debug(`Closed channels forensics scan complete`);
 | 
			
		||||
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$updateNodes() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
@ -109,17 +116,129 @@ class NodeSyncService {
 | 
			
		||||
    try {
 | 
			
		||||
      const channels = await channelsApi.$getChannelsByStatus(0);
 | 
			
		||||
      for (const channel of channels) {
 | 
			
		||||
        const outspends = await bitcoinClient.getTxOut(channel.transaction_id, channel.transaction_vout);
 | 
			
		||||
        if (outspends === null) {
 | 
			
		||||
        const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout);
 | 
			
		||||
        if (spendingTx.spent === true && spendingTx.status?.confirmed === true) {
 | 
			
		||||
          logger.debug('Marking channel: ' + channel.id + ' as closed.');
 | 
			
		||||
          await DB.query(`UPDATE channels SET status = 2 WHERE id = ?`, [channel.id]);
 | 
			
		||||
          if (spendingTx.txid && !channel.closing_transaction_id) {
 | 
			
		||||
            await DB.query(`UPDATE channels SET closing_transaction_id = ? WHERE id = ?`, [spendingTx.txid, channel.id]);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$updateNodes() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
    1. Mutually closed
 | 
			
		||||
    2. Forced closed
 | 
			
		||||
    3. Forced closed with penalty
 | 
			
		||||
  */
 | 
			
		||||
 | 
			
		||||
  private async $runClosedChannelsForensics(): Promise<void> {
 | 
			
		||||
    if (!config.ESPLORA.REST_API_URL) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const channels = await channelsApi.$getClosedChannelsWithoutReason();
 | 
			
		||||
      for (const channel of channels) {
 | 
			
		||||
        let reason = 0;
 | 
			
		||||
        // Only Esplora backend can retrieve spent transaction outputs
 | 
			
		||||
        const outspends = await bitcoinApi.$getOutspends(channel.closing_transaction_id);
 | 
			
		||||
        const lightningScriptReasons: number[] = [];
 | 
			
		||||
        for (const outspend of outspends) {
 | 
			
		||||
          if (outspend.spent && outspend.txid) {
 | 
			
		||||
            const spendingTx = await bitcoinApi.$getRawTransaction(outspend.txid);
 | 
			
		||||
            const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
 | 
			
		||||
            lightningScriptReasons.push(lightningScript);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        if (lightningScriptReasons.length === outspends.length
 | 
			
		||||
          && lightningScriptReasons.filter((r) => r === 1).length === outspends.length) {
 | 
			
		||||
          reason = 1;
 | 
			
		||||
        } else {
 | 
			
		||||
          const filteredReasons = lightningScriptReasons.filter((r) => r !== 1);
 | 
			
		||||
          if (filteredReasons.length) {
 | 
			
		||||
            if (filteredReasons.some((r) => r === 2 || r === 4)) {
 | 
			
		||||
              reason = 3;
 | 
			
		||||
            } else {
 | 
			
		||||
              reason = 2;
 | 
			
		||||
            }
 | 
			
		||||
          } else {
 | 
			
		||||
            /*
 | 
			
		||||
              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
 | 
			
		||||
            */
 | 
			
		||||
            const closingTx = await bitcoinApi.$getRawTransaction(channel.closing_transaction_id);
 | 
			
		||||
            const sequenceHex: string = closingTx.vin[0].sequence.toString(16);
 | 
			
		||||
            const locktimeHex: string = closingTx.locktime.toString(16);
 | 
			
		||||
            if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') {
 | 
			
		||||
              reason = 2; // Here we can't be sure if it's a penalty or not
 | 
			
		||||
            } else {
 | 
			
		||||
              reason = 1;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        if (reason) {
 | 
			
		||||
          logger.debug('Setting closing reason ' + reason + ' for channel: ' + channel.id + '.');
 | 
			
		||||
          await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$runClosedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private findLightningScript(vin: IEsploraApi.Vin): number {
 | 
			
		||||
    const topElement = 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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $saveChannel(channel: ILightningApi.Channel): Promise<void> {
 | 
			
		||||
    const fromChannel = chanNumber({ channel: channel.id }).number;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user