Flow Community Rewards are here. Earn points for engaging in the ecosystem, spend points on prizes. Learn more.
Developers
August 20, 2020
Intro to Flow with Go SDK
Flow
Intro to Flow with Go SDK

Intro to Flow using Flow Go SDK by Community Lead Daniel Podaru

Where to Start

Blockchain terminology can be intimidating at first, and it may result in having to learn many concepts before actually writing code that interacts with the blockchain. In this post, we'll utilize the Flow Go SDK to write code that will interact with Flow.

An intro to Flow

The easiest way to understand Flow is to think of it as a database with super-powers. Multiple copies of the database are running on machines distributed around the planet, all synchronized with each other.

To interact with Flow you will communicate with one of the machines that hold a copy of the database, commonly referred to as a node.

To read the data from Flow, you'll create a program called a script and submit this program to the node. The node will run it and return a result.

To read and write data to Flow, you'll create a program called a transaction, submit it to the node and wait for a response.

Why use scripts if we can do the same thing with transactions?

To execute a transaction the node will require a fee, referred to as the transaction fee.

As transactions change data, and data needs to be validated and kept in sync across all nodes, there is a delay before the result of a transaction is known across the network.

There are no fees to run a script, and as the data is already synced across all nodes, the script results are instant.

How much time do I have to wait for a transaction result?

Transactions are executed in batches, a batch of transactions is referred to as a block. Think of a block as a way to order transactions. It is important to know the order in which transactions are executed because multiple transactions might change the same data.

The delay before the result of a batch of transactions is known as the block time, and is estimated at ~10 seconds. On the Flow Emulator, this delay is set to 5 seconds.

How are transaction fees paid?

Internally Flow has an Account system, conceptually, think of it as a database(or Excel) table. Each row in the table has columns for account address, balance, and code.

The account address is unique, each account having a different address, and it currently looks like this: f4a3472b32eac8d8.

The balance is the number of Flow tokens this account owns and it looks like this 10000. Assuming this is my account and I want to run a transaction, I will need to spend some of the 10000 Flow tokens to pay the transaction fee.

The code field is meant to store a program and is referred to as a smart contract.

How is a Flow account controlled?

Public and private keys are used to control an account.

A public key looks like this: a5f94b68ed62af51c8aa93752117e9df8f9a41363079a06ca10bef0d9d9aaf4e2785327b6c2b8e92ae4e08fa9df6917d71e34de4cdcddbf59628288d2acc8830

A private key looks like this:
3cb6545bd541cdd3ecf47001979662ace4dcda93f27179b85be9aad7b488e0d6.

In terms of security, you can think of your public key as your username and your private key as your password in a classic web account scenario. Public keys can be shared with anyone but private keys must be kept secret. If you lose your private key you lose access to your Flow account. Someone with access to your private key can spend all your tokens and modify all your smart contracts.

How can I get public/private keys?

The Flow SDKs handle the key generation and data signing for you. Keys are generated depending on the signature algorithm used. Flow currently supports the ECDSA (Elliptic Curve Digital Signature Algorithm) signature algorithm. This algorithm is efficient on specific elliptic curves. From these curves, Flow supports the "P256" and "secp256k1" curves.

The public and private keys from above were generated to work with the ECDSA signing algorithm on the P256 curve. The private key from above is actually a large random integer within a specific range, and the public key from above is actually a point expressed by its {x,y} coordinates on the P256 elliptic curve. This is also why the public key is visually twice as long as the private key.

The private key is used to compute the signature of a fixed-length piece of data.

The public key is used to verify that a piece of data was signed with its associated private key, without knowing the private key itself.

Install Go

We will use the Flow Go SDK to interact with Flow.

If you haven't installed the Go programming language yet, you will need to download and install it from the Go website:  https://golang.org/dl/

Once you have installed Go, make sure you have set the GOPATH environment variable pointing to your work-space directory - the folder where you will keep the Go source code you will write.

To check the GOPATH on macOS & Linux, open a terminal and run this command:

echo $GOPATH/Users/daniel/gowork

To check the GOPATH on Windows, open Command Prompt and run this command:

echo %GOPATH%
C:\Users\daniel\gowork>

This work-space directory is called gowork but yours could have a different name.

