Address Tagging Attempt 2 (#1320)

* Address & UTXO tagging

* Fix docs

* Remove useless function, improve docs

* Fix rebase errors

* Rebase fixes

* Fix docs

* Fix small test errors

* Fix Postgres migration

* Fix postgres
This commit is contained in:
Ben Carman 2020-07-08 14:38:39 -05:00 committed by GitHub
parent 18fe4da989
commit ecd3449100
28 changed files with 1046 additions and 68 deletions

View file

@ -0,0 +1,30 @@
package org.bitcoins.core.wallet.utxo
import org.bitcoins.testkit.util.BitcoinSUnitTest
class AddressTagTest extends BitcoinSUnitTest {
behavior of "AddressTag"
it must "read StorageLocationTag from string" in {
StorageLocationTag.fromString("HotStorage").get must be(
StorageLocationTag.HotStorage)
StorageLocationTag.fromString("ColdStorage").get must be(
StorageLocationTag.ColdStorage)
StorageLocationTag.fromString("DeepColdStorage").get must be(
StorageLocationTag.DeepColdStorage)
}
it must "read StorageLocationTagName from string" in {
InternalAddressTagName.fromString("HotStorage") must be(
StorageLocationTag.HotStorageName)
InternalAddressTagName.fromString("ColdStorage") must be(
StorageLocationTag.ColdStorageName)
InternalAddressTagName.fromString("DeepColdStorage") must be(
StorageLocationTag.DeepColdStorageName)
}
}

View file

@ -0,0 +1,43 @@
package org.bitcoins.core.wallet.utxo
/** A type of address tag, many AddressTags of the same type
* should inherit the AddressTagType that they all share
*/
trait AddressTagType {
def typeName: String
def ==(at: AddressTagType): Boolean = typeName == at.typeName
def !=(at: AddressTagType): Boolean = !(this == at)
}
trait AddressTagName {
def name: String
def ==(at: AddressTagType): Boolean = name == at.typeName
def !=(at: AddressTagType): Boolean = !(this == at)
}
/**
* An tag for an address. It's name is what it is referred to as
* and it's tagType is its parent AddressTagType
*/
trait AddressTag {
def tagName: AddressTagName
def tagType: AddressTagType
def ==(at: AddressTag): Boolean =
tagName == at.tagName && tagType == at.tagType
def !=(at: AddressTag): Boolean = !(this == at)
}
trait AddressTagFactory[Tag <: AddressTag] {
def tagType: AddressTagType
def tagNames: Vector[AddressTagName]
def all: Vector[Tag]
def fromString(str: String): Option[Tag] =
all.find(tag => str.toLowerCase() == tag.toString.toLowerCase)
}

View file

@ -0,0 +1,19 @@
package org.bitcoins.core.wallet.utxo
/**
* Address Tag Names defined outside the library, used for other projects
* creating there own address tags that aren't supported by bitcoin-s
*/
trait ExternalAddressTagName extends AddressTagName
/**
* Address Tag Types defined outside the library, used for other projects
* creating there own address tags that aren't supported by bitcoin-s
*/
trait ExternalAddressTagType extends AddressTagType
/**
* Address Tags defined outside the library, used for other projects
* creating there own address tags that aren't supported by bitcoin-s
*/
trait ExternalAddressTag extends AddressTag

View file

@ -0,0 +1,144 @@
package org.bitcoins.core.wallet.utxo
/**
* An AddressTagNames that is native to Bitcoin-S.
* InternalAddressTagNames are still usable when using Bitcoin-S
* as a dependency
*/
sealed trait InternalAddressTagName extends AddressTagName
/**
* An AddressTagType that is native to Bitcoin-S.
* InternalAddressTagTypes are still usable when using Bitcoin-S
* as a dependency
*/
sealed trait InternalAddressTagType extends AddressTagType
/**
* An AddressTag that is native to Bitcoin-S.
* InternalAddressTags are still usable when using Bitcoin-S
* as a dependency
*/
sealed trait InternalAddressTag extends AddressTag
/** An unknown address tag name, most likely an internal representation of an [[ExternalAddressTagName]] */
case class UnknownAddressTagName(name: String) extends InternalAddressTagName {
require(InternalAddressTagName.fromStringOpt(name).isEmpty,
s"This tag name is already defined, got $name")
}
/** An unknown address tag type, most likely an internal representation of an [[ExternalAddressTagType]] */
case class UnknownAddressTagType(typeName: String)
extends InternalAddressTagType {
require(InternalAddressTagType.fromStringOpt(typeName).isEmpty,
s"This tag type is already defined, got $typeName")
}
/** An address tag without an unknown type, most likely an internal representation of an [[ExternalAddressTag]] */
case class UnknownAddressTag(tagName: AddressTagName, tagType: AddressTagType)
extends InternalAddressTag
object UnknownAddressTag {
def apply(tagName: String, tagType: String): UnknownAddressTag =
UnknownAddressTag(UnknownAddressTagName(tagName),
UnknownAddressTagType(tagType))
def apply(tagName: String, tagType: AddressTagType): UnknownAddressTag =
UnknownAddressTag(UnknownAddressTagName(tagName), tagType)
def apply(tagName: AddressTagName, tagType: String): UnknownAddressTag =
UnknownAddressTag(tagName, UnknownAddressTagType(tagType))
}
object InternalAddressTagName {
val all: Seq[InternalAddressTagName] = StorageLocationTag.tagNames
def fromStringOpt(string: String): Option[InternalAddressTagName] =
all.find(_.name.toLowerCase == string.toLowerCase)
def fromString(string: String): InternalAddressTagName =
fromStringOpt(string).getOrElse(UnknownAddressTagName(string))
}
object InternalAddressTagType {
val all: Seq[InternalAddressTagType] = Vector(StorageLocationTagType)
def fromStringOpt(string: String): Option[InternalAddressTagType] =
all.find(_.typeName.toLowerCase == string.toLowerCase)
def fromString(string: String): InternalAddressTagType =
fromStringOpt(string).getOrElse(UnknownAddressTagType(string))
}
object InternalAddressTag {
def apply(
tagName: AddressTagName,
tagType: AddressTagType): InternalAddressTag = {
tagType match {
case unknownType: UnknownAddressTagType =>
UnknownAddressTag(tagName, unknownType)
case StorageLocationTagType =>
tagName match {
case StorageLocationTag.HotStorageName =>
StorageLocationTag.HotStorage
case StorageLocationTag.ColdStorageName =>
StorageLocationTag.ColdStorage
case StorageLocationTag.DeepColdStorageName =>
StorageLocationTag.DeepColdStorage
case unknownName: UnknownAddressTagName =>
UnknownAddressTag(unknownName, StorageLocationTagType)
}
}
}
}
object StorageLocationTagType extends InternalAddressTagType {
override val typeName: String = "StorageLocationTag"
}
/** Storage Location of the private keys associated with the address */
sealed trait StorageLocationTag extends InternalAddressTag {
override val tagType: AddressTagType = StorageLocationTagType
}
object StorageLocationTag extends AddressTagFactory[StorageLocationTag] {
override val tagType: InternalAddressTagType = StorageLocationTagType
override val tagNames =
Vector(HotStorageName, ColdStorageName, DeepColdStorageName)
// Tag Names
case object HotStorageName extends InternalAddressTagName {
override def name: String = "HotStorage"
}
case object ColdStorageName extends InternalAddressTagName {
override def name: String = "ColdStorage"
}
case object DeepColdStorageName extends InternalAddressTagName {
override def name: String = "DeepColdStorage"
}
/** Keys stored on a computer connected to the internet */
case object HotStorage extends StorageLocationTag {
override val tagName: AddressTagName = HotStorageName
}
/** Keys stored on a hardware wallet or other offline device */
case object ColdStorage extends StorageLocationTag {
override val tagName: AddressTagName = ColdStorageName
}
/** Keys stored on a hardware wallet or other offline device locked in a safe in a distant location */
case object DeepColdStorage extends StorageLocationTag {
override val tagName: AddressTagName = DeepColdStorageName
}
override val all: Vector[StorageLocationTag] =
Vector(HotStorage, ColdStorage, DeepColdStorage)
}

View file

@ -58,7 +58,7 @@ class DbManagementTest extends BitcoinSAsyncTest with EmbeddedPg {
dbConfig(ProjectType.Wallet))
val walletDbManagement = createWalletDbManagement(walletAppConfig)
val result = walletDbManagement.migrate()
val expected = if (walletAppConfig.driverName == "postgresql") 1 else 4
val expected = if (walletAppConfig.driverName == "postgresql") 2 else 5
assert(result == expected)
}

