diff --git a/app-commons-test/src/test/scala/org/bitcoins/commons/SerializedPSBTTest.scala b/app-commons-test/src/test/scala/org/bitcoins/commons/SerializedPSBTTest.scala new file mode 100644 index 0000000000..a888955e37 --- /dev/null +++ b/app-commons-test/src/test/scala/org/bitcoins/commons/SerializedPSBTTest.scala @@ -0,0 +1,85 @@ +package org.bitcoins.commons + +import org.bitcoins.commons.jsonmodels.{SerializedPSBT, SerializedTransaction} +import org.bitcoins.core.number.UInt32 +import org.bitcoins.core.psbt.{GlobalPSBTRecord, OutputPSBTRecord, PSBT} +import org.bitcoins.core.script.crypto.HashType +import org.bitcoins.testkit.util.BitcoinSUnitTest +import play.api.libs.json.Json +import scodec.bits._ + +class SerializedPSBTTest extends BitcoinSUnitTest { + + behavior of "SerializedPSBT" + + it must "correctly decode a psbt" in { + val psbt = PSBT.fromBase64( + "cHNidP9Fq2AoiZroZUZZ7/Fl0n4dcF8zKWfpD3QzRcAm1QTxUQzzGnHjM5xU+xYUvYSPokH86tLWHVVmhrOQE2d//fPeu6Px6r0LW2DbYkBubiYldhPvO50/3M0wHtHncJ6w/UmdpFVMt/z1iQfbH9U4bq6iLS930BnOiRlc0KX8DQmDnKFdTdiyceBPOmWKSJT+cR1RIQabSiKY6plO4jkSbZ2yFGMBAP3dAgIAAAABG2Z29d/AhiGmmjjrgO9p0LwwYozRbr404YcnQQ+LTQMEAAAAAAAAAAADGQVGPusCAAD9vwFjVyEDkIsYrb9OJzC2oJUtRDntXxhNo7cZeByTHPkuJeCnAmshA4B9R4qr45hWk4fD3F+0um+wJVmP8kFRyXWflgEYKi61IQMLERF8Yx7x85yRmZyD7888+GENPbV2xACyxsAbEJoZpCEDLfkiP2NbzOWOTrM5qIiGFWLDrAYEoYj+6B/p474XWbQhAuNDwvhwWxJHuG/S7CS/bsk/wr8pR9LzCm8eEEqoQ58NIQPlPMn3Y69Fovcn4cexpU9r1whI0yWZ1OwvYHlsngyAyCECyGo6/HPABsWFUwjKuE+mG57duFkwbk17rug85rd0MCohA8ObOSpT1WPHeKKjc3MwDtFM8xr+LRlrDETYazNJhawyIQNqTHMWXX5dNDAuY71BaXfFF8yoHEaQDnDyNudqgfuMQSEC410iVARHWVmvKpcQUFJJx/H41G2/9sJ5jvRnyYIXwQIhA4APXyr35MW/4PPuMRtYg+caUGFqk+12x9wW+JFxHqnwIQMiS1b6HfL93efBgysG0PrlMPwA7eKMSFexdnOGHhOQP1yuZwXdzsmnALF1dqkUCzczWAtdSewwYE3SNlcLhnxJInGIrGjelDmdMwkAABYAFO26WaDgWxcVeSe4lfoysDTitaKJp/r3t2YNAAC4Y1IhA6WEMWgMOWjUk1FZvxYDTRvyVZKDWeCKsBvgJruAngCOIQKeg5DPhM1ghA26JAP1gvhyZ7K/Z2ttavmOd7AyFv2OyyEDjgniAeDz5qiX/5tyDxMJcyMN4yRoYXZwfFgT9x6iKqghAxmojRQWODhnuwBaEtxJHK9AlJXUkPjIOs4o08raE5zfVK5nBF6KYiOxdSEDvbxvoF42LUei1lcblCHsUzeXo7Wr1zKxw5agIRZVWqysaAAAAAAAAQMEggAAAAAAA6jMPk3fqYXY2pAV+YsGMMB2CAwXDBpwMObcmgEz8M0NO07ZYF60Sf8jj0zms7HOPzU3tBtRMROIXzyZreEJOfmqBIZKk6tg78xgjIW8mR+WuQAA") + + val tx = psbt.transaction + val decoded = SerializedPSBT.decodePSBT(psbt) + + assert(decoded.global.tx == SerializedTransaction.decodeRawTransaction(tx)) + assert(decoded.global.version == UInt32.zero) + assert( + decoded.global.unknowns == Vector(GlobalPSBTRecord.Unknown( + hex"ab6028899ae8654659eff165d27e1d705f332967e90f743345c026d504f1510cf31a71e3339c54fb1614bd848fa241fcead2d61d556686b39013677ffdf3debba3f1eabd0b", + hex"60db62406e6e26257613ef3b9d3fdccd301ed1e7709eb0fd499da4554cb7fcf58907db1fd5386eaea22d2f77d019ce89195cd0a5fc0d09839ca15d4dd8b271e04f3a658a4894fe711d5121069b4a2298ea994ee239126d9db21463" + ))) + + assert(decoded.inputs.size == 1) + assert(decoded.inputs.head.bip32Paths.isEmpty) + assert(decoded.inputs.head.finalizedScriptSig.isEmpty) + assert(decoded.inputs.head.finalizedScriptWitness.isEmpty) + assert(decoded.inputs.head.nonWitnessUtxo.isEmpty) + assert(decoded.inputs.head.witnessUtxo.isEmpty) + assert(decoded.inputs.head.redeemScript.isEmpty) + assert(decoded.inputs.head.witScript.isEmpty) + assert(decoded.inputs.head.proofOfReservesCommitment.isEmpty) + assert(decoded.inputs.head.signatures.isEmpty) + assert(decoded.inputs.head.unknowns.isEmpty) + assert( + decoded.inputs.head.sigHashType + .contains(HashType.sigHashNoneAnyoneCanPay)) + + assert(decoded.outputs.size == 3) + assert(decoded.outputs.head.bip32Paths.isEmpty) + assert(decoded.outputs.head.redeemScript.isEmpty) + assert(decoded.outputs.head.witScript.isEmpty) + assert(decoded.outputs.head.unknowns.isEmpty) + assert(decoded.outputs(1).bip32Paths.isEmpty) + assert(decoded.outputs(1).redeemScript.isEmpty) + assert(decoded.outputs(1).witScript.isEmpty) + assert( + decoded.outputs(1).unknowns == Vector(OutputPSBTRecord.Unknown( + hex"a8cc3e", + hex"dfa985d8da9015f98b0630c076080c170c1a7030e6dc9a0133f0cd0d3b4ed9605eb449ff238f4ce6b3b1ce3f3537b41b513113885f3c99ade10939f9aa04864a93ab60efcc608c85bc991f96b9" + ))) + assert(decoded.outputs.last.bip32Paths.isEmpty) + assert(decoded.outputs.last.redeemScript.isEmpty) + assert(decoded.outputs.last.witScript.isEmpty) + } + + it must "correctly decode a finalized psbt" in { + val psbt = PSBT.fromBase64( + "cHNidP8BAP1FAQIAAAABGYuEj8rH1dQ8DYG/wIJoOmA07cMG3qUA2joduT39u3QBAAAAAEhEAAAGZi6ggGgAAAAqBOhp5BGydSEDT4d2r4kKjBDT7N3xtlQRvQOoZMDnvl9kcu+Yz0N+zOWsB9sJYW0AAAAmaiSqIantmOy8chX8u/gjrWjoJwzItqupL01TN2d15WGchnbpGlW08zmpugUAAAcEuQUHI7J1tyGqaVkFAABJZCEDd0QjA5VNPMrHshsJ5ToPa6aXjJClUKFubu0Z3qUgGCmsZyEDCXN1QPwcYCN+5PBjESbL9eI3/Fd2SW/L1kOKOJ6lAsqsaMnuN1dBTAAAFlMU75XVn2xj3vMdoWxBKP6yYswBfsdaZnFQHAAAACZqJKohqe2Q60JHII6xx+YrJFTmibrNi7e8pICEfsbYEMYNKhOtMwAAAAAAAQCKAgAAAAAC7ekoBUtTAABQYyED6l4Vptz8do0rhD+X5Xlmyl5Wwi/fM/EwJmYE/LE4XWhnBfcyAqcAsnUhA/spF0dsJg2ZiBFYnA80MM2Pxou90wok6B8hA44T/vJlaKy4YEecR1gAAB4CSESydXapFHyp4KCUiJddycf+SqnAZI21/aJFiKwAAAAAAQdqRzBEAiBMdpIM1wDaIgn1BJarrd27uzx9kcTixUzQMFGbb0KTsQIgBxNIy+cxhoe1Bnxy/X3+ankfNtC4ydjJcMHlxV0MJqOAIQJUjewAnrbEt88DGQklYoDSLGlNS5Z2C6ukMaVGy+p6EgAAAAAAAAA=") + val tx = psbt.transaction + val decoded = SerializedPSBT.decodePSBT(psbt) + + assert(decoded.global.tx == SerializedTransaction.decodeRawTransaction(tx)) + assert(decoded.global.version == UInt32.zero) + + println(Json.prettyPrint(decoded.toJson)) + assert(decoded.inputs.size == 1) + assert(decoded.inputs.head.bip32Paths.isEmpty) + assert(decoded.inputs.head.finalizedScriptSig.nonEmpty) + assert(decoded.inputs.head.finalizedScriptWitness.isEmpty) + assert(decoded.inputs.head.nonWitnessUtxo.nonEmpty) + assert(decoded.inputs.head.witnessUtxo.isEmpty) + assert(decoded.inputs.head.redeemScript.isEmpty) + assert(decoded.inputs.head.witScript.isEmpty) + assert(decoded.inputs.head.proofOfReservesCommitment.isEmpty) + assert(decoded.inputs.head.signatures.isEmpty) + assert(decoded.inputs.head.unknowns.isEmpty) + } +} diff --git a/app-commons/src/main/scala/org/bitcoins/commons/jsonmodels/SerializedPSBT.scala b/app-commons/src/main/scala/org/bitcoins/commons/jsonmodels/SerializedPSBT.scala new file mode 100644 index 0000000000..2091689915 --- /dev/null +++ b/app-commons/src/main/scala/org/bitcoins/commons/jsonmodels/SerializedPSBT.scala @@ -0,0 +1,116 @@ +package org.bitcoins.commons.jsonmodels + +import org.bitcoins.commons.jsonmodels.SerializedTransaction._ +import org.bitcoins.commons.serializers.JsonSerializers._ +import org.bitcoins.core.crypto.ExtPublicKey +import org.bitcoins.core.number.UInt32 +import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature +import org.bitcoins.core.psbt._ +import org.bitcoins.core.script.constant.ScriptToken +import org.bitcoins.core.script.crypto.HashType +import play.api.libs.json._ +import scodec.bits.ByteVector + +case class SerializedPSBT( + global: SerializedPSBTGlobalMap, + inputs: Vector[SerializedPSBTInputMap], + outputs: Vector[SerializedPSBTOutputMap]) { + val toJson: JsValue = Json.toJson(this) +} + +case class SerializedPSBTGlobalMap( + tx: SerializedTransaction, + version: UInt32, + xpubs: Option[Vector[ExtPublicKey]], + unknowns: Vector[GlobalPSBTRecord.Unknown]) + +case class SerializedPSBTInputMap( + nonWitnessUtxo: Option[SerializedTransaction], + witnessUtxo: Option[SerializedTransactionOutput], + signatures: Option[Vector[PartialSignature]], + sigHashType: Option[HashType], + redeemScript: Option[Vector[ScriptToken]], + witScript: Option[Vector[ScriptToken]], + bip32Paths: Option[Vector[InputPSBTRecord.BIP32DerivationPath]], + finalizedScriptSig: Option[Vector[ScriptToken]], + finalizedScriptWitness: Option[SerializedTransactionWitness], + proofOfReservesCommitment: Option[ByteVector], + unknowns: Vector[InputPSBTRecord.Unknown]) + +case class SerializedPSBTOutputMap( + redeemScript: Option[Vector[ScriptToken]], + witScript: Option[Vector[ScriptToken]], + bip32Paths: Option[Vector[OutputPSBTRecord.BIP32DerivationPath]], + unknowns: Vector[OutputPSBTRecord.Unknown]) + +object SerializedPSBT { + + def decodeGlobalMap(global: GlobalPSBTMap): SerializedPSBTGlobalMap = { + val decodedTx = decodeRawTransaction(global.unsignedTransaction.transaction) + val version = global.version.version + val xpubs = global.extendedPublicKeys.map(_.xpub) + val xpubsOpt = if (xpubs.nonEmpty) Some(xpubs) else None + val unknownRecords = global.getRecords(PSBTGlobalKeyId.UnknownKeyId) + + SerializedPSBTGlobalMap(decodedTx, version, xpubsOpt, unknownRecords) + } + + def decodeInputMap( + input: InputPSBTMap, + index: Int): SerializedPSBTInputMap = { + val prevTxOpt = input.nonWitnessOrUnknownUTXOOpt.map(_.transactionSpent) + val nonWitnessUtxo = prevTxOpt.map(decodeRawTransaction) + val witnessUtxo = input.witnessUTXOOpt.map(rec => + decodeTransactionOutput(rec.witnessUTXO, index)) + + val sigs = input.partialSignatures + val sigsOpt = if (sigs.nonEmpty) Some(sigs) else None + val hashType = input.sigHashTypeOpt.map(_.hashType) + val redeemScript = input.redeemScriptOpt.map(_.redeemScript.asm.toVector) + val witScript = input.witnessScriptOpt.map(_.witnessScript.asm.toVector) + val bip32Paths = input.BIP32DerivationPaths + val bip32PathsOpt = if (bip32Paths.nonEmpty) Some(bip32Paths) else None + + val finalizedScriptSig = + input.finalizedScriptSigOpt.map(_.scriptSig.asm.toVector) + val finalizedWitScript = input.finalizedScriptWitnessOpt.flatMap(rec => + decodeRawTransactionWitness(rec.scriptWitness)) + + val porCommit = input.proofOfReservesCommitmentOpt.map(_.porCommitment) + + val unknowns = input.getRecords(PSBTInputKeyId.UnknownKeyId) + + SerializedPSBTInputMap(nonWitnessUtxo, + witnessUtxo, + sigsOpt, + hashType, + redeemScript, + witScript, + bip32PathsOpt, + finalizedScriptSig, + finalizedWitScript, + porCommit, + unknowns) + } + + def decodeOutputMap(output: OutputPSBTMap): SerializedPSBTOutputMap = { + val redeemScript = output.redeemScriptOpt.map(_.redeemScript.asm.toVector) + val witScript = output.witnessScriptOpt.map(_.witnessScript.asm.toVector) + val bip32Paths = output.BIP32DerivationPaths + val bip32PathsOpt = if (bip32Paths.nonEmpty) Some(bip32Paths) else None + val unknowns = output.getRecords(PSBTOutputKeyId.UnknownKeyId) + + SerializedPSBTOutputMap(redeemScript, witScript, bip32PathsOpt, unknowns) + } + + def decodePSBT(psbt: PSBT): SerializedPSBT = { + val global = decodeGlobalMap(psbt.globalMap) + val inputs = psbt.inputMaps.zipWithIndex.map { + case (input, index) => + decodeInputMap(input, index) + } + val outputs = psbt.outputMaps.map(decodeOutputMap) + + SerializedPSBT(global, inputs, outputs) + } +} diff --git a/app-commons/src/main/scala/org/bitcoins/commons/jsonmodels/SerializedTransaction.scala b/app-commons/src/main/scala/org/bitcoins/commons/jsonmodels/SerializedTransaction.scala new file mode 100644 index 0000000000..50ea8da1fa --- /dev/null +++ b/app-commons/src/main/scala/org/bitcoins/commons/jsonmodels/SerializedTransaction.scala @@ -0,0 +1,147 @@ +package org.bitcoins.commons.jsonmodels + +import org.bitcoins.core.number.{Int32, UInt32} +import org.bitcoins.core.protocol.script._ +import org.bitcoins.core.protocol.transaction._ +import org.bitcoins.commons.serializers.JsonSerializers._ +import org.bitcoins.core.script.constant.{ + ScriptConstant, + ScriptNumberOperation, + ScriptToken +} +import org.bitcoins.crypto.{ + DoubleSha256DigestBE, + ECDigitalSignature, + ECPublicKey +} +import play.api.libs.json._ +import scodec.bits.ByteVector + +case class SerializedTransaction( + txid: DoubleSha256DigestBE, + wtxid: Option[DoubleSha256DigestBE], + version: Int32, + size: Long, + vsize: Long, + weight: Long, + locktime: UInt32, + vin: Vector[SerializedTransactionInput], + vout: Vector[SerializedTransactionOutput]) { + val toJson: JsValue = Json.toJson(this) +} + +case class SerializedTransactionInput( + txid: DoubleSha256DigestBE, + hex: String, + vout: UInt32, + scriptSig: Vector[ScriptToken], + txinwitness: Option[SerializedTransactionWitness], + sequence: UInt32 +) + +case class SerializedTransactionWitness( + hex: String, + scriptType: Option[String], + script: Option[Vector[ScriptToken]], + pubKey: Option[ECPublicKey], + signature: Option[ECDigitalSignature], + stack: Option[Vector[ByteVector]]) + +case class SerializedTransactionOutput( + value: BigDecimal, + n: UInt32, + scriptPubKey: Vector[ScriptToken], + hex: String +) + +object SerializedTransaction { + + def tokenToString(token: ScriptToken): String = { + token match { + case numOp: ScriptNumberOperation => numOp.toString + case constOp: ScriptConstant => constOp.bytes.toString + case otherOp: ScriptToken => otherOp.toString + } + } + + def decodeRawTransactionWitness( + witness: ScriptWitness): Option[SerializedTransactionWitness] = { + witness match { + case EmptyScriptWitness => None + case p2wpkh: P2WPKHWitnessV0 => + Some( + SerializedTransactionWitness(hex = p2wpkh.hex, + scriptType = Some("P2WPKH"), + script = None, + pubKey = Some(p2wpkh.pubKey), + signature = Some(p2wpkh.signature), + stack = None)) + case p2wsh: P2WSHWitnessV0 => + Some( + SerializedTransactionWitness(hex = p2wsh.hex, + scriptType = Some("P2WSH"), + script = + Some(p2wsh.redeemScript.asm.toVector), + pubKey = None, + signature = None, + stack = Some(p2wsh.stack.toVector.tail))) + } + } + + def decodeTransactionInput( + input: TransactionInput, + witnessOpt: Option[ScriptWitness]): SerializedTransactionInput = { + val decodedWitnessOpt = witnessOpt.flatMap(decodeRawTransactionWitness) + + SerializedTransactionInput( + txid = input.previousOutput.txIdBE, + hex = input.hex, + vout = input.previousOutput.vout, + scriptSig = input.scriptSignature.asm.toVector, + txinwitness = decodedWitnessOpt, + sequence = input.sequence + ) + } + + def decodeTransactionOutput( + output: TransactionOutput, + index: Int): SerializedTransactionOutput = { + SerializedTransactionOutput(value = output.value.toBigDecimal, + n = UInt32(index), + scriptPubKey = output.scriptPubKey.asm.toVector, + hex = output.hex) + } + + def decodeRawTransaction(tx: Transaction): SerializedTransaction = { + val inputs = tx.inputs.toVector.zipWithIndex.map { + case (input, index) => + val witnessOpt = tx match { + case _: NonWitnessTransaction => None + case wtx: WitnessTransaction => + Some(wtx.witness.witnesses(index)) + } + + decodeTransactionInput(input, witnessOpt) + } + + val outputs = tx.outputs.toVector.zipWithIndex.map { + case (output, index) => + decodeTransactionOutput(output, index) + } + + val wtxIdOpt = tx match { + case _: NonWitnessTransaction => None + case wtx: WitnessTransaction => Some(wtx.wTxIdBE) + } + + SerializedTransaction(txid = tx.txIdBE, + wtxid = wtxIdOpt, + version = tx.version, + size = tx.byteSize, + vsize = tx.vsize, + weight = tx.weight, + locktime = tx.lockTime, + vin = inputs, + vout = outputs) + } +} diff --git a/app-commons/src/main/scala/org/bitcoins/commons/serializers/JsonSerializers.scala b/app-commons/src/main/scala/org/bitcoins/commons/serializers/JsonSerializers.scala index 5420fcd8c4..0aca862546 100644 --- a/app-commons/src/main/scala/org/bitcoins/commons/serializers/JsonSerializers.scala +++ b/app-commons/src/main/scala/org/bitcoins/commons/serializers/JsonSerializers.scala @@ -17,12 +17,21 @@ import org.bitcoins.commons.serializers.JsonReaders._ import org.bitcoins.commons.serializers.JsonWriters._ import java.time.LocalDateTime +import org.bitcoins.commons.jsonmodels.SerializedTransaction.tokenToString +import org.bitcoins.commons.jsonmodels._ import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.AddressType import org.bitcoins.commons.jsonmodels.bitcoind._ import org.bitcoins.commons.jsonmodels.wallet._ +import org.bitcoins.core.psbt.{ + GlobalPSBTRecord, + InputPSBTRecord, + OutputPSBTRecord +} +import org.bitcoins.core.script.constant.ScriptToken import org.bitcoins.crypto._ import play.api.libs.functional.syntax._ import play.api.libs.json._ +import scodec.bits.ByteVector import scala.concurrent.duration.DurationLong @@ -556,6 +565,57 @@ object JsonSerializers { implicit val mempoolSpaceResultReads: Reads[MempoolSpaceResult] = Json.reads[MempoolSpaceResult] + implicit val byteVectorWrites: Writes[ByteVector] = + Writes[ByteVector](bytes => JsString(bytes.toHex)) + + implicit val ecDigitalSignatureWrites: Writes[ECDigitalSignature] = + Writes[ECDigitalSignature](sig => JsString(sig.hex)) + + implicit val ecPublicKeyWrites: Writes[ECPublicKey] = + Writes[ECPublicKey](pubKey => JsString(pubKey.hex)) + + implicit val scriptTokenWrites: Writes[ScriptToken] = + Writes[ScriptToken](token => JsString(tokenToString(token))) + + implicit val doubleSha256DigestBEWrites: Writes[DoubleSha256DigestBE] = + Writes[DoubleSha256DigestBE](hash => JsString(hash.hex)) + + implicit val int32Writes: Writes[Int32] = + Writes[Int32](num => JsNumber(num.toLong)) + + implicit val serializedTransactionWitnessWrites: Writes[ + SerializedTransactionWitness] = Json.writes[SerializedTransactionWitness] + + implicit val serializedTransactionInputWrites: Writes[ + SerializedTransactionInput] = Json.writes[SerializedTransactionInput] + + implicit val serializedTransactionOutputWrites: Writes[ + SerializedTransactionOutput] = Json.writes[SerializedTransactionOutput] + + implicit val serializedTransactionWrites: Writes[SerializedTransaction] = + Json.writes[SerializedTransaction] + + implicit val unknownPSBTGlobalWrites: Writes[GlobalPSBTRecord.Unknown] = + GlobalPSBTRecordUnknownWrites + + implicit val unknownPSBTInputWrites: Writes[InputPSBTRecord.Unknown] = + InputPSBTRecordUnknownWrites + + implicit val unknownPSBTOutputWrites: Writes[OutputPSBTRecord.Unknown] = + OutputPSBTRecordUnknownWrites + + implicit val serializedPSBTGlobalWrites: Writes[SerializedPSBTGlobalMap] = + Json.writes[SerializedPSBTGlobalMap] + + implicit val serializedPSBTInputWrites: Writes[SerializedPSBTInputMap] = + Json.writes[SerializedPSBTInputMap] + + implicit val serializedPSBTOutputWrites: Writes[SerializedPSBTOutputMap] = + Json.writes[SerializedPSBTOutputMap] + + implicit val serializedPSBTWrites: Writes[SerializedPSBT] = + Json.writes[SerializedPSBT] + // Map stuff implicit def mapDoubleSha256DigestReadsPreV19: Reads[ Map[DoubleSha256Digest, GetMemPoolResultPreV19]] = diff --git a/app-commons/src/main/scala/org/bitcoins/commons/serializers/JsonWriters.scala b/app-commons/src/main/scala/org/bitcoins/commons/serializers/JsonWriters.scala index ffa917de76..43bf4f1f99 100644 --- a/app-commons/src/main/scala/org/bitcoins/commons/serializers/JsonWriters.scala +++ b/app-commons/src/main/scala/org/bitcoins/commons/serializers/JsonWriters.scala @@ -10,6 +10,11 @@ import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.ln.currency.MilliSatoshis import org.bitcoins.core.protocol.script.{ScriptPubKey, WitnessScriptPubKey} import org.bitcoins.core.protocol.transaction.{Transaction, TransactionInput} +import org.bitcoins.core.psbt.{ + GlobalPSBTRecord, + InputPSBTRecord, + OutputPSBTRecord +} import org.bitcoins.core.script.crypto._ import org.bitcoins.core.util.BytesUtil import org.bitcoins.crypto.{DoubleSha256Digest, DoubleSha256DigestBE} @@ -126,4 +131,57 @@ object JsonWriters { JsObject(jsOpts) } } + + implicit object GlobalPSBTRecordUnknownWrites + extends Writes[GlobalPSBTRecord.Unknown] { + + override def writes(o: GlobalPSBTRecord.Unknown): JsValue = + JsObject( + Seq(("key", JsString(o.key.toHex)), ("value", JsString(o.value.toHex)))) + } + + implicit object InputPSBTRecordUnknownWrites + extends Writes[InputPSBTRecord.Unknown] { + + override def writes(o: InputPSBTRecord.Unknown): JsValue = + JsObject( + Seq(("key", JsString(o.key.toHex)), ("value", JsString(o.value.toHex)))) + } + + implicit object OutputPSBTRecordUnknownWrites + extends Writes[OutputPSBTRecord.Unknown] { + + override def writes(o: OutputPSBTRecord.Unknown): JsValue = + JsObject( + Seq(("key", JsString(o.key.toHex)), ("value", JsString(o.value.toHex)))) + } + + implicit object PartialSignatureWrites + extends Writes[InputPSBTRecord.PartialSignature] { + + override def writes(o: InputPSBTRecord.PartialSignature): JsValue = + JsObject( + Seq(("pubkey", JsString(o.pubKey.hex)), + ("signature", JsString(o.signature.hex)))) + } + + implicit object InputBIP32PathWrites + extends Writes[InputPSBTRecord.BIP32DerivationPath] { + + override def writes(o: InputPSBTRecord.BIP32DerivationPath): JsValue = + JsObject( + Seq(("pubkey", JsString(o.pubKey.hex)), + ("master_fingerprint", JsString(o.masterFingerprint.toHex)), + ("path", JsString(o.path.toString)))) + } + + implicit object OutputBIP32PathWrites + extends Writes[OutputPSBTRecord.BIP32DerivationPath] { + + override def writes(o: OutputPSBTRecord.BIP32DerivationPath): JsValue = + JsObject( + Seq(("pubkey", JsString(o.pubKey.hex)), + ("master_fingerprint", JsString(o.masterFingerprint.toHex)), + ("path", JsString(o.path.toString)))) + } } diff --git a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala index babf7ded29..bf6a98d1ef 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala @@ -901,6 +901,19 @@ object ConsoleCli { })) ), note(sys.props("line.separator") + "=== PSBT ==="), + cmd("decodepsbt") + .action((_, conf) => conf.copy(command = DecodePSBT(PSBT.empty))) + .text("Return a JSON object representing the serialized, base64-encoded partially signed Bitcoin transaction.") + .children( + arg[PSBT]("psbt") + .text("PSBT serialized in hex or base64 format") + .required() + .action((psbt, conf) => + conf.copy(command = conf.command match { + case decode: DecodePSBT => + decode.copy(psbt = psbt) + case other => other + }))), cmd("combinepsbts") .action((_, conf) => conf.copy(command = CombinePSBTs(Seq.empty))) .text("Combines all the given PSBTs") @@ -1255,6 +1268,8 @@ object ConsoleCli { case SendRawTransaction(tx) => RequestParam("sendrawtransaction", Seq(up.writeJs(tx))) // PSBTs + case DecodePSBT(psbt) => + RequestParam("decodepsbt", Seq(up.writeJs(psbt))) case CombinePSBTs(psbts) => RequestParam("combinepsbts", Seq(up.writeJs(psbts))) case JoinPSBTs(psbts) => @@ -1547,6 +1562,7 @@ object CliCommand { extends CliCommand // PSBT + case class DecodePSBT(psbt: PSBT) extends CliCommand case class CombinePSBTs(psbts: Seq[PSBT]) extends CliCommand case class JoinPSBTs(psbts: Seq[PSBT]) extends CliCommand case class FinalizePSBT(psbt: PSBT) extends CliCommand diff --git a/app/server/src/main/scala/org/bitcoins/server/CoreRoutes.scala b/app/server/src/main/scala/org/bitcoins/server/CoreRoutes.scala index e777365326..a6e03c3004 100644 --- a/app/server/src/main/scala/org/bitcoins/server/CoreRoutes.scala +++ b/app/server/src/main/scala/org/bitcoins/server/CoreRoutes.scala @@ -3,6 +3,7 @@ package org.bitcoins.server import akka.actor.ActorSystem import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server._ +import org.bitcoins.commons.jsonmodels.{SerializedPSBT, SerializedTransaction} import org.bitcoins.core.api.core.CoreApi import scala.util.{Failure, Success} @@ -78,8 +79,21 @@ case class CoreRoutes(core: CoreApi)(implicit system: ActorSystem) reject(ValidationRejection("failure", Some(exception))) case Success(DecodeRawTransaction(tx)) => complete { - val jsonStr = SerializedTransaction.decodeRawTransaction(tx) - Server.httpSuccess(jsonStr) + val decoded = SerializedTransaction.decodeRawTransaction(tx) + val uJson = ujson.read(decoded.toJson.toString()) + Server.httpSuccess(uJson) + } + } + + case ServerCommand("decodepsbt", arr) => + DecodePSBT.fromJsArr(arr) match { + case Failure(exception) => + reject(ValidationRejection("failure", Some(exception))) + case Success(DecodePSBT(psbt)) => + complete { + val decoded = SerializedPSBT.decodePSBT(psbt) + val uJson = ujson.read(decoded.toJson.toString()) + Server.httpSuccess(uJson) } } } diff --git a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala index 7e6aef0c07..499c900a3e 100644 --- a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala +++ b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala @@ -286,6 +286,25 @@ object DecodeRawTransaction extends ServerJsonModels { } } +case class DecodePSBT(psbt: PSBT) + +object DecodePSBT extends ServerJsonModels { + + def fromJsArr(jsArr: ujson.Arr): Try[DecodePSBT] = { + jsArr.arr.toList match { + case psbtJs :: Nil => + Try { + val psbt = jsToPSBT(psbtJs) + DecodePSBT(psbt) + } + case other => + Failure( + new IllegalArgumentException( + s"Bad number of arguments: ${other.length}. Expected: 1")) + } + } +} + case class Rescan( batchSize: Option[Int], startBlock: Option[BlockStamp], 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 4e6f05b18b..a167661930 100644 --- a/core/src/main/scala/org/bitcoins/core/psbt/PSBTMap.scala +++ b/core/src/main/scala/org/bitcoins/core/psbt/PSBTMap.scala @@ -101,7 +101,7 @@ case class GlobalPSBTMap(elements: Vector[GlobalPSBTRecord]) getRecords(VersionKeyId).headOption.getOrElse(Version(UInt32.zero)) } - private def getRecords(key: PSBTGlobalKeyId): Vector[key.RecordType] = { + def getRecords(key: PSBTGlobalKeyId): Vector[key.RecordType] = { super.getRecords(key, PSBTGlobalKeyId) } @@ -194,7 +194,7 @@ case class InputPSBTMap(elements: Vector[InputPSBTRecord]) getRecords(ProofOfReservesCommitmentKeyId).headOption } - private def getRecords(key: PSBTInputKeyId): Vector[key.RecordType] = { + def getRecords(key: PSBTInputKeyId): Vector[key.RecordType] = { super.getRecords(key, PSBTInputKeyId) } @@ -832,7 +832,7 @@ case class OutputPSBTMap(elements: Vector[OutputPSBTRecord]) getRecords(BIP32DerivationPathKeyId) } - private def getRecords(key: PSBTOutputKeyId): Vector[key.RecordType] = { + def getRecords(key: PSBTOutputKeyId): Vector[key.RecordType] = { super.getRecords(key, PSBTOutputKeyId) } diff --git a/docs/applications/server.md b/docs/applications/server.md index 1d70f22212..abd685e8d9 100644 --- a/docs/applications/server.md +++ b/docs/applications/server.md @@ -194,6 +194,8 @@ For more information on how to use our built in `cli` to interact with the serve - `tx` - Transaction serialized in hex #### PSBT + - `decodepsbt` `psbt` - Return a JSON object representing the serialized, base64-encoded partially signed Bitcoin transaction. + - `psbt` - PSBT serialized in hex or base64 format - `combinepsbts` `psbts` - Combines all the given PSBTs - `psbts` - PSBTs serialized in hex or base64 format - `joinpsbts` `psbts` - Combines all the given PSBTs