From 7500bfc8983576d008291de0b68dd6c9fe3b9b2d Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 14 Dec 2025 10:23:17 +0530 Subject: [PATCH 1/6] feat: standardize and deprecate builder methods across `UnsignedTransactionBuilder`, `BoxOperations`, and `Address` for improved consistency. --- .../scala/org/ergoplatform/appkit/AnonymousAccessSpec.scala | 2 +- .../ergoplatform/appkit/AppkitProvingInterpreterSpec.scala | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/AnonymousAccessSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/AnonymousAccessSpec.scala index d8c914dc..376db4ba 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/AnonymousAccessSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/AnonymousAccessSpec.scala @@ -116,7 +116,7 @@ object DhtUtils { | proveDHTuple(groupGenerator, g_y, g_x, g_xy) // for alice |}""".stripMargin); - val dhtBoxCreationTx = BoxOperations.createForProver(sender, ctx).withAmountToSpend(amountToSend).putToContractTx(contract) + val dhtBoxCreationTx = BoxOperations.createForProver(sender, ctx).value(amountToSend).putToContractTx(contract) dhtBoxCreationTx } diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/AppkitProvingInterpreterSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/AppkitProvingInterpreterSpec.scala index b9d162e4..d86bbc80 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/AppkitProvingInterpreterSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/AppkitProvingInterpreterSpec.scala @@ -34,7 +34,7 @@ class AppkitProvingInterpreterSpec extends AnyPropSpec is } } - ops.withAmountToSpend(oneErg * 2) + ops.value(oneErg * 2) } /** This method creates an UnsignedTransaction instance directly bypassing builders and @@ -92,7 +92,7 @@ class AppkitProvingInterpreterSpec extends AnyPropSpec tokens.add(ergoToken1) tokens.add(ergoToken2) val unsigned = ops - .withTokensToSpend(tokens) + .tokens(tokens) .putToContractTxUnsigned(address.toErgoContract) val reduced = prover.reduce(unsigned, 0) reduced.getInputBoxesIds.size() shouldBe 2 From a8bddfbd0097a3c404d1cd1c2ec9b432b7180b3a Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 14 Dec 2025 10:23:41 +0530 Subject: [PATCH 2/6] refactor: improve Java API for `UnsignedTransactionBuilder` and `BoxOperations` builder methods, and rename `Address` accessors. --- .../ergoplatform/appkit/BabelFeeSpec.scala | 6 +-- .../ergoplatform/appkit/TxBuilderSpec.scala | 38 +++++++++---------- 2 files changed, 22 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 b581a42d..7028de82 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/BabelFeeSpec.scala @@ -29,8 +29,8 @@ class BabelFeeSpec extends AnyPropSpec with Matchers with ScalaCheckDrivenProper val txCreate = BabelFeeOperations.createNewBabelContractTx( BoxOperations.createForSender(creator, ctx) - .withAmountToSpend(amountToSend) - .withInputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))), + .value(amountToSend) + .inputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))), ErgoId.create(mockTokenId), Parameters.OneErg ) @@ -42,7 +42,7 @@ class BabelFeeSpec extends AnyPropSpec with Matchers with ScalaCheckDrivenProper // now we cancel the babel box val txCancel = BabelFeeOperations.cancelBabelFeeContract( BoxOperations.createForSender(creator, ctx) - .withInputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))), + .inputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))), babelFeeErgoBox) ctx.newProverBuilder() diff --git a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala index ef942418..c57e8730 100644 --- a/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala +++ b/appkit/src/test/scala/org/ergoplatform/appkit/TxBuilderSpec.scala @@ -273,7 +273,7 @@ class TxBuilderSpec extends AnyPropSpec with Matchers val recipient = senderProver.getEip3Addresses.get(1) val amountToSend = 1000000 - val signed = BoxOperations.createForProver(senderProver, ctx).withAmountToSpend(amountToSend) + val signed = BoxOperations.createForProver(senderProver, ctx).value(amountToSend) .send(recipient) assert(signed != null) } @@ -290,8 +290,8 @@ class TxBuilderSpec extends AnyPropSpec with Matchers val amountToSend = 1000000 val unsigned = BoxOperations.createForSenders(senders, ctx) - .withAmountToSpend(amountToSend) - .withMessage("Test message") + .value(amountToSend) + .message("Test message") .putToContractTxUnsigned(pkContract) unsigned.getOutputs.get(0).getRegisters.size() shouldBe 6 unsigned.getOutputs.get(0).getAttachment.getType shouldBe BoxAttachment.Type.PLAIN_TEXT @@ -358,8 +358,8 @@ class TxBuilderSpec extends AnyPropSpec with Matchers val amountToSend = 1000000 val unsigned = BoxOperations.createForSenders(senders, ctx) - .withAmountToSpend(amountToSend) - .withInputBoxesLoader(new ExplorerAndPoolUnspentBoxesLoader()) + .value(amountToSend) + .inputBoxesLoader(new ExplorerAndPoolUnspentBoxesLoader()) .putToContractTxUnsigned(recipientContract) val prover = ctx.newProverBuilder.build // prover without secrets @@ -395,9 +395,9 @@ class TxBuilderSpec extends AnyPropSpec with Matchers .build().convertToInputWith(mockTxId, 1) val operations = BoxOperations.createForSenders(senders, ctx) - .withAmountToSpend(amountToSend) - .withTokensToSpend(Arrays.asList(new ErgoToken(mockTxId, 1))) - .withInputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1, input2))) + .value(amountToSend) + .tokens(Arrays.asList(new ErgoToken(mockTxId, 1))) + .inputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1, input2))) val inputsSelected = operations.loadTop() // both boxes should be selected @@ -405,14 +405,14 @@ class TxBuilderSpec extends AnyPropSpec with Matchers // if we restrict to a single box, we face InputBoxLimitExceededException assertExceptionThrown( - operations.withMaxInputBoxesToSelect(1).loadTop(), + operations.maxInputBoxes(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))) + .value(amountToSend) + .inputBoxesLoader(new MockedBoxesLoader(util.Arrays.asList(input1))) assertExceptionThrown( operations2.loadTop(), @@ -499,8 +499,8 @@ class TxBuilderSpec extends AnyPropSpec with Matchers .build().convertToInputWith(mockTxId, 1) val operations = BoxOperations.createForSenders(senders, ctx) - .withAmountToSpend(amountToSend) - .withInputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1, input2))) + .value(amountToSend) + .inputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1, input2))) val unsigned = operations.putToContractTxUnsigned(pkContract) // all outputs should have 100 tokens at max, and it should contain all input tokens @@ -545,8 +545,8 @@ class TxBuilderSpec extends AnyPropSpec with Matchers .build().convertToInputWith(mockTxId, 0) val unsigned = BoxOperations.createForSenders(senders, ctx) - .withAmountToSpend(amountToSend) - .withInputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))) + .value(amountToSend) + .inputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))) .putToContractTxUnsigned(pkContract) // check if this succeeds without token burning, but with tokens in change box @@ -563,9 +563,9 @@ class TxBuilderSpec extends AnyPropSpec with Matchers // 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))) + .value(amountToSend) + .tokens(Arrays.asList(new ErgoToken(mockTxId, 2))) + .inputBoxesLoader(new MockedBoxesLoader(Arrays.asList(input1))) .putToContractTxUnsigned(pkContract) val reduced2 = prover.reduce(spendAllTokens, 0) @@ -618,7 +618,7 @@ class TxBuilderSpec extends AnyPropSpec with Matchers property("Mint a token and rebuild it from BoxCandidate") { val ergoClient = createMockedErgoClient(data) ergoClient.execute { ctx: BlockchainContext => - val unsigned = BoxOperations.createForSender(address, ctx).withAmountToSpend(15000000) + val unsigned = BoxOperations.createForSender(address, ctx).value(15000000) .mintTokenToContractTxUnsigned(new ErgoTreeContract(address.getErgoAddress.script, address.getNetworkType), { tokenId: String => new Eip4Token(tokenId, 1, "Test name", "Test desc", 0) }) From 57f386d9388e866b31aebb69c3e4172b3354e5d8 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 14 Dec 2025 10:23:57 +0530 Subject: [PATCH 3/6] feat: Refactor API by renaming `Address` and `BoxOperations` methods and adding list-based arguments to `UnsignedTransactionBuilder`. --- .../java/org/ergoplatform/appkit/Address.java | 30 +++++++++++++++++-- .../ergoplatform/appkit/BoxOperations.java | 20 ++++++------- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/common/src/main/java/org/ergoplatform/appkit/Address.java b/common/src/main/java/org/ergoplatform/appkit/Address.java index c5104e80..bf55c0c3 100644 --- a/common/src/main/java/org/ergoplatform/appkit/Address.java +++ b/common/src/main/java/org/ergoplatform/appkit/Address.java @@ -84,11 +84,25 @@ public NetworkType getNetworkType() { * @return underlying {@link P2PKAddress}. * @throws IllegalArgumentException if this instance is not P2PK address */ - public P2PKAddress asP2PK() { + /** + * @return underlying {@link P2PKAddress}. + * @throws IllegalArgumentException if this instance is not P2PK address + */ + public P2PKAddress toP2PK() { InternalUtil.checkArgument(isP2PK(), "This instance %s is not P2PKAddress", this); return (P2PKAddress) _address; } + /** + * @return underlying {@link P2PKAddress}. + * @throws IllegalArgumentException if this instance is not P2PK address + * @deprecated use {@link #toP2PK()} + */ + @Deprecated + public P2PKAddress asP2PK() { + return toP2PK(); + } + /** * @return true if this address has Pay-To-Script type. */ @@ -98,11 +112,21 @@ public P2PKAddress asP2PK() { * @return underlying {@link Pay2SAddress}. * @throws IllegalArgumentException if this instance is not P2S address */ - public Pay2SAddress asP2S() { + public Pay2SAddress toP2S() { InternalUtil.checkArgument(isP2S(), "This instance %s is not Pay2SAddress", this); return (Pay2SAddress) _address; } + /** + * @return underlying {@link Pay2SAddress}. + * @throws IllegalArgumentException if this instance is not P2S address + * @deprecated use {@link #toP2S()} + */ + @Deprecated + public Pay2SAddress asP2S() { + return toP2S(); + } + /** * Obtain an instance of {@link ErgoAddress} related to this Address instance. * @@ -115,7 +139,7 @@ public ErgoAddress getErgoAddress() { /** * Extract public key from P2PKAddress. */ - public DLogProtocol.ProveDlog getPublicKey() { return asP2PK().pubkey(); } + public DLogProtocol.ProveDlog getPublicKey() { return toP2PK().pubkey(); } /** * Extract public key from P2PKAddress and return its group element 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 2a24ba3e..c3e33d85 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/BoxOperations.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/BoxOperations.java @@ -77,7 +77,7 @@ public static BoxOperations createForProver(ErgoProver senderProver, BlockchainC /** * @param amountToSpend nanoerg value to be collected in inboxes */ - public BoxOperations withAmountToSpend(long amountToSpend) { + public BoxOperations value(long amountToSpend) { if (amountToSpend < 0) { throw new IllegalArgumentException("Amount to send must be >= 0"); } @@ -89,7 +89,7 @@ public BoxOperations withAmountToSpend(long amountToSpend) { /** * @param tokensToSpend tokens to be collected in inboxes */ - public BoxOperations withTokensToSpend(@Nonnull List tokensToSpend) { + public BoxOperations tokens(@Nonnull List tokensToSpend) { this.tokensToSpend = tokensToSpend; return this; } @@ -97,7 +97,7 @@ public BoxOperations withTokensToSpend(@Nonnull List tokensToSpend) { /** * @param feeAmount fee amount in nanoerg to be used for generated transactions */ - public BoxOperations withFeeAmount(long feeAmount) { + public BoxOperations fee(long feeAmount) { if (feeAmount < MinFee) { throw new IllegalArgumentException("Amount to send must be >= " + MinFee); } @@ -109,7 +109,7 @@ public BoxOperations withFeeAmount(long feeAmount) { /** * @param attachment attachment to be set for outboxes */ - public BoxOperations withAttachment(@Nullable BoxAttachment attachment) { + public BoxOperations attachment(@Nullable BoxAttachment attachment) { this.attachment = attachment; return this; } @@ -126,7 +126,7 @@ public int getMaxInputBoxesToSelect() { * if set to <= 0 (or not set), there is no input box restriction * checked by loadTop. */ - public BoxOperations withMaxInputBoxesToSelect(int maxInputBoxesToSelect) { + public BoxOperations maxInputBoxes(int maxInputBoxesToSelect) { this.maxInputBoxesToSelect = maxInputBoxesToSelect; return this; } @@ -134,11 +134,11 @@ public BoxOperations withMaxInputBoxesToSelect(int maxInputBoxesToSelect) { /** * @param message message to be set for outboxes as {@link BoxAttachmentPlainText} */ - public BoxOperations withMessage(@Nullable String message) { + public BoxOperations message(@Nullable String message) { if (message != null) { - withAttachment(BoxAttachmentPlainText.buildForText(message)); + attachment(BoxAttachmentPlainText.buildForText(message)); } else { - withAttachment(null); + attachment(null); } return this; } @@ -148,7 +148,7 @@ public BoxOperations withMessage(@Nullable String message) { * See {@link IUnspentBoxesLoader for more information} * Default is {@link ExplorerApiUnspentLoader} */ - public BoxOperations withInputBoxesLoader(@Nonnull IUnspentBoxesLoader inputBoxesSource) { + public BoxOperations inputBoxesLoader(@Nonnull IUnspentBoxesLoader inputBoxesSource) { this.inputBoxesLoader = inputBoxesSource; return this; } @@ -231,7 +231,7 @@ public String send(Address recipient) { * list. * The list is then used to select covering boxes. * - * The method respects a max amount of boxes to be selected set by {@link #withMaxInputBoxesToSelect(int)}. + * The method respects a max amount of boxes to be selected set by {@link #maxInputBoxes(int)}. * If this limit is exceeded, a {@link org.ergoplatform.appkit.InputBoxesSelectionException.InputBoxLimitExceededException} * is thrown and no further boxes are loaded. * From 198696e8a725c58435330577f0d5963ca354aaea Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 14 Dec 2025 10:24:34 +0530 Subject: [PATCH 4/6] feat: Add List-based methods for transaction inputs, data inputs, outputs, and tokens to burn, and remove non-empty token validation. --- .../appkit/UnsignedTransactionBuilder.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) 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 0385f3cd..9e4acbf1 100644 --- a/lib-api/src/main/java/org/ergoplatform/appkit/UnsignedTransactionBuilder.java +++ b/lib-api/src/main/java/org/ergoplatform/appkit/UnsignedTransactionBuilder.java @@ -35,6 +35,15 @@ public interface UnsignedTransactionBuilder { */ UnsignedTransactionBuilder addInputs(InputBox... boxes); + /** + * Adds input boxes to an already specified list of inputs or, if no input boxes defined yet, + * 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. + */ + UnsignedTransactionBuilder addInputs(List boxes); + /** * @deprecated use {@link #addInputs(InputBox...)} */ @@ -53,6 +62,14 @@ public interface UnsignedTransactionBuilder { */ UnsignedTransactionBuilder addDataInputs(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. + * + * @param boxes list of boxes to be used as data-inputs by the transaction. + */ + UnsignedTransactionBuilder addDataInputs(List boxes); + /** * @deprecated use {@link #addDataInputs(InputBox...)} */ @@ -77,6 +94,14 @@ public interface UnsignedTransactionBuilder { */ UnsignedTransactionBuilder addOutputs(OutBox... outBoxes); + /** + * Adds output boxes to an already specified list of outputs or, if no output boxes defined yet, + * as the boxes to be output. The order is preserved. + * + * @param outBoxes output boxes created by the transaction + */ + UnsignedTransactionBuilder addOutputs(List outBoxes); + /** * Adds transaction fee output. * @@ -98,6 +123,13 @@ public interface UnsignedTransactionBuilder { */ UnsignedTransactionBuilder tokensToBurn(ErgoToken... tokens); + /** + * Configures amounts for tokens to be burnt. + * + * @param tokens one or more tokens to be burnt as part of the transaction. + */ + UnsignedTransactionBuilder tokensToBurn(List tokens); + /** * Adds change output to the specified address if needed. * From e7b26d4f3e68ad52baf16c46646a71bba3f68fef Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 14 Dec 2025 10:25:56 +0530 Subject: [PATCH 5/6] feat: Introduce `java.util.List` overloads for input, data input, output, and token burning methods in `UnsignedTransactionBuilder` and remove empty token check in `OutBoxBuilder`. --- .../java/org/ergoplatform/appkit/impl/OutBoxBuilderImpl.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/OutBoxBuilderImpl.scala b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/OutBoxBuilderImpl.scala index 00420035..b9b92343 100644 --- a/lib-impl/src/main/java/org/ergoplatform/appkit/impl/OutBoxBuilderImpl.scala +++ b/lib-impl/src/main/java/org/ergoplatform/appkit/impl/OutBoxBuilderImpl.scala @@ -25,7 +25,6 @@ class OutBoxBuilderImpl(_txB: UnsignedTransactionBuilderImpl) extends OutBoxBuil } override def tokens(tokens: ErgoToken*): OutBoxBuilderImpl = { - require(tokens.nonEmpty, "At least one token should be specified") val maxTokens = SigmaConstants.MaxTokens.value require(tokens.size <= maxTokens, SigmaConstants.MaxTokens.description + s": $maxTokens") _tokens ++= tokens From 1fc636ccb3893b2db815e7fb203eab2d2fcf7480 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 14 Dec 2025 10:26:17 +0530 Subject: [PATCH 6/6] feat: Add `addInputs`, `addDataInputs`, `addOutputs`, `tokensToBurn` methods and update existing API to accept `java.util.List`. --- .../impl/UnsignedTransactionBuilderImpl.scala | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) 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 69a3e971..f2dfe1d3 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 @@ -38,7 +38,12 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un this } - override def boxesToSpend(inputBoxes: List[InputBox]): UnsignedTransactionBuilder = { + override def addInputs(boxes: util.List[InputBox]): UnsignedTransactionBuilder = { + addInputs(JavaHelpers.toIndexedSeq(boxes): _*) + this + } + + override def boxesToSpend(inputBoxes: util.List[InputBox]): UnsignedTransactionBuilder = { require(_inputs.isEmpty, "inputs already specified") addInputs(JavaHelpers.toIndexedSeq(inputBoxes): _*) this @@ -51,7 +56,12 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un this } - override def withDataInputs(inputBoxes: List[InputBox]): UnsignedTransactionBuilder = { + override def addDataInputs(boxes: util.List[InputBox]): UnsignedTransactionBuilder = { + addDataInputs(JavaHelpers.toIndexedSeq(boxes): _*) + this + } + + override def withDataInputs(inputBoxes: util.List[InputBox]): UnsignedTransactionBuilder = { require(_dataInputs.isEmpty, "dataInputs list is already specified") addDataInputs(JavaHelpers.toIndexedSeq(inputBoxes): _*) this @@ -64,6 +74,11 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un this } + override def addOutputs(outBoxes: util.List[OutBox]): UnsignedTransactionBuilder = { + addOutputs(JavaHelpers.toIndexedSeq(outBoxes): _*) + this + } + override def outputs(outputs: OutBox*): UnsignedTransactionBuilder = { require(_outputs.isEmpty, "Outputs already specified.") addOutputs(outputs: _*) @@ -86,6 +101,12 @@ class UnsignedTransactionBuilderImpl(val _ctx: BlockchainContextImpl) extends Un this } + override def tokensToBurn(tokens: util.List[ErgoToken]): UnsignedTransactionBuilder = { + require(_tokensToBurn.isEmpty, "Tokens to burn already specified.") + _tokensToBurn = Some(new util.ArrayList[ErgoToken](tokens)) + this + } + override def sendChangeTo(changeAddress: ErgoAddress): UnsignedTransactionBuilder = { require(_changeAddress.isEmpty, "Change address is already specified") _changeAddress = Some(changeAddress)