This article presents all the steps required to deploy and use a contract using Go.
Few other posts about this exist:
- The official Ethereum documenation, which lacks the full steps required.
- The Create DApp in Go with Geth, which is pretty good, but still does not cover the build steps, and the transactions validation.
So, I've decided creating this article to cover all the steps, hoping you will find it useful.
The article is setup for a private Ethereum network, but can be also used for the public Ethereum network.
The article is setup for a private Ethereum network, but can be also used for the public Ethereum network.
We will review the following steps:
- Create a Smart Contract in Solidity
- Manually Compile the Contract to Go
- Create a Go Application
- Create a 3 stages Dockerfile to compile the contract and the application
1. Create Smart Contract in Solidity
Contracts are created using solidity. In this article, a simple contract with a call method and a transaction method. See this for explanation about the difference between a call and a transaction.
pragma solidity ^0.5.8; contract Price { uint price = 100; function setPrice(uint newPrice) external payable { price = newPrice; } function getPrice() external view returns (uint) { return price; } }
The contract contains the following methods:
- setPrice: a transaction method
- getPrice: a call method
2. Manually Compile the Contract to Go
To use a contract in Go, we will need to generate a Go file for it.
First install the solc: the solidity compiler as explained here.
Next, install abigen, which is part of the geth-tools. The geth tools can be downloaded from the geth download site. For example, the current version is:
https://gethstore.blob.core.windows.net/builds/geth-alltools-linux-amd64-1.9.9-01744997.tar.gz
First install the solc: the solidity compiler as explained here.
Next, install abigen, which is part of the geth-tools. The geth tools can be downloaded from the geth download site. For example, the current version is:
https://gethstore.blob.core.windows.net/builds/geth-alltools-linux-amd64-1.9.9-01744997.tar.gz
Now we can generate the go contract using the following commands:
Copy the Price.go to the a folder named "generated" in the go application source folder.
In later stage we will automate this step as part of a Docker build.
The go application should handle the following:
To use this, update the private key to your owner used Ethereum account private key.
This account must have enough ETH balance to pay for the contract deploy.
This code also includes a call to a track(transaction) function.
We will review transaction tracking later.
Once the contract is deployed we get an address of the contract. This address should be saved out of the scope of the Ethereum, so that it can be reused by other remote/distributed clients.
Notice that the Deploy function returns an instance of the contract, and so we can use it to run the contract method. However I prefer not to use it, and instead to show how to load the contract using the contract address, since you will probably need this.
Using a contract "call" method is very simple:
Using a "transaction" method is similar:
This code also includes a call to a track(transaction) function.
So, why do we need to track the transaction?
The transaction might fail. It also might not be mined at all due to low gas price.
If we want to know that the transaction is success, we need to check the status of it whenever a new block is mined.
This code section tracks the transaction:
solc --abi --output-dir Price.sol solc --bin --output-dir Price.sol abigen --bin Price.bin --abi Price.abi --pkg=price --out=Price.go
Copy the Price.go to the a folder named "generated" in the go application source folder.
In later stage we will automate this step as part of a Docker build.
3. Create a Go Application
- Connect to the Ethereum network
- Deploy the contract
- Load the contract
- Use the contract
To connect to the Ethereum network, we use the following:
url:= "ws://THE_ETHEREUM_NETWORK_IP_ADDRESS" timedContext, _ := context.WithTimeout(context.Background(), 30*time.Second) client, err := ethclient.DialContext(timedContext, url) if err == nil { log.Fatalf("connection failed %v", err) return }
Notice that we use a 30 seconds timeout based connection. Choose whatever timeout that fit your need. Also, replace the URL with your Ethereum network address.
Notice that once all Ethereum actions are done, the ethclient should be close, so eventually call to:
To deploy the contract, we use the Price.go that we've generate before.
client.Close()
To deploy the contract, we use the Price.go that we've generate before.
import ( "crypto/ecdsa" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" contract "sample.com/contract/generated" ) func getAccountPrivateKey() *ecdsa.PrivateKey { // replace the private key here accountPrivateKey := "211dbaa6ca5e3fe1141eef3b00a0dd6d630a8d8e5bfbb7a7516865f1c746a3a0" privateKey, err := crypto.HexToECDSA(accountPrivateKey) if err != nil { log.Fatalf("private key to ECDSA failed: %v", err) } return privateKey } func getAccountPublicKey() common.Address { publicKey := getAccountPrivateKey().Public() publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) if !ok { log.Fatalf("cannot assert type: publicKey is not of type *ecdsa.PublicKey") } address := crypto.PubkeyToAddress(*publicKeyECDSA) return address } func getTransactionOptions(client *ethclient.Client) *bind.TransactOpts { nonce, err := client.PendingNonceAt(context.Background(), getAccountPublicKey()) if err != nil { log.Fatalf("get pending failed: %v", err) } gasPrice, err := client.SuggestGasPrice(context.Background()) if err != nil { log.Fatalf("suggest gas price failed: %v", err) } transactOpts := bind.NewKeyedTransactor(getSystemAccountPrivateKey()) transactOpts.Nonce = big.NewInt(int64(nonce)) transactOpts.Value = big.NewInt(0) // you might need to update gas price if you have extremly large contract transactOpts.GasLimit = uint64(3000000) transactOpts.GasPrice = gasPrice return transactOpts } func Deploy(client *ethclient.Client) string { transactOptions := getTransactionOptions(client) address, transaction, _, err := contract.DeployPriceContract(transactOpts, client) if err != nil { log.Fatalf("deploy contract failed: %v", err) } track(transaction) return address.Hex() }
To use this, update the private key to your owner used Ethereum account private key.
This account must have enough ETH balance to pay for the contract deploy.
This code also includes a call to a track(transaction) function.
We will review transaction tracking later.
Once the contract is deployed we get an address of the contract. This address should be saved out of the scope of the Ethereum, so that it can be reused by other remote/distributed clients.
Notice that the Deploy function returns an instance of the contract, and so we can use it to run the contract method. However I prefer not to use it, and instead to show how to load the contract using the contract address, since you will probably need this.
func LoadContract(client *ethclient.Client, contractAddress string) *contract.PriceContract { address := common.HexToAddress(contractAddress) instance, err := contract.NewPriceContract(address, client) if err != nil { log.Fatalf("could not load contract: %v", err) } return instance }
Using a contract "call" method is very simple:
callOptions := bind.CallOpts{From: getSystemAccountPublicKey()} price, err := contract.GetPrice(&callOptions) if err != nil { log.Fatalf("get price failed: %v", err) }
Using a "transaction" method is similar:
transactionOptions := getTransactionOptions(client) transaction, err := contract.SetPrice(transactionOptions, big.NewInt(777)) if err != nil { Fatal("set price failed: %v", err) } track(transaction)
This code also includes a call to a track(transaction) function.
So, why do we need to track the transaction?
The transaction might fail. It also might not be mined at all due to low gas price.
If we want to know that the transaction is success, we need to check the status of it whenever a new block is mined.
This code section tracks the transaction:
func track(transaction *types.Transaction) { headers = make(chan *types.Header) var err error subscription, err = client.SubscribeNewHead(context.Background(), headers) if err != nil { log.Fatalf("subscribe to read blocks failed: %v", err) } defer subscription.Unsubscribe() for { select { case err := <-subscription.Err(): log.Fatalf("subscription failed: %v", err) case <-headers: // got block, checking transaction transactionLocated := checkTransactionOnce(transaction) if transactionLocated { return } case <-time.After(60*time.Second): log.Fatalf("timeout waiting for transaction") } } } func (wrapper *EthereumTransactionWrapper) checkTransactionOnce(transaction *types.Transaction) bool { _, pending, err := Client.TransactionByHash(context.Background(), transaction.Hash()) if err != nil { log.Fatalf("get transaction failed: %v", err) } if pending { return false } receipt, err := client.TransactionReceipt(context.Background(), transaction.Hash()) if err != nil { log.Fatalf("transaction receipt failed: %v", err) } if receipt.Status == 1 { return true } log.Fatalf("transaction failed with logs %v", receipt.Logs) // dead code return true }
4. Create a Dockerfile
The docker file is a multi-stage docker file.
You might want to review: Use cache in a docker multi stage build for faster builds.
The Dockerfile includes 3 steps:
The Dockerfile includes 3 steps:
- compile the contracts
- compile the go application
- package the compiled go application
#================== # Stage1: contracts #================== FROM ubuntu:18.04 as contracts-compiler # install wget RUN apt-get update && \ apt-get install -y software-properties-common wget && \ rm -rf /var/lib/apt/lists/* # install solc RUN add-apt-repository ppa:ethereum/ethereum && \ apt-get update && \ apt-get install -y solc # install geth tools RUN mkdir /geth_extract ARG SOLIDITY_ALL_TOOLS RUN wget --progress=dot:giga https://gethstore.blob.core.windows.net/builds/${SOLIDITY_ALL_TOOLS} -O /geth_extract/tools.tar.gz RUN tar xvzf /geth_extract/tools.tar.gz -C /geth_extract RUN rm /geth_extract/tools.tar.gz RUN mv /geth_extract/* /geth_extract/extracted RUN mv -v /geth_extract/extracted/* /usr/local/bin RUN rm -rf /geth_extract ADD ./src/PriceContract.sol /contracts/PriceContract.sol WORKDIR /contracts RUN solc --abi --output-dir /compiled PriceContract.sol RUN solc --bin --output-dir /compiled PriceContract.sol WORKDIR /compiled RUN abigen --bin PriceContract.bin --abi PriceContract.abi --pkg=bouncer_contract --out=PriceContract.go #================ # Stage2: compile #================ FROM golang:1.12 AS go-compiler RUN apt-get update && \ apt-get install -y git WORKDIR /src ENV GOPATH=/go ENV GOBIN=/go/bin # get dependencies COPY ["./src/go.mod", "./src/go.sum", "/src/"] RUN GOARCH=amd64 GOOS=linux CGO_ENABLED=0 go mod download # compile source ADD ./src /src # copy generated contract RUN mkdir -p /src/internal/generated COPY --from=contracts-compiler /compiled/PriceContract.go /src/generated/PriceContract.go RUN GOARCH=amd64 GOOS=linux CGO_ENABLED=0 go build -a -installsuffix cgo -o my-go-application #================ # Stage3: package #================ FROM ubuntu:18.04 COPY --from=go-compiler /src/my-go-application /my-go-application WORKDIR / ENTRYPOINT ["/my-go-application"]
Summary
In this article we have reviewed how to use Ethereum contract in a Go based application, include deploy of the contract, using the contract methods, and building the application.
We've included a method of tracking the contract transactions.
We've included a method of tracking the contract transactions.
No comments:
Post a Comment