@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
300 lines (246 loc) • 9.71 kB
text/typescript
import { type UniversalSigner } from '@btc-vision/ecpair';
import { EcKeyPair } from './EcKeyPair.js';
import { fromHex, type Network, networks, type PublicKey, toHex, toXOnly, } from '@btc-vision/bitcoin';
import { Address } from './Address.js';
import { BitcoinUtils } from '../utils/BitcoinUtils.js';
import type { IP2WSHAddress } from '../transaction/mineable/IP2WSHAddress.js';
import {
getMLDSAConfig,
MLDSASecurityLevel,
QuantumBIP32Factory,
type QuantumBIP32Interface,
} from '@btc-vision/bip32';
/**
* Wallet class for managing both classical and quantum-resistant keys
*/
export class Wallet implements Disposable {
private readonly _keypair: UniversalSigner;
private readonly _mldsaKeypair: QuantumBIP32Interface;
private readonly _securityLevel: MLDSASecurityLevel;
private readonly _chainCode: Uint8Array;
private readonly _p2wpkh: string;
private readonly _p2tr: string;
private readonly _p2wda: IP2WSHAddress;
private readonly _legacy: string;
private readonly _segwitLegacy: string;
private readonly _bufferPubKey: Uint8Array;
private readonly _tweakedKey: Uint8Array;
private readonly _address: Address;
constructor(
privateKeyOrWif: string,
mldsaPrivateKeyOrBase58: string,
public readonly network: Network = networks.bitcoin,
securityLevel: MLDSASecurityLevel = MLDSASecurityLevel.LEVEL2,
chainCode?: Uint8Array,
) {
this._securityLevel = securityLevel;
const parsedPrivateKey = privateKeyOrWif.startsWith('0x')
? privateKeyOrWif.slice(2)
: privateKeyOrWif;
if (BitcoinUtils.isValidHex(parsedPrivateKey)) {
this._keypair = EcKeyPair.fromPrivateKey(fromHex(parsedPrivateKey), this.network);
} else {
this._keypair = EcKeyPair.fromWIF(parsedPrivateKey, this.network);
}
const parsedMLDSAKey = mldsaPrivateKeyOrBase58.startsWith('0x')
? mldsaPrivateKeyOrBase58.slice(2)
: mldsaPrivateKeyOrBase58;
if (BitcoinUtils.isValidHex(parsedMLDSAKey)) {
const mldsaBuffer = fromHex(parsedMLDSAKey);
const config = getMLDSAConfig(securityLevel, this.network);
const privateKeySize = config.privateKeySize;
const publicKeySize = config.publicKeySize;
const combinedSize = privateKeySize + publicKeySize;
let mldsaPrivateKeyBuffer: Uint8Array;
if (mldsaBuffer.length === privateKeySize) {
mldsaPrivateKeyBuffer = mldsaBuffer;
} else if (mldsaBuffer.length === combinedSize) {
mldsaPrivateKeyBuffer = mldsaBuffer.subarray(0, privateKeySize);
} else {
throw new Error(
`Invalid ML-DSA key length for security level ${securityLevel}. Expected ${privateKeySize} bytes (private only) or ${combinedSize} bytes (private+public), got ${mldsaBuffer.length} bytes.`,
);
}
if (chainCode && chainCode.length !== 32) {
throw new Error('Chain code must be 32 bytes');
}
this._chainCode = chainCode || new Uint8Array(32);
this._mldsaKeypair = QuantumBIP32Factory.fromPrivateKey(
mldsaPrivateKeyBuffer,
this._chainCode,
this.network,
securityLevel,
);
} else {
this._mldsaKeypair = QuantumBIP32Factory.fromBase58(parsedMLDSAKey);
this._chainCode = new Uint8Array(this._mldsaKeypair.chainCode);
this._securityLevel = this._mldsaKeypair.securityLevel;
}
this._bufferPubKey = this._keypair.publicKey;
this._address = new Address(this._mldsaKeypair.publicKey, this._keypair.publicKey);
this._p2tr = this._address.p2tr(this.network);
this._p2wpkh = this._address.p2wpkh(this.network);
this._legacy = this._address.p2pkh(this.network);
this._segwitLegacy = this._address.p2shp2wpkh(this.network);
this._p2wda = this._address.p2wda(this.network);
this._tweakedKey = this._address.tweakedPublicKeyToBuffer();
}
public get address(): Address {
return this._address;
}
public get tweakedPubKeyKey(): Uint8Array {
return this._tweakedKey;
}
public get keypair(): UniversalSigner {
if (!this._keypair) throw new Error('Keypair not set');
return this._keypair;
}
public get mldsaKeypair(): QuantumBIP32Interface {
return this._mldsaKeypair;
}
public get securityLevel(): MLDSASecurityLevel {
return this._securityLevel;
}
public get chainCode(): Uint8Array {
return this._chainCode;
}
public get p2wpkh(): string {
return this._p2wpkh;
}
public get p2tr(): string {
return this._p2tr;
}
public get p2wda(): IP2WSHAddress {
return this._p2wda;
}
public get legacy(): string {
return this._legacy;
}
public get addresses(): string[] {
return [this.p2wpkh, this.p2tr, this.legacy, this.segwitLegacy];
}
public get segwitLegacy(): string {
return this._segwitLegacy;
}
public get publicKey(): Uint8Array {
if (!this._bufferPubKey) throw new Error('Public key not set');
return this._bufferPubKey;
}
public get quantumPublicKey(): Uint8Array {
return new Uint8Array(this._mldsaKeypair.publicKey);
}
public get quantumPrivateKey(): Uint8Array {
if (!this._mldsaKeypair.privateKey) {
throw new Error('Quantum private key not set');
}
return new Uint8Array(this._mldsaKeypair.privateKey);
}
public get quantumPublicKeyHex(): string {
return toHex(new Uint8Array(this._mldsaKeypair.publicKey));
}
public get quantumPrivateKeyHex(): string {
if (!this._mldsaKeypair.privateKey) {
throw new Error('Quantum private key not set');
}
return toHex(new Uint8Array(this._mldsaKeypair.privateKey));
}
public get xOnly(): Uint8Array {
if (!this.keypair) throw new Error('Keypair not set');
return toXOnly(this._bufferPubKey as PublicKey);
}
public static fromWif(
wif: string,
quantumPrivateKeyHex: string,
network: Network = networks.bitcoin,
securityLevel: MLDSASecurityLevel = MLDSASecurityLevel.LEVEL2,
chainCode?: Uint8Array,
): Wallet {
return new Wallet(wif, quantumPrivateKeyHex, network, securityLevel, chainCode);
}
public static generate(
network: Network = networks.bitcoin,
securityLevel: MLDSASecurityLevel = MLDSASecurityLevel.LEVEL2,
): Wallet {
const walletData = EcKeyPair.generateWallet(network, securityLevel);
if (!walletData.quantumPrivateKey) {
throw new Error('Failed to generate quantum keys');
}
return new Wallet(
walletData.privateKey,
walletData.quantumPrivateKey,
network,
securityLevel,
);
}
public static fromPrivateKeys(
privateKeyHexOrWif: string,
mldsaPrivateKeyOrBase58: string,
network: Network = networks.bitcoin,
securityLevel: MLDSASecurityLevel = MLDSASecurityLevel.LEVEL2,
chainCode?: Uint8Array,
): Wallet {
return new Wallet(
privateKeyHexOrWif,
mldsaPrivateKeyOrBase58,
network,
securityLevel,
chainCode,
);
}
public toWIF(): string {
return this._keypair.toWIF();
}
public toPrivateKeyHex(): string {
if (!this._keypair.privateKey) {
throw new Error('Private key not available');
}
return toHex(this._keypair.privateKey);
}
public toPublicKeyHex(): string {
return toHex(this._bufferPubKey);
}
public toQuantumBase58(): string {
return this._mldsaKeypair.toBase58();
}
/**
* Best-effort zeroing of private key material held by this wallet.
*
* Zeros classical and quantum private key buffers and the chain code in-place.
* This cannot guarantee all copies are erased (the JS runtime may have copied
* buffers internally, and string representations cannot be zeroed), but it
* eliminates the primary references.
*/
public zeroize(): void {
this._keypair.privateKey?.fill(0);
this._mldsaKeypair.privateKey?.fill(0);
this._chainCode.fill(0);
}
public [Symbol.dispose](): void {
this.zeroize();
}
public derivePath(path: string): Wallet {
const derivedQuantum = this._mldsaKeypair.derivePath(path);
if (!this._keypair.privateKey) {
throw new Error('Cannot derive from a watch-only wallet (no private key available)');
}
const bip32Root = EcKeyPair.BIP32.fromPrivateKey(
this._keypair.privateKey,
this._chainCode,
this.network,
);
const derivedClassical = bip32Root.derivePath(path);
if (!derivedClassical.privateKey) {
throw new Error('Failed to derive classical private key');
}
if (!derivedClassical.chainCode) {
throw new Error('Failed to derive classical chain code');
}
return new Wallet(
toHex(new Uint8Array(derivedClassical.privateKey)),
derivedQuantum.toBase58(),
this.network,
this._securityLevel,
new Uint8Array(derivedClassical.chainCode),
);
}
}