diff --git a/.circleci/config.yml b/.circleci/config.yml
index 511cac25f..7b827b062 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -24,7 +24,7 @@ jobs:
- run: mvn test
integration-test:
machine:
- image: "ubuntu-2204:2022.04.2"
+ image: "ubuntu-2204:2022.10.2"
resource_class: medium
steps:
- checkout
diff --git a/.test-env b/.test-env
index a321a7f1e..049289d7b 100644
--- a/.test-env
+++ b/.test-env
@@ -1,6 +1,6 @@
# Configs for testing repo download:
SDK_TESTING_URL="https://github.com/algorand/algorand-sdk-testing"
-SDK_TESTING_BRANCH="V2"
+SDK_TESTING_BRANCH="master"
SDK_TESTING_HARNESS="test-harness"
INSTALL_ONLY=0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 96d4ff643..c546a88d6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,15 @@
+# 2.6.0
+
+
+
+## What's Changed
+### Enhancements
+* Simulate: Support `fixSigners` option by @github-actions in https://github.com/algorand/java-algorand-sdk/pull/721
+* Incentives: Support heartbeat txn, Java 8 Min Version by @algorandskiy in https://github.com/algorand/java-algorand-sdk/pull/758
+
+
+**Full Changelog**: https://github.com/algorand/java-algorand-sdk/compare/2.5.0...2.6.0
+
# 2.5.0
diff --git a/README.md b/README.md
index 4a70162a7..66cf9fa09 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ Maven:
com.algorand
algosdk
- 2.5.0
+ 2.6.0
```
diff --git a/pom.xml b/pom.xml
index 659d1b7c2..cb0b4469e 100755
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
com.algorand
algosdk
- 2.5.0
+ 2.6.0
jar
${project.groupId}:${project.artifactId}
@@ -42,8 +42,8 @@
3.0.0-M5
- 1.7
- 7
+ 1.8
+ 8
8
diff --git a/src/main/java/com/algorand/algosdk/transaction/HeartbeatProof.java b/src/main/java/com/algorand/algosdk/transaction/HeartbeatProof.java
new file mode 100644
index 000000000..e8563d8e8
--- /dev/null
+++ b/src/main/java/com/algorand/algosdk/transaction/HeartbeatProof.java
@@ -0,0 +1,74 @@
+package com.algorand.algosdk.transaction;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+import com.algorand.algosdk.crypto.*;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+
+@JsonPropertyOrder(alphabetic = true)
+@JsonInclude(JsonInclude.Include.NON_DEFAULT)
+public class HeartbeatProof implements Serializable {
+ @JsonProperty("s")
+ public Signature sig = new Signature();
+
+ @JsonProperty("p")
+ public Ed25519PublicKey pk = new Ed25519PublicKey();
+
+ @JsonProperty("p2")
+ public Ed25519PublicKey pk2 = new Ed25519PublicKey();
+
+ @JsonProperty("p1s")
+ public Signature pk1Sig = new Signature();
+
+ @JsonProperty("p2s")
+ public Signature pk2Sig = new Signature();
+
+ public HeartbeatProof() {}
+
+ public HeartbeatProof(
+ Signature sig,
+ Ed25519PublicKey pk,
+ Ed25519PublicKey pk2,
+ Signature pk1Sig,
+ Signature pk2Sig
+ ) {
+ this.sig = Objects.requireNonNull(sig, "sig must not be null");
+ this.pk = Objects.requireNonNull(pk, "pk must not be null");
+ this.pk2 = Objects.requireNonNull(pk2, "pk2 must not be null");
+ this.pk1Sig = Objects.requireNonNull(pk1Sig, "pk1Sig must not be null");
+ this.pk2Sig = Objects.requireNonNull(pk2Sig, "pk2Sig must not be null");
+ }
+
+ @JsonCreator
+ public HeartbeatProof(
+ @JsonProperty("s") byte[] sig,
+ @JsonProperty("p") byte[] pk,
+ @JsonProperty("p2") byte[] pk2,
+ @JsonProperty("p1s") byte[] pk1Sig,
+ @JsonProperty("p2s") byte[] pk2Sig
+ ) {
+ this.sig = new Signature(sig);
+ this.pk = new Ed25519PublicKey(pk);
+ this.pk2 = new Ed25519PublicKey(pk2);
+ this.pk1Sig = new Signature(pk1Sig);
+ this.pk2Sig = new Signature(pk2Sig);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ HeartbeatProof that = (HeartbeatProof) o;
+ return Objects.equals(sig, that.sig) &&
+ Objects.equals(pk, that.pk) &&
+ Objects.equals(pk2, that.pk2) &&
+ Objects.equals(pk1Sig, that.pk1Sig) &&
+ Objects.equals(pk2Sig, that.pk2Sig);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/algorand/algosdk/transaction/HeartbeatTxnFields.java b/src/main/java/com/algorand/algosdk/transaction/HeartbeatTxnFields.java
new file mode 100644
index 000000000..b4975c9d2
--- /dev/null
+++ b/src/main/java/com/algorand/algosdk/transaction/HeartbeatTxnFields.java
@@ -0,0 +1,87 @@
+package com.algorand.algosdk.transaction;
+
+import java.io.Serializable;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.Objects;
+
+import com.algorand.algosdk.crypto.*;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+@JsonPropertyOrder(alphabetic = true)
+@JsonInclude(JsonInclude.Include.NON_DEFAULT)
+public class HeartbeatTxnFields implements Serializable {
+ @JsonProperty("a")
+ public Address hbAddress = new Address();
+
+ @JsonProperty("prf")
+ public HeartbeatProof hbProof = new HeartbeatProof();
+
+ @JsonProperty("sd")
+ public byte[] hbSeed = new byte[32]; // committee.Seed
+
+ @JsonProperty("vid")
+ public ParticipationPublicKey hbVoteID = new ParticipationPublicKey();
+
+ @JsonProperty("kd")
+ public BigInteger hbKeyDilution = BigInteger.valueOf(0);
+
+ public HeartbeatTxnFields() {}
+
+ public HeartbeatTxnFields(
+ Address hbAddress,
+ HeartbeatProof hbProof,
+ byte[] hbSeed,
+ ParticipationPublicKey hbVoteID,
+ BigInteger hbKeyDilution
+ ) {
+ this.hbAddress = Objects.requireNonNull(hbAddress, "hbAddress must not be null");
+ this.hbProof = Objects.requireNonNull(hbProof, "hbProof must not be null");
+ this.hbVoteID = Objects.requireNonNull(hbVoteID, "hbVoteID must not be null");
+ this.hbKeyDilution = Objects.requireNonNull(hbKeyDilution, "hbKeyDilution must not be null");
+ if (hbSeed == null) {
+ throw new NullPointerException("hbSeed must not be null");
+ }
+ System.arraycopy(hbSeed, 0, this.hbSeed, 0, this.hbSeed.length);
+ }
+
+ @JsonCreator
+ public HeartbeatTxnFields(
+ @JsonProperty("a") byte[] hbAddress,
+ @JsonProperty("prf") HeartbeatProof hbProof,
+ @JsonProperty("sd") byte[] hbSeed,
+ @JsonProperty("vid") byte[] hbVoteID,
+ @JsonProperty("kd") BigInteger hbKeyDilution
+ ) {
+ if (hbAddress != null) {
+ this.hbAddress = new Address(hbAddress);
+ }
+ if (hbProof != null) {
+ this.hbProof = hbProof;
+ }
+ if (hbSeed != null) {
+ System.arraycopy(hbSeed, 0, this.hbSeed, 0, this.hbSeed.length);
+ }
+ if (hbVoteID != null) {
+ this.hbVoteID = new ParticipationPublicKey(hbVoteID);
+ }
+ if (hbKeyDilution != null) {
+ this.hbKeyDilution = hbKeyDilution;
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ HeartbeatTxnFields that = (HeartbeatTxnFields) o;
+ return hbAddress.equals(that.hbAddress) &&
+ hbProof.equals(that.hbProof) &&
+ Arrays.equals(hbSeed, that.hbSeed) &&
+ hbVoteID.equals(that.hbVoteID) &&
+ hbKeyDilution.equals(that.hbKeyDilution);
+ }
+}
diff --git a/src/main/java/com/algorand/algosdk/transaction/Transaction.java b/src/main/java/com/algorand/algosdk/transaction/Transaction.java
index aa00104ed..51f4058f2 100644
--- a/src/main/java/com/algorand/algosdk/transaction/Transaction.java
+++ b/src/main/java/com/algorand/algosdk/transaction/Transaction.java
@@ -175,6 +175,9 @@ public class Transaction implements Serializable {
@JsonProperty("spmsg")
public Map stateProofMessage = null;
+ @JsonProperty("hb")
+ public HeartbeatTxnFields heartbeatFields = new HeartbeatTxnFields();
+
/**
* Helper for Jackson conversion.
*/
@@ -238,7 +241,9 @@ private Transaction(@JsonProperty("type") Type type,
@JsonProperty("apid") Long applicationId,
@JsonProperty("apls") StateSchema localStateSchema,
@JsonProperty("apsu") byte[] clearStateProgram,
- @JsonProperty("apep") Long extraPages
+ @JsonProperty("apep") Long extraPages,
+ // heartbeat fields
+ @JsonProperty("hb") HeartbeatTxnFields heartbeatFields
) throws IOException {
this(
type,
@@ -289,7 +294,8 @@ private Transaction(@JsonProperty("type") Type type,
applicationId,
localStateSchema,
clearStateProgram == null ? null : new TEALProgram(clearStateProgram),
- extraPages
+ extraPages,
+ heartbeatFields
);
}
@@ -348,7 +354,8 @@ private Transaction(
Long applicationId,
StateSchema localStateSchema,
TEALProgram clearStateProgram,
- Long extraPages
+ Long extraPages,
+ HeartbeatTxnFields heartbeatFields
) {
if (type != null) this.type = type;
if (sender != null) this.sender = sender;
@@ -393,6 +400,7 @@ private Transaction(
if (localStateSchema != null) this.localStateSchema = localStateSchema;
if (clearStateProgram != null) this.clearStateProgram = clearStateProgram;
if (extraPages != null) this.extraPages = extraPages;
+ if (heartbeatFields != null) this.heartbeatFields = heartbeatFields;
}
// Used by Jackson to determine "default" values.
@@ -433,7 +441,8 @@ public enum Type {
AssetTransfer("axfer"),
AssetFreeze("afrz"),
ApplicationCall("appl"),
- StateProof("stpf");
+ StateProof("stpf"),
+ Heartbeat("hb");
private static Map namesMap = new HashMap(6);
@@ -604,7 +613,8 @@ public boolean equals(Object o) {
freezeState == that.freezeState &&
rekeyTo.equals(that.rekeyTo) &&
extraPages.equals(that.extraPages) &&
- boxReferences.equals(that.boxReferences);
+ boxReferences.equals(that.boxReferences) &&
+ heartbeatFields.equals(that.heartbeatFields);
}
/**
diff --git a/src/main/java/com/algorand/algosdk/v2/client/algod/AccountInformation.java b/src/main/java/com/algorand/algosdk/v2/client/algod/AccountInformation.java
index efdb465c5..77e8277b8 100644
--- a/src/main/java/com/algorand/algosdk/v2/client/algod/AccountInformation.java
+++ b/src/main/java/com/algorand/algosdk/v2/client/algod/AccountInformation.java
@@ -11,7 +11,7 @@
/**
- * Given a specific account public key, this call returns the accounts status,
+ * Given a specific account public key, this call returns the account's status,
* balance and spendable amounts
* /v2/accounts/{address}
*/
diff --git a/src/main/java/com/algorand/algosdk/v2/client/algod/GetBlockHeader.java b/src/main/java/com/algorand/algosdk/v2/client/algod/GetBlockHeader.java
new file mode 100644
index 000000000..db072ead8
--- /dev/null
+++ b/src/main/java/com/algorand/algosdk/v2/client/algod/GetBlockHeader.java
@@ -0,0 +1,66 @@
+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.BlockHeaderResponse;
+
+
+/**
+ * Get the block header for the block on the given round.
+ * /v2/blocks/{round}/header
+ */
+public class GetBlockHeader extends Query {
+
+ private Long round;
+
+ /**
+ * @param round The round from which to fetch block header information.
+ */
+ public GetBlockHeader(Client client, Long round) {
+ super(client, new HttpMethod("get"));
+ addQuery("format", "msgpack");
+ this.round = round;
+ }
+
+ /**
+ * Execute the query.
+ * @return the query response object.
+ * @throws Exception
+ */
+ @Override
+ public Response execute() throws Exception {
+ Response resp = baseExecute();
+ resp.setValueType(BlockHeaderResponse.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(BlockHeaderResponse.class);
+ return resp;
+ }
+
+ protected QueryData getRequestString() {
+ if (this.round == null) {
+ throw new RuntimeException("round is not set. It is a required parameter.");
+ }
+ addPathSegment(String.valueOf("v2"));
+ addPathSegment(String.valueOf("blocks"));
+ addPathSegment(String.valueOf(round));
+ addPathSegment(String.valueOf("header"));
+
+ 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 813479cf0..37a045629 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
@@ -13,6 +13,7 @@
import com.algorand.algosdk.v2.client.algod.GetBlock;
import com.algorand.algosdk.v2.client.algod.GetBlockTxids;
import com.algorand.algosdk.v2.client.algod.GetBlockHash;
+import com.algorand.algosdk.v2.client.algod.GetBlockHeader;
import com.algorand.algosdk.v2.client.algod.GetTransactionProof;
import com.algorand.algosdk.v2.client.algod.GetBlockLogs;
import com.algorand.algosdk.v2.client.algod.GetSupply;
@@ -115,7 +116,7 @@ public GetVersion GetVersion() {
}
/**
- * Given a specific account public key, this call returns the accounts status,
+ * Given a specific account public key, this call returns the account's status,
* balance and spendable amounts
* /v2/accounts/{address}
*/
@@ -180,6 +181,14 @@ public GetBlockHash GetBlockHash(Long round) {
return new GetBlockHash((Client) this, round);
}
+ /**
+ * Get the block header for the block on the given round.
+ * /v2/blocks/{round}/header
+ */
+ public GetBlockHeader GetBlockHeader(Long round) {
+ return new GetBlockHeader((Client) this, round);
+ }
+
/**
* Get a proof for a transaction in a block.
* /v2/blocks/{round}/transactions/{txid}/proof
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 3ca218bbf..b7392e4b7 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
@@ -17,6 +17,7 @@
import com.algorand.algosdk.v2.client.indexer.LookupAssetByID;
import com.algorand.algosdk.v2.client.indexer.LookupAssetBalances;
import com.algorand.algosdk.v2.client.indexer.LookupAssetTransactions;
+import com.algorand.algosdk.v2.client.indexer.SearchForBlockHeaders;
import com.algorand.algosdk.v2.client.indexer.LookupBlock;
import com.algorand.algosdk.v2.client.indexer.LookupTransaction;
import com.algorand.algosdk.v2.client.indexer.SearchForTransactions;
@@ -196,6 +197,15 @@ public LookupAssetTransactions lookupAssetTransactions(Long assetId) {
return new LookupAssetTransactions((Client) this, assetId);
}
+ /**
+ * Search for block headers. Block headers are returned in ascending round order.
+ * Transactions are not included in the output.
+ * /v2/block-headers
+ */
+ public SearchForBlockHeaders searchForBlockHeaders() {
+ return new SearchForBlockHeaders((Client) this);
+ }
+
/**
* Lookup block.
* /v2/blocks/{round-number}
diff --git a/src/main/java/com/algorand/algosdk/v2/client/indexer/LookupAccountByID.java b/src/main/java/com/algorand/algosdk/v2/client/indexer/LookupAccountByID.java
index fc760b3ce..375b8dd5d 100644
--- a/src/main/java/com/algorand/algosdk/v2/client/indexer/LookupAccountByID.java
+++ b/src/main/java/com/algorand/algosdk/v2/client/indexer/LookupAccountByID.java
@@ -50,7 +50,8 @@ public LookupAccountByID includeAll(Boolean includeAll) {
}
/**
- * Include results for the specified round.
+ * Deprecated and disallowed. This parameter used to include results for a
+ * specified round. Requests with this parameter set are now rejected.
*/
public LookupAccountByID round(Long round) {
addQuery("round", String.valueOf(round));
diff --git a/src/main/java/com/algorand/algosdk/v2/client/indexer/SearchForAccounts.java b/src/main/java/com/algorand/algosdk/v2/client/indexer/SearchForAccounts.java
index d67db48a1..93efd9e3d 100644
--- a/src/main/java/com/algorand/algosdk/v2/client/indexer/SearchForAccounts.java
+++ b/src/main/java/com/algorand/algosdk/v2/client/indexer/SearchForAccounts.java
@@ -104,11 +104,8 @@ public SearchForAccounts next(String next) {
}
/**
- * Include results for the specified round. For performance reasons, this parameter
- * may be disabled on some configurations. Using application-id or asset-id filters
- * will return both creator and opt-in accounts. Filtering by include-all will
- * return creator and opt-in accounts for deleted assets and accounts. Non-opt-in
- * managers are not included in the results when asset-id is used.
+ * Deprecated and disallowed. This parameter used to include results for a
+ * specified round. Requests with this parameter set are now rejected.
*/
public SearchForAccounts round(Long round) {
addQuery("round", String.valueOf(round));
diff --git a/src/main/java/com/algorand/algosdk/v2/client/indexer/SearchForBlockHeaders.java b/src/main/java/com/algorand/algosdk/v2/client/indexer/SearchForBlockHeaders.java
new file mode 100644
index 000000000..5b677c583
--- /dev/null
+++ b/src/main/java/com/algorand/algosdk/v2/client/indexer/SearchForBlockHeaders.java
@@ -0,0 +1,138 @@
+package com.algorand.algosdk.v2.client.indexer;
+
+import java.util.Date;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.algorand.algosdk.crypto.Address;
+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.common.Utils;
+import com.algorand.algosdk.v2.client.model.BlockHeadersResponse;
+
+
+/**
+ * Search for block headers. Block headers are returned in ascending round order.
+ * Transactions are not included in the output.
+ * /v2/block-headers
+ */
+public class SearchForBlockHeaders extends Query {
+
+ public SearchForBlockHeaders(Client client) {
+ super(client, new HttpMethod("get"));
+ }
+
+ /**
+ * Accounts marked as absent in the block header's participation updates. This
+ * parameter accepts a comma separated list of addresses.
+ */
+ public SearchForBlockHeaders absent(List absent) {
+ addQuery("absent", StringUtils.join(absent, ","));
+ return this;
+ }
+
+ /**
+ * Include results after the given time. Must be an RFC 3339 formatted string.
+ */
+ public SearchForBlockHeaders afterTime(Date afterTime) {
+ addQuery("after-time", Utils.getDateString(afterTime));
+ return this;
+ }
+
+ /**
+ * Include results before the given time. Must be an RFC 3339 formatted string.
+ */
+ public SearchForBlockHeaders beforeTime(Date beforeTime) {
+ addQuery("before-time", Utils.getDateString(beforeTime));
+ return this;
+ }
+
+ /**
+ * Accounts marked as expired in the block header's participation updates. This
+ * parameter accepts a comma separated list of addresses.
+ */
+ public SearchForBlockHeaders expired(List expired) {
+ addQuery("expired", StringUtils.join(expired, ","));
+ return this;
+ }
+
+ /**
+ * Maximum number of results to return. There could be additional pages even if the
+ * limit is not reached.
+ */
+ public SearchForBlockHeaders limit(Long limit) {
+ addQuery("limit", String.valueOf(limit));
+ return this;
+ }
+
+ /**
+ * Include results at or before the specified max-round.
+ */
+ public SearchForBlockHeaders maxRound(Long maxRound) {
+ addQuery("max-round", String.valueOf(maxRound));
+ return this;
+ }
+
+ /**
+ * Include results at or after the specified min-round.
+ */
+ public SearchForBlockHeaders minRound(Long minRound) {
+ addQuery("min-round", String.valueOf(minRound));
+ return this;
+ }
+
+ /**
+ * The next page of results. Use the next token provided by the previous results.
+ */
+ public SearchForBlockHeaders next(String next) {
+ addQuery("next", String.valueOf(next));
+ return this;
+ }
+
+ /**
+ * Accounts marked as proposer in the block header's participation updates. This
+ * parameter accepts a comma separated list of addresses.
+ */
+ public SearchForBlockHeaders proposers(List proposers) {
+ addQuery("proposers", StringUtils.join(proposers, ","));
+ return this;
+ }
+
+ /**
+ * Execute the query.
+ * @return the query response object.
+ * @throws Exception
+ */
+ @Override
+ public Response execute() throws Exception {
+ Response resp = baseExecute();
+ resp.setValueType(BlockHeadersResponse.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(BlockHeadersResponse.class);
+ return resp;
+ }
+
+ protected QueryData getRequestString() {
+ addPathSegment(String.valueOf("v2"));
+ addPathSegment(String.valueOf("block-headers"));
+
+ return qd;
+ }
+}
diff --git a/src/main/java/com/algorand/algosdk/v2/client/model/BlockHeaderResponse.java b/src/main/java/com/algorand/algosdk/v2/client/model/BlockHeaderResponse.java
new file mode 100644
index 000000000..3576edece
--- /dev/null
+++ b/src/main/java/com/algorand/algosdk/v2/client/model/BlockHeaderResponse.java
@@ -0,0 +1,31 @@
+package com.algorand.algosdk.v2.client.model;
+
+import java.util.HashMap;
+import java.util.Objects;
+
+import com.algorand.algosdk.v2.client.common.PathResponse;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Block header.
+ */
+public class BlockHeaderResponse extends PathResponse {
+
+ /**
+ * Block header data.
+ */
+ @JsonProperty("blockHeader")
+ public HashMap blockHeader;
+
+ @Override
+ public boolean equals(Object o) {
+
+ if (this == o) return true;
+ if (o == null) return false;
+
+ BlockHeaderResponse other = (BlockHeaderResponse) o;
+ if (!Objects.deepEquals(this.blockHeader, other.blockHeader)) return false;
+
+ return true;
+ }
+}
diff --git a/src/main/java/com/algorand/algosdk/v2/client/model/BlockHeadersResponse.java b/src/main/java/com/algorand/algosdk/v2/client/model/BlockHeadersResponse.java
new file mode 100644
index 000000000..b9ee52530
--- /dev/null
+++ b/src/main/java/com/algorand/algosdk/v2/client/model/BlockHeadersResponse.java
@@ -0,0 +1,41 @@
+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;
+
+public class BlockHeadersResponse extends PathResponse {
+
+ @JsonProperty("blocks")
+ public List blocks = new ArrayList();
+
+ /**
+ * Round at which the results were computed.
+ */
+ @JsonProperty("current-round")
+ public Long currentRound;
+
+ /**
+ * 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;
+
+ BlockHeadersResponse other = (BlockHeadersResponse) o;
+ if (!Objects.deepEquals(this.blocks, other.blocks)) return false;
+ if (!Objects.deepEquals(this.currentRound, other.currentRound)) return false;
+ if (!Objects.deepEquals(this.nextToken, other.nextToken)) return false;
+
+ return true;
+ }
+}
diff --git a/src/main/java/com/algorand/algosdk/v2/client/model/Enums.java b/src/main/java/com/algorand/algosdk/v2/client/model/Enums.java
index eee96c87f..8575fa1e9 100644
--- a/src/main/java/com/algorand/algosdk/v2/client/model/Enums.java
+++ b/src/main/java/com/algorand/algosdk/v2/client/model/Enums.java
@@ -191,6 +191,7 @@ public static SigType forValue(String value) {
* (afrz) asset-freeze-transaction
* (appl) application-transaction
* (stpf) state-proof-transaction
+ * (hb) heartbeat-transaction
*/
public enum TxType {
@JsonProperty("pay") PAY("pay"),
@@ -200,6 +201,7 @@ public enum TxType {
@JsonProperty("afrz") AFRZ("afrz"),
@JsonProperty("appl") APPL("appl"),
@JsonProperty("stpf") STPF("stpf"),
+ @JsonProperty("hb") HB("hb"),
@JsonProperty("") UNKNOWN("");
final String serializedName;
diff --git a/src/main/java/com/algorand/algosdk/v2/client/model/HbProofFields.java b/src/main/java/com/algorand/algosdk/v2/client/model/HbProofFields.java
new file mode 100644
index 000000000..d47675fd6
--- /dev/null
+++ b/src/main/java/com/algorand/algosdk/v2/client/model/HbProofFields.java
@@ -0,0 +1,92 @@
+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;
+
+/**
+ * (hbprf) HbProof is a signature using HeartbeatAddress's partkey, thereby showing
+ * it is online.
+ */
+public class HbProofFields extends PathResponse {
+
+ /**
+ * (p) Public key of the heartbeat message.
+ */
+ @JsonProperty("hb-pk")
+ public void hbPk(String base64Encoded) {
+ this.hbPk = Encoder.decodeFromBase64(base64Encoded);
+ }
+ public String hbPk() {
+ return Encoder.encodeToBase64(this.hbPk);
+ }
+ public byte[] hbPk;
+
+ /**
+ * (p1s) Signature of OneTimeSignatureSubkeyOffsetID(PK, Batch, Offset) under the
+ * key PK2.
+ */
+ @JsonProperty("hb-pk1sig")
+ public void hbPk1sig(String base64Encoded) {
+ this.hbPk1sig = Encoder.decodeFromBase64(base64Encoded);
+ }
+ public String hbPk1sig() {
+ return Encoder.encodeToBase64(this.hbPk1sig);
+ }
+ public byte[] hbPk1sig;
+
+ /**
+ * (p2) Key for new-style two-level ephemeral signature.
+ */
+ @JsonProperty("hb-pk2")
+ public void hbPk2(String base64Encoded) {
+ this.hbPk2 = Encoder.decodeFromBase64(base64Encoded);
+ }
+ public String hbPk2() {
+ return Encoder.encodeToBase64(this.hbPk2);
+ }
+ public byte[] hbPk2;
+
+ /**
+ * (p2s) Signature of OneTimeSignatureSubkeyBatchID(PK2, Batch) under the master
+ * key (OneTimeSignatureVerifier).
+ */
+ @JsonProperty("hb-pk2sig")
+ public void hbPk2sig(String base64Encoded) {
+ this.hbPk2sig = Encoder.decodeFromBase64(base64Encoded);
+ }
+ public String hbPk2sig() {
+ return Encoder.encodeToBase64(this.hbPk2sig);
+ }
+ public byte[] hbPk2sig;
+
+ /**
+ * (s) Signature of the heartbeat message.
+ */
+ @JsonProperty("hb-sig")
+ public void hbSig(String base64Encoded) {
+ this.hbSig = Encoder.decodeFromBase64(base64Encoded);
+ }
+ public String hbSig() {
+ return Encoder.encodeToBase64(this.hbSig);
+ }
+ public byte[] hbSig;
+
+ @Override
+ public boolean equals(Object o) {
+
+ if (this == o) return true;
+ if (o == null) return false;
+
+ HbProofFields other = (HbProofFields) o;
+ if (!Objects.deepEquals(this.hbPk, other.hbPk)) return false;
+ if (!Objects.deepEquals(this.hbPk1sig, other.hbPk1sig)) return false;
+ if (!Objects.deepEquals(this.hbPk2, other.hbPk2)) return false;
+ if (!Objects.deepEquals(this.hbPk2sig, other.hbPk2sig)) return false;
+ if (!Objects.deepEquals(this.hbSig, other.hbSig)) return false;
+
+ return true;
+ }
+}
diff --git a/src/main/java/com/algorand/algosdk/v2/client/model/SimulateRequest.java b/src/main/java/com/algorand/algosdk/v2/client/model/SimulateRequest.java
index 9dd56f67b..f098aebd3 100644
--- a/src/main/java/com/algorand/algosdk/v2/client/model/SimulateRequest.java
+++ b/src/main/java/com/algorand/algosdk/v2/client/model/SimulateRequest.java
@@ -43,6 +43,13 @@ public class SimulateRequest extends PathResponse {
@JsonProperty("extra-opcode-budget")
public Long extraOpcodeBudget;
+ /**
+ * If true, signers for transactions that are missing signatures will be fixed
+ * during evaluation.
+ */
+ @JsonProperty("fix-signers")
+ public Boolean fixSigners;
+
/**
* If provided, specifies the round preceding the simulation. State changes through
* this round will be used to run this simulation. Usually only the 4 most recent
@@ -70,6 +77,7 @@ public boolean equals(Object o) {
if (!Objects.deepEquals(this.allowUnnamedResources, other.allowUnnamedResources)) return false;
if (!Objects.deepEquals(this.execTraceConfig, other.execTraceConfig)) return false;
if (!Objects.deepEquals(this.extraOpcodeBudget, other.extraOpcodeBudget)) return false;
+ if (!Objects.deepEquals(this.fixSigners, other.fixSigners)) return false;
if (!Objects.deepEquals(this.round, other.round)) return false;
if (!Objects.deepEquals(this.txnGroups, other.txnGroups)) return false;
diff --git a/src/main/java/com/algorand/algosdk/v2/client/model/SimulateTransactionResult.java b/src/main/java/com/algorand/algosdk/v2/client/model/SimulateTransactionResult.java
index 921f018b2..ac906ba17 100644
--- a/src/main/java/com/algorand/algosdk/v2/client/model/SimulateTransactionResult.java
+++ b/src/main/java/com/algorand/algosdk/v2/client/model/SimulateTransactionResult.java
@@ -1,7 +1,9 @@
package com.algorand.algosdk.v2.client.model;
+import java.security.NoSuchAlgorithmException;
import java.util.Objects;
+import com.algorand.algosdk.crypto.Address;
import com.algorand.algosdk.v2.client.common.PathResponse;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -24,6 +26,24 @@ public class SimulateTransactionResult extends PathResponse {
@JsonProperty("exec-trace")
public SimulationTransactionExecTrace execTrace;
+ /**
+ * The account that needed to sign this transaction when no signature was provided
+ * and the provided signer was incorrect.
+ */
+ @JsonProperty("fixed-signer")
+ public void fixedSigner(String fixedSigner) throws NoSuchAlgorithmException {
+ this.fixedSigner = new Address(fixedSigner);
+ }
+ @JsonProperty("fixed-signer")
+ public String fixedSigner() throws NoSuchAlgorithmException {
+ if (this.fixedSigner != null) {
+ return this.fixedSigner.encodeAsString();
+ } else {
+ return null;
+ }
+ }
+ public Address fixedSigner;
+
/**
* Budget used during execution of a logic sig transaction.
*/
@@ -60,6 +80,7 @@ public boolean equals(Object o) {
SimulateTransactionResult other = (SimulateTransactionResult) o;
if (!Objects.deepEquals(this.appBudgetConsumed, other.appBudgetConsumed)) return false;
if (!Objects.deepEquals(this.execTrace, other.execTrace)) return false;
+ if (!Objects.deepEquals(this.fixedSigner, other.fixedSigner)) return false;
if (!Objects.deepEquals(this.logicSigBudgetConsumed, other.logicSigBudgetConsumed)) return false;
if (!Objects.deepEquals(this.txnResult, other.txnResult)) return false;
if (!Objects.deepEquals(this.unnamedResourcesAccessed, other.unnamedResourcesAccessed)) return false;
diff --git a/src/main/java/com/algorand/algosdk/v2/client/model/SimulationEvalOverrides.java b/src/main/java/com/algorand/algosdk/v2/client/model/SimulationEvalOverrides.java
index aa5b03251..2ef5fca42 100644
--- a/src/main/java/com/algorand/algosdk/v2/client/model/SimulationEvalOverrides.java
+++ b/src/main/java/com/algorand/algosdk/v2/client/model/SimulationEvalOverrides.java
@@ -31,6 +31,13 @@ public class SimulationEvalOverrides extends PathResponse {
@JsonProperty("extra-opcode-budget")
public Long extraOpcodeBudget;
+ /**
+ * If true, signers for transactions that are missing signatures will be fixed
+ * during evaluation.
+ */
+ @JsonProperty("fix-signers")
+ public Boolean fixSigners;
+
/**
* The maximum log calls one can make during simulation
*/
@@ -53,6 +60,7 @@ public boolean equals(Object o) {
if (!Objects.deepEquals(this.allowEmptySignatures, other.allowEmptySignatures)) return false;
if (!Objects.deepEquals(this.allowUnnamedResources, other.allowUnnamedResources)) return false;
if (!Objects.deepEquals(this.extraOpcodeBudget, other.extraOpcodeBudget)) return false;
+ if (!Objects.deepEquals(this.fixSigners, other.fixSigners)) return false;
if (!Objects.deepEquals(this.maxLogCalls, other.maxLogCalls)) return false;
if (!Objects.deepEquals(this.maxLogSize, other.maxLogSize)) return false;
diff --git a/src/main/java/com/algorand/algosdk/v2/client/model/Transaction.java b/src/main/java/com/algorand/algosdk/v2/client/model/Transaction.java
index 1ac8a4423..c7a89c3a7 100644
--- a/src/main/java/com/algorand/algosdk/v2/client/model/Transaction.java
+++ b/src/main/java/com/algorand/algosdk/v2/client/model/Transaction.java
@@ -156,6 +156,14 @@ public String group() {
}
public byte[] group;
+ /**
+ * Fields for a heartbeat transaction.
+ * Definition:
+ * data/transactions/heartbeat.go : HeartbeatTxnFields
+ */
+ @JsonProperty("heartbeat-transaction")
+ public TransactionHeartbeat heartbeatTransaction;
+
/**
* Transaction ID
*/
@@ -321,6 +329,7 @@ public String rekeyTo() throws NoSuchAlgorithmException {
* (afrz) asset-freeze-transaction
* (appl) application-transaction
* (stpf) state-proof-transaction
+ * (hb) heartbeat-transaction
*/
@JsonProperty("tx-type")
public Enums.TxType txType;
@@ -348,6 +357,7 @@ public boolean equals(Object o) {
if (!Objects.deepEquals(this.genesisId, other.genesisId)) return false;
if (!Objects.deepEquals(this.globalStateDelta, other.globalStateDelta)) return false;
if (!Objects.deepEquals(this.group, other.group)) return false;
+ if (!Objects.deepEquals(this.heartbeatTransaction, other.heartbeatTransaction)) return false;
if (!Objects.deepEquals(this.id, other.id)) return false;
if (!Objects.deepEquals(this.innerTxns, other.innerTxns)) return false;
if (!Objects.deepEquals(this.intraRoundOffset, other.intraRoundOffset)) return false;
diff --git a/src/main/java/com/algorand/algosdk/v2/client/model/TransactionHeartbeat.java b/src/main/java/com/algorand/algosdk/v2/client/model/TransactionHeartbeat.java
new file mode 100644
index 000000000..dff70bad2
--- /dev/null
+++ b/src/main/java/com/algorand/algosdk/v2/client/model/TransactionHeartbeat.java
@@ -0,0 +1,75 @@
+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;
+
+/**
+ * Fields for a heartbeat transaction.
+ * Definition:
+ * data/transactions/heartbeat.go : HeartbeatTxnFields
+ */
+public class TransactionHeartbeat extends PathResponse {
+
+ /**
+ * (hbad) HbAddress is the account this txn is proving onlineness for.
+ */
+ @JsonProperty("hb-address")
+ public String hbAddress;
+
+ /**
+ * (hbkd) HbKeyDilution must match HbAddress account's current KeyDilution.
+ */
+ @JsonProperty("hb-key-dilution")
+ public java.math.BigInteger hbKeyDilution;
+
+ /**
+ * (hbprf) HbProof is a signature using HeartbeatAddress's partkey, thereby showing
+ * it is online.
+ */
+ @JsonProperty("hb-proof")
+ public HbProofFields hbProof;
+
+ /**
+ * (hbsd) HbSeed must be the block seed for the this transaction's firstValid
+ * block.
+ */
+ @JsonProperty("hb-seed")
+ public void hbSeed(String base64Encoded) {
+ this.hbSeed = Encoder.decodeFromBase64(base64Encoded);
+ }
+ public String hbSeed() {
+ return Encoder.encodeToBase64(this.hbSeed);
+ }
+ public byte[] hbSeed;
+
+ /**
+ * (hbvid) HbVoteID must match the HbAddress account's current VoteID.
+ */
+ @JsonProperty("hb-vote-id")
+ public void hbVoteId(String base64Encoded) {
+ this.hbVoteId = Encoder.decodeFromBase64(base64Encoded);
+ }
+ public String hbVoteId() {
+ return Encoder.encodeToBase64(this.hbVoteId);
+ }
+ public byte[] hbVoteId;
+
+ @Override
+ public boolean equals(Object o) {
+
+ if (this == o) return true;
+ if (o == null) return false;
+
+ TransactionHeartbeat other = (TransactionHeartbeat) o;
+ if (!Objects.deepEquals(this.hbAddress, other.hbAddress)) return false;
+ if (!Objects.deepEquals(this.hbKeyDilution, other.hbKeyDilution)) return false;
+ if (!Objects.deepEquals(this.hbProof, other.hbProof)) return false;
+ if (!Objects.deepEquals(this.hbSeed, other.hbSeed)) return false;
+ if (!Objects.deepEquals(this.hbVoteId, other.hbVoteId)) return false;
+
+ return true;
+ }
+}
diff --git a/src/test/java/com/algorand/algosdk/transaction/TestTransaction.java b/src/test/java/com/algorand/algosdk/transaction/TestTransaction.java
index 35701da03..e21f67804 100644
--- a/src/test/java/com/algorand/algosdk/transaction/TestTransaction.java
+++ b/src/test/java/com/algorand/algosdk/transaction/TestTransaction.java
@@ -917,6 +917,20 @@ public void EmptyByteArraysShouldBeRejected() throws Exception {
assertThat(tx.lease).isNull();
}
+ @Test
+ public void testDeserializationHeartbeats() throws Exception {
+
+ Address hbAddress = new Address("NRJ2UKUNLR3FHLTIYG5RP576RXX7MAU25F7DW6LCM5D45WF67H6EFQMWNM");
+ Address snd = new Address("GAU5WA6DT2EPFS6LKOA333BQP67NXIHZ7JPOOHMZWJDPZRL4XMHDDDUCKA");
+ String goldenString = "gqRsc2lngaFsxAYLMSAyAxKjdHhuhqJmdmqiZ2jEIP9SQzAGyec/v8omzEOW3/GIM+a7bvPaU5D/ohX7qjFtomhihaFhxCBsU6oqjVx2U65owbsX9/6N7/YCmul+O3liZ0fO2L75/KJrZGSjcHJmhaFwxCAM1TyIrIbgm+yPLT9so6VDI3rKl33t4c4RSGJv6G12eaNwMXPEQBETln14zJzQ1Mb/SNjmDNl0fyQ4DPBQZML8iTEbhqBj+YDAgpNSEduWj7OuVkCSQMq4N/Er/+2HfKUHu//spgOicDLEIB9c5n7WgG+5aOdjfBmuxH3z4TYiQzDVYKjBLhv4IkNfo3Ayc8RAeKpQ+o/GJyGCH0I4f9luN0i7BPXlMlaJAuXLX5Ng8DTN0vtZtztjqYfkwp1cVOYPu+Fce3aIdJHVoUDaJaMIDqFzxEBQN41y5zAZhYHQWf2wWF6CGboqQk6MxDcQ76zXHvVtzrAPUWXZDt4IB8Ha1z+54Hc6LmEoG090pk0IYs+jLN8HonNkxCCPVPjiD5O7V0c3P/SVsHmED7slwllta7c92WiKwnvgoqN2aWTEIHBy8sOi/V0YKXJw8VtW40MbqhtUyO9HC9m/haf84xiGomx2dKNzbmTEIDAp2wPDnojyy8tTgb3sMH++26D5+l7nHZmyRvzFfLsOpHR5cGWiaGI=";
+
+ SignedTransaction o = Encoder.decodeFromMsgPack(goldenString, SignedTransaction.class);
+ assertThat(o.tx.type).isEqualTo(Transaction.Type.Heartbeat);
+ assertThat(o.tx.heartbeatFields.hbKeyDilution).isEqualTo(100);
+ assertThat(o.tx.heartbeatFields.hbAddress).isEqualTo(hbAddress);
+ TestUtil.serializeDeserializeCheck(o);
+ }
+
@Nested
class TestFees {
@Test
diff --git a/src/test/java/com/algorand/algosdk/unit/AlgodResponses.java b/src/test/java/com/algorand/algosdk/unit/AlgodResponses.java
index 33800fb91..dcf9b0d79 100644
--- a/src/test/java/com/algorand/algosdk/unit/AlgodResponses.java
+++ b/src/test/java/com/algorand/algosdk/unit/AlgodResponses.java
@@ -139,6 +139,11 @@ public void the_parsed_Get_Block_response_should_have_rewards_pool(String string
verifyResponse(blockResponse, shared.bodyFile);
}
+ @Then("the parsed Get Block response should have heartbeat address {string}")
+ public void the_parsed_Get_Block_response_should_have_heartbeat_address(String string) throws IOException {
+ verifyResponse(blockResponse, shared.bodyFile);
+ }
+
@Then("the parsed Suggested Transaction Parameters response should have first round valid of {int}")
public void the_parsed_Suggested_Transaction_Parameters_response_should_have_first_round_valid_of(Integer int1) throws IOException {
verifyResponse(transactionParametersResponse, shared.bodyFile);
diff --git a/src/test/java/com/algorand/algosdk/unit/IndexerResponses.java b/src/test/java/com/algorand/algosdk/unit/IndexerResponses.java
index e50175a50..d860c08cd 100644
--- a/src/test/java/com/algorand/algosdk/unit/IndexerResponses.java
+++ b/src/test/java/com/algorand/algosdk/unit/IndexerResponses.java
@@ -1,6 +1,7 @@
package com.algorand.algosdk.unit;
import static com.algorand.algosdk.unit.utils.TestingUtils.verifyResponse;
+import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
import java.math.BigInteger;
@@ -188,4 +189,13 @@ public void the_parsed_SearchAccounts_response_should_be_valid_on_round_and_the_
public void the_parsed_SearchForTransactions_response_should_be_valid_on_round_and_the_array_should_be_of_len_and_the_element_at_index_should_have_rekey_to(Integer int1, Integer int2, Integer int3, String string) throws IOException {
verifyResponse(transactionsResponse, shared.bodyFile);
}
+
+ @When("the parsed SearchForTransactions response should be valid on round {int} and the array should be of len {int} and the element at index {int} should have hbaddress {string}")
+ public void the_parsed_SearchForTransactions_response_should_be_valid_on_round_and_the_array_should_be_of_len_and_the_element_at_index_should_have_hbaddress(Integer round, Integer length, Integer index, String hbaddress) throws IOException {
+ verifyResponse(transactionsResponse, shared.bodyFile);
+
+ assertThat(transactionsResponse.body().currentRound).isEqualTo(round.longValue());
+ assertThat(transactionsResponse.body().transactions.size()).isEqualTo(length.intValue());
+ assertThat(transactionsResponse.body().transactions.get(index).heartbeatTransaction.hbAddress).isEqualTo(hbaddress);
+ }
}
diff --git a/src/test/unit.tags b/src/test/unit.tags
index 8d8ea38d0..a3688039b 100644
--- a/src/test/unit.tags
+++ b/src/test/unit.tags
@@ -2,6 +2,8 @@
@unit.abijson.byname
@unit.algod
@unit.algod.ledger_refactoring
+@unit.algod.heartbeat
+@unit.algod.heartbeat.msgp
@unit.applications
@unit.applications.boxes
@unit.atomic_transaction_composer
@@ -14,6 +16,7 @@
@unit.indexer.ledger_refactoring
@unit.indexer.logs
@unit.indexer.rekey
+@unit.indexer.heartbeat
@unit.offline
@unit.program_sanity_check
@unit.ready