Wallet send from outpoints (#1405)

This commit is contained in:
Ben Carman 2020-05-12 07:24:36 -05:00 committed by GitHub
parent 583da51958
commit c571585b3b
10 changed files with 347 additions and 50 deletions

View file

@ -2,7 +2,7 @@ package org.bitcoins.commons.serializers
import org.bitcoins.core.crypto.ExtPublicKey import org.bitcoins.core.crypto.ExtPublicKey
import org.bitcoins.core.currency.{Bitcoins, Satoshis} import org.bitcoins.core.currency.{Bitcoins, Satoshis}
import org.bitcoins.core.protocol.transaction.Transaction import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutPoint}
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp} import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.psbt.PSBT import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
@ -38,4 +38,7 @@ object Picklers {
implicit val extPubKeyPickler: ReadWriter[ExtPublicKey] = implicit val extPubKeyPickler: ReadWriter[ExtPublicKey] =
readwriter[String].bimap(_.toString, ExtPublicKey.fromString(_).get) readwriter[String].bimap(_.toString, ExtPublicKey.fromString(_).get)
implicit val transactionOutPointPickler: ReadWriter[TransactionOutPoint] =
readwriter[String].bimap(_.hex, TransactionOutPoint.fromHex)
} }

View file

@ -6,7 +6,7 @@ import org.bitcoins.core.config.{NetworkParameters, Networks}
import org.bitcoins.core.currency._ import org.bitcoins.core.currency._
import org.bitcoins.core.protocol.BlockStamp.BlockTime import org.bitcoins.core.protocol.BlockStamp.BlockTime
import org.bitcoins.core.protocol._ import org.bitcoins.core.protocol._
import org.bitcoins.core.protocol.transaction.Transaction import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutPoint}
import org.bitcoins.core.psbt.PSBT import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import scopt._ import scopt._
@ -86,4 +86,11 @@ object CliReaders {
val reads: String => Transaction = Transaction.fromHex val reads: String => Transaction = Transaction.fromHex
} }
implicit val outPointsRead: Read[TransactionOutPoint] =
new Read[TransactionOutPoint] {
val arity: Int = 1
val reads: String => TransactionOutPoint = TransactionOutPoint.fromHex
}
} }

View file

