Skip to content

Commit

Permalink
Boxes: Support GetApplicationBoxes (#347)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeldiamant committed Jul 15, 2022
1 parent c478da8 commit 5a9d077
Show file tree
Hide file tree
Showing 10 changed files with 259 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -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).
Expand All @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BoxesResponse> execute() throws Exception {
Response<BoxesResponse> 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<BoxesResponse> execute(String[] headers, String[] values) throws Exception {
Response<BoxesResponse> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ protected Query(Client client, HttpMethod httpMethod) {

protected abstract QueryData getRequestString();

protected <T>Response<T> baseExecute() throws Exception {
protected <T> Response<T> baseExecute() throws Exception {
return baseExecute(null, null);
}

protected <T>Response<T> baseExecute(String[] headers, String[] values) throws Exception {
protected <T> Response<T> baseExecute(String[] headers, String[] values) throws Exception {

QueryData qData = this.getRequestString();
com.squareup.okhttp.Response resp = this.client.executeCall(qData, httpMethod, headers, values);
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<BoxDescriptor> boxes = new ArrayList<BoxDescriptor>();

@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;
}
}
56 changes: 55 additions & 1 deletion src/test/java/com/algorand/algosdk/integration/Applications.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<Box> boxResp = clients.v2Client.GetApplicationBoxByName(this.appId).name(encodedBoxName).execute();

Expand All @@ -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<byte[]> 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<byte[]> expectedNames = Lists.newArrayList();
if (!encodedBoxesRaw.isEmpty()) {
for (String s : Strings.split(encodedBoxesRaw, ',')) {
expectedNames.add(decodeBoxName(s));
}
}

final Response<BoxesResponse> r = clients.v2Client.GetApplicationBoxes(this.appId).execute();
Assert.assertTrue(r.isSuccessful());

final List<byte[]> 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);
}
}
}
16 changes: 13 additions & 3 deletions src/test/java/com/algorand/algosdk/unit/AlgodPaths.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
32 changes: 20 additions & 12 deletions src/test/java/com/algorand/algosdk/util/ConversionUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,8 +31,11 @@ public static List<byte[]> 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;
})
Expand Down Expand Up @@ -75,21 +77,27 @@ public static List<AppBoxReference> convertBoxes(String boxesStr) {
return null;
}

ArrayList<AppBoxReference> boxReferences = new ArrayList<>();
List<AppBoxReference> 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;
Expand Down
Loading

0 comments on commit 5a9d077

Please sign in to comment.