mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-03-13 11:35:40 +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
|
||||
}))
|
||||
),
|
||||
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 ==="),
|
||||
cmd("getpeers")
|
||||
.action((_, conf) => conf.copy(command = GetPeers))
|
||||
|
@ -501,6 +534,11 @@ object ConsoleCli {
|
|||
up.writeJs(bitcoins),
|
||||
up.writeJs(feeRateOpt),
|
||||
up.writeJs(algo)))
|
||||
case OpReturnCommit(message, hashMessage, satoshisPerVirtualByte) =>
|
||||
RequestParam("opreturncommit",
|
||||
Seq(up.writeJs(message),
|
||||
up.writeJs(hashMessage),
|
||||
up.writeJs(satoshisPerVirtualByte)))
|
||||
// height
|
||||
case GetBlockCount => RequestParam("getblockcount")
|
||||
// filter count
|
||||
|
@ -636,6 +674,11 @@ object CliCommand {
|
|||
feeRateOpt: Option[SatoshisPerVirtualByte],
|
||||
algo: CoinSelectionAlgo)
|
||||
extends CliCommand
|
||||
case class OpReturnCommit(
|
||||
message: String,
|
||||
hashMessage: Boolean,
|
||||
feeRateOpt: Option[SatoshisPerVirtualByte])
|
||||
extends CliCommand
|
||||
case object GetNewAddress extends CliCommand
|
||||
case object GetUtxos 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 {
|
||||
val route =
|
||||
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 {
|
||||
|
||||
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) =>
|
||||
Rescan.fromJsArr(arr) match {
|
||||
case Failure(exception) =>
|
||||
|
|
|
@ -4,11 +4,15 @@ import org.bitcoins.commons.jsonmodels.wallet.CoinSelectionAlgo
|
|||
import org.bitcoins.core.currency._
|
||||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
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.FundWalletUtil.FundedWallet
|
||||
import org.bitcoins.wallet.api.CoinSelector
|
||||
import org.scalatest.{Assertion, FutureOutcome}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
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 =>
|
||||
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.protocol.BitcoinAddress
|
||||
import org.bitcoins.core.protocol.blockchain.BlockHeader
|
||||
import org.bitcoins.core.protocol.script.ScriptPubKey
|
||||
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.fee.FeeUnit
|
||||
import org.bitcoins.core.wallet.utxo.TxoState
|
||||
|
@ -19,7 +23,7 @@ import org.bitcoins.core.wallet.utxo.TxoState.{
|
|||
ConfirmedReceived,
|
||||
PendingConfirmationsReceived
|
||||
}
|
||||
import org.bitcoins.crypto.ECPublicKey
|
||||
import org.bitcoins.crypto.{CryptoUtil, ECPublicKey}
|
||||
import org.bitcoins.keymanager.KeyManagerParams
|
||||
import org.bitcoins.keymanager.bip39.BIP39KeyManager
|
||||
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.internal._
|
||||
import org.bitcoins.wallet.models.{SpendingInfoDb, _}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Failure, Success}
|
||||
|
@ -320,6 +325,30 @@ abstract class Wallet
|
|||
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(
|
||||
outputs: Vector[TransactionOutput],
|
||||
feeRate: FeeUnit,
|
||||
|
|
|
@ -553,6 +553,22 @@ trait WalletApi extends WalletLogger {
|
|||
} 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]
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Reference in a new issue