mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-03-15 20:30:17 +01:00
Add ability to make OP_RETURN commitments (#1417)
This commit is contained in:
parent
cec29e6894
commit
51fcb793be
7 changed files with 227 additions and 2 deletions
|
@ -319,6 +319,39 @@ object ConsoleCli {
|
||||||
case other => other
|
case other => other
|
||||||
}))
|
}))
|
||||||
),
|
),
|
||||||
|
cmd("opreturncommit")
|
||||||
|
.action((_, conf) =>
|
||||||
|
conf.copy(command = OpReturnCommit("", hashMessage = false, None)))
|
||||||
|
.text("Send money to the given address")
|
||||||
|
.children(
|
||||||
|
arg[String]("message")
|
||||||
|
.text("message to put into OP_RETURN commitment")
|
||||||
|
.required()
|
||||||
|
.action((message, conf) =>
|
||||||
|
conf.copy(command = conf.command match {
|
||||||
|
case opReturnCommit: OpReturnCommit =>
|
||||||
|
opReturnCommit.copy(message = message)
|
||||||
|
case other => other
|
||||||
|
})),
|
||||||
|
opt[Unit]("hashMessage")
|
||||||
|
.text("should the message be hashed before commitment")
|
||||||
|
.optional()
|
||||||
|
.action((_, conf) =>
|
||||||
|
conf.copy(command = conf.command match {
|
||||||
|
case opReturnCommit: OpReturnCommit =>
|
||||||
|
opReturnCommit.copy(hashMessage = true)
|
||||||
|
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 opReturnCommit: OpReturnCommit =>
|
||||||
|
opReturnCommit.copy(feeRateOpt = 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))
|
||||||
|
@ -501,6 +534,11 @@ object ConsoleCli {
|
||||||
up.writeJs(bitcoins),
|
up.writeJs(bitcoins),
|
||||||
up.writeJs(feeRateOpt),
|
up.writeJs(feeRateOpt),
|
||||||
up.writeJs(algo)))
|
up.writeJs(algo)))
|
||||||
|
case OpReturnCommit(message, hashMessage, satoshisPerVirtualByte) =>
|
||||||
|
RequestParam("opreturncommit",
|
||||||
|
Seq(up.writeJs(message),
|
||||||
|
up.writeJs(hashMessage),
|
||||||
|
up.writeJs(satoshisPerVirtualByte)))
|
||||||
// height
|
// height
|
||||||
case GetBlockCount => RequestParam("getblockcount")
|
case GetBlockCount => RequestParam("getblockcount")
|
||||||
// filter count
|
// filter count
|
||||||
|
@ -636,6 +674,11 @@ object CliCommand {
|
||||||
feeRateOpt: Option[SatoshisPerVirtualByte],
|
feeRateOpt: Option[SatoshisPerVirtualByte],
|
||||||
algo: CoinSelectionAlgo)
|
algo: CoinSelectionAlgo)
|
||||||
extends CliCommand
|
extends CliCommand
|
||||||
|
case class OpReturnCommit(
|
||||||
|
message: String,
|
||||||
|
hashMessage: Boolean,
|
||||||
|
feeRateOpt: 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
|
||||||
|
|
|
@ -703,6 +703,29 @@ class RoutesSpec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"make an OP_RETURN commitment" in {
|
||||||
|
|
||||||
|
val message = "Never gonna give you up, never gonna let you down"
|
||||||
|
|
||||||
|
(mockWalletApi
|
||||||
|
.makeOpReturnCommitment(_: String, _: Boolean, _: FeeUnit))
|
||||||
|
.expects(message, false, *)
|
||||||
|
.returning(Future.successful(EmptyTransaction))
|
||||||
|
|
||||||
|
(mockNode.broadcastTransaction _)
|
||||||
|
.expects(EmptyTransaction)
|
||||||
|
.returning(FutureUtil.unit)
|
||||||
|
.anyNumberOfTimes()
|
||||||
|
|
||||||
|
val route = walletRoutes.handleCommand(
|
||||||
|
ServerCommand("opreturncommit", Arr(message, Bool(false), Num(4))))
|
||||||
|
|
||||||
|
Post() ~> route ~> check {
|
||||||
|
contentType shouldEqual `application/json`
|
||||||
|
responseAs[String] shouldEqual """{"result":"0000000000000000000000000000000000000000000000000000000000000000","error":null}"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
"return the peer list" in {
|
"return the peer list" in {
|
||||||
val route =
|
val route =
|
||||||
nodeRoutes.handleCommand(ServerCommand("getpeers", Arr()))
|
nodeRoutes.handleCommand(ServerCommand("getpeers", Arr()))
|
||||||
|
|
|
@ -308,6 +308,38 @@ object SendWithAlgo extends ServerJsonModels {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case class OpReturnCommit(
|
||||||
|
message: String,
|
||||||
|
hashMessage: Boolean,
|
||||||
|
feeRateOpt: Option[SatoshisPerVirtualByte])
|
||||||
|
|
||||||
|
object OpReturnCommit extends ServerJsonModels {
|
||||||
|
|
||||||
|
def fromJsArr(jsArr: ujson.Arr): Try[OpReturnCommit] = {
|
||||||
|
jsArr.arr.toList match {
|
||||||
|
case messageJs :: hashMessageJs :: feeRateOptJs :: Nil =>
|
||||||
|
Try {
|
||||||
|
val message = messageJs.str
|
||||||
|
val hashMessage = hashMessageJs.bool
|
||||||
|
val feeRateOpt =
|
||||||
|
nullToOpt(feeRateOptJs).map(satsPerVBytes =>
|
||||||
|
SatoshisPerVirtualByte(Satoshis(satsPerVBytes.num.toLong)))
|
||||||
|
OpReturnCommit(message, hashMessage, feeRateOpt)
|
||||||
|
}
|
||||||
|
case Nil =>
|
||||||
|
Failure(
|
||||||
|
new IllegalArgumentException(
|
||||||
|
"Missing message, hashMessage, 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 = {
|
||||||
|
|
|
@ -149,6 +149,24 @@ case class WalletRoutes(wallet: WalletApi, node: Node)(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case ServerCommand("opreturncommit", arr) =>
|
||||||
|
OpReturnCommit.fromJsArr(arr) match {
|
||||||
|
case Failure(exception) =>
|
||||||
|
reject(ValidationRejection("failure", Some(exception)))
|
||||||
|
case Success(
|
||||||
|
OpReturnCommit(message, hashMessage, satoshisPerVirtualByteOpt)) =>
|
||||||
|
complete {
|
||||||
|
// TODO dynamic fees based off mempool and recent blocks
|
||||||
|
val feeRate =
|
||||||
|
satoshisPerVirtualByteOpt.getOrElse(SatoshisPerByte(100.satoshis))
|
||||||
|
wallet.makeOpReturnCommitment(message, hashMessage, feeRate).map {
|
||||||
|
tx =>
|
||||||
|
node.broadcastTransaction(tx)
|
||||||
|
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) =>
|
||||||
|
|
|
@ -4,11 +4,15 @@ import org.bitcoins.commons.jsonmodels.wallet.CoinSelectionAlgo
|
||||||
import org.bitcoins.core.currency._
|
import org.bitcoins.core.currency._
|
||||||
import org.bitcoins.core.protocol.BitcoinAddress
|
import org.bitcoins.core.protocol.BitcoinAddress
|
||||||
import org.bitcoins.core.protocol.transaction.TransactionOutput
|
import org.bitcoins.core.protocol.transaction.TransactionOutput
|
||||||
import org.bitcoins.core.wallet.fee.SatoshisPerByte
|
import org.bitcoins.core.script.constant.{BytesToPushOntoStack, ScriptConstant}
|
||||||
|
import org.bitcoins.core.script.control.OP_RETURN
|
||||||
|
import org.bitcoins.core.wallet.fee.{SatoshisPerByte, SatoshisPerVirtualByte}
|
||||||
|
import org.bitcoins.crypto.CryptoUtil
|
||||||
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
|
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
|
||||||
import org.bitcoins.testkit.wallet.FundWalletUtil.FundedWallet
|
import org.bitcoins.testkit.wallet.FundWalletUtil.FundedWallet
|
||||||
import org.bitcoins.wallet.api.CoinSelector
|
import org.bitcoins.wallet.api.CoinSelector
|
||||||
import org.scalatest.{Assertion, FutureOutcome}
|
import org.scalatest.{Assertion, FutureOutcome}
|
||||||
|
import scodec.bits.ByteVector
|
||||||
|
|
||||||
import scala.concurrent.Future
|
import scala.concurrent.Future
|
||||||
|
|
||||||
|
@ -114,6 +118,66 @@ class WalletSendingTest extends BitcoinSWalletTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def testOpReturnCommitment(
|
||||||
|
wallet: Wallet,
|
||||||
|
hashMessage: Boolean): Future[Assertion] = {
|
||||||
|
val message = "ben was here"
|
||||||
|
|
||||||
|
for {
|
||||||
|
tx <- wallet.makeOpReturnCommitment(message,
|
||||||
|
hashMessage = hashMessage,
|
||||||
|
SatoshisPerVirtualByte.one)
|
||||||
|
|
||||||
|
outgoingTxDbOpt <- wallet.outgoingTxDAO.read(tx.txIdBE)
|
||||||
|
} yield {
|
||||||
|
val opReturnOutputOpt = tx.outputs.find(_.value == 0.satoshis)
|
||||||
|
assert(opReturnOutputOpt.isDefined, "Missing output with 0 value")
|
||||||
|
val opReturnOutput = opReturnOutputOpt.get
|
||||||
|
|
||||||
|
val messageBytes = if (hashMessage) {
|
||||||
|
CryptoUtil.sha256(ByteVector(message.getBytes)).bytes
|
||||||
|
} else {
|
||||||
|
ByteVector(message.getBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
val expectedAsm =
|
||||||
|
Seq(OP_RETURN,
|
||||||
|
BytesToPushOntoStack(messageBytes.size),
|
||||||
|
ScriptConstant(messageBytes))
|
||||||
|
|
||||||
|
assert(opReturnOutput.scriptPubKey.asm == expectedAsm)
|
||||||
|
|
||||||
|
assert(outgoingTxDbOpt.isDefined, "Missing outgoing tx in database")
|
||||||
|
val outgoingTxDb = outgoingTxDbOpt.get
|
||||||
|
|
||||||
|
assert(outgoingTxDb.sentAmount == 0.satoshis)
|
||||||
|
val changeOutput = tx.outputs.find(_.value > 0.satoshis).get
|
||||||
|
assert(
|
||||||
|
outgoingTxDb.actualFee + changeOutput.value == outgoingTxDb.inputAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it should "correctly make a hashed OP_RETURN commitment" in { fundedWallet =>
|
||||||
|
testOpReturnCommitment(fundedWallet.wallet, hashMessage = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
it should "correctly make an unhashed OP_RETURN commitment" in {
|
||||||
|
fundedWallet =>
|
||||||
|
testOpReturnCommitment(fundedWallet.wallet, hashMessage = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
it should "fail to make an OP_RETURN commitment that is too long" in {
|
||||||
|
fundedWallet =>
|
||||||
|
val wallet = fundedWallet.wallet
|
||||||
|
|
||||||
|
recoverToSucceededIf[IllegalArgumentException] {
|
||||||
|
wallet.makeOpReturnCommitment(
|
||||||
|
"This message is much too long and is over 80 bytes, the limit for OP_RETURN. It should cause an error.",
|
||||||
|
hashMessage = false,
|
||||||
|
feeRate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
it should "fail to send to a different network address" in { fundedWallet =>
|
it should "fail to send to a different network address" in { fundedWallet =>
|
||||||
val wallet = fundedWallet.wallet
|
val wallet = fundedWallet.wallet
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,11 @@ 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.script.ScriptPubKey
|
||||||
import org.bitcoins.core.protocol.transaction._
|
import org.bitcoins.core.protocol.transaction._
|
||||||
|
import org.bitcoins.core.script.constant.ScriptConstant
|
||||||
|
import org.bitcoins.core.script.control.OP_RETURN
|
||||||
|
import org.bitcoins.core.util.BitcoinScriptUtil
|
||||||
import org.bitcoins.core.wallet.builder.BitcoinTxBuilder
|
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
|
||||||
|
@ -19,7 +23,7 @@ import org.bitcoins.core.wallet.utxo.TxoState.{
|
||||||
ConfirmedReceived,
|
ConfirmedReceived,
|
||||||
PendingConfirmationsReceived
|
PendingConfirmationsReceived
|
||||||
}
|
}
|
||||||
import org.bitcoins.crypto.ECPublicKey
|
import org.bitcoins.crypto.{CryptoUtil, ECPublicKey}
|
||||||
import org.bitcoins.keymanager.KeyManagerParams
|
import org.bitcoins.keymanager.KeyManagerParams
|
||||||
import org.bitcoins.keymanager.bip39.BIP39KeyManager
|
import org.bitcoins.keymanager.bip39.BIP39KeyManager
|
||||||
import org.bitcoins.keymanager.util.HDUtil
|
import org.bitcoins.keymanager.util.HDUtil
|
||||||
|
@ -27,6 +31,7 @@ import org.bitcoins.wallet.api._
|
||||||
import org.bitcoins.wallet.config.WalletAppConfig
|
import org.bitcoins.wallet.config.WalletAppConfig
|
||||||
import org.bitcoins.wallet.internal._
|
import org.bitcoins.wallet.internal._
|
||||||
import org.bitcoins.wallet.models.{SpendingInfoDb, _}
|
import org.bitcoins.wallet.models.{SpendingInfoDb, _}
|
||||||
|
import scodec.bits.ByteVector
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
import scala.util.{Failure, Success}
|
import scala.util.{Failure, Success}
|
||||||
|
@ -320,6 +325,30 @@ abstract class Wallet
|
||||||
sendToOutputs(destinations, feeRate, fromAccount, reserveUtxos)
|
sendToOutputs(destinations, feeRate, fromAccount, reserveUtxos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override def makeOpReturnCommitment(
|
||||||
|
message: String,
|
||||||
|
hashMessage: Boolean,
|
||||||
|
feeRate: FeeUnit,
|
||||||
|
fromAccount: AccountDb): Future[Transaction] = {
|
||||||
|
val messageToUse = if (hashMessage) {
|
||||||
|
CryptoUtil.sha256(ByteVector(message.getBytes)).bytes
|
||||||
|
} else {
|
||||||
|
if (message.length > 80) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
s"Message cannot be greater than 80 characters, it should be hashed, got $message")
|
||||||
|
} else ByteVector(message.getBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
val asm = Seq(OP_RETURN) ++ BitcoinScriptUtil.calculatePushOp(messageToUse) :+ ScriptConstant(
|
||||||
|
messageToUse)
|
||||||
|
|
||||||
|
val scriptPubKey = ScriptPubKey(asm)
|
||||||
|
|
||||||
|
val output = TransactionOutput(0.satoshis, scriptPubKey)
|
||||||
|
|
||||||
|
sendToOutputs(Vector(output), feeRate, fromAccount, reserveUtxos = false)
|
||||||
|
}
|
||||||
|
|
||||||
def sendToOutputs(
|
def sendToOutputs(
|
||||||
outputs: Vector[TransactionOutput],
|
outputs: Vector[TransactionOutput],
|
||||||
feeRate: FeeUnit,
|
feeRate: FeeUnit,
|
||||||
|
|
|
@ -553,6 +553,22 @@ trait WalletApi extends WalletLogger {
|
||||||
} yield tx
|
} yield tx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def makeOpReturnCommitment(
|
||||||
|
message: String,
|
||||||
|
hashMessage: Boolean,
|
||||||
|
feeRate: FeeUnit,
|
||||||
|
fromAccount: AccountDb): Future[Transaction]
|
||||||
|
|
||||||
|
def makeOpReturnCommitment(
|
||||||
|
message: String,
|
||||||
|
hashMessage: Boolean,
|
||||||
|
feeRate: FeeUnit): Future[Transaction] = {
|
||||||
|
for {
|
||||||
|
account <- getDefaultAccount()
|
||||||
|
tx <- makeOpReturnCommitment(message, hashMessage, feeRate, account)
|
||||||
|
} yield tx
|
||||||
|
}
|
||||||
|
|
||||||
def createNewAccount(keyManagerParams: KeyManagerParams): Future[Wallet]
|
def createNewAccount(keyManagerParams: KeyManagerParams): Future[Wallet]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Add table
Reference in a new issue