diff --git a/ethers-abi/src/main/kotlin/io/ethers/abi/AbiFunction.kt b/ethers-abi/src/main/kotlin/io/ethers/abi/AbiFunction.kt index 27ebb8a..2f53bfd 100644 --- a/ethers-abi/src/main/kotlin/io/ethers/abi/AbiFunction.kt +++ b/ethers-abi/src/main/kotlin/io/ethers/abi/AbiFunction.kt @@ -34,22 +34,113 @@ data class AbiFunction( } companion object { - private val SIGNATURE_REGEX = "(\\w+)\\((.*?)\\)\\s*?(\\((.*)\\))?".toRegex() + private enum class ParseState { + NAME, + INPUTS, + OUTPUTS, + } + /** + * Parse function signature, returning [AbiFunction] instance, or throwing an exception if signature is invalid. + * Only raw tuple types are supported, which must be wrapped in parentheses, e.g. `(uint256,address)`. + * + * Example signature: + * ``` + * function balanceOf(address owner) public view returns (uint256 balance) + * ``` + * */ fun parseSignature(signature: String): AbiFunction { - val match = SIGNATURE_REGEX.matchEntire( - signature.replace("function", "").replace("returns", "").trim(), - ) ?: throw IllegalArgumentException("Invalid signature: $signature") + val cleanSignature = signature.replace("function", "").trim() + var state = ParseState.NAME + var startIndex = 0 + var nestingLevel = 0 + var name: String? = null + var inputsRaw: String? = null + var outputsRaw: String? = null + + for (i in cleanSignature.indices) { + when (state) { + ParseState.NAME -> { + if (cleanSignature[i] == '(') { + name = cleanSignature.substring(startIndex, i) + startIndex = i + 1 + nestingLevel++ + + state = ParseState.INPUTS + } + } + + ParseState.INPUTS -> { + if (cleanSignature[i] == '(') { + nestingLevel++ + continue + } + + if (cleanSignature[i] == ')') { + nestingLevel-- + + if (nestingLevel == 0) { + inputsRaw = cleanArgumentNames(cleanSignature.substring(startIndex, i)) + startIndex = i + 1 + state = ParseState.OUTPUTS + } + } + } + + ParseState.OUTPUTS -> { + when { + // find outputs start + nestingLevel == 0 && cleanSignature[i] != '(' -> continue + + // found outputs start, increment nesting level and remember start index + nestingLevel == 0 && cleanSignature[i] == '(' -> { + startIndex = i + 1 + nestingLevel++ + continue + } + + // found a nested definition, increment nesting level + nestingLevel > 0 && cleanSignature[i] == '(' -> { + nestingLevel++ + continue + } + } + + if (cleanSignature[i] == ')') { + nestingLevel-- + + if (nestingLevel == 0) { + outputsRaw = cleanArgumentNames(cleanSignature.substring(startIndex, i)) + break + } + } + } + } + } - val name = match.groupValues.getOrNull(1) if (name.isNullOrBlank()) throw IllegalArgumentException("Invalid signature, function has no name: $signature") - val inputsRaw = match.groupValues.getOrNull(2) - val outputsRaw = match.groupValues.getOrNull(4) val inputs = if (inputsRaw.isNullOrBlank()) emptyList() else AbiType.parseSignature(inputsRaw) val outputs = if (outputsRaw.isNullOrBlank()) emptyList() else AbiType.parseSignature(outputsRaw) return AbiFunction(name, inputs, outputs) } + + /** + * Remove all argument names from [signature], leaving only types, separated by commas. + * E.g. `address owner, uint256 amount` -> `address,uint256` + * */ + private fun cleanArgumentNames(signature: String): String { + return signature.split(',').joinToString(",") { + val cleaned = it.trim() + + // handle tuples + if (cleaned.startsWith('(') && cleaned.endsWith(')')) { + "(${cleanArgumentNames(cleaned.substring(1, cleaned.length - 1))})" + } else { + cleaned.split(' ').first() + } + } + } } } diff --git a/ethers-abi/src/test/kotlin/io/ethers/abi/AbiFunctionTest.kt b/ethers-abi/src/test/kotlin/io/ethers/abi/AbiFunctionTest.kt index 0df1004..d7371b2 100644 --- a/ethers-abi/src/test/kotlin/io/ethers/abi/AbiFunctionTest.kt +++ b/ethers-abi/src/test/kotlin/io/ethers/abi/AbiFunctionTest.kt @@ -17,25 +17,31 @@ class AbiFunctionTest : FunSpec({ listOf( TestCase("noArgsFunction()", "noArgsFunction", null, null), TestCase( - "hello(uint256,address)", + "function balanceOf(address owner) public view returns (uint256 balance)", + "balanceOf", + listOf(AbiType.Address), + listOf(AbiType.UInt(256)), + ), + TestCase( + "hello(uint256,address greeter)", "hello", listOf(AbiType.UInt(256), AbiType.Address), null, ), TestCase( - "callSomething(uint256,address)(address, uint256)", + "callSomething(uint256,address)(address owner, uint256)", "callSomething", listOf(AbiType.UInt(256), AbiType.Address), listOf(AbiType.Address, AbiType.UInt(256)), ), TestCase( - "onlyReturnValues()(uint256,address)", + "onlyReturnValues()(uint256 value,address owner)", "onlyReturnValues", null, listOf(AbiType.UInt(256), AbiType.Address), ), TestCase( - " function fullAbiSignature(address, uint256, bytes[]) returns(uint256,address)", + " function fullAbiSignature(address owner, uint256 balance, bytes[]) returns(uint256,address)", "fullAbiSignature", listOf(AbiType.Address, AbiType.UInt(256), AbiType.Array(AbiType.Bytes)), listOf(AbiType.UInt(256), AbiType.Address), @@ -47,7 +53,7 @@ class AbiFunctionTest : FunSpec({ null, ), TestCase( - "complexSignature(address,int256[],uint64[2],(string,bytes12))", + "complexSignature(address,int256[] values,uint64[2] blockTimes ,(string,bytes12))", "complexSignature", listOf( AbiType.Address,