Skip to content

Commit

Permalink
feat(abi): add support for parsing raw abi function signatures with a…
Browse files Browse the repository at this point in the history
…rgument names (#149)
  • Loading branch information
ArtificialPB authored Jul 18, 2024
1 parent 0030066 commit 6182373
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 12 deletions.
105 changes: 98 additions & 7 deletions ethers-abi/src/main/kotlin/io/ethers/abi/AbiFunction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}
}
}
16 changes: 11 additions & 5 deletions ethers-abi/src/test/kotlin/io/ethers/abi/AbiFunctionTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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,
Expand Down

0 comments on commit 6182373

Please sign in to comment.