[NOD-350] Implement testnet faucet (#438)

* [NOD-350] Implement testnet faucet

* [NOD-350] Add JSON annotations to api server response types

* [NOD-350] Fix IP check query, update IP usage with upsert, and make IP a primary key

* [NOD-377] Remove redundant float conversion

* [NOD-377] Change not current database error message

* [NOD-377] change API route from /money_request to /request_money

* [NOD-377] Add a constant for 24 hours

* [NOD-377] Remove redundant call for getWalletUTXOSet()

* [NOD-377] Condition refactoring

* [NOD-377] Fix POST request to API server content type

* [NOD-350] Rename day -> timeBetweenRequests

* [NOD-377] Rename timeBetweenRequests -> minRequestInterval, timeBefore24Hours -> minRequestInterval

* [NOD-350] Rename file responsetypes -> response_types

* [NOD-350] Rename convertTxModelToTxResponse -> convertTxDBModelToTxResponse

* [NOD-350] Explicitly select blue_score in fetchSelectedTipBlueScore

* [NOD-350] Refactor and add comments

* [NOD-350] Make calcFee use MassPerTxByte

* [NOD-350] Convert IP column to varchar(39) to allow ipv6 addresses

* [NOD-350] Add comments to isFundedAndIsChangeOutputRequired

* [NOD-350] Remove approximateConfirmationsForCoinbaseMaturity

* [NOD-350] Fix comments
This commit is contained in:
Ori Newman 2019-10-31 11:59:56 +02:00 committed by Svarog
parent c66fb294c8
commit 0e278ca22b
35 changed files with 1450 additions and 476 deletions

View File

@ -1,4 +1,4 @@
package controllers
package apimodels
// RawTransaction represents a raw transaction posted to the API server
type RawTransaction struct {

View File

@ -0,0 +1,63 @@
package apimodels
// TransactionResponse is a json representation of a transaction
type TransactionResponse struct {
TransactionHash string `json:"transactionHash"`
TransactionID string `json:"transactionId"`
AcceptingBlockHash string `json:"acceptingBlockHash,omitempty"`
AcceptingBlockBlueScore uint64 `json:"acceptingBlockBlueScore,omitempty"`
SubnetworkID string `json:"subnetworkId"`
LockTime uint64 `json:"lockTime"`
Gas uint64 `json:"gas,omitempty"`
PayloadHash string `json:"payloadHash,omitempty"`
Payload string `json:"payload,omitempty"`
Inputs []*TransactionInputResponse `json:"inputs"`
Outputs []*TransactionOutputResponse `json:"outputs"`
Mass uint64 `json:"mass"`
}
// TransactionOutputResponse is a json representation of a transaction output
type TransactionOutputResponse struct {
TransactionID string `json:"transactionId,omitempty"`
Value uint64 `json:"value"`
ScriptPubKey string `json:"scriptPubKey"`
Address string `json:"address,omitempty"`
AcceptingBlockHash *string `json:"acceptingBlockHash,omitempty"`
AcceptingBlockBlueScore uint64 `json:"acceptingBlockBlueScore,omitempty"`
Index uint32 `json:"index"`
IsCoinbase *bool `json:"isCoinbase,omitempty"`
Confirmations *uint64 `json:"confirmations,omitempty"`
}
// TransactionInputResponse is a json representation of a transaction input
type TransactionInputResponse struct {
TransactionID string `json:"transactionId,omitempty"`
PreviousTransactionID string `json:"previousTransactionId"`
PreviousTransactionOutputIndex uint32 `json:"previousTransactionOutputIndex"`
SignatureScript string `json:"signatureScript"`
Sequence uint64 `json:"sequence"`
Address string `json:"address"`
}
// BlockResponse is a json representation of a block
type BlockResponse struct {
BlockHash string `json:"blockHash"`
Version int32 `json:"version"`
HashMerkleRoot string `json:"hashMerkleRoot"`
AcceptedIDMerkleRoot string `json:"acceptedIDMerkleRoot"`
UTXOCommitment string `json:"utxoCommitment"`
Timestamp uint64 `json:"timestamp"`
Bits uint32 `json:"bits"`
Nonce uint64 `json:"nonce"`
AcceptingBlockHash *string `json:"acceptingBlockHash"`
BlueScore uint64 `json:"blueScore"`
IsChainBlock bool `json:"isChainBlock"`
Mass uint64 `json:"mass"`
}
// FeeEstimateResponse is a json representation of a fee estimate
type FeeEstimateResponse struct {
HighPriority float64 `json:"highPriority"`
NormalPriority float64 `json:"normalPriority"`
LowPriority float64 `json:"lowPriority"`
}

View File

@ -3,11 +3,12 @@ package controllers
import (
"encoding/hex"
"fmt"
"github.com/daglabs/btcd/apiserver/apimodels"
"github.com/daglabs/btcd/apiserver/dbmodels"
"github.com/daglabs/btcd/httpserverutils"
"net/http"
"github.com/daglabs/btcd/apiserver/database"
"github.com/daglabs/btcd/apiserver/models"
"github.com/daglabs/btcd/apiserver/utils"
"github.com/daglabs/btcd/util/daghash"
)
@ -26,38 +27,38 @@ const (
const maxGetBlocksLimit = 100
// GetBlockByHashHandler returns a block by a given hash.
func GetBlockByHashHandler(blockHash string) (interface{}, *utils.HandlerError) {
func GetBlockByHashHandler(blockHash string) (interface{}, *httpserverutils.HandlerError) {
if bytes, err := hex.DecodeString(blockHash); err != nil || len(bytes) != daghash.HashSize {
return nil, utils.NewHandlerError(http.StatusUnprocessableEntity,
return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity,
fmt.Sprintf("The given block hash is not a hex-encoded %d-byte hash.", daghash.HashSize))
}
db, err := database.DB()
if err != nil {
return nil, utils.NewInternalServerHandlerError(err.Error())
return nil, httpserverutils.NewInternalServerHandlerError(err.Error())
}
block := &models.Block{}
dbResult := db.Where(&models.Block{BlockHash: blockHash}).Preload("AcceptingBlock").First(block)
block := &dbmodels.Block{}
dbResult := db.Where(&dbmodels.Block{BlockHash: blockHash}).Preload("AcceptingBlock").First(block)
dbErrors := dbResult.GetErrors()
if utils.IsDBRecordNotFoundError(dbErrors) {
return nil, utils.NewHandlerError(http.StatusNotFound, "No block with the given block hash was found.")
if httpserverutils.IsDBRecordNotFoundError(dbErrors) {
return nil, httpserverutils.NewHandlerError(http.StatusNotFound, "No block with the given block hash was found.")
}
if utils.HasDBError(dbErrors) {
return nil, utils.NewHandlerErrorFromDBErrors("Some errors where encountered when loading transactions from the database:", dbResult.GetErrors())
if httpserverutils.HasDBError(dbErrors) {
return nil, httpserverutils.NewHandlerErrorFromDBErrors("Some errors were encountered when loading transactions from the database:", dbResult.GetErrors())
}
return convertBlockModelToBlockResponse(block), nil
}
// GetBlocksHandler searches for all blocks
func GetBlocksHandler(order string, skip uint64, limit uint64) (interface{}, *utils.HandlerError) {
func GetBlocksHandler(order string, skip uint64, limit uint64) (interface{}, *httpserverutils.HandlerError) {
if limit > maxGetBlocksLimit {
return nil, utils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("The maximum allowed value for the limit is %d", maxGetTransactionsLimit))
return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("The maximum allowed value for the limit is %d", maxGetTransactionsLimit))
}
blocks := []*models.Block{}
blocks := []*dbmodels.Block{}
db, err := database.DB()
if err != nil {
return nil, utils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
return nil, httpserverutils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
query := db.
Limit(limit).
@ -68,10 +69,10 @@ func GetBlocksHandler(order string, skip uint64, limit uint64) (interface{}, *ut
} else if order == OrderDescending {
query = query.Order("`id` DESC")
} else {
return nil, utils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("'%s' is not a valid order", order))
return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("'%s' is not a valid order", order))
}
query.Find(&blocks)
blockResponses := make([]*blockResponse, len(blocks))
blockResponses := make([]*apimodels.BlockResponse, len(blocks))
for i, block := range blocks {
blockResponses[i] = convertBlockModelToBlockResponse(block)
}

View File

@ -0,0 +1,63 @@
package controllers
import (
"encoding/hex"
"github.com/daglabs/btcd/apiserver/apimodels"
"github.com/daglabs/btcd/apiserver/dbmodels"
"github.com/daglabs/btcd/btcjson"
)
func convertTxDBModelToTxResponse(tx *dbmodels.Transaction) *apimodels.TransactionResponse {
txRes := &apimodels.TransactionResponse{
TransactionHash: tx.TransactionHash,
TransactionID: tx.TransactionID,
AcceptingBlockHash: tx.AcceptingBlock.BlockHash,
AcceptingBlockBlueScore: tx.AcceptingBlock.BlueScore,
SubnetworkID: tx.Subnetwork.SubnetworkID,
LockTime: tx.LockTime,
Gas: tx.Gas,
PayloadHash: tx.PayloadHash,
Payload: hex.EncodeToString(tx.Payload),
Inputs: make([]*apimodels.TransactionInputResponse, len(tx.TransactionInputs)),
Outputs: make([]*apimodels.TransactionOutputResponse, len(tx.TransactionOutputs)),
Mass: tx.Mass,
}
for i, txOut := range tx.TransactionOutputs {
txRes.Outputs[i] = &apimodels.TransactionOutputResponse{
Value: txOut.Value,
ScriptPubKey: hex.EncodeToString(txOut.ScriptPubKey),
Address: txOut.Address.Address,
Index: txOut.Index,
}
}
for i, txIn := range tx.TransactionInputs {
txRes.Inputs[i] = &apimodels.TransactionInputResponse{
PreviousTransactionID: txIn.PreviousTransactionOutput.Transaction.TransactionID,
PreviousTransactionOutputIndex: txIn.PreviousTransactionOutput.Index,
SignatureScript: hex.EncodeToString(txIn.SignatureScript),
Sequence: txIn.Sequence,
Address: txIn.PreviousTransactionOutput.Address.Address,
}
}
return txRes
}
func convertBlockModelToBlockResponse(block *dbmodels.Block) *apimodels.BlockResponse {
blockRes := &apimodels.BlockResponse{
BlockHash: block.BlockHash,
Version: block.Version,
HashMerkleRoot: block.HashMerkleRoot,
AcceptedIDMerkleRoot: block.AcceptedIDMerkleRoot,
UTXOCommitment: block.UTXOCommitment,
Timestamp: uint64(block.Timestamp.Unix()),
Bits: block.Bits,
Nonce: block.Nonce,
BlueScore: block.BlueScore,
IsChainBlock: block.IsChainBlock,
Mass: block.Mass,
}
if block.AcceptingBlock != nil {
blockRes.AcceptingBlockHash = btcjson.String(block.AcceptingBlock.BlockHash)
}
return blockRes
}

View File

@ -1,11 +1,14 @@
package controllers
import "github.com/daglabs/btcd/apiserver/utils"
import (
"github.com/daglabs/btcd/apiserver/apimodels"
"github.com/daglabs/btcd/httpserverutils"
)
// GetFeeEstimatesHandler returns the fee estimates for different priorities
// for accepting a transaction in the DAG.
func GetFeeEstimatesHandler() (interface{}, *utils.HandlerError) {
return &feeEstimateResponse{
func GetFeeEstimatesHandler() (interface{}, *httpserverutils.HandlerError) {
return &apimodels.FeeEstimateResponse{
HighPriority: 3,
NormalPriority: 2,
LowPriority: 1,

View File

@ -1,113 +0,0 @@
package controllers
import (
"encoding/hex"
"github.com/daglabs/btcd/apiserver/models"
"github.com/daglabs/btcd/btcjson"
)
type transactionResponse struct {
TransactionHash string `json:"transactionHash"`
TransactionID string `json:"transactionId"`
AcceptingBlockHash string `json:"acceptingBlockHash,omitempty"`
AcceptingBlockBlueScore uint64 `json:"acceptingBlockBlueScore,omitempty"`
SubnetworkID string `json:"subnetworkId"`
LockTime uint64 `json:"lockTime"`
Gas uint64 `json:"gas,omitempty"`
PayloadHash string `json:"payloadHash,omitempty"`
Payload string `json:"payload,omitempty"`
Inputs []*transactionInputResponse `json:"inputs"`
Outputs []*transactionOutputResponse `json:"outputs"`
Mass uint64 `json:"mass"`
}
type transactionOutputResponse struct {
TransactionID string `json:"transactionId,omitempty"`
Value uint64 `json:"value"`
ScriptPubKey string `json:"scriptPubKey"`
Address string `json:"address,omitempty"`
AcceptingBlockHash string `json:"acceptingBlockHash,omitempty"`
AcceptingBlockBlueScore uint64 `json:"acceptingBlockBlueScore,omitempty"`
}
type transactionInputResponse struct {
TransactionID string `json:"transactionId,omitempty"`
PreviousTransactionID string `json:"previousTransactionId"`
PreviousTransactionOutputIndex uint32 `json:"previousTransactionOutputIndex"`
SignatureScript string `json:"signatureScript"`
Sequence uint64 `json:"sequence"`
Address string `json:"address"`
}
type blockResponse struct {
BlockHash string
Version int32
HashMerkleRoot string
AcceptedIDMerkleRoot string
UTXOCommitment string
Timestamp uint64
Bits uint32
Nonce uint64
AcceptingBlockHash *string
BlueScore uint64
IsChainBlock bool
Mass uint64
}
type feeEstimateResponse struct {
HighPriority, NormalPriority, LowPriority float64
}
func convertTxModelToTxResponse(tx *models.Transaction) *transactionResponse {
txRes := &transactionResponse{
TransactionHash: tx.TransactionHash,
TransactionID: tx.TransactionID,
AcceptingBlockHash: tx.AcceptingBlock.BlockHash,
AcceptingBlockBlueScore: tx.AcceptingBlock.BlueScore,
SubnetworkID: tx.Subnetwork.SubnetworkID,
LockTime: tx.LockTime,
Gas: tx.Gas,
PayloadHash: tx.PayloadHash,
Payload: hex.EncodeToString(tx.Payload),
Inputs: make([]*transactionInputResponse, len(tx.TransactionInputs)),
Outputs: make([]*transactionOutputResponse, len(tx.TransactionOutputs)),
Mass: tx.Mass,
}
for i, txOut := range tx.TransactionOutputs {
txRes.Outputs[i] = &transactionOutputResponse{
Value: txOut.Value,
ScriptPubKey: hex.EncodeToString(txOut.ScriptPubKey),
Address: txOut.Address.Address,
}
}
for i, txIn := range tx.TransactionInputs {
txRes.Inputs[i] = &transactionInputResponse{
PreviousTransactionID: txIn.PreviousTransactionOutput.Transaction.TransactionID,
PreviousTransactionOutputIndex: txIn.PreviousTransactionOutput.Index,
SignatureScript: hex.EncodeToString(txIn.SignatureScript),
Sequence: txIn.Sequence,
Address: txIn.PreviousTransactionOutput.Address.Address,
}
}
return txRes
}
func convertBlockModelToBlockResponse(block *models.Block) *blockResponse {
blockRes := &blockResponse{
BlockHash: block.BlockHash,
Version: block.Version,
HashMerkleRoot: block.HashMerkleRoot,
AcceptedIDMerkleRoot: block.AcceptedIDMerkleRoot,
UTXOCommitment: block.UTXOCommitment,
Timestamp: uint64(block.Timestamp.Unix()),
Bits: block.Bits,
Nonce: block.Nonce,
BlueScore: block.BlueScore,
IsChainBlock: block.IsChainBlock,
Mass: block.Mass,
}
if block.AcceptingBlock != nil {
blockRes.AcceptingBlockHash = btcjson.String(block.AcceptingBlock.BlockHash)
}
return blockRes
}

View File

@ -5,12 +5,15 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"github.com/daglabs/btcd/apiserver/apimodels"
"github.com/daglabs/btcd/apiserver/dbmodels"
"github.com/daglabs/btcd/blockdag"
"github.com/daglabs/btcd/httpserverutils"
"github.com/daglabs/btcd/util/subnetworkid"
"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"
@ -20,69 +23,69 @@ import (
const maxGetTransactionsLimit = 1000
// GetTransactionByIDHandler returns a transaction by a given transaction ID.
func GetTransactionByIDHandler(txID string) (interface{}, *utils.HandlerError) {
func GetTransactionByIDHandler(txID string) (interface{}, *httpserverutils.HandlerError) {
if bytes, err := hex.DecodeString(txID); err != nil || len(bytes) != daghash.TxIDSize {
return nil, utils.NewHandlerError(http.StatusUnprocessableEntity,
return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity,
fmt.Sprintf("The given txid is not a hex-encoded %d-byte hash.", daghash.TxIDSize))
}
db, err := database.DB()
if err != nil {
return nil, utils.NewInternalServerHandlerError(err.Error())
return nil, httpserverutils.NewInternalServerHandlerError(err.Error())
}
tx := &models.Transaction{}
query := db.Where(&models.Transaction{TransactionID: txID})
tx := &dbmodels.Transaction{}
query := db.Where(&dbmodels.Transaction{TransactionID: txID})
dbResult := addTxPreloadedFields(query).First(&tx)
dbErrors := dbResult.GetErrors()
if utils.IsDBRecordNotFoundError(dbErrors) {
return nil, utils.NewHandlerError(http.StatusNotFound, "No transaction with the given txid was found.")
if httpserverutils.IsDBRecordNotFoundError(dbErrors) {
return nil, httpserverutils.NewHandlerError(http.StatusNotFound, "No transaction with the given txid was found.")
}
if utils.HasDBError(dbErrors) {
return nil, utils.NewHandlerErrorFromDBErrors("Some errors where encountered when loading transaction from the database:", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return nil, httpserverutils.NewHandlerErrorFromDBErrors("Some errors were encountered when loading transaction from the database:", dbErrors)
}
return convertTxModelToTxResponse(tx), nil
return convertTxDBModelToTxResponse(tx), nil
}
// GetTransactionByHashHandler returns a transaction by a given transaction hash.
func GetTransactionByHashHandler(txHash string) (interface{}, *utils.HandlerError) {
func GetTransactionByHashHandler(txHash string) (interface{}, *httpserverutils.HandlerError) {
if bytes, err := hex.DecodeString(txHash); err != nil || len(bytes) != daghash.HashSize {
return nil, utils.NewHandlerError(http.StatusUnprocessableEntity,
return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity,
fmt.Sprintf("The given txhash is not a hex-encoded %d-byte hash.", daghash.HashSize))
}
db, err := database.DB()
if err != nil {
return nil, utils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
return nil, httpserverutils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
tx := &models.Transaction{}
query := db.Where(&models.Transaction{TransactionHash: txHash})
tx := &dbmodels.Transaction{}
query := db.Where(&dbmodels.Transaction{TransactionHash: txHash})
dbResult := addTxPreloadedFields(query).First(&tx)
dbErrors := dbResult.GetErrors()
if utils.IsDBRecordNotFoundError(dbErrors) {
return nil, utils.NewHandlerError(http.StatusNotFound, "No transaction with the given txhash was found.")
if httpserverutils.IsDBRecordNotFoundError(dbErrors) {
return nil, httpserverutils.NewHandlerError(http.StatusNotFound, "No transaction with the given txhash was found.")
}
if utils.HasDBError(dbErrors) {
return nil, utils.NewHandlerErrorFromDBErrors("Some errors where encountered when loading transaction from the database:", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return nil, httpserverutils.NewHandlerErrorFromDBErrors("Some errors were encountered when loading transaction from the database:", dbErrors)
}
return convertTxModelToTxResponse(tx), nil
return convertTxDBModelToTxResponse(tx), nil
}
// GetTransactionsByAddressHandler searches for all transactions
// where the given address is either an input or an output.
func GetTransactionsByAddressHandler(address string, skip uint64, limit uint64) (interface{}, *utils.HandlerError) {
func GetTransactionsByAddressHandler(address string, skip uint64, limit uint64) (interface{}, *httpserverutils.HandlerError) {
if limit > maxGetTransactionsLimit {
return nil, utils.NewHandlerError(http.StatusUnprocessableEntity,
return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity,
fmt.Sprintf("The maximum allowed value for the limit is %d", maxGetTransactionsLimit))
}
db, err := database.DB()
if err != nil {
return nil, utils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
return nil, httpserverutils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
txs := []*models.Transaction{}
txs := []*dbmodels.Transaction{}
query := db.
Joins("LEFT JOIN `transaction_outputs` ON `transaction_outputs`.`transaction_id` = `transactions`.`id`").
Joins("LEFT JOIN `addresses` AS `out_addresses` ON `out_addresses`.`id` = `transaction_outputs`.`address_id`").
@ -96,40 +99,80 @@ func GetTransactionsByAddressHandler(address string, skip uint64, limit uint64)
Order("`transactions`.`id` ASC")
dbResult := addTxPreloadedFields(query).Find(&txs)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return nil, utils.NewHandlerErrorFromDBErrors("Some errors where encountered when loading transactions from the database:", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return nil, httpserverutils.NewHandlerErrorFromDBErrors("Some errors were encountered when loading transactions from the database:", dbErrors)
}
txResponses := make([]*transactionResponse, len(txs))
txResponses := make([]*apimodels.TransactionResponse, len(txs))
for i, tx := range txs {
txResponses[i] = convertTxModelToTxResponse(tx)
txResponses[i] = convertTxDBModelToTxResponse(tx)
}
return txResponses, nil
}
// GetUTXOsByAddressHandler searches for all UTXOs that belong to a certain address.
func GetUTXOsByAddressHandler(address string) (interface{}, *utils.HandlerError) {
func fetchSelectedTipBlueScore() (uint64, *httpserverutils.HandlerError) {
db, err := database.DB()
if err != nil {
return nil, utils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
return 0, httpserverutils.NewInternalServerHandlerError(err.Error())
}
block := &dbmodels.Block{}
dbResult := db.Order("blue_score DESC").
Where(&dbmodels.Block{IsChainBlock: true}).
Select("blue_score").
First(block)
dbErrors := dbResult.GetErrors()
if httpserverutils.HasDBError(dbErrors) {
return 0, httpserverutils.NewHandlerErrorFromDBErrors("Some errors were encountered when loading transactions from the database:", dbErrors)
}
return block.BlueScore, nil
}
// GetUTXOsByAddressHandler searches for all UTXOs that belong to a certain address.
func GetUTXOsByAddressHandler(address string) (interface{}, *httpserverutils.HandlerError) {
db, err := database.DB()
if err != nil {
return nil, httpserverutils.NewInternalServerHandlerError(err.Error())
}
var transactionOutputs []*models.TransactionOutput
var transactionOutputs []*dbmodels.TransactionOutput
dbErrors := db.
Joins("LEFT JOIN `addresses` ON `addresses`.`id` = `transaction_outputs`.`address_id`").
Where("`addresses`.`address` = ? AND `transaction_outputs`.`is_spent` = 0", address).
Preload("Transaction.AcceptingBlock").
Preload("Transaction.Subnetwork").
Find(&transactionOutputs).GetErrors()
if len(dbErrors) > 0 {
return nil, utils.NewHandlerErrorFromDBErrors("Some errors where encountered when loading UTXOs from the database:", dbErrors)
return nil, httpserverutils.NewHandlerErrorFromDBErrors("Some errors were encountered when loading UTXOs from the database:", dbErrors)
}
UTXOsResponses := make([]*transactionOutputResponse, len(transactionOutputs))
selectedTipBlueScore, hErr := fetchSelectedTipBlueScore()
if hErr != nil {
return nil, hErr
}
UTXOsResponses := make([]*apimodels.TransactionOutputResponse, len(transactionOutputs))
for i, transactionOutput := range transactionOutputs {
UTXOsResponses[i] = &transactionOutputResponse{
subnetworkID := &subnetworkid.SubnetworkID{}
err := subnetworkid.Decode(subnetworkID, transactionOutput.Transaction.Subnetwork.SubnetworkID)
if err != nil {
return nil, httpserverutils.NewInternalServerHandlerError(fmt.Sprintf("Couldn't decode subnetwork id %s: %s", transactionOutput.Transaction.Subnetwork.SubnetworkID, err))
}
var acceptingBlockHash *string
var confirmations uint64
acceptingBlockBlueScore := blockdag.UnacceptedBlueScore
if transactionOutput.Transaction.AcceptingBlock != nil {
acceptingBlockHash = btcjson.String(transactionOutput.Transaction.AcceptingBlock.BlockHash)
acceptingBlockBlueScore = transactionOutput.Transaction.AcceptingBlock.BlueScore
confirmations = selectedTipBlueScore - acceptingBlockBlueScore
}
UTXOsResponses[i] = &apimodels.TransactionOutputResponse{
TransactionID: transactionOutput.Transaction.TransactionID,
Value: transactionOutput.Value,
ScriptPubKey: hex.EncodeToString(transactionOutput.ScriptPubKey),
AcceptingBlockHash: transactionOutput.Transaction.AcceptingBlock.BlockHash,
AcceptingBlockBlueScore: transactionOutput.Transaction.AcceptingBlock.BlueScore,
AcceptingBlockHash: acceptingBlockHash,
AcceptingBlockBlueScore: acceptingBlockBlueScore,
Index: transactionOutput.Index,
IsCoinbase: btcjson.Bool(subnetworkID.IsEqual(subnetworkid.SubnetworkIDCoinbase)),
Confirmations: btcjson.Uint64(confirmations),
}
}
return UTXOsResponses, nil
@ -145,23 +188,23 @@ func addTxPreloadedFields(query *gorm.DB) *gorm.DB {
}
// PostTransaction forwards a raw transaction to the JSON-RPC API server
func PostTransaction(requestBody []byte) *utils.HandlerError {
func PostTransaction(requestBody []byte) *httpserverutils.HandlerError {
client, err := jsonrpc.GetClient()
if err != nil {
return utils.NewInternalServerHandlerError(err.Error())
return httpserverutils.NewInternalServerHandlerError(err.Error())
}
rawTx := &RawTransaction{}
rawTx := &apimodels.RawTransaction{}
err = json.Unmarshal(requestBody, rawTx)
if err != nil {
return utils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity,
return httpserverutils.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,
return httpserverutils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity,
fmt.Sprintf("Error decoding hex raw transaction: %s", err),
"The raw transaction is not a hex-encoded transaction")
}
@ -170,17 +213,17 @@ func PostTransaction(requestBody []byte) *utils.HandlerError {
tx := &wire.MsgTx{}
err = tx.BtcDecode(txReader, 0)
if err != nil {
return utils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity,
return httpserverutils.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)
if rpcErr, ok := err.(*btcjson.RPCError); ok && rpcErr.Code == btcjson.ErrRPCVerify {
return httpserverutils.NewHandlerError(http.StatusInternalServerError, rpcErr.Message)
}
return utils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
return httpserverutils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
return nil

View File

@ -1,4 +1,4 @@
package models
package dbmodels
import (
"time"

View File

@ -1,14 +1,12 @@
package server
import (
"encoding/json"
"fmt"
"io/ioutil"
"github.com/daglabs/btcd/httpserverutils"
"net/http"
"strconv"
"github.com/daglabs/btcd/apiserver/controllers"
"github.com/daglabs/btcd/apiserver/utils"
"github.com/gorilla/mux"
)
@ -31,78 +29,7 @@ const (
defaultGetBlocksOrder = controllers.OrderAscending
)
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, requestBody)
if hErr != nil {
sendErr(ctx, w, hErr)
return
}
if response != nil {
sendJSONResponse(w, response)
}
}
}
func flattenQueryParams(queryParams map[string][]string) (map[string]string, *utils.HandlerError) {
flattenedMap := make(map[string]string)
for param, valuesSlice := range queryParams {
if len(valuesSlice) > 1 {
return nil, utils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("Couldn't parse the '%s' query parameter:"+
" expected a single value but got multiple values", param))
}
flattenedMap[param] = valuesSlice[0]
}
return flattenedMap, nil
}
type clientError struct {
ErrorCode int `json:"errorCode"`
ErrorMessage string `json:"errorMessage"`
}
func sendErr(ctx *utils.APIServerContext, w http.ResponseWriter, hErr *utils.HandlerError) {
errMsg := fmt.Sprintf("got error: %s", hErr)
ctx.Warnf(errMsg)
w.WriteHeader(hErr.Code)
sendJSONResponse(w, &clientError{
ErrorCode: hErr.Code,
ErrorMessage: hErr.ClientMessage,
})
}
func sendJSONResponse(w http.ResponseWriter, response interface{}) {
b, err := json.Marshal(response)
if err != nil {
panic(err)
}
_, err = fmt.Fprintf(w, string(b))
if err != nil {
panic(err)
}
}
func mainHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map[string]string, _ []byte) (interface{}, *utils.HandlerError) {
func mainHandler(_ *httpserverutils.ServerContext, _ *http.Request, _ map[string]string, _ map[string]string, _ []byte) (interface{}, *httpserverutils.HandlerError) {
return struct {
Message string `json:"message"`
}{
@ -111,74 +38,74 @@ func mainHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map
}
func addRoutes(router *mux.Router) {
router.HandleFunc("/", makeHandler(mainHandler))
router.HandleFunc("/", httpserverutils.MakeHandler(mainHandler))
router.HandleFunc(
fmt.Sprintf("/transaction/id/{%s}", routeParamTxID),
makeHandler(getTransactionByIDHandler)).
httpserverutils.MakeHandler(getTransactionByIDHandler)).
Methods("GET")
router.HandleFunc(
fmt.Sprintf("/transaction/hash/{%s}", routeParamTxHash),
makeHandler(getTransactionByHashHandler)).
httpserverutils.MakeHandler(getTransactionByHashHandler)).
Methods("GET")
router.HandleFunc(
fmt.Sprintf("/transactions/address/{%s}", routeParamAddress),
makeHandler(getTransactionsByAddressHandler)).
httpserverutils.MakeHandler(getTransactionsByAddressHandler)).
Methods("GET")
router.HandleFunc(
fmt.Sprintf("/utxos/address/{%s}", routeParamAddress),
makeHandler(getUTXOsByAddressHandler)).
httpserverutils.MakeHandler(getUTXOsByAddressHandler)).
Methods("GET")
router.HandleFunc(
fmt.Sprintf("/block/{%s}", routeParamBlockHash),
makeHandler(getBlockByHashHandler)).
httpserverutils.MakeHandler(getBlockByHashHandler)).
Methods("GET")
router.HandleFunc(
"/blocks",
makeHandler(getBlocksHandler)).
httpserverutils.MakeHandler(getBlocksHandler)).
Methods("GET")
router.HandleFunc(
"/fee-estimates",
makeHandler(getFeeEstimatesHandler)).
httpserverutils.MakeHandler(getFeeEstimatesHandler)).
Methods("GET")
router.HandleFunc(
"/transaction",
makeHandler(postTransactionHandler)).
httpserverutils.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, *httpserverutils.HandlerError) {
if _, ok := queryParams[param]; ok {
intValue, err := strconv.Atoi(queryParams[param])
if err != nil {
return 0, utils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("Couldn't parse the '%s' query parameter: %s", param, err))
return 0, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("Couldn't parse the '%s' query parameter: %s", param, err))
}
return intValue, nil
}
return defaultValue, nil
}
func getTransactionByIDHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map[string]string,
_ []byte) (interface{}, *utils.HandlerError) {
func getTransactionByIDHandler(_ *httpserverutils.ServerContext, _ *http.Request, routeParams map[string]string, _ map[string]string,
_ []byte) (interface{}, *httpserverutils.HandlerError) {
return controllers.GetTransactionByIDHandler(routeParams[routeParamTxID])
}
func getTransactionByHashHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map[string]string,
_ []byte) (interface{}, *utils.HandlerError) {
func getTransactionByHashHandler(_ *httpserverutils.ServerContext, _ *http.Request, routeParams map[string]string, _ map[string]string,
_ []byte) (interface{}, *httpserverutils.HandlerError) {
return controllers.GetTransactionByHashHandler(routeParams[routeParamTxHash])
}
func getTransactionsByAddressHandler(_ *utils.APIServerContext, routeParams map[string]string, queryParams map[string]string,
_ []byte) (interface{}, *utils.HandlerError) {
func getTransactionsByAddressHandler(_ *httpserverutils.ServerContext, _ *http.Request, routeParams map[string]string, queryParams map[string]string,
_ []byte) (interface{}, *httpserverutils.HandlerError) {
skip, hErr := convertQueryParamToInt(queryParams, queryParamSkip, 0)
if hErr != nil {
@ -192,33 +119,33 @@ func getTransactionsByAddressHandler(_ *utils.APIServerContext, routeParams map[
var err error
skip, err = strconv.Atoi(queryParams[queryParamLimit])
if err != nil {
return nil, utils.NewHandlerError(http.StatusUnprocessableEntity,
return nil, httpserverutils.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,
_ []byte) (interface{}, *utils.HandlerError) {
func getUTXOsByAddressHandler(_ *httpserverutils.ServerContext, _ *http.Request, routeParams map[string]string, _ map[string]string,
_ []byte) (interface{}, *httpserverutils.HandlerError) {
return controllers.GetUTXOsByAddressHandler(routeParams[routeParamAddress])
}
func getBlockByHashHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map[string]string,
_ []byte) (interface{}, *utils.HandlerError) {
func getBlockByHashHandler(_ *httpserverutils.ServerContext, _ *http.Request, routeParams map[string]string, _ map[string]string,
_ []byte) (interface{}, *httpserverutils.HandlerError) {
return controllers.GetBlockByHashHandler(routeParams[routeParamBlockHash])
}
func getFeeEstimatesHandler(_ *utils.APIServerContext, _ map[string]string, _ map[string]string,
_ []byte) (interface{}, *utils.HandlerError) {
func getFeeEstimatesHandler(_ *httpserverutils.ServerContext, _ *http.Request, _ map[string]string, _ map[string]string,
_ []byte) (interface{}, *httpserverutils.HandlerError) {
return controllers.GetFeeEstimatesHandler()
}
func getBlocksHandler(_ *utils.APIServerContext, _ map[string]string, queryParams map[string]string,
_ []byte) (interface{}, *utils.HandlerError) {
func getBlocksHandler(_ *httpserverutils.ServerContext, _ *http.Request, _ map[string]string, queryParams map[string]string,
_ []byte) (interface{}, *httpserverutils.HandlerError) {
skip, hErr := convertQueryParamToInt(queryParams, queryParamSkip, 0)
if hErr != nil {
@ -231,14 +158,14 @@ func getBlocksHandler(_ *utils.APIServerContext, _ map[string]string, queryParam
order := defaultGetBlocksOrder
if orderParamValue, ok := queryParams[queryParamOrder]; ok {
if orderParamValue != controllers.OrderAscending && orderParamValue != controllers.OrderDescending {
return nil, utils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("'%s' is not a valid value for the '%s' query parameter", orderParamValue, queryParamLimit))
return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("'%s' is not a valid value for the '%s' query parameter", orderParamValue, queryParamLimit))
}
order = orderParamValue
}
return controllers.GetBlocksHandler(order, uint64(skip), uint64(limit))
}
func postTransactionHandler(_ *utils.APIServerContext, _ map[string]string, _ map[string]string,
requestBody []byte) (interface{}, *utils.HandlerError) {
func postTransactionHandler(_ *httpserverutils.ServerContext, _ *http.Request, _ map[string]string, _ map[string]string,
requestBody []byte) (interface{}, *httpserverutils.HandlerError) {
return nil, controllers.PostTransaction(requestBody)
}

View File

@ -2,6 +2,7 @@ package server
import (
"context"
"github.com/daglabs/btcd/httpserverutils"
"net/http"
"time"
@ -15,10 +16,10 @@ const gracefulShutdownTimeout = 30 * time.Second
// function to gracefully shutdown it.
func Start(listenAddr string) func() {
router := mux.NewRouter()
router.Use(addRequestMetadataMiddleware)
router.Use(recoveryMiddleware)
router.Use(loggingMiddleware)
router.Use(setJSONMiddleware)
router.Use(httpserverutils.AddRequestMetadataMiddleware)
router.Use(httpserverutils.RecoveryMiddleware)
router.Use(httpserverutils.LoggingMiddleware)
router.Use(httpserverutils.SetJSONMiddleware)
addRoutes(router)
httpServer := &http.Server{
Addr: listenAddr,

View File

@ -6,10 +6,10 @@ import (
"fmt"
"github.com/daglabs/btcd/apiserver/config"
"github.com/daglabs/btcd/apiserver/database"
"github.com/daglabs/btcd/apiserver/dbmodels"
"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/httpserverutils"
"github.com/daglabs/btcd/txscript"
"github.com/daglabs/btcd/util/daghash"
"github.com/daglabs/btcd/util/subnetworkid"
@ -32,6 +32,7 @@ func startSync(doneChan chan struct{}) error {
if err != nil {
return err
}
log.Infof("Finished syncing past data")
// Keep the node and the API server in sync
sync(client, doneChan)
@ -170,17 +171,17 @@ func findHashOfBluestBlock(mustBeChainBlock bool) (*string, error) {
return nil, err
}
var block models.Block
var block dbmodels.Block
dbQuery := dbTx.Order("blue_score DESC")
if mustBeChainBlock {
dbQuery = dbQuery.Where(&models.Block{IsChainBlock: true})
dbQuery = dbQuery.Where(&dbmodels.Block{IsChainBlock: true})
}
dbResult := dbQuery.First(&block)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return nil, utils.NewErrorFromDBErrors("failed to find hash of bluest block: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return nil, httpserverutils.NewErrorFromDBErrors("failed to find hash of bluest block: ", dbErrors)
}
if utils.IsDBRecordNotFoundError(dbErrors) {
if httpserverutils.IsDBRecordNotFoundError(dbErrors) {
return nil, nil
}
return &block.BlockHash, nil
@ -222,15 +223,15 @@ func addBlocks(client *jsonrpc.Client, blocks []string, rawBlocks []btcjson.GetB
}
func doesBlockExist(dbTx *gorm.DB, blockHash string) (bool, error) {
var dbBlock models.Block
var dbBlock dbmodels.Block
dbResult := dbTx.
Where(&models.Block{BlockHash: blockHash}).
Where(&dbmodels.Block{BlockHash: blockHash}).
First(&dbBlock)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return false, utils.NewErrorFromDBErrors("failed to find block: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return false, httpserverutils.NewErrorFromDBErrors("failed to find block: ", dbErrors)
}
return !utils.IsDBRecordNotFoundError(dbErrors), nil
return !httpserverutils.IsDBRecordNotFoundError(dbErrors), nil
}
// addBlocks inserts all the data that could be gleaned out of the serialized
@ -295,12 +296,12 @@ func addBlock(client *jsonrpc.Client, block string, rawBlock btcjson.GetBlockVer
return nil
}
func insertBlock(dbTx *gorm.DB, rawBlock btcjson.GetBlockVerboseResult) (*models.Block, error) {
func insertBlock(dbTx *gorm.DB, rawBlock btcjson.GetBlockVerboseResult) (*dbmodels.Block, error) {
bits, err := strconv.ParseUint(rawBlock.Bits, 16, 32)
if err != nil {
return nil, err
}
dbBlock := models.Block{
dbBlock := dbmodels.Block{
BlockHash: rawBlock.Hash,
Version: rawBlock.Version,
HashMerkleRoot: rawBlock.HashMerkleRoot,
@ -315,107 +316,107 @@ func insertBlock(dbTx *gorm.DB, rawBlock btcjson.GetBlockVerboseResult) (*models
}
dbResult := dbTx.Create(&dbBlock)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return nil, utils.NewErrorFromDBErrors("failed to insert block: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return nil, httpserverutils.NewErrorFromDBErrors("failed to insert block: ", dbErrors)
}
return &dbBlock, nil
}
func insertBlockParents(dbTx *gorm.DB, rawBlock btcjson.GetBlockVerboseResult, dbBlock *models.Block) error {
func insertBlockParents(dbTx *gorm.DB, rawBlock btcjson.GetBlockVerboseResult, dbBlock *dbmodels.Block) error {
// Exit early if this is the genesis block
if len(rawBlock.ParentHashes) == 0 {
return nil
}
dbWhereBlockIDsIn := make([]*models.Block, len(rawBlock.ParentHashes))
hashesIn := make([]string, len(rawBlock.ParentHashes))
for i, parentHash := range rawBlock.ParentHashes {
dbWhereBlockIDsIn[i] = &models.Block{BlockHash: parentHash}
hashesIn[i] = parentHash
}
var dbParents []models.Block
var dbParents []dbmodels.Block
dbResult := dbTx.
Where(dbWhereBlockIDsIn).
First(&dbParents)
Where("block_hash in (?)", hashesIn).
Find(&dbParents)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to find blocks: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to find blocks: ", dbErrors)
}
if len(dbParents) != len(rawBlock.ParentHashes) {
return fmt.Errorf("some parents are missing for block: %s", rawBlock.Hash)
}
for _, dbParent := range dbParents {
dbParentBlock := models.ParentBlock{
dbParentBlock := dbmodels.ParentBlock{
BlockID: dbBlock.ID,
ParentBlockID: dbParent.ID,
}
dbResult := dbTx.Create(&dbParentBlock)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to insert parentBlock: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to insert parentBlock: ", dbErrors)
}
}
return nil
}
func insertBlockData(dbTx *gorm.DB, block string, dbBlock *models.Block) error {
func insertBlockData(dbTx *gorm.DB, block string, dbBlock *dbmodels.Block) error {
blockData, err := hex.DecodeString(block)
if err != nil {
return err
}
dbRawBlock := models.RawBlock{
dbRawBlock := dbmodels.RawBlock{
BlockID: dbBlock.ID,
BlockData: blockData,
}
dbResult := dbTx.Create(&dbRawBlock)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to insert rawBlock: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to insert rawBlock: ", dbErrors)
}
return nil
}
func insertSubnetwork(dbTx *gorm.DB, transaction *btcjson.TxRawResult, client *jsonrpc.Client) (*models.Subnetwork, error) {
var dbSubnetwork models.Subnetwork
func insertSubnetwork(dbTx *gorm.DB, transaction *btcjson.TxRawResult, client *jsonrpc.Client) (*dbmodels.Subnetwork, error) {
var dbSubnetwork dbmodels.Subnetwork
dbResult := dbTx.
Where(&models.Subnetwork{SubnetworkID: transaction.Subnetwork}).
Where(&dbmodels.Subnetwork{SubnetworkID: transaction.Subnetwork}).
First(&dbSubnetwork)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return nil, utils.NewErrorFromDBErrors("failed to find subnetwork: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return nil, httpserverutils.NewErrorFromDBErrors("failed to find subnetwork: ", dbErrors)
}
if utils.IsDBRecordNotFoundError(dbErrors) {
if httpserverutils.IsDBRecordNotFoundError(dbErrors) {
subnetwork, err := client.GetSubnetwork(transaction.Subnetwork)
if err != nil {
return nil, err
}
dbSubnetwork = models.Subnetwork{
dbSubnetwork = dbmodels.Subnetwork{
SubnetworkID: transaction.Subnetwork,
GasLimit: subnetwork.GasLimit,
}
dbResult := dbTx.Create(&dbSubnetwork)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return nil, utils.NewErrorFromDBErrors("failed to insert subnetwork: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return nil, httpserverutils.NewErrorFromDBErrors("failed to insert subnetwork: ", dbErrors)
}
}
return &dbSubnetwork, nil
}
func insertTransaction(dbTx *gorm.DB, transaction *btcjson.TxRawResult, dbSubnetwork *models.Subnetwork) (*models.Transaction, error) {
var dbTransaction models.Transaction
func insertTransaction(dbTx *gorm.DB, transaction *btcjson.TxRawResult, dbSubnetwork *dbmodels.Subnetwork) (*dbmodels.Transaction, error) {
var dbTransaction dbmodels.Transaction
dbResult := dbTx.
Where(&models.Transaction{TransactionID: transaction.TxID}).
Where(&dbmodels.Transaction{TransactionID: transaction.TxID}).
First(&dbTransaction)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return nil, utils.NewErrorFromDBErrors("failed to find transaction: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return nil, httpserverutils.NewErrorFromDBErrors("failed to find transaction: ", dbErrors)
}
if utils.IsDBRecordNotFoundError(dbErrors) {
if httpserverutils.IsDBRecordNotFoundError(dbErrors) {
payload, err := hex.DecodeString(transaction.Payload)
if err != nil {
return nil, err
}
dbTransaction = models.Transaction{
dbTransaction = dbmodels.Transaction{
TransactionHash: transaction.Hash,
TransactionID: transaction.TxID,
LockTime: transaction.LockTime,
@ -427,38 +428,38 @@ func insertTransaction(dbTx *gorm.DB, transaction *btcjson.TxRawResult, dbSubnet
}
dbResult := dbTx.Create(&dbTransaction)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return nil, utils.NewErrorFromDBErrors("failed to insert transaction: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return nil, httpserverutils.NewErrorFromDBErrors("failed to insert transaction: ", dbErrors)
}
}
return &dbTransaction, nil
}
func insertTransactionBlock(dbTx *gorm.DB, dbBlock *models.Block, dbTransaction *models.Transaction, index uint32) error {
var dbTransactionBlock models.TransactionBlock
func insertTransactionBlock(dbTx *gorm.DB, dbBlock *dbmodels.Block, dbTransaction *dbmodels.Transaction, index uint32) error {
var dbTransactionBlock dbmodels.TransactionBlock
dbResult := dbTx.
Where(&models.TransactionBlock{TransactionID: dbTransaction.ID, BlockID: dbBlock.ID}).
Where(&dbmodels.TransactionBlock{TransactionID: dbTransaction.ID, BlockID: dbBlock.ID}).
First(&dbTransactionBlock)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to find transactionBlock: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to find transactionBlock: ", dbErrors)
}
if utils.IsDBRecordNotFoundError(dbErrors) {
dbTransactionBlock = models.TransactionBlock{
if httpserverutils.IsDBRecordNotFoundError(dbErrors) {
dbTransactionBlock = dbmodels.TransactionBlock{
TransactionID: dbTransaction.ID,
BlockID: dbBlock.ID,
Index: index,
}
dbResult := dbTx.Create(&dbTransactionBlock)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to insert transactionBlock: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to insert transactionBlock: ", dbErrors)
}
}
return nil
}
func insertTransactionInputs(dbTx *gorm.DB, transaction *btcjson.TxRawResult, dbTransaction *models.Transaction) error {
func insertTransactionInputs(dbTx *gorm.DB, transaction *btcjson.TxRawResult, dbTransaction *dbmodels.Transaction) error {
isCoinbase, err := isTransactionCoinbase(transaction)
if err != nil {
return err
@ -484,35 +485,35 @@ func isTransactionCoinbase(transaction *btcjson.TxRawResult) (bool, error) {
return subnetwork.IsEqual(subnetworkid.SubnetworkIDCoinbase), nil
}
func insertTransactionInput(dbTx *gorm.DB, dbTransaction *models.Transaction, input *btcjson.Vin) error {
var dbPreviousTransactionOutput models.TransactionOutput
func insertTransactionInput(dbTx *gorm.DB, dbTransaction *dbmodels.Transaction, input *btcjson.Vin) error {
var dbPreviousTransactionOutput dbmodels.TransactionOutput
dbResult := dbTx.
Joins("LEFT JOIN `transactions` ON `transactions`.`id` = `transaction_outputs`.`transaction_id`").
Where("`transactions`.`transactiond_id` = ? AND `transaction_outputs`.`index` = ?", input.TxID, input.Vout).
First(&dbPreviousTransactionOutput)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to find previous transactionOutput: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to find previous transactionOutput: ", dbErrors)
}
if utils.IsDBRecordNotFoundError(dbErrors) {
if httpserverutils.IsDBRecordNotFoundError(dbErrors) {
return fmt.Errorf("missing output transaction output for txID: %s and index: %d", input.TxID, input.Vout)
}
var dbTransactionInputCount int
dbResult = dbTx.
Model(&models.TransactionInput{}).
Where(&models.TransactionInput{TransactionID: dbTransaction.ID, PreviousTransactionOutputID: dbPreviousTransactionOutput.ID}).
Model(&dbmodels.TransactionInput{}).
Where(&dbmodels.TransactionInput{TransactionID: dbTransaction.ID, PreviousTransactionOutputID: dbPreviousTransactionOutput.ID}).
Count(&dbTransactionInputCount)
dbErrors = dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to find transactionInput: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to find transactionInput: ", dbErrors)
}
if dbTransactionInputCount == 0 {
scriptSig, err := hex.DecodeString(input.ScriptSig.Hex)
if err != nil {
return nil
}
dbTransactionInput := models.TransactionInput{
dbTransactionInput := dbmodels.TransactionInput{
TransactionID: dbTransaction.ID,
PreviousTransactionOutputID: dbPreviousTransactionOutput.ID,
Index: input.Vout,
@ -521,15 +522,15 @@ func insertTransactionInput(dbTx *gorm.DB, dbTransaction *models.Transaction, in
}
dbResult := dbTx.Create(&dbTransactionInput)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to insert transactionInput: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to insert transactionInput: ", dbErrors)
}
}
return nil
}
func insertTransactionOutputs(dbTx *gorm.DB, transaction *btcjson.TxRawResult, dbTransaction *models.Transaction) error {
func insertTransactionOutputs(dbTx *gorm.DB, transaction *btcjson.TxRawResult, dbTransaction *dbmodels.Transaction) error {
for _, output := range transaction.Vout {
scriptPubKey, err := hex.DecodeString(output.ScriptPubKey.Hex)
if err != nil {
@ -547,47 +548,47 @@ func insertTransactionOutputs(dbTx *gorm.DB, transaction *btcjson.TxRawResult, d
return nil
}
func insertAddress(dbTx *gorm.DB, scriptPubKey []byte) (*models.Address, error) {
func insertAddress(dbTx *gorm.DB, scriptPubKey []byte) (*dbmodels.Address, error) {
_, addr, err := txscript.ExtractScriptPubKeyAddress(scriptPubKey, config.ActiveNetParams())
if err != nil {
return nil, err
}
hexAddress := addr.EncodeAddress()
var dbAddress models.Address
var dbAddress dbmodels.Address
dbResult := dbTx.
Where(&models.Address{Address: hexAddress}).
Where(&dbmodels.Address{Address: hexAddress}).
First(&dbAddress)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return nil, utils.NewErrorFromDBErrors("failed to find address: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return nil, httpserverutils.NewErrorFromDBErrors("failed to find address: ", dbErrors)
}
if utils.IsDBRecordNotFoundError(dbErrors) {
dbAddress = models.Address{
if httpserverutils.IsDBRecordNotFoundError(dbErrors) {
dbAddress = dbmodels.Address{
Address: hexAddress,
}
dbResult := dbTx.Create(&dbAddress)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return nil, utils.NewErrorFromDBErrors("failed to insert address: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return nil, httpserverutils.NewErrorFromDBErrors("failed to insert address: ", dbErrors)
}
}
return &dbAddress, nil
}
func insertTransactionOutput(dbTx *gorm.DB, dbTransaction *models.Transaction,
output *btcjson.Vout, scriptPubKey []byte, dbAddress *models.Address) error {
func insertTransactionOutput(dbTx *gorm.DB, dbTransaction *dbmodels.Transaction,
output *btcjson.Vout, scriptPubKey []byte, dbAddress *dbmodels.Address) error {
var dbTransactionOutputCount int
dbResult := dbTx.
Model(&models.TransactionOutput{}).
Where(&models.TransactionOutput{TransactionID: dbTransaction.ID, Index: output.N}).
Model(&dbmodels.TransactionOutput{}).
Where(&dbmodels.TransactionOutput{TransactionID: dbTransaction.ID, Index: output.N}).
Count(&dbTransactionOutputCount)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to find transactionOutput: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to find transactionOutput: ", dbErrors)
}
if dbTransactionOutputCount == 0 {
dbTransactionOutput := models.TransactionOutput{
dbTransactionOutput := dbmodels.TransactionOutput{
TransactionID: dbTransaction.ID,
Index: output.N,
Value: output.Value,
@ -597,8 +598,8 @@ func insertTransactionOutput(dbTx *gorm.DB, dbTransaction *models.Transaction,
}
dbResult := dbTx.Create(&dbTransactionOutput)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to insert transactionOutput: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to insert transactionOutput: ", dbErrors)
}
}
return nil
@ -641,29 +642,29 @@ func updateSelectedParentChain(removedChainHashes []string, addedChainBlocks []b
// * The block is set IsChainBlock = false
// This function will return an error if any of the above are in an unexpected state
func updateRemovedChainHashes(dbTx *gorm.DB, removedHash string) error {
var dbBlock models.Block
var dbBlock dbmodels.Block
dbResult := dbTx.
Where(&models.Block{BlockHash: removedHash}).
Where(&dbmodels.Block{BlockHash: removedHash}).
First(&dbBlock)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to find block: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to find block: ", dbErrors)
}
if utils.IsDBRecordNotFoundError(dbErrors) {
if httpserverutils.IsDBRecordNotFoundError(dbErrors) {
return fmt.Errorf("missing block for hash: %s", removedHash)
}
if !dbBlock.IsChainBlock {
return fmt.Errorf("block erroneously marked as not a chain block: %s", removedHash)
}
var dbTransactions []models.Transaction
var dbTransactions []dbmodels.Transaction
dbResult = dbTx.
Where(&models.Transaction{AcceptingBlockID: &dbBlock.ID}).
Where(&dbmodels.Transaction{AcceptingBlockID: &dbBlock.ID}).
Preload("TransactionInputs.PreviousTransactionOutput").
Find(&dbTransactions)
dbErrors = dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to find transactions: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to find transactions: ", dbErrors)
}
for _, dbTransaction := range dbTransactions {
for _, dbTransactionInput := range dbTransaction.TransactionInputs {
@ -676,24 +677,24 @@ func updateRemovedChainHashes(dbTx *gorm.DB, removedHash string) error {
dbPreviousTransactionOutput.IsSpent = false
dbResult = dbTx.Save(&dbPreviousTransactionOutput)
dbErrors = dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to update transactionOutput: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to update transactionOutput: ", dbErrors)
}
}
dbTransaction.AcceptingBlockID = nil
dbResult := dbTx.Save(&dbTransaction)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to update transaction: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to update transaction: ", dbErrors)
}
}
dbBlock.IsChainBlock = false
dbResult = dbTx.Save(&dbBlock)
dbErrors = dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to update block: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to update block: ", dbErrors)
}
return nil
@ -707,33 +708,33 @@ func updateRemovedChainHashes(dbTx *gorm.DB, removedHash string) error {
// This function will return an error if any of the above are in an unexpected state
func updateAddedChainBlocks(dbTx *gorm.DB, addedBlock *btcjson.ChainBlock) error {
for _, acceptedBlock := range addedBlock.AcceptedBlocks {
var dbAccepedBlock models.Block
var dbAccepedBlock dbmodels.Block
dbResult := dbTx.
Where(&models.Block{BlockHash: acceptedBlock.Hash}).
Where(&dbmodels.Block{BlockHash: acceptedBlock.Hash}).
First(&dbAccepedBlock)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to find block: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to find block: ", dbErrors)
}
if utils.IsDBRecordNotFoundError(dbErrors) {
if httpserverutils.IsDBRecordNotFoundError(dbErrors) {
return fmt.Errorf("missing block for hash: %s", acceptedBlock.Hash)
}
if dbAccepedBlock.IsChainBlock {
return fmt.Errorf("block erroneously marked as a chain block: %s", acceptedBlock.Hash)
}
dbWhereTransactionIDsIn := make([]*models.Transaction, len(acceptedBlock.AcceptedTxIDs))
transactionIDsIn := make([]string, len(acceptedBlock.AcceptedTxIDs))
for i, acceptedTxID := range acceptedBlock.AcceptedTxIDs {
dbWhereTransactionIDsIn[i] = &models.Transaction{TransactionID: acceptedTxID}
transactionIDsIn[i] = acceptedTxID
}
var dbAcceptedTransactions []models.Transaction
var dbAcceptedTransactions []dbmodels.Transaction
dbResult = dbTx.
Where(dbWhereTransactionIDsIn).
Where("transaction_id in (?)", transactionIDsIn).
Preload("TransactionInputs.PreviousTransactionOutput").
First(&dbAcceptedTransactions)
dbErrors = dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to find transactions: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to find transactions: ", dbErrors)
}
if len(dbAcceptedTransactions) != len(acceptedBlock.AcceptedTxIDs) {
return fmt.Errorf("some transaction are missing for block: %s", acceptedBlock.Hash)
@ -750,24 +751,24 @@ func updateAddedChainBlocks(dbTx *gorm.DB, addedBlock *btcjson.ChainBlock) error
dbPreviousTransactionOutput.IsSpent = true
dbResult = dbTx.Save(&dbPreviousTransactionOutput)
dbErrors = dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to update transactionOutput: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to update transactionOutput: ", dbErrors)
}
}
dbAcceptedTransaction.AcceptingBlockID = &dbAccepedBlock.ID
dbResult = dbTx.Save(&dbAcceptedTransaction)
dbErrors = dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to update transaction: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to update transaction: ", dbErrors)
}
}
dbAccepedBlock.IsChainBlock = true
dbResult = dbTx.Save(&dbAccepedBlock)
dbErrors = dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return utils.NewErrorFromDBErrors("failed to update block: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewErrorFromDBErrors("failed to update block: ", dbErrors)
}
}
return nil
@ -776,6 +777,7 @@ func updateAddedChainBlocks(dbTx *gorm.DB, addedBlock *btcjson.ChainBlock) error
// handleBlockAddedMsg handles onBlockAdded messages
func handleBlockAddedMsg(client *jsonrpc.Client, blockAdded *jsonrpc.BlockAddedMsg) {
hash := blockAdded.Header.BlockHash()
log.Debugf("Got block %s from the RPC server", hash)
block, rawBlock, err := fetchBlock(client, hash)
if err != nil {
log.Warnf("Could not fetch block %s: %s", hash, err)
@ -810,19 +812,20 @@ func canHandleChainChangedMsg(chainChanged *jsonrpc.ChainChangedMsg) (bool, erro
}
// Make sure that all the hashes exist in the database
dbWhereBlockHashesIn := make([]*models.Block, len(hashes))
hashesIn := make([]string, len(hashes))
i := 0
for hash := range hashes {
dbWhereBlockHashesIn[i] = &models.Block{BlockHash: hash}
hashesIn[i] = hash
i++
}
var dbBlocksCount int
dbResult := dbTx.
Where(dbWhereBlockHashesIn).
Model(&dbmodels.Block{}).
Where("block_hash in (?)", hashesIn).
Count(&dbBlocksCount)
dbErrors := dbResult.GetErrors()
if utils.HasDBError(dbErrors) {
return false, utils.NewErrorFromDBErrors("failed to find block count: ", dbErrors)
if httpserverutils.HasDBError(dbErrors) {
return false, httpserverutils.NewErrorFromDBErrors("failed to find block count: ", dbErrors)
}
if len(hashes) != dbBlocksCount {
return false, nil
@ -842,7 +845,7 @@ func handleChainChangedMsg(chainChanged *jsonrpc.ChainChangedMsg) {
log.Warnf("Could not update selected parent chain: %s", err)
return
}
log.Infof("Chain changed: removed &d blocks and added %d block",
log.Infof("Chain changed: removed %d blocks and added %d block",
len(removedHashes), len(addedBlocks))
}

View File

@ -16,7 +16,7 @@ const (
// UnacceptedBlueScore is the blue score used for the "block" blueScore
// field of the contextual transaction information provided in a
// transaction store when it has not yet been accepted by a block.
UnacceptedBlueScore = math.MaxUint64
UnacceptedBlueScore uint64 = math.MaxUint64
)
// UTXOEntry houses details about an individual transaction output in a utxo

View File

@ -27,9 +27,18 @@ const (
baseSubsidy = 50 * util.SatoshiPerBitcoin
// the following are used when calculating a transaction's mass
massPerTxByte = 1
massPerScriptPubKeyByte = 10
massPerSigOp = 10000
// MassPerTxByte is the number of grams that any byte
// adds to a transaction.
MassPerTxByte = 1
// MassPerScriptPubKeyByte is the number of grams that any
// scriptPubKey byte adds to a transaction.
MassPerScriptPubKeyByte = 10
// MassPerSigOp is the number of grams that any
// signature operation adds to a transaction.
MassPerSigOp = 10000
)
// isNullOutpoint determines whether or not a previous transaction outpoint
@ -125,7 +134,7 @@ func CheckTransactionSanity(tx *util.Tx, subnetworkID *subnetworkid.SubnetworkID
// A transaction must not exceed the maximum allowed block mass when
// serialized.
serializedTxSize := msgTx.SerializeSize()
if serializedTxSize*massPerTxByte > wire.MaxMassPerTx {
if serializedTxSize*MassPerTxByte > wire.MaxMassPerTx {
str := fmt.Sprintf("serialized transaction is too big - got "+
"%d, max %d", serializedTxSize, wire.MaxMassPerBlock)
return ruleError(ErrTxMassTooHigh, str)
@ -361,7 +370,7 @@ func CalcTxMass(tx *util.Tx, previousScriptPubKeys [][]byte) uint64 {
txSize := tx.MsgTx().SerializeSize()
if tx.IsCoinBase() {
return uint64(txSize * massPerTxByte)
return uint64(txSize * MassPerTxByte)
}
scriptPubKeySize := 0
@ -378,9 +387,9 @@ func CalcTxMass(tx *util.Tx, previousScriptPubKeys [][]byte) uint64 {
sigOpsCount += txscript.GetPreciseSigOpCount(sigScript, previousScriptPubKeys[txInIndex], isP2SH)
}
return uint64(txSize*massPerTxByte +
scriptPubKeySize*massPerScriptPubKeyByte +
sigOpsCount*massPerSigOp)
return uint64(txSize*MassPerTxByte +
scriptPubKeySize*MassPerScriptPubKeyByte +
sigOpsCount*MassPerSigOp)
}
// checkBlockHeaderSanity performs some preliminary checks on a block header to

View File

@ -33,7 +33,7 @@ type config struct {
TargetNumberOfInputs uint64 `long:"num-inputs" description:"Target number of transaction inputs (with some randomization)"`
AveragePayloadSize uint64 `long:"payload-size" description:"Average size of transaction payload"`
AverageGasFraction float64 `long:"gas-fraction" description:"The average portion of gas from the gas limit"`
AverageFeeRate uint64 `long:"fee-rate" description:"Average coins per byte fee rate"`
AverageFeeRate float64 `long:"fee-rate" description:"Average coins per gram fee rate"`
}
func parseConfig() (*config, error) {

View File

@ -204,9 +204,13 @@ func updateWalletTxs(blockAdded *blockAddedMsg, walletTxs map[daghash.TxID]*wall
}
}
func randomWithAverageTarget(target uint64, allowZero bool) uint64 {
func randomWithAverageTarget(target float64) float64 {
randomFraction := random.Float64()
randomNum := randomFraction * float64(target*2)
return randomFraction * target * 2
}
func randomIntegerWithAverageTarget(target uint64, allowZero bool) uint64 {
randomNum := randomWithAverageTarget(float64(target))
if !allowZero && randomNum < 1 {
randomNum = 1
}
@ -237,17 +241,17 @@ func createRandomTxFromFunds(walletUTXOSet utxoSet, cfg *config, gasLimitMap map
}
if !chosenSubnetwork.IsEqual(subnetworkid.SubnetworkIDNative) {
payloadSize = randomWithAverageTarget(cfg.AveragePayloadSize, true)
gas = randomWithAverageTarget(uint64(float64(chosenGasLimit)*cfg.AverageGasFraction), true)
payloadSize = randomIntegerWithAverageTarget(cfg.AveragePayloadSize, true)
gas = randomIntegerWithAverageTarget(uint64(float64(chosenGasLimit)*cfg.AverageGasFraction), true)
if gas > chosenGasLimit {
gas = chosenGasLimit
}
}
targetNumberOfOutputs := randomWithAverageTarget(cfg.TargetNumberOfOutputs, false)
targetNumberOfInputs := randomWithAverageTarget(cfg.TargetNumberOfInputs, false)
targetNumberOfOutputs := randomIntegerWithAverageTarget(cfg.TargetNumberOfOutputs, false)
targetNumberOfInputs := randomIntegerWithAverageTarget(cfg.TargetNumberOfInputs, false)
feeRate := randomWithAverageTarget(cfg.AverageFeeRate, true)
feeRate := randomWithAverageTarget(cfg.AverageFeeRate)
amount := minSpendableAmount + uint64(random.Int63n(int64(maxSpendableAmount-minSpendableAmount)))
amount *= targetNumberOfOutputs
@ -280,7 +284,7 @@ func enqueueTransactions(client *txgenClient, blockAdded *blockAddedMsg, walletU
return nil
}
func createTx(walletUTXOSet utxoSet, minAmount uint64, feeRate uint64, targetNumberOfOutputs uint64, targetNumberOfInputs uint64,
func createTx(walletUTXOSet utxoSet, minAmount uint64, feeRate float64, targetNumberOfOutputs uint64, targetNumberOfInputs uint64,
subnetworkdID *subnetworkid.SubnetworkID, payloadSize uint64, gas uint64, scriptPubKey []byte) (*wire.MsgTx, error) {
var tx *wire.MsgTx
if subnetworkdID.IsEqual(subnetworkid.SubnetworkIDNative) {
@ -340,9 +344,10 @@ func signTx(walletUTXOSet utxoSet, tx *wire.MsgTx) error {
return nil
}
func fundTx(walletUTXOSet utxoSet, tx *wire.MsgTx, amount uint64, feeRate uint64, targetNumberOfOutputs uint64, targetNumberOfInputs uint64) (uint64, error) {
func fundTx(walletUTXOSet utxoSet, tx *wire.MsgTx, amount uint64, feeRate float64, targetNumberOfOutputs uint64, targetNumberOfInputs uint64) (uint64, error) {
amountSelected := uint64(0)
isTxFunded := false
for outpoint, output := range walletUTXOSet {
amountSelected += output.Value
@ -353,29 +358,30 @@ func fundTx(walletUTXOSet utxoSet, tx *wire.MsgTx, amount uint64, feeRate uint64
// Check if transaction has enough funds. If we don't have enough
// coins from he current amount selected to pay the fee, or we have
// less inputs then the targeted amount, continue to grab more coins.
if uint64(len(tx.TxIn)) >= targetNumberOfInputs && isFunded(tx, feeRate, targetNumberOfOutputs, amountSelected, amount, walletUTXOSet) {
isTxFunded = isFunded(tx, feeRate, targetNumberOfOutputs, amountSelected, amount, walletUTXOSet)
if uint64(len(tx.TxIn)) >= targetNumberOfInputs && isTxFunded {
break
}
}
if !isFunded(tx, feeRate, targetNumberOfOutputs, amountSelected, amount, walletUTXOSet) {
if !isTxFunded {
return 0, fmt.Errorf("not enough funds for coin selection")
}
return amountSelected, nil
}
// Check if the transaction has enough funds to cover the fee
// isFunded checks if the transaction has enough funds to cover the fee
// required for the txn.
func isFunded(tx *wire.MsgTx, feeRate uint64, targetNumberOfOutputs uint64, amountSelected uint64, targetAmount uint64, walletUTXOSet utxoSet) bool {
func isFunded(tx *wire.MsgTx, feeRate float64, targetNumberOfOutputs uint64, amountSelected uint64, targetAmount uint64, walletUTXOSet utxoSet) bool {
reqFee := calcFee(tx, feeRate, targetNumberOfOutputs, walletUTXOSet)
return amountSelected > reqFee && amountSelected-reqFee >= targetAmount
}
func calcFee(msgTx *wire.MsgTx, feeRate uint64, numberOfOutputs uint64, walletUTXOSet utxoSet) uint64 {
func calcFee(msgTx *wire.MsgTx, feeRate float64, numberOfOutputs uint64, walletUTXOSet utxoSet) uint64 {
txMass := calcTxMass(msgTx, walletUTXOSet)
txMassWithOutputs := txMass + outputsTotalSize(numberOfOutputs)
reqFee := txMassWithOutputs * feeRate
reqFee := uint64(float64(txMassWithOutputs) * feeRate)
if reqFee < minTxFee {
return minTxFee
}

View File

@ -281,7 +281,7 @@ func LoadAndSetMainConfig() error {
return nil
}
//MainConfig is a getter to the main config
// MainConfig is a getter to the main config
func MainConfig() *Config {
return mainCfg
}

112
faucet/config/config.go Normal file
View File

@ -0,0 +1,112 @@
package config
import (
"github.com/daglabs/btcd/apiserver/logger"
"github.com/daglabs/btcd/dagconfig"
"github.com/daglabs/btcd/util"
"github.com/jessevdk/go-flags"
"github.com/pkg/errors"
"path/filepath"
)
const (
defaultLogFilename = "faucet.log"
defaultErrLogFilename = "faucet_err.log"
)
var (
// Default configuration options
defaultLogDir = util.AppDataDir("faucet", false)
defaultDBAddress = "localhost:3306"
defaultHTTPListen = "0.0.0.0:8081"
// activeNetParams are the currently active net params
activeNetParams *dagconfig.Params
)
// Config defines the configuration options for the API server.
type Config struct {
LogDir string `long:"logdir" description:"Directory to log output."`
HTTPListen string `long:"listen" description:"HTTP address to listen on (default: 0.0.0.0:8081)"`
APIServerURL string `long:"api-server-url" description:"The API server url to connect to" required:"true"`
PrivateKey string `long:"private-key" description:"Faucet Private key" required:"true"`
DBAddress string `long:"dbaddress" description:"Database address"`
DBUser string `long:"dbuser" description:"Database user" required:"true"`
DBPassword string `long:"dbpass" description:"Database password" required:"true"`
DBName string `long:"dbname" description:"Database name" required:"true"`
Migrate bool `long:"migrate" description:"Migrate the database to the latest version. The server will not start when using this flag."`
FeeRate float64 `long:"fee-rate" description:"Coins per gram fee rate"`
TestNet bool `long:"testnet" description:"Connect to testnet"`
SimNet bool `long:"simnet" description:"Connect to the simulation test network"`
DevNet bool `long:"devnet" description:"Connect to the development test network"`
}
var cfg *Config
// Parse parses the CLI arguments and returns a config struct.
func Parse() error {
cfg = &Config{
LogDir: defaultLogDir,
DBAddress: defaultDBAddress,
HTTPListen: defaultHTTPListen,
}
parser := flags.NewParser(cfg, flags.PrintErrors|flags.HelpFlag)
_, err := parser.Parse()
if err != nil {
return err
}
err = resolveNetwork(cfg)
if err != nil {
return err
}
logFile := filepath.Join(cfg.LogDir, defaultLogFilename)
errLogFile := filepath.Join(cfg.LogDir, defaultErrLogFilename)
logger.InitLog(logFile, errLogFile)
return nil
}
func resolveNetwork(cfg *Config) error {
// Multiple networks can't be selected simultaneously.
numNets := 0
if cfg.TestNet {
numNets++
}
if cfg.SimNet {
numNets++
}
if cfg.DevNet {
numNets++
}
if numNets > 1 {
return errors.New("multiple net params (testnet, simnet, devnet, etc.) can't be used " +
"together -- choose one of them")
}
activeNetParams = &dagconfig.MainNetParams
switch {
case cfg.TestNet:
activeNetParams = &dagconfig.TestNetParams
case cfg.SimNet:
activeNetParams = &dagconfig.SimNetParams
case cfg.DevNet:
activeNetParams = &dagconfig.DevNetParams
}
return nil
}
// MainConfig is a getter to the main config
func MainConfig() (*Config, error) {
if cfg == nil {
return nil, errors.New("No configuration was set for the faucet")
}
return cfg, nil
}
// ActiveNetParams returns the currently active net params
func ActiveNetParams() *dagconfig.Params {
return activeNetParams
}

150
faucet/database/database.go Normal file
View File

@ -0,0 +1,150 @@
package database
import (
"errors"
"fmt"
"os"
"github.com/daglabs/btcd/faucet/config"
"github.com/golang-migrate/migrate/v4/source"
"github.com/jinzhu/gorm"
"github.com/golang-migrate/migrate/v4"
)
// db is the API server database.
var db *gorm.DB
// DB returns a reference to the database connection
func DB() (*gorm.DB, error) {
if db == nil {
return nil, errors.New("Database is not connected")
}
return db, nil
}
type gormLogger struct{}
func (l gormLogger) Print(v ...interface{}) {
str := fmt.Sprint(v...)
log.Errorf(str)
}
// Connect connects to the database mentioned in
// config variable.
func Connect() error {
connectionString, err := buildConnectionString()
if err != nil {
return err
}
migrator, driver, err := openMigrator(connectionString)
if err != nil {
return err
}
isCurrent, version, err := isCurrent(migrator, driver)
if err != nil {
return fmt.Errorf("Error checking whether the database is current: %s", err)
}
if !isCurrent {
return fmt.Errorf("Database is not current (version %d). Please migrate"+
" the database by running the faucet with --migrate flag and then run it again.", version)
}
db, err = gorm.Open("mysql", connectionString)
if err != nil {
return err
}
db.SetLogger(gormLogger{})
return nil
}
// Close closes the connection to the database
func Close() error {
if db == nil {
return nil
}
err := db.Close()
db = nil
return err
}
func buildConnectionString() (string, error) {
cfg, err := config.MainConfig()
if err != nil {
return "", err
}
return fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True",
cfg.DBUser, cfg.DBPassword, cfg.DBAddress, cfg.DBName), nil
}
// isCurrent resolves whether the database is on the latest
// version of the schema.
func isCurrent(migrator *migrate.Migrate, driver source.Driver) (bool, uint, error) {
// Get the current version
version, isDirty, err := migrator.Version()
if err == migrate.ErrNilVersion {
return false, 0, nil
}
if err != nil {
return false, 0, err
}
if isDirty {
return false, 0, fmt.Errorf("Database is dirty")
}
// The database is current if Next returns ErrNotExist
_, err = driver.Next(version)
if pathErr, ok := err.(*os.PathError); ok {
if pathErr.Err == os.ErrNotExist {
return true, version, nil
}
}
return false, version, err
}
func openMigrator(connectionString string) (*migrate.Migrate, source.Driver, error) {
driver, err := source.Open("file://migrations")
if err != nil {
return nil, nil, err
}
migrator, err := migrate.NewWithSourceInstance(
"migrations", driver, "mysql://"+connectionString)
if err != nil {
return nil, nil, err
}
return migrator, driver, nil
}
// Migrate database to the latest version.
func Migrate() error {
connectionString, err := buildConnectionString()
if err != nil {
return err
}
migrator, driver, err := openMigrator(connectionString)
if err != nil {
return err
}
isCurrent, version, err := isCurrent(migrator, driver)
if err != nil {
return fmt.Errorf("Error checking whether the database is current: %s", err)
}
if isCurrent {
log.Infof("Database is already up-to-date (version %d)", version)
return nil
}
err = migrator.Up()
if err != nil {
return err
}
version, isDirty, err := migrator.Version()
if err != nil {
return err
}
if isDirty {
return fmt.Errorf("error migrating database: database is dirty")
}
log.Infof("Migrated database to the latest version (version %d)", version)
return nil
}

9
faucet/database/log.go Normal file
View File

@ -0,0 +1,9 @@
package database
import "github.com/daglabs/btcd/util/panics"
import "github.com/daglabs/btcd/apiserver/logger"
var (
log = logger.BackendLog.Logger("DTBS")
spawn = panics.GoroutineWrapperFunc(log, logger.BackendLog)
)

331
faucet/faucet.go Normal file
View File

@ -0,0 +1,331 @@
package main
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/daglabs/btcd/apiserver/apimodels"
"github.com/daglabs/btcd/blockdag"
"github.com/daglabs/btcd/faucet/config"
"github.com/daglabs/btcd/httpserverutils"
"github.com/daglabs/btcd/txscript"
"github.com/daglabs/btcd/util"
"github.com/daglabs/btcd/util/daghash"
"github.com/daglabs/btcd/wire"
"github.com/pkg/errors"
"io/ioutil"
"net/http"
"net/url"
"path"
)
const (
sendAmount = 10000
// Value 8 bytes + serialized varint size for the length of ScriptPubKey +
// ScriptPubKey bytes.
outputSize uint64 = 8 + 1 + 25
minTxFee uint64 = 3000
requiredConfirmations = 10
)
type utxoSet map[wire.Outpoint]*blockdag.UTXOEntry
// apiURL returns a full concatenated URL from the base
// API server URL and the given path.
func apiURL(requestPath string) (string, error) {
cfg, err := config.MainConfig()
if err != nil {
return "", err
}
u, err := url.Parse(cfg.APIServerURL)
if err != nil {
return "", errors.WithStack(err)
}
u.Path = path.Join(u.Path, requestPath)
return u.String(), nil
}
// getFromAPIServer makes an HTTP GET request to the API server
// to the given request path, and returns the response body.
func getFromAPIServer(requestPath string) ([]byte, error) {
getAPIURL, err := apiURL(requestPath)
if err != nil {
return nil, err
}
resp, err := http.Get(getAPIURL)
if err != nil {
return nil, errors.WithStack(err)
}
defer func() {
err := resp.Body.Close()
if err != nil {
panic(errors.WithStack(err))
}
}()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.WithStack(err)
}
if resp.StatusCode != http.StatusOK {
clientError := &httpserverutils.ClientError{}
err := json.Unmarshal(body, &clientError)
if err != nil {
return nil, errors.WithStack(err)
}
return nil, errors.WithStack(clientError)
}
return body, nil
}
// getFromAPIServer makes an HTTP POST request to the API server
// to the given request path. It converts the given data to JSON,
// and post it as the POST data.
func postToAPIServer(requestPath string, data interface{}) error {
dataBytes, err := json.Marshal(data)
if err != nil {
return errors.WithStack(err)
}
r := bytes.NewReader(dataBytes)
postAPIURL, err := apiURL(requestPath)
if err != nil {
return err
}
resp, err := http.Post(postAPIURL, "application/json", r)
if err != nil {
return errors.WithStack(err)
}
defer func() {
err := resp.Body.Close()
if err != nil {
panic(errors.WithStack(err))
}
}()
if resp.StatusCode != http.StatusOK {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return errors.WithStack(err)
}
clientError := &httpserverutils.ClientError{}
err = json.Unmarshal(body, &clientError)
if err != nil {
return errors.WithStack(err)
}
return errors.WithStack(clientError)
}
return nil
}
func isUTXOMatured(entry *blockdag.UTXOEntry, confirmations uint64) bool {
if entry.IsCoinbase() {
return confirmations >= config.ActiveNetParams().BlockCoinbaseMaturity
}
return confirmations >= requiredConfirmations
}
func getWalletUTXOSet() (utxoSet, error) {
body, err := getFromAPIServer(fmt.Sprintf("utxos/address/%s", faucetAddress.EncodeAddress()))
if err != nil {
return nil, err
}
utxoResponses := []*apimodels.TransactionOutputResponse{}
err = json.Unmarshal(body, &utxoResponses)
if err != nil {
return nil, err
}
walletUTXOSet := make(utxoSet)
for _, utxoResponse := range utxoResponses {
scriptPubKey, err := hex.DecodeString(utxoResponse.ScriptPubKey)
if err != nil {
return nil, err
}
txOut := &wire.TxOut{
Value: utxoResponse.Value,
ScriptPubKey: scriptPubKey,
}
txID, err := daghash.NewTxIDFromStr(utxoResponse.TransactionID)
if err != nil {
return nil, err
}
outpoint := wire.NewOutpoint(txID, utxoResponse.Index)
utxoEntry := blockdag.NewUTXOEntry(txOut, *utxoResponse.IsCoinbase, utxoResponse.AcceptingBlockBlueScore)
if !isUTXOMatured(utxoEntry, *utxoResponse.Confirmations) {
continue
}
walletUTXOSet[*outpoint] = utxoEntry
}
return walletUTXOSet, nil
}
func sendToAddress(address util.Address) (*wire.MsgTx, error) {
tx, err := createTx(address)
if err != nil {
return nil, err
}
buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize()))
if err := tx.Serialize(buf); err != nil {
return nil, err
}
rawTx := &apimodels.RawTransaction{RawTransaction: hex.EncodeToString(buf.Bytes())}
return tx, postToAPIServer("transaction", rawTx)
}
func createTx(address util.Address) (*wire.MsgTx, error) {
walletUTXOSet, err := getWalletUTXOSet()
if err != nil {
return nil, err
}
tx, err := createUnsignedTx(walletUTXOSet, address)
if err != nil {
return nil, err
}
err = signTx(walletUTXOSet, tx)
if err != nil {
return nil, err
}
return tx, nil
}
func createUnsignedTx(walletUTXOSet utxoSet, address util.Address) (*wire.MsgTx, error) {
tx := wire.NewNativeMsgTx(wire.TxVersion, nil, nil)
netAmount, isChangeOutputRequired, err := fundTx(walletUTXOSet, tx, sendAmount)
if err != nil {
return nil, err
}
if isChangeOutputRequired {
tx.AddTxOut(&wire.TxOut{
Value: sendAmount,
ScriptPubKey: address.ScriptAddress(),
})
tx.AddTxOut(&wire.TxOut{
Value: netAmount - sendAmount,
ScriptPubKey: faucetScriptPubKey,
})
return tx, nil
}
tx.AddTxOut(&wire.TxOut{
Value: netAmount,
ScriptPubKey: address.ScriptAddress(),
})
return tx, nil
}
// signTx signs a transaction
func signTx(walletUTXOSet utxoSet, tx *wire.MsgTx) error {
for i, txIn := range tx.TxIn {
outpoint := txIn.PreviousOutpoint
sigScript, err := txscript.SignatureScript(tx, i, walletUTXOSet[outpoint].ScriptPubKey(),
txscript.SigHashAll, faucetPrivateKey, true)
if err != nil {
return fmt.Errorf("Failed to sign transaction: %s", err)
}
txIn.SignatureScript = sigScript
}
return nil
}
func fundTx(walletUTXOSet utxoSet, tx *wire.MsgTx, amount uint64) (netAmount uint64, isChangeOutputRequired bool, err error) {
amountSelected := uint64(0)
isTxFunded := false
for outpoint, entry := range walletUTXOSet {
amountSelected += entry.Amount()
// Add the selected output to the transaction
tx.AddTxIn(wire.NewTxIn(&outpoint, nil))
// Check if transaction has enough funds. If we don't have enough
// coins from the current amount selected to pay the fee continue
// to grab more coins.
isTxFunded, isChangeOutputRequired, netAmount, err = isFundedAndIsChangeOutputRequired(tx, amountSelected, amount, walletUTXOSet)
if err != nil {
return 0, false, err
}
if isTxFunded {
break
}
}
if !isTxFunded {
return 0, false, errors.Errorf("not enough funds for coin selection")
}
return netAmount, isChangeOutputRequired, nil
}
// isFundedAndIsChangeOutputRequired returns three values and an error:
// * isTxFunded is whether the transaction inputs cover the target amount + the required fee.
// * isChangeOutputRequired is whether it is profitable to add an additional change
// output to the transaction.
// * netAmount is the amount of coins that will be eventually sent to the recipient. If no
// change output is needed, the netAmount will be usually a little bit higher than the
// targetAmount. Otherwise, it'll be the same as the targetAmount.
func isFundedAndIsChangeOutputRequired(tx *wire.MsgTx, amountSelected uint64, targetAmount uint64, walletUTXOSet utxoSet) (isTxFunded, isChangeOutputRequired bool, netAmount uint64, err error) {
// First check if it can be funded with one output and the required fee for it.
isFundedWithOneOutput, oneOutputFee, err := isFundedWithNumberOfOutputs(tx, 1, amountSelected, targetAmount, walletUTXOSet)
if err != nil {
return false, false, 0, err
}
if !isFundedWithOneOutput {
return false, false, 0, nil
}
// Now check if it can be funded with two outputs and the required fee for it.
isFundedWithTwoOutputs, twoOutputsFee, err := isFundedWithNumberOfOutputs(tx, 2, amountSelected, targetAmount, walletUTXOSet)
if err != nil {
return false, false, 0, err
}
// If it can be funded with two outputs, check if adding a change output worth it: i.e. check if
// the amount you save by not sending the recipient the whole inputs amount (minus fees) is greater
// than the additional fee that is required by adding a change output. If this is the case, return
// isChangeOutputRequired as true.
if isFundedWithTwoOutputs && twoOutputsFee-oneOutputFee < targetAmount-amountSelected {
return true, true, amountSelected - twoOutputsFee, nil
}
return true, false, amountSelected - oneOutputFee, nil
}
// isFundedWithNumberOfOutputs returns whether the transaction inputs cover
// the target amount + the required fee with the assumed number of outputs.
func isFundedWithNumberOfOutputs(tx *wire.MsgTx, numberOfOutputs uint64, amountSelected uint64, targetAmount uint64, walletUTXOSet utxoSet) (isTxFunded bool, fee uint64, err error) {
reqFee, err := calcFee(tx, numberOfOutputs, walletUTXOSet)
if err != nil {
return false, 0, err
}
return amountSelected > reqFee && amountSelected-reqFee >= targetAmount, reqFee, nil
}
func calcFee(msgTx *wire.MsgTx, numberOfOutputs uint64, walletUTXOSet utxoSet) (uint64, error) {
txMass := calcTxMass(msgTx, walletUTXOSet)
txMassWithOutputs := txMass + outputsTotalSize(numberOfOutputs)*blockdag.MassPerTxByte
cfg, err := config.MainConfig()
if err != nil {
return 0, err
}
reqFee := uint64(float64(txMassWithOutputs) * cfg.FeeRate)
if reqFee < minTxFee {
return minTxFee, nil
}
return reqFee, nil
}
func outputsTotalSize(numberOfOutputs uint64) uint64 {
return numberOfOutputs*outputSize + uint64(wire.VarIntSerializeSize(numberOfOutputs))
}
func calcTxMass(msgTx *wire.MsgTx, walletUTXOSet utxoSet) uint64 {
previousScriptPubKeys := getPreviousScriptPubKeys(msgTx, walletUTXOSet)
return blockdag.CalcTxMass(util.NewTx(msgTx), previousScriptPubKeys)
}
func getPreviousScriptPubKeys(msgTx *wire.MsgTx, walletUTXOSet utxoSet) [][]byte {
previousScriptPubKeys := make([][]byte, len(msgTx.TxIn))
for i, txIn := range msgTx.TxIn {
outpoint := txIn.PreviousOutpoint
previousScriptPubKeys[i] = walletUTXOSet[outpoint].ScriptPubKey()
}
return previousScriptPubKeys
}

65
faucet/ip_usage.go Normal file
View File

@ -0,0 +1,65 @@
package main
import (
"github.com/daglabs/btcd/faucet/database"
"github.com/daglabs/btcd/httpserverutils"
"net"
"net/http"
"time"
)
const minRequestInterval = time.Hour * 24
type ipUse struct {
IP string
LastUse time.Time
}
func ipFromRequest(r *http.Request) (string, error) {
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return "", err
}
return ip, nil
}
func validateIPUsage(r *http.Request) *httpserverutils.HandlerError {
db, err := database.DB()
if err != nil {
return httpserverutils.NewInternalServerHandlerError(err.Error())
}
now := time.Now()
timeBeforeMinRequestInterval := now.Add(-minRequestInterval)
var count int
ip, err := ipFromRequest(r)
if err != nil {
return httpserverutils.NewInternalServerHandlerError(err.Error())
}
dbResult := db.Model(&ipUse{}).Where(&ipUse{IP: ip}).Where("last_use BETWEEN ? AND ?", timeBeforeMinRequestInterval, now).Count(&count)
dbErrors := dbResult.GetErrors()
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewHandlerErrorFromDBErrors("Some errors were encountered when checking the last use of an IP:", dbResult.GetErrors())
}
if count != 0 {
return httpserverutils.NewHandlerError(http.StatusForbidden, "A user is allowed to to have one request from the faucet every 24 hours.")
}
return nil
}
func updateIPUsage(r *http.Request) *httpserverutils.HandlerError {
db, err := database.DB()
if err != nil {
return httpserverutils.NewInternalServerHandlerError(err.Error())
}
ip, err := ipFromRequest(r)
if err != nil {
return httpserverutils.NewInternalServerHandlerError(err.Error())
}
dbResult := db.Where(&ipUse{IP: ip}).Assign(&ipUse{LastUse: time.Now()}).FirstOrCreate(&ipUse{})
dbErrors := dbResult.GetErrors()
if httpserverutils.HasDBError(dbErrors) {
return httpserverutils.NewHandlerErrorFromDBErrors("Some errors were encountered when upserting the IP to the new date:", dbResult.GetErrors())
}
return nil
}

11
faucet/log.go Normal file
View File

@ -0,0 +1,11 @@
package main
import (
"github.com/daglabs/btcd/logger"
"github.com/daglabs/btcd/util/panics"
)
var (
log = logger.BackendLog.Logger("FAUC")
spawn = panics.GoroutineWrapperFunc(log, logger.BackendLog)
)

89
faucet/main.go Normal file
View File

@ -0,0 +1,89 @@
package main
import (
"fmt"
"github.com/daglabs/btcd/btcec"
"github.com/daglabs/btcd/dagconfig"
"github.com/daglabs/btcd/faucet/config"
"github.com/daglabs/btcd/faucet/database"
"github.com/daglabs/btcd/txscript"
"github.com/daglabs/btcd/util"
"github.com/daglabs/btcd/util/base58"
"github.com/pkg/errors"
"os"
"github.com/daglabs/btcd/logger"
"github.com/daglabs/btcd/signal"
"github.com/daglabs/btcd/util/panics"
_ "github.com/golang-migrate/migrate/v4/database/mysql"
_ "github.com/golang-migrate/migrate/v4/source/file"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
var (
faucetAddress util.Address
faucetPrivateKey *btcec.PrivateKey
faucetScriptPubKey []byte
)
func main() {
defer panics.HandlePanic(log, logger.BackendLog)
err := config.Parse()
if err != nil {
err := errors.Wrap(err, "Error parsing command-line arguments")
_, err = fmt.Fprintf(os.Stderr, err.Error())
if err != nil {
panic(err)
}
return
}
cfg, err := config.MainConfig()
if err != nil {
panic(err)
}
if cfg.Migrate {
err := database.Migrate()
if err != nil {
panic(fmt.Errorf("Error migrating database: %s", err))
}
return
}
err = database.Connect()
if err != nil {
panic(fmt.Errorf("Error connecting to database: %s", err))
}
defer func() {
err := database.Close()
if err != nil {
panic(fmt.Errorf("Error closing the database: %s", err))
}
}()
privateKeyBytes := base58.Decode(cfg.PrivateKey)
faucetPrivateKey, _ = btcec.PrivKeyFromBytes(btcec.S256(), privateKeyBytes)
faucetAddress, err = privateKeyToP2PKHAddress(faucetPrivateKey, config.ActiveNetParams())
if err != nil {
panic(fmt.Errorf("Failed to get P2PKH address from private key: %s", err))
}
faucetScriptPubKey, err = txscript.PayToAddrScript(faucetAddress)
if err != nil {
panic(fmt.Errorf("failed to generate faucetScriptPubKey to address: %s", err))
}
shutdownServer := startHTTPServer(cfg.HTTPListen)
defer shutdownServer()
interrupt := signal.InterruptListener()
<-interrupt
}
// privateKeyToP2PKHAddress generates p2pkh address from private key.
func privateKeyToP2PKHAddress(key *btcec.PrivateKey, net *dagconfig.Params) (util.Address, error) {
return util.NewAddressPubKeyHashFromPublicKey(key.PubKey().SerializeCompressed(), net.Prefix)
}

View File

@ -0,0 +1 @@
DROP TABLE `ip_uses`;

View File

@ -0,0 +1,6 @@
CREATE TABLE `ip_uses`
(
`ip` VARCHAR(39) NOT NULL,
`last_use` DATETIME NOT NULL,
PRIMARY KEY (`ip`)
);

81
faucet/server.go Normal file
View File

@ -0,0 +1,81 @@
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/daglabs/btcd/faucet/config"
"github.com/daglabs/btcd/httpserverutils"
"github.com/daglabs/btcd/util"
"net/http"
"time"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
)
const gracefulShutdownTimeout = 30 * time.Second
// startHTTPServer starts the HTTP REST server and returns a
// function to gracefully shutdown it.
func startHTTPServer(listenAddr string) func() {
router := mux.NewRouter()
router.Use(httpserverutils.AddRequestMetadataMiddleware)
router.Use(httpserverutils.RecoveryMiddleware)
router.Use(httpserverutils.LoggingMiddleware)
router.Use(httpserverutils.SetJSONMiddleware)
router.HandleFunc(
"/request_money",
httpserverutils.MakeHandler(requestMoneyHandler)).
Methods("POST")
httpServer := &http.Server{
Addr: listenAddr,
Handler: handlers.CORS()(router),
}
spawn(func() {
log.Errorf("%s", httpServer.ListenAndServe())
})
return func() {
ctx, cancel := context.WithTimeout(context.Background(), gracefulShutdownTimeout)
defer cancel()
err := httpServer.Shutdown(ctx)
if err != nil {
log.Errorf("Error shutting down HTTP server: %s", err)
}
}
}
type requestMoneyData struct {
Address string `json:"address"`
}
func requestMoneyHandler(_ *httpserverutils.ServerContext, r *http.Request, _ map[string]string, _ map[string]string,
requestBody []byte) (interface{}, *httpserverutils.HandlerError) {
hErr := validateIPUsage(r)
if hErr != nil {
return nil, hErr
}
requestData := &requestMoneyData{}
err := json.Unmarshal(requestBody, requestData)
if err != nil {
return nil, httpserverutils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity,
fmt.Sprintf("Error unmarshalling request body: %s", err),
"The request body is not json-formatted")
}
address, err := util.DecodeAddress(requestData.Address, config.ActiveNetParams().Prefix)
if err != nil {
return nil, httpserverutils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity,
fmt.Sprintf("Error decoding address: %s", err),
"Error decoding address")
}
tx, err := sendToAddress(address)
if err != nil {
return nil, httpserverutils.NewInternalServerHandlerError(err.Error())
}
hErr = updateIPUsage(r)
if hErr != nil {
return nil, hErr
}
return tx.TxID().String(), nil
}

2
go.mod
View File

@ -5,6 +5,7 @@ go 1.12
require (
bou.ke/monkey v1.0.1
github.com/aead/siphash v1.0.1
github.com/aws/aws-lambda-go v1.13.2
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd
github.com/btcsuite/goleveldb v1.0.0
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792
@ -18,5 +19,6 @@ require (
github.com/jrick/logrotate v1.0.0
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec
github.com/miekg/dns v1.1.6
github.com/pkg/errors v0.8.1
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734
)

8
go.sum
View File

@ -17,6 +17,8 @@ github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBA
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/aws/aws-lambda-go v1.13.2 h1:8lYuRVn6rESoUNZXdbCmtGB4bBk4vcVYojiHjE4mMrM=
github.com/aws/aws-lambda-go v1.13.2/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
@ -36,6 +38,7 @@ github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMe
github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk=
github.com/containerd/containerd v1.2.7 h1:8lqLbl7u1j3MmiL9cJ/O275crSq7bfwUayvvatEupQk=
github.com/containerd/containerd v1.2.7/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg=
github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc=
@ -185,8 +188,10 @@ github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
@ -195,7 +200,10 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=

View File

@ -1,4 +1,4 @@
package utils
package httpserverutils
import (
"context"
@ -11,69 +11,69 @@ const (
contextKeyRequestID contextKey = "REQUEST_ID"
)
// APIServerContext is a context.Context wrapper that
// ServerContext is a context.Context wrapper that
// enables custom logs with request ID.
type APIServerContext struct {
type ServerContext struct {
context.Context
}
// ToAPIServerContext takes a context.Context instance
// and converts it to *ApiServerContext.
func ToAPIServerContext(ctx context.Context) *APIServerContext {
if asCtx, ok := ctx.(*APIServerContext); ok {
// ToServerContext takes a context.Context instance
// and converts it to *ServerContext.
func ToServerContext(ctx context.Context) *ServerContext {
if asCtx, ok := ctx.(*ServerContext); ok {
return asCtx
}
return &APIServerContext{Context: ctx}
return &ServerContext{Context: ctx}
}
// SetRequestID associates a request ID for the context.
func (ctx *APIServerContext) SetRequestID(requestID uint64) context.Context {
func (ctx *ServerContext) SetRequestID(requestID uint64) context.Context {
context.WithValue(ctx, contextKeyRequestID, requestID)
return ctx
}
func (ctx *APIServerContext) requestID() uint64 {
func (ctx *ServerContext) requestID() uint64 {
id := ctx.Value(contextKeyRequestID)
uint64ID, _ := id.(uint64)
return uint64ID
}
func (ctx *APIServerContext) getLogString(format string, params ...interface{}) string {
func (ctx *ServerContext) getLogString(format string, params ...interface{}) string {
return fmt.Sprintf("RID %d: ", ctx.requestID()) + fmt.Sprintf(format, params...)
}
// Tracef writes a customized formatted context
// related log with log level 'Trace'.
func (ctx *APIServerContext) Tracef(format string, params ...interface{}) {
func (ctx *ServerContext) Tracef(format string, params ...interface{}) {
log.Trace(ctx.getLogString(format, params...))
}
// Debugf writes a customized formatted context
// related log with log level 'Debug'.
func (ctx *APIServerContext) Debugf(format string, params ...interface{}) {
func (ctx *ServerContext) Debugf(format string, params ...interface{}) {
log.Debug(ctx.getLogString(format, params...))
}
// Infof writes a customized formatted context
// related log with log level 'Info'.
func (ctx *APIServerContext) Infof(format string, params ...interface{}) {
func (ctx *ServerContext) Infof(format string, params ...interface{}) {
log.Info(ctx.getLogString(format, params...))
}
// Warnf writes a customized formatted context
// related log with log level 'Warn'.
func (ctx *APIServerContext) Warnf(format string, params ...interface{}) {
func (ctx *ServerContext) Warnf(format string, params ...interface{}) {
log.Warn(ctx.getLogString(format, params...))
}
// Errorf writes a customized formatted context
// related log with log level 'Error'.
func (ctx *APIServerContext) Errorf(format string, params ...interface{}) {
func (ctx *ServerContext) Errorf(format string, params ...interface{}) {
log.Error(ctx.getLogString(format, params...))
}
// Criticalf writes a customized formatted context
// related log with log level 'Critical'.
func (ctx *APIServerContext) Criticalf(format string, params ...interface{}) {
func (ctx *ServerContext) Criticalf(format string, params ...interface{}) {
log.Criticalf(ctx.getLogString(format, params...))
}

View File

@ -1,6 +1,7 @@
package utils
package httpserverutils
import (
"encoding/json"
"fmt"
"github.com/jinzhu/gorm"
"net/http"
@ -72,3 +73,39 @@ func IsDBRecordNotFoundError(dbErrors []error) bool {
func HasDBError(dbErrors []error) bool {
return !IsDBRecordNotFoundError(dbErrors) && len(dbErrors) > 0
}
// ClientError is the http response that is sent to the
// client in case of an error.
type ClientError struct {
ErrorCode int `json:"errorCode"`
ErrorMessage string `json:"errorMessage"`
}
func (err *ClientError) Error() string {
return fmt.Sprintf("%s (Code: %d)", err.ErrorMessage, err.ErrorCode)
}
// SendErr takes a HandlerError and create a ClientError out of it that is sent
// to the http client.
func SendErr(ctx *ServerContext, w http.ResponseWriter, hErr *HandlerError) {
errMsg := fmt.Sprintf("got error: %s", hErr)
ctx.Warnf(errMsg)
w.WriteHeader(hErr.Code)
SendJSONResponse(w, &ClientError{
ErrorCode: hErr.Code,
ErrorMessage: hErr.ClientMessage,
})
}
// SendJSONResponse encodes the given response to JSON format and
// sends it to the client
func SendJSONResponse(w http.ResponseWriter, response interface{}) {
b, err := json.Marshal(response)
if err != nil {
panic(err)
}
_, err = fmt.Fprintf(w, string(b))
if err != nil {
panic(err)
}
}

View File

@ -1,4 +1,4 @@
package utils
package httpserverutils
import "github.com/daglabs/btcd/util/panics"
import "github.com/daglabs/btcd/apiserver/logger"

View File

@ -1,48 +1,56 @@
package server
package httpserverutils
import (
"fmt"
"github.com/daglabs/btcd/apiserver/utils"
"net/http"
"runtime/debug"
)
var nextRequestID uint64 = 1
func addRequestMetadataMiddleware(next http.Handler) http.Handler {
// AddRequestMetadataMiddleware is a middleware that adds some
// metadata to the context of every request.
func AddRequestMetadataMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rCtx := utils.ToAPIServerContext(r.Context()).SetRequestID(nextRequestID)
rCtx := ToServerContext(r.Context()).SetRequestID(nextRequestID)
r.WithContext(rCtx)
nextRequestID++
next.ServeHTTP(w, r)
})
}
func loggingMiddleware(next http.Handler) http.Handler {
// LoggingMiddleware is a middleware that writes
// logs for every request.
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := utils.ToAPIServerContext(r.Context())
ctx := ToServerContext(r.Context())
ctx.Infof("Method: %s URI: %s", r.Method, r.RequestURI)
next.ServeHTTP(w, r)
})
}
func recoveryMiddleware(h http.Handler) http.Handler {
// RecoveryMiddleware is a middleware that recovers
// from panics, log it, and sends Internal Server
// Error to the client.
func RecoveryMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := utils.ToAPIServerContext(r.Context())
ctx := ToServerContext(r.Context())
defer func() {
recoveryErr := recover()
if recoveryErr != nil {
recoveryErrStr := fmt.Sprintf("%s", recoveryErr)
log.Criticalf("Fatal error: %s", recoveryErrStr)
log.Criticalf("Stack trace: %s", debug.Stack())
sendErr(ctx, w, utils.NewInternalServerHandlerError(recoveryErrStr))
SendErr(ctx, w, NewInternalServerHandlerError(recoveryErrStr))
}
}()
h.ServeHTTP(w, r)
})
}
func setJSONMiddleware(h http.Handler) http.Handler {
// SetJSONMiddleware is a middleware that sets the content type of
// every request to be application/json.
func SetJSONMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
h.ServeHTTP(w, r)

View File

@ -0,0 +1,58 @@
package httpserverutils
import (
"fmt"
"github.com/gorilla/mux"
"io/ioutil"
"net/http"
)
// HandlerFunc is a handler function that is passed to the
// MakeHandler wrapper and gets the relevant request fields
// from it.
type HandlerFunc func(ctx *ServerContext, r *http.Request, routeParams map[string]string, queryParams map[string]string, requestBody []byte) (
interface{}, *HandlerError)
// MakeHandler is a wrapper function that takes a handler in the form of HandlerFunc
// and returns a function that can be used as a handler in mux.Router.HandleFunc.
func MakeHandler(handler HandlerFunc) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
ctx := ToServerContext(r.Context())
var requestBody []byte
if r.Method == "POST" {
var err error
requestBody, err = ioutil.ReadAll(r.Body)
if err != nil {
SendErr(ctx, w, NewInternalServerHandlerError("Error reading POST data"))
}
}
flattenedQueryParams, hErr := flattenQueryParams(r.URL.Query())
if hErr != nil {
SendErr(ctx, w, hErr)
return
}
response, hErr := handler(ctx, r, mux.Vars(r), flattenedQueryParams, requestBody)
if hErr != nil {
SendErr(ctx, w, hErr)
return
}
if response != nil {
SendJSONResponse(w, response)
}
}
}
func flattenQueryParams(queryParams map[string][]string) (map[string]string, *HandlerError) {
flattenedMap := make(map[string]string)
for param, valuesSlice := range queryParams {
if len(valuesSlice) > 1 {
return nil, NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("Couldn't parse the '%s' query parameter:"+
" expected a single value but got multiple values", param))
}
flattenedMap[param] = valuesSlice[0]
}
return flattenedMap, nil
}

View File

@ -10,7 +10,7 @@ import (
const (
// maxBlocksInGetBlocksResult is the max amount of blocks that are
// allowed in a GetBlocksResult.
maxBlocksInGetBlocksResult = 1000
maxBlocksInGetBlocksResult = 100
)
func handleGetBlocks(s *Server, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) {

View File

@ -9,7 +9,7 @@ import (
// HandlePanic recovers panics, log them, and then exits the process.
func HandlePanic(log logs.Logger, backendLog *logs.Backend) {
if err := recover(); err != nil {
log.Criticalf("Fatal error: %s", err)
log.Criticalf("Fatal error: %+v", err)
log.Criticalf("Stack trace: %s", debug.Stack())
if backendLog != nil {
backendLog.Close()