UNPKG

@btc-vision/transaction

Version:

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

279 lines (278 loc) 10.5 kB
import { networks, Psbt, toXOnly } from '@btc-vision/bitcoin'; import { EcKeyPair } from '../../../keypair/EcKeyPair.js'; import { canSignNonTaprootInput, isTaprootInput, pubkeyInScript, } from '../../../signer/SignerUtils.js'; import { CustomKeypair } from '../BrowserSignerBase.js'; export class XverseSigner extends CustomKeypair { constructor() { super(); this.isInitialized = false; if (!window) { throw new Error('XverseSigner 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 BitcoinProvider() { const module = window.BitcoinProvider; if (!module) { throw new Error('Xverse Wallet extension not found'); } return module; } async init() { if (this.isInitialized) return; const connectResult = await this.BitcoinProvider.request('wallet_connect', null); if ('error' in connectResult) throw new Error(connectResult.error.message); const payementAddress = connectResult.result.addresses.find((address) => address.purpose === 'payment'); if (!payementAddress) { throw new Error('Payment address not found'); } const network = payementAddress.address.startsWith('tb') ? networks.testnet : payementAddress.address.startsWith('bc') ? networks.bitcoin : null; if (!network) throw new Error('Network not supported'); this._network = network; this._publicKey = Buffer.from(payementAddress.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; } async signData(data, address, protocol) { if (!this.isInitialized) { throw new Error('UnisatSigner not initialized'); } const callSign = await this.BitcoinProvider.request('signMessage', { address, message: data.toString(), protocol, }); if ('error' in callSign) throw new Error(callSign.error.message); const res = callSign.result; if (!res.signature) { throw new Error('Signature not found'); } return Buffer.from(res.signature, 'hex'); } 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.toBase64(); 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 toSignInputs = { [this.p2wpkh]: options[0].toSignInputs?.map((input) => input.index) || [], }; const callSign = await this.BitcoinProvider.request('signPsbt', { psbt: toSignPsbts[0], signInputs: toSignInputs, }); if ('error' in callSign) throw new Error(callSign.error.message); const signedPsbts = Psbt.fromBase64(callSign.result.psbt); 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.toBase64(); const toSignInputs = { [this.p2wpkh]: opts.toSignInputs?.map((input) => input.index) || [], }; const callSign = await this.BitcoinProvider.request('signPsbt', { psbt, signInputs: toSignInputs, }); if ('error' in callSign) throw new Error(callSign.error.message); return Psbt.fromBase64(callSign.result.psbt); } 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; } }