Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Mirror Node Contract Queries #1188

Merged
merged 7 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions account_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,9 @@ const (
EvmAddress
)

var httpString string = "http"
var httpsString string = "https"
0xivanov marked this conversation as resolved.
Show resolved Hide resolved

func (id *AccountID) _MirrorNodeRequest(client *Client, populateType string) (map[string]interface{}, error) {
if client.mirrorNetwork == nil || len(client.GetMirrorNetwork()) == 0 {
return nil, errors.New("mirror node is not set")
Expand All @@ -337,11 +340,11 @@ func (id *AccountID) _MirrorNodeRequest(client *Client, populateType string) (ma
mirrorUrl = mirrorUrl[:index]

var url string
protocol := "https"
protocol := httpsString
port := ""

if client.GetLedgerID().String() == "" {
protocol = "http"
protocol = httpString
port = ":5551"
}

Expand Down
4 changes: 3 additions & 1 deletion contract_function_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"errors"
"fmt"
"math/big"
"strings"
)

// ContractFunctionParameters is a struct which builds a solidity function call
Expand Down Expand Up @@ -1718,8 +1719,9 @@

// AddAddress adds an address parameter to the function call
func (contract *ContractFunctionParameters) AddAddress(value string) (*ContractFunctionParameters, error) {
value = strings.TrimPrefix(value, "0x")
if len(value) != 40 {
return contract, errors.Unwrap(fmt.Errorf("address is required to be 40 characters"))
return contract, fmt.Errorf("address is required to be 40 characters")

Check warning on line 1724 in contract_function_parameters.go

View check run for this annotation

Codecov / codecov/patch

contract_function_parameters.go#L1724

Added line #L1724 was not covered by tests
}

addressBytes, err := hex.DecodeString(value)
Expand Down
14 changes: 14 additions & 0 deletions contract_function_parameters_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,9 @@ func TestUint8Min(t *testing.T) {
defer CloseIntegrationTestEnv(env, nil)
deployContract(env)
value := uint8(0)
gas, err := NewMirrorNodeContractEstimateGasQuery().
SetContractID(contractID).SetFunction("returnUint8", NewContractFunctionParameters().AddUint8(value)).Execute(env.Client)
require.NoError(t, err)
contractCal, err := NewContractCallQuery().SetGas(gas).SetQueryPayment(NewHbar(1)).
SetContractID(contractID).SetFunction("returnUint8", NewContractFunctionParameters().AddUint8(value)).SetMaxQueryPayment(NewHbar(20)).Execute(env.Client)
require.NoError(t, err)
Expand All @@ -315,6 +318,9 @@ func TestUint8Max(t *testing.T) {
defer CloseIntegrationTestEnv(env, nil)
deployContract(env)
value := uint8(255)
gas, err := NewMirrorNodeContractEstimateGasQuery().
SetContractID(contractID).SetFunction("returnUint8", NewContractFunctionParameters().AddUint8(value)).Execute(env.Client)
require.NoError(t, err)
contractCal, err := NewContractCallQuery().SetGas(gas).SetQueryPayment(NewHbar(1)).
SetContractID(contractID).SetFunction("returnUint8", NewContractFunctionParameters().AddUint8(value)).SetMaxQueryPayment(NewHbar(20)).Execute(env.Client)
require.NoError(t, err)
Expand All @@ -327,6 +333,8 @@ func TestUint16Min(t *testing.T) {
defer CloseIntegrationTestEnv(env, nil)
deployContract(env)
value := uint16(0)
gas, err := NewMirrorNodeContractEstimateGasQuery().
SetContractID(contractID).SetFunction("returnUint16", NewContractFunctionParameters().AddUint16(value)).Execute(env.Client)
contractCal, err := NewContractCallQuery().SetGas(gas).SetQueryPayment(NewHbar(1)).
SetContractID(contractID).SetFunction("returnUint16", NewContractFunctionParameters().AddUint16(value)).SetMaxQueryPayment(NewHbar(20)).Execute(env.Client)
require.NoError(t, err)
Expand All @@ -339,6 +347,8 @@ func TestUint16Max(t *testing.T) {
defer CloseIntegrationTestEnv(env, nil)
deployContract(env)
value := uint16(65535)
gas, err := NewMirrorNodeContractEstimateGasQuery().
SetContractID(contractID).SetFunction("returnUint16", NewContractFunctionParameters().AddUint16(value)).Execute(env.Client)
contractCal, err := NewContractCallQuery().SetGas(gas).SetQueryPayment(NewHbar(1)).
SetContractID(contractID).SetFunction("returnUint16", NewContractFunctionParameters().AddUint16(value)).SetMaxQueryPayment(NewHbar(20)).Execute(env.Client)
require.NoError(t, err)
Expand Down Expand Up @@ -1656,6 +1666,10 @@ func TestAddress(t *testing.T) {
value := "1234567890123456789012345678901234567890"
params, err := NewContractFunctionParameters().AddAddress(value)
require.NoError(t, err)
gas, err := NewMirrorNodeContractEstimateGasQuery().
SetContractID(contractID).SetFunction("returnAddress", params).Execute(env.Client)
require.NoError(t, err)

contractCal := NewContractCallQuery().SetGas(gas).SetQueryPayment(NewHbar(1)).
SetContractID(contractID).SetFunction("returnAddress", params)
result, err := contractCal.Execute(env.Client)
Expand Down
191 changes: 191 additions & 0 deletions examples/mirror_node_contract_queries/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package main

import (
"encoding/hex"
"fmt"
"os"
"strings"
"time"

"github.com/hiero-ledger/hiero-sdk-go/v2"
)

var contractBytecode = "60806040526040518060400160405280600581526020017f68656c6c6f0000000000000000000000000000000000000000000000000000008152505f90816100479190610293565b50348015610053575f80fd5b50610362565b5f81519050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f60028204905060018216806100d457607f821691505b6020821081036100e7576100e6610090565b5b50919050565b5f819050815f5260205f209050919050565b5f6020601f8301049050919050565b5f82821b905092915050565b5f600883026101497fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8261010e565b610153868361010e565b95508019841693508086168417925050509392505050565b5f819050919050565b5f819050919050565b5f61019761019261018d8461016b565b610174565b61016b565b9050919050565b5f819050919050565b6101b08361017d565b6101c46101bc8261019e565b84845461011a565b825550505050565b5f90565b6101d86101cc565b6101e38184846101a7565b505050565b5b81811015610206576101fb5f826101d0565b6001810190506101e9565b5050565b601f82111561024b5761021c816100ed565b610225846100ff565b81016020851015610234578190505b610248610240856100ff565b8301826101e8565b50505b505050565b5f82821c905092915050565b5f61026b5f1984600802610250565b1980831691505092915050565b5f610283838361025c565b9150826002028217905092915050565b61029c82610059565b67ffffffffffffffff8111156102b5576102b4610063565b5b6102bf82546100bd565b6102ca82828561020a565b5f60209050601f8311600181146102fb575f84156102e9578287015190505b6102f38582610278565b86555061035a565b601f198416610309866100ed565b5f5b828110156103305784890151825560018201915060208501945060208101905061030b565b8683101561034d5784890151610349601f89168261025c565b8355505b6001600288020188555050505b505050505050565b6102178061036f5f395ff3fe608060405234801561000f575f80fd5b5060043610610029575f3560e01c8063ce6d41de1461002d575b5f80fd5b61003561004b565b6040516100429190610164565b60405180910390f35b60605f8054610059906101b1565b80601f0160208091040260200160405190810160405280929190818152602001828054610085906101b1565b80156100d05780601f106100a7576101008083540402835291602001916100d0565b820191905f5260205f20905b8154815290600101906020018083116100b357829003601f168201915b5050505050905090565b5f81519050919050565b5f82825260208201905092915050565b5f5b838110156101115780820151818401526020810190506100f6565b5f8484015250505050565b5f601f19601f8301169050919050565b5f610136826100da565b61014081856100e4565b93506101508185602086016100f4565b6101598161011c565b840191505092915050565b5f6020820190508181035f83015261017c818461012c565b905092915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f60028204905060018216806101c857607f821691505b6020821081036101db576101da610184565b5b5091905056fea26469706673582212202a86c27939bfab6d4a2c61ebbf096d8424e17e22dfdd42320f6e2654863581e964736f6c634300081a0033"

/**
* @summary Mirror Node Contract Queries for estimation or simulation of transactions
*/
func main() {
var client *hiero.Client
var err error

// Retrieving network type from environment variable HEDERA_NETWORK
client, err = hiero.ClientForName(os.Getenv("HEDERA_NETWORK"))
if err != nil {
panic(fmt.Sprintf("%v : error creating client", err))
}

// Retrieving operator ID from environment variable OPERATOR_ID
operatorAccountID, err := hiero.AccountIDFromString(os.Getenv("OPERATOR_ID"))
if err != nil {
panic(fmt.Sprintf("%v : error converting string to AccountID", err))
}

// Retrieving operator key from environment variable OPERATOR_KEY
operatorKey, err := hiero.PrivateKeyFromString(os.Getenv("OPERATOR_KEY"))
if err != nil {
panic(fmt.Sprintf("%v : error converting string to PrivateKey", err))
}

// Setting the client operator ID and key
client.SetOperator(operatorAccountID, operatorKey)

fmt.Println("Example Start!")

/*
* Step 1: Create a contract
*/

hexBytecode, err := hex.DecodeString(contractBytecode)
if err != nil {
panic(fmt.Sprintf("%v : error decoding bytecode", err))
}

resp, err := hiero.NewContractCreateTransaction().
SetBytecode(hexBytecode).
SetGas(2000000).
SetTransactionMemo("Create a simple contract instance").
Execute(client)
if err != nil {
panic(fmt.Sprintf("%v : error creating contract", err))
}

receipt, err := resp.GetReceipt(client)
if err != nil {
panic(fmt.Sprintf("%v : error retrieving receipt", err))
}
contractId := receipt.ContractID
fmt.Println("Created new contract with ID: ", contractId)

/*
* Step 2: Wait for mirror node to import data
*/
time.Sleep(5 * time.Second)

/*
* Step 3: Estimate the gas needed
*/
gas, err := hiero.NewMirrorNodeContractEstimateGasQuery().
SetContractID(*contractId).
SetSender(operatorAccountID).
SetGasLimit(30_000).
SetGasPrice(1234).
SetFunction("getMessage", nil).
Execute(client)
if err != nil {
panic(fmt.Sprintf("%v : error estimating gas", err))
}

fmt.Printf("Gas needed for this query: %v\n", gas)

/*
* Step 4: Do the query against the consensus node using the estimated gas
*/

_, err = hiero.NewContractCallQuery().
SetContractID(*contractId).
SetGas(gas).
SetFunction("getMessage", nil).
Execute(client)

if err != nil {
panic(fmt.Sprintf("%v : error executing contract call query", err))
}

/*
* Step 5: Simulate the transaction for free, using the mirror node
*/

result, err := hiero.NewMirrorNodeContractCallQuery().
SetContractID(*contractId).
SetSender(operatorAccountID).
SetGasLimit(30_000).
SetGasPrice(1234).
SetBlockNumber(10000).
SetFunction("getMessage", nil).
Execute(client)
if err != nil {
panic(fmt.Sprintf("%v : error during contract call", err))
}

decodedResult, err := DecodeABIHexString(result)
if err != nil {
panic(fmt.Sprintf("%v : error decoding result", err))
}

fmt.Println("Simulation result: ", decodedResult)
fmt.Println("Contract call result: ", decodedResult)

/*
* Clean up:
*/
client.Close()

fmt.Println("Example Complete!")

}

// DecodeABIHexString decodes a hex-encoded ABI (Application Binary Interface) string into a UTF-8 string.
// It assumes the input follows the ABI encoding standard for dynamic data.
func DecodeABIHexString(hexStr string) (string, error) {
// Remove the `0x` prefix if present
hexStr = strings.TrimPrefix(hexStr, "0x")

// Ensure the input has at least 128 characters (for metadata and length)
if len(hexStr) < 128 {
return "", fmt.Errorf("input hex string is too short")
}

// Parse the length of the dynamic data (64 to 128) as a hexadecimal integer
lengthHex := hexStr[64:128]
length, err := parseHexToInt(lengthHex)
if err != nil {
return "", fmt.Errorf("failed to parse length from hex string: %w", err)
}

// Calculate the start and end index for the dynamic data
startIndex := 128
endIndex := startIndex + length*2
if len(hexStr) < endIndex {
return "", fmt.Errorf("input hex string is too short for the expected dynamic data")
}

// Extract the dynamic data substring
hexData := hexStr[startIndex:endIndex]

// Convert the hex data to bytes
bytes, err := hex.DecodeString(hexData)
if err != nil {
return "", fmt.Errorf("failed to decode hex data: %w", err)
}

// Convert the bytes to a UTF-8 string and return
return string(bytes), nil
}

// parseHexToInt parses a hexadecimal string into an integer
func parseHexToInt(hexStr string) (int, error) {
if len(hexStr) == 0 {
return 0, fmt.Errorf("empty hex string")
}
parsed, err := hex.DecodeString(hexStr)
if err != nil {
return 0, err
}
// Convert parsed bytes to an integer
result := 0
for _, b := range parsed {
result = result*256 + int(b)
}
return result, nil
}
81 changes: 81 additions & 0 deletions mirror_node_contract_call_query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package hiero

// SPDX-License-Identifier: Apache-2.0

// MirrorNodeContractCallQuery returns a result from EVM transient simulation of read-write operations.
type MirrorNodeContractCallQuery struct {
gsstoykov marked this conversation as resolved.
Show resolved Hide resolved
mirrorNodeContractQuery
}

func NewMirrorNodeContractCallQuery() *MirrorNodeContractCallQuery {
query := new(MirrorNodeContractCallQuery)
return query
}

// SetContractID sets the contract instance to call.
func (query *MirrorNodeContractCallQuery) SetContractID(contractID ContractID) *MirrorNodeContractCallQuery {
query.setContractID(contractID)
return query
}

// SetContractEvmAddress sets the 20-byte EVM address of the contract to call.
func (query *MirrorNodeContractCallQuery) SetContractEvmAddress(contractEvmAddress string) *MirrorNodeContractCallQuery {
query.setContractEvmAddress(contractEvmAddress)
return query
}

// SetSender sets the sender of the transaction simulation.
func (query *MirrorNodeContractCallQuery) SetSender(sender AccountID) *MirrorNodeContractCallQuery {
query.setSender(sender)
return query

Check warning on line 30 in mirror_node_contract_call_query.go

View check run for this annotation

Codecov / codecov/patch

mirror_node_contract_call_query.go#L28-L30

Added lines #L28 - L30 were not covered by tests
}

// SetSenderEvmAddress sets the 20-byte EVM address of the sender of the transaction simulation.
func (query *MirrorNodeContractCallQuery) SetSenderEvmAddress(senderEvmAddress string) *MirrorNodeContractCallQuery {
query.setSenderEvmAddress(senderEvmAddress)
return query
}

// SetFunction sets the function parameters as their raw bytes.
func (query *MirrorNodeContractCallQuery) SetFunction(name string, params *ContractFunctionParameters) *MirrorNodeContractCallQuery {
query.setFunction(name, params)
return query
}

// SetFunction sets the function parameters as their raw bytes.
func (query *MirrorNodeContractCallQuery) SetFunctionParameters(byteArray []byte) *MirrorNodeContractCallQuery {
query.setFunctionParameters(byteArray)
return query
}

// SetValue sets the amount of value (in tinybars or wei) to be sent to the contract in the transaction.
func (query *MirrorNodeContractCallQuery) SetValue(value int64) *MirrorNodeContractCallQuery {
query.setValue(value)
return query
}

// SetGasLimit sets the gas limit for the contract call.
// This specifies the maximum amount of gas that the transaction can consume.
func (query *MirrorNodeContractCallQuery) SetGasLimit(gasLimit int64) *MirrorNodeContractCallQuery {
query.setGasLimit(gasLimit)
return query
}

// SetGasPrice sets the gas price to be used for the contract call.
// This specifies the price of each unit of gas used in the transaction.
func (query *MirrorNodeContractCallQuery) SetGasPrice(gasPrice int64) *MirrorNodeContractCallQuery {
query.setGasPrice(gasPrice)
return query
}

// SetBlockNumber sets the block number for the simulation of the contract call.
// The block number determines the context of the contract call simulation within the blockchain.
func (query *MirrorNodeContractCallQuery) SetBlockNumber(blockNumber int64) *MirrorNodeContractCallQuery {
query.setBlockNumber(blockNumber)
return query
}

// Returns gas estimation for the EVM execution
func (query *MirrorNodeContractCallQuery) Execute(client *Client) (string, error) {
return query.call(client)
}
Loading
Loading