@ -4,7 +4,11 @@ import org.bitcoins.cli.CliCommand._
import org.bitcoins.cli.CliReaders._ import org.bitcoins.cli.CliReaders._
import org.bitcoins.core.config.NetworkParameters import org.bitcoins.core.config.NetworkParameters
import org.bitcoins.core.currency._ import org.bitcoins.core.currency._
import org.bitcoins.core.protocol.transaction.{EmptyTransaction, Transaction} import org.bitcoins.core.protocol.transaction.{
EmptyTransaction,
Transaction,
TransactionOutPoint
}
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp} import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.psbt.PSBT import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
@ -31,7 +35,7 @@ object ConsoleCli {
.action((_, conf) => conf.copy(debug = true)) .action((_, conf) => conf.copy(debug = true))
.text("Print debugging information"), .text("Print debugging information"),
opt[Int]("rpcport") opt[Int]("rpcport")
.action((port,conf) => conf.copy(rpcPort = port)) .action((port, conf) => conf.copy(rpcPort = port))
.text(s"The port to send our rpc request to on the server"), .text(s"The port to send our rpc request to on the server"),
help('h', "help").text("Display this help message and exit"), help('h', "help").text("Display this help message and exit"),
note(sys.props("line.separator") + "Commands:"), note(sys.props("line.separator") + "Commands:"),
@ -218,6 +222,49 @@ object ConsoleCli {
case other => other case other => other
})) }))
), ),
cmd("sendfromoutpoints")
.action((_, conf) =>
conf.copy(
command = SendFromOutPoints(Vector.empty, null, 0.bitcoin, None)))
.text("Send money to the given address")
.children(
arg[Seq[TransactionOutPoint]]("outpoints")
.text("Out Points to send from")
.required()
.action((outPoints, conf) =>
conf.copy(command = conf.command match {
case send: SendFromOutPoints =>
send.copy(outPoints = outPoints.toVector)
case other => other
})),
arg[BitcoinAddress]("address")
.text("Address to send to")
.required()
.action((addr, conf) =>
conf.copy(command = conf.command match {
case send: SendFromOutPoints =>
send.copy(destination = addr)
case other => other
})),
arg[Bitcoins]("amount")
.text("amount to send in BTC")
.required()
.action((btc, conf) =>
conf.copy(command = conf.command match {
case send: SendFromOutPoints =>
send.copy(amount = btc)
case other => other
})),
opt[SatoshisPerVirtualByte]("feerate")
.text("Fee rate in sats per virtual byte")
.optional()
.action((feeRate, conf) =>
conf.copy(command = conf.command match {
case send: SendFromOutPoints =>
send.copy(satoshisPerVirtualByte = Some(feeRate))
case other => other
}))
),
note(sys.props("line.separator") + "=== Network ==="), note(sys.props("line.separator") + "=== Network ==="),
cmd("getpeers") cmd("getpeers")
.action((_, conf) => conf.copy(command = GetPeers)) .action((_, conf) => conf.copy(command = GetPeers))
@ -382,6 +429,15 @@ object ConsoleCli {
Seq(up.writeJs(address), Seq(up.writeJs(address),
up.writeJs(bitcoins), up.writeJs(bitcoins),
up.writeJs(satoshisPerVirtualByte))) up.writeJs(satoshisPerVirtualByte)))
case SendFromOutPoints(outPoints,
address,
bitcoins,
satoshisPerVirtualByte) =>
RequestParam("sendfromoutpoints",
Seq(up.writeJs(outPoints),
up.writeJs(address),
up.writeJs(bitcoins),
up.writeJs(satoshisPerVirtualByte)))
// height // height
case GetBlockCount => RequestParam("getblockcount") case GetBlockCount => RequestParam("getblockcount")
// filter count // filter count
@ -505,6 +561,12 @@ object CliCommand {
amount: Bitcoins, amount: Bitcoins,
satoshisPerVirtualByte: Option[SatoshisPerVirtualByte]) satoshisPerVirtualByte: Option[SatoshisPerVirtualByte])
extends CliCommand extends CliCommand
case class SendFromOutPoints(
outPoints: Vector[TransactionOutPoint],
destination: BitcoinAddress,
amount: Bitcoins,
satoshisPerVirtualByte: Option[SatoshisPerVirtualByte])
extends CliCommand
case object GetNewAddress extends CliCommand case object GetNewAddress extends CliCommand
case object GetUtxos extends CliCommand case object GetUtxos extends CliCommand
case object GetAddresses extends CliCommand case object GetAddresses extends CliCommand

View file

