diff --git a/app/cli/src/main/scala/org/bitcoins/cli/Cli.scala b/app/cli/src/main/scala/org/bitcoins/cli/Cli.scala index aaa22e64cb..e66987d3c7 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/Cli.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/Cli.scala @@ -8,6 +8,8 @@ import org.bitcoins.cli.CliReaders._ import org.bitcoins.core.config.NetworkParameters import org.bitcoins.core.currency._ import org.bitcoins.core.protocol._ +import org.bitcoins.core.protocol.transaction.{EmptyTransaction, Transaction} +import org.bitcoins.core.psbt.PSBT import org.bitcoins.picklers._ import scopt.OParser import ujson.{Num, Str} @@ -44,6 +46,13 @@ object CliCommand { endBlock: Option[BlockStamp], force: Boolean) extends CliCommand + + // PSBT + case class CombinePSBTs(psbts: Seq[PSBT]) extends CliCommand + case class JoinPSBTs(psbts: Seq[PSBT]) extends CliCommand + case class FinalizePSBT(psbt: PSBT) extends CliCommand + case class ExtractFromPSBT(psbt: PSBT) extends CliCommand + case class ConvertToPSBT(transaction: Transaction) extends CliCommand } object Cli extends App { @@ -157,6 +166,77 @@ object Cli extends App { .hidden() .action((_, conf) => conf.copy(command = GetPeers)) .text(s"List the connected peers"), + cmd("combinepsbts") + .hidden() + .action((_, conf) => conf.copy(command = CombinePSBTs(Seq.empty))) + .text("Combines all the given PSBTs") + .children( + opt[Seq[PSBT]]("psbts") + .required() + .action((seq, conf) => + conf.copy(command = conf.command match { + case combinePSBTs: CombinePSBTs => + combinePSBTs.copy(psbts = seq) + case other => other + })) + ), + cmd("joinpsbts") + .hidden() + .action((_, conf) => conf.copy(command = JoinPSBTs(Seq.empty))) + .text("Combines all the given PSBTs") + .children( + opt[Seq[PSBT]]("psbts") + .required() + .action((seq, conf) => + conf.copy(command = conf.command match { + case joinPSBTs: JoinPSBTs => + joinPSBTs.copy(psbts = seq) + case other => other + })) + ), + cmd("finalizepsbt") + .hidden() + .action((_, conf) => conf.copy(command = FinalizePSBT(PSBT.empty))) + .text("Finalizes the given PSBT if it can") + .children( + opt[PSBT]("psbt") + .required() + .action((psbt, conf) => + conf.copy(command = conf.command match { + case finalizePSBT: FinalizePSBT => + finalizePSBT.copy(psbt = psbt) + case other => other + })) + ), + cmd("extractfrompsbt") + .hidden() + .action((_, conf) => conf.copy(command = ExtractFromPSBT(PSBT.empty))) + .text("Extracts a transaction from the given PSBT if it can") + .children( + opt[PSBT]("psbt") + .required() + .action((psbt, conf) => + conf.copy(command = conf.command match { + case extractFromPSBT: ExtractFromPSBT => + extractFromPSBT.copy(psbt = psbt) + case other => other + })) + ), + cmd("converttopsbt") + .hidden() + .action((_, conf) => + conf.copy(command = ConvertToPSBT(EmptyTransaction))) + .text("Creates an empty psbt from the given transaction") + .children( + opt[Transaction]("unsignedTx") + .required() + .action((tx, conf) => + conf.copy(command = conf.command match { + case convertToPSBT: ConvertToPSBT => + convertToPSBT.copy(transaction = tx) + case other => other + })) + ), help('h', "help").text("Display this help message and exit"), arg[String]("") .optional() @@ -228,7 +308,19 @@ object Cli extends App { // besthash case GetBestBlockHash => RequestParam("getbestblockhash") // peers - case GetPeers => RequestParam("getpeers") + case GetPeers => RequestParam("getpeers") + // PSBTs + case CombinePSBTs(psbts) => + RequestParam("combinepsbts", Seq(up.writeJs(psbts))) + case JoinPSBTs(psbts) => + RequestParam("joinpsbts", Seq(up.writeJs(psbts))) + case FinalizePSBT(psbt) => + RequestParam("finalizepsbt", Seq(up.writeJs(psbt))) + case ExtractFromPSBT(psbt) => + RequestParam("extractfrompsbt", Seq(up.writeJs(psbt))) + case ConvertToPSBT(tx) => + RequestParam("converttopsbt", Seq(up.writeJs(tx))) + case NoCommand => ??? } diff --git a/app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala b/app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala index 46a9c9cba9..eafc5f8705 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala @@ -6,6 +6,9 @@ import org.bitcoins.core.config.{NetworkParameters, Networks} import org.bitcoins.core.currency._ import org.bitcoins.core.protocol.BlockStamp.BlockTime import org.bitcoins.core.protocol._ +import org.bitcoins.core.protocol.transaction.Transaction +import org.bitcoins.core.psbt.PSBT +import scodec.bits.ByteVector import scopt._ /** scopt readers for parsing CLI params and options */ @@ -62,4 +65,17 @@ object CliReaders { case _ => BlockStamp.fromString(str).get } } + + implicit val psbtReads: Read[PSBT] = + new Read[PSBT] { + val arity: Int = 1 + + val reads: String => PSBT = PSBT.fromString + } + + implicit val txReads: Read[Transaction] = new Read[Transaction] { + val arity: Int = 1 + + val reads: String => Transaction = Transaction.fromHex + } } diff --git a/app/picklers/src/main/scala/org/bitcoins/picklers/Picklers.scala b/app/picklers/src/main/scala/org/bitcoins/picklers/Picklers.scala index eec087e79a..c88363e396 100644 --- a/app/picklers/src/main/scala/org/bitcoins/picklers/Picklers.scala +++ b/app/picklers/src/main/scala/org/bitcoins/picklers/Picklers.scala @@ -2,6 +2,8 @@ package org.bitcoins import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp} import org.bitcoins.core.currency.Bitcoins +import org.bitcoins.core.protocol.transaction.Transaction +import org.bitcoins.core.psbt.PSBT import upickle.default._ package object picklers { @@ -18,4 +20,10 @@ package object picklers { implicit val blockStampPickler: ReadWriter[BlockStamp] = readwriter[String].bimap(_.mkString, BlockStamp.fromString(_).get) + + implicit val psbtPickler: ReadWriter[PSBT] = + readwriter[String].bimap(_.base64, PSBT.fromString) + + implicit val transactionPickler: ReadWriter[Transaction] = + readwriter[String].bimap(_.hex, Transaction.fromHex) } diff --git a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala index 663812cdcc..b7cf5f27e5 100644 --- a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala +++ b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala @@ -6,6 +6,8 @@ import akka.http.scaladsl.model.ContentTypes._ import akka.http.scaladsl.server.ValidationRejection import akka.http.scaladsl.testkit.ScalatestRouteTest import org.bitcoins.chain.api.ChainApi +import org.bitcoins.core.Core +import org.bitcoins.core.api.CoreApi import org.bitcoins.core.crypto.DoubleSha256DigestBE import org.bitcoins.core.currency.{Bitcoins, CurrencyUnit} import org.bitcoins.core.protocol.BitcoinAddress @@ -15,7 +17,8 @@ import org.bitcoins.core.protocol.BlockStamp.{ BlockTime, InvalidBlockStamp } -import org.bitcoins.core.protocol.transaction.EmptyTransaction +import org.bitcoins.core.protocol.transaction.{EmptyTransaction, Transaction} +import org.bitcoins.core.psbt.PSBT import org.bitcoins.core.util.FutureUtil import org.bitcoins.core.wallet.fee.FeeUnit import org.bitcoins.node.Node @@ -49,8 +52,90 @@ class RoutesSpec val walletRoutes = WalletRoutes(mockWalletApi, mockNode) + val coreRoutes: CoreRoutes = CoreRoutes(Core) + "The server" should { + "combine PSBTs" in { + val psbt1 = PSBT( + "70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f00") + val psbt2 = PSBT( + "70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f00") + val expected = PSBT( + "70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f00") + + val route = + coreRoutes.handleCommand( + ServerCommand("combinepsbts", + Arr(Arr(Str(psbt1.base64), Str(psbt2.base64))))) + + Get() ~> route ~> check { + contentType shouldEqual `application/json` + responseAs[String] shouldEqual s"""{"result":"${expected.base64}","error":null}""" + } + + val joinRoute = + coreRoutes.handleCommand( + ServerCommand("joinpsbts", + Arr(Arr(Str(psbt1.base64), Str(psbt2.base64))))) + + Get() ~> joinRoute ~> check { + contentType shouldEqual `application/json` + responseAs[String] shouldEqual s"""{"result":"${expected.base64}","error":null}""" + } + } + + "finalize a PSBT" in { + val psbt = PSBT( + "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f012202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000") + val expected = PSBT( + "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000") + + val route = + coreRoutes.handleCommand( + ServerCommand("finalizepsbt", Arr(Str(psbt.hex)))) + + Get() ~> route ~> check { + contentType shouldEqual `application/json` + responseAs[String] shouldEqual s"""{"result":"${expected.base64}","error":null}""" + + } + } + + "extract a transaction from a PSBT" in { + val psbt = PSBT( + "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000") + val expected = Transaction( + "0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000") + + val route = + coreRoutes.handleCommand( + ServerCommand("extractfrompsbt", Arr(Str(psbt.hex)))) + + Get() ~> route ~> check { + contentType shouldEqual `application/json` + responseAs[String] shouldEqual s"""{"result":"${expected.hex}","error":null}""" + + } + } + + "convert a transaction to a PSBT" in { + val tx = Transaction( + "020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000") + val expected = PSBT( + "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000000000000000000") + + val route = + coreRoutes.handleCommand( + ServerCommand("converttopsbt", Arr(Str(tx.hex)))) + + Get() ~> route ~> check { + contentType shouldEqual `application/json` + responseAs[String] shouldEqual s"""{"result":"${expected.base64}","error":null}""" + + } + } + "return the block count" in { (mockChainApi.getBlockCount: () => Future[Int]) .expects() diff --git a/app/server/src/main/scala/org/bitcoins/server/CoreRoutes.scala b/app/server/src/main/scala/org/bitcoins/server/CoreRoutes.scala new file mode 100644 index 0000000000..aea7728cef --- /dev/null +++ b/app/server/src/main/scala/org/bitcoins/server/CoreRoutes.scala @@ -0,0 +1,77 @@ +package org.bitcoins.server + +import akka.actor.ActorSystem +import akka.http.scaladsl.server._ +import akka.http.scaladsl.server.Directives._ +import akka.stream.ActorMaterializer +import org.bitcoins.core.api.CoreApi + +import scala.util.{Failure, Success} + +case class CoreRoutes(core: CoreApi)(implicit system: ActorSystem) + extends ServerRoute { + import system.dispatcher + implicit val materializer: ActorMaterializer = ActorMaterializer() + + def handleCommand: PartialFunction[ServerCommand, StandardRoute] = { + case ServerCommand("finalizepsbt", arr) => + FinalizePSBT.fromJsArr(arr) match { + case Success(FinalizePSBT(psbt)) => + complete { + core + .finalizePSBT(psbt) + .map(finalized => Server.httpSuccess(finalized.base64)) + } + case Failure(exception) => + reject(ValidationRejection("failure", Some(exception))) + } + + case ServerCommand("extractfrompsbt", arr) => + ExtractFromPSBT.fromJsArr(arr) match { + case Success(ExtractFromPSBT(psbt)) => + complete { + core + .extractFromPSBT(psbt) + .map(tx => Server.httpSuccess(tx.hex)) + } + case Failure(exception) => + reject(ValidationRejection("failure", Some(exception))) + } + + case ServerCommand("converttopsbt", arr) => + ConvertToPSBT.fromJsArr(arr) match { + case Success(ConvertToPSBT(tx)) => + complete { + core + .convertToPSBT(tx) + .map(psbt => Server.httpSuccess(psbt.base64)) + } + case Failure(exception) => + reject(ValidationRejection("failure", Some(exception))) + } + + case ServerCommand("combinepsbts", arr) => + CombinePSBTs.fromJsArr(arr) match { + case Success(CombinePSBTs(psbts)) => + complete { + core + .combinePSBTs(psbts) + .map(psbt => Server.httpSuccess(psbt.base64)) + } + case Failure(exception) => + reject(ValidationRejection("failure", Some(exception))) + } + + case ServerCommand("joinpsbts", arr) => + JoinPSBTs.fromJsArr(arr) match { + case Success(JoinPSBTs(psbts)) => + complete { + core + .joinPSBTs(psbts) + .map(psbt => Server.httpSuccess(psbt.base64)) + } + case Failure(exception) => + reject(ValidationRejection("failure", Some(exception))) + } + } +} diff --git a/app/server/src/main/scala/org/bitcoins/server/Main.scala b/app/server/src/main/scala/org/bitcoins/server/Main.scala index 8f6346b7e5..6a4fbc1714 100644 --- a/app/server/src/main/scala/org/bitcoins/server/Main.scala +++ b/app/server/src/main/scala/org/bitcoins/server/Main.scala @@ -5,6 +5,7 @@ import java.nio.file.Files import akka.actor.ActorSystem import org.bitcoins.chain.config.ChainAppConfig +import org.bitcoins.core.Core import org.bitcoins.core.api.ChainQueryApi import org.bitcoins.keymanager.KeyManagerInitializeError import org.bitcoins.keymanager.bip39.BIP39KeyManager @@ -55,7 +56,9 @@ object Main extends App { val walletRoutes = WalletRoutes(wallet, node) val nodeRoutes = NodeRoutes(node) val chainRoutes = ChainRoutes(chainApi) - val server = Server(nodeConf, Seq(walletRoutes, nodeRoutes, chainRoutes)) + val coreRoutes = CoreRoutes(Core) + val server = + Server(nodeConf, Seq(walletRoutes, nodeRoutes, chainRoutes, coreRoutes)) server.start() } 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 a65760b0ee..f39461a26a 100644 --- a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala +++ b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala @@ -2,7 +2,9 @@ package org.bitcoins.server import org.bitcoins.core.currency.Bitcoins import org.bitcoins.core.protocol.BlockStamp.BlockHeight +import org.bitcoins.core.protocol.transaction.Transaction import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp} +import org.bitcoins.core.psbt.PSBT import ujson._ import upickle.default._ @@ -15,6 +17,65 @@ object ServerCommand { implicit val rw: ReadWriter[ServerCommand] = macroRW } +case class CombinePSBTs(psbts: Seq[PSBT]) + +object CombinePSBTs extends ServerJsonModels { + + def fromJsArr(jsArr: ujson.Arr): Try[CombinePSBTs] = { + require(jsArr.arr.size == 1, + s"Bad number of arguments: ${jsArr.arr.size}. Expected: 1") + + Try(CombinePSBTs(jsToPSBTSeq(jsArr.arr.head))) + } +} + +case class JoinPSBTs(psbts: Seq[PSBT]) + +object JoinPSBTs extends ServerJsonModels { + + def fromJsArr(jsArr: ujson.Arr): Try[JoinPSBTs] = { + CombinePSBTs + .fromJsArr(jsArr) + .map(combine => JoinPSBTs(combine.psbts)) + } +} + +case class FinalizePSBT(psbt: PSBT) + +object FinalizePSBT extends ServerJsonModels { + + def fromJsArr(jsArr: ujson.Arr): Try[FinalizePSBT] = { + require(jsArr.arr.size == 1, + s"Bad number of arguments: ${jsArr.arr.size}. Expected: 1") + + Try(FinalizePSBT(jsToPSBT(jsArr.arr.head))) + } +} + +case class ExtractFromPSBT(psbt: PSBT) + +object ExtractFromPSBT extends ServerJsonModels { + + def fromJsArr(jsArr: ujson.Arr): Try[ExtractFromPSBT] = { + require(jsArr.arr.size == 1, + s"Bad number of arguments: ${jsArr.arr.size}. Expected: 1") + + Try(ExtractFromPSBT(jsToPSBT(jsArr.arr.head))) + } +} + +case class ConvertToPSBT(tx: Transaction) + +object ConvertToPSBT extends ServerJsonModels { + + def fromJsArr(jsArr: ujson.Arr): Try[ConvertToPSBT] = { + require(jsArr.arr.size == 1, + s"Bad number of arguments: ${jsArr.arr.size}. Expected: 1") + + Try(ConvertToPSBT(jsToTx(jsArr.arr.head))) + } +} + case class Rescan( batchSize: Option[Int], startBlock: Option[BlockStamp], @@ -119,4 +180,12 @@ trait ServerJsonModels { } } + def jsToPSBTSeq(js: Value): Seq[PSBT] = { + js.arr.foldLeft(Seq.empty[PSBT])((seq, psbt) => seq :+ jsToPSBT(psbt)) + } + + def jsToPSBT(js: Value): PSBT = PSBT.fromString(js.str) + + def jsToTx(js: Value): Transaction = Transaction.fromHex(js.str) + } diff --git a/core-test/src/test/scala/org/bitcoins/core/api/CoreApiTest.scala b/core-test/src/test/scala/org/bitcoins/core/api/CoreApiTest.scala new file mode 100644 index 0000000000..eb8fb162f7 --- /dev/null +++ b/core-test/src/test/scala/org/bitcoins/core/api/CoreApiTest.scala @@ -0,0 +1,109 @@ +package org.bitcoins.core.api + +import org.bitcoins.core.Core +import org.bitcoins.core.protocol.script.{P2PKHScriptSignature, ScriptSignature} +import org.bitcoins.core.protocol.transaction._ +import org.bitcoins.core.psbt.PSBT +import org.bitcoins.testkit.util.BitcoinSAsyncTest + +import scala.concurrent.Future + +class CoreApiTest extends BitcoinSAsyncTest { + + behavior of "CoreApi" + + implicit override val generatorDrivenConfig: PropertyCheckConfiguration = + generatorDrivenConfigNewCode + + it must "successfully combine two PSBTs with unknown types" in { + val psbt1 = PSBT( + "70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f00") + val psbt2 = PSBT( + "70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f00") + val expected = PSBT( + "70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f00") + + Core.joinPSBTs(Seq(psbt1, psbt2)).map(psbt => assert(psbt == expected)) + Core.combinePSBTs(Seq(psbt1, psbt2)).map(psbt => assert(psbt == expected)) + } + + it must "fail to combine when given an empty seq" in { + Core + .joinPSBTs(Seq.empty) + .failed + .map(err => assertThrows[IllegalArgumentException](throw err)) + } + + it must "catch errors when combining PSBTs" in { + val psbt0 = PSBT.fromUnsignedTx(Transaction( + "020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000")) + + val psbt1 = PSBT.fromUnsignedTx(Transaction( + "02000000019dfc6628c26c5899fe1bd3dc338665bfd55d7ada10f6220973df2d386dec12760100000000ffffffff01f03dcd1d000000001600147b3a00bfdc14d27795c2b74901d09da6ef13357900000000")) + + Core + .joinPSBTs(Seq(psbt0, psbt1)) + .failed + .map(err => assertThrows[IllegalArgumentException](throw err)) + } + + it must "successfully finalize a PSBT" in { + val psbt = PSBT( + "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f012202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000") + + val expected = PSBT( + "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000") + + Core.finalizePSBT(psbt).map(finalized => assert(finalized == expected)) + } + + it must "catch errors when finalizing a PSBT" in { + val psbt = PSBT( + "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000000000000000000") + + Core + .finalizePSBT(psbt) + .failed + .map(err => assertThrows[IllegalStateException](throw err)) + } + + it must "successfully extract a transaction from a finalized PSBT" in { + val psbt = PSBT( + "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000") + val expected = Transaction.fromHex( + "0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000") + + Core + .extractFromPSBT(psbt) + .map(tx => assert(tx == expected)) + } + + it must "catch errors when extracting a transaction from a finalized PSBT" in { + val psbt = PSBT( + "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000000000000000000") + + Core + .extractFromPSBT(psbt) + .failed + .map(err => assertThrows[IllegalStateException](throw err)) + } + + it must "convert a tx to a PSBT" in { + val tx = Transaction( + "020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000") + val expected = PSBT( + "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000000000000000000") + + Core.convertToPSBT(tx).map(psbt => assert(psbt == expected)) + } + + it must "catch errors when converting a tx to a PSBT" in { + val tx = Transaction( + "0200000002cffe265210b42971f9642adcf88f6e6aaa40e6baec30037ce0088383b47c4f28031e306ffd2403004730450220259683be7d97e0a0f611926057bbd3fc8c5f886a06d39ec5ce330a87bcb33fa9022100ae6eb066c89dee2b5e4fefaa03450cfa6960508c2dc1c82f206bd66b9e8d3913463044022030d398ef4a69646ae40a84af9634e0608f4882269b92777f239c146555a3d4b602200ff23130697bca6bb7be3b69962387d6caeb25e198dc59969328d993119f9b5147304502206c0abf639640f56ab85c9800a2555be92569682a96bde261cfd9f1254578ee90022100c4b42232041d5f228ff832d1615151949d320eaf85bef9389463744f996e46b9473045022058d8a8ac02b5bd7139a99787dd42067e2de3a4e613f0878318aefcac4f13768f022100f620e66d3cc96230a0602e803a3f790861e1f66f72028e2ed79dc0ec3c24b98e4d01025f2103a2ae334a00a2a8b0191b41829b72eac3d4f207f87ea5b102383ed58716a43d452102351e70b50c30827ff18fde7b7458c991f74a938878f94c5f1384840b0775be2b210210c5d0c5528f61def05d0893e7b3145c5c5c58c4771c74de74470c69c8d86da121032999f0c7acd64f80b318beae5be7cedb4a72b0f370dcaa8ab9a0be1190df001821034afa6da2e9a4fbf84b33e5c35c81b0396542b4bf5eddaef0fb7e760833047fca21022875fe400ea9200ca013447c9dfe68e72d09e39a14c2b80f5beba35db00eb3a621031969406b809d0413d156fd2eb449cdc7b4b6fd431a3179abbd73cc2dd8a675bc21022f017723760552871039ba59678db384a1d195f2bcb6207a4514d1b86af0d6f42102a2d346e46656e5070433874521aaf24423f78438ec644c975ce8e2a32f35ef1d21027138384fcde2ae4aee8052f576227c3a158a09be5e8ee9df5145a39a06b5f6f32103d30805f2a4fc3d816b0f37b5500ffa7f59603ef45680c7f75991342d53af30d02103a27ef20b51281a98dcf55afdf53182d6a07c0d48afa511fa52d352faf79169b52102c50b2f80b12f0bbd304802916f906e366d017083d381cd45d64074fdb04b1b3d2103e3a5a4df1fc467589ed98d94f90cc1d41a2ac5a40b68a6c489cc1e4af04c5bb421039af4e1b6903db40e9cfe2c14cdd490c385d1339b0b7386719e277bbd5492378d5fae0f25e5bbcffe265210b42971f9642adcf88f6e6aaa40e6baec30037ce0088383b47c4f28031e306ffd2403004730450220259683be7d97e0a0f611926057bbd3fc8c5f886a06d39ec5ce330a87bcb33fa9022100ae6eb066c89dee2b5e4fefaa03450cfa6960508c2dc1c82f206bd66b9e8d3913463044022030d398ef4a69646ae40a84af9634e0608f4882269b92777f239c146555a3d4b602200ff23130697bca6bb7be3b69962387d6caeb25e198dc59969328d993119f9b5147304502206c0abf639640f56ab85c9800a2555be92569682a96bde261cfd9f1254578ee90022100c4b42232041d5f228ff832d1615151949d320eaf85bef9389463744f996e46b9473045022058d8a8ac02b5bd7139a99787dd42067e2de3a4e613f0878318aefcac4f13768f022100f620e66d3cc96230a0602e803a3f790861e1f66f72028e2ed79dc0ec3c24b98e4d01025f2103a2ae334a00a2a8b0191b41829b72eac3d4f207f87ea5b102383ed58716a43d452102351e70b50c30827ff18fde7b7458c991f74a938878f94c5f1384840b0775be2b210210c5d0c5528f61def05d0893e7b3145c5c5c58c4771c74de74470c69c8d86da121032999f0c7acd64f80b318beae5be7cedb4a72b0f370dcaa8ab9a0be1190df001821034afa6da2e9a4fbf84b33e5c35c81b0396542b4bf5eddaef0fb7e760833047fca21022875fe400ea9200ca013447c9dfe68e72d09e39a14c2b80f5beba35db00eb3a621031969406b809d0413d156fd2eb449cdc7b4b6fd431a3179abbd73cc2dd8a675bc21022f017723760552871039ba59678db384a1d195f2bcb6207a4514d1b86af0d6f42102a2d346e46656e5070433874521aaf24423f78438ec644c975ce8e2a32f35ef1d21027138384fcde2ae4aee8052f576227c3a158a09be5e8ee9df5145a39a06b5f6f32103d30805f2a4fc3d816b0f37b5500ffa7f59603ef45680c7f75991342d53af30d02103a27ef20b51281a98dcf55afdf53182d6a07c0d48afa511fa52d352faf79169b52102c50b2f80b12f0bbd304802916f906e366d017083d381cd45d64074fdb04b1b3d2103e3a5a4df1fc467589ed98d94f90cc1d41a2ac5a40b68a6c489cc1e4af04c5bb421039af4e1b6903db40e9cfe2c14cdd490c385d1339b0b7386719e277bbd5492378d5fae0f25e5bb0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000") + + Core + .convertToPSBT(tx) + .failed + .map(err => assertThrows[IllegalArgumentException](throw err)) + } +} diff --git a/core/src/main/scala/org/bitcoins/core/Core.scala b/core/src/main/scala/org/bitcoins/core/Core.scala new file mode 100644 index 0000000000..0d04d644d2 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/Core.scala @@ -0,0 +1,56 @@ +package org.bitcoins.core + +import org.bitcoins.core.api.CoreApi +import org.bitcoins.core.protocol.transaction.Transaction +import org.bitcoins.core.psbt.PSBT + +import scala.concurrent.Future +import scala.util.{Failure, Success} + +object Core extends CoreApi { + override def combinePSBTs(psbts: Seq[PSBT]): Future[PSBT] = { + if (psbts.isEmpty) { + Future.failed(new IllegalArgumentException("No PSBTs given")) + } else { + try { + val empty = PSBT.fromUnsignedTx(psbts.head.transaction) + val combined = + psbts.foldLeft(empty)((accum, psbt) => accum.combinePSBT(psbt)) + + Future.successful(combined) + } catch { + case err: IllegalArgumentException => + Future.failed(err) + } + } + } + + override def finalizePSBT(psbt: PSBT): Future[PSBT] = { + psbt.finalizePSBT match { + case Success(finalized) => + Future.successful(finalized) + case Failure(err) => + Future.failed(err) + } + } + + override def extractFromPSBT(psbt: PSBT): Future[Transaction] = { + psbt.extractTransactionAndValidate match { + case Success(extracted) => + Future.successful(extracted) + case Failure(err) => + Future.failed(err) + } + } + + override def convertToPSBT(transaction: Transaction): Future[PSBT] = { + try { + val psbt = PSBT.fromUnsignedTx(transaction) + + Future.successful(psbt) + } catch { + case err: IllegalArgumentException => + Future.failed(err) + } + } +} diff --git a/core/src/main/scala/org/bitcoins/core/api/CoreApi.scala b/core/src/main/scala/org/bitcoins/core/api/CoreApi.scala new file mode 100644 index 0000000000..0f55c80099 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/api/CoreApi.scala @@ -0,0 +1,18 @@ +package org.bitcoins.core.api + +import org.bitcoins.core.protocol.transaction.Transaction +import org.bitcoins.core.psbt.PSBT + +import scala.concurrent.Future + +trait CoreApi { + def combinePSBTs(psbts: Seq[PSBT]): Future[PSBT] + + def joinPSBTs(psbts: Seq[PSBT]): Future[PSBT] = combinePSBTs(psbts) + + def finalizePSBT(psbt: PSBT): Future[PSBT] + + def extractFromPSBT(psbt: PSBT): Future[Transaction] + + def convertToPSBT(transaction: Transaction): Future[PSBT] +} diff --git a/core/src/main/scala/org/bitcoins/core/psbt/PSBT.scala b/core/src/main/scala/org/bitcoins/core/psbt/PSBT.scala index a5f165b119..3869509beb 100644 --- a/core/src/main/scala/org/bitcoins/core/psbt/PSBT.scala +++ b/core/src/main/scala/org/bitcoins/core/psbt/PSBT.scala @@ -614,6 +614,23 @@ object PSBT extends Factory[PSBT] { // The magic bytes and separator defined by https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#specification final val magicBytes = hex"70736274ff" + final val empty = fromUnsignedTx(EmptyTransaction) + + def fromString(str: String): PSBT = { + ByteVector.fromHex(str) match { + case Some(hex) => + PSBT(hex) + case None => + ByteVector.fromBase64(str) match { + case Some(base64) => + PSBT(base64) + case None => + throw new IllegalArgumentException( + s"String given must be in base64 or hexadecimal, got: $str") + } + } + } + override def fromBytes(bytes: ByteVector): PSBT = { require(bytes.startsWith(magicBytes), s"A PSBT must start with the magic bytes $magicBytes, got: $bytes")