Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cis2 #304

Merged
merged 39 commits into from
Feb 14, 2024
Merged

Cis2 #304

Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
4635878
cis2 balanceOf query
MilkywayPirate Jan 30, 2024
4ed2305
cis2 queries.
Jan 31, 2024
b24c0bb
transfer and updateOperator
Feb 1, 2024
1f4a1f4
CIS2 tests and API for tracing events.
MilkywayPirate Feb 2, 2024
9b959ad
Merge branch 'main' into cis2
MilkywayPirate Feb 2, 2024
9c7db08
Test for deserializing a contract update transaction.
MilkywayPirate Feb 2, 2024
dde86ad
CHANGELOG.
MilkywayPirate Feb 2, 2024
4a69ad5
Cis2 documentation.
Feb 5, 2024
6b9a7c8
Merge branch 'main' into cis2
Feb 5, 2024
dee8dff
Fixes.
Feb 5, 2024
2d41df2
cleanup a test.
Feb 5, 2024
779c0b4
Fix serialization of CCDAmount.
Feb 5, 2024
92aee44
Fixed paramter handling for smart contract invocations.
Feb 5, 2024
854f0a2
remove unused code.
Feb 5, 2024
fbb78b8
cleanup getEventsFor.
Feb 5, 2024
b7f15be
.
Feb 5, 2024
5c4aaee
Tests and fixes to tokenid.
Feb 6, 2024
e17a40a
More tests for cis2 serialization/deserialization.
Feb 6, 2024
9ab97d8
Parse reject reasons into AccountTransactionDetails and into CIS2Errors.
Feb 6, 2024
f8fd61e
lambda.
Feb 6, 2024
6b82a9c
rework some error handling for requesting events for non finalized tr…
Feb 6, 2024
3e6b671
cleanup.
Feb 6, 2024
e5b0278
fix tests.
Feb 6, 2024
07aec7a
Merge branch 'main' into cis2
MilkywayPirate Feb 6, 2024
2cd882a
Introduce a wrapper type `TokenId` instead of just `byte[]`.
MilkywayPirate Feb 7, 2024
26cdb09
cleanup tests.
MilkywayPirate Feb 7, 2024
f3496a2
Some extra helper functions for generating tokenids via different
MilkywayPirate Feb 7, 2024
d936433
Make the tokenid more consistent with the rust versions.
MilkywayPirate Feb 7, 2024
4d8a9f4
Address review comments.
MilkywayPirate Feb 8, 2024
870d6b1
Add streaming API for tracing CIS2 event.
MilkywayPirate Feb 8, 2024
becfde4
Update android pom with dependency.
MilkywayPirate Feb 8, 2024
183d41a
snapshot version and add cis2 example.
MilkywayPirate Feb 8, 2024
e6a35fe
Merge branch 'main' into cis2
Feb 12, 2024
e3024b6
fix merge.
Feb 12, 2024
7e826a9
fix merge hiccups.
Feb 12, 2024
975c282
Update CHANGELOG.md
MilkywayPirate Feb 14, 2024
0fa6f30
Update CHANGELOG.md
MilkywayPirate Feb 14, 2024
f62f349
Update concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2Error…
MilkywayPirate Feb 14, 2024
3beccba
Handle review comments.
MilkywayPirate Feb 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# Changelog

# Unreleased changes
- 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.
## Unreleased changes
- 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 wrong format for parameters.
MilkywayPirate marked this conversation as resolved.
Show resolved Hide resolved
- 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.<
- Fix .equals() for AccountInfo such that all fields are used to deduce equality.<

## 6.1.0
- Purge remaining usages of V1 GRPC API.
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,6 @@ android:


clean:
cd $(PATH_CRYPTO) && cargo clean
rm -rf $(PATH_JAVA_NATIVE_RESOURCES)*
rm -rf $(PATH_ANDROID_NATIVE_RESOURCES)*
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
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;
}

}
276 changes: 276 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,276 @@
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.AbstractAddress;
import com.concordium.sdk.types.AccountAddress;
import com.concordium.sdk.types.ContractAddress;
import com.concordium.sdk.types.UInt64;
import lombok.val;

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>.
*/
public class Cis2Client {

private final ClientV2 client;
private final ContractAddress contractAddress;
private final InitName contractName;

// Default max energy for contract invocations.
private static final Energy MAX_ENERGY = Energy.from(UInt64.from(3000000));
MilkywayPirate marked this conversation as resolved.
Show resolved Hide resolved

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, Cis2Transfer... transfers) {
val listOfTransfers = Arrays.asList(transfers);
val nextNonce = this.client.getAccountInfo(BlockQuery.LAST_FINAL, AccountQuery.from(sender)).getAccountNonce();
val endpoint = ReceiveName.from(contractName, "transfer");
val parameters = SerializationUtils.serializeTransfers(listOfTransfers);
return this.client.sendTransaction(
TransactionFactory.newUpdateContract()
.maxEnergyCost(UInt64.from(3000))
MilkywayPirate marked this conversation as resolved.
Show resolved Hide resolved
.payload(UpdateContract.from(CCDAmount.from(0), this.contractAddress, endpoint, parameters))
.expiry(Expiry.createNew().addMinutes(5))
.nonce(AccountNonce.from(nextNonce))
.sender(sender)
.signer(signer)
.build());
}

