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