diff --git a/app-commons/src/main/scala/org/bitcoins/commons/serializers/Picklers.scala b/app-commons/src/main/scala/org/bitcoins/commons/serializers/Picklers.scala index 73587dba83..393a98d727 100644 --- a/app-commons/src/main/scala/org/bitcoins/commons/serializers/Picklers.scala +++ b/app-commons/src/main/scala/org/bitcoins/commons/serializers/Picklers.scala @@ -9,6 +9,7 @@ import org.bitcoins.core.crypto.{ MnemonicCode } import org.bitcoins.core.currency.{Bitcoins, Satoshis} +import org.bitcoins.core.hd.AddressType import org.bitcoins.core.number.UInt32 import org.bitcoins.core.protocol.dlc.DLCMessage._ import org.bitcoins.core.protocol.dlc.DLCStatus._ @@ -520,4 +521,10 @@ object Picklers { implicit val extPrivateKeyPickler: ReadWriter[ExtPrivateKey] = readwriter[String].bimap(ExtKey.toString, ExtPrivateKey.fromString) + + implicit val ecPublicKeyPickler: ReadWriter[ECPublicKey] = + readwriter[String].bimap(_.hex, ECPublicKey.fromHex) + + implicit val addressTypePickler: ReadWriter[AddressType] = + readwriter[String].bimap(_.toString, AddressType.fromString) } 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 24c9ee5001..f0a6996aca 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/CliReaders.scala @@ -9,6 +9,7 @@ import org.bitcoins.core.api.wallet.CoinSelectionAlgo import org.bitcoins.core.config.{NetworkParameters, Networks} import org.bitcoins.core.crypto.{ExtPrivateKey, MnemonicCode} import org.bitcoins.core.currency._ +import org.bitcoins.core.hd.AddressType import org.bitcoins.core.number.UInt32 import org.bitcoins.core.protocol.BlockStamp.BlockTime import org.bitcoins.core.protocol._ @@ -334,4 +335,16 @@ object CliReaders { MnemonicCode.fromWords(words.toVector) } } + + implicit val ecPublicKeyReads: Read[ECPublicKey] = new Read[ECPublicKey] { + override def arity: Int = 1 + + override def reads: String => ECPublicKey = ECPublicKey.fromHex + } + + implicit val addressTypeReads: Read[AddressType] = new Read[AddressType] { + override def arity: Int = 1 + + override def reads: String => AddressType = AddressType.fromString + } } 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 f5344aafa6..5e375a9e36 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala @@ -12,6 +12,8 @@ import org.bitcoins.core.api.wallet.CoinSelectionAlgo import org.bitcoins.core.config.NetworkParameters import org.bitcoins.core.crypto.{ExtPrivateKey, MnemonicCode} import org.bitcoins.core.currency._ +import org.bitcoins.core.hd.AddressType +import org.bitcoins.core.hd.AddressType.SegWit import org.bitcoins.core.number.UInt32 import org.bitcoins.core.protocol.tlv._ import org.bitcoins.core.protocol.transaction.{ @@ -27,6 +29,7 @@ import org.bitcoins.core.wallet.utxo.AddressLabelTag import org.bitcoins.crypto.{ AesPassword, DoubleSha256DigestBE, + ECPublicKey, SchnorrDigitalSignature, Sha256DigestBE } @@ -1328,6 +1331,40 @@ object ConsoleCli { case other => other })) ), + note(sys.props("line.separator") + "=== Util ==="), + cmd("createmultisig") + .action((_, conf) => + conf.copy(command = CreateMultisig(0, Vector.empty, SegWit))) + .text("Creates a multi-signature address with n signature of m keys required.") + .children( + arg[Int]("nrequired") + .text("The number of required signatures out of the n keys.") + .required() + .action((nRequired, conf) => + conf.copy(command = conf.command match { + case createMultisig: CreateMultisig => + createMultisig.copy(requiredKeys = nRequired) + case other => other + })), + arg[Seq[ECPublicKey]]("keys") + .text("The hex-encoded public keys.") + .required() + .action((keys, conf) => + conf.copy(command = conf.command match { + case createMultisig: CreateMultisig => + createMultisig.copy(keys = keys.toVector) + case other => other + })), + arg[AddressType]("address_type") + .text("The address type to use. Options are \"legacy\", \"p2sh-segwit\", and \"bech32\"") + .optional() + .action((addrType, conf) => + conf.copy(command = conf.command match { + case createMultisig: CreateMultisig => + createMultisig.copy(addressType = addrType) + case other => other + })) + ), checkConfig { case Config(NoCommand, _, _, _) => failure("You need to provide a command!") @@ -1608,6 +1645,12 @@ object ConsoleCli { case GetSignatures(tlv) => RequestParam("getsignatures", Seq(up.writeJs(tlv))) + case CreateMultisig(requiredKeys, keys, addressType) => + RequestParam("createmultisig", + Seq(up.writeJs(requiredKeys), + up.writeJs(keys), + up.writeJs(addressType))) + case GetVersion => // skip sending to server and just return version number of cli return Success(EnvUtil.getVersion) @@ -1933,4 +1976,10 @@ object CliCommand { case class GetSignatures(oracleEventV0TLV: OracleEventV0TLV) extends CliCommand + + case class CreateMultisig( + requiredKeys: Int, + keys: Vector[ECPublicKey], + addressType: AddressType) + extends CliCommand } 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 0e4e9ce620..1a718b8410 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 @@ -1116,6 +1116,23 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { } } + "create multisig" in { + val key1 = ECPublicKey( + "0369c68f212ecaf3b3be52acb158a6fd87e469bc08726ef98a3b58401b75da3392") + val key2 = ECPublicKey( + "02c23222c46b96c5976930319cc4915791fdf5e1ad1203790ff98cb1e7517eed4a") + + val route = coreRoutes.handleCommand( + ServerCommand("createmultisig", + Arr(Num(1), Arr(Str(key1.hex), Str(key2.hex))))) + + Post() ~> route ~> check { + assert(contentType == `application/json`) + assert( + responseAs[String] == """{"result":{"address":"bcrt1qjtsq4h0thsy0qftjdfxldxwa4tph7kwuplj6nglvvyehduagqqnssf4l0c","redeemScript":"47512102c23222c46b96c5976930319cc4915791fdf5e1ad1203790ff98cb1e7517eed4a210369c68f212ecaf3b3be52acb158a6fd87e469bc08726ef98a3b58401b75da339252ae"},"error":null}""") + } + } + "return the peer list" in { val route = nodeRoutes.handleCommand(ServerCommand("getpeers", Arr())) 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 41bceeb5e4..0d3e4d4229 100644 --- a/app/server/src/main/scala/org/bitcoins/server/CoreRoutes.scala +++ b/app/server/src/main/scala/org/bitcoins/server/CoreRoutes.scala @@ -5,13 +5,23 @@ 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 org.bitcoins.core.hd.AddressType +import org.bitcoins.core.protocol.{Bech32Address, P2SHAddress} +import org.bitcoins.core.protocol.script.{ + MultiSignatureScriptPubKey, + P2SHScriptPubKey, + P2WSHWitnessSPKV0 +} +import org.bitcoins.server.BitcoinSAppConfig.toChainConf import org.bitcoins.server.routes.{Server, ServerCommand, ServerRoute} import ujson._ import scala.collection.mutable import scala.util.{Failure, Success} -case class CoreRoutes(core: CoreApi)(implicit system: ActorSystem) +case class CoreRoutes(core: CoreApi)(implicit + system: ActorSystem, + config: BitcoinSAppConfig) extends ServerRoute { import system.dispatcher @@ -156,5 +166,35 @@ case class CoreRoutes(core: CoreApi)(implicit system: ActorSystem) Server.httpSuccess(json) } } + + case ServerCommand("createmultisig", arr) => + CreateMultisig.fromJsArr(arr) match { + case Failure(exception) => + reject(ValidationRejection("failure", Some(exception))) + case Success(CreateMultisig(requiredKeys, keys, addressType)) => + complete { + val sorted = keys.sortBy(_.hex) + val spk = MultiSignatureScriptPubKey(requiredKeys, sorted) + + val address = addressType match { + case AddressType.SegWit => + val p2wsh = P2WSHWitnessSPKV0(spk) + Bech32Address(p2wsh, config.network) + case AddressType.NestedSegWit => + val p2wsh = P2WSHWitnessSPKV0(spk) + val p2sh = P2SHScriptPubKey(p2wsh) + P2SHAddress(p2sh, config.network) + case AddressType.Legacy => + val p2sh = P2SHScriptPubKey(spk) + P2SHAddress(p2sh, config.network) + } + + val json = Obj( + "address" -> Str(address.toString), + "redeemScript" -> Str(spk.hex) + ) + Server.httpSuccess(json) + } + } } } 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 f4f5a44cf0..15e47490f8 100644 --- a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala +++ b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala @@ -4,13 +4,15 @@ import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.LockUnspentOutputParamet import org.bitcoins.core.api.wallet.CoinSelectionAlgo import org.bitcoins.core.crypto._ import org.bitcoins.core.currency.{Bitcoins, Satoshis} +import org.bitcoins.core.hd.AddressType +import org.bitcoins.core.hd.AddressType.SegWit import org.bitcoins.core.protocol.BlockStamp.BlockHeight import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutPoint} import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp} import org.bitcoins.core.psbt.PSBT import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.core.wallet.utxo.AddressLabelTag -import org.bitcoins.crypto.{AesPassword, DoubleSha256DigestBE} +import org.bitcoins.crypto.{AesPassword, DoubleSha256DigestBE, ECPublicKey} import ujson._ import scala.util.{Failure, Try} @@ -334,6 +336,44 @@ object ImportXprv extends ServerJsonModels { } } +case class CreateMultisig( + requiredKeys: Int, + keys: Vector[ECPublicKey], + addressType: AddressType) + +object CreateMultisig extends ServerJsonModels { + + def fromJsArr(jsArr: ujson.Arr): Try[CreateMultisig] = { + jsArr.arr.toList match { + case requiredKeysJs :: keysJs :: addressTypeJs :: Nil => + Try { + val requiredKeys = requiredKeysJs.num.toInt + + val keys = keysJs.arr.map(value => ECPublicKey(value.str)) + + val addrType = AddressType.fromString(addressTypeJs.str) + CreateMultisig(requiredKeys, keys.toVector, addrType) + } + case requiredKeysJs :: keysJs :: Nil => + Try { + val requiredKeys = requiredKeysJs.num.toInt + + val keys = keysJs.arr.map(value => ECPublicKey(value.str)) + + CreateMultisig(requiredKeys, keys.toVector, SegWit) + } + case Nil => + Failure( + new IllegalArgumentException( + "Missing requiredKeys, keys, and addressType argument")) + case other => + Failure( + new IllegalArgumentException( + s"Bad number of arguments: ${other.length}. Expected: 3")) + } + } +} + case class CombinePSBTs(psbts: Seq[PSBT]) object CombinePSBTs extends ServerJsonModels { diff --git a/core/src/main/scala/org/bitcoins/core/hd/AddressType.scala b/core/src/main/scala/org/bitcoins/core/hd/AddressType.scala index e7b12bdc0b..2b91ab5076 100644 --- a/core/src/main/scala/org/bitcoins/core/hd/AddressType.scala +++ b/core/src/main/scala/org/bitcoins/core/hd/AddressType.scala @@ -1,18 +1,46 @@ package org.bitcoins.core.hd -/** The address types covered by BIP44, BIP49 and BIP84 */ -sealed abstract class AddressType +import org.bitcoins.crypto.StringFactory -object AddressType { +/** The address types covered by BIP44, BIP49 and BIP84 */ +sealed abstract class AddressType { + def altName: String +} + +object AddressType extends StringFactory[AddressType] { /** Uses BIP84 address derivation, gives bech32 address (`bc1...`) */ - final case object SegWit extends AddressType + final case object SegWit extends AddressType { + override def altName: String = "bech32" + } /** Uses BIP49 address derivation, gives SegWit addresses wrapped * in P2SH addresses (`3...`) */ - final case object NestedSegWit extends AddressType + final case object NestedSegWit extends AddressType { + override def altName: String = "p2sh-segwit" + } /** Uses BIP44 address derivation (`1...`) */ - final case object Legacy extends AddressType + final case object Legacy extends AddressType { + override def altName: String = "legacy" + } + + private val all = Vector(SegWit, NestedSegWit, Legacy) + + override def fromStringOpt(str: String): Option[AddressType] = { + all.find(_.toString.toLowerCase == str.toLowerCase) match { + case Some(addressType) => Some(addressType) + case None => + all.find(_.altName.toLowerCase == str.toLowerCase) + } + } + + override def fromString(string: String): AddressType = { + fromStringOpt(string) match { + case Some(addressType) => addressType + case None => + sys.error(s"Could not find address type for string=$string") + } + } } diff --git a/docs/applications/server.md b/docs/applications/server.md index b5235e04a6..63e7aa268e 100644 --- a/docs/applications/server.md +++ b/docs/applications/server.md @@ -229,6 +229,11 @@ For more information on how to use our built in `cli` to interact with the serve - `converttopsbt` `unsignedTx` - Creates an empty psbt from the given transaction - `unsignedTx` - serialized unsigned transaction in hex +#### Util + - `createmultisig` `nrequired` `keys` `[address_type]` - Creates a multi-signature address with n signature of m keys required. + - `nrequired` - The number of required signatures out of the n keys. + - `keys` - The hex-encoded public keys. + - `address_type` -The address type to use. Options are "legacy", "p2sh-segwit", and "bech32" ### Sign PSBT with Wallet Example