From 65bdf00adf7552b04ef101a20c73130b19c68d06 Mon Sep 17 00:00:00 2001 From: Michael Diamant Date: Wed, 2 Nov 2022 08:53:04 -0400 Subject: [PATCH] Boxes: Add support for Boxes (#345) --- .../ApplicationBaseTransactionBuilder.java | 17 + .../ApplicationCallReferencesSetter.java | 9 +- .../MethodCallTransactionBuilder.java | 52 +- .../algosdk/transaction/AppBoxReference.java | 55 ++ .../algosdk/transaction/MethodCallParams.java | 92 ++- .../algosdk/transaction/Transaction.java | 771 ++++++++++-------- .../algosdk/util/BoxQueryEncoding.java | 29 + .../client/algod/GetApplicationBoxByName.java | 82 ++ .../v2/client/algod/GetApplicationBoxes.java | 76 ++ .../algosdk/v2/client/common/AlgodClient.java | 24 + .../algosdk/v2/client/common/Client.java | 2 +- .../v2/client/common/IndexerClient.java | 23 + .../algosdk/v2/client/common/Query.java | 5 +- .../LookupApplicationBoxByIDAndName.java | 82 ++ .../indexer/SearchForApplicationBoxes.java | 83 ++ .../algosdk/v2/client/model/Account.java | 16 + .../algorand/algosdk/v2/client/model/Box.java | 50 ++ .../v2/client/model/BoxDescriptor.java | 37 + .../v2/client/model/BoxesResponse.java | 44 + src/test/integration.tags | 2 +- .../cucumber/shared/TransactionSteps.java | 7 +- .../algosdk/integration/Applications.java | 114 ++- .../algorand/algosdk/integration/Clients.java | 8 +- .../transaction/TestAppBoxReference.java | 36 + .../algosdk/transaction/TestBoxReference.java | 77 ++ .../algosdk/transaction/TestTransaction.java | 35 +- .../com/algorand/algosdk/unit/AlgodPaths.java | 28 +- .../algorand/algosdk/unit/IndexerPaths.java | 12 + .../algosdk/util/ComparableBytes.java | 31 + .../algosdk/util/ConversionUtils.java | 42 +- .../algosdk/util/TestBoxQueryEncoding.java | 64 ++ src/test/unit.tags | 1 + 32 files changed, 1605 insertions(+), 401 deletions(-) create mode 100644 src/main/java/com/algorand/algosdk/transaction/AppBoxReference.java create mode 100644 src/main/java/com/algorand/algosdk/util/BoxQueryEncoding.java create mode 100644 src/main/java/com/algorand/algosdk/v2/client/algod/GetApplicationBoxByName.java create mode 100644 src/main/java/com/algorand/algosdk/v2/client/algod/GetApplicationBoxes.java create mode 100644 src/main/java/com/algorand/algosdk/v2/client/indexer/LookupApplicationBoxByIDAndName.java create mode 100644 src/main/java/com/algorand/algosdk/v2/client/indexer/SearchForApplicationBoxes.java create mode 100644 src/main/java/com/algorand/algosdk/v2/client/model/Box.java create mode 100644 src/main/java/com/algorand/algosdk/v2/client/model/BoxDescriptor.java create mode 100644 src/main/java/com/algorand/algosdk/v2/client/model/BoxesResponse.java create mode 100644 src/test/java/com/algorand/algosdk/transaction/TestAppBoxReference.java create mode 100644 src/test/java/com/algorand/algosdk/transaction/TestBoxReference.java create mode 100644 src/test/java/com/algorand/algosdk/util/ComparableBytes.java create mode 100644 src/test/java/com/algorand/algosdk/util/TestBoxQueryEncoding.java diff --git a/src/main/java/com/algorand/algosdk/builder/transaction/ApplicationBaseTransactionBuilder.java b/src/main/java/com/algorand/algosdk/builder/transaction/ApplicationBaseTransactionBuilder.java index e231ac4ec..a0d4fb496 100644 --- a/src/main/java/com/algorand/algosdk/builder/transaction/ApplicationBaseTransactionBuilder.java +++ b/src/main/java/com/algorand/algosdk/builder/transaction/ApplicationBaseTransactionBuilder.java @@ -1,6 +1,7 @@ package com.algorand.algosdk.builder.transaction; import com.algorand.algosdk.crypto.Address; +import com.algorand.algosdk.transaction.AppBoxReference; import com.algorand.algosdk.transaction.Transaction; import com.algorand.algosdk.util.Encoder; @@ -15,6 +16,7 @@ public abstract class ApplicationBaseTransactionBuilder accounts; private List foreignApps; private List foreignAssets; + private List appBoxReferences; private Long applicationId; /** @@ -36,6 +38,7 @@ protected void applyTo(Transaction txn) { if (accounts != null) txn.accounts = accounts; if (foreignApps != null) txn.foreignApps = foreignApps; if (foreignAssets != null) txn.foreignAssets = foreignAssets; + if (appBoxReferences != null) txn.boxReferences = convertBoxes(appBoxReferences, foreignApps, applicationId); } @Override @@ -63,6 +66,7 @@ public T args(List args) { /** * ApplicationArgs lists some transaction-specific arguments accessible from application logic. + * * @param args List of Base64 encoded strings. */ public T argsBase64Encoded(List args) { @@ -90,4 +94,17 @@ public T foreignAssets(List foreignAssets) { this.foreignAssets = foreignAssets; return (T) this; } + + private List convertBoxes(List abrs, List foreignApps, Long curApp) { + ArrayList xs = new ArrayList<>(); + for (AppBoxReference abr : abrs) { + xs.add(Transaction.BoxReference.fromAppBoxReference(abr, foreignApps, curApp)); + } + return xs; + } + + public T boxReferences(List boxReferences) { + this.appBoxReferences = boxReferences; + return (T) this; + } } diff --git a/src/main/java/com/algorand/algosdk/builder/transaction/ApplicationCallReferencesSetter.java b/src/main/java/com/algorand/algosdk/builder/transaction/ApplicationCallReferencesSetter.java index ddf5b84a2..f91a467a0 100644 --- a/src/main/java/com/algorand/algosdk/builder/transaction/ApplicationCallReferencesSetter.java +++ b/src/main/java/com/algorand/algosdk/builder/transaction/ApplicationCallReferencesSetter.java @@ -3,9 +3,10 @@ import java.util.List; import com.algorand.algosdk.crypto.Address; +import com.algorand.algosdk.transaction.AppBoxReference; public interface ApplicationCallReferencesSetter> { - + /** * ApplicationID is the application being interacted with, or 0 if creating a new application. */ @@ -27,4 +28,10 @@ public interface ApplicationCallReferencesSetter foreignAssets); + + /** + * BoxReferences lists the boxes whose state may be accessed during evaluation of this application call. The apps + * the boxes belong to must be present in ForeignApps. + */ + public T boxReferences(List boxReferences); } diff --git a/src/main/java/com/algorand/algosdk/builder/transaction/MethodCallTransactionBuilder.java b/src/main/java/com/algorand/algosdk/builder/transaction/MethodCallTransactionBuilder.java index 012a6837a..d42b2dd7f 100644 --- a/src/main/java/com/algorand/algosdk/builder/transaction/MethodCallTransactionBuilder.java +++ b/src/main/java/com/algorand/algosdk/builder/transaction/MethodCallTransactionBuilder.java @@ -2,12 +2,15 @@ import com.algorand.algosdk.abi.Method; import com.algorand.algosdk.crypto.Address; +import com.algorand.algosdk.crypto.Digest; import com.algorand.algosdk.crypto.TEALProgram; import com.algorand.algosdk.logic.StateSchema; +import com.algorand.algosdk.transaction.AppBoxReference; import com.algorand.algosdk.transaction.MethodCallParams; import com.algorand.algosdk.transaction.Transaction; import com.algorand.algosdk.transaction.TxnSigner; +import java.math.BigInteger; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -22,6 +25,7 @@ public class MethodCallTransactionBuilder foreignAccounts = new ArrayList<>(); protected List foreignAssets = new ArrayList<>(); protected List foreignApps = new ArrayList<>(); + protected List boxReferences = new ArrayList<>(); protected TEALProgram approvalProgram, clearStateProgram; protected StateSchema localStateSchema; @@ -62,7 +66,7 @@ public T method(Method method) { /** * Specify arguments for the ABI method invocation. - * + *

* This will reset the arguments list to what is passed in by the caller. */ public T methodArguments(List arguments) { @@ -72,7 +76,7 @@ public T methodArguments(List arguments) { /** * Specify arguments for the ABI method invocation. - * + *

* This will add the arguments passed in by the caller to the existing list of arguments. */ public T addMethodArguments(List arguments) { @@ -82,7 +86,7 @@ public T addMethodArguments(List arguments) { /** * Specify arguments for the ABI method invocation. - * + *

* This will add the argument passed in by the caller to the existing list of arguments. */ public T addMethodArgument(Object argument) { @@ -125,6 +129,16 @@ public T foreignAssets(List foreignAssets) { return (T) this; } + @Override + public T boxReferences(List boxReferences) { + if (boxReferences != null) + // duplicate box references can be meaningful, don't get rid of them + this.boxReferences = new ArrayList<>(boxReferences); + else + this.boxReferences.clear(); + return (T) this; + } + @Override public T approvalProgram(TEALProgram approvalProgram) { this.approvalProgram = approvalProgram; @@ -162,10 +176,34 @@ public T extraPages(Long extraPages) { * Build a MethodCallParams object. */ public MethodCallParams build() { - return new MethodCallParams( - appID, method, methodArgs, sender, onCompletion, note, lease, genesisID, genesisHash, + return new MethodCallParamsFactory(appID, method, methodArgs, sender, onCompletion, note, lease, genesisID, genesisHash, firstValid, lastValid, fee, flatFee, rekeyTo, signer, foreignAccounts, foreignAssets, foreignApps, - approvalProgram, clearStateProgram, globalStateSchema, localStateSchema, extraPages - ); + boxReferences, approvalProgram, clearStateProgram, globalStateSchema, localStateSchema, extraPages); + } + + /** + * MethodCallParamsFactory exists only as a way to facilitate construction of + * `MethodCallParams` instances via a protected constructor. + *

+ * No extension or other modification is intended. + */ + private static class MethodCallParamsFactory extends MethodCallParams { + + MethodCallParamsFactory(Long appID, Method method, List methodArgs, Address sender, + Transaction.OnCompletion onCompletion, byte[] note, byte[] lease, String genesisID, Digest genesisHash, + BigInteger firstValid, BigInteger lastValid, BigInteger fee, BigInteger flatFee, + Address rekeyTo, TxnSigner signer, + List
fAccounts, List fAssets, List fApps, List boxes, + TEALProgram approvalProgram, TEALProgram clearProgram, + StateSchema globalStateSchema, StateSchema localStateSchema, Long extraPages) { + super(appID, method, methodArgs, sender, + onCompletion, note, lease, genesisID, genesisHash, + firstValid, lastValid, fee, flatFee, + rekeyTo, signer, + fAccounts, fAssets, fApps, boxes, + approvalProgram, clearProgram, + globalStateSchema, localStateSchema, extraPages); + } + } } diff --git a/src/main/java/com/algorand/algosdk/transaction/AppBoxReference.java b/src/main/java/com/algorand/algosdk/transaction/AppBoxReference.java new file mode 100644 index 000000000..c3b7ccd43 --- /dev/null +++ b/src/main/java/com/algorand/algosdk/transaction/AppBoxReference.java @@ -0,0 +1,55 @@ +package com.algorand.algosdk.transaction; + +import com.algorand.algosdk.util.BoxQueryEncoding; + +import java.util.Arrays; +import java.util.Objects; + +public class AppBoxReference { + // the app ID of the app this box belongs to. Instead of serializing this value, + // it's used to calculate the appIdx for AppBoxReference. + private final long appId; + + // the name of the box unique to the app it belongs to + private final byte[] name; + + public AppBoxReference(long appId, byte[] name) { + this.appId = appId; + this.name = Arrays.copyOf(name, name.length); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AppBoxReference that = (AppBoxReference) o; + return appId == that.appId && Arrays.equals(name, that.name); + } + + @Override + public int hashCode() { + int result = Objects.hash(appId); + result = 31 * result + Arrays.hashCode(name); + return result; + } + + public long getAppId() { + return appId; + } + + public byte[] getName() { + return Arrays.copyOf(name, name.length); + } + + @Override + public String toString() { + return "AppBoxReference{" + + "appID=" + appId + + ", name=" + Arrays.toString(name) + + '}'; + } + + public String nameQueryEncoded() { + return BoxQueryEncoding.encodeBytes(name); + } +} diff --git a/src/main/java/com/algorand/algosdk/transaction/MethodCallParams.java b/src/main/java/com/algorand/algosdk/transaction/MethodCallParams.java index fda05a1e9..21d5fd542 100644 --- a/src/main/java/com/algorand/algosdk/transaction/MethodCallParams.java +++ b/src/main/java/com/algorand/algosdk/transaction/MethodCallParams.java @@ -22,6 +22,7 @@ * MethodCallParams is an object that holds all parameters necessary to invoke {@link AtomicTransactionComposer#addMethodCall(MethodCallParams)} */ public class MethodCallParams { + // if the abi type argument number > 15, then the abi types after 14th should be wrapped in a tuple private static final int MAX_ABI_ARG_TYPE_LEN = 15; @@ -35,7 +36,8 @@ public class MethodCallParams { public final List
foreignAccounts; public final List foreignAssets; public final List foreignApps; - + public final List boxReferences; + public final TEALProgram approvalProgram, clearProgram; public final StateSchema globalStateSchema, localStateSchema; public final Long extraPages; @@ -54,17 +56,13 @@ public class MethodCallParams { public final String genesisID; public final Digest genesisHash; - /** - * NOTE: it's strongly suggested to use {@link com.algorand.algosdk.builder.transaction.MethodCallTransactionBuilder} - * instead of this constructor to create a new MethodCallParams object. - */ - public MethodCallParams(Long appID, Method method, List methodArgs, Address sender, - Transaction.OnCompletion onCompletion, byte[] note, byte[] lease, String genesisID, Digest genesisHash, - BigInteger firstValid, BigInteger lastValid, BigInteger fee, BigInteger flatFee, - Address rekeyTo, TxnSigner signer, - List
fAccounts, List fAssets, List fApps, - TEALProgram approvalProgram, TEALProgram clearProgram, - StateSchema globalStateSchema, StateSchema localStateSchema, Long extraPages) { + protected MethodCallParams(Long appID, Method method, List methodArgs, Address sender, + Transaction.OnCompletion onCompletion, byte[] note, byte[] lease, String genesisID, Digest genesisHash, + BigInteger firstValid, BigInteger lastValid, BigInteger fee, BigInteger flatFee, + Address rekeyTo, TxnSigner signer, + List
fAccounts, List fAssets, List fApps, List boxes, + TEALProgram approvalProgram, TEALProgram clearProgram, + StateSchema globalStateSchema, StateSchema localStateSchema, Long extraPages) { if (appID == null || method == null || sender == null || onCompletion == null || signer == null || genesisID == null || genesisHash == null || firstValid == null || lastValid == null || (fee == null && flatFee == null)) throw new IllegalArgumentException("Method call builder error: some required field not added"); if (fee != null && flatFee != null) @@ -113,6 +111,7 @@ public MethodCallParams(Long appID, Method method, List methodArgs, Addr this.foreignAccounts = new ArrayList<>(fAccounts); this.foreignAssets = new ArrayList<>(fAssets); this.foreignApps = new ArrayList<>(fApps); + this.boxReferences = new ArrayList<>(boxes); this.approvalProgram = approvalProgram; this.clearProgram = clearProgram; this.globalStateSchema = globalStateSchema; @@ -120,9 +119,30 @@ public MethodCallParams(Long appID, Method method, List methodArgs, Addr this.extraPages = extraPages; } + /** + * Deprecated - Use {@link com.algorand.algosdk.builder.transaction.MethodCallTransactionBuilder} + * to create a new MethodCallParams object instead. + */ + @Deprecated + public MethodCallParams(Long appID, Method method, List methodArgs, Address sender, + Transaction.OnCompletion onCompletion, byte[] note, byte[] lease, String genesisID, Digest genesisHash, + BigInteger firstValid, BigInteger lastValid, BigInteger fee, BigInteger flatFee, + Address rekeyTo, TxnSigner signer, + List
fAccounts, List fAssets, List fApps, + TEALProgram approvalProgram, TEALProgram clearProgram, + StateSchema globalStateSchema, StateSchema localStateSchema, Long extraPages) { + this(appID, method, methodArgs, sender, + onCompletion, note, lease, genesisID, genesisHash, + firstValid, lastValid, fee, flatFee, + rekeyTo, signer, + fAccounts, fAssets, fApps, new ArrayList(), + approvalProgram, clearProgram, + globalStateSchema, localStateSchema, extraPages); + } + /** * Create the transactions which will carry out the specified method call. - * + *

* The list of transactions returned by this function will have the same length as method.getTxnCallCount(). */ public List createTransactions() { @@ -136,6 +156,7 @@ public List createTransactions() { List

foreignAccounts = new ArrayList<>(this.foreignAccounts); List foreignAssets = new ArrayList<>(this.foreignAssets); List foreignApps = new ArrayList<>(this.foreignApps); + List boxReferences = new ArrayList<>(this.boxReferences); for (int i = 0; i < this.method.args.size(); i++) { Method.Arg argT = this.method.args.get(i); @@ -199,21 +220,22 @@ public List createTransactions() { ApplicationCallTransactionBuilder txBuilder = ApplicationCallTransactionBuilder.Builder(); txBuilder - .firstValid(this.firstValid) - .lastValid(this.lastValid) - .genesisHash(this.genesisHash) - .genesisID(this.genesisID) - .fee(this.fee) - .flatFee(this.flatFee) - .note(this.note) - .lease(this.lease) - .rekey(this.rekeyTo) - .sender(this.sender) - .applicationId(this.appID) - .args(encodedABIArgs) - .accounts(foreignAccounts) - .foreignApps(foreignApps) - .foreignAssets(foreignAssets); + .firstValid(this.firstValid) + .lastValid(this.lastValid) + .genesisHash(this.genesisHash) + .genesisID(this.genesisID) + .fee(this.fee) + .flatFee(this.flatFee) + .note(this.note) + .lease(this.lease) + .rekey(this.rekeyTo) + .sender(this.sender) + .applicationId(this.appID) + .args(encodedABIArgs) + .accounts(foreignAccounts) + .foreignApps(foreignApps) + .foreignAssets(foreignAssets) + .boxReferences(boxReferences); Transaction tx = txBuilder.build(); @@ -228,7 +250,7 @@ public List createTransactions() { tx.localStateSchema = this.localStateSchema; if (this.extraPages != null) tx.extraPages = this.extraPages; - + TransactionWithSigner methodCall = new TransactionWithSigner(tx, this.signer); transactionArgs.add(methodCall); @@ -245,12 +267,12 @@ private static boolean checkTransactionType(TransactionWithSigner tws, String tx * and this function will return an index that can be used to reference `objectToBeAdded` in `objectArray`. * * @param objectToBeAdded - The value to add to the array. If this value is already present in the array, - * it will not be added again. Instead, the existing index will be returned. - * @param objectArray - The existing foreign array. This input may be modified to append `valueToAdd`. - * @param zerothObject - If provided, this value indicated two things: the 0 value is special for this - * array, so all indexes into `objectArray` must start at 1; additionally, if `objectToBeAdded` equals - * `zerothValue`, then `objectToBeAdded` will not be added to the array, and instead the 0 indexes will - * be returned. + * it will not be added again. Instead, the existing index will be returned. + * @param objectArray - The existing foreign array. This input may be modified to append `valueToAdd`. + * @param zerothObject - If provided, this value indicated two things: the 0 value is special for this + * array, so all indexes into `objectArray` must start at 1; additionally, if `objectToBeAdded` equals + * `zerothValue`, then `objectToBeAdded` will not be added to the array, and instead the 0 indexes will + * be returned. * @return An index that can be used to reference `valueToAdd` in `array`. */ private static int populateForeignArrayIndex(T objectToBeAdded, List objectArray, T zerothObject) { diff --git a/src/main/java/com/algorand/algosdk/transaction/Transaction.java b/src/main/java/com/algorand/algosdk/transaction/Transaction.java index 0f3531c07..d08f2b0af 100644 --- a/src/main/java/com/algorand/algosdk/transaction/Transaction.java +++ b/src/main/java/com/algorand/algosdk/transaction/Transaction.java @@ -19,7 +19,7 @@ * A raw serializable transaction class, used to generate transactions to broadcast to the network. * This is distinct from algod.model.Transaction, which is only returned for GET requests to algod. */ -@JsonPropertyOrder(alphabetic=true) +@JsonPropertyOrder(alphabetic = true) @JsonInclude(JsonInclude.Include.NON_DEFAULT) public class Transaction implements Serializable { private static final byte[] TX_SIGN_PREFIX = ("TX").getBytes(StandardCharsets.UTF_8); @@ -117,7 +117,7 @@ public class Transaction implements Serializable { // account. @JsonProperty("aclose") public Address assetCloseTo = new Address(); - + /* asset freeze fields */ @JsonProperty("fadd") public Address freezeTarget = new Address(); @@ -147,6 +147,9 @@ public class Transaction implements Serializable { @JsonProperty("apas") public List foreignAssets = new ArrayList<>(); + @JsonProperty("apbx") + public List boxReferences = new ArrayList<>(); + @JsonProperty("apgs") public StateSchema globalStateSchema = new StateSchema(); @@ -174,12 +177,13 @@ public class Transaction implements Serializable { /** * Create a payment transaction - * @param fromAddr source address - * @param toAddr destination address - * @param fee transaction fee - * @param amount payment amount + * + * @param fromAddr source address + * @param toAddr destination address + * @param fee transaction fee + * @param amount payment amount * @param firstRound first valid round - * @param lastRound last valid round + * @param lastRound last valid round */ @Deprecated public Transaction(Address fromAddr, Address toAddr, BigInteger fee, BigInteger amount, BigInteger firstRound, @@ -196,12 +200,13 @@ public Transaction(Address fromAddr, Address toAddr, BigInteger fee, BigInteger /** * Create a payment transaction. Make sure to sign with a suggested fee. - * @param fromAddr source address - * @param toAddr destination address - * @param amount amount to send - * @param firstRound first valid round - * @param lastRound last valid round - * @param genesisID genesis id + * + * @param fromAddr source address + * @param toAddr destination address + * @param amount amount to send + * @param firstRound first valid round + * @param lastRound last valid round + * @param genesisID genesis id * @param genesisHash genesis hash */ @Deprecated @@ -237,15 +242,15 @@ public Transaction(Address sender, BigInteger fee, BigInteger firstValid, BigInt */ @Deprecated public static Transaction createPaymentTransaction(Address sender, BigInteger fee, BigInteger firstValid, - BigInteger lastValid, byte[] note, String genesisID, - Digest genesisHash, BigInteger amount, Address receiver, - Address closeRemainderTo) { + BigInteger lastValid, byte[] note, String genesisID, + Digest genesisHash, BigInteger amount, Address receiver, + Address closeRemainderTo) { Objects.requireNonNull(sender, "sender is required."); Objects.requireNonNull(firstValid, "firstValid is required."); Objects.requireNonNull(lastValid, "lastValid is required."); Objects.requireNonNull(genesisHash, "genesisHash is required."); - if (sender == null && closeRemainderTo == null) { + if (receiver == null && closeRemainderTo == null) { throw new IllegalArgumentException("Must set at least one of 'receiver' or 'closeRemainderTo'"); } @@ -302,15 +307,16 @@ public static Transaction createPaymentTransaction(Address sender, BigInteger fe /** * Create a key registration transaction. No field can be null except the note field. - * @param sender source address - * @param fee transaction fee - * @param firstValid first valid round - * @param lastValid last valid round - * @param note optional notes field (can be null) - * @param votePK the new participation key to register - * @param vrfPK the sortition key to register - * @param voteFirst key reg valid first round - * @param voteLast key reg valid last round + * + * @param sender source address + * @param fee transaction fee + * @param firstValid first valid round + * @param lastValid last valid round + * @param note optional notes field (can be null) + * @param votePK the new participation key to register + * @param vrfPK the sortition key to register + * @param voteFirst key reg valid first round + * @param voteLast key reg valid last round * @param voteKeyDilution key reg dilution */ @Deprecated @@ -325,7 +331,7 @@ public Transaction(Address sender, BigInteger fee, BigInteger firstValid, BigInt if (lastValid != null) this.lastValid = lastValid; setNote(note); if (genesisID != null) this.genesisID = genesisID; - if (genesisHash != null) this.genesisHash = genesisHash; + if (genesisHash != null) this.genesisHash = genesisHash; if (votePK != null) this.votePK = votePK; if (vrfPK != null) this.selectionPK = vrfPK; if (voteFirst != null) this.voteFirst = voteFirst; @@ -405,30 +411,31 @@ public static Transaction createKeyRegistrationTransaction(Address sender, BigIn /** * Create an asset creation transaction. Note can be null. manager, reserve, freeze, and clawback can be zeroed. - * @param sender source address - * @param fee transaction fee - * @param firstValid first valid round - * @param lastValid last valid round - * @param note optional note field (can be null) + * + * @param sender source address + * @param fee transaction fee + * @param firstValid first valid round + * @param lastValid last valid round + * @param note optional note field (can be null) * @param genesisID * @param genesisHash - * @param assetTotal total asset issuance + * @param assetTotal total asset issuance * @param assetDecimals asset decimal precision * @param defaultFrozen whether accounts have this asset frozen by default * @param assetUnitName name of unit of the asset - * @param assetName name of the asset - * @param url where more information about the asset can be retrieved - * @param metadataHash specifies a commitment to some unspecified asset metadata. The format of this metadata is up to the application - * @param manager account which can reconfigure the asset - * @param reserve account whose asset holdings count as non-minted - * @param freeze account which can freeze or unfreeze holder accounts - * @param clawback account which can issue clawbacks against holder accounts + * @param assetName name of the asset + * @param url where more information about the asset can be retrieved + * @param metadataHash specifies a commitment to some unspecified asset metadata. The format of this metadata is up to the application + * @param manager account which can reconfigure the asset + * @param reserve account whose asset holdings count as non-minted + * @param freeze account which can freeze or unfreeze holder accounts + * @param clawback account which can issue clawbacks against holder accounts */ @Deprecated private Transaction(Address sender, BigInteger fee, BigInteger firstValid, BigInteger lastValid, byte[] note, - String genesisID, Digest genesisHash, BigInteger assetTotal, Integer assetDecimals, boolean defaultFrozen, - String assetUnitName, String assetName, String url, byte[] metadataHash, - Address manager, Address reserve, Address freeze, Address clawback) { + String genesisID, Digest genesisHash, BigInteger assetTotal, Integer assetDecimals, boolean defaultFrozen, + String assetUnitName, String assetName, String url, byte[] metadataHash, + Address manager, Address reserve, Address freeze, Address clawback) { this.type = Type.AssetConfig; if (sender != null) this.sender = sender; setFee(fee); @@ -437,36 +444,37 @@ private Transaction(Address sender, BigInteger fee, BigInteger firstValid, BigIn setNote(note); if (genesisID != null) this.genesisID = genesisID; if (genesisHash != null) this.genesisHash = genesisHash; - - this.assetParams = new AssetParams(assetTotal, assetDecimals, defaultFrozen, assetUnitName, assetName, url, metadataHash, manager, reserve, freeze, clawback); + + this.assetParams = new AssetParams(assetTotal, assetDecimals, defaultFrozen, assetUnitName, assetName, url, metadataHash, manager, reserve, freeze, clawback); } /** * Create an asset creation transaction. Note can be null. manager, reserve, freeze, and clawback can be zeroed. - * @param sender source address - * @param fee transaction fee - * @param firstValid first valid round - * @param lastValid last valid round - * @param note optional note field (can be null) + * + * @param sender source address + * @param fee transaction fee + * @param firstValid first valid round + * @param lastValid last valid round + * @param note optional note field (can be null) * @param genesisID * @param genesisHash - * @param assetTotal total asset issuance + * @param assetTotal total asset issuance * @param assetDecimals asset decimal precision * @param defaultFrozen whether accounts have this asset frozen by default * @param assetUnitName name of unit of the asset - * @param assetName name of the asset - * @param url where more information about the asset can be retrieved - * @param metadataHash specifies a commitment to some unspecified asset metadata. The format of this metadata is up to the application - * @param manager account which can reconfigure the asset - * @param reserve account whose asset holdings count as non-minted - * @param freeze account which can freeze or unfreeze holder accounts - * @param clawback account which can issue clawbacks against holder accounts + * @param assetName name of the asset + * @param url where more information about the asset can be retrieved + * @param metadataHash specifies a commitment to some unspecified asset metadata. The format of this metadata is up to the application + * @param manager account which can reconfigure the asset + * @param reserve account whose asset holdings count as non-minted + * @param freeze account which can freeze or unfreeze holder accounts + * @param clawback account which can issue clawbacks against holder accounts */ @Deprecated public static Transaction createAssetCreateTransaction(Address sender, BigInteger fee, BigInteger firstValid, BigInteger lastValid, byte[] note, - String genesisID, Digest genesisHash, BigInteger assetTotal, Integer assetDecimals, boolean defaultFrozen, - String assetUnitName, String assetName, String url, byte[] metadataHash, - Address manager, Address reserve, Address freeze, Address clawback) { + String genesisID, Digest genesisHash, BigInteger assetTotal, Integer assetDecimals, boolean defaultFrozen, + String assetUnitName, String assetName, String url, byte[] metadataHash, + Address manager, Address reserve, Address freeze, Address clawback) { Objects.requireNonNull(sender, "sender is required."); Objects.requireNonNull(firstValid, "firstValid is required."); @@ -527,26 +535,27 @@ public static Transaction createAssetCreateTransaction(Address sender, BigIntege null, null); } - + /** * Create an asset configuration transaction. Note can be null. manager, reserve, freeze, and clawback can be zeroed. - * @param sender source address - * @param fee transaction fee - * @param firstValid first valid round - * @param lastValid last valid round - * @param note optional note field (can be null) + * + * @param sender source address + * @param fee transaction fee + * @param firstValid first valid round + * @param lastValid last valid round + * @param note optional note field (can be null) * @param genesisID * @param genesisHash - * @param index asset index - * @param manager account which can reconfigure the asset - * @param reserve account whose asset holdings count as non-minted - * @param freeze account which can freeze or unfreeze holder accounts - * @param clawback account which can issue clawbacks against holder accounts + * @param index asset index + * @param manager account which can reconfigure the asset + * @param reserve account whose asset holdings count as non-minted + * @param freeze account which can freeze or unfreeze holder accounts + * @param clawback account which can issue clawbacks against holder accounts */ private Transaction(Address sender, BigInteger fee, BigInteger firstValid, BigInteger lastValid, byte[] note, - String genesisID, Digest genesisHash, BigInteger index, - Address manager, Address reserve, Address freeze, Address clawback) { - + String genesisID, Digest genesisHash, BigInteger index, + Address manager, Address reserve, Address freeze, Address clawback) { + this.type = Type.AssetConfig; if (sender != null) this.sender = sender; setFee(fee); @@ -557,62 +566,63 @@ private Transaction(Address sender, BigInteger fee, BigInteger firstValid, BigIn if (genesisHash != null) this.genesisHash = genesisHash; this.assetParams = new AssetParams(BigInteger.valueOf(0), 0, false, "", "", "", null, manager, reserve, freeze, clawback); assetIndex = index; - } + } /** * Create an asset configuration transaction. Note can be null. manager, reserve, freeze, and clawback can be zeroed. - * @param sender source address - * @param fee transaction fee - * @param firstValid first valid round - * @param lastValid last valid round - * @param note optional note field (can be null) + * + * @param sender source address + * @param fee transaction fee + * @param firstValid first valid round + * @param lastValid last valid round + * @param note optional note field (can be null) * @param genesisID * @param genesisHash - * @param index asset index - * @param manager account which can reconfigure the asset - * @param reserve account whose asset holdings count as non-minted - * @param freeze account which can freeze or unfreeze holder accounts - * @param clawback account which can issue clawbacks against holder accounts + * @param index asset index + * @param manager account which can reconfigure the asset + * @param reserve account whose asset holdings count as non-minted + * @param freeze account which can freeze or unfreeze holder accounts + * @param clawback account which can issue clawbacks against holder accounts * @param strictEmptyAddressChecking if true, disallow empty admin accounts from being set (preventing accidental disable of admin features) */ @Deprecated public static Transaction createAssetConfigureTransaction( - Address sender, - BigInteger fee, - BigInteger firstValid, - BigInteger lastValid, - byte[] note, - String genesisID, - Digest genesisHash, - BigInteger index, - Address manager, - Address reserve, - Address freeze, - Address clawback, - boolean strictEmptyAddressChecking) { + Address sender, + BigInteger fee, + BigInteger firstValid, + BigInteger lastValid, + byte[] note, + String genesisID, + Digest genesisHash, + BigInteger index, + Address manager, + Address reserve, + Address freeze, + Address clawback, + boolean strictEmptyAddressChecking) { Address defaultAddr = new Address(); - if (strictEmptyAddressChecking && ( - (manager == null || manager.equals(defaultAddr)) || - (reserve == null || reserve.equals(defaultAddr)) || - (freeze == null || freeze.equals(defaultAddr)) || - (clawback == null || clawback.equals(defaultAddr)) - )) { - throw new RuntimeException("strict empty address checking requested but " - + "empty or default address supplied to one or more manager addresses"); - } - return new Transaction( - sender, - fee, - firstValid, - lastValid, - note, - genesisID, - genesisHash, - index, - manager, - reserve, - freeze, - clawback); + if (strictEmptyAddressChecking && ( + (manager == null || manager.equals(defaultAddr)) || + (reserve == null || reserve.equals(defaultAddr)) || + (freeze == null || freeze.equals(defaultAddr)) || + (clawback == null || clawback.equals(defaultAddr)) + )) { + throw new RuntimeException("strict empty address checking requested but " + + "empty or default address supplied to one or more manager addresses"); + } + return new Transaction( + sender, + fee, + firstValid, + lastValid, + note, + genesisID, + genesisHash, + index, + manager, + reserve, + freeze, + clawback); } /** @@ -640,7 +650,7 @@ private Transaction(@JsonProperty("type") Type type, @JsonProperty("gh") byte[] genesisHash, @JsonProperty("lx") byte[] lease, @JsonProperty("rekey") byte[] rekeyTo, - @JsonProperty("grp") byte[] group, + @JsonProperty("grp") byte[] group, // payment fields @JsonProperty("amt") BigInteger amount, @JsonProperty("rcv") byte[] receiver, @@ -673,62 +683,67 @@ private Transaction(@JsonProperty("type") Type type, @JsonProperty("apat") List accounts, @JsonProperty("apfa") List foreignApps, @JsonProperty("apas") List foreignAssets, + @JsonProperty("apbx") List boxReferences, @JsonProperty("apgs") StateSchema globalStateSchema, @JsonProperty("apid") Long applicationId, @JsonProperty("apls") StateSchema localStateSchema, @JsonProperty("apsu") byte[] clearStateProgram, @JsonProperty("apep") Long extraPages - ) throws IOException { + ) throws IOException { this( - type, - //header fields - new Address(sender), - fee, - firstValid, - lastValid, - note, - genesisID, - new Digest(genesisHash), - lease, - new Address(rekeyTo), - new Digest(group), - // payment fields - amount, - new Address(receiver), - new Address(closeRemainderTo), - // keyreg fields - new ParticipationPublicKey(votePK), - new VRFPublicKey(vrfPK), - new MerkleVerifier(stateProofKey), - voteFirst, - voteLast, - voteKeyDilution, - nonpart, - // asset creation and configuration - assetParams, - assetIndex, - // asset transfer fields - xferAsset, - assetAmount, - new Address(assetSender), - new Address(assetReceiver), - new Address(assetCloseTo), - new Address(freezeTarget), - assetFreezeID, - freezeState, - // application fields - applicationArgs, - onCompletion, - approvalProgram == null ? null : new TEALProgram(approvalProgram), - convertToAddressList(accounts), - foreignApps, - foreignAssets, - globalStateSchema, - applicationId, - localStateSchema, - clearStateProgram == null ? null : new TEALProgram(clearStateProgram), - extraPages + type, + //header fields + new Address(sender), + fee, + firstValid, + lastValid, + note, + genesisID, + new Digest(genesisHash), + lease, + new Address(rekeyTo), + new Digest(group), + // payment fields + amount, + new Address(receiver), + new Address(closeRemainderTo), + // keyreg fields + new ParticipationPublicKey(votePK), + new VRFPublicKey(vrfPK), + new MerkleVerifier(stateProofKey), + voteFirst, + voteLast, + voteKeyDilution, + nonpart, + // asset creation and configuration + assetParams, + assetIndex, + // asset transfer fields + xferAsset, + assetAmount, + new Address(assetSender), + new Address(assetReceiver), + new Address(assetCloseTo), + new Address(freezeTarget), + assetFreezeID, + freezeState, + // application fields + applicationArgs, + onCompletion, + approvalProgram == null ? null : new TEALProgram(approvalProgram), + convertToAddressList(accounts), + foreignApps, + foreignAssets, + globalStateSchema, + applicationId, + localStateSchema, + clearStateProgram == null ? null : new TEALProgram(clearStateProgram), + extraPages ); + // Set fields _not_ exposed by public constructor. Needed because: + // * Adding parameters to a public constructor is a breaking API change. + // * To ensure JSON/msgpack serialization (via Jackson's ObjectMapper) works, must add `@JsonProperty` to _a_ constructor. Using a private constructor here to maintain API backwards compatibility. + if (boxReferences != null) this.boxReferences = boxReferences; } /** @@ -844,56 +859,56 @@ public Transaction( * https://developer.algorand.org/docs/reference/transactions/#asset-transfer-transaction */ public Transaction( - Type type, - //header fields - Address sender, - BigInteger fee, - BigInteger firstValid, - BigInteger lastValid, - byte[] note, - String genesisID, - Digest genesisHash, - byte[] lease, - Address rekeyTo, - Digest group, - // payment fields - BigInteger amount, - Address receiver, - Address closeRemainderTo, - // keyreg fields - ParticipationPublicKey votePK, - VRFPublicKey vrfPK, - MerkleVerifier stateProofKey, - BigInteger voteFirst, - BigInteger voteLast, - // voteKeyDilution - BigInteger voteKeyDilution, - boolean nonpart, - // asset creation and configuration - AssetParams assetParams, - BigInteger assetIndex, - // asset transfer fields - BigInteger xferAsset, - BigInteger assetAmount, - Address assetSender, - Address assetReceiver, - Address assetCloseTo, - Address freezeTarget, - BigInteger assetFreezeID, - boolean freezeState, - // application fields - List applicationArgs, - OnCompletion onCompletion, - TEALProgram approvalProgram, - List
accounts, - List foreignApps, - List foreignAssets, - StateSchema globalStateSchema, - Long applicationId, - StateSchema localStateSchema, - TEALProgram clearStateProgram, - Long extraPages - ) { + Type type, + //header fields + Address sender, + BigInteger fee, + BigInteger firstValid, + BigInteger lastValid, + byte[] note, + String genesisID, + Digest genesisHash, + byte[] lease, + Address rekeyTo, + Digest group, + // payment fields + BigInteger amount, + Address receiver, + Address closeRemainderTo, + // keyreg fields + ParticipationPublicKey votePK, + VRFPublicKey vrfPK, + MerkleVerifier stateProofKey, + BigInteger voteFirst, + BigInteger voteLast, + // voteKeyDilution + BigInteger voteKeyDilution, + boolean nonpart, + // asset creation and configuration + AssetParams assetParams, + BigInteger assetIndex, + // asset transfer fields + BigInteger xferAsset, + BigInteger assetAmount, + Address assetSender, + Address assetReceiver, + Address assetCloseTo, + Address freezeTarget, + BigInteger assetFreezeID, + boolean freezeState, + // application fields + List applicationArgs, + OnCompletion onCompletion, + TEALProgram approvalProgram, + List
accounts, + List foreignApps, + List foreignAssets, + StateSchema globalStateSchema, + Long applicationId, + StateSchema localStateSchema, + TEALProgram clearStateProgram, + Long extraPages + ) { if (type != null) this.type = type; if (sender != null) this.sender = sender; setFee(fee); @@ -939,24 +954,26 @@ public Transaction( } // Used by Jackson to determine "default" values. - public Transaction() {} + public Transaction() { + } /** * Base constructor with flat fee for asset xfer/freeze/destroy transactions. - * @param flatFee is the transaction flat fee - * @param firstRound is the first round this txn is valid (txn semantics - * unrelated to asset management) - * @param lastRound is the last round this txn is valid + * + * @param flatFee is the transaction flat fee + * @param firstRound is the first round this txn is valid (txn semantics + * unrelated to asset management) + * @param lastRound is the last round this txn is valid * @param note * @param genesisHash corresponds to the base64-encoded hash of the genesis - * of the network + * of the network **/ private Transaction( Type type, BigInteger flatFee, BigInteger firstRound, BigInteger lastRound, - byte [] note, + byte[] note, Digest genesisHash) { this.type = type; @@ -969,28 +986,29 @@ private Transaction( /** * Creates a tx to mark the account as willing to accept the asset. + * * @param acceptingAccount is a checksummed, human-readable address that - * will accept receiving the asset. - * @param flatFee is the transaction flat fee - * @param firstRound is the first round this txn is valid (txn semantics - * unrelated to asset management) - * @param lastRound is the last round this txn is valid + * will accept receiving the asset. + * @param flatFee is the transaction flat fee + * @param firstRound is the first round this txn is valid (txn semantics + * unrelated to asset management) + * @param lastRound is the last round this txn is valid * @param note - * @param genesisID corresponds to the id of the network - * @param genesisHash corresponds to the base64-encoded hash of the genesis - * of the network - * @param assetIndex is the asset index + * @param genesisID corresponds to the id of the network + * @param genesisHash corresponds to the base64-encoded hash of the genesis + * of the network + * @param assetIndex is the asset index **/ @Deprecated public static Transaction createAssetAcceptTransaction( //AssetTransaction - Address acceptingAccount, - BigInteger flatFee, - BigInteger firstRound, - BigInteger lastRound, - byte [] note, - String genesisID, - Digest genesisHash, - BigInteger assetIndex) { + Address acceptingAccount, + BigInteger flatFee, + BigInteger firstRound, + BigInteger lastRound, + byte[] note, + String genesisID, + Digest genesisHash, + BigInteger assetIndex) { Transaction tx = createAssetTransferTransaction( acceptingAccount, @@ -1010,23 +1028,24 @@ public static Transaction createAssetAcceptTransaction( //AssetTransaction /** * Creates a tx to destroy the asset - * @param senderAccount is a checksummed, human-readable address of the sender - * @param flatFee is the transaction flat fee - * @param firstValid is the first round this txn is valid (txn semantics - * unrelated to asset management) - * @param lastValid is the last round this txn is valid + * + * @param senderAccount is a checksummed, human-readable address of the sender + * @param flatFee is the transaction flat fee + * @param firstValid is the first round this txn is valid (txn semantics + * unrelated to asset management) + * @param lastValid is the last round this txn is valid * @param note - * @param genesisHash corresponds to the base64-encoded hash of the genesis - * of the network - * @param assetIndex is the asset ID to destroy + * @param genesisHash corresponds to the base64-encoded hash of the genesis + * of the network + * @param assetIndex is the asset ID to destroy **/ @Deprecated public static Transaction createAssetDestroyTransaction( - Address senderAccount, + Address senderAccount, BigInteger flatFee, BigInteger firstValid, BigInteger lastValid, - byte [] note, + byte[] note, Digest genesisHash, BigInteger assetIndex) { Transaction tx = new Transaction( @@ -1036,7 +1055,7 @@ public static Transaction createAssetDestroyTransaction( lastValid, note, genesisHash); - + if (assetIndex != null) tx.assetIndex = assetIndex; if (senderAccount != null) tx.sender = senderAccount; return tx; @@ -1044,25 +1063,26 @@ public static Transaction createAssetDestroyTransaction( /** * Creates a tx to freeze/unfreeze assets - * @param senderAccount is a checksummed, human-readable address of the sender - * @param flatFee is the transaction flat fee - * @param firstValid is the first round this txn is valid (txn semantics - * unrelated to asset management) - * @param lastValid is the last round this txn is valid + * + * @param senderAccount is a checksummed, human-readable address of the sender + * @param flatFee is the transaction flat fee + * @param firstValid is the first round this txn is valid (txn semantics + * unrelated to asset management) + * @param lastValid is the last round this txn is valid * @param note - * @param genesisHash corresponds to the base64-encoded hash of the genesis - * of the network - * @param assetIndex is the asset ID to destroy + * @param genesisHash corresponds to the base64-encoded hash of the genesis + * of the network + * @param assetIndex is the asset ID to destroy **/ @Deprecated public static Transaction createAssetFreezeTransaction( - Address senderAccount, + Address senderAccount, Address accountToFreeze, boolean freezeState, BigInteger flatFee, BigInteger firstValid, BigInteger lastValid, - byte [] note, + byte[] note, Digest genesisHash, BigInteger assetIndex) { Transaction tx = new Transaction( @@ -1072,46 +1092,47 @@ public static Transaction createAssetFreezeTransaction( lastValid, note, genesisHash); - + if (senderAccount != null) tx.sender = senderAccount; if (accountToFreeze != null) tx.freezeTarget = accountToFreeze; if (assetIndex != null) tx.assetFreezeID = assetIndex; tx.freezeState = freezeState; return tx; - } - + } + /** * Creates a tx for revoking an asset from an account and sending it to another + * * @param transactionSender is a checksummed, human-readable address that will - * send the transaction - * @param assetRevokedFrom is a checksummed, human-readable address that will - * have assets taken from - * @param assetReceiver is a checksummed, human-readable address what will - * receive the assets - * @param assetAmount is the number of assets to send - * @param flatFee is the transaction flat fee - * @param firstRound is the first round this txn is valid (txn semantics - * unrelated to asset management) - * @param lastRound is the last round this txn is valid + * send the transaction + * @param assetRevokedFrom is a checksummed, human-readable address that will + * have assets taken from + * @param assetReceiver is a checksummed, human-readable address what will + * receive the assets + * @param assetAmount is the number of assets to send + * @param flatFee is the transaction flat fee + * @param firstRound is the first round this txn is valid (txn semantics + * unrelated to asset management) + * @param lastRound is the last round this txn is valid * @param note - * @param genesisID corresponds to the id of the network - * @param genesisHash corresponds to the base64-encoded hash of the genesis - * of the network - * @param assetIndex is the asset index + * @param genesisID corresponds to the id of the network + * @param genesisHash corresponds to the base64-encoded hash of the genesis + * of the network + * @param assetIndex is the asset index **/ @Deprecated public static Transaction createAssetRevokeTransaction(// AssetTransaction - Address transactionSender, - Address assetRevokedFrom, - Address assetReceiver, - BigInteger assetAmount, - BigInteger flatFee, - BigInteger firstRound, - BigInteger lastRound, - byte [] note, - String genesisID, - Digest genesisHash, - BigInteger assetIndex) { + Address transactionSender, + Address assetRevokedFrom, + Address assetReceiver, + BigInteger assetAmount, + BigInteger flatFee, + BigInteger firstRound, + BigInteger lastRound, + byte[] note, + String genesisID, + Digest genesisHash, + BigInteger assetIndex) { Transaction tx = new Transaction( Type.AssetTransfer, @@ -1129,43 +1150,44 @@ public static Transaction createAssetRevokeTransaction(// AssetTransaction return tx; } - + /** * Creates a tx for sending some asset from an asset holder to another user. - * The asset receiver must have marked itself as willing to accept the - * asset. - * @param assetSender is a checksummed, human-readable address that will - * send the transaction and assets + * The asset receiver must have marked itself as willing to accept the + * asset. + * + * @param assetSender is a checksummed, human-readable address that will + * send the transaction and assets * @param assetReceiver is a checksummed, human-readable address what will - * receive the assets - * @param assetCloseTo is a checksummed, human-readable address that - * behaves as a close-to address for the asset transaction; the remaining - * assets not sent to assetReceiver will be sent to assetCloseTo. Leave - * blank for no close-to behavior. - * @param assetAmount is the number of assets to send - * @param flatFee is the transaction flat fee - * @param firstRound is the first round this txn is valid (txn semantics - * unrelated to asset management) - * @param lastRound is the last round this txn is valid + * receive the assets + * @param assetCloseTo is a checksummed, human-readable address that + * behaves as a close-to address for the asset transaction; the remaining + * assets not sent to assetReceiver will be sent to assetCloseTo. Leave + * blank for no close-to behavior. + * @param assetAmount is the number of assets to send + * @param flatFee is the transaction flat fee + * @param firstRound is the first round this txn is valid (txn semantics + * unrelated to asset management) + * @param lastRound is the last round this txn is valid * @param note - * @param genesisID corresponds to the id of the network - * @param genesisHash corresponds to the base64-encoded hash of the genesis - * of the network - * @param assetIndex is the asset index + * @param genesisID corresponds to the id of the network + * @param genesisHash corresponds to the base64-encoded hash of the genesis + * of the network + * @param assetIndex is the asset index **/ @Deprecated public static Transaction createAssetTransferTransaction(// AssetTransaction - Address assetSender, - Address assetReceiver, - Address assetCloseTo, - BigInteger assetAmount, - BigInteger flatFee, - BigInteger firstRound, - BigInteger lastRound, - byte [] note, - String genesisID, - Digest genesisHash, - BigInteger assetIndex) { + Address assetSender, + Address assetReceiver, + Address assetCloseTo, + BigInteger assetAmount, + BigInteger flatFee, + BigInteger firstRound, + BigInteger lastRound, + byte[] note, + String genesisID, + Digest genesisHash, + BigInteger assetIndex) { Transaction tx = new Transaction( Type.AssetTransfer, @@ -1191,15 +1213,15 @@ private void setNote(byte[] note) { /** * Set a transaction fee taking the minimum transaction fee into consideration. - * @param fee * + * @param fee * @Deprecated a transaction builder is coming. */ @Deprecated public void setFee(BigInteger fee) { if (fee != null) { this.fee = fee; - } else { + } else { this.fee = Account.MIN_TX_FEE_UALGOS; } @@ -1220,10 +1242,10 @@ public void setFee(BigInteger fee) { * the lease identified by the (Sender, Lease) pair of the * transaction until the LastValid round passes. While this * transaction possesses the lease, no other transaction - * specifying this lease can be confirmed. - * The Size is fixed at 32 bytes. - * @param lease 32 byte lease + * specifying this lease can be confirmed. + * The Size is fixed at 32 bytes. * + * @param lease 32 byte lease * @Deprecated use setLease(Lease) **/ @Deprecated @@ -1242,6 +1264,7 @@ public void setLease(byte[] lease) { * transaction possesses the lease, no other transaction * specifying this lease can be confirmed. * The Size is fixed at 32 bytes. + * * @param lease Lease object **/ public void setLease(Lease lease) { @@ -1266,19 +1289,21 @@ public enum Type { private static Map namesMap = new HashMap(6); private final String value; + Type(String value) { this.value = value; } /** * Return the enumeration for the given string value. Required for JSON serialization. + * * @param value string representation * @return enumeration type */ @JsonCreator public static Type forValue(String value) { - for (Type t : values()) { - if(t.value.equalsIgnoreCase(value)) { + for (Type t : values()) { + if (t.value.equalsIgnoreCase(value)) { return t; } } @@ -1287,6 +1312,7 @@ public static Type forValue(String value) { /** * Return the string value for this enumeration. Required for JSON serialization. + * * @return string value */ @JsonValue @@ -1312,7 +1338,7 @@ public enum OnCompletion { } public static OnCompletion String(String name) { - for(OnCompletion oc : values()) { + for (OnCompletion oc : values()) { if (oc.serializedName.equalsIgnoreCase(name)) { return oc; } @@ -1322,7 +1348,7 @@ public static OnCompletion String(String name) { @JsonCreator public static OnCompletion forValue(int value) { - for(OnCompletion oc : values()) { + for (OnCompletion oc : values()) { if (oc.serializedValue == value) { return oc; } @@ -1373,7 +1399,7 @@ public byte[] bytesToSign() throws IOException { */ public Digest rawTxID() throws IOException { try { - return new Digest(Digester.digest(this.bytesToSign())); + return new Digest(Digester.digest(this.bytesToSign())); } catch (IOException e) { throw new RuntimeException("tx computation failed", e); } catch (NoSuchAlgorithmException e) { @@ -1416,7 +1442,7 @@ public boolean equals(Object o) { voteFirst.equals(that.voteFirst) && voteLast.equals(that.voteLast) && voteKeyDilution.equals(that.voteKeyDilution) && - nonpart==that.nonpart && + nonpart == that.nonpart && assetParams.equals(that.assetParams) && assetIndex.equals(that.assetIndex) && xferAsset.equals(that.xferAsset) && @@ -1428,8 +1454,8 @@ public boolean equals(Object o) { assetFreezeID.equals(that.assetFreezeID) && freezeState == that.freezeState && rekeyTo.equals(that.rekeyTo) && - Arrays.equals(lease, ((Transaction) o).lease) && - extraPages.equals(that.extraPages); + extraPages.equals(that.extraPages) && + boxReferences.equals(that.boxReferences); } /** @@ -1543,4 +1569,75 @@ public static ApplicationCallTransactionBuilder ApplicationCallTransactionBui public static ApplicationClearTransactionBuilder ApplicationClearTransactionBuilder() { return ApplicationClearTransactionBuilder.Builder(); } + + @JsonPropertyOrder(alphabetic = true) + @JsonInclude(JsonInclude.Include.NON_DEFAULT) + public static class BoxReference { + // the index in the foreign apps array of the app this box belongs to + @JsonProperty("i") + private final int appIndex; + + // the name of the box unique to the app it belongs to + @JsonProperty("n") + private final byte[] name; + + public BoxReference( + @JsonProperty("i") int appIndex, + @JsonProperty("n") byte[] name) { + this.appIndex = appIndex; + this.name = Arrays.copyOf(name, name.length); + } + + // Foreign apps start from index 1. Index 0 is the called App ID. + // Must apply offset to yield the foreign app index expected by algod. + private static final int FOREIGN_APPS_INDEX_OFFSET = 1; + private static final long NEW_APP_ID = 0L; + + public static BoxReference fromAppBoxReference(AppBoxReference abr, List foreignApps, Long currentApp) { + if (abr.getAppId() == NEW_APP_ID) + return new BoxReference(0, abr.getName()); + + if (foreignApps == null || !foreignApps.contains(abr.getAppId())) + // If the app references itself in foreign apps, then prefer foreign app index. + // Otherwise, fallback to comparing against the invoked app (`currentApp`). + if (Long.valueOf(abr.getAppId()).equals(currentApp)) + return new BoxReference(0, abr.getName()); + else + throw new RuntimeException( + String.format("Box app ID (%d) is not present in the foreign apps array: %d %s", abr.getAppId(), currentApp, foreignApps)); + else + return new BoxReference(foreignApps.indexOf(abr.getAppId()) + FOREIGN_APPS_INDEX_OFFSET, abr.getName()); + } + + public byte[] getName() { + return Arrays.copyOf(name, name.length); + } + + public int getAppIndex() { + return appIndex; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BoxReference that = (BoxReference) o; + return appIndex == that.appIndex && Arrays.equals(name, that.name); + } + + @Override + public int hashCode() { + int result = Objects.hash(appIndex); + result = 31 * result + Arrays.hashCode(name); + return result; + } + + @Override + public String toString() { + return "BoxReference{" + + "appIndex=" + appIndex + + ", name=" + Arrays.toString(name) + + '}'; + } + } } diff --git a/src/main/java/com/algorand/algosdk/util/BoxQueryEncoding.java b/src/main/java/com/algorand/algosdk/util/BoxQueryEncoding.java new file mode 100644 index 000000000..d1689d9d4 --- /dev/null +++ b/src/main/java/com/algorand/algosdk/util/BoxQueryEncoding.java @@ -0,0 +1,29 @@ +package com.algorand.algosdk.util; + +import com.algorand.algosdk.transaction.Transaction; +import com.algorand.algosdk.v2.client.model.Box; +import com.algorand.algosdk.v2.client.model.BoxDescriptor; + +/** + * BoxQueryEncoding provides convenience methods to String encode box names for use with Box search APIs (e.g. GetApplicationBoxByName). + */ +public final class BoxQueryEncoding { + + private static final String ENCODING_BASE64_PREFIX = "b64:"; + + public static String encodeBytes(byte[] xs) { + return ENCODING_BASE64_PREFIX + Encoder.encodeToBase64(xs); + } + + public static String encodeBox(Box b) { + return encodeBytes(b.name); + } + + public static String encodeBoxDescriptor(BoxDescriptor b) { + return encodeBytes(b.name); + } + + public static String encodeBoxReference(Transaction.BoxReference br) { + return encodeBytes(br.getName()); + } +} diff --git a/src/main/java/com/algorand/algosdk/v2/client/algod/GetApplicationBoxByName.java b/src/main/java/com/algorand/algosdk/v2/client/algod/GetApplicationBoxByName.java new file mode 100644 index 000000000..2eab16704 --- /dev/null +++ b/src/main/java/com/algorand/algosdk/v2/client/algod/GetApplicationBoxByName.java @@ -0,0 +1,82 @@ +package com.algorand.algosdk.v2.client.algod; + +import com.algorand.algosdk.v2.client.common.Client; +import com.algorand.algosdk.v2.client.common.HttpMethod; +import com.algorand.algosdk.v2.client.common.Query; +import com.algorand.algosdk.v2.client.common.QueryData; +import com.algorand.algosdk.v2.client.common.Response; +import com.algorand.algosdk.v2.client.model.Box; + + +/** + * Given an application ID and box name, it returns the box name and value (each + * base64 encoded). Box names must be in the goal app call arg encoding form + * 'encoding:value'. For ints, use the form 'int:1234'. For raw bytes, use the form + * 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use + * the form 'addr:XYZ...'. + * /v2/applications/{application-id}/box + */ +public class GetApplicationBoxByName extends Query { + + private Long applicationId; + + /** + * @param applicationId An application identifier + */ + public GetApplicationBoxByName(Client client, Long applicationId) { + super(client, new HttpMethod("get")); + this.applicationId = applicationId; + } + + /** + * A box name, in the goal app call arg form 'encoding:value'. For ints, use the + * form 'int:1234'. For raw bytes, use the form 'b64:A=='. For printable strings, + * use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'. + */ + public GetApplicationBoxByName name(String name) { + addQuery("name", String.valueOf(name)); + return this; + } + + /** + * Execute the query. + * @return the query response object. + * @throws Exception + */ + @Override + public Response execute() throws Exception { + Response resp = baseExecute(); + resp.setValueType(Box.class); + return resp; + } + + /** + * Execute the query with custom headers, there must be an equal number of keys and values + * or else an error will be generated. + * @param headers an array of header keys + * @param values an array of header values + * @return the query response object. + * @throws Exception + */ + @Override + public Response execute(String[] headers, String[] values) throws Exception { + Response resp = baseExecute(headers, values); + resp.setValueType(Box.class); + return resp; + } + + protected QueryData getRequestString() { + if (this.applicationId == null) { + throw new RuntimeException("application-id is not set. It is a required parameter."); + } + if (!qd.queries.containsKey("name")) { + throw new RuntimeException("name is not set. It is a required parameter."); + } + addPathSegment(String.valueOf("v2")); + addPathSegment(String.valueOf("applications")); + addPathSegment(String.valueOf(applicationId)); + addPathSegment(String.valueOf("box")); + + return qd; + } +} diff --git a/src/main/java/com/algorand/algosdk/v2/client/algod/GetApplicationBoxes.java b/src/main/java/com/algorand/algosdk/v2/client/algod/GetApplicationBoxes.java new file mode 100644 index 000000000..f8a734254 --- /dev/null +++ b/src/main/java/com/algorand/algosdk/v2/client/algod/GetApplicationBoxes.java @@ -0,0 +1,76 @@ +package com.algorand.algosdk.v2.client.algod; + +import com.algorand.algosdk.v2.client.common.Client; +import com.algorand.algosdk.v2.client.common.HttpMethod; +import com.algorand.algosdk.v2.client.common.Query; +import com.algorand.algosdk.v2.client.common.QueryData; +import com.algorand.algosdk.v2.client.common.Response; +import com.algorand.algosdk.v2.client.model.BoxesResponse; + + +/** + * Given an application ID, return all Box names. No particular ordering is + * guaranteed. Request fails when client or server-side configured limits prevent + * returning all Box names. + * /v2/applications/{application-id}/boxes + */ +public class GetApplicationBoxes extends Query { + + private Long applicationId; + + /** + * @param applicationId An application identifier + */ + public GetApplicationBoxes(Client client, Long applicationId) { + super(client, new HttpMethod("get")); + this.applicationId = applicationId; + } + + /** + * Max number of box names to return. If max is not set, or max == 0, returns all + * box-names. + */ + public GetApplicationBoxes max(Long max) { + addQuery("max", String.valueOf(max)); + return this; + } + + /** + * Execute the query. + * @return the query response object. + * @throws Exception + */ + @Override + public Response execute() throws Exception { + Response resp = baseExecute(); + resp.setValueType(BoxesResponse.class); + return resp; + } + + /** + * Execute the query with custom headers, there must be an equal number of keys and values + * or else an error will be generated. + * @param headers an array of header keys + * @param values an array of header values + * @return the query response object. + * @throws Exception + */ + @Override + public Response execute(String[] headers, String[] values) throws Exception { + Response resp = baseExecute(headers, values); + resp.setValueType(BoxesResponse.class); + return resp; + } + + protected QueryData getRequestString() { + if (this.applicationId == null) { + throw new RuntimeException("application-id is not set. It is a required parameter."); + } + addPathSegment(String.valueOf("v2")); + addPathSegment(String.valueOf("applications")); + addPathSegment(String.valueOf(applicationId)); + addPathSegment(String.valueOf("boxes")); + + return qd; + } +} diff --git a/src/main/java/com/algorand/algosdk/v2/client/common/AlgodClient.java b/src/main/java/com/algorand/algosdk/v2/client/common/AlgodClient.java index f37482abf..6211aefa2 100644 --- a/src/main/java/com/algorand/algosdk/v2/client/common/AlgodClient.java +++ b/src/main/java/com/algorand/algosdk/v2/client/common/AlgodClient.java @@ -22,6 +22,8 @@ import com.algorand.algosdk.v2.client.algod.GetStateProof; import com.algorand.algosdk.v2.client.algod.GetLightBlockHeaderProof; import com.algorand.algosdk.v2.client.algod.GetApplicationByID; +import com.algorand.algosdk.v2.client.algod.GetApplicationBoxes; +import com.algorand.algosdk.v2.client.algod.GetApplicationBoxByName; import com.algorand.algosdk.v2.client.algod.GetAssetByID; import com.algorand.algosdk.v2.client.algod.TealCompile; import com.algorand.algosdk.v2.client.algod.TealDisassemble; @@ -249,6 +251,28 @@ public GetApplicationByID GetApplicationByID(Long applicationId) { return new GetApplicationByID((Client) this, applicationId); } + /** + * Given an application ID, return all Box names. No particular ordering is + * guaranteed. Request fails when client or server-side configured limits prevent + * returning all Box names. + * /v2/applications/{application-id}/boxes + */ + public GetApplicationBoxes GetApplicationBoxes(Long applicationId) { + return new GetApplicationBoxes((Client) this, applicationId); + } + + /** + * Given an application ID and box name, it returns the box name and value (each + * base64 encoded). Box names must be in the goal app call arg encoding form + * 'encoding:value'. For ints, use the form 'int:1234'. For raw bytes, use the form + * 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use + * the form 'addr:XYZ...'. + * /v2/applications/{application-id}/box + */ + public GetApplicationBoxByName GetApplicationBoxByName(Long applicationId) { + return new GetApplicationBoxByName((Client) this, applicationId); + } + /** * Given a asset ID, it returns asset information including creator, name, total * supply and special addresses. diff --git a/src/main/java/com/algorand/algosdk/v2/client/common/Client.java b/src/main/java/com/algorand/algosdk/v2/client/common/Client.java index 37569dd92..fc5f28750 100644 --- a/src/main/java/com/algorand/algosdk/v2/client/common/Client.java +++ b/src/main/java/com/algorand/algosdk/v2/client/common/Client.java @@ -48,7 +48,7 @@ public static HttpUrl getHttpUrl(QueryData qData, int port, String host) { httpUrlBuilder.port(port); for (String ps : qData.pathSegments) { - httpUrlBuilder.addPathSegment(ps); + httpUrlBuilder.addEncodedPathSegment(ps); } for (Entry kvp : qData.queries.entrySet()) { try { diff --git a/src/main/java/com/algorand/algosdk/v2/client/common/IndexerClient.java b/src/main/java/com/algorand/algosdk/v2/client/common/IndexerClient.java index 6661c80d5..3ca218bbf 100644 --- a/src/main/java/com/algorand/algosdk/v2/client/common/IndexerClient.java +++ b/src/main/java/com/algorand/algosdk/v2/client/common/IndexerClient.java @@ -10,6 +10,8 @@ import com.algorand.algosdk.v2.client.indexer.LookupAccountTransactions; import com.algorand.algosdk.v2.client.indexer.SearchForApplications; import com.algorand.algosdk.v2.client.indexer.LookupApplicationByID; +import com.algorand.algosdk.v2.client.indexer.SearchForApplicationBoxes; +import com.algorand.algosdk.v2.client.indexer.LookupApplicationBoxByIDAndName; import com.algorand.algosdk.v2.client.indexer.LookupApplicationLogsByID; import com.algorand.algosdk.v2.client.indexer.SearchForAssets; import com.algorand.algosdk.v2.client.indexer.LookupAssetByID; @@ -133,6 +135,27 @@ public LookupApplicationByID lookupApplicationByID(Long applicationId) { return new LookupApplicationByID((Client) this, applicationId); } + /** + * Given an application ID, returns the box names of that application sorted + * lexicographically. + * /v2/applications/{application-id}/boxes + */ + public SearchForApplicationBoxes searchForApplicationBoxes(Long applicationId) { + return new SearchForApplicationBoxes((Client) this, applicationId); + } + + /** + * Given an application ID and box name, returns base64 encoded box name and value. + * Box names must be in the goal app call arg form 'encoding:value'. For ints, use + * the form 'int:1234'. For raw bytes, encode base 64 and use 'b64' prefix as in + * 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use + * the form 'addr:XYZ...'. + * /v2/applications/{application-id}/box + */ + public LookupApplicationBoxByIDAndName lookupApplicationBoxByIDAndName(Long applicationId) { + return new LookupApplicationBoxByIDAndName((Client) this, applicationId); + } + /** * Lookup application logs. * /v2/applications/{application-id}/logs diff --git a/src/main/java/com/algorand/algosdk/v2/client/common/Query.java b/src/main/java/com/algorand/algosdk/v2/client/common/Query.java index e66a10d90..20901e05f 100644 --- a/src/main/java/com/algorand/algosdk/v2/client/common/Query.java +++ b/src/main/java/com/algorand/algosdk/v2/client/common/Query.java @@ -17,11 +17,11 @@ protected Query(Client client, HttpMethod httpMethod) { protected abstract QueryData getRequestString(); - protected Response baseExecute() throws Exception { + protected Response baseExecute() throws Exception { return baseExecute(null, null); } - protected Response baseExecute(String[] headers, String[] values) throws Exception { + protected Response baseExecute(String[] headers, String[] values) throws Exception { QueryData qData = this.getRequestString(); com.squareup.okhttp.Response resp = this.client.executeCall(qData, httpMethod, headers, values); @@ -69,5 +69,6 @@ protected void addToBody(Object content) { } public abstract Response execute() throws Exception; + public abstract Response execute(String[] headers, String[] values) throws Exception; } diff --git a/src/main/java/com/algorand/algosdk/v2/client/indexer/LookupApplicationBoxByIDAndName.java b/src/main/java/com/algorand/algosdk/v2/client/indexer/LookupApplicationBoxByIDAndName.java new file mode 100644 index 000000000..e4de527c7 --- /dev/null +++ b/src/main/java/com/algorand/algosdk/v2/client/indexer/LookupApplicationBoxByIDAndName.java @@ -0,0 +1,82 @@ +package com.algorand.algosdk.v2.client.indexer; + +import com.algorand.algosdk.v2.client.common.Client; +import com.algorand.algosdk.v2.client.common.HttpMethod; +import com.algorand.algosdk.v2.client.common.Query; +import com.algorand.algosdk.v2.client.common.QueryData; +import com.algorand.algosdk.v2.client.common.Response; +import com.algorand.algosdk.v2.client.model.Box; + + +/** + * Given an application ID and box name, returns base64 encoded box name and value. + * Box names must be in the goal app call arg form 'encoding:value'. For ints, use + * the form 'int:1234'. For raw bytes, encode base 64 and use 'b64' prefix as in + * 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use + * the form 'addr:XYZ...'. + * /v2/applications/{application-id}/box + */ +public class LookupApplicationBoxByIDAndName extends Query { + + private Long applicationId; + + /** + * @param applicationId + */ + public LookupApplicationBoxByIDAndName(Client client, Long applicationId) { + super(client, new HttpMethod("get")); + this.applicationId = applicationId; + } + + /** + * A box name in goal-arg form 'encoding:value'. For ints, use the form 'int:1234'. + * For raw bytes, use the form 'b64:A=='. For printable strings, use the form + * 'str:hello'. For addresses, use the form 'addr:XYZ...'. + */ + public LookupApplicationBoxByIDAndName name(String name) { + addQuery("name", String.valueOf(name)); + return this; + } + + /** + * Execute the query. + * @return the query response object. + * @throws Exception + */ + @Override + public Response execute() throws Exception { + Response resp = baseExecute(); + resp.setValueType(Box.class); + return resp; + } + + /** + * Execute the query with custom headers, there must be an equal number of keys and values + * or else an error will be generated. + * @param headers an array of header keys + * @param values an array of header values + * @return the query response object. + * @throws Exception + */ + @Override + public Response execute(String[] headers, String[] values) throws Exception { + Response resp = baseExecute(headers, values); + resp.setValueType(Box.class); + return resp; + } + + protected QueryData getRequestString() { + if (this.applicationId == null) { + throw new RuntimeException("application-id is not set. It is a required parameter."); + } + if (!qd.queries.containsKey("name")) { + throw new RuntimeException("name is not set. It is a required parameter."); + } + addPathSegment(String.valueOf("v2")); + addPathSegment(String.valueOf("applications")); + addPathSegment(String.valueOf(applicationId)); + addPathSegment(String.valueOf("box")); + + return qd; + } +} diff --git a/src/main/java/com/algorand/algosdk/v2/client/indexer/SearchForApplicationBoxes.java b/src/main/java/com/algorand/algosdk/v2/client/indexer/SearchForApplicationBoxes.java new file mode 100644 index 000000000..5cabe8a26 --- /dev/null +++ b/src/main/java/com/algorand/algosdk/v2/client/indexer/SearchForApplicationBoxes.java @@ -0,0 +1,83 @@ +package com.algorand.algosdk.v2.client.indexer; + +import com.algorand.algosdk.v2.client.common.Client; +import com.algorand.algosdk.v2.client.common.HttpMethod; +import com.algorand.algosdk.v2.client.common.Query; +import com.algorand.algosdk.v2.client.common.QueryData; +import com.algorand.algosdk.v2.client.common.Response; +import com.algorand.algosdk.v2.client.model.BoxesResponse; + + +/** + * Given an application ID, returns the box names of that application sorted + * lexicographically. + * /v2/applications/{application-id}/boxes + */ +public class SearchForApplicationBoxes extends Query { + + private Long applicationId; + + /** + * @param applicationId + */ + public SearchForApplicationBoxes(Client client, Long applicationId) { + super(client, new HttpMethod("get")); + this.applicationId = applicationId; + } + + /** + * Maximum number of results to return. There could be additional pages even if the + * limit is not reached. + */ + public SearchForApplicationBoxes limit(Long limit) { + addQuery("limit", String.valueOf(limit)); + return this; + } + + /** + * The next page of results. Use the next token provided by the previous results. + */ + public SearchForApplicationBoxes next(String next) { + addQuery("next", String.valueOf(next)); + return this; + } + + /** + * Execute the query. + * @return the query response object. + * @throws Exception + */ + @Override + public Response execute() throws Exception { + Response resp = baseExecute(); + resp.setValueType(BoxesResponse.class); + return resp; + } + + /** + * Execute the query with custom headers, there must be an equal number of keys and values + * or else an error will be generated. + * @param headers an array of header keys + * @param values an array of header values + * @return the query response object. + * @throws Exception + */ + @Override + public Response execute(String[] headers, String[] values) throws Exception { + Response resp = baseExecute(headers, values); + resp.setValueType(BoxesResponse.class); + return resp; + } + + protected QueryData getRequestString() { + if (this.applicationId == null) { + throw new RuntimeException("application-id is not set. It is a required parameter."); + } + addPathSegment(String.valueOf("v2")); + addPathSegment(String.valueOf("applications")); + addPathSegment(String.valueOf(applicationId)); + addPathSegment(String.valueOf("boxes")); + + return qd; + } +} diff --git a/src/main/java/com/algorand/algosdk/v2/client/model/Account.java b/src/main/java/com/algorand/algosdk/v2/client/model/Account.java index 8c11e9905..473d551e8 100644 --- a/src/main/java/com/algorand/algosdk/v2/client/model/Account.java +++ b/src/main/java/com/algorand/algosdk/v2/client/model/Account.java @@ -194,6 +194,20 @@ public String authAddr() throws NoSuchAlgorithmException { @JsonProperty("total-assets-opted-in") public Long totalAssetsOptedIn; + /** + * For app-accounts only. The total number of bytes allocated for the keys and + * values of boxes which belong to the associated application. + */ + @JsonProperty("total-box-bytes") + public Long totalBoxBytes; + + /** + * For app-accounts only. The total number of boxes which belong to the associated + * application. + */ + @JsonProperty("total-boxes") + public Long totalBoxes; + /** * The count of all apps (AppParams objects) created by this account. */ @@ -235,6 +249,8 @@ public boolean equals(Object o) { if (!Objects.deepEquals(this.status, other.status)) return false; if (!Objects.deepEquals(this.totalAppsOptedIn, other.totalAppsOptedIn)) return false; if (!Objects.deepEquals(this.totalAssetsOptedIn, other.totalAssetsOptedIn)) return false; + if (!Objects.deepEquals(this.totalBoxBytes, other.totalBoxBytes)) return false; + if (!Objects.deepEquals(this.totalBoxes, other.totalBoxes)) return false; if (!Objects.deepEquals(this.totalCreatedApps, other.totalCreatedApps)) return false; if (!Objects.deepEquals(this.totalCreatedAssets, other.totalCreatedAssets)) return false; diff --git a/src/main/java/com/algorand/algosdk/v2/client/model/Box.java b/src/main/java/com/algorand/algosdk/v2/client/model/Box.java new file mode 100644 index 000000000..326593c4a --- /dev/null +++ b/src/main/java/com/algorand/algosdk/v2/client/model/Box.java @@ -0,0 +1,50 @@ +package com.algorand.algosdk.v2.client.model; + +import java.util.Objects; + +import com.algorand.algosdk.util.Encoder; +import com.algorand.algosdk.v2.client.common.PathResponse; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Box name and its content. + */ +public class Box extends PathResponse { + + /** + * (name) box name, base64 encoded + */ + @JsonProperty("name") + public void name(String base64Encoded) { + this.name = Encoder.decodeFromBase64(base64Encoded); + } + public String name() { + return Encoder.encodeToBase64(this.name); + } + public byte[] name; + + /** + * (value) box value, base64 encoded. + */ + @JsonProperty("value") + public void value(String base64Encoded) { + this.value = Encoder.decodeFromBase64(base64Encoded); + } + public String value() { + return Encoder.encodeToBase64(this.value); + } + public byte[] value; + + @Override + public boolean equals(Object o) { + + if (this == o) return true; + if (o == null) return false; + + Box other = (Box) o; + if (!Objects.deepEquals(this.name, other.name)) return false; + if (!Objects.deepEquals(this.value, other.value)) return false; + + return true; + } +} diff --git a/src/main/java/com/algorand/algosdk/v2/client/model/BoxDescriptor.java b/src/main/java/com/algorand/algosdk/v2/client/model/BoxDescriptor.java new file mode 100644 index 000000000..7e12b7475 --- /dev/null +++ b/src/main/java/com/algorand/algosdk/v2/client/model/BoxDescriptor.java @@ -0,0 +1,37 @@ +package com.algorand.algosdk.v2.client.model; + +import java.util.Objects; + +import com.algorand.algosdk.util.Encoder; +import com.algorand.algosdk.v2.client.common.PathResponse; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Box descriptor describes an app box without a value. + */ +public class BoxDescriptor extends PathResponse { + + /** + * Base64 encoded box name + */ + @JsonProperty("name") + public void name(String base64Encoded) { + this.name = Encoder.decodeFromBase64(base64Encoded); + } + public String name() { + return Encoder.encodeToBase64(this.name); + } + public byte[] name; + + @Override + public boolean equals(Object o) { + + if (this == o) return true; + if (o == null) return false; + + BoxDescriptor other = (BoxDescriptor) o; + if (!Objects.deepEquals(this.name, other.name)) return false; + + return true; + } +} diff --git a/src/main/java/com/algorand/algosdk/v2/client/model/BoxesResponse.java b/src/main/java/com/algorand/algosdk/v2/client/model/BoxesResponse.java new file mode 100644 index 000000000..7a9ea9af5 --- /dev/null +++ b/src/main/java/com/algorand/algosdk/v2/client/model/BoxesResponse.java @@ -0,0 +1,44 @@ +package com.algorand.algosdk.v2.client.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import com.algorand.algosdk.v2.client.common.PathResponse; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Box names of an application + */ +public class BoxesResponse extends PathResponse { + + /** + * (appidx) application index. + */ + @JsonProperty("application-id") + public Long applicationId; + + @JsonProperty("boxes") + public List boxes = new ArrayList(); + + /** + * Used for pagination, when making another request provide this token with the + * next parameter. + */ + @JsonProperty("next-token") + public String nextToken; + + @Override + public boolean equals(Object o) { + + if (this == o) return true; + if (o == null) return false; + + BoxesResponse other = (BoxesResponse) o; + if (!Objects.deepEquals(this.applicationId, other.applicationId)) return false; + if (!Objects.deepEquals(this.boxes, other.boxes)) return false; + if (!Objects.deepEquals(this.nextToken, other.nextToken)) return false; + + return true; + } +} diff --git a/src/test/integration.tags b/src/test/integration.tags index c9421bf47..91dace71b 100644 --- a/src/test/integration.tags +++ b/src/test/integration.tags @@ -1,6 +1,6 @@ @abi @algod -@applications +@applications.boxes @applications.verified @assets @auction diff --git a/src/test/java/com/algorand/algosdk/cucumber/shared/TransactionSteps.java b/src/test/java/com/algorand/algosdk/cucumber/shared/TransactionSteps.java index e44e68e9a..06634ddb4 100644 --- a/src/test/java/com/algorand/algosdk/cucumber/shared/TransactionSteps.java +++ b/src/test/java/com/algorand/algosdk/cucumber/shared/TransactionSteps.java @@ -47,8 +47,8 @@ public TransactionSteps(Base b) { this.base = b; } - @When("I build an application transaction with operation {string}, application-id {long}, sender {string}, approval-program {string}, clear-program {string}, global-bytes {long}, global-ints {long}, local-bytes {long}, local-ints {long}, app-args {string}, foreign-apps {string}, foreign-assets {string}, app-accounts {string}, fee {long}, first-valid {long}, last-valid {long}, genesis-hash {string}, extra-pages {long}") - public void buildApplicationTransactions(String operation, Long applicationId, String sender, String approvalProgramFile, String clearProgramFile, Long globalBytes, Long globalInts, Long localBytes, Long localInts, String appArgs, String foreignApps, String foreignAssets, String appAccounts, Long fee, Long firstValid, Long lastValid, String genesisHash, Long extraPages) throws Exception { + @When("I build an application transaction with operation {string}, application-id {long}, sender {string}, approval-program {string}, clear-program {string}, global-bytes {long}, global-ints {long}, local-bytes {long}, local-ints {long}, app-args {string}, foreign-apps {string}, foreign-assets {string}, app-accounts {string}, fee {long}, first-valid {long}, last-valid {long}, genesis-hash {string}, extra-pages {long}, boxes {string}") + public void buildApplicationTransactions(String operation, Long applicationId, String sender, String approvalProgramFile, String clearProgramFile, Long globalBytes, Long globalInts, Long localBytes, Long localInts, String appArgs, String foreignApps, String foreignAssets, String appAccounts, Long fee, Long firstValid, Long lastValid, String genesisHash, Long extraPages, String boxesStr) throws Exception { ApplicationBaseTransactionBuilder builder = null; // Create builder and apply builder-specific parameters @@ -116,6 +116,9 @@ public void buildApplicationTransactions(String operation, Long applicationId, S if (StringUtils.isNotEmpty(genesisHash)) { builder.genesisHashB64(genesisHash); } + if (StringUtils.isNotEmpty(boxesStr)) { + builder.boxReferences(convertBoxes(boxesStr)); + } builtTransaction = builder.build(); } diff --git a/src/test/java/com/algorand/algosdk/integration/Applications.java b/src/test/java/com/algorand/algosdk/integration/Applications.java index 05f56fba4..c862fcfe6 100644 --- a/src/test/java/com/algorand/algosdk/integration/Applications.java +++ b/src/test/java/com/algorand/algosdk/integration/Applications.java @@ -5,6 +5,7 @@ import com.algorand.algosdk.logic.StateSchema; import com.algorand.algosdk.transaction.SignedTransaction; import com.algorand.algosdk.transaction.Transaction; +import com.algorand.algosdk.util.ComparableBytes; import com.algorand.algosdk.util.Digester; import com.algorand.algosdk.util.Encoder; import com.algorand.algosdk.v2.client.Utils; @@ -14,12 +15,19 @@ import io.cucumber.java.en.Then; import org.apache.commons.lang3.StringUtils; import org.assertj.core.api.Assertions; +import org.assertj.core.util.Lists; +import org.bouncycastle.util.Strings; +import org.junit.Assert; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import static com.algorand.algosdk.util.ResourceUtils.loadTEALProgramFromFile; @@ -42,8 +50,8 @@ public Applications(TransientAccount transientAccount, Clients clients, Stepdefs this.base = base; } - @Given("I build an application transaction with the transient account, the current application, suggested params, operation {string}, approval-program {string}, clear-program {string}, global-bytes {long}, global-ints {long}, local-bytes {long}, local-ints {long}, app-args {string}, foreign-apps {string}, foreign-assets {string}, app-accounts {string}, extra-pages {long}") - public void buildAnApplicationTransactions(String operation, String approvalProgramFile, String clearProgramFile, Long globalBytes, Long globalInts, Long localBytes, Long localInts, String appArgs, String foreignApps, String foreignAssets, String appAccounts, Long extraPages) throws Exception { + @Given("I build an application transaction with the transient account, the current application, suggested params, operation {string}, approval-program {string}, clear-program {string}, global-bytes {long}, global-ints {long}, local-bytes {long}, local-ints {long}, app-args {string}, foreign-apps {string}, foreign-assets {string}, app-accounts {string}, extra-pages {long}, boxes {string}") + public void buildAnApplicationTransactions(String operation, String approvalProgramFile, String clearProgramFile, Long globalBytes, Long globalInts, Long localBytes, Long localInts, String appArgs, String foreignApps, String foreignAssets, String appAccounts, Long extraPages, String boxesStr) throws Exception { ApplicationBaseTransactionBuilder builder = null; // Create builder and apply builder-specific parameters @@ -101,6 +109,9 @@ public void buildAnApplicationTransactions(String operation, String approvalProg if (StringUtils.isNotEmpty(appAccounts)) { builder.accounts(convertAccounts(appAccounts)); } + if (StringUtils.isNotEmpty(boxesStr)) { + builder.boxReferences(convertBoxes(boxesStr)); + } // Send with transient account, suggested params and current application builder.sender(this.transientAccount.transientAccount.getAddress()); @@ -247,4 +258,103 @@ public void checkAccountData( assertThat(found).as("Couldn't find key '%s'", hasKey).isTrue(); } + + @Then("according to {string}, the contents of the box with name {string} in the current application should be {string}. If there is an error it is {string}.") + public void contentsOfBoxShouldBe(String fromClient, String encodedBoxName, String boxContents, String errStr) throws Exception { + Response boxResp; + if (fromClient.equals("algod")) + boxResp = clients.v2Client.GetApplicationBoxByName(this.appId).name(encodedBoxName).execute(); + else if (fromClient.equals("indexer")) + boxResp = clients.v2IndexerClient.lookupApplicationBoxByIDAndName(this.appId).name(encodedBoxName).execute(); + else + throw new IllegalArgumentException("expecting algod or indexer, got " + fromClient); + + // If an error was expected, make sure it is set correctly. + if (StringUtils.isNotEmpty(errStr)) { + assertThat(boxResp.isSuccessful()).isFalse(); + assertThat(boxResp.message()).containsIgnoringCase(errStr); + return; + } + + assertThat(boxResp.body().value()).isEqualTo(boxContents); + } + + private static void assertSetOfByteArraysEqual(Set expected, Set actual) { + Set expectedComparable = new HashSet<>(); + for (byte[] element : expected) { + expectedComparable.add(new ComparableBytes(element)); + } + + Set actualComparable = new HashSet<>(); + for (byte[] element : actual) { + actualComparable.add(new ComparableBytes(element)); + } + + Assert.assertEquals(expectedComparable, actualComparable); + } + + @Then("according to {string}, the current application should have the following boxes {string}.") + public void checkAppBoxes(String fromClient, String encodedBoxesRaw) throws Exception { + Response r; + if (fromClient.equals("algod")) + r = clients.v2Client.GetApplicationBoxes(this.appId).execute(); + else if (fromClient.equals("indexer")) + r = clients.v2IndexerClient.searchForApplicationBoxes(this.appId).execute(); + else + throw new IllegalArgumentException("expecting algod or indexer, got " + fromClient); + + Assert.assertTrue(r.isSuccessful()); + + final Set expectedNames = new HashSet<>(); + if (!encodedBoxesRaw.isEmpty()) { + for (String s : Strings.split(encodedBoxesRaw, ':')) { + expectedNames.add(Encoder.decodeFromBase64(s)); + } + } + + final Set actualNames = new HashSet<>(); + for (BoxDescriptor b : r.body().boxes) { + actualNames.add(b.name); + } + + assertSetOfByteArraysEqual(expectedNames, actualNames); + } + + @Then("according to {string}, with {long} being the parameter that limits results, the current application should have {int} boxes.") + public void checkAppBoxesNum(String fromClient, Long limit, int expected_num) throws Exception { + Response r; + if (fromClient.equals("algod")) + r = clients.v2Client.GetApplicationBoxes(this.appId).max(limit).execute(); + else if (fromClient.equals("indexer")) + r = clients.v2IndexerClient.searchForApplicationBoxes(this.appId).limit(limit).execute(); + else + throw new IllegalArgumentException("expecting algod or indexer, got " + fromClient); + + Assert.assertTrue(r.isSuccessful()); + Assert.assertEquals("expected " + expected_num + " boxes, actual " + r.body().boxes.size(), + r.body().boxes.size(), expected_num); + } + + @Then("according to indexer, with {long} being the parameter that limits results, and {string} being the parameter that sets the next result, the current application should have the following boxes {string}.") + public void indexerCheckAppBoxesWithParams(Long limit, String next, String encodedBoxesRaw) throws Exception { + Response r = clients.v2IndexerClient.searchForApplicationBoxes(this.appId).limit(limit).next(next).execute(); + final Set expectedNames = new HashSet<>(); + if (!encodedBoxesRaw.isEmpty()) { + for (String s : Strings.split(encodedBoxesRaw, ':')) { + expectedNames.add(Encoder.decodeFromBase64(s)); + } + } + + final Set actualNames = new HashSet<>(); + for (BoxDescriptor b : r.body().boxes) { + actualNames.add(b.name); + } + + assertSetOfByteArraysEqual(expectedNames, actualNames); + } + + @Then("I sleep for {int} milliseconds for indexer to digest things down.") + public void sleepForNSecondsForIndexer(int milliseconds) throws Exception { + Thread.sleep(milliseconds); + } } diff --git a/src/test/java/com/algorand/algosdk/integration/Clients.java b/src/test/java/com/algorand/algosdk/integration/Clients.java index 4f4b61b22..562f9316f 100644 --- a/src/test/java/com/algorand/algosdk/integration/Clients.java +++ b/src/test/java/com/algorand/algosdk/integration/Clients.java @@ -9,10 +9,16 @@ public class Clients { AlgodClient v2Client = null; - Map indexerClients = new HashMap<>(); + + IndexerClient v2IndexerClient = null; @Given("an algod v2 client connected to {string} port {int} with token {string}") public void an_algod_v2_client_connected_to_port_with_token(String host, Integer port, String token) { v2Client = new AlgodClient(host, port, token); } + + @Given("an indexer v2 client") + public void indexer_v2_client() { + v2IndexerClient = new IndexerClient("localhost", 59999); + } } diff --git a/src/test/java/com/algorand/algosdk/transaction/TestAppBoxReference.java b/src/test/java/com/algorand/algosdk/transaction/TestAppBoxReference.java new file mode 100644 index 000000000..300ddb208 --- /dev/null +++ b/src/test/java/com/algorand/algosdk/transaction/TestAppBoxReference.java @@ -0,0 +1,36 @@ +package com.algorand.algosdk.transaction; + +import org.junit.Assert; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; + +public class TestAppBoxReference { + + private AppBoxReference genConstant() { + return new AppBoxReference(5, "example".getBytes(StandardCharsets.US_ASCII)); + } + + @Test + public void testEqualsByReference() { + Assert.assertEquals(genConstant(), genConstant()); + } + + @Test + public void testHashCode() { + Assert.assertEquals(genConstant().hashCode(), genConstant().hashCode()); + } + + @Test + public void testNameQueryEncoding() { + String example = "tkÿÿ"; + String expectedEncoding = "b64:dGvDv8O/"; + + AppBoxReference abr = new AppBoxReference(0, example.getBytes(StandardCharsets.UTF_8)); + + Assert.assertEquals( + expectedEncoding, + abr.nameQueryEncoded() + ); + } +} diff --git a/src/test/java/com/algorand/algosdk/transaction/TestBoxReference.java b/src/test/java/com/algorand/algosdk/transaction/TestBoxReference.java new file mode 100644 index 000000000..4594deea7 --- /dev/null +++ b/src/test/java/com/algorand/algosdk/transaction/TestBoxReference.java @@ -0,0 +1,77 @@ +package com.algorand.algosdk.transaction; + +import com.google.common.collect.Lists; +import org.junit.Assert; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; + +public class TestBoxReference { + + private AppBoxReference genWithAppId(long appId) { + return new AppBoxReference(appId, "example".getBytes(StandardCharsets.US_ASCII)); + } + + private AppBoxReference genWithNewAppId() { + return new AppBoxReference(0L, "example".getBytes(StandardCharsets.US_ASCII)); + } + + @Test + public void testAppIndexExists() { + long appId = 7; + AppBoxReference abr = genWithAppId(appId); + + Assert.assertEquals( + new Transaction.BoxReference(4, abr.getName()), + Transaction.BoxReference.fromAppBoxReference( + abr, + Lists.newArrayList(1L, 3L, 4L, appId), + appId - 1 + ) + ); + } + + @Test(expected = RuntimeException.class) + public void testAppIndexDoesNotExist() { + long appId = 7; + AppBoxReference abr = genWithAppId(appId); + + Assert.assertEquals( + new Transaction.BoxReference(4, abr.getName()), + Transaction.BoxReference.fromAppBoxReference( + abr, + Lists.newArrayList(1L, 3L, 4L), + appId - 1 + ) + ); + } + + @Test + public void testNewAppId() { + AppBoxReference abr = genWithNewAppId(); + Assert.assertEquals( + new Transaction.BoxReference(0, abr.getName()), + Transaction.BoxReference.fromAppBoxReference( + abr, Lists.newArrayList(), 1L)); + } + + @Test + public void testFallbackToCurrentApp() { + // Mirrors priority search in goal from `cmd/goal/application.go::translateBoxRefs`. + long appId = 7; + AppBoxReference abr = genWithAppId(appId); + + // Prefer foreign apps index when present. + Assert.assertEquals( + new Transaction.BoxReference(4, abr.getName()), + Transaction.BoxReference.fromAppBoxReference( + abr, Lists.newArrayList(1L, 3L, 4L, appId), appId)); + + // Fallback to current app when absent from foreign apps. + Assert.assertEquals( + new Transaction.BoxReference(0, abr.getName()), + Transaction.BoxReference.fromAppBoxReference( + abr, Lists.newArrayList(1L, 3L, 4L), appId)); + } + +} diff --git a/src/test/java/com/algorand/algosdk/transaction/TestTransaction.java b/src/test/java/com/algorand/algosdk/transaction/TestTransaction.java index 1a63b67f7..88801ef18 100644 --- a/src/test/java/com/algorand/algosdk/transaction/TestTransaction.java +++ b/src/test/java/com/algorand/algosdk/transaction/TestTransaction.java @@ -5,10 +5,8 @@ import com.algorand.algosdk.mnemonic.Mnemonic; import com.algorand.algosdk.util.Encoder; import com.algorand.algosdk.util.TestUtil; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.junit.Assert; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -18,7 +16,6 @@ import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Base64; -import java.util.Objects; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -103,6 +100,8 @@ public void testApplicationTransactionJsonSerialization() throws Exception { .firstValid(301) .lastValid(1300) .genesisHash(new Digest()) + .foreignApps(Arrays.asList(10L)) + .boxReferences(Arrays.asList(new AppBoxReference(10L, "name".getBytes()))) .build(); ObjectMapper objectMapper = new ObjectMapper(); @@ -113,6 +112,32 @@ public void testApplicationTransactionJsonSerialization() throws Exception { assertThat(transactionJson).isEqualTo(transactionJson1); } + @Test + public void testApplicationTransactionWithBoxes() throws Exception { + Address from = new Address("VKM6KSCTDHEM6KGEAMSYCNEGIPFJMHDSEMIRAQLK76CJDIRMMDHKAIRMFQ"); + Transaction tx = Transaction.ApplicationUpdateTransactionBuilder() + .sender(from) + .applicationId(100000L) + .firstValid(301) + .lastValid(1300) + .genesisHash(new Digest()) + .foreignApps(Arrays.asList(10L, 100000L)) + .boxReferences(Arrays.asList( + new AppBoxReference(10L, "name".getBytes(StandardCharsets.US_ASCII)), + new AppBoxReference(100000L, "name2".getBytes(StandardCharsets.US_ASCII)), + new AppBoxReference(0L, "name3".getBytes(StandardCharsets.US_ASCII)))) + .build(); + + Assert.assertEquals(3, tx.boxReferences.size()); + Assert.assertArrayEquals( + new int[]{1, 2, 0}, + new int[]{ + tx.boxReferences.get(0).getAppIndex(), + tx.boxReferences.get(1).getAppIndex(), + tx.boxReferences.get(2).getAppIndex(), + }); + } + @Test public void testSerializationMsgpack() throws Exception { Address from = new Address("VKM6KSCTDHEM6KGEAMSYCNEGIPFJMHDSEMIRAQLK76CJDIRMMDHKAIRMFQ"); @@ -139,7 +164,7 @@ public void testMetadaHashBuilderMethods() throws Exception { // when given as input the same metadata hash // and that it is different when the input is different - String metadataHashUTF8 = "Hello! This is the metadata hash"; + String metadataHashUTF8 = "Hello! This is the metadata hash"; String metadataHashUTF8Different = "Hi! I am another metadata hash.."; byte[] metadataHashBytes = metadataHashUTF8.getBytes(StandardCharsets.UTF_8); // The value below is the base64 of metadataHashUTF8 diff --git a/src/test/java/com/algorand/algosdk/unit/AlgodPaths.java b/src/test/java/com/algorand/algosdk/unit/AlgodPaths.java index 222fdace5..fb9c6ba07 100644 --- a/src/test/java/com/algorand/algosdk/unit/AlgodPaths.java +++ b/src/test/java/com/algorand/algosdk/unit/AlgodPaths.java @@ -2,7 +2,11 @@ import com.algorand.algosdk.crypto.Address; import com.algorand.algosdk.unit.utils.TestingUtils; -import com.algorand.algosdk.v2.client.algod.*; +import com.algorand.algosdk.v2.client.algod.AccountInformation; +import com.algorand.algosdk.v2.client.algod.GetApplicationBoxes; +import com.algorand.algosdk.v2.client.algod.GetPendingTransactions; +import com.algorand.algosdk.v2.client.algod.GetPendingTransactionsByAddress; +import com.algorand.algosdk.v2.client.algod.GetTransactionProof; import com.algorand.algosdk.v2.client.common.AlgodClient; import com.algorand.algosdk.v2.client.model.Enums; import io.cucumber.java.en.When; @@ -67,14 +71,14 @@ public void getApplicationByID(Long id) { @When("we make an Account Application Information call against account {string} applicationID {int}") public void accountApplicationInformation(String string, Integer int1) throws NoSuchAlgorithmException { - ps.q = algodClient.AccountApplicationInformation(new Address(string), (long)int1.intValue()); + ps.q = algodClient.AccountApplicationInformation(new Address(string), (long) int1.intValue()); } - + @When("we make an Account Asset Information call against account {string} assetID {int}") public void accountAssetInformation(String string, Integer int1) throws NoSuchAlgorithmException { - ps.q = algodClient.AccountAssetInformation(new Address(string), (long)int1.intValue()); + ps.q = algodClient.AccountAssetInformation(new Address(string), (long) int1.intValue()); } - + @When("we make an Account Information call against account {string} with exclude {string}") public void accountInformation(String string, String string2) throws NoSuchAlgorithmException { AccountInformation aiq = algodClient.AccountInformation(new Address(string)); @@ -82,6 +86,20 @@ public void accountInformation(String string, String string2) throws NoSuchAlgor ps.q = aiq; } + @When("we make a GetApplicationBoxByName call for applicationID {long} with encoded box name {string}") + public void getBoxByName(Long appID, String encodedBoxName) { + ps.q = algodClient.GetApplicationBoxByName(appID).name(encodedBoxName); + } + + @When("we make a GetApplicationBoxes call for applicationID {long} with max {long}") + public void getBoxes(Long appId, Long max) { + GetApplicationBoxes q = algodClient.GetApplicationBoxes(appId); + + if (TestingUtils.notEmpty(max)) q.max(max); + + ps.q = q; + } + @When("we make a GetTransactionProof call for round {long} txid {string} and hashtype {string}") public void getTransactionProof(Long round, String txid, String hashType) { GetTransactionProof gtp = algodClient.GetTransactionProof(round, txid); diff --git a/src/test/java/com/algorand/algosdk/unit/IndexerPaths.java b/src/test/java/com/algorand/algosdk/unit/IndexerPaths.java index 2af398823..eb49c99a7 100644 --- a/src/test/java/com/algorand/algosdk/unit/IndexerPaths.java +++ b/src/test/java/com/algorand/algosdk/unit/IndexerPaths.java @@ -333,7 +333,19 @@ public void searchForApplications(String string) { ps.q = q; } + @When("we make a LookupApplicationBoxByIDandName call with applicationID {long} with encoded box name {string}") + public void lookUpApplicationBox(Long appID, String boxName) { + ps.q = indexerClient.lookupApplicationBoxByIDAndName(appID).name(boxName); + } + @When("we make a SearchForApplicationBoxes call with applicationID {long} with max {long} nextToken {string}") + public void searchApplicationBoxes(Long appID, Long maxRes, String nextToken) { + SearchForApplicationBoxes q = indexerClient.searchForApplicationBoxes(appID); + if (TestingUtils.notEmpty(maxRes)) q.limit(maxRes); + if (TestingUtils.notEmpty(nextToken)) q.next(nextToken); + ps.q = q; + } + @When("we make a Lookup Block call against round {long} and header {string}") public void anyBlockLookupCall(Long round, String headerOnly) { LookupBlock q = this.indexerClient.lookupBlock(round); diff --git a/src/test/java/com/algorand/algosdk/util/ComparableBytes.java b/src/test/java/com/algorand/algosdk/util/ComparableBytes.java new file mode 100644 index 000000000..da5bd8773 --- /dev/null +++ b/src/test/java/com/algorand/algosdk/util/ComparableBytes.java @@ -0,0 +1,31 @@ +package com.algorand.algosdk.util; + +import java.util.Arrays; + +import org.apache.commons.codec.binary.Hex; + +public class ComparableBytes { + private final byte[] data; + + public ComparableBytes(byte[] data) { + this.data = data; + } + + @Override + public String toString() { + return Hex.encodeHexString(data); + } + + @Override + public int hashCode() { + return Arrays.hashCode(data); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ComparableBytes that = (ComparableBytes) o; + return Arrays.equals(data, that.data); + } +} diff --git a/src/test/java/com/algorand/algosdk/util/ConversionUtils.java b/src/test/java/com/algorand/algosdk/util/ConversionUtils.java index 604323cc3..858d03bf9 100644 --- a/src/test/java/com/algorand/algosdk/util/ConversionUtils.java +++ b/src/test/java/com/algorand/algosdk/util/ConversionUtils.java @@ -1,11 +1,15 @@ package com.algorand.algosdk.util; import com.algorand.algosdk.crypto.Address; +import com.algorand.algosdk.transaction.AppBoxReference; + import org.assertj.core.api.Assertions; import org.bouncycastle.util.Strings; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -20,15 +24,18 @@ public static List convertArgs(String args) { .map(s -> { String[] parts = Strings.split(s, ':'); byte[] converted = null; - switch(parts[0]) { + switch (parts[0]) { case "str": converted = parts[1].getBytes(); break; case "int": converted = BigInteger.valueOf(Integer.parseInt(parts[1])).toByteArray(); break; + case "b64": + converted = Encoder.decodeFromBase64(parts[1]); + break; default: - Assertions.fail("Doesn't currently support '" + parts[0] + "' convertion."); + Assertions.fail("Doesn't currently support '" + parts[0] + "' conversion."); } return converted; }) @@ -65,4 +72,35 @@ public static List
convertAccounts(String accounts) { .collect(Collectors.toList()); } + public static List convertBoxes(String boxesStr) { + if (boxesStr.equals("")) { + return null; + } + + List boxReferences = new ArrayList<>(); + String[] boxesArray = Strings.split(boxesStr, ','); + for (int i = 0; i < boxesArray.length; i += 2) { + long appId = Long.parseLong(boxesArray[i]); + + String enc = Strings.split(boxesArray[i + 1], ':')[0]; + String strName = Strings.split(boxesArray[i + 1], ':')[1]; + + byte[] name; + switch (enc) { + case "str": + name = strName.getBytes(StandardCharsets.US_ASCII); + break; + case "b64": + name = Encoder.decodeFromBase64(strName); + break; + default: + throw new RuntimeException("Unsupported encoding = " + enc); + } + + boxReferences.add(new AppBoxReference(appId, name)); + } + + return boxReferences; + } + } diff --git a/src/test/java/com/algorand/algosdk/util/TestBoxQueryEncoding.java b/src/test/java/com/algorand/algosdk/util/TestBoxQueryEncoding.java new file mode 100644 index 000000000..01caef05f --- /dev/null +++ b/src/test/java/com/algorand/algosdk/util/TestBoxQueryEncoding.java @@ -0,0 +1,64 @@ +package com.algorand.algosdk.util; + +import com.algorand.algosdk.transaction.Transaction; +import com.algorand.algosdk.v2.client.model.Box; +import com.algorand.algosdk.v2.client.model.BoxDescriptor; +import org.junit.Assert; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; + +public class TestBoxQueryEncoding { + + private static class Example { + final String source; + final String expectedEncoding; + + public Example(String source, String expectedEncoding) { + this.source = source; + this.expectedEncoding = "b64:" + expectedEncoding; + } + } + + private final Example e = new Example("tkÿÿ", "dGvDv8O/"); + + @Test + public void testEncodeBytes() { + Assert.assertEquals( + e.expectedEncoding, + BoxQueryEncoding.encodeBytes(e.source.getBytes(StandardCharsets.UTF_8)) + ); + } + + @Test + public void testEncodeBox() { + Box b = new Box(); + b.name(Encoder.encodeToBase64(e.source.getBytes(StandardCharsets.UTF_8))); + + Assert.assertEquals( + e.expectedEncoding, + BoxQueryEncoding.encodeBox(b) + ); + } + + @Test + public void testEncodeBoxDescriptor() { + BoxDescriptor b = new BoxDescriptor(); + b.name(Encoder.encodeToBase64(e.source.getBytes(StandardCharsets.UTF_8))); + + Assert.assertEquals( + e.expectedEncoding, + BoxQueryEncoding.encodeBoxDescriptor(b) + ); + } + + @Test + public void testEncodeBoxReference() { + Transaction.BoxReference br = new Transaction.BoxReference(0, e.source.getBytes(StandardCharsets.UTF_8)); + + Assert.assertEquals( + e.expectedEncoding, + BoxQueryEncoding.encodeBoxReference(br) + ); + } +} diff --git a/src/test/unit.tags b/src/test/unit.tags index 0c4b9f876..b38b77d38 100644 --- a/src/test/unit.tags +++ b/src/test/unit.tags @@ -3,6 +3,7 @@ @unit.algod @unit.algod.ledger_refactoring @unit.applications +@unit.applications.boxes @unit.atomic_transaction_composer @unit.blocksummary @unit.dryrun