From 528b4e01ee597103ea844c8429300b044a49d73d Mon Sep 17 00:00:00 2001
From: Nadav Kohen <nadavk25@gmail.com>
Date: Wed, 23 Dec 2020 18:13:57 -0600
Subject: [PATCH] Outstanding DLC branch diff (#2432)

---
 .../scala/org/bitcoins/gui/GlobalData.scala   | 23 +++++-
 .../org/bitcoins/gui/dlc/DLCPaneModel.scala   | 38 ++++++++--
 .../org/bitcoins/core/psbt/PSBTMap.scala      |  2 +-
 .../org/bitcoins/testkit/util/BytesUtil.scala | 71 +++++++++++++++++++
 .../wallet/models/TransactionDAO.scala        |  5 ++
 5 files changed, 131 insertions(+), 8 deletions(-)
 create mode 100644 testkit/src/main/scala/org/bitcoins/testkit/util/BytesUtil.scala

diff --git a/app/gui/src/main/scala/org/bitcoins/gui/GlobalData.scala b/app/gui/src/main/scala/org/bitcoins/gui/GlobalData.scala
index 10de687eed..57830dc058 100644
--- a/app/gui/src/main/scala/org/bitcoins/gui/GlobalData.scala
+++ b/app/gui/src/main/scala/org/bitcoins/gui/GlobalData.scala
@@ -1,7 +1,8 @@
 package org.bitcoins.gui
 
 import org.bitcoins.cli.Config
-import org.bitcoins.core.config.BitcoinNetwork
+import org.bitcoins.core.config._
+import org.bitcoins.crypto.DoubleSha256DigestBE
 import org.bitcoins.gui.settings.Themes
 import scalafx.beans.property.{DoubleProperty, StringProperty}
 
@@ -34,4 +35,24 @@ object GlobalData {
       case Some(rpcPort) =>
         Config(debug = debug, rpcPort = rpcPort)
     }
+
+  lazy val broadcastUrl: String = GlobalData.network match {
+    case MainNet =>
+      "https://blockstream.info/api/tx"
+    case TestNet3 =>
+      "https://blockstream.info/testnet/api/tx"
+    case net @ (RegTest | SigNet) => s"Broadcast from your own node on $net"
+  }
+
+  /** Builds a url for the blockstream explorer to view the tx */
+  def buildTxUrl(txid: DoubleSha256DigestBE): String = {
+    network match {
+      case MainNet =>
+        s"https://blockstream.info/tx/${txid.hex}"
+      case TestNet3 =>
+        s"https://blockstream.info/testnet/tx/${txid.hex}"
+      case net @ (RegTest | SigNet) =>
+        s"View transaction on your own node on $net"
+    }
+  }
 }
diff --git a/app/gui/src/main/scala/org/bitcoins/gui/dlc/DLCPaneModel.scala b/app/gui/src/main/scala/org/bitcoins/gui/dlc/DLCPaneModel.scala
index 6193e40799..d1abb66fa6 100644
--- a/app/gui/src/main/scala/org/bitcoins/gui/dlc/DLCPaneModel.scala
+++ b/app/gui/src/main/scala/org/bitcoins/gui/dlc/DLCPaneModel.scala
@@ -8,6 +8,7 @@ import org.bitcoins.core.config.MainNet
 import org.bitcoins.core.number.{Int32, UInt16, UInt32}
 import org.bitcoins.core.protocol.dlc.DLCStatus
 import org.bitcoins.core.protocol.tlv._
+import org.bitcoins.core.protocol.transaction.Transaction
 import org.bitcoins.crypto.{CryptoUtil, ECPrivateKey, Sha256DigestBE}
 import org.bitcoins.gui.dlc.dialog._
 import org.bitcoins.gui.{GlobalData, TaskRunner}
@@ -17,11 +18,32 @@ import scalafx.scene.control.TextArea
 import scalafx.stage.Window
 import upickle.default._
 
