mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-02-22 22:36:34 +01:00
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:
parent
18fe4da989
commit
ecd3449100
28 changed files with 1046 additions and 68 deletions
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
143
docs/wallet/address-tagging.md
Normal file
143
docs/wallet/address-tagging.md
Normal 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)
|
||||
}
|
||||
}
|
||||
```
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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] = {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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] = {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Reference in a new issue