diff --git a/CHANGELOG.md b/CHANGELOG.md index c9ffb0506..990b32178 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## Unreleased changes +- Removed unnecessary `amount` parameter from `InvokeInstanceRequest`. +- Added utility functions for converting between `CCDAmount` and `Energy`. Present in utility class `Converter`. - Fixed a bug in `CustomEvent`. Removed unnecessary `tag` field. - Added `Web3IdProof` class with `getWeb3IdProof` method to create Presentations. (And supporting classes) - Fixed an issue where `ConcordiumHdWallet.fromSeedPhrase` always produced an invalid seed as hex. diff --git a/concordium-sdk-examples/src/main/java/com/concordium/sdk/examples/contractexample/cis2nft/Cis2Nft.java b/concordium-sdk-examples/src/main/java/com/concordium/sdk/examples/contractexample/cis2nft/Cis2Nft.java index 53b62de24..658e21f9b 100644 --- a/concordium-sdk-examples/src/main/java/com/concordium/sdk/examples/contractexample/cis2nft/Cis2Nft.java +++ b/concordium-sdk-examples/src/main/java/com/concordium/sdk/examples/contractexample/cis2nft/Cis2Nft.java @@ -2,22 +2,25 @@ import com.concordium.sdk.ClientV2; import com.concordium.sdk.Connection; +import com.concordium.sdk.CurrencyConverter; import com.concordium.sdk.crypto.ed25519.ED25519SecretKey; import com.concordium.sdk.exceptions.ClientInitializationException; 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.chainparameters.ChainParameters; import com.concordium.sdk.responses.modulelist.ModuleRef; +import com.concordium.sdk.responses.smartcontracts.InvokeInstanceResult; import com.concordium.sdk.transactions.*; import com.concordium.sdk.transactions.smartcontracts.SchemaParameter; -import com.concordium.sdk.types.AccountAddress; -import com.concordium.sdk.types.ContractAddress; -import com.concordium.sdk.types.Nonce; -import com.concordium.sdk.types.UInt64; +import com.concordium.sdk.types.*; import lombok.var; import picocli.CommandLine; import java.io.IOException; +import java.math.BigDecimal; import java.net.URL; import java.util.Optional; import java.util.concurrent.Callable; @@ -130,6 +133,20 @@ private void handleInit(ClientV2 client, Nonce nonce) { } private void handleMethod(ClientV2 client, Nonce nonce, SchemaParameter parameter) { + + // Estimate energy used by transaction. + InvokeInstanceRequest invokeInstanceRequest = InvokeInstanceRequest.from(BlockQuery.LAST_FINAL, + CONTRACT_ADDRESS, + parameter, + Optional.empty()); + InvokeInstanceResult invokeInstanceResult = client.invokeInstance(invokeInstanceRequest); + Energy usedEnergy = invokeInstanceResult.getUsedEnergy(); + ChainParameters parameters = client.getChainParameters(BlockQuery.LAST_FINAL); + // Convert to Euro and CCD using ChainParameters from the best block and utility methods in the Converter class. + BigDecimal euros = CurrencyConverter.energyToEuro(usedEnergy, parameters).asBigDecimal(6); + BigDecimal ccd = CurrencyConverter.energyToMicroCCD(usedEnergy, parameters).asBigDecimal(6); + System.out.println("Price of transaction is: " + usedEnergy + " = " + euros + " euros = " + ccd + " micro CCD"); + UpdateContract payload = UpdateContract.from(CONTRACT_ADDRESS, parameter); UpdateContractTransaction transaction = TransactionFactory.newUpdateContract() .sender(AccountAddress.from(SENDER_ADDRESS)) diff --git a/concordium-sdk-examples/src/main/java/com/concordium/sdk/examples/contractexample/wccd/Cis2WCCD.java b/concordium-sdk-examples/src/main/java/com/concordium/sdk/examples/contractexample/wccd/Cis2WCCD.java index ca39a76cf..7c2b44329 100644 --- a/concordium-sdk-examples/src/main/java/com/concordium/sdk/examples/contractexample/wccd/Cis2WCCD.java +++ b/concordium-sdk-examples/src/main/java/com/concordium/sdk/examples/contractexample/wccd/Cis2WCCD.java @@ -2,11 +2,16 @@ import com.concordium.sdk.ClientV2; import com.concordium.sdk.Connection; +import com.concordium.sdk.CurrencyConverter; import com.concordium.sdk.crypto.ed25519.ED25519SecretKey; 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.chainparameters.ChainParameters; import com.concordium.sdk.responses.modulelist.ModuleRef; +import com.concordium.sdk.responses.smartcontracts.InvokeInstanceResult; import com.concordium.sdk.transactions.*; import com.concordium.sdk.transactions.smartcontracts.SchemaParameter; import com.concordium.sdk.types.AccountAddress; @@ -16,6 +21,7 @@ import lombok.var; import picocli.CommandLine; +import java.math.BigDecimal; import java.net.URL; import java.util.Optional; import java.util.concurrent.Callable; @@ -147,6 +153,19 @@ private void handleInit(ClientV2 client, Nonce nonce) { } private void handleMethod(ClientV2 client, Nonce nonce, SchemaParameter parameter) { + // Estimate energy used by transaction. + InvokeInstanceRequest invokeInstanceRequest = InvokeInstanceRequest.from(BlockQuery.LAST_FINAL, + CONTRACT_ADDRESS, + parameter, + Optional.empty()); + InvokeInstanceResult invokeInstanceResult = client.invokeInstance(invokeInstanceRequest); + Energy usedEnergy = invokeInstanceResult.getUsedEnergy(); + ChainParameters parameters = client.getChainParameters(BlockQuery.LAST_FINAL); + // Convert to Euro and CCD using ChainParameters from the best block and utility methods in the Converter class. + BigDecimal euros = CurrencyConverter.energyToEuro(usedEnergy, parameters).asBigDecimal(6); + BigDecimal ccd = CurrencyConverter.energyToMicroCCD(usedEnergy, parameters).asBigDecimal(6); + System.out.println("Price of transaction is: " + usedEnergy + " = " + euros + " euros = " + ccd + " micro CCD"); + UpdateContract payload = UpdateContract.from(CONTRACT_ADDRESS, parameter); UpdateContractTransaction transaction = TransactionFactory.newUpdateContract() .sender(AccountAddress.from(SENDER_ADDRESS)) diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/ClientV2.java b/concordium-sdk/src/main/java/com/concordium/sdk/ClientV2.java index e149a4682..cdbab1179 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/ClientV2.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/ClientV2.java @@ -791,7 +791,7 @@ public InvokeInstanceResult invokeInstance(InvokeInstanceRequest request) { grpcRequest.setInvoker(to(request.getInvoker())); } grpcRequest.setInstance(to(request.getInstance())) - .setAmount(to(request.getAmount())) + .setAmount(to(CCDAmount.from(0))) .setEntrypoint(to(request.getEntrypoint())) .setParameter(to(request.getParameter())); if (request.getEnergy().isPresent()) { diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/CurrencyConverter.java b/concordium-sdk/src/main/java/com/concordium/sdk/CurrencyConverter.java new file mode 100644 index 000000000..a4f01669a --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/CurrencyConverter.java @@ -0,0 +1,137 @@ +package com.concordium.sdk; + +import com.concordium.sdk.requests.smartcontracts.Energy; +import com.concordium.sdk.responses.chainparameters.ChainParameters; +import com.concordium.sdk.transactions.CCDAmount; +import com.concordium.sdk.types.ConversionResult; +import com.concordium.sdk.types.UInt64; + +import static com.concordium.sdk.types.ConversionResult.*; + +/** + * Utility class for converting between {@link CCDAmount}, {@link Energy} and Euros. + * Represents converted values using {@link ConversionResult} to ensure precision. + */ +public class CurrencyConverter { + + /** + * Converts {@link Energy} to euro using exchange rate from the provided {@link ChainParameters}. + * + * @param energy {@link Energy} to convert. + * @param parameters {@link ChainParameters} with exchange rate used for conversion. + * @return {@link ConversionResult} corresponding to the euro value of {@link Energy}. + */ + public static ConversionResult energyToEuro(Energy energy, ChainParameters parameters) { + ConversionResult energyFraction = from(energy.getValue(), UInt64.from(1)); + return energyToEuro(energyFraction, parameters); + } + + /** + * Converts {@link ConversionResult} representing {@link Energy} to euro using exchange rate from the provided {@link ChainParameters}. + * + * @param energy {@link ConversionResult} representing an amount of {@link Energy} to convert. + * @param parameters {@link ChainParameters} with exchange rate used for conversion. + * @return {@link ConversionResult} corresponding to the euro value of {@link Energy}. + */ + public static ConversionResult energyToEuro(ConversionResult energy, ChainParameters parameters) { + ConversionResult euroPerEnergy = from(parameters.getEuroPerEnergy()); + return energy.mult(euroPerEnergy); + } + + /** + * Converts {@link Energy} to micro CCD using exchange rate from the provided {@link ChainParameters}. + * + * @param energy {@link Energy} to convert. + * @param parameters {@link ChainParameters} with exchange rate used for conversion. + * @return {@link ConversionResult} corresponding to the micro CCD value of {@link Energy}. + */ + public static ConversionResult energyToMicroCCD(Energy energy, ChainParameters parameters) { + ConversionResult energyFraction = from(energy.getValue(), UInt64.from(1)); + return energyToMicroCCD(energyFraction, parameters); + } + + /** + * Converts {@link ConversionResult} representing {@link Energy} to micro CCD using exchange rate from the provided {@link ChainParameters}. + * + * @param energy {@link ConversionResult} representing an amount of {@link Energy} to convert. + * @param parameters {@link ChainParameters} with exchange rate used for conversion. + * @return {@link ConversionResult} corresponding to the micro CCD value of {@link Energy}. + */ + public static ConversionResult energyToMicroCCD(ConversionResult energy, ChainParameters parameters) { + ConversionResult euros = energyToEuro(energy, parameters); + return euroToMicroCCD(euros, parameters); + } + + /** + * Converts {@link CCDAmount} to euros using exchange rate from the provided {@link ChainParameters}. + * + * @param ccdAmount {@link CCDAmount} to convert. + * @param parameters {@link ChainParameters} with exchange rate used for conversion. + * @return {@link ConversionResult} corresponding to the euro value of {@link CCDAmount}. + */ + public static ConversionResult microCCDToEuro(CCDAmount ccdAmount, ChainParameters parameters) { + ConversionResult ccd = from(ccdAmount.getValue(), UInt64.from(1)); + return microCCDToEuro(ccd, parameters); + } + + /** + * Converts {@link ConversionResult} representing {@link CCDAmount} to euros using exchange rate from the provided {@link ChainParameters}. + * + * @param ccd {@link ConversionResult} representing {@link CCDAmount} to convert. + * @param parameters {@link ChainParameters} with exchange rate used for conversion. + * @return {@link ConversionResult} corresponding to the euro value of {@link CCDAmount}. + */ + public static ConversionResult microCCDToEuro(ConversionResult ccd, ChainParameters parameters) { + ConversionResult microCCDPerEuro = from(parameters.getMicroCCDPerEuro()); + return ccd.div(microCCDPerEuro); + } + + /** + * Converts {@link CCDAmount} to energy using exchange rate from the provided {@link ChainParameters}. + * + * @param ccdAmount{@link CCDAmount} to convert. + * @param parameters {@link ChainParameters} with exchange rate used for conversion. + * @return {@link ConversionResult} corresponding to the energy value of {@link CCDAmount}. + */ + public static ConversionResult ccdToEnergy(CCDAmount ccdAmount, ChainParameters parameters) { + ConversionResult ccd = from(ccdAmount.getValue(), UInt64.from(1)); + return ccdToEnergy(ccd, parameters); + } + + /** + * Converts {@link ConversionResult} representing {@link CCDAmount} to energy using exchange rate from the provided {@link ChainParameters}. + * + * @param ccdAmount {@link ConversionResult} representing {@link CCDAmount} to convert. + * @param parameters {@link ChainParameters} with exchange rate used for conversion. + * @return {@link ConversionResult} corresponding to the energy value of {@link CCDAmount}. + */ + public static ConversionResult ccdToEnergy(ConversionResult ccdAmount, ChainParameters parameters) { + ConversionResult euros = microCCDToEuro(ccdAmount, parameters); + return euroToEnergy(euros, parameters); + } + + /** + * Converts {@link ConversionResult} representing an amount of euros to micro CCD using exchange rate from the provided {@link ChainParameters}. + * + * @param euros {@link ConversionResult} representing amount of euros to convert. + * @param parameters {@link ChainParameters} with exchange rate used for conversion. + * @return {@link ConversionResult} corresponding to the micro CCD value of the input. + */ + public static ConversionResult euroToMicroCCD(ConversionResult euros, ChainParameters parameters) { + ConversionResult microCCDPerEuro = from(parameters.getMicroCCDPerEuro()); + return euros.mult(microCCDPerEuro); + } + + /** + * Converts {@link ConversionResult} representing an amount of euros to energy using exchange rate from the provided {@link ChainParameters}. + *† + * @param euros {@link ConversionResult} representing amount of euros to convert. + * @param parameters {@link ChainParameters} with exchange rate used for conversion. + * @return {@link ConversionResult} corresponding to the energy value of the input. + */ + public static ConversionResult euroToEnergy(ConversionResult euros, ChainParameters parameters) { + ConversionResult euroPerEnergy = from(parameters.getEuroPerEnergy()); + return euros.div(euroPerEnergy); + } + +} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2Client.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2Client.java index 32c1889ab..bb70875b2 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2Client.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/Cis2Client.java @@ -1,30 +1,20 @@ package com.concordium.sdk.cis2; import com.concordium.sdk.ClientV2; -import com.concordium.sdk.cis2.events.Cis2Event; import com.concordium.sdk.cis2.events.Cis2EventWithMetadata; import com.concordium.sdk.requests.AccountQuery; import com.concordium.sdk.requests.BlockQuery; import com.concordium.sdk.requests.smartcontracts.Energy; import com.concordium.sdk.requests.smartcontracts.InvokeInstanceRequest; -import com.concordium.sdk.responses.blockitemstatus.FinalizedBlockItem; -import com.concordium.sdk.responses.blockitemsummary.Summary; -import com.concordium.sdk.responses.blockitemsummary.Type; import com.concordium.sdk.responses.blocksatheight.BlocksAtHeightRequest; -import com.concordium.sdk.responses.smartcontracts.ContractTraceElement; -import com.concordium.sdk.responses.smartcontracts.ContractTraceElementType; -import com.concordium.sdk.responses.transactionstatus.ContractUpdated; import com.concordium.sdk.responses.transactionstatus.Outcome; -import com.concordium.sdk.responses.transactionstatus.TransactionResultEventType; import com.concordium.sdk.transactions.*; import com.concordium.sdk.types.*; import com.google.common.collect.Lists; import lombok.Getter; import lombok.val; -import lombok.var; import java.util.*; -import java.util.stream.Collectors; /** * A client dedicated to the CIS2 specification. @@ -112,7 +102,7 @@ public Map balanceOf(BalanceQuery... queries) { val listOfQueries = Arrays.asList(queries); val parameter = SerializationUtils.serializeBalanceOfParameter(listOfQueries); val endpoint = ReceiveName.from(contractName, "balanceOf"); - val result = this.client.invokeInstance(InvokeInstanceRequest.from(BlockQuery.LAST_FINAL, this.contractAddress, CCDAmount.from(0), endpoint, parameter, Optional.empty())); + val result = this.client.invokeInstance(InvokeInstanceRequest.from(BlockQuery.LAST_FINAL, this.contractAddress, endpoint, parameter, Optional.empty())); if (result.getOutcome() == Outcome.REJECT) { throw new RuntimeException("balanceOf failed: " + result.getRejectReason().toString()); } @@ -134,7 +124,7 @@ public Map operatorOf(OperatorQuery... queries) { val listOfQueries = Arrays.asList(queries); val parameter = SerializationUtils.serializeOperatorOfParameter(listOfQueries); val endpoint = ReceiveName.from(contractName, "operatorOf"); - val result = this.client.invokeInstance(InvokeInstanceRequest.from(BlockQuery.LAST_FINAL, this.contractAddress, CCDAmount.from(0), endpoint, parameter, Optional.empty())); + val result = this.client.invokeInstance(InvokeInstanceRequest.from(BlockQuery.LAST_FINAL, this.contractAddress, endpoint, parameter, Optional.empty())); if (result.getOutcome() == Outcome.REJECT) { throw new RuntimeException("operatorOf failed: " + result.getRejectReason().toString()); } @@ -156,7 +146,7 @@ public Map tokenMetadata(TokenId... tokenIds) { val listOfQueries = Arrays.asList(tokenIds); val parameter = SerializationUtils.serializeTokenIds(listOfQueries); val endpoint = ReceiveName.from(contractName, "tokenMetadata"); - val result = this.client.invokeInstance(InvokeInstanceRequest.from(BlockQuery.LAST_FINAL, this.contractAddress, CCDAmount.from(0), endpoint, parameter, Optional.empty())); + val result = this.client.invokeInstance(InvokeInstanceRequest.from(BlockQuery.LAST_FINAL, this.contractAddress, endpoint, parameter, Optional.empty())); if (result.getOutcome() == Outcome.REJECT) { throw new RuntimeException("tokenMetadata failed: " + result.getRejectReason().toString()); } diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/requests/smartcontracts/Energy.java b/concordium-sdk/src/main/java/com/concordium/sdk/requests/smartcontracts/Energy.java index 9673b8c07..684a6af6d 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/requests/smartcontracts/Energy.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/requests/smartcontracts/Energy.java @@ -16,7 +16,7 @@ public class Energy { @Override public String toString() { - return this.value.getValue() + " NRG"; + return this.value.toString() + " NRG"; } public static Energy from(com.concordium.grpc.v2.Energy energy) { @@ -26,4 +26,8 @@ public static Energy from(com.concordium.grpc.v2.Energy energy) { public static Energy from(UInt64 value) { return new Energy(value); } + public static Energy from(String val) { + return Energy.from(UInt64.from(val)); + } + } diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/requests/smartcontracts/InvokeInstanceRequest.java b/concordium-sdk/src/main/java/com/concordium/sdk/requests/smartcontracts/InvokeInstanceRequest.java index e42e36cb0..73e77f9a1 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/requests/smartcontracts/InvokeInstanceRequest.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/requests/smartcontracts/InvokeInstanceRequest.java @@ -38,10 +38,6 @@ public class InvokeInstanceRequest { */ private ContractAddress instance; - /** - * Amount to invoke the smart contract instance with. - */ - private CCDAmount amount; /** * The entrypoint of the smart contract instance to invoke. @@ -70,7 +66,6 @@ public class InvokeInstanceRequest { * If this is not supplied then the contract will be invoked by an account with address 0, * no credentials and sufficient amount of CCD to cover the transfer amount. * @param instance Address of the contract instance to invoke. - * @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. If this is not set, then the node @@ -79,7 +74,6 @@ public class InvokeInstanceRequest { 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) { @@ -87,7 +81,6 @@ public static InvokeInstanceRequest from(@NonNull BlockQuery blockHash, .blockHash(blockHash) .invoker(invoker) .instance(instance) - .amount(amount) .entrypoint(entrypoint) .parameter(parameter) .energy(energy).build(); @@ -99,7 +92,6 @@ public static InvokeInstanceRequest from(@NonNull BlockQuery blockHash, * * @param blockHash Block to invoke the contract. The invocation will be at the end of the given block. * @param instance Address of the contract instance to invoke. - * @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. If this is not set, then the node @@ -107,14 +99,12 @@ public static InvokeInstanceRequest from(@NonNull BlockQuery blockHash, */ public static InvokeInstanceRequest from(@NonNull BlockQuery blockHash, @NonNull ContractAddress instance, - @NonNull CCDAmount amount, @NonNull ReceiveName entrypoint, @NonNull Parameter parameter, @NonNull Optional energy) { return InvokeInstanceRequest.builder() .blockHash(blockHash) .instance(instance) - .amount(amount) .entrypoint(entrypoint) .parameter(parameter) .energy(energy).build(); @@ -128,7 +118,6 @@ public static InvokeInstanceRequest from(@NonNull BlockQuery blockHash, * If this is not supplied then the contract will be invoked by an account with address 0, * no credentials and sufficient amount of CCD to cover the transfer amount. * @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. If this is not set, then the node * will decide a sufficient amount of energy allowed for transaction execution. @@ -136,13 +125,12 @@ public static InvokeInstanceRequest from(@NonNull BlockQuery blockHash, public static InvokeInstanceRequest from(@NonNull BlockQuery blockHash, @NonNull AbstractAddress invoker, @NonNull ContractAddress instance, - @NonNull CCDAmount amount, @NonNull SchemaParameter schemaParameter, @NonNull Optional energy) { if (!(schemaParameter.getType() == ParameterType.RECEIVE)) { throw new IllegalArgumentException("Cannot initialize smart contract with InvokeInstance. SchemaParameter for InvokeInstanceRequest must be initialized with a ReceiveName"); } - return from(blockHash, invoker, instance, amount, schemaParameter.getReceiveName(), Parameter.from(schemaParameter), energy); + return from(blockHash, invoker, instance, schemaParameter.getReceiveName(), Parameter.from(schemaParameter), energy); } /** @@ -151,20 +139,18 @@ public static InvokeInstanceRequest from(@NonNull BlockQuery blockHash, * * @param blockHash Block to invoke the contract. The invocation will be at the end of the given block. * @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. If this is not set, then the node * will decide a sufficient amount of energy allowed for transaction execution. */ public static InvokeInstanceRequest from(@NonNull BlockQuery blockHash, @NonNull ContractAddress instance, - @NonNull CCDAmount amount, @NonNull SchemaParameter schemaParameter, @NonNull Optional energy) { if (!(schemaParameter.getType() == ParameterType.RECEIVE)) { throw new IllegalArgumentException("Cannot initialize smart contract with InvokeInstance. SchemaParameter for InvokeInstanceRequest must be initialized with a ReceiveName"); } - return from(blockHash, instance, amount, schemaParameter.getReceiveName(), Parameter.from(schemaParameter), energy); + return from(blockHash, instance, schemaParameter.getReceiveName(), Parameter.from(schemaParameter), energy); } public boolean hasInvoker() { diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/responses/Fraction.java b/concordium-sdk/src/main/java/com/concordium/sdk/responses/Fraction.java index 708ee9f67..9ba390379 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/responses/Fraction.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/responses/Fraction.java @@ -6,6 +6,9 @@ import lombok.EqualsAndHashCode; import lombok.Getter; +import java.math.BigDecimal; +import java.math.RoundingMode; + /** * A fraction */ @@ -54,4 +57,29 @@ public String toString() { public double asDouble() { return (double) this.numerator.getValue() / (double) this.denominator.getValue(); } + + /** + * Get the fraction as a {@link BigDecimal} value with the specified amount of precision i.e. the specified amount of decimal points. + * Result is rounded using {@link RoundingMode#HALF_UP} + * + * @param precision how many decimals of precision. + * @return the fraction represented as a {@link BigDecimal}. + */ + public BigDecimal asBigDecimal(int precision) { + return asBigDecimal(precision, RoundingMode.HALF_UP); + } + + /** + * Get the fraction as a {@link BigDecimal} value with the specified amount of precision i.e. the specified amount of decimal points. + * Result is rounded using the provided {@link RoundingMode}. + * + * @param precision how many decimals of precision. + * @param roundingMode the {@link RoundingMode} to use. + * @return the fraction represented as a {@link BigDecimal}. + */ + public BigDecimal asBigDecimal(int precision, RoundingMode roundingMode) { + BigDecimal numerator = new BigDecimal(this.numerator.toString()); + BigDecimal denominator = new BigDecimal(this.denominator.toString()); + return numerator.divide(denominator, precision, roundingMode); + } } diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/responses/chainparameters/ChainParameters.java b/concordium-sdk/src/main/java/com/concordium/sdk/responses/chainparameters/ChainParameters.java index dfbaf0f08..13dc6fa3e 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/responses/chainparameters/ChainParameters.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/responses/chainparameters/ChainParameters.java @@ -1,5 +1,7 @@ package com.concordium.sdk.responses.chainparameters; +import com.concordium.sdk.responses.Fraction; + /** * Base class for all chain parameters. * All chain parameters must implement 'getVersion'. @@ -8,6 +10,9 @@ public abstract class ChainParameters { public abstract Version getVersion(); + public abstract Fraction getEuroPerEnergy(); + public abstract Fraction getMicroCCDPerEuro(); + /** * Chain parameters versions */ diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/responses/chainparameters/ChainParametersV1.java b/concordium-sdk/src/main/java/com/concordium/sdk/responses/chainparameters/ChainParametersV1.java index 737efa283..869a02500 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/responses/chainparameters/ChainParametersV1.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/responses/chainparameters/ChainParametersV1.java @@ -5,10 +5,7 @@ import com.concordium.sdk.responses.transactionstatus.PartsPerHundredThousand; import com.concordium.sdk.transactions.CCDAmount; import com.concordium.sdk.types.AccountAddress; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.ToString; +import lombok.*; /** * Chain parameters for protocol versions 4 to 5. @@ -16,6 +13,7 @@ @ToString @EqualsAndHashCode(callSuper = false) @Builder +@Getter @AllArgsConstructor public class ChainParametersV1 extends ChainParameters { diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/transactions/CCDAmount.java b/concordium-sdk/src/main/java/com/concordium/sdk/transactions/CCDAmount.java index d1212485e..0069a730c 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/transactions/CCDAmount.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/transactions/CCDAmount.java @@ -8,7 +8,6 @@ import lombok.Getter; import java.nio.ByteBuffer; -import java.text.DecimalFormat; /** * A CCD amount with 'micro' precision. diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/types/ConversionResult.java b/concordium-sdk/src/main/java/com/concordium/sdk/types/ConversionResult.java new file mode 100644 index 000000000..6b921cac1 --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/types/ConversionResult.java @@ -0,0 +1,140 @@ +package com.concordium.sdk.types; + +import com.concordium.sdk.CurrencyConverter; +import com.concordium.sdk.requests.smartcontracts.Energy; +import com.concordium.sdk.responses.Fraction; +import com.concordium.sdk.transactions.CCDAmount; +import lombok.Getter; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; + +/** + * For representing fractions with large numerators or denominators. Used in {@link CurrencyConverter} to convert to/from {@link CCDAmount} and {@link Energy}. + * Use {@link ConversionResult#asBigDecimal(int)} to get numeric value of the fraction. + * Type parameter prevents faulty usage of conversion functions in {@link CurrencyConverter}. + */ +@Getter +public class ConversionResult{ + + private ConversionResult(BigInteger numerator, BigInteger denominator) { + this.numerator = numerator; + this.denominator = denominator; + } + + + public interface ConversionResultType {} + + /** + * Phantom type for {@link ConversionResult} indicating the result represents a {@link CCDAmount}. + */ + public static final class microCCD implements ConversionResultType {} + + /** + * Phantom type for {@link ConversionResult} indicating the result represents an amount of {@link Energy} + */ + public static final class NRG implements ConversionResultType {} + + /** + * Phantom type for {@link ConversionResult} indicating the result represents an amount of Euros + */ + public static final class EUR implements ConversionResultType {} + + public static final class ConversionRate implements ConversionResultType {} + + /** + * Numerator of the fraction. + */ + private final BigInteger numerator; + /** + * Denominator of the fraction. + */ + private final BigInteger denominator; + + public static ConversionResult from(BigInteger numerator, BigInteger denominator) { + return new ConversionResult<>(numerator, denominator); + } + + public static ConversionResult from(String numerator, String denominator) { + return from(new BigInteger(numerator), new BigInteger(denominator)); + } + + public static ConversionResult from(UInt64 numerator, UInt64 denominator) { + return from(numerator.toString(),denominator.toString()); + } + + public static ConversionResult from(Fraction fraction) { + return from(fraction.getNumerator(),fraction.getDenominator()); + } + + @Override + public String toString() { + return this.numerator + "/" + this.denominator; + } + + /** + * Get the fraction as a {@link BigDecimal} value with the specified amount of precision i.e. the specified amount of decimal points. + * Result is rounded using {@link RoundingMode#HALF_UP} + * + * @param precision how many decimals of precision. + * @return the fraction represented as a {@link BigDecimal}. + */ + public BigDecimal asBigDecimal(int precision) { + return asBigDecimal(precision, RoundingMode.HALF_UP); + } + + /** + * Get the fraction as a {@link BigDecimal} value with the specified amount of precision i.e. the specified amount of decimal points. + * Result is rounded using the provided {@link RoundingMode}. + * + * @param precision how many decimals of precision. + * @param roundingMode the {@link RoundingMode} to use. + * @return the fraction represented as a {@link BigDecimal}. + */ + public BigDecimal asBigDecimal(int precision, RoundingMode roundingMode) { + BigDecimal numerator = new BigDecimal(this.getNumerator()); + BigDecimal denominator = new BigDecimal(this.getDenominator()); + return numerator.divide(denominator, precision, roundingMode); + } + + /** + * Multiplies {@link ConversionResult}s. + * Calculates a*c/b*d for input a/b, c/d. + */ + public ConversionResult mult(ConversionResult other) { + BigInteger numerator = this.getNumerator().multiply(other.getNumerator()); + BigInteger denominator = this.getDenominator().multiply(other.getDenominator()); + return ConversionResult.from(numerator, denominator); + } + + /** + * Divides {@link ConversionResult}s. + * Calculates a*d/b*c for input a/b, c/d. + */ + public ConversionResult div(ConversionResult other) { + BigInteger numerator = this.getNumerator().multiply(other.getDenominator()); + BigInteger denominator = this.getDenominator().multiply(other.getNumerator()); + return ConversionResult.from(numerator, denominator); + } + + @Override + public boolean equals(Object o) { + + if (o == this) { + return true; + } + + if (!(o instanceof ConversionResult)) { + return false; + } + + ConversionResult otherFraction = (ConversionResult) o; + + // Comparison done using cross multiplication. a * d = b * c => a/b = c/d + BigInteger ad = this.numerator.multiply(otherFraction.denominator); + BigInteger bc = this.denominator.multiply(otherFraction.numerator); + return ad.equals(bc); + } + +} diff --git a/concordium-sdk/src/test/java/com/concordium/sdk/EnergyToCCDTest.java b/concordium-sdk/src/test/java/com/concordium/sdk/EnergyToCCDTest.java new file mode 100644 index 000000000..fc3fc27af --- /dev/null +++ b/concordium-sdk/src/test/java/com/concordium/sdk/EnergyToCCDTest.java @@ -0,0 +1,208 @@ +package com.concordium.sdk; + +import com.concordium.sdk.requests.smartcontracts.Energy; +import com.concordium.sdk.responses.Fraction; +import com.concordium.sdk.responses.chainparameters.ChainParameters; +import com.concordium.sdk.transactions.CCDAmount; +import com.concordium.sdk.types.ConversionResult; +import com.concordium.sdk.types.ConversionResult.microCCD; +import com.concordium.sdk.types.ConversionResult.EUR; +import com.concordium.sdk.types.ConversionResult.NRG; +import com.concordium.sdk.types.UInt64; +import lombok.SneakyThrows; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests the different conversions between {@link Energy} and {@link CCDAmount}. + */ +public class EnergyToCCDTest { + + private static final long CCD_LONG = 1000000; + private static final long ENERGY_LONG = 10000; + + private static final CCDAmount CCD_AMOUNT = CCDAmount.fromMicro(CCD_LONG); + private static final Energy ENERGY = Energy.from(UInt64.from(ENERGY_LONG)); + + private static final ConversionResult CCD_FRACTION = ConversionResult.from(CCD_AMOUNT.getValue(), UInt64.from(1)); + private static final ConversionResult ENERGY_FRACTION = ConversionResult.from(ENERGY.getValue(), UInt64.from(1)); + private static final ConversionResult EURO_FRACTION = ConversionResult.from(UInt64.from(10), UInt64.from(1)); + + // Euro per energy doesn't change value and isn't nearly as big so can use same value for tests. + private static final Fraction EURO_PER_ENERGY = new Fraction(UInt64.from("1"), UInt64.from("1100000")); + + // 16626124143116419072/78228883861 + private static final Fraction MICRO_CCD_PER_EURO_1 = new Fraction(UInt64.from("16626124143116419072"), UInt64.from("78228883861")); + + // 13578063771458994176/62220885881 + private static final Fraction MICRO_CCD_PER_EURO_2 = new Fraction(UInt64.from("13578063771458994176"), UInt64.from("62220885881")); + + // 3465558596452424320/16276715619 + private static final Fraction MICRO_CCD_PER_EURO_3 = new Fraction(UInt64.from("3465558596452424320"), UInt64.from("16276715619")); + + // 5414544070782746624/24832708375 + private static final Fraction MICRO_CCD_PER_EURO_4 = new Fraction(UInt64.from("5414544070782746624"), UInt64.from("24832708375")); + private static final ChainParameters PARAM_1 = mock(ChainParameters.class); + private static final ChainParameters PARAM_2 = mock(ChainParameters.class); + private static final ChainParameters PARAM_3 = mock(ChainParameters.class); + private static final ChainParameters PARAM_4 = mock(ChainParameters.class); + + private static final List PARAMETERS_LIST = new ArrayList<>(); + + // Setup mock parameters with different ccd/euro ratios. + static { + when(PARAM_1.getMicroCCDPerEuro()).thenReturn(MICRO_CCD_PER_EURO_1); + when(PARAM_2.getMicroCCDPerEuro()).thenReturn(MICRO_CCD_PER_EURO_2); + when(PARAM_3.getMicroCCDPerEuro()).thenReturn(MICRO_CCD_PER_EURO_3); + when(PARAM_4.getMicroCCDPerEuro()).thenReturn(MICRO_CCD_PER_EURO_4); + + when(PARAM_1.getEuroPerEnergy()).thenReturn(EURO_PER_ENERGY); + when(PARAM_2.getEuroPerEnergy()).thenReturn(EURO_PER_ENERGY); + when(PARAM_3.getEuroPerEnergy()).thenReturn(EURO_PER_ENERGY); + when(PARAM_4.getEuroPerEnergy()).thenReturn(EURO_PER_ENERGY); + + PARAMETERS_LIST.add(PARAM_1); + PARAMETERS_LIST.add(PARAM_2); + PARAMETERS_LIST.add(PARAM_3); + PARAMETERS_LIST.add(PARAM_4); + } + + + @SneakyThrows + @Test + public void testShouldBePrecise() { + for (ChainParameters parameters : PARAMETERS_LIST) { + // Converting Energy + energyToCCD(parameters); + energyToEuro(parameters); + // Converting CCD + ccdToEnergy(parameters); + ccdToEuro(parameters); + // Converting Euro + euroToEnergy(parameters); + euroToCCD(parameters); + } + } + + private void energyToCCD(ChainParameters parameters) { + ConversionResult ccdFromEnergy = CurrencyConverter.energyToMicroCCD(ENERGY, parameters); + ConversionResult ccdFromEnergyFraction = CurrencyConverter.energyToMicroCCD(ENERGY_FRACTION, parameters); + // Both conversions yield same result + assertEquals(ccdFromEnergy, ccdFromEnergyFraction); + + ConversionResult energyFromCCD = CurrencyConverter.ccdToEnergy(ccdFromEnergy, parameters); + ConversionResult energyFromCCDFraction = CurrencyConverter.ccdToEnergy(ccdFromEnergyFraction, parameters); + + // Converting back yields same result + assertEquals(energyFromCCD, energyFromCCDFraction); + + // Converting back is precise + assertEquals(energyFromCCD, ENERGY_FRACTION); + } + + private void energyToEuro(ChainParameters parameters) { + ConversionResult euroFromEnergy = CurrencyConverter.energyToEuro(ENERGY, parameters); + ConversionResult euroFromEnergyFraction = CurrencyConverter.energyToEuro(ENERGY_FRACTION, parameters); + // Both conversions yield same result + assertEquals(euroFromEnergy, euroFromEnergyFraction); + + ConversionResult energyFromEuro = CurrencyConverter.euroToEnergy(euroFromEnergy, parameters); + ConversionResult energyFromEuroFraction = CurrencyConverter.euroToEnergy(euroFromEnergyFraction, parameters); + + // Converting back yields same result + assertEquals(energyFromEuro, energyFromEuroFraction); + + // Converting back is precise + assertEquals(energyFromEuro, ENERGY_FRACTION); + } + private void ccdToEnergy(ChainParameters parameters) { + ConversionResult energyFromCCD = CurrencyConverter.ccdToEnergy(CCD_AMOUNT, parameters); + ConversionResult energyFromCCDFraction = CurrencyConverter.ccdToEnergy(CCD_FRACTION, parameters); + + // Both conversions yield same result + assertEquals(energyFromCCD, energyFromCCDFraction); + + ConversionResult ccdFromEnergy = CurrencyConverter.energyToMicroCCD(energyFromCCD, parameters); + ConversionResult ccdFromEnergyFraction = CurrencyConverter.energyToMicroCCD(energyFromCCDFraction, parameters); + + // Converting back yields same result + assertEquals(ccdFromEnergy, ccdFromEnergyFraction); + + // Converting back is precise + assertEquals(ccdFromEnergy, CCD_FRACTION); + } + private void ccdToEuro(ChainParameters parameters) { + ConversionResult euroFromCCD = CurrencyConverter.microCCDToEuro(CCD_AMOUNT, parameters); + ConversionResult euroFromCCDFraction = CurrencyConverter.microCCDToEuro(CCD_FRACTION, parameters); + // Both conversions yield same result + assertEquals(euroFromCCD,euroFromCCDFraction); + + ConversionResult ccdFromEuro = CurrencyConverter.euroToMicroCCD(euroFromCCD, parameters); + ConversionResult ccdFromEuroFraction = CurrencyConverter.euroToMicroCCD(euroFromCCDFraction, parameters); + + // Converting back yields same result + assertEquals(ccdFromEuro, ccdFromEuroFraction); + + // Converting back is precise + assertEquals(ccdFromEuro, CCD_FRACTION); + } + private void euroToEnergy(ChainParameters parameters) { + ConversionResult energyFromEuro = CurrencyConverter.euroToEnergy(EURO_FRACTION, parameters); + ConversionResult euroFromEnergy = CurrencyConverter.energyToEuro(energyFromEuro, parameters); + + // Conversions are precise + assertEquals(euroFromEnergy, EURO_FRACTION); + } + private void euroToCCD(ChainParameters parameters) { + ConversionResult ccdFromEuro = CurrencyConverter.euroToMicroCCD(EURO_FRACTION, parameters); + ConversionResult euroFromCCD = CurrencyConverter.microCCDToEuro(ccdFromEuro, parameters); + + // Conversions are precise + assertEquals(euroFromCCD, EURO_FRACTION); + } + + /** + * Asserts that the calculations performed when using PARAM_1 yield the correct result. + */ + @Test + public void testActualValues() { + // EURO_PER_ENERGY * ENERGY = 1/1100000×10000 = 1/110 + ConversionResult expectedEuroFromEnergy = ConversionResult.from("1", "110"); + ConversionResult euroFromEnergy = CurrencyConverter.energyToEuro(ENERGY, PARAM_1); + assertEquals(expectedEuroFromEnergy, euroFromEnergy); + + // MICRO_CCD_PER_EURO_1 * EURO = 16626124143116419072/78228883861 * 10 = 166261241431164190720/78228883861 + ConversionResult expectedCCDFromEuro = ConversionResult.from("166261241431164190720","78228883861"); + ConversionResult ccdFromEuro = CurrencyConverter.euroToMicroCCD(EURO_FRACTION, PARAM_1); + assertEquals(expectedCCDFromEuro, ccdFromEuro); + + // EURO_PER_ENERGY * ENERGY * MICRO_CCD_PER_EURO_1 = 1/110 * 16626124143116419072/78228883861 = 8313062071558209536/4302588612355 + ConversionResult expectedCCDFromEnergy = ConversionResult.from("8313062071558209536", "4302588612355"); + ConversionResult ccdFromEnergy = CurrencyConverter.energyToMicroCCD(ENERGY, PARAM_1); + assertEquals(expectedCCDFromEnergy, ccdFromEnergy); + + // CCD_AMOUNT / MICRO_CCD_PER_EURO_1 = 1000000 / (16626124143116419072/78228883861) = 1222326310328125/259783189736194048 + ConversionResult expectedEuroFromCCD = ConversionResult.from("1222326310328125", "259783189736194048"); + ConversionResult euroFromCCD = CurrencyConverter.microCCDToEuro(CCD_AMOUNT, PARAM_1); + assertEquals(expectedEuroFromCCD, euroFromCCD); + + // EUROS / EURO_PER_ENERGY = 10 / (1/1100000) = 11000000 + ConversionResult expectedEnergyFromEuro = ConversionResult.from("11000000", "1"); + ConversionResult energyFromEuro = CurrencyConverter.euroToEnergy(EURO_FRACTION, PARAM_1); + assertEquals(expectedEnergyFromEuro, energyFromEuro); + + // CCD_AMOUNT / MICRO_CCD_PER_EURO_1 / EUROS_PER_ENERGY = + // (1000000 / (16626124143116419072/78228883861)) / (1/1100000) = + // 42017466917529296875/8118224679256064 + ConversionResult expectedEnergyFromCCD = ConversionResult.from("42017466917529296875", "8118224679256064"); + ConversionResult energyFromCCD = CurrencyConverter.ccdToEnergy(CCD_AMOUNT, PARAM_1); + assertEquals(expectedEnergyFromCCD, energyFromCCD); + } + +}