Merge pull request #1563 from bitcoinjs/addPsbtMethods
Add PSBT methods
This commit is contained in:
commit
f1d04cec00
12 changed files with 994 additions and 191 deletions
|
@ -1,3 +1,10 @@
|
||||||
|
# 5.2.0
|
||||||
|
__changed__
|
||||||
|
- Updated PSBT to allow for witnessUtxo and nonWitnessUtxo simultaneously (Re: segwit psbt bug) (#1563)
|
||||||
|
|
||||||
|
__added__
|
||||||
|
- PSBT methods `getInputType`, `inputHasPubkey`, `inputHasHDKey`, `outputHasPubkey`, `outputHasHDKey` (#1563)
|
||||||
|
|
||||||
# 5.1.10
|
# 5.1.10
|
||||||
__fixed__
|
__fixed__
|
||||||
- Fixed psbt.signInputAsync (and consequentially all Async signing methods) not handling rejection of keypair.sign properly (#1582)
|
- Fixed psbt.signInputAsync (and consequentially all Async signing methods) not handling rejection of keypair.sign properly (#1582)
|
||||||
|
|
11
package-lock.json
generated
11
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "bitcoinjs-lib",
|
"name": "bitcoinjs-lib",
|
||||||
"version": "5.1.10",
|
"version": "5.2.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -367,9 +367,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bip174": {
|
"bip174": {
|
||||||
"version": "1.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/bip174/-/bip174-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/bip174/-/bip174-2.0.1.tgz",
|
||||||
"integrity": "sha512-Mq2aFs1TdMfxBpYPg7uzjhsiXbAtoVq44TNjEWtvuZBiBgc3m7+n55orYMtTAxdg7jWbL4DtH0MKocJER4xERQ=="
|
"integrity": "sha512-i3X26uKJOkDTAalYAp0Er+qGMDhrbbh2o93/xiPyAN2s25KrClSpe3VXo/7mNJoqA5qfko8rLS2l3RWZgYmjKQ=="
|
||||||
},
|
},
|
||||||
"bip32": {
|
"bip32": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
|
@ -1344,7 +1344,8 @@
|
||||||
"lodash": {
|
"lodash": {
|
||||||
"version": "4.17.19",
|
"version": "4.17.19",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
|
||||||
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
|
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"lodash.flattendeep": {
|
"lodash.flattendeep": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "bitcoinjs-lib",
|
"name": "bitcoinjs-lib",
|
||||||
"version": "5.1.10",
|
"version": "5.2.0",
|
||||||
"description": "Client-side Bitcoin JavaScript library",
|
"description": "Client-side Bitcoin JavaScript library",
|
||||||
"main": "./src/index.js",
|
"main": "./src/index.js",
|
||||||
"types": "./types/index.d.ts",
|
"types": "./types/index.d.ts",
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bech32": "^1.1.2",
|
"bech32": "^1.1.2",
|
||||||
"bip174": "^1.0.1",
|
"bip174": "^2.0.1",
|
||||||
"bip32": "^2.0.4",
|
"bip32": "^2.0.4",
|
||||||
"bip66": "^1.1.0",
|
"bip66": "^1.1.0",
|
||||||
"bitcoin-ops": "^1.4.0",
|
"bitcoin-ops": "^1.4.0",
|
||||||
|
|
|
@ -42,9 +42,9 @@ function reverseBuffer(buffer) {
|
||||||
}
|
}
|
||||||
exports.reverseBuffer = reverseBuffer;
|
exports.reverseBuffer = reverseBuffer;
|
||||||
function cloneBuffer(buffer) {
|
function cloneBuffer(buffer) {
|
||||||
const clone = Buffer.alloc(buffer.length);
|
const clone = Buffer.allocUnsafe(buffer.length);
|
||||||
buffer.copy(clone);
|
buffer.copy(clone);
|
||||||
return buffer;
|
return clone;
|
||||||
}
|
}
|
||||||
exports.cloneBuffer = cloneBuffer;
|
exports.cloneBuffer = cloneBuffer;
|
||||||
/**
|
/**
|
||||||
|
|
330
src/psbt.js
330
src/psbt.js
|
@ -69,6 +69,14 @@ class Psbt {
|
||||||
__NON_WITNESS_UTXO_BUF_CACHE: [],
|
__NON_WITNESS_UTXO_BUF_CACHE: [],
|
||||||
__TX_IN_CACHE: {},
|
__TX_IN_CACHE: {},
|
||||||
__TX: this.data.globalMap.unsignedTx.tx,
|
__TX: this.data.globalMap.unsignedTx.tx,
|
||||||
|
// Old TransactionBuilder behavior was to not confirm input values
|
||||||
|
// before signing. Even though we highly encourage people to get
|
||||||
|
// the full parent transaction to verify values, the ability to
|
||||||
|
// sign non-segwit inputs without the full transaction was often
|
||||||
|
// requested. So the only way to activate is to use @ts-ignore.
|
||||||
|
// We will disable exporting the Psbt when unsafe sign is active.
|
||||||
|
// because it is not BIP174 compliant.
|
||||||
|
__UNSAFE_SIGN_NONSEGWIT: false,
|
||||||
};
|
};
|
||||||
if (this.data.inputs.length === 0) this.setVersion(2);
|
if (this.data.inputs.length === 0) this.setVersion(2);
|
||||||
// Make data hidden when enumerating
|
// Make data hidden when enumerating
|
||||||
|
@ -187,6 +195,7 @@ class Psbt {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
checkInputsForPartialSig(this.data.inputs, 'addInput');
|
checkInputsForPartialSig(this.data.inputs, 'addInput');
|
||||||
|
if (inputData.witnessScript) checkInvalidP2WSH(inputData.witnessScript);
|
||||||
const c = this.__CACHE;
|
const c = this.__CACHE;
|
||||||
this.data.addInput(inputData);
|
this.data.addInput(inputData);
|
||||||
const txIn = c.__TX.ins[c.__TX.ins.length - 1];
|
const txIn = c.__TX.ins[c.__TX.ins.length - 1];
|
||||||
|
@ -283,6 +292,43 @@ class Psbt {
|
||||||
this.data.clearFinalizedInput(inputIndex);
|
this.data.clearFinalizedInput(inputIndex);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
getInputType(inputIndex) {
|
||||||
|
const input = utils_1.checkForInput(this.data.inputs, inputIndex);
|
||||||
|
const script = getScriptFromUtxo(inputIndex, input, this.__CACHE);
|
||||||
|
const result = getMeaningfulScript(
|
||||||
|
script,
|
||||||
|
inputIndex,
|
||||||
|
'input',
|
||||||
|
input.redeemScript || redeemFromFinalScriptSig(input.finalScriptSig),
|
||||||
|
input.witnessScript ||
|
||||||
|
redeemFromFinalWitnessScript(input.finalScriptWitness),
|
||||||
|
);
|
||||||
|
const type = result.type === 'raw' ? '' : result.type + '-';
|
||||||
|
const mainType = classifyScript(result.meaningfulScript);
|
||||||
|
return type + mainType;
|
||||||
|
}
|
||||||
|
inputHasPubkey(inputIndex, pubkey) {
|
||||||
|
const input = utils_1.checkForInput(this.data.inputs, inputIndex);
|
||||||
|
return pubkeyInInput(pubkey, input, inputIndex, this.__CACHE);
|
||||||
|
}
|
||||||
|
inputHasHDKey(inputIndex, root) {
|
||||||
|
const input = utils_1.checkForInput(this.data.inputs, inputIndex);
|
||||||
|
const derivationIsMine = bip32DerivationIsMine(root);
|
||||||
|
return (
|
||||||
|
!!input.bip32Derivation && input.bip32Derivation.some(derivationIsMine)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
outputHasPubkey(outputIndex, pubkey) {
|
||||||
|
const output = utils_1.checkForOutput(this.data.outputs, outputIndex);
|
||||||
|
return pubkeyInOutput(pubkey, output, outputIndex, this.__CACHE);
|
||||||
|
}
|
||||||
|
outputHasHDKey(outputIndex, root) {
|
||||||
|
const output = utils_1.checkForOutput(this.data.outputs, outputIndex);
|
||||||
|
const derivationIsMine = bip32DerivationIsMine(root);
|
||||||
|
return (
|
||||||
|
!!output.bip32Derivation && output.bip32Derivation.some(derivationIsMine)
|
||||||
|
);
|
||||||
|
}
|
||||||
validateSignaturesOfAllInputs() {
|
validateSignaturesOfAllInputs() {
|
||||||
utils_1.checkForInput(this.data.inputs, 0); // making sure we have at least one
|
utils_1.checkForInput(this.data.inputs, 0); // making sure we have at least one
|
||||||
const results = range(this.data.inputs.length).map(idx =>
|
const results = range(this.data.inputs.length).map(idx =>
|
||||||
|
@ -311,6 +357,7 @@ class Psbt {
|
||||||
inputIndex,
|
inputIndex,
|
||||||
Object.assign({}, input, { sighashType: sig.hashType }),
|
Object.assign({}, input, { sighashType: sig.hashType }),
|
||||||
this.__CACHE,
|
this.__CACHE,
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
: { hash: hashCache, script: scriptCache };
|
: { hash: hashCache, script: scriptCache };
|
||||||
sighashCache = sig.hashType;
|
sighashCache = sig.hashType;
|
||||||
|
@ -510,12 +557,15 @@ class Psbt {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
toBuffer() {
|
toBuffer() {
|
||||||
|
checkCache(this.__CACHE);
|
||||||
return this.data.toBuffer();
|
return this.data.toBuffer();
|
||||||
}
|
}
|
||||||
toHex() {
|
toHex() {
|
||||||
|
checkCache(this.__CACHE);
|
||||||
return this.data.toHex();
|
return this.data.toHex();
|
||||||
}
|
}
|
||||||
toBase64() {
|
toBase64() {
|
||||||
|
checkCache(this.__CACHE);
|
||||||
return this.data.toBase64();
|
return this.data.toBase64();
|
||||||
}
|
}
|
||||||
updateGlobal(updateData) {
|
updateGlobal(updateData) {
|
||||||
|
@ -523,6 +573,7 @@ class Psbt {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
updateInput(inputIndex, updateData) {
|
updateInput(inputIndex, updateData) {
|
||||||
|
if (updateData.witnessScript) checkInvalidP2WSH(updateData.witnessScript);
|
||||||
this.data.updateInput(inputIndex, updateData);
|
this.data.updateInput(inputIndex, updateData);
|
||||||
if (updateData.nonWitnessUtxo) {
|
if (updateData.nonWitnessUtxo) {
|
||||||
addNonWitnessTxCache(
|
addNonWitnessTxCache(
|
||||||
|
@ -623,6 +674,11 @@ function canFinalize(input, script, scriptType) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function checkCache(cache) {
|
||||||
|
if (cache.__UNSAFE_SIGN_NONSEGWIT !== false) {
|
||||||
|
throw new Error('Not BIP174 compliant, can not export');
|
||||||
|
}
|
||||||
|
}
|
||||||
function hasSigs(neededSigs, partialSig, pubkeys) {
|
function hasSigs(neededSigs, partialSig, pubkeys) {
|
||||||
if (!partialSig) return false;
|
if (!partialSig) return false;
|
||||||
let sigs;
|
let sigs;
|
||||||
|
@ -658,6 +714,14 @@ const isP2PK = isPaymentFactory(payments.p2pk);
|
||||||
const isP2PKH = isPaymentFactory(payments.p2pkh);
|
const isP2PKH = isPaymentFactory(payments.p2pkh);
|
||||||
const isP2WPKH = isPaymentFactory(payments.p2wpkh);
|
const isP2WPKH = isPaymentFactory(payments.p2wpkh);
|
||||||
const isP2WSHScript = isPaymentFactory(payments.p2wsh);
|
const isP2WSHScript = isPaymentFactory(payments.p2wsh);
|
||||||
|
const isP2SHScript = isPaymentFactory(payments.p2sh);
|
||||||
|
function bip32DerivationIsMine(root) {
|
||||||
|
return d => {
|
||||||
|
if (!d.masterFingerprint.equals(root.fingerprint)) return false;
|
||||||
|
if (!root.derivePath(d.path).publicKey.equals(d.pubkey)) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
function check32Bit(num) {
|
function check32Bit(num) {
|
||||||
if (
|
if (
|
||||||
typeof num !== 'number' ||
|
typeof num !== 'number' ||
|
||||||
|
@ -728,14 +792,7 @@ function checkPartialSigSighashes(input) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function checkScriptForPubkey(pubkey, script, action) {
|
function checkScriptForPubkey(pubkey, script, action) {
|
||||||
const pubkeyHash = crypto_1.hash160(pubkey);
|
if (!pubkeyInScript(pubkey, script)) {
|
||||||
const decompiled = bscript.decompile(script);
|
|
||||||
if (decompiled === null) throw new Error('Unknown script error');
|
|
||||||
const hasKey = decompiled.some(element => {
|
|
||||||
if (typeof element === 'number') return false;
|
|
||||||
return element.equals(pubkey) || element.equals(pubkeyHash);
|
|
||||||
});
|
|
||||||
if (!hasKey) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Can not ${action} for this input with the key ${pubkey.toString('hex')}`,
|
`Can not ${action} for this input with the key ${pubkey.toString('hex')}`,
|
||||||
);
|
);
|
||||||
|
@ -767,13 +824,13 @@ function checkTxInputCache(cache, input) {
|
||||||
cache.__TX_IN_CACHE[key] = 1;
|
cache.__TX_IN_CACHE[key] = 1;
|
||||||
}
|
}
|
||||||
function scriptCheckerFactory(payment, paymentScriptName) {
|
function scriptCheckerFactory(payment, paymentScriptName) {
|
||||||
return (inputIndex, scriptPubKey, redeemScript) => {
|
return (inputIndex, scriptPubKey, redeemScript, ioType) => {
|
||||||
const redeemScriptOutput = payment({
|
const redeemScriptOutput = payment({
|
||||||
redeem: { output: redeemScript },
|
redeem: { output: redeemScript },
|
||||||
}).output;
|
}).output;
|
||||||
if (!scriptPubKey.equals(redeemScriptOutput)) {
|
if (!scriptPubKey.equals(redeemScriptOutput)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${paymentScriptName} for input #${inputIndex} doesn't match the scriptPubKey in the prevout`,
|
`${paymentScriptName} for ${ioType} #${inputIndex} doesn't match the scriptPubKey in the prevout`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -860,6 +917,7 @@ function getHashAndSighashType(
|
||||||
inputIndex,
|
inputIndex,
|
||||||
input,
|
input,
|
||||||
cache,
|
cache,
|
||||||
|
false,
|
||||||
sighashTypes,
|
sighashTypes,
|
||||||
);
|
);
|
||||||
checkScriptForPubkey(pubkey, script, 'sign');
|
checkScriptForPubkey(pubkey, script, 'sign');
|
||||||
|
@ -868,7 +926,7 @@ function getHashAndSighashType(
|
||||||
sighashType,
|
sighashType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function getHashForSig(inputIndex, input, cache, sighashTypes) {
|
function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) {
|
||||||
const unsignedTx = cache.__TX;
|
const unsignedTx = cache.__TX;
|
||||||
const sighashType =
|
const sighashType =
|
||||||
input.sighashType || transaction_1.Transaction.SIGHASH_ALL;
|
input.sighashType || transaction_1.Transaction.SIGHASH_ALL;
|
||||||
|
@ -880,7 +938,7 @@ function getHashForSig(inputIndex, input, cache, sighashTypes) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let hash;
|
let hash;
|
||||||
let script;
|
let prevout;
|
||||||
if (input.nonWitnessUtxo) {
|
if (input.nonWitnessUtxo) {
|
||||||
const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache(
|
const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache(
|
||||||
cache,
|
cache,
|
||||||
|
@ -896,83 +954,64 @@ function getHashForSig(inputIndex, input, cache, sighashTypes) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const prevoutIndex = unsignedTx.ins[inputIndex].index;
|
const prevoutIndex = unsignedTx.ins[inputIndex].index;
|
||||||
const prevout = nonWitnessUtxoTx.outs[prevoutIndex];
|
prevout = nonWitnessUtxoTx.outs[prevoutIndex];
|
||||||
if (input.redeemScript) {
|
|
||||||
// If a redeemScript is provided, the scriptPubKey must be for that redeemScript
|
|
||||||
checkRedeemScript(inputIndex, prevout.script, input.redeemScript);
|
|
||||||
script = input.redeemScript;
|
|
||||||
} else {
|
|
||||||
script = prevout.script;
|
|
||||||
}
|
|
||||||
if (isP2WSHScript(script)) {
|
|
||||||
if (!input.witnessScript)
|
|
||||||
throw new Error('Segwit input needs witnessScript if not P2WPKH');
|
|
||||||
checkWitnessScript(inputIndex, script, input.witnessScript);
|
|
||||||
hash = unsignedTx.hashForWitnessV0(
|
|
||||||
inputIndex,
|
|
||||||
input.witnessScript,
|
|
||||||
prevout.value,
|
|
||||||
sighashType,
|
|
||||||
);
|
|
||||||
script = input.witnessScript;
|
|
||||||
} else if (isP2WPKH(script)) {
|
|
||||||
// P2WPKH uses the P2PKH template for prevoutScript when signing
|
|
||||||
const signingScript = payments.p2pkh({ hash: script.slice(2) }).output;
|
|
||||||
hash = unsignedTx.hashForWitnessV0(
|
|
||||||
inputIndex,
|
|
||||||
signingScript,
|
|
||||||
prevout.value,
|
|
||||||
sighashType,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
hash = unsignedTx.hashForSignature(inputIndex, script, sighashType);
|
|
||||||
}
|
|
||||||
} else if (input.witnessUtxo) {
|
} else if (input.witnessUtxo) {
|
||||||
let _script; // so we don't shadow the `let script` above
|
prevout = input.witnessUtxo;
|
||||||
if (input.redeemScript) {
|
|
||||||
// If a redeemScript is provided, the scriptPubKey must be for that redeemScript
|
|
||||||
checkRedeemScript(
|
|
||||||
inputIndex,
|
|
||||||
input.witnessUtxo.script,
|
|
||||||
input.redeemScript,
|
|
||||||
);
|
|
||||||
_script = input.redeemScript;
|
|
||||||
} else {
|
|
||||||
_script = input.witnessUtxo.script;
|
|
||||||
}
|
|
||||||
if (isP2WPKH(_script)) {
|
|
||||||
// P2WPKH uses the P2PKH template for prevoutScript when signing
|
|
||||||
const signingScript = payments.p2pkh({ hash: _script.slice(2) }).output;
|
|
||||||
hash = unsignedTx.hashForWitnessV0(
|
|
||||||
inputIndex,
|
|
||||||
signingScript,
|
|
||||||
input.witnessUtxo.value,
|
|
||||||
sighashType,
|
|
||||||
);
|
|
||||||
script = _script;
|
|
||||||
} else if (isP2WSHScript(_script)) {
|
|
||||||
if (!input.witnessScript)
|
|
||||||
throw new Error('Segwit input needs witnessScript if not P2WPKH');
|
|
||||||
checkWitnessScript(inputIndex, _script, input.witnessScript);
|
|
||||||
hash = unsignedTx.hashForWitnessV0(
|
|
||||||
inputIndex,
|
|
||||||
input.witnessScript,
|
|
||||||
input.witnessUtxo.value,
|
|
||||||
sighashType,
|
|
||||||
);
|
|
||||||
// want to make sure the script we return is the actual meaningful script
|
|
||||||
script = input.witnessScript;
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`Input #${inputIndex} has witnessUtxo but non-segwit script: ` +
|
|
||||||
`${_script.toString('hex')}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Need a Utxo input item for signing');
|
throw new Error('Need a Utxo input item for signing');
|
||||||
}
|
}
|
||||||
|
const { meaningfulScript, type } = getMeaningfulScript(
|
||||||
|
prevout.script,
|
||||||
|
inputIndex,
|
||||||
|
'input',
|
||||||
|
input.redeemScript,
|
||||||
|
input.witnessScript,
|
||||||
|
);
|
||||||
|
if (['p2sh-p2wsh', 'p2wsh'].indexOf(type) >= 0) {
|
||||||
|
hash = unsignedTx.hashForWitnessV0(
|
||||||
|
inputIndex,
|
||||||
|
meaningfulScript,
|
||||||
|
prevout.value,
|
||||||
|
sighashType,
|
||||||
|
);
|
||||||
|
} else if (isP2WPKH(meaningfulScript)) {
|
||||||
|
// P2WPKH uses the P2PKH template for prevoutScript when signing
|
||||||
|
const signingScript = payments.p2pkh({ hash: meaningfulScript.slice(2) })
|
||||||
|
.output;
|
||||||
|
hash = unsignedTx.hashForWitnessV0(
|
||||||
|
inputIndex,
|
||||||
|
signingScript,
|
||||||
|
prevout.value,
|
||||||
|
sighashType,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// non-segwit
|
||||||
|
if (
|
||||||
|
input.nonWitnessUtxo === undefined &&
|
||||||
|
cache.__UNSAFE_SIGN_NONSEGWIT === false
|
||||||
|
)
|
||||||
|
throw new Error(
|
||||||
|
`Input #${inputIndex} has witnessUtxo but non-segwit script: ` +
|
||||||
|
`${meaningfulScript.toString('hex')}`,
|
||||||
|
);
|
||||||
|
if (!forValidate && cache.__UNSAFE_SIGN_NONSEGWIT !== false)
|
||||||
|
console.warn(
|
||||||
|
'Warning: Signing non-segwit inputs without the full parent transaction ' +
|
||||||
|
'means there is a chance that a miner could feed you incorrect information ' +
|
||||||
|
'to trick you into paying large fees. This behavior is the same as the old ' +
|
||||||
|
'TransactionBuilder class when signing non-segwit scripts. You are not ' +
|
||||||
|
'able to export this Psbt with toBuffer|toBase64|toHex since it is not ' +
|
||||||
|
'BIP174 compliant.\n*********************\nPROCEED WITH CAUTION!\n' +
|
||||||
|
'*********************',
|
||||||
|
);
|
||||||
|
hash = unsignedTx.hashForSignature(
|
||||||
|
inputIndex,
|
||||||
|
meaningfulScript,
|
||||||
|
sighashType,
|
||||||
|
);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
script,
|
script: meaningfulScript,
|
||||||
sighashType,
|
sighashType,
|
||||||
hash,
|
hash,
|
||||||
};
|
};
|
||||||
|
@ -1224,6 +1263,129 @@ function nonWitnessUtxoTxFromCache(cache, input, inputIndex) {
|
||||||
}
|
}
|
||||||
return c[inputIndex];
|
return c[inputIndex];
|
||||||
}
|
}
|
||||||
|
function getScriptFromUtxo(inputIndex, input, cache) {
|
||||||
|
if (input.witnessUtxo !== undefined) {
|
||||||
|
return input.witnessUtxo.script;
|
||||||
|
} else if (input.nonWitnessUtxo !== undefined) {
|
||||||
|
const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache(
|
||||||
|
cache,
|
||||||
|
input,
|
||||||
|
inputIndex,
|
||||||
|
);
|
||||||
|
return nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index].script;
|
||||||
|
} else {
|
||||||
|
throw new Error("Can't find pubkey in input without Utxo data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function pubkeyInInput(pubkey, input, inputIndex, cache) {
|
||||||
|
const script = getScriptFromUtxo(inputIndex, input, cache);
|
||||||
|
const { meaningfulScript } = getMeaningfulScript(
|
||||||
|
script,
|
||||||
|
inputIndex,
|
||||||
|
'input',
|
||||||
|
input.redeemScript,
|
||||||
|
input.witnessScript,
|
||||||
|
);
|
||||||
|
return pubkeyInScript(pubkey, meaningfulScript);
|
||||||
|
}
|
||||||
|
function pubkeyInOutput(pubkey, output, outputIndex, cache) {
|
||||||
|
const script = cache.__TX.outs[outputIndex].script;
|
||||||
|
const { meaningfulScript } = getMeaningfulScript(
|
||||||
|
script,
|
||||||
|
outputIndex,
|
||||||
|
'output',
|
||||||
|
output.redeemScript,
|
||||||
|
output.witnessScript,
|
||||||
|
);
|
||||||
|
return pubkeyInScript(pubkey, meaningfulScript);
|
||||||
|
}
|
||||||
|
function redeemFromFinalScriptSig(finalScript) {
|
||||||
|
if (!finalScript) return;
|
||||||
|
const decomp = bscript.decompile(finalScript);
|
||||||
|
if (!decomp) return;
|
||||||
|
const lastItem = decomp[decomp.length - 1];
|
||||||
|
if (
|
||||||
|
!Buffer.isBuffer(lastItem) ||
|
||||||
|
isPubkeyLike(lastItem) ||
|
||||||
|
isSigLike(lastItem)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
const sDecomp = bscript.decompile(lastItem);
|
||||||
|
if (!sDecomp) return;
|
||||||
|
return lastItem;
|
||||||
|
}
|
||||||
|
function redeemFromFinalWitnessScript(finalScript) {
|
||||||
|
if (!finalScript) return;
|
||||||
|
const decomp = scriptWitnessToWitnessStack(finalScript);
|
||||||
|
const lastItem = decomp[decomp.length - 1];
|
||||||
|
if (isPubkeyLike(lastItem)) return;
|
||||||
|
const sDecomp = bscript.decompile(lastItem);
|
||||||
|
if (!sDecomp) return;
|
||||||
|
return lastItem;
|
||||||
|
}
|
||||||
|
function isPubkeyLike(buf) {
|
||||||
|
return buf.length === 33 && bscript.isCanonicalPubKey(buf);
|
||||||
|
}
|
||||||
|
function isSigLike(buf) {
|
||||||
|
return bscript.isCanonicalScriptSignature(buf);
|
||||||
|
}
|
||||||
|
function getMeaningfulScript(
|
||||||
|
script,
|
||||||
|
index,
|
||||||
|
ioType,
|
||||||
|
redeemScript,
|
||||||
|
witnessScript,
|
||||||
|
) {
|
||||||
|
const isP2SH = isP2SHScript(script);
|
||||||
|
const isP2SHP2WSH = isP2SH && redeemScript && isP2WSHScript(redeemScript);
|
||||||
|
const isP2WSH = isP2WSHScript(script);
|
||||||
|
if (isP2SH && redeemScript === undefined)
|
||||||
|
throw new Error('scriptPubkey is P2SH but redeemScript missing');
|
||||||
|
if ((isP2WSH || isP2SHP2WSH) && witnessScript === undefined)
|
||||||
|
throw new Error(
|
||||||
|
'scriptPubkey or redeemScript is P2WSH but witnessScript missing',
|
||||||
|
);
|
||||||
|
let meaningfulScript;
|
||||||
|
if (isP2SHP2WSH) {
|
||||||
|
meaningfulScript = witnessScript;
|
||||||
|
checkRedeemScript(index, script, redeemScript, ioType);
|
||||||
|
checkWitnessScript(index, redeemScript, witnessScript, ioType);
|
||||||
|
checkInvalidP2WSH(meaningfulScript);
|
||||||
|
} else if (isP2WSH) {
|
||||||
|
meaningfulScript = witnessScript;
|
||||||
|
checkWitnessScript(index, script, witnessScript, ioType);
|
||||||
|
checkInvalidP2WSH(meaningfulScript);
|
||||||
|
} else if (isP2SH) {
|
||||||
|
meaningfulScript = redeemScript;
|
||||||
|
checkRedeemScript(index, script, redeemScript, ioType);
|
||||||
|
} else {
|
||||||
|
meaningfulScript = script;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
meaningfulScript,
|
||||||
|
type: isP2SHP2WSH
|
||||||
|
? 'p2sh-p2wsh'
|
||||||
|
: isP2SH
|
||||||
|
? 'p2sh'
|
||||||
|
: isP2WSH
|
||||||
|
? 'p2wsh'
|
||||||
|
: 'raw',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function checkInvalidP2WSH(script) {
|
||||||
|
if (isP2WPKH(script) || isP2SHScript(script)) {
|
||||||
|
throw new Error('P2WPKH or P2SH can not be contained within P2WSH');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function pubkeyInScript(pubkey, script) {
|
||||||
|
const pubkeyHash = crypto_1.hash160(pubkey);
|
||||||
|
const decompiled = bscript.decompile(script);
|
||||||
|
if (decompiled === null) throw new Error('Unknown script error');
|
||||||
|
return decompiled.some(element => {
|
||||||
|
if (typeof element === 'number') return false;
|
||||||
|
return element.equals(pubkey) || element.equals(pubkeyHash);
|
||||||
|
});
|
||||||
|
}
|
||||||
function classifyScript(script) {
|
function classifyScript(script) {
|
||||||
if (isP2WPKH(script)) return 'witnesspubkeyhash';
|
if (isP2WPKH(script)) return 'witnesspubkeyhash';
|
||||||
if (isP2PKH(script)) return 'pubkeyhash';
|
if (isP2PKH(script)) return 'pubkeyhash';
|
||||||
|
|
18
test/fixtures/psbt.json
vendored
18
test/fixtures/psbt.json
vendored
|
@ -313,6 +313,24 @@
|
||||||
},
|
},
|
||||||
"exception": "Invalid arguments for Psbt\\.addInput\\. Requires single object with at least \\[hash\\] and \\[index\\]"
|
"exception": "Invalid arguments for Psbt\\.addInput\\. Requires single object with at least \\[hash\\] and \\[index\\]"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "checks for invalid p2wsh witnessScript",
|
||||||
|
"inputData": {
|
||||||
|
"hash": "Buffer.from('000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f', 'hex')",
|
||||||
|
"index": 0,
|
||||||
|
"witnessScript": "Buffer.from('0014000102030405060708090a0b0c0d0e0f00010203', 'hex')"
|
||||||
|
},
|
||||||
|
"exception": "P2WPKH or P2SH can not be contained within P2WSH"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "checks for invalid p2wsh witnessScript",
|
||||||
|
"inputData": {
|
||||||
|
"hash": "Buffer.from('000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f', 'hex')",
|
||||||
|
"index": 0,
|
||||||
|
"witnessScript": "Buffer.from('a914000102030405060708090a0b0c0d0e0f0001020387', 'hex')"
|
||||||
|
},
|
||||||
|
"exception": "P2WPKH or P2SH can not be contained within P2WSH"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "should be equal",
|
"description": "should be equal",
|
||||||
"inputData": {
|
"inputData": {
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
import { describe, it } from 'mocha';
|
import { describe, it } from 'mocha';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
bip32,
|
bip32,
|
||||||
ECPair,
|
ECPair,
|
||||||
networks as NETWORKS,
|
networks as NETWORKS,
|
||||||
|
payments,
|
||||||
Psbt,
|
Psbt,
|
||||||
Signer,
|
Signer,
|
||||||
SignerAsync,
|
SignerAsync,
|
||||||
|
@ -597,6 +599,296 @@ describe(`Psbt`, () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getInputType', () => {
|
||||||
|
const key = ECPair.makeRandom();
|
||||||
|
const { publicKey } = key;
|
||||||
|
const p2wpkhPub = (pubkey: Buffer): Buffer =>
|
||||||
|
payments.p2wpkh({
|
||||||
|
pubkey,
|
||||||
|
}).output!;
|
||||||
|
const p2pkhPub = (pubkey: Buffer): Buffer =>
|
||||||
|
payments.p2pkh({
|
||||||
|
pubkey,
|
||||||
|
}).output!;
|
||||||
|
const p2shOut = (output: Buffer): Buffer =>
|
||||||
|
payments.p2sh({
|
||||||
|
redeem: { output },
|
||||||
|
}).output!;
|
||||||
|
const p2wshOut = (output: Buffer): Buffer =>
|
||||||
|
payments.p2wsh({
|
||||||
|
redeem: { output },
|
||||||
|
}).output!;
|
||||||
|
const p2shp2wshOut = (output: Buffer): Buffer => p2shOut(p2wshOut(output));
|
||||||
|
const noOuter = (output: Buffer): Buffer => output;
|
||||||
|
|
||||||
|
function getInputTypeTest({
|
||||||
|
innerScript,
|
||||||
|
outerScript,
|
||||||
|
redeemGetter,
|
||||||
|
witnessGetter,
|
||||||
|
expectedType,
|
||||||
|
finalize,
|
||||||
|
}: any): void {
|
||||||
|
const psbt = new Psbt();
|
||||||
|
psbt
|
||||||
|
.addInput({
|
||||||
|
hash:
|
||||||
|
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||||
|
index: 0,
|
||||||
|
witnessUtxo: {
|
||||||
|
script: outerScript(innerScript(publicKey)),
|
||||||
|
value: 2e3,
|
||||||
|
},
|
||||||
|
...(redeemGetter ? { redeemScript: redeemGetter(publicKey) } : {}),
|
||||||
|
...(witnessGetter ? { witnessScript: witnessGetter(publicKey) } : {}),
|
||||||
|
})
|
||||||
|
.addOutput({
|
||||||
|
script: Buffer.from('0014d85c2b71d0060b09c9886aeb815e50991dda124d'),
|
||||||
|
value: 1800,
|
||||||
|
});
|
||||||
|
if (finalize) psbt.signInput(0, key).finalizeInput(0);
|
||||||
|
const type = psbt.getInputType(0);
|
||||||
|
assert.strictEqual(type, expectedType, 'incorrect input type');
|
||||||
|
}
|
||||||
|
[
|
||||||
|
{
|
||||||
|
innerScript: p2pkhPub,
|
||||||
|
outerScript: noOuter,
|
||||||
|
redeemGetter: null,
|
||||||
|
witnessGetter: null,
|
||||||
|
expectedType: 'pubkeyhash',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
innerScript: p2wpkhPub,
|
||||||
|
outerScript: noOuter,
|
||||||
|
redeemGetter: null,
|
||||||
|
witnessGetter: null,
|
||||||
|
expectedType: 'witnesspubkeyhash',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
innerScript: p2pkhPub,
|
||||||
|
outerScript: p2shOut,
|
||||||
|
redeemGetter: p2pkhPub,
|
||||||
|
witnessGetter: null,
|
||||||
|
expectedType: 'p2sh-pubkeyhash',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
innerScript: p2wpkhPub,
|
||||||
|
outerScript: p2shOut,
|
||||||
|
redeemGetter: p2wpkhPub,
|
||||||
|
witnessGetter: null,
|
||||||
|
expectedType: 'p2sh-witnesspubkeyhash',
|
||||||
|
finalize: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
innerScript: p2pkhPub,
|
||||||
|
outerScript: p2wshOut,
|
||||||
|
redeemGetter: null,
|
||||||
|
witnessGetter: p2pkhPub,
|
||||||
|
expectedType: 'p2wsh-pubkeyhash',
|
||||||
|
finalize: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
innerScript: p2pkhPub,
|
||||||
|
outerScript: p2shp2wshOut,
|
||||||
|
redeemGetter: (pk: Buffer): Buffer => p2wshOut(p2pkhPub(pk)),
|
||||||
|
witnessGetter: p2pkhPub,
|
||||||
|
expectedType: 'p2sh-p2wsh-pubkeyhash',
|
||||||
|
},
|
||||||
|
].forEach(getInputTypeTest);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('inputHasHDKey', () => {
|
||||||
|
it('should return true if HD key is present', () => {
|
||||||
|
const root = bip32.fromSeed(crypto.randomBytes(32));
|
||||||
|
const root2 = bip32.fromSeed(crypto.randomBytes(32));
|
||||||
|
const path = "m/0'/0";
|
||||||
|
const psbt = new Psbt();
|
||||||
|
psbt.addInput({
|
||||||
|
hash:
|
||||||
|
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||||
|
index: 0,
|
||||||
|
bip32Derivation: [
|
||||||
|
{
|
||||||
|
masterFingerprint: root.fingerprint,
|
||||||
|
path,
|
||||||
|
pubkey: root.derivePath(path).publicKey,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.strictEqual(psbt.inputHasHDKey(0, root), true);
|
||||||
|
assert.strictEqual(psbt.inputHasHDKey(0, root2), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('inputHasPubkey', () => {
|
||||||
|
it('should throw', () => {
|
||||||
|
const psbt = new Psbt();
|
||||||
|
psbt.addInput({
|
||||||
|
hash:
|
||||||
|
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
psbt.inputHasPubkey(0, Buffer.from([]));
|
||||||
|
}, new RegExp("Can't find pubkey in input without Utxo data"));
|
||||||
|
|
||||||
|
psbt.updateInput(0, {
|
||||||
|
witnessUtxo: {
|
||||||
|
value: 1337,
|
||||||
|
script: payments.p2sh({
|
||||||
|
redeem: { output: Buffer.from([0x51]) },
|
||||||
|
}).output!,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
psbt.inputHasPubkey(0, Buffer.from([]));
|
||||||
|
}, new RegExp('scriptPubkey is P2SH but redeemScript missing'));
|
||||||
|
|
||||||
|
delete psbt.data.inputs[0].witnessUtxo;
|
||||||
|
|
||||||
|
psbt.updateInput(0, {
|
||||||
|
witnessUtxo: {
|
||||||
|
value: 1337,
|
||||||
|
script: payments.p2wsh({
|
||||||
|
redeem: { output: Buffer.from([0x51]) },
|
||||||
|
}).output!,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
psbt.inputHasPubkey(0, Buffer.from([]));
|
||||||
|
}, new RegExp('scriptPubkey or redeemScript is P2WSH but witnessScript missing'));
|
||||||
|
|
||||||
|
delete psbt.data.inputs[0].witnessUtxo;
|
||||||
|
|
||||||
|
psbt.updateInput(0, {
|
||||||
|
witnessUtxo: {
|
||||||
|
value: 1337,
|
||||||
|
script: payments.p2sh({
|
||||||
|
redeem: payments.p2wsh({
|
||||||
|
redeem: { output: Buffer.from([0x51]) },
|
||||||
|
}),
|
||||||
|
}).output!,
|
||||||
|
},
|
||||||
|
redeemScript: payments.p2wsh({
|
||||||
|
redeem: { output: Buffer.from([0x51]) },
|
||||||
|
}).output!,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
psbt.inputHasPubkey(0, Buffer.from([]));
|
||||||
|
}, new RegExp('scriptPubkey or redeemScript is P2WSH but witnessScript missing'));
|
||||||
|
|
||||||
|
psbt.updateInput(0, {
|
||||||
|
witnessScript: Buffer.from([0x51]),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.doesNotThrow(() => {
|
||||||
|
psbt.inputHasPubkey(0, Buffer.from([0x51]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('outputHasHDKey', () => {
|
||||||
|
it('should return true if HD key is present', () => {
|
||||||
|
const root = bip32.fromSeed(crypto.randomBytes(32));
|
||||||
|
const root2 = bip32.fromSeed(crypto.randomBytes(32));
|
||||||
|
const path = "m/0'/0";
|
||||||
|
const psbt = new Psbt();
|
||||||
|
psbt
|
||||||
|
.addInput({
|
||||||
|
hash:
|
||||||
|
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||||
|
index: 0,
|
||||||
|
})
|
||||||
|
.addOutput({
|
||||||
|
script: Buffer.from(
|
||||||
|
'0014000102030405060708090a0b0c0d0e0f00010203',
|
||||||
|
'hex',
|
||||||
|
),
|
||||||
|
value: 2000,
|
||||||
|
bip32Derivation: [
|
||||||
|
{
|
||||||
|
masterFingerprint: root.fingerprint,
|
||||||
|
path,
|
||||||
|
pubkey: root.derivePath(path).publicKey,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.strictEqual(psbt.outputHasHDKey(0, root), true);
|
||||||
|
assert.strictEqual(psbt.outputHasHDKey(0, root2), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('outputHasPubkey', () => {
|
||||||
|
it('should throw', () => {
|
||||||
|
const psbt = new Psbt();
|
||||||
|
psbt
|
||||||
|
.addInput({
|
||||||
|
hash:
|
||||||
|
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||||
|
index: 0,
|
||||||
|
})
|
||||||
|
.addOutput({
|
||||||
|
script: payments.p2sh({
|
||||||
|
redeem: { output: Buffer.from([0x51]) },
|
||||||
|
}).output!,
|
||||||
|
value: 1337,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
psbt.outputHasPubkey(0, Buffer.from([]));
|
||||||
|
}, new RegExp('scriptPubkey is P2SH but redeemScript missing'));
|
||||||
|
|
||||||
|
(psbt as any).__CACHE.__TX.outs[0].script = payments.p2wsh({
|
||||||
|
redeem: { output: Buffer.from([0x51]) },
|
||||||
|
}).output!;
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
psbt.outputHasPubkey(0, Buffer.from([]));
|
||||||
|
}, new RegExp('scriptPubkey or redeemScript is P2WSH but witnessScript missing'));
|
||||||
|
|
||||||
|
(psbt as any).__CACHE.__TX.outs[0].script = payments.p2sh({
|
||||||
|
redeem: payments.p2wsh({
|
||||||
|
redeem: { output: Buffer.from([0x51]) },
|
||||||
|
}),
|
||||||
|
}).output!;
|
||||||
|
|
||||||
|
psbt.updateOutput(0, {
|
||||||
|
redeemScript: payments.p2wsh({
|
||||||
|
redeem: { output: Buffer.from([0x51]) },
|
||||||
|
}).output!,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
psbt.outputHasPubkey(0, Buffer.from([]));
|
||||||
|
}, new RegExp('scriptPubkey or redeemScript is P2WSH but witnessScript missing'));
|
||||||
|
|
||||||
|
delete psbt.data.outputs[0].redeemScript;
|
||||||
|
|
||||||
|
psbt.updateOutput(0, {
|
||||||
|
witnessScript: Buffer.from([0x51]),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
psbt.outputHasPubkey(0, Buffer.from([]));
|
||||||
|
}, new RegExp('scriptPubkey is P2SH but redeemScript missing'));
|
||||||
|
|
||||||
|
psbt.updateOutput(0, {
|
||||||
|
redeemScript: payments.p2wsh({
|
||||||
|
redeem: { output: Buffer.from([0x51]) },
|
||||||
|
}).output!,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.doesNotThrow(() => {
|
||||||
|
psbt.outputHasPubkey(0, Buffer.from([0x51]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('clone', () => {
|
describe('clone', () => {
|
||||||
it('Should clone a psbt exactly with no reference', () => {
|
it('Should clone a psbt exactly with no reference', () => {
|
||||||
const f = fixtures.clone;
|
const f = fixtures.clone;
|
||||||
|
@ -698,6 +990,8 @@ describe(`Psbt`, () => {
|
||||||
assert.throws(() => {
|
assert.throws(() => {
|
||||||
psbt.setVersion(3);
|
psbt.setVersion(3);
|
||||||
}, new RegExp('Can not modify transaction, signatures exist.'));
|
}, new RegExp('Can not modify transaction, signatures exist.'));
|
||||||
|
assert.strictEqual(psbt.inputHasPubkey(0, alice.publicKey), true);
|
||||||
|
assert.strictEqual(psbt.outputHasPubkey(0, alice.publicKey), false);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
psbt.extractTransaction().toHex(),
|
psbt.extractTransaction().toHex(),
|
||||||
'02000000013ebc8203037dda39d482bf41ff3be955996c50d9d4f7cfc3d2097a694a7' +
|
'02000000013ebc8203037dda39d482bf41ff3be955996c50d9d4f7cfc3d2097a694a7' +
|
||||||
|
@ -762,4 +1056,71 @@ describe(`Psbt`, () => {
|
||||||
assert.ok((psbt as any).data.inputs[index].nonWitnessUtxo.equals(value));
|
assert.ok((psbt as any).data.inputs[index].nonWitnessUtxo.equals(value));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Transaction properties', () => {
|
||||||
|
it('.version is exposed and is settable', () => {
|
||||||
|
const psbt = new Psbt();
|
||||||
|
|
||||||
|
assert.strictEqual(psbt.version, 2);
|
||||||
|
assert.strictEqual(psbt.version, (psbt as any).__CACHE.__TX.version);
|
||||||
|
|
||||||
|
psbt.version = 1;
|
||||||
|
assert.strictEqual(psbt.version, 1);
|
||||||
|
assert.strictEqual(psbt.version, (psbt as any).__CACHE.__TX.version);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.locktime is exposed and is settable', () => {
|
||||||
|
const psbt = new Psbt();
|
||||||
|
|
||||||
|
assert.strictEqual(psbt.locktime, 0);
|
||||||
|
assert.strictEqual(psbt.locktime, (psbt as any).__CACHE.__TX.locktime);
|
||||||
|
|
||||||
|
psbt.locktime = 123;
|
||||||
|
assert.strictEqual(psbt.locktime, 123);
|
||||||
|
assert.strictEqual(psbt.locktime, (psbt as any).__CACHE.__TX.locktime);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.txInputs is exposed as a readonly clone', () => {
|
||||||
|
const psbt = new Psbt();
|
||||||
|
const hash = Buffer.alloc(32);
|
||||||
|
const index = 0;
|
||||||
|
psbt.addInput({ hash, index });
|
||||||
|
|
||||||
|
const input = psbt.txInputs[0];
|
||||||
|
const internalInput = (psbt as any).__CACHE.__TX.ins[0];
|
||||||
|
|
||||||
|
assert.ok(input.hash.equals(internalInput.hash));
|
||||||
|
assert.strictEqual(input.index, internalInput.index);
|
||||||
|
assert.strictEqual(input.sequence, internalInput.sequence);
|
||||||
|
|
||||||
|
input.hash[0] = 123;
|
||||||
|
input.index = 123;
|
||||||
|
input.sequence = 123;
|
||||||
|
|
||||||
|
assert.ok(!input.hash.equals(internalInput.hash));
|
||||||
|
assert.notEqual(input.index, internalInput.index);
|
||||||
|
assert.notEqual(input.sequence, internalInput.sequence);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('.txOutputs is exposed as a readonly clone', () => {
|
||||||
|
const psbt = new Psbt();
|
||||||
|
const address = '1LukeQU5jwebXbMLDVydeH4vFSobRV9rkj';
|
||||||
|
const value = 100000;
|
||||||
|
psbt.addOutput({ address, value });
|
||||||
|
|
||||||
|
const output = psbt.txOutputs[0];
|
||||||
|
const internalInput = (psbt as any).__CACHE.__TX.outs[0];
|
||||||
|
|
||||||
|
assert.strictEqual(output.address, address);
|
||||||
|
|
||||||
|
assert.ok(output.script.equals(internalInput.script));
|
||||||
|
assert.strictEqual(output.value, internalInput.value);
|
||||||
|
|
||||||
|
output.script[0] = 123;
|
||||||
|
output.value = 123;
|
||||||
|
|
||||||
|
assert.ok(!output.script.equals(internalInput.script));
|
||||||
|
assert.notEqual(output.value, internalInput.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -49,9 +49,9 @@ export function reverseBuffer(buffer: Buffer): Buffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cloneBuffer(buffer: Buffer): Buffer {
|
export function cloneBuffer(buffer: Buffer): Buffer {
|
||||||
const clone = Buffer.alloc(buffer.length);
|
const clone = Buffer.allocUnsafe(buffer.length);
|
||||||
buffer.copy(clone);
|
buffer.copy(clone);
|
||||||
return buffer;
|
return clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -9,7 +9,7 @@ import * as script from './script';
|
||||||
export { ECPair, address, bip32, crypto, networks, payments, script };
|
export { ECPair, address, bip32, crypto, networks, payments, script };
|
||||||
|
|
||||||
export { Block } from './block';
|
export { Block } from './block';
|
||||||
export { Psbt } from './psbt';
|
export { Psbt, PsbtTxInput, PsbtTxOutput } from './psbt';
|
||||||
export { OPS as opcodes } from './script';
|
export { OPS as opcodes } from './script';
|
||||||
export { Transaction } from './transaction';
|
export { Transaction } from './transaction';
|
||||||
export { TransactionBuilder } from './transaction_builder';
|
export { TransactionBuilder } from './transaction_builder';
|
||||||
|
|
426
ts_src/psbt.ts
426
ts_src/psbt.ts
|
@ -1,6 +1,7 @@
|
||||||
import { Psbt as PsbtBase } from 'bip174';
|
import { Psbt as PsbtBase } from 'bip174';
|
||||||
import * as varuint from 'bip174/src/lib/converter/varint';
|
import * as varuint from 'bip174/src/lib/converter/varint';
|
||||||
import {
|
import {
|
||||||
|
Bip32Derivation,
|
||||||
KeyValue,
|
KeyValue,
|
||||||
PartialSig,
|
PartialSig,
|
||||||
PsbtGlobalUpdate,
|
PsbtGlobalUpdate,
|
||||||
|
@ -13,7 +14,7 @@ import {
|
||||||
TransactionInput,
|
TransactionInput,
|
||||||
TransactionOutput,
|
TransactionOutput,
|
||||||
} from 'bip174/src/lib/interfaces';
|
} from 'bip174/src/lib/interfaces';
|
||||||
import { checkForInput } from 'bip174/src/lib/utils';
|
import { checkForInput, checkForOutput } from 'bip174/src/lib/utils';
|
||||||
import { fromOutputScript, toOutputScript } from './address';
|
import { fromOutputScript, toOutputScript } from './address';
|
||||||
import { cloneBuffer, reverseBuffer } from './bufferutils';
|
import { cloneBuffer, reverseBuffer } from './bufferutils';
|
||||||
import { hash160 } from './crypto';
|
import { hash160 } from './crypto';
|
||||||
|
@ -27,6 +28,14 @@ import * as payments from './payments';
|
||||||
import * as bscript from './script';
|
import * as bscript from './script';
|
||||||
import { Output, Transaction } from './transaction';
|
import { Output, Transaction } from './transaction';
|
||||||
|
|
||||||
|
export interface PsbtTxInput extends TransactionInput {
|
||||||
|
hash: Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PsbtTxOutput extends TransactionOutput {
|
||||||
|
address: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These are the default arguments for a Psbt instance.
|
* These are the default arguments for a Psbt instance.
|
||||||
*/
|
*/
|
||||||
|
@ -108,6 +117,14 @@ export class Psbt {
|
||||||
__NON_WITNESS_UTXO_BUF_CACHE: [],
|
__NON_WITNESS_UTXO_BUF_CACHE: [],
|
||||||
__TX_IN_CACHE: {},
|
__TX_IN_CACHE: {},
|
||||||
__TX: (this.data.globalMap.unsignedTx as PsbtTransaction).tx,
|
__TX: (this.data.globalMap.unsignedTx as PsbtTransaction).tx,
|
||||||
|
// Old TransactionBuilder behavior was to not confirm input values
|
||||||
|
// before signing. Even though we highly encourage people to get
|
||||||
|
// the full parent transaction to verify values, the ability to
|
||||||
|
// sign non-segwit inputs without the full transaction was often
|
||||||
|
// requested. So the only way to activate is to use @ts-ignore.
|
||||||
|
// We will disable exporting the Psbt when unsafe sign is active.
|
||||||
|
// because it is not BIP174 compliant.
|
||||||
|
__UNSAFE_SIGN_NONSEGWIT: false,
|
||||||
};
|
};
|
||||||
if (this.data.inputs.length === 0) this.setVersion(2);
|
if (this.data.inputs.length === 0) this.setVersion(2);
|
||||||
|
|
||||||
|
@ -146,7 +163,7 @@ export class Psbt {
|
||||||
this.setLocktime(locktime);
|
this.setLocktime(locktime);
|
||||||
}
|
}
|
||||||
|
|
||||||
get txInputs(): TransactionInput[] {
|
get txInputs(): PsbtTxInput[] {
|
||||||
return this.__CACHE.__TX.ins.map(input => ({
|
return this.__CACHE.__TX.ins.map(input => ({
|
||||||
hash: cloneBuffer(input.hash),
|
hash: cloneBuffer(input.hash),
|
||||||
index: input.index,
|
index: input.index,
|
||||||
|
@ -154,7 +171,7 @@ export class Psbt {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
get txOutputs(): TransactionOutput[] {
|
get txOutputs(): PsbtTxOutput[] {
|
||||||
return this.__CACHE.__TX.outs.map(output => {
|
return this.__CACHE.__TX.outs.map(output => {
|
||||||
let address;
|
let address;
|
||||||
try {
|
try {
|
||||||
|
@ -233,6 +250,7 @@ export class Psbt {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
checkInputsForPartialSig(this.data.inputs, 'addInput');
|
checkInputsForPartialSig(this.data.inputs, 'addInput');
|
||||||
|
if (inputData.witnessScript) checkInvalidP2WSH(inputData.witnessScript);
|
||||||
const c = this.__CACHE;
|
const c = this.__CACHE;
|
||||||
this.data.addInput(inputData);
|
this.data.addInput(inputData);
|
||||||
const txIn = c.__TX.ins[c.__TX.ins.length - 1];
|
const txIn = c.__TX.ins[c.__TX.ins.length - 1];
|
||||||
|
@ -346,6 +364,48 @@ export class Psbt {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getInputType(inputIndex: number): AllScriptType {
|
||||||
|
const input = checkForInput(this.data.inputs, inputIndex);
|
||||||
|
const script = getScriptFromUtxo(inputIndex, input, this.__CACHE);
|
||||||
|
const result = getMeaningfulScript(
|
||||||
|
script,
|
||||||
|
inputIndex,
|
||||||
|
'input',
|
||||||
|
input.redeemScript || redeemFromFinalScriptSig(input.finalScriptSig),
|
||||||
|
input.witnessScript ||
|
||||||
|
redeemFromFinalWitnessScript(input.finalScriptWitness),
|
||||||
|
);
|
||||||
|
const type = result.type === 'raw' ? '' : result.type + '-';
|
||||||
|
const mainType = classifyScript(result.meaningfulScript);
|
||||||
|
return (type + mainType) as AllScriptType;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputHasPubkey(inputIndex: number, pubkey: Buffer): boolean {
|
||||||
|
const input = checkForInput(this.data.inputs, inputIndex);
|
||||||
|
return pubkeyInInput(pubkey, input, inputIndex, this.__CACHE);
|
||||||
|
}
|
||||||
|
|
||||||
|
inputHasHDKey(inputIndex: number, root: HDSigner): boolean {
|
||||||
|
const input = checkForInput(this.data.inputs, inputIndex);
|
||||||
|
const derivationIsMine = bip32DerivationIsMine(root);
|
||||||
|
return (
|
||||||
|
!!input.bip32Derivation && input.bip32Derivation.some(derivationIsMine)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
outputHasPubkey(outputIndex: number, pubkey: Buffer): boolean {
|
||||||
|
const output = checkForOutput(this.data.outputs, outputIndex);
|
||||||
|
return pubkeyInOutput(pubkey, output, outputIndex, this.__CACHE);
|
||||||
|
}
|
||||||
|
|
||||||
|
outputHasHDKey(outputIndex: number, root: HDSigner): boolean {
|
||||||
|
const output = checkForOutput(this.data.outputs, outputIndex);
|
||||||
|
const derivationIsMine = bip32DerivationIsMine(root);
|
||||||
|
return (
|
||||||
|
!!output.bip32Derivation && output.bip32Derivation.some(derivationIsMine)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
validateSignaturesOfAllInputs(): boolean {
|
validateSignaturesOfAllInputs(): boolean {
|
||||||
checkForInput(this.data.inputs, 0); // making sure we have at least one
|
checkForInput(this.data.inputs, 0); // making sure we have at least one
|
||||||
const results = range(this.data.inputs.length).map(idx =>
|
const results = range(this.data.inputs.length).map(idx =>
|
||||||
|
@ -375,6 +435,7 @@ export class Psbt {
|
||||||
inputIndex,
|
inputIndex,
|
||||||
Object.assign({}, input, { sighashType: sig.hashType }),
|
Object.assign({}, input, { sighashType: sig.hashType }),
|
||||||
this.__CACHE,
|
this.__CACHE,
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
: { hash: hashCache!, script: scriptCache! };
|
: { hash: hashCache!, script: scriptCache! };
|
||||||
sighashCache = sig.hashType;
|
sighashCache = sig.hashType;
|
||||||
|
@ -605,14 +666,17 @@ export class Psbt {
|
||||||
}
|
}
|
||||||
|
|
||||||
toBuffer(): Buffer {
|
toBuffer(): Buffer {
|
||||||
|
checkCache(this.__CACHE);
|
||||||
return this.data.toBuffer();
|
return this.data.toBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
toHex(): string {
|
toHex(): string {
|
||||||
|
checkCache(this.__CACHE);
|
||||||
return this.data.toHex();
|
return this.data.toHex();
|
||||||
}
|
}
|
||||||
|
|
||||||
toBase64(): string {
|
toBase64(): string {
|
||||||
|
checkCache(this.__CACHE);
|
||||||
return this.data.toBase64();
|
return this.data.toBase64();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -622,6 +686,7 @@ export class Psbt {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateInput(inputIndex: number, updateData: PsbtInputUpdate): this {
|
updateInput(inputIndex: number, updateData: PsbtInputUpdate): this {
|
||||||
|
if (updateData.witnessScript) checkInvalidP2WSH(updateData.witnessScript);
|
||||||
this.data.updateInput(inputIndex, updateData);
|
this.data.updateInput(inputIndex, updateData);
|
||||||
if (updateData.nonWitnessUtxo) {
|
if (updateData.nonWitnessUtxo) {
|
||||||
addNonWitnessTxCache(
|
addNonWitnessTxCache(
|
||||||
|
@ -667,6 +732,7 @@ interface PsbtCache {
|
||||||
__FEE_RATE?: number;
|
__FEE_RATE?: number;
|
||||||
__FEE?: number;
|
__FEE?: number;
|
||||||
__EXTRACTED_TX?: Transaction;
|
__EXTRACTED_TX?: Transaction;
|
||||||
|
__UNSAFE_SIGN_NONSEGWIT: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PsbtOptsOptional {
|
interface PsbtOptsOptional {
|
||||||
|
@ -811,6 +877,12 @@ function canFinalize(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkCache(cache: PsbtCache): void {
|
||||||
|
if (cache.__UNSAFE_SIGN_NONSEGWIT !== false) {
|
||||||
|
throw new Error('Not BIP174 compliant, can not export');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function hasSigs(
|
function hasSigs(
|
||||||
neededSigs: number,
|
neededSigs: number,
|
||||||
partialSig?: any[],
|
partialSig?: any[],
|
||||||
|
@ -852,6 +924,17 @@ const isP2PK = isPaymentFactory(payments.p2pk);
|
||||||
const isP2PKH = isPaymentFactory(payments.p2pkh);
|
const isP2PKH = isPaymentFactory(payments.p2pkh);
|
||||||
const isP2WPKH = isPaymentFactory(payments.p2wpkh);
|
const isP2WPKH = isPaymentFactory(payments.p2wpkh);
|
||||||
const isP2WSHScript = isPaymentFactory(payments.p2wsh);
|
const isP2WSHScript = isPaymentFactory(payments.p2wsh);
|
||||||
|
const isP2SHScript = isPaymentFactory(payments.p2sh);
|
||||||
|
|
||||||
|
function bip32DerivationIsMine(
|
||||||
|
root: HDSigner,
|
||||||
|
): (d: Bip32Derivation) => boolean {
|
||||||
|
return (d: Bip32Derivation): boolean => {
|
||||||
|
if (!d.masterFingerprint.equals(root.fingerprint)) return false;
|
||||||
|
if (!root.derivePath(d.path).publicKey.equals(d.pubkey)) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function check32Bit(num: number): void {
|
function check32Bit(num: number): void {
|
||||||
if (
|
if (
|
||||||
|
@ -930,17 +1013,7 @@ function checkScriptForPubkey(
|
||||||
script: Buffer,
|
script: Buffer,
|
||||||
action: string,
|
action: string,
|
||||||
): void {
|
): void {
|
||||||
const pubkeyHash = hash160(pubkey);
|
if (!pubkeyInScript(pubkey, script)) {
|
||||||
|
|
||||||
const decompiled = bscript.decompile(script);
|
|
||||||
if (decompiled === null) throw new Error('Unknown script error');
|
|
||||||
|
|
||||||
const hasKey = decompiled.some(element => {
|
|
||||||
if (typeof element === 'number') return false;
|
|
||||||
return element.equals(pubkey) || element.equals(pubkeyHash);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasKey) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Can not ${action} for this input with the key ${pubkey.toString('hex')}`,
|
`Can not ${action} for this input with the key ${pubkey.toString('hex')}`,
|
||||||
);
|
);
|
||||||
|
@ -979,11 +1052,12 @@ function checkTxInputCache(
|
||||||
function scriptCheckerFactory(
|
function scriptCheckerFactory(
|
||||||
payment: any,
|
payment: any,
|
||||||
paymentScriptName: string,
|
paymentScriptName: string,
|
||||||
): (idx: number, spk: Buffer, rs: Buffer) => void {
|
): (idx: number, spk: Buffer, rs: Buffer, ioType: 'input' | 'output') => void {
|
||||||
return (
|
return (
|
||||||
inputIndex: number,
|
inputIndex: number,
|
||||||
scriptPubKey: Buffer,
|
scriptPubKey: Buffer,
|
||||||
redeemScript: Buffer,
|
redeemScript: Buffer,
|
||||||
|
ioType: 'input' | 'output',
|
||||||
): void => {
|
): void => {
|
||||||
const redeemScriptOutput = payment({
|
const redeemScriptOutput = payment({
|
||||||
redeem: { output: redeemScript },
|
redeem: { output: redeemScript },
|
||||||
|
@ -991,7 +1065,7 @@ function scriptCheckerFactory(
|
||||||
|
|
||||||
if (!scriptPubKey.equals(redeemScriptOutput)) {
|
if (!scriptPubKey.equals(redeemScriptOutput)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${paymentScriptName} for input #${inputIndex} doesn't match the scriptPubKey in the prevout`,
|
`${paymentScriptName} for ${ioType} #${inputIndex} doesn't match the scriptPubKey in the prevout`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1124,6 +1198,7 @@ function getHashAndSighashType(
|
||||||
inputIndex,
|
inputIndex,
|
||||||
input,
|
input,
|
||||||
cache,
|
cache,
|
||||||
|
false,
|
||||||
sighashTypes,
|
sighashTypes,
|
||||||
);
|
);
|
||||||
checkScriptForPubkey(pubkey, script, 'sign');
|
checkScriptForPubkey(pubkey, script, 'sign');
|
||||||
|
@ -1137,6 +1212,7 @@ function getHashForSig(
|
||||||
inputIndex: number,
|
inputIndex: number,
|
||||||
input: PsbtInput,
|
input: PsbtInput,
|
||||||
cache: PsbtCache,
|
cache: PsbtCache,
|
||||||
|
forValidate: boolean,
|
||||||
sighashTypes?: number[],
|
sighashTypes?: number[],
|
||||||
): {
|
): {
|
||||||
script: Buffer;
|
script: Buffer;
|
||||||
|
@ -1153,7 +1229,7 @@ function getHashForSig(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let hash: Buffer;
|
let hash: Buffer;
|
||||||
let script: Buffer;
|
let prevout: Output;
|
||||||
|
|
||||||
if (input.nonWitnessUtxo) {
|
if (input.nonWitnessUtxo) {
|
||||||
const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache(
|
const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache(
|
||||||
|
@ -1173,85 +1249,67 @@ function getHashForSig(
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevoutIndex = unsignedTx.ins[inputIndex].index;
|
const prevoutIndex = unsignedTx.ins[inputIndex].index;
|
||||||
const prevout = nonWitnessUtxoTx.outs[prevoutIndex] as Output;
|
prevout = nonWitnessUtxoTx.outs[prevoutIndex] as Output;
|
||||||
|
|
||||||
if (input.redeemScript) {
|
|
||||||
// If a redeemScript is provided, the scriptPubKey must be for that redeemScript
|
|
||||||
checkRedeemScript(inputIndex, prevout.script, input.redeemScript);
|
|
||||||
script = input.redeemScript;
|
|
||||||
} else {
|
|
||||||
script = prevout.script;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isP2WSHScript(script)) {
|
|
||||||
if (!input.witnessScript)
|
|
||||||
throw new Error('Segwit input needs witnessScript if not P2WPKH');
|
|
||||||
checkWitnessScript(inputIndex, script, input.witnessScript);
|
|
||||||
hash = unsignedTx.hashForWitnessV0(
|
|
||||||
inputIndex,
|
|
||||||
input.witnessScript,
|
|
||||||
prevout.value,
|
|
||||||
sighashType,
|
|
||||||
);
|
|
||||||
script = input.witnessScript;
|
|
||||||
} else if (isP2WPKH(script)) {
|
|
||||||
// P2WPKH uses the P2PKH template for prevoutScript when signing
|
|
||||||
const signingScript = payments.p2pkh({ hash: script.slice(2) }).output!;
|
|
||||||
hash = unsignedTx.hashForWitnessV0(
|
|
||||||
inputIndex,
|
|
||||||
signingScript,
|
|
||||||
prevout.value,
|
|
||||||
sighashType,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
hash = unsignedTx.hashForSignature(inputIndex, script, sighashType);
|
|
||||||
}
|
|
||||||
} else if (input.witnessUtxo) {
|
} else if (input.witnessUtxo) {
|
||||||
let _script: Buffer; // so we don't shadow the `let script` above
|
prevout = input.witnessUtxo;
|
||||||
if (input.redeemScript) {
|
|
||||||
// If a redeemScript is provided, the scriptPubKey must be for that redeemScript
|
|
||||||
checkRedeemScript(
|
|
||||||
inputIndex,
|
|
||||||
input.witnessUtxo.script,
|
|
||||||
input.redeemScript,
|
|
||||||
);
|
|
||||||
_script = input.redeemScript;
|
|
||||||
} else {
|
|
||||||
_script = input.witnessUtxo.script;
|
|
||||||
}
|
|
||||||
if (isP2WPKH(_script)) {
|
|
||||||
// P2WPKH uses the P2PKH template for prevoutScript when signing
|
|
||||||
const signingScript = payments.p2pkh({ hash: _script.slice(2) }).output!;
|
|
||||||
hash = unsignedTx.hashForWitnessV0(
|
|
||||||
inputIndex,
|
|
||||||
signingScript,
|
|
||||||
input.witnessUtxo.value,
|
|
||||||
sighashType,
|
|
||||||
);
|
|
||||||
script = _script;
|
|
||||||
} else if (isP2WSHScript(_script)) {
|
|
||||||
if (!input.witnessScript)
|
|
||||||
throw new Error('Segwit input needs witnessScript if not P2WPKH');
|
|
||||||
checkWitnessScript(inputIndex, _script, input.witnessScript);
|
|
||||||
hash = unsignedTx.hashForWitnessV0(
|
|
||||||
inputIndex,
|
|
||||||
input.witnessScript,
|
|
||||||
input.witnessUtxo.value,
|
|
||||||
sighashType,
|
|
||||||
);
|
|
||||||
// want to make sure the script we return is the actual meaningful script
|
|
||||||
script = input.witnessScript;
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`Input #${inputIndex} has witnessUtxo but non-segwit script: ` +
|
|
||||||
`${_script.toString('hex')}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Need a Utxo input item for signing');
|
throw new Error('Need a Utxo input item for signing');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { meaningfulScript, type } = getMeaningfulScript(
|
||||||
|
prevout.script,
|
||||||
|
inputIndex,
|
||||||
|
'input',
|
||||||
|
input.redeemScript,
|
||||||
|
input.witnessScript,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (['p2sh-p2wsh', 'p2wsh'].indexOf(type) >= 0) {
|
||||||
|
hash = unsignedTx.hashForWitnessV0(
|
||||||
|
inputIndex,
|
||||||
|
meaningfulScript,
|
||||||
|
prevout.value,
|
||||||
|
sighashType,
|
||||||
|
);
|
||||||
|
} else if (isP2WPKH(meaningfulScript)) {
|
||||||
|
// P2WPKH uses the P2PKH template for prevoutScript when signing
|
||||||
|
const signingScript = payments.p2pkh({ hash: meaningfulScript.slice(2) })
|
||||||
|
.output!;
|
||||||
|
hash = unsignedTx.hashForWitnessV0(
|
||||||
|
inputIndex,
|
||||||
|
signingScript,
|
||||||
|
prevout.value,
|
||||||
|
sighashType,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// non-segwit
|
||||||
|
if (
|
||||||
|
input.nonWitnessUtxo === undefined &&
|
||||||
|
cache.__UNSAFE_SIGN_NONSEGWIT === false
|
||||||
|
)
|
||||||
|
throw new Error(
|
||||||
|
`Input #${inputIndex} has witnessUtxo but non-segwit script: ` +
|
||||||
|
`${meaningfulScript.toString('hex')}`,
|
||||||
|
);
|
||||||
|
if (!forValidate && cache.__UNSAFE_SIGN_NONSEGWIT !== false)
|
||||||
|
console.warn(
|
||||||
|
'Warning: Signing non-segwit inputs without the full parent transaction ' +
|
||||||
|
'means there is a chance that a miner could feed you incorrect information ' +
|
||||||
|
'to trick you into paying large fees. This behavior is the same as the old ' +
|
||||||
|
'TransactionBuilder class when signing non-segwit scripts. You are not ' +
|
||||||
|
'able to export this Psbt with toBuffer|toBase64|toHex since it is not ' +
|
||||||
|
'BIP174 compliant.\n*********************\nPROCEED WITH CAUTION!\n' +
|
||||||
|
'*********************',
|
||||||
|
);
|
||||||
|
hash = unsignedTx.hashForSignature(
|
||||||
|
inputIndex,
|
||||||
|
meaningfulScript,
|
||||||
|
sighashType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
script,
|
script: meaningfulScript,
|
||||||
sighashType,
|
sighashType,
|
||||||
hash,
|
hash,
|
||||||
};
|
};
|
||||||
|
@ -1563,7 +1621,191 @@ function nonWitnessUtxoTxFromCache(
|
||||||
return c[inputIndex];
|
return c[inputIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
function classifyScript(script: Buffer): string {
|
function getScriptFromUtxo(
|
||||||
|
inputIndex: number,
|
||||||
|
input: PsbtInput,
|
||||||
|
cache: PsbtCache,
|
||||||
|
): Buffer {
|
||||||
|
if (input.witnessUtxo !== undefined) {
|
||||||
|
return input.witnessUtxo.script;
|
||||||
|
} else if (input.nonWitnessUtxo !== undefined) {
|
||||||
|
const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache(
|
||||||
|
cache,
|
||||||
|
input,
|
||||||
|
inputIndex,
|
||||||
|
);
|
||||||
|
return nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index].script;
|
||||||
|
} else {
|
||||||
|
throw new Error("Can't find pubkey in input without Utxo data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pubkeyInInput(
|
||||||
|
pubkey: Buffer,
|
||||||
|
input: PsbtInput,
|
||||||
|
inputIndex: number,
|
||||||
|
cache: PsbtCache,
|
||||||
|
): boolean {
|
||||||
|
const script = getScriptFromUtxo(inputIndex, input, cache);
|
||||||
|
const { meaningfulScript } = getMeaningfulScript(
|
||||||
|
script,
|
||||||
|
inputIndex,
|
||||||
|
'input',
|
||||||
|
input.redeemScript,
|
||||||
|
input.witnessScript,
|
||||||
|
);
|
||||||
|
return pubkeyInScript(pubkey, meaningfulScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pubkeyInOutput(
|
||||||
|
pubkey: Buffer,
|
||||||
|
output: PsbtOutput,
|
||||||
|
outputIndex: number,
|
||||||
|
cache: PsbtCache,
|
||||||
|
): boolean {
|
||||||
|
const script = cache.__TX.outs[outputIndex].script;
|
||||||
|
const { meaningfulScript } = getMeaningfulScript(
|
||||||
|
script,
|
||||||
|
outputIndex,
|
||||||
|
'output',
|
||||||
|
output.redeemScript,
|
||||||
|
output.witnessScript,
|
||||||
|
);
|
||||||
|
return pubkeyInScript(pubkey, meaningfulScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
function redeemFromFinalScriptSig(
|
||||||
|
finalScript: Buffer | undefined,
|
||||||
|
): Buffer | undefined {
|
||||||
|
if (!finalScript) return;
|
||||||
|
const decomp = bscript.decompile(finalScript);
|
||||||
|
if (!decomp) return;
|
||||||
|
const lastItem = decomp[decomp.length - 1];
|
||||||
|
if (
|
||||||
|
!Buffer.isBuffer(lastItem) ||
|
||||||
|
isPubkeyLike(lastItem) ||
|
||||||
|
isSigLike(lastItem)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
const sDecomp = bscript.decompile(lastItem);
|
||||||
|
if (!sDecomp) return;
|
||||||
|
return lastItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function redeemFromFinalWitnessScript(
|
||||||
|
finalScript: Buffer | undefined,
|
||||||
|
): Buffer | undefined {
|
||||||
|
if (!finalScript) return;
|
||||||
|
const decomp = scriptWitnessToWitnessStack(finalScript);
|
||||||
|
const lastItem = decomp[decomp.length - 1];
|
||||||
|
if (isPubkeyLike(lastItem)) return;
|
||||||
|
const sDecomp = bscript.decompile(lastItem);
|
||||||
|
if (!sDecomp) return;
|
||||||
|
return lastItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPubkeyLike(buf: Buffer): boolean {
|
||||||
|
return buf.length === 33 && bscript.isCanonicalPubKey(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSigLike(buf: Buffer): boolean {
|
||||||
|
return bscript.isCanonicalScriptSignature(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMeaningfulScript(
|
||||||
|
script: Buffer,
|
||||||
|
index: number,
|
||||||
|
ioType: 'input' | 'output',
|
||||||
|
redeemScript?: Buffer,
|
||||||
|
witnessScript?: Buffer,
|
||||||
|
): {
|
||||||
|
meaningfulScript: Buffer;
|
||||||
|
type: 'p2sh' | 'p2wsh' | 'p2sh-p2wsh' | 'raw';
|
||||||
|
} {
|
||||||
|
const isP2SH = isP2SHScript(script);
|
||||||
|
const isP2SHP2WSH = isP2SH && redeemScript && isP2WSHScript(redeemScript);
|
||||||
|
const isP2WSH = isP2WSHScript(script);
|
||||||
|
|
||||||
|
if (isP2SH && redeemScript === undefined)
|
||||||
|
throw new Error('scriptPubkey is P2SH but redeemScript missing');
|
||||||
|
if ((isP2WSH || isP2SHP2WSH) && witnessScript === undefined)
|
||||||
|
throw new Error(
|
||||||
|
'scriptPubkey or redeemScript is P2WSH but witnessScript missing',
|
||||||
|
);
|
||||||
|
|
||||||
|
let meaningfulScript: Buffer;
|
||||||
|
|
||||||
|
if (isP2SHP2WSH) {
|
||||||
|
meaningfulScript = witnessScript!;
|
||||||
|
checkRedeemScript(index, script, redeemScript!, ioType);
|
||||||
|
checkWitnessScript(index, redeemScript!, witnessScript!, ioType);
|
||||||
|
checkInvalidP2WSH(meaningfulScript);
|
||||||
|
} else if (isP2WSH) {
|
||||||
|
meaningfulScript = witnessScript!;
|
||||||
|
checkWitnessScript(index, script, witnessScript!, ioType);
|
||||||
|
checkInvalidP2WSH(meaningfulScript);
|
||||||
|
} else if (isP2SH) {
|
||||||
|
meaningfulScript = redeemScript!;
|
||||||
|
checkRedeemScript(index, script, redeemScript!, ioType);
|
||||||
|
} else {
|
||||||
|
meaningfulScript = script;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
meaningfulScript,
|
||||||
|
type: isP2SHP2WSH
|
||||||
|
? 'p2sh-p2wsh'
|
||||||
|
: isP2SH
|
||||||
|
? 'p2sh'
|
||||||
|
: isP2WSH
|
||||||
|
? 'p2wsh'
|
||||||
|
: 'raw',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkInvalidP2WSH(script: Buffer): void {
|
||||||
|
if (isP2WPKH(script) || isP2SHScript(script)) {
|
||||||
|
throw new Error('P2WPKH or P2SH can not be contained within P2WSH');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pubkeyInScript(pubkey: Buffer, script: Buffer): boolean {
|
||||||
|
const pubkeyHash = hash160(pubkey);
|
||||||
|
|
||||||
|
const decompiled = bscript.decompile(script);
|
||||||
|
if (decompiled === null) throw new Error('Unknown script error');
|
||||||
|
|
||||||
|
return decompiled.some(element => {
|
||||||
|
if (typeof element === 'number') return false;
|
||||||
|
return element.equals(pubkey) || element.equals(pubkeyHash);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type AllScriptType =
|
||||||
|
| 'witnesspubkeyhash'
|
||||||
|
| 'pubkeyhash'
|
||||||
|
| 'multisig'
|
||||||
|
| 'pubkey'
|
||||||
|
| 'nonstandard'
|
||||||
|
| 'p2sh-witnesspubkeyhash'
|
||||||
|
| 'p2sh-pubkeyhash'
|
||||||
|
| 'p2sh-multisig'
|
||||||
|
| 'p2sh-pubkey'
|
||||||
|
| 'p2sh-nonstandard'
|
||||||
|
| 'p2wsh-pubkeyhash'
|
||||||
|
| 'p2wsh-multisig'
|
||||||
|
| 'p2wsh-pubkey'
|
||||||
|
| 'p2wsh-nonstandard'
|
||||||
|
| 'p2sh-p2wsh-pubkeyhash'
|
||||||
|
| 'p2sh-p2wsh-multisig'
|
||||||
|
| 'p2sh-p2wsh-pubkey'
|
||||||
|
| 'p2sh-p2wsh-nonstandard';
|
||||||
|
type ScriptType =
|
||||||
|
| 'witnesspubkeyhash'
|
||||||
|
| 'pubkeyhash'
|
||||||
|
| 'multisig'
|
||||||
|
| 'pubkey'
|
||||||
|
| 'nonstandard';
|
||||||
|
function classifyScript(script: Buffer): ScriptType {
|
||||||
if (isP2WPKH(script)) return 'witnesspubkeyhash';
|
if (isP2WPKH(script)) return 'witnesspubkeyhash';
|
||||||
if (isP2PKH(script)) return 'pubkeyhash';
|
if (isP2PKH(script)) return 'pubkeyhash';
|
||||||
if (isP2MS(script)) return 'multisig';
|
if (isP2MS(script)) return 'multisig';
|
||||||
|
|
2
types/index.d.ts
vendored
2
types/index.d.ts
vendored
|
@ -7,7 +7,7 @@ import * as payments from './payments';
|
||||||
import * as script from './script';
|
import * as script from './script';
|
||||||
export { ECPair, address, bip32, crypto, networks, payments, script };
|
export { ECPair, address, bip32, crypto, networks, payments, script };
|
||||||
export { Block } from './block';
|
export { Block } from './block';
|
||||||
export { Psbt } from './psbt';
|
export { Psbt, PsbtTxInput, PsbtTxOutput } from './psbt';
|
||||||
export { OPS as opcodes } from './script';
|
export { OPS as opcodes } from './script';
|
||||||
export { Transaction } from './transaction';
|
export { Transaction } from './transaction';
|
||||||
export { TransactionBuilder } from './transaction_builder';
|
export { TransactionBuilder } from './transaction_builder';
|
||||||
|
|
16
types/psbt.d.ts
vendored
16
types/psbt.d.ts
vendored
|
@ -3,6 +3,12 @@ import { KeyValue, PsbtGlobalUpdate, PsbtInput, PsbtInputUpdate, PsbtOutput, Psb
|
||||||
import { Signer, SignerAsync } from './ecpair';
|
import { Signer, SignerAsync } from './ecpair';
|
||||||
import { Network } from './networks';
|
import { Network } from './networks';
|
||||||
import { Transaction } from './transaction';
|
import { Transaction } from './transaction';
|
||||||
|
export interface PsbtTxInput extends TransactionInput {
|
||||||
|
hash: Buffer;
|
||||||
|
}
|
||||||
|
export interface PsbtTxOutput extends TransactionOutput {
|
||||||
|
address: string | undefined;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Psbt class can parse and generate a PSBT binary based off of the BIP174.
|
* Psbt class can parse and generate a PSBT binary based off of the BIP174.
|
||||||
* There are 6 roles that this class fulfills. (Explained in BIP174)
|
* There are 6 roles that this class fulfills. (Explained in BIP174)
|
||||||
|
@ -46,8 +52,8 @@ export declare class Psbt {
|
||||||
readonly inputCount: number;
|
readonly inputCount: number;
|
||||||
version: number;
|
version: number;
|
||||||
locktime: number;
|
locktime: number;
|
||||||
readonly txInputs: TransactionInput[];
|
readonly txInputs: PsbtTxInput[];
|
||||||
readonly txOutputs: TransactionOutput[];
|
readonly txOutputs: PsbtTxOutput[];
|
||||||
combine(...those: Psbt[]): this;
|
combine(...those: Psbt[]): this;
|
||||||
clone(): Psbt;
|
clone(): Psbt;
|
||||||
setMaximumFeeRate(satoshiPerByte: number): void;
|
setMaximumFeeRate(satoshiPerByte: number): void;
|
||||||
|
@ -63,6 +69,11 @@ export declare class Psbt {
|
||||||
getFee(): number;
|
getFee(): number;
|
||||||
finalizeAllInputs(): this;
|
finalizeAllInputs(): this;
|
||||||
finalizeInput(inputIndex: number, finalScriptsFunc?: FinalScriptsFunc): this;
|
finalizeInput(inputIndex: number, finalScriptsFunc?: FinalScriptsFunc): this;
|
||||||
|
getInputType(inputIndex: number): AllScriptType;
|
||||||
|
inputHasPubkey(inputIndex: number, pubkey: Buffer): boolean;
|
||||||
|
inputHasHDKey(inputIndex: number, root: HDSigner): boolean;
|
||||||
|
outputHasPubkey(outputIndex: number, pubkey: Buffer): boolean;
|
||||||
|
outputHasHDKey(outputIndex: number, root: HDSigner): boolean;
|
||||||
validateSignaturesOfAllInputs(): boolean;
|
validateSignaturesOfAllInputs(): boolean;
|
||||||
validateSignaturesOfInput(inputIndex: number, pubkey?: Buffer): boolean;
|
validateSignaturesOfInput(inputIndex: number, pubkey?: Buffer): boolean;
|
||||||
signAllInputsHD(hdKeyPair: HDSigner, sighashTypes?: number[]): this;
|
signAllInputsHD(hdKeyPair: HDSigner, sighashTypes?: number[]): this;
|
||||||
|
@ -143,4 +154,5 @@ isP2WSH: boolean) => {
|
||||||
finalScriptSig: Buffer | undefined;
|
finalScriptSig: Buffer | undefined;
|
||||||
finalScriptWitness: Buffer | undefined;
|
finalScriptWitness: Buffer | undefined;
|
||||||
};
|
};
|
||||||
|
declare type AllScriptType = 'witnesspubkeyhash' | 'pubkeyhash' | 'multisig' | 'pubkey' | 'nonstandard' | 'p2sh-witnesspubkeyhash' | 'p2sh-pubkeyhash' | 'p2sh-multisig' | 'p2sh-pubkey' | 'p2sh-nonstandard' | 'p2wsh-pubkeyhash' | 'p2wsh-multisig' | 'p2wsh-pubkey' | 'p2wsh-nonstandard' | 'p2sh-p2wsh-pubkeyhash' | 'p2sh-p2wsh-multisig' | 'p2sh-p2wsh-pubkey' | 'p2sh-p2wsh-nonstandard';
|
||||||
export {};
|
export {};
|
||||||
|
|
Loading…
Reference in a new issue