From 69b71ef09979ea26967bd82d4e8cc27741256078 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Thu, 29 Feb 2024 16:44:39 +0100 Subject: [PATCH 001/109] Fix `TransactionVersion` test cases --- lib/src/test/kotlin/network/account/AccountTest.kt | 2 +- lib/src/test/kotlin/network/provider/ProviderTest.kt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/test/kotlin/network/account/AccountTest.kt b/lib/src/test/kotlin/network/account/AccountTest.kt index 74fc00777..8344790a7 100644 --- a/lib/src/test/kotlin/network/account/AccountTest.kt +++ b/lib/src/test/kotlin/network/account/AccountTest.kt @@ -479,7 +479,7 @@ class AccountTest { nonce = Felt.ZERO, forFeeEstimate = true, ) - assertEquals(payloadForFeeEstimation.version, Felt(BigInteger("340282366920938463463374607431768211457"))) + assertEquals(Felt.fromHex("0x100000000000000000000000000000001"), payloadForFeeEstimation.version.value) val feePayload = provider.getEstimateFee(listOf(payloadForFeeEstimation)).send() assertTrue(feePayload.first().overallFee.value > Felt.ONE.value) diff --git a/lib/src/test/kotlin/network/provider/ProviderTest.kt b/lib/src/test/kotlin/network/provider/ProviderTest.kt index cec2920ea..a317ed8d5 100644 --- a/lib/src/test/kotlin/network/provider/ProviderTest.kt +++ b/lib/src/test/kotlin/network/provider/ProviderTest.kt @@ -140,7 +140,7 @@ class ProviderTest { val transactionHash = deployAccountV1TransactionHash val tx = provider.getTransaction(transactionHash).send() assertEquals(transactionHash, tx.hash) - assertEquals(Felt.ONE, tx.version) + assertEquals(TransactionVersion.V1, tx.version) val receiptRequest = provider.getTransactionReceipt(transactionHash) val receipt = receiptRequest.send() @@ -161,7 +161,7 @@ class ProviderTest { val tx = provider.getTransaction(transactionHash).send() assertEquals(transactionHash, tx.hash) - assertEquals(Felt(3), tx.version) + assertEquals(TransactionVersion.V3, tx.version) val receiptRequest = provider.getTransactionReceipt(transactionHash) val receipt = receiptRequest.send() @@ -223,7 +223,7 @@ class ProviderTest { assertTrue(tx is InvokeTransaction) assertEquals(transactionHash, tx.hash) assertEquals(TransactionType.INVOKE, tx.type) - assertEquals(Felt(3), tx.version) + assertEquals(TransactionVersion.V3, tx.version) val receiptRequest = provider.getTransactionReceipt(transactionHash) val receipt = receiptRequest.send() @@ -245,7 +245,7 @@ class ProviderTest { val tx = provider.getTransaction(transactionHash).send() as DeclareTransactionV0 assertEquals(transactionHash, tx.hash) assertNotEquals(Felt.ZERO, tx.classHash) - assertEquals(Felt.ZERO, tx.version) + assertEquals(TransactionVersion.V0, tx.version) val receiptRequest = provider.getTransactionReceipt(transactionHash) val receipt = receiptRequest.send() From 7e09e80d77f8d9481ba9b290df05362e63899558 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 1 Mar 2024 11:46:19 +0100 Subject: [PATCH 002/109] Use `Transaction.VX_QUERY` in estimate fee tests instead of checking raw value --- .../test/kotlin/network/account/AccountTest.kt | 2 +- .../starknet/account/StandardAccountTest.kt | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/src/test/kotlin/network/account/AccountTest.kt b/lib/src/test/kotlin/network/account/AccountTest.kt index 8344790a7..f367dd93c 100644 --- a/lib/src/test/kotlin/network/account/AccountTest.kt +++ b/lib/src/test/kotlin/network/account/AccountTest.kt @@ -479,7 +479,7 @@ class AccountTest { nonce = Felt.ZERO, forFeeEstimate = true, ) - assertEquals(Felt.fromHex("0x100000000000000000000000000000001"), payloadForFeeEstimation.version.value) + assertEquals(TransactionVersion.V1_QUERY, payloadForFeeEstimation.version) val feePayload = provider.getEstimateFee(listOf(payloadForFeeEstimation)).send() assertTrue(feePayload.first().overallFee.value > Felt.ONE.value) diff --git a/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt b/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt index 83fd64309..0509153d5 100644 --- a/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt +++ b/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt @@ -177,8 +177,8 @@ class StandardAccountTest { params = InvokeParamsV3(nonce.value.add(BigInteger.ONE).toFelt, ResourceBounds.ZERO), forFeeEstimate = true, ) - assertEquals(Felt.fromHex("0x100000000000000000000000000000001"), invokeTxV1Payload.version.value) - assertEquals(Felt.fromHex("0x100000000000000000000000000000003"), invokeTxV3Payload.version.value) + assertEquals(TransactionVersion.V1_QUERY, invokeTxV1Payload.version) + assertEquals(TransactionVersion.V3_QUERY, invokeTxV3Payload.version) val invokeTxV1PayloadWithoutSignature = invokeTxV1Payload.copy(signature = emptyList()) val invokeTxV3PayloadWithoutSignature = invokeTxV3Payload.copy(signature = emptyList()) @@ -226,7 +226,7 @@ class StandardAccountTest { forFeeEstimate = true, ) - assertEquals(Felt.fromHex("0x100000000000000000000000000000001"), declareTransactionPayload.version.value) + assertEquals(TransactionVersion.V1_QUERY, declareTransactionPayload.version) val request = provider.getEstimateFee(payload = listOf(declareTransactionPayload), simulationFlags = emptySet()) val feeEstimate = request.send().first() @@ -251,7 +251,7 @@ class StandardAccountTest { forFeeEstimate = true, ) - assertEquals(Felt.fromHex("0x100000000000000000000000000000002"), declareTransactionPayload.version.value) + assertEquals(TransactionVersion.V2_QUERY, declareTransactionPayload.version) val request = provider.getEstimateFee(payload = listOf(declareTransactionPayload), simulationFlags = emptySet()) val feeEstimate = request.send().first() @@ -276,7 +276,7 @@ class StandardAccountTest { true, ) - assertEquals(Felt.fromHex("0x100000000000000000000000000000003"), declareTransactionPayload.version.value) + assertEquals(TransactionVersion.V3_QUERY, declareTransactionPayload.version) val request = provider.getEstimateFee(payload = listOf(declareTransactionPayload), simulationFlags = emptySet()) val feeEstimate = request.send().first() @@ -821,7 +821,7 @@ class StandardAccountTest { nonce = Felt.ZERO, forFeeEstimate = true, ) - assertEquals(Felt.fromHex("0x100000000000000000000000000000001"), payloadForFeeEstimation.version.value) + assertEquals(TransactionVersion.V1_QUERY, payloadForFeeEstimation.version) val feePayload = provider.getEstimateFee(listOf(payloadForFeeEstimation)).send() assertTrue(feePayload.first().overallFee.value > Felt.ONE.value) @@ -857,7 +857,7 @@ class StandardAccountTest { forFeeEstimate = true, ) - assertEquals(Felt.fromHex("0x100000000000000000000000000000003"), payloadForFeeEstimation.version.value) + assertEquals(TransactionVersion.V3_QUERY, payloadForFeeEstimation.version) val feePayload = provider.getEstimateFee(listOf(payloadForFeeEstimation)).send() assertTrue(feePayload.first().overallFee.value > Felt.ONE.value) @@ -1008,7 +1008,7 @@ class StandardAccountTest { nonce = Felt.ONE, forFeeEstimate = true, ) - assertEquals(Felt.fromHex("0x100000000000000000000000000000001"), payloadForFeeEstimation.version.value) + assertEquals(TransactionVersion.V1_QUERY, payloadForFeeEstimation.version) assertThrows(RequestFailedException::class.java) { provider.deployAccount(payloadForFeeEstimation).send() From 598a0cb33c458c48d6b2bf3f3268057b6fb87644 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 1 Mar 2024 12:21:46 +0100 Subject: [PATCH 003/109] Add `TypedDataRevision` enum --- .../main/kotlin/com/swmansion/starknet/data/TypedData.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 4bb6dc384..35921ff66 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -7,6 +7,12 @@ import com.swmansion.starknet.data.types.MerkleTree import kotlinx.serialization.Serializable import kotlinx.serialization.json.* +@Serializable +enum class TypedDataRevision(val value: Felt) { + V0(Felt.ZERO), + V1(Felt.ONE), +} + /** * Sign message for off-chain usage. Follows standard proposed [here](https://github.com/argentlabs/argent-x/discussions/14). * From e016d688ff8713461ececb17470eebd38861aa15 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 1 Mar 2024 12:27:51 +0100 Subject: [PATCH 004/109] Add docstring for `TypedDataRevision` --- .../main/kotlin/com/swmansion/starknet/data/TypedData.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 35921ff66..9cef7abf7 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -7,6 +7,14 @@ import com.swmansion.starknet.data.types.MerkleTree import kotlinx.serialization.Serializable import kotlinx.serialization.json.* +/** + * TypedData revision. + * + * The revision of the specification to be used. + * + * [V0] - Legacy revision, represents the de facto spec before [SNIP-12](https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-12.md) was published. + * [V1] - Initial and current revision, represents the spec after [SNIP-12](https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-12.md) was published. + */ @Serializable enum class TypedDataRevision(val value: Felt) { V0(Felt.ZERO), From 0744947fb2e8fcfb2cea100553592e11be406ada Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 1 Mar 2024 12:33:29 +0100 Subject: [PATCH 005/109] Add `EnumType` --- .../main/kotlin/com/swmansion/starknet/data/TypedData.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 9cef7abf7..5f6d8276c 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -137,6 +137,13 @@ data class TypedData private constructor( val contains: String, ) : TypeBase() + @Serializable + data class EnumType( + override val name: String, + override val type: String = "enum", + val contains: String, + ) : TypeBase() + data class Context( val parent: String?, val key: String?, From c4061c61ec339896e0315b9e68561154ee40cb72 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 1 Mar 2024 12:35:04 +0100 Subject: [PATCH 006/109] Update serializer to support `EnumType` --- .../starknet/data/serializers/TypedDataTypeBaseSerializer.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TypedDataTypeBaseSerializer.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TypedDataTypeBaseSerializer.kt index 5327a394b..b00bd00ad 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TypedDataTypeBaseSerializer.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TypedDataTypeBaseSerializer.kt @@ -11,6 +11,7 @@ internal object TypedDataTypeBaseSerializer : JsonContentPolymorphicSerializer TypedData.MerkleTreeType.serializer() + "enum" -> TypedData.EnumType.serializer() is String -> TypedData.Type.serializer() null -> throw IllegalArgumentException("Input element does not contain mandatory field 'type'") else -> throw IllegalArgumentException("Unknown type '$type'") From 19631339e465db00b979a4b45864df5632b9fbb7 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sat, 2 Mar 2024 14:56:03 +0100 Subject: [PATCH 007/109] Fix `TypedDataRevision` serialization --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 5f6d8276c..f20e72fdd 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -4,6 +4,7 @@ import com.swmansion.starknet.crypto.StarknetCurve import com.swmansion.starknet.data.serializers.TypedDataTypeBaseSerializer import com.swmansion.starknet.data.types.Felt import com.swmansion.starknet.data.types.MerkleTree +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.* @@ -17,7 +18,10 @@ import kotlinx.serialization.json.* */ @Serializable enum class TypedDataRevision(val value: Felt) { + @SerialName("0") V0(Felt.ZERO), + + @SerialName("1") V1(Felt.ONE), } From 105fbca1f160d579dbd1418f8ba77bb63dcb5520 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sat, 2 Mar 2024 14:56:34 +0100 Subject: [PATCH 008/109] Reference SNIP-12 as a standard in TypedData docstring --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index f20e72fdd..6f02961db 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -26,7 +26,7 @@ enum class TypedDataRevision(val value: Felt) { } /** - * Sign message for off-chain usage. Follows standard proposed [here](https://github.com/argentlabs/argent-x/discussions/14). + * Sign message for off-chain usage. Follows standard proposed [here](https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-12.md). * * ```java * String typedDataString = """ From 814e54967deb80de3810604992c9c21113ab6c82 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sat, 2 Mar 2024 14:57:36 +0100 Subject: [PATCH 009/109] Add `Domain` helper class --- .../main/kotlin/com/swmansion/starknet/data/TypedData.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 6f02961db..59be6b8bd 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -122,6 +122,14 @@ data class TypedData private constructor( message = Json.parseToJsonElement(message).jsonObject, ) + @Serializable + data class Domain( + val name: String, + val version: String, + val chainId: String, + val revision: TypedDataRevision? = null, + ) + @Serializable(with = TypedDataTypeBaseSerializer::class) sealed class TypeBase { abstract val name: String From 76d76b4b2b4c3f242eb76688ba5fa494fda05981 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sat, 2 Mar 2024 15:00:05 +0100 Subject: [PATCH 010/109] Update type verification logic in `TypedData` - Add `revision`, `domainObjectName` helper fields to `TypedData` - Add new restrictions on types introduced in SNIP-12 - Move versioned `reservedTypes` to companion object --- .../com/swmansion/starknet/data/TypedData.kt | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 59be6b8bd..8db265bfd 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -103,10 +103,33 @@ data class TypedData private constructor( val domain: JsonObject, val message: JsonObject, ) { + private val revision: TypedDataRevision = Json.decodeFromJsonElement(domain).revision ?: TypedDataRevision.V0 + + private val domainObjectName: String = when (revision) { + TypedDataRevision.V0 -> "StarkNetDomain" + TypedDataRevision.V1 -> "StarknetDomain" + } init { - val reservedTypeNames = listOf("felt", "felt*", "string", "string*", "selector", "selector*", "merkletree", "merkletree*", "raw", "raw*") - reservedTypeNames.forEach { - require(!types.containsKey(it)) { "Types must not contain $it." } + verifyTypes() + } + + private fun verifyTypes() { + val reservedTypes = when (revision) { + TypedDataRevision.V0 -> reservedTypesV0 + TypedDataRevision.V1 -> reservedTypesV1 + } + reservedTypes.forEach { require(it !in types) { "Types must not contain $it." } } + + require(domainObjectName in types) { "Types must contain $domainObjectName." } + + val referencedTypes = types.values.flatten().map { it.type } + + types.keys.forEach { + require(it.isNotEmpty()) { "Types cannot be empty." } + require(!it.endsWith("*")) { "Types cannot end in *. $it was found." } + require(!it.startsWith("(") || !it.endsWith(")")) { "Types cannot be enclosed in parenthesis. $it was found." } + require(!it.contains(",")) { "Types cannot contain commas. $it was found." } + require(it in referencedTypes) { "Dangling types are not allowed. Unreferenced type $it was found." } } } @@ -332,6 +355,10 @@ data class TypedData private constructor( } companion object { + private val reservedTypesV0 by lazy { listOf("felt", "bool", "string", "selector", "merkletree", "raw") } + + private val reservedTypesV1 by lazy { reservedTypesV0 + listOf("enum", "bool", "u128", "ContractAddress", "ClassHash", "timestamp", "shortstring") + listOf("u256", "NftId", "TokenAmount") } + /** * Create TypedData from JSON string. * From 0bfb4ab27d6fae33f21d713f2fbb921c67a65576 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sun, 3 Mar 2024 15:45:24 +0100 Subject: [PATCH 011/109] Remove redundand typing --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 8db265bfd..887c079f0 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -103,9 +103,9 @@ data class TypedData private constructor( val domain: JsonObject, val message: JsonObject, ) { - private val revision: TypedDataRevision = Json.decodeFromJsonElement(domain).revision ?: TypedDataRevision.V0 + private val revision = Json.decodeFromJsonElement(domain).revision ?: TypedDataRevision.V0 - private val domainObjectName: String = when (revision) { + private val domainObjectName = when (revision) { TypedDataRevision.V0 -> "StarkNetDomain" TypedDataRevision.V1 -> "StarknetDomain" } From 336073edf518d87320576b939d7f1ba21a1f696f Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sun, 3 Mar 2024 17:45:59 +0100 Subject: [PATCH 012/109] Fix Domain serialization - All fields handled as JsonPrimitives - Allow `chainId` and `chain_id` (should be verified if `chain_id` is valid for revision 0) --- .../kotlin/com/swmansion/starknet/data/TypedData.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 887c079f0..41636e308 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -1,9 +1,11 @@ package com.swmansion.starknet.data +import com.swmansion.starknet.crypto.Poseidon import com.swmansion.starknet.crypto.StarknetCurve import com.swmansion.starknet.data.serializers.TypedDataTypeBaseSerializer import com.swmansion.starknet.data.types.Felt import com.swmansion.starknet.data.types.MerkleTree +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.* @@ -145,11 +147,16 @@ data class TypedData private constructor( message = Json.parseToJsonElement(message).jsonObject, ) + @OptIn(ExperimentalSerializationApi::class) @Serializable data class Domain( - val name: String, - val version: String, - val chainId: String, + val name: JsonPrimitive, + + val version: JsonPrimitive, + + @JsonNames("chain_id", "chainId") + val chainId: JsonPrimitive, + val revision: TypedDataRevision? = null, ) From 3a4c9e0432ca17fe37905a556b361d89a23f079e Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sun, 3 Mar 2024 17:52:13 +0100 Subject: [PATCH 013/109] Fix dangling types check - Add type specific references, arrays - Add `extractEnumTypes` helper - Adjust tests to not have any dangling types --- .../com/swmansion/starknet/data/TypedData.kt | 14 +++++++++++++- lib/src/test/kotlin/starknet/data/TypedDataTest.kt | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 41636e308..b167545ff 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -124,7 +124,13 @@ data class TypedData private constructor( require(domainObjectName in types) { "Types must contain $domainObjectName." } - val referencedTypes = types.values.flatten().map { it.type } + val referencedTypes = domainObjectName + primaryType + types.values.flatten().flatMap { + when (it) { + is EnumType -> extractEnumTypes(it.type) + it.contains + is MerkleTreeType -> listOf(it.contains) + is Type -> listOf(stripPointer(it.type)) + } + }.distinct() types.keys.forEach { require(it.isNotEmpty()) { "Types cannot be empty." } @@ -346,6 +352,12 @@ data class TypedData private constructor( return value.removeSuffix("*") } + private fun extractEnumTypes(value: String): List { + val enumPattern = Regex("""\(\s*([^,]+)\s*\)""") + val matches = enumPattern.findAll(value) + return matches.map { it.groupValues[1].trim() }.toList() + } + fun getStructHash(typeName: String, data: String): Felt { val encodedData = encodeData(typeName, Json.parseToJsonElement(data).jsonObject) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 69d2470de..715766bab 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -222,6 +222,7 @@ internal class TypedDataTest { ) val invalidTypedData = TD_SESSION.copy( types = TD_SESSION.types.toMutableMap().apply { + remove("Policy") val types = this["Session"]!!.toMutableList() types.apply { this[this.indexOfFirst { it.type == "merkletree" }] = invalidMerkleTreeType From 60ccd6c9e5ac017b31cabdddd40e4f72c6d75acc Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sun, 3 Mar 2024 19:30:32 +0100 Subject: [PATCH 014/109] Update `MerkleTree` to allow `Poseidon` hashing - Add `MerkleHashFunction` enum - `MerkleTree` now accepts `hashFunction` as an optinal argument - (breaking) `MerkleTree.hash` now requires `hashFunction` to be specified --- .../starknet/data/types/MerkleTree.kt | 20 ++++- .../starknet/data/types/MerkleTreeTest.kt | 86 ++++++++++++------- 2 files changed, 70 insertions(+), 36 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/MerkleTree.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/MerkleTree.kt index 66e45b8d4..660b22555 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/types/MerkleTree.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/MerkleTree.kt @@ -1,9 +1,21 @@ package com.swmansion.starknet.data.types +import com.swmansion.starknet.crypto.Poseidon import com.swmansion.starknet.crypto.StarknetCurve -data class MerkleTree( +enum class MerkleHashFunction(private val hashFunction: (Felt, Felt) -> Felt) { + PEDERSEN(StarknetCurve::pedersen), + POSEIDON(Poseidon::poseidonHash), + ; + + fun hash(first: Felt, second: Felt): Felt { + return hashFunction.invoke(first, second) + } +} + +data class MerkleTree @JvmOverloads constructor( val leafHashes: List, + val hashFunction: MerkleHashFunction = MerkleHashFunction.PEDERSEN, ) { private val buildResult = build(leafHashes) @@ -12,9 +24,9 @@ data class MerkleTree( companion object { @JvmStatic - fun hash(a: Felt, b: Felt): Felt { + fun hash(a: Felt, b: Felt, hashFunction: MerkleHashFunction): Felt { val (aSorted, bSorted) = if (a < b) Pair(a, b) else Pair(b, a) - return StarknetCurve.pedersen(aSorted, bSorted) + return hashFunction.hash(aSorted, bSorted) } } @@ -32,7 +44,7 @@ data class MerkleTree( false -> branches } val newLeaves = leaves.indices.step(2).map { - hash(leaves[it], leaves.getOrElse(it + 1) { Felt.ZERO }) + hash(leaves[it], leaves.getOrElse(it + 1) { Felt.ZERO }, hashFunction) } return build(newLeaves, newBranches) diff --git a/lib/src/test/kotlin/starknet/data/types/MerkleTreeTest.kt b/lib/src/test/kotlin/starknet/data/types/MerkleTreeTest.kt index 29088539c..f5b20ab68 100644 --- a/lib/src/test/kotlin/starknet/data/types/MerkleTreeTest.kt +++ b/lib/src/test/kotlin/starknet/data/types/MerkleTreeTest.kt @@ -1,30 +1,40 @@ package starknet.data.types +import com.swmansion.starknet.crypto.Poseidon import com.swmansion.starknet.crypto.StarknetCurve import com.swmansion.starknet.data.types.Felt +import com.swmansion.starknet.data.types.MerkleHashFunction import com.swmansion.starknet.data.types.MerkleTree import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource internal class MerkleTreeTest { - @Test - fun `calculate hashes`() { + @ParameterizedTest + @EnumSource(MerkleHashFunction::class) + fun `calculate hashes`(hashFunction: MerkleHashFunction) { + val applyHash: (Felt, Felt) -> Felt = when (hashFunction) { + MerkleHashFunction.PEDERSEN -> StarknetCurve::pedersen + MerkleHashFunction.POSEIDON -> Poseidon::poseidonHash + } + val leaves = listOf( Felt.fromHex("0x12"), Felt.fromHex("0xa"), ) - val merkleHash = MerkleTree.hash(leaves[0], leaves[1]) - val rawHash = StarknetCurve.pedersen(leaves[1], leaves[0]) + val merkleHash = MerkleTree.hash(leaves[0], leaves[1], hashFunction) + val rawHash = applyHash(leaves[1], leaves[0]) assertEquals(rawHash, merkleHash) val leaves2 = listOf( Felt.fromHex("0x5bb9440e27889a364bcb678b1f679ecd1347acdedcbf36e83494f857cc58026"), Felt.fromHex("0x3"), ) - val merkleHash2 = MerkleTree.hash(leaves2[0], leaves2[1]) - val rawHash2 = StarknetCurve.pedersen(leaves2[1], leaves2[0]) + val merkleHash2 = MerkleTree.hash(leaves2[0], leaves2[1], hashFunction) + val rawHash2 = applyHash(leaves2[1], leaves2[0]) assertEquals(rawHash2, merkleHash2) } @@ -39,10 +49,11 @@ internal class MerkleTreeTest { @Nested inner class BuildFromElementsTest { - @Test - fun `build merkle tree from 1 element`() { + @ParameterizedTest + @EnumSource(MerkleHashFunction::class) + fun `build merkle tree from 1 element`(hashFunction: MerkleHashFunction) { val leaves = listOf(Felt.ONE) - val tree = MerkleTree(leaves) + val tree = MerkleTree(leaves, hashFunction) val manualMerkleRootHash = leaves[0] @@ -50,64 +61,75 @@ internal class MerkleTreeTest { assertEquals(manualMerkleRootHash, tree.rootHash) } - @Test - fun `build merkle tree from 2 elements`() { + @ParameterizedTest + @EnumSource(MerkleHashFunction::class) + fun `build merkle tree from 2 elements`(hashFunction: MerkleHashFunction) { val leaves = listOf(Felt.ONE, Felt.fromHex("0x2")) - val tree = MerkleTree(leaves) + val tree = MerkleTree(leaves, hashFunction) - val manualMerkleRootHash = MerkleTree.hash(leaves[0], leaves[1]) + val manualMerkleRootHash = MerkleTree.hash(leaves[0], leaves[1], hashFunction) assertEquals(0, tree.branches.size) assertEquals(manualMerkleRootHash, tree.rootHash) } - @Test - fun `build merkle tree from 4 elements`() { + @ParameterizedTest + @EnumSource(MerkleHashFunction::class) + fun `build merkle tree from 4 elements`(hashFunction: MerkleHashFunction) { val leaves = (1..4).map { Felt.fromHex("0x$it") } - val tree = MerkleTree(leaves) + val tree = MerkleTree(leaves, hashFunction) val manualMerkleRootHash = MerkleTree.hash( - MerkleTree.hash(leaves[0], leaves[1]), - MerkleTree.hash(leaves[2], leaves[3]), + MerkleTree.hash(leaves[0], leaves[1], hashFunction), + MerkleTree.hash(leaves[2], leaves[3], hashFunction), + hashFunction, ) assertEquals(1, tree.branches.size) assertEquals(manualMerkleRootHash, tree.rootHash) } - @Test - fun `build merkle tree from 6 elements`() { + @ParameterizedTest + @EnumSource(MerkleHashFunction::class) + fun `build merkle tree from 6 elements`(hashFunction: MerkleHashFunction) { val leaves = (1..6).map { Felt.fromHex("0x$it") } - val tree = MerkleTree(leaves) + val tree = MerkleTree(leaves, hashFunction) val manualMerkleRootHash = MerkleTree.hash( MerkleTree.hash( - MerkleTree.hash(leaves[0], leaves[1]), - MerkleTree.hash(leaves[2], leaves[3]), + MerkleTree.hash(leaves[0], leaves[1], hashFunction), + MerkleTree.hash(leaves[2], leaves[3], hashFunction), + hashFunction, ), MerkleTree.hash( - MerkleTree.hash(leaves[4], leaves[5]), - Felt.fromHex("0x0"), + MerkleTree.hash(leaves[4], leaves[5], hashFunction), + Felt.ZERO, + hashFunction, ), + hashFunction, ) assertEquals(2, tree.branches.size) assertEquals(manualMerkleRootHash, tree.rootHash) } - @Test - fun `build merkle tree from 7 elements`() { + @ParameterizedTest + @EnumSource(MerkleHashFunction::class) + fun `build merkle tree from 7 elements`(hashFunction: MerkleHashFunction) { val leaves = (1..7).map { Felt.fromHex("0x$it") } - val tree = MerkleTree(leaves) + val tree = MerkleTree(leaves, hashFunction) val manualMerkleRootHash = MerkleTree.hash( MerkleTree.hash( - MerkleTree.hash(leaves[0], leaves[1]), - MerkleTree.hash(leaves[2], leaves[3]), + MerkleTree.hash(leaves[0], leaves[1], hashFunction), + MerkleTree.hash(leaves[2], leaves[3], hashFunction), + hashFunction, ), MerkleTree.hash( - MerkleTree.hash(leaves[4], leaves[5]), - MerkleTree.hash(leaves[6], Felt.ZERO), + MerkleTree.hash(leaves[4], leaves[5], hashFunction), + MerkleTree.hash(leaves[6], Felt.ZERO, hashFunction), + hashFunction, ), + hashFunction, ) assertEquals(2, tree.branches.size) From 1bfe2874cd597774259d3b9a5e90eac4e65cec2e Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 4 Mar 2024 12:30:04 +0100 Subject: [PATCH 015/109] Minor refactor of `TypedData` - Rename `domainObjectName` to `domainSeparatorName` - Format `referencedTypes` logic for better readability --- .../com/swmansion/starknet/data/TypedData.kt | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index b167545ff..7466c98e1 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -107,7 +107,7 @@ data class TypedData private constructor( ) { private val revision = Json.decodeFromJsonElement(domain).revision ?: TypedDataRevision.V0 - private val domainObjectName = when (revision) { + private val domainSeparatorName = when (revision) { TypedDataRevision.V0 -> "StarkNetDomain" TypedDataRevision.V1 -> "StarknetDomain" } @@ -122,15 +122,15 @@ data class TypedData private constructor( } reservedTypes.forEach { require(it !in types) { "Types must not contain $it." } } - require(domainObjectName in types) { "Types must contain $domainObjectName." } + require(domainSeparatorName in types) { "Types must contain $domainSeparatorName." } - val referencedTypes = domainObjectName + primaryType + types.values.flatten().flatMap { + val referencedTypes = types.values.flatten().flatMap { when (it) { is EnumType -> extractEnumTypes(it.type) + it.contains is MerkleTreeType -> listOf(it.contains) is Type -> listOf(stripPointer(it.type)) } - }.distinct() + }.distinct() + domainSeparatorName + primaryType types.keys.forEach { require(it.isNotEmpty()) { "Types cannot be empty." } @@ -183,7 +183,13 @@ data class TypedData private constructor( override val name: String, override val type: String = "merkletree", val contains: String, - ) : TypeBase() + ) : TypeBase() { + init { + require(!contains.endsWith("*")) { + "Merkletree 'contains' field cannot be an array, got '$contains' in type '$name'." + } + } + } @Serializable data class EnumType( @@ -275,9 +281,7 @@ data class TypedData private constructor( ?: throw IllegalArgumentException("Key '$key' is not defined in parent '$parent'.") require(merkleType is MerkleTreeType) { "Key '$key' in parent '$parent' is not a merkletree." } - require(!merkleType.contains.endsWith("*")) { - "Merkletree 'contains' field cannot be an array, got '${merkleType.contains}'." - } + merkleType.contains } else { "raw" From 61c333138d9073814b3cb634f51abe240d65d126 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 4 Mar 2024 12:44:12 +0100 Subject: [PATCH 016/109] Fix TypedDataTest --- .../kotlin/starknet/data/TypedDataTest.kt | 29 ++++--------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 715766bab..1a3aa2d22 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -209,29 +209,12 @@ internal class TypedDataTest { } @Test fun `merkletree with invalid contains`() { - val leaves = listOf( - mapOf("contractAddress" to "0x1", "selector" to "transfer"), - mapOf("contractAddress" to "0x2", "selector" to "transfer"), - mapOf("contractAddress" to "0x3", "selector" to "transfer"), - ) - - val invalidMerkleTreeType = MerkleTreeType( - name = "root", - type = "merkletree", - contains = "felt*", - ) - val invalidTypedData = TD_SESSION.copy( - types = TD_SESSION.types.toMutableMap().apply { - remove("Policy") - val types = this["Session"]!!.toMutableList() - types.apply { - this[this.indexOfFirst { it.type == "merkletree" }] = invalidMerkleTreeType - } - this["Session"] = types - }, - ) - assertThrows("Merkletree 'contains' field cannot be an array, got '${invalidMerkleTreeType.contains}.'") { - invalidTypedData.encodeValue("merkletree", Json.encodeToJsonElement(leaves), Context(parent = "Session", key = "root")) + assertThrows("Merkletree 'contains' field cannot be an array, got 'felt*' in type 'root'.") { + MerkleTreeType( + name = "root", + type = "merkletree", + contains = "felt*", + ) } } From 51f10dd77bf8930d1119d9cbebd5ed852ad1baa7 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 4 Mar 2024 12:52:46 +0100 Subject: [PATCH 017/109] Don't support `chainn_id` in `TypedData.Domain` - Remove @JsonNames from `TypedData.Domain` - Fix `TD_SESSION` test case in `TypedDataTest` - Use `chainId` instead of `chain_id` in `typed_data_session_example.json` --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 5 ----- lib/src/test/kotlin/starknet/data/TypedDataTest.kt | 2 +- .../resources/typed-data/typed_data_session_example.json | 4 ++-- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 7466c98e1..4008c711c 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -153,16 +153,11 @@ data class TypedData private constructor( message = Json.parseToJsonElement(message).jsonObject, ) - @OptIn(ExperimentalSerializationApi::class) @Serializable data class Domain( val name: JsonPrimitive, - val version: JsonPrimitive, - - @JsonNames("chain_id", "chainId") val chainId: JsonPrimitive, - val revision: TypedDataRevision? = null, ) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 1a3aa2d22..15a027c6b 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -117,7 +117,7 @@ internal class TypedDataTest { Arguments.of( TD_SESSION, "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", - "0x751fb7d98545f7649d0d0eadc80d770fcd88d8cfaa55590b284f4e1b701ef0a", + "0x5d28fa1b31f92e63022f7d85271606e52bed89c046c925f16b09e644dc99794", ), Arguments.of( TD_VALIDATE, diff --git a/lib/src/test/resources/typed-data/typed_data_session_example.json b/lib/src/test/resources/typed-data/typed_data_session_example.json index 75242e40a..37ace5514 100644 --- a/lib/src/test/resources/typed-data/typed_data_session_example.json +++ b/lib/src/test/resources/typed-data/typed_data_session_example.json @@ -13,13 +13,13 @@ "StarkNetDomain": [ { "name": "name", "type": "felt" }, { "name": "version", "type": "felt" }, - { "name": "chain_id", "type": "felt" } + { "name": "chainId", "type": "felt" } ] }, "domain": { "name": "StarkNet Mail", "version": "1", - "chain_id": 1 + "chainId": 1 }, "message": { "key": "0x0000000000000000000000000000000000000000000000000000000000000000", From dd59869b03363f3e0ba8ee35fbec88e27a05bd62 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 12:54:18 +0100 Subject: [PATCH 018/109] Format --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 4008c711c..fc5d89144 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -1,11 +1,9 @@ package com.swmansion.starknet.data -import com.swmansion.starknet.crypto.Poseidon import com.swmansion.starknet.crypto.StarknetCurve import com.swmansion.starknet.data.serializers.TypedDataTypeBaseSerializer import com.swmansion.starknet.data.types.Felt import com.swmansion.starknet.data.types.MerkleTree -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.* From 5b84b6d7ab89caa4e1d8cae1a391082b78b86cb8 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 13:05:36 +0100 Subject: [PATCH 019/109] Rename `MerkleHashFunction` -> `HashMethod` - Move to package com.swmansion.starknet.crypto - Implement using abstract function instead of stored lambda --- .../swmansion/starknet/crypto/HashMethod.kt | 18 +++++++++++ .../starknet/data/types/MerkleTree.kt | 17 ++--------- .../starknet/data/types/MerkleTreeTest.kt | 30 +++++++++---------- 3 files changed, 36 insertions(+), 29 deletions(-) create mode 100644 lib/src/main/kotlin/com/swmansion/starknet/crypto/HashMethod.kt diff --git a/lib/src/main/kotlin/com/swmansion/starknet/crypto/HashMethod.kt b/lib/src/main/kotlin/com/swmansion/starknet/crypto/HashMethod.kt new file mode 100644 index 000000000..01817be0c --- /dev/null +++ b/lib/src/main/kotlin/com/swmansion/starknet/crypto/HashMethod.kt @@ -0,0 +1,18 @@ +package com.swmansion.starknet.crypto + +import com.swmansion.starknet.data.types.Felt + +enum class HashMethod { + PEDERSEN { + override fun hash(first: Felt, second: Felt): Felt { + return StarknetCurve.pedersen(first, second) + } + }, + POSEIDON { + override fun hash(first: Felt, second: Felt): Felt { + return Poseidon.poseidonHash(first, second) + } + }, ; + + abstract fun hash(first: Felt, second: Felt): Felt +} diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/MerkleTree.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/MerkleTree.kt index 660b22555..b2581ab28 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/types/MerkleTree.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/MerkleTree.kt @@ -1,21 +1,10 @@ package com.swmansion.starknet.data.types -import com.swmansion.starknet.crypto.Poseidon -import com.swmansion.starknet.crypto.StarknetCurve - -enum class MerkleHashFunction(private val hashFunction: (Felt, Felt) -> Felt) { - PEDERSEN(StarknetCurve::pedersen), - POSEIDON(Poseidon::poseidonHash), - ; - - fun hash(first: Felt, second: Felt): Felt { - return hashFunction.invoke(first, second) - } -} +import com.swmansion.starknet.crypto.HashMethod data class MerkleTree @JvmOverloads constructor( val leafHashes: List, - val hashFunction: MerkleHashFunction = MerkleHashFunction.PEDERSEN, + val hashFunction: HashMethod = HashMethod.PEDERSEN, ) { private val buildResult = build(leafHashes) @@ -24,7 +13,7 @@ data class MerkleTree @JvmOverloads constructor( companion object { @JvmStatic - fun hash(a: Felt, b: Felt, hashFunction: MerkleHashFunction): Felt { + fun hash(a: Felt, b: Felt, hashFunction: HashMethod): Felt { val (aSorted, bSorted) = if (a < b) Pair(a, b) else Pair(b, a) return hashFunction.hash(aSorted, bSorted) } diff --git a/lib/src/test/kotlin/starknet/data/types/MerkleTreeTest.kt b/lib/src/test/kotlin/starknet/data/types/MerkleTreeTest.kt index f5b20ab68..a198dc32d 100644 --- a/lib/src/test/kotlin/starknet/data/types/MerkleTreeTest.kt +++ b/lib/src/test/kotlin/starknet/data/types/MerkleTreeTest.kt @@ -1,9 +1,9 @@ package starknet.data.types +import com.swmansion.starknet.crypto.HashMethod import com.swmansion.starknet.crypto.Poseidon import com.swmansion.starknet.crypto.StarknetCurve import com.swmansion.starknet.data.types.Felt -import com.swmansion.starknet.data.types.MerkleHashFunction import com.swmansion.starknet.data.types.MerkleTree import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested @@ -14,11 +14,11 @@ import org.junit.jupiter.params.provider.EnumSource internal class MerkleTreeTest { @ParameterizedTest - @EnumSource(MerkleHashFunction::class) - fun `calculate hashes`(hashFunction: MerkleHashFunction) { + @EnumSource(HashMethod::class) + fun `calculate hashes`(hashFunction: HashMethod) { val applyHash: (Felt, Felt) -> Felt = when (hashFunction) { - MerkleHashFunction.PEDERSEN -> StarknetCurve::pedersen - MerkleHashFunction.POSEIDON -> Poseidon::poseidonHash + HashMethod.PEDERSEN -> StarknetCurve::pedersen + HashMethod.POSEIDON -> Poseidon::poseidonHash } val leaves = listOf( @@ -50,8 +50,8 @@ internal class MerkleTreeTest { @Nested inner class BuildFromElementsTest { @ParameterizedTest - @EnumSource(MerkleHashFunction::class) - fun `build merkle tree from 1 element`(hashFunction: MerkleHashFunction) { + @EnumSource(HashMethod::class) + fun `build merkle tree from 1 element`(hashFunction: HashMethod) { val leaves = listOf(Felt.ONE) val tree = MerkleTree(leaves, hashFunction) @@ -62,8 +62,8 @@ internal class MerkleTreeTest { } @ParameterizedTest - @EnumSource(MerkleHashFunction::class) - fun `build merkle tree from 2 elements`(hashFunction: MerkleHashFunction) { + @EnumSource(HashMethod::class) + fun `build merkle tree from 2 elements`(hashFunction: HashMethod) { val leaves = listOf(Felt.ONE, Felt.fromHex("0x2")) val tree = MerkleTree(leaves, hashFunction) @@ -74,8 +74,8 @@ internal class MerkleTreeTest { } @ParameterizedTest - @EnumSource(MerkleHashFunction::class) - fun `build merkle tree from 4 elements`(hashFunction: MerkleHashFunction) { + @EnumSource(HashMethod::class) + fun `build merkle tree from 4 elements`(hashFunction: HashMethod) { val leaves = (1..4).map { Felt.fromHex("0x$it") } val tree = MerkleTree(leaves, hashFunction) @@ -89,8 +89,8 @@ internal class MerkleTreeTest { } @ParameterizedTest - @EnumSource(MerkleHashFunction::class) - fun `build merkle tree from 6 elements`(hashFunction: MerkleHashFunction) { + @EnumSource(HashMethod::class) + fun `build merkle tree from 6 elements`(hashFunction: HashMethod) { val leaves = (1..6).map { Felt.fromHex("0x$it") } val tree = MerkleTree(leaves, hashFunction) @@ -113,8 +113,8 @@ internal class MerkleTreeTest { } @ParameterizedTest - @EnumSource(MerkleHashFunction::class) - fun `build merkle tree from 7 elements`(hashFunction: MerkleHashFunction) { + @EnumSource(HashMethod::class) + fun `build merkle tree from 7 elements`(hashFunction: HashMethod) { val leaves = (1..7).map { Felt.fromHex("0x$it") } val tree = MerkleTree(leaves, hashFunction) From 8ce47e9ff9119d4fc869a6573ba075452fa5a5a7 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 15:52:37 +0100 Subject: [PATCH 020/109] Change `domain` from `JsonObject` to `TypedData.Domain` --- .../main/kotlin/com/swmansion/starknet/data/TypedData.kt | 9 +++++---- lib/src/test/kotlin/starknet/data/TypedDataTest.kt | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index fc5d89144..0d249cfe4 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -1,5 +1,6 @@ package com.swmansion.starknet.data +import com.swmansion.starknet.crypto.Poseidon import com.swmansion.starknet.crypto.StarknetCurve import com.swmansion.starknet.data.serializers.TypedDataTypeBaseSerializer import com.swmansion.starknet.data.types.Felt @@ -100,10 +101,10 @@ enum class TypedDataRevision(val value: Felt) { data class TypedData private constructor( val types: Map>, val primaryType: String, - val domain: JsonObject, + val domain: Domain, val message: JsonObject, ) { - private val revision = Json.decodeFromJsonElement(domain).revision ?: TypedDataRevision.V0 + private val revision = domain.revision ?: TypedDataRevision.V0 private val domainSeparatorName = when (revision) { TypedDataRevision.V0 -> "StarkNetDomain" @@ -147,7 +148,7 @@ data class TypedData private constructor( ) : this( types = types, primaryType = primaryType, - domain = Json.parseToJsonElement(domain).jsonObject, + domain = Json.decodeFromString(domain), message = Json.parseToJsonElement(message).jsonObject, ) @@ -364,7 +365,7 @@ data class TypedData private constructor( fun getMessageHash(accountAddress: Felt): Felt { return StarknetCurve.pedersenOnElements( Felt.fromShortString("StarkNet Message"), - getStructHash("StarkNetDomain", domain), + getStructHash("StarkNetDomain", Json.encodeToJsonElement(domain).jsonObject), accountAddress, getStructHash(primaryType, message), ) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 15a027c6b..2629113be 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -278,12 +278,12 @@ internal class TypedDataTest { @MethodSource("getStructHashArguments") fun `struct hash calculation`(data: TypedData, typeName: String, dataSource: String, expectedResult: String) { val dataStruct = if (dataSource == "domain") { - data.domain + Json.encodeToString(data.domain) } else { - data.message + Json.encodeToString(data.message) } - val hash = data.getStructHash(typeName, Json.encodeToString(dataStruct)) + val hash = data.getStructHash(typeName, dataStruct) assertEquals(Felt.fromHex(expectedResult), hash) } From 0a4d59e2f0294ccd307fcb498fd99d7e354e5ac5 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 16:05:44 +0100 Subject: [PATCH 021/109] Add `hash(List)` method to `HashMethod` --- .../kotlin/com/swmansion/starknet/crypto/HashMethod.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/crypto/HashMethod.kt b/lib/src/main/kotlin/com/swmansion/starknet/crypto/HashMethod.kt index 01817be0c..58982a412 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/crypto/HashMethod.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/crypto/HashMethod.kt @@ -7,12 +7,19 @@ enum class HashMethod { override fun hash(first: Felt, second: Felt): Felt { return StarknetCurve.pedersen(first, second) } + override fun hash(values: List): Felt { + return StarknetCurve.pedersen(values) + } }, POSEIDON { override fun hash(first: Felt, second: Felt): Felt { return Poseidon.poseidonHash(first, second) } + override fun hash(values: List): Felt { + return Poseidon.poseidonHash(values) + } }, ; abstract fun hash(first: Felt, second: Felt): Felt + abstract fun hash(values: List): Felt } From b65ed36423d033b2c982ba09262ea8dc50dec189 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 16:14:52 +0100 Subject: [PATCH 022/109] Introduce revision-based hashing in `TypedData` - Add revision-based `hashMethod`, `hashArray()` - Use `hashArray()` for hasing arrays - Instantiate `MerkleTree` with `hashMethod` --- .../com/swmansion/starknet/data/TypedData.kt | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 0d249cfe4..f9b2a665a 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -1,6 +1,6 @@ package com.swmansion.starknet.data -import com.swmansion.starknet.crypto.Poseidon +import com.swmansion.starknet.crypto.HashMethod import com.swmansion.starknet.crypto.StarknetCurve import com.swmansion.starknet.data.serializers.TypedDataTypeBaseSerializer import com.swmansion.starknet.data.types.Felt @@ -110,10 +110,18 @@ data class TypedData private constructor( TypedDataRevision.V0 -> "StarkNetDomain" TypedDataRevision.V1 -> "StarknetDomain" } + + private val hashMethod = when (revision) { + TypedDataRevision.V0 -> HashMethod.PEDERSEN + TypedDataRevision.V1 -> HashMethod.POSEIDON + } + init { verifyTypes() } + private fun hashArray(values: List) = hashMethod.hash(values) + private fun verifyTypes() { val reservedTypes = when (revision) { TypedDataRevision.V0 -> reservedTypesV0 @@ -294,7 +302,7 @@ data class TypedData private constructor( if (types.containsKey(stripPointer(typeName))) { val array = value as JsonArray val hashes = array.map { struct -> getStructHash(stripPointer(typeName), struct as JsonObject) } - val hash = StarknetCurve.pedersenOnElements(hashes) + val hash = hashArray(hashes) return typeName to hash } @@ -303,7 +311,7 @@ data class TypedData private constructor( "felt*" -> { val array = value as JsonArray val feltArray = array.map { feltFromPrimitive(it.jsonPrimitive) } - val hash = StarknetCurve.pedersenOnElements(feltArray) + val hash = hashArray(feltArray) typeName to hash } "felt" -> "felt" to feltFromPrimitive(value.jsonPrimitive) @@ -314,7 +322,7 @@ data class TypedData private constructor( val merkleTreeType = getMerkleTreeType(context) val array = value as JsonArray val structHashes = array.map { struct -> encodeValue(merkleTreeType, struct).second } - val root = MerkleTree(structHashes).rootHash + val root = MerkleTree(structHashes, hashMethod).rootHash "merkletree" to root } else -> throw IllegalArgumentException("Type [$typeName] is not defined in types.") @@ -343,7 +351,7 @@ data class TypedData private constructor( private fun getStructHash(typeName: String, data: JsonObject): Felt { val encodedData = encodeData(typeName, data) - return StarknetCurve.pedersenOnElements(getTypeHash(typeName), *encodedData.toTypedArray()) + return hashArray(listOf(getTypeHash(typeName)) + encodedData) } private fun stripPointer(value: String): String { @@ -359,7 +367,7 @@ data class TypedData private constructor( fun getStructHash(typeName: String, data: String): Felt { val encodedData = encodeData(typeName, Json.parseToJsonElement(data).jsonObject) - return StarknetCurve.pedersenOnElements(getTypeHash(typeName), *encodedData.toTypedArray()) + return hashArray(listOf(getTypeHash(typeName)) + encodedData) } fun getMessageHash(accountAddress: Felt): Felt { From a26bb6035eab7702e8b7a3f45167cd515f1f6397 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 16:18:23 +0100 Subject: [PATCH 023/109] Move `TypedData.domainSeparatorName` to `Domain.separatorName` --- .../com/swmansion/starknet/data/TypedData.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index f9b2a665a..4ae299773 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -106,11 +106,6 @@ data class TypedData private constructor( ) { private val revision = domain.revision ?: TypedDataRevision.V0 - private val domainSeparatorName = when (revision) { - TypedDataRevision.V0 -> "StarkNetDomain" - TypedDataRevision.V1 -> "StarknetDomain" - } - private val hashMethod = when (revision) { TypedDataRevision.V0 -> HashMethod.PEDERSEN TypedDataRevision.V1 -> HashMethod.POSEIDON @@ -129,7 +124,7 @@ data class TypedData private constructor( } reservedTypes.forEach { require(it !in types) { "Types must not contain $it." } } - require(domainSeparatorName in types) { "Types must contain $domainSeparatorName." } + require(domain.separatorName in types) { "Types must contain ${domain.separatorName}." } val referencedTypes = types.values.flatten().flatMap { when (it) { @@ -137,7 +132,7 @@ data class TypedData private constructor( is MerkleTreeType -> listOf(it.contains) is Type -> listOf(stripPointer(it.type)) } - }.distinct() + domainSeparatorName + primaryType + }.distinct() + domain.separatorName + primaryType types.keys.forEach { require(it.isNotEmpty()) { "Types cannot be empty." } @@ -166,7 +161,12 @@ data class TypedData private constructor( val version: JsonPrimitive, val chainId: JsonPrimitive, val revision: TypedDataRevision? = null, - ) + ) { + val separatorName = when (revision ?: TypedDataRevision.V0) { + TypedDataRevision.V0 -> "StarkNetDomain" + TypedDataRevision.V1 -> "StarknetDomain" + } + } @Serializable(with = TypedDataTypeBaseSerializer::class) sealed class TypeBase { From f800d3a47b9ea999b873b72406c86f8fd24074f6 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 16:32:19 +0100 Subject: [PATCH 024/109] Replace `contains` and `containKeys` calls with `in` --- .../main/kotlin/com/swmansion/starknet/data/TypedData.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 4ae299773..9428882ce 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -216,7 +216,7 @@ data class TypedData private constructor( for (param in params) { val typeStripped = stripPointer(param.type) - if (types.containsKey(typeStripped) && !deps.contains(typeStripped)) { + if (typeStripped in types && typeStripped !in deps) { deps.add(typeStripped) toVisit.add(typeStripped) } @@ -295,11 +295,11 @@ data class TypedData private constructor( value: JsonElement, context: Context = Context(null, null), ): Pair { - if (types.containsKey(typeName)) { + if (typeName in types) { return typeName to getStructHash(typeName, value as JsonObject) } - if (types.containsKey(stripPointer(typeName))) { + if (stripPointer(typeName) in types) { val array = value as JsonArray val hashes = array.map { struct -> getStructHash(stripPointer(typeName), struct as JsonObject) } val hash = hashArray(hashes) From 9db210e06155391962db29db251c7aa08143f89d Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 16:57:03 +0100 Subject: [PATCH 025/109] Add helper `String.isArray()` and `String.isEnum()` extensions --- .../main/kotlin/com/swmansion/starknet/data/TypedData.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 9428882ce..73c1ca620 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -136,7 +136,7 @@ data class TypedData private constructor( types.keys.forEach { require(it.isNotEmpty()) { "Types cannot be empty." } - require(!it.endsWith("*")) { "Types cannot end in *. $it was found." } + require(!it.isArray()) { "Types cannot end in *. $it was found." } require(!it.startsWith("(") || !it.endsWith(")")) { "Types cannot be enclosed in parenthesis. $it was found." } require(!it.contains(",")) { "Types cannot contain commas. $it was found." } require(it in referencedTypes) { "Dangling types are not allowed. Unreferenced type $it was found." } @@ -394,3 +394,7 @@ data class TypedData private constructor( Json.decodeFromString(serializer(), typedData) } } + +internal fun String.isArray() = endsWith("*") + +internal fun String.isEnum() = startsWith("(") && endsWith(")") From 9cb3121e1ca8346269ef59a6aacbd4fed48a7ed7 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 18:17:42 +0100 Subject: [PATCH 026/109] Fix `HashMethod.PEDERSEN.hash` --- lib/src/main/kotlin/com/swmansion/starknet/crypto/HashMethod.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/crypto/HashMethod.kt b/lib/src/main/kotlin/com/swmansion/starknet/crypto/HashMethod.kt index 58982a412..b1588712c 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/crypto/HashMethod.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/crypto/HashMethod.kt @@ -8,7 +8,7 @@ enum class HashMethod { return StarknetCurve.pedersen(first, second) } override fun hash(values: List): Felt { - return StarknetCurve.pedersen(values) + return StarknetCurve.pedersenOnElements(values) } }, POSEIDON { From 51abbd9a391221839379e2a4d7c1f28ca10ec287 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 18:18:40 +0100 Subject: [PATCH 027/109] Update `TypedData.getDependencies` - Support REV1 enum types - Support getting dep from contains for merkletree (this should probably be verified) - Refactor logic --- .../com/swmansion/starknet/data/TypedData.kt | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 73c1ca620..50c64a936 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -215,10 +215,19 @@ data class TypedData private constructor( for (param in params) { val typeStripped = stripPointer(param.type) - - if (typeStripped in types && typeStripped !in deps) { - deps.add(typeStripped) - toVisit.add(typeStripped) + params.forEach { param -> + val extractedTypes = when { + + param is EnumType && revision == TypedDataRevision.V1 -> listOf(param.contains) + param.type.isEnum() && revision == TypedDataRevision.V1 -> extractEnumTypes(param.type) + else -> listOf(param.type) + }.map { stripPointer(it) } + + extractedTypes.forEach { + if (it in types && it !in deps) { + deps.add(it) + toVisit.add(it) + } } } } From 9111651151e1d0c64a00d49c8cbb021994521d6d Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 18:20:22 +0100 Subject: [PATCH 028/109] Fix serialization process for `enum` type - Support revision 0 type named enum --- .../starknet/data/serializers/TypedDataTypeBaseSerializer.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TypedDataTypeBaseSerializer.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TypedDataTypeBaseSerializer.kt index b00bd00ad..4385aa192 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TypedDataTypeBaseSerializer.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TypedDataTypeBaseSerializer.kt @@ -11,7 +11,10 @@ internal object TypedDataTypeBaseSerializer : JsonContentPolymorphicSerializer TypedData.MerkleTreeType.serializer() - "enum" -> TypedData.EnumType.serializer() + "enum" -> when (element.jsonObject["contains"]?.jsonPrimitive?.content) { + null -> TypedData.Type.serializer() + else -> TypedData.EnumType.serializer() + } is String -> TypedData.Type.serializer() null -> throw IllegalArgumentException("Input element does not contain mandatory field 'type'") else -> throw IllegalArgumentException("Unknown type '$type'") From 5d020e34e0f245e5026362b4ee982a3162ba8d2f Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 18:23:41 +0100 Subject: [PATCH 029/109] Revert "Update `TypedData.getDependencies`" This reverts commit 51abbd9a391221839379e2a4d7c1f28ca10ec287. --- .../com/swmansion/starknet/data/TypedData.kt | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 50c64a936..73c1ca620 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -215,19 +215,10 @@ data class TypedData private constructor( for (param in params) { val typeStripped = stripPointer(param.type) - params.forEach { param -> - val extractedTypes = when { - - param is EnumType && revision == TypedDataRevision.V1 -> listOf(param.contains) - param.type.isEnum() && revision == TypedDataRevision.V1 -> extractEnumTypes(param.type) - else -> listOf(param.type) - }.map { stripPointer(it) } - - extractedTypes.forEach { - if (it in types && it !in deps) { - deps.add(it) - toVisit.add(it) - } + + if (typeStripped in types && typeStripped !in deps) { + deps.add(typeStripped) + toVisit.add(typeStripped) } } } From 34282db96b18d628dde3d7981163c6c6458c3f59 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 18:24:41 +0100 Subject: [PATCH 030/109] Update `TypedData.getDependencies` - Support rev 1 enum types - Refactor logic --- .../com/swmansion/starknet/data/TypedData.kt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 73c1ca620..3c3672cf7 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -213,12 +213,18 @@ data class TypedData private constructor( val type = toVisit.removeFirst() val params = types[type] ?: emptyList() - for (param in params) { - val typeStripped = stripPointer(param.type) - - if (typeStripped in types && typeStripped !in deps) { - deps.add(typeStripped) - toVisit.add(typeStripped) + params.forEach { param -> + val extractedTypes = when { + param is EnumType && revision == TypedDataRevision.V1 -> listOf(param.contains) + param.type.isEnum() && revision == TypedDataRevision.V1 -> extractEnumTypes(param.type) + else -> listOf(param.type) + }.map { stripPointer(it) } + + extractedTypes.forEach { + if (it in types && it !in deps) { + deps.add(it) + toVisit.add(it) + } } } } From c7090a2af3034dcf7da80c3d5557496694315a50 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 19:08:59 +0100 Subject: [PATCH 031/109] Move secondary `TypedData` constructor to the top --- .../com/swmansion/starknet/data/TypedData.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 3c3672cf7..752632a10 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -104,6 +104,18 @@ data class TypedData private constructor( val domain: Domain, val message: JsonObject, ) { + constructor( + types: Map>, + primaryType: String, + domain: String, + message: String, + ) : this( + types = types, + primaryType = primaryType, + domain = Json.decodeFromString(domain), + message = Json.parseToJsonElement(message).jsonObject, + ) + private val revision = domain.revision ?: TypedDataRevision.V0 private val hashMethod = when (revision) { @@ -143,18 +155,6 @@ data class TypedData private constructor( } } - constructor( - types: Map>, - primaryType: String, - domain: String, - message: String, - ) : this( - types = types, - primaryType = primaryType, - domain = Json.decodeFromString(domain), - message = Json.parseToJsonElement(message).jsonObject, - ) - @Serializable data class Domain( val name: JsonPrimitive, From ab7257e08f58a85074f0955980e255abf32d4ea8 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 19:09:42 +0100 Subject: [PATCH 032/109] Use isArray() instead of raw check --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 752632a10..43f3f4323 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -187,7 +187,7 @@ data class TypedData private constructor( val contains: String, ) : TypeBase() { init { - require(!contains.endsWith("*")) { + require(!contains.isArray()) { "Merkletree 'contains' field cannot be an array, got '$contains' in type '$name'." } } From 73b53e4f28a52c62d5b3928ee4005378cf6cb25d Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 19:55:26 +0100 Subject: [PATCH 033/109] Support preset types in `TypedData` - [BREAKING] Rename `types` to `customTypes` - Add `presetTypesV0`, `presetTypesV1` - Add argument to `verifyTypes`, only verify customTypes - Add `types` as additional helper property --- .../com/swmansion/starknet/data/TypedData.kt | 46 +++++++++++++++++-- .../kotlin/starknet/data/TypedDataTest.kt | 2 +- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 43f3f4323..6a6412cf1 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -7,6 +7,7 @@ import com.swmansion.starknet.data.types.Felt import com.swmansion.starknet.data.types.MerkleTree import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient import kotlinx.serialization.json.* /** @@ -99,18 +100,22 @@ enum class TypedDataRevision(val value: Felt) { @Suppress("DataClassPrivateConstructor") @Serializable data class TypedData private constructor( - val types: Map>, + @SerialName("types") + val customTypes: Map>, + val primaryType: String, + val domain: Domain, + val message: JsonObject, ) { constructor( - types: Map>, + customTypes: Map>, primaryType: String, domain: String, message: String, ) : this( - types = types, + customTypes = customTypes, primaryType = primaryType, domain = Json.decodeFromString(domain), message = Json.parseToJsonElement(message).jsonObject, @@ -118,18 +123,28 @@ data class TypedData private constructor( private val revision = domain.revision ?: TypedDataRevision.V0 + @Transient + val types: Map> = run { + val presetTypes = when (revision) { + TypedDataRevision.V0 -> presetTypesV0 + TypedDataRevision.V1 -> presetTypesV1 + } + customTypes + presetTypes + } + private val hashMethod = when (revision) { TypedDataRevision.V0 -> HashMethod.PEDERSEN TypedDataRevision.V1 -> HashMethod.POSEIDON } init { - verifyTypes() + verifyTypes(customTypes) } private fun hashArray(values: List) = hashMethod.hash(values) private fun verifyTypes() { + private fun verifyTypes(types: Map>) { val reservedTypes = when (revision) { TypedDataRevision.V0 -> reservedTypesV0 TypedDataRevision.V1 -> reservedTypesV1 @@ -388,7 +403,28 @@ data class TypedData private constructor( companion object { private val reservedTypesV0 by lazy { listOf("felt", "bool", "string", "selector", "merkletree", "raw") } - private val reservedTypesV1 by lazy { reservedTypesV0 + listOf("enum", "bool", "u128", "ContractAddress", "ClassHash", "timestamp", "shortstring") + listOf("u256", "NftId", "TokenAmount") } + private val reservedTypesV1 by lazy { + reservedTypesV0 + listOf("enum", "bool", "u128", "ContractAddress", "ClassHash", "timestamp", "shortstring") + presetTypesV1.keys + } + + private val presetTypesV0: Map> by lazy { emptyMap() } + + private val presetTypesV1: Map> by lazy { + mapOf( + "u256" to listOf( + Type("low", "u128"), + Type("high", "u128"), + ), + "TokenAmount" to listOf( + Type("token_address", "ContractAddress"), + Type("amount", "u256"), + ), + "NftId" to listOf( + Type("collection_address", "ContractAddress"), + Type("token_id", "u256"), + ), + ) + } /** * Create TypedData from JSON string. diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 2629113be..843d98ba9 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -242,7 +242,7 @@ internal class TypedDataTest { fun `invalid types`(type: String) { assertThrows("Types must not contain $type.") { TypedData( - types = mapOf(type to emptyList()), + customTypes = mapOf(type to emptyList()), primaryType = type, domain = "{}", message = "{\"$type\": 1}", From 4cdcd54c6de294f0bfac9eaf4ce7ead6f6799781 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 20:31:10 +0100 Subject: [PATCH 034/109] [BREAKING] Rename Type-related classes in `TypedData` - Rename `TypedData.Type`->`TypedData.StandardType` - Rename `TypedData.TypeBase`->`TypedData.Type` --- .../com/swmansion/starknet/data/TypedData.kt | 43 ++++++++++--------- .../TypedDataTypeBaseSerializer.kt | 10 ++--- .../kotlin/starknet/data/TypedDataTest.kt | 2 +- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 6a6412cf1..6cde26836 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -101,7 +101,7 @@ enum class TypedDataRevision(val value: Felt) { @Serializable data class TypedData private constructor( @SerialName("types") - val customTypes: Map>, + val customTypes: Map>, val primaryType: String, @@ -110,7 +110,7 @@ data class TypedData private constructor( val message: JsonObject, ) { constructor( - customTypes: Map>, + customTypes: Map>, primaryType: String, domain: String, message: String, @@ -124,7 +124,7 @@ data class TypedData private constructor( private val revision = domain.revision ?: TypedDataRevision.V0 @Transient - val types: Map> = run { + val types: Map> = run { val presetTypes = when (revision) { TypedDataRevision.V0 -> presetTypesV0 TypedDataRevision.V1 -> presetTypesV1 @@ -132,9 +132,11 @@ data class TypedData private constructor( customTypes + presetTypes } - private val hashMethod = when (revision) { - TypedDataRevision.V0 -> HashMethod.PEDERSEN - TypedDataRevision.V1 -> HashMethod.POSEIDON + private val hashMethod by lazy { + when (revision) { + TypedDataRevision.V0 -> HashMethod.PEDERSEN + TypedDataRevision.V1 -> HashMethod.POSEIDON + } } init { @@ -145,6 +147,7 @@ data class TypedData private constructor( private fun verifyTypes() { private fun verifyTypes(types: Map>) { + private fun verifyTypes(types: Map>) { val reservedTypes = when (revision) { TypedDataRevision.V0 -> reservedTypesV0 TypedDataRevision.V1 -> reservedTypesV1 @@ -157,7 +160,7 @@ data class TypedData private constructor( when (it) { is EnumType -> extractEnumTypes(it.type) + it.contains is MerkleTreeType -> listOf(it.contains) - is Type -> listOf(stripPointer(it.type)) + is StandardType -> listOf(stripPointer(it.type)) } }.distinct() + domain.separatorName + primaryType @@ -184,23 +187,23 @@ data class TypedData private constructor( } @Serializable(with = TypedDataTypeBaseSerializer::class) - sealed class TypeBase { + sealed class Type { abstract val name: String abstract val type: String } @Serializable - data class Type( + data class StandardType( override val name: String, override val type: String, - ) : TypeBase() + ) : Type() @Serializable data class MerkleTreeType( override val name: String, override val type: String = "merkletree", val contains: String, - ) : TypeBase() { + ) : Type() { init { require(!contains.isArray()) { "Merkletree 'contains' field cannot be an array, got '$contains' in type '$name'." @@ -213,7 +216,7 @@ data class TypedData private constructor( override val name: String, override val type: String = "enum", val contains: String, - ) : TypeBase() + ) : Type() data class Context( val parent: String?, @@ -407,21 +410,21 @@ data class TypedData private constructor( reservedTypesV0 + listOf("enum", "bool", "u128", "ContractAddress", "ClassHash", "timestamp", "shortstring") + presetTypesV1.keys } - private val presetTypesV0: Map> by lazy { emptyMap() } + private val presetTypesV0: Map> by lazy { emptyMap() } - private val presetTypesV1: Map> by lazy { + private val presetTypesV1: Map> by lazy { mapOf( "u256" to listOf( - Type("low", "u128"), - Type("high", "u128"), + StandardType("low", "u128"), + StandardType("high", "u128"), ), "TokenAmount" to listOf( - Type("token_address", "ContractAddress"), - Type("amount", "u256"), + StandardType("token_address", "ContractAddress"), + StandardType("amount", "u256"), ), "NftId" to listOf( - Type("collection_address", "ContractAddress"), - Type("token_id", "u256"), + StandardType("collection_address", "ContractAddress"), + StandardType("token_id", "u256"), ), ) } diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TypedDataTypeBaseSerializer.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TypedDataTypeBaseSerializer.kt index 4385aa192..5a4fab6f1 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TypedDataTypeBaseSerializer.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/serializers/TypedDataTypeBaseSerializer.kt @@ -1,21 +1,21 @@ package com.swmansion.starknet.data.serializers import com.swmansion.starknet.data.TypedData -import com.swmansion.starknet.data.TypedData.TypeBase +import com.swmansion.starknet.data.TypedData.Type import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.json.* -internal object TypedDataTypeBaseSerializer : JsonContentPolymorphicSerializer(TypeBase::class) { - override fun selectDeserializer(element: JsonElement): DeserializationStrategy { +internal object TypedDataTypeBaseSerializer : JsonContentPolymorphicSerializer(Type::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy { val type = element.jsonObject["type"]?.jsonPrimitive?.content return when (type) { "merkletree" -> TypedData.MerkleTreeType.serializer() "enum" -> when (element.jsonObject["contains"]?.jsonPrimitive?.content) { - null -> TypedData.Type.serializer() + null -> TypedData.StandardType.serializer() else -> TypedData.EnumType.serializer() } - is String -> TypedData.Type.serializer() + is String -> TypedData.StandardType.serializer() null -> throw IllegalArgumentException("Input element does not contain mandatory field 'type'") else -> throw IllegalArgumentException("Unknown type '$type'") } diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 843d98ba9..d683bec2b 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -257,7 +257,7 @@ internal class TypedDataTest { ) { TypedData( mapOf( - "house" to listOf(TypedData.Type("fridge", "ice cream")), + "house" to listOf(TypedData.StandardType("fridge", "ice cream")), ), "felt", "{}", From e80a57ad94fcfda29ca9df2aca7fec9a194e0fc6 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 20:33:00 +0100 Subject: [PATCH 035/109] Commit random uncommited changes --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 6cde26836..307b110ef 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -145,8 +145,6 @@ data class TypedData private constructor( private fun hashArray(values: List) = hashMethod.hash(values) - private fun verifyTypes() { - private fun verifyTypes(types: Map>) { private fun verifyTypes(types: Map>) { val reservedTypes = when (revision) { TypedDataRevision.V0 -> reservedTypesV0 From 0a889a38e112bd55ed0cd31f6d12a390faf986d2 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 20:33:54 +0100 Subject: [PATCH 036/109] Make `TypedData.verifyTypes` argument-less again --- .../com/swmansion/starknet/data/TypedData.kt | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 307b110ef..40a165d78 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -140,21 +140,26 @@ data class TypedData private constructor( } init { - verifyTypes(customTypes) + verifyTypes() } private fun hashArray(values: List) = hashMethod.hash(values) - private fun verifyTypes(types: Map>) { + private fun escapeTypeString(type: String) = when (revision) { + TypedDataRevision.V0 -> type + TypedDataRevision.V1 -> "\"$type\"" + } + + private fun verifyTypes() { val reservedTypes = when (revision) { TypedDataRevision.V0 -> reservedTypesV0 TypedDataRevision.V1 -> reservedTypesV1 } - reservedTypes.forEach { require(it !in types) { "Types must not contain $it." } } + reservedTypes.forEach { require(it !in customTypes) { "Types must not contain $it." } } - require(domain.separatorName in types) { "Types must contain ${domain.separatorName}." } + require(domain.separatorName in customTypes) { "Types must contain ${domain.separatorName}." } - val referencedTypes = types.values.flatten().flatMap { + val referencedTypes = customTypes.values.flatten().flatMap { when (it) { is EnumType -> extractEnumTypes(it.type) + it.contains is MerkleTreeType -> listOf(it.contains) @@ -162,7 +167,7 @@ data class TypedData private constructor( } }.distinct() + domain.separatorName + primaryType - types.keys.forEach { + customTypes.keys.forEach { require(it.isNotEmpty()) { "Types cannot be empty." } require(!it.isArray()) { "Types cannot end in *. $it was found." } require(!it.startsWith("(") || !it.endsWith(")")) { "Types cannot be enclosed in parenthesis. $it was found." } From a62f8607023f667d4802c9b50774af443fdb1845 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 20:59:58 +0100 Subject: [PATCH 037/109] Update `encodeDependency` to escape when encoding and support enums - Rename `escapeTypeString`->`escape` --- .../com/swmansion/starknet/data/TypedData.kt | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 40a165d78..22682d6ba 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -145,9 +145,9 @@ data class TypedData private constructor( private fun hashArray(values: List) = hashMethod.hash(values) - private fun escapeTypeString(type: String) = when (revision) { - TypedDataRevision.V0 -> type - TypedDataRevision.V1 -> "\"$type\"" + private fun escape(typeName: String) = when (revision) { + TypedDataRevision.V0 -> typeName + TypedDataRevision.V1 -> "\"$typeName\"" } private fun verifyTypes() { @@ -263,9 +263,21 @@ data class TypedData private constructor( } private fun encodeDependency(dependency: String): String { - val fields = - types[dependency] ?: throw IllegalArgumentException("Dependency [$dependency] is not defined in types.") - val encodedFields = fields.joinToString(",") { "${it.name}:${it.type}" } + val fields = types.getOrElse(dependency) { + throw IllegalArgumentException("Dependency [$dependency] is not defined in types.") + } + val encodedFields = fields.joinToString(",") { + val targetType = when { + it is EnumType && revision == TypedDataRevision.V1 -> it.contains + else -> it.type + } + val typeString = when { + targetType.isEnum() -> extractEnumTypes(targetType).joinToString("", transform = ::escape) + else -> escape(targetType) + } + "${it.name}:$typeString" + } + return "$dependency($encodedFields)" } From e229118aa45a5d0492e224e44b728b8617c79159 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 6 Mar 2024 21:02:24 +0100 Subject: [PATCH 038/109] Move `TypedData.escape` to `TypedData.encodeDependency.escape` --- .../kotlin/com/swmansion/starknet/data/TypedData.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 22682d6ba..2baf4998c 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -145,11 +145,6 @@ data class TypedData private constructor( private fun hashArray(values: List) = hashMethod.hash(values) - private fun escape(typeName: String) = when (revision) { - TypedDataRevision.V0 -> typeName - TypedDataRevision.V1 -> "\"$typeName\"" - } - private fun verifyTypes() { val reservedTypes = when (revision) { TypedDataRevision.V0 -> reservedTypesV0 @@ -263,6 +258,11 @@ data class TypedData private constructor( } private fun encodeDependency(dependency: String): String { + fun escape(typeName: String) = when (revision) { + TypedDataRevision.V0 -> typeName + TypedDataRevision.V1 -> "\"$typeName\"" + } + val fields = types.getOrElse(dependency) { throw IllegalArgumentException("Dependency [$dependency] is not defined in types.") } From 869e7e3ddda8a05e29b2357bf2bc132e5b0e3986 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Thu, 7 Mar 2024 11:49:48 +0100 Subject: [PATCH 039/109] Update array handling in `TypedData.encodeValue` --- .../com/swmansion/starknet/data/TypedData.kt | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 2baf4998c..8facf57c8 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -335,24 +335,18 @@ data class TypedData private constructor( context: Context = Context(null, null), ): Pair { if (typeName in types) { - return typeName to getStructHash(typeName, value as JsonObject) + return typeName to getStructHash(typeName, value.jsonObject) } - if (stripPointer(typeName) in types) { - val array = value as JsonArray - val hashes = array.map { struct -> getStructHash(stripPointer(typeName), struct as JsonObject) } - val hash = hashArray(hashes) + if (typeName.isArray()) { + val hashes = value.jsonArray.map { + encodeValue(stripPointer(typeName), it).second + } - return typeName to hash + return typeName to hashArray(hashes) } return when (typeName) { - "felt*" -> { - val array = value as JsonArray - val feltArray = array.map { feltFromPrimitive(it.jsonPrimitive) } - val hash = hashArray(feltArray) - typeName to hash - } "felt" -> "felt" to feltFromPrimitive(value.jsonPrimitive) "string" -> "string" to feltFromPrimitive(value.jsonPrimitive) "raw" -> "raw" to feltFromPrimitive(value.jsonPrimitive) @@ -362,7 +356,7 @@ data class TypedData private constructor( val array = value as JsonArray val structHashes = array.map { struct -> encodeValue(merkleTreeType, struct).second } val root = MerkleTree(structHashes, hashMethod).rootHash - "merkletree" to root + "felt" to root } else -> throw IllegalArgumentException("Type [$typeName] is not defined in types.") } From 13c3cf3ae891453d6ee0e5c187b58b92a11be418 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Thu, 7 Mar 2024 12:00:22 +0100 Subject: [PATCH 040/109] Support handling 'enum' basic type in `TypedData.encodeValue` --- .../com/swmansion/starknet/data/TypedData.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 8facf57c8..1920d39db 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -5,6 +5,7 @@ import com.swmansion.starknet.crypto.StarknetCurve import com.swmansion.starknet.data.serializers.TypedDataTypeBaseSerializer import com.swmansion.starknet.data.types.Felt import com.swmansion.starknet.data.types.MerkleTree +import com.swmansion.starknet.extensions.toFelt import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient @@ -347,6 +348,30 @@ data class TypedData private constructor( } return when (typeName) { + "enum" -> { + require(revision == TypedDataRevision.V1) { "'enum' basic type is not supported in revision ${revision.value}." } + + val (variantKey, variantData) = value.jsonObject.entries.single() + val parent = context.parent ?: throw IllegalArgumentException("Parent is not defined for 'enum' type.") + val parentType = types.getOrElse(parent) { + throw IllegalArgumentException("Parent '$parent' is not defined in types.") + }.first() + require(parentType is EnumType) + val enumType = types.getOrElse(parentType.contains) { throw IllegalArgumentException("Type '${parentType.contains}' is not defined in types") } + val variantType = enumType.find { it.name == variantKey } + ?: throw IllegalArgumentException("Key '$variantKey' is not defined in parent '$parent'.") + + val variantIndex = extractEnumTypes(variantType.type).indexOf(variantData.jsonPrimitive.content) + + val encodedSubtypes = extractEnumTypes(variantType.type) + .filter { it.isNotEmpty() } + .mapIndexed { index, subtype -> + val subtypeData = variantData.jsonArray[index] + encodeValue(subtype, subtypeData).second + } + + "enum" to hashArray(listOf(variantIndex.toFelt) + encodedSubtypes) + } "felt" -> "felt" to feltFromPrimitive(value.jsonPrimitive) "string" -> "string" to feltFromPrimitive(value.jsonPrimitive) "raw" -> "raw" to feltFromPrimitive(value.jsonPrimitive) From 0445bde38b3c622a7dc5245526d66e0ecabbc30a Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Thu, 7 Mar 2024 12:42:36 +0100 Subject: [PATCH 041/109] Move `merkletree` handling higher in `TypedData.encodeValue` --- .../main/kotlin/com/swmansion/starknet/data/TypedData.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 1920d39db..50771aae9 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -372,10 +372,6 @@ data class TypedData private constructor( "enum" to hashArray(listOf(variantIndex.toFelt) + encodedSubtypes) } - "felt" -> "felt" to feltFromPrimitive(value.jsonPrimitive) - "string" -> "string" to feltFromPrimitive(value.jsonPrimitive) - "raw" -> "raw" to feltFromPrimitive(value.jsonPrimitive) - "selector" -> "felt" to prepareSelector(value.jsonPrimitive.content) "merkletree" -> { val merkleTreeType = getMerkleTreeType(context) val array = value as JsonArray @@ -383,6 +379,11 @@ data class TypedData private constructor( val root = MerkleTree(structHashes, hashMethod).rootHash "felt" to root } + "felt" -> "felt" to feltFromPrimitive(value.jsonPrimitive) + "string" -> "string" to feltFromPrimitive(value.jsonPrimitive) + "raw" -> "raw" to feltFromPrimitive(value.jsonPrimitive) + "selector" -> "felt" to prepareSelector(value.jsonPrimitive.content) + else -> throw IllegalArgumentException("Type [$typeName] is not defined in types.") } } From 2caa85c1e2f1a21a2b281252e24950612515035b Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Thu, 7 Mar 2024 15:52:18 +0100 Subject: [PATCH 042/109] Update `string` handling in `TypedData.encodeValue` - Support long strings (rev 1) - Add helper extension `String.splitToShortStrings` --- .../com/swmansion/starknet/data/TypedData.kt | 25 ++++++++++++++++++- .../extensions/SplitToShortStrings.kt | 9 +++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 lib/src/main/kotlin/com/swmansion/starknet/extensions/SplitToShortStrings.kt diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 50771aae9..3f616018b 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -5,6 +5,7 @@ import com.swmansion.starknet.crypto.StarknetCurve import com.swmansion.starknet.data.serializers.TypedDataTypeBaseSerializer import com.swmansion.starknet.data.types.Felt import com.swmansion.starknet.data.types.MerkleTree +import com.swmansion.starknet.extensions.splitToShortStrings import com.swmansion.starknet.extensions.toFelt import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -306,6 +307,25 @@ data class TypedData private constructor( } } + private fun prepareLongString(string: String): Felt { + val shortStrings = string.splitToShortStrings() + + val encodedShortStrings = shortStrings.map(Felt::fromShortString) + + val (data, pendingWord, pendingWordLength) = when { + shortStrings.isEmpty() -> Triple(listOf(Felt.ZERO), Felt.ZERO, 0) + shortStrings.last().length == 31 -> Triple(encodedShortStrings, Felt.ZERO, 0) + else -> Triple( + encodedShortStrings.dropLast(1), + encodedShortStrings.last(), + shortStrings.last().length, + ) + } + + val elements = listOf(data.size.toFelt) + data + listOf(pendingWord, pendingWordLength.toFelt) + return hashArray(elements) + } + private fun prepareSelector(name: String): Felt { return try { Felt.fromHex(name) @@ -379,8 +399,11 @@ data class TypedData private constructor( val root = MerkleTree(structHashes, hashMethod).rootHash "felt" to root } + "string" -> when (revision) { + TypedDataRevision.V0 -> "string" to feltFromPrimitive(value.jsonPrimitive) + TypedDataRevision.V1 -> "string" to prepareLongString(value.jsonPrimitive.content) + } "felt" -> "felt" to feltFromPrimitive(value.jsonPrimitive) - "string" -> "string" to feltFromPrimitive(value.jsonPrimitive) "raw" -> "raw" to feltFromPrimitive(value.jsonPrimitive) "selector" -> "felt" to prepareSelector(value.jsonPrimitive.content) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/extensions/SplitToShortStrings.kt b/lib/src/main/kotlin/com/swmansion/starknet/extensions/SplitToShortStrings.kt new file mode 100644 index 000000000..d1af73ad1 --- /dev/null +++ b/lib/src/main/kotlin/com/swmansion/starknet/extensions/SplitToShortStrings.kt @@ -0,0 +1,9 @@ +package com.swmansion.starknet.extensions + +@JvmSynthetic +internal fun String.splitToShortStrings(): List { + val maxLen = 31 + return (indices step maxLen).map { + substring(it, (it + maxLen).coerceAtMost(length)) + } +} From b2888b007a4cab8e55afe99a8df5ea30c4daa418 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Thu, 7 Mar 2024 16:03:46 +0100 Subject: [PATCH 043/109] Add support for rev 1 basic types in `TypedData.encodeValue` - Support "bool", "u128", "i128", "ContractAddress", "ClassHash", "timestamp", "shortstring" - Modify `TypedData.feltFromPrimitive` to handle bools --- .../kotlin/com/swmansion/starknet/data/TypedData.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 3f616018b..ab37ffd52 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -295,6 +295,11 @@ data class TypedData private constructor( return Felt(it) } + val boolean = primitive.booleanOrNull + boolean?.let { + return Felt(if (it) 1 else 0) + } + return try { Felt.fromHex(primitive.content) } catch (e: Exception) { @@ -406,7 +411,10 @@ data class TypedData private constructor( "felt" -> "felt" to feltFromPrimitive(value.jsonPrimitive) "raw" -> "raw" to feltFromPrimitive(value.jsonPrimitive) "selector" -> "felt" to prepareSelector(value.jsonPrimitive.content) - + "bool", "u128", "i128", "ContractAddress", "ClassHash", "timestamp", "shortstring" -> { + require(revision == TypedDataRevision.V1) { "'$typeName' basic type is not supported in revision ${revision.value}." } + typeName to feltFromPrimitive(value.jsonPrimitive) + } else -> throw IllegalArgumentException("Type [$typeName] is not defined in types.") } } From f83c5dfac417750ab124fa4098a28c5fdf3270c2 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Thu, 7 Mar 2024 16:18:53 +0100 Subject: [PATCH 044/109] Update `TypedData.getMessageHash` to support rev 1 - Use correct domain separator name based on revision --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index ab37ffd52..53c370aac 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -463,7 +463,7 @@ data class TypedData private constructor( fun getMessageHash(accountAddress: Felt): Felt { return StarknetCurve.pedersenOnElements( Felt.fromShortString("StarkNet Message"), - getStructHash("StarkNetDomain", Json.encodeToJsonElement(domain).jsonObject), + getStructHash(domain.separatorName, Json.encodeToJsonElement(domain).jsonObject), accountAddress, getStructHash(primaryType, message), ) From bdb7bec96a63a67307660cfa04e5544535cc550a Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Thu, 7 Mar 2024 23:46:05 +0100 Subject: [PATCH 045/109] Add `Felt.fromSigned` helper factory constructors --- .../com/swmansion/starknet/data/types/Felt.kt | 19 ++++++++++++ .../starknet/data/types/NumAsHexBaseTests.kt | 30 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Felt.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Felt.kt index 164d831af..feb74970d 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Felt.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Felt.kt @@ -99,5 +99,24 @@ data class Felt(override val value: BigInteger) : NumAsHexBase(value), Convertib return true } + + @JvmStatic + internal fun fromSigned(value: BigInteger): Felt { + if (value < BigInteger.ZERO) { + require(-value < PRIME) { "Values below -Felt.PRIME are not supported, $value given." } + return Felt(PRIME + value) + } + return Felt(value) + } + + @JvmStatic + internal fun fromSigned(value: Long): Felt { + return fromSigned(BigInteger.valueOf(value)) + } + + @JvmStatic + internal fun fromSigned(value: Int): Felt { + return fromSigned(BigInteger.valueOf(value.toLong())) + } } } diff --git a/lib/src/test/kotlin/com/swmansion/starknet/data/types/NumAsHexBaseTests.kt b/lib/src/test/kotlin/com/swmansion/starknet/data/types/NumAsHexBaseTests.kt index a4c57e935..a6026732e 100644 --- a/lib/src/test/kotlin/com/swmansion/starknet/data/types/NumAsHexBaseTests.kt +++ b/lib/src/test/kotlin/com/swmansion/starknet/data/types/NumAsHexBaseTests.kt @@ -129,6 +129,36 @@ internal class NumAsHexBaseTests { assertEquals("\nhello", decoded) } + @Test + fun `from signed integer`() { + assertEquals( + Felt.MAX.toFelt, + Felt.fromSigned(-1), + ) + assertEquals( + Felt.ONE, + Felt.fromSigned(-Felt.MAX), + ) + assertEquals( + (Felt.PRIME - Int.MAX_VALUE.toBigInteger()).toFelt, + Felt.fromSigned(-Int.MAX_VALUE), + ) + assertEquals( + (Felt.PRIME - Long.MAX_VALUE.toBigInteger()).toFelt, + Felt.fromSigned(-Long.MAX_VALUE), + ) + + assertEquals(Felt.ZERO, Felt.fromSigned(0)) + assertEquals(Felt.ONE, Felt.fromSigned(1)) + + assertThrows { + Felt.fromSigned(Felt.PRIME) + } + assertThrows { + Felt.fromSigned(-Felt.PRIME) + } + } + @Test fun `felt array is convertible to calldata`() { val convertibleToCalldata = ArrayList() From aaa1c0ac4c296cdb2322aa96eecd47b1fc18afb9 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Thu, 7 Mar 2024 23:54:56 +0100 Subject: [PATCH 046/109] Properly handle `bool` and `i128` basic types in `TypedData.encodeValue` - Support `bool` for revision 0 - Update `Felt.feltFromPrimitive` to handle negative values --- .../kotlin/com/swmansion/starknet/data/TypedData.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 53c370aac..b1080d96b 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -283,7 +283,7 @@ data class TypedData private constructor( return "$dependency($encodedFields)" } - private fun feltFromPrimitive(primitive: JsonPrimitive): Felt { + private fun feltFromPrimitive(primitive: JsonPrimitive, allowSigned: Boolean = false): Felt { when (primitive.isString) { true -> { if (primitive.content == "") { @@ -292,7 +292,7 @@ data class TypedData private constructor( val decimal = primitive.content.toBigIntegerOrNull() decimal?.let { - return Felt(it) + return if (allowSigned) Felt.fromSigned(it) else Felt(it) } val boolean = primitive.booleanOrNull @@ -307,7 +307,7 @@ data class TypedData private constructor( } } false -> { - return Felt(primitive.long) + return if (allowSigned) return Felt.fromSigned(primitive.long) else Felt(primitive.long) } } } @@ -411,7 +411,9 @@ data class TypedData private constructor( "felt" -> "felt" to feltFromPrimitive(value.jsonPrimitive) "raw" -> "raw" to feltFromPrimitive(value.jsonPrimitive) "selector" -> "felt" to prepareSelector(value.jsonPrimitive.content) - "bool", "u128", "i128", "ContractAddress", "ClassHash", "timestamp", "shortstring" -> { + "bool" -> "bool" to feltFromPrimitive(value.jsonPrimitive) + "i128" -> "i128" to feltFromPrimitive(value.jsonPrimitive, allowSigned = true) + "u128", "ContractAddress", "ClassHash", "timestamp", "shortstring" -> { require(revision == TypedDataRevision.V1) { "'$typeName' basic type is not supported in revision ${revision.value}." } typeName to feltFromPrimitive(value.jsonPrimitive) } From 2f386a8ac1650e92e63f97db3dcedd6fc8640a4e Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Thu, 7 Mar 2024 23:57:05 +0100 Subject: [PATCH 047/109] Refactor `reservedTypesV0` and `reservedTypesV1` to be sets --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index b1080d96b..c98a92efb 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -472,10 +472,10 @@ data class TypedData private constructor( } companion object { - private val reservedTypesV0 by lazy { listOf("felt", "bool", "string", "selector", "merkletree", "raw") } + private val reservedTypesV0 by lazy { setOf("felt", "bool", "string", "selector", "merkletree", "raw") } private val reservedTypesV1 by lazy { - reservedTypesV0 + listOf("enum", "bool", "u128", "ContractAddress", "ClassHash", "timestamp", "shortstring") + presetTypesV1.keys + reservedTypesV0 + setOf("enum", "bool", "u128", "i128", "ContractAddress", "ClassHash", "timestamp", "shortstring") + presetTypesV1.keys } private val presetTypesV0: Map> by lazy { emptyMap() } From a2b3c265041c4fbb62637265100b8101a56e7b55 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Thu, 7 Mar 2024 23:57:27 +0100 Subject: [PATCH 048/109] Reorder checks in `TypedData.verifyTypes()` --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index c98a92efb..02b421377 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -148,14 +148,14 @@ data class TypedData private constructor( private fun hashArray(values: List) = hashMethod.hash(values) private fun verifyTypes() { + require(domain.separatorName in customTypes) { "Types must contain ${domain.separatorName}." } + val reservedTypes = when (revision) { TypedDataRevision.V0 -> reservedTypesV0 TypedDataRevision.V1 -> reservedTypesV1 } reservedTypes.forEach { require(it !in customTypes) { "Types must not contain $it." } } - require(domain.separatorName in customTypes) { "Types must contain ${domain.separatorName}." } - val referencedTypes = customTypes.values.flatten().flatMap { when (it) { is EnumType -> extractEnumTypes(it.type) + it.contains From 36ce4df4a15a9f2597de5968498e12398b4e726b Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 8 Mar 2024 00:33:55 +0100 Subject: [PATCH 049/109] Change `TypedDataRevision` `value` type from `Felt` to `Hex` --- .../main/kotlin/com/swmansion/starknet/data/TypedData.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 02b421377..16466639e 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -21,12 +21,12 @@ import kotlinx.serialization.json.* * [V1] - Initial and current revision, represents the spec after [SNIP-12](https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-12.md) was published. */ @Serializable -enum class TypedDataRevision(val value: Felt) { +enum class TypedDataRevision(val value: Int) { @SerialName("0") - V0(Felt.ZERO), + V0(0), @SerialName("1") - V1(Felt.ONE), + V1(1), } /** From 03f9d23b5bbd3c484cacb5cc2b7484778302eaff Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 8 Mar 2024 00:35:04 +0100 Subject: [PATCH 050/109] Update reserved types tests --- .../kotlin/starknet/data/TypedDataTest.kt | 69 ++++++++++++++++--- 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index d683bec2b..0c3556c00 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -3,6 +3,7 @@ package starknet.data import com.swmansion.starknet.data.TypedData import com.swmansion.starknet.data.TypedData.Context import com.swmansion.starknet.data.TypedData.MerkleTreeType +import com.swmansion.starknet.data.TypedDataRevision import com.swmansion.starknet.data.selectorFromName import com.swmansion.starknet.data.types.Felt import com.swmansion.starknet.data.types.MerkleTree @@ -10,12 +11,13 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.encodeToJsonElement import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.EnumSource import org.junit.jupiter.params.provider.MethodSource -import org.junit.jupiter.params.provider.ValueSource import java.io.File fun loadTypedData(name: String): TypedData { @@ -237,16 +239,61 @@ internal class TypedDataTest { } } - @ParameterizedTest - @ValueSource(strings = ["felt", "string", "selector", "merkletree", "felt*", "string*", "selector*", "merkletree*"]) - fun `invalid types`(type: String) { - assertThrows("Types must not contain $type.") { - TypedData( - customTypes = mapOf(type to emptyList()), - primaryType = type, - domain = "{}", - message = "{\"$type\": 1}", - ) + @Nested + inner class InvalidTypesTest { + private val domainTypeV0 = "StarkNetDomain" to listOf( + TypedData.StandardType("name", "felt"), + TypedData.StandardType("version", "felt"), + TypedData.StandardType("chainId", "felt"), + ) + private val domainTypeV1 = "StarknetDomain" to listOf( + TypedData.StandardType("name", "shortstring"), + TypedData.StandardType("version", "shortstring"), + TypedData.StandardType("chainId", "shortstring"), + TypedData.StandardType("revision", "shortstring"), + ) + private val domainObjectV0 = """ + { + "name": "DomainV0", + "version": 1, + "chainId": 2137 + } + """.trimIndent() + private val domainObjectV1 = """ + { + "name": "DomainV1", + "version": "1", + "chainId": "2137", + "revision": "1" + } + """.trimIndent() + + private val reservedTypesV0 = setOf("felt", "bool", "string", "selector", "merkletree") + private val reservedTypesV1 = reservedTypesV0 + setOf("enum", "u128", "i128", "ContractAddress", "ClassHash", "timestamp", "shortstring", "u256", "TokenAmount", "NftId") + + @ParameterizedTest + @EnumSource(TypedDataRevision::class) + fun `redefined basic types`(revision: TypedDataRevision) { + val (domainType, domainObject, types) = when (revision) { + TypedDataRevision.V0 -> Triple(domainTypeV0, domainObjectV0, reservedTypesV0) + TypedDataRevision.V1 -> Triple(domainTypeV1, domainObjectV1, reservedTypesV1) + } + + types.forEach { type -> + val exception = assertThrows { + TypedData( + customTypes = mapOf( + type to emptyList(), + domainType, + ), + primaryType = type, + domain = domainObject, + message = "{\"$type\": 1}", + ) + } + + assertEquals("Types must not contain $type.", exception.message) + } } } From 9e57392dd49922e8d426a2fd17ba183396c47f4d Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 8 Mar 2024 12:09:52 +0100 Subject: [PATCH 051/109] Lazily load TD from files in TypedDataTest - Should simplify debugging test cases that do not use said TD --- lib/src/test/kotlin/starknet/data/TypedDataTest.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 0c3556c00..983bbd0a7 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -30,12 +30,12 @@ internal class TypedDataTest { companion object { - private val TD = loadTypedData("typed_data_example.json") - private val TD_FELT_ARR = loadTypedData("typed_data_felt_array_example.json") - private val TD_STRING = loadTypedData("typed_data_long_string_example.json") - private val TD_STRUCT_ARR = loadTypedData("typed_data_struct_array_example.json") - private val TD_SESSION = loadTypedData("typed_data_session_example.json") - private val TD_VALIDATE = loadTypedData("typed_data_validate_example.json") + private val TD by lazy { loadTypedData("typed_data_example.json") } + private val TD_FELT_ARR by lazy { loadTypedData("typed_data_felt_array_example.json") } + private val TD_STRING by lazy { loadTypedData("typed_data_long_string_example.json") } + private val TD_STRUCT_ARR by lazy { loadTypedData("typed_data_struct_array_example.json") } + private val TD_SESSION by lazy { loadTypedData("typed_data_session_example.json") } + private val TD_VALIDATE by lazy { loadTypedData("typed_data_validate_example.json") } @JvmStatic fun getTypeHashArguments() = listOf( From 49bfbf4567d4bd24be0a5a8c5644d80b68a585e0 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 8 Mar 2024 12:40:56 +0100 Subject: [PATCH 052/109] Refactor reserved types in `TypedData` - Separate `reservedTypesVX` into `basicTypesVX` and `presetTypesVX` - Convert `basicTypesVX` and `presetTypesVX`to getters - Add helper methods `getBasicTypes` and `getPresetTypes` - Update `verifyTypes` to check basic type and preset types separately --- .../com/swmansion/starknet/data/TypedData.kt | 36 +++++++++++------ .../kotlin/starknet/data/TypedDataTest.kt | 39 +++++++++++++++---- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 16466639e..14330b25e 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -150,11 +150,9 @@ data class TypedData private constructor( private fun verifyTypes() { require(domain.separatorName in customTypes) { "Types must contain ${domain.separatorName}." } - val reservedTypes = when (revision) { - TypedDataRevision.V0 -> reservedTypesV0 - TypedDataRevision.V1 -> reservedTypesV1 - } - reservedTypes.forEach { require(it !in customTypes) { "Types must not contain $it." } } + getBasicTypes(revision).forEach { require(it !in customTypes) { "Types must not contain basic types. [$it] was found." } } + getPresetTypes(revision).keys.forEach { require(it !in customTypes) { "Types must not contain preset types. [$it] was found." } } + val referencedTypes = customTypes.values.flatten().flatMap { when (it) { @@ -472,16 +470,31 @@ data class TypedData private constructor( } companion object { - private val reservedTypesV0 by lazy { setOf("felt", "bool", "string", "selector", "merkletree", "raw") } + private fun getBasicTypes(revision: TypedDataRevision): Set { + return when (revision) { + TypedDataRevision.V0 -> basicTypesV0 + TypedDataRevision.V1 -> basicTypesV1 + } + } - private val reservedTypesV1 by lazy { - reservedTypesV0 + setOf("enum", "bool", "u128", "i128", "ContractAddress", "ClassHash", "timestamp", "shortstring") + presetTypesV1.keys + private fun getPresetTypes(revision: TypedDataRevision): Map> { + return when (revision) { + TypedDataRevision.V0 -> presetTypesV0 + TypedDataRevision.V1 -> presetTypesV1 + } } - private val presetTypesV0: Map> by lazy { emptyMap() } + private val basicTypesV0: Set + get() = setOf("felt", "bool", "string", "selector", "merkletree", "raw") + + private val basicTypesV1: Set + get() = basicTypesV0 + setOf("enum", "u128", "i128", "ContractAddress", "ClassHash", "timestamp", "shortstring") - private val presetTypesV1: Map> by lazy { - mapOf( + private val presetTypesV0: Map> + get() = emptyMap() + + private val presetTypesV1: Map> + get() = mapOf( "u256" to listOf( StandardType("low", "u128"), StandardType("high", "u128"), @@ -495,7 +508,6 @@ data class TypedData private constructor( StandardType("token_id", "u256"), ), ) - } /** * Create TypedData from JSON string. diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 983bbd0a7..02c4e898b 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -268,31 +268,56 @@ internal class TypedDataTest { } """.trimIndent() - private val reservedTypesV0 = setOf("felt", "bool", "string", "selector", "merkletree") - private val reservedTypesV1 = reservedTypesV0 + setOf("enum", "u128", "i128", "ContractAddress", "ClassHash", "timestamp", "shortstring", "u256", "TokenAmount", "NftId") + private val basicTypesV0 = setOf("felt", "bool", "string", "selector", "merkletree") + private val basicTypesV1 = basicTypesV0 + setOf("enum", "u128", "i128", "ContractAddress", "ClassHash", "timestamp", "shortstring", ) + private val presetTypesV0 = emptySet() + private val presetTypesV1 = setOf("u256", "TokenAmount", "NftId") @ParameterizedTest @EnumSource(TypedDataRevision::class) - fun `redefined basic types`(revision: TypedDataRevision) { + fun `basic types redefinition`(revision: TypedDataRevision) { val (domainType, domainObject, types) = when (revision) { - TypedDataRevision.V0 -> Triple(domainTypeV0, domainObjectV0, reservedTypesV0) - TypedDataRevision.V1 -> Triple(domainTypeV1, domainObjectV1, reservedTypesV1) + TypedDataRevision.V0 -> Triple(domainTypeV0, domainObjectV0, basicTypesV0) + TypedDataRevision.V1 -> Triple(domainTypeV1, domainObjectV1, basicTypesV1) } types.forEach { type -> val exception = assertThrows { TypedData( customTypes = mapOf( - type to emptyList(), domainType, + type to emptyList(), ), primaryType = type, domain = domainObject, message = "{\"$type\": 1}", ) } + assertEquals("Types must not contain basic types. [$type] was found.", exception.message) + } + } - assertEquals("Types must not contain $type.", exception.message) + @ParameterizedTest + @EnumSource(TypedDataRevision::class) + fun `preset types redefinition`(revision: TypedDataRevision) { + val (domainType, domainObject, types) = when (revision) { + TypedDataRevision.V0 -> Triple(domainTypeV0, domainObjectV0, presetTypesV0) + TypedDataRevision.V1 -> Triple(domainTypeV1, domainObjectV1, presetTypesV1) + } + + types.forEach { type -> + val exception = assertThrows { + TypedData( + customTypes = mapOf( + domainType, + type to emptyList(), + ), + primaryType = type, + domain = domainObject, + message = "{\"$type\": 1}", + ) + } + assertEquals("Types must not contain preset types. [$type] was found.", exception.message) } } } From f01a93c416c36e455c9a04c39fb0fb55e288ac59 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 8 Mar 2024 13:04:44 +0100 Subject: [PATCH 053/109] Make `Felt.fromSigned` methods public, add docstring, slightly refactor logic --- .../com/swmansion/starknet/data/types/Felt.kt | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Felt.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Felt.kt index feb74970d..f01a4c236 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/types/Felt.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/Felt.kt @@ -100,22 +100,35 @@ data class Felt(override val value: BigInteger) : NumAsHexBase(value), Convertib return true } + /** + * Create Felt from signed [BigInteger] value. It must be in range (-[Felt.PRIME], [Felt.PRIME]). + * + * Calculated as [value] mod [Felt.PRIME]. + */ @JvmStatic - internal fun fromSigned(value: BigInteger): Felt { - if (value < BigInteger.ZERO) { - require(-value < PRIME) { "Values below -Felt.PRIME are not supported, $value given." } - return Felt(PRIME + value) - } - return Felt(value) + fun fromSigned(value: BigInteger): Felt { + require(value.abs() < PRIME) { "Values outside the range (-Felt.PRIME, Felt.PRIME) are not allowed, [$value] given." } + + return Felt(value.mod(PRIME)) } + /** + * Create Felt from signed [Long] value. + * + * Calculated as [value] mod [Felt.PRIME]. + */ @JvmStatic - internal fun fromSigned(value: Long): Felt { + fun fromSigned(value: Long): Felt { return fromSigned(BigInteger.valueOf(value)) } + /** + * Create Felt from signed [Int] value. + * + * Calculated as [value] mod [Felt.PRIME]. + */ @JvmStatic - internal fun fromSigned(value: Int): Felt { + fun fromSigned(value: Int): Felt { return fromSigned(BigInteger.valueOf(value.toLong())) } } From d044e7e1800cb4cda85cdfc10ad16beb30f23098 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 8 Mar 2024 13:04:58 +0100 Subject: [PATCH 054/109] Format --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 1 - lib/src/test/kotlin/starknet/data/TypedDataTest.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 14330b25e..b2d60848e 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -153,7 +153,6 @@ data class TypedData private constructor( getBasicTypes(revision).forEach { require(it !in customTypes) { "Types must not contain basic types. [$it] was found." } } getPresetTypes(revision).keys.forEach { require(it !in customTypes) { "Types must not contain preset types. [$it] was found." } } - val referencedTypes = customTypes.values.flatten().flatMap { when (it) { is EnumType -> extractEnumTypes(it.type) + it.contains diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 02c4e898b..659ed5061 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -269,7 +269,7 @@ internal class TypedDataTest { """.trimIndent() private val basicTypesV0 = setOf("felt", "bool", "string", "selector", "merkletree") - private val basicTypesV1 = basicTypesV0 + setOf("enum", "u128", "i128", "ContractAddress", "ClassHash", "timestamp", "shortstring", ) + private val basicTypesV1 = basicTypesV0 + setOf("enum", "u128", "i128", "ContractAddress", "ClassHash", "timestamp", "shortstring") private val presetTypesV0 = emptySet() private val presetTypesV1 = setOf("u256", "TokenAmount", "NftId") From f88d4426e88d2b94b7bdcacb2dbf2b40383d6a3d Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 8 Mar 2024 13:30:52 +0100 Subject: [PATCH 055/109] Move `TypedDataRevision` to `TypedData.Revision` --- .../com/swmansion/starknet/data/TypedData.kt | 106 +++++++++--------- .../kotlin/starknet/data/TypedDataTest.kt | 18 +-- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index b2d60848e..60f54997d 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -12,23 +12,6 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlinx.serialization.json.* -/** - * TypedData revision. - * - * The revision of the specification to be used. - * - * [V0] - Legacy revision, represents the de facto spec before [SNIP-12](https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-12.md) was published. - * [V1] - Initial and current revision, represents the spec after [SNIP-12](https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-12.md) was published. - */ -@Serializable -enum class TypedDataRevision(val value: Int) { - @SerialName("0") - V0(0), - - @SerialName("1") - V1(1), -} - /** * Sign message for off-chain usage. Follows standard proposed [here](https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-12.md). * @@ -123,21 +106,21 @@ data class TypedData private constructor( message = Json.parseToJsonElement(message).jsonObject, ) - private val revision = domain.revision ?: TypedDataRevision.V0 + private val revision = domain.revision ?: Revision.V0 @Transient val types: Map> = run { val presetTypes = when (revision) { - TypedDataRevision.V0 -> presetTypesV0 - TypedDataRevision.V1 -> presetTypesV1 + Revision.V0 -> presetTypesV0 + Revision.V1 -> presetTypesV1 } customTypes + presetTypes } private val hashMethod by lazy { when (revision) { - TypedDataRevision.V0 -> HashMethod.PEDERSEN - TypedDataRevision.V1 -> HashMethod.POSEIDON + Revision.V0 -> HashMethod.PEDERSEN + Revision.V1 -> HashMethod.POSEIDON } } @@ -148,7 +131,7 @@ data class TypedData private constructor( private fun hashArray(values: List) = hashMethod.hash(values) private fun verifyTypes() { - require(domain.separatorName in customTypes) { "Types must contain ${domain.separatorName}." } + require(domain.separatorName in customTypes) { "Types must contain '${domain.separatorName}'." } getBasicTypes(revision).forEach { require(it !in customTypes) { "Types must not contain basic types. [$it] was found." } } getPresetTypes(revision).keys.forEach { require(it !in customTypes) { "Types must not contain preset types. [$it] was found." } } @@ -163,23 +146,40 @@ data class TypedData private constructor( customTypes.keys.forEach { require(it.isNotEmpty()) { "Types cannot be empty." } - require(!it.isArray()) { "Types cannot end in *. $it was found." } - require(!it.startsWith("(") || !it.endsWith(")")) { "Types cannot be enclosed in parenthesis. $it was found." } - require(!it.contains(",")) { "Types cannot contain commas. $it was found." } - require(it in referencedTypes) { "Dangling types are not allowed. Unreferenced type $it was found." } + require(!it.isArray()) { "Types cannot end in *. [$it] was found." } + require(!it.startsWith("(") || !it.endsWith(")")) { "Types cannot be enclosed in parenthesis. [$it] was found." } + require(!it.contains(",")) { "Types cannot contain commas. [$it] was found." } + require(it in referencedTypes) { "Dangling types are not allowed. Unreferenced type [$it] was found." } } } + /** + * TypedData revision. + * + * The revision of the specification to be used. + * + * [V0] - Legacy revision, represents the de facto spec before [SNIP-12](https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-12.md) was published. + * [V1] - Initial and current revision, represents the spec after [SNIP-12](https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-12.md) was published. + */ + @Serializable + enum class Revision(val value: Int) { + @SerialName("0") + V0(0), + + @SerialName("1") + V1(1), + } + @Serializable data class Domain( val name: JsonPrimitive, val version: JsonPrimitive, val chainId: JsonPrimitive, - val revision: TypedDataRevision? = null, + val revision: Revision? = null, ) { - val separatorName = when (revision ?: TypedDataRevision.V0) { - TypedDataRevision.V0 -> "StarkNetDomain" - TypedDataRevision.V1 -> "StarknetDomain" + val separatorName = when (revision ?: Revision.V0) { + Revision.V0 -> "StarkNetDomain" + Revision.V1 -> "StarknetDomain" } } @@ -203,7 +203,7 @@ data class TypedData private constructor( ) : Type() { init { require(!contains.isArray()) { - "Merkletree 'contains' field cannot be an array, got '$contains' in type '$name'." + "Merkletree 'contains' field cannot be an array, got [$contains] in type [$name]." } } } @@ -230,8 +230,8 @@ data class TypedData private constructor( params.forEach { param -> val extractedTypes = when { - param is EnumType && revision == TypedDataRevision.V1 -> listOf(param.contains) - param.type.isEnum() && revision == TypedDataRevision.V1 -> extractEnumTypes(param.type) + param is EnumType && revision == Revision.V1 -> listOf(param.contains) + param.type.isEnum() && revision == Revision.V1 -> extractEnumTypes(param.type) else -> listOf(param.type) }.map { stripPointer(it) } @@ -258,8 +258,8 @@ data class TypedData private constructor( private fun encodeDependency(dependency: String): String { fun escape(typeName: String) = when (revision) { - TypedDataRevision.V0 -> typeName - TypedDataRevision.V1 -> "\"$typeName\"" + Revision.V0 -> typeName + Revision.V1 -> "\"$typeName\"" } val fields = types.getOrElse(dependency) { @@ -267,7 +267,7 @@ data class TypedData private constructor( } val encodedFields = fields.joinToString(",") { val targetType = when { - it is EnumType && revision == TypedDataRevision.V1 -> it.contains + it is EnumType && revision == Revision.V1 -> it.contains else -> it.type } val typeString = when { @@ -340,11 +340,11 @@ data class TypedData private constructor( val (parent, key) = context.parent to context.key return if (parent != null && key != null) { - val parentType = types.getOrElse(parent) { throw IllegalArgumentException("Parent '$parent' is not defined in types.") } + val parentType = types.getOrElse(parent) { throw IllegalArgumentException("Parent [$parent] is not defined in types.") } val merkleType = parentType.find { it.name == key } - ?: throw IllegalArgumentException("Key '$key' is not defined in parent '$parent'.") + ?: throw IllegalArgumentException("Key [$key] is not defined in parent [$parent].") - require(merkleType is MerkleTreeType) { "Key '$key' in parent '$parent' is not a merkletree." } + require(merkleType is MerkleTreeType) { "Key [$key] in parent [$parent] is not a merkletree." } merkleType.contains } else { @@ -371,17 +371,17 @@ data class TypedData private constructor( return when (typeName) { "enum" -> { - require(revision == TypedDataRevision.V1) { "'enum' basic type is not supported in revision ${revision.value}." } + require(revision == Revision.V1) { "'enum' basic type is not supported in revision ${revision.value}." } val (variantKey, variantData) = value.jsonObject.entries.single() val parent = context.parent ?: throw IllegalArgumentException("Parent is not defined for 'enum' type.") val parentType = types.getOrElse(parent) { - throw IllegalArgumentException("Parent '$parent' is not defined in types.") + throw IllegalArgumentException("Parent [$parent] is not defined in types.") }.first() require(parentType is EnumType) - val enumType = types.getOrElse(parentType.contains) { throw IllegalArgumentException("Type '${parentType.contains}' is not defined in types") } + val enumType = types.getOrElse(parentType.contains) { throw IllegalArgumentException("Type [${parentType.contains}] is not defined in types") } val variantType = enumType.find { it.name == variantKey } - ?: throw IllegalArgumentException("Key '$variantKey' is not defined in parent '$parent'.") + ?: throw IllegalArgumentException("Key [$variantKey] is not defined in parent [$parent].") val variantIndex = extractEnumTypes(variantType.type).indexOf(variantData.jsonPrimitive.content) @@ -402,8 +402,8 @@ data class TypedData private constructor( "felt" to root } "string" -> when (revision) { - TypedDataRevision.V0 -> "string" to feltFromPrimitive(value.jsonPrimitive) - TypedDataRevision.V1 -> "string" to prepareLongString(value.jsonPrimitive.content) + Revision.V0 -> "string" to feltFromPrimitive(value.jsonPrimitive) + Revision.V1 -> "string" to prepareLongString(value.jsonPrimitive.content) } "felt" -> "felt" to feltFromPrimitive(value.jsonPrimitive) "raw" -> "raw" to feltFromPrimitive(value.jsonPrimitive) @@ -411,7 +411,7 @@ data class TypedData private constructor( "bool" -> "bool" to feltFromPrimitive(value.jsonPrimitive) "i128" -> "i128" to feltFromPrimitive(value.jsonPrimitive, allowSigned = true) "u128", "ContractAddress", "ClassHash", "timestamp", "shortstring" -> { - require(revision == TypedDataRevision.V1) { "'$typeName' basic type is not supported in revision ${revision.value}." } + require(revision == Revision.V1) { "'$typeName' basic type is not supported in revision ${revision.value}." } typeName to feltFromPrimitive(value.jsonPrimitive) } else -> throw IllegalArgumentException("Type [$typeName] is not defined in types.") @@ -469,17 +469,17 @@ data class TypedData private constructor( } companion object { - private fun getBasicTypes(revision: TypedDataRevision): Set { + private fun getBasicTypes(revision: Revision): Set { return when (revision) { - TypedDataRevision.V0 -> basicTypesV0 - TypedDataRevision.V1 -> basicTypesV1 + Revision.V0 -> basicTypesV0 + Revision.V1 -> basicTypesV1 } } - private fun getPresetTypes(revision: TypedDataRevision): Map> { + private fun getPresetTypes(revision: Revision): Map> { return when (revision) { - TypedDataRevision.V0 -> presetTypesV0 - TypedDataRevision.V1 -> presetTypesV1 + Revision.V0 -> presetTypesV0 + Revision.V1 -> presetTypesV1 } } diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 659ed5061..dc2b24282 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -3,7 +3,7 @@ package starknet.data import com.swmansion.starknet.data.TypedData import com.swmansion.starknet.data.TypedData.Context import com.swmansion.starknet.data.TypedData.MerkleTreeType -import com.swmansion.starknet.data.TypedDataRevision +import com.swmansion.starknet.data.TypedData.Revision import com.swmansion.starknet.data.selectorFromName import com.swmansion.starknet.data.types.Felt import com.swmansion.starknet.data.types.MerkleTree @@ -274,11 +274,11 @@ internal class TypedDataTest { private val presetTypesV1 = setOf("u256", "TokenAmount", "NftId") @ParameterizedTest - @EnumSource(TypedDataRevision::class) - fun `basic types redefinition`(revision: TypedDataRevision) { + @EnumSource(Revision::class) + fun `basic types redefinition`(revision: Revision) { val (domainType, domainObject, types) = when (revision) { - TypedDataRevision.V0 -> Triple(domainTypeV0, domainObjectV0, basicTypesV0) - TypedDataRevision.V1 -> Triple(domainTypeV1, domainObjectV1, basicTypesV1) + Revision.V0 -> Triple(domainTypeV0, domainObjectV0, basicTypesV0) + Revision.V1 -> Triple(domainTypeV1, domainObjectV1, basicTypesV1) } types.forEach { type -> @@ -298,11 +298,11 @@ internal class TypedDataTest { } @ParameterizedTest - @EnumSource(TypedDataRevision::class) - fun `preset types redefinition`(revision: TypedDataRevision) { + @EnumSource(Revision::class) + fun `preset types redefinition`(revision: Revision) { val (domainType, domainObject, types) = when (revision) { - TypedDataRevision.V0 -> Triple(domainTypeV0, domainObjectV0, presetTypesV0) - TypedDataRevision.V1 -> Triple(domainTypeV1, domainObjectV1, presetTypesV1) + Revision.V0 -> Triple(domainTypeV0, domainObjectV0, presetTypesV0) + Revision.V1 -> Triple(domainTypeV1, domainObjectV1, presetTypesV1) } types.forEach { type -> From b32bfd800ccf528447280f343a8e8cd0a1cea30c Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 8 Mar 2024 14:38:42 +0100 Subject: [PATCH 056/109] Add remaining `InvalidTypeTest` tests; Fix `TypedData.verifyTypes()` - Refactor tests to avoid duplication --- .../com/swmansion/starknet/data/TypedData.kt | 2 +- .../kotlin/starknet/data/TypedDataTest.kt | 102 +++++++++++++----- 2 files changed, 79 insertions(+), 25 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 60f54997d..6a6dd7893 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -147,7 +147,7 @@ data class TypedData private constructor( customTypes.keys.forEach { require(it.isNotEmpty()) { "Types cannot be empty." } require(!it.isArray()) { "Types cannot end in *. [$it] was found." } - require(!it.startsWith("(") || !it.endsWith(")")) { "Types cannot be enclosed in parenthesis. [$it] was found." } + require(!it.startsWith("(") && !it.endsWith(")")) { "Types cannot be enclosed in parentheses. [$it] was found." } require(!it.contains(",")) { "Types cannot contain commas. [$it] was found." } require(it in referencedTypes) { "Dangling types are not allowed. Unreferenced type [$it] was found." } } diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index dc2b24282..8a5072006 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -276,22 +276,14 @@ internal class TypedDataTest { @ParameterizedTest @EnumSource(Revision::class) fun `basic types redefinition`(revision: Revision) { - val (domainType, domainObject, types) = when (revision) { - Revision.V0 -> Triple(domainTypeV0, domainObjectV0, basicTypesV0) - Revision.V1 -> Triple(domainTypeV1, domainObjectV1, basicTypesV1) + val types = when (revision) { + Revision.V0 -> basicTypesV0 + Revision.V1 -> basicTypesV1 } types.forEach { type -> val exception = assertThrows { - TypedData( - customTypes = mapOf( - domainType, - type to emptyList(), - ), - primaryType = type, - domain = domainObject, - message = "{\"$type\": 1}", - ) + makeTypedData(revision, type) } assertEquals("Types must not contain basic types. [$type] was found.", exception.message) } @@ -300,26 +292,88 @@ internal class TypedDataTest { @ParameterizedTest @EnumSource(Revision::class) fun `preset types redefinition`(revision: Revision) { - val (domainType, domainObject, types) = when (revision) { - Revision.V0 -> Triple(domainTypeV0, domainObjectV0, presetTypesV0) - Revision.V1 -> Triple(domainTypeV1, domainObjectV1, presetTypesV1) + val types = when (revision) { + Revision.V0 -> presetTypesV0 + Revision.V1 -> presetTypesV1 } types.forEach { type -> val exception = assertThrows { - TypedData( - customTypes = mapOf( - domainType, - type to emptyList(), - ), - primaryType = type, - domain = domainObject, - message = "{\"$type\": 1}", - ) + makeTypedData(revision, type) } assertEquals("Types must not contain preset types. [$type] was found.", exception.message) } } + + @Test + fun `type with asterisk`() { + val types = listOf("felt*", "u256*", "mytype*") + types.forEach { type -> + val exception = assertThrows { + makeTypedData(Revision.V1, type) + } + assertEquals("Types cannot end in *. [$type] was found.", exception.message) + } + } + + @Test + fun `type with parentheses`() { + val types = listOf("(left", "right)", "(both)") + types.forEach { type -> + val exception = assertThrows { + makeTypedData(Revision.V1, type) + } + assertEquals("Types cannot be enclosed in parentheses. [$type] was found.", exception.message) + } + } + + @Test + fun `type with commas`() { + val types = listOf(",mytype", "my,type", "mytype,") + types.forEach { type -> + val exception = assertThrows { + makeTypedData(Revision.V1, type) + } + assertEquals("Types cannot contain commas. [$type] was found.", exception.message) + } + } + + @Test + fun `dangling types`() { + val exception = assertThrows { + TypedData( + customTypes = mapOf( + domainTypeV1, + "dangling" to emptyList(), + "mytype" to emptyList(), + ), + primaryType = "mytype", + domain = domainObjectV1, + message = "{\"mytype\": 1}", + ) + } + assertEquals("Dangling types are not allowed. Unreferenced type [dangling] was found.", exception.message) + } + + private fun makeTypedData( + revision: Revision, + includedType: String, + ) { + val (domainType, domainObject) = when (revision) { + Revision.V0 -> domainTypeV0 to domainObjectV0 + Revision.V1 -> domainTypeV1 to domainObjectV1 + } + + TypedData( + customTypes = mapOf( + domainType, + includedType to emptyList(), + ), + primaryType = includedType, + domain = domainObject, + message = "{\"$includedType\": 1}", + ) + } } @Test From a57e53e75b81218db63b0cb180b845db37c09d0a Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 8 Mar 2024 14:58:35 +0100 Subject: [PATCH 057/109] Make `TypedData.Domain.separatorName` internal --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 6a6dd7893..c1d5157f5 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -177,7 +177,7 @@ data class TypedData private constructor( val chainId: JsonPrimitive, val revision: Revision? = null, ) { - val separatorName = when (revision ?: Revision.V0) { + internal val separatorName = when (revision ?: Revision.V0) { Revision.V0 -> "StarkNetDomain" Revision.V1 -> "StarknetDomain" } From 78b3e66ba198b78f9c4d00efaea1c3e811d9d608 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 8 Mar 2024 15:05:07 +0100 Subject: [PATCH 058/109] Add missing `escape()`` call in `TypedData.encodeDependency()` --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index c1d5157f5..054f954d2 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -277,7 +277,7 @@ data class TypedData private constructor( "${it.name}:$typeString" } - return "$dependency($encodedFields)" + return "${escape(dependency)}($encodedFields)" } private fun feltFromPrimitive(primitive: JsonPrimitive, allowSigned: Boolean = false): Felt { From 5bfeffc40fb8750135aa70c7e4d10d057ea499cf Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 8 Mar 2024 18:31:08 +0100 Subject: [PATCH 059/109] Refactor tests to prepare for rev 1 tests - Make `loadTypedData` internal - Add `CasesRev0` helper class, move rev 0 TD imported from files there - Add `@Nested MerkletreeTest` to gorup `merkletree` tests --- .../kotlin/starknet/data/TypedDataTest.kt | 219 +++++++++--------- 1 file changed, 113 insertions(+), 106 deletions(-) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 8a5072006..cc5c98497 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -20,7 +20,7 @@ import org.junit.jupiter.params.provider.EnumSource import org.junit.jupiter.params.provider.MethodSource import java.io.File -fun loadTypedData(name: String): TypedData { +internal fun loadTypedData(name: String): TypedData { val content = File("src/test/resources/typed-data/$name").readText() return TypedData.fromJsonString(content) @@ -29,65 +29,68 @@ fun loadTypedData(name: String): TypedData { internal class TypedDataTest { companion object { - - private val TD by lazy { loadTypedData("typed_data_example.json") } - private val TD_FELT_ARR by lazy { loadTypedData("typed_data_felt_array_example.json") } - private val TD_STRING by lazy { loadTypedData("typed_data_long_string_example.json") } - private val TD_STRUCT_ARR by lazy { loadTypedData("typed_data_struct_array_example.json") } - private val TD_SESSION by lazy { loadTypedData("typed_data_session_example.json") } - private val TD_VALIDATE by lazy { loadTypedData("typed_data_validate_example.json") } + internal class CasesRev0 { + companion object { + val TD by lazy { loadTypedData("typed_data_example.json") } + val TD_FELT_ARR by lazy { loadTypedData("typed_data_felt_array_example.json") } + val TD_STRING by lazy { loadTypedData("typed_data_long_string_example.json") } + val TD_STRUCT_ARR by lazy { loadTypedData("typed_data_struct_array_example.json") } + val TD_SESSION by lazy { loadTypedData("typed_data_session_example.json") } + val TD_VALIDATE by lazy { loadTypedData("typed_data_validate_example.json") } + } + } @JvmStatic fun getTypeHashArguments() = listOf( - Arguments.of(TD, "StarkNetDomain", "0x1bfc207425a47a5dfa1a50a4f5241203f50624ca5fdf5e18755765416b8e288"), - Arguments.of(TD, "Person", "0x2896dbe4b96a67110f454c01e5336edc5bbc3635537efd690f122f4809cc855"), - Arguments.of(TD, "Mail", "0x13d89452df9512bf750f539ba3001b945576243288137ddb6c788457d4b2f79"), - Arguments.of(TD_STRING, "String", "0x1933fe9de7e181d64298eecb44fc43b4cec344faa26968646761b7278df4ae2"), - Arguments.of(TD_STRING, "Mail", "0x1ac6f84a5d41cee97febb378ddabbe1390d4e8036df8f89dee194e613411b09"), - Arguments.of(TD_FELT_ARR, "Mail", "0x5b03497592c0d1fe2f3667b63099761714a895c7df96ec90a85d17bfc7a7a0"), - Arguments.of(TD_STRUCT_ARR, "Post", "0x1d71e69bf476486b43cdcfaf5a85c00bb2d954c042b281040e513080388356d"), - Arguments.of(TD_STRUCT_ARR, "Mail", "0x873b878e35e258fc99e3085d5aaad3a81a0c821f189c08b30def2cde55ff27"), - Arguments.of(TD_SESSION, "Session", "0x1aa0e1c56b45cf06a54534fa1707c54e520b842feb21d03b7deddb6f1e340c"), - Arguments.of(TD_SESSION, "Policy", "0x2f0026e78543f036f33e26a8f5891b88c58dc1e20cbbfaf0bb53274da6fa568"), - Arguments.of(TD_VALIDATE, "Validate", "0x1fc17ee4903c000b1c8c6c1424136d4efc4759d1e83915e981b18bc1074a72d"), - Arguments.of(TD_VALIDATE, "Airdrop", "0x37dcb14df3270824843bbbf50c72a724bcb303179dfcce56b653262cbb6957c"), + Arguments.of(CasesRev0.TD, "StarkNetDomain", "0x1bfc207425a47a5dfa1a50a4f5241203f50624ca5fdf5e18755765416b8e288"), + Arguments.of(CasesRev0.TD, "Person", "0x2896dbe4b96a67110f454c01e5336edc5bbc3635537efd690f122f4809cc855"), + Arguments.of(CasesRev0.TD, "Mail", "0x13d89452df9512bf750f539ba3001b945576243288137ddb6c788457d4b2f79"), + Arguments.of(CasesRev0.TD_STRING, "String", "0x1933fe9de7e181d64298eecb44fc43b4cec344faa26968646761b7278df4ae2"), + Arguments.of(CasesRev0.TD_STRING, "Mail", "0x1ac6f84a5d41cee97febb378ddabbe1390d4e8036df8f89dee194e613411b09"), + Arguments.of(CasesRev0.TD_FELT_ARR, "Mail", "0x5b03497592c0d1fe2f3667b63099761714a895c7df96ec90a85d17bfc7a7a0"), + Arguments.of(CasesRev0.TD_STRUCT_ARR, "Post", "0x1d71e69bf476486b43cdcfaf5a85c00bb2d954c042b281040e513080388356d"), + Arguments.of(CasesRev0.TD_STRUCT_ARR, "Mail", "0x873b878e35e258fc99e3085d5aaad3a81a0c821f189c08b30def2cde55ff27"), + Arguments.of(CasesRev0.TD_SESSION, "Session", "0x1aa0e1c56b45cf06a54534fa1707c54e520b842feb21d03b7deddb6f1e340c"), + Arguments.of(CasesRev0.TD_SESSION, "Policy", "0x2f0026e78543f036f33e26a8f5891b88c58dc1e20cbbfaf0bb53274da6fa568"), + Arguments.of(CasesRev0.TD_VALIDATE, "Validate", "0x1fc17ee4903c000b1c8c6c1424136d4efc4759d1e83915e981b18bc1074a72d"), + Arguments.of(CasesRev0.TD_VALIDATE, "Airdrop", "0x37dcb14df3270824843bbbf50c72a724bcb303179dfcce56b653262cbb6957c"), ) @JvmStatic fun getStructHashArguments() = listOf( Arguments.of( - TD, + CasesRev0.TD, "StarkNetDomain", "domain", "0x54833b121883a3e3aebff48ec08a962f5742e5f7b973469c1f8f4f55d470b07", ), - Arguments.of(TD, "Mail", "message", "0x4758f1ed5e7503120c228cbcaba626f61514559e9ef5ed653b0b885e0f38aec"), + Arguments.of(CasesRev0.TD, "Mail", "message", "0x4758f1ed5e7503120c228cbcaba626f61514559e9ef5ed653b0b885e0f38aec"), Arguments.of( - TD_STRING, + CasesRev0.TD_STRING, "Mail", "message", "0x1d16b9b96f7cb7a55950b26cc8e01daa465f78938c47a09d5a066ca58f9936f", ), Arguments.of( - TD_FELT_ARR, + CasesRev0.TD_FELT_ARR, "Mail", "message", "0x26186b02dddb59bf12114f771971b818f48fad83c373534abebaaa39b63a7ce", ), Arguments.of( - TD_STRUCT_ARR, + CasesRev0.TD_STRUCT_ARR, "Mail", "message", "0x5650ec45a42c4776a182159b9d33118a46860a6e6639bb8166ff71f3c41eaef", ), Arguments.of( - TD_SESSION, + CasesRev0.TD_SESSION, "Session", "message", "0x73602062421caf6ad2e942253debfad4584bff58930981364dcd378021defe8", ), Arguments.of( - TD_VALIDATE, + CasesRev0.TD_VALIDATE, "Validate", "message", "0x389e55e4a3d36c6ba04f46f1021a695c934d6782eaf64e47ac059a06a2520c2", @@ -97,32 +100,32 @@ internal class TypedDataTest { @JvmStatic fun getMessageHashArguments() = listOf( Arguments.of( - TD, + CasesRev0.TD, "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", "0x6fcff244f63e38b9d88b9e3378d44757710d1b244282b435cb472053c8d78d0", ), Arguments.of( - TD_STRING, + CasesRev0.TD_STRING, "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", "0x691b977ee0ee645647336f01d724274731f544ad0d626b078033d2541ee641d", ), Arguments.of( - TD_FELT_ARR, + CasesRev0.TD_FELT_ARR, "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", "0x30ab43ef724b08c3b0a9bbe425e47c6173470be75d1d4c55fd5bf9309896bce", ), Arguments.of( - TD_STRUCT_ARR, + CasesRev0.TD_STRUCT_ARR, "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", "0x5914ed2764eca2e6a41eb037feefd3d2e33d9af6225a9e7fe31ac943ff712c", ), Arguments.of( - TD_SESSION, + CasesRev0.TD_SESSION, "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", "0x5d28fa1b31f92e63022f7d85271606e52bed89c046c925f16b09e644dc99794", ), Arguments.of( - TD_VALIDATE, + CasesRev0.TD_VALIDATE, "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", "0x6038f35de58f40a6afa9d359859b2f930e5eb987580ba6875324cc4dbfcee", ), @@ -134,11 +137,11 @@ internal class TypedDataTest { val selector = "transfer" val selectorHash = selectorFromName(selector) - val rawSelectorValueHash = TD_SESSION.encodeValue( + val rawSelectorValueHash = CasesRev0.TD_SESSION.encodeValue( typeName = "felt", value = Json.encodeToJsonElement(selectorHash), ) - val selectorValueHash = TD_SESSION.encodeValue( + val selectorValueHash = CasesRev0.TD_SESSION.encodeValue( typeName = "selector", value = Json.encodeToJsonElement(selector), ) @@ -150,92 +153,96 @@ internal class TypedDataTest { ) } - @Test - fun `merkletree type`() { - val tree = MerkleTree( - listOf( - Felt(1), - Felt(2), - Felt(3), - ), - ) - val leaves = tree.leafHashes - - val merkleTreeHash = TD_SESSION.encodeValue( - typeName = "merkletree", - value = Json.encodeToJsonElement(leaves), - ).second - - assertEquals(tree.rootHash, merkleTreeHash) - assertEquals(Felt.fromHex("0x15ac9e457789ef0c56e5d559809e7336a909c14ee2511503fa7af69be1ba639"), merkleTreeHash) - } - - @Test - fun `merkletree with custom types`() { - val leaves = listOf( - mapOf("contractAddress" to "0x1", "selector" to "transfer"), - mapOf("contractAddress" to "0x2", "selector" to "transfer"), - mapOf("contractAddress" to "0x3", "selector" to "transfer"), - ) + @Nested + inner class MerkletreeTest { + @Test + fun `merkletree type`() { + val tree = MerkleTree( + listOf( + Felt(1), + Felt(2), + Felt(3), + ), + ) + val leaves = tree.leafHashes - val hashedLeaves = leaves.map { leaf -> - TD_SESSION.encodeValue( - typeName = "Policy", - value = Json.encodeToJsonElement(leaf), + val merkleTreeHash = CasesRev0.TD_SESSION.encodeValue( + typeName = "merkletree", + value = Json.encodeToJsonElement(leaves), ).second + + assertEquals(tree.rootHash, merkleTreeHash) + assertEquals(Felt.fromHex("0x15ac9e457789ef0c56e5d559809e7336a909c14ee2511503fa7af69be1ba639"), merkleTreeHash) } - val tree = MerkleTree(hashedLeaves) - val merkleTreeHash = TD_SESSION.encodeValue( - typeName = "merkletree", - value = Json.encodeToJsonElement(leaves), - context = Context(parent = "Session", key = "root"), - ).second + @Test + fun `merkletree with custom types`() { + val leaves = listOf( + mapOf("contractAddress" to "0x1", "selector" to "transfer"), + mapOf("contractAddress" to "0x2", "selector" to "transfer"), + mapOf("contractAddress" to "0x3", "selector" to "transfer"), + ) - assertEquals(tree.rootHash, merkleTreeHash) - assertEquals( - Felt.fromHex("0x12354b159e3799dc0ebe86d62dde4ce7b300538d471e5a7fef23dcbac076011"), - merkleTreeHash, - ) - } + val hashedLeaves = leaves.map { leaf -> + CasesRev0.TD_SESSION.encodeValue( + typeName = "Policy", + value = Json.encodeToJsonElement(leaf), + ).second + } + val tree = MerkleTree(hashedLeaves) - @Test - fun `merkletree from empty leaves`() { - assertThrows("Cannot build Merkle tree from an empty list of leaves.") { - TD_SESSION.encodeValue( + val merkleTreeHash = CasesRev0.TD_SESSION.encodeValue( typeName = "merkletree", - value = Json.encodeToJsonElement(emptyList()), + value = Json.encodeToJsonElement(leaves), context = Context(parent = "Session", key = "root"), + ).second + + assertEquals(tree.rootHash, merkleTreeHash) + assertEquals( + Felt.fromHex("0x12354b159e3799dc0ebe86d62dde4ce7b300538d471e5a7fef23dcbac076011"), + merkleTreeHash, ) } - } - @Test fun `merkletree with invalid contains`() { - assertThrows("Merkletree 'contains' field cannot be an array, got 'felt*' in type 'root'.") { - MerkleTreeType( - name = "root", - type = "merkletree", - contains = "felt*", - ) + @Test + fun `merkletree from empty leaves`() { + assertThrows("Cannot build Merkle tree from an empty list of leaves.") { + CasesRev0.TD_SESSION.encodeValue( + typeName = "merkletree", + value = Json.encodeToJsonElement(emptyList()), + context = Context(parent = "Session", key = "root"), + ) + } } - } - @Test - fun `merkletree with invalid context`() { - val leaves = listOf( - mapOf("contractAddress" to "0x1", "selector" to "transfer"), - mapOf("contractAddress" to "0x2", "selector" to "transfer"), - mapOf("contractAddress" to "0x3", "selector" to "transfer"), - ) + @Test + fun `merkletree with invalid contains`() { + assertThrows("Merkletree 'contains' field cannot be an array, got 'felt*' in type 'root'.") { + MerkleTreeType( + name = "root", + type = "merkletree", + contains = "felt*", + ) + } + } - val invalidParentContext = Context(parent = "UndefinedParent", key = "root") - val invalidKeyContext = Context(parent = "Session", key = "undefinedKey") + @Test + fun `merkletree with invalid context`() { + val leaves = listOf( + mapOf("contractAddress" to "0x1", "selector" to "transfer"), + mapOf("contractAddress" to "0x2", "selector" to "transfer"), + mapOf("contractAddress" to "0x3", "selector" to "transfer"), + ) - assertThrows("Parent type '${invalidParentContext.parent}' is not defined in types.") { - TD_SESSION.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidParentContext) - } - assertThrows("Key '${invalidKeyContext.key}' is not defined in type '${invalidKeyContext.parent}'.") { - TD_SESSION.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidKeyContext) + val invalidParentContext = Context(parent = "UndefinedParent", key = "root") + val invalidKeyContext = Context(parent = "Session", key = "undefinedKey") + + assertThrows("Parent type '${invalidParentContext.parent}' is not defined in types.") { + CasesRev0.TD_SESSION.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidParentContext) + } + assertThrows("Key '${invalidKeyContext.key}' is not defined in type '${invalidKeyContext.parent}'.") { + CasesRev0.TD_SESSION.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidKeyContext) + } } } From a734cc9ed137762b585c4ef4bf64ca56784f6935 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 8 Mar 2024 18:31:53 +0100 Subject: [PATCH 060/109] Add rev 0 `encode type` tests - Make `TypedData.encodeType()` internal --- .../kotlin/com/swmansion/starknet/data/TypedData.kt | 2 +- lib/src/test/kotlin/starknet/data/TypedDataTest.kt | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 054f954d2..f1171a0fe 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -247,7 +247,7 @@ data class TypedData private constructor( return deps } - private fun encodeType(type: String): String { + internal fun encodeType(type: String): String { val deps = getDependencies(type) val sorted = deps.subList(1, deps.size).sorted() diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index cc5c98497..e85350f66 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -132,6 +132,18 @@ internal class TypedDataTest { ) } + @Test + fun `encode type`() { + assertEquals( + "Mail(from:Person,to:Person,contents:felt)Person(name:felt,wallet:felt)", + CasesRev0.TD.encodeType("Mail"), + ) + assertEquals( + "Mail(from:Person,to:Person,posts_len:felt,posts:Post*)Person(name:felt,wallet:felt)Post(title:felt,content:felt)", + CasesRev0.TD_STRUCT_ARR.encodeType("Mail"), + ) + } + @Test fun `selector type`() { val selector = "transfer" From 3e105f495c3d979154521c13fbf53ec1e912a28e Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 8 Mar 2024 18:48:55 +0100 Subject: [PATCH 061/109] Move typed data test files - Move `src/test/resources/typed-data/typed_data_*.json` to `typed_data/` --- .../test/kotlin/starknet/data/TypedDataTest.kt | 16 ++++++++-------- .../rev_0}/typed_data_example.json | 0 .../rev_0}/typed_data_felt_array_example.json | 0 .../rev_0}/typed_data_long_string_example.json | 0 .../rev_0}/typed_data_session_example.json | 0 .../rev_0}/typed_data_struct_array_example.json | 0 .../rev_0}/typed_data_validate_example.json | 0 7 files changed, 8 insertions(+), 8 deletions(-) rename lib/src/test/resources/{typed-data => typed_data/rev_0}/typed_data_example.json (100%) rename lib/src/test/resources/{typed-data => typed_data/rev_0}/typed_data_felt_array_example.json (100%) rename lib/src/test/resources/{typed-data => typed_data/rev_0}/typed_data_long_string_example.json (100%) rename lib/src/test/resources/{typed-data => typed_data/rev_0}/typed_data_session_example.json (100%) rename lib/src/test/resources/{typed-data => typed_data/rev_0}/typed_data_struct_array_example.json (100%) rename lib/src/test/resources/{typed-data => typed_data/rev_0}/typed_data_validate_example.json (100%) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index e85350f66..5449ed2d7 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -20,8 +20,8 @@ import org.junit.jupiter.params.provider.EnumSource import org.junit.jupiter.params.provider.MethodSource import java.io.File -internal fun loadTypedData(name: String): TypedData { - val content = File("src/test/resources/typed-data/$name").readText() +internal fun loadTypedData(path: String): TypedData { + val content = File("src/test/resources/typed_data/$path").readText() return TypedData.fromJsonString(content) } @@ -31,12 +31,12 @@ internal class TypedDataTest { companion object { internal class CasesRev0 { companion object { - val TD by lazy { loadTypedData("typed_data_example.json") } - val TD_FELT_ARR by lazy { loadTypedData("typed_data_felt_array_example.json") } - val TD_STRING by lazy { loadTypedData("typed_data_long_string_example.json") } - val TD_STRUCT_ARR by lazy { loadTypedData("typed_data_struct_array_example.json") } - val TD_SESSION by lazy { loadTypedData("typed_data_session_example.json") } - val TD_VALIDATE by lazy { loadTypedData("typed_data_validate_example.json") } + val TD by lazy { loadTypedData("rev_0/typed_data_example.json") } + val TD_FELT_ARR by lazy { loadTypedData("rev_0/typed_data_felt_array_example.json") } + val TD_STRING by lazy { loadTypedData("rev_0/typed_data_long_string_example.json") } + val TD_STRUCT_ARR by lazy { loadTypedData("rev_0/typed_data_struct_array_example.json") } + val TD_SESSION by lazy { loadTypedData("rev_0/typed_data_session_example.json") } + val TD_VALIDATE by lazy { loadTypedData("rev_0/typed_data_validate_example.json") } } } diff --git a/lib/src/test/resources/typed-data/typed_data_example.json b/lib/src/test/resources/typed_data/rev_0/typed_data_example.json similarity index 100% rename from lib/src/test/resources/typed-data/typed_data_example.json rename to lib/src/test/resources/typed_data/rev_0/typed_data_example.json diff --git a/lib/src/test/resources/typed-data/typed_data_felt_array_example.json b/lib/src/test/resources/typed_data/rev_0/typed_data_felt_array_example.json similarity index 100% rename from lib/src/test/resources/typed-data/typed_data_felt_array_example.json rename to lib/src/test/resources/typed_data/rev_0/typed_data_felt_array_example.json diff --git a/lib/src/test/resources/typed-data/typed_data_long_string_example.json b/lib/src/test/resources/typed_data/rev_0/typed_data_long_string_example.json similarity index 100% rename from lib/src/test/resources/typed-data/typed_data_long_string_example.json rename to lib/src/test/resources/typed_data/rev_0/typed_data_long_string_example.json diff --git a/lib/src/test/resources/typed-data/typed_data_session_example.json b/lib/src/test/resources/typed_data/rev_0/typed_data_session_example.json similarity index 100% rename from lib/src/test/resources/typed-data/typed_data_session_example.json rename to lib/src/test/resources/typed_data/rev_0/typed_data_session_example.json diff --git a/lib/src/test/resources/typed-data/typed_data_struct_array_example.json b/lib/src/test/resources/typed_data/rev_0/typed_data_struct_array_example.json similarity index 100% rename from lib/src/test/resources/typed-data/typed_data_struct_array_example.json rename to lib/src/test/resources/typed_data/rev_0/typed_data_struct_array_example.json diff --git a/lib/src/test/resources/typed-data/typed_data_validate_example.json b/lib/src/test/resources/typed_data/rev_0/typed_data_validate_example.json similarity index 100% rename from lib/src/test/resources/typed-data/typed_data_validate_example.json rename to lib/src/test/resources/typed_data/rev_0/typed_data_validate_example.json From 8b9d522237341dfa6e84d3fdbc3ecc720c30ec32 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sat, 9 Mar 2024 13:14:31 +0100 Subject: [PATCH 062/109] Add remaining `encodeType` tests - Add `CasesRev1` in `TypedDataTest` - Add matching new test example files - typed_data_basic_types_example.json - typed_data_enum_example.json - typed_data_example.json - typed_data_preset_types_example.json - typed_data_struct_array_example.json --- .../kotlin/starknet/data/TypedDataTest.kt | 40 ++++++++++++++++ .../rev_1/typed_data_basic_types_example.json | 41 +++++++++++++++++ .../rev_1/typed_data_enum_example.json | 28 +++++++++++ .../typed_data/rev_1/typed_data_example.json | 37 +++++++++++++++ .../typed_data_preset_types_example.json | 37 +++++++++++++++ .../typed_data_struct_array_example.json | 46 +++++++++++++++++++ 6 files changed, 229 insertions(+) create mode 100644 lib/src/test/resources/typed_data/rev_1/typed_data_basic_types_example.json create mode 100644 lib/src/test/resources/typed_data/rev_1/typed_data_enum_example.json create mode 100644 lib/src/test/resources/typed_data/rev_1/typed_data_example.json create mode 100644 lib/src/test/resources/typed_data/rev_1/typed_data_preset_types_example.json create mode 100644 lib/src/test/resources/typed_data/rev_1/typed_data_struct_array_example.json diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 5449ed2d7..897d39896 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -40,6 +40,16 @@ internal class TypedDataTest { } } + internal class CasesRev1 { + companion object{ + val TD_BASIC_TYPES by lazy { loadTypedData("rev_1/typed_data_basic_types_example.json") } + val TD_PRESET_TYPES by lazy { loadTypedData("rev_1/typed_data_preset_types_example.json") } + val TD_ENUM_TYPE by lazy { loadTypedData("rev_1/typed_data_enum_example.json") } + val TD by lazy { loadTypedData("rev_1/typed_data_example.json") } + val TD_STRUCT_ARR by lazy { loadTypedData("rev_1/typed_data_struct_array_example.json") } + } + } + @JvmStatic fun getTypeHashArguments() = listOf( Arguments.of(CasesRev0.TD, "StarkNetDomain", "0x1bfc207425a47a5dfa1a50a4f5241203f50624ca5fdf5e18755765416b8e288"), @@ -142,6 +152,36 @@ internal class TypedDataTest { "Mail(from:Person,to:Person,posts_len:felt,posts:Post*)Person(name:felt,wallet:felt)Post(title:felt,content:felt)", CasesRev0.TD_STRUCT_ARR.encodeType("Mail"), ) + assertEquals( + """ + "Mail"("from":"Person","to":"Person","contents":"felt")"Person"("name":"felt","wallet":"felt") + """.trimIndent(), + CasesRev1.TD.encodeType("Mail"), + ) + assertEquals( + """ + "Mail"("from":"Person","to":"Person","posts_len":"felt","posts":"Post*")"Person"("name":"felt","wallet":"felt")"Post"("title":"felt","content":"felt") + """.trimIndent(), + CasesRev1.TD_STRUCT_ARR.encodeType("Mail"), + ) + assertEquals( + """ + "Example"("n0":"felt","n1":"bool","n2":"string","n3":"selector","n4":"u128","n5":"i128","n6":"ContractAddress","n7":"ClassHash","n8":"timestamp","n9":"shortstring") + """.trimIndent(), + CasesRev1.TD_BASIC_TYPES.encodeType("Example"), + ) + assertEquals( + """ + "Example"("n0":"TokenAmount","n1":"NftId")"NftId"("collection_address":"ContractAddress","token_id":"u256")"TokenAmount"("token_address":"ContractAddress","amount":"u256")"u256"("low":"u128","high":"u128") + """.trimIndent(), + CasesRev1.TD_PRESET_TYPES.encodeType("Example"), + ) + assertEquals( + """ + "Example"("someEnum":"MyEnum")"MyEnum"("Variant 1":(),"Variant 2":("u128","u128*"),"Variant 3":("u128")) + """.trimIndent(), + CasesRev1.TD_ENUM_TYPE.encodeType("Example"), + ) } @Test diff --git a/lib/src/test/resources/typed_data/rev_1/typed_data_basic_types_example.json b/lib/src/test/resources/typed_data/rev_1/typed_data_basic_types_example.json new file mode 100644 index 000000000..a8a4eb111 --- /dev/null +++ b/lib/src/test/resources/typed_data/rev_1/typed_data_basic_types_example.json @@ -0,0 +1,41 @@ +{ + "types": { + "StarknetDomain": [ + { "name": "name", "type": "shortstring" }, + { "name": "version", "type": "shortstring" }, + { "name": "chainId", "type": "shortstring" }, + { "name": "revision", "type": "shortstring" } + ], + "Example": [ + { "name": "n0", "type": "felt" }, + { "name": "n1", "type": "bool" }, + { "name": "n2", "type": "string" }, + { "name": "n3", "type": "selector" }, + { "name": "n4", "type": "u128" }, + { "name": "n5", "type": "i128" }, + { "name": "n6", "type": "ContractAddress" }, + { "name": "n7", "type": "ClassHash" }, + { "name": "n8", "type": "timestamp" }, + { "name": "n9", "type": "shortstring" } + ] + }, + "primaryType": "Example", + "domain": { + "name": "StarkNet Mail", + "version": "1", + "chainId": "1", + "revision": "1" + }, + "message": { + "n0": "0x3e8", + "n1": true, + "n2": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + "n3": "transfer", + "n4": "0x3e8", + "n5": -170141183460469231731687303715884105727, + "n6": "0x3e8", + "n7": "0x3e8", + "n8": 1000, + "n9": "transfer" + } +} \ No newline at end of file diff --git a/lib/src/test/resources/typed_data/rev_1/typed_data_enum_example.json b/lib/src/test/resources/typed_data/rev_1/typed_data_enum_example.json new file mode 100644 index 000000000..86943db7e --- /dev/null +++ b/lib/src/test/resources/typed_data/rev_1/typed_data_enum_example.json @@ -0,0 +1,28 @@ +{ + "types": { + "StarknetDomain": [ + { "name": "name", "type": "shortstring" }, + { "name": "version", "type": "shortstring" }, + { "name": "chainId", "type": "shortstring" }, + { "name": "revision", "type": "shortstring" } + ], + "Example": [{ "name": "someEnum", "type": "enum", "contains": "MyEnum" }], + "MyEnum": [ + { "name": "Variant 1", "type": "()" }, + { "name": "Variant 2", "type": "(u128,u128*)" }, + { "name": "Variant 3", "type": "(u128)" } + ] + }, + "primaryType": "Example", + "domain": { + "name": "StarkNet Mail", + "version": "1", + "chainId": "1", + "revision": "1" + }, + "message": { + "someEnum": { + "Variant 2": [2, [0, 1]] + } + } +} diff --git a/lib/src/test/resources/typed_data/rev_1/typed_data_example.json b/lib/src/test/resources/typed_data/rev_1/typed_data_example.json new file mode 100644 index 000000000..29828991a --- /dev/null +++ b/lib/src/test/resources/typed_data/rev_1/typed_data_example.json @@ -0,0 +1,37 @@ +{ + "types": { + "StarknetDomain": [ + { "name": "name", "type": "shortstring" }, + { "name": "version", "type": "shortstring" }, + { "name": "chainId", "type": "shortstring" }, + { "name": "revision", "type": "shortstring" } + ], + "Person": [ + { "name": "name", "type": "felt" }, + { "name": "wallet", "type": "felt" } + ], + "Mail": [ + { "name": "from", "type": "Person" }, + { "name": "to", "type": "Person" }, + { "name": "contents", "type": "felt" } + ] + }, + "primaryType": "Mail", + "domain": { + "name": "StarkNet Mail", + "version": "1", + "chainId": "1", + "revision": "1" + }, + "message": { + "from": { + "name": "Cow", + "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" + }, + "to": { + "name": "Bob", + "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" + }, + "contents": "Hello, Bob!" + } +} diff --git a/lib/src/test/resources/typed_data/rev_1/typed_data_preset_types_example.json b/lib/src/test/resources/typed_data/rev_1/typed_data_preset_types_example.json new file mode 100644 index 000000000..65d860fe1 --- /dev/null +++ b/lib/src/test/resources/typed_data/rev_1/typed_data_preset_types_example.json @@ -0,0 +1,37 @@ +{ + "types": { + "StarknetDomain": [ + { "name": "name", "type": "shortstring" }, + { "name": "version", "type": "shortstring" }, + { "name": "chainId", "type": "shortstring" }, + { "name": "revision", "type": "shortstring" } + ], + "Example": [ + { "name": "n0", "type": "TokenAmount" }, + { "name": "n1", "type": "NftId" } + ] + }, + "primaryType": "Example", + "domain": { + "name": "StarkNet Mail", + "version": "1", + "chainId": "1", + "revision": "1" + }, + "message": { + "n0": { + "token_address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + "amount": { + "low": "0x3e8", + "high": "0x0" + } + }, + "n1": { + "collection_address": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + "token_id": { + "low": "0x3e8", + "high": "0x0" + } + } + } +} diff --git a/lib/src/test/resources/typed_data/rev_1/typed_data_struct_array_example.json b/lib/src/test/resources/typed_data/rev_1/typed_data_struct_array_example.json new file mode 100644 index 000000000..a6539f8e2 --- /dev/null +++ b/lib/src/test/resources/typed_data/rev_1/typed_data_struct_array_example.json @@ -0,0 +1,46 @@ +{ + "types": { + "StarknetDomain": [ + { "name": "name", "type": "shortstring" }, + { "name": "version", "type": "shortstring" }, + { "name": "chainId", "type": "shortstring" }, + { "name": "revision", "type": "shortstring" } + ], + "Person": [ + { "name": "name", "type": "felt" }, + { "name": "wallet", "type": "felt" } + ], + "Post": [ + { "name": "title", "type": "felt" }, + { "name": "content", "type": "felt" } + ], + "Mail": [ + { "name": "from", "type": "Person" }, + { "name": "to", "type": "Person" }, + { "name": "posts_len", "type": "felt" }, + { "name": "posts", "type": "Post*" } + ] + }, + "primaryType": "Mail", + "domain": { + "name": "StarkNet Mail", + "version": "1", + "chainId": "1", + "revision": "1" + }, + "message": { + "from": { + "name": "Cow", + "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" + }, + "to": { + "name": "Bob", + "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" + }, + "posts_len": 2, + "posts": [ + { "title": "Greeting", "content": "Hello, Bob!" }, + { "title": "Farewell", "content": "Goodbye, Bob!" } + ] + } +} From f608f9e7f4271884ecea7aafe125835314049882 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sat, 9 Mar 2024 13:16:49 +0100 Subject: [PATCH 063/109] Fix encoding of name in `TypedData.encodeDependency()` --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index f1171a0fe..9c38fb8ba 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -274,7 +274,7 @@ data class TypedData private constructor( targetType.isEnum() -> extractEnumTypes(targetType).joinToString("", transform = ::escape) else -> escape(targetType) } - "${it.name}:$typeString" + "${escape(it.name)}:$typeString" } return "${escape(dependency)}($encodedFields)" From de13a20b6dab7daccda5c98839dde6dff51d76fe Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sat, 9 Mar 2024 13:20:37 +0100 Subject: [PATCH 064/109] Fix `enum` handling in `TypedData.encodeDependency()` - Fix `extractEnumTypes`, use String methods instead of regex - Adjust `encodeDependency()`: - Add parentheses - Add comma separation --- .../com/swmansion/starknet/data/TypedData.kt | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 9c38fb8ba..4093821ed 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -271,7 +271,12 @@ data class TypedData private constructor( else -> it.type } val typeString = when { - targetType.isEnum() -> extractEnumTypes(targetType).joinToString("", transform = ::escape) + targetType.isEnum() -> extractEnumTypes(targetType).joinToString( + separator = ",", + prefix = "(", + postfix = ")", + transform = ::escape, + ) else -> escape(targetType) } "${escape(it.name)}:$typeString" @@ -447,10 +452,15 @@ data class TypedData private constructor( return value.removeSuffix("*") } - private fun extractEnumTypes(value: String): List { - val enumPattern = Regex("""\(\s*([^,]+)\s*\)""") - val matches = enumPattern.findAll(value) - return matches.map { it.groupValues[1].trim() }.toList() + private fun extractEnumTypes(type: String): List { + require(type.isEnum()) { "Type [$type] is not an enum." } + + return type.substring(1, type.length-1).let{ + when { + it.trim().isEmpty() -> emptyList() + else -> it.split(",").map(String::trim) + } + } } fun getStructHash(typeName: String, data: String): Felt { From c0b07cb38bcea7ae5aeb43f3124aacabcc470d01 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sat, 9 Mar 2024 13:22:26 +0100 Subject: [PATCH 065/109] Adjust `enum` handling `TypedData.verifyTypes()` --- .../main/kotlin/com/swmansion/starknet/data/TypedData.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 4093821ed..13717b685 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -138,9 +138,12 @@ data class TypedData private constructor( val referencedTypes = customTypes.values.flatten().flatMap { when (it) { - is EnumType -> extractEnumTypes(it.type) + it.contains + is EnumType -> listOf(it.contains) is MerkleTreeType -> listOf(it.contains) - is StandardType -> listOf(stripPointer(it.type)) + is StandardType -> when { + it.type.isEnum() && revision == Revision.V1 -> extractEnumTypes(it.type) + else -> listOf(stripPointer(it.type)) + } } }.distinct() + domain.separatorName + primaryType From 3d88accd758e54fda9af95c1a46f67bed21250a4 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sat, 9 Mar 2024 13:22:42 +0100 Subject: [PATCH 066/109] Change `TypedData.stripPointer()` signature --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 13717b685..4726b178c 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -451,8 +451,8 @@ data class TypedData private constructor( return hashArray(listOf(getTypeHash(typeName)) + encodedData) } - private fun stripPointer(value: String): String { - return value.removeSuffix("*") + private fun stripPointer(type: String): String { + return type.removeSuffix("*") } private fun extractEnumTypes(type: String): List { From 1724efc267cc878476b1e9d8439e1d9ae0fa3a09 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sat, 9 Mar 2024 13:47:46 +0100 Subject: [PATCH 067/109] Parametrize `encode type` test --- .../kotlin/starknet/data/TypedDataTest.kt | 91 +++++++++++-------- 1 file changed, 51 insertions(+), 40 deletions(-) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 897d39896..391fd7f69 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -50,6 +50,51 @@ internal class TypedDataTest { } } + @JvmStatic + fun encodeTypeArguments() = listOf( + Arguments.of(CasesRev0.TD, "Mail", "Mail(from:Person,to:Person,contents:felt)Person(name:felt,wallet:felt)"), + Arguments.of( + CasesRev0.TD_STRUCT_ARR, + "Mail", + "Mail(from:Person,to:Person,posts_len:felt,posts:Post*)Person(name:felt,wallet:felt)Post(title:felt,content:felt)", + ), + Arguments.of( + CasesRev1.TD, + "Mail", + """ + "Mail"("from":"Person","to":"Person","contents":"felt")"Person"("name":"felt","wallet":"felt") + """.trimIndent(), + ), + Arguments.of( + CasesRev1.TD_STRUCT_ARR, + "Mail", + """ + "Mail"("from":"Person","to":"Person","posts_len":"felt","posts":"Post*")"Person"("name":"felt","wallet":"felt")"Post"("title":"felt","content":"felt") + """.trimIndent(), + ), + Arguments.of( + CasesRev1.TD_BASIC_TYPES, + "Example", + """ + "Example"("n0":"felt","n1":"bool","n2":"string","n3":"selector","n4":"u128","n5":"i128","n6":"ContractAddress","n7":"ClassHash","n8":"timestamp","n9":"shortstring") + """.trimIndent(), + ), + Arguments.of( + CasesRev1.TD_PRESET_TYPES, + "Example", + """ + "Example"("n0":"TokenAmount","n1":"NftId")"NftId"("collection_address":"ContractAddress","token_id":"u256")"TokenAmount"("token_address":"ContractAddress","amount":"u256")"u256"("low":"u128","high":"u128") + """.trimIndent(), + ), + Arguments.of( + CasesRev1.TD_ENUM_TYPE, + "Example", + """ + "Example"("someEnum":"MyEnum")"MyEnum"("Variant 1":(),"Variant 2":("u128","u128*"),"Variant 3":("u128")) + """.trimIndent(), + ), + ) + @JvmStatic fun getTypeHashArguments() = listOf( Arguments.of(CasesRev0.TD, "StarkNetDomain", "0x1bfc207425a47a5dfa1a50a4f5241203f50624ca5fdf5e18755765416b8e288"), @@ -142,46 +187,12 @@ internal class TypedDataTest { ) } - @Test - fun `encode type`() { - assertEquals( - "Mail(from:Person,to:Person,contents:felt)Person(name:felt,wallet:felt)", - CasesRev0.TD.encodeType("Mail"), - ) - assertEquals( - "Mail(from:Person,to:Person,posts_len:felt,posts:Post*)Person(name:felt,wallet:felt)Post(title:felt,content:felt)", - CasesRev0.TD_STRUCT_ARR.encodeType("Mail"), - ) - assertEquals( - """ - "Mail"("from":"Person","to":"Person","contents":"felt")"Person"("name":"felt","wallet":"felt") - """.trimIndent(), - CasesRev1.TD.encodeType("Mail"), - ) - assertEquals( - """ - "Mail"("from":"Person","to":"Person","posts_len":"felt","posts":"Post*")"Person"("name":"felt","wallet":"felt")"Post"("title":"felt","content":"felt") - """.trimIndent(), - CasesRev1.TD_STRUCT_ARR.encodeType("Mail"), - ) - assertEquals( - """ - "Example"("n0":"felt","n1":"bool","n2":"string","n3":"selector","n4":"u128","n5":"i128","n6":"ContractAddress","n7":"ClassHash","n8":"timestamp","n9":"shortstring") - """.trimIndent(), - CasesRev1.TD_BASIC_TYPES.encodeType("Example"), - ) - assertEquals( - """ - "Example"("n0":"TokenAmount","n1":"NftId")"NftId"("collection_address":"ContractAddress","token_id":"u256")"TokenAmount"("token_address":"ContractAddress","amount":"u256")"u256"("low":"u128","high":"u128") - """.trimIndent(), - CasesRev1.TD_PRESET_TYPES.encodeType("Example"), - ) - assertEquals( - """ - "Example"("someEnum":"MyEnum")"MyEnum"("Variant 1":(),"Variant 2":("u128","u128*"),"Variant 3":("u128")) - """.trimIndent(), - CasesRev1.TD_ENUM_TYPE.encodeType("Example"), - ) + @ParameterizedTest + @MethodSource("encodeTypeArguments") + fun `encode type`(data: TypedData, typeName: String, expectedResult: String) { + val encodedType = data.encodeType(typeName) + + assertEquals(expectedResult, encodedType) } @Test From 65565cc5d53f7d68d00d9b14b2faf2e649bfe4cb Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sat, 9 Mar 2024 13:48:01 +0100 Subject: [PATCH 068/109] Fix missing dependency test --- .../kotlin/starknet/data/TypedDataTest.kt | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 391fd7f69..7482859c4 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -425,6 +425,23 @@ internal class TypedDataTest { assertEquals("Dangling types are not allowed. Unreferenced type [dangling] was found.", exception.message) } + @Test + fun `missing dependency`() { + val td = TypedData( + customTypes = mapOf( + domainTypeV1, + "house" to listOf(TypedData.StandardType("fridge", "ice cream")), + ), + primaryType = "house", + domain = domainObjectV1, + message = "{\"fridge\": 1}", + ) + val exception = assertThrows { + td.getStructHash("house", "{\"fridge\": 1}") + } + assertEquals("Type [ice cream] is not defined in types.", exception.message) + } + private fun makeTypedData( revision: Revision, includedType: String, @@ -446,22 +463,6 @@ internal class TypedDataTest { } } - @Test - fun `missing dependency`() { - assertThrows( - "Dependency [ice cream] is not defined in types.", - ) { - TypedData( - mapOf( - "house" to listOf(TypedData.StandardType("fridge", "ice cream")), - ), - "felt", - "{}", - "{}", - ).getStructHash("house", "{\"fridge\": 1}") - } - } - @ParameterizedTest @MethodSource("getTypeHashArguments") fun `type hash calculation`(data: TypedData, typeName: String, expectedResult: String) { From 760adb7bd330647cf2f91aa7317cf10a780c293e Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sat, 9 Mar 2024 13:48:29 +0100 Subject: [PATCH 069/109] Format --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 2 +- lib/src/test/kotlin/starknet/data/TypedDataTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 4726b178c..8045d2aaa 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -458,7 +458,7 @@ data class TypedData private constructor( private fun extractEnumTypes(type: String): List { require(type.isEnum()) { "Type [$type] is not an enum." } - return type.substring(1, type.length-1).let{ + return type.substring(1, type.length - 1).let { when { it.trim().isEmpty() -> emptyList() else -> it.split(",").map(String::trim) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 7482859c4..5638517dc 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -41,7 +41,7 @@ internal class TypedDataTest { } internal class CasesRev1 { - companion object{ + companion object { val TD_BASIC_TYPES by lazy { loadTypedData("rev_1/typed_data_basic_types_example.json") } val TD_PRESET_TYPES by lazy { loadTypedData("rev_1/typed_data_preset_types_example.json") } val TD_ENUM_TYPE by lazy { loadTypedData("rev_1/typed_data_enum_example.json") } From 31219cd6212251a3c10a97af2d5588a64a169870 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sat, 9 Mar 2024 16:16:09 +0100 Subject: [PATCH 070/109] Add rev 1 `TypedData.getTypeHash()` tests --- lib/src/test/kotlin/starknet/data/TypedDataTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 5638517dc..923d7d881 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -109,6 +109,10 @@ internal class TypedDataTest { Arguments.of(CasesRev0.TD_SESSION, "Policy", "0x2f0026e78543f036f33e26a8f5891b88c58dc1e20cbbfaf0bb53274da6fa568"), Arguments.of(CasesRev0.TD_VALIDATE, "Validate", "0x1fc17ee4903c000b1c8c6c1424136d4efc4759d1e83915e981b18bc1074a72d"), Arguments.of(CasesRev0.TD_VALIDATE, "Airdrop", "0x37dcb14df3270824843bbbf50c72a724bcb303179dfcce56b653262cbb6957c"), + Arguments.of(CasesRev1.TD_BASIC_TYPES, "Example", "0x1f94cd0be8b4097a41486170fdf09a4cd23aefbc74bb2344718562994c2c111"), + // TODO: verify whether the hash is correct + Arguments.of(CasesRev1.TD_PRESET_TYPES, "Example", "0x1a25a8bb84b761090b1fadaebe762c4b679b0d8883d2bedda695ea340839a55"), + Arguments.of(CasesRev1.TD_ENUM_TYPE, "Example", "0x380a54d417fb58913b904675d94a8a62e2abc3467f4b5439de0fd65fafdd1a8"), ) @JvmStatic From 1cc57c5bfbdefc6ee909a3e842e81835a1359118 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 11 Mar 2024 19:01:42 +0100 Subject: [PATCH 071/109] Fix `TypedData.getMessageHash()` - Use `hashArray()` instead of pedersen --- .../kotlin/com/swmansion/starknet/data/TypedData.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 8045d2aaa..e853a62e3 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -1,7 +1,6 @@ package com.swmansion.starknet.data import com.swmansion.starknet.crypto.HashMethod -import com.swmansion.starknet.crypto.StarknetCurve import com.swmansion.starknet.data.serializers.TypedDataTypeBaseSerializer import com.swmansion.starknet.data.types.Felt import com.swmansion.starknet.data.types.MerkleTree @@ -473,11 +472,13 @@ data class TypedData private constructor( } fun getMessageHash(accountAddress: Felt): Felt { - return StarknetCurve.pedersenOnElements( - Felt.fromShortString("StarkNet Message"), - getStructHash(domain.separatorName, Json.encodeToJsonElement(domain).jsonObject), - accountAddress, - getStructHash(primaryType, message), + return hashArray( + listOf( + Felt.fromShortString("StarkNet Message"), + getStructHash(domain.separatorName, Json.encodeToJsonElement(domain).jsonObject), + accountAddress, + getStructHash(primaryType, message), + ), ) } From 27c9127bc15383dc61d2fd82119e19edcbfcdee4 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 11 Mar 2024 19:08:21 +0100 Subject: [PATCH 072/109] Introduce `StarknetByteArray`; fix logic - Add `StarknetByteArray` - Move logic for encoding long string from `TypedData.prepareLongString()` to `StarknetByteArray` - Fix logic to properly handle short strings <31 - Do not prepend `Felt.ZERO` to `StarknetByteArray.data` --- .../com/swmansion/starknet/data/TypedData.kt | 19 +---- .../starknet/data/types/StarknetByteArray.kt | 27 +++++++ .../starknet/data/types/ByteArrayTests.kt | 77 +++++++++++++++++++ 3 files changed, 107 insertions(+), 16 deletions(-) create mode 100644 lib/src/main/kotlin/com/swmansion/starknet/data/types/StarknetByteArray.kt create mode 100644 lib/src/test/kotlin/com/swmansion/starknet/data/types/ByteArrayTests.kt diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index e853a62e3..81e5297ec 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -4,7 +4,7 @@ import com.swmansion.starknet.crypto.HashMethod import com.swmansion.starknet.data.serializers.TypedDataTypeBaseSerializer import com.swmansion.starknet.data.types.Felt import com.swmansion.starknet.data.types.MerkleTree -import com.swmansion.starknet.extensions.splitToShortStrings +import com.swmansion.starknet.data.types.StarknetByteArray import com.swmansion.starknet.extensions.toFelt import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -317,22 +317,9 @@ data class TypedData private constructor( } private fun prepareLongString(string: String): Felt { - val shortStrings = string.splitToShortStrings() + val byteArray = StarknetByteArray.fromString(string) - val encodedShortStrings = shortStrings.map(Felt::fromShortString) - - val (data, pendingWord, pendingWordLength) = when { - shortStrings.isEmpty() -> Triple(listOf(Felt.ZERO), Felt.ZERO, 0) - shortStrings.last().length == 31 -> Triple(encodedShortStrings, Felt.ZERO, 0) - else -> Triple( - encodedShortStrings.dropLast(1), - encodedShortStrings.last(), - shortStrings.last().length, - ) - } - - val elements = listOf(data.size.toFelt) + data + listOf(pendingWord, pendingWordLength.toFelt) - return hashArray(elements) + return hashArray(byteArray.toCalldata()) } private fun prepareSelector(name: String): Felt { diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/StarknetByteArray.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/StarknetByteArray.kt new file mode 100644 index 000000000..d92b94bdb --- /dev/null +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/StarknetByteArray.kt @@ -0,0 +1,27 @@ +package com.swmansion.starknet.data.types + +import com.swmansion.starknet.data.types.conversions.ConvertibleToCalldata +import com.swmansion.starknet.extensions.splitToShortStrings +import com.swmansion.starknet.extensions.toFelt + +data class StarknetByteArray( + val data: List, + val pendingWord: Felt, + val pendingWordLen: Int, +) : ConvertibleToCalldata { + override fun toCalldata(): List { + return listOf(data.size.toFelt) + data + listOf(pendingWord, pendingWordLen.toFelt) + } + + companion object { + fun fromString(string: String): StarknetByteArray { + val shortStrings = string.splitToShortStrings() + val encodedShortStrings = shortStrings.map(Felt::fromShortString) + + return if (shortStrings.isEmpty() || shortStrings.last().length == 31) + StarknetByteArray(encodedShortStrings, Felt.ZERO, 0) + else + StarknetByteArray(encodedShortStrings.dropLast(1), encodedShortStrings.last(), shortStrings.last().length) + } + } +} diff --git a/lib/src/test/kotlin/com/swmansion/starknet/data/types/ByteArrayTests.kt b/lib/src/test/kotlin/com/swmansion/starknet/data/types/ByteArrayTests.kt new file mode 100644 index 000000000..92f8d9a76 --- /dev/null +++ b/lib/src/test/kotlin/com/swmansion/starknet/data/types/ByteArrayTests.kt @@ -0,0 +1,77 @@ +package com.swmansion.starknet.data.types + +import com.swmansion.starknet.extensions.toFelt +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +internal class ByteArrayTests { + data class ByteArrayTestCase( + val input: String, + val data: List, + val pendingWord: Felt, + val pendingWordLen: Int, + ) + + companion object { + @JvmStatic + private fun getByteArrayFromString(): List { + return listOf( + ByteArrayTestCase( + input = "hello", + data = emptyList(), + pendingWord = Felt.fromHex("0x68656c6c6f"), + pendingWordLen = 5, + ), + ByteArrayTestCase( + input = "Long string, more than 31 characters.", + data = listOf(Felt.fromHex("0x4c6f6e6720737472696e672c206d6f7265207468616e203331206368617261")), + pendingWord = Felt.fromHex("0x63746572732e"), + pendingWordLen = 6, + ), + ByteArrayTestCase( + input = "ABCDEFGHIJKLMNOPQRSTUVWXYZ12345AAADEFGHIJKLMNOPQRSTUVWXYZ12345A", + data = listOf( + Felt.fromHex("0x4142434445464748494a4b4c4d4e4f505152535455565758595a3132333435"), + Felt.fromHex("0x4141414445464748494a4b4c4d4e4f505152535455565758595a3132333435"), + ), + pendingWord = Felt.fromHex("0x41"), + pendingWordLen = 1, + ), + ByteArrayTestCase( + input = "ABCDEFGHIJKLMNOPQRSTUVWXYZ12345", + data = listOf(Felt.fromHex("0x4142434445464748494a4b4c4d4e4f505152535455565758595a3132333435")), + pendingWord = Felt.ZERO, + pendingWordLen = 0, + ), + ByteArrayTestCase( + input = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234", + data = listOf(), + pendingWord = Felt.fromHex("0x4142434445464748494a4b4c4d4e4f505152535455565758595a31323334"), + pendingWordLen = 30, + ), + ByteArrayTestCase( + input = "", + data = emptyList(), + pendingWord = Felt.ZERO, + pendingWordLen = 0, + ), + ) + } + } + + @ParameterizedTest + @MethodSource("getByteArrayFromString") + fun `byte array from string`(testCase: ByteArrayTestCase) { + val byteArray = StarknetByteArray.fromString(testCase.input) + + assertEquals(testCase.data, byteArray.data) + assertEquals(testCase.pendingWord, byteArray.pendingWord) + assertEquals(testCase.pendingWordLen, byteArray.pendingWordLen) + + assertEquals( + listOf(testCase.data.size.toFelt) + testCase.data + listOf(testCase.pendingWord, testCase.pendingWordLen.toFelt), + byteArray.toCalldata(), + ) + } +} From e0b57e593f0e081b75ef27eb71c1d86d2d9bf3e3 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 11 Mar 2024 19:12:08 +0100 Subject: [PATCH 073/109] Fix and refactor `TypedData.feltFromPrimitive()` - Boolean handled outisde of string case - Decimals always handled in the beggining, avoid additional `.long` cast - Throw exception on invalid primitive --- .../com/swmansion/starknet/data/TypedData.kt | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 81e5297ec..6e80c100a 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -288,32 +288,27 @@ data class TypedData private constructor( } private fun feltFromPrimitive(primitive: JsonPrimitive, allowSigned: Boolean = false): Felt { - when (primitive.isString) { - true -> { - if (primitive.content == "") { - return Felt.ZERO - } - - val decimal = primitive.content.toBigIntegerOrNull() - decimal?.let { - return if (allowSigned) Felt.fromSigned(it) else Felt(it) - } - - val boolean = primitive.booleanOrNull - boolean?.let { - return Felt(if (it) 1 else 0) - } + val decimal = primitive.content.toBigIntegerOrNull() + decimal?.let { + return if (allowSigned) Felt.fromSigned(it) else Felt(it) + } + primitive.booleanOrNull?.let { + return if (it) Felt.ONE else Felt.ZERO + } - return try { - Felt.fromHex(primitive.content) - } catch (e: Exception) { - Felt.fromShortString(primitive.content) - } + if (primitive.isString) { + if (primitive.content == "") { + return Felt.ZERO } - false -> { - return if (allowSigned) return Felt.fromSigned(primitive.long) else Felt(primitive.long) + + return try { + Felt.fromHex(primitive.content) + } catch (e: Exception) { + Felt.fromShortString(primitive.content) } } + + throw IllegalArgumentException("Unsupported primitive type: $primitive") } private fun prepareLongString(string: String): Felt { From 5a1e7be0122d3d77034829a0aa29d9edb499c7cc Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 11 Mar 2024 19:17:17 +0100 Subject: [PATCH 074/109] Change `typed_data_basic_types_example.json` - `Json` is unable to convert large number **(investigate better solution?)** --- .../typed_data/rev_1/typed_data_basic_types_example.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/test/resources/typed_data/rev_1/typed_data_basic_types_example.json b/lib/src/test/resources/typed_data/rev_1/typed_data_basic_types_example.json index a8a4eb111..140b7c5ea 100644 --- a/lib/src/test/resources/typed_data/rev_1/typed_data_basic_types_example.json +++ b/lib/src/test/resources/typed_data/rev_1/typed_data_basic_types_example.json @@ -32,7 +32,7 @@ "n2": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "n3": "transfer", "n4": "0x3e8", - "n5": -170141183460469231731687303715884105727, + "n5": "-170141183460469231731687303715884105727", "n6": "0x3e8", "n7": "0x3e8", "n8": 1000, From 942c1ad6f2018e67a02a9752eb8e92686ba5ebc2 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 11 Mar 2024 20:36:15 +0100 Subject: [PATCH 075/109] Better handle `enum` in `TypedData.encodeValue()` - Retrieve by key, not by `.first()`` --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 6e80c100a..45d8a61d3 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -364,15 +364,16 @@ data class TypedData private constructor( val (variantKey, variantData) = value.jsonObject.entries.single() val parent = context.parent ?: throw IllegalArgumentException("Parent is not defined for 'enum' type.") + val key = context.key ?: throw IllegalArgumentException("Key is not defined for 'enum' type.") val parentType = types.getOrElse(parent) { throw IllegalArgumentException("Parent [$parent] is not defined in types.") - }.first() + }.find { it.name == context.key } ?: throw IllegalArgumentException("Key [$key] is not defined in parent [$parent].") require(parentType is EnumType) val enumType = types.getOrElse(parentType.contains) { throw IllegalArgumentException("Type [${parentType.contains}] is not defined in types") } val variantType = enumType.find { it.name == variantKey } ?: throw IllegalArgumentException("Key [$variantKey] is not defined in parent [$parent].") - val variantIndex = extractEnumTypes(variantType.type).indexOf(variantData.jsonPrimitive.content) + val variantIndex = enumType.indexOf(variantType) val encodedSubtypes = extractEnumTypes(variantType.type) .filter { it.isNotEmpty() } From 03363b3964c328e2e564cf7598daf1e69cf2154f Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 11 Mar 2024 21:03:26 +0100 Subject: [PATCH 076/109] Use non-docs `StarknetByteArray.fromString()` logic - Remains to be verified if it's correct one --- .../swmansion/starknet/data/types/StarknetByteArray.kt | 8 +++++--- .../com/swmansion/starknet/data/types/ByteArrayTests.kt | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/StarknetByteArray.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/StarknetByteArray.kt index d92b94bdb..28b892d4b 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/types/StarknetByteArray.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/StarknetByteArray.kt @@ -18,10 +18,12 @@ data class StarknetByteArray( val shortStrings = string.splitToShortStrings() val encodedShortStrings = shortStrings.map(Felt::fromShortString) - return if (shortStrings.isEmpty() || shortStrings.last().length == 31) - StarknetByteArray(encodedShortStrings, Felt.ZERO, 0) + val (data, pendingWord, pendingWordLen) = if (shortStrings.isEmpty() || shortStrings.last().length == 31) + Triple(encodedShortStrings, Felt.ZERO, 0) else - StarknetByteArray(encodedShortStrings.dropLast(1), encodedShortStrings.last(), shortStrings.last().length) + Triple(encodedShortStrings.dropLast(1), encodedShortStrings.last(), shortStrings.last().length) + + return StarknetByteArray(data.ifEmpty { listOf(Felt.ZERO) }, pendingWord, pendingWordLen) } } } diff --git a/lib/src/test/kotlin/com/swmansion/starknet/data/types/ByteArrayTests.kt b/lib/src/test/kotlin/com/swmansion/starknet/data/types/ByteArrayTests.kt index 92f8d9a76..3d3d991a7 100644 --- a/lib/src/test/kotlin/com/swmansion/starknet/data/types/ByteArrayTests.kt +++ b/lib/src/test/kotlin/com/swmansion/starknet/data/types/ByteArrayTests.kt @@ -19,7 +19,7 @@ internal class ByteArrayTests { return listOf( ByteArrayTestCase( input = "hello", - data = emptyList(), + data = listOf(Felt.ZERO), pendingWord = Felt.fromHex("0x68656c6c6f"), pendingWordLen = 5, ), @@ -46,13 +46,13 @@ internal class ByteArrayTests { ), ByteArrayTestCase( input = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234", - data = listOf(), + data = listOf(Felt.ZERO), pendingWord = Felt.fromHex("0x4142434445464748494a4b4c4d4e4f505152535455565758595a31323334"), pendingWordLen = 30, ), ByteArrayTestCase( input = "", - data = emptyList(), + data = listOf(Felt.ZERO), pendingWord = Felt.ZERO, pendingWordLen = 0, ), From d24fdc3f095e69eff9a3149c9f4a462bc5a68250 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Tue, 12 Mar 2024 13:18:05 +0100 Subject: [PATCH 077/109] Add remaining `getStructHash` and `getMessageHashArguments` tests - Verified against bugfixed sn.js --- .../kotlin/starknet/data/TypedDataTest.kt | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 923d7d881..69fdf5fba 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -110,7 +110,6 @@ internal class TypedDataTest { Arguments.of(CasesRev0.TD_VALIDATE, "Validate", "0x1fc17ee4903c000b1c8c6c1424136d4efc4759d1e83915e981b18bc1074a72d"), Arguments.of(CasesRev0.TD_VALIDATE, "Airdrop", "0x37dcb14df3270824843bbbf50c72a724bcb303179dfcce56b653262cbb6957c"), Arguments.of(CasesRev1.TD_BASIC_TYPES, "Example", "0x1f94cd0be8b4097a41486170fdf09a4cd23aefbc74bb2344718562994c2c111"), - // TODO: verify whether the hash is correct Arguments.of(CasesRev1.TD_PRESET_TYPES, "Example", "0x1a25a8bb84b761090b1fadaebe762c4b679b0d8883d2bedda695ea340839a55"), Arguments.of(CasesRev1.TD_ENUM_TYPE, "Example", "0x380a54d417fb58913b904675d94a8a62e2abc3467f4b5439de0fd65fafdd1a8"), ) @@ -154,6 +153,31 @@ internal class TypedDataTest { "message", "0x389e55e4a3d36c6ba04f46f1021a695c934d6782eaf64e47ac059a06a2520c2", ), + Arguments.of( + CasesRev1.TD_BASIC_TYPES, + "StarknetDomain", + "domain", + "0x555f72e550b308e50c1a4f8611483a174026c982a9893a05c185eeb85399657", + ), + Arguments.of( + CasesRev1.TD_BASIC_TYPES, + "Example", + "message", + "0x391d09a51a31dd17f7270aaa9904688fbeeb9c56a7e2d15c5a6af32e981c730", + ), + Arguments.of( + CasesRev1.TD_PRESET_TYPES, + "Example", + "message", + "0x74fba3f77f8a6111a9315bac313bf75ecfa46d1234e0fda60312fb6a6517667", + ), + Arguments.of( + CasesRev1.TD_ENUM_TYPE, + "Example", + "message", + "0x3d4384ff5cec32b86462e89f5a803b55ff0048c4f5a10ba9d6cd381317d9c3", + ), + ) @JvmStatic @@ -188,6 +212,21 @@ internal class TypedDataTest { "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", "0x6038f35de58f40a6afa9d359859b2f930e5eb987580ba6875324cc4dbfcee", ), + Arguments.of( + CasesRev1.TD_BASIC_TYPES, + "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", + "0x2d80b87b8bc32068247c779b2ef0f15f65c9c449325e44a9df480fb01eb43ec", + ), + Arguments.of( + CasesRev1.TD_PRESET_TYPES, + "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", + "0x185b339d5c566a883561a88fb36da301051e2c0225deb325c91bb7aa2f3473a", + ), + Arguments.of( + CasesRev1.TD_ENUM_TYPE, + "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", + "0x3df10475ad5a8f49db4345a04a5b09164d2e24b09f6e1e236bc1ccd87627cc", + ), ) } From 890e3049dab4fa471cd5c759a56f8800cd413c12 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Tue, 12 Mar 2024 15:13:17 +0100 Subject: [PATCH 078/109] Simplify `enum` handling in `TypedData.encodeValue()` --- .../com/swmansion/starknet/data/TypedData.kt | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 45d8a61d3..845f6f403 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -341,6 +341,23 @@ data class TypedData private constructor( } } + private fun getEnumVariants(context: Context): List { + val (parent, key) = context.parent to context.key + + requireNotNull(parent) { "Parent is not defined for 'enum' type." } + requireNotNull(key) { "Key is not defined for 'enum' type." } + + val parentType = types.getOrElse(parent) { throw IllegalArgumentException("Parent [$parent] is not defined in types.") } + val enumType = parentType.find { it.name == key } + ?: throw IllegalArgumentException("Key [$key] is not defined in parent [$parent].") + + require(enumType is EnumType) { "Key [$key] in parent [$parent] is not an 'enum'." } + + val variants = types.getOrElse(enumType.contains) { throw IllegalArgumentException("Type [${enumType.contains}] is not defined in types") } + + return variants + } + internal fun encodeValue( typeName: String, value: JsonElement, @@ -362,25 +379,16 @@ data class TypedData private constructor( "enum" -> { require(revision == Revision.V1) { "'enum' basic type is not supported in revision ${revision.value}." } - val (variantKey, variantData) = value.jsonObject.entries.single() - val parent = context.parent ?: throw IllegalArgumentException("Parent is not defined for 'enum' type.") - val key = context.key ?: throw IllegalArgumentException("Key is not defined for 'enum' type.") - val parentType = types.getOrElse(parent) { - throw IllegalArgumentException("Parent [$parent] is not defined in types.") - }.find { it.name == context.key } ?: throw IllegalArgumentException("Key [$key] is not defined in parent [$parent].") - require(parentType is EnumType) - val enumType = types.getOrElse(parentType.contains) { throw IllegalArgumentException("Type [${parentType.contains}] is not defined in types") } - val variantType = enumType.find { it.name == variantKey } - ?: throw IllegalArgumentException("Key [$variantKey] is not defined in parent [$parent].") - - val variantIndex = enumType.indexOf(variantType) - - val encodedSubtypes = extractEnumTypes(variantType.type) - .filter { it.isNotEmpty() } - .mapIndexed { index, subtype -> - val subtypeData = variantData.jsonArray[index] - encodeValue(subtype, subtypeData).second - } + val (variantName, variantData) = value.jsonObject.entries.singleOrNull()?.let { it.key to it.value.jsonArray } ?: throw IllegalArgumentException("Only one 'enum' variant can be selected.") + val variants = getEnumVariants(context) + + val variantType = variants.singleOrNull { it.name == variantName } ?: throw IllegalArgumentException("Variant [$variantName] is not defined in parent [${context.parent}].") + val variantIndex = variants.indexOf(variantType) + + val encodedSubtypes = extractEnumTypes(variantType.type).mapIndexed { index, subtype -> + val subtypeData = variantData[index] + encodeValue(subtype, subtypeData).second + } "enum" to hashArray(listOf(variantIndex.toFelt) + encodedSubtypes) } From 67c370c34e13e2c9ea6b15b3b68c0ab63b6cabaa Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Tue, 12 Mar 2024 15:15:14 +0100 Subject: [PATCH 079/109] Move `enum` `encodeValue()` logic to a helper function --- .../com/swmansion/starknet/data/TypedData.kt | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 845f6f403..ee4fd9011 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -358,6 +358,21 @@ data class TypedData private constructor( return variants } + private fun prepareEnum(value: JsonObject, context: Context): Felt { + val variants = getEnumVariants(context) + + val (variantName, variantData) = value.entries.singleOrNull()?.let { it.key to it.value.jsonArray } ?: throw IllegalArgumentException("Only one 'enum' variant can be selected.") + val variantType = variants.singleOrNull { it.name == variantName } ?: throw IllegalArgumentException("Variant [$variantName] is not defined in parent [${context.parent}].") + val variantIndex = variants.indexOf(variantType) + + val encodedSubtypes = extractEnumTypes(variantType.type).mapIndexed { index, subtype -> + val subtypeData = variantData[index] + encodeValue(subtype, subtypeData).second + } + + return hashArray(listOf(variantIndex.toFelt) + encodedSubtypes) + } + internal fun encodeValue( typeName: String, value: JsonElement, @@ -379,18 +394,7 @@ data class TypedData private constructor( "enum" -> { require(revision == Revision.V1) { "'enum' basic type is not supported in revision ${revision.value}." } - val (variantName, variantData) = value.jsonObject.entries.singleOrNull()?.let { it.key to it.value.jsonArray } ?: throw IllegalArgumentException("Only one 'enum' variant can be selected.") - val variants = getEnumVariants(context) - - val variantType = variants.singleOrNull { it.name == variantName } ?: throw IllegalArgumentException("Variant [$variantName] is not defined in parent [${context.parent}].") - val variantIndex = variants.indexOf(variantType) - - val encodedSubtypes = extractEnumTypes(variantType.type).mapIndexed { index, subtype -> - val subtypeData = variantData[index] - encodeValue(subtype, subtypeData).second - } - - "enum" to hashArray(listOf(variantIndex.toFelt) + encodedSubtypes) + "enum" to prepareEnum(value.jsonObject, context) } "merkletree" -> { val merkleTreeType = getMerkleTreeType(context) From 4e37267c428b89da0b642fc8b09fc61613e538fc Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Tue, 12 Mar 2024 15:27:34 +0100 Subject: [PATCH 080/109] Move `merkletree` `encodeValue()` logic to a helper method --- .../com/swmansion/starknet/data/TypedData.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index ee4fd9011..6f852c159 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -373,6 +373,13 @@ data class TypedData private constructor( return hashArray(listOf(variantIndex.toFelt) + encodedSubtypes) } + private fun prepareMerkletreeRoot(value: JsonArray, context: Context): Felt { + val merkleTreeType = getMerkleTreeType(context) + val structHashes = value.map { struct -> encodeValue(merkleTreeType, struct).second } + + return MerkleTree(structHashes, hashMethod).rootHash + } + internal fun encodeValue( typeName: String, value: JsonElement, @@ -396,13 +403,7 @@ data class TypedData private constructor( "enum" to prepareEnum(value.jsonObject, context) } - "merkletree" -> { - val merkleTreeType = getMerkleTreeType(context) - val array = value as JsonArray - val structHashes = array.map { struct -> encodeValue(merkleTreeType, struct).second } - val root = MerkleTree(structHashes, hashMethod).rootHash - "felt" to root - } + "merkletree" -> "felt" to prepareMerkletreeRoot(value.jsonArray, context) "string" -> when (revision) { Revision.V0 -> "string" to feltFromPrimitive(value.jsonPrimitive) Revision.V1 -> "string" to prepareLongString(value.jsonPrimitive.content) From 908687cdcdbeb9018c2f42d2683f49779f1c0025 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Tue, 12 Mar 2024 15:31:49 +0100 Subject: [PATCH 081/109] Refactor `encodeValue()` - Remove newlines - Merge handling of rev 0 basic types --- .../main/kotlin/com/swmansion/starknet/data/TypedData.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 6f852c159..a6585eeef 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -393,14 +393,12 @@ data class TypedData private constructor( val hashes = value.jsonArray.map { encodeValue(stripPointer(typeName), it).second } - return typeName to hashArray(hashes) } return when (typeName) { "enum" -> { require(revision == Revision.V1) { "'enum' basic type is not supported in revision ${revision.value}." } - "enum" to prepareEnum(value.jsonObject, context) } "merkletree" -> "felt" to prepareMerkletreeRoot(value.jsonArray, context) @@ -408,10 +406,8 @@ data class TypedData private constructor( Revision.V0 -> "string" to feltFromPrimitive(value.jsonPrimitive) Revision.V1 -> "string" to prepareLongString(value.jsonPrimitive.content) } - "felt" -> "felt" to feltFromPrimitive(value.jsonPrimitive) - "raw" -> "raw" to feltFromPrimitive(value.jsonPrimitive) + "felt", "bool", "raw" -> typeName to feltFromPrimitive(value.jsonPrimitive) "selector" -> "felt" to prepareSelector(value.jsonPrimitive.content) - "bool" -> "bool" to feltFromPrimitive(value.jsonPrimitive) "i128" -> "i128" to feltFromPrimitive(value.jsonPrimitive, allowSigned = true) "u128", "ContractAddress", "ClassHash", "timestamp", "shortstring" -> { require(revision == Revision.V1) { "'$typeName' basic type is not supported in revision ${revision.value}." } From 13bc1829382c4e2ad8e1b4e787b800022e80a3d8 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 13 Mar 2024 10:19:01 +0100 Subject: [PATCH 082/109] Drop `raw` type from `encodeValue()`; Unify and improve `merkletree` and `enum` logic - `Context` no longer has optional values; Instead, `Context` itself is optional in `encodeValue()` - `requireNotNull` on `Context` in `encodeValue()` for `merkletree` and `enum` - `merkletree` therefore no longer supports `raw` type - Adjust tests to reflect that fact, add `typed_data_merkletree_felt_example.json` to test both rev 1 merkletree and raw value encoding - Additionally, rename `CasesRev0.TD_SESSION`-> `CasesRev0.TD_STRUCT_MERKLETREE` - Move logic for resolving type based on context to helper `resolveType()` - Simplify `getMerkleTreeType()` and `getEnumVariants` - Improve error message on `.singleOrNull()` calls - Re-order merkle and enum helper methods --- .../com/swmansion/starknet/data/TypedData.kt | 65 +++++++++---------- .../kotlin/starknet/data/TypedDataTest.kt | 50 +++++++------- .../typed_data_felt_merkletree_example.json | 30 +++++++++ 3 files changed, 90 insertions(+), 55 deletions(-) create mode 100644 lib/src/test/resources/typed_data/rev_1/typed_data_felt_merkletree_example.json diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index a6585eeef..610daf9d7 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -218,8 +218,8 @@ data class TypedData private constructor( ) : Type() data class Context( - val parent: String?, - val key: String?, + val parent: String, + val key: String, ) private fun getDependencies(typeName: String): List { @@ -325,33 +325,33 @@ data class TypedData private constructor( } } - private fun getMerkleTreeType(context: Context): String { + private inline fun resolveType(context: Context): T { val (parent, key) = context.parent to context.key - return if (parent != null && key != null) { - val parentType = types.getOrElse(parent) { throw IllegalArgumentException("Parent [$parent] is not defined in types.") } - val merkleType = parentType.find { it.name == key } - ?: throw IllegalArgumentException("Key [$key] is not defined in parent [$parent].") + val parentType = types.getOrElse(parent) { throw IllegalArgumentException("Parent [$parent] is not defined in types.") } + val targetType = parentType.singleOrNull { it.name == key } + ?: throw IllegalArgumentException("Key [$key] is not defined in parent [$parent] or multiple definitions are present.") - require(merkleType is MerkleTreeType) { "Key [$key] in parent [$parent] is not a merkletree." } + require(targetType is T) { "Key [$key] in parent [$parent] is not a '${T::class.simpleName}'." } - merkleType.contains - } else { - "raw" - } + return targetType } - private fun getEnumVariants(context: Context): List { - val (parent, key) = context.parent to context.key + private fun getMerkleTreeLeavesType(context: Context): String { + val merkleType = resolveType(context) - requireNotNull(parent) { "Parent is not defined for 'enum' type." } - requireNotNull(key) { "Key is not defined for 'enum' type." } + return merkleType.contains + } - val parentType = types.getOrElse(parent) { throw IllegalArgumentException("Parent [$parent] is not defined in types.") } - val enumType = parentType.find { it.name == key } - ?: throw IllegalArgumentException("Key [$key] is not defined in parent [$parent].") + private fun prepareMerkletreeRoot(value: JsonArray, context: Context): Felt { + val merkleTreeType = getMerkleTreeLeavesType(context) + val structHashes = value.map { struct -> encodeValue(merkleTreeType, struct).second } - require(enumType is EnumType) { "Key [$key] in parent [$parent] is not an 'enum'." } + return MerkleTree(structHashes, hashMethod).rootHash + } + + private fun getEnumVariants(context: Context): List { + val enumType = resolveType(context) val variants = types.getOrElse(enumType.contains) { throw IllegalArgumentException("Type [${enumType.contains}] is not defined in types") } @@ -359,10 +359,12 @@ data class TypedData private constructor( } private fun prepareEnum(value: JsonObject, context: Context): Felt { - val variants = getEnumVariants(context) + val (variantName, variantData) = value.entries.singleOrNull()?.let { it.key to it.value.jsonArray } + ?: throw IllegalArgumentException("'enum' value must contain a single variant.") - val (variantName, variantData) = value.entries.singleOrNull()?.let { it.key to it.value.jsonArray } ?: throw IllegalArgumentException("Only one 'enum' variant can be selected.") - val variantType = variants.singleOrNull { it.name == variantName } ?: throw IllegalArgumentException("Variant [$variantName] is not defined in parent [${context.parent}].") + val variants = getEnumVariants(context) + val variantType = variants.singleOrNull { it.name == variantName } + ?: throw IllegalArgumentException("Variant [$variantName] is not defined in 'enum' type [${context.key}] or multiple definitions are present.") val variantIndex = variants.indexOf(variantType) val encodedSubtypes = extractEnumTypes(variantType.type).mapIndexed { index, subtype -> @@ -373,17 +375,10 @@ data class TypedData private constructor( return hashArray(listOf(variantIndex.toFelt) + encodedSubtypes) } - private fun prepareMerkletreeRoot(value: JsonArray, context: Context): Felt { - val merkleTreeType = getMerkleTreeType(context) - val structHashes = value.map { struct -> encodeValue(merkleTreeType, struct).second } - - return MerkleTree(structHashes, hashMethod).rootHash - } - internal fun encodeValue( typeName: String, value: JsonElement, - context: Context = Context(null, null), + context: Context? = null, ): Pair { if (typeName in types) { return typeName to getStructHash(typeName, value.jsonObject) @@ -399,14 +394,18 @@ data class TypedData private constructor( return when (typeName) { "enum" -> { require(revision == Revision.V1) { "'enum' basic type is not supported in revision ${revision.value}." } + requireNotNull(context) { "Context is not provided for 'enum' type." } "enum" to prepareEnum(value.jsonObject, context) } - "merkletree" -> "felt" to prepareMerkletreeRoot(value.jsonArray, context) + "merkletree" -> { + requireNotNull(context) { "Context is not provided for 'merkletree' type." } + "felt" to prepareMerkletreeRoot(value.jsonArray, context) + } "string" -> when (revision) { Revision.V0 -> "string" to feltFromPrimitive(value.jsonPrimitive) Revision.V1 -> "string" to prepareLongString(value.jsonPrimitive.content) } - "felt", "bool", "raw" -> typeName to feltFromPrimitive(value.jsonPrimitive) + "felt", "bool" -> typeName to feltFromPrimitive(value.jsonPrimitive) "selector" -> "felt" to prepareSelector(value.jsonPrimitive.content) "i128" -> "i128" to feltFromPrimitive(value.jsonPrimitive, allowSigned = true) "u128", "ContractAddress", "ClassHash", "timestamp", "shortstring" -> { diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 69fdf5fba..71848c3af 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -1,5 +1,6 @@ package starknet.data +import com.swmansion.starknet.crypto.HashMethod import com.swmansion.starknet.data.TypedData import com.swmansion.starknet.data.TypedData.Context import com.swmansion.starknet.data.TypedData.MerkleTreeType @@ -10,6 +11,8 @@ import com.swmansion.starknet.data.types.MerkleTree import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.encodeToJsonElement +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -35,7 +38,7 @@ internal class TypedDataTest { val TD_FELT_ARR by lazy { loadTypedData("rev_0/typed_data_felt_array_example.json") } val TD_STRING by lazy { loadTypedData("rev_0/typed_data_long_string_example.json") } val TD_STRUCT_ARR by lazy { loadTypedData("rev_0/typed_data_struct_array_example.json") } - val TD_SESSION by lazy { loadTypedData("rev_0/typed_data_session_example.json") } + val TD_STRUCT_MERKLETREE by lazy { loadTypedData("rev_0/typed_data_session_example.json") } val TD_VALIDATE by lazy { loadTypedData("rev_0/typed_data_validate_example.json") } } } @@ -47,6 +50,7 @@ internal class TypedDataTest { val TD_ENUM_TYPE by lazy { loadTypedData("rev_1/typed_data_enum_example.json") } val TD by lazy { loadTypedData("rev_1/typed_data_example.json") } val TD_STRUCT_ARR by lazy { loadTypedData("rev_1/typed_data_struct_array_example.json") } + val TD_FELT_MERKLETREE by lazy { loadTypedData("rev_1/typed_data_felt_merkletree_example.json") } } } @@ -105,8 +109,8 @@ internal class TypedDataTest { Arguments.of(CasesRev0.TD_FELT_ARR, "Mail", "0x5b03497592c0d1fe2f3667b63099761714a895c7df96ec90a85d17bfc7a7a0"), Arguments.of(CasesRev0.TD_STRUCT_ARR, "Post", "0x1d71e69bf476486b43cdcfaf5a85c00bb2d954c042b281040e513080388356d"), Arguments.of(CasesRev0.TD_STRUCT_ARR, "Mail", "0x873b878e35e258fc99e3085d5aaad3a81a0c821f189c08b30def2cde55ff27"), - Arguments.of(CasesRev0.TD_SESSION, "Session", "0x1aa0e1c56b45cf06a54534fa1707c54e520b842feb21d03b7deddb6f1e340c"), - Arguments.of(CasesRev0.TD_SESSION, "Policy", "0x2f0026e78543f036f33e26a8f5891b88c58dc1e20cbbfaf0bb53274da6fa568"), + Arguments.of(CasesRev0.TD_STRUCT_MERKLETREE, "Session", "0x1aa0e1c56b45cf06a54534fa1707c54e520b842feb21d03b7deddb6f1e340c"), + Arguments.of(CasesRev0.TD_STRUCT_MERKLETREE, "Policy", "0x2f0026e78543f036f33e26a8f5891b88c58dc1e20cbbfaf0bb53274da6fa568"), Arguments.of(CasesRev0.TD_VALIDATE, "Validate", "0x1fc17ee4903c000b1c8c6c1424136d4efc4759d1e83915e981b18bc1074a72d"), Arguments.of(CasesRev0.TD_VALIDATE, "Airdrop", "0x37dcb14df3270824843bbbf50c72a724bcb303179dfcce56b653262cbb6957c"), Arguments.of(CasesRev1.TD_BASIC_TYPES, "Example", "0x1f94cd0be8b4097a41486170fdf09a4cd23aefbc74bb2344718562994c2c111"), @@ -142,7 +146,7 @@ internal class TypedDataTest { "0x5650ec45a42c4776a182159b9d33118a46860a6e6639bb8166ff71f3c41eaef", ), Arguments.of( - CasesRev0.TD_SESSION, + CasesRev0.TD_STRUCT_MERKLETREE, "Session", "message", "0x73602062421caf6ad2e942253debfad4584bff58930981364dcd378021defe8", @@ -203,7 +207,7 @@ internal class TypedDataTest { "0x5914ed2764eca2e6a41eb037feefd3d2e33d9af6225a9e7fe31ac943ff712c", ), Arguments.of( - CasesRev0.TD_SESSION, + CasesRev0.TD_STRUCT_MERKLETREE, "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", "0x5d28fa1b31f92e63022f7d85271606e52bed89c046c925f16b09e644dc99794", ), @@ -243,11 +247,11 @@ internal class TypedDataTest { val selector = "transfer" val selectorHash = selectorFromName(selector) - val rawSelectorValueHash = CasesRev0.TD_SESSION.encodeValue( + val rawSelectorValueHash = CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( typeName = "felt", value = Json.encodeToJsonElement(selectorHash), ) - val selectorValueHash = CasesRev0.TD_SESSION.encodeValue( + val selectorValueHash = CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( typeName = "selector", value = Json.encodeToJsonElement(selector), ) @@ -262,23 +266,25 @@ internal class TypedDataTest { @Nested inner class MerkletreeTest { @Test - fun `merkletree type`() { + fun `merkletree with felt leaves`() { + val td = CasesRev1.TD_FELT_MERKLETREE + + val leaves = td.message.getValue("root").jsonArray.map { Felt.fromHex(it.jsonPrimitive.content) } + assertEquals((1..3).map { Felt(it) }, leaves) + val tree = MerkleTree( - listOf( - Felt(1), - Felt(2), - Felt(3), - ), + leafHashes = leaves, + hashFunction = HashMethod.POSEIDON, ) - val leaves = tree.leafHashes - val merkleTreeHash = CasesRev0.TD_SESSION.encodeValue( + val merkleTreeHash = td.encodeValue( typeName = "merkletree", - value = Json.encodeToJsonElement(leaves), + value = Json.encodeToJsonElement(tree.leafHashes), + context = Context(parent = "Example", key = "root"), ).second assertEquals(tree.rootHash, merkleTreeHash) - assertEquals(Felt.fromHex("0x15ac9e457789ef0c56e5d559809e7336a909c14ee2511503fa7af69be1ba639"), merkleTreeHash) + assertEquals(Felt.fromHex("0x48924a3b2a7a7b7cc1c9371357e95e322899880a6534bdfe24e96a828b9d780"), merkleTreeHash) } @Test @@ -290,14 +296,14 @@ internal class TypedDataTest { ) val hashedLeaves = leaves.map { leaf -> - CasesRev0.TD_SESSION.encodeValue( + CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( typeName = "Policy", value = Json.encodeToJsonElement(leaf), ).second } val tree = MerkleTree(hashedLeaves) - val merkleTreeHash = CasesRev0.TD_SESSION.encodeValue( + val merkleTreeHash = CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( typeName = "merkletree", value = Json.encodeToJsonElement(leaves), context = Context(parent = "Session", key = "root"), @@ -313,7 +319,7 @@ internal class TypedDataTest { @Test fun `merkletree from empty leaves`() { assertThrows("Cannot build Merkle tree from an empty list of leaves.") { - CasesRev0.TD_SESSION.encodeValue( + CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( typeName = "merkletree", value = Json.encodeToJsonElement(emptyList()), context = Context(parent = "Session", key = "root"), @@ -344,10 +350,10 @@ internal class TypedDataTest { val invalidKeyContext = Context(parent = "Session", key = "undefinedKey") assertThrows("Parent type '${invalidParentContext.parent}' is not defined in types.") { - CasesRev0.TD_SESSION.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidParentContext) + CasesRev0.TD_STRUCT_MERKLETREE.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidParentContext) } assertThrows("Key '${invalidKeyContext.key}' is not defined in type '${invalidKeyContext.parent}'.") { - CasesRev0.TD_SESSION.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidKeyContext) + CasesRev0.TD_STRUCT_MERKLETREE.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidKeyContext) } } } diff --git a/lib/src/test/resources/typed_data/rev_1/typed_data_felt_merkletree_example.json b/lib/src/test/resources/typed_data/rev_1/typed_data_felt_merkletree_example.json new file mode 100644 index 000000000..dd901cf64 --- /dev/null +++ b/lib/src/test/resources/typed_data/rev_1/typed_data_felt_merkletree_example.json @@ -0,0 +1,30 @@ +{ + "primaryType": "Example", + "types": { + "Example": [ + { "name": "value", "type": "felt" }, + { "name": "root", "type": "merkletree", "contains": "felt" } + ], + "StarknetDomain": [ + { "name": "name", "type": "shortstring" }, + { "name": "version", "type": "shortstring" }, + { "name": "chainId", "type": "shortstring" }, + { "name": "revision", "type": "shortstring" } + ] + }, + "domain": { + "name": "StarkNet Mail", + "version": "1", + "chainId": "1", + "revision": "1" + }, + "message": { + "key": "0x0000000000000000000000000000000000000000000000000000000000000000", + "expires": "0x0000000000000000000000000000000000000000000000000000000000000000", + "root": [ + "0x1", + "0x2", + "0x3" + ] + } +} From 7e109e845e22bb3dabb0c414d818e3c075d90eca Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 13 Mar 2024 12:18:30 +0100 Subject: [PATCH 083/109] Fix `TD_FELT_MERKLETREE` tests `typed_data_felt_merkletree_example.json` --- lib/src/test/kotlin/starknet/data/TypedDataTest.kt | 13 ++++++++++++- .../rev_1/typed_data_felt_merkletree_example.json | 3 +-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 71848c3af..5c63e701c 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -116,6 +116,7 @@ internal class TypedDataTest { Arguments.of(CasesRev1.TD_BASIC_TYPES, "Example", "0x1f94cd0be8b4097a41486170fdf09a4cd23aefbc74bb2344718562994c2c111"), Arguments.of(CasesRev1.TD_PRESET_TYPES, "Example", "0x1a25a8bb84b761090b1fadaebe762c4b679b0d8883d2bedda695ea340839a55"), Arguments.of(CasesRev1.TD_ENUM_TYPE, "Example", "0x380a54d417fb58913b904675d94a8a62e2abc3467f4b5439de0fd65fafdd1a8"), + Arguments.of(CasesRev1.TD_FELT_MERKLETREE, "Example", "0x160b9c0e8a7c561f9c5d9e3cc2990a1b4d26e94aa319e9eb53e163cd06c71be"), ) @JvmStatic @@ -181,7 +182,12 @@ internal class TypedDataTest { "message", "0x3d4384ff5cec32b86462e89f5a803b55ff0048c4f5a10ba9d6cd381317d9c3", ), - + Arguments.of( + CasesRev1.TD_FELT_MERKLETREE, + "Example", + "message", + "0x40ef40c56c0469799a916f0b7e3bc4f1bbf28bf659c53fb8c5ee4d8d1b4f5f0", + ), ) @JvmStatic @@ -231,6 +237,11 @@ internal class TypedDataTest { "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", "0x3df10475ad5a8f49db4345a04a5b09164d2e24b09f6e1e236bc1ccd87627cc", ), + Arguments.of( + CasesRev1.TD_FELT_MERKLETREE, + "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", + "0x4f706783e0d7d0e61433d41343a248a213e9ab341d50ba978dfc055f26484c9", + ), ) } diff --git a/lib/src/test/resources/typed_data/rev_1/typed_data_felt_merkletree_example.json b/lib/src/test/resources/typed_data/rev_1/typed_data_felt_merkletree_example.json index dd901cf64..891476c48 100644 --- a/lib/src/test/resources/typed_data/rev_1/typed_data_felt_merkletree_example.json +++ b/lib/src/test/resources/typed_data/rev_1/typed_data_felt_merkletree_example.json @@ -19,8 +19,7 @@ "revision": "1" }, "message": { - "key": "0x0000000000000000000000000000000000000000000000000000000000000000", - "expires": "0x0000000000000000000000000000000000000000000000000000000000000000", + "value": "0x2137", "root": [ "0x1", "0x2", From 879f995393142e0c21ee54060eb7a2206f7d5470 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 13 Mar 2024 12:31:32 +0100 Subject: [PATCH 084/109] Add simple rev 1 test with no extra types and structs --- lib/src/test/kotlin/starknet/data/TypedDataTest.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 5c63e701c..a5c0717b4 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -113,6 +113,9 @@ internal class TypedDataTest { Arguments.of(CasesRev0.TD_STRUCT_MERKLETREE, "Policy", "0x2f0026e78543f036f33e26a8f5891b88c58dc1e20cbbfaf0bb53274da6fa568"), Arguments.of(CasesRev0.TD_VALIDATE, "Validate", "0x1fc17ee4903c000b1c8c6c1424136d4efc4759d1e83915e981b18bc1074a72d"), Arguments.of(CasesRev0.TD_VALIDATE, "Airdrop", "0x37dcb14df3270824843bbbf50c72a724bcb303179dfcce56b653262cbb6957c"), + Arguments.of(CasesRev1.TD, "StarknetDomain", "0x1ff2f602e42168014d405a94f75e8a93d640751d71d16311266e140d8b0a210"), + Arguments.of(CasesRev1.TD, "Person", "0x30f7aa21b8d67cb04c30f962dd29b95ab320cb929c07d1605f5ace304dadf34"), + Arguments.of(CasesRev1.TD, "Mail", "0x560430bf7a02939edd1a5c104e7b7a55bbab9f35928b1cf5c7c97de3a907bd"), Arguments.of(CasesRev1.TD_BASIC_TYPES, "Example", "0x1f94cd0be8b4097a41486170fdf09a4cd23aefbc74bb2344718562994c2c111"), Arguments.of(CasesRev1.TD_PRESET_TYPES, "Example", "0x1a25a8bb84b761090b1fadaebe762c4b679b0d8883d2bedda695ea340839a55"), Arguments.of(CasesRev1.TD_ENUM_TYPE, "Example", "0x380a54d417fb58913b904675d94a8a62e2abc3467f4b5439de0fd65fafdd1a8"), @@ -158,6 +161,12 @@ internal class TypedDataTest { "message", "0x389e55e4a3d36c6ba04f46f1021a695c934d6782eaf64e47ac059a06a2520c2", ), + Arguments.of( + CasesRev1.TD, + "StarknetDomain", + "domain", + "0x555f72e550b308e50c1a4f8611483a174026c982a9893a05c185eeb85399657", + ), Arguments.of( CasesRev1.TD_BASIC_TYPES, "StarknetDomain", @@ -222,6 +231,11 @@ internal class TypedDataTest { "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", "0x6038f35de58f40a6afa9d359859b2f930e5eb987580ba6875324cc4dbfcee", ), + Arguments.of( + CasesRev1.TD, + "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", + "0x7f6e8c3d8965b5535f5cc68f837c04e3bbe568535b71aa6c621ddfb188932b8", + ), Arguments.of( CasesRev1.TD_BASIC_TYPES, "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", From 0b6ae2275a07a3abc2332f9ef9c8f5d529721553 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 13 Mar 2024 12:40:04 +0100 Subject: [PATCH 085/109] Revert "Use non-docs `StarknetByteArray.fromString()` logic" This reverts commit 03363b3964c328e2e564cf7598daf1e69cf2154f. It was confirmed that logic from the docs should be used: https://github.com/starknet-io/starknet.js/issues/1003#issuecomment-1994171859 --- .../swmansion/starknet/data/types/StarknetByteArray.kt | 8 +++----- .../com/swmansion/starknet/data/types/ByteArrayTests.kt | 6 +++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/types/StarknetByteArray.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/types/StarknetByteArray.kt index 28b892d4b..d92b94bdb 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/types/StarknetByteArray.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/types/StarknetByteArray.kt @@ -18,12 +18,10 @@ data class StarknetByteArray( val shortStrings = string.splitToShortStrings() val encodedShortStrings = shortStrings.map(Felt::fromShortString) - val (data, pendingWord, pendingWordLen) = if (shortStrings.isEmpty() || shortStrings.last().length == 31) - Triple(encodedShortStrings, Felt.ZERO, 0) + return if (shortStrings.isEmpty() || shortStrings.last().length == 31) + StarknetByteArray(encodedShortStrings, Felt.ZERO, 0) else - Triple(encodedShortStrings.dropLast(1), encodedShortStrings.last(), shortStrings.last().length) - - return StarknetByteArray(data.ifEmpty { listOf(Felt.ZERO) }, pendingWord, pendingWordLen) + StarknetByteArray(encodedShortStrings.dropLast(1), encodedShortStrings.last(), shortStrings.last().length) } } } diff --git a/lib/src/test/kotlin/com/swmansion/starknet/data/types/ByteArrayTests.kt b/lib/src/test/kotlin/com/swmansion/starknet/data/types/ByteArrayTests.kt index 3d3d991a7..92f8d9a76 100644 --- a/lib/src/test/kotlin/com/swmansion/starknet/data/types/ByteArrayTests.kt +++ b/lib/src/test/kotlin/com/swmansion/starknet/data/types/ByteArrayTests.kt @@ -19,7 +19,7 @@ internal class ByteArrayTests { return listOf( ByteArrayTestCase( input = "hello", - data = listOf(Felt.ZERO), + data = emptyList(), pendingWord = Felt.fromHex("0x68656c6c6f"), pendingWordLen = 5, ), @@ -46,13 +46,13 @@ internal class ByteArrayTests { ), ByteArrayTestCase( input = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234", - data = listOf(Felt.ZERO), + data = listOf(), pendingWord = Felt.fromHex("0x4142434445464748494a4b4c4d4e4f505152535455565758595a31323334"), pendingWordLen = 30, ), ByteArrayTestCase( input = "", - data = listOf(Felt.ZERO), + data = emptyList(), pendingWord = Felt.ZERO, pendingWordLen = 0, ), From a4a5ef9d04ef2c9a57f78ec595155891d57fb69e Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 13 Mar 2024 12:57:20 +0100 Subject: [PATCH 086/109] Fix `StandardAccountTest` typed data tests; Add rev 1 test; Group to `SignTypedDataTest` --- .../starknet/account/StandardAccountTest.kt | 97 ++++++++++++------- 1 file changed, 61 insertions(+), 36 deletions(-) diff --git a/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt b/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt index 192470fdc..67e1f3e20 100644 --- a/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt +++ b/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt @@ -463,48 +463,73 @@ class StandardAccountTest { } } - @Test - fun `sign TypedData`() { - val typedData = loadTypedData("typed_data_struct_array_example.json") - - // Sign typedData - val signature = account.signTypedData(typedData) - assertTrue(signature.isNotEmpty()) - - // Verify the signature - val request = account.verifyTypedDataSignature(typedData, signature) - val isValid = request.send() - assertTrue(isValid) - - // Verify invalid signature does not pass - val request2 = account.verifyTypedDataSignature(typedData, listOf(Felt.ONE, Felt.ONE)) - val isValid2 = request2.send() - assertFalse(isValid2) - } + @Nested + inner class SignTypedDataTest { + private val tdRev0 by lazy { loadTypedData("rev_0/typed_data_struct_array_example.json") } + private val tdRev1 by lazy { loadTypedData("rev_1/typed_data_basic_types_example.json") } - @Test - fun `sign TypedData rethrows exceptions other than signature related`() { - val httpService = mock { - on { send(any()) } doReturn HttpResponse( - false, - 500, - """ + @Test + fun `sign TypedData revision 0`() { + val typedData = tdRev0 + + // Sign typedData + val signature = account.signTypedData(typedData) + assertTrue(signature.isNotEmpty()) + + // Verify the signature + val request = account.verifyTypedDataSignature(typedData, signature) + val isValid = request.send() + assertTrue(isValid) + + // Verify invalid signature does not pass + val request2 = account.verifyTypedDataSignature(typedData, listOf(Felt.ONE, Felt.ONE)) + val isValid2 = request2.send() + assertFalse(isValid2) + } + + @Test + fun `sign TypedData revision 1`() { + val typedData = tdRev1 + + // Sign typedData + val signature = account.signTypedData(typedData) + assertTrue(signature.isNotEmpty()) + + // Verify the signature + val request = account.verifyTypedDataSignature(typedData, signature) + val isValid = request.send() + assertTrue(isValid) + + // Verify invalid signature does not pass + val request2 = account.verifyTypedDataSignature(typedData, listOf(Felt.ONE, Felt.ONE)) + val isValid2 = request2.send() + assertFalse(isValid2) + } + + @Test + fun `sign TypedData rethrows exceptions other than signature related`() { + val httpService = mock { + on { send(any()) } doReturn HttpResponse( + false, + 500, + """ { "something": "broke" } - """.trimIndent(), - ) - } - val provider = JsonRpcProvider(devnetClient.rpcUrl, httpService) - val account = StandardAccount(Felt.ONE, Felt.ONE, provider, chainId) + """.trimIndent(), + ) + } + val provider = JsonRpcProvider(devnetClient.rpcUrl, httpService) + val account = StandardAccount(Felt.ONE, Felt.ONE, provider, chainId) - val typedData = loadTypedData("typed_data_struct_array_example.json") - val signature = account.signTypedData(typedData) - assertTrue(signature.isNotEmpty()) + val typedData = tdRev0 + val signature = account.signTypedData(typedData) + assertTrue(signature.isNotEmpty()) - val request = account.verifyTypedDataSignature(typedData, signature) - assertThrows(RequestFailedException::class.java) { - request.send() + val request = account.verifyTypedDataSignature(typedData, signature) + assertThrows(RequestFailedException::class.java) { + request.send() + } } } From 9d9fad3d5b29a207d28c3f5bc2b506ee102430ad Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 13 Mar 2024 13:25:01 +0100 Subject: [PATCH 087/109] Add `encodeType` `merkletree` tests --- lib/src/test/kotlin/starknet/data/TypedDataTest.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index a5c0717b4..f131ce32a 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -62,6 +62,11 @@ internal class TypedDataTest { "Mail", "Mail(from:Person,to:Person,posts_len:felt,posts:Post*)Person(name:felt,wallet:felt)Post(title:felt,content:felt)", ), + Arguments.of( + CasesRev0.TD_STRUCT_MERKLETREE, + "Session", + "Session(key:felt,expires:felt,root:merkletree)", + ), Arguments.of( CasesRev1.TD, "Mail", @@ -97,6 +102,13 @@ internal class TypedDataTest { "Example"("someEnum":"MyEnum")"MyEnum"("Variant 1":(),"Variant 2":("u128","u128*"),"Variant 3":("u128")) """.trimIndent(), ), + Arguments.of( + CasesRev1.TD_FELT_MERKLETREE, + "Example", + """ + "Example"("value":"felt","root":"merkletree") + """.trimIndent() + ) ) @JvmStatic From 772688dcfdfb1f1377913f2f88709a58501bbb84 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 13 Mar 2024 13:39:19 +0100 Subject: [PATCH 088/109] Format --- lib/src/test/kotlin/starknet/data/TypedDataTest.kt | 4 ++-- .../typed_data/rev_1/typed_data_basic_types_example.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index f131ce32a..28969b340 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -107,8 +107,8 @@ internal class TypedDataTest { "Example", """ "Example"("value":"felt","root":"merkletree") - """.trimIndent() - ) + """.trimIndent(), + ), ) @JvmStatic diff --git a/lib/src/test/resources/typed_data/rev_1/typed_data_basic_types_example.json b/lib/src/test/resources/typed_data/rev_1/typed_data_basic_types_example.json index 140b7c5ea..ed0cea61a 100644 --- a/lib/src/test/resources/typed_data/rev_1/typed_data_basic_types_example.json +++ b/lib/src/test/resources/typed_data/rev_1/typed_data_basic_types_example.json @@ -38,4 +38,4 @@ "n8": 1000, "n9": "transfer" } -} \ No newline at end of file +} From c26e2dddf304d5deef4d4a6273d6ee2abd9106b1 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 13 Mar 2024 14:01:22 +0100 Subject: [PATCH 089/109] Fix `merkletree with invalid contains` test --- lib/src/test/kotlin/starknet/data/TypedDataTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 28969b340..0acedcb5c 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -366,13 +366,14 @@ internal class TypedDataTest { @Test fun `merkletree with invalid contains`() { - assertThrows("Merkletree 'contains' field cannot be an array, got 'felt*' in type 'root'.") { + val exception = assertThrows { MerkleTreeType( name = "root", type = "merkletree", contains = "felt*", ) } + assertEquals("Merkletree 'contains' field cannot be an array, got [felt*] in type [root].", exception.message) } @Test From 89428cb231c639d024ae7b22b744825b40560ca2 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 13 Mar 2024 14:13:26 +0100 Subject: [PATCH 090/109] Don't nest test cases --- .../starknet/account/StandardAccountTest.kt | 115 ++++----- .../kotlin/starknet/data/TypedDataTest.kt | 243 +++++++++--------- 2 files changed, 177 insertions(+), 181 deletions(-) diff --git a/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt b/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt index 67e1f3e20..3839c1478 100644 --- a/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt +++ b/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt @@ -463,73 +463,70 @@ class StandardAccountTest { } } - @Nested - inner class SignTypedDataTest { - private val tdRev0 by lazy { loadTypedData("rev_0/typed_data_struct_array_example.json") } - private val tdRev1 by lazy { loadTypedData("rev_1/typed_data_basic_types_example.json") } + private val tdRev0 by lazy { loadTypedData("rev_0/typed_data_struct_array_example.json") } + private val tdRev1 by lazy { loadTypedData("rev_1/typed_data_basic_types_example.json") } - @Test - fun `sign TypedData revision 0`() { - val typedData = tdRev0 - - // Sign typedData - val signature = account.signTypedData(typedData) - assertTrue(signature.isNotEmpty()) - - // Verify the signature - val request = account.verifyTypedDataSignature(typedData, signature) - val isValid = request.send() - assertTrue(isValid) - - // Verify invalid signature does not pass - val request2 = account.verifyTypedDataSignature(typedData, listOf(Felt.ONE, Felt.ONE)) - val isValid2 = request2.send() - assertFalse(isValid2) - } + @Test + fun `sign TypedData revision 0`() { + val typedData = tdRev0 + + // Sign typedData + val signature = account.signTypedData(typedData) + assertTrue(signature.isNotEmpty()) + + // Verify the signature + val request = account.verifyTypedDataSignature(typedData, signature) + val isValid = request.send() + assertTrue(isValid) + + // Verify invalid signature does not pass + val request2 = account.verifyTypedDataSignature(typedData, listOf(Felt.ONE, Felt.ONE)) + val isValid2 = request2.send() + assertFalse(isValid2) + } - @Test - fun `sign TypedData revision 1`() { - val typedData = tdRev1 - - // Sign typedData - val signature = account.signTypedData(typedData) - assertTrue(signature.isNotEmpty()) - - // Verify the signature - val request = account.verifyTypedDataSignature(typedData, signature) - val isValid = request.send() - assertTrue(isValid) - - // Verify invalid signature does not pass - val request2 = account.verifyTypedDataSignature(typedData, listOf(Felt.ONE, Felt.ONE)) - val isValid2 = request2.send() - assertFalse(isValid2) - } + @Test + fun `sign TypedData revision 1`() { + val typedData = tdRev1 + + // Sign typedData + val signature = account.signTypedData(typedData) + assertTrue(signature.isNotEmpty()) + + // Verify the signature + val request = account.verifyTypedDataSignature(typedData, signature) + val isValid = request.send() + assertTrue(isValid) + + // Verify invalid signature does not pass + val request2 = account.verifyTypedDataSignature(typedData, listOf(Felt.ONE, Felt.ONE)) + val isValid2 = request2.send() + assertFalse(isValid2) + } - @Test - fun `sign TypedData rethrows exceptions other than signature related`() { - val httpService = mock { - on { send(any()) } doReturn HttpResponse( - false, - 500, - """ + @Test + fun `sign TypedData rethrows exceptions other than signature related`() { + val httpService = mock { + on { send(any()) } doReturn HttpResponse( + false, + 500, + """ { "something": "broke" } - """.trimIndent(), - ) - } - val provider = JsonRpcProvider(devnetClient.rpcUrl, httpService) - val account = StandardAccount(Felt.ONE, Felt.ONE, provider, chainId) + """.trimIndent(), + ) + } + val provider = JsonRpcProvider(devnetClient.rpcUrl, httpService) + val account = StandardAccount(Felt.ONE, Felt.ONE, provider, chainId) - val typedData = tdRev0 - val signature = account.signTypedData(typedData) - assertTrue(signature.isNotEmpty()) + val typedData = tdRev0 + val signature = account.signTypedData(typedData) + assertTrue(signature.isNotEmpty()) - val request = account.verifyTypedDataSignature(typedData, signature) - assertThrows(RequestFailedException::class.java) { - request.send() - } + val request = account.verifyTypedDataSignature(typedData, signature) + assertThrows(RequestFailedException::class.java) { + request.send() } } diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 0acedcb5c..d6f91af5a 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -54,6 +54,38 @@ internal class TypedDataTest { } } + private val domainTypeV0 = "StarkNetDomain" to listOf( + TypedData.StandardType("name", "felt"), + TypedData.StandardType("version", "felt"), + TypedData.StandardType("chainId", "felt"), + ) + private val domainTypeV1 = "StarknetDomain" to listOf( + TypedData.StandardType("name", "shortstring"), + TypedData.StandardType("version", "shortstring"), + TypedData.StandardType("chainId", "shortstring"), + TypedData.StandardType("revision", "shortstring"), + ) + private val domainObjectV0 = """ + { + "name": "DomainV0", + "version": 1, + "chainId": 2137 + } + """.trimIndent() + private val domainObjectV1 = """ + { + "name": "DomainV1", + "version": "1", + "chainId": "2137", + "revision": "1" + } + """.trimIndent() + + private val basicTypesV0 = setOf("felt", "bool", "string", "selector", "merkletree") + private val basicTypesV1 = basicTypesV0 + setOf("enum", "u128", "i128", "ContractAddress", "ClassHash", "timestamp", "shortstring") + private val presetTypesV0 = emptySet() + private val presetTypesV1 = setOf("u256", "TokenAmount", "NftId") + @JvmStatic fun encodeTypeArguments() = listOf( Arguments.of(CasesRev0.TD, "Mail", "Mail(from:Person,to:Person,contents:felt)Person(name:felt,wallet:felt)"), @@ -396,158 +428,125 @@ internal class TypedDataTest { } } - @Nested - inner class InvalidTypesTest { - private val domainTypeV0 = "StarkNetDomain" to listOf( - TypedData.StandardType("name", "felt"), - TypedData.StandardType("version", "felt"), - TypedData.StandardType("chainId", "felt"), - ) - private val domainTypeV1 = "StarknetDomain" to listOf( - TypedData.StandardType("name", "shortstring"), - TypedData.StandardType("version", "shortstring"), - TypedData.StandardType("chainId", "shortstring"), - TypedData.StandardType("revision", "shortstring"), - ) - private val domainObjectV0 = """ - { - "name": "DomainV0", - "version": 1, - "chainId": 2137 - } - """.trimIndent() - private val domainObjectV1 = """ - { - "name": "DomainV1", - "version": "1", - "chainId": "2137", - "revision": "1" - } - """.trimIndent() - - private val basicTypesV0 = setOf("felt", "bool", "string", "selector", "merkletree") - private val basicTypesV1 = basicTypesV0 + setOf("enum", "u128", "i128", "ContractAddress", "ClassHash", "timestamp", "shortstring") - private val presetTypesV0 = emptySet() - private val presetTypesV1 = setOf("u256", "TokenAmount", "NftId") - @ParameterizedTest - @EnumSource(Revision::class) - fun `basic types redefinition`(revision: Revision) { - val types = when (revision) { - Revision.V0 -> basicTypesV0 - Revision.V1 -> basicTypesV1 - } - types.forEach { type -> - val exception = assertThrows { - makeTypedData(revision, type) - } - assertEquals("Types must not contain basic types. [$type] was found.", exception.message) - } + @ParameterizedTest + @EnumSource(Revision::class) + fun `basic types redefinition`(revision: Revision) { + val types = when (revision) { + Revision.V0 -> basicTypesV0 + Revision.V1 -> basicTypesV1 } - @ParameterizedTest - @EnumSource(Revision::class) - fun `preset types redefinition`(revision: Revision) { - val types = when (revision) { - Revision.V0 -> presetTypesV0 - Revision.V1 -> presetTypesV1 + types.forEach { type -> + val exception = assertThrows { + makeTypedData(revision, type) } + assertEquals("Types must not contain basic types. [$type] was found.", exception.message) + } + } - types.forEach { type -> - val exception = assertThrows { - makeTypedData(revision, type) - } - assertEquals("Types must not contain preset types. [$type] was found.", exception.message) - } + @ParameterizedTest + @EnumSource(Revision::class) + fun `preset types redefinition`(revision: Revision) { + val types = when (revision) { + Revision.V0 -> presetTypesV0 + Revision.V1 -> presetTypesV1 } - @Test - fun `type with asterisk`() { - val types = listOf("felt*", "u256*", "mytype*") - types.forEach { type -> - val exception = assertThrows { - makeTypedData(Revision.V1, type) - } - assertEquals("Types cannot end in *. [$type] was found.", exception.message) + types.forEach { type -> + val exception = assertThrows { + makeTypedData(revision, type) } + assertEquals("Types must not contain preset types. [$type] was found.", exception.message) } + } - @Test - fun `type with parentheses`() { - val types = listOf("(left", "right)", "(both)") - types.forEach { type -> - val exception = assertThrows { - makeTypedData(Revision.V1, type) - } - assertEquals("Types cannot be enclosed in parentheses. [$type] was found.", exception.message) + @Test + fun `type with asterisk`() { + val types = listOf("felt*", "u256*", "mytype*") + types.forEach { type -> + val exception = assertThrows { + makeTypedData(Revision.V1, type) } + assertEquals("Types cannot end in *. [$type] was found.", exception.message) } + } - @Test - fun `type with commas`() { - val types = listOf(",mytype", "my,type", "mytype,") - types.forEach { type -> - val exception = assertThrows { - makeTypedData(Revision.V1, type) - } - assertEquals("Types cannot contain commas. [$type] was found.", exception.message) + @Test + fun `type with parentheses`() { + val types = listOf("(left", "right)", "(both)") + types.forEach { type -> + val exception = assertThrows { + makeTypedData(Revision.V1, type) } + assertEquals("Types cannot be enclosed in parentheses. [$type] was found.", exception.message) } + } - @Test - fun `dangling types`() { + @Test + fun `type with commas`() { + val types = listOf(",mytype", "my,type", "mytype,") + types.forEach { type -> val exception = assertThrows { - TypedData( - customTypes = mapOf( - domainTypeV1, - "dangling" to emptyList(), - "mytype" to emptyList(), - ), - primaryType = "mytype", - domain = domainObjectV1, - message = "{\"mytype\": 1}", - ) + makeTypedData(Revision.V1, type) } - assertEquals("Dangling types are not allowed. Unreferenced type [dangling] was found.", exception.message) + assertEquals("Types cannot contain commas. [$type] was found.", exception.message) } + } - @Test - fun `missing dependency`() { - val td = TypedData( + @Test + fun `dangling types`() { + val exception = assertThrows { + TypedData( customTypes = mapOf( domainTypeV1, - "house" to listOf(TypedData.StandardType("fridge", "ice cream")), + "dangling" to emptyList(), + "mytype" to emptyList(), ), - primaryType = "house", + primaryType = "mytype", domain = domainObjectV1, - message = "{\"fridge\": 1}", + message = "{\"mytype\": 1}", ) - val exception = assertThrows { - td.getStructHash("house", "{\"fridge\": 1}") - } - assertEquals("Type [ice cream] is not defined in types.", exception.message) } + assertEquals("Dangling types are not allowed. Unreferenced type [dangling] was found.", exception.message) + } - private fun makeTypedData( - revision: Revision, - includedType: String, - ) { - val (domainType, domainObject) = when (revision) { - Revision.V0 -> domainTypeV0 to domainObjectV0 - Revision.V1 -> domainTypeV1 to domainObjectV1 - } + @Test + fun `missing dependency`() { + val td = TypedData( + customTypes = mapOf( + domainTypeV1, + "house" to listOf(TypedData.StandardType("fridge", "ice cream")), + ), + primaryType = "house", + domain = domainObjectV1, + message = "{\"fridge\": 1}", + ) + val exception = assertThrows { + td.getStructHash("house", "{\"fridge\": 1}") + } + assertEquals("Type [ice cream] is not defined in types.", exception.message) + } - TypedData( - customTypes = mapOf( - domainType, - includedType to emptyList(), - ), - primaryType = includedType, - domain = domainObject, - message = "{\"$includedType\": 1}", - ) + private fun makeTypedData( + revision: Revision, + includedType: String, + ) { + val (domainType, domainObject) = when (revision) { + Revision.V0 -> domainTypeV0 to domainObjectV0 + Revision.V1 -> domainTypeV1 to domainObjectV1 } + + TypedData( + customTypes = mapOf( + domainType, + includedType to emptyList(), + ), + primaryType = includedType, + domain = domainObject, + message = "{\"$includedType\": 1}", + ) } @ParameterizedTest From 03abdae17cc4696dc5a36e94444e9a0aaa8b30b2 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 13 Mar 2024 14:15:05 +0100 Subject: [PATCH 091/109] Format --- lib/src/test/kotlin/starknet/data/TypedDataTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index d6f91af5a..2911ee5cf 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -428,8 +428,6 @@ internal class TypedDataTest { } } - - @ParameterizedTest @EnumSource(Revision::class) fun `basic types redefinition`(revision: Revision) { From eb9908f86e66f5dc775af772b73be953ffd830cc Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 13 Mar 2024 15:28:46 +0100 Subject: [PATCH 092/109] Require `enum` to be used only in revision 1 --- .../com/swmansion/starknet/data/TypedData.kt | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 610daf9d7..9158cba2f 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -137,10 +137,16 @@ data class TypedData private constructor( val referencedTypes = customTypes.values.flatten().flatMap { when (it) { - is EnumType -> listOf(it.contains) + is EnumType -> { + require(revision == Revision.V1) { "'enum' basic type is not supported in revision ${revision.value}." } + listOf(it.contains) + } is MerkleTreeType -> listOf(it.contains) is StandardType -> when { - it.type.isEnum() && revision == Revision.V1 -> extractEnumTypes(it.type) + it.type.isEnum() -> { + require(revision == Revision.V1) { "Enum types are not supported in revision ${revision.value}." } + extractEnumTypes(it.type) + } else -> listOf(stripPointer(it.type)) } } @@ -232,8 +238,14 @@ data class TypedData private constructor( params.forEach { param -> val extractedTypes = when { - param is EnumType && revision == Revision.V1 -> listOf(param.contains) - param.type.isEnum() && revision == Revision.V1 -> extractEnumTypes(param.type) + param is EnumType -> { + require(revision == Revision.V1) { "'enum' basic type is not supported in revision ${revision.value}." } + listOf(param.contains) + } + param.type.isEnum() -> { + require(revision == Revision.V1) { "Enum types are not supported in revision ${revision.value}." } + extractEnumTypes(param.type) + } else -> listOf(param.type) }.map { stripPointer(it) } @@ -273,12 +285,16 @@ data class TypedData private constructor( else -> it.type } val typeString = when { - targetType.isEnum() -> extractEnumTypes(targetType).joinToString( - separator = ",", - prefix = "(", - postfix = ")", - transform = ::escape, - ) + targetType.isEnum() -> { + require(revision == Revision.V1) { "Enum types are not supported in revision ${revision.value}." } + + extractEnumTypes(targetType).joinToString( + separator = ",", + prefix = "(", + postfix = ")", + transform = ::escape, + ) + } else -> escape(targetType) } "${escape(it.name)}:$typeString" From 3cf768fe6f4fd2c882f43d01a0053bf906381be1 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 13 Mar 2024 15:43:09 +0100 Subject: [PATCH 093/109] Don't nest merkletree tests --- .../kotlin/starknet/data/TypedDataTest.kt | 150 +++++++++--------- 1 file changed, 73 insertions(+), 77 deletions(-) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 2911ee5cf..7f505dd0e 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -14,7 +14,6 @@ import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonPrimitive import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest @@ -332,99 +331,96 @@ internal class TypedDataTest { ) } - @Nested - inner class MerkletreeTest { - @Test - fun `merkletree with felt leaves`() { - val td = CasesRev1.TD_FELT_MERKLETREE + @Test + fun `merkletree with felt leaves`() { + val td = CasesRev1.TD_FELT_MERKLETREE - val leaves = td.message.getValue("root").jsonArray.map { Felt.fromHex(it.jsonPrimitive.content) } - assertEquals((1..3).map { Felt(it) }, leaves) + val leaves = td.message.getValue("root").jsonArray.map { Felt.fromHex(it.jsonPrimitive.content) } + assertEquals((1..3).map { Felt(it) }, leaves) - val tree = MerkleTree( - leafHashes = leaves, - hashFunction = HashMethod.POSEIDON, - ) + val tree = MerkleTree( + leafHashes = leaves, + hashFunction = HashMethod.POSEIDON, + ) - val merkleTreeHash = td.encodeValue( - typeName = "merkletree", - value = Json.encodeToJsonElement(tree.leafHashes), - context = Context(parent = "Example", key = "root"), - ).second + val merkleTreeHash = td.encodeValue( + typeName = "merkletree", + value = Json.encodeToJsonElement(tree.leafHashes), + context = Context(parent = "Example", key = "root"), + ).second + + assertEquals(tree.rootHash, merkleTreeHash) + assertEquals(Felt.fromHex("0x48924a3b2a7a7b7cc1c9371357e95e322899880a6534bdfe24e96a828b9d780"), merkleTreeHash) + } + + @Test + fun `merkletree with custom types`() { + val leaves = listOf( + mapOf("contractAddress" to "0x1", "selector" to "transfer"), + mapOf("contractAddress" to "0x2", "selector" to "transfer"), + mapOf("contractAddress" to "0x3", "selector" to "transfer"), + ) - assertEquals(tree.rootHash, merkleTreeHash) - assertEquals(Felt.fromHex("0x48924a3b2a7a7b7cc1c9371357e95e322899880a6534bdfe24e96a828b9d780"), merkleTreeHash) + val hashedLeaves = leaves.map { leaf -> + CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( + typeName = "Policy", + value = Json.encodeToJsonElement(leaf), + ).second } + val tree = MerkleTree(hashedLeaves) - @Test - fun `merkletree with custom types`() { - val leaves = listOf( - mapOf("contractAddress" to "0x1", "selector" to "transfer"), - mapOf("contractAddress" to "0x2", "selector" to "transfer"), - mapOf("contractAddress" to "0x3", "selector" to "transfer"), - ) + val merkleTreeHash = CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( + typeName = "merkletree", + value = Json.encodeToJsonElement(leaves), + context = Context(parent = "Session", key = "root"), + ).second - val hashedLeaves = leaves.map { leaf -> - CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( - typeName = "Policy", - value = Json.encodeToJsonElement(leaf), - ).second - } - val tree = MerkleTree(hashedLeaves) + assertEquals(tree.rootHash, merkleTreeHash) + assertEquals( + Felt.fromHex("0x12354b159e3799dc0ebe86d62dde4ce7b300538d471e5a7fef23dcbac076011"), + merkleTreeHash, + ) + } - val merkleTreeHash = CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( + @Test + fun `merkletree from empty leaves`() { + assertThrows("Cannot build Merkle tree from an empty list of leaves.") { + CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( typeName = "merkletree", - value = Json.encodeToJsonElement(leaves), + value = Json.encodeToJsonElement(emptyList()), context = Context(parent = "Session", key = "root"), - ).second - - assertEquals(tree.rootHash, merkleTreeHash) - assertEquals( - Felt.fromHex("0x12354b159e3799dc0ebe86d62dde4ce7b300538d471e5a7fef23dcbac076011"), - merkleTreeHash, ) } + } - @Test - fun `merkletree from empty leaves`() { - assertThrows("Cannot build Merkle tree from an empty list of leaves.") { - CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( - typeName = "merkletree", - value = Json.encodeToJsonElement(emptyList()), - context = Context(parent = "Session", key = "root"), - ) - } - } - - @Test - fun `merkletree with invalid contains`() { - val exception = assertThrows { - MerkleTreeType( - name = "root", - type = "merkletree", - contains = "felt*", - ) - } - assertEquals("Merkletree 'contains' field cannot be an array, got [felt*] in type [root].", exception.message) + @Test + fun `merkletree with invalid contains`() { + val exception = assertThrows { + MerkleTreeType( + name = "root", + type = "merkletree", + contains = "felt*", + ) } + assertEquals("Merkletree 'contains' field cannot be an array, got [felt*] in type [root].", exception.message) + } - @Test - fun `merkletree with invalid context`() { - val leaves = listOf( - mapOf("contractAddress" to "0x1", "selector" to "transfer"), - mapOf("contractAddress" to "0x2", "selector" to "transfer"), - mapOf("contractAddress" to "0x3", "selector" to "transfer"), - ) + @Test + fun `merkletree with invalid context`() { + val leaves = listOf( + mapOf("contractAddress" to "0x1", "selector" to "transfer"), + mapOf("contractAddress" to "0x2", "selector" to "transfer"), + mapOf("contractAddress" to "0x3", "selector" to "transfer"), + ) - val invalidParentContext = Context(parent = "UndefinedParent", key = "root") - val invalidKeyContext = Context(parent = "Session", key = "undefinedKey") + val invalidParentContext = Context(parent = "UndefinedParent", key = "root") + val invalidKeyContext = Context(parent = "Session", key = "undefinedKey") - assertThrows("Parent type '${invalidParentContext.parent}' is not defined in types.") { - CasesRev0.TD_STRUCT_MERKLETREE.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidParentContext) - } - assertThrows("Key '${invalidKeyContext.key}' is not defined in type '${invalidKeyContext.parent}'.") { - CasesRev0.TD_STRUCT_MERKLETREE.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidKeyContext) - } + assertThrows("Parent type '${invalidParentContext.parent}' is not defined in types.") { + CasesRev0.TD_STRUCT_MERKLETREE.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidParentContext) + } + assertThrows("Key '${invalidKeyContext.key}' is not defined in type '${invalidKeyContext.parent}'.") { + CasesRev0.TD_STRUCT_MERKLETREE.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidKeyContext) } } From c9d1394ea6383cb619305ffa78c940d10dd5b8ea Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Thu, 14 Mar 2024 00:46:41 +0100 Subject: [PATCH 094/109] Don't trim type in `extractEnumTypes()` --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 9158cba2f..eb91ccaf4 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -465,10 +465,7 @@ data class TypedData private constructor( require(type.isEnum()) { "Type [$type] is not an enum." } return type.substring(1, type.length - 1).let { - when { - it.trim().isEmpty() -> emptyList() - else -> it.split(",").map(String::trim) - } + if (it.isEmpty()) emptyList() else it.split(",") } } From 6cb5e5e438bafc50fea849f580b0a373d1e208ea Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Thu, 14 Mar 2024 14:20:57 +0100 Subject: [PATCH 095/109] Simplify `types` logic; Make it private --- .../main/kotlin/com/swmansion/starknet/data/TypedData.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index eb91ccaf4..a26fe1e8d 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -108,13 +108,7 @@ data class TypedData private constructor( private val revision = domain.revision ?: Revision.V0 @Transient - val types: Map> = run { - val presetTypes = when (revision) { - Revision.V0 -> presetTypesV0 - Revision.V1 -> presetTypesV1 - } - customTypes + presetTypes - } + private val types: Map> = customTypes + getPresetTypes(revision) private val hashMethod by lazy { when (revision) { From 6b7844a9159a506e8a8071084f1e0b3addfd9024 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Thu, 14 Mar 2024 14:21:13 +0100 Subject: [PATCH 096/109] Mark `revision` as `@Transient` --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index a26fe1e8d..1608b95e4 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -105,6 +105,7 @@ data class TypedData private constructor( message = Json.parseToJsonElement(message).jsonObject, ) + @Transient private val revision = domain.revision ?: Revision.V0 @Transient From ee896608e5ded65fb456b223b8e82c9e6f8ab034 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 15 Mar 2024 10:55:34 +0100 Subject: [PATCH 097/109] Improve wording in `verifyTypes()` checks --- .../main/kotlin/com/swmansion/starknet/data/TypedData.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 1608b95e4..77abccd1e 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -148,10 +148,10 @@ data class TypedData private constructor( }.distinct() + domain.separatorName + primaryType customTypes.keys.forEach { - require(it.isNotEmpty()) { "Types cannot be empty." } - require(!it.isArray()) { "Types cannot end in *. [$it] was found." } - require(!it.startsWith("(") && !it.endsWith(")")) { "Types cannot be enclosed in parentheses. [$it] was found." } - require(!it.contains(",")) { "Types cannot contain commas. [$it] was found." } + require(it.isNotEmpty()) { "Type names cannot be empty." } + require(!it.isArray()) { "Type names cannot end in *. [$it] was found." } + require(!it.startsWith("(") && !it.endsWith(")")) { "Type names cannot be enclosed in parentheses. [$it] was found." } + require(!it.contains(",")) { "Type names cannot contain commas. [$it] was found." } require(it in referencedTypes) { "Dangling types are not allowed. Unreferenced type [$it] was found." } } } From a695a265a6bbb926f23b6d3c0bbae08c5c390e27 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 15 Mar 2024 12:30:46 +0100 Subject: [PATCH 098/109] Remove "raw" from `basicTypesV0` --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 77abccd1e..ebf49ddf6 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -497,7 +497,7 @@ data class TypedData private constructor( } private val basicTypesV0: Set - get() = setOf("felt", "bool", "string", "selector", "merkletree", "raw") + get() = setOf("felt", "bool", "string", "selector", "merkletree") private val basicTypesV1: Set get() = basicTypesV0 + setOf("enum", "u128", "i128", "ContractAddress", "ClassHash", "timestamp", "shortstring") From f0d8a8a9502b3ea9bf4b990d8ba5f87f89245dd8 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 15 Mar 2024 12:37:58 +0100 Subject: [PATCH 099/109] Fix tests --- lib/src/test/kotlin/starknet/data/TypedDataTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 3e90a2114..7cc8cff13 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -463,7 +463,7 @@ internal class TypedDataTest { val exception = assertThrows { makeTypedData(Revision.V1, type) } - assertEquals("Types cannot end in *. [$type] was found.", exception.message) + assertEquals("Type names cannot end in *. [$type] was found.", exception.message) } } @@ -474,7 +474,7 @@ internal class TypedDataTest { val exception = assertThrows { makeTypedData(Revision.V1, type) } - assertEquals("Types cannot be enclosed in parentheses. [$type] was found.", exception.message) + assertEquals("Type names cannot be enclosed in parentheses. [$type] was found.", exception.message) } } @@ -485,7 +485,7 @@ internal class TypedDataTest { val exception = assertThrows { makeTypedData(Revision.V1, type) } - assertEquals("Types cannot contain commas. [$type] was found.", exception.message) + assertEquals("Type names cannot contain commas. [$type] was found.", exception.message) } } From 7695f27e9768df7d33239f98aa8ecb56575c722e Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 15 Mar 2024 17:31:09 +0100 Subject: [PATCH 100/109] Don't discard type names with parenthesis unless they're enclosed in parentheses from both sidews - Only `(example_name)` is not allowed, `(example_name` and `example_name)` are now considered valid --- .../kotlin/com/swmansion/starknet/data/TypedData.kt | 2 +- lib/src/test/kotlin/starknet/data/TypedDataTest.kt | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index ebf49ddf6..cf5d4838b 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -150,7 +150,7 @@ data class TypedData private constructor( customTypes.keys.forEach { require(it.isNotEmpty()) { "Type names cannot be empty." } require(!it.isArray()) { "Type names cannot end in *. [$it] was found." } - require(!it.startsWith("(") && !it.endsWith(")")) { "Type names cannot be enclosed in parentheses. [$it] was found." } + require(!it.isEnum()) { "Type names cannot be enclosed in parentheses. [$it] was found." } require(!it.contains(",")) { "Type names cannot contain commas. [$it] was found." } require(it in referencedTypes) { "Dangling types are not allowed. Unreferenced type [$it] was found." } } diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 7cc8cff13..7389b6969 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -469,13 +469,11 @@ internal class TypedDataTest { @Test fun `type with parentheses`() { - val types = listOf("(left", "right)", "(both)") - types.forEach { type -> - val exception = assertThrows { - makeTypedData(Revision.V1, type) - } - assertEquals("Type names cannot be enclosed in parentheses. [$type] was found.", exception.message) + val type = "(mytype)" + val exception = assertThrows { + makeTypedData(Revision.V1, type) } + assertEquals("Type names cannot be enclosed in parentheses. [$type] was found.", exception.message) } @Test From 471b2d66e8308532c14ad5b5caac2f3e16a4ee52 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 15 Mar 2024 17:35:47 +0100 Subject: [PATCH 101/109] Rename `types`->`allTypes`, `customTypes`->`types` --- .../com/swmansion/starknet/data/TypedData.kt | 36 +++++++++---------- .../kotlin/starknet/data/TypedDataTest.kt | 6 ++-- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index cf5d4838b..954e98287 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -84,22 +84,18 @@ import kotlinx.serialization.json.* @Suppress("DataClassPrivateConstructor") @Serializable data class TypedData private constructor( - @SerialName("types") - val customTypes: Map>, - + val types: Map>, val primaryType: String, - val domain: Domain, - val message: JsonObject, ) { constructor( - customTypes: Map>, + types: Map>, primaryType: String, domain: String, message: String, ) : this( - customTypes = customTypes, + types = types, primaryType = primaryType, domain = Json.decodeFromString(domain), message = Json.parseToJsonElement(message).jsonObject, @@ -109,7 +105,7 @@ data class TypedData private constructor( private val revision = domain.revision ?: Revision.V0 @Transient - private val types: Map> = customTypes + getPresetTypes(revision) + private val allTypes: Map> = types + getPresetTypes(revision) private val hashMethod by lazy { when (revision) { @@ -125,12 +121,12 @@ data class TypedData private constructor( private fun hashArray(values: List) = hashMethod.hash(values) private fun verifyTypes() { - require(domain.separatorName in customTypes) { "Types must contain '${domain.separatorName}'." } + require(domain.separatorName in types) { "Types must contain '${domain.separatorName}'." } - getBasicTypes(revision).forEach { require(it !in customTypes) { "Types must not contain basic types. [$it] was found." } } - getPresetTypes(revision).keys.forEach { require(it !in customTypes) { "Types must not contain preset types. [$it] was found." } } + getBasicTypes(revision).forEach { require(it !in types) { "Types must not contain basic types. [$it] was found." } } + getPresetTypes(revision).keys.forEach { require(it !in types) { "Types must not contain preset types. [$it] was found." } } - val referencedTypes = customTypes.values.flatten().flatMap { + val referencedTypes = types.values.flatten().flatMap { when (it) { is EnumType -> { require(revision == Revision.V1) { "'enum' basic type is not supported in revision ${revision.value}." } @@ -147,7 +143,7 @@ data class TypedData private constructor( } }.distinct() + domain.separatorName + primaryType - customTypes.keys.forEach { + types.keys.forEach { require(it.isNotEmpty()) { "Type names cannot be empty." } require(!it.isArray()) { "Type names cannot end in *. [$it] was found." } require(!it.isEnum()) { "Type names cannot be enclosed in parentheses. [$it] was found." } @@ -229,7 +225,7 @@ data class TypedData private constructor( while (toVisit.isNotEmpty()) { val type = toVisit.removeFirst() - val params = types[type] ?: emptyList() + val params = allTypes[type] ?: emptyList() params.forEach { param -> val extractedTypes = when { @@ -245,7 +241,7 @@ data class TypedData private constructor( }.map { stripPointer(it) } extractedTypes.forEach { - if (it in types && it !in deps) { + if (it in allTypes && it !in deps) { deps.add(it) toVisit.add(it) } @@ -271,7 +267,7 @@ data class TypedData private constructor( Revision.V1 -> "\"$typeName\"" } - val fields = types.getOrElse(dependency) { + val fields = allTypes.getOrElse(dependency) { throw IllegalArgumentException("Dependency [$dependency] is not defined in types.") } val encodedFields = fields.joinToString(",") { @@ -339,7 +335,7 @@ data class TypedData private constructor( private inline fun resolveType(context: Context): T { val (parent, key) = context.parent to context.key - val parentType = types.getOrElse(parent) { throw IllegalArgumentException("Parent [$parent] is not defined in types.") } + val parentType = allTypes.getOrElse(parent) { throw IllegalArgumentException("Parent [$parent] is not defined in types.") } val targetType = parentType.singleOrNull { it.name == key } ?: throw IllegalArgumentException("Key [$key] is not defined in parent [$parent] or multiple definitions are present.") @@ -364,7 +360,7 @@ data class TypedData private constructor( private fun getEnumVariants(context: Context): List { val enumType = resolveType(context) - val variants = types.getOrElse(enumType.contains) { throw IllegalArgumentException("Type [${enumType.contains}] is not defined in types") } + val variants = allTypes.getOrElse(enumType.contains) { throw IllegalArgumentException("Type [${enumType.contains}] is not defined in types") } return variants } @@ -391,7 +387,7 @@ data class TypedData private constructor( value: JsonElement, context: Context? = null, ): Pair { - if (typeName in types) { + if (typeName in allTypes) { return typeName to getStructHash(typeName, value.jsonObject) } @@ -430,7 +426,7 @@ data class TypedData private constructor( private fun encodeData(typeName: String, data: JsonObject): List { val values = mutableListOf() - for (param in types.getValue(typeName)) { + for (param in allTypes.getValue(typeName)) { val encodedValue = encodeValue( typeName = param.type, value = data.getValue(param.name), diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 7389b6969..5a85ba5fa 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -491,7 +491,7 @@ internal class TypedDataTest { fun `dangling types`() { val exception = assertThrows { TypedData( - customTypes = mapOf( + types = mapOf( domainTypeV1, "dangling" to emptyList(), "mytype" to emptyList(), @@ -507,7 +507,7 @@ internal class TypedDataTest { @Test fun `missing dependency`() { val td = TypedData( - customTypes = mapOf( + types = mapOf( domainTypeV1, "house" to listOf(TypedData.StandardType("fridge", "ice cream")), ), @@ -531,7 +531,7 @@ internal class TypedDataTest { } TypedData( - customTypes = mapOf( + types = mapOf( domainType, includedType to emptyList(), ), From 2897787f731ae8b92e7e9cb009f08b47f2e43d14 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sun, 17 Mar 2024 17:31:13 +0100 Subject: [PATCH 102/109] Improve error messages; Fix `merkletree with invalid context` test --- .../main/kotlin/com/swmansion/starknet/data/TypedData.kt | 4 ++-- lib/src/test/kotlin/starknet/data/TypedDataTest.kt | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 954e98287..226dafd7a 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -337,9 +337,9 @@ data class TypedData private constructor( val parentType = allTypes.getOrElse(parent) { throw IllegalArgumentException("Parent [$parent] is not defined in types.") } val targetType = parentType.singleOrNull { it.name == key } - ?: throw IllegalArgumentException("Key [$key] is not defined in parent [$parent] or multiple definitions are present.") + ?: throw IllegalArgumentException("Key [$key] is not defined in type [$parent] or multiple definitions are present.") - require(targetType is T) { "Key [$key] in parent [$parent] is not a '${T::class.simpleName}'." } + require(targetType is T) { "Key [$key] in type [$parent] is not a '${T::class.simpleName}'." } return targetType } diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 5a85ba5fa..738121e9a 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -416,12 +416,14 @@ internal class TypedDataTest { val invalidParentContext = Context(parent = "UndefinedParent", key = "root") val invalidKeyContext = Context(parent = "Session", key = "undefinedKey") - assertThrows("Parent type '${invalidParentContext.parent}' is not defined in types.") { + val parentException = assertThrows{ CasesRev0.TD_STRUCT_MERKLETREE.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidParentContext) } - assertThrows("Key '${invalidKeyContext.key}' is not defined in type '${invalidKeyContext.parent}'.") { + assertEquals("Parent [${invalidParentContext.parent}] is not defined in types.", parentException.message) + val keyException = assertThrows{ CasesRev0.TD_STRUCT_MERKLETREE.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidKeyContext) } + assertEquals("Key [${invalidKeyContext.key}] is not defined in type [${invalidKeyContext.parent}] or multiple definitions are present.", keyException.message) } @ParameterizedTest From 04dd026bc438f23261fb9fd664f87336b1ee9e3a Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Sun, 17 Mar 2024 19:04:56 +0100 Subject: [PATCH 103/109] Format --- lib/src/test/kotlin/starknet/data/TypedDataTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index 738121e9a..ee42509e7 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -416,11 +416,11 @@ internal class TypedDataTest { val invalidParentContext = Context(parent = "UndefinedParent", key = "root") val invalidKeyContext = Context(parent = "Session", key = "undefinedKey") - val parentException = assertThrows{ + val parentException = assertThrows { CasesRev0.TD_STRUCT_MERKLETREE.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidParentContext) } assertEquals("Parent [${invalidParentContext.parent}] is not defined in types.", parentException.message) - val keyException = assertThrows{ + val keyException = assertThrows { CasesRev0.TD_STRUCT_MERKLETREE.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidKeyContext) } assertEquals("Key [${invalidKeyContext.key}] is not defined in type [${invalidKeyContext.parent}] or multiple definitions are present.", keyException.message) From 565c27200c5e6676070473aba7830e50810f7bed Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 18 Mar 2024 10:27:50 +0100 Subject: [PATCH 104/109] Minor `feltFromPrimitive()` refactor --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 226dafd7a..5a4a99249 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -299,7 +299,9 @@ data class TypedData private constructor( decimal?.let { return if (allowSigned) Felt.fromSigned(it) else Felt(it) } - primitive.booleanOrNull?.let { + + val boolean = primitive.booleanOrNull + boolean?.let { return if (it) Felt.ONE else Felt.ZERO } From 510c1f1a6cbda10edef91c083d67f9d1c8fe6c0a Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau <43750648+DelevoXDG@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:42:19 +0300 Subject: [PATCH 105/109] Use sealed classes for basic and preset types (#440) --- lib/build.gradle.kts | 3 + .../com/swmansion/starknet/data/TypedData.kt | 158 +++++++++++------- 2 files changed, 100 insertions(+), 61 deletions(-) diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index bff23ef26..eac6c2550 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -123,6 +123,9 @@ dependencies { // Use the Kotlin JDK 8 standard library. implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.20") + // Use the Kotlin reflection library. + implementation(kotlin("reflect")) + // Use the JUnit test library. testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.0") testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.0") diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 5a4a99249..71a0fcfd2 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -105,7 +105,7 @@ data class TypedData private constructor( private val revision = domain.revision ?: Revision.V0 @Transient - private val allTypes: Map> = types + getPresetTypes(revision) + private val allTypes: Map> = types + PresetType.values(revision).associate { it.name to it.params } private val hashMethod by lazy { when (revision) { @@ -123,13 +123,13 @@ data class TypedData private constructor( private fun verifyTypes() { require(domain.separatorName in types) { "Types must contain '${domain.separatorName}'." } - getBasicTypes(revision).forEach { require(it !in types) { "Types must not contain basic types. [$it] was found." } } - getPresetTypes(revision).keys.forEach { require(it !in types) { "Types must not contain preset types. [$it] was found." } } + BasicType.values(revision).forEach { require(it.name !in types) { "Types must not contain basic types. [${it.name}] was found." } } + PresetType.values(revision).forEach { require(it.name !in types) { "Types must not contain preset types. [${it.name}] was found." } } val referencedTypes = types.values.flatten().flatMap { when (it) { is EnumType -> { - require(revision == Revision.V1) { "'enum' basic type is not supported in revision ${revision.value}." } + require(revision == Revision.V1) { "'${BasicType.Enum.name}' basic type is not supported in revision ${revision.value}." } listOf(it.contains) } is MerkleTreeType -> listOf(it.contains) @@ -197,7 +197,7 @@ data class TypedData private constructor( @Serializable data class MerkleTreeType( override val name: String, - override val type: String = "merkletree", + override val type: String = BasicType.MerkleTree.name, val contains: String, ) : Type() { init { @@ -210,7 +210,7 @@ data class TypedData private constructor( @Serializable data class EnumType( override val name: String, - override val type: String = "enum", + override val type: String = BasicType.Enum.name, val contains: String, ) : Type() @@ -230,7 +230,7 @@ data class TypedData private constructor( params.forEach { param -> val extractedTypes = when { param is EnumType -> { - require(revision == Revision.V1) { "'enum' basic type is not supported in revision ${revision.value}." } + require(revision == Revision.V1) { "'${BasicType.Enum.name}' basic type is not supported in revision ${revision.value}." } listOf(param.contains) } param.type.isEnum() -> { @@ -373,7 +373,7 @@ data class TypedData private constructor( val variants = getEnumVariants(context) val variantType = variants.singleOrNull { it.name == variantName } - ?: throw IllegalArgumentException("Variant [$variantName] is not defined in 'enum' type [${context.key}] or multiple definitions are present.") + ?: throw IllegalArgumentException("Variant [$variantName] is not defined in '${BasicType.Enum.name}' type [${context.key}] or multiple definitions are present.") val variantIndex = variants.indexOf(variantType) val encodedSubtypes = extractEnumTypes(variantType.type).mapIndexed { index, subtype -> @@ -400,28 +400,23 @@ data class TypedData private constructor( return typeName to hashArray(hashes) } - return when (typeName) { - "enum" -> { - require(revision == Revision.V1) { "'enum' basic type is not supported in revision ${revision.value}." } - requireNotNull(context) { "Context is not provided for 'enum' type." } - "enum" to prepareEnum(value.jsonObject, context) - } - "merkletree" -> { - requireNotNull(context) { "Context is not provided for 'merkletree' type." } - "felt" to prepareMerkletreeRoot(value.jsonArray, context) - } - "string" -> when (revision) { - Revision.V0 -> "string" to feltFromPrimitive(value.jsonPrimitive) - Revision.V1 -> "string" to prepareLongString(value.jsonPrimitive.content) + val basicType = BasicType.fromName(typeName, revision) + ?: throw IllegalArgumentException("Type [$typeName] is not defined in types.") + + return basicType.encodeToType to when (basicType) { + BasicType.Enum -> { + requireNotNull(context) { "Context is not provided for '${basicType.name}' type." } + prepareEnum(value.jsonObject, context) } - "felt", "bool" -> typeName to feltFromPrimitive(value.jsonPrimitive) - "selector" -> "felt" to prepareSelector(value.jsonPrimitive.content) - "i128" -> "i128" to feltFromPrimitive(value.jsonPrimitive, allowSigned = true) - "u128", "ContractAddress", "ClassHash", "timestamp", "shortstring" -> { - require(revision == Revision.V1) { "'$typeName' basic type is not supported in revision ${revision.value}." } - typeName to feltFromPrimitive(value.jsonPrimitive) + BasicType.MerkleTree -> { + requireNotNull(context) { "Context is not provided for '${basicType.name}' type." } + prepareMerkletreeRoot(value.jsonArray, context) } - else -> throw IllegalArgumentException("Type [$typeName] is not defined in types.") + BasicType.StringV0 -> feltFromPrimitive(value.jsonPrimitive) + BasicType.StringV1 -> prepareLongString(value.jsonPrimitive.content) + BasicType.Selector -> prepareSelector(value.jsonPrimitive.content) + BasicType.I128 -> feltFromPrimitive(value.jsonPrimitive, allowSigned = true) + BasicType.Felt, BasicType.Bool, BasicType.ShortString, BasicType.ContractAddress, BasicType.ClassHash, BasicType.U128, BasicType.Timestamp -> feltFromPrimitive(value.jsonPrimitive) } } @@ -479,46 +474,87 @@ data class TypedData private constructor( ) } - companion object { - private fun getBasicTypes(revision: Revision): Set { - return when (revision) { - Revision.V0 -> basicTypesV0 - Revision.V1 -> basicTypesV1 + private sealed class BasicType { + abstract val name: String + open val encodeToType get() = name + + sealed interface V0 + sealed interface V1 + + data object Felt : BasicType(), V0, V1 { override val name = "felt" } + data object Bool : BasicType(), V0, V1 { override val name = "bool" } + data object Selector : BasicType(), V0, V1 { override val name = "selector"; override val encodeToType = Felt.name } + data object MerkleTree : BasicType(), V0, V1 { override val name = "merkletree"; override val encodeToType = Felt.name } + data object StringV0 : BasicType(), V0 { override val name = "string" } + data object StringV1 : BasicType(), V1 { override val name = "string" } + data object Enum : BasicType(), V1 { override val name = "enum"; override val encodeToType = Felt.name } + data object I128 : BasicType(), V1 { override val name = "i128" } + data object U128 : BasicType(), V1 { override val name = "u128" } + data object ContractAddress : BasicType(), V1 { override val name = "ContractAddress" } + data object ClassHash : BasicType(), V1 { override val name = "ClassHash" } + data object Timestamp : BasicType(), V1 { override val name = "timestamp" } + data object ShortString : BasicType(), V1 { override val name = "shortstring" } + + companion object { + fun values(revision: Revision): List { + val instances = BasicType::class.sealedSubclasses.mapNotNull { it.objectInstance } + + return when (revision) { + Revision.V0 -> instances.filterIsInstance() + Revision.V1 -> instances.filterIsInstance() + }.map { it as BasicType } } - } - private fun getPresetTypes(revision: Revision): Map> { - return when (revision) { - Revision.V0 -> presetTypesV0 - Revision.V1 -> presetTypesV1 + fun fromName(name: String, revision: Revision): BasicType? { + return values(revision).find { it.name == name } } } + } + + private sealed class PresetType { + abstract val name: String + abstract val params: List + + sealed interface V1 + + @Suppress("unused") + data object U256 : PresetType(), V1 { + override val name = "u256" + override val params = listOf( + StandardType("low", "u128"), + StandardType("high", "u128"), + ) + } + + @Suppress("unused") + data object TokenAmount : PresetType(), V1 { + override val name = "TokenAmount" + override val params = listOf( + StandardType("token_address", "ContractAddress"), + StandardType("amount", "u256"), + ) + } - private val basicTypesV0: Set - get() = setOf("felt", "bool", "string", "selector", "merkletree") - - private val basicTypesV1: Set - get() = basicTypesV0 + setOf("enum", "u128", "i128", "ContractAddress", "ClassHash", "timestamp", "shortstring") - - private val presetTypesV0: Map> - get() = emptyMap() - - private val presetTypesV1: Map> - get() = mapOf( - "u256" to listOf( - StandardType("low", "u128"), - StandardType("high", "u128"), - ), - "TokenAmount" to listOf( - StandardType("token_address", "ContractAddress"), - StandardType("amount", "u256"), - ), - "NftId" to listOf( - StandardType("collection_address", "ContractAddress"), - StandardType("token_id", "u256"), - ), + @Suppress("unused") + data object NftId : PresetType(), V1 { + override val name = "NftId" + override val params = listOf( + StandardType("collection_address", "ContractAddress"), + StandardType("token_id", "u256"), ) + } + companion object { + fun values(revision: Revision): List { + return when (revision) { + Revision.V0 -> emptyList() + Revision.V1 -> PresetType::class.sealedSubclasses.mapNotNull { it.objectInstance }.filterIsInstance() + }.map { it as PresetType } + } + } + } + + companion object { /** * Create TypedData from JSON string. * From fae15d073125ab39f9cdaa2414270264fde4c397 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau <43750648+DelevoXDG@users.noreply.github.com> Date: Mon, 18 Mar 2024 18:11:25 +0300 Subject: [PATCH 106/109] Add separate methods for primitives (#442) --- .../com/swmansion/starknet/data/TypedData.kt | 45 ++++- .../kotlin/starknet/data/TypedDataTest.kt | 168 +++++++++++++++--- 2 files changed, 186 insertions(+), 27 deletions(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 71a0fcfd2..5a92586f4 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -10,6 +10,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlinx.serialization.json.* +import java.math.BigInteger /** * Sign message for off-chain usage. Follows standard proposed [here](https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-12.md). @@ -294,7 +295,12 @@ data class TypedData private constructor( return "${escape(dependency)}($encodedFields)" } - private fun feltFromPrimitive(primitive: JsonPrimitive, allowSigned: Boolean = false): Felt { + private fun feltFromPrimitive( + primitive: JsonPrimitive, + allowSigned: Boolean = false, + allowBoolean: Boolean = false, + allowShortString: Boolean = true, + ): Felt { val decimal = primitive.content.toBigIntegerOrNull() decimal?.let { return if (allowSigned) Felt.fromSigned(it) else Felt(it) @@ -302,6 +308,7 @@ data class TypedData private constructor( val boolean = primitive.booleanOrNull boolean?.let { + require(allowBoolean) { "Unexpected boolean value: [$primitive]." } return if (it) Felt.ONE else Felt.ZERO } @@ -313,11 +320,36 @@ data class TypedData private constructor( return try { Felt.fromHex(primitive.content) } catch (e: Exception) { + require(allowShortString) { "Unexpected string value: [$primitive]." } Felt.fromShortString(primitive.content) } } - throw IllegalArgumentException("Unsupported primitive type: $primitive") + throw IllegalArgumentException("Unsupported primitive type: [$primitive].") + } + + private fun boolFromPrimitive(primitive: JsonPrimitive): Felt { + val felt = feltFromPrimitive(primitive, allowBoolean = true, allowShortString = false) + + require(felt.value < BigInteger.TWO) { "Expected boolean value, got [$primitive]." } + + return felt + } + + private fun u128fromPrimitive(primitive: JsonPrimitive): Felt { + val felt = feltFromPrimitive(primitive, allowShortString = false) + + require(felt.value < BigInteger.TWO.pow(128)) { "Value [$primitive] is out of range for '${BasicType.U128.name}'." } + + return felt + } + + private fun i128fromPrimitive(primitive: JsonPrimitive): Felt { + val felt = feltFromPrimitive(primitive, allowSigned = true, allowShortString = false) + + require(felt.value < BigInteger.TWO.pow(127) || felt.value >= Felt.PRIME - BigInteger.TWO.pow(127)) { "Value [$primitive] is out of range for '${BasicType.I128.name}'." } + + return felt } private fun prepareLongString(string: String): Felt { @@ -412,11 +444,12 @@ data class TypedData private constructor( requireNotNull(context) { "Context is not provided for '${basicType.name}' type." } prepareMerkletreeRoot(value.jsonArray, context) } - BasicType.StringV0 -> feltFromPrimitive(value.jsonPrimitive) - BasicType.StringV1 -> prepareLongString(value.jsonPrimitive.content) + BasicType.Felt, BasicType.StringV0, BasicType.ShortString, BasicType.ContractAddress, BasicType.ClassHash -> feltFromPrimitive(value.jsonPrimitive) + BasicType.Bool -> boolFromPrimitive(value.jsonPrimitive) BasicType.Selector -> prepareSelector(value.jsonPrimitive.content) - BasicType.I128 -> feltFromPrimitive(value.jsonPrimitive, allowSigned = true) - BasicType.Felt, BasicType.Bool, BasicType.ShortString, BasicType.ContractAddress, BasicType.ClassHash, BasicType.U128, BasicType.Timestamp -> feltFromPrimitive(value.jsonPrimitive) + BasicType.StringV1 -> prepareLongString(value.jsonPrimitive.content) + BasicType.U128, BasicType.Timestamp -> u128fromPrimitive(value.jsonPrimitive) + BasicType.I128 -> i128fromPrimitive(value.jsonPrimitive) } } diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index ee42509e7..ec2fadbb6 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -9,11 +9,10 @@ import com.swmansion.starknet.data.selectorFromName import com.swmansion.starknet.data.types.Felt import com.swmansion.starknet.data.types.MerkleTree import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.encodeToJsonElement -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.* import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest @@ -21,6 +20,7 @@ import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.EnumSource import org.junit.jupiter.params.provider.MethodSource import java.io.File +import java.math.BigInteger internal fun loadTypedData(path: String): TypedData { val content = File("src/test/resources/typed_data/$path").readText() @@ -310,25 +310,151 @@ internal class TypedDataTest { assertEquals(expectedResult, encodedType) } - @Test - fun `selector type`() { - val selector = "transfer" - val selectorHash = selectorFromName(selector) + @Nested + inner class EncodeBasicTypeTests { + @Test + fun `encode selector`() { + val selector = "transfer" + val selectorHash = selectorFromName(selector) - val rawSelectorValueHash = CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( - typeName = "felt", - value = Json.encodeToJsonElement(selectorHash), - ) - val selectorValueHash = CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( - typeName = "selector", - value = Json.encodeToJsonElement(selector), - ) + val rawSelectorValueHash = CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( + typeName = "felt", + value = Json.encodeToJsonElement(selectorHash), + ) + val selectorValueHash = CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( + typeName = "selector", + value = Json.encodeToJsonElement(selector), + ) - assertEquals(rawSelectorValueHash, selectorValueHash) - assertEquals( - "felt" to Felt.fromHex("0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e"), - selectorValueHash, - ) + assertEquals(rawSelectorValueHash, selectorValueHash) + assertEquals( + "felt" to Felt.fromHex("0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e"), + selectorValueHash, + ) + } + + @Test + fun `encode bool`() { + val values = listOf(true, false, "true", "false", "0x1", "0x0", "1", "0", 1, 0) + values.forEach { + val encodedValue = CasesRev1.TD_BASIC_TYPES.encodeValue("bool", encodeToJsonElement(it)) + assertTrue(encodedValue.second in listOf(Felt.ONE, Felt.ZERO)) + } + } + + @Test + fun `encode bool - invalid values`() { + val invalidValues = listOf("0x2", "2", 2, 1000) + invalidValues.forEach { + val value = encodeToJsonElement(it) + val exception = assertThrows { + CasesRev1.TD_BASIC_TYPES.encodeValue("bool", value) + } + assertEquals("Expected boolean value, got [$value].", exception.message) + } + } + + @Test + fun `encode u128`() { + val values = listOf(0, 1, 1000000, "0x0", "0x1", "0x64", (BigInteger.TWO.pow(128) - BigInteger.ONE).toString()) + + values.forEach { + val encodedValue = CasesRev1.TD_BASIC_TYPES.encodeValue("u128", encodeToJsonElement(it)) + assertEquals(feltFromAny(it), encodedValue.second) + } + } + + @Test + fun `encode u128 - underflow`() { + val values = listOf(-1, "-1") + + values.forEach { + val value = encodeToJsonElement(it) + val exception = assertThrows { + CasesRev1.TD_BASIC_TYPES.encodeValue("u128", value) + } + assertEquals("Default Felt constructor does not accept negative numbers, [-1] given.", exception.message) + } + } + + @Test + fun `encode u128 - overflow`() { + val values = listOf( + "0x" + (BigInteger.TWO.pow(128)).toString(16), + (BigInteger.TWO.pow(128) + BigInteger.ONE).toString(), + ) + + values.forEach { + val value = Json.encodeToJsonElement(it) + val exception = assertThrows { + CasesRev1.TD_BASIC_TYPES.encodeValue("u128", value) + } + assertEquals("Value [$value] is out of range for 'u128'.", exception.message) + } + } + + @Test + fun `encode i128`() { + val positiveValues = listOf(0, 1, 1000000, "0x0", "0x1", "0x64", (BigInteger.TWO.pow(127) - BigInteger.ONE).toString()) + val negativeValues = listOf(-1, -1000000, "-1", BigInteger.TWO.pow(127).negate().toString()) + + (positiveValues + negativeValues).forEach { + val encodedValue = CasesRev1.TD_BASIC_TYPES.encodeValue("i128", encodeToJsonElement(it)) + assertEquals(feltFromAny(it), encodedValue.second) + } + } + + @Test + fun `encode i128 - out of range`() { + val values = listOf( + (-BigInteger.TWO.pow(127) - BigInteger.ONE).toString(), + (BigInteger.TWO.pow(127)).toString(), + ) + + values.forEach { + val value = Json.encodeToJsonElement(it) + val exception = assertThrows { + CasesRev1.TD_BASIC_TYPES.encodeValue("i128", value) + } + assertEquals("Value [$value] is out of range for 'i128'.", exception.message) + } + } + + @Test + fun `unexpected values`() { + val td = CasesRev1.TD_BASIC_TYPES + + val exception = assertThrows { + td.encodeValue("u128", Json.encodeToJsonElement("hello")) + } + assertEquals("Unexpected string value: [\"hello\"].", exception.message) + val exception2 = assertThrows { + td.encodeValue("u128", Json.encodeToJsonElement(true)) + } + assertEquals("Unexpected boolean value: [true].", exception2.message) + val exception3 = assertThrows { + td.encodeValue("u128", Json.encodeToJsonElement(21.37)) + } + assertEquals("Unsupported primitive type: [21.37].", exception3.message) + } + + private fun encodeToJsonElement(value: Any): JsonElement { + return when (value) { + is String -> Json.encodeToJsonElement(value) + is Int -> Json.encodeToJsonElement(value) + is Boolean -> Json.encodeToJsonElement(value) + else -> throw IllegalArgumentException("Invalid value type.") + } + } + + private fun feltFromAny(value: Any): Felt { + return when (value) { + is Int -> Felt.fromSigned(value) + is String -> if (value.startsWith("0x")) Felt.fromHex(value) else Felt.fromSigned(value.toBigInteger()) + is Boolean -> if (value) Felt.ONE else Felt.ZERO + else -> throw IllegalArgumentException("Invalid value type.") + } + } } @Test From 605700abcb730bff7c1e3dfe637dac3dd68c5213 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau <43750648+DelevoXDG@users.noreply.github.com> Date: Mon, 18 Mar 2024 18:33:43 +0300 Subject: [PATCH 107/109] Nest TypedData-related test cases (#434) --- .../starknet/account/StandardAccountTest.kt | 115 ++--- .../kotlin/starknet/data/TypedDataTest.kt | 394 +++++++++--------- 2 files changed, 259 insertions(+), 250 deletions(-) diff --git a/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt b/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt index 3839c1478..67e1f3e20 100644 --- a/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt +++ b/lib/src/test/kotlin/starknet/account/StandardAccountTest.kt @@ -463,70 +463,73 @@ class StandardAccountTest { } } - private val tdRev0 by lazy { loadTypedData("rev_0/typed_data_struct_array_example.json") } - private val tdRev1 by lazy { loadTypedData("rev_1/typed_data_basic_types_example.json") } + @Nested + inner class SignTypedDataTest { + private val tdRev0 by lazy { loadTypedData("rev_0/typed_data_struct_array_example.json") } + private val tdRev1 by lazy { loadTypedData("rev_1/typed_data_basic_types_example.json") } - @Test - fun `sign TypedData revision 0`() { - val typedData = tdRev0 - - // Sign typedData - val signature = account.signTypedData(typedData) - assertTrue(signature.isNotEmpty()) - - // Verify the signature - val request = account.verifyTypedDataSignature(typedData, signature) - val isValid = request.send() - assertTrue(isValid) - - // Verify invalid signature does not pass - val request2 = account.verifyTypedDataSignature(typedData, listOf(Felt.ONE, Felt.ONE)) - val isValid2 = request2.send() - assertFalse(isValid2) - } + @Test + fun `sign TypedData revision 0`() { + val typedData = tdRev0 + + // Sign typedData + val signature = account.signTypedData(typedData) + assertTrue(signature.isNotEmpty()) + + // Verify the signature + val request = account.verifyTypedDataSignature(typedData, signature) + val isValid = request.send() + assertTrue(isValid) + + // Verify invalid signature does not pass + val request2 = account.verifyTypedDataSignature(typedData, listOf(Felt.ONE, Felt.ONE)) + val isValid2 = request2.send() + assertFalse(isValid2) + } - @Test - fun `sign TypedData revision 1`() { - val typedData = tdRev1 - - // Sign typedData - val signature = account.signTypedData(typedData) - assertTrue(signature.isNotEmpty()) - - // Verify the signature - val request = account.verifyTypedDataSignature(typedData, signature) - val isValid = request.send() - assertTrue(isValid) - - // Verify invalid signature does not pass - val request2 = account.verifyTypedDataSignature(typedData, listOf(Felt.ONE, Felt.ONE)) - val isValid2 = request2.send() - assertFalse(isValid2) - } + @Test + fun `sign TypedData revision 1`() { + val typedData = tdRev1 + + // Sign typedData + val signature = account.signTypedData(typedData) + assertTrue(signature.isNotEmpty()) + + // Verify the signature + val request = account.verifyTypedDataSignature(typedData, signature) + val isValid = request.send() + assertTrue(isValid) + + // Verify invalid signature does not pass + val request2 = account.verifyTypedDataSignature(typedData, listOf(Felt.ONE, Felt.ONE)) + val isValid2 = request2.send() + assertFalse(isValid2) + } - @Test - fun `sign TypedData rethrows exceptions other than signature related`() { - val httpService = mock { - on { send(any()) } doReturn HttpResponse( - false, - 500, - """ + @Test + fun `sign TypedData rethrows exceptions other than signature related`() { + val httpService = mock { + on { send(any()) } doReturn HttpResponse( + false, + 500, + """ { "something": "broke" } - """.trimIndent(), - ) - } - val provider = JsonRpcProvider(devnetClient.rpcUrl, httpService) - val account = StandardAccount(Felt.ONE, Felt.ONE, provider, chainId) + """.trimIndent(), + ) + } + val provider = JsonRpcProvider(devnetClient.rpcUrl, httpService) + val account = StandardAccount(Felt.ONE, Felt.ONE, provider, chainId) - val typedData = tdRev0 - val signature = account.signTypedData(typedData) - assertTrue(signature.isNotEmpty()) + val typedData = tdRev0 + val signature = account.signTypedData(typedData) + assertTrue(signature.isNotEmpty()) - val request = account.verifyTypedDataSignature(typedData, signature) - assertThrows(RequestFailedException::class.java) { - request.send() + val request = account.verifyTypedDataSignature(typedData, signature) + assertThrows(RequestFailedException::class.java) { + request.send() + } } } diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index ec2fadbb6..ba33699e6 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -53,38 +53,6 @@ internal class TypedDataTest { } } - private val domainTypeV0 = "StarkNetDomain" to listOf( - TypedData.StandardType("name", "felt"), - TypedData.StandardType("version", "felt"), - TypedData.StandardType("chainId", "felt"), - ) - private val domainTypeV1 = "StarknetDomain" to listOf( - TypedData.StandardType("name", "shortstring"), - TypedData.StandardType("version", "shortstring"), - TypedData.StandardType("chainId", "shortstring"), - TypedData.StandardType("revision", "shortstring"), - ) - private val domainObjectV0 = """ - { - "name": "DomainV0", - "version": 1, - "chainId": 2137 - } - """.trimIndent() - private val domainObjectV1 = """ - { - "name": "DomainV1", - "version": "1", - "chainId": "2137", - "revision": "1" - } - """.trimIndent() - - private val basicTypesV0 = setOf("felt", "bool", "string", "selector", "merkletree") - private val basicTypesV1 = basicTypesV0 + setOf("enum", "u128", "i128", "ContractAddress", "ClassHash", "timestamp", "shortstring") - private val presetTypesV0 = emptySet() - private val presetTypesV1 = setOf("u256", "TokenAmount", "NftId") - @JvmStatic fun encodeTypeArguments() = listOf( Arguments.of(CasesRev0.TD, "Mail", "Mail(from:Person,to:Person,contents:felt)Person(name:felt,wallet:felt)"), @@ -311,7 +279,7 @@ internal class TypedDataTest { } @Nested - inner class EncodeBasicTypeTests { + inner class EncodeBasicTypeTest { @Test fun `encode selector`() { val selector = "transfer" @@ -457,216 +425,254 @@ internal class TypedDataTest { } } - @Test - fun `merkletree with felt leaves`() { - val td = CasesRev1.TD_FELT_MERKLETREE + @Nested + inner class MerkletreeTest { + @Test + fun `merkletree with felt leaves`() { + val td = CasesRev1.TD_FELT_MERKLETREE - val leaves = td.message.getValue("root").jsonArray.map { Felt.fromHex(it.jsonPrimitive.content) } - assertEquals((1..3).map { Felt(it) }, leaves) + val leaves = td.message.getValue("root").jsonArray.map { Felt.fromHex(it.jsonPrimitive.content) } + assertEquals((1..3).map { Felt(it) }, leaves) - val tree = MerkleTree( - leafHashes = leaves, - hashFunction = HashMethod.POSEIDON, - ) + val tree = MerkleTree( + leafHashes = leaves, + hashFunction = HashMethod.POSEIDON, + ) - val merkleTreeHash = td.encodeValue( - typeName = "merkletree", - value = Json.encodeToJsonElement(tree.leafHashes), - context = Context(parent = "Example", key = "root"), - ).second + val merkleTreeHash = td.encodeValue( + typeName = "merkletree", + value = Json.encodeToJsonElement(tree.leafHashes), + context = Context(parent = "Example", key = "root"), + ).second - assertEquals(tree.rootHash, merkleTreeHash) - assertEquals(Felt.fromHex("0x48924a3b2a7a7b7cc1c9371357e95e322899880a6534bdfe24e96a828b9d780"), merkleTreeHash) - } + assertEquals(tree.rootHash, merkleTreeHash) + assertEquals(Felt.fromHex("0x48924a3b2a7a7b7cc1c9371357e95e322899880a6534bdfe24e96a828b9d780"), merkleTreeHash) + } - @Test - fun `merkletree with custom types`() { - val leaves = listOf( - mapOf("contractAddress" to "0x1", "selector" to "transfer"), - mapOf("contractAddress" to "0x2", "selector" to "transfer"), - mapOf("contractAddress" to "0x3", "selector" to "transfer"), - ) + @Test + fun `merkletree with custom types`() { + val leaves = listOf( + mapOf("contractAddress" to "0x1", "selector" to "transfer"), + mapOf("contractAddress" to "0x2", "selector" to "transfer"), + mapOf("contractAddress" to "0x3", "selector" to "transfer"), + ) - val hashedLeaves = leaves.map { leaf -> - CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( - typeName = "Policy", - value = Json.encodeToJsonElement(leaf), - ).second - } - val tree = MerkleTree(hashedLeaves, HashMethod.PEDERSEN) - - val merkleTreeHash = CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( - typeName = "merkletree", - value = Json.encodeToJsonElement(leaves), - context = Context(parent = "Session", key = "root"), - ).second - - assertEquals(tree.rootHash, merkleTreeHash) - assertEquals( - Felt.fromHex("0x12354b159e3799dc0ebe86d62dde4ce7b300538d471e5a7fef23dcbac076011"), - merkleTreeHash, - ) - } + val hashedLeaves = leaves.map { leaf -> + CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( + typeName = "Policy", + value = Json.encodeToJsonElement(leaf), + ).second + } + val tree = MerkleTree(hashedLeaves, HashMethod.PEDERSEN) - @Test - fun `merkletree from empty leaves`() { - assertThrows("Cannot build Merkle tree from an empty list of leaves.") { - CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( + val merkleTreeHash = CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( typeName = "merkletree", - value = Json.encodeToJsonElement(emptyList()), + value = Json.encodeToJsonElement(leaves), context = Context(parent = "Session", key = "root"), + ).second + + assertEquals(tree.rootHash, merkleTreeHash) + assertEquals( + Felt.fromHex("0x12354b159e3799dc0ebe86d62dde4ce7b300538d471e5a7fef23dcbac076011"), + merkleTreeHash, ) } - } - @Test - fun `merkletree with invalid contains`() { - val exception = assertThrows { - MerkleTreeType( - name = "root", - type = "merkletree", - contains = "felt*", + @Test + fun `merkletree from empty leaves`() { + assertThrows("Cannot build Merkle tree from an empty list of leaves.") { + CasesRev0.TD_STRUCT_MERKLETREE.encodeValue( + typeName = "merkletree", + value = Json.encodeToJsonElement(emptyList()), + context = Context(parent = "Session", key = "root"), + ) + } + } + + @Test + fun `merkletree with invalid contains`() { + val exception = assertThrows { + MerkleTreeType( + name = "root", + type = "merkletree", + contains = "felt*", + ) + } + assertEquals("Merkletree 'contains' field cannot be an array, got [felt*] in type [root].", exception.message) + } + + @Test + fun `merkletree with invalid context`() { + val leaves = listOf( + mapOf("contractAddress" to "0x1", "selector" to "transfer"), + mapOf("contractAddress" to "0x2", "selector" to "transfer"), + mapOf("contractAddress" to "0x3", "selector" to "transfer"), ) + + val invalidParentContext = Context(parent = "UndefinedParent", key = "root") + val invalidKeyContext = Context(parent = "Session", key = "undefinedKey") + + val parentException = assertThrows { + CasesRev0.TD_STRUCT_MERKLETREE.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidParentContext) + } + assertEquals("Parent [${invalidParentContext.parent}] is not defined in types.", parentException.message) + val keyException = assertThrows { + CasesRev0.TD_STRUCT_MERKLETREE.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidKeyContext) + } + assertEquals("Key [${invalidKeyContext.key}] is not defined in type [${invalidKeyContext.parent}] or multiple definitions are present.", keyException.message) } - assertEquals("Merkletree 'contains' field cannot be an array, got [felt*] in type [root].", exception.message) } - @Test - fun `merkletree with invalid context`() { - val leaves = listOf( - mapOf("contractAddress" to "0x1", "selector" to "transfer"), - mapOf("contractAddress" to "0x2", "selector" to "transfer"), - mapOf("contractAddress" to "0x3", "selector" to "transfer"), + @Nested + inner class InvalidTypesTest { + private val domainTypeV0 = "StarkNetDomain" to listOf( + TypedData.StandardType("name", "felt"), + TypedData.StandardType("version", "felt"), + TypedData.StandardType("chainId", "felt"), ) + private val domainTypeV1 = "StarknetDomain" to listOf( + TypedData.StandardType("name", "shortstring"), + TypedData.StandardType("version", "shortstring"), + TypedData.StandardType("chainId", "shortstring"), + TypedData.StandardType("revision", "shortstring"), + ) + private val domainObjectV0 = """ + { + "name": "DomainV0", + "version": 1, + "chainId": 2137 + } + """.trimIndent() + private val domainObjectV1 = """ + { + "name": "DomainV1", + "version": "1", + "chainId": "2137", + "revision": "1" + } + """.trimIndent() - val invalidParentContext = Context(parent = "UndefinedParent", key = "root") - val invalidKeyContext = Context(parent = "Session", key = "undefinedKey") + private val basicTypesV0 = setOf("felt", "bool", "string", "selector", "merkletree") + private val basicTypesV1 = basicTypesV0 + setOf("enum", "u128", "i128", "ContractAddress", "ClassHash", "timestamp", "shortstring") + private val presetTypesV0 = emptySet() + private val presetTypesV1 = setOf("u256", "TokenAmount", "NftId") - val parentException = assertThrows { - CasesRev0.TD_STRUCT_MERKLETREE.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidParentContext) - } - assertEquals("Parent [${invalidParentContext.parent}] is not defined in types.", parentException.message) - val keyException = assertThrows { - CasesRev0.TD_STRUCT_MERKLETREE.encodeValue("merkletree", Json.encodeToJsonElement(leaves), invalidKeyContext) - } - assertEquals("Key [${invalidKeyContext.key}] is not defined in type [${invalidKeyContext.parent}] or multiple definitions are present.", keyException.message) - } + @ParameterizedTest + @EnumSource(Revision::class) + fun `basic types redefinition`(revision: Revision) { + val types = when (revision) { + Revision.V0 -> basicTypesV0 + Revision.V1 -> basicTypesV1 + } - @ParameterizedTest - @EnumSource(Revision::class) - fun `basic types redefinition`(revision: Revision) { - val types = when (revision) { - Revision.V0 -> basicTypesV0 - Revision.V1 -> basicTypesV1 + types.forEach { type -> + val exception = assertThrows { + makeTypedData(revision, type) + } + assertEquals("Types must not contain basic types. [$type] was found.", exception.message) + } } - types.forEach { type -> - val exception = assertThrows { - makeTypedData(revision, type) + @ParameterizedTest + @EnumSource(Revision::class) + fun `preset types redefinition`(revision: Revision) { + val types = when (revision) { + Revision.V0 -> presetTypesV0 + Revision.V1 -> presetTypesV1 } - assertEquals("Types must not contain basic types. [$type] was found.", exception.message) - } - } - @ParameterizedTest - @EnumSource(Revision::class) - fun `preset types redefinition`(revision: Revision) { - val types = when (revision) { - Revision.V0 -> presetTypesV0 - Revision.V1 -> presetTypesV1 + types.forEach { type -> + val exception = assertThrows { + makeTypedData(revision, type) + } + assertEquals("Types must not contain preset types. [$type] was found.", exception.message) + } } - types.forEach { type -> - val exception = assertThrows { - makeTypedData(revision, type) + @Test + fun `type with asterisk`() { + val types = listOf("felt*", "u256*", "mytype*") + types.forEach { type -> + val exception = assertThrows { + makeTypedData(Revision.V1, type) + } + assertEquals("Type names cannot end in *. [$type] was found.", exception.message) } - assertEquals("Types must not contain preset types. [$type] was found.", exception.message) } - } - @Test - fun `type with asterisk`() { - val types = listOf("felt*", "u256*", "mytype*") - types.forEach { type -> + @Test + fun `type with parentheses`() { + val type = "(mytype)" val exception = assertThrows { makeTypedData(Revision.V1, type) } - assertEquals("Type names cannot end in *. [$type] was found.", exception.message) + assertEquals("Type names cannot be enclosed in parentheses. [$type] was found.", exception.message) } - } - @Test - fun `type with parentheses`() { - val type = "(mytype)" - val exception = assertThrows { - makeTypedData(Revision.V1, type) + @Test + fun `type with commas`() { + val types = listOf(",mytype", "my,type", "mytype,") + types.forEach { type -> + val exception = assertThrows { + makeTypedData(Revision.V1, type) + } + assertEquals("Type names cannot contain commas. [$type] was found.", exception.message) + } } - assertEquals("Type names cannot be enclosed in parentheses. [$type] was found.", exception.message) - } - @Test - fun `type with commas`() { - val types = listOf(",mytype", "my,type", "mytype,") - types.forEach { type -> + @Test + fun `dangling types`() { val exception = assertThrows { - makeTypedData(Revision.V1, type) + TypedData( + types = mapOf( + domainTypeV1, + "dangling" to emptyList(), + "mytype" to emptyList(), + ), + primaryType = "mytype", + domain = domainObjectV1, + message = "{\"mytype\": 1}", + ) } - assertEquals("Type names cannot contain commas. [$type] was found.", exception.message) + assertEquals("Dangling types are not allowed. Unreferenced type [dangling] was found.", exception.message) } - } - @Test - fun `dangling types`() { - val exception = assertThrows { - TypedData( + @Test + fun `missing dependency`() { + val td = TypedData( types = mapOf( domainTypeV1, - "dangling" to emptyList(), - "mytype" to emptyList(), + "house" to listOf(TypedData.StandardType("fridge", "ice cream")), ), - primaryType = "mytype", + primaryType = "house", domain = domainObjectV1, - message = "{\"mytype\": 1}", + message = "{\"fridge\": 1}", ) + val exception = assertThrows { + td.getStructHash("house", "{\"fridge\": 1}") + } + assertEquals("Type [ice cream] is not defined in types.", exception.message) } - assertEquals("Dangling types are not allowed. Unreferenced type [dangling] was found.", exception.message) - } - @Test - fun `missing dependency`() { - val td = TypedData( - types = mapOf( - domainTypeV1, - "house" to listOf(TypedData.StandardType("fridge", "ice cream")), - ), - primaryType = "house", - domain = domainObjectV1, - message = "{\"fridge\": 1}", - ) - val exception = assertThrows { - td.getStructHash("house", "{\"fridge\": 1}") - } - assertEquals("Type [ice cream] is not defined in types.", exception.message) - } + private fun makeTypedData( + revision: Revision, + includedType: String, + ) { + val (domainType, domainObject) = when (revision) { + Revision.V0 -> domainTypeV0 to domainObjectV0 + Revision.V1 -> domainTypeV1 to domainObjectV1 + } - private fun makeTypedData( - revision: Revision, - includedType: String, - ) { - val (domainType, domainObject) = when (revision) { - Revision.V0 -> domainTypeV0 to domainObjectV0 - Revision.V1 -> domainTypeV1 to domainObjectV1 + TypedData( + types = mapOf( + domainType, + includedType to emptyList(), + ), + primaryType = includedType, + domain = domainObject, + message = "{\"$includedType\": 1}", + ) } - - TypedData( - types = mapOf( - domainType, - includedType to emptyList(), - ), - primaryType = includedType, - domain = domainObject, - message = "{\"$includedType\": 1}", - ) } @ParameterizedTest From a7453dd0f311642f513a03e4859572c2eeb96c94 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 18 Mar 2024 16:47:25 +0100 Subject: [PATCH 108/109] Cover extra `verifyTypes()` errors --- .../kotlin/starknet/data/TypedDataTest.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt index ba33699e6..023ac0ec7 100644 --- a/lib/src/test/kotlin/starknet/data/TypedDataTest.kt +++ b/lib/src/test/kotlin/starknet/data/TypedDataTest.kt @@ -557,6 +557,28 @@ internal class TypedDataTest { private val presetTypesV0 = emptySet() private val presetTypesV1 = setOf("u256", "TokenAmount", "NftId") + @ParameterizedTest + @EnumSource(Revision::class) + fun `types should contain domain type`(revision: Revision) { + val domain = when (revision) { + Revision.V0 -> domainObjectV0 + Revision.V1 -> domainObjectV1 + } + val exception = assertThrows { + TypedData( + types = emptyMap(), + primaryType = "Mail", + domain = domain, + message = "{}", + ) + } + val domainTypeName = when (revision) { + Revision.V0 -> "StarkNetDomain" + Revision.V1 -> "StarknetDomain" + } + assertEquals("Types must contain '$domainTypeName'.", exception.message) + } + @ParameterizedTest @EnumSource(Revision::class) fun `basic types redefinition`(revision: Revision) { @@ -589,6 +611,14 @@ internal class TypedDataTest { } } + @Test + fun `empty type name`() { + val exception = assertThrows { + makeTypedData(Revision.V1, "") + } + assertEquals("Type names cannot be empty.", exception.message) + } + @Test fun `type with asterisk`() { val types = listOf("felt*", "u256*", "mytype*") From 55b936124ebb522dea73c877a00a299c4d7d9a6d Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 18 Mar 2024 16:47:50 +0100 Subject: [PATCH 109/109] Use `BasicType` instead of raw string --- lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt index 5a92586f4..242364073 100644 --- a/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt +++ b/lib/src/main/kotlin/com/swmansion/starknet/data/TypedData.kt @@ -401,7 +401,7 @@ data class TypedData private constructor( private fun prepareEnum(value: JsonObject, context: Context): Felt { val (variantName, variantData) = value.entries.singleOrNull()?.let { it.key to it.value.jsonArray } - ?: throw IllegalArgumentException("'enum' value must contain a single variant.") + ?: throw IllegalArgumentException("'${BasicType.Enum.name}' value must contain a single variant.") val variants = getEnumVariants(context) val variantType = variants.singleOrNull { it.name == variantName }