Skip to content

Commit

Permalink
Anvil RPC tests, introduced generalised interface IEthereumLikeNode a…
Browse files Browse the repository at this point in the history
…nd common test suite CommonForkNodeWeb3JTest
  • Loading branch information
ruXlab committed Oct 15, 2023
1 parent 6f67e45 commit 13e3396
Show file tree
Hide file tree
Showing 13 changed files with 254 additions and 27 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# 🍴 PokeFork: EVM Network Forking Library for Kotlin, Scala, and Java Developers
# 🍴 PokeFork: Seamless integration of Hardhat and Foundry Anvil with your Java/Kotlin app

![build status](https://github.com/ruXlab/pokefork/actions/workflows/tests.yml/badge.svg)

PokeFork is an open-source library project designed to empower
developers working with Ethereum Virtual Machine (EVM) networks.
It offers the ability to fork off Ethereum-like networks and enables
the massive swarm of JVM to test their smart contracts and applications.
It allows to run a local development or forked node of the Ethereum-like network
using Hardhat or Foundry Anvil and interact with it from your Java/Kotlin/Scala code.

## Motivation

Expand Down
2 changes: 1 addition & 1 deletion core/src/main/kotlin/vc/rux/pokefork/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import vc.rux.pokefork.hardhat.HardHatNodeConfig
import vc.rux.pokefork.hardhat.HardhatNode

fun main() {
val hhFork = HardhatNode.fork(
val hhFork = HardhatNode.start(
HardHatNodeConfig.fork(
"https://rpc.ankr.com/eth",
)
Expand Down
9 changes: 6 additions & 3 deletions core/src/main/kotlin/vc/rux/pokefork/anvil/AnvilNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,24 @@ import vc.rux.pokefork.common.idPrefix
import vc.rux.pokefork.common.waitForRpcToBoot
import vc.rux.pokefork.defaultDockerClient
import vc.rux.pokefork.hardhat.HardhatNode
import vc.rux.pokefork.hardhat.IEthereumLikeNode
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

class AnvilNode private constructor(
val config: AnvilNodeConfig,
private val dockerClient: DockerClient = defaultDockerClient
) {
): IEthereumLikeNode {
private val chainId = config.nodeMode.chainId ?: 31337
private val imageName: String by config::imageName
private val imageTag = config.imageTag ?: mkDefaultImageTag()
private val fullImage = "$imageName:$imageTag"

lateinit var containerId: String

lateinit var localRpcNodeUrl: String
override lateinit var localRpcNodeUrl: String
override val nodeMode: NodeMode by config::nodeMode


private fun mkDefaultImageTag(): String =
"anvil-${config.nodeMode.idPrefix}$chainId"
Expand Down Expand Up @@ -81,7 +84,7 @@ class AnvilNode private constructor(
}


fun stop() {
override fun stop() {
if (!::containerId.isInitialized)
throw IllegalStateException("The container is not running, cannot stop it")

Expand Down
10 changes: 5 additions & 5 deletions core/src/main/kotlin/vc/rux/pokefork/hardhat/HardhatNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@ import vc.rux.pokefork.hardhat.internal.HardHatDockerfile
import java.nio.file.Files
import kotlin.time.Duration.Companion.seconds


class HardhatNode private constructor(
val config: HardHatNodeConfig,
private val dockerClient: DockerClient = defaultDockerClient
) {
) : IEthereumLikeNode {
private val chainId = config.nodeMode.chainId ?: 31337L
private val imageName: String by config::imageName
private val imageTag = config.imageTag ?: mkDefaultImageTag()
private val fullImage = "$imageName:$imageTag"

lateinit var containerId: String

lateinit var localRpcNodeUrl: String
override lateinit var localRpcNodeUrl: String
override val nodeMode: NodeMode by config::nodeMode

private fun mkDefaultImageTag(): String =
"hardhat-${config.hardhatVersion}-${config.nodeMode.idPrefix}$chainId"
Expand Down Expand Up @@ -66,7 +66,7 @@ class HardhatNode private constructor(
waitForRpcToBoot(localRpcNodeUrl, imageName, MAX_WAIT_BOOT_TIME)
}

fun stop() {
override fun stop() {
if (!::containerId.isInitialized)
throw IllegalStateException("The container is not running, cannot stop it")

Expand Down Expand Up @@ -124,7 +124,7 @@ class HardhatNode private constructor(
private val MAX_WAIT_BOOT_TIME = 60.seconds

@JvmStatic
fun fork(config: HardHatNodeConfig): HardhatNode {
fun start(config: HardHatNodeConfig): HardhatNode {
return HardhatNode(config).also { it.run() }
}
}
Expand Down
10 changes: 10 additions & 0 deletions core/src/main/kotlin/vc/rux/pokefork/hardhat/IEthereumLikeNode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package vc.rux.pokefork.hardhat

import vc.rux.pokefork.NodeMode

interface IEthereumLikeNode {
val localRpcNodeUrl: String

val nodeMode: NodeMode
fun stop()
}
2 changes: 1 addition & 1 deletion core/src/test/kotlin/hardhat/HardhatNodeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class HardhatTest {
"local" -> HardHatNodeConfig.local()
else -> throw IllegalArgumentException("Unsupported new nodeMode: $nodeMode, please update the test")
}
val connection = HardhatNode.fork(config)
val connection = HardhatNode.start(config)

// given precondition: given container is running
var container = defaultDockerClient.listContainersCmd().exec()
Expand Down
10 changes: 5 additions & 5 deletions web3j/src/main/kotlin/vc/rux/pokefork/web3j/LocalWeb3jNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,23 @@ import org.web3j.protocol.http.HttpService
import vc.rux.pokefork.IForkNode
import vc.rux.pokefork.ILocalNode
import vc.rux.pokefork.NodeMode
import vc.rux.pokefork.hardhat.HardhatNode
import vc.rux.pokefork.hardhat.IEthereumLikeNode
import vc.rux.pokefork.web3j.utils.toHexStringPrefixed
import vc.rux.pokefork.web3j.utils.toHexStringSuffixed
import java.lang.System.currentTimeMillis
import java.math.BigInteger

// hm, looks like it can be a separate project
class LocalWeb3jNode(
private val hardhatNode: HardhatNode,
private val hardhatNode: IEthereumLikeNode,
private val jsonRpc20: JsonRpc2_0Web3j,
private val httpService: HttpService,
) : ILocalNode, IForkNode, Web3j by jsonRpc20 {
override fun forkBlock(blockNumber: Long) {
log.debug("forkBlock: forking from $blockNumber")
val startedAt = currentTimeMillis()
val realRpcUrl = (hardhatNode.config.nodeMode as? NodeMode.Fork)?.realNodeRpc
?: throw IllegalArgumentException("Node is not running in fork mode, used configuration: ${hardhatNode.config.nodeMode.javaClass.simpleName}")
val realRpcUrl = (hardhatNode.nodeMode as? NodeMode.Fork)?.realNodeRpc
?: throw IllegalArgumentException("Node is not running in fork mode, used configuration: ${hardhatNode.nodeMode.javaClass.simpleName}")

sendRpcCallAndCheckResponse("hardhat_reset", listOf(
mapOf("forking" to mapOf(
Expand Down Expand Up @@ -93,7 +93,7 @@ class LocalWeb3jNode(
companion object {
private val log = LoggerFactory.getLogger(LocalWeb3jNode::class.java)

fun from(hardhatNode: HardhatNode): LocalWeb3jNode {
fun from(hardhatNode: IEthereumLikeNode): LocalWeb3jNode {
val http = HttpService(hardhatNode.localRpcNodeUrl)
return LocalWeb3jNode(hardhatNode, JsonRpc2_0Web3j(http), http)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal fun Response<*>.throwIfErrored() {
}
}
internal fun Response<*>.throwIfResultIsNotTrue() {
if (result != "true") {
if (result != null && result != "true") {
throw PokeForkError("No errors were reported but server returned '$result' instead of expected 'true'")
}
}
Expand Down
18 changes: 18 additions & 0 deletions web3j/src/test/kotlin/vc/rux/pokefork/web3j/AnvilForkWeb3Test.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package vc.rux.pokefork.web3j

import vc.rux.pokefork.NodeMode
import vc.rux.pokefork.anvil.AnvilNode
import vc.rux.pokefork.anvil.AnvilNodeConfig

/**
* Since junit5 doesn't support tests with multiple paramterised parameters in constructor and
* inside function body, we have to create a separate class for each test of the implementation.
*
* While this approach works, it reduces readability, so feel free to make PR to address this issue.
*/
class AnvilForkWeb3Test : CommonForkNodeWeb3JTest() {
override fun forkImplementationFactory(mode: NodeMode): AnvilNode =
AnvilNode.start(
AnvilNodeConfig(nodeMode = mode)
)
}
173 changes: 173 additions & 0 deletions web3j/src/test/kotlin/vc/rux/pokefork/web3j/CommonForkNodeWeb3JTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package vc.rux.pokefork.web3j

import assertk.all
import assertk.assertThat
import assertk.assertions.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.web3j.contracts.eip20.generated.ERC20
import org.web3j.protocol.core.DefaultBlockParameterName.LATEST
import org.web3j.tx.gas.DefaultGasProvider
import vc.rux.pokefork.NodeMode
import vc.rux.pokefork.errors.PokeForkError
import vc.rux.pokefork.hardhat.IEthereumLikeNode
import vc.rux.pokefork.web3j.utils.toHexStringSuffixed
import java.math.BigDecimal
import java.math.BigDecimal.TEN
import java.math.BigInteger
import kotlin.text.RegexOption.IGNORE_CASE

abstract class CommonForkNodeWeb3JTest {
protected lateinit var fork: IEthereumLikeNode
abstract fun forkImplementationFactory(mode: NodeMode): IEthereumLikeNode

fun defaultMainnetFork(): IEthereumLikeNode =
forkImplementationFactory(NodeMode.Fork(MAINNET_RPC, 1))

@AfterEach
fun afterEach() {
if (::fork.isInitialized)
fork.stop()
}

@CsvSource(value = ["18100000,0.208933821146944046", "15000000,321495.8128334608039745"])
@ParameterizedTest(name = "forkBlock changes blocks - blockNumber: {0}, expectedBalance: {1}")
fun `forkBlock changes blocks`(blockNumber: Long, expectedBalance: BigDecimal) {
// given
val fork = defaultMainnetFork()
val web3 = LocalWeb3jNode.from(fork)

// when
web3.forkBlock(blockNumber)

// then
assertThat(web3.ethGetBalance(FTX_WALLET, LATEST).send().balance)
.isEqualTo((expectedBalance * TEN.pow(18)).toBigInteger())
}

@Test
fun `chainId is set to the required one `() {
// given
fork = forkImplementationFactory(NodeMode.Fork(MAINNET_RPC, 42))

// when
val web3 = LocalWeb3jNode.from(fork)

// then
val chainId = web3.netVersion().send().netVersion
assertThat(chainId).isEqualTo("42")
}

@Test
fun `when forked, the block number must be greater than 0`() {
// given
fork = defaultMainnetFork()
val web3 = LocalWeb3jNode.from(fork)

// when and then
assertThat(web3.ethBlockNumber().send().blockNumber.toLong())
.isGreaterThan(0)
}

@Test
fun `can mine blocks`() {
// given
fork = defaultMainnetFork()
val web3 = LocalWeb3jNode.from(fork)
val bnBeforeMine = web3.ethBlockNumber().send().blockNumber.toLong()
println(bnBeforeMine)

// when
web3.mine(42)

// then
val bnAfterMine = web3.ethBlockNumber().send().blockNumber.toLong()
assertThat(bnAfterMine).isEqualTo(bnBeforeMine + 42)
}

@Test
fun `setBalance can set balance in forked network`() {
// given
fork = defaultMainnetFork()
val web3 = LocalWeb3jNode.from(fork)
val balanceBefore = web3.ethGetBalance(VITALIK_WALLET, LATEST).send().balance

// when
web3.setBalance(VITALIK_WALLET, 42.toBigInteger())
web3.mine(1)

// then
val balanceAfter = web3.ethGetBalance(VITALIK_WALLET, LATEST).send().balance
assertThat(balanceAfter).all {
println(balanceBefore)
println(balanceAfter)
isNotEqualTo(balanceBefore)
isEqualTo(42.toBigInteger())
}
}


@Test
fun `setStorageAt can change the token name`() {
// given
fork = defaultMainnetFork()
val web3 = LocalWeb3jNode.from(fork)

val ust = ERC20.load(UST_TOKEN, web3, randomCredentials, DefaultGasProvider());
val newName = "UnSTablecoin"
val newNameInMemoryLayout = run { // let's do low level string formation
val nameAsHexAlignedLeft = BigInteger(newName.toByteArray()).toHexStringSuffixed(32).drop(2)
BigInteger(nameAsHexAlignedLeft, 16).or(newName.length.toBigInteger() * BigInteger.TWO)
}
// precondition
assertThat(ust.symbol().send()).isEqualTo("UST")

// when
// utf8 string to BigInteger
web3.setStorageAt(UST_TOKEN, 0x4.toBigInteger(), newNameInMemoryLayout)

// then
assertThat(ust.symbol().send()).isEqualTo(newName)
}

@Test
fun `setNextBlockBaseFeePerGas can set base fee per gas`() {
// given
fork = defaultMainnetFork()
val web3 = LocalWeb3jNode.from(fork)

// when
web3.setNextBlockBaseFeePerGas(42.toBigInteger())
web3.mine(1)

// then
val block = web3.ethGetBlockByNumber(LATEST, false).send().block
assertThat(block.baseFeePerGas).isEqualTo(42.toBigInteger())
}

@Test
fun `setBalance throws exception if bad params passed`() {
// given
fork = defaultMainnetFork()
val web3 = LocalWeb3jNode.from(fork)

// when and then
val error = assertThrows<PokeForkError> {
web3.setBalance("0xPoKeForK", 42.toBigInteger())
}
assertThat(error)
.isInstanceOf<PokeForkRpcCallError>()
.transform { it.error.message }
.containsMatch("(invalid value)|(invalid length)".toRegex(IGNORE_CASE)) // hardhat returns '..invalid value..', anvil - '..invalid length..'
}


companion object {
const val VITALIK_WALLET = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
const val FTX_WALLET = "0x2FAF487A4414Fe77e2327F0bf4AE2a264a776AD2"
const val UST_TOKEN = "0xa47c8bf37f92abed4a126bda807a7b7498661acd"
}
}
18 changes: 18 additions & 0 deletions web3j/src/test/kotlin/vc/rux/pokefork/web3j/HardhatForkWeb3Test.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package vc.rux.pokefork.web3j

import vc.rux.pokefork.NodeMode
import vc.rux.pokefork.hardhat.HardHatNodeConfig
import vc.rux.pokefork.hardhat.HardhatNode

/**
* Since junit5 doesn't support tests with multiple paramterised parameters in constructor and
* inside function body, we have to create a separate class for each test of the implementation.
*
* While this approach works, it reduces readability, so feel free to make PR to address this issue.
*/
class HardhatForkWeb3Test : CommonForkNodeWeb3JTest() {
override fun forkImplementationFactory(mode: NodeMode): HardhatNode =
HardhatNode.start(
HardHatNodeConfig(nodeMode = mode)
)
}
Loading

0 comments on commit 13e3396

Please sign in to comment.