Skip to content

Commit

Permalink
Insight API [13] [14] [19] [20] [20.1] [20.2] [20.3] [20.4] [21] [21.…
Browse files Browse the repository at this point in the history
…1] (decred#506)

This implements the following Insight API endpoints (listed in decred#400): [13] [14] [19] [20] [21]
  • Loading branch information
papacarp authored and Jujhar committed Sep 26, 2018
1 parent 6a3f04f commit d16d2f8
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 33 deletions.
27 changes: 26 additions & 1 deletion api/insight/apimiddleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const (
ctxBlockIndex
ctxNoTxList
ctxAddrCmd
ctxNbBlocks
)

// BlockHashPathAndIndexCtx is a middleware that embeds the value at the url
Expand All @@ -43,7 +44,8 @@ func (c *insightApiContext) BlockHashPathAndIndexCtx(next http.Handler) http.Han
}

// StatusCtx is a middleware that embeds into the request context the data for
// the "?q=x" URL query, where x is "getInfo" or "getDifficulty".
// the "?q=x" URL query, where x is "getInfo" or "getDifficulty" or
// "getBestBlockHash" or "getLastBlockHash".
func (c *insightApiContext) StatusInfoCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := m.StatusInfoCtx(r, c.BlockData.ChainDB)
Expand Down Expand Up @@ -379,3 +381,26 @@ func (c *insightApiContext) BlockDateLimitQueryCtx(next http.Handler) http.Handl
next.ServeHTTP(w, r.WithContext(ctx))
})
}

// GetNbBlocksCtx retrieves the ctxNbBlocks data from the request context. If not
// set, the return value is 0.
func (c *insightApiContext) GetNbBlocksCtx(r *http.Request) int {
nbBlocks, ok := r.Context().Value(ctxNbBlocks).(int)
if !ok {
return 0
}
return nbBlocks
}

