Skip to content

Commit 16db6c5

Browse files
authored
Add --fork flag to emulator for flow.json-aware network forking (#2148)
Add --fork flag to emulator for flow.json-aware network forking This adds a --fork flag that resolves network names from flow.json and passes the access node endpoint to the emulator's --fork-host flag. If --fork is provided without a value, it defaults to mainnet.
1 parent c5a654c commit 16db6c5

4 files changed

Lines changed: 276 additions & 12 deletions

File tree

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ require (
1919
github.com/onflow/fcl-dev-wallet v0.8.0
2020
github.com/onflow/flixkit-go/v2 v2.6.0
2121
github.com/onflow/flow-core-contracts/lib/go/templates v1.9.1
22-
github.com/onflow/flow-emulator v1.9.0
22+
github.com/onflow/flow-emulator v1.10.0
2323
github.com/onflow/flow-evm-gateway v1.3.5
2424
github.com/onflow/flow-go v0.43.3-0.20251021182938-b0fef2c5ca47
2525
github.com/onflow/flow-go-sdk v1.9.0
26+
github.com/onflow/flow/protobuf/go/flow v0.4.16
2627
github.com/onflow/flowkit/v2 v2.7.0
2728
github.com/onflowser/flowser/v3 v3.2.1-0.20240131200229-7d4d22715f48
2829
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
@@ -206,7 +207,6 @@ require (
206207
github.com/onflow/flow-ft/lib/go/templates v1.0.1 // indirect
207208
github.com/onflow/flow-nft/lib/go/contracts v1.3.0 // indirect
208209
github.com/onflow/flow-nft/lib/go/templates v1.3.0 // indirect
209-
github.com/onflow/flow/protobuf/go/flow v0.4.16 // indirect
210210
github.com/onflow/go-ethereum v1.15.10 // indirect
211211
github.com/onflow/nft-storefront/lib/go/contracts v1.0.0 // indirect
212212
github.com/onflow/sdks v0.6.0-preview.1 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -793,8 +793,8 @@ github.com/onflow/flow-core-contracts/lib/go/contracts v1.9.1 h1:u6am8NzuWOIKkSk
793793
github.com/onflow/flow-core-contracts/lib/go/contracts v1.9.1/go.mod h1:jBDqVep0ICzhXky56YlyO4aiV2Jl/5r7wnqUPpvi7zE=
794794
github.com/onflow/flow-core-contracts/lib/go/templates v1.9.1 h1:ebyynXy74ZcfW+JpPwI+aaY0ezlxxA0cUgUrjhJonWg=
795795
github.com/onflow/flow-core-contracts/lib/go/templates v1.9.1/go.mod h1:twSVyUt3rNrgzAmxtBX+1Gw64QlPemy17cyvnXYy1Ug=
796-
github.com/onflow/flow-emulator v1.9.0 h1:cAi64Xi7UROU2KNWXV0un009OZy+S6N2j4LCne29LRk=
797-
github.com/onflow/flow-emulator v1.9.0/go.mod h1:Mxo1VjgerVyAbYVPdb6+21Gzng5Ckfufy5gzHgLXX3c=
796+
github.com/onflow/flow-emulator v1.10.0 h1:zrAlCP6yEFmlDg80fja55AqwVtD00OmrVGzeBf+gvcg=
797+
github.com/onflow/flow-emulator v1.10.0/go.mod h1:t4mJAxj+czpJz6y/Jz4POw5ylBDXPrXFYejm2Env9Ak=
798798
github.com/onflow/flow-evm-bridge v0.1.0 h1:7X2osvo4NnQgHj8aERUmbYtv9FateX8liotoLnPL9nM=
799799
github.com/onflow/flow-evm-bridge v0.1.0/go.mod h1:5UYwsnu6WcBNrwitGFxphCl5yq7fbWYGYuiCSTVF6pk=
800800
github.com/onflow/flow-evm-gateway v1.3.5 h1:2Nx5eCYwUsVBVOMNOMPab66PNKj8784t+SPgAckw2zk=

internal/emulator/start.go

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import (
3232
"github.com/dukex/mixpanel"
3333
"github.com/onflow/flow-emulator/cmd/emulator/start"
3434
"github.com/onflow/flow-go-sdk/crypto"
35+
"github.com/rs/zerolog"
36+
"github.com/rs/zerolog/log"
3537
"github.com/spf13/afero"
3638
"github.com/spf13/cobra"
3739

@@ -66,20 +68,20 @@ func configuredServiceKey(
6668
if init {
6769
state, err = flowkit.Init(loader)
6870
if err != nil {
69-
exitf(1, err.Error())
71+
log.Fatal().Msgf("%s", err.Error())
7072
} else {
7173
err = state.SaveDefault()
7274
if err != nil {
73-
exitf(1, err.Error())
75+
log.Fatal().Msgf("%s", err.Error())
7476
}
7577
}
7678
} else {
7779
state, err = flowkit.Load(command.Flags.ConfigPaths, loader)
7880
if err != nil {
7981
if errors.Is(err, config.ErrDoesNotExist) {
80-
exitf(1, "🙏 Configuration (flow.json) is missing, are you in the correct directory? If you are trying to create a new project, initialize it with 'flow init' and then rerun this command.")
82+
log.Fatal().Msg("🙏 Configuration (flow.json) is missing, are you in the correct directory? If you are trying to create a new project, initialize it with 'flow init' and then rerun this command.")
8183
} else {
82-
exitf(1, err.Error())
84+
log.Fatal().Msgf("%s", err.Error())
8385
}
8486
}
8587
}
@@ -133,6 +135,16 @@ func trackRequestMiddleware(next http.Handler) http.Handler {
133135
}
134136

135137
func init() {
138+
// Configure zerolog to use console format matching the emulator's output
139+
consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr}
140+
consoleWriter.FormatMessage = func(i interface{}) string {
141+
if i == nil {
142+
return ""
143+
}
144+
return fmt.Sprintf("%-44s", i)
145+
}
146+
log.Logger = log.Output(consoleWriter).Level(zerolog.InfoLevel)
147+
136148
// Initialize mixpanel client only if metrics are enabled and token is not empty
137149
if settings.MetricsEnabled() && command.MixpanelToken != "" {
138150
mixpanelClient = mixpanel.New(command.MixpanelToken, "")
@@ -147,13 +159,63 @@ func init() {
147159
})
148160
}
149161

162+
// Add --fork flag with optional value (defaults to mainnet when value omitted)
163+
Cmd.Flags().String("fork", "", "fork from a remote network defined in flow.json. If provided without a value, defaults to mainnet")
164+
if f := Cmd.Flags().Lookup("fork"); f != nil {
165+
f.NoOptDefVal = "mainnet"
166+
}
167+
150168
Cmd.Use = "emulator"
151169
Cmd.Short = "Run Flow network for development"
152170
Cmd.GroupID = "tools"
153171
SnapshotCmd.AddToParent(Cmd)
154-
}
155172

156-
func exitf(code int, msg string, args ...any) {
157-
fmt.Printf(msg+"\n", args...)
158-
os.Exit(code)
173+
// Translate --fork to --fork-host before emulator reads flags
174+
Cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
175+
fh, err := cmd.Flags().GetString("fork-host")
176+
if err != nil {
177+
return err
178+
}
179+
if fh != "" {
180+
return nil
181+
}
182+
forkOpt, err := cmd.Flags().GetString("fork")
183+
if err != nil {
184+
return err
185+
}
186+
if forkOpt == "" {
187+
return nil
188+
}
189+
loader := &afero.Afero{Fs: afero.NewOsFs()}
190+
state, err := flowkit.Load(command.Flags.ConfigPaths, loader)
191+
if err != nil {
192+
return fmt.Errorf("failed to load flow.json: %w", err)
193+
}
194+
195+
// Resolve network endpoint from flow.json
196+
network, err := state.Networks().ByName(forkOpt)
197+
if err != nil {
198+
return fmt.Errorf("network %q not found in flow.json", forkOpt)
199+
}
200+
host := network.Host
201+
if host == "" {
202+
return fmt.Errorf("network %q has no host configured", forkOpt)
203+
}
204+
205+
// Set fork-host flag
206+
if err := cmd.Flags().Set("fork-host", host); err != nil {
207+
return err
208+
}
209+
210+
// Automatically disable signature validation when forking
211+
// This is necessary because forked transactions were signed for the original network
212+
if err := cmd.Flags().Set("skip-tx-validation", "true"); err != nil {
213+
return err
214+
}
215+
216+
// Log info to stderr
217+
log.Info().Msg("Signature validation automatically disabled for fork mode")
218+
219+
return nil
220+
}
159221
}

internal/emulator/start_test.go

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*
2+
* Flow CLI
3+
*
4+
* Copyright Flow Foundation
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package emulator
20+
21+
import (
22+
"fmt"
23+
"os"
24+
"path/filepath"
25+
"testing"
26+
27+
"github.com/spf13/afero"
28+
"github.com/spf13/cobra"
29+
"github.com/stretchr/testify/assert"
30+
"github.com/stretchr/testify/require"
31+
32+
"github.com/onflow/flowkit/v2"
33+
)
34+
35+
func Test_PersistentPreRunE_ForkFlag(t *testing.T) {
36+
// Create a temporary directory for flow.json
37+
tempDir := t.TempDir()
38+
flowJSONPath := filepath.Join(tempDir, "flow.json")
39+
40+
// Create a sample flow.json with networks
41+
flowJSONContent := `{
42+
"networks": {
43+
"mainnet": "access.mainnet.nodes.onflow.org:9000",
44+
"testnet": "access.devnet.nodes.onflow.org:9000"
45+
}
46+
}`
47+
err := os.WriteFile(flowJSONPath, []byte(flowJSONContent), 0644)
48+
require.NoError(t, err)
49+
50+
// Create a command with the fork flag
51+
cmd := &cobra.Command{}
52+
cmd.Flags().String("fork", "", "")
53+
cmd.Flags().String("fork-host", "", "")
54+
cmd.Flags().Bool("skip-tx-validation", false, "")
55+
56+
// Set the PersistentPreRunE function (copied from init)
57+
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
58+
fh, err := cmd.Flags().GetString("fork-host")
59+
if err != nil {
60+
return err
61+
}
62+
if fh != "" {
63+
return nil
64+
}
65+
forkOpt, err := cmd.Flags().GetString("fork")
66+
if err != nil {
67+
return err
68+
}
69+
if forkOpt == "" {
70+
return nil
71+
}
72+
loader := &afero.Afero{Fs: afero.NewOsFs()}
73+
state, err := flowkit.Load([]string{flowJSONPath}, loader) // Use the test path
74+
if err != nil {
75+
return fmt.Errorf("failed to load flow.json: %w", err)
76+
}
77+
78+
// Resolve network endpoint from flow.json
79+
network, err := state.Networks().ByName(forkOpt)
80+
if err != nil {
81+
return fmt.Errorf("network %q not found in flow.json", forkOpt)
82+
}
83+
host := network.Host
84+
if host == "" {
85+
return fmt.Errorf("network %q has no host configured", forkOpt)
86+
}
87+
88+
// Set fork-host flag
89+
if err := cmd.Flags().Set("fork-host", host); err != nil {
90+
return err
91+
}
92+
93+
// Automatically disable signature validation when forking
94+
if err := cmd.Flags().Set("skip-tx-validation", "true"); err != nil {
95+
return err
96+
}
97+
98+
return nil
99+
}
100+
101+
// Test case 1: Fork with mainnet
102+
err = cmd.Flags().Set("fork", "mainnet")
103+
require.NoError(t, err)
104+
err = cmd.PersistentPreRunE(cmd, []string{})
105+
assert.NoError(t, err)
106+
107+
forkHost, _ := cmd.Flags().GetString("fork-host")
108+
assert.Equal(t, "access.mainnet.nodes.onflow.org:9000", forkHost)
109+
110+
skipValidation, _ := cmd.Flags().GetBool("skip-tx-validation")
111+
assert.True(t, skipValidation)
112+
113+
// Reset flags for next test
114+
err = cmd.Flags().Set("fork-host", "")
115+
require.NoError(t, err)
116+
err = cmd.Flags().Set("skip-tx-validation", "false")
117+
require.NoError(t, err)
118+
119+
// Test case 2: Fork with testnet
120+
err = cmd.Flags().Set("fork", "testnet")
121+
require.NoError(t, err)
122+
err = cmd.PersistentPreRunE(cmd, []string{})
123+
assert.NoError(t, err)
124+
125+
forkHost, _ = cmd.Flags().GetString("fork-host")
126+
assert.Equal(t, "access.devnet.nodes.onflow.org:9000", forkHost)
127+
128+
skipValidation, _ = cmd.Flags().GetBool("skip-tx-validation")
129+
assert.True(t, skipValidation)
130+
}
131+
132+
func Test_PersistentPreRunE_ForkFlag_Errors(t *testing.T) {
133+
// Create a temporary directory for flow.json
134+
tempDir := t.TempDir()
135+
flowJSONPath := filepath.Join(tempDir, "flow.json")
136+
137+
// Create a flow.json without the network
138+
flowJSONContent := `{
139+
"networks": {
140+
"mainnet": "access.mainnet.nodes.onflow.org:9000"
141+
}
142+
}`
143+
err := os.WriteFile(flowJSONPath, []byte(flowJSONContent), 0644)
144+
require.NoError(t, err)
145+
146+
// Create a command
147+
cmd := &cobra.Command{}
148+
cmd.Flags().String("fork", "", "")
149+
cmd.Flags().String("fork-host", "", "")
150+
151+
// Set the PersistentPreRunE function
152+
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
153+
fh, err := cmd.Flags().GetString("fork-host")
154+
if err != nil {
155+
return err
156+
}
157+
if fh != "" {
158+
return nil
159+
}
160+
forkOpt, err := cmd.Flags().GetString("fork")
161+
if err != nil {
162+
return err
163+
}
164+
if forkOpt == "" {
165+
return nil
166+
}
167+
loader := &afero.Afero{Fs: afero.NewOsFs()}
168+
state, err := flowkit.Load([]string{flowJSONPath}, loader)
169+
if err != nil {
170+
return fmt.Errorf("failed to load flow.json: %w", err)
171+
}
172+
173+
// Resolve network endpoint from flow.json
174+
network, err := state.Networks().ByName(forkOpt)
175+
if err != nil {
176+
return fmt.Errorf("network %q not found in flow.json", forkOpt)
177+
}
178+
host := network.Host
179+
if host == "" {
180+
return fmt.Errorf("network %q has no host configured", forkOpt)
181+
}
182+
183+
// Set fork-host flag
184+
if err := cmd.Flags().Set("fork-host", host); err != nil {
185+
return err
186+
}
187+
188+
// Automatically disable signature validation when forking
189+
if err := cmd.Flags().Set("skip-tx-validation", "true"); err != nil {
190+
return err
191+
}
192+
193+
return nil
194+
}
195+
196+
// Test case: Network not found
197+
err = cmd.Flags().Set("fork", "nonexistent")
198+
require.NoError(t, err)
199+
err = cmd.PersistentPreRunE(cmd, []string{})
200+
assert.Error(t, err)
201+
assert.Contains(t, err.Error(), "network \"nonexistent\" not found")
202+
}

0 commit comments

Comments
 (0)