From 20c837d52b0d347adf5809cdc84c4350ec7d8c55 Mon Sep 17 00:00:00 2001 From: Emil Lai <39825288+EmilLa1@users.noreply.github.com> Date: Wed, 14 Feb 2024 09:01:25 +0100 Subject: [PATCH] Cis2 (#304) --- CHANGELOG.md | 25 +- Makefile | 1 + concordium-android-sdk/pom.xml | 5 + .../com/concordium/sdk/examples/Cis2.java | 68 +++++ concordium-sdk/pom.xml | 5 + .../java/com/concordium/sdk/ClientV2.java | 57 ++-- .../sdk/ClientV2MapperExtensions.java | 2 +- .../com/concordium/sdk/cis2/BalanceQuery.java | 32 ++ .../com/concordium/sdk/cis2/Cis2Client.java | 243 +++++++++++++++ .../com/concordium/sdk/cis2/Cis2Error.java | 46 +++ .../sdk/cis2/Cis2EventIterator.java | 119 ++++++++ .../com/concordium/sdk/cis2/Cis2Transfer.java | 49 +++ .../concordium/sdk/cis2/OperatorQuery.java | 30 ++ .../sdk/cis2/SerializationUtils.java | 272 +++++++++++++++++ .../com/concordium/sdk/cis2/TokenAmount.java | 95 ++++++ .../java/com/concordium/sdk/cis2/TokenId.java | 98 ++++++ .../concordium/sdk/cis2/TokenMetadata.java | 41 +++ .../concordium/sdk/cis2/events/BurnEvent.java | 42 +++ .../concordium/sdk/cis2/events/Cis2Event.java | 38 +++ .../cis2/events/Cis2EventWithMetadata.java | 65 ++++ .../sdk/cis2/events/CustomEvent.java | 27 ++ .../concordium/sdk/cis2/events/MintEvent.java | 44 +++ .../sdk/cis2/events/TokenMetadataEvent.java | 38 +++ .../sdk/cis2/events/TransferEvent.java | 50 ++++ .../sdk/cis2/events/UpdateOperatorEvent.java | 43 +++ .../concordium/sdk/requests/BlockQuery.java | 2 + .../smartcontracts/InvokeInstanceRequest.java | 65 ++-- .../AccountTransactionDetails.java | 282 +++++++++++++----- .../ContractTraceElementType.java | 10 +- .../RejectReasonAlreadyABaker.java | 3 +- ...RejectReasonDelegationTargetNotABaker.java | 3 +- .../RejectReasonDuplicateAggregationKey.java | 3 +- .../RejectReasonInvalidCredentials.java | 2 +- .../RejectReasonInvalidInitMethod.java | 6 +- .../RejectReasonInvalidModuleReference.java | 3 +- .../RejectReasonInvalidReceiveMethod.java | 8 +- .../RejectReasonModuleHashAlreadyExists.java | 3 +- .../RejectReasonRejectedReceive.java | 8 +- .../sdk/transactions/AccountTransaction.java | 5 +- .../sdk/transactions/Parameter.java | 31 +- .../sdk/transactions/ReceiveName.java | 24 ++ .../sdk/transactions/UpdateContract.java | 14 +- .../concordium/sdk/types/AbstractAddress.java | 3 - .../concordium/sdk/types/ContractAddress.java | 6 + .../java/com/concordium/sdk/types/UInt16.java | 14 +- .../sdk/cis2/Cis2SerializationTest.java | 115 +++++++ .../concordium/sdk/cis2/TokenAmountTest.java | 67 +++++ .../sdk/transactions/ReceiveNameTest.java | 17 ++ .../TransactionSerializationTest.java | 53 +++- .../sdk/types/ContractAddressTest.java | 17 ++ 50 files changed, 2119 insertions(+), 180 deletions(-) create mode 100644 concordium-sdk-examples/src/main/java/com/concordium/sdk/examples/Cis2.java create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/cis2/BalanceQuery.java create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2Client.java create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2Error.java create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2EventIterator.java create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2Transfer.java create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/cis2/OperatorQuery.java create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/cis2/SerializationUtils.java create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenAmount.java create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenId.java create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenMetadata.java create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/BurnEvent.java create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/Cis2Event.java create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/Cis2EventWithMetadata.java create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/CustomEvent.java create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/MintEvent.java create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/TokenMetadataEvent.java create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/TransferEvent.java create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/UpdateOperatorEvent.java create mode 100644 concordium-sdk/src/test/java/com/concordium/sdk/cis2/Cis2SerializationTest.java create mode 100644 concordium-sdk/src/test/java/com/concordium/sdk/cis2/TokenAmountTest.java create mode 100644 concordium-sdk/src/test/java/com/concordium/sdk/transactions/ReceiveNameTest.java create mode 100644 concordium-sdk/src/test/java/com/concordium/sdk/types/ContractAddressTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index b25315b32..06e12da95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,21 @@ # Changelog -# Unreleased changes - - Cleanup the API a bit for configure baker transaction by using `PartsPerHundredThousands` for determining the commission rates. - - Fix serialization of `ConfigureDelegation` transaction - - Remove `AccountNonce` in favor of just using the `Nonce` type across the API. - - Fix a bug in the serialization of `AccountIndex` - - Fix a bug that caused `getAccountInfo` to fail for delegator and baker accounts if they had no stake pending changes. - This change is also propagated to the type level such that `Baker` and `AccountDelegation` retains an `Optional` - as opposed to just `PendingChange`. - - Fix .equals() for AccountInfo such that all fields are used to deduce equality.< +## Unreleased changes +- Make the `energy` parameter for invoking an instance `Optional`. +- Parse the underlying reject reasons into `AccountTransactionDetails`. +- Introduced Cis2Client for interfacing with CIS2 compliant smart contracts. +- Support for deserializing contract update transactions. +- Fix a bug where contract invocations used the wrong format for parameters. +- Fix a bug in the serialization of `AccountIndex` +- Fix a bug that caused `getAccountInfo` to fail for delegator and baker accounts if they had no stake pending changes. +- Cleanup the API a bit for configure baker transaction by using `PartsPerHundredThousands` for determining the commission rates. +- Fix serialization of `ConfigureDelegation` transaction +- Remove `AccountNonce` in favor of just using the `Nonce` type across the API. +- Fix a bug in the serialization of `AccountIndex` +- Fix a bug that caused `getAccountInfo` to fail for delegator and baker accounts if they had no stake pending changes. + This change is also propagated to the type level such that `Baker` and `AccountDelegation` retains an `Optional` + as opposed to just `PendingChange`. +- Fix .equals() for AccountInfo such that all fields are used to deduce equality. ## 6.1.0 - Purge remaining usages of V1 GRPC API. diff --git a/Makefile b/Makefile index 87a18f246..1c6af4497 100644 --- a/Makefile +++ b/Makefile @@ -51,5 +51,6 @@ android: clean: + cd $(PATH_CRYPTO) && cargo clean rm -rf $(PATH_JAVA_NATIVE_RESOURCES)* rm -rf $(PATH_ANDROID_NATIVE_RESOURCES)* diff --git a/concordium-android-sdk/pom.xml b/concordium-android-sdk/pom.xml index 4141bd8ef..f893694e8 100644 --- a/concordium-android-sdk/pom.xml +++ b/concordium-android-sdk/pom.xml @@ -83,6 +83,11 @@ + + org.apache.commons + commons-lang3 + 3.14.0 + diff --git a/concordium-sdk-examples/src/main/java/com/concordium/sdk/examples/Cis2.java b/concordium-sdk-examples/src/main/java/com/concordium/sdk/examples/Cis2.java new file mode 100644 index 000000000..ca85c643d --- /dev/null +++ b/concordium-sdk-examples/src/main/java/com/concordium/sdk/examples/Cis2.java @@ -0,0 +1,68 @@ +package com.concordium.sdk.examples; + +import com.concordium.sdk.ClientV2; +import com.concordium.sdk.Connection; +import com.concordium.sdk.cis2.BalanceQuery; +import com.concordium.sdk.cis2.Cis2Client; +import com.concordium.sdk.cis2.TokenAmount; +import com.concordium.sdk.cis2.TokenId; +import com.concordium.sdk.exceptions.ClientInitializationException; +import com.concordium.sdk.requests.BlockQuery; +import com.concordium.sdk.transactions.Hash; +import com.concordium.sdk.types.AbstractAddress; +import com.concordium.sdk.types.AccountAddress; +import com.concordium.sdk.types.ContractAddress; +import lombok.val; +import picocli.CommandLine; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.concurrent.Callable; + +/** + * Example usage of the CIS2 client + */ +@CommandLine.Command(name = "Cis2", mixinStandardHelpOptions = true) +public class Cis2 implements Callable { + + @CommandLine.Option( + names = {"--endpoint"}, + description = "GRPC interface of the node.", + defaultValue = "http://localhost:20000") + private String endpoint; + + @CommandLine.Option( + names = {"--index"}, + description = "Index of the contract.", + defaultValue = "9390") + private long contractIndex; + + @Override + public Integer call() throws ClientInitializationException, MalformedURLException { + URL endpointUrl = new URL(this.endpoint); + val client = Cis2Client.newClient(ClientV2.from(Connection.newBuilder() + .host(endpointUrl.getHost()) + .port(endpointUrl.getPort()) + .build()), ContractAddress.from(contractIndex, 0)); + + val eventsForBlock = client.getEventsFor(BlockQuery.HASH(Hash.from("cfd9a3de1b7de2d2942f80b102135bcc8553f472c53c6c8074110aba38bca43c"))); + while (eventsForBlock.hasNext()) { + System.out.println(eventsForBlock.next()); + } + + val balances = client.balanceOf(new BalanceQuery(TokenId.min(), AccountAddress.from("3rXssmPErqhHvDMByFLCEydYAJwot7ZkL7xcu8y296iMJxwNGC"))); + for (BalanceQuery balanceQuery : balances.keySet()) { + TokenId tokenId = balanceQuery.getTokenId(); + AbstractAddress owner = balanceQuery.getAddress(); + TokenAmount balance = balances.get(balanceQuery); + System.out.println("TokenId: " + tokenId + " Owner: " + owner + " Balance " + balance); + } + + return 0; + } + + public static void main(String[] args) { + int exitCode = new CommandLine(new Cis2()).execute(args); + System.exit(exitCode); + } +} diff --git a/concordium-sdk/pom.xml b/concordium-sdk/pom.xml index bd037b493..a89ff1fa3 100644 --- a/concordium-sdk/pom.xml +++ b/concordium-sdk/pom.xml @@ -107,6 +107,11 @@ 3.3.3 test + + org.apache.commons + commons-lang3 + 3.14.0 + diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/ClientV2.java b/concordium-sdk/src/main/java/com/concordium/sdk/ClientV2.java index 055239a4c..e149a4682 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/ClientV2.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/ClientV2.java @@ -55,6 +55,7 @@ import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.util.Iterator; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -141,7 +142,6 @@ public Iterator getBlocks(int timeoutMillis) { * * @param timeoutMillis Timeout for the request in Milliseconds. * @return {@link Iterator} - * */ public Iterator getFinalizedBlocks(int timeoutMillis) { val grpcOutput = this.server(timeoutMillis) @@ -323,8 +323,8 @@ public Nonce getNextAccountSequenceNumber(AccountAddress address) { * @param transactionHash The transaction {@link Hash} * @return The {@link BlockItemStatus} * @throws io.grpc.StatusRuntimeException with {@link io.grpc.Status.Code}:
    - *
  • {@link io.grpc.Status#NOT_FOUND} if the transaction is not known to the node. - *
+ *
  • {@link io.grpc.Status#NOT_FOUND} if the transaction is not known to the node. + * */ public BlockItemStatus getBlockItemStatus(Hash transactionHash) { val grpcOutput = this.server() @@ -793,9 +793,10 @@ public InvokeInstanceResult invokeInstance(InvokeInstanceRequest request) { grpcRequest.setInstance(to(request.getInstance())) .setAmount(to(request.getAmount())) .setEntrypoint(to(request.getEntrypoint())) - .setParameter(to(request.getParameter())) - .setEnergy(com.concordium.grpc.v2.Energy.newBuilder().setValue(request.getEnergy().getValue().getValue())); - + .setParameter(to(request.getParameter())); + if (request.getEnergy().isPresent()) { + grpcRequest.setEnergy(com.concordium.grpc.v2.Energy.newBuilder().setValue(request.getEnergy().get().getValue().getValue())); + } val grpcResponse = this.server().invokeInstance(grpcRequest.build()); return InvokeInstanceResult.parse(grpcResponse); } @@ -807,7 +808,7 @@ public InvokeInstanceResult invokeInstance(InvokeInstanceRequest request) { * @param input The block to query. * @return {@link ImmutableList} with the {@link BakerRewardPeriodInfo} of all the bakers in the block. * @throws io.grpc.StatusRuntimeException with {@link io.grpc.Status.Code}: - *
    • {@link io.grpc.Status.Code#UNIMPLEMENTED} if the protocol does not support the endpoint.
    + *
    • {@link io.grpc.Status.Code#UNIMPLEMENTED} if the protocol does not support the endpoint.
    */ public ImmutableList getBakersRewardPeriod(BlockQuery input) { val response = this.server().getBakersRewardPeriod(to(input)); @@ -824,9 +825,9 @@ public ImmutableList getBakersRewardPeriod(BlockQuery inp * @param block The block to query * @return {@link BlockCertificates} of the block. * @throws io.grpc.StatusRuntimeException with {@link io.grpc.Status.Code}:
      - *
    • {@link io.grpc.Status.Code#UNIMPLEMENTED} if the endpoint is not enabled by the node. - *
    • {@link io.grpc.Status.Code#INVALID_ARGUMENT} if the block being pointed to is not a product of ConcordiumBFT, i.e. created before protocol version 6. - *
    + *
  • {@link io.grpc.Status.Code#UNIMPLEMENTED} if the endpoint is not enabled by the node. + *
  • {@link io.grpc.Status.Code#INVALID_ARGUMENT} if the block being pointed to is not a product of ConcordiumBFT, i.e. created before protocol version 6. + * */ public BlockCertificates getBlockCertificates(BlockQuery block) { val res = this.server().getBlockCertificates(to(block)); @@ -835,7 +836,7 @@ public BlockCertificates getBlockCertificates(BlockQuery block) { /** * Get the projected earliest time at which a particular baker will be required to bake a block.

    - * + *

    * If the baker is not a baker for the current reward period, this returns a timestamp at the * start of the next reward period.

    * If the baker is a baker for the current reward period, the @@ -848,10 +849,11 @@ public BlockCertificates getBlockCertificates(BlockQuery block) { * epoch. This is because the seed for the leader election is updated at the epoch boundary, and * so the winners cannot be predicted beyond that.

    * Note that in some circumstances the returned timestamp can be in the past, especially at the end of an epoch. + * * @param bakerId id of the baker to query. * @return {@link Timestamp} as described in the method documentation. * @throws io.grpc.StatusRuntimeException with {@link io.grpc.Status.Code}: - *

    • {@link io.grpc.Status.Code#UNIMPLEMENTED} if the current consensus version is 0, as the endpoint is only supported by consensus version 1.
    + *
    • {@link io.grpc.Status.Code#UNIMPLEMENTED} if the current consensus version is 0, as the endpoint is only supported by consensus version 1.
    */ public Timestamp getBakerEarliestWinTime(BakerId bakerId) { val res = this.server().getBakerEarliestWinTime(to(bakerId)); @@ -864,12 +866,12 @@ public Timestamp getBakerEarliestWinTime(BakerId bakerId) { * @param epochQuery {@link EpochQuery} representing the specific epoch to query. * @return {@link Hash} of the first finalized block in the epoch. * @throws io.grpc.StatusRuntimeException with {@link io.grpc.Status.Code}:
      - *
    • {@link io.grpc.Status#NOT_FOUND} if the query specifies an unknown block. - *
    • {@link io.grpc.Status#UNAVAILABLE} if the query is for an epoch that is not finalized in the current genesis index, or is for a future genesis index. - *
    • {@link io.grpc.Status#INVALID_ARGUMENT} if the query is for an epoch with no finalized blocks for a past genesis index. - *
    • {@link io.grpc.Status#INVALID_ARGUMENT} if the input {@link EpochQuery} is malformed. - *
    • {@link io.grpc.Status#UNIMPLEMENTED} if the endpoint is disabled on the node. - *
    + *
  • {@link io.grpc.Status#NOT_FOUND} if the query specifies an unknown block. + *
  • {@link io.grpc.Status#UNAVAILABLE} if the query is for an epoch that is not finalized in the current genesis index, or is for a future genesis index. + *
  • {@link io.grpc.Status#INVALID_ARGUMENT} if the query is for an epoch with no finalized blocks for a past genesis index. + *
  • {@link io.grpc.Status#INVALID_ARGUMENT} if the input {@link EpochQuery} is malformed. + *
  • {@link io.grpc.Status#UNIMPLEMENTED} if the endpoint is disabled on the node. + * */ public Hash getFirstBlockEpoch(EpochQuery epochQuery) { val res = this.server().getFirstBlockEpoch(to(epochQuery)); @@ -880,16 +882,17 @@ public Hash getFirstBlockEpoch(EpochQuery epochQuery) { * Get the list of bakers that won the lottery in a particular historical epoch (i.e. the last finalized block is in a later epoch).

    * This lists the winners for each round in the epoch, starting from the round after the last block in the previous epoch, running to the round before the first block in the next epoch.

    * It also indicates if a block in each round was included in the finalized chain. + * * @param epochQuery {@link EpochQuery} representing the specific epoch to query. * @return {@link ImmutableList} of bakers that won the lottery in the specified epoch. * @throws io.grpc.StatusRuntimeException with {@link io.grpc.Status.Code}:

      - *
    • {@link io.grpc.Status#NOT_FOUND} if the query specifies an unknown block. - *
    • {@link io.grpc.Status#UNAVAILABLE} if the query is for an epoch that is not finalized in the current genesis index, or is for a future genesis index. - *
    • {@link io.grpc.Status#INVALID_ARGUMENT} if the query is for an epoch that is not finalized for a past genesis index. - *
    • {@link io.grpc.Status#INVALID_ARGUMENT} if the query is for a genesis index at consensus version 0. - *
    • {@link io.grpc.Status#INVALID_ARGUMENT} if the input {@link EpochQuery} is malformed. - *
    • {@link io.grpc.Status#UNIMPLEMENTED} if the endpoint is disabled on the node. - *
    + *
  • {@link io.grpc.Status#NOT_FOUND} if the query specifies an unknown block. + *
  • {@link io.grpc.Status#UNAVAILABLE} if the query is for an epoch that is not finalized in the current genesis index, or is for a future genesis index. + *
  • {@link io.grpc.Status#INVALID_ARGUMENT} if the query is for an epoch that is not finalized for a past genesis index. + *
  • {@link io.grpc.Status#INVALID_ARGUMENT} if the query is for a genesis index at consensus version 0. + *
  • {@link io.grpc.Status#INVALID_ARGUMENT} if the input {@link EpochQuery} is malformed. + *
  • {@link io.grpc.Status#UNIMPLEMENTED} if the endpoint is disabled on the node. + * */ public ImmutableList getWinningBakersEpoch(EpochQuery epochQuery) { val res = this.server().getWinningBakersEpoch(to(epochQuery)); @@ -904,8 +907,9 @@ public ImmutableList getWinningBakersEpoch(EpochQuery epochQuery) * Waits until a given transaction is finalized and returns the corresponding {@link Optional}. * If the transaction is unknown to the node or not finalized, the client starts listening for newly finalized blocks, * and returns the corresponding {@link Optional} once the transaction is finalized. + * * @param transactionHash the {@link Hash} of the transaction to wait for. - * @param timeoutMillis the number of milliseconds to listen for newly finalized blocks. + * @param timeoutMillis the number of milliseconds to listen for newly finalized blocks. * @return {@link Optional} of the transaction if it was finalized before exceeding the timeout, Empty otherwise. */ public Optional waitUntilFinalized(Hash transactionHash, int timeoutMillis) { @@ -939,6 +943,7 @@ public Optional waitUntilFinalized(Hash transactionHash, int /** * Helper function for {@link ClientV2#waitUntilFinalized(Hash, int)}. Retrieves the {@link Optional} of the transaction if it is finalized. + * * @param transactionHash the {@link Hash} of the transaction to wait for. * @return {@link Optional} of the transaction if it is finalized, Empty otherwise. */ diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/ClientV2MapperExtensions.java b/concordium-sdk/src/main/java/com/concordium/sdk/ClientV2MapperExtensions.java index 95990019c..2c6c3c9af 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/ClientV2MapperExtensions.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/ClientV2MapperExtensions.java @@ -1649,7 +1649,7 @@ static com.concordium.grpc.v2.ReceiveName to(ReceiveName receiveName) { static com.concordium.grpc.v2.Parameter to(Parameter parameter) { return com.concordium.grpc.v2.Parameter.newBuilder() - .setValue(ByteString.copyFrom(parameter.getBytes())) + .setValue(ByteString.copyFrom(parameter.getBytesForContractInvocation())) .build(); } diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/BalanceQuery.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/BalanceQuery.java new file mode 100644 index 000000000..5d68d2466 --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/BalanceQuery.java @@ -0,0 +1,32 @@ +package com.concordium.sdk.cis2; + +import com.concordium.sdk.types.AbstractAddress; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * An object used for querying token balances. + * See here for the specification. + */ +@Getter +@ToString +@EqualsAndHashCode +public class BalanceQuery { + + /** + * The token id to query + */ + private final TokenId tokenId; + + /** + * The address to query the balance of + */ + private final AbstractAddress address; + + public BalanceQuery(TokenId tokenId, AbstractAddress address) { + this.tokenId = tokenId; + this.address = address; + } + +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2Client.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2Client.java new file mode 100644 index 000000000..32c1889ab --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2Client.java @@ -0,0 +1,243 @@ +package com.concordium.sdk.cis2; + +import com.concordium.sdk.ClientV2; +import com.concordium.sdk.cis2.events.Cis2Event; +import com.concordium.sdk.cis2.events.Cis2EventWithMetadata; +import com.concordium.sdk.requests.AccountQuery; +import com.concordium.sdk.requests.BlockQuery; +import com.concordium.sdk.requests.smartcontracts.Energy; +import com.concordium.sdk.requests.smartcontracts.InvokeInstanceRequest; +import com.concordium.sdk.responses.blockitemstatus.FinalizedBlockItem; +import com.concordium.sdk.responses.blockitemsummary.Summary; +import com.concordium.sdk.responses.blockitemsummary.Type; +import com.concordium.sdk.responses.blocksatheight.BlocksAtHeightRequest; +import com.concordium.sdk.responses.smartcontracts.ContractTraceElement; +import com.concordium.sdk.responses.smartcontracts.ContractTraceElementType; +import com.concordium.sdk.responses.transactionstatus.ContractUpdated; +import com.concordium.sdk.responses.transactionstatus.Outcome; +import com.concordium.sdk.responses.transactionstatus.TransactionResultEventType; +import com.concordium.sdk.transactions.*; +import com.concordium.sdk.types.*; +import com.google.common.collect.Lists; +import lombok.Getter; +import lombok.val; +import lombok.var; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * A client dedicated to the CIS2 specification. + */ +@Getter +public class Cis2Client { + + final ClientV2 client; + private final ContractAddress contractAddress; + private final InitName contractName; + + private Cis2Client(ClientV2 client, ContractAddress contractAddress, InitName contractName) { + this.client = client; + this.contractAddress = contractAddress; + this.contractName = contractName; + } + + /** + * Construct a new {@link Cis2Client} with the provided {@link ClientV2} for the provided {@link ContractAddress} + * + * @param client client to use + * @param address the address of the cis 2 contract + * @return a cis2 client for interfacing with the provided contract + */ + public static Cis2Client newClient(ClientV2 client, ContractAddress address) { + val instanceInfo = client.getInstanceInfo(BlockQuery.LAST_FINAL, address); + return new Cis2Client(client, address, InitName.from(instanceInfo.getName())); + } + + /** + * Perform a CIS2 transfer on the contract. + * + * @param sender address of the sender of the transaction. + * @param signer signer of the transaction. + * @param transfers the CIS2 transfers. + * @return the transaction hash + */ + public Hash transfer(AccountAddress sender, TransactionSigner signer, Energy maxEnergyCost, Cis2Transfer... transfers) { + val listOfTransfers = Arrays.asList(transfers); + val nextNonce = this.client.getAccountInfo(BlockQuery.LAST_FINAL, AccountQuery.from(sender)).getNonce(); + val endpoint = ReceiveName.from(contractName, "transfer"); + val parameters = SerializationUtils.serializeTransfers(listOfTransfers); + return this.client.sendTransaction( + TransactionFactory.newUpdateContract() + .maxEnergyCost(maxEnergyCost.getValue()) + .payload(UpdateContract.from(CCDAmount.from(0), this.contractAddress, endpoint, parameters)) + .expiry(Expiry.createNew().addMinutes(5)) + .nonce(nextNonce) + .sender(sender) + .signer(signer) + .build()); + } + + /** + * Update the addresses of which the owner (sender of this transaction) operates. + * + * @param operatorUpdates the updates to carry out. The keys of the map correspond to the + * addresses which the sender (owner) should or should not operate given + * by the provided boolean. + * @return the transaction hash + */ + public Hash updateOperator(AccountAddress sender, TransactionSigner signer, Energy maxEnergyCost, Map operatorUpdates) { + val nextNonce = this.client.getAccountInfo(BlockQuery.LAST_FINAL, AccountQuery.from(sender)).getNonce(); + val endpoint = ReceiveName.from(contractName, "updateOperator"); + val parameters = SerializationUtils.serializeUpdateOperators(operatorUpdates); + return this.client.sendTransaction( + TransactionFactory.newUpdateContract() + .maxEnergyCost(maxEnergyCost.getValue()) + .payload(UpdateContract.from(CCDAmount.from(0), this.contractAddress, endpoint, parameters)) + .expiry(Expiry.createNew().addMinutes(5)) + .nonce(nextNonce) + .sender(sender) + .signer(signer) + .build()); + + } + + /** + * Query the balance of token ids and associated {@link com.concordium.sdk.types.AbstractAddress} + * + * @param queries the token ids and addresses to query + * @return the balances together with the queries used + */ + public Map balanceOf(BalanceQuery... queries) { + val listOfQueries = Arrays.asList(queries); + val parameter = SerializationUtils.serializeBalanceOfParameter(listOfQueries); + val endpoint = ReceiveName.from(contractName, "balanceOf"); + val result = this.client.invokeInstance(InvokeInstanceRequest.from(BlockQuery.LAST_FINAL, this.contractAddress, CCDAmount.from(0), endpoint, parameter, Optional.empty())); + if (result.getOutcome() == Outcome.REJECT) { + throw new RuntimeException("balanceOf failed: " + result.getRejectReason().toString()); + } + val balances = SerializationUtils.deserializeTokenAmounts(result.getReturnValue()); + val responses = new HashMap(); + for (int i = 0; i < balances.length; i++) { + responses.put(listOfQueries.get(i), balances[i]); + } + return responses; + } + + /** + * Query whether one or more owners are operators for one or more addresses. + * + * @param queries the addresses to query. + * @return A map where the values indicate whether the specified owner was indeed operator of the supplied address. + */ + public Map operatorOf(OperatorQuery... queries) { + val listOfQueries = Arrays.asList(queries); + val parameter = SerializationUtils.serializeOperatorOfParameter(listOfQueries); + val endpoint = ReceiveName.from(contractName, "operatorOf"); + val result = this.client.invokeInstance(InvokeInstanceRequest.from(BlockQuery.LAST_FINAL, this.contractAddress, CCDAmount.from(0), endpoint, parameter, Optional.empty())); + if (result.getOutcome() == Outcome.REJECT) { + throw new RuntimeException("operatorOf failed: " + result.getRejectReason().toString()); + } + val isOperatorOf = SerializationUtils.deserializeOperatorOfResponse(result.getReturnValue()); + val responses = new HashMap(); + for (int i = 0; i < isOperatorOf.length; i++) { + responses.put(listOfQueries.get(i), isOperatorOf[i]); + } + return responses; + } + + /** + * Query the token metadata for each provided token id + * + * @param tokenIds the token ids to query + * @return A map where the values indicate the token metadata responses for each {@link TokenId} + */ + public Map tokenMetadata(TokenId... tokenIds) { + val listOfQueries = Arrays.asList(tokenIds); + val parameter = SerializationUtils.serializeTokenIds(listOfQueries); + val endpoint = ReceiveName.from(contractName, "tokenMetadata"); + val result = this.client.invokeInstance(InvokeInstanceRequest.from(BlockQuery.LAST_FINAL, this.contractAddress, CCDAmount.from(0), endpoint, parameter, Optional.empty())); + if (result.getOutcome() == Outcome.REJECT) { + throw new RuntimeException("tokenMetadata failed: " + result.getRejectReason().toString()); + } + val tokenMetadatas = SerializationUtils.deserializeTokenMetadatas(result.getReturnValue()); + val responses = new HashMap(); + for (int i = 0; i < tokenMetadatas.length; i++) { + responses.put(listOfQueries.get(i), tokenMetadatas[i]); + } + return responses; + } + + /** + * Retrieve all events emitted from the CIS2 contract. + * + * @param from block to start from + * @param to block to end from + * @return the list of events. + */ + public Iterator getEvents(BlockQuery from, BlockQuery to) { + final long[] current = {this.client.getBlockInfo(from).getBlockHeight().getValue()}; + val end = this.client.getBlockInfo(to).getBlockHeight().getValue(); + if (current[0] >= end) { + throw new IllegalArgumentException("Starting block must be before the end block"); + } + return new Cis2EventIterator(this, new Iterator() { + @Override + public boolean hasNext() { + return end > current[0]; + } + + @Override + public BlockQuery next() { + BlockQuery query = BlockQuery.HEIGHT(BlocksAtHeightRequest.newAbsolute(current[0])); + current[0] = current[0] + 1; + return query; + } + }); + } + + + /** + * Get any events associated emitted from the specified CIS2 contract. + * + * @param queries blocks to query + * @return the list of events. + */ + public Iterator getEventsFor(BlockQuery... queries) { + return new Cis2EventIterator(this, Lists.newArrayList(queries).iterator()); + } + + /** + * Get any events associated emitted from the specified CIS2 contract by the + * supplied transaction hash. + * + * @param transactionHash the hash of the transaction to query outcome for. + * @return the list of events which originated from the specified transaction hash. + * @throws IllegalArgumentException if the transaction was not finalized. + */ + public Iterator getEventsForFinalizedTransaction(Hash transactionHash) { + val status = this.client.getBlockItemStatus(transactionHash); + if (!status.getFinalizedBlockItem().isPresent()) { + if (status.getCommittedBlockItem().isPresent()) { + throw new IllegalArgumentException("Transaction was not finalized. But it was committed in block(s) " + status.getCommittedBlockItem().get().getSummaries().keySet()); + } + throw new IllegalArgumentException("Transaction was not finalized, but was " + status.getStatus().toString()); + } + val accumulator = new ArrayList(); + val finalizedTransaction = status.getFinalizedBlockItem().get(); + return new Cis2EventIterator(this, Lists.newArrayList(BlockQuery.HASH(finalizedTransaction.getBlockHash())).iterator()); + } + + + /** + * Get any events associated with the CIS2 contract that this client is instantiated with. + * + * @param blockQuery the block to query events for. + * @return The list of events if there are any. + */ + private Iterator getEventsFor(BlockQuery blockQuery) { + return new Cis2EventIterator(this, Lists.newArrayList(blockQuery).iterator()); + } + + +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2Error.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2Error.java new file mode 100644 index 000000000..48e92de02 --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2Error.java @@ -0,0 +1,46 @@ +package com.concordium.sdk.cis2; + +import com.concordium.sdk.responses.transactionstatus.RejectReason; +import com.concordium.sdk.responses.transactionstatus.RejectReasonRejectedInit; +import com.concordium.sdk.responses.transactionstatus.RejectReasonRejectedReceive; +import com.concordium.sdk.responses.transactionstatus.RejectReasonType; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + + +/** + * Errors defined by the CIS2 standard https://proposals.concordium.software/CIS/cis-2.html#rejection-errors + */ +@EqualsAndHashCode +@ToString +@Getter +public class Cis2Error { + + /** + * Type of the error. + * See {@link Cis2Error.Type} for standardized variants. + */ + private final Type type; + + private final int rawErrorCode; + + Cis2Error(Type type, int rawErrorCode) { + this.type = type; + this.rawErrorCode = rawErrorCode; + } + + public static Cis2Error from(int errorCode) { + if (errorCode == -42000001) return new Cis2Error(Type.INVALID_TOKEN_ID, errorCode); + if (errorCode == -42000002) return new Cis2Error(Type.INSUFFICIENT_FUNDS, errorCode); + if (errorCode == -42000003) return new Cis2Error(Type.UNAUTHORIZED, errorCode); + return new Cis2Error(Type.CUSTOM, errorCode); + } + + public enum Type { + INVALID_TOKEN_ID, + INSUFFICIENT_FUNDS, + UNAUTHORIZED, + CUSTOM + } +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2EventIterator.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2EventIterator.java new file mode 100644 index 000000000..924815365 --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2EventIterator.java @@ -0,0 +1,119 @@ +package com.concordium.sdk.cis2; + +import com.concordium.sdk.cis2.events.Cis2EventWithMetadata; +import com.concordium.sdk.requests.BlockQuery; +import com.concordium.sdk.responses.blockitemsummary.AccountTransactionDetails; +import com.concordium.sdk.responses.blockitemsummary.Summary; +import com.concordium.sdk.responses.blockitemsummary.Type; +import com.concordium.sdk.responses.smartcontracts.ContractTraceElementType; +import com.concordium.sdk.responses.transactionstatus.*; +import com.concordium.sdk.transactions.Hash; +import lombok.val; + +import java.util.*; + +class Cis2EventIterator implements Iterator { + + private final Cis2Client client; + + private final Iterator queries; + + private final Queue buffer = new LinkedList<>(); + + Cis2EventIterator(Cis2Client client, Iterator queries) { + this.client = client; + this.queries = queries; + } + + @Override + public boolean hasNext() { + if (!queries.hasNext() && buffer.isEmpty()) return false; + val query = queries.next(); + tryAddEvents(query); + if (!buffer.isEmpty()) { + return true; + } else { + // deplete the queries buffer until we fill up the events buffer or there are no + // more queries left. + while (queries.hasNext()) { + tryAddEvents(queries.next()); + if (!buffer.isEmpty()) return true; + } + return false; + } + } + + @Override + public Cis2EventWithMetadata next() { + return buffer.remove(); + } + + private void tryAddEvents(BlockQuery query) { + val blockEvents = client.getClient().getBlockTransactionEvents(query); + while (blockEvents.hasNext()) { + buffer.addAll(extractCis2Events(query, blockEvents.next())); + } + } + + /** + * Extract any events from the specified contract. + * The events are added to the supplied accumulator. + * + * @param blockQuery a block identifier. + * @param summary the transaction summary to extract from. + * @return list of cis2 events emitted from the contract in the block specified. + */ + private List extractCis2Events(BlockQuery blockQuery, Summary summary) { + val accumulator = new ArrayList(); + if (summary.getDetails().getType() == Type.ACCOUNT_TRANSACTION) { + val details = summary.getDetails().getAccountTransactionDetails(); + if (details.isSuccessful()) { + accumulator.addAll(getSuccessEvents(details, blockQuery, summary.getTransactionHash())); + } else { + val rejectReason = details.getRejectReason(); + if (rejectReason.getType() == RejectReasonType.REJECTED_RECEIVE) { + val rejectReceive = (RejectReasonRejectedReceive) rejectReason; + if (this.client.getContractAddress().equals(rejectReceive.getContractAddress())) { + accumulator.add(Cis2EventWithMetadata.err(Cis2Error.from(rejectReceive.getRejectReason()), blockQuery, summary.getTransactionHash())); + } + } + } + } + return accumulator; + } + + /** + * Parse events from the contract specified that originated from a successfully executed transaction. + * The events can either origin from a contract update or a contract initialization. + * + * @param details the account transaction details + * @param blockQuery the block identifier + * @param transactionHash the origin transaction + * @return list of parsed events. + */ + private List getSuccessEvents(AccountTransactionDetails details, BlockQuery blockQuery, Hash transactionHash) { + val accumulator = new ArrayList(); + val eventType = details.getType(); + if (eventType == TransactionResultEventType.CONTRACT_UPDATED) { + val contractUpdatedEvents = details.getContractUpdated(); + for (val e : contractUpdatedEvents) { + if (e.getTraceType() == ContractTraceElementType.INSTANCE_UPDATED) { + val updatedEvent = (ContractUpdated) e; + if (this.client.getContractAddress().equals(updatedEvent.getAddress())) { + for (val rawUpdateEvent : updatedEvent.getEvents()) { + accumulator.add(Cis2EventWithMetadata.ok(SerializationUtils.deserializeCis2Event(rawUpdateEvent), blockQuery, transactionHash)); + } + } + } + } + } else if (eventType == TransactionResultEventType.CONTRACT_INITIALIZED) { + val contractInitialized = details.getContractInitialized(); + if (this.client.getContractAddress().equals(contractInitialized.getAddress())) { + for (val rawInitializeEvent : contractInitialized.getEvents()) { + accumulator.add(Cis2EventWithMetadata.ok(SerializationUtils.deserializeCis2Event(rawInitializeEvent), blockQuery, transactionHash)); + } + } + } + return accumulator; + } +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2Transfer.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2Transfer.java new file mode 100644 index 000000000..db18d7f88 --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2Transfer.java @@ -0,0 +1,49 @@ +package com.concordium.sdk.cis2; + +import com.concordium.sdk.types.AbstractAddress; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * Object representing a CIS2 transfer https://proposals.concordium.software/CIS/cis-2.html#parameter + * + * Note that the maximum allowed size of the serialized parameter is 64kb. + */ +@ToString +@Getter +@EqualsAndHashCode +public class Cis2Transfer { + /** + * The token id. + */ + private final TokenId tokenId; + + /** + * The token amount. + */ + private final TokenAmount tokenAmount; + + /** + * Sender of the token. + */ + private final AbstractAddress sender; + + /** + * Receiver of the token. + */ + private final AbstractAddress receiver; + + /** + * Additional data. + */ + private final byte[] additionalData; + + public Cis2Transfer(TokenId tokenId, TokenAmount tokenAmount, AbstractAddress sender, AbstractAddress receiver, byte[] additionalData) { + this.tokenId = tokenId; + this.tokenAmount = tokenAmount; + this.sender = sender; + this.receiver = receiver; + this.additionalData = additionalData; + } +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/OperatorQuery.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/OperatorQuery.java new file mode 100644 index 000000000..2a4f4cad8 --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/OperatorQuery.java @@ -0,0 +1,30 @@ +package com.concordium.sdk.cis2; + +import com.concordium.sdk.types.AbstractAddress; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * An object used for querying the operator of a specific {@link AbstractAddress} + * See here for the specification. + */ +@Getter +@ToString +@EqualsAndHashCode +public class OperatorQuery { + /** + * Potential operator + */ + private final AbstractAddress owner; + + /** + * The address which operates {@link OperatorQuery#owner}. + */ + private final AbstractAddress address; + + public OperatorQuery(AbstractAddress owner, AbstractAddress address) { + this.owner = owner; + this.address = address; + } +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/SerializationUtils.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/SerializationUtils.java new file mode 100644 index 000000000..c9f90838b --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/SerializationUtils.java @@ -0,0 +1,272 @@ +package com.concordium.sdk.cis2; + +import com.concordium.sdk.cis2.events.*; +import com.concordium.sdk.transactions.Parameter; +import com.concordium.sdk.types.AbstractAddress; +import com.concordium.sdk.types.AccountAddress; +import com.concordium.sdk.types.ContractAddress; +import com.concordium.sdk.types.UInt16; +import lombok.SneakyThrows; +import lombok.val; + +import java.io.ByteArrayOutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class SerializationUtils { + + /** + * Size of a serialized {@link AccountAddress} + * Tag + the bytes of the account address. + */ + private static final int ACCOUNT_ADDRESS_SIZE = 1 + AccountAddress.BYTES; + + /** + * Size of a serialized {@link ContractAddress} + * Tag + 8 bytes for the {@link ContractAddress#getIndex()} + 8 bytes for the {@link ContractAddress#getSubIndex()}. + */ + private static final int CONTRACT_ADDRESS_SIZE = 1 + (2 * 8); + + @SneakyThrows + public static Parameter serializeTransfers(List listOfTransfers) { + val bos = new ByteArrayOutputStream(); + bos.write(UInt16.from(listOfTransfers.size()).getBytesLittleEndian()); + for (Cis2Transfer transfer : listOfTransfers) { + bos.write(SerializationUtils.serializeTokenId(transfer.getTokenId())); + bos.write(transfer.getTokenAmount().encode()); + bos.write(SerializationUtils.serializeAddress(transfer.getSender())); + bos.write(SerializationUtils.serializeAddress(transfer.getReceiver())); + if (Objects.isNull(transfer.getAdditionalData()) || transfer.getAdditionalData().length == 0) { + bos.write(UInt16.from(0).getBytesLittleEndian()); + } else { + bos.write(UInt16.from(transfer.getAdditionalData().length).getBytesLittleEndian()); + bos.write(transfer.getAdditionalData()); + } + } + return Parameter.from(bos.toByteArray()); + } + + @SneakyThrows + static Parameter serializeBalanceOfParameter(Collection queries) { + val bos = new ByteArrayOutputStream(); + // lengths are stored as little endian. + bos.write(UInt16.from(queries.size()).getBytesLittleEndian()); + for (BalanceQuery balanceQuery : queries) { + bos.write(SerializationUtils.serializeTokenId(balanceQuery.getTokenId())); + bos.write(SerializationUtils.serializeAddress(balanceQuery.getAddress())); + } + return Parameter.from(bos.toByteArray()); + } + + public static TokenAmount[] deserializeTokenAmounts(byte[] returnValue) { + val resultBuffer = ByteBuffer.wrap(returnValue); + // lengths are stored as little endian. + resultBuffer.order(ByteOrder.LITTLE_ENDIAN); + val noOfOutputs = UInt16.from(resultBuffer.getShort()).getValue(); + resultBuffer.order(ByteOrder.BIG_ENDIAN); + val outputs = new TokenAmount[noOfOutputs]; + for (int i = 0; i < noOfOutputs; i++) { + outputs[i] = TokenAmount.decode(resultBuffer); + } + return outputs; + } + + @SneakyThrows + static Parameter serializeOperatorOfParameter(Collection queries) { + val bos = new ByteArrayOutputStream(); + // lengths are stored as little endian. + bos.write(UInt16.from(queries.size()).getBytesLittleEndian()); + for (OperatorQuery operatorQuery : queries) { + bos.write(SerializationUtils.serializeAddress(operatorQuery.getOwner())); + bos.write(SerializationUtils.serializeAddress(operatorQuery.getAddress())); + } + return Parameter.from(bos.toByteArray()); + } + + static boolean[] deserializeOperatorOfResponse(byte[] returnValue) { + val resultBuffer = ByteBuffer.wrap(returnValue); + // lengths are stored as little endian. + resultBuffer.order(ByteOrder.LITTLE_ENDIAN); + val noOfOutputs = UInt16.from(resultBuffer.getShort()).getValue(); + resultBuffer.order(ByteOrder.BIG_ENDIAN); + val outputs = new boolean[noOfOutputs]; + for (int i = 0; i < noOfOutputs; i++) { + outputs[i] = resultBuffer.get() != 0; + } + return outputs; + } + + @SneakyThrows + static Parameter serializeTokenIds(List listOfQueries) { + val bos = new ByteArrayOutputStream(); + // lengths are stored as little endian. + bos.write(UInt16.from(listOfQueries.size()).getBytesLittleEndian()); + for (TokenId tokenId : listOfQueries) { + bos.write(SerializationUtils.serializeTokenId(tokenId)); + } + return Parameter.from(bos.toByteArray()); + } + + @SneakyThrows + static TokenMetadata[] deserializeTokenMetadatas(byte[] returnValue) { + val resultBuffer = ByteBuffer.wrap(returnValue); + // lengths are stored as little endian. + resultBuffer.order(ByteOrder.LITTLE_ENDIAN); + val noOfOutputs = UInt16.from(resultBuffer.getShort()).getValue(); + val outputs = new TokenMetadata[noOfOutputs]; + for (int i = 0; i < noOfOutputs; i++) { + outputs[i] = deserializeTokenMetadata(resultBuffer); + resultBuffer.order(ByteOrder.LITTLE_ENDIAN); + } + return outputs; + } + + private static TokenMetadata deserializeTokenMetadata(ByteBuffer resultBuffer) throws MalformedURLException { + resultBuffer.order(ByteOrder.LITTLE_ENDIAN); + val urlLength = UInt16.from(resultBuffer.getShort()).getValue(); + resultBuffer.order(ByteOrder.BIG_ENDIAN); + val urlBytes = new byte[urlLength]; + resultBuffer.get(urlBytes); + val hasChecksum = resultBuffer.get() != 0; + byte[] checksumBuffer = null; + if (hasChecksum) { + checksumBuffer = new byte[32]; + resultBuffer.get(checksumBuffer); + } + return new TokenMetadata(new URL(new String(urlBytes, StandardCharsets.UTF_8)), checksumBuffer); + } + + @SneakyThrows + static byte[] serializeTokenId(TokenId tokenId) { + // size of token + serialized token id. + val tokenSize = tokenId.getSize(); + val buffer = ByteBuffer.allocate(1 + tokenSize); + buffer.put((byte) tokenSize); + if (tokenSize != 0) { + buffer.put(tokenId.getBytes()); + } + return buffer.array(); + } + + static byte[] serializeAddress(AbstractAddress address) { + if (address instanceof AccountAddress) { + val accountAddress = (AccountAddress) address; + val buffer = ByteBuffer.allocate(ACCOUNT_ADDRESS_SIZE); + buffer.put((byte) 0); // tag + buffer.put(accountAddress.getBytes()); + return buffer.array(); + } else if (address instanceof ContractAddress) { + ContractAddress contractAddress = (ContractAddress) address; + val buffer = ByteBuffer.allocate(CONTRACT_ADDRESS_SIZE); + buffer.put((byte) 1); // tag + // index and sub-index are stored as little endian. + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.putLong(contractAddress.getIndex()); + buffer.putLong(contractAddress.getSubIndex()); + return buffer.array(); + } + throw new IllegalArgumentException("AbstractAddress must be either an account address or contract address"); + } + + @SneakyThrows + public static Parameter serializeUpdateOperators(Map operatorUpdates) { + val bos = new ByteArrayOutputStream(); + bos.write(UInt16.from(operatorUpdates.size()).getBytesLittleEndian()); + for (AbstractAddress address : operatorUpdates.keySet()) { + bos.write((byte) (operatorUpdates.get(address) ? 1 : 0)); + bos.write(SerializationUtils.serializeAddress(address)); + } + return Parameter.from(bos.toByteArray()); + } + + public static Cis2Event deserializeCis2Event(byte[] eventBytes) { + val buffer = ByteBuffer.wrap(eventBytes); + val tag = buffer.get(); + val eventType = Cis2Event.Type.parse(tag); + switch (eventType) { + case TRANSFER: + return SerializationUtils.deserializeTransferEvent(buffer); + case MINT: + return SerializationUtils.deserializeMintEvent(buffer); + case BURN: + return SerializationUtils.deserializeBurnEvent(buffer); + case UPDATE_OPERATOR_OF: + return SerializationUtils.deserializeUpdateOperatorOfEvent(buffer); + case TOKEN_METADATA: + return SerializationUtils.deserializeTokenMetadataEvent(buffer); + case CUSTOM: + return SerializationUtils.deserializeCustomEvent(tag, buffer); + } + throw new IllegalArgumentException("Malformed CIS2 event"); + } + + private static Cis2Event deserializeCustomEvent(byte tag, ByteBuffer buffer) { + return new CustomEvent(tag, buffer.array()); + } + + @SneakyThrows + private static Cis2Event deserializeTokenMetadataEvent(ByteBuffer buffer) { + val tokenId = deserializeTokenId(buffer); + val tokenMetadata = deserializeTokenMetadata(buffer); + return new TokenMetadataEvent(tokenId, tokenMetadata); + } + + private static Cis2Event deserializeUpdateOperatorOfEvent(ByteBuffer buffer) { + val isOperator = buffer.get() != 0; + val owner = deserializeAddress(buffer); + val operator = deserializeAddress(buffer); + return new UpdateOperatorEvent(isOperator, owner, operator); + } + + private static Cis2Event deserializeBurnEvent(ByteBuffer buffer) { + val tokenId = deserializeTokenId(buffer); + val tokenAmount = TokenAmount.decode(buffer); + val owner = deserializeAddress(buffer); + return new BurnEvent(tokenId, tokenAmount, owner); + } + + private static Cis2Event deserializeMintEvent(ByteBuffer buffer) { + val tokenId = deserializeTokenId(buffer); + val tokenAmount = TokenAmount.decode(buffer); + val owner = deserializeAddress(buffer); + return new MintEvent(tokenId, tokenAmount, owner); + } + + public static Cis2Event deserializeTransferEvent(ByteBuffer buffer) { + val tokenId = deserializeTokenId(buffer); + val tokenAmount = TokenAmount.decode(buffer); + val from = deserializeAddress(buffer); + val to = deserializeAddress(buffer); + return new TransferEvent(tokenId, tokenAmount, from, to); + } + + @SneakyThrows + static TokenId deserializeTokenId(ByteBuffer buffer) { + byte tokenLength = buffer.get(); + val tokenBuffer = new byte[tokenLength]; + buffer.get(tokenBuffer); + return TokenId.from(tokenBuffer); + } + + static AbstractAddress deserializeAddress(ByteBuffer buffer) { + byte tag = buffer.get(); + if (tag == 0) { + val addressBuffer = new byte[AccountAddress.BYTES]; + buffer.get(addressBuffer); + return AccountAddress.from(addressBuffer); + } + if (tag == 1) { + long index = buffer.getLong(); + long subIndex = buffer.getLong(); + return ContractAddress.from(index, subIndex); + } + throw new IllegalArgumentException("Malformed address"); + } +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenAmount.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenAmount.java new file mode 100644 index 000000000..350cda652 --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenAmount.java @@ -0,0 +1,95 @@ +package com.concordium.sdk.cis2; + +import lombok.*; + +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.nio.ByteBuffer; + +/** + * An amount as specified in the CIS2 specification. + * https://proposals.concordium.software/CIS/cis-2.html#tokenamount + *

    + * It is an unsigned integer where the max value is 2^256 - 1. + */ +@EqualsAndHashCode +@ToString +public class TokenAmount { + + /** + * The maximum value of a CIS2 token as per the standard 2^256 - 1. + */ + public static final BigInteger MAX_VALUE = new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 16); + + @Getter + private final BigInteger amount; + + private TokenAmount(BigInteger value) { + if (value.compareTo(MAX_VALUE) > 0) throw new IllegalArgumentException("TokenAmount exceeds max value"); + this.amount = value; + } + + public static TokenAmount from(long value) { + return new TokenAmount(BigInteger.valueOf(value)); + } + + public static TokenAmount from(String value) { + if (value.startsWith("-")) throw new IllegalArgumentException("TokenAmount must be positive"); + return new TokenAmount(new BigInteger(value)); + } + + /** + * Encode the {@link TokenAmount} in LEB128 unsigned format. + * + * @return the serialized token amount + * @throws RuntimeException if the resulting byte array would exceed 37 bytes. + */ + @SneakyThrows + public byte[] encode() { + if (this.amount.equals(BigInteger.ZERO)) return new byte[]{0}; + val bos = new ByteArrayOutputStream(); + var value = this.amount; + // Loop until the most significant byte is zero or less + while (value.compareTo(BigInteger.ZERO) > 0) { + // Take the 7 least significant bits of the current value and set the MSB + var currentByte = value.and(BigInteger.valueOf(0x7F)).byteValue(); + value = value.shiftRight(7); + if (value.compareTo(BigInteger.ZERO) != 0) { + currentByte |= 0x80; // Set the MSB to 1 to indicate there are more bytes to come + } + bos.write(currentByte); + if (bos.size() > 37) + throw new IllegalArgumentException("Invalid encoding of TokenAmount. Must not exceed 37 byes."); + } + return bos.toByteArray(); + } + + /** + * Deserialize a {@link TokenAmount} from the provided buffer. + * This function assumes that the token amounts are LEB128U encoded. + * + * @param buffer the buffer to read from. + * @return the parsed {@link TokenAmount} + * @throws RuntimeException if the encoding is more than 37 bytes. + */ + public static TokenAmount decode(ByteBuffer buffer) { + var result = BigInteger.ZERO; + int shift = 0; + int count = 0; + while (true) { + if (count > 37) + throw new IllegalArgumentException("Tried to decode a TokenAmount which consists of more than 37 bytes."); + byte b = buffer.get(); + BigInteger byteValue = BigInteger.valueOf(b & 0x7F); // Mask to get 7 least significant bits + result = result.or(byteValue.shiftLeft(shift)); + if ((b & 0x80) == 0) { + break; // If MSB is 0, this is the last byte + } + shift += 7; + count++; + } + return new TokenAmount(result); + } + + +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenId.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenId.java new file mode 100644 index 000000000..9ac24d663 --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenId.java @@ -0,0 +1,98 @@ +package com.concordium.sdk.cis2; + +import com.concordium.sdk.types.UInt16; +import com.concordium.sdk.types.UInt32; +import com.concordium.sdk.types.UInt64; +import com.google.common.collect.Lists; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.val; +import org.apache.commons.lang3.ArrayUtils; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * A simple wrapper around the actual token id. + * https://proposals.concordium.software/CIS/cis-2.html#tokenid + * The token id simply consists of 0 or more bytes. + */ +@EqualsAndHashCode +@Getter +@ToString +public class TokenId { + + private static final int MAX_BYTES_SIZE = 255; + + private final byte[] bytes; + + private TokenId(byte[] bytes) { + this.bytes = bytes; + } + + /** + * Construct a minimum token id i.e., the empty one. + * @return the token id + */ + public static TokenId min() { + return new TokenId(new byte[0]); + } + + /** + * Get the size of the token (in number of bytes) + * @return the size of the token + */ + public int getSize() { + return this.bytes.length; + } + + /** + * Create a token from the provided byte array + * @param bytes the token id in raw bytes + * @return the token id + */ + public static TokenId from(byte[] bytes) { + if (bytes.length > MAX_BYTES_SIZE) { + throw new IllegalArgumentException("TokenId supersedes max allowed size."); + } + return new TokenId(bytes); + } + + /** + * Create a token id from the {@link UInt16} in little endian + * @param value the token id + * @return a new token id + */ + public static TokenId from(UInt16 value) { + val tokenBytes = value.getBytes(); + ArrayUtils.reverse(tokenBytes); + return new TokenId(tokenBytes); + } + + /** + * Create a token id from the {@link UInt32} in little endian + * @param value the token id + * @return a new token id + */ + public static TokenId from(UInt32 value) { + val tokenBytes = value.getBytes(); + ArrayUtils.reverse(tokenBytes); + return new TokenId(tokenBytes); + } + + /** + *Create a token id from the {@link UInt64} in little endian + * @param value the token id + * @return a new token id + */ + public static TokenId from(UInt64 value) { + val tokenBytes = value.getBytes(); + ArrayUtils.reverse(tokenBytes); + return new TokenId(tokenBytes); + } + +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenMetadata.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenMetadata.java new file mode 100644 index 000000000..d5d7584b2 --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenMetadata.java @@ -0,0 +1,41 @@ +package com.concordium.sdk.cis2; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +import java.net.URL; +import java.util.Optional; + +/** + * A single response to a {@link Cis2Client#tokenMetadata(String...)} query. + */ +@ToString +@EqualsAndHashCode +public class TokenMetadata { + + /** + * The metadata url. + */ + @Getter + private final URL metadataUrl; + + /** + * An optional checksum of the metadata url. + */ + private final byte[] checksum; + + + public TokenMetadata(URL metadataUrl, byte[] checksum) { + this.metadataUrl = metadataUrl; + this.checksum = checksum; + } + + /** + * Get the checksum if available. + * @return the checksum + */ + public Optional getChecksum() { + return Optional.ofNullable(this.checksum); + } +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/BurnEvent.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/BurnEvent.java new file mode 100644 index 000000000..8bbea739f --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/BurnEvent.java @@ -0,0 +1,42 @@ +package com.concordium.sdk.cis2.events; + +import com.concordium.sdk.cis2.TokenAmount; +import com.concordium.sdk.cis2.TokenId; +import com.concordium.sdk.types.AbstractAddress; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * An event emitted by a CIS2 compliant smart contract when one or more tokens have been burned. + * https://proposals.concordium.software/CIS/cis-2.html#burnevent + */ +@Getter +@ToString +@EqualsAndHashCode +public class BurnEvent implements Cis2Event { + + /** + * The token that was burned. + */ + private final TokenId tokenId; + /** + * The amount of tokens that was burned. + */ + private final TokenAmount tokenAmount; + /** + * The owner of the token + */ + private final AbstractAddress owner; + + public BurnEvent(TokenId tokenId, TokenAmount tokenAmount, AbstractAddress owner) { + this.tokenId = tokenId; + this.tokenAmount = tokenAmount; + this.owner = owner; + } + + @Override + public Type getType() { + return Type.BURN; + } +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/Cis2Event.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/Cis2Event.java new file mode 100644 index 000000000..31300048a --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/Cis2Event.java @@ -0,0 +1,38 @@ +package com.concordium.sdk.cis2.events; + +/** + * A common API for an event emitted by a CIS2 compliant contract. + */ +public interface Cis2Event { + /** + * Get the type of the CIS2 event. + */ + Type getType(); + + + /** + * Types of events that are supported by the CIS2 specification + */ + enum Type { + TRANSFER, + MINT, + BURN, + UPDATE_OPERATOR_OF, + TOKEN_METADATA, + CUSTOM; + + /** + * Parse the type of CIS2 event. + * @param tag the tag of the event. + * @return the resulting {@link Type} + */ + public static Type parse(byte tag) { + if (tag == -1) return TRANSFER; + if (tag == -2) return MINT; + if (tag == -3) return BURN; + if (tag == -4) return UPDATE_OPERATOR_OF; + if (tag == -5) return TOKEN_METADATA; + return CUSTOM; + } + } +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/Cis2EventWithMetadata.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/Cis2EventWithMetadata.java new file mode 100644 index 000000000..cc99969d5 --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/Cis2EventWithMetadata.java @@ -0,0 +1,65 @@ +package com.concordium.sdk.cis2.events; + +import com.concordium.sdk.cis2.Cis2Error; +import com.concordium.sdk.requests.BlockQuery; +import com.concordium.sdk.transactions.Hash; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +import java.util.Objects; + +/** + * An event emitted from a CIS2 compliant contract with some metadata. + */ +@EqualsAndHashCode +@ToString +@Getter +public class Cis2EventWithMetadata { + + /** + * The event of a successfully executed transaction. + * This is only present if the transaction was successfully executed. + */ + private final Cis2Event event; + + /** + * The error of a failed transaction. + * This is only present if the tranasction failed. + */ + private final Cis2Error error; + + /** + * The block of which the event was emitted in. + */ + private final BlockQuery blockIdentifier; + + /** + * The hash of the transaction that caused the event. + */ + private final Hash transactionHashOrigin; + + private Cis2EventWithMetadata(Cis2Event event, Cis2Error error, BlockQuery blockIdentifier, Hash origin) { + this.event = event; + this.error = error; + this.blockIdentifier = blockIdentifier; + this.transactionHashOrigin = origin; + } + + /** + * A flag that indicates whether the event retains an + * {@link Cis2Event} (as a result of a successfully executed transaction) + * or a {@link Cis2Error} as a result of a failed transaction. + */ + public boolean isSuccessfull() { + return Objects.isNull(error); + } + + public static Cis2EventWithMetadata ok(Cis2Event event, BlockQuery blockIdentifier, Hash origin) { + return new Cis2EventWithMetadata(event, null, blockIdentifier, origin); + } + + public static Cis2EventWithMetadata err(Cis2Error error, BlockQuery blockIdentifier, Hash origin) { + return new Cis2EventWithMetadata(null, error, blockIdentifier, origin); + } +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/CustomEvent.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/CustomEvent.java new file mode 100644 index 000000000..4428da218 --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/CustomEvent.java @@ -0,0 +1,27 @@ +package com.concordium.sdk.cis2.events; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * A custom event emitted from a CIS2 compliant contract. + */ +@EqualsAndHashCode +@ToString +@Getter +public class CustomEvent implements Cis2Event { + + private final byte tag; + private final byte[] data; + + public CustomEvent(byte tag, byte[] data) { + this.tag = tag; + this.data = data; + } + + @Override + public Type getType() { + return Type.CUSTOM; + } +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/MintEvent.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/MintEvent.java new file mode 100644 index 000000000..b628cc1fa --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/MintEvent.java @@ -0,0 +1,44 @@ +package com.concordium.sdk.cis2.events; + +import com.concordium.sdk.cis2.TokenAmount; +import com.concordium.sdk.cis2.TokenId; +import com.concordium.sdk.types.AbstractAddress; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * An event emitted by a CIS2 compliant smart contract when one or more tokens have been minted. + * https://proposals.concordium.software/CIS/cis-2.html#mintevent + */ +@Getter +@ToString +@EqualsAndHashCode +public class MintEvent implements Cis2Event { + + /** + * ID of the token that was minted. + */ + private final TokenId tokenId; + + /** + * Amount of tokens that was minted. + */ + private final TokenAmount tokenAmount; + + /** + * The owner of the minted tokens. + */ + private final AbstractAddress owner; + + public MintEvent(TokenId tokenId, TokenAmount tokenAmount, AbstractAddress owner) { + this.tokenId = tokenId; + this.tokenAmount = tokenAmount; + this.owner = owner; + } + + @Override + public Type getType() { + return Type.MINT; + } +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/TokenMetadataEvent.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/TokenMetadataEvent.java new file mode 100644 index 000000000..dca9e5856 --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/TokenMetadataEvent.java @@ -0,0 +1,38 @@ +package com.concordium.sdk.cis2.events; + +import com.concordium.sdk.cis2.TokenId; +import com.concordium.sdk.cis2.TokenMetadata; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * An event that is emitted by a CIS2 compliant contract when a + * particular token has its metadata url set. + */ +@Getter +@EqualsAndHashCode +@ToString +public class TokenMetadataEvent implements Cis2Event { + + /** + * The token id which was queried + */ + private final TokenId tokenId; + + /** + * The new metadata url for the token. + */ + private final TokenMetadata metadataUrl; + + public TokenMetadataEvent(TokenId tokenId, TokenMetadata metadataUrl) { + this.tokenId = tokenId; + this.metadataUrl = metadataUrl; + } + + + @Override + public Type getType() { + return Type.TOKEN_METADATA; + } +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/TransferEvent.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/TransferEvent.java new file mode 100644 index 000000000..e95fe3810 --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/TransferEvent.java @@ -0,0 +1,50 @@ +package com.concordium.sdk.cis2.events; + +import com.concordium.sdk.cis2.TokenAmount; +import com.concordium.sdk.cis2.TokenId; +import com.concordium.sdk.types.AbstractAddress; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * An event that is emitted from a CIS2 compliant contract when a token has been transferred. + * https://proposals.concordium.software/CIS/cis-2.html#transferevent + */ +@Getter +@EqualsAndHashCode +@ToString +public class TransferEvent implements Cis2Event { + + /** + * The id of the token(s) that were transferred. + */ + private final TokenId tokenId; + + /** + * The amount of tokens that was transferred. + */ + private final TokenAmount tokenAmount; + + /** + * The previous owner of the token(s). + */ + private final AbstractAddress from; + + /** + * The new owner of the token(s). + */ + private final AbstractAddress to; + + public TransferEvent(TokenId tokenId, TokenAmount tokenAmount, AbstractAddress from, AbstractAddress to) { + this.tokenId = tokenId; + this.tokenAmount = tokenAmount; + this.from = from; + this.to = to; + } + + @Override + public Type getType() { + return Type.TRANSFER; + } +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/UpdateOperatorEvent.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/UpdateOperatorEvent.java new file mode 100644 index 000000000..bec567400 --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/events/UpdateOperatorEvent.java @@ -0,0 +1,43 @@ +package com.concordium.sdk.cis2.events; + +import com.concordium.sdk.types.AbstractAddress; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * An event that is emitted from a CIS2 compliant contract when the operator has been updated + * for a particular address. + * https://proposals.concordium.software/CIS/cis-2.html#updateoperatorevent + */ +@Getter +@EqualsAndHashCode +@ToString +public class UpdateOperatorEvent implements Cis2Event { + + /** + * Whether an operator was added or removed. + */ + private final boolean isOperator; + + /** + * Invoker of the updateOperatorOf + */ + private final AbstractAddress owner; + + /** + * The operator that is either added or removed. + */ + private final AbstractAddress operator; + + public UpdateOperatorEvent(boolean isOperator, AbstractAddress owner, AbstractAddress operator) { + this.isOperator = isOperator; + this.owner = owner; + this.operator = operator; + } + + @Override + public Type getType() { + return Type.UPDATE_OPERATOR_OF; + } +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/requests/BlockQuery.java b/concordium-sdk/src/main/java/com/concordium/sdk/requests/BlockQuery.java index 1f45b427f..4b19ee67a 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/requests/BlockQuery.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/requests/BlockQuery.java @@ -5,6 +5,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.ToString; import javax.annotation.Nullable; @@ -12,6 +13,7 @@ * Type of Block to query an API with. For a list of types see {@link BlockQueryType} */ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@ToString public class BlockQuery { /** diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/requests/smartcontracts/InvokeInstanceRequest.java b/concordium-sdk/src/main/java/com/concordium/sdk/requests/smartcontracts/InvokeInstanceRequest.java index adec23194..e42e36cb0 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/requests/smartcontracts/InvokeInstanceRequest.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/requests/smartcontracts/InvokeInstanceRequest.java @@ -9,6 +9,7 @@ import lombok.*; import java.util.Objects; +import java.util.Optional; /** * Request for InvokeInstance @@ -56,8 +57,10 @@ public class InvokeInstanceRequest { * The amount of energy to allow for execution. * This cannot exceed `100_000_000_000`, but in practice it should be much less. * The maximum block energy is typically in the range of a few million. + * If this is not set, then the node will determine a sufficient amount of energy allowed for the + * transaction execution. */ - private Energy energy; + private Optional energy; /** * Creates a {@link InvokeInstanceRequest} from the given parameters. @@ -70,15 +73,16 @@ public class InvokeInstanceRequest { * @param amount Amount to invoke the smart contract instance with. * @param entrypoint The {@link ReceiveName} of the smart contract instance to invoke. * @param parameter The parameter bytes to include in the invocation of the entrypoint. - * @param energy The amount of energy to allow for execution. + * @param energy The amount of energy to allow for execution. If this is not set, then the node + * will decide a sufficient amount of energy allowed for transaction execution. */ - public static InvokeInstanceRequest from(BlockQuery blockHash, - AbstractAddress invoker, - ContractAddress instance, - CCDAmount amount, - ReceiveName entrypoint, - Parameter parameter, - Energy energy) { + public static InvokeInstanceRequest from(@NonNull BlockQuery blockHash, + @NonNull AbstractAddress invoker, + @NonNull ContractAddress instance, + @NonNull CCDAmount amount, + @NonNull ReceiveName entrypoint, + @NonNull Parameter parameter, + @NonNull Optional energy) { return InvokeInstanceRequest.builder() .blockHash(blockHash) .invoker(invoker) @@ -98,14 +102,15 @@ public static InvokeInstanceRequest from(BlockQuery blockHash, * @param amount Amount to invoke the smart contract instance with. * @param entrypoint The {@link ReceiveName} of the smart contract instance to invoke. * @param parameter The parameter bytes to include in the invocation of the entrypoint. - * @param energy The amount of energy to allow for execution. + * @param energy The amount of energy to allow for execution. If this is not set, then the node + * will decide a sufficient amount of energy allowed for transaction execution. */ - public static InvokeInstanceRequest from(BlockQuery blockHash, - ContractAddress instance, - CCDAmount amount, - ReceiveName entrypoint, - Parameter parameter, - Energy energy) { + public static InvokeInstanceRequest from(@NonNull BlockQuery blockHash, + @NonNull ContractAddress instance, + @NonNull CCDAmount amount, + @NonNull ReceiveName entrypoint, + @NonNull Parameter parameter, + @NonNull Optional energy) { return InvokeInstanceRequest.builder() .blockHash(blockHash) .instance(instance) @@ -125,14 +130,15 @@ public static InvokeInstanceRequest from(BlockQuery blockHash, * @param instance Address of the contract instance to invoke. * @param amount Amount to invoke the smart contract instance with. * @param schemaParameter {@link SchemaParameter} message to invoke the contract with. Must be initialized with {@link SchemaParameter#initialize()} beforehand. - * @param energy The amount of energy to allow for execution. + * @param energy The amount of energy to allow for execution. If this is not set, then the node + * will decide a sufficient amount of energy allowed for transaction execution. */ - public static InvokeInstanceRequest from(BlockQuery blockHash, - AbstractAddress invoker, - ContractAddress instance, - CCDAmount amount, - SchemaParameter schemaParameter, - Energy energy) { + public static InvokeInstanceRequest from(@NonNull BlockQuery blockHash, + @NonNull AbstractAddress invoker, + @NonNull ContractAddress instance, + @NonNull CCDAmount amount, + @NonNull SchemaParameter schemaParameter, + @NonNull Optional energy) { if (!(schemaParameter.getType() == ParameterType.RECEIVE)) { throw new IllegalArgumentException("Cannot initialize smart contract with InvokeInstance. SchemaParameter for InvokeInstanceRequest must be initialized with a ReceiveName"); } @@ -147,13 +153,14 @@ public static InvokeInstanceRequest from(BlockQuery blockHash, * @param instance Address of the contract instance to invoke. * @param amount Amount to invoke the smart contract instance with. * @param schemaParameter {@link SchemaParameter} message to invoke the contract with. Must be initialized with {@link SchemaParameter#initialize()} beforehand. - * @param energy The amount of energy to allow for execution. + * @param energy The amount of energy to allow for execution. If this is not set, then the node + * will decide a sufficient amount of energy allowed for transaction execution. */ - public static InvokeInstanceRequest from(BlockQuery blockHash, - ContractAddress instance, - CCDAmount amount, - SchemaParameter schemaParameter, - Energy energy) { + public static InvokeInstanceRequest from(@NonNull BlockQuery blockHash, + @NonNull ContractAddress instance, + @NonNull CCDAmount amount, + @NonNull SchemaParameter schemaParameter, + @NonNull Optional energy) { if (!(schemaParameter.getType() == ParameterType.RECEIVE)) { throw new IllegalArgumentException("Cannot initialize smart contract with InvokeInstance. SchemaParameter for InvokeInstanceRequest must be initialized with a ReceiveName"); } diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/responses/blockitemsummary/AccountTransactionDetails.java b/concordium-sdk/src/main/java/com/concordium/sdk/responses/blockitemsummary/AccountTransactionDetails.java index b92417d4c..c5003edcd 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/responses/blockitemsummary/AccountTransactionDetails.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/responses/blockitemsummary/AccountTransactionDetails.java @@ -1,22 +1,23 @@ package com.concordium.sdk.responses.blockitemsummary; -import com.concordium.grpc.v2.AccountTransactionEffects; +import com.concordium.sdk.crypto.bls.BLSPublicKey; +import com.concordium.sdk.responses.BakerId; import com.concordium.sdk.responses.modulelist.ModuleRef; import com.concordium.sdk.responses.smartcontracts.ContractTraceElement; import com.concordium.sdk.responses.transactionstatus.*; import com.concordium.sdk.transactions.*; import com.concordium.sdk.types.AccountAddress; +import com.concordium.sdk.types.ContractAddress; import lombok.*; import java.util.List; -import java.util.Optional; import java.util.stream.Collectors; /** * Details of a transaction with a sender account. - * + *

    * Users should always check whether the {@link AccountTransactionDetails} is {@link AccountTransactionDetails#isSuccessful()} or not. - * If this returns false, then one should consult the {@link AccountTransactionDetails#rejectReason} for why the transaction failed. + * If this returns false, then one should consult the {@link AccountTransactionDetails#rejectReasonType} for why the transaction failed. * If the transaction was successful then first check the type via {@link AccountTransactionDetails#getType()} and use the corresponding * getter for getting the concrete event. */ @@ -47,7 +48,13 @@ public class AccountTransactionDetails { /** * Present if the transaction failed. */ - private final RejectReasonType rejectReason; + private final RejectReasonType rejectReasonType; + + + /** + * Present if the transaction failed. + */ + private final RejectReason rejectReason; /** * True if the transaction was successfully executed. @@ -171,114 +178,68 @@ public class AccountTransactionDetails { public static AccountTransactionDetails from(com.concordium.grpc.v2.AccountTransactionDetails tx) { val sender = AccountAddress.from(tx.getSender()); - val detailsBuilder = AccountTransactionDetails - .builder() - .sender(sender) - .cost(CCDAmount.from(tx.getCost())) - .successful(true); + val detailsBuilder = AccountTransactionDetails.builder().sender(sender).cost(CCDAmount.from(tx.getCost())).successful(true); val effects = tx.getEffects(); switch (effects.getEffectCase()) { case NONE: - detailsBuilder - .successful(false) - .rejectReason(RejectReasonType.from(effects.getNone().getRejectReason())); + val reason = effects.getNone().getRejectReason(); + detailsBuilder.successful(false).rejectReasonType(RejectReasonType.from(reason)); + extractRejectReasonError(detailsBuilder, reason); break; case MODULE_DEPLOYED: - detailsBuilder - .type(TransactionResultEventType.MODULE_DEPLOYED) - .moduleDeployed(ModuleRef.from(effects.getModuleDeployed().getValue().toByteArray())); + detailsBuilder.type(TransactionResultEventType.MODULE_DEPLOYED).moduleDeployed(ModuleRef.from(effects.getModuleDeployed().getValue().toByteArray())); break; case CONTRACT_INITIALIZED: - detailsBuilder - .type(TransactionResultEventType.CONTRACT_INITIALIZED) - .contractInitialized(ContractInitializedResult.from(effects.getContractInitialized())); + detailsBuilder.type(TransactionResultEventType.CONTRACT_INITIALIZED).contractInitialized(ContractInitializedResult.from(effects.getContractInitialized())); break; case CONTRACT_UPDATE_ISSUED: - val updateEvents = effects - .getContractUpdateIssued() - .getEffectsList() - .stream() - .map(ContractTraceElement::from) - .collect(Collectors.toList()); - detailsBuilder - .type(TransactionResultEventType.CONTRACT_UPDATED) - .contractUpdated(updateEvents); + val updateEvents = effects.getContractUpdateIssued().getEffectsList().stream().map(ContractTraceElement::from).collect(Collectors.toList()); + detailsBuilder.type(TransactionResultEventType.CONTRACT_UPDATED).contractUpdated(updateEvents); break; case ACCOUNT_TRANSFER: - detailsBuilder - .type(TransactionResultEventType.TRANSFERRED) - .accountTransfer(TransferredResult.from(effects.getAccountTransfer(), sender)); - + detailsBuilder.type(TransactionResultEventType.TRANSFERRED).accountTransfer(TransferredResult.from(effects.getAccountTransfer(), sender)); break; case BAKER_ADDED: - detailsBuilder - .type(TransactionResultEventType.BAKER_ADDED) - .bakerAdded(BakerAddedResult.from(effects.getBakerAdded())); + detailsBuilder.type(TransactionResultEventType.BAKER_ADDED).bakerAdded(BakerAddedResult.from(effects.getBakerAdded())); break; case BAKER_REMOVED: - detailsBuilder - .type(TransactionResultEventType.BAKER_REMOVED) - .bakerRemoved(BakerRemovedResult.from(effects.getBakerRemoved(), sender)); + detailsBuilder.type(TransactionResultEventType.BAKER_REMOVED).bakerRemoved(BakerRemovedResult.from(effects.getBakerRemoved(), sender)); break; case BAKER_STAKE_UPDATED: - detailsBuilder - .type(TransactionResultEventType.BAKER_STAKE_UPDATED) - .bakerStakeUpdated(BakerStakeUpdated.from(effects.getBakerStakeUpdated(), sender)); + detailsBuilder.type(TransactionResultEventType.BAKER_STAKE_UPDATED).bakerStakeUpdated(BakerStakeUpdated.from(effects.getBakerStakeUpdated(), sender)); break; case BAKER_RESTAKE_EARNINGS_UPDATED: - detailsBuilder - .type(TransactionResultEventType.BAKER_SET_RESTAKE_EARNINGS) - .bakerRestakeEarningsUpdated(BakerSetRestakeEarningsResult.from(effects.getBakerRestakeEarningsUpdated(), sender)); + detailsBuilder.type(TransactionResultEventType.BAKER_SET_RESTAKE_EARNINGS).bakerRestakeEarningsUpdated(BakerSetRestakeEarningsResult.from(effects.getBakerRestakeEarningsUpdated(), sender)); break; case BAKER_KEYS_UPDATED: - detailsBuilder - .type(TransactionResultEventType.BAKER_KEYS_UPDATED) - .bakerKeysUpdated(BakerKeysUpdatedResult.from(effects.getBakerKeysUpdated(), sender)); + detailsBuilder.type(TransactionResultEventType.BAKER_KEYS_UPDATED).bakerKeysUpdated(BakerKeysUpdatedResult.from(effects.getBakerKeysUpdated(), sender)); break; case ENCRYPTED_AMOUNT_TRANSFERRED: - detailsBuilder - .type(TransactionResultEventType.ENCRYPTED_TRANSFER) - .encryptedTransfer(EncryptedTransferResult.from(effects.getEncryptedAmountTransferred())); + detailsBuilder.type(TransactionResultEventType.ENCRYPTED_TRANSFER).encryptedTransfer(EncryptedTransferResult.from(effects.getEncryptedAmountTransferred())); break; case TRANSFERRED_TO_ENCRYPTED: - detailsBuilder - .type(TransactionResultEventType.ENCRYPTED_SELF_AMOUNT_ADDED) - .addedToEncryptedBalance(EncryptedSelfAmountAddedResult.from(effects.getTransferredToEncrypted())); + detailsBuilder.type(TransactionResultEventType.ENCRYPTED_SELF_AMOUNT_ADDED).addedToEncryptedBalance(EncryptedSelfAmountAddedResult.from(effects.getTransferredToEncrypted())); break; case TRANSFERRED_TO_PUBLIC: - detailsBuilder - .type(TransactionResultEventType.ENCRYPTED_AMOUNTS_REMOVED) - .removedFromEncryptedBalance(EncryptedAmountsRemovedResult.from(effects.getTransferredToPublic().getRemoved())); + detailsBuilder.type(TransactionResultEventType.ENCRYPTED_AMOUNTS_REMOVED).removedFromEncryptedBalance(EncryptedAmountsRemovedResult.from(effects.getTransferredToPublic().getRemoved())); break; case TRANSFERRED_WITH_SCHEDULE: - detailsBuilder - .type(TransactionResultEventType.TRANSFERRED_WITH_SCHEDULE) - .transferredWithSchedule(TransferredWithScheduleResult.from(effects.getTransferredWithSchedule(), sender)); + detailsBuilder.type(TransactionResultEventType.TRANSFERRED_WITH_SCHEDULE).transferredWithSchedule(TransferredWithScheduleResult.from(effects.getTransferredWithSchedule(), sender)); break; case CREDENTIAL_KEYS_UPDATED: - detailsBuilder - .type(TransactionResultEventType.CREDENTIAL_KEYS_UPDATED) - .credentialKeysUpdated(CredentialKeysUpdatedResult.from(effects.getCredentialKeysUpdated())); + detailsBuilder.type(TransactionResultEventType.CREDENTIAL_KEYS_UPDATED).credentialKeysUpdated(CredentialKeysUpdatedResult.from(effects.getCredentialKeysUpdated())); break; case CREDENTIALS_UPDATED: - detailsBuilder - .type(TransactionResultEventType.CREDENTIALS_UPDATED) - .credentialsUpdated(CredentialsUpdatedResult.from(effects.getCredentialsUpdated(), sender)); + detailsBuilder.type(TransactionResultEventType.CREDENTIALS_UPDATED).credentialsUpdated(CredentialsUpdatedResult.from(effects.getCredentialsUpdated(), sender)); break; case DATA_REGISTERED: - detailsBuilder - .type(TransactionResultEventType.DATA_REGISTERED) - .dataRegistered(DataRegisteredResult.from(effects.getDataRegistered())); + detailsBuilder.type(TransactionResultEventType.DATA_REGISTERED).dataRegistered(DataRegisteredResult.from(effects.getDataRegistered())); break; case BAKER_CONFIGURED: - detailsBuilder - .type(TransactionResultEventType.BAKER_CONFIGURED) - .bakerConfigured(BakerConfigured.from(effects.getBakerConfigured(), sender)); + detailsBuilder.type(TransactionResultEventType.BAKER_CONFIGURED).bakerConfigured(BakerConfigured.from(effects.getBakerConfigured(), sender)); break; case DELEGATION_CONFIGURED: - detailsBuilder - .type(TransactionResultEventType.DELEGATION_CONFIGURED) - .delegatorConfigured(DelegatorConfigured.from(effects.getDelegationConfigured(), sender)); + detailsBuilder.type(TransactionResultEventType.DELEGATION_CONFIGURED).delegatorConfigured(DelegatorConfigured.from(effects.getDelegationConfigured(), sender)); break; case EFFECT_NOT_SET: throw new IllegalArgumentException("Unrecognized effect."); @@ -286,4 +247,177 @@ public static AccountTransactionDetails from(com.concordium.grpc.v2.AccountTrans return detailsBuilder.build(); } + private static void extractRejectReasonError(AccountTransactionDetailsBuilder detailsBuilder, com.concordium.grpc.v2.RejectReason reason) { + switch (reason.getReasonCase()) { + case MODULE_NOT_WF: + break; + case MODULE_HASH_ALREADY_EXISTS: + detailsBuilder.rejectReason(RejectReasonModuleHashAlreadyExists.builder().moduleRef(ModuleRef.from(reason.getModuleHashAlreadyExists().toByteArray())).build()); + break; + case INVALID_ACCOUNT_REFERENCE: + detailsBuilder.rejectReason(RejectReasonInvalidAccountReference.builder().address(AccountAddress.from(reason.getInvalidAccountReference())).build()); + break; + case INVALID_INIT_METHOD: + val invalidInitName = InitName.from(reason.getInvalidInitMethod().getInitName().getValue()); + detailsBuilder.rejectReason(RejectReasonInvalidInitMethod.builder().initName(invalidInitName).moduleRef(ModuleRef.from(reason.getInvalidInitMethod().getModuleRef())).build()); + break; + case INVALID_RECEIVE_METHOD: + detailsBuilder.rejectReason(RejectReasonInvalidReceiveMethod.builder().moduleRef(ModuleRef.from(reason.getInvalidReceiveMethod().getModuleRef().getValue().toByteArray())).receiveName(ReceiveName.from(reason.getInvalidReceiveMethod().getReceiveName())).build()); + break; + case INVALID_MODULE_REFERENCE: + detailsBuilder.rejectReason(RejectReasonInvalidModuleReference.builder().moduleRef(ModuleRef.from(reason.getInvalidModuleReference().toByteArray())).build()); + break; + case INVALID_CONTRACT_ADDRESS: + detailsBuilder.rejectReason(RejectReasonInvalidContractAddress.builder().contractAddress(ContractAddress.from(reason.getInvalidContractAddress())).build()); + break; + case RUNTIME_FAILURE: + detailsBuilder.rejectReason(new RejectReasonRuntimeFailure()); + break; + case AMOUNT_TOO_LARGE: + detailsBuilder.rejectReason(RejectReasonAmountTooLarge.builder().account(AccountAddress.from(reason.getAmountTooLarge().getAddress())).amount(CCDAmount.from(reason.getAmountTooLarge().getAmount())).build()); + break; + case SERIALIZATION_FAILURE: + detailsBuilder.rejectReason(new RejectReasonSerializationFailure()); + break; + case OUT_OF_ENERGY: + detailsBuilder.rejectReason(new RejectReasonOutOfEnergy()); + break; + case REJECTED_INIT: + detailsBuilder.rejectReason(RejectReasonRejectedInit.builder().rejectedInit(reason.getRejectedInit().getRejectReason()).build()); + break; + case REJECTED_RECEIVE: + detailsBuilder.rejectReason(RejectReasonRejectedReceive.builder().rejectReason(reason.getRejectedReceive().getRejectReason()).receiveName(ReceiveName.parse(reason.getRejectedReceive().getReceiveName().getValue())).contractAddress(ContractAddress.from(reason.getRejectedReceive().getContractAddress())).parameter(Parameter.from(reason.getRejectedReceive().getParameter())).build()); + break; + case INVALID_PROOF: + detailsBuilder.rejectReason(new RejectReasonInvalidProof()); + break; + case ALREADY_A_BAKER: + detailsBuilder.rejectReason(RejectReasonAlreadyABaker.builder().bakerId(BakerId.from(reason.getAlreadyABaker().getValue())).build()); + break; + case NOT_A_BAKER: + detailsBuilder.rejectReason(RejectReasonNotABaker.builder().accountAddress(AccountAddress.from(reason.getNotABaker())).build()); + break; + case INSUFFICIENT_BALANCE_FOR_BAKER_STAKE: + detailsBuilder.rejectReason(new RejectReasonInsufficientBalanceForBakerStake()); + break; + case STAKE_UNDER_MINIMUM_THRESHOLD_FOR_BAKING: + detailsBuilder.rejectReason(new RejectReasonStakeUnderMinimumThresholdForBaking()); + break; + case BAKER_IN_COOLDOWN: + detailsBuilder.rejectReason(new RejectReasonBakerInCooldown()); + break; + case DUPLICATE_AGGREGATION_KEY: + detailsBuilder.rejectReason(RejectReasonDuplicateAggregationKey.builder().publicKey(BLSPublicKey.from(reason.getDuplicateAggregationKey().toByteArray())).build()); + break; + case NON_EXISTENT_CREDENTIAL_ID: + detailsBuilder.rejectReason(new RejectReasonNonExistentCredentialID()); + break; + case KEY_INDEX_ALREADY_IN_USE: + detailsBuilder.rejectReason(new RejectReasonKeyIndexAlreadyInUse()); + break; + case INVALID_ACCOUNT_THRESHOLD: + detailsBuilder.rejectReason(new RejectReasonInvalidAccountThreshold()); + break; + case INVALID_CREDENTIAL_KEY_SIGN_THRESHOLD: + detailsBuilder.rejectReason(new RejectReasonInvalidCredentialKeySignThreshold()); + break; + case INVALID_ENCRYPTED_AMOUNT_TRANSFER_PROOF: + detailsBuilder.rejectReason(new RejectReasonInvalidEncryptedAmountTransferProof()); + break; + case INVALID_TRANSFER_TO_PUBLIC_PROOF: + detailsBuilder.rejectReason(new RejectReasonInvalidTransferToPublicProof()); + break; + case ENCRYPTED_AMOUNT_SELF_TRANSFER: + detailsBuilder.rejectReason(RejectReasonEncryptedAmountSelfTransfer.builder().address(AccountAddress.from(reason.getEncryptedAmountSelfTransfer())).build()); + break; + case INVALID_INDEX_ON_ENCRYPTED_TRANSFER: + detailsBuilder.rejectReason(new RejectReasonInvalidIndexOnEncryptedTransfer()); + break; + case ZERO_SCHEDULEDAMOUNT: + detailsBuilder.rejectReason(new RejectReasonZeroScheduledAmount()); + break; + case NON_INCREASING_SCHEDULE: + detailsBuilder.rejectReason(new RejectReasonNonIncreasingSchedule()); + break; + case FIRST_SCHEDULED_RELEASE_EXPIRED: + detailsBuilder.rejectReason(new RejectReasonFirstScheduledReleaseExpired()); + break; + case SCHEDULED_SELF_TRANSFER: + detailsBuilder.rejectReason(RejectReasonScheduledSelfTransfer.builder().accountAddress(AccountAddress.from(reason.getScheduledSelfTransfer())).build()); + break; + case INVALID_CREDENTIALS: + detailsBuilder.rejectReason(new RejectReasonInvalidCredentials()); + break; + case DUPLICATE_CRED_IDS: + detailsBuilder.rejectReason(RejectReasonDuplicateCredIDs.builder().duplicates(reason.getDuplicateCredIds().getIdsList().stream().map(CredentialRegistrationId::from).collect(Collectors.toList())).build()); + break; + case NON_EXISTENT_CRED_IDS: + detailsBuilder.rejectReason(RejectReasonNonExistentCredIDs.builder().ids(reason.getNonExistentCredIds().getIdsList().stream().map(CredentialRegistrationId::from).collect(Collectors.toList())).build()); + break; + case REMOVE_FIRST_CREDENTIAL: + detailsBuilder.rejectReason(new RejectReasonRemoveFirstCredential()); + break; + case CREDENTIAL_HOLDER_DID_NOT_SIGN: + detailsBuilder.rejectReason(new RejectReasonCredentialHolderDidNotSign()); + break; + case NOT_ALLOWED_MULTIPLE_CREDENTIALS: + detailsBuilder.rejectReason(new RejectReasonNotAllowedMultipleCredentials()); + break; + case NOT_ALLOWED_TO_RECEIVE_ENCRYPTED: + detailsBuilder.rejectReason(new RejectReasonNotAllowedToReceiveEncrypted()); + break; + case NOT_ALLOWED_TO_HANDLE_ENCRYPTED: + detailsBuilder.rejectReason(new RejectReasonNotAllowedToHandleEncrypted()); + break; + case MISSING_BAKER_ADD_PARAMETERS: + detailsBuilder.rejectReason(new RejectReasonMissingBakerAddParameters()); + break; + case FINALIZATION_REWARD_COMMISSION_NOT_IN_RANGE: + detailsBuilder.rejectReason(new RejectReasonFinalizationRewardCommissionNotInRange()); + break; + case BAKING_REWARD_COMMISSION_NOT_IN_RANGE: + detailsBuilder.rejectReason(new RejectReasonBakingRewardCommissionNotInRange()); + break; + case TRANSACTION_FEE_COMMISSION_NOT_IN_RANGE: + detailsBuilder.rejectReason(new RejectReasonTransactionFeeCommissionNotInRange()); + break; + case ALREADY_A_DELEGATOR: + detailsBuilder.rejectReason(RejectReasonAlreadyABaker.builder().bakerId(BakerId.from(reason.getAlreadyABaker())).build()); + break; + case INSUFFICIENT_BALANCE_FOR_DELEGATION_STAKE: + detailsBuilder.rejectReason(new RejectReasonInsufficientDelegationStake()); + break; + case MISSING_DELEGATION_ADD_PARAMETERS: + detailsBuilder.rejectReason(new RejectReasonMissingDelegationAddParameters()); + break; + case INSUFFICIENT_DELEGATION_STAKE: + detailsBuilder.rejectReason(new RejectReasonInsufficientDelegationStake()); + break; + case DELEGATOR_IN_COOLDOWN: + detailsBuilder.rejectReason(new RejectReasonDelegatorInCooldown()); + break; + case NOT_A_DELEGATOR: + detailsBuilder.rejectReason(RejectReasonNotADelegator.builder() + .accountAddress(AccountAddress.from(reason.getNotADelegator())) + .build()); + break; + case DELEGATION_TARGET_NOT_A_BAKER: + detailsBuilder.rejectReason(RejectReasonDelegationTargetNotABaker.builder() + .bakerId(BakerId.from(reason.getDelegationTargetNotABaker())) + .build()); + break; + case STAKE_OVER_MAXIMUM_THRESHOLD_FOR_POOL: + detailsBuilder.rejectReason(new RejectReasonStakeOverMaximumThresholdForPool()); + break; + case POOL_WOULD_BECOME_OVER_DELEGATED: + detailsBuilder.rejectReason(new RejectReasonPoolWouldBecomeOverDelegated()); + break; + case POOL_CLOSED: + detailsBuilder.rejectReason(new RejectReasonPoolClosed()); + break; + case REASON_NOT_SET: + break; + } + } + } diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/responses/smartcontracts/ContractTraceElementType.java b/concordium-sdk/src/main/java/com/concordium/sdk/responses/smartcontracts/ContractTraceElementType.java index d76547264..fed1a53ed 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/responses/smartcontracts/ContractTraceElementType.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/responses/smartcontracts/ContractTraceElementType.java @@ -7,27 +7,27 @@ public enum ContractTraceElementType { /** * A contract instance was updated. - * This event type corresponds to the concrete event {@link ContractUpdated} + * This event type corresponds to the concrete event {@link com.concordium.sdk.responses.transactionstatus.ContractUpdated} */ INSTANCE_UPDATED, /** * A contract to account transfer occurred. - * This event type corresponds to the concrete event {@link TransferredResult}. + * This event type corresponds to the concrete event {@link com.concordium.sdk.responses.transactionstatus.TransferredResult}. */ TRANSFERRED, /** * A contract was interrupted. - * This event type corresponds to the concrete event {@link InterruptedResult}. + * This event type corresponds to the concrete event {@link com.concordium.sdk.responses.transactionstatus.InterruptedResult}. */ INTERRUPTED, /** * A previously interrupted contract was resumed. - * This event type corresponds to the concrete event {@link ResumedResult}. + * This event type corresponds to the concrete event {@link com.concordium.sdk.responses.transactionstatus.ResumedResult}. */ RESUMED, /** * A contract was successfully upgraded. - * This event type corresponds to the concrete event {@link UpgradedResult}. + * This event type corresponds to the concrete event {@link com.concordium.sdk.responses.transactionstatus.UpgradedResult}. */ UPGRADED } diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonAlreadyABaker.java b/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonAlreadyABaker.java index ffa3df00d..1ec5d4255 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonAlreadyABaker.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonAlreadyABaker.java @@ -1,6 +1,7 @@ package com.concordium.sdk.responses.transactionstatus; import com.concordium.sdk.responses.AccountIndex; +import com.concordium.sdk.responses.BakerId; import lombok.Builder; import lombok.Getter; import lombok.ToString; @@ -12,7 +13,7 @@ @Getter @Builder public class RejectReasonAlreadyABaker extends RejectReason { - private final AccountIndex bakerId; + private final BakerId bakerId; @Override public RejectReasonType getType() { diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonDelegationTargetNotABaker.java b/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonDelegationTargetNotABaker.java index d3b538ba2..361696cab 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonDelegationTargetNotABaker.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonDelegationTargetNotABaker.java @@ -1,6 +1,7 @@ package com.concordium.sdk.responses.transactionstatus; import com.concordium.sdk.responses.AccountIndex; +import com.concordium.sdk.responses.BakerId; import lombok.Builder; import lombok.Getter; import lombok.ToString; @@ -15,7 +16,7 @@ public class RejectReasonDelegationTargetNotABaker extends RejectReason { /** * The delegation target which was not a baker. */ - private final AccountIndex bakerId; + private final BakerId bakerId; @Override public RejectReasonType getType() { diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonDuplicateAggregationKey.java b/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonDuplicateAggregationKey.java index bd7e25557..8da09542f 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonDuplicateAggregationKey.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonDuplicateAggregationKey.java @@ -1,5 +1,6 @@ package com.concordium.sdk.responses.transactionstatus; +import com.concordium.sdk.crypto.bls.BLSPublicKey; import lombok.Builder; import lombok.Getter; import lombok.ToString; @@ -11,7 +12,7 @@ @Builder public class RejectReasonDuplicateAggregationKey extends RejectReason { @Getter - private final String publicKey; + private final BLSPublicKey publicKey; @Override public RejectReasonType getType() { diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonInvalidCredentials.java b/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonInvalidCredentials.java index 0a7edb8cc..c6ca26a99 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonInvalidCredentials.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonInvalidCredentials.java @@ -6,7 +6,7 @@ * At least one of the credentials was either malformed or its proof was incorrect. */ @ToString -class RejectReasonInvalidCredentials extends RejectReason { +public class RejectReasonInvalidCredentials extends RejectReason { @Override public RejectReasonType getType() { return RejectReasonType.INVALID_CREDENTIALS; diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonInvalidInitMethod.java b/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonInvalidInitMethod.java index 9963a2cf0..4cf5159c4 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonInvalidInitMethod.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonInvalidInitMethod.java @@ -1,5 +1,7 @@ package com.concordium.sdk.responses.transactionstatus; +import com.concordium.sdk.responses.modulelist.ModuleRef; +import com.concordium.sdk.transactions.InitName; import lombok.Builder; import lombok.Getter; import lombok.ToString; @@ -11,8 +13,8 @@ @ToString @Builder public class RejectReasonInvalidInitMethod extends RejectReason { - private final String moduleRef; - private final String initName; + private final ModuleRef moduleRef; + private final InitName initName; @Override public RejectReasonType getType() { diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonInvalidModuleReference.java b/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonInvalidModuleReference.java index 4d0d64bdb..4c1accc99 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonInvalidModuleReference.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonInvalidModuleReference.java @@ -1,5 +1,6 @@ package com.concordium.sdk.responses.transactionstatus; +import com.concordium.sdk.responses.modulelist.ModuleRef; import lombok.Builder; import lombok.Getter; import lombok.ToString; @@ -11,7 +12,7 @@ @Builder public class RejectReasonInvalidModuleReference extends RejectReason { @Getter - private final String moduleRef; + private final ModuleRef moduleRef; @Override public RejectReasonType getType() { diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonInvalidReceiveMethod.java b/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonInvalidReceiveMethod.java index a79238822..a73ca4c50 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonInvalidReceiveMethod.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonInvalidReceiveMethod.java @@ -1,5 +1,7 @@ package com.concordium.sdk.responses.transactionstatus; +import com.concordium.sdk.responses.modulelist.ModuleRef; +import com.concordium.sdk.transactions.ReceiveName; import lombok.Builder; import lombok.Getter; import lombok.ToString; @@ -10,9 +12,9 @@ @Getter @ToString @Builder -class RejectReasonInvalidReceiveMethod extends RejectReason { - private final String moduleRef; - private final String receiveName; +public class RejectReasonInvalidReceiveMethod extends RejectReason { + private final ModuleRef moduleRef; + private final ReceiveName receiveName; @Override public RejectReasonType getType() { diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonModuleHashAlreadyExists.java b/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonModuleHashAlreadyExists.java index 5da8e281c..87815055f 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonModuleHashAlreadyExists.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonModuleHashAlreadyExists.java @@ -1,5 +1,6 @@ package com.concordium.sdk.responses.transactionstatus; +import com.concordium.sdk.responses.modulelist.ModuleRef; import lombok.Builder; import lombok.Getter; import lombok.ToString; @@ -11,7 +12,7 @@ @ToString @Builder public class RejectReasonModuleHashAlreadyExists extends RejectReason { - private final String moduleRef; + private final ModuleRef moduleRef; @Override public RejectReasonType getType() { return RejectReasonType.MODULE_HASH_ALREADY_EXISTS; diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonRejectedReceive.java b/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonRejectedReceive.java index edbdbaa4b..ca2079362 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonRejectedReceive.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/responses/transactionstatus/RejectReasonRejectedReceive.java @@ -1,5 +1,7 @@ package com.concordium.sdk.responses.transactionstatus; +import com.concordium.sdk.transactions.Parameter; +import com.concordium.sdk.transactions.ReceiveName; import com.concordium.sdk.types.ContractAddress; import lombok.Builder; import lombok.Getter; @@ -23,11 +25,11 @@ public class RejectReasonRejectedReceive extends RejectReason { /** * The name of the receive method. */ - private final String receiveName; + private final ReceiveName receiveName; /** - * The parameter the it was called with. + * The parameter the contract was called with. */ - private final String parameter; + private final Parameter parameter; @Override public RejectReasonType getType() { diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/transactions/AccountTransaction.java b/concordium-sdk/src/main/java/com/concordium/sdk/transactions/AccountTransaction.java index 623f60c87..19e8e293c 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/transactions/AccountTransaction.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/transactions/AccountTransaction.java @@ -123,6 +123,9 @@ public static AccountTransaction fromBytes(ByteBuffer source) { byte tag = source.get(); Payload payload; switch (tag) { + case 2: + payload = UpdateContract.fromBytes(source); + break; case 3: payload = Transfer.fromBytes(source); break; @@ -133,7 +136,7 @@ public static AccountTransaction fromBytes(ByteBuffer source) { payload = TransferWithMemo.fromBytes(source); break; default: - throw new UnsupportedOperationException("Only transfers and transfers with memo are currently supported."); + throw new UnsupportedOperationException("Unsupported transaction type: " + tag); } return new AccountTransaction(signature, header, payload); diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/transactions/Parameter.java b/concordium-sdk/src/main/java/com/concordium/sdk/transactions/Parameter.java index 85e3db23a..e23922a18 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/transactions/Parameter.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/transactions/Parameter.java @@ -1,5 +1,6 @@ package com.concordium.sdk.transactions; +import com.concordium.sdk.requests.smartcontracts.InvokeInstanceRequest; import com.concordium.sdk.responses.ProtocolVersion; import com.concordium.sdk.transactions.smartcontracts.SchemaParameter; import com.concordium.sdk.types.UInt16; @@ -13,14 +14,12 @@ /** - * The parameters are used for updating the smart contract instance. - * i.e. calling a "receive" function exposed in the smart contract with the parameters. - * Buffer of the parameters message. + * The parameters are used for calling a smart contract instance. + * i.e. calling a "init" or "receive" function exposed in the smart contract with the parameters. + * This object retains the raw parameters that are sent to the contract. * For protocol versions below {@link ProtocolVersion#V5} the size is limited to 1kb. * From protocol version {@link ProtocolVersion#V5} and onwards the size is limited to be 64kb. */ - -@Getter @ToString @EqualsAndHashCode public final class Parameter { @@ -28,7 +27,6 @@ public final class Parameter { public static final Parameter EMPTY = Parameter.from(new byte[0]); private final byte[] bytes; - @JsonCreator Parameter(byte[] bytes) { this.bytes = bytes; } @@ -41,8 +39,15 @@ public static Parameter from(byte[] parameter) { return new Parameter(parameter); } + public static Parameter from(com.concordium.grpc.v2.Parameter parameter) { + return Parameter.from(parameter.getValue().toByteArray()); + } + /** - * @return buffer bytes of {@link Parameter}. + * Get the serialized parameter, namely the length + * of the parameter (encoded via 2 bytes, big endian) and concatenated with the + * actual parameter bytes. + * @return the serialized parameter */ public byte[] getBytes() { val paramBuffer = this.bytes; @@ -52,6 +57,18 @@ public byte[] getBytes() { return buffer.array(); } + /** + * Get the parameter bytes for contract invocation i.e. off-chain operation. + * This differs from {@link Parameter#getBytes()} as this does not prepend the + * returned byte array with the length of the parameters. + * + * This function should be only be used for {@link com.concordium.sdk.ClientV2#invokeInstance(InvokeInstanceRequest)} calls. + * @return the parameters + */ + public byte[] getBytesForContractInvocation() { + return this.bytes; + } + /** * Constructs a {@link Parameter} from the provided {@link SchemaParameter}. * Provided {@link SchemaParameter} must be initialized using {@link SchemaParameter#initialize()} beforehand. diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/transactions/ReceiveName.java b/concordium-sdk/src/main/java/com/concordium/sdk/transactions/ReceiveName.java index 292a9c868..e362f8a81 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/transactions/ReceiveName.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/transactions/ReceiveName.java @@ -7,6 +7,7 @@ import lombok.val; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; /** @@ -44,6 +45,16 @@ public static ReceiveName from(String contractName, String method) { return new ReceiveName(contractName, method); } + /** + * Create a {@link ReceiveName} from a {@link InitName} + * @param initName the {@link InitName} + * @param endpoint name of the receive endpoint + * @return a {@link ReceiveName} + */ + public static ReceiveName from(InitName initName, String endpoint) { + return new ReceiveName(initName.getName().split("init_")[1], endpoint); + } + public static ReceiveName parse(final String value) { val parts = value.split("\\."); if (parts.length != 2) { @@ -53,6 +64,10 @@ public static ReceiveName parse(final String value) { return from(parts[0], parts[1]); } + public static ReceiveName from(com.concordium.grpc.v2.ReceiveName receiveName) { + return ReceiveName.parse(receiveName.getValue()); + } + public byte[] getBytes() { val receiveNameBuffer = (contractName + "." + method).getBytes(); val buffer = ByteBuffer.allocate(UInt16.BYTES + receiveNameBuffer.length); @@ -60,4 +75,13 @@ public byte[] getBytes() { buffer.put(receiveNameBuffer); return buffer.array(); } + + public static ReceiveName from(ByteBuffer buffer) { + val length = UInt16.fromBytes(buffer); + val nameBuffer = new byte[length.getValue()]; + buffer.get(nameBuffer); + val receiveName = new String(nameBuffer, StandardCharsets.UTF_8); + val split = receiveName.split("\\."); + return new ReceiveName(split[0], split[1]); + } } diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/transactions/UpdateContract.java b/concordium-sdk/src/main/java/com/concordium/sdk/transactions/UpdateContract.java index 9cd6783e7..ecf25fba9 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/transactions/UpdateContract.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/transactions/UpdateContract.java @@ -68,8 +68,8 @@ public static UpdateContract from(final long amount, * * @param amount The amount of CCD to be sent to the contract. * @param contractAddress Address of the contract instance to invoke. - * @param receiveName The {@link ReceiveName} of the smart contract instance to invoke. - * @param param The parameter of the contract method. + * @param receiveName The {@link ReceiveName} of the smart contract instance to invoke. + * @param param The parameter of the contract method. * @return A new UpdateContractPayload object. */ public static UpdateContract from(@NonNull final CCDAmount amount, @@ -112,6 +112,7 @@ public static UpdateContract from(CCDAmount amount, return from(amount, contractAddress, schemaParameter.getReceiveName(), Parameter.from(schemaParameter)); } + @Override public TransactionType getTransactionType() { return TransactionType.UPDATE_SMART_CONTRACT_INSTANCE; @@ -135,4 +136,13 @@ public byte[] getRawPayloadBytes() { return buffer.array(); } + + public static Payload fromBytes(ByteBuffer source) { + val amount = CCDAmount.fromBytes(source); + val contractAddress = ContractAddress.from(source); + val receiveName = ReceiveName.from(source); + byte[] parameterBuffer = new byte[source.remaining()]; + source.get(parameterBuffer); + return UpdateContract.from(amount, contractAddress, receiveName, Parameter.from(parameterBuffer)); + } } diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/types/AbstractAddress.java b/concordium-sdk/src/main/java/com/concordium/sdk/types/AbstractAddress.java index 767b245fa..8c010d7e0 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/types/AbstractAddress.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/types/AbstractAddress.java @@ -8,10 +8,7 @@ import com.fasterxml.jackson.databind.SerializerProvider; import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.val; - import java.io.IOException; -import java.util.Map; /** * An abstract Address. diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/types/ContractAddress.java b/concordium-sdk/src/main/java/com/concordium/sdk/types/ContractAddress.java index 8321c7670..29e7e66dd 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/types/ContractAddress.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/types/ContractAddress.java @@ -39,6 +39,12 @@ public static ContractAddress from(com.concordium.grpc.v2.ContractAddress addres return ContractAddress.from(address.getIndex(), address.getSubindex()); } + public static ContractAddress from(ByteBuffer source) { + long contractIndex = source.getLong(); + long contractSubIndex = source.getLong(); + return new ContractAddress(contractSubIndex, contractIndex); + } + public String toJson() { try { return JsonMapper.INSTANCE.writeValueAsString(this); diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/types/UInt16.java b/concordium-sdk/src/main/java/com/concordium/sdk/types/UInt16.java index bc607f83e..cf7625ef7 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/types/UInt16.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/types/UInt16.java @@ -19,7 +19,9 @@ private UInt16(int value) { this.value = value; } - //Big endian + /** + * Get the value serialized as big endian. + */ public byte[] getBytes() { val bytes = new byte[2]; bytes[0] = (byte) ((value >> 8) & 0xff); @@ -27,6 +29,16 @@ public byte[] getBytes() { return bytes; } + /** + * Get the value serialized as little endian. + */ + public byte[] getBytesLittleEndian() { + val bytes = new byte[2]; + bytes[0] = (byte) (value & 0xff); + bytes[1] = (byte) ((value >> 8) & 0xff); + return bytes; + } + public static UInt16 from(String value) { return UInt16.from(Integer.parseUnsignedInt(value)); } diff --git a/concordium-sdk/src/test/java/com/concordium/sdk/cis2/Cis2SerializationTest.java b/concordium-sdk/src/test/java/com/concordium/sdk/cis2/Cis2SerializationTest.java new file mode 100644 index 000000000..137b01d77 --- /dev/null +++ b/concordium-sdk/src/test/java/com/concordium/sdk/cis2/Cis2SerializationTest.java @@ -0,0 +1,115 @@ +package com.concordium.sdk.cis2; + +import com.concordium.sdk.cis2.events.*; +import com.concordium.sdk.transactions.Parameter; +import com.concordium.sdk.types.AbstractAddress; +import com.concordium.sdk.types.AccountAddress; +import com.concordium.sdk.types.UInt16; +import lombok.SneakyThrows; +import lombok.val; +import org.apache.commons.codec.binary.Hex; +import org.bouncycastle.util.Arrays; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; + +import static org.junit.Assert.*; + +public class Cis2SerializationTest { + + @SneakyThrows + @Test + public void testDeserializeTransferEvent() { + val event = Hex.decodeHex("ff00d0ff9f05009e15fc57bbe167411d4d9c0686e31e8e937d751625972f7c566de4a97f650dc500fd3dd07c83e42461554cf0dd90d73c1ff04531fc2b9c90b9762df8793319e48d"); + val cis2Event = SerializationUtils.deserializeCis2Event(event); + assertEquals(Cis2Event.Type.TRANSFER, cis2Event.getType()); + val transferEvent = (TransferEvent) cis2Event; + assertEquals(TokenId.min(), transferEvent.getTokenId()); + assertEquals(TokenAmount.from(11010000), transferEvent.getTokenAmount()); + assertEquals(AccountAddress.from("49NGYqmPtbuCkXSQt7298mL6Xp52UpSR4U2jVzJjKW9P3b3whw"), transferEvent.getFrom()); + assertEquals(AccountAddress.from("4sGtbuGKgakv5pKSMsy3CEQbW3sn2PbTzTVLZLA6zxX5bB3C5a"), transferEvent.getTo()); + } + + @SneakyThrows + @Test + public void testDeserializeMintEvent() { + val event = Hex.decodeHex("fe00c0843d0097567a23128fb54e3f9fa2f0236f7468ed3c61c4f66a92907d4162f5742ec3ca"); + val cis2Event = SerializationUtils.deserializeCis2Event(event); + assertEquals(Cis2Event.Type.MINT, cis2Event.getType()); + val mintEvent = (MintEvent) cis2Event; + assertEquals(TokenId.min(), mintEvent.getTokenId()); + assertEquals(TokenAmount.from(1000000), mintEvent.getTokenAmount()); + assertEquals(AccountAddress.from("46Pu3wVfURgihzAXoDxMxWucyFo5irXvaEmacNgeK7i49MKyiD"), mintEvent.getOwner()); + } + + @SneakyThrows + @Test + public void testDeserializeBurnEvent() { + val event = Hex.decodeHex("fd00c0843d0097567a23128fb54e3f9fa2f0236f7468ed3c61c4f66a92907d4162f5742ec3ca"); + val cis2Event = SerializationUtils.deserializeCis2Event(event); + assertEquals(Cis2Event.Type.BURN, cis2Event.getType()); + val burnEvent = (BurnEvent) cis2Event; + assertEquals(TokenId.min(), burnEvent.getTokenId()); + assertEquals(TokenAmount.from(1000000), burnEvent.getTokenAmount()); + assertEquals(AccountAddress.from("46Pu3wVfURgihzAXoDxMxWucyFo5irXvaEmacNgeK7i49MKyiD"), burnEvent.getOwner()); + } + + @SneakyThrows + @Test + public void testDeserializeUpdateOperatorOfEvent() { + val event = Hex.decodeHex("fc0100023302aff95a61564321285047ebe9eb4dbc5a17b7ae6da94f66d91eaa062c3a0081f65db5aab2469518cadb87d4f3d8f3b01773dfe0da103353a80ad8118e393e"); + val cis2Event = SerializationUtils.deserializeCis2Event(event); + assertEquals(Cis2Event.Type.UPDATE_OPERATOR_OF, cis2Event.getType()); + val updateOperatorOfEvent = (UpdateOperatorEvent) cis2Event; + assertEquals(AccountAddress.from("2xiMWLvvDjfsYoVHTVJimq68u3qhsMwHbf7QK7iRTYTBiKkJgw"), updateOperatorOfEvent.getOwner()); + assertEquals(AccountAddress.from("3vytfBE9RMZz57dhzKsRuniqeErBC8z4N52ef4LYFFib6XvA1E"), updateOperatorOfEvent.getOperator()); + assertTrue(updateOperatorOfEvent.isOperator()); + } + + @SneakyThrows + @Test + public void testSerializeTransfer() { + // this is a raw transfer parameter i.e. the length of the actual parameter is not included. + val expectedParameter = Hex.decodeHex("010000a995a405009e15fc57bbe167411d4d9c0686e31e8e937d751625972f7c566de4a97f650dc500fd3dd07c83e42461554cf0dd90d73c1ff04531fc2b9c90b9762df8793319e48d0000"); + val transfers = new ArrayList(); + transfers.add(new Cis2Transfer(TokenId.min(), TokenAmount.from(11078313), AccountAddress.from("49NGYqmPtbuCkXSQt7298mL6Xp52UpSR4U2jVzJjKW9P3b3whw"), AccountAddress.from("4sGtbuGKgakv5pKSMsy3CEQbW3sn2PbTzTVLZLA6zxX5bB3C5a"), null)); + Parameter parameter = SerializationUtils.serializeTransfers(transfers); + byte[] lengthBytes = UInt16.from(expectedParameter.length).getBytes(); + assertArrayEquals(Arrays.concatenate(lengthBytes, expectedParameter), parameter.getBytes()); + } + + @SneakyThrows + @Test + public void testSerializeTransferWithAdditionalData() { + // this is a raw transfer parameter i.e. the length of the actual parameter is not included. + val expectedParameter = Hex.decodeHex("010000a995a405009e15fc57bbe167411d4d9c0686e31e8e937d751625972f7c566de4a97f650dc500fd3dd07c83e42461554cf0dd90d73c1ff04531fc2b9c90b9762df8793319e48d010001"); + val transfers = new ArrayList(); + transfers.add(new Cis2Transfer(TokenId.min(), TokenAmount.from("11078313"), AccountAddress.from("49NGYqmPtbuCkXSQt7298mL6Xp52UpSR4U2jVzJjKW9P3b3whw"), AccountAddress.from("4sGtbuGKgakv5pKSMsy3CEQbW3sn2PbTzTVLZLA6zxX5bB3C5a"), new byte[]{1})); + Parameter parameter = SerializationUtils.serializeTransfers(transfers); + byte[] lengthBytes = UInt16.from(expectedParameter.length).getBytes(); + assertArrayEquals(Arrays.concatenate(lengthBytes, expectedParameter), parameter.getBytes()); + } + + @SneakyThrows + @Test + public void testSerializeUpdateOperatorOf() { + // this is a raw update parameter i.e. the length of the actual parameter is not included. + val expectedParameter = Hex.decodeHex("0100010081f65db5aab2469518cadb87d4f3d8f3b01773dfe0da103353a80ad8118e393e"); + val updates = new HashMap(); + updates.put(AccountAddress.from("3vytfBE9RMZz57dhzKsRuniqeErBC8z4N52ef4LYFFib6XvA1E"), true); + Parameter parameter = SerializationUtils.serializeUpdateOperators(updates); + byte[] lengthBytes = UInt16.from(expectedParameter.length).getBytes(); + assertArrayEquals(Arrays.concatenate(lengthBytes, expectedParameter), parameter.getBytes()); + } + + @Test + public void testSerializeTokenId() { + val emptyTokenId = TokenId.min(); + val serializedEmptyTokenId = SerializationUtils.serializeTokenId(emptyTokenId); + assertArrayEquals(new byte[]{0}, serializedEmptyTokenId); + val tokenId = TokenId.from(new byte[]{1, 2, 3, 4}); + val serializedTokenId = SerializationUtils.serializeTokenId(tokenId); + assertArrayEquals(new byte[]{4, 1, 2, 3, 4}, serializedTokenId); + } +} diff --git a/concordium-sdk/src/test/java/com/concordium/sdk/cis2/TokenAmountTest.java b/concordium-sdk/src/test/java/com/concordium/sdk/cis2/TokenAmountTest.java new file mode 100644 index 000000000..b73e58e19 --- /dev/null +++ b/concordium-sdk/src/test/java/com/concordium/sdk/cis2/TokenAmountTest.java @@ -0,0 +1,67 @@ +package com.concordium.sdk.cis2; + +import lombok.SneakyThrows; +import lombok.val; +import org.apache.commons.codec.binary.Hex; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.math.BigInteger; +import java.nio.ByteBuffer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class TokenAmountTest { + @Test + public void testCreateTokenWithMinValue() { + TokenAmount.from("0"); + } + + @Test + public void testCreateTokenWithMaxValue() { + TokenAmount.from("115792089237316195423570985008687907853269984665640564039457584007913129639935"); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateTokenWithNegativeValue() { + TokenAmount.from("-1"); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateTokenWithMoreThanMaxValue() { + TokenAmount.from("115792089237316195423570985008687907853269984665640564039457584007913129639936"); + } + + @Test + public void testSerializeDeserializeMinimumTokenAmount() { + val tokenAmount = TokenAmount.from(0); + val buffer = ByteBuffer.wrap(tokenAmount.encode()); + assertEquals(tokenAmount, TokenAmount.decode(buffer)); + assertFalse(buffer.hasRemaining()); + } + + @Test + public void testSerializeDeserializeMaximumTokenAmount() { + val tokenAmount = TokenAmount.from("115792089237316195423570985008687907853269984665640564039457584007913129639935"); + val buffer = ByteBuffer.wrap(tokenAmount.encode()); + assertEquals(tokenAmount, TokenAmount.decode(buffer)); + assertFalse(buffer.hasRemaining()); + } + + @SneakyThrows + @Test(expected = IllegalArgumentException.class) + public void testDeserializeTokenAmountThatExceeds37Bytes() { + TokenAmount.decode(ByteBuffer.wrap(Hex.decodeHex("8080808080808080808080d2e38b94e9f4d3ca9bc3f0b19393ad8db7ab8ff3efc2e2ef969a01"))); + } + + @SneakyThrows + @Test(expected = IllegalArgumentException.class) + public void testSeserializeTokenAmountThatExceeds37Bytes() { + val invalidTokenAmount = TokenAmount.from(0); + Field amountField = TokenAmount.class.getDeclaredField("amount"); + amountField.setAccessible(true); + amountField.set(invalidTokenAmount, new BigInteger("1115792089237316195423570985008687907853269984665640564039457584007913129639936")); + invalidTokenAmount.encode(); + } +} diff --git a/concordium-sdk/src/test/java/com/concordium/sdk/transactions/ReceiveNameTest.java b/concordium-sdk/src/test/java/com/concordium/sdk/transactions/ReceiveNameTest.java new file mode 100644 index 000000000..fdb267147 --- /dev/null +++ b/concordium-sdk/src/test/java/com/concordium/sdk/transactions/ReceiveNameTest.java @@ -0,0 +1,17 @@ +package com.concordium.sdk.transactions; + +import lombok.val; +import org.junit.Test; + +import java.nio.ByteBuffer; + +import static org.junit.Assert.assertEquals; + +public class ReceiveNameTest { + + @Test + public void testSerializeAndDeserializeReceiveName() { + val receiveName = ReceiveName.from("mycontract", "myfunction"); + assertEquals(receiveName, ReceiveName.from(ByteBuffer.wrap(receiveName.getBytes()))); + } +} diff --git a/concordium-sdk/src/test/java/com/concordium/sdk/transactions/TransactionSerializationTest.java b/concordium-sdk/src/test/java/com/concordium/sdk/transactions/TransactionSerializationTest.java index c6a1a8632..e8153524d 100644 --- a/concordium-sdk/src/test/java/com/concordium/sdk/transactions/TransactionSerializationTest.java +++ b/concordium-sdk/src/test/java/com/concordium/sdk/transactions/TransactionSerializationTest.java @@ -1,15 +1,23 @@ package com.concordium.sdk.transactions; +import com.concordium.sdk.cis2.Cis2Transfer; +import com.concordium.sdk.cis2.SerializationUtils; +import com.concordium.sdk.cis2.TokenAmount; +import com.concordium.sdk.cis2.TokenId; +import com.concordium.sdk.cis2.events.Cis2Event; import com.concordium.sdk.crypto.ed25519.ED25519SecretKey; -import com.concordium.sdk.types.AccountAddress; -import com.concordium.sdk.types.Nonce; -import com.concordium.sdk.types.UInt64; +import com.concordium.sdk.types.*; +import com.google.common.collect.Lists; import lombok.SneakyThrows; +import lombok.val; import org.apache.commons.codec.binary.Hex; +import org.bouncycastle.util.Arrays; import org.junit.Test; +import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; public class TransactionSerializationTest { @@ -36,7 +44,7 @@ public void testSerializeAndDeserializeSimpleTransfer() { @Test @SneakyThrows public void testDeserializeSimpleTransfer() { - BlockItem blockItem = BlockItem.fromVersionedBytes(ByteBuffer.wrap(Hex.decodeHex(biTransfer))); + BlockItem blockItem = BlockItem.fromVersionedBytes(ByteBuffer.wrap(Hex.decodeHex(BI_TRANSFER))); assertEquals(BlockItemType.ACCOUNT_TRANSACTION, blockItem.getBlockItemType()); Payload payload = ((AccountTransaction) blockItem).getPayload(); // check signature @@ -59,7 +67,7 @@ public void testDeserializeSimpleTransfer() { @Test @SneakyThrows public void testDeserializeTransferWithMemo() { - BlockItem blockItem = BlockItem.fromVersionedBytes(ByteBuffer.wrap(Hex.decodeHex(biTransferWithMemo))); + BlockItem blockItem = BlockItem.fromVersionedBytes(ByteBuffer.wrap(Hex.decodeHex(BI_TRANSFER_WITH_MEMO))); assertEquals(BlockItemType.ACCOUNT_TRANSACTION, blockItem.getBlockItemType()); Payload payload = ((AccountTransaction) blockItem).getPayload(); // check signature @@ -80,13 +88,42 @@ public void testDeserializeTransferWithMemo() { assertEquals(Memo.from(new byte[]{100, 116, 101, 115, 116}), memoTransfer.getPayload().getMemo()); } + @SneakyThrows + @Test + public void testDeserializeContractUpdate() { + BlockItem blockItem = BlockItem.fromVersionedBytes(ByteBuffer.wrap(Hex.decodeHex(BI_CONTRACT_UPDATE))); + assertEquals(BlockItemType.ACCOUNT_TRANSACTION, blockItem.getBlockItemType()); + Payload payload = ((AccountTransaction) blockItem).getPayload(); + // check signature + TransactionSignature signature = ((AccountTransaction) blockItem).getSignature(); + assertEquals(1, signature.getSignatures().size()); + assertEquals(Signature.from(Hex.decodeHex("a3e911353ae8558d42b149683c5f96c5bd2fe7a7b03f6ad879f9fe7c12f95b6bd51fba0fd305d680a37a0555c9d281e3ba5c5091f6584e784f7bb1c73855b608")), signature.getSignatures().get(Index.from(0)).getSignatures().get(Index.from(0))); + // check header + TransactionHeader header = ((AccountTransaction) blockItem).getHeader(); + assertEquals(AccountAddress.from("49NGYqmPtbuCkXSQt7298mL6Xp52UpSR4U2jVzJjKW9P3b3whw"), header.getSender()); + assertEquals(Nonce.from(21), header.getNonce()); + assertEquals(UInt64.from(1706724577), header.getExpiry()); + // check payload + TransactionType transactionType = payload.getTransactionType(); + assertEquals(TransactionType.UPDATE_SMART_CONTRACT_INSTANCE, transactionType); + UpdateContract updateContract = (UpdateContract) payload; + assertEquals(CCDAmount.from(0), updateContract.getAmount()); + assertEquals(ContractAddress.from(9390, 0), updateContract.getContractAddress()); + assertEquals(ReceiveName.from("euroe_stablecoin", "transfer"), updateContract.getReceiveName()); + Cis2Transfer expectedParameters = new Cis2Transfer(TokenId.from(new byte[0]), TokenAmount.from(11078313), AccountAddress.from("49NGYqmPtbuCkXSQt7298mL6Xp52UpSR4U2jVzJjKW9P3b3whw"), AccountAddress.from("4sGtbuGKgakv5pKSMsy3CEQbW3sn2PbTzTVLZLA6zxX5bB3C5a"), null); + byte[] expectedParams = updateContract.getParam().getBytes(); + byte[] cis2TransferParams = SerializationUtils.serializeTransfers(Lists.newArrayList(expectedParameters)).getBytes(); + assertArrayEquals(expectedParams, Arrays.concatenate(UInt16.from(cis2TransferParams.length).getBytes(), cis2TransferParams)); + } + // a hex encoded normal transfer - private static final String biTransfer = "0000010001000040afb0dbb92c8fdc1eb16959c3eb8b6e9a69e5634c0ff4da2966b79dbdca0f69576c5b0f401175bc18ce7d296b926fddcd48a3bbcb74fde13d65800712992f05023319809bed082478e4762c135db732d058b66ff3ac374ece15f484ad3ac95039000000000000000100000000000001f5000000290000000064c910f003015e8d5ce9525d4630f7fbdd58dacebefb64b964b241da3f921521d937b49ba80000000000000000"; + private static final String BI_TRANSFER = "0000010001000040afb0dbb92c8fdc1eb16959c3eb8b6e9a69e5634c0ff4da2966b79dbdca0f69576c5b0f401175bc18ce7d296b926fddcd48a3bbcb74fde13d65800712992f05023319809bed082478e4762c135db732d058b66ff3ac374ece15f484ad3ac95039000000000000000100000000000001f5000000290000000064c910f003015e8d5ce9525d4630f7fbdd58dacebefb64b964b241da3f921521d937b49ba80000000000000000"; // a hex encoded transfer with memo - private static final String biTransferWithMemo = "000001000100004052545c8418bf3228469cde4aa33ed8bb646aaffc6595c7bfcc1c0a4f353c1b1b874f5bd54a4e953c4a38e6529ae2e1b9a44597505d0676b8f7d15741b62fba00d6cea90316c56d994abd205876e423c6f6c9488e2d7c4f0b044539de501f1174000000000000000500000000000001fc000000300000000064ca278b16393d88b218e44301cceb22bd64184edba56996498523710ebb43b4cf430db89700056474657374000000000bebc200"; - + private static final String BI_TRANSFER_WITH_MEMO = "000001000100004052545c8418bf3228469cde4aa33ed8bb646aaffc6595c7bfcc1c0a4f353c1b1b874f5bd54a4e953c4a38e6529ae2e1b9a44597505d0676b8f7d15741b62fba00d6cea90316c56d994abd205876e423c6f6c9488e2d7c4f0b044539de501f1174000000000000000500000000000001fc000000300000000064ca278b16393d88b218e44301cceb22bd64184edba56996498523710ebb43b4cf430db89700056474657374000000000bebc200"; + + private static final String BI_CONTRACT_UPDATE = "0000010001000040a3e911353ae8558d42b149683c5f96c5bd2fe7a7b03f6ad879f9fe7c12f95b6bd51fba0fd305d680a37a0555c9d281e3ba5c5091f6584e784f7bb1c73855b6089e15fc57bbe167411d4d9c0686e31e8e937d751625972f7c566de4a97f650dc500000000000000150000000000002831000000810000000065ba8ce102000000000000000000000000000024ae000000000000000000196575726f655f737461626c65636f696e2e7472616e73666572004b010000a995a405009e15fc57bbe167411d4d9c0686e31e8e937d751625972f7c566de4a97f650dc500fd3dd07c83e42461554cf0dd90d73c1ff04531fc2b9c90b9762df8793319e48d0000"; private static final BlockItem bi = Transfer.createNew( AccountAddress.from("3hYXYEPuGyhFcVRhSk2cVgKBhzVcAryjPskYk4SecpwGnoHhuM"), CCDAmount.fromMicro(17)) diff --git a/concordium-sdk/src/test/java/com/concordium/sdk/types/ContractAddressTest.java b/concordium-sdk/src/test/java/com/concordium/sdk/types/ContractAddressTest.java new file mode 100644 index 000000000..a6fed525f --- /dev/null +++ b/concordium-sdk/src/test/java/com/concordium/sdk/types/ContractAddressTest.java @@ -0,0 +1,17 @@ +package com.concordium.sdk.types; + +import lombok.val; +import org.junit.Test; + +import java.nio.ByteBuffer; + +import static org.junit.Assert.assertEquals; + +public class ContractAddressTest { + + @Test + public void testSerializeDeserializeContractAddress() { + val contractAddress = ContractAddress.from(1, 0); + assertEquals(contractAddress, ContractAddress.from(ByteBuffer.wrap(contractAddress.getBytes()))); + } +}