From 1d5762ad7291080f892db88e395eccb4bfb4d61e Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Wed, 31 Aug 2022 11:33:15 +0200 Subject: [PATCH 01/44] appkit-5.0: update sigma dependency --- build.sbt | 2 +- .../appkit/AppkitProvingInterpreter.scala | 25 +++++++++++++++++-- .../org/ergoplatform/appkit/Signature.java | 2 +- .../ReducedErgoLikeTransactionSpec.scala | 5 ++-- .../appkit/impl/BlockchainContextBase.java | 2 ++ 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/build.sbt b/build.sbt index e0287684..fe907a2a 100644 --- a/build.sbt +++ b/build.sbt @@ -126,7 +126,7 @@ assemblyMergeStrategy in assembly := { lazy val allConfigDependency = "compile->compile;test->test" -val sigmaStateVersion = "4.0.5" +val sigmaStateVersion = "4.0.6-564-6696ebfe-SNAPSHOT" val ergoWalletVersion = "4.0.27" lazy val sigmaState = ("org.scorexfoundation" %% "sigma-state" % sigmaStateVersion).force() .exclude("ch.qos.logback", "logback-classic") diff --git a/common/src/main/java/org/ergoplatform/appkit/AppkitProvingInterpreter.scala b/common/src/main/java/org/ergoplatform/appkit/AppkitProvingInterpreter.scala index 627001a4..42922311 100644 --- a/common/src/main/java/org/ergoplatform/appkit/AppkitProvingInterpreter.scala +++ b/common/src/main/java/org/ergoplatform/appkit/AppkitProvingInterpreter.scala @@ -10,13 +10,14 @@ import org.ergoplatform.wallet.secrets.ExtendedSecretKey import sigmastate.basics.{SigmaProtocolCommonInput, DiffieHellmanTupleProverInput, SigmaProtocol, SigmaProtocolPrivateInput} import org.ergoplatform._ import org.ergoplatform.appkit.JavaHelpers.{TokenColl, subtractTokenColls} +import org.ergoplatform.appkit.ReducedInputData.createReductionResult import org.ergoplatform.utils.ArithUtils import org.ergoplatform.wallet.protocol.context.{ErgoLikeStateContext, ErgoLikeParameters, TransactionContext} import sigmastate.Values.{SigmaBoolean, ErgoTree} import scala.util.Try import sigmastate.eval.CompiletimeIRContext -import sigmastate.interpreter.Interpreter.{ReductionResult, ScriptEnv} +import sigmastate.interpreter.Interpreter.{ReductionResult, JitReductionResult, ScriptEnv, AotReductionResult, FullReductionResult} import sigmastate.interpreter.{Interpreter, CostedProverResult, ContextExtension, ProverInterpreter, HintsBag} import sigmastate.lang.exceptions.CostLimitException import sigmastate.serialization.SigmaSerializer @@ -26,6 +27,8 @@ import sigmastate.utils.Helpers._ // for Scala 2.11 import sigmastate.utils.{SigmaByteWriter, SigmaByteReader} import spire.syntax.all.cfor import scalan.util.Extensions.LongOps +import sigmastate.VersionContext +import sigmastate.VersionContext.JitActivationVersion import scala.collection.mutable @@ -306,6 +309,22 @@ case class TokenBalanceException( */ case class ReducedInputData(reductionResult: ReductionResult, extension: ContextExtension) +object ReducedInputData { + /** Creates [[ReductionResult]] for the given blockVersion. + * + * @param blockVersion version of Ergo protocol (stored in block header) + * @param sb sigma proposition (typically result of script reduction) + * @param cost cost accumulated during reduction + */ + def createReductionResult(blockVersion: Byte, sb: SigmaBoolean, cost: Long): ReductionResult = { + val scriptVersion = blockVersion - 1 + if (scriptVersion >= JitActivationVersion) + FullReductionResult(null, JitReductionResult(sb, cost)) + else + FullReductionResult(AotReductionResult(sb, cost), null) + } +} + /** Represent `reduced` transaction, i.e. unsigned transaction where each unsigned input * is augmented with [[ReducedInputData]] which contains a script reduction result. * After an unsigned transaction is reduced it can be signed without context. @@ -356,7 +375,9 @@ object ReducedErgoLikeTransactionSerializer extends SigmaSerializer[ReducedErgoL val cost = r.getULong() val input = tx.inputs(i) val extension = input.extension - reducedInputs(i) = ReducedInputData(ReductionResult(sb, cost), extension) + val currentBlockVersion: Byte = (VersionContext.current.activatedVersion + 1).toByte + val reductionResult = createReductionResult(currentBlockVersion, sb, cost) + reducedInputs(i) = ReducedInputData(reductionResult, extension) unsignedInputs(i) = new UnsignedInput(input.boxId, extension) } diff --git a/common/src/main/java/org/ergoplatform/appkit/Signature.java b/common/src/main/java/org/ergoplatform/appkit/Signature.java index 6e4c8099..85ddd94a 100644 --- a/common/src/main/java/org/ergoplatform/appkit/Signature.java +++ b/common/src/main/java/org/ergoplatform/appkit/Signature.java @@ -14,6 +14,6 @@ private Signature() { * @return whether signature is valid or not */ public static boolean verifySignature(SigmaProp sigmaProp, byte[] message, byte[] signature) { - return SigmaPropInterpreter.verifySignature(sigmaProp.getSigmaBoolean(), message, signature); + return SigmaPropInterpreter.verifySignature(sigmaProp.getSigmaBoolean(), message, signature, null); } } diff --git a/common/src/test/scala/org/ergoplatform/appkit/ReducedErgoLikeTransactionSpec.scala b/common/src/test/scala/org/ergoplatform/appkit/ReducedErgoLikeTransactionSpec.scala index f3ed2d6b..b8ed8ffc 100644 --- a/common/src/test/scala/org/ergoplatform/appkit/ReducedErgoLikeTransactionSpec.scala +++ b/common/src/test/scala/org/ergoplatform/appkit/ReducedErgoLikeTransactionSpec.scala @@ -1,11 +1,12 @@ package org.ergoplatform.appkit import org.ergoplatform.UnsignedErgoLikeTransaction +import org.ergoplatform.appkit.ReducedInputData.createReductionResult import org.scalacheck.Gen import org.scalatest.{PropSpec, Assertion, Matchers} import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import sigmastate.CrossVersionProps import sigmastate.interpreter.ContextExtension -import sigmastate.interpreter.Interpreter.ReductionResult import sigmastate.serialization.SigmaSerializer import sigmastate.serialization.generators.ObjectGenerators @@ -16,7 +17,7 @@ class ReducedErgoLikeTransactionSpec extends PropSpec sb <- sigmaBooleanGen cost <- Gen.choose(10L, 1000L) } yield - ReducedInputData(ReductionResult(sb, cost), extension) + ReducedInputData(createReductionResult(activatedVersionInTests, sb, cost), extension) def reducedErgoLikeTransactionGen( unsignedTx: UnsignedErgoLikeTransaction): Gen[ReducedErgoLikeTransaction] = { diff --git a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/BlockchainContextBase.java b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/BlockchainContextBase.java index 0d393726..28c0759a 100644 --- a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/BlockchainContextBase.java +++ b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/BlockchainContextBase.java @@ -12,7 +12,9 @@ import org.ergoplatform.appkit.SignedTransaction; import org.ergoplatform.appkit.BlockchainParameters; +import scala.Function0; import sigmastate.Values; +import sigmastate.VersionContext$; import sigmastate.serialization.SigmaSerializer$; import sigmastate.utils.SigmaByteReader; From 8d0fa9342d6a950b5c45d08284f45e59f7e2a06e Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Wed, 31 Aug 2022 11:36:26 +0200 Subject: [PATCH 02/44] appkit-5.0: add ColdClientBlockVersion and pass to ColdErgoClient --- .../main/scala/org/ergoplatform/appkit/ColdErgoClient.scala | 5 +++-- .../test/scala/org/ergoplatform/appkit/ErgoAuthSpec.scala | 2 +- .../test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala | 2 +- common/src/main/java/org/ergoplatform/appkit/Parameters.java | 5 +++++ 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/appkit/src/main/scala/org/ergoplatform/appkit/ColdErgoClient.scala b/appkit/src/main/scala/org/ergoplatform/appkit/ColdErgoClient.scala index 36119657..ab1dc916 100644 --- a/appkit/src/main/scala/org/ergoplatform/appkit/ColdErgoClient.scala +++ b/appkit/src/main/scala/org/ergoplatform/appkit/ColdErgoClient.scala @@ -10,10 +10,11 @@ class ColdErgoClient(networkType: NetworkType, params: BlockchainParameters) ext /** * Convenience constructor for giving maxBlockCost */ - def this(networkType: NetworkType, maxBlockCost: Int) { + def this(networkType: NetworkType, maxBlockCost: Int, blockVersion: Byte) { this(networkType, new NodeInfoParameters( new NodeInfo().parameters(new client.Parameters() - .maxBlockCost(Integer.valueOf(maxBlockCost))))) + .maxBlockCost(Integer.valueOf(maxBlockCost)) + .blockVersion(Integer.valueOf(blockVersion))))) } override def execute[T](action: function.Function[BlockchainContext, T]): T = { diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/ErgoAuthSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/ErgoAuthSpec.scala index 8aafcdaf..30a358b2 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/ErgoAuthSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/ErgoAuthSpec.scala @@ -22,7 +22,7 @@ class ErgoAuthSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyC // EIP-28: "the wallet app adds some own bytes to the obtained message from ErgoAuthRequest" val signedMessage = new String(Random.randomBytes(16)) + requestedMessage + new String(Random.randomBytes(32)) - val signature = new ColdErgoClient(address.getNetworkType, Parameters.ColdClientMaxBlockCost) + val signature = new ColdErgoClient(address.getNetworkType, Parameters.ColdClientMaxBlockCost, Parameters.ColdClientBlockVersion) .execute { ctx: BlockchainContext => val prover = ctx.newProverBuilder().withMnemonic(mnemonic, SecretString.empty()).build() diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala index 8a51d03b..37d953d5 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala @@ -308,7 +308,7 @@ class TxBuilderSpec extends PropSpec with Matchers // the only necessary parameter can either be hard-coded or passed // together with ReducedTransaction val maxBlockCost = Parameters.ColdClientMaxBlockCost - val coldClient = new ColdErgoClient(NetworkType.MAINNET, maxBlockCost) + val coldClient = new ColdErgoClient(NetworkType.MAINNET, maxBlockCost, Parameters.ColdClientBlockVersion) coldClient.execute { ctx: BlockchainContext => // test that context is cold diff --git a/common/src/main/java/org/ergoplatform/appkit/Parameters.java b/common/src/main/java/org/ergoplatform/appkit/Parameters.java index 20295275..43bc4b43 100644 --- a/common/src/main/java/org/ergoplatform/appkit/Parameters.java +++ b/common/src/main/java/org/ergoplatform/appkit/Parameters.java @@ -32,4 +32,9 @@ public class Parameters { * Max block cost for Cold Client */ public static final int ColdClientMaxBlockCost = 1000000; + + /** + * Activated version for Cold Client + */ + public static final byte ColdClientBlockVersion = 2; } From 17ae4879709ac3318a9e95b5260429d830d10674 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Wed, 31 Aug 2022 11:42:59 +0200 Subject: [PATCH 03/44] appkit-5.0: preform ReducedErgoLikeTransaction parsing under correct activated script version --- .../ergoplatform/appkit/AppkitProvingInterpreter.scala | 2 +- .../appkit/ReducedErgoLikeTransactionSpec.scala | 2 +- .../ergoplatform/appkit/impl/BlockchainContextBase.java | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/org/ergoplatform/appkit/AppkitProvingInterpreter.scala b/common/src/main/java/org/ergoplatform/appkit/AppkitProvingInterpreter.scala index 42922311..af235cae 100644 --- a/common/src/main/java/org/ergoplatform/appkit/AppkitProvingInterpreter.scala +++ b/common/src/main/java/org/ergoplatform/appkit/AppkitProvingInterpreter.scala @@ -317,7 +317,7 @@ object ReducedInputData { * @param cost cost accumulated during reduction */ def createReductionResult(blockVersion: Byte, sb: SigmaBoolean, cost: Long): ReductionResult = { - val scriptVersion = blockVersion - 1 + val scriptVersion = blockVersion - 1 // convert to script version if (scriptVersion >= JitActivationVersion) FullReductionResult(null, JitReductionResult(sb, cost)) else diff --git a/common/src/test/scala/org/ergoplatform/appkit/ReducedErgoLikeTransactionSpec.scala b/common/src/test/scala/org/ergoplatform/appkit/ReducedErgoLikeTransactionSpec.scala index b8ed8ffc..6e7bac6f 100644 --- a/common/src/test/scala/org/ergoplatform/appkit/ReducedErgoLikeTransactionSpec.scala +++ b/common/src/test/scala/org/ergoplatform/appkit/ReducedErgoLikeTransactionSpec.scala @@ -10,7 +10,7 @@ import sigmastate.interpreter.ContextExtension import sigmastate.serialization.SigmaSerializer import sigmastate.serialization.generators.ObjectGenerators -class ReducedErgoLikeTransactionSpec extends PropSpec +class ReducedErgoLikeTransactionSpec extends PropSpec with CrossVersionProps with Matchers with ScalaCheckDrivenPropertyChecks with ObjectGenerators { def reducedInputDataGen(extension: ContextExtension): Gen[ReducedInputData] = for { diff --git a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/BlockchainContextBase.java b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/BlockchainContextBase.java index 28c0759a..8d1d78d8 100644 --- a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/BlockchainContextBase.java +++ b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/BlockchainContextBase.java @@ -45,7 +45,14 @@ public NetworkType getNetworkType() { @Override public ReducedTransaction parseReducedTransaction(byte[] txBytes) { SigmaByteReader r = SigmaSerializer$.MODULE$.startReader(txBytes, 0); - ReducedErgoLikeTransaction tx = ReducedErgoLikeTransactionSerializer$.MODULE$.parse(r); + byte activatedScriptVersion = (byte)(getParameters().getBlockVersion() - 1); + ReducedErgoLikeTransaction tx = (ReducedErgoLikeTransaction)VersionContext$.MODULE$ + .withVersions(activatedScriptVersion, activatedScriptVersion, new Function0() { + @Override + public Object apply() { + return ReducedErgoLikeTransactionSerializer$.MODULE$.parse(r); + } + }); int cost = (int)r.getUInt(); // TODO use java7.compat.Math.toIntExact when it will available in Sigma return new ReducedTransactionImpl(this, tx, cost); } From 08a4e17e78832bb6bd34791a82431bd2ba4ea111 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Wed, 31 Aug 2022 11:50:27 +0200 Subject: [PATCH 04/44] appkit-5.0: code cleanup --- .../test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala | 2 -- .../org/ergoplatform/appkit/AppkitProvingInterpreter.scala | 2 +- .../ergoplatform/appkit/ReducedErgoLikeTransactionSpec.scala | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala index 37d953d5..d3faaf94 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala @@ -4,7 +4,6 @@ import com.google.gson.Gson import com.google.gson.reflect.TypeToken import org.ergoplatform.appkit.InputBoxesSelectionException.NotEnoughErgsException import org.ergoplatform.appkit.JavaHelpers._ -import org.ergoplatform.appkit.examples.RunMockedScala.data import org.ergoplatform.appkit.impl.{Eip4TokenBuilder, ErgoTreeContract} import org.ergoplatform.appkit.testing.AppkitTesting import org.ergoplatform.explorer.client.model.{Items, TokenInfo} @@ -23,7 +22,6 @@ import java.math.BigInteger import java.util import java.util.Arrays import java.util.function.Consumer -import scala.collection.JavaConversions class TxBuilderSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChecks diff --git a/common/src/main/java/org/ergoplatform/appkit/AppkitProvingInterpreter.scala b/common/src/main/java/org/ergoplatform/appkit/AppkitProvingInterpreter.scala index af235cae..7c4cd51f 100644 --- a/common/src/main/java/org/ergoplatform/appkit/AppkitProvingInterpreter.scala +++ b/common/src/main/java/org/ergoplatform/appkit/AppkitProvingInterpreter.scala @@ -9,7 +9,7 @@ import java.util.{Objects, List => JList} import org.ergoplatform.wallet.secrets.ExtendedSecretKey import sigmastate.basics.{SigmaProtocolCommonInput, DiffieHellmanTupleProverInput, SigmaProtocol, SigmaProtocolPrivateInput} import org.ergoplatform._ -import org.ergoplatform.appkit.JavaHelpers.{TokenColl, subtractTokenColls} +import org.ergoplatform.appkit.JavaHelpers.TokenColl import org.ergoplatform.appkit.ReducedInputData.createReductionResult import org.ergoplatform.utils.ArithUtils import org.ergoplatform.wallet.protocol.context.{ErgoLikeStateContext, ErgoLikeParameters, TransactionContext} diff --git a/common/src/test/scala/org/ergoplatform/appkit/ReducedErgoLikeTransactionSpec.scala b/common/src/test/scala/org/ergoplatform/appkit/ReducedErgoLikeTransactionSpec.scala index 6e7bac6f..0c6f0640 100644 --- a/common/src/test/scala/org/ergoplatform/appkit/ReducedErgoLikeTransactionSpec.scala +++ b/common/src/test/scala/org/ergoplatform/appkit/ReducedErgoLikeTransactionSpec.scala @@ -3,14 +3,14 @@ package org.ergoplatform.appkit import org.ergoplatform.UnsignedErgoLikeTransaction import org.ergoplatform.appkit.ReducedInputData.createReductionResult import org.scalacheck.Gen -import org.scalatest.{PropSpec, Assertion, Matchers} +import org.scalatest.{Assertion, Matchers} import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks import sigmastate.CrossVersionProps import sigmastate.interpreter.ContextExtension import sigmastate.serialization.SigmaSerializer import sigmastate.serialization.generators.ObjectGenerators -class ReducedErgoLikeTransactionSpec extends PropSpec with CrossVersionProps +class ReducedErgoLikeTransactionSpec extends CrossVersionProps with Matchers with ScalaCheckDrivenPropertyChecks with ObjectGenerators { def reducedInputDataGen(extension: ContextExtension): Gen[ReducedInputData] = for { From 2bb32fb0db30381266865b19f96148dd5537744b Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Wed, 31 Aug 2022 13:53:59 +0200 Subject: [PATCH 05/44] appkit-5.0: fix Scala 2.11 compilation --- .../ergoplatform/appkit/AppkitProvingInterpreter.scala | 9 +++++++++ .../ergoplatform/appkit/impl/BlockchainContextBase.java | 9 +-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/common/src/main/java/org/ergoplatform/appkit/AppkitProvingInterpreter.scala b/common/src/main/java/org/ergoplatform/appkit/AppkitProvingInterpreter.scala index 7c4cd51f..1c90e194 100644 --- a/common/src/main/java/org/ergoplatform/appkit/AppkitProvingInterpreter.scala +++ b/common/src/main/java/org/ergoplatform/appkit/AppkitProvingInterpreter.scala @@ -385,5 +385,14 @@ object ReducedErgoLikeTransactionSerializer extends SigmaSerializer[ReducedErgoL ReducedErgoLikeTransaction(unsignedTx, reducedInputs) } + /** Parses the [[ReducedErgoLikeTransaction]] using the given blockVersion. + * @param blockVersion version of Ergo protocol to use during parsing. + */ + def parse(r: SigmaByteReader, blockVersion: Byte): ReducedErgoLikeTransaction = { + val scriptVersion = (blockVersion - 1).toByte + VersionContext.withVersions(scriptVersion, scriptVersion) { + parse(r) + } + } } diff --git a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/BlockchainContextBase.java b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/BlockchainContextBase.java index 8d1d78d8..97eb7356 100644 --- a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/BlockchainContextBase.java +++ b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/BlockchainContextBase.java @@ -45,14 +45,7 @@ public NetworkType getNetworkType() { @Override public ReducedTransaction parseReducedTransaction(byte[] txBytes) { SigmaByteReader r = SigmaSerializer$.MODULE$.startReader(txBytes, 0); - byte activatedScriptVersion = (byte)(getParameters().getBlockVersion() - 1); - ReducedErgoLikeTransaction tx = (ReducedErgoLikeTransaction)VersionContext$.MODULE$ - .withVersions(activatedScriptVersion, activatedScriptVersion, new Function0() { - @Override - public Object apply() { - return ReducedErgoLikeTransactionSerializer$.MODULE$.parse(r); - } - }); + ReducedErgoLikeTransaction tx = ReducedErgoLikeTransactionSerializer$.MODULE$.parse(r, getParameters().getBlockVersion()); int cost = (int)r.getUInt(); // TODO use java7.compat.Math.toIntExact when it will available in Sigma return new ReducedTransactionImpl(this, tx, cost); } From cd2af8e82332383821cfbf7f2d004fc73a75959c Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Wed, 31 Aug 2022 18:37:34 +0200 Subject: [PATCH 06/44] appkit-5.0: note added in MIGRATION --- MIGRATION | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MIGRATION b/MIGRATION index 8b8ae0f7..d8f8d034 100644 --- a/MIGRATION +++ b/MIGRATION @@ -1,3 +1,5 @@ +[5.0.0] +- block version now required to construct ColdErgoClient [4.0.9] - ExplorerAndPoolUnspentBoxesLoader moved from package org.ergoplatform.appkit.impl to org.ergoplatform.appkit - DefaultApi.getApiV1AddressesP1Transactions new parameter "concise" - use false for old behaviour From b34db9c1007160a96c012a5eafbc619ac3b4e961 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Tue, 18 Oct 2022 12:16:40 +0200 Subject: [PATCH 07/44] sigma-update: update sigma to v5.0.0 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index fe907a2a..db4f54e5 100644 --- a/build.sbt +++ b/build.sbt @@ -126,7 +126,7 @@ assemblyMergeStrategy in assembly := { lazy val allConfigDependency = "compile->compile;test->test" -val sigmaStateVersion = "4.0.6-564-6696ebfe-SNAPSHOT" +val sigmaStateVersion = "5.0.0" val ergoWalletVersion = "4.0.27" lazy val sigmaState = ("org.scorexfoundation" %% "sigma-state" % sigmaStateVersion).force() .exclude("ch.qos.logback", "logback-classic") From 6891956e12939b1cf1cee453419dd17a48ab3ba3 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Sun, 30 Oct 2022 15:33:40 +0100 Subject: [PATCH 08/44] Test case for issue #199 --- .../ergoplatform/appkit/TxBuilderSpec.scala | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala index 21c84c74..52cf0df7 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala @@ -469,6 +469,47 @@ class TxBuilderSpec extends PropSpec with Matchers } + property("Test same token multiple times") { + val ergoClient = createMockedErgoClient(data) + + ergoClient.execute { ctx: BlockchainContext => + val (storage, _) = loadStorageE2() + + val recipient = address + + // send 1 ERG + val amountToSend = 1000L * 1000 * 1000 + val pkContract = recipient.toErgoContract + + val senders = Arrays.asList(storage.getAddressFor(NetworkType.MAINNET)) + + val input1 = ctx.newTxBuilder.outBoxBuilder + .value(amountToSend + Parameters.MinFee + Parameters.MinChangeValue) + .contract(pkContract) + // the same token twice + .tokens(new ErgoToken(mockTxId, 1), new ErgoToken(mockTxId, 1)) + .build().convertToInputWith(mockTxId, 0) + + val unsigned = BoxOperations.createForSenders(senders, ctx) + .withAmountToSpend(amountToSend) + .withInputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))) + .putToContractTxUnsigned(pkContract) + + // this fails due to token burning check - instead, tokens should be in change box FIXME + val prover = ctx.newProverBuilder.build // prover without secrets + val reduced = prover.reduce(unsigned, 0) + + // this fails with NotEnoughTokensException, although there are enough tokens available + val spendAllTokens = BoxOperations.createForSenders(senders, ctx) + .withAmountToSpend(amountToSend) + .withTokensToSpend(Arrays.asList(new ErgoToken(mockTxId, 2))) + .withInputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))) + .putToContractTxUnsigned(pkContract) + + } + + } + property("Special tx building cases") { val ergoClient = createMockedErgoClient(MockData(Nil, Nil)) ergoClient.execute { ctx: BlockchainContext => From 11a380a0df0d86fba1821deecf38a49264890ce9 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Mon, 31 Oct 2022 13:02:03 +0100 Subject: [PATCH 09/44] Excludes tests from javaClientGenerated (#201) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e200f3d1..301808ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} - name: Runs tests and collect coverage - run: sbt -jvm-opts ci/ci.jvmopts ++${{ matrix.scala }} test + run: sbt -jvm-opts ci/ci.jvmopts ++${{ matrix.scala }} "project appkit" test; sbt -jvm-opts ci/ci.jvmopts ++${{ matrix.scala }} "project common" test; sbt -jvm-opts ci/ci.jvmopts ++${{ matrix.scala }} "project libImpl" test # - name: Upload coverage report # run: sbt ++${{ matrix.scala }} coverageReport coverageAggregate coveralls # env: From 0ab249364b3694a4167b059f5acfb32d41c45d24 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Tue, 1 Nov 2022 20:05:16 +0100 Subject: [PATCH 10/44] Add methods to access spent boxes and save to registers (#198) * Add BlockchainDataSource#getBoxByIdWithSpent, InputBox#toErgoValue, InputBoxImpl constructor for Explorer OutputInfo * ScalaBridge.isoExplTransactionOutput set asset indices * BlockchainDataSource#getBoxById with spent/pool flag parameters * Migration info getBoxById --- MIGRATION | 5 ++ .../appkit/BlockchainDataSource.java | 15 ++--- .../org/ergoplatform/appkit/InputBox.java | 6 ++ .../appkit/impl/BlockchainContextImpl.java | 2 +- .../appkit/impl/InputBoxImpl.java | 14 +++++ .../impl/NodeAndExplorerDataSourceImpl.java | 31 +++++++--- .../appkit/impl/ScalaBridge.scala | 62 ++++++++++--------- .../impl/BlockchainContextImplTest.java | 7 +-- 8 files changed, 88 insertions(+), 54 deletions(-) diff --git a/MIGRATION b/MIGRATION index 8b8ae0f7..78c74420 100644 --- a/MIGRATION +++ b/MIGRATION @@ -1,3 +1,8 @@ +[4.0.12] +- getBoxById(String boxId) was replaced by getBoxById(String boxId, boolean findInPool, boolean findInSpent) + the old behaviour (returning only confirmed unspent boxes) can be achieved by calling + getBoxById(boxId, false, false) + [4.0.9] - ExplorerAndPoolUnspentBoxesLoader moved from package org.ergoplatform.appkit.impl to org.ergoplatform.appkit - DefaultApi.getApiV1AddressesP1Transactions new parameter "concise" - use false for old behaviour diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/BlockchainDataSource.java b/lib-api/src/main/java/org/ergoplatform/appkit/BlockchainDataSource.java index c482f963..c3614959 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/BlockchainDataSource.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/BlockchainDataSource.java @@ -24,20 +24,15 @@ public interface BlockchainDataSource { List getLastBlockHeaders(int count, boolean onlyFullHeaders); /** - * Get box contents for a box by a unique identifier for use as an Input + * Get box contents for an unspent box by a unique identifier for use as an Input, + * including mempool boxes * * @param boxId ID of a wanted box (required) + * @param findInPool whether to find boxes that are currently in mempool + * @param findInSpent whether to find boxes that are spent * @return InputBox */ - InputBox getBoxById(String boxId); - - /** - * Get box contents for a box by a unique identifier for use as an Input, including mempool boxes - * - * @param boxId ID of a wanted box (required) - * @return InputBox - */ - InputBox getBoxByIdWithMemPool(String boxId); + InputBox getBoxById(String boxId, boolean findInPool, boolean findInSpent); /** * Send an Ergo transaction diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/InputBox.java b/lib-api/src/main/java/org/ergoplatform/appkit/InputBox.java index 86198d36..2c815ecf 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/InputBox.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/InputBox.java @@ -1,5 +1,7 @@ package org.ergoplatform.appkit; +import special.sigma.Box; + /** * Interface of UTXO boxes which can be accessed in the blockchain node. * Instances of this interface can be {@link BlockchainContext#getBoxesById(String...) obtained} @@ -50,5 +52,9 @@ public interface InputBox extends TransactionBox { */ byte[] getBytes(); + /** + * @return this box as an ergo value to store in a register + */ + ErgoValue toErgoValue(); } diff --git a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/BlockchainContextImpl.java b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/BlockchainContextImpl.java index a7c729f4..06b0e169 100644 --- a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/BlockchainContextImpl.java +++ b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/BlockchainContextImpl.java @@ -76,7 +76,7 @@ public BlockchainDataSource getDataSource() { public InputBox[] getBoxesById(String... boxIds) throws ErgoClientException { List list = new ArrayList<>(); for (String id : boxIds) { - list.add(_dataSource.getBoxById(id)); + list.add(_dataSource.getBoxById(id, false, false)); } return list.toArray(new InputBox[0]); } diff --git a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/InputBoxImpl.java b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/InputBoxImpl.java index fadae200..319cc35f 100644 --- a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/InputBoxImpl.java +++ b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/InputBoxImpl.java @@ -4,6 +4,7 @@ import org.ergoplatform.ErgoBox; import org.ergoplatform.appkit.*; +import org.ergoplatform.explorer.client.model.OutputInfo; import org.ergoplatform.restapi.client.ErgoTransactionOutput; import org.ergoplatform.restapi.client.JSON; @@ -13,6 +14,7 @@ import sigmastate.Values; import sigmastate.interpreter.ContextExtension; +import special.sigma.Box; public class InputBoxImpl implements InputBox { private final ErgoId _id; @@ -27,6 +29,13 @@ public InputBoxImpl(ErgoTransactionOutput boxData) { _extension = ContextExtension.empty(); } + public InputBoxImpl(OutputInfo outputInfo) { + _id = ErgoId.create(outputInfo.getBoxId()); + _ergoBox = ScalaBridge.isoExplTransactionOutput().to(outputInfo); + _boxData = ScalaBridge.isoErgoTransactionOutput().from(_ergoBox); + _extension = ContextExtension.empty(); + } + public InputBoxImpl(ErgoBox ergoBox) { _ergoBox = ergoBox; _id = new ErgoId((byte[])ergoBox.id()); @@ -116,4 +125,9 @@ public ContextExtension getExtension() { public String toString() { return String.format("InputBox(%s, %s)", getId(), getValue()); } + + @Override + public ErgoValue toErgoValue() { + return ErgoValue.of(getErgoBox()); + } } diff --git a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/NodeAndExplorerDataSourceImpl.java b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/NodeAndExplorerDataSourceImpl.java index 4cf23158..17ae330c 100644 --- a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/NodeAndExplorerDataSourceImpl.java +++ b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/NodeAndExplorerDataSourceImpl.java @@ -145,17 +145,32 @@ public List getLastBlockHeaders(int count, boolean onlyFullHeaders) } @Override - public InputBox getBoxById(String boxId) { - ErgoTransactionOutput boxData = executeCall(nodeUtxoApi.getBoxById(boxId)); - return new InputBoxImpl(boxData); + public InputBox getBoxById(String boxId, boolean findInPool, boolean findInSpent) { + if (findInSpent && !findInPool) { + return getBoxByIdExplorer(boxId); + } else if (!findInSpent) { + return getUnspentBoxByIdNode(boxId, findInPool); + } else { + // find in spent and find in pool => try node first and fall back to explorer + try { + return getUnspentBoxByIdNode(boxId, findInPool); + } catch (Throwable t) { + return getBoxByIdExplorer(boxId); + } + } } - @Override - public InputBox getBoxByIdWithMemPool(String boxId) { - ErgoTransactionOutput boxData = executeCall(nodeUtxoApi.getBoxWithPoolById(boxId)); + private InputBox getUnspentBoxByIdNode(String boxId, boolean findInPool) { + ErgoTransactionOutput boxData = (findInPool) ? executeCall(nodeUtxoApi.getBoxWithPoolById(boxId)) + : executeCall(nodeUtxoApi.getBoxById(boxId)); return new InputBoxImpl(boxData); } + private InputBox getBoxByIdExplorer(String boxId) { + OutputInfo outputInfo = executeCall(explorerApi.getApiV1BoxesP1(boxId)); + return new InputBoxImpl(outputInfo); + } + @Override public String sendTransaction(SignedTransaction tx) { ErgoLikeTransaction ergoTx = ((SignedTransactionImpl) tx).getTx(); @@ -202,7 +217,7 @@ public List getUnconfirmedUnspentBoxesFor(Address address, int offset, if (output.getAddress().equals(senderAddress) && output.getSpentTransactionId() == null) { // we have an unconfirmed box - get info from node for it try { - InputBox boxInfo = getBoxByIdWithMemPool(output.getBoxId()); + InputBox boxInfo = getBoxById(output.getBoxId(), true, false); if (boxInfo != null) { inputBoxes.add(boxInfo); } @@ -231,7 +246,7 @@ private List getInputBoxes(List boxes) { for (OutputInfo box : boxes) { String boxId = box.getBoxId(); try { - InputBox boxInfo = getBoxById(boxId); + InputBox boxInfo = getBoxById(boxId, false, false); // can be null if node does not know about the box (yet) // instead of throwing an error, we continue with the boxes actually known if (boxInfo != null) { diff --git a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/ScalaBridge.scala b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/ScalaBridge.scala index 6a4f51d2..a191adbc 100644 --- a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/ScalaBridge.scala +++ b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/ScalaBridge.scala @@ -1,10 +1,11 @@ package org.ergoplatform.appkit.impl import _root_.org.ergoplatform.restapi.client._ -import org.ergoplatform.explorer.client.model.{AdditionalRegister, AdditionalRegisters => ERegisters, AssetInfo => EAsset} +import org.ergoplatform.explorer.client.model.{AdditionalRegister, AssetInstanceInfo, OutputInfo, AdditionalRegisters => ERegisters, AssetInfo => EAsset} import java.util import java.util.List +import java.util.{Map => JMap, List => JList} import java.lang.{Byte => JByte} import org.ergoplatform.ErgoBox.{NonMandatoryRegisterId, TokenId} import org.ergoplatform.{DataInput, ErgoBox, ErgoLikeTransaction, Input} @@ -146,7 +147,7 @@ object ScalaBridge { override def to(boxData: ErgoTransactionOutput): ErgoBox = { val tree= boxData.getErgoTree.convertTo[ErgoTree] val tokens = boxData.getAssets.convertTo[Coll[(TokenId, Long)]] - val regs = boxData.getAdditionalRegisters().convertTo[AdditionalRegisters] + val regs = boxData.getAdditionalRegisters.convertTo[AdditionalRegisters] new ErgoBox(boxData.getValue, tree, tokens, regs, ModifierId @@ boxData.getTransactionId, @@ -170,33 +171,36 @@ object ScalaBridge { } } -// implicit val isoExplTransactionOutput: Iso[TransactionOutput, ErgoBox] = new Iso[TransactionOutput, ErgoBox] { -// override def to(boxData: TransactionOutput): ErgoBox = { -// val tree = boxData.getErgoTree.convertTo[ErgoTree] -// val tokens = boxData.getAssets.convertTo[Coll[(TokenId, Long)]] -// val regs = boxData.getAdditionalRegisters().convertTo[AdditionalRegisters] -// new ErgoBox(boxData.getValue, tree, -// tokens, regs, -// ModifierId @@ boxData.getTransactionId, -// boxData.getIndex.shortValue, -// boxData.getCreationHeight) -// } -// -// override def from(box: ErgoBox): ErgoTransactionOutput = { -// val assets = box.additionalTokens.convertTo[List[Asset]] -// val regs = isoRegistersToMap.from(box.additionalRegisters) -// val out = new ErgoTransactionOutput() -// .boxId(ErgoAlgos.encode(box.id)) -// .value(box.value) -// .ergoTree(ErgoAlgos.encode(TreeSerializer.serializeErgoTree(box.ergoTree))) -// .assets(assets) -// .additionalRegisters(regs) -// .creationHeight(box.creationHeight) -// .transactionId(box.transactionId) -// .index(box.index) -// out -// } -// } + implicit val isoExplTransactionOutput: Iso[OutputInfo, ErgoBox] = new Iso[OutputInfo, ErgoBox] { + override def to(boxData: OutputInfo): ErgoBox = { + val tree = boxData.getErgoTree.convertTo[ErgoTree] + val tokens = boxData.getAssets.convertTo[IndexedSeq[AssetInstanceInfo]].sortBy(_.getIndex) + .map(asset => new ErgoToken(asset.getTokenId, asset.getAmount)).convertTo[JList[ErgoToken]].convertTo[Coll[(TokenId, Long)]] + val regs = boxData.getAdditionalRegisters.convertTo[AdditionalRegisters] + new ErgoBox(boxData.getValue, tree, + tokens, regs, + ModifierId @@ boxData.getTransactionId, + boxData.getIndex.shortValue, + boxData.getCreationHeight) + } + + override def from(box: ErgoBox): OutputInfo = { + val assets = box.additionalTokens.convertTo[List[Asset]] + val regs = isoExplRegistersToMap.from(box.additionalRegisters) + val out = new OutputInfo() + .boxId(ErgoAlgos.encode(box.id)) + .value(box.value) + .ergoTree(ErgoAlgos.encode(TreeSerializer.serializeErgoTree(box.ergoTree))) + .assets(assets.convertTo[IndexedSeq[Asset]].zipWithIndex + .map{ case (asset: Asset, idx: Int) => new AssetInstanceInfo().tokenId(asset.getTokenId).amount(asset.getAmount).index(idx) } + .convertTo[JList[AssetInstanceInfo]]) + .additionalRegisters(regs) + .creationHeight(box.creationHeight) + .transactionId(box.transactionId) + .index(box.index) + out + } + } implicit val isoBlockHeader: Iso[BlockHeader, Header] = new Iso[BlockHeader, Header] { override def to(h: BlockHeader): Header = diff --git a/lib-impl/src/test/java/org/ergoplatform/appkit/impl/BlockchainContextImplTest.java b/lib-impl/src/test/java/org/ergoplatform/appkit/impl/BlockchainContextImplTest.java index 508789f0..9ceb975f 100644 --- a/lib-impl/src/test/java/org/ergoplatform/appkit/impl/BlockchainContextImplTest.java +++ b/lib-impl/src/test/java/org/ergoplatform/appkit/impl/BlockchainContextImplTest.java @@ -102,7 +102,7 @@ public List getLastBlockHeaders(int count, boolean onlyFullHeaders) } @Override - public InputBox getBoxById(String boxId) { + public InputBox getBoxById(String boxId, boolean findInPool, boolean findInSpent) { return null; } @@ -111,11 +111,6 @@ public List getUnconfirmedUnspentBoxesFor(Address address, int offset, return null; } - @Override - public InputBox getBoxByIdWithMemPool(String boxId) { - return null; - } - @Override public List getUnconfirmedTransactions(int offset, int limit) { return null; From 4fcbe655ce396ec661b35bd54a82a068a3d30d81 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Tue, 1 Nov 2022 20:15:58 +0100 Subject: [PATCH 11/44] Add methods to construct Coll from Java and use in ErgoValue more easy (#200) * Add methods to construct Coll from Java and use in ErgoValue more easy * Use some non-zero values to test --- .../ergoplatform/appkit/ErgoValueTest.java | 12 +++++++++++ .../org/ergoplatform/appkit/ErgoValue.java | 20 +++++++++++++++++++ .../org/ergoplatform/appkit/JavaHelpers.scala | 8 ++++++++ 3 files changed, 40 insertions(+) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/ErgoValueTest.java b/appkit/src/test/scala/org/ergoplatform/appkit/ErgoValueTest.java index e70d2424..b07fbe43 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/ErgoValueTest.java +++ b/appkit/src/test/scala/org/ergoplatform/appkit/ErgoValueTest.java @@ -20,9 +20,17 @@ public void testTypeDeclarations() { ErgoValue shortErgoValue = ErgoValue.of((short) 1); ErgoValue bigIntErgoValue = ErgoValue.of(BigInteger.ZERO); ErgoValue> byteArrayErgoValue = ErgoValue.of(new byte[] {0, 1, 2}); + ErgoValue> shortArrayErgoValue = ErgoValue.of(new short[] {1, 2, 3}); + ErgoValue> intArrayErgoValue = ErgoValue.of(new int[] {2, 3, 4}); + ErgoValue> boolArrayErgoValue = ErgoValue.of(new boolean[] {false, true}); + ErgoValue> longArrayErgoValue = ErgoValue.of(new long[] {3, 4, 5}); BigInteger bigIntValue = bigIntErgoValue.getValue().value(); + boolean booleanFromCollValue = boolArrayErgoValue.getValue().apply(0); byte byteFromCollValue = byteArrayErgoValue.getValue().apply(0); + short shortFromCollValue = shortArrayErgoValue.getValue().apply(0); + int intFromCollValue = intArrayErgoValue.getValue().apply(0); + long longFromCollValue = longArrayErgoValue.getValue().apply(0); boolean booleanValue = booleanErgoValue.getValue(); byte byteValue = byteErgoValue.getValue(); short shortValue = shortErgoValue.getValue(); @@ -34,7 +42,11 @@ public void testTypeDeclarations() { assertEquals(true, booleanValue); assertEquals(1, byteValue); assertEquals(1, shortValue); + assertFalse(booleanFromCollValue); assertEquals(0, byteFromCollValue); + assertEquals(1, shortFromCollValue); + assertEquals(2, intFromCollValue); + assertEquals(3, longFromCollValue); assertEquals(BigInteger.ZERO, bigIntValue); } } diff --git a/common/src/main/java/org/ergoplatform/appkit/ErgoValue.java b/common/src/main/java/org/ergoplatform/appkit/ErgoValue.java index dad25752..4acd691a 100644 --- a/common/src/main/java/org/ergoplatform/appkit/ErgoValue.java +++ b/common/src/main/java/org/ergoplatform/appkit/ErgoValue.java @@ -132,6 +132,26 @@ static public ErgoValue> of(byte[] arr) { return new ErgoValue>(value, type); } + static public ErgoValue> of(long[] arr) { + return new ErgoValue>((Coll) JavaHelpers.collFrom(arr), + ErgoType.collType(ErgoType.longType())); + } + + static public ErgoValue> of(boolean[] arr) { + return new ErgoValue>((Coll) JavaHelpers.collFrom(arr), + ErgoType.collType(ErgoType.booleanType())); + } + + static public ErgoValue> of(short[] arr) { + return new ErgoValue>((Coll) JavaHelpers.collFrom(arr), + ErgoType.collType(ErgoType.shortType())); + } + + static public ErgoValue> of(int[] arr) { + return new ErgoValue>((Coll) JavaHelpers.collFrom(arr), + ErgoType.collType(ErgoType.integerType())); + } + static public ErgoValue> pairOf(ErgoValue val1, ErgoValue val2) { return new ErgoValue<>(new Tuple2<>(val1.getValue(), val2.getValue()), ErgoType.pairType(val1.getType(), val2.getType())); diff --git a/common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala b/common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala index 3ac40383..d77e11dc 100644 --- a/common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala +++ b/common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala @@ -475,6 +475,14 @@ object JavaHelpers { in.toArray } + def collFrom(arr: Array[Long]): Coll[Long] = Colls.fromArray(arr) + + def collFrom(arr: Array[Int]): Coll[Int] = Colls.fromArray(arr) + + def collFrom(arr: Array[Boolean]): Coll[Boolean] = Colls.fromArray(arr) + + def collFrom(arr: Array[Short]): Coll[Short] = Colls.fromArray(arr) + def ergoTreeTemplateBytes(ergoTree: ErgoTree): Array[Byte] = { val r = SigmaSerializer.startReader(ergoTree.bytes) ErgoTreeSerializer.DefaultSerializer.deserializeHeaderWithTreeBytes(r)._4 From 09a10de2aa0bbcb3dd79d52d7c71df514e2b686e Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Wed, 2 Nov 2022 14:07:44 +0100 Subject: [PATCH 12/44] Use InputBoxesValidator instead of DefaultBoxSelector (#202) * Use InputBoxesValidator instead of DefaultBoxSelector to get rid of unnecessary input box restrictions, closes #182 * Fix Scala 2.11 compile * Update lib-api/src/main/java/org/ergoplatform/appkit/InputBoxesValidator.scala Co-authored-by: Alexander Slesarenko * Improve JavaDoc * PR feedback Co-authored-by: Alexander Slesarenko --- .../ergoplatform/appkit/TxBuilderSpec.scala | 41 +++++++ .../ergoplatform/appkit/BoxOperations.java | 20 +++- .../appkit/InputBoxesValidator.scala | 108 ++++++++++++++++++ ...la => InputBoxesValidatorJavaHelper.scala} | 44 +++---- .../impl/UnsignedTransactionBuilderImpl.scala | 17 ++- 5 files changed, 193 insertions(+), 37 deletions(-) create mode 100644 lib-api/src/main/java/org/ergoplatform/appkit/InputBoxesValidator.scala rename lib-api/src/main/java/org/ergoplatform/appkit/{BoxSelectorsJavaHelpers.scala => InputBoxesValidatorJavaHelper.scala} (58%) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala index 21c84c74..df686ed0 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala @@ -409,6 +409,47 @@ class TxBuilderSpec extends PropSpec with Matchers } + property("use changebox to consolidate") { + // this demonstrates that unnecessary input boxes can be picked up to consolidate wallet boxes + // via change box on the fly (issue #182) + + val ergoClient = createMockedErgoClient(data) + + ergoClient.execute { ctx: BlockchainContext => + val (_, senders) = loadStorageE2() + val recipient = address + val pkContract = recipient.toErgoContract + + // send 0.5 ERG + val amountToSend = 500L * 1000 * 1000 + + // first box: 1 ERG + val input1 = ctx.newTxBuilder.outBoxBuilder + .value(Parameters.OneErg) + .contract(pkContract) + .build().convertToInputWith(mockTxId, 0) + // second box: 1 ERG + val input2 = ctx.newTxBuilder.outBoxBuilder + .value(Parameters.OneErg) + .contract(pkContract) + .build().convertToInputWith(mockTxId, 1) + + val tx = ctx.newTxBuilder().boxesToSpend(util.Arrays.asList(input1, input2)) + .outputs(ctx.newTxBuilder().outBoxBuilder().contract(pkContract).value(amountToSend).build()) + .sendChangeTo(recipient.getErgoAddress) + .fee(Parameters.MinFee) + .build() + + // both boxes should be selected + // ergo-wallet's DefaultBoxSelector discarded the second input because it is not necessary for + // the outputs, so this test checks if all inputs are used (size 2) + tx.getInputs.size() shouldBe 2 + tx.getOutputs.size() shouldBe 3 + + } + + } + property("Test changebox token amount max 100") { val ergoClient = createMockedErgoClient(data) diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/BoxOperations.java b/lib-api/src/main/java/org/ergoplatform/appkit/BoxOperations.java index 18b3fd24..3db89ae8 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/BoxOperations.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/BoxOperations.java @@ -6,8 +6,6 @@ import com.google.common.base.Preconditions; -import org.ergoplatform.P2PKAddress; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -240,8 +238,19 @@ public String send(Address recipient) { * @return a list of boxes covering the given amount */ public List loadTop() { + return loadTop(0); + } + + /** + * Like {@link #loadTop()} loading and returning unspent boxes covering the given amount of + * nanoergs, fee and tokens, but you can specify an amount of nanoergs that is already covered + * by other input boxes and does not need to be satisfied. + * + * @param amountCovered nanoerg amount that is assumed to be covered by input boxes you provide + */ + public List loadTop(long amountCovered) { List unspentBoxes = new ArrayList<>(); - long grossAmount = amountToSpend + feeAmount; + long grossAmount = amountToSpend + feeAmount - amountCovered; long remainingAmount = grossAmount; boolean changeBoxConsidered = false; SelectTokensHelper tokensHelper = new SelectTokensHelper(tokensToSpend); @@ -275,8 +284,9 @@ public List loadTop() { if (remainingAmount <= 0 && tokensHelper.areTokensCovered()) break; remainingTokens = tokensHelper.getRemainingTokenList(); } - List selected = BoxSelectorsJavaHelpers.selectBoxes(unspentBoxes, grossAmount, tokensToSpend); - return selected; + // check if we have enough tokens and ERG + InputBoxesValidatorJavaHelper.validateBoxes(unspentBoxes, grossAmount, tokensToSpend); + return unspentBoxes; } /** diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/InputBoxesValidator.scala b/lib-api/src/main/java/org/ergoplatform/appkit/InputBoxesValidator.scala new file mode 100644 index 00000000..84b00a90 --- /dev/null +++ b/lib-api/src/main/java/org/ergoplatform/appkit/InputBoxesValidator.scala @@ -0,0 +1,108 @@ +package org.ergoplatform.appkit + +import org.ergoplatform.wallet.Constants.MaxAssetsPerBox +import org.ergoplatform.wallet.boxes.BoxSelector._ +import org.ergoplatform.wallet.boxes.DefaultBoxSelector._ +import org.ergoplatform.wallet.boxes.{BoxSelector, ReemissionData} +import org.ergoplatform.wallet.{AssetUtils, TokensMap} +import org.ergoplatform.{ErgoBoxAssets, ErgoBoxAssetsHolder} +import scorex.util.ModifierId +import sigmastate.utils.Helpers.EitherOps + +import scala.collection.mutable + +/** + * Pass through implementation of the box selector. Unlike DefaultBoxSelector from ergo-wallet, + * it does not select input boxes. We do this in appkit ourselves and do not need the selector + * to interfere with how we built our transaction. Instead, this selector performs validation + * and calculates the necessary change box + */ +class InputBoxesValidator extends BoxSelector { + + override def reemissionDataOpt: Option[ReemissionData] = None + + override def select[T <: ErgoBoxAssets](inputBoxes: Iterator[T], + externalFilter: T => Boolean, + targetBalance: Long, + targetAssets: TokensMap): Either[BoxSelectionError, BoxSelectionResult[T]] = { + //mutable structures to collect results + val res = mutable.Buffer[T]() + var currentBalance = 0L + val currentAssets = mutable.Map[ModifierId, Long]() + + // select all input boxes - we only validate here + inputBoxes.foreach { box: T => + currentBalance = currentBalance + box.value + AssetUtils.mergeAssetsMut(currentAssets, box.tokens) + res += box + } + + if (currentBalance - targetBalance >= 0) { + //now check if we found all tokens + if (targetAssets.forall { + case (id, targetAmt) => currentAssets.getOrElse(id, 0L) >= targetAmt + }) { + formChangeBoxes(currentBalance, targetBalance, currentAssets, targetAssets).mapRight { changeBoxes => + BoxSelectionResult(res, changeBoxes) + } + } else { + Left(NotEnoughTokensError( + s"Not enough tokens in input boxes to send $targetAssets (found only $currentAssets)", currentAssets.toMap) + ) + } + } else { + Left(NotEnoughErgsError( + s"not enough boxes to meet ERG needs $targetBalance (found only $currentBalance)", currentBalance) + ) + } + } + + /** + * Helper method to construct change outputs + * + * @param foundBalance - ERG balance of boxes collected + * (spendable only, so after possibly deducting re-emission tokens) + * @param targetBalance - ERG amount to be transferred to recipients + * @param foundBoxAssets - assets balances of boxes + * @param targetBoxAssets - assets amounts to be transferred to recipients + * @return + */ + def formChangeBoxes(foundBalance: Long, + targetBalance: Long, + foundBoxAssets: mutable.Map[ModifierId, Long], + targetBoxAssets: TokensMap): Either[BoxSelectionError, Seq[ErgoBoxAssets]] = { + AssetUtils.subtractAssetsMut(foundBoxAssets, targetBoxAssets) + val changeBoxesAssets: Seq[mutable.Map[ModifierId, Long]] = foundBoxAssets.grouped(MaxAssetsPerBox).toSeq + val changeBalance = foundBalance - targetBalance + //at least a minimum amount of ERG should be assigned per a created box + if (changeBoxesAssets.size * MinBoxValue > changeBalance) { + Left(NotEnoughCoinsForChangeBoxesError( + s"Not enough nanoERGs ($changeBalance nanoERG) to create ${changeBoxesAssets.size} change boxes, \nfor $changeBoxesAssets" + )) + } else { + val changeBoxes = if (changeBoxesAssets.nonEmpty) { + val baseChangeBalance = changeBalance / changeBoxesAssets.size + + val changeBoxesNoBalanceAdjusted = changeBoxesAssets.map { a => + ErgoBoxAssetsHolder(baseChangeBalance, a.toMap) + } + + val modifiedBoxOpt = changeBoxesNoBalanceAdjusted.headOption.map { firstBox => + ErgoBoxAssetsHolder( + changeBalance - baseChangeBalance * (changeBoxesAssets.size - 1), + firstBox.tokens + ) + } + + modifiedBoxOpt.toSeq ++ changeBoxesNoBalanceAdjusted.tail + } else if (changeBalance > 0) { + Seq(ErgoBoxAssetsHolder(changeBalance)) + } else { + Seq.empty + } + + Right(changeBoxes) + } + } + +} diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/BoxSelectorsJavaHelpers.scala b/lib-api/src/main/java/org/ergoplatform/appkit/InputBoxesValidatorJavaHelper.scala similarity index 58% rename from lib-api/src/main/java/org/ergoplatform/appkit/BoxSelectorsJavaHelpers.scala rename to lib-api/src/main/java/org/ergoplatform/appkit/InputBoxesValidatorJavaHelper.scala index 762fd7d5..93ab25ce 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/BoxSelectorsJavaHelpers.scala +++ b/lib-api/src/main/java/org/ergoplatform/appkit/InputBoxesValidatorJavaHelper.scala @@ -1,40 +1,41 @@ package org.ergoplatform.appkit -import scala.collection.mutable -import org.ergoplatform.wallet.boxes.DefaultBoxSelector -import org.ergoplatform.wallet.boxes.DefaultBoxSelector.NotEnoughCoinsForChangeBoxesError -import org.ergoplatform.wallet.boxes.DefaultBoxSelector.NotEnoughErgsError -import org.ergoplatform.wallet.boxes.DefaultBoxSelector.NotEnoughTokensError - -import java.util.{List => JList, Map => JMap} -import org.ergoplatform.appkit.JavaHelpers._ -import org.ergoplatform.appkit.Iso._ -import org.ergoplatform.ErgoBox.TokenId import org.ergoplatform.ErgoBoxAssets -import org.ergoplatform.ErgoBoxAssetsHolder import org.ergoplatform.appkit.InputBoxesSelectionException.{NotEnoughErgsException, NotEnoughTokensException} +import org.ergoplatform.appkit.Iso._ +import org.ergoplatform.appkit.JavaHelpers._ +import org.ergoplatform.wallet.AssetUtils +import org.ergoplatform.wallet.boxes.DefaultBoxSelector.{NotEnoughCoinsForChangeBoxesError, NotEnoughErgsError, NotEnoughTokensError} import scorex.util.{ModifierId, bytesToId} import java.util +import java.util.{List => JList} +import scala.collection.mutable -object BoxSelectorsJavaHelpers { +object InputBoxesValidatorJavaHelper { final case class InputBoxWrapper(val inputBox: InputBox) extends ErgoBoxAssets { override def value: Long = inputBox.getValue - override def tokens: Map[ModifierId, Long] = - inputBox.getTokens.convertTo[mutable.LinkedHashMap[ModifierId, Long]].toMap + + override def tokens: Map[ModifierId, Long] = { + val tokens = mutable.Map[ModifierId, Long]() + inputBox.getTokens.convertTo[IndexedSeq[ErgoToken]].foreach { token: ErgoToken => + AssetUtils.mergeAssetsMut(tokens, Map.apply(bytesToId(token.getId.getBytes) -> token.getValue)) + } + tokens.toMap + } } - def selectBoxes(unspentBoxes: JList[InputBox], - amountToSpend: Long, - tokensToSpend: JList[ErgoToken]): JList[InputBox] = { + def validateBoxes(unspentBoxes: JList[InputBox], + amountToSpend: Long, + tokensToSpend: JList[ErgoToken]): Unit = { val inputBoxes = unspentBoxes.convertTo[IndexedSeq[InputBox]] - .map(InputBoxWrapper.apply).toIterator + .map(InputBoxWrapper.apply) val targetAssets = tokensToSpend.convertTo[mutable.LinkedHashMap[ModifierId, Long]].toMap - val foundBoxes: IndexedSeq[InputBox] = new DefaultBoxSelector(None).select(inputBoxes, amountToSpend, targetAssets) match { + new InputBoxesValidator().select(inputBoxes.toIterator, amountToSpend, targetAssets) match { case Left(err: NotEnoughCoinsForChangeBoxesError) => - throw new InputBoxesSelectionException.NotEnoughCoinsForChangeException(err.message) + throw new InputBoxesSelectionException.NotEnoughCoinsForChangeException(err.message) case Left(err: NotEnoughErgsError) => { // we might have a ChangeBox error here as well, so let's report it correctly if (err.balanceFound >= amountToSpend) { @@ -53,9 +54,8 @@ object BoxSelectorsJavaHelpers { case Left(err) => throw new InputBoxesSelectionException( s"Not enough funds in boxes to pay $amountToSpend nanoERGs, \ntokens: $tokensToSpend, \nreason: $err") - case Right(v) => v.boxes.map(_.inputBox).toIndexedSeq + case Right(v) => // do nothing, everything alright } - foundBoxes.convertTo[JList[InputBox]] } } diff --git a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala index 68f996de..769fa615 100644 --- a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala +++ b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala @@ -1,22 +1,19 @@ package org.ergoplatform.appkit.impl -import java.util import org.ergoplatform._ -import org.ergoplatform.appkit.{Iso, _} +import org.ergoplatform.appkit.JavaHelpers._ +import org.ergoplatform.appkit.Parameters.{MinChangeValue, MinFee} +import org.ergoplatform.appkit._ import org.ergoplatform.wallet.protocol.context.ErgoLikeStateContext import org.ergoplatform.wallet.transactions.TransactionBuilder -import org.ergoplatform.wallet.boxes.DefaultBoxSelector -import org.ergoplatform.wallet.boxes.BoxSelector +import scorex.crypto.authds.ADDigest +import sigmastate.eval.Colls import special.collection.Coll import special.sigma.Header +import java.util import java.util._ import java.util.stream.Collectors -import org.ergoplatform.appkit.Parameters.{MinChangeValue, MinFee} -import scorex.crypto.authds.ADDigest -import org.ergoplatform.appkit.JavaHelpers._ -import sigmastate.eval.Colls - import scala.collection.JavaConversions class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends UnsignedTransactionBuilder { @@ -131,7 +128,7 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un changeAddress = changeAddress, minChangeValue = MinChangeValue, minerRewardDelay = rewardDelay, burnTokens = burnTokens, - boxSelector = new DefaultBoxSelector(None)).get + boxSelector = new InputBoxesValidator()).get // the method above don't accept ContextExtension along with inputs, thus, after the // transaction has been built we need to zip with the extensions that have been From 44218b415674aa22b53f24b1b6d7e0be5fc8fd01 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Tue, 1 Nov 2022 13:28:54 +0100 Subject: [PATCH 13/44] EIP-31 Basic implementation of data classes and transaction to create fee box --- .../ergoplatform/appkit/BabelFeeSpec.scala | 129 +++++++++++++++++ .../org/ergoplatform/appkit/ErgoValue.java | 4 + .../appkit/babelfee/BabelFeeBox.java | 134 ++++++++++++++++++ .../appkit/babelfee/BabelFeeBoxBuilder.java | 60 ++++++++ .../appkit/babelfee/BabelFeeBoxContract.java | 62 ++++++++ .../appkit/babelfee/BabelFeeOperations.java | 38 +++++ 6 files changed, 427 insertions(+) create mode 100644 appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala create mode 100644 lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBox.java create mode 100644 lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxBuilder.java create mode 100644 lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java create mode 100644 lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala new file mode 100644 index 00000000..4e8d1ebe --- /dev/null +++ b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala @@ -0,0 +1,129 @@ +package org.ergoplatform.appkit + +import org.ergoplatform.ErgoScriptPredef +import org.ergoplatform.appkit.babelfee.{BabelFeeBox, BabelFeeBoxBuilder, BabelFeeOperations} +import org.ergoplatform.appkit.examples.RunMockedScala.createMockedErgoClient +import org.scalatest.{Matchers, PropSpec} +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import scorex.util.Random +import scorex.util.encode.Base16 +import sigmastate.interpreter.HintsBag + +import java.nio.charset.StandardCharsets +import java.util.Arrays + +class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChecks + with HttpClientTesting + with AppkitTestingCommon { + + private val script = + """{ + | + | // ===== Contract Information ===== // + | // Name: EIP-0031 Babel Fees Contract + | // Description: Contract guarding the babel fee box, checking if valid output babel box was recreated and the token exchange was valid. + | // Version: 1.0.0 + | + | // ===== Relevant Variables ===== // + | val babelFeeBoxCreator: SigmaProp = SELF.R4[SigmaProp].get + | val ergPricePerToken: Long = SELF.R5[Long].get + | val tokenId: Coll[Byte] = _tokenId + | val recreatedBabelBoxIndex: Option[Int] = getVar[Int](0) + | + | // ===== Perform Babel Fee Swap ===== // + | if (recreatedBabelBoxIndex.isDefined) { + | + | // Check conditions for a valid babel fee swap + | val validBabelFeeSwap: Boolean = { + | + | // Output babel fee box + | val recreatedBabelBox: Box = OUTPUTS(recreatedBabelBoxIndex.get) + | + | // Check that the babel fee box is recreated correctly + | val validBabelFeeBoxRecreation: Boolean = + | + | allOf(Coll( + | (recreatedBabelBox.propositionBytes == SELF.propositionBytes), + | (recreatedBabelBox.tokens(0)._1 == tokenId), + | (recreatedBabelBox.R4[SigmaProp].get == babelFeeBoxCreator), + | (recreatedBabelBox.R5[Long].get == ergPricePerToken), + | (recreatedBabelBox.R6[Coll[Byte]].get == SELF.id) + | )) + | + | + | + | // Check that the user's token was exchanged correctly + | val validBabelFeeExchange: Boolean = { + | + | val nanoErgsDifference: Long = SELF.value - recreatedBabelBox.value + | val babelTokensBefore: Long = if (SELF.tokens.size > 0) SELF.tokens(0)._2 else 0L + | val babelTokensDifference: Long = recreatedBabelBox.tokens(0)._2 - babelTokensBefore + | + | allOf(Coll( + | (babelTokensDifference * ergPricePerToken >= nanoErgsDifference), + | (nanoErgsDifference >= 0) + | )) + | + | } + | + | allOf(Coll( + | validBabelFeeBoxRecreation, + | validBabelFeeExchange + | )) + | + | } + | + | sigmaProp(validBabelFeeSwap) + | + | } else { + | + | // ===== Perform Babel Fee Box Withdrawl ===== // + | babelFeeBoxCreator + | + | } + | + |}""".stripMargin + + val mockTokenId = "f9e5ce5aa0d95f5d54a7bc89c46730d9662397067250aa18a0039631c0f5b809" + + property("Compile contract for certain token") { + val ergoClient = new ColdErgoClient(NetworkType.MAINNET, 0) + val contract = ergoClient.execute { ctx: BlockchainContext => + ctx.compileContract( + ConstantsBuilder.create() + .item("_tokenId", ErgoId.create(mockTokenId).getBytes) + .build(), + script + ) + } + + println(Base16.encode(contract.getErgoTree.bytes)) + } + + + property("babel fee box creation and revoke") { + val ergoClient = createMockedErgoClient(MockData(Nil, Nil)) + ergoClient.execute { ctx: BlockchainContext => + val creator = address + + val amountToSend = Parameters.OneErg * 100 + + val input1 = ctx.newTxBuilder.outBoxBuilder + .value(amountToSend + Parameters.MinFee) + .contract(creator.toErgoContract) + .build().convertToInputWith(mockTokenId, 0) + + val tx = BabelFeeOperations.createNewBabelContractTx(BoxOperations.createForSender(creator, ctx) + .withAmountToSpend(amountToSend) + .withInputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))), + ErgoId.create(mockTokenId), + Parameters.OneErg); + + ctx.newProverBuilder().build().reduce(tx, 0) + + val babelFeeErgoBox = tx.getOutputs.get(0).convertToInputWith(mockTokenId, 0) + + val babelFeeBox = new BabelFeeBox(babelFeeErgoBox) + } + } +} diff --git a/common/src/main/java/org/ergoplatform/appkit/ErgoValue.java b/common/src/main/java/org/ergoplatform/appkit/ErgoValue.java index 4acd691a..687ce327 100644 --- a/common/src/main/java/org/ergoplatform/appkit/ErgoValue.java +++ b/common/src/main/java/org/ergoplatform/appkit/ErgoValue.java @@ -114,6 +114,10 @@ static public ErgoValue of(Values.SigmaBoolean value) { return new ErgoValue<>(JavaHelpers.SigmaDsl().SigmaProp(value), ErgoType.sigmaPropType()); } + static public ErgoValue of(org.ergoplatform.appkit.SigmaProp value) { + return new ErgoValue<>(JavaHelpers.SigmaDsl().SigmaProp(value.getSigmaBoolean()), ErgoType.sigmaPropType()); + } + static public ErgoValue of(AvlTreeData value) { return new ErgoValue<>(JavaHelpers.SigmaDsl().avlTree(value), ErgoType.avlTreeType()); } diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBox.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBox.java new file mode 100644 index 00000000..aefa6fb6 --- /dev/null +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBox.java @@ -0,0 +1,134 @@ +package org.ergoplatform.appkit.babelfee; + +import org.ergoplatform.appkit.ErgoId; +import org.ergoplatform.appkit.ErgoToken; +import org.ergoplatform.appkit.ErgoValue; +import org.ergoplatform.appkit.OutBox; +import org.ergoplatform.appkit.OutBoxBuilder; +import org.ergoplatform.appkit.Parameters; +import org.ergoplatform.appkit.SigmaProp; +import org.ergoplatform.appkit.TransactionBox; +import org.ergoplatform.appkit.UnsignedTransactionBuilder; + +import java.util.List; + +/** + * Represents a Babel Fee Box, see EIP-0031 + */ +public class BabelFeeBox { + + private final long pricePerToken; + private final ErgoId tokenId; + private final SigmaProp boxCreator; + private final long value; + private final long tokenAmount; + + public BabelFeeBox(TransactionBox ergoBox) { + value = ergoBox.getValue(); + List> registers = ergoBox.getRegisters(); + boxCreator = new SigmaProp((special.sigma.SigmaProp) registers.get(0).getValue()); + pricePerToken = (long) registers.get(1).getValue(); + this.tokenId = new BabelFeeBoxContract().getTokenIdFromErgoTree(ergoBox.getErgoTree()); + + if (!ergoBox.getTokens().isEmpty()) { + ErgoToken ergoToken = ergoBox.getTokens().get(0); + tokenAmount = ergoToken.getValue(); + + if (!ergoToken.getId().equals(tokenId)) { + throw new IllegalStateException("token id of contract and token id in box diverge"); + } + } else { + tokenAmount = 0; + } + } + + BabelFeeBox(long pricePerToken, ErgoId tokenId, SigmaProp boxCreator, long value, long tokenAmount) { + this.pricePerToken = pricePerToken; + this.tokenId = tokenId; + this.boxCreator = boxCreator; + this.value = value; + this.tokenAmount = tokenAmount; + } + + /** + * @return price offered per raw token amount + */ + public long getPricePerToken() { + return pricePerToken; + } + + /** + * @return token id this babel fee box is offering change for + */ + public ErgoId getTokenId() { + return tokenId; + } + + /** + * @return box creator or owner of the babel fee box + */ + public SigmaProp getBoxCreator() { + return boxCreator; + } + + /** + * @return overall ERG value in the box. not all is available to change for token as a small + * amount must remain in the box + */ + public long getValue() { + return value; + } + + /** + * @return overall ERG value available to change for token + */ + public long getValueAvailableToBuy() { + return value - Parameters.MinChangeValue; + } + + /** + * @return max token amount possible to swap at best price + */ + public long maxAmountToBuy() { + return getValueAvailableToBuy() / pricePerToken; + } + + /** + * @param nanoergs amount we want to receive, usually to pay transaction fees + * @return the amount of tokens to sell to receive at least his amount of nanoergs + */ + public long tokensToSellForErgAmount(long nanoergs) { + long floorAmount = nanoergs / pricePerToken; + return (floorAmount * pricePerToken >= nanoergs) ? floorAmount : floorAmount + 1; + } + + public BabelFeeBox buildSucceedingBabelFeeBox(long tokenAmountChange) { + if (tokenAmountChange <= 0) + throw new IllegalArgumentException("tokenAmountChange must be greater than 0"); + if (tokenAmountChange > maxAmountToBuy()) + throw new IllegalArgumentException("tokenAmountChange must be less or equal maxAmountToBuy"); + + return new BabelFeeBox(pricePerToken, tokenId, boxCreator, + value - tokenAmountChange * pricePerToken, + tokenAmountChange + tokenAmount); + } + + /** + * @return outbox representing this babel fee box + */ + public OutBox buildOutbox(UnsignedTransactionBuilder txBuilder) { + OutBoxBuilder outBoxBuilder = txBuilder.outBoxBuilder() + .contract(new BabelFeeBoxContract().getContractForToken(tokenId, txBuilder.getNetworkType())) + .value(value) + .registers(ErgoValue.of(boxCreator), ErgoValue.of(pricePerToken)); + + if (tokenAmount > 0) + outBoxBuilder.tokens(new ErgoToken(tokenId, tokenAmount)); + + return outBoxBuilder.build(); + } + + public static BabelFeeBoxBuilder newBuilder() { + return new BabelFeeBoxBuilder(); + } +} diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxBuilder.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxBuilder.java new file mode 100644 index 00000000..e6718856 --- /dev/null +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxBuilder.java @@ -0,0 +1,60 @@ +package org.ergoplatform.appkit.babelfee; + +import org.ergoplatform.appkit.Address; +import org.ergoplatform.appkit.ErgoId; +import org.ergoplatform.appkit.SigmaProp; + +import java.util.Objects; + +public class BabelFeeBoxBuilder { + private long pricePerToken; + private ErgoId tokenId; + private SigmaProp boxCreator; + private long value; + private long tokenAmount; + + public BabelFeeBoxBuilder withPricePerToken(long pricePerToken) { + this.pricePerToken = pricePerToken; + return this; + } + + public BabelFeeBoxBuilder withTokenId(ErgoId tokenId) { + this.tokenId = tokenId; + return this; + } + + public BabelFeeBoxBuilder withBoxCreator(SigmaProp boxCreator) { + this.boxCreator = boxCreator; + return this; + } + + public BabelFeeBoxBuilder withBoxCreator(Address address) { + this.boxCreator = SigmaProp.createFromAddress(address); + return this; + } + + public BabelFeeBoxBuilder withValue(long value) { + this.value = value; + return this; + } + + public BabelFeeBoxBuilder withTokenAmount(long tokenAmount) { + if (tokenAmount < 0) + throw new IllegalArgumentException("pricePerToken must be equal or greater than 0"); + this.tokenAmount = tokenAmount; + return this; + } + + public BabelFeeBox build() { + Objects.requireNonNull(boxCreator, "Box creator not set"); + Objects.requireNonNull(tokenId, "Token ID not set"); + if (value <= 0) { + throw new IllegalArgumentException("value must be greater than 0"); + } + if (pricePerToken <= 0) { + throw new IllegalArgumentException("pricePerToken must be greater than 0"); + } + + return new BabelFeeBox(pricePerToken, tokenId, boxCreator, value, tokenAmount); + } +} diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java new file mode 100644 index 00000000..c3c6e7a9 --- /dev/null +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java @@ -0,0 +1,62 @@ +package org.ergoplatform.appkit.babelfee; + +import org.ergoplatform.appkit.ErgoContract; +import org.ergoplatform.appkit.ErgoId; +import org.ergoplatform.appkit.NetworkType; +import org.ergoplatform.appkit.impl.ErgoTreeContract; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +import scorex.util.encode.Base16; +import sigmastate.Values; +import sigmastate.serialization.ErgoTreeSerializer; + +public class BabelFeeBoxContract { + static final String contractTemplateHexPref = "100604000e20"; + static final String contractTemplateHexSuf = "0400040005000500d803d601e30004d602e4c6a70408d603e4c6a7050595e67201d804d604b2a5e4720100d605b2db63087204730000d606db6308a7d60799c1a7c17204d1968302019683050193c27204c2a7938c720501730193e4c672040408720293e4c672040505720393e4c67204060ec5a796830201929c998c7205029591b1720673028cb272067303000273047203720792720773057202"; + + private static byte[] contractPref; + private static byte[] contractSuf; + + private static void prepare() { + if (contractPref == null || contractSuf == null) { + contractPref = Base16.decode(contractTemplateHexPref).get(); + contractSuf = Base16.decode(contractTemplateHexSuf).get(); + } + } + + public ErgoContract getContractForToken(ErgoId tokenId, NetworkType networkType) { + prepare(); + + byte[] idBytes = tokenId.getBytes(); + + byte[] completeContract = ByteBuffer.allocate(contractPref.length + contractSuf.length + idBytes.length) + .put(contractPref) + .put(idBytes) + .put(contractSuf) + .array(); + + return new ErgoTreeContract(ErgoTreeSerializer.DefaultSerializer().deserializeErgoTree(completeContract), networkType); + } + + public ErgoId getTokenIdFromErgoTree(Values.ErgoTree ergoTree) { + prepare(); + + byte[] treeBytes = ergoTree.bytes(); + + ByteBuffer bb = ByteBuffer.wrap(treeBytes); + + byte[] thisPref = new byte[contractPref.length]; + byte[] thisSuf = new byte[contractSuf.length]; + byte[] tokenId = new byte[treeBytes.length - contractPref.length - contractSuf.length]; + bb.get(thisPref, 0, thisPref.length); + bb.get(tokenId, 0, tokenId.length); + bb.get(thisSuf, 0, thisSuf.length); + + if (!Arrays.equals(thisPref, contractPref) || !Arrays.equals(thisSuf, contractSuf)) + throw new IllegalArgumentException("Contract does not fit template"); + + return new ErgoId(tokenId); + } +} diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java new file mode 100644 index 00000000..57575a28 --- /dev/null +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java @@ -0,0 +1,38 @@ +package org.ergoplatform.appkit.babelfee; + +import org.ergoplatform.appkit.Address; +import org.ergoplatform.appkit.BlockchainContext; +import org.ergoplatform.appkit.BoxOperations; +import org.ergoplatform.appkit.ErgoId; +import org.ergoplatform.appkit.OutBox; +import org.ergoplatform.appkit.UnsignedTransaction; + +public class BabelFeeOperations { + /** + * creates a new babel fee box for a given token id and price per token + * + * @param boxOperations prepared BoxOperations object defining babel fee box creator and amount + * to spend + * @param tokenId tokenId to create the babel fee box for + * @param pricePerToken the price per raw token value + * @return prepared transaction to create the new babel fee box + */ + public static UnsignedTransaction createNewBabelContractTx( + BoxOperations boxOperations, + ErgoId tokenId, + long pricePerToken + ) { + BabelFeeBox babelFeeBox = BabelFeeBox.newBuilder().withBoxCreator(boxOperations.getSenders().get(0)) + .withPricePerToken(pricePerToken) + .withTokenId(tokenId) + .withValue(boxOperations.getAmountToSpend()) + .build(); + + return boxOperations.buildTxWithDefaultInputs(txB -> { + OutBox outBox = babelFeeBox.buildOutbox(txB); + txB.outputs(outBox); + return txB; + }); + } + +} From c8a613adb006c662aeb70e46cbc9d20702f77223 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Tue, 1 Nov 2022 16:34:33 +0100 Subject: [PATCH 14/44] EIP-0031 add BabelFeeOperations.cancelBabelFeeContract --- .../ergoplatform/appkit/BabelFeeSpec.scala | 13 +++++-- .../appkit/UnsignedTransactionBuilder.java | 9 +++++ .../appkit/babelfee/BabelFeeBox.java | 7 ++++ .../appkit/babelfee/BabelFeeOperations.java | 39 ++++++++++++++++++- .../impl/UnsignedTransactionBuilderImpl.scala | 4 ++ 5 files changed, 67 insertions(+), 5 deletions(-) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala index 4e8d1ebe..756d66b8 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala @@ -113,17 +113,22 @@ class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyC .contract(creator.toErgoContract) .build().convertToInputWith(mockTokenId, 0) - val tx = BabelFeeOperations.createNewBabelContractTx(BoxOperations.createForSender(creator, ctx) + val txCreate = BabelFeeOperations.createNewBabelContractTx(BoxOperations.createForSender(creator, ctx) .withAmountToSpend(amountToSend) .withInputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))), ErgoId.create(mockTokenId), Parameters.OneErg); - ctx.newProverBuilder().build().reduce(tx, 0) + ctx.newProverBuilder().build().reduce(txCreate, 0) - val babelFeeErgoBox = tx.getOutputs.get(0).convertToInputWith(mockTokenId, 0) + val babelFeeErgoBox = txCreate.getOutputs.get(0).convertToInputWith(mockTokenId, 0) - val babelFeeBox = new BabelFeeBox(babelFeeErgoBox) + // now we cancel the babel box + val txCancel = BabelFeeOperations.cancelBabelFeeContract(BoxOperations.createForSender(creator, ctx) + .withInputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))), babelFeeErgoBox) + + ctx.newProverBuilder().withMnemonic(mnemonic, SecretString.empty(), false).build() + .sign(txCancel) } } } 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 f02e0069..b4597ce1 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/UnsignedTransactionBuilder.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/UnsignedTransactionBuilder.java @@ -79,9 +79,18 @@ public interface UnsignedTransactionBuilder { * Adds change output to the specified address if needed. * * @param address address to send output + * @deprecated use {@link #sendChangeTo(Address)} */ + @Deprecated UnsignedTransactionBuilder sendChangeTo(ErgoAddress address); + /** + * Adds change output to the specified address if needed. + * + * @param address address to send output + */ + UnsignedTransactionBuilder sendChangeTo(Address address); + /** * Builds a new unsigned transaction in the {@link BlockchainContext context} inherited from this builder. * diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBox.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBox.java index aefa6fb6..09df451c 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBox.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBox.java @@ -102,6 +102,13 @@ public long tokensToSellForErgAmount(long nanoergs) { return (floorAmount * pricePerToken >= nanoergs) ? floorAmount : floorAmount + 1; } + /** + * constructs a new babel fee box state based on this fee box with a certain token amount change + * done + * + * @param tokenAmountChange the token amount to add to the new babel fee box + * @return new babel fee box after swap was done + */ public BabelFeeBox buildSucceedingBabelFeeBox(long tokenAmountChange) { if (tokenAmountChange <= 0) throw new IllegalArgumentException("tokenAmountChange must be greater than 0"); diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java index 57575a28..e44e1325 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java @@ -1,11 +1,17 @@ package org.ergoplatform.appkit.babelfee; import org.ergoplatform.appkit.Address; -import org.ergoplatform.appkit.BlockchainContext; import org.ergoplatform.appkit.BoxOperations; import org.ergoplatform.appkit.ErgoId; +import org.ergoplatform.appkit.InputBox; import org.ergoplatform.appkit.OutBox; +import org.ergoplatform.appkit.OutBoxBuilder; +import org.ergoplatform.appkit.Parameters; import org.ergoplatform.appkit.UnsignedTransaction; +import org.ergoplatform.appkit.UnsignedTransactionBuilder; + +import java.util.ArrayList; +import java.util.List; public class BabelFeeOperations { /** @@ -35,4 +41,35 @@ public static UnsignedTransaction createNewBabelContractTx( }); } + public static UnsignedTransaction cancelBabelFeeContract(BoxOperations boxOperations, InputBox babelBox) { + BabelFeeBox babelFeeBox = new BabelFeeBox(babelBox); + + Address creatorAddress = Address.fromSigmaBoolean(babelFeeBox.getBoxCreator().getSigmaBoolean(), + boxOperations.getBlockchainContext().getNetworkType()); + + // if we can't pay tx fee from the babel box content, we need to load another box + List boxesToSpend; + if (babelFeeBox.getValue() < boxOperations.getFeeAmount() + Parameters.MinChangeValue) + boxesToSpend = boxOperations.loadTop(); + else + boxesToSpend = new ArrayList<>(); + + boxesToSpend.add(0, babelBox); + + UnsignedTransactionBuilder txB = boxOperations.getBlockchainContext().newTxBuilder(); + + OutBoxBuilder outBoxBuilder = txB.outBoxBuilder() + .value(Math.max(Parameters.MinChangeValue, babelFeeBox.getValue() - boxOperations.getFeeAmount())) + .contract(creatorAddress.toErgoContract()); + + if (!babelBox.getTokens().isEmpty()) + outBoxBuilder.tokens(babelBox.getTokens().get(0)); + + return txB.boxesToSpend(boxesToSpend) + .fee(boxOperations.getFeeAmount()) + .sendChangeTo(creatorAddress) + .outputs(outBoxBuilder.build()) + .build(); + + } } diff --git a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala index 769fa615..a9cb2c9b 100644 --- a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala +++ b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala @@ -83,6 +83,10 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un this } + override def sendChangeTo(changeAddress: Address): UnsignedTransactionBuilder = { + sendChangeTo(changeAddress.getErgoAddress) + } + def getNonEmpty[T](list: Option[List[T]], msg: => String): List[T] = { list match { case Some(list) if !list.isEmpty => list From 448ff3d788212ba2ee48140204e00dfc6493fcac Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Tue, 1 Nov 2022 18:25:50 +0100 Subject: [PATCH 15/44] EIP-0031 Add BabelFeeOperations#getBabelFeeTransactionBuilder --- .../ergoplatform/appkit/BabelFeeSpec.scala | 46 +++++- .../appkit/babelfee/BabelFeeBoxBuilder.java | 4 +- ...BabelFeeBox.java => BabelFeeBoxState.java} | 19 ++- .../appkit/babelfee/BabelFeeOperations.java | 154 +++++++++++++++++- 4 files changed, 201 insertions(+), 22 deletions(-) rename lib-api/src/main/java/org/ergoplatform/appkit/babelfee/{BabelFeeBox.java => BabelFeeBoxState.java} (87%) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala index 756d66b8..5f4d9e99 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala @@ -1,15 +1,11 @@ package org.ergoplatform.appkit -import org.ergoplatform.ErgoScriptPredef -import org.ergoplatform.appkit.babelfee.{BabelFeeBox, BabelFeeBoxBuilder, BabelFeeOperations} -import org.ergoplatform.appkit.examples.RunMockedScala.createMockedErgoClient +import org.ergoplatform.appkit.babelfee.{BabelFeeBoxState, BabelFeeOperations} import org.scalatest.{Matchers, PropSpec} import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks -import scorex.util.Random import scorex.util.encode.Base16 -import sigmastate.interpreter.HintsBag -import java.nio.charset.StandardCharsets +import java.util import java.util.Arrays class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyChecks @@ -131,4 +127,42 @@ class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyC .sign(txCancel) } } + + property("babel fee box use") { + val ergoClient = createMockedErgoClient(MockData(Nil, Nil)) + ergoClient.execute { ctx: BlockchainContext => + val sender = address + + val txB = ctx.newTxBuilder + val babelFeeBoxState = BabelFeeBoxState.newBuilder().withValue(Parameters.OneErg) + .withTokenId(ErgoId.create(mockTokenId)) + .withBoxCreator(Address.create(secondEip3AddrStr)) + .withPricePerToken(Parameters.MinFee) + .build() + + val fee = Parameters.MinFee + + val output = txB.outBoxBuilder + .value(Parameters.MinChangeValue) + .contract(sender.toErgoContract) + .tokens(new ErgoToken(ErgoId.create(mockTokenId), 1000 - babelFeeBoxState.tokensToSellForErgAmount(fee))) + .build() + val input = txB.outBoxBuilder + .value(Parameters.MinChangeValue) + .contract(sender.toErgoContract) + .tokens(new ErgoToken(ErgoId.create(mockTokenId), 1000)) + .build().convertToInputWith(mockTokenId, 0) + + val babelTxB = BabelFeeOperations.getBabelFeeTransactionBuilder(txB, babelFeeBoxState.buildOutbox(txB, null).convertToInputWith(mockTokenId, 0), fee) + + val tx = babelTxB.fee(fee) + .outputs(output) + .boxesToSpend(util.Arrays.asList(input)) + .sendChangeTo(sender) + .build() + + ctx.newProverBuilder().withMnemonic(mnemonic, SecretString.empty(), false).build() + .sign(tx) + } + } } diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxBuilder.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxBuilder.java index e6718856..f9b03dd7 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxBuilder.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxBuilder.java @@ -45,7 +45,7 @@ public BabelFeeBoxBuilder withTokenAmount(long tokenAmount) { return this; } - public BabelFeeBox build() { + public BabelFeeBoxState build() { Objects.requireNonNull(boxCreator, "Box creator not set"); Objects.requireNonNull(tokenId, "Token ID not set"); if (value <= 0) { @@ -55,6 +55,6 @@ public BabelFeeBox build() { throw new IllegalArgumentException("pricePerToken must be greater than 0"); } - return new BabelFeeBox(pricePerToken, tokenId, boxCreator, value, tokenAmount); + return new BabelFeeBoxState(pricePerToken, tokenId, boxCreator, value, tokenAmount); } } diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBox.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java similarity index 87% rename from lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBox.java rename to lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java index 09df451c..ee36b3d7 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBox.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java @@ -3,6 +3,7 @@ import org.ergoplatform.appkit.ErgoId; import org.ergoplatform.appkit.ErgoToken; import org.ergoplatform.appkit.ErgoValue; +import org.ergoplatform.appkit.InputBox; import org.ergoplatform.appkit.OutBox; import org.ergoplatform.appkit.OutBoxBuilder; import org.ergoplatform.appkit.Parameters; @@ -12,10 +13,12 @@ import java.util.List; +import javax.annotation.Nullable; + /** - * Represents a Babel Fee Box, see EIP-0031 + * Represents a Babel Fee Box state, see EIP-0031 */ -public class BabelFeeBox { +public class BabelFeeBoxState { private final long pricePerToken; private final ErgoId tokenId; @@ -23,7 +26,7 @@ public class BabelFeeBox { private final long value; private final long tokenAmount; - public BabelFeeBox(TransactionBox ergoBox) { + public BabelFeeBoxState(TransactionBox ergoBox) { value = ergoBox.getValue(); List> registers = ergoBox.getRegisters(); boxCreator = new SigmaProp((special.sigma.SigmaProp) registers.get(0).getValue()); @@ -42,7 +45,7 @@ public BabelFeeBox(TransactionBox ergoBox) { } } - BabelFeeBox(long pricePerToken, ErgoId tokenId, SigmaProp boxCreator, long value, long tokenAmount) { + BabelFeeBoxState(long pricePerToken, ErgoId tokenId, SigmaProp boxCreator, long value, long tokenAmount) { this.pricePerToken = pricePerToken; this.tokenId = tokenId; this.boxCreator = boxCreator; @@ -109,13 +112,13 @@ public long tokensToSellForErgAmount(long nanoergs) { * @param tokenAmountChange the token amount to add to the new babel fee box * @return new babel fee box after swap was done */ - public BabelFeeBox buildSucceedingBabelFeeBox(long tokenAmountChange) { + public BabelFeeBoxState buildSucceedingState(long tokenAmountChange) { if (tokenAmountChange <= 0) throw new IllegalArgumentException("tokenAmountChange must be greater than 0"); if (tokenAmountChange > maxAmountToBuy()) throw new IllegalArgumentException("tokenAmountChange must be less or equal maxAmountToBuy"); - return new BabelFeeBox(pricePerToken, tokenId, boxCreator, + return new BabelFeeBoxState(pricePerToken, tokenId, boxCreator, value - tokenAmountChange * pricePerToken, tokenAmountChange + tokenAmount); } @@ -123,11 +126,11 @@ public BabelFeeBox buildSucceedingBabelFeeBox(long tokenAmountChange) { /** * @return outbox representing this babel fee box */ - public OutBox buildOutbox(UnsignedTransactionBuilder txBuilder) { + public OutBox buildOutbox(UnsignedTransactionBuilder txBuilder, @Nullable InputBox babelBox) { OutBoxBuilder outBoxBuilder = txBuilder.outBoxBuilder() .contract(new BabelFeeBoxContract().getContractForToken(tokenId, txBuilder.getNetworkType())) .value(value) - .registers(ErgoValue.of(boxCreator), ErgoValue.of(pricePerToken)); + .registers(ErgoValue.of(boxCreator), ErgoValue.of(pricePerToken), ErgoValue.of(babelBox != null ? babelBox.getId().getBytes() : new byte[0])); if (tokenAmount > 0) outBoxBuilder.tokens(new ErgoToken(tokenId, tokenAmount)); diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java index e44e1325..dd7d070d 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java @@ -1,16 +1,24 @@ package org.ergoplatform.appkit.babelfee; +import org.ergoplatform.ErgoAddress; import org.ergoplatform.appkit.Address; +import org.ergoplatform.appkit.BlockchainContext; import org.ergoplatform.appkit.BoxOperations; +import org.ergoplatform.appkit.ContextVar; import org.ergoplatform.appkit.ErgoId; +import org.ergoplatform.appkit.ErgoToken; +import org.ergoplatform.appkit.ErgoValue; import org.ergoplatform.appkit.InputBox; +import org.ergoplatform.appkit.NetworkType; import org.ergoplatform.appkit.OutBox; import org.ergoplatform.appkit.OutBoxBuilder; import org.ergoplatform.appkit.Parameters; +import org.ergoplatform.appkit.PreHeader; import org.ergoplatform.appkit.UnsignedTransaction; import org.ergoplatform.appkit.UnsignedTransactionBuilder; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; public class BabelFeeOperations { @@ -28,28 +36,28 @@ public static UnsignedTransaction createNewBabelContractTx( ErgoId tokenId, long pricePerToken ) { - BabelFeeBox babelFeeBox = BabelFeeBox.newBuilder().withBoxCreator(boxOperations.getSenders().get(0)) + BabelFeeBoxState babelFeeBoxState = BabelFeeBoxState.newBuilder().withBoxCreator(boxOperations.getSenders().get(0)) .withPricePerToken(pricePerToken) .withTokenId(tokenId) .withValue(boxOperations.getAmountToSpend()) .build(); return boxOperations.buildTxWithDefaultInputs(txB -> { - OutBox outBox = babelFeeBox.buildOutbox(txB); + OutBox outBox = babelFeeBoxState.buildOutbox(txB, null); txB.outputs(outBox); return txB; }); } public static UnsignedTransaction cancelBabelFeeContract(BoxOperations boxOperations, InputBox babelBox) { - BabelFeeBox babelFeeBox = new BabelFeeBox(babelBox); + BabelFeeBoxState babelFeeBoxState = new BabelFeeBoxState(babelBox); - Address creatorAddress = Address.fromSigmaBoolean(babelFeeBox.getBoxCreator().getSigmaBoolean(), + Address creatorAddress = Address.fromSigmaBoolean(babelFeeBoxState.getBoxCreator().getSigmaBoolean(), boxOperations.getBlockchainContext().getNetworkType()); // if we can't pay tx fee from the babel box content, we need to load another box List boxesToSpend; - if (babelFeeBox.getValue() < boxOperations.getFeeAmount() + Parameters.MinChangeValue) + if (babelFeeBoxState.getValue() < boxOperations.getFeeAmount() + Parameters.MinChangeValue) boxesToSpend = boxOperations.loadTop(); else boxesToSpend = new ArrayList<>(); @@ -59,7 +67,7 @@ public static UnsignedTransaction cancelBabelFeeContract(BoxOperations boxOperat UnsignedTransactionBuilder txB = boxOperations.getBlockchainContext().newTxBuilder(); OutBoxBuilder outBoxBuilder = txB.outBoxBuilder() - .value(Math.max(Parameters.MinChangeValue, babelFeeBox.getValue() - boxOperations.getFeeAmount())) + .value(Math.max(Parameters.MinChangeValue, babelFeeBoxState.getValue() - boxOperations.getFeeAmount())) .contract(creatorAddress.toErgoContract()); if (!babelBox.getTokens().isEmpty()) @@ -72,4 +80,138 @@ public static UnsignedTransaction cancelBabelFeeContract(BoxOperations boxOperat .build(); } + + public static InputBox findBabelFeeBox(ErgoId tokenId, long feeAmount) { + throw new UnsupportedOperationException(); //TODO + } + + /** + * Creates a transaction builder for the given babel fee box and the amount to be covered by + * babel fees. The returned transaction builder can be used like normal and automatically will + * add the babel fee inbox and outbox to the transaction. You need to make sure that other + * inboxes cover the amount of tokens needed. + * + * @param txB transaction builder the new transaction builder uses under the hoods + * @param babelBox input babel box to make the swap with + * @param nanoErgToCover nanoergs to be covered by babel box, usually the fee amount needed, maybe a change amount as well + * @return transaction builder to be used + */ + public static UnsignedTransactionBuilder getBabelFeeTransactionBuilder( + UnsignedTransactionBuilder txB, + InputBox babelBox, + long nanoErgToCover + ) { + return new BabelFeeTransactionBuilder(txB, babelBox, nanoErgToCover); + } + + private static class BabelFeeTransactionBuilder implements UnsignedTransactionBuilder { + private final UnsignedTransactionBuilder superBuilder; + private final InputBox inputBabelBox; + private final OutBox outBabelBox; + + private List boxesToSpend; + private OutBox[] outputs; + + private BabelFeeTransactionBuilder(UnsignedTransactionBuilder superBuilder, InputBox babelBox, long nanoErgToCover) { + this.superBuilder = superBuilder; + inputBabelBox = babelBox; + BabelFeeBoxState babelBoxState = new BabelFeeBoxState(babelBox); + BabelFeeBoxState outBabelBoxState = babelBoxState.buildSucceedingState(babelBoxState.tokensToSellForErgAmount(nanoErgToCover)); + outBabelBox = outBabelBoxState.buildOutbox(superBuilder, babelBox); + } + + private void setBoxesToSpendAndOutputsToSuperBuilder() { + // when both boxes to spend and outputs are defined, we can add the babel box to the list + // and invoke the super transaction builder + // this is not possible before because we need to know output position for the input box + if (boxesToSpend != null && outputs != null) { + List allBoxesToSpend = new LinkedList<>(boxesToSpend); + allBoxesToSpend.add(inputBabelBox.withContextVars(ContextVar.of((byte) 0, ErgoValue.of(outputs.length)))); + OutBox[] allOutBoxes = new OutBox[outputs.length + 1]; + System.arraycopy(outputs, 0, allOutBoxes, 0, outputs.length); + allOutBoxes[outputs.length] = outBabelBox; + superBuilder.boxesToSpend(allBoxesToSpend); + superBuilder.outputs(allOutBoxes); + } + } + + @Override + public UnsignedTransactionBuilder preHeader(PreHeader ph) { + return superBuilder.preHeader(ph); + } + + @Override + public UnsignedTransactionBuilder boxesToSpend(List boxes) { + boxesToSpend = boxes; + setBoxesToSpendAndOutputsToSuperBuilder(); + return this; + } + + @Override + public UnsignedTransactionBuilder withDataInputs(List boxes) { + superBuilder.withDataInputs(boxes); + return this; + } + + @Override + public UnsignedTransactionBuilder outputs(OutBox... outputs) { + this.outputs = outputs; + setBoxesToSpendAndOutputsToSuperBuilder(); + return this; + } + + @Override + public UnsignedTransactionBuilder fee(long feeAmount) { + superBuilder.fee(feeAmount); + return this; + } + + @Override + public UnsignedTransactionBuilder tokensToBurn(ErgoToken... tokens) { + superBuilder.tokensToBurn(tokens); + return this; + } + + @Override + public UnsignedTransactionBuilder sendChangeTo(ErgoAddress address) { + superBuilder.sendChangeTo(address); + return this; + } + + @Override + public UnsignedTransactionBuilder sendChangeTo(Address address) { + superBuilder.sendChangeTo(address); + return this; + } + + @Override + public UnsignedTransaction build() { + return superBuilder.build(); + } + + @Override + public BlockchainContext getCtx() { + return superBuilder.getCtx(); + } + + @Override + public PreHeader getPreHeader() { + return superBuilder.getPreHeader(); + } + + @Override + public NetworkType getNetworkType() { + return superBuilder.getNetworkType(); + } + + @Override + public OutBoxBuilder outBoxBuilder() { + return superBuilder.outBoxBuilder(); + } + + @Override + public List getInputBoxes() { + return boxesToSpend; + } + } } From d440b20a4feb2ed0a62443abe8244854ec6658be Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Tue, 1 Nov 2022 19:54:52 +0100 Subject: [PATCH 16/44] EIP-0031 BabelFeeOperations#findBabelFeeBox --- .../ergoplatform/appkit/BabelFeeSpec.scala | 6 +-- .../appkit/babelfee/BabelFeeOperations.java | 46 ++++++++++++++++++- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala index 5f4d9e99..53f83227 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala @@ -1,9 +1,8 @@ package org.ergoplatform.appkit -import org.ergoplatform.appkit.babelfee.{BabelFeeBoxState, BabelFeeOperations} +import org.ergoplatform.appkit.babelfee.{BabelFeeBoxContract, BabelFeeBoxState, BabelFeeOperations} import org.scalatest.{Matchers, PropSpec} import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks -import scorex.util.encode.Base16 import java.util import java.util.Arrays @@ -93,7 +92,8 @@ class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyC ) } - println(Base16.encode(contract.getErgoTree.bytes)) + val contract2 = new BabelFeeBoxContract().getContractForToken(ErgoId.create(mockTokenId), NetworkType.MAINNET) + contract.getErgoTree shouldBe contract2.getErgoTree } diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java index dd7d070d..1aa12253 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java @@ -5,6 +5,7 @@ import org.ergoplatform.appkit.BlockchainContext; import org.ergoplatform.appkit.BoxOperations; import org.ergoplatform.appkit.ContextVar; +import org.ergoplatform.appkit.ErgoContract; import org.ergoplatform.appkit.ErgoId; import org.ergoplatform.appkit.ErgoToken; import org.ergoplatform.appkit.ErgoValue; @@ -18,9 +19,12 @@ import org.ergoplatform.appkit.UnsignedTransactionBuilder; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedList; import java.util.List; +import javax.annotation.Nullable; + public class BabelFeeOperations { /** * creates a new babel fee box for a given token id and price per token @@ -81,8 +85,46 @@ public static UnsignedTransaction cancelBabelFeeContract(BoxOperations boxOperat } - public static InputBox findBabelFeeBox(ErgoId tokenId, long feeAmount) { - throw new UnsupportedOperationException(); //TODO + /** + * Tries to fetch a babel fee box from blockchain data source with the given unspent boxes loader + * + * @param ctx current blockchain context + * @param loader loader to receive unspent boxes + * @param tokenId tokenId offered to swap + * @param feeAmount nanoerg amount needed to swap + * @return babel fee box satisfying the needs, or null if none available + */ + @Nullable + public static InputBox findBabelFeeBox(BlockchainContext ctx, BoxOperations.IUnspentBoxesLoader loader, ErgoId tokenId, long feeAmount) { + ErgoContract contractForToken = new BabelFeeBoxContract().getContractForToken(tokenId, ctx.getNetworkType()); + Address address = contractForToken.toAddress(); + loader.prepare(ctx, Collections.singletonList(address), feeAmount, new ArrayList<>()); + + int page = 0; + List inputBoxes = loader.loadBoxesPage(ctx, address, 0); + + InputBox returnBox = null; + long pricePerToken = Long.MAX_VALUE; + + while (!inputBoxes.isEmpty() && returnBox == null) { + + // find the cheapest box satisfying our fee amount needs + for (InputBox inputBox : inputBoxes) { + try { + BabelFeeBoxState babelFeeBoxState = new BabelFeeBoxState(inputBox); + if (babelFeeBoxState.getValueAvailableToBuy() > feeAmount && babelFeeBoxState.getPricePerToken() < pricePerToken) + returnBox = inputBox; + } catch (Throwable t) { + // ignore, check next + } + } + + page++; + // get another page + inputBoxes = loader.loadBoxesPage(ctx, address, page); + } + + return returnBox; } /** From fd5ac706fedf20eedc0969a1a5e7104450d9599c Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Tue, 1 Nov 2022 20:09:35 +0100 Subject: [PATCH 17/44] Fix Java 8 compile --- .../java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java index ee36b3d7..1fe80993 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java @@ -30,7 +30,7 @@ public BabelFeeBoxState(TransactionBox ergoBox) { value = ergoBox.getValue(); List> registers = ergoBox.getRegisters(); boxCreator = new SigmaProp((special.sigma.SigmaProp) registers.get(0).getValue()); - pricePerToken = (long) registers.get(1).getValue(); + pricePerToken = (Long) registers.get(1).getValue(); this.tokenId = new BabelFeeBoxContract().getTokenIdFromErgoTree(ergoBox.getErgoTree()); if (!ergoBox.getTokens().isEmpty()) { From b22fef8a14d15f1a5df2344bef561fa8c9ebdfef Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Wed, 2 Nov 2022 14:52:20 +0100 Subject: [PATCH 18/44] New TransactionsApi endpoints added in ergoplatform/ergo#1869 (#197) --- .../restapi/client/TransactionsApi.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/java-client-generated/src/main/java/org/ergoplatform/restapi/client/TransactionsApi.java b/java-client-generated/src/main/java/org/ergoplatform/restapi/client/TransactionsApi.java index 3d097ff1..382769db 100755 --- a/java-client-generated/src/main/java/org/ergoplatform/restapi/client/TransactionsApi.java +++ b/java-client-generated/src/main/java/org/ergoplatform/restapi/client/TransactionsApi.java @@ -77,6 +77,37 @@ Call getUnconfirmedTransactions( @retrofit2.http.Query("limit") Integer limit , @retrofit2.http.Query("offset") Integer offset ); + /** + * Get unconfirmed transaction from pool. Available from node version 4.0.105 and up. Older + * versions will return a 404 error, as will newer versions when the transaction is not in + * mempool. + * + * @param txId ID of a transaction in question + * @return Ergo Transaction + */ + @GET("transactions/unconfirmed/byTransactionId/{txId}") + Call getUnconfirmedTransactionById( + @retrofit2.http.Path("txId") String txId + ); + + /** + * Finds unconfirmed transactions by ErgoTree hex of one of its output or input boxes + * (if present in UtxoState). Available from node version 4.0.105 and up. Older + * versions will return a 404 error. + * + * @param ergoTreeHex ErgoTree hex representation with surrounding quotes ("0008cd...") + * @param limit The number of items in list to return (optional, default to 50) + * @param offset The number of items in list to skip (optional, default to 0) + * @return Call<Transactions> + */ + @POST("transactions/unconfirmed/byErgoTree") + @Headers({ + "Content-Type:application/json" + }) + Call getUnconfirmedTransactionsByErgoTree( + @retrofit2.http.Body String ergoTreeHex, @retrofit2.http.Query("offset") Integer offset, @retrofit2.http.Query("limit") Integer limit + ); + /** * Submit an Ergo transaction to unconfirmed pool to send it over the network * From daa5610abd746ab7cb35e4628488805714b5d690 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Thu, 3 Nov 2022 15:15:13 +0100 Subject: [PATCH 19/44] eip-31-babelfees: typo fixes --- .../ergoplatform/appkit/babelfee/BabelFeeBoxState.java | 10 +++++----- .../appkit/babelfee/BabelFeeOperations.java | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java index 1fe80993..6da07c7a 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java @@ -97,12 +97,12 @@ public long maxAmountToBuy() { } /** - * @param nanoergs amount we want to receive, usually to pay transaction fees - * @return the amount of tokens to sell to receive at least his amount of nanoergs + * @param nanoErgs amount we want to receive, usually to pay transaction fees + * @return the amount of tokens to sell to receive at least this amount of nanoErgs */ - public long tokensToSellForErgAmount(long nanoergs) { - long floorAmount = nanoergs / pricePerToken; - return (floorAmount * pricePerToken >= nanoergs) ? floorAmount : floorAmount + 1; + public long tokensToSellForErgAmount(long nanoErgs) { + long floorAmount = nanoErgs / pricePerToken; + return (floorAmount * pricePerToken >= nanoErgs) ? floorAmount : floorAmount + 1; } /** diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java index 1aa12253..742b44aa 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java @@ -40,7 +40,8 @@ public static UnsignedTransaction createNewBabelContractTx( ErgoId tokenId, long pricePerToken ) { - BabelFeeBoxState babelFeeBoxState = BabelFeeBoxState.newBuilder().withBoxCreator(boxOperations.getSenders().get(0)) + BabelFeeBoxState babelFeeBoxState = BabelFeeBoxState.newBuilder() + .withBoxCreator(boxOperations.getSenders().get(0)) .withPricePerToken(pricePerToken) .withTokenId(tokenId) .withValue(boxOperations.getAmountToSpend()) From 61b66579276cdf0d3bbca6f48c9f111fe377ad5c Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Thu, 3 Nov 2022 16:26:58 +0100 Subject: [PATCH 20/44] eip-31-babelfees: some formatting fixes --- .../ergoplatform/appkit/BabelFeeSpec.scala | 34 +++++++++++++------ .../appkit/babelfee/BabelFeeOperations.java | 2 +- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala index 53f83227..54e557b9 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala @@ -109,21 +109,27 @@ class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyC .contract(creator.toErgoContract) .build().convertToInputWith(mockTokenId, 0) - val txCreate = BabelFeeOperations.createNewBabelContractTx(BoxOperations.createForSender(creator, ctx) - .withAmountToSpend(amountToSend) - .withInputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))), + val txCreate = BabelFeeOperations.createNewBabelContractTx( + BoxOperations.createForSender(creator, ctx) + .withAmountToSpend(amountToSend) + .withInputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))), ErgoId.create(mockTokenId), - Parameters.OneErg); + Parameters.OneErg + ) ctx.newProverBuilder().build().reduce(txCreate, 0) val babelFeeErgoBox = txCreate.getOutputs.get(0).convertToInputWith(mockTokenId, 0) // now we cancel the babel box - val txCancel = BabelFeeOperations.cancelBabelFeeContract(BoxOperations.createForSender(creator, ctx) - .withInputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))), babelFeeErgoBox) + val txCancel = BabelFeeOperations.cancelBabelFeeContract( + BoxOperations.createForSender(creator, ctx) + .withInputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))), + babelFeeErgoBox) - ctx.newProverBuilder().withMnemonic(mnemonic, SecretString.empty(), false).build() + ctx.newProverBuilder() + .withMnemonic(mnemonic, SecretString.empty(), false) + .build() .sign(txCancel) } } @@ -145,15 +151,21 @@ class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyC val output = txB.outBoxBuilder .value(Parameters.MinChangeValue) .contract(sender.toErgoContract) - .tokens(new ErgoToken(ErgoId.create(mockTokenId), 1000 - babelFeeBoxState.tokensToSellForErgAmount(fee))) + .tokens(new ErgoToken( + ErgoId.create(mockTokenId), + 1000 - babelFeeBoxState.tokensToSellForErgAmount(fee))) .build() + val input = txB.outBoxBuilder .value(Parameters.MinChangeValue) .contract(sender.toErgoContract) .tokens(new ErgoToken(ErgoId.create(mockTokenId), 1000)) .build().convertToInputWith(mockTokenId, 0) - val babelTxB = BabelFeeOperations.getBabelFeeTransactionBuilder(txB, babelFeeBoxState.buildOutbox(txB, null).convertToInputWith(mockTokenId, 0), fee) + val babelTxB = BabelFeeOperations.getBabelFeeTransactionBuilder(txB, + babelFeeBoxState.buildOutbox(txB, null) + .convertToInputWith(mockTokenId, 0), + fee) val tx = babelTxB.fee(fee) .outputs(output) @@ -161,7 +173,9 @@ class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyC .sendChangeTo(sender) .build() - ctx.newProverBuilder().withMnemonic(mnemonic, SecretString.empty(), false).build() + ctx.newProverBuilder() + .withMnemonic(mnemonic, SecretString.empty(), false) + .build() .sign(tx) } } diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java index 742b44aa..e5df3eda 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java @@ -87,7 +87,7 @@ public static UnsignedTransaction cancelBabelFeeContract(BoxOperations boxOperat } /** - * Tries to fetch a babel fee box from blockchain data source with the given unspent boxes loader + * Tries to fetch a babel fee box for the given tokenId from blockchain data source using the given loader * * @param ctx current blockchain context * @param loader loader to receive unspent boxes From 5102a527506966dafa3bcdf390824b27daadf3b8 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Thu, 3 Nov 2022 19:32:06 +0100 Subject: [PATCH 21/44] EIP-0031 use ErgoTreeSerializer.DefaultSerializer.substituteConstants, remove contract as ergotree is now in EIP --- .../ergoplatform/appkit/BabelFeeSpec.scala | 86 +------------------ .../org/ergoplatform/appkit/JavaHelpers.scala | 7 ++ .../appkit/babelfee/BabelFeeBoxContract.java | 65 +++++--------- .../appkit/babelfee/BabelFeeBoxState.java | 5 +- .../appkit/babelfee/BabelFeeOperations.java | 3 +- 5 files changed, 35 insertions(+), 131 deletions(-) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala index 54e557b9..84263a42 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala @@ -1,6 +1,6 @@ package org.ergoplatform.appkit -import org.ergoplatform.appkit.babelfee.{BabelFeeBoxContract, BabelFeeBoxState, BabelFeeOperations} +import org.ergoplatform.appkit.babelfee.{BabelFeeBoxState, BabelFeeOperations} import org.scalatest.{Matchers, PropSpec} import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @@ -11,92 +11,8 @@ class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyC with HttpClientTesting with AppkitTestingCommon { - private val script = - """{ - | - | // ===== Contract Information ===== // - | // Name: EIP-0031 Babel Fees Contract - | // Description: Contract guarding the babel fee box, checking if valid output babel box was recreated and the token exchange was valid. - | // Version: 1.0.0 - | - | // ===== Relevant Variables ===== // - | val babelFeeBoxCreator: SigmaProp = SELF.R4[SigmaProp].get - | val ergPricePerToken: Long = SELF.R5[Long].get - | val tokenId: Coll[Byte] = _tokenId - | val recreatedBabelBoxIndex: Option[Int] = getVar[Int](0) - | - | // ===== Perform Babel Fee Swap ===== // - | if (recreatedBabelBoxIndex.isDefined) { - | - | // Check conditions for a valid babel fee swap - | val validBabelFeeSwap: Boolean = { - | - | // Output babel fee box - | val recreatedBabelBox: Box = OUTPUTS(recreatedBabelBoxIndex.get) - | - | // Check that the babel fee box is recreated correctly - | val validBabelFeeBoxRecreation: Boolean = - | - | allOf(Coll( - | (recreatedBabelBox.propositionBytes == SELF.propositionBytes), - | (recreatedBabelBox.tokens(0)._1 == tokenId), - | (recreatedBabelBox.R4[SigmaProp].get == babelFeeBoxCreator), - | (recreatedBabelBox.R5[Long].get == ergPricePerToken), - | (recreatedBabelBox.R6[Coll[Byte]].get == SELF.id) - | )) - | - | - | - | // Check that the user's token was exchanged correctly - | val validBabelFeeExchange: Boolean = { - | - | val nanoErgsDifference: Long = SELF.value - recreatedBabelBox.value - | val babelTokensBefore: Long = if (SELF.tokens.size > 0) SELF.tokens(0)._2 else 0L - | val babelTokensDifference: Long = recreatedBabelBox.tokens(0)._2 - babelTokensBefore - | - | allOf(Coll( - | (babelTokensDifference * ergPricePerToken >= nanoErgsDifference), - | (nanoErgsDifference >= 0) - | )) - | - | } - | - | allOf(Coll( - | validBabelFeeBoxRecreation, - | validBabelFeeExchange - | )) - | - | } - | - | sigmaProp(validBabelFeeSwap) - | - | } else { - | - | // ===== Perform Babel Fee Box Withdrawl ===== // - | babelFeeBoxCreator - | - | } - | - |}""".stripMargin - val mockTokenId = "f9e5ce5aa0d95f5d54a7bc89c46730d9662397067250aa18a0039631c0f5b809" - property("Compile contract for certain token") { - val ergoClient = new ColdErgoClient(NetworkType.MAINNET, 0) - val contract = ergoClient.execute { ctx: BlockchainContext => - ctx.compileContract( - ConstantsBuilder.create() - .item("_tokenId", ErgoId.create(mockTokenId).getBytes) - .build(), - script - ) - } - - val contract2 = new BabelFeeBoxContract().getContractForToken(ErgoId.create(mockTokenId), NetworkType.MAINNET) - contract.getErgoTree shouldBe contract2.getErgoTree - } - - property("babel fee box creation and revoke") { val ergoClient = createMockedErgoClient(MockData(Nil, Nil)) ergoClient.execute { ctx: BlockchainContext => diff --git a/common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala b/common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala index d77e11dc..9bcbe71e 100644 --- a/common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala +++ b/common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala @@ -41,6 +41,7 @@ import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator import org.bouncycastle.crypto.params.KeyParameter import org.ergoplatform.appkit.JavaHelpers.{TokenColl, TokenIdRType} import sigmastate.eval.Colls.outerJoin +import sigmastate.eval.CostingSigmaDslBuilder.validationSettings import special.collection.ExtensionMethods.PairCollOps /** Type-class of isomorphisms between types. @@ -327,6 +328,12 @@ object JavaHelpers { ErgoTreeSerializer.DefaultSerializer.deserializeErgoTree(Base16.decode(base16).get) } + def substituteErgoTreeConstants(ergoTreeBytes: Array[Byte], positions: Array[Int], newValues: Array[ErgoValue[_]]): ErgoTree = { + val newBytes = ErgoTreeSerializer.DefaultSerializer.substituteConstants( + ergoTreeBytes, positions, newValues.map(Iso.isoErgoValueToSValue.to)) + ErgoTreeSerializer.DefaultSerializer.deserializeErgoTree(newBytes._1) + } + def createP2PKAddress(pk: ProveDlog, networkPrefix: NetworkPrefix): P2PKAddress = { implicit val ergoAddressEncoder: ErgoAddressEncoder = ErgoAddressEncoder(networkPrefix) P2PKAddress(pk) diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java index c3c6e7a9..20e04fc2 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java @@ -1,62 +1,41 @@ package org.ergoplatform.appkit.babelfee; -import org.ergoplatform.appkit.ErgoContract; import org.ergoplatform.appkit.ErgoId; -import org.ergoplatform.appkit.NetworkType; -import org.ergoplatform.appkit.impl.ErgoTreeContract; - -import java.nio.ByteBuffer; -import java.util.Arrays; +import org.ergoplatform.appkit.ErgoValue; +import org.ergoplatform.appkit.JavaHelpers; +import org.ergoplatform.appkit.ScalaHelpers; import scorex.util.encode.Base16; import sigmastate.Values; -import sigmastate.serialization.ErgoTreeSerializer; +import special.collection.Coll; public class BabelFeeBoxContract { - static final String contractTemplateHexPref = "100604000e20"; - static final String contractTemplateHexSuf = "0400040005000500d803d601e30004d602e4c6a70408d603e4c6a7050595e67201d804d604b2a5e4720100d605b2db63087204730000d606db6308a7d60799c1a7c17204d1968302019683050193c27204c2a7938c720501730193e4c672040408720293e4c672040505720393e4c67204060ec5a796830201929c998c7205029591b1720673028cb272067303000273047203720792720773057202"; - - private static byte[] contractPref; - private static byte[] contractSuf; + private static final String contractTemplateHex = "100604000e000400040005000500d803d601e30004d602e4c6a70408d603e4c6a7050595e67201d804d604b2a5e4720100d605b2db63087204730000d606db6308a7d60799c1a7c17204d1968302019683050193c27204c2a7938c720501730193e4c672040408720293e4c672040505720393e4c67204060ec5a796830201929c998c7205029591b1720673028cb272067303000273047203720792720773057202"; + private static final byte[] contractTemplate; - private static void prepare() { - if (contractPref == null || contractSuf == null) { - contractPref = Base16.decode(contractTemplateHexPref).get(); - contractSuf = Base16.decode(contractTemplateHexSuf).get(); - } + static { + contractTemplate = Base16.decode(contractTemplateHex).get(); } - public ErgoContract getContractForToken(ErgoId tokenId, NetworkType networkType) { - prepare(); + private final ErgoId tokenId; + private final Values.ErgoTree ergoTree; + public BabelFeeBoxContract(ErgoId tokenId) { + this.tokenId = tokenId; byte[] idBytes = tokenId.getBytes(); - - byte[] completeContract = ByteBuffer.allocate(contractPref.length + contractSuf.length + idBytes.length) - .put(contractPref) - .put(idBytes) - .put(contractSuf) - .array(); - - return new ErgoTreeContract(ErgoTreeSerializer.DefaultSerializer().deserializeErgoTree(completeContract), networkType); + ergoTree = JavaHelpers.substituteErgoTreeConstants(contractTemplate, new int[]{1}, new ErgoValue[]{ErgoValue.of(idBytes)}); } - public ErgoId getTokenIdFromErgoTree(Values.ErgoTree ergoTree) { - prepare(); - - byte[] treeBytes = ergoTree.bytes(); - - ByteBuffer bb = ByteBuffer.wrap(treeBytes); - - byte[] thisPref = new byte[contractPref.length]; - byte[] thisSuf = new byte[contractSuf.length]; - byte[] tokenId = new byte[treeBytes.length - contractPref.length - contractSuf.length]; - bb.get(thisPref, 0, thisPref.length); - bb.get(tokenId, 0, tokenId.length); - bb.get(thisSuf, 0, thisSuf.length); + public BabelFeeBoxContract(Values.ErgoTree ergoTree) { + this.ergoTree = ergoTree; + tokenId = new ErgoId(ScalaHelpers.collByteToByteArray((Coll) ergoTree.constants().apply(1).value())); + } - if (!Arrays.equals(thisPref, contractPref) || !Arrays.equals(thisSuf, contractSuf)) - throw new IllegalArgumentException("Contract does not fit template"); + public Values.ErgoTree getErgoTree() { + return ergoTree; + } - return new ErgoId(tokenId); + public ErgoId getTokenId() { + return tokenId; } } diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java index 6da07c7a..c4c3e77b 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java @@ -10,6 +10,7 @@ import org.ergoplatform.appkit.SigmaProp; import org.ergoplatform.appkit.TransactionBox; import org.ergoplatform.appkit.UnsignedTransactionBuilder; +import org.ergoplatform.appkit.impl.ErgoTreeContract; import java.util.List; @@ -31,7 +32,7 @@ public BabelFeeBoxState(TransactionBox ergoBox) { List> registers = ergoBox.getRegisters(); boxCreator = new SigmaProp((special.sigma.SigmaProp) registers.get(0).getValue()); pricePerToken = (Long) registers.get(1).getValue(); - this.tokenId = new BabelFeeBoxContract().getTokenIdFromErgoTree(ergoBox.getErgoTree()); + this.tokenId = new BabelFeeBoxContract(ergoBox.getErgoTree()).getTokenId(); if (!ergoBox.getTokens().isEmpty()) { ErgoToken ergoToken = ergoBox.getTokens().get(0); @@ -128,7 +129,7 @@ public BabelFeeBoxState buildSucceedingState(long tokenAmountChange) { */ public OutBox buildOutbox(UnsignedTransactionBuilder txBuilder, @Nullable InputBox babelBox) { OutBoxBuilder outBoxBuilder = txBuilder.outBoxBuilder() - .contract(new BabelFeeBoxContract().getContractForToken(tokenId, txBuilder.getNetworkType())) + .contract(new ErgoTreeContract(new BabelFeeBoxContract(tokenId).getErgoTree(), txBuilder.getNetworkType())) .value(value) .registers(ErgoValue.of(boxCreator), ErgoValue.of(pricePerToken), ErgoValue.of(babelBox != null ? babelBox.getId().getBytes() : new byte[0])); diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java index e5df3eda..72cbe939 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java @@ -17,6 +17,7 @@ import org.ergoplatform.appkit.PreHeader; import org.ergoplatform.appkit.UnsignedTransaction; import org.ergoplatform.appkit.UnsignedTransactionBuilder; +import org.ergoplatform.appkit.impl.ErgoTreeContract; import java.util.ArrayList; import java.util.Collections; @@ -97,7 +98,7 @@ public static UnsignedTransaction cancelBabelFeeContract(BoxOperations boxOperat */ @Nullable public static InputBox findBabelFeeBox(BlockchainContext ctx, BoxOperations.IUnspentBoxesLoader loader, ErgoId tokenId, long feeAmount) { - ErgoContract contractForToken = new BabelFeeBoxContract().getContractForToken(tokenId, ctx.getNetworkType()); + ErgoContract contractForToken = new ErgoTreeContract(new BabelFeeBoxContract(tokenId).getErgoTree(), ctx.getNetworkType()); Address address = contractForToken.toAddress(); loader.prepare(ctx, Collections.singletonList(address), feeAmount, new ArrayList<>()); From 31f229faab6123f84909dab77d1688b7d6543ce5 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Thu, 3 Nov 2022 19:38:10 +0100 Subject: [PATCH 22/44] Improve exception message Co-authored-by: Alexander Slesarenko --- .../java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java index c4c3e77b..2df042b1 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java @@ -39,7 +39,7 @@ public BabelFeeBoxState(TransactionBox ergoBox) { tokenAmount = ergoToken.getValue(); if (!ergoToken.getId().equals(tokenId)) { - throw new IllegalStateException("token id of contract and token id in box diverge"); + throw new IllegalStateException("Token id of contract and token id in babel box should be equial."); } } else { tokenAmount = 0; From 77b709625a40d5d4cc4a706d40fc1b942c5a2798 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Thu, 3 Nov 2022 20:18:56 +0100 Subject: [PATCH 23/44] EIP-0031 test for findBabelFeeBox, documentation changes, improve BabelFeeOperations#findBabelFeeBox --- .../ergoplatform/appkit/BabelFeeSpec.scala | 36 +++++++++++++++- .../ergoplatform/appkit/TransactionBox.java | 2 +- .../appkit/babelfee/BabelFeeBoxState.java | 38 ++++++++++++----- ...lder.java => BabelFeeBoxStateBuilder.java} | 41 +++++++++++++++---- .../appkit/babelfee/BabelFeeOperations.java | 18 +++++--- 5 files changed, 108 insertions(+), 27 deletions(-) rename lib-api/src/main/java/org/ergoplatform/appkit/babelfee/{BabelFeeBoxBuilder.java => BabelFeeBoxStateBuilder.java} (55%) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala index 84263a42..f3f59b74 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala @@ -69,7 +69,7 @@ class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyC .contract(sender.toErgoContract) .tokens(new ErgoToken( ErgoId.create(mockTokenId), - 1000 - babelFeeBoxState.tokensToSellForErgAmount(fee))) + 1000 - babelFeeBoxState.calcTokensToSellForErgAmount(fee))) .build() val input = txB.outBoxBuilder @@ -95,4 +95,38 @@ class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyC .sign(tx) } } + + property("fetch babel fee boxes") { + val ergoClient = createMockedErgoClient(MockData(Nil, Nil)) + ergoClient.execute { ctx: BlockchainContext => + val creator = address + + val tockenId = ErgoId.create(mockTokenId) + + // find no boxes + val babelBox1 = BabelFeeOperations.findBabelFeeBox(ctx, new MockedBoxesLoader(new util.ArrayList[InputBox]()), + tockenId, Parameters.MinFee) + + babelBox1 shouldBe(null) + + val inputBabelBox = BabelFeeBoxState.newBuilder() + .withValue(Parameters.OneErg) + .withTokenId(tockenId) + .withPricePerToken(Parameters.MinFee) + .withBoxCreator(creator) + .build().buildOutbox(ctx.newTxBuilder(), null) + .convertToInputWith(mockTokenId, 0) + + val babelBox2 = BabelFeeOperations.findBabelFeeBox(ctx, new MockedBoxesLoader(util.Arrays.asList(inputBabelBox)), + tockenId, Parameters.MinFee) + + babelBox2 shouldBe inputBabelBox + + // the amount needed (2 ERG) is more than inputBabelBox can offer, so it is discarded + val babelBox3 = BabelFeeOperations.findBabelFeeBox(ctx, new MockedBoxesLoader(util.Arrays.asList(inputBabelBox)), + tockenId, Parameters.OneErg * 2) + + babelBox3 shouldBe(null) + } + } } diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/TransactionBox.java b/lib-api/src/main/java/org/ergoplatform/appkit/TransactionBox.java index c3c8bad7..b085cbbe 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/TransactionBox.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/TransactionBox.java @@ -12,7 +12,7 @@ */ public interface TransactionBox { /** - * Returns the ERG value stored in this box, i.e. unspent value in UTXO. + * Returns the nanoERG value stored in this box, i.e. unspent value in UTXO. */ long getValue(); diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java index 2df042b1..6ca92047 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java @@ -18,6 +18,11 @@ /** * Represents a Babel Fee Box state, see EIP-0031 + * https://github.com/ergoplatform/eips/blob/master/eip-0031.md + *

+ * The term “babel fees“ refers to the concept of paying transaction fees in tokens instead of + * platform’s primary token (ERG). It is a contract that buys tokens and pays ERG, suitable to be + * used in any transaction. */ public class BabelFeeBoxState { @@ -55,7 +60,7 @@ public BabelFeeBoxState(TransactionBox ergoBox) { } /** - * @return price offered per raw token amount + * @return nanoErg amount offered per raw token amount */ public long getPricePerToken() { return pricePerToken; @@ -76,13 +81,20 @@ public SigmaProp getBoxCreator() { } /** - * @return overall ERG value in the box. not all is available to change for token as a small - * amount must remain in the box + * @return overall nanoErg value in the box. Not all is available to change for tokens as a + * small amount must remain in the successor box */ public long getValue() { return value; } + /** + * @return raw amount of tokens already collected in the box + */ + public long getTokenAmount() { + return tokenAmount; + } + /** * @return overall ERG value available to change for token */ @@ -91,9 +103,9 @@ public long getValueAvailableToBuy() { } /** - * @return max token amount possible to swap at best price + * @return max token raw amount possible to swap at best price */ - public long maxAmountToBuy() { + public long getMaxTokenAmountToBuy() { return getValueAvailableToBuy() / pricePerToken; } @@ -101,7 +113,7 @@ public long maxAmountToBuy() { * @param nanoErgs amount we want to receive, usually to pay transaction fees * @return the amount of tokens to sell to receive at least this amount of nanoErgs */ - public long tokensToSellForErgAmount(long nanoErgs) { + public long calcTokensToSellForErgAmount(long nanoErgs) { long floorAmount = nanoErgs / pricePerToken; return (floorAmount * pricePerToken >= nanoErgs) ? floorAmount : floorAmount + 1; } @@ -116,7 +128,7 @@ public long tokensToSellForErgAmount(long nanoErgs) { public BabelFeeBoxState buildSucceedingState(long tokenAmountChange) { if (tokenAmountChange <= 0) throw new IllegalArgumentException("tokenAmountChange must be greater than 0"); - if (tokenAmountChange > maxAmountToBuy()) + if (tokenAmountChange > getMaxTokenAmountToBuy()) throw new IllegalArgumentException("tokenAmountChange must be less or equal maxAmountToBuy"); return new BabelFeeBoxState(pricePerToken, tokenId, boxCreator, @@ -125,13 +137,17 @@ public BabelFeeBoxState buildSucceedingState(long tokenAmountChange) { } /** + * @param txBuilder txBuilder to build the new outbox with + * @param precedingBabelBox if this is not the initial babel fee box, preceeding babel fee box + * must be given to set registers correct * @return outbox representing this babel fee box */ - public OutBox buildOutbox(UnsignedTransactionBuilder txBuilder, @Nullable InputBox babelBox) { + public OutBox buildOutbox(UnsignedTransactionBuilder txBuilder, @Nullable InputBox precedingBabelBox) { OutBoxBuilder outBoxBuilder = txBuilder.outBoxBuilder() .contract(new ErgoTreeContract(new BabelFeeBoxContract(tokenId).getErgoTree(), txBuilder.getNetworkType())) .value(value) - .registers(ErgoValue.of(boxCreator), ErgoValue.of(pricePerToken), ErgoValue.of(babelBox != null ? babelBox.getId().getBytes() : new byte[0])); + .registers(ErgoValue.of(boxCreator), ErgoValue.of(pricePerToken), + ErgoValue.of(precedingBabelBox != null ? precedingBabelBox.getId().getBytes() : new byte[0])); if (tokenAmount > 0) outBoxBuilder.tokens(new ErgoToken(tokenId, tokenAmount)); @@ -139,7 +155,7 @@ public OutBox buildOutbox(UnsignedTransactionBuilder txBuilder, @Nullable InputB return outBoxBuilder.build(); } - public static BabelFeeBoxBuilder newBuilder() { - return new BabelFeeBoxBuilder(); + public static BabelFeeBoxStateBuilder newBuilder() { + return new BabelFeeBoxStateBuilder(); } } diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxBuilder.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxStateBuilder.java similarity index 55% rename from lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxBuilder.java rename to lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxStateBuilder.java index f9b03dd7..bb4ea0b8 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxBuilder.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxStateBuilder.java @@ -6,45 +6,70 @@ import java.util.Objects; -public class BabelFeeBoxBuilder { +/** + * Builder class to conveniently instantiate a {@link BabelFeeBoxState} with self-defined + * information. + */ +public class BabelFeeBoxStateBuilder { private long pricePerToken; private ErgoId tokenId; private SigmaProp boxCreator; private long value; private long tokenAmount; - public BabelFeeBoxBuilder withPricePerToken(long pricePerToken) { + /** + * see {@link BabelFeeBoxState#getPricePerToken()} + */ + public BabelFeeBoxStateBuilder withPricePerToken(long pricePerToken) { this.pricePerToken = pricePerToken; return this; } - public BabelFeeBoxBuilder withTokenId(ErgoId tokenId) { + /** + * see {@link BabelFeeBoxState#getTokenId()} + */ + public BabelFeeBoxStateBuilder withTokenId(ErgoId tokenId) { this.tokenId = tokenId; return this; } - public BabelFeeBoxBuilder withBoxCreator(SigmaProp boxCreator) { + /** + * see {@link BabelFeeBoxState#getBoxCreator()} + */ + public BabelFeeBoxStateBuilder withBoxCreator(SigmaProp boxCreator) { this.boxCreator = boxCreator; return this; } - public BabelFeeBoxBuilder withBoxCreator(Address address) { + /** + * see {@link BabelFeeBoxState#getBoxCreator()} + */ + public BabelFeeBoxStateBuilder withBoxCreator(Address address) { this.boxCreator = SigmaProp.createFromAddress(address); return this; } - public BabelFeeBoxBuilder withValue(long value) { - this.value = value; + /** + * see {@link BabelFeeBoxState#getValue()} + */ + public BabelFeeBoxStateBuilder withValue(long nanoErgValue) { + this.value = nanoErgValue; return this; } - public BabelFeeBoxBuilder withTokenAmount(long tokenAmount) { + /** + * see {@link BabelFeeBoxState#getTokenAmount()} + */ + public BabelFeeBoxStateBuilder withTokenAmount(long tokenAmount) { if (tokenAmount < 0) throw new IllegalArgumentException("pricePerToken must be equal or greater than 0"); this.tokenAmount = tokenAmount; return this; } + /** + * @return Babel fee box state built with the given data + */ public BabelFeeBoxState build() { Objects.requireNonNull(boxCreator, "Box creator not set"); Objects.requireNonNull(tokenId, "Token ID not set"); diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java index 72cbe939..a57227a2 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java @@ -88,12 +88,17 @@ public static UnsignedTransaction cancelBabelFeeContract(BoxOperations boxOperat } /** - * Tries to fetch a babel fee box for the given tokenId from blockchain data source using the given loader + * Tries to fetch a babel fee box for the given tokenId from blockchain data source using the + * given loader. + * The box returned is in general the first box satisfying the given fee amount that is returned + * by the loader. Under certain circumstances, a babel fee box with a better price than the + * first one is returned, but a best price is not guaranteed. Clients should implement an own + * logic to retrieve babel fee boxes if needed. * * @param ctx current blockchain context * @param loader loader to receive unspent boxes * @param tokenId tokenId offered to swap - * @param feeAmount nanoerg amount needed to swap + * @param feeAmount nanoErg amount needed to swap * @return babel fee box satisfying the needs, or null if none available */ @Nullable @@ -103,18 +108,19 @@ public static InputBox findBabelFeeBox(BlockchainContext ctx, BoxOperations.IUns loader.prepare(ctx, Collections.singletonList(address), feeAmount, new ArrayList<>()); int page = 0; - List inputBoxes = loader.loadBoxesPage(ctx, address, 0); + List inputBoxes = null; InputBox returnBox = null; long pricePerToken = Long.MAX_VALUE; - while (!inputBoxes.isEmpty() && returnBox == null) { + while ((page == 0 || !inputBoxes.isEmpty()) && returnBox == null) { + inputBoxes = loader.loadBoxesPage(ctx, address, 0); // find the cheapest box satisfying our fee amount needs for (InputBox inputBox : inputBoxes) { try { BabelFeeBoxState babelFeeBoxState = new BabelFeeBoxState(inputBox); - if (babelFeeBoxState.getValueAvailableToBuy() > feeAmount && babelFeeBoxState.getPricePerToken() < pricePerToken) + if (babelFeeBoxState.getValueAvailableToBuy() >= feeAmount && babelFeeBoxState.getPricePerToken() < pricePerToken) returnBox = inputBox; } catch (Throwable t) { // ignore, check next @@ -160,7 +166,7 @@ private BabelFeeTransactionBuilder(UnsignedTransactionBuilder superBuilder, Inpu this.superBuilder = superBuilder; inputBabelBox = babelBox; BabelFeeBoxState babelBoxState = new BabelFeeBoxState(babelBox); - BabelFeeBoxState outBabelBoxState = babelBoxState.buildSucceedingState(babelBoxState.tokensToSellForErgAmount(nanoErgToCover)); + BabelFeeBoxState outBabelBoxState = babelBoxState.buildSucceedingState(babelBoxState.calcTokensToSellForErgAmount(nanoErgToCover)); outBabelBox = outBabelBoxState.buildOutbox(superBuilder, babelBox); } From 406ceea7e38f4ff3359fb63a2456ca7a5450c67f Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Fri, 4 Nov 2022 16:37:17 +0100 Subject: [PATCH 24/44] UnsignedTransactionBuilder add methods addInputs(), addOutputs(), addDataInputs(), inputs() --- .../ergoplatform/appkit/TxBuilderSpec.scala | 6 +- .../appkit/examples/DataInputsSpec.scala | 8 +- .../appkit/UnsignedTransactionBuilder.java | 30 +++++++ .../impl/UnsignedTransactionBuilderImpl.scala | 81 ++++++++++++------- 4 files changed, 86 insertions(+), 39 deletions(-) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala index df686ed0..7ae51b27 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala @@ -161,8 +161,8 @@ class TxBuilderSpec extends PropSpec with Matchers .build() val changeAddr = Address.fromErgoTree(input.getErgoTree, NetworkType.MAINNET).getErgoAddress - val unsigned = txB.boxesToSpend(Arrays.asList(input)) - .outputs(output, feeOut) + val unsigned = txB.inputs(input) + .outputs(output).addOutputs(feeOut) .sendChangeTo(changeAddr) .build() val prover = ctx.newProverBuilder().build() @@ -434,7 +434,7 @@ class TxBuilderSpec extends PropSpec with Matchers .contract(pkContract) .build().convertToInputWith(mockTxId, 1) - val tx = ctx.newTxBuilder().boxesToSpend(util.Arrays.asList(input1, input2)) + val tx = ctx.newTxBuilder().inputs(input1).addInputs(input2) .outputs(ctx.newTxBuilder().outBoxBuilder().contract(pkContract).value(amountToSend).build()) .sendChangeTo(recipient.getErgoAddress) .fee(Parameters.MinFee) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/examples/DataInputsSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/examples/DataInputsSpec.scala index b6e03ca3..dc0f8b49 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/examples/DataInputsSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/examples/DataInputsSpec.scala @@ -77,15 +77,13 @@ class DataInputsSpec extends PropSpec with Matchers .contract(truePropContract(ctx)).build() val inputs = new java.util.ArrayList[InputBox]() - val dataInputs = new java.util.ArrayList[InputBox]() inputs.add(input) - dataInputs.add(dataInput) val ergoTree = JavaHelpers.decodeStringToErgoTree(dummyErgoTree) val changeAddr = Address.fromErgoTree(ergoTree, NetworkType.MAINNET).getErgoAddress - val unsigned = txB.boxesToSpend(inputs).outputs(dummyOutput).withDataInputs(dataInputs).fee(10000000).sendChangeTo(changeAddr).build() + val unsigned = txB.boxesToSpend(inputs).outputs(dummyOutput).withDataInputs(dataInput).fee(10000000).sendChangeTo(changeAddr).build() an[Exception] shouldBe thrownBy { ctx.newProverBuilder().build().sign(unsigned) @@ -115,15 +113,13 @@ class DataInputsSpec extends PropSpec with Matchers .contract(truePropContract(ctx)).build() val inputs = new java.util.ArrayList[InputBox]() - val dataInputs = new java.util.ArrayList[InputBox]() inputs.add(input) - dataInputs.add(dataInput) val ergoTree = JavaHelpers.decodeStringToErgoTree(dummyErgoTree) val changeAddr = Address.fromErgoTree(ergoTree, NetworkType.MAINNET).getErgoAddress - val unsigned = txB.boxesToSpend(inputs).outputs(dummyOutput).withDataInputs(dataInputs).fee(10000000).sendChangeTo(changeAddr).build() + val unsigned = txB.boxesToSpend(inputs).outputs(dummyOutput).addDataInputs(dataInput).fee(10000000).sendChangeTo(changeAddr).build() ctx.newProverBuilder().build().sign(unsigned) } 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 f02e0069..5f1eff6b 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/UnsignedTransactionBuilder.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/UnsignedTransactionBuilder.java @@ -30,6 +30,18 @@ public interface UnsignedTransactionBuilder { * as {@link OutBox} and then {@link OutBox#convertToInputWith(String, short) converted} to * {@link InputBox}. */ + UnsignedTransactionBuilder inputs(InputBox... boxes); + + /** + * Adds input boxes to an already specified list of inputs or, if no input boxes defined yet, + * as the boxes to spent. The order is preserved. + */ + UnsignedTransactionBuilder addInputs(InputBox... boxes); + + /** + * @deprecated use {@link #inputs(InputBox...)} + */ + @Deprecated UnsignedTransactionBuilder boxesToSpend(List boxes); /** @@ -41,6 +53,18 @@ public interface UnsignedTransactionBuilder { * as {@link OutBox} and then {@link OutBox#convertToInputWith(String, short) converted} to * {@link InputBox}. */ + UnsignedTransactionBuilder withDataInputs(InputBox... boxes); + + /** + * Adds input boxes to an already specified list of data inputs or, if no data input boxes + * defined yet, set the boxes as the data input boxes to be used. The order is preserved. + */ + UnsignedTransactionBuilder addDataInputs(InputBox... boxes); + + /** + * @deprecated use {@link #withDataInputs(InputBox...)} + */ + @Deprecated UnsignedTransactionBuilder withDataInputs(List boxes); /** @@ -54,6 +78,12 @@ public interface UnsignedTransactionBuilder { */ UnsignedTransactionBuilder outputs(OutBox... outputs); + /** + * Adds output boxes to an already specified list of outputs or, if no output boxes defined yet, + * as the boxes to be outputted. The order is preserved. + */ + UnsignedTransactionBuilder addOutputs(OutBox... outBoxes); + /** * Adds transaction fee output. * diff --git a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala index 769fa615..82844bbe 100644 --- a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala +++ b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala @@ -13,15 +13,13 @@ import special.sigma.Header import java.util import java.util._ -import java.util.stream.Collectors import scala.collection.JavaConversions +import scala.collection.JavaConversions.iterableAsScalaIterable class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends UnsignedTransactionBuilder { - private[impl] var _inputs: List[UnsignedInput] = _ - private var _inputBoxes: Option[List[InputBoxImpl]] = None + private[impl] var _inputs: List[InputBoxImpl] = new ArrayList[InputBoxImpl]() - private[impl] var _dataInputs: List[DataInput] = new ArrayList[DataInput]() - private var _dataInputBoxes: Option[List[InputBoxImpl]] = None + private[impl] var _dataInputs: List[InputBoxImpl] = new ArrayList[InputBoxImpl]() private[impl] var _outputCandidates: Option[List[ErgoBoxCandidate]] = None private var _tokensToBurn: Option[List[ErgoToken]] = None @@ -35,29 +33,56 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un this } - override def boxesToSpend(inputBoxes: List[InputBox]): UnsignedTransactionBuilder = { - require(_inputBoxes.isEmpty, "boxesToSpend list is already specified") - _inputs = inputBoxes - .map(box => JavaHelpers.createUnsignedInput(box.getId.getBytes)) - _inputBoxes = Some(inputBoxes.map(b => b.asInstanceOf[InputBoxImpl])) + override def addInputs(boxes: InputBox*): UnsignedTransactionBuilder = { + _inputs.addAll(boxes + .map(b => b.asInstanceOf[InputBoxImpl]) + .toIndexedSeq.asInstanceOf[IndexedSeq[InputBoxImpl]] + .convertTo[util.List[InputBoxImpl]]) this } - override def withDataInputs(inputBoxes: List[InputBox]): UnsignedTransactionBuilder = { - require(_dataInputBoxes.isEmpty, "dataInputs list is already specified") - _dataInputs = inputBoxes - .map(box => JavaHelpers.createDataInput(box.getId.getBytes)) - _dataInputBoxes = Some(inputBoxes.map(_.asInstanceOf[InputBoxImpl])) + override def inputs(boxes: InputBox*): UnsignedTransactionBuilder = { + require(_inputs.isEmpty, "inputs already specified") + addInputs(boxes: _*) this } - override def outputs(outputs: OutBox*): UnsignedTransactionBuilder = { - require(_outputCandidates.isEmpty, "Outputs already specified.") - val candidates = outputs + override def boxesToSpend(inputBoxes: List[InputBox]): UnsignedTransactionBuilder = + inputs(inputBoxes.toSeq: _*) + + override def addDataInputs(boxes: InputBox*): UnsignedTransactionBuilder = { + _dataInputs.addAll(boxes + .toIndexedSeq.asInstanceOf[IndexedSeq[InputBoxImpl]] + .convertTo[util.List[InputBoxImpl]]) + this + } + + override def withDataInputs(boxes: InputBox*): UnsignedTransactionBuilder = { + require(_dataInputs.isEmpty, "dataInputs list is already specified") + addDataInputs(boxes: _*) + this + } + + override def withDataInputs(inputBoxes: List[InputBox]): UnsignedTransactionBuilder = + withDataInputs(inputBoxes.toSeq: _*) + + override def addOutputs(outBoxes: OutBox*): UnsignedTransactionBuilder = { + val candidates = outBoxes .map(c => c.asInstanceOf[OutBoxImpl].getErgoBoxCandidate) .toIndexedSeq.asInstanceOf[IndexedSeq[ErgoBoxCandidate]] .convertTo[List[ErgoBoxCandidate]] - _outputCandidates = Some(candidates) + + if (_outputCandidates.isEmpty) + _outputCandidates = Some(candidates) + else + _outputCandidates.get.addAll(candidates) + + this + } + + override def outputs(outputs: OutBox*): UnsignedTransactionBuilder = { + require(_outputCandidates.isEmpty, "Outputs already specified.") + addOutputs(outputs: _*) this } @@ -71,7 +96,7 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un require(_tokensToBurn.isEmpty, "Tokens to burn already specified.") _tokensToBurn = Some({ val res = new util.ArrayList[ErgoToken]() - Collections.addAll(res, tokens:_*) + Collections.addAll(res, tokens: _*) res }) this @@ -87,7 +112,7 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un list match { case Some(list) if !list.isEmpty => list case _ => - throw new IllegalArgumentException("requirement failed: "+ msg) + throw new IllegalArgumentException("requirement failed: " + msg) } } @@ -95,19 +120,18 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un opt match { case Some(x) => x case _ => - throw new IllegalArgumentException("requirement failed: "+ msg) + throw new IllegalArgumentException("requirement failed: " + msg) } } override def build: UnsignedTransaction = { - val inputBoxes = getInputBoxesImpl + val inputBoxes = _inputs val outputCandidates = getNonEmpty(_outputCandidates, "Output boxes are not specified") val boxesToSpend = inputBoxes .map(b => ExtendedInputBox(b.getErgoBox, b.getExtension)) - val dataInputBoxes = _dataInputBoxes - .getOrElse(new util.ArrayList[InputBoxImpl]()) - .map(b => b.getErgoBox) + val dataInputBoxes = _dataInputs.map(b => b.getErgoBox) val dataInputs = JavaHelpers.toIndexedSeq(_dataInputs) + .map(box => JavaHelpers.createDataInput(box.getId.getBytes)) require(_feeAmount.isEmpty || _feeAmount.get >= MinFee, s"When fee amount is defined it should be >= $MinFee, got ${_feeAmount.get}") val changeAddress = getDefined(_changeAddress, "Change address is not defined") @@ -169,10 +193,7 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un override def getNetworkType: NetworkType = _ctx.getNetworkType - private def getInputBoxesImpl: List[InputBoxImpl] = - getNonEmpty(_inputBoxes, "Input boxes are not specified") - override def getInputBoxes: List[InputBox] = - getInputBoxesImpl.stream.collect(Collectors.toList[InputBox]) + _inputs.map(b => b.asInstanceOf[InputBox]) } From 7648164723574c9c81d867033a080915d5e35af1 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Sat, 5 Nov 2022 11:48:34 +0100 Subject: [PATCH 25/44] UnsignedTransactionBuilder add method getOutputBoxes --- .../appkit/UnsignedTransactionBuilder.java | 5 ++++ .../impl/UnsignedTransactionBuilderImpl.scala | 25 ++++++++----------- 2 files changed, 16 insertions(+), 14 deletions(-) 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 5f1eff6b..f0e30749 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/UnsignedTransactionBuilder.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/UnsignedTransactionBuilder.java @@ -145,4 +145,9 @@ public interface UnsignedTransactionBuilder { * Returns all input boxes attached to this builder. */ List getInputBoxes(); + + /** + * Returns all output boxes attached to this builder. + */ + List getOutputBoxes(); } diff --git a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala index 82844bbe..fea8b339 100644 --- a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala +++ b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala @@ -18,10 +18,9 @@ import scala.collection.JavaConversions.iterableAsScalaIterable class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends UnsignedTransactionBuilder { private[impl] var _inputs: List[InputBoxImpl] = new ArrayList[InputBoxImpl]() - + private[impl] var _outputs: List[OutBoxImpl] = new ArrayList[OutBoxImpl]() private[impl] var _dataInputs: List[InputBoxImpl] = new ArrayList[InputBoxImpl]() - private[impl] var _outputCandidates: Option[List[ErgoBoxCandidate]] = None private var _tokensToBurn: Option[List[ErgoToken]] = None private var _feeAmount: Option[Long] = None private var _changeAddress: Option[ErgoAddress] = None @@ -67,21 +66,15 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un withDataInputs(inputBoxes.toSeq: _*) override def addOutputs(outBoxes: OutBox*): UnsignedTransactionBuilder = { - val candidates = outBoxes - .map(c => c.asInstanceOf[OutBoxImpl].getErgoBoxCandidate) - .toIndexedSeq.asInstanceOf[IndexedSeq[ErgoBoxCandidate]] - .convertTo[List[ErgoBoxCandidate]] - - if (_outputCandidates.isEmpty) - _outputCandidates = Some(candidates) - else - _outputCandidates.get.addAll(candidates) - + _outputs.addAll(outBoxes + .toIndexedSeq + .asInstanceOf[IndexedSeq[OutBoxImpl]] + .convertTo[util.List[OutBoxImpl]]) this } override def outputs(outputs: OutBox*): UnsignedTransactionBuilder = { - require(_outputCandidates.isEmpty, "Outputs already specified.") + require(_outputs.isEmpty, "Outputs already specified.") addOutputs(outputs: _*) this } @@ -126,7 +119,8 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un override def build: UnsignedTransaction = { val inputBoxes = _inputs - val outputCandidates = getNonEmpty(_outputCandidates, "Output boxes are not specified") + val outputCandidates = _outputs.map(c => c.getErgoBoxCandidate) + require(!outputCandidates.isEmpty, "Output boxes are not specified") val boxesToSpend = inputBoxes .map(b => ExtendedInputBox(b.getErgoBox, b.getExtension)) val dataInputBoxes = _dataInputs.map(b => b.getErgoBox) @@ -195,5 +189,8 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un override def getInputBoxes: List[InputBox] = _inputs.map(b => b.asInstanceOf[InputBox]) + + override def getOutputBoxes: util.List[OutBox] = + _outputs.map(b => b.asInstanceOf[OutBox]) } From 933f8a7d70037bc866bdab2fa8d83dd8ec0b0ada Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Sat, 5 Nov 2022 11:59:02 +0100 Subject: [PATCH 26/44] EIP-0031 use new methods from #205 --- .../ergoplatform/appkit/BabelFeeSpec.scala | 17 +-- .../appkit/babelfee/BabelFeeOperations.java | 133 ++---------------- 2 files changed, 18 insertions(+), 132 deletions(-) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala index f3f59b74..03babaf3 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala @@ -78,16 +78,17 @@ class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyC .tokens(new ErgoToken(ErgoId.create(mockTokenId), 1000)) .build().convertToInputWith(mockTokenId, 0) - val babelTxB = BabelFeeOperations.getBabelFeeTransactionBuilder(txB, + txB.fee(fee) + .outputs(output) + .boxesToSpend(util.Arrays.asList(input)) + .sendChangeTo(sender) + + BabelFeeOperations.addBabelFeeBoxes(txB, babelFeeBoxState.buildOutbox(txB, null) .convertToInputWith(mockTokenId, 0), fee) - val tx = babelTxB.fee(fee) - .outputs(output) - .boxesToSpend(util.Arrays.asList(input)) - .sendChangeTo(sender) - .build() + val tx = txB.build() ctx.newProverBuilder() .withMnemonic(mnemonic, SecretString.empty(), false) @@ -107,7 +108,7 @@ class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyC val babelBox1 = BabelFeeOperations.findBabelFeeBox(ctx, new MockedBoxesLoader(new util.ArrayList[InputBox]()), tockenId, Parameters.MinFee) - babelBox1 shouldBe(null) + babelBox1 shouldBe (null) val inputBabelBox = BabelFeeBoxState.newBuilder() .withValue(Parameters.OneErg) @@ -126,7 +127,7 @@ class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyC val babelBox3 = BabelFeeOperations.findBabelFeeBox(ctx, new MockedBoxesLoader(util.Arrays.asList(inputBabelBox)), tockenId, Parameters.OneErg * 2) - babelBox3 shouldBe(null) + babelBox3 shouldBe (null) } } } diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java index a57227a2..f17c4d2b 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java @@ -1,27 +1,22 @@ package org.ergoplatform.appkit.babelfee; -import org.ergoplatform.ErgoAddress; import org.ergoplatform.appkit.Address; import org.ergoplatform.appkit.BlockchainContext; import org.ergoplatform.appkit.BoxOperations; import org.ergoplatform.appkit.ContextVar; import org.ergoplatform.appkit.ErgoContract; import org.ergoplatform.appkit.ErgoId; -import org.ergoplatform.appkit.ErgoToken; import org.ergoplatform.appkit.ErgoValue; import org.ergoplatform.appkit.InputBox; -import org.ergoplatform.appkit.NetworkType; import org.ergoplatform.appkit.OutBox; import org.ergoplatform.appkit.OutBoxBuilder; import org.ergoplatform.appkit.Parameters; -import org.ergoplatform.appkit.PreHeader; import org.ergoplatform.appkit.UnsignedTransaction; import org.ergoplatform.appkit.UnsignedTransactionBuilder; import org.ergoplatform.appkit.impl.ErgoTreeContract; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import javax.annotation.Nullable; @@ -136,132 +131,22 @@ public static InputBox findBabelFeeBox(BlockchainContext ctx, BoxOperations.IUns } /** - * Creates a transaction builder for the given babel fee box and the amount to be covered by - * babel fees. The returned transaction builder can be used like normal and automatically will - * add the babel fee inbox and outbox to the transaction. You need to make sure that other - * inboxes cover the amount of tokens needed. + * Adds babel fee boxes (input and output) to the given transaction builder * - * @param txB transaction builder the new transaction builder uses under the hoods + * @param txB transaction builder, can already contain input and output boxes * @param babelBox input babel box to make the swap with - * @param nanoErgToCover nanoergs to be covered by babel box, usually the fee amount needed, maybe a change amount as well - * @return transaction builder to be used + * @param nanoErgToCover nanoErgs to be covered by babel box, usually the fee amount needed, maybe a change amount as well */ - public static UnsignedTransactionBuilder getBabelFeeTransactionBuilder( + public static void addBabelFeeBoxes( UnsignedTransactionBuilder txB, InputBox babelBox, long nanoErgToCover ) { - return new BabelFeeTransactionBuilder(txB, babelBox, nanoErgToCover); - } - - private static class BabelFeeTransactionBuilder implements UnsignedTransactionBuilder { - private final UnsignedTransactionBuilder superBuilder; - private final InputBox inputBabelBox; - private final OutBox outBabelBox; - - private List boxesToSpend; - private OutBox[] outputs; - - private BabelFeeTransactionBuilder(UnsignedTransactionBuilder superBuilder, InputBox babelBox, long nanoErgToCover) { - this.superBuilder = superBuilder; - inputBabelBox = babelBox; - BabelFeeBoxState babelBoxState = new BabelFeeBoxState(babelBox); - BabelFeeBoxState outBabelBoxState = babelBoxState.buildSucceedingState(babelBoxState.calcTokensToSellForErgAmount(nanoErgToCover)); - outBabelBox = outBabelBoxState.buildOutbox(superBuilder, babelBox); - } - - private void setBoxesToSpendAndOutputsToSuperBuilder() { - // when both boxes to spend and outputs are defined, we can add the babel box to the list - // and invoke the super transaction builder - // this is not possible before because we need to know output position for the input box - if (boxesToSpend != null && outputs != null) { - List allBoxesToSpend = new LinkedList<>(boxesToSpend); - allBoxesToSpend.add(inputBabelBox.withContextVars(ContextVar.of((byte) 0, ErgoValue.of(outputs.length)))); - OutBox[] allOutBoxes = new OutBox[outputs.length + 1]; - System.arraycopy(outputs, 0, allOutBoxes, 0, outputs.length); - allOutBoxes[outputs.length] = outBabelBox; - superBuilder.boxesToSpend(allBoxesToSpend); - superBuilder.outputs(allOutBoxes); - } - } - - @Override - public UnsignedTransactionBuilder preHeader(PreHeader ph) { - return superBuilder.preHeader(ph); - } - - @Override - public UnsignedTransactionBuilder boxesToSpend(List boxes) { - boxesToSpend = boxes; - setBoxesToSpendAndOutputsToSuperBuilder(); - return this; - } - - @Override - public UnsignedTransactionBuilder withDataInputs(List boxes) { - superBuilder.withDataInputs(boxes); - return this; - } - - @Override - public UnsignedTransactionBuilder outputs(OutBox... outputs) { - this.outputs = outputs; - setBoxesToSpendAndOutputsToSuperBuilder(); - return this; - } - - @Override - public UnsignedTransactionBuilder fee(long feeAmount) { - superBuilder.fee(feeAmount); - return this; - } + BabelFeeBoxState babelBoxState = new BabelFeeBoxState(babelBox); + BabelFeeBoxState outBabelBoxState = babelBoxState.buildSucceedingState(babelBoxState.calcTokensToSellForErgAmount(nanoErgToCover)); + OutBox outBabelBox = outBabelBoxState.buildOutbox(txB, babelBox); - @Override - public UnsignedTransactionBuilder tokensToBurn(ErgoToken... tokens) { - superBuilder.tokensToBurn(tokens); - return this; - } - - @Override - public UnsignedTransactionBuilder sendChangeTo(ErgoAddress address) { - superBuilder.sendChangeTo(address); - return this; - } - - @Override - public UnsignedTransactionBuilder sendChangeTo(Address address) { - superBuilder.sendChangeTo(address); - return this; - } - - @Override - public UnsignedTransaction build() { - return superBuilder.build(); - } - - @Override - public BlockchainContext getCtx() { - return superBuilder.getCtx(); - } - - @Override - public PreHeader getPreHeader() { - return superBuilder.getPreHeader(); - } - - @Override - public NetworkType getNetworkType() { - return superBuilder.getNetworkType(); - } - - @Override - public OutBoxBuilder outBoxBuilder() { - return superBuilder.outBoxBuilder(); - } - - @Override - public List getInputBoxes() { - return boxesToSpend; - } + txB.addInputs(babelBox.withContextVars(ContextVar.of((byte) 0, ErgoValue.of(txB.getOutputBoxes().size())))); + txB.addOutputs(outBabelBox); } } From 93f2eb740ecd63bb10a95fa510635a0360a88d36 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Sat, 5 Nov 2022 14:25:41 +0100 Subject: [PATCH 27/44] EIP-0031 use ErgoTreeTemplate --- .../ergoplatform/appkit/BabelFeeSpec.scala | 7 ++- .../ergoplatform/appkit/ErgoTreeTemplate.java | 51 ++++++++++++------- .../appkit/babelfee/BabelFeeBoxContract.java | 13 +++-- 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala index 03babaf3..d08ee30c 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala @@ -1,6 +1,6 @@ package org.ergoplatform.appkit -import org.ergoplatform.appkit.babelfee.{BabelFeeBoxState, BabelFeeOperations} +import org.ergoplatform.appkit.babelfee.{BabelFeeBoxContract, BabelFeeBoxState, BabelFeeOperations} import org.scalatest.{Matchers, PropSpec} import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks @@ -47,6 +47,11 @@ class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyC .withMnemonic(mnemonic, SecretString.empty(), false) .build() .sign(txCancel) + + // check if the contract really is for the token + new BabelFeeBoxContract(babelFeeErgoBox.getErgoTree).getTokenId.toString shouldBe mockTokenId + // check template hash + ErgoTreeTemplate.fromErgoTree(babelFeeErgoBox.getErgoTree).getTemplateHashHex shouldBe BabelFeeBoxContract.templateHash } } diff --git a/common/src/main/java/org/ergoplatform/appkit/ErgoTreeTemplate.java b/common/src/main/java/org/ergoplatform/appkit/ErgoTreeTemplate.java index 002c7fad..8de0d188 100644 --- a/common/src/main/java/org/ergoplatform/appkit/ErgoTreeTemplate.java +++ b/common/src/main/java/org/ergoplatform/appkit/ErgoTreeTemplate.java @@ -1,14 +1,10 @@ package org.ergoplatform.appkit; -import scala.NotImplementedError; -import scala.collection.IndexedSeq; +import java.util.Arrays; + import scorex.util.encode.Base16; -import sigmastate.SType; import sigmastate.Values; - -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; +import sigmastate.serialization.ErgoTreeSerializer; /** * Represents ErgoTree template, which is an ErgoTree instance with placeholders. @@ -48,14 +44,22 @@ public int hashCode() { * @return template bytes at the tail of the serialized ErgoTree (i.e. exclusing header and segregated * constants) */ - public byte[] getBytes() { return _templateBytes; } + public byte[] getBytes() { + return _templateBytes; + } /** * Returns template bytes encoded as Base16 string. * * @see ErgoTreeTemplate#getBytes */ - public String getEncodedBytes() { return Base16.encode(getBytes()); } + public String getEncodedBytes() { + return Base16.encode(getBytes()); + } + + public String getTemplateHashHex() { + return Base16.encode(scorex.crypto.hash.Sha256.hash(_templateBytes)); + } /** * A number of placeholders in the template, which can be substituted (aka parameters). @@ -64,16 +68,16 @@ public int hashCode() { * {@link ErgoTreeTemplate#applyParameters} method. * In general, constants of ErgoTree cannot be replaced, but every placeholder can. */ - public int getParameterCount() { return _tree.constants().length(); } + public int getParameterCount() { + return _tree.constants().length(); + } /** - * Returns types of all template parameters (placeholders in the ErgoTree). + * @param index 0-based + * @return value object of paramter */ - public List> getParameterTypes() { - Iso>, IndexedSeq>> iso = - Iso.JListToIndexedSeq(Iso.identityIso()); - List> ergoValues = iso.from(_tree.constants()); - return ergoValues.stream().map(v -> Iso.isoErgoTypeToSType().from(v.tpe())).collect(Collectors.toList()); + public ErgoValue getParameter(int index) { + return Iso.isoErgoValueToSValue().from(_tree.constants().apply(index + 1)); } /** @@ -89,11 +93,22 @@ public List> getParameterTypes() { * @return new ErgoTree with the same template as this but with all it's parameters * replaced with `newValues` */ - public Values.ErgoTree applyParameters(ErgoValue newValues) { - throw new NotImplementedError(); + public Values.ErgoTree applyParameters(ErgoValue... newValues) { + int[] positions = new int[newValues.length]; + for (int position : positions) { + positions[position] = position + 1; + } + + return JavaHelpers.substituteErgoTreeConstants(_tree.bytes(), positions, newValues); } public static ErgoTreeTemplate fromErgoTree(Values.ErgoTree tree) { return new ErgoTreeTemplate(tree); } + + public static ErgoTreeTemplate fromErgoTreeBytes(byte[] treeBytes) { + return fromErgoTree(ErgoTreeSerializer.DefaultSerializer().deserializeErgoTree(treeBytes)); + } + + // TODO public static ErgoTreeTemplate fromTemplateBytes(byte[] templateBytes) } diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java index 20e04fc2..fe7320b6 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java @@ -1,8 +1,8 @@ package org.ergoplatform.appkit.babelfee; import org.ergoplatform.appkit.ErgoId; +import org.ergoplatform.appkit.ErgoTreeTemplate; import org.ergoplatform.appkit.ErgoValue; -import org.ergoplatform.appkit.JavaHelpers; import org.ergoplatform.appkit.ScalaHelpers; import scorex.util.encode.Base16; @@ -10,6 +10,13 @@ import special.collection.Coll; public class BabelFeeBoxContract { + + /** + * babel fee box ErgoTreeTemplateHash to be used for Explorer requests + */ + public static final String templateHash = "4e83fa68ef3ed9794bbab5d8799998a9e09fb10a0afe8d1bf928936dfd11c465"; + // this was calced by ErgoTreeTemplate.fromTemplateBytes(contractTemplate).getTemplateHashHex() + private static final String contractTemplateHex = "100604000e000400040005000500d803d601e30004d602e4c6a70408d603e4c6a7050595e67201d804d604b2a5e4720100d605b2db63087204730000d606db6308a7d60799c1a7c17204d1968302019683050193c27204c2a7938c720501730193e4c672040408720293e4c672040505720393e4c67204060ec5a796830201929c998c7205029591b1720673028cb272067303000273047203720792720773057202"; private static final byte[] contractTemplate; @@ -23,12 +30,12 @@ public class BabelFeeBoxContract { public BabelFeeBoxContract(ErgoId tokenId) { this.tokenId = tokenId; byte[] idBytes = tokenId.getBytes(); - ergoTree = JavaHelpers.substituteErgoTreeConstants(contractTemplate, new int[]{1}, new ErgoValue[]{ErgoValue.of(idBytes)}); + ergoTree = ErgoTreeTemplate.fromErgoTreeBytes(contractTemplate).applyParameters(new ErgoValue[]{ErgoValue.of(idBytes)}); } public BabelFeeBoxContract(Values.ErgoTree ergoTree) { this.ergoTree = ergoTree; - tokenId = new ErgoId(ScalaHelpers.collByteToByteArray((Coll) ergoTree.constants().apply(1).value())); + tokenId = new ErgoId(ScalaHelpers.collByteToByteArray((Coll) ErgoTreeTemplate.fromErgoTree(ergoTree).getParameter(0).getValue())); } public Values.ErgoTree getErgoTree() { From 3d67aaa20298f68bb60caec4d3a482aca21a5f89 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Sun, 6 Nov 2022 16:09:12 +0100 Subject: [PATCH 28/44] Add BoxOperations.prepareOutBox method --- .../ergoplatform/appkit/BoxOperations.java | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/BoxOperations.java b/lib-api/src/main/java/org/ergoplatform/appkit/BoxOperations.java index 3db89ae8..c47c295d 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/BoxOperations.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/BoxOperations.java @@ -312,20 +312,29 @@ public UnsignedTransaction putToContractTxUnsigned( ErgoContract contract) { return buildTxWithDefaultInputs(txB -> { - OutBoxBuilder outBoxBuilder = txB.outBoxBuilder() - .value(amountToSpend) + OutBoxBuilder outBoxBuilder = prepareOutBox(txB) .contract(contract); - if (!tokensToSpend.isEmpty()) - outBoxBuilder.tokens(tokensToSpend.toArray(new ErgoToken[]{})); - if (attachment != null) { - outBoxBuilder.registers(attachment.getOutboxRegistersForAttachment()); - } OutBox newBox = outBoxBuilder.build(); txB.outputs(newBox); return txB; }); } + /** + * @return OutBox prepared with the properties set to this BoxOperations instance: tokens to + * spend, amount to spend and attachment. + */ + public OutBoxBuilder prepareOutBox(UnsignedTransactionBuilder txB) { + OutBoxBuilder outBoxBuilder = txB.outBoxBuilder() + .value(amountToSpend); + if (!tokensToSpend.isEmpty()) + outBoxBuilder.tokens(tokensToSpend.toArray(new ErgoToken[]{})); + if (attachment != null) { + outBoxBuilder.registers(attachment.getOutboxRegistersForAttachment()); + } + return outBoxBuilder; + } + /** * Creates a new {@link UnsignedTransaction} which sends the given amount of NanoErgs and a * newly minted token to the given contract. @@ -373,20 +382,20 @@ public UnsignedTransaction buildTxWithDefaultInputs(Function boxes, - ErgoProver sender, Address recipient, long amount, long fee) { + BlockchainContext ctx, + UnsignedTransactionBuilder txB, + List boxes, + ErgoProver sender, Address recipient, long amount, long fee) { OutBox newBox = txB.outBoxBuilder() - .value(amount) - .contract(recipient.toErgoContract()) - .build(); + .value(amount) + .contract(recipient.toErgoContract()) + .build(); UnsignedTransaction tx = txB.boxesToSpend(boxes) - .outputs(newBox) - .fee(fee) - .sendChangeTo(sender.getP2PKAddress()) - .build(); + .outputs(newBox) + .fee(fee) + .sendChangeTo(sender.getP2PKAddress()) + .build(); SignedTransaction signed = sender.sign(tx); return signed; } @@ -400,7 +409,7 @@ public static SignedTransaction spendBoxesTx( * - returns a list of {@link InputBox} to select from. First items are preferred to be selected * - must not return null * - returning an empty list means the source of input boxes is drained and no further page will - * be loaded + * be loaded * * @param amountToSpend amount of NanoErgs to be covered * @param tokensToSpend ErgoToken to spent @@ -450,7 +459,7 @@ private static CoveringBoxes getCoveringBoxesFor(long amountToSpend, if (remainingAmountToCover <= 0 && tokensRemaining.areTokensCovered()) return new CoveringBoxes(amountToSpend, selectedCoveringBoxes, tokensToSpend, changeBoxConsidered); - // check the maxBoxToSelect restriction, if it is set + // check the maxBoxToSelect restriction, if it is set else if (maxBoxesToSelect > 0 && selectedCoveringBoxes.size() >= maxBoxesToSelect) { List remainingTokenList = tokensRemaining.getRemainingTokenList(); throw new InputBoxesSelectionException.InputBoxLimitExceededException( From 21b7d29582e8858b8018d90a211200a56b762b58 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Sun, 6 Nov 2022 17:50:05 +0100 Subject: [PATCH 29/44] Fix ClassCastException introduced in #202 --- .../org/ergoplatform/appkit/TxBuilderSpec.scala | 12 +++++++++++- .../ergoplatform/appkit/InputBoxesValidator.scala | 6 +++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala index df686ed0..39d6a4f7 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala @@ -2,7 +2,7 @@ package org.ergoplatform.appkit import com.google.gson.Gson import com.google.gson.reflect.TypeToken -import org.ergoplatform.appkit.InputBoxesSelectionException.{InputBoxLimitExceededException, NotEnoughErgsException} +import org.ergoplatform.appkit.InputBoxesSelectionException.{InputBoxLimitExceededException, NotEnoughCoinsForChangeException, NotEnoughErgsException} import org.ergoplatform.appkit.JavaHelpers._ import org.ergoplatform.appkit.examples.RunMockedScala.data import org.ergoplatform.appkit.impl.{Eip4TokenBuilder, ErgoTreeContract} @@ -405,6 +405,16 @@ class TxBuilderSpec extends PropSpec with Matchers operations.withMaxInputBoxesToSelect(1).loadTop(), exceptionLike[InputBoxLimitExceededException]("could not cover 1000000 nanoERG") ) + + // if there is only a single input box, we face NotEnoughCoinsForChangeException + val operations2 = BoxOperations.createForSenders(senders, ctx) + .withAmountToSpend(amountToSend) + .withInputBoxesLoader(new MockedBoxesLoader(util.Arrays.asList(input1))) + + assertExceptionThrown( + operations2.loadTop(), + exceptionLike[NotEnoughCoinsForChangeException]() + ) } } diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/InputBoxesValidator.scala b/lib-api/src/main/java/org/ergoplatform/appkit/InputBoxesValidator.scala index 84b00a90..2af13817 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/InputBoxesValidator.scala +++ b/lib-api/src/main/java/org/ergoplatform/appkit/InputBoxesValidator.scala @@ -7,7 +7,6 @@ import org.ergoplatform.wallet.boxes.{BoxSelector, ReemissionData} import org.ergoplatform.wallet.{AssetUtils, TokensMap} import org.ergoplatform.{ErgoBoxAssets, ErgoBoxAssetsHolder} import scorex.util.ModifierId -import sigmastate.utils.Helpers.EitherOps import scala.collection.mutable @@ -42,8 +41,9 @@ class InputBoxesValidator extends BoxSelector { if (targetAssets.forall { case (id, targetAmt) => currentAssets.getOrElse(id, 0L) >= targetAmt }) { - formChangeBoxes(currentBalance, targetBalance, currentAssets, targetAssets).mapRight { changeBoxes => - BoxSelectionResult(res, changeBoxes) + formChangeBoxes(currentBalance, targetBalance, currentAssets, targetAssets) match { + case Right(changeBoxes) => Right(BoxSelectionResult(res, changeBoxes)) + case Left(error) => Left(error) } } else { Left(NotEnoughTokensError( From d5b0ef930c578534bec81b5fd42407aed82efd6f Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Mon, 7 Nov 2022 11:12:21 +0100 Subject: [PATCH 30/44] Deprecate UnsignedTransactionBuilder.outputs, remove UnsignedTransactionBuilder.inputs --- .../ergoplatform/appkit/TxBuilderSpec.scala | 4 +- .../appkit/examples/DataInputsSpec.scala | 2 +- .../appkit/UnsignedTransactionBuilder.java | 38 ++++++++----------- .../impl/UnsignedTransactionBuilderImpl.scala | 14 ++----- 4 files changed, 22 insertions(+), 36 deletions(-) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala index 7ae51b27..7b6c438a 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala @@ -161,7 +161,7 @@ class TxBuilderSpec extends PropSpec with Matchers .build() val changeAddr = Address.fromErgoTree(input.getErgoTree, NetworkType.MAINNET).getErgoAddress - val unsigned = txB.inputs(input) + val unsigned = txB.addInputs(input) .outputs(output).addOutputs(feeOut) .sendChangeTo(changeAddr) .build() @@ -434,7 +434,7 @@ class TxBuilderSpec extends PropSpec with Matchers .contract(pkContract) .build().convertToInputWith(mockTxId, 1) - val tx = ctx.newTxBuilder().inputs(input1).addInputs(input2) + val tx = ctx.newTxBuilder().addInputs(input1).addInputs(input2) .outputs(ctx.newTxBuilder().outBoxBuilder().contract(pkContract).value(amountToSend).build()) .sendChangeTo(recipient.getErgoAddress) .fee(Parameters.MinFee) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/examples/DataInputsSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/examples/DataInputsSpec.scala index dc0f8b49..e587ab4c 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/examples/DataInputsSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/examples/DataInputsSpec.scala @@ -83,7 +83,7 @@ class DataInputsSpec extends PropSpec with Matchers val ergoTree = JavaHelpers.decodeStringToErgoTree(dummyErgoTree) val changeAddr = Address.fromErgoTree(ergoTree, NetworkType.MAINNET).getErgoAddress - val unsigned = txB.boxesToSpend(inputs).outputs(dummyOutput).withDataInputs(dataInput).fee(10000000).sendChangeTo(changeAddr).build() + val unsigned = txB.boxesToSpend(inputs).outputs(dummyOutput).addDataInputs(dataInput).fee(10000000).sendChangeTo(changeAddr).build() an[Exception] shouldBe thrownBy { ctx.newProverBuilder().build().sign(unsigned) 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 f0e30749..bb79a2ef 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/UnsignedTransactionBuilder.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/UnsignedTransactionBuilder.java @@ -22,7 +22,9 @@ public interface UnsignedTransactionBuilder { UnsignedTransactionBuilder preHeader(PreHeader ph); /** - * Specifies boxes that will be spent by the transaction when it will be included in a block. + * Adds input boxes to an already specified list of inputs or, if no input boxes defined yet, + * as the boxes to spent. The order is preserved. + * The boxes boxes that will be spent by the transaction when it will be included in a block. * * @param boxes list of boxes to be spent by the transaction. The boxes can either be * {@link BlockchainContext#getBoxesById(String...) obtained} from context of created from @@ -30,22 +32,17 @@ public interface UnsignedTransactionBuilder { * as {@link OutBox} and then {@link OutBox#convertToInputWith(String, short) converted} to * {@link InputBox}. */ - UnsignedTransactionBuilder inputs(InputBox... boxes); - - /** - * Adds input boxes to an already specified list of inputs or, if no input boxes defined yet, - * as the boxes to spent. The order is preserved. - */ UnsignedTransactionBuilder addInputs(InputBox... boxes); /** - * @deprecated use {@link #inputs(InputBox...)} + * @deprecated use {@link #addInputs(InputBox...)} */ @Deprecated UnsignedTransactionBuilder boxesToSpend(List boxes); /** - * Specifies boxes that will be used as data-inputs by the transaction when it will be included in a block. + * Adds input boxes to an already specified list of data inputs or, if no data input boxes + * defined yet, set the boxes as the data input boxes to be used. The order is preserved. * * @param boxes list of boxes to be used as data-inputs by the transaction. The boxes can either be * {@link BlockchainContext#getBoxesById(String...) obtained} from context of created from @@ -53,34 +50,29 @@ public interface UnsignedTransactionBuilder { * as {@link OutBox} and then {@link OutBox#convertToInputWith(String, short) converted} to * {@link InputBox}. */ - UnsignedTransactionBuilder withDataInputs(InputBox... boxes); - - /** - * Adds input boxes to an already specified list of data inputs or, if no data input boxes - * defined yet, set the boxes as the data input boxes to be used. The order is preserved. - */ UnsignedTransactionBuilder addDataInputs(InputBox... boxes); /** - * @deprecated use {@link #withDataInputs(InputBox...)} + * @deprecated use {@link #addDataInputs(InputBox...)} */ @Deprecated UnsignedTransactionBuilder withDataInputs(List boxes); /** - * Specifies output boxes of the transaction. After this transaction is - * {@link UnsignedTransactionBuilder#build() built}, {@link ErgoProver#sign(UnsignedTransaction)} signed, - * {@link BlockchainContext#sendTransaction(SignedTransaction) sent} to the node and included into a - * next block - * the output boxes will be put in the UTXO set. - * - * @param outputs output boxes created by the transaction + * @deprecated use {@link #addOutputs(OutBox...)} */ + @Deprecated UnsignedTransactionBuilder outputs(OutBox... outputs); /** * Adds output boxes to an already specified list of outputs or, if no output boxes defined yet, * as the boxes to be outputted. The order is preserved. + * After this transaction is {@link UnsignedTransactionBuilder#build() built}, + * {@link ErgoProver#sign(UnsignedTransaction)} signed, + * {@link BlockchainContext#sendTransaction(SignedTransaction) sent} to the node and included + * into a next block the output boxes will be put in the UTXO set. + * + * @param outBoxes output boxes created by the transaction */ UnsignedTransactionBuilder addOutputs(OutBox... outBoxes); diff --git a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala index fea8b339..3b7a2a12 100644 --- a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala +++ b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala @@ -40,15 +40,12 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un this } - override def inputs(boxes: InputBox*): UnsignedTransactionBuilder = { + override def boxesToSpend(inputBoxes: List[InputBox]): UnsignedTransactionBuilder = { require(_inputs.isEmpty, "inputs already specified") - addInputs(boxes: _*) + addInputs(inputBoxes.toSeq: _*) this } - override def boxesToSpend(inputBoxes: List[InputBox]): UnsignedTransactionBuilder = - inputs(inputBoxes.toSeq: _*) - override def addDataInputs(boxes: InputBox*): UnsignedTransactionBuilder = { _dataInputs.addAll(boxes .toIndexedSeq.asInstanceOf[IndexedSeq[InputBoxImpl]] @@ -56,15 +53,12 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un this } - override def withDataInputs(boxes: InputBox*): UnsignedTransactionBuilder = { + override def withDataInputs(inputBoxes: List[InputBox]): UnsignedTransactionBuilder = { require(_dataInputs.isEmpty, "dataInputs list is already specified") - addDataInputs(boxes: _*) + addDataInputs(inputBoxes.toSeq: _*) this } - override def withDataInputs(inputBoxes: List[InputBox]): UnsignedTransactionBuilder = - withDataInputs(inputBoxes.toSeq: _*) - override def addOutputs(outBoxes: OutBox*): UnsignedTransactionBuilder = { _outputs.addAll(outBoxes .toIndexedSeq From a3105eaa2a3c2f222d5d6d4d669933e36c74886d Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Mon, 7 Nov 2022 15:00:38 +0100 Subject: [PATCH 31/44] transactionbuilder-addmethods: various fixes and imporovements --- .../ergoplatform/appkit/BoxOperations.java | 11 ++++---- .../appkit/UnsignedTransactionBuilder.java | 6 ++--- .../impl/UnsignedTransactionBuilderImpl.scala | 26 +++++++++---------- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/BoxOperations.java b/lib-api/src/main/java/org/ergoplatform/appkit/BoxOperations.java index c47c295d..f8121040 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/BoxOperations.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/BoxOperations.java @@ -321,7 +321,7 @@ public UnsignedTransaction putToContractTxUnsigned( } /** - * @return OutBox prepared with the properties set to this BoxOperations instance: tokens to + * @return OutBoxBuilder prepared with the properties set to this BoxOperations instance: tokens to * spend, amount to spend and attachment. */ public OutBoxBuilder prepareOutBox(UnsignedTransactionBuilder txB) { @@ -392,7 +392,7 @@ public static SignedTransaction spendBoxesTx( .build(); UnsignedTransaction tx = txB.boxesToSpend(boxes) - .outputs(newBox) + .addOutputs(newBox) .fee(fee) .sendChangeTo(sender.getP2PKAddress()) .build(); @@ -409,7 +409,7 @@ public static SignedTransaction spendBoxesTx( * - returns a list of {@link InputBox} to select from. First items are preferred to be selected * - must not return null * - returning an empty list means the source of input boxes is drained and no further page will - * be loaded + * be loaded * * @param amountToSpend amount of NanoErgs to be covered * @param tokensToSpend ErgoToken to spent @@ -458,9 +458,8 @@ private static CoveringBoxes getCoveringBoxesFor(long amountToSpend, } if (remainingAmountToCover <= 0 && tokensRemaining.areTokensCovered()) return new CoveringBoxes(amountToSpend, selectedCoveringBoxes, tokensToSpend, changeBoxConsidered); - - // check the maxBoxToSelect restriction, if it is set - else if (maxBoxesToSelect > 0 && selectedCoveringBoxes.size() >= maxBoxesToSelect) { + else // check the maxBoxToSelect restriction, if it is set + if (maxBoxesToSelect > 0 && selectedCoveringBoxes.size() >= maxBoxesToSelect) { List remainingTokenList = tokensRemaining.getRemainingTokenList(); throw new InputBoxesSelectionException.InputBoxLimitExceededException( "Input box limit exceeded, could not cover " + remainingAmountToCover + 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 bb79a2ef..17863dcd 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/UnsignedTransactionBuilder.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/UnsignedTransactionBuilder.java @@ -23,8 +23,8 @@ public interface UnsignedTransactionBuilder { /** * Adds input boxes to an already specified list of inputs or, if no input boxes defined yet, - * as the boxes to spent. The order is preserved. - * The boxes boxes that will be spent by the transaction when it will be included in a block. + * as the boxes to spend. The order is preserved. + * The boxes that will be spent by the transaction when it will be included in a block. * * @param boxes list of boxes to be spent by the transaction. The boxes can either be * {@link BlockchainContext#getBoxesById(String...) obtained} from context of created from @@ -66,7 +66,7 @@ public interface UnsignedTransactionBuilder { /** * Adds output boxes to an already specified list of outputs or, if no output boxes defined yet, - * as the boxes to be outputted. The order is preserved. + * as the boxes to be output. The order is preserved. * After this transaction is {@link UnsignedTransactionBuilder#build() built}, * {@link ErgoProver#sign(UnsignedTransaction)} signed, * {@link BlockchainContext#sendTransaction(SignedTransaction) sent} to the node and included diff --git a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala index 3b7a2a12..c63cfe09 100644 --- a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala +++ b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/UnsignedTransactionBuilderImpl.scala @@ -17,9 +17,9 @@ import scala.collection.JavaConversions import scala.collection.JavaConversions.iterableAsScalaIterable class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends UnsignedTransactionBuilder { - private[impl] var _inputs: List[InputBoxImpl] = new ArrayList[InputBoxImpl]() - private[impl] var _outputs: List[OutBoxImpl] = new ArrayList[OutBoxImpl]() - private[impl] var _dataInputs: List[InputBoxImpl] = new ArrayList[InputBoxImpl]() + private[impl] val _inputs: List[InputBoxImpl] = new ArrayList[InputBoxImpl]() + private[impl] val _outputs: List[OutBoxImpl] = new ArrayList[OutBoxImpl]() + private[impl] val _dataInputs: List[InputBoxImpl] = new ArrayList[InputBoxImpl]() private var _tokensToBurn: Option[List[ErgoToken]] = None private var _feeAmount: Option[Long] = None @@ -33,10 +33,9 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un } override def addInputs(boxes: InputBox*): UnsignedTransactionBuilder = { - _inputs.addAll(boxes - .map(b => b.asInstanceOf[InputBoxImpl]) - .toIndexedSeq.asInstanceOf[IndexedSeq[InputBoxImpl]] - .convertTo[util.List[InputBoxImpl]]) + boxes.foreach { case b: InputBoxImpl => + _inputs.add(b) + } this } @@ -47,9 +46,9 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un } override def addDataInputs(boxes: InputBox*): UnsignedTransactionBuilder = { - _dataInputs.addAll(boxes - .toIndexedSeq.asInstanceOf[IndexedSeq[InputBoxImpl]] - .convertTo[util.List[InputBoxImpl]]) + boxes.foreach { case b: InputBoxImpl => + _dataInputs.add(b) + } this } @@ -60,10 +59,9 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un } override def addOutputs(outBoxes: OutBox*): UnsignedTransactionBuilder = { - _outputs.addAll(outBoxes - .toIndexedSeq - .asInstanceOf[IndexedSeq[OutBoxImpl]] - .convertTo[util.List[OutBoxImpl]]) + outBoxes.foreach { case b: OutBoxImpl => + _outputs.add(b) + } this } From 15fee58227d1bcaf6739ceb3b32031c304c374c9 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Mon, 7 Nov 2022 19:26:02 +0100 Subject: [PATCH 32/44] transactionbuilder-addmethods: correct implementation of ErgoTreeTemplate --- .../ergoplatform/appkit/ErgoTreeTemplate.java | 68 ++++++++++++++----- .../org/ergoplatform/appkit/JavaHelpers.scala | 17 ++++- .../appkit/babelfee/BabelFeeBoxContract.java | 5 +- 3 files changed, 69 insertions(+), 21 deletions(-) diff --git a/common/src/main/java/org/ergoplatform/appkit/ErgoTreeTemplate.java b/common/src/main/java/org/ergoplatform/appkit/ErgoTreeTemplate.java index 8de0d188..4f0e31df 100644 --- a/common/src/main/java/org/ergoplatform/appkit/ErgoTreeTemplate.java +++ b/common/src/main/java/org/ergoplatform/appkit/ErgoTreeTemplate.java @@ -1,9 +1,14 @@ package org.ergoplatform.appkit; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import scala.collection.IndexedSeq; import scorex.util.encode.Base16; +import sigmastate.SType; import sigmastate.Values; +import sigmastate.Values.Constant; import sigmastate.serialization.ErgoTreeSerializer; /** @@ -13,14 +18,30 @@ */ public class ErgoTreeTemplate { + private static int[] _noParameters = new int[0]; // immutable and shared by all instances private final Values.ErgoTree _tree; private final byte[] _templateBytes; + private int[] _parameterPositions = _noParameters; private ErgoTreeTemplate(Values.ErgoTree tree) { _tree = tree; _templateBytes = JavaHelpers.ergoTreeTemplateBytes(_tree); } + /** + * Specifies which ErgoTree constants will be used as template parameters. + * Which tree constants to be used as parameters depends on the contract and use case. + * + * @param positions zero-based indexes in `ErgoTree.constants` array which can be + * substituted as parameters using + * {@link ErgoTreeTemplate#applyParameters(ErgoValue[])} method. + * @see sigmastate.Values.ErgoTree + */ + public ErgoTreeTemplate withParameterPositions(int[] positions) { + _parameterPositions = positions; + return this; + } + @Override public boolean equals(java.lang.Object o) { if (this == o) { @@ -62,22 +83,34 @@ public String getTemplateHashHex() { } /** - * A number of placeholders in the template, which can be substituted (aka parameters). - * This is immutable property of a {@link ErgoTreeTemplate}, which counts all the constants in the - * {@link sigmastate.Values.ErgoTree} which can be replaced by new values using - * {@link ErgoTreeTemplate#applyParameters} method. - * In general, constants of ErgoTree cannot be replaced, but every placeholder can. + * A number of parameters in this template. + * In general, there may be more constants of ErgoTree then template parameters because + * not every constant make sense as a parameter. */ public int getParameterCount() { - return _tree.constants().length(); + return _parameterPositions.length; + } + + /** + * Returns types of all template parameters (i.e. specified constants in the ErgoTree). + */ + public List> getParameterTypes() { + List> types = new ArrayList<>(); + IndexedSeq> constants = _tree.constants(); + for (int position : _parameterPositions) { + SType tpe = constants.apply(position).tpe(); + types.add(Iso.isoErgoTypeToSType().from(tpe)); + } + return types; } /** - * @param index 0-based - * @return value object of paramter + * @param index 0-based index of parameter in [0 .. getParameterCount()) range + * @return ErgoValue of the given parameter */ - public ErgoValue getParameter(int index) { - return Iso.isoErgoValueToSValue().from(_tree.constants().apply(index + 1)); + public ErgoValue getParameterValue(int index) { + Constant c = _tree.constants().apply(_parameterPositions[index]); + return Iso.isoErgoValueToSValue().from(c); } /** @@ -94,12 +127,11 @@ public ErgoValue getParameter(int index) { * replaced with `newValues` */ public Values.ErgoTree applyParameters(ErgoValue... newValues) { - int[] positions = new int[newValues.length]; - for (int position : positions) { - positions[position] = position + 1; - } - - return JavaHelpers.substituteErgoTreeConstants(_tree.bytes(), positions, newValues); + if (newValues.length != _parameterPositions.length) + throw new IllegalArgumentException( + "Wrong number of newValues. Expected " + _parameterPositions.length + + " but was " + newValues.length); + return JavaHelpers.substituteErgoTreeConstants(_tree.bytes(), _parameterPositions, newValues); } public static ErgoTreeTemplate fromErgoTree(Values.ErgoTree tree) { @@ -107,7 +139,9 @@ public static ErgoTreeTemplate fromErgoTree(Values.ErgoTree tree) { } public static ErgoTreeTemplate fromErgoTreeBytes(byte[] treeBytes) { - return fromErgoTree(ErgoTreeSerializer.DefaultSerializer().deserializeErgoTree(treeBytes)); + Values.ErgoTree ergoTree = + ErgoTreeSerializer.DefaultSerializer().deserializeErgoTree(treeBytes); + return fromErgoTree(ergoTree); } // TODO public static ErgoTreeTemplate fromTemplateBytes(byte[] templateBytes) diff --git a/common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala b/common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala index 9bcbe71e..52f7a8a9 100644 --- a/common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala +++ b/common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala @@ -328,10 +328,23 @@ object JavaHelpers { ErgoTreeSerializer.DefaultSerializer.deserializeErgoTree(Base16.decode(base16).get) } + /** Transforms serialized bytes of ErgoTree with segregated constants by + * replacing constants at given positions with new values. This operation + * allow to use serialized scripts as pre-defined templates. + * See [[sigmastate.SubstConstants]] for details. + * + * @param ergoTreeBytes serialized ErgoTree with ConstantSegregationFlag set to 1. + * @param positions zero based indexes in ErgoTree.constants array which + * should be replaced with new values + * @param newValues new values to be injected into the corresponding + * positions in ErgoTree.constants array + * @return a new ErgoTree such that only specified constants + * are replaced and all other remain exactly the same + */ def substituteErgoTreeConstants(ergoTreeBytes: Array[Byte], positions: Array[Int], newValues: Array[ErgoValue[_]]): ErgoTree = { - val newBytes = ErgoTreeSerializer.DefaultSerializer.substituteConstants( + val (newBytes, _) = ErgoTreeSerializer.DefaultSerializer.substituteConstants( ergoTreeBytes, positions, newValues.map(Iso.isoErgoValueToSValue.to)) - ErgoTreeSerializer.DefaultSerializer.deserializeErgoTree(newBytes._1) + ErgoTreeSerializer.DefaultSerializer.deserializeErgoTree(newBytes) } def createP2PKAddress(pk: ProveDlog, networkPrefix: NetworkPrefix): P2PKAddress = { diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java index fe7320b6..adb7a7f6 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java @@ -30,12 +30,13 @@ public class BabelFeeBoxContract { public BabelFeeBoxContract(ErgoId tokenId) { this.tokenId = tokenId; byte[] idBytes = tokenId.getBytes(); - ergoTree = ErgoTreeTemplate.fromErgoTreeBytes(contractTemplate).applyParameters(new ErgoValue[]{ErgoValue.of(idBytes)}); + ergoTree = ErgoTreeTemplate.fromErgoTreeBytes(contractTemplate) + .applyParameters(new ErgoValue[]{ErgoValue.of(idBytes)}); } public BabelFeeBoxContract(Values.ErgoTree ergoTree) { this.ergoTree = ergoTree; - tokenId = new ErgoId(ScalaHelpers.collByteToByteArray((Coll) ErgoTreeTemplate.fromErgoTree(ergoTree).getParameter(0).getValue())); + tokenId = new ErgoId(ScalaHelpers.collByteToByteArray((Coll) ErgoTreeTemplate.fromErgoTree(ergoTree).getParameterValue(0).getValue())); } public Values.ErgoTree getErgoTree() { From fe3f0fd52055285b628c1184436350d7e3d88891 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Mon, 7 Nov 2022 21:16:55 +0100 Subject: [PATCH 33/44] transactionbuilder-addmethods: tests for ErgoTreeTemplate --- .../ergoplatform/appkit/ErgoTreeTemplate.java | 15 ++++- .../appkit/ErgoTreeTemplateSpec.scala | 63 +++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 common/src/test/scala/org/ergoplatform/appkit/ErgoTreeTemplateSpec.scala diff --git a/common/src/main/java/org/ergoplatform/appkit/ErgoTreeTemplate.java b/common/src/main/java/org/ergoplatform/appkit/ErgoTreeTemplate.java index 4f0e31df..a96a4d03 100644 --- a/common/src/main/java/org/ergoplatform/appkit/ErgoTreeTemplate.java +++ b/common/src/main/java/org/ergoplatform/appkit/ErgoTreeTemplate.java @@ -5,6 +5,8 @@ import java.util.List; import scala.collection.IndexedSeq; +import scala.collection.immutable.StringOps; +import scala.collection.mutable.ArrayOps; import scorex.util.encode.Base16; import sigmastate.SType; import sigmastate.Values; @@ -38,6 +40,13 @@ private ErgoTreeTemplate(Values.ErgoTree tree) { * @see sigmastate.Values.ErgoTree */ public ErgoTreeTemplate withParameterPositions(int[] positions) { + if (Arrays.stream(positions).distinct().count() != positions.length) + throw new IllegalArgumentException("Duplicate positions: " + + new ArrayOps.ofInt(positions).mkString("[", ",", "]")); + + for (int p : positions) + if (!_tree.constants().isDefinedAt(p)) + throw new IllegalArgumentException("Invalid parameter position " + p); _parameterPositions = positions; return this; } @@ -105,11 +114,11 @@ public List> getParameterTypes() { } /** - * @param index 0-based index of parameter in [0 .. getParameterCount()) range + * @param paramIndex 0-based index of parameter in [0 .. getParameterCount()) range * @return ErgoValue of the given parameter */ - public ErgoValue getParameterValue(int index) { - Constant c = _tree.constants().apply(_parameterPositions[index]); + public ErgoValue getParameterValue(int paramIndex) { + Constant c = _tree.constants().apply(_parameterPositions[paramIndex]); return Iso.isoErgoValueToSValue().from(c); } diff --git a/common/src/test/scala/org/ergoplatform/appkit/ErgoTreeTemplateSpec.scala b/common/src/test/scala/org/ergoplatform/appkit/ErgoTreeTemplateSpec.scala new file mode 100644 index 00000000..2fa26f45 --- /dev/null +++ b/common/src/test/scala/org/ergoplatform/appkit/ErgoTreeTemplateSpec.scala @@ -0,0 +1,63 @@ +package org.ergoplatform.appkit + +import org.ergoplatform.appkit.JavaHelpers.UniversalConverter +import sigmastate.{SInt, EQ, Plus, SType} +import sigmastate.Values.{IntConstant, ErgoTree} +import sigmastate.helpers.NegativeTesting +import sigmastate.serialization.generators.ObjectGenerators + +import java.util.{List => JList} + +class ErgoTreeTemplateSpec extends TestingBase + with AppkitTestingCommon + with ObjectGenerators + with NegativeTesting { + val tree = ErgoTree.fromProposition(EQ(IntConstant(10), Plus(IntConstant(9), IntConstant(1)))) + + property("should create template without parameters") { + tree.constants.length shouldBe 3 + val template = ErgoTreeTemplate.fromErgoTree(tree) + template.getParameterCount shouldBe 0 + } + + property("should create template with parameters") { + val template = ErgoTreeTemplate.fromErgoTree(tree) + .withParameterPositions(Array(0)) + template.getParameterCount shouldBe 1 + template.getParameterValue(0) shouldBe ErgoValue.of(10) + val expectedTypes = IndexedSeq(SInt: SType).convertTo[JList[ErgoType[_]]] + template.getParameterTypes shouldBe expectedTypes + } + + property("should apply parameters") { + val template = ErgoTreeTemplate.fromErgoTree(tree) + .withParameterPositions(Array(0)) + val newTree = template.applyParameters(ErgoValue.of(11)) + val expectedTree = ErgoTree.fromProposition( + EQ(IntConstant(11), Plus(IntConstant(9), IntConstant(1))) + ) + newTree shouldBe expectedTree + + assertExceptionThrown( + template.applyParameters(ErgoValue.of(11), ErgoValue.of(20)), + exceptionLike[IllegalArgumentException]("Wrong number of newValues. Expected 1 but was 2") + ) + + assertExceptionThrown( + template.applyParameters(ErgoValue.of(1.toByte)), // invalid type of ErgoValue (should be Int) + exceptionLike[AssertionError]() + ) + } + + property("should validate parameters") { + assertExceptionThrown( + ErgoTreeTemplate.fromErgoTree(tree).withParameterPositions(Array(0, 0)), + exceptionLike[IllegalArgumentException]("Duplicate positions: [0,0]") + ) + assertExceptionThrown( + ErgoTreeTemplate.fromErgoTree(tree).withParameterPositions(Array(3)), + exceptionLike[IllegalArgumentException]("Invalid parameter position 3") + ) + } + +} From 9ec3f36c3edc72c853e7262d982f84eda2257d5f Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Wed, 9 Nov 2022 17:18:20 +0100 Subject: [PATCH 34/44] Added BabelFeeOperations.findBabelFeeBox parameter to search through all boxes, fixed errors regading ErgoTreeTemplate --- .../ergoplatform/appkit/BabelFeeSpec.scala | 6 ++--- .../appkit/babelfee/BabelFeeBoxContract.java | 6 ++++- .../appkit/babelfee/BabelFeeOperations.java | 24 ++++++++++++------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala index d08ee30c..869d478c 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala @@ -111,7 +111,7 @@ class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyC // find no boxes val babelBox1 = BabelFeeOperations.findBabelFeeBox(ctx, new MockedBoxesLoader(new util.ArrayList[InputBox]()), - tockenId, Parameters.MinFee) + tockenId, Parameters.MinFee, 1) babelBox1 shouldBe (null) @@ -124,13 +124,13 @@ class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyC .convertToInputWith(mockTokenId, 0) val babelBox2 = BabelFeeOperations.findBabelFeeBox(ctx, new MockedBoxesLoader(util.Arrays.asList(inputBabelBox)), - tockenId, Parameters.MinFee) + tockenId, Parameters.MinFee, 1) babelBox2 shouldBe inputBabelBox // the amount needed (2 ERG) is more than inputBabelBox can offer, so it is discarded val babelBox3 = BabelFeeOperations.findBabelFeeBox(ctx, new MockedBoxesLoader(util.Arrays.asList(inputBabelBox)), - tockenId, Parameters.OneErg * 2) + tockenId, Parameters.OneErg * 2, 1) babelBox3 shouldBe (null) } diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java index adb7a7f6..756ff176 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java @@ -19,6 +19,7 @@ public class BabelFeeBoxContract { private static final String contractTemplateHex = "100604000e000400040005000500d803d601e30004d602e4c6a70408d603e4c6a7050595e67201d804d604b2a5e4720100d605b2db63087204730000d606db6308a7d60799c1a7c17204d1968302019683050193c27204c2a7938c720501730193e4c672040408720293e4c672040505720393e4c67204060ec5a796830201929c998c7205029591b1720673028cb272067303000273047203720792720773057202"; private static final byte[] contractTemplate; + private static final int[] contractParameterPositions = new int[] {1}; static { contractTemplate = Base16.decode(contractTemplateHex).get(); @@ -31,12 +32,15 @@ public BabelFeeBoxContract(ErgoId tokenId) { this.tokenId = tokenId; byte[] idBytes = tokenId.getBytes(); ergoTree = ErgoTreeTemplate.fromErgoTreeBytes(contractTemplate) + .withParameterPositions(contractParameterPositions) .applyParameters(new ErgoValue[]{ErgoValue.of(idBytes)}); } public BabelFeeBoxContract(Values.ErgoTree ergoTree) { this.ergoTree = ergoTree; - tokenId = new ErgoId(ScalaHelpers.collByteToByteArray((Coll) ErgoTreeTemplate.fromErgoTree(ergoTree).getParameterValue(0).getValue())); + tokenId = new ErgoId(ScalaHelpers.collByteToByteArray((Coll) ErgoTreeTemplate.fromErgoTree(ergoTree) + .withParameterPositions(contractParameterPositions) + .getParameterValue(0).getValue())); } public Values.ErgoTree getErgoTree() { diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java index f17c4d2b..4732dfc4 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java @@ -85,19 +85,27 @@ public static UnsignedTransaction cancelBabelFeeContract(BoxOperations boxOperat /** * Tries to fetch a babel fee box for the given tokenId from blockchain data source using the * given loader. - * The box returned is in general the first box satisfying the given fee amount that is returned - * by the loader. Under certain circumstances, a babel fee box with a better price than the - * first one is returned, but a best price is not guaranteed. Clients should implement an own - * logic to retrieve babel fee boxes if needed. + * If maxPagesToLoadForPriceSearch is 0, the babel fee box with the best price satisfying + * feeAmount is returned. As this can result in infinite loading times, it is recommended to + * specify a maximum pages to load variable which means that the box with the best price within + * these pages is returnd. + * Clients should implement an own logic to retrieve babel fee boxes if needed. * * @param ctx current blockchain context * @param loader loader to receive unspent boxes * @param tokenId tokenId offered to swap * @param feeAmount nanoErg amount needed to swap + * @param maxPagesToLoadForPriceSearch number of pages to load to search for best price, or 0 to + * load all pages * @return babel fee box satisfying the needs, or null if none available */ @Nullable - public static InputBox findBabelFeeBox(BlockchainContext ctx, BoxOperations.IUnspentBoxesLoader loader, ErgoId tokenId, long feeAmount) { + public static InputBox findBabelFeeBox( + BlockchainContext ctx, + BoxOperations.IUnspentBoxesLoader loader, + ErgoId tokenId, + long feeAmount, + int maxPagesToLoadForPriceSearch) { ErgoContract contractForToken = new ErgoTreeContract(new BabelFeeBoxContract(tokenId).getErgoTree(), ctx.getNetworkType()); Address address = contractForToken.toAddress(); loader.prepare(ctx, Collections.singletonList(address), feeAmount, new ArrayList<>()); @@ -108,8 +116,8 @@ public static InputBox findBabelFeeBox(BlockchainContext ctx, BoxOperations.IUns InputBox returnBox = null; long pricePerToken = Long.MAX_VALUE; - while ((page == 0 || !inputBoxes.isEmpty()) && returnBox == null) { - inputBoxes = loader.loadBoxesPage(ctx, address, 0); + while ((page == 0 || !inputBoxes.isEmpty()) && (returnBox == null || maxPagesToLoadForPriceSearch == 0 || page < maxPagesToLoadForPriceSearch)) { + inputBoxes = loader.loadBoxesPage(ctx, address, page); // find the cheapest box satisfying our fee amount needs for (InputBox inputBox : inputBoxes) { @@ -123,8 +131,6 @@ public static InputBox findBabelFeeBox(BlockchainContext ctx, BoxOperations.IUns } page++; - // get another page - inputBoxes = loader.loadBoxesPage(ctx, address, page); } return returnBox; From e4400fc09a1772bb171666251f37275001f9b588 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Wed, 9 Nov 2022 17:23:41 +0100 Subject: [PATCH 35/44] Linked to EIP version from PR --- .../java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java index 6ca92047..a368a07a 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java @@ -18,7 +18,7 @@ /** * Represents a Babel Fee Box state, see EIP-0031 - * https://github.com/ergoplatform/eips/blob/master/eip-0031.md + * https://github.com/ergoplatform/eips/blob/81edece9d2589a80c09baa33be26922a125a4cb4/eip-0031.md *

* The term “babel fees“ refers to the concept of paying transaction fees in tokens instead of * platform’s primary token (ERG). It is a contract that buys tokens and pays ERG, suitable to be From 93faed413fc73f6e66a3a8a32677e7d6cdb83df4 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Thu, 10 Nov 2022 19:28:07 +0100 Subject: [PATCH 36/44] eip-31-babelfees: fix box loading + formatting --- .../org/ergoplatform/appkit/BabelFeeSpec.scala | 3 ++- .../appkit/babelfee/BabelFeeBoxContract.java | 8 +++++--- .../appkit/babelfee/BabelFeeOperations.java | 15 +++++++++++---- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala index 869d478c..4e54240e 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala @@ -110,7 +110,8 @@ class BabelFeeSpec extends PropSpec with Matchers with ScalaCheckDrivenPropertyC val tockenId = ErgoId.create(mockTokenId) // find no boxes - val babelBox1 = BabelFeeOperations.findBabelFeeBox(ctx, new MockedBoxesLoader(new util.ArrayList[InputBox]()), + val babelBox1 = BabelFeeOperations.findBabelFeeBox(ctx, + new MockedBoxesLoader(new util.ArrayList[InputBox]()), tockenId, Parameters.MinFee, 1) babelBox1 shouldBe (null) diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java index 756ff176..0c231e83 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxContract.java @@ -38,9 +38,11 @@ public BabelFeeBoxContract(ErgoId tokenId) { public BabelFeeBoxContract(Values.ErgoTree ergoTree) { this.ergoTree = ergoTree; - tokenId = new ErgoId(ScalaHelpers.collByteToByteArray((Coll) ErgoTreeTemplate.fromErgoTree(ergoTree) - .withParameterPositions(contractParameterPositions) - .getParameterValue(0).getValue())); + tokenId = new ErgoId(ScalaHelpers.collByteToByteArray( + (Coll) ErgoTreeTemplate.fromErgoTree(ergoTree) + .withParameterPositions(contractParameterPositions) + .getParameterValue(0).getValue() + )); } public Values.ErgoTree getErgoTree() { diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java index 4732dfc4..b7a6e96c 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java @@ -106,7 +106,8 @@ public static InputBox findBabelFeeBox( ErgoId tokenId, long feeAmount, int maxPagesToLoadForPriceSearch) { - ErgoContract contractForToken = new ErgoTreeContract(new BabelFeeBoxContract(tokenId).getErgoTree(), ctx.getNetworkType()); + ErgoContract contractForToken = new ErgoTreeContract( + new BabelFeeBoxContract(tokenId).getErgoTree(), ctx.getNetworkType()); Address address = contractForToken.toAddress(); loader.prepare(ctx, Collections.singletonList(address), feeAmount, new ArrayList<>()); @@ -114,17 +115,23 @@ public static InputBox findBabelFeeBox( List inputBoxes = null; InputBox returnBox = null; - long pricePerToken = Long.MAX_VALUE; + long minPricePerToken = Long.MAX_VALUE; - while ((page == 0 || !inputBoxes.isEmpty()) && (returnBox == null || maxPagesToLoadForPriceSearch == 0 || page < maxPagesToLoadForPriceSearch)) { + while ((page == 0 || !inputBoxes.isEmpty()) && + (returnBox == null || + maxPagesToLoadForPriceSearch == 0 || + page < maxPagesToLoadForPriceSearch)) { inputBoxes = loader.loadBoxesPage(ctx, address, page); // find the cheapest box satisfying our fee amount needs for (InputBox inputBox : inputBoxes) { try { BabelFeeBoxState babelFeeBoxState = new BabelFeeBoxState(inputBox); - if (babelFeeBoxState.getValueAvailableToBuy() >= feeAmount && babelFeeBoxState.getPricePerToken() < pricePerToken) + long priceFromBox = babelFeeBoxState.getPricePerToken(); + if (babelFeeBoxState.getValueAvailableToBuy() >= feeAmount && priceFromBox < minPricePerToken) { returnBox = inputBox; + minPricePerToken = priceFromBox; + } } catch (Throwable t) { // ignore, check next } From 882c628337f969018f6c1abca8a60f6c13307b09 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Fri, 11 Nov 2022 21:46:25 +0100 Subject: [PATCH 37/44] Revert "Linked to EIP version from PR" This reverts commit e4400fc09a1772bb171666251f37275001f9b588. --- .../java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java index a368a07a..6ca92047 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeBoxState.java @@ -18,7 +18,7 @@ /** * Represents a Babel Fee Box state, see EIP-0031 - * https://github.com/ergoplatform/eips/blob/81edece9d2589a80c09baa33be26922a125a4cb4/eip-0031.md + * https://github.com/ergoplatform/eips/blob/master/eip-0031.md *

* The term “babel fees“ refers to the concept of paying transaction fees in tokens instead of * platform’s primary token (ERG). It is a contract that buys tokens and pays ERG, suitable to be From 052a7a80267533424357412fdda134207944abc7 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Fri, 11 Nov 2022 21:47:29 +0100 Subject: [PATCH 38/44] EIP-31 BabelFeeOperations best price for babel fees is the highest price, not the lowest --- .../ergoplatform/appkit/babelfee/BabelFeeOperations.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java index b7a6e96c..86311701 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/babelfee/BabelFeeOperations.java @@ -115,7 +115,7 @@ public static InputBox findBabelFeeBox( List inputBoxes = null; InputBox returnBox = null; - long minPricePerToken = Long.MAX_VALUE; + long bestPricePerToken = 0; while ((page == 0 || !inputBoxes.isEmpty()) && (returnBox == null || @@ -128,9 +128,9 @@ public static InputBox findBabelFeeBox( try { BabelFeeBoxState babelFeeBoxState = new BabelFeeBoxState(inputBox); long priceFromBox = babelFeeBoxState.getPricePerToken(); - if (babelFeeBoxState.getValueAvailableToBuy() >= feeAmount && priceFromBox < minPricePerToken) { + if (babelFeeBoxState.getValueAvailableToBuy() >= feeAmount && priceFromBox > bestPricePerToken) { returnBox = inputBox; - minPricePerToken = priceFromBox; + bestPricePerToken = priceFromBox; } } catch (Throwable t) { // ignore, check next From 890f27d76075c3a95fd362ad7713c758afd7d959 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Sat, 12 Nov 2022 15:52:41 +0100 Subject: [PATCH 39/44] develop: test vectors for ErgoValue toHex/fromHex for pairs --- .../org/ergoplatform/appkit/ErgoValueSpec.scala | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/common/src/test/scala/org/ergoplatform/appkit/ErgoValueSpec.scala b/common/src/test/scala/org/ergoplatform/appkit/ErgoValueSpec.scala index 3e63ca46..3c890b62 100644 --- a/common/src/test/scala/org/ergoplatform/appkit/ErgoValueSpec.scala +++ b/common/src/test/scala/org/ergoplatform/appkit/ErgoValueSpec.scala @@ -6,6 +6,7 @@ import sigmastate.Values.Constant import sigmastate.serialization.ValueSerializer import sigmastate.serialization.generators.ObjectGenerators import JavaHelpers._ +import sigmastate.eval.Evaluation.fromDslTuple import special.collection.Coll class ErgoValueSpec extends TestingBase with AppkitTestingCommon with ObjectGenerators { @@ -36,7 +37,20 @@ class ErgoValueSpec extends TestingBase with AppkitTestingCommon with ObjectGene val t = ErgoType.collType(ErgoType.byteType) val collV = ErgoValue.of(coll.convertTo[Coll[Coll[java.lang.Byte]]], t) collV.toHex shouldBe hex + } + + property("ErgoValue with pair (hex test vector)") { + val tuple = (10.toByte, 20L) + val tupSType = STuple(SByte, SLong) + val c = Constant[STuple](fromDslTuple(tuple, tupSType), tupSType) + val hex = constToHex(c) + hex shouldBe "3e050a28" + val t = ErgoType.pairType(ErgoType.byteType, ErgoType.longType()) + val tupleV = ErgoValue.pairOf(ErgoValue.of(tuple._1), ErgoValue.of(tuple._2)) + tupleV.getType shouldBe t + tupleV.toHex shouldBe hex + ErgoValue.fromHex(hex) shouldBe tupleV } property("fromHex/toHex roundtrip") { From 94e54d4f9ab46dfece3957f6797819a876b89a01 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Mon, 14 Nov 2022 12:51:31 +0100 Subject: [PATCH 40/44] appkit-5.0: fixes after merge --- build.sbt | 4 ++-- .../src/main/java/org/ergoplatform/appkit/JavaHelpers.scala | 2 +- .../scala/org/ergoplatform/appkit/ErgoTreeTemplateSpec.scala | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.sbt b/build.sbt index 192c46c6..3cf9270b 100644 --- a/build.sbt +++ b/build.sbt @@ -126,8 +126,8 @@ assemblyMergeStrategy in assembly := { lazy val allConfigDependency = "compile->compile;test->test" -val sigmaStateVersion = "5.0.0" -val ergoWalletVersion = "4.0.104" +val sigmaStateVersion = "5.0.1" +val ergoWalletVersion = "5.0.2" 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 52f7a8a9..2acd336b 100644 --- a/common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala +++ b/common/src/main/java/org/ergoplatform/appkit/JavaHelpers.scala @@ -343,7 +343,7 @@ object JavaHelpers { */ def substituteErgoTreeConstants(ergoTreeBytes: Array[Byte], positions: Array[Int], newValues: Array[ErgoValue[_]]): ErgoTree = { val (newBytes, _) = ErgoTreeSerializer.DefaultSerializer.substituteConstants( - ergoTreeBytes, positions, newValues.map(Iso.isoErgoValueToSValue.to)) + ergoTreeBytes, positions, newValues.map(v => Iso.isoErgoValueToSValue.to(v).asInstanceOf[Constant[SType]])) ErgoTreeSerializer.DefaultSerializer.deserializeErgoTree(newBytes) } diff --git a/common/src/test/scala/org/ergoplatform/appkit/ErgoTreeTemplateSpec.scala b/common/src/test/scala/org/ergoplatform/appkit/ErgoTreeTemplateSpec.scala index 2fa26f45..0ba3f7ed 100644 --- a/common/src/test/scala/org/ergoplatform/appkit/ErgoTreeTemplateSpec.scala +++ b/common/src/test/scala/org/ergoplatform/appkit/ErgoTreeTemplateSpec.scala @@ -45,7 +45,7 @@ class ErgoTreeTemplateSpec extends TestingBase assertExceptionThrown( template.applyParameters(ErgoValue.of(1.toByte)), // invalid type of ErgoValue (should be Int) - exceptionLike[AssertionError]() + exceptionLike[IllegalArgumentException]("expected new constant to have the same SInt$ tpe, got SByte$") ) } From eedc1095d10a8661f652d3b306a2c4b2acde1173 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Mon, 14 Nov 2022 12:58:54 +0100 Subject: [PATCH 41/44] appkit-5.0: update sigma to v5.0.2 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 3cf9270b..50b4d017 100644 --- a/build.sbt +++ b/build.sbt @@ -126,7 +126,7 @@ assemblyMergeStrategy in assembly := { lazy val allConfigDependency = "compile->compile;test->test" -val sigmaStateVersion = "5.0.1" +val sigmaStateVersion = "5.0.2" val ergoWalletVersion = "5.0.2" lazy val sigmaState = ("org.scorexfoundation" %% "sigma-state" % sigmaStateVersion).force() .exclude("ch.qos.logback", "logback-classic") From 80b9061dd9ab92fe430a3da7e13eec2193b632a8 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Mon, 14 Nov 2022 13:40:48 +0100 Subject: [PATCH 42/44] closes #199 --- .../test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala index baff948b..0e0e6a6b 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala @@ -544,17 +544,19 @@ class TxBuilderSpec extends PropSpec with Matchers .withInputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))) .putToContractTxUnsigned(pkContract) - // this fails due to token burning check - instead, tokens should be in change box FIXME + // check if this succeeds without token burning, but with tokens in change box + // otherwise exception would be raised val prover = ctx.newProverBuilder.build // prover without secrets val reduced = prover.reduce(unsigned, 0) - // this fails with NotEnoughTokensException, although there are enough tokens available + // check if this suceeds finding all tokens and not raising any exception val spendAllTokens = BoxOperations.createForSenders(senders, ctx) .withAmountToSpend(amountToSend) .withTokensToSpend(Arrays.asList(new ErgoToken(mockTxId, 2))) .withInputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))) .putToContractTxUnsigned(pkContract) + val reduced2 = prover.reduce(spendAllTokens, 0) } } From 3bcbfc50f57d4752314d2e4ee4dae192e2d2a9c3 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Mon, 14 Nov 2022 14:08:08 +0100 Subject: [PATCH 43/44] #199 Test same token multiple times add expected token sum --- .../test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala index 0e0e6a6b..8214e31c 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala @@ -549,6 +549,13 @@ class TxBuilderSpec extends PropSpec with Matchers val prover = ctx.newProverBuilder.build // prover without secrets val reduced = prover.reduce(unsigned, 0) + // outputs should contain the two tokens going in + unsigned.getOutputs.convertTo[IndexedSeq[OutBox]] + .map(_.getTokens.convertTo[IndexedSeq[ErgoToken]]) + .flatten(identity) + .filter(_.getId.toString.equals(mockTxId)) + .map(_.getValue).sum shouldBe 2L + // check if this suceeds finding all tokens and not raising any exception val spendAllTokens = BoxOperations.createForSenders(senders, ctx) .withAmountToSpend(amountToSend) From 1d7503595eab13f8762efa36c426ad61dbfd58ce Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Mon, 14 Nov 2022 16:23:52 +0100 Subject: [PATCH 44/44] Update MIGRATION --- MIGRATION | 2 -- 1 file changed, 2 deletions(-) diff --git a/MIGRATION b/MIGRATION index 490236c2..d1370644 100644 --- a/MIGRATION +++ b/MIGRATION @@ -1,7 +1,5 @@ [5.0.0] - block version now required to construct ColdErgoClient - -[4.0.12] - getBoxById(String boxId) was replaced by getBoxById(String boxId, boolean findInPool, boolean findInSpent) the old behaviour (returning only confirmed unspent boxes) can be achieved by calling getBoxById(boxId, false, false)