Repository: trustwallet/blockatlas
Pull request #968 (2020-03-17 14:17:34)

[Observer] Start rewriting observer service: separate service - add observer_parser and observer_notifier

todo[bot]: ## notifier

https://github.com/trustwallet/blockatlas/blob/ce881cada242fd68fb48dfebf7e3bfb12da2209d/cmd/observer_notifier/main.go#L42-L43

---

###### This comment was generated by [todo](https://todo.jasonet.co) based on a `TODO` comment in ce881cada242fd68fb48dfebf7e3bfb12da2209d in #968. cc @trustwallet.
todo[bot]: ## add retry

https://github.com/trustwallet/blockatlas/blob/ce881cada242fd68fb48dfebf7e3bfb12da2209d/observer/stream.go#L107-L112

---

###### This comment was generated by [todo](https://todo.jasonet.co) based on a `TODO` comment in ce881cada242fd68fb48dfebf7e3bfb12da2209d in #968. cc @trustwallet.
EnoRage: #966 
EnoRage: /azp run

azure-pipelines[bot]: <samp>
Azure Pipelines failed to run 1 pipeline(s).<br>

</samp>

Patch:
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
deleted file mode 100644
index 5624ce3..0000000
--- a/CODE_OF_CONDUCT.md
+++ /dev/null
@@ -1,2 +0,0 @@
-* There are no stupid questions
-* Be respectful :)
diff --git a/Makefile b/Makefile
index bc49234..95278e9 100644
--- a/Makefile
+++ b/Makefile
@@ -7,7 +7,8 @@ BUILD := $(shell git rev-parse --short HEAD)
 DATETIME := $(shell date +"%Y.%m.%d-%H:%M:%S")
 PROJECT_NAME := $(shell basename "$(PWD)")
 API_SERVICE := platform_api
-OBSERVER_SERVICE := observer_worker
+OSERVER_NOTIFIER := observer_notifier
+OBSERVER_PARSER := observer_parser
 OBSERVER_SUBSCRIBER := observer_subscriber
 SWAGGER_API := swagger_api
 COIN_FILE := coin/coins.yml
@@ -34,7 +35,8 @@ STDERR := /tmp/.$(PROJECT_NAME)-stderr.txt
 
 # PID file will keep the process id of the server
 PID_API := /tmp/.$(PROJECT_NAME).$(API_SERVICE).pid
-PID_OBSERVER := /tmp/.$(PROJECT_NAME).$(OBSERVER_SERVICE).pid
+PID_OBSERVER_NOTIFIER := /tmp/.$(PROJECT_NAME).$(OBSERVER_NOTIFIER).pid
+PID_OBSERVER_PARSER := /tmp/.$(PROJECT_NAME).$(OBSERVER_PARSER).pid
 PID_OBSERVER_SUBSCRIBER := /tmp/.$(PROJECT_NAME).$(OBSERVER_SUBSCRIBER).pid
 PID_SWAGGER_API := /tmp/.$(PROJECT_NAME).$(SWAGGER_API).pid
 PID_DYSON := /tmp/.$(PROJECT_NAME).dyson.pid
@@ -46,7 +48,7 @@ install: go-get
 
 ## start: Start API, Observer and Sync in development mode.
 start:
-	@bash -c "$(MAKE) clean compile start-platform-api start-observer-worker start-observer-subscriber"
+	@bash -c "$(MAKE) clean compile start-platform-api start-observer-parser start-observer-notifier start-observer-subscriber"
 
 ## start-api: Start platform api in development mode.
 start-platform-api: stop
@@ -69,14 +71,21 @@ start-swagger-api: stop
 	@cat $(PID_SWAGGER_API) | sed "/^/s/^/  \>  Sync PID: /"
 	@echo "  >  Error log: $(STDERR)"
 
-## start-observer-worker: Start swagger api in development mode.
-start-observer-worker: stop
+## start-observer-parser: Start observer-parser in development mode.
+start-observer-parser: stop
 	@echo "  >  Starting $(PROJECT_NAME)"
-	@-$(GOBIN)/$(OBSERVER_SERVICE)/observer_worker -c $(CONFIG_FILE) 2>&1 & echo $$! > $(PID_OBSERVER)
-	@cat $(PID_OBSERVER) | sed "/^/s/^/  \>  Sync PID: /"
+	@-$(GOBIN)/$(OBSERVER_PARSER)/observer_parser -c $(CONFIG_FILE) 2>&1 & echo $$! > $(PID_OBSERVER_PARSER)
+	@cat $(PID_OBSERVER_PARSER) | sed "/^/s/^/  \>  Sync PID: /"
 	@echo "  >  Error log: $(STDERR)"
 
-## start-observer-worker: Start swagger api in development mode.
+## start-observer-notifier: Start observer-notifier in development mode.
+start-observer-notifier: stop
+	@echo "  >  Starting $(PROJECT_NAME)"
+	@-$(GOBIN)/$(OBSERVER_NOTIFIER)/observer_notifier -c $(CONFIG_FILE) 2>&1 & echo $$! > $(PID_OBSERVER_NOTIFIER)
+	@cat $(PID_OBSERVER_NOTIFIER) | sed "/^/s/^/  \>  Sync PID: /"
+	@echo "  >  Error log: $(STDERR)"
+
+## start-observer-subscriber: Start observer-subscriber in development mode.
 start-observer-subscriber: stop
 	@echo "  >  Starting $(PROJECT_NAME)"
 	@-$(GOBIN)/$(OBSERVER_SUBSCRIBER)/observer_subscriber -c $(CONFIG_FILE) 2>&1 & echo $$! > $(PID_OBSERVER_SUBSCRIBER)
@@ -85,13 +94,14 @@ start-observer-subscriber: stop
 
 ## stop: Stop development mode.
 stop:
-	@-touch $(PID_API) $(PID_OBSERVER) $(PID_OBSERVER_SUBSCRIBER) $(PID_SWAGGER_API)
+	@-touch $(PID_API) $(PID_OBSERVER_NOTIFIER) $(PID_OBSERVER_PARSER) $(PID_OBSERVER_SUBSCRIBER) $(PID_SWAGGER_API)
 	@-kill `cat $(PID_API)` 2> /dev/null || true
-	@-kill `cat $(PID_OBSERVER)` 2> /dev/null || true
+	@-kill `cat $(OBSERVER_NOTIFIER)` 2> /dev/null || true
+	@-kill `cat $(OBSERVER_PARSER)` 2> /dev/null || true
 	@-kill `cat $(PID_OBSERVER_SUBSCRIBER)` 2> /dev/null || true
 	@-kill `cat $(PID_SWAGGER_API)` 2> /dev/null || true
 	@-kill `cat $(PID_DYSON)` 2> /dev/null || true
-	@-rm $(PID_API) $(PID_OBSERVER) $(PID_OBSERVER_SUBSCRIBER) $(PID_SWAGGER_API)
+	@-rm $(PID_API) $(PID_OBSERVER_NOTIFIER) $(PID_OBSERVER_PARSER) $(PID_OBSERVER_SUBSCRIBER) $(PID_SWAGGER_API)
 
 stop-dyson:
 	@kill `cat $(PID_DYSON)` 2> /dev/null || true
@@ -205,8 +215,10 @@ go-compile: go-get go-build
 go-build:
 	@echo "  >  Building platform_api binary..."
 	GOBIN=$(GOBIN) go build $(LDFLAGS) -o $(GOBIN)/$(API_SERVICE)/platform_api ./cmd/$(API_SERVICE)
-	@echo "  >  Building observer_worker binary..."
-	GOBIN=$(GOBIN) go build $(LDFLAGS) -o $(GOBIN)/$(OBSERVER_SERVICE)/observer_worker ./cmd/$(OBSERVER_SERVICE)
+	@echo "  >  Building observer_notifier binary..."
+	GOBIN=$(GOBIN) go build $(LDFLAGS) -o $(GOBIN)/$(OSERVER_NOTIFIER)/observer_notifier ./cmd/$(OSERVER_NOTIFIER)
+	@echo "  >  Building observer_parser binary..."
+	GOBIN=$(GOBIN) go build $(LDFLAGS) -o $(GOBIN)/$(OBSERVER_PARSER)/observer_parser ./cmd/$(OBSERVER_PARSER)
 	@echo "  >  Building observer_subscriber binary..."
 	GOBIN=$(GOBIN) go build $(LDFLAGS) -o $(GOBIN)/$(OBSERVER_SUBSCRIBER)/observer_subscriber ./cmd/$(OBSERVER_SUBSCRIBER)
 	@echo "  >  Building swagger_api binary..."
diff --git a/api/observer_healthcheck.go b/api/observer_healthcheck.go
new file mode 100644
index 0000000..dafe734
--- /dev/null
+++ b/api/observer_healthcheck.go
@@ -0,0 +1,12 @@
+package api
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/trustwallet/blockatlas/services/observer/healthcheck"
+	"net/http"
+)
+
+func GetObserverStatus(c *gin.Context) {
+	exclude := splitParam(c.Query("exclude"))
+	c.JSON(http.StatusOK, healthcheck.GetStatus(exclude))
+}
diff --git a/api/routes.go b/api/routes.go
index c843a21..8766293 100644
--- a/api/routes.go
+++ b/api/routes.go
@@ -7,6 +7,7 @@ import (
 	"github.com/trustwallet/blockatlas/pkg/ginutils"
 	"github.com/trustwallet/blockatlas/pkg/logger"
 	"github.com/trustwallet/blockatlas/platform"
+	"strings"
 )
 
 var routers = make(map[string]gin.IRouter)
@@ -74,6 +75,10 @@ func SetupPlatformAPI(root gin.IRouter) {
 	logger.Info("Routes set up", logger.Params{"routes": len(routers)})
 }
 
+func SetupHealthCheckApi(root gin.IRouter) {
+	root.GET("/", GetObserverStatus)
+}
+
 // getRouter lazy loads routers
 func getRouter(router *gin.RouterGroup, handle string) gin.IRouter {
 	key := fmt.Sprintf("%s/%s", router.BasePath(), handle)
@@ -107,3 +112,10 @@ func GetStatus(c *gin.Context) {
 		"date":   internal.Date,
 	})
 }
+
+func splitParam(param string) []string {
+	splitFn := func(c rune) bool {
+		return c == ','
+	}
+	return strings.FieldsFunc(param, splitFn)
+}
\ No newline at end of file
diff --git a/cmd/observer_healthcheck/main.go b/cmd/observer_healthcheck/main.go
new file mode 100644
index 0000000..b716972
--- /dev/null
+++ b/cmd/observer_healthcheck/main.go
@@ -0,0 +1,50 @@
+package main
+
+import (
+	sentrygin "github.com/getsentry/sentry-go/gin"
+	"github.com/gin-gonic/gin"
+	"github.com/spf13/viper"
+	"github.com/trustwallet/blockatlas/api"
+	"github.com/trustwallet/blockatlas/internal"
+	"github.com/trustwallet/blockatlas/pkg/logger"
+	"github.com/trustwallet/blockatlas/platform"
+	"github.com/trustwallet/blockatlas/services/observer/healthcheck"
+	"github.com/trustwallet/blockatlas/storage"
+	"time"
+)
+
+const (
+	defaultPort       = "3333"
+	defaultConfigPath = "../../config.yml"
+)
+
+var (
+	port, confPath string
+	engine         *gin.Engine
+	cache          *storage.Storage
+)
+
+func init() {
+	port, confPath = internal.ParseArgs(defaultPort, defaultConfigPath)
+
+	internal.InitConfig(confPath)
+	redisHost := viper.GetString("storage.redis")
+	cache = internal.InitRedis(redisHost)
+	logger.InitLogger()
+	tmp := sentrygin.New(sentrygin.Options{})
+	sg := &tmp
+
+	engine = internal.InitEngine(sg, viper.GetString("gin.mode"))
+
+	platform.Init(viper.GetString("platform"))
+}
+
+func main() {
+	for _, blockAPI := range platform.BlockAPIs {
+		go healthcheck.Worker(cache, blockAPI)
+		time.Sleep(time.Millisecond * 200)
+	}
+
+	api.SetupHealthCheckApi(engine)
+	internal.SetupGracefulShutdown(port, engine)
+}
diff --git a/cmd/observer_notifier/main.go b/cmd/observer_notifier/main.go
new file mode 100644
index 0000000..d3032ea
--- /dev/null
+++ b/cmd/observer_notifier/main.go
@@ -0,0 +1,56 @@
+package main
+
+import (
+	"context"
+	"github.com/spf13/viper"
+	"github.com/trustwallet/blockatlas/internal"
+	"github.com/trustwallet/blockatlas/mq"
+	"github.com/trustwallet/blockatlas/pkg/logger"
+	"github.com/trustwallet/blockatlas/services/observer/notifier"
+	"github.com/trustwallet/blockatlas/storage"
+	"time"
+)
+
+const (
+	defaultConfigPath = "../../config.yml"
+)
+
+var (
+	confPath string
+	cache    *storage.Storage
+)
+
+func init() {
+	_, confPath = internal.ParseArgs("", defaultConfigPath)
+
+	internal.InitConfig(confPath)
+	logger.InitLogger()
+
+	redisHost := viper.GetString("storage.redis")
+	mqHost := viper.GetString("observer.rabbitmq.uri")
+	prefetchCount := viper.GetInt("observer.rabbitmq.consumer.prefetch_count")
+
+	cache = internal.InitRedis(redisHost)
+	internal.InitRabbitMQ(mqHost, prefetchCount)
+
+	if err := mq.RawTransactions.Declare(); err != nil {
+		logger.Fatal(err)
+	}
+
+	if err := mq.Transactions.Declare(); err != nil {
+		logger.Fatal(err)
+	}
+
+	go storage.RestoreConnectionWorker(cache, redisHost, time.Second*10)
+	go mq.RestoreConnectionWorker(mqHost, mq.RawTransactions, time.Second*10)
+}
+
+func main() {
+	defer mq.Close()
+
+	ctx, cancel := context.WithCancel(context.Background())
+
+	go mq.RawTransactions.RunConsumerWithCancel(notifier.RunNotifier, cache, ctx)
+
+	internal.SetupGracefulShutdownForObserver(cancel)
+}
diff --git a/cmd/observer_parser/main.go b/cmd/observer_parser/main.go
new file mode 100644
index 0000000..978418d
--- /dev/null
+++ b/cmd/observer_parser/main.go
@@ -0,0 +1,142 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"github.com/spf13/viper"
+	"github.com/trustwallet/blockatlas/internal"
+	"github.com/trustwallet/blockatlas/mq"
+	"github.com/trustwallet/blockatlas/pkg/logger"
+	"github.com/trustwallet/blockatlas/platform"
+	"github.com/trustwallet/blockatlas/services/observer/notifier"
+	"github.com/trustwallet/blockatlas/services/observer/parser"
+	"github.com/trustwallet/blockatlas/storage"
+	"os"
+	"os/signal"
+	"sync"
+	"syscall"
+	"time"
+)
+
+const (
+	defaultConfigPath = "../../config.yml"
+)
+
+var (
+	confPath                              string
+	cache                                 *storage.Storage
+	backlogTime, minInterval, maxInterval time.Duration
+	maxBackLogBlocks                      int64
+	txsBatchLimit                         uint
+)
+
+func init() {
+	_, confPath = internal.ParseArgs("", defaultConfigPath)
+
+	internal.InitConfig(confPath)
+	logger.InitLogger()
+
+	redisHost := viper.GetString("storage.redis")
+	mqHost := viper.GetString("observer.rabbitmq.uri")
+	prefetchCount := viper.GetInt("observer.rabbitmq.consumer.prefetch_count")
+	platformHandle := viper.GetString("platform")
+
+	cache = internal.InitRedis(redisHost)
+	internal.InitRabbitMQ(mqHost, prefetchCount)
+	platform.Init(platformHandle)
+
+	if err := mq.RawTransactions.Declare(); err != nil {
+		logger.Fatal(err)
+	}
+
+	if len(platform.BlockAPIs) == 0 {
+		logger.Fatal("No APIs to observe")
+	}
+	txsBatchLimit = viper.GetUint("observer.txs_batch_limit")
+	backlogTime = viper.GetDuration("observer.backlog")
+	minInterval = viper.GetDuration("observer.block_poll.min")
+	maxInterval = viper.GetDuration("observer.block_poll.max")
+	maxBackLogBlocks = viper.GetInt64("observer.backlog_max_blocks")
+	if minInterval >= maxInterval {
+		logger.Fatal("minimum block polling interval cannot be greater or equal than maximum")
+	}
+
+	go mq.FatalWorker(time.Second * 10)
+	time.Sleep(time.Millisecond)
+	go storage.RestoreConnectionWorker(cache, redisHost, time.Second*10)
+	time.Sleep(time.Millisecond)
+}
+
+func main() {
+	defer mq.Close()
+	var (
+		wg          sync.WaitGroup
+		coinCancel  = make(map[string]context.CancelFunc)
+		stopChannel = make(chan<- struct{}, len(platform.BlockAPIs))
+	)
+
+	wg.Add(len(platform.BlockAPIs))
+	for _, api := range platform.BlockAPIs {
+		coin := api.Coin()
+		pollInterval := notifier.GetInterval(coin.BlockTime, minInterval, maxInterval)
+
+		var backlogCount int
+		if coin.BlockTime == 0 {
+			backlogCount = 50
+			logger.Warn("Unknown block time", logger.Params{"coin": coin.Handle})
+		} else {
+			backlogCount = int(backlogTime / pollInterval)
+		}
+
+		// do not allow
+		if txsBatchLimit < parser.MinTxsBatchLimit {
+			txsBatchLimit = parser.MinTxsBatchLimit
+		}
+
+		ctx, cancel := context.WithCancel(context.Background())
+
+		coinCancel[coin.Handle] = cancel
+
+		params := parser.Params{
+			Ctx:                   ctx,
+			Api:                   api,
+			Storage:               cache,
+			Queue:                 mq.RawTransactions,
+			ParsingBlocksInterval: pollInterval,
+			BacklogCount:          backlogCount,
+			MaxBacklogBlocks:      maxBackLogBlocks,
+			StopChannel:           stopChannel,
+			TxBatchLimit:          txsBatchLimit,
+		}
+
+		go parser.RunParser(params)
+
+		logger.Info("Parser params", logger.Params{
+			"interval":        pollInterval,
+			"backlog":         backlogCount,
+			"Max backlog":     maxBackLogBlocks,
+			"Txs Batch limit": txsBatchLimit,
+		})
+
+		wg.Done()
+	}
+
+	wg.Wait()
+
+	quit := make(chan os.Signal, 1)
+	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
+	<-quit
+	logger.Info("Shutdown parser ...")
+	for coin, cancel := range coinCancel {
+		logger.Info(fmt.Sprintf("Starting to stop %s parser...", coin))
+		cancel()
+	}
+	for {
+		if len(stopChannel) == len(platform.BlockAPIs) {
+			logger.Info("All parsers are stopped")
+			break
+		}
+	}
+
+	logger.Info("Exiting gracefully")
+}
diff --git a/cmd/observer_subscriber/main.go b/cmd/observer_subscriber/main.go
index f383026..3a16999 100644
--- a/cmd/observer_subscriber/main.go
+++ b/cmd/observer_subscriber/main.go
@@ -1,12 +1,13 @@
 package main
 
 import (
+	"context"
 	"github.com/spf13/viper"
 	_ "github.com/trustwallet/blockatlas/docs"
 	"github.com/trustwallet/blockatlas/internal"
 	"github.com/trustwallet/blockatlas/mq"
 	"github.com/trustwallet/blockatlas/pkg/logger"
-	"github.com/trustwallet/blockatlas/services/subscription"
+	"github.com/trustwallet/blockatlas/services/observer/subscriber"
 	"github.com/trustwallet/blockatlas/storage"
 	"time"
 )
@@ -43,6 +44,9 @@ func main() {
 	if err := mq.Subscriptions.Declare(); err != nil {
 		logger.Fatal(err)
 	}
-	mq.Subscriptions.RunConsumer(subscription.Consume, cache)
-	<-make(chan struct{})
+	ctx, cancel := context.WithCancel(context.Background())
+
+	go mq.Subscriptions.RunConsumerWithCancel(subscriber.RunSubscriber, cache, ctx)
+
+	internal.SetupGracefulShutdownForObserver(cancel)
 }
diff --git a/cmd/observer_worker/main.go b/cmd/observer_worker/main.go
deleted file mode 100644
index 6521174..0000000
--- a/cmd/observer_worker/main.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package main
-
-import (
-	"context"
-	"github.com/spf13/viper"
-	"github.com/trustwallet/blockatlas/internal"
-	"github.com/trustwallet/blockatlas/mq"
-	"github.com/trustwallet/blockatlas/observer"
-	"github.com/trustwallet/blockatlas/pkg/logger"
-	"github.com/trustwallet/blockatlas/platform"
-	"github.com/trustwallet/blockatlas/storage"
-	"sync"
-	"time"
-)
-
-const (
-	defaultConfigPath = "../../config.yml"
-)
-
-var (
-	confPath string
-	cache    *storage.Storage
-)
-
-func init() {
-	_, confPath = internal.ParseArgs("", defaultConfigPath)
-
-	internal.InitConfig(confPath)
-	logger.InitLogger()
-
-	redisHost := viper.GetString("storage.redis")
-	mqHost := viper.GetString("observer.rabbitmq.uri")
-	prefetchCount := viper.GetInt("observer.rabbitmq.consumer.prefetch_count")
-	platformHandle := viper.GetString("platform")
-
-	cache = internal.InitRedis(redisHost)
-	internal.InitRabbitMQ(mqHost, prefetchCount)
-	platform.Init(platformHandle)
-
-	go mq.RestoreConnectionWorker(mqHost, mq.Transactions, time.Second*10)
-	go storage.RestoreConnectionWorker(cache, redisHost, time.Second*10)
-}
-
-func main() {
-	defer mq.Close()
-	if err := mq.Transactions.Declare(); err != nil {
-		logger.Fatal(err)
-	}
-
-	if len(platform.BlockAPIs) == 0 {
-		logger.Fatal("No APIs to observe")
-	}
-
-	backlogTime := viper.GetDuration("observer.backlog")
-	minInterval := viper.GetDuration("observer.block_poll.min")
-	maxInterval := viper.GetDuration("observer.block_poll.max")
-
-	if minInterval >= maxInterval {
-		logger.Fatal("minimum block polling interval cannot be greater or equal than maximum")
-	}
-
-	var wg sync.WaitGroup
-	wg.Add(len(platform.BlockAPIs))
-
-	for _, api := range platform.BlockAPIs {
-		coin := api.Coin()
-		pollInterval := observer.GetInterval(coin.BlockTime, minInterval, maxInterval)
-
-		// Stream incoming blocks
-		var backlogCount int
-		if coin.BlockTime == 0 {
-			backlogCount = 50
-			logger.Warn("Unknown block time", logger.Params{"coin": coin.ID})
-		} else {
-			backlogCount = int(backlogTime / pollInterval)
-		}
-
-		stream := observer.Stream{
-			BlockAPI:     api,
-			Tracker:      cache,
-			PollInterval: pollInterval,
-			BacklogCount: backlogCount,
-		}
-		blocks := stream.Execute(context.Background())
-
-		// Check for transaction events
-		obs := observer.Observer{
-			Storage: cache,
-			Coin:    coin.ID,
-		}
-		events := obs.Execute(blocks)
-
-		// Dispatch events
-		var dispatcher observer.Dispatcher
-		go func() {
-			dispatcher.Run(events)
-			wg.Done()
-		}()
-
-		logger.Info("Observing", logger.Params{
-			"coin":     coin,
-			"interval": pollInterval,
-			"backlog":  backlogCount,
-		})
-	}
-
-	wg.Wait()
-	logger.Info("Exiting cleanly")
-}
diff --git a/coin/coins.go b/coin/coins.go
index 53613fd..6fa1f08 100644
--- a/coin/coins.go
+++ b/coin/coins.go
@@ -1,6 +1,6 @@
 // Code generated by go generate; DO NOT EDIT.
 // This file was generated by robots at
-// 2020-03-15 10:51:30.879337 &#43;0800 CST m=&#43;0.001247730
+// 2020-03-19 12:32:14.57052 -0600 MDT m=&#43;0.001450289
 // using data from coins.yml
 package coin
 
@@ -71,6 +71,7 @@ const (
 	DGB = 20
 	ONE = 1023
 	KSM = 434
+	SOL = 501
 	NEAR = 397
 )
 
@@ -535,6 +536,16 @@ var Coins = map[uint]Coin{
 		MinConfirmations: 0,
 		SampleAddr:       "HewiDTQv92L2bVtkziZC8ASxrFUxr6ajQ62RXAnwQ8FDVmg",
 	},
