mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-03-03 10:46:42 +01:00
Import Seed cli commands (#2376)
* Import Seed cli commands * Respond to review
This commit is contained in:
parent
4d9c9415d9
commit
559bebbdbe
7 changed files with 274 additions and 10 deletions
|
@ -2,7 +2,12 @@ package org.bitcoins.commons.serializers
|
|||
|
||||
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.LockUnspentOutputParameter
|
||||
import org.bitcoins.core.api.wallet.CoinSelectionAlgo
|
||||
import org.bitcoins.core.crypto.ExtPublicKey
|
||||
import org.bitcoins.core.crypto.{
|
||||
ExtKey,
|
||||
ExtPrivateKey,
|
||||
ExtPublicKey,
|
||||
MnemonicCode
|
||||
}
|
||||
import org.bitcoins.core.currency.{Bitcoins, Satoshis}
|
||||
import org.bitcoins.core.number.UInt32
|
||||
import org.bitcoins.core.protocol.dlc.DLCMessage._
|
||||
|
@ -507,4 +512,12 @@ object Picklers {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val mnemonicCodePickler: ReadWriter[MnemonicCode] =
|
||||
readwriter[String].bimap(
|
||||
_.words.mkString(" "),
|
||||
str => MnemonicCode.fromWords(str.split(' ').toVector))
|
||||
|
||||
implicit val extPrivateKeyPickler: ReadWriter[ExtPrivateKey] =
|
||||
readwriter[String].bimap(ExtKey.toString, ExtPrivateKey.fromString)
|
||||
}
|
||||
|
|
|
@ -3,11 +3,11 @@ package org.bitcoins.cli
|
|||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.time.{Instant, ZoneId, ZonedDateTime}
|
||||
|
||||
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.LockUnspentOutputParameter
|
||||
import org.bitcoins.core.protocol.dlc.DLCMessage._
|
||||
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.number.UInt32
|
||||
import org.bitcoins.core.protocol.BlockStamp.BlockTime
|
||||
|
@ -317,4 +317,21 @@ object CliReaders {
|
|||
override def reads: String => LnMessage[DLCSignTLV] =
|
||||
LnMessageFactory(DLCSignTLV).fromHex
|
||||
}
|
||||
|
||||
implicit val extPrivKeyReads: Read[ExtPrivateKey] = new Read[ExtPrivateKey] {
|
||||
override def arity: Int = 1
|
||||
|
||||
override def reads: String => ExtPrivateKey = ExtPrivateKey.fromString
|
||||
}
|
||||
|
||||
implicit val mnemonicCodeReads: Read[MnemonicCode] = new Read[MnemonicCode] {
|
||||
override def arity: Int = 1
|
||||
|
||||
override def reads: String => MnemonicCode =
|
||||
str => {
|
||||
val words = str.split(' ')
|
||||
|
||||
MnemonicCode.fromWords(words.toVector)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.bitcoins.core.protocol.dlc.DLCMessage._
|
|||
import org.bitcoins.commons.serializers.Picklers._
|
||||
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.number.UInt32
|
||||
import org.bitcoins.core.protocol.tlv._
|
||||
|
@ -757,6 +758,68 @@ object ConsoleCli {
|
|||
case other => other
|
||||
}))
|
||||
),
|
||||
cmd("importseed")
|
||||
.action((_, conf) => conf.copy(command = ImportSeed("", null, None)))
|
||||
.text("Imports a mnemonic seed as a new seed file")
|
||||
.children(
|
||||
arg[String]("walletname")
|
||||
.text("Name to associate with this seed")
|
||||
.required()
|
||||
.action((walletName, conf) =>
|
||||
conf.copy(command = conf.command match {
|
||||
case is: ImportSeed =>
|
||||
is.copy(walletName = walletName)
|
||||
case other => other
|
||||
})),
|
||||
arg[MnemonicCode]("words")
|
||||
.text("Mnemonic seed words, space separated")
|
||||
.required()
|
||||
.action((mnemonic, conf) =>
|
||||
conf.copy(command = conf.command match {
|
||||
case is: ImportSeed =>
|
||||
is.copy(mnemonic = mnemonic)
|
||||
case other => other
|
||||
})),
|
||||
arg[AesPassword]("passphrase")
|
||||
.text("Passphrase to encrypt the seed with")
|
||||
.action((password, conf) =>
|
||||
conf.copy(command = conf.command match {
|
||||
case is: ImportSeed =>
|
||||
is.copy(passwordOpt = Some(password))
|
||||
case other => other
|
||||
}))
|
||||
),
|
||||
cmd("importxprv")
|
||||
.action((_, conf) => conf.copy(command = ImportXprv("", null, None)))
|
||||
.text("Imports a xprv as a new seed file")
|
||||
.children(
|
||||
arg[String]("walletname")
|
||||
.text("What name to associate with this seed")
|
||||
.required()
|
||||
.action((walletName, conf) =>
|
||||
conf.copy(command = conf.command match {
|
||||
case ix: ImportXprv =>
|
||||
ix.copy(walletName = walletName)
|
||||
case other => other
|
||||
})),
|
||||
arg[ExtPrivateKey]("xprv")
|
||||
.text("base58 encoded extended private key")
|
||||
.required()
|
||||
.action((xprv, conf) =>
|
||||
conf.copy(command = conf.command match {
|
||||
case ix: ImportXprv =>
|
||||
ix.copy(xprv = xprv)
|
||||
case other => other
|
||||
})),
|
||||
arg[AesPassword]("passphrase")
|
||||
.text("Passphrase to encrypt this seed with")
|
||||
.action((password, conf) =>
|
||||
conf.copy(command = conf.command match {
|
||||
case ix: ImportXprv =>
|
||||
ix.copy(passwordOpt = Some(password))
|
||||
case other => other
|
||||
}))
|
||||
),
|
||||
cmd("keymanagerpassphrasechange")
|
||||
.action((_, conf) =>
|
||||
conf.copy(command = KeyManagerPassphraseChange(null, null)))
|
||||
|
@ -1370,6 +1433,18 @@ object ConsoleCli {
|
|||
case KeyManagerPassphraseSet(password) =>
|
||||
RequestParam("keymanagerpassphraseset", Seq(up.writeJs(password)))
|
||||
|
||||
case ImportSeed(walletName, mnemonic, passwordOpt) =>
|
||||
RequestParam("importseed",
|
||||
Seq(up.writeJs(walletName),
|
||||
up.writeJs(mnemonic),
|
||||
up.writeJs(passwordOpt)))
|
||||
|
||||
case ImportXprv(walletName, xprv, passwordOpt) =>
|
||||
RequestParam("importxprv",
|
||||
Seq(up.writeJs(walletName),
|
||||
up.writeJs(xprv),
|
||||
up.writeJs(passwordOpt)))
|
||||
|
||||
// height
|
||||
case GetBlockCount => RequestParam("getblockcount")
|
||||
// filter count
|
||||
|
@ -1678,6 +1753,18 @@ object CliCommand {
|
|||
extends CliCommand
|
||||
case class KeyManagerPassphraseSet(password: AesPassword) extends CliCommand
|
||||
|
||||
case class ImportSeed(
|
||||
walletName: String,
|
||||
mnemonic: MnemonicCode,
|
||||
passwordOpt: Option[AesPassword])
|
||||
extends CliCommand
|
||||
|
||||
case class ImportXprv(
|
||||
walletName: String,
|
||||
xprv: ExtPrivateKey,
|
||||
passwordOpt: Option[AesPassword])
|
||||
extends CliCommand
|
||||
|
||||
// Node
|
||||
case object GetPeers extends CliCommand
|
||||
case object Stop extends CliCommand
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.bitcoins.server
|
|||
import java.time.Instant
|
||||
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.LockUnspentOutputParameter
|
||||
import org.bitcoins.core.api.wallet.CoinSelectionAlgo
|
||||
import org.bitcoins.core.crypto._
|
||||
import org.bitcoins.core.currency.{Bitcoins, Satoshis}
|
||||
import org.bitcoins.core.protocol.BlockStamp.BlockHeight
|
||||
import org.bitcoins.core.protocol.tlv._
|
||||
|
@ -258,6 +259,91 @@ object KeyManagerPassphraseSet extends ServerJsonModels {
|
|||
}
|
||||
}
|
||||
|
||||
case class ImportSeed(
|
||||
walletName: String,
|
||||
mnemonic: MnemonicCode,
|
||||
passwordOpt: Option[AesPassword])
|
||||
|
||||
object ImportSeed extends ServerJsonModels {
|
||||
|
||||
def fromJsArr(jsArr: ujson.Arr): Try[ImportSeed] = {
|
||||
jsArr.arr.toList match {
|
||||
case walletNameJs :: mnemonicJs :: passJs :: Nil =>
|
||||
Try {
|
||||
val walletName = walletNameJs.str
|
||||
|
||||
val mnemonicWords = mnemonicJs match {
|
||||
case Str(str) => str.split(' ').toVector
|
||||
case Arr(arr) => arr.map(_.str).toVector
|
||||
case Null | False | True | Num(_) | Obj(_) =>
|
||||
throw new IllegalArgumentException(
|
||||
"mnemonic must be a string or array of strings")
|
||||
}
|
||||
val mnemonic = MnemonicCode.fromWords(mnemonicWords)
|
||||
|
||||
val pass = passJs match {
|
||||
case Str(str) =>
|
||||
Some(AesPassword.fromString(str))
|
||||
case Null =>
|
||||
None
|
||||
case Arr(_) | False | True | Num(_) | Obj(_) =>
|
||||
throw new IllegalArgumentException(
|
||||
"password must be a string or null")
|
||||
}
|
||||
|
||||
ImportSeed(walletName, mnemonic, pass)
|
||||
}
|
||||
case Nil =>
|
||||
Failure(
|
||||
new IllegalArgumentException(
|
||||
"Missing walletName, mnemonic, and password argument"))
|
||||
case other =>
|
||||
Failure(
|
||||
new IllegalArgumentException(
|
||||
s"Bad number of arguments: ${other.length}. Expected: 3"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case class ImportXprv(
|
||||
walletName: String,
|
||||
xprv: ExtPrivateKey,
|
||||
passwordOpt: Option[AesPassword])
|
||||
|
||||
object ImportXprv extends ServerJsonModels {
|
||||
|
||||
def fromJsArr(jsArr: ujson.Arr): Try[ImportXprv] = {
|
||||
jsArr.arr.toList match {
|
||||
case walletNameJs :: xprvJs :: passJs :: Nil =>
|
||||
Try {
|
||||
val walletName = walletNameJs.str
|
||||
|
||||
val xprv = ExtPrivateKey.fromString(xprvJs.str)
|
||||
|
||||
val pass = passJs match {
|
||||
case Str(str) =>
|
||||
Some(AesPassword.fromString(str))
|
||||
case Null =>
|
||||
None
|
||||
case Arr(_) | False | True | Bool(_) | Num(_) | Obj(_) =>
|
||||
throw new IllegalArgumentException(
|
||||
"password must be a string or null")
|
||||
}
|
||||
|
||||
ImportXprv(walletName, xprv, pass)
|
||||
}
|
||||
case Nil =>
|
||||
Failure(
|
||||
new IllegalArgumentException(
|
||||
"Missing walletName, xprv, and password 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 {
|
||||
|
|
|
@ -10,10 +10,12 @@ import org.bitcoins.core.currency._
|
|||
import org.bitcoins.core.protocol.transaction.Transaction
|
||||
import org.bitcoins.core.wallet.utxo.{AddressLabelTagType, TxoState}
|
||||
import org.bitcoins.crypto.NetworkElement
|
||||
import org.bitcoins.keymanager.WalletStorage
|
||||
import org.bitcoins.keymanager._
|
||||
import org.bitcoins.keymanager.config.KeyManagerAppConfig
|
||||
import org.bitcoins.wallet.config.WalletAppConfig
|
||||
import ujson._
|
||||
|
||||
import java.time.Instant
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
|
@ -23,6 +25,8 @@ case class WalletRoutes(wallet: AnyHDWalletApi)(implicit
|
|||
extends ServerRoute {
|
||||
import system.dispatcher
|
||||
|
||||
implicit val kmConf: KeyManagerAppConfig = walletConf.kmConf
|
||||
|
||||
private def spendingInfoDbToJson(spendingInfoDb: SpendingInfoDb): Value = {
|
||||
Obj(
|
||||
"outpoint" -> Str(spendingInfoDb.outPoint.hex),
|
||||
|
@ -469,6 +473,54 @@ case class WalletRoutes(wallet: AnyHDWalletApi)(implicit
|
|||
val path = walletConf.seedPath
|
||||
WalletStorage.changeAesPassword(path, None, Some(password))
|
||||
|
||||
Server.httpSuccess(ujson.Null)
|
||||
}
|
||||
}
|
||||
|
||||
case ServerCommand("importseed", arr) =>
|
||||
ImportSeed.fromJsArr(arr) match {
|
||||
case Failure(err) =>
|
||||
reject(ValidationRejection("failure", Some(err)))
|
||||
case Success(ImportSeed(walletName, mnemonic, passwordOpt)) =>
|
||||
complete {
|
||||
val seedPath = kmConf.seedFolder.resolve(
|
||||
s"$walletName-${WalletStorage.ENCRYPTED_SEED_FILE_NAME}")
|
||||
|
||||
val creationTime = Instant.ofEpochSecond(WalletStorage.GENESIS_TIME)
|
||||
|
||||
val mnemonicState = passwordOpt match {
|
||||
case Some(pass) =>
|
||||
DecryptedMnemonic(mnemonic, creationTime).encrypt(pass)
|
||||
case None =>
|
||||
DecryptedMnemonic(mnemonic, creationTime)
|
||||
}
|
||||
|
||||
WalletStorage.writeSeedToDisk(seedPath, mnemonicState)
|
||||
|
||||
Server.httpSuccess(ujson.Null)
|
||||
}
|
||||
}
|
||||
|
||||
case ServerCommand("importxprv", arr) =>
|
||||
ImportXprv.fromJsArr(arr) match {
|
||||
case Failure(err) =>
|
||||
reject(ValidationRejection("failure", Some(err)))
|
||||
case Success(ImportXprv(walletName, xprv, passwordOpt)) =>
|
||||
complete {
|
||||
val seedPath = kmConf.seedFolder.resolve(
|
||||
s"$walletName-${WalletStorage.ENCRYPTED_SEED_FILE_NAME}")
|
||||
|
||||
val creationTime = Instant.ofEpochSecond(WalletStorage.GENESIS_TIME)
|
||||
|
||||
val mnemonicState = passwordOpt match {
|
||||
case Some(pass) =>
|
||||
DecryptedExtPrivKey(xprv, creationTime).encrypt(pass)
|
||||
case None =>
|
||||
DecryptedExtPrivKey(xprv, creationTime)
|
||||
}
|
||||
|
||||
WalletStorage.writeSeedToDisk(seedPath, mnemonicState)
|
||||
|
||||
Server.httpSuccess(ujson.Null)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -189,10 +189,18 @@ For more information on how to use our built in `cli` to interact with the serve
|
|||
- `lockunspent` `unlock` `transactions` - Temporarily lock (unlock=false) or unlock (unlock=true) specified transaction outputs.
|
||||
- `unlock` - Whether to unlock (true) or lock (false) the specified transactions
|
||||
- `transactions` - The transaction outpoints to unlock/lock
|
||||
- `walletpassphrasechange` `oldpassphrase` `newpassphrase` - Changes the wallet passphrase
|
||||
- `importseed` `walletname` `words` `passphrase` - Imports a mnemonic seed as a new seed file
|
||||
- `walletname` - Name to associate with this seed
|
||||
- `words` - Mnemonic seed words, space separated
|
||||
- `passphrase` - Passphrase to encrypt this seed with
|
||||
- `importxprv` `walletname` `xprv` `passphrase` - Imports a mnemonic seed as a new seed file
|
||||
- `walletname` - Name to associate with this seed
|
||||
- `xprv` - base58 encoded extended private key
|
||||
- `passphrase` - Passphrase to encrypt this seed with
|
||||
- `keymanagerpassphrasechange` `oldpassphrase` `newpassphrase` - Changes the wallet passphrase
|
||||
- `oldpassphrase` - The current passphrase
|
||||
- `newpassphrase` - The new passphrase
|
||||
- `walletpassphraseset` `passphrase` - Encrypts the wallet with the given passphrase
|
||||
- `keymanagerpassphraseset` `passphrase` - Encrypts the wallet with the given passphrase
|
||||
- `passphrase` - The passphrase to encrypt the wallet with
|
||||
|
||||
#### Network
|
||||
|
|
|
@ -33,6 +33,9 @@ case class KeyManagerAppConfig(
|
|||
config.getStringOrNone(s"bitcoin-s.wallet.walletName")
|
||||
}
|
||||
|
||||
lazy val seedFolder: Path = baseDatadir
|
||||
.resolve(WalletStorage.SEED_FOLDER_NAME)
|
||||
|
||||
/** The path to our encrypted mnemonic seed */
|
||||
lazy val seedPath: Path = {
|
||||
val prefix = walletNameOpt match {
|
||||
|
@ -40,17 +43,15 @@ case class KeyManagerAppConfig(
|
|||
s"$walletName-"
|
||||
case None => ""
|
||||
}
|
||||
baseDatadir
|
||||
.resolve(WalletStorage.SEED_FOLDER_NAME)
|
||||
.resolve(s"$prefix${WalletStorage.ENCRYPTED_SEED_FILE_NAME}")
|
||||
|
||||
seedFolder.resolve(s"$prefix${WalletStorage.ENCRYPTED_SEED_FILE_NAME}")
|
||||
}
|
||||
|
||||
override def start(): Future[Unit] = {
|
||||
val oldDefaultFile =
|
||||
baseDatadir.resolve(WalletStorage.ENCRYPTED_SEED_FILE_NAME)
|
||||
|
||||
val newDefaultFile = baseDatadir
|
||||
.resolve(WalletStorage.SEED_FOLDER_NAME)
|
||||
val newDefaultFile = seedFolder
|
||||
.resolve(WalletStorage.ENCRYPTED_SEED_FILE_NAME)
|
||||
|
||||
if (!Files.exists(newDefaultFile) && Files.exists(oldDefaultFile)) {
|
||||
|
|
Loading…
Add table
Reference in a new issue