Flowkit Go Tutorial: Working with Flow Project State
Introduction
Flowkit is a Go package for interacting with the Flow blockchain in the context of flow.json configuration files. It provides APIs for managing Flow projects, including:
- Loading and managing project configuration (
flow.json) - Resolving import statements in Cadence contracts, scripts, and transactions
- Deploying contracts to different networks (emulator, testnet, mainnet)
- Managing accounts, networks, and deployments
- Executing scripts and building transactions with proper import resolution
Flowkit is the core package used by the Flow CLI and can be integrated into any Go application that needs to interact with Flow projects.
Installation
Prerequisites
- Go 1.25.0 or higher
- A Flow project with a
flow.jsonconfiguration file
Install the Package
Add Flowkit to your Go module:
_10go get github.com/onflow/flowkit/v2
This will install Flowkit v2 and all its dependencies.
Import in Your Code
_10import (_10 "github.com/onflow/flowkit/v2"_10 "github.com/onflow/flowkit/v2/config"_10 "github.com/onflow/flowkit/v2/project"_10 "github.com/spf13/afero"_10)
Loading Project State
The first step when working with Flowkit is loading your project's state from flow.json.
Basic Usage
_18package main_18_18import (_18 "log"_18 "github.com/onflow/flowkit/v2"_18 "github.com/spf13/afero"_18)_18_18func main() {_18 // Load flow.json from the current directory_18 state, err := flowkit.Load([]string{"flow.json"}, afero.Afero{Fs: afero.NewOsFs()})_18 if err != nil {_18 log.Fatalf("Failed to load project state: %v", err)_18 }_18_18 // Now you can work with the project state_18 log.Println("Project state loaded successfully!")_18}
Creating a New Project State
If you need to create a new project from scratch:
_10// Initialize an empty state_10state, err := flowkit.Init(afero.Afero{Fs: afero.NewOsFs()})_10if err != nil {_10 log.Fatalf("Failed to initialize state: %v", err)_10}
Accessing State Components
The State object provides access to all project configuration:
_14// Get all contracts_14contracts := state.Contracts()_14_14// Get all networks_14networks := state.Networks()_14_14// Get all accounts_14accounts := state.Accounts()_14_14// Get all deployments_14deployments := state.Deployments()_14_14// Get the underlying config_14config := state.Config()
Working with Contracts
Contracts are Cadence smart contracts defined in your project.
Getting Contract Information
_13// Get a contract by name_13contract, err := state.Contracts().ByName("MyContract")_13if err != nil {_13 log.Fatalf("Contract not found: %v", err)_13}_13_13log.Printf("Contract: %s\n", contract.Name)_13log.Printf("Location: %s\n", contract.Location)_13_13// Get all contract names_13for _, c := range *state.Contracts() {_13 log.Printf("Available contract: %s\n", c.Name)_13}
Getting Deployment Contracts for a Network
When deploying or executing code, you often need contracts with their target addresses:
_16import "github.com/onflow/flowkit/v2/config"_16_16// Get all contracts configured for deployment on testnet_16contracts, err := state.DeploymentContractsByNetwork(config.TestnetNetwork)_16if err != nil {_16 log.Fatalf("Failed to get deployment contracts: %v", err)_16}_16_16// Each contract includes deployment information_16for _, contract := range contracts {_16 log.Printf("Contract: %s\n", contract.Name)_16 log.Printf(" Location: %s\n", contract.Location())_16 log.Printf(" Target Account: %s\n", contract.AccountAddress)_16 log.Printf(" Account Name: %s\n", contract.AccountName)_16 log.Printf(" Code Size: %d bytes\n", len(contract.Code()))_16}
Working with Networks
Flowkit supports multiple networks including emulator, testnet, and mainnet.
Available Networks
_10import "github.com/onflow/flowkit/v2/config"_10_10// Predefined networks_10emulator := config.EmulatorNetwork // Local emulator_10testnet := config.TestnetNetwork // Flow testnet_10mainnet := config.MainnetNetwork // Flow mainnet_10_10log.Printf("Emulator: %s\n", emulator.Host)_10log.Printf("Testnet: %s\n", testnet.Host)_10log.Printf("Mainnet: %s\n", mainnet.Host)
Getting Networks from State
_11// Get all networks defined in flow.json_11networks := state.Networks()_11_11// Get a specific network by name_11testnet, err := networks.ByName("testnet")_11if err != nil {_11 log.Fatalf("Network not found: %v", err)_11}_11_11log.Printf("Network: %s\n", testnet.Name)_11log.Printf("Host: %s\n", testnet.Host)
Adding or Updating Networks
_14import "github.com/onflow/flowkit/v2/config"_14_14// Add a custom network_14networks := state.Networks()_14networks.AddOrUpdate(config.Network{_14 Name: "custom-network",_14 Host: "localhost:3570",_14})_14_14// Save the updated configuration_14err := state.SaveDefault()_14if err != nil {_14 log.Fatalf("Failed to save state: %v", err)_14}
Getting Network-Specific Aliases
Network aliases map contract names/locations to their deployed addresses on specific networks:
_10import "github.com/onflow/flowkit/v2/config"_10_10// Get aliases for testnet_10aliases := state.AliasesForNetwork(config.TestnetNetwork)_10_10// aliases is a map[string]string of location/name -> address_10for location, address := range aliases {_10 log.Printf("%s deployed at %s on testnet\n", location, address)_10}
Resolving Imports with ImportReplacer
The ImportReplacer resolves import statements in Cadence contracts, scripts, and transactions by replacing relative file paths and contract names with their deployed blockchain addresses.
Basic Usage
When you have a Cadence program with imports like import "Kibble", you need to resolve these to blockchain addresses:
_37import "github.com/onflow/flowkit/v2/project"_37import "github.com/onflow/flowkit/v2/config"_37_37// Get contracts for your target network_37contracts, err := state.DeploymentContractsByNetwork(config.TestnetNetwork)_37if err != nil {_37 log.Fatal(err)_37}_37_37// Create an import replacer with your project's contracts_37importReplacer := project.NewImportReplacer(contracts, nil)_37_37// Parse your Cadence program_37code := []byte(`_37import "Kibble"_37import "FungibleToken"_37_37transaction {_37 prepare(signer: &Account) {_37 // ..._37 }_37}_37`)_37_37program, err := project.NewProgram(code, nil, "")_37if err != nil {_37 log.Fatalf("Failed to parse program: %v", err)_37}_37_37// Replace imports with deployed addresses_37resolvedProgram, err := importReplacer.Replace(program)_37if err != nil {_37 log.Fatalf("Failed to resolve imports: %v", err)_37}_37_37// The resolved program now has addresses instead of file paths_37log.Printf("Resolved code:\n%s", string(resolvedProgram.Code()))
Integration with Project State
The most common pattern is to use network-specific aliases from your project state:
_33// Load project state and get network-specific contracts and aliases_33state, err := flowkit.Load([]string{"flow.json"}, afero.Afero{Fs: afero.NewOsFs()})_33if err != nil {_33 log.Fatal(err)_33}_33_33// Choose your target network_33network := config.TestnetNetwork_33_33// Get contracts for this network_33contracts, err := state.DeploymentContractsByNetwork(network)_33if err != nil {_33 log.Fatal(err)_33}_33_33// Use network-specific aliases for address mapping_33importReplacer := project.NewImportReplacer(_33 contracts,_33 state.AliasesForNetwork(network),_33)_33_33// Parse and resolve your program_33program, err := project.NewProgram(scriptCode, nil, "script.cdc")_33if err != nil {_33 log.Fatalf("Failed to parse program: %v", err)_33}_33resolvedProgram, err := importReplacer.Replace(program)_33if err != nil {_33 log.Fatalf("Failed to resolve imports: %v", err)_33}_33_33// Use the resolved program for execution_33log.Printf("Ready to execute:\n%s", string(resolvedProgram.Code()))
Working with Accounts
Accounts represent Flow blockchain accounts used for signing transactions and deploying contracts.
Getting Account Information
_25import "github.com/onflow/flow-go-sdk"_25_25accounts := state.Accounts()_25_25// Get account by name_25account, err := accounts.ByName("emulator-account")_25if err != nil {_25 log.Fatalf("Account not found: %v", err)_25}_25_25log.Printf("Account: %s\n", account.Name)_25log.Printf("Address: %s\n", account.Address)_25_25// Get all account names_25names := accounts.Names()_25for _, name := range names {_25 log.Printf("Available account: %s\n", name)_25}_25_25// Get account by address_25addr := flow.HexToAddress("0xf8d6e0586b0a20c7")_25account, err = accounts.ByAddress(addr)_25if err != nil {_25 log.Fatalf("Account not found by address: %v", err)_25}
Getting the Emulator Service Account
_10// Get the emulator's default service account_10serviceAccount, err := state.EmulatorServiceAccount()_10if err != nil {_10 log.Fatalf("Failed to get service account: %v", err)_10}_10_10log.Printf("Service account address: %s\n", serviceAccount.Address)
Working with Deployments
Deployments define which contracts should be deployed to which accounts on specific networks.
Getting Deployment Information
_20deployments := state.Deployments()_20_20// Get all deployments for a network_20testnetDeployments := deployments.ByNetwork("testnet")_20_20for _, deployment := range testnetDeployments {_20 log.Printf("Account: %s\n", deployment.Account)_20 log.Printf("Network: %s\n", deployment.Network)_20 log.Printf("Contracts:\n")_20_20 for _, contract := range deployment.Contracts {_20 log.Printf(" - %s\n", contract.Name)_20 }_20}_20_20// Get deployment for specific account and network_20deployment := deployments.ByAccountAndNetwork("my-account", "testnet")_20if deployment != nil {_20 log.Printf("Found deployment: %d contracts\n", len(deployment.Contracts))_20}
Complete Example
Here's a complete example that ties everything together:
_71package main_71_71import (_71 "log"_71_71 "github.com/onflow/flowkit/v2"_71 "github.com/onflow/flowkit/v2/config"_71 "github.com/onflow/flowkit/v2/project"_71 "github.com/spf13/afero"_71)_71_71func main() {_71 // 1. Load project state_71 state, err := flowkit.Load([]string{"flow.json"}, afero.Afero{Fs: afero.NewOsFs()})_71 if err != nil {_71 log.Fatalf("Failed to load state: %v", err)_71 }_71_71 // 2. Choose target network_71 network := config.TestnetNetwork_71 log.Printf("Using network: %s\n", network.Name)_71_71 // 3. Get deployment contracts for the network_71 contracts, err := state.DeploymentContractsByNetwork(network)_71 if err != nil {_71 log.Fatalf("Failed to get contracts: %v", err)_71 }_71_71 log.Printf("Found %d contracts for deployment\n", len(contracts))_71 for _, contract := range contracts {_71 log.Printf(" - %s -> %s\n", contract.Name, contract.AccountAddress)_71 }_71_71 // 4. Get network aliases_71 aliases := state.AliasesForNetwork(network)_71 log.Printf("Network has %d aliases\n", len(aliases))_71_71 // 5. Create import replacer_71 importReplacer := project.NewImportReplacer(contracts, aliases)_71_71 // 6. Resolve imports in a script_71 scriptCode := []byte(`_71 import "Kibble"_71 import "FungibleToken"_71_71 access(all) fun main(): String {_71 return "Hello, Flow!"_71 }_71 `)_71_71 program, err := project.NewProgram(scriptCode, nil, "script.cdc")_71 if err != nil {_71 log.Fatalf("Failed to parse program: %v", err)_71 }_71 resolvedProgram, err := importReplacer.Replace(program)_71 if err != nil {_71 log.Fatalf("Failed to resolve imports: %v", err)_71 }_71_71 log.Printf("Resolved script:\n%s\n", string(resolvedProgram.Code()))_71_71 // 7. Get account for signing_71 account, err := state.Accounts().ByName("testnet-account")_71 if err != nil {_71 log.Fatalf("Failed to get account: %v", err)_71 }_71_71 log.Printf("Using account: %s (%s)\n", account.Name, account.Address)_71_71 log.Println("Setup complete! Ready to interact with Flow.")_71}
Conclusion
Flowkit provides a powerful and flexible API for managing Flow projects in Go. By understanding how to work with project state, contracts, networks, and import resolution, you can build robust applications that interact with the Flow blockchain.
The import replacer is particularly critical for ensuring your Cadence code works correctly across different networks by automatically resolving contract imports to their deployed addresses.