mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2024-11-19 18:02:54 +01:00
Add createmultisig cli command (#2495)
This commit is contained in:
parent
98ace6f14e
commit
84cde975d8
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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()))
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user