Install the Flow Go SDK

Use Terminal or Command prompt and type:

go get -u github.com/onflow/flow-go-sdk
go get -u github.com/onflow/cadence

Project directory

Inside your Go work-space folder, you will find a src folder. Navigate inside it and create a new folder for your project.

cd $GOPATH
cd src
mkdir helloflow

Note: If there is no src folder inside your Go work-space, go ahead and create it with mkdir src.

Flow Emulator

For development, we will need to install the Flow Emulator which simulates the complete Flow network running locally on our machine. The emulator comes bundled with the Flow CLI tool.

On macOS & Linux, open the Terminal and run the install script to get the appropriate binary for your system:

sh -ci "$(curl -fsSL https://storage.googleapis.com/flow-cli/install.sh)"

On Windows, open PowerShell and run the install script to download a flow.exe file in your current  directory:

iex "& { $(irm 'https://storage.googleapis.com/flow-cli/install.ps1') }"

Start the emulator from inside the helloflow directory. In Terminal (macOS, Linux) or Command prompt (Windows) type:

cd helloflow
flow emulator start --init

The additional --init argument will tell the emulator to create a flow.json file in the current folder. Once the file is created, you can omit this argument as the emulator will read the data from the flow.json file.

When the emulator starts it will print something like this:

INFO[0000] ⚙️ Using service account 0xf8d6e0586b0a20c7 serviceAddress=f8d6e0586b0a20c7 serviceHashAlgo=SHA3_256 servicePrivKey=0ab0b3c92adf319ab118f6c073003f7029bb6fa8eb986f47f9b139fbb189e655 servicePubKey=163f52e9bf4bf0b9855c0302de2892f3bf3e3c78441c18fce342063b8d05313ad91e335c1e47291e5ca3a83ba92ce2c8abf65499abbbfc6804923009bd01c55c serviceSigAlgo=ECDSA_P256
INFO[0002] 🌱 Starting gRPC server on port 3569... port=3569
INFO[0002] 🌱 Starting HTTP server on port 8080... port=8080

The Flow node is managed by a service account, which can be thought of as an admin account.On startup, the node displays info about the service account:

  • f8d6e0586b0a20c7 is the unique address of this account
  • 0ab0b3c92adf319ab118f6c073003f7029bb6fa8eb986f47f9b139fbb189e655 is the private key used to control this account.
  • 163f52e9bf4bf0b9855c0302de2892f3bf3e3c78441c18fce342063b8d05313ad91e335c1e47291e5ca3a83ba92ce2c8abf65499abbbfc6804923009bd01c55c is the public key used by this account.
  • ECDSA_P256 tells us that the key pair for this account was created according to the ECDSA signing algorithm on the curve P256.
  • SHA3_256 tells us that this public key will use the 256 bits variant of the SHA3 algorithm when verifying signatures. We'll use the same hashing algorithm to make our data fixed-length before signing it.

Start Coding

In this intro, we'll create a Flow account that will hold a smart contract and a script that will call one of the methods declared in the smart contract.

All code that we'll write below can be found here:https://github.com/cybercent/intro-flow-sdk

The Go SDK version used is v0.8.0.

Let's write some code to generate a public and private key pair.

Fire up your favourite IDE and create a new file called account.go inside the helloflow folder.

Add the code below to the account.go file:

package main

// [1]
import (
"crypto/rand"
"encoding/hex"
"fmt"
"github.com/onflow/flow-go-sdk/crypto"
)

// [2]
func GenerateKeys(sigAlgoName string) (string, string) {
seed := make([]byte, crypto.MinSeedLength)
_, err := rand.Read(seed)
if err != nil {
panic(err)
}

// [3]
sigAlgo := crypto.StringToSignatureAlgorithm(sigAlgoName)
privateKey, err := crypto.GeneratePrivateKey(sigAlgo, seed)
if err != nil {
panic(err)
}

// [4]
publicKey := privateKey.PublicKey()

pubKeyHex := hex.EncodeToString(publicKey.Encode())
privKeyHex := hex.EncodeToString(privateKey.Encode())

return pubKeyHex, privKeyHex
}

// [5]
func main() {
pubKey, privKey := GenerateKeys("ECDSA_P256")
fmt.Println(pubKey)
fmt.Println(privKey)
}

