From 2f91f20a8645339385ada602684f4957f20f4da4 Mon Sep 17 00:00:00 2001 From: jloup Date: Thu, 22 Feb 2018 11:31:59 +0100 Subject: [PATCH] Poloniex connection. --- Gopkg.lock | 22 +- Gopkg.toml | 8 + api/const.go | 9 +- api/const_string.go | 4 +- api/external_services.go | 40 ++ api/market_config.go | 76 ++ api/markets.go | 29 + api/routes.go | 11 + cmd/web/Makefile | 19 +- cmd/web/js/api.js | 11 + cmd/web/js/main.jsx | 41 +- cmd/web/js/otp.jsx | 6 +- cmd/web/js/poloniex.jsx | 176 ++++- cmd/web/js/signin.jsx | 4 +- cmd/web/js/signup.jsx | 4 +- cmd/web/static/cryptocoins.css | 1150 ++++++++++++++++++++++++++++++ cmd/web/static/cryptocoins.ttf | Bin 0 -> 96448 bytes cmd/web/static/cryptocoins.woff | Bin 0 -> 58768 bytes cmd/web/static/cryptocoins.woff2 | Bin 0 -> 49976 bytes cmd/web/static/index.html | 1 + cmd/web/static/style.css | 15 +- markets/poloniex.go | 177 +++++ 22 files changed, 1732 insertions(+), 71 deletions(-) create mode 100644 api/external_services.go create mode 100644 api/markets.go create mode 100644 cmd/web/static/cryptocoins.css create mode 100644 cmd/web/static/cryptocoins.ttf create mode 100644 cmd/web/static/cryptocoins.woff create mode 100644 cmd/web/static/cryptocoins.woff2 create mode 100644 markets/poloniex.go diff --git a/Gopkg.lock b/Gopkg.lock index 7f0f166..88c2eda 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -76,6 +76,12 @@ packages = ["."] revision = "1c35d901db3da928c72a72d8458480cc9ade058f" +[[projects]] + branch = "master" + name = "github.com/jloup/poloniex" + packages = ["."] + revision = "e75e6fd7991c1d71576ad97de73fc922f24a5fd2" + [[projects]] branch = "master" name = "github.com/jloup/utils" @@ -98,6 +104,12 @@ revision = "b7b89250c468c06871d3837bee02e2d5c155ae19" version = "v1.0.0" +[[projects]] + branch = "master" + name = "github.com/shopspring/decimal" + packages = ["."] + revision = "e3482495ff4cba75613e4177ed79825c890058a9" + [[projects]] name = "github.com/ugorji/go" packages = ["codec"] @@ -112,7 +124,13 @@ "blowfish", "ssh/terminal" ] - revision = "650f4a345ab4e5b245a3034b110ebc7299e68186" + revision = "432090b8f568c018896cd8a0fb0345872bbac6ce" + +[[projects]] + branch = "master" + name = "golang.org/x/net" + packages = ["websocket"] + revision = "cbe0f9307d0156177f9dd5dc85da1a31abc5f2fb" [[projects]] branch = "master" @@ -138,6 +156,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "c9af022a586632799c6259f6c48eef8dad7080e36b96e8cb5cb905b316c4cb9b" + inputs-digest = "d3c9b3094ed174bcf1631e3a998a75d557c65c195d7a8fd5ca9912f71f334ce1" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index f4686b5..4feee4d 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -56,3 +56,11 @@ [prune] go-tests = true unused-packages = true + +[[constraint]] + branch = "master" + name = "github.com/jloup/poloniex" + +[[constraint]] + branch = "master" + name = "github.com/shopspring/decimal" diff --git a/api/const.go b/api/const.go index 2edd6f4..1b22355 100644 --- a/api/const.go +++ b/api/const.go @@ -6,15 +6,19 @@ import "net/http" type Status uint32 type ErrorCode uint32 +const EXTERNAL_SERVICE_TIMEOUT_SECONDS = 10 + const ( OK Status = iota NOK BadRequest ErrorCode = iota + 1 EmailExists + ExternalServiceTimeout InternalError InvalidCredentials InvalidEmail + InvalidMarketCredentials InvalidOtp InvalidPassword NeedOtpValidation @@ -31,7 +35,7 @@ func StatusToHttpCode(status Status, code ErrorCode) int { } switch code { - case BadRequest, InvalidPassword, InvalidEmail: + case BadRequest, InvalidPassword, InvalidEmail, InvalidMarketCredentials: return http.StatusBadRequest case InvalidCredentials, InvalidOtp: @@ -45,6 +49,9 @@ func StatusToHttpCode(status Status, code ErrorCode) int { case NotFound: return http.StatusNotFound + + case ExternalServiceTimeout: + return http.StatusGatewayTimeout } return http.StatusInternalServerError diff --git a/api/const_string.go b/api/const_string.go index 611db40..e4b9e50 100644 --- a/api/const_string.go +++ b/api/const_string.go @@ -15,9 +15,9 @@ func (i Status) String() string { return _Status_name[_Status_index[i]:_Status_index[i+1]] } -const _ErrorCode_name = "BadRequestEmailExistsInternalErrorInvalidCredentialsInvalidEmailInvalidOtpInvalidPasswordNeedOtpValidationNotAuthorizedNotFoundOtpAlreadySetupOtpNotSetupUserNotConfirmed" +const _ErrorCode_name = "BadRequestEmailExistsExternalServiceTimeoutInternalErrorInvalidCredentialsInvalidEmailInvalidMarketCredentialsInvalidOtpInvalidPasswordNeedOtpValidationNotAuthorizedNotFoundOtpAlreadySetupOtpNotSetupUserNotConfirmed" -var _ErrorCode_index = [...]uint8{0, 10, 21, 34, 52, 64, 74, 89, 106, 119, 127, 142, 153, 169} +var _ErrorCode_index = [...]uint8{0, 10, 21, 43, 56, 74, 86, 110, 120, 135, 152, 165, 173, 188, 199, 215} func (i ErrorCode) String() string { i -= 3 diff --git a/api/external_services.go b/api/external_services.go new file mode 100644 index 0000000..c467171 --- /dev/null +++ b/api/external_services.go @@ -0,0 +1,40 @@ +package api + +import ( + "context" + "fmt" + "time" +) + +// Use this to call external services. It will handle timeout and request cancellation gracefully. +func CallExternalService(tag string, timeout time.Duration, routine func() *Error) *Error { + routineDone := make(chan *Error) + + go func() { + routineDone <- routine() + }() + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + select { + case err := <-routineDone: + return err + case <-ctx.Done(): + return &Error{ExternalServiceTimeout, "external service timeout", fmt.Errorf("'%v' routine timeouted", tag)} + } +} + +var ErrorChan chan error + +func ErrorMonitoring() { + for { + err := <-ErrorChan + log.Errorf("error: %v", err) + } +} + +func init() { + ErrorChan = make(chan error) + go ErrorMonitoring() +} diff --git a/api/market_config.go b/api/market_config.go index 3fd10ae..d85af4d 100644 --- a/api/market_config.go +++ b/api/market_config.go @@ -2,7 +2,13 @@ package api import ( "fmt" + "strings" + "time" + "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/markets" + + "github.com/jloup/utils" + "github.com/shopspring/decimal" "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/db" ) @@ -42,6 +48,73 @@ func (q MarketConfigQuery) Run() (interface{}, *Error) { return config.Config, nil } +type MarketBalanceQuery struct { + In struct { + User db.User + Market string + Currency string + } +} + +func (q MarketBalanceQuery) ValidateParams() *Error { + if q.In.Market != "poloniex" { + return &Error{BadRequest, "invalid market name", fmt.Errorf("'%v' is not a valid market name", q.In.Market)} + } + + // TODO: we should request market for available currencies. + if q.In.Currency != "BTC" && q.In.Currency != "USDT" && q.In.Currency != "ETH" { + return &Error{BadRequest, "invalid currency, accept [BTC, USDT, ETH]", fmt.Errorf("'%v' is not a valid currency", q.In.Currency)} + } + + return nil +} + +func (q MarketBalanceQuery) Run() (interface{}, *Error) { + config, err := db.GetUserMarketConfig(q.In.User.Id, q.In.Market) + if err != nil { + return nil, NewInternalError(err) + } + + if config.Config["key"] == "" || config.Config["secret"] == "" { + return nil, &Error{BadRequest, "your credentials for this market are not setup", fmt.Errorf("'%v' credentials are not setup", q.In.Market)} + } + + result := struct { + Value decimal.Decimal `json:"value"` + ValueCurrency string `json:"valueCurrency"` + Balance map[string]decimal.Decimal `json:"balance"` + }{} + + resultErr := CallExternalService(fmt.Sprintf("'%s' GetBalanceValue", q.In.Market), EXTERNAL_SERVICE_TIMEOUT_SECONDS*time.Second, func() *Error { + balance, err := Poloniex.GetBalance(config.Config["key"], config.Config["secret"]) + + if utils.ErrIs(err, markets.InvalidCredentials) { + return &Error{InvalidMarketCredentials, "wrong market credentials", fmt.Errorf("wrong '%v' market credentials", q.In.Market)} + } + + if err != nil { + return NewInternalError(err) + } + + value, err := Poloniex.ComputeAccountBalanceValue(balance, q.In.Currency) + if err != nil { + return NewInternalError(err) + } + + result.Balance = balance + result.ValueCurrency = q.In.Currency + result.Value = value.Round(8) + + return nil + }) + + if resultErr != nil { + return nil, resultErr + } + + return &result, nil +} + type UpdateMarketConfigQuery struct { In struct { User db.User @@ -56,6 +129,9 @@ func (q UpdateMarketConfigQuery) ValidateParams() *Error { return &Error{BadRequest, "invalid market name", fmt.Errorf("'%v' is not a valid market name", q.In.Market)} } + q.In.Secret = strings.TrimSpace(q.In.Secret) + q.In.Key = strings.TrimSpace(q.In.Key) + return nil } diff --git a/api/markets.go b/api/markets.go new file mode 100644 index 0000000..60fb912 --- /dev/null +++ b/api/markets.go @@ -0,0 +1,29 @@ +package api + +import ( + "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/markets" +) + +var Poloniex *markets.Poloniex + +func OpenMarketsConnection() error { + for { + err := Poloniex.StartTicker() + if err != nil { + return err + } + log.Warn("connection to poloniex stream ended, restarting it...") + } +} + +func init() { + Poloniex = markets.NewPoloniex() + + // We open markets connections in the background as it can take time. + go func() { + err := OpenMarketsConnection() + if err != nil { + ErrorChan <- err + } + }() +} diff --git a/api/routes.go b/api/routes.go index d7e712c..cdf3dd9 100644 --- a/api/routes.go +++ b/api/routes.go @@ -41,6 +41,7 @@ var Groups = []Group{ []Route{ {"GET", []gin.HandlerFunc{GetMarketConfig}, "/:name"}, {"POST", []gin.HandlerFunc{UpdateMarketConfig}, "/:name/update"}, + {"GET", []gin.HandlerFunc{GetMarketBalance}, "/:name/balance"}, }, }, } @@ -111,6 +112,16 @@ func GetMarketConfig(c *gin.Context) { RunQuery(query, c) } +func GetMarketBalance(c *gin.Context) { + query := &MarketBalanceQuery{} + + query.In.User = GetUser(c) + query.In.Market = c.Param("name") + query.In.Currency = c.Query("currency") + + RunQuery(query, c) +} + func UpdateMarketConfig(c *gin.Context) { query := &UpdateMarketConfigQuery{} diff --git a/cmd/web/Makefile b/cmd/web/Makefile index 1d98085..2ebb734 100644 --- a/cmd/web/Makefile +++ b/cmd/web/Makefile @@ -6,6 +6,7 @@ SRC_DIR=js BUILD_DIR=build/js JSX_SRC= main.jsx signup.jsx signin.jsx otp.jsx poloniex.jsx JS_SRC= cookies.js app.js api.js +STATIC_FILES= index.html style.css cryptocoins.css cryptocoins.ttf cryptocoins.woff cryptocoins.woff2 JSX_OBJS=$(addprefix $(BUILD_DIR)/,$(JSX_SRC:.jsx=.js)) JS_OBJS=$(addprefix $(BUILD_DIR)/,$(JS_SRC)) STATIC_BUILD_DIR=build/static @@ -16,18 +17,18 @@ install: yarn --version yarn install -static: js $(STATIC_BUILD_DIR)/index.html $(STATIC_BUILD_DIR)/style.css +static: $(STATIC_BUILD_DIR) js $(addprefix $(STATIC_BUILD_DIR)/, $(STATIC_FILES)) js: build/static/main.js -$(STATIC_BUILD_DIR)/index.html: static/index.html - cp static/index.html $@ +$(STATIC_BUILD_DIR)/%: static/% + cp $< $@ -$(STATIC_BUILD_DIR)/style.css: static/style.css - cp static/style.css $@ +$(STATIC_BUILD_DIR): + mkdir -p $(BUILD_DIR) + mkdir -p $@ $(BUILD_DIR)/%.js: $(SRC_DIR)/%.jsx - mkdir -p $(@D) jscs --fix $< babel $< -o $@ jshint $@ @@ -42,11 +43,11 @@ build/static/main.js: $(JSX_OBJS) $(JS_OBJS) env/$(ENV).env -t [ debowerify ] \ $(BUILD_DIR)/main.js -o $@ -build/webapp.tar.gz: $(STATIC_BUILD_DIR)/main.js $(STATIC_BUILD_DIR)/index.html $(STATIC_BUILD_DIR)/style.css +build/webapp.tar.gz: $(addprefix $(STATIC_BUILD_DIR)/, $(STATIC_FILES)) build/static/main.js tar czf $@ --directory=$(dir $<) $(notdir $^) -release: build/webapp.tar.gz +release: $(STATIC_BUILD_DIR) build/webapp.tar.gz clean: rm -rf build - rm -rf node_modules \ No newline at end of file + rm -rf node_modules diff --git a/cmd/web/js/api.js b/cmd/web/js/api.js index e2acd1d..5c19fdf 100644 --- a/cmd/web/js/api.js +++ b/cmd/web/js/api.js @@ -53,6 +53,17 @@ var ApiEndpoints = { return '/market/' + params.name; } }, + 'MARKET_BALANCE': { + 'type': 'GET', + 'auth': true, + 'parameters': [ + {'name': 'name', 'mandatory': true, 'inquery': false}, + {'name': 'currency', 'mandatory': true, 'inquery': true}, + ], + 'buildUrl': function(params) { + return '/market/' + params.name + '/balance'; + } + }, 'UPDATE_MARKET': { 'type': 'POST', 'auth': true, diff --git a/cmd/web/js/main.jsx b/cmd/web/js/main.jsx index eb53057..e5e505d 100644 --- a/cmd/web/js/main.jsx +++ b/cmd/web/js/main.jsx @@ -1,15 +1,17 @@ -var SignupForm = require('./signup.js').SignupForm; -var SigninForm = require('./signin.js').SigninForm; -var OtpEnrollForm = require('./otp.js').OtpEnrollForm; -var PoloniexForm = require('./poloniex.js').PoloniexForm; -var App = require('./app.js'); -var Api = require('./api.js').Api; -var cookies = require('./cookies.js'); +var SignupForm = require('./signup.js').SignupForm; +var SigninForm = require('./signin.js').SigninForm; +var OtpEnrollForm = require('./otp.js').OtpEnrollForm; +var PoloniexController = require('./poloniex.js').PoloniexController; +var App = require('./app.js'); +var Api = require('./api.js').Api; +var cookies = require('./cookies.js'); var Logo = React.createClass({ render: function() { - return (