Composition over inheritance

This commit is contained in:
junderw 2019-07-11 17:17:49 +09:00
parent 2f1609b918
commit 1feef9569c
No known key found for this signature in database
GPG key ID: B256185D3A971908
6 changed files with 402 additions and 130 deletions
ts_src

View file

@ -1,11 +1,21 @@
import { Psbt as PsbtBase } from 'bip174';
import * as varuint from 'bip174/src/lib/converter/varint';
import {
Bip32Derivation,
FinalScriptSig,
FinalScriptWitness,
GlobalXpub,
KeyValue,
NonWitnessUtxo,
PartialSig,
PorCommitment,
PsbtInput,
RedeemScript,
SighashType,
TransactionInput,
TransactionOutput,
WitnessScript,
WitnessUtxo,
} from 'bip174/src/lib/interfaces';
import { checkForInput } from 'bip174/src/lib/utils';
import { toOutputScript } from './address';
@ -26,37 +36,42 @@ const DEFAULT_OPTS: PsbtOpts = {
maximumFeeRate: 5000, // satoshi per byte
};
export class Psbt extends PsbtBase {
static fromTransaction<T extends typeof PsbtBase>(
this: T,
txBuf: Buffer,
): InstanceType<T> {
export class Psbt {
static fromTransaction(txBuf: Buffer, opts: PsbtOptsOptional = {}): Psbt {
const tx = Transaction.fromBuffer(txBuf);
checkTxEmpty(tx);
const psbt = new this() as Psbt;
const psbtBase = new PsbtBase();
const psbt = new Psbt(opts, psbtBase);
psbt.__CACHE.__TX = tx;
checkTxForDupeIns(tx, psbt.__CACHE);
let inputCount = tx.ins.length;
let outputCount = tx.outs.length;
while (inputCount > 0) {
psbt.inputs.push({
keyVals: [],
psbtBase.inputs.push({
unknownKeyVals: [],
});
inputCount--;
}
while (outputCount > 0) {
psbt.outputs.push({
keyVals: [],
psbtBase.outputs.push({
unknownKeyVals: [],
});
outputCount--;
}
return psbt as InstanceType<T>;
return psbt;
}
static fromBuffer<T extends typeof PsbtBase>(
this: T,
buffer: Buffer,
): InstanceType<T> {
static fromBase64(data: string, opts: PsbtOptsOptional = {}): Psbt {
const buffer = Buffer.from(data, 'base64');
return this.fromBuffer(buffer, opts);
}
static fromHex(data: string, opts: PsbtOptsOptional = {}): Psbt {
const buffer = Buffer.from(data, 'hex');
return this.fromBuffer(buffer, opts);
}
static fromBuffer(buffer: Buffer, opts: PsbtOptsOptional = {}): Psbt {
let tx: Transaction | undefined;
const txCountGetter = (
txBuf: Buffer,
@ -71,10 +86,11 @@ export class Psbt extends PsbtBase {
outputCount: tx.outs.length,
};
};
const psbt = super.fromBuffer(buffer, txCountGetter) as Psbt;
const psbtBase = PsbtBase.fromBuffer(buffer, txCountGetter);
const psbt = new Psbt(opts, psbtBase);
psbt.__CACHE.__TX = tx!;
checkTxForDupeIns(tx!, psbt.__CACHE);
return psbt as InstanceType<T>;
return psbt;
}
private __CACHE: PsbtCache = {
@ -85,17 +101,19 @@ export class Psbt extends PsbtBase {
};
private opts: PsbtOpts;
constructor(opts: PsbtOptsOptional = {}) {
super();
constructor(
opts: PsbtOptsOptional = {},
readonly data: PsbtBase = new PsbtBase(),
) {
// set defaults
this.opts = Object.assign({}, DEFAULT_OPTS, opts);
const c = this.__CACHE;
c.__TX = Transaction.fromBuffer(this.globalMap.unsignedTx!);
this.setVersion(2);
c.__TX = Transaction.fromBuffer(data.globalMap.unsignedTx!);
if (this.data.inputs.length === 0) this.setVersion(2);
// set cache
delete this.globalMap.unsignedTx;
Object.defineProperty(this.globalMap, 'unsignedTx', {
delete data.globalMap.unsignedTx;
Object.defineProperty(data.globalMap, 'unsignedTx', {
enumerable: true,
get(): Buffer {
const buf = c.__TX_BUF_CACHE;
@ -106,8 +124,8 @@ export class Psbt extends PsbtBase {
return c.__TX_BUF_CACHE;
}
},
set(data: Buffer): void {
c.__TX_BUF_CACHE = data;
set(_data: Buffer): void {
c.__TX_BUF_CACHE = _data;
},
});
@ -127,12 +145,17 @@ export class Psbt extends PsbtBase {
}
get inputCount(): number {
return this.inputs.length;
return this.data.inputs.length;
}
combine(...those: Psbt[]): this {
this.data.combine(...those.map(o => o.data));
return this;
}
clone(): Psbt {
// TODO: more efficient cloning
const res = Psbt.fromBuffer(this.toBuffer());
const res = Psbt.fromBuffer(this.data.toBuffer());
res.opts = JSON.parse(JSON.stringify(this.opts));
return res;
}
@ -144,7 +167,7 @@ export class Psbt extends PsbtBase {
setVersion(version: number): this {
check32Bit(version);
checkInputsForPartialSig(this.inputs, 'setVersion');
checkInputsForPartialSig(this.data.inputs, 'setVersion');
const c = this.__CACHE;
c.__TX.version = version;
c.__TX_BUF_CACHE = undefined;
@ -154,7 +177,7 @@ export class Psbt extends PsbtBase {
setLocktime(locktime: number): this {
check32Bit(locktime);
checkInputsForPartialSig(this.inputs, 'setLocktime');
checkInputsForPartialSig(this.data.inputs, 'setLocktime');
const c = this.__CACHE;
c.__TX.locktime = locktime;
c.__TX_BUF_CACHE = undefined;
@ -164,7 +187,7 @@ export class Psbt extends PsbtBase {
setSequence(inputIndex: number, sequence: number): this {
check32Bit(sequence);
checkInputsForPartialSig(this.inputs, 'setSequence');
checkInputsForPartialSig(this.data.inputs, 'setSequence');
const c = this.__CACHE;
if (c.__TX.ins.length <= inputIndex) {
throw new Error('Input index too high');
@ -181,10 +204,16 @@ export class Psbt extends PsbtBase {
}
addInput(inputData: TransactionInput): this {
checkInputsForPartialSig(this.inputs, 'addInput');
checkInputsForPartialSig(this.data.inputs, 'addInput');
const c = this.__CACHE;
const inputAdder = getInputAdder(c);
super.addInput(inputData, inputAdder);
this.data.addInput(inputData, inputAdder);
const inputIndex = this.data.inputs.length - 1;
const input = this.data.inputs[inputIndex];
if (input.nonWitnessUtxo) {
addNonWitnessTxCache(this.__CACHE, input, inputIndex);
}
c.__FEE_RATE = undefined;
c.__EXTRACTED_TX = undefined;
return this;
@ -196,7 +225,7 @@ export class Psbt extends PsbtBase {
}
addOutput(outputData: TransactionOutput): this {
checkInputsForPartialSig(this.inputs, 'addOutput');
checkInputsForPartialSig(this.data.inputs, 'addOutput');
const { address } = outputData as any;
if (typeof address === 'string') {
const { network } = this.opts;
@ -205,36 +234,26 @@ export class Psbt extends PsbtBase {
}
const c = this.__CACHE;
const outputAdder = getOutputAdder(c);
super.addOutput(outputData, true, outputAdder);
this.data.addOutput(outputData, outputAdder, true);
c.__FEE_RATE = undefined;
c.__EXTRACTED_TX = undefined;
return this;
}
addNonWitnessUtxoToInput(
inputIndex: number,
nonWitnessUtxo: NonWitnessUtxo,
): this {
super.addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo);
const input = this.inputs[inputIndex];
addNonWitnessTxCache(this.__CACHE, input, inputIndex);
return this;
}
extractTransaction(disableFeeCheck?: boolean): Transaction {
if (!this.inputs.every(isFinalized)) throw new Error('Not finalized');
if (!this.data.inputs.every(isFinalized)) throw new Error('Not finalized');
const c = this.__CACHE;
if (!disableFeeCheck) {
checkFees(this, c, this.opts);
}
if (c.__EXTRACTED_TX) return c.__EXTRACTED_TX;
const tx = c.__TX.clone();
inputFinalizeGetAmts(this.inputs, tx, c, true);
inputFinalizeGetAmts(this.data.inputs, tx, c, true);
return tx;
}
getFeeRate(): number {
if (!this.inputs.every(isFinalized))
if (!this.data.inputs.every(isFinalized))
throw new Error('PSBT must be finalized to calculate fee rate');
const c = this.__CACHE;
if (c.__FEE_RATE) return c.__FEE_RATE;
@ -246,18 +265,18 @@ export class Psbt extends PsbtBase {
} else {
tx = c.__TX.clone();
}
inputFinalizeGetAmts(this.inputs, tx, c, mustFinalize);
inputFinalizeGetAmts(this.data.inputs, tx, c, mustFinalize);
return c.__FEE_RATE!;
}
finalizeAllInputs(): this {
checkForInput(this.inputs, 0); // making sure we have at least one
range(this.inputs.length).forEach(idx => this.finalizeInput(idx));
checkForInput(this.data.inputs, 0); // making sure we have at least one
range(this.data.inputs.length).forEach(idx => this.finalizeInput(idx));
return this;
}
finalizeInput(inputIndex: number): this {
const input = checkForInput(this.inputs, inputIndex);
const input = checkForInput(this.data.inputs, inputIndex);
const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput(
inputIndex,
input,
@ -281,26 +300,26 @@ export class Psbt extends PsbtBase {
);
if (finalScriptSig)
this.addFinalScriptSigToInput(inputIndex, finalScriptSig);
this.data.addFinalScriptSigToInput(inputIndex, finalScriptSig);
if (finalScriptWitness)
this.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness);
this.data.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness);
if (!finalScriptSig && !finalScriptWitness)
throw new Error(`Unknown error finalizing input #${inputIndex}`);
this.clearFinalizedInput(inputIndex);
this.data.clearFinalizedInput(inputIndex);
return this;
}
validateAllSignatures(): boolean {
checkForInput(this.inputs, 0); // making sure we have at least one
const results = range(this.inputs.length).map(idx =>
checkForInput(this.data.inputs, 0); // making sure we have at least one
const results = range(this.data.inputs.length).map(idx =>
this.validateSignatures(idx),
);
return results.reduce((final, res) => res === true && final, true);
}
validateSignatures(inputIndex: number, pubkey?: Buffer): boolean {
const input = this.inputs[inputIndex];
const input = this.data.inputs[inputIndex];
const partialSig = (input || {}).partialSig;
if (!input || !partialSig || partialSig.length < 1)
throw new Error('No signatures to validate');
@ -343,7 +362,7 @@ export class Psbt extends PsbtBase {
// as input information is added, then eventually
// optimize this method.
const results: boolean[] = [];
for (const i of range(this.inputs.length)) {
for (const i of range(this.data.inputs.length)) {
try {
this.signInput(i, keyPair, sighashTypes);
results.push(true);
@ -371,7 +390,7 @@ export class Psbt extends PsbtBase {
// optimize this method.
const results: boolean[] = [];
const promises: Array<Promise<void>> = [];
for (const [i] of this.inputs.entries()) {
for (const [i] of this.data.inputs.entries()) {
promises.push(
this.signInputAsync(i, keyPair, sighashTypes).then(
() => {
@ -401,7 +420,7 @@ export class Psbt extends PsbtBase {
if (!keyPair || !keyPair.publicKey)
throw new Error('Need Signer to sign input');
const { hash, sighashType } = getHashAndSighashType(
this.inputs,
this.data.inputs,
inputIndex,
keyPair.publicKey,
this.__CACHE,
@ -413,7 +432,8 @@ export class Psbt extends PsbtBase {
signature: bscript.signature.encode(keyPair.sign(hash), sighashType),
};
return this.addPartialSigToInput(inputIndex, partialSig);
this.data.addPartialSigToInput(inputIndex, partialSig);
return this;
}
signInputAsync(
@ -426,7 +446,7 @@ export class Psbt extends PsbtBase {
if (!keyPair || !keyPair.publicKey)
return reject(new Error('Need Signer to sign input'));
const { hash, sighashType } = getHashAndSighashType(
this.inputs,
this.data.inputs,
inputIndex,
keyPair.publicKey,
this.__CACHE,
@ -439,12 +459,143 @@ export class Psbt extends PsbtBase {
signature: bscript.signature.encode(signature, sighashType),
};
this.addPartialSigToInput(inputIndex, partialSig);
this.data.addPartialSigToInput(inputIndex, partialSig);
resolve();
});
},
);
}
toBuffer(): Buffer {
return this.data.toBuffer();
}
toHex(): string {
return this.data.toHex();
}
toBase64(): string {
return this.data.toBase64();
}
addGlobalXpubToGlobal(globalXpub: GlobalXpub): this {
this.data.addGlobalXpubToGlobal(globalXpub);
return this;
}
addNonWitnessUtxoToInput(
inputIndex: number,
nonWitnessUtxo: NonWitnessUtxo,
): this {
this.data.addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo);
const input = this.data.inputs[inputIndex];
addNonWitnessTxCache(this.__CACHE, input, inputIndex);
return this;
}
addWitnessUtxoToInput(inputIndex: number, witnessUtxo: WitnessUtxo): this {
this.data.addWitnessUtxoToInput(inputIndex, witnessUtxo);
return this;
}
addPartialSigToInput(inputIndex: number, partialSig: PartialSig): this {
this.data.addPartialSigToInput(inputIndex, partialSig);
return this;
}
addSighashTypeToInput(inputIndex: number, sighashType: SighashType): this {
this.data.addSighashTypeToInput(inputIndex, sighashType);
return this;
}
addRedeemScriptToInput(inputIndex: number, redeemScript: RedeemScript): this {
this.data.addRedeemScriptToInput(inputIndex, redeemScript);
return this;
}
addWitnessScriptToInput(
inputIndex: number,
witnessScript: WitnessScript,
): this {
this.data.addWitnessScriptToInput(inputIndex, witnessScript);
return this;
}
addBip32DerivationToInput(
inputIndex: number,
bip32Derivation: Bip32Derivation,
): this {
this.data.addBip32DerivationToInput(inputIndex, bip32Derivation);
return this;
}
addFinalScriptSigToInput(
inputIndex: number,
finalScriptSig: FinalScriptSig,
): this {
this.data.addFinalScriptSigToInput(inputIndex, finalScriptSig);
return this;
}
addFinalScriptWitnessToInput(
inputIndex: number,
finalScriptWitness: FinalScriptWitness,
): this {
this.data.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness);
return this;
}
addPorCommitmentToInput(
inputIndex: number,
porCommitment: PorCommitment,
): this {
this.data.addPorCommitmentToInput(inputIndex, porCommitment);
return this;
}
addRedeemScriptToOutput(
outputIndex: number,
redeemScript: RedeemScript,
): this {
this.data.addRedeemScriptToOutput(outputIndex, redeemScript);
return this;
}
addWitnessScriptToOutput(
outputIndex: number,
witnessScript: WitnessScript,
): this {
this.data.addWitnessScriptToOutput(outputIndex, witnessScript);
return this;
}
addBip32DerivationToOutput(
outputIndex: number,
bip32Derivation: Bip32Derivation,
): this {
this.data.addBip32DerivationToOutput(outputIndex, bip32Derivation);
return this;
}
addUnknownKeyValToGlobal(keyVal: KeyValue): this {
this.data.addUnknownKeyValToGlobal(keyVal);
return this;
}
addUnknownKeyValToInput(inputIndex: number, keyVal: KeyValue): this {
this.data.addUnknownKeyValToInput(inputIndex, keyVal);
return this;
}
addUnknownKeyValToOutput(outputIndex: number, keyVal: KeyValue): this {
this.data.addUnknownKeyValToOutput(outputIndex, keyVal);
return this;
}
clearFinalizedInput(inputIndex: number): this {
this.data.clearFinalizedInput(inputIndex);
return this;
}
}
interface PsbtCache {