View file

@ -23,7 +23,7 @@ import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature
import org.bitcoins.core.script.ScriptType
import org.bitcoins.core.serializers.script.RawScriptWitnessParser
import org.bitcoins.core.wallet.fee.{SatoshisPerByte, SatoshisPerVirtualByte}
import org.bitcoins.core.wallet.utxo.TxoState
import org.bitcoins.core.wallet.utxo._
import org.bitcoins.crypto._
import scodec.bits.ByteVector
import slick.jdbc.{GetResult, JdbcProfile}
@ -213,6 +213,17 @@ class DbCommonsColumnMappers(val profile: JdbcProfile) {
.base[SatoshisPerByte, Long](_.toLong, SatoshisPerByte.fromLong)
}
implicit val addressTagMapper: BaseColumnType[AddressTagName] = {
MappedColumnType
.base[AddressTagName, String](_.name, InternalAddressTagName.fromString)
}
implicit val addressTagTypeMapper: BaseColumnType[AddressTagType] = {
MappedColumnType
.base[AddressTagType, String](_.typeName,
InternalAddressTagType.fromString)
}
implicit val hdAccountMapper: BaseColumnType[HDAccount] = {
MappedColumnType.base[HDAccount, String](
_.toString,

View file

@ -0,0 +1,143 @@
---
id: address-tagging
title: Address and UTXO tagging
---
```scala mdoc:invisible
import org.bitcoins.core.wallet.utxo._
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.wallet.fee._
import org.bitcoins.core.currency._
import org.bitcoins.wallet.models.AccountDb
import org.bitcoins.wallet._
val ExampleAddressTag = UnknownAddressTag("name", "tagType")
val exampleAddress = BitcoinAddress("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa")
val account: AccountDb = null
val destinations = Vector(TransactionOutput(Satoshis.one, exampleAddress.scriptPubKey))
case object wallet {
// need to define these functions so we don't have to implement every function
def fundRawTransaction(
destinations: Vector[TransactionOutput],
feeRate: FeeUnit,
fromTagOpt: Option[AddressTag],
markAsReserved: Boolean) = true
def getNewAddress(tags: Vector[AddressTag]) = true
def sendToAddress(
address: BitcoinAddress,
amount: CurrencyUnit,
feeRate: FeeUnit,
fromAccount: AccountDb,
newTags: Vector[AddressTag]) = true
}
```
### Using AddressTags
The Bitcoin-S wallet allows you to give addresses, and their associated utxos,
a tag. These tags allow you to separate funds between utxos so you can query utxos,
and spend from them, based off of an AddressTag. The system also allows you to create
your own custom address tags, that will be enforced by the library.
An address tag consists of the tag name, and a tag type. We use a tag type so we can have
tag with the same name without complications.
To create an address with a tag you can use `getNewAddress` but pass in a `Vector[AddressTag]`.
It will add to the address tag database along with all the corresponding tags.
```scala mdoc:silent
wallet.getNewAddress(tags = Vector(ExampleAddressTag))
```
When sending with `sendToAddress` you can also a `Vector` of new `AddressTag`s that will be applied to the
resulting change outputs. Any tags of a different tag type not included in `newTag`s will also be applied to
the change outputs.
```scala mdoc:silent
wallet.sendToAddress(exampleAddress, Bitcoins(2), SatoshisPerVirtualByte.one, account, Vector(ExampleAddressTag))
```
Also, when sending you can use `fundRawTransaction` and use `fromTagOpt` to pass in an optional `AddressTag`,
this will use only utxos associated with the `AddressTag`.
```scala mdoc:silent
wallet.fundRawTransaction(
destinations = destinations,
feeRate = SatoshisPerVirtualByte.one,
fromTagOpt = Some(ExampleAddressTag),
markAsReserved = false)
```
### Creating your own AddressTags
You can create your own custom `AddressTag`s. This allows you to tag addresses and utxos in any way that your
application needs. To do this you are going to need to use `ExternalAddressTag`. As an example we will create
`AddressTag`s for user specific funds.
We will need to define the tag type, then define the tag name for each tag, as well as a way to go to and
from a `String`. Then we define the actual tags, we are going to have a `Company`, `InsuranceFund`, and `UserId`
tags. We are going to make the `UserId` tag special, and allow it to take in any user id so we can have a huge
set of users but all with different ids.
```scala mdoc:silent
object UserIdTagType extends ExternalAddressTagType {
override val typeName: String = "UserIdTag"
}
/** Allows to assign funds in a specific address to a user */
sealed trait UserIdTag extends ExternalAddressTag {
override val tagType: AddressTagType = UserIdTagType
}
object UserIdTags extends AddressTagFactory[UserIdTag] {
override val tagType: ExternalAddressTagType = UserIdTagType
case object CompanyTagName extends ExternalAddressTagName {
override def name: String = "Company"
}
case object InsuranceFundTagName extends ExternalAddressTagName {
override def name: String = "InsuranceFund"
}
/** Funds that do not belong to any user and instead belong to the company */
case object Company extends ExternalAddressTag with UserIdTag {
override val tagName: ExternalAddressTagName = CompanyTagName
}
/** Funds in the company's insurance fund */
case object InsuranceFund extends ExternalAddressTag with UserIdTag {
override val tagName: ExternalAddressTagName = InsuranceFundTagName
}
/** Funds that are specific to an individual user */
case class UserId(id: String) extends ExternalAddressTag with UserIdTag {
override val tagName: ExternalAddressTagName = new ExternalAddressTagName {
override def name: String = id
}
val uid = id.toLong
}
override val all: Vector[UserIdTag] = Vector(Company, InsuranceFund)
override val tagNames = Vector(CompanyTagName, InsuranceFundTagName)
override def fromString(str: String): Option[UserIdTag] = {
all.find(tag => str.toLowerCase() == tag.toString.toLowerCase) match {
case Some(tag) =>
Some(tag)
case None =>
Some(UserId(str))
}
}
def fromUID(uid: Long): UserIdTag = {
UserId(uid.toString)
}
}
```

View file

@ -11,6 +11,7 @@ import scala.concurrent.Future
case class WalletDAOs(
accountDAO: AccountDAO,
addressDAO: AddressDAO,
addressTagDAO: AddressTagDAO,
utxoDAO: SpendingInfoDAO,
transactionDAO: TransactionDAO,
incomingTxDAO: IncomingTransactionDAO,
@ -21,11 +22,12 @@ trait WalletDAOFixture extends FixtureAsyncFlatSpec with BitcoinSWalletTest {
private lazy val daos: WalletDAOs = {
val account = AccountDAO()
val address = AddressDAO()
val tags = AddressTagDAO()
val utxo = SpendingInfoDAO()
val tx = TransactionDAO()
val incomingTx = IncomingTransactionDAO()
val outgoingTx = OutgoingTransactionDAO()
WalletDAOs(account, address, utxo, tx, incomingTx, outgoingTx)
WalletDAOs(account, address, tags, utxo, tx, incomingTx, outgoingTx)
}
final override type FixtureParam = WalletDAOs

View file

@ -78,7 +78,17 @@ object WalletTestUtil {
private def freshXpub(): ExtPublicKey =
CryptoGenerators.extPublicKey.sampleSome
val defaultHdAccount = HDAccount(HDCoin(HDPurposes.SegWit, hdCoinType), 0)
/** Checks that the given values are the same-ish, save for fee-level deviations */
def isCloseEnough(
first: CurrencyUnit,
second: CurrencyUnit,
delta: CurrencyUnit = 300.sats): Boolean = {
Math.abs(
first.satoshis.toLong - second.satoshis.toLong) < delta.satoshis.toLong
}
val defaultHdAccount: HDAccount =
HDAccount(HDCoin(HDPurposes.SegWit, hdCoinType), 0)
def getHdAccount1(walletAppConfig: WalletAppConfig): HDAccount = {
val purpose = walletAppConfig.defaultAccountKind

View file

@ -0,0 +1,115 @@
package org.bitcoins.wallet
import org.bitcoins.core.currency._
import org.bitcoins.core.hd.HDChainType
import org.bitcoins.core.protocol.transaction.TransactionOutput
import org.bitcoins.core.wallet.builder.RawTxSigner
import org.bitcoins.core.wallet.fee.SatoshisPerByte
import org.bitcoins.core.wallet.utxo.{InternalAddressTag, StorageLocationTag}
import org.bitcoins.testkit.wallet.{
BitcoinSWalletTest,
WalletTestUtil,
WalletWithBitcoind,
WalletWithBitcoindRpc
}
import org.scalatest.FutureOutcome
class AddressTagIntegrationTest extends BitcoinSWalletTest {
override type FixtureParam = WalletWithBitcoind
override def withFixture(test: OneArgAsyncTest): FutureOutcome =
withNewWalletAndBitcoind(test)
behavior of "Address Tag - integration test"
val feeRate: SatoshisPerByte = SatoshisPerByte(Satoshis.one)
val exampleTag: InternalAddressTag = StorageLocationTag.HotStorage
it should "correctly keep tagged utxos separated" in { walletWithBitcoind =>
val WalletWithBitcoindRpc(wallet, bitcoind) = walletWithBitcoind
// the amount we're receiving from bitcoind
val valueFromBitcoind = Bitcoins.one
// the amount we're sending to bitcoind
val valueToBitcoind = Bitcoins(0.5)
for {
addr <- wallet.getNewAddress()
taggedAddr <- wallet.getNewAddress(Vector(exampleTag))
txId <- bitcoind.sendMany(
Map(addr -> valueFromBitcoind, taggedAddr -> valueFromBitcoind))
tx <- bitcoind.getRawTransactionRaw(txId)
// before processing TX, wallet should be completely empty
_ <- wallet.listUtxos().map(utxos => assert(utxos.isEmpty))
_ <- wallet.getBalance().map(confirmed => assert(confirmed == 0.bitcoin))
_ <-
wallet
.getUnconfirmedBalance()
.map(unconfirmed => assert(unconfirmed == 0.bitcoin))
// after this, tx is unconfirmed in wallet
_ <- wallet.processTransaction(tx, None)
// we should now have one UTXO in the wallet
// it should not be confirmed
utxosPostAdd <- wallet.listUtxos()
_ = assert(utxosPostAdd.length == 2)
_ <-
wallet
.getConfirmedBalance()
.map(confirmed => assert(confirmed == 0.bitcoin))
_ <-
wallet
.getUnconfirmedBalance()
.map(unconfirmed => assert(unconfirmed == valueFromBitcoind * 2))
incomingTx <- wallet.incomingTxDAO.findByTxId(tx.txIdBE)
_ = assert(incomingTx.isDefined)
_ = assert(incomingTx.get.incomingAmount == valueFromBitcoind * 2)
taggedUtxosPostAdd <- wallet.listUtxos(exampleTag)
_ = assert(taggedUtxosPostAdd.length == 1)
_ <-
wallet
.getUnconfirmedBalance(exampleTag)
.map(unconfirmed => assert(unconfirmed == valueFromBitcoind))
account <- wallet.getDefaultAccount()
(txBuilder, utxoInfos) <- bitcoind.getNewAddress.flatMap { addr =>
val output = TransactionOutput(valueToBitcoind, addr.scriptPubKey)
wallet
.fundRawTransactionInternal(destinations = Vector(output),
feeRate = feeRate,
fromAccount = account,
keyManagerOpt = Some(wallet.keyManager),
fromTagOpt = Some(exampleTag))
}
utx <- txBuilder.buildTx()
signedTx <- RawTxSigner.sign(utx, utxoInfos, feeRate)
_ <- wallet.processTransaction(signedTx, None)
utxos <- wallet.listUtxos()
balancePostSend <- wallet.getBalance()
tagBalancePostSend <- wallet.getBalance(exampleTag)
} yield {
// One change one external
assert(utxos.size == 2)
assert(
utxos.exists(_.privKeyPath.chain.chainType == HDChainType.External))
assert(utxos.exists(_.privKeyPath.chain.chainType == HDChainType.Change))
// untagged balance should be untouched
assert(balancePostSend - tagBalancePostSend == valueFromBitcoind.satoshis)
// change UTXO should be smaller than what we had, but still have money in it
assert(tagBalancePostSend > 0.sats)
assert(tagBalancePostSend < valueFromBitcoind)
assert(
WalletTestUtil.isCloseEnough(tagBalancePostSend,
valueFromBitcoind - valueToBitcoind))
}
}
}

View file

@ -2,15 +2,19 @@ package org.bitcoins.wallet
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.protocol.transaction.TransactionOutput
import org.bitcoins.core.wallet.builder.RawTxSigner
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.TxoState
import org.bitcoins.core.wallet.utxo.StorageLocationTag.HotStorage
import org.bitcoins.core.wallet.utxo._
import org.bitcoins.testkit.util.TestUtil
import org.bitcoins.testkit.wallet.{
BitcoinSWalletTest,
WalletTestUtil,
WalletWithBitcoind
}
import org.scalatest.FutureOutcome
import org.scalatest.{Assertion, FutureOutcome}
import scala.concurrent.Future
class FundTransactionHandlingTest extends BitcoinSWalletTest {
@ -20,8 +24,9 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
withFundedWalletAndBitcoind(test, getBIP39PasswordOpt())
}
val destination = TransactionOutput(Bitcoins(0.5), TestUtil.p2pkhScriptPubKey)
val feeRate = SatoshisPerVirtualByte.one
val destination: TransactionOutput =
TransactionOutput(Bitcoins(0.5), TestUtil.p2pkhScriptPubKey)
val feeRate: SatoshisPerVirtualByte = SatoshisPerVirtualByte.one
it must "fund a simple raw transaction that requires one utxo" in {
fundedWallet: WalletWithBitcoind =>
@ -29,7 +34,8 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
val fundedTxF = wallet.fundRawTransaction(destinations =
Vector(destination),
feeRate = feeRate,
false)
fromTagOpt = None,
markAsReserved = false)
for {
fundedTx <- fundedTxF
} yield {
@ -49,7 +55,8 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
val fundedTxF = wallet.fundRawTransaction(destinations =
Vector(newDestination),
feeRate = feeRate,
false)
fromTagOpt = None,
markAsReserved = false)
for {
fundedTx <- fundedTxF
} yield {
@ -68,7 +75,8 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
val wallet = fundedWallet.wallet
val fundedTxF = wallet.fundRawTransaction(destinations = destinations,
feeRate = feeRate,
false)
fromTagOpt = None,
markAsReserved = false)
for {
fundedTx <- fundedTxF
} yield {
@ -91,7 +99,8 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
val fundedTxF = wallet.fundRawTransaction(destinations =
Vector(tooBigOutput),
feeRate = feeRate,
false)
fromTagOpt = None,
markAsReserved = false)
recoverToSucceededIf[RuntimeException] {
fundedTxF
@ -110,7 +119,8 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
val fundedTxF = wallet.fundRawTransaction(destinations =
Vector(tooBigOutput),
feeRate = feeRate,
false)
fromTagOpt = None,
markAsReserved = false)
recoverToSucceededIf[RuntimeException] {
fundedTxF
@ -131,8 +141,7 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
account1DbOpt <- account1DbF
fundedTx <- wallet.fundRawTransaction(Vector(newDestination),
feeRate,
account1DbOpt.get,
false)
account1DbOpt.get)
} yield {
assert(fundedTx.inputs.nonEmpty)
assert(fundedTx.outputs.contains(newDestination))
@ -153,8 +162,7 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
account1DbOpt <- account1DbF
fundedTx <- wallet.fundRawTransaction(Vector(newDestination),
feeRate,
account1DbOpt.get,
false)
account1DbOpt.get)
} yield fundedTx
recoverToSucceededIf[RuntimeException] {
@ -190,6 +198,7 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
val fundedTxF = wallet.fundRawTransaction(destinations =
Vector(destination),
feeRate = feeRate,
fromTagOpt = None,
markAsReserved = true)
for {
fundedTx <- fundedTxF
@ -198,4 +207,51 @@ class FundTransactionHandlingTest extends BitcoinSWalletTest {
assert(spendingInfos.exists(_.state == TxoState.Reserved))
}
}
def testAddressTagFunding(
wallet: Wallet,
tag: AddressTag): Future[Assertion] = {
for {
account <- wallet.getDefaultAccount()
taggedAddr <- wallet.getNewAddress(Vector(tag))
_ <-
wallet.sendToAddress(taggedAddr, destination.value * 2, Some(feeRate))
taggedBalance <- wallet.getBalance(tag)
_ = assert(taggedBalance == destination.value * 2)
expectedUtxos <- wallet.listUtxos(account.hdAccount, tag)
(txBuilder, utxoInfos) <-
wallet
.fundRawTransactionInternal(
destinations = Vector(destination),
feeRate = feeRate,
fromAccount = account,
keyManagerOpt = Some(wallet.keyManager),
fromTagOpt = Some(tag),
markAsReserved = true
)
utx <- txBuilder.buildTx()
tx <- RawTxSigner.sign(utx, utxoInfos, feeRate)
} yield {
assert(tx.inputs.forall(input =>
expectedUtxos.exists(_.outPoint == input.previousOutput)))
}
}
it must "fund a transaction with only utxos with an unknown address tag" in {
fundedWallet: WalletWithBitcoind =>
val wallet = fundedWallet.wallet
val exampleTag: UnknownAddressTag =
UnknownAddressTag("Example", "ExampleTagType")
testAddressTagFunding(wallet, exampleTag)
}
it must "fund a transaction with only utxos with an internal address tag" in {
fundedWallet: WalletWithBitcoind =>
val wallet = fundedWallet.wallet
testAddressTagFunding(wallet, HotStorage)
}
}

View file

@ -53,7 +53,8 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
feeRate = SatoshisPerByte(Satoshis(3)),
inputAmount = Satoshis(4000),
sentAmount = Satoshis(3000),
blockHashOpt = None)
blockHashOpt = None,
newTags = Vector.empty)
updatedCoin <-
wallet.spendingInfoDAO.findByScriptPubKey(addr.scriptPubKey)
@ -71,6 +72,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
for {
tx <- wallet.fundRawTransaction(Vector(dummyOutput),
SatoshisPerVirtualByte.one,
fromTagOpt = None,
markAsReserved = true)
updatedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
@ -88,6 +90,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
for {
tx <- wallet.fundRawTransaction(Vector(dummyOutput),
SatoshisPerVirtualByte.one,
fromTagOpt = None,
markAsReserved = true)
reservedUtxos <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
@ -108,6 +111,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
for {
tx <- wallet.fundRawTransaction(Vector(dummyOutput),
SatoshisPerVirtualByte.one,
fromTagOpt = None,
markAsReserved = true)
unreservedUtxos <- wallet.unmarkUTXOsAsReserved(tx)
} yield {

View file

@ -5,6 +5,7 @@ import org.bitcoins.core.hd.HDChainType
import org.bitcoins.core.wallet.fee.SatoshisPerByte
import org.bitcoins.testkit.wallet.{
BitcoinSWalletTest,
WalletTestUtil,
WalletWithBitcoind,
WalletWithBitcoindRpc
}
@ -21,21 +22,6 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
val feeRate: SatoshisPerByte = SatoshisPerByte(Satoshis.one)
/** Checks that the given values are the same-ish, save for fee-level deviations */
private def isCloseEnough(
first: CurrencyUnit,
second: CurrencyUnit,
delta: CurrencyUnit = 1000.sats): Boolean = {
val diff =
if (first > second) {
first - second
} else if (first < second) {
second - first
} else 0.sats
diff < delta
}
it should ("create an address, receive funds to it from bitcoind, import the"
+ " UTXO and construct a valid, signed transaction that's"
+ " broadcast and confirmed by bitcoind") in { walletWithBitcoind =>
@ -127,9 +113,9 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
_ = assert(outgoingTx.get.feeRate == feeRate)
_ = assert(outgoingTx.get.expectedFee == feeRate.calc(signedTx))
_ = assert(
isCloseEnough(feeRate.calc(signedTx),
outgoingTx.get.actualFee,
3.satoshi))
WalletTestUtil.isCloseEnough(feeRate.calc(signedTx),
outgoingTx.get.actualFee,
3.satoshi))
// Safe to use utxos.head because we've already asserted that we only have our change output
_ = assert(
outgoingTx.get.actualFee + outgoingTx.get.sentAmount == outgoingTx.get.inputAmount - utxos.head.output.value)
@ -141,7 +127,8 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
assert(balancePostSend < valueFromBitcoind)
assert(
isCloseEnough(balancePostSend, valueFromBitcoind - valueToBitcoind))
WalletTestUtil.isCloseEnough(balancePostSend,
valueFromBitcoind - valueToBitcoind))
}
} yield {

View file

@ -0,0 +1,85 @@
package org.bitcoins.wallet.models
import java.sql.SQLException
import org.bitcoins.core.wallet.utxo.StorageLocationTag.HotStorage
import org.bitcoins.core.wallet.utxo.{
AddressTag,
UnknownAddressTag,
UnknownAddressTagName,
UnknownAddressTagType
}
import org.bitcoins.testkit.fixtures.WalletDAOFixture
import org.bitcoins.testkit.util.TestUtil
import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletTestUtil}
import org.scalatest.Assertion
import scala.concurrent.Future
class AddressTagDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
behavior of "AddressTagDAO"
val exampleTag: UnknownAddressTag =
UnknownAddressTag(UnknownAddressTagName("Example"),
UnknownAddressTagType("ExampleTagType"))
def testInsertionFailure(
daos: FixtureParam,
tag: AddressTag): Future[Assertion] = {
val tagDAO = daos.addressTagDAO
val addr = TestUtil.testBitcoinAddress
val tagDb = AddressTagDb(addr, tag.tagName, tag.tagType)
val readF = tagDAO.create(tagDb)
recoverToSucceededIf[SQLException](readF)
}
def testInsertion(daos: FixtureParam, tag: AddressTag): Future[Assertion] = {
val accountDAO = daos.accountDAO
val addressDAO = daos.addressDAO
val addressTagDAO = daos.addressTagDAO
for {
createdAccount <- {
val account = WalletTestUtil.firstAccountDb
accountDAO.create(account)
}
createdAddress <- {
val addressDb = WalletTestUtil.getAddressDb(createdAccount)
addressDAO.create(addressDb)
}
createdAddressTag <- {
val tagDb =
AddressTagDb(createdAddress.address, tag)
addressTagDAO.create(tagDb)
}
readAddressTagOpt <- addressTagDAO.read(createdAddressTag.address)
} yield {
assert(readAddressTagOpt.isDefined)
val readAddressTag = readAddressTagOpt.get
assert(readAddressTag.address == createdAddress.address)
assert(readAddressTag.addressTag == tag)
}
}
it should "fail to insert and read an unknown address tag into the database without a corresponding address" in {
daos =>
testInsertionFailure(daos, exampleTag)
}
it should "fail to insert and read an internal address tag into the database without a corresponding address" in {
daos =>
testInsertionFailure(daos, HotStorage)
}
it should "insert and read an unknown address tag into the database" in {
daos =>
testInsertion(daos, exampleTag)
}
it should "insert and read an internal address tag into the database" in {
daos =>
testInsertion(daos, HotStorage)
}
}

View file

@ -16,7 +16,7 @@ class SpendingInfoDAOTest extends BitcoinSWalletTest with WalletDAOFixture {
behavior of "SpendingInfoDAO"
it must "be able to update multiple utxos" in { daos =>
val WalletDAOs(_, addressDAO, spendingInfoDAO, _, _, _) = daos
val WalletDAOs(_, addressDAO, _, spendingInfoDAO, _, _, _) = daos
for {
account <- daos.accountDAO.create(WalletTestUtil.firstAccountDb)

View file

@ -0,0 +1 @@
CREATE TABLE wallet_address_tags (id SERIAL UNIQUE,address VARCHAR(254) NOT NULL,tag_name VARCHAR(254) NOT NULL,tag_type VARCHAR(254) NOT NULL,constraint fk_address foreign key(address) references addresses(address) on update NO ACTION on delete NO ACTION);

View file

@ -0,0 +1 @@
CREATE TABLE "wallet_address_tags" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"address" VARCHAR(254) NOT NULL,"tag_name" VARCHAR(254) NOT NULL,"tag_type" VARCHAR(254) NOT NULL,constraint "fk_address" foreign key("address") references "addresses"("address") on update NO ACTION on delete NO ACTION);

View file

@ -25,6 +25,7 @@ import org.bitcoins.core.wallet.utxo.TxoState.{
PendingConfirmationsReceived
}
import org.bitcoins.core.wallet.utxo.{
AddressTag,
InputInfo,
ScriptSignatureParams,
TxoState
@ -61,6 +62,7 @@ abstract class Wallet
private[wallet] val outgoingTxDAO: OutgoingTransactionDAO =
OutgoingTransactionDAO()
private[wallet] val addressTagDAO: AddressTagDAO = AddressTagDAO()
val nodeApi: NodeApi
val chainQueryApi: ChainQueryApi
@ -192,6 +194,13 @@ abstract class Wallet
unspentInAccountF.map(_.foldLeft(CurrencyUnits.zero)(_ + _.output.value))
}
override def getConfirmedBalance(tag: AddressTag): Future[CurrencyUnit] = {
spendingInfoDAO.findAllUnspentForTag(tag).map { allUnspent =>
val confirmed = allUnspent.filter(_.state == ConfirmedReceived)
confirmed.foldLeft(CurrencyUnits.zero)(_ + _.output.value)
}
}
override def getUnconfirmedBalance(): Future[CurrencyUnit] = {
val unconfirmed = filterThenSum(_.state == PendingConfirmationsReceived)
unconfirmed.foreach(balance =>
@ -215,6 +224,13 @@ abstract class Wallet
unspentInAccountF.map(_.foldLeft(CurrencyUnits.zero)(_ + _.output.value))
}
override def getUnconfirmedBalance(tag: AddressTag): Future[CurrencyUnit] = {
spendingInfoDAO.findAllUnspentForTag(tag).map { allUnspent =>
val confirmed = allUnspent.filter(_.state == PendingConfirmationsReceived)
confirmed.foldLeft(CurrencyUnits.zero)(_ + _.output.value)
}
}
/** Enumerates all the TX outpoints in the wallet */
protected[wallet] def listOutpoints(): Future[Vector[TransactionOutPoint]] =
spendingInfoDAO.findAllOutpoints()
@ -265,7 +281,8 @@ abstract class Wallet
txBuilder: RawTxBuilderWithFinalizer[StandardNonInteractiveFinalizer],
utxoInfos: Vector[ScriptSignatureParams[InputInfo]],
sentAmount: CurrencyUnit,
feeRate: FeeUnit): Future[Transaction] = {
feeRate: FeeUnit,
newTags: Vector[AddressTag]): Future[Transaction] = {
for {
utx <- txBuilder.buildTx()
signed <- RawTxSigner.sign(utx, utxoInfos, feeRate)
@ -275,7 +292,8 @@ abstract class Wallet
feeRate = feeRate,
inputAmount = creditingAmount,
sentAmount = sentAmount,
blockHashOpt = None)
blockHashOpt = None,
newTags = newTags)
} yield {
logger.debug(
s"Signed transaction=${signed.txIdBE.hex} with outputs=${signed.outputs.length}, inputs=${signed.inputs.length}")
@ -322,7 +340,7 @@ abstract class Wallet
feeRate,
changeAddr.scriptPubKey)
tx <- finishSend(txBuilder, utxos, amount, feeRate)
tx <- finishSend(txBuilder, utxos, amount, feeRate, Vector.empty)
} yield tx
}
@ -331,7 +349,8 @@ abstract class Wallet
amount: CurrencyUnit,
feeRate: FeeUnit,
algo: CoinSelectionAlgo,
fromAccount: AccountDb): Future[Transaction] = {
fromAccount: AccountDb,
newTags: Vector[AddressTag]): Future[Transaction] = {
require(
address.networkParameters.isSameNetworkBytes(networkParameters),
s"Cannot send to address on other network, got ${address.networkParameters}"
@ -344,9 +363,10 @@ abstract class Wallet
feeRate = feeRate,
fromAccount = fromAccount,
keyManagerOpt = Some(keyManager),
coinSelectionAlgo = algo)
coinSelectionAlgo = algo,
fromTagOpt = None)
tx <- finishSend(txBuilder, utxoInfos, amount, feeRate)
tx <- finishSend(txBuilder, utxoInfos, amount, feeRate, newTags)
} yield tx
}
@ -361,6 +381,19 @@ abstract class Wallet
CoinSelectionAlgo.AccumulateLargest,
fromAccount)
override def sendToAddress(
address: BitcoinAddress,
amount: CurrencyUnit,
feeRate: FeeUnit,
fromAccount: AccountDb,
newTags: Vector[AddressTag]): Future[Transaction] =
sendWithAlgo(address,
amount,
feeRate,
CoinSelectionAlgo.AccumulateLargest,
fromAccount,
newTags)
override def sendToAddresses(
addresses: Vector[BitcoinAddress],
amounts: Vector[CurrencyUnit],
@ -417,9 +450,10 @@ abstract class Wallet
feeRate = feeRate,
fromAccount = fromAccount,
keyManagerOpt = Some(keyManager),
fromTagOpt = None,
markAsReserved = reserveUtxos)
sentAmount = outputs.foldLeft(CurrencyUnits.zero)(_ + _.value)
tx <- finishSend(txBuilder, utxoInfos, sentAmount, feeRate)
tx <- finishSend(txBuilder, utxoInfos, sentAmount, feeRate, Vector.empty)
} yield tx
}

