From 35d8ab453f3354a74a6f2580f645949f76adec73 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:50:03 -0500 Subject: [PATCH] Incentives: Support heartbeat txn, Java 8 Min Version (#758) * Regenerate code from specification file * Support HeartbeatTxnFields * Bump ubuntu image to ubuntu-2204:2022.10.2 Co-authored-by: Algorand Generation Bot --- .circleci/config.yml | 2 +- pom.xml | 4 +- .../algosdk/transaction/HeartbeatProof.java | 74 ++++++++++ .../transaction/HeartbeatTxnFields.java | 87 +++++++++++ .../algosdk/transaction/Transaction.java | 20 ++- .../v2/client/algod/AccountInformation.java | 2 +- .../v2/client/algod/GetBlockHeader.java | 66 +++++++++ .../algosdk/v2/client/common/AlgodClient.java | 11 +- .../v2/client/common/IndexerClient.java | 10 ++ .../v2/client/indexer/LookupAccountByID.java | 3 +- .../v2/client/indexer/SearchForAccounts.java | 7 +- .../client/indexer/SearchForBlockHeaders.java | 138 ++++++++++++++++++ .../v2/client/model/BlockHeaderResponse.java | 31 ++++ .../v2/client/model/BlockHeadersResponse.java | 41 ++++++ .../algosdk/v2/client/model/Enums.java | 2 + .../v2/client/model/HbProofFields.java | 92 ++++++++++++ .../algosdk/v2/client/model/Transaction.java | 10 ++ .../v2/client/model/TransactionHeartbeat.java | 75 ++++++++++ .../algosdk/transaction/TestTransaction.java | 14 ++ .../algorand/algosdk/unit/AlgodResponses.java | 5 + .../algosdk/unit/IndexerResponses.java | 10 ++ src/test/unit.tags | 3 + 22 files changed, 691 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/algorand/algosdk/transaction/HeartbeatProof.java create mode 100644 src/main/java/com/algorand/algosdk/transaction/HeartbeatTxnFields.java create mode 100644 src/main/java/com/algorand/algosdk/v2/client/algod/GetBlockHeader.java create mode 100644 src/main/java/com/algorand/algosdk/v2/client/indexer/SearchForBlockHeaders.java create mode 100644 src/main/java/com/algorand/algosdk/v2/client/model/BlockHeaderResponse.java create mode 100644 src/main/java/com/algorand/algosdk/v2/client/model/BlockHeadersResponse.java create mode 100644 src/main/java/com/algorand/algosdk/v2/client/model/HbProofFields.java create mode 100644 src/main/java/com/algorand/algosdk/v2/client/model/TransactionHeartbeat.java 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/pom.xml b/pom.xml index 659d1b7c2..769b0e301 100755 --- a/pom.xml +++ b/pom.xml @@ -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/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