mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-03-30 15:08:33 +00:00
[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:
parent
c66fb294c8
commit
0e278ca22b
@ -1,4 +1,4 @@
|
||||
package controllers
|
||||
package apimodels
|
||||
|
||||
// RawTransaction represents a raw transaction posted to the API server
|
||||
type RawTransaction struct {
|
63
apiserver/apimodels/response_types.go
Normal file
63
apiserver/apimodels/response_types.go
Normal 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"`
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
63
apiserver/controllers/common.go
Normal file
63
apiserver/controllers/common.go
Normal 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
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
package models
|
||||
package dbmodels
|
||||
|
||||
import (
|
||||
"time"
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
112
faucet/config/config.go
Normal 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
150
faucet/database/database.go
Normal 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
9
faucet/database/log.go
Normal 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
331
faucet/faucet.go
Normal 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
65
faucet/ip_usage.go
Normal 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
11
faucet/log.go
Normal 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
89
faucet/main.go
Normal 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)
|
||||
}
|
1
faucet/migrations/000001_create_ip_uses_table.down.sql
Normal file
1
faucet/migrations/000001_create_ip_uses_table.down.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE `ip_uses`;
|
6
faucet/migrations/000001_create_ip_uses_table.up.sql
Normal file
6
faucet/migrations/000001_create_ip_uses_table.up.sql
Normal 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
81
faucet/server.go
Normal 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
2
go.mod
@ -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
8
go.sum
@ -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=
|
||||
|
@ -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...))
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package utils
|
||||
package httpserverutils
|
||||
|
||||
import "github.com/daglabs/btcd/util/panics"
|
||||
import "github.com/daglabs/btcd/apiserver/logger"
|
@ -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)
|
58
httpserverutils/request.go
Normal file
58
httpserverutils/request.go
Normal 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
|
||||
}
|
@ -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) {
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user