mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-03-03 10:46:42 +01:00
Wallet send from outpoints (#1405)
This commit is contained in:
parent
583da51958
commit
c571585b3b
10 changed files with 347 additions and 50 deletions
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,8 +35,8 @@ 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:"),
|
||||||
note(sys.props("line.separator") + "===Blockchain ==="),
|
note(sys.props("line.separator") + "===Blockchain ==="),
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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()))
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) =>
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue