Skip to content

Commit

Permalink
Merge pull request #76 from icerockdev/develop
Browse files Browse the repository at this point in the history
Release 0.13.0
  • Loading branch information
anton6tak authored Oct 25, 2021
2 parents 5ab0ed5 + d2dfefb commit 9e3550c
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 125 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ allprojects {
project build.gradle
```groovy
dependencies {
commonMainApi("dev.icerock.moko:web3:0.12.0")
commonMainApi("dev.icerock.moko:web3:0.13.0")
}
```

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ buildscript {

dependencies {
classpath(":web3-build-logic")
classpath("org.jetbrains.kotlin:kotlin-serialization:1.5.20")
classpath("org.jetbrains.kotlin:kotlin-serialization:1.5.31")
}
}

Expand Down
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
kotlinVersion = "1.5.20"
kotlinVersion = "1.5.31"
androidAppCompatVersion = "1.3.1"
materialDesignVersion = "1.4.0"
lifecycleVersion = "2.3.1"
Expand All @@ -9,7 +9,7 @@ kbignumVersion = "2.2.0"
klockVersion = "2.2.2"
ktorClientVersion = "1.6.1"
mokoTestVersion = "0.4.0"
mokoWeb3Version = "0.12.0"
mokoWeb3Version = "0.13.0"
multidexVersion = "2.0.1"

[libraries]
Expand Down
2 changes: 1 addition & 1 deletion web3-build-logic/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ repositories {

dependencies {
api("dev.icerock:mobile-multiplatform:0.12.0")
api("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21")
api("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31")
api("com.android.tools.build:gradle:4.2.1")
api("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.15.0")
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

package dev.icerock.moko.web3.contract

import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonArray

class AbiMethodNotFoundException(
val method: String,
val methodsAbi: Map<String, JsonObject>
val methodsAbi: JsonArray
) : Throwable("method $method not found in ABI $methodsAbi")
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@ package dev.icerock.moko.web3.contract

import com.soywiz.kbignum.bi

class ListParam<T>(private val subtypeEncoder: StaticEncoder<T>) : DynamicEncoder<List<T>> {
class ListParam<T>(private val subtypeEncoder: Encoder<T>) : DynamicEncoder<List<T>> {
override fun encode(item: List<T>): ByteArray {
val sizeEncoded = UInt256Param.encode(item.size.bi)
return item
.map(subtypeEncoder::encode)
.fold(sizeEncoded) { acc, part -> acc + part }
}
override fun decode(source: ByteArray): List<T> {
val chunkedSource = source.toList().chunked(size = SmartContract.PART_SIZE)
// We drop first element since the first element is always list size
return chunkedSource.drop(n = 1)
.map { subtypeEncoder.decode(it.toByteArray()) }
// val chunkedSource = source.toList().chunked(size = ParamsEncoder.PART_SIZE)
// // We drop first element since the first element is always list size
// return chunkedSource.drop(n = 1)
// .map { subtypeEncoder.decode(it.toByteArray()) }
TODO("moko-web3 does not support decoding dynamic params yet")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.icerock.moko.web3.contract

import com.soywiz.kbignum.bi
import dev.icerock.moko.web3.crypto.KeccakParameter
import dev.icerock.moko.web3.crypto.digestKeccak
import dev.icerock.moko.web3.crypto.toHex
import io.ktor.utils.io.core.toByteArray
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive

object MethodEncoder {
@Suppress("UNCHECKED_CAST")
private fun <T> Encoder<T>.encodeUnchecked(value: Any) = encode(value as T)

fun createCallData(abi: JsonArray, method: String, params: List<Any>): String {
val methodAbi: JsonObject = abi.map { it.jsonObject }
.firstOrNull { it["name"]?.jsonPrimitive?.contentOrNull == method }
?: throw AbiMethodNotFoundException(method, abi)

val inputParams: List<JsonObject> =
methodAbi.getValue(key = "inputs").jsonArray.map { it.jsonObject }

val methodSignature: ByteArray = generateMethodSignature(method, inputParams)

val data = methodSignature + encodeParams(inputParams, params)

return "0x" + data.toHex().lowercase()
}

fun encodeParams(inputParams: List<JsonObject>, params: List<Any>): ByteArray {
val paramsEncoders = params.indices
.map { index ->
val param = inputParams[index]
val paramName = param.getValue(key = "type").jsonPrimitive.content
return@map param to paramName
}.map { (param, typeAnnotation) -> resolveEncoderForType(param, typeAnnotation) }

val headPartiallyEncoded = paramsEncoders
.zip(params)
.map { (encoder, param) ->
when(encoder) {
is StaticEncoder<*> -> EncodedPart.StaticPart(encoder.encodeUnchecked(param))
is DynamicEncoder<*> -> EncodedPart.DynamicPart(encoder.encodeUnchecked(param))
}
}

val dynamicPartsSizes = headPartiallyEncoded
.runningFold(initial = headPartiallyEncoded.size * PART_SIZE) { acc: Int, encodedPart: EncodedPart ->
when(encodedPart) {
is EncodedPart.DynamicPart -> acc + encodedPart.encoded.size
is EncodedPart.StaticPart -> acc
}
}

val headEncoded = headPartiallyEncoded
.mapIndexed { index, encodedPart ->
if(encodedPart is EncodedPart.StaticPart)
encodedPart.encoded
else
UInt256Param.encode(dynamicPartsSizes[index].bi)
}.fold(byteArrayOf()) { acc, part -> acc + part }

val dynamicPartEncoded = headPartiallyEncoded
.filterIsInstance<EncodedPart.DynamicPart>()
.fold(byteArrayOf()) { acc, part -> acc + part.encoded }

return headEncoded + dynamicPartEncoded
}

private val listTypeRegex = Regex("(.*)\\[]")
private fun resolveEncoderForType(param: JsonObject, typeAnnotation: String) = when {
typeAnnotation.matches(listTypeRegex) -> {
val (subtypeAnnotation) = listTypeRegex.find(typeAnnotation)!!.destructured
ListParam(StaticEncoders.forType(subtypeAnnotation).encoder)
}
typeAnnotation == "tuple" -> TupleParam(param)
else -> StaticEncoders.forType(typeAnnotation).encoder
}

@OptIn(ExperimentalStdlibApi::class)
private fun generateMethodSignature(
method: String,
inputParams: List<JsonObject>
): ByteArray {
val signature = "$method(${generateParamsString(inputParams)})".toByteArray()
return signature.digestKeccak(KeccakParameter.KECCAK_256).sliceArray(0..3)
}

private fun generateParamsString(inputParams: List<JsonObject>): String = buildString {
inputParams.forEachIndexed { index, param ->
if (index != 0) append(",")
append(stringifyType(param, param.getValue(key = "type").jsonPrimitive.content))
}
}

private fun stringifyType(param: JsonObject, typeAnnotation: String) = when (typeAnnotation) {
"tuple" -> {
val components = param.getValue(key = "components").jsonArray.map(JsonElement::jsonObject)
"(${generateParamsString(components)})"
}
else -> typeAnnotation
}

internal const val PART_SIZE = 32

/**
* First iteration while encoding is an iteration when all static params encoded,
* while dynamic params replaced with DynamicPass, later it is being replaced with encoded
* dynamic params
*/
private sealed interface EncodedPart {
val encoded: ByteArray
class DynamicPart(override val encoded: ByteArray) : EncodedPart
class StaticPart(override val encoded: ByteArray) : EncodedPart
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,28 @@
package dev.icerock.moko.web3.contract

import com.soywiz.kbignum.BigInt
import com.soywiz.kbignum.bi
import dev.icerock.moko.web3.ContractAddress
import dev.icerock.moko.web3.TransactionHash
import dev.icerock.moko.web3.WalletAddress
import dev.icerock.moko.web3.Web3Executor
import dev.icerock.moko.web3.Web3RpcRequest
import dev.icerock.moko.web3.annotation.Web3Stub
import dev.icerock.moko.web3.crypto.KeccakParameter
import dev.icerock.moko.web3.crypto.digestKeccak
import dev.icerock.moko.web3.crypto.toHex
import dev.icerock.moko.web3.contract.MethodEncoder.createCallData
import dev.icerock.moko.web3.requests.Web3Requests
import dev.icerock.moko.web3.requests.executeBatch
import io.ktor.utils.io.core.toByteArray
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive

class SmartContract(
private val executor: Web3Executor,
val contractAddress: ContractAddress,
abiJson: JsonArray
private val abiJson: JsonArray
) {
private val json = Json {
ignoreUnknownKeys = true
Expand Down Expand Up @@ -92,114 +86,13 @@ class SmartContract(
from: WalletAddress? = null,
value: BigInt? = null
): ContractRPC {
val callData: String = createCallData(method, params)
val callData: String = createCallData(abiJson, method, params)
return ContractRPC(
to = contractAddress.prefixed,
from = from?.prefixed,
data = callData,
value = value
)
}

@Suppress("UNCHECKED_CAST")
private fun <T> Encoder<T>.encodeUnchecked(value: Any) = encode(value as T)

private fun createCallData(method: String, params: List<Any>): String {
val methodAbi: JsonObject = methodsAbi[method] ?: throw AbiMethodNotFoundException(
method,
methodsAbi
)
val inputParams: List<JsonObject> =
methodAbi.getValue("inputs").jsonArray.map { it.jsonObject }

val methodSignature: ByteArray = generateMethodSignature(method, inputParams)

val paramsEncoders = params.indices
.map { index -> inputParams[index].getValue("type").jsonPrimitive.content }
.map(::resolveEncoderForType)

val headPartiallyEncoded = paramsEncoders
.zip(params)
.map { (encoder, param) ->
when(encoder) {
is StaticEncoder<*> -> EncodedPart.StaticPart(encoder.encodeUnchecked(param))
is DynamicEncoder<*> -> EncodedPart.DynamicPart(encoder.encodeUnchecked(param))
}
}

val dynamicPartsSizes = headPartiallyEncoded
.runningFold(initial = headPartiallyEncoded.size * PART_SIZE) { acc: Int, encodedPart: EncodedPart ->
when(encodedPart) {
is EncodedPart.DynamicPart -> acc + encodedPart.encoded.size
is EncodedPart.StaticPart -> acc
}
}

val headEncoded = headPartiallyEncoded
.mapIndexed { index, encodedPart ->
if(encodedPart is EncodedPart.StaticPart)
encodedPart.encoded
else
UInt256Param.encode(dynamicPartsSizes[index].bi)
}.fold(byteArrayOf()) { acc, part -> acc + part }

val dynamicPartEncoded = headPartiallyEncoded
.filterIsInstance<EncodedPart.DynamicPart>()
.fold(byteArrayOf()) { acc, part -> acc + part.encoded }

val data = methodSignature + headEncoded + dynamicPartEncoded

return "0x" + data.toHex().lowercase()
}

private val listTypeRegex = Regex("(.*)\\[]")
private fun resolveEncoderForType(typeAnnotation: String) = when {
typeAnnotation.matches(listTypeRegex) -> {
val (subtypeAnnotation) = listTypeRegex.find(typeAnnotation)!!.destructured
ListParam(StaticEncoders.forType(subtypeAnnotation).encoder)
}
else -> StaticEncoders.forType(typeAnnotation).encoder
}

@OptIn(ExperimentalStdlibApi::class)
private fun generateMethodSignature(
method: String,
inputParams: List<JsonObject>
): ByteArray {
val signature = StringBuilder().apply {
append(method)
append("(")
inputParams.forEachIndexed { index, param ->
if (index != 0) append(",")
append(param.getValue("type").jsonPrimitive.content)
}
append(")")
}.toString().toByteArray()
val sha3 = signature.digestKeccak(KeccakParameter.KECCAK_256)
return sha3.copyOf(4)
}

companion object {
internal const val PART_SIZE = 32
}
}

/**
* First iteration while encoding is an iteration when all static params encoded,
* while dynamic params replaced with DynamicPass, later it is being replaced with encoded
* dynamic params
*/
private sealed interface EncodedPart {
val encoded: ByteArray
class DynamicPart(override val encoded: ByteArray) : EncodedPart
class StaticPart(override val encoded: ByteArray) : EncodedPart
}

/**
* First iteration while decoding is an iteration over params head, at that moment,
* static types are fully decoded, while for dynamic types only offset decoded
*/
private sealed interface DecodedPart {
class PartiallyDynamic(val offset: Int) : DecodedPart
class Fully(val value: Any) : DecodedPart
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.icerock.moko.web3.contract

import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject

class TupleParam(val param: JsonObject) : DynamicEncoder<List<Any>> {
override fun encode(item: List<Any>): ByteArray {
val components = param.getValue(key = "components")
.jsonArray.map(JsonElement::jsonObject)
return MethodEncoder.encodeParams(components, item)
}

override fun decode(source: ByteArray): List<Any> {
TODO("moko-web3 does not support decoding dynamic params yet")
}
}
8 changes: 6 additions & 2 deletions web3/src/commonTest/kotlin/dev.icerock.moko.web3/Web3Test.kt
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,16 @@ class Web3Test {
params = listOf(
address,
bigInt,
list
list,
listOf(
address,
list
)
)
)
}
assertEquals(
expected = "0x34ba830c0000000000000000000000009a0a2498ec7f105ef65586592a5b6d4da3590d74000000000000000000000000000000000000000000000000016345785d8a0000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000020000000000000000000000009a0a2498ec7f105ef65586592a5b6d4da3590d74000000000000000000000000000000000000000000000000016345785d8a0000",
expected = "0x170159cd0000000000000000000000009a0a2498ec7f105ef65586592a5b6d4da3590d74000000000000000000000000000000000000000000000000016345785d8a0000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000020000000000000000000000009a0a2498ec7f105ef65586592a5b6d4da3590d74000000000000000000000000000000000000000000000000016345785d8a00000000000000000000000000009a0a2498ec7f105ef65586592a5b6d4da3590d74000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000009a0a2498ec7f105ef65586592a5b6d4da3590d74000000000000000000000000000000000000000000000000016345785d8a0000",
actual = result.data
)
}
Expand Down
Loading

0 comments on commit 9e3550c

Please sign in to comment.