UNPKG

@btc-vision/transaction

Version:

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

397 lines (326 loc) 12.5 kB
import { crypto as bitCrypto, equals, fromHex, type Network, networks, Psbt, type PsbtInput, script as bitScript, type TapScriptSig, toHex, toXOnly, } from '@btc-vision/bitcoin'; import type { PartialSig } from 'bip174'; import { createPublicKey, type MessageHash, type PublicKey, type SchnorrSignature, type Signature, } from '@btc-vision/ecpair'; import { EcKeyPair } from '../../../keypair/EcKeyPair.js'; import { canSignNonTaprootInput, isTaprootInput } from '../../../signer/SignerUtils.js'; import { CustomKeypair } from '../BrowserSignerBase.js'; import { type PsbtSignatureOptions, SignatureType, type Unisat } from '../types/Unisat.js'; import { WalletNetworks } from '../WalletNetworks.js'; import type { OPWallet } from '../types/OPWallet.js'; export interface WindowWithWallets { unisat?: Unisat; opnet?: OPWallet; } export class UnisatSigner extends CustomKeypair { private isInitialized: boolean = false; constructor() { super(); if (!window) { throw new Error('UnisatSigner can only be used in a browser environment'); } } private _p2tr: string | undefined; public get p2tr(): string { if (!this._p2tr) { throw new Error('P2TR address not set'); } return this._p2tr; } private _p2wpkh: string | undefined; public get p2wpkh(): string { if (!this._p2wpkh) { throw new Error('P2PKH address not set'); } return this._p2wpkh; } private _addresses: string[] | undefined; public get addresses(): string[] { if (!this._addresses) { throw new Error('Addresses not set'); } return this._addresses; } private _publicKey: PublicKey | undefined; public get publicKey(): PublicKey { if (!this._publicKey) { throw new Error('Public key not set'); } return this._publicKey; } public _network: Network | undefined; public get network(): Network { if (!this._network) { throw new Error('Network not set'); } return this._network; } public get unisat(): Unisat { if (!window) throw new Error('Window not found'); const module = (window as WindowWithWallets).unisat; if (!module) { throw new Error('Unisat extension not found'); } return module; } public async signData(data: Uint8Array, type: SignatureType): Promise<Uint8Array> { const str = toHex(data); const signature = await this.unisat.signData(str, type); return fromHex(signature); } public async init(): Promise<void> { if (this.isInitialized) { return; } const network = await this.unisat.getNetwork(); switch (network) { case WalletNetworks.Mainnet: this._network = networks.bitcoin; break; case WalletNetworks.Testnet: this._network = networks.testnet; break; case WalletNetworks.OpnetTestnet: this._network = networks.opnetTestnet; break; case WalletNetworks.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 = createPublicKey(fromHex(publicKey)); this._p2wpkh = EcKeyPair.getP2WPKHAddress(this, this.network); this._p2tr = EcKeyPair.getTaprootAddress(this, this.network); this._addresses = [this._p2wpkh, this._p2tr]; this.isInitialized = true; } public getPublicKey(): PublicKey { if (!this.isInitialized) { throw new Error('UnisatSigner not initialized'); } return this.publicKey; } public sign(_hash: MessageHash, _lowR?: boolean): Signature { throw new Error('Not implemented: sign'); } public signSchnorr(_hash: MessageHash): SchnorrSignature { throw new Error('Not implemented: signSchnorr'); } public verify(_hash: MessageHash, _signature: Signature): boolean { throw new Error('Not implemented: verify'); } public async signTaprootInput( transaction: Psbt, i: number, sighashTypes: number[], ): Promise<void> { const input = transaction.data.inputs[i] as PsbtInput; 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); } public async signInput(transaction: Psbt, i: number, sighashTypes: number[]): Promise<void> { const input = transaction.data.inputs[i] as PsbtInput; 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); } public async multiSignPsbt(transactions: Psbt[]): Promise<void> { const toSignPsbts: string[] = []; const options: PsbtSignatureOptions[] = []; 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; // for opnet, we use original keys. break; } } } if (!needsToSign && input.tapInternalKey) { const tapInternalKey = input.tapInternalKey; const xOnlyPubKey = toXOnly(this.publicKey); if (equals(tapInternalKey, xOnlyPubKey)) { needsToSign = true; viaTaproot = true; } } } else if (canSignNonTaprootInput(input, this.publicKey)) { // Non-Taproot input needsToSign = true; viaTaproot = false; } if (needsToSign) { return { index: i, publicKey: toHex(this.publicKey), disableTweakSigner: !viaTaproot, }; } else { return null; } }) .filter((v) => v !== null); options.push({ autoFinalized: false, toSignInputs: toSignInputs, }); } const signed = await this.unisat.signPsbt( toSignPsbts[0] as string, options[0] as PsbtSignatureOptions, ); const signedPsbts = Psbt.fromHex(signed); (transactions[0] as Psbt).combine(signedPsbts); } private hasAlreadySignedTapScriptSig(input: TapScriptSig[]): boolean { for (let i = 0; i < input.length; i++) { const item = input[i] as TapScriptSig; const buf = new Uint8Array(item.pubkey); if (equals(buf, this.publicKey) && item.signature) { return true; } } return false; } private hasAlreadyPartialSig(input: PartialSig[]): boolean { for (let i = 0; i < input.length; i++) { const item = input[i] as PartialSig; const buf = new Uint8Array(item.pubkey); if (equals(buf, this.publicKey) && item.signature) { return true; } } return false; } private combine(transaction: Psbt, newPsbt: Psbt, i: number): void { const signedInput = newPsbt.data.inputs[i] as PsbtInput; const originalInput = transaction.data.inputs[i] as PsbtInput; 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 }); } } } private async signAllTweaked( transaction: Psbt, sighashTypes: number[], disableTweakSigner: boolean = false, ): Promise<Psbt> { const pubKey = toHex(this.publicKey); const toSign = transaction.data.inputs.map((_, i) => { return [ { index: i, publicKey: pubKey, sighashTypes, disableTweakSigner: disableTweakSigner, }, ]; }); const opts: PsbtSignatureOptions = { autoFinalized: false, toSignInputs: toSign.flat(), }; const psbt = transaction.toHex(); const signed = await this.unisat.signPsbt(psbt, opts); return Psbt.fromHex(signed); } private getNonDuplicateScriptSig( scriptSig1: TapScriptSig[], scriptSig2: TapScriptSig[], ): TapScriptSig[] { const nonDuplicate: TapScriptSig[] = []; for (let i = 0; i < scriptSig2.length; i++) { const sig2 = scriptSig2[i] as TapScriptSig; const found = scriptSig1.find((item) => equals(item.pubkey, sig2.pubkey)); if (!found) { nonDuplicate.push(sig2); } } return nonDuplicate; } } function pubkeyInScript(pubkey: Uint8Array, script: Uint8Array): boolean { return pubkeyPositionInScript(pubkey, script) !== -1; } function pubkeyPositionInScript(pubkey: Uint8Array, script: Uint8Array): number { const pubkeyHash = bitCrypto.hash160(pubkey); const pubkeyXOnly = toXOnly(pubkey as PublicKey); 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 ( element instanceof Uint8Array && (equals(element, pubkey) || equals(element, pubkeyHash) || equals(element, pubkeyXOnly)) ); }); }