[NOD-309] post transaction (#403)

* [NOD-319] Add query params to api server route handler

* Temp commit

* [NOD-322] Make database.DB a function

* [NOD-322] Move context to be the first parameter in all functions

* [NOD-322] Set db to nil on database.Close()

* [NOD-322] Tidy go.mod/go.sum

* [NOD-323] Move rpc-client to separate package

* [NOD-309] Add controller for POST /transaction

* [NOD-309] Added route for POST /transaction

* [NOD-309] in POST /transaction: Forward reject errors to client

* [NOD-309] Added custom client messages to errors in POST /transaction

* [NOD-309] Use utils.NewInternalServerHandlerError where appropriate
This commit is contained in:
Svarog 2019-09-18 16:09:48 +03:00 committed by Ori Newman
parent 47c5eddf38
commit d4083cbdbe
8 changed files with 125 additions and 27 deletions

View File

@ -0,0 +1,6 @@
package controllers
// RawTransaction represents a raw transaction posted to the API server
type RawTransaction struct {
RawTransaction string `json:"rawTransaction"`
}

View File

@ -1,14 +1,19 @@
package controllers package controllers
import ( import (
"bytes"
"encoding/hex" "encoding/hex"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"github.com/daglabs/btcd/apiserver/database" "github.com/daglabs/btcd/apiserver/database"
"github.com/daglabs/btcd/apiserver/jsonrpc"
"github.com/daglabs/btcd/apiserver/models" "github.com/daglabs/btcd/apiserver/models"
"github.com/daglabs/btcd/apiserver/utils" "github.com/daglabs/btcd/apiserver/utils"
"github.com/daglabs/btcd/btcjson"
"github.com/daglabs/btcd/util/daghash" "github.com/daglabs/btcd/util/daghash"
"github.com/daglabs/btcd/wire"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
) )
@ -47,7 +52,7 @@ func GetTransactionByHashHandler(txHash string) (interface{}, *utils.HandlerErro
db, err := database.DB() db, err := database.DB()
if err != nil { if err != nil {
return nil, utils.NewInternalServerHandlerError(err.Error()) return nil, utils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
} }
tx := &models.Transaction{} tx := &models.Transaction{}
@ -72,7 +77,7 @@ func GetTransactionsByAddressHandler(address string, skip uint64, limit uint64)
db, err := database.DB() db, err := database.DB()
if err != nil { if err != nil {
return nil, utils.NewInternalServerHandlerError(err.Error()) return nil, utils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
} }
txs := []*models.Transaction{} txs := []*models.Transaction{}
@ -102,7 +107,7 @@ func GetTransactionsByAddressHandler(address string, skip uint64, limit uint64)
func GetUTXOsByAddressHandler(address string) (interface{}, *utils.HandlerError) { func GetUTXOsByAddressHandler(address string) (interface{}, *utils.HandlerError) {
db, err := database.DB() db, err := database.DB()
if err != nil { if err != nil {
return nil, utils.NewInternalServerHandlerError(err.Error()) return nil, utils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
} }
var transactionOutputs []*models.TransactionOutput var transactionOutputs []*models.TransactionOutput
@ -135,3 +140,45 @@ func addTxPreloadedFields(query *gorm.DB) *gorm.DB {
Preload("TransactionInputs.TransactionOutput.Transaction"). Preload("TransactionInputs.TransactionOutput.Transaction").
Preload("TransactionInputs.TransactionOutput.Address") Preload("TransactionInputs.TransactionOutput.Address")
} }
// PostTransaction forwards a raw transaction to the JSON-RPC API server
func PostTransaction(requestBody []byte) *utils.HandlerError {
client, err := jsonrpc.GetClient()
if err != nil {
return utils.NewInternalServerHandlerError(err.Error())
}
rawTx := &RawTransaction{}
err = json.Unmarshal(requestBody, rawTx)
if err != nil {
return utils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity,
fmt.Sprintf("Error unmarshalling request body: %s", err),
"The request body is not json-formatted")
}
txBytes, err := hex.DecodeString(rawTx.RawTransaction)
if err != nil {
return utils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity,
fmt.Sprintf("Error decoding hex raw transaction: %s", err),
"The raw transaction is not a hex-encoded transaction")
}
txReader := bytes.NewReader(txBytes)
tx := &wire.MsgTx{}
err = tx.BtcDecode(txReader, 0)
if err != nil {
return utils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity,
fmt.Sprintf("Error decoding raw transaction: %s", err),
"Error decoding raw transaction")
}
_, err = client.SendRawTransaction(tx, true)
if err != nil {
if rpcErr, ok := err.(btcjson.RPCError); ok && rpcErr.Code == btcjson.ErrRPCVerify {
return utils.NewHandlerError(http.StatusInternalServerError, rpcErr.Message)
}
return utils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
return nil
}

View File

@ -51,6 +51,7 @@ func Connect(cfg *config.Config) error {
if err != nil { if err != nil {
return err return err
} }
db.SetLogger(gormLogger{}) db.SetLogger(gormLogger{})
return nil return nil
} }