// NbBlocksCtx will parse the query parameters for nbBlocks.
func (c *insightApiContext) NbBlocksCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
nbBlocks := r.FormValue("nbBlocks")
nbBlocksint, err := strconv.Atoi(nbBlocks)
if err == nil {
ctx = context.WithValue(r.Context(), ctxNbBlocks, nbBlocksint)
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
4 changes: 3 additions & 1 deletion api/insight/apirouter.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,10 @@ func NewInsightApiRouter(app *insightApiContext, userRealIP bool) ApiMux {
mux.With(m.TransactionHashCtx).Get("/rawtx/{txid}", app.getTransactionHex)
mux.With(m.TransactionsCtx).Get("/txs", app.getTransactions)

// Status
// Status and Utility
mux.With(app.StatusInfoCtx).Get("/status", app.getStatusInfo)
mux.With(app.NbBlocksCtx).Get("/utils/estimatefee", app.getEstimateFee)
mux.Get("/peer", app.GetPeerStatus)

// Addresses endpoints
mux.Route("/addrs", func(rd chi.Router) {
Expand Down
248 changes: 218 additions & 30 deletions api/insight/apiroutes.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,39 +396,128 @@ func (c *insightApiContext) getTransactions(w http.ResponseWriter, r *http.Reque
hash := m.GetBlockHashCtx(r)
address := m.GetAddressCtx(r)
if hash == "" && address == "" {
http.Error(w, http.StatusText(422), 422)
writeInsightError(w, "Required query parameters (address or block) not present.")
return
}

if hash != "" {
blockTransactions := c.BlockData.GetTransactionsForBlockByHash(hash)
if blockTransactions == nil {
blkTrans := c.BlockData.GetBlockVerboseByHash(hash, true)
if blkTrans == nil {
apiLog.Errorf("Unable to get block %s transactions", hash)
http.Error(w, http.StatusText(422), 422)
writeInsightError(w, fmt.Sprintf("Unable to get block %s transactions", hash))
return
}

txsOld := []*dcrjson.TxRawResult{}
txcount := len(blkTrans.RawTx) + len(blkTrans.RawSTx)
// Merge tx and stx together and limit result to 10 max
count := 0
for _, tx := range blkTrans.RawTx {
txsOld = append(txsOld, &tx)
count++
if count > 10 {
break
}
}
if count < 10 {
for _, tx := range blkTrans.RawSTx {
txsOld = append(txsOld, &tx)
count++
if count > 10 {
break
}
}
}

// Convert to Insight struct
txsNew, err := c.TxConverter(txsOld)
if err != nil {
apiLog.Error("Error Processing Transactions")
writeInsightError(w, "Error Processing Transactions")
return
}

blockTransactions := apitypes.InsightBlockAddrTxSummary{
PagesTotal: int64(txcount),
Txs: txsNew,
}
writeJSON(w, blockTransactions, c.getIndentQuery(r))
return
}

if address != "" {
address := m.GetAddressCtx(r)
if address == "" {
http.Error(w, http.StatusText(422), 422)
// Validate Address
_, err := dcrutil.DecodeAddress(address)
if err != nil {
writeInsightError(w, fmt.Sprintf("Address is invalid (%s)", address))
return
}
txs := c.BlockData.InsightGetAddressTransactions(address, 20, 0)
if txs == nil {
http.Error(w, http.StatusText(422), 422)
addresses := []string{address}
rawTxs, recentTxs := c.BlockData.ChainDB.InsightPgGetAddressTransactions(addresses, int64(c.Status.Height-2))

addressOuts, _, err := c.MemPool.UnconfirmedTxnsForAddress(address)
UnconfirmedTxs := []string{}

if err != nil {
writeInsightError(w, fmt.Sprintf("Error gathering mempool transactions (%s)", err))
return
}

txsOutput := struct {
Txs []*dcrjson.SearchRawTransactionsResult `json:"txs"`
}{
txs,
FUNDING_TX_DUPLICATE_CHECK:
for _, f := range addressOuts.Outpoints {
// Confirm its not already in our recent transactions
for _, v := range recentTxs {
if v == f.Hash.String() {
continue FUNDING_TX_DUPLICATE_CHECK
}
}
UnconfirmedTxs = append(UnconfirmedTxs, f.Hash.String()) // Funding tx
recentTxs = append(recentTxs, f.Hash.String())
}
SPENDING_TX_DUPLICATE_CHECK:
for _, f := range addressOuts.PrevOuts {
for _, v := range recentTxs {
if v == f.TxSpending.String() {
continue SPENDING_TX_DUPLICATE_CHECK
}
}
UnconfirmedTxs = append(UnconfirmedTxs, f.TxSpending.String()) // Spending tx
recentTxs = append(recentTxs, f.TxSpending.String())
}

// Merge unconfirmed with confirmed transactions
rawTxs = append(UnconfirmedTxs, rawTxs...)

txcount := len(rawTxs)

if txcount > 10 {
rawTxs = rawTxs[0:10]
}
writeJSON(w, txsOutput, c.getIndentQuery(r))

txsOld := []*dcrjson.TxRawResult{}
for _, rawTx := range rawTxs {
txOld, err1 := c.BlockData.GetRawTransaction(rawTx)
if err1 != nil {
apiLog.Errorf("Unable to get transaction %s", rawTx)
writeInsightError(w, fmt.Sprintf("Error gathering transaction details (%s)", err1))
return
}
txsOld = append(txsOld, txOld)
}

// Convert to Insight struct
txsNew, err := c.TxConverter(txsOld)
if err != nil {
apiLog.Error("Error Processing Transactions")
writeInsightError(w, "Error Processing Transactions")
return
}

addrTransactions := apitypes.InsightBlockAddrTxSummary{
PagesTotal: int64(txcount),
Txs: txsNew,
}
writeJSON(w, addrTransactions, c.getIndentQuery(r))
}
}

Expand Down Expand Up @@ -613,33 +702,90 @@ func (c *insightApiContext) getAddressTotalSent(w http.ResponseWriter, r *http.R
writeText(w, strconv.Itoa(int(addressInfo.TotalSpent)))
}

// TODO getDifficulty and getInfo
func (c *insightApiContext) getStatusInfo(w http.ResponseWriter, r *http.Request) {
statusInfo := m.GetStatusInfoCtx(r)

if statusInfo == "" {
http.Error(w, http.StatusText(422), 422)
// best block idx is also embedded through the middleware. We could use
// this value or the other best blocks as done below. Which one is best?
// idx := m.GetBlockIndexCtx(r)

infoResult, err := c.nodeClient.GetInfo()
if err != nil {
apiLog.Error("Error getting status")
writeInsightError(w, fmt.Sprintf("Error getting status (%s)", err))
return
}

if statusInfo == "getLastBlockHash" {
hash := c.getBlockHashCtx(r)
hashOutput := struct {
LastBlockHash string `json:"lastblockhash"`
switch statusInfo {
case "getDifficulty":
info := struct {
Difficulty float64 `json:"difficulty"`
}{
hash,
infoResult.Difficulty,
}
writeJSON(w, info, c.getIndentQuery(r))
case "getBestBlockHash":
blockhash, err := c.nodeClient.GetBlockHash(int64(infoResult.Blocks))
if err != nil {
apiLog.Errorf("Error getting block hash %d (%s)", infoResult.Blocks, err)
writeInsightError(w, fmt.Sprintf("Error getting block hash %d (%s)", infoResult.Blocks, err))
return
}
writeJSON(w, hashOutput, c.getIndentQuery(r))
}

if statusInfo == "getBestBlockHash" {
hash := c.getBlockHashCtx(r)
hashOutput := struct {
info := struct {
BestBlockHash string `json:"bestblockhash"`
}{
hash,
blockhash.String(),
}
writeJSON(w, info, c.getIndentQuery(r))
case "getLastBlockHash":
blockhashtip, err := c.nodeClient.GetBlockHash(int64(infoResult.Blocks))
if err != nil {
apiLog.Errorf("Error getting block hash %d (%s)", infoResult.Blocks, err)
writeInsightError(w, fmt.Sprintf("Error getting block hash %d (%s)", infoResult.Blocks, err))
return
}
lastblockhash, err := c.nodeClient.GetBlockHash(int64(c.Status.Height))
if err != nil {
apiLog.Errorf("Error getting block hash %d (%s)", c.Status.Height, err)
writeInsightError(w, fmt.Sprintf("Error getting block hash %d (%s)", c.Status.Height, err))
return
}
writeJSON(w, hashOutput, c.getIndentQuery(r))

info := struct {
SyncTipHash string `json:"syncTipHash"`
LastBlockHash string `json:"lastblockhash"`
}{
blockhashtip.String(),
lastblockhash.String(),
}
writeJSON(w, info, c.getIndentQuery(r))
default:
info := struct {
Version int32 `json:"version"`
Protocolversion int32 `json:"protocolversion"`
Blocks int32 `json:"blocks"`
NodeTimeoffset int64 `json:"timeoffset"`
NodeConnections int32 `json:"connections"`
Proxy string `json:"proxy"`
Difficulty float64 `json:"difficulty"`
Testnet bool `json:"testnet"`
Relayfee float64 `json:"relayfee"`
Errors string `json:"errors"`
}{
infoResult.Version,
infoResult.ProtocolVersion,
infoResult.Blocks,
infoResult.TimeOffset,
infoResult.Connections,
infoResult.Proxy,
infoResult.Difficulty,
infoResult.TestNet,
infoResult.RelayFee,
infoResult.Errors,
}

writeJSON(w, info, c.getIndentQuery(r))
}

}
Expand Down Expand Up @@ -866,3 +1012,45 @@ func (c *insightApiContext) getAddressInfo(w http.ResponseWriter, r *http.Reques

writeJSON(w, addressInfo, c.getIndentQuery(r))
}

func (c *insightApiContext) getEstimateFee(w http.ResponseWriter, r *http.Request) {
nbBlocks := c.GetNbBlocksCtx(r)
if nbBlocks == 0 {
nbBlocks = 2
}
estimateFee := make(map[string]float64)

// A better solution would be a call to the DCRD RPC "estimatefee" endpoint
// but that does not appear to be exposed currently.
infoResult, err := c.nodeClient.GetInfo()
if err != nil {
apiLog.Error("Error getting status")
writeInsightError(w, fmt.Sprintf("Error getting status (%s)", err))
return
}
estimateFee[strconv.Itoa(nbBlocks)] = infoResult.RelayFee

writeJSON(w, estimateFee, c.getIndentQuery(r))
}

// GetPeerStatus handles requests for node peer info (i.e. getpeerinfo RPC).
func (c *insightApiContext) GetPeerStatus(w http.ResponseWriter, r *http.Request) {
// Use a RPC call to tell if we are connected or not
_, err := c.nodeClient.GetPeerInfo()
var connected bool
if err == nil {
connected = true
} else {
connected = false
}
var port *string
peerInfo := struct {
Connected bool `json:"connected"`
Host string `json:"host"`
Port *string `json:"port"`
}{
connected, "127.0.0.1", port,
}

writeJSON(w, peerInfo, c.getIndentQuery(r))
}
7 changes: 7 additions & 0 deletions api/types/insightapitypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,10 @@ type InsightBlocksSummaryResult struct {
MoreTs int64 `json:"moreTs,omitempty"`
} `json:"pagination"`
}

// InsightBlockAddrTxSummary models the data required by addrtx json return for
// Insight API
type InsightBlockAddrTxSummary struct {
PagesTotal int64 `json:"pagesTotal"`
Txs []InsightTx `json:"txs"`
}
2 changes: 1 addition & 1 deletion stakedb/stakedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ func (db *StakeDatabase) applyDiff(poolDiff PoolDiff) {
for _, hash := range poolDiff.In {
_, ok := db.liveTicketCache[hash]
if ok {
log.Error("Just tried to add a ticket (%v) to the pool, but it was already there!", hash)
log.Errorf("Just tried to add a ticket (%v) to the pool, but it was already there!", hash)
continue
}

Expand Down

0 comments on commit d16d2f8

Please sign in to comment.