Sending Transactions on GETH's Simulated Backend - Kauri
Articles
Communities
Collections
Write Article
Create Collection
Import from medium
Loading...
Sign in
POSTED 01 Oct 2019 20:10

Sending Transactions on GETH's Simulated Backend

ethereum
geth
testing
AUTHOR
Dave Appleton

Now that we have a contract deployed, we want to interact with it.

In the last session there was some interaction without any explanation. This time we will go a bit deeper.

As you should be aware, there are several types of interaction with the ethereum blockchain (not including mining).

The main ones are

  1. Sending a Transaction
  2. Reading the state of the blockchain or a transaction
  3. Requesting Data about the current state of a contract
  4. Viewing events that were generated by interactions with a contract

Sending a transaction

Sending a transaction involves your client to add the transaction to its transaction pool and propagate it to its peers so that it can be be included in a block. This requires the transaction to be mined and the entire network to reach a state of consensus.

This can take time so you cannot expect an immediate reply as to whether your transaction has succeeded or failed.

Transactions must be signed by the originator of the transaction and must be paid for (in gas).

All of these operations involve sending transactions

  1. Sending ETH to another address
  2. Deploying a contract
  3. Calling a function or method in a contract

Sending ETH

Sending any transaction requires that we form a transaction and get it signed before submitting it.

A transaction can be created by the NewTransaction function found in the GETH repo under /core/types

func NewTransaction(nonce uint64, to common.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, data []byte) *Transaction

Let's go through these

nonce : a serial number tied to the sending account. This is used to ensure that no transaction can be processed twice. Transactions from an account are processed in order of the nonce. Even if you submit Tx #4 before Tx #3, it will not be processed until #3 has been processed.

You can obtain the nonce using client.PendingNonceAt from the client library that you are using e.g. ethclient.

amount : the amount of ETH (in wei) that want to transfer. This can be ZERO if you are calling a transaction or merely signalling that you control the keys to an address.

data : when sending ETH to a non contract address this is rarely used (it can be used to convey information). When deploying or talking to a contract it contains the contract's byte code or a function signature and parameters

gasLimit : As you should be aware, fees (paid in eth) are termed gas. The amount of gas to be paid depends on the amount of work to be done and the amount of storage used. client.EstimateGas can help you estimate how much gas needs to be paid for the transaction. If you are sending ETH to a normal address (not a contract) the amount of gas is currently fixed at 2100 but there is no guarantee that this will always be the case. If you set the gas limit too high, you will normally get a refund for unused gas.

gasPrice : Your transaction needs to get mined in order to have any effect. When the network is busy, everybody is competing to get their transaction mined - so they offer more incentive to the miners in terms of gas price which is rated in GWei (1 million Wei). In general, transactions with higher gas prices tend to get mined first but there is no guarantee.

The cost of the transaction can be calculated as gasLimit x gasPrice So, a transaction sent with a gasLimit of 200,000 and a gasPrice of 10 GWei could cost up to 200,000 x 10,000,000,000 Wei = 2 x 10^15 Wei = 0.002 ether

You can get current average gas prices from https://ethgasstation.io the transaction cost calculator is at https://ethgasstation.info/calculatorTxV.php

Now you have a transaction, you need to sign it. For the simulated back end we will use the homestead signer. For mainnet you will probably need to use an EIP155Signer.

  senderKey,_ := memorykeys.GetPrivateKey()
  s := types.HomesteadSigner{}
  tx, err := types.SignTx(t, s, senderKey
  if err != nil {
    return nil, err
  }

Now you have a signed transaction, you need to submit it to the network

  err = client.SendTransaction(context.Background(), tx)

You now need to wait for the transaction to be mined - or in the simulated backend you call client.Commit()

On the simulatedBackend you can simply call client.Commit()

On the main / test nets you need to either wait for it to be mined using bind.WaitMined(context, client, transaction) or check the transaction receipt yourself until it shows that it has been executed.

Deploying a contract

In a previous post we have covered deploying a contract using ABIGEN.

  auctionAddress, tx, auctionContract, err := contracts.DeployAuction(bankTx, client, startBids, endBids, startReveal, endReveal, minimumBid, *wallet)

We could deploy a contract by compiling it using solc, getting the deploycode then using that as the data in a transaction but the wrapped contract makes it a lot easier.

Interacting with the contract

As you should know, there are two ways to interact with a contract

  1. Enquiring the state of the contract
  2. Attempting to interact with the contract in a way that changes the state of the contract or the blockchain

Since we know that each fully synced node contains an exact complete copy of the EVM state, we can ask ANY node to tell us the state of a contract. Most enquiries do not need to know who sent the enquiry so the request usually need not be signed.

Changing that state, on the other hand, requires that the transaction be transmitted to the network and a consensus be reached. This requires a signed transaction, gas and takes an uncertain amount of time.

Both, however, can be easily handled by ABIGEN which creates wrappers for all functions.

Making a request

Most of the time that you request information from a contract, the function you are calling will not require any information from the transaction (sender, amount, gasPrice, gasLimit) in which case ABIGEN allows you to use nil as the transaction operator.

min, err := auctionContract.MinimumBid(nil)

If the function required the sender's address you need to supply a bind.CallOpts structure

Changing the state

We have already submitted one transaction that changes the state when we deployed the contract. Let's look at that in a bit more detail.

We used the memorykeys module to get a transactor

bankTx, _ := memorykeys.GetTransactor("banker")

The GetTransactor function in memorykeys uses a function in the bind module NewKeyedTransactor to create a transactor that can be used to sign transactions

func GetTransactor(keyName string) (*bind.TransactOpts, error) {
    key, err := GetPrivateKey(keyName)
    if err != nil {
        return nil, err
    }
    return bind.NewKeyedTransactor(key), nil
}

This can then be used to deploy a contract (as seen before) or send transactions to it.

So in the continued context of the Devcon5 Auction Contract, first we make a call to get the hash of our chosen phrase

    hash, err := auctionContract.CalculateHash(nil, minimumBid, []byte("mary had a little lamb"))
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }

Then we use that hash and send our bid (assuming that the account user01 has been funded with ETH)

    userTx, _ := memorykeys.GetTransactor("user01")
    userTx.Value = minimumBid
    tx, err = auctionContract.BiddingTime(userTx, hash)
    if err != nil {
        fmt.Println("bid : ", err)
        os.Exit(1)
    }
    client.Commit()

SendEth code

func sendEth(ec bind.ContractTransactor, sender string, recipient string, amount *big.Int, gasLimit uint64) (*types.Transaction, error) {
  senderKey, _ := memorykeys.GetPrivateKey(sender)
  senderAddress, _ := memorykeys.GetAddress(sender)
  recipientAddress, _ := memorykeys.GetAddress(recipient)
  nonce, err := ec.PendingNonceAt(context.Background(), *senderAddress)
  if err != nil {
    return nil, err
  }
  gasPrice, err := ec.SuggestGasPrice(context.Background())
  if err != nil {
    return nil, err
  }
  s := types.HomesteadSigner{}
  data := common.FromHex("0x")
  t := types.NewTransaction(nonce, *recipientAddress, amount, gasLimit, gasPrice, data)
  tx, err := types.SignTx(t, s, senderKey)
  if err != nil {
    return nil, err
  }
  err = ec.SendTransaction(context.Background(), tx)
  return tx, err
}

Main code

func main() {
  client, err := getClient()
  if err != nil {
    log.Fatal(err)
  }
  startBids := getTime("13/09/19")
  endBids := getTime("15/09/19")
  startReveal := endBids
  endReveal := getTime("17/09/19")
  minimumBid, _ := etherUtils.StrToEther("4.7")
  wallet, _ := memorykeys.GetAddress("wallet")
  bankTx, _ := memorykeys.GetTransactor("banker")
  txes := make(map[string]*types.Transaction)
  for user := 0; user < 100; user++ {
    userID := fmt.Sprintf("user%02d", user)
    txes[userID], _ = sendEth(client, "banker", userID, new(big.Int).Add(minimumBid, etherUtils.OneEther()), 21000)
  }
  auctionAddress, tx, auctionContract, err := contracts.DeployAuction(bankTx, client, startBids, endBids, startReveal, endReveal, minimumBid, *wallet)
  chkerr(err)
  fmt.Println(auctionAddress.Hex(), tx.Hash().Hex())
  client.Commit()
  min, err := auctionContract.MinimumBid(nil)
  chkerr(err)
  fmt.Println("minumum bid is ", etherUtils.EtherToStr(min))
  fmt.Println("time :", currentTime())
  isBiddingOpen(auctionContract)
  jumpTo(startBids)
  fmt.Println("time :", currentTime())
  isBiddingOpen(auctionContract)
  userTx, _ := memorykeys.GetTransactor("user01")
  hash, err := auctionContract.CalculateHash(nil, minimumBid, []byte("mary had a little lamb"))
  if err != nil {
    fmt.Println(err)
    os.Exit(1)
  }
  userTx.Value = minimumBid
  tx, err = auctionContract.BiddingTime(userTx, hash)
  if err != nil {
    fmt.Println("bid : ", err)
    os.Exit(1)
  }
  client.Commit()
  contractBalance, _ := client.BalanceAt(context.Background(), auctionAddress, nil)
  userAddress, _ := memorykeys.GetAddress("user01")
  userBalance, _ := client.BalanceAt(context.Background(), *userAddress, nil)
  fmt.Println("User Balance ", etherUtils.EtherToStr(userBalance))
  fmt.Println("Auction Balance ", etherUtils.EtherToStr(contractBalance))
}

You can find the code and articles in github at

https://github.io/DaveAppleton/testEnvArticle

Outline
  • Sending a transaction