View File

@ -3,9 +3,11 @@ package jsonrpc
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"time"
"github.com/daglabs/btcd/apiserver/config" "github.com/daglabs/btcd/apiserver/config"
"github.com/daglabs/btcd/util/daghash" "github.com/daglabs/btcd/util/daghash"
"io/ioutil"
"github.com/daglabs/btcd/rpcclient" "github.com/daglabs/btcd/rpcclient"
"github.com/daglabs/btcd/util" "github.com/daglabs/btcd/util"
@ -67,6 +69,7 @@ func Connect(cfg *config.Config) error {
User: cfg.RPCUser, User: cfg.RPCUser,
Pass: cfg.RPCPassword, Pass: cfg.RPCPassword,
DisableTLS: cfg.DisableTLS, DisableTLS: cfg.DisableTLS,
RequestTimeout: time.Second * 5,
} }
if !cfg.DisableTLS { if !cfg.DisableTLS {

View File

@ -3,6 +3,7 @@ package server
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"strconv" "strconv"
@ -30,24 +31,37 @@ const (
defaultGetBlocksOrder = controllers.OrderAscending defaultGetBlocksOrder = controllers.OrderAscending
) )
func makeHandler( type handlerFunc func(ctx *utils.APIServerContext, routeParams map[string]string, queryParams map[string]string, requestBody []byte) (
handler func(ctx *utils.APIServerContext, routeParams map[string]string, queryParams map[string]string) ( interface{}, *utils.HandlerError)
interface{}, *utils.HandlerError)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
func makeHandler(handler handlerFunc) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
ctx := utils.ToAPIServerContext(r.Context()) ctx := utils.ToAPIServerContext(r.Context())
var requestBody []byte
if r.Method == "POST" {
var err error
requestBody, err = ioutil.ReadAll(r.Body)
if err != nil {
sendErr(ctx, w, utils.NewHandlerError(500, "Internal server error occured"))
}
}
flattenedQueryParams, hErr := flattenQueryParams(r.URL.Query()) flattenedQueryParams, hErr := flattenQueryParams(r.URL.Query())
if hErr != nil { if hErr != nil {
sendErr(ctx, w, hErr) sendErr(ctx, w, hErr)
return return
} }
response, hErr := handler(ctx, mux.Vars(r), flattenedQueryParams)
response, hErr := handler(ctx, mux.Vars(r), flattenedQueryParams, requestBody)
if hErr != nil { if hErr != nil {
sendErr(ctx, w, hErr) sendErr(ctx, w, hErr)
return return
} }
if response != nil {
sendJSONResponse(w, response) sendJSONResponse(w, response)
} }
}
} }
func flattenQueryParams(queryParams map[string][]string) (map[string]string, *utils.HandlerError) { func flattenQueryParams(queryParams map[string][]string) (map[string]string, *utils.HandlerError) {
@ -88,7 +102,7 @@ func sendJSONResponse(w http.ResponseWriter, response interface{}) {
} }
} }
func mainHandler(_ *utils.APIServerContext, _ map[string]string, _ map[string]string) (interface{}, *utils.HandlerError) { func mainHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map[string]string, _ []byte) (interface{}, *utils.HandlerError) {
return struct { return struct {
Message string `json:"message"` Message string `json:"message"`
}{ }{
@ -133,6 +147,11 @@ func addRoutes(router *mux.Router) {
"/fee-estimates", "/fee-estimates",
makeHandler(getFeeEstimatesHandler)). makeHandler(getFeeEstimatesHandler)).
Methods("GET") Methods("GET")
router.HandleFunc(
"/transaction",
makeHandler(postTransactionHandler)).
Methods("POST")
} }
func convertQueryParamToInt(queryParams map[string]string, param string, defaultValue int) (int, *utils.HandlerError) { func convertQueryParamToInt(queryParams map[string]string, param string, defaultValue int) (int, *utils.HandlerError) {
@ -146,15 +165,21 @@ func convertQueryParamToInt(queryParams map[string]string, param string, default
return defaultValue, nil return defaultValue, nil
} }
func getTransactionByIDHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map[string]string) (interface{}, *utils.HandlerError) { func getTransactionByIDHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map[string]string,
_ []byte) (interface{}, *utils.HandlerError) {
return controllers.GetTransactionByIDHandler(routeParams[routeParamTxID]) return controllers.GetTransactionByIDHandler(routeParams[routeParamTxID])
} }
func getTransactionByHashHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map[string]string) (interface{}, *utils.HandlerError) { func getTransactionByHashHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map[string]string,
_ []byte) (interface{}, *utils.HandlerError) {
return controllers.GetTransactionByHashHandler(routeParams[routeParamTxHash]) return controllers.GetTransactionByHashHandler(routeParams[routeParamTxHash])
} }
func getTransactionsByAddressHandler(_ *utils.APIServerContext, routeParams map[string]string, queryParams map[string]string) (interface{}, *utils.HandlerError) { func getTransactionsByAddressHandler(_ *utils.APIServerContext, routeParams map[string]string, queryParams map[string]string,
_ []byte) (interface{}, *utils.HandlerError) {
skip, hErr := convertQueryParamToInt(queryParams, queryParamSkip, 0) skip, hErr := convertQueryParamToInt(queryParams, queryParamSkip, 0)
if hErr != nil { if hErr != nil {
return nil, hErr return nil, hErr
@ -167,25 +192,34 @@ func getTransactionsByAddressHandler(_ *utils.APIServerContext, routeParams map[
var err error var err error
skip, err = strconv.Atoi(queryParams[queryParamLimit]) skip, err = strconv.Atoi(queryParams[queryParamLimit])
if err != nil { if err != nil {
return nil, utils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("Couldn't parse the '%s' query parameter: %s", queryParamLimit, err)) return nil, utils.NewHandlerError(http.StatusUnprocessableEntity,
fmt.Sprintf("Couldn't parse the '%s' query parameter: %s", queryParamLimit, err))
} }
} }
return controllers.GetTransactionsByAddressHandler(routeParams[routeParamAddress], uint64(skip), uint64(limit)) return controllers.GetTransactionsByAddressHandler(routeParams[routeParamAddress], uint64(skip), uint64(limit))
} }
func getUTXOsByAddressHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map[string]string) (interface{}, *utils.HandlerError) { func getUTXOsByAddressHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map[string]string,
_ []byte) (interface{}, *utils.HandlerError) {
return controllers.GetUTXOsByAddressHandler(routeParams[routeParamAddress]) return controllers.GetUTXOsByAddressHandler(routeParams[routeParamAddress])
} }
func getBlockByHashHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map[string]string) (interface{}, *utils.HandlerError) { func getBlockByHashHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map[string]string,
_ []byte) (interface{}, *utils.HandlerError) {
return controllers.GetBlockByHashHandler(routeParams[routeParamBlockHash]) return controllers.GetBlockByHashHandler(routeParams[routeParamBlockHash])
} }
func getFeeEstimatesHandler(_ *utils.APIServerContext, _ map[string]string, _ map[string]string) (interface{}, *utils.HandlerError) { func getFeeEstimatesHandler(_ *utils.APIServerContext, _ map[string]string, _ map[string]string,
_ []byte) (interface{}, *utils.HandlerError) {
return controllers.GetFeeEstimatesHandler() return controllers.GetFeeEstimatesHandler()
} }
func getBlocksHandler(_ *utils.APIServerContext, _ map[string]string, queryParams map[string]string) (interface{}, *utils.HandlerError) { func getBlocksHandler(_ *utils.APIServerContext, _ map[string]string, queryParams map[string]string,
_ []byte) (interface{}, *utils.HandlerError) {
skip, hErr := convertQueryParamToInt(queryParams, queryParamSkip, 0) skip, hErr := convertQueryParamToInt(queryParams, queryParamSkip, 0)
if hErr != nil { if hErr != nil {
return nil, hErr return nil, hErr
@ -203,3 +237,8 @@ func getBlocksHandler(_ *utils.APIServerContext, _ map[string]string, queryParam
} }
return controllers.GetBlocksHandler(order, uint64(skip), uint64(limit)) return controllers.GetBlocksHandler(order, uint64(skip), uint64(limit))
} }
func postTransactionHandler(_ *utils.APIServerContext, _ map[string]string, _ map[string]string,
requestBody []byte) (interface{}, *utils.HandlerError) {
return nil, controllers.PostTransaction(requestBody)
}

View File

@ -2,10 +2,11 @@ package server
import ( import (
"context" "context"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"net/http" "net/http"
"time" "time"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
) )
const gracefulShutdownTimeout = 30 * time.Second const gracefulShutdownTimeout = 30 * time.Second
@ -26,6 +27,7 @@ func Start(listenAddr string) func() {
spawn(func() { spawn(func() {
log.Errorf("%s", httpServer.ListenAndServe()) log.Errorf("%s", httpServer.ListenAndServe())
}) })
return func() { return func() {
ctx, cancel := context.WithTimeout(context.Background(), gracefulShutdownTimeout) ctx, cancel := context.WithTimeout(context.Background(), gracefulShutdownTimeout)
defer cancel() defer cancel()

View File

@ -3418,7 +3418,7 @@ func handleSendRawTransaction(s *Server, cmd interface{}, closeChan <-chan struc
tx.ID(), err) tx.ID(), err)
} }
return nil, &btcjson.RPCError{ return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCDeserialization, Code: btcjson.ErrRPCVerify,
Message: "TX rejected: " + err.Error(), Message: "TX rejected: " + err.Error(),
} }
} }