View file

@ -19,6 +19,7 @@ import org.bitcoins.core.protocol.transaction.{
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.core.wallet.utxo.AddressTag
import org.bitcoins.crypto.{
AesPassword,
DoubleSha256Digest,
@ -37,7 +38,7 @@ import scala.util.{Failure, Success}
/**
* API for the wallet project.
*
* This wallet API is BIP344 compliant.
* This wallet API is BIP44 compliant.
*
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki BIP44]]
*/
@ -151,16 +152,31 @@ trait WalletApi extends WalletLogger {
}
}
/** Gets the sum of all UTXOs in this wallet with the address tag */
def getBalance(tag: AddressTag): Future[CurrencyUnit] = {
val confirmedF = getConfirmedBalance(tag)
val unconfirmedF = getUnconfirmedBalance(tag)
for {
confirmed <- confirmedF
unconfirmed <- unconfirmedF
} yield confirmed + unconfirmed
}
/** Gets the sum of all confirmed UTXOs in this wallet */
def getConfirmedBalance(): Future[CurrencyUnit]
def getConfirmedBalance(account: HDAccount): Future[CurrencyUnit]
def getConfirmedBalance(tag: AddressTag): Future[CurrencyUnit]
/** Gets the sum of all unconfirmed UTXOs in this wallet */
def getUnconfirmedBalance(): Future[CurrencyUnit]
def getUnconfirmedBalance(account: HDAccount): Future[CurrencyUnit]
def getUnconfirmedBalance(tag: AddressTag): Future[CurrencyUnit]
/**
* If a UTXO is spent outside of the wallet, we
* need to remove it from the database so it won't be
@ -175,6 +191,12 @@ trait WalletApi extends WalletLogger {
def listUtxos(account: HDAccount): Future[Vector[SpendingInfoDb]]
def listUtxos(tag: AddressTag): Future[Vector[SpendingInfoDb]]
def listUtxos(
hdAccount: HDAccount,
tag: AddressTag): Future[Vector[SpendingInfoDb]]
def listAddresses(): Future[Vector[AddressDb]]
def listAddresses(account: HDAccount): Future[Vector[AddressDb]]
@ -238,6 +260,16 @@ trait WalletApi extends WalletLogger {
} yield address
}
def getNewAddress(
addressType: AddressType,
tags: Vector[AddressTag]): Future[BitcoinAddress]
def getNewAddress(tags: Vector[AddressTag]): Future[BitcoinAddress] = {
for {
address <- getNewAddress(walletConfig.defaultAddressType, tags)
} yield address
}
/**
* Gets a external address from the account associated with
* the given AddressType. Calling this method multiple
@ -489,7 +521,16 @@ trait WalletApi extends WalletLogger {
amount: CurrencyUnit,
feeRate: FeeUnit,
algo: CoinSelectionAlgo,
fromAccount: AccountDb): Future[Transaction]
fromAccount: AccountDb,
newTags: Vector[AddressTag]): Future[Transaction]
def sendWithAlgo(
address: BitcoinAddress,
amount: CurrencyUnit,
feeRate: FeeUnit,
algo: CoinSelectionAlgo,
fromAccount: AccountDb): Future[Transaction] =
sendWithAlgo(address, amount, feeRate, algo, fromAccount, Vector.empty)
def sendWithAlgo(
address: BitcoinAddress,
@ -557,6 +598,13 @@ trait WalletApi extends WalletLogger {
} yield tx
}
def sendToAddress(
address: BitcoinAddress,
amount: CurrencyUnit,
feeRate: FeeUnit,
fromAccount: AccountDb,
newTags: Vector[AddressTag]): Future[Transaction]
/**
* Sends money from the specified account
*

View file

@ -2,14 +2,7 @@ package org.bitcoins.wallet.db
import org.bitcoins.db.{DbManagement, JdbcProfileComponent}
import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.wallet.models.{
AccountDAO,
AddressDAO,
IncomingTransactionDAO,
OutgoingTransactionDAO,
SpendingInfoDAO,
TransactionDAO
}
import org.bitcoins.wallet.models._
import scala.concurrent.ExecutionContext
@ -28,6 +21,10 @@ trait WalletDbManagement extends DbManagement {
AddressDAO()(ec, appConfig).table
}
private lazy val addressTagTable: TableQuery[Table[_]] = {
AddressTagDAO()(ec, appConfig).table
}
private lazy val utxoTable: TableQuery[Table[_]] = {
SpendingInfoDAO()(ec, appConfig).table
}
@ -44,9 +41,12 @@ trait WalletDbManagement extends DbManagement {
OutgoingTransactionDAO()(ec, appConfig).table
}
// Ordering matters here, tables with a foreign key should be listed after
// the table that key references
override lazy val allTables: List[TableQuery[Table[_]]] = {
List(accountTable,
addressTable,
addressTagTable,
txTable,
incomingTxTable,
utxoTable,

View file

@ -12,10 +12,16 @@ import org.bitcoins.core.protocol.transaction.{
TransactionOutPoint,
TransactionOutput
}
import org.bitcoins.core.wallet.utxo.AddressTag
import org.bitcoins.crypto.ECPublicKey
import org.bitcoins.wallet._
import org.bitcoins.wallet.api.AddressInfo
import org.bitcoins.wallet.models.{AccountDb, AddressDb, AddressDbHelper}
import org.bitcoins.wallet.models.{
AccountDb,
AddressDb,
AddressDbHelper,
AddressTagDb
}
import scala.concurrent.{Await, Future, Promise, TimeoutException}
import scala.util.{Failure, Success}
@ -341,6 +347,19 @@ private[wallet] trait AddressHandling extends WalletLogger {
} yield address
}
/** @inheritdoc */
override def getNewAddress(
addressType: AddressType,
tags: Vector[AddressTag]): Future[BitcoinAddress] = {
for {
account <- getDefaultAccountForType(addressType)
address <- getNewAddressHelper(account, HDChainType.External)
tagDbs = tags.map(tag => AddressTagDb(address, tag))
_ <- addressTagDAO.createAll(tagDbs)
} yield address
}
/** Generates a new change address */
override protected[wallet] def getNewChangeAddress(
account: AccountDb): Future[BitcoinAddress] = {

View file

@ -9,7 +9,11 @@ import org.bitcoins.core.wallet.builder.{
StandardNonInteractiveFinalizer
}
import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.core.wallet.utxo.{InputInfo, ScriptSignatureParams}
import org.bitcoins.core.wallet.utxo.{
AddressTag,
InputInfo,
ScriptSignatureParams
}
import org.bitcoins.crypto.Sign
import org.bitcoins.keymanager.bip39.BIP39KeyManager
import org.bitcoins.wallet.api.{AddressInfo, CoinSelector}
@ -23,12 +27,14 @@ trait FundTransactionHandling extends WalletLogger { self: Wallet =>
def fundRawTransaction(
destinations: Vector[TransactionOutput],
feeRate: FeeUnit,
fromTagOpt: Option[AddressTag],
markAsReserved: Boolean): Future[Transaction] = {
for {
account <- getDefaultAccount()
funded <- fundRawTransaction(destinations = destinations,
feeRate = feeRate,
fromAccount = account,
fromTagOpt = fromTagOpt,
markAsReserved = markAsReserved)
} yield funded
}
@ -37,12 +43,14 @@ trait FundTransactionHandling extends WalletLogger { self: Wallet =>
destinations: Vector[TransactionOutput],
feeRate: FeeUnit,
fromAccount: AccountDb,
fromTagOpt: Option[AddressTag] = None,
markAsReserved: Boolean = false): Future[Transaction] = {
val txBuilderF =
fundRawTransactionInternal(destinations = destinations,
feeRate = feeRate,
fromAccount = fromAccount,
keyManagerOpt = None,
fromTagOpt = fromTagOpt,
markAsReserved = markAsReserved)
txBuilderF.flatMap(_._1.buildTx())
}
@ -64,11 +72,17 @@ trait FundTransactionHandling extends WalletLogger { self: Wallet =>
keyManagerOpt: Option[BIP39KeyManager],
coinSelectionAlgo: CoinSelectionAlgo =
CoinSelectionAlgo.AccumulateLargest,
fromTagOpt: Option[AddressTag],
markAsReserved: Boolean = false): Future[(
RawTxBuilderWithFinalizer[StandardNonInteractiveFinalizer],
Vector[ScriptSignatureParams[InputInfo]])] = {
val utxosF = for {
utxos <- listUtxos(fromAccount.hdAccount)
utxos <- fromTagOpt match {
case None =>
listUtxos(fromAccount.hdAccount)
case Some(tag) =>
listUtxos(fromAccount.hdAccount, tag)
}
// Need to remove immature coinbase inputs
coinbaseUtxos = utxos.filter(_.outPoint == EmptyTransactionOutPoint)

View file

@ -2,11 +2,12 @@ package org.bitcoins.wallet.internal
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.blockchain.Block
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutput}
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.core.wallet.utxo.TxoState
import org.bitcoins.core.wallet.utxo.{AddressTag, TxoState}
import org.bitcoins.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.wallet._
import org.bitcoins.wallet.api.{AddUtxoError, AddUtxoSuccess}
@ -31,7 +32,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
blockHashOpt: Option[DoubleSha256DigestBE]
): Future[Wallet] = {
for {
result <- processTransactionImpl(transaction, blockHashOpt)
result <- processTransactionImpl(transaction, blockHashOpt, Vector.empty)
} yield {
logger.debug(
s"Finished processing of transaction=${transaction.txIdBE}. Relevant incomingTXOs=${result.updatedIncoming.length}, outgoingTXOs=${result.updatedOutgoing.length}")
@ -96,13 +97,14 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
feeRate: FeeUnit,
inputAmount: CurrencyUnit,
sentAmount: CurrencyUnit,
blockHashOpt: Option[DoubleSha256DigestBE]): Future[ProcessTxResult] = {
blockHashOpt: Option[DoubleSha256DigestBE],
newTags: Vector[AddressTag]): Future[ProcessTxResult] = {
logger.info(
s"Processing TX from our wallet, transaction=${transaction.txIdBE} with blockHash=$blockHashOpt")
for {
_ <-
insertOutgoingTransaction(transaction, feeRate, inputAmount, sentAmount)
result <- processTransactionImpl(transaction, blockHashOpt)
result <- processTransactionImpl(transaction, blockHashOpt, newTags)
} yield {
val txid = transaction.txIdBE
val changeOutputs = result.updatedIncoming.length
@ -156,7 +158,8 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
*/
private def processTransactionImpl(
transaction: Transaction,
blockHashOpt: Option[DoubleSha256DigestBE]): Future[ProcessTxResult] = {
blockHashOpt: Option[DoubleSha256DigestBE],
newTags: Vector[AddressTag]): Future[ProcessTxResult] = {
logger.debug(
s"Processing transaction=${transaction.txIdBE} with blockHash=$blockHashOpt")
@ -169,7 +172,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
.flatMap {
// no existing elements found
case Vector() =>
processNewIncomingTx(transaction, blockHashOpt)
processNewIncomingTx(transaction, blockHashOpt, newTags)
.map(_.toVector)
case txos: Vector[SpendingInfoDb] =>
@ -371,8 +374,8 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
*/
private def processNewIncomingTx(
transaction: Transaction,
blockHashOpt: Option[DoubleSha256DigestBE]): Future[
Seq[SpendingInfoDb]] = {
blockHashOpt: Option[DoubleSha256DigestBE],
newTags: Vector[AddressTag]): Future[Seq[SpendingInfoDb]] = {
addressDAO.findAll().flatMap { addrs =>
val relevantOutsWithIdx: Seq[OutputWithIndex] = {
val withIndex =
@ -406,6 +409,17 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
for {
_ <- insertIncomingTransaction(transaction, totalIncoming)
prevTagDbs <- addressTagDAO.findTx(transaction, networkParameters)
prevTags = prevTagDbs.map(_.addressTag)
tagsToUse =
prevTags
.filterNot(tag => newTags.contains(tag)) ++ newTags
newTagDbs = relevantOutsWithIdx.flatMap { out =>
val address = BitcoinAddress
.fromScriptPubKey(out.output.scriptPubKey, networkParameters)
tagsToUse.map(tag => AddressTagDb(address, tag))
}
_ <- addressTagDAO.createAll(newTagDbs.toVector)
utxos <- addUTXOsFut(outputsWithIndex, transaction, blockHashOpt)
} yield utxos
}

View file

@ -16,7 +16,7 @@ import org.bitcoins.core.protocol.transaction.{
TransactionOutput
}
import org.bitcoins.core.util.{EitherUtil, FutureUtil}
import org.bitcoins.core.wallet.utxo.TxoState
import org.bitcoins.core.wallet.utxo.{AddressTag, TxoState}
import org.bitcoins.crypto.DoubleSha256DigestBE
import org.bitcoins.wallet.api.{AddUtxoError, AddUtxoResult, AddUtxoSuccess}
import org.bitcoins.wallet.models._
@ -52,6 +52,20 @@ private[wallet] trait UtxoHandling extends WalletLogger {
.map(_.filter(spendingInfo => outPoints.contains(spendingInfo.outPoint)))
}
override def listUtxos(tag: AddressTag): Future[Vector[SpendingInfoDb]] = {
spendingInfoDAO.findAllUnspentForTag(tag)
}
override def listUtxos(
hdAccount: HDAccount,
tag: AddressTag): Future[Vector[SpendingInfoDb]] = {
spendingInfoDAO.findAllUnspentForTag(tag).map { utxos =>
utxos.filter(utxo =>
HDAccount.isSameAccount(bip32Path = utxo.privKeyPath,
account = hdAccount))
}
}
protected def updateUtxoConfirmedState(
txo: SpendingInfoDb,
blockHash: DoubleSha256DigestBE): Future[SpendingInfoDb] = {

View file

@ -0,0 +1,137 @@
package org.bitcoins.wallet.models
import org.bitcoins.core.config.NetworkParameters
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.wallet.utxo.{
AddressTag,
AddressTagName,
AddressTagType,
InternalAddressTagType
}
import org.bitcoins.db.{CRUD, SlickUtil}
import org.bitcoins.wallet.config.WalletAppConfig
import slick.lifted.{ForeignKeyQuery, ProvenShape}
import scala.concurrent.{ExecutionContext, Future}
case class AddressTagDAO()(implicit
val ec: ExecutionContext,
override val appConfig: WalletAppConfig)
extends CRUD[AddressTagDb, BitcoinAddress]
with SlickUtil[AddressTagDb, BitcoinAddress] {
import profile.api._
private val mappers = new org.bitcoins.db.DbCommonsColumnMappers(profile)
import mappers._
override val table: profile.api.TableQuery[AddressTagTable] =
TableQuery[AddressTagTable]
private lazy val spendingInfoTable: slick.lifted.TableQuery[
SpendingInfoDAO#SpendingInfoTable] = {
SpendingInfoDAO().table
}
private lazy val addressTable: slick.lifted.TableQuery[
AddressDAO#AddressTable] = {
AddressDAO().table
}
override def createAll(
ts: Vector[AddressTagDb]): Future[Vector[AddressTagDb]] =
createAllNoAutoInc(ts, safeDatabase)
/** Finds the rows that correlate to the given primary keys */
override def findByPrimaryKeys(addresses: Vector[BitcoinAddress]): Query[
AddressTagTable,
AddressTagDb,
Seq] =
table.filter(_.address.inSet(addresses))
override def findByPrimaryKey(
address: BitcoinAddress): Query[Table[_], AddressTagDb, Seq] = {
table.filter(_.address === address)
}
override def findAll(
ts: Vector[AddressTagDb]): Query[Table[_], AddressTagDb, Seq] =
findByPrimaryKeys(ts.map(_.address))
def findByAddress(address: BitcoinAddress): Future[Vector[AddressTagDb]] = {
val query = table.filter(_.address === address)
safeDatabase.run(query.result).map(_.toVector)
}
def findByTag(tag: AddressTag): Future[Vector[AddressTagDb]] = {
val query = table
.filter(_.tagName === tag.tagName)
.filter(_.tagType === tag.tagType)
safeDatabase.run(query.result).map(_.toVector)
}
def findByTagType(addressTagType: String): Future[Vector[AddressTagDb]] = {
val tagType = InternalAddressTagType.fromString(addressTagType)
findByTagType(tagType)
}
def findByTagType(tagType: AddressTagType): Future[Vector[AddressTagDb]] = {
val query = table.filter(_.tagType === tagType)
safeDatabase.run(query.result).map(_.toVector)
}
def findTx(
tx: Transaction,
network: NetworkParameters): Future[Vector[AddressTagDb]] = {
val txIds = tx.inputs.map(_.previousOutput.txIdBE)
val infoQuery = spendingInfoTable.filter(_.txid.inSet(txIds))
val spendingInfosF = safeDatabase.runVec(infoQuery.result)
spendingInfosF.flatMap { spendingInfos =>
if (spendingInfos.isEmpty) {
Future.successful(Vector.empty)
} else {
val spks = spendingInfos.map(_.output.scriptPubKey)
val addresses =
spks.map(spk => BitcoinAddress.fromScriptPubKey(spk, network))
val findByAddressFs = addresses.map(address => findByAddress(address))
Future.sequence(findByAddressFs).map(_.flatten)
}
}
}
class AddressTagTable(t: Tag)
extends Table[AddressTagDb](t, "wallet_address_tags") {
def address: Rep[BitcoinAddress] = column[BitcoinAddress]("address")
def tagName: Rep[AddressTagName] = column[AddressTagName]("tag_name")
def tagType: Rep[AddressTagType] = column[AddressTagType]("tag_type")
private type AddressTagTuple =
(BitcoinAddress, AddressTagName, AddressTagType)
private val fromTuple: AddressTagTuple => AddressTagDb = {
case (address, tagName, tagType) =>
AddressTagDb(address, tagName, tagType)
}
private val toTuple: AddressTagDb => Option[AddressTagTuple] =
addrTag => Some((addrTag.address, addrTag.tagName, addrTag.tagType))
override def * : ProvenShape[AddressTagDb] =
(address, tagName, tagType) <> (fromTuple, toTuple)
/** All tags must have an associated address */
def fk_address: ForeignKeyQuery[_, AddressDb] = {
foreignKey("fk_address",
sourceColumns = address,
targetTableQuery = addressTable)(_.address)
}
}
}

View file

@ -0,0 +1,28 @@
package org.bitcoins.wallet.models
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.wallet.utxo.{
AddressTag,
AddressTagName,
AddressTagType,
InternalAddressTag
}
case class AddressTagDb(
address: BitcoinAddress,
tagName: AddressTagName,
tagType: AddressTagType) {
val addressTag: AddressTag = InternalAddressTag(tagName, tagType)
def ==(at: AddressTagDb): Boolean =
address == at.address && addressTag == at.addressTag
def !=(at: AddressTagDb): Boolean = !(this == at)
}
object AddressTagDb {
def apply(address: BitcoinAddress, addressTag: AddressTag): AddressTagDb =
AddressTagDb(address, addressTag.tagName, addressTag.tagType)
}

View file

@ -12,7 +12,7 @@ import org.bitcoins.core.protocol.transaction.{
TransactionOutPoint,
TransactionOutput
}
import org.bitcoins.core.wallet.utxo.TxoState
import org.bitcoins.core.wallet.utxo.{AddressTag, TxoState}
import org.bitcoins.crypto.DoubleSha256DigestBE
import org.bitcoins.db.CRUDAutoInc
import org.bitcoins.wallet.config._
@ -42,6 +42,11 @@ case class SpendingInfoDAO()(implicit
IncomingTransactionDAO().table
}
private lazy val tagTable: profile.api.TableQuery[
AddressTagDAO#AddressTagTable] = {
AddressTagDAO().table
}
/**
* Fetches all the incoming TXOs in our DB that are in
* the given TX
@ -168,6 +173,19 @@ case class SpendingInfoDAO()(implicit
safeDatabase.runVec(query.result).map(_.toVector)
}
def findAllUnspentForTag(tag: AddressTag): Future[Vector[SpendingInfoDb]] = {
val query = table
.filter(_.state.inSet(TxoState.receivedStates))
.join(addrTable)
.on(_.scriptPubKey === _.scriptPubKey)
.join(tagTable)
.on(_._2.address === _.address)
.filter(_._2.tagName === tag.tagName)
.filter(_._2.tagType === tag.tagType)
safeDatabase.runVec(query.result).map(_.map(_._1._1))
}
/**
* This table stores the necessary information to spend
* a transaction output (TXO) at a later point in time. It

View file

@ -41,6 +41,7 @@
"wallet/wallet",
"wallet/wallet-callbacks",
"wallet/wallet-get-address",
"wallet/address-tagging",
"wallet/chain-query-api",
"wallet/node-api",
"wallet/dlc",