UNPKG

@btc-vision/transaction

Version:

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

274 lines (273 loc) 10.2 kB
import { crypto as bitCrypto, networks, Psbt, script as bitScript, toXOnly, } from '@btc-vision/bitcoin'; import { EcKeyPair } from '../../../keypair/EcKeyPair.js'; import { canSignNonTaprootInput, isTaprootInput } from '../../../signer/SignerUtils.js'; import { CustomKeypair } from '../BrowserSignerBase.js'; import { UnisatNetwork } from '../types/Unisat.js'; export class UnisatSigner extends CustomKeypair { constructor() { super(); this.isInitialized = false; if (!window) { throw new Error('UnisatSigner can only be used in a browser environment'); } } get p2tr() { if (!this._p2tr) { throw new Error('P2TR address not set'); } return this._p2tr; } get p2wpkh() { if (!this._p2wpkh) { throw new Error('P2PKH address not set'); } return this._p2wpkh; } get addresses() { if (!this._addresses) { throw new Error('Addresses not set'); } return this._addresses; } get publicKey() { if (!this._publicKey) { throw new Error('Public key not set'); } return this._publicKey; } get network() { if (!this._network) { throw new Error('Network not set'); } return this._network; } get unisat() { if (!window) throw new Error('Window not found'); const module = window.unisat; if (!module) { throw new Error('Unisat extension not found'); } return module; } async signData(data, type) { const str = data.toString('hex'); const signature = await this.unisat.signData(str, type); return Buffer.from(signature, 'hex'); } async init() { if (this.isInitialized) { return; } const network = await this.unisat.getNetwork(); switch (network) { case UnisatNetwork.mainnet: this._network = networks.bitcoin; break; case UnisatNetwork.testnet: this._network = networks.testnet; break; case UnisatNetwork.regtest: this._network = networks.regtest; break; default: throw new Error(`Invalid network: ${network}`); } const publicKey = await this.unisat.getPublicKey(); if (publicKey === '') { throw new Error('Unlock your wallet first'); } this._publicKey = Buffer.from(publicKey, 'hex'); this._p2wpkh = EcKeyPair.getP2WPKHAddress(this, this.network); this._p2tr = EcKeyPair.getTaprootAddress(this, this.network); this._addresses = [this._p2wpkh, this._p2tr]; this.isInitialized = true; } getPublicKey() { if (!this.isInitialized) { throw new Error('UnisatSigner not initialized'); } return this.publicKey; } sign(_hash, _lowR) { throw new Error('Not implemented: sign'); } signSchnorr(_hash) { throw new Error('Not implemented: signSchnorr'); } verify(_hash, _signature) { throw new Error('Not implemented: verify'); } async signTaprootInput(transaction, i, sighashTypes) { const input = transaction.data.inputs[i]; if (input.tapKeySig || input.finalScriptSig || (Array.isArray(input.partialSig) && input.partialSig.length && this.hasAlreadyPartialSig(input.partialSig)) || (Array.isArray(input.tapScriptSig) && input.tapScriptSig.length && this.hasAlreadySignedTapScriptSig(input.tapScriptSig))) { return; } const firstSignature = await this.signAllTweaked(transaction, sighashTypes, false); this.combine(transaction, firstSignature, i); } async signInput(transaction, i, sighashTypes) { const input = transaction.data.inputs[i]; if (input.tapKeySig || input.finalScriptSig || (Array.isArray(input.partialSig) && input.partialSig.length && this.hasAlreadyPartialSig(input.partialSig)) || (Array.isArray(input.tapScriptSig) && input.tapScriptSig.length && this.hasAlreadySignedTapScriptSig(input.tapScriptSig))) { return; } const firstSignature = await this.signAllTweaked(transaction, sighashTypes, true); this.combine(transaction, firstSignature, i); } async multiSignPsbt(transactions) { const toSignPsbts = []; const options = []; for (const psbt of transactions) { const hex = psbt.toHex(); toSignPsbts.push(hex); const toSignInputs = psbt.data.inputs .map((input, i) => { let needsToSign = false; let viaTaproot = false; if (isTaprootInput(input)) { if (input.tapLeafScript && input.tapLeafScript.length > 0) { for (const tapLeafScript of input.tapLeafScript) { if (pubkeyInScript(this.publicKey, tapLeafScript.script)) { needsToSign = true; viaTaproot = false; break; } } } if (!needsToSign && input.tapInternalKey) { const tapInternalKey = input.tapInternalKey; const xOnlyPubKey = toXOnly(this.publicKey); if (tapInternalKey.equals(xOnlyPubKey)) { needsToSign = true; viaTaproot = true; } } } else if (canSignNonTaprootInput(input, this.publicKey)) { needsToSign = true; viaTaproot = false; } if (needsToSign) { return { index: i, publicKey: this.publicKey.toString('hex'), disableTweakSigner: !viaTaproot, }; } else { return null; } }) .filter((v) => v !== null); options.push({ autoFinalized: false, toSignInputs: toSignInputs, }); } const signed = await this.unisat.signPsbt(toSignPsbts[0], options[0]); const signedPsbts = Psbt.fromHex(signed); transactions[0].combine(signedPsbts); } hasAlreadySignedTapScriptSig(input) { for (let i = 0; i < input.length; i++) { const item = input[i]; const buf = Buffer.from(item.pubkey); if (buf.equals(this.publicKey) && item.signature) { return true; } } return false; } hasAlreadyPartialSig(input) { for (let i = 0; i < input.length; i++) { const item = input[i]; const buf = Buffer.from(item.pubkey); if (buf.equals(this.publicKey) && item.signature) { return true; } } return false; } combine(transaction, newPsbt, i) { const signedInput = newPsbt.data.inputs[i]; const originalInput = transaction.data.inputs[i]; if (signedInput.partialSig) { transaction.updateInput(i, { partialSig: signedInput.partialSig }); } if (signedInput.tapKeySig && !originalInput.tapKeySig) { transaction.updateInput(i, { tapKeySig: signedInput.tapKeySig }); } if (signedInput.tapScriptSig?.length) { const lastScriptSig = originalInput.tapScriptSig; if (lastScriptSig) { const getNonDuplicate = this.getNonDuplicateScriptSig(lastScriptSig, signedInput.tapScriptSig); if (getNonDuplicate.length) { transaction.updateInput(i, { tapScriptSig: getNonDuplicate }); } } else { transaction.updateInput(i, { tapScriptSig: signedInput.tapScriptSig }); } } } async signAllTweaked(transaction, sighashTypes, disableTweakSigner = false) { const pubKey = this.publicKey.toString('hex'); const toSign = transaction.data.inputs.map((_, i) => { return [ { index: i, publicKey: pubKey, sighashTypes, disableTweakSigner: disableTweakSigner, }, ]; }); const opts = { autoFinalized: false, toSignInputs: toSign.flat(), }; const psbt = transaction.toHex(); const signed = await this.unisat.signPsbt(psbt, opts); return Psbt.fromHex(signed); } getNonDuplicateScriptSig(scriptSig1, scriptSig2) { const nonDuplicate = []; for (let i = 0; i < scriptSig2.length; i++) { const found = scriptSig1.find((item) => item.pubkey.equals(scriptSig2[i].pubkey)); if (!found) { nonDuplicate.push(scriptSig2[i]); } } return nonDuplicate; } } function pubkeyInScript(pubkey, script) { return pubkeyPositionInScript(pubkey, script) !== -1; } function pubkeyPositionInScript(pubkey, script) { const pubkeyHash = bitCrypto.hash160(pubkey); const pubkeyXOnly = toXOnly(pubkey); const decompiled = bitScript.decompile(script); if (decompiled === null) throw new Error('Unknown script error'); return decompiled.findIndex((element) => { if (typeof element === 'number') return false; return (Buffer.isBuffer(element) && (element.equals(pubkey) || element.equals(pubkeyHash) || element.equals(pubkeyXOnly))); }); }