Repository: onflow/flow-cli
Pull request #440 (2022-01-20 17:49:21)

Khalil/1652 download protocol snapshot
Closes [#1652](https://github.com/dapperlabs/flow-internal/issues/1652)

## Description

This PR adds a command to flow-cli that allows users to download the latest finalized protocol state snapshot.

______

For contributor use:

- [ ] Targeted PR against `master` branch
- [ ] Linked to Github issue with discussion and accepted design OR link to spec that describes this work
- [ ] Code follows the [standards mentioned here](https://github.com/onflow/flow-cli/blob/master/CONTRIBUTING.md#styleguides)
- [ ] Updated relevant documentation
- [ ] Re-reviewed `Files changed` in the Github PR explorer
- [ ] Added appropriate labels



Patch:
diff --git a/Makefile b/Makefile
index e2eb947d..17f97e43 100644
--- a/Makefile
+++ b/Makefile
@@ -93,6 +93,10 @@ install-linter:
 lint:
 	golangci-lint run -v ./...
 
+.PHONY: fix-lint
+fix-lint:
+	golangci-lint run -v --fix ./...
+
 .PHONY: check-headers
 check-headers:
 	@./check-headers.sh
diff --git a/cmd/flow/main.go b/cmd/flow/main.go
index 415a0140..8984dcb0 100644
--- a/cmd/flow/main.go
+++ b/cmd/flow/main.go
@@ -34,6 +34,7 @@ import (
 	"github.com/onflow/flow-cli/internal/quick"
 	"github.com/onflow/flow-cli/internal/scripts"
 	"github.com/onflow/flow-cli/internal/signatures"
+	"github.com/onflow/flow-cli/internal/snapshot"
 	"github.com/onflow/flow-cli/internal/status"
 	"github.com/onflow/flow-cli/internal/transactions"
 	"github.com/onflow/flow-cli/internal/version"
@@ -69,6 +70,7 @@ func main() {
 	cmd.AddCommand(config.Cmd)
 	cmd.AddCommand(app.Cmd)
 	cmd.AddCommand(signatures.Cmd)
+	cmd.AddCommand(snapshot.Cmd)
 
 	command.InitFlags(cmd)
 	// Set usage template to custom template
diff --git a/docs/snapshot-save.md b/docs/snapshot-save.md
new file mode 100644
index 00000000..af4292bc
--- /dev/null
+++ b/docs/snapshot-save.md
@@ -0,0 +1,72 @@
+---
+title: Save a protocol snapshot with the FLOW CLI
+sidebar_title: Snapshot Save
+description: How to save a protocol snapshot from the command line
+---
+
+The FLOW CLI provides a command to save the latest finalized protocol state snapshot
+
+```shell
+flow snapshot save <output path>
+```
+
+## Example Usage
+
+```shell
+flow snapshot save  /tmp/snapshot.json --network testnet
+```
+
+### Example response
+```shell
+snapshot saved: /tmp/snapshot.json
+```
+
+## Arguments
+
+### Output Path
+- Name: `output path`
+- Valid Input: any valid string path
+
+Output path where the protocol snapshot JSON file will be saved.
+
+## Flags
+
+
+### Host
+- Flag: `--host`
+- Valid inputs: an IP address or hostname.
+- Default: `127.0.0.1:3569` (Flow Emulator)
+
+Specify the hostname of the Access API that will be
+used to execute the commands.
+
+### Network
+
+- Flag: `--network`
+- Short Flag: `-n`
+- Valid inputs: the name of a network defined in the configuration (`flow.json`)
+- Default: `emulator`
+
+Specify which network you want the command to use for execution.
+
+### Filter
+
+- Flag: `--filter`
+- Short Flag: `-x`
+- Valid inputs: case-sensitive name of the result property.
+
+Specify any property name from the result you want to return as the only value.
+
+### Output
+
+- Flag: `--output`
+- Short Flag: `-o`
+- Valid inputs: `json`, `inline`
+
+Specify in which format you want to display the result.
+
+
+
+
+
+
diff --git a/internal/snapshot/save.go b/internal/snapshot/save.go
new file mode 100644
index 00000000..084df3ca
--- /dev/null
+++ b/internal/snapshot/save.go
@@ -0,0 +1,66 @@
+/*
+ * Flow CLI
+ *
+ * Copyright 2019-2022 Dapper Labs, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package snapshot
+
+import (
+	"fmt"
+	"path/filepath"
+
+	"github.com/onflow/flow-cli/internal/command"
+	"github.com/onflow/flow-cli/pkg/flowkit"
+	"github.com/onflow/flow-cli/pkg/flowkit/services"
+
+	"github.com/spf13/cobra"
+)
+
+var SaveCommand = &command.Command{
+	Cmd: &cobra.Command{
+		Use:     "save",
+		Short:   "Get the latest finalized protocol snapshot",
+		Example: "flow snapshot save /tmp/snapshot.json",
+	},
+	Flags: &struct{}{},
+	Run:   save,
+}
+
+func save(
+	args []string,
+	readerWriter flowkit.ReaderWriter,
+	_ command.GlobalFlags,
+	services *services.Services,
+) (command.Result, error) {
+	fileName := args[0]
+
+	snapshotBytes, err := services.Snapshot.GetLatestProtocolStateSnapshot()
+	if err != nil {
+		return nil, err
+	}
+
+	outputPath, err := filepath.Abs(fileName)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get absolute output path for protocol snapshot")
+	}
+
+	err = readerWriter.WriteFile(outputPath, snapshotBytes, 0644)
+	if err != nil {
+		return nil, fmt.Errorf("failed to write protocol snapshot file to %s: %w", outputPath, err)
+	}
+
+	return &SaveResult{OutputPath: outputPath}, nil
+}
diff --git a/internal/snapshot/snapshot.go b/internal/snapshot/snapshot.go
new file mode 100644
index 00000000..96e7a517
--- /dev/null
+++ b/internal/snapshot/snapshot.go
@@ -0,0 +1,52 @@
+/*
+ * Flow CLI
+ *
+ * Copyright 2019-2022 Dapper Labs, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package snapshot
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+var Cmd = &cobra.Command{
+	Use:              "snapshot",
+	Short:            "Utilities to download the latest finalized protocol state snapshot",
+	TraverseChildren: true,
+}
+
+func init() {
+	SaveCommand.AddToParent(Cmd)
+}
+
+// SaveResult represents the result of the snapshot save command.
+type SaveResult struct {
+	OutputPath string
+}
+
+func (r *SaveResult) JSON() interface{} {
+	return map[string]string{"path": r.OutputPath}
+}
+
+func (r *SaveResult) String() string {
+	return fmt.Sprintf("snapshot saved: %s", r.OutputPath)
+}
+
+func (r *SaveResult) Oneliner() string {
+	return fmt.Sprintf("snapshot saved: %s", r.OutputPath)
+}
diff --git a/pkg/flowkit/gateway/emulator.go b/pkg/flowkit/gateway/emulator.go
index 74de0700..5ec08859 100644
--- a/pkg/flowkit/gateway/emulator.go
+++ b/pkg/flowkit/gateway/emulator.go
@@ -25,12 +25,13 @@ import (
 	"github.com/onflow/flow-cli/pkg/flowkit"
 	"github.com/onflow/flow-cli/pkg/flowkit/config"
 
-	"github.com/onflow/cadence"
 	emulator "github.com/onflow/flow-emulator"
+	flowGo "github.com/onflow/flow-go/model/flow"
+
+	"github.com/onflow/cadence"
 	"github.com/onflow/flow-go-sdk"
 	"github.com/onflow/flow-go-sdk/client"
 	"github.com/onflow/flow-go-sdk/client/convert"
-	flowGo "github.com/onflow/flow-go/model/flow"
 )
 
 type EmulatorGateway struct {
@@ -202,3 +203,8 @@ func (g *EmulatorGateway) GetBlockByHeight(height uint64) (*flow.Block, error) {
 	block, err := g.emulator.GetBlockByHeight(height)
 	return convertBlock(block), err
 }
+
+// GetLatestProtocolStateSnapshot placeholder func to complete gateway interface implementation
+func (g *EmulatorGateway) GetLatestProtocolStateSnapshot() ([]byte, error) {
+	return []byte{}, nil
+}
diff --git a/pkg/flowkit/gateway/gateway.go b/pkg/flowkit/gateway/gateway.go
index 2c9f3881..046daf82 100644
--- a/pkg/flowkit/gateway/gateway.go
+++ b/pkg/flowkit/gateway/gateway.go
@@ -38,5 +38,6 @@ type Gateway interface {
 	GetBlockByID(flow.Identifier) (*flow.Block, error)
 	GetEvents(string, uint64, uint64) ([]client.BlockEvents, error)
 	GetCollection(flow.Identifier) (*flow.Collection, error)
+	GetLatestProtocolStateSnapshot() ([]byte, error)
 	Ping() error
 }
diff --git a/pkg/flowkit/gateway/grpc.go b/pkg/flowkit/gateway/grpc.go
index ce7a7221..0c572eea 100644
--- a/pkg/flowkit/gateway/grpc.go
+++ b/pkg/flowkit/gateway/grpc.go
@@ -154,6 +154,11 @@ func (g *GrpcGateway) GetCollection(id flow.Identifier) (*flow.Collection, error
 	return g.client.GetCollection(g.ctx, id)
 }
 
+// GetLatestProtocolStateSnapshot gets the latest finalized protocol state snapshot
+func (g *GrpcGateway) GetLatestProtocolStateSnapshot() ([]byte, error) {
+	return g.client.GetLatestProtocolStateSnapshot(g.ctx)
+}
+
 // Ping is used to check if the access node is alive and healthy.
 func (g *GrpcGateway) Ping() error {
 	return g.client.Ping(g.ctx)
diff --git a/pkg/flowkit/services/services.go b/pkg/flowkit/services/services.go
index 96e3b3a9..4cbc88f7 100644
--- a/pkg/flowkit/services/services.go
+++ b/pkg/flowkit/services/services.go
@@ -36,6 +36,7 @@ type Services struct {
 	Project      *Project
 	Blocks       *Blocks
 	Status       *Status
+	Snapshot     *Snapshot
 }
 
 // NewServices returns a new services collection for a state,
@@ -55,5 +56,6 @@ func NewServices(
 		Project:      NewProject(gateway, state, logger),
 		Blocks:       NewBlocks(gateway, state, logger),
 		Status:       NewStatus(gateway, state, logger),
+		Snapshot:     NewSnapshot(gateway, state, logger),
 	}
 }
diff --git a/pkg/flowkit/services/snapshot.go b/pkg/flowkit/services/snapshot.go
new file mode 100644
index 00000000..63d40d9f
--- /dev/null
+++ b/pkg/flowkit/services/snapshot.go
@@ -0,0 +1,61 @@
+/*
+ * Flow CLI
+ *
+ * Copyright 2019-2022 Dapper Labs, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package services
+
+import (
+	"fmt"
+
+	"github.com/onflow/flow-cli/pkg/flowkit"
+	"github.com/onflow/flow-cli/pkg/flowkit/gateway"
+	"github.com/onflow/flow-cli/pkg/flowkit/output"
+)
+
+// Snapshot is a service that handles downloading the latest finalized root protocol snapshot from the gateway.
+type Snapshot struct {
+	gateway gateway.Gateway
+	state   *flowkit.State
+	logger  output.Logger
+}
+
+// NewSnapshot returns a new snapshot service.
+func NewSnapshot(
+	gateway gateway.Gateway,
+	state *flowkit.State,
+	logger output.Logger,
+) *Snapshot {
+	return &Snapshot{
+		gateway: gateway,
+		state:   state,
+		logger:  logger,
+	}
+}
+
+// GetLatestProtocolStateSnapshot returns the latest finalized protocol snapshot
+func (s *Snapshot) GetLatestProtocolStateSnapshot() ([]byte, error) {
+	s.logger.StartProgress("Downloading protocol snapshot...")
+
+	b, err := s.gateway.GetLatestProtocolStateSnapshot()
+	if err != nil {
+		return nil, fmt.Errorf("failed to get latest finalized protocol snapshot from gateway: %w", err)
+	}
+
+	s.logger.StopProgress()
+
+	return b, nil
+}


Test patch:
diff --git a/tests/mocks/Gateway.go b/tests/mocks/Gateway.go
index 9ff67c7a..05ff1704 100644
--- a/tests/mocks/Gateway.go
+++ b/tests/mocks/Gateway.go
@@ -1,16 +1,16 @@
-// Code generated by mockery 2.7.5. DO NOT EDIT.
+// Code generated by mockery v1.0.0. DO NOT EDIT.
 
 package mocks
 
 import (
-	"github.com/onflow/cadence"
-	"github.com/stretchr/testify/mock"
-
+	cadence "github.com/onflow/cadence"
 	client "github.com/onflow/flow-go-sdk/client"
 
 	flow "github.com/onflow/flow-go-sdk"
 
 	flowkit "github.com/onflow/flow-cli/pkg/flowkit"
+
+	mock "github.com/stretchr/testify/mock"
 )
 
 // Gateway is an autogenerated mock type for the Gateway type
@@ -179,6 +179,29 @@ func (_m *Gateway) GetLatestBlock() (*flow.Block, error) {
 	return r0, r1
 }
 
+// GetLatestProtocolStateSnapshot provides a mock function with given fields:
+func (_m *Gateway) GetLatestProtocolStateSnapshot() ([]byte, error) {
+	ret := _m.Called()
+
+	var r0 []byte
+	if rf, ok := ret.Get(0).(func() []byte); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]byte)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func() error); ok {
+		r1 = rf()
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
 // GetTransaction provides a mock function with given fields: _a0
 func (_m *Gateway) GetTransaction(_a0 flow.Identifier) (*flow.Transaction, error) {
 	ret := _m.Called(_a0)