mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-03-13 19:37:30 +01:00
Merge pull request #497 from torkelrogstad/2019-06-05-legacy-addresses
Legacy addresses
This commit is contained in:
commit
9d7952e411
17 changed files with 424 additions and 100 deletions
|
@ -12,12 +12,12 @@ import scala.concurrent.Promise
|
|||
import scala.util.Success
|
||||
import scala.util.Failure
|
||||
|
||||
case class ChainAppConfig(val confs: Config*) extends AppConfig {
|
||||
case class ChainAppConfig(private val confs: Config*) extends AppConfig {
|
||||
override protected val configOverrides: List[Config] = confs.toList
|
||||
override protected val moduleName: String = "chain"
|
||||
override protected type ConfigType = ChainAppConfig
|
||||
override protected def newConfigOfType(
|
||||
configs: List[Config]): ChainAppConfig = ChainAppConfig(configs: _*)
|
||||
override protected def newConfigOfType(configs: Seq[Config]): ChainAppConfig =
|
||||
ChainAppConfig(configs: _*)
|
||||
|
||||
/**
|
||||
* Checks whether or not the chain project is initialized by
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
bitcoin-s {
|
||||
datadir = ${HOME}/.bitcoin-s
|
||||
network = regtest # regtest, testnet3, mainnet
|
||||
|
||||
# settings for wallet module
|
||||
wallet {
|
||||
defaultAccountType = legacy # legacy, segwit, nested-segwit
|
||||
}
|
||||
}
|
|
@ -54,7 +54,7 @@ abstract class AppConfig extends BitcoinSLogger {
|
|||
protected type ConfigType <: AppConfig
|
||||
|
||||
/** Constructor to make a new instance of this config type */
|
||||
protected def newConfigOfType(configOverrides: List[Config]): ConfigType
|
||||
protected def newConfigOfType(configOverrides: Seq[Config]): ConfigType
|
||||
|
||||
/** List of user-provided configs that should
|
||||
* override defaults
|
||||
|
@ -88,9 +88,23 @@ abstract class AppConfig extends BitcoinSLogger {
|
|||
logger.debug(oldConfStr)
|
||||
}
|
||||
|
||||
val newConf = newConfigOfType(
|
||||
configOverrides = List(firstOverride) ++ configs
|
||||
)
|
||||
val configOverrides = firstOverride +: configs
|
||||
if (logger.isTraceEnabled()) {
|
||||
configOverrides.zipWithIndex.foreach {
|
||||
case (c, idx) => logger.trace(s"Override no. $idx: ${c.asReadableJson}")
|
||||
}
|
||||
}
|
||||
val newConf = {
|
||||
// the idea here is that after resolving the configuration,
|
||||
// we extract the value under the 'bitcoin-s' key and use
|
||||
// that as our config. here we have to do the reverse, to
|
||||
// get the keys to resolve correctly
|
||||
val reconstructedStr = s"""
|
||||
bitcoin-s: ${this.config.asReadableJson}
|
||||
"""
|
||||
val reconstructed = ConfigFactory.parseString(reconstructedStr)
|
||||
newConfigOfType(reconstructed +: configOverrides)
|
||||
}
|
||||
|
||||
// to avoid non-necessary lazy load
|
||||
if (logger.isDebugEnabled()) {
|
||||
|
@ -229,7 +243,8 @@ abstract class AppConfig extends BitcoinSLogger {
|
|||
.reduce(_.withFallback(_))
|
||||
|
||||
val interestingOverrides = overrides.getConfig("bitcoin-s")
|
||||
logger.trace(s"User-overrides for bitcoin-s config:")
|
||||
logger.trace(
|
||||
s"${configOverrides.length} user-overrides for bitcoin-s config:")
|
||||
logger.trace(interestingOverrides.asReadableJson)
|
||||
|
||||
// to make the overrides actually override
|
||||
|
@ -282,7 +297,7 @@ object AppConfig extends BitcoinSLogger {
|
|||
*/
|
||||
private[bitcoins] def throwIfDefaultDatadir(config: AppConfig): Unit = {
|
||||
val datadirStr = config.datadir.toString()
|
||||
AppConfig.defaultDatadirRegex.findFirstMatchIn(datadirStr) match {
|
||||
defaultDatadirRegex.findFirstMatchIn(datadirStr) match {
|
||||
case None => () // pass
|
||||
case Some(_) =>
|
||||
val errMsg =
|
||||
|
@ -291,6 +306,8 @@ object AppConfig extends BitcoinSLogger {
|
|||
s"Your data directory is $datadirStr. This would cause tests to potentially",
|
||||
"overwrite your existing data, which you probably don't want."
|
||||
).mkString(" ")
|
||||
logger.error(errMsg)
|
||||
logger.error(s"Configuration: ${config.config.asReadableJson}")
|
||||
throw new RuntimeException(errMsg)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,11 +8,11 @@ import org.bitcoins.node.db.NodeDbManagement
|
|||
import scala.util.Failure
|
||||
import scala.util.Success
|
||||
|
||||
case class NodeAppConfig(confs: Config*) extends AppConfig {
|
||||
case class NodeAppConfig(private val confs: Config*) extends AppConfig {
|
||||
override val configOverrides: List[Config] = confs.toList
|
||||
override protected def moduleName: String = "node"
|
||||
override protected type ConfigType = NodeAppConfig
|
||||
override protected def newConfigOfType(configs: List[Config]): NodeAppConfig =
|
||||
override protected def newConfigOfType(configs: Seq[Config]): NodeAppConfig =
|
||||
NodeAppConfig(configs: _*)
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package org.bitcoins.wallet
|
||||
|
||||
import org.bitcoins.wallet.api.UnlockedWalletApi
|
||||
import org.bitcoins.wallet.util.BitcoinSWalletTest
|
||||
import org.scalatest.FutureOutcome
|
||||
import org.bitcoins.wallet.api.UnlockWalletError.BadPassword
|
||||
import org.bitcoins.wallet.api.UnlockWalletError.JsonParsingError
|
||||
import org.bitcoins.wallet.api.UnlockWalletSuccess
|
||||
import org.bitcoins.core.crypto.AesPassword
|
||||
import org.bitcoins.wallet.api.UnlockWalletError.MnemonicNotFound
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import org.bitcoins.core.protocol.P2PKHAddress
|
||||
import org.bitcoins.core.hd.HDPurposes
|
||||
|
||||
class LegacyWalletTest extends BitcoinSWalletTest {
|
||||
|
||||
override type FixtureParam = UnlockedWalletApi
|
||||
|
||||
override def withFixture(test: OneArgAsyncTest): FutureOutcome =
|
||||
withLegacyWallet(test)
|
||||
|
||||
it should "generate legacy addresses" in { wallet: UnlockedWalletApi =>
|
||||
for {
|
||||
addr <- wallet.getNewAddress()
|
||||
account <- wallet.getDefaultAccount()
|
||||
otherAddr <- wallet.getNewAddress()
|
||||
allAddrs <- wallet.listAddresses()
|
||||
} yield {
|
||||
assert(account.hdAccount.purpose == HDPurposes.Legacy)
|
||||
assert(allAddrs.forall(_.address.isInstanceOf[P2PKHAddress]))
|
||||
assert(allAddrs.length == 2)
|
||||
assert(allAddrs.exists(_.address == addr))
|
||||
assert(allAddrs.exists(_.address == otherAddr))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package org.bitcoins.wallet
|
||||
|
||||
import org.bitcoins.wallet.api.UnlockedWalletApi
|
||||
import org.bitcoins.wallet.util.BitcoinSWalletTest
|
||||
import org.scalatest.FutureOutcome
|
||||
import org.bitcoins.wallet.api.UnlockWalletError.BadPassword
|
||||
import org.bitcoins.wallet.api.UnlockWalletError.JsonParsingError
|
||||
import org.bitcoins.wallet.api.UnlockWalletSuccess
|
||||
import org.bitcoins.core.crypto.AesPassword
|
||||
import org.bitcoins.wallet.api.UnlockWalletError.MnemonicNotFound
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import org.bitcoins.core.protocol.P2PKHAddress
|
||||
import org.bitcoins.core.protocol.Bech32Address
|
||||
import org.bitcoins.core.hd.HDPurposes
|
||||
|
||||
class SegwitWalletTest extends BitcoinSWalletTest {
|
||||
|
||||
override type FixtureParam = UnlockedWalletApi
|
||||
|
||||
override def withFixture(test: OneArgAsyncTest): FutureOutcome = {
|
||||
withSegwitWallet(test)
|
||||
}
|
||||
|
||||
it should "generate segwit addresses" in { wallet: UnlockedWalletApi =>
|
||||
for {
|
||||
addr <- wallet.getNewAddress()
|
||||
account <- wallet.getDefaultAccount()
|
||||
otherAddr <- wallet.getNewAddress()
|
||||
allAddrs <- wallet.listAddresses()
|
||||
} yield {
|
||||
assert(account.hdAccount.purpose == HDPurposes.SegWit)
|
||||
assert(allAddrs.forall(_.address.isInstanceOf[Bech32Address]))
|
||||
assert(allAddrs.length == 2)
|
||||
assert(allAddrs.exists(_.address == addr))
|
||||
assert(allAddrs.exists(_.address == otherAddr))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,6 +8,8 @@ import com.typesafe.config.ConfigFactory
|
|||
import org.bitcoins.core.config.RegTest
|
||||
import org.bitcoins.core.config.MainNet
|
||||
import org.bitcoins.wallet.config.WalletAppConfig
|
||||
import java.nio.file.Paths
|
||||
import org.bitcoins.core.hd.HDPurposes
|
||||
|
||||
class WalletAppConfigTest extends BitcoinSUnitTest {
|
||||
val config = WalletAppConfig()
|
||||
|
@ -24,6 +26,40 @@ class WalletAppConfigTest extends BitcoinSUnitTest {
|
|||
assert(mainnet.network == MainNet)
|
||||
}
|
||||
|
||||
it should "not matter how the overrides are passed in" in {
|
||||
val dir = Paths.get("/", "bar", "biz")
|
||||
val overrider = ConfigFactory.parseString(s"""
|
||||
|bitcoin-s {
|
||||
| datadir = $dir
|
||||
| network = mainnet
|
||||
|}
|
||||
|""".stripMargin)
|
||||
|
||||
val throughConstuctor = WalletAppConfig(overrider)
|
||||
val throughWithOverrides = config.withOverrides(overrider)
|
||||
assert(throughWithOverrides.network == MainNet)
|
||||
assert(throughWithOverrides.network == throughConstuctor.network)
|
||||
|
||||
assert(throughWithOverrides.datadir.startsWith(dir))
|
||||
assert(throughWithOverrides.datadir == throughConstuctor.datadir)
|
||||
|
||||
}
|
||||
|
||||
it must "be overridable without screwing up other options" in {
|
||||
val dir = Paths.get("/", "foo", "bar")
|
||||
val otherConf = ConfigFactory.parseString(s"bitcoin-s.datadir = $dir")
|
||||
val thirdConf = ConfigFactory.parseString(
|
||||
s"bitcoin-s.wallet.defaultAccountType = nested-segwit")
|
||||
|
||||
val overriden = config.withOverrides(otherConf)
|
||||
|
||||
val twiceOverriden = overriden.withOverrides(thirdConf)
|
||||
|
||||
assert(overriden.datadir.startsWith(dir))
|
||||
assert(twiceOverriden.datadir.startsWith(dir))
|
||||
assert(twiceOverriden.defaultAccountKind == HDPurposes.NestedSegWit)
|
||||
}
|
||||
|
||||
it must "be overridable with multiple levels" in {
|
||||
val testnet = ConfigFactory.parseString("bitcoin-s.network = testnet3")
|
||||
val mainnet = ConfigFactory.parseString("bitcoin-s.network = mainnet")
|
||||
|
|
|
@ -23,14 +23,11 @@ class WalletUnitTest extends BitcoinSWalletTest {
|
|||
accounts <- wallet.listAccounts()
|
||||
addresses <- wallet.listAddresses()
|
||||
} yield {
|
||||
assert(accounts.length == 1)
|
||||
assert(accounts.length == 3) // legacy, segwit and nested segwit
|
||||
assert(addresses.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
// eventually this test should NOT succeed, as BIP44
|
||||
// requires a limit to addresses being generated when
|
||||
// they haven't received any funds
|
||||
it should "generate addresses" in { wallet: UnlockedWalletApi =>
|
||||
for {
|
||||
addr <- wallet.getNewAddress()
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package org.bitcoins.wallet.models
|
||||
|
||||
import org.bitcoins.core.currency.Bitcoins
|
||||
import org.bitcoins.core.currency._
|
||||
import org.bitcoins.core.protocol.transaction.{
|
||||
TransactionOutPoint,
|
||||
TransactionOutput
|
||||
}
|
||||
import org.bitcoins.wallet.fixtures.UtxoDAOFixture
|
||||
import org.bitcoins.wallet.util.{BitcoinSWalletTest, WalletTestUtil}
|
||||
import org.bitcoins.wallet.Wallet
|
||||
import org.bitcoins.wallet.util.WalletTestUtil
|
||||
import org.bitcoins.wallet.util.BitcoinSWalletTest
|
||||
|
||||
class UTXOSpendingInfoDAOTest extends BitcoinSWalletTest with UtxoDAOFixture {
|
||||
behavior of "UTXOSpendingInfoDAO"
|
||||
|
@ -14,11 +16,11 @@ class UTXOSpendingInfoDAOTest extends BitcoinSWalletTest with UtxoDAOFixture {
|
|||
it should "insert a segwit UTXO and read it" in { utxoDAO =>
|
||||
val outpoint =
|
||||
TransactionOutPoint(WalletTestUtil.sampleTxid, WalletTestUtil.sampleVout)
|
||||
val output = TransactionOutput(Bitcoins.one, WalletTestUtil.sampleSPK)
|
||||
val output = TransactionOutput(1.bitcoin, WalletTestUtil.sampleSPK)
|
||||
val scriptWitness = WalletTestUtil.sampleScriptWitness
|
||||
val privkeyPath = WalletTestUtil.sampleSegwitPath
|
||||
val utxo =
|
||||
SegWitUTOXSpendingInfodb(
|
||||
NativeV0UTXOSpendingInfoDb(
|
||||
id = None,
|
||||
outPoint = outpoint,
|
||||
output = output,
|
||||
|
@ -31,8 +33,19 @@ class UTXOSpendingInfoDAOTest extends BitcoinSWalletTest with UtxoDAOFixture {
|
|||
} yield assert(read.contains(created))
|
||||
}
|
||||
|
||||
it should "insert a legacy UTXO and read it" ignore { _ =>
|
||||
???
|
||||
it should "insert a legacy UTXO and read it" in { utxoDAO =>
|
||||
val outpoint =
|
||||
TransactionOutPoint(WalletTestUtil.sampleTxid, WalletTestUtil.sampleVout)
|
||||
val output = TransactionOutput(1.bitcoin, WalletTestUtil.sampleSPK)
|
||||
val privKeyPath = WalletTestUtil.sampleLegacyPath
|
||||
val utxo = LegacyUTXOSpendingInfoDb(id = None,
|
||||
outPoint = outpoint,
|
||||
output = output,
|
||||
privKeyPath = privKeyPath)
|
||||
for {
|
||||
created <- utxoDAO.create(utxo)
|
||||
read <- utxoDAO.read(created.id.get)
|
||||
} yield assert(read.contains(created))
|
||||
}
|
||||
|
||||
it should "insert a nested segwit UTXO and read it" ignore { _ =>
|
||||
|
|
|
@ -23,6 +23,8 @@ import scala.concurrent.{ExecutionContext, Future}
|
|||
import org.bitcoins.db.AppConfig
|
||||
import org.bitcoins.testkit.BitcoinSAppConfig
|
||||
import org.bitcoins.testkit.BitcoinSAppConfig._
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
|
||||
trait BitcoinSWalletTest
|
||||
extends fixture.AsyncFlatSpec
|
||||
|
@ -57,21 +59,64 @@ trait BitcoinSWalletTest
|
|||
.map(_ => ())
|
||||
}
|
||||
|
||||
def createNewWallet(): Future[UnlockedWalletApi] = {
|
||||
|
||||
for {
|
||||
_ <- config.initialize()
|
||||
wallet <- Wallet.initialize().map {
|
||||
case InitializeWalletSuccess(wallet) => wallet
|
||||
case err: InitializeWalletError =>
|
||||
logger.error(s"Could not initialize wallet: $err")
|
||||
fail(err)
|
||||
/** Returns a function that can be used to create a wallet fixture.
|
||||
* If you pass in a configuration to this method that configuration
|
||||
* is given to the wallet as user-provided overrides. You could for
|
||||
* example use this to override the default data directory, network
|
||||
* or account type.
|
||||
*/
|
||||
private def createNewWallet(
|
||||
extraConfig: Option[Config]): () => Future[UnlockedWalletApi] =
|
||||
() => {
|
||||
val defaultConf = config.walletConf
|
||||
val walletConfig = extraConfig match {
|
||||
case None => defaultConf
|
||||
case Some(c) => defaultConf.withOverrides(c)
|
||||
}
|
||||
} yield wallet
|
||||
|
||||
// we want to check we're not overwriting
|
||||
// any user data
|
||||
AppConfig.throwIfDefaultDatadir(walletConfig)
|
||||
|
||||
walletConfig.initialize().flatMap { _ =>
|
||||
Wallet
|
||||
.initialize()(implicitly[ExecutionContext], walletConfig)
|
||||
.map {
|
||||
case InitializeWalletSuccess(wallet) => wallet
|
||||
case err: InitializeWalletError =>
|
||||
logger.error(s"Could not initialize wallet: $err")
|
||||
fail(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates a wallet with the default configuration */
|
||||
private def createDefaultWallet(): Future[UnlockedWalletApi] =
|
||||
createNewWallet(None)() // get the standard config
|
||||
|
||||
/** Lets you customize the parameters for the created wallet */
|
||||
val withNewConfiguredWallet: Config => OneArgAsyncTest => FutureOutcome =
|
||||
walletConfig =>
|
||||
makeDependentFixture(build = createNewWallet(Some(walletConfig)),
|
||||
destroy = destroyWallet)
|
||||
|
||||
/** Fixture for an initialized wallet which produce legacy addresses */
|
||||
def withLegacyWallet(test: OneArgAsyncTest): FutureOutcome = {
|
||||
val confOverride =
|
||||
ConfigFactory.parseString("bitcoin-s.wallet.defaultAccountType = legacy")
|
||||
withNewConfiguredWallet(confOverride)(test)
|
||||
}
|
||||
|
||||
/** Fixture for an initialized wallet which produce segwit addresses */
|
||||
def withSegwitWallet(test: OneArgAsyncTest): FutureOutcome = {
|
||||
val confOverride =
|
||||
ConfigFactory.parseString("bitcoin-s.wallet.defaultAccountType = segwit")
|
||||
withNewConfiguredWallet(confOverride)(test)
|
||||
}
|
||||
|
||||
def withNewWallet(test: OneArgAsyncTest): FutureOutcome =
|
||||
makeDependentFixture(build = createNewWallet, destroy = destroyWallet)(test)
|
||||
makeDependentFixture(build = createDefaultWallet, destroy = destroyWallet)(
|
||||
test)
|
||||
|
||||
case class WalletWithBitcoind(
|
||||
wallet: UnlockedWalletApi,
|
||||
|
@ -96,7 +141,7 @@ trait BitcoinSWalletTest
|
|||
|
||||
def withNewWalletAndBitcoind(test: OneArgAsyncTest): FutureOutcome = {
|
||||
val builder: () => Future[WalletWithBitcoind] = composeBuildersAndWrap(
|
||||
createNewWallet,
|
||||
createDefaultWallet,
|
||||
createWalletWithBitcoind,
|
||||
(_: UnlockedWalletApi, walletWithBitcoind: WalletWithBitcoind) =>
|
||||
walletWithBitcoind
|
||||
|
|
|
@ -48,6 +48,12 @@ object WalletTestUtil {
|
|||
HDChainType.External,
|
||||
addressIndex = 0)
|
||||
|
||||
/** Sample legacy HD path */
|
||||
lazy val sampleLegacyPath = LegacyHDPath(hdCoinType,
|
||||
accountIndex = 0,
|
||||
HDChainType.Change,
|
||||
addressIndex = 0)
|
||||
|
||||
def freshXpub: ExtPublicKey =
|
||||
CryptoGenerators.extPublicKey.sample.getOrElse(freshXpub)
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ abstract class LockedWallet extends LockedWalletApi with BitcoinSLogger {
|
|||
case MainNetChainParams => HDCoinType.Bitcoin
|
||||
case RegTestNetChainParams | TestNetChainParams => HDCoinType.Testnet
|
||||
}
|
||||
HDCoin(Wallet.DEFAULT_HD_PURPOSE, coinType)
|
||||
HDCoin(walletConfig.defaultAccountKind, coinType)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -88,6 +88,7 @@ abstract class LockedWallet extends LockedWalletApi with BitcoinSLogger {
|
|||
case Failure(_) => Future.successful(Left(BadSPK))
|
||||
}
|
||||
|
||||
/** Constructs a DB level representation of the given UTXO, and persist it to disk */
|
||||
private def writeUtxo(
|
||||
output: TransactionOutput,
|
||||
outPoint: TransactionOutPoint,
|
||||
|
@ -95,16 +96,21 @@ abstract class LockedWallet extends LockedWalletApi with BitcoinSLogger {
|
|||
|
||||
val utxo: UTXOSpendingInfoDb = addressDb match {
|
||||
case segwitAddr: SegWitAddressDb =>
|
||||
SegWitUTOXSpendingInfodb(
|
||||
NativeV0UTXOSpendingInfoDb(
|
||||
id = None,
|
||||
outPoint = outPoint,
|
||||
output = output,
|
||||
privKeyPath = segwitAddr.path,
|
||||
scriptWitness = segwitAddr.witnessScript
|
||||
)
|
||||
case otherAddr @ (_: LegacyAddressDb | _: NestedSegWitAddressDb) =>
|
||||
case LegacyAddressDb(path, _, _, _) =>
|
||||
LegacyUTXOSpendingInfoDb(id = None,
|
||||
outPoint = outPoint,
|
||||
output = output,
|
||||
privKeyPath = path)
|
||||
case nested: NestedSegWitAddressDb =>
|
||||
throw new IllegalArgumentException(
|
||||
s"Bad utxo $otherAddr. Note: Only Segwit is implemented")
|
||||
s"Bad utxo $nested. Note: nested segwit is not implemented")
|
||||
}
|
||||
|
||||
utxoDAO.create(utxo).map { written =>
|
||||
|
@ -201,27 +207,34 @@ abstract class LockedWallet extends LockedWalletApi with BitcoinSLogger {
|
|||
address.toPath
|
||||
}
|
||||
|
||||
val addressDb =
|
||||
val addressDb = {
|
||||
val pathDiff =
|
||||
account.hdAccount.diff(addrPath) match {
|
||||
case Some(value) => value
|
||||
case None =>
|
||||
throw new RuntimeException(
|
||||
s"Could not diff ${account.hdAccount} and $addrPath")
|
||||
}
|
||||
|
||||
val pubkey = account.xpub.deriveChildPubKey(pathDiff) match {
|
||||
case Failure(exception) => throw exception
|
||||
case Success(value) => value.key
|
||||
}
|
||||
|
||||
addrPath match {
|
||||
case segwitPath: SegWitHDPath =>
|
||||
val pathDiff = account.hdAccount.diff(segwitPath) match {
|
||||
case Some(value) => value
|
||||
case None =>
|
||||
throw new RuntimeException(
|
||||
s"Could not diff ${account.hdAccount} and $segwitPath")
|
||||
}
|
||||
|
||||
val pubkey = account.xpub.deriveChildPubKey(pathDiff) match {
|
||||
case Failure(exception) => throw exception
|
||||
case Success(value) => value.key
|
||||
}
|
||||
|
||||
AddressDbHelper
|
||||
.getP2WPKHAddress(pubkey, segwitPath, networkParameters)
|
||||
case _: HDPath =>
|
||||
throw new IllegalArgumentException(
|
||||
"P2PKH and nested segwit P2PKH not yet implemented")
|
||||
.getSegwitAddress(pubkey, segwitPath, networkParameters)
|
||||
case legacyPath: LegacyHDPath =>
|
||||
AddressDbHelper.getLegacyAddress(pubkey,
|
||||
legacyPath,
|
||||
networkParameters)
|
||||
case nestedPath: NestedSegWitHDPath =>
|
||||
AddressDbHelper.getNestedSegwitAddress(pubkey,
|
||||
nestedPath,
|
||||
networkParameters)
|
||||
}
|
||||
}
|
||||
val writeF = addressDAO.create(addressDb)
|
||||
writeF.foreach { written =>
|
||||
logger.info(
|
||||
|
|
|
@ -62,12 +62,15 @@ sealed abstract class Wallet
|
|||
.get
|
||||
.toUTXOSpendingInfo(fromAccount, seed))
|
||||
|
||||
logger.info(s"Spending UTXOs: ${utxos
|
||||
.map { utxo =>
|
||||
import utxo.outPoint
|
||||
s"${outPoint.txId.hex}:${outPoint.vout.toInt}"
|
||||
}
|
||||
.mkString(", ")}")
|
||||
logger.info({
|
||||
val utxosStr = utxos
|
||||
.map { utxo =>
|
||||
import utxo.outPoint
|
||||
s"${outPoint.txId.hex}:${outPoint.vout.toInt}"
|
||||
}
|
||||
.mkString(", ")
|
||||
s"Spending UTXOs: $utxosStr"
|
||||
})
|
||||
|
||||
utxos.zipWithIndex.foreach {
|
||||
case (utxo, index) =>
|
||||
|
@ -100,11 +103,6 @@ sealed abstract class Wallet
|
|||
// todo: create multiple wallets, need to maintain multiple databases
|
||||
object Wallet extends CreateWalletApi with BitcoinSLogger {
|
||||
|
||||
// The default HD purpose of the bitcoin-s wallet. Can be
|
||||
// one of segwit, nested segwit or legacy. Hard coded for
|
||||
// now, could be make configurable in the future
|
||||
private[wallet] val DEFAULT_HD_PURPOSE: HDPurpose = HDPurposes.SegWit
|
||||
|
||||
private case class WalletImpl(
|
||||
mnemonicCode: MnemonicCode
|
||||
)(
|
||||
|
@ -168,24 +166,33 @@ object Wallet extends CreateWalletApi with BitcoinSLogger {
|
|||
encrypted <- encryptedMnemonicE
|
||||
} yield {
|
||||
val wallet = WalletImpl(mnemonic)
|
||||
val coin =
|
||||
HDCoin(DEFAULT_HD_PURPOSE, HDUtil.getCoinType(config.network))
|
||||
val account = HDAccount(coin, 0)
|
||||
val xpriv = wallet.xprivForPurpose(DEFAULT_HD_PURPOSE)
|
||||
|
||||
// safe since we're deriving from a priv
|
||||
val xpub = xpriv.deriveChildPubKey(account).get
|
||||
val accountDb = AccountDb(xpub, account)
|
||||
|
||||
val mnemonicPath =
|
||||
WalletStorage.writeMnemonicToDisk(encrypted)
|
||||
logger.debug(s"Saved encrypted wallet mnemonic to $mnemonicPath")
|
||||
|
||||
for {
|
||||
_ <- config.initialize()
|
||||
_ <- wallet.accountDAO
|
||||
.create(accountDb)
|
||||
.map(_ => logger.trace(s"Saved account to DB"))
|
||||
_ = {
|
||||
val mnemonicPath =
|
||||
WalletStorage.writeMnemonicToDisk(encrypted)
|
||||
logger.debug(s"Saved encrypted wallet mnemonic to $mnemonicPath")
|
||||
}
|
||||
_ <- {
|
||||
// We want to make sure all level 0 accounts are created,
|
||||
// so the user can change the default account kind later
|
||||
// and still have their wallet work
|
||||
val createAccountFutures =
|
||||
HDPurposes.all.map(createRootAccount(wallet, _))
|
||||
|
||||
val accountCreationF = Future.sequence(createAccountFutures)
|
||||
|
||||
accountCreationF.foreach(_ =>
|
||||
logger.debug(s"Created root level accounts for wallet"))
|
||||
|
||||
accountCreationF.failed.foreach { err =>
|
||||
logger.error(s"Failed to create root level accounts: $err")
|
||||
}
|
||||
|
||||
accountCreationF
|
||||
}
|
||||
|
||||
} yield wallet
|
||||
}
|
||||
|
||||
|
@ -199,4 +206,26 @@ object Wallet extends CreateWalletApi with BitcoinSLogger {
|
|||
case Left(err) => err
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates the level 0 account for the given HD purpose */
|
||||
private def createRootAccount(wallet: Wallet, purpose: HDPurpose)(
|
||||
implicit config: WalletAppConfig,
|
||||
ec: ExecutionContext): Future[AccountDb] = {
|
||||
val coin =
|
||||
HDCoin(purpose, HDUtil.getCoinType(config.network))
|
||||
val account = HDAccount(coin, 0)
|
||||
val xpriv = wallet.xprivForPurpose(purpose)
|
||||
// safe since we're deriving from a priv
|
||||
val xpub = xpriv.deriveChildPubKey(account).get
|
||||
val accountDb = AccountDb(xpub, account)
|
||||
|
||||
logger.debug(s"Creating account with constant prefix $purpose")
|
||||
wallet.accountDAO
|
||||
.create(accountDb)
|
||||
.map { written =>
|
||||
logger.debug(s"Saved account with constant prefix $purpose to DB")
|
||||
written
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,14 +7,26 @@ import org.bitcoins.wallet.db.WalletDbManagement
|
|||
import scala.util.Failure
|
||||
import scala.util.Success
|
||||
import java.nio.file.Files
|
||||
import org.bitcoins.core.hd.HDPurpose
|
||||
import org.bitcoins.core.hd.HDPurposes
|
||||
|
||||
case class WalletAppConfig(conf: Config*) extends AppConfig {
|
||||
case class WalletAppConfig(private val conf: Config*) extends AppConfig {
|
||||
override val configOverrides: List[Config] = conf.toList
|
||||
override def moduleName: String = "wallet"
|
||||
override type ConfigType = WalletAppConfig
|
||||
override def newConfigOfType(configs: List[Config]): WalletAppConfig =
|
||||
override def newConfigOfType(configs: Seq[Config]): WalletAppConfig =
|
||||
WalletAppConfig(configs: _*)
|
||||
|
||||
lazy val defaultAccountKind: HDPurpose =
|
||||
config.getString("wallet.defaultAccountType") match {
|
||||
case "legacy" => HDPurposes.Legacy
|
||||
case "segwit" => HDPurposes.SegWit
|
||||
case "nested-segwit" => HDPurposes.NestedSegWit
|
||||
// todo: validate this pre-app startup
|
||||
case other: String =>
|
||||
throw new RuntimeException(s"$other is not a valid account type!")
|
||||
}
|
||||
|
||||
override def initialize()(implicit ec: ExecutionContext): Future[Unit] = {
|
||||
logger.debug(s"Initializing wallet setup")
|
||||
|
||||
|
|
|
@ -46,5 +46,5 @@ class AccountTable(tag: Tag) extends Table[AccountDb](tag, "wallet_accounts") {
|
|||
(purpose, xpub, coinType, index) <> (fromTuple, toTuple)
|
||||
|
||||
def primaryKey: PrimaryKey =
|
||||
primaryKey("pk_account", (coinType, index))
|
||||
primaryKey("pk_account", sourceColumns = (purpose, coinType, index))
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import slick.jdbc.SQLiteProfile.api._
|
|||
import slick.lifted.ProvenShape
|
||||
import org.bitcoins.core.protocol.P2SHAddress
|
||||
import org.bitcoins.core.protocol.P2PKHAddress
|
||||
import org.bitcoins.core.protocol.script.P2PKHScriptPubKey
|
||||
|
||||
sealed trait AddressDb {
|
||||
protected type PathType <: HDPath
|
||||
|
@ -71,7 +72,7 @@ case class LegacyAddressDb(
|
|||
object AddressDbHelper {
|
||||
|
||||
/** Get a Segwit pay-to-pubkeyhash address */
|
||||
def getP2WPKHAddress(
|
||||
def getSegwitAddress(
|
||||
pub: ECPublicKey,
|
||||
path: SegWitHDPath,
|
||||
np: NetworkParameters): SegWitAddressDb = {
|
||||
|
@ -87,6 +88,27 @@ object AddressDbHelper {
|
|||
witnessScript = scriptWitness
|
||||
)
|
||||
}
|
||||
|
||||
/** Get a legacy pay-to-pubkeyhash address */
|
||||
def getLegacyAddress(
|
||||
pub: ECPublicKey,
|
||||
path: LegacyHDPath,
|
||||
np: NetworkParameters): LegacyAddressDb = {
|
||||
val spk = P2PKHScriptPubKey(pub)
|
||||
val addr = P2PKHAddress(spk, np)
|
||||
LegacyAddressDb(path = path,
|
||||
ecPublicKey = pub,
|
||||
hashedPubKey = spk.pubKeyHash,
|
||||
address = addr)
|
||||
}
|
||||
|
||||
/** Get a nested Segwit pay-to-pubkeyhash address */
|
||||
def getNestedSegwitAddress(
|
||||
pub: ECPublicKey,
|
||||
path: NestedSegWitHDPath,
|
||||
np: NetworkParameters): NestedSegWitAddressDb = {
|
||||
???
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -160,6 +182,14 @@ class AddressTable(tag: Tag) extends Table[AddressDb](tag, "addresses") {
|
|||
hashedPubKey = hashedPubKey,
|
||||
address = bechAddr,
|
||||
witnessScript = scriptWitness)
|
||||
|
||||
case (HDPurposes.Legacy, legacyAddr: P2PKHAddress, None) =>
|
||||
val path = LegacyHDPath(coinType = accountCoin,
|
||||
accountIndex = accountIndex,
|
||||
chainType = accountChain,
|
||||
addressIndex = addressIndex)
|
||||
LegacyAddressDb(path, pubKey, hashedPubKey, legacyAddr)
|
||||
|
||||
case (purpose: HDPurpose, address: BitcoinAddress, scriptWitnessOpt) =>
|
||||
throw new IllegalArgumentException(
|
||||
s"Got invalid combination of HD purpose, address and script witness: $purpose, $address, $scriptWitnessOpt" +
|
||||
|
@ -180,7 +210,21 @@ class AddressTable(tag: Tag) extends Table[AddressDb](tag, "addresses") {
|
|||
pubKey,
|
||||
hashedPubKey,
|
||||
ScriptType.WITNESS_V0_KEYHASH))
|
||||
case other => throw new RuntimeException(s"$other is not implemented yet")
|
||||
case LegacyAddressDb(path, pubkey, hashedPub, address) =>
|
||||
Some(
|
||||
path.purpose,
|
||||
path.account.index,
|
||||
path.coin.coinType,
|
||||
path.chain.chainType,
|
||||
address,
|
||||
None, // scriptwitness
|
||||
path.address.index,
|
||||
pubkey,
|
||||
hashedPub,
|
||||
ScriptType.PUBKEYHASH
|
||||
)
|
||||
case _: NestedSegWitAddressDb =>
|
||||
throw new RuntimeException(s"Nested segwit is not implemented yet!")
|
||||
|
||||
}
|
||||
|
||||
|
@ -200,6 +244,9 @@ class AddressTable(tag: Tag) extends Table[AddressDb](tag, "addresses") {
|
|||
|
||||
// for some reason adding a type annotation here causes compile error
|
||||
def fk =
|
||||
foreignKey("fk_account", (accountCoin, accountIndex), accounts)(
|
||||
accountTable => (accountTable.coinType, accountTable.index))
|
||||
foreignKey("fk_account",
|
||||
sourceColumns = (purpose, accountCoin, accountIndex),
|
||||
targetTableQuery = accounts) { accountTable =>
|
||||
(accountTable.purpose, accountTable.coinType, accountTable.index)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,26 +18,51 @@ import org.bitcoins.core.hd.SegWitHDPath
|
|||
import org.bitcoins.core.crypto.BIP39Seed
|
||||
import org.bitcoins.core.util.BitcoinSLogger
|
||||
import org.bitcoins.core.hd.LegacyHDPath
|
||||
import org.bitcoins.core.hd.NestedSegWitHDPath
|
||||
|
||||
case class SegWitUTOXSpendingInfodb(
|
||||
/**
|
||||
* DB representation of a native V0
|
||||
* SegWit UTXO
|
||||
*/
|
||||
case class NativeV0UTXOSpendingInfoDb(
|
||||
id: Option[Long],
|
||||
outPoint: TransactionOutPoint,
|
||||
output: TransactionOutput,
|
||||
privKeyPath: SegWitHDPath,
|
||||
scriptWitness: ScriptWitness
|
||||
) extends UTXOSpendingInfoDb {
|
||||
override def redeemScriptOpt: Option[ScriptPubKey] = None
|
||||
override def scriptWitnessOpt: Option[ScriptWitness] = Some(scriptWitness)
|
||||
override val redeemScriptOpt: Option[ScriptPubKey] = None
|
||||
override val scriptWitnessOpt: Option[ScriptWitness] = Some(scriptWitness)
|
||||
|
||||
override type PathType = SegWitHDPath
|
||||
|
||||
override def copyWithId(id: Long): SegWitUTOXSpendingInfodb =
|
||||
override def copyWithId(id: Long): NativeV0UTXOSpendingInfoDb =
|
||||
copy(id = Some(id))
|
||||
}
|
||||
|
||||
case class LegacyUTXOSpendingInfoDb(
|
||||
id: Option[Long],
|
||||
outPoint: TransactionOutPoint,
|
||||
output: TransactionOutput,
|
||||
privKeyPath: LegacyHDPath
|
||||
) extends UTXOSpendingInfoDb {
|
||||
override val redeemScriptOpt: Option[ScriptPubKey] = None
|
||||
override def scriptWitnessOpt: Option[ScriptWitness] = None
|
||||
|
||||
override type PathType = LegacyHDPath
|
||||
|
||||
override def copyWithId(id: Long): LegacyUTXOSpendingInfoDb =
|
||||
copy(id = Some(id))
|
||||
}
|
||||
|
||||
// TODO add case for nested segwit
|
||||
// and legacy
|
||||
/**
|
||||
* The database level representation of a UTXO.
|
||||
* When storing a UTXO we don't want to store
|
||||
* sensitive material such as private keys.
|
||||
* We instead store the necessary information
|
||||
* we need to derive the private keys, given
|
||||
* the root wallet seed.
|
||||
*/
|
||||
sealed trait UTXOSpendingInfoDb
|
||||
extends DbRowAutoInc[UTXOSpendingInfoDb]
|
||||
with BitcoinSLogger {
|
||||
|
@ -55,6 +80,9 @@ sealed trait UTXOSpendingInfoDb
|
|||
|
||||
def value: CurrencyUnit = output.value
|
||||
|
||||
/** Converts a non-sensitive DB representation of a UTXO into
|
||||
* a signable (and sensitive) real-world UTXO
|
||||
*/
|
||||
def toUTXOSpendingInfo(
|
||||
account: AccountDb,
|
||||
walletSeed: BIP39Seed): BitcoinUTXOSpendingInfo = {
|
||||
|
@ -114,20 +142,22 @@ case class UTXOSpendingInfoTable(tag: Tag)
|
|||
outpoint,
|
||||
output,
|
||||
path: SegWitHDPath,
|
||||
None,
|
||||
None, // ReedemScript
|
||||
Some(scriptWitness)) =>
|
||||
SegWitUTOXSpendingInfodb(id, outpoint, output, path, scriptWitness)
|
||||
.asInstanceOf[UTXOSpendingInfoDb]
|
||||
NativeV0UTXOSpendingInfoDb(id, outpoint, output, path, scriptWitness)
|
||||
|
||||
case (id,
|
||||
outpoint,
|
||||
output,
|
||||
path @ (_: LegacyHDPath | _: NestedSegWitHDPath),
|
||||
spkOpt,
|
||||
swOpt) =>
|
||||
path: LegacyHDPath,
|
||||
None, // RedeemScript
|
||||
None // ScriptWitness
|
||||
) =>
|
||||
LegacyUTXOSpendingInfoDb(id, outpoint, output, path)
|
||||
case (id, outpoint, output, path, spkOpt, swOpt) =>
|
||||
throw new IllegalArgumentException(
|
||||
"Could not construct UtxoSpendingInfoDb from bad tuple:"
|
||||
+ s" ($id, $outpoint, $output, $path, $spkOpt, $swOpt) . Note: Only Segwit is implemented")
|
||||
+ s" ($id, $outpoint, $output, $path, $spkOpt, $swOpt) . Note: Nested Segwit is not implemented")
|
||||
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue