Address & script parsing refactor
This commit is contained in:
		
							parent
							
								
									3b419be341
								
							
						
					
					
						commit
						7dfdb5553e
					
				| @ -1,7 +1,7 @@ | ||||
| import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core'; | ||||
| import { Vin, Vout } from '../../interfaces/electrs.interface'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { parseMultisigScript } from '../../bitcoin.utils'; | ||||
| import { AddressType, AddressTypeInfo } from '../../shared/address-utils'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-address-labels', | ||||
| @ -12,6 +12,7 @@ import { parseMultisigScript } from '../../bitcoin.utils'; | ||||
| export class AddressLabelsComponent implements OnChanges { | ||||
|   network = ''; | ||||
| 
 | ||||
|   @Input() address: AddressTypeInfo; | ||||
|   @Input() vin: Vin; | ||||
|   @Input() vout: Vout; | ||||
|   @Input() channel: any; | ||||
| @ -28,10 +29,10 @@ export class AddressLabelsComponent implements OnChanges { | ||||
|   ngOnChanges() { | ||||
|     if (this.channel) { | ||||
|       this.handleChannel(); | ||||
|     } else if (this.address) { | ||||
|       this.handleAddress(); | ||||
|     } else if (this.vin) { | ||||
|       this.handleVin(); | ||||
|     } else if (this.vout) { | ||||
|       this.handleVout(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -42,74 +43,22 @@ export class AddressLabelsComponent implements OnChanges { | ||||
|     this.label = `Channel ${type}: ${leftNodeName} <> ${rightNodeName}`; | ||||
|   } | ||||
| 
 | ||||
|   handleAddress() { | ||||
|     if (this.address?.scripts.size) { | ||||
|       const script = this.address?.scripts.values().next().value; | ||||
|       if (script.template?.label) { | ||||
|         this.label = script.template.label; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   handleVin() { | ||||
|     if (this.vin.inner_witnessscript_asm) { | ||||
|       if (this.vin.inner_witnessscript_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0 || this.vin.inner_witnessscript_asm.indexOf('OP_PUSHNUM_15 OP_CHECKMULTISIG OP_IFDUP OP_NOTIF OP_PUSHBYTES_2') === 1259) { | ||||
|         if (this.vin.witness.length > 11) { | ||||
|           this.label = 'Liquid Peg Out'; | ||||
|         } else { | ||||
|           this.label = 'Emergency Liquid Peg Out'; | ||||
|         } | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       const topElement = this.vin.witness[this.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(this.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
 | ||||
|           this.label = 'Revoked Lightning Force Close'; | ||||
|         } else { | ||||
|           // top element is '', this is a delayed to_local output
 | ||||
|           this.label = 'Lightning Force Close'; | ||||
|         } | ||||
|         return; | ||||
|       } 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(this.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(this.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
 | ||||
|           this.label = 'Revoked Lightning HTLC'; | ||||
|         } else if (topElement) { | ||||
|           // top element is a preimage
 | ||||
|           this.label = 'Lightning HTLC'; | ||||
|         } else { | ||||
|           // top element is '' to get in the expiry of the script
 | ||||
|           this.label = 'Expired Lightning HTLC'; | ||||
|         } | ||||
|         return; | ||||
|       } else if (/^OP_PUSHBYTES_33 \w{66} OP_CHECKSIG OP_IFDUP OP_NOTIF OP_PUSHNUM_16 OP_CSV OP_ENDIF$/.test(this.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
 | ||||
|           this.label = 'Lightning Anchor'; | ||||
|         } else { | ||||
|           // top element is '', it has been swept after 16 blocks
 | ||||
|           this.label = 'Swept Lightning Anchor'; | ||||
|         } | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       this.detectMultisig(this.vin.inner_witnessscript_asm); | ||||
|     } | ||||
| 
 | ||||
|     this.detectMultisig(this.vin.inner_redeemscript_asm); | ||||
| 
 | ||||
|     this.detectMultisig(this.vin.prevout.scriptpubkey_asm); | ||||
|   } | ||||
| 
 | ||||
|   detectMultisig(script: string) { | ||||
|     const ms = parseMultisigScript(script); | ||||
| 
 | ||||
|     if (ms) { | ||||
|       this.label = $localize`:@@address-label.multisig:Multisig ${ms.m}:multisigM: of ${ms.n}:multisigN:`; | ||||
|     const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin]) | ||||
|     if (address?.scripts.size) { | ||||
|       const script = address?.scripts.values().next().value; | ||||
|       if (script.template?.label) { | ||||
|         this.label = script.template.label; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|   handleVout() { | ||||
|     this.detectMultisig(this.vout.scriptpubkey_asm); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -220,14 +220,14 @@ | ||||
| <ng-template #volumeRow> | ||||
|   <tr> | ||||
|     <td i18n="address.volume">Volume</td> | ||||
|     <td><app-amount [satoshis]="chainStats.volume + mempoolStats.volume"></app-amount></td> | ||||
|     <td *ngIf="chainStats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="chainStats.volume + mempoolStats.volume"></app-amount></td> | ||||
|   </tr> | ||||
| </ng-template> | ||||
| 
 | ||||
| <ng-template #typeRow> | ||||
|   <tr> | ||||
|     <td i18n="address.type">Type</td> | ||||
|     <td><app-address-type [vout]="exampleVout || exampleVin?.prevout || null"></app-address-type><app-address-labels [channel]="exampleChannel" [vin]="exampleVin" [vout]="exampleVout" class="ml-1"></app-address-labels></td> | ||||
|     <td><app-address-type [address]="addressTypeInfo"></app-address-type><app-address-labels [channel]="exampleChannel" [address]="addressTypeInfo" class="ml-1"></app-address-labels></td> | ||||
|   </tr> | ||||
| </ng-template> | ||||
| 
 | ||||
|  | ||||
| @ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy, HostListener } from '@angular/core'; | ||||
| import { ActivatedRoute, ParamMap } from '@angular/router'; | ||||
| import { ElectrsApiService } from '../../services/electrs-api.service'; | ||||
| import { switchMap, filter, catchError, map, tap } from 'rxjs/operators'; | ||||
| import { Address, ChainStats, Transaction, Vin, Vout } from '../../interfaces/electrs.interface'; | ||||
| import { Address, ChainStats, Transaction, Vin } from '../../interfaces/electrs.interface'; | ||||
| import { WebsocketService } from '../../services/websocket.service'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { AudioService } from '../../services/audio.service'; | ||||
| @ -11,6 +11,7 @@ import { of, merge, Subscription, Observable } from 'rxjs'; | ||||
| import { SeoService } from '../../services/seo.service'; | ||||
| import { seoDescriptionNetwork } from '../../shared/common.utils'; | ||||
| import { AddressInformation } from '../../interfaces/node-api.interface'; | ||||
| import { AddressTypeInfo } from '../../shared/address-utils'; | ||||
| 
 | ||||
| class AddressStats implements ChainStats { | ||||
|   address: string; | ||||
| @ -112,14 +113,13 @@ export class AddressComponent implements OnInit, OnDestroy { | ||||
|   blockTxSubscription: Subscription; | ||||
|   addressLoadingStatus$: Observable<number>; | ||||
|   addressInfo: null | AddressInformation = null; | ||||
|   addressTypeInfo: null | AddressTypeInfo; | ||||
| 
 | ||||
|   fullyLoaded = false; | ||||
|   chainStats: AddressStats; | ||||
|   mempoolStats: AddressStats; | ||||
| 
 | ||||
|   exampleChannel?: any; | ||||
|   exampleVin?: Vin; | ||||
|   exampleVout?: Vout; | ||||
| 
 | ||||
|   now = Date.now() / 1000; | ||||
|   balancePeriod: 'all' | '1m' = 'all'; | ||||
| @ -161,8 +161,6 @@ export class AddressComponent implements OnInit, OnDestroy { | ||||
|           this.transactions = null; | ||||
|           this.addressInfo = null; | ||||
|           this.exampleChannel = null; | ||||
|           this.exampleVin = null; | ||||
|           this.exampleVout = null; | ||||
|           document.body.scrollTo(0, 0); | ||||
|           this.addressString = params.get('id') || ''; | ||||
|           if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(this.addressString)) { | ||||
| @ -171,6 +169,8 @@ export class AddressComponent implements OnInit, OnDestroy { | ||||
|           this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`); | ||||
|           this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`); | ||||
| 
 | ||||
|           this.addressTypeInfo = new AddressTypeInfo(this.stateService.network || 'mainnet', this.addressString); | ||||
| 
 | ||||
|           return merge( | ||||
|             of(true), | ||||
|             this.stateService.connectionState$ | ||||
| @ -268,17 +268,13 @@ export class AddressComponent implements OnInit, OnDestroy { | ||||
|         } | ||||
|         this.isLoadingTransactions = false; | ||||
| 
 | ||||
|         let addressVin: Vin[] = []; | ||||
|         for (const tx of this.transactions) { | ||||
|           if (!this.exampleVin) { | ||||
|             this.exampleVin = tx.vin.find(v => v.prevout?.scriptpubkey_address === this.address.address); | ||||
|           } | ||||
|           if (!this.exampleVout) { | ||||
|             this.exampleVout = tx.vout.find(v => v.scriptpubkey_address === this.address.address); | ||||
|           } | ||||
|           if (this.exampleVin && this.exampleVout) { | ||||
|             break; | ||||
|           } | ||||
|           addressVin = addressVin.concat(tx.vin.filter(v => v.prevout?.scriptpubkey_address === this.address.address)); | ||||
|         } | ||||
|         this.addressTypeInfo.processInputs(addressVin); | ||||
|         // hack to trigger change detection
 | ||||
|         this.addressTypeInfo = this.addressTypeInfo.clone(); | ||||
| 
 | ||||
|         if (!this.showBalancePeriod()) { | ||||
|           this.setBalancePeriod('all'); | ||||
|  | ||||
							
								
								
									
										193
									
								
								frontend/src/app/shared/address-utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								frontend/src/app/shared/address-utils.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,193 @@ | ||||
| import '@angular/localize/init'; | ||||
| import { ScriptInfo } from './script.utils'; | ||||
| import { Vin } from '../interfaces/electrs.interface'; | ||||
| import { BECH32_CHARS_LW, BASE58_CHARS, HEX_CHARS } from './regex.utils'; | ||||
| 
 | ||||
| export type AddressType = 'fee' | ||||
|   | 'empty' | ||||
|   | 'provably_unspendable' | ||||
|   | 'op_return' | ||||
|   | 'multisig' | ||||
|   | 'p2pk' | ||||
|   | 'p2pkh' | ||||
|   | 'p2sh' | ||||
|   | 'p2sh-p2wpkh' | ||||
|   | 'p2sh-p2wsh' | ||||
|   | 'v0_p2wpkh' | ||||
|   | 'v0_p2wsh' | ||||
|   | 'v1_p2tr' | ||||
|   | 'confidential' | ||||
|   | 'unknown' | ||||
| 
 | ||||
| const ADDRESS_PREFIXES = { | ||||
|   mainnet: { | ||||
|     base58: { | ||||
|       pubkey: ['1'], | ||||
|       script: ['3'], | ||||
|     }, | ||||
|     bech32: 'bc1', | ||||
|   }, | ||||
|   testnet: { | ||||
|     base58: { | ||||
|       pubkey: ['m', 'n'], | ||||
|       script: '2', | ||||
|     }, | ||||
|     bech32: 'tb1', | ||||
|   }, | ||||
|   testnet4: { | ||||
|     base58: { | ||||
|       pubkey: ['m', 'n'], | ||||
|       script: '2', | ||||
|     }, | ||||
|     bech32: 'tb1', | ||||
|   }, | ||||
|   signet: { | ||||
|     base58: { | ||||
|       pubkey: ['m', 'n'], | ||||
|       script: '2', | ||||
|     }, | ||||
|     bech32: 'tb1', | ||||
|   }, | ||||
|   liquid: { | ||||
|     base58: { | ||||
|       pubkey: ['P','Q'], | ||||
|       script: ['G','H'], | ||||
|       confidential: ['V'], | ||||
|     }, | ||||
|     bech32: 'ex1', | ||||
|     confidential: 'lq1', | ||||
|   }, | ||||
|   liquidtestnet: { | ||||
|     base58: { | ||||
|       pubkey: ['F'], | ||||
|       script: ['8','9'], | ||||
|       confidential: ['V'], // TODO: check if this is actually correct
 | ||||
|     }, | ||||
|     bech32: 'tex1', | ||||
|     confidential: 'tlq1', | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| // precompiled regexes for common address types (excluding prefixes)
 | ||||
| const base58Regex = RegExp('^' + BASE58_CHARS + '{26,34}$'); | ||||
| const confidentialb58Regex = RegExp('^[TJ]' + BASE58_CHARS + '{78}$'); | ||||
| const p2wpkhRegex = RegExp('^q' + BECH32_CHARS_LW + '{38}$'); | ||||
| const p2wshRegex = RegExp('^q' + BECH32_CHARS_LW + '{58}$'); | ||||
| const p2trRegex = RegExp('^p' + BECH32_CHARS_LW + '{58}$'); | ||||
| const pubkeyRegex = RegExp('^' + `(04${HEX_CHARS}{128})|(0[23]${HEX_CHARS}{64})$`); | ||||
| 
 | ||||
| export function detectAddressType(address: string, network: string): AddressType { | ||||
|   // normal address types
 | ||||
|   const firstChar = address.substring(0, 1); | ||||
|   if (ADDRESS_PREFIXES[network].base58.pubkey.includes(firstChar) && base58Regex.test(address.slice(1))) { | ||||
|     return 'p2pkh'; | ||||
|   } else if (ADDRESS_PREFIXES[network].base58.script.includes(firstChar) && base58Regex.test(address.slice(1))) { | ||||
|     return 'p2sh'; | ||||
|   } else if (address.startsWith(ADDRESS_PREFIXES[network].bech32)) { | ||||
|     const suffix = address.slice(ADDRESS_PREFIXES[network].bech32.length); | ||||
|     if (p2wpkhRegex.test(suffix)) { | ||||
|       return 'v0_p2wpkh'; | ||||
|     } else if (p2wshRegex.test(suffix)) { | ||||
|       return 'v0_p2wsh'; | ||||
|     } else if (p2trRegex.test(suffix)) { | ||||
|       return 'v1_p2tr'; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // p2pk
 | ||||
|   if (pubkeyRegex.test(address)) { | ||||
|     return 'p2pk'; | ||||
|   } | ||||
| 
 | ||||
|   // liquid-specific types
 | ||||
|   if (network.startsWith('liquid')) { | ||||
|     if (ADDRESS_PREFIXES[network].base58.confidential.includes(firstChar) && confidentialb58Regex.test(address.slice(1))) { | ||||
|       return 'confidential'; | ||||
|     } else if (address.startsWith(ADDRESS_PREFIXES[network].confidential)) { | ||||
|       return 'confidential'; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return 'unknown'; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Parses & classifies address types + properties from address strings | ||||
|  * | ||||
|  * can optionally augment this data with examples of spends from the address, | ||||
|  * e.g. to classify revealed scripts for scripthash-type addresses. | ||||
|  */ | ||||
| export class AddressTypeInfo { | ||||
|   network: string; | ||||
|   address: string; | ||||
|   type: AddressType; | ||||
|   // script data
 | ||||
|   scripts: Map<string, ScriptInfo>; // raw script
 | ||||
|   // flags
 | ||||
|   isMultisig?: { m: number, n: number }; | ||||
|   tapscript?: boolean; | ||||
| 
 | ||||
|   constructor (network: string, address: string, type?: AddressType, vin?: Vin[]) { | ||||
|     this.network = network; | ||||
|     this.address = address; | ||||
|     this.scripts = new Map(); | ||||
|     if (type) { | ||||
|       this.type = type; | ||||
|     } else { | ||||
|       this.type = detectAddressType(address, network); | ||||
|     } | ||||
|     this.processInputs(vin); | ||||
|   } | ||||
| 
 | ||||
|   public clone(): AddressTypeInfo { | ||||
|     const cloned = new AddressTypeInfo(this.network, this.address, this.type); | ||||
|     cloned.scripts = new Map(Array.from(this.scripts, ([key, value]) => [key, value?.clone()])); | ||||
|     cloned.isMultisig = this.isMultisig; | ||||
|     cloned.tapscript = this.tapscript; | ||||
|     return cloned; | ||||
|   } | ||||
| 
 | ||||
|   public processInputs(vin: Vin[] = []): void { | ||||
|     // taproot can have multiple script paths
 | ||||
|     if (this.type === 'v1_p2tr') { | ||||
|       for (const v of vin) { | ||||
|         if (v.inner_witnessscript_asm) { | ||||
|           this.tapscript = true; | ||||
|           const controlBlock = v.witness[v.witness.length - 1].startsWith('50') ? v.witness[v.witness.length - 2] : v.witness[v.witness.length - 1]; | ||||
|           this.processScript(new ScriptInfo('inner_witnessscript', undefined, v.inner_witnessscript_asm, v.witness, controlBlock)); | ||||
|         } | ||||
|       } | ||||
|     // for single-script types, if we've seen one input we've seen them all
 | ||||
|     } else if (['p2sh', 'v0_p2wsh'].includes(this.type)) { | ||||
|       if (!this.scripts.size && vin.length) { | ||||
|         const v = vin[0]; | ||||
|         // wrapped segwit
 | ||||
|         if (this.type === 'p2sh' && v.witness?.length) { | ||||
|           if (v.scriptsig.startsWith('160014')) { | ||||
|             this.type = 'p2sh-p2wpkh'; | ||||
|           } else if (v.scriptsig.startsWith('220020')) { | ||||
|             this.type = 'p2sh-p2wsh'; | ||||
|           } | ||||
|         } | ||||
|         // real script
 | ||||
|         if (this.type !== 'p2sh-p2wpkh') { | ||||
|           if (v.inner_witnessscript_asm) { | ||||
|             this.processScript(new ScriptInfo('inner_witnessscript', undefined, v.inner_witnessscript_asm, v.witness)); | ||||
|           } else if (v.inner_redeemscript_asm) { | ||||
|             this.processScript(new ScriptInfo('inner_redeemscript', undefined, v.inner_redeemscript_asm, v.witness)); | ||||
|           } else if (v.scriptsig || v.scriptsig_asm) { | ||||
|             this.processScript(new ScriptInfo('scriptsig', v.scriptsig, v.scriptsig_asm, v.witness)); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     // and there's nothing more to learn from processing inputs for non-scripthash types
 | ||||
|   } | ||||
| 
 | ||||
|   private processScript(script: ScriptInfo): void { | ||||
|     this.scripts.set(script.key, script); | ||||
|     if (script.template?.type === 'multisig') { | ||||
|       this.isMultisig = { m: script.template['m'], n: script.template['n'] }; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,4 +1,4 @@ | ||||
| @switch (vout?.scriptpubkey_type || null) { | ||||
| @switch (address.type || null) { | ||||
|   @case ('fee') { | ||||
|     <span i18n="address.fee">fee</span> | ||||
|   } | ||||
| @ -24,6 +24,6 @@ | ||||
|     <span>unknown</span> | ||||
|   } | ||||
|   @default { | ||||
|     <span>{{ vout.scriptpubkey_type.toUpperCase() }}</span> | ||||
|     <span>{{ address.type.toUpperCase() }}</span> | ||||
|   } | ||||
| } | ||||
| @ -1,5 +1,5 @@ | ||||
| import { Component, Input } from '@angular/core'; | ||||
| import { Vout } from '../../../interfaces/electrs.interface'; | ||||
| import { AddressTypeInfo } from '../../address-utils'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-address-type', | ||||
| @ -7,5 +7,5 @@ import { Vout } from '../../../interfaces/electrs.interface'; | ||||
|   styleUrls: [] | ||||
| }) | ||||
| export class AddressTypeComponent { | ||||
|   @Input() vout: Vout; | ||||
|   @Input() address: AddressTypeInfo; | ||||
| } | ||||
|  | ||||
| @ -1,14 +1,14 @@ | ||||
| import { Env } from '../services/state.service'; | ||||
| 
 | ||||
| // all base58 characters
 | ||||
| const BASE58_CHARS = `[a-km-zA-HJ-NP-Z1-9]`; | ||||
| export const BASE58_CHARS = `[a-km-zA-HJ-NP-Z1-9]`; | ||||
| 
 | ||||
| // all bech32 characters (after the separator)
 | ||||
| const BECH32_CHARS_LW = `[ac-hj-np-z02-9]`; | ||||
| export const BECH32_CHARS_LW = `[ac-hj-np-z02-9]`; | ||||
| const BECH32_CHARS_UP = `[AC-HJ-NP-Z02-9]`; | ||||
| 
 | ||||
| // Hex characters
 | ||||
| const HEX_CHARS = `[a-fA-F0-9]`; | ||||
| export const HEX_CHARS = `[a-fA-F0-9]`; | ||||
| 
 | ||||
| // A regex to say "A single 0 OR any number with no leading zeroes"
 | ||||
| // Capped at 9 digits so as to not be confused with lightning channel IDs (which are around 17 digits)
 | ||||
|  | ||||
| @ -145,8 +145,116 @@ for (let i = 187; i <= 255; i++) { | ||||
| 
 | ||||
| export { opcodes }; | ||||
| 
 | ||||
| export type ScriptType = 'scriptpubkey' | ||||
|   | 'scriptsig' | ||||
|   | 'inner_witnessscript' | ||||
|   | 'inner_redeemscript' | ||||
| 
 | ||||
| export interface ScriptTemplate { | ||||
|   type: string; | ||||
|   label: string; | ||||
| } | ||||
| 
 | ||||
| export const ScriptTemplates: { [type: string]: (...args: any) => ScriptTemplate } = { | ||||
|   liquid_peg_out: () => ({ type: 'liquid_peg_out', label: 'Liquid Peg Out' }), | ||||
|   liquid_peg_out_emergency: () => ({ type: 'liquid_peg_out_emergency', label: 'Emergency Liquid Peg Out' }), | ||||
|   ln_force_close: () => ({ type: 'ln_force_close', label: 'Lightning Force Close' }), | ||||
|   ln_force_close_revoked: () => ({ type: 'ln_force_close_revoked', label: 'Revoked Lightning Force Close' }), | ||||
|   ln_htlc: () => ({ type: 'ln_htlc', label: 'Lightning HTLC' }), | ||||
|   ln_htlc_revoked: () => ({ type: 'ln_htlc_revoked', label: 'Revoked Lightning HTLC' }), | ||||
|   ln_htlc_expired: () => ({ type: 'ln_htlc_expired', label: 'Expired Lightning HTLC' }), | ||||
|   ln_anchor: () => ({ type: 'ln_anchor', label: 'Lightning Anchor' }), | ||||
|   ln_anchor_swept: () => ({ type: 'ln_anchor_swept', label: 'Swept Lightning Anchor' }), | ||||
|   multisig: (m: number, n: number) => ({ type: 'multisig', m, n, label: $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:` }), | ||||
| }; | ||||
| 
 | ||||
| export class ScriptInfo { | ||||
|   type: ScriptType; | ||||
|   scriptPath?: string; | ||||
|   hex?: string; | ||||
|   asm?: string; | ||||
|   template: ScriptTemplate; | ||||
| 
 | ||||
|   constructor(type: ScriptType, hex?: string, asm?: string, witness?: string[], scriptPath?: string) { | ||||
|     this.type = type; | ||||
|     this.hex = hex; | ||||
|     this.asm = asm; | ||||
|     if (scriptPath) { | ||||
|       this.scriptPath = scriptPath; | ||||
|     } | ||||
|     if (this.asm) { | ||||
|       this.template = detectScriptTemplate(this.type, this.asm, witness); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public clone(): ScriptInfo { | ||||
|     return { ...this }; | ||||
|   } | ||||
| 
 | ||||
|   get key(): string { | ||||
|     return this.type + (this.scriptPath || ''); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** parses an inner_witnessscript + witness stack, and detects named script types */ | ||||
| export function detectScriptTemplate(type: ScriptType, script_asm: string, witness?: string[]): ScriptTemplate | undefined { | ||||
|   if (type === 'inner_witnessscript' && witness?.length) { | ||||
|     if (script_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0 || script_asm.indexOf('OP_PUSHNUM_15 OP_CHECKMULTISIG OP_IFDUP OP_NOTIF OP_PUSHBYTES_2') === 1259) { | ||||
|       if (witness.length > 11) { | ||||
|         return ScriptTemplates.liquid_peg_out(); | ||||
|       } else { | ||||
|         return ScriptTemplates.liquid_peg_out_emergency(); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     const topElement = witness[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(script_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
 | ||||
|         return ScriptTemplates.ln_force_close_revoked(); | ||||
|       } else { | ||||
|         // top element is '', this is a delayed to_local output
 | ||||
|         return ScriptTemplates.ln_force_close(); | ||||
|       } | ||||
|     } 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(script_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(script_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
 | ||||
|         return ScriptTemplates.ln_htlc_revoked(); | ||||
|       } else if (topElement) { | ||||
|         // top element is a preimage
 | ||||
|         return ScriptTemplates.ln_htlc(); | ||||
|       } else { | ||||
|         // top element is '' to get in the expiry of the script
 | ||||
|         return ScriptTemplates.ln_htlc_expired(); | ||||
|       } | ||||
|     } else if (/^OP_PUSHBYTES_33 \w{66} OP_CHECKSIG OP_IFDUP OP_NOTIF OP_PUSHNUM_16 OP_CSV OP_ENDIF$/.test(script_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
 | ||||
|         return ScriptTemplates.ln_anchor(); | ||||
|       } else { | ||||
|         // top element is '', it has been swept after 16 blocks
 | ||||
|         return ScriptTemplates.ln_anchor_swept(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const multisig = parseMultisigScript(script_asm); | ||||
|   if (multisig) { | ||||
|     return ScriptTemplates.multisig(multisig.m, multisig.n); | ||||
|   } | ||||
| 
 | ||||
|   return; | ||||
| } | ||||
| 
 | ||||
| /** extracts m and n from a multisig script (asm), returns nothing if it is not a multisig script */ | ||||
| export function parseMultisigScript(script: string): void | { m: number, n: number } { | ||||
| export function parseMultisigScript(script: string): undefined | { m: number, n: number } { | ||||
|   if (!script) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user