All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.web3j.evm.EmbeddedEthereum.kt Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2019 Web3 Labs Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.web3j.evm

import com.google.common.io.Resources
import org.apache.tuweni.bytes.Bytes
import org.apache.tuweni.units.bigints.UInt256
import org.hyperledger.besu.cli.config.EthNetworkConfig
import org.hyperledger.besu.cli.config.NetworkName
import org.hyperledger.besu.config.GenesisConfigFile
import org.hyperledger.besu.datatypes.Address
import org.hyperledger.besu.datatypes.Hash
import org.hyperledger.besu.datatypes.Wei
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.BlockParameter
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.WithdrawalParameter
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.BlockResult
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.TransactionCompleteResult
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.TransactionHashResult
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.TransactionReceiptLogResult
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.TransactionReceiptResult
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.TransactionReceiptRootResult
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.TransactionReceiptStatusResult
import org.hyperledger.besu.ethereum.api.query.TransactionReceiptWithMetadata
import org.hyperledger.besu.ethereum.core.PrivacyParameters
import org.hyperledger.besu.ethereum.core.Transaction
import org.hyperledger.besu.ethereum.mainnet.MainnetProtocolSchedule
import org.hyperledger.besu.ethereum.mainnet.TransactionReceiptType
import org.hyperledger.besu.ethereum.rlp.RLP
import org.hyperledger.besu.ethereum.transaction.CallParameter
import org.hyperledger.besu.evm.internal.EvmConfiguration
import org.hyperledger.besu.evm.tracing.OperationTracer
import org.slf4j.LoggerFactory
import org.web3j.evm.utils.TestAccountsConstants
import org.web3j.protocol.core.methods.response.AccessListObject
import org.web3j.protocol.core.methods.response.EthBlock
import org.web3j.protocol.core.methods.response.EthBlock.Withdrawal
import org.web3j.utils.Numeric
import java.math.BigInteger
import java.nio.charset.StandardCharsets
import java.util.Optional
import org.web3j.abi.datatypes.Address as wAddress
import org.web3j.protocol.core.methods.request.Transaction as wTransaction
import org.web3j.protocol.core.methods.response.TransactionReceipt as wTransactionReceipt

/**
 * Embedded Web3j Ethereum blockchain.
 */
