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.