From d8fecbc87cc7446397f505c86f0d778ce47992e3 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Chevalier Date: Fri, 15 Sep 2023 14:48:37 +0200 Subject: [PATCH] :sparkles: added static hex word class and constraint --- .../main/scala/cleareth/model/Address.scala | 6 ++-- .../main/scala/cleareth/model/BlockHash.scala | 6 ++-- .../main/scala/cleareth/model/HexWord.scala | 28 ++++++++++++++++ .../main/scala/cleareth/model/TxHash.scala | 6 ++-- model/src/main/scala/cleareth/model/Wei.scala | 6 ++-- .../cleareth/model/constraint/IsHexWord.scala | 14 ++++++++ .../{HexWord.scala => IsHexWordStrict.scala} | 8 ++--- .../constraint/{UInt.scala => IsUInt.scala} | 32 +++++++++---------- .../cleareth/model/constraint/package.scala | 2 ++ .../scala/cleareth/model/HexWordTest.scala | 24 ++++++++++++++ 10 files changed, 100 insertions(+), 32 deletions(-) create mode 100644 model/src/main/scala/cleareth/model/HexWord.scala create mode 100644 model/src/main/scala/cleareth/model/constraint/IsHexWord.scala rename model/src/main/scala/cleareth/model/constraint/{HexWord.scala => IsHexWordStrict.scala} (53%) rename model/src/main/scala/cleareth/model/constraint/{UInt.scala => IsUInt.scala} (80%) create mode 100644 model/src/test/scala/cleareth/model/HexWordTest.scala diff --git a/model/src/main/scala/cleareth/model/Address.scala b/model/src/main/scala/cleareth/model/Address.scala index f7d0c59..898d5f5 100644 --- a/model/src/main/scala/cleareth/model/Address.scala +++ b/model/src/main/scala/cleareth/model/Address.scala @@ -1,12 +1,12 @@ package cleareth.model -import cleareth.model.constraint.HexWord +import cleareth.model.constraint.IsHexWordStrict import io.github.iltotore.iron.* import scodec.bits.ByteVector /** This object represents an Ethereum Public address, which must be an hexadecimal string of 20 bytes */ -opaque type Address = ByteVector :| HexWord[20] +opaque type Address = ByteVector :| IsHexWordStrict[20] -object Address extends HexWord.Utils[20, Address]: +object Address extends IsHexWordStrict.Utils[20, Address]: val mint: Address = Address("0x0000000000000000000000000000000000000000") diff --git a/model/src/main/scala/cleareth/model/BlockHash.scala b/model/src/main/scala/cleareth/model/BlockHash.scala index 77ac8fb..cca5f8c 100644 --- a/model/src/main/scala/cleareth/model/BlockHash.scala +++ b/model/src/main/scala/cleareth/model/BlockHash.scala @@ -1,11 +1,11 @@ package cleareth.model -import cleareth.model.constraint.HexWord +import cleareth.model.constraint.IsHexWordStrict import io.github.iltotore.iron.* import scodec.bits.ByteVector /** This object represents an Ethereum Block hash, which must be an hexadecimal string of 32 bytes */ -opaque type BlockHash = ByteVector :| HexWord[32] +opaque type BlockHash = ByteVector :| IsHexWordStrict[32] -object BlockHash extends HexWord.Utils[32, BlockHash] +object BlockHash extends IsHexWordStrict.Utils[32, BlockHash] diff --git a/model/src/main/scala/cleareth/model/HexWord.scala b/model/src/main/scala/cleareth/model/HexWord.scala new file mode 100644 index 0000000..e0f67df --- /dev/null +++ b/model/src/main/scala/cleareth/model/HexWord.scala @@ -0,0 +1,28 @@ +package cleareth.model + +import cleareth.model.constraint.IsHexWord +import io.github.iltotore.iron.* +import scodec.bits.ByteVector + +/** This object represents a static hex word, which must be an hexadecimal string of at most 32 bytes + */ +opaque type HexWord = ByteVector :| IsHexWord + +object HexWord extends RefinedTypeOpsImpl[ByteVector, IsHexWord, HexWord]: + + inline def apply(inline bv: ByteVector): HexWord = applyUnsafe(bv) + inline def apply(inline str: String): HexWord = applyUnsafe(ByteVector.fromValidHex(str)) + + inline def from(inline bv: ByteVector): Option[HexWord] = option(bv) + inline def from(inline str: String): Option[HexWord] = + for + bv <- ByteVector.fromHex(str) + word <- option(bv) + yield word + + inline def fromDescriptive(inline bv: ByteVector): Either[String, HexWord] = either(bv) + inline def fromDescriptive(inline str: String): Either[String, HexWord] = + for + bv <- ByteVector.fromHexDescriptive(str) + word <- either(bv.padTo(32)) + yield word diff --git a/model/src/main/scala/cleareth/model/TxHash.scala b/model/src/main/scala/cleareth/model/TxHash.scala index 0e1e680..ff60fcb 100644 --- a/model/src/main/scala/cleareth/model/TxHash.scala +++ b/model/src/main/scala/cleareth/model/TxHash.scala @@ -1,11 +1,11 @@ package cleareth.model -import cleareth.model.constraint.HexWord +import cleareth.model.constraint.IsHexWordStrict import io.github.iltotore.iron.* import scodec.bits.ByteVector /** This object represents an Ethereum Transaction hash, which must be an hexadecimal string of 32 bytes */ -opaque type TxHash = ByteVector :| HexWord[32] +opaque type TxHash = ByteVector :| IsHexWordStrict[32] -object TxHash extends HexWord.Utils[32, TxHash] +object TxHash extends IsHexWordStrict.Utils[32, TxHash] diff --git a/model/src/main/scala/cleareth/model/Wei.scala b/model/src/main/scala/cleareth/model/Wei.scala index 1bace39..78b91b1 100644 --- a/model/src/main/scala/cleareth/model/Wei.scala +++ b/model/src/main/scala/cleareth/model/Wei.scala @@ -1,9 +1,9 @@ package cleareth.model -import cleareth.model.constraint.UInt +import cleareth.model.constraint.IsUInt import io.github.iltotore.iron.* import scodec.bits.ByteVector -opaque type Wei = ByteVector :| UInt[256] +opaque type Wei = ByteVector :| IsUInt[256] -object Wei extends UInt.Utils[256, Wei] +object Wei extends IsUInt.Utils[256, Wei] diff --git a/model/src/main/scala/cleareth/model/constraint/IsHexWord.scala b/model/src/main/scala/cleareth/model/constraint/IsHexWord.scala new file mode 100644 index 0000000..55f09cb --- /dev/null +++ b/model/src/main/scala/cleareth/model/constraint/IsHexWord.scala @@ -0,0 +1,14 @@ +package cleareth.model.constraint + +import io.github.iltotore.iron.* +import scala.compiletime.constValue +import scodec.bits.ByteVector + +final class IsHexWord + +object IsHexWord: + trait Utils[L <: ByteLength, IT <: ByteVector :| IsHexWord] extends ByteVectorUtils[IsHexWord, IT] + + given [L <: ByteLength]: Constraint[ByteVector, IsHexWord] with + inline def test(value: ByteVector): Boolean = value.length <= EVM_WORD_LENGTH + inline def message: String = s"Expecting at most $EVM_WORD_LENGTH bytes" diff --git a/model/src/main/scala/cleareth/model/constraint/HexWord.scala b/model/src/main/scala/cleareth/model/constraint/IsHexWordStrict.scala similarity index 53% rename from model/src/main/scala/cleareth/model/constraint/HexWord.scala rename to model/src/main/scala/cleareth/model/constraint/IsHexWordStrict.scala index 9d0e322..1d6e83a 100644 --- a/model/src/main/scala/cleareth/model/constraint/HexWord.scala +++ b/model/src/main/scala/cleareth/model/constraint/IsHexWordStrict.scala @@ -4,11 +4,11 @@ import io.github.iltotore.iron.* import scala.compiletime.constValue import scodec.bits.ByteVector -final class HexWord[L <: ByteLength] +final class IsHexWordStrict[L <: ByteLength] -object HexWord: - trait Utils[L <: ByteLength, IT <: ByteVector :| HexWord[L]] extends ByteVectorUtils[HexWord[L], IT] +object IsHexWordStrict: + trait Utils[L <: ByteLength, IT <: ByteVector :| IsHexWordStrict[L]] extends ByteVectorUtils[IsHexWordStrict[L], IT] - given [L <: ByteLength]: Constraint[ByteVector, HexWord[L]] with + given [L <: ByteLength]: Constraint[ByteVector, IsHexWordStrict[L]] with inline def test(value: ByteVector): Boolean = value.length == constValue[L] inline def message: String = s"Expecting ${constValue[L]} bytes" diff --git a/model/src/main/scala/cleareth/model/constraint/UInt.scala b/model/src/main/scala/cleareth/model/constraint/IsUInt.scala similarity index 80% rename from model/src/main/scala/cleareth/model/constraint/UInt.scala rename to model/src/main/scala/cleareth/model/constraint/IsUInt.scala index b944161..5abc309 100644 --- a/model/src/main/scala/cleareth/model/constraint/UInt.scala +++ b/model/src/main/scala/cleareth/model/constraint/IsUInt.scala @@ -4,59 +4,59 @@ import io.github.iltotore.iron.* import scala.compiletime.constValue import scodec.bits.ByteVector -final class UInt[L <: BitLength] +final class IsUInt[L <: BitLength] -object UInt: +object IsUInt: - trait Utils[L <: BitLength, IT <: ByteVector :| UInt[L]] extends ByteVectorUtils[UInt[L], IT]: - inline def apply(inline short: Short)(using Constraint[ByteVector, UInt[L]]): IT = + trait Utils[L <: BitLength, IT <: ByteVector :| IsUInt[L]] extends ByteVectorUtils[IsUInt[L], IT]: + inline def apply(inline short: Short)(using Constraint[ByteVector, IsUInt[L]]): IT = require(short >= 0, s"supplied short should be positive, got $short") applyUnsafe(ByteVector.fromShort(short)) - inline def apply(inline int: Int)(using Constraint[ByteVector, UInt[L]]): IT = + inline def apply(inline int: Int)(using Constraint[ByteVector, IsUInt[L]]): IT = require(int >= 0, s"supplied int should be positive, got $int") applyUnsafe(ByteVector.fromInt(int)) - inline def apply(inline long: Long)(using Constraint[ByteVector, UInt[L]]): IT = + inline def apply(inline long: Long)(using Constraint[ByteVector, IsUInt[L]]): IT = require(long >= 0, s"supplied long should be positive, got $long") applyUnsafe(ByteVector.fromLong(long)) - inline def apply(inline bigInt: BigInt)(using Constraint[ByteVector, UInt[L]]): IT = + inline def apply(inline bigInt: BigInt)(using Constraint[ByteVector, IsUInt[L]]): IT = require(bigInt >= 0, s"supplied bigInt should be positive, got $bigInt") applyUnsafe(ByteVector(bigInt.toByteArray).dropWhile(_ == 0)) - inline def from(inline short: Short)(using Constraint[ByteVector, UInt[L]]): Option[IT] = + inline def from(inline short: Short)(using Constraint[ByteVector, IsUInt[L]]): Option[IT] = require(short >= 0, s"supplied short should be positive, got $short") option(ByteVector.fromShort(short)) - inline def from(inline int: Int)(using Constraint[ByteVector, UInt[L]]): Option[IT] = + inline def from(inline int: Int)(using Constraint[ByteVector, IsUInt[L]]): Option[IT] = require(int >= 0, s"supplied int should be positive, got $int") option(ByteVector.fromInt(int)) - inline def from(inline long: Long)(using Constraint[ByteVector, UInt[L]]): Option[IT] = + inline def from(inline long: Long)(using Constraint[ByteVector, IsUInt[L]]): Option[IT] = require(long >= 0, s"supplied long should be positive, got $long") option(ByteVector.fromLong(long)) - inline def from(inline bigInt: BigInt)(using Constraint[ByteVector, UInt[L]]): Option[IT] = + inline def from(inline bigInt: BigInt)(using Constraint[ByteVector, IsUInt[L]]): Option[IT] = require(bigInt >= 0, s"supplied bigInt should be positive, got $bigInt") option(ByteVector(bigInt.toByteArray).dropWhile(_ == 0)) - inline def fromDescriptive(inline short: Short)(using Constraint[ByteVector, UInt[L]]): Either[String, IT] = + inline def fromDescriptive(inline short: Short)(using Constraint[ByteVector, IsUInt[L]]): Either[String, IT] = require(short >= 0, s"supplied short should be positive, got $short") either(ByteVector.fromShort(short)) - inline def fromDescriptive(inline int: Int)(using Constraint[ByteVector, UInt[L]]): Either[String, IT] = + inline def fromDescriptive(inline int: Int)(using Constraint[ByteVector, IsUInt[L]]): Either[String, IT] = require(int >= 0, s"supplied int should be positive, got $int") either(ByteVector.fromInt(int)) - inline def fromDescriptive(inline long: Long)(using Constraint[ByteVector, UInt[L]]): Either[String, IT] = + inline def fromDescriptive(inline long: Long)(using Constraint[ByteVector, IsUInt[L]]): Either[String, IT] = require(long >= 0, s"supplied long should be positive, got $long") either(ByteVector.fromLong(long)) - inline def fromDescriptive(inline bigInt: BigInt)(using Constraint[ByteVector, UInt[L]]): Either[String, IT] = + inline def fromDescriptive(inline bigInt: BigInt)(using Constraint[ByteVector, IsUInt[L]]): Either[String, IT] = require(bigInt >= 0, s"supplied bigInt should be positive, got $bigInt") either(ByteVector(bigInt.toByteArray).dropWhile(_ == 0)) - given [L <: BitLength]: Constraint[ByteVector, UInt[L]] with + given [L <: BitLength]: Constraint[ByteVector, IsUInt[L]] with inline def test(value: ByteVector): Boolean = value.length * 8 <= constValue[L] inline def message: String = s"Expecting ${constValue[L]} bits" diff --git a/model/src/main/scala/cleareth/model/constraint/package.scala b/model/src/main/scala/cleareth/model/constraint/package.scala index 91bbb6b..807322d 100644 --- a/model/src/main/scala/cleareth/model/constraint/package.scala +++ b/model/src/main/scala/cleareth/model/constraint/package.scala @@ -4,3 +4,5 @@ package object constraint: type BitLength = 8 | 16 | 32 | 64 | 128 | 256 type ByteLength = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 + + val EVM_WORD_LENGTH = 32 diff --git a/model/src/test/scala/cleareth/model/HexWordTest.scala b/model/src/test/scala/cleareth/model/HexWordTest.scala new file mode 100644 index 0000000..76c9b85 --- /dev/null +++ b/model/src/test/scala/cleareth/model/HexWordTest.scala @@ -0,0 +1,24 @@ +package cleareth.model + +import munit.FunSuite +import scodec.bits.ByteVector + +class HexWordTest extends FunSuite: + + test(s"creation from a valid hexadecimal string should succeed (length < 64)") { + HexWord("0xdeadbeefdeadbeef") + } + + test(s"creation from a valid hexadecimal string should succeed (`0x` string)") { + HexWord("0x") + } + + test(s"creation from a valid hexadecimal string should succeed (length = 64)") { + HexWord("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + } + + test(s"creation from a valid hexadecimal string should fail (length > 64)") { + interceptMessage[IllegalArgumentException]( + "Expecting at most 32 bytes" + )(HexWord("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")) + }