@ -17,12 +17,7 @@ import org.bitcoins.core.protocol.BlockStamp.{
InvalidBlockStamp InvalidBlockStamp
} }
import org.bitcoins.core.protocol.script.EmptyScriptWitness import org.bitcoins.core.protocol.script.EmptyScriptWitness
import org.bitcoins.core.protocol.transaction.{ import org.bitcoins.core.protocol.transaction._
EmptyTransaction,
EmptyTransactionOutPoint,
EmptyTransactionOutput,
Transaction
}
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp, P2PKHAddress} import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp, P2PKHAddress}
import org.bitcoins.core.psbt.PSBT import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.util.FutureUtil import org.bitcoins.core.util.FutureUtil
@ -477,6 +472,85 @@ class RoutesSpec
} }
"send from outpoints" in {
// positive cases
(mockWalletApi
.sendFromOutPoints(_: Vector[TransactionOutPoint],
_: BitcoinAddress,
_: CurrencyUnit,
_: FeeUnit))
.expects(Vector.empty[TransactionOutPoint],
testAddress,
Bitcoins(100),
*)
.returning(Future.successful(EmptyTransaction))
(mockNode.broadcastTransaction _)
.expects(EmptyTransaction)
.returning(FutureUtil.unit)
.anyNumberOfTimes()
val route = walletRoutes.handleCommand(
ServerCommand("sendfromoutpoints",
Arr(Arr(), Str(testAddressStr), Num(100), Num(4))))
Post() ~> route ~> check {
contentType shouldEqual `application/json`
responseAs[String] shouldEqual """{"result":"0000000000000000000000000000000000000000000000000000000000000000","error":null}"""
}
// negative cases
val route1 = walletRoutes.handleCommand(
ServerCommand("sendfromoutpoints", Arr(Arr(), Null, Null, Null)))
Post() ~> route1 ~> check {
rejection shouldEqual ValidationRejection(
"failure",
Some(InvalidData(Null, "Expected ujson.Str")))
}
val route2 = walletRoutes.handleCommand(
ServerCommand("sendfromoutpoints", Arr(Arr(), "Null", Null, Null)))
Post() ~> route2 ~> check {
rejection shouldEqual ValidationRejection(
"failure",
Some(InvalidData("Null", "Expected a valid address")))
}
val route3 = walletRoutes.handleCommand(
ServerCommand("sendfromoutpoints",
Arr(Arr(), Str(testAddressStr), Null, Null)))
Post() ~> route3 ~> check {
rejection shouldEqual ValidationRejection(
"failure",
Some(InvalidData(Null, "Expected ujson.Num")))
}
val route4 = walletRoutes.handleCommand(
ServerCommand("sendfromoutpoints",
Arr(Arr(), Str(testAddressStr), Str("abc"), Null)))
Post() ~> route4 ~> check {
rejection shouldEqual ValidationRejection(
"failure",
Some(InvalidData("abc", "Expected ujson.Num")))
}
val route5 = walletRoutes.handleCommand(
ServerCommand("sendfromoutpoints",
Arr(Null, Str(testAddressStr), Num(100), Num(4))))
Post() ~> route5 ~> check {
rejection shouldEqual ValidationRejection(
"failure",
Some(InvalidData(Null, "Expected ujson.Arr")))
}
}
"return the peer list" in { "return the peer list" in {
val route = val route =
nodeRoutes.handleCommand(ServerCommand("getpeers", Arr())) nodeRoutes.handleCommand(ServerCommand("getpeers", Arr()))

View file

@ -2,7 +2,7 @@ package org.bitcoins.server
import org.bitcoins.core.currency.{Bitcoins, Satoshis} import org.bitcoins.core.currency.{Bitcoins, Satoshis}
import org.bitcoins.core.protocol.BlockStamp.BlockHeight import org.bitcoins.core.protocol.BlockStamp.BlockHeight
import org.bitcoins.core.protocol.transaction.Transaction import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutPoint}
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp} import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.psbt.PSBT import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
@ -235,6 +235,43 @@ object SendToAddress extends ServerJsonModels {
} }
case class SendFromOutpoints(
outPoints: Vector[TransactionOutPoint],
address: BitcoinAddress,
amount: Bitcoins,
satoshisPerVirtualByte: Option[SatoshisPerVirtualByte])
object SendFromOutpoints extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[SendFromOutpoints] = {
jsArr.arr.toList match {
case outPointsJs :: addrJs :: bitcoinsJs :: satsPerVBytesJs :: Nil =>
Try {
val outPoints = jsToTransactionOutPointSeq(outPointsJs).toVector
val address = jsToBitcoinAddress(addrJs)
val bitcoins = Bitcoins(bitcoinsJs.num)
val satoshisPerVirtualByte =
nullToOpt(satsPerVBytesJs).map(satsPerVBytes =>
SatoshisPerVirtualByte(Satoshis(satsPerVBytes.num.toLong)))
SendFromOutpoints(outPoints,
address,
bitcoins,
satoshisPerVirtualByte)
}
case Nil =>
Failure(
new IllegalArgumentException(
"Missing outPoints, address, amount, and fee rate arguments"))
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 3"))
}
}
}
trait ServerJsonModels { trait ServerJsonModels {
def jsToBitcoinAddress(js: Value): BitcoinAddress = { def jsToBitcoinAddress(js: Value): BitcoinAddress = {
@ -252,6 +289,14 @@ trait ServerJsonModels {
def jsToPSBT(js: Value): PSBT = PSBT.fromString(js.str) def jsToPSBT(js: Value): PSBT = PSBT.fromString(js.str)
def jsToTransactionOutPointSeq(js: Value): Seq[TransactionOutPoint] = {
js.arr.foldLeft(Seq.empty[TransactionOutPoint])((seq, outPoint) =>
seq :+ jsToTransactionOutPoint(outPoint))
}
def jsToTransactionOutPoint(js: Value): TransactionOutPoint =
TransactionOutPoint(js.str)
def jsToTx(js: Value): Transaction = Transaction.fromHex(js.str) def jsToTx(js: Value): Transaction = Transaction.fromHex(js.str)
def nullToOpt(value: Value): Option[Value] = value match { def nullToOpt(value: Value): Option[Value] = value match {

View file

@ -107,6 +107,30 @@ case class WalletRoutes(wallet: WalletApi, node: Node)(
} }
} }
case ServerCommand("sendfromoutpoints", arr) =>
SendFromOutpoints.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(
SendFromOutpoints(outPoints,
address,
bitcoins,
satoshisPerVirtualByteOpt)) =>
complete {
// TODO dynamic fees based off mempool and recent blocks
val feeRate =
satoshisPerVirtualByteOpt.getOrElse(SatoshisPerByte(100.satoshis))
for {
tx <- wallet.sendFromOutPoints(outPoints,
address,
bitcoins,
feeRate)
_ <- node.broadcastTransaction(tx)
} yield Server.httpSuccess(tx.txIdBE)
}
}
case ServerCommand("rescan", arr) => case ServerCommand("rescan", arr) =>
Rescan.fromJsArr(arr) match { Rescan.fromJsArr(arr) match {
case Failure(exception) => case Failure(exception) =>

View file

@ -17,15 +17,16 @@ class WalletSendingTest extends BitcoinSWalletTest {
behavior of "Wallet" behavior of "Wallet"
val testAddress: BitcoinAddress =
BitcoinAddress("bcrt1qlhctylgvdsvaanv539rg7hyn0sjkdm23y70kgq").get
val amountToSend: Bitcoins = Bitcoins(0.5)
val feeRate: SatoshisPerByte = SatoshisPerByte(Satoshis.one) val feeRate: SatoshisPerByte = SatoshisPerByte(Satoshis.one)
it should "correctly send to an address" in { fundedWallet => it should "correctly send to an address" in { fundedWallet =>
val wallet = fundedWallet.wallet val wallet = fundedWallet.wallet
val testAddress =
BitcoinAddress("bcrt1qlhctylgvdsvaanv539rg7hyn0sjkdm23y70kgq").get
val amountToSend: Bitcoins = Bitcoins(0.5)
for { for {
tx <- wallet.sendToAddress(testAddress, amountToSend, feeRate) tx <- wallet.sendToAddress(testAddress, amountToSend, feeRate)
} yield { } yield {
@ -140,4 +141,27 @@ class WalletSendingTest extends BitcoinSWalletTest {
sendToAddressesF sendToAddressesF
} }
} }
it should "correctly send from outpoints" in { fundedWallet =>
val wallet = fundedWallet.wallet
for {
allOutPoints <- wallet.spendingInfoDAO.findAllOutpoints()
// use half of them
outPoints = allOutPoints.drop(allOutPoints.size / 2)
tx <- wallet.sendFromOutPoints(outPoints,
testAddress,
amountToSend,
feeRate)
} yield {
assert(outPoints.forall(outPoint =>
tx.inputs.exists(_.previousOutput == outPoint)),
"Every outpoint was not included included")
assert(tx.inputs.size == outPoints.size, "An extra input was added")
val expectedOutput =
TransactionOutput(amountToSend, testAddress.scriptPubKey)
assert(tx.outputs.contains(expectedOutput),
"Did not contain expected output")
}
}
} }

View file

@ -4,12 +4,14 @@ import java.time.Instant
import org.bitcoins.core.api.{ChainQueryApi, NodeApi} import org.bitcoins.core.api.{ChainQueryApi, NodeApi}
import org.bitcoins.core.bloom.{BloomFilter, BloomUpdateAll} import org.bitcoins.core.bloom.{BloomFilter, BloomUpdateAll}
import org.bitcoins.core.config.BitcoinNetwork
import org.bitcoins.core.crypto.ExtPublicKey import org.bitcoins.core.crypto.ExtPublicKey
import org.bitcoins.core.currency._ import org.bitcoins.core.currency._
import org.bitcoins.core.hd.{HDAccount, HDCoin, HDPurposes} import org.bitcoins.core.hd.{HDAccount, HDCoin, HDPurposes}
import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.blockchain.BlockHeader import org.bitcoins.core.protocol.blockchain.BlockHeader
import org.bitcoins.core.protocol.transaction._ import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.wallet.builder.BitcoinTxBuilder
import org.bitcoins.core.wallet.fee.FeeUnit import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.core.wallet.utxo.TxoState import org.bitcoins.core.wallet.utxo.TxoState
import org.bitcoins.core.wallet.utxo.TxoState.{ import org.bitcoins.core.wallet.utxo.TxoState.{
@ -205,6 +207,62 @@ abstract class Wallet
} yield updatedInfos } yield updatedInfos
} }
/** Takes a [[BitcoinTxBuilder]] for a transaction to be sent, and completes it by:
* signing the transaction, then correctly processing the it and logging it
*/
private def finishSend(txBuilder: BitcoinTxBuilder): Future[Transaction] = {
for {
signed <- txBuilder.sign
ourOuts <- findOurOuts(signed)
_ <- processOurTransaction(transaction = signed,
feeRate = txBuilder.feeRate,
inputAmount = txBuilder.creditingAmount,
sentAmount = txBuilder.destinationAmount,
blockHashOpt = None)
} yield {
logger.debug(
s"Signed transaction=${signed.txIdBE.hex} with outputs=${signed.outputs.length}, inputs=${signed.inputs.length}")
logger.trace(s"Change output(s) for transaction=${signed.txIdBE.hex}")
ourOuts.foreach { out =>
logger.trace(s" $out")
}
signed
}
}
override def sendFromOutPoints(
outPoints: Vector[TransactionOutPoint],
address: BitcoinAddress,
amount: CurrencyUnit,
feeRate: FeeUnit,
fromAccount: AccountDb): Future[Transaction] = {
require(
address.networkParameters.isSameNetworkBytes(networkParameters),
s"Cannot send to address on other network, got ${address.networkParameters}"
)
logger.info(s"Sending $amount to $address at feerate $feeRate")
for {
utxoDbs <- spendingInfoDAO.findByOutPoints(outPoints)
diff = utxoDbs.map(_.outPoint).diff(outPoints)
_ = require(diff.isEmpty,
s"Not all OutPoints belong to this wallet, diff $diff")
utxos = utxoDbs.map(_.toUTXOSpendingInfo(keyManager))
changeAddr <- getNewChangeAddress(fromAccount.hdAccount)
output = TransactionOutput(amount, address.scriptPubKey)
txBuilder <- BitcoinTxBuilder(
Vector(output),
utxos,
feeRate,
changeAddr.scriptPubKey,
networkParameters.asInstanceOf[BitcoinNetwork])
tx <- finishSend(txBuilder)
} yield tx
}
override def sendToAddress( override def sendToAddress(
address: BitcoinAddress, address: BitcoinAddress,
amount: CurrencyUnit, amount: CurrencyUnit,
@ -222,23 +280,9 @@ abstract class Wallet
feeRate = feeRate, feeRate = feeRate,
fromAccount = fromAccount, fromAccount = fromAccount,
keyManagerOpt = Some(keyManager)) keyManagerOpt = Some(keyManager))
signed <- txBuilder.sign
ourOuts <- findOurOuts(signed)
_ <- processOurTransaction(transaction = signed,
feeRate = feeRate,
inputAmount = txBuilder.creditingAmount,
sentAmount = txBuilder.destinationAmount,
blockHashOpt = None)
} yield {
logger.debug(
s"Signed transaction=${signed.txIdBE.hex} with outputs=${signed.outputs.length}, inputs=${signed.inputs.length}")
logger.trace(s"Change output(s) for transaction=${signed.txIdBE.hex}") tx <- finishSend(txBuilder)
ourOuts.foreach { out => } yield tx
logger.trace(s" $out")
}
signed
}
} }
override def sendToAddresses( override def sendToAddresses(
@ -273,23 +317,8 @@ abstract class Wallet
fromAccount = fromAccount, fromAccount = fromAccount,
keyManagerOpt = Some(keyManager), keyManagerOpt = Some(keyManager),
markAsReserved = reserveUtxos) markAsReserved = reserveUtxos)
signed <- txBuilder.sign tx <- finishSend(txBuilder)
ourOuts <- findOurOuts(signed) } yield tx
_ <- processOurTransaction(transaction = signed,
feeRate = feeRate,
inputAmount = txBuilder.creditingAmount,
sentAmount = txBuilder.destinationAmount,
blockHashOpt = None)
} yield {
logger.debug(
s"Signed transaction=${signed.txIdBE.hex} with outputs=${signed.outputs.length}, inputs=${signed.inputs.length}")
logger.trace(s"Change output(s) for transaction=${signed.txIdBE.hex}")
ourOuts.foreach { out =>
logger.trace(s" $out")
}
signed
}
} }
/** Creates a new account my reading from our account database, finding the last account, /** Creates a new account my reading from our account database, finding the last account,

View file

@ -10,7 +10,11 @@ import org.bitcoins.core.gcs.{GolombFilter, SimpleFilterMatcher}
import org.bitcoins.core.hd.{AddressType, HDAccount, HDChainType, HDPurpose} import org.bitcoins.core.hd.{AddressType, HDAccount, HDChainType, HDPurpose}
import org.bitcoins.core.protocol.blockchain.{Block, BlockHeader, ChainParams} import org.bitcoins.core.protocol.blockchain.{Block, BlockHeader, ChainParams}
import org.bitcoins.core.protocol.script.ScriptPubKey import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutput} import org.bitcoins.core.protocol.transaction.{
Transaction,
TransactionOutPoint,
TransactionOutput
}
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp} import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.util.FutureUtil import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.wallet.fee.FeeUnit import org.bitcoins.core.wallet.fee.FeeUnit
@ -415,6 +419,24 @@ trait WalletApi extends WalletLogger {
def keyManager: BIP39KeyManager def keyManager: BIP39KeyManager
def sendFromOutPoints(
outPoints: Vector[TransactionOutPoint],
address: BitcoinAddress,
amount: CurrencyUnit,
feeRate: FeeUnit,
fromAccount: AccountDb): Future[Transaction]
def sendFromOutPoints(
outPoints: Vector[TransactionOutPoint],
address: BitcoinAddress,
amount: CurrencyUnit,
feeRate: FeeUnit): Future[Transaction] = {
for {
account <- getDefaultAccount()
tx <- sendFromOutPoints(outPoints, address, amount, feeRate, account)
} yield tx
}
/** /**
* *
* Sends money from the specified account * Sends money from the specified account

View file

@ -163,6 +163,13 @@ case class SpendingInfoDAO()(
safeDatabase.runVec(query.result).map(_.toVector) safeDatabase.runVec(query.result).map(_.toVector)
} }
/** Enumerates all TX outpoints in the wallet */
def findByOutPoints(outPoints: Vector[TransactionOutPoint]): Future[
Vector[SpendingInfoDb]] = {
val query = table.filter(_.outPoint.inSet(outPoints))
safeDatabase.runVec(query.result).map(_.toVector)
}
/** /**
* This table stores the necessary information to spend * This table stores the necessary information to spend
* a transaction output (TXO) at a later point in time. It * a transaction output (TXO) at a later point in time. It