-import scala.util.{Failure, Success}
+import scala.util.{Failure, Success, Try}
 
 class DLCPaneModel(resultArea: TextArea, oracleInfoArea: TextArea) {
   var taskRunner: TaskRunner = _
 
+  lazy val txPrintFunc: String => String = str => {
+    // See if it was an error or not
+    Try(Transaction.fromHex(str)) match {
+      case Failure(_) =>
+        // if it was print the error
+        str
+      case Success(tx) =>
+        s"""|TxId: ${tx.txIdBE.hex}
+            |
+            |url: ${GlobalData.buildTxUrl(tx.txIdBE)}
+            |
+            |If the tx doesn't show up after a few minutes at this url you may need to manually
+            |broadcast the tx with the full hex below
+            |
+            |Link to broadcast: ${GlobalData.broadcastUrl}
+            |
+            |Transaction: ${tx.hex}
+      """.stripMargin
+    }
+  }
+
   // Sadly, it is a Java "pattern" to pass null into
   // constructors to signal that you want some default
   val parentWindow: ObjectProperty[Window] =
@@ -62,7 +84,8 @@ class DLCPaneModel(resultArea: TextArea, oracleInfoArea: TextArea) {
 
   def printDLCDialogResult[T <: CliCommand](
       caption: String,
-      dialog: DLCDialog[T]): Unit = {
+      dialog: DLCDialog[T],
+      postProcessStr: String => String = str => str): Unit = {
     val result = dialog.showAndWait(parentWindow.value)
 
     result match {
@@ -71,7 +94,8 @@ class DLCPaneModel(resultArea: TextArea, oracleInfoArea: TextArea) {
           caption = caption,
           op = {
             ConsoleCli.exec(command, GlobalData.consoleCliConfig) match {
-              case Success(commandReturn) => resultArea.text = commandReturn
+              case Success(commandReturn) =>
+                resultArea.text = postProcessStr(commandReturn)
               case Failure(err) =>
                 err.printStackTrace()
                 resultArea.text = s"Error executing command:\n${err.getMessage}"
@@ -220,15 +244,17 @@ class DLCPaneModel(resultArea: TextArea, oracleInfoArea: TextArea) {
   }
 
   def onGetFunding(): Unit = {
-    printDLCDialogResult("GetDLCFundingTx", new GetFundingDLCDialog)
+    printDLCDialogResult("GetDLCFundingTx",
+                         new GetFundingDLCDialog,
+                         txPrintFunc)
   }
 
   def onExecute(): Unit = {
-    printDLCDialogResult("ExecuteDLC", new ExecuteDLCDialog)
+    printDLCDialogResult("ExecuteDLC", new ExecuteDLCDialog, txPrintFunc)
   }
 
   def onRefund(): Unit = {
-    printDLCDialogResult("ExecuteDLCRefund", new RefundDLCDialog)
+    printDLCDialogResult("ExecuteDLCRefund", new RefundDLCDialog, txPrintFunc)
   }
 
   def viewDLC(status: DLCStatus): Unit = {
diff --git a/core/src/main/scala/org/bitcoins/core/psbt/PSBTMap.scala b/core/src/main/scala/org/bitcoins/core/psbt/PSBTMap.scala
index a2dc23ebe1..68174e06ff 100644
--- a/core/src/main/scala/org/bitcoins/core/psbt/PSBTMap.scala
+++ b/core/src/main/scala/org/bitcoins/core/psbt/PSBTMap.scala
@@ -673,7 +673,7 @@ case class InputPSBTMap(elements: Vector[InputPSBTRecord])
       tx.outputs(txIn.previousOutput.vout.toInt)
     } else {
       throw new UnsupportedOperationException(
-        "Not enough information in the InputPSBTMap to get a valid InputInfo")
+        s"Not enough information in the InputPSBTMap to get a valid InputInfo: $elements")
     }
 
     val redeemScriptOpt = finalizedScriptSigOpt match {
diff --git a/testkit/src/main/scala/org/bitcoins/testkit/util/BytesUtil.scala b/testkit/src/main/scala/org/bitcoins/testkit/util/BytesUtil.scala
new file mode 100644
index 0000000000..b723ba5c7f
--- /dev/null
+++ b/testkit/src/main/scala/org/bitcoins/testkit/util/BytesUtil.scala
@@ -0,0 +1,71 @@
+package org.bitcoins.testkit.util
+
+import org.bitcoins.core.protocol.dlc.{CETSignatures, FundingSignatures}
+import org.bitcoins.core.protocol.script.{
+  EmptyScriptPubKey,
+  P2WPKHWitnessV0,
+  P2WSHWitnessV0,
+  ScriptWitnessV0
+}
+import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature
+import org.bitcoins.crypto.{ECAdaptorSignature, ECDigitalSignature}
+import scodec.bits.ByteVector
+
+object BytesUtil {
+
+  def flipAtIndex(bytes: ByteVector, byteIndex: Int): ByteVector = {
+    val (front, backWithToFlip) = bytes.splitAt(byteIndex)
+    val (toFlip, back) = backWithToFlip.splitAt(1)
+    front ++ toFlip.xor(ByteVector.fromByte(1)) ++ back
+  }
+
+  def flipBit(signature: ECDigitalSignature): ECDigitalSignature = {
+    ECDigitalSignature(flipAtIndex(signature.bytes, 60))
+  }
+
+  def flipBit(partialSignature: PartialSignature): PartialSignature = {
+    partialSignature.copy(signature = flipBit(partialSignature.signature))
+  }
+
+  def flipBit(adaptorSignature: ECAdaptorSignature): ECAdaptorSignature = {
+    ECAdaptorSignature(flipAtIndex(adaptorSignature.bytes, 40))
+  }
+
+  def flipBit(witness: ScriptWitnessV0): ScriptWitnessV0 = {
+    witness match {
+      case p2wpkh: P2WPKHWitnessV0 =>
+        P2WPKHWitnessV0(p2wpkh.pubKey, flipBit(p2wpkh.signature))
+      case p2wsh: P2WSHWitnessV0 =>
+        val sigOpt = p2wsh.stack.zipWithIndex.find {
+          case (bytes, _) =>
+            bytes.length >= 67 && bytes.length <= 73
+        }
+
+        sigOpt match {
+          case Some((sig, index)) =>
+            P2WSHWitnessV0(
+              EmptyScriptPubKey,
+              p2wsh.stack.updated(index,
+                                  flipBit(ECDigitalSignature(sig)).bytes))
+          case None =>
+            P2WSHWitnessV0(
+              EmptyScriptPubKey,
+              p2wsh.stack.updated(0, flipAtIndex(p2wsh.stack.head, 0)))
+        }
+    }
+  }
+
+  def flipBit(fundingSigs: FundingSignatures): FundingSignatures = {
+    val (firstOutPoint, witness) = fundingSigs.head
+    val badWitness = flipBit(witness)
+    FundingSignatures(fundingSigs.tail.toVector.+:(firstOutPoint -> badWitness))
+  }
+
+  def flipBit(cetSigs: CETSignatures): CETSignatures = {
+    val badOutcomeSigs = cetSigs.outcomeSigs.map {
+      case (outcome, sig) => outcome -> flipBit(sig)
+    }
+    val badRefundSig = flipBit(cetSigs.refundSig)
+    CETSignatures(badOutcomeSigs, badRefundSig)
+  }
+}
diff --git a/wallet/src/main/scala/org/bitcoins/wallet/models/TransactionDAO.scala b/wallet/src/main/scala/org/bitcoins/wallet/models/TransactionDAO.scala
index 21b66be39f..fe1c4a7d4e 100644
--- a/wallet/src/main/scala/org/bitcoins/wallet/models/TransactionDAO.scala
+++ b/wallet/src/main/scala/org/bitcoins/wallet/models/TransactionDAO.scala
@@ -83,6 +83,11 @@ trait TxDAO[DbEntryType <: TxDB]
 
   def findByTxId(txId: DoubleSha256Digest): Future[Option[DbEntryType]] =
     findByTxId(txId.flip)
+
+  def findByTxIdBEs(
+      txIdBEs: Vector[DoubleSha256DigestBE]): Future[Vector[DbEntryType]] = {
+    database.run(findByPrimaryKeys(txIdBEs).result).map(_.toVector)
+  }
 }
 
 case class TransactionDAO()(implicit