UNPKG

@btc-vision/transaction

Version:

OPNet transaction library allows you to create and sign transactions for the OPNet network.

330 lines (271 loc) 10.3 kB
import { address, fromHex, type Network, payments, type Script } from '@btc-vision/bitcoin'; import { EcKeyPair } from './EcKeyPair.js'; import { BitcoinUtils } from '../utils/BitcoinUtils.js'; import { P2WDADetector } from '../p2wda/P2WDADetector.js'; import { MLDSASecurityLevel } from '@btc-vision/bip32'; export enum AddressTypes { P2PKH = 'P2PKH', P2OP = 'P2OP', P2SH_OR_P2SH_P2WPKH = 'P2SH_OR_P2SH-P2WPKH', P2PK = 'P2PK', P2TR = 'P2TR', P2MR = 'P2MR', P2WPKH = 'P2WPKH', P2WSH = 'P2WSH', P2WDA = 'P2WDA', } export interface ValidatedP2WDAAddress { readonly isValid: boolean; readonly isPotentiallyP2WDA: boolean; readonly isDefinitelyP2WDA: boolean; readonly publicKey?: Uint8Array; readonly error?: string; } export class AddressVerificator { public static isValidP2TRAddress(inAddress: string, network: Network): boolean { if (!inAddress || inAddress.length < 50) return false; let isValidTapRootAddress: boolean = false; try { address.toOutputScript(inAddress, network); const decodedAddress = address.fromBech32(inAddress); isValidTapRootAddress = decodedAddress.version === 1; } catch {} return isValidTapRootAddress; } /** * Validates that the given address is a valid P2MR (BIP 360) address. * Checks for witness version 2 with a 32-byte Merkle root program. */ public static isValidP2MRAddress(inAddress: string, network: Network): boolean { if (!inAddress || inAddress.length < 50) return false; let isValidP2MR: boolean = false; try { address.toOutputScript(inAddress, network); const decodedAddress = address.fromBech32(inAddress); isValidP2MR = decodedAddress.version === 2 && decodedAddress.data.length === 32; } catch {} return isValidP2MR; } public static isP2WPKHAddress(inAddress: string, network: Network): boolean { if (!inAddress || inAddress.length < 20 || inAddress.length > 50) return false; let isValidSegWitAddress: boolean = false; try { const decodedAddress = address.fromBech32(inAddress); address.toOutputScript(inAddress, network); isValidSegWitAddress = decodedAddress.version === 0 && decodedAddress.data.length === 20; } catch {} return isValidSegWitAddress; } public static isP2WDAWitnessScript(witnessScript: Uint8Array): boolean { return P2WDADetector.isP2WDAWitnessScript(witnessScript); } public static isP2PKHOrP2SH(addy: string, network: Network): boolean { try { const decodedBase58 = address.fromBase58Check(addy); if (decodedBase58.version === network.pubKeyHash) { return true; } return decodedBase58.version === network.scriptHash; } catch (error) { return false; } } public static isValidPublicKey(input: string, network: Network): boolean { try { if (input.startsWith('0x')) { input = input.slice(2); } if (!BitcoinUtils.isValidHex(input)) { return false; } if (input.length === 64) { return true; } const pubKeyBuffer = fromHex(input); if ((input.length === 130 && pubKeyBuffer[0] === 0x06) || pubKeyBuffer[0] === 0x07) { return true; } if (input.length === 66 || input.length === 130) { EcKeyPair.fromPublicKey(pubKeyBuffer, network); return true; } } catch (e) { return false; } return false; } public static isValidMLDSAPublicKey(input: string | Uint8Array): MLDSASecurityLevel | null { try { let byteLength: number; if (typeof input !== 'string' && input instanceof Uint8Array) { byteLength = input.length; } else { if (input.startsWith('0x')) { input = input.slice(2); } if (!BitcoinUtils.isValidHex(input)) { return null; } byteLength = input.length / 2; } switch (byteLength) { case 1312: return MLDSASecurityLevel.LEVEL2; case 1952: return MLDSASecurityLevel.LEVEL3; case 2592: return MLDSASecurityLevel.LEVEL5; default: return null; } } catch (e) { return null; } } public static isValidP2OPAddress(inAddress: string, network: Network): boolean { if (!inAddress || inAddress.length < 20) return false; try { const decodedAddress = address.fromBech32(inAddress); const validPrefix = decodedAddress.prefix === network.bech32 || decodedAddress.prefix === network.bech32Opnet; if (!validPrefix) { return false; } return decodedAddress.version === 16 && decodedAddress.data.length === 21; } catch { return false; } } public static requireRedeemScript(addy: string, network: Network): boolean { try { const decodedBase58 = address.fromBase58Check(addy); if (decodedBase58.version === network.pubKeyHash) { return false; } return decodedBase58.version === network.scriptHash; } catch { return false; } } public static detectAddressType(addy: string, network: Network): AddressTypes | null { if (AddressVerificator.isValidPublicKey(addy, network)) { return AddressTypes.P2PK; } try { const decodedBase58 = address.fromBase58Check(addy); if (decodedBase58.version === network.pubKeyHash) { return AddressTypes.P2PKH; } if (decodedBase58.version === network.scriptHash) { return AddressTypes.P2SH_OR_P2SH_P2WPKH; } } catch {} try { const decodedBech32 = address.fromBech32(addy); if ( (decodedBech32.prefix === network.bech32Opnet || decodedBech32.prefix === network.bech32) && decodedBech32.version === 16 && decodedBech32.data.length === 21 ) { return AddressTypes.P2OP; } if (decodedBech32.prefix === network.bech32) { if (decodedBech32.version === 0 && decodedBech32.data.length === 20) { return AddressTypes.P2WPKH; } if (decodedBech32.version === 0 && decodedBech32.data.length === 32) { return AddressTypes.P2WSH; } if (decodedBech32.version === 1 && decodedBech32.data.length === 32) { return AddressTypes.P2TR; } if (decodedBech32.version === 2 && decodedBech32.data.length === 32) { return AddressTypes.P2MR; } } } catch {} return null; } public static detectAddressTypeWithWitnessScript( addy: string, network: Network, witnessScript?: Uint8Array, ): AddressTypes | null { const baseType = AddressVerificator.detectAddressType(addy, network); if (baseType === AddressTypes.P2WSH && witnessScript) { if (AddressVerificator.isP2WDAWitnessScript(witnessScript)) { return AddressTypes.P2WDA; } } return baseType; } public static validateP2WDAAddress( address: string, network: Network, witnessScript?: Uint8Array, ): ValidatedP2WDAAddress { try { const addressType = AddressVerificator.detectAddressType(address, network); if (addressType !== AddressTypes.P2WSH) { return { isValid: false, isPotentiallyP2WDA: false, isDefinitelyP2WDA: false, error: 'Not a P2WSH address', }; } if (!witnessScript) { return { isValid: true, isPotentiallyP2WDA: true, isDefinitelyP2WDA: false, }; } if (!AddressVerificator.isP2WDAWitnessScript(witnessScript)) { return { isValid: true, isPotentiallyP2WDA: true, isDefinitelyP2WDA: false, error: 'Witness script does not match P2WDA pattern', }; } const p2wsh = payments.p2wsh({ redeem: { output: witnessScript as Script }, network, }); if (p2wsh.address !== address) { return { isValid: false, isPotentiallyP2WDA: false, isDefinitelyP2WDA: false, error: 'Witness script does not match address', }; } const publicKey = P2WDADetector.extractPublicKeyFromP2WDA(witnessScript); if (!publicKey) { return { isValid: false, isPotentiallyP2WDA: false, isDefinitelyP2WDA: false, error: 'Failed to extract public key from witness script', }; } return { isValid: true, isPotentiallyP2WDA: true, isDefinitelyP2WDA: true, publicKey, }; } catch (error) { return { isValid: false, isPotentiallyP2WDA: false, isDefinitelyP2WDA: false, error: (error as Error).message, }; } } }