diff --git a/chain/src/main/scala/org/bitcoins/chain/config/ChainAppConfig.scala b/chain/src/main/scala/org/bitcoins/chain/config/ChainAppConfig.scala index 36db487993..8a24b07499 100644 --- a/chain/src/main/scala/org/bitcoins/chain/config/ChainAppConfig.scala +++ b/chain/src/main/scala/org/bitcoins/chain/config/ChainAppConfig.scala @@ -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 diff --git a/db-commons/src/main/resources/reference.conf b/db-commons/src/main/resources/reference.conf index 2053250f39..216b124d39 100644 --- a/db-commons/src/main/resources/reference.conf +++ b/db-commons/src/main/resources/reference.conf @@ -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 + } } \ No newline at end of file diff --git a/db-commons/src/main/scala/org/bitcoins/db/AppConfig.scala b/db-commons/src/main/scala/org/bitcoins/db/AppConfig.scala index 0bd1592c56..531275fade 100644 --- a/db-commons/src/main/scala/org/bitcoins/db/AppConfig.scala +++ b/db-commons/src/main/scala/org/bitcoins/db/AppConfig.scala @@ -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) } } diff --git a/node/src/main/scala/org/bitcoins/node/config/NodeAppConfig.scala b/node/src/main/scala/org/bitcoins/node/config/NodeAppConfig.scala index 0daaee94ff..53eb41a1e9 100644 --- a/node/src/main/scala/org/bitcoins/node/config/NodeAppConfig.scala +++ b/node/src/main/scala/org/bitcoins/node/config/NodeAppConfig.scala @@ -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: _*) /** diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/LegacyWalletTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/LegacyWalletTest.scala new file mode 100644 index 0000000000..467496ae88 --- /dev/null +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/LegacyWalletTest.scala @@ -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)) + } + } +} diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/SegwitWalletTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/SegwitWalletTest.scala new file mode 100644 index 0000000000..34e0001465 --- /dev/null +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/SegwitWalletTest.scala @@ -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)) + } + } +} diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletAppConfigTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletAppConfigTest.scala index ea24515366..96a0de512d 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletAppConfigTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletAppConfigTest.scala @@ -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") diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala index e948c3f085..f492ec513d 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala @@ -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() diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/models/UTXOSpendingInfoDAOTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/models/UTXOSpendingInfoDAOTest.scala index e7943f0b59..06bbe0eb51 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/models/UTXOSpendingInfoDAOTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/models/UTXOSpendingInfoDAOTest.scala @@ -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 { _ => diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/util/BitcoinSWalletTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/util/BitcoinSWalletTest.scala index 81009e080d..26c7398fdd 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/util/BitcoinSWalletTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/util/BitcoinSWalletTest.scala @@ -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 diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/util/WalletTestUtil.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/util/WalletTestUtil.scala index 4f901bc1bc..c0bead76cc 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/util/WalletTestUtil.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/util/WalletTestUtil.scala @@ -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) diff --git a/wallet/src/main/scala/org/bitcoins/wallet/LockedWallet.scala b/wallet/src/main/scala/org/bitcoins/wallet/LockedWallet.scala index ae5421cb20..337c03201d 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/LockedWallet.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/LockedWallet.scala @@ -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( diff --git a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala index 3e5c3ce480..d56c1924df 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala @@ -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 + } + + } } diff --git a/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala b/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala index 3d640c1d9a..bb54d500e1 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala @@ -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") diff --git a/wallet/src/main/scala/org/bitcoins/wallet/models/AccountTable.scala b/wallet/src/main/scala/org/bitcoins/wallet/models/AccountTable.scala index 05c07bfcd6..ff97f64ddb 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/models/AccountTable.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/models/AccountTable.scala @@ -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)) } diff --git a/wallet/src/main/scala/org/bitcoins/wallet/models/AddressTable.scala b/wallet/src/main/scala/org/bitcoins/wallet/models/AddressTable.scala index ab9cc5032e..2dc0f43743 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/models/AddressTable.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/models/AddressTable.scala @@ -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) + } } diff --git a/wallet/src/main/scala/org/bitcoins/wallet/models/UTXOSpendingInfoTable.scala b/wallet/src/main/scala/org/bitcoins/wallet/models/UTXOSpendingInfoTable.scala index dd194b4965..b60decc200 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/models/UTXOSpendingInfoTable.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/models/UTXOSpendingInfoTable.scala @@ -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") }