// Copyright 2017-2018 DERO Project. All rights reserved. // Use of this source code in any form is governed by RESEARCH license. // license can be found in the LICENSE file. // GPG: 0F39 E425 8C65 3947 702A 8234 08B2 0360 A03A 9DE8 // // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL // THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package main // this file implements the explorer for DERO blockchain // this needs only RPC access // NOTE: Only use data exported from within the RPC interface, do direct use of exported variables fom packages // NOTE: we can use structs defined within the RPCserver package // This is being developed to track down and confirm some bugs // NOTE: This is NO longer entirely compliant with the xyz RPC interface ( the pool part is not compliant), currently and can be used as it for their chain, // atleast for the last 1 year // TODO: error handling is non-existant ( as this was built up in hrs ). Add proper error handling // import "time" import "fmt" import "net" import "bytes" import "strings" import "encoding/hex" import "net/http" import "html/template" import "encoding/json" import "io/ioutil" import "github.com/docopt/docopt-go" import log "github.com/sirupsen/logrus" import "github.com/ybbus/jsonrpc" import "github.com/deroproject/derosuite/block" import "github.com/deroproject/derosuite/crypto" import "github.com/deroproject/derosuite/transaction" import "github.com/deroproject/derosuite/blockchain/rpcserver" var command_line string = `dero_explorer DERO Explorer: A secure, private blockchain with smart-contracts Usage: dero_explorer [--help] [--version] [--debug] [--rpc-server-address=<127.0.0.1:18091>] [--http-address=<0.0.0.0:8080>] dero_explorer -h | --help dero_explorer --version Options: -h --help Show this screen. --version Show version. --debug Debug mode enabled, print log messages --rpc-server-address=<127.0.0.1:18091> connect to this daemon port as client --http-address=<0.0.0.0:8080> explorer listens on this port to serve user requests` var rpcClient *jsonrpc.RPCClient var netClient *http.Client var endpoint string var replacer = strings.NewReplacer("h", ":", "m", ":", "s", "") func main() { var err error var arguments map[string]interface{} arguments, err = docopt.Parse(command_line, nil, true, "DERO Explorer : work in progress", false) if err != nil { log.Fatalf("Error while parsing options err: %s\n", err) } if arguments["--debug"].(bool) == true { log.SetLevel(log.DebugLevel) } log.Debugf("Arguments %+v", arguments) log.Infof("DERO Exporer : This is under heavy development, use it for testing/evaluations purpose only") log.Infof("Copyright 2017-2018 DERO Project. All rights reserved.") endpoint = "127.0.0.1:9999" if arguments["--rpc-server-address"] != nil { endpoint = arguments["--rpc-server-address"].(string) } log.Infof("using RPC endpoint %s", endpoint) listen_address := "0.0.0.0:8080" if arguments["--http-address"] != nil { listen_address = arguments["--http-address"].(string) } log.Infof("Will listen on %s", listen_address) // create client rpcClient = jsonrpc.NewRPCClient("http://" + endpoint + "/json_rpc") var netTransport = &http.Transport{ Dial: (&net.Dialer{ Timeout: 5 * time.Second, }).Dial, TLSHandshakeTimeout: 5 * time.Second, } netClient = &http.Client{ Timeout: time.Second * 10, Transport: netTransport, } // execute rpc to service response, err := rpcClient.Call("get_info") if err == nil { log.Infof("Connection to RPC server successful") } else { log.Fatalf("Connection to RPC server Failed err %s", err) } var info rpcserver.GetInfo_Result err = response.GetObject(&info) fmt.Printf("%+v err %s\n", info, err) http.HandleFunc("/search", search_handler) http.HandleFunc("/page/", page_handler) http.HandleFunc("/block/", block_handler) http.HandleFunc("/txpool/", txpool_handler) http.HandleFunc("/tx/", tx_handler) http.HandleFunc("/", root_handler) fmt.Printf("Listening for requests\n") err = http.ListenAndServe(listen_address, nil) log.Warnf("Listening to port %s err : %s", listen_address, err) } // all the tx info which ever needs to be printed type txinfo struct { Height string // height at which tx was mined Depth uint64 Timestamp uint64 // timestamp Age string // time diff from current time Block_time string // UTC time from block header Epoch uint64 // Epoch time In_Pool bool // whether tx was in pool Hash string // hash for hash PrefixHash string // prefix hash Version int // version of tx Size string // size of tx in KB Sizeuint64 uint64 // size of tx in bytes Fee string // fee in TX Feeuint64 uint64 // fee in atomic units In int // inputs counts Out int // outputs counts Amount string CoinBase bool // is tx coin base Extra string // extra within tx Keyimages []string // key images within tx OutAddress []string // contains output secret key OutOffset []uint64 // contains index offsets Type string // ringct or ruffct ( bulletproof) Ring_size int } // any information for block which needs to be printed type block_info struct { Major_Version uint64 Minor_Version uint64 Height uint64 Depth uint64 Timestamp uint64 Hash string Prev_Hash string Nonce uint64 Fees string Reward string Size string Age string // time diff from current time Block_time string // UTC time from block header Epoch uint64 // Epoch time Outputs string Mtx txinfo Txs []txinfo Orphan_Status bool Tx_Count int } // load and setup block_info from rpc // if hash is less than 64 bytes then it is considered a height parameter func load_block_from_rpc(info *block_info, block_hash string, recursive bool) (err error) { var bl block.Block var bresult rpcserver.GetBlock_Result var block_height int var block_bin []byte if len(block_hash) != 64 { // parameter is a height fmt.Sscanf(block_hash, "%d", &block_height) // user requested block height log.Debugf("User requested block at height %d user input %s", block_height, block_hash) response, err := rpcClient.CallNamed("getblock", map[string]interface{}{"height": uint64(block_height)}) if err != nil { return err } err = response.GetObject(&bresult) if err != nil { return err } } else { // parameter is the hex blob log.Debugf("User requested block %s", block_hash) response, err := rpcClient.CallNamed("getblock", map[string]interface{}{"hash": block_hash}) if err != nil { log.Warnf("err %s ", err) return err } if response.Error != nil { log.Warnf("err %s ", response.Error) return fmt.Errorf("No Such block or other Error") } err = response.GetObject(&bresult) if err != nil { return err } } // fmt.Printf("block %d %+v\n",i, bresult) info.Height = bresult.Block_Header.Height info.Depth = bresult.Block_Header.Depth duration_second := (uint64(time.Now().UTC().Unix()) - bresult.Block_Header.Timestamp) info.Age = replacer.Replace((time.Duration(duration_second) * time.Second).String()) info.Block_time = time.Unix(int64(bresult.Block_Header.Timestamp), 0).Format("2006-01-02 15:04:05") info.Epoch = bresult.Block_Header.Timestamp info.Outputs = fmt.Sprintf("%.03f", float32(bresult.Block_Header.Reward)/1000000000000.0) info.Size = "N/A" info.Hash = bresult.Block_Header.Hash info.Prev_Hash = bresult.Block_Header.Prev_Hash info.Orphan_Status = bresult.Block_Header.Orphan_Status info.Nonce = bresult.Block_Header.Nonce info.Major_Version = bresult.Block_Header.Major_Version info.Minor_Version = bresult.Block_Header.Minor_Version info.Reward = fmt.Sprintf("%.03f", float32(bresult.Block_Header.Reward)/1000000000000.0) block_bin, _ = hex.DecodeString(bresult.Blob) bl.Deserialize(block_bin) if recursive { // fill in miner tx info err = load_tx_from_rpc(&info.Mtx, bl.Miner_tx.GetHash().String()) //TODO handle error info.Tx_Count = len(bl.Tx_hashes) fees := uint64(0) size := uint64(0) // if we have any other tx load them also for i := 0; i < len(bl.Tx_hashes); i++ { var tx txinfo err = load_tx_from_rpc(&tx, bl.Tx_hashes[i].String()) //TODO handle error info.Txs = append(info.Txs, tx) fees += tx.Feeuint64 size += tx.Sizeuint64 } info.Fees = fmt.Sprintf("%.03f", float32(fees)/1000000000000.0) info.Size = fmt.Sprintf("%.03f", float32(size)/1024) } return } // this will fill up the info struct from the tx func load_tx_info_from_tx(info *txinfo, tx *transaction.Transaction) (err error) { info.Hash = tx.GetHash().String() info.PrefixHash = tx.GetPrefixHash().String() info.Size = fmt.Sprintf("%.03f", float32(len(tx.Serialize()))/1024) info.Sizeuint64 = uint64(len(tx.Serialize())) info.Version = int(tx.Version) info.Extra = fmt.Sprintf("%x", tx.Extra) info.In = len(tx.Vin) info.Out = len(tx.Vout) if !tx.IsCoinbase() { info.Fee = fmt.Sprintf("%.012f", float64(tx.RctSignature.Get_TX_Fee())/1000000000000) info.Feeuint64 = tx.RctSignature.Get_TX_Fee() info.Amount = "?" info.Ring_size = len(tx.Vin[0].(transaction.Txin_to_key).Key_offsets) for i := 0; i < len(tx.Vin); i++ { info.Keyimages = append(info.Keyimages, fmt.Sprintf("%s ring members %+v", tx.Vin[i].(transaction.Txin_to_key).K_image, tx.Vin[i].(transaction.Txin_to_key).Key_offsets)) } } else { info.CoinBase = true info.In = 0 info.Amount = fmt.Sprintf("%.012f", float64(tx.Vout[0].Amount)/1000000000000) } for i := 0; i < len(tx.Vout); i++ { info.OutAddress = append(info.OutAddress, tx.Vout[i].Target.(transaction.Txout_to_key).Key.String()) } // if outputs cannot be located, do not panic // this will be the case for pool transactions if len(info.OutAddress) != len(info.OutOffset) { info.OutOffset = make([]uint64, len(info.OutAddress), len(info.OutAddress)) } switch tx.RctSignature.Get_Sig_Type() { case 0: info.Type = "RingCT/0" case 1: info.Type = "RingCT/1 MG" case 2: info.Type = "RingCT/2 Simple" } if !info.In_Pool { // find the age of block and other meta var blinfo block_info err := load_block_from_rpc(&blinfo, fmt.Sprintf("%s", info.Height), false) // we only need block data and not data of txs if err != nil { return err } // fmt.Printf("Blinfo %+v height %d", blinfo, info.Height); info.Age = blinfo.Age info.Block_time = blinfo.Block_time info.Epoch = blinfo.Epoch info.Timestamp = blinfo.Epoch info.Depth = blinfo.Depth } return nil } // load and setup txinfo from rpc func load_tx_from_rpc(info *txinfo, txhash string) (err error) { var tx_params rpcserver.GetTransaction_Params var tx_result rpcserver.GetTransaction_Result //fmt.Printf("Requesting tx data %s", txhash); tx_params.Tx_Hashes = append(tx_params.Tx_Hashes, txhash) request_bytes, err := json.Marshal(&tx_params) response, err := http.Post("http://"+endpoint+"/gettransactions", "application/json", bytes.NewBuffer(request_bytes)) if err != nil { //fmt.Printf("err while requesting tx err %s",err); return } buf, err := ioutil.ReadAll(response.Body) if err != nil { // fmt.Printf("err while reading reponse body err %s",err); return } err = json.Unmarshal(buf, &tx_result) if err != nil { return } // fmt.Printf("TX response %+v", tx_result) if tx_result.Status != "OK" { return fmt.Errorf("No Such TX RPC error status %s", tx_result.Status) } var tx transaction.Transaction if len(tx_result.Txs_as_hex[0]) < 50 { return } tx_bin, _ := hex.DecodeString(tx_result.Txs_as_hex[0]) tx.DeserializeHeader(tx_bin) // fill as much info required from headers if tx_result.Txs[0].In_pool { info.In_Pool = true } else { info.Height = fmt.Sprintf("%d", tx_result.Txs[0].Block_Height) } for x := range tx_result.Txs[0].Output_Indices { info.OutOffset = append(info.OutOffset, tx_result.Txs[0].Output_Indices[x]) } //fmt.Printf("tx_result %+v\n",tx_result.Txs) return load_tx_info_from_tx(info, &tx) } func block_handler(w http.ResponseWriter, r *http.Request) { param := "" fmt.Sscanf(r.URL.EscapedPath(), "/block/%s", ¶m) var blinfo block_info err := load_block_from_rpc(&blinfo, param, true) _ = err // execute template now data := map[string]interface{}{} data["title"] = "DERO BlockChain Explorer (Golang)" data["servertime"] = time.Now().UTC().Format("2006-01-02 15:04:05") data["block"] = blinfo t, err := template.New("foo").Parse(header_template + block_template + footer_template) err = t.ExecuteTemplate(w, "block", data) if err != nil { return } return // fmt.Fprint(w, "This is a valid block") } func tx_handler(w http.ResponseWriter, r *http.Request) { var info txinfo tx_hex := "" fmt.Sscanf(r.URL.EscapedPath(), "/tx/%s", &tx_hex) txhash := crypto.HashHexToHash(tx_hex) log.Debugf("user requested TX %s", tx_hex) err := load_tx_from_rpc(&info, txhash.String()) //TODO handle error _ = err // execute template now data := map[string]interface{}{} data["title"] = "DERO BlockChain Explorer (Golang)" data["servertime"] = time.Now().UTC().Format("2006-01-02 15:04:05") data["info"] = info t, err := template.New("foo").Parse(header_template + tx_template + footer_template) err = t.ExecuteTemplate(w, "tx", data) if err != nil { return } return } func pool_handler(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "This is a valid pool") } // if there is any error, we return back empty // if pos is wrong we return back // pos is descending order func fill_tx_structure(pos int, size_in_blocks int) (data []block_info) { for i := pos; i > (pos-11) && i >= 0; i-- { // query blocks by height var blinfo block_info err := load_block_from_rpc(&blinfo, fmt.Sprintf("%d", i), true) if err == nil { data = append(data, blinfo) } } return } func show_page(w http.ResponseWriter, page int) { data := map[string]interface{}{} var info rpcserver.GetInfo_Result data["title"] = "DERO BlockChain Explorer (Golang)" data["servertime"] = time.Now().UTC().Format("2006-01-02 15:04:05") t, err := template.New("foo").Parse(header_template + txpool_template + main_template + paging_template + footer_template) // collect all the data afresh // execute rpc to service response, err := rpcClient.Call("get_info") if err != nil { goto exit_error } err = response.GetObject(&info) if err != nil { goto exit_error } //fmt.Printf("get info %+v", info) data["Network_Difficulty"] = info.Difficulty data["hash_rate"] = fmt.Sprintf("%.03f", float32(info.Difficulty/1000000)/float32(info.Target)) data["txpool_size"] = info.Tx_pool_size data["testnet"] = info.Testnet if int(info.Height) < page*11 { // use requested invalid page, give page 0 page = 0 } data["previous_page"] = 0 if page > 0 { data["previous_page"] = page - 1 } data["current_page"] = page data["total_page"] = int(info.Height) / 11 data["next_page"] = page + 1 if (page + 1) > int(info.Height)/11 { data["next_page"] = page } fill_tx_pool_info(data, 25) data["block_array"] = fill_tx_structure(int(info.Height)-(page*11), 11) err = t.ExecuteTemplate(w, "main", data) if err != nil { goto exit_error } return exit_error: fmt.Fprintf(w, "Error occurred err %s", err) } func txpool_handler(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{} var info rpcserver.GetInfo_Result data["title"] = "DERO BlockChain Explorer (Golang)" data["servertime"] = time.Now().UTC().Format("2006-01-02 15:04:05") t, err := template.New("foo").Parse(header_template + txpool_template + main_template + paging_template + footer_template + txpool_page_template) // collect all the data afresh // execute rpc to service response, err := rpcClient.Call("get_info") if err != nil { goto exit_error } err = response.GetObject(&info) if err != nil { goto exit_error } //fmt.Printf("get info %+v", info) data["Network_Difficulty"] = info.Difficulty data["hash_rate"] = fmt.Sprintf("%.03f", float32(info.Difficulty/1000000)/float32(info.Target)) data["txpool_size"] = info.Tx_pool_size data["testnet"] = info.Testnet fill_tx_pool_info(data, 500) // show only 500 txs err = t.ExecuteTemplate(w, "txpool_page", data) if err != nil { goto exit_error } return exit_error: fmt.Fprintf(w, "Error occurred err %s", err) } // shows a page func page_handler(w http.ResponseWriter, r *http.Request) { page := 0 page_string := r.URL.EscapedPath() fmt.Sscanf(page_string, "/page/%d", &page) log.Debugf("user requested page %d", page) show_page(w, page) } // root shows page 0 func root_handler(w http.ResponseWriter, r *http.Request) { log.Debugf("Showing main page") show_page(w, 0) } // search handler, finds the items using rpc bruteforce func search_handler(w http.ResponseWriter, r *http.Request) { log.Debugf("Showing search page") values, ok := r.URL.Query()["value"] if !ok || len(values) < 1 { show_page(w, 0) return } // Query()["key"] will return an array of items, // we only want the single item. value := values[0] // check whether the page is block or tx or height var blinfo block_info var tx txinfo err := load_block_from_rpc(&blinfo, value, false) if err == nil { log.Debugf("Redirecting user to block page") http.Redirect(w, r, "/block/"+value, 302) return } err = load_tx_from_rpc(&tx, value) //TODO handle error if err == nil { log.Debugf("Redirecting user to tx page") http.Redirect(w, r, "/tx/"+value, 302) return } show_page(w, 0) return } // fill all the tx pool info as per requested func fill_tx_pool_info(data map[string]interface{}, max_count int) error { var txs []txinfo var txpool rpcserver.GetTxPool_Result data["mempool"] = txs // initialize with empty data // collect all the data afresh // execute rpc to service response, err := rpcClient.Call("gettxpool") if err != nil { return fmt.Errorf("gettxpool rpc failed") } err = response.GetObject(&txpool) if err != nil { return fmt.Errorf("gettxpool rpc failed") } for i := range txpool.Tx_list { var info txinfo err := load_tx_from_rpc(&info, txpool.Tx_list[i]) //TODO handle error if err != nil { continue } txs = append(txs, info) if len(txs) >= max_count { break } } data["mempool"] = txs return nil }