[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
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"github.com/daglabs/btcd/apiserver/database"
"github.com/daglabs/btcd/apiserver/jsonrpc"
"github.com/daglabs/btcd/apiserver/models"
"github.com/daglabs/btcd/apiserver/utils"
"github.com/daglabs/btcd/btcjson"
"github.com/daglabs/btcd/util/daghash"
"github.com/daglabs/btcd/wire"
"github.com/jinzhu/gorm"
)
@ -47,7 +52,7 @@ func GetTransactionByHashHandler(txHash string) (interface{}, *utils.HandlerErro
db, err := database.DB()
if err != nil {
return nil, utils.NewInternalServerHandlerError(err.Error())
return nil, utils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
tx := &models.Transaction{}
@ -72,7 +77,7 @@ func GetTransactionsByAddressHandler(address string, skip uint64, limit uint64)
db, err := database.DB()
if err != nil {
return nil, utils.NewInternalServerHandlerError(err.Error())
return nil, utils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
txs := []*models.Transaction{}
@ -102,7 +107,7 @@ func GetTransactionsByAddressHandler(address string, skip uint64, limit uint64)
func GetUTXOsByAddressHandler(address string) (interface{}, *utils.HandlerError) {
db, err := database.DB()
if err != nil {
return nil, utils.NewInternalServerHandlerError(err.Error())
return nil, utils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
var transactionOutputs []*models.TransactionOutput
@ -135,3 +140,45 @@ func addTxPreloadedFields(query *gorm.DB) *gorm.DB {
Preload("TransactionInputs.TransactionOutput.Transaction").
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 {
return err
}
db.SetLogger(gormLogger{})
return nil
}

View File

@ -3,9 +3,11 @@ package jsonrpc
import (
"errors"
"fmt"
"io/ioutil"
"time"
"github.com/daglabs/btcd/apiserver/config"
"github.com/daglabs/btcd/util/daghash"
"io/ioutil"
"github.com/daglabs/btcd/rpcclient"
"github.com/daglabs/btcd/util"
@ -62,11 +64,12 @@ func Connect(cfg *config.Config) error {
}
connCfg := &rpcclient.ConnConfig{
Host: cfg.RPCServer,
Endpoint: "ws",
User: cfg.RPCUser,
Pass: cfg.RPCPassword,
DisableTLS: cfg.DisableTLS,
Host: cfg.RPCServer,
Endpoint: "ws",
User: cfg.RPCUser,
Pass: cfg.RPCPassword,
DisableTLS: cfg.DisableTLS,
RequestTimeout: time.Second * 5,
}
if !cfg.DisableTLS {

View File

@ -3,6 +3,7 @@ package server
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strconv"
@ -30,23 +31,36 @@ const (
defaultGetBlocksOrder = controllers.OrderAscending
)
func makeHandler(
handler func(ctx *utils.APIServerContext, routeParams map[string]string, queryParams map[string]string) (
interface{}, *utils.HandlerError)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
type handlerFunc func(ctx *utils.APIServerContext, routeParams map[string]string, queryParams map[string]string, requestBody []byte) (
interface{}, *utils.HandlerError)
func makeHandler(handler handlerFunc) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
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())
if hErr != nil {
sendErr(ctx, w, hErr)
return
}
response, hErr := handler(ctx, mux.Vars(r), flattenedQueryParams)
response, hErr := handler(ctx, mux.Vars(r), flattenedQueryParams, requestBody)
if hErr != nil {
sendErr(ctx, w, hErr)
return
}
sendJSONResponse(w, response)
if response != nil {
sendJSONResponse(w, response)
}
}
}
@ -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 {
Message string `json:"message"`
}{
@ -133,6 +147,11 @@ func addRoutes(router *mux.Router) {
"/fee-estimates",
makeHandler(getFeeEstimatesHandler)).
Methods("GET")
router.HandleFunc(
"/transaction",
makeHandler(postTransactionHandler)).
Methods("POST")
}
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
}
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])
}
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])
}
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)
if hErr != nil {
return nil, hErr
@ -167,25 +192,34 @@ func getTransactionsByAddressHandler(_ *utils.APIServerContext, routeParams map[
var err error
skip, err = strconv.Atoi(queryParams[queryParamLimit])
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))
}
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])
}
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])
}
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()
}
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)
if hErr != nil {
return nil, hErr
@ -203,3 +237,8 @@ func getBlocksHandler(_ *utils.APIServerContext, _ map[string]string, queryParam
}
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 (
"context"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"net/http"
"time"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
)
const gracefulShutdownTimeout = 30 * time.Second
@ -26,6 +27,7 @@ func Start(listenAddr string) func() {
spawn(func() {
log.Errorf("%s", httpServer.ListenAndServe())
})
return func() {
ctx, cancel := context.WithTimeout(context.Background(), gracefulShutdownTimeout)
defer cancel()

View File

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