Add createmultisig cli command (#2495)

This commit is contained in:
benthecarman 2021-01-09 09:54:34 -06:00 committed by GitHub
parent 98ace6f14e
commit 84cde975d8
8 changed files with 207 additions and 8 deletions

View File

@ -9,6 +9,7 @@ import org.bitcoins.core.crypto.{
MnemonicCode MnemonicCode
} }
import org.bitcoins.core.currency.{Bitcoins, Satoshis} import org.bitcoins.core.currency.{Bitcoins, Satoshis}
import org.bitcoins.core.hd.AddressType
import org.bitcoins.core.number.UInt32 import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.dlc.DLCMessage._ import org.bitcoins.core.protocol.dlc.DLCMessage._
import org.bitcoins.core.protocol.dlc.DLCStatus._ import org.bitcoins.core.protocol.dlc.DLCStatus._
@ -520,4 +521,10 @@ object Picklers {
implicit val extPrivateKeyPickler: ReadWriter[ExtPrivateKey] = implicit val extPrivateKeyPickler: ReadWriter[ExtPrivateKey] =
readwriter[String].bimap(ExtKey.toString, ExtPrivateKey.fromString) 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)
} }

View File

@ -9,6 +9,7 @@ import org.bitcoins.core.api.wallet.CoinSelectionAlgo
import org.bitcoins.core.config.{NetworkParameters, Networks} import org.bitcoins.core.config.{NetworkParameters, Networks}
import org.bitcoins.core.crypto.{ExtPrivateKey, MnemonicCode} import org.bitcoins.core.crypto.{ExtPrivateKey, MnemonicCode}
import org.bitcoins.core.currency._ import org.bitcoins.core.currency._
import org.bitcoins.core.hd.AddressType
import org.bitcoins.core.number.UInt32 import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.BlockStamp.BlockTime import org.bitcoins.core.protocol.BlockStamp.BlockTime
import org.bitcoins.core.protocol._ import org.bitcoins.core.protocol._
@ -334,4 +335,16 @@ object CliReaders {
MnemonicCode.fromWords(words.toVector) 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
}
} }

View File

@ -12,6 +12,8 @@ import org.bitcoins.core.api.wallet.CoinSelectionAlgo
import org.bitcoins.core.config.NetworkParameters import org.bitcoins.core.config.NetworkParameters
import org.bitcoins.core.crypto.{ExtPrivateKey, MnemonicCode} import org.bitcoins.core.crypto.{ExtPrivateKey, MnemonicCode}
import org.bitcoins.core.currency._ 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.number.UInt32
import org.bitcoins.core.protocol.tlv._ import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.protocol.transaction.{ import org.bitcoins.core.protocol.transaction.{
@ -27,6 +29,7 @@ import org.bitcoins.core.wallet.utxo.AddressLabelTag
import org.bitcoins.crypto.{ import org.bitcoins.crypto.{
AesPassword, AesPassword,
DoubleSha256DigestBE, DoubleSha256DigestBE,
ECPublicKey,
SchnorrDigitalSignature, SchnorrDigitalSignature,
Sha256DigestBE Sha256DigestBE
} }
@ -1328,6 +1331,40 @@ object ConsoleCli {
case other => other 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 { checkConfig {
case Config(NoCommand, _, _, _) => case Config(NoCommand, _, _, _) =>
failure("You need to provide a command!") failure("You need to provide a command!")
@ -1608,6 +1645,12 @@ object ConsoleCli {
case GetSignatures(tlv) => case GetSignatures(tlv) =>
RequestParam("getsignatures", Seq(up.writeJs(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 => case GetVersion =>
// skip sending to server and just return version number of cli // skip sending to server and just return version number of cli
return Success(EnvUtil.getVersion) return Success(EnvUtil.getVersion)
@ -1933,4 +1976,10 @@ object CliCommand {
case class GetSignatures(oracleEventV0TLV: OracleEventV0TLV) case class GetSignatures(oracleEventV0TLV: OracleEventV0TLV)
extends CliCommand extends CliCommand
case class CreateMultisig(
requiredKeys: Int,
keys: Vector[ECPublicKey],
addressType: AddressType)
extends CliCommand
} }

View File

@ -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 { "return the peer list" in {
val route = val route =
nodeRoutes.handleCommand(ServerCommand("getpeers", Arr())) nodeRoutes.handleCommand(ServerCommand("getpeers", Arr()))

View File

@ -5,13 +5,23 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._ import akka.http.scaladsl.server._
import org.bitcoins.commons.jsonmodels.{SerializedPSBT, SerializedTransaction} import org.bitcoins.commons.jsonmodels.{SerializedPSBT, SerializedTransaction}
import org.bitcoins.core.api.core.CoreApi 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 org.bitcoins.server.routes.{Server, ServerCommand, ServerRoute}
import ujson._ import ujson._
import scala.collection.mutable import scala.collection.mutable
import scala.util.{Failure, Success} 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 { extends ServerRoute {
import system.dispatcher import system.dispatcher
@ -156,5 +166,35 @@ case class CoreRoutes(core: CoreApi)(implicit system: ActorSystem)
Server.httpSuccess(json) 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)
}
}
} }
} }

View File

@ -4,13 +4,15 @@ import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.LockUnspentOutputParamet
import org.bitcoins.core.api.wallet.CoinSelectionAlgo import org.bitcoins.core.api.wallet.CoinSelectionAlgo
import org.bitcoins.core.crypto._ import org.bitcoins.core.crypto._
import org.bitcoins.core.currency.{Bitcoins, Satoshis} 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.BlockStamp.BlockHeight
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutPoint} import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutPoint}
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp} import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.psbt.PSBT import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.AddressLabelTag import org.bitcoins.core.wallet.utxo.AddressLabelTag
import org.bitcoins.crypto.{AesPassword, DoubleSha256DigestBE} import org.bitcoins.crypto.{AesPassword, DoubleSha256DigestBE, ECPublicKey}
import ujson._ import ujson._
import scala.util.{Failure, Try} 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]) case class CombinePSBTs(psbts: Seq[PSBT])
object CombinePSBTs extends ServerJsonModels { object CombinePSBTs extends ServerJsonModels {

View File

@ -1,18 +1,46 @@
package org.bitcoins.core.hd package org.bitcoins.core.hd
/** The address types covered by BIP44, BIP49 and BIP84 */ import org.bitcoins.crypto.StringFactory
sealed abstract class AddressType
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...`) */ /** 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 /** Uses BIP49 address derivation, gives SegWit addresses wrapped
* in P2SH addresses (`3...`) * 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...`) */ /** 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")
}
}
} }

View File

@ -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 - `converttopsbt` `unsignedTx` - Creates an empty psbt from the given transaction
- `unsignedTx` - serialized unsigned transaction in hex - `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 ### Sign PSBT with Wallet Example