/**
* Update which addresses that the sender (owner) operates.
MilkywayPirate marked this conversation as resolved.
Show resolved Hide resolved
*
* @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(Map<AbstractAddress, Boolean> operatorUpdates, AccountAddress sender, TransactionSigner signer) {
val nextNonce = this.client.getAccountInfo(BlockQuery.LAST_FINAL, AccountQuery.from(sender)).getAccountNonce();
val endpoint = ReceiveName.from(contractName, "updateOperator");
val parameters = SerializationUtils.serializeUpdateOperators(operatorUpdates);
return this.client.sendTransaction(
TransactionFactory.newUpdateContract()
.maxEnergyCost(UInt64.from(3000))
MilkywayPirate marked this conversation as resolved.
Show resolved Hide resolved
.payload(UpdateContract.from(CCDAmount.from(0), this.contractAddress, endpoint, parameters))
.expiry(Expiry.createNew().addMinutes(5))
.nonce(AccountNonce.from(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, Long> balanceOf(BalanceQuery... queries) {
MilkywayPirate marked this conversation as resolved.
Show resolved Hide resolved
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, MAX_ENERGY));
if (result.getOutcome() == Outcome.REJECT) {
throw new RuntimeException("balanceOf failed: " + result.getRejectReason().toString());
}
val balances = SerializationUtils.deserializeTokenAmounts(result.getReturnValue());
val responses = new HashMap<BalanceQuery, Long>();
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, MAX_ENERGY));
MilkywayPirate marked this conversation as resolved.
Show resolved Hide resolved
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 hex encoded token id.
*/
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, MAX_ENERGY));
if (result.getOutcome() == Outcome.REJECT) {
throw new RuntimeException("operatorOf failed: " + result.getRejectReason().toString());
MilkywayPirate marked this conversation as resolved.
Show resolved Hide resolved
}
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 List<Cis2EventWithMetadata> getEvents(BlockQuery from, BlockQuery to) {
MilkywayPirate marked this conversation as resolved.
Show resolved Hide resolved
long current = this.client.getBlockInfo(from).getBlockHeight().getValue();
val end = this.client.getBlockInfo(to).getBlockHeight().getValue();
if (current >= end) {
throw new IllegalArgumentException("Starting block must be before the end block");
}
MilkywayPirate marked this conversation as resolved.
Show resolved Hide resolved
val accumulator = new ArrayList<Cis2EventWithMetadata>();
while (current <= end) {
val events = getEventsFor(BlockQuery.HEIGHT(BlocksAtHeightRequest.newAbsolute(current)));
accumulator.addAll(events);
current++;
}
return accumulator;
}

/**
* Get any events associated emitted from the specified CIS2 contract.
*
* @param queries blocks to query
* @return the list of events.
*/
public List<Cis2EventWithMetadata> getEventsFor(BlockQuery... queries) {
val accumulator = new ArrayList<Cis2EventWithMetadata>();
for (BlockQuery query : queries) {
accumulator.addAll(getEventsFor(query));
}
return accumulator;
}

/**
* 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 List<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();
extractCis2Events(BlockQuery.HASH(finalizedTransaction.getBlockHash()), accumulator, finalizedTransaction.getSummary());
return accumulator;
}


/**
* 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 List<Cis2EventWithMetadata> getEventsFor(BlockQuery blockQuery) {
val accumulator = new ArrayList<Cis2EventWithMetadata>();
val summaries = this.client.getBlockTransactionEvents(blockQuery);
while (summaries.hasNext()) {
val summary = summaries.next();
extractCis2Events(blockQuery, accumulator, summary);
}
return accumulator;
}

/**
* Extract any events from the CIS2 specified contract.
* The events are added to the supplied accumulator.
* @param blockQuery a block identifier.
* @param accumulator accumulator used for aggregating the events.
* @param summary the transaction summary to extract from.
*/
private void extractCis2Events(BlockQuery blockQuery, ArrayList<Cis2EventWithMetadata> accumulator, Summary summary) {
if (summary.getDetails().getType() == Type.ACCOUNT_TRANSACTION) {
val details = summary.getDetails().getAccountTransactionDetails();
if (details.isSuccessful() && details.getType() == TransactionResultEventType.CONTRACT_UPDATED) {
MilkywayPirate marked this conversation as resolved.
Show resolved Hide resolved
val contractUpdated = details.getContractUpdated();
for (ContractTraceElement contractTraceElement : contractUpdated) {
if (contractTraceElement.getTraceType() == ContractTraceElementType.INSTANCE_UPDATED) {
val updatedEvent = (ContractUpdated) contractTraceElement;
if (this.contractAddress.equals(updatedEvent.getAddress())) {
for (byte[] event : updatedEvent.getEvents()) {
accumulator.add(Cis2EventWithMetadata.ok(SerializationUtils.deserializeCis2Event(event), blockQuery, summary.getTransactionHash()));
}
}
}
}
} else if (!Objects.isNull(details.getRejectReason())) {
accumulator.add(Cis2EventWithMetadata.err(Cis2Error.from(details.getRejectReason()), blockQuery, summary.getTransactionHash()));
}
}
}

}
Loading
Loading