Skip to content

Commit

Permalink
Cis2 (#304)
Browse files Browse the repository at this point in the history
MilkywayPirate authored Feb 14, 2024
1 parent 47576a4 commit 20c837d
Showing 50 changed files with 2,119 additions and 180 deletions.
25 changes: 16 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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<PendingChange>`
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<PendingChange>`
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.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -51,5 +51,6 @@ android:


clean:
cd $(PATH_CRYPTO) && cargo clean
rm -rf $(PATH_JAVA_NATIVE_RESOURCES)*
rm -rf $(PATH_ANDROID_NATIVE_RESOURCES)*
5 changes: 5 additions & 0 deletions concordium-android-sdk/pom.xml
Original file line number Diff line number Diff line change
@@ -83,6 +83,11 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
</dependencies>

<profiles>
Original file line number Diff line number Diff line change
@@ -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<Integer> {

@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);
}
}
5 changes: 5 additions & 0 deletions concordium-sdk/pom.xml
Original file line number Diff line number Diff line change
@@ -107,6 +107,11 @@
<version>3.3.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
</dependencies>

<profiles>
57 changes: 31 additions & 26 deletions concordium-sdk/src/main/java/com/concordium/sdk/ClientV2.java
Original file line number Diff line number Diff line change
@@ -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<BlockIdentifier> getBlocks(int timeoutMillis) {
*
* @param timeoutMillis Timeout for the request in Milliseconds.
* @return {@link Iterator<BlockIdentifier>}
*
*/
public Iterator<BlockIdentifier> 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}: <ul>
* <li> {@link io.grpc.Status#NOT_FOUND} if the transaction is not known to the node.
* </ul>
* <li> {@link io.grpc.Status#NOT_FOUND} if the transaction is not known to the node.
* </ul>
*/
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}:
* <ul><li>{@link io.grpc.Status.Code#UNIMPLEMENTED} if the protocol does not support the endpoint.</ul>
* <ul><li>{@link io.grpc.Status.Code#UNIMPLEMENTED} if the protocol does not support the endpoint.</ul>
*/
public ImmutableList<BakerRewardPeriodInfo> getBakersRewardPeriod(BlockQuery input) {
val response = this.server().getBakersRewardPeriod(to(input));
@@ -824,9 +825,9 @@ public ImmutableList<BakerRewardPeriodInfo> 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}:<ul>
* <li>{@link io.grpc.Status.Code#UNIMPLEMENTED} if the endpoint is not enabled by the node.
* <li>{@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.
* </ul>
* <li>{@link io.grpc.Status.Code#UNIMPLEMENTED} if the endpoint is not enabled by the node.
* <li>{@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.
* </ul>
*/
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. <p>
*
* <p>
* If the baker is not a baker for the current reward period, this returns a timestamp at the
* start of the next reward period. <p>
* 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. <p>
* 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}:
* <ul><li>{@link io.grpc.Status.Code#UNIMPLEMENTED} if the current consensus version is 0, as the endpoint is only supported by consensus version 1.</ul>
* <ul><li>{@link io.grpc.Status.Code#UNIMPLEMENTED} if the current consensus version is 0, as the endpoint is only supported by consensus version 1.</ul>
*/
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}: <ul>
* <li> {@link io.grpc.Status#NOT_FOUND} if the query specifies an unknown block.
* <li> {@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.
* <li> {@link io.grpc.Status#INVALID_ARGUMENT} if the query is for an epoch with no finalized blocks for a past genesis index.
* <li> {@link io.grpc.Status#INVALID_ARGUMENT} if the input {@link EpochQuery} is malformed.
* <li> {@link io.grpc.Status#UNIMPLEMENTED} if the endpoint is disabled on the node.
* </ul>
* <li> {@link io.grpc.Status#NOT_FOUND} if the query specifies an unknown block.
* <li> {@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.
* <li> {@link io.grpc.Status#INVALID_ARGUMENT} if the query is for an epoch with no finalized blocks for a past genesis index.
* <li> {@link io.grpc.Status#INVALID_ARGUMENT} if the input {@link EpochQuery} is malformed.
* <li> {@link io.grpc.Status#UNIMPLEMENTED} if the endpoint is disabled on the node.
* </ul>
*/
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). <p>
* 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. <p>
* 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}: <ul>
* <li> {@link io.grpc.Status#NOT_FOUND} if the query specifies an unknown block.
* <li> {@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.
* <li> {@link io.grpc.Status#INVALID_ARGUMENT} if the query is for an epoch that is not finalized for a past genesis index.
* <li> {@link io.grpc.Status#INVALID_ARGUMENT} if the query is for a genesis index at consensus version 0.
* <li> {@link io.grpc.Status#INVALID_ARGUMENT} if the input {@link EpochQuery} is malformed.
* <li> {@link io.grpc.Status#UNIMPLEMENTED} if the endpoint is disabled on the node.
* </ul>
* <li> {@link io.grpc.Status#NOT_FOUND} if the query specifies an unknown block.
* <li> {@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.
* <li> {@link io.grpc.Status#INVALID_ARGUMENT} if the query is for an epoch that is not finalized for a past genesis index.
* <li> {@link io.grpc.Status#INVALID_ARGUMENT} if the query is for a genesis index at consensus version 0.
* <li> {@link io.grpc.Status#INVALID_ARGUMENT} if the input {@link EpochQuery} is malformed.
* <li> {@link io.grpc.Status#UNIMPLEMENTED} if the endpoint is disabled on the node.
* </ul>
*/
public ImmutableList<WinningBaker> getWinningBakersEpoch(EpochQuery epochQuery) {
val res = this.server().getWinningBakersEpoch(to(epochQuery));
@@ -904,8 +907,9 @@ public ImmutableList<WinningBaker> getWinningBakersEpoch(EpochQuery epochQuery)
* Waits until a given transaction is finalized and returns the corresponding {@link Optional<FinalizedBlockItem>}.
* 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<FinalizedBlockItem>} 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<FinalizedBlockItem>} of the transaction if it was finalized before exceeding the timeout, Empty otherwise.
*/
public Optional<FinalizedBlockItem> waitUntilFinalized(Hash transactionHash, int timeoutMillis) {
@@ -939,6 +943,7 @@ public Optional<FinalizedBlockItem> waitUntilFinalized(Hash transactionHash, int

/**
* Helper function for {@link ClientV2#waitUntilFinalized(Hash, int)}. Retrieves the {@link Optional<FinalizedBlockItem>} of the transaction if it is finalized.
*
* @param transactionHash the {@link Hash} of the transaction to wait for.
* @return {@link Optional<FinalizedBlockItem>} of the transaction if it is finalized, Empty otherwise.
*/
Original file line number Diff line number Diff line change
@@ -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();
}

Original file line number Diff line number Diff line change
@@ -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 <a href="https://proposals.concordium.software/CIS/cis-2.html#balanceof">here</a> 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;
}

}
243 changes: 243 additions & 0 deletions concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2Client.java
Original file line number Diff line number Diff line change
@@ -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 <a href="https://proposals.concordium.software/CIS/cis-2.html">specification</a>.
*/
@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<AbstractAddress, Boolean> 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<BalanceQuery, TokenAmount> 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<BalanceQuery, TokenAmount>();
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<OperatorQuery, Boolean> 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<OperatorQuery, Boolean>();
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<TokenId, TokenMetadata> 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<TokenId, TokenMetadata>();
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<Cis2EventWithMetadata> 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<BlockQuery>() {
@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<Cis2EventWithMetadata> 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<Cis2EventWithMetadata> 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<Cis2EventWithMetadata>();
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<Cis2EventWithMetadata> getEventsFor(BlockQuery blockQuery) {
return new Cis2EventIterator(this, Lists.newArrayList(blockQuery).iterator());
}


}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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<Cis2EventWithMetadata> {

private final Cis2Client client;

private final Iterator<BlockQuery> queries;

private final Queue<Cis2EventWithMetadata> buffer = new LinkedList<>();

Cis2EventIterator(Cis2Client client, Iterator<BlockQuery> 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<Cis2EventWithMetadata> extractCis2Events(BlockQuery blockQuery, Summary summary) {
val accumulator = new ArrayList<Cis2EventWithMetadata>();
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<Cis2EventWithMetadata> getSuccessEvents(AccountTransactionDetails details, BlockQuery blockQuery, Hash transactionHash) {
val accumulator = new ArrayList<Cis2EventWithMetadata>();
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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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 <a href="https://proposals.concordium.software/CIS/cis-2.html#balanceof">here</a> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<Cis2Transfer> 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<BalanceQuery> 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<OperatorQuery> 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<TokenId> 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<AbstractAddress, Boolean> 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");
}
}
Original file line number Diff line number Diff line change
@@ -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
* <p>
* 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);
}


}
98 changes: 98 additions & 0 deletions concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenId.java
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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<byte[]> getChecksum() {
return Optional.ofNullable(this.checksum);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -5,13 +5,15 @@
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;

import javax.annotation.Nullable;

/**
* Type of Block to query an API with. For a list of types see {@link BlockQueryType}
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@ToString
public class BlockQuery {

/**
Original file line number Diff line number Diff line change
@@ -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> 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> 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> 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> 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> energy) {
if (!(schemaParameter.getType() == ParameterType.RECEIVE)) {
throw new IllegalArgumentException("Cannot initialize smart contract with InvokeInstance. SchemaParameter for InvokeInstanceRequest must be initialized with a ReceiveName");
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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() {
Original file line number Diff line number Diff line change
@@ -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() {
Original file line number Diff line number Diff line change
@@ -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() {
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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() {
Original file line number Diff line number Diff line change
@@ -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() {
Original file line number Diff line number Diff line change
@@ -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() {
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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() {
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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,22 +14,19 @@


/**
* 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 {
public static final int MAX_SIZE = 65535;
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.
Original file line number Diff line number Diff line change
@@ -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,11 +64,24 @@ 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);
buffer.put(UInt16.from(receiveNameBuffer.length).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]);
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -19,14 +19,26 @@ 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);
bytes[1] = (byte) (value & 0xff);
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));
}
Original file line number Diff line number Diff line change
@@ -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<Cis2Transfer>();
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<Cis2Transfer>();
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<AbstractAddress, Boolean>();
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);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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())));
}
}
Original file line number Diff line number Diff line change
@@ -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))
Original file line number Diff line number Diff line change
@@ -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())));
}
}

0 comments on commit 20c837d

Please sign in to comment.