Sending Transactions on GETH's Simulated Backend
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
- Sending a Transaction
- Reading the state of the blockchain or a transaction
- Requesting Data about the current state of a contract
- 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
- Sending ETH to another address
- Deploying a contract
- 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
- Enquiring the state of the contract
- 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
- Kauri original title: Sending Transactions on GETH's Simulated Backend
- Kauri original link: https://kauri.io/sending-transactions-on-geths-simulated-backend/282ffccc00104339823222f239d949ee/a
- Kauri original author: Dave Appleton (@daveappleton)
- Kauri original Publication date: 2019-10-01
- Kauri original tags: ethereum, geth, testing
- Kauri original hash: QmTmmYK6DdTkpbBtgv5HjCVFkfK8XDnctAXW12Z8vQR8Yd
- Kauri original checkpoint: QmYRYAA1TRyDiXS6uLXdt6qS8AnW63tqJHYpUQKrdyNz7h