From Terminal / Command prompt run the program:

go run account.go

92e8566fdd5ee25aae2e08e1d8b5ca56e1fb84c8303e05c9e1fbfc8c36a52a90394353235306937dc9c0e3ef357626d24eba0e15f4e9a0a15bc9b2262c1bce69

3e42822b2a3764beee464cdcc1c03fba1db96e43b41be6e5e9b38c64d2987aae

In the output, the first line is the public key, the second line is the private key.

The steps we took in the program:

[1] Import the external libraries that we will use in the program.

[2] Define a new function called GenerateKeys. It will take as argument the name of a signing algorithm and it will return a public key and private key

[3] Generate a random seed from which we will generate the private key to be used with the signing algorithm.

[4] Get the public key associated with this private key.

[5] In the main function we invoke the key generation function and tell it that the keys we want should be generated to work with the ECDSA signing algorithm on the curve P256.

Next, let's add a function that will create a Flow account that can be controlled by the new key pair we just created.

The content of the  account.go file is:

package main

// [1]
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"github.com/onflow/flow-go-sdk"
"github.com/onflow/flow-go-sdk/client"
"github.com/onflow/flow-go-sdk/crypto"
"github.com/onflow/flow-go-sdk/templates"
"google.golang.org/grpc"
"strings"
"time"
)

// [2]
func GenerateKeys(sigAlgoName string) (string, string) {
// Code has not changed here.
}

// [3]
func CreateAccount(node string,
publicKeyHex string,
sigAlgoName string,
hashAlgoName string,
code string,
serviceAddressHex string,
servicePrivKeyHex string,
serviceSigAlgoName string,
gasLimit uint64) string {

ctx := context.Background()

sigAlgo := crypto.StringToSignatureAlgorithm(sigAlgoName)
publicKey, err := crypto.DecodePublicKeyHex(sigAlgo, publicKeyHex)
if err != nil {
panic(err)
}

hashAlgo := crypto.StringToHashAlgorithm(hashAlgoName)

 // [4]
accountKey := flow.NewAccountKey().
SetPublicKey(publicKey).
SetSigAlgo(sigAlgo).
SetHashAlgo(hashAlgo).
SetWeight(flow.AccountKeyWeightThreshold)

// [5]
accountCode := []byte(nil)
if strings.TrimSpace(code) != "" {
accountCode = []byte(code)
}

 // [6]
c, err := client.New(node, grpc.WithInsecure())
if err != nil {
panic("failed to connect to node")
}

serviceSigAlgo := crypto.StringToSignatureAlgorithm(serviceSigAlgoName)
servicePrivKey, err := crypto.DecodePrivateKeyHex(serviceSigAlgo, servicePrivKeyHex)
if err != nil {
panic(err)
}

serviceAddress := flow.HexToAddress(serviceAddressHex)
serviceAccount, err := c.GetAccountAtLatestBlock(ctx, serviceAddress)
if err != nil {
panic(err)
}

// [7]
serviceAccountKey := serviceAccount.Keys[0]
serviceSigner := crypto.NewInMemorySigner(servicePrivKey, serviceAccountKey.HashAlgo)

 // [8]
tx := templates.CreateAccount([]*flow.AccountKey{accountKey}, accountCode, serviceAddress)
tx.SetProposalKey(serviceAddress, serviceAccountKey.ID, serviceAccountKey.SequenceNumber)
tx.SetPayer(serviceAddress)
tx.SetGasLimit(uint64(gasLimit))

err = tx.SignEnvelope(serviceAddress, serviceAccountKey.ID, serviceSigner)
if err != nil {
panic(err)
}

 // [9]
err = c.SendTransaction(ctx, *tx)
if err != nil {
panic(err)
}

 // [10]
return tx.ID().String()
}

[1] Import libraries, including the flow client and flow transaction templates.

[2] The GenerateKeys function stays unchanged.