class EmbeddedEthereum(
    private val configuration: Configuration,
    operationTracer: OperationTracer,
) {
    private val chain = InMemoryBesuChain(configuration, operationTracer)
    private val blockchainQueries = chain.blockchainQueries

    /**
     * Process an unsigned Web3j transaction object.
     *
     * The transaction will be signed by the matching key pair
     * for the provided "from" address in the transaction if it is
     * defined in the test accounts, otherwise will be signed with
     * the first test account key pair.
     *
     * @param web3jTransaction The web3j transaction
     * @return the transaction hash
     * @see TestAccountsConstants.TEST_ACCOUNTS
     */
    fun processTransaction(web3jTransaction: wTransaction): String {
        val transaction = web3jTransaction.toSignedBesuTx()
        chain.processTransaction(transaction)
        return transaction.hash.toHexString()
    }

    /**
     * Process a signed RLP encoded transaction.
     *
     * @param signedTransactionData The singed RLP encoded transaction data
     * @return the transaction hash
     */
    fun processTransaction(signedTransactionData: String): String {
        val transaction = Transaction.readFrom(RLP.input(Bytes.fromHexString(signedTransactionData)))
        chain.processTransaction(transaction)
        return transaction.hash.toHexString()
    }

    /**
     * Maps unsigned Web3J to Besu signed transaction.
     */
    private fun wTransaction.toSignedBesuTx(): Transaction {
        with(this) {
            val chainId = chainId?.toBigInteger()
                ?: chain.chainId
            val nonce = if (nonce == null) {
                getTransactionCount(wAddress(from)).toLong()
            } else {
                hexToULong(nonce)
            }
            val from = (from ?: "0x0").lowercase()

            // TODO should we add support for account 0x0 fake signature?
            return Transaction.builder()
                .gasLimit(transactionGasLimitOrDefault(this))
                .gasPrice(Wei.of(hexToULong(gasPrice)))
                .nonce(nonce)
                .sender(Address.fromHexString(from))
                .to(Address.fromHexString(to))
                .payload(Bytes.fromHexString(data))
                .value(Wei.of(UInt256.fromHexString(value ?: "0x0")))
                .chainId(chainId)
                .guessType()
                .signAndBuild(
                    TestAccountsConstants.TEST_ACCOUNTS[from] ?: TestAccountsConstants.TEST_ACCOUNTS.values.first(),
                )
        }
    }

    fun getTransactionCount(w3jAddress: wAddress): BigInteger {
        val count = blockchainQueries.getTransactionCount(w3jAddress.asBesu())
        return BigInteger.valueOf(count)
    }

    fun getTransactionReceipt(transactionHashParam: String): wTransactionReceipt? {
        val hash = Hash.fromHexStringLenient(transactionHashParam)

        val genesisConfig = if (configuration.genesisFileUrl === null) {
            val networkConfig = EthNetworkConfig.getNetworkConfig(NetworkName.DEV)
            GenesisConfigFile.fromConfig(networkConfig.genesisConfig)
        } else {
            GenesisConfigFile.fromConfig(
                @Suppress("UnstableApiUsage")
                Resources.toString(
                    configuration.genesisFileUrl,
                    StandardCharsets.UTF_8,
                ),
            )
        }
        val configOptions = genesisConfig.getConfigOptions(InMemoryBesuChain.DEFAULT_GENESIS_OVERRIDES)

        val protocolSchedule = MainnetProtocolSchedule.fromConfig(
            configOptions,
            PrivacyParameters.DEFAULT,
            true,
            EvmConfiguration.DEFAULT,
        )

        val result = blockchainQueries
            .transactionReceiptByTransactionHash(hash, protocolSchedule)
            .map { receipt -> getResult(receipt) }
            .orElse(null)
            ?: return null

        val blockHash = result.blockHash
        val blockNumber = result.blockNumber
        val contractAddress = result.contractAddress
        val cumulativeGasUsed = result.cumulativeGasUsed
        val from = result.from
        val gasUsed = result.gasUsed
        val effectiveGasPrice = result.effectiveGasPrice
        val logs = result.logs.map(this::mapTransactionReceiptLog)
        val logsBloom = result.logsBloom
        val to = result.to
        val transactionHash = result.transactionHash
        val transactionIndex = result.transactionIndex
        val root = if (result is TransactionReceiptRootResult) result.root else null
        val status = if (result is TransactionReceiptStatusResult) result.status else null

        return wTransactionReceipt(
            transactionHash,
            transactionIndex,
            blockHash,
            blockNumber,
            cumulativeGasUsed,
            gasUsed,
            contractAddress,
            root,
            status,
            from,
            to,
            logs,
            logsBloom,
            "",
            "",
            effectiveGasPrice,

        )
    }

    private fun mapTransactionReceiptLog(result: TransactionReceiptLogResult): org.web3j.protocol.core.methods.response.Log {
        val removed = result.isRemoved
        val logIndex = result.logIndex
        val transactionIndex = result.transactionIndex
        val transactionHash = result.transactionHash
        val blockHash = result.blockHash
        val blockNumber = result.blockNumber
        val address = result.address
        val data = result.data
        val type: String? = null // TODO?
        val topics = result.topics

        return org.web3j.protocol.core.methods.response.Log(
            removed,
            logIndex,
            transactionIndex,
            transactionHash,
            blockHash,
            blockNumber,
            address,
            data,
            type,
            topics,
        )
    }

    private fun getResult(receipt: TransactionReceiptWithMetadata): TransactionReceiptResult {
        return if (receipt.receipt.transactionReceiptType == TransactionReceiptType.ROOT) {
            TransactionReceiptRootResult(receipt)
        } else {
            TransactionReceiptStatusResult(receipt)
        }
    }

    fun ethCall(
        web3jTransaction: wTransaction,
    ): String {
        val result = chain.call(web3jTransaction.asCallParameter())
        // TODO handle errors/**/
        return result.get().result.output.toHexString()
    }

    fun estimateGas(
        web3jTransaction: wTransaction,
    ): String {
        val result = chain.call(web3jTransaction.asCallParameter())
        return longToHex(result.get().result.estimateGasUsedByTransaction)
    }

    fun ethBlockNumber(): String {
        return longToHex(blockchainQueries.headBlockNumber())
    }

    fun ethGetBalance(
        w3jAddress: org.web3j.abi.datatypes.Address,
        @Suppress("UNUSED_PARAMETER") defaultBlockParameter: String,
    ): String? {
        return blockchainQueries
            .accountBalance(Address.fromHexString(w3jAddress.value), blockchainQueries.headBlockNumber())
            .map(Wei::toHexString)
            .orElse(null)
    }

    fun ethBlockByHash(hash: String, fullTransactionObjects: Boolean): EthBlock.Block? {
        val maybeBlockResult = chain.ethBlockByHash(Hash.fromHexString(hash), fullTransactionObjects)
        return maybeBlockResult.map { br -> ethBlockByBlockResult(br, fullTransactionObjects) }.orElse(null)
    }

    fun ethBlockByNumber(blockNumber: String, fullTransactionObjects: Boolean): EthBlock.Block? {
        val maybeBlockResult = chain.ethBlockByNumber(hexToULong(blockNumber), fullTransactionObjects)
        return maybeBlockResult.map { br -> ethBlockByBlockResult(br, fullTransactionObjects) }.orElse(null)
    }

    private fun ethBlockFullTransactionObject(blockResult: BlockResult): List {
        return blockResult.transactions.map { it ->
            val tcr = it as TransactionCompleteResult
            EthBlock.TransactionObject(
                tcr.hash,
                tcr.nonce,
                tcr.blockHash,
                tcr.blockNumber,
                tcr.chainId,
                tcr.transactionIndex,
                tcr.from,
                tcr.to,
                tcr.value,
                tcr.gasPrice,
                tcr.gas,
                tcr.input,
                null, // TODO?
                null, // TODO?
                null, // TODO?
                tcr.r,
                tcr.s,
                hexToULong(tcr.v),
                tcr.yParity,
                tcr.type,
                tcr.maxFeePerGas,
                tcr.maxPriorityFeePerGas,
                tcr.accessList?.map {
                    AccessListObject(
                        it.address.toString(),
                        mutableListOf(it.storageKeys.toString()),
                    )
                },
            )
        }
    }

    private fun ethBlockTransactionHash(blockResult: BlockResult): List {
        return blockResult.transactions.map {
            val thr = it as TransactionHashResult
            EthBlock.TransactionHash(
                thr.hash,
            )
        }
    }

    private fun ethBlockByBlockResult(blockResult: BlockResult, fullTransactionObjects: Boolean): EthBlock.Block? {
        val transactionResults = if (fullTransactionObjects) {
            ethBlockFullTransactionObject(blockResult)
        } else {
            ethBlockTransactionHash(blockResult)
        }

        return EthBlock.Block(
            blockResult.number,
            blockResult.hash,
            blockResult.parentHash,
            blockResult.nonce,
            blockResult.sha3Uncles,
            blockResult.logsBloom,
            blockResult.transactionsRoot,
            blockResult.stateRoot,
            blockResult.receiptsRoot,
            null, // TODO?
            blockResult.miner,
            null, // TODO?
            blockResult.difficulty,
            blockResult.totalDifficulty,
            blockResult.extraData,
            blockResult.size,
            blockResult.gasLimit,
            blockResult.gasUsed,
            blockResult.timestamp,
            transactionResults,
            null, // TODO?
            null, // TODO?
            blockResult.baseFeePerGas,
            blockResult.withdrawalsRoot,
            blockResult.withdrawals?.map { withdrawalParameter ->
                toWithdrawal(withdrawalParameter)
            }?.toList(),
        )
    }

    private fun toWithdrawal(withdrawalParameter: WithdrawalParameter): Withdrawal {
        val withdrawal = withdrawalParameter.toWithdrawal()
        return Withdrawal(withdrawal.index.toString(), withdrawal.validatorIndex.toString(), withdrawal.address.toHexString(), withdrawal.amount.toHexString())
    }

    fun ethGetCode(w3jAddress: wAddress, defaultBlockParameter: String): String {
        val blockParameter = BlockParameter(defaultBlockParameter)
        val blockNumber: Optional = blockParameter.number
        val address = w3jAddress.asBesu()

        return when {
            blockNumber.isPresent -> {
                blockchainQueries.getCode(address, blockNumber.get())?.get()?.toString() ?: "0x"
            }
            blockParameter.isLatest -> {
                blockchainQueries.getCode(address, blockchainQueries.headBlockNumber())?.get()?.toString() ?: "0x"
            }
            blockParameter.isEarliest -> {
                blockchainQueries.getCode(address, 0)?.get()?.toString() ?: "0x"
            }
            else -> {
                // If block parameter is pending but there is no pending state so take latest
                blockchainQueries.getCode(address, blockchainQueries.headBlockNumber())?.get()?.toString() ?: "0x"
            }
        }
    }

    fun ethGetBlockTransactionCountByHash(hash: Hash): String {
        return Numeric.encodeQuantity(BigInteger.valueOf(blockchainQueries.getTransactionCount(hash).toLong()))
    }

    fun ethGetBlockTransactionCountByNumber(blockNumber: Long): String {
        return Numeric.encodeQuantity(
            BigInteger.valueOf(
                blockchainQueries.getTransactionCount(blockNumber).get().toLong(),
            ),
        )
    }

    companion object {
        private val LOG = LoggerFactory.getLogger(EmbeddedEthereum::class.java)

        private fun hexToULong(hex: String): Long {
            return UInt256.fromHexString(hex).toLong()
        }

        private fun longToHex(value: Long): String {
            return UInt256.valueOf(value).toShortHexString()
        }

        private fun transactionGasLimitOrDefault(web3jTransaction: org.web3j.protocol.core.methods.request.Transaction): Long {
            // TODO: Verify happy with default value..
            return if (web3jTransaction.gas == null) 10_000_000L else hexToULong(web3jTransaction.gas)
        }
    }
}

private fun wTransaction.asCallParameter(): CallParameter {
    with(this) {
        return CallParameter(
            Address.fromHexString(from),
            Address.fromHexString(to),
            Long.MAX_VALUE,
            Wei.ZERO,
            Wei.ZERO,
            Bytes.fromHexString(data),
        )
    }
}

private fun wAddress.asBesu(): Address {
    return Address.fromHexString(this.value)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy