Repository: onflow/flow-cli (go)

Issue:
Remove emulator default configuration from `flow.json`
### Issue To Be Solved

```
  "emulators": {
    "default": {
      "port": 3569,
      "serviceAccount": "emulator-account"
    }
  },
```


### (Optional): Suggest A Solution

This section of the default `flow.json` file can be omitted. The CLI should assume these defaults unless they are changed, in which case this section could be re-aded with alternate values.

### (Optional): Context

Suggestion to remove default configuration in favour of opt-in to configuration changes when needed will help to _simplify_ the `flow.json` file, which will make it easier to understand for newcomers.




Pull request:
feature/prevent default emulator from being printed during project init in fl…
Closes #400 

## Description

This PR prevents default emulator settings from being printed on flow.json during project initialization. User should still be able to change emulator settings by adding emulator field configuration in flow.json.

______

For contributor use:

- [x] Targeted PR against `master` branch
- [x] Linked to Github issue with discussion and accepted design OR link to spec that describes this work
- [x] Code follows the [standards mentioned here](https://github.com/onflow/flow-cli/blob/master/CONTRIBUTING.md#styleguides)
- [x] Updated relevant documentation
- [x] Re-reviewed `Files changed` in the Github PR explorer
- [x] Added appropriate labels


Patch:
diff --git a/docs/configuration.md b/docs/configuration.md
index 5c8c0727..f7c6db2d 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -10,7 +10,6 @@ Flow configuration (`flow.json`) file will contain the following properties:
 
 - A `networks` list pre-populated with the Flow emulator, testnet and mainnet connection configuration.
 - An `accounts` list pre-populated with the Flow Emulator service account.
-- An `emulators` list pre-populated with Flow Emulator configuration.
 - A `deployments` empty object where all [deployment targets](project-contracts.md) can be defined. 
 - A `contracts` empty object where you [define contracts](project-contracts.md) you wish to deploy.
 
@@ -18,12 +17,6 @@ Flow configuration (`flow.json`) file will contain the following properties:
 
 ```json
 {
-  "emulators": {
-    "default": {
-      "port": 3569,
-      "serviceAccount": "emulator-account"
-    }
-  },
   "networks": {
     "emulator": "127.0.0.1:3569",
     "mainnet": "access.mainnet.nodes.onflow.org:9000",
@@ -99,13 +92,6 @@ We'll walk through each property one by one.
       "Host": "access-001.devnet30.nodes.onflow.org:9001",
       "NetworkKey": "ba69f7d2e82b9edf25b103c195cd371cf0cc047ef8884a9bbe331e62982d46daeebf836f7445a2ac16741013b192959d8ad26998aff12f2adc67a99e1eb2988d"
     }
-  },
-
-  "emulators": {
-    "default": {
-      "port": 3569,
-      "serviceAccount": "emulator-account"
-    }
   }
 }
 ```
@@ -321,3 +307,23 @@ Format for networks is:
 
 ...
 ```
+### Emulators
+
+The default emulator CLI is automatically configured with name being `"default"` and values of 
+`serviceAccount`: `"emulator-account"` and `port`: `"3569"`. The default emulator configuration will not show up on 
+flow.json.
+
+To customize emulator values, add emulator section like the example below: 
+
+```json
+...
+
+"emulators": {
+    "custom-emulator": {
+        "port": 3600,
+        "serviceAccount": "emulator-account"
+    }
+}
+
+...
+```
diff --git a/pkg/flowkit/config/config.go b/pkg/flowkit/config/config.go
index 5b37f2bb..16fa961e 100644
--- a/pkg/flowkit/config/config.go
+++ b/pkg/flowkit/config/config.go
@@ -47,6 +47,7 @@ const (
 	KeyTypeGoogleKMS                  KeyType = "google-kms"
 	DefaultEmulatorConfigName                 = "default"
 	DefaultEmulatorServiceAccountName         = "emulator-account"
+	DefaultEmulatorPort                       = 3569
 )
 
 // Validate the configuration values.
diff --git a/pkg/flowkit/config/emulator.go b/pkg/flowkit/config/emulator.go
index 27689194..79b7ce64 100644
--- a/pkg/flowkit/config/emulator.go
+++ b/pkg/flowkit/config/emulator.go
@@ -35,9 +35,9 @@ func DefaultEmulators() Emulators {
 // DefaultEmulator gets default emulator.
 func DefaultEmulator() Emulator {
 	return Emulator{
-		Name:           "default",
+		Name:           DefaultEmulatorConfigName,
 		ServiceAccount: DefaultEmulatorServiceAccountName,
-		Port:           3569,
+		Port:           DefaultEmulatorPort,
 	}
 }
 
diff --git a/pkg/flowkit/config/json/config.go b/pkg/flowkit/config/json/config.go
index 867bec90..5019430d 100644
--- a/pkg/flowkit/config/json/config.go
+++ b/pkg/flowkit/config/json/config.go
@@ -27,7 +27,7 @@ import (
 
 // jsonConfig implements JSON format for persisting and parsing configuration.
 type jsonConfig struct {
-	Emulators   jsonEmulators   `json:"emulators"`
+	Emulators   jsonEmulators   `json:"emulators,omitempty"`
 	Contracts   jsonContracts   `json:"contracts"`
 	Networks    jsonNetworks    `json:"networks"`
 	Accounts    jsonAccounts    `json:"accounts"`
@@ -70,7 +70,6 @@ func (j *jsonConfig) transformToConfig() (*config.Config, error) {
 
 	return conf, nil
 }
-
 func transformConfigToJSON(config *config.Config) jsonConfig {
 	return jsonConfig{
 		Emulators:   transformEmulatorsToJSON(config.Emulators),
@@ -112,7 +111,6 @@ func NewParser() *Parser {
 // Serialize configuration to raw.
 func (p *Parser) Serialize(conf *config.Config) ([]byte, error) {
 	jsonConf := transformConfigToJSON(conf)
-
 	data, err := json.MarshalIndent(jsonConf, "", "\t")
 	if err != nil {
 		return nil, err
diff --git a/pkg/flowkit/config/json/emulator.go b/pkg/flowkit/config/json/emulator.go
index b7498323..f5b474bc 100644
--- a/pkg/flowkit/config/json/emulator.go
+++ b/pkg/flowkit/config/json/emulator.go
@@ -52,6 +52,9 @@ func transformEmulatorsToJSON(emulators config.Emulators) jsonEmulators {
 	jsonEmulators := jsonEmulators{}
 
 	for _, e := range emulators {
+		if e == config.DefaultEmulator() {
+			continue
+		}
 		jsonEmulators[e.Name] = jsonEmulator{
 			Port:           e.Port,
 			ServiceAccount: e.ServiceAccount,
diff --git a/pkg/flowkit/state.go b/pkg/flowkit/state.go
index f2b6c0f3..c5a150f5 100644
--- a/pkg/flowkit/state.go
+++ b/pkg/flowkit/state.go
@@ -248,7 +248,11 @@ func Load(configFilePaths []string, readerWriter ReaderWriter) (*State, error) {
 	if err != nil {
 		return nil, err
 	}
-
+	// only add a default emulator in the config if the emulator account is present in accounts
+	_, err = conf.Accounts.ByName(config.DefaultEmulatorServiceAccountName)
+	if err == nil && len(conf.Emulators) == 0 {
+		conf.Emulators.AddOrUpdate("", config.DefaultEmulator())
+	}
 	proj, err := newProject(conf, confLoader, readerWriter)
 	if err != nil {
 		return nil, fmt.Errorf("invalid project configuration: %s", err)


Test patch:
diff --git a/pkg/flowkit/config/json/config_test.go b/pkg/flowkit/config/json/config_test.go
index d273beb0..70240568 100644
--- a/pkg/flowkit/config/json/config_test.go
+++ b/pkg/flowkit/config/json/config_test.go
@@ -20,9 +20,19 @@ package json
 import (
 	"testing"
 
+	"github.com/onflow/flow-cli/pkg/flowkit/config"
+
+	"github.com/onflow/flow-go-sdk"
+	"github.com/onflow/flow-go-sdk/crypto"
 	"github.com/stretchr/testify/assert"
 )
 
+func keys() []crypto.PrivateKey {
+	var keys []crypto.PrivateKey
+	key, _ := crypto.DecodePrivateKeyHex(crypto.ECDSA_P256, "dd72967fd2bd75234ae9037dd4694c1f00baad63a10c35172bf65fbb8ad74b47")
+	keys = append(keys, key)
+	return keys
+}
 func Test_SimpleJSONConfig(t *testing.T) {
 	b := []byte(`{
 		"emulators": {
@@ -156,3 +166,98 @@ func Test_NonExistingAccountForEmulator(t *testing.T) {
 	err = conf.Validate()
 	assert.Equal(t, err.Error(), "emulator default contains nonexisting service account emulator-account")
 }
+
+//If config has default emulator values, it will not show up in flow.json
+func Test_SerializeConfigToJsonEmulatorDefault(t *testing.T) {
+	configJson := []byte(`{
+		"contracts": {},
+		"accounts": {
+			"emulator-account": {
+				"address": "f8d6e0586b0a20c7",
+				"key": "dd72967fd2bd75234ae9037dd4694c1f00baad63a10c35172bf65fbb8ad74b47"
+			}
+		},
+		"networks": {
+			"emulator": "127.0.0.1.3569"
+		},
+		"deployments": {
+		}
+	}`)
+	config := config.Config{
+		Emulators: config.Emulators{{
+			Name:           "default",
+			Port:           3569,
+			ServiceAccount: "emulator-account",
+		}},
+		Contracts:   config.Contracts{},
+		Deployments: config.Deployments{},
+		Accounts: config.Accounts{{
+			Name:    "emulator-account",
+			Address: flow.ServiceAddress(flow.Emulator),
+			Key: config.AccountKey{
+				Type:       config.KeyTypeHex,
+				Index:      0,
+				SigAlgo:    crypto.ECDSA_P256,
+				HashAlgo:   crypto.SHA3_256,
+				PrivateKey: keys()[0],
+			},
+		}},
+		Networks: config.Networks{{
+			Name: "emulator",
+			Host: "127.0.0.1.3569",
+		}},
+	}
+	parser := NewParser()
+	conf, _ := parser.Serialize(&config)
+	assert.JSONEq(t, string(configJson), string(conf))
+}
+func Test_SerializeConfigToJsonEmulatorNotDefault(t *testing.T) {
+	configJson := []byte(`{
+		"contracts": {},
+		"emulators": {
+			"default": {
+				"port": 6000,
+				"serviceAccount": "emulator-account"
+			}
+		},
+		"accounts": {
+			"emulator-account": {
+				"address": "f8d6e0586b0a20c7",
+				"key": "dd72967fd2bd75234ae9037dd4694c1f00baad63a10c35172bf65fbb8ad74b47"
+			}
+		},
+		"networks": {
+			"emulator": "127.0.0.1.3569"
+		},
+		"deployments": {
+		}
+	}`)
+	config := config.Config{
+		Emulators: config.Emulators{{
+			Name:           "default",
+			Port:           6000,
+			ServiceAccount: "emulator-account",
+		}},
+		Contracts:   config.Contracts{},
+		Deployments: config.Deployments{},
+		Accounts: config.Accounts{{
+			Name:    "emulator-account",
+			Address: flow.ServiceAddress(flow.Emulator),
+			Key: config.AccountKey{
+				Type:       config.KeyTypeHex,
+				Index:      0,
+				SigAlgo:    crypto.ECDSA_P256,
+				HashAlgo:   crypto.SHA3_256,
+				PrivateKey: keys()[0],
+			},
+		}},
+		Networks: config.Networks{{
+			Name: "emulator",
+			Host: "127.0.0.1.3569",
+		}},
+	}
+	parser := NewParser()
+	conf, _ := parser.Serialize(&config)
+	assert.JSONEq(t, string(configJson), string(conf))
+
+}
diff --git a/pkg/flowkit/config/json/emulator_test.go b/pkg/flowkit/config/json/emulator_test.go
index d61faf92..725b7148 100644
--- a/pkg/flowkit/config/json/emulator_test.go
+++ b/pkg/flowkit/config/json/emulator_test.go
@@ -42,3 +42,29 @@ func Test_ConfigEmulatorSimple(t *testing.T) {
 	assert.Equal(t, emulators[0].Name, "default")
 	assert.Equal(t, emulators[0].Port, 9000)
 }
+func Test_ConfigMultipleEmulators(t *testing.T) {
+	b := []byte(`{
+		 "default": {
+				"port": 9000,
+				"serviceAccount": "emulator-account"
+		 }, 
+		 "custom-emulator": {
+				"port": 3000,
+				"serviceAccount": "custom-emulator-account"
+		 }
+	 }`)
+
+	var jsonEmulators jsonEmulators
+	err := json.Unmarshal(b, &jsonEmulators)
+	assert.NoError(t, err)
+
+	emulators, err := jsonEmulators.transformToConfig()
+	assert.NoError(t, err)
+	assert.Len(t, emulators, 2)
+	assert.Equal(t, emulators[0].Name, "default")
+	assert.Equal(t, emulators[0].Port, 9000)
+	assert.Equal(t, emulators[0].ServiceAccount, "emulator-account")
+	assert.Equal(t, emulators[1].Name, "custom-emulator")
+	assert.Equal(t, emulators[1].Port, 3000)
+	assert.Equal(t, emulators[1].ServiceAccount, "custom-emulator-account")
+}
diff --git a/pkg/flowkit/state_test.go b/pkg/flowkit/state_test.go
index 083405f6..eb4b5684 100644
--- a/pkg/flowkit/state_test.go
+++ b/pkg/flowkit/state_test.go
@@ -665,3 +665,218 @@ func Test_Saving(t *testing.T) {
 	err = s.SaveEdited([]string{config.GlobalPath(), config.DefaultPath})
 	assert.NoError(t, err)
 }
+
+//ensures that default emulator values are in config when no emulator is defined in flow.json
+func Test_DefaultEmulatorNotPresentInConfig(t *testing.T) {
+	configJson := []byte(`{
+		"contracts": {},
+		"accounts": {
+			"emulator-account": {
+				"address": "f8d6e0586b0a20c7",
+				"key": "dd72967fd2bd75234ae9037dd4694c1f00baad63a10c35172bf65fbb8ad74b47"
+			}
+		},
+		"networks": {
+			"emulator": "127.0.0.1.3569"
+		},
+		"deployments": {
+		}
+	}`)
+	config := config.Config{
+		Emulators: config.Emulators{{
+			Name:           "default",
+			Port:           3569,
+			ServiceAccount: "emulator-account",
+		}},
+		Contracts:   config.Contracts{},
+		Deployments: config.Deployments{},
+		Accounts: config.Accounts{{
+			Name:    "emulator-account",
+			Address: flow.ServiceAddress(flow.Emulator),
+			Key: config.AccountKey{
+				Type:       config.KeyTypeHex,
+				Index:      0,
+				SigAlgo:    crypto.ECDSA_P256,
+				HashAlgo:   crypto.SHA3_256,
+				PrivateKey: keys()[0],
+			},
+		}},
+		Networks: config.Networks{{
+			Name: "emulator",
+			Host: "127.0.0.1.3569",
+		}},
+	}
+	af := afero.Afero{Fs: afero.NewMemMapFs()}
+	err := afero.WriteFile(af.Fs, "flow.json", configJson, 0644)
+	assert.NoError(t, err)
+	paths := []string{"flow.json"}
+	state, err := Load(paths, af)
+	assert.Equal(t, state.conf, &config)
+	assert.NoError(t, err)
+}
+
+//ensures that default emulator values are not in config when no emulator is defined in flow.json
+func Test_DefaultEmulatorWithoutEmulatorAccountInConfig(t *testing.T) {
+	configJson := []byte(`{
+		"contracts": {},
+		"accounts": {
+			"testnet-account": {
+      			"address": "1e82856bf20e2aa6",
+				"key": "388e3fbdc654b765942610679bb3a66b74212149ab9482187067ee116d9a8118"
+    		}
+		},
+		"networks": {
+			"emulator": "127.0.0.1.3569"
+		},
+		"deployments": {
+		}
+	}`)
+	af := afero.Afero{Fs: afero.NewMemMapFs()}
+	err := afero.WriteFile(af.Fs, "flow.json", configJson, 0644)
+	assert.NoError(t, err)
+	paths := []string{"flow.json"}
+	state, _ := Load(paths, af)
+	assert.Equal(t, config.Emulators{}, state.conf.Emulators)
+}
+
+//ensures that default emulator values are in config when emulator is defined in flow.json
+func Test_DefaultEmulatorWithEmulatorAccountInConfig(t *testing.T) {
+	configJson := []byte(`{
+		"contracts": {},
+		"accounts": {
+			"testnet-account": {
+      			"address": "1e82856bf20e2aa6",
+				"key": "388e3fbdc654b765942610679bb3a66b74212149ab9482187067ee116d9a8118"
+    		},
+			"emulator-account": {
+				"address": "f8d6e0586b0a20c7",
+				"key": "dd72967fd2bd75234ae9037dd4694c1f00baad63a10c35172bf65fbb8ad74b47"
+			}
+		},
+		"networks": {
+			"emulator": "127.0.0.1.3569"
+		},
+		"deployments": {
+		}
+	}`)
+	af := afero.Afero{Fs: afero.NewMemMapFs()}
+	err := afero.WriteFile(af.Fs, "flow.json", configJson, 0644)
+	assert.NoError(t, err)
+	paths := []string{"flow.json"}
+	state, _ := Load(paths, af)
+	assert.Len(t, state.conf.Emulators, 1)
+	assert.Equal(t, state.conf.Emulators, config.DefaultEmulators())
+}
+
+//backward compatibility test to ensure that default emulator values are still observed in flow.json
+func Test_DefaultEmulatorPresentInConfig(t *testing.T) {
+	configJson := []byte(`{
+		"contracts": {},
+		"emulators": {
+			"default": {
+				"port": 3569,
+				"serviceAccount": "emulator-account"
+			}
+		},
+		"accounts": {
+			"emulator-account": {
+				"address": "f8d6e0586b0a20c7",
+				"key": "dd72967fd2bd75234ae9037dd4694c1f00baad63a10c35172bf65fbb8ad74b47"
+			}
+		},
+		"networks": {
+			"emulator": "127.0.0.1.3569"
+		},
+		"deployments": {
+		}
+	}`)
+	config := config.Config{
+		Emulators: config.Emulators{{
+			Name:           "default",
+			Port:           3569,
+			ServiceAccount: "emulator-account",
+		}},
+		Contracts:   config.Contracts{},
+		Deployments: config.Deployments{},
+		Accounts: config.Accounts{{
+			Name:    "emulator-account",
+			Address: flow.ServiceAddress(flow.Emulator),
+			Key: config.AccountKey{
+				Type:       config.KeyTypeHex,
+				Index:      0,
+				SigAlgo:    crypto.ECDSA_P256,
+				HashAlgo:   crypto.SHA3_256,
+				PrivateKey: keys()[0],
+			},
+		}},
+		Networks: config.Networks{{
+			Name: "emulator",
+			Host: "127.0.0.1.3569",
+		}},
+	}
+	af := afero.Afero{Fs: afero.NewMemMapFs()}
+	err := afero.WriteFile(af.Fs, "flow.json", configJson, 0644)
+	assert.NoError(t, err)
+	paths := []string{"flow.json"}
+	state, err := Load(paths, af)
+	assert.Equal(t, 1, len(state.conf.Emulators))
+	assert.Equal(t, state.conf, &config)
+	assert.NoError(t, err)
+}
+
+//ensures that custom emulator values are still observed in flow.json
+func Test_CustomEmulatorValuesInConfig(t *testing.T) {
+	configJson := []byte(`{
+		"contracts": {},
+		"emulators": {
+			"custom-emulator": {
+				"port": 2000,
+				"serviceAccount": "emulator-account"
+			}
+		},
+		"accounts": {
+			"emulator-account": {
+				"address": "f8d6e0586b0a20c7",
+				"key": "dd72967fd2bd75234ae9037dd4694c1f00baad63a10c35172bf65fbb8ad74b47"
+			}
+		},
+		"networks": {
+			"emulator": "127.0.0.1.3569"
+		},
+		"deployments": {
+		}
+	}`)
+	config := config.Config{
+		Emulators: config.Emulators{{
+			Name:           "custom-emulator",
+			Port:           2000,
+			ServiceAccount: "emulator-account",
+		}},
+		Contracts:   config.Contracts{},
+		Deployments: config.Deployments{},
+		Accounts: config.Accounts{{
+			Name:    "emulator-account",
+			Address: flow.ServiceAddress(flow.Emulator),
+			Key: config.AccountKey{
+				Type:       config.KeyTypeHex,
+				Index:      0,
+				SigAlgo:    crypto.ECDSA_P256,
+				HashAlgo:   crypto.SHA3_256,
+				PrivateKey: keys()[0],
+			},
+		}},
+		Networks: config.Networks{{
+			Name: "emulator",
+			Host: "127.0.0.1.3569",
+		}},
+	}
+	af := afero.Afero{Fs: afero.NewMemMapFs()}
+	err := afero.WriteFile(af.Fs, "flow.json", configJson, 0644)
+	assert.NoError(t, err)
+	paths := []string{"flow.json"}
+	state, err := Load(paths, af)
+	assert.Equal(t, "custom-emulator", state.conf.Emulators[0].Name)
+	assert.Equal(t, 1, len(state.conf.Emulators))
+	assert.Equal(t, state.conf, &config)
+	assert.NoError(t, err)
+}