Example #1
0
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)
}
Example #2
0
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()
}