[3] Declare the CreateAccount function that takes as arguments:

  • node the IP and port of the node we will connect to, in our case this is the Emulator
  • publicKeyHex the hexadecimal representation of our public key
  • sigAlgoName the name of the signature algorithm used when we generated our keys
  • hashAlgoName the name of the hashing algorithm that we want this public key to use when verifying signatures.
  • code the smart contract source code that we want to publish under our account.
  • serviceAddressHex  the service address in hexadecimal format
  • servicePrivKeyHex the service private key in hexadecimal format
  • serviceSigAlgoName the name of the signature algorithm that was used when generating the service keys
  • gasLimit the maximum amount of Flow tokens that will be spent in transaction fees to create this account.

Why do we need to include info related to the service account ?

To create a new account, we need to write new data to Flow so we will use a transaction.

Account creation isn't free, which means that an existing account needs to pay to create a new account. Luckily the service account can do this for us.

The service account will authorize the payment by signing the transaction, which is why we included its private key.

[4]  Create a new account key that will be used to control the account that we will soon create. The account key needs to know the public key, the hash algorithm, and the signing algorithm that will be used when signing data using the private key for this account.

Also, note that we set a weight for our account key. The weight parameter indicates if this account key can unlock the account by itself or multiple account keys are needed. Think of it as a door with 1 lock, or 2 locks, or many - you will need as many keys as locks to open it.

[5] Check that the source code of the smart contract we want to add to our account is not composed of only empty spaces and we transform it into bytes.

[6] Connect to the Flow node, in our case the Emulator, and request info about the account key used by the service account c.GetAccountAtLatestBlock(ctx, serviceAddress). The reason we're doing this is that we need the latest sequenceNumber the account key has.

A sequenceNumber is a self-incrementing number stored alongside each account key.When a user signs a transaction the sequence number for that account key needs to match the one that's stored on Flow. Each time a transaction is sent to Flow the sequence number is incremented, independently of the transaction result.

[7] Create a new signer from the service account private key and hash algorithm.

[8] Use a transaction to create the account. The Cadence source code for this transaction is already declared in one of the SDK templates so we will use it.

Next, we set the proposal account key - the account key that initiated this transaction - in our case the account key that will manage the account we will create.

After we set the address of the account from which's balance the transaction fees will be taken.

Finally, we set the maximum number of Flow tokens that the payer is willing to spend on this transaction.

The transaction envelope is signed by the service account. The envelope is constructed by the SDK behind the scenes, and it consists of joining all the transaction data that needs to be signed.

Why is it called an envelope?

The process of encrypting a key with another key is called envelope encryption.

In our transaction, there is only one signer, the payer. In other use-cases there are multiple signers, each signing the transaction data. Their signatures are included when creating the envelope that will be signed by the payer. The payer always signs last.

[9] Send the transaction to Flow.

[10] As discussed above, before the transaction is executed and its result is known, there is a delay known as the block time, so we only return the transaction ID here.

Next, we will create a function that will take the transaction ID as an argument and return the address of the account that was created in that transaction.

Add this function to the account.go file.

// [11]
func GetAddress(node string, txIDHex string) string {
ctx := context.Background()
c, err := client.New(node, grpc.WithInsecure())
if err != nil {
panic("failed to connect to node")
}

// [12]
txID := flow.HexToID(txIDHex)
result, err := c.GetTransactionResult(ctx, txID)
if err != nil {
panic("failed to get transaction result")
}

// [13]
var address flow.Address

if result.Status == flow.TransactionStatusSealed {
for _, event := range result.Events {
if event.Type == flow.EventAccountCreated {
accountCreatedEvent := flow.AccountCreatedEvent(event)
address = accountCreatedEvent.Address()
}
}
}

 // [14]
return address.Hex()
}

[11] Declare the GetAddress function. Next, we connect to the node.

[12] Ask the node the result of the transaction with the given ID.

[13] A transaction may transition through different states:  unknown, pending, finalized executed, and sealed. We're interested if our transaction has been sealed - meaning the modifications we made took place and they can't be reverted. Next, we iterate through the transaction events and extract the account address from the account created event and return it as a hexadecimal string.

Next, we'll call the functions we created from the main function.

In account.go change the main function.

