From c9f399e509c5375580e9c196e3c2c5eeb59aa053 Mon Sep 17 00:00:00 2001
From: junderw <junderwood@bitcoinbank.co.jp>
Date: Wed, 29 Apr 2020 11:05:33 +0900
Subject: [PATCH] Add getInputType

---
 src/psbt.js             | 37 ++++++++++++++---
 test/fixtures/psbt.json | 18 +++++++++
 test/psbt.spec.ts       | 89 +++++++++++++++++++++++++++++++++++++++++
 ts_src/psbt.ts          | 79 +++++++++++++++++++++++++++++++-----
 types/psbt.d.ts         |  2 +
 5 files changed, 208 insertions(+), 17 deletions(-)

diff --git a/src/psbt.js b/src/psbt.js
index 5958b1e..7cab795 100644
--- a/src/psbt.js
+++ b/src/psbt.js
@@ -189,6 +189,7 @@ class Psbt {
       );
     }
     checkInputsForPartialSig(this.data.inputs, 'addInput');
+    if (inputData.witnessScript) checkInvalidP2WSH(inputData.witnessScript);
     const c = this.__CACHE;
     this.data.addInput(inputData);
     const txIn = c.__TX.ins[c.__TX.ins.length - 1];
@@ -285,6 +286,20 @@ class Psbt {
     this.data.clearFinalizedInput(inputIndex);
     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,
+      input.witnessScript,
+    );
+    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);
@@ -538,6 +553,7 @@ class Psbt {
     return this;
   }
   updateInput(inputIndex, updateData) {
+    if (updateData.witnessScript) checkInvalidP2WSH(updateData.witnessScript);
     this.data.updateInput(inputIndex, updateData);
     if (updateData.nonWitnessUtxo) {
       addNonWitnessTxCache(
@@ -924,7 +940,7 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) {
     input.redeemScript,
     input.witnessScript,
   );
-  if (['p2shp2wsh', 'p2wsh'].indexOf(type) >= 0) {
+  if (['p2sh-p2wsh', 'p2wsh'].indexOf(type) >= 0) {
     hash = unsignedTx.hashForWitnessV0(
       inputIndex,
       meaningfulScript,
@@ -1220,20 +1236,22 @@ function nonWitnessUtxoTxFromCache(cache, input, inputIndex) {
   }
   return c[inputIndex];
 }
-function pubkeyInInput(pubkey, input, inputIndex, cache) {
-  let script;
+function getScriptFromUtxo(inputIndex, input, cache) {
   if (input.witnessUtxo !== undefined) {
-    script = input.witnessUtxo.script;
+    return input.witnessUtxo.script;
   } else if (input.nonWitnessUtxo !== undefined) {
     const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache(
       cache,
       input,
       inputIndex,
     );
-    script = nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index].script;
+    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,
@@ -1275,9 +1293,11 @@ function getMeaningfulScript(
     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);
@@ -1287,7 +1307,7 @@ function getMeaningfulScript(
   return {
     meaningfulScript,
     type: isP2SHP2WSH
-      ? 'p2shp2wsh'
+      ? 'p2sh-p2wsh'
       : isP2SH
       ? 'p2sh'
       : isP2WSH
@@ -1295,6 +1315,11 @@ function getMeaningfulScript(
       : '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);
diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json
index e3062e8..0e51d57 100644
--- a/test/fixtures/psbt.json
+++ b/test/fixtures/psbt.json
@@ -313,6 +313,24 @@
         },
         "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",
         "inputData": {
diff --git a/test/psbt.spec.ts b/test/psbt.spec.ts
index ff2131b..5e88fe0 100644
--- a/test/psbt.spec.ts
+++ b/test/psbt.spec.ts
@@ -542,6 +542,95 @@ describe(`Psbt`, () => {
     });
   });
 
+  describe('getInputType', () => {
+    const { publicKey } = ECPair.makeRandom();
+    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,
+    }: 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) } : {}),
+      });
+      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',
+      },
+      {
+        innerScript: p2pkhPub,
+        outerScript: p2wshOut,
+        redeemGetter: null,
+        witnessGetter: p2pkhPub,
+        expectedType: 'p2wsh-pubkeyhash',
+      },
+      {
+        innerScript: p2pkhPub,
+        outerScript: p2shp2wshOut,
+        redeemGetter: (pk: Buffer): Buffer => p2wshOut(p2pkhPub(pk)),
+        witnessGetter: p2pkhPub,
+        expectedType: 'p2sh-p2wsh-pubkeyhash',
+      },
+    ].forEach(getInputTypeTest);
+  });
+
   describe('inputHasPubkey', () => {
     it('should throw', () => {
       const psbt = new Psbt();
diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts
index 23bdc1d..cb14fc5 100644
--- a/ts_src/psbt.ts
+++ b/ts_src/psbt.ts
@@ -242,6 +242,7 @@ export class Psbt {
       );
     }
     checkInputsForPartialSig(this.data.inputs, 'addInput');
+    if (inputData.witnessScript) checkInvalidP2WSH(inputData.witnessScript);
     const c = this.__CACHE;
     this.data.addInput(inputData);
     const txIn = c.__TX.ins[c.__TX.ins.length - 1];
@@ -355,6 +356,21 @@ export class Psbt {
     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,
+      input.witnessScript,
+    );
+    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);
@@ -648,6 +664,7 @@ export class Psbt {
   }
 
   updateInput(inputIndex: number, updateData: PsbtInputUpdate): this {
+    if (updateData.witnessScript) checkInvalidP2WSH(updateData.witnessScript);
     this.data.updateInput(inputIndex, updateData);
     if (updateData.nonWitnessUtxo) {
       addNonWitnessTxCache(
@@ -1215,7 +1232,7 @@ function getHashForSig(
     input.witnessScript,
   );
 
-  if (['p2shp2wsh', 'p2wsh'].indexOf(type) >= 0) {
+  if (['p2sh-p2wsh', 'p2wsh'].indexOf(type) >= 0) {
     hash = unsignedTx.hashForWitnessV0(
       inputIndex,
       meaningfulScript,
@@ -1572,25 +1589,32 @@ function nonWitnessUtxoTxFromCache(
   return c[inputIndex];
 }
 
-function pubkeyInInput(
-  pubkey: Buffer,
-  input: PsbtInput,
+function getScriptFromUtxo(
   inputIndex: number,
+  input: PsbtInput,
   cache: PsbtCache,
-): boolean {
-  let script: Buffer;
+): Buffer {
   if (input.witnessUtxo !== undefined) {
-    script = input.witnessUtxo.script;
+    return input.witnessUtxo.script;
   } else if (input.nonWitnessUtxo !== undefined) {
     const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache(
       cache,
       input,
       inputIndex,
     );
-    script = nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index].script;
+    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,
@@ -1626,7 +1650,7 @@ function getMeaningfulScript(
   witnessScript?: Buffer,
 ): {
   meaningfulScript: Buffer;
-  type: 'p2sh' | 'p2wsh' | 'p2shp2wsh' | 'raw';
+  type: 'p2sh' | 'p2wsh' | 'p2sh-p2wsh' | 'raw';
 } {
   const isP2SH = isP2SHScript(script);
   const isP2SHP2WSH = isP2SH && redeemScript && isP2WSHScript(redeemScript);
@@ -1645,9 +1669,11 @@ function getMeaningfulScript(
     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);
@@ -1657,7 +1683,7 @@ function getMeaningfulScript(
   return {
     meaningfulScript,
     type: isP2SHP2WSH
-      ? 'p2shp2wsh'
+      ? 'p2sh-p2wsh'
       : isP2SH
       ? 'p2sh'
       : isP2WSH
@@ -1666,6 +1692,12 @@ function getMeaningfulScript(
   };
 }
 
+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);
 
@@ -1678,7 +1710,32 @@ function pubkeyInScript(pubkey: Buffer, script: Buffer): boolean {
   });
 }
 
-function classifyScript(script: Buffer): string {
+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 (isP2PKH(script)) return 'pubkeyhash';
   if (isP2MS(script)) return 'multisig';
diff --git a/types/psbt.d.ts b/types/psbt.d.ts
index 127ef0f..4d1c099 100644
--- a/types/psbt.d.ts
+++ b/types/psbt.d.ts
@@ -69,6 +69,7 @@ export declare class Psbt {
     getFee(): number;
     finalizeAllInputs(): this;
     finalizeInput(inputIndex: number, finalScriptsFunc?: FinalScriptsFunc): this;
+    getInputType(inputIndex: number): AllScriptType;
     inputHasPubkey(inputIndex: number, pubkey: Buffer): boolean;
     outputHasPubkey(outputIndex: number, pubkey: Buffer): boolean;
     validateSignaturesOfAllInputs(): boolean;
@@ -151,4 +152,5 @@ isP2WSH: boolean) => {
     finalScriptSig: 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 {};