diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/ChangeOutputSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/ChangeOutputSpec.scala index 29798c95..48929495 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/ChangeOutputSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/ChangeOutputSpec.scala @@ -1,13 +1,14 @@ package org.ergoplatform.appkit -import org.ergoplatform.{ErgoAddressEncoder, Pay2SAddress} -import org.scalatest.{Matchers, PropSpec} +import org.scalatest.{PropSpec, Matchers} import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks -import scorex.util.encode.Base16 import sigmastate.eval._ import sigmastate.interpreter.CryptoConstants -import sigmastate.serialization.ErgoTreeSerializer import special.sigma.GroupElement +import JavaHelpers._ +import java.util.{List => JList} + +import org.ergoplatform.appkit.Parameters.MinFee class ChangeOutputSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChecks @@ -97,7 +98,7 @@ class ChangeOutputSpec extends PropSpec with Matchers } } - property("NoTokenChangeOutput") { + property("NoTokenChangeOutput + token burning") { val ergoClient = createMockedErgoClient(MockData(Nil, Nil)) val g: GroupElement = CryptoConstants.dlogGroup.generator val x = BigInt("187235612876647164378132684712638457631278").bigInteger @@ -120,6 +121,8 @@ class ChangeOutputSpec extends PropSpec with Matchers )).build().convertToInputWith("f9e5ce5aa0d95f5d54a7bc89c46730d9662397067250aa18a0039631c0f5b809", 0) val tokenId = input0.getId.toString + val tokenAmount = 5000000000L + val tokenAmountToBurn = 2000000000L val ph = ctx.createPreHeader() .height(ctx.getHeight + 1) @@ -128,7 +131,7 @@ class ChangeOutputSpec extends PropSpec with Matchers val txB = ctx.newTxBuilder().preHeader(ph) // for issuing token val tokenBox = txB.outBoxBuilder .value(15000000) // value of token box, doesn't really matter - .tokens(new ErgoToken(tokenId, 5000000000L)) // amount of token issuing + .tokens(new ErgoToken(tokenId, tokenAmount)) // amount of token issuing .contract(ctx.compileContract( // contract of the box containing tokens, just has to be spendable ConstantsBuilder.empty(), "{sigmaProp(1 < 2)}" @@ -148,6 +151,43 @@ class ChangeOutputSpec extends PropSpec with Matchers val outputs = signed.getOutputsToSpend assert(outputs.size == 2) println(signed.toJson(false)) + val boxWithToken = outputs.get(0) + assert(boxWithToken.getTokens.size() == 1) + + // move Ergs from boxWithToken and burn tokens in the transaction + { + val expectedChange = MinFee + val txB = ctx.newTxBuilder() + val ergAmountToSend = boxWithToken.getValue - MinFee - expectedChange + val out = txB.outBoxBuilder + .value(ergAmountToSend) + .contract(ctx.compileContract( // contract of the box containing tokens, just has to be spendable + ConstantsBuilder.empty(), "{sigmaProp(1 < 2)}" + )) + .build() + val unsigned = txB + .boxesToSpend(IndexedSeq(boxWithToken).convertTo[JList[InputBox]]) + .outputs(out) + .tokensToBurn(new ErgoToken(tokenId, tokenAmountToBurn)) + .fee(MinFee) + .sendChangeTo(changeAddr) + .build() + val signed = ctx.newProverBuilder().build().sign(unsigned) + val outputs = signed.getOutputsToSpend + assert(outputs.size == 3) + val out0 = outputs.get(0) + val fee = outputs.get(1) + val change = outputs.get(2) + out0.getValue shouldBe ergAmountToSend + out0.getTokens.size() shouldBe 0 + + fee.getValue shouldBe MinFee + fee.getTokens.size shouldBe 0 + + change.getValue shouldBe expectedChange + change.getTokens.size() shouldBe 1 + change.getTokens.get(0).getValue shouldBe (tokenAmount - tokenAmountToBurn) + } } } } diff --git a/build.sbt b/build.sbt index 9c8c114c..5050abfa 100644 --- a/build.sbt +++ b/build.sbt @@ -122,7 +122,7 @@ assemblyMergeStrategy in assembly := { lazy val allConfigDependency = "compile->compile;test->test" val sigmaStateVersion = "3.3.1" -val ergoWalletVersion = "v3.2.3-11f8615f-SNAPSHOT" +val ergoWalletVersion = "v3.3.3-c42d8b5b-SNAPSHOT" lazy val sigmaState = ("org.scorexfoundation" %% "sigma-state" % sigmaStateVersion).force() .exclude("ch.qos.logback", "logback-classic") .exclude("org.scorexfoundation", "scrypto") diff --git a/common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala b/common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala index 38115442..36c9b10f 100644 --- a/common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala +++ b/common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala @@ -25,6 +25,7 @@ import java.util.{List => JList, Map => JMap} import sigmastate.utils.Helpers._ // don't remove, required for Scala 2.11 import org.ergoplatform.ErgoAddressEncoder.NetworkPrefix +import org.ergoplatform.wallet.TokensMap import scorex.util.encode.Base16 import sigmastate.basics.DLogProtocol.ProveDlog import sigmastate.basics.{ProveDHTuple, DiffieHellmanTupleProverInput} @@ -79,22 +80,23 @@ object Iso extends LowPriorityIsos { override def from(t: (TokenId, Long)): ErgoToken = new ErgoToken(t._1, t._2) } - implicit val isoJListErgoTokenToMapPair: Iso[JList[ErgoToken], mutable.LinkedHashMap[ModifierId, Long]] = + implicit val isoJListErgoTokenToMapPair: Iso[JList[ErgoToken], mutable.LinkedHashMap[ModifierId, Long]] = new Iso[JList[ErgoToken], mutable.LinkedHashMap[ModifierId, Long]] { - override def to(a: JList[ErgoToken]): mutable.LinkedHashMap[ModifierId, Long] = { - import JavaHelpers._ - val lhm = new mutable.LinkedHashMap[ModifierId, Long]() - a.convertTo[IndexedSeq[(TokenId, Long)]] - .map(t => bytesToId(t._1) -> t._2) - .foldLeft(lhm)(_ += _) - } - override def from(t: mutable.LinkedHashMap[ModifierId, Long]): JList[ErgoToken] = { - import JavaHelpers._ - val pairs: IndexedSeq[(TokenId, Long)] = t.toIndexedSeq - .map(t => (Digest32 @@ idToBytes(t._1)) -> t._2) - pairs.convertTo[JList[ErgoToken]] + override def to(a: JList[ErgoToken]): mutable.LinkedHashMap[ModifierId, Long] = { + import JavaHelpers._ + val lhm = new mutable.LinkedHashMap[ModifierId, Long]() + a.convertTo[IndexedSeq[(TokenId, Long)]] + .map(t => bytesToId(t._1) -> t._2) + .foldLeft(lhm)(_ += _) + } + + override def from(t: mutable.LinkedHashMap[ModifierId, Long]): JList[ErgoToken] = { + import JavaHelpers._ + val pairs: IndexedSeq[(TokenId, Long)] = t.toIndexedSeq + .map(t => (Digest32 @@ idToBytes(t._1)) -> t._2) + pairs.convertTo[JList[ErgoToken]] + } } - } implicit val isoErgoTypeToSType: Iso[ErgoType[_], SType] = new Iso[ErgoType[_], SType] { override def to(et: ErgoType[_]): SType = Evaluation.rtypeToSType(et.getRType) @@ -347,6 +349,9 @@ object JavaHelpers { DiffieHellmanTupleProverInput(x, dht) } + def createTokensMap(linkedMap: mutable.LinkedHashMap[ModifierId, Long]): TokensMap = { + linkedMap.toMap + } } diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/SignedTransaction.java b/lib-api/src/main/java/org/ergoplatform/appkit/SignedTransaction.java index cbe88307..7615ddcc 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/SignedTransaction.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/SignedTransaction.java @@ -28,7 +28,7 @@ public interface SignedTransaction { List getSignedInputs(); /** - * Outputs of this transaction represented as {@link InputBox} objects read to be spent in the next + * Outputs of this transaction represented as {@link InputBox} objects ready to be spent in the next * chained transaction. * This method can be used to create a chain of transactions. Thus {@code tx1.getOutputsToSpend()} returns * a list of boxes which are ready to be included as input boxes to a new tx2. diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/UnsignedTransactionBuilder.java b/lib-api/src/main/java/org/ergoplatform/appkit/UnsignedTransactionBuilder.java index 4a4083a3..f02e0069 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/UnsignedTransactionBuilder.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/UnsignedTransactionBuilder.java @@ -61,6 +61,20 @@ public interface UnsignedTransactionBuilder { */ UnsignedTransactionBuilder fee(long feeAmount); + /** + * Configures amounts for tokens to be burnt. + * Each Ergo box can store zero or more tokens (aka assets). + * In contrast to strict requirement on ERG balance between transaction inputs and outputs, + * the amounts of output tokens can be less then the amounts of input tokens. + * This is interpreted as token burning i.e. reducing the total amount of tokens in + * circulation in the blockchain. + * Note, once issued/burnt, the amount of tokens in circulation cannot be increased. + * + * @param tokens one or more tokens to be burnt as part of the transaction. + * @see ErgoToken + */ + UnsignedTransactionBuilder tokensToBurn(ErgoToken... tokens); + /** * Adds change output to the specified address if needed. * diff --git a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.java b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.java index c6503688..7d7f6f4e 100644 --- a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.java +++ b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.java @@ -7,8 +7,8 @@ import org.ergoplatform.wallet.transactions.TransactionBuilder; import org.ergoplatform.wallet.boxes.DefaultBoxSelector$; import org.ergoplatform.wallet.boxes.BoxSelector; -import scala.Option; import scala.collection.IndexedSeq; +import scala.collection.immutable.Map; import special.collection.Coll; import special.sigma.Header; import special.sigma.PreHeader; @@ -29,6 +29,7 @@ public class UnsignedTransactionBuilderImpl implements UnsignedTransactionBuilde ArrayList _outputCandidates = new ArrayList<>(); private List _inputBoxes; private List _dataInputBoxes = new ArrayList<>(); + private List _tokensToBurn = new ArrayList<>(); private long _feeAmount; private ErgoAddress _changeAddress; private PreHeaderImpl _ph; @@ -86,6 +87,12 @@ public UnsignedTransactionBuilder fee(long feeAmount) { return this; } + @Override + public UnsignedTransactionBuilder tokensToBurn(ErgoToken... tokens) { + _tokensToBurn.addAll(Stream.of(tokens).collect(Collectors.toList())); + return this; + } + private void appendOutputs(OutBox... outputs) { ErgoBoxCandidate[] boxes = Stream.of(outputs).map(c -> ((OutBoxImpl)c).getErgoBoxCandidate()).toArray(n -> new ErgoBoxCandidate[n]); @@ -101,8 +108,12 @@ public UnsignedTransactionBuilder sendChangeTo(ErgoAddress changeAddress) { @Override public UnsignedTransaction build() { - List boxesToSpend = _inputBoxes.stream().map(b -> b.getErgoBox()).collect(Collectors.toList()); - List dataInputBoxes = _dataInputBoxes.stream().map(b -> b.getErgoBox()).collect(Collectors.toList()); + List boxesToSpend = _inputBoxes.stream() + .map(b -> b.getErgoBox()) + .collect(Collectors.toList()); + List dataInputBoxes = _dataInputBoxes.stream() + .map(b -> b.getErgoBox()) + .collect(Collectors.toList()); IndexedSeq dataInputs = JavaHelpers.toIndexedSeq(_dataInputs); checkState(_feeAmount > 0, "Fee amount should be defined (using fee() method)."); @@ -111,16 +122,20 @@ public UnsignedTransaction build() { IndexedSeq outputCandidates = JavaHelpers.toIndexedSeq(_outputCandidates); IndexedSeq inputBoxes = JavaHelpers.toIndexedSeq(boxesToSpend); + Map burnTokens = JavaHelpers.createTokensMap( + Iso$.MODULE$.isoJListErgoTokenToMapPair().to(_tokensToBurn) + ); BoxSelector boxSelector = DefaultBoxSelector$.MODULE$; UnsignedErgoLikeTransaction tx = TransactionBuilder.buildUnsignedTx( inputBoxes, - dataInputs, - outputCandidates, + dataInputs, + outputCandidates, _ctx.getHeight(), - _feeAmount, - _changeAddress, - MinChangeValue, + _feeAmount, + _changeAddress, + MinChangeValue, Parameters.MinerRewardDelay, + burnTokens, boxSelector).get(); ErgoLikeStateContext stateContext = createErgoLikeStateContext();