Repository: vegaprotocol/vegawallet
Pull request #562 (2022-05-16 08:05:41)

Release version 0.15.0
Release version 0.15.0


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 97988cc..d1235fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
 # Changelog
 
-## Unreleased (0.15.0)
+## Unreleased (0.16.0)
 
 ### 🚨 Breaking changes
 - [](https://github.com/vegaprotocol/vegawallet/pull/) -
@@ -17,10 +17,19 @@
 ### 🐛 Fixes
 - [](https://github.com/vegaprotocol/vegawallet/pull/) -
 
+## 0.15.0
+
+### 🛠  Improvements
+- [555](https://github.com/vegaprotocol/vegawallet/issues/555) - Now using full transaction structures for event emit
+- [551](https://github.com/vegaprotocol/vegawallet/pull/551) - Enable system-tests with Vegacapsule
+
+### 🐛 Fixes
+- [548](https://github.com/vegaprotocol/vegawallet/pull/548) - Printer utility shouldn't write in service response
+
 ## 0.14.2
 
 ### 🛠  Improvements
-- [544](https://github.com/vegaprotocol/vegawallet/issues/544) - Display transaction hash after sending transaction
+- [544](https://github.com/vegaprotocol/vegagwallet/issues/544) - Display transaction hash after sending transaction
 
 ## 0.14.1
 
diff --git a/Jenkinsfile b/Jenkinsfile
index a7b6d76..e1c9fc6 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -162,10 +162,10 @@ pipeline {
                         '''
                     }
                 }
-                stage('System Tests') {
+                stage('LNL System Tests') {
                     steps {
                         script {
-                            systemTests ignoreFailure: !isPRBuild(),
+                            systemTestsLNL ignoreFailure: !isPRBuild(),
                                 vegaCore: params.VEGA_CORE_BRANCH,
                                 dataNode: params.DATA_NODE_BRANCH,
                                 vegawallet: commitHash,
@@ -177,18 +177,18 @@ pipeline {
                         }
                     }
                 }
-                stage('LNL System Tests') {
+                stage('Capsule System Tests') {
                     steps {
                         script {
-                            systemTestsLNL ignoreFailure: !isPRBuild(),
-                                vegaCore: params.VEGA_CORE_BRANCH,
+                            systemTestsCapsule vegaCore: params.VEGA_CORE_BRANCH,
                                 dataNode: params.DATA_NODE_BRANCH,
                                 vegawallet: commitHash,
-                                ethereumEventForwarder: params.ETHEREUM_EVENT_FORWARDER_BRANCH,
                                 devopsInfra: params.DEVOPS_INFRA_BRANCH,
                                 vegatools: params.VEGATOOLS_BRANCH,
                                 systemTests: params.SYSTEM_TESTS_BRANCH,
-                                protos: params.PROTOS_BRANCH
+                                protos: params.PROTOS_BRANCH,
+                                ignoreFailure: !isPRBuild()
+
                         }
                     }
                 }
diff --git a/cmd/service_run.go b/cmd/service_run.go
index a7d250b..84ab1c6 100644
--- a/cmd/service_run.go
+++ b/cmd/service_run.go
@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"os"
 	"os/signal"
+	"strings"
 	"syscall"
 
 	vgterm "code.vegaprotocol.io/shared/libs/term"
@@ -209,6 +210,7 @@ func RunService(w io.Writer, rf *RootFlags, f *RunServiceFlags) error {
 	}
 
 	pendingConsents := make(chan service.ConsentRequest, 1)
+	sentTxs := make(chan service.SentTransaction)
 
 	cliLog, cliLogPath, err := BuildJSONLogger(cfg.Level.String(), vegaPaths, paths.WalletCLILogsHome)
 	if err != nil {
@@ -226,7 +228,7 @@ func RunService(w io.Writer, rf *RootFlags, f *RunServiceFlags) error {
 			policy = service.NewAutomaticConsentPolicy()
 		} else {
 			cliLog.Info("Explicit consent enabled")
-			policy = service.NewExplicitConsentPolicy(pendingConsents)
+			policy = service.NewExplicitConsentPolicy(pendingConsents, sentTxs)
 		}
 	} else {
 		cliLog.Info("No TTY detected")
@@ -294,7 +296,7 @@ func RunService(w io.Writer, rf *RootFlags, f *RunServiceFlags) error {
 		p.NextLine()
 	}
 
-	waitSig(ctx, cancel, cliLog, pendingConsents, p)
+	waitSig(ctx, cancel, cliLog, pendingConsents, sentTxs, p)
 
 	return nil
 }
@@ -364,7 +366,7 @@ func startTokenDApp(log *zap.Logger, f *RunServiceFlags, cfg *network.Network, c
 }
 
 // waitSig will wait for a sigterm or sigint interrupt.
-func waitSig(ctx context.Context, cfunc func(), log *zap.Logger, pendingSigRequests chan service.ConsentRequest, p *printer.InteractivePrinter) {
+func waitSig(ctx context.Context, cfunc func(), log *zap.Logger, pendingSigRequests chan service.ConsentRequest, sentTxs chan service.SentTransaction, p *printer.InteractivePrinter) {
 	gracefulStop := make(chan os.Signal, 1)
 
 	signal.Notify(gracefulStop, syscall.SIGTERM)
@@ -393,7 +395,19 @@ func waitSig(ctx context.Context, cfunc func(), log *zap.Logger, pendingSigReque
 			if flags.DoYouApproveTx() {
 				log.Info("user approved the signing of the transaction", zap.Any("transaction", txStr))
 				signRequest.Confirmations <- service.ConsentConfirmation{Decision: true, TxStr: txStr}
-				p.CheckMark().SuccessText("Transaction approved").NextSection()
+				p.CheckMark().SuccessText("Transaction approved").NextLine()
+
+				sentTx := <-sentTxs
+				log.Info("transaction sent", zap.Any("ID", sentTx.TxID), zap.Any("hash", sentTx.TxHash))
+				if sentTx.Error != nil {
+					log.Error("transaction failed", zap.Any("transaction", txStr))
+					p.BangMark().DangerText("Transaction failed! ").NextLine()
+					p.BangMark().DangerText("Error: ").DangerText(sentTx.Error.Error()).NextLine()
+					p.BangMark().DangerText("Details: ").DangerText(strings.Join(sentTx.ErrorDetails, " ,")).NextSection()
+				} else {
+					log.Info("transaction sent", zap.Any("hash", sentTx.TxHash))
+					p.CheckMark().Text("Transaction with hash ").SuccessText(sentTx.TxHash).Text(" sent!").NextSection()
+				}
 			} else {
 				log.Info("user rejected the signing of the transaction", zap.Any("transaction", txStr))
 				signRequest.Confirmations <- service.ConsentConfirmation{Decision: false, TxStr: txStr}
diff --git a/cmd/tx_send.go b/cmd/tx_send.go
index e505c08..21c8060 100644
--- a/cmd/tx_send.go
+++ b/cmd/tx_send.go
@@ -205,7 +205,7 @@ func SendTx(w io.Writer, rf *RootFlags, req *SendTxRequest) error {
 
 	log.Info("transaction successfully sent", zap.String("hash", txHash))
 	if rf.Output == flags.InteractiveOutput {
-		p.CheckMark().InfoText("Transaction sent:").SuccessText(txHash).NextLine()
+		p.NextLine().CheckMark().Text("Transaction sent: ").SuccessText(txHash).NextLine()
 	}
 
 	return nil
diff --git a/go.mod b/go.mod
index 92bd2e9..badfb2e 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module code.vegaprotocol.io/vegawallet
 go 1.17
 
 require (
-	code.vegaprotocol.io/protos v0.50.3
+	code.vegaprotocol.io/protos v0.51.0
 	code.vegaprotocol.io/shared v0.0.0-20220321185018-3b5684b00533
 	github.com/blang/semver/v4 v4.0.0
 	github.com/cenkalti/backoff/v4 v4.1.2
diff --git a/go.sum b/go.sum
index 4f059b1..ea68477 100644
--- a/go.sum
+++ b/go.sum
@@ -30,8 +30,10 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-code.vegaprotocol.io/protos v0.50.3 h1:9gSw3SDnvZ8oaPooEl2Mg7MbfMroKwutqMTvaLTD5NE=
-code.vegaprotocol.io/protos v0.50.3/go.mod h1:4BqwDw6jhc/mnwbXq8ZFUtYBFCnk8tBW6zuPsBt8OrQ=
+code.vegaprotocol.io/protos v0.50.4-0.20220503163151-7fc5916bf54e h1:8r9buW3Cl2aBkZj37Yuboq2Pj+pxcmXSRXI8lc3Z7Sk=
+code.vegaprotocol.io/protos v0.50.4-0.20220503163151-7fc5916bf54e/go.mod h1:4BqwDw6jhc/mnwbXq8ZFUtYBFCnk8tBW6zuPsBt8OrQ=
+code.vegaprotocol.io/protos v0.51.0 h1:HscwExZenysYze9N43JTqR1mStpUB82Hb48oY+lHX8M=
+code.vegaprotocol.io/protos v0.51.0/go.mod h1:4BqwDw6jhc/mnwbXq8ZFUtYBFCnk8tBW6zuPsBt8OrQ=
 code.vegaprotocol.io/shared v0.0.0-20220321185018-3b5684b00533 h1:IxTWvyF0i0AgUAS+FTgQKIpbsS6Ki/Y9Ug0GSlgChJw=
 code.vegaprotocol.io/shared v0.0.0-20220321185018-3b5684b00533/go.mod h1:pNHKwqRDkotUN0s6jErl7Eb2EpZa2HAf03lyPMTCUT0=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
diff --git a/node/forwarder.go b/node/forwarder.go
index 9a4bde4..8ff86da 100644
--- a/node/forwarder.go
+++ b/node/forwarder.go
@@ -108,6 +108,34 @@ func (n *Forwarder) LastBlockHeightAndHash(ctx context.Context) (*api.LastBlockH
 	return resp, clt, err
 }
 
+func (n *Forwarder) CheckTx(ctx context.Context, tx *commandspb.Transaction, cltIdx int) (*api.CheckTransactionResponse, error) {
+	req := api.CheckTransactionRequest{
+		Tx: tx,
+	}
+	var resp *api.CheckTransactionResponse
+	if cltIdx < 0 {
+		cltIdx = n.nextClt()
+	}
+	err := backoff.Retry(
+		func() error {
+			clt := n.clts[cltIdx]
+			r, err := clt.CheckTransaction(ctx, &req)
+			if err != nil {
+				n.log.Error("Couldn't check transaction", zap.Error(err))
+				return err
+			}
+			n.log.Debug("Response from CheckTransaction",
+				zap.Bool("success", r.Success),
+			)
+			resp = r
+			return nil
+		},
+		backoff.WithMaxRetries(backoff.NewExponentialBackOff(), n.nodeCfgs.Retries),
+	)
+
+	return resp, err
+}
+
 func (n *Forwarder) SendTx(ctx context.Context, tx *commandspb.Transaction, ty api.SubmitTransactionRequest_Type, cltIdx int) (string, error) {
 	req := api.SubmitTransactionRequest{
 		Tx:   tx,
diff --git a/service/mocks/node_forward_mock.go b/service/mocks/node_forward_mock.go
index 618f65a..92cf43c 100644
--- a/service/mocks/node_forward_mock.go
+++ b/service/mocks/node_forward_mock.go
@@ -66,6 +66,15 @@ func (mr *MockNodeForwardMockRecorder) LastBlockHeightAndHash(arg0 interface{})
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LastBlockHeightAndHash", reflect.TypeOf((*MockNodeForward)(nil).LastBlockHeightAndHash), arg0)
 }
 
+// CheckTx mocks base method.
+func (m *MockNodeForward) CheckTx(arg0 context.Context, arg1 *v10.Transaction, arg2 int) (*v1.CheckTransactionResponse, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CheckTx", arg0, arg1, arg2)
+	ret0, _ := ret[0].(*v1.CheckTransactionResponse)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
 // SendTx mocks base method.
 func (m *MockNodeForward) SendTx(arg0 context.Context, arg1 *v10.Transaction, arg2 v1.SubmitTransactionRequest_Type, arg3 int) (string, error) {
 	m.ctrl.T.Helper()
@@ -75,6 +84,12 @@ func (m *MockNodeForward) SendTx(arg0 context.Context, arg1 *v10.Transaction, ar
 	return ret0, ret1
 }
 
+// CheckTx indicates an expected call of CheckTx.
+func (mr *MockNodeForwardMockRecorder) CheckTx(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckTx", reflect.TypeOf((*MockNodeForward)(nil).CheckTx), arg0, arg1, arg2)
+}
+
 // SendTx indicates an expected call of SendTx.
 func (mr *MockNodeForwardMockRecorder) SendTx(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
diff --git a/service/mocks/policies_mock.go b/service/mocks/policies_mock.go
index 924e988..6ed0daa 100644
--- a/service/mocks/policies_mock.go
+++ b/service/mocks/policies_mock.go
@@ -1,21 +1,29 @@
 package mocks
 
 import (
+	"time"
+
 	v1 "code.vegaprotocol.io/protos/vega/wallet/v1"
 	"code.vegaprotocol.io/vegawallet/service"
 )
 
 type MockConsentPolicy struct {
 	pendingEvents chan service.ConsentRequest
+	sentTxs       chan service.SentTransaction
+}
+
+func (p *MockConsentPolicy) Report(tx service.SentTransaction) {
+	p.sentTxs <- tx
 }
 
-func NewMockConsentPolicy(pending chan service.ConsentRequest) service.Policy {
+func NewMockConsentPolicy(pending chan service.ConsentRequest, sentTxs chan service.SentTransaction) *MockConsentPolicy {
 	return &MockConsentPolicy{
 		pendingEvents: pending,
+		sentTxs:       sentTxs,
 	}
 }
 
-func (p *MockConsentPolicy) Ask(tx *v1.SubmitTransactionRequest) (bool, error) {
+func (p *MockConsentPolicy) Ask(tx *v1.SubmitTransactionRequest, txID string, receivedAt time.Time) (bool, error) {
 	if tx.PubKey == "toBeDeclined" {
 		return false, nil
 	}
diff --git a/service/mocks/wallet_handler_mock.go b/service/mocks/wallet_handler_mock.go
index d2699c2..f653510 100644
--- a/service/mocks/wallet_handler_mock.go
+++ b/service/mocks/wallet_handler_mock.go
@@ -159,6 +159,12 @@ func (m *MockWalletHandler) SignTx(arg0 string, arg1 *v10.SubmitTransactionReque
 	return ret0, ret1
 }
 
+// SignTx indicates an expected call of CheckTx
+func (mr *MockWalletHandlerMockRecorder) CheckTx(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckTx", reflect.TypeOf((*MockWalletHandler)(nil).SignTx), arg0, arg1, arg2)
+}
+
 // SignTx indicates an expected call of SignTx
 func (mr *MockWalletHandlerMockRecorder) SignTx(arg0, arg1, arg2 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
diff --git a/service/policies.go b/service/policies.go
index 5a47d7a..b9306df 100644
--- a/service/policies.go
+++ b/service/policies.go
@@ -1,10 +1,9 @@
 package service
 
 import (
-	"crypto/sha256"
-	"fmt"
 	"time"
 
+	commandspb "code.vegaprotocol.io/protos/vega/commands/v1"
 	v1 "code.vegaprotocol.io/protos/vega/wallet/v1"
 	"github.com/golang/protobuf/jsonpb"
 )
@@ -27,15 +26,18 @@ func (r *ConsentRequest) String() (string, error) {
 	return marshalledRequest, err
 }
 
-func (r *ConsentRequest) GetTxID() string {
-	h := sha256.New()
-	h.Write([]byte(fmt.Sprintf("%s%v%t", r.Tx.PubKey, r.Tx.Command, r.Tx.Propagate)))
-
-	return fmt.Sprintf("%x", h.Sum(nil))
+type SentTransaction struct {
+	TxHash       string
+	TxID         string
+	ReceivedAt   time.Time
+	Tx           *commandspb.Transaction
+	Error        error
+	ErrorDetails []string
 }
 
 type Policy interface {
-	Ask(tx *v1.SubmitTransactionRequest) (bool, error)
+	Ask(tx *v1.SubmitTransactionRequest, txID string, receivedAt time.Time) (bool, error)
+	Report(tx SentTransaction)
 	NeedsInteractiveOutput() bool
 }
 
@@ -45,34 +47,44 @@ func NewAutomaticConsentPolicy() Policy {
 	return &AutomaticConsentPolicy{}
 }
 
-func (p *AutomaticConsentPolicy) Ask(_ *v1.SubmitTransactionRequest) (bool, error) {
+func (p *AutomaticConsentPolicy) Ask(_ *v1.SubmitTransactionRequest, txID string, receivedAt time.Time) (bool, error) {
 	return true, nil
 }
 
+func (p *AutomaticConsentPolicy) Report(_ SentTransaction) {
+	// Nothing to report as we expect this policy to be non-interactive.
+}
+
 func (p *AutomaticConsentPolicy) NeedsInteractiveOutput() bool {
 	return false
 }
 
 type ExplicitConsentPolicy struct {
 	pendingEvents chan ConsentRequest
+	sentTxs       chan SentTransaction
 }
 
-func NewExplicitConsentPolicy(pending chan ConsentRequest) Policy {
+func NewExplicitConsentPolicy(pending chan ConsentRequest, sentTxs chan SentTransaction) Policy {
 	return &ExplicitConsentPolicy{
 		pendingEvents: pending,
+		sentTxs:       sentTxs,
 	}
 }
 
-func (p *ExplicitConsentPolicy) Ask(tx *v1.SubmitTransactionRequest) (bool, error) {
+func (p *ExplicitConsentPolicy) Ask(tx *v1.SubmitTransactionRequest, txID string, receivedAt time.Time) (bool, error) {
 	confirmations := make(chan ConsentConfirmation)
-	consentReq := ConsentRequest{Tx: tx, Confirmations: confirmations, ReceivedAt: time.Now()}
-	consentReq.TxID = consentReq.GetTxID()
+	consentReq := ConsentRequest{Tx: tx, Confirmations: confirmations, ReceivedAt: receivedAt}
+	consentReq.TxID = txID
 	p.pendingEvents <- consentReq
 
 	c := <-confirmations
 	return c.Decision, nil
 }
 
+func (p *ExplicitConsentPolicy) Report(tx SentTransaction) {
+	p.sentTxs <- tx
+}
+
 func (p *ExplicitConsentPolicy) NeedsInteractiveOutput() bool {
 	return true
 }
diff --git a/service/service.go b/service/service.go
index 45ff7fc..08ddb6f 100644
--- a/service/service.go
+++ b/service/service.go
@@ -9,6 +9,9 @@ import (
 	"fmt"
 	"io/ioutil"
 	"net/http"
+	"time"
+
+	vgrand "code.vegaprotocol.io/shared/libs/rand"
 
 	"code.vegaprotocol.io/protos/commands"
 	typespb "code.vegaprotocol.io/protos/vega"
@@ -16,7 +19,6 @@ import (
 	commandspb "code.vegaprotocol.io/protos/vega/commands/v1"
 	walletpb "code.vegaprotocol.io/protos/vega/wallet/v1"
 	vgcrypto "code.vegaprotocol.io/shared/libs/crypto"
-	"code.vegaprotocol.io/vegawallet/cmd/printer"
 	wcommands "code.vegaprotocol.io/vegawallet/commands"
 	"code.vegaprotocol.io/vegawallet/network"
 	"code.vegaprotocol.io/vegawallet/version"
@@ -47,6 +49,8 @@ type CreateWalletRequest struct {
 	Passphrase string `json:"passphrase"`
 }
 
+const TXIDLENGTH = 20
+
 func ParseCreateWalletRequest(r *http.Request) (*CreateWalletRequest, commands.Errors) {
 	errs := commands.NewErrors()
 
@@ -325,7 +329,9 @@ func ParseVerifyAnyRequest(r *http.Request) (*VerifyAnyRequest, commands.Errors)
 func ParseSubmitTransactionRequest(r *http.Request) (*walletpb.SubmitTransactionRequest, commands.Errors) {
 	errs := commands.NewErrors()
 
-	req := &walletpb.SubmitTransactionRequest{}
+	req := &walletpb.SubmitTransactionRequest{
+		Propagate: true,
+	}
 	if err := jsonpb.Unmarshal(r.Body, req); err != nil {
 		return nil, errs.FinalAdd(err)
 	}
@@ -408,6 +414,7 @@ type Auth interface {
 //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, int) (string, error)
+	CheckTx(context.Context, *commandspb.Transaction, int) (*api.CheckTransactionResponse, error)
 	HealthCheck(context.Context) error
 	LastBlockHeightAndHash(context.Context) (*api.LastBlockHeightResponse, int, error)
 }
@@ -444,6 +451,7 @@ func NewService(log *zap.Logger, net *network.Network, h WalletHandler, a Auth,
 
 	s.handle(http.MethodPost, "/api/v1/command", extractToken(s.SignTx))
 	s.handle(http.MethodPost, "/api/v1/command/sync", extractToken(s.SignTxSync))
+	s.handle(http.MethodPost, "/api/v1/command/check", extractToken(s.CheckTx))
 	s.handle(http.MethodPost, "/api/v1/command/commit", extractToken(s.SignTxCommit))
 	s.handle(http.MethodPost, "/api/v1/sign", extractToken(s.SignAny))
 	s.handle(http.MethodPost, "/api/v1/verify", s.VerifyAny)
@@ -708,6 +716,79 @@ func (s *Service) VerifyAny(w http.ResponseWriter, r *http.Request, _ httprouter
 	s.writeSuccess(w, VerifyAnyResponse{Valid: verified})
 }
 
+func (s *Service) CheckTx(token string, w http.ResponseWriter, r *http.Request, p httprouter.Params) {
+	defer r.Body.Close()
+
+	name, err := s.auth.VerifyToken(token)
+	if err != nil {
+		s.writeForbiddenError(w, err)
+		return
+	}
+
+	req, errs := ParseSubmitTransactionRequest(r)
+	if !errs.Empty() {
+		s.writeBadRequest(w, errs)
+		return
+	}
+
+	blockData, cltIdx, err := s.nodeForward.LastBlockHeightAndHash(r.Context())
+	if err != nil {
+		s.writeInternalError(w, ErrCouldNotGetBlockHeight)
+		return
+	}
+
+	tx, err := s.handler.SignTx(name, req, blockData.Height)
+	if err != nil {
+		s.writeInternalError(w, err)
+		return
+	}
+
+	// generate proof of work for the transaction
+	tid := vgcrypto.RandomHash()
+	powNonce, _, err := vgcrypto.PoW(blockData.Hash, tid, uint(blockData.SpamPowDifficulty), vgcrypto.Sha3)
+	if err != nil {
+		s.writeInternalError(w, err)
+		return
+	}
+	tx.Pow = &commandspb.ProofOfWork{
+		Tid:   tid,
+		Nonce: powNonce,
+	}
+
+	result, err := s.nodeForward.CheckTx(r.Context(), tx, cltIdx)
+	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)
+				}
+				details = append(details, v.Message)
+			}
+
+			s.writeError(w, newErrorWithDetails(err.Error(), details), http.StatusInternalServerError)
+		} else {
+			s.writeInternalError(w, err)
+		}
+		return
+	}
+
+	s.writeSuccess(w, struct {
+		Success   bool                    `json:"success"`
+		Code      uint32                  `json:"code"`
+		GasWanted int64                   `json:"gas_wanted"`
+		GasUsed   int64                   `json:"gas_used"`
+		Tx        *commandspb.Transaction `json:"tx"`
+	}{
+		Success:   result.Success,
+		Code:      result.Code,
+		GasWanted: result.GasWanted,
+		GasUsed:   result.GasUsed,
+		Tx:        tx,
+	})
+}
+
 func (s *Service) SignTxSync(token string, w http.ResponseWriter, r *http.Request, p httprouter.Params) {
 	s.signTx(token, w, r, p, api.SubmitTransactionRequest_TYPE_SYNC)
 }
@@ -735,7 +816,9 @@ func (s *Service) signTx(token string, w http.ResponseWriter, r *http.Request, _
 		return
 	}
 
-	approved, err := s.policy.Ask(req)
+	txID := vgrand.RandomStr(TXIDLENGTH)
+	receivedAt := time.Now()
+	approved, err := s.policy.Ask(req, txID, receivedAt)
 	if err != nil {
 		s.log.Panic("failed getting transaction sign request answer", zap.Error(err))
 	}
@@ -759,11 +842,6 @@ func (s *Service) signTx(token string, w http.ResponseWriter, r *http.Request, _
 		return
 	}
 
-	if !req.Propagate {
-		s.writeSuccess(w, nil)
-		return
-	}
-
 	// generate proof of work for the transaction
 	tid := vgcrypto.RandomHash()
 	powNonce, _, err := vgcrypto.PoW(blockData.Hash, tid, uint(blockData.SpamPowDifficulty), vgcrypto.Sha3)
@@ -787,24 +865,43 @@ func (s *Service) signTx(token string, w http.ResponseWriter, r *http.Request, _
 				}
 				details = append(details, v.Message)
 			}
+			s.policy.Report(SentTransaction{
+				Tx:           tx,
+				ReceivedAt:   receivedAt,
+				TxID:         txID,
+				Error:        err,
+				ErrorDetails: details,
+			})
 			s.writeError(w, newErrorWithDetails(err.Error(), details), http.StatusInternalServerError)
 		} else {
+			s.policy.Report(SentTransaction{
+				Tx:         tx,
+				ReceivedAt: receivedAt,
+				TxID:       txID,
+				Error:      err,
+			})
 			s.writeInternalError(w, err)
 		}
 		return
 	}
 
-	if s.policy.NeedsInteractiveOutput() {
-		p := printer.NewInteractivePrinter(w)
-		p.CheckMark().InfoText("Transaction sent:").SuccessText(txHash).NextLine()
-	}
+	s.policy.Report(SentTransaction{
+		TxHash:     txHash,
+		ReceivedAt: receivedAt,
+		TxID:       txID,
+		Tx:         tx,
+	})
 
 	s.writeSuccess(w, struct {
-		TxHash string                  `json:"txHash"`
-		Tx     *commandspb.Transaction `json:"tx"`
+		TxHash     string                  `json:"txHash"`
+		ReceivedAt time.Time               `json:"receivedAt"`
+		TxID       string                  `json:"txId"`
+		Tx         *commandspb.Transaction `json:"tx"`
 	}{
-		TxHash: txHash,
-		Tx:     tx,
+		TxHash:     txHash,
+		ReceivedAt: receivedAt,
+		TxID:       txID,
+		Tx:         tx,
 	})
 }
 
diff --git a/version/version.go b/version/version.go
index 2a9963f..660db4a 100644
--- a/version/version.go
+++ b/version/version.go
@@ -10,7 +10,7 @@ const (
 	ReleasesAPI        = "https://api.github.com/repos/vegaprotocol/vegawallet/releases"
 	ReleasesURL        = "https://github.com/vegaprotocol/vegawallet/releases"
 	defaultVersionHash = "unknown"
-	defaultVersion     = "v0.14.2"
+	defaultVersion     = "v0.15.0"
 )
 
 var (


Test patch:
diff --git a/service/service_test.go b/service/service_test.go
index d230707..fcf03b4 100644
--- a/service/service_test.go
+++ b/service/service_test.go
@@ -52,13 +52,14 @@ func getTestService(t *testing.T, consentPolicy string) *testService {
 	nodeForward := mocks.NewMockNodeForward(ctrl)
 
 	pendingConsents := make(chan service.ConsentRequest, 1)
+	sentTxs := make(chan service.SentTransaction, 1)
 
 	var policy service.Policy
 	switch consentPolicy {
 	case "automatic":
 		policy = service.NewAutomaticConsentPolicy()
 	case "manual":
-		policy = mocks.NewMockConsentPolicy(pendingConsents)
+		policy = mocks.NewMockConsentPolicy(pendingConsents, sentTxs)
 	default:
 		t.Fatalf("unknown consent policy: %s", consentPolicy)
 	}
@@ -98,9 +99,11 @@ func TestService(t *testing.T) {
 	t.Run("taint fail invalid request", testServiceTaintFailInvalidRequest)
 	t.Run("update metadata", testServiceUpdateMetaOK)
 	t.Run("update metadata invalid request", testServiceUpdateMetaFailInvalidRequest)
-	t.Run("Signing transaction automatically succeeds", testSigningTransactionSucceeds)
-	t.Run("Signing transaction manually succeeds", testAcceptSigningTransactionManuallySucceeds)
+	t.Run("Signing transaction succeeds", testAcceptSigningTransactionSucceeds)
 	t.Run("using ask function in policy succeeds", testAskingConsentPolicySucceeds)
+	t.Run("Checking transaction succeeds", testCheckTransactionSucceeds)
+	t.Run("Checking transaction with rejected transaction succeeds", testCheckTransactionWithRejectedTransactionSucceeds)
+	t.Run("Checking transaction with failed transaction fails", testCheckTransactionWithFailedTransactionFails)
 	t.Run("Decline signing transaction manually succeeds", testDeclineSigningTransactionManuallySucceeds)
 	t.Run("Signing transaction with propagation succeeds", testSigningTransactionWithPropagationSucceeds)
 	t.Run("Signing transaction with failed propagation fails", testSigningTransactionWithFailedPropagationFails)
@@ -694,35 +697,85 @@ func testServiceUpdateMetaFailInvalidRequest(t *testing.T) {
 	}
 }
 
-func testSigningTransactionSucceeds(t *testing.T) {
-	s := getTestService(t, "automatic")
+func testCheckTransactionSucceeds(t *testing.T) {
+	s := getTestService(t, "manual")
 	defer s.ctrl.Finish()
 
 	// given
 	walletName := vgrand.RandomStr(5)
 	token := vgrand.RandomStr(5)
 	headers := authHeaders(t, token)
-	payload := fmt.Sprintf(`{"pubKey": "%s", "orderCancellation": {}}`, vgrand.RandomStr(5))
+	pubKey := vgrand.RandomStr(5)
+	payload := fmt.Sprintf(`{"pubKey": "%s", "orderCancellation": {}}`, pubKey)
 
 	// 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, gomock.Any()).Times(0)
+	s.handler.EXPECT().SignTx(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(&commandspb.Transaction{}, nil)
+	s.nodeForward.EXPECT().CheckTx(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(&api.CheckTransactionResponse{
+		Success:   true,
+		Code:      0,
+		GasWanted: 300,
+		GasUsed:   200,
+	}, nil)
 	s.nodeForward.EXPECT().LastBlockHeightAndHash(gomock.Any()).Times(1).Return(&api.LastBlockHeightResponse{
 		Height:              42,
 		Hash:                "0292041e2f0cf741894503fb3ead4cb817bca2375e543aa70f7c4d938157b5a6",
 		SpamPowDifficulty:   2,
 		SpamPowHashFunction: "sha3_24_rounds",
 	}, 0, nil)
+	// when
+
+	statusCode, body := serveHTTP(t, s, checkTxRequest(t, payload, headers))
+	assert.Equal(t, http.StatusOK, statusCode)
+
+	resp := &api.CheckTransactionResponse{}
+	if err := json.Unmarshal(body, resp); err != nil {
+		t.Fatalf("couldn't unmarshal responde: %v", err)
+	}
+	assert.True(t, resp.Success)
+	assert.Equal(t, uint32(0), resp.Code)
+	assert.Equal(t, int64(300), resp.GasWanted)
+}
+
+func testCheckTransactionWithRejectedTransactionSucceeds(t *testing.T) {
+	s := getTestService(t, "manual")
+	defer s.ctrl.Finish()
 
+	// given
+	walletName := vgrand.RandomStr(5)
+	token := vgrand.RandomStr(5)
+	headers := authHeaders(t, token)
+	pubKey := vgrand.RandomStr(5)
+	payload := fmt.Sprintf(`{"pubKey": "%s", "orderCancellation": {}}`, pubKey)
+
+	// setup
+	s.auth.EXPECT().VerifyToken(token).Times(1).Return(walletName, nil)
+	s.handler.EXPECT().SignTx(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(&commandspb.Transaction{}, nil)
+	s.nodeForward.EXPECT().CheckTx(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(&api.CheckTransactionResponse{
+		Success: false,
+		Code:    4,
+	}, nil)
+	s.nodeForward.EXPECT().LastBlockHeightAndHash(gomock.Any()).Times(1).Return(&api.LastBlockHeightResponse{
+		Height:              42,
+		Hash:                "0292041e2f0cf741894503fb3ead4cb817bca2375e543aa70f7c4d938157b5a6",
+		SpamPowDifficulty:   2,
+		SpamPowHashFunction: "sha3_24_rounds",
+	}, 0, nil)
 	// when
-	statusCode, _ := serveHTTP(t, s, signTxRequest(t, payload, headers))
 
-	// then
+	statusCode, body := serveHTTP(t, s, checkTxRequest(t, payload, headers))
 	assert.Equal(t, http.StatusOK, statusCode)
+
+	resp := &api.CheckTransactionResponse{}
+	if err := json.Unmarshal(body, resp); err != nil {
+		t.Fatalf("couldn't unmarshal responde: %v", err)
+	}
+	assert.False(t, resp.Success)
+	assert.Equal(t, uint32(4), resp.Code)
+	assert.Equal(t, int64(0), resp.GasWanted)
 }
 
-func testAcceptSigningTransactionManuallySucceeds(t *testing.T) {
+func testCheckTransactionWithFailedTransactionFails(t *testing.T) {
 	s := getTestService(t, "manual")
 	defer s.ctrl.Finish()
 
@@ -731,7 +784,34 @@ func testAcceptSigningTransactionManuallySucceeds(t *testing.T) {
 	token := vgrand.RandomStr(5)
 	headers := authHeaders(t, token)
 	pubKey := vgrand.RandomStr(5)
-	payload := fmt.Sprintf(`{"propagate": true, "pubKey": "%s", "orderCancellation": {}}`, pubKey)
+	payload := fmt.Sprintf(`{"pubKey": "%s", "orderCancellation": {}}`, pubKey)
+
+	// setup
+	s.auth.EXPECT().VerifyToken(token).Times(1).Return(walletName, nil)
+	s.handler.EXPECT().SignTx(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(&commandspb.Transaction{}, nil)
+	s.nodeForward.EXPECT().CheckTx(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil, assert.AnError)
+	s.nodeForward.EXPECT().LastBlockHeightAndHash(gomock.Any()).Times(1).Return(&api.LastBlockHeightResponse{
+		Height:              42,
+		Hash:                "0292041e2f0cf741894503fb3ead4cb817bca2375e543aa70f7c4d938157b5a6",
+		SpamPowDifficulty:   2,
+		SpamPowHashFunction: "sha3_24_rounds",
+	}, 0, nil)
+	// when
+
+	statusCode, _ := serveHTTP(t, s, checkTxRequest(t, payload, headers))
+	assert.Equal(t, http.StatusInternalServerError, statusCode)
+}
+
+func testAcceptSigningTransactionSucceeds(t *testing.T) {
+	s := getTestService(t, "manual")
+	defer s.ctrl.Finish()
+
+	// given
+	walletName := vgrand.RandomStr(5)
+	token := vgrand.RandomStr(5)
+	headers := authHeaders(t, token)
+	pubKey := vgrand.RandomStr(5)
+	payload := fmt.Sprintf(`{"pubKey": "%s", "orderCancellation": {}}`, pubKey)
 
 	// setup
 	s.auth.EXPECT().VerifyToken(token).Times(1).Return(walletName, nil)
@@ -753,7 +833,8 @@ func testAskingConsentPolicySucceeds(t *testing.T) {
 	txn := &v1.SubmitTransactionRequest{}
 
 	pendingConsents := make(chan service.ConsentRequest, 1)
-	p := service.NewExplicitConsentPolicy(pendingConsents)
+	sentTxs := make(chan service.SentTransaction, 1)
+	p := service.NewExplicitConsentPolicy(pendingConsents, sentTxs)
 
 	go func() {
 		req := <-pendingConsents
@@ -762,9 +843,22 @@ func testAskingConsentPolicySucceeds(t *testing.T) {
 		req.Confirmations <- d
 	}()
 
-	answer, err := p.Ask(txn)
+	nowTime := time.Now()
+	answer, err := p.Ask(txn, "testTx", nowTime)
 	require.Nil(t, err)
 	require.False(t, answer)
+
+	p.Report(service.SentTransaction{
+		TxHash:     "txHash",
+		ReceivedAt: nowTime,
+		TxID:       "testTx",
+		Tx:         &commandspb.Transaction{},
+	})
+
+	sent := <-sentTxs
+	require.Equal(t, "txHash", sent.TxHash)
+	require.Equal(t, "testTx", sent.TxID)
+	require.Equal(t, nowTime, sent.ReceivedAt)
 }
 
 func testDeclineSigningTransactionManuallySucceeds(t *testing.T) {
@@ -1110,6 +1204,11 @@ func annotateKeyRequest(t *testing.T, id, payload string, headers map[string]str
 	return buildRequest(t, http.MethodPut, fmt.Sprintf("/api/v1/keys/%s/metadata", id), payload, headers)
 }
 
+func checkTxRequest(t *testing.T, payload string, headers map[string]string) *http.Request {
+	t.Helper()
+	return buildRequest(t, http.MethodPost, "/api/v1/command/check", payload, headers)
+}
+
 func signTxRequest(t *testing.T, payload string, headers map[string]string) *http.Request {
 	t.Helper()
 	return buildRequest(t, http.MethodPost, "/api/v1/command", payload, headers)