CLI Commands for PSBTs (#1074)

CLI Commands for PSBTs
This commit is contained in:
Ben Carman 2020-01-30 14:50:08 -06:00 committed by GitHub
commit d1aa6386f0
11 changed files with 553 additions and 3 deletions

View File

@ -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]("<cmd>")
.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 => ???
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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)))
}
}
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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))
}
}

View File

@ -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)
}
}
}

View File

@ -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]
}

View File

@ -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")