From ed1fac1a2a41be98599f80c5581b8dd868c76371 Mon Sep 17 00:00:00 2001 From: Jamie Holdstock Date: Sat, 19 Nov 2022 04:06:47 +0800 Subject: [PATCH] Add types module for API requests/responses/errors. (#356) * Remove helper func getCommitmentAddress. This function is only used in one place so removing it does not introduce any duplication. Removing the func also removes the need for errors.Is comparison, which will be very useful in upcoming changes. * Rename apiError to ErrorCode. * Don't use inline type for API errors. * Export webapi errors. * Export webapi request/responses. * Add types module for API requests/responses/errors --- go.mod | 5 ++ run_tests.sh | 33 +++++++-- types/errors.go | 120 ++++++++++++++++++++++++++++++++ types/errors_test.go | 85 +++++++++++++++++++++++ types/go.mod | 3 + {webapi => types}/types.go | 31 +++++---- types/types_test.go | 53 ++++++++++++++ webapi/errors.go | 125 ---------------------------------- webapi/errors_test.go | 90 ------------------------ webapi/getfeeaddress.go | 31 +++++---- webapi/helpers.go | 27 -------- webapi/middleware.go | 104 +++++++++++++++++----------- webapi/payfee.go | 49 ++++++------- webapi/setaltsignaddr.go | 25 +++---- webapi/setaltsignaddr_test.go | 3 +- webapi/setvotechoices.go | 29 ++++---- webapi/ticketstatus.go | 11 +-- webapi/vspinfo.go | 3 +- webapi/webapi.go | 25 +++---- 19 files changed, 468 insertions(+), 384 deletions(-) create mode 100644 types/errors.go create mode 100644 types/errors_test.go create mode 100644 types/go.mod rename {webapi => types}/types.go (86%) create mode 100644 types/types_test.go delete mode 100644 webapi/errors.go delete mode 100644 webapi/errors_test.go diff --git a/go.mod b/go.mod index b2c492d..5c2bf0c 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/decred/dcrd/txscript/v4 v4.0.0 github.com/decred/dcrd/wire v1.5.0 github.com/decred/slog v1.2.0 + github.com/decred/vspd/types v0.0.0-00010101000000-000000000000 github.com/dustin/go-humanize v1.0.0 github.com/gin-gonic/gin v1.8.1 github.com/gorilla/sessions v1.2.1 @@ -61,3 +62,7 @@ require ( google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) + +replace ( + github.com/decred/vspd/types => ./types +) diff --git a/run_tests.sh b/run_tests.sh index c239180..66f2f89 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -1,18 +1,39 @@ #!/bin/bash # -# Copyright (c) 2020-2021 The Decred developers +# Copyright (c) 2020-2022 The Decred developers # Use of this source code is governed by an ISC # license that can be found in the LICENSE file. # # usage: # ./run_tests.sh -set -ex +set -e go version -# run tests -env GORACE="halt_on_error=1" go test -race ./... +# run tests on all modules +ROOTPATH=$(go list -m) +ROOTPATHPATTERN=$(echo $ROOTPATH | sed 's/\\/\\\\/g' | sed 's/\//\\\//g') +MODPATHS=$(go list -m all | grep "^$ROOTPATHPATTERN" | cut -d' ' -f1) +for module in $MODPATHS; do + echo "==> ${module}" + env GORACE="halt_on_error=1" go test -race ${module}/... -# run linter -golangci-lint run + # check linters + MODNAME=$(echo $module | sed -E -e "s/^$ROOTPATHPATTERN//" \ + -e 's,^/,,' -e 's,/v[0-9]+$,,') + if [ -z "$MODNAME" ]; then + MODNAME=. + fi + + # run commands in the module directory as a subshell + ( + cd $MODNAME + + # run linter + golangci-lint run + ) +done + +echo "------------------------------------------" +echo "Tests completed successfully!" diff --git a/types/errors.go b/types/errors.go new file mode 100644 index 0000000..887acdc --- /dev/null +++ b/types/errors.go @@ -0,0 +1,120 @@ +// Copyright (c) 2020-2022 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package types + +import "net/http" + +// ErrorCode is an integer which represents a kind of error which may be +// encountered by vspd. +type ErrorCode int64 + +const ( + ErrBadRequest ErrorCode = iota + ErrInternalError + ErrVspClosed + ErrFeeAlreadyReceived + ErrInvalidFeeTx + ErrFeeTooSmall + ErrUnknownTicket + ErrTicketCannotVote + ErrFeeExpired + ErrInvalidVoteChoices + ErrBadSignature + ErrInvalidPrivKey + ErrFeeNotReceived + ErrInvalidTicket + ErrCannotBroadcastTicket + ErrCannotBroadcastFee + ErrCannotBroadcastFeeUnknownOutputs + ErrInvalidTimestamp +) + +// HTTPStatus returns a corresponding HTTP status code for a given error code. +func (e ErrorCode) HTTPStatus() int { + switch e { + case ErrBadRequest: + return http.StatusBadRequest + case ErrInternalError: + return http.StatusInternalServerError + case ErrVspClosed: + return http.StatusBadRequest + case ErrFeeAlreadyReceived: + return http.StatusBadRequest + case ErrInvalidFeeTx: + return http.StatusBadRequest + case ErrFeeTooSmall: + return http.StatusBadRequest + case ErrUnknownTicket: + return http.StatusBadRequest + case ErrTicketCannotVote: + return http.StatusBadRequest + case ErrFeeExpired: + return http.StatusBadRequest + case ErrInvalidVoteChoices: + return http.StatusBadRequest + case ErrBadSignature: + return http.StatusBadRequest + case ErrInvalidPrivKey: + return http.StatusBadRequest + case ErrFeeNotReceived: + return http.StatusBadRequest + case ErrInvalidTicket: + return http.StatusBadRequest + case ErrCannotBroadcastTicket: + return http.StatusInternalServerError + case ErrCannotBroadcastFee: + return http.StatusInternalServerError + case ErrCannotBroadcastFeeUnknownOutputs: + return http.StatusPreconditionRequired + case ErrInvalidTimestamp: + return http.StatusBadRequest + default: + return http.StatusInternalServerError + } +} + +// DefaultMessage returns a descriptive error string for a given error code. +func (e ErrorCode) DefaultMessage() string { + switch e { + case ErrBadRequest: + return "bad request" + case ErrInternalError: + return "internal error" + case ErrVspClosed: + return "vsp is closed" + case ErrFeeAlreadyReceived: + return "fee tx already received for ticket" + case ErrInvalidFeeTx: + return "invalid fee tx" + case ErrFeeTooSmall: + return "fee too small" + case ErrUnknownTicket: + return "unknown ticket" + case ErrTicketCannotVote: + return "ticket not eligible to vote" + case ErrFeeExpired: + return "fee has expired" + case ErrInvalidVoteChoices: + return "invalid vote choices" + case ErrBadSignature: + return "bad request signature" + case ErrInvalidPrivKey: + return "invalid private key" + case ErrFeeNotReceived: + return "no fee tx received for ticket" + case ErrInvalidTicket: + return "not a valid ticket tx" + case ErrCannotBroadcastTicket: + return "ticket transaction could not be broadcast" + case ErrCannotBroadcastFee: + return "fee transaction could not be broadcast" + case ErrCannotBroadcastFeeUnknownOutputs: + return "fee transaction could not be broadcast due to unknown outputs" + case ErrInvalidTimestamp: + return "old or reused timestamp" + default: + return "unknown error" + } +} diff --git a/types/errors_test.go b/types/errors_test.go new file mode 100644 index 0000000..cba500a --- /dev/null +++ b/types/errors_test.go @@ -0,0 +1,85 @@ +// Copyright (c) 2022 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package types + +import ( + "net/http" + "testing" +) + +// TestErrorDefaultMessages ensures each ErrorKind can be mapped to a default +// descriptive error message. +func TestErrorDefaultMessages(t *testing.T) { + tests := []struct { + in ErrorCode + wantMsg string + }{ + {ErrBadRequest, "bad request"}, + {ErrInternalError, "internal error"}, + {ErrVspClosed, "vsp is closed"}, + {ErrFeeAlreadyReceived, "fee tx already received for ticket"}, + {ErrInvalidFeeTx, "invalid fee tx"}, + {ErrFeeTooSmall, "fee too small"}, + {ErrUnknownTicket, "unknown ticket"}, + {ErrTicketCannotVote, "ticket not eligible to vote"}, + {ErrFeeExpired, "fee has expired"}, + {ErrInvalidVoteChoices, "invalid vote choices"}, + {ErrBadSignature, "bad request signature"}, + {ErrInvalidPrivKey, "invalid private key"}, + {ErrFeeNotReceived, "no fee tx received for ticket"}, + {ErrInvalidTicket, "not a valid ticket tx"}, + {ErrCannotBroadcastTicket, "ticket transaction could not be broadcast"}, + {ErrCannotBroadcastFee, "fee transaction could not be broadcast"}, + {ErrCannotBroadcastFeeUnknownOutputs, "fee transaction could not be broadcast due to unknown outputs"}, + {ErrInvalidTimestamp, "old or reused timestamp"}, + {ErrorCode(9999), "unknown error"}, + } + + for _, test := range tests { + actualMsg := test.in.DefaultMessage() + if actualMsg != test.wantMsg { + t.Errorf("wrong default message for ErrorKind(%d). expected: %q actual: %q ", + test.in, test.wantMsg, actualMsg) + continue + } + } +} + +// TestErrorHTTPStatus ensures each ErrorCode can be mapped to a corresponding HTTP status code. +func TestErrorHTTPStatus(t *testing.T) { + tests := []struct { + in ErrorCode + wantStatus int + }{ + {ErrBadRequest, http.StatusBadRequest}, + {ErrInternalError, http.StatusInternalServerError}, + {ErrVspClosed, http.StatusBadRequest}, + {ErrFeeAlreadyReceived, http.StatusBadRequest}, + {ErrInvalidFeeTx, http.StatusBadRequest}, + {ErrFeeTooSmall, http.StatusBadRequest}, + {ErrUnknownTicket, http.StatusBadRequest}, + {ErrTicketCannotVote, http.StatusBadRequest}, + {ErrFeeExpired, http.StatusBadRequest}, + {ErrInvalidVoteChoices, http.StatusBadRequest}, + {ErrBadSignature, http.StatusBadRequest}, + {ErrInvalidPrivKey, http.StatusBadRequest}, + {ErrFeeNotReceived, http.StatusBadRequest}, + {ErrInvalidTicket, http.StatusBadRequest}, + {ErrCannotBroadcastTicket, http.StatusInternalServerError}, + {ErrCannotBroadcastFee, http.StatusInternalServerError}, + {ErrCannotBroadcastFeeUnknownOutputs, http.StatusPreconditionRequired}, + {ErrInvalidTimestamp, http.StatusBadRequest}, + {ErrorCode(9999), http.StatusInternalServerError}, + } + + for _, test := range tests { + result := test.in.HTTPStatus() + if result != test.wantStatus { + t.Errorf("wrong HTTP status for ErrorKind(%d). expected: %d actual: %d ", + test.in, test.wantStatus, result) + continue + } + } +} diff --git a/types/go.mod b/types/go.mod new file mode 100644 index 0000000..0f1f5b4 --- /dev/null +++ b/types/go.mod @@ -0,0 +1,3 @@ +module github.com/decred/vspd/types + +go 1.19 diff --git a/webapi/types.go b/types/types.go similarity index 86% rename from webapi/types.go rename to types/types.go index 1e8adc0..88ae2e2 100644 --- a/webapi/types.go +++ b/types/types.go @@ -2,9 +2,16 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -package webapi +package types -type vspInfoResponse struct { +type APIError struct { + Code int64 `json:"code"` + Message string `json:"message"` +} + +func (e APIError) Error() string { return e.Message } + +type VspInfoResponse struct { APIVersions []int64 `json:"apiversions"` Timestamp int64 `json:"timestamp"` PubKey []byte `json:"pubkey"` @@ -22,14 +29,14 @@ type vspInfoResponse struct { NetworkProportion float32 `json:"estimatednetworkproportion"` } -type feeAddressRequest struct { +type FeeAddressRequest struct { Timestamp int64 `json:"timestamp" binding:"required"` TicketHash string `json:"tickethash" binding:"required"` TicketHex string `json:"tickethex" binding:"required"` ParentHex string `json:"parenthex" binding:"required"` } -type feeAddressResponse struct { +type FeeAddressResponse struct { Timestamp int64 `json:"timestamp"` FeeAddress string `json:"feeaddress"` FeeAmount int64 `json:"feeamount"` @@ -37,7 +44,7 @@ type feeAddressResponse struct { Request []byte `json:"request"` } -type payFeeRequest struct { +type PayFeeRequest struct { Timestamp int64 `json:"timestamp" binding:"required"` TicketHash string `json:"tickethash" binding:"required"` FeeTx string `json:"feetx" binding:"required"` @@ -47,12 +54,12 @@ type payFeeRequest struct { TreasuryPolicy map[string]string `json:"treasurypolicy" binding:"max=3"` } -type payFeeResponse struct { +type PayFeeResponse struct { Timestamp int64 `json:"timestamp"` Request []byte `json:"request"` } -type setVoteChoicesRequest struct { +type SetVoteChoicesRequest struct { Timestamp int64 `json:"timestamp" binding:"required"` TicketHash string `json:"tickethash" binding:"required"` VoteChoices map[string]string `json:"votechoices" binding:"required"` @@ -60,16 +67,16 @@ type setVoteChoicesRequest struct { TreasuryPolicy map[string]string `json:"treasurypolicy" binding:"max=3"` } -type setVoteChoicesResponse struct { +type SetVoteChoicesResponse struct { Timestamp int64 `json:"timestamp"` Request []byte `json:"request"` } -type ticketStatusRequest struct { +type TicketStatusRequest struct { TicketHash string `json:"tickethash" binding:"required"` } -type ticketStatusResponse struct { +type TicketStatusResponse struct { Timestamp int64 `json:"timestamp"` TicketConfirmed bool `json:"ticketconfirmed"` FeeTxStatus string `json:"feetxstatus"` @@ -81,7 +88,7 @@ type ticketStatusResponse struct { Request []byte `json:"request"` } -type setAltSignAddrRequest struct { +type SetAltSignAddrRequest struct { Timestamp int64 `json:"timestamp" binding:"required"` TicketHash string `json:"tickethash" binding:"required"` TicketHex string `json:"tickethex" binding:"required"` @@ -89,7 +96,7 @@ type setAltSignAddrRequest struct { AltSignAddress string `json:"altsignaddress" binding:"required"` } -type setAltSignAddrResponse struct { +type SetAltSignAddrResponse struct { Timestamp int64 `json:"timestamp"` Request []byte `json:"request"` } diff --git a/types/types_test.go b/types/types_test.go new file mode 100644 index 0000000..d659ebc --- /dev/null +++ b/types/types_test.go @@ -0,0 +1,53 @@ +// Copyright (c) 2022 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package types + +import ( + "errors" + "testing" +) + +// TestAPIErrorAs ensures APIError can be unwrapped via errors.As. +func TestAPIErrorAs(t *testing.T) { + + tests := []struct { + testName string + apiError error + expectedKind ErrorCode + expectedMessage string + }{{ + testName: "BadRequest error", + apiError: APIError{Message: "something went wrong", Code: int64(ErrBadRequest)}, + expectedKind: ErrBadRequest, + expectedMessage: "something went wrong", + }, + { + testName: "Unknown error", + apiError: APIError{Message: "something went wrong again", Code: int64(999)}, + expectedKind: 999, + expectedMessage: "something went wrong again", + }} + + for _, test := range tests { + // Ensure APIError can be unwrapped from error. + var parsedError APIError + if !errors.As(test.apiError, &parsedError) { + t.Errorf("%s: unable to unwrap error", test.testName) + continue + } + + if parsedError.Code != int64(test.expectedKind) { + t.Errorf("%s: error was wrong kind. expected: %d actual %d", + test.testName, test.expectedKind, parsedError.Code) + continue + } + + if parsedError.Message != test.expectedMessage { + t.Errorf("%s: error had wrong message. expected: %q actual %q", + test.testName, test.expectedMessage, parsedError.Message) + continue + } + } +} diff --git a/webapi/errors.go b/webapi/errors.go deleted file mode 100644 index f932ef1..0000000 --- a/webapi/errors.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) 2020 The Decred developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package webapi - -import "net/http" - -// apiError is a kind of error. It has full support for errors.Is and -// errors.As. -type apiError int - -const ( - errBadRequest apiError = iota - errInternalError - errVspClosed - errFeeAlreadyReceived - errInvalidFeeTx - errFeeTooSmall - errUnknownTicket - errTicketCannotVote - errFeeExpired - errInvalidVoteChoices - errBadSignature - errInvalidPrivKey - errFeeNotReceived - errInvalidTicket - errCannotBroadcastTicket - errCannotBroadcastFee - errCannotBroadcastFeeUnknownOutputs - errInvalidTimestamp -) - -// httpStatus maps application error codes to HTTP status codes. -func (e apiError) httpStatus() int { - switch e { - case errBadRequest: - return http.StatusBadRequest - case errInternalError: - return http.StatusInternalServerError - case errVspClosed: - return http.StatusBadRequest - case errFeeAlreadyReceived: - return http.StatusBadRequest - case errInvalidFeeTx: - return http.StatusBadRequest - case errFeeTooSmall: - return http.StatusBadRequest - case errUnknownTicket: - return http.StatusBadRequest - case errTicketCannotVote: - return http.StatusBadRequest - case errFeeExpired: - return http.StatusBadRequest - case errInvalidVoteChoices: - return http.StatusBadRequest - case errBadSignature: - return http.StatusBadRequest - case errInvalidPrivKey: - return http.StatusBadRequest - case errFeeNotReceived: - return http.StatusBadRequest - case errInvalidTicket: - return http.StatusBadRequest - case errCannotBroadcastTicket: - return http.StatusInternalServerError - case errCannotBroadcastFee: - return http.StatusInternalServerError - case errCannotBroadcastFeeUnknownOutputs: - return http.StatusPreconditionRequired - case errInvalidTimestamp: - return http.StatusBadRequest - default: - return http.StatusInternalServerError - } -} - -// String returns a descriptive error string for a given error code. -func (e apiError) String() string { - switch e { - case errBadRequest: - return "bad request" - case errInternalError: - return "internal error" - case errVspClosed: - return "vsp is closed" - case errFeeAlreadyReceived: - return "fee tx already received for ticket" - case errInvalidFeeTx: - return "invalid fee tx" - case errFeeTooSmall: - return "fee too small" - case errUnknownTicket: - return "unknown ticket" - case errTicketCannotVote: - return "ticket not eligible to vote" - case errFeeExpired: - return "fee has expired" - case errInvalidVoteChoices: - return "invalid vote choices" - case errBadSignature: - return "bad request signature" - case errInvalidPrivKey: - return "invalid private key" - case errFeeNotReceived: - return "no fee tx received for ticket" - case errInvalidTicket: - return "not a valid ticket tx" - case errCannotBroadcastTicket: - return "ticket transaction could not be broadcast" - case errCannotBroadcastFee: - return "fee transaction could not be broadcast" - case errCannotBroadcastFeeUnknownOutputs: - return "fee transaction could not be broadcast due to unknown outputs" - case errInvalidTimestamp: - return "old or reused timestamp" - default: - return "unknown error" - } -} - -// Error satisfies the error interface and returns a human-readable error string. -func (e apiError) Error() string { - return e.String() -} diff --git a/webapi/errors_test.go b/webapi/errors_test.go deleted file mode 100644 index 9cf22f1..0000000 --- a/webapi/errors_test.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2022 The Decred developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -package webapi - -import ( - "errors" - "testing" -) - -// TestApiErrorString tests the stringized output for the apiError type. -func TestApiErrorString(t *testing.T) { - tests := []struct { - in apiError - want string - }{ - {errBadRequest, "bad request"}, - {errInternalError, "internal error"}, - {errVspClosed, "vsp is closed"}, - {errFeeAlreadyReceived, "fee tx already received for ticket"}, - {errInvalidFeeTx, "invalid fee tx"}, - {errFeeTooSmall, "fee too small"}, - {errUnknownTicket, "unknown ticket"}, - {errTicketCannotVote, "ticket not eligible to vote"}, - {errFeeExpired, "fee has expired"}, - {errInvalidVoteChoices, "invalid vote choices"}, - {errBadSignature, "bad request signature"}, - {errInvalidPrivKey, "invalid private key"}, - {errFeeNotReceived, "no fee tx received for ticket"}, - {errInvalidTicket, "not a valid ticket tx"}, - {errCannotBroadcastTicket, "ticket transaction could not be broadcast"}, - {errCannotBroadcastFee, "fee transaction could not be broadcast"}, - {errCannotBroadcastFeeUnknownOutputs, "fee transaction could not be broadcast due to unknown outputs"}, - {errInvalidTimestamp, "old or reused timestamp"}, - } - - for i, test := range tests { - result := test.in.Error() - if result != test.want { - t.Errorf("%d: got: %s want: %s", i, result, test.want) - continue - } - } -} - -// TestApiErrorIsAs ensures apiError can be identified via errors.Is and unwrapped via errors.As. -func TestApiErrorIsAs(t *testing.T) { - tests := []struct { - name string - err error - target error - wantMatch bool - wantAs apiError - }{{ - name: "errBadRequest == errBadRequest", - err: errBadRequest, - target: errBadRequest, - wantMatch: true, - wantAs: errBadRequest, - }, { - name: "errBadRequest != errInternalError", - err: errBadRequest, - target: errInternalError, - wantMatch: false, - wantAs: errBadRequest, - }} - for _, test := range tests { - // Ensure the error matches or not depending on the expected result. - result := errors.Is(test.err, test.target) - if result != test.wantMatch { - t.Errorf("%s: incorrect error identification -- got %v, want %v", - test.name, result, test.wantMatch) - continue - } - - // Ensure the underlying apiError can be unwrapped and is the - // expected type. - var err apiError - if !errors.As(test.err, &err) { - t.Errorf("%s: unable to unwrap error", test.name) - continue - } - if err != test.wantAs { - t.Errorf("%s: unexpected unwrapped error -- got %v, want %v", - test.name, err, test.wantAs) - continue - } - } -} diff --git a/webapi/getfeeaddress.go b/webapi/getfeeaddress.go index a378f11..5e76286 100644 --- a/webapi/getfeeaddress.go +++ b/webapi/getfeeaddress.go @@ -12,6 +12,7 @@ import ( "github.com/decred/dcrd/dcrutil/v4" "github.com/decred/vspd/database" "github.com/decred/vspd/rpc" + "github.com/decred/vspd/types" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" ) @@ -84,20 +85,20 @@ func (s *Server) feeAddress(c *gin.Context) { dcrdErr := c.MustGet(dcrdErrorKey) if dcrdErr != nil { s.log.Errorf("%s: Could not get dcrd client: %v", funcName, dcrdErr.(error)) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } reqBytes := c.MustGet(requestBytesKey).([]byte) if s.cfg.VspClosed { - s.sendError(errVspClosed, c) + s.sendError(types.ErrVspClosed, c) return } - var request feeAddressRequest + var request types.FeeAddressRequest if err := binding.JSON.BindBody(reqBytes, &request); err != nil { s.log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err) - s.sendErrorWithMsg(err.Error(), errBadRequest, c) + s.sendErrorWithMsg(err.Error(), types.ErrBadRequest, c) return } @@ -110,7 +111,7 @@ func (s *Server) feeAddress(c *gin.Context) { ticket.FeeTxStatus == database.FeeConfirmed) { s.log.Warnf("%s: Fee tx already received (clientIP=%s, ticketHash=%s)", funcName, c.ClientIP(), ticket.Hash) - s.sendError(errFeeAlreadyReceived, c) + s.sendError(types.ErrFeeAlreadyReceived, c) return } @@ -118,7 +119,7 @@ func (s *Server) feeAddress(c *gin.Context) { rawTicket, err := dcrdClient.GetRawTransaction(ticketHash) if err != nil { s.log.Errorf("%s: dcrd.GetRawTransaction for ticket failed (ticketHash=%s): %v", funcName, ticketHash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } @@ -126,13 +127,13 @@ func (s *Server) feeAddress(c *gin.Context) { canVote, err := canTicketVote(rawTicket, dcrdClient, s.cfg.NetParams) if err != nil { s.log.Errorf("%s: canTicketVote error (ticketHash=%s): %v", funcName, ticketHash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } if !canVote { s.log.Warnf("%s: Unvotable ticket (clientIP=%s, ticketHash=%s)", funcName, c.ClientIP(), ticketHash) - s.sendError(errTicketCannotVote, c) + s.sendError(types.ErrTicketCannotVote, c) return } @@ -145,7 +146,7 @@ func (s *Server) feeAddress(c *gin.Context) { newFee, err := s.getCurrentFee(dcrdClient) if err != nil { s.log.Errorf("%s: getCurrentFee error (ticketHash=%s): %v", funcName, ticket.Hash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } ticket.FeeExpiration = now.Add(feeAddressExpiration).Unix() @@ -155,13 +156,13 @@ func (s *Server) feeAddress(c *gin.Context) { if err != nil { s.log.Errorf("%s: db.UpdateTicket error, failed to update fee expiry (ticketHash=%s): %v", funcName, ticket.Hash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } s.log.Debugf("%s: Expired fee updated (newFeeAmt=%s, ticketHash=%s)", funcName, newFee, ticket.Hash) } - s.sendJSONResponse(feeAddressResponse{ + s.sendJSONResponse(types.FeeAddressResponse{ Timestamp: now.Unix(), Request: reqBytes, FeeAddress: ticket.FeeAddress, @@ -178,14 +179,14 @@ func (s *Server) feeAddress(c *gin.Context) { fee, err := s.getCurrentFee(dcrdClient) if err != nil { s.log.Errorf("%s: getCurrentFee error (ticketHash=%s): %v", funcName, ticketHash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } newAddress, newAddressIdx, err := s.getNewFeeAddress() if err != nil { s.log.Errorf("%s: getNewFeeAddress error (ticketHash=%s): %v", funcName, ticketHash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } @@ -216,7 +217,7 @@ func (s *Server) feeAddress(c *gin.Context) { err = s.db.InsertNewTicket(dbTicket) if err != nil { s.log.Errorf("%s: db.InsertNewTicket failed (ticketHash=%s): %v", funcName, ticketHash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } @@ -224,7 +225,7 @@ func (s *Server) feeAddress(c *gin.Context) { "feeAddr=%s, feeAmt=%s, ticketHash=%s)", funcName, confirmed, newAddressIdx, newAddress, fee, ticketHash) - s.sendJSONResponse(feeAddressResponse{ + s.sendJSONResponse(types.FeeAddressResponse{ Timestamp: now.Unix(), Request: reqBytes, FeeAddress: newAddress, diff --git a/webapi/helpers.go b/webapi/helpers.go index dc83a8b..3099f7f 100644 --- a/webapi/helpers.go +++ b/webapi/helpers.go @@ -17,7 +17,6 @@ import ( dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v3" "github.com/decred/dcrd/wire" "github.com/decred/vspd/database" - "github.com/decred/vspd/rpc" ) func currentVoteVersion(params *chaincfg.Params) uint32 { @@ -170,32 +169,6 @@ func validateTicketHash(hash string) error { return nil } -// getCommitmentAddress gets the commitment address of the provided ticket hash -// from the chain. -func getCommitmentAddress(hash string, dcrdClient *rpc.DcrdRPC, params *chaincfg.Params) (string, error) { - resp, err := dcrdClient.GetRawTransaction(hash) - if err != nil { - return "", fmt.Errorf("dcrd.GetRawTransaction for ticket failed: %w", err) - } - - msgTx, err := decodeTransaction(resp.Hex) - if err != nil { - return "", fmt.Errorf("failed to decode ticket hex: %w", err) - } - - err = isValidTicket(msgTx) - if err != nil { - return "", fmt.Errorf("invalid ticket: %w", errInvalidTicket) - } - - addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript, params) - if err != nil { - return "", fmt.Errorf("AddrFromSStxPkScrCommitment error: %w", err) - } - - return addr.String(), nil -} - // canTicketVote checks determines whether a ticket is able to vote at some // point in the future by checking that it is currently either immature or live. func canTicketVote(rawTx *dcrdtypes.TxRawResult, dcrdClient Node, netParams *chaincfg.Params) (bool, error) { diff --git a/webapi/middleware.go b/webapi/middleware.go index f2248b5..c15ed83 100644 --- a/webapi/middleware.go +++ b/webapi/middleware.go @@ -11,7 +11,9 @@ import ( "net/http" "strings" + "github.com/decred/dcrd/blockchain/stake/v4" "github.com/decred/vspd/rpc" + "github.com/decred/vspd/types" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/gorilla/sessions" @@ -121,7 +123,7 @@ func (s *Server) broadcastTicket(c *gin.Context) { reqBytes, err := drainAndReplaceBody(c.Request) if err != nil { s.log.Warnf("%s: Error reading request (clientIP=%s): %v", funcName, c.ClientIP(), err) - s.sendErrorWithMsg(err.Error(), errBadRequest, c) + s.sendErrorWithMsg(err.Error(), types.ErrBadRequest, c) return } @@ -133,7 +135,7 @@ func (s *Server) broadcastTicket(c *gin.Context) { } if err := binding.JSON.BindBody(reqBytes, &request); err != nil { s.log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err) - s.sendErrorWithMsg(err.Error(), errBadRequest, c) + s.sendErrorWithMsg(err.Error(), types.ErrBadRequest, c) return } @@ -142,7 +144,7 @@ func (s *Server) broadcastTicket(c *gin.Context) { if err != nil { s.log.Errorf("%s: Failed to decode ticket hex (ticketHash=%s): %v", funcName, request.TicketHash, err) - s.sendErrorWithMsg("cannot decode ticket hex", errBadRequest, c) + s.sendErrorWithMsg("cannot decode ticket hex", types.ErrBadRequest, c) return } @@ -150,7 +152,7 @@ func (s *Server) broadcastTicket(c *gin.Context) { if err != nil { s.log.Warnf("%s: Invalid ticket (clientIP=%s, ticketHash=%s): %v", funcName, c.ClientIP(), request.TicketHash, err) - s.sendError(errInvalidTicket, c) + s.sendError(types.ErrInvalidTicket, c) return } @@ -158,7 +160,7 @@ func (s *Server) broadcastTicket(c *gin.Context) { if msgTx.TxHash().String() != request.TicketHash { s.log.Warnf("%s: Ticket hex/hash mismatch (clientIP=%s, ticketHash=%s)", funcName, c.ClientIP(), request.TicketHash) - s.sendErrorWithMsg("ticket hex does not match hash", errBadRequest, c) + s.sendErrorWithMsg("ticket hex does not match hash", types.ErrBadRequest, c) return } @@ -166,7 +168,7 @@ func (s *Server) broadcastTicket(c *gin.Context) { parentTx, err := decodeTransaction(request.ParentHex) if err != nil { s.log.Errorf("%s: Failed to decode parent hex (ticketHash=%s): %v", funcName, request.TicketHash, err) - s.sendErrorWithMsg("cannot decode parent hex", errBadRequest, c) + s.sendErrorWithMsg("cannot decode parent hex", types.ErrBadRequest, c) return } parentHash := parentTx.TxHash() @@ -176,7 +178,7 @@ func (s *Server) broadcastTicket(c *gin.Context) { dcrdErr := c.MustGet(dcrdErrorKey) if dcrdErr != nil { s.log.Errorf("%s: Could not get dcrd client: %v", funcName, dcrdErr.(error)) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } @@ -201,7 +203,7 @@ func (s *Server) broadcastTicket(c *gin.Context) { if !found { s.log.Errorf("%s: Invalid ticket parent (ticketHash=%s)", funcName, request.TicketHash) - s.sendErrorWithMsg("invalid ticket parent", errBadRequest, c) + s.sendErrorWithMsg("invalid ticket parent", types.ErrBadRequest, c) return } @@ -210,14 +212,14 @@ func (s *Server) broadcastTicket(c *gin.Context) { if err != nil { s.log.Errorf("%s: dcrd.SendRawTransaction for parent tx failed (ticketHash=%s): %v", funcName, request.TicketHash, err) - s.sendError(errCannotBroadcastTicket, c) + s.sendError(types.ErrCannotBroadcastTicket, c) return } } else { s.log.Errorf("%s: dcrd.GetRawTransaction for ticket parent failed (ticketHash=%s): %v", funcName, request.TicketHash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } @@ -236,13 +238,13 @@ func (s *Server) broadcastTicket(c *gin.Context) { if err != nil { s.log.Errorf("%s: dcrd.SendRawTransaction for ticket failed (ticketHash=%s): %v", funcName, request.TicketHash, err) - s.sendError(errCannotBroadcastTicket, c) + s.sendError(types.ErrCannotBroadcastTicket, c) return } } else { s.log.Errorf("%s: dcrd.GetRawTransaction for ticket failed (ticketHash=%s): %v", funcName, request.TicketHash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } } @@ -261,7 +263,7 @@ func (s *Server) vspAuth(c *gin.Context) { reqBytes, err := drainAndReplaceBody(c.Request) if err != nil { s.log.Warnf("%s: Error reading request (clientIP=%s): %v", funcName, c.ClientIP(), err) - s.sendErrorWithMsg(err.Error(), errBadRequest, c) + s.sendErrorWithMsg(err.Error(), types.ErrBadRequest, c) return } @@ -275,7 +277,7 @@ func (s *Server) vspAuth(c *gin.Context) { } if err := binding.JSON.BindBody(reqBytes, &request); err != nil { s.log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err) - s.sendErrorWithMsg(err.Error(), errBadRequest, c) + s.sendErrorWithMsg(err.Error(), types.ErrBadRequest, c) return } hash := request.TicketHash @@ -284,7 +286,7 @@ func (s *Server) vspAuth(c *gin.Context) { err = validateTicketHash(hash) if err != nil { s.log.Errorf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err) - s.sendErrorWithMsg("invalid ticket hash", errBadRequest, c) + s.sendErrorWithMsg("invalid ticket hash", types.ErrBadRequest, c) return } @@ -292,45 +294,67 @@ func (s *Server) vspAuth(c *gin.Context) { ticket, ticketFound, err := s.db.GetTicketByHash(hash) if err != nil { s.log.Errorf("%s: db.GetTicketByHash error (ticketHash=%s): %v", funcName, hash, err) - s.sendError(errInternalError, c) - return - } - - // If the ticket was found in the database, we already know its - // commitment address. Otherwise we need to get it from the chain. - dcrdClient := c.MustGet(dcrdKey).(*rpc.DcrdRPC) - dcrdErr := c.MustGet(dcrdErrorKey) - if dcrdErr != nil { - s.log.Errorf("%s: Could not get dcrd client: %v", funcName, dcrdErr.(error)) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } var commitmentAddress string if ticketFound { + // The commitment address is already known if the ticket already exists + // in the database. commitmentAddress = ticket.CommitmentAddress } else { - commitmentAddress, err = getCommitmentAddress(hash, dcrdClient, s.cfg.NetParams) - if err != nil { - s.log.Errorf("%s: Failed to get commitment address (clientIP=%s, ticketHash=%s): %v", - funcName, c.ClientIP(), hash, err) - - var apiErr *apiError - if errors.Is(err, apiErr) { - s.sendError(errInvalidTicket, c) - } else { - s.sendError(errInternalError, c) - } - + // Otherwise the commitment address must be retrieved from the chain + // using dcrd. + dcrdClient := c.MustGet(dcrdKey).(*rpc.DcrdRPC) + dcrdErr := c.MustGet(dcrdErrorKey) + if dcrdErr != nil { + s.log.Errorf("%s: Could not get dcrd client (clientIP=%s, ticketHash=%s): %v", + funcName, c.ClientIP(), hash, dcrdErr.(error)) + s.sendError(types.ErrInternalError, c) return } + + rawTx, err := dcrdClient.GetRawTransaction(hash) + if err != nil { + s.log.Errorf("%s: dcrd.GetRawTransaction for ticket failed (clientIP=%s, ticketHash=%s): %v", + funcName, c.ClientIP(), hash, err) + s.sendError(types.ErrInternalError, c) + return + } + + msgTx, err := decodeTransaction(rawTx.Hex) + if err != nil { + s.log.Errorf("%s: Failed to decode ticket hex (clientIP=%s, ticketHash=%s): %v", + funcName, c.ClientIP(), hash, err) + s.sendError(types.ErrInternalError, c) + return + } + + err = isValidTicket(msgTx) + if err != nil { + s.log.Errorf("%s: Invalid ticket (clientIP=%s, ticketHash=%s)", + funcName, c.ClientIP(), hash) + s.sendError(types.ErrInvalidTicket, c) + return + } + + addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript, s.cfg.NetParams) + if err != nil { + s.log.Errorf("%s: AddrFromSStxPkScrCommitment error (clientIP=%s, ticketHash=%s): %v", + funcName, c.ClientIP(), hash, err) + s.sendError(types.ErrInternalError, c) + return + } + + commitmentAddress = addr.String() } // Ensure a signature is provided. signature := c.GetHeader("VSP-Client-Signature") if signature == "" { s.log.Warnf("%s: No VSP-Client-Signature header (clientIP=%s)", funcName, c.ClientIP()) - s.sendErrorWithMsg("no VSP-Client-Signature header", errBadRequest, c) + s.sendErrorWithMsg("no VSP-Client-Signature header", types.ErrBadRequest, c) return } @@ -339,7 +363,7 @@ func (s *Server) vspAuth(c *gin.Context) { if err != nil { s.log.Errorf("%s: Couldn't validate signature (clientIP=%s, ticketHash=%s): %v", funcName, c.ClientIP(), hash, err) - s.sendError(errBadSignature, c) + s.sendError(types.ErrBadSignature, c) return } diff --git a/webapi/payfee.go b/webapi/payfee.go index bb1c1ff..548c6e2 100644 --- a/webapi/payfee.go +++ b/webapi/payfee.go @@ -15,6 +15,7 @@ import ( "github.com/decred/dcrd/txscript/v4/stdaddr" "github.com/decred/vspd/database" "github.com/decred/vspd/rpc" + "github.com/decred/vspd/types" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" ) @@ -30,26 +31,26 @@ func (s *Server) payFee(c *gin.Context) { dcrdErr := c.MustGet(dcrdErrorKey) if dcrdErr != nil { s.log.Errorf("%s: Could not get dcrd client: %v", funcName, dcrdErr.(error)) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } reqBytes := c.MustGet(requestBytesKey).([]byte) if s.cfg.VspClosed { - s.sendError(errVspClosed, c) + s.sendError(types.ErrVspClosed, c) return } if !knownTicket { s.log.Warnf("%s: Unknown ticket (clientIP=%s)", funcName, c.ClientIP()) - s.sendError(errUnknownTicket, c) + s.sendError(types.ErrUnknownTicket, c) return } - var request payFeeRequest + var request types.PayFeeRequest if err := binding.JSON.BindBody(reqBytes, &request); err != nil { s.log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err) - s.sendErrorWithMsg(err.Error(), errBadRequest, c) + s.sendErrorWithMsg(err.Error(), types.ErrBadRequest, c) return } @@ -59,7 +60,7 @@ func (s *Server) payFee(c *gin.Context) { ticket.FeeTxStatus == database.FeeConfirmed { s.log.Warnf("%s: Fee tx already received (clientIP=%s, ticketHash=%s)", funcName, c.ClientIP(), ticket.Hash) - s.sendError(errFeeAlreadyReceived, c) + s.sendError(types.ErrFeeAlreadyReceived, c) return } @@ -67,7 +68,7 @@ func (s *Server) payFee(c *gin.Context) { rawTicket, err := dcrdClient.GetRawTransaction(ticket.Hash) if err != nil { s.log.Errorf("%s: dcrd.GetRawTransaction for ticket failed (ticketHash=%s): %v", funcName, ticket.Hash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } @@ -75,13 +76,13 @@ func (s *Server) payFee(c *gin.Context) { canVote, err := canTicketVote(rawTicket, dcrdClient, s.cfg.NetParams) if err != nil { s.log.Errorf("%s: canTicketVote error (ticketHash=%s): %v", funcName, ticket.Hash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } if !canVote { s.log.Warnf("%s: Unvotable ticket (clientIP=%s, ticketHash=%s)", funcName, c.ClientIP(), ticket.Hash) - s.sendError(errTicketCannotVote, c) + s.sendError(types.ErrTicketCannotVote, c) return } @@ -89,7 +90,7 @@ func (s *Server) payFee(c *gin.Context) { if ticket.FeeExpired() { s.log.Warnf("%s: Expired payfee request (clientIP=%s, ticketHash=%s)", funcName, c.ClientIP(), ticket.Hash) - s.sendError(errFeeExpired, c) + s.sendError(types.ErrFeeExpired, c) return } @@ -99,7 +100,7 @@ func (s *Server) payFee(c *gin.Context) { if err != nil { s.log.Warnf("%s: Failed to decode WIF (clientIP=%s, ticketHash=%s): %v", funcName, c.ClientIP(), ticket.Hash, err) - s.sendError(errInvalidPrivKey, c) + s.sendError(types.ErrInvalidPrivKey, c) return } @@ -135,7 +136,7 @@ func (s *Server) payFee(c *gin.Context) { if err != nil { s.log.Warnf("%s: Failed to decode fee tx hex (clientIP=%s, ticketHash=%s): %v", funcName, c.ClientIP(), ticket.Hash, err) - s.sendError(errInvalidFeeTx, c) + s.sendError(types.ErrInvalidFeeTx, c) return } @@ -143,7 +144,7 @@ func (s *Server) payFee(c *gin.Context) { if err != nil { s.log.Warnf("%s: Fee tx failed sanity check (clientIP=%s, ticketHash=%s): %v", funcName, c.ClientIP(), ticket.Hash, err) - s.sendError(errInvalidFeeTx, c) + s.sendError(types.ErrInvalidFeeTx, c) return } @@ -152,7 +153,7 @@ func (s *Server) payFee(c *gin.Context) { if err != nil { s.log.Errorf("%s: Failed to decode fee address (ticketHash=%s): %v", funcName, ticket.Hash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } @@ -174,7 +175,7 @@ func (s *Server) payFee(c *gin.Context) { funcName, ticket.Hash, ticket.FeeAddress, c.ClientIP()) s.sendErrorWithMsg( fmt.Sprintf("feetx did not include any payments for fee address %s", ticket.FeeAddress), - errInvalidFeeTx, c) + types.ErrInvalidFeeTx, c) return } @@ -183,7 +184,7 @@ func (s *Server) payFee(c *gin.Context) { if feePaid < minFee { s.log.Warnf("%s: Fee too small (ticketHash=%s, clientIP=%s): was %s, expected minimum %s", funcName, ticket.Hash, c.ClientIP(), feePaid, minFee) - s.sendError(errFeeTooSmall, c) + s.sendError(types.ErrFeeTooSmall, c) return } @@ -193,7 +194,7 @@ func (s *Server) payFee(c *gin.Context) { if err != nil { s.log.Errorf("%s: Failed to get voting address from WIF (ticketHash=%s, clientIP=%s): %v", funcName, ticket.Hash, c.ClientIP(), err) - s.sendError(errInvalidPrivKey, c) + s.sendError(types.ErrInvalidPrivKey, c) return } @@ -204,7 +205,7 @@ func (s *Server) payFee(c *gin.Context) { if err != nil { s.log.Warnf("%s: Failed to decode ticket hex (ticketHash=%s): %v", funcName, ticket.Hash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } @@ -217,7 +218,7 @@ func (s *Server) payFee(c *gin.Context) { s.log.Warnf("%s: Voting address does not match provided private key: (ticketHash=%s)", funcName, ticket.Hash) s.sendErrorWithMsg("voting address does not match provided private key", - errInvalidPrivKey, c) + types.ErrInvalidPrivKey, c) return } @@ -246,7 +247,7 @@ func (s *Server) payFee(c *gin.Context) { if err != nil { s.log.Errorf("%s: db.UpdateTicket error, failed to set fee tx (ticketHash=%s): %v", funcName, ticket.Hash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } @@ -263,9 +264,9 @@ func (s *Server) payFee(c *gin.Context) { if strings.Contains(err.Error(), "references outputs of unknown or fully-spent transaction") { - s.sendError(errCannotBroadcastFeeUnknownOutputs, c) + s.sendError(types.ErrCannotBroadcastFeeUnknownOutputs, c) } else { - s.sendError(errCannotBroadcastFee, c) + s.sendError(types.ErrCannotBroadcastFee, c) } err = s.db.UpdateTicket(ticket) @@ -283,7 +284,7 @@ func (s *Server) payFee(c *gin.Context) { if err != nil { s.log.Errorf("%s: db.UpdateTicket error, failed to set fee tx as broadcast (ticketHash=%s): %v", funcName, ticket.Hash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } @@ -292,7 +293,7 @@ func (s *Server) payFee(c *gin.Context) { } // Send success response to client. - resp, respSig := s.sendJSONResponse(payFeeResponse{ + resp, respSig := s.sendJSONResponse(types.PayFeeResponse{ Timestamp: time.Now().Unix(), Request: reqBytes, }, c) diff --git a/webapi/setaltsignaddr.go b/webapi/setaltsignaddr.go index bbff468..ad7bada 100644 --- a/webapi/setaltsignaddr.go +++ b/webapi/setaltsignaddr.go @@ -11,6 +11,7 @@ import ( "github.com/decred/dcrd/txscript/v4/stdaddr" "github.com/decred/vspd/database" "github.com/decred/vspd/rpc" + "github.com/decred/vspd/types" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" ) @@ -34,20 +35,20 @@ func (s *Server) setAltSignAddr(c *gin.Context) { dcrdErr := c.MustGet(dcrdErrorKey) if dcrdErr != nil { s.log.Errorf("%s: Could not get dcrd client: %v", funcName, dcrdErr.(error)) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } reqBytes := c.MustGet(requestBytesKey).([]byte) if s.cfg.VspClosed { - s.sendError(errVspClosed, c) + s.sendError(types.ErrVspClosed, c) return } - var request setAltSignAddrRequest + var request types.SetAltSignAddrRequest if err := binding.JSON.BindBody(reqBytes, &request); err != nil { s.log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err) - s.sendErrorWithMsg(err.Error(), errBadRequest, c) + s.sendErrorWithMsg(err.Error(), types.ErrBadRequest, c) return } @@ -56,13 +57,13 @@ func (s *Server) setAltSignAddr(c *gin.Context) { currentData, err := s.db.AltSignAddrData(ticketHash) if err != nil { s.log.Errorf("%s: db.AltSignAddrData (ticketHash=%s): %v", funcName, ticketHash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } if currentData != nil { msg := "alternate sign address data already exists" s.log.Warnf("%s: %s (ticketHash=%s)", funcName, msg, ticketHash) - s.sendErrorWithMsg(msg, errBadRequest, c) + s.sendErrorWithMsg(msg, types.ErrBadRequest, c) return } @@ -71,12 +72,12 @@ func (s *Server) setAltSignAddr(c *gin.Context) { addr, err := stdaddr.DecodeAddressV0(altSignAddr, s.cfg.NetParams) if err != nil { s.log.Warnf("%s: Alt sign address cannot be decoded (clientIP=%s): %v", funcName, c.ClientIP(), err) - s.sendErrorWithMsg(err.Error(), errBadRequest, c) + s.sendErrorWithMsg(err.Error(), types.ErrBadRequest, c) return } if _, ok := addr.(*stdaddr.AddressPubKeyHashEcdsaSecp256k1V0); !ok { s.log.Warnf("%s: Alt sign address is unexpected type (clientIP=%s, type=%T)", funcName, c.ClientIP(), addr) - s.sendErrorWithMsg("wrong type for alternate signing address", errBadRequest, c) + s.sendErrorWithMsg("wrong type for alternate signing address", types.ErrBadRequest, c) return } @@ -84,7 +85,7 @@ func (s *Server) setAltSignAddr(c *gin.Context) { rawTicket, err := dcrdClient.GetRawTransaction(ticketHash) if err != nil { s.log.Errorf("%s: dcrd.GetRawTransaction for ticket failed (ticketHash=%s): %v", funcName, ticketHash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } @@ -92,18 +93,18 @@ func (s *Server) setAltSignAddr(c *gin.Context) { canVote, err := canTicketVote(rawTicket, dcrdClient, s.cfg.NetParams) if err != nil { s.log.Errorf("%s: canTicketVote error (ticketHash=%s): %v", funcName, ticketHash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } if !canVote { s.log.Warnf("%s: unvotable ticket (clientIP=%s, ticketHash=%s)", funcName, c.ClientIP(), ticketHash) - s.sendError(errTicketCannotVote, c) + s.sendError(types.ErrTicketCannotVote, c) return } // Send success response to client. - resp, respSig := s.sendJSONResponse(setAltSignAddrResponse{ + resp, respSig := s.sendJSONResponse(types.SetAltSignAddrResponse{ Timestamp: time.Now().Unix(), Request: reqBytes, }, c) diff --git a/webapi/setaltsignaddr_test.go b/webapi/setaltsignaddr_test.go index 6cf424e..773a5ba 100644 --- a/webapi/setaltsignaddr_test.go +++ b/webapi/setaltsignaddr_test.go @@ -21,6 +21,7 @@ import ( dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v3" "github.com/decred/slog" "github.com/decred/vspd/database" + "github.com/decred/vspd/types" "github.com/gin-gonic/gin" ) @@ -211,7 +212,7 @@ func TestSetAltSignAddress(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { ticketHash := randString(64, hexCharset) - req := &setAltSignAddrRequest{ + req := &types.SetAltSignAddrRequest{ Timestamp: time.Now().Unix(), TicketHash: ticketHash, TicketHex: randString(504, hexCharset), diff --git a/webapi/setvotechoices.go b/webapi/setvotechoices.go index 3f3ef37..59f722e 100644 --- a/webapi/setvotechoices.go +++ b/webapi/setvotechoices.go @@ -11,6 +11,7 @@ import ( "github.com/decred/vspd/database" "github.com/decred/vspd/rpc" + "github.com/decred/vspd/types" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" ) @@ -28,20 +29,20 @@ func (s *Server) setVoteChoices(c *gin.Context) { // If we cannot set the vote choices on at least one voting wallet right // now, don't update the database, just return an error. if len(walletClients) == 0 { - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } if !knownTicket { s.log.Warnf("%s: Unknown ticket (clientIP=%s)", funcName, c.ClientIP()) - s.sendError(errUnknownTicket, c) + s.sendError(types.ErrUnknownTicket, c) return } if ticket.FeeTxStatus == database.NoFee { s.log.Warnf("%s: No fee tx for ticket (clientIP=%s, ticketHash=%s)", funcName, c.ClientIP(), ticket.Hash) - s.sendError(errFeeNotReceived, c) + s.sendError(types.ErrFeeNotReceived, c) return } @@ -50,14 +51,14 @@ func (s *Server) setVoteChoices(c *gin.Context) { s.log.Warnf("%s: Ticket not eligible to vote (clientIP=%s, ticketHash=%s)", funcName, c.ClientIP(), ticket.Hash) s.sendErrorWithMsg(fmt.Sprintf("ticket not eligible to vote (status=%s)", ticket.Outcome), - errTicketCannotVote, c) + types.ErrTicketCannotVote, c) return } - var request setVoteChoicesRequest + var request types.SetVoteChoicesRequest if err := binding.JSON.BindBody(reqBytes, &request); err != nil { s.log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err) - s.sendErrorWithMsg(err.Error(), errBadRequest, c) + s.sendErrorWithMsg(err.Error(), types.ErrBadRequest, c) return } @@ -67,7 +68,7 @@ func (s *Server) setVoteChoices(c *gin.Context) { if err != nil { s.log.Errorf("%s: db.GetVoteChanges error (ticketHash=%s): %v", funcName, ticket.Hash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } @@ -79,7 +80,7 @@ func (s *Server) setVoteChoices(c *gin.Context) { if err != nil { s.log.Errorf("%s: Could not unmarshal vote change record (ticketHash=%s): %v", funcName, ticket.Hash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } @@ -87,7 +88,7 @@ func (s *Server) setVoteChoices(c *gin.Context) { s.log.Warnf("%s: Request uses invalid timestamp, %d is not greater "+ "than %d (ticketHash=%s)", funcName, request.Timestamp, prevReq.Timestamp, ticket.Hash) - s.sendError(errInvalidTimestamp, c) + s.sendError(types.ErrInvalidTimestamp, c) return } } @@ -98,7 +99,7 @@ func (s *Server) setVoteChoices(c *gin.Context) { if err != nil { s.log.Warnf("%s: Invalid consensus vote choices (clientIP=%s, ticketHash=%s): %v", funcName, c.ClientIP(), ticket.Hash, err) - s.sendErrorWithMsg(err.Error(), errInvalidVoteChoices, c) + s.sendErrorWithMsg(err.Error(), types.ErrInvalidVoteChoices, c) return } @@ -106,14 +107,14 @@ func (s *Server) setVoteChoices(c *gin.Context) { if err != nil { s.log.Warnf("%s: Invalid treasury policy (clientIP=%s, ticketHash=%s): %v", funcName, c.ClientIP(), ticket.Hash, err) - s.sendErrorWithMsg(err.Error(), errInvalidVoteChoices, c) + s.sendErrorWithMsg(err.Error(), types.ErrInvalidVoteChoices, c) } err = validTSpendPolicy(request.TSpendPolicy) if err != nil { s.log.Warnf("%s: Invalid tspend policy (clientIP=%s, ticketHash=%s): %v", funcName, c.ClientIP(), ticket.Hash, err) - s.sendErrorWithMsg(err.Error(), errInvalidVoteChoices, c) + s.sendErrorWithMsg(err.Error(), types.ErrInvalidVoteChoices, c) } // Update voting preferences in the database before updating the wallets. DB @@ -135,7 +136,7 @@ func (s *Server) setVoteChoices(c *gin.Context) { if err != nil { s.log.Errorf("%s: db.UpdateTicket error, failed to set consensus vote choices (ticketHash=%s): %v", funcName, ticket.Hash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } @@ -179,7 +180,7 @@ func (s *Server) setVoteChoices(c *gin.Context) { s.log.Debugf("%s: Vote choices updated (ticketHash=%s)", funcName, ticket.Hash) // Send success response to client. - resp, respSig := s.sendJSONResponse(setVoteChoicesResponse{ + resp, respSig := s.sendJSONResponse(types.SetVoteChoicesResponse{ Timestamp: time.Now().Unix(), Request: reqBytes, }, c) diff --git a/webapi/ticketstatus.go b/webapi/ticketstatus.go index 96b4ee2..a8263c8 100644 --- a/webapi/ticketstatus.go +++ b/webapi/ticketstatus.go @@ -8,6 +8,7 @@ import ( "time" "github.com/decred/vspd/database" + "github.com/decred/vspd/types" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" ) @@ -23,14 +24,14 @@ func (s *Server) ticketStatus(c *gin.Context) { if !knownTicket { s.log.Warnf("%s: Unknown ticket (clientIP=%s)", funcName, c.ClientIP()) - s.sendError(errUnknownTicket, c) + s.sendError(types.ErrUnknownTicket, c) return } - var request ticketStatusRequest + var request types.TicketStatusRequest if err := binding.JSON.BindBody(reqBytes, &request); err != nil { s.log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err) - s.sendErrorWithMsg(err.Error(), errBadRequest, c) + s.sendErrorWithMsg(err.Error(), types.ErrBadRequest, c) return } @@ -38,7 +39,7 @@ func (s *Server) ticketStatus(c *gin.Context) { altSignAddrData, err := s.db.AltSignAddrData(ticket.Hash) if err != nil { s.log.Errorf("%s: db.AltSignAddrData error (ticketHash=%s): %v", funcName, ticket.Hash, err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return } @@ -47,7 +48,7 @@ func (s *Server) ticketStatus(c *gin.Context) { altSignAddr = altSignAddrData.AltSignAddr } - s.sendJSONResponse(ticketStatusResponse{ + s.sendJSONResponse(types.TicketStatusResponse{ Timestamp: time.Now().Unix(), Request: reqBytes, TicketConfirmed: ticket.Confirmed, diff --git a/webapi/vspinfo.go b/webapi/vspinfo.go index c7aa262..0fc3288 100644 --- a/webapi/vspinfo.go +++ b/webapi/vspinfo.go @@ -7,6 +7,7 @@ package webapi import ( "time" + "github.com/decred/vspd/types" "github.com/decred/vspd/version" "github.com/gin-gonic/gin" ) @@ -14,7 +15,7 @@ import ( // vspInfo is the handler for "GET /api/v3/vspinfo". func (s *Server) vspInfo(c *gin.Context) { cachedStats := s.cache.getData() - s.sendJSONResponse(vspInfoResponse{ + s.sendJSONResponse(types.VspInfoResponse{ APIVersions: []int64{3}, Timestamp: time.Now().Unix(), PubKey: s.signPubKey, diff --git a/webapi/webapi.go b/webapi/webapi.go index ae06e65..03f05fd 100644 --- a/webapi/webapi.go +++ b/webapi/webapi.go @@ -21,6 +21,7 @@ import ( "github.com/decred/slog" "github.com/decred/vspd/database" "github.com/decred/vspd/rpc" + "github.com/decred/vspd/types" "github.com/dustin/go-humanize" "github.com/gin-gonic/gin" "github.com/gorilla/sessions" @@ -276,7 +277,7 @@ func (s *Server) sendJSONResponse(resp interface{}, c *gin.Context) (string, str dec, err := json.Marshal(resp) if err != nil { s.log.Errorf("JSON marshal error: %v", err) - s.sendError(errInternalError, c) + s.sendError(types.ErrInternalError, c) return "", "" } @@ -289,21 +290,21 @@ func (s *Server) sendJSONResponse(resp interface{}, c *gin.Context) (string, str return string(dec), sigStr } -// sendError sends an error response to the client using the default error -// message. -func (s *Server) sendError(e apiError, c *gin.Context) { - msg := e.Error() +// sendError sends an error response with the provided error code and the +// default message for that code. +func (s *Server) sendError(e types.ErrorCode, c *gin.Context) { + msg := e.DefaultMessage() s.sendErrorWithMsg(msg, e, c) } -// sendErrorWithMsg sends an error response to the client using the provided -// error message. -func (s *Server) sendErrorWithMsg(msg string, e apiError, c *gin.Context) { - status := e.httpStatus() +// sendErrorWithMsg sends an error response with the provided error code and +// message. +func (s *Server) sendErrorWithMsg(msg string, e types.ErrorCode, c *gin.Context) { + status := e.HTTPStatus() - resp := gin.H{ - "code": int(e), - "message": msg, + resp := types.APIError{ + Code: int64(e), + Message: msg, } // Try to sign the error response. If it fails, send it without a signature.