Repository: vegaprotocol/vegawallet (go)

Issue:
Return the transaction hash as part of the response for submit tx
The core node now return the transaction hash from tendermin: https://github.com/vegaprotocol/vega/pull/4400.

Let's add this hash in the wallet response.

Pull request:
Return the tx hash after sending a transaction
Close #397 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b75289..335eddf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@
 - [398](https://github.com/vegaprotocol/vegawallet/pull/398) - More idiomatic test suite
 - [396](https://github.com/vegaprotocol/vegawallet/pull/396) - Update the "important" section of `key generate`
 - [405](https://github.com/vegaprotocol/vegawallet/pull/405) - Add command `network delete` to delete a network
+- [409](https://github.com/vegaprotocol/vegawallet/pull/409) - Return the tx hash after sending a transaction
 
 ### 🔥 Removal
 - [387](https://github.com/vegaprotocol/vegawallet/pull/387) - Remove network initialisation in favor of `network import`
diff --git a/cmd/command_send.go b/cmd/command_send.go
index 30ce3e1..91bbedd 100644
--- a/cmd/command_send.go
+++ b/cmd/command_send.go
@@ -270,12 +270,13 @@ func SendCommand(w io.Writer, rf *RootFlags, req *SendCommandRequest) error {
 
 	log.Info("transaction successfully signed", zap.String("signature", tx.Signature.Value))
 
-	if err = forwarder.SendTx(ctx, tx, api.SubmitTransactionRequest_TYPE_ASYNC); err != nil {
+	txHash, err := forwarder.SendTx(ctx, tx, api.SubmitTransactionRequest_TYPE_ASYNC)
+	if err != nil {
 		log.Error("couldn't send transaction", zap.Error(err))
 		return fmt.Errorf("couldn't send transaction: %w", err)
 	}
 
-	log.Info("transaction successfully sent")
+	log.Info("transaction successfully sent", zap.String("hash", txHash))
 
 	return nil
 }
diff --git a/cmd/tx_send.go b/cmd/tx_send.go
index 3e9aec9..1153c79 100644
--- a/cmd/tx_send.go
+++ b/cmd/tx_send.go
@@ -183,7 +183,7 @@ func SendTx(w io.Writer, rf *RootFlags, req *SendTxRequest) error {
 	}
 	defer func() {
 		if err = forwarder.Stop(); err != nil {
-			log.Warn("couldn't stop the forwarder", zap.Error(err))
+			log.Warn("Couldn't stop the forwarder", zap.Error(err))
 		}
 	}()
 
@@ -195,12 +195,13 @@ func SendTx(w io.Writer, rf *RootFlags, req *SendTxRequest) error {
 	ctx, cancelFn := context.WithTimeout(context.Background(), ForwarderRequestTimeout)
 	defer cancelFn()
 
-	if err = forwarder.SendTx(ctx, req.Tx, api.SubmitTransactionRequest_TYPE_ASYNC); err != nil {
-		log.Error("couldn't send transaction", zap.Error(err))
+	txHash, err := forwarder.SendTx(ctx, req.Tx, api.SubmitTransactionRequest_TYPE_ASYNC)
+	if err != nil {
+		log.Error("Couldn't send transaction", zap.Error(err))
 		return fmt.Errorf("couldn't send transaction: %w", err)
 	}
 
-	log.Info("transaction successfully sent")
+	log.Info("transaction successfully sent", zap.String("hash", txHash))
 
 	return nil
 }
diff --git a/node/forwarder.go b/node/forwarder.go
index 8242b3d..b9d5404 100644
--- a/node/forwarder.go
+++ b/node/forwarder.go
@@ -32,6 +32,7 @@ func NewForwarder(log *zap.Logger, nodeConfigs network.GRPCConfig) (*Forwarder,
 	for _, v := range nodeConfigs.Hosts {
 		conn, err := grpc.Dial(v, grpc.WithInsecure())
 		if err != nil {
+			log.Debug("Couldn't dial gRPC host", zap.String("address", v))
 			return nil, err
 		}
 		conns = append(conns, conn)
@@ -48,12 +49,13 @@ func NewForwarder(log *zap.Logger, nodeConfigs network.GRPCConfig) (*Forwarder,
 
 func (n *Forwarder) Stop() error {
 	for i, v := range n.nodeCfgs.Hosts {
-		n.log.Info("closing gRPC client", zap.String("address", v))
+		n.log.Debug("Closing gRPC client", zap.String("address", v))
 		if err := n.conns[i].Close(); err != nil {
-			n.log.Warn("couldn't close gRPC client", zap.Error(err))
+			n.log.Warn("Couldn't close gRPC client", zap.Error(err))
 			return err
 		}
 	}
+	n.log.Info("gRPC clients successfully closed")
 	return nil
 }
 
@@ -66,7 +68,7 @@ func (n *Forwarder) HealthCheck(ctx context.Context) error {
 			if err != nil {
 				return err
 			}
-			n.log.Debug("response from GetVegaTime", zap.Int64("timestamp", resp.Timestamp))
+			n.log.Debug("Response from GetVegaTime", zap.Int64("timestamp", resp.Timestamp))
 			return nil
 		},
 		backoff.WithMaxRetries(backoff.NewExponentialBackOff(), n.nodeCfgs.Retries),
@@ -81,7 +83,7 @@ func (n *Forwarder) LastBlockHeight(ctx context.Context) (uint64, error) {
 			clt := n.nextClt()
 			resp, err := clt.LastBlockHeight(ctx, &req)
 			if err != nil {
-				n.log.Debug("couldn't get last block", zap.Error(err))
+				n.log.Debug("Couldn't get last block", zap.Error(err))
 				return err
 			}
 			height = resp.Height
@@ -91,9 +93,9 @@ func (n *Forwarder) LastBlockHeight(ctx context.Context) (uint64, error) {
 	)
 
 	if err != nil {
-		n.log.Error("couldn't get last block", zap.Error(err))
+		n.log.Error("Couldn't get last block", zap.Error(err))
 	} else {
-		n.log.Debug("last block when sending transaction",
+		n.log.Debug("Last block when sending transaction",
 			zap.Time("request.time", time.Now()),
 			zap.Uint64("block.height", height),
 		)
@@ -102,29 +104,39 @@ func (n *Forwarder) LastBlockHeight(ctx context.Context) (uint64, error) {
 	return height, err
 }
 
-func (n *Forwarder) SendTx(ctx context.Context, tx *commandspb.Transaction, ty api.SubmitTransactionRequest_Type) error {
+func (n *Forwarder) SendTx(ctx context.Context, tx *commandspb.Transaction, ty api.SubmitTransactionRequest_Type) (string, error) {
 	req := api.SubmitTransactionRequest{
 		Tx:   tx,
 		Type: ty,
 	}
-	return backoff.Retry(
+	var resp *api.SubmitTransactionResponse
+	err := backoff.Retry(
 		func() error {
 			clt := n.nextClt()
-			resp, err := clt.SubmitTransaction(ctx, &req)
+			r, err := clt.SubmitTransaction(ctx, &req)
 			if err != nil {
-				n.log.Error("couldn't send transaction", zap.Error(err))
+				n.log.Error("Couldn't send transaction", zap.Error(err))
 				return err
 			}
-			n.log.Debug("response from SubmitTransactionV2", zap.Bool("success", resp.Success))
+			n.log.Debug("Response from SubmitTransaction",
+				zap.Bool("success", r.Success),
+				zap.String("hash", r.TxHash),
+			)
+			resp = r
 			return nil
 		},
 		backoff.WithMaxRetries(backoff.NewExponentialBackOff(), n.nodeCfgs.Retries),
 	)
+	if err != nil {
+		return "", err
+	}
+	return resp.TxHash, nil
 }
 
 func (n *Forwarder) nextClt() api.CoreServiceClient {
 	i := atomic.AddUint64(&n.next, 1)
-	n.log.Info("sending transaction to Vega node",
-		zap.String("host", n.nodeCfgs.Hosts[(int(i)-1)%len(n.clts)]))
+	n.log.Info("Sending transaction to Vega node",
+		zap.String("host", n.nodeCfgs.Hosts[(int(i)-1)%len(n.clts)]),
+	)
 	return n.clts[(int(i)-1)%len(n.clts)]
 }
diff --git a/service/mocks/node_forward_mock.go b/service/mocks/node_forward_mock.go
index 844eecc..88980cb 100644
--- a/service/mocks/node_forward_mock.go
+++ b/service/mocks/node_forward_mock.go
@@ -65,11 +65,12 @@ func (mr *MockNodeForwardMockRecorder) LastBlockHeight(arg0 interface{}) *gomock
 }
 
 // SendTx mocks base method
-func (m *MockNodeForward) SendTx(arg0 context.Context, arg1 *v10.Transaction, arg2 v1.SubmitTransactionRequest_Type) error {
+func (m *MockNodeForward) SendTx(arg0 context.Context, arg1 *v10.Transaction, arg2 v1.SubmitTransactionRequest_Type) (string, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "SendTx", arg0, arg1, arg2)
-	ret0, _ := ret[0].(error)
-	return ret0
+	ret0, _ := ret[0].(string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
 }
 
 // SendTx indicates an expected call of SendTx
diff --git a/service/service.go b/service/service.go
index 78b2e07..2de11f2 100644
--- a/service/service.go
+++ b/service/service.go
@@ -21,7 +21,6 @@ import (
 	"code.vegaprotocol.io/vegawallet/wallet"
 
 	"github.com/golang/protobuf/jsonpb"
-	"github.com/golang/protobuf/proto"
 	"github.com/julienschmidt/httprouter"
 	"github.com/rs/cors"
 	"go.uber.org/zap"
@@ -405,7 +404,7 @@ type Auth interface {
 // NodeForward ...
 //go:generate go run github.com/golang/mock/mockgen -destination mocks/node_forward_mock.go -package mocks code.vegaprotocol.io/vegawallet/service NodeForward
 type NodeForward interface {
-	SendTx(context.Context, *commandspb.Transaction, api.SubmitTransactionRequest_Type) error
+	SendTx(context.Context, *commandspb.Transaction, api.SubmitTransactionRequest_Type) (string, error)
 	HealthCheck(context.Context) error
 	LastBlockHeight(context.Context) (uint64, error)
 }
@@ -741,26 +740,34 @@ func (s *Service) signTx(token string, w http.ResponseWriter, r *http.Request, _
 		return
 	}
 
-	if req.Propagate {
-		if err := s.nodeForward.SendTx(r.Context(), tx, ty); err != nil {
-			if st, ok := status.FromError(err); ok {
-				var details []string
-				for _, v := range st.Details() {
-					v, ok := v.(*typespb.ErrorDetail)
-					if !ok {
-						s.writeError(w, newErrorResponse(fmt.Sprintf("couldn't cast status details to error details: %v", v)), http.StatusInternalServerError)
-					}
-					details = append(details, v.Message)
+	if !req.Propagate {
+		s.writeSuccess(w, nil)
+		return
+	}
+
+	txHash, err := s.nodeForward.SendTx(r.Context(), tx, ty)
+	if err != nil {
+		if st, ok := status.FromError(err); ok {
+			var details []string
+			for _, v := range st.Details() {
+				v, ok := v.(*typespb.ErrorDetail)
+				if !ok {
+					s.writeError(w, newErrorResponse(fmt.Sprintf("couldn't cast status details to error details: %v", v)), http.StatusInternalServerError)
 				}
-				s.writeError(w, newErrorWithDetails(err.Error(), details), http.StatusInternalServerError)
-			} else {
-				s.writeInternalError(w, err)
+				details = append(details, v.Message)
 			}
-			return
+			s.writeError(w, newErrorWithDetails(err.Error(), details), http.StatusInternalServerError)
+		} else {
+			s.writeInternalError(w, err)
 		}
+		return
 	}
 
-	s.writeSuccessProto(w, tx, http.StatusOK)
+	s.writeSuccess(w, struct {
+		TxHash string `json:"txHash"`
+	}{
+		TxHash: txHash,
+	})
 }
 
 func (s *Service) Version(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
@@ -871,19 +878,6 @@ func (s *Service) writeSuccess(w http.ResponseWriter, data interface{}) {
 	s.log.Info(fmt.Sprintf("%d %s", http.StatusOK, http.StatusText(http.StatusOK)))
 }
 
-func (s *Service) writeSuccessProto(w http.ResponseWriter, data proto.Message, status int) {
-	w.Header().Set("Content-Type", "application/json")
-	w.WriteHeader(status)
-
-	marshaller := jsonpb.Marshaler{}
-	if err := marshaller.Marshal(w, data); err != nil {
-		s.log.Error("couldn't marshal proto message", zap.Error(err))
-		s.writeInternalError(w, fmt.Errorf("couldn't marshal proto message: %w", err))
-		return
-	}
-	s.log.Info(fmt.Sprintf("%d %s", status, http.StatusText(status)))
-}
-
 func (s *Service) handle(method string, path string, handle httprouter.Handle) {
 	loggedEndpoint := func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
 		s.log.Info(fmt.Sprintf("--> %s %s", method, path))


Test patch:
diff --git a/service/service_test.go b/service/service_test.go
index 2a0b403..4850748 100644
--- a/service/service_test.go
+++ b/service/service_test.go
@@ -733,7 +733,7 @@ func testSigningTransactionWithFailedPropagationFails(t *testing.T) {
 	// setup
 	s.auth.EXPECT().VerifyToken(token).Times(1).Return(walletName, nil)
 	s.handler.EXPECT().SignTx(walletName, gomock.Any(), gomock.Any()).Times(1).Return(&commandspb.Transaction{}, nil)
-	s.nodeForward.EXPECT().SendTx(gomock.Any(), &commandspb.Transaction{}, api.SubmitTransactionRequest_TYPE_ASYNC).Times(1).Return(assert.AnError)
+	s.nodeForward.EXPECT().SendTx(gomock.Any(), &commandspb.Transaction{}, api.SubmitTransactionRequest_TYPE_ASYNC).Times(1).Return("", assert.AnError)
 	s.nodeForward.EXPECT().LastBlockHeight(gomock.Any()).Times(1).Return(uint64(42), nil)
 
 	// when