+	SOL: {
+		ID:               501,
+		Handle:           "solana",
+		Symbol:           "SOL",
+		Name:             "Solana",
+		Decimals:         9,
+		BlockTime:        500,
+		MinConfirmations: 0,
+		SampleAddr:       "boot1Z6jb15CLqpaMTn2CxktktwZpRAVAgHZEW6SxQ7",
+	},
 	NEAR: {
 		ID:               397,
 		Handle:           "near",
@@ -684,6 +695,9 @@ func Harmony() Coin {
 func Kusama() Coin {
 	return Coins[KSM]
 }
+func Solana() Coin {
+	return Coins[SOL]
+}
 func Near() Coin {
 	return Coins[NEAR]
 }
diff --git a/coin/coins.yml b/coin/coins.yml
index df23ee9..92a442f 100644
--- a/coin/coins.yml
+++ b/coin/coins.yml
@@ -364,6 +364,14 @@
   blockTime: 6000
   sampleAddress: 'HewiDTQv92L2bVtkziZC8ASxrFUxr6ajQ62RXAnwQ8FDVmg'
 
+- id: 501
+  symbol: SOL
+  handle: solana
+  name: Solana
+  decimals: 9
+  blockTime: 500
+  sampleAddress: 'boot1Z6jb15CLqpaMTn2CxktktwZpRAVAgHZEW6SxQ7'
+
 - id: 397
   symbol: NEAR
   handle: near
diff --git a/config.yml b/config.yml
index 462139f..36d6b99 100644
--- a/config.yml
+++ b/config.yml
@@ -17,19 +17,18 @@ platform: all
 
 # The transaction watcher
 observer:
-  auth: test
   # Don't request blocks older than this
   backlog: 3h
   # Don't request more than N blocks at once
   backlog_max_blocks: 200
-  # Max connections to open to API
-  stream_conns: 16
+  # Limit amount of transactions in batch
+  txs_batch_limit: 3000
   # Block polling interval
   block_poll:
     min: 3s
     max: 30s
   rabbitmq:
-    uri: amqp://rabbit:5672
+    uri: amqp://localhost:5672
     consumer:
       prefetch_count: 10
 
@@ -212,5 +211,8 @@ kava:
 kusama:
   api: https://kusama.subscan.io/api
 
+solana:
+  api: https://api.mainnet-beta.solana.com
+
 near:
   api: https://staging-rpc.nearprotocol.com
diff --git a/docker-compose.yml b/docker-compose.yml
index 7601470..c7513dc 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -33,7 +33,7 @@ services:
     build:
       context: .
       args:
-        - SERVICE=observer_worker
+        - SERVICE=observer_notifier
     links:
       - redis
       - rabbit
diff --git a/go.mod b/go.mod
index b5168b5..5644f85 100644
--- a/go.mod
+++ b/go.mod
@@ -8,9 +8,11 @@ require (
 	github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
 	github.com/Pantani/httpexpect v2.0.0+incompatible
 	github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
+	github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 // indirect
+	github.com/alicebob/miniredis v2.5.0+incompatible
+	github.com/aristanetworks/goarista v0.0.0-20200310212843-2da4c1f5881b // indirect
 	github.com/btcsuite/btcutil v1.0.1
-	github.com/cenkalti/backoff v2.2.1+incompatible
-	github.com/chenjiandongx/ginprom v0.0.0-20191227144730-e11ebf56bc05
+	github.com/cenkalti/backoff v2.2.1+incompatible // indirect
 	github.com/containerd/continuity v0.0.0-20200228182428-0f16d7a0959c // indirect
 	github.com/deckarep/golang-set v1.7.1
 	github.com/docker/go-connections v0.4.0 // indirect
@@ -19,29 +21,30 @@ require (
 	github.com/getsentry/sentry-go v0.4.0
 	github.com/gin-gonic/gin v1.5.0
 	github.com/go-redis/redis v6.15.6+incompatible
+	github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect
 	github.com/hewigovens/go-coincodec v1.0.4
-	github.com/jinzhu/gorm v1.9.12
 	github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
+	github.com/lib/pq v1.1.1 // indirect
 	github.com/mitchellh/mapstructure v1.1.2
 	github.com/mr-tron/base58 v1.1.3
 	github.com/opencontainers/image-spec v1.0.1 // indirect
 	github.com/opencontainers/runc v0.1.1 // indirect
 	github.com/ory/dockertest v3.3.5+incompatible
 	github.com/patrickmn/go-cache v2.1.0+incompatible
-	github.com/pkg/errors v0.9.1 // indirect
-	github.com/prometheus/client_golang v1.4.0
-	github.com/robfig/cron/v3 v3.0.1
 	github.com/shopspring/decimal v0.0.0-20200105231215-408a2507e114
 	github.com/sirupsen/logrus v1.4.2
-	github.com/spf13/cast v1.3.1
+	github.com/spf13/cast v1.3.1 // indirect
 	github.com/spf13/viper v1.6.2
 	github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71
 	github.com/stretchr/testify v1.4.0
 	github.com/swaggo/gin-swagger v1.2.0
 	github.com/swaggo/swag v1.6.5
 	github.com/wealdtech/go-ens/v3 v3.2.0
-	golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d
+	github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb // indirect
+	go.uber.org/atomic v1.4.0
+	golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d
 	golang.org/x/net v0.0.0-20200301022130-244492dfa37a // indirect
+	golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
 	golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 // indirect
 	gopkg.in/yaml.v2 v2.2.8
 )
diff --git a/go.sum b/go.sum
index 849be02..20543a1 100644
--- a/go.sum
+++ b/go.sum
@@ -16,6 +16,7 @@ github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxB
 github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
 github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
 github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw=
 github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w=
@@ -37,6 +38,8 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
+github.com/Shopify/sarama v1.26.1/go.mod h1:NbSGBSSndYaIhRcBtY9V0U7AyH+x71bG668AuWys/yU=
+github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
 github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
 github.com/VictoriaMetrics/fastcache v1.5.3 h1:2odJnXLbFZcoV9KYtQ+7TH1UOq3dn3AssMgieaezkR4=
 github.com/VictoriaMetrics/fastcache v1.5.3/go.mod h1:+jv9Ckb+za/P1ZRg/sulP5Ni1v49daAVERr0H3CuscE=
@@ -48,19 +51,27 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 h1:45bxf7AZMwWcqkLzDAQugVEwedisr5nRJ1r+7LYnv0U=
+github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
+github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI=
+github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
 github.com/allegro/bigcache v1.2.0/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
 github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
 github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
 github.com/apilayer/freegeoip v3.5.0+incompatible/go.mod h1:CUfFqErhFhXneJendyQ/rRcuA8kH8JxHvYnbOozmlCU=
+github.com/aristanetworks/fsnotify v1.4.2/go.mod h1:D/rtu7LpjYM8tRJphJ0hUBYpjai8SfX+aSNsWDTq/Ks=
+github.com/aristanetworks/glog v0.0.0-20191112221043-67e8567f59f3/go.mod h1:KASm+qXFKs/xjSoWn30NrWBBvdTTQq+UjkhjEJHfSFA=
 github.com/aristanetworks/goarista v0.0.0-20170210015632-ea17b1a17847/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ=
 github.com/aristanetworks/goarista v0.0.0-20190219163901-728bce664cf5 h1:L0TwgZQo7Mga9im6FvKEZGIvyLE/VG/HI5loz5LpvC0=
 github.com/aristanetworks/goarista v0.0.0-20190219163901-728bce664cf5/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ=
+github.com/aristanetworks/goarista v0.0.0-20200310212843-2da4c1f5881b h1:VBFuX8nQQ57A6OGYGOLugx/Sc488F0nNIdTtcmNq9qE=
+github.com/aristanetworks/goarista v0.0.0-20200310212843-2da4c1f5881b/go.mod h1:QZe5Yh80Hp1b6JxQdpfSEEe8X7hTyTEZSosSrFf/oJE=
+github.com/aristanetworks/splunk-hec-go v0.3.3/go.mod h1:1VHO9r17b0K7WmOlLb9nTk/2YanvOEnLMUgsFrxBROc=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/aws/aws-sdk-go v1.25.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
-github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/btcsuite/btcd v0.0.0-20171128150713-2e60448ffcc6/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ=
 github.com/btcsuite/btcd v0.0.0-20190418232430-6867ff32788a/go.mod h1:DrZx5ec/dmnfpw9KyYoQyYo7d0KEvTkk/5M/vbZjAr8=
@@ -79,15 +90,18 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtE
 github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
 github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
 github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
+github.com/cespare/cp v1.1.1 h1:nCb6ZLdB7NRaqsm91JtQTAme2SKJzXVsdPIPkyJr1MU=
 github.com/cespare/cp v1.1.1/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
 github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.0.1-0.20190104013014-3767db7a7e18/go.mod h1:HD5P3vAIAh+Y2GAxg0PrPN1P8WkepXGpjbUPDHJqqKM=
 github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/chenjiandongx/ginprom v0.0.0-20191227144730-e11ebf56bc05 h1:2qhpRcnS61pafr2CbHYBr9ZWN7I8txgEOkBUm8Z7NH4=
-github.com/chenjiandongx/ginprom v0.0.0-20191227144730-e11ebf56bc05/go.mod h1:lINNCb1ZH3c0uL/9ApaQ8muR4QILsi0STj8Ojt8ZmwU=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cloudflare/cloudflare-go v0.10.2-0.20190916151808-a80f83b9add9/go.mod h1:1MxXX1Ux4x6mqPmjkUgTP1CdXIBXKX7T+Jk9Gxrmx+U=
 github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
@@ -110,8 +124,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
 github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ=
 github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
-github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
-github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
 github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
@@ -124,6 +136,9 @@ github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw
 github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
 github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
+github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
+github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
 github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
 github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw=
 github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
@@ -131,26 +146,33 @@ github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZi
 github.com/elastic/gosigar v0.8.1-0.20180330100440-37f05ff46ffa/go.mod h1:cdorVVzy1fhmEqmtgqkoE3bYtCfSCkVyjTyCIo22xvs=
 github.com/elastic/gosigar v0.10.4 h1:6jfw75dsoflhBMRdO6QPzQUgLqUYTsQQQRkkcsHsuPo=
 github.com/elastic/gosigar v0.10.4/go.mod h1:cdorVVzy1fhmEqmtgqkoE3bYtCfSCkVyjTyCIo22xvs=
-github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
 github.com/ethereum/go-ethereum v1.9.2/go.mod h1:PwpWDrCLZrV+tfrhqqF6kPknbISMHaJv9Ln3kPCZLwY=
 github.com/ethereum/go-ethereum v1.9.10 h1:jooX7tWcscpC7ytufk73t9JMCeJQ7aJF2YmZJQEuvFo=
 github.com/ethereum/go-ethereum v1.9.10/go.mod h1:lXHkVo/MTvsEXfYsmNzelZ8R1e0DTvdk/wMZJIRpaRw=
+github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 h1:DddqAaWDpywytcG8w/qoQ5sAN8X12d3Z3koB0C3Rxsc=
 github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
 github.com/fatih/color v1.3.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
 github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
 github.com/fjl/memsize v0.0.0-20180418122429-ca190fb6ffbc/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0=
+github.com/fjl/memsize v0.0.0-20180929194037-2a09253e352a h1:1znxn4+q2MrEdTk1eCk6KIV3muTYVclBIB6CTVR/zBc=
 github.com/fjl/memsize v0.0.0-20180929194037-2a09253e352a/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0=
 github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA=
+github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
+github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
 github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
 github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI=
 github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
 github.com/getsentry/sentry-go v0.4.0 h1:WqRI2/7EiALbdG9qGB47c0Aks1tdznG5DZd6GSQ1y/8=
 github.com/getsentry/sentry-go v0.4.0/go.mod h1:xkGcb82SipKQloDNa5b7hTV4VdEyc2bhwd1/UczP52k=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gin-contrib/gzip v0.0.1 h1:ezvKOL6jH+jlzdHNE4h9h8q8uMpDQjyl0NN0Jd7jozc=
 github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w=
 github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
 github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
@@ -161,10 +183,13 @@ github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/
 github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc=
 github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
 github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
+github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
 github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
@@ -190,7 +215,6 @@ github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rm
 github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
 github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be8FqJQRhdQZ5Sg=
 github.com/go-redis/redis v6.15.6+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
-github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
@@ -198,8 +222,6 @@ github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
 github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
-github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
-github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -208,9 +230,12 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
 github.com/golang/protobuf v1.3.2-0.20190517061210-b285ee9cfc6c/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38 h1:y0Wmhvml7cGnzPa9nocn/fMraMH/lMDdeG+rkx4VgYY=
 github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -228,11 +253,14 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA
 github.com/gorilla/websocket v1.4.1-0.20190629185528-ae1634f6a989/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
 github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQZOmDhRqsv5mZf+Jb1RnSLxcqZcI=
+github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
 github.com/graph-gophers/graphql-go v0.0.0-20190724201507-010347b5f9e6/go.mod h1:Au3iQ8DvDis8hZ4q2OzRcaKYlAsPt+fYvib5q4nIqu4=
 github.com/graph-gophers/graphql-go v0.0.0-20191115155744-f33e81362277/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
 github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
@@ -242,6 +270,7 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
 github.com/hewigovens/go-coincodec v1.0.4 h1:asF02q8WfDwVQjU1O0VZYbOBer0cPJUmTiZKfAgqBYM=
 github.com/hewigovens/go-coincodec v1.0.4/go.mod h1:+LTdzScnu782gMt0J3ZccPY9S2uyrGe0R1LUYGWecfc=
 github.com/howeyc/fsnotify v0.9.0/go.mod h1:41HzSPxBGeFRQKEEwgh49TRw/nKBsYZ2cF1OzPjSJsA=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/huin/goupnp v0.0.0-20161224104101-679507af18f3/go.mod h1:MZ2ZmwcBpvOoJ22IJsc7va19ZwoheaBk43rKg12SKag=
 github.com/huin/goupnp v1.0.0 h1:wg75sLpL6DZqwHQN6E1Cfk6mtfzS45z8OV+ic+DtHRo=
@@ -252,6 +281,7 @@ github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/C
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/influxdata/influxdb v1.2.3-0.20180221223340-01288bdb0883/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
 github.com/influxdata/influxdb v1.7.7/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
+github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
 github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
 github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
 github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI=
@@ -259,12 +289,8 @@ github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrO
 github.com/jackpal/go-nat-pmp v1.0.1/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
 github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458 h1:6OvNmYgJyexcZ3pYbTI9jWx5tHo1Dee/tWbLMfPe2TA=
 github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
+github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
 github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
-github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=
-github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
-github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
-github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
-github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
@@ -273,12 +299,14 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
 github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
 github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
 github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
 github.com/julienschmidt/httprouter v1.1.1-0.20170430222011-975b5c4c7c21/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
 github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
 github.com/karalabe/usb v0.0.0-20190819132248-550797b1cad8/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU=
 github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356 h1:I/yrLt2WilKxlQKCM52clh5rGzTKpVctGT1lH4Dc8Jw=
@@ -293,15 +321,23 @@ github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6
 github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/compress v1.9.0 h1:GhthINjveNZAdFUD8QoQYfjxnOONZgztK/Yr6M23UTY=
 github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/compress v1.10.1/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/klauspost/reedsolomon v1.9.3/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
@@ -319,6 +355,7 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq
 github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
+github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
 github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
 github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
@@ -332,8 +369,6 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y
 github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
 github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
-github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw=
-github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@@ -348,8 +383,10 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
 github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
 github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
@@ -372,10 +409,14 @@ github.com/olekukonko/tablewriter v0.0.2-0.20190409134802-7e037d187b0c/go.mod h1
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
 github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/openconfig/gnmi v0.0.0-20190823184014-89b2bf29312c/go.mod h1:t+O9It+LKzfOAhKTT5O0ehDix+MTqbtT0T9t+7zzOvc=
+github.com/openconfig/reference v0.0.0-20190727015836-8dfd928c9696/go.mod h1:ym2A+zigScwkSEb/cVQB0/ZMpU3rqiH6X7WRRsxgOGw=
 github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
 github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
 github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
@@ -394,6 +435,8 @@ github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP
 github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
+github.com/pierrec/lz4 v2.4.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
+github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
 github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -406,32 +449,29 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.4.0 h1:YVIb/fVcOTMSqtqZWSKnHpSLBxu8DKgxq8z6RuBZwqI=
-github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
+github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=
 github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
+github.com/prometheus/procfs v0.0.10/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
 github.com/prometheus/tsdb v0.6.2-0.20190402121629-4f204dcbc150/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/prometheus/tsdb v0.10.0 h1:If5rVCMTp6W2SiRAQFlbpJNgVlgMEd+U2GZckwK38ic=
 github.com/prometheus/tsdb v0.10.0/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4=
+github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho=
 github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8=
 github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM=
 github.com/robertkrimen/otto v0.0.0-20170205013659-6a77b7cbc37d/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
-github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
-github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
 github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI=
@@ -453,6 +493,7 @@ github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
@@ -501,6 +542,9 @@ github.com/swaggo/swag v1.6.5/go.mod h1:Y7ZLSS0d0DdxhWGVhQdu+Bu1QhaF5k0RD7FKdiAy
 github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
 github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d h1:gZZadD8H+fF+n9CmNhYL1Y0dJB+kLOmKd7FbPJLeGHs=
 github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA=
+github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU=
+github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4=
+github.com/tjfoc/gmsm v1.3.0/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs=
 github.com/tyler-smith/go-bip39 v1.0.2 h1:+t3w+KwLXO6154GNJY+qUtIxLTmFjfUmpguQT1OlOT8=
@@ -524,16 +568,18 @@ github.com/valyala/fasthttp v1.6.0 h1:uWF8lgKmeaIewWVPwi4GRq2P6+R46IgYZdxWtM+GtE
 github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
 github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
 github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
-github.com/wealdtech/go-ens/v3 v3.1.0/go.mod h1:P2OEBvgkhXLrPzPN+eR5z2/wFIGwHyijTDvpuC1xLlo=
 github.com/wealdtech/go-ens/v3 v3.2.0 h1:bshYk63BUJaIriX3UoAcYKh1tVI85kI1Xbg3jAnM/NM=
 github.com/wealdtech/go-ens/v3 v3.2.0/go.mod h1:P2OEBvgkhXLrPzPN+eR5z2/wFIGwHyijTDvpuC1xLlo=
 github.com/wealdtech/go-multicodec v1.2.0 h1:9AHSxcSE9F9r6ZvQLAO0EXCdM08QfYohaXmW3k6sSh4=
 github.com/wealdtech/go-multicodec v1.2.0/go.mod h1:aedGMaTeYkIqi/KCPre1ho5rTb3hGpu/snBOS3GQLw4=
 github.com/wealdtech/go-slip44 v1.0.0 h1:g0Wi5EufdD/ULZ4hcZKQ3TGlA6FZhGTCkmVzYmf+7Is=
 github.com/wealdtech/go-slip44 v1.0.0/go.mod h1://S3V9M6iN1cCflLXMjJYciRPakCDubG9YpWVyW1AvM=
+github.com/wealdtech/go-string2eth v1.0.0 h1:jY6b1MVqU6k2Uw/kvcU1Y9/3dDyXfPzZrOFspt82UJs=
 github.com/wealdtech/go-string2eth v1.0.0/go.mod h1:UZA/snEybGcD6n+Pl+yoDjmexlEJ6dtoS9myfM83Ol4=
 github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208 h1:1cngl9mPEoITZG8s8cVcUy5CeIBYhEESkOB7m6Gmkrk=
 github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208/go.mod h1:IotVbo4F+mw0EzQ08zFqg7pK3FebNXpaMsRy2RT+Ees=
+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=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
@@ -542,14 +588,20 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17
 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/xtaci/kcp-go v5.4.20+incompatible/go.mod h1:bN6vIwHQbfHaHtFpEssmWsN45a+AZwO7eyRCmEIbtvE=
+github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE=
 github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
 github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
 github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
 github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
 github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
 github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
+github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
 github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
+github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0=
+github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
@@ -558,17 +610,23 @@ golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnf
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d h1:9FCpayM9Egr1baVnV1SX0H87m+XB0B8S0hAMi99X/3U=
 golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=
+golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -576,6 +634,7 @@ golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -587,6 +646,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
 golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -594,6 +655,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -603,6 +665,7 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -613,54 +676,77 @@ golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59 h1:QjA/9ArTfVTLfEhClDCG7SGrZkZixxWpwNCDiwJfh88=
 golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd h1:hHkvGJK23seRCflePJnVa9IMv8fsuavSCWKd11kDQFs=
+golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200218151345-dad8c97a84f5/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/bsm/ratelimit.v1 v1.0.0-20160220154919-db14e161995a/go.mod h1:KF9sEfUPAXdG8Oev9e99iLGnl2uJMjc5B+4y3O7x610=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
+gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
 gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
 gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
 gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
 gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
 gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
 gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo=
+gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q=
+gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4=
+gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=
+gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8=
 gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
+gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
 gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
 gopkg.in/olebedev/go-duktape.v3 v3.0.0-20190213234257-ec84240a7772/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns=
 gopkg.in/olebedev/go-duktape.v3 v3.0.0-20190709231704-1e4459ed25ff h1:uuol9OUzSvZntY1v963NAbVd7A+PHLMz1FlCe3Lorcs=
 gopkg.in/olebedev/go-duktape.v3 v3.0.0-20190709231704-1e4459ed25ff/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns=
+gopkg.in/redis.v4 v4.2.4/go.mod h1:8KREHdypkCEojGKQcjMqAODMICIVwZAONWq8RowTITA=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0=
 gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0=
 gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -669,5 +755,7 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
 gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/internal/shutdown.go b/internal/shutdown.go
index 37f9586..234766d 100644
--- a/internal/shutdown.go
+++ b/internal/shutdown.go
@@ -43,3 +43,13 @@ func SetupGracefulShutdown(port string, engine *gin.Engine) {
 	logger.Info("Stop signal Received", stop)
 	logger.Info("Waiting for all jobs to stop")
 }
+
+func SetupGracefulShutdownForObserver(cancel context.CancelFunc) {
+	quit := make(chan os.Signal, 1)
+	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
+	<-quit
+	cancel()
+	logger.Info("Shutdown ...")
+	time.Sleep(time.Second * 5)
+	logger.Info("Exiting  gracefully")
+}
diff --git a/mq/mq.go b/mq/mq.go
index 5804b8c..1ad87c0 100644
--- a/mq/mq.go
+++ b/mq/mq.go
@@ -1,6 +1,7 @@
 package mq
 
 import (
+	"context"
 	"github.com/streadway/amqp"
 	"github.com/trustwallet/blockatlas/pkg/logger"
 	"github.com/trustwallet/blockatlas/storage"
@@ -14,13 +15,15 @@ var (
 )
 
 type (
-	Queue    string
-	Consumer func(amqp.Delivery, storage.Addresses)
+	Queue          string
+	Consumer       func(amqp.Delivery, storage.Addresses)
+	MessageChannel <-chan amqp.Delivery
 )
 
 const (
 	Transactions         Queue = "transactions"
 	Subscriptions        Queue = "subscriptions"
+	RawTransactions      Queue = "rawTransactions"
 	defaultPrefetchCount       = 5
 	minPrefetchCount           = 1
 )
@@ -55,6 +58,21 @@ func (q Queue) Publish(body []byte) error {
 	})
 }
 
+func (q Queue) RunConsumerForChannelWithCancel(consumer Consumer, messageChannel MessageChannel, cache storage.Addresses, ctx context.Context) {
+	for {
+		select {
+		case <-ctx.Done():
+			logger.Info("Consumer stopped")
+			return
+		case message := <-messageChannel:
+			if message.Body == nil {
+				continue
+			}
+			go consumer(message, cache)
+		}
+	}
+}
+
 func (q Queue) RunConsumer(consumer Consumer, cache storage.Addresses) {
 	messageChannel, err := amqpChan.Consume(
 		string(q),
@@ -90,6 +108,71 @@ func (q Queue) RunConsumer(consumer Consumer, cache storage.Addresses) {
 	}
 }
 
+func (q Queue) RunConsumerWithCancel(consumer Consumer, cache storage.Addresses, ctx context.Context) {
+	messageChannel, err := amqpChan.Consume(
+		string(q),
+		"",
+		false,
+		false,
+		false,
+		false,
+		nil,
+	)
+	if err != nil {
+		logger.Fatal("MQ issue " + err.Error())
+		return
+	}
+
+	if PrefetchCount < minPrefetchCount {
+		logger.Info("Change prefetch count to default")
+		PrefetchCount = defaultPrefetchCount
+	}
+
+	err = amqpChan.Qos(
+		PrefetchCount,
+		0,
+		true,
+	)
+
+	if err != nil {
+		logger.Error("no qos limit ", err)
+	}
+
+	for {
+		select {
+		case <-ctx.Done():
+			logger.Info("Consumer stopped")
+			return
+		case message := <-messageChannel:
+			if message.Body == nil {
+				continue
+			}
+			go consumer(message, cache)
+		}
+	}
+}
+
+func (q Queue) GetMessageChannel() MessageChannel {
+	messageChannel, err := amqpChan.Consume(
+		string(q),
+		"",
+		false,
+		false,
+		false,
+		false,
+		nil,
+	)
+	if err != nil {
+		logger.Fatal("MQ issue " + err.Error())
+	}
+
+	return messageChannel
+}
+
+func (mc MessageChannel) GetMessage() amqp.Delivery {
+	return <-mc
+}
+
 func RestoreConnectionWorker(uri string, queue Queue, timeout time.Duration) {
 	logger.Info("Run MQ RestoreConnectionWorker")
 	for {
diff --git a/observer/dispatcher.go b/observer/dispatcher.go
deleted file mode 100644
index 4301249..0000000
--- a/observer/dispatcher.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package observer
-
-import (
-	"encoding/json"
-	"github.com/trustwallet/blockatlas/mq"
-	"github.com/trustwallet/blockatlas/pkg/blockatlas"
-	"github.com/trustwallet/blockatlas/pkg/errors"
-	"github.com/trustwallet/blockatlas/pkg/logger"
-	"net/http"
-)
-
-type Dispatcher struct {
-	Client http.Client
-}
-
-type DispatchEvent struct {
-	Action blockatlas.TransactionType `json:"action"`
-	Result *blockatlas.Tx             `json:"result"`
-	GUID   string                     `json:"guid"`
-}
-
-func (d *Dispatcher) Run(events <-chan Event) {
-	for event := range events {
-		d.dispatch(event)
-	}
-}
-
-func (d *Dispatcher) dispatch(event Event) {
-	guid := event.Subscription.GUID
-
-	action := DispatchEvent{
-		Action: event.Tx.Type,
-		Result: event.Tx,
-		GUID:   guid,
-	}
-
-	txJson, err := json.Marshal(action)
-	if err != nil {
-		logger.Panic(err)
-	}
-
-	logParams := logger.Params{
-		"guid": guid,
-		"coin": event.Subscription.Coin,
-		"txID": event.Tx.ID,
-	}
-
-	go d.postMessageToQueue(guid, txJson, logParams)
-
-	logger.Info("Dispatching messages...", logParams)
-}
-
-func (d *Dispatcher) postMessageToQueue(message string, rawMessage []byte, logParams logger.Params) {
-	err := mq.Transactions.Publish(rawMessage)
-	if err != nil {
-		err = errors.E(err, "Failed to dispatch event", errors.Params{"message": message}, logParams)
-		logger.Fatal(err, logger.Params{"message": message}, logParams)
-	}
-	logger.Info("Message dispatched", logger.Params{"message": message}, logParams)
-}
diff --git a/observer/observer.go b/observer/observer.go
deleted file mode 100644
index aaf8470..0000000
--- a/observer/observer.go
+++ /dev/null
@@ -1,128 +0,0 @@
-package observer
-
-import (
-	mapset "github.com/deckarep/golang-set"
-	"github.com/trustwallet/blockatlas/coin"
-	"github.com/trustwallet/blockatlas/pkg/blockatlas"
-	"github.com/trustwallet/blockatlas/pkg/numbers"
-	"github.com/trustwallet/blockatlas/platform/bitcoin"
-	"github.com/trustwallet/blockatlas/storage"
-	"time"
-)
-
-type Event struct {
-	Subscription blockatlas.Subscription
-	Tx           *blockatlas.Tx
-}
-
-type Observer struct {
-	Storage storage.Addresses
-	Coin    uint
-}
-
-func (o *Observer) Execute(blocks <-chan *blockatlas.Block) <-chan Event {
-	events := make(chan Event)
-	go o.run(events, blocks)
-	return events
-}
-
-func (o *Observer) run(events chan<- Event, blocks <-chan *blockatlas.Block) {
-	for block := range blocks {
-		o.processBlock(events, block)
-	}
-}
-
-func (o *Observer) processBlock(events chan<- Event, block *blockatlas.Block) {
-	txMap := GetTxs(block)
-	if len(txMap) == 0 {
-		return
-	}
-
-	// Build list of unique addresses
-	var addresses []string
-	for address := range txMap {
-		if len(address) == 0 {
-			continue
-		}
-		addresses = append(addresses, address)
-	}
-
-	// Lookup subscriptions
-	subs, err := o.Storage.Lookup(o.Coin, addresses)
-	if err != nil || len(subs) == 0 {
-		return
-	}
-	for _, sub := range subs {
-		tx, ok := txMap[sub.Address]
-		if !ok {
-			continue
-		}
-		for _, tx := range tx.Txs() {
-			tx.Direction = getDirection(tx, sub.Address)
-			inferUtxoValue(&tx, sub.Address, o.Coin)
-			events <- Event{
-				Subscription: sub,
-				Tx:           &tx,
-			}
-		}
-	}
-}
-
-func GetTxs(block *blockatlas.Block) map[string]*blockatlas.TxSet {
-	txMap := make(map[string]*blockatlas.TxSet)
-	for i := 0; i < len(block.Txs); i++ {
-		addresses := block.Txs[i].GetAddresses()
-		addresses = append(addresses, block.Txs[i].GetUtxoAddresses()...)
-		for _, address := range addresses {
-			if txMap[address] == nil {
-				txMap[address] = new(blockatlas.TxSet)
-			}
-			txMap[address].Add(&block.Txs[i])
-		}
-	}
-	return txMap
-}
-
-func getDirection(tx blockatlas.Tx, address string) blockatlas.Direction {
-	if len(tx.Inputs) > 0 && len(tx.Outputs) > 0 {
-		addressSet := mapset.NewSet(address)
-		return bitcoin.InferDirection(&tx, addressSet)
-	}
-	switch meta := tx.Meta.(type) {
-	case blockatlas.TokenTransfer:
-		return determineDirection(address, meta.From, meta.To)
-	case blockatlas.NativeTokenTransfer:
-		return determineDirection(address, meta.From, meta.To)
-	default:
-		return determineDirection(address, tx.From, tx.To)
-	}
-}
-
-func determineDirection(address, from, to string) blockatlas.Direction {
-	if address == to {
-		if from == to {
-			return blockatlas.DirectionSelf
-		}
-		return blockatlas.DirectionIncoming
-	}
-	return blockatlas.DirectionOutgoing
-}
-
-func inferUtxoValue(tx *blockatlas.Tx, address string, coinIndex uint) {
-	if len(tx.Inputs) > 0 && len(tx.Outputs) > 0 {
-		addressSet := mapset.NewSet(address)
-		value := bitcoin.InferValue(tx, tx.Direction, addressSet)
-		tx.Meta = blockatlas.Transfer{
-			Value:    value,
-			Symbol:   coin.Coins[coinIndex].Symbol,
-			Decimals: coin.Coins[coinIndex].Decimals,
-		}
-	}
-}
-
-func GetInterval(value int, minInterval, maxInterval time.Duration) time.Duration {
-	interval := time.Duration(value) * time.Millisecond
-	pMin := numbers.Max(minInterval.Nanoseconds(), interval.Nanoseconds())
-	pMax := numbers.Min(int(maxInterval.Nanoseconds()), int(pMin))
-	return time.Duration(pMax)
-}
diff --git a/observer/retry.go b/observer/retry.go
deleted file mode 100644
index 7d3b685..0000000
--- a/observer/retry.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package observer
-
-import (
-	"github.com/trustwallet/blockatlas/pkg/blockatlas"
-	"github.com/trustwallet/blockatlas/pkg/logger"
-	"math/rand"
-	"time"
-)
-
-type GetBlockByNumber func(num int64) (*blockatlas.Block, error)
-
-type stop struct {
-	error
-}
-
-func retry(attempts int, sleep time.Duration, f GetBlockByNumber, n int64) (*blockatlas.Block, error) {
-	r, err := f(n)
-	if err != nil {
-		if s, ok := err.(stop); ok {
-			return nil, s.error
-		}
-		if attempts--; attempts > 0 {
-			// Add some randomness to prevent creating a Thundering Herd
-			jitter := time.Duration(rand.Int63n(int64(sleep)))
-			sleep = sleep + jitter/2
-
-			logger.Info("GetBlockByNumber retry",
-				logger.Params{
-					"number":   n,
-					"attempts": attempts,
-					"sleep":    sleep.String(),
-				},
-			)
-
-			time.Sleep(sleep)
-			return retry(attempts, sleep*2, f, n)
-		}
-	}
-	return r, err
-}
diff --git a/observer/stream.go b/observer/stream.go
deleted file mode 100644
index b86c95a..0000000
--- a/observer/stream.go
+++ /dev/null
@@ -1,107 +0,0 @@
-package observer
-
-import (
-	"context"
-	"github.com/spf13/viper"
-	"github.com/trustwallet/blockatlas/pkg/blockatlas"
-	"github.com/trustwallet/blockatlas/pkg/logger"
-	"github.com/trustwallet/blockatlas/pkg/semaphore"
-	"github.com/trustwallet/blockatlas/storage"
-	"sync"
-	"sync/atomic"
-	"time"
-)
-
-type Stream struct {
-	BlockAPI     blockatlas.BlockAPI
-	Tracker      storage.Tracker
-	PollInterval time.Duration
-	BacklogCount int
-	coin         uint
-	logParams    logger.Params
-
-	// Concurrency
-	blockNumber int64
-	semaphore   *semaphore.Semaphore
-	wg          sync.WaitGroup
-}
-
-func (s *Stream) Execute(ctx context.Context) <-chan *blockatlas.Block {
-	cn := s.BlockAPI.Coin()
-	s.coin = cn.ID
-	s.logParams = logger.Params{"platform": cn.Handle}
-	conns := viper.GetInt("observer.stream_conns")
-	if conns == 0 {
-		logger.Fatal("observer.stream_conns is 0")
-	}
-	s.semaphore = semaphore.NewSemaphore(conns)
-	c := make(chan *blockatlas.Block)
-	go s.run(ctx, c)
-	return c
-}
-
-func (s *Stream) run(ctx context.Context, c chan<- *blockatlas.Block) {
-	ticker := time.NewTicker(s.PollInterval)
-	for {
-		select {
-		case <-ctx.Done():
-			ticker.Stop()
-			close(c)
-			return
-		case <-ticker.C:
-			s.load(c)
-		}
-	}
-}
-
-func (s *Stream) load(c chan<- *blockatlas.Block) {
-	lastHeight, err := s.Tracker.GetBlockNumber(s.coin)
-	if err != nil {
-		logger.Error(err, "Polling failed: tracker didn't return last known block number", s.logParams)
-		return
-	}
-
-	height, err := s.BlockAPI.CurrentBlockNumber()
-	height -= s.BlockAPI.Coin().MinConfirmations
-	if err != nil {
-		logger.Error(err, "Polling failed: source didn't return chain head number", s.logParams)
-		return
-	}
-
-	if height-lastHeight > int64(s.BacklogCount) {
-		lastHeight = height - int64(s.BacklogCount)
-	}
-	backLogMax := viper.GetInt64("observer.backlog_max_blocks")
-	if height-lastHeight > backLogMax {
-		lastHeight = height - backLogMax
-	}
-
-	atomic.StoreInt64(&s.blockNumber, lastHeight)
-	for i := lastHeight + 1; i <= height; i++ {
-		s.wg.Add(1)
-		go s.loadBlock(c, i)
-	}
-	s.wg.Wait()
-}
-
-func (s *Stream) loadBlock(c chan<- *blockatlas.Block, num int64) {
-	defer s.wg.Done()
-	s.semaphore.Acquire()
-	defer s.semaphore.Release()
-
-	block, err := retry(5, time.Second*5, s.BlockAPI.GetBlockByNumber, num)
-	if err != nil {
-		logger.Error(err, "Polling failed: could not get block", s.logParams, logger.Params{"block": num})
-		return
-	}
-	c <- block
-	logger.Info(err, "Got new block", s.logParams, logger.Params{"block": num, "txs": len(block.Txs)})
-
-	// Not strictly correct nor avoids race conditions
-	// But good enough
-	newNum := atomic.AddInt64(&s.blockNumber, 1)
-	err = s.Tracker.SetBlockNumber(s.coin, newNum)
-	if err != nil {
-		logger.Error(err, "SetBlockNumber failed", s.logParams, logger.Params{"block": num, "coin": s.coin})
-	}
-}
diff --git a/pkg/blockatlas/api.go b/pkg/blockatlas/api.go
deleted file mode 100644
index 1d4bfb1..0000000
--- a/pkg/blockatlas/api.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package blockatlas
-
-import (
-	"github.com/gin-gonic/gin"
-	"github.com/trustwallet/blockatlas/coin"
-)
-
-type (
-	// Platform can be used to access a crypto service
-	Platform interface {
-		Coin() coin.Coin
-	}
-
-	// TxAPI provides transaction lookups
-	TxAPI interface {
-		Platform
-		GetTxsByAddress(address string) (TxPage, error)
-	}
-
-	// TokenTxAPI provides token transaction lookups
-	TokenTxAPI interface {
-		Platform
-		GetTokenTxsByAddress(address, token string) (TxPage, error)
-	}
-
-	// TokenAPI provides token lookups
-	TokenAPI interface {
-		Platform
-		GetTokenListByAddress(address string) (TokenPage, error)
-	}
-
-	// BlockAPI provides block information and lookups
-	BlockAPI interface {
-		Platform
-		CurrentBlockNumber() (int64, error)
-		GetBlockByNumber(num int64) (*Block, error)
-	}
-
-	// AddressAPI provides address information
-	AddressAPI interface {
-		Platform
-		GetAddressesFromXpub(xpub string) ([]string, error)
-	}
-
-	// StakingAPI provides staking information
-	StakeAPI interface {
-		Platform
-		UndelegatedBalance(address string) (string, error)
-		GetDetails() StakingDetails
-		GetValidators() (ValidatorPage, error)
-		GetDelegations(address string) (DelegationsPage, error)
-	}
-
-	CollectionAPI interface {
-		Platform
-		GetCollections(owner string) (CollectionPage, error)
-		GetCollectibles(owner, collectibleID string) (CollectiblePage, error)
-
-		OldGetCollections(owner string) (CollectionPage, error)
-		OldGetCollectibles(owner, collectibleID string) (CollectiblePage, error)
-
-		GetCollectionsV4(owner string) (CollectionPage, error)
-		GetCollectiblesV4(owner, collectibleID string) (CollectiblePage, error)
-	}
-
-	// CustomAPI provides custom HTTP routes
-	CustomAPI interface {
-		Platform
-		RegisterRoutes(router gin.IRouter)
-	}
-
-	// NamingServiceAPI provides public name service domains HTTP routes
-	NamingServiceAPI interface {
-		Platform
-		Lookup(coins []uint64, name string) ([]Resolved, error)
-	}
-)
diff --git a/pkg/blockatlas/observer.go b/pkg/blockatlas/observer.go
index 1c3ba0f..168c94e 100644
--- a/pkg/blockatlas/observer.go
+++ b/pkg/blockatlas/observer.go
@@ -29,12 +29,6 @@ type (
 		Status  bool   `json:"status"`
 		Message string `json:"message"`
 	}
-
-	Block struct {
-		Number int64  `json:"number"`
-		ID     string `json:"id,omitempty"`
-		Txs    []Tx   `json:"txs"`
-	}
 )
 
 func (e *SubscriptionEvent) ParseSubscriptions(s Subscriptions) []Subscription {
diff --git a/pkg/blockatlas/platform.go b/pkg/blockatlas/platform.go
index 0dad231..28bbbb6 100644
--- a/pkg/blockatlas/platform.go
+++ b/pkg/blockatlas/platform.go
@@ -1,5 +1,81 @@
 package blockatlas
 
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/trustwallet/blockatlas/coin"
+)
+
+type (
+	// Platform can be used to access a crypto service
+	Platform interface {
+		Coin() coin.Coin
+	}
+
+	// TxAPI provides transaction lookups
+	TxAPI interface {
+		Platform
+		GetTxsByAddress(address string) (TxPage, error)
+	}
+
+	// TokenTxAPI provides token transaction lookups
+	TokenTxAPI interface {
+		Platform
+		GetTokenTxsByAddress(address, token string) (TxPage, error)
+	}
+
+	// TokenAPI provides token lookups
+	TokenAPI interface {
+		Platform
+		GetTokenListByAddress(address string) (TokenPage, error)
+	}
+
+	// BlockAPI provides block information and lookups
+	BlockAPI interface {
+		Platform
+		CurrentBlockNumber() (int64, error)
+		GetBlockByNumber(num int64) (*Block, error)
+	}
+
+	// AddressAPI provides address information
+	AddressAPI interface {
+		Platform
+		GetAddressesFromXpub(xpub string) ([]string, error)
+	}
+
+	// StakingAPI provides staking information
+	StakeAPI interface {
+		Platform
+		UndelegatedBalance(address string) (string, error)
+		GetDetails() StakingDetails
+		GetValidators() (ValidatorPage, error)
+		GetDelegations(address string) (DelegationsPage, error)
+	}
+
+	CollectionAPI interface {
+		Platform
+		GetCollections(owner string) (CollectionPage, error)
+		GetCollectibles(owner, collectibleID string) (CollectiblePage, error)
+
+		OldGetCollections(owner string) (CollectionPage, error)
+		OldGetCollectibles(owner, collectibleID string) (CollectiblePage, error)
+
+		GetCollectionsV4(owner string) (CollectionPage, error)
+		GetCollectiblesV4(owner, collectibleID string) (CollectiblePage, error)
+	}
+
+	// CustomAPI provides custom HTTP routes
+	CustomAPI interface {
+		Platform
+		RegisterRoutes(router gin.IRouter)
+	}
+
+	// NamingServiceAPI provides public name service domains HTTP routes
+	NamingServiceAPI interface {
+		Platform
+		Lookup(coins []uint64, name string) ([]Resolved, error)
+	}
+)
+
 type Platforms map[string]Platform
 
 func (ps Platforms) GetPlatformList() []Platform {
diff --git a/pkg/blockatlas/tx.go b/pkg/blockatlas/tx.go
index 0d46eff..8a1e1dd 100644
--- a/pkg/blockatlas/tx.go
+++ b/pkg/blockatlas/tx.go
@@ -1,5 +1,11 @@
 package blockatlas
 
+import (
+	mapset "github.com/deckarep/golang-set"
+	"github.com/trustwallet/blockatlas/coin"
+	"github.com/trustwallet/blockatlas/pkg/numbers"
+)
+
 const (
 	StatusCompleted Status = "completed"
 	StatusPending   Status = "pending"
@@ -57,6 +63,12 @@ type (
 	KeyType         string
 	KeyTitle        string
 
+	Block struct {
+		Number int64  `json:"number"`
+		ID     string `json:"id,omitempty"`
+		Txs    []Tx   `json:"txs"`
+	}
+
 	// TxPage is a page of transactions
 	TxPage []Tx
 
@@ -193,8 +205,25 @@ type (
 		Coin     uint      `json:"coin"`
 		Type     TokenType `json:"type"`
 	}
+
+	Txs []Tx
 )
 
+func (t Txs) GetTransactionsMap() TxSetMap {
+	txSetMap := TxSetMap{Map: make(map[string]*TxSet)}
+	for i := 0; i < len(t); i++ {
+		addresses := t[i].GetAddresses()
+		addresses = append(addresses, t[i].GetUtxoAddresses()...)
+		for _, address := range addresses {
+			if txSetMap.Map[address] == nil {
+				txSetMap.Map[address] = new(TxSet)
+			}
+			txSetMap.Map[address].Add(&t[i])
+		}
+	}
+	return txSetMap
+}
+
 func (t *Tx) GetUtxoAddresses() (addresses []string) {
 	for _, input := range t.Inputs {
 		addresses = append(addresses, input.Address)
@@ -234,3 +263,80 @@ func (t *Tx) GetAddresses() []string {
 		return addresses
 	}
 }
+
+func (t *Tx) GetTransactionDirection(address string) Direction {
+	if len(t.Inputs) > 0 && len(t.Outputs) > 0 {
+		addressSet := mapset.NewSet(address)
+		return InferDirection(t, addressSet)
+	}
+	switch meta := t.Meta.(type) {
+	case TokenTransfer:
+		return determineTransactionDirection(address, meta.From, meta.To)
+	case NativeTokenTransfer:
+		return determineTransactionDirection(address, meta.From, meta.To)
+	default:
+		return determineTransactionDirection(address, t.From, t.To)
+	}
+}
+
+func determineTransactionDirection(address, from, to string) Direction {
+	if address == to {
+		if from == to {
+			return DirectionSelf
+		}
+		return DirectionIncoming
+	}
+	return DirectionOutgoing
+}
+
+func (t *Tx) InferUtxoValue(address string, coinIndex uint) {
+	if len(t.Inputs) > 0 && len(t.Outputs) > 0 {
+		addressSet := mapset.NewSet(address)
+		value := InferValue(t, t.Direction, addressSet)
+		t.Meta = Transfer{
+			Value:    value,
+			Symbol:   coin.Coins[coinIndex].Symbol,
+			Decimals: coin.Coins[coinIndex].Decimals,
+		}
+	}
+}
+
+func InferDirection(tx *Tx, addressSet mapset.Set) Direction {
+	inputSet := mapset.NewSet()
+	for _, address := range tx.Inputs {
+		inputSet.Add(address.Address)
+	}
+	outputSet := mapset.NewSet()
+	for _, address := range tx.Outputs {
+		outputSet.Add(address.Address)
+	}
+	intersect := addressSet.Intersect(inputSet)
+	if intersect.Cardinality() == 0 {
+		return DirectionIncoming
+	}
+	if outputSet.IsProperSubset(addressSet) || outputSet.Equal(inputSet) {
+		return DirectionSelf
+	}
+	return DirectionOutgoing
+}
+
+func InferValue(tx *Tx, direction Direction, addressSet mapset.Set) Amount {
+	value := Amount("0")
+	if len(tx.Outputs) == 0 {
+		return value
+	}
+	if direction == DirectionOutgoing || direction == DirectionSelf {
+		value = tx.Outputs[0].Value
+	} else if direction == DirectionIncoming {
+		amount := value
+		for _, output := range tx.Outputs {
+			if !addressSet.Contains(output.Address) {
+				continue
+			}
+			value := numbers.AddAmount(string(amount), string(output.Value))
+			amount = Amount(value)
+		}
+		value = amount
+	}
+	return value
+}
diff --git a/pkg/blockatlas/txset.go b/pkg/blockatlas/txset.go
index a3a27ac..1819a14 100644
--- a/pkg/blockatlas/txset.go
+++ b/pkg/blockatlas/txset.go
@@ -2,12 +2,18 @@ package blockatlas
 
 import "sync"
 
-type TxSet struct {
-	items map[*Tx]bool
-	lock  sync.RWMutex
-}
+type (
+	TxSetMap struct {
+		Map map[string]*TxSet
+	}
+
+	TxSet struct {
+		items map[*Tx]bool
+		lock  sync.RWMutex
+	}
+)
 
-// Add adds a new element to the Set. Returns a pointer to the Set.
+// Add adds a new element to the Map. Returns a pointer to the Map.
 func (s *TxSet) Add(t *Tx) *TxSet {
 	s.lock.Lock()
 	defer s.lock.Unlock()
@@ -36,3 +42,14 @@ func (s *TxSet) Size() int {
 	defer s.lock.RUnlock()
 	return len(s.items)
 }
+
+func (s TxSetMap) GetUniqueAddresses() []string {
+	var addresses []string
+	for address := range s.Map {
+		if len(address) == 0 {
+			continue
+		}
+		addresses = append(addresses, address)
+	}
+	return addresses
+}
diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go
index d2746cb..c0d2f1a 100644
--- a/pkg/errors/errors.go
+++ b/pkg/errors/errors.go
@@ -27,11 +27,7 @@ func (e *Error) isEmpty() bool {
 }
 
 func (e *Error) Error() string {
-	r, err := e.MarshalJSON()
-	if err != nil {
-		return e.Err.Error()
-	}
-	return string(r)
+	return e.String()
 }
 
 func (e *Error) String() string {
@@ -39,9 +35,6 @@ func (e *Error) String() string {
 	if e.Type != TypeNone {
 		msg = fmt.Sprintf("%s | Type: %s", msg, e.Type.String())
 	}
-	if len(e.Meta()) > 0 {
-		msg = fmt.Sprintf("%s | Meta: %s", msg, e.Meta())
-	}
 	if len(e.stack) > 0 {
 		msg = fmt.Sprintf("%s | Stack: %s", msg, e.stack)
 	}
diff --git a/pkg/semaphore/semaphore.go b/pkg/semaphore/semaphore.go
deleted file mode 100644
index e46e332..0000000
--- a/pkg/semaphore/semaphore.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package semaphore
-
-type Semaphore struct {
-	c chan bool
-}
-
-func NewSemaphore(n int) *Semaphore {
-	return &Semaphore{make(chan bool, n)}
-}
-
-func (s *Semaphore) Acquire() {
-	s.c <- true
-}
-
-func (s *Semaphore) Release() {
-	<-s.c
-}
diff --git a/platform/bitcoin/transaction.go b/platform/bitcoin/transaction.go
index 2388dc1..fd4d250 100644
--- a/platform/bitcoin/transaction.go
+++ b/platform/bitcoin/transaction.go
@@ -92,8 +92,8 @@ func normalizeTxs(sourceTxs TransactionsList, coinIndex uint, addressSet mapset.
 
 func normalizeTransfer(transaction Transaction, coinIndex uint, addressSet mapset.Set) (tx blockatlas.Tx, ok bool) {
 	tx = normalizeTransaction(transaction, coinIndex)
-	direction := InferDirection(&tx, addressSet)
-	value := InferValue(&tx, direction, addressSet)
+	direction := blockatlas.InferDirection(&tx, addressSet)
+	value := blockatlas.InferValue(&tx, direction, addressSet)
 
 	tx.Direction = direction
 	tx.Meta = blockatlas.Transfer{
@@ -105,46 +105,6 @@ func normalizeTransfer(transaction Transaction, coinIndex uint, addressSet mapse
 	return tx, true
 }
 
-func InferDirection(tx *blockatlas.Tx, addressSet mapset.Set) blockatlas.Direction {
-	inputSet := mapset.NewSet()
-	for _, address := range tx.Inputs {
-		inputSet.Add(address.Address)
-	}
-	outputSet := mapset.NewSet()
-	for _, address := range tx.Outputs {
-		outputSet.Add(address.Address)
-	}
-	intersect := addressSet.Intersect(inputSet)
-	if intersect.Cardinality() == 0 {
-		return blockatlas.DirectionIncoming
-	}
-	if outputSet.IsProperSubset(addressSet) || outputSet.Equal(inputSet) {
-		return blockatlas.DirectionSelf
-	}
-	return blockatlas.DirectionOutgoing
-}
-
-func InferValue(tx *blockatlas.Tx, direction blockatlas.Direction, addressSet mapset.Set) blockatlas.Amount {
-	value := blockatlas.Amount("0")
-	if len(tx.Outputs) == 0 {
-		return value
-	}
-	if direction == blockatlas.DirectionOutgoing || direction == blockatlas.DirectionSelf {
-		value = tx.Outputs[0].Value
-	} else if direction == blockatlas.DirectionIncoming {
-		amount := value
-		for _, output := range tx.Outputs {
-			if !addressSet.Contains(output.Address) {
-				continue
-			}
-			value := numbers.AddAmount(string(amount), string(output.Value))
-			amount = blockatlas.Amount(value)
-		}
-		value = amount
-	}
-	return value
-}
-
 func normalizeTransaction(tx Transaction, coinIndex uint) blockatlas.Tx {
 	inputs := parseOutputs(tx.Vin)
 	outputs := parseOutputs(tx.Vout)
diff --git a/platform/platform.go b/platform/platform.go
index ce97f1d..b19940f 100644
--- a/platform/platform.go
+++ b/platform/platform.go
@@ -24,6 +24,7 @@ import (
 	"github.com/trustwallet/blockatlas/platform/ontology"
 	"github.com/trustwallet/blockatlas/platform/polkadot"
 	"github.com/trustwallet/blockatlas/platform/ripple"
+	"github.com/trustwallet/blockatlas/platform/solana"
 	"github.com/trustwallet/blockatlas/platform/stellar"
 	"github.com/trustwallet/blockatlas/platform/tezos"
 	"github.com/trustwallet/blockatlas/platform/theta"
@@ -71,6 +72,7 @@ func getPlatformMap() blockatlas.Platforms {
 		coin.Ontology().Handle:     ontology.Init(GetApiVar(coin.ONT)),
 		coin.Algorand().Handle:     algorand.Init(GetApiVar(coin.ALGO)),
 		coin.Aeternity().Handle:    aeternity.Init(GetApiVar(coin.AE)),
+		coin.Solana().Handle:       solana.Init(GetApiVar(coin.SOL)),
 		coin.Tezos().Handle:        tezos.Init(GetApiVar(coin.XTZ), GetRpcVar(coin.XTZ)),
 		coin.Binance().Handle:      binance.Init(GetApiVar(coin.BNB), GetVar("binance.dex")),
 		coin.Zilliqa().Handle:      zilliqa.Init(GetApiVar(coin.ZIL), GetVar("zilliqa.key"), GetRpcVar(coin.ZIL), GetVar("zilliqa.lookup")),
diff --git a/platform/solana/base.go b/platform/solana/base.go
new file mode 100644
index 0000000..f14ad7a
--- /dev/null
+++ b/platform/solana/base.go
@@ -0,0 +1,22 @@
+package solana
+
+import (
+	"github.com/trustwallet/blockatlas/coin"
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+)
+
+type Platform struct {
+	client Client
+}
+
+func Init(api string) *Platform {
+	p := &Platform{
+		client: Client{blockatlas.InitClient(api)},
+	}
+	p.client.Headers["Content-Type"] = "application/json"
+	return p
+}
+
+func (p *Platform) Coin() coin.Coin {
+	return coin.Coins[coin.SOL]
+}
diff --git a/platform/solana/client.go b/platform/solana/client.go
new file mode 100644
index 0000000..94c2bc6
--- /dev/null
+++ b/platform/solana/client.go
@@ -0,0 +1,38 @@
+package solana
+
+import (
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+)
+
+const stakeProgramId = "Stake11111111111111111111111111111111111111"
+
+type Client struct {
+	blockatlas.Request
+}
+
+func (c *Client) GetCurrentVoteAccounts() (validators []VoteAccount, err error) {
+	var v VoteAccounts
+	err = c.RpcCall(&v, "getVoteAccounts", []string{})
+	return v.Current, err
+}
+
+func (c *Client) GetStakeAccounts() (accounts []KeyedAccount, err error) {
+	err = c.RpcCall(&accounts, "getProgramAccounts", []string{stakeProgramId})
+	return
+}
+
+func (c *Client) GetAccount(pubkey string) (account Account, err error) {
+	var r RpcAccount
+	err = c.RpcCall(&r, "getAccountInfo", []string{pubkey})
+	return r.Account, err
+}
+
+func (c *Client) GetEpochInfo() (epochInfo EpochInfo, err error) {
+	err = c.RpcCall(&epochInfo, "getEpochInfo", []string{})
+	return
+}
+
+func (c *Client) GetMinimumBalanceForRentExemption() (minimumBalance uint64, err error) {
+	err = c.RpcCall(&minimumBalance, "getMinimumBalanceForRentExemption", []uint64{4008})
+	return
+}
diff --git a/platform/solana/model.go b/platform/solana/model.go
new file mode 100644
index 0000000..bffac00
--- /dev/null
+++ b/platform/solana/model.go
@@ -0,0 +1,62 @@
+package solana
+
+type VoteAccount struct {
+	NodePubkey       string     `json:"nodePubkey"`
+	VotePubkey       string     `json:"votePubkey"`
+	Commission       uint64     `json:"commission"`
+	ActivatedStake   uint64     `json:"activatedStake"`
+	RootSlot         uint64     `json:"rootSlot"`
+	LastVote         uint64     `json:"lastVote"`
+	EpochCredits     [][]uint64 `json:"epochCredits"`
+	EpochVoteAccount bool       `json:"epochVoteAccount"`
+}
+
+type VoteAccounts struct {
+	Current    []VoteAccount `json:"current"`
+	Delinquent []VoteAccount `json:"delinquent"`
+}
+
+type Account struct {
+	Data       string `json:"data"`
+	Executable bool   `json:"executable"`
+	Lamports   uint64 `json:"lamports"`
+	Owner      string `json:"owner"`
+	RentEpoch  uint64 `json:"rentEpoch"`
+}
+
+type KeyedAccount struct {
+	Account Account `json:"account"`
+	Pubkey  string  `json:"pubkey"`
+}
+
+type RpcAccount struct {
+	Context RpcContext `json:"context"`
+	Account Account    `json:"value"`
+}
+
+type RpcContext struct {
+	Slot uint64 `json:"slot"`
+}
+
+type StakeState struct {
+	State                uint32
+	RentExemptReserve    uint64
+	AuthorizedStaker     [32]byte
+	AuthorizedWithdrawer [32]byte
+	UnixTimestamp        int64
+	LockupEpoch          uint64
+	Custodian            [32]byte
+	VoterPubkey          [32]byte
+	Stake                uint64
+	ActivationEpoch      uint64
+	DeactivationEpoch    uint64
+	WarmupCooldownRate   float64
+	CreditsObserved      uint64
+}
+
+type EpochInfo struct {
+	AbsoluteSlot uint64 `json:"absoluteSlot"`
+	Epoch        uint64 `json:"epoch"`
+	SlotIndex    uint64 `json:"slotIndex"`
+	SlotsInEpoch uint64 `json:"slotsInEpoch"`
+}
diff --git a/platform/solana/stake.go b/platform/solana/stake.go
new file mode 100644
index 0000000..c69de5f
--- /dev/null
+++ b/platform/solana/stake.go
@@ -0,0 +1,138 @@
+package solana
+
+import (
+	"bytes"
+	"encoding/binary"
+	"github.com/btcsuite/btcutil/base58"
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"github.com/trustwallet/blockatlas/pkg/errors"
+	services "github.com/trustwallet/blockatlas/services/assets"
+	"strconv"
+)
+
+func arrayOfPubkey(pubkey string) [32]byte {
+	var array [32]byte
+	copy(array[:], base58.Decode(pubkey))
+	return array
+}
+
+func (p *Platform) GetValidators() (blockatlas.ValidatorPage, error) {
+	results := make(blockatlas.ValidatorPage, 0)
+
+	validators, err := p.client.GetCurrentVoteAccounts()
+	if err != nil {
+		return results, err
+	}
+
+	minimumBalance, err := p.client.GetMinimumBalanceForRentExemption()
+	if err != nil {
+		minimumBalance = 0
+	}
+
+	for _, v := range validators {
+		results = append(results, normalizeValidator(v, minimumBalance))
+	}
+	return results, nil
+}
+
+func (p *Platform) GetDetails() blockatlas.StakingDetails {
+	return getDetails()
+}
+
+func getDetails() blockatlas.StakingDetails {
+	return blockatlas.StakingDetails{
+		Reward:        blockatlas.StakingReward{Annual: 0},
+		MinimumAmount: "0",
+		LockTime:      0,
+		Type:          blockatlas.DelegationTypeDelegate,
+	}
+}
+
+func normalizeValidator(v VoteAccount, minimumBalance uint64) (validator blockatlas.Validator) {
+	minimumAmount := strconv.FormatUint(minimumBalance+1, 10) // Must stake at least 1 lamport
+	return blockatlas.Validator{
+		Status: true,
+		ID:     v.VotePubkey,
+		Details: blockatlas.StakingDetails{
+			Reward:        blockatlas.StakingReward{Annual: 0},
+			MinimumAmount: blockatlas.Amount(minimumAmount),
+			LockTime:      0,
+			Type:          blockatlas.DelegationTypeDelegate,
+		},
+	}
+}
+
+func (p *Platform) GetDelegations(address string) (blockatlas.DelegationsPage, error) {
+	accounts, err := p.client.GetStakeAccounts()
+	if err != nil {
+		return nil, err
+	}
+
+	stakeAccounts := make([]StakeState, 0)
+	for _, keyedAccount := range accounts {
+		account, err := parseStakeData(keyedAccount.Account)
+		if err != nil {
+			return nil, err
+		}
+		if isAuthorized(account, address) {
+			stakeAccounts = append(stakeAccounts, account)
+		}
+	}
+	if len(stakeAccounts) == 0 {
+		return make(blockatlas.DelegationsPage, 0), nil
+	}
+
+	validators, err := services.GetValidatorsMap(p)
+	if err != nil {
+		return nil, err
+	}
+
+	epochInfo, err := p.client.GetEpochInfo()
+	if err != nil {
+		return nil, err
+	}
+
+	return NormalizeDelegations(stakeAccounts, validators, epochInfo)
+}
+
+func parseStakeData(account Account) (stakeAccount StakeState, err error) {
+	buffer := base58.Decode(account.Data)
+	r := bytes.NewReader(buffer)
+	err = binary.Read(r, binary.LittleEndian, &stakeAccount)
+	return
+}
+
+func isAuthorized(stakeAccount StakeState, address string) bool {
+	return stakeAccount.AuthorizedStaker == arrayOfPubkey(address)
+}
+
+func (p *Platform) UndelegatedBalance(address string) (string, error) {
+	account, err := p.client.GetAccount(address)
+	if err != nil {
+		return "0", err
+	}
+	return strconv.FormatUint(account.Lamports, 10), nil
+}
+
+func NormalizeDelegations(stakeAccounts []StakeState, validators blockatlas.ValidatorMap, epochInfo EpochInfo) (blockatlas.DelegationsPage, error) {
+	results := make([]blockatlas.Delegation, 0)
+	for _, stakeState := range stakeAccounts {
+		votePubkey := base58.Encode(stakeState.VoterPubkey[:])
+		validator, ok := validators[votePubkey]
+		if !ok {
+			return nil, errors.E("Validator not found",
+				errors.Params{"Validator": votePubkey, "Platform": "solana"})
+		}
+		status := blockatlas.DelegationStatusPending
+		if stakeState.ActivationEpoch > 0 && stakeState.ActivationEpoch <= epochInfo.Epoch {
+			status = blockatlas.DelegationStatusActive
+		}
+		delegation := blockatlas.Delegation{
+			Delegator: validator,
+			Value:     strconv.FormatUint(stakeState.Stake, 10),
+			Status:    status,
+		}
+		results = append(results, delegation)
+	}
+	return results, nil
+}
diff --git a/platform/solana/transaction.go b/platform/solana/transaction.go
new file mode 100644
index 0000000..8ded470
--- /dev/null
+++ b/platform/solana/transaction.go
@@ -0,0 +1,10 @@
+package solana
+
+import (
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+)
+
+func (p *Platform) GetTxsByAddress(address string) (blockatlas.TxPage, error) {
+	results := make(blockatlas.TxPage, 0)
+	return results, nil
+}
diff --git a/platform/theta/model.go b/platform/theta/model.go
index b6dce25..0211cae 100644
--- a/platform/theta/model.go
+++ b/platform/theta/model.go
@@ -20,7 +20,7 @@ type Tx struct {
 
 type Data struct {
 	Fee     Fee      `json:"fee"`
-	Inputs  []Inputs `json:"inputs"`
+	Inputs  []Input  `json:"inputs"`
 	Outputs []Output `json:"outputs"`
 }
 
@@ -29,7 +29,7 @@ type Fee struct {
 	Tfuelwei string `json:"tfuelwei"`
 }
 
-type Inputs struct {
+type Input struct {
 	Address  string `json:"address"`
 	Sequence string `json:"sequence"`
 }
diff --git a/platform/theta/transaction.go b/platform/theta/transaction.go
index 72b192d..d96100f 100644
--- a/platform/theta/transaction.go
+++ b/platform/theta/transaction.go
@@ -1,9 +1,11 @@
 package theta
 
 import (
+	"fmt"
 	"github.com/trustwallet/blockatlas/coin"
 	"github.com/trustwallet/blockatlas/pkg/blockatlas"
 	"strconv"
+	"strings"
 )
 
 func (p *Platform) GetTxsByAddress(address string) (blockatlas.TxPage, error) {
@@ -11,7 +13,7 @@ func (p *Platform) GetTxsByAddress(address string) (blockatlas.TxPage, error) {
 	return p.GetTokenTxsByAddress(address, "")
 }
 
-func (p *Platform) GetTokenTxsByAddress(address string, token string) (blockatlas.TxPage, error) {
+func (p *Platform) GetTokenTxsByAddress(address, token string) (blockatlas.TxPage, error) {
 	trx, err := p.client.FetchAddressTransactions(address)
 	if err != nil {
 		return nil, err
@@ -44,16 +46,28 @@ func Normalize(trx *Tx, address, token string) (tx blockatlas.Tx, ok bool) {
 		Block:    block,
 		Sequence: block,
 	}
+	inputs := trx.Data.Inputs
+	outputs := trx.Data.Outputs
 
-	input := trx.Data.Inputs[0]
-	output := trx.Data.Outputs[0]
+	// Doesn't support multisend
+	if len(inputs) == 0 && len(outputs) == 0 {
+		return tx, false
+	}
+
+	input := inputs[0]
+	output := outputs[0]
 	sequence, _ := strconv.ParseUint(input.Sequence, 10, 64)
 
-	// Condition for transfer THETA trnafer
+	// Condition for transfer THETA transfer
 	if address != "" && token == "" && output.Coins.Tfuelwei == "0" {
+		direction, err := getDirection(address, input, output)
+		if err != nil {
+			return tx, false
+		}
 		tx.From = input.Address
 		tx.To = output.Address
 		tx.Sequence = sequence
+		tx.Direction = direction
 		tx.Type = blockatlas.TxTransfer
 		tx.Meta = blockatlas.Transfer{
 			Value:    blockatlas.Amount(output.Coins.Thetawei),
@@ -68,9 +82,15 @@ func Normalize(trx *Tx, address, token string) (tx blockatlas.Tx, ok bool) {
 	if address != "" && token == "tfuel" && output.Coins.Thetawei == "0" {
 		from := input.Address
 		to := output.Address
+		direction, err := getDirection(address, input, output)
+		if err != nil {
+			return tx, false
+		}
+
 		tx.From = from
 		tx.To = to
 		tx.Sequence = sequence
+		tx.Direction = direction
 		tx.Type = blockatlas.TxNativeTokenTransfer
 		tx.Meta = blockatlas.NativeTokenTransfer{
 			Name:     "Theta Fuel",
@@ -87,3 +107,21 @@ func Normalize(trx *Tx, address, token string) (tx blockatlas.Tx, ok bool) {
 
 	return tx, false
 }
+
+// Get transaction direction
+func getDirection(a string, inputs Input, outputs Output) (dir blockatlas.Direction, err error) {
+	address := strings.ToLower(a)
+	inAddr := inputs.Address
+	outAddr := outputs.Address
+
+	switch {
+	case inAddr == address && outAddr == address:
+		return blockatlas.DirectionSelf, nil
+	case inAddr == address && outAddr != address:
+		return blockatlas.DirectionOutgoing, nil
+	case inAddr != address && outAddr == address:
+		return blockatlas.DirectionIncoming, nil
+	}
+
+	return "", fmt.Errorf("direction unknown")
+}
diff --git a/platform/tron/client.go b/platform/tron/client.go
index 56b654a..e5c5f65 100644
--- a/platform/tron/client.go
+++ b/platform/tron/client.go
@@ -31,9 +31,9 @@ func (c *Client) GetTxsOfAddress(address, token string) ([]Tx, error) {
 
 	var txs Page
 	err := c.Get(&txs, path, url.Values{
-		"limit":          {"25"},
-		"token_id":       {token},
-		"order_by":       {"block_timestamp,desc"},
+		"limit":    {"25"},
+		"token_id": {token},
+		"order_by": {"block_timestamp,desc"},
 	})
 
 	return txs.Txs, err
diff --git a/scripts/run_tests_postman.sh b/scripts/run_tests_postman.sh
old mode 100755
new mode 100644
diff --git a/services/observer/healthcheck/healthcheck.go b/services/observer/healthcheck/healthcheck.go
new file mode 100644
index 0000000..1fbde8a
--- /dev/null
+++ b/services/observer/healthcheck/healthcheck.go
@@ -0,0 +1,112 @@
+package healthcheck
+
+import (
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"github.com/trustwallet/blockatlas/pkg/logger"
+	"github.com/trustwallet/blockatlas/storage"
+	"sync"
+	"time"
+)
+
+var observerStatus status
+
+func init() {
+	observerStatus = status{
+		m: make(map[string]Info),
+	}
+}
+
+type status struct {
+	m map[string]Info
+	sync.RWMutex
+}
+
+func (cr *status) update(coin string, info Info) {
+	cr.Lock()
+	cr.m[coin] = info
+	cr.Unlock()
+}
+
+func (cr *status) get() map[string]Info {
+	cr.RLock()
+	defer cr.RUnlock()
+	return cr.m
+}
+
+func Worker(storage storage.Tracker, api blockatlas.BlockAPI) {
+	var (
+		lastParsedBlock, currentBlock int64
+		err                           error
+	)
+
+	lastParsedBlock, err = storage.GetLastParsedBlockNumber(api.Coin().ID)
+	if err != nil {
+		logger.Fatal(err)
+	}
+
+	var duration time.Duration
+	t := api.Coin().BlockTime / 1000
+
+	if t > 30 {
+		duration = time.Duration(int64(time.Second) * int64(t))
+	} else {
+		duration = time.Minute * 11
+	}
+	logger.Info("Setting update duration", logger.Params{"duration": duration})
+
+	for {
+		currentBlock, err = storage.GetLastParsedBlockNumber(api.Coin().ID)
+		if err != nil {
+			logger.Error(err)
+			continue
+		}
+
+		latestBlock, err := api.CurrentBlockNumber()
+		if err != nil {
+			logger.Error(err)
+			continue
+		}
+
+		logger.Info("fetched", logger.Params{"currentBlock": currentBlock, "lastParsedBlock": lastParsedBlock, "latestBlock": latestBlock, "handle": api.Coin().Handle})
+
+		if currentBlock > lastParsedBlock || latestBlock == currentBlock {
+			lastParsedBlock = currentBlock
+			observerStatus.update(api.Coin().Handle, Info{
+				LastParsedBlock: lastParsedBlock,
+				Healthy:         true,
+			})
+		} else {
+			observerStatus.update(api.Coin().Handle, Info{
+				LastParsedBlock: lastParsedBlock,
+				Healthy:         false,
+			})
+		}
+
+		time.Sleep(duration)
+	}
+}
+
+func GetStatus(exclude []string) map[string]interface{} {
+	total := true
+	result := make(map[string]interface{})
+	status := observerStatus.get()
+	for k, v := range status{
+		if !contain(k, exclude){
+			if v.Healthy == false{
+				total = false
+			}
+			result[k] = v
+		}
+	}
+	result["total"] = total
+	return result
+}
+
+func contain(elem string, list []string) bool{
+	for _, v := range list{
+		if v == elem{
+			return true
+		}
+	}
+	return false
+}
diff --git a/services/observer/healthcheck/model.go b/services/observer/healthcheck/model.go
new file mode 100644
index 0000000..6a4fa94
--- /dev/null
+++ b/services/observer/healthcheck/model.go
@@ -0,0 +1,6 @@
+package healthcheck
+
+type Info struct {
+	LastParsedBlock int64 `json:"lastParsedBlock"`
+	Healthy         bool  `json:"healthy"`
+}
diff --git a/services/observer/notifier/notifier.go b/services/observer/notifier/notifier.go
new file mode 100644
index 0000000..0f75260
--- /dev/null
+++ b/services/observer/notifier/notifier.go
@@ -0,0 +1,101 @@
+package notifier
+
+import (
+	"encoding/json"
+	"github.com/streadway/amqp"
+	"github.com/trustwallet/blockatlas/mq"
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"github.com/trustwallet/blockatlas/pkg/errors"
+	"github.com/trustwallet/blockatlas/pkg/logger"
+	"github.com/trustwallet/blockatlas/pkg/numbers"
+	"github.com/trustwallet/blockatlas/storage"
+	"sync"
+	"time"
+)
+
+type DispatchEvent struct {
+	Action blockatlas.TransactionType `json:"action"`
+	Result *blockatlas.Tx             `json:"result"`
+	GUID   string                     `json:"guid"`
+}
+
+func RunNotifier(delivery amqp.Delivery, s storage.Addresses) {
+	defer func() {
+		if err := delivery.Ack(false); err != nil {
+			logger.Error(err)
+		}
+	}()
+	var txs blockatlas.Txs
+	if err := json.Unmarshal(delivery.Body, &txs); err != nil {
+		logger.Error(err)
+		return
+	}
+	if len(txs) == 0 {
+		return
+	}
+
+	logger.Info("Consumed", logger.Params{"txs": len(txs), "coin": txs[0].Coin})
+
+	blockTransactions := txs.GetTransactionsMap()
+	if len(blockTransactions.Map) == 0 {
+		return
+	}
+
+	addresses := blockTransactions.GetUniqueAddresses()
+	subs, err := s.FindSubscriptions(txs[0].Coin, addresses)
+	if err != nil || len(subs) == 0 {
+		return
+	}
+
+	var wg sync.WaitGroup
+	wg.Add(len(subs))
+	for _, sub := range subs {
+		go buildAndPostMessage(blockTransactions, sub, &wg)
+	}
+	wg.Wait()
+}
+
+func buildAndPostMessage(blockTransactions blockatlas.TxSetMap, sub blockatlas.Subscription, wg *sync.WaitGroup) {
+	defer wg.Done()
+	tx, ok := blockTransactions.Map[sub.Address]
+	if !ok {
+		return
+	}
+	for _, tx := range tx.Txs() {
+		tx.Direction = tx.GetTransactionDirection(sub.Address)
+		tx.InferUtxoValue(sub.Address, tx.Coin)
+		action := DispatchEvent{
+			Action: tx.Type,
+			Result: &tx,
+			GUID:   sub.GUID,
+		}
+		txJson, err := json.Marshal(action)
+		if err != nil {
+			logger.Panic(err)
+		}
+
+		logParams := logger.Params{
+			"guid": sub.GUID,
+			"coin": sub.Coin,
+			"txID": tx.ID,
+		}
+
+		publishTransaction(sub.GUID, txJson, logParams)
+	}
+}
+
+func publishTransaction(message string, rawMessage []byte, logParams logger.Params) {
+	err := mq.Transactions.Publish(rawMessage)
+	if err != nil {
+		err = errors.E(err, "Failed to dispatch event", errors.Params{"message": message}, logParams)
+		logger.Fatal(err, logger.Params{"message": message}, logParams)
+	}
+	logger.Info("Message dispatched", logger.Params{"message": message}, logParams)
+}
+
+func GetInterval(value int, minInterval, maxInterval time.Duration) time.Duration {
+	interval := time.Duration(value) * time.Millisecond
+	pMin := numbers.Max(minInterval.Nanoseconds(), interval.Nanoseconds())
+	pMax := numbers.Min(int(maxInterval.Nanoseconds()), int(pMin))
+	return time.Duration(pMax)
+}
diff --git a/services/observer/parser/parser.go b/services/observer/parser/parser.go
new file mode 100644
index 0000000..bcec764
--- /dev/null
+++ b/services/observer/parser/parser.go
@@ -0,0 +1,297 @@
+package parser
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"github.com/trustwallet/blockatlas/mq"
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"github.com/trustwallet/blockatlas/pkg/errors"
+	"sync/atomic"
+
+	"github.com/trustwallet/blockatlas/pkg/logger"
+	"github.com/trustwallet/blockatlas/storage"
+	"math/rand"
+	"sort"
+	"sync"
+	"time"
+)
+
+type (
+	Params struct {
+		Ctx                   context.Context
+		Api                   blockatlas.BlockAPI
+		Storage               storage.Tracker
+		Queue                 mq.Queue
+		ParsingBlocksInterval time.Duration
+		BacklogCount          int
+		MaxBacklogBlocks      int64
+		StopChannel           chan<- struct{}
+		TxBatchLimit          uint
+	}
+
+	GetBlockByNumber func(num int64) (*blockatlas.Block, error)
+
+	stop struct {
+		error
+	}
+
+	transactionsBatch struct {
+		sync.Mutex
+		blockatlas.Txs
+	}
+)
+
+const MinTxsBatchLimit = 500
+
+func RunParser(params Params) {
+	logger.Info("------------------------------------------------------------")
+	for {
+		select {
+		case <-params.Ctx.Done():
+			logger.Info(fmt.Sprintf("Parser of %s stopped parsing blocks", params.Api.Coin().Handle))
+			params.StopChannel <- struct{}{}
+			return
+		default:
+			lastParsedBlock, currentBlock, err := GetBlocksIntervalToFetch(params)
+			if err != nil {
+				logger.Error(err, logger.Params{"coin": params.Api.Coin().Handle})
+				time.Sleep(params.ParsingBlocksInterval)
+				continue
+			}
+
+			blocks := FetchBlocks(params.Api, lastParsedBlock, currentBlock)
+
+			err = SaveLastParsedBlock(params, blocks)
+			if err != nil {
+				logger.Error(err, logger.Params{"coin": params.Api.Coin().Handle})
+				time.Sleep(params.ParsingBlocksInterval)
+				continue
+			}
+
+			txs := ConvertToBatch(blocks)
+
+			PublishTransactionsBatch(params, txs)
+
+			time.Sleep(params.ParsingBlocksInterval)
+		}
+	}
+}
+
+func GetBlocksIntervalToFetch(params Params) (int64, int64, error) {
+	lastParsedBlock, err := params.Storage.GetLastParsedBlockNumber(params.Api.Coin().ID)
+	if err != nil {
+		return 0, 0, errors.E(err, "Polling failed: tracker didn't return last known block number")
+	}
+	currentBlock, err := params.Api.CurrentBlockNumber()
+	currentBlock -= params.Api.Coin().MinConfirmations
+	if err != nil {
+		return 0, 0, errors.E(err, "Polling failed: source didn't return chain head number")
+	}
+
+	if currentBlock-lastParsedBlock > int64(params.BacklogCount) {
+		lastParsedBlock = currentBlock - int64(params.BacklogCount)
+	}
+
+	if currentBlock-lastParsedBlock > params.MaxBacklogBlocks {
+		lastParsedBlock = currentBlock - params.MaxBacklogBlocks
+	}
+
+	return lastParsedBlock, currentBlock, nil
+}
+
+func FetchBlocks(api blockatlas.BlockAPI, lastParsedBlock, currentBlock int64) []blockatlas.Block {
+	if lastParsedBlock == currentBlock {
+		logger.Info("No new blocks", logger.Params{"last": lastParsedBlock, "coin": api.Coin().ID, "time": time.Now().Unix()})
+		return nil
+	}
+
+	blocksCount := currentBlock - lastParsedBlock
+	if blocksCount < 0 {
+		logger.Error("Current block is 0", logger.Params{"coin": api.Coin().Handle})
+		return nil
+	}
+
+	var (
+		blocksChan = make(chan blockatlas.Block, blocksCount)
+		errorsChan = make(chan error, blocksCount)
+		totalCount int32
+		wg         sync.WaitGroup
+	)
+
+	for i := lastParsedBlock + 1; i <= currentBlock; i++ {
+		wg.Add(1)
+		go func(i int64, wg *sync.WaitGroup) {
+			defer wg.Done()
+			err := fetchBlock(api, i, blocksChan)
+			if err != nil {
+				errorsChan <- err
+				return
+			}
+			atomic.AddInt32(&totalCount, 1)
+		}(i, &wg)
+	}
+
+	wg.Wait()
+	close(errorsChan)
+	close(blocksChan)
+
+	if len(errorsChan) > 0 {
+		var (
+			errorsList = make([]error, 0, len(errorsChan))
+		)
+		for err := range errorsChan {
+			errorsList = append(errorsList, err)
+		}
+		logger.Error("Fetch blocks errors", logger.Params{"count": len(errorsList), "blocks": errorsList})
+	}
+
+	blocksList := make([]blockatlas.Block, 0, len(blocksChan))
+	for block := range blocksChan {
+		blocksList = append(blocksList, block)
+	}
+
+	logger.Info("Fetched blocks batch", logger.Params{"from": lastParsedBlock, "to": currentBlock, "total": totalCount})
+	return blocksList
+}
+
+func fetchBlock(api blockatlas.BlockAPI, num int64, blocksChan chan<- blockatlas.Block) error {
+	block, err := getBlockByNumberWithRetry(5, time.Second*5, api.GetBlockByNumber, num)
+	if err != nil {
+		return errors.E(fmt.Sprintf("%d", num))
+	}
+	blocksChan <- *block
+	return nil
+}
+
+func SaveLastParsedBlock(params Params, blocks []blockatlas.Block) error {
+	if len(blocks) == 0 {
+		return nil
+	}
+
+	sort.Slice(blocks, func(i, j int) bool {
+		return blocks[i].Number < blocks[j].Number
+	})
+
+	lastBlockNumber := blocks[len(blocks)-1].Number
+
+	err := params.Storage.SetLastParsedBlockNumber(params.Api.Coin().ID, lastBlockNumber)
+	if err != nil {
+		return err
+	}
+
+	logger.Info(err, "Save last parsed block", logger.Params{"block": lastBlockNumber, "coin": params.Api.Coin().Handle})
+	return nil
+}
+
+func ConvertToBatch(blocks []blockatlas.Block) blockatlas.Txs {
+	if len(blocks) == 0 {
+		return nil
+	}
+
+	var (
+		txsBatch transactionsBatch
+		wg       sync.WaitGroup
+	)
+
+	for _, block := range blocks {
+		wg.Add(1)
+		go func(block blockatlas.Block, wg *sync.WaitGroup) {
+			defer wg.Done()
+			txsBatch.fillBatch(block.Txs)
+		}(block, &wg)
+	}
+	wg.Wait()
+
+	if len(txsBatch.Txs) == 0 {
+		logger.Info("Blocks converted to transactions batch, there is no transactions", logger.Params{"blocks": len(blocks)})
+		return nil
+	}
+
+	logger.Info("Blocks converted to transactions batch", logger.Params{"blocks": len(blocks), "txs": len(txsBatch.Txs)})
+	return txsBatch.Txs
+}
+
+func PublishTransactionsBatch(params Params, txs blockatlas.Txs) {
+	if len(txs) == 0 {
+		logger.Info("------------------------------------------------------------")
+		return
+	}
+
+	batches := getTxsBatches(txs, params.TxBatchLimit)
+
+	var wg sync.WaitGroup
+	for _, batch := range batches {
+		wg.Add(1)
+		go publish(params, batch, &wg)
+	}
+	wg.Wait()
+
+	logger.Info("Published transactions batch", logger.Params{"txs": len(txs), "batchCount": len(batches)})
+	logger.Info("------------------------------------------------------------")
+}
+
+func getTxsBatches(txs blockatlas.Txs, sizeUint uint) []blockatlas.Txs {
+	size := int(sizeUint)
+	resultLength := (len(txs) + size - 1) / size
+	result := make([]blockatlas.Txs, resultLength)
+	lo, hi := 0, size
+	for i := range result {
+		if hi > len(txs) {
+			hi = len(txs)
+		}
+		result[i] = txs[lo:hi:hi]
+		lo, hi = hi, hi+size
+	}
+	return result
+}
+
+func publish(params Params, txs blockatlas.Txs, wg *sync.WaitGroup) {
+	defer wg.Done()
+	body, err := json.Marshal(txs)
+	if err != nil {
+		logger.Error(err, logger.Params{"coin": params.Api.Coin().Handle})
+		return
+	}
+	err = params.Queue.Publish(body)
+	if err != nil {
+		logger.Error(err, logger.Params{"coin": params.Api.Coin().Handle})
+		return
+	}
+}
+
+func getBlockByNumberWithRetry(attempts int, sleep time.Duration, getBlockByNumber GetBlockByNumber, n int64) (*blockatlas.Block, error) {
+	r, err := getBlockByNumber(n)
+	if err != nil {
+		if s, ok := err.(stop); ok {
+			return nil, s.error
+		}
+		if attempts--; attempts > 0 {
+			// Add some randomness to prevent creating a Thundering Herd
+			rand.Seed(time.Now().UnixNano())
+			jitter := time.Duration(rand.Int63n(int64(sleep)))
+			sleep = sleep + jitter/2
+
+			logger.Info("retry GetBlockByNumber",
+				logger.Params{
+					"number":   n,
+					"attempts": attempts,
+					"sleep":    sleep.String(),
+				},
+			)
+
+			time.Sleep(sleep)
+			return getBlockByNumberWithRetry(attempts, sleep*2, getBlockByNumber, n)
+		}
+	}
+	return r, err
+}
+
+func (t *transactionsBatch) fillBatch(transactions blockatlas.Txs) {
+	t.Lock()
+	defer t.Unlock()
+	if len(transactions) == 0 {
+		return
+	}
+	t.Txs = append(t.Txs, transactions...)
+}
diff --git a/services/subscription/subscribtion.go b/services/observer/subscriber/subscriber.go
similarity index 82%
rename from services/subscription/subscribtion.go
rename to services/observer/subscriber/subscriber.go
index a381fd5..dc9cc4a 100644
--- a/services/subscription/subscribtion.go
+++ b/services/observer/subscriber/subscriber.go
@@ -1,4 +1,4 @@
-package subscription
+package subscriber
 
 import (
 	"encoding/json"
@@ -14,7 +14,7 @@ const (
 	UpdateSubscription blockatlas.SubscriptionOperation = "UpdateSubscription"
 )
 
-func Consume(delivery amqp.Delivery, storage storage.Addresses) {
+func RunSubscriber(delivery amqp.Delivery, storage storage.Addresses) {
 	var event blockatlas.SubscriptionEvent
 	err := json.Unmarshal(delivery.Body, &event)
 	if err != nil {
@@ -35,30 +35,23 @@ func Consume(delivery amqp.Delivery, storage storage.Addresses) {
 		if err != nil {
 			logger.Error(err, params)
 		}
-		err = delivery.Ack(false)
-		if err != nil {
-			logger.Error(err, params)
-		}
 		logger.Info("Updated", params)
 	case AddSubscription:
 		err = storage.AddSubscriptions(newSubscriptions)
 		if err != nil {
 			logger.Error(err, params)
 		}
-		err = delivery.Ack(false)
-		if err != nil {
-			logger.Error(err, params)
-		}
 		logger.Info("Added", params)
 	case DeleteSubscription:
 		err := storage.DeleteSubscriptions(oldSubscriptions)
 		if err != nil {
 			logger.Error(err, params)
 		}
-		err = delivery.Ack(false)
-		if err != nil {
-			logger.Error(err, params)
-		}
 		logger.Info("Deleted", params)
 	}
+
+	err = delivery.Ack(false)
+	if err != nil {
+		logger.Error(err, params)
+	}
 }
diff --git a/storage/addresses.go b/storage/addresses.go
index 97e8cb2..8a3a1cf 100644
--- a/storage/addresses.go
+++ b/storage/addresses.go
@@ -4,76 +4,133 @@ import (
 	"fmt"
 	"github.com/trustwallet/blockatlas/pkg/blockatlas"
 	"github.com/trustwallet/blockatlas/pkg/errors"
+	"sync"
 )
 
 const (
 	ATLAS_OBSERVER = "ATLAS_OBSERVER"
 )
 
-func (s *Storage) Lookup(coin uint, addresses []string) ([]blockatlas.Subscription, error) {
+type SubscriptionOperation func(sub blockatlas.Subscription, wg *sync.WaitGroup, errorsChan chan<- error)
+
+func (s *Storage) RunOperation(subscriptions []blockatlas.Subscription, operation SubscriptionOperation) error {
+	var (
+		errorsChan = make(chan error, len(subscriptions))
+		wg         sync.WaitGroup
+	)
+
+	for _, sub := range subscriptions {
+		wg.Add(1)
+		go operation(sub, &wg, errorsChan)
+	}
+	wg.Wait()
+	close(errorsChan)
+
+	if len(errorsChan) != 0 {
+		var errorsStr string
+		for err := range errorsChan {
+			errorsStr += err.Error() + " "
+		}
+		return errors.E(errorsStr)
+	}
+
+	return nil
+}
+
+func (s *Storage) DeleteSubscriptions(subscriptions []blockatlas.Subscription) error {
+	return s.RunOperation(subscriptions, s.deleteSubscriptions)
+}
+
+func (s *Storage) AddSubscriptions(subscriptions []blockatlas.Subscription) error {
+	return s.RunOperation(subscriptions, s.addSubscriptions)
+}
+
+func (s *Storage) FindSubscriptions(coin uint, addresses []string) ([]blockatlas.Subscription, error) {
 	if len(addresses) == 0 {
 		return nil, errors.E("cannot look up an empty list")
 	}
-	observers := make([]blockatlas.Subscription, 0)
+
+	observersSliceChan := make(chan []blockatlas.Subscription, len(addresses))
+	observers := make([]blockatlas.Subscription, 0, len(addresses))
+
+	var wg sync.WaitGroup
+	wg.Add(len(addresses))
+
 	for _, address := range addresses {
-		key := getSubscriptionKey(coin, address)
-		var guids []string
-		err := s.GetHMValue(ATLAS_OBSERVER, key, &guids)
-		if err != nil {
-			continue
-		}
-		for _, guid := range guids {
-			observers = append(observers, blockatlas.Subscription{Coin: coin, Address: address, GUID: guid})
+		go s.findSubscriptionsByAddress(coin, address, observersSliceChan, &wg)
+	}
+	wg.Wait()
+	close(observersSliceChan)
+
+	for slice := range observersSliceChan {
+		for _, v := range slice {
+			observers = append(observers, v)
 		}
 	}
+
 	return observers, nil
 }
 
-func (s *Storage) AddSubscriptions(subscriptions []blockatlas.Subscription) error {
-	for _, sub := range subscriptions {
-		key := getSubscriptionKey(sub.Coin, sub.Address)
-		var guids []string
-		s.GetHMValue(ATLAS_OBSERVER, key, &guids)
-		if guids == nil {
-			guids = make([]string, 0)
-		}
-		if hasObject(guids, sub.GUID) {
-			continue
-		}
-		guids = append(guids, sub.GUID)
-		err := s.AddHM(ATLAS_OBSERVER, key, guids)
-		if err != nil {
-			return err
-		}
+func (s *Storage) findSubscriptionsByAddress(coin uint, address string, sub chan<- []blockatlas.Subscription, wg *sync.WaitGroup) {
+	defer wg.Done()
+	key := getSubscriptionKey(coin, address)
+	var guids []string
+	err := s.GetHMValue(ATLAS_OBSERVER, key, &guids)
+	if err != nil {
+		return
 	}
-	return nil
+
+	result := make([]blockatlas.Subscription, 0, len(guids))
+
+	for _, guid := range guids {
+		result = append(result, blockatlas.Subscription{Coin: coin, Address: address, GUID: guid})
+	}
+
+	sub <- result
 }
 
-func (s *Storage) DeleteSubscriptions(subscriptions []blockatlas.Subscription) error {
-	for _, sub := range subscriptions {
-		key := getSubscriptionKey(sub.Coin, sub.Address)
-		var guids []string
-		err := s.GetHMValue(ATLAS_OBSERVER, key, &guids)
-		if err != nil {
-			continue
-		}
-		newHooks := make([]string, 0)
-		for _, guid := range guids {
-			if guid == sub.GUID {
-				continue
-			}
-			newHooks = append(newHooks, guid)
-		}
-		if len(newHooks) == 0 {
-			_ = s.DeleteHM(ATLAS_OBSERVER, key)
+func (s *Storage) deleteSubscriptions(sub blockatlas.Subscription, wg *sync.WaitGroup, errorsChan chan<- error) {
+	defer wg.Done()
+	key := getSubscriptionKey(sub.Coin, sub.Address)
+	var guids []string
+	err := s.GetHMValue(ATLAS_OBSERVER, key, &guids)
+	if err != nil {
+		errorsChan <- err
+		return
+	}
+	newHooks := make([]string, 0)
+	for _, guid := range guids {
+		if guid == sub.GUID {
 			continue
 		}
-		err = s.AddHM(ATLAS_OBSERVER, key, newHooks)
-		if err != nil {
-			return err
-		}
+		newHooks = append(newHooks, guid)
+	}
+	if len(newHooks) == 0 {
+		_ = s.DeleteHM(ATLAS_OBSERVER, key)
+		return
+	}
+	err = s.AddHM(ATLAS_OBSERVER, key, newHooks)
+	if err != nil {
+		errorsChan <- err
+	}
+}
+
+func (s *Storage) addSubscriptions(sub blockatlas.Subscription, wg *sync.WaitGroup, errorsChan chan<- error) {
+	defer wg.Done()
+	key := getSubscriptionKey(sub.Coin, sub.Address)
+	var guids []string
+	s.GetHMValue(ATLAS_OBSERVER, key, &guids)
+	if guids == nil {
+		guids = make([]string, 0)
+	}
+	if hasObject(guids, sub.GUID) {
+		return
+	}
+	guids = append(guids, sub.GUID)
+	err := s.AddHM(ATLAS_OBSERVER, key, guids)
+	if err != nil {
+		errorsChan <- err
 	}
-	return nil
 }
 
 func getSubscriptionKey(coin uint, address string) string {
diff --git a/storage/storage.go b/storage/storage.go
index 0b66d1b..2849776 100644
--- a/storage/storage.go
+++ b/storage/storage.go
@@ -40,12 +40,12 @@ func RestoreConnectionWorker(storage *Storage, uri string, timeout time.Duration
 }
 
 type Tracker interface {
-	GetBlockNumber(coin uint) (int64, error)
-	SetBlockNumber(coin uint, num int64) error
+	GetLastParsedBlockNumber(coin uint) (int64, error)
+	SetLastParsedBlockNumber(coin uint, num int64) error
 }
 
 type Addresses interface {
-	Lookup(coin uint, addresses []string) ([]blockatlas.Subscription, error)
+	FindSubscriptions(coin uint, addresses []string) ([]blockatlas.Subscription, error)
 	AddSubscriptions(subscriptions []blockatlas.Subscription) error
 	DeleteSubscriptions(subscriptions []blockatlas.Subscription) error
 }
diff --git a/storage/tracker.go b/storage/tracker.go
index 469de1b..78ab2b6 100644
--- a/storage/tracker.go
+++ b/storage/tracker.go
@@ -33,7 +33,7 @@ func (bm *BlockMap) GetHeights() map[uint]int64 {
 	return bm.heights
 }
 
-func (s *Storage) GetBlockNumber(coin uint) (int64, error) {
+func (s *Storage) GetLastParsedBlockNumber(coin uint) (int64, error) {
 	b, ok := s.blockHeights.GetBlock(coin)
 	if ok {
 		return b, nil
@@ -46,7 +46,7 @@ func (s *Storage) GetBlockNumber(coin uint) (int64, error) {
 	return b, nil
 }
 
-func (s *Storage) SetBlockNumber(coin uint, num int64) error {
+func (s *Storage) SetLastParsedBlockNumber(coin uint, num int64) error {
 	s.blockHeights.SetBlock(coin, num)
 	return s.Add(getBlockKey(coin), num)
 }


Test patch:
diff --git a/observer/observer_test.go b/observer/observer_test.go
deleted file mode 100644
index 4c5b9fd..0000000
--- a/observer/observer_test.go
+++ /dev/null
@@ -1,357 +0,0 @@
-package observer
-
-import (
-	"github.com/stretchr/testify/assert"
-	"github.com/trustwallet/blockatlas/coin"
-	"github.com/trustwallet/blockatlas/pkg/blockatlas"
-	"testing"
-	"time"
-)
-
-var transferDst1 = blockatlas.Tx{
-	ID:     "1681EE543FB4B5A628EF21D746E031F018E226D127044A4F9BA5EE2542A44555",
-	Coin:   coin.BNB,
-	From:   "tbnb1fhr04azuhcj0dulm7ka40y0cqjlafwae9k9gk2",
-	To:     "tbnb1sylyjw032eajr9cyllp26n04300qzzre38qyv5",
-	Fee:    "125000",
-	Date:   1555049867,
-	Block:  7761368,
-	Status: blockatlas.StatusCompleted,
-	Memo:   "test",
-	Meta: blockatlas.Transfer{
-		Value:    "10000000000000",
-		Decimals: 8,
-		Symbol:   "BNB",
-	},
-}
-
-var transferDst2 = blockatlas.Tx{
-	ID:     "1681EE543FB4B5A628EF21D746E031F018E226D127044A4F9BA5EE2542A44556",
-	Coin:   coin.BNB,
-	From:   "tbnb1fhr04azuhcj0dulm7ka40y0cqjlafwae9k9gk2",
-	To:     "tbnb1fhr04azuhcj0dulm7ka40y0cqjlafwae9k9gk2",
-	Fee:    "125000",
-	Date:   1555049867,
-	Block:  7761368,
-	Status: blockatlas.StatusCompleted,
-	Memo:   "test",
-	Meta: blockatlas.Transfer{
-		Value:    "10000000000000",
-		Decimals: 8,
-		Symbol:   "BNB",
-	},
-}
-
-var nativeTransferDst1 = blockatlas.Tx{
-	ID:     "95CF63FAA27579A9B6AF84EF8B2DFEAC29627479E9C98E7F5AE4535E213FA4C9",
-	Coin:   coin.BNB,
-	From:   "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
-	To:     "tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex",
-	Fee:    "125000",
-	Date:   1555117625,
-	Block:  7928667,
-	Status: blockatlas.StatusCompleted,
-	Memo:   "test",
-	Meta: blockatlas.NativeTokenTransfer{
-		TokenID:  "YLC-D8B",
-		Symbol:   "YLC",
-		Value:    "210572645",
-		Decimals: 8,
-		From:     "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
-		To:       "tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex",
-	},
-}
-
-var nativeTransferDst2 = blockatlas.Tx{
-	ID:     "95CF63FAA27579A9B6AF84EF8B2DFEAC29627479E9C98E7F5AE4535E213FA4D0",
-	Coin:   coin.BNB,
-	From:   "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
-	To:     "tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex",
-	Fee:    "125000",
-	Date:   1555117625,
-	Block:  7928667,
-	Status: blockatlas.StatusCompleted,
-	Memo:   "test",
-	Meta: blockatlas.NativeTokenTransfer{
-		TokenID:  "YLC-D8B",
-		Symbol:   "YLC",
-		Value:    "210572645",
-		Decimals: 8,
-		From:     "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
-		To:       "tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex",
-	},
-}
-
-var txsBlock = blockatlas.Block{
-	Number: 12345,
-	ID:     "12345",
-	Txs: []blockatlas.Tx{
-		transferDst1,
-		transferDst2,
-		nativeTransferDst1,
-		nativeTransferDst2,
-	},
-}
-
-func TestGetTxs(t *testing.T) {
-	txs := GetTxs(&txsBlock)
-	assert.Equal(t, len(txs), 4)
-	assert.Equal(t, txs["tbnb1fhr04azuhcj0dulm7ka40y0cqjlafwae9k9gk2"].Size(), 2)
-	assert.Equal(t, txs["tbnb1sylyjw032eajr9cyllp26n04300qzzre38qyv5"].Size(), 1)
-	assert.Equal(t, txs["tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a"].Size(), 2)
-	assert.Equal(t, txs["tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex"].Size(), 2)
-}
-
-func Test_getDirection(t *testing.T) {
-	type args struct {
-		tx      blockatlas.Tx
-		address string
-	}
-	tests := []struct {
-		name string
-		args args
-		want blockatlas.Direction
-	}{
-		{"Test Direction Self",
-			args{
-				blockatlas.Tx{
-					From: "0xfc10cab6a50a1ab10c56983c80cc82afc6559cf1", To: "0xfc10cab6a50a1ab10c56983c80cc82afc6559cf1"},
-				"0xfc10cab6a50a1ab10c56983c80cc82afc6559cf1"}, blockatlas.DirectionSelf,
-		},
-		{"Test Direction Outgoing",
-			args{
-				blockatlas.Tx{
-					From: "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB", To: "0x74c8199372c584DAB8b14c519bc8BC8C622F37b7"},
-				"0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB"}, blockatlas.DirectionOutgoing,
-		},
-		{"Test Direction Incoming",
-			args{
-				blockatlas.Tx{
-					From: "0x74c8199372c584DAB8b14c519bc8BC8C622F37b7", To: "0xfc10cab6a50a1ab10c56983c80cc82afc6559cf1"},
-				"0xfc10cab6a50a1ab10c56983c80cc82afc6559cf1"}, blockatlas.DirectionIncoming,
-		},
-		{"Test UTXO Direction Self",
-			args{
-				blockatlas.Tx{
-					Outputs: []blockatlas.TxOutput{
-						{Address: "DAzruJfMBhd1vcQ13gVVyqb2g1vSEo2d5S", Value: "72934112534"},
-						{Address: "DAzruJfMBhd1vcQ13gVVyqb2g1vSEo2d5S", Value: "500000000"},
-					},
-					Inputs: []blockatlas.TxOutput{
-						{Address: "DAzruJfMBhd1vcQ13gVVyqb2g1vSEo2d5S", Value: "73196112534"},
-					},
-				}, "DAzruJfMBhd1vcQ13gVVyqb2g1vSEo2d5S",
-			}, blockatlas.DirectionSelf,
-		},
-		{"Test UTXO Direction Outgoing",
-			args{
-				blockatlas.Tx{
-					Outputs: []blockatlas.TxOutput{
-						{Address: "3BMEXVshYmWqc8qcQLyBQPgRgAPfogWdJ4", Value: "4471835"},
-						{Address: "324Wmkkbr9uT9xnLUqFvCA3ntqqpqEZQDp", Value: "1600000"},
-						{Address: "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd", Value: "1262899630"},
-					},
-					Inputs: []blockatlas.TxOutput{
-						{Address: "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd", Value: "1268998877"},
-					},
-				}, "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd",
-			}, blockatlas.DirectionOutgoing,
-		},
-		{"Test UTXO Direction Incoming",
-			args{
-				blockatlas.Tx{
-					Outputs: []blockatlas.TxOutput{
-						{Address: "3BMEXVshYmWqc8qcQLyBQPgRgAPfogWdJ4", Value: "4471835"},
-						{Address: "324Wmkkbr9uT9xnLUqFvCA3ntqqpqEZQDp", Value: "1600000"},
-						{Address: "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd", Value: "1262899630"},
-					},
-					Inputs: []blockatlas.TxOutput{
-						{Address: "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd", Value: "1268998877"},
-					},
-				}, "3BMEXVshYmWqc8qcQLyBQPgRgAPfogWdJ4",
-			}, blockatlas.DirectionIncoming,
-		},
-		{"Test NativeTokenTransfer Direction Self",
-			args{
-				blockatlas.Tx{
-					Meta: blockatlas.NativeTokenTransfer{
-						From: "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-						To:   "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-					},
-				}, "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-			}, blockatlas.DirectionSelf,
-		},
-		{"Test NativeTokenTransfer Direction Outgoing",
-			args{
-				blockatlas.Tx{
-					Meta: blockatlas.NativeTokenTransfer{
-						From: "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-						To:   "0x74c8199372c584DAB8b14c519bc8BC8C622F37b7",
-					},
-				}, "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-			}, blockatlas.DirectionOutgoing,
-		},
-		{"Test NativeTokenTransfer Direction Incoming",
-			args{
-				blockatlas.Tx{
-					Meta: blockatlas.NativeTokenTransfer{
-						From: "0x74c8199372c584DAB8b14c519bc8BC8C622F37b7",
-						To:   "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-					},
-				}, "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-			}, blockatlas.DirectionIncoming,
-		},
-		{"Test TokenTransfer Direction Self",
-			args{
-				blockatlas.Tx{
-					Meta: blockatlas.TokenTransfer{
-						From: "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-						To:   "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-					},
-				}, "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-			}, blockatlas.DirectionSelf,
-		},
-		{"Test TokenTransfer Direction Outgoing",
-			args{
-				blockatlas.Tx{
-					Meta: blockatlas.TokenTransfer{
-						From: "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-						To:   "0x74c8199372c584DAB8b14c519bc8BC8C622F37b7",
-					},
-				}, "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-			}, blockatlas.DirectionOutgoing,
-		},
-		{"Test TokenTransfer Direction Incoming",
-			args{
-				blockatlas.Tx{
-					Meta: blockatlas.TokenTransfer{
-						From: "0x74c8199372c584DAB8b14c519bc8BC8C622F37b7",
-						To:   "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-					},
-				}, "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-			}, blockatlas.DirectionIncoming,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := getDirection(tt.args.tx, tt.args.address); got != tt.want {
-				t.Errorf("getDirection() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
-
-func Test_inferUtxoValue(t *testing.T) {
-	type args struct {
-		tx        blockatlas.Tx
-		address   string
-		coinIndex uint
-	}
-	tests := []struct {
-		name       string
-		args       args
-		wantAmount blockatlas.Amount
-	}{
-		{"Test UTXO Direction Self",
-			args{
-				blockatlas.Tx{
-					Outputs: []blockatlas.TxOutput{
-						{Address: "DAzruJfMBhd1vcQ13gVVyqb2g1vSEo2d5S", Value: "72934112534"},
-						{Address: "DAzruJfMBhd1vcQ13gVVyqb2g1vSEo2d5S", Value: "500000000"},
-					},
-					Inputs: []blockatlas.TxOutput{
-						{Address: "DAzruJfMBhd1vcQ13gVVyqb2g1vSEo2d5S", Value: "73196112534"},
-					},
-				}, "DAzruJfMBhd1vcQ13gVVyqb2g1vSEo2d5S", 3,
-			}, blockatlas.Amount("72934112534"),
-		},
-		{"Test UTXO Direction Outgoing",
-			args{
-				blockatlas.Tx{
-					Outputs: []blockatlas.TxOutput{
-						{Address: "3BMEXVshYmWqc8qcQLyBQPgRgAPfogWdJ4", Value: "4471835"},
-						{Address: "324Wmkkbr9uT9xnLUqFvCA3ntqqpqEZQDp", Value: "1600000"},
-						{Address: "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd", Value: "1262899630"},
-					},
-					Inputs: []blockatlas.TxOutput{
-						{Address: "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd", Value: "1268998877"},
-					},
-				}, "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd", 0,
-			}, blockatlas.Amount("4471835"),
-		},
-		{"Test UTXO Direction Incoming",
-			args{
-				blockatlas.Tx{
-					Outputs: []blockatlas.TxOutput{
-						{Address: "3BMEXVshYmWqc8qcQLyBQPgRgAPfogWdJ4", Value: "4471835"},
-						{Address: "324Wmkkbr9uT9xnLUqFvCA3ntqqpqEZQDp", Value: "1600000"},
-						{Address: "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd", Value: "1262899630"},
-					},
-					Inputs: []blockatlas.TxOutput{
-						{Address: "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd", Value: "1268998877"},
-					},
-				}, "3BMEXVshYmWqc8qcQLyBQPgRgAPfogWdJ4", 0,
-			}, blockatlas.Amount("4471835"),
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			expect := blockatlas.Transfer{
-				Value:    tt.wantAmount,
-				Symbol:   coin.Coins[tt.args.coinIndex].Symbol,
-				Decimals: coin.Coins[tt.args.coinIndex].Decimals,
-			}
-			tt.args.tx.Direction = getDirection(tt.args.tx, tt.args.address)
-			if inferUtxoValue(&tt.args.tx, tt.args.address, tt.args.coinIndex); tt.args.tx.Meta != expect {
-				t.Errorf("inferUtxoValue() = %v, want %v", tt.args.tx.Meta, expect)
-			}
-		})
-	}
-}
-
-func TestGetInterval(t *testing.T) {
-	min, _ := time.ParseDuration("2s")
-	max, _ := time.ParseDuration("30s")
-	type args struct {
-		blockTime   int
-		minInterval time.Duration
-		maxInterval time.Duration
-	}
-	tests := []struct {
-		name string
-		args args
-		want time.Duration
-	}{
-		{
-			"test minimum",
-			args{
-				blockTime:   100,
-				minInterval: min,
-				maxInterval: max,
-			},
-			min,
-		}, {
-			"test maximum",
-			args{
-				blockTime:   600000,
-				minInterval: min,
-				maxInterval: max,
-			},
-			max,
-		}, {
-			"test right blocktime",
-			args{
-				blockTime:   5000,
-				minInterval: min,
-				maxInterval: max,
-			},
-			5000 * time.Millisecond,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got := GetInterval(tt.args.blockTime, tt.args.minInterval, tt.args.maxInterval)
-			assert.EqualValues(t, tt.want, got)
-		})
-	}
-}
diff --git a/observer/retry_test.go b/observer/retry_test.go
deleted file mode 100644
index 7b43332..0000000
--- a/observer/retry_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package observer
-
-import (
-	"errors"
-	"github.com/trustwallet/blockatlas/pkg/blockatlas"
-	"testing"
-	"time"
-)
-
-func getBlock(num int64) (*blockatlas.Block, error) {
-	if num == 0 {
-		return nil, errors.New("teste")
-	}
-	return &blockatlas.Block{}, nil
-}
-
-func TestRetry(t *testing.T) {
-	block, err := retry(3, time.Second*1, getBlock, 1)
-	if err != nil {
-		t.Error(err)
-	}
-
-	if block == nil {
-		t.Error("block is nil")
-	}
-}
-
-func TestRetryError(t *testing.T) {
-	now := time.Now()
-	block, err := retry(3, time.Second*1, getBlock, 0)
-	elapsed := time.Since(now)
-	if err == nil {
-		t.Error("retry method need fail")
-	}
-
-	if block != nil {
-		t.Error("block object need be nil")
-	}
-
-	if elapsed > time.Second*6 {
-		t.Error("Thundering Herd prevent doesn't work")
-	}
-}
diff --git a/pkg/blockatlas/tx_test.go b/pkg/blockatlas/tx_test.go
index 8c72c81..f6b77e7 100644
--- a/pkg/blockatlas/tx_test.go
+++ b/pkg/blockatlas/tx_test.go
@@ -1,6 +1,7 @@
 package blockatlas
 
 import (
+	mapset "github.com/deckarep/golang-set"
 	"github.com/stretchr/testify/assert"
 	"github.com/trustwallet/blockatlas/coin"
 	"testing"
@@ -127,3 +128,367 @@ func TestTx_GetUtxoAddresses(t *testing.T) {
 	assert.Equal(t, utxoTransferDst1.GetUtxoAddresses(), []string{"bc1qhn03cww757mnnlpkdvvfkaydxqygm86nvkm92h", "bc1qc7ekqf2t0elfsmtgr2mgd7da2up4vgq8uqk2nh", "bc1qv454wacvnenr3hzzldjqn8cgfltdlxwe96h737", "bc1qjcslq88cht8llqmh3aqshjx9we9msv386jvxl6"})
 	assert.Equal(t, utxoTransferDst2.GetUtxoAddresses(), []string{"bc1q6e8sdxlgc7ekqkqyevtrx8wshfv7sg66z3z6ce", "bc1q7nn4txus4g6fc5v7d2tha35ely8mfpd8qvv6eg", "bc1q2fpry7zwqh575huc9urwfdvjtuvz508wez56ff", "bc1qk3yj6h79qw7tnsg4durc9sd5fpd3qt0p0m8u5p", "bc1qm8836plkzft2rhh23z6j8s9s8fxrzd4zag95z8"})
 }
+
+func Test_getDirection(t *testing.T) {
+	type args struct {
+		tx      Tx
+		address string
+	}
+	tests := []struct {
+		name string
+		args args
+		want Direction
+	}{
+		{"Test Direction Self",
+			args{
+				Tx{
+					From: "0xfc10cab6a50a1ab10c56983c80cc82afc6559cf1", To: "0xfc10cab6a50a1ab10c56983c80cc82afc6559cf1"},
+				"0xfc10cab6a50a1ab10c56983c80cc82afc6559cf1"}, DirectionSelf,
+		},
+		{"Test Direction Outgoing",
+			args{
+				Tx{
+					From: "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB", To: "0x74c8199372c584DAB8b14c519bc8BC8C622F37b7"},
+				"0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB"}, DirectionOutgoing,
+		},
+		{"Test Direction Incoming",
+			args{
+				Tx{
+					From: "0x74c8199372c584DAB8b14c519bc8BC8C622F37b7", To: "0xfc10cab6a50a1ab10c56983c80cc82afc6559cf1"},
+				"0xfc10cab6a50a1ab10c56983c80cc82afc6559cf1"}, DirectionIncoming,
+		},
+		{"Test UTXO Direction Self",
+			args{
+				Tx{
+					Outputs: []TxOutput{
+						{Address: "DAzruJfMBhd1vcQ13gVVyqb2g1vSEo2d5S", Value: "72934112534"},
+						{Address: "DAzruJfMBhd1vcQ13gVVyqb2g1vSEo2d5S", Value: "500000000"},
+					},
+					Inputs: []TxOutput{
+						{Address: "DAzruJfMBhd1vcQ13gVVyqb2g1vSEo2d5S", Value: "73196112534"},
+					},
+				}, "DAzruJfMBhd1vcQ13gVVyqb2g1vSEo2d5S",
+			}, DirectionSelf,
+		},
+		{"Test UTXO Direction Outgoing",
+			args{
+				Tx{
+					Outputs: []TxOutput{
+						{Address: "3BMEXVshYmWqc8qcQLyBQPgRgAPfogWdJ4", Value: "4471835"},
+						{Address: "324Wmkkbr9uT9xnLUqFvCA3ntqqpqEZQDp", Value: "1600000"},
+						{Address: "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd", Value: "1262899630"},
+					},
+					Inputs: []TxOutput{
+						{Address: "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd", Value: "1268998877"},
+					},
+				}, "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd",
+			}, DirectionOutgoing,
+		},
+		{"Test UTXO Direction Incoming",
+			args{
+				Tx{
+					Outputs: []TxOutput{
+						{Address: "3BMEXVshYmWqc8qcQLyBQPgRgAPfogWdJ4", Value: "4471835"},
+						{Address: "324Wmkkbr9uT9xnLUqFvCA3ntqqpqEZQDp", Value: "1600000"},
+						{Address: "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd", Value: "1262899630"},
+					},
+					Inputs: []TxOutput{
+						{Address: "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd", Value: "1268998877"},
+					},
+				}, "3BMEXVshYmWqc8qcQLyBQPgRgAPfogWdJ4",
+			}, DirectionIncoming,
+		},
+		{"Test NativeTokenTransfer Direction Self",
+			args{
+				Tx{
+					Meta: NativeTokenTransfer{
+						From: "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
+						To:   "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
+					},
+				}, "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
+			}, DirectionSelf,
+		},
+		{"Test NativeTokenTransfer Direction Outgoing",
+			args{
+				Tx{
+					Meta: NativeTokenTransfer{
+						From: "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
+						To:   "0x74c8199372c584DAB8b14c519bc8BC8C622F37b7",
+					},
+				}, "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
+			}, DirectionOutgoing,
+		},
+		{"Test NativeTokenTransfer Direction Incoming",
+			args{
+				Tx{
+					Meta: NativeTokenTransfer{
+						From: "0x74c8199372c584DAB8b14c519bc8BC8C622F37b7",
+						To:   "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
+					},
+				}, "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
+			}, DirectionIncoming,
+		},
+		{"Test TokenTransfer Direction Self",
+			args{
+				Tx{
+					Meta: TokenTransfer{
+						From: "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
+						To:   "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
+					},
+				}, "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
+			}, DirectionSelf,
+		},
+		{"Test TokenTransfer Direction Outgoing",
+			args{
+				Tx{
+					Meta: TokenTransfer{
+						From: "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
+						To:   "0x74c8199372c584DAB8b14c519bc8BC8C622F37b7",
+					},
+				}, "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
+			}, DirectionOutgoing,
+		},
+		{"Test TokenTransfer Direction Incoming",
+			args{
+				Tx{
+					Meta: TokenTransfer{
+						From: "0x74c8199372c584DAB8b14c519bc8BC8C622F37b7",
+						To:   "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
+					},
+				}, "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
+			}, DirectionIncoming,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.args.tx.GetTransactionDirection(tt.args.address); got != tt.want {
+				t.Errorf("getTransactionDirection() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_inferUtxoValue(t *testing.T) {
+	type args struct {
+		tx        Tx
+		address   string
+		coinIndex uint
+	}
+	tests := []struct {
+		name       string
+		args       args
+		wantAmount Amount
+	}{
+		{"Test UTXO Direction Self",
+			args{
+				Tx{
+					Outputs: []TxOutput{
+						{Address: "DAzruJfMBhd1vcQ13gVVyqb2g1vSEo2d5S", Value: "72934112534"},
+						{Address: "DAzruJfMBhd1vcQ13gVVyqb2g1vSEo2d5S", Value: "500000000"},
+					},
+					Inputs: []TxOutput{
+						{Address: "DAzruJfMBhd1vcQ13gVVyqb2g1vSEo2d5S", Value: "73196112534"},
+					},
+				}, "DAzruJfMBhd1vcQ13gVVyqb2g1vSEo2d5S", 3,
+			}, Amount("72934112534"),
+		},
+		{"Test UTXO Direction Outgoing",
+			args{
+				Tx{
+					Outputs: []TxOutput{
+						{Address: "3BMEXVshYmWqc8qcQLyBQPgRgAPfogWdJ4", Value: "4471835"},
+						{Address: "324Wmkkbr9uT9xnLUqFvCA3ntqqpqEZQDp", Value: "1600000"},
+						{Address: "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd", Value: "1262899630"},
+					},
+					Inputs: []TxOutput{
+						{Address: "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd", Value: "1268998877"},
+					},
+				}, "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd", 0,
+			}, Amount("4471835"),
+		},
+		{"Test UTXO Direction Incoming",
+			args{
+				Tx{
+					Outputs: []TxOutput{
+						{Address: "3BMEXVshYmWqc8qcQLyBQPgRgAPfogWdJ4", Value: "4471835"},
+						{Address: "324Wmkkbr9uT9xnLUqFvCA3ntqqpqEZQDp", Value: "1600000"},
+						{Address: "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd", Value: "1262899630"},
+					},
+					Inputs: []TxOutput{
+						{Address: "32yRH5tNnFtAXE844wNrHN7Bf3SBcb3Uhd", Value: "1268998877"},
+					},
+				}, "3BMEXVshYmWqc8qcQLyBQPgRgAPfogWdJ4", 0,
+			}, Amount("4471835"),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			expect := Transfer{
+				Value:    tt.wantAmount,
+				Symbol:   coin.Coins[tt.args.coinIndex].Symbol,
+				Decimals: coin.Coins[tt.args.coinIndex].Decimals,
+			}
+			tt.args.tx.Direction = tt.args.tx.GetTransactionDirection(tt.args.address)
+			if tt.args.tx.InferUtxoValue(tt.args.address, tt.args.coinIndex); tt.args.tx.Meta != expect {
+				t.Errorf("inferUtxoValue() = %v, want %v", tt.args.tx.Meta, expect)
+			}
+		})
+	}
+}
+
+// zpub: zpub6r9CEhEkruYbEcu2yQCaRKQ1qufTa4zLrx6ezs31P627UpAepVNBE2td3d3mHnSaXyRbwksRwDJGzLBWQeZPFMut8N3BvXpcwRwEWGEwAnq
+var (
+	btcSet = mapset.NewSet("bc1qfrrncxmf7skye2glyef95xlpmrlmf2e8qlav2l", "bc1qxm90n0rxkadhdkvglev56k60qths73luzlnn7a",
+		"bc1q2sykr9c342mjpm9mwnps8ksk6e35lz75rpdlfe", "bc1qs86ucvr3unce2grvfp77433npy66nzha9w0e3c")
+	btcInputs1  = []TxOutput{{Address: "bc1q2sykr9c342mjpm9mwnps8ksk6e35lz75rpdlfe"}}
+	btcOutputs1 = []TxOutput{{Address: "bc1q6wf7tj62f0uwr6almah3666th2ejefdg72ek6t"}}
+	btcInputs2  = []TxOutput{{
+		Address: "3CgvDkzcJ7yMZe75jNBem6Bj6nkMAWwMEf"},
+		{Address: "3LyzYcB54pm9EAMmzXpFfb1kzEDAFvqBgT"},
+		{Address: "3Q6DYour5q5WdMhyXsyPgBeAqPCXchzCsF"},
+		{Address: "3JZZM1rwst7G5izxbFL7KNvy7ZiZ47SVqG"}}
+	btcOutputs2 = []TxOutput{
+		{Address: "139f1CrnLWvVajGzs3ZtpQhbGWxM599sho"},
+		{Address: "3LyzYcB54pm9EAMmzXpFfb1kzEDAFvqBgT"},
+		{Address: "bc1q9mx5tm66zs7epa4skvyuf2vfuwmtnlttj74cnl"},
+		{Address: "3JZZM1rwst7G5izxbFL7KNvy7ZiZ47SVqG"}}
+
+	dogeSet     = mapset.NewSet("DB49sNjVdxyREXEBEzUV54TrQYYpvi3Be7")
+	dogeInputs  = []TxOutput{{Address: "DAukM5pPtGdbPxMX1u2LYHoyhbDhEFHbnH"}}
+	dogeOutputs = []TxOutput{{Address: "DB49sNjVdxyREXEBEzUV54TrQYYpvi3Be7"}, {Address: "DAukM5pPtGdbPxMX1u2LYHoyhbDhEFHbnH"}}
+)
+
+func TestInferDirection(t *testing.T) {
+	var tests = []struct {
+		AddressSet mapset.Set
+		Inputs     []TxOutput
+		Outputs    []TxOutput
+		Expected   Direction
+		Coin       uint
+	}{
+		{
+			btcSet,
+			btcInputs1,
+			btcOutputs1,
+			DirectionOutgoing,
+			coin.BTC,
+		},
+		{
+			btcSet,
+			btcInputs2,
+			btcOutputs2,
+			DirectionIncoming,
+			coin.BTC,
+		},
+		{
+			dogeSet,
+			dogeInputs,
+			dogeOutputs,
+			DirectionIncoming,
+			coin.DOGE,
+		},
+	}
+
+	for _, test := range tests {
+		tx := Tx{
+			Inputs:  test.Inputs,
+			Outputs: test.Outputs,
+		}
+
+		direction := InferDirection(&tx, test.AddressSet)
+		if direction != test.Expected {
+			t.Errorf("direction is not %s", test.Expected)
+		}
+	}
+}
+
+var (
+	transferDstOne = Tx{
+		ID:     "1681EE543FB4B5A628EF21D746E031F018E226D127044A4F9BA5EE2542A44555",
+		Coin:   coin.BNB,
+		From:   "tbnb1fhr04azuhcj0dulm7ka40y0cqjlafwae9k9gk2",
+		To:     "tbnb1sylyjw032eajr9cyllp26n04300qzzre38qyv5",
+		Fee:    "125000",
+		Date:   1555049867,
+		Block:  7761368,
+		Status: StatusCompleted,
+		Memo:   "test",
+		Meta: Transfer{
+			Value:    "10000000000000",
+			Decimals: 8,
+			Symbol:   "BNB",
+		},
+	}
+
+	transferDst2 = Tx{
+		ID:     "1681EE543FB4B5A628EF21D746E031F018E226D127044A4F9BA5EE2542A44556",
+		Coin:   coin.BNB,
+		From:   "tbnb1fhr04azuhcj0dulm7ka40y0cqjlafwae9k9gk2",
+		To:     "tbnb1fhr04azuhcj0dulm7ka40y0cqjlafwae9k9gk2",
+		Fee:    "125000",
+		Date:   1555049867,
+		Block:  7761368,
+		Status: StatusCompleted,
+		Memo:   "test",
+		Meta: Transfer{
+			Value:    "10000000000000",
+			Decimals: 8,
+			Symbol:   "BNB",
+		},
+	}
+
+	nativeTransferDstOne = Tx{
+		ID:     "95CF63FAA27579A9B6AF84EF8B2DFEAC29627479E9C98E7F5AE4535E213FA4C9",
+		Coin:   coin.BNB,
+		From:   "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
+		To:     "tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex",
+		Fee:    "125000",
+		Date:   1555117625,
+		Block:  7928667,
+		Status: StatusCompleted,
+		Memo:   "test",
+		Meta: NativeTokenTransfer{
+			TokenID:  "YLC-D8B",
+			Symbol:   "YLC",
+			Value:    "210572645",
+			Decimals: 8,
+			From:     "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
+			To:       "tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex",
+		},
+	}
+
+	nativeTransferDst2 = Tx{
+		ID:     "95CF63FAA27579A9B6AF84EF8B2DFEAC29627479E9C98E7F5AE4535E213FA4D0",
+		Coin:   coin.BNB,
+		From:   "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
+		To:     "tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex",
+		Fee:    "125000",
+		Date:   1555117625,
+		Block:  7928667,
+		Status: StatusCompleted,
+		Memo:   "test",
+		Meta: NativeTokenTransfer{
+			TokenID:  "YLC-D8B",
+			Symbol:   "YLC",
+			Value:    "210572645",
+			Decimals: 8,
+			From:     "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
+			To:       "tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex",
+		},
+	}
+)
+
+func TestGetTxsTx(t *testing.T) {
+	tx := Txs{
+		transferDstOne,
+		transferDst2,
+		nativeTransferDstOne,
+		nativeTransferDst2,
+	}
+	txs := tx.GetTransactionsMap()
+	assert.Equal(t, len(txs.Map), 4)
+	assert.Equal(t, txs.Map["tbnb1fhr04azuhcj0dulm7ka40y0cqjlafwae9k9gk2"].Size(), 2)
+	assert.Equal(t, txs.Map["tbnb1sylyjw032eajr9cyllp26n04300qzzre38qyv5"].Size(), 1)
+	assert.Equal(t, txs.Map["tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a"].Size(), 2)
+	assert.Equal(t, txs.Map["tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex"].Size(), 2)
+}
diff --git a/platform/bitcoin/transaction_test.go b/platform/bitcoin/transaction_test.go
index e7a18dd..fd93c95 100644
--- a/platform/bitcoin/transaction_test.go
+++ b/platform/bitcoin/transaction_test.go
@@ -269,70 +269,6 @@ func TestNormalizeTransfer(t *testing.T) {
 	}
 }
 
-// zpub: zpub6r9CEhEkruYbEcu2yQCaRKQ1qufTa4zLrx6ezs31P627UpAepVNBE2td3d3mHnSaXyRbwksRwDJGzLBWQeZPFMut8N3BvXpcwRwEWGEwAnq
-var btcSet = mapset.NewSet("bc1qfrrncxmf7skye2glyef95xlpmrlmf2e8qlav2l", "bc1qxm90n0rxkadhdkvglev56k60qths73luzlnn7a",
-	"bc1q2sykr9c342mjpm9mwnps8ksk6e35lz75rpdlfe", "bc1qs86ucvr3unce2grvfp77433npy66nzha9w0e3c")
-var btcInputs1 = []blockatlas.TxOutput{{Address: "bc1q2sykr9c342mjpm9mwnps8ksk6e35lz75rpdlfe"}}
-var btcOutputs1 = []blockatlas.TxOutput{{Address: "bc1q6wf7tj62f0uwr6almah3666th2ejefdg72ek6t"}}
-var btcInputs2 = []blockatlas.TxOutput{{
-	Address: "3CgvDkzcJ7yMZe75jNBem6Bj6nkMAWwMEf"},
-	{Address: "3LyzYcB54pm9EAMmzXpFfb1kzEDAFvqBgT"},
-	{Address: "3Q6DYour5q5WdMhyXsyPgBeAqPCXchzCsF"},
-	{Address: "3JZZM1rwst7G5izxbFL7KNvy7ZiZ47SVqG"}}
-var btcOutputs2 = []blockatlas.TxOutput{
-	{Address: "139f1CrnLWvVajGzs3ZtpQhbGWxM599sho"},
-	{Address: "3LyzYcB54pm9EAMmzXpFfb1kzEDAFvqBgT"},
-	{Address: "bc1q9mx5tm66zs7epa4skvyuf2vfuwmtnlttj74cnl"},
-	{Address: "3JZZM1rwst7G5izxbFL7KNvy7ZiZ47SVqG"}}
-
-var dogeSet = mapset.NewSet("DB49sNjVdxyREXEBEzUV54TrQYYpvi3Be7")
-var dogeInputs = []blockatlas.TxOutput{{Address: "DAukM5pPtGdbPxMX1u2LYHoyhbDhEFHbnH"}}
-var dogeOutputs = []blockatlas.TxOutput{{Address: "DB49sNjVdxyREXEBEzUV54TrQYYpvi3Be7"}, {Address: "DAukM5pPtGdbPxMX1u2LYHoyhbDhEFHbnH"}}
-
-func TestInferDirection(t *testing.T) {
-	var tests = []struct {
-		AddressSet mapset.Set
-		Inputs     []blockatlas.TxOutput
-		Outputs    []blockatlas.TxOutput
-		Expected   blockatlas.Direction
-		Coin       uint
-	}{
-		{
-			btcSet,
-			btcInputs1,
-			btcOutputs1,
-			blockatlas.DirectionOutgoing,
-			coin.BTC,
-		},
-		{
-			btcSet,
-			btcInputs2,
-			btcOutputs2,
-			blockatlas.DirectionIncoming,
-			coin.BTC,
-		},
-		{
-			dogeSet,
-			dogeInputs,
-			dogeOutputs,
-			blockatlas.DirectionIncoming,
-			coin.DOGE,
-		},
-	}
-
-	for _, test := range tests {
-		tx := blockatlas.Tx{
-			Inputs:  test.Inputs,
-			Outputs: test.Outputs,
-		}
-
-		direction := InferDirection(&tx, test.AddressSet)
-		if direction != test.Expected {
-			t.Errorf("direction is not %s", test.Expected)
-		}
-	}
-}
-
 func TestTransactionStatus(t *testing.T) {
 	var tests = []struct {
 		Tx       Transaction
diff --git a/platform/solana/stake_test.go b/platform/solana/stake_test.go
new file mode 100644
index 0000000..4817d23
--- /dev/null
+++ b/platform/solana/stake_test.go
@@ -0,0 +1,151 @@
+package solana
+
+import (
+	"encoding/json"
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const currentValidators = `
+[
+  {
+    "activatedStake": 3733867423940,
+    "commission": 100,
+    "epochCredits": [
+      [70,524224,516032],[71,532416,524224],[72,540608,532416],[73,548800,540608],[74,556992,548800],[75,565184,556992],[76,573376,565184],[77,581568,573376],[78,589760,581568],[79,597952,589760],[80,601055,597952]
+    ],
+    "epochVoteAccount": true,
+    "lastVote": 601085,
+    "nodePubkey": "boot1Z6jb15CLqpaMTn2CxktktwZpRAVAgHZEW6SxQ7",
+    "rootSlot": 601054,
+    "votePubkey": "2Afu38M1KaSfDBpjZjnJb9BSWP6YkBkoPiBfnFedD7JW"
+  },
+  {
+    "activatedStake": 10540011934,
+    "commission": 100 ,
+    "epochCredits": [
+      [78,4760,0],[79,12952,4760],[80,16055,12952]
+    ],
+    "epochVoteAccount": true,
+    "lastVote": 601085,
+    "nodePubkey": "B52Da5MCyTcyVJEsR9RUnbf715YuBAJMxCEEPzyZXgvY",
+    "rootSlot": 601054,
+    "votePubkey": "5CgQubGD1uwodwCe5UXDADbC69SiqXR8qq6pDMSm7ut5"
+  }
+]`
+
+var expectedValidators = []blockatlas.Validator{
+	blockatlas.Validator{
+		Status: true,
+		ID:     "2Afu38M1KaSfDBpjZjnJb9BSWP6YkBkoPiBfnFedD7JW",
+		Details: blockatlas.StakingDetails{
+			Reward:        blockatlas.StakingReward{Annual: 0},
+			MinimumAmount: blockatlas.Amount("2282881"),
+			LockTime:      0,
+			Type:          blockatlas.DelegationTypeDelegate,
+		},
+	},
+	blockatlas.Validator{
+		Status: true,
+		ID:     "5CgQubGD1uwodwCe5UXDADbC69SiqXR8qq6pDMSm7ut5",
+		Details: blockatlas.StakingDetails{
+			Reward:        blockatlas.StakingReward{Annual: 0},
+			MinimumAmount: blockatlas.Amount("2282881"),
+			LockTime:      0,
+			Type:          blockatlas.DelegationTypeDelegate,
+		},
+	},
+}
+
+func TestNormalizeValidator(t *testing.T) {
+	var validators []VoteAccount
+	minimumBalanceForRentExemption := uint64(2282880) // Arbitrary value; minimumBalanceForRentExemption is calculated from account data size
+	err := json.Unmarshal([]byte(currentValidators), &validators)
+	assert.Nil(t, err)
+	for i, v := range validators {
+		result := normalizeValidator(v, minimumBalanceForRentExemption)
+		assert.Equal(t, expectedValidators[i], result)
+	}
+}
+
+var keyedStakeAccount = KeyedAccount{
+	Account: Account{
+		Data:       "mrWMHx6j3BkmepJy67XLycC7LVeiq2NBESfV2YNmZvY62xT5jTgKnMzRBQheYtAuajncAniTEmU8QxgkpnytnXynTrMSJN4p6ihefU5cobkyCeSwMKugKGuBbyDLjQoUMu6BUKjDTFvjpJUHFCgz1Vaa8HSVscUqqRcioByf3owMUwHmEYsF8vuouLAqEQmo61wFkKfZELxLrhBbi2PQQZucryrnNDKXV4DY3oegLy9aMnMDZUeoDtDPPiJeM2F1Trh8ZkH1sQL6sQ5V",
+		Executable: false,
+		Lamports:   100000000000,
+		Owner:      "Stake11111111111111111111111111111111111111",
+		RentEpoch:  80,
+	},
+	Pubkey: "EgR17fgGmwQQaMZPsuJdk9oHw2xY8TJQj3Bp44o24mar",
+}
+
+var stakeState = StakeState{
+	State:                2,
+	RentExemptReserve:    2282880,
+	AuthorizedStaker:     arrayOfPubkey("B52Da5MCyTcyVJEsR9RUnbf715YuBAJMxCEEPzyZXgvY"),
+	AuthorizedWithdrawer: arrayOfPubkey("B52Da5MCyTcyVJEsR9RUnbf715YuBAJMxCEEPzyZXgvY"),
+	UnixTimestamp:        0,
+	LockupEpoch:          0,
+	Custodian:            arrayOfPubkey("11111111111111111111111111111111"),
+	VoterPubkey:          arrayOfPubkey("5CgQubGD1uwodwCe5UXDADbC69SiqXR8qq6pDMSm7ut5"),
+	Stake:                99997717120,
+	ActivationEpoch:      79,
+	DeactivationEpoch:    ^uint64(0),
+	WarmupCooldownRate:   0.25,
+	CreditsObserved:      21143,
+}
+
+func TestParseStakeData(t *testing.T) {
+
+	result, err := parseStakeData(keyedStakeAccount.Account)
+	assert.Nil(t, err)
+	assert.Equal(t, result, stakeState)
+}
+
+func TestIsAuthorized(t *testing.T) {
+	address := "B52Da5MCyTcyVJEsR9RUnbf715YuBAJMxCEEPzyZXgvY"
+	account, _ := parseStakeData(keyedStakeAccount.Account)
+	assert.True(t, isAuthorized(account, address))
+
+	address = "BADaddresscyVJEsR9RUnbf715YuBAJMxCEEPzyZXgvY"
+	assert.False(t, isAuthorized(account, address))
+}
+
+var stakeValidator = blockatlas.StakeValidator{
+	ID:     "5CgQubGD1uwodwCe5UXDADbC69SiqXR8qq6pDMSm7ut5",
+	Status: true,
+	Info: blockatlas.StakeValidatorInfo{
+		Name:        "Certus One",
+		Description: "Stake and earn rewards with the most secure and stable validator. Winner of the Game of Stakes. Operated by Certus One Inc. By delegating, you confirm that you are aware of the risk of slashing and that Certus One Inc is not liable for any potential damages to your investment.",
+		Image:       "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/solana/validators/assets/2Afu38M1KaSfDBpjZjnJb9BSWP6YkBkoPiBfnFedD7JW/logo.png",
+		Website:     "https://certus.one",
+	},
+	Details: getDetails(),
+}
+
+var validatorMap = blockatlas.ValidatorMap{
+	"5CgQubGD1uwodwCe5UXDADbC69SiqXR8qq6pDMSm7ut5": stakeValidator,
+}
+
+var epochInfo = EpochInfo{
+	AbsoluteSlot: 165763,
+	Epoch:        80,
+	SlotIndex:    1923,
+	SlotsInEpoch: 2048,
+}
+
+var delegation = blockatlas.DelegationsPage{
+	{
+		Delegator: stakeValidator,
+		Value:     "99997717120",
+		Status:    blockatlas.DelegationStatusActive,
+	},
+}
+
+func TestNormalizeDelegations(t *testing.T) {
+	result, err := NormalizeDelegations([]StakeState{stakeState}, validatorMap, epochInfo)
+	assert.NoError(t, err)
+	assert.Equal(t, delegation, result)
+}
diff --git a/platform/theta/transaction_test.go b/platform/theta/transaction_test.go
index 130c177..956f395 100644
--- a/platform/theta/transaction_test.go
+++ b/platform/theta/transaction_test.go
@@ -5,10 +5,11 @@ import (
 	"encoding/json"
 	"github.com/trustwallet/blockatlas/coin"
 	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"gotest.tools/assert"
 	"testing"
 )
 
-var transferReceipt = `{
+var thetaTransfer = `{
 	"hash": "0x413d8423fd1e6df99fc57f425dfd58c791c877657b364c62c15905ade5114a70",
 	"data": {
 		"fee": {
@@ -52,7 +53,7 @@ var tFuelTransfer = `
 		},
 		"inputs": [
 			{
-				"address": "0xac0eeb6ee3e32e2c74e14ac74155063e4f4f981f",
+				"address": "0x0a7d7141e9abe5d1c760cffa1129c6eb94f35a2a",
 				"coins": {
 					"thetawei": "0",
 					"tfuelwei": "44326000000000000"
@@ -62,7 +63,7 @@ var tFuelTransfer = `
 		],
 		"outputs": [
 			{
-				"address": "0x0a7d7141e9abe5d1c760cffa1129c6eb94f35a2a",
+				"address": "0xac0eeb6ee3e32e2c74e14ac74155063e4f4f981f",
 				"coins": {
 					"thetawei": "0",
 					"tfuelwei": "44324000000000000"
@@ -78,16 +79,17 @@ var tFuelTransfer = `
 `
 
 var expectedTransferTrx = blockatlas.Tx{
-	ID:       "0x413d8423fd1e6df99fc57f425dfd58c791c877657b364c62c15905ade5114a70",
-	Coin:     coin.THETA,
-	From:     "0xac0eeb6ee3e32e2c74e14ac74155063e4f4f981f",
-	To:       "0x0a7d7141e9abe5d1c760cffa1129c6eb94f35a2a",
-	Fee:      "2000000000000",
-	Date:     1557136781,
-	Type:     "transfer",
-	Status:   blockatlas.StatusCompleted,
-	Block:    700321,
-	Sequence: 43,
+	ID:        "0x413d8423fd1e6df99fc57f425dfd58c791c877657b364c62c15905ade5114a70",
+	Coin:      coin.THETA,
+	From:      "0xac0eeb6ee3e32e2c74e14ac74155063e4f4f981f",
+	To:        "0x0a7d7141e9abe5d1c760cffa1129c6eb94f35a2a",
+	Fee:       "2000000000000",
+	Date:      1557136781,
+	Type:      "transfer",
+	Status:    blockatlas.StatusCompleted,
+	Block:     700321,
+	Sequence:  43,
+	Direction: blockatlas.DirectionOutgoing,
 	Meta: blockatlas.Transfer{
 		Value:    "4000000000000000000",
 		Symbol:   "THETA",
@@ -96,24 +98,25 @@ var expectedTransferTrx = blockatlas.Tx{
 }
 
 var expectedTfuelTransfer = blockatlas.Tx{
-	ID:       "0x558cb5ec877119c2c84a677277efb5b3059adb830c6e74971b3dbe93221b7132",
-	Coin:     coin.THETA,
-	From:     "0xac0eeb6ee3e32e2c74e14ac74155063e4f4f981f",
-	To:       "0x0a7d7141e9abe5d1c760cffa1129c6eb94f35a2a",
-	Fee:      "2000000000000",
-	Date:     1557136821,
-	Type:     blockatlas.TxNativeTokenTransfer,
-	Status:   blockatlas.StatusCompleted,
-	Sequence: 44,
-	Block:    700327,
+	ID:        "0x558cb5ec877119c2c84a677277efb5b3059adb830c6e74971b3dbe93221b7132",
+	Coin:      coin.THETA,
+	From:      "0x0a7d7141e9abe5d1c760cffa1129c6eb94f35a2a",
+	To:        "0xac0eeb6ee3e32e2c74e14ac74155063e4f4f981f",
+	Fee:       "2000000000000",
+	Date:      1557136821,
+	Type:      blockatlas.TxNativeTokenTransfer,
+	Status:    blockatlas.StatusCompleted,
+	Sequence:  44,
+	Block:     700327,
+	Direction: blockatlas.DirectionIncoming,
 	Meta: blockatlas.NativeTokenTransfer{
 		Name:     "Theta Fuel",
 		Symbol:   "TFUEL",
 		TokenID:  "tfuel",
 		Decimals: 18,
 		Value:    "44324000000000000",
-		From:     "0xac0eeb6ee3e32e2c74e14ac74155063e4f4f981f",
-		To:       "0x0a7d7141e9abe5d1c760cffa1129c6eb94f35a2a",
+		From:     "0x0a7d7141e9abe5d1c760cffa1129c6eb94f35a2a",
+		To:       "0xac0eeb6ee3e32e2c74e14ac74155063e4f4f981f",
 	},
 }
 
@@ -124,7 +127,7 @@ func TestNormalize(t *testing.T) {
 		Token       string
 		Expected    blockatlas.Tx
 	}{
-		{transferReceipt, "0xac0eeb6ee3e32e2c74e14ac74155063e4f4f981f", "", expectedTransferTrx},
+		{thetaTransfer, "0xac0eeb6ee3e32e2c74e14ac74155063e4f4f981f", "", expectedTransferTrx},
 		{tFuelTransfer, "0xac0eeb6ee3e32e2c74e14ac74155063e4f4f981f", "tfuel", expectedTfuelTransfer},
 	}
 
@@ -160,3 +163,28 @@ func TestNormalize(t *testing.T) {
 		}
 	}
 }
+
+func TestGetDirection(t *testing.T) {
+	var addrChecksum = "0x42616C88c7076FbE6e1596b734c13356b5A508a4"
+	var addr = "0x42616c88c7076fbe6e1596b734c13356b5a508a4"
+	var otherAddr = "0x8665a3cbc02ff17cf9d712e8a20f3d7bb1444517"
+
+	var tests = []struct {
+		address     string
+		expectedDir blockatlas.Direction
+		trxInput    Input
+		trxOutput   Output
+	}{
+		{address: addrChecksum, expectedDir: blockatlas.DirectionSelf, trxInput: Input{Address: addr}, trxOutput: Output{Address: addr}},
+		{address: addrChecksum, expectedDir: blockatlas.DirectionOutgoing, trxInput: Input{Address: addr}, trxOutput: Output{Address: otherAddr}},
+		{address: addrChecksum, expectedDir: blockatlas.DirectionIncoming, trxInput: Input{Address: otherAddr}, trxOutput: Output{Address: addr}},
+	}
+
+	for _, test := range tests {
+		actualDir, err := getDirection(test.address, test.trxInput, test.trxOutput)
+		if err != nil {
+			t.Fatal(err)
+		}
+		assert.Equal(t, test.expectedDir, actualDir, test.expectedDir)
+	}
+}
diff --git a/services/observer/notifier/notifier_test.go b/services/observer/notifier/notifier_test.go
new file mode 100644
index 0000000..882343e
--- /dev/null
+++ b/services/observer/notifier/notifier_test.go
@@ -0,0 +1,54 @@
+package notifier
+
+import (
+	"github.com/stretchr/testify/assert"
+	"testing"
+	"time"
+)
+
+func TestGetInterval(t *testing.T) {
+	min, _ := time.ParseDuration("2s")
+	max, _ := time.ParseDuration("30s")
+	type args struct {
+		blockTime   int
+		minInterval time.Duration
+		maxInterval time.Duration
+	}
+	tests := []struct {
+		name string
+		args args
+		want time.Duration
+	}{
+		{
+			"test minimum",
+			args{
+				blockTime:   100,
+				minInterval: min,
+				maxInterval: max,
+			},
+			min,
+		}, {
+			"test maximum",
+			args{
+				blockTime:   600000,
+				minInterval: min,
+				maxInterval: max,
+			},
+			max,
+		}, {
+			"test right blocktime",
+			args{
+				blockTime:   5000,
+				minInterval: min,
+				maxInterval: max,
+			},
+			5000 * time.Millisecond,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := GetInterval(tt.args.blockTime, tt.args.minInterval, tt.args.maxInterval)
+			assert.EqualValues(t, tt.want, got)
+		})
+	}
+}
diff --git a/services/observer/parser/parser_test.go b/services/observer/parser/parser_test.go
new file mode 100644
index 0000000..38457c6
--- /dev/null
+++ b/services/observer/parser/parser_test.go
@@ -0,0 +1,240 @@
+package parser
+
+import (
+	"errors"
+	"fmt"
+	"github.com/alicebob/miniredis"
+	"github.com/stretchr/testify/assert"
+	"github.com/trustwallet/blockatlas/coin"
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"github.com/trustwallet/blockatlas/pkg/logger"
+	"github.com/trustwallet/blockatlas/storage"
+	"sync"
+	"testing"
+	"time"
+)
+
+var (
+	wantedMockedNumber int64
+
+	block = blockatlas.Block{
+		Number: 110,
+		ID:     "",
+		Txs: []blockatlas.Tx{
+			{
+				ID:     "95CF63FAA27579A9B6AF84EF8B2DFEAC29627479E9C98E7F5AE4535E213FA4C9",
+				Coin:   coin.BNB,
+				From:   "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
+				To:     "tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex",
+				Fee:    "125000",
+				Date:   1555117625,
+				Block:  7928667,
+				Status: blockatlas.StatusCompleted,
+				Memo:   "test",
+				Meta: blockatlas.NativeTokenTransfer{
+					TokenID:  "YLC-D8B",
+					Symbol:   "YLC",
+					Value:    "210572645",
+					Decimals: 8,
+					From:     "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
+					To:       "tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex",
+				},
+			},
+		},
+	}
+
+	txs = blockatlas.Txs{
+		{
+			ID:     "95CF63FAA27579A9B6AF84EF8B2DFEAC29627479E9C98E7F5AE4535E213FA4C9",
+			Coin:   coin.BNB,
+			From:   "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
+			To:     "tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex",
+			Fee:    "125000",
+			Date:   1555117625,
+			Block:  7928667,
+			Status: blockatlas.StatusCompleted,
+			Memo:   "test",
+			Meta: blockatlas.NativeTokenTransfer{
+				TokenID:  "YLC-D8B",
+				Symbol:   "YLC",
+				Value:    "210572645",
+				Decimals: 8,
+				From:     "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
+				To:       "tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex",
+			},
+		},
+	}
+)
+
+func Test_GetBlocksIntervalToFetch(t *testing.T) {
+
+	params := Params{
+		ParsingBlocksInterval: time.Minute,
+		BacklogCount:          10,
+		MaxBacklogBlocks:      100,
+		Storage:               getMockedRedis(t),
+		Api:                   getMockedBlockAPI(),
+	}
+	latestParsedBlock := int64(100)
+	wantedMockedNumber = 110
+	lastParsedBlock, currentBlock, err := GetBlocksIntervalToFetch(params)
+	assert.Nil(t, err)
+	assert.Equal(t, wantedMockedNumber, currentBlock)
+	assert.Equal(t, latestParsedBlock, lastParsedBlock)
+}
+
+func TestFetchBlocks(t *testing.T) {
+	blocks := FetchBlocks(getMockedBlockAPI(), 0, 100)
+	assert.Equal(t, len(blocks), 100)
+}
+
+func TestSaveLastParsedBlock(t *testing.T) {
+	blocks := make([]blockatlas.Block, 0)
+	blocks = append(blocks, block)
+	s := getMockedRedis(t)
+
+	params := Params{
+		ParsingBlocksInterval: time.Minute,
+		BacklogCount:          10,
+		MaxBacklogBlocks:      100,
+		Storage:               s,
+		Api:                   getMockedBlockAPI(),
+	}
+	err := SaveLastParsedBlock(params, blocks)
+	assert.Nil(t, err)
+
+	lastParsedBlock, err := s.GetLastParsedBlockNumber(getMockedBlockAPI().Coin().ID)
+	assert.Nil(t, err)
+	assert.Equal(t, lastParsedBlock, int64(110))
+}
+
+func TestParser_ConvertToBatch(t *testing.T) {
+	blocks := []blockatlas.Block{block, block, block, block}
+	txs := ConvertToBatch(blocks)
+	assert.Equal(t, 4, len(txs))
+
+	empty := []blockatlas.Block{}
+	txsEmpty := ConvertToBatch(empty)
+	assert.Equal(t, 0, len(txsEmpty))
+}
+
+func TestParser_add(t *testing.T) {
+	blocks := []blockatlas.Block{block, block, block, block}
+	txs := ConvertToBatch(blocks)
+
+	batch := transactionsBatch{
+		Mutex: sync.Mutex{},
+		Txs:   txs,
+	}
+
+	batch.fillBatch(txs)
+	assert.Equal(t, 8, len(batch.Txs))
+
+	batch.fillBatch(nil)
+	assert.Equal(t, 8, len(batch.Txs))
+}
+
+func TestParser_getBlockByNumberWithRetry(t *testing.T) {
+	block, err := getBlockByNumberWithRetry(3, time.Millisecond*1, getBlock, 1)
+	if err != nil {
+		t.Error(err)
+	}
+
+	if block == nil {
+		t.Error("block is nil")
+	}
+}
+
+func TestParser_getBlockByNumberWithRetry_Error(t *testing.T) {
+	now := time.Now()
+	block, err := getBlockByNumberWithRetry(2, time.Millisecond*2, getBlock, 0)
+	elapsed := time.Since(now)
+	if err == nil {
+		t.Error("getBlockByNumberWithRetry method need fail")
+	}
+
+	if block != nil {
+		t.Error("block object need be nil")
+	}
+
+	if elapsed > time.Millisecond*7 {
+		t.Error("Thundering Herd prevent doesn't work")
+	}
+}
+
+func getBlock(num int64) (*blockatlas.Block, error) {
+	if num == 0 {
+		return nil, errors.New("test")
+	}
+	return &blockatlas.Block{}, nil
+}
+
+func getMockedBlockAPI() blockatlas.BlockAPI {
+	p := Platform{CoinIndex: 60}
+	return &p
+}
+
+func getMockedRedis(t *testing.T) *storage.Storage {
+	s, err := miniredis.Run()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	cache := storage.New()
+	err = cache.Init(fmt.Sprintf("redis://%s", s.Addr()))
+	if err != nil {
+		logger.Fatal(err)
+	}
+	return cache
+}
+
+type Platform struct {
+	CoinIndex uint
+}
+
+func (p *Platform) CurrentBlockNumber() (int64, error) {
+	return wantedMockedNumber, nil
+}
+
+func (p *Platform) Coin() coin.Coin {
+	return coin.Coins[p.CoinIndex]
+}
+
+func (p *Platform) GetBlockByNumber(num int64) (*blockatlas.Block, error) {
+	return &blockatlas.Block{}, nil
+}
+
+func TestGetTxBatches(t *testing.T) {
+	txs := make(blockatlas.Txs, 10000)
+	batches := getTxsBatches(txs, 1000)
+	assert.Len(t, batches, 10)
+	batches = getTxsBatches(txs, 100)
+	assert.Len(t, batches, 100)
+	batches = getTxsBatches(txs, 500)
+	assert.Len(t, batches, 20)
+
+	txs = make(blockatlas.Txs, 3800)
+	batches = getTxsBatches(txs, 100)
+	assert.Len(t, batches, 38)
+	batches = getTxsBatches(txs, 1000)
+	assert.Len(t, batches, 4)
+
+	txs = make(blockatlas.Txs, 5000)
+	batches = getTxsBatches(txs, 10000)
+	assert.Len(t, batches, 1)
+
+	txs = make(blockatlas.Txs, 0)
+	batches = getTxsBatches(txs, 100)
+	assert.Len(t, batches, 0)
+
+	txs = make(blockatlas.Txs, 0)
+	batches = getTxsBatches(txs, 100)
+	assert.Len(t, batches, 0)
+
+	batches = getTxsBatches(nil, 100)
+	assert.Len(t, batches, 0)
+
+	txs = make(blockatlas.Txs, 1000000)
+	batches = getTxsBatches(txs, 5000)
+	assert.Len(t, batches, 200)
+}
diff --git a/storage/addresses_test.go b/storage/addresses_test.go
new file mode 100644
index 0000000..52ef385
--- /dev/null
+++ b/storage/addresses_test.go
@@ -0,0 +1,347 @@
+package storage
+
+import (
+	"fmt"
+	"github.com/alicebob/miniredis"
+	"github.com/stretchr/testify/assert"
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"github.com/trustwallet/blockatlas/pkg/logger"
+	"testing"
+)
+
+func TestStorage_Lookup(t *testing.T) {
+	s := initStorage(t)
+	assert.NotNil(t, s)
+
+	type fields struct {
+		coin      int
+		addresses []string
+	}
+	tests := []struct {
+		name          string
+		fields        fields
+		wantData      []blockatlas.Subscription
+		wantCondition bool
+	}{
+		{"test all guids found",
+			fields{coin: 60, addresses: []string{"1", "2", "3"}},
+			[]blockatlas.Subscription{
+				{Coin: 60, Address: "1", GUID: "1"},
+				{Coin: 60, Address: "2", GUID: "2"},
+				{Coin: 60, Address: "3", GUID: "3"},
+			},
+			true,
+		},
+		{"test not found",
+			fields{coin: 60, addresses: []string{"1", "4", "3"}},
+			[]blockatlas.Subscription{
+				{Coin: 60, Address: "1", GUID: "1"},
+				{Coin: 60, Address: "2", GUID: "2"},
+				{Coin: 60, Address: "3", GUID: "3"},
+			},
+			false,
+		},
+	}
+
+	for _, tt := range tests {
+		for i, a := range tt.fields.addresses {
+			key := getSubscriptionKey(uint(tt.fields.coin), a)
+			err := s.AddHM(ATLAS_OBSERVER, key, []string{tt.wantData[i].GUID})
+			if err != nil {
+				t.Fatal(err)
+			}
+		}
+
+		t.Run(tt.name, func(t *testing.T) {
+			if got, err := s.FindSubscriptions(uint(tt.fields.coin), tt.fields.addresses); !(isEqualSubscriptions(got, tt.wantData) == tt.wantCondition) || err != nil {
+				t.Fatal(got)
+			}
+		})
+	}
+}
+
+func TestStorage_Lookup_MultipleGUIDs(t *testing.T) {
+	s := initStorage(t)
+	assert.NotNil(t, s)
+
+	want := []blockatlas.Subscription{
+		{Coin: 60, Address: "1", GUID: "1"},
+		{Coin: 60, Address: "2", GUID: "2"},
+		{Coin: 60, Address: "2", GUID: "3"},
+		{Coin: 60, Address: "3", GUID: "3"},
+	}
+
+	key1 := getSubscriptionKey(uint(60), "1")
+	err := s.AddHM(ATLAS_OBSERVER, key1, []string{"1"})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	key2 := getSubscriptionKey(uint(60), "2")
+	err = s.AddHM(ATLAS_OBSERVER, key2, []string{"2", "3"})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	key3 := getSubscriptionKey(uint(60), "3")
+	err = s.AddHM(ATLAS_OBSERVER, key3, []string{"3"})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	given, err := s.FindSubscriptions(uint(60), []string{"1", "2", "3"})
+	assert.Nil(t, err)
+	assert.True(t, isEqualSubscriptions(given, want))
+}
+
+func TestStorage_Lookup_NotFoundSeveral(t *testing.T) {
+	s := initStorage(t)
+	assert.NotNil(t, s)
+
+	want := []blockatlas.Subscription{
+		{Coin: 60, Address: "1", GUID: "1"},
+	}
+
+	key1 := getSubscriptionKey(uint(60), "1")
+	err := s.AddHM(ATLAS_OBSERVER, key1, []string{"1"})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	key2 := getSubscriptionKey(uint(60), "2")
+	err = s.AddHM(ATLAS_OBSERVER, key2, []string{"2", "3"})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	key3 := getSubscriptionKey(uint(60), "3")
+	err = s.AddHM(ATLAS_OBSERVER, key3, []string{"3"})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	given, err := s.FindSubscriptions(uint(60), []string{"1", "4", "5"})
+	assert.Nil(t, err)
+	assert.True(t, isEqualSubscriptions(given, want))
+}
+
+func TestStorage_AddSubscriptions(t *testing.T) {
+	s := initStorage(t)
+	assert.NotNil(t, s)
+
+	subs := []blockatlas.Subscription{
+		{Coin: 60, Address: "1", GUID: "1"},
+		{Coin: 144, Address: "11212", GUID: "112"},
+		{Coin: 144, Address: "112121", GUID: "112"},
+	}
+	var want []string
+	for _, sub := range subs {
+		key := getSubscriptionKey(sub.Coin, sub.Address)
+		var guids []string
+
+		err := s.GetHMValue(ATLAS_OBSERVER, key, &guids)
+		assert.NotNil(t, err)
+		assert.Equal(t, want, guids)
+	}
+
+	err := s.AddSubscriptions(subs)
+	assert.Nil(t, err)
+
+	for _, sub := range subs {
+		key := getSubscriptionKey(sub.Coin, sub.Address)
+		var guids []string
+		err := s.GetHMValue(ATLAS_OBSERVER, key, &guids)
+		assert.Nil(t, err)
+		assert.NotNil(t, guids)
+
+		var counter int
+		for _, g := range guids {
+			if g == sub.GUID {
+				counter++
+			}
+		}
+		assert.True(t, counter == 1)
+	}
+
+	NewSubs := []blockatlas.Subscription{
+		{Coin: 714, Address: "2", GUID: "2"},
+		{Coin: 148, Address: "21", GUID: "21"},
+		{Coin: 148, Address: "21", GUID: "21"},
+	}
+
+	err = s.AddSubscriptions(NewSubs)
+	assert.Nil(t, err)
+
+	for _, sub := range subs {
+		key := getSubscriptionKey(sub.Coin, sub.Address)
+		var guids []string
+		err := s.GetHMValue(ATLAS_OBSERVER, key, &guids)
+		assert.Nil(t, err)
+		assert.NotNil(t, guids)
+
+		var counter int
+		for _, g := range guids {
+			if g == sub.GUID {
+				counter++
+			}
+		}
+		assert.True(t, counter == 1)
+	}
+
+	for _, sub := range NewSubs {
+		key := getSubscriptionKey(sub.Coin, sub.Address)
+		var guids []string
+		err := s.GetHMValue(ATLAS_OBSERVER, key, &guids)
+		assert.Nil(t, err)
+		assert.NotNil(t, guids)
+
+		var counter int
+		for _, g := range guids {
+			if g == sub.GUID {
+				counter++
+			}
+		}
+		assert.True(t, counter == 1)
+	}
+
+	NotExistingSubs := []blockatlas.Subscription{
+		{Coin: 111, Address: "2", GUID: "2"},
+		{Coin: 222, Address: "21", GUID: "21"},
+	}
+
+	for _, sub := range NotExistingSubs {
+		key := getSubscriptionKey(sub.Coin, sub.Address)
+		var guids []string
+		err := s.GetHMValue(ATLAS_OBSERVER, key, &guids)
+		assert.NotNil(t, err)
+		assert.Nil(t, guids)
+
+		var counter int
+		for _, g := range guids {
+			if g == sub.GUID {
+				counter++
+			}
+		}
+		assert.False(t, counter == 1)
+	}
+}
+
+func TestStorage_DeleteSubscriptions(t *testing.T) {
+	s := initStorage(t)
+	assert.NotNil(t, s)
+
+	subs := []blockatlas.Subscription{
+		{Coin: 60, Address: "1", GUID: "1"},
+		{Coin: 144, Address: "11212", GUID: "112"},
+		{Coin: 255, Address: "112121", GUID: "1121"},
+	}
+	var want []string
+	for _, sub := range subs {
+		key := getSubscriptionKey(sub.Coin, sub.Address)
+		var guids []string
+
+		err := s.GetHMValue(ATLAS_OBSERVER, key, &guids)
+		assert.NotNil(t, err)
+		assert.Equal(t, want, guids)
+	}
+
+	err := s.AddSubscriptions(subs)
+	assert.Nil(t, err)
+
+	for _, sub := range subs {
+		key := getSubscriptionKey(sub.Coin, sub.Address)
+		var guids []string
+		err := s.GetHMValue(ATLAS_OBSERVER, key, &guids)
+		assert.Nil(t, err)
+		assert.NotNil(t, guids)
+
+		var counter int
+		for _, g := range guids {
+			if g == sub.GUID {
+				counter++
+			}
+		}
+		assert.True(t, counter == 1)
+	}
+
+	deleted_subs := []blockatlas.Subscription{
+		{Coin: 60, Address: "1", GUID: "1"},
+		{Coin: 144, Address: "11212", GUID: "112"},
+	}
+
+	err = s.DeleteSubscriptions(deleted_subs)
+	assert.Nil(t, err)
+
+	for _, sub := range deleted_subs {
+		key := getSubscriptionKey(sub.Coin, sub.Address)
+		var guids []string
+		err := s.GetHMValue(ATLAS_OBSERVER, key, &guids)
+		assert.NotNil(t, err)
+		assert.Nil(t, guids)
+
+		var counter int
+		for _, g := range guids {
+			if g == sub.GUID {
+				counter++
+			}
+		}
+		assert.True(t, counter == 0)
+	}
+
+	existing_subs := []blockatlas.Subscription{
+		{Coin: 255, Address: "112121", GUID: "1121"},
+	}
+
+	for _, sub := range existing_subs {
+		key := getSubscriptionKey(sub.Coin, sub.Address)
+		var guids []string
+		err := s.GetHMValue(ATLAS_OBSERVER, key, &guids)
+		assert.Nil(t, err)
+		assert.NotNil(t, guids)
+
+		var counter int
+		for _, g := range guids {
+			if g == sub.GUID {
+				counter++
+			}
+		}
+		assert.True(t, counter == 1)
+	}
+}
+
+func isEqualSubscriptions(given, want []blockatlas.Subscription) bool {
+	if len(given) != len(want) {
+		return false
+	}
+	var givenCounter int
+	for _, g := range given {
+		var wantCounter int
+		for _, w := range want {
+			if w == g {
+				wantCounter++
+			}
+		}
+		if wantCounter > 0 {
+			givenCounter++
+		}
+	}
+
+	if givenCounter == len(want) {
+		return true
+	}
+	return false
+}
+
+func initStorage(t *testing.T) *Storage {
+	s, err := miniredis.Run()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	storage := New()
+	err = storage.Init(fmt.Sprintf("redis://%s", s.Addr()))
+	if err != nil {
+		logger.Fatal(err)
+	}
+	return storage
+}
diff --git a/storage/tracker_test.go b/storage/tracker_test.go
new file mode 100644
index 0000000..56a7a30
--- /dev/null
+++ b/storage/tracker_test.go
@@ -0,0 +1,24 @@
+package storage
+
+import (
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestBlockMap(t *testing.T) {
+	s := initStorage(t)
+	assert.NotNil(t, s)
+
+	block, err := s.GetLastParsedBlockNumber(60)
+	assert.Nil(t, err)
+	assert.Equal(t, int64(0), block)
+
+	newBlock := int64(1400)
+
+	err = s.SetLastParsedBlockNumber(60, newBlock)
+	assert.Nil(t, err)
+
+	current, err := s.GetLastParsedBlockNumber(60)
+	assert.Nil(t, err)
+	assert.Equal(t, newBlock, current)
+}
diff --git a/tests/docker_test/environment_run_test.go b/tests/docker_test/environment_run_test.go
index ca13dd7..35e38f3 100644
--- a/tests/docker_test/environment_run_test.go
+++ b/tests/docker_test/environment_run_test.go
@@ -3,13 +3,33 @@
 package docker_test
 
 import (
+	"github.com/trustwallet/blockatlas/mq"
 	"github.com/trustwallet/blockatlas/tests/docker_test/setup"
+	"log"
 	"os"
 	"testing"
 )
 
+var (
+	rawTransactionsChannel,
+	transactionsChannel, subscriptionChannel mq.MessageChannel
+)
+
 func TestMain(m *testing.M) {
 	setup.RunMQContainer()
+	if err := mq.RawTransactions.Declare(); err != nil {
+		log.Fatal(err)
+	}
+	if err := mq.Transactions.Declare(); err != nil {
+		log.Fatal(err)
+	}
+	if err := mq.Subscriptions.Declare(); err != nil {
+		log.Fatal(err)
+	}
+	rawTransactionsChannel = mq.RawTransactions.GetMessageChannel()
+	subscriptionChannel = mq.Subscriptions.GetMessageChannel()
+	transactionsChannel = mq.Transactions.GetMessageChannel()
+
 	setup.RunRedisContainer()
 	code := m.Run()
 	setup.StopMQContainer()
diff --git a/tests/docker_test/notifier_test.go b/tests/docker_test/notifier_test.go
new file mode 100644
index 0000000..ea9fa95
--- /dev/null
+++ b/tests/docker_test/notifier_test.go
@@ -0,0 +1,108 @@
+// +build integration
+
+package docker_test
+
+import (
+	"context"
+	"encoding/json"
+	"github.com/streadway/amqp"
+	"github.com/stretchr/testify/assert"
+	"github.com/trustwallet/blockatlas/coin"
+	"github.com/trustwallet/blockatlas/mq"
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"github.com/trustwallet/blockatlas/services/observer/notifier"
+	"github.com/trustwallet/blockatlas/tests/docker_test/setup"
+	"testing"
+	"time"
+)
+
+var (
+	txs = blockatlas.Txs{
+		{
+			ID:     "95CF63FAA27579A9B6AF84EF8B2DFEAC29627479E9C98E7F5AE4535E213FA4C9",
+			Coin:   coin.BNB,
+			From:   "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
+			To:     "tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex",
+			Fee:    "125000",
+			Date:   1555117625,
+			Block:  7928667,
+			Status: blockatlas.StatusCompleted,
+			Memo:   "test",
+			Meta: blockatlas.NativeTokenTransfer{
+				TokenID:  "YLC-D8B",
+				Symbol:   "YLC",
+				Value:    "210572645",
+				Decimals: 8,
+				From:     "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
+				To:       "tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex",
+			},
+		},
+	}
+)
+
+func TestNotifier(t *testing.T) {
+	err := setup.Cache.AddSubscriptions([]blockatlas.Subscription{{Coin: 714, Address: "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a", GUID: "guid_test"}})
+	assert.Nil(t, err)
+
+	err = produceTxs(txs)
+	assert.Nil(t, err)
+
+	ctx, cancel := context.WithCancel(context.Background())
+
+	go mq.RawTransactions.RunConsumerForChannelWithCancel(notifier.RunNotifier, rawTransactionsChannel, setup.Cache, ctx)
+	time.Sleep(time.Second * 3)
+	msg := transactionsChannel.GetMessage()
+	ConsumerToTestTransactions(msg, t)
+	cancel()
+}
+
+func ConsumerToTestTransactions(delivery amqp.Delivery, t *testing.T) {
+	var event notifier.DispatchEvent
+	if err := json.Unmarshal(delivery.Body, &event); err != nil {
+		assert.Nil(t, err)
+		return
+	}
+	err := delivery.Ack(false)
+	if err != nil {
+		assert.Nil(t, err)
+	}
+
+	memo := blockatlas.NativeTokenTransfer{
+		Name:     "",
+		TokenID:  "YLC-D8B",
+		Symbol:   "YLC",
+		Value:    "210572645",
+		Decimals: 8,
+		From:     "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
+		To:       "tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex",
+	}
+
+	assert.Equal(t, notifier.DispatchEvent{
+		Action: blockatlas.TxNativeTokenTransfer,
+		Result: &blockatlas.Tx{
+			Type:      blockatlas.TxNativeTokenTransfer,
+			Direction: "outgoing",
+			ID:        "95CF63FAA27579A9B6AF84EF8B2DFEAC29627479E9C98E7F5AE4535E213FA4C9",
+			Coin:      coin.BNB,
+			From:      "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
+			To:        "tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex",
+			Fee:       "125000",
+			Date:      1555117625,
+			Block:     7928667,
+			Status:    blockatlas.StatusCompleted,
+			Memo:      "test",
+			Meta:      &memo,
+		},
+		GUID: "guid_test",
+	}, event)
+
+	return
+}
+
+func produceTxs(txs blockatlas.Txs) error {
+	body, err := json.Marshal(txs)
+	if err != nil {
+		return err
+	}
+	return mq.RawTransactions.Publish(body)
+}
diff --git a/tests/docker_test/observer_test.go b/tests/docker_test/observer_test.go
index 5bccfff..8c60b23 100644
--- a/tests/docker_test/observer_test.go
+++ b/tests/docker_test/observer_test.go
@@ -3,62 +3,158 @@
 package docker_test
 
 import (
+	"context"
 	"encoding/json"
+	"github.com/streadway/amqp"
 	"github.com/stretchr/testify/assert"
+	"github.com/trustwallet/blockatlas/coin"
 	"github.com/trustwallet/blockatlas/mq"
 	"github.com/trustwallet/blockatlas/pkg/blockatlas"
-	"github.com/trustwallet/blockatlas/services/subscription"
-	"github.com/trustwallet/blockatlas/storage"
+	"github.com/trustwallet/blockatlas/services/observer/notifier"
+	"github.com/trustwallet/blockatlas/services/observer/parser"
 	"github.com/trustwallet/blockatlas/tests/docker_test/setup"
-	"io/ioutil"
-	"path/filepath"
-	"runtime"
+	"go.uber.org/atomic"
 	"testing"
 	"time"
 )
 
-func TestSubscriberAddSubscription(t *testing.T) {
-	_, goFile, _, _ := runtime.Caller(0)
-	testFilePathGiven := filepath.Join(filepath.Dir(goFile), "data", "given_subscriptions_added.json")
-	testFileGiven, err := ioutil.ReadFile(testFilePathGiven)
-	if err != nil {
-		t.Fatal(err)
+var counter atomic.Int32
+var counterBlock atomic.Int32
+
+func TestFullFlow(t *testing.T) {
+	err := setup.Cache.AddSubscriptions([]blockatlas.Subscription{{Coin: 60, Address: "testAddress", GUID: "guid_test"}})
+	assert.Nil(t, err)
+
+	ctx, cancel := context.WithCancel(context.Background())
+
+	stopChan := make(chan struct{}, 1)
+
+	params := setupParserFull(stopChan)
+	params.Ctx = ctx
+	params.Queue = mq.RawTransactions
+
+	go parser.RunParser(params)
+	time.Sleep(time.Second * 2)
+
+	go mq.RawTransactions.RunConsumerForChannelWithCancel(notifier.RunNotifier, rawTransactionsChannel, setup.Cache, ctx)
+	time.Sleep(time.Second * 5)
+
+	for i := 0; i < 11; i++ {
+		x := transactionsChannel.GetMessage()
+		ConsumerToTestTransactionsFull(x, t, cancel, i)
 	}
-	var givenEvents []blockatlas.SubscriptionEvent
-	if err := json.Unmarshal(testFileGiven, &givenEvents); err != nil {
-		t.Fatal(err)
+	<-stopChan
+}
+
+func getMockedBlockAPIFull() blockatlas.BlockAPI {
+	p := PlatformFullFlow{CoinIndex: 60}
+	return &p
+}
+
+type PlatformFullFlow struct {
+	CoinIndex uint
+}
+
+func (p *PlatformFullFlow) CurrentBlockNumber() (int64, error) {
+	i := counterBlock.Load()
+	counterBlock.Add(1)
+	return int64(i), nil
+}
+
+func (p *PlatformFullFlow) Coin() coin.Coin {
+	return coin.Coins[p.CoinIndex]
+}
+
+func (p *PlatformFullFlow) GetBlockByNumber(num int64) (*blockatlas.Block, error) {
+	if num < 100 {
+		return &blockatlas.Block{
+			Number: num,
+			ID:     "",
+			Txs: []blockatlas.Tx{
+				{
+					ID:     "95CF63FAA27579A9B6AF84EF8B2DFEAC29627479E9C98E7F5AE4535E213FA4C9",
+					Coin:   coin.ETH,
+					From:   "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
+					To:     "testAddress",
+					Fee:    "125000",
+					Date:   1555117625,
+					Block:  7928667,
+					Status: blockatlas.StatusCompleted,
+					Memo:   "test",
+					Meta: blockatlas.NativeTokenTransfer{
+						TokenID:  "YLC-D8B",
+						Symbol:   "YLC",
+						Value:    "210572645",
+						Decimals: 8,
+						From:     "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
+						To:       "testAddress",
+					},
+				},
+			},
+		}, nil
 	}
+	return &blockatlas.Block{}, nil
+}
 
-	testFilePathWanted := filepath.Join(filepath.Dir(goFile), "data", "wanted_subscriptions_added.json")
-	testFileWanted, err := ioutil.ReadFile(testFilePathWanted)
+func ConsumerToTestTransactionsFull(delivery amqp.Delivery, t *testing.T, cancel context.CancelFunc, counter int) {
+	var event notifier.DispatchEvent
+	if err := json.Unmarshal(delivery.Body, &event); err != nil {
+		assert.Nil(t, err)
+		return
+	}
+	err := delivery.Ack(false)
 	if err != nil {
-		t.Fatal(err)
+		assert.Nil(t, err)
 	}
-	var wantedEvents []blockatlas.Subscription
-	if err := json.Unmarshal(testFileWanted, &wantedEvents); err != nil {
-		t.Fatal(err)
+	memo := blockatlas.NativeTokenTransfer{
+		TokenID:  "YLC-D8B",
+		Symbol:   "YLC",
+		Value:    "210572645",
+		Decimals: 8,
+		From:     "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
+		To:       "testAddress",
 	}
 
-	assert.Nil(t, mq.Subscriptions.Declare())
+	assert.Equal(t, notifier.DispatchEvent{
+		Action: blockatlas.TxNativeTokenTransfer,
+		Result: &blockatlas.Tx{
+			Type:      blockatlas.TxNativeTokenTransfer,
+			Direction: "incoming",
+			ID:        "95CF63FAA27579A9B6AF84EF8B2DFEAC29627479E9C98E7F5AE4535E213FA4C9",
+			Coin:      coin.ETH,
+			From:      "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
+			To:        "testAddress",
+			Fee:       "125000",
+			Date:      1555117625,
+			Block:     7928667,
+			Status:    blockatlas.StatusCompleted,
+			Memo:      "test",
+			Meta:      &memo,
+		},
+		GUID: "guid_test",
+	}, event)
 
-	for _, event := range givenEvents {
-		body, err := json.Marshal(event)
-		assert.Nil(t, err)
+	if counter == 10 {
+		cancel()
+	}
+}
 
-		err = mq.Subscriptions.Publish(body)
-		assert.Nil(t, err)
+func setupParserFull(stopChan chan<- struct{}) parser.Params {
+	minTime := time.Second
+	maxTime := time.Second * 2
+	maxBatchBlocksAmount := 1
 
-		go mq.Subscriptions.RunConsumer(subscription.Consume, setup.Cache)
-		time.Sleep(time.Second / 5)
-	}
+	pollInterval := notifier.GetInterval(0, minTime, maxTime)
 
-	result, err := setup.Cache.GetAllHM(storage.ATLAS_OBSERVER)
-	assert.Nil(t, err)
-	assert.NotNil(t, result)
+	backlogCount := 1
 
-	for _, wanted := range wantedEvents {
-		result, err := setup.Cache.Lookup(wanted.Coin, []string{wanted.Address})
-		assert.Nil(t, err)
-		assert.Equal(t, result[0], wanted)
+	return parser.Params{
+		Api:                   getMockedBlockAPIFull(),
+		Storage:               setup.Cache,
+		ParsingBlocksInterval: pollInterval,
+		BacklogCount:          backlogCount,
+		MaxBacklogBlocks:      int64(maxBatchBlocksAmount),
+		TxBatchLimit:          100,
+		StopChannel:           stopChan,
 	}
 }
diff --git a/tests/docker_test/parser_test.go b/tests/docker_test/parser_test.go
new file mode 100644
index 0000000..1c1005a
--- /dev/null
+++ b/tests/docker_test/parser_test.go
@@ -0,0 +1,119 @@
+// +build integration
+
+package docker_test
+
+import (
+	"context"
+	"encoding/json"
+	"github.com/streadway/amqp"
+	"github.com/stretchr/testify/assert"
+	"github.com/trustwallet/blockatlas/coin"
+	"github.com/trustwallet/blockatlas/mq"
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"github.com/trustwallet/blockatlas/pkg/logger"
+	"github.com/trustwallet/blockatlas/services/observer/notifier"
+	"github.com/trustwallet/blockatlas/services/observer/parser"
+	"github.com/trustwallet/blockatlas/tests/docker_test/setup"
+	"testing"
+	"time"
+)
+
+func TestParserFetchAndPublishBlock_NormalCase(t *testing.T) {
+	stopChan := make(chan struct{}, 1)
+
+	params := setupParser(stopChan)
+
+	ctx, cancel := context.WithCancel(context.Background())
+
+	params.Ctx = ctx
+	params.Queue = mq.RawTransactions
+
+	go parser.RunParser(params)
+
+	time.Sleep(time.Microsecond)
+	ConsumerToTestAmountOfBlocks(rawTransactionsChannel.GetMessage(), t, cancel)
+	<-stopChan
+}
+
+func getMockedBlockAPI() blockatlas.BlockAPI {
+	p := Platform{CoinIndex: 60}
+	return &p
+}
+
+type Platform struct {
+	CoinIndex uint
+}
+
+func (p *Platform) CurrentBlockNumber() (int64, error) {
+	return int64(100), nil
+}
+
+func (p *Platform) Coin() coin.Coin {
+	return coin.Coins[p.CoinIndex]
+}
+
+func (p *Platform) GetBlockByNumber(num int64) (*blockatlas.Block, error) {
+	if num < 101 {
+		return &blockatlas.Block{
+			Number: num,
+			ID:     "",
+			Txs: []blockatlas.Tx{
+				{
+					ID:     "95CF63FAA27579A9B6AF84EF8B2DFEAC29627479E9C98E7F5AE4535E213FA4C9",
+					Coin:   coin.BNB,
+					From:   "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
+					To:     "tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex",
+					Fee:    "125000",
+					Date:   1555117625,
+					Block:  7928667,
+					Status: blockatlas.StatusCompleted,
+					Memo:   "test",
+					Meta: blockatlas.NativeTokenTransfer{
+						TokenID:  "YLC-D8B",
+						Symbol:   "YLC",
+						Value:    "210572645",
+						Decimals: 8,
+						From:     "tbnb1ttyn4csghfgyxreu7lmdu3lcplhqhxtzced45a",
+						To:       "tbnb12hlquylu78cjylk5zshxpdj6hf3t0tahwjt3ex",
+					},
+				},
+			},
+		}, nil
+	}
+	return &blockatlas.Block{}, nil
+}
+
+func ConsumerToTestAmountOfBlocks(delivery amqp.Delivery, t *testing.T, cancelFunc context.CancelFunc) {
+	var txs blockatlas.Txs
+	if err := json.Unmarshal(delivery.Body, &txs); err != nil {
+		logger.Error(err)
+		return
+	}
+	err := delivery.Ack(false)
+	if err != nil {
+		logger.Error(err)
+	}
+
+	assert.Equal(t, len(txs), 50)
+	cancelFunc()
+}
+
+func setupParser(stopChan chan struct{}) parser.Params {
+	minTime := time.Second
+	maxTime := time.Second * 2
+	maxBatchBlocksAmount := 100
+
+	pollInterval := notifier.GetInterval(0, minTime, maxTime)
+
+	backlogCount := 50
+
+	return parser.Params{
+		Api:                   getMockedBlockAPI(),
+		Storage:               setup.Cache,
+		ParsingBlocksInterval: pollInterval,
+		BacklogCount:          backlogCount,
+		MaxBacklogBlocks:      int64(maxBatchBlocksAmount),
+		TxBatchLimit:          100,
+		StopChannel:           stopChan,
+	}
+}
diff --git a/tests/docker_test/subscriber_test.go b/tests/docker_test/subscriber_test.go
new file mode 100644
index 0000000..ca65276
--- /dev/null
+++ b/tests/docker_test/subscriber_test.go
@@ -0,0 +1,66 @@
+// +build integration
+
+package docker_test
+
+import (
+	"context"
+	"encoding/json"
+	"github.com/stretchr/testify/assert"
+	"github.com/trustwallet/blockatlas/mq"
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"github.com/trustwallet/blockatlas/services/observer/subscriber"
+	"github.com/trustwallet/blockatlas/storage"
+	"github.com/trustwallet/blockatlas/tests/docker_test/setup"
+	"io/ioutil"
+	"path/filepath"
+	"runtime"
+	"testing"
+	"time"
+)
+
+func TestSubscriberAddSubscription(t *testing.T) {
+	_, goFile, _, _ := runtime.Caller(0)
+	testFilePathGiven := filepath.Join(filepath.Dir(goFile), "data", "given_subscriptions_added.json")
+	testFileGiven, err := ioutil.ReadFile(testFilePathGiven)
+	if err != nil {
+		t.Fatal(err)
+	}
+	var givenEvents []blockatlas.SubscriptionEvent
+	if err := json.Unmarshal(testFileGiven, &givenEvents); err != nil {
+		t.Fatal(err)
+	}
+
+	testFilePathWanted := filepath.Join(filepath.Dir(goFile), "data", "wanted_subscriptions_added.json")
+	testFileWanted, err := ioutil.ReadFile(testFilePathWanted)
+	if err != nil {
+		t.Fatal(err)
+	}
+	var wantedEvents []blockatlas.Subscription
+	if err := json.Unmarshal(testFileWanted, &wantedEvents); err != nil {
+		t.Fatal(err)
+	}
+
+	for _, event := range givenEvents {
+		body, err := json.Marshal(event)
+		assert.Nil(t, err)
+
+		err = mq.Subscriptions.Publish(body)
+		assert.Nil(t, err)
+
+		ctx, cancel := context.WithCancel(context.Background())
+
+		go mq.Subscriptions.RunConsumerForChannelWithCancel(subscriber.RunSubscriber, subscriptionChannel, setup.Cache, ctx)
+		time.Sleep(time.Second / 5)
+		cancel()
+	}
+
+	result, err := setup.Cache.GetAllHM(storage.ATLAS_OBSERVER)
+	assert.Nil(t, err)
+	assert.NotNil(t, result)
+
+	for _, wanted := range wantedEvents {
+		result, err := setup.Cache.FindSubscriptions(wanted.Coin, []string{wanted.Address})
+		assert.Nil(t, err)
+		assert.Equal(t, result[0], wanted)
+	}
+}