Files
mempool/frontend/src/app/shared/address-utils.ts
2024-08-30 21:39:22 +00:00

220 lines
6.6 KiB
TypeScript

import '@angular/localize/init';
import { ScriptInfo } from './script.utils';
import { Vin, Vout } 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'
| 'anchor'
| '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[], vout?: Vout) {
this.network = network;
this.address = address;
this.scripts = new Map();
if (type) {
this.type = type;
} else {
this.type = detectAddressType(address, network);
}
this.processInputs(vin);
if (vout) {
this.processOutput(vout);
}
}
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));
}
}
}
} else if (this.type === 'multisig') {
if (vin.length) {
const v = vin[0];
this.processScript(new ScriptInfo('scriptpubkey', v.prevout.scriptpubkey, v.prevout.scriptpubkey_asm));
}
} else if (this.type === 'unknown') {
for (const v of vin) {
if (v.prevout?.scriptpubkey === '51024e73') {
this.type = 'anchor';
}
}
}
// and there's nothing more to learn from processing inputs for other types
}
public processOutput(output: Vout): void {
if (this.type === 'multisig') {
if (!this.scripts.size) {
this.processScript(new ScriptInfo('scriptpubkey', output.scriptpubkey, output.scriptpubkey_asm));
}
} else if (this.type === 'unknown') {
if (output.scriptpubkey === '51024e73') {
this.type = 'anchor';
}
}
}
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'] };
}
}
}