BlueWallet/class/wallets/segwit-p2sh-wallet.ts

171 lines
4.9 KiB
TypeScript
Raw Normal View History

2024-02-23 07:32:47 +00:00
import * as bitcoin from 'bitcoinjs-lib';
import { CoinSelectTarget } from 'coinselect';
2021-12-20 17:11:07 +00:00
import { ECPairFactory } from 'ecpair';
import ecc from '../../blue_modules/noble_ecc';
2024-02-23 07:32:47 +00:00
import { LegacyWallet } from './legacy-wallet';
import { CreateTransactionResult, CreateTransactionUtxo } from './types';
2021-12-20 17:11:07 +00:00
const ECPair = ECPairFactory(ecc);
2018-03-20 22:41:07 +02:00
2019-09-14 07:15:59 +09:00
/**
* Creates Segwit P2SH Bitcoin address
* @param pubkey
* @param network
* @returns {String}
*/
2024-02-23 07:32:47 +00:00
function pubkeyToP2shSegwitAddress(pubkey: Buffer): string | false {
2019-09-14 07:15:59 +09:00
const { address } = bitcoin.payments.p2sh({
2024-02-23 07:32:47 +00:00
redeem: bitcoin.payments.p2wpkh({ pubkey }),
2019-09-14 07:15:59 +09:00
});
2024-02-23 07:32:47 +00:00
return address ?? false;
2019-09-14 07:15:59 +09:00
}
2018-03-20 22:41:07 +02:00
export class SegwitP2SHWallet extends LegacyWallet {
static type = 'segwitP2SH';
static typeReadable = 'SegWit (P2SH)';
2021-03-11 13:45:49 +03:00
static segwitType = 'p2sh(p2wpkh)';
2018-03-20 22:41:07 +02:00
2024-02-23 07:32:47 +00:00
static witnessToAddress(witness: string): string | false {
try {
const pubKey = Buffer.from(witness, 'hex');
return pubkeyToP2shSegwitAddress(pubKey);
} catch (_) {
return false;
}
2018-06-24 22:27:34 +01:00
}
/**
* Converts script pub key to p2sh address if it can. Returns FALSE if it cant.
*
* @param scriptPubKey
* @returns {boolean|string} Either p2sh address or false
*/
2024-02-23 07:32:47 +00:00
static scriptPubKeyToAddress(scriptPubKey: string): string | false {
try {
const scriptPubKey2 = Buffer.from(scriptPubKey, 'hex');
2024-02-23 07:32:47 +00:00
return (
bitcoin.payments.p2sh({
output: scriptPubKey2,
network: bitcoin.networks.bitcoin,
}).address ?? false
);
} catch (_) {
return false;
}
}
2024-02-23 07:32:47 +00:00
getAddress(): string | false {
2018-03-20 22:41:07 +02:00
if (this._address) return this._address;
let address;
try {
2021-11-25 15:50:40 +00:00
const keyPair = ECPair.fromWIF(this.secret);
const pubKey = keyPair.publicKey;
if (!keyPair.compressed) {
console.warn('only compressed public keys are good for segwit');
return false;
}
2019-09-14 07:15:59 +09:00
address = pubkeyToP2shSegwitAddress(pubKey);
2018-03-20 22:41:07 +02:00
} catch (err) {
return false;
}
this._address = address;
return this._address;
}
/**
*
* @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String, txhex: String, }>} List of spendable utxos
* @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate)
* @param feeRate {Number} satoshi per byte
* @param changeAddress {String} Excessive coins will go back to that address
* @param sequence {Number} Used in RBF
* @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case
* @param masterFingerprint {number} Decimal number of wallet's master fingerprint
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}}
*/
2024-02-23 07:32:47 +00:00
createTransaction(
utxos: CreateTransactionUtxo[],
targets: CoinSelectTarget[],
feeRate: number,
changeAddress: string,
sequence: number,
skipSigning = false,
masterFingerprint: number,
): CreateTransactionResult {
if (targets.length === 0) throw new Error('No destination provided');
// compensating for coinselect inability to deal with segwit inputs, and overriding script length for proper vbytes calculation
for (const u of utxos) {
u.script = { length: 50 };
}
2020-09-14 13:49:08 +03:00
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate, changeAddress);
sequence = sequence || 0xffffffff; // disable RBF by default
const psbt = new bitcoin.Psbt();
let c = 0;
2024-02-23 07:32:47 +00:00
const values: Record<number, number> = {};
const keyPair = ECPair.fromWIF(this.secret);
inputs.forEach(input => {
values[c] = input.value;
c++;
const pubkey = keyPair.publicKey;
const p2wpkh = bitcoin.payments.p2wpkh({ pubkey });
const p2sh = bitcoin.payments.p2sh({ redeem: p2wpkh });
2024-02-23 07:32:47 +00:00
if (!p2sh.output) {
throw new Error('Internal error: no p2sh.output during createTransaction()');
}
psbt.addInput({
hash: input.txid,
index: input.vout,
sequence,
witnessUtxo: {
script: p2sh.output,
value: input.value,
},
redeemScript: p2wpkh.output,
});
});
outputs.forEach(output => {
// if output has no address - this is change output
if (!output.address) {
output.address = changeAddress;
}
const outputData = {
address: output.address,
value: output.value,
};
psbt.addOutput(outputData);
});
if (!skipSigning) {
// skiping signing related stuff
for (let cc = 0; cc < c; cc++) {
psbt.signInput(cc, keyPair);
}
2018-03-20 22:41:07 +02:00
}
let tx;
if (!skipSigning) {
tx = psbt.finalizeAllInputs().extractTransaction();
2018-03-20 22:41:07 +02:00
}
return { tx, inputs, outputs, fee, psbt };
}
2021-09-09 12:00:11 +01:00
allowSendMax() {
return true;
}
isSegwit() {
return true;
}
2021-03-09 14:26:56 +03:00
allowSignVerifyMessage() {
return true;
}
2018-03-20 22:41:07 +02:00
}