diff --git a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala index 380266e0c5..1d47243b23 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala @@ -208,6 +208,21 @@ object ConsoleCli { cmd("stop") .action((_, conf) => conf.copy(command = Stop)) .text("Request a graceful shutdown of Bitcoin-S"), + cmd("sendrawtransaction") + .action((_, conf) => + conf.copy(command = SendRawTransaction(EmptyTransaction))) + .text("Broadcasts the raw transaction") + .children( + arg[Transaction]("tx") + .text("Transaction serialized in hex") + .required() + .action((tx, conf) => + conf.copy(command = conf.command match { + case sendRawTransaction: SendRawTransaction => + sendRawTransaction.copy(tx = tx) + case other => other + })) + ), note(sys.props("line.separator") + "=== PSBT ==="), cmd("combinepsbts") .action((_, conf) => conf.copy(command = CombinePSBTs(Seq.empty))) @@ -356,6 +371,8 @@ object ConsoleCli { // peers case GetPeers => RequestParam("getpeers") case Stop => RequestParam("stop") + case SendRawTransaction(tx) => + RequestParam("sendrawtransaction", Seq(up.writeJs(tx))) // PSBTs case CombinePSBTs(psbts) => RequestParam("combinepsbts", Seq(up.writeJs(psbts))) @@ -477,6 +494,7 @@ object CliCommand { // Node case object GetPeers extends CliCommand case object Stop extends CliCommand + case class SendRawTransaction(tx: Transaction) extends CliCommand // Chain case object GetBestBlockHash extends CliCommand diff --git a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala index 6102ee2c21..e6352a8bc0 100644 --- a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala +++ b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala @@ -393,6 +393,26 @@ class RoutesSpec } } + "send a raw transaction" in { + val tx = Transaction( + "020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000") + + (mockNode + .broadcastTransaction(_: Transaction)) + .expects(tx) + .returning(FutureUtil.unit) + .anyNumberOfTimes() + + val route = + nodeRoutes.handleCommand( + ServerCommand("sendrawtransaction", Arr(Str(tx.hex)))) + + Get() ~> route ~> check { + contentType shouldEqual `application/json` + responseAs[String] shouldEqual s"""{"result":"Broadcasted ${tx.txIdBE}","error":null}""" + } + } + "send to an address" in { // positive cases diff --git a/app/server/src/main/scala/org/bitcoins/server/NodeRoutes.scala b/app/server/src/main/scala/org/bitcoins/server/NodeRoutes.scala index 10affc5650..81a9db2733 100644 --- a/app/server/src/main/scala/org/bitcoins/server/NodeRoutes.scala +++ b/app/server/src/main/scala/org/bitcoins/server/NodeRoutes.scala @@ -6,6 +6,8 @@ import akka.http.scaladsl.server._ import akka.stream.ActorMaterializer import org.bitcoins.node.Node +import scala.util.{Failure, Success} + case class NodeRoutes(node: Node)(implicit system: ActorSystem) extends ServerRoute { import system.dispatcher @@ -25,5 +27,17 @@ case class NodeRoutes(node: Node)(implicit system: ActorSystem) system.terminate() nodeStopping } + + case ServerCommand("sendrawtransaction", arr) => + SendRawTransaction.fromJsArr(arr) match { + case Failure(exception) => + reject(ValidationRejection("failure", Some(exception))) + case Success(SendRawTransaction(tx)) => + complete { + node.broadcastTransaction(tx).map { _ => + Server.httpSuccess(s"Broadcasted ${tx.txIdBE}") + } + } + } } } diff --git a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala index 070ff7910f..8f30cc3bef 100644 --- a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala +++ b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala @@ -68,6 +68,18 @@ object GetAddressInfo extends ServerJsonModels { } } +case class SendRawTransaction(tx: Transaction) + +object SendRawTransaction extends ServerJsonModels { + + def fromJsArr(jsArr: ujson.Arr): Try[SendRawTransaction] = { + require(jsArr.arr.size == 1, + s"Bad number of arguments: ${jsArr.arr.size}. Expected: 1") + + Try(SendRawTransaction(jsToTx(jsArr.arr.head))) + } +} + case class CombinePSBTs(psbts: Seq[PSBT]) object CombinePSBTs extends ServerJsonModels { diff --git a/core/src/main/scala/org/bitcoins/core/api/NodeApi.scala b/core/src/main/scala/org/bitcoins/core/api/NodeApi.scala index b25e5f2bfa..f412de9ea7 100644 --- a/core/src/main/scala/org/bitcoins/core/api/NodeApi.scala +++ b/core/src/main/scala/org/bitcoins/core/api/NodeApi.scala @@ -1,6 +1,7 @@ package org.bitcoins.core.api import org.bitcoins.core.crypto.DoubleSha256Digest +import org.bitcoins.core.protocol.transaction.Transaction import scala.concurrent.Future @@ -9,6 +10,11 @@ import scala.concurrent.Future */ trait NodeApi { + /** + * Broadcasts the given transaction over the P2P network + */ + def broadcastTransaction(transaction: Transaction): Future[Unit] + /** * Request the underlying node to download the given blocks from its peers and feed the blocks to [[org.bitcoins.node.NodeCallbacks]]. */ diff --git a/docs/wallet/node-api.md b/docs/wallet/node-api.md index a7c3dd9ff8..63397a0709 100644 --- a/docs/wallet/node-api.md +++ b/docs/wallet/node-api.md @@ -8,6 +8,7 @@ import akka.actor.ActorSystem import org.bitcoins.core.api.NodeApi import org.bitcoins.core.crypto.DoubleSha256Digest import org.bitcoins.core.protocol.blockchain.Block +import org.bitcoins.core.protocol.transaction.Transaction import org.bitcoins.keymanager.bip39.BIP39KeyManager import org.bitcoins.node.NodeCallbacks import org.bitcoins.node.networking.peer.DataMessageHandler._ @@ -85,6 +86,11 @@ val exampleCallback = createCallback(exampleProcessBlock) // Here is where we are defining our actual node api, Ideally this could be it's own class // but for the examples sake we will keep it small. val nodeApi = new NodeApi { + + override def broadcastTransaction(transaction: Transaction): Future[Unit] = { + bitcoind.sendRawTransaction(transaction).map(_ => ()) + } + override def downloadBlocks( blockHashes: Vector[DoubleSha256Digest]): Future[Unit] = { val blockFs = blockHashes.map(hash => bitcoind.getBlockRaw(hash)) diff --git a/docs/wallet/wallet.md b/docs/wallet/wallet.md index 5e59999711..7fb87eb000 100644 --- a/docs/wallet/wallet.md +++ b/docs/wallet/wallet.md @@ -141,6 +141,7 @@ val keyManager = BIP39KeyManager.initialize(walletConfig.kmParams, bip39Password // once this future completes, we have a initialized // wallet val wallet = Wallet(keyManager, new NodeApi { + override def broadcastTransaction(tx: Transaction): Future[Unit] = Future.successful(()) override def downloadBlocks(blockHashes: Vector[DoubleSha256Digest]): Future[Unit] = Future.successful(()) }, new ChainQueryApi { override def getBlockHeight(blockHash: DoubleSha256DigestBE): Future[Option[Int]] = Future.successful(None) diff --git a/node/src/main/scala/org/bitcoins/node/Node.scala b/node/src/main/scala/org/bitcoins/node/Node.scala index 656cb79e78..aac84607d3 100644 --- a/node/src/main/scala/org/bitcoins/node/Node.scala +++ b/node/src/main/scala/org/bitcoins/node/Node.scala @@ -189,7 +189,7 @@ trait Node extends NodeApi with ChainQueryApi with P2PLogger { } /** Broadcasts the given transaction over the P2P network */ - def broadcastTransaction(transaction: Transaction): Future[Unit] = { + override def broadcastTransaction(transaction: Transaction): Future[Unit] = { val broadcastTx = BroadcastAbleTransaction(transaction) txDAO.create(broadcastTx).onComplete { diff --git a/testkit/src/main/scala/org/bitcoins/testkit/chain/SyncUtil.scala b/testkit/src/main/scala/org/bitcoins/testkit/chain/SyncUtil.scala index 58a73e88bc..98da4fb93b 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/chain/SyncUtil.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/chain/SyncUtil.scala @@ -4,13 +4,14 @@ import org.bitcoins.chain.blockchain.sync.FilterWithHeaderHash import org.bitcoins.core.api.ChainQueryApi.FilterResponse import org.bitcoins.core.api.{ChainQueryApi, NodeApi, NodeChainQueryApi} import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE} -import org.bitcoins.core.gcs.{FilterType} +import org.bitcoins.core.gcs.FilterType import org.bitcoins.core.protocol.BlockStamp import org.bitcoins.core.protocol.blockchain.{Block, BlockHeader} import org.bitcoins.core.util.{BitcoinSLogger, FutureUtil} import org.bitcoins.rpc.client.common.BitcoindRpcClient import org.bitcoins.rpc.client.v19.BitcoindV19RpcClient import org.bitcoins.commons.jsonmodels.bitcoind.GetBlockFilterResult +import org.bitcoins.core.protocol.transaction.Transaction import org.bitcoins.wallet.Wallet import scala.concurrent.{ExecutionContext, Future} @@ -117,6 +118,11 @@ abstract class SyncUtil extends BitcoinSLogger { implicit ec: ExecutionContext): NodeApi = { new NodeApi { + override def broadcastTransaction( + transaction: Transaction): Future[Unit] = { + bitcoindRpcClient.sendRawTransaction(transaction).map(_ => ()) + } + /** * Request the underlying node to download the given blocks from its peers and feed the blocks to [[org.bitcoins.node.NodeCallbacks]]. */ @@ -153,9 +159,6 @@ abstract class SyncUtil extends BitcoinSLogger { walletF: Future[Wallet])(implicit ec: ExecutionContext): NodeApi = { new NodeApi { - /** - * Request the underlying node to download the given blocks from its peers and feed the blocks to [[org.bitcoins.node.NodeCallbacks]]. - */ /** * Request the underlying node to download the given blocks from its peers and feed the blocks to [[org.bitcoins.node.NodeCallbacks]]. */ @@ -196,6 +199,14 @@ abstract class SyncUtil extends BitcoinSLogger { () } } + + /** + * Broadcasts the given transaction over the P2P network + */ + override def broadcastTransaction( + transaction: Transaction): Future[Unit] = { + bitcoindRpcClient.sendRawTransaction(transaction).map(_ => ()) + } } } diff --git a/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala b/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala index 785874c539..922f3f2e20 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala @@ -8,6 +8,7 @@ import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE} import org.bitcoins.core.currency._ import org.bitcoins.core.gcs.BlockFilter import org.bitcoins.core.protocol.BlockStamp +import org.bitcoins.core.protocol.transaction.Transaction import org.bitcoins.core.util.FutureUtil import org.bitcoins.db.AppConfig import org.bitcoins.keymanager.bip39.BIP39KeyManager @@ -249,6 +250,9 @@ object BitcoinSWalletTest extends WalletLogger { object MockNodeApi extends NodeApi { + override def broadcastTransaction(transaction: Transaction): Future[Unit] = + FutureUtil.unit + override def downloadBlocks( blockHashes: Vector[DoubleSha256Digest]): Future[Unit] = FutureUtil.unit diff --git a/wallet/src/main/scala/org/bitcoins/wallet/api/WalletApi.scala b/wallet/src/main/scala/org/bitcoins/wallet/api/WalletApi.scala index 57cb8f2e18..be03b69e65 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/api/WalletApi.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/api/WalletApi.scala @@ -48,6 +48,9 @@ sealed trait WalletApi { */ trait LockedWalletApi extends WalletApi with WalletLogger { + def broadcastTransaction(transaction: Transaction): Future[Unit] = + nodeApi.broadcastTransaction(transaction) + /** * Retrieves a bloom filter that that can be sent to a P2P network node * to get information about our transactions, pubkeys and scripts.