diff --git a/.gitignore b/.gitignore index 09b8dc3..190a006 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,8 @@ # Dependency directories (remove the comment below to include it) # vendor/ build/ + +cmd/feederd/tdexd +cmd/feederd/config.json + +config.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 71f6082..6c0c381 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN go mod download COPY . . -RUN go build -o feederd-linux cmd/feederd/main.go +RUN GOOS=linux GOARCH=amd64 go build -o feederd-linux cmd/feederd/main.go WORKDIR /build @@ -20,4 +20,4 @@ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates COPY --from=builder /build/ / -CMD ["/feederd-linux","-debug","-conf=./data/config.json"] \ No newline at end of file +CMD ["/feederd-linux"] \ No newline at end of file diff --git a/Makefile b/Makefile index 2099678..a3ca052 100644 --- a/Makefile +++ b/Makefile @@ -41,10 +41,12 @@ help: ## run-linux: Run locally with default configuration run-linux: clean build-linux + export FEEDER_LOG_LEVEL=5; \ ./build/feederd-linux-amd64 ## run-mac: Run locally with default configuration run-mac: clean build-mac + export FEEDER_LOG_LEVEL=5; \ ./build/feederd-darwin-amd64 ## vet: code analysis @@ -52,9 +54,14 @@ vet: @echo "Vet..." @go vet ./... - ## test: runs go unit test with default values -test: fmt - chmod u+x ./scripts/test - ./scripts/test +test: fmt shorttest + +shorttest: + @echo "Testing..." + go test -v -count=1 -race -short ./... +integrationtest: + export FEEDER_CONFIG_PATH="./config.test.json"; \ + export FEEDER_LOG_LEVEL=5; \ + go test -v -count=1 ./cmd/feederd \ No newline at end of file diff --git a/README.md b/README.md index 26bec14..7349f8d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ # tdex-feeder -Feeder allows to connect an external price feed to the TDex Daemon to determine the current market price +Feeder allows to connect several price feeds to TDex Daemon(s) in order to automatically update the markets prices. ## Overview tdex-feeder connects to exchanges and retrieves market prices in order to consume the gRPC interface exposed from tdex-deamon `UpdateMarketPrice`. +![tdex-schema](./tdexfeeder.png) + ## ⬇️ Run Standalone ### Install @@ -24,11 +26,8 @@ interface exposed from tdex-deamon `UpdateMarketPrice`. # Run with default config and default flags. $ feederd -# Run with debug mode on. -$ feederd -debug - # Run with debug mode and different config path. -$ feederd -debug -conf=./config.json +$ FEEDER_CONFIG_PATH=./config.json feederd ``` ## 🖥 Local Development @@ -43,7 +42,7 @@ Build and use `feederd` with docker. At the root of the repository ``` -docker build -t tdex-feederd . +docker build --pull --rm -f 'Dockerfile' -t feederd:latest . ``` #### Run the daemon @@ -51,9 +50,11 @@ docker build -t tdex-feederd . Create a [config.json](#config-file) file and run the following command in the same folder: ``` -docker run -it -d --net=host -v $PWD/config.json:/data/config.json tdex-feederd +docker run -it --name feederd -v $HOME/config.json:/config.json --network="host" feederd ``` -`--net=host` in case you're running tdex-deamon locally +the `$HOME/config.json` is the path to the feederd configuration file. + +> `--net=host` in case you're running tdex-deamon locally ### Build it yourself @@ -71,13 +72,6 @@ Builds feeder as static binary and runs the project with default configuration. `make run-linux` -##### Flags - -``` --conf: Configuration File Path. Default: "./config.json" --debug: Log Debug Informations Default: false -``` - ##### Config file Rename the file `./config.example.json` into `./config.json` @@ -86,11 +80,10 @@ connects to kraken socket and to a local instance of tdex-deamon. ``` daemon_endpoint: String with the address and port of gRPC host. Required. -daemon_macaroon: String with the daemon_macaroon necessary for authentication. kraken_ws_endpoint: String with the address and port of kraken socket. Required. markets: Json List with necessary markets informations. Required. -base_asset: String of the Hash of the base asset for gRPC request. Required. -quote_asset: String of the Hash of the quote asset for gRPC request. Required. -kraken_ticker: String with the ticker we want kraken to provide informations on. Required. -interval: Int with the time in secods between gRPC requests. Required. -``` \ No newline at end of file + base_asset: String of the Hash of the base asset for gRPC request. Required. + quote_asset: String of the Hash of the quote asset for gRPC request. Required. + kraken_ticker: String with the ticker we want kraken to provide informations on. Required. + interval: the minimum time in milliseconds between two updateMarketPrice requests. Required. +``` diff --git a/cmd/feeder/main.go b/cmd/feeder/main.go deleted file mode 100644 index 7905807..0000000 --- a/cmd/feeder/main.go +++ /dev/null @@ -1,5 +0,0 @@ -package main - -func main() { - -} diff --git a/cmd/feederd/config.test.json b/cmd/feederd/config.test.json new file mode 100755 index 0000000..0278506 --- /dev/null +++ b/cmd/feederd/config.test.json @@ -0,0 +1,10 @@ +{ + "daemon_endpoint":"127.0.0.1:9000", + "kraken_ws_endpoint":"ws.kraken.com", + "markets": [ + { + "base_asset":"5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225","quote_asset":"bffce3908a595436b6ab08f916fea2c9fc6a702f46b268ca354205d127f60c48","kraken_ticker":"LTC/USDT", + "interval":500 + } + ] +} \ No newline at end of file diff --git a/cmd/feederd/main.go b/cmd/feederd/main.go index f32b37b..57adfeb 100644 --- a/cmd/feederd/main.go +++ b/cmd/feederd/main.go @@ -4,114 +4,63 @@ package main import ( - "flag" + "encoding/json" + "io/ioutil" "os" "os/signal" - "time" + "syscall" log "github.com/sirupsen/logrus" - "google.golang.org/grpc" - - "github.com/gorilla/websocket" "github.com/tdex-network/tdex-feeder/config" - "github.com/tdex-network/tdex-feeder/pkg/conn" - "github.com/tdex-network/tdex-feeder/pkg/marketinfo" - - pboperator "github.com/tdex-network/tdex-protobuf/generated/go/operator" -) - -const ( - defaultConfigPath = "./config.json" + "github.com/tdex-network/tdex-feeder/internal/adapters" + "github.com/tdex-network/tdex-feeder/internal/application" ) func main() { - interrupt, cSocket, marketsInfos, conngRPC := setup() - infiniteLoops(interrupt, cSocket, marketsInfos, conngRPC) -} - -func setup() (chan os.Signal, *websocket.Conn, []marketinfo.MarketInfo, *grpc.ClientConn) { - conf := checkFlags() - // Interrupt Notification. interrupt := make(chan os.Signal, 1) - signal.Notify(interrupt, os.Interrupt) + signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) - // Dials the connection the the Socket. - cSocket, err := conn.ConnectToSocket(conf.KrakenWsEndpoint) - if err != nil { - log.Fatal("Socket Connection Error: ", err) - } - marketsInfos := loadMarkets(conf, cSocket) - if len(marketsInfos) == 0 { - log.Warn("list of market to feed is empty") - } + // retrieve feeder service from config file + feeder := configFileToFeederService(config.GetConfigPath()) - // Set up the connection to the gRPC server. - conngRPC, err := conn.ConnectTogRPC(conf.DaemonEndpoint) + log.Info("Start the feeder...") + go func() { + err := feeder.Start() + if err != nil { + log.Fatal(err) + } + }() + + // check for interupt + <-interrupt + log.Info("Shutting down the feeder...") + err := feeder.Stop() + log.Info("Feeder service stopped") if err != nil { - log.Fatal("gRPC Connection Error: ", err) + log.Fatal(err) } - return interrupt, cSocket, marketsInfos, conngRPC + os.Exit(0) } -// Checks for command line flags for Config Path and Debug mode. -// Loads flags as required. -func checkFlags() config.Config { - confFlag := flag.String("conf", defaultConfigPath, "Configuration File Path") - debugFlag := flag.Bool("debug", false, "Log Debug Informations") - flag.Parse() - if *debugFlag == true { - log.SetLevel(log.DebugLevel) - } - // Loads Config File. - conf, err := config.LoadConfig(*confFlag) +func configFileToFeederService(configFilePath string) application.FeederService { + jsonFile, err := os.Open(configFilePath) if err != nil { log.Fatal(err) } - return conf -} + defer jsonFile.Close() -// Loads Config Markets infos into Data Structure and Subscribes to -// Messages from this Markets. -func loadMarkets(conf config.Config, cSocket *websocket.Conn) []marketinfo.MarketInfo { - numberOfMarkets := len(conf.Markets) - marketsInfos := make([]marketinfo.MarketInfo, numberOfMarkets) - for i, marketConfig := range conf.Markets { - marketsInfos[i] = marketinfo.InitialMarketInfo(marketConfig) - m := conn.CreateSubscribeToMarketMessage(marketConfig.KrakenTicker) - err := conn.SendRequestMessage(cSocket, m) - if err != nil { - log.Fatal("Couldn't send request message: ", err) - } + configBytes, err := ioutil.ReadAll(jsonFile) + if err != nil { + log.Fatal(err) } - return marketsInfos -} -func infiniteLoops(interrupt chan os.Signal, cSocket *websocket.Conn, marketsInfos []marketinfo.MarketInfo, conngRPC *grpc.ClientConn) { - defer cSocket.Close() - defer conngRPC.Close() - clientgRPC := pboperator.NewOperatorClient(conngRPC) - done := make(chan string) - // Handles Messages from subscriptions. Will periodically call the - // gRPC UpdateMarketPrice with the price info from the messages. - go conn.HandleMessages(done, cSocket, marketsInfos, clientgRPC) - checkInterrupt(interrupt, cSocket, done) -} - -// Loop to keep cycle alive. Waits Interrupt to close the connection. -func checkInterrupt(interrupt chan os.Signal, cSocket *websocket.Conn, done chan string) { - for { - for range interrupt { - log.Println("Shutting down Feeder") - err := cSocket.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) - if err != nil { - log.Fatal("write close:", err) - } - select { - case <-done: - case <-time.After(time.Second): - } - return - } + config := &adapters.Config{} + err = json.Unmarshal(configBytes, config) + if err != nil { + log.Fatal(err) } + + feeder := config.ToFeederService() + return feeder } diff --git a/cmd/feederd/main_test.go b/cmd/feederd/main_test.go new file mode 100644 index 0000000..a270b96 --- /dev/null +++ b/cmd/feederd/main_test.go @@ -0,0 +1,226 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" + "os/exec" + "testing" + "time" + + "github.com/tdex-network/tdex-feeder/config" + "github.com/tdex-network/tdex-feeder/internal/adapters" +) + +const ( + containerName = "tdexd-feeder-test" + daemonEndpoint = "127.0.0.1:9000" + krakenWsEndpoint = "ws.kraken.com" + // nigiriUrl = "https://nigiri.network/liquid/api" + nigiriUrl = "http://localhost:3001" + password = "vulpemsecret" +) + +func TestFeeder(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + runDaemonAndInitConfigFile(t) + t.Cleanup(stopAndDeleteContainer) + + go main() + time.Sleep(30 * time.Second) + os.Exit(0) +} + +func runDaemonAndInitConfigFile(t *testing.T) { + usdt := runDaemonAndCreateMarket(t) + + configJson := adapters.ConfigJson{ + DaemonEndpoint: daemonEndpoint, + KrakenWsEndpoint: krakenWsEndpoint, + Markets: []adapters.MarketJson{ + adapters.MarketJson{ + KrakenTicker: "LTC/USDT", + BaseAsset: "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225", + QuoteAsset: usdt, + Interval: 500, + }, + }, + } + + bytes, err := json.Marshal(configJson) + if err != nil { + t.Error(err) + } + + err = ioutil.WriteFile(config.GetConfigPath(), bytes, os.ModePerm) + if err != nil { + t.Error(err) + } +} + +func runDaemonAndCreateMarket(t *testing.T) string { + _, err := execute( + "docker", "run", "--name", containerName, + "-p", "9000:9000", + "-d", + "-v", "tdexd:/.tdex-daemon", + "-e", "TDEX_NETWORK=regtest", + "-e", "TDEX_EXPLORER_ENDPOINT="+nigiriUrl, + "-e", "TDEX_FEE_ACCOUNT_BALANCE_TRESHOLD=1000", + "-e", "TDEX_BASE_ASSET=5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225", + "-e", "TDEX_LOG_LEVEL=5", + "--network=host", + "tdexd:latest", + ) + + if err != nil { + t.Error(err) + } + + time.Sleep(5 * time.Second) + + _, err = runCLICommand("config", "init") + if err != nil { + t.Error(err) + } + + // init the wallet + seed, err := runCLICommand("genseed") + if err != nil { + t.Error(err) + } + + _, err = runCLICommand("init", "--seed", seed, "--password", password) + if err != nil { + t.Error(err) + } + + _, err = runCLICommand("unlock", "--password", password) + if err != nil { + t.Error(err) + } + + depositMarketJson, err := runCLICommand("depositmarket", "--base_asset", "", "--quote_asset", "") + if err != nil { + t.Error(err) + } + + var depositMarketResult map[string]interface{} + + err = json.Unmarshal([]byte(depositMarketJson), &depositMarketResult) + if err != nil { + t.Error(t, err) + } + + address := depositMarketResult["address"].(string) + usdt := fundMarketAddress(t, address) + + return usdt +} + +func stopAndDeleteContainer() { + _, err := execute("docker", "stop", containerName) + if err != nil { + panic(err) + } + + _, err = execute("docker", "container", "rm", containerName) + if err != nil { + panic(err) + } +} + +func fundMarketAddress(t *testing.T, address string) string { + _, err := faucet(address) + if err != nil { + t.Error(err) + } + + _, shitcoin, err := mint(address, 100) + if err != nil { + t.Error(err) + } + + time.Sleep(3 * time.Second) + return shitcoin +} + +func mint(address string, amount int) (string, string, error) { + url := fmt.Sprintf("%s/mint", nigiriUrl) + payload := map[string]interface{}{"address": address, "quantity": amount} + body, _ := json.Marshal(payload) + resp, err := http.Post(url, "application/json", bytes.NewBuffer(body)) + if err != nil { + return "", "", err + } + + if resp.StatusCode != 200 { + return "", "", errors.New("Internal server error") + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", "", err + } + respBody := map[string]interface{}{} + err = json.Unmarshal(data, &respBody) + if err != nil { + return "", "", err + } + + if respBody["asset"].(string) == "" { + return mint(address, amount) + } + + return respBody["txId"].(string), respBody["asset"].(string), nil +} + +func faucet(address string) (string, error) { + url := fmt.Sprintf("%s/faucet", nigiriUrl) + payload := map[string]string{"address": address} + body, _ := json.Marshal(payload) + resp, err := http.Post(url, "application/json", bytes.NewBuffer(body)) + if err != nil { + return "", err + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + respBody := map[string]string{} + err = json.Unmarshal(data, &respBody) + if err != nil { + return "", err + } + + return respBody["txId"], nil +} + +func execute(command string, args ...string) (string, error) { + cmd := exec.Command(command, args...) + var out bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &stderr + err := cmd.Run() + result := out.String() + if err == nil { + return result, nil + } + + return out.String(), errors.New(fmt.Sprint(err) + ": " + stderr.String()) +} + +func runCLICommand(cliCommand string, args ...string) (string, error) { + commandArgs := []string{"exec", containerName, "tdex", cliCommand} + commandArgs = append(commandArgs, args...) + output, err := execute("docker", commandArgs...) + return output, err +} diff --git a/config.example.json b/config.example.json old mode 100644 new mode 100755 index fd6bdac..15c7ec5 --- a/config.example.json +++ b/config.example.json @@ -1,13 +1,10 @@ { - "daemon_endpoint": "localhost:9000", - "daemon_macaroon": "string", - "kraken_ws_endpoint": "ws.kraken.com", - "markets": [ - { - "base_asset": "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225", - "quote_asset": "691e6b9e00cb75f6383f2581ef001f2695074746e213d21771dff5a890f21104", - "kraken_ticker": "XBT/USD", - "interval": 10 - } - ] + "daemon_endpoint":"127.0.0.1:9000", + "kraken_ws_endpoint":"ws.kraken.com", + "markets":[{ + "base_asset":"5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225", + "quote_asset":"e3ec88b0ed9228c3337f746f5d9ea20c1e68bbe1f73d94621c1e8452bebb9e22", + "kraken_ticker":"LTC/USDT", + "interval":500 + }] } \ No newline at end of file diff --git a/config/config.go b/config/config.go index f97e77e..e9bdebe 100644 --- a/config/config.go +++ b/config/config.go @@ -1,96 +1,56 @@ package config import ( - "encoding/json" "errors" - "io/ioutil" + "log" "os" - "reflect" - "strings" - log "github.com/sirupsen/logrus" + "github.com/spf13/viper" ) const ( - defaultDaemonEndpoint = "localhost:9000" - defaultKrakenWsEndpoint = "ws.kraken.com" + // ConfigFilePathKey is the location of the config.json file. + ConfigFilePathKey = "CONFIG_PATH" + // LogLevelKey ... + LogLevelKey = "LOG_LEVEL" ) -// Config defines the struct for the configuration JSON file -type Config struct { - DaemonEndpoint string `json:"daemon_endpoint,required"` - DaemonMacaroon string `json:"daemon_macaroon"` - KrakenWsEndpoint string `json:"kraken_ws_endpoint,required"` - Markets []Market `json:"markets,required"` -} +var vip *viper.Viper -// DefaultConfig returns the datastructure needed -// for a default connection. -func defaultConfig() Config { - return Config{ - DaemonEndpoint: defaultDaemonEndpoint, - KrakenWsEndpoint: defaultKrakenWsEndpoint, - Markets: nil, - } -} +func init() { + vip = viper.New() + vip.SetEnvPrefix("FEEDER") + vip.AutomaticEnv() -// LoadConfigFromFile reads a file with the intended running behaviour -// and returns a Config struct with the respective configurations. -func loadConfigFromFile(filePath string) (Config, error) { - jsonFile, err := os.Open(filePath) - if err != nil { - return Config{}, err - } - defer jsonFile.Close() + vip.SetDefault(LogLevelKey, 4) - var config Config + validate() - byteValue, err := ioutil.ReadAll(jsonFile) - if err != nil { - return Config{}, err - } - err = json.Unmarshal(byteValue, &config) - if err != nil { - return Config{}, err - } - err = checkConfigParsing(config) - if err != nil { - return Config{}, err - } + // this skip the check for default config file (avoid make test fail) + vip.SetDefault(ConfigFilePathKey, "./config.json") +} - return config, nil +func GetConfigPath() string { + return vip.GetString(ConfigFilePathKey) } -// checkConfigParsing checks if all the required fields -// were correctly loaded into the Config struct. -func checkConfigParsing(config Config) error { - fields := reflect.ValueOf(config) - for i := 0; i < fields.NumField(); i++ { - tags := fields.Type().Field(i).Tag - if strings.Contains(string(tags), "required") && fields.Field(i).IsZero() { - return errors.New("Config required field is missing: " + string(tags)) - } +func validate() { + if err := validatePath(vip.GetString(ConfigFilePathKey)); err != nil { + log.Fatal(err) } - for _, market := range config.Markets { - fields := reflect.ValueOf(market) - for i := 0; i < fields.NumField(); i++ { - tags := fields.Type().Field(i).Tag - if strings.Contains(string(tags), "required") && fields.Field(i).IsZero() { - return errors.New("Config required field is missing: " + string(tags)) - } - } - } - return nil } -// LoadConfig handles the default behaviour for loading -// config.json files. In case the file is not found, -// it loads the default config. -func LoadConfig(filePath string) (Config, error) { - _, err := os.Stat(filePath) - if os.IsNotExist(err) { - log.Debugf("File not found: %s. Loading default config.\n", filePath) - return defaultConfig(), nil +func validatePath(path string) error { + if path != "" { + stat, err := os.Stat(path) + if err != nil { + return err + } + + if stat.IsDir() { + return errors.New("not a file") + } } - return loadConfigFromFile(filePath) + + return nil } diff --git a/config/types.go b/config/types.go deleted file mode 100644 index 246fee2..0000000 --- a/config/types.go +++ /dev/null @@ -1,8 +0,0 @@ -package config - -type Market struct { - BaseAsset string `json:"base_asset,required"` - QuoteAsset string `json:"quote_asset,required"` - KrakenTicker string `json:"kraken_ticker,required"` - Interval int `json:"interval,required"` -} diff --git a/go.mod b/go.mod index 0fcc774..f46f8c1 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,11 @@ module github.com/tdex-network/tdex-feeder go 1.15 require ( - github.com/gorilla/websocket v1.4.2 + github.com/aopoltorzhicky/go_kraken/websocket v0.0.10 + github.com/prometheus/common v0.4.0 github.com/sirupsen/logrus v1.7.0 - github.com/stretchr/testify v1.2.2 + github.com/spf13/viper v1.7.1 + github.com/stretchr/testify v1.6.1 github.com/tdex-network/tdex-protobuf v0.0.0-20201029153650-f5164a4b6a77 google.golang.org/grpc v1.33.2 ) diff --git a/go.sum b/go.sum index 6009be7..231b689 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,71 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/aopoltorzhicky/go_kraken/rest v0.0.3 h1:oTRp0xqqsq0g83UOu6dCQtbKTk1lZjqtH3TsaDwDt/o= +github.com/aopoltorzhicky/go_kraken/rest v0.0.3/go.mod h1:cen8hPWBicFQ1T4EoseSAkxvCD7zzZYIzxb0OVTHDk0= +github.com/aopoltorzhicky/go_kraken/websocket v0.0.10 h1:3oOS3BIiPXjtuAJ+Yzfi7WjwA6eA7jyu1ESJ3jvxbp8= +github.com/aopoltorzhicky/go_kraken/websocket v0.0.10/go.mod h1:keiaKS3AsahdTS/QD2b220DGzWV/t4tWIjmy+4EruHI= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +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/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.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/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +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/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= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -20,58 +74,260 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +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/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +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/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +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-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +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/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +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_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.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +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/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +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= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tdex-network/tdex-protobuf v0.0.0-20201029153650-f5164a4b6a77 h1:njOphJsTU5qCJROyzw4ReARDbucTCbfj5t7Mgnnt9u0= github.com/tdex-network/tdex-protobuf v0.0.0-20201029153650-f5164a4b6a77/go.mod h1:tVWv01BSMH/neJOsixdDFU5T0ll0OZbPliLXBsYQjA8= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +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= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 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-20181023162649-9b4f9f5ad519/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-20181201002055-351d144fa1fc/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-20190108225652-1e06a53dbb7e/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 h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/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-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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 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/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= @@ -86,5 +342,25 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +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= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/internal/adapters/config_service.go b/internal/adapters/config_service.go new file mode 100644 index 0000000..c78201a --- /dev/null +++ b/internal/adapters/config_service.go @@ -0,0 +1,126 @@ +package adapters + +import ( + "encoding/json" + "regexp" + "time" + + "github.com/tdex-network/tdex-feeder/internal/application" + "github.com/tdex-network/tdex-feeder/internal/domain" +) + +type MarketJson struct { + BaseAsset string `json:"base_asset"` + QuoteAsset string `json:"quote_asset"` + KrakenTicker string `json:"kraken_ticker"` + Interval int `json:"interval"` +} + +type ConfigJson struct { + DaemonEndpoint string `json:"daemon_endpoint"` + KrakenWsEndpoint string `json:"kraken_ws_endpoint"` + Markets []MarketJson `json:"markets"` +} + +type Config struct { + daemonEndpoint string + krakenWSaddress string + markets map[string]domain.Market + marketIntervals map[domain.Market]time.Duration +} + +func (config *Config) ToFeederService() application.FeederService { + feederSvc := application.NewFeederService(application.NewFeederServiceArgs{ + KrakenWSaddress: config.krakenWSaddress, + OperatorEndpoint: config.daemonEndpoint, + TickerToMarket: config.markets, + MarketToInterval: config.marketIntervals, + }) + + return feederSvc +} + +func (config *Config) UnmarshalJSON(data []byte) error { + jsonConfig := &ConfigJson{} + err := json.Unmarshal(data, jsonConfig) + if err != nil { + return err + } + + err = jsonConfig.validate() + if err != nil { + return err + } + + config.daemonEndpoint = jsonConfig.DaemonEndpoint + config.krakenWSaddress = jsonConfig.KrakenWsEndpoint + + configTickerToMarketMap := make(map[string]domain.Market) + marketIntervalsMap := make(map[domain.Market]time.Duration) + + for _, marketJson := range jsonConfig.Markets { + market := domain.Market{ + BaseAsset: marketJson.BaseAsset, + QuoteAsset: marketJson.QuoteAsset, + } + + configTickerToMarketMap[marketJson.KrakenTicker] = market + marketIntervalsMap[market] = time.Duration(marketJson.Interval) * time.Millisecond + } + + config.markets = configTickerToMarketMap + config.marketIntervals = marketIntervalsMap + + return nil +} + +func (configJson ConfigJson) validate() error { + if configJson.DaemonEndpoint == "" { + return ErrDaemonEndpointIsEmpty + } + + if configJson.KrakenWsEndpoint == "" { + return ErrKrakenEndpointIsEmpty + } + + if len(configJson.Markets) == 0 { + return ErrNeedAtLeastOneMarketToFeed + } + + for _, marketJson := range configJson.Markets { + if marketJson.KrakenTicker == "" { + return ErrKrakenTickerIsEmpty + } + + err := validateAssetString(marketJson.BaseAsset) + if err != nil { + return err + } + + err = validateAssetString(marketJson.QuoteAsset) + if err != nil { + return err + } + + if marketJson.Interval < 0 { + return ErrIntervalIsNotPositiveNumber + } + } + + return nil +} + +func validateAssetString(asset string) error { + const regularExpression = `[0-9A-Fa-f]{64}` + + matched, err := regexp.Match(regularExpression, []byte(asset)) + if err != nil { + return err + } + + if !matched { + return ErrInvalidAssetHash{asset: asset} + } + + return nil +} diff --git a/internal/adapters/errors.go b/internal/adapters/errors.go new file mode 100644 index 0000000..ef9bfa6 --- /dev/null +++ b/internal/adapters/errors.go @@ -0,0 +1,19 @@ +package adapters + +import "errors" + +var ( + ErrDaemonEndpointIsEmpty = errors.New("daemon endpoint is empty") + ErrKrakenEndpointIsEmpty = errors.New("kraken websocket endpoint is empty") + ErrNeedAtLeastOneMarketToFeed = errors.New("need at least 1 market to feed") + ErrKrakenTickerIsEmpty = errors.New("krakenTicker should not be an empty string") + ErrIntervalIsNotPositiveNumber = errors.New("interval must be greater (or equal) than 0") +) + +type ErrInvalidAssetHash struct { + asset string +} + +func (e ErrInvalidAssetHash) Error() string { + return "the string '" + e.asset + "' is an invalid asset string." +} diff --git a/internal/application/feed_service.go b/internal/application/feed_service.go new file mode 100644 index 0000000..ac739b2 --- /dev/null +++ b/internal/application/feed_service.go @@ -0,0 +1,96 @@ +package application + +import ( + log "github.com/sirupsen/logrus" + + "github.com/tdex-network/tdex-feeder/internal/domain" + "github.com/tdex-network/tdex-feeder/internal/ports" +) + +type FeedService interface { + Start() + Stop() + GetFeed() domain.Feed +} + +type krakenFeedService struct { + feed domain.Feed + krakenWebSocket ports.KrakenWebSocket + stopChan chan bool + tickersToMarketMap map[string]domain.Market +} + +func NewKrakenFeedService( + address string, + tickersToMarketMap map[string]domain.Market, +) (FeedService, error) { + newFeed := domain.NewFeed() + + tickersToSubscribe := make([]string, 0) + for k := range tickersToMarketMap { + tickersToSubscribe = append(tickersToSubscribe, k) + } + + krakenSocket := ports.NewKrakenWebSocket() + err := krakenSocket.Connect(address, tickersToSubscribe) + if err != nil { + return nil, err + } + + return &krakenFeedService{ + krakenWebSocket: krakenSocket, + feed: newFeed, + stopChan: make(chan bool), + tickersToMarketMap: tickersToMarketMap, + }, nil +} + +// Start is the main function of krakenFeedService +// when start, the services is listening for new data from kraken server +func (f *krakenFeedService) Start() { + listening := true + log.Info("Kraken web socket feed is listening") + tickerWithPriceChan, err := f.krakenWebSocket.StartListen() + if err != nil { + log.Fatal(err) + } + for listening { + select { + case <-f.stopChan: + listening = false + err := f.krakenWebSocket.Close() + if err != nil { + log.Fatal(err) + } + + log.Info("Kraken web socket feed stopped") + break + case tickerWithPrice := <-tickerWithPriceChan: + log.Debug("Kraken web socket receive message = " + string(tickerWithPrice.Ticker)) + + market, ok := f.tickersToMarketMap[tickerWithPrice.Ticker] + if !ok { + log.Debug("Market not found for ticker: ", tickerWithPrice.Ticker) + continue + } + + f.feed.AddMarketPrice(domain.MarketPrice{ + Market: market, + Price: domain.Price{ + BasePrice: 1 / float32(tickerWithPrice.Price), + QuotePrice: float32(tickerWithPrice.Price), + }, + }) + } + } +} + +// Stop just send data to the stopChan in order to stop listening from kraken web socket +func (f *krakenFeedService) Stop() { + f.stopChan <- true +} + +// GetFeed is a getter function for kraken's feed member +func (f *krakenFeedService) GetFeed() domain.Feed { + return f.feed +} diff --git a/internal/application/feed_service_test.go b/internal/application/feed_service_test.go new file mode 100644 index 0000000..5f1227e --- /dev/null +++ b/internal/application/feed_service_test.go @@ -0,0 +1,46 @@ +package application + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/tdex-network/tdex-feeder/internal/domain" +) + +const ( + baseAsset = "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225" + quoteAsset = "a64b14f3de72bc602d0786e6f034720a879a6b9339d59b09ddd49e1783ed227a" + krakenTicker = "XBT/USDT" + krakenWsEndpoint = "ws.kraken.com" +) + +func TestKrakenFeedService(t *testing.T) { + tickerMap := make(map[string]domain.Market) + tickerMap[krakenTicker] = domain.Market{ + BaseAsset: baseAsset, + QuoteAsset: quoteAsset, + } + + svc, err := NewKrakenFeedService(krakenWsEndpoint, tickerMap) + if err != nil { + t.Error(err) + } + go svc.Start() + defer svc.Stop() + + feed := svc.GetFeed() + target := &mockTarget{marketPrices: []domain.MarketPrice{}} + feeder := NewTdexFeeder([]domain.Feed{feed}, []domain.Target{target}) + go func() { + err := feeder.Start() + if err != nil { + t.Error(err) + } + }() + + time.Sleep(10 * time.Second) + feeder.Stop() + + assert.Equal(t, true, len(target.marketPrices) > 0) +} diff --git a/internal/application/feeder_service.go b/internal/application/feeder_service.go new file mode 100644 index 0000000..2d149c9 --- /dev/null +++ b/internal/application/feeder_service.go @@ -0,0 +1,59 @@ +package application + +import ( + "time" + + log "github.com/sirupsen/logrus" + "github.com/tdex-network/tdex-feeder/internal/domain" +) + +type FeederService interface { + Start() error + Stop() error +} + +type feederService struct { + tdexFeeder TdexFeeder + krakenService FeedService + target *TdexDaemonTarget +} + +type NewFeederServiceArgs struct { + OperatorEndpoint string + MarketToInterval map[domain.Market]time.Duration + KrakenWSaddress string + TickerToMarket map[string]domain.Market +} + +func NewFeederService(args NewFeederServiceArgs) FeederService { + target := NewTdexDaemonTarget(args.OperatorEndpoint, args.MarketToInterval) + + krakenFeedService, err := NewKrakenFeedService(args.KrakenWSaddress, args.TickerToMarket) + if err != nil { + log.Fatal(err) + } + + feeder := NewTdexFeeder( + []domain.Feed{krakenFeedService.GetFeed()}, + []domain.Target{target}, + ) + + return &feederService{ + tdexFeeder: feeder, + krakenService: krakenFeedService, + target: target.(*TdexDaemonTarget), + } +} + +func (feeder *feederService) Start() error { + go feeder.krakenService.Start() + err := feeder.tdexFeeder.Start() + return err +} + +func (feeder *feederService) Stop() error { + feeder.krakenService.Stop() + feeder.target.Stop() + feeder.tdexFeeder.Stop() + return nil +} diff --git a/internal/application/tdex_feeder.go b/internal/application/tdex_feeder.go new file mode 100644 index 0000000..6594ad5 --- /dev/null +++ b/internal/application/tdex_feeder.go @@ -0,0 +1,94 @@ +package application + +import ( + "errors" + "sync" + + log "github.com/sirupsen/logrus" + "github.com/tdex-network/tdex-feeder/internal/domain" +) + +type TdexFeeder interface { + Start() error + Stop() + IsRunning() bool +} + +type tdexFeeder struct { + feeds []domain.Feed + targets []domain.Target + stopChan chan bool + running bool + locker sync.Locker +} + +func NewTdexFeeder(feeds []domain.Feed, targets []domain.Target) TdexFeeder { + return &tdexFeeder{ + feeds: feeds, + targets: targets, + stopChan: make(chan bool), + running: false, + locker: &sync.Mutex{}, + } +} + +// Start observe all the feeds chan (using merge function) +// and push the results to all targets +func (t *tdexFeeder) Start() error { + if t.IsRunning() { + return errors.New("the feeder is already started") + } + + t.running = true + marketPriceChannel := merge(t.feeds...) + + for t.IsRunning() { + select { + case <-t.stopChan: + t.running = false + break + case marketPrice := <-marketPriceChannel: + log.Info("Market ", marketPrice.Market.BaseAsset[:4], "-", marketPrice.Market.QuoteAsset[:4], " | Base Price ", marketPrice.Price.BasePrice, " | Quote Price ", marketPrice.Price.QuotePrice) + for index, target := range t.targets { + target.Push(marketPrice) + log.Debug("Pushed to target ", index) + } + } + } + + return nil +} + +func (t *tdexFeeder) Stop() { + t.stopChan <- true +} + +func (t *tdexFeeder) IsRunning() bool { + t.locker.Lock() + defer t.locker.Unlock() + return t.running +} + +// merge gathers several feeds into a unique channel +func merge(feeds ...domain.Feed) <-chan domain.MarketPrice { + mergedChan := make(chan domain.MarketPrice) + var wg sync.WaitGroup + + wg.Add(len(feeds)) + for _, feed := range feeds { + c := feed.GetMarketPriceChan() + go func(c <-chan domain.MarketPrice) { + for marketPrice := range c { + mergedChan <- marketPrice + } + wg.Done() + }(c) + } + + go func() { + wg.Wait() + close(mergedChan) + }() + + return mergedChan +} diff --git a/internal/application/tdex_feeder_test.go b/internal/application/tdex_feeder_test.go new file mode 100644 index 0000000..49fb4b2 --- /dev/null +++ b/internal/application/tdex_feeder_test.go @@ -0,0 +1,59 @@ +package application + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/tdex-network/tdex-feeder/internal/domain" +) + +func TestFeeder(t *testing.T) { + feed := domain.NewFeed() + feedBis := domain.NewFeed() + + target := &mockTarget{ + marketPrices: make([]domain.MarketPrice, 0), + } + + feeder := NewTdexFeeder( + []domain.Feed{feed, feedBis}, + []domain.Target{target}, + ) + + marketPrice := domain.MarketPrice{ + Market: domain.Market{ + BaseAsset: "1111", + QuoteAsset: "0000", + }, + Price: domain.Price{ + BasePrice: 0.2, + QuotePrice: 1, + }, + } + + go func() { + err := feeder.Start() + if err != nil { + t.Error(err) + } + }() + + time.Sleep(time.Second) + assert.Equal(t, true, feeder.IsRunning()) + + go func() { + for i := 0; i < 5; i++ { + feedBis.AddMarketPrice(marketPrice) + } + }() + + for i := 0; i < 10; i++ { + feed.AddMarketPrice(marketPrice) + } + + time.Sleep(500 * time.Millisecond) + feeder.Stop() + + assert.Equal(t, 15, len(target.marketPrices)) +} diff --git a/internal/application/test_utils.go b/internal/application/test_utils.go new file mode 100644 index 0000000..3245c13 --- /dev/null +++ b/internal/application/test_utils.go @@ -0,0 +1,11 @@ +package application + +import "github.com/tdex-network/tdex-feeder/internal/domain" + +type mockTarget struct { + marketPrices []domain.MarketPrice +} + +func (t *mockTarget) Push(marketPrice domain.MarketPrice) { + t.marketPrices = append(t.marketPrices, marketPrice) +} diff --git a/internal/application/updater_service.go b/internal/application/updater_service.go new file mode 100644 index 0000000..5aa6a78 --- /dev/null +++ b/internal/application/updater_service.go @@ -0,0 +1,88 @@ +package application + +import ( + "context" + "sync" + "time" + + log "github.com/sirupsen/logrus" + "github.com/tdex-network/tdex-feeder/internal/domain" + "github.com/tdex-network/tdex-feeder/internal/ports" +) + +// Implements the domain.Target interface and manage interval for each market +type TdexDaemonTarget struct { + Endpoint string + priceUpdater ports.TdexDaemonPriceUpdater + priceUpdaterLocker sync.Locker + marketsToUpdate map[domain.Market]domain.Price + closeChan chan bool +} + +// NewTdexDaemonTarget configure a tdexDaemonUpdater using the endpoint +// and start goroutines depending of the configured intervals for each market. +func NewTdexDaemonTarget( + tdexDaemonOperatorInterfaceEnpoint string, + marketToIntervalMap map[domain.Market]time.Duration, +) domain.Target { + now := time.Now() + mapLastSent := make(map[domain.Market]time.Time) + + for market := range marketToIntervalMap { + mapLastSent[market] = now + } + + tdexTarget := &TdexDaemonTarget{ + Endpoint: tdexDaemonOperatorInterfaceEnpoint, + priceUpdater: ports.NewTdexDaemonPriceUpdater(context.Background(), tdexDaemonOperatorInterfaceEnpoint), + priceUpdaterLocker: &sync.Mutex{}, + closeChan: make(chan bool, 1), + marketsToUpdate: make(map[domain.Market]domain.Price), + } + + for market, interval := range marketToIntervalMap { + go func(duration time.Duration, market domain.Market) { + for { + select { + case <-tdexTarget.closeChan: + log.Info("Stop the tdex updater") + break + case <-time.After(duration): + tdexTarget.updatePrice(market) + continue + } + } + }(interval, market) + } + + return tdexTarget +} + +// Push is a method of the Target interface +// The tdexDaemonTarget stores the marketPrice in a local cache. +func (daemon *TdexDaemonTarget) Push(marketPrice domain.MarketPrice) { + daemon.priceUpdaterLocker.Lock() + defer daemon.priceUpdaterLocker.Unlock() + + daemon.marketsToUpdate[marketPrice.Market] = marketPrice.Price +} + +// Stop is used to stop all the goroutines launched in NewTdexDaemonTarget +func (daemon *TdexDaemonTarget) Stop() { + daemon.closeChan <- true +} + +func (daemon *TdexDaemonTarget) updatePrice(market domain.Market) { + daemon.priceUpdaterLocker.Lock() + defer daemon.priceUpdaterLocker.Unlock() + + price, ok := daemon.marketsToUpdate[market] + if ok { + err := daemon.priceUpdater.UpdateMarketPrice(context.Background(), domain.MarketPrice{Market: market, Price: price}) + if err != nil { + log.Error("error updatePrice: ", err) + return + } + delete(daemon.marketsToUpdate, market) + } +} diff --git a/internal/domain/feed.go b/internal/domain/feed.go new file mode 100644 index 0000000..801c68d --- /dev/null +++ b/internal/domain/feed.go @@ -0,0 +1,32 @@ +package domain + +type MarketPrice struct { + Market Market + Price Price +} + +// Feed represents a source of MarketPrice data +type Feed interface { + AddMarketPrice(marketPrice MarketPrice) + GetMarketPriceChan() <-chan MarketPrice +} + +type feed struct { + marketPriceChan chan MarketPrice +} + +// NewFeed creates a Feed (i.e an empty channel) +func NewFeed() Feed { + return &feed{ + marketPriceChan: make(chan MarketPrice), + } +} + +// AddMarketPrice send a new marketPrice value inside the Feed's channel. +func (f feed) AddMarketPrice(marketPrice MarketPrice) { + f.marketPriceChan <- marketPrice +} + +func (f feed) GetMarketPriceChan() <-chan MarketPrice { + return f.marketPriceChan +} diff --git a/internal/domain/market.go b/internal/domain/market.go new file mode 100644 index 0000000..c954a50 --- /dev/null +++ b/internal/domain/market.go @@ -0,0 +1,6 @@ +package domain + +type Market struct { + BaseAsset string + QuoteAsset string +} diff --git a/internal/domain/price.go b/internal/domain/price.go new file mode 100644 index 0000000..c017327 --- /dev/null +++ b/internal/domain/price.go @@ -0,0 +1,6 @@ +package domain + +type Price struct { + BasePrice float32 + QuotePrice float32 +} diff --git a/internal/domain/target.go b/internal/domain/target.go new file mode 100644 index 0000000..a93beeb --- /dev/null +++ b/internal/domain/target.go @@ -0,0 +1,5 @@ +package domain + +type Target interface { + Push(marketPrice MarketPrice) +} diff --git a/internal/ports/kraken_websocket.go b/internal/ports/kraken_websocket.go new file mode 100644 index 0000000..1e377a1 --- /dev/null +++ b/internal/ports/kraken_websocket.go @@ -0,0 +1,80 @@ +package ports + +import ( + "errors" + + ws "github.com/aopoltorzhicky/go_kraken/websocket" + log "github.com/sirupsen/logrus" +) + +type KrakenWebSocket interface { + Connect(address string, tickersToSubscribe []string) error + StartListen() (chan TickerWithPrice, error) + Close() error +} + +type krakenWebSocket struct { + krakenWS *ws.Client + tickerWithPriceChan chan TickerWithPrice +} + +func NewKrakenWebSocket() KrakenWebSocket { + return &krakenWebSocket{ + krakenWS: ws.New(), + tickerWithPriceChan: make(chan TickerWithPrice), + } +} + +// Connect method will connect to the websocket kraken server, ping it and subscribe to tickers threads. +func (socket *krakenWebSocket) Connect(address string, tickersToSubscribe []string) error { + // connect to server + err := socket.krakenWS.Connect() + if err != nil { + return err + } + // test if the server is alive + err = socket.krakenWS.Ping() + if err != nil { + return err + } + + // subscribe to tickers + err = socket.krakenWS.SubscribeTicker(tickersToSubscribe) + if err != nil { + return err + } + + return nil +} + +func (socket *krakenWebSocket) StartListen() (chan TickerWithPrice, error) { + if socket.krakenWS == nil { + return nil, errors.New("Socket not connected") + } + + go func() { + for obj := range socket.krakenWS.Listen() { + switch obj := obj.(type) { + case error: + log.Debug("Channel closed: ", obj) + case ws.DataUpdate: + tickerUpdate, ok := obj.Data.(ws.TickerUpdate) + if ok { + result := TickerWithPrice{ + Ticker: tickerUpdate.Pair, + Price: tickerUpdate.Close.Today.(float64), + } + socket.tickerWithPriceChan <- result + } + } + } + }() + + return socket.tickerWithPriceChan, nil +} + +func (socket *krakenWebSocket) Close() error { + socket.krakenWS.Close() + socket.krakenWS = nil + return nil +} diff --git a/internal/ports/kraken_websocket_test.go b/internal/ports/kraken_websocket_test.go new file mode 100644 index 0000000..a780b4c --- /dev/null +++ b/internal/ports/kraken_websocket_test.go @@ -0,0 +1,36 @@ +package ports + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + address = "ws.kraken.com" + ticker = "XBT/USDT" +) + +func createAndConnect() (KrakenWebSocket, error) { + krakenWS := NewKrakenWebSocket() + err := krakenWS.Connect(address, []string{ticker}) + return krakenWS, err +} + +func TestConnectToKrakenWebSocket(t *testing.T) { + _, err := createAndConnect() + assert.Nil(t, err) +} + +func TestListen(t *testing.T) { + ws, err := createAndConnect() + if err != nil { + t.Error(err) + } + + tickerWithPriceChan, err := ws.StartListen() + assert.Nil(t, err) + + nextTickerWithPrice := <-tickerWithPriceChan + assert.NotNil(t, nextTickerWithPrice) +} diff --git a/internal/ports/tdex_daemon_price_updater.go b/internal/ports/tdex_daemon_price_updater.go new file mode 100644 index 0000000..87bbb45 --- /dev/null +++ b/internal/ports/tdex_daemon_price_updater.go @@ -0,0 +1,74 @@ +package ports + +import ( + "context" + "errors" + + log "github.com/sirupsen/logrus" + + "github.com/tdex-network/tdex-feeder/internal/domain" + pboperator "github.com/tdex-network/tdex-protobuf/generated/go/operator" + "github.com/tdex-network/tdex-protobuf/generated/go/types" + "google.golang.org/grpc" +) + +type TdexDaemonPriceUpdater interface { + UpdateMarketPrice(ctx context.Context, marketPrice domain.MarketPrice) error +} + +// NewTdexDaemonPriceUpdater uses the operatorInterfaceEndpoint to create a gRPC client. +func NewTdexDaemonPriceUpdater(ctx context.Context, operatorInterfaceEndpoint string) TdexDaemonPriceUpdater { + connGrpc, err := connectToGRPC(ctx, operatorInterfaceEndpoint) + if err != nil { + log.Fatal(err) + } + + operatorClient := pboperator.NewOperatorClient(connGrpc) + + return &tdexDaemonPriceUpdater{ + clientGRPC: operatorClient, + } +} + +type tdexDaemonPriceUpdater struct { + clientGRPC pboperator.OperatorClient +} + +// UpdateMarketPrice gets a marketPrice and sends updateMarketPrice request through gRPC client. +func (updater *tdexDaemonPriceUpdater) UpdateMarketPrice(ctx context.Context, marketPrice domain.MarketPrice) error { + if marketPrice.Price.BasePrice == 0.00 { + return errors.New("Base price is 0.00") + } + + if marketPrice.Price.BasePrice == 0.00 { + return errors.New("Quote price is 0.00") + } + + args := pboperator.UpdateMarketPriceRequest{ + Market: &types.Market{ + BaseAsset: marketPrice.Market.BaseAsset, + QuoteAsset: marketPrice.Market.QuoteAsset, + }, + Price: &types.Price{ + BasePrice: marketPrice.Price.BasePrice, + QuotePrice: marketPrice.Price.QuotePrice, + }, + } + + _, err := updater.clientGRPC.UpdateMarketPrice(ctx, &args) + if err != nil { + return err + } + + return nil +} + +// ConnectTogRPC dials and returns a new client connection to a remote host +func connectToGRPC(ctx context.Context, daemonEndpoint string) (*grpc.ClientConn, error) { + conn, err := grpc.DialContext(ctx, daemonEndpoint, grpc.WithInsecure(), grpc.WithBlock()) + if err != nil { + return conn, err + } + log.Println("Connected to gRPC:", daemonEndpoint) + return conn, nil +} diff --git a/internal/ports/types.go b/internal/ports/types.go new file mode 100644 index 0000000..fc4119d --- /dev/null +++ b/internal/ports/types.go @@ -0,0 +1,6 @@ +package ports + +type TickerWithPrice struct { + Ticker string + Price float64 +} diff --git a/pkg/conn/grpc.go b/pkg/conn/grpc.go deleted file mode 100644 index aac8028..0000000 --- a/pkg/conn/grpc.go +++ /dev/null @@ -1,59 +0,0 @@ -package conn - -import ( - "context" - "time" - - log "github.com/sirupsen/logrus" - - "github.com/tdex-network/tdex-feeder/pkg/marketinfo" - pboperator "github.com/tdex-network/tdex-protobuf/generated/go/operator" - pbtypes "github.com/tdex-network/tdex-protobuf/generated/go/types" - "google.golang.org/grpc" -) - -const ( - timeout = 3 -) - -// ConnectTogRPC dials and returns a new client connection to a remote host -func ConnectTogRPC(daemonEndpoint string) (*grpc.ClientConn, error) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*timeout) - defer cancel() - conn, err := grpc.DialContext(ctx, daemonEndpoint, grpc.WithInsecure(), grpc.WithBlock()) - if err != nil { - return conn, err - } - log.Println("Connected to gRPC:", daemonEndpoint) - return conn, nil -} - -// UpdateMarketPricegRPC calls the tdex daemon UpdateMarketPrice rpc endpoint to update a defined market -func UpdateMarketPricegRPC(marketInfo marketinfo.MarketInfo, clientgRPC pboperator.OperatorClient) { - - if marketInfo.Price == 0.00 { - log.Println("Can't send gRPC request with no price") - return - } - - log.Printf("%s %g for market %s-%s", marketInfo.Config.KrakenTicker, marketInfo.Price, marketInfo.Config.BaseAsset[:4], marketInfo.Config.QuoteAsset[:4]) - - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - - _, err := clientgRPC.UpdateMarketPrice(ctx, &pboperator.UpdateMarketPriceRequest{ - Market: &pbtypes.Market{ - BaseAsset: marketInfo.Config.BaseAsset, - QuoteAsset: marketInfo.Config.QuoteAsset, - }, - Price: &pbtypes.Price{ - BasePrice: 1 / float32(marketInfo.Price), - QuotePrice: float32(marketInfo.Price), - }, - }) - - if err != nil { - log.Println(err) - return - } -} diff --git a/pkg/conn/messages.go b/pkg/conn/messages.go deleted file mode 100644 index 308701b..0000000 --- a/pkg/conn/messages.go +++ /dev/null @@ -1,115 +0,0 @@ -package conn - -import ( - "encoding/json" - "strconv" - "time" - - "github.com/gorilla/websocket" - log "github.com/sirupsen/logrus" - - "github.com/tdex-network/tdex-feeder/pkg/marketinfo" - "github.com/tdex-network/tdex-protobuf/generated/go/operator" -) - -const ( - nanoToSeconds = 1000000000 -) - -// RequestMessage is the data structure used to create -// jsons in order subscribe to market updates on Kraken -type RequestMessage struct { - Event string `json:"event"` - Pair []string `json:"pair,omitempty"` - Subscription *Subscription `json:"subscription,omitempty"` - Reqid int `json:"reqid,omitempty"` -} - -type Subscription struct { - Name string `json:"name"` - Interval int `json:"interval,omitempty"` - Token string `json:"token,omitempty"` - Depth int `json:"depth,omitempty"` - Snapshop bool `json:"snapshot,omitempty"` -} - -// CreatePingMessage returns a RequestMessage struct -// with a ping Event. -func CreatePingMessage() RequestMessage { - return RequestMessage{Event: "ping"} -} - -// CreateSubscribeToMarketMessage gets a string with a market pair and returns -// a RequestMessage struct with instructions to subscrive to that market pair ticker. -func CreateSubscribeToMarketMessage(marketpair string) RequestMessage { - s := Subscription{Name: "ticker"} - return RequestMessage{"subscribe", []string{marketpair}, &s, 0} -} - -// SendRequestMessage gets a socket connection and a RequestMessage struct, -// marshalls the struct and sends the message using the socket. -func SendRequestMessage(c *websocket.Conn, m RequestMessage) error { - b, err := json.Marshal(m) - if err != nil { - return err - } - err = c.WriteMessage(websocket.TextMessage, []byte(b)) - if err != nil { - return err - } - return nil -} - -// HandleMessages is responsible for the perpetual loop of receiving messages -// from subscriptions, retrieving the price from them and send the gRPC request -// to update the market price in the predeterminated interval. -func HandleMessages(done chan string, cSocket *websocket.Conn, marketsInfos []marketinfo.MarketInfo, clientgRPC operator.OperatorClient) { - defer close(done) - for { - _, message, err := cSocket.ReadMessage() - if err != nil { - log.Debug("Message Error:", err) - return - } - log.Debug(string(message)) - marketsInfos = retrievePriceFromMessage(message, marketsInfos) - marketsInfos = checkInterval(marketsInfos, clientgRPC) - } -} - -// checkInterval handles the gRPC calls for UpdateMarketPrice -// at a predeterminated inteval for each market. -func checkInterval(marketsInfos []marketinfo.MarketInfo, clientgRPC operator.OperatorClient) []marketinfo.MarketInfo { - for i, marketInfo := range marketsInfos { - elapsedSeconds := time.Since(marketInfo.LastSent).Round(time.Second) - marketInterval := time.Duration(marketInfo.Config.Interval * int(nanoToSeconds)) - if elapsedSeconds == marketInterval { - UpdateMarketPricegRPC(marketInfo, clientgRPC) - marketInfo.LastSent = time.Now() - marketsInfos[i] = marketInfo - } - } - return marketsInfos -} - -// retrievePriceFromMessage gets a message from a subscription and retrieves the -// price information, updating the price of the specific market. -func retrievePriceFromMessage(message []byte, marketsInfos []marketinfo.MarketInfo) []marketinfo.MarketInfo { - var result []interface{} - err := json.Unmarshal([]byte(message), &result) - if err != nil { - return marketsInfos - } - if len(result) == 4 { - pricesJson := result[1].(map[string]interface{}) - priceAsk := pricesJson["c"].([]interface{}) - price, _ := strconv.ParseFloat(priceAsk[0].(string), 64) - for i, marketInfo := range marketsInfos { - if marketInfo.Config.KrakenTicker == result[3] { - marketInfo.Price = price - marketsInfos[i] = marketInfo - } - } - } - return marketsInfos -} diff --git a/pkg/conn/socket.go b/pkg/conn/socket.go deleted file mode 100644 index 54151f5..0000000 --- a/pkg/conn/socket.go +++ /dev/null @@ -1,21 +0,0 @@ -package conn - -import ( - "net/url" - - log "github.com/sirupsen/logrus" - - "github.com/gorilla/websocket" -) - -// ConnectToSocket dials and returns a new client connection to a remote host -func ConnectToSocket(address string) (*websocket.Conn, error) { - u := url.URL{Scheme: "wss", Host: address, Path: "/"} - - c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) - if err != nil { - return c, err - } - log.Println("Connected to socket:", u.String()) - return c, nil -} diff --git a/pkg/marketinfo/marketinfo.go b/pkg/marketinfo/marketinfo.go deleted file mode 100644 index 52086b4..0000000 --- a/pkg/marketinfo/marketinfo.go +++ /dev/null @@ -1,25 +0,0 @@ -package marketinfo - -import ( - "time" - - "github.com/tdex-network/tdex-feeder/config" -) - -// MarketInfo stores the informations necessary for -// handling different market pair prices in real-time. -type MarketInfo struct { - Config config.Market - LastSent time.Time - Price float64 -} - -// InitialMarketInfo returns a pointer to a MarketInfo struct -// with the default configurations. -func InitialMarketInfo(market config.Market) MarketInfo { - return MarketInfo{ - Config: market, - LastSent: time.Now(), - Price: 0, - } -} diff --git a/scripts/test b/scripts/test deleted file mode 100755 index 3002ae2..0000000 --- a/scripts/test +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -set -e - -PARENT_PATH=$(dirname $( - cd $(dirname $0) - pwd -P -)) - -pushd $PARENT_PATH -go test -v -count=1 -race ./... -popd diff --git a/tdexfeeder.png b/tdexfeeder.png new file mode 100644 index 0000000..f77934e Binary files /dev/null and b/tdexfeeder.png differ