@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
JavaScript
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;
}
}