func main() { var err error var latestheight, latestheightcache int var blockscached *[]*btcplex.Block usage := `BTCplex webapp/API server. Usage: btcplex-server [--config=<path>] btcplex-server -h | --help Options: -h --help Show this screen. -c <path>, --config <path> Path to config file [default: config.json]. ` arguments, _ := docopt.Parse(usage, nil, true, "btcplex-server", false) confFile := "config.json" if arguments["--config"] != nil { confFile = arguments["--config"].(string) } log.Println("Starting btcplex-server") conf, err = btcplex.LoadConfig(confFile) if err != nil { log.Fatalf("Can't load config file: %v\n", err) } // Used for pub/sub in the webapp and data like latest processed height pool, err := btcplex.GetRedis(conf) if err != nil { log.Fatalf("Can't connect to Redis: %v\n", err) } // Due to args injection I can't use two *redis.Pool with maritini rediswrapper := new(RedisWrapper) rediswrapper.Pool = pool ssdb, err := btcplex.GetSSDB(conf) if err != nil { log.Fatalf("Can't connect to SSDB: %v\n", err) } // Setup some pubsub: // Compute the unconfirmed transaction count in a ticker utxscnt := 0 utxscntticker := time.NewTicker(1 * time.Second) go func(pool *redis.Pool, utxscnt *int) { c := pool.Get() defer c.Close() for _ = range utxscntticker.C { *utxscnt, _ = redis.Int(c.Do("ZCARD", "btcplex:rawmempool")) } }(pool, &utxscnt) // Pool the latest height from BTCplex db, // also track the status/check if BTCplex goes out of sync latestheightticker := time.NewTicker(1 * time.Second) checkinprogress := false bitcoindheight := btcplex.GetBlockCountRPC(conf) btcplexsynced := true go func(pool *redis.Pool, latestheight *int) { c := pool.Get() defer c.Close() for _ = range latestheightticker.C { *latestheight, _ = redis.Int(c.Do("GET", "height:latest")) if latestheightcache != *latestheight { log.Println("Re-building homepage blocks cache") blocks, _ := btcplex.GetLastXBlocks(ssdb, uint(*latestheight), uint(*latestheight-30)) blockscached = &blocks latestheightcache = *latestheight } bitcoindheight = btcplex.GetBlockCountRPC(conf) if uint(latestheightcache) != bitcoindheight && !checkinprogress && btcplexsynced { checkinprogress = true go func(checkinprogress *bool) { if bitcoindheight-uint(latestheightcache) > 20 { btcplexsynced = false log.Printf("CRITICAL: OUT OF SYNC / btcplex:%v, bitcoind:%v\n", latestheightcache, bitcoindheight) } else { log.Println("WARNING: BTCplex Out of sync, waiting before another check") time.Sleep(synctimeout * time.Second) if btcplexsynced && uint(latestheightcache) != bitcoindheight { btcplexsynced = false log.Printf("CRITICAL: OUT OF SYNC / btcplex:%v, bitcoind:%v\n", latestheightcache, bitcoindheight) } } *checkinprogress = false }(&checkinprogress) } if uint(latestheightcache) == bitcoindheight && !btcplexsynced { log.Println("INFO: Sync with bitcoind done") btcplexsynced = true } } }(ssdb, &latestheight) // PubSub channel for blocknotify bitcoind RPC like blocknotifygroup := bcast.NewGroup() go blocknotifygroup.Broadcasting(0) go bcastToRedisPubSub(pool, blocknotifygroup, "btcplex:blocknotify2") // PubSub channel for unconfirmed txs / rawmemorypool utxgroup := bcast.NewGroup() go utxgroup.Broadcasting(0) go bcastToRedisPubSub(pool, utxgroup, "btcplex:utxs") // TODO Ticker for utxs count => events_unconfirmed newblockgroup := bcast.NewGroup() go newblockgroup.Broadcasting(0) go bcastToRedisPubSub(pool, newblockgroup, "btcplex:newblock") btcplexsyncedgroup := bcast.NewGroup() go btcplexsyncedgroup.Broadcasting(0) // Go template helper appHelpers := template.FuncMap{ "cut": func(addr string, length int) string { return fmt.Sprintf("%v...", addr[:length]) }, "cutmiddle": func(addr string, length int) string { return fmt.Sprintf("%v...%v", addr[:length], addr[len(addr)-length:]) }, "tokb": func(size uint32) string { return fmt.Sprintf("%.3f", float32(size)/1024) }, "computefee": func(tx *btcplex.Tx) string { if tx.TotalIn == 0 { return "0" } return fmt.Sprintf("%v", float32(tx.TotalIn-tx.TotalOut)/1e8) }, "generationmsg": func(tx *btcplex.Tx) string { reward := btcplex.GetBlockReward(tx.BlockHeight) fee := float64(tx.TotalOut-uint64(reward)) / 1e8 return fmt.Sprintf("%v MAZA + %.8f total fees", float64(reward)/1e8, fee) }, "tobtc": func(val uint64) string { return fmt.Sprintf("%.8f", float64(val)/1e8) }, "inttobtc": func(val int64) string { return fmt.Sprintf("%.8f", float64(val)/1e8) }, "formatprevout": func(prevout *btcplex.PrevOut) string { return fmt.Sprintf("%v:%v", prevout.Hash, prevout.Vout) }, "formattime": func(ts uint32) string { return fmt.Sprintf("%v", time.Unix(int64(ts), 0).UTC()) }, "formatiso": func(ts uint32) string { return fmt.Sprintf("%v", time.Unix(int64(ts), 0).Format(time.RFC3339)) }, "sub": func(h, p uint) uint { return h - p }, "add": func(h, p uint) uint { return h + p }, "iadd": func(h, p int) int { return h + p }, "confirmation": func(hash string, height uint) uint { bm, _ := btcplex.NewBlockMeta(ssdb, hash) if bm.Main == false { return 0 } return uint(latestheight) - height + 1 }, "is_orphaned": func(block *btcplex.Block) bool { if block.Height == uint(latestheight) { return false } return !block.Main }, } m := martini.Classic() m.Map(rediswrapper) m.Map(ssdb) tmpldir := "templates" if conf.AppTemplatesPath != "" { tmpldir = conf.AppTemplatesPath } m.Use(render.Renderer(render.Options{ Directory: tmpldir, Layout: "layout", Funcs: []template.FuncMap{appHelpers}, })) // We rate limit the API if enabled in the config if conf.AppApiRateLimited { m.Use(func(res http.ResponseWriter, req *http.Request, rediswrapper *RedisWrapper, log *log.Logger) { remoteIP := strings.Split(req.RemoteAddr, ":")[0] _, xforwardedfor := req.Header["X-Forwarded-For"] if xforwardedfor { remoteIP = req.Header["X-Forwarded-For"][1] } log.Printf("R:%v\nip:%+v\n", time.Now(), remoteIP) if strings.Contains(req.RequestURI, "/api/") { ratelimited, cnt, reset := rateLimited(rediswrapper, remoteIP) // Set X-RateLimit-* Header res.Header().Set("X-RateLimit-Limit", strconv.Itoa(ratelimitcnt)) res.Header().Set("X-RateLimit-Remaining", strconv.Itoa(ratelimitcnt-cnt)) res.Header().Set("X-RateLimit-Reset", strconv.Itoa(reset)) // Set CORS header res.Header().Set("Access-Control-Expose-Headers", " X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset") res.Header().Set("Access-Control-Allow-Origin", "*") if ratelimited { res.WriteHeader(429) } } }) } // Don't want Google to crawl API m.Get("/robots.txt", func() string { return "User-agent: *\nDisallow: /api" }) m.Get("/", func(r render.Render, db *redis.Pool) { pm := new(pageMeta) pm.BtcplexSynced = btcplexsynced pm.Blocks = blockscached pm.Title = "Latest Bitcoin blocks" pm.Description = "Open source Bitcoin block chain explorer with JSON API" pm.Menu = "latest_blocks" pm.LastHeight = uint(latestheight) pm.Analytics = conf.AppGoogleAnalytics r.HTML(200, "index", &pm) }) m.Get("/blocks/:currentheight", func(params martini.Params, r render.Render, db *redis.Pool) { pm := new(pageMeta) pm.BtcplexSynced = btcplexsynced currentheight, _ := strconv.ParseUint(params["currentheight"], 10, 0) blocks, _ := btcplex.GetLastXBlocks(db, uint(currentheight), uint(currentheight-30)) pm.Blocks = &blocks pm.Title = "Bitcoin blocks" pm.Menu = "blocks" pm.LastHeight = uint(latestheight) pm.CurrentHeight = uint(currentheight) pm.Analytics = conf.AppGoogleAnalytics r.HTML(200, "blocks", &pm) }) m.Get("/block/:hash", func(params martini.Params, r render.Render, db *redis.Pool) { pm := new(pageMeta) pm.BtcplexSynced = btcplexsynced pm.LastHeight = uint(latestheight) block, _ := btcplex.GetBlockCachedByHash(db, params["hash"]) block.FetchMeta(db) btcplex.By(btcplex.TxIndex).Sort(block.Txs) pm.Block = block pm.Title = fmt.Sprintf("Bitcoin block #%v", block.Height) pm.Description = fmt.Sprintf("Bitcoin block #%v summary and related transactions", block.Height) pm.Analytics = conf.AppGoogleAnalytics r.HTML(200, "block", &pm) }) m.Get("/api/block/:hash", func(params martini.Params, r render.Render, db *redis.Pool, req *http.Request) { block, _ := btcplex.GetBlockCachedByHash(db, params["hash"]) block.FetchMeta(db) btcplex.By(btcplex.TxIndex).Sort(block.Txs) block.Links = initHATEOAS(block.Links, req) if block.Parent != "" { block.Links = addHATEOAS(block.Links, "previous_block", fmt.Sprintf("%v/api/block/%v", conf.AppUrl, block.Parent)) } if block.Next != "" { block.Links = addHATEOAS(block.Links, "next_block", fmt.Sprintf("%v/api/block/%v", conf.AppUrl, block.Next)) } r.JSON(200, block) }) m.Get("/unconfirmed-transactions", func(params martini.Params, r render.Render, db *redis.Pool, rdb *RedisWrapper) { //rpool := rdb.Pool pm := new(pageMeta) pm.BtcplexSynced = btcplexsynced pm.LastHeight = uint(latestheight) pm.Menu = "utxs" pm.Title = "Unconfirmed transactions" pm.Description = "Transactions waiting to be included in a Bitcoin block, updated in real time." //utxs, _ := btcplex.GetUnconfirmedTxs(rpool) pm.Txs = &[]*btcplex.Tx{} pm.Analytics = conf.AppGoogleAnalytics r.HTML(200, "unconfirmed-transactions", &pm) }) m.Get("/tx/:hash", func(params martini.Params, r render.Render, db *redis.Pool, rdb *RedisWrapper) { var tx *btcplex.Tx rpool := rdb.Pool pm := new(pageMeta) pm.BtcplexSynced = btcplexsynced pm.LastHeight = uint(latestheight) isutx, _ := btcplex.IsUnconfirmedTx(rpool, params["hash"]) if isutx { pm.TxUnconfirmed = true tx, _ = btcplex.GetUnconfirmedTx(rpool, params["hash"]) } else { tx, _ = btcplex.GetTx(db, params["hash"]) tx.Build(db) } pm.Tx = tx pm.Title = fmt.Sprintf("Bitcoin transaction %v", tx.Hash) pm.Description = fmt.Sprintf("Bitcoin transaction %v summary.", tx.Hash) pm.Analytics = conf.AppGoogleAnalytics r.HTML(200, "tx", pm) }) m.Get("/api/tx/:hash", func(params martini.Params, r render.Render, db *redis.Pool, rdb *RedisWrapper, req *http.Request) { var tx *btcplex.Tx rpool := rdb.Pool isutx, _ := btcplex.IsUnconfirmedTx(rpool, params["hash"]) if isutx { tx, _ = btcplex.GetUnconfirmedTx(rpool, params["hash"]) } else { tx, _ = btcplex.GetTx(db, params["hash"]) tx.Build(db) } tx.Links = initHATEOAS(tx.Links, req) if tx.BlockHash != "" { tx.Links = addHATEOAS(tx.Links, "block", fmt.Sprintf("%v/api/block/%v", conf.AppUrl, tx.BlockHash)) } r.JSON(200, tx) }) m.Get("/address/:address", func(params martini.Params, r render.Render, db *redis.Pool, req *http.Request) { pm := new(pageMeta) pm.BtcplexSynced = btcplexsynced pm.LastHeight = uint(latestheight) pm.PaginationData = new(PaginationData) pm.Title = fmt.Sprintf("Bitcoin address %v", params["address"]) pm.Description = fmt.Sprintf("Transactions and summary for the Bitcoin address %v.", params["address"]) // AddressData addressdata, _ := btcplex.GetAddress(db, params["address"]) pm.AddressData = addressdata // Pagination d := float64(addressdata.TxCnt) / float64(txperpage) pm.PaginationData.MaxPage = int(math.Ceil(d)) currentPage := req.URL.Query().Get("page") if currentPage == "" { currentPage = "1" } pm.PaginationData.CurrentPage, _ = strconv.Atoi(currentPage) pm.PaginationData.Pages = N(pm.PaginationData.MaxPage) pm.PaginationData.Next = 0 pm.PaginationData.Prev = 0 if pm.PaginationData.CurrentPage > 1 { pm.PaginationData.Prev = pm.PaginationData.CurrentPage - 1 } if pm.PaginationData.CurrentPage < pm.PaginationData.MaxPage { pm.PaginationData.Next = pm.PaginationData.CurrentPage + 1 } fmt.Printf("%+v\n", pm.PaginationData) // Fetch txs given the pagination addressdata.FetchTxs(db, txperpage*(pm.PaginationData.CurrentPage-1), txperpage*pm.PaginationData.CurrentPage) r.HTML(200, "address", pm) }) m.Get("/api/address/:address", func(params martini.Params, r render.Render, db *redis.Pool, req *http.Request) { addressdata, _ := btcplex.GetAddress(db, params["address"]) lastPage := int(math.Ceil(float64(addressdata.TxCnt) / float64(txperpage))) currentPageStr := req.URL.Query().Get("page") if currentPageStr == "" { currentPageStr = "1" } currentPage, _ := strconv.Atoi(currentPageStr) // HATEOS section addressdata.Links = initHATEOAS(addressdata.Links, req) pageurl := "%v/api/address/%v?page=%v" if currentPage < lastPage { addressdata.Links = addHATEOAS(addressdata.Links, "last", fmt.Sprintf(pageurl, conf.AppUrl, params["address"], lastPage)) addressdata.Links = addHATEOAS(addressdata.Links, "next", fmt.Sprintf(pageurl, conf.AppUrl, params["address"], currentPage+1)) } if currentPage > 1 { addressdata.Links = addHATEOAS(addressdata.Links, "previous", fmt.Sprintf(pageurl, conf.AppUrl, params["address"], currentPage-1)) } addressdata.FetchTxs(db, txperpage*(currentPage-1), txperpage*currentPage) r.JSON(200, addressdata) }) m.Get("/about", func(r render.Render) { pm := new(pageMeta) pm.BtcplexSynced = btcplexsynced pm.LastHeight = uint(latestheight) pm.Title = "About" pm.Description = "Learn more about BTCPlex, an open source Bitcoin blockchain browser written in Go." pm.Menu = "about" pm.Analytics = conf.AppGoogleAnalytics r.HTML(200, "about", pm) }) m.Get("/status", func(r render.Render) { pm := new(pageMeta) pm.BtcplexSynced = btcplexsynced pm.LastHeight = uint(latestheight) pm.Title = "Status" pm.Description = "BTCplex status page." pm.Menu = "status" pm.Analytics = conf.AppGoogleAnalytics btcplexinfo, _ := btcplex.GetInfoRPC(conf) pm.BitcoindInfo = btcplexinfo r.HTML(200, "status", pm) }) m.Post("/search", binding.Form(searchForm{}), binding.ErrorHandler, func(search searchForm, r render.Render, db *redis.Pool, rdb *RedisWrapper) { rpool := rdb.Pool pm := new(pageMeta) pm.BtcplexSynced = btcplexsynced // Check if the query isa block height isblockheight, hash := btcplex.IsBlockHeight(db, search.Query) if isblockheight && hash != "" { r.Redirect(fmt.Sprintf("/block/%v", hash)) } // Check if the query is block hash isblockhash, hash := btcplex.IsBlockHash(db, search.Query) if isblockhash { r.Redirect(fmt.Sprintf("/block/%v", hash)) } // Check for TX istxhash, txhash := btcplex.IsTxHash(db, search.Query) if istxhash { r.Redirect(fmt.Sprintf("/tx/%v", txhash)) } isutx, txhash := btcplex.IsUnconfirmedTx(rpool, search.Query) if isutx { r.Redirect(fmt.Sprintf("/tx/%v", txhash)) } // Check for Bitcoin address isaddress, address := btcplex.IsAddress(search.Query) if isaddress { r.Redirect(fmt.Sprintf("/address/%v", address)) } pm.Title = "Search" pm.Error = "Nothing found" pm.Analytics = conf.AppGoogleAnalytics r.HTML(200, "search", pm) }) m.Get("/api/getblockcount", func(r render.Render) { r.JSON(200, latestheight) }) // m.Get("/api/latesthash", func(r render.Render) { // r.JSON(200, latesthash) // }) m.Get("/api/getblockhash/:height", func(r render.Render, params martini.Params, db *redis.Pool) { height, _ := strconv.ParseUint(params["height"], 10, 0) blockhash, _ := btcplex.GetBlockHash(db, uint(height)) r.JSON(200, blockhash) }) m.Get("/api/getreceivedbyaddress/:address", func(r render.Render, params martini.Params, db *redis.Pool) { res, _ := btcplex.GetReceivedByAddress(db, params["address"]) r.JSON(200, res) }) m.Get("/api/getsentbyaddress/:address", func(r render.Render, params martini.Params, db *redis.Pool) { res, _ := btcplex.GetSentByAddress(db, params["address"]) r.JSON(200, res) }) m.Get("/api/addressbalance/:address", func(r render.Render, params martini.Params, db *redis.Pool) { res, _ := btcplex.AddressBalance(db, params["address"]) r.JSON(200, res) }) m.Get("/api/checkaddress/:address", func(params martini.Params, r render.Render) { valid, _ := btcplex.ValidA58([]byte(params["address"])) r.JSON(200, valid) }) m.Get("/api/blocknotify", func(w http.ResponseWriter, r *http.Request) { incrementClient() defer decrementClient() running := true notifier := w.(http.CloseNotifier).CloseNotify() timer := time.NewTimer(time.Second * 1800) f, _ := w.(http.Flusher) w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") bnotifier := blocknotifygroup.Join() defer bnotifier.Close() var ls interface{} for { if running { select { case ls = <-bnotifier.In: io.WriteString(w, fmt.Sprintf("data: %v\n\n", ls.(string))) f.Flush() case <-notifier: running = false log.Println("CLOSED") break case <-timer.C: running = false log.Println("TimeOUT") } } else { log.Println("DONE") break } } }) m.Get("/api/utxs/:address", func(w http.ResponseWriter, params martini.Params, r *http.Request, rdb *RedisWrapper) { incrementClient() defer decrementClient() rpool := rdb.Pool running := true notifier := w.(http.CloseNotifier).CloseNotify() timer := time.NewTimer(time.Second * 3600) f, _ := w.(http.Flusher) w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") utxs := make(chan string) go func(rpool *redis.Pool, utxs chan<- string) { conn := rpool.Get() defer conn.Close() psc := redis.PubSubConn{Conn: conn} psc.Subscribe(fmt.Sprintf("addr:%v:txs", params["address"])) for { switch v := psc.Receive().(type) { case redis.Message: utxs <- string(v.Data) } } }(rpool, utxs) var ls string for { if running { select { case ls = <-utxs: io.WriteString(w, fmt.Sprintf("data: %v\n\n", ls)) f.Flush() case <-notifier: running = false log.Println("CLOSED") break case <-timer.C: running = false log.Println("TimeOUT") } } else { log.Println("DONE") break } } }) m.Get("/api/utxs", func(w http.ResponseWriter, r *http.Request) { incrementClient() defer decrementClient() running := true notifier := w.(http.CloseNotifier).CloseNotify() timer := time.NewTimer(time.Second * 3600) f, _ := w.(http.Flusher) w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") utx := utxgroup.Join() defer utx.Close() var ls interface{} for { if running { select { case ls = <-utx.In: io.WriteString(w, fmt.Sprintf("data: %v\n\n", ls.(string))) f.Flush() case <-notifier: running = false log.Println("CLOSED") break case <-timer.C: running = false log.Println("TimeOUT") } } else { log.Println("DONE") break } } }) m.Get("/events", func(w http.ResponseWriter, r *http.Request) { running := true notifier := w.(http.CloseNotifier).CloseNotify() timer := time.NewTimer(time.Second * 8400) f, _ := w.(http.Flusher) w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") newblockg := newblockgroup.Join() defer newblockg.Close() var ls interface{} for { if running { select { case ls = <-newblockg.In: io.WriteString(w, fmt.Sprintf("data: %v\n\n", ls.(string))) f.Flush() case <-notifier: running = false log.Println("CLOSED") break case <-timer.C: running = false log.Println("TimeOUT") } } else { log.Println("DONE") break } } }) m.Get("/events_unconfirmed", func(w http.ResponseWriter, r *http.Request) { running := true notifier := w.(http.CloseNotifier).CloseNotify() timer := time.NewTimer(time.Second * 3600) f, _ := w.(http.Flusher) w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") utx := utxgroup.Join() defer utx.Close() var ls interface{} for { if running { select { case ls = <-utx.In: buf := bytes.NewBufferString("") utx := new(btcplex.Tx) json.Unmarshal([]byte(ls.(string)), utx) t := template.New("").Funcs(appHelpers) utxtmpl, _ := ioutil.ReadFile(fmt.Sprintf("%v/utx.tmpl", tmpldir)) t, err := t.Parse(string(utxtmpl)) if err != nil { log.Printf("ERR:%v", err) } err = t.Execute(buf, utx) if err != nil { log.Printf("ERR EXEC:%v", err) } res := map[string]interface{}{} // Full unconfirmed cnt from global variables res["cnt"] = utxscnt // HTML template of the transaction res["tmpl"] = buf.String() // Last updated time res["time"] = time.Now().UTC().Format(time.RFC3339) resjson, _ := json.Marshal(res) io.WriteString(w, fmt.Sprintf("data: %v\n\n", string(resjson))) f.Flush() case <-notifier: running = false log.Println("CLOSED") break case <-timer.C: running = false log.Println("TimeOUT") } } else { log.Println("DONE") break } } }) m.Get("/api/info", func(r render.Render) { activeclientsmutex.Lock() defer activeclientsmutex.Unlock() btcplexinfo, _ := btcplex.GetInfoRPC(conf) r.JSON(200, map[string]interface{}{"activeclients": activeclients, "info": btcplexinfo}) }) log.Printf("Listening on port: %v\n", conf.AppPort) http.ListenAndServe(fmt.Sprintf(":%v", conf.AppPort), m) }
func main() { fmt.Printf("GOMAXPROCS is %d\n", getGOMAXPROCS()) confFile := "config.json" conf, err := btcplex.LoadConfig(confFile) if err != nil { log.Fatalf("Can't load config file: %v", err) } pool, err := btcplex.GetSSDB(conf) if err != nil { log.Fatalf("Can't connect to SSDB: %v", err) } opts := levigo.NewOptions() opts.SetCreateIfMissing(true) filter := levigo.NewBloomFilter(10) opts.SetFilterPolicy(filter) ldb, err := levigo.Open(conf.LevelDbPath, opts) //alpha defer ldb.Close() if err != nil { log.Fatalf("failed to load db: %s\n", err) } wo := levigo.NewWriteOptions() //wo.SetSync(true) defer wo.Close() ro := levigo.NewReadOptions() defer ro.Close() wb := levigo.NewWriteBatch() defer wb.Close() conn := pool.Get() defer conn.Close() log.Println("Waiting 3 seconds before starting...") time.Sleep(3 * time.Second) latestheight := 0 log.Printf("Latest height: %v\n", latestheight) running = true cs := make(chan os.Signal, 1) signal.Notify(cs, os.Interrupt) go func() { for sig := range cs { running = false log.Printf("Captured %v, waiting for everything to finish...\n", sig) wg.Wait() defer os.Exit(1) } }() concurrency := 250 sem := make(chan bool, concurrency) // Real network magic byte blockchain, blockchainerr := blkparser.NewBlockchain(conf.BitcoindBlocksPath, [4]byte{0xF8, 0xB5, 0x03, 0xDF}) if blockchainerr != nil { log.Fatalf("Error loading block file: ", blockchainerr) } block_height := uint(0) for { if !running { break } wg.Add(1) bl, er := blockchain.NextBlock() if er != nil { log.Println("Initial import done.") break } bl.Raw = nil if bl.Parent == "" { block_height = uint(0) conn.Do("HSET", fmt.Sprintf("block:%v:h", bl.Hash), "main", true) conn.Do("HSET", fmt.Sprintf("block:%v:h", bl.Hash), "height", 0) } else { parentheight, _ := redis.Int(conn.Do("HGET", fmt.Sprintf("block:%v:h", bl.Parent), "height")) block_height = uint(parentheight + 1) conn.Do("HSET", fmt.Sprintf("block:%v:h", bl.Hash), "height", block_height) prevheight := block_height - 1 prevhashtest := bl.Parent prevnext := bl.Hash for { prevkey := fmt.Sprintf("height:%v", prevheight) prevcnt, _ := redis.Int(conn.Do("ZCARD", prevkey)) // SSDB doesn't support negative slice yet prevs, _ := redis.Strings(conn.Do("ZRANGE", prevkey, 0, prevcnt-1)) for _, cprevhash := range prevs { if cprevhash == prevhashtest { // current block parent prevhashtest, _ = redis.String(conn.Do("HGET", fmt.Sprintf("block:%v:h", cprevhash), "parent")) // Set main to 1 and the next => prevnext conn.Do("HMSET", fmt.Sprintf("block:%v:h", cprevhash), "main", true, "next", prevnext) conn.Do("SET", fmt.Sprintf("block:height:%v", prevheight), cprevhash) prevnext = cprevhash } else { // Set main to 0 conn.Do("HSET", fmt.Sprintf("block:%v:h", cprevhash), "main", false) oblock, _ := btcplex.GetBlockCachedByHash(pool, cprevhash) for _, otx := range oblock.Txs { otx.Revert(pool) } } } if len(prevs) == 1 { break } prevheight-- } //} } // Orphans blocks handling conn.Do("ZADD", fmt.Sprintf("height:%v", block_height), bl.BlockTime, bl.Hash) conn.Do("HSET", fmt.Sprintf("block:%v:h", bl.Hash), "parent", bl.Parent) if latestheight != 0 && !(latestheight+1 <= int(block_height)) { log.Printf("Skipping block #%v\n", block_height) continue } log.Printf("Current block: %v (%v)\n", block_height, bl.Hash) block := new(Block) block.Hash = bl.Hash block.Height = block_height block.Version = bl.Version block.MerkleRoot = bl.MerkleRoot block.BlockTime = bl.BlockTime block.Bits = bl.Bits block.Nonce = bl.Nonce block.Size = bl.Size block.Parent = bl.Parent txs := []*Tx{} total_bl_out := uint64(0) for tx_index, tx := range bl.Txs { //log.Printf("Tx #%v: %v\n", tx_index, tx.Hash) total_tx_out := uint64(0) total_tx_in := uint64(0) //conn.Send("MULTI") txos := []*btcplex.TxOut{} txis := []*btcplex.TxIn{} for txo_index, txo := range tx.TxOuts { txwg.Add(1) sem <- true go func(bl *blkparser.Block, tx *blkparser.Tx, pool *redis.Pool, total_tx_out *uint64, txo *blkparser.TxOut, txo_index int) { conn := pool.Get() defer conn.Close() defer func() { <-sem }() defer txwg.Done() atomic.AddUint64(total_tx_out, uint64(txo.Value)) //atomic.AddUint32(txos_cnt, 1) ntxo := new(btcplex.TxOut) ntxo.TxHash = tx.Hash ntxo.BlockHash = bl.Hash ntxo.BlockTime = bl.BlockTime ntxo.Addr = txo.Addr ntxo.Value = txo.Value ntxo.Index = uint32(txo_index) txospent := new(btcplex.TxoSpent) ntxo.Spent = txospent ntxocached := new(TxOutCached) ntxocached.Addr = txo.Addr ntxocached.Value = txo.Value ntxocachedjson, _ := json.Marshal(ntxocached) ldb.Put(wo, []byte(fmt.Sprintf("txo:%v:%v", tx.Hash, txo_index)), ntxocachedjson) ntxojson, _ := json.Marshal(ntxo) ntxokey := fmt.Sprintf("txo:%v:%v", tx.Hash, txo_index) conn.Do("SET", ntxokey, ntxojson) //conn.Send("ZADD", fmt.Sprintf("txo:%v", tx.Hash), txo_index, ntxokey) conn.Do("ZADD", fmt.Sprintf("addr:%v", ntxo.Addr), bl.BlockTime, tx.Hash) conn.Do("ZADD", fmt.Sprintf("addr:%v:received", ntxo.Addr), bl.BlockTime, tx.Hash) conn.Do("HINCRBY", fmt.Sprintf("addr:%v:h", ntxo.Addr), "tr", ntxo.Value) txomut.Lock() txos = append(txos, ntxo) txomut.Unlock() }(bl, tx, pool, &total_tx_out, txo, txo_index) } //txis_cnt := uint32(0) // Skip the ins if it's a CoinBase Tx (1 TxIn for newly generated coins) if !(len(tx.TxIns) == 1 && tx.TxIns[0].InputVout == 0xffffffff) { for txi_index, txi := range tx.TxIns { txwg.Add(1) sem <- true go func(txi *blkparser.TxIn, bl *blkparser.Block, tx *blkparser.Tx, pool *redis.Pool, total_tx_in *uint64, txi_index int) { conn := pool.Get() defer conn.Close() defer func() { <-sem }() defer txwg.Done() ntxi := new(btcplex.TxIn) ntxi.TxHash = tx.Hash ntxi.BlockHash = bl.Hash ntxi.BlockTime = bl.BlockTime ntxi.Index = uint32(txi_index) nprevout := new(btcplex.PrevOut) nprevout.Vout = txi.InputVout nprevout.Hash = txi.InputHash ntxi.PrevOut = nprevout prevtxo := new(TxOutCached) prevtxocachedraw, err := ldb.Get(ro, []byte(fmt.Sprintf("txo:%v:%v", txi.InputHash, txi.InputVout))) if err != nil { log.Printf("Err getting prevtxocached: %v", err) } if len(prevtxocachedraw) > 0 { if err := json.Unmarshal(prevtxocachedraw, prevtxo); err != nil { panic(err) } } else { // Shouldn't happen! //log.Println("Fallback to SSDB") prevtxoredisjson, err := redis.String(conn.Do("GET", fmt.Sprintf("txo:%v:%v", txi.InputHash, txi.InputVout))) if err != nil { log.Printf("KEY:%v\n", fmt.Sprintf("txo:%v:%v", txi.InputHash, txi.InputVout)) panic(err) } prevtxoredis := new(btcplex.TxOut) json.Unmarshal([]byte(prevtxoredisjson), prevtxoredis) prevtxo.Addr = prevtxoredis.Addr prevtxo.Value = prevtxoredis.Value //prevtxo.Id = prevtxomongo.Id.Hex() } ldb.Delete(wo, []byte(fmt.Sprintf("txo:%v:%v", txi.InputHash, txi.InputVout))) nprevout.Address = prevtxo.Addr nprevout.Value = prevtxo.Value txospent := new(btcplex.TxoSpent) txospent.Spent = true txospent.BlockHeight = uint32(block_height) txospent.InputHash = tx.Hash txospent.InputIndex = uint32(txi_index) //total_tx_in+= uint(nprevout.Value) atomic.AddUint64(total_tx_in, nprevout.Value) tximut.Lock() txis = append(txis, ntxi) tximut.Unlock() //atomic.AddUint32(txis_cnt, 1) //log.Println("Starting update prev txo") ntxijson, _ := json.Marshal(ntxi) ntxikey := fmt.Sprintf("txi:%v:%v", tx.Hash, txi_index) txospentjson, _ := json.Marshal(txospent) conn.Do("SET", ntxikey, ntxijson) //conn.Send("ZADD", fmt.Sprintf("txi:%v", tx.Hash), txi_index, ntxikey) conn.Do("SET", fmt.Sprintf("txo:%v:%v:spent", txi.InputHash, txi.InputVout), txospentjson) conn.Do("ZADD", fmt.Sprintf("addr:%v", nprevout.Address), bl.BlockTime, tx.Hash) conn.Do("ZADD", fmt.Sprintf("addr:%v:sent", nprevout.Address), bl.BlockTime, tx.Hash) conn.Do("HINCRBY", fmt.Sprintf("addr:%v:h", nprevout.Address), "ts", nprevout.Value) }(txi, bl, tx, pool, &total_tx_in, txi_index) } } err := ldb.Write(wo, wb) if err != nil { log.Fatalf("Err write batch: %v", err) } wb.Clear() txwg.Wait() total_bl_out += total_tx_out ntx := new(Tx) ntx.Index = uint32(tx_index) ntx.Hash = tx.Hash ntx.Size = tx.Size ntx.LockTime = tx.LockTime ntx.Version = tx.Version ntx.TxInCnt = uint32(len(txis)) ntx.TxOutCnt = uint32(len(txos)) ntx.TotalOut = uint64(total_tx_out) ntx.TotalIn = uint64(total_tx_in) ntx.BlockHash = bl.Hash ntx.BlockHeight = block_height ntx.BlockTime = bl.BlockTime ntxjson, _ := json.Marshal(ntx) ntxjsonkey := fmt.Sprintf("tx:%v", ntx.Hash) conn.Do("SET", ntxjsonkey, ntxjson) conn.Do("ZADD", fmt.Sprintf("tx:%v:blocks", tx.Hash), bl.BlockTime, bl.Hash) conn.Do("ZADD", fmt.Sprintf("block:%v:txs", block.Hash), tx_index, ntxjsonkey) ntx.TxIns = txis ntx.TxOuts = txos txs = append(txs, ntx) } block.TotalBTC = uint64(total_bl_out) block.TxCnt = uint32(len(txs)) blockjson, _ := json.Marshal(block) conn.Do("ZADD", "blocks", block.BlockTime, block.Hash) conn.Do("MSET", fmt.Sprintf("block:%v", block.Hash), blockjson, "height:latest", int(block_height), fmt.Sprintf("block:height:%v", block.Height), block.Hash) block.Txs = txs blockjsoncache, _ := json.Marshal(block) conn.Do("SET", fmt.Sprintf("block:%v:cached", block.Hash), blockjsoncache) if !running { log.Printf("Done. Stopped at height: %v.", block_height) } wg.Done() } wg.Wait() }