From 5a9d07766f74f1baa53591e7fd16695b691b1539 Mon Sep 17 00:00:00 2001 From: Michael Diamant Date: Fri, 15 Jul 2022 13:47:07 -0400 Subject: [PATCH] Boxes: Support GetApplicationBoxes (#347) --- .../algosdk/util/BoxQueryEncoding.java | 6 +- .../v2/client/algod/GetApplicationBoxes.java | 75 +++++++++++++++++++ .../algosdk/v2/client/common/AlgodClient.java | 10 +++ .../algosdk/v2/client/common/Query.java | 5 +- .../v2/client/model/BoxDescriptor.java | 37 +++++++++ .../v2/client/model/BoxesResponse.java | 29 +++++++ .../algosdk/integration/Applications.java | 56 +++++++++++++- .../com/algorand/algosdk/unit/AlgodPaths.java | 16 +++- .../algosdk/util/ConversionUtils.java | 32 +++++--- .../algosdk/util/TestBoxQueryEncoding.java | 13 +++- 10 files changed, 259 insertions(+), 20 deletions(-) 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/model/BoxDescriptor.java create mode 100644 src/main/java/com/algorand/algosdk/v2/client/model/BoxesResponse.java diff --git a/src/main/java/com/algorand/algosdk/util/BoxQueryEncoding.java b/src/main/java/com/algorand/algosdk/util/BoxQueryEncoding.java index 0d6a3733a..d1689d9d4 100644 --- a/src/main/java/com/algorand/algosdk/util/BoxQueryEncoding.java +++ b/src/main/java/com/algorand/algosdk/util/BoxQueryEncoding.java @@ -1,8 +1,8 @@ package com.algorand.algosdk.util; -import com.algorand.algosdk.transaction.AppBoxReference; 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). @@ -19,6 +19,10 @@ 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/GetApplicationBoxes.java b/src/main/java/com/algorand/algosdk/v2/client/algod/GetApplicationBoxes.java new file mode 100644 index 000000000..5dd27a9a5 --- /dev/null +++ b/src/main/java/com/algorand/algosdk/v2/client/algod/GetApplicationBoxes.java @@ -0,0 +1,75 @@ +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, it returns the box names of that application. No + * particular ordering is guaranteed. + * /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 453e9617c..825343069 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 @@ -19,6 +19,7 @@ import com.algorand.algosdk.v2.client.algod.GetPendingTransactions; import com.algorand.algosdk.v2.client.algod.PendingTransactionInformation; 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; @@ -225,6 +226,15 @@ public GetApplicationByID GetApplicationByID(Long applicationId) { return new GetApplicationByID((Client) this, applicationId); } + /** + * Given an application ID, it returns the box names of that application. No + * particular ordering is guaranteed. + * /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 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/model/BoxDescriptor.java b/src/main/java/com/algorand/algosdk/v2/client/model/BoxDescriptor.java new file mode 100644 index 000000000..9a4a4335b --- /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 a Box. + */ +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..3b8740b4b --- /dev/null +++ b/src/main/java/com/algorand/algosdk/v2/client/model/BoxesResponse.java @@ -0,0 +1,29 @@ +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 { + + @JsonProperty("boxes") + public List boxes = new ArrayList(); + + @Override + public boolean equals(Object o) { + + if (this == o) return true; + if (o == null) return false; + + BoxesResponse other = (BoxesResponse) o; + if (!Objects.deepEquals(this.boxes, other.boxes)) return false; + + return true; + } +} diff --git a/src/test/java/com/algorand/algosdk/integration/Applications.java b/src/test/java/com/algorand/algosdk/integration/Applications.java index 43159e8e7..9e176c0f1 100644 --- a/src/test/java/com/algorand/algosdk/integration/Applications.java +++ b/src/test/java/com/algorand/algosdk/integration/Applications.java @@ -14,11 +14,16 @@ 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.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -251,7 +256,7 @@ public void checkAccountData( assertThat(found).as("Couldn't find key '%s'", hasKey).isTrue(); } - @Then("the contents of the box with name {string} should be {string}. If there is an error it is {string}.") + @Then("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 encodedBoxName, String boxContents, String errStr) throws Exception { Response boxResp = clients.v2Client.GetApplicationBoxByName(this.appId).name(encodedBoxName).execute(); @@ -264,4 +269,53 @@ public void contentsOfBoxShouldBe(String encodedBoxName, String boxContents, Str assertThat(boxResp.body().value().equals(boxContents)); } + + private static byte[] decodeBoxName(String encodedBoxName) { + String[] split = Strings.split(encodedBoxName, ':'); + if (split.length != 2) + throw new RuntimeException("encodedBoxName (" + encodedBoxName + ") does not match expected format"); + + String encoding = split[0]; + String encoded = split[1]; + switch (encoding) { + case "str": + return encoded.getBytes(StandardCharsets.US_ASCII); + case "b64": + return Encoder.decodeFromBase64(encoded); + default: + throw new RuntimeException("Unsupported encoding = " + encoding); + } + } + + private static boolean contains(byte[] elem, List xs) { + for (byte[] e : xs) { + if (Arrays.equals(e, elem)) + return true; + } + return false; + } + + @Then("the current application should have the following boxes {string}.") + public void checkAppBoxes(String encodedBoxesRaw) throws Exception { + final List expectedNames = Lists.newArrayList(); + if (!encodedBoxesRaw.isEmpty()) { + for (String s : Strings.split(encodedBoxesRaw, ',')) { + expectedNames.add(decodeBoxName(s)); + } + } + + final Response r = clients.v2Client.GetApplicationBoxes(this.appId).execute(); + Assert.assertTrue(r.isSuccessful()); + + final List actualNames = Lists.newArrayList(); + for (BoxDescriptor b : r.body().boxes) { + actualNames.add(b.name); + } + + Assert.assertEquals("expected and actual box names length do not match", expectedNames.size(), actualNames.size()); + for (byte[] e : expectedNames) { + if (!contains(e, actualNames)) + throw new RuntimeException("expected and actual box names do not match: " + expectedNames + " != " + actualNames); + } + } } diff --git a/src/test/java/com/algorand/algosdk/unit/AlgodPaths.java b/src/test/java/com/algorand/algosdk/unit/AlgodPaths.java index ccb14b093..679ec3a33 100644 --- a/src/test/java/com/algorand/algosdk/unit/AlgodPaths.java +++ b/src/test/java/com/algorand/algosdk/unit/AlgodPaths.java @@ -4,6 +4,7 @@ import com.algorand.algosdk.unit.utils.QueryMapper; import com.algorand.algosdk.unit.utils.TestingUtils; 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.common.AlgodClient; @@ -84,8 +85,17 @@ public void accountInformation(String string, String string2) throws NoSuchAlgor ps.q = aiq; } - @When("we make a GetApplicationBoxByName call for applicationID {int} with encoded box name {string}") - public void getBoxByName(Integer appID, String encodedBoxName) { - ps.q = algodClient.GetApplicationBoxByName(Long.valueOf(appID)).name(encodedBoxName); + @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; } } diff --git a/src/test/java/com/algorand/algosdk/util/ConversionUtils.java b/src/test/java/com/algorand/algosdk/util/ConversionUtils.java index 60a217885..858d03bf9 100644 --- a/src/test/java/com/algorand/algosdk/util/ConversionUtils.java +++ b/src/test/java/com/algorand/algosdk/util/ConversionUtils.java @@ -6,9 +6,8 @@ import org.assertj.core.api.Assertions; import org.bouncycastle.util.Strings; -import okio.ByteString; - import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; @@ -32,8 +31,11 @@ public static List convertArgs(String args) { 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; }) @@ -75,21 +77,27 @@ public static List convertBoxes(String boxesStr) { return null; } - ArrayList boxReferences = new ArrayList<>(); + List boxReferences = new ArrayList<>(); String[] boxesArray = Strings.split(boxesStr, ','); for (int i = 0; i < boxesArray.length; i += 2) { - Long appID = Long.parseLong(boxesArray[i]); - byte[] name = null; + long appId = Long.parseLong(boxesArray[i]); + String enc = Strings.split(boxesArray[i + 1], ':')[0]; String strName = Strings.split(boxesArray[i + 1], ':')[1]; - if (enc.equals("str")) { - name = strName.getBytes(); - } else { - // b64 encoding - name = ByteString.decodeBase64(strName).toByteArray(); + + 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)); + 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 index 191cb0105..01caef05f 100644 --- a/src/test/java/com/algorand/algosdk/util/TestBoxQueryEncoding.java +++ b/src/test/java/com/algorand/algosdk/util/TestBoxQueryEncoding.java @@ -1,8 +1,8 @@ package com.algorand.algosdk.util; -import com.algorand.algosdk.transaction.AppBoxReference; 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; @@ -41,6 +41,17 @@ public void testEncodeBox() { ); } + @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));