func main() {
pubKey, privKey := GenerateKeys("ECDSA_P256")
fmt.Println(pubKey)
fmt.Println(privKey)

 // [15]
node := "127.0.0.1:3569"

sigAlgoName := "ECDSA_P256"
hashAlgoName := "SHA3_256"
code := `
pub contract HelloWorld {

pub let greeting: String

init() {
   self.greeting = "Hello, World!"
}

pub fun hello(): String {
   return self.greeting
}
}
`

serviceAddressHex := "f8d6e0586b0a20c7"
servicePrivKeyHex := "0ab0b3c92adf319ab118f6c073003f7029bb6fa8eb986f47f9b139fbb189e655"
serviceSigAlgoHex := "ECDSA_P256"

// [16]
gasLimit := uint64(100)

// [17]
txID := CreateAccount(node, pubKey, sigAlgoName, hashAlgoName, code, serviceAddressHex, servicePrivKeyHex, serviceSigAlgoHex, gasLimit)

fmt.Println(txID)

 // [18]
blockTime := 10 * time.Second
time.Sleep(blockTime)

// [19]
address := GetAddress(node, txID)
fmt.Println(address)
}

[15] Set the node address to point to the emulator IP and port. We set the hashing and signing algorithm names to the ones we used when we generated our key pair.

Under our account code, we'll store a hello world smart-contract written in Cadence.

We set the service address, private key, and signing algorithms to match the ones displayed by the emulator for the service account - these are also available in the flow.json file.

[16] Set the gas limit (the maximum amount of Flow tokens to be spent on this transaction) to 100.

[17] Call the CreateAccount function we created above and expect it to return a transaction id.[18] The transaction result will not be available instantly so we tell our program to sleep for 10 seconds.

[19] Finally we call the GetAddress function that will return the address of the newly created account.

From Terminal / Command prompt, run the program:

go run account.go

a1efeb1ef68f941b4abcb5c69175b37a24b756fdd35af3f1ba9d9fd158840a14e9a3ed77d91cf4138d0c995d327ca0cda2ee973da06f8301df366ed107f12135

2b1ebf34cb2c5070333806e9454d1d461c73deadb8a0d525e9c7be126d983e6e

0aefd29b8b1ca662dbb375d7cdb79130208588579a2950a88c89078e6c672f6d

179b6b1cb6755e31

In order, we'll have a new public key, a new private key, the transaction id, and the address of the newly created account 179b6b1cb6755e31

Each time you run the program you'll generate a new key pair. You could also, generate the key pair once and use it to control multiple accounts.

Next, we'll interact with the contract that we published under this account by using a script.

We'll create a function that will execute any script, and pass as an argument to this function the Candence script we want to use to interact with our smart contract.

Create a new file hello.go in the same folder

package main

// [1]
import (
"context"
"fmt"
"github.com/onflow/flow-go-sdk/client"
"google.golang.org/grpc"
)

// [2]
func ExecuteScript(node string, script []byte) {
ctx := context.Background()
c, err := client.New(node, grpc.WithInsecure())
if err != nil {
panic("failed to connect to node")
}

// [3]
result, err := c.ExecuteScriptAtLatestBlock(ctx, script, nil)
if err != nil {
panic(err)
}

fmt.Println(result)
}

// [4]
func main() {
node := "127.0.0.1:3569"
script := `
import HelloWorld from 0x179b6b1cb6755e31

pub fun main(): String {
   return HelloWorld.hello()
}
`
ExecuteScript(node, []byte(script))
}

[1] Import the external libraries that we'll use in this program.

[2] Declare the ExecuteScript function that takes as arguments the IP and port of the node and the Cadence script.

[3] Send the script to the node to be executed, we instantly get a result that we return.

[4] In the main function, we set the node to be the emulator IP and port.

Next, create a script that will import the HelloWorld smart contract found in the account that we created. 179b6b1cb6755e31 is the account address of the account I created on my machine. The one you created will be different, so you'll need to use that address instead. Also, note that the account address is prefixed by 0x when used in the import declaration in Cadence 0x179b6b1cb6755e31. The 0x tells Cadence that this is a number represented in hexadecimal (base 16).

Finally we call the ExecuteScript function.

From Terminal / Command prompt  run the program:

go run hello.go

Hello, World!

The result comes from the execution of the hello function inside the HelloWorld contract.

Congratulations!

You should now have a better understanding of Flow. Here's a recap of what we covered:

  • How to create a Flow account using the Flow Go SDK.
  • How public and private keys are used in Flow to secure an account.
  • How to create transactions and scripts and what their differences are.