diff --git a/testkit/src/main/scala/org/bitcoins/testkit/wallet/WalletTestUtil.scala b/testkit/src/main/scala/org/bitcoins/testkit/wallet/WalletTestUtil.scala index ebf8f04e9f..e4f58a6303 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/wallet/WalletTestUtil.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/wallet/WalletTestUtil.scala @@ -14,11 +14,14 @@ import scodec.bits.HexStringSyntax import org.bitcoins.core.hd._ import org.bitcoins.core.protocol.script.ScriptWitness import org.bitcoins.core.protocol.script.P2WPKHWitnessV0 -import org.bitcoins.core.protocol.transaction.TransactionOutput -import org.bitcoins.core.protocol.transaction.TransactionOutPoint -import org.bitcoins.wallet.models.NativeV0UTXOSpendingInfoDb -import org.bitcoins.core.currency._ -import org.bitcoins.wallet.models.LegacyUTXOSpendingInfoDb +import org.bitcoins.core.protocol.transaction.Transaction +import org.bitcoins.testkit.core.gen.TransactionGenerators +import scala.concurrent.Future +import org.bitcoins.wallet.models.IncomingTransaction +import org.bitcoins.wallet.models.AddressDb +import org.bitcoins.wallet.models.AddressDbHelper +import org.bitcoins.testkit.fixtures.WalletDAOs +import scala.concurrent.ExecutionContext object WalletTestUtil { @@ -70,30 +73,62 @@ object WalletTestUtil { lazy val sampleSPK: ScriptPubKey = ScriptPubKey.fromAsmBytes(hex"001401b2ac67587e4b603bb3ad709a8102c30113892d") - lazy val sampleSegwitUtxo: NativeV0UTXOSpendingInfoDb = { - val outpoint = - TransactionOutPoint(WalletTestUtil.sampleTxid, WalletTestUtil.sampleVout) - val output = TransactionOutput(1.bitcoin, WalletTestUtil.sampleSPK) - val scriptWitness = WalletTestUtil.sampleScriptWitness - val privkeyPath = WalletTestUtil.sampleSegwitPath - NativeV0UTXOSpendingInfoDb(id = None, - outPoint = outpoint, - output = output, - privKeyPath = privkeyPath, - scriptWitness = scriptWitness, - incomingTxId = None) - } - - lazy val sampleLegacyUtxo = { - val outpoint = - TransactionOutPoint(WalletTestUtil.sampleTxid, WalletTestUtil.sampleVout) - val output = TransactionOutput(1.bitcoin, WalletTestUtil.sampleSPK) - val privKeyPath = WalletTestUtil.sampleLegacyPath - LegacyUTXOSpendingInfoDb(id = None, - outPoint = outpoint, - output = output, - privKeyPath = privKeyPath, - incomingTxId = None) - } lazy val sampleScriptWitness: ScriptWitness = P2WPKHWitnessV0(freshXpub.key) + + /** + * Inserts a incoming TX, and returns it with the address it was sent to + * + * This method also does some asserts on the result, to make sure what + * we're writing and reading matches up + */ + def insertIncomingTx(daos: WalletDAOs)(implicit ec: ExecutionContext): Future[ + (IncomingTransaction, AddressDb)] = { + val WalletDAOs(accountDAO, addressDAO, txDAO, _, _) = daos + + /** Get a TX with outputs */ + def getTx: Transaction = + TransactionGenerators.transaction + .suchThat(_.outputs.nonEmpty) + .sample + .getOrElse(getTx) + + val account = WalletTestUtil.firstAccountDb + + val address = { + val pub = ECPublicKey() + val path = + account.hdAccount + .toChain(HDChainType.External) + .toAddress(0) + .toPath + + AddressDbHelper.getAddress(pub, path, RegTest) + } + + val tx = getTx + val txDb = IncomingTransaction(tx, + confirmations = 3, + scriptPubKey = address.scriptPubKey) + for { + _ <- accountDAO.create(account) + _ <- addressDAO.create(address) + createdTx <- txDAO.create(txDb) + txAndAddr <- txDAO.withAddress(createdTx.transaction) + } yield + txAndAddr match { + case None => + throw new org.scalatest.exceptions.TestFailedException( + s"Couldn't read back TX with address from DB!", + 0) + case Some((foundTx, foundAddr)) => + assert(foundTx.confirmations == txDb.confirmations) + assert(foundTx.scriptPubKey == txDb.scriptPubKey) + assert(foundTx.transaction == txDb.transaction) + + assert(foundAddr == address) + + (foundTx, foundAddr) + } + + } } diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/models/IncomingTransactionDAOTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/models/IncomingTransactionDAOTest.scala index b2f48322b2..9e6019d87c 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/models/IncomingTransactionDAOTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/models/IncomingTransactionDAOTest.scala @@ -11,57 +11,14 @@ import org.bitcoins.wallet.config.WalletAppConfig import org.bouncycastle.crypto.tls.CertChainType import org.bitcoins.core.hd.HDChainType import org.bitcoins.core.hd.LegacyHDPath +import scala.concurrent.Future class IncomingTransactionDAOTest extends BitcoinSWalletTest with WalletDAOFixture { - private def getTx: Transaction = - TransactionGenerators.transaction - .suchThat(_.outputs.nonEmpty) - .sample - .getOrElse(getTx) it must "insert a incoming transaction and read it back with its address" in { daos => - val txDao = daos.incomingTxDAO - val addrDao = daos.addressDAO - implicit val walletconf: WalletAppConfig = config - val accountDAO = AccountDAO() - - val account = WalletTestUtil.firstAccountDb - - val address = { - val pub = ECPublicKey() - val path = - account.hdAccount - .toChain(HDChainType.External) - .toAddress(0) - .toPath - - AddressDbHelper.getAddress(pub, path, RegTest) - } - - val tx = getTx - val txDb = IncomingTransaction(tx, - confirmations = 3, - scriptPubKey = address.scriptPubKey) - import org.bitcoins.db.DbCommonsColumnMappers._ - for { - _ <- accountDAO.create(account) - createdAddress <- addrDao.create(address) - createdTx <- txDao.create(txDb) - txAndAddr <- txDao.withAddress(createdTx.transaction) - } yield { - txAndAddr match { - case None => fail(s"Couldn't read back TX with address from DB!") - case Some((foundTx, foundAddr)) => - // can't do just foundTx == txDb, ID's are different (None/Some(_)) - assert(foundTx.confirmations == txDb.confirmations) - assert(foundTx.scriptPubKey == txDb.scriptPubKey) - assert(foundTx.transaction == txDb.transaction) - - assert(foundAddr == address) - } - } + WalletTestUtil.insertIncomingTx(daos).map(_ => succeed) } } diff --git a/wallet/README.md b/wallet/README.md index 2ab5269073..feb75e9d0a 100644 --- a/wallet/README.md +++ b/wallet/README.md @@ -19,6 +19,26 @@ This is meant to be a stand alone project that can be used as a cold storage wal [BIP44/BIP49/BIP84 paths](../core/src/main/scala/org/bitcoins/core/hd/HDPath.scala) and script types, so that everything we need for spending the money sent to an address is derivable. +- **The wallet is a "dumb" wallet that acts mostly as a database of UTXOs, transactions and + addresses, with associated operations on these.** + The wallet module does very little verification of incoming data about transactions, + UTXOs and reorgs. We're aiming to write small, self contained modules, that can be + composed together into more fully fledged systems. That means the `chain` and `node` + modules does the actual verification of data we receive, and `wallet` just blindly + acts on this. This results in a design where you can swap out `node` for a Bitcoin Core + full node, use it with hardware wallets, or something else entirely. However, that also + means that users of `wallet` that doesn't want to use the other modules we provide have + to make sure that the data they are feeding the wallet is correct. + +#### Database structure + +We store information in the following tables: + +- UTXOs - must reference the incoming transaction it was received in +- Addresses - must reference the account it belongs to +- Accounts +- Incoming transactions - must reference the SPK (in our address table) that a TX spends to +- Outgoing transactions - must reference the UTXO(s) it spends #### Mnemonic encryption 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 cdc4111f0b..46f2af2b50 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/models/UTXOSpendingInfoTable.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/models/UTXOSpendingInfoTable.scala @@ -24,12 +24,12 @@ import org.bitcoins.core.hd.LegacyHDPath * SegWit UTXO */ case class NativeV0UTXOSpendingInfoDb( - id: Option[Long], + id: Option[Long] = None, outPoint: TransactionOutPoint, output: TransactionOutput, privKeyPath: SegWitHDPath, scriptWitness: ScriptWitness, - incomingTxId: Option[Long] + incomingTxId: Long ) extends UTXOSpendingInfoDb { override val redeemScriptOpt: Option[ScriptPubKey] = None override val scriptWitnessOpt: Option[ScriptWitness] = Some(scriptWitness) @@ -41,11 +41,11 @@ case class NativeV0UTXOSpendingInfoDb( } case class LegacyUTXOSpendingInfoDb( - id: Option[Long], + id: Option[Long] = None, outPoint: TransactionOutPoint, output: TransactionOutput, privKeyPath: LegacyHDPath, - incomingTxId: Option[Long] + incomingTxId: Long ) extends UTXOSpendingInfoDb { override val redeemScriptOpt: Option[ScriptPubKey] = None override def scriptWitnessOpt: Option[ScriptWitness] = None @@ -83,7 +83,7 @@ sealed trait UTXOSpendingInfoDb def value: CurrencyUnit = output.value /** The ID of the transaction this UTXO was received in */ - def incomingTxId: Option[Long] + def incomingTxId: Long /** Converts a non-sensitive DB representation of a UTXO into * a signable (and sensitive) real-world UTXO @@ -151,7 +151,7 @@ case class UTXOSpendingInfoTable(tag: Tag) HDPath, Option[ScriptPubKey], Option[ScriptWitness], - Option[Long] // incoming TX ID + Long // incoming TX ID ) private val fromTuple: UTXOTuple => UTXOSpendingInfoDb = { @@ -202,5 +202,5 @@ case class UTXOSpendingInfoTable(tag: Tag) privKeyPath, redeemScriptOpt, scriptWitnessOpt, - incomingTxId.?) <> (fromTuple, toTuple) + incomingTxId) <> (fromTuple, toTuple) }