UNPKG

@btc-vision/transaction

Version:

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

553 lines (483 loc) 18.8 kB
import { backend } from '../ecc/backend.js'; import bip32, { type BIP32API, BIP32Factory, type BIP32Interface, type MLDSAKeyPair, MLDSASecurityLevel, QuantumBIP32Factory, } from '@btc-vision/bip32'; import bitcoin, { address, concat, fromHex, fromOutputScript, type Network, networks, opcodes, payments, type PrivateKey, type PublicKey, script, type Signer, toHex, toXOnly, type XOnlyPublicKey, } from '@btc-vision/bitcoin'; import { ECPairSigner, type UniversalSigner } from '@btc-vision/ecpair'; import type { IWallet } from './interfaces/IWallet.js'; import { secp256k1 } from '@noble/curves/secp256k1.js'; import { mod } from '@noble/curves/abstract/modular.js'; import { sha256 } from '@noble/hashes/sha2.js'; import { bytesToNumberBE, concatBytes, randomBytes } from '@noble/curves/utils.js'; const BIP32factory = typeof bip32 === 'function' ? bip32 : BIP32Factory; if (!BIP32factory) { throw new Error('Failed to load BIP32 library'); } const Point = secp256k1.Point; const CURVE_N = Point.Fn.ORDER; const TAP_TAG = new Uint8Array([84, 97, 112, 84, 119, 101, 97, 107]); // 'TapTweak' in UTF-8 const TAP_TAG_HASH = sha256(TAP_TAG); function tapTweakHash(x: Uint8Array): Uint8Array { return sha256(concatBytes(TAP_TAG_HASH, TAP_TAG_HASH, x)); } /** * Class for handling EC key pairs * @class EcKeyPair * @module EcKeyPair * @typicalname EcKeyPair * @example import { EcKeyPair } from '@btc-vision/transaction'; */ export class EcKeyPair { public static BIP32: BIP32API = BIP32factory(backend); public static ECPairSigner = ECPairSigner; // Initialize precomputation for better performance static { // Precompute tables for the base point for better performance Point.BASE.precompute(8); } /** * Generate a keypair from a WIF * @param {string} wif - The WIF to use * @param {Network} network - The network to use * @returns {UniversalSigner} - The generated keypair */ public static fromWIF(wif: string, network: Network = networks.bitcoin): UniversalSigner { return ECPairSigner.fromWIF(backend, wif, network); } /** * Generate a keypair from a private key * @param {Uint8Array} privateKey - The private key to use * @param {Network} network - The network to use * @returns {UniversalSigner} - The generated keypair */ public static fromPrivateKey( privateKey: Uint8Array | PrivateKey, network: Network = networks.bitcoin, ): UniversalSigner { return ECPairSigner.fromPrivateKey(backend, privateKey as PrivateKey, network); } /** * Generate a keypair from a public key * @param {Uint8Array} publicKey - The public key to use * @param {Network} network - The network to use * @returns {UniversalSigner} - The generated keypair */ public static fromPublicKey( publicKey: Uint8Array | PublicKey, network: Network = networks.bitcoin, ): UniversalSigner { return ECPairSigner.fromPublicKey(backend, publicKey as PublicKey, network); } /** * Generate a multi-sig address * @param {Uint8Array[]} pubKeys - The public keys to use * @param {number} minimumSignatureRequired - The minimum number of signatures required * @param {Network} network - The network to use * @returns {string} - The generated address * @throws {Error} - If the address cannot be generated */ public static generateMultiSigAddress( pubKeys: Uint8Array[] | PublicKey[], minimumSignatureRequired: number, network: Network = networks.bitcoin, ): string { const publicKeys: Uint8Array[] = this.verifyPubKeys(pubKeys, network); if (publicKeys.length !== pubKeys.length) throw new Error(`Contains invalid public keys`); const p2ms = payments.p2ms({ m: minimumSignatureRequired, pubkeys: publicKeys as PublicKey[], network: network, }); const p2wsh = payments.p2wsh({ redeem: p2ms, network: network }); const address = p2wsh.address; if (!address) { throw new Error('Failed to generate address'); } return address; } /** * Verify public keys and return the public keys * @param {Uint8Array[]} pubKeys - The public keys to verify * @param {Network} network - The network to use * @returns {Uint8Array[]} - The verified public keys * @throws {Error} - If the key cannot be regenerated */ public static verifyPubKeys( pubKeys: Uint8Array[], network: Network = networks.bitcoin, ): Uint8Array[] { return pubKeys.map((pubKey) => { const key = EcKeyPair.fromPublicKey(pubKey, network); if (!key) { throw new Error('Failed to regenerate key'); } return key.publicKey; }); } /** * Get a P2WPKH address from a keypair * @param {UniversalSigner} keyPair - The keypair to get the address for * @param {Network} network - The network to use * @returns {string} - The address */ public static getP2WPKHAddress( keyPair: UniversalSigner | Signer, network: Network = networks.bitcoin, ): string { const res = payments.p2wpkh({ pubkey: keyPair.publicKey, network: network }); if (!res.address) { throw new Error('Failed to generate wallet'); } return res.address; } /** * Get the address of a tweaked public key * @param {string} tweakedPubKeyHex - The tweaked public key hex string * @param {Network} network - The network to use * @returns {string} - The address * @throws {Error} - If the address cannot be generated */ public static tweakedPubKeyToAddress(tweakedPubKeyHex: string, network: Network): string { if (tweakedPubKeyHex.startsWith('0x')) { tweakedPubKeyHex = tweakedPubKeyHex.slice(2); } // Convert the tweaked public key hex string to a Uint8Array let tweakedPubKeyBuffer: XOnlyPublicKey = fromHex(tweakedPubKeyHex) as XOnlyPublicKey; if (tweakedPubKeyBuffer.length !== 32) { tweakedPubKeyBuffer = toXOnly(tweakedPubKeyBuffer); } return EcKeyPair.tweakedPubKeyBufferToAddress(tweakedPubKeyBuffer, network); } /** * Get the address of a tweaked public key * @param {Uint8Array} tweakedPubKeyBuffer - The tweaked public key buffer * @param {Network} network - The network to use * @returns {string} - The address * @throws {Error} - If the address cannot be generated */ public static tweakedPubKeyBufferToAddress( tweakedPubKeyBuffer: XOnlyPublicKey, network: Network, ): string { // Generate the Taproot address using the p2tr payment method const { address } = payments.p2tr({ pubkey: tweakedPubKeyBuffer, network: network, }); if (!address) { throw new Error('Failed to generate Taproot address'); } return address; } /** * Generate a P2OP address * @param bytes - The bytes to use for the P2OP address * @param network - The network to use * @param deploymentVersion - The deployment version (default is 0) * @returns {string} - The generated P2OP address */ public static p2op( bytes: Uint8Array, network: Network = networks.bitcoin, deploymentVersion: number = 0, ): string { // custom opnet contract addresses const witnessProgram = concat([ new Uint8Array([deploymentVersion]), bitcoin.crypto.hash160(bytes), ]); if (witnessProgram.length < 2 || witnessProgram.length > 40) { throw new Error('Witness program must be 2-40 bytes.'); } const scriptData = script.compile([opcodes.OP_16, witnessProgram]); return fromOutputScript(scriptData, network); } /** * Get the address of a xOnly tweaked public key * @param {string} tweakedPubKeyHex - The xOnly tweaked public key hex string * @param {Network} network - The network to use * @returns {string} - The address * @throws {Error} - If the address cannot be generated */ public static xOnlyTweakedPubKeyToAddress(tweakedPubKeyHex: string, network: Network): string { if (tweakedPubKeyHex.startsWith('0x')) { tweakedPubKeyHex = tweakedPubKeyHex.slice(2); } // Convert the tweaked public key hex string to a Uint8Array const tweakedPubKeyBuffer = fromHex(tweakedPubKeyHex) as XOnlyPublicKey; if (tweakedPubKeyBuffer.length !== 32) { throw new Error('Invalid xOnly public key length'); } // Generate the Taproot address using the p2tr payment method const { address } = payments.p2tr({ pubkey: tweakedPubKeyBuffer, network: network, }); if (!address) { throw new Error('Failed to generate Taproot address'); } return address; } /** * Tweak a public key * @param {Uint8Array | string} pub - The public key to tweak * @returns {Uint8Array} - The tweaked public key * @throws {Error} - If the public key cannot be tweaked */ public static tweakPublicKey(pub: Uint8Array | string): Uint8Array { if (typeof pub === 'string' && pub.startsWith('0x')) pub = pub.slice(2); const hexStr = typeof pub === 'string' ? pub : toHex(pub); const P = Point.fromHex(hexStr); const Peven = (P.y & 1n) === 0n ? P : P.negate(); const xBytes = Peven.toBytes(true).subarray(1); const tBytes = tapTweakHash(xBytes); const t = mod(bytesToNumberBE(tBytes), CURVE_N); const Q = Peven.add(Point.BASE.multiply(t)); return Q.toBytes(true); } /** * Tweak a batch of public keys * @param {readonly Uint8Array[]} pubkeys - The public keys to tweak * @param {bigint} tweakScalar - The scalar to use for tweaking * @returns {Uint8Array[]} - The tweaked public keys */ public static tweakBatchSharedT( pubkeys: readonly Uint8Array[], tweakScalar: bigint, ): Uint8Array[] { const T = Point.BASE.multiply(tweakScalar); return pubkeys.map((bytes) => { const P = Point.fromHex(toHex(bytes)); const P_even = P.y % 2n === 0n ? P : P.negate(); const Q = P_even.add(T); return Q.toBytes(true); }); } /** * Generate a random wallet with both classical and quantum keys * * @param network - The network to use * @param securityLevel - The ML-DSA security level for quantum keys (default: LEVEL2/44) * @returns An object containing both classical and quantum key information */ public static generateWallet( network: Network = networks.bitcoin, securityLevel: MLDSASecurityLevel = MLDSASecurityLevel.LEVEL2, ): IWallet { const keyPair = ECPairSigner.makeRandom(backend, network, { rng: (size: number): Uint8Array => { return randomBytes(size); }, }); const wallet = this.getP2WPKHAddress(keyPair, network); if (!wallet) { throw new Error('Failed to generate wallet'); } // Generate random quantum keypair with network const quantumKeyPair = this.generateQuantumKeyPair(securityLevel, network); return { address: wallet, privateKey: keyPair.toWIF(), publicKey: toHex(keyPair.publicKey), quantumPrivateKey: toHex(quantumKeyPair.privateKey), quantumPublicKey: toHex(quantumKeyPair.publicKey), }; } /** * Generate a random quantum ML-DSA keypair * * This creates a standalone quantum-resistant keypair without using BIP32 derivation. * The keys are generated using cryptographically secure random bytes. * * @param securityLevel - The ML-DSA security level (default: LEVEL2/44) * @param network - The Bitcoin network (default: bitcoin mainnet) * @returns A random ML-DSA keypair */ public static generateQuantumKeyPair( securityLevel: MLDSASecurityLevel = MLDSASecurityLevel.LEVEL2, network: Network = networks.bitcoin, ): MLDSAKeyPair { // Generate random seed for quantum key generation const randomSeed = randomBytes(64); // Create a quantum root from the random seed with network parameter const quantumRoot = QuantumBIP32Factory.fromSeed(randomSeed, network, securityLevel); if (!quantumRoot.privateKey || !quantumRoot.publicKey) { throw new Error('Failed to generate quantum keypair'); } return { privateKey: new Uint8Array(quantumRoot.privateKey), publicKey: new Uint8Array(quantumRoot.publicKey), }; } /** * Verify that a contract address is a valid p2tr address * @param {string} contractAddress - The contract address to verify * @param {Network} network - The network to use * @returns {boolean} - Whether the address is valid */ public static verifyContractAddress( contractAddress: string, network: Network = networks.bitcoin, ): boolean { return !!address.toOutputScript(contractAddress, network); } /** * Get the legacy segwit address from a keypair * @param {UniversalSigner} keyPair - The keypair to get the address for * @param {Network} network - The network to use * @returns {string} - The legacy address */ public static getLegacySegwitAddress( keyPair: UniversalSigner, network: Network = networks.bitcoin, ): string { const wallet = payments.p2sh({ redeem: payments.p2wpkh({ pubkey: keyPair.publicKey, network: network }), network: network, }); if (!wallet.address) { throw new Error('Failed to generate wallet'); } return wallet.address; } /** * Get the legacy address from a keypair * @param {UniversalSigner} keyPair - The keypair to get the address for * @param {Network} network - The network to use * @returns {string} - The legacy address */ public static getLegacyAddress( keyPair: UniversalSigner, network: Network = networks.bitcoin, ): string { const wallet = payments.p2pkh({ pubkey: keyPair.publicKey, network: network }); if (!wallet.address) { throw new Error('Failed to generate wallet'); } return wallet.address; } /** * Get the legacy address from a public key * @param publicKey * @param {Network} network - The network to use * @returns {string} - The legacy address */ public static getP2PKH(publicKey: PublicKey, network: Network = networks.bitcoin): string { const wallet = payments.p2pkh({ pubkey: publicKey, network: network }); if (!wallet.address) { throw new Error('Failed to generate wallet'); } return wallet.address; } /** * Get the P2PK output from a keypair * @param {UniversalSigner} keyPair - The keypair to get the address for * @param {Network} network - The network to use * @returns {string} - The legacy address */ public static getP2PKAddress( keyPair: UniversalSigner, network: Network = networks.bitcoin, ): string { const wallet = payments.p2pk({ pubkey: keyPair.publicKey, network: network }); if (!wallet.output) { throw new Error('Failed to generate wallet'); } return '0x' + toHex(wallet.output); } /** * Generate a random keypair * @param {Network} network - The network to use * @returns {UniversalSigner} - The generated keypair */ public static generateRandomKeyPair(network: Network = networks.bitcoin): UniversalSigner { return ECPairSigner.makeRandom(backend, network, { rng: (size: number): Uint8Array => { return randomBytes(size); }, }); } /** * Generate a BIP32 keypair from a seed * @param {Uint8Array} seed - The seed to generate the keypair from * @param {Network} network - The network to use * @returns {BIP32Interface} - The generated keypair */ public static fromSeed(seed: Uint8Array, network: Network = networks.bitcoin): BIP32Interface { return this.BIP32.fromSeed(seed, network); } /** * Get taproot address from keypair * @param {UniversalSigner | Signer} keyPair - The keypair to get the taproot address for * @param {Network} network - The network to use * @returns {string} - The taproot address */ public static getTaprootAddress( keyPair: UniversalSigner | Signer, network: Network = networks.bitcoin, ): string { const { address } = payments.p2tr({ internalPubkey: toXOnly(keyPair.publicKey), network: network, }); if (!address) { throw new Error(`Failed to generate sender address for transaction`); } return address; } /** * Get taproot address from address * @param {string} inAddr - The address to convert to taproot * @param {Network} network - The network to use * @returns {string} - The taproot address */ public static getTaprootAddressFromAddress( inAddr: string, network: Network = networks.bitcoin, ): string { const { address } = payments.p2tr({ address: inAddr, network: network, }); if (!address) { throw new Error(`Failed to generate sender address for transaction`); } return address; } /** * Get a keypair from a given seed. * @param {Uint8Array} seed - The seed to generate the key pair from * @param {Network} network - The network to use * @returns {UniversalSigner} - The generated key pair */ public static fromSeedKeyPair( seed: Uint8Array, network: Network = networks.bitcoin, ): UniversalSigner { const fromSeed = this.BIP32.fromSeed(seed, network); const privKey = fromSeed.privateKey; if (!privKey) throw new Error('Failed to generate key pair'); return ECPairSigner.fromPrivateKey(backend, privKey, network); } }