From ee978c073542195adf79bf500b5b18d10c9886bd Mon Sep 17 00:00:00 2001 From: magnusbechwind Date: Wed, 13 Mar 2024 13:31:14 +0100 Subject: [PATCH 1/8] Implemented WasmModule.getSchema. Added utility class LEB128U to encode/decode LEB128U integers. --- .../com/concordium/sdk/cis2/TokenAmount.java | 51 +++----- .../smartcontracts/WasmModule.java | 71 +++++++++++- .../com/concordium/sdk/types/LEB128U.java | 109 ++++++++++++++++++ .../sdk/smartcontract/GetSchemaTest.java | 63 ++++++++++ .../unit-test-with-schema.wasm | Bin 0 -> 20518 bytes .../smartcontractschema/unit-test.wasm | Bin 0 -> 20113 bytes 6 files changed, 256 insertions(+), 38 deletions(-) create mode 100644 concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java create mode 100644 concordium-sdk/src/test/java/com/concordium/sdk/smartcontract/GetSchemaTest.java create mode 100644 concordium-sdk/src/test/testresources/smartcontractschema/unit-test-with-schema.wasm create mode 100644 concordium-sdk/src/test/testresources/smartcontractschema/unit-test.wasm diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenAmount.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenAmount.java index 350cda652..1f2567e14 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenAmount.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenAmount.java @@ -1,8 +1,8 @@ package com.concordium.sdk.cis2; +import com.concordium.sdk.types.LEB128U; import lombok.*; -import java.io.ByteArrayOutputStream; import java.math.BigInteger; import java.nio.ByteBuffer; @@ -42,26 +42,15 @@ public static TokenAmount from(String 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. + * @throws IllegalArgumentException 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."); + try { + return LEB128U.encode(this.amount, 37); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid encoding of TokenAmount. Must not exceed 37 byes.", e); } - return bos.toByteArray(); + } /** @@ -70,26 +59,18 @@ public byte[] encode() { * * @param buffer the buffer to read from. * @return the parsed {@link TokenAmount} - * @throws RuntimeException if the encoding is more than 37 bytes. + * @throws IllegalArgumentException 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++; + + try { + val result = LEB128U.decode(buffer, 37); + return new TokenAmount(result); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Tried to decode a TokenAmount consisting of more than 37 bytes.", e); } - return new TokenAmount(result); } +} + -} diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/transactions/smartcontracts/WasmModule.java b/concordium-sdk/src/main/java/com/concordium/sdk/transactions/smartcontracts/WasmModule.java index 856b79fde..4a71c8950 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/transactions/smartcontracts/WasmModule.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/transactions/smartcontracts/WasmModule.java @@ -2,16 +2,19 @@ import com.concordium.sdk.crypto.SHA256; import com.concordium.sdk.responses.modulelist.ModuleRef; -import com.concordium.sdk.transactions.Payload; -import com.concordium.sdk.transactions.TransactionType; -import com.concordium.sdk.types.UInt64; +import com.concordium.sdk.types.LEB128U; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; import lombok.val; +import org.bouncycastle.util.Strings; +import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.Arrays; +import java.util.Optional; /** * A compiled Smart Contract Module in WASM with source and version. @@ -83,6 +86,17 @@ public static WasmModule from(final byte[] bytes) { return from(moduleBytes, version); } + /** + * Create {@link WasmModule} from compiled module WASM file. Passed module should have version prefixed. + * @param path path to the compiled WASM module. + * @return parsed {@link WasmModule}. + * @throws IOException if an I/O exception occurs reading from the provided path. + */ + public static WasmModule from(String path) throws IOException { + val moduleBytes = Files.readAllBytes(Paths.get(path)); + return from(moduleBytes); + } + /** * Get the identifier of the WasmModule. * The identifier is a SHA256 hash of the raw module bytes. @@ -107,4 +121,55 @@ public byte[] getBytes() { return buffer.array(); } + /** + * Retrieve the {@link Schema} corresponding to the contract, if embedded. + * Behaviour is not specified if the bytes of {@link WasmModule} do not represent a valid concordium wasm module. + * @return {@link Optional} containing the {@link Schema} if found, empty otherwise. + */ + public Optional getSchema() { + val moduleSourceBytes = source.getBytes(); + val buffer = ByteBuffer.wrap(moduleSourceBytes.clone()); + + // Skip 4 byte length of WasmModuleSource (UInt32.BYTES) + 4 byte magic number + 4 byte WASM version + buffer.position(buffer.position() + 12); + + while (buffer.hasRemaining()) { + // A section is a 1 byte id followed by the length of the section as a LEB128U encoded u32. + byte id = buffer.get(); + int remainingSectionLength = LEB128U.decode(buffer, LEB128U.U32_BYTES).intValue(); + + // Custom sections have id 0 så all other ids are skipped. + if (id != 0) { + buffer.position(buffer.position() + remainingSectionLength); + continue; + } + + // Custom sections have a name encoded as a vector i.e. a length followed by the actual bytes + int beforeName = buffer.position(); + int nameLength = LEB128U.decode(buffer, LEB128U.U32_BYTES).intValue(); + int nameLengthBytes = buffer.position() - beforeName; + + byte[] nameBytes = new byte[nameLength]; + buffer.get(nameBytes); + + String name = Strings.fromByteArray(nameBytes); + + // We've incremented the buffer by reading the length of the name and the name. + remainingSectionLength = remainingSectionLength - nameLengthBytes - nameLength; + + if (name.equals("concordium-schema")) { + // After reading the name, the remaining contents of the custom section is the schema itself. + byte[] schemaBytes = new byte[remainingSectionLength]; + buffer.get(schemaBytes); + Schema schema = Schema.from(schemaBytes); + return Optional.of(schema); + } + + // Go to the next section if the name didn't match. + buffer.position(buffer.position() + remainingSectionLength); + } + + return Optional.empty(); + } + } diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java b/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java new file mode 100644 index 000000000..091adb4f2 --- /dev/null +++ b/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java @@ -0,0 +1,109 @@ +package com.concordium.sdk.types; + +import lombok.*; + +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.nio.ByteBuffer; + +/** + * Contains methods to encode/decode LEB128U amounts. + */ +public class LEB128U { + + /** + * LEB128U integer of unbounded size. + */ + public static int UNBOUNDED = -1; + + /** + * Max number of bytes in a LEB128U encoded u64. ceil(64/7). + */ + public static int U64_BYTES = 10; + + /** + * Max number of bytes in a LEB128U encoded u32. ceil(32/7). + */ + public static int U32_BYTES = 5; + + /** + * Deserialize a LEB128U encoded value from the provided buffer. + * Behaves like decode(buffer, LEB128U.UNBOUNDED). + * + * @param buffer the buffer to read from. + * @return {@link BigInteger} representing the encoded value + */ + public static BigInteger decode(ByteBuffer buffer) { + return decode(buffer, UNBOUNDED); + } + + /** + * Deserialize a LEB128U encoded value from the provided buffer. + * + * @param buffer the buffer to read from. + * @param maxSize the max amount of bytes to decode. + * @return {@link BigInteger} representing the encoded value + * @throws IllegalArgumentException if more than `maxSize` bytes are decoded. + */ + public static BigInteger decode(ByteBuffer buffer, int maxSize) { + var result = BigInteger.ZERO; + int shift = 0; + int count = 0; + while (true) { + 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++; + if (maxSize != UNBOUNDED && count > maxSize) { + throw new IllegalArgumentException("LEB128U encoded integer is larger than provided max size: " + maxSize); + } + } + return result; + } + + /** + * Encode the provided {@link BigInteger} in LEB128 unsigned format. + * Behaves like encode(value, LEB128U.UNBOUNDED). + * + * @param value {@link BigInteger} representing the value to encode. + * @return byte array containing the encoded value. + */ + public static byte[] encode(BigInteger value) { + return encode(value, UNBOUNDED); + } + + /** + * Encode the provided {@link BigInteger} in LEB128 unsigned format. + * + * @param value {@link BigInteger} representing the value to encode. + * @param maxSize the max amount of bytes to decode. + * @return byte array containing the encoded value. + * @throws IllegalArgumentException if more than `maxSize` bytes are encoded. + */ + public static byte[] encode(BigInteger value, int maxSize) { + + if (value.equals(BigInteger.ZERO)) { + return new byte[]{0}; + } + val bos = new ByteArrayOutputStream(); + var valueToEncode = value; + // Loop until the most significant byte is zero or less + while (valueToEncode.compareTo(BigInteger.ZERO) > 0) { + // Take the 7 least significant bits of the current value and set the MSB + var currentByte = valueToEncode.and(BigInteger.valueOf(0x7F)).byteValue(); + valueToEncode = valueToEncode.shiftRight(7); + if (valueToEncode.compareTo(BigInteger.ZERO) != 0) { + currentByte |= 0x80; // Set the MSB to 1 to indicate there are more bytes to come + } + bos.write(currentByte); + if (maxSize != UNBOUNDED && bos.size() > maxSize) { + throw new IllegalArgumentException("BigInteger: " + value + " does not fit withing provided max size: " + maxSize); + } + } + return bos.toByteArray(); + } +} diff --git a/concordium-sdk/src/test/java/com/concordium/sdk/smartcontract/GetSchemaTest.java b/concordium-sdk/src/test/java/com/concordium/sdk/smartcontract/GetSchemaTest.java new file mode 100644 index 000000000..491cfb2ce --- /dev/null +++ b/concordium-sdk/src/test/java/com/concordium/sdk/smartcontract/GetSchemaTest.java @@ -0,0 +1,63 @@ +package com.concordium.sdk.smartcontract; + +import com.concordium.sdk.transactions.ReceiveName; +import com.concordium.sdk.transactions.smartcontracts.Schema; +import com.concordium.sdk.transactions.smartcontracts.WasmModule; +import com.concordium.sdk.transactions.smartcontracts.parameters.AccountAddressParam; +import com.concordium.sdk.types.AccountAddress; +import org.junit.Test; + +import java.io.IOException; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * Ensures correct retrieval of {@link Schema} from {@link WasmModule} if embedded. + */ +public class GetSchemaTest { + + static WasmModule MODULE_WITH_SCHEMA; + + static WasmModule MODULE_WITHOUT_SCHEMA; + + static String CONTRACT_NAME = "java_sdk_schema_unit_test"; + + static { + try { + MODULE_WITH_SCHEMA = WasmModule.from("./src/test/testresources/smartcontractschema/unit-test-with-schema.wasm"); + MODULE_WITHOUT_SCHEMA = WasmModule.from("./src/test/testresources/smartcontractschema/unit-test.wasm"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Test + public void shouldFindSchema() { + Optional optionalSchema = MODULE_WITH_SCHEMA.getSchema(); + assert(optionalSchema.isPresent()); + Schema schema = optionalSchema.get(); + ReceiveName receiveName = ReceiveName.from(CONTRACT_NAME, "account_address_test"); + AccountAddress address = AccountAddress.from("3XSLuJcXg6xEua6iBPnWacc3iWh93yEDMCqX8FbE3RDSbEnT9P"); + AccountAddressParam accountAddressParam = new AccountAddressParam(schema, receiveName, address); + try { + accountAddressParam.initialize(); + } catch (Exception e) { + fail(); + } + try { + accountAddressParam.initialize(); + } catch (Exception e) { + fail(); + } + } + + @Test + public void shouldNotFindSchema() { + Optional optionalSchema = MODULE_WITHOUT_SCHEMA.getSchema(); + assertEquals(optionalSchema, Optional.empty()); + } + + +} diff --git a/concordium-sdk/src/test/testresources/smartcontractschema/unit-test-with-schema.wasm b/concordium-sdk/src/test/testresources/smartcontractschema/unit-test-with-schema.wasm new file mode 100644 index 0000000000000000000000000000000000000000..363d2ef01f9eef17f634f6c5f5669b10bcae3800 GIT binary patch literal 20518 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 qSy^229GKEkjgRQ)e^Cslp` literal 0 HcmV?d00001 From b100ccc3b92d6b8da51f5efcac957a92454bcf46 Mon Sep 17 00:00:00 2001 From: magnusbechwind Date: Wed, 13 Mar 2024 13:34:51 +0100 Subject: [PATCH 2/8] Updated CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38b97c99c..95a3cf0f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased changes +- Added function in `WasmModule` to exctract a `Schema` if embedded. - 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. From e01f20ecf2bb1e67e8dbda9c5d0942a52f014b56 Mon Sep 17 00:00:00 2001 From: magnusbechwind Date: Wed, 13 Mar 2024 13:44:20 +0100 Subject: [PATCH 3/8] Fixup in test --- .../com/concordium/sdk/smartcontract/GetSchemaTest.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/concordium-sdk/src/test/java/com/concordium/sdk/smartcontract/GetSchemaTest.java b/concordium-sdk/src/test/java/com/concordium/sdk/smartcontract/GetSchemaTest.java index 491cfb2ce..e932d9c3c 100644 --- a/concordium-sdk/src/test/java/com/concordium/sdk/smartcontract/GetSchemaTest.java +++ b/concordium-sdk/src/test/java/com/concordium/sdk/smartcontract/GetSchemaTest.java @@ -42,11 +42,7 @@ public void shouldFindSchema() { AccountAddress address = AccountAddress.from("3XSLuJcXg6xEua6iBPnWacc3iWh93yEDMCqX8FbE3RDSbEnT9P"); AccountAddressParam accountAddressParam = new AccountAddressParam(schema, receiveName, address); try { - accountAddressParam.initialize(); - } catch (Exception e) { - fail(); - } - try { + // Asserts that the extracted Schema is actually a valid Schema. accountAddressParam.initialize(); } catch (Exception e) { fail(); From 53ba9cebb017d408656a0dea3112398269c47b4b Mon Sep 17 00:00:00 2001 From: magnusbechwind Date: Wed, 13 Mar 2024 13:51:10 +0100 Subject: [PATCH 4/8] Added guard for negative numbers in LEB128U.encode --- .../src/main/java/com/concordium/sdk/types/LEB128U.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java b/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java index 091adb4f2..b34e45665 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java @@ -82,10 +82,12 @@ public static byte[] encode(BigInteger value) { * @param value {@link BigInteger} representing the value to encode. * @param maxSize the max amount of bytes to decode. * @return byte array containing the encoded value. - * @throws IllegalArgumentException if more than `maxSize` bytes are encoded. + * @throws IllegalArgumentException if more than `maxSize` bytes are encoded or `value` is negative. */ public static byte[] encode(BigInteger value, int maxSize) { - + if (value.compareTo(BigInteger.ZERO) < 0) { + throw new IllegalArgumentException("Cannot encode negative amount: " + value); + } if (value.equals(BigInteger.ZERO)) { return new byte[]{0}; } From 4c90a5325cb10f253ad3db55a3cb7ba995b18e46 Mon Sep 17 00:00:00 2001 From: magnusbechwind Date: Wed, 13 Mar 2024 13:53:03 +0100 Subject: [PATCH 5/8] Spelling mistake --- .../src/main/java/com/concordium/sdk/types/LEB128U.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java b/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java index b34e45665..24ed5445e 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java @@ -103,7 +103,7 @@ public static byte[] encode(BigInteger value, int maxSize) { } bos.write(currentByte); if (maxSize != UNBOUNDED && bos.size() > maxSize) { - throw new IllegalArgumentException("BigInteger: " + value + " does not fit withing provided max size: " + maxSize); + throw new IllegalArgumentException("BigInteger: " + value + " does not fit within provided max size: " + maxSize); } } return bos.toByteArray(); From 38844c13b0c57709e7078f66fcf596d6b4b2943e Mon Sep 17 00:00:00 2001 From: magnusbechwind Date: Wed, 13 Mar 2024 13:56:03 +0100 Subject: [PATCH 6/8] Extra documentation --- .../src/main/java/com/concordium/sdk/types/LEB128U.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java b/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java index 24ed5445e..ce5d121b8 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java @@ -7,7 +7,8 @@ import java.nio.ByteBuffer; /** - * Contains methods to encode/decode LEB128U amounts. + * Contains methods to encode/decode LEB128U amounts. + * Max byte numbers */ public class LEB128U { From d18d1e384dee339522b71c0d82c11a61621582da Mon Sep 17 00:00:00 2001 From: magnusbechwind Date: Wed, 13 Mar 2024 14:07:20 +0100 Subject: [PATCH 7/8] Fixed bug where negative TokenAmount could be instantiated with TokenAmount.from(long value) --- .../src/main/java/com/concordium/sdk/cis2/TokenAmount.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenAmount.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenAmount.java index 1f2567e14..595d9bc8f 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenAmount.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenAmount.java @@ -25,6 +25,7 @@ public class TokenAmount { private final BigInteger amount; private TokenAmount(BigInteger value) { + if (value.compareTo(BigInteger.ZERO) < 0) throw new IllegalArgumentException("TokenAmount must be positive"); if (value.compareTo(MAX_VALUE) > 0) throw new IllegalArgumentException("TokenAmount exceeds max value"); this.amount = value; } @@ -34,7 +35,6 @@ public static TokenAmount from(long value) { } public static TokenAmount from(String value) { - if (value.startsWith("-")) throw new IllegalArgumentException("TokenAmount must be positive"); return new TokenAmount(new BigInteger(value)); } From 33282b4501ee44d1e314029dac4d25109b856004 Mon Sep 17 00:00:00 2001 From: magnusbechwind Date: Wed, 13 Mar 2024 14:36:19 +0100 Subject: [PATCH 8/8] Final touch-ups --- .../src/main/java/com/concordium/sdk/cis2/TokenAmount.java | 5 +---- .../sdk/transactions/smartcontracts/WasmModule.java | 6 +++--- .../src/main/java/com/concordium/sdk/types/LEB128U.java | 1 + .../com/concordium/sdk/smartcontract/GetSchemaTest.java | 4 +--- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenAmount.java b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenAmount.java index 595d9bc8f..93afb457a 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenAmount.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/cis2/TokenAmount.java @@ -7,8 +7,7 @@ import java.nio.ByteBuffer; /** - * An amount as specified in the CIS2 specification. - * https://proposals.concordium.software/CIS/cis-2.html#tokenamount + * An amount as specified in the CIS2 specification. *

* It is an unsigned integer where the max value is 2^256 - 1. */ @@ -50,7 +49,6 @@ public byte[] encode() { } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Invalid encoding of TokenAmount. Must not exceed 37 byes.", e); } - } /** @@ -62,7 +60,6 @@ public byte[] encode() { * @throws IllegalArgumentException if the encoding is more than 37 bytes. */ public static TokenAmount decode(ByteBuffer buffer) { - try { val result = LEB128U.decode(buffer, 37); return new TokenAmount(result); diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/transactions/smartcontracts/WasmModule.java b/concordium-sdk/src/main/java/com/concordium/sdk/transactions/smartcontracts/WasmModule.java index 4a71c8950..2b3c95be3 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/transactions/smartcontracts/WasmModule.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/transactions/smartcontracts/WasmModule.java @@ -127,8 +127,8 @@ public byte[] getBytes() { * @return {@link Optional} containing the {@link Schema} if found, empty otherwise. */ public Optional getSchema() { - val moduleSourceBytes = source.getBytes(); - val buffer = ByteBuffer.wrap(moduleSourceBytes.clone()); + val moduleSourceBytes = source.getBytes().clone(); + val buffer = ByteBuffer.wrap(moduleSourceBytes); // Skip 4 byte length of WasmModuleSource (UInt32.BYTES) + 4 byte magic number + 4 byte WASM version buffer.position(buffer.position() + 12); @@ -138,7 +138,7 @@ public Optional getSchema() { byte id = buffer.get(); int remainingSectionLength = LEB128U.decode(buffer, LEB128U.U32_BYTES).intValue(); - // Custom sections have id 0 så all other ids are skipped. + // Custom sections have id 0 so all other ids are skipped. if (id != 0) { buffer.position(buffer.position() + remainingSectionLength); continue; diff --git a/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java b/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java index ce5d121b8..4e01adcb8 100644 --- a/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java +++ b/concordium-sdk/src/main/java/com/concordium/sdk/types/LEB128U.java @@ -72,6 +72,7 @@ public static BigInteger decode(ByteBuffer buffer, int maxSize) { * * @param value {@link BigInteger} representing the value to encode. * @return byte array containing the encoded value. + * @throws IllegalArgumentException if value is negative. */ public static byte[] encode(BigInteger value) { return encode(value, UNBOUNDED); diff --git a/concordium-sdk/src/test/java/com/concordium/sdk/smartcontract/GetSchemaTest.java b/concordium-sdk/src/test/java/com/concordium/sdk/smartcontract/GetSchemaTest.java index e932d9c3c..0146f6852 100644 --- a/concordium-sdk/src/test/java/com/concordium/sdk/smartcontract/GetSchemaTest.java +++ b/concordium-sdk/src/test/java/com/concordium/sdk/smartcontract/GetSchemaTest.java @@ -22,8 +22,6 @@ public class GetSchemaTest { static WasmModule MODULE_WITHOUT_SCHEMA; - static String CONTRACT_NAME = "java_sdk_schema_unit_test"; - static { try { MODULE_WITH_SCHEMA = WasmModule.from("./src/test/testresources/smartcontractschema/unit-test-with-schema.wasm"); @@ -38,7 +36,7 @@ public void shouldFindSchema() { Optional optionalSchema = MODULE_WITH_SCHEMA.getSchema(); assert(optionalSchema.isPresent()); Schema schema = optionalSchema.get(); - ReceiveName receiveName = ReceiveName.from(CONTRACT_NAME, "account_address_test"); + ReceiveName receiveName = ReceiveName.from("java_sdk_schema_unit_test", "account_address_test"); AccountAddress address = AccountAddress.from("3XSLuJcXg6xEua6iBPnWacc3iWh93yEDMCqX8FbE3RDSbEnT9P"); AccountAddressParam accountAddressParam = new AccountAddressParam(schema, receiveName, address); try {